diff --git a/backend/routes.py b/backend/routes.py index 1acf410..89b0ba2 100644 --- a/backend/routes.py +++ b/backend/routes.py @@ -257,6 +257,7 @@ def index(): @bp.route("/api/bootstrap") def bootstrap(): + """轻量级 bootstrap:只返回 summary + financeMonthly(首页用),其他模块按需加载""" if "user_id" not in session: return jsonify({"error": "未登录"}), 401 _year = date.today().year @@ -270,154 +271,20 @@ def bootstrap(): tenant = allowed[0] conn = db() try: - # 总工作台:聚合所有工作台的首页数据 if tenant == "总览": real_tenants = [t for t in allowed if t != "总览"] all_metrics = [] all_monthly = [] all_recent = [] for t in real_tenants: - t_pfs = rows(conn, "SELECT * FROM project_finances WHERE tenant=? ORDER BY id DESC", [t]) t_ops = attach_common(conn, "operations", rows(conn, "SELECT * FROM operation_projects WHERE tenant=? ORDER BY id ASC", [t])) t_sales = attach_common(conn, "sales", rows(conn, "SELECT * FROM sales_leads WHERE tenant=? ORDER BY id DESC", [t])) t_products = attach_common(conn, "products", rows(conn, "SELECT * FROM product_versions WHERE tenant=? ORDER BY id DESC", [t])) - t_expense = rows(conn, "SELECT * FROM expense_records WHERE tenant=?", [t]) t_proposals = attach_common(conn, "proposals", rows(conn, "SELECT * FROM business_proposals WHERE tenant=? ORDER BY id DESC", [t])) - t_signed_pfs = t_pfs - def t_parse_budget(pf): - try: - budget = json.loads(pf.get("budget_data") or "[]") - except (json.JSONDecodeError, TypeError): - budget = [] - return {(b.get("month") or "").replace("-", "_"): b for b in budget} - t_bm = {pf["id"]: t_parse_budget(pf) for pf in t_pfs} - def t_sum_budget(field, months_range): - total = 0 - for pf in t_pfs: - bm = t_bm.get(pf["id"], {}) - for m in months_range: - b = bm.get(f"{_year}_{m:02d}") - if b: - total += float(b.get(field) or 0) - return total - - def t_parse_expense(pf): - try: - ed = json.loads(pf.get("expense_data") or "[]") - except (json.JSONDecodeError, TypeError): - ed = [] - return {(e.get("month") or "").replace("-", "_"): e for e in ed} - - t_em = {pf["id"]: t_parse_expense(pf) for pf in t_pfs} - - def t_sum_expense(field, months_range): - total = 0 - for pf in t_pfs: - em = t_em.get(pf["id"], {}) - for m in months_range: - e = em.get(f"{_year}_{m:02d}") - if e: - total += float(e.get(field) or 0) - return total - - _now_month = date.today().month - _q_start = ((_now_month - 1) // 3) * 3 + 1 - _q_range = range(_q_start, _q_start + 3) - _q_months = [f"{_year}-{m:02d}" for m in _q_range] - _prev_q_start = ((_now_month - 4) // 3) * 3 + 1 - _prev_q_range = range(max(_prev_q_start, 1), _prev_q_start + 3) - _prev_q_months = [f"{_year}-{m:02d}" for m in _prev_q_range] - _prev_month = _now_month - 1 if _now_month > 1 else None - all_metrics.append({ - "total_projects": len(t_signed_pfs), - "total_proposals": len(t_ops), - "total_products": len(t_proposals), - "upcoming_products": len(t_products), - "signed_amount": sum(x["sign_amount"] or 0 for x in t_pfs), - "signed_annual": sum(x["sign_amount"] or 0 for x in t_pfs), - "signed_q2": sum(x["sign_amount"] or 0 for x in t_pfs if (x.get("sign_month") or "")[:7] in _q_months), - "signed_month": sum(x["sign_amount"] or 0 for x in t_pfs if (x.get("sign_month") or "")[:7] == f"{_year}-{_now_month:02d}"), - "signed_prev_q": sum(x["sign_amount"] or 0 for x in t_pfs if (x.get("sign_month") or "")[:7] in _prev_q_months), - "signed_prev_month": sum(x["sign_amount"] or 0 for x in t_pfs if (x.get("sign_month") or "")[:7] == f"{_year}-{_prev_month:02d}") if _prev_month else 0, - "revenue_annual": t_sum_budget("rev", range(1, 13)), - "revenue_q2": t_sum_budget("rev", _q_range), - "monthly_revenue": t_sum_budget("rev", [_now_month]), - "revenue_prev_q": t_sum_budget("rev", _prev_q_range), - "revenue_prev_month": t_sum_budget("rev", [_prev_month]) if _prev_month else 0, - "gross_annual": t_sum_budget("gross", range(1, 13)), - "gross_q2": t_sum_budget("gross", _q_range), - "monthly_net_profit": t_sum_budget("gross", [_now_month]), - "gross_prev_q": t_sum_budget("gross", _prev_q_range), - "gross_prev_month": t_sum_budget("gross", [_prev_month]) if _prev_month else 0, - "payment_annual": t_sum_budget("payment", range(1, 13)), - "payment_q2": t_sum_budget("payment", _q_range), - "payment_month": t_sum_budget("payment", [_now_month]), - "payment_prev_q": t_sum_budget("payment", _prev_q_range), - "payment_prev_month": t_sum_budget("payment", [_prev_month]) if _prev_month else 0, - "cost_annual": t_sum_expense("cost", range(1, 13)), - "cost_q2": t_sum_expense("cost", _q_range), - "cost_month": t_sum_expense("cost", [_now_month]), - "cost_prev_q": t_sum_expense("cost", _prev_q_range), - "cost_prev_month": t_sum_expense("cost", [_prev_month]) if _prev_month else 0, - }) - # 费用汇总(从 expense_records 表计算) - def t_expense_sum(m_range): - total = 0 - for r in t_expense: - if (r.get("status") or "") == "暂不计入": continue - m = (r.get("expense_month") or "").strip() - if not m or "-" not in m: continue - try: - if int(m.split("-")[1]) in m_range: - total += float(r.get("amount") or 0) - except: pass - return total - all_metrics[-1]["expense_annual"] = t_expense_sum(range(1, 13)) - all_metrics[-1]["expense_q2"] = t_expense_sum(_q_range) - all_metrics[-1]["expense_month"] = t_expense_sum([_now_month]) - all_metrics[-1]["expense_prev_q"] = t_expense_sum(_prev_q_range) - all_metrics[-1]["expense_prev_month"] = t_expense_sum([_prev_month]) if _prev_month else 0 - all_metrics[-1]["paid_annual"] = t_sum_expense("paid", range(1, 13)) - all_metrics[-1]["paid_q2"] = t_sum_expense("paid", _q_range) - all_metrics[-1]["paid_month"] = t_sum_expense("paid", [_now_month]) - all_metrics[-1]["paid_prev_q"] = t_sum_expense("paid", _prev_q_range) - all_metrics[-1]["paid_prev_month"] = t_sum_expense("paid", [_prev_month]) if _prev_month else 0 - all_metrics[-1]["proj_expense_annual"] = t_sum_budget("expense", range(1, 13)) - all_metrics[-1]["proj_expense_q2"] = t_sum_budget("expense", _q_range) - all_metrics[-1]["proj_expense_month"] = t_sum_budget("expense", [_now_month]) - all_metrics[-1]["proj_expense_prev_q"] = t_sum_budget("expense", _prev_q_range) - all_metrics[-1]["proj_expense_prev_month"] = t_sum_budget("expense", [_prev_month]) if _prev_month else 0 - all_metrics[-1]["cashflow_annual"] = all_metrics[-1]["payment_annual"] - all_metrics[-1]["paid_annual"] - all_metrics[-1]["cashflow_q2"] = all_metrics[-1]["payment_q2"] - all_metrics[-1]["paid_q2"] - all_metrics[-1]["cashflow_month"] = all_metrics[-1]["payment_month"] - all_metrics[-1]["paid_month"] - all_metrics[-1]["cashflow_prev_q"] = all_metrics[-1]["payment_prev_q"] - all_metrics[-1]["paid_prev_q"] - all_metrics[-1]["cashflow_prev_month"] = all_metrics[-1]["payment_prev_month"] - all_metrics[-1]["paid_prev_month"] - def t_expense_paid_sum(m_range): - total = 0 - for r in t_expense: - if (r.get("status") or "") == "暂不计入": continue - m = (r.get("expense_month") or "").strip() - if not m or "-" not in m: continue - try: - if int(m.split("-")[1]) in m_range: - total += float(r.get("incurred_amount") or 0) - except: pass - return total - all_metrics[-1]["expense_paid_annual"] = t_expense_paid_sum(range(1, 13)) - all_metrics[-1]["expense_paid_q2"] = t_expense_paid_sum(_q_range) - all_metrics[-1]["expense_paid_month"] = t_expense_paid_sum([_now_month]) - all_metrics[-1]["expense_paid_prev_q"] = t_expense_paid_sum(_prev_q_range) - all_metrics[-1]["expense_paid_prev_month"] = t_expense_paid_sum([_prev_month]) if _prev_month else 0 - all_metrics[-1]["dept_cf_annual"] = all_metrics[-1]["cashflow_annual"] - all_metrics[-1]["expense_paid_annual"] - all_metrics[-1]["dept_cf_q2"] = all_metrics[-1]["cashflow_q2"] - all_metrics[-1]["expense_paid_q2"] - all_metrics[-1]["dept_cf_month"] = all_metrics[-1]["cashflow_month"] - all_metrics[-1]["expense_paid_month"] - all_metrics[-1]["dept_cf_prev_q"] = all_metrics[-1]["cashflow_prev_q"] - all_metrics[-1]["expense_paid_prev_q"] - all_metrics[-1]["dept_cf_prev_month"] = all_metrics[-1]["cashflow_prev_month"] - all_metrics[-1]["expense_paid_prev_month"] - all_metrics[-1]["profit_annual"] = all_metrics[-1]["gross_annual"] - all_metrics[-1]["proj_expense_annual"] - all_metrics[-1]["profit_q2"] = all_metrics[-1]["gross_q2"] - all_metrics[-1]["proj_expense_q2"] - all_metrics[-1]["profit_month"] = all_metrics[-1]["monthly_net_profit"] - all_metrics[-1]["proj_expense_month"] - all_metrics[-1]["profit_prev_q"] = all_metrics[-1]["gross_prev_q"] - all_metrics[-1]["proj_expense_prev_q"] - all_metrics[-1]["profit_prev_month"] = all_metrics[-1]["gross_prev_month"] - all_metrics[-1]["proj_expense_prev_month"] + t_pfs = rows(conn, "SELECT * FROM project_finances WHERE tenant=? ORDER BY id DESC", [t]) + t_expense = rows(conn, "SELECT * FROM expense_records WHERE tenant=? ORDER BY id DESC", [t]) + t_metrics = compute_metrics(t_pfs, t_ops, t_sales, t_products, t_proposals, t_expense, _year, conn, t) + all_metrics.append(t_metrics) all_monthly.append(monthly_finance(conn, t)) all_recent.extend(rows(conn, "SELECT * FROM follow_up_records WHERE tenant=? ORDER BY id DESC LIMIT 4", [t])) agg = {} @@ -435,20 +302,15 @@ def bootstrap(): "recent": sorted(all_recent, key=lambda x: x.get("id", 0), reverse=True)[:8], "risks": [], } - return jsonify({"summary": summary, "sales": [], "proposals": [], "operations": [], "products": [], "finance": [], "projectFinances": [], "financeMonthly": merged_monthly, "tasks": [], "expense": [], "planFinances": [], "planExpense": [], "tenant": tenant, "tenants": allowed}) + return jsonify({"summary": summary, "financeMonthly": merged_monthly, "tenant": tenant, "tenants": allowed}) - def q(sql, *args): - return rows(conn, sql, args) - sales = attach_common(conn, "sales", q("SELECT * FROM sales_leads WHERE tenant=? ORDER BY id DESC", tenant)) - proposals = attach_common(conn, "proposals", q("SELECT * FROM business_proposals WHERE tenant=? ORDER BY id DESC", tenant)) - operations = attach_common(conn, "operations", q("SELECT * FROM operation_projects WHERE tenant=? ORDER BY id ASC", 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) - 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) - expense = q("SELECT * FROM expense_records WHERE tenant=? ORDER BY id DESC", tenant) - planPfs = q("SELECT * FROM plan_finances WHERE tenant=? ORDER BY id DESC", tenant) - planExpense = q("SELECT * FROM plan_expense_records WHERE tenant=? ORDER BY id DESC", tenant) + # 普通工作台:只查 summary 所需的数据(project_finances + operations + sales + products + proposals + expense) + pfs = rows(conn, "SELECT * FROM project_finances WHERE tenant=? ORDER BY id DESC", [tenant]) + operations = attach_common(conn, "operations", rows(conn, "SELECT * FROM operation_projects WHERE tenant=? ORDER BY id ASC", [tenant])) + sales = attach_common(conn, "sales", rows(conn, "SELECT * FROM sales_leads WHERE tenant=? ORDER BY id DESC", [tenant])) + products = attach_common(conn, "products", rows(conn, "SELECT * FROM product_versions WHERE tenant=? ORDER BY id DESC", [tenant])) + proposals = attach_common(conn, "proposals", rows(conn, "SELECT * FROM business_proposals WHERE tenant=? ORDER BY id DESC", [tenant])) + expense = rows(conn, "SELECT * FROM expense_records WHERE tenant=? ORDER BY id DESC", [tenant]) signed_pfs = pfs def parse_budget(pf): @@ -581,7 +443,7 @@ def bootstrap(): pipeline_amount = sum(x["expected_contract_amount"] or 0 for x in operations if x["project_status"] not in ["已签约","已丢单","已归档","已完成"]) signed_not_executed = sum(x["expected_contract_amount"] or 0 for x in operations if x["project_type"] == "execution" and x["execution_progress"] < 100) summary = { - "project_name": "科普(慰心斋)", + "project_name": tenant, "metrics": { "p0_customers": len([x for x in sales if x["priority"] == "P0"]), "active_sales": len([x for x in sales if x["status"] in ["待跟进", "跟进中", "方案中", "商务谈判"]]), @@ -656,10 +518,10 @@ def bootstrap(): "proj_expense_prev_month": proj_expense_prev_month, "signed_not_executed": signed_not_executed, }, - "recent": q("SELECT * FROM follow_up_records WHERE tenant=? ORDER BY id DESC LIMIT 8", tenant), + "recent": rows(conn, "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], } - return jsonify({"summary": summary, "sales": sales, "proposals": proposals, "operations": operations, "products": products, "finance": finance, "projectFinances": pfs, "financeMonthly": monthly_finance(conn, tenant), "tasks": tasks, "expense": expense, "planFinances": planPfs, "planExpense": planExpense, "tenant": tenant, "tenants": allowed}) + return jsonify({"summary": summary, "financeMonthly": monthly_finance(conn, tenant), "tenant": tenant, "tenants": allowed}) finally: conn.close() diff --git a/static/modules/finance.js b/static/modules/finance.js index 2766e98..5d30463 100644 --- a/static/modules/finance.js +++ b/static/modules/finance.js @@ -110,11 +110,12 @@ function renderFinance() { const nowD = new Date(); const activeQ = state.finQuarter !== undefined ? state.finQuarter : Math.floor(nowD.getMonth() / 3); const activeMonths = qMonths[activeQ]; - const finHeaderBase = `
|按季度:${qLabels.map((q,i) => ``).join('')}|按月度:${activeMonths.map(m => { const ms = `2026-${String(m).padStart(2,'0')}`; return ``; }).join('')}
`; + const yearOpts = [2024,2025,2026,2027]; + const finHeaderBase = `
|年份:|季度:${qLabels.map((q,i) => ``).join('')}|月度:${activeMonths.map(m => { const ms = `${state.finYear}-${String(m).padStart(2,'0')}`; return ``; }).join('')}
`; const finAddBtn = ``; const isOverviewMode = state.tenant === "总览"; - const finFilterTabs = `
${isOverviewMode ? '' : ``}
`; + const finFilterTabs = `
${isOverviewMode ? '' : ``}
`; document.querySelector("#finance").innerHTML = `
${finFilterTabs} @@ -390,12 +391,12 @@ function renderFinance() { const qRange = qRanges[selQ]; 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) {} + try { JSON.parse(pf.budget_data || "[]").forEach(b => { const m = parseInt((b.month || "").substring(5)) || 0; const y = parseInt((b.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && y === state.finYear) total += parseFloat(b[field] || 0); }); } catch (e) {} return total; }; const sumExpense = (pf, field) => { let total = 0; - try { JSON.parse(pf.expense_data || "[]").forEach(e => { const m = parseInt((e.month || "").substring(5)) || 0; if (qRange.includes(m)) total += parseFloat(e[field] || 0); }); } catch (e) {} + try { JSON.parse(pf.expense_data || "[]").forEach(e => { const m = parseInt((e.month || "").substring(5)) || 0; const y = parseInt((e.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && y === state.finYear) total += parseFloat(e[field] || 0); }); } catch (e) {} return total; }; var dataItems = []; diff --git a/static/modules/plan.js b/static/modules/plan.js index f30e00e..37e693d 100644 --- a/static/modules/plan.js +++ b/static/modules/plan.js @@ -108,11 +108,12 @@ function renderPlan() { const nowD = new Date(); const activeQ = state.planQuarter !== undefined ? state.planQuarter : Math.floor(nowD.getMonth() / 3); const activeMonths = qMonths[activeQ]; - const finHeaderBase = `
|按季度:${qLabels.map((q,i) => ``).join('')}|按月度:${activeMonths.map(m => { const ms = `2026-${String(m).padStart(2,'0')}`; return ``; }).join('')}
`; + const yearOpts = [2024,2025,2026,2027]; + const finHeaderBase = `
|年份:|季度:${qLabels.map((q,i) => ``).join('')}|月度:${activeMonths.map(m => { const ms = `${state.planYear}-${String(m).padStart(2,'0')}`; return ``; }).join('')}
`; const finAddBtn = ``; const isOverviewMode = state.tenant === "总览"; - const planFilterTabs = `
${isOverviewMode ? '' : ``}
`; + const planFilterTabs = `
${isOverviewMode ? '' : ``}
`; document.querySelector("#plan").innerHTML = `
${planFilterTabs} @@ -290,6 +291,7 @@ function renderPlan() { return h; }; const SIGNED_COLS = [ + { key: '_expand', label: '' }, { key: 'client_name', label: '客户' }, { key: 'customer_name', label: '项目名称' }, { key: 'weight', label: '加权' }, @@ -345,7 +347,24 @@ function renderPlan() { const grossR = rev && gross ? Math.round(gross / rev * 100) + '%' : ''; const cf = cashflow ? money(cashflow) : ''; const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600'; - return `${esc(pf.client_name || "")}${esc(pf.customer_name)}${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}${money(pf.sign_amount)}${fmtNoUnit(rev)}${fmtNoUnit(gross)}${grossR}${fmtNoUnit(payment)}${payR}${fmtNoUnit(cost)}${fmtNoUnit(paid)}${paidR}${cf}`; + var budgetArr = []; + var expenseArr = []; + try { budgetArr = JSON.parse(pf.budget_data || "[]"); } catch(e) {} + try { expenseArr = JSON.parse(pf.expense_data || "[]"); } catch(e) {} + var detailRows = budgetArr.filter(function(b) { + var exp = expenseArr.find(function(e) { return e.month === (b.month || ''); }) || {}; + var bRev = parseFloat(b.rev || 0), bGross = parseFloat(b.gross || 0), bPay = parseFloat(b.payment || 0); + var eCost = parseFloat(exp.cost || 0), ePaid = parseFloat(exp.paid || 0); + return bRev || bGross || bPay || eCost || ePaid; + }).map(function(b) { + var m = b.month || ''; + var exp = expenseArr.find(function(e) { return e.month === m; }) || {}; + var bRev = parseFloat(b.rev || 0), bGross = parseFloat(b.gross || 0), bPay = parseFloat(b.payment || 0); + var eCost = parseFloat(exp.cost || 0), ePaid = parseFloat(exp.paid || 0); + var bCf = bPay - ePaid; + return '' + m + '' + (bRev ? money(bRev) : '—') + '' + (bGross ? money(bGross) : '—') + '' + (bPay ? money(bPay) : '—') + '' + (eCost ? money(eCost) : '—') + '' + (ePaid ? money(ePaid) : '—') + '' + (bCf ? money(bCf) : '—') + ''; + }).join(''); + return `${esc(pf.client_name || "")}${esc(pf.customer_name)}${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}${money(pf.sign_amount)}${fmtNoUnit(rev)}${fmtNoUnit(gross)}${grossR}${fmtNoUnit(payment)}${payR}${fmtNoUnit(cost)}${fmtNoUnit(paid)}${paidR}${cf}${detailRows}
月份确收毛利回款成本支出现金流
`; }; // 待签约简版行 const tdRowUnsigned = (pf, cost, gross) => { @@ -412,12 +431,12 @@ function renderPlan() { const qRange = qRanges[selQ]; 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) {} + try { JSON.parse(pf.budget_data || "[]").forEach(b => { const m = parseInt((b.month || "").substring(5)) || 0; const y = parseInt((b.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && y === state.planYear) total += parseFloat(b[field] || 0); }); } catch (e) {} return total; }; const sumExpense = (pf, field) => { let total = 0; - try { JSON.parse(pf.expense_data || "[]").forEach(e => { const m = parseInt((e.month || "").substring(5)) || 0; if (qRange.includes(m)) total += parseFloat(e[field] || 0); }); } catch (e) {} + try { JSON.parse(pf.expense_data || "[]").forEach(e => { const m = parseInt((e.month || "").substring(5)) || 0; const y = parseInt((e.month || "").substring(0, 4)) || 0; if (qRange.includes(m) && y === state.planYear) total += parseFloat(e[field] || 0); }); } catch (e) {} return total; }; let dataItems = []; @@ -781,6 +800,19 @@ window.planDeleteFollowup = async (event, followupId) => { if (pfId) await planLoadFollowups(pfId); }; +window.planToggleExpand = (pfId) => { + var row = document.getElementById('plan_expand_' + pfId); + var icon = document.getElementById('plan_expand_icon_' + pfId); + if (!row) return; + if (row.classList.contains('hidden')) { + row.classList.remove('hidden'); + if (icon) { icon.setAttribute('data-lucide', 'chevron-down'); if (window.lucide) window.lucide.createIcons(); } + } else { + row.classList.add('hidden'); + if (icon) { icon.setAttribute('data-lucide', 'chevron-right'); if (window.lucide) window.lucide.createIcons(); } + } +}; + window.planOpenPfEditModal = (pfId) => { const pf = (state.data.planFinances || []).find(x => x.id === pfId); if (!pf) return; diff --git a/static/modules/utils.js b/static/modules/utils.js index 8980c1b..c6659d3 100644 --- a/static/modules/utils.js +++ b/static/modules/utils.js @@ -12,6 +12,8 @@ const state = { finView: "overview", finSort: null, // { key: 'col', asc: true } planFilter: "projects", + planYear: new Date().getFullYear(), + finYear: new Date().getFullYear(), planView: "overview", planSort: null, proposalTab: "standard", @@ -167,8 +169,22 @@ function hideLoadingOverlay() { } async function load() { - showLoadingOverlay(_loadingSteps); + var allSteps = [{ label: '正在加载工作台数据(查询10+张表+计算财务汇总)' }].concat(_loadingSteps); + showLoadingOverlay(allSteps); state.data = await api(`/api/bootstrap?tenant=${encodeURIComponent(state.tenant)}`); + // 初始化空数据,按需加载 + if (!state.data.sales) state.data.sales = []; + if (!state.data.proposals) state.data.proposals = []; + if (!state.data.operations) state.data.operations = []; + if (!state.data.products) state.data.products = []; + if (!state.data.finance) state.data.finance = []; + if (!state.data.tasks) state.data.tasks = []; + if (!state.data.projectFinances) state.data.projectFinances = []; + if (!state.data.expense) state.data.expense = []; + if (!state.data.planFinances) state.data.planFinances = []; + if (!state.data.planExpense) state.data.planExpense = []; + clearTabCache(); + updateLoadingStep(0, allSteps.length); // 首次加载时确保标准资料库 7 项已初始化 if (typeof ensureStandardProposals === "function") { const existing = (state.data.proposals || []).filter(p => p.proposal_type === "标准资料"); @@ -183,19 +199,51 @@ async function load() { for (var i = 0; i < _loadingSteps.length; i++) { var step = _loadingSteps[i]; if (step.skip && step.skip()) { - updateLoadingStep(i, _loadingSteps.length); + updateLoadingStep(i + 1, allSteps.length); continue; } if (typeof window[step.fn] === 'function') { window[step.fn](); } - updateLoadingStep(i, _loadingSteps.length); + updateLoadingStep(i + 1, allSteps.length); await new Promise(function(r) { setTimeout(r, 50); }); } if (window.lucide) window.lucide.createIcons(); setTimeout(hideLoadingOverlay, 300); } +var _tabResourceMap = { + projects: 'operations', + proposals: 'proposals', + products: 'products', + finance: 'projectFinances', + plan: 'planFinances', +}; + +var _tabLoaded = {}; + +async function ensureTabData(tab) { + if (tab === 'home' || tab === 'performance') return; + var resource = _tabResourceMap[tab]; + if (!resource) return; + var dataKey = resource; + if (_tabLoaded[dataKey + '_' + state.tenant]) return; + _tabLoaded[dataKey + '_' + state.tenant] = true; + try { + state.data[dataKey] = await api("/api/" + resource + "/list?tenant=" + encodeURIComponent(state.tenant)); + } catch(e) { /* non-critical */ } + // Load related expense data for finance/plan tabs + if (tab === 'finance') { + try { state.data.expense = await api("/api/expense/list?tenant=" + encodeURIComponent(state.tenant)); } catch(e) {} + } else if (tab === 'plan') { + try { state.data.planExpense = await api("/api/planExpense/list?tenant=" + encodeURIComponent(state.tenant)); } catch(e) {} + } +} + +function clearTabCache() { + _tabLoaded = {}; +} + function switchTab(tab) { state.active = tab; localStorage.setItem("opc-active-tab", tab); @@ -203,11 +251,15 @@ function switchTab(tab) { document.querySelectorAll(".panel").forEach((panel) => panel.classList.toggle("active", panel.id === tab)); // 更新顶部标题 - const titles = { home: 'OPC 工作台', finance: '实际管理', plan: '计划管理', projects: '重点工作台账', proposals: '业务方案', products: '版本与运营', performance: '季度绩效考核' }; + const titles = { home: 'OPC 工作台', finance: '执行管理', plan: '预算管理', projects: '重点工作台账', proposals: '业务方案', products: '版本与运营', performance: '季度绩效考核' }; const titleEl = document.querySelector('#workspaceTitle'); if (titleEl) titleEl.textContent = titles[tab] || state.tenant + ' OPC 工作台'; - render(); + // 按需加载 tab 数据 + ensureTabData(tab).then(function() { + renderActive(); + if (window.lucide) window.lucide.createIcons(); + }); } function updateSidebarTabs() { @@ -273,6 +325,7 @@ window.setFinView = (view) => { }; window.setFinQuarter = (q) => { state.finQuarter = q; state.finView = 'quarterly'; renderFinance(); }; window.setFinMonth = (month) => { state.finMonth = month; state.finView = 'monthly'; renderFinance(); }; +window.setFinYear = (year) => { state.finYear = parseInt(year); renderFinance(); }; window.switchFinFilter = (filter) => { state.finFilter = filter; state.finSort = null; @@ -291,6 +344,7 @@ window.setPlanView = (view) => { }; window.setPlanQuarter = (q) => { state.planQuarter = q; state.planView = 'quarterly'; renderPlan(); }; window.setPlanMonth = (month) => { state.planMonth = month; state.planView = 'monthly'; renderPlan(); }; +window.setPlanYear = (year) => { state.planYear = parseInt(year); renderPlan(); }; window.switchPlanFilter = (filter) => { state.planFilter = filter; state.planSort = null; @@ -312,6 +366,15 @@ window.switchTenant = (tenant) => { if (tenant === "总览") { state.planFilter = 'overview'; state.finFilter = 'overview'; switchTab("home"); } load(); }; + +// 轻量刷新辅助函数 +async function refreshResource(resource, renderFn) { + try { + var dataKey = resource; + state.data[dataKey] = await api("/api/" + resource + "/list?tenant=" + encodeURIComponent(state.tenant)); + if (renderFn && typeof window[renderFn] === 'function') window[renderFn](); + } catch(e) { /* non-critical */ } +} window.doLogout = async () => { await api("/api/auth/logout", { method: "POST" }); location.href = "/login"; diff --git a/templates/index.html b/templates/index.html index 9b24a8e..6e1cec4 100644 --- a/templates/index.html +++ b/templates/index.html @@ -76,7 +76,7 @@
-

OPC Manager v1.2.0.3

+

OPC Manager v1.2.0.4

科普 OPC 工作台