// plan.js — 计划管理模块 console.log('plan.js loaded'); function renderPlan() { const pfs = state.data.planFinances || []; const ops = state.data.operations || []; const fmTypesByTenant = { "科普·无界": ["科普音频","科普视频","科普文章","科普专访","患教会","全品类科普","调研问卷"], "科研·无界": ["真实世界研究","调研问卷","病例征集","患者招募"], "医患·无界": ["医患运营","患者管理","患教会","创新支付","电商","其他"], }; const fmTypes = fmTypesByTenant[state.tenant] || fmTypesByTenant["科普·无界"]; const tenantOps = (state.data.operations || []).filter(o => (o.project_name || "").includes(state.tenant.replace("·无界","")) || o.tenant === state.tenant); const now = new Date(); const thisMonth = now.getMonth() + 1; const displayMonths = []; for (let i = 0; i < 4; i++) { const m = thisMonth + i; const mm = m > 12 ? m - 12 : m; displayMonths.push({ key: "2026_" + String(mm).padStart(2, "0"), label: mm + "月" }); } const months = displayMonths.map(d => d.key); const monthLabels = displayMonths.map(d => d.label); 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)); const monthRev = months.map(m => { return signed.reduce((s, pf) => { let budget = []; try { budget = JSON.parse(pf.budget_data || "[]"); } catch (e) {} const row = budget.find(b => (b.month || "").replace("-", "_") === m); return s + (row ? (parseFloat(row.rev) || 0) : 0); }, 0); }); const monthGross = months.map(m => { return signed.reduce((s, pf) => { let budget = []; try { budget = JSON.parse(pf.budget_data || "[]"); } catch (e) {} const row = budget.find(b => (b.month || "").replace("-", "_") === m); return s + (row ? (parseFloat(row.gross) || 0) : 0); }, 0); }); const thisMonthKey = displayMonths[0].key; const thisMonthRev = monthRev[0]; const thisMonthGross = monthGross[0]; let monthPayment = 0, monthCost = 0; for (const pf of pfs) { let budget = []; try { budget = JSON.parse(pf.budget_data || "[]"); } catch (e) {} for (const b of budget) { const bKey = (b.month || "").replace("-", "_"); if (bKey === thisMonthKey) { monthPayment += parseFloat(b.payment || 0); monthCost += parseFloat(b.cost || 0); break; } } } monthPayment = Math.round(monthPayment); monthCost = Math.round(monthCost); const monthCashflow = monthPayment - monthCost; const renderPfRow = (pf) => { let budgetMap = {}; try { const budget = JSON.parse(pf.budget_data || "[]"); budget.forEach(b => { budgetMap[(b.month || "").replace("-", "_")] = b; }); } catch (e) {} const isRevView = state.planView !== "cashflow" && state.planView !== "overview" && state.planView !== "monthly" && state.planView !== "quarterly"; const mCols = months.map(m => { const b = budgetMap[m] || {}; if (isRevView) { const rev = b.rev || 0; const gross = b.gross || 0; return `${rev ? money(rev) : '—'}
${gross ? money(gross) : '—'}`; } else { const payment = b.payment || 0; const cost = b.cost || 0; return `${payment ? money(payment) : '—'}
${cost ? money(cost) : '—'}`; } }).join(""); const totalCol = (() => { if (isRevView) { const totalRev = pf.total_rev || 0; const totalGross = pf.total_gross || 0; return `${totalRev ? money(totalRev) : '—'}
${totalGross ? money(totalGross) : '—'}`; } 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 `${totalPayment ? money(totalPayment) : '—'}
${totalCost ? money(totalCost) : '—'}`; } })(); const sm = pf.sign_month || ""; const signMonthCell = `${sm || '—'}`; return `${esc(pf.client_name || "")}${esc(pf.customer_name)}${esc(pf.weight || "20%")}${esc(pf.project_type || "待签约")}${signMonthCell}${money(pf.sign_amount)}${mCols}${totalCol}`; }; const now2 = new Date(); const defaultMonth2 = now2.getFullYear() + "-" + String(now2.getMonth()+1).padStart(2,"0"); if (!state.planMonth) state.planMonth = defaultMonth2; if (state.planQuarter === undefined) state.planQuarter = Math.floor(now2.getMonth() / 3); const qLabels = ['Q1','Q2','Q3','Q4']; const qMonths = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]]; const nowD = new Date(); const activeQ = state.planQuarter !== undefined ? state.planQuarter : Math.floor(nowD.getMonth() / 3); const activeMonths = qMonths[activeQ]; const finHeaderBase = `
|按季度:${qLabels.map((q,i) => ``).join('')}|按月度:${activeMonths.map(m => { const ms = `2026-${String(m).padStart(2,'0')}`; return ``; }).join('')}
`; const finAddBtn = ``; const planFilterTabs = `
`; document.querySelector("#plan").innerHTML = `
${planFilterTabs}
计划提醒:计划也是预算,只有真正能够列出计划的预算,才有靠谱的落地可能性
${state.planFilter !== 'expense' && state.planFilter !== 'overview' && state.planFilter !== 'quarterlyOverview' ? card(`
${finHeaderBase}
${finAddBtn}
`, "p-4") : ''} ${(() => { if (state.planFilter === 'overview') { setTimeout(() => renderPlanOverview(), 10); return `
`; } if (state.planFilter === 'quarterlyOverview') { setTimeout(() => renderPlanQuarterlyOverview(), 10); return `
`; } if (state.planFilter === 'expense') { setTimeout(() => renderPlanExpense(), 10); return `
`; } const isUnsigned = false; 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: '项目利润', value: ctx => { const v = ctx.sumGross; return { val: moneyWan(v), cls: v >= 0 ? 'text-green-600' : 'text-red-600' }; }, color: null }, { 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 => ctx.sumRev && ctx.signTotal ? Math.round(ctx.sumRev / ctx.signTotal * 100) + '%' : '—', color: 'text-slate-500' }, { 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' }, ].filter(c => true); // 列排序 if (!state.planSort) state.planSort = { key: null, asc: true }; const sortData = (data, key) => { if (!key || !state.planSort.key) return data; var asc = state.planSort.asc ? 1 : -1; return data.slice().sort(function(a, b) { var va = a[key], vb = b[key]; if (typeof va === 'string') va = va || '', vb = vb || ''; else { va = parseFloat(va) || 0; vb = parseFloat(vb) || 0; } if (va < vb) return -1 * asc; if (va > vb) return 1 * asc; return 0; }); }; const buildThead = (cols, sortKey) => { var h = ''; cols.forEach(function(c) { var arrow = sortKey ? (sortKey === c.key ? (state.planSort.asc ? ' ↑' : ' ↓') : '') : ''; var clickable = sortKey ? ' cursor-pointer select-none' : ''; h += '' + c.label + arrow + ''; }); h += ''; return h; }; const SIGNED_COLS = [ { key: 'client_name', label: '客户' }, { key: 'customer_name', label: '项目名称' }, { key: 'weight', label: '加权' }, { key: 'project_type', label: '项目类型' }, { key: 'sign_amount', label: '签约金额' }, { key: 'rev', label: '已确收' }, { key: 'gross', label: '毛利' }, { key: 'cost', label: '成本' }, { key: 'profit', label: '项目利润' }, { key: 'payment', label: '已回款' }, { key: 'paid', label: '已付' }, { key: 'cashflow', label: '现金流' }, { key: 'exe_rate', label: '执行率' }, { key: 'pay_rate', label: '回款率' }, ]; const UNSIGNED_COLS = [ { key: 'client_name', label: '客户' }, { key: 'customer_name', label: '项目名称' }, { key: 'weight', label: '加权' }, { key: 'project_type', label: '项目类型' }, { key: 'sign_amount', label: '签约金额' }, { key: 'sign_month', label: '签约月份' }, { key: 'gross', label: '毛利' }, { key: 'cost', label: '成本' }, { key: 'profit', label: '项目利润' }, ]; const renderView = (rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, emptyText) => { const ctx = { sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt }; const cards = METRIC_CARDS.map(c => { const v = c.value(ctx); 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 ? 9 : 14; const tbody = rows.length ? `${rows.join("")}` : `${emptyText}`; const cardGrid = isUnsigned ? 'grid-cols-4' : 'grid-cols-6'; const subtitle = isUnsigned ? '合同 → 毛利 → 成本 → 项目利润' : '合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流'; return card(`
${cards.map(([l, v, c]) => '
' + l + '' + v + '
').join("")}
`, "p-4") + card(`

项目财务明细

${subtitle}

${thead}${tbody}
`, "p-4"); }; const fmtNoUnit = (v) => v ? `${Number(v||0).toLocaleString('zh-CN')}` : ''; const tdRow = (pf, rev, payment, cost, paid, gross) => { const cashflow = payment - paid; const exe = rev && pf.sign_amount ? Math.round(rev / pf.sign_amount * 100) + '%' : ''; const payRVal = rev && payment ? Math.round(payment / rev * 100) : null; const payRCls = payRVal === null ? '' : payRVal < 30 ? 'text-red-600' : payRVal < 80 ? 'text-amber-600' : 'text-green-600'; const payR = payRVal !== null ? '' + payRVal + '%' : ''; const paidR = cost && paid ? Math.round(paid / cost * 100) + '%' : ''; const grossR = rev && gross ? Math.round(gross / rev * 100) + '%' : ''; const cf = cashflow ? money(cashflow) : ''; 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) : ''; return `${esc(pf.client_name || "")}${esc(pf.customer_name)}${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}${money(pf.sign_amount)}${fmtNoUnit(rev)}${fmtNoUnit(gross)}${fmtNoUnit(cost)}${pfVal}${fmtNoUnit(payment)}${fmtNoUnit(paid)}${cf}${exe}${payR}`; }; // 待签约简版行 const tdRowUnsigned = (pf, cost, gross) => { const profit = gross; const pfCls = profit >= 0 ? 'text-green-600' : 'text-red-600'; const pfVal = profit ? money(profit) : ''; return `${esc(pf.client_name || "")}${esc(pf.customer_name)}${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}${money(pf.sign_amount)}${pf.sign_month || '—'}${fmtNoUnit(gross)}${fmtNoUnit(cost)}${pfVal}`; }; if (state.planView === 'monthly') { const allPfs = pfs; const now = new Date(); const defaultMonth = now.getFullYear() + "-" + String(now.getMonth() + 1).padStart(2, "0"); if (!state.planMonth) state.planMonth = defaultMonth; const selMonth = state.planMonth; var dataItems = []; allPfs.forEach(pf => { let bd = []; try { bd = JSON.parse(pf.budget_data || "[]"); } catch (e) {} let ed = []; try { ed = JSON.parse(pf.expense_data || "[]"); } catch (e) {} const b = bd.find(x => (x.month || "") === selMonth) || {}; const e = ed.find(x => (x.month || "") === selMonth) || {}; const rev = Math.round(parseFloat(b.rev || 0) || 0); const payment = Math.round(parseFloat(b.payment || 0) || 0); const gross = Math.round(parseFloat(b.gross || 0) || 0); const cost = Math.round(parseFloat(e.cost || 0) || 0); const paid = Math.round(parseFloat(e.paid || 0) || 0); if (!rev && !payment && !cost && !paid && !gross) return; dataItems.push({ pf: pf, customer_name: pf.customer_name || '', sign_amount: pf.sign_amount || 0, sign_month: pf.sign_month || '', rev: rev, gross: gross, cost: cost, profit: gross, payment: payment, paid: paid, cashflow: payment - paid, exe_rate: rev && pf.sign_amount ? Math.round(rev / pf.sign_amount * 100) : 0, gross_rate: rev && gross ? Math.round(gross / rev * 100) : 0, pay_rate: rev && payment ? Math.round(payment / rev * 100) : 0, paid_rate: cost && paid ? Math.round(paid / cost * 100) : 0, }); }); var sortK = state.planSort && state.planSort.key ? state.planSort.key.split('|')[1] : null; dataItems = sortData(dataItems, sortK); const rows = []; let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0; dataItems.forEach(d => { sumRev += d.rev; sumPay += d.payment; sumCost += d.cost; sumPaid += d.paid; sumGross += d.gross; 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.sign_month || "").substring(0, 7) === selMonth).length; return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, '该月份暂无数据'); } if (state.planView === 'quarterly') { 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) { const saved = localStorage.getItem("opc-fin-quarter"); state.planQuarter = saved !== null ? parseInt(saved) : Math.floor(now.getMonth() / 3); } const selQ = state.planQuarter; const qRange = qRanges[selQ]; const sumBudget = (pf, field) => { let total = 0; try { JSON.parse(pf.budget_data || "[]").forEach(b => { const m = parseInt((b.month || "").substring(5)) || 0; if (qRange.includes(m)) total += parseFloat(b[field] || 0); }); } catch (e) {} return total; }; const sumExpense = (pf, field) => { let total = 0; try { JSON.parse(pf.expense_data || "[]").forEach(e => { const m = parseInt((e.month || "").substring(5)) || 0; if (qRange.includes(m)) total += parseFloat(e[field] || 0); }); } catch (e) {} return total; }; let dataItems = []; allPfs.forEach(pf => { const rev = sumBudget(pf, "rev"); const payment = sumBudget(pf, "payment"); const gross = sumBudget(pf, "gross"); const cost = sumExpense(pf, "cost"); const paid = sumExpense(pf, "paid"); if (!rev && !payment && !cost && !paid && !gross) return; dataItems.push({ pf: pf, customer_name: pf.customer_name || '', sign_amount: pf.sign_amount || 0, sign_month: pf.sign_month || '', rev: rev, gross: gross, cost: cost, profit: gross, payment: payment, paid: paid, cashflow: payment - paid, exe_rate: rev && pf.sign_amount ? Math.round(rev / pf.sign_amount * 100) : 0, gross_rate: rev && gross ? Math.round(gross / rev * 100) : 0, pay_rate: rev && payment ? Math.round(payment / rev * 100) : 0, paid_rate: cost && paid ? Math.round(paid / cost * 100) : 0, }); }); var sortK = state.planSort && state.planSort.key ? state.planSort.key.split('|')[1] : null; dataItems = sortData(dataItems, sortK); let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0; const rows = []; dataItems.forEach(d => { sumRev += d.rev; sumPay += d.payment; sumCost += d.cost; sumPaid += d.paid; sumGross += d.gross; 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 => qRange.includes(parseInt((x.sign_month || "0").substring(5)) || 0)).length; return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, '该季度暂无数据'); } // 总视图 const calcTotals = (pf) => { let budget = []; try { budget = JSON.parse(pf.budget_data || "[]"); } catch (e) {} let expense = []; try { expense = JSON.parse(pf.expense_data || "[]"); } catch (e) {} let rev = 0, payment = 0, gross = 0; budget.forEach(b => { rev += parseFloat(b.rev || 0) || 0; gross += parseFloat(b.gross || 0) || 0; payment += parseFloat(b.payment || 0) || 0; }); let cost = 0, paid = 0; 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; var dataItems = allPfs.map(pf => { var t = calcTotals(pf); var cashflow = t.payment - t.paid; return { pf: pf, customer_name: pf.customer_name || '', 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, exe_rate: t.rev && pf.sign_amount ? Math.round(t.rev / pf.sign_amount * 100) : 0, gross_rate: t.rev && t.gross ? Math.round(t.gross / t.rev * 100) : 0, pay_rate: t.rev && t.payment ? Math.round(t.payment / t.rev * 100) : 0, paid_rate: t.cost && t.paid ? Math.round(t.paid / t.cost * 100) : 0, }; }); var sortK = null; if (state.planSort && state.planSort.key && state.planSort.key.startsWith('sig|')) sortK = state.planSort.key.split('|')[1]; dataItems = sortData(dataItems, sortK); let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0, sumGross = 0; const rows = []; dataItems.forEach(d => { sumRev += d.rev; sumPay += d.payment; sumCost += d.cost; sumPaid += d.paid; sumGross += d.gross; 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.length; return renderView(rows, sumRev, sumPay, sumCost, sumPaid, sumGross, signTotal, signCnt, '暂无数据'); })()}
`; if (window.lucide) window.lucide.createIcons(); } window.openPlanModal = () => { const modal = document.querySelector("#planModal"); const form = modal.querySelector("form"); form.querySelector('[name="project_id"]').value = state.tenant; const dept = form.querySelector('input[disabled]'); if (dept) dept.value = state.tenant; const pfIdInput = form.querySelector('[name="pf_id"]'); if (!pfIdInput || !pfIdInput.value) { planInitRevpayTable(null); planInitCostTable(null); planInitTaskTable(null); document.querySelector("#planDeleteBtn").classList.add("hidden"); } modal.classList.remove("hidden"); }; window.planAddRevpayRow = (month = '', rev = '', gross = '', payment = '', rev_note = '') => { const tbody = document.querySelector("#plan_revpayTbody"); if (!tbody) return; const row = document.createElement("tr"); row.innerHTML = ` `; tbody.appendChild(row); if (window.lucide) window.lucide.createIcons(); }; window.planAddCostRow = (month = '', expense_type = '', cost = '', paid = '', exp_note = '') => { const tbody = document.querySelector("#plan_costTbody"); if (!tbody) return; const row = document.createElement("tr"); row.innerHTML = ` `; tbody.appendChild(row); if (window.lucide) window.lucide.createIcons(); }; window.planUpdateRevpaySummary = () => { const revEl = document.querySelector("#plan_revpayTotalRev"); const grossEl = document.querySelector("#plan_revpayTotalGross"); const paymentEl = document.querySelector("#plan_revpayTotalPayment"); if (!revEl || !paymentEl) return; const revInputs = document.querySelectorAll('[name="budget_rev[]"]'); const grossInputs = document.querySelectorAll('[name="budget_gross[]"]'); const paymentInputs = document.querySelectorAll('[name="budget_payment[]"]'); let totalRev = 0, totalGross = 0, totalPayment = 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; }); revEl.textContent = money(totalRev); if (grossEl) grossEl.textContent = money(totalGross); paymentEl.textContent = money(totalPayment); }; window.planUpdateCostSummary = () => { const costEl = document.querySelector("#plan_costTotalCost"); const paidEl = document.querySelector("#plan_costTotalPaid"); if (!costEl || !paidEl) return; const costInputs = document.querySelectorAll('[name="expense_cost[]"]'); const paidInputs = document.querySelectorAll('[name="expense_paid[]"]'); let totalCost = 0, totalPaid = 0; costInputs.forEach(el => { totalCost += parseFloat(el.value) || 0; }); paidInputs.forEach(el => { totalPaid += parseFloat(el.value) || 0; }); costEl.textContent = money(totalCost); paidEl.textContent = money(totalPaid); }; window.planInitRevpayTable = (budgetData) => { const tbody = document.querySelector("#plan_revpayTbody"); if (!tbody) return; tbody.innerHTML = ""; const rows = budgetData || []; rows.forEach(r => planAddRevpayRow(r.month || '', r.rev || '', r.gross || '', r.payment || '', r.rev_note || '')); setTimeout(() => planUpdateRevpaySummary(), 50); }; window.planInitCostTable = (expenseData) => { const tbody = document.querySelector("#plan_costTbody"); if (!tbody) return; tbody.innerHTML = ""; const rows = expenseData || []; rows.forEach(r => planAddCostRow(r.month || '', r.expense_type || '', r.cost || '', r.paid || '', r.exp_note || '')); setTimeout(() => planUpdateCostSummary(), 50); }; // ---------- 任务管理 tab ---------- function planTaskMonthOptions(selected) { const now = new Date(); const opts = []; for (let i = -2; i <= 12; i++) { const d = new Date(now.getFullYear(), now.getMonth() + i, 1); const v = d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0"); opts.push(``); } return opts.join(""); } window.planAddTaskRow = (taskMonth = '', taskType = '', taskCount = '', executedCount = '', unitPrice = '') => { const tbody = document.querySelector("#plan_taskTbody"); if (!tbody) return; const row = document.createElement("tr"); const defaultMonth = (() => { const n = new Date(); return n.getFullYear() + "-" + String(n.getMonth()+1).padStart(2,"0"); })(); const isPreset = TASK_TYPES.includes(taskType); const typeCell = isPreset || !taskType ? `` : ``; row.innerHTML = ` ${typeCell} `; tbody.appendChild(row); if (window.lucide) window.lucide.createIcons(); planUpdateRowCalc(row); }; window.planUpdateTaskDiff = (el) => { const row = el.closest('tr'); if (!row) return; planUpdateRowCalc(row); }; function planUpdateRowCalc(row) { const countInput = row.querySelector('[name="task_count[]"]'); const execInput = row.querySelector('[name="task_executed[]"]'); const priceInput = row.querySelector('[name="task_unit_price[]"]'); const diffInput = row.querySelector('[name="task_diff[]"]'); const execAmtInput = row.querySelector('[name="task_exec_amount[]"]'); const unexecAmtInput = row.querySelector('[name="task_unexec_amount[]"]'); const c = parseFloat(countInput.value) || 0; const e = parseFloat(execInput.value) || 0; const p = parseFloat(priceInput.value) || 0; const diff = c - e; // 差额 diffInput.value = (!c && !e) ? '' : diff; // 执行金额 = 单价 * 已执行 const execAmt = p * e; execAmtInput.value = execAmt ? execAmt.toFixed(2) : ''; // 未执行金额 = 单价 * 差额 const unexecAmt = p * diff; unexecAmtInput.value = unexecAmt ? unexecAmt.toFixed(2) : ''; } window.planToggleBtChip = (chip) => { const cb = chip.querySelector('input'); cb.checked = !cb.checked; chip.classList.toggle('bg-blue-50', cb.checked); chip.classList.toggle('border-blue-400', cb.checked); chip.classList.toggle('text-blue-600', cb.checked); }; window.planInitTaskTable = (taskData) => { const tbody = document.querySelector("#plan_taskTbody"); if (!tbody) return; tbody.innerHTML = ""; const rows = taskData || []; rows.forEach(r => planAddTaskRow(r.task_month || '', r.task_type || '', r.task_count || '', r.task_executed || '', r.unit_price || '')); }; window.planOnTaskTypeChange = (sel) => { if (sel.value !== '__custom__') return; const td = sel.parentElement; const oldVal = sel.value; td.innerHTML = ``; if (window.lucide) window.lucide.createIcons(); td.querySelector('input').focus(); }; window.planRevertTaskType = (btn) => { const td = btn.parentElement; td.innerHTML = ``; if (window.lucide) window.lucide.createIcons(); }; window.closePlanModal = () => { const modal = document.querySelector("#planModal"); if (modal) modal.classList.add("hidden"); }; window.planEditSignMonth = (event, pfId) => { event.stopPropagation(); const pf = (state.data.planFinances || []).find(x => x.id === pfId); if (!pf) return; const span = event.currentTarget; const td = span.parentElement; const currentValue = pf.sign_month || ""; const select = document.createElement("select"); select.innerHTML = monthOptions(currentValue); select.className = "form-ctrl form-ctrl-sm w-full"; select.value = currentValue; select.addEventListener("change", async () => { const newValue = select.value; try { await api(`/api/planFinances/${pfId}`, { method: "PUT", body: JSON.stringify({ data: { sign_month: newValue } }) }); pf.sign_month = newValue; td.innerHTML = `${newValue || '—'}`; } catch (e) { toast("修改失败:" + e.message, "error"); } }); select.addEventListener("blur", () => { td.innerHTML = `${currentValue || '—'}`; }); td.innerHTML = ""; td.appendChild(select); select.focus(); }; window.switchPlanTab = (tab) => { document.querySelectorAll(".plan-tab").forEach(b => b.classList.toggle("active", b.dataset.tab === tab)); document.querySelector("#plan_financeTabInfo").classList.toggle("hidden", tab !== "info"); document.querySelector("#plan_financeTabRevpay").classList.toggle("hidden", tab !== "revpay"); document.querySelector("#plan_financeTabCost").classList.toggle("hidden", tab !== "cost"); document.querySelector("#plan_financeTabExec").classList.toggle("hidden", tab !== "exec"); document.querySelector("#plan_financeTabTasks").classList.toggle("hidden", tab !== "tasks"); document.querySelector("#plan_financeTabActivity").classList.toggle("hidden", tab !== "activity"); document.querySelector(".plan-form-actions").classList.toggle("hidden", tab === "activity"); if (tab === "activity") planInitSquire(); }; // ---------- 活动与跟进 ---------- async function planLoadFollowups(pfId) { const list = document.querySelector("#plan_finActivityList"); if (!list || !pfId) return; try { const fups = await api(`/api/followups/project_finance/${pfId}`); list.innerHTML = fups.length ? fups.map(f => `
${esc(f.follower)} · ${esc(f.follow_up_method)}${esc(f.followed_at)}
${f.next_action ? `

下一步:${text(f.next_action)}

` : ""}
`).join("") : '

暂无跟进记录

'; if (window.lucide) window.lucide.createIcons(); list.querySelectorAll(".rich-content").forEach(el => { const html = el.dataset.html; if (html) el.innerHTML = decodeURIComponent(html); }); } catch (e) { /* ignore */ } } function planInitSquire() { const ed = document.querySelector("#plan_squire_finance"); if (!ed || !window.Squire) return; if (window.squireInstances["squire_finance"]) { window.squireInstances["squire_finance"].destroy(); } const sq = new Squire(ed, { blockTag: "P" }); window.squireInstances["squire_finance"] = sq; ed.addEventListener("focus", () => ed.classList.add("focused")); ed.addEventListener("blur", () => { if (!ed.textContent.trim()) ed.classList.remove("focused"); }); } window.planSubmitComment = async () => { const pfId = document.querySelector("#plan_pf-id-input").value; if (!pfId) return; const sq = window.squireInstances["squire_finance"]; const content = sq ? sq.getHTML().trim() : ""; if (!content || content === "

" || content === "


") return; const btn = document.querySelector("#financeTabActivity .comment-submit"); btn.disabled = true; btn.textContent = "发送中…"; await api(`/api/followups/project_finance/${pfId}`, { method: "POST", body: JSON.stringify({ data: { content } }) }); sq.setHTML(""); btn.disabled = false; btn.textContent = "评论"; await planLoadFollowups(pfId); }; window.planDeleteFollowup = async (event, followupId) => { event.stopPropagation(); if (!confirm("确认删除这条评论?")) return; await api(`/api/followups/${followupId}`, { method: "DELETE" }); const pfId = document.querySelector("#plan_pf-id-input").value; if (pfId) await planLoadFollowups(pfId); }; window.planOpenPfEditModal = (pfId) => { const pf = (state.data.planFinances || []).find(x => x.id === pfId); if (!pf) return; document.querySelector("#plan_pf-id-input").value = pf.id; document.querySelector("#planModalTitle").textContent = "编辑项目财务"; document.querySelector("#planDeleteBtn").classList.remove("hidden"); const form = document.querySelector("#planModal form"); form.querySelector('[name="project_id"]').value = pf.project_id || ""; const deptDisplay = form.querySelector('.bg-slate-50 [disabled]'); if (deptDisplay) deptDisplay.value = pf.project_id || ""; // 回填业务类型多选 const btValues = (pf.business_type || "").split(/[,,]/).map(s => s.trim()).filter(Boolean); form.querySelectorAll('[name="business_type[]"]').forEach(cb => { cb.checked = btValues.includes(cb.value); const chip = cb.closest('.bt-chip'); if (chip) { chip.classList.toggle('bg-blue-50', cb.checked); chip.classList.toggle('border-blue-400', cb.checked); chip.classList.toggle('text-blue-600', cb.checked); } }); form.querySelector('[name="customer_name"]').value = pf.customer_name || ""; const setVal = (name, val) => { const el = form.querySelector(`[name="${name}"]`); if (el) el.value = val || ""; }; setVal("project_code", pf.project_code); form.querySelector('[name="sign_amount"]').value = pf.sign_amount || ""; const signMonthValue = pf.sign_month || ""; const signMonthEl = form.querySelector('[name="sign_month"]'); if (signMonthEl && signMonthValue) { signMonthEl.innerHTML = monthOptions(signMonthValue); signMonthEl.value = signMonthValue; } form.querySelector('[name="sales_person"]').value = pf.sales_person || ""; form.querySelector('[name="owner"]').value = pf.owner || ""; setVal("start_date", pf.start_date); setVal("end_date", pf.end_date); setVal("task_type", pf.task_type); setVal("task_count", pf.task_count); setVal("service_fee_standard", pf.service_fee_standard || 5); setVal("project_manager", pf.project_manager); setVal("contact_name", pf.contact_name); setVal("contact_phone", pf.contact_phone); setVal("other_info", pf.other_info); setVal("client_name", pf.client_name); form.querySelector('[name="project_type"]').value = pf.project_type || "待签约"; form.querySelector('[name="weight"]').value = pf.weight || "20%"; let budgetData = []; try { budgetData = JSON.parse(pf.budget_data || "[]"); } catch (e) { budgetData = []; } planInitRevpayTable(budgetData.length ? budgetData : null); let expenseData = []; try { expenseData = JSON.parse(pf.expense_data || "[]"); } catch (e) { expenseData = []; } planInitCostTable(expenseData.length ? expenseData : null); let taskData = []; try { taskData = JSON.parse(pf.task_data || "[]"); } catch (e) { taskData = []; } planInitTaskTable(taskData.length ? taskData : null); setTimeout(() => updateBudgetSummary(), 100); planLoadFollowups(pf.id); openPlanModal(); }; window.createPlanFinance = async (event) => { event.preventDefault(); const form = event.currentTarget; const data = Object.fromEntries(new FormData(form).entries()); // 业务类型多选:合并为逗号分隔 const btChecked = form.querySelectorAll('[name="business_type[]"]:checked'); data.business_type = Array.from(btChecked).map(cb => cb.value).join(","); data.tenant = state.tenant; // 必填校验 if (!data.customer_name || !data.customer_name.trim()) { toast("项目名称必填", "error"); return; } if (!data.sales_person || !data.sales_person.trim()) { toast("商务负责人必填", "error"); return; } if (!data.owner || !data.owner.trim()) { toast("经营负责人必填", "error"); return; } if (!data.sign_month) { toast("签约月份必填", "error"); return; } data.sign_amount = parseFloat(data.sign_amount) || 0; if (!(data.sign_amount > 0)) { toast("签约金额必须大于 0", "error"); return; } const months = form.querySelectorAll('[name="budget_month[]"]'); const revs = form.querySelectorAll('[name="budget_rev[]"]'); const grosses = form.querySelectorAll('[name="budget_gross[]"]'); const payments = form.querySelectorAll('[name="budget_payment[]"]'); const revNotes = form.querySelectorAll('[name="budget_rev_note[]"]'); const budgetRows = []; let totalRev = 0, totalGross = 0, totalPayment = 0; for (let i = 0; i < months.length; i++) { const m = months[i].value.trim(); if (!m) continue; const rev = parseFloat(revs[i].value) || 0; const gross = parseFloat(grosses[i].value) || 0; const payment = parseFloat(payments[i].value) || 0; const rev_note = (revNotes[i] ? revNotes[i].value : '') || ''; budgetRows.push({ month: m, rev, gross, payment, rev_note }); totalRev += rev; totalGross += gross; totalPayment += payment; } data.budget_data = JSON.stringify(budgetRows); data.total_rev = totalRev; data.total_gross = totalGross; data.total_payment = totalPayment; // 费用明细数据 const expMonths = form.querySelectorAll('[name="expense_month[]"]'); const expTypes = form.querySelectorAll('[name="expense_type[]"]'); const expCosts = form.querySelectorAll('[name="expense_cost[]"]'); const expPaids = form.querySelectorAll('[name="expense_paid[]"]'); const expNotes = form.querySelectorAll('[name="expense_note[]"]'); const expenseRows = []; let totalCost = 0, totalPaid = 0; for (let i = 0; i < expMonths.length; i++) { const m = expMonths[i].value.trim(); if (!m) continue; const expense_type = (expTypes[i] ? expTypes[i].value : '') || ''; const cost = parseFloat(expCosts[i].value) || 0; const paid = parseFloat(expPaids[i].value) || 0; const exp_note = (expNotes[i] ? expNotes[i].value : '') || ''; expenseRows.push({ month: m, expense_type, cost, paid, exp_note }); totalCost += cost; totalPaid += paid; } data.expense_data = JSON.stringify(expenseRows); data.total_paid = totalPaid; data.total_cost = totalCost; for (const r of budgetRows) { totalPayment += r.payment; } data.total_payment = totalPayment; // 收集任务管理数据 const taskTypeInputs = form.querySelectorAll('[name="task_type[]"]'); const taskCountInputs = form.querySelectorAll('[name="task_count[]"]'); const taskExecInputs = form.querySelectorAll('[name="task_executed[]"]'); const taskRows = []; for (let i = 0; i < taskTypeInputs.length; i++) { const tt = taskTypeInputs[i].value.trim(); if (!tt) continue; taskRows.push({ task_month: form.querySelectorAll('[name="task_month[]"]')[i].value || '', task_type: tt, task_count: parseFloat(taskCountInputs[i].value) || 0, task_executed: parseFloat(taskExecInputs[i].value) || 0, unit_price: parseFloat(form.querySelectorAll('[name="task_unit_price[]"]')[i].value) || 0, }); } data.task_data = JSON.stringify(taskRows); // 清除数组命名的字段(FormData 会收集 task_type[] 等),避免后端写入不存在的列 delete data.task_type; delete data.task_count; for (const key of Object.keys(data)) { if (key.endsWith('[]')) delete data[key]; } const pfId = data.pf_id; delete data.pf_id; try { if (pfId) { await api("/api/planFinances/" + pfId, { method: "PUT", body: JSON.stringify({ data }) }); if (data.customer_name) logActivity("finance", pfId, "更新了「" + data.customer_name + "」的财务信息"); } else { const result = await api("/api/planFinances", { method: "POST", body: JSON.stringify({ data }) }); if (result.id && data.customer_name) logActivity("finance", result.id, "创建了「" + data.customer_name + "」的财务项目"); } form.reset(); document.querySelector("#plan_pf-id-input").value = ""; document.querySelector("#planModalTitle").textContent = "新增项目财务"; closePlanModal(); await load(); } catch (error) { toast("保存失败:" + error.message, "error"); } }; window.deletePlanItem = async () => { const pfId = document.querySelector("#plan_pf-id-input").value; if (!pfId) return; const pf = (state.data.planFinances || []).find(x => x.id === parseInt(pfId)); const name = pf ? (pf.customer_name || "此项目") : "此项目"; if (!confirm(`确认删除「${name}」?此操作不可撤销。`)) return; try { await api(`/api/planFinances/${pfId}`, { method: "DELETE" }); closePlanModal(); await load(); toast("已删除", "success"); } catch (error) { toast("删除失败:" + error.message, "error"); } } // 从 planFinances 计算月度数据 function computePlanMonthly(planFinances) { var pfs = planFinances || []; var months = []; var year = new Date().getFullYear(); for (var mi = 1; mi <= 12; mi++) { months.push(year + '-' + String(mi).padStart(2, '0')); } var data = []; for (var i = 0; i < 12; i++) { var month = months[i]; var sign = 0, revenue = 0, gross = 0, payment = 0, cost = 0, paid = 0; for (var j = 0; j < pfs.length; j++) { var pf = pfs[j]; if ((pf.sign_month || '') === month) sign += parseFloat(pf.sign_amount || 0); var bd = []; try { bd = JSON.parse(pf.budget_data || '[]'); } catch(e) {} var b = bd.find(function(x) { return (x.month || '') === month; }); if (b) { revenue += parseFloat(b.rev || 0); gross += parseFloat(b.gross || 0); payment += parseFloat(b.payment || 0); } var ed = []; try { ed = JSON.parse(pf.expense_data || '[]'); } catch(e) {} var e = ed.find(function(x) { return (x.month || '') === month; }); if (e) { cost += parseFloat(e.cost || 0); paid += parseFloat(e.paid || 0); } } data.push({ month: month, sign: Math.round(sign), revenue: Math.round(revenue), gross: Math.round(gross), payment: Math.round(payment), cost: Math.round(cost), paid: Math.round(paid) }); } return data; } // 总览渲染 function renderPlanOverview() { const planFinances = state.data.planFinances || []; const fm = computePlanMonthly(planFinances); const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN"); const moneyWan = (v) => { const n = Number(v || 0); return n >= 10000 ? (n / 10000).toFixed(0) + "万" : n.toLocaleString("zh-CN"); }; const card = (body, cls) => '
' + body + '
'; // 从 planFinances 计算年度累计 var annualSums = { sign: 0, revenue: 0, gross: 0, cost: 0, payment: 0, paid: 0 }; for (var i = 0; i < fm.length; i++) { annualSums.sign += fm[i].sign; annualSums.revenue += fm[i].revenue; annualSums.gross += fm[i].gross; annualSums.cost += fm[i].cost; annualSums.payment += fm[i].payment; annualSums.paid += fm[i].paid; } const mLbl = ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月']; const LABELS = ['合同金额','确收金额','确收毛利','成本','项目利润','回款金额','已付','现金流']; // 每行的年度累计 + 12个月值 const annualVals = [ annualSums.sign, annualSums.revenue, annualSums.gross, annualSums.cost, annualSums.gross - annualSums.cost, annualSums.payment, annualSums.paid, annualSums.payment - annualSums.paid, ]; // 字段映射: sign, revenue, gross, cost, profit(=gross-cost), payment, paid, cashflow(=payment-paid) const fmFields = ['sign','revenue','gross','cost',null,'payment','paid',null]; var thead = '指标年度累计'; for (var mi = 0; mi < 12; mi++) { thead += '' + mLbl[mi] + ''; } thead += ''; var tbody = ''; for (var ri = 0; ri < 8; ri++) { var rowHtml = '' + LABELS[ri] + ''; rowHtml += '' + moneyIntL(annualVals[ri]) + ''; for (var mi = 0; mi < 12; mi++) { var val = 0; if (fm[mi]) { if (ri === 4) { val = (fm[mi].gross || 0) - (fm[mi].cost || 0); } else if (ri === 7) { val = (fm[mi].payment || 0) - (fm[mi].paid || 0); } else { var fk = fmFields[ri]; if (fk) val = fm[mi][fk] || 0; } } var cls = 'text-slate-700'; if (ri === 4) cls = val >= 0 ? 'text-green-600' : 'text-red-600'; if (ri === 7) cls = val >= 0 ? 'text-green-600' : 'text-red-600'; rowHtml += '' + moneyIntL(val) + ''; } tbody += rowHtml + ''; } document.querySelector("#planOverview").innerHTML = '
' + card('

业务(项目)财务概览

合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流

' + thead + '' + tbody + '
', 'p-4') + '
' + card('

月度签约趋势

2026
', 'p-4') + card('

月度确收与毛利

2026
', 'p-4') + card('

月度回款与已付

2026
', 'p-4') + card('

月度项目利润

2026
', 'p-4') + '
'; if (window.lucide) window.lucide.createIcons(); // 渲染图表(复用 home.js 的 moneyTick / chartOptions / monthLabels) if (fm && window.Chart) { renderPlanCharts(fm); } } function renderPlanQuarterlyOverview() { const planFinances = state.data.planFinances || []; const fm = computePlanMonthly(planFinances); const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN"); const card = (body, cls) => '
' + body + '
'; 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.gross - annualSums2.cost, annualSums2.payment, annualSums2.paid, annualSums2.payment - annualSums2.paid]; const fmFields = ['sign','revenue','gross','cost',null,'payment','paid',null]; // 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],[0,0,0,0],[0,0,0,0],[0,0,0,0]]; for (var qi = 0; qi < 4; qi++) { for (var ri = 0; ri < 8; ri++) { var sum = 0; for (var mi = 0; mi < 3; mi++) { var idx = qRanges[qi][mi]; if (fm[idx]) { if (ri === 4) { sum += (fm[idx].gross||0) - (fm[idx].cost||0); } else if (ri === 7) { 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 = '指标年度累计'; thead += 'Q1'; thead += 'Q2环比'; thead += 'Q3环比'; thead += 'Q4环比'; thead += ''; var qoq = function(cur, prev) { if (!prev) return ''; var pct = Math.round((cur - prev) / prev * 100); var cls = pct >= 0 ? 'text-green-600' : 'text-red-600'; var sign = pct >= 0 ? '+' : ''; return '' + sign + pct + '%'; }; var tbody = ''; for (var ri = 0; ri < 8; ri++) { var rowHtml = '' + LABELS[ri] + ''; rowHtml += '' + moneyIntL(annualVals[ri]) + ''; for (var qi = 0; qi < 4; qi++) { var val = qData[ri][qi]; var cls = 'text-slate-700'; if (ri === 4) cls = val >= 0 ? 'text-green-600' : 'text-red-600'; if (ri === 7) cls = val >= 0 ? 'text-green-600' : 'text-red-600'; rowHtml += '' + moneyIntL(val) + ''; if (qi > 0) { rowHtml += '' + qoq(val, qData[ri][qi - 1]) + ''; } } tbody += rowHtml + ''; } document.querySelector("#planQuarterlyOverview").innerHTML = '
' + card('

业务(项目)财务季度概览

合同 → 确收 → 毛利 → 成本 → 项目利润 → 回款 → 已付 → 现金流

' + thead + '' + tbody + '
', 'p-4') + '
'; if (window.lucide) window.lucide.createIcons(); } function renderPlanCharts(data) { if (typeof monthLabels !== 'function') return; const labels = monthLabels(data); const baseOpts = typeof chartOptions === 'function' ? chartOptions(moneyTick) : { responsive: true, maintainAspectRatio: false }; var iconf = { type: "line", options: baseOpts, data: { labels: labels } }; var c1 = document.querySelector("#planChartSign"); if (c1 && window.Chart) { if (state.planChart1) state.planChart1.destroy(); state.planChart1 = new Chart(c1, Object.assign({}, iconf, { data: { labels: labels, datasets: [{ label: "签约金额", data: data.map(function(x) { return x.sign || 0; }), borderColor: "#6366f1", backgroundColor: "rgba(99,102,241,0.06)", fill: true, tension: 0.3 }] } })); } var c2 = document.querySelector("#planChartRev"); if (c2 && window.Chart) { if (state.planChart2) state.planChart2.destroy(); state.planChart2 = new Chart(c2, Object.assign({}, iconf, { data: { labels: labels, datasets: [{ label: "确收", data: data.map(function(x) { return x.revenue || 0; }), borderColor: "#2563eb", backgroundColor: "rgba(37,99,235,0.06)", fill: true, tension: 0.3 }, { label: "毛利", data: data.map(function(x) { return x.gross || 0; }), borderColor: "#059669", backgroundColor: "rgba(5,150,105,0.06)", fill: true, tension: 0.3 }] } })); } var c3 = document.querySelector("#planChartCash"); if (c3 && window.Chart) { if (state.planChart3) state.planChart3.destroy(); state.planChart3 = new Chart(c3, Object.assign({}, iconf, { data: { labels: labels, datasets: [{ label: "回款", data: data.map(function(x) { return x.payment || 0; }), borderColor: "#d97706", backgroundColor: "rgba(217,119,6,0.06)", fill: true, tension: 0.3 }, { label: "已付", data: data.map(function(x) { return x.cost || 0; }), borderColor: "#7c3aed", backgroundColor: "rgba(124,58,237,0.06)", fill: true, tension: 0.3 }] } })); } var c4 = document.querySelector("#planChartProfit"); if (c4 && window.Chart) { if (state.planChart4) state.planChart4.destroy(); state.planChart4 = new Chart(c4, Object.assign({}, iconf, { data: { labels: labels, datasets: [{ label: "利润", data: data.map(function(x) { return x.gross || 0; }), borderColor: "#6366f1", backgroundColor: "rgba(99,102,241,0.06)", fill: true, tension: 0.3 }] } })); } }; function planInlineSelect(name, pfId, field, currentVal, options) { var opts = ''; for (var i = 0; i < options.length; i++) { opts += ''; } return ''; } window.planInlineUpdate = async (pfId, field, value) => { try { await api("/api/planFinances/" + pfId, { method: "PUT", body: JSON.stringify({ data: { [field]: value } }) }); await load(); toast("已更新", "success"); } catch (error) { toast("更新失败:" + error.message, "error"); } };