Compare commits

...

120 Commits

Author SHA1 Message Date
mac
fd4baffc28 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-17 17:23:19 +08:00
mac
2e5704a615 feat: 方案→wiki + 项目详情简化 + 去除活动/跟进
- 侧边栏「方案」改为 wiki,移至最后
- 项目详情抽屉隐藏负责人、截止时间、金额
- 项目详情去除活动/跟进区域
2026-07-17 17:23:19 +08:00
mac
65f1487add Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-17 17:08:56 +08:00
mac
d4fb9a0349 feat: 绩效模块重构 — 目标自动统计 + 年/季/月筛选 + 完成率列
- 目标自动从 plan_finances 季度预算统计(签约/收入/毛利/回款)
- 完成情况自动从 project_finances 统计
- 筛选区对齐已发生模块:年份下拉 | 全年 | Q1-Q4 | 月度
- 新增完成率列(完成/目标×100%),颜色分级绿/琥珀/红
- 目标列改为只读纯显示
- 支持年度、季度、月度三种视图切换
- lazy loading 加载 planFinances + projectFinances 数据
2026-07-17 17:08:56 +08:00
mac
056933110d Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 14s
2026-07-17 14:40:29 +08:00
mac
56b8bfbf84 fix: 删除 finance.js/plan.js 明细行中残留的 exp 变量引用
- plan.js 第403行 var eCost = parseFloat(exp.cost...) 残留引用已删
- finance.js 第385行 var eCost = parseFloat(exp.cost...) 残留引用已删
- exp 变量已在上一版聚合改造中移除,残留引用导致 ReferenceError
2026-07-17 14:40:29 +08:00
mac
ad4f6a7c20 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-17 14:03:21 +08:00
mac
d3a7252e26 fix: 同月多条费用被 Array.find 丢弃导致数据不完整
- plan.js/finance.js 所有 ed.find 改为 filter+forEach 聚合同月 cost/paid
- computePlanMonthly 同步修复 bd.find 和 ed.find
- 明细行(expand panel) 的双重 find 也全部聚合
- 月度视图的 else 分支同改
2026-07-17 14:03:21 +08:00
mac
21b5c40626 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-15 14:49:18 +08:00
mac
3930451ce8 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-15 10:03:12 +08:00
mac
e2a421cde7 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-14 15:25:38 +08:00
mac
aa3ec75179 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-13 16:34:57 +08:00
mac
4ad19d438f Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-13 14:57:49 +08:00
mac
30b1a50490 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-13 13:47:56 +08:00
mac
01718e9acc Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-13 13:41:48 +08:00
mac
87f0abbbd8 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 11s
2026-07-13 09:15:18 +08:00
mac
c2ed20caeb Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-12 21:48:12 +08:00
mac
a3f269046d Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
2026-07-12 17:23:28 +08:00
mac
ec062fb2ec Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 14s
2026-07-12 17:13:06 +08:00
mac
c4af452115 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 13s
2026-07-12 17:05:03 +08:00
mac
8c318fb3ad Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 15s
2026-07-12 16:47:25 +08:00
mac
9c2beee51e Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
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
3fff80425b Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
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
f30d40fe70 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
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
67bc00314c Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
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
8e38f50c99 Merge branch 'dev'
All checks were successful
Deploy / deploy (push) Successful in 12s
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
7 changed files with 198 additions and 63 deletions

View File

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

View File

