(function () { window.__mhLedgerEnhancementLoaded = false; if (typeof S === "undefined" || typeof E === "undefined" || typeof render !== "function") return; window.__mhLedgerEnhancementLoaded = true; if (!S.dashboard) S.dashboard = { year: "", section: "active" }; if (!S.collapsedGroups) S.collapsedGroups = {}; function bgToday() { var now = new Date(); return new Date(now.getFullYear(), now.getMonth(), now.getDate()); } function bgParseDate(value) { var text = String(value || "").trim(); if (!text) return null; var match = text.match(/(20\d{2})\D?(\d{1,2})\D?(\d{1,2})/); if (match) { var parsed = new Date(Number(match[1]), Number(match[2]) - 1, Number(match[3])); return isNaN(parsed.getTime()) ? null : parsed; } var fallback = new Date(text); if (isNaN(fallback.getTime())) return null; return new Date(fallback.getFullYear(), fallback.getMonth(), fallback.getDate()); } function bgYearFromText(value) { var match = String(value || "").trim().match(/(20\d{2})/); return match ? match[1] : ""; } function bgStartYear(row) { return bgYearFromText(row && row.sDate); } function bgEndYear(row) { return bgYearFromText(row && row.eDate); } function bgDisplayYear(row) { var start = bgStartYear(row); if (start) return start; var contractMatch = String((row && row.cDate) || "").trim().match(/(20\d{2})/); if (contractMatch) return contractMatch[1]; var nameMatch = String((row && row.name) || "").trim().match(/^(20\d{2})/); if (nameMatch) return nameMatch[1]; return bgEndYear(row) || "미지정"; } function bgCompletionYear(row) { return bgEndYear(row) || bgDisplayYear(row); } function bgDateOrYearStart(row) { var yearText = bgDisplayYear(row); return bgParseDate(row && row.sDate) || bgParseDate(row && row.cDate) || (/^20\d{2}$/.test(yearText) ? new Date(Number(yearText), 0, 1) : null); } function bgDateOrYearEnd(row) { var completionYear = bgCompletionYear(row); return bgParseDate(row && row.eDate) || (/^20\d{2}$/.test(completionYear) ? new Date(Number(completionYear), 11, 31) : null); } function bgYearCutoff(year) { var targetYear = Number(year || 0); if (!targetYear) return null; var today = bgToday(); if (targetYear < today.getFullYear()) return new Date(targetYear, 11, 31); if (targetYear === today.getFullYear()) return today; return null; } function bgYearStartDate(year) { var targetYear = Number(year || 0); return targetYear ? new Date(targetYear, 0, 1) : null; } function bgActiveInYear(row, year) { var cutoff = bgYearCutoff(year); var yearStart = bgYearStartDate(year); var startDate = bgDateOrYearStart(row); var endDate = bgDateOrYearEnd(row); if (!(cutoff && yearStart && startDate)) return false; if (startDate > cutoff) return false; if (endDate && endDate < yearStart) return false; return !(endDate && endDate <= cutoff); } function bgStartedInYear(row, year) { var cutoff = bgYearCutoff(year); var startDate = bgDateOrYearStart(row); if (!(cutoff && startDate)) return false; return startDate.getFullYear() === Number(year || 0) && startDate <= cutoff; } function bgCompletedInYear(row, year) { var cutoff = bgYearCutoff(year); var endDate = bgDateOrYearEnd(row); if (!(cutoff && endDate)) return false; return endDate.getFullYear() === Number(year || 0) && endDate <= cutoff; } function bgYearRange(row) { var years = []; var startYear = Number(bgDisplayYear(row) || 0); var endYear = Number(bgCompletionYear(row) || 0); if (startYear && endYear && endYear >= startYear) { for (var year = startYear; year <= endYear; year += 1) years.push(String(year)); } else if (startYear) { years.push(String(startYear)); } return years; } function bgYears(rows) { var currentYear = new Date().getFullYear(); var years = Array.from(new Set((Array.isArray(rows) ? rows : []).flatMap(bgYearRange).filter(function (year) { return /^20\d{2}$/.test(year); }))).sort(function (a, b) { return Number(b) - Number(a); }); years = years.filter(function (year) { var numericYear = Number(year); return numericYear >= 2018 && numericYear <= currentYear; }); return years.length ? years : [String(currentYear)]; } function bgEnsureYear(rows) { var years = bgYears(rows); if (!years.includes(S.dashboard.year)) S.dashboard.year = years[0]; return years; } function bgTotals(targetRows) { return (Array.isArray(targetRows) ? targetRows : []).reduce(function (acc, row) { acc.c += Number((row && row.cSup) || 0); acc.col += Number((row && row.col) || 0); acc.recv += Number((row && row.recv) || 0); return acc; }, { c: 0, col: 0, recv: 0 }); } function isSupportServiceRow(row) { var category = String((row && row.cat) || "").trim(); return category.indexOf("경영지원") >= 0 || category.indexOf("서비스") >= 0; } function isBaronProjectRow(row) { var category = String((row && row.cat) || "").trim(); if (category.indexOf("바론") < 0) return false; if (isSupportServiceRow(row)) return false; return true; } function bgSummarize(rows, selectedYear) { var items = Array.isArray(rows) ? rows : []; var targetYear = selectedYear || bgEnsureYear(items)[0]; var activeRows = items.filter(function (row) { return bgActiveInYear(row, targetYear); }); var newProjectRows = items.filter(function (row) { return bgStartedInYear(row, targetYear); }); var completedRows = items.filter(function (row) { return bgCompletedInYear(row, targetYear); }); var managementRows = newProjectRows.filter(isSupportServiceRow); return { targetYear: targetYear, activeRows: activeRows, newProjectRows: newProjectRows, completedRows: completedRows, managementRows: managementRows, managementTotals: bgTotals(managementRows) }; } function bgMatches(row) { var section = S.dashboard.section || "active"; var selectedYear = S.dashboard.year || bgEnsureYear(S.all)[0]; if (section === "new") return bgStartedInYear(row, selectedYear); if (section === "completed") return bgCompletedInYear(row, selectedYear); return bgActiveInYear(row, selectedYear); } function normalizeStatusLabel(status) { var value = String(status || "").trim(); if (!value) return "-"; if (value.indexOf("진행") >= 0) return "과업 진행중"; return value; } function formatSplitPercent(split) { var numeric = parseFloat(String(split || "").replace(/[^0-9.\-]/g, "")); if (!Number.isFinite(numeric) || numeric === 0) return "분담율 -%"; return "분담율 " + numeric.toFixed(2) + "%"; } function projectYear(row) { var start = String((row && row.sDate) || "").trim(); var startMatch = start.match(/(20\d{2})/); if (startMatch) return startMatch[1]; var name = String((row && row.name) || "").trim(); var nameMatch = name.match(/^(20\d{2})/); if (nameMatch) return nameMatch[1]; var end = String((row && row.eDate) || "").trim(); var endMatch = end.match(/(20\d{2})/); if (endMatch) return endMatch[1]; return "미지정"; } function groupSortRank(row) { var selectedYear = Number((S.dashboard && S.dashboard.year) || projectYear(row) || 0); var startYear = Number(projectYear(row) || 0); if (typeof bgCompletedInYear === "function" && bgCompletedInYear(row, String(selectedYear))) return 9999; if (!startYear) return 9998; return startYear; } function tableGroupLabel(row) { var startYear = projectYear(row); if (/^20\d{2}$/.test(startYear)) return startYear + "년"; return "미지정"; } function renderLedgerTable() { var table = document.querySelector(".panel table"); if (!table || !E.tbody) return; var thead = table.querySelector("thead"); if (thead) { thead.innerHTML = '' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + '
' + ""; } var rows = (Array.isArray(S.viewRows) ? S.viewRows : []).slice().sort(function (a, b) { var ar = groupSortRank(a); var br = groupSortRank(b); if (ar !== br) return ar - br; return Number(b.recv || 0) - Number(a.recv || 0); }); S.viewRows = rows; var lastGroupLabel = ""; E.tbody.innerHTML = rows.map(function (r) { var groupLabel = tableGroupLabel(r); var isCollapsed = !!S.collapsedGroups[groupLabel]; var groupRow = ""; if (groupLabel !== lastGroupLabel) { groupRow = '"; lastGroupLabel = groupLabel; } if (isCollapsed) return groupRow; return groupRow + '' + '
= 0 ? 'badge-baron' : 'badge-family') + '">' + esc(r.cat || "-") + '
' + '
' + esc(r.code || "-") + '
' + '
' + esc(r.periodText || "-") + '
' + '
' + esc((r.client || "").trim() || "-") + '
' + esc(formatSplitPercent(r.split)) + '
' + '
' + esc(r.order || "-") + '
' + '
= 0 ? 'ok' : '') + '">' + esc(normalizeStatusLabel(r.status)) + '
' + '' + esc(won(r.cSup || 0)) + '' + '' + esc(r.outsourceCost ? won(r.outsourceCost) : "-") + '' + '' + esc(won(r.recv || 0)) + '' + '' + esc(won(r.col || 0)) + '' + '' + esc((Number(r.rate || 0)).toFixed(2) + "%") + '' + ''; }).join(""); } function renderCollectionBoard(r) { var payments = Array.isArray(r.payments) && r.payments.length ? r.payments : [{ pay: r.pay || "-", issueDate: r.issueDate || "", collectDate: r.collectDateSummary || r.colDate || "", collected: r.col || 0, receivable: r.recv || Math.max(0, Number(r.sTot || 0) - Number(r.col || 0)), note: r.note || "", status: r.status || "" }]; return '
C
수금 및 기성 현황
기성 차수별 세금계산서 발행 및 수금 내역
총 수금 ' + esc(won(r.col || 0)) + '
' + payments.map(function (payment, index) { var noteParts = []; if (payment.status) noteParts.push(payment.status); if (payment.note) noteParts.push(payment.note); return ''; }).join("") + "
기성 차수세금계산서 발행일수금일수금금액미수금액비고
' + esc((index + 1) + "차") + '' + esc(payment.pay || "-") + '' + esc(payment.issueDate ? d(payment.issueDate) : "-") + '' + esc(payment.collectDate ? d(payment.collectDate) : "-") + '' + esc(won(payment.collected || 0)) + '' + esc(won(payment.receivable || 0)) + '' + esc(noteParts.join(" / ") || "-") + '
"; } function renderContactCard(label, name, company, department, phone, email) { var hasValue = [name, company, department, phone, email].some(function (value) { return String(value || "").trim() !== ""; }); if (!hasValue) { return '
' + esc(label) + '
등록된 담당자 정보가 없습니다.
'; } return '
' + esc(label) + '
' + '
이름
' + esc(name || "-") + '
' + '
소속
' + esc(company || "-") + '
' + esc(department || "-") + '
' + '
연락처
' + esc(phone || "-") + '
' + '
이메일
' + esc(email || "-") + '
' + "
"; } function renderProjectInline(r) { var payments = Array.isArray(r.payments) ? r.payments : []; var latestCollect = d(r.collectDateSummary || r.colDate); var hasOutsource = (Array.isArray(r.outsourceItems) && r.outsourceItems.length > 0) || Number(r.outsourceCost || 0) > 0 || Number(r.outsourcePaid || 0) > 0 || Number(r.outsourceRemaining || 0) > 0; var clientDisplay = typeof normalizeClientDisplay === "function" ? normalizeClientDisplay(r.client) : (String(r.client || "").trim() || "-"); var splitDisplay = typeof formatSplitDisplay === "function" ? formatSplitDisplay(r.split) : formatSplitPercent(r.split).replace("분담율 ", ""); var summaryCards = [ '
계약금
' + esc(won(r.cSup || 0)) + '
', '
수금액
' + esc(won(r.col || 0)) + '
' + esc(latestCollect === "-" ? "수금일 없음" : "최종 수금일 " + latestCollect) + '
', '
수금률
' + esc((Number(r.rate || 0)).toFixed(2) + "%") + '
' + esc(payments.length ? "기성 " + payments.length + "차까지 반영" : "차수 정보 없음") + '
', '
미수금액
' + esc(won(r.recv || 0)) + '
잔여 수금 필요 금액
' ].join(""); var boards = [ hasOutsource && typeof renderOutsourceBoard === "function" ? renderOutsourceBoard(r) : "", renderCollectionBoard(r) ].filter(Boolean).join(""); return '
계약법인
' + esc(r.corp || "-") + '
발주처
' + esc(clientDisplay) + '
' + esc(splitDisplay ? "분담율 " + splitDisplay : "분담율 -") + '
발주방법
' + esc(r.order || "-") + '
PM
' + esc(r.pm || "-") + '
' + summaryCards + '
' + renderContactCard("계약 / 청구 담당자", r.cmNm, r.cmCo, r.cmDp, r.cmPh, r.cmEm) + renderContactCard("부서 담당자", r.dmNm, r.dmCo, r.dmDp, r.dmPh, r.dmEm) + '
' + boards + '
'; } function openProjectWindow(r) { var popupKey = typeof rowKey === "function" ? rowKey(r).replace(/[^0-9a-zA-Z]/g, "_") : String((r.code || "project") + "_" + (r.name || "")).replace(/[^0-9a-zA-Z_]/g, "_"); var popup = window.open("", "business_project_" + popupKey, "width=1600,height=980,resizable=yes,scrollbars=yes"); if (!popup) return; var styleText = Array.from(document.querySelectorAll("style")).map(function (el) { return el.textContent || ""; }).join("\n"); var detailHtml = renderProjectInline(r); var pageHtml = '' + esc(r.name || "사업 상세") + '"; popup.document.open(); popup.document.write(pageHtml); popup.document.close(); popup.focus(); } async function tryLoadDbDefaultBusinessLedger() { if (window.__mhBusinessDefaultLoaded) return; window.__mhBusinessDefaultLoaded = true; try { var response = await fetch("/api/integration/business-ledger-default"); if (!response.ok) throw new Error("기본 사업관리대장 원본을 불러오지 못했습니다."); var fileName = response.headers.get("x-source-filename") || "사업관리대장-1.xlsx"; var buffer = await response.arrayBuffer(); if (!buffer || !buffer.byteLength) throw new Error("기본 사업관리대장 원본 데이터가 비어 있습니다."); await loadLedgerFile(buffer, fileName); } catch (error) { console.error(error); } } function applyDashboardChrome() { if (!E.cards) return; document.body.setAttribute("data-mh-ledger-enhanced", "true"); var wrap = document.querySelector(".wrap"); var panel = document.querySelector(".panel"); if (wrap && panel) { var shell = wrap.querySelector(".business-shell"); if (!shell) { shell = document.createElement("div"); shell.className = "business-shell"; wrap.insertBefore(shell, E.cards); } if (E.cards.parentNode !== shell) shell.appendChild(E.cards); if (panel.parentNode !== shell) shell.appendChild(panel); } var years = bgEnsureYear(S.all); var summary = bgSummarize(S.all, S.dashboard.year); var rows = Array.isArray(S.rows) ? S.rows : []; var visibleBaronProjectRows = rows.filter(isBaronProjectRow); var totals = bgTotals(visibleBaronProjectRows); var totalRate = typeof rate === "function" ? rate("", totals.col, totals.col + totals.recv) : 0; var toolbarHtml = '
' + '
' + years.map(function (year) { return '"; }).join("") + '' + "
" + '
' + '' + '' + '' + "
"; var cards = [ { label: summary.targetYear + "년 프로젝트", value: visibleBaronProjectRows.length.toLocaleString("ko-KR") + " 건", note: "" }, { label: "계약금", value: won(totals.c), note: "" }, { label: "수금액", value: won(totals.col), note: "" }, { label: "미수금", value: won(totals.recv), note: "" }, { label: "수금률(%)", value: totalRate.toFixed(2) + "%", note: "" }, { label: "경영지원서비스 금액", value: won(summary.managementTotals.c), note: "", className: "management" } ]; E.cards.innerHTML = toolbarHtml + cards.map(function (card) { return '
' + esc(card.label) + '
' + esc(card.value) + '
' + esc(card.note || "") + "
"; }).join(""); var searchWrap = E.cards.querySelector(".cards-toolbar-search"); if (searchWrap && E.search) { searchWrap.appendChild(E.search); E.search.placeholder = "전체 검색"; } } var originalRender = render; render = function () { originalRender(); applyDashboardChrome(); renderLedgerTable(); }; filter = function () { bgEnsureYear(S.all); var q = String(E.search.value || "").trim().toLowerCase(); var searched = !q ? S.all.slice() : S.all.filter(function (r) { return [r.code, r.name, r.client, r.pm, r.status, r.cat, r.corp, r.pay, (r.payments || []).map(function (p) { return p.pay; }).join(" "), r.periodText].join(" ").toLowerCase().includes(q); }); S.rows = searched.filter(function (r) { return bgMatches(r) && matchesColumnFilters(r); }); render(); }; if (E.cards && !E.cards.dataset.dashboardBound) { E.cards.dataset.dashboardBound = "true"; E.cards.addEventListener("click", function (event) { var yearButton = event.target && event.target.closest ? event.target.closest("[data-dashboard-year]") : null; if (yearButton) { S.dashboard.year = yearButton.getAttribute("data-dashboard-year") || S.dashboard.year; filter(); return; } var sectionButton = event.target && event.target.closest ? event.target.closest("[data-dashboard-section]") : null; if (sectionButton) { S.dashboard.section = sectionButton.getAttribute("data-dashboard-section") || "active"; filter(); } }); } if (E.tbody && !E.tbody.dataset.projectBound) { E.tbody.dataset.projectBound = "true"; E.tbody.addEventListener("click", function (event) { var groupButton = event.target && event.target.closest ? event.target.closest("[data-group-label]") : null; if (groupButton) { var label = groupButton.getAttribute("data-group-label") || ""; if (label) { S.collapsedGroups[label] = !S.collapsedGroups[label]; render(); } return; } var trigger = event.target && event.target.closest ? event.target.closest(".project-link") : null; if (!trigger) return; var key = trigger.getAttribute("data-project-key") || ""; var rows = Array.isArray(S.viewRows) ? S.viewRows : []; var row = rows.find(function (item) { return (String(item.code || "") + "|" + String(item.name || "")) === key; }); if (row) openProjectWindow(row); }); } setTimeout(function () { try { filter(); if (typeof loadLedgerFile === "function") { tryLoadDbDefaultBusinessLedger(); } } catch (error) { console.error(error); } }, 0); window.addEventListener("message", function (event) { var data = event.data || {}; if (data.source !== "total-upload" || data.type !== "business") return; setTimeout(function () { try { applyDashboardChrome(); renderLedgerTable(); } catch (error) { console.error(error); } }, 50); }); })();