Compare commits

...

108 Commits

Author SHA1 Message Date
mac
9c2beee51e Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-11 15:43:43 +08:00
mac
c5be3ce134 style: 总览模块所有表格指标列固定宽度100px
- thead和tbody中指标列添加min-width:100px
- 覆盖月报/季报/损益/现金流/项目5个模块
- 版本号 v1.2.0.33
2026-07-11 15:43:43 +08:00
mac
6a582c65b8 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-11 15:39:35 +08:00
mac
14368e83d0 fix: 月报/季报总览合同金额显示为0
- 后端新增sign_monthly[12]按月份拆分签单数据
- 月报/季报API均导出sign_monthly,季度转换包含sign_monthly
- 前端projVals首项改用sign_monthly替代硬编码0
- 版本号 v1.2.0.32
2026-07-11 15:39:35 +08:00
mac
3fff80425b Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-11 15:27:12 +08:00
mac
63ddffb12a fix: 补全finance/plan筛选器控制函数
- 新增setFinYear/setPlanYear等年份/季度/月度/视图切换函数
- 新增switchFinFilter/switchPlanFilter tab切换函数
- 新增setPlanStatusFilter状态筛选函数
- 版本号 v1.2.0.31
2026-07-11 15:27:12 +08:00
mac
02abe41793 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-11 15:23:16 +08:00
mac
2c7cd21095 fix: 待回款列移至回款后面,正数显示+前缀
- finance.js/plan.js SIGNED_COLS: 待回款列移到回款与回款率之间
- 行渲染同步调序, 补回意外删除的cost/paid/cf列定义
- 正数待回款显示为+金额
- 版本号 v1.2.0.30
2026-07-11 15:23:16 +08:00
mac
f30d40fe70 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-11 15:10:16 +08:00
mac
9273be3143 fix: 发生总览损益/现金流详细报表补上零值月份跳过逻辑
- renderFinanceDeptMatrixData和renderFinanceCfMatrixData添加isAllZero检查
- 与预算总览三个详细报表行为对齐
- 版本号 v1.2.0.29
2026-07-11 15:10:16 +08:00
mac
eb300731a3 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-11 15:06:23 +08:00
mac
2aab73a86a fix: 项目经营详细报表零值月份检查排除签约行
- isAllZero跳过sign(合同金额)行, 仅其他指标全零时隐藏月份
- 发生总览和预算总览均已同步
- 版本号 v1.2.0.28
2026-07-11 15:06:23 +08:00
mac
67bc00314c Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-11 14:54:28 +08:00
mac
e0000c2abb fix: 总览工作台首次加载跳过提案初始化避免卡顿
- 总览为虚拟聚合视图无proposal数据, 首次加载时missingCount=7触发ensureStandardProposals
- 新增state.tenant!=='总览'条件跳过提案初始化
- 版本号 v1.2.0.27
2026-07-11 14:54:28 +08:00
mac
d34e52eff4 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 17s
2026-07-11 14:36:00 +08:00
mac
356c129668 feat: 总览工作台新增损益/现金流/项目经营详细报表
- 预算总览和发生总览各新增3个tab: 损益详细报表、现金流详细报表、项目经营详细报表
- 12个月卡片布局, 按部门(学术/科普/科研/医患/MCN)展示矩阵
- 后端新增6个聚合API: dept-matrix/cf-matrix/project-matrix x 2
- tab改为finance-tab风格, 与经营月报一致
- 修复项目报表函数未注册导致的加载失败
- 版本号 v1.2.0.26
2026-07-11 14:36:00 +08:00
mac
8e38f50c99 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-11 12:31:50 +08:00
mac
94b9f25938 fix: 总览工作台tab显示问题
- 移除hidden class避免CSS覆盖style.display
- 总览模式下隐藏部门和预算/已发生tab
- 只显示预算总览和发生总览
- 版本号 v1.2.0.25
2026-07-11 12:31:50 +08:00
mac
3a40e78c3e Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-11 12:26:29 +08:00
mac
a1a4f41d79 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-11 11:57:42 +08:00
mac
d4889d56ec Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 17s
2026-07-11 11:50:18 +08:00
mac
35706f123d Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-11 11:44:05 +08:00
mac
e4d1cd59f3 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-10 23:47:54 +08:00
mac
1673603b02 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-10 23:22:12 +08:00
mac
db93eb8d31 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-10 23:02:48 +08:00
mac
94ad7aed55 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 16s
2026-07-10 22:32:27 +08:00
mac
043938670a Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-10 21:43:19 +08:00
mac
9862d9fe56 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-10 21:19:53 +08:00
mac
150e627811 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-10 20:14:53 +08:00
mac
295bf5a81c Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-10 20:06:03 +08:00
mac
e8a26059d5 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-10 19:52:00 +08:00
mac
543d620384 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-10 19:49:11 +08:00
mac
975034c589 feat: 新增测试工作台+tip样式调整+tip/tab位置对调
- 新增测试·无界工作台(所有人可见)
- 为所有opc_owner用户添加测试工作台权限
- 预算模块tip改淡黄色+新文案
- 预算/已发生模块tip和tab位置对调(tip在上)
- 版本号 v1.2.0.12
2026-07-10 19:49:11 +08:00
mac
71e90bd55f Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 17s
2026-07-10 19:32:00 +08:00
mac
3042791a90 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-10 19:11:32 +08:00
mac
a8243acc5a Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 21s
2026-07-10 18:22:54 +08:00
mac
cb0e3fdac2 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-10 17:33:05 +08:00
mac
f5c92d09af Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-10 13:38:11 +08:00
mac
e18626105b Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 16s
2026-07-10 13:27:22 +08:00
mac
b587530437 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-10 13:12:42 +08:00
mac
9a1260db68 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-10 13:10:04 +08:00
mac
7c33bc05f2 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-09 18:18:03 +08:00
mac
9de0a63be2 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-09 17:41:01 +08:00
mac
f3c94c57d6 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-09 16:52:35 +08:00
mac
9db627770b Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 16s
2026-07-09 16:33:49 +08:00
mac
75b988c77a Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-09 16:20:21 +08:00
mac
2b7f67436e Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-09 16:03:30 +08:00
mac
ed5455f309 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-08 20:48:52 +08:00
mac
452d410843 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-08 18:55:27 +08:00
mac
c95373b85e Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 20s
2026-07-08 17:45:48 +08:00
mac
6be10481d5 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 14s
2026-07-08 17:04:47 +08:00
mac
4ce0e66d90 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-08 16:49:11 +08:00
mac
0f9e1b6cf2 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-08 16:29:30 +08:00
mac
dc8d3d59d2 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-08 16:14:51 +08:00
mac
1e5c5b5706 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-08 16:05:45 +08:00
mac
b68144ebb0 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-08 15:52:45 +08:00
mac
3cae1d85cf Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 15s
2026-07-08 15:11:26 +08:00
mac
ca0cd9eab1 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-08 14:50:14 +08:00
mac
2bdc0e0779 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-08 14:16:31 +08:00
mac
93dca56c3f Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-08 13:46:10 +08:00
mac
8c4f50fc52 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-08 13:21:54 +08:00
mac
13864f1968 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-08 13:04:51 +08:00
mac
d11e8f9313 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-08 13:00:17 +08:00
mac
02a12a208c Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 14s
2026-07-08 12:23:32 +08:00
mac
b977627895 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-08 12:22:02 +08:00
mac
edf8e6179c Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-08 12:13:07 +08:00
mac
3148af05b3 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-08 11:42:07 +08:00
mac
4029933ada Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-08 11:34:00 +08:00
mac
95d3f9809c Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-07 19:22:08 +08:00
mac
8b040489e2 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-07 19:19:46 +08:00
mac
db9700c8ee Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-07 19:14:07 +08:00
mac
11e30f33b9 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-07 19:11:42 +08:00
mac
95f52b61d0 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-07 19:05:20 +08:00
mac
a4012f2189 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-07 19:03:00 +08:00
mac
e1c5e65c1e Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-07 19:00:53 +08:00
mac
a3d78b3ca0 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-07 18:44:10 +08:00
mac
9dc5631574 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-07 18:39:11 +08:00
mac
c5a59508d2 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-07 18:34:47 +08:00
mac
ab1d57492c Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-07 18:01:04 +08:00
mac
9a81a30feb Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 18s
2026-07-07 17:02:47 +08:00
mac
d6d21b03dd Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-07 15:56:13 +08:00
mac
b9a05e8732 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-07 13:15:54 +08:00
mac
440707e19a Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-07 12:56:27 +08:00
mac
e0dc1a56b2 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-06 21:42:24 +08:00
mac
f45afd954d Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-06 21:34:48 +08:00
mac
664da22329 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-06 20:58:19 +08:00
mac
73a6ac126c Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-06 19:34:24 +08:00
mac
878e26d4ca Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-06 19:19:41 +08:00
mac
e12ed0936b Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-06 19:10:18 +08:00
mac
8edb74f173 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-06 18:20:38 +08:00
mac
1a42c6e6f3 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-06 17:31:40 +08:00
mac
d21497bce3 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-06 15:39:56 +08:00
mac
574e3c5700 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-06 13:05:01 +08:00
mac
c70293b447 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-03 19:06:57 +08:00
mac
adeff08827 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-02 20:10:45 +08:00
mac
8bd40de41d Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-02 18:30:24 +08:00
mac
ac6eacea82 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-02 17:55:40 +08:00
mac
83c2a5ca94 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-02 14:50:45 +08:00
mac
fbd2290d29 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-02 14:31:06 +08:00
mac
97fcf88c61 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-06-29 17:02:55 +08:00
mac
a01afef599 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-06-29 16:52:37 +08:00
mac
28fa244fe5 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-06-29 16:45:21 +08:00
mac
ad3885e0be Merge branch 'dev'
Some checks failed
Deploy / deploy (push) Failing after 23s
2026-06-29 16:02:10 +08:00
mac
0fb7ee2992 Merge branch 'dev'
Some checks failed
Deploy / deploy (push) Failing after 14m52s
2026-06-26 12:21:02 +08:00
mac
f6792cad39 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 10s
2026-06-24 12:42:56 +08:00
mac
bed6e9192a Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-06-23 23:12:03 +08:00
mac
aaa213a765 fix(deploy): 修复 data 目录不存在导致 ln 软链失败
All checks were successful
Deploy / deploy (push) Successful in 10s
rsync 排除了 data/uploads 但没保留 data/ 空目录,
导致 ln -sfn shared/uploads data/uploads 时父目录不存在而失败。
在 ln 之前加 mkdir -p 确保目录存在。
2026-06-23 23:07:25 +08:00
Deploy Test
207629a9bb test: trigger workflow debug
Some checks failed
Deploy / deploy (push) Failing after 0s
2026-06-23 23:03:22 +08:00
8 changed files with 800 additions and 66 deletions

