Files
opc-manager/static/modules/overview_plan.js
mac c5be3ce134 style: 总览模块所有表格指标列固定宽度100px
- thead和tbody中指标列添加min-width:100px
- 覆盖月报/季报/损益/现金流/项目5个模块
- 版本号 v1.2.0.33
2026-07-11 15:43:43 +08:00

302 lines
19 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_plan.js — 预算总览模块5 个工作台数据聚合)
function renderOverviewPlan() {
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 = 11; mi >= 0; mi--) {
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 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 += '</tr>';
if (isAllZero(data, tenants, keys.filter(function(k) { return k !== 'sign'; }), mi)) continue;
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 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 += '</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-blue-200" 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 ts = 0;
var vals = [];
for (var ti = 0; ti < tenants.length; ti++) {
var v = data.data[tenants[ti]][keys[ri]][mi] || 0;
ts += v;
vals.push(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';
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" style="min-width:100px">' + labels[ri] + '</td><td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td>';
for (var ti = 0; ti < tenants.length; ti++) {
var cls = 'text-slate-700';
if (ri === 2) cls = 'text-green-600';
if (ri === keys.length - 1) cls = vals[ti] >= 0 ? 'text-green-600' : 'text-red-600';
rh += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + mf(vals[ti]) + '</td>';
}
rh += '</tr>';
tb += rh;
}
return tb;
}
function renderOverviewPlanData(data, view) {
const container = document.querySelector("#overview_plan");
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;
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" style="min-width:100px">指标</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];
var 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" style="min-width:100px">' + 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];
var 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" style="min-width:100px">' + 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.sign_monthly || [], 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" style="min-width:100px">指标</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" style="min-width:100px">' + LABELS[ri] + '</td>';
rowHtml += '<td class="py-1.5 text-right font-semibold text-slate-800 whitespace-nowrap">' + moneyIntL(projAnnual[ri]) + '</td>';
for (var i = 0; i < cols; i++) {
var val = projVals[ri] ? (projVals[ri][i] || 0) : 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.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 = 11; mi >= 0; mi--) {
if (isAllZero(data, tenants, ['project_gross','dept_expense','profit'], mi)) continue;
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 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 += '</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-blue-200" 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 = 11; mi >= 0; mi--) {
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 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 += '</tr>';
if (isAllZero(data, tenants, ['project_cf','dept_cf','bl_cf'], mi)) continue;
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 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 += '</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-blue-200" 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 ts = 0;
var vals = [];
for (var ti = 0; ti < tenants.length; ti++) {
var v = data.data[tenants[ti]][keys[ri]][mi] || 0;
ts += v;
vals.push(v);
}
var tCls = ts >= 0 ? 'text-green-600' : 'text-red-600';
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" style="min-width:100px">' + labels[ri] + '</td><td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td>';
for (var ti = 0; ti < tenants.length; ti++) {
var cls = vals[ti] >= 0 ? 'text-green-600' : 'text-red-600';
rh += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + mf(vals[ti]) + '</td>';
}
rh += '</tr>';
tb += rh;
}
return tb;
function isAllZero(data, tenants, keys, mi) {
for (var ti = 0; ti < tenants.length; ti++) {
for (var ki = 0; ki < keys.length; ki++) {
if ((data.data[tenants[ti]][keys[ki]][mi] || 0) !== 0) return false;
}
}
return true;
}
// ====== 项目经营详情报表 ======
}