Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
14368e83d0 | ||
|
|
63ddffb12a | ||
|
|
2c7cd21095 | ||
|
|
9273be3143 | ||
|
|
2aab73a86a | ||
|
|
e0000c2abb | ||
|
|
356c129668 | ||
|
|
94b9f25938 |
@@ -724,6 +724,359 @@ def delete_file(file_id):
|
||||
conn.close()
|
||||
|
||||
|
||||
|
||||
# ---------- 总览部门损益矩阵 API ----------
|
||||
|
||||
@bp.route("/api/overview/dept-matrix")
|
||||
@login_required
|
||||
def overview_dept_matrix():
|
||||
"""返回各工作台的损益数据矩阵(按年/季/月筛选)"""
|
||||
conn = db()
|
||||
_year = int(request.args.get("year", date.today().year))
|
||||
view = request.args.get("view", "monthly")
|
||||
|
||||
# 12个月数组
|
||||
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
|
||||
|
||||
tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
|
||||
result = {"tenants": tenants, "months": months, "data": {}, "total": {"project_gross": [0]*12, "dept_expense": [0]*12, "profit": [0]*12}}
|
||||
|
||||
for tenant in tenants:
|
||||
# 项目毛利(从 budget_data JSON)
|
||||
pfs = rows(conn, "SELECT budget_data FROM plan_finances WHERE tenant=?", [tenant])
|
||||
gross = [0]*12
|
||||
for pf in pfs:
|
||||
try:
|
||||
bd = json.loads(pf.get("budget_data") or "[]")
|
||||
except:
|
||||
bd = []
|
||||
for b in bd:
|
||||
m = (b.get("month") or "")[:7]
|
||||
if m in months:
|
||||
i = months.index(m)
|
||||
gross[i] += float(b.get("gross") or 0)
|
||||
|
||||
# 部门费用(从 plan_expense_records)
|
||||
expense = [0]*12
|
||||
exps = rows(conn, "SELECT expense_month, amount FROM plan_expense_records WHERE tenant=? AND expense_month LIKE %s", [tenant, f"{_year}-%"])
|
||||
for e in exps:
|
||||
m = (e.get("expense_month") or "")[:7]
|
||||
if m in months:
|
||||
i = months.index(m)
|
||||
expense[i] += float(e.get("amount") or 0)
|
||||
|
||||
profit = [gross[i] - expense[i] for i in range(12)]
|
||||
|
||||
result["data"][tenant] = {
|
||||
"project_gross": gross,
|
||||
"dept_expense": expense,
|
||||
"profit": profit
|
||||
}
|
||||
|
||||
for i in range(12):
|
||||
result["total"]["project_gross"][i] += gross[i]
|
||||
result["total"]["dept_expense"][i] += expense[i]
|
||||
result["total"]["profit"][i] += profit[i]
|
||||
|
||||
# 按视图聚合
|
||||
if view == "quarterly":
|
||||
def to_quarterly(arr12):
|
||||
qr = [(0,1,2),(3,4,5),(6,7,8),(9,10,11)]
|
||||
return [arr12[q[0]]+arr12[q[1]]+arr12[q[2]] for q in qr]
|
||||
result["months"] = ["Q1","Q2","Q3","Q4"]
|
||||
for t in tenants:
|
||||
for key in ["project_gross","dept_expense","profit"]:
|
||||
result["data"][t][key] = to_quarterly(result["data"][t][key])
|
||||
for key in ["project_gross","dept_expense","profit"]:
|
||||
result["total"][key] = to_quarterly(result["total"][key])
|
||||
elif view == "monthly":
|
||||
sel_month = request.args.get("month", "")
|
||||
if sel_month and sel_month in months:
|
||||
mi = months.index(sel_month)
|
||||
result["months"] = [sel_month]
|
||||
for t in tenants:
|
||||
for key in ["project_gross","dept_expense","profit"]:
|
||||
result["data"][t][key] = [result["data"][t][key][mi]]
|
||||
for key in ["project_gross","dept_expense","profit"]:
|
||||
result["total"][key] = [result["total"][key][mi]]
|
||||
# else: yearly (default) — keep all 12 months
|
||||
|
||||
# Compute annual totals
|
||||
result["annual"] = {}
|
||||
for t in tenants:
|
||||
d = result["data"][t]
|
||||
result["annual"][t] = {
|
||||
"project_gross": sum(d["project_gross"]),
|
||||
"dept_expense": sum(d["dept_expense"]),
|
||||
"profit": sum(d["profit"])
|
||||
}
|
||||
result["annual"]["total"] = {
|
||||
"project_gross": sum(result["total"]["project_gross"]),
|
||||
"dept_expense": sum(result["total"]["dept_expense"]),
|
||||
"profit": sum(result["total"]["profit"])
|
||||
}
|
||||
|
||||
conn.close()
|
||||
return jsonify(result)
|
||||
|
||||
@bp.route("/api/overview/finance-dept-matrix")
|
||||
@login_required
|
||||
def overview_finance_dept_matrix():
|
||||
"""返回各工作台已发生损益数据矩阵"""
|
||||
conn = db()
|
||||
_year = date.today().year
|
||||
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
|
||||
tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
|
||||
result = {"tenants": tenants, "months": months, "data": {}, "total": {"project_gross": [0]*12, "dept_expense": [0]*12, "profit": [0]*12}}
|
||||
|
||||
for tenant in tenants:
|
||||
pfs = rows(conn, "SELECT budget_data FROM project_finances WHERE tenant=?", [tenant])
|
||||
gross = [0]*12
|
||||
for pf in pfs:
|
||||
try:
|
||||
bd = json.loads(pf.get("budget_data") or "[]")
|
||||
except:
|
||||
bd = []
|
||||
for b in bd:
|
||||
m = (b.get("month") or "")[:7]
|
||||
if m in months:
|
||||
i = months.index(m)
|
||||
gross[i] += float(b.get("gross") or 0)
|
||||
|
||||
expense = [0]*12
|
||||
exps = rows(conn, "SELECT expense_month, amount FROM expense_records WHERE tenant=? AND expense_month LIKE %s", [tenant, f"{_year}-%"])
|
||||
for e in exps:
|
||||
m = (e.get("expense_month") or "")[:7]
|
||||
if m in months:
|
||||
i = months.index(m)
|
||||
expense[i] += float(e.get("amount") or 0)
|
||||
|
||||
profit = [gross[i] - expense[i] for i in range(12)]
|
||||
result["data"][tenant] = {"project_gross": gross, "dept_expense": expense, "profit": profit}
|
||||
for i in range(12):
|
||||
result["total"]["project_gross"][i] += gross[i]
|
||||
result["total"]["dept_expense"][i] += expense[i]
|
||||
result["total"]["profit"][i] += profit[i]
|
||||
|
||||
result["annual"] = {}
|
||||
for t in tenants:
|
||||
d = result["data"][t]
|
||||
result["annual"][t] = {"project_gross": sum(d["project_gross"]), "dept_expense": sum(d["dept_expense"]), "profit": sum(d["profit"])}
|
||||
result["annual"]["total"] = {"project_gross": sum(result["total"]["project_gross"]), "dept_expense": sum(result["total"]["dept_expense"]), "profit": sum(result["total"]["profit"])}
|
||||
|
||||
conn.close()
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@bp.route("/api/overview/dept-cf-matrix")
|
||||
@login_required
|
||||
def overview_dept_cf_matrix():
|
||||
"""返回各工作台预算现金流数据矩阵"""
|
||||
conn = db()
|
||||
_year = date.today().year
|
||||
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
|
||||
tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
|
||||
result = {"tenants": tenants, "months": months, "data": {}, "total": {"project_cf": [0]*12, "dept_cf": [0]*12, "bl_cf": [0]*12}}
|
||||
|
||||
for tenant in tenants:
|
||||
pfs = rows(conn, "SELECT budget_data, expense_data FROM plan_finances WHERE tenant=?", [tenant])
|
||||
project_cf = [0]*12
|
||||
for pf in pfs:
|
||||
try: bd = json.loads(pf.get("budget_data") or "[]")
|
||||
except: bd = []
|
||||
try: ed = json.loads(pf.get("expense_data") or "[]")
|
||||
except: ed = []
|
||||
for b in bd:
|
||||
m = (b.get("month") or "")[:7]
|
||||
if m in months:
|
||||
i = months.index(m)
|
||||
project_cf[i] += float(b.get("payment") or 0)
|
||||
for e in ed:
|
||||
m = (e.get("month") or "")[:7]
|
||||
if m in months:
|
||||
i = months.index(m)
|
||||
project_cf[i] -= float(e.get("paid") or 0)
|
||||
|
||||
dept_cf = [0]*12
|
||||
exps = rows(conn, "SELECT expense_month, incurred_amount FROM plan_expense_records WHERE tenant=? AND expense_month LIKE %s", [tenant, f"{_year}-%"])
|
||||
for e in exps:
|
||||
m = (e.get("expense_month") or "")[:7]
|
||||
if m in months:
|
||||
i = months.index(m)
|
||||
dept_cf[i] += float(e.get("incurred_amount") or 0)
|
||||
|
||||
bl_cf = [project_cf[i] - dept_cf[i] for i in range(12)]
|
||||
result["data"][tenant] = {"project_cf": project_cf, "dept_cf": dept_cf, "bl_cf": bl_cf}
|
||||
for i in range(12):
|
||||
result["total"]["project_cf"][i] += project_cf[i]
|
||||
result["total"]["dept_cf"][i] += dept_cf[i]
|
||||
result["total"]["bl_cf"][i] += bl_cf[i]
|
||||
|
||||
result["annual"] = {}
|
||||
for t in tenants:
|
||||
d = result["data"][t]
|
||||
result["annual"][t] = {"project_cf": sum(d["project_cf"]), "dept_cf": sum(d["dept_cf"]), "bl_cf": sum(d["bl_cf"])}
|
||||
result["annual"]["total"] = {"project_cf": sum(result["total"]["project_cf"]), "dept_cf": sum(result["total"]["dept_cf"]), "bl_cf": sum(result["total"]["bl_cf"])}
|
||||
|
||||
conn.close()
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@bp.route("/api/overview/finance-dept-cf-matrix")
|
||||
@login_required
|
||||
def overview_finance_dept_cf_matrix():
|
||||
"""返回各工作台已发生现金流数据矩阵"""
|
||||
conn = db()
|
||||
_year = date.today().year
|
||||
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
|
||||
tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
|
||||
result = {"tenants": tenants, "months": months, "data": {}, "total": {"project_cf": [0]*12, "dept_cf": [0]*12, "bl_cf": [0]*12}}
|
||||
|
||||
for tenant in tenants:
|
||||
pfs = rows(conn, "SELECT budget_data, expense_data FROM project_finances WHERE tenant=?", [tenant])
|
||||
project_cf = [0]*12
|
||||
for pf in pfs:
|
||||
try: bd = json.loads(pf.get("budget_data") or "[]")
|
||||
except: bd = []
|
||||
try: ed = json.loads(pf.get("expense_data") or "[]")
|
||||
except: ed = []
|
||||
for b in bd:
|
||||
m = (b.get("month") or "")[:7]
|
||||
if m in months:
|
||||
i = months.index(m)
|
||||
project_cf[i] += float(b.get("payment") or 0)
|
||||
for e in ed:
|
||||
m = (e.get("month") or "")[:7]
|
||||
if m in months:
|
||||
i = months.index(m)
|
||||
project_cf[i] -= float(e.get("paid") or 0)
|
||||
|
||||
dept_cf = [0]*12
|
||||
exps = rows(conn, "SELECT expense_month, incurred_amount FROM expense_records WHERE tenant=? AND expense_month LIKE %s", [tenant, f"{_year}-%"])
|
||||
for e in exps:
|
||||
m = (e.get("expense_month") or "")[:7]
|
||||
if m in months:
|
||||
i = months.index(m)
|
||||
dept_cf[i] += float(e.get("incurred_amount") or 0)
|
||||
|
||||
bl_cf = [project_cf[i] - dept_cf[i] for i in range(12)]
|
||||
result["data"][tenant] = {"project_cf": project_cf, "dept_cf": dept_cf, "bl_cf": bl_cf}
|
||||
for i in range(12):
|
||||
result["total"]["project_cf"][i] += project_cf[i]
|
||||
result["total"]["dept_cf"][i] += dept_cf[i]
|
||||
result["total"]["bl_cf"][i] += bl_cf[i]
|
||||
|
||||
result["annual"] = {}
|
||||
for t in tenants:
|
||||
d = result["data"][t]
|
||||
result["annual"][t] = {"project_cf": sum(d["project_cf"]), "dept_cf": sum(d["dept_cf"]), "bl_cf": sum(d["bl_cf"])}
|
||||
result["annual"]["total"] = {"project_cf": sum(result["total"]["project_cf"]), "dept_cf": sum(result["total"]["dept_cf"]), "bl_cf": sum(result["total"]["bl_cf"])}
|
||||
|
||||
conn.close()
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@bp.route("/api/overview/dept-project-matrix")
|
||||
@login_required
|
||||
def overview_dept_project_matrix():
|
||||
"""返回各工作台预算项目经营详情矩阵"""
|
||||
conn = db()
|
||||
_year = date.today().year
|
||||
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
|
||||
tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
|
||||
keys = ["sign","revenue","gross","cost","paid","payment","cashflow"]
|
||||
result = {"tenants": tenants, "months": months, "data": {}, "total": {k: [0]*12 for k in keys}}
|
||||
|
||||
for tenant in tenants:
|
||||
tdata = {k: [0]*12 for k in keys}
|
||||
pfs = rows(conn, "SELECT sign_amount, sign_month, budget_data, expense_data FROM plan_finances WHERE tenant=?", [tenant])
|
||||
for pf in pfs:
|
||||
sm = (pf.get("sign_month") or "")[:7]
|
||||
if sm in months:
|
||||
i = months.index(sm)
|
||||
tdata["sign"][i] += float(pf.get("sign_amount") or 0)
|
||||
try: bd = json.loads(pf.get("budget_data") or "[]")
|
||||
except: bd = []
|
||||
try: ed = json.loads(pf.get("expense_data") or "[]")
|
||||
except: ed = []
|
||||
for b in bd:
|
||||
m = (b.get("month") or "")[:7]
|
||||
if m in months:
|
||||
i = months.index(m)
|
||||
tdata["revenue"][i] += float(b.get("rev") or 0)
|
||||
tdata["gross"][i] += float(b.get("gross") or 0)
|
||||
tdata["payment"][i] += float(b.get("payment") or 0)
|
||||
for e in ed:
|
||||
m = (e.get("month") or "")[:7]
|
||||
if m in months:
|
||||
i = months.index(m)
|
||||
tdata["cost"][i] += float(e.get("cost") or 0)
|
||||
tdata["paid"][i] += float(e.get("paid") or 0)
|
||||
for i in range(12):
|
||||
tdata["cashflow"][i] = tdata["payment"][i] - tdata["paid"][i]
|
||||
result["data"][tenant] = tdata
|
||||
for k in keys:
|
||||
for i in range(12):
|
||||
result["total"][k][i] += tdata[k][i]
|
||||
|
||||
result["annual"] = {}
|
||||
for t in tenants:
|
||||
result["annual"][t] = {k: sum(result["data"][t][k]) for k in keys}
|
||||
result["annual"]["total"] = {k: sum(result["total"][k]) for k in keys}
|
||||
conn.close()
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@bp.route("/api/overview/finance-dept-project-matrix")
|
||||
@login_required
|
||||
def overview_finance_dept_project_matrix():
|
||||
"""返回各工作台已发生项目经营详情矩阵"""
|
||||
conn = db()
|
||||
_year = date.today().year
|
||||
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
|
||||
tenants = ["学术·无界", "科普·无界", "科研·无界", "医患·无界", "MCN·无界"]
|
||||
keys = ["sign","revenue","gross","cost","paid","payment","cashflow"]
|
||||
result = {"tenants": tenants, "months": months, "data": {}, "total": {k: [0]*12 for k in keys}}
|
||||
|
||||
for tenant in tenants:
|
||||
tdata = {k: [0]*12 for k in keys}
|
||||
pfs = rows(conn, "SELECT sign_amount, sign_month, budget_data, expense_data FROM project_finances WHERE tenant=?", [tenant])
|
||||
for pf in pfs:
|
||||
sm = (pf.get("sign_month") or "")[:7]
|
||||
if sm in months:
|
||||
i = months.index(sm)
|
||||
tdata["sign"][i] += float(pf.get("sign_amount") or 0)
|
||||
try: bd = json.loads(pf.get("budget_data") or "[]")
|
||||
except: bd = []
|
||||
try: ed = json.loads(pf.get("expense_data") or "[]")
|
||||
except: ed = []
|
||||
for b in bd:
|
||||
m = (b.get("month") or "")[:7]
|
||||
if m in months:
|
||||
i = months.index(m)
|
||||
tdata["revenue"][i] += float(b.get("rev") or 0)
|
||||
tdata["gross"][i] += float(b.get("gross") or 0)
|
||||
tdata["payment"][i] += float(b.get("payment") or 0)
|
||||
for e in ed:
|
||||
m = (e.get("month") or "")[:7]
|
||||
if m in months:
|
||||
i = months.index(m)
|
||||
tdata["cost"][i] += float(e.get("cost") or 0)
|
||||
tdata["paid"][i] += float(e.get("paid") or 0)
|
||||
for i in range(12):
|
||||
tdata["cashflow"][i] = tdata["payment"][i] - tdata["paid"][i]
|
||||
result["data"][tenant] = tdata
|
||||
for k in keys:
|
||||
for i in range(12):
|
||||
result["total"][k][i] += tdata[k][i]
|
||||
|
||||
result["annual"] = {}
|
||||
for t in tenants:
|
||||
result["annual"][t] = {k: sum(result["data"][t][k]) for k in keys}
|
||||
result["annual"]["total"] = {k: sum(result["total"][k]) for k in keys}
|
||||
conn.close()
|
||||
return jsonify(result)
|
||||
|
||||
# ---------- health ----------
|
||||
|
||||
@bp.route("/api/health")
|
||||
@@ -740,6 +1093,7 @@ def _aggregate_monthly_from_json(conn, table, tenants):
|
||||
_year = date.today().year
|
||||
months = [f"{_year}-{m:02d}" for m in range(1, 13)]
|
||||
rev, gross, payment, cost, paid = [0]*12, [0]*12, [0]*12, [0]*12, [0]*12
|
||||
sign_monthly = [0]*12
|
||||
sign_total = 0
|
||||
sign_cnt = 0
|
||||
|
||||
@@ -750,6 +1104,10 @@ def _aggregate_monthly_from_json(conn, table, tenants):
|
||||
if sm.startswith(str(_year)):
|
||||
sign_total += float(pf.get("sign_amount") or 0)
|
||||
sign_cnt += 1
|
||||
# 签单按月拆分
|
||||
sm7 = sm[:7]
|
||||
if sm7 in months:
|
||||
sign_monthly[months.index(sm7)] += float(pf.get("sign_amount") or 0)
|
||||
try:
|
||||
bd = json.loads(pf.get("budget_data") or "[]")
|
||||
except:
|
||||
@@ -777,6 +1135,7 @@ def _aggregate_monthly_from_json(conn, table, tenants):
|
||||
"rev": rev, "gross": gross, "payment": payment, "cost": cost, "paid": paid,
|
||||
"cashflow": [payment[i] - paid[i] for i in range(12)],
|
||||
"sign_total": sign_total, "sign_cnt": sign_cnt,
|
||||
"sign_monthly": sign_monthly,
|
||||
}
|
||||
|
||||
|
||||
@@ -828,6 +1187,7 @@ def _build_overview_result(agg, exp_amount, exp_incurred):
|
||||
# 项目经营详情
|
||||
project = {
|
||||
"sign": agg["sign_total"],
|
||||
"sign_monthly": agg["sign_monthly"],
|
||||
"revenue": rev,
|
||||
"gross": gross,
|
||||
"cost": cost,
|
||||
@@ -865,7 +1225,7 @@ def overview_plan_quarterly():
|
||||
conn.close()
|
||||
# Convert monthly to quarterly
|
||||
result = _build_overview_result(agg, exp_amount, exp_incurred)
|
||||
for key in ["revenue","gross","payment","cost","paid","cashflow"]:
|
||||
for key in ["sign_monthly","revenue","gross","payment","cost","paid","cashflow"]:
|
||||
result["project"][key] = _quarterly_from_monthly(result["project"][key])
|
||||
result["dept"]["project_gross"] = _quarterly_from_monthly(result["dept"]["project_gross"])
|
||||
result["dept"]["dept_expense"] = _quarterly_from_monthly(result["dept"]["dept_expense"])
|
||||
@@ -894,7 +1254,7 @@ def overview_finance_quarterly():
|
||||
exp_amount, exp_incurred = _aggregate_expenses(conn, "expense_records", OVERVIEW_TENANTS)
|
||||
conn.close()
|
||||
result = _build_overview_result(agg, exp_amount, exp_incurred)
|
||||
for key in ["revenue","gross","payment","cost","paid","cashflow"]:
|
||||
for key in ["sign_monthly","revenue","gross","payment","cost","paid","cashflow"]:
|
||||
result["project"][key] = _quarterly_from_monthly(result["project"][key])
|
||||
result["dept"]["project_gross"] = _quarterly_from_monthly(result["dept"]["project_gross"])
|
||||
result["dept"]["dept_expense"] = _quarterly_from_monthly(result["dept"]["dept_expense"])
|
||||
|
||||
@@ -297,10 +297,9 @@ function renderFinance() {
|
||||
{ key: '_expand', label: '' },
|
||||
{ key: 'client_name', label: '客户' }, { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' },
|
||||
{ key: 'rev', label: '已确收' }, { key: 'gross', label: '毛利' }, { key: 'gross_rate', label: '毛利率' },
|
||||
{ key: 'payment', label: '回款' }, { key: 'pay_rate', label: '回款率' },
|
||||
{ key: 'payment', label: '回款' }, { key: 'pending', label: '待回款' }, { key: 'pay_rate', label: '回款率' },
|
||||
{ key: 'cost', label: '成本' }, { key: 'paid', label: '支出' }, { key: 'paid_rate', label: '付款率' },
|
||||
{ key: 'cashflow', label: '现金流' },
|
||||
{ key: 'pending', label: '待回款' },
|
||||
];
|
||||
const UNSIGNED_COLS = [
|
||||
{ key: 'client_name', label: '客户' }, { key: 'customer_name', label: '项目名称' }, { key: 'sign_amount', label: '签约金额' },
|
||||
@@ -340,7 +339,7 @@ function renderFinance() {
|
||||
const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
const pending = rev - payment;
|
||||
const pendingCls = pending > 0 ? 'text-red-600' : pending < 0 ? 'text-green-600' : 'text-slate-400';
|
||||
const pendingVal = pending ? money(pending) : '<span class="text-slate-300">—</span>';
|
||||
const pendingVal = pending ? (pending > 0 ? '+' : '') + money(pending) : '<span class="text-slate-300">—</span>';
|
||||
var budgetArr = [];
|
||||
var expenseArr = [];
|
||||
try { budgetArr = JSON.parse(pf.budget_data || "[]"); } catch(e) {}
|
||||
@@ -363,7 +362,7 @@ function renderFinance() {
|
||||
var bCf = bPay - ePaid;
|
||||
return '<tr class="bg-slate-50"><td class="px-2 py-px"></td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-slate-400">' + m + '</td><td class="px-2 py-px text-center text-xs text-blue-600">' + (bRev ? money(bRev) : '—') + '</td><td class="px-2 py-px text-center text-xs text-green-600">' + (bGross ? money(bGross) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-amber-600">' + (bPay ? money(bPay) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-rose-600">' + (eCost ? money(eCost) : '—') + '</td><td class="px-2 py-px text-center text-xs text-purple-600">' + (ePaid ? money(ePaid) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs ' + (bCf >= 0 ? 'text-green-600' : 'text-red-600') + '">' + (bCf ? money(bCf) : '—') + '</td><td class="px-2 py-px text-center text-xs ' + (bRev - bPay > 0 ? 'text-red-600' : bRev - bPay < 0 ? 'text-green-600' : 'text-slate-300') + '">' + (bRev - bPay ? money(bRev - bPay) : '—') + '</td></tr>';
|
||||
}).join('');
|
||||
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation();finToggleExpand(${pf.id})"><i data-lucide="chevron-right" style="width:14px;height:14px;color:#94a3b8" id="fin_expand_icon_${pf.id}"></i></td><td class="px-2 py-px text-center align-middle text-sm truncate" style="max-width:100px" title="${esc(pf.client_name || '')}">${esc(pf.client_name || "")}</td><td class="px-2 py-px text-sm font-medium text-center align-middle truncate" style="min-width:300px;max-width:300px;width:300px" title="${esc(pf.customer_name)}">${esc(pf.customer_name)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center align-middle text-sm">${grossR}</td><td class="px-2 py-px text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="px-2 py-px text-center align-middle text-sm">${payR}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="px-2 py-px text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="px-2 py-px text-center align-middle text-sm">${paidR}</td><td class="px-2 py-px text-center font-semibold align-middle ${cfCls}">${cf}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${pendingCls}">${pendingVal}</td></tr><tr class="hidden" id="fin_expand_${pf.id}"><td colspan="14"><table class="w-full"><thead><tr class="border-b border-slate-200"><th class="px-2 py-1"></th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">月份</th><th class="px-2 py-1 text-center text-xs text-slate-400">确收</th><th class="px-2 py-1 text-center text-xs text-slate-400">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">成本</th><th class="px-2 py-1 text-center text-xs text-slate-400">支出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-400">待回款</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
|
||||
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="openPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation();finToggleExpand(${pf.id})"><i data-lucide="chevron-right" style="width:14px;height:14px;color:#94a3b8" id="fin_expand_icon_${pf.id}"></i></td><td class="px-2 py-px text-center align-middle text-sm truncate" style="max-width:100px" title="${esc(pf.client_name || '')}">${esc(pf.client_name || "")}</td><td class="px-2 py-px text-sm font-medium text-center align-middle truncate" style="min-width:300px;max-width:300px;width:300px" title="${esc(pf.customer_name)}">${esc(pf.customer_name)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center align-middle text-sm">${grossR}</td><td class="px-2 py-px text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${pendingCls}">${pendingVal}</td><td class="px-2 py-px text-center align-middle text-sm">${payR}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="px-2 py-px text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="px-2 py-px text-center align-middle text-sm">${paidR}</td><td class="px-2 py-px text-center font-semibold align-middle ${cfCls}">${cf}</td></tr><tr class="hidden" id="fin_expand_${pf.id}"><td colspan="14"><table class="w-full"><thead><tr class="border-b border-slate-200"><th class="px-2 py-1"></th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">月份</th><th class="px-2 py-1 text-center text-xs text-slate-400">确收</th><th class="px-2 py-1 text-center text-xs text-slate-400">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">成本</th><th class="px-2 py-1 text-center text-xs text-slate-400">支出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-400">待回款</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
|
||||
};
|
||||
// 待签约简版行
|
||||
const tdRowUnsigned = (pf, cost, gross) => {
|
||||
@@ -1249,3 +1248,10 @@ function renderOverviewCharts(data) {
|
||||
state.finChart4 = new Chart(c4, Object.assign({}, iconf, { data: { labels: labels, datasets: [{ label: "利润", data: data.map(function(x) { return x.gross || 0; }), borderColor: "#6366f1", backgroundColor: "rgba(99,102,241,0.06)", fill: true, tension: 0.3 }] } }));
|
||||
}
|
||||
};
|
||||
|
||||
// ====== 筛选器控制函数 ======
|
||||
window.switchFinFilter = (f) => { state.finFilter = f; renderFinance(); };
|
||||
window.setFinView = (v) => { state.finView = v; renderFinance(); };
|
||||
window.setFinYear = (y) => { state.finYear = parseInt(y); renderFinance(); };
|
||||
window.setFinQuarter = (q) => { state.finView = 'quarterly'; state.finQuarter = q; renderFinance(); };
|
||||
window.setFinMonth = (m) => { state.finView = 'monthly'; state.finMonth = m; renderFinance(); };
|
||||
|
||||
@@ -1,38 +1,123 @@
|
||||
// overview_finance.js — 发生总览模块(5 个工作台数据聚合)
|
||||
|
||||
function renderOverviewFinance() {
|
||||
const view = state.overviewView || "monthly";
|
||||
const api = view === "quarterly" ? "/api/overview/finance-quarterly" : "/api/overview/finance-monthly";
|
||||
const container = document.querySelector("#overview_finance");
|
||||
var view = state.overviewView || "monthly";
|
||||
if (view === 'matrix') { renderFinanceDeptMatrix(); return; }
|
||||
if (view === 'cf_matrix') { renderFinanceCfMatrix(); return; }
|
||||
if (view === 'project_matrix') { renderFinanceProjectMatrix(); return; }
|
||||
|
||||
var api = view === "quarterly" ? "/api/overview/finance-quarterly" : "/api/overview/finance-monthly";
|
||||
var container = document.querySelector("#overview_finance");
|
||||
if (!container) return;
|
||||
container.innerHTML = '<div class="p-4 text-center text-slate-400">加载中...</div>';
|
||||
|
||||
fetch(api).then(r => r.json()).then(data => {
|
||||
if (state.overviewView !== view) return;
|
||||
renderOverviewFinanceData(data, view);
|
||||
}).catch(() => {
|
||||
if (state.overviewView !== view) return;
|
||||
container.innerHTML = '<div class="p-4 text-center text-slate-400">加载失败</div>';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function isAllZero(data, tenants, keys, mi) {
|
||||
for (var ti = 0; ti < tenants.length; ti++) {
|
||||
for (var ki = 0; ki < keys.length; ki++) {
|
||||
if ((data.data[tenants[ti]][keys[ki]][mi] || 0) !== 0) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// ====== 项目经营详情报表(顶部定义,避免调用时未注册) ======
|
||||
|
||||
function renderFinanceProjectMatrix() {
|
||||
var c = document.querySelector("#overview_finance");
|
||||
if (!c) return;
|
||||
c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载项目经营详情矩阵...</div>';
|
||||
fetch('/api/overview/finance-dept-project-matrix').then(function(r) {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
return r.json();
|
||||
}).then(function(data) {
|
||||
renderFinanceProjectMatrixData(data, c);
|
||||
}).catch(function(e) {
|
||||
c.innerHTML = '<div class="p-3 text-center text-red-500 text-sm">加载失败: ' + (e.message || '未知错误') + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderFinanceProjectMatrixData(data, container) {
|
||||
try {
|
||||
var mf = function(v) { return Math.round(Number(v || 0)).toLocaleString("zh-CN"); };
|
||||
var tenants = data.tenants;
|
||||
var sn = tenants.map(function(t) { return t.replace('·无界', ''); });
|
||||
var mL = ['01月','02月','03月','04月','05月','06月','07月','08月','09月','10月','11月','12月'];
|
||||
var keys = ["sign","revenue","gross","cost","paid","payment","cashflow"];
|
||||
var labels = ["合同金额","确收金额","确收毛利","当期成本","当期流出","回款","项目现金流"];
|
||||
|
||||
var vBtns = financeBtnBar('project_matrix');
|
||||
var cards = '';
|
||||
for (var mi = 11; mi >= 0; mi--) {
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
|
||||
th += '</tr>';
|
||||
if (isAllZero(data, tenants, keys.filter(function(k) { return k !== 'sign'; }), mi)) continue;
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
|
||||
th += '</tr>';
|
||||
|
||||
var tb = financeProjectMatrixTbody(data, tenants, mi, keys, labels, mf);
|
||||
cards += '<div class="flex items-center gap-3"><div class="text-5xl font-bold text-blue-200" style="width:120px;text-align:center;white-space:nowrap">' + mL[mi] + '</div><div class="card p-4 flex-1"><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + th + '</thead><tbody>' + tb + '</tbody></table></div></div></div>';
|
||||
}
|
||||
container.innerHTML = '<div class="grid gap-2.5">' + vBtns + '<div class="flex flex-col gap-2">' + cards + '</div></div>';
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
} catch(e) { container.innerHTML = '<div class="p-3 text-center text-red-500 text-sm">渲染失败: ' + (e.message || '未知错误') + '</div>'; }
|
||||
}
|
||||
|
||||
function financeProjectMatrixTbody(data, tenants, mi, keys, labels, mf) {
|
||||
var tb = '';
|
||||
for (var ri = 0; ri < keys.length; ri++) {
|
||||
var rowCls = ri === keys.length - 1 ? 'font-semibold' : '';
|
||||
var ts = 0;
|
||||
var vals = [];
|
||||
for (var ti = 0; ti < tenants.length; ti++) {
|
||||
var v = data.data[tenants[ti]][keys[ri]][mi] || 0;
|
||||
ts += v;
|
||||
vals.push(v);
|
||||
}
|
||||
var tCls = 'text-slate-700';
|
||||
if (ri === 2) tCls = 'text-green-600';
|
||||
if (ri === keys.length - 1) tCls = ts >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
var rh = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + labels[ri] + '</td><td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) {
|
||||
var cls = 'text-slate-700';
|
||||
if (ri === 2) cls = 'text-green-600';
|
||||
if (ri === keys.length - 1) cls = vals[ti] >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
rh += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + mf(vals[ti]) + '</td>';
|
||||
}
|
||||
rh += '</tr>';
|
||||
tb += rh;
|
||||
}
|
||||
return tb;
|
||||
}
|
||||
|
||||
function renderOverviewFinanceData(data, view) {
|
||||
const container = document.querySelector("#overview_finance");
|
||||
if (!container) return;
|
||||
|
||||
const moneyIntL = (v) => Math.round(Number(v || 0)).toLocaleString("zh-CN");
|
||||
const card = (body, cls) => '<div class="card ' + (cls || 'p-4') + '">' + body + '</div>';
|
||||
|
||||
const isQuarterly = view === "quarterly";
|
||||
const headers = isQuarterly ? ['Q1','Q2','Q3','Q4'] : ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'];
|
||||
const cols = isQuarterly ? 4 : 12;
|
||||
const d = data.dept, cf = data.dept_cf, p = data.project;
|
||||
|
||||
const d = data.dept;
|
||||
const cf = data.dept_cf;
|
||||
const p = data.project;
|
||||
var btns = [];
|
||||
btns.push('<button onclick="setOverviewFinanceView(\'monthly\')" class="finance-tab' + (view === 'monthly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">月报总览</button>');
|
||||
btns.push('<button onclick="setOverviewFinanceView(\'quarterly\')" class="finance-tab' + (view === 'quarterly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">季报总览</button>');
|
||||
btns.push('<button onclick="setOverviewFinanceView(\'matrix\')" class="finance-tab' + (view === 'matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">损益详细报表</button>');
|
||||
btns.push('<button onclick="setOverviewFinanceView(\'cf_matrix\')" class="finance-tab' + (view === 'cf_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">现金流详细报表</button>');
|
||||
btns.push('<button onclick="setOverviewFinanceView(\'project_matrix\')" class="finance-tab' + (view === 'project_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">项目经营详细报表</button>');
|
||||
var viewBtns = '<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:12px">' + btns.join('') + '</div>';
|
||||
|
||||
// 视图切换按钮
|
||||
var viewBtns = '<div class="flex items-center gap-2 mb-2"><button onclick="setOverviewFinanceView(\'monthly\')" class="px-3 py-0.5 rounded text-sm font-medium ' + (view === 'monthly' ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200') + '">月报总览</button><button onclick="setOverviewFinanceView(\'quarterly\')" class="px-3 py-0.5 rounded text-sm font-medium ' + (view === 'quarterly' ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200') + '">季报总览</button></div>';
|
||||
|
||||
// ====== 部门损益情况 ======
|
||||
var deptThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
for (var hi = 0; hi < cols; hi++) { deptThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + headers[hi] + '</th>'; }
|
||||
deptThead += '</tr>';
|
||||
@@ -44,8 +129,7 @@ function renderOverviewFinanceData(data, view) {
|
||||
];
|
||||
var deptTbody = '';
|
||||
for (var dri = 0; dri < 3; dri++) {
|
||||
var dr = deptRows[dri];
|
||||
var rowCls = dri === 2 ? 'font-semibold' : '';
|
||||
var dr = deptRows[dri], rowCls = dri === 2 ? 'font-semibold' : '';
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + dr.label + '</td>';
|
||||
var annualCls = dri === 2 ? (dr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
|
||||
rowHtml += '<td class="py-1.5 text-right ' + rowCls + ' ' + annualCls + ' whitespace-nowrap">' + moneyIntL(dr.annual) + '</td>';
|
||||
@@ -57,7 +141,6 @@ function renderOverviewFinanceData(data, view) {
|
||||
}
|
||||
var deptCard = card('<h3 class="text-base font-bold text-slate-700 mb-3">部门损益情况 <span class="text-sm text-slate-500 font-normal ml-3">部门利润 = 所有项目产生的毛利 - 部门 1 级产生的人力成本,研发成本,差旅和其他支出</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + deptTbody + '</tbody></table></div>', 'p-4');
|
||||
|
||||
// ====== 部门现金流 ======
|
||||
var cfRows = [
|
||||
{ label: '项目现金流', vals: cf.project_cf, annual: cf.annual[0] },
|
||||
{ label: '部门现金流', vals: cf.dept_cf_item, annual: cf.annual[1] },
|
||||
@@ -65,8 +148,7 @@ function renderOverviewFinanceData(data, view) {
|
||||
];
|
||||
var cfTbody = '';
|
||||
for (var cfri = 0; cfri < 3; cfri++) {
|
||||
var cfr = cfRows[cfri];
|
||||
var cfRowCls = cfri === 2 ? 'font-semibold' : '';
|
||||
var cfr = cfRows[cfri], cfRowCls = cfri === 2 ? 'font-semibold' : '';
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + cfRowCls + ' text-slate-700 whitespace-nowrap">' + cfr.label + '</td>';
|
||||
var cAnnualCls = cfri === 2 ? (cfr.annual >= 0 ? 'text-green-600' : 'text-red-600') : 'text-slate-700';
|
||||
rowHtml += '<td class="py-1.5 text-right ' + cfRowCls + ' ' + cAnnualCls + ' whitespace-nowrap">' + moneyIntL(cfr.annual) + '</td>';
|
||||
@@ -78,21 +160,18 @@ function renderOverviewFinanceData(data, view) {
|
||||
}
|
||||
var cfCard = card('<h3 class="text-base font-bold text-slate-700 mb-3">部门现金流 <span class="text-sm text-slate-500 font-normal ml-3">部门现金流=所有项目产生的现金流 - 部门一级因费用产生的现金流出或流入</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + deptThead + '</thead><tbody>' + cfTbody + '</tbody></table></div>', 'p-4');
|
||||
|
||||
// ====== 项目经营详情 ======
|
||||
var LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流'];
|
||||
var projVals = [p.revenue, p.gross, p.cost, p.paid, p.payment, p.cashflow];
|
||||
var projVals = [p.sign_monthly || [], p.revenue, p.gross, p.cost, p.paid, p.payment, p.cashflow];
|
||||
var projAnnual = p.annual;
|
||||
|
||||
var projThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
for (var phi = 0; phi < cols; phi++) { projThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + headers[phi] + '</th>'; }
|
||||
projThead += '</tr>';
|
||||
|
||||
var projTbody = '';
|
||||
for (var ri = 0; ri < 7; ri++) {
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left font-semibold text-slate-700 whitespace-nowrap">' + LABELS[ri] + '</td>';
|
||||
rowHtml += '<td class="py-1.5 text-right font-semibold text-slate-800 whitespace-nowrap">' + (ri === 0 ? moneyIntL(p.sign) : moneyIntL(projAnnual[ri])) + '</td>';
|
||||
rowHtml += '<td class="py-1.5 text-right font-semibold text-slate-800 whitespace-nowrap">' + moneyIntL(projAnnual[ri]) + '</td>';
|
||||
for (var i = 0; i < cols; i++) {
|
||||
var val = ri === 0 ? 0 : (projVals[ri - 1] ? projVals[ri - 1][i] : 0);
|
||||
var val = projVals[ri] ? (projVals[ri][i] || 0) : 0;
|
||||
var cls = 'text-slate-700';
|
||||
if (ri === 2) cls = 'text-green-600';
|
||||
if (ri === 6) cls = val >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
@@ -106,7 +185,97 @@ function renderOverviewFinanceData(data, view) {
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
}
|
||||
|
||||
window.setOverviewFinanceView = (v) => {
|
||||
state.overviewView = v;
|
||||
renderOverviewFinance();
|
||||
};
|
||||
window.setOverviewFinanceView = (v) => { state.overviewView = v; renderOverviewFinance(); };
|
||||
|
||||
// ====== 损益报表 ======
|
||||
|
||||
function renderFinanceDeptMatrix() {
|
||||
var c = document.querySelector("#overview_finance");
|
||||
if (!c) return;
|
||||
c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载损益矩阵...</div>';
|
||||
fetch('/api/overview/finance-dept-matrix').then(r => r.json()).then(function(data) { renderFinanceDeptMatrixData(data, c); })
|
||||
.catch(function() { c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载失败</div>'; });
|
||||
}
|
||||
|
||||
function renderFinanceDeptMatrixData(data, container) {
|
||||
var mf = function(v) { return Math.round(Number(v || 0)).toLocaleString("zh-CN"); };
|
||||
var tenants = data.tenants;
|
||||
var sn = tenants.map(function(t) { return t.replace('·无界', ''); });
|
||||
var mL = ['01月','02月','03月','04月','05月','06月','07月','08月','09月','10月','11月','12月'];
|
||||
var vBtns = financeBtnBar('matrix');
|
||||
var cards = '';
|
||||
for (var mi = 11; mi >= 0; mi--) {
|
||||
if (isAllZero(data, tenants, ['project_gross','dept_expense','profit'], mi)) continue;
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
|
||||
th += '</tr>';
|
||||
var tb = financeMatrixTbody(data, tenants, mi, ['project_gross','dept_expense','profit'], ['项目毛利','部门费用','利润'], mf);
|
||||
cards += '<div class="flex items-center gap-3"><div class="text-5xl font-bold text-blue-200" style="width:120px;text-align:center;white-space:nowrap">' + mL[mi] + '</div><div class="card p-4 flex-1"><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + th + '</thead><tbody>' + tb + '</tbody></table></div></div></div>';
|
||||
}
|
||||
container.innerHTML = '<div class="grid gap-2.5">' + vBtns + '<div class="flex flex-col gap-2">' + cards + '</div></div>';
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
}
|
||||
|
||||
// ====== 现金流报表 ======
|
||||
|
||||
function renderFinanceCfMatrix() {
|
||||
var c = document.querySelector("#overview_finance");
|
||||
if (!c) return;
|
||||
c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载现金流矩阵...</div>';
|
||||
fetch('/api/overview/finance-dept-cf-matrix').then(r => r.json()).then(function(data) { renderFinanceCfMatrixData(data, c); })
|
||||
.catch(function() { c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载失败</div>'; });
|
||||
}
|
||||
|
||||
function renderFinanceCfMatrixData(data, container) {
|
||||
var mf = function(v) { return Math.round(Number(v || 0)).toLocaleString("zh-CN"); };
|
||||
var tenants = data.tenants;
|
||||
var sn = tenants.map(function(t) { return t.replace('·无界', ''); });
|
||||
var mL = ['01月','02月','03月','04月','05月','06月','07月','08月','09月','10月','11月','12月'];
|
||||
var vBtns = financeBtnBar('cf_matrix');
|
||||
var cards = '';
|
||||
for (var mi = 11; mi >= 0; mi--) {
|
||||
if (isAllZero(data, tenants, ['project_cf','dept_cf','bl_cf'], mi)) continue;
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
|
||||
th += '</tr>';
|
||||
var tb = financeMatrixTbody(data, tenants, mi, ['project_cf','dept_cf','bl_cf'], ['项目现金流','部门现金流','经营现金流'], mf);
|
||||
cards += '<div class="flex items-center gap-3"><div class="text-5xl font-bold text-blue-200" style="width:120px;text-align:center;white-space:nowrap">' + mL[mi] + '</div><div class="card p-4 flex-1"><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + th + '</thead><tbody>' + tb + '</tbody></table></div></div></div>';
|
||||
}
|
||||
container.innerHTML = '<div class="grid gap-2.5">' + vBtns + '<div class="flex flex-col gap-2">' + cards + '</div></div>';
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
}
|
||||
|
||||
// ====== 共用工具 ======
|
||||
|
||||
function financeBtnBar(active) {
|
||||
var btns = [];
|
||||
btns.push('<button onclick="setOverviewFinanceView(\'monthly\')" class="finance-tab' + (active === 'monthly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">月报总览</button>');
|
||||
btns.push('<button onclick="setOverviewFinanceView(\'quarterly\')" class="finance-tab' + (active === 'quarterly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">季报总览</button>');
|
||||
btns.push('<button onclick="setOverviewFinanceView(\'matrix\')" class="finance-tab' + (active === 'matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">损益详细报表</button>');
|
||||
btns.push('<button onclick="setOverviewFinanceView(\'cf_matrix\')" class="finance-tab' + (active === 'cf_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">现金流详细报表</button>');
|
||||
btns.push('<button onclick="setOverviewFinanceView(\'project_matrix\')" class="finance-tab' + (active === 'project_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">项目经营详细报表</button>');
|
||||
return '<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:12px">' + btns.join('') + '</div>';
|
||||
}
|
||||
|
||||
function financeMatrixTbody(data, tenants, mi, keys, labels, mf) {
|
||||
var tb = '';
|
||||
for (var ri = 0; ri < keys.length; ri++) {
|
||||
var rowCls = ri === keys.length - 1 ? 'font-semibold' : '';
|
||||
var ts = 0;
|
||||
var vals = [];
|
||||
for (var ti = 0; ti < tenants.length; ti++) {
|
||||
var v = data.data[tenants[ti]][keys[ri]][mi] || 0;
|
||||
ts += v;
|
||||
vals.push(v);
|
||||
}
|
||||
var tCls = ts >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
var rh = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + labels[ri] + '</td><td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) {
|
||||
var cls = vals[ti] >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
rh += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + mf(vals[ti]) + '</td>';
|
||||
}
|
||||
rh += '</tr>';
|
||||
tb += rh;
|
||||
}
|
||||
return tb;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,93 @@
|
||||
// overview_plan.js — 预算总览模块(5 个工作台数据聚合)
|
||||
|
||||
function renderOverviewPlan() {
|
||||
const view = state.overviewView || "monthly";
|
||||
const api = view === "quarterly" ? "/api/overview/plan-quarterly" : "/api/overview/plan-monthly";
|
||||
const container = document.querySelector("#overview_plan");
|
||||
var view = state.overviewView || "monthly";
|
||||
var container = document.querySelector("#overview_plan");
|
||||
if (!container) return;
|
||||
container.innerHTML = '<div class="p-4 text-center text-slate-400">加载中...</div>';
|
||||
|
||||
if (view === 'matrix') { renderDeptMatrix(); return; }
|
||||
if (view === 'cf_matrix') { renderCfMatrix(); return; }
|
||||
if (view === 'project_matrix') { renderProjectMatrix(); return; }
|
||||
|
||||
var api = view === "quarterly" ? "/api/overview/plan-quarterly" : "/api/overview/plan-monthly";
|
||||
fetch(api).then(r => r.json()).then(data => {
|
||||
if (state.overviewView !== view) return;
|
||||
renderOverviewPlanData(data, view);
|
||||
}).catch(() => {
|
||||
if (state.overviewView !== view) return;
|
||||
container.innerHTML = '<div class="p-4 text-center text-slate-400">加载失败</div>';
|
||||
});
|
||||
}
|
||||
function renderProjectMatrix() {
|
||||
var c = document.querySelector("#overview_plan");
|
||||
if (!c) return;
|
||||
c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载项目经营详情矩阵...</div>';
|
||||
fetch('/api/overview/dept-project-matrix').then(function(r) {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
return r.json();
|
||||
}).then(function(data) {
|
||||
renderProjectMatrixData(data, c);
|
||||
}).catch(function(e) {
|
||||
c.innerHTML = '<div class="p-3 text-center text-red-500 text-sm">加载失败: ' + (e.message || '未知错误') + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
function renderProjectMatrixData(data, container) {
|
||||
try {
|
||||
var mf = function(v) { return Math.round(Number(v || 0)).toLocaleString("zh-CN"); };
|
||||
var tenants = data.tenants;
|
||||
var sn = tenants.map(function(t) { return t.replace('·无界', ''); });
|
||||
var mL = ['01月','02月','03月','04月','05月','06月','07月','08月','09月','10月','11月','12月'];
|
||||
var keys = ["sign","revenue","gross","cost","paid","payment","cashflow"];
|
||||
var labels = ["合同金额","确收金额","确收毛利","当期成本","当期流出","回款","项目现金流"];
|
||||
|
||||
var vBtns = btnBar('project_matrix');
|
||||
var cards = '';
|
||||
for (var mi = 11; mi >= 0; mi--) {
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
|
||||
th += '</tr>';
|
||||
|
||||
if (isAllZero(data, tenants, keys.filter(function(k) { return k !== 'sign'; }), mi)) continue;
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
|
||||
th += '</tr>';
|
||||
|
||||
var tb = projectMatrixTbody(data, tenants, mi, keys, labels, mf);
|
||||
cards += '<div class="flex items-center gap-3"><div class="text-5xl font-bold text-blue-200" style="width:120px;text-align:center;white-space:nowrap">' + mL[mi] + '</div><div class="card p-4 flex-1"><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + th + '</thead><tbody>' + tb + '</tbody></table></div></div></div>';
|
||||
}
|
||||
container.innerHTML = '<div class="grid gap-2.5">' + vBtns + '<div class="flex flex-col gap-2">' + cards + '</div></div>';
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
} catch(e) { container.innerHTML = '<div class="p-3 text-center text-red-500 text-sm">渲染失败: ' + (e.message || '未知错误') + '</div>'; }
|
||||
}
|
||||
|
||||
function projectMatrixTbody(data, tenants, mi, keys, labels, mf) {
|
||||
var tb = '';
|
||||
for (var ri = 0; ri < keys.length; ri++) {
|
||||
var rowCls = ri === keys.length - 1 ? 'font-semibold' : '';
|
||||
var ts = 0;
|
||||
var vals = [];
|
||||
for (var ti = 0; ti < tenants.length; ti++) {
|
||||
var v = data.data[tenants[ti]][keys[ri]][mi] || 0;
|
||||
ts += v;
|
||||
vals.push(v);
|
||||
}
|
||||
var tCls = 'text-slate-700';
|
||||
if (ri === 2) tCls = 'text-green-600';
|
||||
if (ri === keys.length - 1) tCls = ts >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
var rh = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + labels[ri] + '</td><td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) {
|
||||
var cls = 'text-slate-700';
|
||||
if (ri === 2) cls = 'text-green-600';
|
||||
if (ri === keys.length - 1) cls = vals[ti] >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
rh += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + mf(vals[ti]) + '</td>';
|
||||
}
|
||||
rh += '</tr>';
|
||||
tb += rh;
|
||||
}
|
||||
return tb;
|
||||
}
|
||||
|
||||
function renderOverviewPlanData(data, view) {
|
||||
const container = document.querySelector("#overview_plan");
|
||||
@@ -29,6 +104,14 @@ function renderOverviewPlanData(data, view) {
|
||||
const cf = data.dept_cf;
|
||||
const p = data.project;
|
||||
|
||||
// 视图切换按钮
|
||||
var btns = [];
|
||||
btns.push('<button onclick="setOverviewView(\'monthly\')" class="finance-tab' + (view === 'monthly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">月报总览</button>');
|
||||
btns.push('<button onclick="setOverviewView(\'quarterly\')" class="finance-tab' + (view === 'quarterly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">季报总览</button>');
|
||||
btns.push('<button onclick="setOverviewView(\'matrix\')" class="finance-tab' + (view === 'matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">损益详细报表</button>');
|
||||
btns.push('<button onclick="setOverviewView(\'cf_matrix\')" class="finance-tab' + (view === 'cf_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">现金流详细报表</button>'); btns.push('<button onclick="setOverviewView(\'project_matrix\')" class="finance-tab' + (view === 'project_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">项目经营详细报表</button>');
|
||||
var viewBtns = '<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:12px">' + btns.join('') + '</div>';
|
||||
|
||||
// ====== 部门损益情况 ======
|
||||
var deptThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
for (var hi = 0; hi < cols; hi++) { deptThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + headers[hi] + '</th>'; }
|
||||
@@ -77,8 +160,8 @@ function renderOverviewPlanData(data, view) {
|
||||
|
||||
// ====== 项目经营详情 ======
|
||||
var LABELS = ['合同金额','确收金额','确收毛利','成本','支出','回款','项目现金流'];
|
||||
var projVals = [p.revenue, p.gross, p.cost, p.paid, p.payment, p.cashflow];
|
||||
var projAnnual = p.annual; // [sign, revenue_sum, gross_sum, cost_sum, paid_sum, payment_sum, cf_sum]
|
||||
var projVals = [p.sign_monthly || [], p.revenue, p.gross, p.cost, p.paid, p.payment, p.cashflow];
|
||||
var projAnnual = p.annual;
|
||||
|
||||
var projThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">年度累计</th>';
|
||||
for (var phi = 0; phi < cols; phi++) { projThead += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + headers[phi] + '</th>'; }
|
||||
@@ -87,9 +170,9 @@ function renderOverviewPlanData(data, view) {
|
||||
var projTbody = '';
|
||||
for (var ri = 0; ri < 7; ri++) {
|
||||
var rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left font-semibold text-slate-700 whitespace-nowrap">' + LABELS[ri] + '</td>';
|
||||
rowHtml += '<td class="py-1.5 text-right font-semibold text-slate-800 whitespace-nowrap">' + (ri === 0 ? moneyIntL(p.sign) : moneyIntL(projAnnual[ri])) + '</td>';
|
||||
rowHtml += '<td class="py-1.5 text-right font-semibold text-slate-800 whitespace-nowrap">' + moneyIntL(projAnnual[ri]) + '</td>';
|
||||
for (var i = 0; i < cols; i++) {
|
||||
var val = ri === 0 ? 0 : (projVals[ri - 1] ? projVals[ri - 1][i] : 0);
|
||||
var val = projVals[ri] ? (projVals[ri][i] || 0) : 0;
|
||||
var cls = 'text-slate-700';
|
||||
if (ri === 2) cls = 'text-green-600';
|
||||
if (ri === 6) cls = val >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
@@ -99,14 +182,121 @@ function renderOverviewPlanData(data, view) {
|
||||
}
|
||||
var projCard = card('<h3 class="text-base font-bold text-slate-700 mb-3">项目经营详情 <span class="text-sm text-slate-500 font-normal ml-3">所有项目产生的签单,确收,毛利,回款,当期成本,当期流出,现金流</span></h3><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + projThead + '</thead><tbody>' + projTbody + '</tbody></table></div>', 'p-4');
|
||||
|
||||
// 视图切换按钮
|
||||
var viewBtns = '<div class="flex items-center gap-2 mb-2"><button onclick="setOverviewView(\'monthly\')" class="px-3 py-0.5 rounded text-sm font-medium ' + (view === 'monthly' ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200') + '">月报总览</button><button onclick="setOverviewView(\'quarterly\')" class="px-3 py-0.5 rounded text-sm font-medium ' + (view === 'quarterly' ? 'bg-blue-600 text-white' : 'bg-slate-100 text-slate-600 hover:bg-slate-200') + '">季报总览</button></div>';
|
||||
|
||||
container.innerHTML = '<div class="grid gap-2.5">' + viewBtns + deptCard + cfCard + projCard + '</div>';
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
}
|
||||
|
||||
window.setOverviewView = (v) => {
|
||||
state.overviewView = v;
|
||||
renderOverviewPlan();
|
||||
};
|
||||
window.setOverviewView = (v) => { state.overviewView = v; renderOverviewPlan(); };
|
||||
|
||||
// ====== 损益报表 ======
|
||||
|
||||
function renderDeptMatrix() {
|
||||
var c = document.querySelector("#overview_plan");
|
||||
if (!c) return;
|
||||
c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载损益矩阵...</div>';
|
||||
fetch('/api/overview/dept-matrix').then(r => r.json()).then(function(data) { renderDeptMatrixData(data, c); })
|
||||
.catch(function() { c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载失败</div>'; });
|
||||
}
|
||||
|
||||
function renderDeptMatrixData(data, container) {
|
||||
var mf = function(v) { return Math.round(Number(v || 0)).toLocaleString("zh-CN"); };
|
||||
var tenants = data.tenants;
|
||||
var sn = tenants.map(function(t) { return t.replace('·无界', ''); });
|
||||
var mL = ['01月','02月','03月','04月','05月','06月','07月','08月','09月','10月','11月','12月'];
|
||||
|
||||
var vBtns = btnBar('matrix');
|
||||
var cards = '';
|
||||
for (var mi = 11; mi >= 0; mi--) {
|
||||
if (isAllZero(data, tenants, ['project_gross','dept_expense','profit'], mi)) continue;
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
|
||||
th += '</tr>';
|
||||
|
||||
var tb = matrixTbody(data, tenants, mi, ['project_gross','dept_expense','profit'], ['项目毛利','部门费用','利润'], mf);
|
||||
cards += '<div class="flex items-center gap-3"><div class="text-5xl font-bold text-blue-200" style="width:120px;text-align:center;white-space:nowrap">' + mL[mi] + '</div><div class="card p-4 flex-1"><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + th + '</thead><tbody>' + tb + '</tbody></table></div></div></div>';
|
||||
}
|
||||
container.innerHTML = '<div class="grid gap-2.5">' + vBtns + '<div class="flex flex-col gap-2">' + cards + '</div></div>';
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
}
|
||||
|
||||
// ====== 现金流报表 ======
|
||||
|
||||
function renderCfMatrix() {
|
||||
var c = document.querySelector("#overview_plan");
|
||||
if (!c) return;
|
||||
c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载现金流矩阵...</div>';
|
||||
fetch('/api/overview/dept-cf-matrix').then(r => r.json()).then(function(data) { renderCfMatrixData(data, c); })
|
||||
.catch(function() { c.innerHTML = '<div class="p-3 text-center text-slate-400 text-sm">加载失败</div>'; });
|
||||
}
|
||||
|
||||
function renderCfMatrixData(data, container) {
|
||||
var mf = function(v) { return Math.round(Number(v || 0)).toLocaleString("zh-CN"); };
|
||||
var tenants = data.tenants;
|
||||
var sn = tenants.map(function(t) { return t.replace('·无界', ''); });
|
||||
var mL = ['01月','02月','03月','04月','05月','06月','07月','08月','09月','10月','11月','12月'];
|
||||
|
||||
var vBtns = btnBar('cf_matrix');
|
||||
var cards = '';
|
||||
for (var mi = 11; mi >= 0; mi--) {
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
|
||||
th += '</tr>';
|
||||
|
||||
if (isAllZero(data, tenants, ['project_cf','dept_cf','bl_cf'], mi)) continue;
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap">指标</th><th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">月度合计</th>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) { th += '<th class="py-1.5 text-right font-semibold text-slate-600 whitespace-nowrap">' + sn[ti] + '</th>'; }
|
||||
th += '</tr>';
|
||||
|
||||
var tb = matrixTbody(data, tenants, mi, ['project_cf','dept_cf','bl_cf'], ['项目现金流','部门现金流','经营现金流'], mf);
|
||||
cards += '<div class="flex items-center gap-3"><div class="text-5xl font-bold text-blue-200" style="width:120px;text-align:center;white-space:nowrap">' + mL[mi] + '</div><div class="card p-4 flex-1"><div class="overflow-x-auto"><table class="w-full text-sm"><thead>' + th + '</thead><tbody>' + tb + '</tbody></table></div></div></div>';
|
||||
}
|
||||
container.innerHTML = '<div class="grid gap-2.5">' + vBtns + '<div class="flex flex-col gap-2">' + cards + '</div></div>';
|
||||
if (window.lucide) window.lucide.createIcons();
|
||||
}
|
||||
|
||||
// ====== 共用工具 ======
|
||||
|
||||
function btnBar(active) {
|
||||
var btns = [];
|
||||
btns.push('<button onclick="setOverviewView(\'monthly\')" class="finance-tab' + (active === 'monthly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">月报总览</button>');
|
||||
btns.push('<button onclick="setOverviewView(\'quarterly\')" class="finance-tab' + (active === 'quarterly' ? ' active' : '') + '" style="font-size:14px;font-weight:600">季报总览</button>');
|
||||
btns.push('<button onclick="setOverviewView(\'matrix\')" class="finance-tab' + (active === 'matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">损益详细报表</button>');
|
||||
btns.push('<button onclick="setOverviewView(\'cf_matrix\')" class="finance-tab' + (active === 'cf_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">现金流详细报表</button>'); btns.push('<button onclick="setOverviewView(\'project_matrix\')" class="finance-tab' + (active === 'project_matrix' ? ' active' : '') + '" style="font-size:14px;font-weight:600">项目经营详细报表</button>');
|
||||
return '<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:12px">' + btns.join('') + '</div>';
|
||||
}
|
||||
|
||||
function matrixTbody(data, tenants, mi, keys, labels, mf) {
|
||||
var tb = '';
|
||||
for (var ri = 0; ri < keys.length; ri++) {
|
||||
var rowCls = ri === keys.length - 1 ? 'font-semibold' : '';
|
||||
var ts = 0;
|
||||
var vals = [];
|
||||
for (var ti = 0; ti < tenants.length; ti++) {
|
||||
var v = data.data[tenants[ti]][keys[ri]][mi] || 0;
|
||||
ts += v;
|
||||
vals.push(v);
|
||||
}
|
||||
var tCls = ts >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
var rh = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap">' + labels[ri] + '</td><td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td>';
|
||||
for (var ti = 0; ti < tenants.length; ti++) {
|
||||
var cls = vals[ti] >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
rh += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + mf(vals[ti]) + '</td>';
|
||||
}
|
||||
rh += '</tr>';
|
||||
tb += rh;
|
||||
}
|
||||
return tb;
|
||||
|
||||
|
||||
function isAllZero(data, tenants, keys, mi) {
|
||||
for (var ti = 0; ti < tenants.length; ti++) {
|
||||
for (var ki = 0; ki < keys.length; ki++) {
|
||||
if ((data.data[tenants[ti]][keys[ki]][mi] || 0) !== 0) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// ====== 项目经营详情报表 ======
|
||||
|
||||
|
||||
}
|
||||
@@ -306,11 +306,11 @@ function renderPlan() {
|
||||
{ key: 'gross', label: '毛利' },
|
||||
{ key: 'gross_rate', label: '毛利率' },
|
||||
{ key: 'payment', label: '回款' },
|
||||
{ key: 'pending', label: '待回款' },
|
||||
{ key: 'pay_rate', label: '回款率' },
|
||||
{ key: 'cost', label: '当期成本' },
|
||||
{ key: 'paid', label: '当期流出' },
|
||||
{ key: 'cashflow', label: '现金流' },
|
||||
{ key: 'pending', label: '待回款' },
|
||||
];
|
||||
const UNSIGNED_COLS = [
|
||||
{ key: 'client_name', label: '客户' },
|
||||
@@ -369,7 +369,7 @@ function renderPlan() {
|
||||
const cfCls = cashflow >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
const pending = rev - payment;
|
||||
const pendingCls = pending > 0 ? 'text-red-600' : pending < 0 ? 'text-green-600' : 'text-slate-400';
|
||||
const pendingVal = pending ? money(pending) : '<span class="text-slate-300">—</span>';
|
||||
const pendingVal = pending ? (pending > 0 ? '+' : '') + money(pending) : '<span class="text-slate-300">—</span>';
|
||||
var budgetArr = [];
|
||||
var expenseArr = [];
|
||||
try { budgetArr = JSON.parse(pf.budget_data || "[]"); } catch(e) {}
|
||||
@@ -392,7 +392,7 @@ function renderPlan() {
|
||||
var bCf = bPay - ePaid;
|
||||
return '<tr class="bg-slate-50"><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-slate-400">' + m + '</td><td class="px-2 py-px text-center text-xs text-blue-600">' + (bRev ? money(bRev) : '—') + '</td><td class="px-2 py-px text-center text-xs text-green-600">' + (bGross ? money(bGross) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-amber-600">' + (bPay ? money(bPay) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-rose-600">' + (eCost ? money(eCost) : '—') + '</td><td class="px-2 py-px text-center text-xs text-purple-600">' + (ePaid ? money(ePaid) : '—') + '</td><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs ' + (bCf >= 0 ? 'text-green-600' : 'text-red-600') + '">' + (bCf ? money(bCf) : '—') + '</td><td class="px-2 py-px text-center text-xs ' + (bRev - bPay > 0 ? 'text-red-600' : bRev - bPay < 0 ? 'text-green-600' : 'text-slate-300') + '">' + (bRev - bPay ? money(bRev - bPay) : '—') + '</td></tr>';
|
||||
}).join('');
|
||||
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="planOpenPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation();planToggleExpand(${pf.id})"><i data-lucide="chevron-right" style="width:14px;height:14px;color:#94a3b8" id="plan_expand_icon_${pf.id}"></i></td><td class="px-2 py-px text-center align-middle text-sm truncate" style="max-width:100px" title="${esc(pf.client_name || '')}">${esc(pf.client_name || "")}</td><td class="px-2 py-px text-sm font-medium text-center align-middle truncate" style="min-width:300px;max-width:300px;width:300px" title="${esc(pf.customer_name)}">${esc(pf.customer_name)}</td><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}</td><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center align-middle text-sm">${grossR}</td><td class="px-2 py-px text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="px-2 py-px text-center align-middle text-sm">${payR}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="px-2 py-px text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="px-2 py-px text-center font-semibold align-middle ${cfCls}">${cf}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${pendingCls}">${pendingVal}</td></tr><tr class="hidden" id="plan_expand_${pf.id}"><td colspan="15"><table class="w-full"><thead><tr class="border-b border-slate-200"><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">月份</th><th class="px-2 py-1 text-center text-xs text-slate-400">确收</th><th class="px-2 py-1 text-center text-xs text-slate-400">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">当期成本</th><th class="px-2 py-1 text-center text-xs text-slate-400">当期流出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-400">待回款</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
|
||||
return `<tr class="border-b border-slate-100 hover:bg-slate-50 cursor-pointer" onclick="planOpenPfEditModal(${pf.id})"><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation();planToggleExpand(${pf.id})"><i data-lucide="chevron-right" style="width:14px;height:14px;color:#94a3b8" id="plan_expand_icon_${pf.id}"></i></td><td class="px-2 py-px text-center align-middle text-sm truncate" style="max-width:100px" title="${esc(pf.client_name || '')}">${esc(pf.client_name || "")}</td><td class="px-2 py-px text-sm font-medium text-center align-middle truncate" style="min-width:300px;max-width:300px;width:300px" title="${esc(pf.customer_name)}">${esc(pf.customer_name)}</td><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('weight', pf.id, 'weight', pf.weight || '20%', ['20%','40%','60%','80%','100%'])}</td><td class="px-2 py-px text-center align-middle text-sm" onclick="event.stopPropagation()">${planInlineSelect('ptype', pf.id, 'project_type', pf.project_type || '待签约', ['待签约','已签约'])}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${money(pf.sign_amount)}</td><td class="px-2 py-px text-center text-blue-700 align-middle">${fmtNoUnit(rev)}</td><td class="px-2 py-px text-center align-middle text-sm font-medium">${fmtNoUnit(gross)}</td><td class="px-2 py-px text-center align-middle text-sm">${grossR}</td><td class="px-2 py-px text-center text-amber-700 align-middle">${fmtNoUnit(payment)}</td><td class="px-2 py-px text-center align-middle text-sm font-semibold ${pendingCls}">${pendingVal}</td><td class="px-2 py-px text-center align-middle text-sm">${payR}</td><td class="px-2 py-px text-center text-rose-700 align-middle">${fmtNoUnit(cost)}</td><td class="px-2 py-px text-center text-purple-700 align-middle">${fmtNoUnit(paid)}</td><td class="px-2 py-px text-center font-semibold align-middle ${cfCls}">${cf}</td></tr><tr class="hidden" id="plan_expand_${pf.id}"><td colspan="15"><table class="w-full"><thead><tr class="border-b border-slate-200"><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">月份</th><th class="px-2 py-1 text-center text-xs text-slate-400">确收</th><th class="px-2 py-1 text-center text-xs text-slate-400">毛利</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">回款</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">当期成本</th><th class="px-2 py-1 text-center text-xs text-slate-400">当期流出</th><th class="px-2 py-1"></th><th class="px-2 py-1 text-center text-xs text-slate-400">现金流</th><th class="px-2 py-1 text-center text-xs text-slate-400">待回款</th></tr></thead><tbody>${detailRows}</tbody></table></td></tr>`;
|
||||
};
|
||||
// 待签约简版行
|
||||
const tdRowUnsigned = (pf, cost, gross) => {
|
||||
@@ -1356,3 +1356,11 @@ window.planInlineUpdate = async (pfId, field, value) => {
|
||||
toast("更新失败:" + error.message, "error");
|
||||
}
|
||||
};
|
||||
|
||||
// ====== 筛选器控制函数 ======
|
||||
window.switchPlanFilter = (f) => { state.planFilter = f; renderPlan(); };
|
||||
window.setPlanView = (v) => { state.planView = v; renderPlan(); };
|
||||
window.setPlanYear = (y) => { state.planYear = parseInt(y); renderPlan(); };
|
||||
window.setPlanQuarter = (q) => { state.planView = 'quarterly'; state.planQuarter = q; renderPlan(); };
|
||||
window.setPlanMonth = (m) => { state.planView = 'monthly'; state.planMonth = m; renderPlan(); };
|
||||
window.setPlanStatusFilter = (f) => { state.planStatusFilter = f; renderPlan(); };
|
||||
|
||||
@@ -121,7 +121,7 @@ function renderTable(headers, rows, rowClicks) {
|
||||
}
|
||||
|
||||
var _loadingSteps = [
|
||||
{ fn: 'renderHome', label: '正在加载首页仪表盘(指标卡片+图表)', skip: function() { return state.tenant !== '总览'; } },
|
||||
{ fn: 'renderHome', label: '正在加载首页仪表盘(指标卡片+图表)', skip: function() { return true; } },
|
||||
{ fn: 'renderOverviewPlan', label: '正在加载预算总览', skip: function() { return state.tenant !== '总览'; } },
|
||||
{ fn: 'renderOverviewFinance', label: '正在加载发生总览', skip: function() { return state.tenant !== '总览'; } },
|
||||
{ fn: 'renderProjects', label: '正在加载业务机会列表' },
|
||||
@@ -193,7 +193,7 @@ async function load() {
|
||||
// 按需加载当前 tab 的数据(必须在 ensureStandardProposals 之前,否则会被 return 跳过)
|
||||
await ensureTabData(state.active);
|
||||
// 首次加载时确保标准资料库 7 项已初始化(仅首次,避免每次切换工作台都触发)
|
||||
if (!_proposalsInitialized && typeof ensureStandardProposals === "function") {
|
||||
if (!_proposalsInitialized && typeof ensureStandardProposals === "function" && state.tenant !== '总览') {
|
||||
_proposalsInitialized = true;
|
||||
try {
|
||||
const proposalsData = await api("/api/proposals/list?tenant=" + encodeURIComponent(state.tenant));
|
||||
@@ -282,7 +282,7 @@ function updateSidebarTabs() {
|
||||
document.querySelectorAll(".sidebar-tab").forEach((btn) => {
|
||||
const tab = btn.dataset.tab;
|
||||
if (isOverview) {
|
||||
btn.style.display = (tab === "home" || tab === "overview_plan" || tab === "overview_finance") ? "" : "none";
|
||||
btn.style.display = (tab === "overview_plan" || tab === "overview_finance") ? "" : "none";
|
||||
} else {
|
||||
btn.style.display = (tab === "overview_plan" || tab === "overview_finance" || tab === "home") ? "none" : "";
|
||||
}
|
||||
|
||||
@@ -53,11 +53,11 @@
|
||||
<i data-lucide="folder-open-dot" style="width:20px;height:20px"></i>
|
||||
<span class="text-[10px] mt-1">已发生</span>
|
||||
</div>
|
||||
<div class="sidebar-tab hidden" data-tab="overview_plan" onclick="switchTab('overview_plan')" title="预算总览">
|
||||
<div class="sidebar-tab" data-tab="overview_plan" onclick="switchTab('overview_plan')" title="预算总览">
|
||||
<i data-lucide="bar-chart-horizontal" style="width:20px;height:20px"></i>
|
||||
<span class="text-[10px] mt-1">预算总览</span>
|
||||
</div>
|
||||
<div class="sidebar-tab hidden" data-tab="overview_finance" onclick="switchTab('overview_finance')" title="发生总览">
|
||||
<div class="sidebar-tab" data-tab="overview_finance" onclick="switchTab('overview_finance')" title="发生总览">
|
||||
<i data-lucide="pie-chart" style="width:20px;height:20px"></i>
|
||||
<span class="text-[10px] mt-1">发生总览</span>
|
||||
</div>
|
||||
@@ -84,7 +84,7 @@
|
||||
<header class="topbar border-b border-slate-200 bg-white px-8 py-5">
|
||||
<div class="flex items-center gap-3 w-full">
|
||||
<div class="flex-1">
|
||||
<p class="eyebrow text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">OPC Manager <span class="ml-2 text-xs font-normal text-slate-400 tracking-normal">v1.2.0.24</span></p>
|
||||
<p class="eyebrow text-xs font-semibold uppercase tracking-[0.18em] text-blue-700">OPC Manager <span class="ml-2 text-xs font-normal text-slate-400 tracking-normal">v1.2.0.32</span></p>
|
||||
<div class="flex items-center gap-4 mt-1">
|
||||
<h1 class="text-2xl font-semibold" id="workspaceTitle">科普 OPC 工作台</h1>
|
||||
<div class="hidden sm:flex items-center gap-1.5 bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-100 rounded-full px-4 py-1.5">
|
||||
|
||||
Reference in New Issue
Block a user