View File

@@ -53,3 +53,4 @@ curl http://127.0.0.1:5177/api/health
- 产品:慰心斋产品路线图中的 5 个产品版本
- 财务:首版财务样例和原财务 manager 合并方向
# test trigger

View File

@@ -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"])

View File

@@ -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(); };

View File

@@ -1,39 +1,124 @@
// 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" style="min-width:100px">指标</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" style="min-width:100px">指标</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" style="min-width:100px">' + 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>';
var deptThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</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,9 +129,8 @@ function renderOverviewFinanceData(data, view) {
];
var deptTbody = '';
for (var dri = 0; dri < 3; dri++) {
var dr = deptRows[dri];
var 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 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" style="min-width:100px">' + 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>';
for (var i = 0; i < cols; i++) {
@@ -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,9 +148,8 @@ function renderOverviewFinanceData(data, view) {
];
var cfTbody = '';
for (var cfri = 0; cfri < 3; cfri++) {
var cfr = cfRows[cfri];
var 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 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" style="min-width:100px">' + 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>';
for (var i = 0; i < cols; i++) {
@@ -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>';
var projThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</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>';
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" style="min-width:100px">' + LABELS[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" style="min-width:100px">指标</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" style="min-width:100px">指标</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" style="min-width:100px">' + 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;
}

View File

@@ -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" style="min-width:100px">指标</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" style="min-width:100px">指标</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" style="min-width:100px">' + 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,8 +104,16 @@ 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>';
var deptThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</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>';
@@ -43,7 +126,7 @@ function renderOverviewPlanData(data, view) {
for (var dri = 0; dri < 3; dri++) {
var dr = deptRows[dri];
var 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 rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + rowCls + ' text-slate-700 whitespace-nowrap" style="min-width:100px">' + 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>';
for (var i = 0; i < cols; i++) {
@@ -64,7 +147,7 @@ function renderOverviewPlanData(data, view) {
for (var cfri = 0; cfri < 3; cfri++) {
var cfr = cfRows[cfri];
var 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 rowHtml = '<tr class="border-b border-slate-100 last:border-0"><td class="py-1.5 text-left ' + cfRowCls + ' text-slate-700 whitespace-nowrap" style="min-width:100px">' + 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>';
for (var i = 0; i < cols; i++) {
@@ -77,19 +160,19 @@ 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>';
var projThead = '<tr class="border-b border-slate-200"><th class="py-1.5 text-left text-slate-500 whitespace-nowrap" style="min-width:100px">指标</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>';
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" style="min-width:100px">' + LABELS[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" style="min-width:100px">指标</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" style="min-width:100px">指标</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" style="min-width:100px">指标</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" style="min-width:100px">' + 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;
}
// ====== 项目经营详情报表 ======
}

View File

@@ -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(); };

View File

@@ -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" : "";
}

View File

@@ -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.33</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">