diff --git a/static/modules/finance.js b/static/modules/finance.js
index 351ed5b..a1e7968 100644
--- a/static/modules/finance.js
+++ b/static/modules/finance.js
@@ -113,11 +113,14 @@ function renderFinance() {
const toolQuarterSelect = qLabels2.map((l,i) => '').join("");
const toolDateSelect = state.finView==='monthly'?'月份:':state.finView==='quarterly'?'季度:':'';
- const finHeaderBase = `视图:状态:${toolDateSelect}`;
+ const finHeaderBase = `视图:${toolDateSelect}`;
const finAddBtn = ``;
+ const finFilterTabs = `
`;
+
document.querySelector("#finance").innerHTML = `
- ${card(`
${finHeaderBase}
${finAddBtn}
`, "p-3")}
+ ${finFilterTabs}
+ ${card(`
${finHeaderBase}
${finAddBtn}
`, "p-4")}
@@ -234,20 +237,21 @@ function renderFinance() {
${(() => {
+ const isUnsigned = state.finFilter === '待签约';
const METRIC_CARDS = [
{ label: '签约金额', value: ctx => moneyWan(ctx.signTotal), color: 'text-slate-700' },
- { label: '已确收', value: ctx => moneyWan(ctx.sumRev), color: 'text-blue-700' },
+ { label: '已确收', hideUnsigned: true, value: ctx => moneyWan(ctx.sumRev), color: 'text-blue-700' },
{ label: '毛利', value: ctx => money(ctx.sumGross), color: 'text-green-700' },
{ label: '成本', value: ctx => moneyWan(ctx.sumCost), color: 'text-rose-700' },
{ label: '项目利润', value: ctx => { const v = ctx.sumGross; return { val: moneyWan(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null },
- { label: '已回款', value: ctx => moneyWan(ctx.sumPay), color: 'text-amber-700' },
- { label: '已付', value: ctx => moneyWan(ctx.sumPaid), color: 'text-purple-700' },
- { label: '现金流', value: ctx => { const v = ctx.sumPay - ctx.sumPaid; return { val: money(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null },
- { label: '执行率', value: ctx => ctx.sumRev && ctx.signTotal ? Math.round(ctx.sumRev / ctx.signTotal * 100) + '%' : '—', color: 'text-slate-500' },
- { label: '毛利率', value: ctx => ctx.sumRev && ctx.sumGross ? Math.round(ctx.sumGross / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' },
- { label: '回款率', value: ctx => ctx.sumRev && ctx.sumPay ? Math.round(ctx.sumPay / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' },
- { label: '付款率', value: ctx => ctx.sumCost && ctx.sumPaid ? Math.round(ctx.sumPaid / ctx.sumCost * 100) + '%' : '—', color: 'text-slate-500' },
- ];
+ { label: '已回款', hideUnsigned: true, value: ctx => moneyWan(ctx.sumPay), color: 'text-amber-700' },
+ { label: '已付', hideUnsigned: true, value: ctx => moneyWan(ctx.sumPaid), color: 'text-purple-700' },
+ { label: '现金流', hideUnsigned: true, value: ctx => { const v = ctx.sumPay - ctx.sumPaid; return { val: money(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null },
+ { label: '执行率', hideUnsigned: true, value: ctx => ctx.sumRev && ctx.signTotal ? Math.round(ctx.sumRev / ctx.signTotal * 100) + '%' : '—', color: 'text-slate-500' },
+ { label: '毛利率', hideUnsigned: true, value: ctx => ctx.sumRev && ctx.sumGross ? Math.round(ctx.sumGross / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' },
+ { label: '回款率', hideUnsigned: true, value: ctx => ctx.sumRev && ctx.sumPay ? Math.round(ctx.sumPay / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' },
+ { label: '付款率', hideUnsigned: true, value: ctx => ctx.sumCost && ctx.sumPaid ? Math.round(ctx.sumPaid / ctx.sumCost * 100) + '%' : '—', color: 'text-slate-500' },
+ ].filter(c => state.finFilter !== '待签约' || !c.hideUnsigned);
const renderView = (rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, emptyText) => {
const ctx = { sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt };
@@ -255,26 +259,41 @@ function renderFinance() {
const v = c.value(ctx);
return typeof v === 'object' ? [c.label, v.val, v.cls] : [c.label, v, c.color];
});
- const thead = '' +
- '| 项目名称 | ' +
- '状态 | ' +
- '签约金额 | ' +
- '已确收 | ' +
- '毛利 | ' +
- '成本 | ' +
- '项目利润 | ' +
- '已回款 | ' +
- '已付 | ' +
- '现金流 | ' +
- '执行率 | ' +
- '毛利率 | ' +
- '回款率 | ' +
- '付款率 | ' +
- '
';
- const tbody = rows.length ? `${rows.join("")}` : `| ${emptyText} |
`;
+ const thead = isUnsigned
+ ? '' +
+ '| 项目名称 | ' +
+ '状态 | ' +
+ '签约金额 | ' +
+ '签约月份 | ' +
+ '毛利 | ' +
+ '成本 | ' +
+ '项目利润 | ' +
+ '
'
+ : '' +
+ '| 项目名称 | ' +
+ '状态 | ' +
+ '签约金额 | ' +
+ '已确收 | ' +
+ '毛利 | ' +
+ '成本 | ' +
+ '项目利润 | ' +
+ '已回款 | ' +
+ '已付 | ' +
+ '现金流 | ' +
+ '执行率 | ' +
+ '毛利率 | ' +
+ '回款率 | ' +
+ '付款率 | ' +
+ '
';
+ const theadCols = isUnsigned ? 7 : 14;
+ const tbody = rows.length ? `${rows.join("")}` : `| ${emptyText} |
`;
- return card(`${cards.map(([l, v, c]) => '
' + l + '' + v + '
').join("")}
`, "p-4") +
- card(`项目财务明细
合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流 → 执行率 → 毛利率 → 回款率 → 付款率
`, "p-4");
+ const cardGrid = isUnsigned ? 'grid-cols-4' : 'grid-cols-6';
+ const subtitle = isUnsigned
+ ? '合同 → 毛利 → 成本 → 项目利润'
+ : '合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流 → 执行率 → 毛利率 → 回款率 → 付款率';
+ return card(`${cards.map(([l, v, c]) => '
' + l + '' + v + '
').join("")}
`, "p-4") +
+ card(`项目财务明细
${subtitle}
`, "p-4");
};
const fmtNoUnit = (v) => v ? `${Number(v||0).toLocaleString('zh-CN')}` : '—';
@@ -292,6 +311,14 @@ function renderFinance() {
const pfVal = profit ? money(profit) : '—';
return `| ${esc(pf.customer_name)} | ${st} | ${money(pf.sign_amount)} | ${fmtNoUnit(rev)} | ${fmtNoUnit(gross)} | ${fmtNoUnit(cost)} | ${pfVal} | ${fmtNoUnit(payment)} | ${fmtNoUnit(paid)} | ${cf} | ${exe} | ${grossR} | ${payR} | ${paidR} |
`;
};
+ // 待签约简版行
+ const tdRowUnsigned = (pf, cost, gross) => {
+ const st = pf.status === '待签约' ? '待签约' : '已签约';
+ const profit = gross;
+ const pfCls = profit >= 0 ? 'text-green-600' : 'text-red-600';
+ const pfVal = profit ? money(profit) : '—';
+ return `| ${esc(pf.customer_name)} | ${st} | ${money(pf.sign_amount)} | ${pf.sign_month || '—'} | ${fmtNoUnit(gross)} | ${fmtNoUnit(cost)} | ${pfVal} |
`;
+ };
if (state.finView === 'monthly') {
const allPfs = pfs.filter(x => x.status === state.finFilter);
@@ -313,7 +340,7 @@ function renderFinance() {
const paid = Math.round(parseFloat(e.paid || 0) || 0);
if (!rev && !payment && !cost && !paid && !gross) return;
sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; sumGross += gross;
- rows.push(tdRow(pf, rev, payment, cost, paid, gross));
+ rows.push(isUnsigned ? tdRowUnsigned(pf, cost, gross) : tdRow(pf, rev, payment, cost, paid, gross));
});
const signTotal = Math.round(allPfs.filter(x => (x.sign_month || "").substring(0, 7) === selMonth).reduce((a, p) => a + (p.sign_amount || 0), 0));
const signCnt = allPfs.filter(x => x.status === "已签约" && (x.sign_month || "").substring(0, 7) === selMonth).length;
@@ -349,7 +376,7 @@ function renderFinance() {
const cost = sumExpense(pf, "cost");
const paid = sumExpense(pf, "paid");
sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; sumGross += gross;
- rows.push(tdRow(pf, rev, payment, cost, paid, gross));
+ rows.push(isUnsigned ? tdRowUnsigned(pf, cost, gross) : tdRow(pf, rev, payment, cost, paid, gross));
});
const signTotal = Math.round(allPfs.filter(x => { const m = parseInt((x.sign_month || "0").substring(5)) || 0; return qRange.includes(m); }).reduce((a, p) => a + (p.sign_amount || 0), 0));
const signCnt = allPfs.filter(x => x.status === "已签约" && qRange.includes(parseInt((x.sign_month || "0").substring(5)) || 0)).length;
@@ -372,7 +399,7 @@ function renderFinance() {
allPfs.forEach(pf => {
const t = calcTotals(pf);
sumRev += t.rev; sumPay += t.payment; sumCost += t.cost; sumPaid += t.paid; sumGross += t.gross;
- rows.push(tdRow(pf, t.rev, t.payment, t.cost, t.paid, t.gross));
+ rows.push(isUnsigned ? tdRowUnsigned(pf, t.cost, t.gross) : tdRow(pf, t.rev, t.payment, t.cost, t.paid, t.gross));
});
const signTotal = Math.round(allPfs.reduce((a, p) => a + (p.sign_amount || 0), 0));
const signCnt = allPfs.filter(x => x.status === "已签约").length;
diff --git a/static/modules/utils.js b/static/modules/utils.js
index 42c9d11..89510f6 100644
--- a/static/modules/utils.js
+++ b/static/modules/utils.js
@@ -192,6 +192,10 @@ window.setFinView = (view) => {
localStorage.setItem("opc-fin-view", view);
renderFinance();
};
+window.switchFinFilter = (filter) => {
+ state.finFilter = filter;
+ renderFinance();
+};
window.switchTenant = (tenant) => {
state.tenant = tenant;
state.selectedProject = null;