Improve ledger filters and dev sync checks
This commit is contained in:
@@ -10,6 +10,10 @@
|
||||
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
}
|
||||
|
||||
function bgNormalizeText(value) {
|
||||
return String(value || "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function bgParseDate(value) {
|
||||
var text = String(value || "").trim();
|
||||
if (!text) return null;
|
||||
@@ -36,6 +40,44 @@
|
||||
return bgYearFromText(row && row.eDate);
|
||||
}
|
||||
|
||||
function normalizedCategory(row) {
|
||||
var category = bgNormalizeText(row && row.cat);
|
||||
if (category.indexOf("가족사") >= 0) return "가족사";
|
||||
var corp = bgNormalizeText(row && row.corp);
|
||||
if (corp && corp !== "바론") return "가족사";
|
||||
return "바론";
|
||||
}
|
||||
|
||||
function isSupportServiceRow(row) {
|
||||
return bgNormalizeText(row && row.name).indexOf("경영 및 기술지원 서비스") >= 0;
|
||||
}
|
||||
|
||||
function projectTypeLabel(row) {
|
||||
if (isSupportServiceRow(row)) return "기술지원서비스";
|
||||
return normalizedCategory(row);
|
||||
}
|
||||
|
||||
function projectTypeRank(row) {
|
||||
var label = projectTypeLabel(row);
|
||||
if (label === "바론") return 0;
|
||||
if (label === "가족사") return 1;
|
||||
return 2;
|
||||
}
|
||||
|
||||
function normalizeStatusLabel(status) {
|
||||
var value = bgNormalizeText(status);
|
||||
if (!value) return "-";
|
||||
if (value === "완료") return "준공";
|
||||
if (value === "진행") return "과업진행중";
|
||||
if (value === "대기") return "계약대기";
|
||||
if (value === "중지") return "과업중지";
|
||||
return value;
|
||||
}
|
||||
|
||||
function rowStatusLabel(row) {
|
||||
return normalizeStatusLabel(row && row.status);
|
||||
}
|
||||
|
||||
function bgDisplayYear(row) {
|
||||
var start = bgStartYear(row);
|
||||
if (start) return start;
|
||||
@@ -82,7 +124,7 @@
|
||||
if (!(cutoff && yearStart && startDate)) return false;
|
||||
if (startDate > cutoff) return false;
|
||||
if (endDate && endDate < yearStart) return false;
|
||||
return !(endDate && endDate <= cutoff);
|
||||
return rowStatusLabel(row) === "과업진행중";
|
||||
}
|
||||
|
||||
function bgStartedInYear(row, year) {
|
||||
@@ -96,7 +138,7 @@
|
||||
var cutoff = bgYearCutoff(year);
|
||||
var endDate = bgDateOrYearEnd(row);
|
||||
if (!(cutoff && endDate)) return false;
|
||||
return endDate.getFullYear() === Number(year || 0) && endDate <= cutoff;
|
||||
return rowStatusLabel(row) === "준공" && endDate.getFullYear() === Number(year || 0) && endDate <= cutoff;
|
||||
}
|
||||
|
||||
function bgYearRange(row) {
|
||||
@@ -140,16 +182,32 @@
|
||||
}, { 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) {
|
||||
return projectTypeLabel(row) === "바론";
|
||||
}
|
||||
|
||||
function isBaronProjectRow(row) {
|
||||
var category = String((row && row.cat) || "").trim();
|
||||
if (category.indexOf("바론") < 0) return false;
|
||||
if (isSupportServiceRow(row)) return false;
|
||||
return true;
|
||||
function isSoftwareProjectRow(row) {
|
||||
var name = bgNormalizeText(row && row.name).toLowerCase();
|
||||
if (!name) return false;
|
||||
return [
|
||||
"프로그램",
|
||||
"소프트웨어",
|
||||
"software",
|
||||
" sw",
|
||||
"sw ",
|
||||
"erp",
|
||||
"tova",
|
||||
"ipipe",
|
||||
"eg-bim",
|
||||
"cad"
|
||||
].some(function (keyword) {
|
||||
return name.indexOf(keyword) >= 0;
|
||||
});
|
||||
}
|
||||
|
||||
function shouldSinkProjectName(row) {
|
||||
var name = bgNormalizeText(row && row.name);
|
||||
return name.indexOf("프로그램") >= 0 || name.indexOf("사용") >= 0;
|
||||
}
|
||||
|
||||
function bgSummarize(rows, selectedYear) {
|
||||
@@ -158,14 +216,18 @@
|
||||
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);
|
||||
var managementRows = activeRows.filter(isSupportServiceRow);
|
||||
var baronActiveRows = activeRows.filter(isBaronProjectRow);
|
||||
return {
|
||||
targetYear: targetYear,
|
||||
activeRows: activeRows,
|
||||
newProjectRows: newProjectRows,
|
||||
completedRows: completedRows,
|
||||
managementRows: managementRows,
|
||||
managementTotals: bgTotals(managementRows)
|
||||
managementTotals: bgTotals(managementRows),
|
||||
baronActiveRows: baronActiveRows,
|
||||
baronProjectTotals: bgTotals(baronActiveRows),
|
||||
baronSoftwareCount: baronActiveRows.filter(isSoftwareProjectRow).length
|
||||
};
|
||||
}
|
||||
|
||||
@@ -177,13 +239,6 @@
|
||||
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 "분담율 -%";
|
||||
@@ -204,17 +259,57 @@
|
||||
}
|
||||
|
||||
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 "미지정";
|
||||
if (/^20\d{2}$/.test(startYear)) return startYear + " " + projectTypeLabel(row);
|
||||
return "미지정 " + projectTypeLabel(row);
|
||||
}
|
||||
|
||||
function compareDashboardRows(a, b) {
|
||||
var typeRankDiff = projectTypeRank(a) - projectTypeRank(b);
|
||||
if (typeRankDiff !== 0) return typeRankDiff;
|
||||
var groupDiff = groupSortRank(a) - groupSortRank(b);
|
||||
if (groupDiff !== 0) return groupDiff;
|
||||
var sinkDiff = Number(shouldSinkProjectName(a)) - Number(shouldSinkProjectName(b));
|
||||
if (sinkDiff !== 0) return sinkDiff;
|
||||
return bgNormalizeText(a && a.name).localeCompare(bgNormalizeText(b && b.name), "ko");
|
||||
}
|
||||
|
||||
function filterCategoryLabel(row) {
|
||||
return projectTypeLabel(row);
|
||||
}
|
||||
|
||||
function filterClientLabel(row) {
|
||||
if (typeof normalizeClientDisplay === "function") {
|
||||
return normalizeClientDisplay(row && row.client);
|
||||
}
|
||||
return bgNormalizeText(row && row.client) || "-";
|
||||
}
|
||||
|
||||
function filterOrderLabel(row) {
|
||||
return bgNormalizeText(row && row.order) || "-";
|
||||
}
|
||||
|
||||
function receivableFilterLabel(row) {
|
||||
var amount = Number((row && row.recv) || 0);
|
||||
if (amount <= 0) return "미수 없음";
|
||||
if (amount < 10000000) return "1천만 미만";
|
||||
if (amount < 100000000) return "1천만 이상";
|
||||
return "1억 이상";
|
||||
}
|
||||
|
||||
function refreshFilterDom() {
|
||||
E.filterButtons = Object.fromEntries(Array.from(document.querySelectorAll(".th-trigger")).map(function (el) {
|
||||
return [el.dataset.filter, el];
|
||||
}));
|
||||
E.filterMenus = Object.fromEntries(Array.from(document.querySelectorAll(".th-menu")).map(function (el) {
|
||||
return [el.dataset.filter, el];
|
||||
}));
|
||||
}
|
||||
|
||||
function renderLedgerTable() {
|
||||
@@ -236,12 +331,7 @@
|
||||
+ '<th class="num"><div class="th-head end"><button type="button" class="th-trigger" data-filter="rate" data-label="수금률"><span class="th-title">수금률</span><span class="th-mark"></span><span class="th-caret">▼</span></button><div id="filterRateMenu" class="th-menu" data-filter="rate"></div></div></th>'
|
||||
+ "</tr>";
|
||||
}
|
||||
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);
|
||||
});
|
||||
var rows = (Array.isArray(S.viewRows) ? S.viewRows : []).slice().sort(compareDashboardRows);
|
||||
S.viewRows = rows;
|
||||
var lastGroupLabel = "";
|
||||
E.tbody.innerHTML = rows.map(function (r) {
|
||||
@@ -259,7 +349,7 @@
|
||||
+ '<td><button type="button" class="project-link" data-project-key="' + escAttr(String(r.code || "") + "|" + String(r.name || "")) + '">' + esc(r.name || "-") + '</button><div class="subline">' + esc(r.periodText || "-") + '</div></td>'
|
||||
+ '<td><div class="client-main">' + esc((r.client || "").trim() || "-") + '</div><div class="subline">' + esc(formatSplitPercent(r.split)) + '</div></td>'
|
||||
+ '<td><div>' + esc(r.order || "-") + '</div></td>'
|
||||
+ '<td><div class="badge ' + (String(r.status || "").indexOf("완료") >= 0 ? 'ok' : '') + '">' + esc(normalizeStatusLabel(r.status)) + '</div></td>'
|
||||
+ '<td><div class="badge ' + (rowStatusLabel(r) === "준공" ? 'ok' : '') + '">' + esc(rowStatusLabel(r)) + '</div></td>'
|
||||
+ '<td class="num"><strong>' + esc(won(r.cSup || 0)) + '</strong></td>'
|
||||
+ '<td class="num"><strong>' + esc(r.outsourceCost ? won(r.outsourceCost) : "-") + '</strong></td>'
|
||||
+ '<td class="num"><strong>' + esc(won(r.recv || 0)) + '</strong></td>'
|
||||
@@ -267,6 +357,8 @@
|
||||
+ '<td class="num"><strong style="color:' + (isSettledRow(r) ? '#b7aa93' : '#1a5645') + '">' + esc((Number(r.rate || 0)).toFixed(2) + "%") + '</strong></td>'
|
||||
+ '</tr>';
|
||||
}).join("");
|
||||
refreshFilterDom();
|
||||
if (typeof syncColumnFilters === "function") syncColumnFilters(S.all);
|
||||
}
|
||||
|
||||
function renderCollectionBoard(r) {
|
||||
@@ -379,10 +471,8 @@
|
||||
}
|
||||
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 totals = summary.baronProjectTotals;
|
||||
var totalRate = totals.c > 0 ? (totals.col / totals.c) * 100 : 0;
|
||||
var toolbarHtml = '<div class="cards-toolbar">'
|
||||
+ '<div class="cards-toolbar-row">'
|
||||
+ years.map(function (year) {
|
||||
@@ -393,14 +483,14 @@
|
||||
+ '<div class="cards-toolbar-metrics">'
|
||||
+ '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "active" ? "active" : "") + '" data-dashboard-section="active"><span class="label">' + esc(summary.targetYear) + '년 진행과업</span><span class="count">' + summary.activeRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">전년도 이월 사업 포함</span></button>'
|
||||
+ '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "new" ? "active" : "") + '" data-dashboard-section="new"><span class="label">' + esc(summary.targetYear) + '년 신규프로젝트</span><span class="count">' + summary.newProjectRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">계약기간 시작년도 기준</span></button>'
|
||||
+ '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "completed" ? "active" : "") + '" data-dashboard-section="completed"><span class="label">' + esc(summary.targetYear) + '년 완료과업</span><span class="count">' + summary.completedRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">해당년도 종료 사업 기준</span></button>'
|
||||
+ '<button type="button" class="summary-filter-chip ' + (S.dashboard.section === "completed" ? "active" : "") + '" data-dashboard-section="completed"><span class="label">' + esc(summary.targetYear) + '년 완료과업</span><span class="count">' + summary.completedRows.length.toLocaleString("ko-KR") + '건</span><span class="meta">진행상태 준공 기준</span></button>'
|
||||
+ "</div></div>";
|
||||
var cards = [
|
||||
{ label: summary.targetYear + "년 프로젝트", value: visibleBaronProjectRows.length.toLocaleString("ko-KR") + " 건", note: "" },
|
||||
{ label: "계약금", value: won(totals.c), note: "" },
|
||||
{ label: summary.targetYear + "년 프로젝트", value: summary.baronActiveRows.length.toLocaleString("ko-KR") + "건 (" + summary.baronSoftwareCount.toLocaleString("ko-KR") + "건)", note: "바론 수행중 프로젝트 / SW" },
|
||||
{ label: "계약금 (VAT별도)", 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: totalRate.toFixed(2) + "%", note: "계약금 대비 수금액" },
|
||||
{ label: "경영지원서비스 금액", value: won(summary.managementTotals.c), note: "", className: "management" }
|
||||
];
|
||||
E.cards.innerHTML = toolbarHtml + cards.map(function (card) {
|
||||
@@ -429,9 +519,62 @@
|
||||
S.rows = searched.filter(function (r) {
|
||||
return bgMatches(r) && matchesColumnFilters(r);
|
||||
});
|
||||
S.rows.sort(compareDashboardRows);
|
||||
render();
|
||||
};
|
||||
|
||||
filterDefinitions = function () {
|
||||
return [
|
||||
{ key: "cat", map: filterCategoryLabel },
|
||||
{ key: "code", map: function (r) { return r.code || "-"; } },
|
||||
{ key: "name", map: function (r) { return r.name || "-"; } },
|
||||
{ key: "client", map: filterClientLabel },
|
||||
{ key: "order", map: filterOrderLabel },
|
||||
{ key: "status", map: rowStatusLabel },
|
||||
{ key: "amount", map: amountFilterLabel },
|
||||
{ key: "outsource", map: outsourceFilterLabel },
|
||||
{ key: "receivable", map: receivableFilterLabel },
|
||||
{ key: "collected", map: collectedFilterLabel },
|
||||
{ key: "rate", map: rateFilterLabel }
|
||||
];
|
||||
};
|
||||
|
||||
updateFilterButtons = function () {
|
||||
Object.keys(E.filterButtons || {}).forEach(function (key) {
|
||||
var btn = E.filterButtons[key];
|
||||
if (!btn) return;
|
||||
var active = !!S.filters[key];
|
||||
btn.classList.toggle("active", active);
|
||||
btn.title = active ? ((btn.dataset.label || "") + ": " + S.filters[key]) : (btn.dataset.label || "");
|
||||
var mark = btn.querySelector(".th-mark");
|
||||
if (mark) mark.textContent = active ? "•" : "";
|
||||
});
|
||||
};
|
||||
|
||||
syncColumnFilters = function (rows) {
|
||||
filterDefinitions().forEach(function (def) {
|
||||
var values = uniqueFilterValues(rows, def.map);
|
||||
if (S.filters[def.key] && !values.includes(S.filters[def.key])) delete S.filters[def.key];
|
||||
renderFilterMenu(def.key, values);
|
||||
});
|
||||
updateFilterButtons();
|
||||
};
|
||||
|
||||
matchesColumnFilters = function (r) {
|
||||
if (S.filters.cat && filterCategoryLabel(r) !== S.filters.cat) return false;
|
||||
if (S.filters.code && (r.code || "-") !== S.filters.code) return false;
|
||||
if (S.filters.name && (r.name || "-") !== S.filters.name) return false;
|
||||
if (S.filters.client && filterClientLabel(r) !== S.filters.client) return false;
|
||||
if (S.filters.order && filterOrderLabel(r) !== S.filters.order) return false;
|
||||
if (S.filters.status && rowStatusLabel(r) !== S.filters.status) return false;
|
||||
if (S.filters.amount && amountFilterLabel(r) !== S.filters.amount) return false;
|
||||
if (S.filters.outsource && outsourceFilterLabel(r) !== S.filters.outsource) return false;
|
||||
if (S.filters.receivable && receivableFilterLabel(r) !== S.filters.receivable) return false;
|
||||
if (S.filters.collected && collectedFilterLabel(r) !== S.filters.collected) return false;
|
||||
if (S.filters.rate && rateFilterLabel(r) !== S.filters.rate) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
if (E.cards && !E.cards.dataset.dashboardBound) {
|
||||
E.cards.dataset.dashboardBound = "true";
|
||||
E.cards.addEventListener("click", function (event) {
|
||||
@@ -472,6 +615,26 @@
|
||||
});
|
||||
}
|
||||
|
||||
var panel = document.querySelector(".panel");
|
||||
if (panel && !panel.dataset.ledgerFilterBound) {
|
||||
panel.dataset.ledgerFilterBound = "true";
|
||||
panel.addEventListener("click", function (event) {
|
||||
var trigger = event.target && event.target.closest ? event.target.closest(".th-trigger") : null;
|
||||
if (trigger) {
|
||||
refreshFilterDom();
|
||||
event.stopPropagation();
|
||||
toggleFilterMenu(trigger.dataset.filter);
|
||||
return;
|
||||
}
|
||||
var option = event.target && event.target.closest ? event.target.closest("button[data-filter-value]") : null;
|
||||
var menu = event.target && event.target.closest ? event.target.closest(".th-menu") : null;
|
||||
if (option && menu) {
|
||||
event.stopPropagation();
|
||||
setFilterValue(menu.dataset.filter, option.getAttribute("data-filter-value") || "");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
try {
|
||||
filter();
|
||||
|
||||
Reference in New Issue
Block a user