Compare commits

...

3 Commits
v3.0 ... v3.0.3

2 changed files with 18 additions and 5 deletions

View File

@@ -319,11 +319,11 @@ def attach_common(conn, resource, items):
def monthly_finance(conn, tenant="科普·无界"): def monthly_finance(conn, tenant="科普·无界"):
from datetime import date from datetime import date
today = date.today() today = date.today()
# 12 months: 9 before + current + 2 after # 6 months: 3 before + current + 2 after
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
start = today + relativedelta(months=-9) start = today + relativedelta(months=-3)
months = [] months = []
for i in range(12): for i in range(6):
m = start + relativedelta(months=i) m = start + relativedelta(months=i)
months.append(m.strftime("%Y-%m")) months.append(m.strftime("%Y-%m"))
data = [] data = []

View File

@@ -3,6 +3,7 @@ const state = {
data: null, data: null,
tenant: "科普·无界", tenant: "科普·无界",
opFilter: "all", opFilter: "all",
finFilter: "已签单",
projectView: null, projectView: null,
chart: null, chart: null,
chart2: null, chart2: null,
@@ -452,11 +453,23 @@ function renderFinance() {
${[["已签项目","" + signed.length],["签约金额",money(sumSign)],["待签项目","" + pending.length],["待签金额",money(sumPending)],["本月确收",money(monthRev[0])],["本月毛利",money(monthGross[0])]].map(([l,v]) => `<div class="bg-white rounded-lg border border-slate-200 p-3 text-center"><p class="text-xs text-slate-500">${l}</p><p class="text-xl font-bold text-slate-800">${v}</p></div>`).join("")} ${[["已签项目","" + signed.length],["签约金额",money(sumSign)],["待签项目","" + pending.length],["待签金额",money(sumPending)],["本月确收",money(monthRev[0])],["本月毛利",money(monthGross[0])]].map(([l,v]) => `<div class="bg-white rounded-lg border border-slate-200 p-3 text-center"><p class="text-xs text-slate-500">${l}</p><p class="text-xl font-bold text-slate-800">${v}</p></div>`).join("")}
</div> </div>
${card(`<div class="flex items-center justify-between cursor-pointer" onclick="toggleFinanceChart()"><h2 class="text-lg font-bold text-slate-700">月度趋势</h2><i data-lucide="chevron-down" class="transition-transform rotate-90" id="financeChartIcon"></i></div><div id="financeChartWrap" class="hidden mt-4"><div style="position:relative;height:300px"><canvas id="financeChart2"></canvas></div></div>`, "p-5")} ${card(`<div class="flex items-center justify-between cursor-pointer" onclick="toggleFinanceChart()"><h2 class="text-lg font-bold text-slate-700">月度趋势</h2><i data-lucide="chevron-down" class="transition-transform rotate-90" id="financeChartIcon"></i></div><div id="financeChartWrap" class="hidden mt-4"><div style="position:relative;height:300px"><canvas id="financeChart2"></canvas></div></div>`, "p-5")}
${card(`<div class="flex items-center justify-between cursor-pointer" onclick="document.querySelector('#pf-form-wrap').classList.toggle('hidden')"><h3 class="font-bold text-slate-700">新增项目财务</h3><i data-lucide="plus-circle" class="text-blue-600"></i></div><div id="pf-form-wrap" class="hidden mt-3"><form onsubmit="createFinance(event)" class="grid gap-3"><div class="grid grid-cols-3 gap-3"><label class="text-xs text-slate-500">项目<select name="project_id" class="w-full rounded border px-2 py-1.5 text-sm">${ops.map(o => `<option value="${o.id}">${o.project_name}</option>`).join("")}</select></label><label class="text-xs text-slate-500">业务类型<select name="business_type" class="w-full rounded border px-2 py-1.5 text-sm">${["患者管理","创新支付","电商","MCN","科普视频","科普运营","医患运营","真实世界研究","其他"].map(t => `<option>${t}</option>`).join("")}</select></label><label class="text-xs text-slate-500">客户名称<input name="customer_name" class="w-full rounded border px-2 py-1.5 text-sm"></label><label class="text-xs text-slate-500">签约金额/万<input name="sign_amount" type="number" step="0.01" class="w-full rounded border px-2 py-1.5 text-sm"></label><label class="text-xs text-slate-500">签约月份<input name="sign_month" class="w-full rounded border px-2 py-1.5 text-sm"></label><label class="text-xs text-slate-500">状态<select name="status" class="w-full rounded border px-2 py-1.5 text-sm"><option>已签单</option><option>待签</option></select></label></div><div class="grid grid-cols-8 gap-2">${["06","07","08","09"].map(m => `<div class="border rounded p-2"><p class="text-xs text-slate-400 mb-1">${m}月</p><label class="text-xs text-slate-500">确收<br><input name="rev_2026_${m}" type="number" step="0.01" class="w-full rounded border px-1 py-1 text-sm" placeholder="万"></label><label class="text-xs text-slate-500 mt-1">毛利<br><input name="gross_2026_${m}" type="number" step="0.01" class="w-full rounded border px-1 py-1 text-sm" placeholder="万"></label></div>`).join("")}</div><button type="submit" class="btn btn-primary btn-sm justify-self-end">新增项目财务</button></form></div>`, "p-4")}
${card(`<h3 class="font-bold text-slate-700 mb-3">项目明细 <span class="text-slate-400 font-normal">(${pfs.length})</span></h3><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-left font-semibold">客户</th><th class="p-2 text-left font-semibold">类型</th><th class="p-2 text-left font-semibold">状态</th><th class="p-2 text-right font-semibold">签约金额</th>${monthLabels.map(l => `<th class="p-2 text-center font-semibold">${l}<br><span class="text-xs text-slate-400">确收/毛利</span></th>`).join("")}<th class="p-2 text-left font-semibold">销售</th></tr></thead><tbody>${pfs.map(renderPfRow).join("")}</tbody></table></div>`, "p-4")} <div class="flex justify-end"><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/30" onclick="closeFinanceModal()"><div class="bg-white rounded-xl shadow-xl w-full max-w-3xl mx-4 max-h-[90vh] overflow-y-auto" onclick="event.stopPropagation()"><div class="sticky top-0 bg-white border-b px-6 py-4 flex items-center justify-between"><h3 class="font-bold text-lg">新增项目财务</h3><button class="btn btn-ghost btn-sm" onclick="closeFinanceModal()"><i data-lucide="x"></i></button></div><form onsubmit="createFinance(event)" class="p-6 grid gap-3"><div class="grid grid-cols-3 gap-3"><label class="text-xs">项目<select name="project_id" class="rounded border px-2 py-1.5 text-sm w-full">${ops.map(o => `<option value="${o.id}">${o.project_name}</option>`).join("")}</select></label><label class="text-xs">业务类型<select name="business_type" class="rounded border px-2 py-1.5 text-sm w-full">${["患者管理","创新支付","电商","MCN","科普视频","科普运营","医患运营","真实世界研究","其他"].map(t => `<option>${t}</option>`).join("")}</select></label><label class="text-xs">客户名称<input name="customer_name" class="rounded border px-2 py-1.5 text-sm w-full"></label><label class="text-xs">签约金额/万<input name="sign_amount" type="number" step="0.01" class="rounded border px-2 py-1.5 text-sm w-full"></label><label class="text-xs">签约月份<input name="sign_month" class="rounded border px-2 py-1.5 text-sm w-full"></label><label class="text-xs">状态<select name="status" class="rounded border px-2 py-1.5 text-sm w-full"><option>已签单</option><option>待签</option></select></label></div><div class="grid grid-cols-4 gap-2">${["06","07","08","09"].map(m => `<div class="border rounded p-2"><p class="text-xs text-slate-400 mb-1">${m}月</p><label class="text-xs text-slate-500">确收<br><input name="rev_2026_${m}" type="number" step="0.01" class="w-full rounded border px-1 py-1 text-sm" placeholder="万"></label><label class="text-xs text-slate-500 mt-1">毛利<br><input name="gross_2026_${m}" type="number" step="0.01" class="w-full rounded border px-1 py-1 text-sm" placeholder="万"></label></div>`).join("")}</div><div class="flex justify-end gap-2 pt-3 border-t"><button type="button" class="btn btn-ghost btn-sm" onclick="closeFinanceModal()">取消</button><button type="submit" class="btn btn-primary btn-sm">确认新增</button></div></form></div></div>
${card(`<h3 class="font-bold text-slate-700 mb-3">项目明细 <span class="text-slate-400 font-normal">(${pfs.length})</span></h3><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-left font-semibold">客户</th><th class="p-2 text-left font-semibold">类型</th><th class="p-2 text-left font-semibold">状态</th><th class="p-2 text-right font-semibold">签约金额</th>${monthLabels.map(l => `<th class="p-2 text-center font-semibold">${l}<br><span class="text-xs text-slate-400">确收/毛利</span></th>`).join("")}<th class="p-2 text-left font-semibold">销售</th></tr></thead><tbody>${pfs.filter(x => x.status === state.finFilter).map(renderPfRow).join("")}</tbody></table></div>`, "p-4")}
</div>`; </div>`;
if (window.lucide) window.lucide.createIcons(); if (window.lucide) window.lucide.createIcons();
} }
window.openFinanceModal = () => {
const modal = document.querySelector("#financeModal");
modal.classList.remove("hidden");
};
window.closeFinanceModal = () => {
const modal = document.querySelector("#financeModal");
modal.classList.add("hidden");
};
window.toggleFinanceChart = () => { window.toggleFinanceChart = () => {
const wrap = document.querySelector("#financeChartWrap"); const wrap = document.querySelector("#financeChartWrap");
const icon = document.querySelector("#financeChartIcon"); const icon = document.querySelector("#financeChartIcon");