@@ -20,7 +20,7 @@ function openDrawer(resource, id) {
const fields = resource === "sales"
? [["target_customer","业务机会"],["priority","优先级"],["status","状态"]]
: resource === "operations"
? [["project_name","项目名称"],["owner","负责人"],["expected_sign_date","截止时间"],["expected_contract_amount","金额"],["notes","项目说明"]]
? [["project_name","项目名称"],["notes","项目说明"]]
: resource === "proposals"
? [["customer_or_project_name","客户/项目"],["proposal_type","方案类型"],["notes","备注"]]
: [["product_name","版本名称"],["version","版本号"],["priority","优先级"],["version_goal","版本目标"],["start_date","启动时间"],["plan_date","产品方案"],["dev_done_date","研发完成"],["test_date","测试完成"],["launch_date","上线时间"],["notes","进展备注"]];
@@ -36,7 +36,7 @@ function openDrawer(resource, id) {
test_date: "bug", devs: "users", testers: "shield-check"
};
const multilineFields = ["customer_need", "current_deliverable", "risks", "next_action", "version_goal", "feature_list", "notes"];
const followupTarget = resource === "sales" ? "sales" : resource === "proposals" ? "proposal" : resource === "operations" ? "operation" : resource === "products" ? "product" : "";
const followupTarget = resource === "sales" ? "sales" : resource === "proposals" ? "proposal" : resource === "products" ? "product" : "";
const title = esc(item.target_customer || item.project_name || (item.customer_or_project_name ? `${item.customer_or_project_name} · ${item.proposal_type || ''}` : "") || item.product_name);
const titleForAttr = esc(item.target_customer || item.project_name || (item.customer_or_project_name ? `${item.customer_or_project_name} · ${item.proposal_type || ''}` : "") || item.product_name);
drawer.innerHTML = `<div class="drawer-panel"><div class="sticky top-0 z-10 flex items-center justify-between border-b border-slate-200 bg-white/95 px-5 py-3 backdrop-blur"><div><p class="text-[10px] font-semibold uppercase tracking-[0.18em] text-slate-400">Detail Drawer</p><div class="flex items-center gap-2"><h2 class="drawer-title text-[17px] font-semibold leading-6 text-slate-900">${title}</h2><span id="drawerSaveStatus" class="save-status"></span></div></div><div class="flex items-center gap-2"><button class="btn btn-ghost btn-sm text-red-600 hover:bg-red-50" onclick="deleteDrawerItem('${resource}', ${id})"><i data-lucide="trash-2"></i>删除</button><button class="btn btn-ghost btn-sm" onclick="closeDrawer()">关闭</button></div></div><div class="grid gap-5 p-5">

View File

@@ -367,15 +367,21 @@ function renderFinance() {
expenseArr.forEach(function(e) { allMonths[e.month || ''] = true; });
var detailRows = Object.keys(allMonths).filter(function(m) {
var b = budgetArr.find(function(x) { return x.month === m; }) || {};
var exp = expenseArr.find(function(x) { return x.month === m; }) || {};
var bRev = parseFloat(b.rev || 0), bGross = parseFloat(b.gross || 0), bPay = parseFloat(b.payment || 0);
var eCost = parseFloat(exp.cost || 0), ePaid = parseFloat(exp.paid || 0);
var eCost = 0, ePaid = 0;
expenseArr.filter(function(x) { return x.month === m; }).forEach(function(e) {
eCost += parseFloat(e.cost || 0) || 0;
ePaid += parseFloat(e.paid || 0) || 0;
});
return bRev || bGross || bPay || eCost || ePaid;
}).sort().map(function(m) {
var b = budgetArr.find(function(x) { return x.month === m; }) || {};
var exp = expenseArr.find(function(x) { return x.month === m; }) || {};
var eCost = 0, ePaid = 0;
expenseArr.filter(function(x) { return x.month === m; }).forEach(function(e) {
eCost += parseFloat(e.cost || 0) || 0;
ePaid += parseFloat(e.paid || 0) || 0;
});
var bRev = parseFloat(b.rev || 0), bGross = parseFloat(b.gross || 0), bPay = parseFloat(b.payment || 0);
var eCost = parseFloat(exp.cost || 0), ePaid = parseFloat(exp.paid || 0);
var bCf = bPay - ePaid;
return '<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-600">' + m + '</td><td class="px-2 py-px text-center text-xs text-brand-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('');
@@ -405,12 +411,16 @@ function renderFinance() {
rev = Math.round(rev); payment = Math.round(payment); gross = Math.round(gross); cost = Math.round(cost); paid = Math.round(paid);
} else {
const b = bd.find(x => (x.month || "") === selMonth) || {};
const e = ed.find(x => (x.month || "") === selMonth) || {};
let cost2 = 0, paid2 = 0;
ed.filter(x => (x.month || "") === selMonth).forEach(e => {
cost2 += parseFloat(e.cost || 0) || 0;
paid2 += parseFloat(e.paid || 0) || 0;
});
rev = Math.round(parseFloat(b.rev || 0) || 0);
payment = Math.round(parseFloat(b.payment || 0) || 0);
gross = Math.round(parseFloat(b.gross || 0) || 0);
cost = Math.round(parseFloat(e.cost || 0) || 0);
paid = Math.round(parseFloat(e.paid || 0) || 0);
cost = Math.round(cost2);
paid = Math.round(paid2);
}
if (!rev && !payment && !cost && !paid && !gross) return;
dataItems.push({

View File

@@ -3,35 +3,93 @@ window.renderPerformance = () => {
const data = state.data;
if (!data) return;
// 确保预算和已发生数据已加载
if (!data._perfDataLoaded) {
loadPerfData().then(() => { data._perfDataLoaded = true; renderPerformance(); });
return;
}
// ── 状态初始化 ──
if (state.planYear == null) state.planYear = new Date().getFullYear();
const planYear = state.planYear;
if (!['annual','quarterly','monthly'].includes(state.perfView)) state.perfView = 'quarterly';
if (state.perfQuarter === undefined) state.perfQuarter = Math.floor(new Date().getMonth() / 3);
const q = state.perfQuarter;
const qLabel = ['Q1 (1-3月)', 'Q2 (4-6月)', 'Q3 (7-9月)', 'Q4 (10-12月)'];
const nowD = new Date();
const activeQ = state.perfQuarter;
const qRanges = [[1,2,3],[4,5,6],[7,8,9],[10,11,12]];
const qRange = qRanges[q];
const storageKey = 'opc-performance-Q' + (q + 1);
const activeMonths = qRanges[activeQ];
const qLabels = ['Q1','Q2','Q3','Q4'];
const pfs = (data.projectFinances || []);
// 当前聚合范围
let aggMonths = [];
if (state.perfView === 'annual') {
for (var mi = 1; mi <= 12; mi++) aggMonths.push(mi);
} else if (state.perfView === 'quarterly') {
aggMonths = qRanges[activeQ];
} else if (state.perfView === 'monthly') {
const m = state.perfMonth;
aggMonths = m ? [parseInt(m.substring(5))] : [];
}
const sumQ = (field) => {
// 月度选择器默认值
if (state.perfView === 'monthly' && !state.perfMonth) {
state.perfMonth = planYear + '-' + String(nowD.getMonth() + 1).padStart(2, '0');
}
// ── 数据源 ──
const pfs = (data.projectFinances || []); // 已发生 → 完成情况
const planPfs = (data.planFinances || []); // 预算 → 目标
// ── 通用聚合(支持年份过滤 + 月度范围)──
function sumByMonths(items, field, useSign, months) {
let total = 0;
pfs.forEach(p => {
try { JSON.parse(p.budget_data || '[]').forEach(b => { const m = parseInt((b.month || '').substring(5)) || 0; if (qRange.includes(m)) total += parseFloat(b[field] || 0); }); } catch (e) {}
items.forEach(p => {
if (useSign) {
const sm = p.sign_month || '';
const smY = parseInt(sm.substring(0, 4)) || 0;
const smM = parseInt(sm.substring(5)) || 0;
if (!months.includes(smM)) return;
if (planYear !== 0 && smY && smY !== planYear) return;
total += parseFloat(p.sign_amount || 0);
} else {
let bd = [];
try { bd = JSON.parse(p.budget_data || '[]'); } catch (e) {}
bd.forEach(b => {
const m = parseInt((b.month || '').substring(5)) || 0;
const y = parseInt((b.month || '').substring(0, 4)) || 0;
if (!months.includes(m)) return;
if (planYear !== 0 && y && y !== planYear) return;
total += parseFloat(b[field] || 0);
});
}
});
return Math.round(total);
};
}
const signTotal = Math.round(pfs.filter(x => { const m = parseInt((x.sign_month || '0').substring(5)) || 0; return qRange.includes(m); }).reduce((a, p) => a + (p.sign_amount || 0), 0));
const revTotal = sumQ('rev');
const grossTotal = sumQ('gross');
const paymentTotal = sumQ('payment');
// ── 计算 ──
const signActual = sumByMonths(pfs, '', true, aggMonths);
const revActual = sumByMonths(pfs, 'rev', false, aggMonths);
const grossActual = sumByMonths(pfs, 'gross', false, aggMonths);
const paymentActual = sumByMonths(pfs, 'payment', false, aggMonths);
const signPlan = sumByMonths(planPfs, '', true, aggMonths);
const revPlan = sumByMonths(planPfs, 'rev', false, aggMonths);
const grossPlan = sumByMonths(planPfs, 'gross', false, aggMonths);
const paymentPlan = sumByMonths(planPfs, 'payment', false, aggMonths);
const rows = [
{ key: 'sign', label: '签约', complete: signTotal, icon: 'file-text', defWeight: 10 },
{ key: 'rev', label: '收入', complete: revTotal, icon: 'dollar-sign', defWeight: 20 },
{ key: 'gross', label: '毛利', complete: grossTotal, icon: 'trending-up', defWeight: 50 },
{ key: 'payment', label: '回款', complete: paymentTotal, icon: 'coins', defWeight: 20 },
{ key: 'sign', label: '签约', complete: signActual, plan: signPlan, icon: 'file-text', defWeight: 10 },
{ key: 'rev', label: '收入', complete: revActual, plan: revPlan, icon: 'dollar-sign', defWeight: 20 },
{ key: 'gross', label: '毛利', complete: grossActual, plan: grossPlan, icon: 'trending-up', defWeight: 50 },
{ key: 'payment', label: '回款', complete: paymentActual, plan: paymentPlan, icon: 'coins', defWeight: 20 },
];
// 存储 key 基于当前视图模式
let storageKey = 'opc-perf-t'; // default annual
if (state.perfView === 'quarterly') storageKey = 'opc-perf-Q' + (activeQ + 1);
else if (state.perfView === 'monthly') storageKey = 'opc-perf-M' + (state.perfMonth || '');
const saved = JSON.parse(localStorage.getItem(storageKey) || '{}');
function val(key, field, def) {
@@ -40,27 +98,23 @@ window.renderPerformance = () => {
}
function moneyWan(v) {
if (v >= 10000) return (v / 10000).toFixed(1) + '万';
if (Math.abs(v) >= 10000) return (v / 10000).toFixed(1) + '万';
return v.toLocaleString();
}
let totalScore = 0;
const tableRows = rows.map(r => {
const targetWan = val(r.key, 'targetWan', 0);
const target = Math.round(targetWan * 10000);
const target = r.plan;
const weight = val(r.key, 'weight', r.defWeight);
const score = target > 0 ? Math.round((r.complete / target) * weight * 10) / 10 : 0;
const rate = target > 0 ? Math.round((r.complete / target) * 100) : 0;
totalScore += score;
return `<tr class="border-b border-slate-100">
<td class="p-3 align-middle text-left font-medium text-slate-700"><i data-lucide="${r.icon}" style="width:16px;height:16px;display:inline-block;vertical-align:-3px;margin-right:6px" class="text-brand-500"></i>${r.label}</td>
<td class="p-3 align-middle text-center">
<div class="inline-flex items-center gap-1">
<input type="number" step="0.1" min="0" value="${targetWan.toFixed(1)}" class="perf-target form-ctrl form-ctrl-sm w-20 text-center" style="appearance:none;-webkit-appearance:none" data-key="${r.key}" onchange="updatePerfScore('${r.key}','targetWan',this.value)">
<span class="text-xs text-slate-400">万</span>
</div>
</td>
<td class="p-3 align-middle text-center text-slate-700 font-mono text-sm">${moneyWan(target)}</td>
<td class="p-3 align-middle text-center text-slate-700 font-mono text-sm">${moneyWan(r.complete)}</td>
<td class="p-3 align-middle text-center font-semibold text-sm ${rate >= 100 ? 'text-green-600' : rate >= 50 ? 'text-amber-600' : 'text-red-500'}">${rate}%</td>
<td class="p-3 align-middle text-center">
<div class="inline-flex justify-center">
<input type="number" step="0.1" min="0" max="100" value="${weight}" class="perf-weight form-ctrl form-ctrl-sm w-16 text-center" style="appearance:none;-webkit-appearance:none" data-key="${r.key}" onchange="updatePerfScore('${r.key}','weight',this.value)">
@@ -70,9 +124,27 @@ window.renderPerformance = () => {
</tr>`;
}).join('');
const quarterTabs = `<div class="finance-tabs" style="padding:0;border-bottom:1px solid #e2e8f0;margin-bottom:0">` + qLabel.map((l, i) => {
return `<button onclick="switchPerfQuarter(${i})" class="finance-tab${i === q ? ' active' : ''}" style="font-size:14px;font-weight:600">${l}</button>`;
}).join('') + `</div>`;
// ── Header: 仿预算/已发生模块的年/季/月筛选区 ──
const yearOpts = [2020, 2021, 2022, 2023, 2024, 2025, 2026, 2027];
const yearSelect = `<span class="text-sm text-slate-500">年份:</span><select onchange="setPerfYear(this.value)" class="px-2 py-0.5 pr-6 rounded text-sm font-medium border border-slate-200 bg-white"><option value="0"${planYear===0?' selected':''}>所有年份</option>${yearOpts.map(y => `<option value="${y}" ${planYear===y?'selected':''}>${y}年</option>`).join('')}</select>`;
const viewBtns = `<button onclick="setPerfView('annual')" class="px-3 py-0.5 rounded text-sm font-medium ${state.perfView==='annual'?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">全年</button>`;
const quarterBtns = qLabels.map((ql, i) =>
`<button onclick="setPerfQuarter(${i})" class="px-3 py-0.5 rounded text-sm font-medium ${state.perfView==='quarterly'&&activeQ===i?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${ql}</button>`
).join('');
const monthBtns = activeMonths.map(m => {
const ms = planYear + '-' + String(m).padStart(2, '0');
return `<button onclick="setPerfMonth('${ms}')" class="px-3 py-0.5 rounded text-sm font-medium ${state.perfView==='monthly'&&state.perfMonth===ms?'bg-brand-600 text-white':'bg-slate-100 text-slate-600 hover:bg-slate-200'}">${m}月</button>`;
}).join('');
const headerBar = `<div class="flex items-center gap-2 flex-wrap">
${yearSelect}<span class="text-slate-300 mx-1">|</span>
${viewBtns}
<span class="text-slate-300 mx-1">|</span><span class="text-sm text-slate-500">季度:</span>${quarterBtns}
<span class="text-slate-300 mx-1">|</span><span class="text-sm text-slate-500">月度:</span>${monthBtns}
</div>`;
document.querySelector("#performance").innerHTML = `
<style>
@@ -83,13 +155,14 @@ window.renderPerformance = () => {
.perf-target, .perf-weight { -moz-appearance: textfield; }
</style>
<div class="grid gap-4">
${quarterTabs}
<div class="card py-2 px-4">${headerBar}</div>
<section class="card p-4">
<table class="w-full text-sm">
<thead><tr class="bg-slate-50 border-b border-slate-200">
<th class="p-3 text-left font-semibold text-slate-500">指标</th>
<th class="p-3 text-center font-semibold text-slate-500">目标(万)</th>
<th class="p-3 text-center font-semibold text-slate-500">完成情况</th>
<th class="p-3 text-center font-semibold text-slate-500">完成率</th>
<th class="p-3 text-center font-semibold text-slate-500">权重</th>
<th class="p-3 text-center font-semibold text-slate-500">评分</th>
</tr></thead>
@@ -98,6 +171,7 @@ window.renderPerformance = () => {
<td class="p-3 text-left text-slate-700">合计</td>
<td class="p-3 text-center text-slate-500">—</td>
<td class="p-3 text-center text-slate-500">—</td>
<td class="p-3 text-center text-slate-500">—</td>
<td class="p-3 text-center text-slate-700">100</td>
<td class="p-3 text-center text-brand-700 text-base">${totalScore.toFixed(1)}</td>
</tr></tfoot>
@@ -108,17 +182,47 @@ window.renderPerformance = () => {
if (window.lucide) window.lucide.createIcons();
};
window.switchPerfQuarter = (q) => {
// ── 筛选切换函数 ──
window.setPerfYear = (y) => {
state.planYear = parseInt(y);
renderPerformance();
};
window.setPerfView = (v) => {
state.perfView = v;
renderPerformance();
};
window.setPerfQuarter = (q) => {
state.perfView = 'quarterly';
state.perfQuarter = q;
renderPerformance();
};
window.setPerfMonth = (m) => {
state.perfView = 'monthly';
state.perfMonth = m;
renderPerformance();
};
// ── 编辑保存 ──
window.updatePerfScore = (key, field, value) => {
const q = state.perfQuarter || 0;
const storageKey = 'opc-performance-Q' + (q + 1);
let storageKey = 'opc-perf-t';
if (state.perfView === 'quarterly') storageKey = 'opc-perf-Q' + ((state.perfQuarter || 0) + 1);
else if (state.perfView === 'monthly') storageKey = 'opc-perf-M' + (state.perfMonth || '');
const saved = JSON.parse(localStorage.getItem(storageKey) || '{}');
saved[key] = saved[key] || {};
saved[key][field] = value;
localStorage.setItem(storageKey, JSON.stringify(saved));
renderPerformance();
};
// ── 数据加载 ──
async function loadPerfData() {
for (var i = 0; i < 2; i++) {
var r = i === 0 ? 'planFinances' : 'projectFinances';
try {
state.data[r] = await api("/api/" + r + "/list?tenant=" + encodeURIComponent(state.tenant));
} catch(e) { console.error('loadPerfData failed:', r, e.message); }
}
}

View File

@@ -383,16 +383,23 @@ function renderPlan() {
budgetArr.forEach(function(b) { allMonths[b.month || ''] = true; });
expenseArr.forEach(function(e) { allMonths[e.month || ''] = true; });
var detailRows = Object.keys(allMonths).filter(function(m) {
if (state.planYear !== 0 && parseInt(m.substring(0, 4)) !== state.planYear) return false;
var b = budgetArr.find(function(x) { return x.month === m; }) || {};
var exp = expenseArr.find(function(x) { return x.month === m; }) || {};
var bRev = parseFloat(b.rev || 0), bGross = parseFloat(b.gross || 0), bPay = parseFloat(b.payment || 0);
var eCost = parseFloat(exp.cost || 0), ePaid = parseFloat(exp.paid || 0);
var eCost = 0, ePaid = 0;
expenseArr.filter(function(x) { return x.month === m; }).forEach(function(e) {
eCost += parseFloat(e.cost || 0) || 0;
ePaid += parseFloat(e.paid || 0) || 0;
});
return bRev || bGross || bPay || eCost || ePaid;
}).sort().map(function(m) {
var b = budgetArr.find(function(x) { return x.month === m; }) || {};
var exp = expenseArr.find(function(x) { return x.month === m; }) || {};
var eCost = 0, ePaid = 0;
expenseArr.filter(function(x) { return x.month === m; }).forEach(function(e) {
eCost += parseFloat(e.cost || 0) || 0;
ePaid += parseFloat(e.paid || 0) || 0;
});
var bRev = parseFloat(b.rev || 0), bGross = parseFloat(b.gross || 0), bPay = parseFloat(b.payment || 0);
var eCost = parseFloat(exp.cost || 0), ePaid = parseFloat(exp.paid || 0);
var bCf = bPay - ePaid;
return '<tr class="bg-slate-50"><td class="px-2 py-px"></td><td class="px-2 py-px text-center text-xs text-slate-600">' + m + '</td><td class="px-2 py-px text-center text-xs text-brand-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('');
@@ -417,12 +424,17 @@ function renderPlan() {
let bd = []; try { bd = JSON.parse(pf.budget_data || "[]"); } catch (e) {}
let ed = []; try { ed = JSON.parse(pf.expense_data || "[]"); } catch (e) {}
const b = bd.find(x => (x.month || "") === selMonth) || {};
const e = ed.find(x => (x.month || "") === selMonth) || {};
const rev = Math.round(parseFloat(b.rev || 0) || 0);
const payment = Math.round(parseFloat(b.payment || 0) || 0);
const gross = Math.round(parseFloat(b.gross || 0) || 0);
const cost = Math.round(parseFloat(e.cost || 0) || 0);
const paid = Math.round(parseFloat(e.paid || 0) || 0);
let rev = Math.round(parseFloat(b.rev || 0) || 0);
let payment2 = Math.round(parseFloat(b.payment || 0) || 0);
let gross = Math.round(parseFloat(b.gross || 0) || 0);
let cost = 0, paid = 0;
ed.filter(x => (x.month || "") === selMonth).forEach(e => {
cost += parseFloat(e.cost || 0) || 0;
paid += parseFloat(e.paid || 0) || 0;
});
cost = Math.round(cost);
paid = Math.round(paid);
payment = payment2;
if (!rev && !payment && !cost && !paid && !gross) return;
dataItems.push({
pf: pf,
@@ -511,9 +523,15 @@ function renderPlan() {
let budget = []; try { budget = JSON.parse(pf.budget_data || "[]"); } catch (e) {}
let expense = []; try { expense = JSON.parse(pf.expense_data || "[]"); } catch (e) {}
let rev = 0, payment = 0, gross = 0;
budget.forEach(b => { rev += parseFloat(b.rev || 0) || 0; gross += parseFloat(b.gross || 0) || 0; payment += parseFloat(b.payment || 0) || 0; });
budget.forEach(b => {
if (state.planYear !== 0 && parseInt((b.month || '').substring(0, 4)) !== state.planYear) return;
rev += parseFloat(b.rev || 0) || 0; gross += parseFloat(b.gross || 0) || 0; payment += parseFloat(b.payment || 0) || 0;
});
let cost = 0, paid = 0;
expense.forEach(e => { cost += parseFloat(e.cost || 0) || 0; paid += parseFloat(e.paid || 0) || 0; });
expense.forEach(e => {
if (state.planYear !== 0 && parseInt((e.month || '').substring(0, 4)) !== state.planYear) return;
cost += parseFloat(e.cost || 0) || 0; paid += parseFloat(e.paid || 0) || 0;
});
return { rev: Math.round(rev), payment: Math.round(payment), cost: Math.round(cost), paid: Math.round(paid), gross: Math.round(gross) };
};
const allPfs = pfs;
@@ -1049,11 +1067,13 @@ function computePlanMonthly(planFinances, year) {
var pf = pfs[j];
if ((pf.sign_month || '') === month) sign += parseFloat(pf.sign_amount || 0);
var bd = []; try { bd = JSON.parse(pf.budget_data || '[]'); } catch(e) {}
var b = bd.find(function(x) { return (x.month || '') === month; });
if (b) { revenue += parseFloat(b.rev || 0); gross += parseFloat(b.gross || 0); payment += parseFloat(b.payment || 0); }
bd.filter(function(x) { return (x.month || '') === month; }).forEach(function(b) {
revenue += parseFloat(b.rev || 0); gross += parseFloat(b.gross || 0); payment += parseFloat(b.payment || 0);
});
var ed = []; try { ed = JSON.parse(pf.expense_data || '[]'); } catch(e) {}
var e = ed.find(function(x) { return (x.month || '') === month; });
if (e) { cost += parseFloat(e.cost || 0); paid += parseFloat(e.paid || 0); }
ed.filter(function(x) { return (x.month || '') === month; }).forEach(function(e) {
cost += parseFloat(e.cost || 0); paid += parseFloat(e.paid || 0);
});
}
data.push({ month: month, sign: Math.round(sign), revenue: Math.round(revenue), gross: Math.round(gross), payment: Math.round(payment), cost: Math.round(cost), paid: Math.round(paid) });
}

View File

@@ -266,7 +266,7 @@ function switchTab(tab) {
document.querySelectorAll(".panel").forEach((panel) => panel.classList.toggle("active", panel.id === tab));
// 更新顶部标题
const titles = { finance: '执行管理', plan: '预算管理', projects: '重点工作台账', proposals: '业务方案', products: '版本与运营', performance: '季度绩效考核', overview_plan: '预算总览', overview_finance: '发生总览' };
const titles = { finance: '执行管理', plan: '预算管理', projects: '重点工作台账', proposals: 'wiki', products: '版本与运营', performance: '季度绩效考核', overview_plan: '预算总览', overview_finance: '发生总览' };
const titleEl = document.querySelector('#workspaceTitle');
if (titleEl) titleEl.textContent = titles[tab] || state.tenant + ' OPC 工作台';

View File

@@ -83,10 +83,6 @@
<i data-lucide="file-text" style="width:20px;height:20px"></i>
<span class="text-[10px] mt-1">Focus</span>
</div>
<div class="sidebar-tab" data-tab="proposals" onclick="switchTab('proposals')" title="业务方案">
<i data-lucide="package" style="width:20px;height:20px"></i>
<span class="text-[10px] mt-1">方案</span>
</div>
<div class="sidebar-tab" data-tab="products" onclick="switchTab('products')" title="产品迭代">
<i data-lucide="wallet-cards" style="width:20px;height:20px"></i>
<span class="text-[10px] mt-1">产品</span>
@@ -95,6 +91,10 @@
<i data-lucide="target" style="width:20px;height:20px"></i>
<span class="text-[10px] mt-1">绩效</span>
</div>
<div class="sidebar-tab" data-tab="proposals" onclick="switchTab('proposals')" title="wiki">
<i data-lucide="package" style="width:20px;height:20px"></i>
<span class="text-[10px] mt-1">wiki</span>
</div>
</div>
</aside>
<!-- 主内容区 -->