From 5d0a5f9a386574d91396dc1f41f6ed58b50c175e Mon Sep 17 00:00:00 2001 From: shunli Date: Sat, 18 Jul 2026 15:46:14 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=BA=94=E4=BA=BA=E5=9B=A2?= =?UTF-8?q?=E9=98=9F=E6=AD=A3=E5=BC=8F=E5=86=85=E7=94=A8=E6=9C=80=E5=B0=8F?= =?UTF-8?q?=E6=94=B6=E5=8F=A3=EF=BC=8807-18-small-team-internal-readiness?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .trellis/spec/hyb-oa/approval-workflow.md | 77 ++ .../spec/hyb-oa/backend-fastify-prisma.md | 79 ++ .trellis/spec/hyb-oa/deployment-trialrun.md | 74 ++ .../spec/hyb-oa/frontend-react-vite-design.md | 63 + .../check.jsonl | 8 + .../design.md | 99 ++ .../evidence.md | 93 ++ .../implement.jsonl | 8 + .../prd.md | 105 ++ .../task.json | 26 + .../check.jsonl | 0 .../design.md | 0 .../implement.jsonl | 0 .../implement.md | 0 .../05-31-unified-page-header-actions/prd.md | 10 +- .../task.json | 6 +- .../research/findings.md | 34 +- client/src/api/approvalRules.ts | 3 +- client/src/api/employees.ts | 9 +- client/src/api/errorMessages.ts | 26 +- client/src/api/paymentBatches.ts | 46 +- client/src/api/purchaseRequests.ts | 3 +- client/src/api/reimbursements.ts | 3 +- client/src/auth/permissions.ts | 22 + .../common/AttachmentDeleteConfirmDialog.tsx | 99 ++ .../components/common/UnsavedChangesGuard.tsx | 159 +++ client/src/components/navigation/NavLinks.tsx | 16 +- .../src/components/patterns/ListWorkspace.tsx | 12 +- client/src/index.css | 3 +- client/src/lib/notificationLinks.ts | 2 +- client/src/lib/runtimeContext.ts | 5 +- client/src/main.tsx | 11 +- client/src/pages/ApOperationsPage.tsx | 131 +- client/src/pages/ApprovalRulesPage.tsx | 1 - client/src/pages/AssetRegistryPage.tsx | 43 +- client/src/pages/ContractsRegistryPage.tsx | 129 +- client/src/pages/EmployeeProfilePage.tsx | 41 +- client/src/pages/InvoicePoolPage.tsx | 32 +- client/src/pages/MobilePortalPage.tsx | 2 +- client/src/pages/MobileProfilePage.tsx | 1 + .../pages/MobileReimbursementDetailPage.tsx | 22 +- client/src/pages/PaymentBatchesPage.tsx | 277 +++-- client/src/pages/PeoplePage.tsx | 4 +- client/src/pages/ProfilePage.tsx | 1 + client/src/pages/PurchaseRequestsPage.tsx | 139 ++- client/src/pages/ReimbursementDetailPage.tsx | 50 +- client/src/pages/SystemSettingsPage.tsx | 4 +- client/src/pages/UserManagementPage.tsx | 142 ++- client/src/pages/WecomApprovalArchivePage.tsx | 48 +- .../user-management/CreateUserDialog.tsx | 11 +- .../pages/user-management/UserRoleDialog.tsx | 24 +- client/src/router/index.tsx | 45 +- client/tests/e2e/assets-flow.spec.ts | 3 +- .../tests/e2e/internal-readiness-ui.spec.ts | 405 +++++++ .../e2e/local-candidate-witnesses.spec.ts | 538 +++++++++ client/tests/e2e/main-flow.spec.ts | 14 +- .../e2e/manual/asset1d-public-trial.spec.ts | 3 +- .../r7e-browser-business-trialrun.spec.ts | 11 +- .../e2e/payment-reviewer-permissions.spec.ts | 430 +++++++ .../e2e/pc-detail-mobile-visual-wave3.spec.ts | 83 +- .../e2e/pc-finance-admin-visual-wave4.spec.ts | 44 +- .../e2e/permission-direct-access.spec.ts | 2 +- .../e2e/r102-reimbursement-main-flow.spec.ts | 17 + .../r103-cross-page-ui-consistency.spec.ts | 382 +++++- .../reimbursement-payment-batch-flow.spec.ts | 10 +- .../e2e/system-action-confirmation.spec.ts | 183 ++- client/tests/e2e/user-onboarding.spec.ts | 133 +- docs/文档事实源状态矩阵.md | 2 +- docs/试运行与自动化验证手册.md | 25 +- .../assets-1024x768.png | Bin 0 -> 80529 bytes .../assets-768x1024.png | Bin 0 -> 60826 bytes .../contracts-registry-1024x768.png | Bin 0 -> 108261 bytes .../contracts-registry-768x1024.png | Bin 0 -> 92261 bytes .../desktop-archive-1024x768.png | Bin 0 -> 151224 bytes .../mobile-archive-detail-375x812.png | Bin 0 -> 76977 bytes .../mobile-archive-empty-375x812.png | Bin 0 -> 71363 bytes .../mobile-attendance-375x812.png | Bin 0 -> 59995 bytes .../payment-batches-1024x768.png | Bin 0 -> 79733 bytes .../payment-batches-768x1024.png | Bin 0 -> 66999 bytes .../projects-1024x768.png | Bin 0 -> 78670 bytes .../projects-768x1024.png | Bin 0 -> 57984 bytes .../reimbursements-1024x768.png | Bin 0 -> 78177 bytes .../reimbursements-768x1024.png | Bin 0 -> 57855 bytes scripts/check-minimal.sh | 33 +- scripts/qa/check-flow-coverage.mjs | 637 ++++++---- scripts/qa/route-manifest.json | 189 +++ scripts/qa/run-local-candidate.sh | 165 +++ scripts/qa/run-web-gates.sh | 73 +- scripts/qa/tests/check-flow-coverage.test.mjs | 310 +++++ .../qa/tests/check-minimal-profile.test.mjs | 106 ++ .../qa/tests/validate-loopback-url.test.mjs | 40 + .../verify-local-candidate-results.test.mjs | 189 +++ scripts/qa/validate-loopback-url.mjs | 44 + scripts/qa/verify-local-candidate-results.mjs | 199 +++ .../migration.sql | 2 + .../migration.sql | 2 + server/prisma/schema.prisma | 2 + server/src/constants/permissions.js | 22 + server/src/routes/apOperations.js | 2 +- server/src/routes/approvalRules.js | 11 +- server/src/routes/employees.js | 543 +++++++-- server/src/routes/exports.js | 2 +- server/src/routes/paymentBatches.js | 1070 ++++++++++++----- server/src/routes/payroll.js | 2 +- server/src/routes/purchaseRequests.js | 16 + server/src/routes/reimbursements.js | 61 + server/src/routes/users.js | 433 ++++--- server/src/routes/workflow.js | 13 +- .../check-payment-postgres-concurrency.js | 769 ++++++++++++ .../src/services/employeeDirectoryFilters.js | 11 +- server/src/services/storage/index.js | 32 +- server/src/services/workspaceSettings.js | 34 +- server/src/utils/workflow.js | 45 +- .../tests/unit/constants/permissions.test.js | 34 + .../routes/approvalRules.permissions.test.js | 145 +++ .../unit/routes/employees.attachments.test.js | 293 ++++- .../tests/unit/routes/employees.stats.test.js | 55 +- .../paymentBatches.reimbursement.test.js | 898 +++++++++++++- .../tests/unit/routes/payroll.exports.test.js | 2 +- .../tests/unit/routes/payroll.items.test.js | 18 +- .../unit/routes/payroll.payslips.test.js | 2 +- .../routes/payroll.salary-profile.test.js | 2 +- .../unit/routes/purchase.attachments.test.js | 61 +- .../routes/reimbursements.attachments.test.js | 33 +- .../reimbursements.payment-batch-gate.test.js | 186 +++ server/tests/unit/routes/users.import.test.js | 91 +- server/tests/unit/routes/users.list.test.js | 77 +- .../users.sensitive-permissions.test.js | 171 +++ server/tests/unit/routes/users.write.test.js | 250 +++- .../unit/routes/workflow.permissions.test.js | 11 + .../unit/routes/workflow.transfer.test.js | 59 +- .../payment-postgres-concurrency.test.js | 167 +++ server/tests/unit/utils/workflow.test.js | 37 + .../unit/workspaceSettings.navigation.test.js | 21 + 134 files changed, 10790 insertions(+), 1413 deletions(-) create mode 100644 .trellis/tasks/07-18-small-team-internal-readiness/check.jsonl create mode 100644 .trellis/tasks/07-18-small-team-internal-readiness/design.md create mode 100644 .trellis/tasks/07-18-small-team-internal-readiness/evidence.md create mode 100644 .trellis/tasks/07-18-small-team-internal-readiness/implement.jsonl create mode 100644 .trellis/tasks/07-18-small-team-internal-readiness/prd.md create mode 100644 .trellis/tasks/07-18-small-team-internal-readiness/task.json rename .trellis/tasks/{ => archive/2026-07}/05-31-unified-page-header-actions/check.jsonl (100%) rename .trellis/tasks/{ => archive/2026-07}/05-31-unified-page-header-actions/design.md (100%) rename .trellis/tasks/{ => archive/2026-07}/05-31-unified-page-header-actions/implement.jsonl (100%) rename .trellis/tasks/{ => archive/2026-07}/05-31-unified-page-header-actions/implement.md (100%) rename .trellis/tasks/{ => archive/2026-07}/05-31-unified-page-header-actions/prd.md (88%) rename .trellis/tasks/{ => archive/2026-07}/05-31-unified-page-header-actions/task.json (58%) create mode 100644 client/src/components/common/AttachmentDeleteConfirmDialog.tsx create mode 100644 client/src/components/common/UnsavedChangesGuard.tsx create mode 100644 client/tests/e2e/internal-readiness-ui.spec.ts create mode 100644 client/tests/e2e/local-candidate-witnesses.spec.ts create mode 100644 client/tests/e2e/payment-reviewer-permissions.spec.ts create mode 100644 output/playwright/07-18-small-team-internal-readiness/assets-1024x768.png create mode 100644 output/playwright/07-18-small-team-internal-readiness/assets-768x1024.png create mode 100644 output/playwright/07-18-small-team-internal-readiness/contracts-registry-1024x768.png create mode 100644 output/playwright/07-18-small-team-internal-readiness/contracts-registry-768x1024.png create mode 100644 output/playwright/07-18-small-team-internal-readiness/desktop-archive-1024x768.png create mode 100644 output/playwright/07-18-small-team-internal-readiness/mobile-archive-detail-375x812.png create mode 100644 output/playwright/07-18-small-team-internal-readiness/mobile-archive-empty-375x812.png create mode 100644 output/playwright/07-18-small-team-internal-readiness/mobile-attendance-375x812.png create mode 100644 output/playwright/07-18-small-team-internal-readiness/payment-batches-1024x768.png create mode 100644 output/playwright/07-18-small-team-internal-readiness/payment-batches-768x1024.png create mode 100644 output/playwright/07-18-small-team-internal-readiness/projects-1024x768.png create mode 100644 output/playwright/07-18-small-team-internal-readiness/projects-768x1024.png create mode 100644 output/playwright/07-18-small-team-internal-readiness/reimbursements-1024x768.png create mode 100644 output/playwright/07-18-small-team-internal-readiness/reimbursements-768x1024.png mode change 100755 => 100644 scripts/qa/check-flow-coverage.mjs create mode 100644 scripts/qa/route-manifest.json create mode 100644 scripts/qa/run-local-candidate.sh create mode 100644 scripts/qa/tests/check-flow-coverage.test.mjs create mode 100644 scripts/qa/tests/check-minimal-profile.test.mjs create mode 100644 scripts/qa/tests/validate-loopback-url.test.mjs create mode 100644 scripts/qa/tests/verify-local-candidate-results.test.mjs create mode 100644 scripts/qa/validate-loopback-url.mjs create mode 100644 scripts/qa/verify-local-candidate-results.mjs create mode 100644 server/prisma/migrations/20260718120000_add_system_admin_role/migration.sql create mode 100644 server/prisma/migrations/20260718123000_small_team_payment_cancel_reason/migration.sql create mode 100644 server/src/scripts/check-payment-postgres-concurrency.js create mode 100644 server/tests/unit/routes/approvalRules.permissions.test.js create mode 100644 server/tests/unit/routes/reimbursements.payment-batch-gate.test.js create mode 100644 server/tests/unit/routes/users.sensitive-permissions.test.js create mode 100644 server/tests/unit/scripts/payment-postgres-concurrency.test.js diff --git a/.trellis/spec/hyb-oa/approval-workflow.md b/.trellis/spec/hyb-oa/approval-workflow.md index 2d02f20d..9061820f 100644 --- a/.trellis/spec/hyb-oa/approval-workflow.md +++ b/.trellis/spec/hyb-oa/approval-workflow.md @@ -16,3 +16,80 @@ task before modifying approval behavior. not drift because rules changed later. - Reviewer review is required after changes touching workflow, status machine, approval rules, permissions or migrations. + +## Scenario: System Operator Is Not A Business Approver + +### 1. Scope / Trigger + +- Trigger: adding or changing OA account administration, role assignment, + approval eligibility, payment review or emergency-superuser behavior. + +### 2. Signatures + +- Daily responsibility roles: `manager`, `ceo`, `finance`, `system_admin`. +- Emergency role: `admin` (`*`), hidden from ordinary assignment UI. +- System permissions: `system.accounts.manage`, `system.roles.manage`. +- Payment reviewer permission: `payment.batch.review`. + +### 3. Contracts + +- `system_admin` manages ordinary accounts and system configuration only; it + does not imply leadership, finance, payroll, personnel-sensitive access or + business approval authority. +- A payment reviewer must have both `payment.batch.review` and an explicit + `manager` or `ceo` role. `admin` or `system_admin` alone is not a normal + reviewer. +- `manager` does not imply that every employee reports to that account; + reporting scope remains in `direct_manager_id`. +- A non-emergency system administrator cannot edit their own roles or grant, + revoke or import `admin` / `system_admin`. Role changes remain audited. +- Employee aggregate endpoints must not treat `system.settings` as personnel- + sensitive authority. Salary summaries require `payroll.manage`; performance + summaries require `system.users_sensitive`; unauthorized branches issue no + salary/performance query and return only the non-sensitive summary shape. + +### 4. Validation & Error Matrix + +- System administrator changes own roles -> `403 PERM_001` before role writes. +- System administrator changes a protected role -> `403 PERM_001`, zero role + and audit writes. +- Reviewer has permission but lacks `manager|ceo` -> `403 PERM_001`. +- Batch creator attempts review -> deterministic `409 VALID_001`. +- `system_admin` opens ordinary employee statistics -> salary is null, + performance is empty and both sensitive query families remain untouched. + +### 5. Good/Base/Bad Cases + +- Good: one person may explicitly hold both `manager` and `system_admin`, while + each route evaluates the responsibility required for that action. +- Base: `system_admin` creates or disables an ordinary account without seeing + payroll, bank, identity or employee-contract content. +- Bad: treat `admin` as the default company leader or use `system_admin` as an + approval fallback because the team is small. + +### 6. Tests Required + +- Role matrix for employee, manager, CEO, finance, system administrator and + emergency administrator. +- Negative tests for self-role change, protected-role assignment/import and + permission-only payment review. +- Negative employee-statistics test using the real `system_admin` permission + set, plus positive tests for explicit payroll/personnel-sensitive authority. +- Reviewer read-only review after changing role or approval semantics. + +### 7. Wrong vs Correct + +#### Wrong + +```js +if (hasPermission(actor, "*")) allowPaymentReview(); +``` + +#### Correct + +```js +if (!hasPermission(actor, "payment.batch.review") + || !actor.roleCodes.some((role) => role === "manager" || role === "ceo")) { + throw forbidden(); +} +``` diff --git a/.trellis/spec/hyb-oa/backend-fastify-prisma.md b/.trellis/spec/hyb-oa/backend-fastify-prisma.md index 85301744..b986e0ba 100644 --- a/.trellis/spec/hyb-oa/backend-fastify-prisma.md +++ b/.trellis/spec/hyb-oa/backend-fastify-prisma.md @@ -383,3 +383,82 @@ const currentSession = await tx.authSession.findUnique({ where: { id: sessionId if (!currentSession || currentSession.revoked_at) return invalidRefresh(); await rotateSession(tx, currentSession); ``` + +## Scenario: Small-Team Payment Batch Consistency + +### 1. Scope / Trigger + +- Trigger: changing payment-batch creation, review, execution, retry, + cancellation, receipt evidence or the legacy direct-payment path. + +### 2. Signatures + +- Candidate API: `GET /api/payment-batches/candidates?q=&page=&pageSize=`. +- State APIs: `POST /api/payment-batches/:id/review`, + `POST /api/payment-batches/:id/execute`, + `POST /api/payment-batches/:id/cancel` with `{ reason }`. +- Receipt invalidation accepts `{ reason }` and retains the record. +- DB: `payment_batches.cancelled_reason TEXT NULL`. +- Isolated concurrency gate: + `bash scripts/qa/run-web-gates.sh payment-concurrency-local`. + +### 3. Contracts + +- Normal flow is finance maker/executor plus one independent `manager|ceo` + reviewer; the maker cannot review their own batch. +- Lock the batch before state checks, then lock source entities in stable order + and recheck status, amount and reservation inside the same transaction. +- When `payment_batch_v1` is enabled, an approved reimbursement can enter only + one active batch and the legacy direct-payment route must not form a second + write path. +- Retry processes only `pending|failed` items. Existing `executed` items never + repeat payment writes, follow-ups or notifications. Persisted execution + summary is recomputed from every final item state; the audit separately + records the current attempt. +- Cancellation requires a 2-500 character reason. Receipt evidence is voided + with audit history, not physically deleted. + +### 4. Validation & Error Matrix + +- Missing review, maker-reviewer conflict or terminal state -> `409 VALID_001`. +- Concurrent loser after a valid winner -> deterministic `409`, zero duplicate + state, audit or notification writes. +- Missing/invalid cancellation or void reason -> `400 VALID_001`. +- Source already paid, amount changed or reserved by another active batch -> + `409 VALID_001` and no partial batch mutation. + +### 5. Good/Base/Bad Cases + +- Good: one executor wins, a concurrent cancel or second executor receives + `409`, and the final payment/audit/notification counts remain one. +- Base: a failed batch retries only failed items and reaches `executed` with an + all-item final summary. +- Bad: count only the retry attempt in `execution_summary_json`, or allow the + direct route to pay while the same source is in a valid batch. + +### 6. Tests Required + +- Focused route tests for candidate authority, maker-checker, retry summary, + cancellation, receipt void and zero-side-effect refusals. +- Real disposable PostgreSQL races for double execute, execute/cancel, double + create and direct-payment/batch competition; assert unique audits and + notifications and zero residual test databases. +- Prisma validate, clean empty-database migration application and a read-only + reviewer after payment-state changes. + +### 7. Wrong vs Correct + +#### Wrong + +```js +summary.successCount = successesInThisRetry; +batch.status = failuresInThisRetry ? "failed" : "executed"; +``` + +#### Correct + +```js +const finalSummary = summarizeFinalItemStatuses(finalItemStatuses); +batch.execution_summary_json = finalSummary; +batch.status = finalSummary.failedCount > 0 ? "failed" : "executed"; +``` diff --git a/.trellis/spec/hyb-oa/deployment-trialrun.md b/.trellis/spec/hyb-oa/deployment-trialrun.md index f786aac3..8afb0f85 100644 --- a/.trellis/spec/hyb-oa/deployment-trialrun.md +++ b/.trellis/spec/hyb-oa/deployment-trialrun.md @@ -442,3 +442,77 @@ validate_isolated_target_before_writes run_mutating_auth_regression drop_owned_database_or_fail ``` + +## Scenario: Repository Local Candidate Gate + +### 1. Scope / Trigger + +- Trigger: declaring a checkout ready for internal deployment planning without + deploying it or using public, production or real-account evidence. + +### 2. Signatures + +- Full gate: `bash scripts/qa/run-local-candidate.sh full`. +- Browser-only rerun: `bash scripts/qa/run-local-candidate.sh focused`. +- Parse-only check: `bash scripts/qa/run-local-candidate.sh list`. +- Route contract: `scripts/qa/route-manifest.json`. +- Result verifier: `scripts/qa/verify-local-candidate-results.mjs`. + +### 3. Contracts + +- `full` alone may print `LOCAL_CANDIDATE_PASS`; `focused` prints + `FOCUSED_BROWSER_PASS`, and `list` prints `LIST_ONLY`. +- The gate accepts only loopback HTTP origins and canonical TCP ports. It uses + local synthetic fixtures, excludes `manual/live` specs and does not consume + historical Issue CSV as a candidate prerequisite. +- Every registered route is classified. Every core bundle has one stable, + source-bound witness; candidate spec basenames are unique. +- Every selected Playwright result must pass. Missing, duplicate, unknown, + skipped, multi-tag or wrong-source witnesses and any failed/skipped + acceptance test make the gate fail. +- The script's full pass proves only its local scope. A task changing payment + concurrency also requires the isolated PostgreSQL payment gate; deployment, + public checks and real-device behavior remain separate states. + +### 4. Validation & Error Matrix + +- Non-loopback URL, credentialed URL, path/query target or invalid port -> fail + before Vite/Playwright use. +- Unclassified/orphan route or missing core behavior/visual witness -> non-zero + scanner result. +- Playwright suite error, unexpected, failed or skipped test -> non-zero result + gate with basename-only redacted diagnostics. +- `focused` succeeds -> useful rerun evidence, but not a full candidate claim. + +### 5. Good/Base/Bad Cases + +- Good: full base checks pass, all source-bound witnesses and acceptance specs + execute, and the runner prints one full-pass label. +- Base: use `list` before a long run to prove assets parse, while recording that + no behavior has run. +- Bad: reuse a prior browser pass after parser changes, or report focused/list + output as `LOCAL_CANDIDATE_PASS`. + +### 6. Tests Required + +- Negative tests for route gaps, manual/live evidence, duplicate basenames, + invalid loopback inputs, witness result/source drift and mode labels. +- Current-checkout `full` run with zero skipped/unexpected results. +- For payment-state changes, run the disposable PostgreSQL gate and prove + cleanup left no matching temporary databases. + +### 7. Wrong vs Correct + +#### Wrong + +```bash +bash scripts/qa/run-local-candidate.sh list +echo LOCAL_CANDIDATE_PASS +``` + +#### Correct + +```bash +bash scripts/qa/run-local-candidate.sh full +# Only this successful branch emits LOCAL_CANDIDATE_PASS. +``` diff --git a/.trellis/spec/hyb-oa/frontend-react-vite-design.md b/.trellis/spec/hyb-oa/frontend-react-vite-design.md index cbcc04e0..46cdc6f6 100644 --- a/.trellis/spec/hyb-oa/frontend-react-vite-design.md +++ b/.trellis/spec/hyb-oa/frontend-react-vite-design.md @@ -938,6 +938,69 @@ validation prevent repeated re-entry and reduce support/debugging cost. - Focused Playwright coverage for draft save, submit validation, unsaved-change protection and server-validation failure paths when changing form behavior. +## Scenario: Data-Router Unsaved-Change Protection + +### 1. Scope / Trigger + +- Trigger: a React form can lose unsaved work through in-app navigation, + browser back/forward, reload, closing its panel or switching records. + +### 2. Signatures + +- Router: `createBrowserRouter(...)` rendered through `RouterProvider`. +- Navigation blocker: React Router `useBlocker(isDirty)`. +- Reload/close guard: `beforeunload` while `isDirty === true`. +- Shared UI contract: one unsaved-change dialog with continue-editing and + discard actions. + +### 3. Contracts + +- Block navigation before the route or selected record changes; do not restore + old UI state after navigation has already occurred. +- `blocker.reset()` keeps the current route and form values. `blocker.proceed()` + is called only after explicit discard confirmation. +- Record switching and panel closing use the same pending-action contract as + route navigation. Successful save or explicit discard clears dirty state. +- Register `beforeunload` only while dirty and remove it during cleanup. + +### 4. Validation & Error Matrix + +- Clean form navigation -> continue without a dialog. +- Dirty in-app navigation or browser back -> block and show one dialog. +- Continue editing -> URL, record selection and values remain unchanged. +- Discard -> execute exactly one pending navigation/selection and clear dirty. +- Reload/close while dirty -> browser-level unload warning remains active. + +### 5. Good/Base/Bad Cases + +- Good: reimbursement, procurement, contract and asset forms reuse one guard + while keeping their own save and dirty-field logic. +- Base: a short non-editable filter form does not register the guard. +- Bad: listen to history after navigation, then push the previous URL back; the + user can see state flicker or lose form data before confirmation. + +### 6. Tests Required + +- Playwright covers link navigation, browser back, reload and record switch. +- Assert cancel preserves URL and values; discard changes target once; save + removes the warning. +- Frontend typecheck and build after router or shared guard changes. + +### 7. Wrong vs Correct + +#### Wrong + +```ts +window.addEventListener("popstate", () => navigate(previousUrl)); +``` + +#### Correct + +```ts +const blocker = useBlocker(isDirty); +// reset to keep editing; proceed only after explicit discard. +``` + ## Convention: Detail Evidence Layout **What**: Detail pages use one evidence-first structure so users can understand diff --git a/.trellis/tasks/07-18-small-team-internal-readiness/check.jsonl b/.trellis/tasks/07-18-small-team-internal-readiness/check.jsonl new file mode 100644 index 00000000..d1b15bf8 --- /dev/null +++ b/.trellis/tasks/07-18-small-team-internal-readiness/check.jsonl @@ -0,0 +1,8 @@ +{"file":".trellis/spec/hyb-oa/project-governance.md","reason":"验证任务合同、复审、提交和归档边界"} +{"file":".trellis/spec/hyb-oa/backend-fastify-prisma.md","reason":"验证权限、事务、迁移与服务端回归"} +{"file":".trellis/spec/hyb-oa/frontend-react-vite-design.md","reason":"验证页面一致性、响应式和浏览器证据"} +{"file":".trellis/spec/hyb-oa/approval-workflow.md","reason":"验证审批角色、自审和 workflow 约束"} +{"file":".trellis/spec/hyb-oa/docs-and-evidence.md","reason":"验证证据、finding 生命周期和文档事实源"} +{"file":".trellis/spec/hyb-oa/security-privacy.md","reason":"验证敏感字段、日志、附件与安全摘要"} +{"file":".trellis/spec/hyb-oa/deployment-trialrun.md","reason":"验证本地候选结论没有越界为已部署"} +{"file":".trellis/tasks/archive/2026-07/07-16-system-multidimensional-audit/research/findings.md","reason":"逐项核对已承接 finding 的状态和证据"} diff --git a/.trellis/tasks/07-18-small-team-internal-readiness/design.md b/.trellis/tasks/07-18-small-team-internal-readiness/design.md new file mode 100644 index 00000000..0f6b8c3e --- /dev/null +++ b/.trellis/tasks/07-18-small-team-internal-readiness/design.md @@ -0,0 +1,99 @@ +# 五人团队正式内用最小收口设计 + +## 实施顺序 + +1. **角色与敏感资料**:先完成 enum/权限常量、账号与角色边界、员工 DTO、工资/银行/证件/合同授权及矩阵测试。 +2. **付款一致性**:在新角色合同稳定后完成候选查询、行锁状态机、取消原因、回单作废、前端选择器及 PostgreSQL 并发验证。 +3. **页面与门禁**:完成共享表格、dirty guard、移动路由、权限显隐、路由 manifest 和 `local-candidate` runner,最后回填旧标题任务。 + +每阶段先跑聚焦测试再扩展到全量。权限、状态机、workflow 和迁移在实现后由只读 reviewer 复核,不让 reviewer 修改代码。 + +## 角色与权限设计 + +### 角色模型 + +- `employee` 是所有日常账号基础角色。 +- `manager`、`ceo`、`finance`、`system_admin` 是可兼任职责。 +- `finance_mgr`、`hr`、`administration` 保留兼容,不自动迁移。 +- `admin` 仍映射 `*`,仅现有超级管理员/初始化流程可用;普通创建和普通角色编辑不提供该选项。 + +### system_admin 授权 + +允许:账号创建/启停/凭据重置、为其他用户分配普通业务角色、系统设置、集成、通知渠道、脱敏审计查看。 + +拒绝:修改自身角色、授予或撤销 `admin/system_admin`、人员完整档案、工资、银行/证件、员工合同、业务审批、付款制单/执行/复核。 + +普通角色修改继续使用既有 endpoint,但服务端按操作者角色和目标用户实施额外约束;UI 限制只做体验,不作为安全边界。角色变更审计只记录操作者、目标用户 ID、前后角色代码和结果,不记录凭据或员工敏感字段。 + +### 敏感资料数据流 + +```text +角色/对象判断 -> 选择最小 Prisma select -> DTO 字段省略 -> 前端按权限渲染 + -> 脱敏审计(成功访问/写入) +``` + +- 业务员工目录固定字段:`id / employeeNo / name / title / department / avatar / status`。 +- 完整 profile:本人或人员资料管理权限。 +- 银行资料:finance 只读;admin/明确管理权限可写。 +- 证件、员工合同:仅明确管理权限;内部路径、原始正文和不必要文件名不进入 DTO/审计。 + +## 付款设计 + +### 状态机 + +```text +draft -> reviewed -> executed + -> failed -> executed(仅失败项) + -> cancelled(仅未执行项) + -> cancelled +``` + +- `executed`、`cancelled` 终态;已执行 item 永不回滚。 +- review 需要 `payment.batch.review` 且角色代码包含 `manager` 或 `ceo`;创建者自审返回 409。 +- 50000 元阈值只影响警示/确认文案,不增加状态或第三复核人。 + +### 锁与并发 + +- review/execute/cancel/付款元数据更新/回单作废:同一事务内先 `SELECT ... FOR UPDATE` 锁批次。 +- create:稳定排序并锁定源报销,复查 `approved`、未付款且未进入非 cancelled 批次。 +- execute:稳定排序锁定开放 item 对应源单,再复查状态、金额和回单门槛。 +- 竞争失败统一返回 409 业务码;失败重试按所有 item 最终状态重算汇总。 +- `payment_batch_v1` 启用时旧直接付款端点返回兼容性 409;关闭 flag 可回滚旧代码路径。 + +### API / 数据 + +- Prisma 加法迁移:`RoleCode.system_admin`;`payment_batches.cancelled_reason TEXT NULL`。 +- `GET /api/payment-batches/candidates?q=&page=&pageSize=` 返回业务展示字段,entity ID 仅用于后续提交。 +- 取消 body:`{ reason }`;DTO 增 `cancelledReason`。 +- 回单作废:`{ reason }`;复用现有治理/归档字段,不物理删除。 +- 旧 DELETE/直接付款仅作为 feature flag 回滚路径;新版开启时不得产生第二写路径。 + +### 迁移与回滚 + +- 只提交加法 migration,不运行 migrate deploy,不修改真实数据。 +- 代码回滚时关闭 `payment_batch_v1` 并回退应用提交;enum 和 nullable 列保留,避免破坏已写数据。 +- 一旦部署后曾授予 `system_admin`,回退到不识别该枚举值的旧应用前,必须先在兼容版本中撤销或转换该角色,或保留可识别该值的兼容解析;不得让旧 Prisma client 直接读取未知枚举值。 +- 不修改迁移历史,不执行 migrate reset。 + +## 页面与门禁设计 + +- 表格修复优先调整共享容器:表格自身保留最小宽度,容器 `overflow-x:auto`,document 不横溢;五个代表页做命中和尺寸断言。 +- dirty guard 使用共享 hook/provider,统一拦截站内导航、浏览器返回/刷新和记录切换;保存成功或显式放弃后清除 dirty。 +- `/mobile/attendance` 复用现有响应式考勤页;移动 resolver、首页和通知目标统一使用该路径,避免 `/attendance -> /mobile` 自循环。 +- 移动归档只处理长字段换行/局部 overflow 和底栏安全区,不扩展移动信息架构。 +- 路由 manifest 每条注册路由必须显式分类;`core` 必须有可实际执行的行为 spec,标记 `visualRequired` 的路由还必须有视觉 spec;manual/live 文件不计本地候选证据。 +- `local-candidate` 组合基础门禁、路由/流程可信扫描和核心本地浏览器测试,不读取公网或生产环境变量。 + +## 验证矩阵 + +- **权限**:employee / manager / ceo / finance / system_admin / admin 正负矩阵;字段省略;拒绝时零 DB/file side effect;角色自提升负例。 +- **付款**:候选筛选、POST 权威拒绝、maker-checker、高额单次复核、取消原因、回单作废、审计、失败重试和权限负例。 +- **PostgreSQL 并发**:双 execute、execute vs cancel、双 create 同一报销、direct pay vs batch execute;使用隔离合成数据库并清理。 +- **浏览器**:375×812 移动考勤/归档;768×1024 与 1024×768 五类表格;四类 dirty form;危险动作 0/0/1 mutation;三处权限按钮 count=0。 +- **门禁反例**:unclassified、manual-only、core behavior missing、visualRequired missing 均非零退出。 + +## 证据与状态 + +- 截图和临时浏览器产物放 `output/playwright/07-18-small-team-internal-readiness/`,只保留合成/脱敏内容。 +- 原 finding 台账只更新当前修复状态和证据引用,不改发现时严重度;未部署项继续保持部署/外部验证待定。 +- 本地完成结论固定为 `LOCAL_CANDIDATE_PASS`。 diff --git a/.trellis/tasks/07-18-small-team-internal-readiness/evidence.md b/.trellis/tasks/07-18-small-team-internal-readiness/evidence.md new file mode 100644 index 00000000..9c0ab25a --- /dev/null +++ b/.trellis/tasks/07-18-small-team-internal-readiness/evidence.md @@ -0,0 +1,93 @@ +# 五人团队正式内用最小收口证据 + +## 证据身份与边界 + +- 发现基线:`bbb5c89`;规划基线:`58c8dec`。 +- 复核对象:本文件所在工作提交;提交前所有结果仅归属于当前受控工作树,提交后以本文件所在提交为 `reviewedCommit`。 +- 允许环境:回环 HTTP、回环 PostgreSQL、合成角色/业务数据、无网络外部适配器。 +- 未执行:部署、push、公网、生产配置、共享或真实数据库、真实账号、企业微信真机、真实 OCR、真实付款、银行流水和 ERP。 +- 状态上限:`LOCAL_CANDIDATE_PASS`;不得据此告知用户线上可测试。 + +## 最终门禁摘要 + +| 证据 | 结果 | 关键结论 | +| --- | --- | --- | +| 权限与敏感资料只读复审 | PASS,聚焦 107/107;统计权限 4/4 | `system_admin` 与经理/CEO/财务/超级管理员分离;工资/绩效汇总使用独立敏感权限,拒绝路径先于敏感查询或文件读取。 | +| 付款状态机只读复审 | PASS,聚焦 36/36;重试回归 27/27 | maker-checker、锁顺序、直付关闭、失败项重试、取消/回单审计均无 P1。 | +| 候选 workflow 只读复审 | `PASS_WITH_FOLLOWUPS`,follow-up 已修复 | witness 来源、结果 parser、loopback、模式标签与无历史 Issue CSV 依赖均有反例。 | +| migration 只读复审 | PASS,无 P1/P2 未结项 | 两项加法 migration;`system_admin` 位于 `admin` 前;可空取消原因;回滚前置已记录。 | +| 服务端全量 | PASS,161/161 文件、985/985 测试 | 包含用户导入 8/8、付款批次 27/27、员工统计权限 4/4。 | +| 前端静态门禁 | PASS | typecheck 与 Vite production build 通过;仅既有 Browserslist 提示。 | +| 默认完整基础门禁 | PASS,exit 0 | `bash scripts/check-minimal.sh` 的历史流程基线、984 项服务端测试、前端 build 与功能矩阵全部通过;本机 stub 使用回环权限。 | +| 路由与候选反例 | PASS,52/52 | 71 条路由全部分类;10/10 核心流程束、10/10 witness、7/7 acceptance、0 blocking gap。 | +| 隔离 PostgreSQL | PASS,exit 0 | 当前 61 项 migration 从空库应用;五组付款竞态 PASS;审计/通知唯一;临时库残留 0。 | +| Playwright focused | `FOCUSED_BROWSER_PASS`,44/44 | 0 failed、0 skipped、0 unexpected;10 个 witness 均来自指定 spec。 | +| Playwright/full 候选 | `LOCAL_CANDIDATE_PASS`,exit 0 | 2026-07-18 13:27 CST 完成;44/44,0 failed、0 skipped、0 unexpected;基础层与结果门禁同时通过。 | + +默认门禁第一次在禁止回环监听的沙箱内运行时,两个本机 stub 测试未能产生完整结果;允许本机-only stub 后聚焦 12/12 通过,随后原命令完整 exit 0。该诊断未访问公网,也未作为 PASS 替代原命令。 + +## PostgreSQL 竞态与迁移证据 + +命令:`bash scripts/qa/run-web-gates.sh payment-concurrency-local`。 + +- 双执行:一个成功、一个确定性 409;付款/执行审计和内部通知各唯一。 +- 执行与取消:本轮执行成功、取消 409;无重复状态或副作用。 +- 双创建:一个 201、一个 409;仅一个有效批次/项及一条创建审计。 +- 直接付款与批次付款:范围外均拒绝且零副作用;范围内直付 409、批次执行成功。 +- 缺失制单人:复核 409,批次保持 draft,零复核审计。 +- 空库 migration 后只读确认枚举相邻顺序为 `system_admin` 后接 `admin`。 +- runner 与独立检查结束后,`hyb_oa_payment_%` 临时数据库计数为 0。 + +## 十个核心流程 witness + +每项均断言最终路径、唯一 H1、合成角色、允许/禁止动作,以及 `unhandled API / page error / 非白名单 console error = 0`。 + +| 流程束 | 最终路径 | H1 | 角色 fixture | 允许动作 | 禁止动作 | +| --- | --- | --- | --- | --- | --- | +| 登录退出 | `/login` | 海音达 OA | employee | 登录 | 应急超级管理员 | +| 报销 | `/reimbursements` | 我的报销 | employee | 新建报销 | 导出 | +| 审批 | `/workflow/tasks` | 待我处理 | manager | 处理 | 通过 | +| 票据 | `/invoice-pool` | 票据 | employee | 上传票据 | 作废 | +| 采购 | `/purchase-requests` | 采购 | employee | 新建采购 | 通过 | +| 项目/合同 | `/contracts-registry` | 合同 | manager | 新增合同 | 执行付款 | +| 付款/AP | `/payment-batches` | 付款 | finance | 新建批次 | 复核批次 | +| 人员/审计 | `/system/users` | 系统用户管理 | system_admin | 新建用户 | 应急超级管理员 | +| 考勤/归档 | `/mobile/attendance` | 考勤 | employee | 修正申请 | 提交应急修正 | +| 移动工作台 | `/mobile` | 今天待办 | employee | 新建报销 | 财务 | + +## 浏览器视口与截图 + +- 移动:375×812,覆盖移动考勤、归档空态、长字段详情、换行、底栏遮挡与页面横溢。 +- PC:768×1024、1024×768,覆盖项目、资产、合同、付款、报销五类表格;局部横滚可用、右侧操作可命中、document 横溢不超过 2px。 +- 截图均使用合成身份和合成业务记录,已人工复核未见真实资料、内部 Secret、完整账号、银行资料或原始 payload。 +- 产物目录为 `output/playwright/07-18-small-team-internal-readiness/`;最终 full 复跑后的 SHA-256 如下。 + +| 截图 | SHA-256 | +| --- | --- | +| `assets-1024x768.png` | `d3d0ff0d9dd413521a068ca1c87ad6978d12d08e836d6fe3c1048559f013014b` | +| `assets-768x1024.png` | `1a5c9b07617479cfa14fa9a4525bbdf32fac6eb74635d8dbd7641e319871abbc` | +| `contracts-registry-1024x768.png` | `f318e1637f3eb65eb853f3d0436c1825efca4fcc9336b80a6cccb6ab7236b3b9` | +| `contracts-registry-768x1024.png` | `5ce2b667aef2fd24cdc918729fe998327991246417477d6301f8314406bde7dd` | +| `desktop-archive-1024x768.png` | `9d099e825e897f37383ee7d40622322946356a9dd89b8893d9531120da6750f6` | +| `mobile-archive-detail-375x812.png` | `6a1359c4bae284cd68872faa148755e2b6fc53b7be41b1739ae2aff0090a9436` | +| `mobile-archive-empty-375x812.png` | `9b6fb325287e4436de7a71f80cf5b75c3a4e5d4f183b8919af5ffa59cf1070ca` | +| `mobile-attendance-375x812.png` | `01f75c88be01b4e4ab0696bb1de39f211eaaee402377757bf7384a278c197a7d` | +| `payment-batches-1024x768.png` | `0d8f69b360a7fbfcf5a0774aa031a0c6845dac6f468a28486c72a575db0a8db1` | +| `payment-batches-768x1024.png` | `72554a7d3a6778547a69732a5a2c1cf49bfdb2e1394acb3f465782becd559ef1` | +| `projects-1024x768.png` | `cf925dede4124e1780c8a9e67e82bf596e96278131fd03c1a115e6d45dd22eba` | +| `projects-768x1024.png` | `da5dd4b0842b407d715b91e5025f8b4e6c68cb7d7b5a87c0e460630a95f40308` | +| `reimbursements-1024x768.png` | `30726bbdba0d8dc0e80d8aa269c57996d72ce0564c84c333b7cb8a97e832e096` | +| `reimbursements-768x1024.png` | `46a146af17f1f69069492b1232071de4ae55f81f73a36ed95d5672f56e887621` | + +## Finding 生命周期边界 + +- 原 finding 台账已按本证据更新本地修复状态,未改写发现时严重度、置信度或发现状态。 +- `05-31-unified-page-header-actions` 的权限显隐、资产旧 E2E、七页面标题动作及项目/资产视觉验收已闭合,并归档至 `archive/2026-07/`。 +- 本地验证通过的条目只更新 `当前修复状态=LOCAL_FIX_VERIFIED`。 +- 未部署的运行时条目继续 `DEPLOYMENT_PENDING`;企业微信/角色/公网行为继续 `EXTERNAL_BEHAVIOR_PENDING`。 +- 纯测试/门禁条目使用 `NOT_APPLICABLE` 部署状态;不需要外部行为的条目使用 `NOT_REQUIRED`。 +- 移动通知定位只闭合考勤入口增量;其他对象定位不在本任务范围,`MOB-TARGET-001` 整体保持开放。 + +## 后续授权边界 + +真实账号职责分配、超级管理员托管、内部地址部署、migration 执行、备份恢复、部署来源、公网角色、企业微信真机和新 Day 1 连续试运行均由后续获批 Trellis task 承担。 diff --git a/.trellis/tasks/07-18-small-team-internal-readiness/implement.jsonl b/.trellis/tasks/07-18-small-team-internal-readiness/implement.jsonl new file mode 100644 index 00000000..c85f2308 --- /dev/null +++ b/.trellis/tasks/07-18-small-team-internal-readiness/implement.jsonl @@ -0,0 +1,8 @@ +{"file":".trellis/spec/hyb-oa/project-governance.md","reason":"任务、审批、迁移、复审和提交边界"} +{"file":".trellis/spec/hyb-oa/backend-fastify-prisma.md","reason":"Fastify、Prisma、事务、迁移和服务端测试规则"} +{"file":".trellis/spec/hyb-oa/frontend-react-vite-design.md","reason":"React/Vite 页面、权限显隐、响应式和视觉验证规则"} +{"file":".trellis/spec/hyb-oa/approval-workflow.md","reason":"经理、公司负责人、自审和审批链合同"} +{"file":".trellis/spec/hyb-oa/reimbursement-invoice-ocr.md","reason":"报销、付款、附件和回单业务边界"} +{"file":".trellis/spec/hyb-oa/security-privacy.md","reason":"员工敏感数据、审计脱敏和附件安全边界"} +{"file":".trellis/spec/hyb-oa/deployment-trialrun.md","reason":"LOCAL_CANDIDATE 与部署/公网结论分离"} +{"file":".trellis/tasks/archive/2026-07/07-16-system-multidimensional-audit/research/findings.md","reason":"本任务承接 finding 生命周期与控制目标"} diff --git a/.trellis/tasks/07-18-small-team-internal-readiness/prd.md b/.trellis/tasks/07-18-small-team-internal-readiness/prd.md new file mode 100644 index 00000000..931b3a57 --- /dev/null +++ b/.trellis/tasks/07-18-small-team-internal-readiness/prd.md @@ -0,0 +1,105 @@ +# 五人团队正式内用最小收口 + +## Goal + +在不部署、不连接真实数据、不进行公网或企业微信真机验证的前提下,把当前代码收敛为适合约 5 人公司内部使用的本地候选版本。角色上明确分离业务管理、财务和 OA 系统运维;业务上保证付款状态、敏感资料和高影响操作可控;工程上建立可信的本地候选门禁。 + +本任务承接用户在 2026-07-17 批准的 `07-17-small-team-internal-readiness` 方案。由于任务实际创建日期为 2026-07-18,Trellis 目录按规则使用 `07-18-small-team-internal-readiness`。 + +## Scope + +- Backend:Fastify 权限、员工/账号/工资/合同和付款批次路由,Prisma 加法迁移,聚焦与并发测试。 +- Frontend:角色管理、敏感资料与付款页面、共享表格/dirty guard、移动考勤与归档、聚焦 E2E/视觉断言。 +- QA / governance:路由分类、流程覆盖扫描、`local-candidate` runner、当前 task、必要 Trellis spec/现有手册及 finding 生命周期。 +- 只使用本地合成数据和隔离测试资源;不改变生产配置或真实数据。 + +## Requirements + +### 角色、权限与敏感资料 + +- 所有账号保留员工基础能力;日常职责可显式兼任 `manager`、`ceo`、`finance` 和新增 `system_admin`。 +- `manager` 与员工 `direct_manager_id` 分开维护;经理身份不自动建立汇报关系。 +- 新增 `RoleCode.system_admin`。现有 `admin` 继续作为通配超级管理员,但只用于初始化和应急,不作为普通预设。 +- 普通用户管理界面显示员工、直属经理、公司负责人、财务、OA 系统管理员;`finance_mgr/hr/administration` 保留为高级兼容职责;普通界面不可授予 `admin`。 +- `system_admin` 可管理其他普通账号、业务角色、系统配置、集成和审计;不得修改自己的角色,不得授予或撤销 `admin/system_admin`,所有变更写脱敏审计。 +- `system_admin` 不获得工资、银行、证件、员工合同、付款执行或业务审批权限。 +- 付款复核要求显式具有 `manager` 或 `ceo`,不能仅凭 `admin` 或 `system_admin` 进入正常审批路径。 +- 新增细分权限:`system.accounts.manage`、`system.roles.manage`、`payroll.manage`、`employee.bank.read`、`employee.bank.manage`、`employee.identity.manage`、`employee.contract.manage`、`payment.batch.review`。 +- `/employees` 对业务选择调用者只返回窄 DTO;完整员工档案仅本人或人员资料管理员可读,权限拒绝发生在敏感查询或文件读取前。 +- 财务只读银行字段,不读取证件或修改敏感资料;未授权字段从响应中省略。 +- 员工合同列表、元数据、附件下载和写操作使用独立权限;DTO 不返回内部路径,成功访问和变更写脱敏审计。 +- 前端按权限隐藏动作;后端仍为最终授权边界。 + +### 付款一致性与最小审计 + +- 固定“财务制单/执行 + 经理或公司负责人复核”;创建者不得复核自己的批次。 +- 所有金额只需一次独立复核;总金额达到 50000 元时增加警示和强确认,不增加第三人审批。 +- 批次状态为 `draft -> reviewed -> executed|failed|cancelled`;`failed` 只重试失败项或取消剩余项;`executed/cancelled` 为终态。 +- 创建、复核、执行、取消、付款元数据更新和回单作废在事务内锁定批次;执行再按稳定顺序锁定源单并复查状态和金额。 +- 并发竞争返回确定性 409,不重复更新付款状态、审计或通知。 +- 正式范围只接受已审批、未付款且未进入其他有效批次的报销;`payment_batch_v1` 启用时直接报销付款端点不得形成第二付款路径。 +- 新增付款候选查询,页面按业务编号、人员、金额和日期选择,不要求粘贴内部 UUID;创建接口继续权威复查。 +- 取消必须填写 2–500 字原因;取消失败批次只终止未付款项,不表示退款或冲正。 +- 回单采用有理由的留痕作废,不物理删除;已复核后的付款元数据变更写 before/after 审计。 + +### 核心页面与本地候选门禁 + +- 项目、资产、合同、付款、报销表格在 768×1024 和 1024×768 下操作可达;容器可控横滚,页面整体不横向溢出。 +- 报销、采购、合同、资产长表单共享未保存保护,覆盖站内跳转、记录切换、浏览器返回和刷新。 +- 高敏附件删除和付款回单作废显示对象、后果和理由确认;普通可恢复动作不升级为强确认。 +- 新增 `/mobile/attendance` 并消除移动考勤自循环;修复 375×812 下移动归档长字段裁切和底栏遮挡。 +- 完成付款、合同、票据的权限显隐;修正资产旧 E2E,补齐项目/资产标题动作视觉覆盖;通过后回填并归档 `05-31-unified-page-header-actions`。 +- 所有注册路由分类为 `core`、`supported-noncore`、`deferred` 或 `frozen`。未分类、核心行为测试缺失、仅由 `manual/live` 测试覆盖时门禁非零退出。 +- 本地核心矩阵覆盖:登录退出、报销、审批、票据、采购、项目/合同、付款/AP、人员/审计、考勤/归档、移动工作台。 +- 建立仓库内可移植的 `local-candidate` 门禁,不依赖公网、生产配置、历史 Issue CSV 或个人绝对路径。 + +## Acceptance Criteria + +- [x] Prisma enum 包含 `system_admin`,加法迁移可验证且有回滚说明;既有角色与历史账号不自动迁移。 +- [x] 角色矩阵证明业务管理、财务、系统管理员和超级管理员职责分离,系统管理员不能自我提权。 +- [x] 员工目录、完整档案、银行、证件、工资和员工合同权限满足字段级与对象级边界;拒绝路径无敏感 DB/file 副作用。 +- [x] 付款批次创建、复核、执行、失败重试、取消和回单作废满足状态机、maker-checker、原因与审计合同。 +- [x] 合成本地 PostgreSQL 并发验证覆盖双执行、执行与取消、双创建、直接付款与批次付款竞争,结果唯一且不重复审计/通知。 +- [x] 付款页面使用业务候选选择器,不要求普通用户输入 UUID;高额仅增加警示和确认。 +- [x] 五类表格、四类未保存表单、不可逆动作、移动考勤和移动归档通过聚焦浏览器验证。 +- [x] 付款、合同、票据权限显隐与后端拒绝一致;`05-31-unified-page-header-actions` 剩余测试和视觉证据闭合后才归档。 +- [x] 路由清单无未分类项,门禁反例对 unmapped、manual-only、核心缺失行为/视觉均能失败。 +- [x] 十个核心流程束在本地合成环境实际运行,记录最终 URL、H1、角色、主操作和页面错误断言。 +- [x] 完整本地质量门禁通过;权限、状态机、workflow 和迁移变更完成独立只读复审。 +- [x] 收口结论只写 `LOCAL_CANDIDATE_PASS`,不写已部署、可公网测试或企业微信真机通过。 + +## Definition of Done + +- 代码、迁移、测试、现有规范/手册和 finding 生命周期同步更新,未新建重复审查报告。 +- `bash scripts/check-minimal.sh`、Prisma 校验、服务端聚焦及全量测试、前端 typecheck/build、聚焦 Playwright、task validate、Trellis governance、敏感扫描、文档 registry 和 `git diff --check` 通过。 +- 三个实施阶段分别复核,最后以中文 commit 引用本任务;不部署、不 push。 +- `05-31-unified-page-header-actions` 仅在其剩余验收确实闭合后归档;当前任务完成后按 Trellis finish-work 归档并记录 journal。 + +## Test Plan + +- 静态与单元:Prisma validate、触及服务端文件 `node --check`、权限/DTO/审计/付款路由 Vitest、前端 typecheck/build。 +- 集成:隔离合成 PostgreSQL 应用已提交 migration,运行付款四类并发竞争,结束后清理数据库;不连接共享开发库或生产库。 +- 浏览器:本地 Vite dev 与合成 fixture 实际运行十个核心流程;PC 768×1024/1024×768 和 WeCom UA 375×812 记录 URL、H1、角色、错误及溢出指标。 +- 门禁可信度:为 unclassified、manual-only、core behavior missing、visualRequired missing 提供反例测试并断言非零退出;Playwright `--list` 只证明资产可解析。 +- 全量收口:`bash scripts/check-minimal.sh`、task validate、Trellis governance、敏感扫描、文档 registry、`git diff --check`;权限/状态机/workflow/迁移另做只读 reviewer 复核。 + +## Decision (ADR-lite) + +**Context**:公司约 5 人,需要低配置成本,但系统维护者不一定是领导,付款与高敏资料仍需要最小职责分离。 + +**Decision**:保留现有多角色引擎和业务职责,新增受限 `system_admin`,保留隐藏的应急 `admin`;付款使用数据库行锁和 maker-checker,不建设通用权限平台、事件平台或多级审批系统;发布只增加本地候选模式。 + +**Consequences**:日常界面简单且允许一人兼任多个职责;数据库仅做加法迁移;至少两个不同账号才能闭环付款;真实账号分配和内部环境验证必须在后续授权任务完成。 + +## Out of Scope + +- 部署、公网检查、生产配置、真实账号分配、真实数据库写入、迁移 deploy、数据库重置、备份恢复演练。 +- 企业微信真机、真实 OCR、真实付款、银行直连/流水、退款冲正、ERP/总账、差旅付款恢复。 +- 通用 RBAC 平台、权限表达式设计器、全局幂等平台、事件总线、分布式锁、可配置多级金额审批。 +- 全量可访问性、搜索直达、复杂离线数据生命周期、所有通知精准定位、非核心路由视觉重做。 + +## Technical Notes + +- 规划基线:`58c8dec`;实现开始前若 HEAD 变化,更新 task notes 并重跑全部门禁。 +- 发现来源:归档任务 `07-16-system-multidimensional-audit` 中 `DATA-EMP-001`、`DATA-HR-ATT-001`、`PAY-STATE-001`、`PAY-AUDIT-001`、`PAY-UX-001`、`UX-SAFE-001`、`UX-FORM-001`、`MOB-ATTEND-001`、`RESP-TABLE-001`、`REL-GATE-001`、`UI-PERM-001`、`MOB-ARCHIVE-001`。 +- 页面可靠性沿用现有设计语言,不做新视觉风格裁决;当前会话没有可用的 `frontend-design` skill。 diff --git a/.trellis/tasks/07-18-small-team-internal-readiness/task.json b/.trellis/tasks/07-18-small-team-internal-readiness/task.json new file mode 100644 index 00000000..34770212 --- /dev/null +++ b/.trellis/tasks/07-18-small-team-internal-readiness/task.json @@ -0,0 +1,26 @@ +{ + "id": "small-team-internal-readiness", + "name": "small-team-internal-readiness", + "title": "五人团队正式内用最小收口", + "description": "面向约 5 人公司,分离业务管理与 OA 系统运维,收敛敏感资料、付款一致性、核心页面和本地候选门禁。", + "status": "in_progress", + "dev_type": null, + "scope": "fullstack", + "package": null, + "priority": "P1", + "creator": "shunli", + "assignee": "shunli", + "createdAt": "2026-07-18", + "completedAt": null, + "branch": null, + "base_branch": "main", + "worktree_path": null, + "commit": null, + "pr_url": null, + "subtasks": [], + "children": [], + "parent": null, + "relatedFiles": [], + "notes": "承接用户批准的 07-17 方案;实际创建日期为 2026-07-18,规划基线 58c8dec。2026-07-18 当前工作提交通过 full 本地候选门禁(44/44 浏览器、10/10 witness、0 skipped/failed/unexpected)、服务端 985/985、隔离 PostgreSQL 61 migrations 与五组竞态、权限/付款/workflow/migration 只读复审;复审发现的员工统计敏感汇总权限漏口已修复并以 system_admin 零敏感查询回归验证,结论为 LOCAL_CANDIDATE_PASS。本轮未部署、未 push、未访问公网/生产/真实数据、未做企微真机或真实付款;后续内部部署另行授权。", + "meta": {} +} diff --git a/.trellis/tasks/05-31-unified-page-header-actions/check.jsonl b/.trellis/tasks/archive/2026-07/05-31-unified-page-header-actions/check.jsonl similarity index 100% rename from .trellis/tasks/05-31-unified-page-header-actions/check.jsonl rename to .trellis/tasks/archive/2026-07/05-31-unified-page-header-actions/check.jsonl diff --git a/.trellis/tasks/05-31-unified-page-header-actions/design.md b/.trellis/tasks/archive/2026-07/05-31-unified-page-header-actions/design.md similarity index 100% rename from .trellis/tasks/05-31-unified-page-header-actions/design.md rename to .trellis/tasks/archive/2026-07/05-31-unified-page-header-actions/design.md diff --git a/.trellis/tasks/05-31-unified-page-header-actions/implement.jsonl b/.trellis/tasks/archive/2026-07/05-31-unified-page-header-actions/implement.jsonl similarity index 100% rename from .trellis/tasks/05-31-unified-page-header-actions/implement.jsonl rename to .trellis/tasks/archive/2026-07/05-31-unified-page-header-actions/implement.jsonl diff --git a/.trellis/tasks/05-31-unified-page-header-actions/implement.md b/.trellis/tasks/archive/2026-07/05-31-unified-page-header-actions/implement.md similarity index 100% rename from .trellis/tasks/05-31-unified-page-header-actions/implement.md rename to .trellis/tasks/archive/2026-07/05-31-unified-page-header-actions/implement.md diff --git a/.trellis/tasks/05-31-unified-page-header-actions/prd.md b/.trellis/tasks/archive/2026-07/05-31-unified-page-header-actions/prd.md similarity index 88% rename from .trellis/tasks/05-31-unified-page-header-actions/prd.md rename to .trellis/tasks/archive/2026-07/05-31-unified-page-header-actions/prd.md index ad45bc2f..a88b46ac 100644 --- a/.trellis/tasks/05-31-unified-page-header-actions/prd.md +++ b/.trellis/tasks/archive/2026-07/05-31-unified-page-header-actions/prd.md @@ -79,7 +79,7 @@ - [x] 规则写入前端设计规范,后续页面可复用。 - [x] 标题、筛选、列表列、空状态、详情/编辑操作区至少完成一致性审计,形成后续改造清单。 - [x] 前端 typecheck/build 通过。 -- [ ] 聚焦 Playwright/视觉复检覆盖主要业务页。 +- [x] 聚焦 Playwright/视觉复检覆盖主要业务页。 ## Test Plan @@ -114,3 +114,11 @@ node scripts/qa/check-trellis-governance.mjs - `HeaderActionGroup` 目前只统一布局,不约束动作数量;缺少七页面统一标题动作合同测试,现有截图缺项目和资产列表。 上述 finding 需要独立修复 task;在修复并补齐视觉/回归覆盖前,本任务保持 `in_progress`,不得虚报完成或归档。 + +## Final Closeout Verification (2026-07-18) + +- 承接任务 `07-18-small-team-internal-readiness` 已闭合上述 follow-up:付款复核角色看不到新建批次,应付账款导出按权限隐藏,合同只读角色看不到新增/编辑/续约/归档,票据无上传权限时 fail-closed;对应合成 Playwright 5/5 纳入最终候选门禁。 +- `assets-flow.spec.ts` 和历史 manual 资产用例已改为通过标题区 `更多 -> 资产分类`;资产流程 spec 可解析,当前菜单行为由实际浏览器回归覆盖。 +- `r103-cross-page-ui-consistency.spec.ts` 扩展为 8 项:七个页面标题区严格执行“一主操作 + 最多一个更多”,并覆盖项目/资产代表视觉、资产“更多 -> 资产分类”及核心页面动作合同。 +- 当前 checkout 的 `bash scripts/qa/run-local-candidate.sh full` 最终为 44/44 passed、0 skipped/failed/unexpected;项目、资产、合同、付款、报销双断点截图及权限显隐 spec 均包含在候选 acceptance 集合。 +- 本任务仅完成本地前端验收并归档;未部署、未公网复检,不能据此宣称线上可测。 diff --git a/.trellis/tasks/05-31-unified-page-header-actions/task.json b/.trellis/tasks/archive/2026-07/05-31-unified-page-header-actions/task.json similarity index 58% rename from .trellis/tasks/05-31-unified-page-header-actions/task.json rename to .trellis/tasks/archive/2026-07/05-31-unified-page-header-actions/task.json index f0a8ee10..927a3135 100644 --- a/.trellis/tasks/05-31-unified-page-header-actions/task.json +++ b/.trellis/tasks/archive/2026-07/05-31-unified-page-header-actions/task.json @@ -3,7 +3,7 @@ "name": "unified-page-header-actions", "title": "统一页面右上角操作区规划", "description": "统一 PC 页面标题右上角操作区规划,收敛主操作、更多、筛选、导出、刷新等按钮位置。", - "status": "in_progress", + "status": "completed", "dev_type": null, "scope": "frontend", "package": null, @@ -11,7 +11,7 @@ "creator": "shunli", "assignee": "shunli", "createdAt": "2026-05-31", - "completedAt": null, + "completedAt": "2026-07-18", "branch": null, "base_branch": "main", "worktree_path": null, @@ -21,6 +21,6 @@ "children": [], "parent": null, "relatedFiles": [], - "notes": "2026-07-16 收口验证:typecheck/build 和 r103 3/3 通过,标题迁移 6 项验收已证实;仍有权限显隐、资产旧测试和项目/资产视觉覆盖 finding,需独立修复 task 后才能归档。", + "notes": "2026-07-18 由 07-18-small-team-internal-readiness 闭合剩余项:付款/合同/票据权限显隐合成 Playwright 5/5,资产旧 E2E 改用更多菜单,r103 扩展为 8 项并覆盖七页面标题动作及项目/资产视觉;最终 local-candidate full 为 44/44、0 skipped/failed/unexpected。本任务仅本地验收,未部署或公网复检,可归档。", "meta": {} } diff --git a/.trellis/tasks/archive/2026-07/07-16-system-multidimensional-audit/research/findings.md b/.trellis/tasks/archive/2026-07/07-16-system-multidimensional-audit/research/findings.md index 750c5942..43990390 100644 --- a/.trellis/tasks/archive/2026-07/07-16-system-multidimensional-audit/research/findings.md +++ b/.trellis/tasks/archive/2026-07/07-16-system-multidimensional-audit/research/findings.md @@ -3,7 +3,7 @@ ## 台账口径 - 本文件是当前任务唯一的分级与生命周期事实源。分域报告保留发现证据,但不再各自决定当前严重度或状态。 -- 发现基线为 `bbb5c89`;当前本地增量目标为 `8f8606c`。发现时严重度不因后续修复而改写,收敛校准会在单元格中显式注明。 +- 发现基线为 `bbb5c89`;原审查收口增量目标为 `8f8606c`。后续修复任务 `07-18-small-team-internal-readiness` 的规划基线为 `58c8dec`,其本地验证以该任务 `evidence.md` 所在工作提交为准。发现时严重度不因后续修复而改写,收敛校准会在单元格中显式注明。 - 发现状态、当前修复状态、部署状态、外部验证状态彼此独立。 - `NO_FIX_TO_DEPLOY` 表示尚无修复产物,不等于待部署;`NOT_APPLICABLE` 仅用于非运行时代码/纯测试合同。 - `ACTIVE_TASK_OPEN` 表示该项仍开放但已由现有活动 task 承载;本轮只做去重,不另建 task。`FOLLOW_UP_RECOMMENDED` 表示持续治理建议,不是已修复。 @@ -15,24 +15,24 @@ | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | | `SEC-AUTH-001` | 认证 / 代理信任 | P0 | 高 | `CONFIRMED` | `LOCAL_FIX_VERIFIED` | `DEPLOYMENT_PENDING` | `EXTERNAL_BEHAVIOR_PENDING` | `business-security-review.md#bsr-sec-001`;归档任务 `07-17-auth-challenge-trusted-proxy-hardening` | 认证边界不足可能导致账号被未授权控制。 | 挑战单次有效、并发一致、来源可信且可审计。 | 当前 HEAD 聚焦负路径、完整服务端与隔离并发验证通过。 | 另行授权:部署与公网复核 | | `SEC-ATT-001` | 业务附件 / 浏览器隔离 | P0 | 高 | `CONFIRMED` | `LOCAL_FIX_VERIFIED` | `DEPLOYMENT_PENDING` | `EXTERNAL_BEHAVIOR_PENDING` | `business-security-review.md#bsr-sec-002`;归档任务 `07-17-purchase-attachment-xss-hardening` | 主动内容可能破坏 OA 同源会话与业务数据边界。 | 实际内容验证、重新授权/审计、非执行下载。 | 当前 HEAD 策略/路由、完整服务端与聚焦浏览器验证通过。 | 另行授权:部署与公网复核 | -| `DATA-EMP-001` | 员工敏感数据授权 | P1 | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `EXTERNAL_BEHAVIOR_PENDING` | `business-security-review.md#bsr-authz-001` | 广泛能力可能读取超出岗位需要的员工敏感字段。 | 字段级授权与人员范围同时最小化。 | 只读跨层检查确认权限粒度不足,未读取真实资料。 | WP-1 员工敏感数据范围授权 | -| `PAY-STATE-001` | 付款状态机 / 并发 | P1 | 高 | `LIKELY` | `OPEN` | `NO_FIX_TO_DEPLOY` | `EXTERNAL_BEHAVIOR_PENDING` | `business-security-review.md#bsr-data-001` | 并发状态变化可能造成重复结果或证据覆盖。 | 条件更新/锁、幂等和不可覆盖审计保证单一结果。 | 静态事务检查发现保护证据不足,未连接数据库或执行付款。 | WP-2 付款状态一致性与审计 | -| `DATA-HR-ATT-001` | 员工合同附件 | P1 | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `EXTERNAL_BEHAVIOR_PENDING` | `business-security-review.md#bsr-priv-001` | 高敏合同资料可能受过宽权限控制且缺访问用途审计。 | 独立最小权限、对象级授权、安全 DTO 和下载审计。 | 只读路由/测试合同确认缺口,未读取真实合同。 | WP-3 员工合同附件权限与访问审计 | +| `DATA-EMP-001` | 员工敏感数据授权 | P1 | 高 | `CONFIRMED` | `LOCAL_FIX_VERIFIED` | `DEPLOYMENT_PENDING` | `EXTERNAL_BEHAVIOR_PENDING` | `business-security-review.md#bsr-authz-001`;任务 `07-18-small-team-internal-readiness/evidence.md` | 广泛能力可能读取超出岗位需要的员工敏感字段。 | 字段级授权与人员范围同时最小化。 | 当前工作提交的窄目录 DTO、字段/对象权限、员工统计敏感汇总独立授权、零敏感副作用矩阵与全量服务端门禁通过;未读取真实资料。 | WP-1 员工敏感数据范围授权 | +| `PAY-STATE-001` | 付款状态机 / 并发 | P1 | 高 | `LIKELY` | `LOCAL_FIX_VERIFIED` | `DEPLOYMENT_PENDING` | `EXTERNAL_BEHAVIOR_PENDING` | `business-security-review.md#bsr-data-001`;任务 `07-18-small-team-internal-readiness/evidence.md` | 并发状态变化可能造成重复结果或证据覆盖。 | 条件更新/锁、幂等和不可覆盖审计保证单一结果。 | 空隔离 PostgreSQL 应用 61 项 migration,五组竞态、审计/通知唯一性和临时库零残留通过;未执行真实付款。 | WP-2 付款状态一致性与审计 | +| `DATA-HR-ATT-001` | 员工合同附件 | P1 | 高 | `CONFIRMED` | `LOCAL_FIX_VERIFIED` | `DEPLOYMENT_PENDING` | `EXTERNAL_BEHAVIOR_PENDING` | `business-security-review.md#bsr-priv-001`;任务 `07-18-small-team-internal-readiness/evidence.md` | 高敏合同资料可能受过宽权限控制且缺访问用途审计。 | 独立最小权限、对象级授权、安全 DTO 和下载审计。 | 当前工作提交的独立权限、无内部路径 DTO、内容复验、审计先于响应及拒绝零文件读取测试通过;未读取真实合同。 | WP-3 员工合同附件权限与访问审计 | | `DATA-EXPORT-001` | 统一导出 | P1 | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `NOT_REQUIRED` | `business-security-review.md#bsr-sec-003` | 导出内容可能在表格软件中形成非预期活动行为。 | 全部 CSV/表格出口统一安全编码并有合成负例。 | 静态检查确认缺少统一中和合同,未使用真实导出。 | WP-5 客户端数据生命周期与统一导出 | | `PRIV-OFFLINE-001` | 客户端数据生命周期 | P1 | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `EXTERNAL_BEHAVIOR_PENDING` | `business-security-review.md#bsr-priv-002` | 退出或换账号后残留身份、审批或草稿可能被后续使用者读取。 | 先确认离线必要性,再按账号隔离、最小缓存并可靠清除。 | 静态检查确认持久化和清理未闭合,未检查真实设备。 | WP-5 客户端数据生命周期与统一导出 | | `DATA-DOWNLOAD-001` | 下载凭证 / 日志面 | P2 | 中高 | `LIKELY` | `OPEN` | `NO_FIX_TO_DEPLOY` | `EXTERNAL_GATED` | `business-security-review.md#bsr-sec-004` | 短期访问凭据可能进入常规浏览器或边缘记录面。 | 使用当前认证上下文下载并最小化 URL、日志和缓存暴露。 | 代码存在风险可能性;真实边缘日志未获授权检查。 | WP-5 客户端数据生命周期与统一导出 | | `DATA-ACL-001` | 受限资料规则 | P2 | 中高 | `LIKELY` | `OPEN` | `NO_FIX_TO_DEPLOY` | `NOT_REQUIRED` | `business-security-review.md#bsr-authz-002` | 已保存但未执行的规则会造成受限资料边界误判。 | 产品明确其为 ACL 或废弃元数据;保留则由后端强制执行。 | 静态检查确认规则与执行证据不一致。 | WP-5 客户端数据生命周期与统一导出 | -| `PAY-AUDIT-001` | 付款取消审计 | P2 | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `NOT_REQUIRED` | `business-security-review.md#bsr-data-002` | 取消责任与前后状态难以完整还原。 | 记录原因、操作者、前后状态与并发条件。 | 静态路由/模型检查确认缺口,未执行真实状态变化。 | WP-2 付款状态一致性与审计 | +| `PAY-AUDIT-001` | 付款取消审计 | P2 | 高 | `CONFIRMED` | `LOCAL_FIX_VERIFIED` | `DEPLOYMENT_PENDING` | `NOT_REQUIRED` | `business-security-review.md#bsr-data-002`;任务 `07-18-small-team-internal-readiness/evidence.md` | 取消责任与前后状态难以完整还原。 | 记录原因、操作者、前后状态与并发条件。 | 可空取消原因 migration、2–500 字原因、保留已执行项、回单留痕作废及 before/after 审计回归通过。 | WP-2 付款状态一致性与审计 | | `WEB-DEFENSE-001` | 浏览器防御纵深 | P2 | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `EXTERNAL_BEHAVIOR_PENDING` | `business-security-review.md#bsr-sec-005` | 缺少内容执行限制会降低未知缺陷的纵深保护。 | 基于资源清单分阶段收紧 CSP 并保持附件独立沙箱。 | 本地配置确认当前未启用全站策略;未推断具体利用路径。 | WP-5 客户端数据生命周期与统一导出 | -| `UI-PERM-001` | PC 写动作显隐 | P2(收敛校准;原分域报告 P1/P2 不一) | 高 | `CONFIRMED` | `ACTIVE_TASK_OPEN` | `NO_FIX_TO_DEPLOY` | `NOT_REQUIRED` | `page-static-review.md#既有-finding-复核与去重`;`BSR-UX-001`;活动任务 `05-31-unified-page-header-actions` | 后端虽拒绝写入,PC 用户仍可能进入最终失败的流程。 | PC 显隐与后端拒绝共享同一能力合同,后端保持权威。 | 付款、合同、票据三处既有 PC finding 完成只读去重;移动导航由 `MOB-PERM-001` 单列。 | WP-6 页面权限显隐及旧标题任务收口 | -| `PAY-UX-001` | 付款业务可用性 | P1 | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `NOT_REQUIRED` | `page-static-review.md#page-p1-001` | 财务人员难以凭业务信息选择待付对象,可能错选或无法创建。 | 以待付池和业务编号/人员/金额选择对象,内部标识仅由系统传递。 | 静态页面与 E2E 合同确认普通流程依赖内部标识。 | WP-2 付款状态一致性与审计 | -| `UX-SAFE-001` | 不可逆证据 / 资金动作 | P1(收敛后边界) | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `NOT_REQUIRED` | `page-static-review.md#page-p1-002` | 误触可能删除不可逆证据或改变资金结果。 | 强确认展示对象、后果、可逆性与必要理由。 | 静态检查确认代表性高影响动作在 mutation 前缺少强确认。 | WP-6 页面权限显隐及旧标题任务收口 | +| `UI-PERM-001` | PC 写动作显隐 | P2(收敛校准;原分域报告 P1/P2 不一) | 高 | `CONFIRMED` | `LOCAL_FIX_VERIFIED` | `DEPLOYMENT_PENDING` | `NOT_REQUIRED` | `page-static-review.md#既有-finding-复核与去重`;`BSR-UX-001`;归档任务 `05-31-unified-page-header-actions`;任务 `07-18-small-team-internal-readiness/evidence.md` | 后端虽拒绝写入,PC 用户仍可能进入最终失败的流程。 | PC 显隐与后端拒绝共享同一能力合同,后端保持权威。 | 付款、合同、票据正负显隐合成 Playwright 5/5 与后端拒绝测试通过;旧标题任务已完成本地验收并归档。 | WP-6 页面权限显隐及旧标题任务收口 | +| `PAY-UX-001` | 付款业务可用性 | P1 | 高 | `CONFIRMED` | `LOCAL_FIX_VERIFIED` | `DEPLOYMENT_PENDING` | `NOT_REQUIRED` | `page-static-review.md#page-p1-001`;任务 `07-18-small-team-internal-readiness/evidence.md` | 财务人员难以凭业务信息选择待付对象,可能错选或无法创建。 | 以待付池和业务编号/人员/金额选择对象,内部标识仅由系统传递。 | 当前工作提交的候选接口权威复查及合成浏览器业务选择器通过,普通页面不要求粘贴内部 UUID。 | WP-2 付款状态一致性与审计 | +| `UX-SAFE-001` | 不可逆证据 / 资金动作 | P1(收敛后边界) | 高 | `CONFIRMED` | `LOCAL_FIX_VERIFIED` | `DEPLOYMENT_PENDING` | `NOT_REQUIRED` | `page-static-review.md#page-p1-002`;任务 `07-18-small-team-internal-readiness/evidence.md` | 误触可能删除不可逆证据或改变资金结果。 | 强确认展示对象、后果、可逆性与必要理由。 | 合成浏览器证明高敏附件删除与回单作废展示对象、后果和理由;取消或理由不足时零 mutation。 | WP-6 页面权限显隐及旧标题任务收口 | | `UX-SAFE-002` | 可恢复管理动作 | P2(由原 PAGE-P1-002 拆分校准) | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `NOT_REQUIRED` | `page-static-review.md#page-p1-002` | 普通启停等可恢复动作存在误触和反馈不清。 | 使用轻确认或可撤销反馈,按可逆性分级。 | 只读检查确认部分可恢复动作仍直接执行。 | WP-6 页面权限显隐及旧标题任务收口 | -| `UX-FORM-001` | 核心长表单可靠性 | P1 | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `NOT_REQUIRED` | `page-static-review.md#page-p1-003` | 采购、报销、合同、资产等长输入可能因离开页面丢失。 | 共享 dirty-state 合同覆盖路由离开、刷新和记录切换。 | 静态检查未找到未保存保护实现。 | WP-6 页面权限显隐及旧标题任务收口 | -| `MOB-ATTEND-001` | 移动考勤入口 | P1(由原 PAGE-P1-004 拆分) | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `EXTERNAL_BEHAVIOR_PENDING` | `page-static-review.md#page-p1-004` | 企业微信/触控小屏考勤入口会回到原移动首页。 | 提供明确移动镜像或允许只读响应式页,不形成自循环。 | 静态路由解析确认自循环;真机点击待另行授权。 | WP-6 页面权限显隐及旧标题任务收口 | -| `MOB-TARGET-001` | 移动通知定位 | P2(由原 PAGE-P1-004 拆分校准) | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `EXTERNAL_BEHAVIOR_PENDING` | `page-static-review.md#page-p1-004` | 部分通知不能定位原业务对象,用户需重新查找。 | 每种通知保留稳定对象标识并验证最终 URL/内容。 | 静态链接映射确认目标参数不完整;真实通知待授权。 | WP-6 页面权限显隐及旧标题任务收口 | +| `UX-FORM-001` | 核心长表单可靠性 | P1 | 高 | `CONFIRMED` | `LOCAL_FIX_VERIFIED` | `DEPLOYMENT_PENDING` | `NOT_REQUIRED` | `page-static-review.md#page-p1-003`;任务 `07-18-small-team-internal-readiness/evidence.md` | 采购、报销、合同、资产等长输入可能因离开页面丢失。 | 共享 dirty-state 合同覆盖路由离开、刷新和记录切换。 | data router / `useBlocker` 共享保护在报销、采购、合同、资产中覆盖站内离开、返回/刷新和记录切换,合成浏览器通过。 | WP-6 页面权限显隐及旧标题任务收口 | +| `MOB-ATTEND-001` | 移动考勤入口 | P1(由原 PAGE-P1-004 拆分) | 高 | `CONFIRMED` | `LOCAL_FIX_VERIFIED` | `DEPLOYMENT_PENDING` | `EXTERNAL_BEHAVIOR_PENDING` | `page-static-review.md#page-p1-004`;任务 `07-18-small-team-internal-readiness/evidence.md` | 企业微信/触控小屏考勤入口会回到原移动首页。 | 提供明确移动镜像或允许只读响应式页,不形成自循环。 | `/mobile/attendance` 的 375×812 最终 URL、H1、角色、动作、错误和溢出断言通过;真机点击待另行授权。 | WP-6 页面权限显隐及旧标题任务收口 | +| `MOB-TARGET-001` | 移动通知定位 | P2(由原 PAGE-P1-004 拆分校准) | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `EXTERNAL_BEHAVIOR_PENDING` | `page-static-review.md#page-p1-004`;任务 `07-18-small-team-internal-readiness/evidence.md` | 部分通知不能定位原业务对象,用户需重新查找。 | 每种通知保留稳定对象标识并验证最终 URL/内容。 | 考勤目标增量已实现并验证;资产/盘点等其他对象定位未在本任务闭合,整体保持开放,真实通知待授权。 | WP-6 页面权限显隐及旧标题任务收口 | | `SEARCH-NAV-001` | 全局搜索导航 | P2(收敛校准;原报告 P1) | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `NOT_REQUIRED` | `page-static-review.md#page-p1-005` | 点击部分搜索结果只到列表,不能确认命中对象。 | 对象型结果提供稳定详情或定位/高亮合同。 | 静态 href 与现有单实体 E2E 确认覆盖不完整。 | WP-6 页面权限显隐及旧标题任务收口 | -| `RESP-TABLE-001` | 中间断点表格 | P1 | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `NOT_REQUIRED` | `page-static-review.md#page-p1-006`;`browser-review.md` | 共享表格隐藏超出宽度,多个核心列表的右侧操作不可达。 | 操作列可见或容器可控横滚,页面本身不溢出。 | 768 下容器缺 58px、1024 下缺 106px 且 `overflow-x:hidden`;代表页面操作命中失败。 | WP-6 页面权限显隐及旧标题任务收口 | +| `RESP-TABLE-001` | 中间断点表格 | P1 | 高 | `CONFIRMED` | `LOCAL_FIX_VERIFIED` | `DEPLOYMENT_PENDING` | `NOT_REQUIRED` | `page-static-review.md#page-p1-006`;`browser-review.md`;任务 `07-18-small-team-internal-readiness/evidence.md` | 共享表格隐藏超出宽度,多个核心列表的右侧操作不可达。 | 操作列可见或容器可控横滚,页面本身不溢出。 | 项目、资产、合同、付款、报销在 768×1024 与 1024×768 下局部横滚、右侧操作命中、document 无横溢断言与脱敏截图通过。 | WP-6 页面权限显隐及旧标题任务收口 | | `A11Y-FORM-001` | 表单可访问性 | P2(收敛校准;原报告 P1) | 高 | `CONFIRMED` | `FOLLOW_UP_RECOMMENDED` | `NO_FIX_TO_DEPLOY` | `NOT_REQUIRED` | `page-static-review.md#page-p1-007` | 辅助技术用户较难识别字段和动态错误。 | 共享字段/错误组件提供名称、关联、错误状态和 live feedback。 | 静态样例确认语义缺口;无行为证据证明核心流程完全不可完成。 | WP-6 页面权限显隐及旧标题任务收口 | | `NAV-META-001` | 路由 / 标题元数据 | P2 | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `NOT_REQUIRED` | `page-static-review.md#page-p2-001` | 标题、最近访问和浏览器 tab 语义漂移。 | 统一 typed route manifest 与页面可控覆写。 | 静态映射确认缺失、误匹配与固定浏览器标题。 | WP-6 页面权限显隐及旧标题任务收口 | | `ERR-RECOVERY-001` | 错误恢复 | P2 | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `NOT_REQUIRED` | `page-static-review.md#page-p2-002` | 403/404/渲染错误可能把用户带向隐藏旧入口。 | 所有恢复动作回到稳定工作台或安全上一页。 | 静态链接确认仍指向旧路径。 | WP-6 页面权限显隐及旧标题任务收口 | @@ -40,13 +40,13 @@ | `UX-ERROR-001` | 普通页面错误文案 | P2 | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `NOT_REQUIRED` | `page-static-review.md#page-p2-004` | 用户先看到内部码/枚举,难以理解原因与下一步。 | 普通界面展示自然语言和恢复动作,诊断码仅进受控区域。 | 静态样例确认普通页面直接组合技术信息。 | WP-6 页面权限显隐及旧标题任务收口 | | `A11Y-TABLE-001` | 表格键盘可达 | P2 | 高 | `CONFIRMED` | `FOLLOW_UP_RECOMMENDED` | `NO_FIX_TO_DEPLOY` | `NOT_REQUIRED` | `page-static-review.md#page-p2-005` | 键盘用户无法等价选择部分表格记录。 | 使用真实按钮/链接和明确选中状态。 | 静态样例确认行点击缺少键盘等价入口。 | WP-6 页面权限显隐及旧标题任务收口 | | `A11Y-VISUAL-001` | 对比度 / 减弱动效 | P2 | 高 | `CONFIRMED` | `FOLLOW_UP_RECOMMENDED` | `NO_FIX_TO_DEPLOY` | `NOT_REQUIRED` | `page-static-review.md#page-p2-006` | 低视力与减弱动效用户体验下降。 | 共享 token 达到既定对比度并尊重 reduced motion。 | 静态 token/样式检查确认当前合同不足。 | WP-6 页面权限显隐及旧标题任务收口 | -| `QA-VISUAL-001` | 视觉证据可信度 | P2 | 高 | `CONFIRMED` | `OPEN` | `NOT_APPLICABLE` | `NOT_REQUIRED` | `page-static-review.md#page-p2-007` | 报告可能遗漏真实路由/中间断点却给出过强信心。 | 显式路由计划、最终 URL/标题/错误页断言和代表断点。 | 静态矩阵确认通配、旧路由和断点缺口。 | WP-4 发布门禁可信化 | +| `QA-VISUAL-001` | 视觉证据可信度 | P2 | 高 | `CONFIRMED` | `LOCAL_FIX_VERIFIED` | `NOT_APPLICABLE` | `NOT_REQUIRED` | `page-static-review.md#page-p2-007`;任务 `07-18-small-team-internal-readiness/evidence.md` | 报告可能遗漏真实路由/中间断点却给出过强信心。 | 显式路由计划、最终 URL/标题/错误页断言和代表断点。 | 71 条路由分类、10 个 source-bound witness、7 个 acceptance spec、代表断点和结果 parser 反例均通过。 | WP-4 发布门禁可信化 | | `MOB-PERM-001` | 移动导航权限 | P2 | 中高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `NOT_REQUIRED` | `page-static-review.md#page-p2-008` | 最小权限角色看到无效入口或进入 403。 | 移动入口统一复用能力谓词,未授权默认隐藏。 | 静态导航与守卫对照确认不一致。 | WP-6 页面权限显隐及旧标题任务收口 | -| `QA-ASSET-001` | 资产 E2E 漂移 | P2 | 高 | `DUPLICATE` | `ACTIVE_TASK_OPEN` | `NOT_APPLICABLE` | `NOT_REQUIRED` | `page-static-review.md#page-dup-p2-001`;活动任务 `05-31-unified-page-header-actions` | 自动化仍按旧交互执行,不能证明当前页面。 | 测试跟随当前“更多”菜单交互,不为旧测试恢复旧按钮。 | 只读测试/页面对照确认重复项。 | WP-6 页面权限显隐及旧标题任务收口 | -| `QA-HEADER-001` | 标题动作合同 / 视觉覆盖 | P2 | 高 | `DUPLICATE` | `ACTIVE_TASK_OPEN` | `NOT_APPLICABLE` | `NOT_REQUIRED` | `page-static-review.md#page-dup-p2-002`;活动任务 `05-31-unified-page-header-actions` | 标题区可能继续堆叠动作且现有回归不报警。 | 可测试的主操作/更多槽位及项目、资产代表性视觉断言。 | 静态组件与测试矩阵确认重复项。 | WP-6 页面权限显隐及旧标题任务收口 | +| `QA-ASSET-001` | 资产 E2E 漂移 | P2 | 高 | `DUPLICATE` | `LOCAL_FIX_VERIFIED` | `NOT_APPLICABLE` | `NOT_REQUIRED` | `page-static-review.md#page-dup-p2-001`;归档任务 `05-31-unified-page-header-actions`;任务 `07-18-small-team-internal-readiness/evidence.md` | 自动化仍按旧交互执行,不能证明当前页面。 | 测试跟随当前“更多”菜单交互,不为旧测试恢复旧按钮。 | 资产 E2E 已改为“更多→资产分类”,spec 可解析且实际菜单行为/代表视觉由候选浏览器回归通过。 | WP-6 页面权限显隐及旧标题任务收口 | +| `QA-HEADER-001` | 标题动作合同 / 视觉覆盖 | P2 | 高 | `DUPLICATE` | `LOCAL_FIX_VERIFIED` | `NOT_APPLICABLE` | `NOT_REQUIRED` | `page-static-review.md#page-dup-p2-002`;归档任务 `05-31-unified-page-header-actions`;任务 `07-18-small-team-internal-readiness/evidence.md` | 标题区可能继续堆叠动作且现有回归不报警。 | 可测试的主操作/更多槽位及项目、资产代表性视觉断言。 | 七页面“一主操作+最多一个更多”合同、项目/资产代表视觉和资产菜单行为纳入 44/44 候选回归。 | WP-6 页面权限显隐及旧标题任务收口 | | `QA-COVERAGE-001` | 风险型测试覆盖 | P2 | 高 | `CONFIRMED` | `OPEN` | `NOT_APPLICABLE` | `NOT_REQUIRED` | `engineering-review.md#测试资产与实测覆盖率` | 全量用例通过仍不能防止关键 worker/脚本覆盖退化。 | 对认证、资金、worker、迁移/恢复入口设置分层防回退门禁。 | 旧基线 coverage 与入口清单确认关键运行面无阈值。 | WP-4 发布门禁可信化 | -| `REL-GATE-001` | 发布门禁可信度 | P1 | 高 | `CONFIRMED` | `OPEN` | `NOT_APPLICABLE` | `NOT_REQUIRED` | `engineering-review.md#流程覆盖扫描可信度`、`#项目级发布门禁可执行性`;`current-head-delta.md` | 标准门禁不可移植且会把大量未映射路由报告成无高风险缺口。 | 仓库内可执行、显式映射/豁免、排除 manual/live、缺口非零退出并自测反例。 | 当前 HEAD 扫描 68 路由仍有 39 未映射却无 gap;标准 runner 依赖失效外部前置。 | WP-4 发布门禁可信化 | -| `MOB-ARCHIVE-001` | 移动归档详情响应式 | P2 | 高 | `CONFIRMED` | `OPEN` | `NO_FIX_TO_DEPLOY` | `EXTERNAL_BEHAVIOR_PENDING` | `baseline.md#移动路由静态合同`;`browser-review.md` | 长标识使详情内部内容被隐藏,用户无法横向找回。 | 保持路由/卡片/底栏基线并让详情长字段安全换行或可控滚动。 | 375×812 的 URL、H1、空态、列表和底栏通过;长详情 panel 351/457,约 105px 被 `overflow-x:hidden` 裁切。 | WP-6 页面权限显隐及旧标题任务收口 | +| `REL-GATE-001` | 发布门禁可信度 | P1 | 高 | `CONFIRMED` | `LOCAL_FIX_VERIFIED` | `NOT_APPLICABLE` | `NOT_REQUIRED` | `engineering-review.md#流程覆盖扫描可信度`、`#项目级发布门禁可执行性`;`current-head-delta.md`;任务 `07-18-small-team-internal-readiness/evidence.md` | 标准门禁不可移植且会把大量未映射路由报告成无高风险缺口。 | 仓库内可执行、显式映射/豁免、排除 manual/live、缺口非零退出并自测反例。 | `full` 门禁在仓库内 exit 0;71 路由、10/10 witness、7/7 acceptance、52 个反例与 44/44 浏览器结果通过,且 full/focused/list 标签分离。 | WP-4 发布门禁可信化 | +| `MOB-ARCHIVE-001` | 移动归档详情响应式 | P2 | 高 | `CONFIRMED` | `LOCAL_FIX_VERIFIED` | `DEPLOYMENT_PENDING` | `EXTERNAL_BEHAVIOR_PENDING` | `baseline.md#移动路由静态合同`;`browser-review.md`;任务 `07-18-small-team-internal-readiness/evidence.md` | 长标识使详情内部内容被隐藏,用户无法横向找回。 | 保持路由/卡片/底栏基线并让详情长字段安全换行或可控滚动。 | 375×812 的空态、记录、长详情、底栏与 document 横溢断言及脱敏截图通过;企业微信真机仍待授权。 | WP-6 页面权限显隐及旧标题任务收口 | ## 不进入 Finding 的观察与外部补证 diff --git a/client/src/api/approvalRules.ts b/client/src/api/approvalRules.ts index f73b12c2..232b44cd 100644 --- a/client/src/api/approvalRules.ts +++ b/client/src/api/approvalRules.ts @@ -7,8 +7,7 @@ export type ApprovalRoleCode = | "finance" | "finance_mgr" | "administration" - | "hr" - | "admin"; + | "hr"; export type ApprovalOwnerSelfFallbackRoleCode = "finance_mgr"; export type ApprovalRule = { diff --git a/client/src/api/employees.ts b/client/src/api/employees.ts index 8af52fd4..f74921cc 100644 --- a/client/src/api/employees.ts +++ b/client/src/api/employees.ts @@ -164,7 +164,7 @@ export type EmployeeContract = { startDate: string | null; endDate: string | null; terminatedDate: string | null; - attachmentPath: string | null; + hasAttachment: boolean; attachmentUrl: string | null; status: EmployeeContractStatus; createdAt: string | null; @@ -285,6 +285,7 @@ export type FetchEmployeesParams = { hireDateFrom?: string; hireDateTo?: string; employmentStatus?: EmployeeEmploymentStatus; + scope?: "directory" | "management"; }; export type BatchOperationResult = { @@ -384,6 +385,7 @@ export async function fetchEmployees(params: FetchEmployeesParams) { if (params.hireDateFrom) search.set("hireDateFrom", params.hireDateFrom); if (params.hireDateTo) search.set("hireDateTo", params.hireDateTo); if (params.employmentStatus) search.set("employmentStatus", params.employmentStatus); + if (params.scope) search.set("scope", params.scope); return apiRequestData(`/api/employees?${search.toString()}`); } @@ -547,7 +549,6 @@ export async function createEmployeeContract( startDate: string; endDate: string; terminatedDate?: string | null; - attachmentPath?: string | null; }, ) { return apiRequestData(`/api/employees/${employeeId}/contracts`, { @@ -565,7 +566,6 @@ export async function patchEmployeeContract( startDate: string; endDate: string; terminatedDate: string | null; - attachmentPath: string | null; }>, ) { return apiRequestData(`/api/employees/${employeeId}/contracts/${contractId}`, { @@ -588,9 +588,10 @@ export async function downloadEmployeeContractAttachment(attachmentUrl: string): return apiRequestBlob(attachmentUrl); } -export async function deleteEmployeeContract(employeeId: string, contractId: string) { +export async function deleteEmployeeContract(employeeId: string, contractId: string, reason: string) { return apiRequestData<{ contractId: string; deleted: boolean }>(`/api/employees/${employeeId}/contracts/${contractId}`, { method: "DELETE", + body: JSON.stringify({ reason }), }); } diff --git a/client/src/api/errorMessages.ts b/client/src/api/errorMessages.ts index 9dda200d..e667b01c 100644 --- a/client/src/api/errorMessages.ts +++ b/client/src/api/errorMessages.ts @@ -138,7 +138,23 @@ export function mapApiErrorMessage( } if (details?.payment === "already_paid_conflict") { - return "该报销单已支付且付款信息冲突,请刷新后确认。"; + return "该单据已由其他付款动作处理,请刷新后核对付款记录。"; + } + + if (details?.payment === "source_amount_changed") { + return "源单金额已发生变化,本次付款未执行,请刷新后重新制单。"; + } + + if (details?.payment === "payment_batch_v1_required") { + return "付款批次已启用,请从付款页面完成制单、复核和执行。"; + } + + if (details?.entityIds === "contains_active_payment_batch") { + return "所选报销已进入其他有效付款批次,请刷新候选列表。"; + } + + if (details?.receiptAttachment === "void_required") { + return "银行回单不能直接删除,请填写原因后作废。"; } if (details?.status === "invalid_pay_status") { @@ -149,14 +165,6 @@ export function mapApiErrorMessage( return "复核人与批次创建人不能是同一人。"; } - if (details?.review === "second_reviewer_conflict") { - return "二次复核人不能与首个复核人相同。"; - } - - if (details?.review === "high_risk_second_review_required") { - return "当前为高风险付款批次,完成二次复核后才能执行。"; - } - if (details?.review === "batch_review_required") { return "当前批次尚未完成复核,不能执行。"; } diff --git a/client/src/api/paymentBatches.ts b/client/src/api/paymentBatches.ts index e30ad303..234d5f86 100644 --- a/client/src/api/paymentBatches.ts +++ b/client/src/api/paymentBatches.ts @@ -39,7 +39,7 @@ export type PaymentBatch = { thresholdAmount: string; requiredReviewCount: number; completedReviewCount: number; - nextStep: "first_review" | "second_review" | "ready_for_execute"; + nextStep: "first_review" | "ready_for_execute"; makerCheckerRequired: boolean; reviewerPermission: string; }; @@ -57,9 +57,32 @@ export type PaymentBatch = { executedBy: string | null; cancelledAt: string | null; cancelledBy: string | null; + cancelledReason: string | null; items: PaymentBatchItem[]; }; +export type PaymentBatchCandidate = { + entityId: string; + entityType: "reimbursement"; + businessNo: string; + employee: { + id: string; + employeeNo: string; + name: string; + }; + amount: string; + currencyCode: "CNY"; + businessDate: string | null; + summary: string | null; +}; + +export type PaymentBatchCandidatesResponse = { + items: PaymentBatchCandidate[]; + page: number; + pageSize: number; + total: number; +}; + export type PaymentBatchReceiptAttachment = { id: string; originalName: string; @@ -89,6 +112,15 @@ export async function fetchPaymentBatchDetail(batchId: string) { return apiRequestData<{ item: PaymentBatch }>(`/api/payment-batches/${batchId}`); } +export async function fetchPaymentBatchCandidates(query: { q?: string; page?: number; pageSize?: number } = {}) { + const params = new URLSearchParams(); + if (query.q?.trim()) params.set("q", query.q.trim()); + if (query.page) params.set("page", String(query.page)); + if (query.pageSize) params.set("pageSize", String(query.pageSize)); + const suffix = params.toString() ? `?${params.toString()}` : ""; + return apiRequestData(`/api/payment-batches/candidates${suffix}`); +} + export async function createPaymentBatch(payload: { entityType: PaymentBatchEntityType; name: string; @@ -131,10 +163,10 @@ export async function executePaymentBatch(batchId: string) { }); } -export async function cancelPaymentBatch(batchId: string) { +export async function cancelPaymentBatch(batchId: string, reason: string) { return apiRequestData<{ item: PaymentBatch }>(`/api/payment-batches/${batchId}/cancel`, { method: "POST", - body: JSON.stringify({}), + body: JSON.stringify({ reason }), }); } @@ -170,9 +202,9 @@ export async function downloadPaymentBatchReceiptAttachment( return apiRequestBlob(buildAttachmentFileUrl(paymentBatchReceiptAttachmentFileUrl(batchId, attachmentId), reason)); } -export async function deletePaymentBatchReceiptAttachment(batchId: string, attachmentId: string) { - return apiRequestData<{ attachmentId: string; outcome: "deleted" }>( - `/api/payment-batches/${batchId}/receipt-attachments/${attachmentId}`, - { method: "DELETE" }, +export async function voidPaymentBatchReceiptAttachment(batchId: string, attachmentId: string, reason: string) { + return apiRequestData<{ attachmentId: string; outcome: "voided"; voidedAt: string }>( + `/api/payment-batches/${batchId}/receipt-attachments/${attachmentId}/void`, + { method: "POST", body: JSON.stringify({ reason }) }, ); } diff --git a/client/src/api/purchaseRequests.ts b/client/src/api/purchaseRequests.ts index 86a1ae75..1b08cf94 100644 --- a/client/src/api/purchaseRequests.ts +++ b/client/src/api/purchaseRequests.ts @@ -377,11 +377,12 @@ export async function uploadPurchaseAttachment(purchaseRequestId: string, file: }); } -export async function deletePurchaseAttachment(purchaseRequestId: string, attachmentId: string) { +export async function deletePurchaseAttachment(purchaseRequestId: string, attachmentId: string, reason: string) { return apiRequestData<{ outcome: "deleted"; attachmentId: string }>( `/api/purchase-requests/${purchaseRequestId}/attachments/${attachmentId}`, { method: "DELETE", + body: JSON.stringify({ reason }), }, ); } diff --git a/client/src/api/reimbursements.ts b/client/src/api/reimbursements.ts index f6da7ad6..83cce450 100644 --- a/client/src/api/reimbursements.ts +++ b/client/src/api/reimbursements.ts @@ -505,11 +505,12 @@ export async function uploadReimbursementAttachments(reimbursementId: string, fi ); } -export async function deleteReimbursementAttachment(reimbursementId: string, attachmentId: string) { +export async function deleteReimbursementAttachment(reimbursementId: string, attachmentId: string, reason: string) { return apiRequestData<{ attachmentId: string; outcome: string }>( `/api/reimbursements/${reimbursementId}/attachments/${attachmentId}`, { method: "DELETE", + body: JSON.stringify({ reason }), }, ); } diff --git a/client/src/auth/permissions.ts b/client/src/auth/permissions.ts index 5dcd80ab..91141eff 100644 --- a/client/src/auth/permissions.ts +++ b/client/src/auth/permissions.ts @@ -65,6 +65,7 @@ const ROLE_PERMISSIONS: Record = { "project.manage", "budget.view", "budget.manage", + "payment.batch.review", ], ceo: [ ...EMPLOYEE_BASE_PERMISSIONS, @@ -82,6 +83,7 @@ const ROLE_PERMISSIONS: Record = { "asset.view_all", "report.dashboard.view", "budget.view", + "payment.batch.review", ], finance: [ ...EMPLOYEE_BASE_PERMISSIONS, @@ -97,6 +99,8 @@ const ROLE_PERMISSIONS: Record = { "procurement.view_all", "asset.view_all", "attendance.view_all", + "employee.bank.read", + "payroll.manage", ], finance_mgr: [ ...EMPLOYEE_BASE_PERMISSIONS, @@ -127,6 +131,8 @@ const ROLE_PERMISSIONS: Record = { "attendance.view_all", "attendance.manage", "attendance.sync", + "payroll.manage", + "employee.bank.read", ], administration: [ "expense.proxy_submit", @@ -152,9 +158,25 @@ const ROLE_PERMISSIONS: Record = { "attendance.sync", "system.users", "system.users_sensitive", + "system.accounts.manage", + "system.roles.manage", + "employee.bank.manage", + "employee.identity.manage", + "employee.contract.manage", "expense.view_all", "report.dashboard.view", ], + system_admin: [ + ...EMPLOYEE_BASE_PERMISSIONS, + ...PROCUREMENT_SELF_SERVICE_PERMISSIONS, + ...ASSET_SELF_SERVICE_PERMISSIONS, + "system.accounts.manage", + "system.roles.manage", + "system.settings", + "integration.manage", + "notification.channel.manage", + "system.audit.view", + ], admin: ["*"], }; diff --git a/client/src/components/common/AttachmentDeleteConfirmDialog.tsx b/client/src/components/common/AttachmentDeleteConfirmDialog.tsx new file mode 100644 index 00000000..1a515620 --- /dev/null +++ b/client/src/components/common/AttachmentDeleteConfirmDialog.tsx @@ -0,0 +1,99 @@ +import { type ReactNode, useEffect, useState } from "react"; +import { Button } from "../ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; +import { Textarea } from "../ui/textarea"; + +type AttachmentDeleteConfirmDialogProps = { + open: boolean; + targetName?: string; + title?: string; + description?: ReactNode; + pending?: boolean; + onOpenChange: (open: boolean) => void; + onConfirm: (reason: string) => void | Promise; +}; + +export function AttachmentDeleteConfirmDialog({ + open, + targetName, + title = "确认删除附件?", + description, + pending = false, + onOpenChange, + onConfirm, +}: AttachmentDeleteConfirmDialogProps) { + const [reason, setReason] = useState(""); + const [error, setError] = useState(""); + + useEffect(() => { + if (!open) { + setReason(""); + setError(""); + } + }, [open]); + + const confirm = async () => { + const normalizedReason = reason.trim(); + if (normalizedReason.length < 2) { + setError("请填写至少 2 个字的删除理由"); + return; + } + await onConfirm(normalizedReason); + }; + + return ( + + + + {title} + + {description ?? ( + <> + {targetName ? `附件:${targetName}。` : "该附件"} + 删除后将从当前业务单据移除,且无法在 OA 内恢复。 + + )} + + +
+