// finance.js — 经营管理(财务)模块 const moneyInt = (v) => `${Math.round(Number(v || 0)).toLocaleString("zh-CN")} 元`; const moneyWan = (v) => `${(Number(v || 0) / 10000).toFixed(1)} 万`; function renderFinance() { const pfs = state.data.projectFinances || []; 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.filter(x => x.status === "已签约"); const inContract = pfs.filter(x => x.status === "流程中"); const pending = pfs.filter(x => x.status === "待签约"); 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 sumContract = Math.round(inContract.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.finView !== "cashflow" && state.finView !== "overview" && state.finView !== "monthly" && state.finView !== "quarterly"; const mCols = months.map(m => { const b = budgetMap[m] || {}; if (isRevView) { const rev = b.rev || 0; const gross = b.gross || 0; return `
填写项目财务信息与月度预算
| 项目名称 | 状态 | 已确收 | 已回款 | 回款差额 | 应付 | 已付 | 应付差额 | 现金流 |
|---|---|---|---|---|---|---|---|---|
| 该月份暂无数据 | ||||||||
| 合计 | ${money(sumRev)} | ${money(sumPay)} | ${fmtDiff(sumRev - sumPay)} | ${money(sumCost)} | ${money(sumPaid)} | ${fmtDiff(sumCost - sumPaid)} | ${money(sumPay - sumPaid)} | |
| 项目名称 | 状态 | 已确收 | 已回款 | 回款差额 | 应付 | 已付 | 应付差额 | 现金流 |
|---|---|---|---|---|---|---|---|---|
| 该季度暂无数据 | ||||||||
| 合计 | ${money(sumRev)} | ${money(sumPay)} | ${fmtDiff(sumRev - sumPay)} | ${money(sumCost)} | ${money(sumPaid)} | ${fmtDiff(sumCost - sumPaid)} | ${money(sumPay - sumPaid)} | |
| 项目名称 | 状态 | 已确收 | 已回款 | 回款差额 | 应付 | 已付 | 应付差额 | 现金流 |
|---|---|---|---|---|---|---|---|---|
| ${esc(pf.customer_name)} | ${pf.status==='已签约'?'已签约':pf.status==='流程中'?'流程中':'待签约'} | ${fmt(t.rev)} | ${fmt(t.payment)} | ${fmtDiff(payDiff)} | ${fmt(t.cost)} | ${fmt(t.paid)} | ${fmtDiff(costDiff)} | ${cashflow ? money(cashflow) : '—'} |
| 合计 | ${money(sumRev)} | ${money(sumPay)} | ${fmtDiff(sumRev - sumPay)} | ${money(sumCost)} | ${money(sumPaid)} | ${fmtDiff(sumCost - sumPaid)} | ${money(sumPay - sumPaid)} | |
下一步:${text(f.next_action)}
` : ""}暂无跟进记录
'; 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 initFinSquire() { const ed = document.querySelector("#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.submitFinComment = async () => { const pfId = document.querySelector("#pf-id-input").value; if (!pfId) return; const sq = window.squireInstances["squire_finance"]; const content = sq ? sq.getHTML().trim() : ""; if (!content || content === "