Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf08b2d241 | ||
|
|
4911f24d40 | ||
|
|
8c24abd53e | ||
|
|
0a7f70757d | ||
|
|
29dc7e040e | ||
|
|
c8387011cc | ||
|
|
5061de70f8 | ||
|
|
ea3ba25da5 | ||
|
|
bd7125fab8 | ||
|
|
94dd1fe677 | ||
|
|
fa6c9b1711 |
@@ -319,26 +319,29 @@ 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 = []
|
||||||
for month in months:
|
for month in months:
|
||||||
def s(cat):
|
col_month = month.replace("-", "_")
|
||||||
return one(conn, "SELECT COALESCE(SUM(amount),0) AS v FROM finance_records WHERE month=? AND category=? AND tenant=?", (month, cat, tenant))["v"]
|
col_rev = f"rev_{col_month}"
|
||||||
revenue = s("确认收入") + s("签单")
|
col_gross = f"gross_{col_month}"
|
||||||
labor = s("人力成本")
|
# Only project_finances has columns for 2026-06 through 2026-09
|
||||||
expense = s("费用")
|
if month in ["2026-06", "2026-07", "2026-08", "2026-09"]:
|
||||||
purchase = s("外部采购")
|
revenue = one(conn, f"SELECT COALESCE(SUM({col_rev}),0) AS v FROM project_finances WHERE tenant=?", (tenant,))["v"]
|
||||||
net = revenue - labor - expense - purchase
|
gross = one(conn, f"SELECT COALESCE(SUM({col_gross}),0) AS v FROM project_finances WHERE tenant=?", (tenant,))["v"]
|
||||||
|
else:
|
||||||
|
revenue = 0
|
||||||
|
gross = 0
|
||||||
data.append({
|
data.append({
|
||||||
"month": month, "revenue": revenue,
|
"month": month, "revenue": revenue,
|
||||||
"labor": labor, "expense": expense, "purchase": purchase,
|
"labor": 0, "expense": 0, "purchase": 0,
|
||||||
"net_profit": net,
|
"net_profit": gross,
|
||||||
})
|
})
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@@ -361,24 +364,17 @@ def bootstrap():
|
|||||||
products = attach_common(conn, "products", q("SELECT * FROM product_versions WHERE tenant=? ORDER BY id DESC", tenant))
|
products = attach_common(conn, "products", q("SELECT * FROM product_versions WHERE tenant=? ORDER BY id DESC", tenant))
|
||||||
finance = q("SELECT * FROM finance_records WHERE tenant=? ORDER BY month DESC, id DESC", tenant)
|
finance = q("SELECT * FROM finance_records WHERE tenant=? ORDER BY month DESC, id DESC", tenant)
|
||||||
tasks = q("SELECT * FROM project_tasks WHERE tenant=? ORDER BY phase, sort_order, id", tenant)
|
tasks = q("SELECT * FROM project_tasks WHERE tenant=? ORDER BY phase, sort_order, id", tenant)
|
||||||
|
pfs = q("SELECT * FROM project_finances WHERE tenant=? ORDER BY id DESC", tenant)
|
||||||
current_month = "2026-06"
|
current_month = "2026-06"
|
||||||
# Finance aggregates
|
# Finance aggregates — from project_finances (project-based)
|
||||||
def sum_cat(months, cat):
|
def pf_sum(field):
|
||||||
return sum(x["amount"] for x in finance if x["month"] in months and x.get("category") == cat)
|
return sum(x[field] or 0 for x in pfs)
|
||||||
months_2026 = [f"2026-{m:02d}" for m in range(1,7)]
|
rev_month = pf_sum("rev_2026_06")
|
||||||
months_q2 = ["2026-04","2026-05","2026-06"]
|
gross_month = pf_sum("gross_2026_06")
|
||||||
rev_annual = sum_cat(months_2026, "确认收入") + sum_cat(months_2026, "签单")
|
rev_q2 = pf_sum("rev_2026_06")
|
||||||
labor_annual = sum_cat(months_2026, "人力成本")
|
gross_q2 = pf_sum("gross_2026_06")
|
||||||
expense_annual = sum_cat(months_2026, "费用")
|
rev_annual = rev_q2
|
||||||
purchase_annual = sum_cat(months_2026, "外部采购")
|
gross_annual = gross_q2
|
||||||
rev_q2 = sum_cat(months_q2, "确认收入") + sum_cat(months_q2, "签单")
|
|
||||||
labor_q2 = sum_cat(months_q2, "人力成本")
|
|
||||||
expense_q2 = sum_cat(months_q2, "费用")
|
|
||||||
purchase_q2 = sum_cat(months_q2, "外部采购")
|
|
||||||
rev_month = sum_cat([current_month], "确认收入") + sum_cat([current_month], "签单")
|
|
||||||
labor_month = sum_cat([current_month], "人力成本")
|
|
||||||
expense_month = sum_cat([current_month], "费用")
|
|
||||||
purchase_month = sum_cat([current_month], "外部采购")
|
|
||||||
# Contract aggregates — time-based
|
# Contract aggregates — time-based
|
||||||
signed_amount = sum(x["expected_contract_amount"] or 0 for x in operations if x["project_status"] == "已签约")
|
signed_amount = sum(x["expected_contract_amount"] or 0 for x in operations if x["project_status"] == "已签约")
|
||||||
from datetime import date
|
from datetime import date
|
||||||
@@ -402,8 +398,8 @@ def bootstrap():
|
|||||||
"execution_projects": len([x for x in operations if x["project_type"] == "execution"]),
|
"execution_projects": len([x for x in operations if x["project_type"] == "execution"]),
|
||||||
"risk_projects": len([x for x in operations if x["project_status"] == "有风险" or x["risks"]]),
|
"risk_projects": len([x for x in operations if x["project_status"] == "有风险" or x["risks"]]),
|
||||||
"monthly_revenue": rev_month,
|
"monthly_revenue": rev_month,
|
||||||
"monthly_net_profit": rev_month - labor_month - expense_month - purchase_month,
|
"monthly_net_profit": gross_month,
|
||||||
"monthly_gross": rev_month - labor_month - expense_month - purchase_month,
|
"monthly_gross": gross_month,
|
||||||
"upcoming_products": len([x for x in products if x["status"] in ["规划中", "设计中", "开发中", "测试中"]]),
|
"upcoming_products": len([x for x in products if x["status"] in ["规划中", "设计中", "开发中", "测试中"]]),
|
||||||
"total_projects": len(operations),
|
"total_projects": len(operations),
|
||||||
"total_proposals": len(proposals),
|
"total_proposals": len(proposals),
|
||||||
@@ -416,14 +412,14 @@ def bootstrap():
|
|||||||
"pipeline_amount": pipeline_amount,
|
"pipeline_amount": pipeline_amount,
|
||||||
"revenue_annual": rev_annual,
|
"revenue_annual": rev_annual,
|
||||||
"revenue_q2": rev_q2,
|
"revenue_q2": rev_q2,
|
||||||
"gross_annual": rev_annual - labor_annual - expense_annual - purchase_annual,
|
"gross_annual": gross_annual,
|
||||||
"gross_q2": rev_q2 - labor_q2 - expense_q2 - purchase_q2,
|
"gross_q2": gross_q2,
|
||||||
"signed_not_executed": signed_not_executed,
|
"signed_not_executed": signed_not_executed,
|
||||||
},
|
},
|
||||||
"recent": q("SELECT * FROM follow_up_records WHERE tenant=? ORDER BY id DESC LIMIT 8", tenant),
|
"recent": q("SELECT * FROM follow_up_records WHERE tenant=? ORDER BY id DESC LIMIT 8", tenant),
|
||||||
"risks": [{"title": "执行提醒", "content": x["next_action"]} for x in operations if x["next_action"]][:5],
|
"risks": [{"title": "执行提醒", "content": x["next_action"]} for x in operations if x["next_action"]][:5],
|
||||||
}
|
}
|
||||||
return jsonify({"summary": summary, "sales": sales, "proposals": proposals, "operations": operations, "products": products, "finance": finance, "financeMonthly": monthly_finance(conn, tenant), "tasks": tasks, "tenant": tenant, "tenants": ["科普·无界","科研·无界","医患·无界"]})
|
return jsonify({"summary": summary, "sales": sales, "proposals": proposals, "operations": operations, "products": products, "finance": finance, "projectFinances": pfs, "financeMonthly": monthly_finance(conn, tenant), "tasks": tasks, "tenant": tenant, "tenants": ["科普·无界","科研·无界","医患·无界"]})
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
@@ -435,6 +431,7 @@ TABLES = {
|
|||||||
"products": ("product_versions", ["product_name", "version", "version_goal", "feature_list", "launch_date", "status", "platform", "notes", "tenant"]),
|
"products": ("product_versions", ["product_name", "version", "version_goal", "feature_list", "launch_date", "status", "platform", "notes", "tenant"]),
|
||||||
"finance": ("finance_records", ["month", "project_name", "record_type", "category", "amount", "occurred_date", "notes", "tenant"]),
|
"finance": ("finance_records", ["month", "project_name", "record_type", "category", "amount", "occurred_date", "notes", "tenant"]),
|
||||||
"tasks": ("project_tasks", ["project_id", "phase", "milestone", "task", "owner", "due_date", "blockers", "notes", "status", "sort_order", "tenant"]),
|
"tasks": ("project_tasks", ["project_id", "phase", "milestone", "task", "owner", "due_date", "blockers", "notes", "status", "sort_order", "tenant"]),
|
||||||
|
"projectFinances": ("project_finances", ["project_id", "tenant", "business_type", "customer_name", "sign_amount", "sign_month", "status", "sales_person", "rev_2026_06", "rev_2026_07", "rev_2026_08", "rev_2026_09", "gross_2026_06", "gross_2026_07", "gross_2026_08", "gross_2026_09"]),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
109
static/app.js
109
static/app.js
@@ -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,
|
||||||
@@ -232,17 +233,30 @@ window.submitTaskForm = async (event, projectId) => {
|
|||||||
};
|
};
|
||||||
window.createFinance = async (event) => {
|
window.createFinance = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const data = Object.fromEntries(new FormData(event.currentTarget).entries());
|
const form = event.currentTarget;
|
||||||
// Map form: month(date)→month(YYYY-MM), category→category, amount, tenant
|
const data = Object.fromEntries(new FormData(form).entries());
|
||||||
data.month = data.month.substring(0, 7);
|
|
||||||
data.project_name = state.tenant;
|
|
||||||
data.tenant = state.tenant;
|
data.tenant = state.tenant;
|
||||||
data.record_type = "revenue"; // default, will be adjusted by category
|
data.sign_amount = parseFloat(data.sign_amount) || 0;
|
||||||
|
for (const m of ["2026-06","2026-07","2026-08","2026-09"]) {
|
||||||
|
const k = m.replace("-","_");
|
||||||
|
data["rev_"+k] = parseFloat(data["rev_"+k]) || 0;
|
||||||
|
data["gross_"+k] = parseFloat(data["gross_"+k]) || 0;
|
||||||
|
}
|
||||||
|
const pfId = data.pf_id;
|
||||||
|
delete data.pf_id;
|
||||||
try {
|
try {
|
||||||
await api("/api/finance", { method: "POST", body: JSON.stringify({ data }) });
|
if (pfId) {
|
||||||
|
await api("/api/projectFinances/" + pfId, { method: "PUT", body: JSON.stringify({ data }) });
|
||||||
|
} else {
|
||||||
|
await api("/api/projectFinances", { method: "POST", body: JSON.stringify({ data }) });
|
||||||
|
}
|
||||||
|
form.reset();
|
||||||
|
document.querySelector("#pf-id-input").value = "";
|
||||||
|
document.querySelector("#financeModalTitle").textContent = "新增项目财务";
|
||||||
|
closeFinanceModal();
|
||||||
await load();
|
await load();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert("新增失败:" + error.message);
|
alert("保存失败:" + error.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.switchTab = switchTab;
|
window.switchTab = switchTab;
|
||||||
@@ -421,19 +435,75 @@ function renderProducts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderFinance() {
|
function renderFinance() {
|
||||||
const rows = state.data.finance.map((x) => [x.month, x.category, money(x.amount), text(x.notes)]);
|
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 months = ["2026-06","2026-07","2026-08","2026-09"];
|
||||||
|
const monthLabels = ["6月","7月","8月","9月"];
|
||||||
|
|
||||||
|
// Aggregates
|
||||||
|
const signed = pfs.filter(x => x.status === "已签单");
|
||||||
|
const pending = pfs.filter(x => x.status !== "已签单");
|
||||||
|
const sumSign = signed.reduce((s,x) => s + (x.sign_amount||0), 0);
|
||||||
|
const sumPending = pending.reduce((s,x) => s + (x.sign_amount||0), 0);
|
||||||
|
const monthRev = months.map(m => pfs.reduce((s,x) => s + (x["rev_"+m.replace("-","_")]||0), 0));
|
||||||
|
const monthGross = months.map(m => pfs.reduce((s,x) => s + (x["gross_"+m.replace("-","_")]||0), 0));
|
||||||
|
|
||||||
|
const renderPfRow = (pf) => {
|
||||||
|
const mCols = months.map(m => {
|
||||||
|
const rev = pf["rev_"+m.replace("-","_")] || 0;
|
||||||
|
const gross = pf["gross_"+m.replace("-","_")] || 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>`;
|
||||||
|
}).join("");
|
||||||
|
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">${pf.customer_name}</td><td class="p-2 text-sm">${pf.business_type}</td><td class="p-2 text-sm">${pf.status === "已签单" ? badge("已签") : badge(pf.status,"amber")}</td><td class="p-2 text-right text-sm">${money(pf.sign_amount)}</td>${mCols}<td class="p-2 text-sm text-slate-500">${pf.sales_person || ""}</td></tr>`;
|
||||||
|
};
|
||||||
|
|
||||||
document.querySelector("#finance").innerHTML = `<div class="grid gap-4">
|
document.querySelector("#finance").innerHTML = `<div class="grid gap-4">
|
||||||
${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")}
|
<div class="grid grid-cols-6 gap-3">
|
||||||
${card(formHtml([
|
${[["已签项目","" + 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("")}
|
||||||
{ label: "日期", input: `<input name="month" type="date" required>` },
|
</div>
|
||||||
{ label: "类型", input: `<select name="category"><option>签单</option><option>确认收入</option><option>人力成本</option><option>费用</option><option>外部采购</option></select>` },
|
${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")}
|
||||||
{ label: "金额/万", input: `<input name="amount" type="number" step="0.01" required>` },
|
|
||||||
{ label: "费用说明", input: `<input name="notes" placeholder="摘要说明">` },
|
<div class="flex justify-end"><button class="btn btn-primary btn-sm" onclick="openFinanceModal()"><i data-lucide="plus"></i>新增财务项目</button></div>
|
||||||
], { handler: "createFinance", text: "新增明细" }), "p-4")}
|
<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><button class="btn btn-ghost btn-sm rounded-full w-8 h-8 p-0" onclick="closeFinanceModal()"><i data-lucide="x"></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 class="grid grid-cols-2 gap-6"><div class="bg-slate-50 rounded-xl p-5"><h4 class="text-sm font-semibold text-slate-500 mb-4 flex items-center gap-2"><i data-lucide="briefcase-business"></i>基本信息</h4><div class="grid gap-4"><label class="block"><span class="text-xs font-medium text-slate-500">项目</span><select name="project_id" class="mt-1 block w-full rounded-lg border border-slate-200 px-3 py-2.5 text-sm bg-white">${tenantOps.map(o => `<option value="${o.id}">${o.project_name}</option>`).join("")}</select></label><label class="block"><span class="text-xs font-medium text-slate-500">业务类型</span><select name="business_type" class="mt-1 block w-full rounded-lg border border-slate-200 px-3 py-2.5 text-sm bg-white">${fmTypes.map(t => `<option>${t}</option>`).join("")}</select></label><label class="block"><span class="text-xs font-medium text-slate-500">客户名称</span><input name="customer_name" class="mt-1 block w-full rounded-lg border border-slate-200 px-3 py-2.5 text-sm"></label></div></div><div class="bg-slate-50 rounded-xl p-5"><h4 class="text-sm font-semibold text-slate-500 mb-4 flex items-center gap-2"><i data-lucide="banknote"></i>签约信息</h4><div class="grid gap-4"><label class="block"><span class="text-xs font-medium text-slate-500">签约金额(万元)</span><input name="sign_amount" type="number" step="0.01" class="mt-1 block w-full rounded-lg border border-slate-200 px-3 py-2.5 text-sm" placeholder="0.00"></label><label class="block"><span class="text-xs font-medium text-slate-500">签约月份</span><input name="sign_month" class="mt-1 block w-full rounded-lg border border-slate-200 px-3 py-2.5 text-sm" placeholder="如 2026-06"></label><label class="block"><span class="text-xs font-medium text-slate-500">项目状态</span><select name="status" class="mt-1 block w-full rounded-lg border border-slate-200 px-3 py-2.5 text-sm bg-white"><option>已签单</option><option>待签</option></select></label></div></div></div><div class="bg-slate-50 rounded-xl p-5"><h4 class="text-sm font-semibold text-slate-500 mb-4 flex items-center gap-2"><i data-lucide="calendar"></i>月度确收 & 毛利预估(万元)</h4><div class="grid grid-cols-4 gap-3">${["06","07","08","09"].map(m => `<div class="bg-white rounded-lg border border-slate-200 p-3 text-center"><p class="text-xs font-semibold text-slate-400 mb-2">${m}月</p><div class="grid grid-cols-2 gap-2"><div><p class="text-[10px] text-slate-400 mb-0.5">确收</p><input name="rev_2026_${m}" type="number" step="0.01" class="w-full rounded-md border border-slate-200 px-2 py-1.5 text-sm text-center" placeholder="—"></div><div><p class="text-[10px] text-slate-400 mb-0.5">毛利</p><input name="gross_2026_${m}" type="number" step="0.01" class="w-full rounded-md border border-slate-200 px-2 py-1.5 text-sm text-center" placeholder="—"></div></div></div>`).join("")}</div></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(`<h3 class="font-bold text-slate-700 mb-3">明细列表 <span class="text-slate-400 font-normal">(${state.data.finance.length})</span></h3>${renderTable(["日期", "类型", "金额", "备注"], rows)}`, "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="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.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 = "编辑项目财务";
|
||||||
|
const form = document.querySelector("#financeModal form");
|
||||||
|
form.querySelector('[name="project_id"]').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 || "";
|
||||||
|
form.querySelector('[name="sign_month"]').value = pf.sign_month || "";
|
||||||
|
form.querySelector('[name="status"]').value = pf.status || "已签单";
|
||||||
|
for (const m of ["06","07","08","09"]) {
|
||||||
|
form.querySelector('[name="rev_2026_' + m + '"]').value = pf["rev_2026_" + m] || "";
|
||||||
|
form.querySelector('[name="gross_2026_' + m + '"]').value = pf["gross_2026_" + m] || "";
|
||||||
|
}
|
||||||
|
openFinanceModal();
|
||||||
|
};
|
||||||
window.toggleFinanceChart = () => {
|
window.toggleFinanceChart = () => {
|
||||||
const wrap = document.querySelector("#financeChartWrap");
|
const wrap = document.querySelector("#financeChartWrap");
|
||||||
const icon = document.querySelector("#financeChartIcon");
|
const icon = document.querySelector("#financeChartIcon");
|
||||||
@@ -452,11 +522,8 @@ function renderFinanceChart() {
|
|||||||
data: {
|
data: {
|
||||||
labels: financeMonthly.map((x) => x.month),
|
labels: financeMonthly.map((x) => x.month),
|
||||||
datasets: [
|
datasets: [
|
||||||
{ label: "确认收入", data: financeMonthly.map((x) => x.revenue), borderColor: "#2563eb", tension: 0.3 },
|
{ label: "月度确收", data: financeMonthly.map((x) => x.revenue), borderColor: "#2563eb", tension: 0.3 },
|
||||||
{ label: "人力成本", data: financeMonthly.map((x) => x.labor), borderColor: "#ef4444", tension: 0.3 },
|
{ label: "月度毛利", data: financeMonthly.map((x) => x.net_profit), borderColor: "#059669", tension: 0.3 },
|
||||||
{ label: "费用", data: financeMonthly.map((x) => x.expense), borderColor: "#d97706", tension: 0.3 },
|
|
||||||
{ label: "外部采购", data: financeMonthly.map((x) => x.purchase), borderColor: "#8b5cf6", tension: 0.3 },
|
|
||||||
{ label: "月度净利", data: financeMonthly.map((x) => x.net_profit), borderColor: "#059669", tension: 0.3, borderDash: [5,3] },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: "bottom", labels: { boxWidth: 12, font: { size: 11 } } } }, scales: { x: { ticks: { font: { size: 11 } }, grid: { display: false } }, y: { ticks: { font: { size: 11 }, callback: (v) => v + "万" } } } },
|
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: "bottom", labels: { boxWidth: 12, font: { size: 11 } } } }, scales: { x: { ticks: { font: { size: 11 } }, grid: { display: false } }, y: { ticks: { font: { size: 11 }, callback: (v) => v + "万" } } } },
|
||||||
|
|||||||
Reference in New Issue
Block a user