feat: 首页重构 — 财务概览转置+部门经营卡片+命名统一
首页: - 5卡片→表格(行=指标,列=时间),新增已付/现金流/项目利润行,费用行 - 行列对换:8列(年度/季度/季环比/本月/月环比/上季度/上月)×9行 - 新增「部门经营情况」卡片(部门利润/费用/净利) - 趋势图3→4列,新增月度项目利润折线图(indigo) - 命名:利润→项目利润,费用→项目费用,月度利润→月度项目利润 - 卡片改名:经营管理→项目管理,财务概览→业务(项目)财务概览 - 指标列统一颜色,去掉行编号 项目模块: - 利润→项目利润,费用→项目费用,总费用→总项目费用 - 月度回款与费用→月度回款与已付,柱状图→折线图 后端: - 新增paid/cashflow/profit指标聚合(单/多租户) - monthly_finance新增paid字段 - helpers.py修复重复return data
This commit is contained in:
@@ -186,12 +186,12 @@ function renderFinance() {
|
||||
<p class="text-lg font-bold text-purple-700" id="budgetTotalPaid">¥0</p>
|
||||
</div>
|
||||
<div class="bg-orange-50 rounded-lg p-3 text-center border border-orange-100" id="budgetTotalExpenseCard">
|
||||
<p class="text-xs text-orange-600 font-medium">总费用</p>
|
||||
<p class="text-xs text-orange-600 font-medium">总项目费用</p>
|
||||
<p class="text-lg font-bold text-orange-700" id="budgetTotalExpense">¥0</p>
|
||||
</div>
|
||||
</div>
|
||||
<table class="w-full text-sm border border-slate-200 rounded-lg overflow-hidden" id="budgetTable">
|
||||
<thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2.5 text-left font-medium text-slate-500" style="min-width:140px">月份</th><th class="p-2.5 text-right font-medium text-slate-500">确收</th><th class="p-2.5 text-right font-medium text-slate-500">毛利</th><th class="p-2.5 text-right font-medium text-slate-500">回款</th><th class="p-2.5 text-right font-medium text-slate-500">应付</th><th class="p-2.5 text-right font-medium text-slate-500">已付</th><th class="p-2.5 text-right font-medium text-slate-500">费用</th><th class="p-2.5 w-8"></th></tr></thead>
|
||||
<thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2.5 text-left font-medium text-slate-500" style="min-width:140px">月份</th><th class="p-2.5 text-right font-medium text-slate-500">确收</th><th class="p-2.5 text-right font-medium text-slate-500">毛利</th><th class="p-2.5 text-right font-medium text-slate-500">回款</th><th class="p-2.5 text-right font-medium text-slate-500">应付</th><th class="p-2.5 text-right font-medium text-slate-500">已付</th><th class="p-2.5 text-right font-medium text-slate-500">项目费用</th><th class="p-2.5 w-8"></th></tr></thead>
|
||||
<tbody id="budgetTbody"></tbody>
|
||||
</table>
|
||||
<button type="button" class="btn btn-ghost btn-sm mt-3" onclick="addBudgetRow()"><i data-lucide="plus"></i>添加月份</button>
|
||||
@@ -235,14 +235,14 @@ function renderFinance() {
|
||||
{ label: '执行率', value: ctx => ctx.sumRev && ctx.signTotal ? Math.round(ctx.sumRev / ctx.signTotal * 100) + '%' : '—', color: 'text-slate-500' },
|
||||
{ label: '毛利', value: ctx => money(ctx.sumGross), color: 'text-green-700' },
|
||||
{ label: '毛利率', value: ctx => ctx.sumRev && ctx.sumGross ? Math.round(ctx.sumGross / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' },
|
||||
{ label: '费用', value: ctx => money(ctx.sumExpense), color: 'text-orange-700' },
|
||||
{ label: '项目费用', value: ctx => money(ctx.sumExpense), color: 'text-orange-700' },
|
||||
{ label: '已回款', value: ctx => moneyWan(ctx.sumPay), color: 'text-amber-700' },
|
||||
{ label: '回款率', value: ctx => ctx.sumRev && ctx.sumPay ? Math.round(ctx.sumPay / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' },
|
||||
{ label: '应付', value: ctx => moneyWan(ctx.sumCost), color: 'text-rose-700' },
|
||||
{ label: '已付', value: ctx => moneyWan(ctx.sumPaid), color: 'text-purple-700' },
|
||||
{ label: '付款率', value: ctx => ctx.sumCost && ctx.sumPaid ? Math.round(ctx.sumPaid / ctx.sumCost * 100) + '%' : '—', color: 'text-slate-500' },
|
||||
{ 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 => { const v = ctx.sumGross - ctx.sumExpense; return { val: moneyWan(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null },
|
||||
{ label: '项目利润', value: ctx => { const v = ctx.sumGross - ctx.sumExpense; return { val: moneyWan(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null },
|
||||
];
|
||||
|
||||
const renderView = (rows, sumRev, sumPay, sumCost, sumPaid, sumGross, sumExpense, signTotal, signCnt, emptyText) => {
|
||||
@@ -264,9 +264,9 @@ function renderFinance() {
|
||||
'<th class="p-2 text-center font-semibold align-middle text-rose-600">应付</th>' +
|
||||
'<th class="p-2 text-center font-semibold align-middle text-purple-600">已付</th>' +
|
||||
'<th class="p-2 text-center font-semibold align-middle">付款率</th>' +
|
||||
'<th class="p-2 text-center font-semibold align-middle text-orange-600">费用</th>' +
|
||||
'<th class="p-2 text-center font-semibold align-middle text-orange-600">项目费用</th>' +
|
||||
'<th class="p-2 text-center font-semibold align-middle text-slate-700">现金流</th>' +
|
||||
'<th class="p-2 text-center font-semibold align-middle">利润</th>' +
|
||||
'<th class="p-2 text-center font-semibold align-middle">项目利润</th>' +
|
||||
'</tr></thead>';
|
||||
const tbody = rows.length ? `<tbody>${rows.join("")}</tbody>` : `<tr><td colspan="15" class="p-6 text-center text-slate-400">${emptyText}</td></tr>`;
|
||||
|
||||
|
||||
@@ -39,12 +39,26 @@ function renderHome() {
|
||||
["上季度累计", moneyInt(m.cost_prev_q || 0)],
|
||||
["上月累计", moneyInt(m.cost_prev_month || 0)],
|
||||
];
|
||||
const rows6 = [
|
||||
["年度累计", moneyInt(m.expense_annual || 0)],
|
||||
["季度累计", moneyInt(m.expense_q2 || 0)],
|
||||
["本月新增", moneyInt(m.expense_month || 0)],
|
||||
["上季度累计", moneyInt(m.expense_prev_q || 0)],
|
||||
["上月累计", moneyInt(m.expense_prev_month || 0)],
|
||||
const rows7 = [
|
||||
["年度累计", moneyInt(m.paid_annual || 0)],
|
||||
["季度累计", moneyInt(m.paid_q2 || 0)],
|
||||
["本月新增", moneyInt(m.paid_month || 0)],
|
||||
["上季度累计", moneyInt(m.paid_prev_q || 0)],
|
||||
["上月累计", moneyInt(m.paid_prev_month || 0)],
|
||||
];
|
||||
const rows8 = [
|
||||
["年度累计", moneyInt(m.cashflow_annual || 0)],
|
||||
["季度累计", moneyInt(m.cashflow_q2 || 0)],
|
||||
["本月新增", moneyInt(m.cashflow_month || 0)],
|
||||
["上季度累计", moneyInt(m.cashflow_prev_q || 0)],
|
||||
["上月累计", moneyInt(m.cashflow_prev_month || 0)],
|
||||
];
|
||||
const rows9 = [
|
||||
["年度累计", moneyInt(m.profit_annual || 0)],
|
||||
["季度累计", moneyInt(m.profit_q2 || 0)],
|
||||
["本月新增", moneyInt(m.profit_month || 0)],
|
||||
["上季度累计", moneyInt(m.profit_prev_q || 0)],
|
||||
["上月累计", moneyInt(m.profit_prev_month || 0)],
|
||||
];
|
||||
const qoq = (cur, prev) => {
|
||||
if (!prev) return '<span class="text-slate-300">—</span>';
|
||||
@@ -53,49 +67,73 @@ function renderHome() {
|
||||
const sign = pct >= 0 ? '+' : '';
|
||||
return '<span class="' + cls + '">' + sign + pct + '%</span>';
|
||||
};
|
||||
const COLORS = ['text-slate-700','text-blue-600','text-green-600','text-amber-600','text-rose-600','text-orange-600'];
|
||||
const LABELS = ['合同金额','确收金额','确收毛利','回款金额','应付金额','费用'];
|
||||
const ALL_ROWS = [rows1, rows2, rows3, rows4, rows5, rows6];
|
||||
// 季度环比:Q 值 vs prev_Q 值;月度环比:month 值 vs prev_month 值
|
||||
const Q_FIELDS = [m.signed_q2||0, m.revenue_q2, m.gross_q2, m.payment_q2||0, m.cost_q2||0, m.expense_q2||0];
|
||||
const Q_PREV = [m.signed_prev_q||0, m.revenue_prev_q||0, m.gross_prev_q||0, m.payment_prev_q||0, m.cost_prev_q||0, m.expense_prev_q||0];
|
||||
const M_FIELDS = [m.signed_month||0, m.monthly_revenue, m.monthly_net_profit, m.payment_month||0, m.cost_month||0, m.expense_month||0];
|
||||
const M_PREV = [m.signed_prev_month||0, m.revenue_prev_month||0, m.gross_prev_month||0, m.payment_prev_month||0, m.cost_prev_month||0, m.expense_prev_month||0];
|
||||
var thead = '<tr class="border-b border-slate-200"><th class="py-2 text-left text-slate-500"></th>';
|
||||
for (var ci = 0; ci < 6; ci++) {
|
||||
thead += '<th class="py-2 text-right font-semibold ' + COLORS[ci] + '">' + LABELS[ci] + '</th><th class="py-2 text-right font-semibold text-slate-400" style="font-size:11px">环比</th>';
|
||||
}
|
||||
thead += '</tr>';
|
||||
const COLORS = ['text-slate-700','text-blue-600','text-green-600','text-amber-600','text-rose-600','text-purple-600','text-cyan-600','text-indigo-600'];
|
||||
const LABELS = ['合同金额','确收金额','确收毛利','回款金额','应付金额','已付','现金流','项目利润'];
|
||||
const Q_FIELDS = [m.signed_q2||0, m.revenue_q2, m.gross_q2, m.payment_q2||0, m.cost_q2||0, m.paid_q2||0, m.cashflow_q2||0, m.profit_q2||0];
|
||||
const Q_PREV = [m.signed_prev_q||0, m.revenue_prev_q||0, m.gross_prev_q||0, m.payment_prev_q||0, m.cost_prev_q||0, m.paid_prev_q||0, m.cashflow_prev_q||0, m.profit_prev_q||0];
|
||||
const M_FIELDS = [m.signed_month||0, m.monthly_revenue, m.monthly_net_profit, m.payment_month||0, m.cost_month||0, m.paid_month||0, m.cashflow_month||0, m.profit_month||0];
|
||||
const M_PREV = [m.signed_prev_month||0, m.revenue_prev_month||0, m.gross_prev_month||0, m.payment_prev_month||0, m.cost_prev_month||0, m.paid_prev_month||0, m.cashflow_prev_month||0, m.profit_prev_month||0];
|
||||
const ROWS = [rows1, rows2, rows3, rows4, rows5, rows7, rows8, rows9];
|
||||
var thead = '<tr class="border-b border-slate-200"><th class="py-2 text-left text-slate-500">指标</th><th class="py-2 text-right font-semibold text-slate-700">年度累计</th><th class="py-2 text-right font-semibold text-slate-700">季度累计</th><th class="py-2 text-right font-semibold text-slate-400">季环比</th><th class="py-2 text-right font-semibold text-slate-700">本月新增</th><th class="py-2 text-right font-semibold text-slate-400">月环比</th><th class="py-2 text-right font-semibold text-slate-700">上季度累计</th><th class="py-2 text-right font-semibold text-slate-700">上月累计</th></tr>';
|
||||
var tbody = '';
|
||||
for (var ri = 0; ri < 5; ri++) {
|
||||
var rowLabel = rows1[ri][0];
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-2 text-slate-500">' + rowLabel + '</td>';
|
||||
for (var ci = 0; ci < 6; ci++) {
|
||||
rowHtml += '<td class="py-2 text-right font-semibold text-slate-800">' + ALL_ROWS[ci][ri][1] + '</td>';
|
||||
var momHtml = '';
|
||||
if (ri === 1) momHtml = qoq(Q_FIELDS[ci], Q_PREV[ci]); // 季度累计行:季度环比
|
||||
else if (ri === 2) momHtml = qoq(M_FIELDS[ci], M_PREV[ci]); // 本月新增行:月度环比
|
||||
else momHtml = '<span class="text-slate-300">—</span>';
|
||||
rowHtml += '<td class="py-2 text-right" style="font-size:12px">' + momHtml + '</td>';
|
||||
}
|
||||
rowHtml += '</tr>';
|
||||
tbody += rowHtml;
|
||||
for (var ri = 0; ri < 8; ri++) {
|
||||
var vals = ROWS[ri];
|
||||
tbody += '<tr class="border-b border-slate-100 last:border-0"><td class="py-2 text-left font-semibold text-slate-700">' + LABELS[ri] + '</td>' +
|
||||
'<td class="py-2 text-right font-semibold text-slate-800">' + vals[0][1] + '</td>' +
|
||||
'<td class="py-2 text-right font-semibold text-slate-800">' + vals[1][1] + '</td>' +
|
||||
'<td class="py-2 text-right">' + qoq(Q_FIELDS[ri], Q_PREV[ri]) + '</td>' +
|
||||
'<td class="py-2 text-right font-semibold text-slate-800">' + vals[2][1] + '</td>' +
|
||||
'<td class="py-2 text-right">' + qoq(M_FIELDS[ri], M_PREV[ri]) + '</td>' +
|
||||
'<td class="py-2 text-right font-semibold text-slate-800">' + vals[3][1] + '</td>' +
|
||||
'<td class="py-2 text-right font-semibold text-slate-800">' + vals[4][1] + '</td></tr>';
|
||||
}
|
||||
document.querySelector("#home").innerHTML = `
|
||||
<div class="grid gap-5">
|
||||
${state.tenant === "总工作台" ? "" : `<div class="grid grid-cols-4 gap-3">
|
||||
${[
|
||||
["经营管理", m.total_projects, "finance"],
|
||||
["项目管理", m.total_projects, "finance"],
|
||||
["重点工作与台账", m.total_proposals, "projects"],
|
||||
["业务方案", m.total_products, "proposals"],
|
||||
["产品迭代", m.upcoming_products, "products"],
|
||||
].map(([label, value, tab]) => `<button class="metric-card" onclick="switchTab('${tab}')"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide="gauge"></i>${label}</span><strong class="mt-2 block text-2xl">${value}</strong></button>`).join("")}
|
||||
</div>`}
|
||||
${card(`<h3 class="text-sm font-bold text-slate-700 mb-3">财务概览</h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>${thead}</thead><tbody>${tbody}</tbody></table></div>`, "p-4")}
|
||||
<div class="grid grid-cols-3 gap-5">
|
||||
${card(`<h3 class="text-sm font-bold text-slate-700 mb-3">部门经营情况</h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="border-b border-slate-200"><th class="py-2 text-left text-slate-500">指标</th><th class="py-2 text-right font-semibold text-slate-700">年度累计</th><th class="py-2 text-right font-semibold text-slate-700">季度累计</th><th class="py-2 text-right font-semibold text-slate-400">季环比</th><th class="py-2 text-right font-semibold text-slate-700">本月新增</th><th class="py-2 text-right font-semibold text-slate-400">月环比</th><th class="py-2 text-right font-semibold text-slate-700">上季度累计</th><th class="py-2 text-right font-semibold text-slate-700">上月累计</th></tr></thead><tbody>
|
||||
${(() => {
|
||||
var d = [
|
||||
['部门利润', [m.profit_annual||0, m.profit_q2||0, m.profit_month||0, m.profit_prev_q||0, m.profit_prev_month||0],
|
||||
[m.profit_q2||0,m.profit_prev_q||0], [m.profit_month||0,m.profit_prev_month||0]],
|
||||
['部门费用', [m.expense_annual||0, m.expense_q2||0, (m.expense_month||0), m.expense_prev_q||0, m.expense_prev_month||0],
|
||||
[m.expense_q2||0,m.expense_prev_q||0], [(m.expense_month||0),m.expense_prev_month||0]],
|
||||
['部门净利', [
|
||||
(m.profit_annual||0)-(m.expense_annual||0), (m.profit_q2||0)-(m.expense_q2||0), (m.profit_month||0)-(m.expense_month||0),
|
||||
(m.profit_prev_q||0)-(m.expense_prev_q||0), (m.profit_prev_month||0)-(m.expense_prev_month||0)
|
||||
], [
|
||||
(m.profit_q2||0)-(m.expense_q2||0), (m.profit_prev_q||0)-(m.expense_prev_q||0)
|
||||
], [
|
||||
(m.profit_month||0)-(m.expense_month||0), (m.profit_prev_month||0)-(m.expense_prev_month||0)
|
||||
]]
|
||||
];
|
||||
var h = '';
|
||||
for (var di = 0; di < 3; di++) {
|
||||
var r = d[di];
|
||||
h += '<tr class="border-b border-slate-100 last:border-0"><td class="py-2 text-left font-semibold text-slate-700">' + r[0] + '</td>' +
|
||||
'<td class="py-2 text-right font-semibold text-slate-800">' + moneyInt(r[1][0]) + '</td>' +
|
||||
'<td class="py-2 text-right font-semibold text-slate-800">' + moneyInt(r[1][1]) + '</td>' +
|
||||
'<td class="py-2 text-right">' + qoq(r[2][0], r[2][1]) + '</td>' +
|
||||
'<td class="py-2 text-right font-semibold text-slate-800">' + moneyInt(r[1][2]) + '</td>' +
|
||||
'<td class="py-2 text-right">' + qoq(r[3][0], r[3][1]) + '</td>' +
|
||||
'<td class="py-2 text-right font-semibold text-slate-800">' + moneyInt(r[1][3]) + '</td>' +
|
||||
'<td class="py-2 text-right font-semibold text-slate-800">' + moneyInt(r[1][4]) + '</td></tr>';
|
||||
}
|
||||
return h;
|
||||
})()}
|
||||
</tbody></table></div>`, "p-4")}
|
||||
${card(`<h3 class="text-sm font-bold text-slate-700 mb-3">业务(项目)财务概览</h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>${thead}</thead><tbody>${tbody}</tbody></table></div>`, "p-4")}
|
||||
<div class="grid grid-cols-4 gap-5">
|
||||
${card(`<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度签约趋势</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="chartSign"></canvas></div>`, "p-4")}
|
||||
${card(`<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度确收与毛利</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="chartRev"></canvas></div>`, "p-4")}
|
||||
${card(`<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度回款与费用</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="chartCash"></canvas></div>`, "p-4")}
|
||||
${card(`<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度回款与已付</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="chartCash"></canvas></div>`, "p-4")}
|
||||
${card(`<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度项目利润</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="chartProfit"></canvas></div>`, "p-4")}
|
||||
</div>
|
||||
${card(`<h2 class="text-lg font-bold">近期动态</h2><div class="mt-4 grid gap-2">${summary.recent.map((r) => `<div class="flex items-start justify-between rounded-md bg-slate-50 px-3 py-2 text-sm group"><span class="break-words">${r.content}</span><div class="flex items-center gap-2 flex-shrink-0 ml-2"><span class="text-xs text-slate-400">${r.followed_at}</span><button class="btn btn-ghost btn-sm text-red-400 opacity-0 group-hover:opacity-100 p-0 w-5 h-5" onclick="event.preventDefault();deleteActivity(${r.id})" title="删除动态"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button></div></div>`).join("")}</div>`, "p-5")}
|
||||
</div>
|
||||
@@ -149,15 +187,28 @@ function renderCharts(data) {
|
||||
});
|
||||
}
|
||||
|
||||
// 图3:月度回款与费用
|
||||
// 图3:月度回款与已付
|
||||
const c3 = document.querySelector("#chartCash");
|
||||
if (c3 && window.Chart) {
|
||||
if (state.chart3) state.chart3.destroy();
|
||||
state.chart3 = new Chart(c3, {
|
||||
type: "bar",
|
||||
type: "line",
|
||||
data: { labels, datasets: [
|
||||
{ label: "回款", data: data.map((x) => x.payment || 0), backgroundColor: "#d97706", borderRadius: 4 },
|
||||
{ label: "费用", data: data.map((x) => x.cost || 0), backgroundColor: "#ef4444", borderRadius: 4 },
|
||||
{ label: "回款", data: data.map((x) => x.payment || 0), borderColor: "#d97706", backgroundColor: "rgba(217,119,6,0.06)", fill: true, tension: 0.3 },
|
||||
{ label: "已付", data: data.map((x) => x.cost || 0), borderColor: "#7c3aed", backgroundColor: "rgba(124,58,237,0.06)", fill: true, tension: 0.3 },
|
||||
]},
|
||||
options: baseOpts,
|
||||
});
|
||||
}
|
||||
|
||||
// 图4:月度项目利润
|
||||
const c4 = document.querySelector("#chartProfit");
|
||||
if (c4 && window.Chart) {
|
||||
if (state.chart4) state.chart4.destroy();
|
||||
state.chart4 = new Chart(c4, {
|
||||
type: "line",
|
||||
data: { labels, datasets: [
|
||||
{ label: "利润", data: data.map((x) => (x.gross || 0)), borderColor: "#6366f1", backgroundColor: "rgba(99,102,241,0.06)", fill: true, tension: 0.3 },
|
||||
]},
|
||||
options: baseOpts,
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ const state = {
|
||||
chart: null,
|
||||
chart2: null,
|
||||
chart3: null,
|
||||
chart4: null,
|
||||
productPlatform: "all",
|
||||
uploadTasks: [],
|
||||
expenseFilter: "全部",
|
||||
|
||||
Reference in New Issue
Block a user