feat: 大版本更新-工作台改版+总报完善+UI优化
工作台改版: - 学会→学术, 总工作台→总览(数据库迁移+session清理) - 侧边栏工作台从下拉菜单改为竖向平铺+独立图标 - 5个工作台去除部门模块, 只有总览显示 - 工作台顺序: 总览→学术→MCN→科普→医患→科研 经营月报+季度月报: - 月度/季度总览改名(月度总览→经营月报, 季度总览→季度月报) - 财务概览增加回款/项目现金流行(7行) - 新增业务线经营情况卡片(毛利/平台费用/利润) - 新增业务线现金流卡片(项目现金流/平台费用已付/业务线现金流) - 季度月报完善为三卡片结构, 与经营月报一致 - 计划模块总览独立数据源(computePlanMonthly) UI优化: - 汇总卡片单行显示+5x2网格+去外层卡片 - 项目表格行高压缩(py-px+line-height:0.95) - 表头不压缩, 只压缩tbody - 加权/项目类型内联下拉去原生箭头+自定义箭头 - 加权/项目类型表头去除排序箭头 - 实际模块已付→支出, 已回款→回款, 成本→成本(不含平台费用) - 现金流注释(回款-支出) - 卡片间距/筛选区/tip高度压缩 - 实际模块增加执行提示tip 修复: - routes.py planFinances client_name列去重 - plan_expense.js savePlanExpense改用api()+load() - plan.js createPlanFinance拆分try-catch - 版本号 v1.0.0.35
This commit is contained in:
@@ -76,27 +76,27 @@ function renderPlan() {
|
||||
if (isRevView) {
|
||||
const rev = b.rev || 0;
|
||||
const gross = b.gross || 0;
|
||||
return `<td class="p-2 text-center whitespace-nowrap align-middle"><span class="${rev ? 'text-blue-700 font-medium' : 'text-slate-300'}">${rev ? money(rev) : '—'}</span><br><span class="text-xs ${gross ? 'text-green-600' : 'text-slate-300'}">${gross ? money(gross) : '—'}</span></td>`;
|
||||
return `<td class="px-2 py-px text-center whitespace-nowrap align-middle"><span class="${rev ? 'text-blue-700 font-medium' : 'text-slate-300'}">${rev ? money(rev) : '—'}</span><br><span class="text-xs ${gross ? 'text-green-600' : 'text-slate-300'}">${gross ? money(gross) : '—'}</span></td>`;
|
||||
} else {
|
||||
const payment = b.payment || 0;
|
||||
const cost = b.cost || 0;
|
||||
return `<td class="p-2 text-center whitespace-nowrap align-middle"><span class="${payment ? 'text-amber-700 font-medium' : 'text-slate-300'}">${payment ? money(payment) : '—'}</span><br><span class="text-xs ${cost ? 'text-rose-600' : 'text-slate-300'}">${cost ? money(cost) : '—'}</span></td>`;
|
||||
return `<td class="px-2 py-px text-center whitespace-nowrap align-middle"><span class="${payment ? 'text-amber-700 font-medium' : 'text-slate-300'}">${payment ? money(payment) : '—'}</span><br><span class="text-xs ${cost ? 'text-rose-600' : 'text-slate-300'}">${cost ? money(cost) : '—'}</span></td>`;
|
||||
}
|
||||
}).join("");
|
||||
const totalCol = (() => {
|
||||
if (isRevView) {
|
||||
const totalRev = pf.total_rev || 0;
|
||||
const totalGross = pf.total_gross || 0;
|
||||
return `<td class="p-2 text-center whitespace-nowrap align-middle font-semibold"><span class="${totalRev ? 'text-blue-700' : 'text-slate-300'}">${totalRev ? money(totalRev) : '—'}</span><br><span class="text-xs ${totalGross ? 'text-green-600' : 'text-slate-300'}">${totalGross ? money(totalGross) : '—'}</span></td>`;
|
||||
return `<td class="px-2 py-px text-center whitespace-nowrap align-middle font-semibold"><span class="${totalRev ? 'text-blue-700' : 'text-slate-300'}">${totalRev ? money(totalRev) : '—'}</span><br><span class="text-xs ${totalGross ? 'text-green-600' : 'text-slate-300'}">${totalGross ? money(totalGross) : '—'}</span></td>`;
|
||||
} else {
|
||||
let totalPayment = 0, totalCost = 0;
|
||||
try { JSON.parse(pf.budget_data || "[]").forEach(b => { totalPayment += parseFloat(b.payment||0)||0; totalCost += parseFloat(b.cost||0)||0; }); } catch (e) {}
|
||||
return `<td class="p-2 text-center whitespace-nowrap align-middle font-semibold"><span class="${totalPayment ? 'text-amber-700' : 'text-slate-300'}">${totalPayment ? money(totalPayment) : '—'}</span><br><span class="text-xs ${totalCost ? 'text-rose-600' : 'text-slate-300'}">${totalCost ? money(totalCost) : '—'}</span></td>`;
|
||||
return `<td class="px-2 py-px text-center whitespace-nowrap align-middle font-semibold"><span class="${totalPayment ? 'text-amber-700' : 'text-slate-300'}">${totalPayment ? money(totalPayment) : '—'}</span><br><span class="text-xs ${totalCost ? 'text-rose-600' : 'text-slate-300'}">${totalCost ? money(totalCost) : '—'}</span></td>`;
|
||||
}
|
||||
})();
|
||||
const sm = pf.sign_month || "";
|
||||
const signMonthCell = `<td class="p-2 text-center align-middle text-sm"><span class="pf-sm-text cursor-pointer hover:text-blue-600" id="pf-sm-${pf.id}" onclick="event.stopPropagation(); planEditSignMonth(event, ${pf.id})">${sm || '—'}</span></td>`;
|
||||
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="planOpenPfEditModal(${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">${esc(pf.customer_name)}</td><td class="p-2 text-center align-middle text-sm">${esc(pf.weight || "20%")}</td><td class="p-2 text-center align-middle text-sm">${esc(pf.project_type || "待签约")}</td>${signMonthCell}<td class="p-2 text-center align-middle text-sm">${money(pf.sign_amount)}</td>${mCols}${totalCol}</tr>`;
|
||||
const signMonthCell = `<td class="px-2 py-px text-center align-middle text-sm"><span class="pf-sm-text cursor-pointer hover:text-blue-600" id="pf-sm-${pf.id}" onclick="event.stopPropagation(); planEditSignMonth(event, ${pf.id})">${sm || '—'}</span></td>`;
|
||||
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">${esc(pf.client_name || "")}</td><td class="px-2 py-px text-sm font-medium text-center align-middle">${esc(pf.customer_name)}</td><td class="px-2 py-px text-center align-middle text-sm">${esc(pf.weight || "20%")}</td><td class="px-2 py-px text-center align-middle text-sm">${esc(pf.project_type || "待签约")}</td>${signMonthCell}<td class="px-2 py-px text-center align-middle text-sm">${money(pf.sign_amount)}</td>${mCols}${totalCol}</tr>`;
|
||||
};
|
||||
|
||||
const now2 = new Date();
|
||||
@@ -108,18 +108,18 @@ function renderPlan() {
|
||||
const nowD = new Date();
|
||||
const activeQ = state.planQuarter !== undefined ? state.planQuarter : Math.floor(nowD.getMonth() / 3);
|
||||
const activeMonths = qMonths[activeQ];
|
||||
const finHeaderBase = `<div class="flex items-center gap-2"><button onclick="setPlanView('overview')" class="px-3 py-1 rounded text-sm font-medium ${state.planView==='overview'?'bg-blue-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">总视图</button><span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">按季度:</span>${qLabels.map((q,i) => `<button onclick="setPlanQuarter(${i})" class="px-3 py-1 rounded text-sm font-medium ${state.planView==='quarterly'&&activeQ===i?'bg-blue-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${q}</button>`).join('')}<span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">按月度:</span>${activeMonths.map(m => { const ms = `2026-${String(m).padStart(2,'0')}`; return `<button onclick="setPlanMonth('${ms}')" class="px-3 py-1 rounded text-sm font-medium ${state.planView==='monthly'&&state.planMonth===ms?'bg-blue-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`; }).join('')}</div>`;
|
||||
const finHeaderBase = `<div class="flex items-center gap-2"><button onclick="setPlanView('overview')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planView==='overview'?'bg-blue-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">总视图</button><span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">按季度:</span>${qLabels.map((q,i) => `<button onclick="setPlanQuarter(${i})" class="px-3 py-0.5 rounded text-sm font-medium ${state.planView==='quarterly'&&activeQ===i?'bg-blue-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${q}</button>`).join('')}<span class="text-slate-300 mx-2">|</span><span class="text-sm text-slate-500">按月度:</span>${activeMonths.map(m => { const ms = `2026-${String(m).padStart(2,'0')}`; return `<button onclick="setPlanMonth('${ms}')" class="px-3 py-0.5 rounded text-sm font-medium ${state.planView==='monthly'&&state.planMonth===ms?'bg-blue-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`; }).join('')}</div>`;
|
||||
const finAddBtn = `<button class="btn btn-primary btn-sm" onclick="openPlanModal()">新增财务项目</button>`;
|
||||
|
||||
const planFilterTabs = `<div class="finance-tabs" style="padding:0 0 0 0;border-bottom:1px solid #e2e8f0;margin-bottom:0;display:flex;gap:4px"><button onclick="switchPlanFilter('overview')" class="finance-tab${state.planFilter==='overview'?' active':''}" style="font-size:14px;font-weight:600;padding:8px 16px;margin-right:0">经营月报</button><button onclick="switchPlanFilter('quarterlyOverview')" class="finance-tab${state.planFilter==='quarterlyOverview'?' active':''}" style="font-size:14px;font-weight:600">季度月报</button><button onclick="switchPlanFilter('projects')" class="finance-tab${state.planFilter==='projects'?' active':''}" style="font-size:14px;font-weight:600">项目计划 (${pfs.length})</button><button onclick="switchPlanFilter('expense')" class="finance-tab${state.planFilter==='expense'?' active':''}" style="font-size:14px;font-weight:600">平台费用计划</button></div>`;
|
||||
|
||||
document.querySelector("#plan").innerHTML = `<div class="grid gap-4">
|
||||
document.querySelector("#plan").innerHTML = `<div class="grid gap-2">
|
||||
${planFilterTabs}
|
||||
<div class="flex items-center gap-3 px-4 py-3 rounded-lg border" style="background:linear-gradient(135deg,#dbeafe,#bfdbfe);border-color:#3b82f6">
|
||||
<div class="flex items-center gap-3 px-4 py-2 rounded-lg border" style="background:linear-gradient(135deg,#dbeafe,#bfdbfe);border-color:#3b82f6">
|
||||
<i data-lucide="lightbulb" class="text-blue-600 flex-shrink-0" style="width:18px;height:18px"></i>
|
||||
<span class="text-sm text-blue-900"><strong>计划提醒:</strong>计划也是预算,只有真正能够列出计划的预算,才有靠谱的落地可能性</span>
|
||||
</div>
|
||||
${state.planFilter !== 'expense' && state.planFilter !== 'overview' && state.planFilter !== 'quarterlyOverview' ? card(`<div class="flex justify-between items-center"><div class="flex items-center gap-2">${finHeaderBase}</div>${finAddBtn}</div>`, "p-4") : ''}
|
||||
${state.planFilter !== 'expense' && state.planFilter !== 'overview' && state.planFilter !== 'quarterlyOverview' ? card(`<div class="flex justify-between items-center"><div class="flex items-center gap-2">${finHeaderBase}</div>${finAddBtn}</div>`, "py-2 px-4") : ''}
|
||||
<div id="planModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closePlanModal()"><div class="bg-white rounded-2xl shadow-2xl w-full max-w-6xl mx-4 max-h-[92vh] overflow-y-auto" onclick="event.stopPropagation()"><div class="sticky top-0 z-10 bg-white/95 backdrop-blur border-b border-slate-100 px-8 py-5 flex items-center justify-between"><div><h3 class="text-xl font-bold text-slate-800" id="planModalTitle">新增项目财务</h3><p class="text-xs text-slate-400 mt-0.5">填写项目财务信息与月度预算</p></div><div class="flex items-center gap-2"><button class="btn btn-ghost btn-sm text-red-600 hidden" id="planDeleteBtn" onclick="deletePlanItem()"><i data-lucide="trash-2"></i>删除</button><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closePlanModal()"><i data-lucide="x"></i></button></div></div>
|
||||
<div class="finance-tabs">
|
||||
<button class="finance-tab active" data-tab="info" onclick="switchPlanTab('info')"><i data-lucide="info" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>基本信息</button>
|
||||
@@ -132,7 +132,7 @@ function renderPlan() {
|
||||
<form onsubmit="createPlanFinance(event)" class="finance-form" novalidate><input type="hidden" name="pf_id" id="plan_pf-id-input" value="">
|
||||
<div class="finance-tab-body">
|
||||
<div id="plan_financeTabInfo">
|
||||
<div class="grid gap-4">
|
||||
<div class="grid gap-2">
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<label class="block"><span class="fin-label">部门</span><input type="hidden" name="project_id" value="${state.tenant}"><input class="form-ctrl bg-slate-50 cursor-not-allowed" value="${state.tenant}" disabled></label>
|
||||
<label class="block"><span class="fin-label">项目编号</span><input name="project_code" class="form-ctrl" placeholder="如:KP-2026-001"></label>
|
||||
@@ -181,7 +181,7 @@ function renderPlan() {
|
||||
<p class="text-lg font-bold text-amber-700" id="plan_revpayTotalPayment">¥0</p>
|
||||
</div>
|
||||
</div>
|
||||
<table class="w-full text-sm border border-slate-200 rounded-lg overflow-hidden" id="plan_revpayTable">
|
||||
<table class="w-full text-sm" style="line-height:0.95 border border-slate-200 rounded-lg overflow-hidden" id="plan_revpayTable">
|
||||
<thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2.5 text-left font-medium text-slate-500" style="min-width:140px">月份</th><th class="p-2.5 text-right font-medium text-slate-500">确收</th><th class="p-2.5 text-right font-medium text-slate-500">毛利</th><th class="p-2.5 text-right font-medium text-slate-500">回款</th><th class="p-2.5 text-left font-medium text-slate-500">备注</th><th class="p-2.5 w-8"></th></tr></thead>
|
||||
<tbody id="plan_revpayTbody"></tbody>
|
||||
</table>
|
||||
@@ -198,14 +198,14 @@ function renderPlan() {
|
||||
<p class="text-lg font-bold text-purple-700" id="plan_costTotalPaid">¥0</p>
|
||||
</div>
|
||||
</div>
|
||||
<table class="w-full text-sm border border-slate-200 rounded-lg overflow-hidden" id="plan_costTable">
|
||||
<table class="w-full text-sm" style="line-height:0.95 border border-slate-200 rounded-lg overflow-hidden" id="plan_costTable">
|
||||
<thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2.5 text-left font-medium text-slate-500" style="min-width:140px">月份</th><th class="p-2.5 text-left font-medium text-slate-500" style="min-width:120px">费用类型</th><th class="p-2.5 text-right font-medium text-slate-500">成本</th><th class="p-2.5 text-right font-medium text-slate-500">已付</th><th class="p-2.5 text-left font-medium text-slate-500">备注</th><th class="p-2.5 w-8"></th></tr></thead>
|
||||
<tbody id="plan_costTbody"></tbody>
|
||||
</table>
|
||||
<button type="button" class="btn btn-ghost btn-sm mt-3" onclick="planAddCostRow()"><i data-lucide="plus"></i>添加月份</button>
|
||||
</div>
|
||||
<div id="plan_financeTabTasks" class="hidden">
|
||||
<table class="w-full text-sm border border-slate-200 rounded-lg overflow-hidden" id="plan_taskTable">
|
||||
<table class="w-full text-sm" style="line-height:0.95 border border-slate-200 rounded-lg overflow-hidden" id="plan_taskTable">
|
||||
<thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2.5 text-left font-medium text-slate-500" style="min-width:120px">月份</th><th class="p-2.5 text-left font-medium text-slate-500" style="min-width:120px">任务类型</th><th class="p-2.5 text-right font-medium text-slate-500">任务数量</th><th class="p-2.5 text-right font-medium text-slate-500">已执行</th><th class="p-2.5 text-right font-medium text-slate-500">差额</th><th class="p-2.5 text-right font-medium text-slate-500">单价</th><th class="p-2.5 text-right font-medium text-slate-500">执行金额</th><th class="p-2.5 text-right font-medium text-slate-500">未执行金额</th><th class="p-2.5 w-8"></th></tr></thead>
|
||||
<tbody id="plan_taskTbody"></tbody>
|
||||
</table>
|
||||
@@ -252,11 +252,11 @@ function renderPlan() {
|
||||
const METRIC_CARDS = [
|
||||
{ label: '签约金额', value: ctx => moneyWan(ctx.signTotal), color: 'text-slate-700' },
|
||||
{ label: '已确收', hideUnsigned: true, value: ctx => moneyWan(ctx.sumRev), color: 'text-blue-700' },
|
||||
{ label: '毛利', value: ctx => money(ctx.sumGross), color: 'text-green-700' },
|
||||
{ label: '成本', value: ctx => moneyWan(ctx.sumCost), color: 'text-rose-700' },
|
||||
{ label: '已回款', hideUnsigned: true, value: ctx => moneyWan(ctx.sumPay), color: 'text-amber-700' },
|
||||
{ label: '毛利', value: ctx => moneyWan(ctx.sumGross), color: 'text-green-700' },
|
||||
{ label: '成本(不含平台费用)', value: ctx => moneyWan(ctx.sumCost), color: 'text-rose-700' },
|
||||
{ label: '回款', hideUnsigned: true, value: ctx => moneyWan(ctx.sumPay), color: 'text-amber-700' },
|
||||
{ label: '已付', hideUnsigned: true, value: ctx => moneyWan(ctx.sumPaid), color: 'text-purple-700' },
|
||||
{ label: '现金流', hideUnsigned: true, value: ctx => { const v = ctx.sumPay - ctx.sumPaid; return { val: money(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null },
|
||||
{ label: '现金流(回款-支出)', hideUnsigned: true, value: ctx => { const v = ctx.sumPay - ctx.sumPaid; return { val: moneyWan(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null },
|
||||
{ label: '毛利率', hideUnsigned: true, value: ctx => ctx.sumRev && ctx.sumGross ? Math.round(ctx.sumGross / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' },
|
||||
{ label: '回款率', hideUnsigned: true, value: ctx => ctx.sumRev && ctx.sumPay ? Math.round(ctx.sumPay / ctx.sumRev * 100) + '%' : '—', color: 'text-slate-500' },
|
||||
{ label: '付款率', hideUnsigned: true, value: ctx => ctx.sumCost && ctx.sumPaid ? Math.round(ctx.sumPaid / ctx.sumCost * 100) + '%' : '—', color: 'text-slate-500' },
|
||||
@@ -277,11 +277,13 @@ function renderPlan() {
|
||||
});
|
||||
};
|
||||
const buildThead = (cols, sortKey) => {
|
||||
var noSortKeys = ['weight', 'project_type'];
|
||||
var h = '<thead><tr class="bg-slate-50 border-b border-slate-200">';
|
||||
cols.forEach(function(c) {
|
||||
var arrow = sortKey ? (sortKey === c.key ? (state.planSort.asc ? ' ↑' : ' ↓') : '') : '';
|
||||
var clickable = sortKey ? ' cursor-pointer select-none' : '';
|
||||
h += '<th class="p-2 text-center font-semibold align-middle' + clickable + '"' + (sortKey ? ' onclick="setPlanSort(\'' + sortKey + '|' + c.key + '\')"' : '') + '>' + c.label + arrow + '</th>';
|
||||
var canSort = sortKey && noSortKeys.indexOf(c.key) < 0;
|
||||
var arrow = canSort ? (sortKey === c.key ? (state.planSort.asc ? ' ↑' : ' ↓') : '') : '';
|
||||
var clickable = canSort ? ' cursor-pointer select-none' : '';
|
||||
h += '<th class="p-2 text-center font-semibold align-middle' + clickable + '"' + (canSort ? ' onclick="setPlanSort(\'' + sortKey + '|' + c.key + '\')"' : '') + '>' + c.label + arrow + '</th>';
|
||||
});
|
||||
h += '</tr></thead>';
|
||||
return h;
|
||||
@@ -322,13 +324,13 @@ function renderPlan() {
|
||||
});
|
||||
const thead = isUnsigned ? buildThead(UNSIGNED_COLS, 'uns') : buildThead(SIGNED_COLS, 'sig');
|
||||
const theadCols = isUnsigned ? 9 : 14;
|
||||
const tbody = rows.length ? `<tbody>${rows.join("")}</tbody>` : `<tr><td colspan="${theadCols}" class="p-6 text-center text-slate-400">${emptyText}</td></tr>`;
|
||||
const tbody = rows.length ? `<tbody style="line-height:0.95">${rows.join("")}</tbody>` : `<tr><td colspan="${theadCols}" class="p-6 text-center text-slate-400">${emptyText}</td></tr>`;
|
||||
|
||||
const cardGrid = isUnsigned ? 'grid-cols-4' : 'grid-cols-6';
|
||||
const cardGrid = isUnsigned ? 'grid-cols-4' : 'grid-cols-5';
|
||||
const subtitle = isUnsigned
|
||||
? '合同 → 毛利 → 成本 → 项目利润'
|
||||
: '合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流';
|
||||
return card(`<div class="grid ${cardGrid} gap-3 mb-3">${cards.map(([l, v, c]) => '<div class="metric-card p-3"><span class="flex items-center gap-2 text-sm text-slate-500">' + l + '</span><strong class="mt-2 block text-xl ' + c + '">' + v + '</strong></div>').join("")}</div>`, "p-4") +
|
||||
return `<div class="grid ${cardGrid} gap-1.5 mb-1.5">${cards.map(([l, v, c]) => '<div class="metric-card px-3 py-1.5"><span class="text-xs text-slate-500">' + l + '</span> <strong class="text-base ' + c + '">' + v + '</strong></div>').join("")}</div>` +
|
||||
card(`<h3 class="text-sm font-bold text-slate-700">项目财务明细</h3><p class="text-xs text-slate-400 mb-3">${subtitle}</p><div class="overflow-x-auto"><table class="w-full text-sm">${thead}${tbody}</table></div>`, "p-4");
|
||||
};
|
||||
|
||||
@@ -342,14 +344,14 @@ function renderPlan() {
|
||||
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 cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="planOpenPfEditModal(${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" onclick="event.stopPropagation()">${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}</td><td class="p-2 text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}</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 align-middle text-sm">${grossR}</td><td class="p-2 text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="p-2 text-center align-middle text-sm">${payR}</td><td class="p-2 text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="p-2 text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="p-2 text-center align-middle text-sm">${paidR}</td><td class="p-2 text-center font-semibold align-middle ${cfCls}">${cf}</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">${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 align-middle text-sm">${paidR}</td><td class="p-2 text-center font-semibold align-middle ${cfCls}">${cf}</td></tr>`;
|
||||
};
|
||||
// 待签约简版行
|
||||
const tdRowUnsigned = (pf, cost, gross) => {
|
||||
const profit = gross;
|
||||
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="planOpenPfEditModal(${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" onclick="event.stopPropagation()">${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}</td><td class="p-2 text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}</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>`;
|
||||
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">${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 align-middle text-sm">${pf.sign_month || '—'}</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 text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="p-2 text-center font-semibold align-middle ${pfCls}">${pfVal}</td></tr>`;
|
||||
};
|
||||
|
||||
if (state.planView === 'monthly') {
|
||||
@@ -1016,13 +1018,13 @@ function renderPlanOverview() {
|
||||
];
|
||||
// 字段映射: sign, revenue, gross, cost, paid, payment, cashflow(=payment-paid)
|
||||
const fmFields = ['sign','revenue','gross','cost','paid','payment',null];
|
||||
var thead = '<tr class="border-b border-slate-200"><th class="py-2 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-2 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
for (var mi = 0; mi < 12; mi++) { thead += '<th class="py-2 text-right font-semibold text-slate-600 whitespace-nowrap">' + mLbl[mi] + '</th>'; }
|
||||
var thead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
for (var mi = 0; mi < 12; mi++) { thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + mLbl[mi] + '</th>'; }
|
||||
thead += '</tr>';
|
||||
var tbody = '';
|
||||
for (var ri = 0; ri < 7; ri++) {
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-2 text-left font-semibold text-slate-700 whitespace-nowrap">' + LABELS[ri] + '</td>';
|
||||
rowHtml += '<td class="py-2 text-right font-semibold text-slate-800 whitespace-nowrap">' + moneyIntL(annualVals[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">' + LABELS[ri] + '</td>';
|
||||
rowHtml += '<td class="py-1.5 text-right font-semibold text-slate-800 whitespace-nowrap">' + moneyIntL(annualVals[ri]) + '</td>';
|
||||
for (var mi = 0; mi < 12; mi++) {
|
||||
var val = 0;
|
||||
if (fm[mi]) {
|
||||
@@ -1032,7 +1034,7 @@ function renderPlanOverview() {
|
||||
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-2 text-right text-sm whitespace-nowrap ' + cls + '">' + moneyIntL(val) + '</td>';
|
||||
rowHtml += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + moneyIntL(val) + '</td>';
|
||||
}
|
||||
tbody += rowHtml + '</tr>';
|
||||
}
|
||||
@@ -1047,8 +1049,8 @@ function renderPlanOverview() {
|
||||
}
|
||||
var expAnnual = 0; for (var ei = 0; ei < 12; ei++) expAnnual += expMonthly[ei];
|
||||
var grossAnnual = 0; for (var ei = 0; ei < 12; ei++) grossAnnual += (fm[ei] ? fm[ei].gross : 0);
|
||||
var deptThead = '<tr class="border-b border-slate-200"><th class="py-2 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-2 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
for (var dmi = 0; dmi < 12; dmi++) { deptThead += '<th class="py-2 text-right font-semibold text-slate-600 whitespace-nowrap">' + mLbl[dmi] + '</th>'; }
|
||||
var deptThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
for (var dmi = 0; dmi < 12; dmi++) { deptThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + mLbl[dmi] + '</th>'; }
|
||||
deptThead += '</tr>';
|
||||
var deptRows = [
|
||||
{ label: '毛利', vals: fm.map(function(x) { return x ? x.gross : 0; }), annual: grossAnnual },
|
||||
@@ -1059,12 +1061,12 @@ function renderPlanOverview() {
|
||||
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-2 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">' + dr.label + '</td>';
|
||||
var annualCls = dri === 2 ? (dr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
|
||||
rowHtml += '<td class="py-2 text-right ' + rowCls + ' ' + annualCls + ' whitespace-nowrap">' + moneyIntL(dr.annual) + '</td>';
|
||||
rowHtml += '<td class="py-1.5 text-right ' + rowCls + ' ' + annualCls + ' whitespace-nowrap">' + moneyIntL(dr.annual) + '</td>';
|
||||
for (var dmi = 0; dmi < 12; dmi++) {
|
||||
var cellCls = dri === 2 ? (dr.vals[dmi] >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
|
||||
rowHtml += '<td class="py-2 text-right text-sm whitespace-nowrap ' + cellCls + '">' + moneyIntL(dr.vals[dmi]) + '</td>';
|
||||
rowHtml += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cellCls + '">' + moneyIntL(dr.vals[dmi]) + '</td>';
|
||||
}
|
||||
deptTbody += rowHtml + '</tr>';
|
||||
}
|
||||
@@ -1094,22 +1096,22 @@ function renderPlanOverview() {
|
||||
var cfdr = cfRows[cfri];
|
||||
var cfRowCls = cfri === 2 ? 'font-semibold' : '';
|
||||
var cfValCls = cfri === 2 ? (cfdr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
|
||||
var cfRowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-2 text-left ' + cfRowCls + ' text-slate-700 whitespace-nowrap">' + cfdr.label + '</td>';
|
||||
cfRowHtml += '<td class="py-2 text-right ' + cfRowCls + ' ' + cfValCls + ' whitespace-nowrap">' + moneyIntL(cfdr.annual) + '</td>';
|
||||
var cfRowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + cfRowCls + ' text-slate-700 whitespace-nowrap">' + cfdr.label + '</td>';
|
||||
cfRowHtml += '<td class="py-1.5 text-right ' + cfRowCls + ' ' + cfValCls + ' whitespace-nowrap">' + moneyIntL(cfdr.annual) + '</td>';
|
||||
for (var cfdmi = 0; cfdmi < 12; cfdmi++) {
|
||||
var cellCls = cfValCls;
|
||||
if (cfri === 2) cellCls = blCfMonthly[cfdmi] >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
cfRowHtml += '<td class="py-2 text-right text-sm whitespace-nowrap ' + cellCls + '">' + moneyIntL(cfdr.vals[cfdmi]) + '</td>';
|
||||
cfRowHtml += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cellCls + '">' + moneyIntL(cfdr.vals[cfdmi]) + '</td>';
|
||||
}
|
||||
cfTbody += cfRowHtml + '</tr>';
|
||||
}
|
||||
var cfCard = card('<h3 class="text-sm font-bold text-slate-700">业务线现金流</h3><p class="text-xs text-slate-400 mb-3">项目现金流 - 平台费用(已付)= 业务线现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4');
|
||||
|
||||
document.querySelector("#planOverview").innerHTML = '<div class="grid gap-5">' +
|
||||
document.querySelector("#planOverview").innerHTML = '<div class="grid gap-2.5">' +
|
||||
card('<h3 class="text-sm font-bold text-slate-700">业务(项目)财务概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 已付</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
|
||||
deptCard +
|
||||
cfCard +
|
||||
'<div class="grid grid-cols-3 gap-5">' +
|
||||
'<div class="grid grid-cols-3 gap-2.5">' +
|
||||
card('<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度签约趋势</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="planChartSign"></canvas></div>', 'p-4') +
|
||||
card('<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度确收与毛利</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="planChartRev"></canvas></div>', 'p-4') +
|
||||
card('<div class="mb-3 flex items-center justify-between"><h2 class="text-sm font-bold text-slate-600">月度回款与已付</h2><span class="text-xs text-slate-400">2026</span></div><div style="position:relative;height:200px"><canvas id="planChartCash"></canvas></div>', 'p-4') +
|
||||
@@ -1128,59 +1130,122 @@ function renderPlanQuarterlyOverview() {
|
||||
const fm = computePlanMonthly(planFinances);
|
||||
const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN");
|
||||
const card = (body, cls) => '<div class="card ' + (cls || 'p-4') + '">' + body + '</div>';
|
||||
const LABELS = ['合同金额','确收金额','确收毛利','成本','支出'];
|
||||
const qLbl = ['Q1','Q2','Q3','Q4'];
|
||||
const qRanges = [[0,1,2],[3,4,5],[6,7,8],[9,10,11]];
|
||||
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.paid];
|
||||
const fmFields = ['sign','revenue','gross','cost','paid'];
|
||||
var qRanges = [[0,1,2],[3,4,5],[6,7,8],[9,10,11]];
|
||||
|
||||
// Calculate quarterly sums
|
||||
var qData = [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]];
|
||||
// 财务概览: 7 rows
|
||||
var LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流'];
|
||||
var annualVals = [annualSums2.sign, annualSums2.revenue, annualSums2.gross, annualSums2.cost, annualSums2.paid, annualSums2.payment, annualSums2.payment - annualSums2.paid];
|
||||
var fmFields = ['sign','revenue','gross','cost','paid','payment',null];
|
||||
var qData = [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]];
|
||||
for (var qi = 0; qi < 4; qi++) {
|
||||
for (var ri = 0; ri < 5; ri++) {
|
||||
for (var ri = 0; ri < 7; ri++) {
|
||||
var sum = 0;
|
||||
for (var mi = 0; mi < 3; mi++) {
|
||||
var idx = qRanges[qi][mi];
|
||||
if (fm[idx]) {
|
||||
var fk = fmFields[ri]; if (fk) sum += fm[idx][fk] || 0;
|
||||
if (ri === 6) { sum += (fm[idx].payment||0) - (fm[idx].paid||0); }
|
||||
else { var fk = fmFields[ri]; if (fk) sum += fm[idx][fk] || 0; }
|
||||
}
|
||||
}
|
||||
qData[ri][qi] = sum;
|
||||
}
|
||||
}
|
||||
|
||||
var thead = '<tr class="border-b border-slate-200"><th class="py-2 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-2 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
thead += '<th class="py-2 text-right font-semibold text-slate-600 whitespace-nowrap">Q1</th>';
|
||||
thead += '<th class="py-2 text-right font-semibold text-slate-600 whitespace-nowrap">Q2</th><th class="py-2 text-right font-semibold text-slate-400 whitespace-nowrap">环比</th>';
|
||||
thead += '<th class="py-2 text-right font-semibold text-slate-600 whitespace-nowrap">Q3</th><th class="py-2 text-right font-semibold text-slate-400 whitespace-nowrap">环比</th>';
|
||||
thead += '<th class="py-2 text-right font-semibold text-slate-600 whitespace-nowrap">Q4</th><th class="py-2 text-right font-semibold text-slate-400 whitespace-nowrap">环比</th>';
|
||||
var thead = '<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>';
|
||||
thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q1</th>';
|
||||
thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q2</th><th class="py-1.5 text-right font-semibold text-slate-400 whitespace-nowrap">环比</th>';
|
||||
thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q3</th><th class="py-1.5 text-right font-semibold text-slate-400 whitespace-nowrap">环比</th>';
|
||||
thead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q4</th><th class="py-1.5 text-right font-semibold text-slate-400 whitespace-nowrap">环比</th>';
|
||||
thead += '</tr>';
|
||||
|
||||
var qoq = function(cur, prev) { if (!prev) return '<span class="text-slate-300">—</span>'; var pct = Math.round((cur - prev) / prev * 100); var cls = pct >= 0 ? 'text-green-600' : 'text-red-600'; var sign = pct >= 0 ? '+' : ''; return '<span class="' + cls + '">' + sign + pct + '%</span>'; };
|
||||
|
||||
var tbody = '';
|
||||
for (var ri = 0; ri < 5; ri++) {
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-2 text-left font-semibold text-slate-700 whitespace-nowrap">' + LABELS[ri] + '</td>';
|
||||
rowHtml += '<td class="py-2 text-right font-semibold text-slate-800 whitespace-nowrap">' + moneyIntL(annualVals[ri]) + '</td>';
|
||||
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">' + moneyIntL(annualVals[ri]) + '</td>';
|
||||
for (var qi = 0; qi < 4; qi++) {
|
||||
var val = qData[ri][qi];
|
||||
var cls = 'text-slate-700';
|
||||
rowHtml += '<td class="py-2 text-right text-sm whitespace-nowrap ' + cls + '">' + moneyIntL(val) + '</td>';
|
||||
if (qi > 0) {
|
||||
rowHtml += '<td class="py-2 text-right text-sm whitespace-nowrap">' + qoq(val, qData[ri][qi - 1]) + '</td>';
|
||||
}
|
||||
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>';
|
||||
if (qi > 0) { rowHtml += '<td class="py-1.5 text-right text-sm whitespace-nowrap">' + qoq(val, qData[ri][qi - 1]) + '</td>'; }
|
||||
}
|
||||
tbody += rowHtml + '</tr>';
|
||||
}
|
||||
|
||||
document.querySelector("#planQuarterlyOverview").innerHTML = '<div class="grid gap-5">' +
|
||||
card('<h3 class="text-sm font-bold text-slate-700">业务(项目)财务季度概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
|
||||
// 业务线经营情况: quarterly
|
||||
var planExp = state.data.planExpense || [];
|
||||
var expQ = [0,0,0,0];
|
||||
for (var ei = 0; ei < planExp.length; ei++) {
|
||||
var em = (planExp[ei].expense_month || '').substring(0, 7);
|
||||
var emi = parseInt(em.substring(5)) - 1;
|
||||
if (emi >= 0 && emi < 12) { var eqi = Math.floor(emi / 3); expQ[eqi] += parseFloat(planExp[ei].amount || 0); }
|
||||
}
|
||||
var expAnnual = 0; for (var ei = 0; ei < 4; ei++) expAnnual += expQ[ei];
|
||||
var grossQ = [qData[2][0], qData[2][1], qData[2][2], qData[2][3]];
|
||||
var grossAnnual = annualSums2.gross;
|
||||
var deptThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
for (var qi = 0; qi < 4; qi++) { deptThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">Q' + (qi+1) + '</th>'; }
|
||||
deptThead += '</tr>';
|
||||
var deptRows = [
|
||||
{ label: '毛利', vals: grossQ, annual: grossAnnual },
|
||||
{ label: '平台费用', vals: expQ, annual: expAnnual },
|
||||
{ label: '利润', vals: grossQ.map(function(v, i) { return v - expQ[i]; }), annual: grossAnnual - expAnnual }
|
||||
];
|
||||
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">' + 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 qi = 0; qi < 4; qi++) {
|
||||
var cellCls = dri === 2 ? (dr.vals[qi] >= 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[qi]) + '</td>';
|
||||
}
|
||||
deptTbody += rowHtml + '</tr>';
|
||||
}
|
||||
|
||||
// 业务线现金流: quarterly
|
||||
var cfQ = [qData[5][0], qData[5][1], qData[5][2], qData[5][3]];
|
||||
var cfAnnual = annualSums2.payment;
|
||||
var expPaidQ = [0,0,0,0];
|
||||
for (var ei = 0; ei < planExp.length; ei++) {
|
||||
var em = (planExp[ei].expense_month || '').substring(0, 7);
|
||||
var emi = parseInt(em.substring(5)) - 1;
|
||||
if (emi >= 0 && emi < 12) { var eqi = Math.floor(emi / 3); expPaidQ[eqi] += parseFloat(planExp[ei].incurred_amount || 0); }
|
||||
}
|
||||
var expPaidAnnual = 0; for (var ei = 0; ei < 4; ei++) expPaidAnnual += expPaidQ[ei];
|
||||
var blCfQ = cfQ.map(function(v, i) { return v - expPaidQ[i]; });
|
||||
var blCfAnnual = cfAnnual - expPaidAnnual;
|
||||
var cfRows = [
|
||||
{ label: '项目现金流', vals: cfQ, annual: cfAnnual },
|
||||
{ label: '平台费用(已付)', vals: expPaidQ, annual: expPaidAnnual },
|
||||
{ label: '业务线现金流', vals: blCfQ, annual: blCfAnnual }
|
||||
];
|
||||
var cfTbody = '';
|
||||
for (var cfri = 0; cfri < 3; cfri++) {
|
||||
var cfdr = 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">' + cfdr.label + '</td>';
|
||||
var annualCls = cfri === 2 ? (cfdr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
|
||||
rowHtml += '<td class="py-1.5 text-right ' + cfRowCls + ' ' + annualCls + ' whitespace-nowrap">' + moneyIntL(cfdr.annual) + '</td>';
|
||||
for (var qi = 0; qi < 4; qi++) {
|
||||
var cellCls = cfri === 2 ? (blCfQ[qi] >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
|
||||
rowHtml += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cellCls + '">' + moneyIntL(cfdr.vals[qi]) + '</td>';
|
||||
}
|
||||
cfTbody += rowHtml + '</tr>';
|
||||
}
|
||||
|
||||
document.querySelector("#planQuarterlyOverview").innerHTML = '<div class="grid gap-2.5">' +
|
||||
card('<h3 class="text-sm font-bold text-slate-700">业务(项目)财务季度概览</h3><p class="text-xs text-slate-400 mb-3">合同 → 确收 → 毛利 → 成本 → 支出 → 回款 → 项目现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + thead + '</thead><tbody>' + tbody + '</tbody></table></div>', 'p-4') +
|
||||
card('<h3 class="text-sm font-bold text-slate-700">业务线经营情况</h3><p class="text-xs text-slate-400 mb-3">毛利 - 平台费用 = 利润</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + deptTbody + '</tbody></table></div>', 'p-4') +
|
||||
card('<h3 class="text-sm font-bold text-slate-700">业务线现金流</h3><p class="text-xs text-slate-400 mb-3">项目现金流 - 平台费用(已付)= 业务线现金流</p><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4') +
|
||||
'</div>';
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
}
|
||||
@@ -1213,7 +1278,7 @@ function planInlineSelect(name, pfId, field, currentVal, options) {
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
opts += '<option value="' + options[i] + '"' + (options[i] === currentVal ? ' selected' : '') + '>' + options[i] + '</option>';
|
||||
}
|
||||
return '<select onchange="planInlineUpdate(' + pfId + ', \'' + field + '\'' + ', this.value)" class="text-sm bg-transparent border-0 cursor-pointer text-center" style="appearance:auto;-webkit-appearance:auto;vertical-align:middle">' + opts + '</select>';
|
||||
return '<select onchange="planInlineUpdate(' + pfId + ', \'' + field + '\'' + ', this.value)" class="text-sm bg-transparent border-0 cursor-pointer text-center" style="appearance:none;-webkit-appearance:none;vertical-align:middle;padding-right:14px;background:url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2210%22 height=%2210%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%23999%22 stroke-width=%222%22%3E%3Cpolyline points=%226 9 12 15 18 9%22%3E%3C/polyline%3E%3C/svg%3E') no-repeat right center">' + opts + '</select>';
|
||||
}
|
||||
|
||||
window.planInlineUpdate = async (pfId, field, value) => {
|
||||
|
||||
Reference in New Issue
Block a user