Files
opc-manager/static/modules/overview_finance.js
mac 356c129668 feat: 总览工作台新增损益/现金流/项目经营详细报表
- 预算总览和发生总览各新增3个tab: 损益详细报表、现金流详细报表、项目经营详细报表
- 12个月卡片布局, 按部门(学术/科普/科研/医患/MCN)展示矩阵
- 后端新增6个聚合API: dept-matrix/cf-matrix/project-matrix x 2
- tab改为finance-tab风格, 与经营月报一致
- 修复项目报表函数未注册导致的加载失败
- 版本号 v1.2.0.26
2026-07-11 14:36:00 +08:00

258 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// overview_finance.js — 发生总览模块5 个工作台数据聚合)
function renderOverviewFinance() {
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 = '<div class="p-4 text-center text-slate-400">加载中...</div>';
fetch(api).then(r => r.json()).then(data => {
if (state.overviewView !== view) return;
renderOverviewFinanceData(data, view);
}).catch(() => {
if (state.overviewView !== view) return;
container.innerHTML = '<div class="p-4 text-center text-slate-400">加载失败</div>';
});
}
// ====== 项目经营详情报表(顶部定义,避免调用时未注册) ======
function renderFinanceProjectMatrix() {
var c = document.querySelector("#overview_finance");
if (!c) return;
c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载项目经营详情矩阵...</div>';
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 = '<div class="p-3 text-center text-red-500 text-sm">加载失败: ' + (e.message || '未知错误') + '</div>';
});
}
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 = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th>';
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
th += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">合计</th></tr>';
var tb = financeProjectMatrixTbody(data, tenants, mi, keys, labels, mf);
cards += '<div class="flex items-center gap-3"><div class="text-5xl font-bold text-slate-300" style="width:120px;text-align:center;white-space:nowrap">' + mL[mi] + '</div><div class="card p-4 flex-1"><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + th + '</thead><tbody>' + tb + '</tbody></table></div></div></div>';
}
container.innerHTML = '<div class="grid gap-2.5">' + vBtns + '<div class="flex flex-col gap-2">' + cards + '</div></div>';
if (window.lucide) window.lucide.createIcons();
} catch(e) { container.innerHTML = '<div class="p-3 text-center text-red-500 text-sm">渲染失败: ' + (e.message || '未知错误') + '</div>'; }
}
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 = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + labels[ri] + '</td>';
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 += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + mf(v) + '</td>';
}
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 += '<td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td></tr>';
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) => '<div class="card ' + (cls || 'p-4') + '">' + body + '</div>';
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;
var btns = [];
btns.push('<button onclick="setOverviewFinanceView(\'monthly\')" class="finance-tab' + (view === 'monthly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">月报总览</button>');
btns.push('<button onclick="setOverviewFinanceView(\'quarterly\')" class="finance-tab' + (view === 'quarterly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">季报总览</button>');
btns.push('<button onclick="setOverviewFinanceView(\'matrix\')" class="finance-tab' + (view === 'matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">损益详细报表</button>');
btns.push('<button onclick="setOverviewFinanceView(\'cf_matrix\')" class="finance-tab' + (view === 'cf_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">现金流详细报表</button>');
btns.push('<button onclick="setOverviewFinanceView(\'project_matrix\')" class="finance-tab' + (view === 'project_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">项目经营详细报表</button>');
var viewBtns = '<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:12px">' + btns.join('') + '</div>';
var deptThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
for (var hi = 0; hi < cols; hi++) { deptThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + headers[hi] + '</th>'; }
deptThead += '</tr>';
var deptRows = [
{ label: '项目毛利', vals: d.project_gross, annual: d.annual[0] },
{ label: '部门费用', vals: d.dept_expense, annual: d.annual[1] },
{ label: '利润', vals: d.profit, annual: d.annual[2] }
];
var deptTbody = '';
for (var dri = 0; dri < 3; dri++) {
var dr = deptRows[dri], rowCls = dri === 2 ? 'font-semibold' : '';
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + dr.label + '</td>';
var annualCls = dri === 2 ? (dr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
rowHtml += '<td class="py-1.5 text-right ' + rowCls + ' ' + annualCls + ' whitespace-nowrap">' + moneyIntL(dr.annual) + '</td>';
for (var i = 0; i < cols; i++) {
var cellCls = dri === 2 ? (dr.vals[i] >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
rowHtml += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cellCls + '">' + moneyIntL(dr.vals[i]) + '</td>';
}
deptTbody += rowHtml + '</tr>';
}
var deptCard = card('<h3 class="text-base font-bold text-slate-700 mb-3">部门损益情况 <span class="text-sm text-slate-500 font-normal ml-3">部门利润 = 所有项目产生的毛利 - 部门 1 级产生的人力成本,研发成本,差旅和其他支出</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + deptTbody + '</tbody></table></div>', 'p-4');
var cfRows = [
{ label: '项目现金流', vals: cf.project_cf, annual: cf.annual[0] },
{ label: '部门现金流', vals: cf.dept_cf_item, annual: cf.annual[1] },
{ label: '经营现金流', vals: cf.bl_cf, annual: cf.annual[2] }
];
var cfTbody = '';
for (var cfri = 0; cfri < 3; cfri++) {
var cfr = cfRows[cfri], cfRowCls = cfri === 2 ? 'font-semibold' : '';
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + cfRowCls + ' text-slate-700 whitespace-nowrap">' + cfr.label + '</td>';
var cAnnualCls = cfri === 2 ? (cfr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
rowHtml += '<td class="py-1.5 text-right ' + cfRowCls + ' ' + cAnnualCls + ' whitespace-nowrap">' + moneyIntL(cfr.annual) + '</td>';
for (var i = 0; i < cols; i++) {
var cCellCls = cfri === 2 ? (cfr.vals[i] >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
rowHtml += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cCellCls + '">' + moneyIntL(cfr.vals[i]) + '</td>';
}
cfTbody += rowHtml + '</tr>';
}
var cfCard = card('<h3 class="text-base font-bold text-slate-700 mb-3">部门现金流 <span class="text-sm text-slate-500 font-normal ml-3">部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4');
var LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流'];
var projVals = [p.revenue, p.gross, p.cost, p.paid, p.payment, p.cashflow];
var projAnnual = p.annual;
var projThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
for (var phi = 0; phi < cols; phi++) { projThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + headers[phi] + '</th>'; }
projThead += '</tr>';
var projTbody = '';
for (var ri = 0; ri < 7; ri++) {
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left font-semibold text-slate-700 whitespace-nowrap">' + LABELS[ri] + '</td>';
rowHtml += '<td class="py-1.5 text-right font-semibold text-slate-800 whitespace-nowrap">' + (ri === 0 ? moneyIntL(p.sign) : moneyIntL(projAnnual[ri])) + '</td>';
for (var i = 0; i < cols; i++) {
var val = ri === 0 ? 0 : (projVals[ri - 1] ? projVals[ri - 1][i] : 0);
var cls = 'text-slate-700';
if (ri === 2) cls = 'text-green-600';
if (ri === 6) cls = val >= 0 ? 'text-green-600' : 'text-red-600';
rowHtml += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + moneyIntL(val) + '</td>';
}
projTbody += rowHtml + '</tr>';
}
var projCard = card('<h3 class="text-base font-bold text-slate-700 mb-3">项目经营详情 <span class="text-sm text-slate-500 font-normal ml-3">所有项目产生的签单,确收,毛利,回款,当期成本,当期流出,现金流</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + projThead + '</thead><tbody>' + projTbody + '</tbody></table></div>', 'p-4');
container.innerHTML = '<div class="grid gap-2.5">' + viewBtns + deptCard + cfCard + projCard + '</div>';
if (window.lucide) window.lucide.createIcons();
}
window.setOverviewFinanceView = (v) => { state.overviewView = v; renderOverviewFinance(); };
// ====== 损益报表 ======
function renderFinanceDeptMatrix() {
var c = document.querySelector("#overview_finance");
if (!c) return;
c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载损益矩阵...</div>';
fetch('/api/overview/finance-dept-matrix').then(r => r.json()).then(function(data) { renderFinanceDeptMatrixData(data, c); })
.catch(function() { c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载失败</div>'; });
}
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 = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th>';
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
th += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">合计</th></tr>';
var tb = financeMatrixTbody(data, tenants, mi, ['project_gross','dept_expense','profit'], ['项目毛利','部门费用','利润'], mf);
cards += '<div class="flex items-center gap-3"><div class="text-5xl font-bold text-slate-300" style="width:120px;text-align:center;white-space:nowrap">' + mL[mi] + '</div><div class="card p-4 flex-1"><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + th + '</thead><tbody>' + tb + '</tbody></table></div></div></div>';
}
container.innerHTML = '<div class="grid gap-2.5">' + vBtns + '<div class="flex flex-col gap-2">' + cards + '</div></div>';
if (window.lucide) window.lucide.createIcons();
}
// ====== 现金流报表 ======
function renderFinanceCfMatrix() {
var c = document.querySelector("#overview_finance");
if (!c) return;
c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载现金流矩阵...</div>';
fetch('/api/overview/finance-dept-cf-matrix').then(r => r.json()).then(function(data) { renderFinanceCfMatrixData(data, c); })
.catch(function() { c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载失败</div>'; });
}
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 = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th>';
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
th += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">合计</th></tr>';
var tb = financeMatrixTbody(data, tenants, mi, ['project_cf','dept_cf','bl_cf'], ['项目现金流','部门现金流','经营现金流'], mf);
cards += '<div class="flex items-center gap-3"><div class="text-5xl font-bold text-slate-300" style="width:120px;text-align:center;white-space:nowrap">' + mL[mi] + '</div><div class="card p-4 flex-1"><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + th + '</thead><tbody>' + tb + '</tbody></table></div></div></div>';
}
container.innerHTML = '<div class="grid gap-2.5">' + vBtns + '<div class="flex flex-col gap-2">' + cards + '</div></div>';
if (window.lucide) window.lucide.createIcons();
}
// ====== 共用工具 ======
function financeBtnBar(active) {
var btns = [];
btns.push('<button onclick="setOverviewFinanceView(\'monthly\')" class="finance-tab' + (active === 'monthly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">月报总览</button>');
btns.push('<button onclick="setOverviewFinanceView(\'quarterly\')" class="finance-tab' + (active === 'quarterly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">季报总览</button>');
btns.push('<button onclick="setOverviewFinanceView(\'matrix\')" class="finance-tab' + (active === 'matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">损益详细报表</button>');
btns.push('<button onclick="setOverviewFinanceView(\'cf_matrix\')" class="finance-tab' + (active === 'cf_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">现金流详细报表</button>');
btns.push('<button onclick="setOverviewFinanceView(\'project_matrix\')" class="finance-tab' + (active === 'project_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">项目经营详细报表</button>');
return '<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:12px">' + btns.join('') + '</div>';
}
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 = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + labels[ri] + '</td>';
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 += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + mf(v) + '</td>';
}
var tCls = ts >= 0 ? 'text-green-600' : 'text-red-600';
rh += '<td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td></tr>';
tb += rh;
}
return tb;
}