Compare commits
106 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c2beee51e | ||
|
|
c5be3ce134 | ||
|
|
6a582c65b8 | ||
|
|
14368e83d0 | ||
|
|
3fff80425b | ||
|
|
63ddffb12a | ||
|
|
02abe41793 | ||
|
|
2c7cd21095 | ||
|
|
f30d40fe70 | ||
|
|
9273be3143 | ||
|
|
eb300731a3 | ||
|
|
2aab73a86a | ||
|
|
67bc00314c | ||
|
|
e0000c2abb | ||
|
|
d34e52eff4 | ||
|
|
8e38f50c99 | ||
|
|
3a40e78c3e | ||
|
|
a1a4f41d79 | ||
|
|
d4889d56ec | ||
|
|
35706f123d | ||
|
|
e4d1cd59f3 | ||
|
|
1673603b02 | ||
|
|
db93eb8d31 | ||
|
|
94ad7aed55 | ||
|
|
043938670a | ||
|
|
9862d9fe56 | ||
|
|
150e627811 | ||
|
|
295bf5a81c | ||
|
|
e8a26059d5 | ||
|
|
543d620384 | ||
|
|
975034c589 | ||
|
|
71e90bd55f | ||
|
|
3042791a90 | ||
|
|
a8243acc5a | ||
|
|
cb0e3fdac2 | ||
|
|
f5c92d09af | ||
|
|
e18626105b | ||
|
|
b587530437 | ||
|
|
9a1260db68 | ||
|
|
7c33bc05f2 | ||
|
|
9de0a63be2 | ||
|
|
f3c94c57d6 | ||
|
|
9db627770b | ||
|
|
75b988c77a | ||
|
|
2b7f67436e | ||
|
|
ed5455f309 | ||
|
|
452d410843 | ||
|
|
c95373b85e | ||
|
|
6be10481d5 | ||
|
|
4ce0e66d90 | ||
|
|
0f9e1b6cf2 | ||
|
|
dc8d3d59d2 | ||
|
|
1e5c5b5706 | ||
|
|
b68144ebb0 | ||
|
|
3cae1d85cf | ||
|
|
ca0cd9eab1 | ||
|
|
2bdc0e0779 | ||
|
|
93dca56c3f | ||
|
|
8c4f50fc52 | ||
|
|
13864f1968 | ||
|
|
d11e8f9313 | ||
|
|
02a12a208c | ||
|
|
b977627895 | ||
|
|
edf8e6179c | ||
|
|
3148af05b3 | ||
|
|
4029933ada | ||
|
|
95d3f9809c | ||
|
|
8b040489e2 | ||
|
|
db9700c8ee | ||
|
|
11e30f33b9 | ||
|
|
95f52b61d0 | ||
|
|
a4012f2189 | ||
|
|
e1c5e65c1e | ||
|
|
a3d78b3ca0 | ||
|
|
9dc5631574 | ||
|
|
c5a59508d2 | ||
|
|
ab1d57492c | ||
|
|
9a81a30feb | ||
|
|
d6d21b03dd | ||
|
|
b9a05e8732 | ||
|
|
440707e19a | ||
|
|
e0dc1a56b2 | ||
|
|
f45afd954d | ||
|
|
664da22329 | ||
|
|
73a6ac126c | ||
|
|
878e26d4ca | ||
|
|
e12ed0936b | ||
|
|
8edb74f173 | ||
|
|
1a42c6e6f3 | ||
|
|
d21497bce3 | ||
|
|
574e3c5700 | ||
|
|
c70293b447 | ||
|
|
adeff08827 | ||
|
|
8bd40de41d | ||
|
|
ac6eacea82 | ||
|
|
83c2a5ca94 | ||
|
|
fbd2290d29 | ||
|
|
97fcf88c61 | ||
|
|
a01afef599 | ||
|
|
28fa244fe5 | ||
|
|
ad3885e0be | ||
|
|
0fb7ee2992 | ||
|
|
f6792cad39 | ||
|
|
bed6e9192a | ||
|
|
aaa213a765 | ||
|
|
207629a9bb |
@@ -53,3 +53,4 @@ curl http://127.0.0.1:5177/api/health
|
||||
- 产品:慰心斋产品路线图中的 5 个产品版本
|
||||
- 财务:首版财务样例和原财务 manager 合并方向
|
||||
|
||||
# test trigger
|
||||
|
||||
@@ -1093,6 +1093,7 @@ def _aggregate_monthly_from_json(conn, table, tenants):
|
||||
_year = date.today().year
|
||||
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
|
||||
rev, gross, payment, cost, paid = [0]*12, [0]*12, [0]*12, [0]*12, [0]*12
|
||||
sign_monthly = [0]*12
|
||||
sign_total = 0
|
||||
sign_cnt = 0
|
||||
|
||||
@@ -1103,6 +1104,10 @@ def _aggregate_monthly_from_json(conn, table, tenants):
|
||||
if sm.startswith(str(_year)):
|
||||
sign_total += float(pf.get("sign_amount") or 0)
|
||||
sign_cnt += 1
|
||||
# 签单按月拆分
|
||||
sm7 = sm[:7]
|
||||
if sm7 in months:
|
||||
sign_monthly[months.index(sm7)] += float(pf.get("sign_amount") or 0)
|
||||
try:
|
||||
bd = json.loads(pf.get("budget_data") or "[]")
|
||||
except:
|
||||
@@ -1130,6 +1135,7 @@ def _aggregate_monthly_from_json(conn, table, tenants):
|
||||
"rev": rev, "gross": gross, "payment": payment, "cost": cost, "paid": paid,
|
||||
"cashflow": [payment[i] - paid[i] for i in range(12)],
|
||||
"sign_total": sign_total, "sign_cnt": sign_cnt,
|
||||
"sign_monthly": sign_monthly,
|
||||
}
|
||||
|
||||
|
||||
@@ -1181,6 +1187,7 @@ def _build_overview_result(agg, exp_amount, exp_incurred):
|
||||
# 项目经营详情
|
||||
project = {
|
||||
"sign": agg["sign_total"],
|
||||
"sign_monthly": agg["sign_monthly"],
|
||||
"revenue": rev,
|
||||
"gross": gross,
|
||||
"cost": cost,
|
||||
@@ -1218,7 +1225,7 @@ def overview_plan_quarterly():
|
||||
conn.close()
|
||||
# Convert monthly to quarterly
|
||||
result = _build_overview_result(agg, exp_amount, exp_incurred)
|
||||
for key in ["revenue","gross","payment","cost","paid","cashflow"]:
|
||||
for key in ["sign_monthly","revenue","gross","payment","cost","paid","cashflow"]:
|
||||
result["project"][key] = _quarterly_from_monthly(result["project"][key])
|
||||
result["dept"]["project_gross"] = _quarterly_from_monthly(result["dept"]["project_gross"])
|
||||
result["dept"]["dept_expense"] = _quarterly_from_monthly(result["dept"]["dept_expense"])
|
||||
@@ -1247,7 +1254,7 @@ def overview_finance_quarterly():
|
||||
exp_amount, exp_incurred = _aggregate_expenses(conn, "expense_records", OVERVIEW_TENANTS)
|
||||
conn.close()
|
||||
result = _build_overview_result(agg, exp_amount, exp_incurred)
|
||||
for key in ["revenue","gross","payment","cost","paid","cashflow"]:
|
||||
for key in ["sign_monthly","revenue","gross","payment","cost","paid","cashflow"]:
|
||||
result["project"][key] = _quarterly_from_monthly(result["project"][key])
|
||||
result["dept"]["project_gross"] = _quarterly_from_monthly(result["dept"]["project_gross"])
|
||||
result["dept"]["dept_expense"] = _quarterly_from_monthly(result["dept"]["dept_expense"])
|
||||
|
||||
@@ -297,10 +297,9 @@ function renderFinance() {
|
||||
{ key: '_expand', label: '' },
|
||||
{ key: 'client_name', label: '客户' }, { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' },
|
||||
{ key: 'rev', label: '已确收' }, { key: 'gross', label: '毛利' }, { key: 'gross_rate', label: '毛利率' },
|
||||
{ key: 'payment', label: '回款' }, { key: 'pay_rate', label: '回款率' },
|
||||
{ key: 'payment', label: '回款' }, { key: 'pending', label: '待回款' }, { key: 'pay_rate', label: '回款率' },
|
||||
{ key: 'cost', label: '成本' }, { key: 'paid', label: '支出' }, { key: 'paid_rate', label: '付款率' },
|
||||
{ key: 'cashflow', label: '现金流' },
|
||||
{ key: 'pending', label: '待回款' },
|
||||
];
|
||||
const UNSIGNED_COLS = [
|
||||
{ key: 'client_name', label: '客户' }, { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' },
|
||||
@@ -340,7 +339,7 @@ function renderFinance() {
|
||||
const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
const pending = rev - payment;
|
||||
const pendingCls = pending > 0 ? 'text-red-600' : pending < 0 ? 'text-green-600' : 'text-slate-400';
|
||||
const pendingVal = pending ? money(pending) : '<span class="text-slate-300">—</span>';
|
||||
const pendingVal = pending ? (pending > 0 ? '+' : '') + money(pending) : '<span class="text-slate-300">—</span>';
|
||||
var budgetArr = [];
|
||||
var expenseArr = [];
|
||||
try { budgetArr = JSON.parse(pf.budget_data || "[]"); } catch(e) {}
|
||||
@@ -363,7 +362,7 @@ function renderFinance() {
|
||||
var bCf = bPay - ePaid;
|
||||
return '<tr class="bg-slate-50"><td class="px-2 py-px"></td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-slate-400">' + m + '</td><td class="px-2 py-px text-center text-xs text-blue-600">' + (bRev ? money(bRev) : '—') + '</td><td class="px-2 py-px text-center text-xs text-green-600">' + (bGross ? money(bGross) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-amber-600">' + (bPay ? money(bPay) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-rose-600">' + (eCost ? money(eCost) : '—') + '</td><td class="px-2 py-px text-center text-xs text-purple-600">' + (ePaid ? money(ePaid) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs ' + (bCf >= 0 ? 'text-green-600' : 'text-red-600') + '">' + (bCf ? money(bCf) : '—') + '</td><td class="px-2 py-px text-center text-xs ' + (bRev - bPay > 0 ? 'text-red-600' : bRev - bPay < 0 ? 'text-green-600' : 'text-slate-300') + '">' + (bRev - bPay ? money(bRev - bPay) : '—') + '</td></tr>';
|
||||
}).join('');
|
||||
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation();finToggleExpand(${pf.id})"><i data-lucide="chevron-right" style="width:14px;height:14px;color:#94a3b8" id="fin_expand_icon_${pf.id}"></i></td><td class="px-2 py-px text-center align-middle text-sm truncate" style="max-width:100px" title="${esc(pf.client_name || '')}">${esc(pf.client_name || "")}</td><td class="px-2 py-px text-sm font-medium text-center align-middle truncate" style="min-width:300px;max-width:300px;width:300px" title="${esc(pf.customer_name)}">${esc(pf.customer_name)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center align-middle text-sm">${grossR}</td><td class="px-2 py-px text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="px-2 py-px text-center align-middle text-sm">${payR}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="px-2 py-px text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="px-2 py-px text-center align-middle text-sm">${paidR}</td><td class="px-2 py-px text-center font-semibold align-middle ${cfCls}">${cf}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${pendingCls}">${pendingVal}</td></tr><tr class="hidden" id="fin_expand_${pf.id}"><td colspan="14"><table class="w-full"><thead><tr class="border-b border-slate-200"><th class="px-2 py-1"></th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">月份</th><th class="px-2 py-1 text-center text-xs text-slate-400">确收</th><th class="px-2 py-1 text-center text-xs text-slate-400">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">成本</th><th class="px-2 py-1 text-center text-xs text-slate-400">支出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-400">待回款</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
|
||||
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation();finToggleExpand(${pf.id})"><i data-lucide="chevron-right" style="width:14px;height:14px;color:#94a3b8" id="fin_expand_icon_${pf.id}"></i></td><td class="px-2 py-px text-center align-middle text-sm truncate" style="max-width:100px" title="${esc(pf.client_name || '')}">${esc(pf.client_name || "")}</td><td class="px-2 py-px text-sm font-medium text-center align-middle truncate" style="min-width:300px;max-width:300px;width:300px" title="${esc(pf.customer_name)}">${esc(pf.customer_name)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center align-middle text-sm">${grossR}</td><td class="px-2 py-px text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${pendingCls}">${pendingVal}</td><td class="px-2 py-px text-center align-middle text-sm">${payR}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="px-2 py-px text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="px-2 py-px text-center align-middle text-sm">${paidR}</td><td class="px-2 py-px text-center font-semibold align-middle ${cfCls}">${cf}</td></tr><tr class="hidden" id="fin_expand_${pf.id}"><td colspan="14"><table class="w-full"><thead><tr class="border-b border-slate-200"><th class="px-2 py-1"></th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">月份</th><th class="px-2 py-1 text-center text-xs text-slate-400">确收</th><th class="px-2 py-1 text-center text-xs text-slate-400">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">成本</th><th class="px-2 py-1 text-center text-xs text-slate-400">支出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-400">待回款</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
|
||||
};
|
||||
// 待签约简版行
|
||||
const tdRowUnsigned = (pf, cost, gross) => {
|
||||
@@ -1249,3 +1248,10 @@ function renderOverviewCharts(data) {
|
||||
state.finChart4 = new Chart(c4, Object.assign({}, iconf, { data: { labels: labels, datasets: [{ label: "利润", data: data.map(function(x) { return x.gross || 0; }), borderColor: "#6366f1", backgroundColor: "rgba(99,102,241,0.06)", fill: true, tension: 0.3 }] } }));
|
||||
}
|
||||
};
|
||||
|
||||
// ====== 筛选器控制函数 ======
|
||||
window.switchFinFilter = (f) => { state.finFilter = f; renderFinance(); };
|
||||
window.setFinView = (v) => { state.finView = v; renderFinance(); };
|
||||
window.setFinYear = (y) => { state.finYear = parseInt(y); renderFinance(); };
|
||||
window.setFinQuarter = (q) => { state.finView = 'quarterly'; state.finQuarter = q; renderFinance(); };
|
||||
window.setFinMonth = (m) => { state.finView = 'monthly'; state.finMonth = m; renderFinance(); };
|
||||
|
||||
@@ -20,6 +20,15 @@ function renderOverviewFinance() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
// ====== 项目经营详情报表(顶部定义,避免调用时未注册) ======
|
||||
|
||||
function renderFinanceProjectMatrix() {
|
||||
@@ -47,12 +56,17 @@ function renderFinanceProjectMatrixData(data, container) {
|
||||
|
||||
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 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 += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">合计</th></tr>';
|
||||
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 = 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>';
|
||||
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();
|
||||
@@ -63,20 +77,24 @@ 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;
|
||||
var vals = [];
|
||||
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>';
|
||||
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';
|
||||
rh += '<td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td></tr>';
|
||||
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;
|
||||
@@ -100,7 +118,7 @@ function renderOverviewFinanceData(data, view) {
|
||||
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>';
|
||||
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>';
|
||||
|
||||
@@ -112,7 +130,7 @@ function renderOverviewFinanceData(data, view) {
|
||||
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 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++) {
|
||||
@@ -131,7 +149,7 @@ function renderOverviewFinanceData(data, view) {
|
||||
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 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++) {
|
||||
@@ -143,17 +161,17 @@ 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 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">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
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">' + 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>';
|
||||
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 = ri === 0 ? 0 : (projVals[ri - 1] ? projVals[ri - 1][i] : 0);
|
||||
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';
|
||||
@@ -186,12 +204,13 @@ function renderFinanceDeptMatrixData(data, container) {
|
||||
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 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 += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">合计</th></tr>';
|
||||
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>';
|
||||
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();
|
||||
@@ -214,12 +233,13 @@ function renderFinanceCfMatrixData(data, container) {
|
||||
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 mi = 11; mi >= 0; mi--) {
|
||||
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 += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">合计</th></tr>';
|
||||
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>';
|
||||
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();
|
||||
@@ -241,16 +261,20 @@ 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;
|
||||
var vals = [];
|
||||
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>';
|
||||
vals.push(v);
|
||||
}
|
||||
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>';
|
||||
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;
|
||||
|
||||
@@ -44,13 +44,18 @@ function renderProjectMatrixData(data, container) {
|
||||
|
||||
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 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 += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">合计</th></tr>';
|
||||
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-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>';
|
||||
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();
|
||||
@@ -61,20 +66,24 @@ 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;
|
||||
var vals = [];
|
||||
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>';
|
||||
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';
|
||||
rh += '<td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td></tr>';
|
||||
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;
|
||||
@@ -104,7 +113,7 @@ function renderOverviewPlanData(data, view) {
|
||||
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>';
|
||||
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>';
|
||||
|
||||
@@ -117,7 +126,7 @@ function renderOverviewPlanData(data, view) {
|
||||
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">' + dr.label + '</td>';
|
||||
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++) {
|
||||
@@ -138,7 +147,7 @@ function renderOverviewPlanData(data, view) {
|
||||
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">' + cfr.label + '</td>';
|
||||
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++) {
|
||||
@@ -151,19 +160,19 @@ function renderOverviewPlanData(data, view) {
|
||||
|
||||
// ====== 项目经营详情 ======
|
||||
var LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流'];
|
||||
var projVals = [p.revenue, p.gross, p.cost, p.paid, p.payment, p.cashflow];
|
||||
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">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
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">' + 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>';
|
||||
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 = ri === 0 ? 0 : (projVals[ri - 1] ? projVals[ri - 1][i] : 0);
|
||||
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';
|
||||
@@ -197,13 +206,14 @@ function renderDeptMatrixData(data, container) {
|
||||
|
||||
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 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 += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">合计</th></tr>';
|
||||
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>';
|
||||
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();
|
||||
@@ -227,13 +237,18 @@ function renderCfMatrixData(data, container) {
|
||||
|
||||
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 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 += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">合计</th></tr>';
|
||||
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-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>';
|
||||
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();
|
||||
@@ -254,20 +269,33 @@ 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;
|
||||
var vals = [];
|
||||
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>';
|
||||
vals.push(v);
|
||||
}
|
||||
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>';
|
||||
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;
|
||||
}
|
||||
// ====== 项目经营详情报表 ======
|
||||
|
||||
|
||||
|
||||
@@ -306,11 +306,11 @@ function renderPlan() {
|
||||
{ key: 'gross', label: '毛利' },
|
||||
{ key: 'gross_rate', label: '毛利率' },
|
||||
{ key: 'payment', label: '回款' },
|
||||
{ key: 'pending', label: '待回款' },
|
||||
{ key: 'pay_rate', label: '回款率' },
|
||||
{ key: 'cost', label: '当期成本' },
|
||||
{ key: 'paid', label: '当期流出' },
|
||||
{ key: 'cashflow', label: '现金流' },
|
||||
{ key: 'pending', label: '待回款' },
|
||||
];
|
||||
const UNSIGNED_COLS = [
|
||||
{ key: 'client_name', label: '客户' },
|
||||
@@ -369,7 +369,7 @@ function renderPlan() {
|
||||
const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
const pending = rev - payment;
|
||||
const pendingCls = pending > 0 ? 'text-red-600' : pending < 0 ? 'text-green-600' : 'text-slate-400';
|
||||
const pendingVal = pending ? money(pending) : '<span class="text-slate-300">—</span>';
|
||||
const pendingVal = pending ? (pending > 0 ? '+' : '') + money(pending) : '<span class="text-slate-300">—</span>';
|
||||
var budgetArr = [];
|
||||
var expenseArr = [];
|
||||
try { budgetArr = JSON.parse(pf.budget_data || "[]"); } catch(e) {}
|
||||
@@ -392,7 +392,7 @@ function renderPlan() {
|
||||
var bCf = bPay - ePaid;
|
||||
return '<tr class="bg-slate-50"><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-slate-400">' + m + '</td><td class="px-2 py-px text-center text-xs text-blue-600">' + (bRev ? money(bRev) : '—') + '</td><td class="px-2 py-px text-center text-xs text-green-600">' + (bGross ? money(bGross) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-amber-600">' + (bPay ? money(bPay) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-rose-600">' + (eCost ? money(eCost) : '—') + '</td><td class="px-2 py-px text-center text-xs text-purple-600">' + (ePaid ? money(ePaid) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs ' + (bCf >= 0 ? 'text-green-600' : 'text-red-600') + '">' + (bCf ? money(bCf) : '—') + '</td><td class="px-2 py-px text-center text-xs ' + (bRev - bPay > 0 ? 'text-red-600' : bRev - bPay < 0 ? 'text-green-600' : 'text-slate-300') + '">' + (bRev - bPay ? money(bRev - bPay) : '—') + '</td></tr>';
|
||||
}).join('');
|
||||
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="planOpenPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation();planToggleExpand(${pf.id})"><i data-lucide="chevron-right" style="width:14px;height:14px;color:#94a3b8" id="plan_expand_icon_${pf.id}"></i></td><td class="px-2 py-px text-center align-middle text-sm truncate" style="max-width:100px" title="${esc(pf.client_name || '')}">${esc(pf.client_name || "")}</td><td class="px-2 py-px text-sm font-medium text-center align-middle truncate" style="min-width:300px;max-width:300px;width:300px" title="${esc(pf.customer_name)}">${esc(pf.customer_name)}</td><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}</td><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center align-middle text-sm">${grossR}</td><td class="px-2 py-px text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="px-2 py-px text-center align-middle text-sm">${payR}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="px-2 py-px text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="px-2 py-px text-center font-semibold align-middle ${cfCls}">${cf}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${pendingCls}">${pendingVal}</td></tr><tr class="hidden" id="plan_expand_${pf.id}"><td colspan="15"><table class="w-full"><thead><tr class="border-b border-slate-200"><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">月份</th><th class="px-2 py-1 text-center text-xs text-slate-400">确收</th><th class="px-2 py-1 text-center text-xs text-slate-400">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">当期成本</th><th class="px-2 py-1 text-center text-xs text-slate-400">当期流出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-400">待回款</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
|
||||
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="planOpenPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation();planToggleExpand(${pf.id})"><i data-lucide="chevron-right" style="width:14px;height:14px;color:#94a3b8" id="plan_expand_icon_${pf.id}"></i></td><td class="px-2 py-px text-center align-middle text-sm truncate" style="max-width:100px" title="${esc(pf.client_name || '')}">${esc(pf.client_name || "")}</td><td class="px-2 py-px text-sm font-medium text-center align-middle truncate" style="min-width:300px;max-width:300px;width:300px" title="${esc(pf.customer_name)}">${esc(pf.customer_name)}</td><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}</td><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center align-middle text-sm">${grossR}</td><td class="px-2 py-px text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${pendingCls}">${pendingVal}</td><td class="px-2 py-px text-center align-middle text-sm">${payR}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="px-2 py-px text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="px-2 py-px text-center font-semibold align-middle ${cfCls}">${cf}</td></tr><tr class="hidden" id="plan_expand_${pf.id}"><td colspan="15"><table class="w-full"><thead><tr class="border-b border-slate-200"><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">月份</th><th class="px-2 py-1 text-center text-xs text-slate-400">确收</th><th class="px-2 py-1 text-center text-xs text-slate-400">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">当期成本</th><th class="px-2 py-1 text-center text-xs text-slate-400">当期流出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-400">待回款</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
|
||||
};
|
||||
// 待签约简版行
|
||||
const tdRowUnsigned = (pf, cost, gross) => {
|
||||
@@ -1356,3 +1356,11 @@ window.planInlineUpdate = async (pfId, field, value) => {
|
||||
toast("更新失败:" + error.message, "error");
|
||||
}
|
||||
};
|
||||
|
||||
// ====== 筛选器控制函数 ======
|
||||
window.switchPlanFilter = (f) => { state.planFilter = f; renderPlan(); };
|
||||
window.setPlanView = (v) => { state.planView = v; renderPlan(); };
|
||||
window.setPlanYear = (y) => { state.planYear = parseInt(y); renderPlan(); };
|
||||
window.setPlanQuarter = (q) => { state.planView = 'quarterly'; state.planQuarter = q; renderPlan(); };
|
||||
window.setPlanMonth = (m) => { state.planView = 'monthly'; state.planMonth = m; renderPlan(); };
|
||||
window.setPlanStatusFilter = (f) => { state.planStatusFilter = f; renderPlan(); };
|
||||
|
||||
@@ -193,7 +193,7 @@ async function load() {
|
||||
// 按需加载当前 tab 的数据(必须在 ensureStandardProposals 之前,否则会被 return 跳过)
|
||||
await ensureTabData(state.active);
|
||||
// 首次加载时确保标准资料库 7 项已初始化(仅首次,避免每次切换工作台都触发)
|
||||
if (!_proposalsInitialized && typeof ensureStandardProposals === "function") {
|
||||
if (!_proposalsInitialized && typeof ensureStandardProposals === "function" && state.tenant !== '总览') {
|
||||
_proposalsInitialized = true;
|
||||
try {
|
||||
const proposalsData = await api("/api/proposals/list?tenant=" + encodeURIComponent(state.tenant));
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
<header class="topbar border-b border-slate-200 bg-white px-8 py-5">
|
||||
<div class="flex items-center gap-3 w-full">
|
||||
<div class="flex-1">
|
||||
<p class="eyebrow text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">OPC Manager <span class="ml-2 text-xs font-normal text-slate-400 tracking-normal">v1.2.0.26</span></p>
|
||||
<p class="eyebrow text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">OPC Manager <span class="ml-2 text-xs font-normal text-slate-400 tracking-normal">v1.2.0.33</span></p>
|
||||
<div class="flex items-center gap-4 mt-1">
|
||||
<h1 class="text-2xl font-semibold" id="workspaceTitle">科普 OPC 工作台</h1>
|
||||
<div class="hidden sm:flex items-center gap-1.5 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100 rounded-full px-4 py-1.5">
|
||||
|
||||
Reference in New Issue
Block a user