Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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")
|
||||
|
||||
@@ -1,38 +1,105 @@
|
||||
// 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 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 = 0; mi < 12; mi++) {
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 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 += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">合计</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-slate-300" 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 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>';
|
||||
var ts = 0;
|
||||
for (var ti = 0; ti < tenants.length; ti++) {
|
||||
var v = data.data[tenants[ti]][keys[ri]][mi] || 0;
|
||||
ts += v;
|
||||
var cls = 'text-slate-700';
|
||||
if (ri === 2) cls = 'text-green-600';
|
||||
if (ri === keys.length - 1) cls = v >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
rh += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + mf(v) + '</td>';
|
||||
}
|
||||
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';
|
||||
rh += '<td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td></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 +111,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 +123,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 +130,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,15 +142,12 @@ 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 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>';
|
||||
@@ -106,7 +167,91 @@ 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 = 0; mi < 12; mi++) {
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 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 += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">合计</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-slate-300" 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 = 0; mi < 12; mi++) {
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 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 += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">合计</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-slate-300" 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 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>';
|
||||
var ts = 0;
|
||||
for (var ti = 0; ti < tenants.length; ti++) {
|
||||
var v = data.data[tenants[ti]][keys[ri]][mi] || 0;
|
||||
ts += v;
|
||||
var cls = v >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
rh += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + mf(v) + '</td>';
|
||||
}
|
||||
var tCls = ts >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
rh += '<td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td></tr>';
|
||||
tb += rh;
|
||||
}
|
||||
return tb;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,84 @@
|
||||
// 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 = 0; mi < 12; mi++) {
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 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 += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">合计</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-slate-300" 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 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>';
|
||||
var ts = 0;
|
||||
for (var ti = 0; ti < tenants.length; ti++) {
|
||||
var v = data.data[tenants[ti]][keys[ri]][mi] || 0;
|
||||
ts += v;
|
||||
var cls = 'text-slate-700';
|
||||
if (ri === 2) cls = 'text-green-600';
|
||||
if (ri === keys.length - 1) cls = v >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
rh += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + mf(v) + '</td>';
|
||||
}
|
||||
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';
|
||||
rh += '<td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td></tr>';
|
||||
tb += rh;
|
||||
}
|
||||
return tb;
|
||||
}
|
||||
|
||||
function renderOverviewPlanData(data, view) {
|
||||
const container = document.querySelector("#overview_plan");
|
||||
@@ -29,6 +95,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>'; }
|
||||
@@ -78,7 +152,7 @@ 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 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>'; }
|
||||
@@ -99,14 +173,102 @@ 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 = 0; mi < 12; mi++) {
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 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 += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">合计</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-slate-300" 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 = 0; mi < 12; mi++) {
|
||||
var th = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 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 += '<th class="py-1.5 text-right font-semibold text-slate-700 whitespace-nowrap">合计</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-slate-300" 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 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>';
|
||||
var ts = 0;
|
||||
for (var ti = 0; ti < tenants.length; ti++) {
|
||||
var v = data.data[tenants[ti]][keys[ri]][mi] || 0;
|
||||
ts += v;
|
||||
var cls = v >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
rh += '<td class="py-1.5 text-right text-sm whitespace-nowrap ' + cls + '">' + mf(v) + '</td>';
|
||||
}
|
||||
var tCls = ts >= 0 ? 'text-green-600' : 'text-red-600';
|
||||
rh += '<td class="py-1.5 text-right font-semibold text-sm whitespace-nowrap ' + tCls + '">' + mf(ts) + '</td></tr>';
|
||||
tb += rh;
|
||||
}
|
||||
return tb;
|
||||
|
||||
// ====== 项目经营详情报表 ======
|
||||
|
||||
|
||||
}
|
||||
@@ -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: '正在加载业务机会列表' },
|
||||
@@ -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.26</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