// home.js — 首页渲染 + 财务趋势图
function renderHome() {
const { summary, financeMonthly } = state.data;
const m = summary.metrics;
const moneyInt = (v) => `${Math.round(Number(v || 0)).toLocaleString("zh-CN")} 元`;
const rows1 = [
["年度累计", moneyInt(m.signed_annual || m.signed_amount)],
["季度累计", moneyInt(m.signed_q2 || 0)],
["本月新增", moneyInt(m.signed_month || 0)],
["上季度累计", moneyInt(m.signed_prev_q || 0)],
["上月累计", moneyInt(m.signed_prev_month || 0)],
];
const rows2 = [
["年度累计", moneyInt(m.revenue_annual)],
["季度累计", moneyInt(m.revenue_q2)],
["本月新增", moneyInt(m.monthly_revenue)],
["上季度累计", moneyInt(m.revenue_prev_q || 0)],
["上月累计", moneyInt(m.revenue_prev_month || 0)],
];
const rows3 = [
["年度累计", moneyInt(m.gross_annual)],
["季度累计", moneyInt(m.gross_q2)],
["本月新增", moneyInt(m.monthly_net_profit)],
["上季度累计", moneyInt(m.gross_prev_q || 0)],
["上月累计", moneyInt(m.gross_prev_month || 0)],
];
const rows4 = [
["年度累计", moneyInt(m.payment_annual || 0)],
["季度累计", moneyInt(m.payment_q2 || 0)],
["本月新增", moneyInt(m.payment_month || 0)],
["上季度累计", moneyInt(m.payment_prev_q || 0)],
["上月累计", moneyInt(m.payment_prev_month || 0)],
];
const rows5 = [
["年度累计", moneyInt(m.cost_annual || 0)],
["季度累计", moneyInt(m.cost_q2 || 0)],
["本月新增", moneyInt(m.cost_month || 0)],
["上季度累计", moneyInt(m.cost_prev_q || 0)],
["上月累计", moneyInt(m.cost_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 '—';
const pct = Math.round((cur - prev) / prev * 100);
const cls = pct >= 0 ? 'text-green-600' : 'text-red-600';
const sign = pct >= 0 ? '+' : '';
return '' + sign + pct + '%';
};
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 = '
| 指标 | 年度累计 | 季度累计 | 季环比 | 本月新增 | 月环比 | 上季度累计 | 上月累计 |
';
var tbody = '';
for (var ri = 0; ri < 8; ri++) {
var vals = ROWS[ri];
tbody += '| ' + LABELS[ri] + ' | ' +
'' + vals[0][1] + ' | ' +
'' + vals[1][1] + ' | ' +
'' + qoq(Q_FIELDS[ri], Q_PREV[ri]) + ' | ' +
'' + vals[2][1] + ' | ' +
'' + qoq(M_FIELDS[ri], M_PREV[ri]) + ' | ' +
'' + vals[3][1] + ' | ' +
'' + vals[4][1] + ' |
';
}
document.querySelector("#home").innerHTML = `
${state.tenant === "总工作台" ? "" : `
${[
["项目管理", m.total_projects, "finance"],
["重点工作与台账", m.total_proposals, "projects"],
["业务方案", m.total_products, "proposals"],
["产品迭代", m.upcoming_products, "products"],
].map(([label, value, tab]) => ``).join("")}
`}
${card(`
部门经营情况
| 指标 | 年度累计 | 季度累计 | 季环比 | 本月新增 | 月环比 | 上季度累计 | 上月累计 |
${(() => {
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 += '| ' + r[0] + ' | ' +
'' + moneyInt(r[1][0]) + ' | ' +
'' + moneyInt(r[1][1]) + ' | ' +
'' + qoq(r[2][0], r[2][1]) + ' | ' +
'' + moneyInt(r[1][2]) + ' | ' +
'' + qoq(r[3][0], r[3][1]) + ' | ' +
'' + moneyInt(r[1][3]) + ' | ' +
'' + moneyInt(r[1][4]) + ' |
';
}
return h;
})()}
`, "p-4")}
${card(`
业务(项目)财务概览
`, "p-4")}
${card(`
月度签约趋势
2026`, "p-4")}
${card(`
月度确收与毛利
2026`, "p-4")}
${card(`
月度回款与已付
2026`, "p-4")}
${card(`
月度项目利润
2026`, "p-4")}
${card(`
近期动态
${summary.recent.map((r) => `
${r.content}${r.followed_at}
`).join("")}
`, "p-5")}
`;
renderCharts(financeMonthly);
}
function chartOptions(yCallback) {
return {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: "bottom", labels: { boxWidth: 12, font: { size: 11 } } } },
scales: {
x: { ticks: { font: { size: 10 } }, grid: { display: false } },
y: { ticks: { font: { size: 11 }, callback: yCallback } },
},
};
}
const moneyTick = (v) => v >= 10000 ? (v / 10000).toFixed(0) + "万" : v;
const monthLabels = (data) => data.map((x) => parseInt(x.month.split("-")[1]) + "月");
function renderCharts(data) {
const labels = monthLabels(data);
const baseOpts = chartOptions(moneyTick);
// 图1:月度签约
const c1 = document.querySelector("#chartSign");
if (c1 && window.Chart) {
if (state.chart) state.chart.destroy();
state.chart = new Chart(c1, {
type: "line",
data: { labels, datasets: [
{ label: "签约金额", data: data.map((x) => x.sign || 0), borderColor: "#6366f1", backgroundColor: "rgba(99,102,241,0.06)", fill: true, tension: 0.3 },
]},
options: baseOpts,
});
}
// 图2:月度确收与毛利
const c2 = document.querySelector("#chartRev");
if (c2 && window.Chart) {
if (state.chart2) state.chart2.destroy();
state.chart2 = new Chart(c2, {
type: "line",
data: { labels, datasets: [
{ label: "确收", data: data.map((x) => x.revenue || 0), borderColor: "#2563eb", backgroundColor: "rgba(37,99,235,0.06)", fill: true, tension: 0.3 },
{ label: "毛利", data: data.map((x) => x.gross || 0), borderColor: "#059669", backgroundColor: "rgba(5,150,105,0.06)", fill: true, tension: 0.3 },
]},
options: baseOpts,
});
}
// 图3:月度回款与已付
const c3 = document.querySelector("#chartCash");
if (c3 && window.Chart) {
if (state.chart3) state.chart3.destroy();
state.chart3 = new Chart(c3, {
type: "line",
data: { labels, datasets: [
{ 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,
});
}
}