feat: 总览工作台新增损益/现金流/项目经营详细报表

- 预算总览和发生总览各新增3个tab: 损益详细报表、现金流详细报表、项目经营详细报表
- 12个月卡片布局, 按部门(学术/科普/科研/医患/MCN)展示矩阵
- 后端新增6个聚合API: dept-matrix/cf-matrix/project-matrix x 2
- tab改为finance-tab风格, 与经营月报一致
- 修复项目报表函数未注册导致的加载失败
- 版本号 v1.2.0.26
This commit is contained in:
mac
2026-07-11 14:36:00 +08:00
parent 94b9f25938
commit 356c129668
4 changed files with 696 additions and 36 deletions

View File

@@ -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 = '<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;
const d = data.dept;
const cf = data.dept_cf;
const 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 viewBtns = '<div class="flex items-center gap-2 mb-2"><button onclick="setOverviewFinanceView(\'monthly\')" class="px-3 py-0.5 rounded text-sm font-medium ' + (view === 'monthly' ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200') + '">月报总览</button><button onclick="setOverviewFinanceView(\'quarterly\')" class="px-3 py-0.5 rounded text-sm font-medium ' + (view === 'quarterly' ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200') + '">季报总览</button></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>';
@@ -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 = '<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>';
@@ -57,7 +123,6 @@ function renderOverviewFinanceData(data, view) {
}
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] },
@@ -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 = '<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>';
@@ -78,15 +142,12 @@ function renderOverviewFinanceData(data, view) {
}
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>';
@@ -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 = '<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;
}

View File

@@ -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 = '<div class="p-4 text-center text-slate-400">加载中...</div>';
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 = '<div class="p-4 text-center text-slate-400">加载失败</div>';
});
}
function renderProjectMatrix() {
var c = document.querySelector("#overview_plan");
if (!c) return;
c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载项目经营详情矩阵...</div>';
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 = '<div class="p-3 text-center text-red-500 text-sm">加载失败: ' + (e.message || '未知错误') + '</div>';
});
}
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 = '<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 = projectMatrixTbody(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 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 = '<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 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('<button onclick="setOverviewView(\'monthly\')" class="finance-tab' + (view === 'monthly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">月报总览</button>');
btns.push('<button onclick="setOverviewView(\'quarterly\')" class="finance-tab' + (view === 'quarterly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">季报总览</button>');
btns.push('<button onclick="setOverviewView(\'matrix\')" class="finance-tab' + (view === 'matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">损益详细报表</button>');
btns.push('<button onclick="setOverviewView(\'cf_matrix\')" class="finance-tab' + (view === 'cf_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">现金流详细报表</button>'); btns.push('<button onclick="setOverviewView(\'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>'; }
@@ -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 = '<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>'; }
@@ -99,14 +173,102 @@ function renderOverviewPlanData(data, view) {
}
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');
// 视图切换按钮
var viewBtns = '<div class="flex items-center gap-2 mb-2"><button onclick="setOverviewView(\'monthly\')" class="px-3 py-0.5 rounded text-sm font-medium ' + (view === 'monthly' ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200') + '">月报总览</button><button onclick="setOverviewView(\'quarterly\')" class="px-3 py-0.5 rounded text-sm font-medium ' + (view === 'quarterly' ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200') + '">季报总览</button></div>';
container.innerHTML = '<div class="grid gap-2.5">' + viewBtns + deptCard + cfCard + projCard + '</div>';
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 = '<div class="p-3 text-center text-slate-400 text-sm">加载损益矩阵...</div>';
fetch('/api/overview/dept-matrix').then(r => r.json()).then(function(data) { renderDeptMatrixData(data, c); })
.catch(function() { c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载失败</div>'; });
}
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 = '<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 = matrixTbody(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 renderCfMatrix() {
var c = document.querySelector("#overview_plan");
if (!c) return;
c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载现金流矩阵...</div>';
fetch('/api/overview/dept-cf-matrix').then(r => r.json()).then(function(data) { renderCfMatrixData(data, c); })
.catch(function() { c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载失败</div>'; });
}
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 = '<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 = matrixTbody(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 btnBar(active) {
var btns = [];
btns.push('<button onclick="setOverviewView(\'monthly\')" class="finance-tab' + (active === 'monthly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">月报总览</button>');
btns.push('<button onclick="setOverviewView(\'quarterly\')" class="finance-tab' + (active === 'quarterly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">季报总览</button>');
btns.push('<button onclick="setOverviewView(\'matrix\')" class="finance-tab' + (active === 'matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">损益详细报表</button>');
btns.push('<button onclick="setOverviewView(\'cf_matrix\')" class="finance-tab' + (active === 'cf_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">现金流详细报表</button>'); btns.push('<button onclick="setOverviewView(\'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 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 = '<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;
// ====== 项目经营详情报表 ======
}