302 lines
19 KiB
JavaScript
302 lines
19 KiB
JavaScript
// 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;
|
||
}
|
||
// ====== 项目经营详情报表 ======
|
||
|
||
|
||
} |