// 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 `${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.customer_name)}${signMonthCell}${money(pf.sign_amount)}${mCols}${totalCol}`; }; const finHeaderBase = `|筛选:状态:`; const finAddBtn = ``; document.querySelector("#finance").innerHTML = `
${[["已签项目","" + signed.length,"file-check-2"],["签约金额",moneyWan(sumSign),"coins"],["待签项目","" + pending.length,"file-question"],["待签金额",moneyWan(sumPending),"hourglass"]].map(([l,v,icon]) => `
${l}${v}
`).join("")}
${[["本月确收",moneyInt(thisMonthRev),"trending-up"],["本月毛利",moneyInt(thisMonthGross),"percent"],["本月回款",moneyInt(monthPayment),"wallet"],["本月应付",moneyInt(monthCost),"receipt"],["本月现金流",moneyInt(monthCashflow),"repeat"]].map(([l,v,icon]) => `
${l}${v}
`).join("")}
${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 => ``).join(""); const fmt = (v) => v ? `${money(v)}` : ''; const fmtDiff = (v) => { if (!v) return ''; return `${money(Math.abs(v))}`; }; 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(`${esc(pf.customer_name)}${pf.status==='已签约'?'已签约':pf.status==='流程中'?'流程中':'待签约'}${fmt(rev)}${fmt(payment)}${fmtDiff(payDiff)}${fmt(cost)}${fmt(paid)}${fmtDiff(costDiff)}${cashflow ? money(cashflow) : ''}`); }); return card(`
${finHeaderBase}月份:
${finAddBtn}
${rows.length ? rows.join("") : ''}
项目名称状态已确收已回款回款差额应付已付应付差额现金流
该月份暂无数据
合计${money(sumRev)}${money(sumPay)}${fmtDiff(sumRev - sumPay)}${money(sumCost)}${money(sumPaid)}${fmtDiff(sumCost - sumPaid)}${money(sumPay - sumPaid)}
`, "p-4"); })() : state.finView === 'quarterly' ? (() => { const allPfs = pfs.filter(x => x.status === state.finFilter); const qRanges = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]]; const qLabels = ["Q1 (1-3月)", "Q2 (4-6月)", "Q3 (7-9月)", "Q4 (10-12月)"]; const now = new Date(); if (state.finQuarter === undefined) { const saved = localStorage.getItem("opc-fin-quarter"); state.finQuarter = saved !== null ? parseInt(saved) : Math.floor(now.getMonth() / 3); } const selQ = state.finQuarter; const qRange = qRanges[selQ]; const quarterOpts = qLabels.map((l, i) => ``).join(""); 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 fmt = (v) => v ? `${money(v)}` : ''; const fmtDiff = (v) => { if (!v) return ''; return `${money(Math.abs(v))}`; }; let sumRev = 0, sumPay = 0, sumCost = 0, sumPaid = 0; const rows = []; allPfs.forEach(pf => { const rev = sumBudget(pf, "rev"); const payment = sumBudget(pf, "payment"); const cost = sumBudget(pf, "cost"); const paid = sumBudget(pf, "paid"); const payDiff = rev - payment; const costDiff = cost - paid; const cashflow = payment - paid; sumRev += rev; sumPay += payment; sumCost += cost; sumPaid += paid; rows.push(`${esc(pf.customer_name)}${pf.status==='已签约'?'已签约':pf.status==='流程中'?'流程中':'待签约'}${fmt(rev)}${fmt(payment)}${fmtDiff(payDiff)}${fmt(cost)}${fmt(paid)}${fmtDiff(costDiff)}${cashflow ? money(cashflow) : ''}`); }); return card(`
${finHeaderBase}季度:
${finAddBtn}
${rows.length ? rows.join("") : ''}
项目名称状态已确收已回款回款差额应付已付应付差额现金流
该季度暂无数据
合计${money(sumRev)}${money(sumPay)}${fmtDiff(sumRev - sumPay)}${money(sumCost)}${money(sumPaid)}${fmtDiff(sumCost - sumPaid)} ${money(sumPay - sumPaid)}
`, "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 ? `${money(v)}` : ''; const fmtDiff = (v) => { if (!v) return ''; return `${money(Math.abs(v))}`; }; return card(`
${finHeaderBase}
${finAddBtn}
${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 ``; }).join("")}
项目名称状态已确收已回款回款差额应付已付应付差额现金流
${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)}
`, "p-4"); })()}
`; if (window.lucide) window.lucide.createIcons(); } window.openFinanceModal = () => { const modal = document.querySelector("#financeModal"); 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) { initBudgetTable(null); initTaskTable(null); document.querySelector("#financeDeleteBtn").classList.add("hidden"); } modal.classList.remove("hidden"); }; window.addBudgetRow = (month = '', rev = '', gross = '', payment = '', cost = '', paid = '') => { const tbody = document.querySelector("#budgetTbody"); if (!tbody) return; const row = document.createElement("tr"); row.innerHTML = ` `; tbody.appendChild(row); if (window.lucide) window.lucide.createIcons(); }; window.updateBudgetSummary = () => { const revEl = document.querySelector("#budgetTotalRev"); 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[]"]'); 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) => { const tbody = document.querySelector("#budgetTbody"); if (!tbody) return; tbody.innerHTML = ""; const rows = budgetData || []; rows.forEach(r => addBudgetRow(r.month || '', r.rev || '', r.gross || '', r.payment || '', r.cost || '', r.paid || '')); setTimeout(() => updateBudgetSummary(), 50); }; // ---------- 任务管理 tab ---------- const TASK_TYPES = ["科普视频", "科普专访", "科普文章", "问卷调研", "病例征集"]; function taskMonthOptions(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.addTaskRow = (taskMonth = '', taskType = '', taskCount = '', executedCount = '', unitPrice = '') => { const tbody = document.querySelector("#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(); updateRowCalc(row); }; window.updateTaskDiff = (el) => { const row = el.closest('tr'); if (!row) return; updateRowCalc(row); }; function updateRowCalc(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.toggleBtChip = (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.initTaskTable = (taskData) => { const tbody = document.querySelector("#taskTbody"); if (!tbody) return; tbody.innerHTML = ""; const rows = taskData || []; rows.forEach(r => addTaskRow(r.task_month || '', r.task_type || '', r.task_count || '', r.task_executed || '', r.unit_price || '')); }; window.onTaskTypeChange = (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.revertTaskType = (btn) => { const td = btn.parentElement; td.innerHTML = ``; if (window.lucide) window.lucide.createIcons(); }; window.closeFinanceModal = () => { const modal = document.querySelector("#financeModal"); modal.classList.add("hidden"); }; window.editPfSignMonth = (event, pfId) => { event.stopPropagation(); const pf = (state.data.projectFinances || []).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/projectFinances/${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.switchFinanceTab = (tab) => { document.querySelectorAll(".finance-tab").forEach(b => b.classList.toggle("active", b.dataset.tab === tab)); document.querySelector("#financeTabInfo").classList.toggle("hidden", tab !== "info"); document.querySelector("#financeTabBudget").classList.toggle("hidden", tab !== "budget"); document.querySelector("#financeTabExec").classList.toggle("hidden", tab !== "exec"); document.querySelector("#financeTabTasks").classList.toggle("hidden", tab !== "tasks"); document.querySelector("#financeTabActivity").classList.toggle("hidden", tab !== "activity"); document.querySelector(".finance-form-actions").classList.toggle("hidden", tab === "activity"); if (tab === "activity") initFinSquire(); }; // ---------- 活动与跟进 ---------- async function loadFinFollowups(pfId) { const list = document.querySelector("#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 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 === "

" || 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 loadFinFollowups(pfId); }; window.deleteFinFollowup = async (event, followupId) => { event.stopPropagation(); if (!confirm("确认删除这条评论?")) return; await api(`/api/followups/${followupId}`, { method: "DELETE" }); const pfId = document.querySelector("#pf-id-input").value; if (pfId) await loadFinFollowups(pfId); }; window.openPfEditModal = (pfId) => { const pf = (state.data.projectFinances || []).find(x => x.id === pfId); if (!pf) return; document.querySelector("#pf-id-input").value = pf.id; document.querySelector("#financeModalTitle").textContent = "编辑项目财务"; document.querySelector("#financeDeleteBtn").classList.remove("hidden"); const form = document.querySelector("#financeModal 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="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); 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); let budgetData = []; try { budgetData = JSON.parse(pf.budget_data || "[]"); } catch (e) { budgetData = []; } initBudgetTable(budgetData.length ? budgetData : null); let taskData = []; try { taskData = JSON.parse(pf.task_data || "[]"); } catch (e) { taskData = []; } initTaskTable(taskData.length ? taskData : null); setTimeout(() => updateBudgetSummary(), 100); loadFinFollowups(pf.id); openFinanceModal(); }; window.createFinance = 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 costs = form.querySelectorAll('[name="budget_cost[]"]'); const paids = form.querySelectorAll('[name="budget_paid[]"]'); const budgetRows = []; let totalRev = 0, totalGross = 0, totalPaidFromBudget = 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 cost = parseFloat(costs[i].value) || 0; 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 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/projectFinances/" + pfId, { method: "PUT", body: JSON.stringify({ data }) }); if (data.customer_name) logActivity("finance", pfId, "更新了「" + data.customer_name + "」的财务信息"); } else { const result = await api("/api/projectFinances", { method: "POST", body: JSON.stringify({ data }) }); if (result.id && data.customer_name) logActivity("finance", result.id, "创建了「" + data.customer_name + "」的财务项目"); } form.reset(); document.querySelector("#pf-id-input").value = ""; document.querySelector("#financeModalTitle").textContent = "新增项目财务"; closeFinanceModal(); await load(); } catch (error) { toast("保存失败:" + error.message, "error"); } }; window.deleteFinanceItem = async () => { const pfId = document.querySelector("#pf-id-input").value; if (!pfId) return; const pf = (state.data.projectFinances || []).find(x => x.id === parseInt(pfId)); const name = pf ? (pf.customer_name || "此项目") : "此项目"; if (!confirm(`确认删除「${name}」?此操作不可撤销。`)) return; try { await api(`/api/projectFinances/${pfId}`, { method: "DELETE" }); closeFinanceModal(); await load(); toast("已删除", "success"); } catch (error) { toast("删除失败:" + error.message, "error"); } };