From 356c129668d2afc47c38ae746b1be9f7f53a658e Mon Sep 17 00:00:00 2001 From: mac Date: Sat, 11 Jul 2026 14:36:00 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=80=BB=E8=A7=88=E5=B7=A5=E4=BD=9C?= =?UTF-8?q?=E5=8F=B0=E6=96=B0=E5=A2=9E=E6=8D=9F=E7=9B=8A/=E7=8E=B0?= =?UTF-8?q?=E9=87=91=E6=B5=81/=E9=A1=B9=E7=9B=AE=E7=BB=8F=E8=90=A5?= =?UTF-8?q?=E8=AF=A6=E7=BB=86=E6=8A=A5=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 预算总览和发生总览各新增3个tab: 损益详细报表、现金流详细报表、项目经营详细报表 - 12个月卡片布局, 按部门(学术/科普/科研/医患/MCN)展示矩阵 - 后端新增6个聚合API: dept-matrix/cf-matrix/project-matrix x 2 - tab改为finance-tab风格, 与经营月报一致 - 修复项目报表函数未注册导致的加载失败 - 版本号 v1.2.0.26 --- backend/routes.py | 353 +++++++++++++++++++++++++++++ static/modules/overview_finance.js | 193 ++++++++++++++-- static/modules/overview_plan.js | 184 ++++++++++++++- templates/index.html | 2 +- 4 files changed, 696 insertions(+), 36 deletions(-) diff --git a/backend/routes.py b/backend/routes.py index c8991b8..a767c8a 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -724,6 +724,359 @@ def delete_file(file_id): conn.close() + +# ---------- 总览部门损益矩阵 API ---------- + +@bp.route("/api/overview/dept-matrix") +@login_required +def overview_dept_matrix(): + """返回各工作台的损益数据矩阵(按年/季/月筛选)""" + conn = db() + _year = int(request.args.get("year", date.today().year)) + view = request.args.get("view", "monthly") + + # 12个月数组 + months = [f"{_year}-{m:02d}" for m in range(1, 13)] + + tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"] + result = {"tenants": tenants, "months": months, "data": {}, "total": {"project_gross": [0]*12, "dept_expense": [0]*12, "profit": [0]*12}} + + for tenant in tenants: + # 项目毛利(从 budget_data JSON) + pfs = rows(conn, "SELECT budget_data FROM plan_finances WHERE tenant=?", [tenant]) + gross = [0]*12 + for pf in pfs: + try: + bd = json.loads(pf.get("budget_data") or "[]") + except: + bd = [] + for b in bd: + m = (b.get("month") or "")[:7] + if m in months: + i = months.index(m) + gross[i] += float(b.get("gross") or 0) + + # 部门费用(从 plan_expense_records) + expense = [0]*12 + exps = rows(conn, "SELECT expense_month, amount FROM plan_expense_records WHERE tenant=? AND expense_month LIKE %s", [tenant, f"{_year}-%"]) + for e in exps: + m = (e.get("expense_month") or "")[:7] + if m in months: + i = months.index(m) + expense[i] += float(e.get("amount") or 0) + + profit = [gross[i] - expense[i] for i in range(12)] + + result["data"][tenant] = { + "project_gross": gross, + "dept_expense": expense, + "profit": profit + } + + for i in range(12): + result["total"]["project_gross"][i] += gross[i] + result["total"]["dept_expense"][i] += expense[i] + result["total"]["profit"][i] += profit[i] + + # 按视图聚合 + if view == "quarterly": + def to_quarterly(arr12): + qr = [(0,1,2),(3,4,5),(6,7,8),(9,10,11)] + return [arr12[q[0]]+arr12[q[1]]+arr12[q[2]] for q in qr] + result["months"] = ["Q1","Q2","Q3","Q4"] + for t in tenants: + for key in ["project_gross","dept_expense","profit"]: + result["data"][t][key] = to_quarterly(result["data"][t][key]) + for key in ["project_gross","dept_expense","profit"]: + result["total"][key] = to_quarterly(result["total"][key]) + elif view == "monthly": + sel_month = request.args.get("month", "") + if sel_month and sel_month in months: + mi = months.index(sel_month) + result["months"] = [sel_month] + for t in tenants: + for key in ["project_gross","dept_expense","profit"]: + result["data"][t][key] = [result["data"][t][key][mi]] + for key in ["project_gross","dept_expense","profit"]: + result["total"][key] = [result["total"][key][mi]] + # else: yearly (default) — keep all 12 months + + # Compute annual totals + result["annual"] = {} + for t in tenants: + d = result["data"][t] + result["annual"][t] = { + "project_gross": sum(d["project_gross"]), + "dept_expense": sum(d["dept_expense"]), + "profit": sum(d["profit"]) + } + result["annual"]["total"] = { + "project_gross": sum(result["total"]["project_gross"]), + "dept_expense": sum(result["total"]["dept_expense"]), + "profit": sum(result["total"]["profit"]) + } + + conn.close() + return jsonify(result) + +@bp.route("/api/overview/finance-dept-matrix") +@login_required +def overview_finance_dept_matrix(): + """返回各工作台已发生损益数据矩阵""" + conn = db() + _year = date.today().year + months = [f"{_year}-{m:02d}" for m in range(1, 13)] + tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"] + result = {"tenants": tenants, "months": months, "data": {}, "total": {"project_gross": [0]*12, "dept_expense": [0]*12, "profit": [0]*12}} + + for tenant in tenants: + pfs = rows(conn, "SELECT budget_data FROM project_finances WHERE tenant=?", [tenant]) + gross = [0]*12 + for pf in pfs: + try: + bd = json.loads(pf.get("budget_data") or "[]") + except: + bd = [] + for b in bd: + m = (b.get("month") or "")[:7] + if m in months: + i = months.index(m) + gross[i] += float(b.get("gross") or 0) + + expense = [0]*12 + exps = rows(conn, "SELECT expense_month, amount FROM expense_records WHERE tenant=? AND expense_month LIKE %s", [tenant, f"{_year}-%"]) + for e in exps: + m = (e.get("expense_month") or "")[:7] + if m in months: + i = months.index(m) + expense[i] += float(e.get("amount") or 0) + + profit = [gross[i] - expense[i] for i in range(12)] + result["data"][tenant] = {"project_gross": gross, "dept_expense": expense, "profit": profit} + for i in range(12): + result["total"]["project_gross"][i] += gross[i] + result["total"]["dept_expense"][i] += expense[i] + result["total"]["profit"][i] += profit[i] + + result["annual"] = {} + for t in tenants: + d = result["data"][t] + result["annual"][t] = {"project_gross": sum(d["project_gross"]), "dept_expense": sum(d["dept_expense"]), "profit": sum(d["profit"])} + result["annual"]["total"] = {"project_gross": sum(result["total"]["project_gross"]), "dept_expense": sum(result["total"]["dept_expense"]), "profit": sum(result["total"]["profit"])} + + conn.close() + return jsonify(result) + + +@bp.route("/api/overview/dept-cf-matrix") +@login_required +def overview_dept_cf_matrix(): + """返回各工作台预算现金流数据矩阵""" + conn = db() + _year = date.today().year + months = [f"{_year}-{m:02d}" for m in range(1, 13)] + tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"] + result = {"tenants": tenants, "months": months, "data": {}, "total": {"project_cf": [0]*12, "dept_cf": [0]*12, "bl_cf": [0]*12}} + + for tenant in tenants: + pfs = rows(conn, "SELECT budget_data, expense_data FROM plan_finances WHERE tenant=?", [tenant]) + project_cf = [0]*12 + for pf in pfs: + try: bd = json.loads(pf.get("budget_data") or "[]") + except: bd = [] + try: ed = json.loads(pf.get("expense_data") or "[]") + except: ed = [] + for b in bd: + m = (b.get("month") or "")[:7] + if m in months: + i = months.index(m) + project_cf[i] += float(b.get("payment") or 0) + for e in ed: + m = (e.get("month") or "")[:7] + if m in months: + i = months.index(m) + project_cf[i] -= float(e.get("paid") or 0) + + dept_cf = [0]*12 + exps = rows(conn, "SELECT expense_month, incurred_amount FROM plan_expense_records WHERE tenant=? AND expense_month LIKE %s", [tenant, f"{_year}-%"]) + for e in exps: + m = (e.get("expense_month") or "")[:7] + if m in months: + i = months.index(m) + dept_cf[i] += float(e.get("incurred_amount") or 0) + + bl_cf = [project_cf[i] - dept_cf[i] for i in range(12)] + result["data"][tenant] = {"project_cf": project_cf, "dept_cf": dept_cf, "bl_cf": bl_cf} + for i in range(12): + result["total"]["project_cf"][i] += project_cf[i] + result["total"]["dept_cf"][i] += dept_cf[i] + result["total"]["bl_cf"][i] += bl_cf[i] + + result["annual"] = {} + for t in tenants: + d = result["data"][t] + result["annual"][t] = {"project_cf": sum(d["project_cf"]), "dept_cf": sum(d["dept_cf"]), "bl_cf": sum(d["bl_cf"])} + result["annual"]["total"] = {"project_cf": sum(result["total"]["project_cf"]), "dept_cf": sum(result["total"]["dept_cf"]), "bl_cf": sum(result["total"]["bl_cf"])} + + conn.close() + return jsonify(result) + + +@bp.route("/api/overview/finance-dept-cf-matrix") +@login_required +def overview_finance_dept_cf_matrix(): + """返回各工作台已发生现金流数据矩阵""" + conn = db() + _year = date.today().year + months = [f"{_year}-{m:02d}" for m in range(1, 13)] + tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"] + result = {"tenants": tenants, "months": months, "data": {}, "total": {"project_cf": [0]*12, "dept_cf": [0]*12, "bl_cf": [0]*12}} + + for tenant in tenants: + pfs = rows(conn, "SELECT budget_data, expense_data FROM project_finances WHERE tenant=?", [tenant]) + project_cf = [0]*12 + for pf in pfs: + try: bd = json.loads(pf.get("budget_data") or "[]") + except: bd = [] + try: ed = json.loads(pf.get("expense_data") or "[]") + except: ed = [] + for b in bd: + m = (b.get("month") or "")[:7] + if m in months: + i = months.index(m) + project_cf[i] += float(b.get("payment") or 0) + for e in ed: + m = (e.get("month") or "")[:7] + if m in months: + i = months.index(m) + project_cf[i] -= float(e.get("paid") or 0) + + dept_cf = [0]*12 + exps = rows(conn, "SELECT expense_month, incurred_amount FROM expense_records WHERE tenant=? AND expense_month LIKE %s", [tenant, f"{_year}-%"]) + for e in exps: + m = (e.get("expense_month") or "")[:7] + if m in months: + i = months.index(m) + dept_cf[i] += float(e.get("incurred_amount") or 0) + + bl_cf = [project_cf[i] - dept_cf[i] for i in range(12)] + result["data"][tenant] = {"project_cf": project_cf, "dept_cf": dept_cf, "bl_cf": bl_cf} + for i in range(12): + result["total"]["project_cf"][i] += project_cf[i] + result["total"]["dept_cf"][i] += dept_cf[i] + result["total"]["bl_cf"][i] += bl_cf[i] + + result["annual"] = {} + for t in tenants: + d = result["data"][t] + result["annual"][t] = {"project_cf": sum(d["project_cf"]), "dept_cf": sum(d["dept_cf"]), "bl_cf": sum(d["bl_cf"])} + result["annual"]["total"] = {"project_cf": sum(result["total"]["project_cf"]), "dept_cf": sum(result["total"]["dept_cf"]), "bl_cf": sum(result["total"]["bl_cf"])} + + conn.close() + return jsonify(result) + + +@bp.route("/api/overview/dept-project-matrix") +@login_required +def overview_dept_project_matrix(): + """返回各工作台预算项目经营详情矩阵""" + conn = db() + _year = date.today().year + months = [f"{_year}-{m:02d}" for m in range(1, 13)] + tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"] + keys = ["sign","revenue","gross","cost","paid","payment","cashflow"] + result = {"tenants": tenants, "months": months, "data": {}, "total": {k: [0]*12 for k in keys}} + + for tenant in tenants: + tdata = {k: [0]*12 for k in keys} + pfs = rows(conn, "SELECT sign_amount, sign_month, budget_data, expense_data FROM plan_finances WHERE tenant=?", [tenant]) + for pf in pfs: + sm = (pf.get("sign_month") or "")[:7] + if sm in months: + i = months.index(sm) + tdata["sign"][i] += float(pf.get("sign_amount") or 0) + try: bd = json.loads(pf.get("budget_data") or "[]") + except: bd = [] + try: ed = json.loads(pf.get("expense_data") or "[]") + except: ed = [] + for b in bd: + m = (b.get("month") or "")[:7] + if m in months: + i = months.index(m) + tdata["revenue"][i] += float(b.get("rev") or 0) + tdata["gross"][i] += float(b.get("gross") or 0) + tdata["payment"][i] += float(b.get("payment") or 0) + for e in ed: + m = (e.get("month") or "")[:7] + if m in months: + i = months.index(m) + tdata["cost"][i] += float(e.get("cost") or 0) + tdata["paid"][i] += float(e.get("paid") or 0) + for i in range(12): + tdata["cashflow"][i] = tdata["payment"][i] - tdata["paid"][i] + result["data"][tenant] = tdata + for k in keys: + for i in range(12): + result["total"][k][i] += tdata[k][i] + + result["annual"] = {} + for t in tenants: + result["annual"][t] = {k: sum(result["data"][t][k]) for k in keys} + result["annual"]["total"] = {k: sum(result["total"][k]) for k in keys} + conn.close() + return jsonify(result) + + +@bp.route("/api/overview/finance-dept-project-matrix") +@login_required +def overview_finance_dept_project_matrix(): + """返回各工作台已发生项目经营详情矩阵""" + conn = db() + _year = date.today().year + months = [f"{_year}-{m:02d}" for m in range(1, 13)] + tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"] + keys = ["sign","revenue","gross","cost","paid","payment","cashflow"] + result = {"tenants": tenants, "months": months, "data": {}, "total": {k: [0]*12 for k in keys}} + + for tenant in tenants: + tdata = {k: [0]*12 for k in keys} + pfs = rows(conn, "SELECT sign_amount, sign_month, budget_data, expense_data FROM project_finances WHERE tenant=?", [tenant]) + for pf in pfs: + sm = (pf.get("sign_month") or "")[:7] + if sm in months: + i = months.index(sm) + tdata["sign"][i] += float(pf.get("sign_amount") or 0) + try: bd = json.loads(pf.get("budget_data") or "[]") + except: bd = [] + try: ed = json.loads(pf.get("expense_data") or "[]") + except: ed = [] + for b in bd: + m = (b.get("month") or "")[:7] + if m in months: + i = months.index(m) + tdata["revenue"][i] += float(b.get("rev") or 0) + tdata["gross"][i] += float(b.get("gross") or 0) + tdata["payment"][i] += float(b.get("payment") or 0) + for e in ed: + m = (e.get("month") or "")[:7] + if m in months: + i = months.index(m) + tdata["cost"][i] += float(e.get("cost") or 0) + tdata["paid"][i] += float(e.get("paid") or 0) + for i in range(12): + tdata["cashflow"][i] = tdata["payment"][i] - tdata["paid"][i] + result["data"][tenant] = tdata + for k in keys: + for i in range(12): + result["total"][k][i] += tdata[k][i] + + result["annual"] = {} + for t in tenants: + result["annual"][t] = {k: sum(result["data"][t][k]) for k in keys} + result["annual"]["total"] = {k: sum(result["total"][k]) for k in keys} + conn.close() + return jsonify(result) + # ---------- health ---------- @bp.route("/api/health") diff --git a/static/modules/overview_finance.js b/static/modules/overview_finance.js index 3f21807..6c7030f 100644 --- a/static/modules/overview_finance.js +++ b/static/modules/overview_finance.js @@ -1,38 +1,105 @@ // overview_finance.js — 发生总览模块(5 个工作台数据聚合) function renderOverviewFinance() { - const view = state.overviewView || "monthly"; - const api = view === "quarterly" ? "/api/overview/finance-quarterly" : "/api/overview/finance-monthly"; - const container = document.querySelector("#overview_finance"); + var view = state.overviewView || "monthly"; + if (view === 'matrix') { renderFinanceDeptMatrix(); return; } + if (view === 'cf_matrix') { renderFinanceCfMatrix(); return; } + if (view === 'project_matrix') { renderFinanceProjectMatrix(); return; } + + var api = view === "quarterly" ? "/api/overview/finance-quarterly" : "/api/overview/finance-monthly"; + var container = document.querySelector("#overview_finance"); if (!container) return; container.innerHTML = '
加载中...
'; fetch(api).then(r => r.json()).then(data => { + if (state.overviewView !== view) return; renderOverviewFinanceData(data, view); }).catch(() => { + if (state.overviewView !== view) return; container.innerHTML = '
加载失败
'; }); } +// ====== 项目经营详情报表(顶部定义,避免调用时未注册) ====== + +function renderFinanceProjectMatrix() { + var c = document.querySelector("#overview_finance"); + if (!c) return; + c.innerHTML = '
加载项目经营详情矩阵...
'; + fetch('/api/overview/finance-dept-project-matrix').then(function(r) { + if (!r.ok) throw new Error('HTTP ' + r.status); + return r.json(); + }).then(function(data) { + renderFinanceProjectMatrixData(data, c); + }).catch(function(e) { + c.innerHTML = '
加载失败: ' + (e.message || '未知错误') + '
'; + }); +} + +function renderFinanceProjectMatrixData(data, container) { + try { + var mf = function(v) { return Math.round(Number(v || 0)).toLocaleString("zh-CN"); }; + var tenants = data.tenants; + var sn = tenants.map(function(t) { return t.replace('·无界', ''); }); + var mL = ['01月','02月','03月','04月','05月','06月','07月','08月','09月','10月','11月','12月']; + var keys = ["sign","revenue","gross","cost","paid","payment","cashflow"]; + var labels = ["合同金额","确收金额","确收毛利","当期成本","当期流出","回款","项目现金流"]; + + var vBtns = financeBtnBar('project_matrix'); + var cards = ''; + for (var mi = 0; mi < 12; mi++) { + var th = '指标'; + for (var ti = 0; ti < tenants.length; ti++) { th += '' + sn[ti] + ''; } + th += '合计'; + var tb = financeProjectMatrixTbody(data, tenants, mi, keys, labels, mf); + cards += '
' + mL[mi] + '
' + th + '' + tb + '
'; + } + container.innerHTML = '
' + vBtns + '
' + cards + '
'; + if (window.lucide) window.lucide.createIcons(); + } catch(e) { container.innerHTML = '
渲染失败: ' + (e.message || '未知错误') + '
'; } +} + +function financeProjectMatrixTbody(data, tenants, mi, keys, labels, mf) { + var tb = ''; + for (var ri = 0; ri < keys.length; ri++) { + var rowCls = ri === keys.length - 1 ? 'font-semibold' : ''; + var rh = '' + labels[ri] + ''; + var ts = 0; + for (var ti = 0; ti < tenants.length; ti++) { + var v = data.data[tenants[ti]][keys[ri]][mi] || 0; + ts += v; + var cls = 'text-slate-700'; + if (ri === 2) cls = 'text-green-600'; + if (ri === keys.length - 1) cls = v >= 0 ? 'text-green-600' : 'text-red-600'; + rh += '' + mf(v) + ''; + } + var tCls = 'text-slate-700'; + if (ri === 2) tCls = 'text-green-600'; + if (ri === keys.length - 1) tCls = ts >= 0 ? 'text-green-600' : 'text-red-600'; + rh += '' + mf(ts) + ''; + tb += rh; + } + return tb; +} + function renderOverviewFinanceData(data, view) { const container = document.querySelector("#overview_finance"); if (!container) return; - const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN"); const card = (body, cls) => '
' + body + '
'; - const isQuarterly = view === "quarterly"; const headers = isQuarterly ? ['Q1','Q2','Q3','Q4'] : ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月']; const cols = isQuarterly ? 4 : 12; + const d = data.dept, cf = data.dept_cf, p = data.project; - const d = data.dept; - const cf = data.dept_cf; - const p = data.project; + var btns = []; + btns.push(''); + btns.push(''); + btns.push(''); + btns.push(''); + btns.push(''); + var viewBtns = '
' + btns.join('') + '
'; - // 视图切换按钮 - var viewBtns = '
'; - - // ====== 部门损益情况 ====== var deptThead = '指标年度累计'; for (var hi = 0; hi < cols; hi++) { deptThead += '' + headers[hi] + ''; } deptThead += ''; @@ -44,8 +111,7 @@ function renderOverviewFinanceData(data, view) { ]; var deptTbody = ''; for (var dri = 0; dri < 3; dri++) { - var dr = deptRows[dri]; - var rowCls = dri === 2 ? 'font-semibold' : ''; + var dr = deptRows[dri], rowCls = dri === 2 ? 'font-semibold' : ''; var rowHtml = '' + dr.label + ''; var annualCls = dri === 2 ? (dr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700'; rowHtml += '' + moneyIntL(dr.annual) + ''; @@ -57,7 +123,6 @@ function renderOverviewFinanceData(data, view) { } var deptCard = card('

部门损益情况 部门利润 = 所有项目产生的毛利 - 部门 1 级产生的人力成本,研发成本,差旅和其他支出

' + deptThead + '' + deptTbody + '
', 'p-4'); - // ====== 部门现金流 ====== var cfRows = [ { label: '项目现金流', vals: cf.project_cf, annual: cf.annual[0] }, { label: '部门现金流', vals: cf.dept_cf_item, annual: cf.annual[1] }, @@ -65,8 +130,7 @@ function renderOverviewFinanceData(data, view) { ]; var cfTbody = ''; for (var cfri = 0; cfri < 3; cfri++) { - var cfr = cfRows[cfri]; - var cfRowCls = cfri === 2 ? 'font-semibold' : ''; + var cfr = cfRows[cfri], cfRowCls = cfri === 2 ? 'font-semibold' : ''; var rowHtml = '' + cfr.label + ''; var cAnnualCls = cfri === 2 ? (cfr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700'; rowHtml += '' + moneyIntL(cfr.annual) + ''; @@ -78,15 +142,12 @@ function renderOverviewFinanceData(data, view) { } var cfCard = card('

部门现金流 部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入

' + deptThead + '' + cfTbody + '
', 'p-4'); - // ====== 项目经营详情 ====== var LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流']; var projVals = [p.revenue, p.gross, p.cost, p.paid, p.payment, p.cashflow]; var projAnnual = p.annual; - var projThead = '指标年度累计'; for (var phi = 0; phi < cols; phi++) { projThead += '' + headers[phi] + ''; } projThead += ''; - var projTbody = ''; for (var ri = 0; ri < 7; ri++) { var rowHtml = '' + LABELS[ri] + ''; @@ -106,7 +167,91 @@ function renderOverviewFinanceData(data, view) { if (window.lucide) window.lucide.createIcons(); } -window.setOverviewFinanceView = (v) => { - state.overviewView = v; - renderOverviewFinance(); -}; +window.setOverviewFinanceView = (v) => { state.overviewView = v; renderOverviewFinance(); }; + +// ====== 损益报表 ====== + +function renderFinanceDeptMatrix() { + var c = document.querySelector("#overview_finance"); + if (!c) return; + c.innerHTML = '
加载损益矩阵...
'; + fetch('/api/overview/finance-dept-matrix').then(r => r.json()).then(function(data) { renderFinanceDeptMatrixData(data, c); }) + .catch(function() { c.innerHTML = '
加载失败
'; }); +} + +function renderFinanceDeptMatrixData(data, container) { + var mf = function(v) { return Math.round(Number(v || 0)).toLocaleString("zh-CN"); }; + var tenants = data.tenants; + var sn = tenants.map(function(t) { return t.replace('·无界', ''); }); + var mL = ['01月','02月','03月','04月','05月','06月','07月','08月','09月','10月','11月','12月']; + var vBtns = financeBtnBar('matrix'); + var cards = ''; + for (var mi = 0; mi < 12; mi++) { + var th = '指标'; + for (var ti = 0; ti < tenants.length; ti++) { th += '' + sn[ti] + ''; } + th += '合计'; + var tb = financeMatrixTbody(data, tenants, mi, ['project_gross','dept_expense','profit'], ['项目毛利','部门费用','利润'], mf); + cards += '
' + mL[mi] + '
' + th + '' + tb + '
'; + } + container.innerHTML = '
' + vBtns + '
' + cards + '
'; + if (window.lucide) window.lucide.createIcons(); +} + +// ====== 现金流报表 ====== + +function renderFinanceCfMatrix() { + var c = document.querySelector("#overview_finance"); + if (!c) return; + c.innerHTML = '
加载现金流矩阵...
'; + fetch('/api/overview/finance-dept-cf-matrix').then(r => r.json()).then(function(data) { renderFinanceCfMatrixData(data, c); }) + .catch(function() { c.innerHTML = '
加载失败
'; }); +} + +function renderFinanceCfMatrixData(data, container) { + var mf = function(v) { return Math.round(Number(v || 0)).toLocaleString("zh-CN"); }; + var tenants = data.tenants; + var sn = tenants.map(function(t) { return t.replace('·无界', ''); }); + var mL = ['01月','02月','03月','04月','05月','06月','07月','08月','09月','10月','11月','12月']; + var vBtns = financeBtnBar('cf_matrix'); + var cards = ''; + for (var mi = 0; mi < 12; mi++) { + var th = '指标'; + for (var ti = 0; ti < tenants.length; ti++) { th += '' + sn[ti] + ''; } + th += '合计'; + var tb = financeMatrixTbody(data, tenants, mi, ['project_cf','dept_cf','bl_cf'], ['项目现金流','部门现金流','经营现金流'], mf); + cards += '
' + mL[mi] + '
' + th + '' + tb + '
'; + } + container.innerHTML = '
' + vBtns + '
' + cards + '
'; + if (window.lucide) window.lucide.createIcons(); +} + +// ====== 共用工具 ====== + +function financeBtnBar(active) { + var btns = []; + btns.push(''); + btns.push(''); + btns.push(''); + btns.push(''); + btns.push(''); + return '
' + btns.join('') + '
'; +} + +function financeMatrixTbody(data, tenants, mi, keys, labels, mf) { + var tb = ''; + for (var ri = 0; ri < keys.length; ri++) { + var rowCls = ri === keys.length - 1 ? 'font-semibold' : ''; + var rh = '' + labels[ri] + ''; + var ts = 0; + for (var ti = 0; ti < tenants.length; ti++) { + var v = data.data[tenants[ti]][keys[ri]][mi] || 0; + ts += v; + var cls = v >= 0 ? 'text-green-600' : 'text-red-600'; + rh += '' + mf(v) + ''; + } + var tCls = ts >= 0 ? 'text-green-600' : 'text-red-600'; + rh += '' + mf(ts) + ''; + tb += rh; + } + return tb; +} diff --git a/static/modules/overview_plan.js b/static/modules/overview_plan.js index 35d0c3e..c99dbcc 100644 --- a/static/modules/overview_plan.js +++ b/static/modules/overview_plan.js @@ -1,18 +1,84 @@ // overview_plan.js — 预算总览模块(5 个工作台数据聚合) function renderOverviewPlan() { - const view = state.overviewView || "monthly"; - const api = view === "quarterly" ? "/api/overview/plan-quarterly" : "/api/overview/plan-monthly"; - const container = document.querySelector("#overview_plan"); + var view = state.overviewView || "monthly"; + var container = document.querySelector("#overview_plan"); if (!container) return; container.innerHTML = '
加载中...
'; + if (view === 'matrix') { renderDeptMatrix(); return; } + if (view === 'cf_matrix') { renderCfMatrix(); return; } + if (view === 'project_matrix') { renderProjectMatrix(); return; } + + var api = view === "quarterly" ? "/api/overview/plan-quarterly" : "/api/overview/plan-monthly"; fetch(api).then(r => r.json()).then(data => { + if (state.overviewView !== view) return; renderOverviewPlanData(data, view); }).catch(() => { + if (state.overviewView !== view) return; container.innerHTML = '
加载失败
'; }); } +function renderProjectMatrix() { + var c = document.querySelector("#overview_plan"); + if (!c) return; + c.innerHTML = '
加载项目经营详情矩阵...
'; + fetch('/api/overview/dept-project-matrix').then(function(r) { + if (!r.ok) throw new Error('HTTP ' + r.status); + return r.json(); + }).then(function(data) { + renderProjectMatrixData(data, c); + }).catch(function(e) { + c.innerHTML = '
加载失败: ' + (e.message || '未知错误') + '
'; + }); +} + +function renderProjectMatrixData(data, container) { + try { + var mf = function(v) { return Math.round(Number(v || 0)).toLocaleString("zh-CN"); }; + var tenants = data.tenants; + var sn = tenants.map(function(t) { return t.replace('·无界', ''); }); + var mL = ['01月','02月','03月','04月','05月','06月','07月','08月','09月','10月','11月','12月']; + var keys = ["sign","revenue","gross","cost","paid","payment","cashflow"]; + var labels = ["合同金额","确收金额","确收毛利","当期成本","当期流出","回款","项目现金流"]; + + var vBtns = btnBar('project_matrix'); + var cards = ''; + for (var mi = 0; mi < 12; mi++) { + var th = '指标'; + for (var ti = 0; ti < tenants.length; ti++) { th += '' + sn[ti] + ''; } + th += '合计'; + + var tb = projectMatrixTbody(data, tenants, mi, keys, labels, mf); + cards += '
' + mL[mi] + '
' + th + '' + tb + '
'; + } + container.innerHTML = '
' + vBtns + '
' + cards + '
'; + if (window.lucide) window.lucide.createIcons(); + } catch(e) { container.innerHTML = '
渲染失败: ' + (e.message || '未知错误') + '
'; } +} + +function projectMatrixTbody(data, tenants, mi, keys, labels, mf) { + var tb = ''; + for (var ri = 0; ri < keys.length; ri++) { + var rowCls = ri === keys.length - 1 ? 'font-semibold' : ''; + var rh = '' + labels[ri] + ''; + var ts = 0; + for (var ti = 0; ti < tenants.length; ti++) { + var v = data.data[tenants[ti]][keys[ri]][mi] || 0; + ts += v; + var cls = 'text-slate-700'; + if (ri === 2) cls = 'text-green-600'; + if (ri === keys.length - 1) cls = v >= 0 ? 'text-green-600' : 'text-red-600'; + rh += '' + mf(v) + ''; + } + var tCls = 'text-slate-700'; + if (ri === 2) tCls = 'text-green-600'; + if (ri === keys.length - 1) tCls = ts >= 0 ? 'text-green-600' : 'text-red-600'; + rh += '' + mf(ts) + ''; + tb += rh; + } + return tb; +} function renderOverviewPlanData(data, view) { const container = document.querySelector("#overview_plan"); @@ -29,6 +95,14 @@ function renderOverviewPlanData(data, view) { const cf = data.dept_cf; const p = data.project; + // 视图切换按钮 + var btns = []; + btns.push(''); + btns.push(''); + btns.push(''); + btns.push(''); btns.push(''); + var viewBtns = '
' + btns.join('') + '
'; + // ====== 部门损益情况 ====== var deptThead = '指标年度累计'; for (var hi = 0; hi < cols; hi++) { deptThead += '' + headers[hi] + ''; } @@ -78,7 +152,7 @@ function renderOverviewPlanData(data, view) { // ====== 项目经营详情 ====== var LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流']; var projVals = [p.revenue, p.gross, p.cost, p.paid, p.payment, p.cashflow]; - var projAnnual = p.annual; // [sign, revenue_sum, gross_sum, cost_sum, paid_sum, payment_sum, cf_sum] + var projAnnual = p.annual; var projThead = '指标年度累计'; for (var phi = 0; phi < cols; phi++) { projThead += '' + headers[phi] + ''; } @@ -99,14 +173,102 @@ function renderOverviewPlanData(data, view) { } var projCard = card('

项目经营详情 所有项目产生的签单,确收,毛利,回款,当期成本,当期流出,现金流

' + projThead + '' + projTbody + '
', 'p-4'); - // 视图切换按钮 - var viewBtns = '
'; - container.innerHTML = '
' + viewBtns + deptCard + cfCard + projCard + '
'; if (window.lucide) window.lucide.createIcons(); } -window.setOverviewView = (v) => { - state.overviewView = v; - renderOverviewPlan(); -}; +window.setOverviewView = (v) => { state.overviewView = v; renderOverviewPlan(); }; + +// ====== 损益报表 ====== + +function renderDeptMatrix() { + var c = document.querySelector("#overview_plan"); + if (!c) return; + c.innerHTML = '
加载损益矩阵...
'; + fetch('/api/overview/dept-matrix').then(r => r.json()).then(function(data) { renderDeptMatrixData(data, c); }) + .catch(function() { c.innerHTML = '
加载失败
'; }); +} + +function renderDeptMatrixData(data, container) { + var mf = function(v) { return Math.round(Number(v || 0)).toLocaleString("zh-CN"); }; + var tenants = data.tenants; + var sn = tenants.map(function(t) { return t.replace('·无界', ''); }); + var mL = ['01月','02月','03月','04月','05月','06月','07月','08月','09月','10月','11月','12月']; + + var vBtns = btnBar('matrix'); + var cards = ''; + for (var mi = 0; mi < 12; mi++) { + var th = '指标'; + for (var ti = 0; ti < tenants.length; ti++) { th += '' + sn[ti] + ''; } + th += '合计'; + + var tb = matrixTbody(data, tenants, mi, ['project_gross','dept_expense','profit'], ['项目毛利','部门费用','利润'], mf); + cards += '
' + mL[mi] + '
' + th + '' + tb + '
'; + } + container.innerHTML = '
' + vBtns + '
' + cards + '
'; + if (window.lucide) window.lucide.createIcons(); +} + +// ====== 现金流报表 ====== + +function renderCfMatrix() { + var c = document.querySelector("#overview_plan"); + if (!c) return; + c.innerHTML = '
加载现金流矩阵...
'; + fetch('/api/overview/dept-cf-matrix').then(r => r.json()).then(function(data) { renderCfMatrixData(data, c); }) + .catch(function() { c.innerHTML = '
加载失败
'; }); +} + +function renderCfMatrixData(data, container) { + var mf = function(v) { return Math.round(Number(v || 0)).toLocaleString("zh-CN"); }; + var tenants = data.tenants; + var sn = tenants.map(function(t) { return t.replace('·无界', ''); }); + var mL = ['01月','02月','03月','04月','05月','06月','07月','08月','09月','10月','11月','12月']; + + var vBtns = btnBar('cf_matrix'); + var cards = ''; + for (var mi = 0; mi < 12; mi++) { + var th = '指标'; + for (var ti = 0; ti < tenants.length; ti++) { th += '' + sn[ti] + ''; } + th += '合计'; + + var tb = matrixTbody(data, tenants, mi, ['project_cf','dept_cf','bl_cf'], ['项目现金流','部门现金流','经营现金流'], mf); + cards += '
' + mL[mi] + '
' + th + '' + tb + '
'; + } + container.innerHTML = '
' + vBtns + '
' + cards + '
'; + if (window.lucide) window.lucide.createIcons(); +} + +// ====== 共用工具 ====== + +function btnBar(active) { + var btns = []; + btns.push(''); + btns.push(''); + btns.push(''); + btns.push(''); btns.push(''); + return '
' + btns.join('') + '
'; +} + +function matrixTbody(data, tenants, mi, keys, labels, mf) { + var tb = ''; + for (var ri = 0; ri < keys.length; ri++) { + var rowCls = ri === keys.length - 1 ? 'font-semibold' : ''; + var rh = '' + labels[ri] + ''; + var ts = 0; + for (var ti = 0; ti < tenants.length; ti++) { + var v = data.data[tenants[ti]][keys[ri]][mi] || 0; + ts += v; + var cls = v >= 0 ? 'text-green-600' : 'text-red-600'; + rh += '' + mf(v) + ''; + } + var tCls = ts >= 0 ? 'text-green-600' : 'text-red-600'; + rh += '' + mf(ts) + ''; + tb += rh; + } + return tb; + +// ====== 项目经营详情报表 ====== + + +} \ No newline at end of file diff --git a/templates/index.html b/templates/index.html index 0b5508a..0b80e7a 100644 --- a/templates/index.html +++ b/templates/index.html @@ -84,7 +84,7 @@
-

OPC Manager v1.2.0.25

+

OPC Manager v1.2.0.26

科普 OPC 工作台