From d2841b166dc03a007540e789e8d8b6814930280f Mon Sep 17 00:00:00 2001 From: shunli Date: Fri, 31 Jul 2026 10:05:35 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AE=A1=E6=89=B9=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E6=8A=A5=E9=94=80=E5=85=A5=E5=8F=A3=EF=BC=8807-27-yan?= =?UTF-8?q?g-jun-reimbursement-readiness=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../spec/hyb-oa/frontend-react-vite-design.md | 59 +++++++++++++++++++ .../design.md | 9 +++ .../implement.md | 17 +++++- .../prd.md | 7 ++- client/src/pages/MobileApprovalsPage.tsx | 15 ++++- client/src/pages/WorkflowTaskDetailPage.tsx | 11 ++++ .../e2e/dg7-mobile-information-dedupe.spec.ts | 16 ++++- .../e2e/pc-detail-mobile-visual-wave3.spec.ts | 6 ++ 8 files changed, 134 insertions(+), 6 deletions(-) diff --git a/.trellis/spec/hyb-oa/frontend-react-vite-design.md b/.trellis/spec/hyb-oa/frontend-react-vite-design.md index b5d19737..9887577c 100644 --- a/.trellis/spec/hyb-oa/frontend-react-vite-design.md +++ b/.trellis/spec/hyb-oa/frontend-react-vite-design.md @@ -168,6 +168,65 @@ reference, not a copy of Feishu or Ant Design branding. | `请假记录` / `考勤` | 业务台账列表 or record view | Enterprise WeChat-owned records, mirror status, exceptions and audit | Ordinary users do not see OA daily leave/punch approval creation; OA emergency entry only for authorized emergency/admin scope. | | Mobile routes | Mobile expression of the same template | Top status/focus, card list or sectioned detail | One bottom safe-area primary action; secondary actions in `更多` or confirmation. | +## Scenario: Approval Task Business Detail Exit + +### 1. Scope / Trigger + +- Trigger: an approval task detail shows only a business summary while the approver must inspect line items, invoices, attachments or other evidence before acting. + +### 2. Signatures + +- Mobile reimbursement target: `/mobile/reimbursements/:reimbursementId`. +- PC reimbursement target: `/reimbursements/:reimbursementId`. +- Mobile source state: ``. + +### 3. Contracts + +- The pending-task list card continues to open `/mobile/approvals/:taskId`; do not confuse task selection with the separate business-detail exit inside the task detail. +- When `WorkflowTaskDetail.reimbursement` exists, the task detail exposes an explicitly named `查看完整报销单` or `查看报销单` link near the reimbursement summary. +- Reuse the existing business detail route and its permissions. Do not duplicate reimbursement items, invoice or attachment viewers inside the approval page merely to add reachability. +- Mobile navigation passes the current approval task URL through the existing `state.from` convention so the business detail back button returns to that task. This is a one-level source contract, not a new nested return stack. +- Approval actions remain on the approval task page. Opening the business detail does not approve, reject, return or mutate the task. + +### 4. Validation & Error Matrix + +- Task has a reimbursement -> show the reimbursement-detail link. +- Task is another entity type -> do not show a reimbursement link; add that entity's route only in a scoped task. +- Reimbursement summary is absent -> render no broken or guessed target. +- Business detail permission fails -> preserve the existing permission-denied behavior; do not expand roles from the frontend. + +### 5. Good/Base/Bad Cases + +- Good: approver opens the task, follows `查看完整报销单`, inspects evidence, returns to the same task and then chooses an approval action. +- Base: a completed reimbursement task still provides the read-only business-detail exit when its existing detail permission allows it. +- Bad: make a static-looking summary card secretly clickable, change the pending-list click handler, or copy a partial invoice/attachment viewer into the task page. + +### 6. Tests Required + +- Mobile E2E: task detail -> reimbursement detail -> items/evidence visible -> back to the same task -> approval actions still visible. +- PC E2E: task detail link -> existing reimbursement detail route and overview visible. +- Assert non-reimbursement task details do not receive a reimbursement target. +- Run `cd client && npm run typecheck && npm run build`. + +### 7. Wrong vs Correct + +#### Wrong + +```tsx +
handleTaskSelection(task.id)}>报销摘要
+``` + +#### Correct + +```tsx + + 查看完整报销单 + +``` + **Implementation order for design refactors**: 1. A-class frequent pages: workbench, pending tasks, workflow task detail, diff --git a/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/design.md b/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/design.md index f25e108a..8e825ffc 100644 --- a/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/design.md +++ b/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/design.md @@ -147,3 +147,12 @@ - 后续只有出现第二名实际审批主管,或有人基于真实职责承担 `finance_mgr`,并且业务证明需要金额升级时才重开分级设计。大额关注使用风险标记或通知,不通过同一自然人重复审批实现。 - 生产变更复用 `PATCH /api/approval-rules/:id`:先把低档规则扩为 `[0,+∞)`,再停用中、高两档,避免出现无人可匹配的短暂金额空窗;每步保留 `APPROVAL_RULE_UPDATED` 审计,失败则通过同一接口补偿恢复原三档。 - 最终审批只把报销置为 `approved`,不会自动创建付款批次或付款记录;付款批次仍由王芝玲主动创建并由马建勇复核。 + +## 9. 审批任务到完整报销单的可达性 + +- 根因不在待办列表卡片:移动待办列表已经通过 `handleTaskSelection` 进入 `/mobile/approvals/:taskId`。真实阻断发生在审批任务详情,其报销摘要只有单号、申请人和金额,没有费用明细、发票和附件,也没有进入既有报销详情的出口。 +- 移动端只在审批任务详情的报销摘要区增加明确的“查看完整报销单”入口,目标复用 `/mobile/reimbursements/:reimbursementId`;导航继续使用当前移动页面通用的 `{ state: { from: currentMobileSource } }` 约定,不新增 `returnTo` 参数或另一套回跳状态。 +- PC 端在 `WorkflowTaskDetailPage` 的报销摘要区提供“查看报销单”入口,目标复用 `/reimbursements/:reimbursementId`。 +- 现有 `manager`、`ceo` 和 `finance` 角色已经包含 `expense.view_all`,因此本轮不调整角色、权限矩阵、后端报销详情鉴权或 workflow 数据结构。 +- 不把整个摘要卡片改造成无提示的隐式链接;使用明确命名的链接按钮,避免与待办列表卡片的既有任务选择交互混淆。 +- 请假、采购、差旅审批详情存在同类业务详情缺口,但本轮只修复真实报销链;其他类型另轮统一处理。 diff --git a/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/implement.md b/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/implement.md index 45841b91..f8e27832 100644 --- a/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/implement.md +++ b/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/implement.md @@ -67,11 +67,19 @@ - [x] 在杨俊提交前通过现有审批规则管理接口把三档规则收敛为唯一 `[0,+∞)` 规则,审批链固定为 `department_manager → finance`;不扩角色、不保留 CEO 重复节点。 - [x] 规则写入前后只读核对 499.99 / 500 / 5000 三个金额,确认解析结果完全相同且 active `finance` 有唯一实际处理人。 - [ ] 请杨俊本人从企业微信进入 OA,确认身份解析成功并能看到报销入口。 -- [ ] 杨俊使用真实业务数据创建报销,至少添加一条费用明细并上传/绑定状态有效的真实票据后提交。 +- [x] 杨俊使用真实业务数据创建报销,至少添加一条费用明细并上传/绑定状态有效的真实票据后提交。 - [ ] 确认马建勇能看到该真实审批待办并按业务事实处理。 - [ ] 审批通过后确认单据进入王芝玲负责的财务处理链路;后续付款批次继续遵守既有 maker-checker。 - [ ] 用户验收期间不在聊天、截图或任务证据中粘贴票据、金额、银行资料或完整账号。 +## 6.1 审批详情可达性修复 + +- [x] 保留移动待办列表卡片现有 `handleTaskSelection` 行为,不修改任务列表到审批任务详情的路由。 +- [x] 在 `MobileApprovalsPage` 的审批任务详情报销摘要区增加“查看完整报销单”,复用 `/mobile/reimbursements/:id` 和现有 `state.from` 来源回跳约定。 +- [x] 在 `WorkflowTaskDetailPage` 的报销摘要区增加“查看报销单”,复用 PC 报销详情路由。 +- [x] 不修改后端、审批状态机、权限矩阵或生产数据;请假、采购、差旅同类入口缺口仅记录,不在本轮扩展。 +- [x] 增加聚焦前端回归,验证移动进入/返回及 PC 入口;运行前端 typecheck、build、Trellis task/governance 和 `git diff --check`。 + ## 7. 完成条件 - [ ] PRD 全部验收项有脱敏证据或明确由用户完成的真实业务验收结果。 @@ -98,3 +106,10 @@ - 审批规则 finding:`[0,500)` 可由马建勇和唯一 finance 王芝玲处理;`[500,5000)` 与 `[5000,+∞)` 均以 `finance_mgr` 收尾,但 active `finance_mgr=0`,达到 500 元会形成无人处理待办。未修正规则前不得笼统通知杨俊可提交。 - 审批规则收口:写入前报销 reviewing/工作流 pending/待办 pending 均为 0;备份 watchdog 与 ARM dead-man 均为 success。正式接口更新 3 次、审计新增 3,生产仅剩 1 条 `[0,+∞)` active 规则。 - 金额边界验证:实际工作流构造函数对 499.99、500、5000 均输出 `department_manager → finance`;直属主管解析到马建勇,active finance 唯一且由王芝玲承担。写后仍无报销、实例或待办新增,公网 ready 正常、构建标识未变化。 +- 真实报销生产只读核验:杨俊最新真实报销为 `reviewing`,工作流为 `pending`,当前唯一待办为主管审批且已指派主管;付款批次 0。未输出金额、单号、UUID、附件或原始 payload,未执行生产写入。 +- 真实验收 finding:主管能进入审批任务详情,但该页只有报销摘要,没有费用明细、发票、附件或完整报销单入口;因此尚未按业务事实审批。 +- 审批详情入口本地实现:移动任务详情新增“查看完整报销单”,复用移动报销详情和 `state.from`;PC 任务详情新增“查看报销单”。未修改后端、权限矩阵、审批状态机或列表卡片点击行为。 +- 首次聚焦 Playwright 运行因本地前端服务未启动,9 个用例均在 `/login` 前以 `ERR_CONNECTION_REFUSED` 失败,未执行到功能断言;启动 Vite 后改为只跑本次相关用例。 +- 移动聚焦用例首次进入/返回链路通过,随后因固定在 2026-05 的“已处理”测试数据超过页面最近 30 天窗口而失败;已把完成时间改为运行时近期时间,避免日历漂移。 +- 最终本地验证:`npm run typecheck` 通过,`npm run build` 通过;移动审批入口聚焦 Playwright 1/1 通过,PC 审批入口聚焦 Playwright 1/1 通过。 +- 交付门禁:当前 task validate、Trellis governance、process contract、敏感扫描和 `git diff --check` 均通过;文档注册审计为 0 error、1 条既有未注册文档汇总 warning。 diff --git a/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/prd.md b/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/prd.md index 9436f1cc..73eaa452 100644 --- a/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/prd.md +++ b/.trellis/tasks/07-27-yang-jun-reimbursement-readiness/prd.md @@ -36,10 +36,11 @@ - 最近部署后必须重装自动备份三项 service/timer 和日常巡检 timer,使 unit BUILD_SHA 与活动 server 一致;重装后手工触发一次真实自动异机备份,确认源端成功且 ARM 恢复点新鲜,再允许写入杨俊账号或主管关系。 - 第一笔真实业务由员工本人正常发起,目标链路为“员工提交 → 真实主管审批 → 财务处理”;付款批次仍遵守既有 maker-checker。 - 当前小团队取消报销金额分层,所有金额统一按“直属主管 → finance”审批;不得保留无人持有的 `finance_mgr` 节点,也不得因马建勇同时是主管和 CEO 而让同一人重复审批。 +- 真实首笔报销暴露审批可读性阻断:待办列表卡片能够进入审批任务详情,但任务详情只显示单号、申请人和金额摘要,没有通往费用明细、发票和附件的业务详情入口。移动端必须在审批任务详情区提供明确的“查看完整报销单”入口,复用既有 `/mobile/reimbursements/:id` 路由和 `state.from` 回跳约定;PC 审批任务详情同步提供报销单入口。 ## Scope -本轮范围只包括:修复生产 automatic-backup/WARCH3 timer 的构建合同并取得新鲜异机恢复点;从通讯录导入杨俊及两个既有企微测试成员;设置各自真实或测试主管关系;用两个测试成员完成一条可清理的生产报销体验验收;清理测试业务数据并停用测试 OA 账号;最后由杨俊本人使用真实数据发起报销。所有真实报销内容由杨俊本人提供和提交,所有真实审批与财务处理由对应真实人员完成。 +本轮范围只包括:修复生产 automatic-backup/WARCH3 timer 的构建合同并取得新鲜异机恢复点;从通讯录导入杨俊及两个既有企微测试成员;设置各自真实或测试主管关系;用两个测试成员完成一条可清理的生产报销体验验收;清理测试业务数据并停用测试 OA 账号;最后由杨俊本人使用真实数据发起报销;修复真实审批任务详情到完整报销单的移动端和 PC 入口。所有真实报销内容由杨俊本人提供和提交,所有真实审批与财务处理由对应真实人员完成。 ## Acceptance Criteria @@ -60,6 +61,8 @@ - [x] 生产已显式关闭 mock OCR;`off` 路径已用测试账号复验,空发票元数据仍可绑定、提交、主管退回和清理,真实使用暂由财务查看 PDF 原件并人工核对。 - [x] 本轮没有调用企微成员写接口;企微成员资料未被 OA 反向修改。 - [x] 生产审批规则已收敛为唯一全金额规则,499.99、500、5000 三个边界均解析为 `department_manager → finance`,且没有 CEO 重复节点或 `finance_mgr` 空节点。 +- [ ] 移动审批任务详情中的报销摘要区提供明确的“查看完整报销单”入口,进入现有移动报销详情并能通过既有 `state.from` 返回原审批任务。 +- [ ] PC 审批任务详情提供对应的“查看报销单”入口;两端均不修改待办列表卡片的既有点击行为,不修改审批状态机、权限矩阵或后端数据合同。 ## Out of Scope @@ -70,6 +73,7 @@ - 不在未获单独批准时执行通讯录 apply、主管/法体写入、生产报销或付款操作。 - 不使用真实发票、真实金额、真实银行资料或真实员工个人资料作为测试材料;不让测试单进入“通过/待财务/付款”状态。 - 不把手机号缺失视为导入阻断项;该员工使用企微或企业邮箱登录,后续按需补手机号。 +- 不在本轮顺带修复请假、采购、差旅等其他审批类型的同类“业务单据详情入口”缺口;将其记录为后续统一审批中心体验任务候选。 ## Test Plan @@ -77,3 +81,4 @@ - 通讯录聚焦 dry-run 仅输出人数、计划、冲突、跳过和失败计数。 - 生产只读 SQL/Prisma 只返回布尔值和计数,不输出手机号、邮箱、企微 userid、完整账号或原始 payload。 - 使用既有报销 workflow/权限聚焦回归验证主管解析和 `expense.approve`,不写生产业务单据。 +- 前端运行 `cd client && npm run typecheck && npm run build`,并用聚焦 Playwright 覆盖:移动审批任务详情进入完整报销单、返回原任务后审批动作仍可见,以及 PC 审批任务详情进入报销单。 diff --git a/client/src/pages/MobileApprovalsPage.tsx b/client/src/pages/MobileApprovalsPage.tsx index 4e83e9fc..e163d755 100644 --- a/client/src/pages/MobileApprovalsPage.tsx +++ b/client/src/pages/MobileApprovalsPage.tsx @@ -1177,8 +1177,19 @@ export function MobileApprovalsPage() { ) : null} {selectedTask.reimbursement ? ( -
- {joinBusinessMeta([`报销日期:${selectedTask.reimbursement.reimbursementDate ?? "-"}`, selectedTask.reimbursement.description])} +
+
+ {joinBusinessMeta([`报销日期:${selectedTask.reimbursement.reimbursementDate ?? "-"}`, selectedTask.reimbursement.description])} +
+
) : null} {selectedTask.leaveRequest ? ( diff --git a/client/src/pages/WorkflowTaskDetailPage.tsx b/client/src/pages/WorkflowTaskDetailPage.tsx index 80f9128c..81885e99 100644 --- a/client/src/pages/WorkflowTaskDetailPage.tsx +++ b/client/src/pages/WorkflowTaskDetailPage.tsx @@ -398,6 +398,17 @@ export function WorkflowTaskDetailPage() { ) : null} + + {detail.reimbursement ? ( + + ) : null}
diff --git a/client/tests/e2e/dg7-mobile-information-dedupe.spec.ts b/client/tests/e2e/dg7-mobile-information-dedupe.spec.ts index 22b5729d..63e66c69 100644 --- a/client/tests/e2e/dg7-mobile-information-dedupe.spec.ts +++ b/client/tests/e2e/dg7-mobile-information-dedupe.spec.ts @@ -242,6 +242,7 @@ function buildNotifications() { function workflowTaskSummary(status: "pending" | "completed" = "pending") { const isCompleted = status === "completed"; + const completedAt = isCompleted ? new Date().toISOString() : null; return { id: isCompleted ? "task-dg7-completed" : "task-dg7", instanceId: "instance-dg7", @@ -252,7 +253,7 @@ function workflowTaskSummary(status: "pending" | "completed" = "pending") { status, outcome: isCompleted ? "approved" : null, createdAt: "2026-05-12T08:00:00.000Z", - completedAt: isCompleted ? "2026-05-12T09:00:00.000Z" : null, + completedAt, workflowInstance: { entityType: "reimbursement", entityId: "reimbursement-dg7", @@ -272,6 +273,7 @@ function workflowTaskSummary(status: "pending" | "completed" = "pending") { function workflowTaskDetail(status: "pending" | "completed" = "pending") { const isCompleted = status === "completed"; + const completedAt = isCompleted ? new Date().toISOString() : null; const history = [ { id: "history-workflow-submit-dg7", @@ -309,7 +311,7 @@ function workflowTaskDetail(status: "pending" | "completed" = "pending") { outcome: isCompleted ? "approved" : null, comment: null, createdAt: "2026-05-12T08:00:00.000Z", - completedAt: isCompleted ? "2026-05-12T09:00:00.000Z" : null, + completedAt, }, workflowInstance: { id: "instance-dg7", @@ -989,6 +991,16 @@ test.describe("MIF7 移动信息架构整改", () => { await mobile.page.getByTestId("mobile-approvals-detail-header").getByRole("button", { name: "返回" }).click(); await expect(mobile.page).toHaveURL(/\/mobile$/); + + await mobile.page.getByTestId("mobile-pending-summary").getByRole("link").first().click(); + await expect(mobile.page).toHaveURL(/\/mobile\/approvals\/task-dg7$/); + await mobile.page.getByTestId("mobile-approval-reimbursement-detail-link").click(); + await expect(mobile.page).toHaveURL(/\/mobile\/reimbursements\/reimbursement-dg7$/); + await expect(mobile.page.getByTestId("mobile-reimbursement-items")).toBeVisible(); + await mobile.page.getByTestId("mobile-reimbursement-detail-header").getByRole("button", { name: "返回" }).click(); + await expect(mobile.page).toHaveURL(/\/mobile\/approvals\/task-dg7$/); + await expectApprovalActionsStable(mobile.page); + await mobile.page.goto("/mobile/approvals"); await expect(mobile.page.getByRole("tab", { name: "待我处理" })).toBeVisible(); await expect(mobile.page.getByRole("tab", { name: "已处理" })).toBeVisible(); diff --git a/client/tests/e2e/pc-detail-mobile-visual-wave3.spec.ts b/client/tests/e2e/pc-detail-mobile-visual-wave3.spec.ts index a641995e..aa74656a 100644 --- a/client/tests/e2e/pc-detail-mobile-visual-wave3.spec.ts +++ b/client/tests/e2e/pc-detail-mobile-visual-wave3.spec.ts @@ -1097,6 +1097,12 @@ test("Wave3 PC 详情和通知中心视觉截图", async ({ page }) => { await expect(page.getByText("审批动作")).toHaveCount(0); await expect(page.getByRole("heading", { name: "处理", exact: true })).toBeVisible(); await expect(page.getByTestId("workflow-task-overview")).toBeVisible(); + await page.getByTestId("workflow-task-reimbursement-detail-link").click(); + await expect(page).toHaveURL(/\/reimbursements\/reim-wave3$/); + await expect(page.getByTestId("reimbursement-overview")).toBeVisible(); + await page.goBack(); + await expect(page).toHaveURL(/\/workflow\/tasks\/task-wave3$/); + await expect(page.getByTestId("workflow-task-overview")).toBeVisible(); await page.screenshot({ path: screenshotPath("02-pc-workflow-detail.png"), fullPage: true }); await page.goto("/notifications");