feat: 按需加载+年份筛选+行展开明细+UI优化
后端: - bootstrap精简为轻量版(只返回summary+financeMonthly) 0.028s - 按需加载: 切tab时通过/api/<resource>/list获取数据 - 修复risk_projects语法错误 前端: - 项目管理tab改名(项目计划/项目→项目管理) - 筛选区增加年份下拉框(2024-2027) - 季度筛选增加年份过滤 - 项目行增加展开箭头,显示月度确收回款明细 - 自动过滤全0月份记录 - 平台费用: 金额→费用计提, 已发生金额→现金支出 - 计划→预算, 实际→执行 - 经营月报/季报卡片顺序调整 - 非总览工作台跳过renderHome - 版本号 v1.2.0.4
This commit is contained in:
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user