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")}
${(() => { + 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(`

项目财务明细

合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流 → 执行率 → 毛利率 → 回款率 → 付款率

${thead}${tbody}
`, "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}

${thead}${tbody}
`, "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;