diff --git a/backend/routes.py b/backend/routes.py index 2d06019..20e21e4 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -247,6 +247,7 @@ def bootstrap(): t_ops = attach_common(conn, "operations", rows(conn, "SELECT * FROM operation_projects WHERE tenant=? ORDER BY id ASC", [t])) t_sales = attach_common(conn, "sales", rows(conn, "SELECT * FROM sales_leads WHERE tenant=? ORDER BY id DESC", [t])) t_products = attach_common(conn, "products", rows(conn, "SELECT * FROM product_versions WHERE tenant=? ORDER BY id DESC", [t])) + t_expense = rows(conn, "SELECT * FROM expense_records WHERE tenant=?", [t]) t_proposals = attach_common(conn, "proposals", rows(conn, "SELECT * FROM business_proposals WHERE tenant=? ORDER BY id DESC", [t])) t_signed_pfs = [x for x in t_pfs if x["status"] == "已签约"] def t_parse_budget(pf): @@ -305,10 +306,26 @@ def bootstrap(): "cost_prev_q": t_sum_budget("cost", _prev_q_range), "cost_prev_month": t_sum_budget("cost", [_prev_month]) if _prev_month else 0, }) + # 费用汇总(从 expense_records 表计算) + def t_expense_sum(m_range): + total = 0 + for r in t_expense: + m = (r.get("expense_month") or "").strip() + if not m or "-" not in m: continue + try: + if int(m.split("-")[1]) in m_range: + total += float(r.get("amount") or 0) + except: pass + return total + all_metrics[-1]["expense_annual"] = t_expense_sum(range(1, 13)) + all_metrics[-1]["expense_q2"] = t_expense_sum(_q_range) + all_metrics[-1]["expense_month"] = t_expense_sum([_now_month]) + all_metrics[-1]["expense_prev_q"] = t_expense_sum(_prev_q_range) + all_metrics[-1]["expense_prev_month"] = t_expense_sum([_prev_month]) if _prev_month else 0 all_monthly.append(monthly_finance(conn, t)) all_recent.extend(rows(conn, "SELECT * FROM follow_up_records WHERE tenant=? ORDER BY id DESC LIMIT 4", [t])) agg = {} - for key in ["total_projects","total_proposals","total_products","upcoming_products","signed_amount","signed_annual","signed_q2","signed_month","signed_prev_q","signed_prev_month","revenue_annual","revenue_q2","monthly_revenue","revenue_prev_q","revenue_prev_month","gross_annual","gross_q2","monthly_net_profit","gross_prev_q","gross_prev_month","payment_annual","payment_q2","payment_month","payment_prev_q","payment_prev_month","cost_annual","cost_q2","cost_month","cost_prev_q","cost_prev_month"]: + for key in ["total_projects","total_proposals","total_products","upcoming_products","signed_amount","signed_annual","signed_q2","signed_month","signed_prev_q","signed_prev_month","revenue_annual","revenue_q2","monthly_revenue","revenue_prev_q","revenue_prev_month","gross_annual","gross_q2","monthly_net_profit","gross_prev_q","gross_prev_month","payment_annual","payment_q2","payment_month","payment_prev_q","payment_prev_month","cost_annual","cost_q2","cost_month","cost_prev_q","cost_prev_month","expense_annual","expense_q2","expense_month","expense_prev_q","expense_prev_month"]: agg[key] = sum(m.get(key, 0) for m in all_metrics) merged_monthly = [] for i in range(12): @@ -380,6 +397,21 @@ def bootstrap(): cost_prev_q = sum_budget("cost", _prev_q_range) payment_prev_month = sum_budget("payment", [_prev_month]) if _prev_month else 0 cost_prev_month = sum_budget("cost", [_prev_month]) if _prev_month else 0 + def expense_sum(month_range): + total = 0 + for r in expense: + m = (r.get("expense_month") or "").strip() + if not m or "-" not in m: continue + try: + if int(m.split("-")[1]) in month_range: + total += float(r.get("amount") or 0) + except: pass + return total + expense_annual = expense_sum(range(1, 13)) + expense_q2 = expense_sum(_q_range) + expense_month_val = expense_sum([_now_month]) + expense_prev_q = expense_sum(_prev_q_range) + expense_prev_month = expense_sum([_prev_month]) if _prev_month else 0 def pf_status_sum(status): return sum(x["sign_amount"] or 0 for x in pfs if x["status"] == status) signed_amount = pf_status_sum("已签约") @@ -431,6 +463,11 @@ def bootstrap(): "cost_month": cost_month, "cost_prev_q": cost_prev_q, "cost_prev_month": cost_prev_month, + "expense_annual": expense_annual, + "expense_q2": expense_q2, + "expense_month": expense_month_val, + "expense_prev_q": expense_prev_q, + "expense_prev_month": expense_prev_month, "signed_not_executed": signed_not_executed, }, "recent": q("SELECT * FROM follow_up_records WHERE tenant=? ORDER BY id DESC LIMIT 8", tenant), diff --git a/static/modules/home.js b/static/modules/home.js index c36426b..3b7c039 100644 --- a/static/modules/home.js +++ b/static/modules/home.js @@ -39,24 +39,48 @@ function renderHome() { ["上季度累计", moneyInt(m.cost_prev_q || 0)], ["上月累计", moneyInt(m.cost_prev_month || 0)], ]; -let _cardId = 0; -const tblCard = (title, rows) => { - const id = 'finCard' + (++_cardId); - const visible = rows.slice(0, 3); - const extra = rows.slice(3); - const rowHtml = (r) => r.map(([label, value]) => `${label}${value}`).join(""); - return card(`

${title}

${rowHtml(visible)}
${rowHtml(extra)}`, "p-4"); -}; -window.toggleFinCard = (id) => { - const t = document.getElementById(id); - if (!t) return; - t.classList.toggle("hidden"); - const btn = document.getElementById(id + "_btn"); - btn.innerHTML = t.classList.contains("hidden") - ? '' - : ''; - if (window.lucide) window.lucide.createIcons(); -}; + 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 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-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 = ''; + for (var ci = 0; ci < 6; ci++) { + thead += '' + LABELS[ci] + '环比'; + } + thead += ''; + var tbody = ''; + for (var ri = 0; ri < 5; ri++) { + var rowLabel = rows1[ri][0]; + var rowHtml = '' + rowLabel + ''; + for (var ci = 0; ci < 6; ci++) { + rowHtml += '' + ALL_ROWS[ci][ri][1] + ''; + 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 = ''; + rowHtml += '' + momHtml + ''; + } + rowHtml += ''; + tbody += rowHtml; + } document.querySelector("#home").innerHTML = `
${state.tenant === "总工作台" ? "" : `
@@ -67,7 +91,7 @@ window.toggleFinCard = (id) => { ["产品迭代", m.upcoming_products, "products"], ].map(([label, value, tab]) => ``).join("")}
`} -
${tblCard("合同金额", rows1)}${tblCard("确收金额", rows2)}${tblCard("确收毛利", rows3)}${tblCard("回款金额", rows4)}${tblCard("应付金额", rows5)}
+ ${card(`

财务概览

${thead}${tbody}
`, "p-4")}
${card(`

月度签约趋势

2026
`, "p-4")} ${card(`

月度确收与毛利

2026
`, "p-4")}