@yss123/cage-cli 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/approval-tree.d.ts +88 -0
- package/dist/approval-tree.js +18 -0
- package/dist/approval-tree.js.map +1 -0
- package/dist/capabilities.js +46 -31
- package/dist/capabilities.js.map +1 -1
- package/dist/graph-approvals.d.ts +11 -1
- package/dist/graph-approvals.js +18 -5
- package/dist/graph-approvals.js.map +1 -1
- package/dist/graph-relations.js +10 -8
- package/dist/graph-relations.js.map +1 -1
- package/dist/graph.js +2 -0
- package/dist/graph.js.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/ontology.js +8 -5
- package/dist/ontology.js.map +1 -1
- package/dist/project-structure.js +2 -2
- package/dist/project-structure.js.map +1 -1
- package/dist/sync.d.ts +1 -1
- package/dist/sync.js +25 -108
- package/dist/sync.js.map +1 -1
- package/graph-ui/approval-flow.js +28 -8
- package/graph-ui/approval-parallel.js +110 -0
- package/graph-ui/approval-tree.js +17 -0
- package/graph-ui/business.css +26 -0
- package/graph-ui/business.js +30 -13
- package/graph-ui/graph-approvals.js +18 -5
- package/graph-ui/graph-relations.js +10 -8
- package/package.json +6 -6
- package/templates/app/.agents/skills/cage-development/SKILL.md +10 -0
- package/templates/app/AGENTS.md +39 -122
- package/templates/app/CLAUDE.md +6 -0
- package/templates/app/README.md +9 -8
- package/templates/app/apps/api/package.json +2 -2
- package/templates/app/apps/web/package.json +1 -1
- package/templates/app/apps/web/src/modules/approval/components/task-operation-dialog.tsx +61 -0
- package/templates/app/apps/web/src/modules/approval/hooks/use-approval-tasks.ts +6 -9
- package/templates/app/apps/web/src/modules/approval/pages/approval-tasks-page.tsx +81 -49
- package/templates/app/apps/web/src/modules/approval/pages/approval-tasks.css +18 -0
- package/templates/app/apps/web/src/modules/purchase/components/approval-history.tsx +39 -4
- package/templates/app/apps/web/src/modules/purchase/components/order-reviewers.tsx +38 -0
- package/templates/app/apps/web/src/modules/purchase/components/purchase-order-form.tsx +9 -2
- package/templates/app/apps/web/src/modules/purchase/pages/purchase-intelligence.css +2 -0
- package/templates/app/apps/web/src/modules/purchase/pages/purchase-order-detail-page.tsx +2 -0
- package/templates/app/apps/web/src/modules/purchase/pages/purchase-orders-page.tsx +2 -0
- package/templates/app/apps/web/src/shared/lib/error-message.ts +6 -1
- package/templates/app/apps/web/src/shared/ui/approval-member-picker.css +10 -0
- package/templates/app/apps/web/src/shared/ui/approval-member-picker.tsx +51 -0
- package/templates/app/cage.config.json +14 -0
- package/templates/app/db/migrations/0004_draft.sql +27 -0
- package/templates/app/docs/README.md +11 -0
- package/templates/app/docs/development/README.md +64 -0
- package/templates/app/docs/development/examples/note/note-detail-page.tsx +106 -0
- package/templates/app/docs/development/examples/note/note.ts +67 -0
- package/templates/app/docs/development/examples/note/notes-page.tsx +87 -0
- package/templates/{blank/docs/ai → app/docs/development/examples}/purchase-example.md +24 -10
- package/templates/{blank/docs/ai → app/docs/development/getting-started}/architecture.md +4 -4
- package/templates/app/docs/development/getting-started/first-feature.md +66 -0
- package/templates/{blank/docs/ai → app/docs/development/getting-started}/workflow.md +13 -12
- package/templates/app/docs/development/guides/approvals.md +270 -0
- package/templates/app/docs/development/guides/business-collaboration.md +63 -0
- package/templates/{blank/docs/ai → app/docs/development/guides}/files.md +5 -1
- package/templates/app/docs/{ai → development/guides}/intelligence.md +4 -4
- package/templates/{blank/docs/ai → app/docs/development/guides}/recipes.md +31 -11
- package/templates/app/docs/{ai → development/guides}/relations.md +2 -2
- package/templates/app/docs/{ai → development/guides}/routing.md +1 -1
- package/templates/{blank/docs/ai → app/docs/development/guides}/transfers.md +1 -1
- package/templates/{blank/docs/ai → app/docs/development/reference}/commenting.md +1 -1
- package/templates/app/docs/{ai → development/reference}/primitives.md +8 -62
- package/templates/app/docs/{ai → development/reference}/project-structure.md +2 -2
- package/templates/app/docs/development/skills/cage-development/SKILL.md +134 -0
- package/templates/{blank/docs/ai → app/docs/development/tools}/diagnostics.md +3 -3
- package/templates/{blank/docs/ai → app/docs/development/tools}/ontology-context.md +3 -3
- package/templates/app/docs/{ai → development/tools}/troubleshooting.md +7 -1
- package/templates/app/docs/errors/CAGE-AI-001.md +3 -3
- package/templates/app/docs/errors/CAGE-APPR-001.md +13 -10
- package/templates/app/docs/errors/CAGE-APPR-002.md +1 -1
- package/templates/app/docs/errors/CAGE-APPR-003.md +1 -1
- package/templates/app/docs/errors/CAGE-APPR-004.md +1 -1
- package/templates/app/docs/errors/CAGE-APPR-005.md +5 -5
- package/templates/app/docs/errors/CAGE-APPR-006.md +25 -10
- package/templates/app/docs/errors/CAGE-APPR-007.md +9 -7
- package/templates/app/docs/errors/CAGE-APPR-010.md +1 -1
- package/templates/app/docs/errors/CAGE-AUDIT-001.md +1 -1
- package/templates/app/docs/errors/CAGE-AUTH-001.md +1 -1
- package/templates/app/docs/errors/CAGE-BIZ-001.md +1 -1
- package/templates/app/docs/errors/CAGE-BIZ-002.md +1 -1
- package/templates/app/docs/errors/CAGE-BUDGET-001.md +1 -1
- package/templates/app/docs/errors/CAGE-BUDGET-002.md +1 -1
- package/templates/app/docs/errors/CAGE-BUDGET-003.md +1 -1
- package/templates/app/docs/errors/CAGE-CMD-002.md +1 -1
- package/templates/app/docs/errors/CAGE-CMD-003.md +1 -1
- package/templates/app/docs/errors/CAGE-CONN-001.md +1 -1
- package/templates/app/docs/errors/CAGE-CONN-002.md +1 -1
- package/templates/app/docs/errors/CAGE-CONN-003.md +1 -1
- package/templates/app/docs/errors/CAGE-CONN-004.md +1 -1
- package/templates/app/docs/errors/CAGE-CONN-005.md +1 -1
- package/templates/app/docs/errors/CAGE-CONN-006.md +1 -1
- package/templates/app/docs/errors/CAGE-CONN-007.md +1 -1
- package/templates/app/docs/errors/CAGE-CONN-008.md +1 -1
- package/templates/app/docs/errors/CAGE-CONN-009.md +1 -1
- package/templates/app/docs/errors/CAGE-CONN-010.md +1 -1
- package/templates/app/docs/errors/CAGE-CONN-011.md +1 -1
- package/templates/app/docs/errors/CAGE-CONN-012.md +1 -1
- package/templates/app/docs/errors/CAGE-CONN-013.md +1 -1
- package/templates/app/docs/errors/CAGE-CONN-014.md +1 -1
- package/templates/app/docs/errors/CAGE-CONN-015.md +1 -1
- package/templates/app/docs/errors/CAGE-CREATE-001.md +2 -2
- package/templates/app/docs/errors/CAGE-DECL-001.md +1 -1
- package/templates/app/docs/errors/CAGE-DECL-002.md +1 -1
- package/templates/app/docs/errors/CAGE-DECL-003.md +1 -1
- package/templates/app/docs/errors/CAGE-DECL-004.md +1 -1
- package/templates/app/docs/errors/CAGE-DEP-001.md +1 -1
- package/templates/app/docs/errors/CAGE-DEP-002.md +1 -1
- package/templates/app/docs/errors/CAGE-DEP-003.md +1 -1
- package/templates/app/docs/errors/CAGE-DEP-004.md +1 -1
- package/templates/app/docs/errors/CAGE-DEP-005.md +1 -1
- package/templates/app/docs/errors/CAGE-DIAG-001.md +1 -1
- package/templates/app/docs/errors/CAGE-DLQ-001.md +1 -1
- package/templates/app/docs/errors/CAGE-ENV-001.md +1 -1
- package/templates/app/docs/errors/CAGE-ENV-002.md +1 -1
- package/templates/app/docs/errors/CAGE-ENV-003.md +1 -1
- package/templates/app/docs/errors/CAGE-EVENT-001.md +2 -2
- package/templates/app/docs/errors/CAGE-EVENT-002.md +1 -1
- package/templates/app/docs/errors/CAGE-FIELD-001.md +1 -1
- package/templates/app/docs/errors/CAGE-FIELD-002.md +1 -1
- package/templates/app/docs/errors/CAGE-FIELD-003.md +1 -1
- package/templates/app/docs/errors/CAGE-FIELD-004.md +1 -1
- package/templates/app/docs/errors/CAGE-FILE-001.md +1 -1
- package/templates/app/docs/errors/CAGE-GEN-001.md +1 -1
- package/templates/app/docs/errors/CAGE-GEN-002.md +1 -1
- package/templates/app/docs/errors/CAGE-GEN-003.md +1 -1
- package/templates/app/docs/errors/CAGE-GRANT-001.md +1 -1
- package/templates/app/docs/errors/CAGE-GRANT-002.md +1 -1
- package/templates/app/docs/errors/CAGE-GRANT-003.md +1 -1
- package/templates/app/docs/errors/CAGE-IMPACT-001.md +1 -1
- package/templates/app/docs/errors/CAGE-IMPACT-002.md +1 -1
- package/templates/app/docs/errors/CAGE-INIT-001.md +1 -1
- package/templates/app/docs/errors/CAGE-INIT-002.md +1 -1
- package/templates/app/docs/errors/CAGE-INIT-003.md +1 -1
- package/templates/app/docs/errors/CAGE-INIT-004.md +1 -1
- package/templates/app/docs/errors/CAGE-INIT-005.md +1 -1
- package/templates/app/docs/errors/CAGE-JOB-001.md +1 -1
- package/templates/app/docs/errors/CAGE-JOB-002.md +1 -1
- package/templates/app/docs/errors/CAGE-JOB-003.md +1 -1
- package/templates/app/docs/errors/CAGE-LINT-001.md +1 -1
- package/templates/app/docs/errors/CAGE-MIG-001.md +1 -1
- package/templates/app/docs/errors/CAGE-MIG-002.md +1 -1
- package/templates/app/docs/errors/CAGE-MIG-005.md +1 -1
- package/templates/app/docs/errors/CAGE-NAME-001.md +1 -1
- package/templates/app/docs/errors/CAGE-NAME-002.md +1 -1
- package/templates/app/docs/errors/CAGE-NAME-003.md +1 -1
- package/templates/app/docs/errors/CAGE-NAME-004.md +1 -1
- package/templates/app/docs/errors/CAGE-NAME-005.md +1 -1
- package/templates/app/docs/errors/CAGE-NAME-006.md +1 -1
- package/templates/app/docs/errors/CAGE-ORG-001.md +1 -1
- package/templates/app/docs/errors/CAGE-PAIR-002.md +1 -1
- package/templates/app/docs/errors/CAGE-QUERY-001.md +1 -1
- package/templates/app/docs/errors/CAGE-QUERY-002.md +1 -1
- package/templates/app/docs/errors/CAGE-QUERY-003.md +1 -1
- package/templates/app/docs/errors/CAGE-REACT-001.md +1 -1
- package/templates/app/docs/errors/CAGE-REACT-002.md +1 -1
- package/templates/app/docs/errors/CAGE-REACT-003.md +1 -1
- package/templates/app/docs/errors/CAGE-READS-001.md +1 -1
- package/templates/app/docs/errors/CAGE-REG-001.md +2 -2
- package/templates/app/docs/errors/CAGE-REG-002.md +1 -1
- package/templates/app/docs/errors/CAGE-REG-003.md +1 -1
- package/templates/app/docs/errors/CAGE-REL-001.md +1 -1
- package/templates/app/docs/errors/CAGE-ROUTE-001.md +1 -1
- package/templates/app/docs/errors/CAGE-SCOPE-001.md +1 -1
- package/templates/app/docs/errors/CAGE-SCOPE-002.md +1 -1
- package/templates/app/docs/errors/CAGE-SECRET-001.md +1 -1
- package/templates/app/docs/errors/CAGE-SECRET-002.md +1 -1
- package/templates/app/docs/errors/CAGE-SENS-001.md +1 -1
- package/templates/app/docs/errors/CAGE-SQL-001.md +1 -1
- package/templates/app/docs/errors/CAGE-SQL-002.md +1 -1
- package/templates/app/docs/errors/CAGE-STATE-001.md +1 -1
- package/templates/app/docs/errors/CAGE-STATE-002.md +1 -1
- package/templates/app/docs/errors/CAGE-STATE-003.md +1 -1
- package/templates/app/docs/errors/CAGE-STATE-004.md +1 -1
- package/templates/app/docs/errors/CAGE-STATE-005.md +1 -1
- package/templates/app/docs/errors/CAGE-STATE-010.md +1 -1
- package/templates/app/docs/errors/CAGE-STATE-011.md +1 -1
- package/templates/app/docs/errors/CAGE-STRUCT-001.md +4 -4
- package/templates/app/docs/errors/CAGE-STRUCT-002.md +3 -3
- package/templates/app/docs/errors/CAGE-SYNC-001.md +4 -4
- package/templates/app/docs/errors/CAGE-TEST-001.md +1 -1
- package/templates/app/docs/errors/CAGE-TEST-002.md +1 -1
- package/templates/app/docs/errors/CAGE-TEST-003.md +1 -1
- package/templates/app/docs/errors/CAGE-UNSAFE-001.md +1 -1
- package/templates/app/docs/errors/CAGE-UPGRADE-001.md +1 -1
- package/templates/app/docs/errors/CAGE-WORKER-001.md +1 -1
- package/templates/app/docs/errors/CAGE-WORKER-002.md +1 -1
- package/templates/app/docs/errors/CAGE-WORKER-003.md +1 -1
- package/templates/app/docs/errors/README.md +3 -3
- package/templates/app/docs/errors/runtime.md +1 -1
- package/templates/app/package.json +3 -3
- package/templates/app/packages/contracts/package.json +1 -1
- package/templates/app/packages/contracts/src/aggregates/approval-member/index.ts +70 -0
- package/templates/app/packages/contracts/src/aggregates/purchase-order/index.ts +10 -2
- package/templates/app/packages/contracts/src/index.ts +9 -0
- package/templates/app/packages/domain/package.json +5 -5
- package/templates/app/packages/domain/src/modules/purchase/approval-members.ts +54 -0
- package/templates/app/packages/domain/src/modules/purchase/approvals.ts +19 -8
- package/templates/app/packages/domain/src/modules/purchase/index.ts +2 -1
- package/templates/app/packages/domain/src/modules/purchase/intelligence.test.ts +169 -7
- package/templates/app/packages/domain/src/modules/purchase/purchase-order.ts +56 -5
- package/templates/blank/.agents/skills/cage-development/SKILL.md +10 -0
- package/templates/blank/AGENTS.md +39 -122
- package/templates/blank/CLAUDE.md +6 -0
- package/templates/blank/README.md +3 -3
- package/templates/blank/apps/api/package.json +1 -1
- package/templates/blank/apps/web/package.json +1 -1
- package/templates/blank/docs/README.md +11 -0
- package/templates/blank/docs/development/README.md +64 -0
- package/templates/blank/docs/development/examples/note/note-detail-page.tsx +106 -0
- package/templates/blank/docs/development/examples/note/note.ts +67 -0
- package/templates/blank/docs/development/examples/note/notes-page.tsx +87 -0
- package/templates/{app/docs/ai → blank/docs/development/examples}/purchase-example.md +24 -10
- package/templates/{app/docs/ai → blank/docs/development/getting-started}/architecture.md +4 -4
- package/templates/blank/docs/development/getting-started/first-feature.md +66 -0
- package/templates/{app/docs/ai → blank/docs/development/getting-started}/workflow.md +13 -12
- package/templates/blank/docs/development/guides/approvals.md +270 -0
- package/templates/blank/docs/development/guides/business-collaboration.md +63 -0
- package/templates/{app/docs/ai → blank/docs/development/guides}/files.md +5 -1
- package/templates/blank/docs/{ai → development/guides}/intelligence.md +4 -4
- package/templates/{app/docs/ai → blank/docs/development/guides}/recipes.md +31 -11
- package/templates/blank/docs/{ai → development/guides}/relations.md +2 -2
- package/templates/blank/docs/{ai → development/guides}/routing.md +1 -1
- package/templates/{app/docs/ai → blank/docs/development/guides}/transfers.md +1 -1
- package/templates/{app/docs/ai → blank/docs/development/reference}/commenting.md +1 -1
- package/templates/blank/docs/{ai → development/reference}/primitives.md +8 -62
- package/templates/blank/docs/{ai → development/reference}/project-structure.md +2 -2
- package/templates/blank/docs/development/skills/cage-development/SKILL.md +134 -0
- package/templates/{app/docs/ai → blank/docs/development/tools}/diagnostics.md +3 -3
- package/templates/{app/docs/ai → blank/docs/development/tools}/ontology-context.md +3 -3
- package/templates/blank/docs/{ai → development/tools}/troubleshooting.md +7 -1
- package/templates/blank/docs/errors/CAGE-AI-001.md +3 -3
- package/templates/blank/docs/errors/CAGE-APPR-001.md +13 -10
- package/templates/blank/docs/errors/CAGE-APPR-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-APPR-003.md +1 -1
- package/templates/blank/docs/errors/CAGE-APPR-004.md +1 -1
- package/templates/blank/docs/errors/CAGE-APPR-005.md +5 -5
- package/templates/blank/docs/errors/CAGE-APPR-006.md +25 -10
- package/templates/blank/docs/errors/CAGE-APPR-007.md +9 -7
- package/templates/blank/docs/errors/CAGE-APPR-010.md +1 -1
- package/templates/blank/docs/errors/CAGE-AUDIT-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-AUTH-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-BIZ-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-BIZ-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-BUDGET-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-BUDGET-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-BUDGET-003.md +1 -1
- package/templates/blank/docs/errors/CAGE-CMD-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-CMD-003.md +1 -1
- package/templates/blank/docs/errors/CAGE-CONN-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-CONN-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-CONN-003.md +1 -1
- package/templates/blank/docs/errors/CAGE-CONN-004.md +1 -1
- package/templates/blank/docs/errors/CAGE-CONN-005.md +1 -1
- package/templates/blank/docs/errors/CAGE-CONN-006.md +1 -1
- package/templates/blank/docs/errors/CAGE-CONN-007.md +1 -1
- package/templates/blank/docs/errors/CAGE-CONN-008.md +1 -1
- package/templates/blank/docs/errors/CAGE-CONN-009.md +1 -1
- package/templates/blank/docs/errors/CAGE-CONN-010.md +1 -1
- package/templates/blank/docs/errors/CAGE-CONN-011.md +1 -1
- package/templates/blank/docs/errors/CAGE-CONN-012.md +1 -1
- package/templates/blank/docs/errors/CAGE-CONN-013.md +1 -1
- package/templates/blank/docs/errors/CAGE-CONN-014.md +1 -1
- package/templates/blank/docs/errors/CAGE-CONN-015.md +1 -1
- package/templates/blank/docs/errors/CAGE-CREATE-001.md +2 -2
- package/templates/blank/docs/errors/CAGE-DECL-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-DECL-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-DECL-003.md +1 -1
- package/templates/blank/docs/errors/CAGE-DECL-004.md +1 -1
- package/templates/blank/docs/errors/CAGE-DEP-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-DEP-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-DEP-003.md +1 -1
- package/templates/blank/docs/errors/CAGE-DEP-004.md +1 -1
- package/templates/blank/docs/errors/CAGE-DEP-005.md +1 -1
- package/templates/blank/docs/errors/CAGE-DIAG-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-DLQ-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-ENV-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-ENV-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-ENV-003.md +1 -1
- package/templates/blank/docs/errors/CAGE-EVENT-001.md +2 -2
- package/templates/blank/docs/errors/CAGE-EVENT-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-FIELD-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-FIELD-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-FIELD-003.md +1 -1
- package/templates/blank/docs/errors/CAGE-FIELD-004.md +1 -1
- package/templates/blank/docs/errors/CAGE-FILE-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-GEN-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-GEN-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-GEN-003.md +1 -1
- package/templates/blank/docs/errors/CAGE-GRANT-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-GRANT-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-GRANT-003.md +1 -1
- package/templates/blank/docs/errors/CAGE-IMPACT-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-IMPACT-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-INIT-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-INIT-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-INIT-003.md +1 -1
- package/templates/blank/docs/errors/CAGE-INIT-004.md +1 -1
- package/templates/blank/docs/errors/CAGE-INIT-005.md +1 -1
- package/templates/blank/docs/errors/CAGE-JOB-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-JOB-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-JOB-003.md +1 -1
- package/templates/blank/docs/errors/CAGE-LINT-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-MIG-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-MIG-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-MIG-005.md +1 -1
- package/templates/blank/docs/errors/CAGE-NAME-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-NAME-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-NAME-003.md +1 -1
- package/templates/blank/docs/errors/CAGE-NAME-004.md +1 -1
- package/templates/blank/docs/errors/CAGE-NAME-005.md +1 -1
- package/templates/blank/docs/errors/CAGE-NAME-006.md +1 -1
- package/templates/blank/docs/errors/CAGE-ORG-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-PAIR-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-QUERY-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-QUERY-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-QUERY-003.md +1 -1
- package/templates/blank/docs/errors/CAGE-REACT-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-REACT-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-REACT-003.md +1 -1
- package/templates/blank/docs/errors/CAGE-READS-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-REG-001.md +2 -2
- package/templates/blank/docs/errors/CAGE-REG-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-REG-003.md +1 -1
- package/templates/blank/docs/errors/CAGE-REL-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-ROUTE-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-SCOPE-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-SCOPE-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-SECRET-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-SECRET-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-SENS-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-SQL-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-SQL-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-STATE-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-STATE-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-STATE-003.md +1 -1
- package/templates/blank/docs/errors/CAGE-STATE-004.md +1 -1
- package/templates/blank/docs/errors/CAGE-STATE-005.md +1 -1
- package/templates/blank/docs/errors/CAGE-STATE-010.md +1 -1
- package/templates/blank/docs/errors/CAGE-STATE-011.md +1 -1
- package/templates/blank/docs/errors/CAGE-STRUCT-001.md +4 -4
- package/templates/blank/docs/errors/CAGE-STRUCT-002.md +3 -3
- package/templates/blank/docs/errors/CAGE-SYNC-001.md +4 -4
- package/templates/blank/docs/errors/CAGE-TEST-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-TEST-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-TEST-003.md +1 -1
- package/templates/blank/docs/errors/CAGE-UNSAFE-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-UPGRADE-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-WORKER-001.md +1 -1
- package/templates/blank/docs/errors/CAGE-WORKER-002.md +1 -1
- package/templates/blank/docs/errors/CAGE-WORKER-003.md +1 -1
- package/templates/blank/docs/errors/README.md +3 -3
- package/templates/blank/docs/errors/runtime.md +1 -1
- package/templates/blank/docs/examples/purchase-admin/README.md +1 -1
- package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/approval/components/task-operation-dialog.tsx +61 -0
- package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/approval/hooks/use-approval-tasks.ts +6 -9
- package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/approval/pages/approval-tasks-page.tsx +81 -49
- package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/approval/pages/approval-tasks.css +18 -0
- package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/purchase/components/approval-history.tsx +39 -4
- package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/purchase/components/order-reviewers.tsx +38 -0
- package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/purchase/components/purchase-order-form.tsx +9 -2
- package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/purchase/pages/purchase-intelligence.css +2 -0
- package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/purchase/pages/purchase-order-detail-page.tsx +2 -0
- package/templates/blank/docs/examples/purchase-admin/apps/web/src/modules/purchase/pages/purchase-orders-page.tsx +2 -0
- package/templates/blank/docs/examples/purchase-admin/apps/web/src/shared/lib/error-message.ts +6 -1
- package/templates/blank/docs/examples/purchase-admin/apps/web/src/shared/ui/approval-member-picker.css +10 -0
- package/templates/blank/docs/examples/purchase-admin/apps/web/src/shared/ui/approval-member-picker.tsx +51 -0
- package/templates/blank/docs/examples/purchase-admin/cage.config.json +14 -0
- package/templates/blank/docs/examples/purchase-admin/packages/contracts/src/aggregates/approval-member/index.ts +70 -0
- package/templates/blank/docs/examples/purchase-admin/packages/contracts/src/aggregates/purchase-order/index.ts +10 -2
- package/templates/blank/docs/examples/purchase-admin/packages/contracts/src/index.ts +9 -0
- package/templates/blank/docs/examples/purchase-admin/packages/domain/src/modules/purchase/approval-members.ts +54 -0
- package/templates/blank/docs/examples/purchase-admin/packages/domain/src/modules/purchase/approvals.ts +19 -8
- package/templates/blank/docs/examples/purchase-admin/packages/domain/src/modules/purchase/index.ts +2 -1
- package/templates/blank/docs/examples/purchase-admin/packages/domain/src/modules/purchase/intelligence.test.ts +169 -7
- package/templates/blank/docs/examples/purchase-admin/packages/domain/src/modules/purchase/purchase-order.ts +56 -5
- package/templates/blank/package.json +3 -3
- package/templates/blank/packages/contracts/package.json +1 -1
- package/templates/blank/packages/domain/package.json +5 -5
- package/templates/app/docs/ai/README.md +0 -98
- package/templates/blank/docs/ai/README.md +0 -98
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 人工待办的转办、委派和三种加签操作表单,由审批收件箱按当前任务快照打开。
|
|
3
|
+
* 选择来自业务协作名录,提交携带任务 revision 和必填原因;权限/同节点重复责任由引擎重查。
|
|
4
|
+
* 失败保留选人和原因;成功刷新真实任务并关闭。等待/受托任务不从页面打开本表单,不在前端改流程状态。
|
|
5
|
+
*/
|
|
6
|
+
import { useState, type FormEvent } from 'react';
|
|
7
|
+
import { client } from '@app/contracts';
|
|
8
|
+
import type { ApprovalTask, ApprovalTaskAction } from '@yss123/cage-client';
|
|
9
|
+
import { ApprovalMemberPicker } from '../../../shared/ui/approval-member-picker';
|
|
10
|
+
import { errorMessage } from '../../../shared/lib/error-message';
|
|
11
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
12
|
+
|
|
13
|
+
/** 页面允许发起的人员管理操作,归还/领取/释放在任务行直接处理。 */
|
|
14
|
+
export type PersonnelOperation = 'transfer' | 'delegate' | 'parallel' | 'before' | 'after';
|
|
15
|
+
/** 业务名称与引擎 placement 对应;不引入额外执行步骤或暗示自动授予权限。 */
|
|
16
|
+
const labels = { transfer: '转办审批', delegate: '委派协助', parallel: '并行加签', before: '前加签', after: '后加签' } as const;
|
|
17
|
+
const descriptions = {
|
|
18
|
+
transfer: '将这项审批责任交给另一位人员,你将不再处理此任务。',
|
|
19
|
+
delegate: '请他人核对材料,受托人归还后仍由你决定。',
|
|
20
|
+
parallel: '你与新增人员可以同时处理,所有追加意见都是必签。',
|
|
21
|
+
before: '先请新增人员签署,全部完成后这项任务才回到你手中。',
|
|
22
|
+
after: '你先签署,随后激活新增人员的必签任务。',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* task 与 operation 在打开时固定;任务变化以服务端冲突响应为准,不自动改用新版本重试。
|
|
27
|
+
* close 返回当前收件箱,成功通过查询缓存刷新;无效输入禁用提交,Esc/取消不产生写入。
|
|
28
|
+
*/
|
|
29
|
+
export function TaskOperationDialog({ task, operation, close }: { task: ApprovalTask; operation: PersonnelOperation; close: () => void }) {
|
|
30
|
+
const [userIds, setUserIds] = useState<string[]>([]), [reason, setReason] = useState('');
|
|
31
|
+
const [busy, setBusy] = useState(false), [error, setError] = useState('');
|
|
32
|
+
const cache = useQueryClient();
|
|
33
|
+
const single = operation === 'transfer' || operation === 'delegate';
|
|
34
|
+
/**
|
|
35
|
+
* 将人员选择映射为对应动作;每次携带打开弹窗时的 revision,旧任务冲突时保留输入供用户核对。
|
|
36
|
+
* 只在服务端确认成功后刷新并关闭,不能把选中人员或提交请求本身视为已经完成交接。
|
|
37
|
+
*/
|
|
38
|
+
async function submit(event: FormEvent) {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
if (busy || !userIds.length || !reason.trim()) return;
|
|
41
|
+
setBusy(true); setError('');
|
|
42
|
+
const input: ApprovalTaskAction = single
|
|
43
|
+
? { action: operation, targetUserId: userIds[0], reason, expectedRevision: task.revision }
|
|
44
|
+
: { action: 'addSign', placement: operation, userIds, reason, expectedRevision: task.revision };
|
|
45
|
+
const result = await client.approvalTaskAction(task.id, input);
|
|
46
|
+
setBusy(false);
|
|
47
|
+
if (!result.ok) { setError(errorMessage(result)); return; }
|
|
48
|
+
await cache.invalidateQueries(); close();
|
|
49
|
+
}
|
|
50
|
+
return <div className="modal-backdrop"><section className="modal approval-operation-dialog" role="dialog" aria-modal="true" aria-labelledby="approval-operation-title"
|
|
51
|
+
onKeyDown={event => { if (event.key === 'Escape' && !busy) close(); }}>
|
|
52
|
+
<div className="section-head"><div><h2 id="approval-operation-title">{labels[operation]}</h2><p className="muted">{task.title}</p></div>
|
|
53
|
+
<button type="button" disabled={busy} onClick={close} aria-label="关闭操作">×</button></div>
|
|
54
|
+
<form onSubmit={submit}><p>{descriptions[operation]}</p>
|
|
55
|
+
<ApprovalMemberPicker value={userIds} onChange={setUserIds} max={single ? 1 : 20} exclude={[task.assigneeId, task.ownerId].filter((id): id is string => !!id)} disabled={busy} />
|
|
56
|
+
<label>操作原因<textarea autoFocus rows={3} maxLength={1000} required value={reason} disabled={busy} onChange={event => setReason(event.target.value)} placeholder="说明业务原因,供单据参与者查看" /></label>
|
|
57
|
+
{error && <p className="error" role="alert">{error}</p>}
|
|
58
|
+
<div className="actions"><button type="button" disabled={busy} onClick={close}>取消</button><button className="primary" disabled={busy || !userIds.length || !reason.trim()}>{busy ? '提交中…' : `确认${labels[operation]}`}</button></div>
|
|
59
|
+
</form>
|
|
60
|
+
</section></div>;
|
|
61
|
+
}
|
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 当前账号本人、候选或等待加签任务的共享查询入口,默认待办供 app 数量组合复用。
|
|
3
3
|
* 任务分配与实际决策授权均来自服务端,不从角色名称在前端自行推导。
|
|
4
4
|
*/
|
|
5
5
|
import { useQuery } from '@tanstack/react-query';
|
|
6
6
|
import { client } from '@app/contracts';
|
|
7
7
|
import { errorMessage } from '../../../shared/lib/error-message';
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* 待办列表展示所需字段:id 是任务标识,businessKey 是业务记录键,title 为任务标题,createdAt 为创建时间字符串。
|
|
11
|
-
*/
|
|
12
|
-
type Task = { id: string; businessKey: string; title: string; createdAt: string };
|
|
13
|
-
|
|
14
9
|
/**
|
|
15
10
|
* 审批是框架通用接口,不属于某个聚合的生成方法,因此请求封装在审批模块内。
|
|
16
11
|
* 待办页面和导航角标复用同一缓存键,调用方不需要了解接口地址或响应形状。
|
|
17
12
|
*/
|
|
18
13
|
/**
|
|
19
14
|
* 在 React Query 中读取审批待办,非 ok 结果转为查询错误。
|
|
15
|
+
* view 默认 pending,与导航角标共享;candidates/waiting 使用独立缓存,不混入可决定的任务数量。
|
|
20
16
|
* 返回查询状态及 refetch;空数组表示当前结果没有待办,未加载或失败不等同于零条。
|
|
21
17
|
*/
|
|
22
|
-
export function useApprovalTasks() {
|
|
18
|
+
export function useApprovalTasks(view: 'pending' | 'candidates' | 'waiting' = 'pending') {
|
|
23
19
|
return useQuery({
|
|
24
|
-
queryKey: ['tasks'],
|
|
20
|
+
queryKey: ['tasks', view],
|
|
25
21
|
queryFn: async () => {
|
|
26
|
-
|
|
22
|
+
// 使用框架任务协议保留 revision,避免页面决定丢失任务流转后的并发校验。
|
|
23
|
+
const result = await (view === 'candidates' ? client.approvalCandidateTasks() : client.approvalTasks(view));
|
|
27
24
|
if (!result.ok) throw new Error(errorMessage(result));
|
|
28
25
|
return result.data;
|
|
29
26
|
},
|
|
@@ -1,72 +1,104 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* 通过 app
|
|
2
|
+
* 审批收件箱使用 URL 的 view 切换本人、可领取和等待加签任务,刷新/前进后退保留视图。
|
|
3
|
+
* 通过 app 注入详情链接,界面调用客户端完成领取、释放、人员流转与决定;候选身份和版本仍由服务端校验。
|
|
4
|
+
* 原因是行内临时草稿,动作成功后由 useCommand 失效查询并重读;waiting 仅供查看,不显示决定入口。
|
|
4
5
|
*/
|
|
5
|
-
|
|
6
|
+
import { useState } from 'react';
|
|
6
7
|
import { client } from '@app/contracts';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
8
|
+
import type { ApprovalTask } from '@yss123/cage-client';
|
|
9
|
+
import { Link, useSearchParams } from 'react-router-dom';
|
|
10
|
+
import { useCommand, type RunCommand } from '../../../shared/hooks/use-command';
|
|
9
11
|
import { Notice } from '../../../shared/ui/notice';
|
|
10
12
|
import { PageHeading } from '../../../shared/ui/page-heading';
|
|
11
13
|
import { useApprovalTasks } from '../hooks/use-approval-tasks';
|
|
12
14
|
import { AssistantPage } from '../../../shared/ui/assistant-scope';
|
|
15
|
+
import './approval-tasks.css';
|
|
16
|
+
import { TaskOperationDialog, type PersonnelOperation } from '../components/task-operation-dialog';
|
|
17
|
+
|
|
18
|
+
/** 三种任务视图进入查询参数;只有 pending 会计入原有导航待办数。 */
|
|
19
|
+
const views = { pending: '待我处理', candidates: '可领取', waiting: '等待加签' } as const;
|
|
13
20
|
|
|
14
21
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
22
|
+
* 为一项任务展示顺序、意见和合法阶段入口。busy 来自整页,提交时防止同页重复操作。
|
|
23
|
+
* 领取/释放/委派归还必须人工填写原因;决定意见可选。每次都提交当前 revision,失败保留草稿供核对。
|
|
24
|
+
* @param task 服务端安全摘要;assigneeId 为空表示尚未领取,不可直接批准。
|
|
25
|
+
* @param orderUrl app 组合的详情 URL 生成器,不访问其他业务模块内部。
|
|
26
|
+
* @param run 统一结果/缓存处理;此组件不乐观修改任务或赋予权限。
|
|
27
|
+
*/
|
|
28
|
+
function TaskRow({ task, orderUrl, busy, run }: { task: ApprovalTask; orderUrl: (id: string) => string; busy: boolean; run: RunCommand }) {
|
|
29
|
+
const [reason, setReason] = useState('');
|
|
30
|
+
const [operation, setOperation] = useState<PersonnelOperation | null>(null);
|
|
31
|
+
const waiting = task.status === 'waiting';
|
|
32
|
+
const candidate = task.assigneeId === null;
|
|
33
|
+
const delegated = task.delegation === 'pending';
|
|
34
|
+
const manage = (action: 'claim' | 'unclaim' | 'resolve', success: string) => run(
|
|
35
|
+
() => client.approvalTaskAction(task.id, { action, expectedRevision: task.revision, reason }), success);
|
|
36
|
+
const position = task.signPosition === 'before' ? '前签' : task.signPosition === 'after' ? '后签' : task.taskKind === 'additional' ? '并行加签' : null;
|
|
37
|
+
return (
|
|
38
|
+
<div className="task">
|
|
39
|
+
<div className="approval-task-summary">
|
|
40
|
+
<h3>{task.title}{position && <small className="muted"> · {position}</small>}</h3>
|
|
41
|
+
{!!task.branchPath?.length && <p className="muted">{task.branchPath.map(part => `${part.parallel} · ${part.branch}`).join(' / ')}</p>}
|
|
42
|
+
<p className="mono muted">PO-{task.businessKey.slice(0, 8).toUpperCase()}</p>
|
|
43
|
+
<p className="muted">金额 ¥{String(task.vars.amount ?? '—')}{task.vars.needsSpecialistReview === true ? ' · 含专业会签' : ''}</p>
|
|
44
|
+
<p className="muted">{waiting ? '前置签署完成后恢复处理' : candidate ? '领取后由你负责处理' : delegated ? '受托处理中,请先归还原审批人' : '请核对单据后提交意见'}</p>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="approval-task-controls">
|
|
47
|
+
<div className="actions">
|
|
48
|
+
<Link className="route-link" to={orderUrl(task.businessKey)}>查看采购单 ↗</Link>
|
|
49
|
+
{!waiting && <input aria-label={`${task.title}处理意见`} placeholder={candidate || delegated ? '填写操作原因(必填)' : '审批意见;释放任务时必填'}
|
|
50
|
+
maxLength={1000} value={reason} disabled={busy} onChange={event => setReason(event.target.value)} />}
|
|
51
|
+
</div>
|
|
52
|
+
{!waiting && <div className="actions">
|
|
53
|
+
{candidate ? <button className="primary" disabled={busy || !reason.trim()} onClick={() => void manage('claim', '已领取,请在待我处理中继续')}>领取任务</button>
|
|
54
|
+
: delegated ? <button disabled={busy || !reason.trim()} onClick={() => void manage('resolve', '已归还原审批人')}>归还审批人</button>
|
|
55
|
+
: <>
|
|
56
|
+
{task.assignment === 'candidate' && <button disabled={busy || !reason.trim()} onClick={() => void manage('unclaim', '已释放到候选池')}>释放任务</button>}
|
|
57
|
+
<button disabled={busy} onClick={() => void run(() => client.decideApprovalTask(task.id, 'reject', { expectedRevision: task.revision, comment: reason }), '已驳回')}>驳回</button>
|
|
58
|
+
<button disabled={busy} onClick={() => void run(() => client.decideApprovalTask(task.id, 'return', { expectedRevision: task.revision, comment: reason }), '已退回修改')}>退回修改</button>
|
|
59
|
+
<button disabled={busy} className="primary" onClick={() => void run(() => client.decideApprovalTask(task.id, 'approve', { expectedRevision: task.revision, comment: reason }), '已通过审批')}>通过审批</button>
|
|
60
|
+
{task.actions.transfer && <button disabled={busy} onClick={() => setOperation('transfer')}>转办</button>}
|
|
61
|
+
{task.actions.delegate && <button disabled={busy} onClick={() => setOperation('delegate')}>委派</button>}
|
|
62
|
+
{task.actions.addSign && task.taskKind === 'original' && task.signPositions.map(position => <button key={position} disabled={busy}
|
|
63
|
+
onClick={() => setOperation(position)}>{position === 'before' ? '前加签' : position === 'after' ? '后加签' : '并行加签'}</button>)}
|
|
64
|
+
</>}
|
|
65
|
+
</div>}
|
|
66
|
+
</div>
|
|
67
|
+
{operation && <TaskOperationDialog task={task} operation={operation} close={() => setOperation(null)} />}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 读取当前视图并区分加载、错误与空结果;URL 中未知 view 按默认待办读取,不制造无效请求。
|
|
74
|
+
* @param orderUrl 由 app 将业务主键转为采购详情地址;组件只改变 view 参数,保留其他 URL 参数。
|
|
75
|
+
* 人员操作使用业务协作名录,不读取完整组织用户;动作政策与签序来自实例快照,不按最新声明猜测在途权限。
|
|
18
76
|
*/
|
|
19
77
|
export function ApprovalTasksPage({ orderUrl }: { orderUrl: (id: string) => string }) {
|
|
20
|
-
const
|
|
78
|
+
const [params, setParams] = useSearchParams();
|
|
79
|
+
const selected = params.get('view');
|
|
80
|
+
const view = selected === 'candidates' || selected === 'waiting' ? selected : 'pending';
|
|
81
|
+
const tasks = useApprovalTasks(view);
|
|
21
82
|
const { run, busy, message, clearMessage } = useCommand();
|
|
22
83
|
return (
|
|
23
84
|
<>
|
|
24
85
|
<AssistantPage context={{ page: { id: 'approval-inbox', title: '审批待办', kind: 'list', route: '/approvals' },
|
|
25
|
-
visibleContent: [{ id: '
|
|
86
|
+
visibleContent: [{ id: 'visible-tasks', text: tasks.data ? `${views[view]}当前显示 ${tasks.data.length} 项,以重新查询为准。` : '审批任务正在读取或暂时不可用。' }],
|
|
26
87
|
}} suggestions={['查看当前待我处理的审批', '通过、驳回和退回修改有什么区别?']} />
|
|
27
|
-
<PageHeading eyebrow="APPROVAL INBOX" title="审批待办" description="
|
|
88
|
+
<PageHeading eyebrow="APPROVAL INBOX" title="审批待办" description="领取、处理和跟进你的审批任务。" />
|
|
28
89
|
{message && <Notice onClose={clearMessage}>{message}</Notice>}
|
|
29
|
-
<section className="panel">
|
|
90
|
+
<section className="panel approval-inbox">
|
|
30
91
|
<div className="section-head">
|
|
31
|
-
<
|
|
32
|
-
|
|
92
|
+
<div className="actions" aria-label="审批任务视图">
|
|
93
|
+
{Object.entries(views).map(([key, label]) => <button key={key} disabled={busy} aria-pressed={key === view} className={key === view ? 'primary' : ''}
|
|
94
|
+
onClick={() => setParams(previous => { const next = new URLSearchParams(previous); if (key === 'pending') next.delete('view'); else next.set('view', key); return next; })}>{label}</button>)}
|
|
95
|
+
</div>
|
|
96
|
+
<button disabled={busy || tasks.isFetching} onClick={() => void tasks.refetch()}>刷新</button>
|
|
33
97
|
</div>
|
|
34
98
|
{tasks.isError && <p className="error">{tasks.error.message}</p>}
|
|
35
|
-
{tasks.isPending && <p className="empty"
|
|
36
|
-
{tasks.data?.map(
|
|
37
|
-
|
|
38
|
-
<div>
|
|
39
|
-
<h3>{task.title}</h3>
|
|
40
|
-
<p className="mono muted">PO-{task.businessKey.slice(0, 8).toUpperCase()}</p>
|
|
41
|
-
</div>
|
|
42
|
-
<div className="actions">
|
|
43
|
-
{/* 单据的页面地址由 app 组合层提供,审批模块不依赖采购页面内部文件。 */}
|
|
44
|
-
<Link className="route-link" to={orderUrl(task.businessKey)}>
|
|
45
|
-
查看采购单 ↗
|
|
46
|
-
</Link>
|
|
47
|
-
<button
|
|
48
|
-
disabled={busy}
|
|
49
|
-
onClick={() => run(() => client.post(`/api/approval/tasks/${task.id}/reject`), '已驳回')}
|
|
50
|
-
>
|
|
51
|
-
驳回
|
|
52
|
-
</button>
|
|
53
|
-
<button disabled={busy} onClick={() => run(() => client.post(`/api/approval/tasks/${task.id}/return`), '已退回修改')}>退回修改</button>
|
|
54
|
-
<button
|
|
55
|
-
disabled={busy}
|
|
56
|
-
className="primary"
|
|
57
|
-
onClick={() => run(() => client.post(`/api/approval/tasks/${task.id}/approve`), '已通过审批')}
|
|
58
|
-
>
|
|
59
|
-
通过审批
|
|
60
|
-
</button>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
))}
|
|
64
|
-
{tasks.data?.length === 0 && (
|
|
65
|
-
<div className="empty">
|
|
66
|
-
<h3>待办已清空</h3>
|
|
67
|
-
<p>新任务分配给你后会显示在这里。</p>
|
|
68
|
-
</div>
|
|
69
|
-
)}
|
|
99
|
+
{tasks.isPending && <p className="empty">正在读取任务…</p>}
|
|
100
|
+
{tasks.data?.map(task => <TaskRow key={`${task.id}:${task.revision}`} task={task} orderUrl={orderUrl} busy={busy} run={run} />)}
|
|
101
|
+
{tasks.data?.length === 0 && <div className="empty"><h3>当前没有{views[view]}的任务</h3><p>任务分配或签署顺序变化后,可刷新查看。</p></div>}
|
|
70
102
|
</section>
|
|
71
103
|
</>
|
|
72
104
|
);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 审批收件箱局部布局,沿用工作台配色与按钮;将意见、详情及提交分行,避免窄屏压缩链接。
|
|
3
|
+
* 只作用于审批模块,桌面保留事项/操作两栏,空间不足自动换行;等待阶段由组件省略操作控件。
|
|
4
|
+
*/
|
|
5
|
+
.approval-inbox .task { flex-wrap: wrap; gap: 20px; }
|
|
6
|
+
.approval-task-summary { flex: 1 1 240px; }
|
|
7
|
+
.approval-task-controls { display: grid; gap: 12px; flex: 0 1 420px; }
|
|
8
|
+
.approval-task-controls .actions { align-items: center; flex-wrap: wrap; }
|
|
9
|
+
.approval-task-controls .route-link { white-space: nowrap; }
|
|
10
|
+
.approval-task-controls input { min-width: 180px; flex: 1; }
|
|
11
|
+
.approval-operation-dialog { width: min(760px, calc(100vw - 32px)); max-height: calc(100vh - 48px); overflow-y: auto; }
|
|
12
|
+
.approval-operation-dialog form { padding: 24px; }
|
|
13
|
+
.approval-operation-dialog textarea { width: 100%; }
|
|
14
|
+
.approval-inbox .section-head { gap: 16px; flex-wrap: wrap; }
|
|
15
|
+
@media (max-width: 660px) {
|
|
16
|
+
.approval-task-controls { flex-basis: 100%; }
|
|
17
|
+
.approval-inbox .task { padding: 20px; }
|
|
18
|
+
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 采购单审批轨迹展示,按单据与版本读取服务端整理后的轮次和步骤。
|
|
3
|
-
* AI
|
|
3
|
+
* AI 与人工状态使用同一记录来源;并行节点按服务端分支路径展示,序号仅定位节点、不代表串行先后。
|
|
4
|
+
* 受限详情不显示理由,不能根据缺少细节推断未执行;该组件只消费框架历史,不实现审批调度。
|
|
4
5
|
*/
|
|
5
|
-
import { useQuery } from '@tanstack/react-query';
|
|
6
|
+
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
|
|
6
7
|
import { client } from '@yss123/cage-client';
|
|
8
|
+
import { useApprovalMembers } from '../../../shared/ui/approval-member-picker';
|
|
9
|
+
|
|
10
|
+
/** 人工操作与引擎自动激活都来自已提交历史;未知动作保留原值,不能伪装成通过。 */
|
|
11
|
+
const actionLabels: Record<string, string> = { transfer: '转办', delegate: '委派协助', resolve: '归还审批人', addSign: '增加签署人', claim: '领取任务', unclaim: '释放任务', approved: '通过审批', rejected: '驳回', returned: '退回修改', cancelled: '取消流程', activated: '激活等待任务', skipped: '结束其余待办', advanced: '推进审批分支', urged: '申请人催办', reminded: '流程时限提醒' };
|
|
12
|
+
const positionLabels: Record<string, string> = { before: '前加签', after: '后加签', parallel: '并行加签' };
|
|
7
13
|
|
|
8
14
|
/**
|
|
9
15
|
* 运行、步骤与人工待办状态的展示映射,未知状态仍由页面按实际值处理。
|
|
@@ -19,14 +25,43 @@ export function ApprovalHistory({ id, version, pending }: { id: string; version:
|
|
|
19
25
|
if (!result.ok) throw new Error(`审批记录读取失败:${result.code}`);
|
|
20
26
|
return result.data;
|
|
21
27
|
}, refetchInterval: pending ? 1500 : false });
|
|
28
|
+
const members = useApprovalMembers(1, true);
|
|
29
|
+
// 跨轮次动作按服务端游标递增分页;轮询重读已经打开的页,避免只更新首屏或丢失后续意见。
|
|
30
|
+
const actions = useInfiniteQuery({ queryKey: ['approval-task-history', id], initialPageParam: undefined as string | undefined,
|
|
31
|
+
queryFn: async ({ pageParam }) => {
|
|
32
|
+
const result = await client.approvalTaskHistory('po-approval', 'PurchaseOrder', id, { after: pageParam, limit: 50 });
|
|
33
|
+
if (!result.ok) throw new Error(`操作记录读取失败:${result.code}`);
|
|
34
|
+
return result.data;
|
|
35
|
+
}, getNextPageParam: last => last.nextCursor ?? undefined, refetchInterval: pending ? 1500 : false });
|
|
36
|
+
const person = (userId: string | null) => userId ? members.data?.items.find(member => member.userId === userId)?.displayName ?? userId.slice(0, 8) : '系统';
|
|
22
37
|
return <section className="panel approval-history"><div className="section-head"><h2>审批记录</h2><span className="muted">退回后重新提交,从第一步重新审核</span></div>
|
|
23
38
|
{history.isPending && <p className="muted">正在读取审核记录…</p>}
|
|
24
39
|
{history.isError && <p role="alert">{history.error.message}</p>}
|
|
25
|
-
{history.data?.length === 0 && <p className="muted"
|
|
40
|
+
{history.data?.length === 0 && <p className="muted">提交后按已配置流程审批,实际环节和并行分支将在此显示。</p>}
|
|
26
41
|
{history.data?.map((round, index) => <div className="review-round" key={round.id}><h3>第 {history.data.length - index} 轮 · {status[round.status] ?? round.status}</h3>
|
|
27
|
-
{round.
|
|
42
|
+
{(round.currentSteps?.length ?? 0) > 1 && <p className="muted">当前有 {round.currentSteps!.length} 个审批环节并行办理,全部完成后才继续。</p>}
|
|
43
|
+
{round.steps.map(step => <div className="review-step" key={step.index}><strong>{step.index + 1}. {step.title ?? (step.kind === 'ai' ? 'AI 材料预审' : '人工审批')} · {status[step.status] ?? step.status}</strong>
|
|
44
|
+
{!!step.branchPath?.length && <p className="muted">{step.branchPath.map(part => `${part.parallel} · ${part.branch}`).join(' / ')}</p>}
|
|
28
45
|
{step.detailRestricted ? <p>当前权限无法查看审核解释。</p> : <>{step.decision && <p>{step.decision.reason}</p>}{step.decision?.suggestions.map((text, i) => <p className="review-suggestion" key={i}>{text}</p>)}</>}
|
|
29
46
|
</div>)}
|
|
30
47
|
</div>)}
|
|
48
|
+
<div className="review-actions"><h3>人员流转与处理意见</h3>
|
|
49
|
+
{actions.isPending && <p role="status">正在读取操作记录…</p>}
|
|
50
|
+
{actions.isError && <p role="alert">{actions.error.message} <button onClick={() => void actions.refetch()}>重试</button></p>}
|
|
51
|
+
{actions.data?.pages.flatMap(page => page.items).map(item => {
|
|
52
|
+
const placement = typeof item.details.placement === 'string' ? positionLabels[item.details.placement] : undefined;
|
|
53
|
+
const targets = Array.isArray(item.details.userIds) ? item.details.userIds.filter((value): value is string => typeof value === 'string') : [];
|
|
54
|
+
const to = typeof item.details.to === 'string' ? item.details.to : null;
|
|
55
|
+
const roundIndex = history.data?.findIndex(round => round.id === item.instanceId) ?? -1;
|
|
56
|
+
return <article className="review-step" key={item.id}>
|
|
57
|
+
<strong>{person(item.actorId)} · {placement ?? actionLabels[item.action] ?? item.action}</strong>
|
|
58
|
+
<small className="muted">{roundIndex >= 0 ? `第 ${history.data!.length - roundIndex} 轮 · ` : ''}{new Date(item.createdAt).toLocaleString('zh-CN')}</small>
|
|
59
|
+
{targets.length > 0 ? <p>签署人:{targets.map(person).join('、')}</p> : to && ['transfer', 'delegate', 'resolve'].includes(item.action) ? <p>交给:{person(to)}</p> : null}
|
|
60
|
+
{item.reason && <p>{item.reason}</p>}
|
|
61
|
+
</article>;
|
|
62
|
+
})}
|
|
63
|
+
{actions.data?.pages[0].items.length === 0 && <p className="muted">尚无人工操作记录。</p>}
|
|
64
|
+
{actions.hasNextPage && <button disabled={actions.isFetchingNextPage} onClick={() => void actions.fetchNextPage()}>加载后续记录</button>}
|
|
65
|
+
</div>
|
|
31
66
|
</section>;
|
|
32
67
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 采购详情中的专业复核配置,申请人在草稿或退回后选择实际会签人。
|
|
3
|
+
* 配置通过 setReviewers 命令保存,提交后只读;页面不能更改已经生成的审批实例快照。
|
|
4
|
+
* 名录只用于展示与选人,服务端重查有效名录、提单人隔离、状态、版本及命令权限。
|
|
5
|
+
*/
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
import { can, purchaseOrder, type PurchaseOrder } from '@app/contracts';
|
|
8
|
+
import { useCommand } from '../../../shared/hooks/use-command';
|
|
9
|
+
import { ApprovalMemberPicker, useApprovalMembers } from '../../../shared/ui/approval-member-picker';
|
|
10
|
+
import { Notice } from '../../../shared/ui/notice';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* order 为安全读取的最新单据;null 草稿表示未编辑,避免后台刷新覆盖用户尚未保存的选人。
|
|
14
|
+
* 保存携带当前业务版本,成功清空本地草稿并刷新缓存;失败保留选择和提示,不重试写入。
|
|
15
|
+
* 名录首页外或已不可读人员保留 UUID,不能把查不到名称解释为已删除或无需签署。
|
|
16
|
+
*/
|
|
17
|
+
export function OrderReviewers({ order }: { order: PurchaseOrder }) {
|
|
18
|
+
const [draft, setDraft] = useState<{ enabled: boolean; ids: string[] } | null>(null);
|
|
19
|
+
const members = useApprovalMembers(1, true);
|
|
20
|
+
const { run, busy, message, clearMessage } = useCommand();
|
|
21
|
+
const editable = order.status === 'draft' && can('PurchaseOrder.setReviewers');
|
|
22
|
+
const selection = draft ?? { enabled: order.needsSpecialistReview ?? false, ids: order.reviewerIds ?? [] };
|
|
23
|
+
const names = (order.reviewerIds ?? []).map(id => members.data?.items.find(member => member.userId === id)?.displayName ?? id);
|
|
24
|
+
return <section className="purpose-editor">
|
|
25
|
+
<h2>审批安排{draft && editable && <small className="muted"> · 尚未保存</small>}</h2>
|
|
26
|
+
<p className="muted">{selection.enabled ? '需要所选专业人员全部签署。' : '本单不要求专业人员会签。'}具体流转与其他审批环节以提交后的流程记录为准。</p>
|
|
27
|
+
{message && <Notice onClose={clearMessage}>{message}</Notice>}
|
|
28
|
+
{editable ? <>
|
|
29
|
+
<label className="review-checkbox"><input type="checkbox" checked={selection.enabled} disabled={busy}
|
|
30
|
+
onChange={event => setDraft({ ...selection, enabled: event.target.checked })} />需要专业会签(技术、质量等)</label>
|
|
31
|
+
{selection.enabled && <ApprovalMemberPicker value={selection.ids} onChange={ids => setDraft({ ...selection, ids })} exclude={[order.createdBy]} disabled={busy} />}
|
|
32
|
+
<div className="actions"><span className="muted">退回后可重新安排,重提将开启新一轮审批。</span>
|
|
33
|
+
<button disabled={busy || draft === null || (selection.enabled && !selection.ids.length)} onClick={async () => {
|
|
34
|
+
if (await run(() => purchaseOrder.setReviewers(order.id, { needsSpecialistReview: selection.enabled, reviewerIds: selection.enabled ? selection.ids : [] }, order.version), '审批安排已保存')) setDraft(null);
|
|
35
|
+
}}>保存审批安排</button></div>
|
|
36
|
+
</> : <p>{order.needsSpecialistReview ? `专业会签:${names.join('、')}` : '无需专业会签'}</p>}
|
|
37
|
+
</section>;
|
|
38
|
+
}
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
// 只处理采购草稿输入;状态迁移、创建人和权限仍由领域命令决定。
|
|
6
6
|
import { useState, type FormEvent } from 'react';
|
|
7
|
-
import { purchaseOrder, PurchaseOrderFileFields } from '@app/contracts';
|
|
7
|
+
import { purchaseOrder, PurchaseOrderFileFields, currentUser } from '@app/contracts';
|
|
8
8
|
import type { RunCommand } from '../../../shared/hooks/use-command';
|
|
9
9
|
import { AttachmentUpload } from '../../../shared/ui/attachment-upload';
|
|
10
10
|
import './purchase-order-form.css';
|
|
11
|
+
import { ApprovalMemberPicker } from '../../../shared/ui/approval-member-picker';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* 在弹窗中维护附件列表、上传状态和提交状态。
|
|
@@ -18,6 +19,8 @@ export function PurchaseOrderForm({ close, run }: { close: () => void; run: RunC
|
|
|
18
19
|
const [busy, setBusy] = useState(false);
|
|
19
20
|
const [attachments, setAttachments] = useState<string[]>([]);
|
|
20
21
|
const [uploading, setUploading] = useState(false);
|
|
22
|
+
const [needsSpecialistReview, setNeedsSpecialistReview] = useState(false);
|
|
23
|
+
const [reviewerIds, setReviewerIds] = useState<string[]>([]);
|
|
21
24
|
/**
|
|
22
25
|
* 读取 FormData 并构造创建输入:金额保留字符串,数量转数值,可选空文本转 undefined。
|
|
23
26
|
* 等待 run 返回后解除提交态,只有成功才关闭;依赖 run 返回结果,不额外捕获任意抛出异常。
|
|
@@ -35,6 +38,7 @@ export function PurchaseOrderForm({ close, run }: { close: () => void; run: RunC
|
|
|
35
38
|
reason: String(data.get('reason')) || undefined,
|
|
36
39
|
items: [{ sku: String(data.get('sku')), qty: Number(data.get('qty')) }],
|
|
37
40
|
attachments,
|
|
41
|
+
needsSpecialistReview, reviewerIds: needsSpecialistReview ? reviewerIds : [],
|
|
38
42
|
}),
|
|
39
43
|
'采购单已保存为草稿',
|
|
40
44
|
);
|
|
@@ -71,8 +75,11 @@ export function PurchaseOrderForm({ close, run }: { close: () => void; run: RunC
|
|
|
71
75
|
</label>
|
|
72
76
|
<p className="muted">保存后可在列表提交审批,创建人和部门会自动填写。</p>
|
|
73
77
|
<label>采购用途与必要性<textarea name="reason" rows={3} maxLength={2000} placeholder="说明具体用途;提交后先进行 AI 材料预审" /></label>
|
|
78
|
+
<label className="review-checkbox"><input type="checkbox" checked={needsSpecialistReview} onChange={event => setNeedsSpecialistReview(event.target.checked)} />需要专业会签(技术、质量等)</label>
|
|
79
|
+
{needsSpecialistReview && <ApprovalMemberPicker value={reviewerIds} onChange={setReviewerIds} exclude={currentUser() ? [currentUser()!.id] : []} disabled={busy} />}
|
|
80
|
+
<p className="muted">{needsSpecialistReview ? '所选专业人员需要全部签署。' : '本次不要求专业会签。'}提交后可在审批记录查看实际环节和办理进度。</p>
|
|
74
81
|
<AttachmentUpload command="PurchaseOrder.create" field="attachments" policy={PurchaseOrderFileFields.attachments} value={attachments} onChange={setAttachments} onBusyChange={setUploading} />
|
|
75
|
-
<button className="primary" disabled={busy || uploading}>
|
|
82
|
+
<button className="primary" disabled={busy || uploading || (needsSpecialistReview && reviewerIds.length === 0)}>
|
|
76
83
|
{busy ? '保存中…' : '保存草稿'}
|
|
77
84
|
</button>
|
|
78
85
|
</form>
|
|
@@ -13,3 +13,5 @@
|
|
|
13
13
|
.review-step { margin: 14px 0; border-left: 2px solid #bbd0bd; padding: 4px 0 4px 18px; font-size: 13px; line-height: 1.8; }
|
|
14
14
|
.review-step p { margin: 6px 0; }
|
|
15
15
|
.review-suggestion { color: #956128; }
|
|
16
|
+
.review-actions { margin-top: 24px; border-top: 1px solid var(--line, #e4e8df); padding-top: 12px; }
|
|
17
|
+
.review-actions small { display: block; }
|
|
@@ -14,6 +14,7 @@ import { currency, statusLabels } from '../lib/presentation';
|
|
|
14
14
|
import { OrderAttachments } from '../components/order-attachments';
|
|
15
15
|
import { PurchaseAssistantContext } from '../components/purchase-assistant-context';
|
|
16
16
|
import { ApprovalHistory } from '../components/approval-history';
|
|
17
|
+
import { OrderReviewers } from '../components/order-reviewers';
|
|
17
18
|
import './purchase-intelligence.css';
|
|
18
19
|
|
|
19
20
|
const list = '/procurement/orders';
|
|
@@ -122,6 +123,7 @@ function OrderDetail({ id }: { id: string }) {
|
|
|
122
123
|
}}>保存用途</button></div>
|
|
123
124
|
</> : <p>{order.reason || '尚未填写用途'}</p>}
|
|
124
125
|
</div>
|
|
126
|
+
<OrderReviewers order={order} />
|
|
125
127
|
<OrderAttachments order={order} />
|
|
126
128
|
<div className="section-head">
|
|
127
129
|
<h2>采购物料</h2>
|
|
@@ -170,6 +170,8 @@ export function PurchaseOrdersPage({ pendingTasks }: { pendingTasks?: number })
|
|
|
170
170
|
>
|
|
171
171
|
PO-{order.id.slice(0, 8).toUpperCase()}
|
|
172
172
|
</Link>
|
|
173
|
+
{/* 用途摘要帮助采购员辨认单据;完整材料仍在详情页按安全读权限展示。 */}
|
|
174
|
+
{order.reason && <small title={order.reason}>{order.reason.length > 48 ? `${order.reason.slice(0, 48)}…` : order.reason}</small>}
|
|
173
175
|
<small>{new Date(order.createdAt).toLocaleString('zh-CN')}</small>
|
|
174
176
|
</td>
|
|
175
177
|
<td>{order.items.map((i) => `${i.sku} × ${i.qty}`).join('、')}</td>
|
|
@@ -14,10 +14,15 @@ export function errorMessage(r: Extract<CageResult<unknown, string>, { ok: false
|
|
|
14
14
|
const labels: Record<string, string> = {
|
|
15
15
|
FORBIDDEN: '你没有执行此操作的权限',
|
|
16
16
|
INVALID_INPUT: '请检查填写内容',
|
|
17
|
-
CONFLICT: '
|
|
17
|
+
CONFLICT: '数据或任务已更新,请刷新后重试',
|
|
18
18
|
UNAUTHENTICATED: '登录已过期',
|
|
19
19
|
CONSTRAINT_VIOLATED: '未满足审批条件,请检查审批人或额度',
|
|
20
20
|
STOCK_SHORT: '库存不足',
|
|
21
|
+
REVIEWERS_REQUIRED: '请为专业会签选择 1–5 位协作人',
|
|
22
|
+
REVIEWERS_NOT_REQUIRED: '取消专业会签时请清空复核人员',
|
|
23
|
+
DUPLICATE_REVIEWERS: '专业会签人员不能重复',
|
|
24
|
+
REVIEWER_IS_APPLICANT: '申请人不能担任本单专业会签人',
|
|
25
|
+
REVIEWER_UNAVAILABLE: '所选人员已不可用,请重新核对协作名录',
|
|
21
26
|
NETWORK: '连接失败,请稍后重试',
|
|
22
27
|
};
|
|
23
28
|
return `${labels[r.domainCode ?? r.code] ?? r.domainCode ?? r.code}${r.requestId ? ` · 请求 ${r.requestId.slice(0, 8)}` : ''}`;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** 协作名录的复选行布局供采购和审批模块复用;不覆盖其他表单,窄屏单列保持可点击标签。 */
|
|
2
|
+
.approval-member-picker { border: 1px solid #dbe3d9; border-radius: 8px; padding: 14px; margin: 14px 0; min-width: 0; }
|
|
3
|
+
.approval-member-picker legend { font-size: 12px; padding: 0 6px; font-weight: 600; }
|
|
4
|
+
.approval-member-options { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 8px; margin: 12px 0; max-height: 250px; overflow-y: auto; }
|
|
5
|
+
.approval-member-options label { display: flex; align-items: center; gap: 10px; padding: 10px; border: 1px solid #e5e9e2; border-radius: 6px; cursor: pointer; }
|
|
6
|
+
.approval-member-options input[type='checkbox'] { width: 16px; height: 16px; flex: 0 0 auto; }
|
|
7
|
+
.approval-member-options small { display: block; margin-top: 3px; color: #647365; }
|
|
8
|
+
/* 开关与选人复选框保留原生尺寸,避免通用表单的 width:100% 让标签错位。 */
|
|
9
|
+
label.review-checkbox { display: flex; align-items: center; gap: 10px; }
|
|
10
|
+
label.review-checkbox input { width: auto; flex: none; }
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 采购参与者共用的审批协作名录选择器,由采购表单和审批管理弹窗消费。
|
|
3
|
+
* 仅通过生成契约读取 ApprovalMember 安全视图,不读取完整 User 或凭账号名猜测权限。
|
|
4
|
+
* 目录是公开协作身份,不保证被选者对当前单据有审批权限;实际提交由服务端复核,空/失败不能伪装成可选名单。
|
|
5
|
+
*/
|
|
6
|
+
import { useState } from 'react';
|
|
7
|
+
import { useQuery } from '@tanstack/react-query';
|
|
8
|
+
import { approvalMember } from '@app/contracts';
|
|
9
|
+
import { errorMessage } from '../lib/error-message';
|
|
10
|
+
import './approval-member-picker.css';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 在有名录读取权限的界面加载当前页,可按显示名/职责就地筛选;最多读取 200 项,不声称返回完整组织。
|
|
14
|
+
* page 进入查询缓存键,退出账号时由应用会话层清理;retired 默认不用于选人。
|
|
15
|
+
*/
|
|
16
|
+
export function useApprovalMembers(page = 1, includeRetired = false) {
|
|
17
|
+
return useQuery({ queryKey: ['approval-members', page, includeRetired], queryFn: async () => {
|
|
18
|
+
const result = await approvalMember.list({ page, pageSize: 200, ...(includeRetired ? {} : { status: 'active' }) });
|
|
19
|
+
if (!result.ok) throw new Error(errorMessage(result));
|
|
20
|
+
return result.data;
|
|
21
|
+
} });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 单选/多选均输出真实 userId,value 与 onChange 由父表单持有,exclude 只改善体验,不替代服务端重复票检查。
|
|
26
|
+
* max 限制本次选择数量,已选项始终可取消;翻页保留已选 ID,页面展示本次已选数量。
|
|
27
|
+
* disabled 用于在父级提交时冻结编辑;刷新与失败保留用户选择,不自动选择或发起写操作。
|
|
28
|
+
*/
|
|
29
|
+
export function ApprovalMemberPicker({ value, onChange, max = 5, exclude = [], disabled = false }: {
|
|
30
|
+
value: string[]; onChange: (ids: string[]) => void; max?: number; exclude?: string[]; disabled?: boolean;
|
|
31
|
+
}) {
|
|
32
|
+
const [search, setSearch] = useState(''), [page, setPage] = useState(1);
|
|
33
|
+
const query = useApprovalMembers(page);
|
|
34
|
+
const entries = query.data?.items.filter(member => !exclude.includes(member.userId) &&
|
|
35
|
+
`${member.displayName} ${member.specialty}`.includes(search.trim())) ?? [];
|
|
36
|
+
return <fieldset className="approval-member-picker" disabled={disabled}>
|
|
37
|
+
<legend>选择协作人员 · 已选 {value.length}/{max}</legend>
|
|
38
|
+
<input aria-label="筛选协作人员" placeholder="按姓名或职责筛选当前页" value={search} onChange={event => setSearch(event.target.value)} />
|
|
39
|
+
{query.isPending && <p role="status">正在读取协作名录…</p>}
|
|
40
|
+
{query.isError && <p role="alert">{query.error.message} <button type="button" onClick={() => void query.refetch()}>重试</button></p>}
|
|
41
|
+
<div className="approval-member-options">{entries.map(member => <label key={member.id}>
|
|
42
|
+
<input type="checkbox" checked={value.includes(member.userId)} disabled={disabled || (!value.includes(member.userId) && value.length >= max && max !== 1)}
|
|
43
|
+
onChange={event => onChange(event.target.checked ? max === 1 ? [member.userId] : [...value, member.userId] : value.filter(id => id !== member.userId))} />
|
|
44
|
+
<span><strong>{member.displayName}</strong><small>{member.specialty}</small></span>
|
|
45
|
+
</label>)}</div>
|
|
46
|
+
{query.data && entries.length === 0 && <p className="muted">当前页没有匹配人员,请调整筛选或联系名录维护人。</p>}
|
|
47
|
+
{(query.data?.total ?? 0) > 200 && <div className="actions"><button type="button" disabled={page === 1} onClick={() => setPage(page - 1)}>上一页</button>
|
|
48
|
+
<span>第 {page} 页</span><button type="button" disabled={page * 200 >= query.data!.total} onClick={() => setPage(page + 1)}>下一页</button></div>}
|
|
49
|
+
<small className="muted">名录只提供选人信息,处理权限、数据范围和最终额度仍以服务端校验为准。</small>
|
|
50
|
+
</fieldset>;
|
|
51
|
+
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"module:purchase": "采购管理",
|
|
10
10
|
"module:inventory": "库存管理",
|
|
11
11
|
"aggregate:PurchaseOrder": "采购单",
|
|
12
|
+
"aggregate:ApprovalMember": "采购审批协作名录",
|
|
12
13
|
"aggregate:Inventory": "库存台账",
|
|
13
14
|
"aggregate:InventoryLock": "库存锁",
|
|
14
15
|
"aggregate:Dept": "部门",
|
|
@@ -16,8 +17,21 @@
|
|
|
16
17
|
"aggregate:Role": "角色",
|
|
17
18
|
"command:PurchaseOrder.create": "新建采购单",
|
|
18
19
|
"command:PurchaseOrder.explainPurpose": "补充采购用途",
|
|
20
|
+
"command:PurchaseOrder.setReviewers": "设置专业复核人员",
|
|
19
21
|
"command:PurchaseOrder.returnForChanges": "退回修改",
|
|
20
22
|
"field:PurchaseOrder.reason": "采购用途",
|
|
23
|
+
"field:PurchaseOrder.needsSpecialistReview": "需要专业复核",
|
|
24
|
+
"field:PurchaseOrder.reviewerIds": "专业复核人员",
|
|
25
|
+
"command:ApprovalMember.create": "登记采购协作人",
|
|
26
|
+
"command:ApprovalMember.update": "修改协作人公开资料",
|
|
27
|
+
"command:ApprovalMember.retire": "退役采购协作人",
|
|
28
|
+
"field:ApprovalMember.userId": "协作账号",
|
|
29
|
+
"field:ApprovalMember.displayName": "公开业务姓名",
|
|
30
|
+
"field:ApprovalMember.specialty": "专业领域",
|
|
31
|
+
"state:ApprovalMember.active": "可选协作人",
|
|
32
|
+
"state:ApprovalMember.retired": "已退役协作人",
|
|
33
|
+
"capability:ApprovalMember.read": "查看采购协作名录",
|
|
34
|
+
"capability:ApprovalMember.export": "导出采购协作名录",
|
|
21
35
|
"capability:Sys.ai.use": "使用智能助理",
|
|
22
36
|
"capability:Sys.ai.models.manage": "管理 AI 模型",
|
|
23
37
|
"command:PurchaseOrder.setAttachments": "修改采购附件",
|