|
|
|
|
@@ -22,8 +22,8 @@ function renderPlan() {
|
|
|
|
|
const months = displayMonths.map(d => d.key);
|
|
|
|
|
const monthLabels = displayMonths.map(d => d.label);
|
|
|
|
|
|
|
|
|
|
const signed = pfs.filter(x => x.status === "已签约");
|
|
|
|
|
const pending = pfs.filter(x => x.status === "待签约");
|
|
|
|
|
const signed = pfs;
|
|
|
|
|
const pending = pfs;
|
|
|
|
|
const sumSign = Math.round(signed.reduce((s,x) => s + (x.sign_amount||0), 0));
|
|
|
|
|
const sumPending = Math.round(pending.reduce((s,x) => s + (x.sign_amount||0), 0));
|
|
|
|
|
|
|
|
|
|
@@ -114,9 +114,13 @@ function renderPlan() {
|
|
|
|
|
const finHeaderBase = `<span class="text-sm text-slate-500">视图:</span><select onchange="setPlanView(this.value)" class="text-sm font-medium py-1 px-2.5 cursor-pointer" style="appearance:none;-webkit-appearance:none;background-color:transparent;border:0;outline:none;background:url('data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 width=%2212%22 height=%2212%22 viewBox=%220 0 24 24%22 fill=%22none%22 stroke=%22%236b7280%22 stroke-width=%222%22 stroke-linecap=%22round%22 stroke-linejoin=%22round%22%3E%3Cpolyline points=%226 9 12 15 18 9%22%3E%3C/polyline%3E%3C/svg%3E') no-repeat right 4px center;padding-right:22px;min-height:30px"><option value="overview" ${state.planView==='overview'?'selected':''}>总视图</option><option value="quarterly" ${state.planView==='quarterly'?'selected':''}>季度视图</option><option value="monthly" ${state.planView==='monthly'?'selected':''}>月度视图</option></select>${toolDateSelect}`;
|
|
|
|
|
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('待签约')" class="finance-tab${state.planFilter==='待签约'?' active':''}" style="font-size:14px;font-weight:600">项目计划 (${pfs.filter(x=>x.status==='待签约').length})</button><button onclick="switchPlanFilter('expense')" class="finance-tab${state.planFilter==='expense'?' active':''}" style="font-size:14px;font-weight:600">平台费用计划</button></div>`;
|
|
|
|
|
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('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">
|
|
|
|
|
<div class="flex items-center gap-3 px-4 py-3 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>
|
|
|
|
|
${planFilterTabs}
|
|
|
|
|
${state.planFilter !== 'expense' && state.planFilter !== 'overview' ? card(`<div class="flex justify-between items-center"><div class="flex items-center gap-2">${finHeaderBase}</div>${finAddBtn}</div>`, "p-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>
|
|
|
|
|
@@ -140,7 +144,7 @@ function renderPlan() {
|
|
|
|
|
<div class="grid grid-cols-3 gap-3">
|
|
|
|
|
<label class="block"><span class="fin-label">签约金额(元) <span class="text-red-500">*</span></span><input name="sign_amount" class="form-ctrl" placeholder="必须大于 0"></label>
|
|
|
|
|
<label class="block"><span class="fin-label">签约月份 <span class="text-red-500">*</span></span><select name="sign_month" required class="form-ctrl bg-white"><option value="">选择</option>${monthOptions('')}</select></label>
|
|
|
|
|
<label class="block"><span class="fin-label">项目状态</span><select name="status" class="form-ctrl bg-white"><option>已签约</option></select></label>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
<div class="grid grid-cols-3 gap-3">
|
|
|
|
|
<label class="block"><span class="fin-label">商务负责人 <span class="text-red-500">*</span></span><input name="sales_person" required class="form-ctrl" placeholder="请输入商务负责人"></label>
|
|
|
|
|
@@ -285,7 +289,7 @@ function renderPlan() {
|
|
|
|
|
};
|
|
|
|
|
const SIGNED_COLS = [
|
|
|
|
|
{ key: 'customer_name', label: '项目名称' },
|
|
|
|
|
{ key: 'status', label: '状态' },
|
|
|
|
|
,
|
|
|
|
|
{ key: 'sign_amount', label: '签约金额' },
|
|
|
|
|
{ key: 'rev', label: '已确收' },
|
|
|
|
|
{ key: 'gross', label: '毛利' },
|
|
|
|
|
@@ -301,7 +305,7 @@ function renderPlan() {
|
|
|
|
|
];
|
|
|
|
|
const UNSIGNED_COLS = [
|
|
|
|
|
{ key: 'customer_name', label: '项目名称' },
|
|
|
|
|
{ key: 'status', label: '状态' },
|
|
|
|
|
,
|
|
|
|
|
{ key: 'sign_amount', label: '签约金额' },
|
|
|
|
|
{ key: 'sign_month', label: '签约月份' },
|
|
|
|
|
{ key: 'gross', label: '毛利' },
|
|
|
|
|
@@ -316,7 +320,7 @@ function renderPlan() {
|
|
|
|
|
return typeof v === 'object' ? [c.label, v.val, v.cls] : [c.label, v, c.color];
|
|
|
|
|
});
|
|
|
|
|
const thead = isUnsigned ? buildThead(UNSIGNED_COLS, 'uns') : buildThead(SIGNED_COLS, 'sig');
|
|
|
|
|
const theadCols = isUnsigned ? 7 : 14;
|
|
|
|
|
const theadCols = isUnsigned ? 6 : 13;
|
|
|
|
|
const tbody = rows.length ? `<tbody>${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';
|
|
|
|
|
@@ -336,25 +340,23 @@ function renderPlan() {
|
|
|
|
|
const payR = payRVal !== null ? '<span class="' + payRCls + ' font-semibold">' + payRVal + '%</span>' : '<span class="text-slate-300">—</span>';
|
|
|
|
|
const paidR = cost && paid ? Math.round(paid / cost * 100) + '%' : '<span class="text-slate-300">—</span>';
|
|
|
|
|
const grossR = rev && gross ? Math.round(gross / rev * 100) + '%' : '<span class="text-slate-300">—</span>';
|
|
|
|
|
const st = '<span class="text-amber-600">待签约</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 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-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">${st}</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>`;
|
|
|
|
|
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="planOpenPfEditModal(${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 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 st = '<span class="text-amber-600">待签约</span>';
|
|
|
|
|
const profit = 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-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">${st}</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="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.planView === 'monthly') {
|
|
|
|
|
const allPfs = pfs.filter(x => x.status === state.planFilter);
|
|
|
|
|
const allPfs = pfs;
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const defaultMonth = now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0");
|
|
|
|
|
if (!state.planMonth) state.planMonth = defaultMonth;
|
|
|
|
|
@@ -374,8 +376,7 @@ function renderPlan() {
|
|
|
|
|
dataItems.push({
|
|
|
|
|
pf: pf,
|
|
|
|
|
customer_name: pf.customer_name || '',
|
|
|
|
|
status: pf.status || '',
|
|
|
|
|
sign_amount: pf.sign_amount || 0,
|
|
|
|
|
sign_amount: pf.sign_amount || 0,
|
|
|
|
|
sign_month: pf.sign_month || '',
|
|
|
|
|
rev: rev, gross: gross, cost: cost,
|
|
|
|
|
profit: gross, payment: payment, paid: paid,
|
|
|
|
|
@@ -395,12 +396,12 @@ function renderPlan() {
|
|
|
|
|
rows.push(isUnsigned ? tdRowUnsigned(d.pf, d.cost, d.gross) : tdRow(d.pf, d.rev, d.payment, d.cost, d.paid, d.gross));
|
|
|
|
|
});
|
|
|
|
|
const signTotal = Math.round(allPfs.filter(x => (x.sign_month || "").substring(0, 7) === selMonth).reduce((a, p) => a + (p.sign_amount || 0), 0));
|
|
|
|
|
const signCnt = allPfs.filter(x => x.status === "已签约" && (x.sign_month || "").substring(0, 7) === selMonth).length;
|
|
|
|
|
const signCnt = allPfs.filter(x => (x.sign_month || "").substring(0, 7) === selMonth).length;
|
|
|
|
|
return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, '该月份暂无数据');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (state.planView === 'quarterly') {
|
|
|
|
|
const allPfs = pfs.filter(x => x.status === state.planFilter);
|
|
|
|
|
const allPfs = pfs;
|
|
|
|
|
const qRanges = [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]];
|
|
|
|
|
const now = new Date();
|
|
|
|
|
if (state.planQuarter === undefined) {
|
|
|
|
|
@@ -430,8 +431,7 @@ function renderPlan() {
|
|
|
|
|
dataItems.push({
|
|
|
|
|
pf: pf,
|
|
|
|
|
customer_name: pf.customer_name || '',
|
|
|
|
|
status: pf.status || '',
|
|
|
|
|
sign_amount: pf.sign_amount || 0,
|
|
|
|
|
sign_amount: pf.sign_amount || 0,
|
|
|
|
|
sign_month: pf.sign_month || '',
|
|
|
|
|
rev: rev, gross: gross, cost: cost,
|
|
|
|
|
profit: gross, payment: payment, paid: paid,
|
|
|
|
|
@@ -451,7 +451,7 @@ function renderPlan() {
|
|
|
|
|
rows.push(isUnsigned ? tdRowUnsigned(d.pf, d.cost, d.gross) : tdRow(d.pf, d.rev, d.payment, d.cost, d.paid, d.gross));
|
|
|
|
|
});
|
|
|
|
|
const signTotal = Math.round(allPfs.filter(x => { const m = parseInt((x.sign_month || "0").substring(5)) || 0; return qRange.includes(m); }).reduce((a, p) => a + (p.sign_amount || 0), 0));
|
|
|
|
|
const signCnt = allPfs.filter(x => x.status === "已签约" && qRange.includes(parseInt((x.sign_month || "0").substring(5)) || 0)).length;
|
|
|
|
|
const signCnt = allPfs.filter(x => qRange.includes(parseInt((x.sign_month || "0").substring(5)) || 0)).length;
|
|
|
|
|
return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, '该季度暂无数据');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -465,15 +465,14 @@ function renderPlan() {
|
|
|
|
|
expense.forEach(e => { cost += parseFloat(e.cost || 0) || 0; paid += parseFloat(e.paid || 0) || 0; });
|
|
|
|
|
return { rev: Math.round(rev), payment: Math.round(payment), cost: Math.round(cost), paid: Math.round(paid), gross: Math.round(gross) };
|
|
|
|
|
};
|
|
|
|
|
const allPfs = pfs.filter(x => x.status === state.planFilter);
|
|
|
|
|
const allPfs = pfs;
|
|
|
|
|
var dataItems = allPfs.map(pf => {
|
|
|
|
|
var t = calcTotals(pf);
|
|
|
|
|
var cashflow = t.payment - t.paid;
|
|
|
|
|
return {
|
|
|
|
|
pf: pf,
|
|
|
|
|
customer_name: pf.customer_name || '',
|
|
|
|
|
status: pf.status || '',
|
|
|
|
|
sign_amount: pf.sign_amount || 0,
|
|
|
|
|
sign_amount: pf.sign_amount || 0,
|
|
|
|
|
rev: t.rev, gross: t.gross, cost: t.cost,
|
|
|
|
|
profit: t.gross, payment: t.payment, paid: t.paid,
|
|
|
|
|
cashflow: cashflow,
|
|
|
|
|
@@ -493,7 +492,7 @@ function renderPlan() {
|
|
|
|
|
rows.push(isUnsigned ? tdRowUnsigned(d.pf, d.cost, d.gross) : tdRow(d.pf, d.rev, d.payment, d.cost, d.paid, d.gross));
|
|
|
|
|
});
|
|
|
|
|
const signTotal = Math.round(allPfs.reduce((a, p) => a + (p.sign_amount || 0), 0));
|
|
|
|
|
const signCnt = allPfs.filter(x => x.status === "已签约").length;
|
|
|
|
|
const signCnt = allPfs.length;
|
|
|
|
|
return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, '暂无数据');
|
|
|
|
|
})()}
|
|
|
|
|
</div>`;
|
|
|
|
|
@@ -689,7 +688,6 @@ window.planRevertTaskType = (btn) => {
|
|
|
|
|
window.closePlanModal = () => {
|
|
|
|
|
const modal = document.querySelector("#planModal");
|
|
|
|
|
if (modal) modal.classList.add("hidden");
|
|
|
|
|
render();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.planEditSignMonth = (event, pfId) => {
|
|
|
|
|
@@ -814,7 +812,6 @@ window.planOpenPfEditModal = (pfId) => {
|
|
|
|
|
signMonthEl.innerHTML = monthOptions(signMonthValue);
|
|
|
|
|
signMonthEl.value = signMonthValue;
|
|
|
|
|
}
|
|
|
|
|
form.querySelector('[name="status"]').value = pf.status || "待签约";
|
|
|
|
|
form.querySelector('[name="sales_person"]').value = pf.sales_person || "";
|
|
|
|
|
form.querySelector('[name="owner"]').value = pf.owner || "";
|
|
|
|
|
setVal("start_date", pf.start_date);
|
|
|
|
|
|