Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
13e63b98e3 | ||
|
|
8952829fe5 |
@@ -286,16 +286,16 @@ function renderFinance() {
|
|||||||
return h;
|
return h;
|
||||||
};
|
};
|
||||||
const SIGNED_COLS = [
|
const SIGNED_COLS = [
|
||||||
{ key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' },
|
{ key: 'client_name', label: '客户' }, { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' },
|
||||||
{ key: 'rev', label: '已确收' }, { key: 'gross', label: '毛利' }, { key: 'cost', label: '成本' },
|
{ key: 'rev', label: '已确收' }, { key: 'gross', label: '毛利' }, { key: 'cost', label: '成本' },
|
||||||
{ key: 'profit', label: '项目利润' }, { key: 'payment', label: '已回款' }, { key: 'paid', label: '已付' },
|
{ key: 'payment', label: '已回款' }, { key: 'paid', label: '已付' },
|
||||||
{ key: 'cashflow', label: '现金流' }, { key: 'exe_rate', label: '执行率' }, { key: 'gross_rate', label: '毛利率' },
|
{ key: 'cashflow', label: '现金流' }, { key: 'exe_rate', label: '执行率' }, { key: 'gross_rate', label: '毛利率' },
|
||||||
{ key: 'pay_rate', label: '回款率' }, { key: 'paid_rate', label: '付款率' },
|
{ key: 'pay_rate', label: '回款率' }, { key: 'paid_rate', label: '付款率' },
|
||||||
];
|
];
|
||||||
const UNSIGNED_COLS = [
|
const UNSIGNED_COLS = [
|
||||||
{ key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' },
|
{ key: 'client_name', label: '客户' }, { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' },
|
||||||
{ key: 'sign_month', label: '签约月份' }, { key: 'gross', label: '毛利' }, { key: 'cost', label: '成本' },
|
{ key: 'sign_month', label: '签约月份' }, { key: 'gross', label: '毛利' }, { key: 'cost', label: '成本' },
|
||||||
{ key: 'profit', label: '项目利润' },
|
,
|
||||||
];
|
];
|
||||||
|
|
||||||
const renderView = (rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, emptyText) => {
|
const renderView = (rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, emptyText) => {
|
||||||
@@ -327,17 +327,11 @@ function renderFinance() {
|
|||||||
const grossR = rev && gross ? Math.round(gross / rev * 100) + '%' : '<span class="text-slate-300">—</span>';
|
const grossR = rev && gross ? Math.round(gross / rev * 100) + '%' : '<span class="text-slate-300">—</span>';
|
||||||
const cf = cashflow ? money(cashflow) : '<span class="text-slate-300">—</span>';
|
const cf = cashflow ? money(cashflow) : '<span class="text-slate-300">—</span>';
|
||||||
const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600';
|
const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600';
|
||||||
const profit = gross;
|
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal(${pf.id})"><td class="p-2 text-center align-middle text-sm">${esc(pf.client_name || "")}</td><td class="p-2 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="p-2 text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="p-2 text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="p-2 text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="p-2 text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="p-2 text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="p-2 text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="p-2 text-center font-semibold align-middle ${cfCls}">${cf}</td><td class="p-2 text-center align-middle text-sm">${exe}</td><td class="p-2 text-center align-middle text-sm">${grossR}</td><td class="p-2 text-center align-middle text-sm">${payR}</td><td class="p-2 text-center align-middle text-sm">${paidR}</td></tr>`;
|
||||||
const pfCls = profit >= 0 ? 'text-green-600' : 'text-red-600';
|
|
||||||
const pfVal = profit ? money(profit) : '<span class="text-slate-300">—</span>';
|
|
||||||
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal(${pf.id})"><td class="p-2 text-center align-middle text-sm">${esc(pf.client_name || "")}</td><td class="p-2 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="p-2 text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="p-2 text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="p-2 text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="p-2 text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="p-2 text-center font-semibold align-middle ${pfCls}">${pfVal}</td><td class="p-2 text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="p-2 text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="p-2 text-center font-semibold align-middle ${cfCls}">${cf}</td><td class="p-2 text-center align-middle text-sm">${exe}</td><td class="p-2 text-center align-middle text-sm">${grossR}</td><td class="p-2 text-center align-middle text-sm">${payR}</td><td class="p-2 text-center align-middle text-sm">${paidR}</td></tr>`;
|
|
||||||
};
|
};
|
||||||
// 待签约简版行
|
// 待签约简版行
|
||||||
const tdRowUnsigned = (pf, cost, gross) => {
|
const tdRowUnsigned = (pf, cost, gross) => {
|
||||||
const profit = gross;
|
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal(${pf.id})"><td class="p-2 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="p-2 text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="p-2 text-center align-middle text-sm">${pf.sign_month || '—'}</td><td class="p-2 text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="p-2 text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td></tr>`;
|
||||||
const pfCls = profit >= 0 ? 'text-green-600' : 'text-red-600';
|
|
||||||
const pfVal = profit ? money(profit) : '<span class="text-slate-300">—</span>';
|
|
||||||
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal(${pf.id})"><td class="p-2 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="p-2 text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="p-2 text-center align-middle text-sm">${pf.sign_month || '—'}</td><td class="p-2 text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="p-2 text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="p-2 text-center font-semibold align-middle ${pfCls}">${pfVal}</td></tr>`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (state.finView === 'monthly') {
|
if (state.finView === 'monthly') {
|
||||||
@@ -360,7 +354,7 @@ function renderFinance() {
|
|||||||
if (!rev && !payment && !cost && !paid && !gross) return;
|
if (!rev && !payment && !cost && !paid && !gross) return;
|
||||||
dataItems.push({
|
dataItems.push({
|
||||||
pf: pf, customer_name: pf.customer_name || '', sign_amount: pf.sign_amount || 0, sign_month: pf.sign_month || '',
|
pf: pf, customer_name: pf.customer_name || '', sign_amount: pf.sign_amount || 0, sign_month: pf.sign_month || '',
|
||||||
rev: rev, gross: gross, cost: cost, profit: gross, payment: payment, paid: paid,
|
rev: rev, gross: gross, cost: cost, payment: payment, paid: paid,
|
||||||
cashflow: payment - paid,
|
cashflow: payment - paid,
|
||||||
exe_rate: rev && pf.sign_amount ? Math.round(rev / pf.sign_amount * 100) : 0,
|
exe_rate: rev && pf.sign_amount ? Math.round(rev / pf.sign_amount * 100) : 0,
|
||||||
gross_rate: rev && gross ? Math.round(gross / rev * 100) : 0,
|
gross_rate: rev && gross ? Math.round(gross / rev * 100) : 0,
|
||||||
@@ -411,7 +405,7 @@ function renderFinance() {
|
|||||||
if (!rev && !payment && !cost && !paid && !gross) return;
|
if (!rev && !payment && !cost && !paid && !gross) return;
|
||||||
dataItems.push({
|
dataItems.push({
|
||||||
pf: pf, customer_name: pf.customer_name || '', sign_amount: pf.sign_amount || 0, sign_month: pf.sign_month || '',
|
pf: pf, customer_name: pf.customer_name || '', sign_amount: pf.sign_amount || 0, sign_month: pf.sign_month || '',
|
||||||
rev: rev, gross: gross, cost: cost, profit: gross, payment: payment, paid: paid,
|
rev: rev, gross: gross, cost: cost, payment: payment, paid: paid,
|
||||||
cashflow: payment - paid,
|
cashflow: payment - paid,
|
||||||
exe_rate: rev && pf.sign_amount ? Math.round(rev / pf.sign_amount * 100) : 0,
|
exe_rate: rev && pf.sign_amount ? Math.round(rev / pf.sign_amount * 100) : 0,
|
||||||
gross_rate: rev && gross ? Math.round(gross / rev * 100) : 0,
|
gross_rate: rev && gross ? Math.round(gross / rev * 100) : 0,
|
||||||
@@ -447,7 +441,7 @@ function renderFinance() {
|
|||||||
var t = calcTotals(pf);
|
var t = calcTotals(pf);
|
||||||
return {
|
return {
|
||||||
pf: pf, customer_name: pf.customer_name || '', sign_amount: pf.sign_amount || 0, sign_month: pf.sign_month || '',
|
pf: pf, customer_name: pf.customer_name || '', sign_amount: pf.sign_amount || 0, sign_month: pf.sign_month || '',
|
||||||
rev: t.rev, gross: t.gross, cost: t.cost, profit: t.gross,
|
rev: t.rev, gross: t.gross, cost: t.cost,
|
||||||
payment: t.payment, paid: t.paid, cashflow: t.payment - t.paid,
|
payment: t.payment, paid: t.paid, cashflow: t.payment - t.paid,
|
||||||
exe_rate: t.rev && pf.sign_amount ? Math.round(t.rev / pf.sign_amount * 100) : 0,
|
exe_rate: t.rev && pf.sign_amount ? Math.round(t.rev / pf.sign_amount * 100) : 0,
|
||||||
gross_rate: t.rev && t.gross ? Math.round(t.gross / t.rev * 100) : 0,
|
gross_rate: t.rev && t.gross ? Math.round(t.gross / t.rev * 100) : 0,
|
||||||
|
|||||||
@@ -965,11 +965,35 @@ window.deletePlanItem = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 从 planFinances 计算月度数据
|
||||||
|
function computePlanMonthly(planFinances) {
|
||||||
|
var pfs = planFinances || [];
|
||||||
|
var months = [];
|
||||||
|
var year = new Date().getFullYear();
|
||||||
|
for (var mi = 1; mi <= 12; mi++) { months.push(year + '-' + String(mi).padStart(2, '0')); }
|
||||||
|
var data = [];
|
||||||
|
for (var i = 0; i < 12; i++) {
|
||||||
|
var month = months[i];
|
||||||
|
var sign = 0, revenue = 0, gross = 0, payment = 0, cost = 0, paid = 0;
|
||||||
|
for (var j = 0; j < pfs.length; j++) {
|
||||||
|
var pf = pfs[j];
|
||||||
|
if ((pf.sign_month || '') === month) sign += parseFloat(pf.sign_amount || 0);
|
||||||
|
var bd = []; try { bd = JSON.parse(pf.budget_data || '[]'); } catch(e) {}
|
||||||
|
var b = bd.find(function(x) { return (x.month || '') === month; });
|
||||||
|
if (b) { revenue += parseFloat(b.rev || 0); gross += parseFloat(b.gross || 0); payment += parseFloat(b.payment || 0); }
|
||||||
|
var ed = []; try { ed = JSON.parse(pf.expense_data || '[]'); } catch(e) {}
|
||||||
|
var e = ed.find(function(x) { return (x.month || '') === month; });
|
||||||
|
if (e) { cost += parseFloat(e.cost || 0); paid += parseFloat(e.paid || 0); }
|
||||||
|
}
|
||||||
|
data.push({ month: month, sign: Math.round(sign), revenue: Math.round(revenue), gross: Math.round(gross), payment: Math.round(payment), cost: Math.round(cost), paid: Math.round(paid) });
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
// 总览渲染
|
// 总览渲染
|
||||||
function renderPlanOverview() {
|
function renderPlanOverview() {
|
||||||
const { summary, financeMonthly } = state.data;
|
const planFinances = state.data.planFinances || [];
|
||||||
if (!summary) return;
|
const fm = computePlanMonthly(planFinances);
|
||||||
const m = summary.metrics;
|
|
||||||
const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN");
|
const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN");
|
||||||
const moneyWan = (v) => {
|
const moneyWan = (v) => {
|
||||||
const n = Number(v || 0);
|
const n = Number(v || 0);
|
||||||
@@ -977,20 +1001,20 @@ function renderPlanOverview() {
|
|||||||
};
|
};
|
||||||
const card = (body, cls) => '<div class="card ' + (cls || 'p-4') + '">' + body + '</div>';
|
const card = (body, cls) => '<div class="card ' + (cls || 'p-4') + '">' + body + '</div>';
|
||||||
|
|
||||||
// 月度数据 (financeMonthly 有12个月)
|
// 从 planFinances 计算年度累计
|
||||||
const fm = financeMonthly || [];
|
var annualSums = { sign: 0, revenue: 0, gross: 0, cost: 0, payment: 0, paid: 0 };
|
||||||
|
for (var i = 0; i < fm.length; i++) {
|
||||||
|
annualSums.sign += fm[i].sign; annualSums.revenue += fm[i].revenue;
|
||||||
|
annualSums.gross += fm[i].gross; annualSums.cost += fm[i].cost;
|
||||||
|
annualSums.payment += fm[i].payment; annualSums.paid += fm[i].paid;
|
||||||
|
}
|
||||||
const mLbl = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];
|
const mLbl = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];
|
||||||
const LABELS = ['合同金额','确收金额','确收毛利','成本','项目利润','回款金额','已付','现金流'];
|
const LABELS = ['合同金额','确收金额','确收毛利','成本','项目利润','回款金额','已付','现金流'];
|
||||||
// 每行的年度累计 + 12个月值
|
// 每行的年度累计 + 12个月值
|
||||||
const annualVals = [
|
const annualVals = [
|
||||||
m.signed_annual || m.signed_amount || 0,
|
annualSums.sign, annualSums.revenue, annualSums.gross, annualSums.cost,
|
||||||
m.revenue_annual || 0,
|
annualSums.gross - annualSums.cost, annualSums.payment, annualSums.paid,
|
||||||
m.gross_annual || 0,
|
annualSums.payment - annualSums.paid,
|
||||||
m.cost_annual || 0,
|
|
||||||
m.profit_annual || 0,
|
|
||||||
m.payment_annual || 0,
|
|
||||||
m.paid_annual || 0,
|
|
||||||
m.cashflow_annual || 0,
|
|
||||||
];
|
];
|
||||||
// 字段映射: sign, revenue, gross, cost, profit(=gross-cost), payment, paid, cashflow(=payment-paid)
|
// 字段映射: sign, revenue, gross, cost, profit(=gross-cost), payment, paid, cashflow(=payment-paid)
|
||||||
const fmFields = ['sign','revenue','gross','cost',null,'payment','paid',null];
|
const fmFields = ['sign','revenue','gross','cost',null,'payment','paid',null];
|
||||||
@@ -1027,23 +1051,27 @@ function renderPlanOverview() {
|
|||||||
|
|
||||||
if (window.lucide) window.lucide.createIcons();
|
if (window.lucide) window.lucide.createIcons();
|
||||||
// 渲染图表(复用 home.js 的 moneyTick / chartOptions / monthLabels)
|
// 渲染图表(复用 home.js 的 moneyTick / chartOptions / monthLabels)
|
||||||
if (financeMonthly && window.Chart) {
|
if (fm && window.Chart) {
|
||||||
renderPlanCharts(financeMonthly);
|
renderPlanCharts(fm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderPlanQuarterlyOverview() {
|
function renderPlanQuarterlyOverview() {
|
||||||
const { summary, financeMonthly } = state.data;
|
const planFinances = state.data.planFinances || [];
|
||||||
if (!summary) return;
|
const fm = computePlanMonthly(planFinances);
|
||||||
const m = summary.metrics;
|
|
||||||
const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN");
|
const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN");
|
||||||
const card = (body, cls) => '<div class="card ' + (cls || 'p-4') + '">' + body + '</div>';
|
const card = (body, cls) => '<div class="card ' + (cls || 'p-4') + '">' + body + '</div>';
|
||||||
const fm = financeMonthly || [];
|
|
||||||
const LABELS = ['合同金额','确收金额','确收毛利','成本','项目利润','回款金额','已付','现金流'];
|
const LABELS = ['合同金额','确收金额','确收毛利','成本','项目利润','回款金额','已付','现金流'];
|
||||||
const qLbl = ['Q1','Q2','Q3','Q4'];
|
const qLbl = ['Q1','Q2','Q3','Q4'];
|
||||||
const qRanges = [[0,1,2],[3,4,5],[6,7,8],[9,10,11]];
|
const qRanges = [[0,1,2],[3,4,5],[6,7,8],[9,10,11]];
|
||||||
const annualVals = [m.signed_annual||m.signed_amount||0, m.revenue_annual||0, m.gross_annual||0, m.cost_annual||0, m.profit_annual||0, m.payment_annual||0, m.paid_annual||0, m.cashflow_annual||0];
|
var annualSums2 = { sign: 0, revenue: 0, gross: 0, cost: 0, payment: 0, paid: 0 };
|
||||||
|
for (var i2 = 0; i2 < fm.length; i2++) {
|
||||||
|
annualSums2.sign += fm[i2].sign; annualSums2.revenue += fm[i2].revenue;
|
||||||
|
annualSums2.gross += fm[i2].gross; annualSums2.cost += fm[i2].cost;
|
||||||
|
annualSums2.payment += fm[i2].payment; annualSums2.paid += fm[i2].paid;
|
||||||
|
}
|
||||||
|
const annualVals = [annualSums2.sign, annualSums2.revenue, annualSums2.gross, annualSums2.cost, annualSums2.gross - annualSums2.cost, annualSums2.payment, annualSums2.paid, annualSums2.payment - annualSums2.paid];
|
||||||
const fmFields = ['sign','revenue','gross','cost',null,'payment','paid',null];
|
const fmFields = ['sign','revenue','gross','cost',null,'payment','paid',null];
|
||||||
|
|
||||||
// Calculate quarterly sums
|
// Calculate quarterly sums
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
<header class="topbar border-b border-slate-200 bg-white px-8 py-5">
|
<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 items-center gap-3 w-full">
|
||||||
<div class="flex-1">
|
<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.0.0.27</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.0.0.29</span></p>
|
||||||
<div class="flex items-center gap-4 mt-1">
|
<div class="flex items-center gap-4 mt-1">
|
||||||
<h1 class="text-2xl font-semibold" id="workspaceTitle">科普 OPC 工作台</h1>
|
<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">
|
<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