Some checks failed
Deploy / deploy (push) Failing after 1s
- 经营管理/重点工作台账卡片改用 .metric-card 类(与首页一致) - 卡片增加 lucide 图标(签约/金额/任务/状态等) - 布局:左对齐、text-2xl、图标+标签
367 lines
25 KiB
JavaScript
367 lines
25 KiB
JavaScript
// finance.js — 经营管理(财务)模块
|
|
|
|
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";
|
|
const mCols = months.map(m => {
|
|
const b = budgetMap[m] || {};
|
|
if (isRevView) {
|
|
const rev = b.rev || 0;
|
|
const gross = b.gross || 0;
|
|
return `<td class="p-2 text-right whitespace-nowrap"><span class="${rev ? 'text-blue-700 font-medium' : 'text-slate-300'}">${rev ? money(rev) : '—'}</span><br><span class="text-xs ${gross ? 'text-green-600' : 'text-slate-300'}">${gross ? money(gross) : '—'}</span></td>`;
|
|
} else {
|
|
const payment = b.payment || 0;
|
|
const cost = b.cost || 0;
|
|
return `<td class="p-2 text-right whitespace-nowrap"><span class="${payment ? 'text-amber-700 font-medium' : 'text-slate-300'}">${payment ? money(payment) : '—'}</span><br><span class="text-xs ${cost ? 'text-rose-600' : 'text-slate-300'}">${cost ? money(cost) : '—'}</span></td>`;
|
|
}
|
|
}).join("");
|
|
const totalCol = (() => {
|
|
if (isRevView) {
|
|
const totalRev = pf.total_rev || 0;
|
|
const totalGross = pf.total_gross || 0;
|
|
return `<td class="p-2 text-right whitespace-nowrap font-semibold"><span class="${totalRev ? 'text-blue-700' : 'text-slate-300'}">${totalRev ? money(totalRev) : '—'}</span><br><span class="text-xs ${totalGross ? 'text-green-600' : 'text-slate-300'}">${totalGross ? money(totalGross) : '—'}</span></td>`;
|
|
} else {
|
|
let totalPayment = 0, totalCost = 0;
|
|
try { JSON.parse(pf.budget_data || "[]").forEach(b => { totalPayment += parseFloat(b.payment||0)||0; totalCost += parseFloat(b.cost||0)||0; }); } catch (e) {}
|
|
return `<td class="p-2 text-right whitespace-nowrap font-semibold"><span class="${totalPayment ? 'text-amber-700' : 'text-slate-300'}">${totalPayment ? money(totalPayment) : '—'}</span><br><span class="text-xs ${totalCost ? 'text-rose-600' : 'text-slate-300'}">${totalCost ? money(totalCost) : '—'}</span></td>`;
|
|
}
|
|
})();
|
|
const sm = pf.sign_month || "";
|
|
const signMonthCell = `<td class="p-2 text-center text-sm"><span class="pf-sm-text cursor-pointer hover:text-blue-600" id="pf-sm-${pf.id}" onclick="event.stopPropagation(); editPfSignMonth(event, ${pf.id})">${sm || '—'}</span></td>`;
|
|
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal(${pf.id})"><td class="p-2 text-sm font-medium text-center">${esc(pf.customer_name)}</td><td class="p-2 text-sm text-center">${esc(pf.business_type)}</td><td class="p-2 text-sm text-center">${pf.status === "已签约" ? badge("已签约") : pf.status === "流程中" ? badge("流程中","blue") : badge("待签约","amber")}</td>${signMonthCell}<td class="p-2 text-center text-sm">${money(pf.sign_amount)}</td>${mCols}${totalCol}<td class="p-2 text-sm text-slate-500 text-center">${esc(pf.sales_person) || "—"}</td><td class="p-2 text-sm text-slate-500 text-center">${esc(pf.owner) || "—"}</td></tr>`;
|
|
};
|
|
|
|
document.querySelector("#finance").innerHTML = `<div class="grid gap-4">
|
|
<div class="grid grid-cols-6 gap-3">
|
|
${[["已签项目","" + signed.length,"file-sign"],["签约金额",money(sumSign),"coins"],["流程项目","" + inContract.length,"file-clock"],["流程金额",money(sumContract),"clock"],["待签项目","" + pending.length,"file-question"],["待签金额",money(sumPending),"hourglass"]].map(([l,v,icon]) => `<div class="metric-card"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide="${icon}" style="width:14px;height:14px"></i>${l}</span><strong class="mt-2 block text-2xl">${v}</strong></div>`).join("")}
|
|
</div>
|
|
<div class="grid grid-cols-5 gap-3">
|
|
${[["本月确收",money(thisMonthRev),"trending-up"],["本月毛利",money(thisMonthGross),"percent"],["本月回款",money(monthPayment),"wallet"],["本月费用",money(monthCost),"receipt"],["本月现金流",money(monthCashflow),"repeat"]].map(([l,v,icon]) => `<div class="metric-card"><span class="flex items-center gap-2 text-xs text-slate-500"><i data-lucide="${icon}" style="width:14px;height:14px"></i>${l}</span><strong class="mt-2 block text-2xl">${v}</strong></div>`).join("")}
|
|
</div>
|
|
<div class="flex justify-between items-center"><div class="flex items-center gap-1" id="finViewToggle"><button class="btn btn-sm ${state.finView !== 'cashflow' ? 'btn-primary' : 'btn-ghost'} p-1.5" data-view="rev" onclick="setFinView('rev')" title="确收/毛利视图"><i data-lucide="trending-up" style="width:16px;height:16px"></i></button><button class="btn btn-sm ${state.finView === 'cashflow' ? 'btn-primary' : 'btn-ghost'} p-1.5" data-view="cashflow" onclick="setFinView('cashflow')" title="回款/费用视图"><i data-lucide="wallet" style="width:16px;height:16px"></i></button></div><button class="btn btn-primary btn-sm" onclick="openFinanceModal()"><i data-lucide="plus"></i>新增财务项目</button></div>
|
|
<div id="financeModal" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/40" onclick="closeFinanceModal()"><div class="bg-white rounded-2xl shadow-2xl w-full max-w-5xl mx-4 max-h-[92vh] overflow-y-auto" onclick="event.stopPropagation()"><div class="sticky top-0 z-10 bg-white/95 backdrop-blur border-b border-slate-100 px-8 py-5 flex items-center justify-between"><div><h3 class="text-xl font-bold text-slate-800" id="financeModalTitle">新增项目财务</h3><p class="text-xs text-slate-400 mt-0.5">填写项目财务信息与月度预算</p></div><div class="flex items-center gap-2"><button class="btn btn-ghost btn-sm text-red-600 hidden" id="financeDeleteBtn" onclick="deleteFinanceItem()"><i data-lucide="trash-2"></i>删除</button><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closeFinanceModal()"><i data-lucide="x"></i></button></div></div>
|
|
<div class="finance-tabs">
|
|
<button class="finance-tab active" data-tab="info" onclick="switchFinanceTab('info')"><i data-lucide="info" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>基本信息</button>
|
|
<button class="finance-tab" data-tab="budget" onclick="switchFinanceTab('budget')"><i data-lucide="calendar" style="width:14px;height:14px;display:inline-block;vertical-align:-2px;margin-right:4px"></i>月度预算</button>
|
|
</div>
|
|
<form onsubmit="createFinance(event)" class="p-8 grid gap-6"><input type="hidden" name="pf_id" id="pf-id-input" value="">
|
|
<div id="financeTabInfo">
|
|
<div class="grid grid-cols-2 gap-5">
|
|
<div class="fin-field-group">
|
|
<p class="fin-section-label">项目信息</p>
|
|
<div class="grid gap-4">
|
|
<label class="block"><span class="fin-label">部门</span><input type="hidden" name="project_id" value="${state.tenant}"><input class="form-ctrl bg-slate-50 cursor-not-allowed" value="${state.tenant}" disabled></label>
|
|
<label class="block"><span class="fin-label">业务类型</span><select name="business_type" class="form-ctrl bg-white">${fmTypes.map(t => `<option>${t}</option>`).join("")}</select></label>
|
|
<label class="block"><span class="fin-label">项目名称 <span class="text-red-500">*</span></span><input name="customer_name" required class="form-ctrl" placeholder="请输入项目名称"></label>
|
|
</div>
|
|
</div>
|
|
<div class="fin-field-group">
|
|
<p class="fin-section-label">合同信息</p>
|
|
<div class="grid gap-4">
|
|
<label class="block"><span class="fin-label">签约金额(元) <span class="text-red-500">*</span></span><input name="sign_amount" type="number" step="0.01" min="0.01" required class="form-ctrl" placeholder="必须大于 0"></label>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<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><option>流程中</option><option>待签约</option></select></label>
|
|
</div>
|
|
<div class="grid grid-cols-2 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>
|
|
<label class="block"><span class="fin-label">经营负责人 <span class="text-red-500">*</span></span><input name="owner" required class="form-ctrl" placeholder="请输入经营负责人"></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="financeTabBudget" class="hidden">
|
|
<div class="grid grid-cols-4 gap-3 mb-4" id="budgetSummary">
|
|
<div class="bg-blue-50 rounded-lg p-3 text-center border border-blue-100">
|
|
<p class="text-xs text-blue-600 font-medium">总确收</p>
|
|
<p class="text-lg font-bold text-blue-700" id="budgetTotalRev">¥0</p>
|
|
</div>
|
|
<div class="bg-green-50 rounded-lg p-3 text-center border border-green-100">
|
|
<p class="text-xs text-green-600 font-medium">总毛利</p>
|
|
<p class="text-lg font-bold text-green-700" id="budgetTotalGross">¥0</p>
|
|
</div>
|
|
<div class="bg-amber-50 rounded-lg p-3 text-center border border-amber-100" id="budgetTotalPaymentCard">
|
|
<p class="text-xs text-amber-600 font-medium">总回款</p>
|
|
<p class="text-lg font-bold text-amber-700" id="budgetTotalPayment">¥0</p>
|
|
</div>
|
|
<div class="bg-rose-50 rounded-lg p-3 text-center border border-rose-100" id="budgetTotalCostCard">
|
|
<p class="text-xs text-rose-600 font-medium">总费用</p>
|
|
<p class="text-lg font-bold text-rose-700" id="budgetTotalCost">¥0</p>
|
|
</div>
|
|
</div>
|
|
<table class="w-full text-sm border border-slate-200 rounded-lg overflow-hidden" id="budgetTable">
|
|
<thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2.5 text-left font-medium text-slate-500" style="min-width:140px">月份</th><th class="p-2.5 text-right font-medium text-slate-500">确收</th><th class="p-2.5 text-right font-medium text-slate-500">毛利</th><th class="p-2.5 text-right font-medium text-slate-500">回款</th><th class="p-2.5 text-right font-medium text-slate-500">费用</th><th class="p-2.5 w-8"></th></tr></thead>
|
|
<tbody id="budgetTbody"></tbody>
|
|
</table>
|
|
<button type="button" class="btn btn-ghost btn-sm mt-3" onclick="addBudgetRow()"><i data-lucide="plus"></i>添加月份</button>
|
|
</div>
|
|
<div class="flex justify-end gap-3 pt-2"><button type="button" class="btn btn-ghost btn-sm px-6" onclick="closeFinanceModal()">取消</button><button type="submit" class="btn btn-primary btn-sm px-8">保存</button></div></form></div></div>
|
|
${card(`<div class="flex items-center justify-between mb-3"><h3 class="font-bold text-slate-700">项目明细 <span class="text-slate-400 font-normal">(${pfs.length})</span></h3></div><div class="flex gap-2 mb-3">${[["已签约","已签约"],["流程中","流程中"],["待签约","待签约"]].map(([k,v]) => `<button class="btn btn-sm ${state.finFilter === k ? 'btn-primary' : 'btn-ghost'}" onclick="state.finFilter='${k}';renderFinance()">${v} (${pfs.filter(x=>x.status===k).length})</button>`).join("")}</div><div class="overflow-x-auto"><table class="w-full text-sm"><thead><tr class="bg-slate-50 border-b border-slate-200"><th class="p-2 text-center font-semibold">项目名称</th><th class="p-2 text-center font-semibold">类型</th><th class="p-2 text-center font-semibold">状态</th><th class="p-2 text-center font-semibold">签约月份</th><th class="p-2 text-center font-semibold">签约金额</th>${monthLabels.map(l => `<th class="p-2 text-center font-semibold">${l}<br><span class="text-xs text-slate-400">${state.finView !== 'cashflow' ? '确收/毛利' : '回款/费用'}</span></th>`).join("")}<th class="p-2 text-center font-semibold">总计<br><span class="text-xs text-slate-400">${state.finView !== 'cashflow' ? '确收/毛利' : '回款/费用'}</span></th><th class="p-2 text-center font-semibold">商务负责人</th><th class="p-2 text-center font-semibold">经营负责人</th></tr></thead><tbody>${pfs.filter(x => x.status === state.finFilter).map(renderPfRow).join("")}</tbody></table></div>`, "p-4")}
|
|
</div>`;
|
|
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);
|
|
document.querySelector("#financeDeleteBtn").classList.add("hidden");
|
|
}
|
|
modal.classList.remove("hidden");
|
|
};
|
|
|
|
window.addBudgetRow = (month = '', rev = '', gross = '', payment = '', cost = '') => {
|
|
const tbody = document.querySelector("#budgetTbody");
|
|
if (!tbody) return;
|
|
const row = document.createElement("tr");
|
|
row.innerHTML = `<td><select name="budget_month[]" class="form-ctrl form-ctrl-sm w-full" style="min-width:140px" onchange="updateBudgetSummary()">${monthOptions(month)}</select></td>
|
|
<td><input name="budget_rev[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value="${rev}" oninput="updateBudgetSummary()"></td>
|
|
<td><input name="budget_gross[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value="${gross}" oninput="updateBudgetSummary()"></td>
|
|
<td><input name="budget_payment[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value="${payment}" oninput="updateBudgetSummary()"></td>
|
|
<td><input name="budget_cost[]" type="number" step="0.01" class="form-ctrl form-ctrl-sm w-full text-right" placeholder="0" value="${cost}" oninput="updateBudgetSummary()"></td>
|
|
<td><button type="button" class="btn btn-ghost btn-sm text-red-500 p-0 w-6 h-6" onclick="this.closest('tr').remove();updateBudgetSummary()"><i data-lucide="trash-2" style="width:14px;height:14px"></i></button></td>`;
|
|
tbody.appendChild(row);
|
|
if (window.lucide) window.lucide.createIcons();
|
|
};
|
|
|
|
window.updateBudgetSummary = () => {
|
|
const revEl = document.querySelector("#budgetTotalRev");
|
|
const grossEl = document.querySelector("#budgetTotalGross");
|
|
const paymentEl = document.querySelector("#budgetTotalPayment");
|
|
const costEl = document.querySelector("#budgetTotalCost");
|
|
if (!revEl || !grossEl) return;
|
|
const revInputs = document.querySelectorAll('[name="budget_rev[]"]');
|
|
const grossInputs = document.querySelectorAll('[name="budget_gross[]"]');
|
|
const paymentInputs = document.querySelectorAll('[name="budget_payment[]"]');
|
|
const costInputs = document.querySelectorAll('[name="budget_cost[]"]');
|
|
let totalRev = 0, totalGross = 0, totalPayment = 0, totalCost = 0;
|
|
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; });
|
|
revEl.textContent = money(totalRev);
|
|
grossEl.textContent = money(totalGross);
|
|
if (paymentEl) paymentEl.textContent = money(totalPayment);
|
|
if (costEl) costEl.textContent = money(totalCost);
|
|
};
|
|
|
|
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 || ''));
|
|
setTimeout(() => updateBudgetSummary(), 50);
|
|
};
|
|
|
|
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 = `<span class="pf-sm-text cursor-pointer hover:text-blue-600" onclick="editPfSignMonth(event, ${pfId})">${newValue || '—'}</span>`;
|
|
} catch (e) { toast("修改失败:" + e.message, "error"); }
|
|
});
|
|
select.addEventListener("blur", () => {
|
|
td.innerHTML = `<span class="pf-sm-text cursor-pointer hover:text-blue-600" onclick="editPfSignMonth(event, ${pfId})">${currentValue || '—'}</span>`;
|
|
});
|
|
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");
|
|
};
|
|
|
|
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 || "";
|
|
form.querySelector('[name="business_type"]').value = pf.business_type || "";
|
|
form.querySelector('[name="customer_name"]').value = pf.customer_name || "";
|
|
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 || "";
|
|
let budgetData = [];
|
|
try { budgetData = JSON.parse(pf.budget_data || "[]"); } catch (e) { budgetData = []; }
|
|
initBudgetTable(budgetData.length ? budgetData : null);
|
|
setTimeout(() => updateBudgetSummary(), 100);
|
|
openFinanceModal();
|
|
};
|
|
|
|
window.createFinance = async (event) => {
|
|
event.preventDefault();
|
|
const form = event.currentTarget;
|
|
const data = Object.fromEntries(new FormData(form).entries());
|
|
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 budgetRows = [];
|
|
let totalRev = 0, totalGross = 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;
|
|
budgetRows.push({ month: m, rev, gross, payment, cost });
|
|
totalRev += rev;
|
|
totalGross += gross;
|
|
}
|
|
data.budget_data = JSON.stringify(budgetRows);
|
|
data.total_rev = totalRev;
|
|
data.total_gross = totalGross;
|
|
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");
|
|
}
|
|
};
|