收敛报销退回动作与审批布局 [09-04-approval-return-reject-actions]
这个提交包含在:
@@ -490,6 +490,66 @@ const label = status === "draft" ? "草稿" : status;
|
||||
{needsBankReceipt ? <UploadReceipt /> : canRegister ? <RegisterPaid /> : null}
|
||||
```
|
||||
|
||||
## Scenario: Entity-Specific Return And Reject Actions
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
- Trigger: changing approval action availability, labels or layouts for reimbursement, leave, attendance correction, purchase or travel tasks.
|
||||
- Action availability follows the business entity's real edit/resubmit consequence; do not infer one global meaning from workflow outcome names alone.
|
||||
|
||||
### 2. Signatures
|
||||
|
||||
- Capability source: `server/src/routes/workflow.js#listAllowedActionsForTask`.
|
||||
- Reimbursement UI: `client/src/pages/reimbursement-workspace/ReimbursementApprovalAdapter.tsx`.
|
||||
- Legacy audit values remain `workflow_history.action=return|reject` and task outcome `returned|rejected`.
|
||||
|
||||
### 3. Contracts
|
||||
|
||||
- New reimbursement tasks advertise `approve + return` and optional permitted `transfer`; they do not advertise `reject`. The UI labels return as `退回修改`, writes the existing returned history and returns the same reimbursement to `draft` for editing and resubmission.
|
||||
- Historical rejected reimbursements remain readable and editable under the existing compatibility rule. Never rewrite them to returned or remove reject mappings from history, print, processing progress or notifications.
|
||||
- Leave, purchase and travel retain both return and reject until their module semantics are separately changed. Attendance correction retains reject only. Do not globally remove reject from fixtures or shared confirmation copy.
|
||||
- Purchase/travel return restores `draft`; reject leaves the entity `rejected`, which is not editable or submittable in place. This is a real consequence difference.
|
||||
- The legacy reimbursement reject endpoints may remain callable during compatibility rollout even though new `allowedActions` and product UI do not expose them. Endpoint retirement requires a separate caller/compatibility audit.
|
||||
- Reimbursement fail-closed conditions, permissions, B1/B2 checks and return transaction stay unchanged when action presentation changes.
|
||||
|
||||
### 4. Validation & Error Matrix
|
||||
|
||||
- Reimbursement pending + approve permission -> `approve,return` plus optional transfer; reject absent.
|
||||
- Leave/purchase/travel pending + matching permission -> `approve,reject,return` plus optional transfer.
|
||||
- Attendance pending + matching permission -> `approve,reject` plus optional transfer; return absent.
|
||||
- Processed, non-actionable or unauthorized task -> no actions.
|
||||
- Historical reimbursement status rejected -> keep `已驳回`, edit/resubmit compatibility and original audit history.
|
||||
|
||||
### 5. Good/Base/Bad Cases
|
||||
|
||||
- Good: finance sees one clear modification outcome, `退回修改`, and the applicant edits the same reimbursement before a new workflow instance starts.
|
||||
- Base: purchase approval keeps both return and reject because only return permits in-place correction.
|
||||
- Bad: hide reject only with CSS while the API still advertises it, rename reject to return while calling the reject endpoint, or erase historical rejected labels because new reimbursement tasks no longer produce them.
|
||||
|
||||
### 6. Tests Required
|
||||
|
||||
- Table-driven unit coverage for the five entity action matrices, transfer permission, processed tasks and unauthorized actors.
|
||||
- Reimbursement PC/mobile coverage for no visible reject, return progressive disclosure, fail-closed disabled actions and a two-action responsive group.
|
||||
- Regression coverage proving the four non-reimbursement action sets and shared panel DOM remain unchanged.
|
||||
- Compatibility coverage for historical rejected reimbursement display/edit/resubmit and legacy reject endpoint behavior.
|
||||
|
||||
### 7. Wrong vs Correct
|
||||
|
||||
#### Wrong
|
||||
|
||||
```text
|
||||
API allowedActions = approve,reject,return
|
||||
reimbursement UI hides reject locally
|
||||
```
|
||||
|
||||
#### Correct
|
||||
|
||||
```text
|
||||
API reimbursement allowedActions = approve,return[,transfer]
|
||||
reimbursement UI renders only declared capabilities
|
||||
legacy reject endpoint and rejected history remain compatible but undiscoverable from the new UI
|
||||
```
|
||||
|
||||
## Scenario: Shared Payment Processing Projection
|
||||
|
||||
### 1. Scope / Trigger
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,92 @@
|
||||
# 技术设计:审批退回驳回语义与动作布局
|
||||
|
||||
## 1. 决策
|
||||
|
||||
采用按实体区分的动作模型,不全局合并状态机:
|
||||
|
||||
- 报销:新待办对客户端宣告 `approve / return / transfer`,不宣告 `reject`;界面文案为“通过 / 退回修改 / 转审”。
|
||||
- 请假、采购、差旅:保留 `approve / reject / return`;补卡保留 `approve / reject`。
|
||||
- 本任务只调整报销动作投影与报销 PC/移动布局;通用审批页仍消费其他四类实体的既有动作。
|
||||
|
||||
这不是把 `reject` 改名为 `return`,也不改写任何已存在的 outcome。新报销审批的修改诉求统一走原有 return 事务。
|
||||
|
||||
## 2. 服务端边界
|
||||
|
||||
在 `listAllowedActionsForTask` 中按实体产生对外 capability:
|
||||
|
||||
```text
|
||||
reimbursement -> approve, return, [transfer]
|
||||
attendance_correction -> approve, reject, [transfer]
|
||||
leave/purchase/travel -> approve, reject, return, [transfer]
|
||||
```
|
||||
|
||||
`transfer` 仍由既有权限决定。本任务不增加或删除审批权限。
|
||||
|
||||
兼容策略:
|
||||
|
||||
- `/workflow/tasks/:taskId/reject` 与 `/workflow/batch-reject` 暂不删除,避免旧页面、离线队列或内部调用在同一版本中发生破坏性变化。
|
||||
- 新页面以 `allowedActions` 为可见能力来源,不为报销渲染 reject。
|
||||
- 既有 `rejected` 业务单、task、instance、history 和 notification 原样保留;报销 `draft/rejected` 的编辑与重提能力不变。
|
||||
- 不迁移数据库、不改 Prisma schema、不重写历史状态。
|
||||
|
||||
## 3. PC 报销动作区
|
||||
|
||||
默认态只保留一个紧凑动作组:
|
||||
|
||||
```text
|
||||
审批决定
|
||||
────────────────────────
|
||||
[ 通过 ] [ 退回修改 ]
|
||||
────────────────────────
|
||||
转审
|
||||
```
|
||||
|
||||
- “通过”和“退回修改”同一行、同高;主按钮使用实心主色,退回使用次级描边。
|
||||
- 两列在右栏宽度内稳定铺满,不使用一个通栏按钮加若干内在宽度小按钮。
|
||||
- “转审”保持现有 ghost/低频区域,不恢复更多菜单,也不改变其能力条件。
|
||||
- 点击“退回修改”后才展开意见和引用清单;确认仍二次确认。
|
||||
- fail-closed 原因继续紧贴动作组上方;被阻断时通过、退回修改、转审全部禁用。
|
||||
|
||||
为避免影响共享组件的其他消费者,优先由 `ReimbursementApprovalAdapter` 注入一个报销专用 action-group 节点;不改变 `BusinessDetailActionPanel` 其他消费者的 DOM 顺序。
|
||||
|
||||
## 4. 移动报销动作区
|
||||
|
||||
默认 sticky action bar 改为两个直接动作:
|
||||
|
||||
```text
|
||||
[ 退回修改 ] [ 通过 ]
|
||||
```
|
||||
|
||||
- 两个按钮均不小于 44px 高;通过保持主视觉,退回为次级样式。
|
||||
- 点击退回后,在正文动作卡中渐进展开意见、异常优先引用清单和确认动作。
|
||||
- 转审继续留在正文的低频处理区,不塞入 sticky 主动作组。
|
||||
- 离线报销继续全动作 fail-closed,不因本次减少按钮而开放离线退回或驳回。
|
||||
|
||||
## 5. 数据与历史
|
||||
|
||||
- 新退回历史仍写 `workflow_history.action=return`、task outcome=`returned`、instance status=`terminated`、报销 status=`draft`。
|
||||
- 历史 rejected 报销仍显示“已驳回”,并可按既有规则修改重提。
|
||||
- 打印页、处理进度和审批历史继续同时认识 return/reject;不得因为新 UI 不再产生报销 reject 而删除历史映射。
|
||||
- 通知继续使用 `reimbursement.returned`;历史 `reimbursement.rejected` 类型保留。
|
||||
|
||||
## 6. 测试边界
|
||||
|
||||
- 服务端:五类实体的 allowedActions 矩阵;报销新任务不含 reject,其他实体保持原样。
|
||||
- 报销 PC:默认动作区只有通过、退回修改、转审;无驳回按钮;两项核心动作同组;退回展开/引用/确认保持。
|
||||
- 报销移动:sticky 包含退回修改与通过;无驳回入口;离线全部禁用。
|
||||
- 兼容:历史 rejected 报销仍可编辑重提,history/print 显示不变;旧 reject endpoint 的既有测试继续通过。
|
||||
- 回归:请假、补卡、采购、差旅动作集合、确认文案和页面布局不变。
|
||||
|
||||
## 7. 风险与回滚
|
||||
|
||||
- 风险:`allowedActions` fixture 广泛硬编码,需只修改报销 fixture,不能全局删除 reject。
|
||||
- 风险:移动通用审批页与报销专用工作台并存,必须验证报销路由最终只呈现一套动作。
|
||||
- 风险:旧 bundle 在切换窗口仍可能调用兼容 reject endpoint;因此首轮上线后仍可能出现少量新 rejected 历史,这是兼容期预期,不做数据修正。
|
||||
- 回滚:纯代码回滚即可恢复原动作投影和三按钮布局;无数据库回滚步骤。
|
||||
|
||||
## 8. 延后事项
|
||||
|
||||
- 请假/采购/差旅 return 缺少结果通知类型与发送,是独立通知治理问题。
|
||||
- 是否最终退役报销 reject 与 batch-reject API,需在兼容期和调用审计后另立任务。
|
||||
- 转审是否隐藏或进入“更多”仍等待真实使用证据。
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
|
||||
@@ -0,0 +1,64 @@
|
||||
# 实施计划:审批退回驳回语义与动作布局
|
||||
|
||||
## 实施顺序
|
||||
|
||||
1. 为 `listAllowedActionsForTask` 增加实体动作矩阵,报销不再宣告 reject,其他四类保持现状。
|
||||
2. 更新服务端聚焦测试,覆盖五类实体的正向与负向 allowedActions,并保留 reject/return endpoint 的兼容测试。
|
||||
3. 收敛 `ReimbursementApprovalAdapter` 的 PC 动作组:通过与退回修改同组,删除报销可见驳回入口,转审不动。
|
||||
4. 收敛移动报销 sticky action bar:退回修改与通过并列;退回意见、引用清单和确认仍渐进展开;删除离线时强制渲染驳回的旧分支。
|
||||
5. 更新报销 PC/移动 E2E fixtures 和断言:无驳回、动作同组、退回展开、fail-closed、离线禁用、历史 rejected 兼容。
|
||||
6. 回归通用审批页的请假、补卡、采购、差旅动作集合与确认流程,确认共享组件 DOM 未漂移。
|
||||
7. 仅在稳定合同确有变化时更新 `docs/需求-统一审批中心规约.md` 与 `.trellis/spec/hyb-oa/approval-workflow.md`,明确报销用“退回修改”、其他实体按后果保留驳回。
|
||||
|
||||
## 风险文件
|
||||
|
||||
- `server/src/routes/workflow.js`:共享工作流动作投影,必须覆盖五类实体。
|
||||
- `client/src/pages/reimbursement-workspace/ReimbursementApprovalAdapter.tsx`:PC/移动和离线分支集中,不能误删 return 引用能力或 fail-closed。
|
||||
- `client/src/pages/MobileApprovalsPage.tsx`:通用移动审批回归面,原则上不改。
|
||||
- `client/src/components/patterns/BusinessDetailTemplate.tsx`:共享动作面板,原则上不改。
|
||||
- `client/src/lib/workflowActionConfirm.ts`:通用后果文案,原则上不改;报销仅不再进入 reject 分支。
|
||||
|
||||
## 验证命令
|
||||
|
||||
```bash
|
||||
cd server && npm run prisma:validate
|
||||
cd server && node --check src/routes/workflow.js
|
||||
cd server && npm test -- tests/unit/routes/workflow.permissions.test.js tests/unit/routes/workflow.batch.test.js tests/unit/routes/workflow.travel.test.js
|
||||
cd server && npm test -- tests/unit/routes/reimbursements.detail.test.js tests/unit/routes/reimbursements.invoice-item-link.test.js
|
||||
cd client && npm run build
|
||||
cd client && npx playwright test tests/e2e/reimbursement-content-consistency.spec.ts --workers=1 --reporter=list
|
||||
cd client && npx playwright test tests/e2e/workflow-action-confirm-ui.spec.ts --workers=1 --reporter=list
|
||||
cd client && npx playwright test tests/e2e/mobile-approvals-offline.spec.ts --grep '报销证据不进入 IndexedDB' --workers=1 --reporter=list
|
||||
node scripts/qa/check-ui-design-rules.mjs --strict
|
||||
node scripts/qa/check-ui-design-rules.mjs --self-test
|
||||
node scripts/qa/check-process-contract.mjs --baseline --json
|
||||
node scripts/qa/audit-docs.mjs --check-registry
|
||||
node scripts/qa/check-trellis-governance.mjs
|
||||
python3 ./.trellis/scripts/task.py validate 09-04-approval-return-reject-actions
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## 验证结果
|
||||
|
||||
- Prisma schema 与 `workflow.js` 语法检查通过。
|
||||
- 服务端动作矩阵、旧 reject 兼容及四类非报销回归共 24/24 通过;历史报销详情与 rejected 可维护证据回归 48/48 通过。
|
||||
- 前端 typecheck、生产构建通过;报销静态契约 9/9、审批动作浏览器回归 11/11 通过。
|
||||
- 移动报销离线专项未进入断言:本机缺 Playwright WebKit executable;保留为环境边界,不记为产品失败,也未安装新浏览器或更改测试配置。
|
||||
- UI strict 扫描 155 文件、0 findings;self-test PASS。
|
||||
- process contract、Trellis governance、task validate 与 `git diff --check` 通过。
|
||||
- docs registry 审计为 0 error / 1 warning;warning 为仓库既有 22 个未注册 docs 文件,本任务未新增该类文件。
|
||||
|
||||
## 人工验收
|
||||
|
||||
1. PC 报销审批右栏:通过与退回修改同组、无驳回、转审位置不变;1280px 无溢出。
|
||||
2. 移动报销审批:底部直接显示退回修改与通过;点击退回后才展开意见与引用清单。
|
||||
3. 任一报销 fail-closed fixture:原因紧邻动作,所有可见动作均禁用。
|
||||
4. 历史 rejected 报销仍显示已驳回、仍可修改重提;历史 timeline/打印无丢失。
|
||||
5. 请假、补卡、采购、差旅各打开一个:动作数量、文案和确认流程无变化。
|
||||
|
||||
## 提交与部署边界
|
||||
|
||||
- commit 使用中文并引用 `09-04-approval-return-reject-actions`。
|
||||
- 实施后先做只读 review;用户批准后才能提交。
|
||||
- push、生产部署和生产数据检查均需分别获得明确授权。
|
||||
- 无 migration/schema/seed/flag/生产数据写入;回滚到上一构建即可。
|
||||
@@ -0,0 +1,118 @@
|
||||
# 审批退回驳回语义与动作布局
|
||||
|
||||
## Goal
|
||||
|
||||
基于系统真实后果而不是行业常识,厘清“退回”与“驳回”的实体差异;优先收敛唯一真实使用的报销审批链路,使审批人只面对必要动作,并修复当前“通栏主按钮 + 零散小按钮”的割裂布局。
|
||||
|
||||
## Background
|
||||
|
||||
当前报销审批右栏把“通过”作为通栏主按钮,“退回”“驳回”作为下一行两个内在宽度按钮,“转审”再单独分组。该布局既失衡,也把两个对报销申请人后果近似的动作同时交给审批人判断。
|
||||
|
||||
报销现有“驳回”并未兑现通常意义上的终止:`rejected` 报销仍可在原单编辑并重提。因此移除报销审批端的“驳回”不损失已存在的终止能力,而是撤掉一个与真实后果不符的语义承诺。审批人若认为该笔费用根本不应申报,现阶段在退回意见中说明,申请人收到后可删除草稿;真正的审批人终止能力如有现实需求,必须另立状态机任务,不能继续借用名不副实的 reject。
|
||||
|
||||
只读核查确认五类实体不能全局合并:
|
||||
|
||||
| 实体 | 退回后 | 驳回后 | 后续能力 | 结论 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 报销 | 原单回 `draft` | 原单为 `rejected` | 两者均可在原单修改、重提,并创建新 workflow instance | 新审批只保留“退回修改” |
|
||||
| 请假 | 原单回 `draft` | 原单为 `rejected` | 两者均可修改、重提并重新预留假期;但只有驳回发送结果通知 | 本次不改 |
|
||||
| 补卡 | 不提供退回 | 原单为 `rejected` | 无原单编辑/重提 | 本次不改 |
|
||||
| 采购 | 原单回 `draft` | 原单为 `rejected` | 只有退回后能修改、重提 | 两动作有实质差异,本次不改 |
|
||||
| 差旅 | 原单回 `draft` | 原单为 `rejected` | 只有退回后能修改、重提 | 两动作有实质差异,本次不改 |
|
||||
|
||||
服务端证据与文件锚点见 `research/return-reject-semantics.md`。国内 OA 的通行叫法仅作为参考,目标口径由本系统状态、编辑/重提、通知和释放副作用共同决定。
|
||||
|
||||
## Scope
|
||||
|
||||
- 按五类实体建立服务端动作矩阵,修改新报销待办的 `allowedActions` 投影。
|
||||
- 收敛报销 PC 与移动审批动作文案、分组和渐进展开方式。
|
||||
- 增加服务端矩阵测试、报销 PC/移动交互测试和四类非报销回归。
|
||||
- 保留历史状态与旧接口兼容,必要时同步审批规约和稳定工程 spec。
|
||||
|
||||
## Requirements
|
||||
|
||||
### R1:按实体投影动作
|
||||
|
||||
- 服务端 `allowedActions` 必须按实体后果产生,不再默认给所有非补卡实体同一组动作。
|
||||
- 新报销待办只宣告 `approve / return`,有既有转审权限时再宣告 `transfer`;不再向 PC 或移动客户端宣告 `reject`。
|
||||
- 请假、采购、差旅继续宣告 `approve / reject / return`;补卡继续宣告 `approve / reject`;各实体的转审仍沿用现有权限。
|
||||
|
||||
### R2:报销只保留“退回修改”
|
||||
|
||||
- 报销审批界面将 return 展示为“退回修改”,继续调用既有 return 事务。
|
||||
- 退回后必须保持:报销单回 `draft`、原单号保留、历史追加 returned、申请人可修改并重提、重提新建 workflow instance。
|
||||
- 不得把 reject 改名后继续调用 reject,也不得把历史 rejected 重写为 returned。
|
||||
|
||||
### R3:PC 动作布局收敛
|
||||
|
||||
- 报销 PC 右栏的“通过”和“退回修改”必须在同一个两列动作组中,同高、铺满可用宽度,以颜色和样式表达主次,不再使用一枚通栏按钮加零散小按钮。
|
||||
- “转审”保持现有低频区域、权限和展开行为;不恢复“更多”菜单。
|
||||
- 点击“退回修改”后才展开意见、异常优先引用清单与确认入口。
|
||||
|
||||
### R4:移动动作布局收敛
|
||||
|
||||
- 报销移动端 sticky action bar 必须直接显示“退回修改”和“通过”,触控高度不小于 44px。
|
||||
- 退回编辑内容在点击后渐进展开;转审继续留在低频处理区。
|
||||
- 报销离线仍 fail-closed,不因减少一个按钮而开放任何远程审批动作。
|
||||
|
||||
### R5:安全与解释不退化
|
||||
|
||||
- 不增加或删除现有 `decisionReady`、B1/B2、权限、自审批或内容完整性条件。
|
||||
- fail-closed 原因继续紧邻动作组上方;被阻断时所有可见动作全部禁用。
|
||||
- 通过、退回修改和转审继续使用现有二次确认、必填意见与审计规则。
|
||||
|
||||
### R6:历史和接口兼容
|
||||
|
||||
- 既有 rejected 报销继续显示“已驳回”,仍按现有规则允许修改重提;打印、处理进度、审批历史和通知映射不得删除 reject。
|
||||
- 本任务不迁移数据库、不修改 schema、不改写 task/instance/history/outcome。
|
||||
- 旧单项 reject endpoint 和隐藏的 batch reject endpoint 作为兼容入口暂时保留,但不得新增或恢复任何报销可见入口;其最终退役另行立项。
|
||||
|
||||
### R7:其他实体零行为变化
|
||||
|
||||
- 请假、补卡、采购、差旅的动作集合、状态写入、通知、释放逻辑、确认文案和页面布局本次不变。
|
||||
- 请假/采购/差旅 return 缺少结果通知属于已识别既有缺口,另记待办,不在本任务顺手修复。
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] AC1:服务端测试逐项证明 reimbursement=`approve,return[,transfer]`、leave/purchase/travel=`approve,reject,return[,transfer]`、attendance_correction=`approve,reject[,transfer]`;无权限或已处理任务仍返回空动作。
|
||||
- [ ] AC2:报销 PC 和移动 fixture 的 `allowedActions` 及可见 UI 均不含 reject;页面上“驳回”“确认驳回”按钮数为 0。
|
||||
- [ ] AC3:PC 报销右栏“通过 / 退回修改”同处一个两列动作组、等高且无横向溢出;不存在旧的一通栏加两个小按钮布局。
|
||||
- [ ] AC4:移动报销 sticky action bar 默认直接包含“退回修改 / 通过”,两者触控高度至少 44px;转审不进入该主动作组。
|
||||
- [ ] AC5:展开退回修改前,意见输入和引用清单不存在;展开后意见必填、异常引用项优先、确认弹窗准确说明回到申请人修改态。
|
||||
- [ ] AC6:任一现有 fail-closed 条件成立时,原因紧贴动作组上方,通过、退回修改、转审全部禁用;正常报销的可见动作不被误伤。
|
||||
- [ ] AC7:执行退回修改后,服务端响应和持久化仍为 returned/draft;修改后重提创建新 workflow instance 并从重新匹配的首个可执行节点开始。
|
||||
- [ ] AC8:历史 rejected 报销仍可读取、显示、编辑和重提;打印/时间线仍正确显示“驳回”,不发生数据迁移或历史改写。
|
||||
- [ ] AC9:旧 reject endpoint 的既有兼容测试继续通过;产品 UI 和新 `allowedActions` 不再为报销提供调用路径。
|
||||
- [ ] AC10:请假、补卡、采购、差旅分别通过动作集合与确认流程回归;共享 `BusinessDetailActionPanel` 的其他消费者 DOM 顺序不变。
|
||||
- [ ] AC11:typecheck、build、相关后端测试、PC/移动 E2E、UI strict/self-test、process contract、Trellis governance 与 `git diff --check` 全部通过。
|
||||
- [ ] AC12:面向用户可测试前,需另行批准 push/部署,并通过公网 bundle、PC 1280px、移动端和五类审批回归。
|
||||
|
||||
## Test Plan
|
||||
|
||||
- 服务端:五类实体 `allowedActions` 矩阵、无权限/已处理负向分支、报销 return 状态与重提新实例、旧 reject endpoint 兼容。
|
||||
- PC:报销两列动作组、无驳回入口、退回渐进展开、引用与确认、正常态及 fail-closed;四类通用审批动作回归。
|
||||
- 移动:报销 sticky 双动作、44px 触控、离线 fail-closed、退回编辑;通用移动审批不受影响。
|
||||
- 历史:rejected 报销读取、编辑、重提、打印和 timeline 映射不变。
|
||||
- 门禁:前后端聚焦测试、typecheck/build、UI strict/self-test、process contract、Trellis governance、`git diff --check`。
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- 删除 reject/batch-reject API、迁移或清理历史 rejected 数据。
|
||||
- 调整请假、补卡、采购、差旅的动作语义或页面布局。
|
||||
- 修复请假/采购/差旅 return 通知缺口。
|
||||
- 新增退回上一节点、退回指定人或续接旧 workflow instance 的能力。
|
||||
- B1/B2、审批链配置、权限模型、自审批规则和状态机的其他改造。
|
||||
- 转审入口归属、全局按钮体系或其他页面视觉重构。
|
||||
- 生产部署和真实数据写入。
|
||||
|
||||
## Risks And Deferred Items
|
||||
|
||||
- 旧 bundle 在发布切换窗口仍可能调用兼容 reject endpoint,因此短暂兼容期内仍可能产生 rejected 报销;不自动修正此类历史。
|
||||
- 采购和差旅页面存在“驳回后补材料”类文案与服务端只允许 draft 编辑之间的既有漂移,需独立任务处理,不混入本次报销收敛。
|
||||
- 请假/采购/差旅 return 不发送结果通知;需按各模块实际使用优先级另行治理。
|
||||
- 请假/采购/差旅 return 结果通知缺口应作为本任务之后的独立高优先级任务,优先于一般视觉和代码清理债;不得在本任务中夹带实现。
|
||||
- 转审是否隐藏、保留或进入更多菜单,继续等待真实使用证据。
|
||||
|
||||
## Planning Gate
|
||||
|
||||
本任务为复杂 workflow + UI 改动,`design.md`、`implement.md` 和研究记录已齐备。只有用户审批本版最终规划后,才可执行 `task.py start` 并修改产品代码。
|
||||
@@ -0,0 +1,63 @@
|
||||
# 退回 / 驳回语义核查
|
||||
|
||||
## 核查边界
|
||||
|
||||
只读检查服务端状态写入、业务对象编辑/重提条件、工作流实例创建、通知类型、额度/预算释放、前端动作投影和历史展示。未查询或修改生产数据。
|
||||
|
||||
## 总结
|
||||
|
||||
五类实体不能用一套“退回 / 驳回等价”结论:
|
||||
|
||||
| 实体 | 退回 | 驳回 | 申请人后续能力 | 通知 | 结论 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 报销 | 业务单回 `draft`;实例 `terminated`;task outcome=`returned` | 业务单回 `rejected`;实例/task=`rejected` | 两者均可编辑并在原单重提;重提创建新实例、重新从首个可执行节点开始 | 两者各有通知类型和不同标题 | 当前用户后果近似,审批端同时暴露两个动作没有足够价值 |
|
||||
| 请假 | 业务单回 `draft`;释放已预留假期 | 业务单回 `rejected`;同样释放已预留假期 | 两者均可编辑并在原单重提;重提创建新实例并重新预留 | 驳回有通知;退回当前没有对应通知类型 | 业务能力近似但副作用不等价,不能顺手合并 |
|
||||
| 补卡 | 不在 `allowedActions` 中,return handler 也无业务对象分支 | 业务单变 `rejected` | 没有原单编辑/重提入口 | 驳回有通知 | 只有驳回,语义明确 |
|
||||
| 采购 | 业务单回 `draft`;释放预算 | 业务单变 `rejected`;同样释放预算 | 只有 `draft` 能编辑/提交;驳回后原单不能继续修改 | 驳回有通知;退回当前没有对应通知类型 | 两者有实质差异,必须保留 |
|
||||
| 差旅 | 业务单回 `draft`;释放预算 | 业务单变 `rejected`;同样释放预算 | 只有 `draft` 能编辑/提交;驳回后原单不能继续修改 | 驳回有通知;退回当前没有对应通知类型 | 两者有实质差异,必须保留 |
|
||||
|
||||
## 关键证据
|
||||
|
||||
### 服务端动作投影
|
||||
|
||||
- `server/src/routes/workflow.js:684-703`:当前所有可审批实体默认获得 `approve + reject`,补卡以外再获得 `return`;差异仅按补卡特殊处理,没有按真实后果建模。
|
||||
- `server/src/routes/workflow.js:1858-2013`:驳回把 task/outcome、workflow instance 和各业务对象写成 `rejected`;请假释放假期,采购/差旅释放预算。
|
||||
- `server/src/routes/workflow.js:2990-3136`:退回把 task outcome 写成 `returned`、instance 写成 `terminated`,报销/请假/采购/差旅业务对象回到 `draft`;请假释放假期,采购/差旅释放预算;补卡没有业务对象更新分支。
|
||||
|
||||
### 编辑与重提
|
||||
|
||||
- 报销:提交及维护接受 `draft/rejected`;重提在 `server/src/routes/reimbursements.js:4556` 创建新 workflow instance。
|
||||
- 请假:编辑、提交接受 `draft/rejected`(`server/src/routes/leave.js:805-850`、`:1009-1010`);`:1113-1130` 创建新 workflow instance;`:1169-1184` 重新预留假期。
|
||||
- 采购:权限投影、编辑、提交均只接受 `draft`(`server/src/routes/purchaseRequests.js:675-678`、`:1652-1653`、`:1828-1829`);提交在 `:1959-1975` 创建新实例。
|
||||
- 差旅:权限投影、编辑、提交均只接受 `draft`(`server/src/routes/travelRequests.js:493-496`、`:808-809`、`:945-946`);提交在 `:1032-1050` 创建新实例。
|
||||
- 补卡:创建时直接创建 workflow instance(`server/src/routes/attendanceCorrections.js:307-322`),后续只有取消接口,没有原单编辑/重提接口。
|
||||
|
||||
### 通知
|
||||
|
||||
- `server/src/routes/workflow.js:1073-1096`:五类实体都有 rejected 通知;returned 仅映射报销。
|
||||
- `server/src/services/notifications/workflowCopy.js:238-253`:退回和驳回通知文案分别使用“已退回”“已驳回”。
|
||||
- 因此请假/采购/差旅执行退回时,`queueWorkflowOutcomeNotifications` 会因 notification type 为 `null` 直接返回。该缺口是既有行为,本任务不顺手扩张为通知治理。
|
||||
|
||||
### 现有产品合同与实现漂移
|
||||
|
||||
- `docs/需求-统一审批中心规约.md:205-242` 将驳回定义为终止或按模块重新发起,将退回定义为回到修改态并保留原单。
|
||||
- 采购/差旅服务端与该合同一致;报销/请假允许 `rejected` 原单直接编辑重提,比文档中的默认驳回语义更宽松。
|
||||
- `client/src/lib/workflowActionConfirm.ts` 仍按通用合同解释:退回原单修改,驳回通常重新发起。该文案对采购/差旅成立,对报销/请假不完全成立。
|
||||
|
||||
## 布局证据
|
||||
|
||||
- `client/src/components/patterns/BusinessDetailTemplate.tsx:83-138` 将 primary、secondary、more 拆成三个视觉分组。
|
||||
- 报销在 `client/src/pages/reimbursement-workspace/ReimbursementApprovalAdapter.tsx:398-414` 将“通过”放 primary,将“退回/驳回”放 secondary,将“转审”放 more,形成当前通栏主按钮、两个内在宽度按钮和分隔后的转审。
|
||||
- 移动报销在同文件 `:361-376` 把三个次动作放正文卡片,只把“通过”放 sticky action bar;信息层级与 PC 不一致。
|
||||
- `ui-ux-pro-max` 的相关规则要求危险动作二次确认、错误靠近字段/动作、触控目标至少 44px;现有确认和 fail-closed 规则应保留。
|
||||
|
||||
## 规划结论
|
||||
|
||||
采用“按实体区分”,但本次产品改动只收敛 OA 主链路报销:
|
||||
|
||||
1. 新报销审批不再向 UI 宣告 `reject`,仅提供“通过 / 退回修改 / 转审”。
|
||||
2. 报销“退回修改”继续调用既有 return 路径,保持 `draft + returned history + returned notification`。
|
||||
3. 历史 `rejected` 报销继续按原状态展示和允许修改重提;不改写历史记录、不迁移数据。
|
||||
4. 旧 reject endpoint 和隐藏的 batch reject endpoint暂作兼容保留,不新增入口;另行治理 API 退役,避免本次视觉改造扩大为破坏性接口变更。
|
||||
5. 请假、补卡、采购、差旅的动作与布局本次不改;它们作为回归面。请假/采购/差旅 return 通知缺口另记待办。
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"id": "approval-return-reject-actions",
|
||||
"name": "approval-return-reject-actions",
|
||||
"title": "审批退回驳回语义与动作布局",
|
||||
"description": "核清五类审批实体的退回与驳回真实后果,并按实体收敛报销动作投影及 PC/移动布局。",
|
||||
"status": "in_progress",
|
||||
"dev_type": null,
|
||||
"scope": "trellis,server,client,tests,docs",
|
||||
"package": null,
|
||||
"priority": "P2",
|
||||
"creator": "shunli",
|
||||
"assignee": "shunli",
|
||||
"createdAt": "2026-09-04",
|
||||
"completedAt": null,
|
||||
"branch": null,
|
||||
"base_branch": "main",
|
||||
"worktree_path": null,
|
||||
"commit": null,
|
||||
"pr_url": null,
|
||||
"subtasks": [],
|
||||
"children": [],
|
||||
"parent": null,
|
||||
"relatedFiles": [],
|
||||
"notes": "",
|
||||
"meta": {}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户