|
|
|
|
@@ -73,47 +73,47 @@ function renderFinance() {
|
|
|
|
|
const budget = JSON.parse(pf.budget_data || "[]");
|
|
|
|
|
budget.forEach(b => { budgetMap[(b.month || "").replace("-", "_")] = b; });
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
const isRevView = state.finView !== "cashflow";
|
|
|
|
|
const isRevView = state.finView !== "cashflow" && state.finView !== "overview" && state.finView !== "monthly";
|
|
|
|
|
const mCols = months.map(m => {
|
|
|
|
|
const b = budgetMap[m] || {};
|
|
|
|
|
if (isRevView) {
|
|
|
|
|
const rev = b.rev || 0;
|
|
|
|
|
const gross = b.gross || 0;
|
|
|
|
|
return `<td class="p-2 text-right whitespace-nowrap"><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="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>`;
|
|
|
|
|
} else {
|
|
|
|
|
const payment = b.payment || 0;
|
|
|
|
|
const cost = b.cost || 0;
|
|
|
|
|
return `<td class="p-2 text-right whitespace-nowrap"><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="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>`;
|
|
|
|
|
}
|
|
|
|
|
}).join("");
|
|
|
|
|
const totalCol = (() => {
|
|
|
|
|
if (isRevView) {
|
|
|
|
|
const totalRev = pf.total_rev || 0;
|
|
|
|
|
const totalGross = pf.total_gross || 0;
|
|
|
|
|
return `<td class="p-2 text-right whitespace-nowrap 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="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>`;
|
|
|
|
|
} 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-right whitespace-nowrap 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="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>`;
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
const sm = pf.sign_month || "";
|
|
|
|
|
const signMonthCell = `<td class="p-2 text-center text-sm"><span class="pf-sm-text cursor-pointer hover:text-blue-600" id="pf-sm-${pf.id}" onclick="event.stopPropagation(); editPfSignMonth(event, ${pf.id})">${sm || '—'}</span></td>`;
|
|
|
|
|
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">${esc(pf.customer_name)}</td><td class="p-2 text-sm text-center">${esc(pf.business_type)}</td><td class="p-2 text-sm text-center">${pf.status === "已签约" ? badge("已签约") : pf.status === "流程中" ? badge("流程中","blue") : badge("待签约","amber")}</td>${signMonthCell}<td class="p-2 text-center text-sm">${money(pf.sign_amount)}</td>${mCols}${totalCol}<td class="p-2 text-sm text-slate-500 text-center">${esc(pf.sales_person) || "—"}</td><td class="p-2 text-sm text-slate-500 text-center">${esc(pf.owner) || "—"}</td></tr>`;
|
|
|
|
|
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">${esc(pf.customer_name)}</td><td class="p-2 text-sm text-center">${esc(pf.business_type)}</td><td class="p-2 text-sm text-center">${pf.status === "已签约" ? badge("已签约") : pf.status === "流程中" ? badge("流程中","blue") : badge("待签约","amber")}</td>${signMonthCell}<td class="p-2 text-center text-sm">${money(pf.sign_amount)}</td>${mCols}${totalCol}</tr>`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
document.querySelector("#finance").innerHTML = `<div class="grid gap-4">
|
|
|
|
|
<div class="grid grid-cols-6 gap-3">
|
|
|
|
|
${[["已签项目","" + signed.length,"file-check-2"],["签约金额",moneyInt(sumSign),"coins"],["流程项目","" + inContract.length,"file-clock"],["流程金额",moneyInt(sumContract),"clock"],["待签项目","" + pending.length,"file-question"],["待签金额",moneyInt(sumPending),"hourglass"]].map(([l,v,icon]) => `<div class="metric-card"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide="${icon}" style="width:14px;height:14px"></i>${l}</span><strong class="mt-2 block text-2xl">${v}</strong></div>`).join("")}
|
|
|
|
|
<div class="grid grid-cols-4 gap-3">
|
|
|
|
|
${[["已签项目","" + signed.length,"file-check-2"],["签约金额",moneyInt(sumSign),"coins"],["待签项目","" + pending.length,"file-question"],["待签金额",moneyInt(sumPending),"hourglass"]].map(([l,v,icon]) => `<div class="metric-card"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide="${icon}" style="width:14px;height:14px"></i>${l}</span><strong class="mt-2 block text-2xl">${v}</strong></div>`).join("")}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="grid grid-cols-5 gap-3">
|
|
|
|
|
${[["本月确收",moneyInt(thisMonthRev),"trending-up"],["本月毛利",moneyInt(thisMonthGross),"percent"],["本月回款",moneyInt(monthPayment),"wallet"],["本月费用",moneyInt(monthCost),"receipt"],["本月现金流",moneyInt(monthCashflow),"repeat"]].map(([l,v,icon]) => `<div class="metric-card"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide="${icon}" style="width:14px;height:14px"></i>${l}</span><strong class="mt-2 block text-2xl">${v}</strong></div>`).join("")}
|
|
|
|
|
${[["本月确收",moneyInt(thisMonthRev),"trending-up"],["本月毛利",moneyInt(thisMonthGross),"percent"],["本月回款",moneyInt(monthPayment),"wallet"],["本月应付",moneyInt(monthCost),"receipt"],["本月现金流",moneyInt(monthCashflow),"repeat"]].map(([l,v,icon]) => `<div class="metric-card"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide="${icon}" style="width:14px;height:14px"></i>${l}</span><strong class="mt-2 block text-2xl">${v}</strong></div>`).join("")}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex justify-between items-center"><div class="flex items-center gap-1" id="finViewToggle"><button class="btn btn-sm ${state.finView !== 'cashflow' ? 'btn-primary' : 'btn-ghost'} px-2 py-1.5" data-view="rev" onclick="setFinView('rev')" title="确收/毛利视图"><i data-lucide="trending-up" style="width:16px;height:16px"></i><span class="text-xs ml-1">确收/毛利</span></button><button class="btn btn-sm ${state.finView === 'cashflow' ? 'btn-primary' : 'btn-ghost'} px-2 py-1.5" data-view="cashflow" onclick="setFinView('cashflow')" title="回款/费用视图"><i data-lucide="wallet" style="width:16px;height:16px"></i><span class="text-xs ml-1">回款/费用</span></button></div><button class="btn btn-primary btn-sm" onclick="openFinanceModal()"><i data-lucide="plus"></i>新增财务项目</button></div>
|
|
|
|
|
<div class="flex justify-between items-center"><div class="flex items-center gap-1" id="finViewToggle"><button class="btn btn-sm ${state.finView === 'overview' ? 'btn-primary' : 'btn-ghost'} px-2 py-1.5" data-view="overview" onclick="setFinView('overview')" title="总视图"><i data-lucide="layout-dashboard" style="width:16px;height:16px"></i><span class="text-xs ml-1">总视图</span></button><button class="btn btn-sm ${state.finView === 'monthly' ? 'btn-primary' : 'btn-ghost'} px-2 py-1.5" data-view="monthly" onclick="setFinView('monthly')" title="月度视图"><i data-lucide="calendar-days" style="width:16px;height:16px"></i><span class="text-xs ml-1">月度视图</span></button></div><button class="btn btn-primary btn-sm" onclick="openFinanceModal()"><i data-lucide="plus"></i>新增财务项目</button></div>
|
|
|
|
|
<div id="financeModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closeFinanceModal()"><div class="bg-white rounded-2xl shadow-2xl w-full max-w-5xl 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="financeModalTitle">新增项目财务</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="financeDeleteBtn" onclick="deleteFinanceItem()"><i data-lucide="trash-2"></i>删除</button><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closeFinanceModal()"><i data-lucide="x"></i></button></div></div>
|
|
|
|
|
<div class="finance-tabs">
|
|
|
|
|
<button class="finance-tab active" data-tab="info" onclick="switchFinanceTab('info')"><i data-lucide="info" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>基本信息</button>
|
|
|
|
|
<button class="finance-tab" data-tab="budget" onclick="switchFinanceTab('budget')"><i data-lucide="calendar" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>月度预算</button>
|
|
|
|
|
<button class="finance-tab" data-tab="budget" onclick="switchFinanceTab('budget')"><i data-lucide="calendar" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>月度流水</button>
|
|
|
|
|
</div>
|
|
|
|
|
<form onsubmit="createFinance(event)" class="p-8 grid gap-6"><input type="hidden" name="pf_id" id="pf-id-input" value="">
|
|
|
|
|
<div id="financeTabInfo">
|
|
|
|
|
@@ -143,7 +143,7 @@ function renderFinance() {
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div id="financeTabBudget" class="hidden">
|
|
|
|
|
<div class="grid grid-cols-4 gap-3 mb-4" id="budgetSummary">
|
|
|
|
|
<div class="grid grid-cols-5 gap-3 mb-4" id="budgetSummary">
|
|
|
|
|
<div class="bg-blue-50 rounded-lg p-3 text-center border border-blue-100">
|
|
|
|
|
<p class="text-xs text-blue-600 font-medium">总确收</p>
|
|
|
|
|
<p class="text-lg font-bold text-blue-700" id="budgetTotalRev">¥0</p>
|
|
|
|
|
@@ -157,18 +157,66 @@ function renderFinance() {
|
|
|
|
|
<p class="text-lg font-bold text-amber-700" id="budgetTotalPayment">¥0</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="bg-rose-50 rounded-lg p-3 text-center border border-rose-100" id="budgetTotalCostCard">
|
|
|
|
|
<p class="text-xs text-rose-600 font-medium">总费用</p>
|
|
|
|
|
<p class="text-xs text-rose-600 font-medium">总应付</p>
|
|
|
|
|
<p class="text-lg font-bold text-rose-700" id="budgetTotalCost">¥0</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="bg-purple-50 rounded-lg p-3 text-center border border-purple-100">
|
|
|
|
|
<p class="text-xs text-purple-600 font-medium">总已付</p>
|
|
|
|
|
<p class="text-lg font-bold text-purple-700" id="budgetTotalPaid">¥0</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<table class="w-full text-sm border border-slate-200 rounded-lg overflow-hidden" id="budgetTable">
|
|
|
|
|
<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-right font-medium text-slate-500">费用</th><th class="p-2.5 w-8"></th></tr></thead>
|
|
|
|
|
<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-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="budgetTbody"></tbody>
|
|
|
|
|
</table>
|
|
|
|
|
<button type="button" class="btn btn-ghost btn-sm mt-3" onclick="addBudgetRow()"><i data-lucide="plus"></i>添加月份</button>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex justify-end gap-3 pt-2"><button type="button" class="btn btn-ghost btn-sm px-6" onclick="closeFinanceModal()">取消</button><button type="submit" class="btn btn-primary btn-sm px-8">保存</button></div></form></div></div>
|
|
|
|
|
${card(`<div class="flex items-center justify-between mb-3"><h3 class="font-bold text-slate-700">项目明细 <span class="text-slate-400 font-normal">(${pfs.length})</span></h3></div><div class="flex gap-2 mb-3">${[["已签约","已签约"],["流程中","流程中"],["待签约","待签约"]].map(([k,v]) => `<button class="btn btn-sm ${state.finFilter === k ? 'btn-primary' : 'btn-ghost'}" onclick="state.finFilter='${k}';renderFinance()">${v} (${pfs.filter(x=>x.status===k).length})</button>`).join("")}</div><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2 text-center font-semibold">项目名称</th><th class="p-2 text-center font-semibold">类型</th><th class="p-2 text-center font-semibold">状态</th><th class="p-2 text-center font-semibold">签约月份</th><th class="p-2 text-center font-semibold">签约金额</th>${monthLabels.map(l => `<th class="p-2 text-center font-semibold">${l}<br><span class="text-xs text-slate-400">${state.finView !== 'cashflow' ? '确收/毛利' : '回款/费用'}</span></th>`).join("")}<th class="p-2 text-center font-semibold">总计<br><span class="text-xs text-slate-400">${state.finView !== 'cashflow' ? '确收/毛利' : '回款/费用'}</span></th><th class="p-2 text-center font-semibold">商务负责人</th><th class="p-2 text-center font-semibold">经营负责人</th></tr></thead><tbody>${pfs.filter(x => x.status === state.finFilter).map(renderPfRow).join("")}</tbody></table></div>`, "p-4")}
|
|
|
|
|
${state.finView === 'monthly' ? (() => {
|
|
|
|
|
const allPfs = pfs.filter(x => x.status === state.finFilter);
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const defaultMonth = now.getFullYear() + "-" + String(now.getMonth()+1).padStart(2,"0");
|
|
|
|
|
if (!state.finMonth) state.finMonth = defaultMonth;
|
|
|
|
|
const selMonth = state.finMonth;
|
|
|
|
|
// 收集所有有数据的月份 + 当前月
|
|
|
|
|
const monthSet = new Set([defaultMonth]);
|
|
|
|
|
allPfs.forEach(pf => { let bd = []; try { bd = JSON.parse(pf.budget_data || "[]"); } catch(e) {} bd.forEach(b => { if (b.month) monthSet.add(b.month); }); });
|
|
|
|
|
const sortedMonths = [...monthSet].sort().reverse();
|
|
|
|
|
const monthOpts = sortedMonths.map(m => `<option value="${m}" ${m === selMonth ? 'selected' : ''}>${m}</option>`).join("");
|
|
|
|
|
const fmt = (v) => v ? `<span class="font-medium">${money(v)}</span>` : '<span class="text-slate-300">—</span>';
|
|
|
|
|
const fmtDiff = (v) => { if (!v) return '<span class="text-slate-300">—</span>'; return `<span class="${v > 0 ? 'text-amber-600 font-medium' : 'text-green-600 font-medium'}">${money(Math.abs(v))}</span>`; };
|
|
|
|
|
const rows = [];
|
|
|
|
|
let sumRev=0, sumPay=0, sumCost=0, sumPaid=0;
|
|
|
|
|
allPfs.forEach(pf => {
|
|
|
|
|
let bd = []; try { bd = JSON.parse(pf.budget_data || "[]"); } catch(e) {}
|
|
|
|
|
const b = bd.find(x => (x.month||"") === selMonth) || {};
|
|
|
|
|
const rev = Math.round(parseFloat(b.rev||0)||0);
|
|
|
|
|
const payment = Math.round(parseFloat(b.payment||0)||0);
|
|
|
|
|
const cost = Math.round(parseFloat(b.cost||0)||0);
|
|
|
|
|
const paid = Math.round(parseFloat(b.paid||0)||0);
|
|
|
|
|
if (!rev && !payment && !cost && !paid) return;
|
|
|
|
|
const payDiff = rev - payment;
|
|
|
|
|
const costDiff = cost - paid;
|
|
|
|
|
const cashflow = payment - paid;
|
|
|
|
|
sumRev+=rev; sumPay+=payment; sumCost+=cost; sumPaid+=paid;
|
|
|
|
|
rows.push(`<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">${esc(pf.customer_name)}</td><td class="p-2 text-sm text-center text-slate-500">${esc(pf.business_type)}</td><td class="p-2 text-sm text-center">${pf.status === '已签约' ? badge('已签约') : pf.status === '流程中' ? badge('流程中','blue') : badge('待签约','amber')}</td><td class="p-2 text-center text-blue-700 align-middle">${fmt(rev)}</td><td class="p-2 text-center text-amber-700 align-middle">${fmt(payment)}</td><td class="p-2 text-center align-middle">${fmtDiff(payDiff)}</td><td class="p-2 text-center text-rose-700 align-middle">${fmt(cost)}</td><td class="p-2 text-center text-purple-700 align-middle">${fmt(paid)}</td><td class="p-2 text-center align-middle">${fmtDiff(costDiff)}</td><td class="p-2 text-center font-semibold align-middle ${cashflow >= 0 ? 'text-green-600' : 'text-red-600'}">${cashflow ? money(cashflow) : '<span class="text-slate-300">—</span>'}</td></tr>`);
|
|
|
|
|
});
|
|
|
|
|
return card(`<div class="flex items-center justify-between mb-3"><h3 class="font-bold text-slate-700">月度视图 <span class="text-slate-400 font-normal">(${allPfs.length} 项目)</span></h3></div><div class="flex items-center justify-between mb-3"><div class="flex gap-2">${[["已签约","已签约"],["流程中","流程中"],["待签约","待签约"]].map(([k,v]) => `<button class="btn btn-sm ${state.finFilter === k ? 'btn-primary' : 'btn-ghost'}" onclick="state.finFilter='${k}';renderFinance()">${v} (${pfs.filter(x=>x.status===k).length})</button>`).join("")}</div><label class="inline-flex items-center gap-1 text-sm text-slate-500 cursor-pointer relative"><i data-lucide="calendar" style="width:14px;height:14px"></i><select onchange="state.finMonth=this.value;renderFinance()" class="bg-transparent border-0 text-sm text-slate-600 font-medium cursor-pointer pr-5" style="appearance:none;-webkit-appearance:none;-moz-appearance:none;outline:none">${monthOpts}</select><i data-lucide="chevron-down" style="width:14px;height:14px;position:absolute;right:0;top:50%;transform:translateY(-50%);pointer-events:none"></i></label></div><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2 text-center font-semibold align-middle">项目名称</th><th class="p-2 text-center font-semibold align-middle">类型</th><th class="p-2 text-center font-semibold align-middle">状态</th><th class="p-2 text-center font-semibold align-middle text-blue-600">已确收</th><th class="p-2 text-center font-semibold align-middle text-amber-600">已回款</th><th class="p-2 text-center font-semibold align-middle">回款差额</th><th class="p-2 text-center font-semibold align-middle text-rose-600">应付</th><th class="p-2 text-center font-semibold align-middle text-purple-600">已付</th><th class="p-2 text-center font-semibold align-middle">应付差额</th><th class="p-2 text-center font-semibold align-middle text-slate-700">现金流</th></tr></thead><tbody>${rows.length ? rows.join("") : '<tr><td colspan="10" class="p-6 text-center text-slate-400">该月份暂无数据</td></tr>'}<tr class="border-t-2 border-slate-200 bg-slate-50 font-bold"><td class="p-2 text-center" colspan="3">合计</td><td class="p-2 text-center text-blue-700 align-middle">${money(sumRev)}</td><td class="p-2 text-center text-amber-700 align-middle">${money(sumPay)}</td><td class="p-2 text-center align-middle">${fmtDiff(sumRev - sumPay)}</td><td class="p-2 text-center text-rose-700 align-middle">${money(sumCost)}</td><td class="p-2 text-center text-purple-700 align-middle">${money(sumPaid)}</td><td class="p-2 text-center align-middle">${fmtDiff(sumCost - sumPaid)}</td><td class="p-2 text-center font-semibold align-middle ${sumPay - sumPaid >= 0 ? 'text-green-600' : 'text-red-600'}">${money(sumPay - sumPaid)}</td></tr></tbody></table></div>`, "p-4");
|
|
|
|
|
})() : (() => {
|
|
|
|
|
const calcTotals = (pf) => {
|
|
|
|
|
let budget = []; try { budget = JSON.parse(pf.budget_data || "[]"); } catch (e) {}
|
|
|
|
|
let rev = 0, payment = 0, cost = 0;
|
|
|
|
|
budget.forEach(b => { rev += parseFloat(b.rev||0)||0; payment += parseFloat(b.payment||0)||0; cost += parseFloat(b.cost||0)||0; });
|
|
|
|
|
const paid = parseFloat(pf.total_paid) || 0;
|
|
|
|
|
return { rev: Math.round(rev), payment: Math.round(payment), cost: Math.round(cost), paid: Math.round(paid) };
|
|
|
|
|
};
|
|
|
|
|
const allPfs = pfs.filter(x => x.status === state.finFilter);
|
|
|
|
|
let sumRev=0, sumPay=0, sumCost=0, sumPaid=0;
|
|
|
|
|
allPfs.forEach(pf => { const t = calcTotals(pf); sumRev+=t.rev; sumPay+=t.payment; sumCost+=t.cost; sumPaid+=t.paid; });
|
|
|
|
|
const fmt = (v) => v ? `<span class="font-medium">${money(v)}</span>` : '<span class="text-slate-300">—</span>';
|
|
|
|
|
const fmtDiff = (v) => { if (!v) return '<span class="text-slate-300">—</span>'; return `<span class="${v > 0 ? 'text-amber-600 font-medium' : 'text-green-600 font-medium'}">${money(Math.abs(v))}</span>`; };
|
|
|
|
|
return card(`<div class="flex items-center justify-between mb-3"><h3 class="font-bold text-slate-700">总视图 <span class="text-slate-400 font-normal">(${allPfs.length})</span></h3></div><div class="flex gap-2 mb-3">${[["已签约","已签约"],["流程中","流程中"],["待签约","待签约"]].map(([k,v]) => `<button class="btn btn-sm ${state.finFilter === k ? 'btn-primary' : 'btn-ghost'}" onclick="state.finFilter='${k}';renderFinance()">${v} (${pfs.filter(x=>x.status===k).length})</button>`).join("")}</div><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2 text-center font-semibold align-middle">项目名称</th><th class="p-2 text-center font-semibold align-middle">类型</th><th class="p-2 text-center font-semibold align-middle">状态</th><th class="p-2 text-center font-semibold align-middle text-blue-600">已确收</th><th class="p-2 text-center font-semibold align-middle text-amber-600">已回款</th><th class="p-2 text-center font-semibold align-middle">回款差额</th><th class="p-2 text-center font-semibold align-middle text-rose-600">应付</th><th class="p-2 text-center font-semibold align-middle text-purple-600">已付</th><th class="p-2 text-center font-semibold align-middle">应付差额</th><th class="p-2 text-center font-semibold align-middle text-slate-700">现金流</th></tr></thead><tbody>${allPfs.map(pf => { const t = calcTotals(pf); const payDiff = t.rev - t.payment; const costDiff = t.cost - t.paid; const cashflow = t.payment - t.paid; 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">${esc(pf.customer_name)}</td><td class="p-2 text-sm text-center text-slate-500">${esc(pf.business_type)}</td><td class="p-2 text-sm text-center">${pf.status === '已签约' ? badge('已签约') : pf.status === '流程中' ? badge('流程中','blue') : badge('待签约','amber')}</td><td class="p-2 text-center text-blue-700 align-middle">${fmt(t.rev)}</td><td class="p-2 text-center text-amber-700 align-middle">${fmt(t.payment)}</td><td class="p-2 text-center align-middle">${fmtDiff(payDiff)}</td><td class="p-2 text-center text-rose-700 align-middle">${fmt(t.cost)}</td><td class="p-2 text-center text-purple-700 align-middle">${fmt(t.paid)}</td><td class="p-2 text-center align-middle">${fmtDiff(costDiff)}</td><td class="p-2 text-center font-semibold align-middle ${cashflow >= 0 ? 'text-green-600' : 'text-red-600'}">${cashflow ? money(cashflow) : '<span class="text-slate-300">—</span>'}</td></tr>`; }).join("")}<tr class="border-t-2 border-slate-200 bg-slate-50 font-bold"><td class="p-2 text-center" colspan="3">合计</td><td class="p-2 text-center text-blue-700 align-middle">${money(sumRev)}</td><td class="p-2 text-center text-amber-700 align-middle">${money(sumPay)}</td><td class="p-2 text-center align-middle">${fmtDiff(sumRev - sumPay)}</td><td class="p-2 text-center text-rose-700 align-middle">${money(sumCost)}</td><td class="p-2 text-center text-purple-700 align-middle">${money(sumPaid)}</td><td class="p-2 text-center align-middle">${fmtDiff(sumCost - sumPaid)}</td><td class="p-2 text-center font-semibold align-middle ${sumPay - sumPaid >= 0 ? 'text-green-600' : 'text-red-600'}">${money(sumPay - sumPaid)}</td></tr></tbody></table></div>`, "p-4");
|
|
|
|
|
})()}
|
|
|
|
|
</div>`;
|
|
|
|
|
if (window.lucide) window.lucide.createIcons();
|
|
|
|
|
}
|
|
|
|
|
@@ -187,7 +235,7 @@ window.openFinanceModal = () => {
|
|
|
|
|
modal.classList.remove("hidden");
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.addBudgetRow = (month = '', rev = '', gross = '', payment = '', cost = '') => {
|
|
|
|
|
window.addBudgetRow = (month = '', rev = '', gross = '', payment = '', cost = '', paid = '') => {
|
|
|
|
|
const tbody = document.querySelector("#budgetTbody");
|
|
|
|
|
if (!tbody) return;
|
|
|
|
|
const row = document.createElement("tr");
|
|
|
|
|
@@ -196,6 +244,7 @@ window.addBudgetRow = (month = '', rev = '', gross = '', payment = '', cost = ''
|
|
|
|
|
<td><input name="budget_gross[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value="${gross}" oninput="updateBudgetSummary()"></td>
|
|
|
|
|
<td><input name="budget_payment[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value="${payment}" oninput="updateBudgetSummary()"></td>
|
|
|
|
|
<td><input name="budget_cost[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value="${cost}" oninput="updateBudgetSummary()"></td>
|
|
|
|
|
<td><input name="budget_paid[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value="${paid}" oninput="updateBudgetSummary()"></td>
|
|
|
|
|
<td><button type="button" class="btn btn-ghost btn-sm text-red-500 p-0 w-6 h-6" onclick="this.closest('tr').remove();updateBudgetSummary()"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button></td>`;
|
|
|
|
|
tbody.appendChild(row);
|
|
|
|
|
if (window.lucide) window.lucide.createIcons();
|
|
|
|
|
@@ -206,20 +255,24 @@ window.updateBudgetSummary = () => {
|
|
|
|
|
const grossEl = document.querySelector("#budgetTotalGross");
|
|
|
|
|
const paymentEl = document.querySelector("#budgetTotalPayment");
|
|
|
|
|
const costEl = document.querySelector("#budgetTotalCost");
|
|
|
|
|
const paidEl = document.querySelector("#budgetTotalPaid");
|
|
|
|
|
if (!revEl || !grossEl) return;
|
|
|
|
|
const revInputs = document.querySelectorAll('[name="budget_rev[]"]');
|
|
|
|
|
const grossInputs = document.querySelectorAll('[name="budget_gross[]"]');
|
|
|
|
|
const paymentInputs = document.querySelectorAll('[name="budget_payment[]"]');
|
|
|
|
|
const costInputs = document.querySelectorAll('[name="budget_cost[]"]');
|
|
|
|
|
let totalRev = 0, totalGross = 0, totalPayment = 0, totalCost = 0;
|
|
|
|
|
const paidInputs = document.querySelectorAll('[name="budget_paid[]"]');
|
|
|
|
|
let totalRev = 0, totalGross = 0, totalPayment = 0, totalCost = 0, totalPaid = 0;
|
|
|
|
|
revInputs.forEach(el => { totalRev += parseFloat(el.value) || 0; });
|
|
|
|
|
grossInputs.forEach(el => { totalGross += parseFloat(el.value) || 0; });
|
|
|
|
|
paymentInputs.forEach(el => { totalPayment += parseFloat(el.value) || 0; });
|
|
|
|
|
costInputs.forEach(el => { totalCost += parseFloat(el.value) || 0; });
|
|
|
|
|
paidInputs.forEach(el => { totalPaid += parseFloat(el.value) || 0; });
|
|
|
|
|
revEl.textContent = money(totalRev);
|
|
|
|
|
grossEl.textContent = money(totalGross);
|
|
|
|
|
if (paymentEl) paymentEl.textContent = money(totalPayment);
|
|
|
|
|
if (costEl) costEl.textContent = money(totalCost);
|
|
|
|
|
if (paidEl) paidEl.textContent = money(totalPaid);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.initBudgetTable = (budgetData) => {
|
|
|
|
|
@@ -227,7 +280,7 @@ window.initBudgetTable = (budgetData) => {
|
|
|
|
|
if (!tbody) return;
|
|
|
|
|
tbody.innerHTML = "";
|
|
|
|
|
const rows = budgetData || [];
|
|
|
|
|
rows.forEach(r => addBudgetRow(r.month || '', r.rev || '', r.gross || '', r.payment || '', r.cost || ''));
|
|
|
|
|
rows.forEach(r => addBudgetRow(r.month || '', r.rev || '', r.gross || '', r.payment || '', r.cost || '', r.paid || ''));
|
|
|
|
|
setTimeout(() => updateBudgetSummary(), 50);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
@@ -315,8 +368,9 @@ window.createFinance = async (event) => {
|
|
|
|
|
const grosses = form.querySelectorAll('[name="budget_gross[]"]');
|
|
|
|
|
const payments = form.querySelectorAll('[name="budget_payment[]"]');
|
|
|
|
|
const costs = form.querySelectorAll('[name="budget_cost[]"]');
|
|
|
|
|
const paids = form.querySelectorAll('[name="budget_paid[]"]');
|
|
|
|
|
const budgetRows = [];
|
|
|
|
|
let totalRev = 0, totalGross = 0;
|
|
|
|
|
let totalRev = 0, totalGross = 0, totalPaidFromBudget = 0;
|
|
|
|
|
for (let i = 0; i < months.length; i++) {
|
|
|
|
|
const m = months[i].value.trim();
|
|
|
|
|
if (!m) continue;
|
|
|
|
|
@@ -324,13 +378,20 @@ window.createFinance = async (event) => {
|
|
|
|
|
const gross = parseFloat(grosses[i].value) || 0;
|
|
|
|
|
const payment = parseFloat(payments[i].value) || 0;
|
|
|
|
|
const cost = parseFloat(costs[i].value) || 0;
|
|
|
|
|
budgetRows.push({ month: m, rev, gross, payment, cost });
|
|
|
|
|
const paid = parseFloat(paids[i].value) || 0;
|
|
|
|
|
budgetRows.push({ month: m, rev, gross, payment, cost, paid });
|
|
|
|
|
totalRev += rev;
|
|
|
|
|
totalGross += gross;
|
|
|
|
|
totalPaidFromBudget += paid;
|
|
|
|
|
}
|
|
|
|
|
data.budget_data = JSON.stringify(budgetRows);
|
|
|
|
|
data.total_rev = totalRev;
|
|
|
|
|
data.total_gross = totalGross;
|
|
|
|
|
data.total_paid = totalPaidFromBudget;
|
|
|
|
|
let totalPayment = 0, totalCost = 0;
|
|
|
|
|
for (const r of budgetRows) { totalPayment += r.payment; totalCost += r.cost; }
|
|
|
|
|
data.total_payment = totalPayment;
|
|
|
|
|
data.total_cost = totalCost;
|
|
|
|
|
const pfId = data.pf_id;
|
|
|
|
|
delete data.pf_id;
|
|
|
|
|
try {
|
|
|
|
|
|