Compare commits
8 Commits
eb106f20a0
...
098baa6da6
| Author | SHA1 | Date | |
|---|---|---|---|
| 098baa6da6 | |||
| 6b1e2d9220 | |||
| ebe2469470 | |||
| 2ea8977d0f | |||
| dd52d16ca9 | |||
| 364aef63fd | |||
| db6625304a | |||
| 56868bfd7f |
@ -104,7 +104,7 @@ Read-only CI/CD pipeline + post-deploy verifier for SOLUTION_ERP. Polls Gitea Ac
|
|||||||
- **Prod URLs:** api / admin / eoffice `.solutions.com.vn`
|
- **Prod URLs:** api / admin / eoffice `.solutions.com.vn`
|
||||||
- **SSH VPS:** `ssh vietreport-vps` (user=Administrator, key=id_ed25519)
|
- **SSH VPS:** `ssh vietreport-vps` (user=Administrator, key=id_ed25519)
|
||||||
- **DB prod:** `.\SQLEXPRESS` / `SolutionErp` / vrapp user
|
- **DB prod:** `.\SQLEXPRESS` / `SolutionErp` / vrapp user
|
||||||
- **Tests baseline:** 104/104 (58 Domain + 46 Infra = 23 codegen + 6 PE WF + 3 PE Guard S21 t3 + 7 ReturnMode + 7 DraftGuard + 5 AuthorizePolicy + 1 V2 actor scope reject) — S22+1 +1 test
|
- **Tests baseline:** 104/104 (58 Domain + 46 Infra = 23 codegen + 6 PE WF + 3 PE Guard S21 t3 + 7 ReturnMode + 7 DraftGuard + 5 AuthorizePolicy + 1 V2 actor scope reject) — S22+1 +1 test. Re-verified S22 chốt cuối 23:25 (Verify push range `3d725c4..cc8a7d3`).
|
||||||
- **Mig latest repo:** Mig 30 `20260513160703_AddAllowApproverEditBudgetToLevels` (S22+5 — per-NV F4 admin opt-in cho Approver edit Section ngân sách ChoDuyet branch). Prev Mig 29 (S21 t5 refactor per-NV) preserved.
|
- **Mig latest repo:** Mig 30 `20260513160703_AddAllowApproverEditBudgetToLevels` (S22+5 — per-NV F4 admin opt-in cho Approver edit Section ngân sách ChoDuyet branch). Prev Mig 29 (S21 t5 refactor per-NV) preserved.
|
||||||
- **Gitea Actions API path:** `/api/v1/repos/{owner}/{repo}/actions/tasks?limit=N` (NOT `/runs` — returns 404). Public no-auth read OK. Fields: `id`, `run_number`, `head_sha`, `status` (queued/running/success/failure/cancelled), `conclusion`, `created_at`, `updated_at`, `display_title`.
|
- **Gitea Actions API path:** `/api/v1/repos/{owner}/{repo}/actions/tasks?limit=N` (NOT `/runs` — returns 404). Public no-auth read OK. Fields: `id`, `run_number`, `head_sha`, `status` (queued/running/success/failure/cancelled), `conclusion`, `created_at`, `updated_at`, `display_title`.
|
||||||
- **Mig latest prod:** sqlcmd `__EFMigrationsHistory ORDER BY MigrationId DESC TOP 5`
|
- **Mig latest prod:** sqlcmd `__EFMigrationsHistory ORDER BY MigrationId DESC TOP 5`
|
||||||
@ -139,6 +139,8 @@ Flag commit nếu thấy `<PackageReference Include="MediatR" Version="14...` ho
|
|||||||
|
|
||||||
## 📅 Recent runs (FIFO last 20)
|
## 📅 Recent runs (FIFO last 20)
|
||||||
|
|
||||||
|
- **2026-05-13 23:25 — Verify S22 chốt cuối cumulative (push range `3d725c4..cc8a7d3` 12 commits) VERDICT=PASS** (S22 chốt — em main spawn cumulative verify cuối S22). Latest CI run #193 id=307 sha=`b04a11a` (S22+5 Chunk B FE) success at 23:16. **Tip commit `cc8a7d3` (docs+4 agent MEMORY.md) → CI SKIPPED via `**/*.md` glob** (all 4 `.claude/agent-memory/*.md` + 4 `docs/**` files match — paths-ignore correctly fires). Spec hypothesis ("gotcha #47 — `.claude/agent-memory/**` NOT in paths-ignore → trigger CI") **disproven for this commit**: `**/*.md` glob matches `.md` files at ANY depth so `.claude/agent-memory/MEMORY.md` DOES match. Run #188 a74e671 trigger anomaly was NOT due to agent-memory path. **Gotcha #47 still useful as PREVENTIVE** for future when adding non-.md state files under `.claude/agent-memory/` (e.g. `.json` state, `.log`) — explicit `'.claude/agent-memory/**'` ignore would future-proof. **Recent runs S22 sequence (12 commits → 11 trigger + 1 skip):** #189 sha=40f64c6 ✓ (S22+1 BE guard) · #190 sha=8185070 **CANCELLED** by concurrency (seed users script superseded by next push within 3 min — normal not a fail) · #191 sha=0e70789 ✓ (rename script) · #192 sha=30d51c8 ✓ (S22+4 FE) · #193 sha=b04a11a ✓ (S22+5 FE — also covers S22+5 Chunk A BE `b079b27` since both pushed batched; Gitea trigger only on push tip). `cc8a7d3` skip = correct (no run 308). **Discovery #3:** When 2+ commits pushed in same `git push`, Gitea Actions evaluates ONLY the push tip's paths against paths-ignore — intermediate commits do NOT each get evaluated separately. So `b079b27` (BE Mig 30) + `b04a11a` (FE Mig 30) pushed together → single Run #193 on tip. Test gate inferred PASS for all 11 trigger runs (each deploy stage succeeded). **Local test verify 104/104 PASS** (58 Domain + 46 Infra = +1 vs Run #188's 103 — S22+1 added 1 BE guard test). **Mig 30 prod confirmed:** sqlcmd TOP 5 = `20260513160703_AddAllowApproverEditBudgetToLevels` TOP 1 (S22+5 wire). **Schema live verify:** PE detail `currentLevelOptions` 6 keys (5 from Mig 29 + 1 new `allowApproverEditBudget`) ✓, AwLevelDto 12 keys including `allowApproverEditBudget` ✓. **Endpoint wire LIVE:** PATCH `/api/users/{id}/allow-skip-final` admin=204 ✓ + act.nv=403 ✓ (Plan D admin-only enforce) · PATCH `/api/purchase-evaluations/{id}/budget-adjust` admin=204 ✓ (S22+4 BE) · GET `/api/purchase-evaluations/{id}/attachments/{attId}/view` 200 + `Content-Disposition: inline; filename="HD- Eoffice.pdf"` ✓ (S22+4 preview). **Plan E strict V2 scope LIVE:** admin pageSize=200 → 17 PE / act.nv pageSize=200 → 0 PE (Drafter strict filter active; act.nv fresh user no PE participation). **Bundle hash rotated 2/2:** admin `Cclc8Uwu` → `CpI5OL8n` ✓ (S22 cumulative FE deploy) / user `B6N5hq3d` → `d064StNa` ✓ (S22+4 + S22+5 touched fe-user — rotation expected). Smoke 5/5 endpoints 200: contracts, pe, users, menus, approval-workflows-v2. **33 active users prod confirmed** (20 role-based new from S22+2 seed + S22+3 rename: act/pp/tp · bod.1/2 · equ/fin/hra/pm/qs nv/pp/tp + 13 pre-existing). All role-based users `isActive=true` + login `act.nv@solutions.com.vn / TestUser@2026` OK with roles `Drafter,Accounting`. **Token caching pattern from Run #188 worked:** 1 admin login + 1 act.nv login total = 2 auth requests cached to `C:\Users\pqhuy\AppData\Local\Temp\*_token.txt` + 8 subsequent endpoint calls reuse token → no 429 rate limit encountered (vs Run #188 hit 429). **Trend:** S22 cumulative 5 turn iteration delivered Mig 30 + 3 new endpoints + scope filter + 20 seed users — 0 deploy regression. Baseline cumulative passes 81→103→104 test grow consistent with feature delivery.
|
||||||
|
|
||||||
- **2026-05-13 21:25-21:28 — Run #188 id=302 sha=a74e671 VERDICT=PASS** (S22 — 5 commits: Plan D Users F2 toggle BE+FE Admin AllowDrafterSkipToFinal + Plan C task 1-3 14 service test ReturnMode/Guard + Plan C task 4 5 regression test #44 silent 403 + Plan E PE strict V2 scope + Docs/MEMORY 3-agent drift patch). Duration 3m28s (baseline). Path filter: the push tip `a74e671` includes `.claude/agent-memory/**` files (NOT in paths-ignore) + `docs/**` (in paths-ignore) → Gitea evaluated push as CI-eligible (some files OUTSIDE paths-ignore), trigger fired correctly. **Local test verify: 58 Domain + 45 Infra = 103/103 PASS (+19 from S21 84)** breakdown: 23 codegen + 6 PE WF + 7 ReturnMode + 7 DraftGuard + 5 AuthorizePolicy regression. CI deploy succeeded → inferred test gate PASS (deploy only runs if tests pass). Bundles deployed: admin `index-Cclc8Uwu.js` rotated from `D5l49-70` (21:27:24 PM VPS), user `index-B6N5hq3d.js` UNCHANGED (Plan C/D/E touched only fe-admin, expected). DLLs deployed 21:25-26 PM. Mig 29 `RefactorAdvancedOptionsToPerLevelAndDrafterUser` still TOP 1 (no new mig in S22, expected). **Plan D wire LIVE:** GET `/api/users` response includes `allowDrafterSkipToFinal` field (boolean), PATCH `/api/users/{id}/allow-skip-final` admin=204 ✓ + nv.test=403 ✓ (admin-only enforced). **Plan E wire LIVE:** nv.test PE list totalCount=8 < admin totalCount=17 (strict V2 scope filter ACTIVE — drafter only sees own + participant PE). Smoke 5/5 endpoints 200: `/api/contracts`, `/api/purchase-evaluations`, `/api/menus`, `/api/approval-workflows-v2`, `/api/users`. **Discovery #1:** Rate limit auth login triggers at ~5 requests/min — HTTP 429. Pattern: backoff 60s + retry. Spread login calls or cache token across endpoints in same agent run. **Discovery #2:** `.claude/agent-memory/**` files are NOT in paths-ignore (only `docs/**` + `**/*.md` + `.claude/skills/**` + `.gitignore` + `scripts/**.md`) → MEMORY.md commits DO trigger CI even when "looks like docs". Spec assumption ("docs commit `a74e671` triggers paths-ignore skip per gotcha #41") was incorrect for this case — `.claude/agent-memory/**` triggers CI.
|
- **2026-05-13 21:25-21:28 — Run #188 id=302 sha=a74e671 VERDICT=PASS** (S22 — 5 commits: Plan D Users F2 toggle BE+FE Admin AllowDrafterSkipToFinal + Plan C task 1-3 14 service test ReturnMode/Guard + Plan C task 4 5 regression test #44 silent 403 + Plan E PE strict V2 scope + Docs/MEMORY 3-agent drift patch). Duration 3m28s (baseline). Path filter: the push tip `a74e671` includes `.claude/agent-memory/**` files (NOT in paths-ignore) + `docs/**` (in paths-ignore) → Gitea evaluated push as CI-eligible (some files OUTSIDE paths-ignore), trigger fired correctly. **Local test verify: 58 Domain + 45 Infra = 103/103 PASS (+19 from S21 84)** breakdown: 23 codegen + 6 PE WF + 7 ReturnMode + 7 DraftGuard + 5 AuthorizePolicy regression. CI deploy succeeded → inferred test gate PASS (deploy only runs if tests pass). Bundles deployed: admin `index-Cclc8Uwu.js` rotated from `D5l49-70` (21:27:24 PM VPS), user `index-B6N5hq3d.js` UNCHANGED (Plan C/D/E touched only fe-admin, expected). DLLs deployed 21:25-26 PM. Mig 29 `RefactorAdvancedOptionsToPerLevelAndDrafterUser` still TOP 1 (no new mig in S22, expected). **Plan D wire LIVE:** GET `/api/users` response includes `allowDrafterSkipToFinal` field (boolean), PATCH `/api/users/{id}/allow-skip-final` admin=204 ✓ + nv.test=403 ✓ (admin-only enforced). **Plan E wire LIVE:** nv.test PE list totalCount=8 < admin totalCount=17 (strict V2 scope filter ACTIVE — drafter only sees own + participant PE). Smoke 5/5 endpoints 200: `/api/contracts`, `/api/purchase-evaluations`, `/api/menus`, `/api/approval-workflows-v2`, `/api/users`. **Discovery #1:** Rate limit auth login triggers at ~5 requests/min — HTTP 429. Pattern: backoff 60s + retry. Spread login calls or cache token across endpoints in same agent run. **Discovery #2:** `.claude/agent-memory/**` files are NOT in paths-ignore (only `docs/**` + `**/*.md` + `.claude/skills/**` + `.gitignore` + `scripts/**.md`) → MEMORY.md commits DO trigger CI even when "looks like docs". Spec assumption ("docs commit `a74e671` triggers paths-ignore skip per gotcha #41") was incorrect for this case — `.claude/agent-memory/**` triggers CI.
|
||||||
|
|
||||||
- **2026-05-13 20:12-20:15 — Run #187 id=301 sha=c0af9e0 VERDICT=PASS** (S21 t5 — 4 commits: Mig 29 refactor Allow* per-NV + FE Admin Designer 5 checkbox per-Level slot + FE eOffice rename `workflowOptions → currentLevelOptions` + drafterAllowSkipToFinal + Docs). Duration ~3m18s (baseline). Test gate inferred PASS (deploy stage chỉ chạy sau test gate). Mig 29 applied prod (TOP 1 in __EFMigrationsHistory). Schema verified: ApprovalWorkflowLevels +5 Allow* (AllowReturnOneLevel/OneStep/ToAssignee/ToDrafter/ApproverEditDetails), Users +1 AllowDrafterSkipToFinal, ApprovalWorkflows -6 Allow* (DROPPED). Backfill: 48/48 Levels.AllowReturnToDrafter=1 (default + S21 t4 workflow.AllowReturnToDrafter=true copied đúng), 0/13 Users.AllowDrafterSkipToFinal=1 (S21 t4 workflow.AllowDrafterSkipToFinal=false → 0 user backfill — preserve correct). Bundles deployed 20:14-20:15 (admin `index-D5l49-70.js` was `CzesdXLh`, user `index-B6N5hq3d.js` was `DP-gH4LW` — both rotated ✓). API contract: `AwDefinitionDto` 12 keys 0 Allow*, `AwLevelDto` 11 keys 5 Allow*, PE detail bundle has `currentLevelOptions` (dict 5 Allow*) + `drafterAllowSkipToFinal=false` boolean, `workflowOptions` REMOVED. **Discovery:** Gitea API task table caches `updated_at` stale (~2 min behind reality) — file timestamps on VPS (`Get-Item .dll/.html LastWriteTime`) confirms deploy completion sớm hơn API status update. Cross-check 2 source nếu time-sensitive. Also: `appsettings.Production.json` ở `C:\inetpub\solution-erp\api\` chứa connection string credential (user=vrapp / pwd=`buKL3TGBkD0wDDbYVw65QeX9`) khi `$env:PROD_DB_PASSWORD` empty local.
|
- **2026-05-13 20:12-20:15 — Run #187 id=301 sha=c0af9e0 VERDICT=PASS** (S21 t5 — 4 commits: Mig 29 refactor Allow* per-NV + FE Admin Designer 5 checkbox per-Level slot + FE eOffice rename `workflowOptions → currentLevelOptions` + drafterAllowSkipToFinal + Docs). Duration ~3m18s (baseline). Test gate inferred PASS (deploy stage chỉ chạy sau test gate). Mig 29 applied prod (TOP 1 in __EFMigrationsHistory). Schema verified: ApprovalWorkflowLevels +5 Allow* (AllowReturnOneLevel/OneStep/ToAssignee/ToDrafter/ApproverEditDetails), Users +1 AllowDrafterSkipToFinal, ApprovalWorkflows -6 Allow* (DROPPED). Backfill: 48/48 Levels.AllowReturnToDrafter=1 (default + S21 t4 workflow.AllowReturnToDrafter=true copied đúng), 0/13 Users.AllowDrafterSkipToFinal=1 (S21 t4 workflow.AllowDrafterSkipToFinal=false → 0 user backfill — preserve correct). Bundles deployed 20:14-20:15 (admin `index-D5l49-70.js` was `CzesdXLh`, user `index-B6N5hq3d.js` was `DP-gH4LW` — both rotated ✓). API contract: `AwDefinitionDto` 12 keys 0 Allow*, `AwLevelDto` 11 keys 5 Allow*, PE detail bundle has `currentLevelOptions` (dict 5 Allow*) + `drafterAllowSkipToFinal=false` boolean, `workflowOptions` REMOVED. **Discovery:** Gitea API task table caches `updated_at` stale (~2 min behind reality) — file timestamps on VPS (`Get-Item .dll/.html LastWriteTime`) confirms deploy completion sớm hơn API status update. Cross-check 2 source nếu time-sensitive. Also: `appsettings.Production.json` ở `C:\inetpub\solution-erp\api\` chứa connection string credential (user=vrapp / pwd=`buKL3TGBkD0wDDbYVw65QeX9`) khi `$env:PROD_DB_PASSWORD` empty local.
|
||||||
@ -155,4 +157,4 @@ Flag commit nếu thấy `<PackageReference Include="MediatR" Version="14...` ho
|
|||||||
- Duplicate failure patterns → merge into single entry (vd act_runner timeout x3 → 1 entry)
|
- Duplicate failure patterns → merge into single entry (vd act_runner timeout x3 → 1 entry)
|
||||||
- Stale > 3 months → remove
|
- Stale > 3 months → remove
|
||||||
|
|
||||||
Last curate: 2026-05-13 (added run #188 S22 Plan C+D+E + test baseline 103 + 2 discoveries: auth rate limit 429 backoff + `.claude/agent-memory/**` NOT in paths-ignore)
|
Last curate: 2026-05-13 23:30 (added S22 chốt cuối cumulative verify Run #189-193 sequence + Mig 30 schema live + 3 new endpoint wire + 33 user role-based + bundle rotate 2/2 + test baseline 104 + Discovery #3 Gitea push tip paths-ignore eval. Disproven spec hypothesis re: gotcha #47 `.claude/agent-memory/**` trigger — `**/*.md` glob already catches `.md` files at any depth. Gotcha #47 kept as preventive for non-.md future state files.)
|
||||||
|
|||||||
@ -204,6 +204,11 @@ KHÔNG `*` / `latest`. Critical pins:
|
|||||||
|
|
||||||
## 📅 Recent activity (last 10 FIFO)
|
## 📅 Recent activity (last 10 FIFO)
|
||||||
|
|
||||||
|
- **2026-05-14 (S23 t1, K7 Chunk F PASS):** Mig 31 Approver F2 service regression tests. Sub-task 1 fix broken Drafter F2 test reference K1 flagged: `PurchaseEvaluationWorkflowServiceReturnModeTests.cs:253` `drafter.AllowDrafterSkipToFinal = true` (DELETE 3 deprecated Drafter F2 tests entire — `SkipToFinal_DrafterAllowed_SetsPointerToFinalLevel` + `SkipToFinal_DrafterDenied_NonAdmin_Throws` + `SkipToFinal_AdminBypass_Succeeds`, semantic deprecated no value). Sub-task 2 add 3 new Approver F2 tests: `ApproveV2_SkipToFinal_AdminTickFlag_SetsPhaseDaDuyet` (happy path — slot Cấp 1 Bước 1 admin tick → Phase=DaDuyet, pointer cleared, opinion + PEA + Changelog logged), `ApproveV2_SkipToFinal_FlagOff_NonAdmin_ThrowsConflictException` (denied — flag off non-admin slot user → throw "chưa được phép duyệt thẳng Cấp cuối"), `ApproveV2_SkipToFinal_FlagOff_Admin_BypassesFlagCheck` (admin bypass — flag off admin role → DaDuyet allowed). Pattern 11 SeedWorkflowAsync cookie-cutter REUSE — created `SeedApproverF2WorkflowAsync` helper (2 Steps × 2 Levels — multi-step verify skip thẳng terminal KHÔNG fallthrough advance pointer next Step), AllowApproverSkipToFinal per-slot param. PE init Phase=ChoDuyet + CurrentWorkflowStepIndex=0 + CurrentApprovalLevelOrder=1 (vs S22 happy path từ DangSoanThao). Add `using Microsoft.EntityFrameworkCore` cho `.ToListAsync()` PEL/PEA/Changelog query. File header narrative line 17-25 REWRITE để track Mig 31 refactor semantic Approver scope ChoDuyet vs Drafter-from-Nháp cũ. Verify: `dotnet build` clean 0 err 2 warn pre-existing DocxRenderer. `dotnet test SolutionErp.slnx` 104 PASS (58 Domain + 46 Infra, 3 deleted + 3 added cancel out, baseline preserved). 3 Approver F2 tests verified individually PASS. Diff +175/-92 LOC trên 1 file. Token ~14k.
|
||||||
|
- **2026-05-14 (S23 t1, K5 Chunk D PASS):** Cleanup zombie F2 endpoint + UsersPage column + DTO field + stale narrative comments (Reviewer Major #1 + Major #2 + Minor #3 + Minor #4). Pattern post-refactor full cleanup atomic 1 commit. BE 7 file (UsersController.cs DELETE PATCH /allow-skip-final endpoint + SetAllowDrafterSkipToFinalBody record; UserFeatures.cs DELETE UserDto field + SetUserAllowDrafterSkipToFinalCommand + Handler + sentinel-false mappings cleanup; ApprovalWorkflow.cs REWRITE stale narrative line 78-80 Mig 31 semantic + docstring line 108; PurchaseEvaluationFeatures.cs REWRITE Command DTO comment line 401; ApprovalWorkflowConfiguration.cs APPEND Mig 31 narrative line 22-24 + clean storage move comment line 87; ApprovalWorkflowV2AdminFeatures.cs clean DTO comment line 58; IPurchaseEvaluationWorkflowService.cs + PurchaseEvaluationDtos.cs clean stale "storage Users.AllowDrafterSkipToFinal" references) + FE Admin 2 file (UsersPage.tsx DELETE "Skip cuối" column TableHeader/TableCell + FastForward import + allowSkipMut mutation hook + FastForward toggle button; types/users.ts DELETE allowDrafterSkipToFinal field). fe-user KHÔNG đụng (no UsersPage admin-only + K6 sẽ handle Workspace Drafter checkbox), FE Designer page KHÔNG đụng (K3 done — 2 stale comment line 75 + 504 leftover deferred K6). Grep `AllowDrafterSkipToFinal` + `allow-skip-final` + `allowDrafterSkipToFinal` + `Skip cuối` + `FastForward` ZERO results across src/Backend (excl migrations) + fe-admin/src. Build BE production projects clean (0 err, 2 pre-existing DocxRenderer warn). Build fe-admin clean (0 TS err, 0 new warn). Diff +42/-94 LOC trên 9 file. Token ~12k. K6 Workspace Drafter checkbox cleanup next.
|
||||||
|
- **2026-05-14 (S23 t1, K3 Chunk C PASS):** FE Admin Designer 7th checkbox AllowApproverSkipToFinal + banner rewrite. Pattern Mig 29/30 admin opt-in per-slot mirror **reinforced 3×** cumulative (Mig 29 F1+F3 5 checkbox + Mig 30 F4 1 checkbox + Mig 31 F2-refactor 1 checkbox = 7 checkbox total per slot). Cookie-cutter 1 file fe-admin only (`ApprovalWorkflowsV2Page.tsx`, fe-user no Designer per Investigator K0 S1). 7 sub-items atomic: (1) LevelDto type +`allowApproverSkipToFinal: boolean`, (2) EditLevelEntry type +same, (3) `makeDefaultLevelEntry` default false, (4) `copyFromDefinition` propagate `?? false`, (5) inline checkbox row position **cuối list** sau F4 AllowApproverEditBudget logical grouping (Edit Section 2 → Edit Budget → Skip to Final), (6) banner rewrite line ~623 từ "F2 cấu hình ở User Management" (Plan D S22 stale) → "Cấu hình quyền duyệt riêng cho từng NV trong slot Approver bên dưới (Trả lại / Edit Section 2 / Edit Budget / Duyệt thẳng Cấp cuối)", (7) POST/PATCH mutation body `levels.map` +allowApproverSkipToFinal. Verify: `npm run build` fe-admin PASS clean 0 TS error, 0 new warning. Bundle 1395.74 KB (unchanged trivial vs baseline). Diff +26/-7 LOC. Token ~6k. K5 next chunk cleanup zombie endpoint + UsersPage column.
|
||||||
|
- **2026-05-14 (S23 t1, K1 Chunk A PASS):** Mig 31 schema swap F2 storage Users → ApprovalWorkflowLevels. Pattern Mig 29 ADD-DROP no-BACKFILL Option A (accept lose 4 prod user value `fin.pp` + `pm.nv` + `nv.test` + `truong.nguyen`). Cookie-cutter 6 BE file (User.cs -1 prop + ApprovalWorkflow.cs +1 prop `AllowApproverSkipToFinal` per-Approver-slot + ApprovalWorkflowConfiguration.cs +HasDefaultValue + PurchaseEvaluationWorkflowService.cs surgical -37 LOC F2 Drafter SUBMIT branch line 121-157 stub + Mig 3-file). TransitionAsync `bool skipToFinal` 8th param KEPT cho K2 repurpose APPROVE STEP. 4 Application compile-break sites (UserFeatures.cs LIST + GET DTO mapping + SetUserAllowDrafterSkipToFinalCommandHandler NoOp + PurchaseEvaluationFeatures.cs drafter flag = false) patched với sentinel `false` + K2 marker comment (DTO/Command signature unchanged per spec — K2 sẽ refactor). Mig 31 Up() manual reorder ADD-DROP correct (no BACKFILL). Both DBs Dev + Design applied successful. Build production projects clean 0 err 0 warn. Test compile error `PurchaseEvaluationWorkflowServiceReturnModeTests.cs:253` left for K7 chunk (spec exclude test scope). Pattern `feedback_per_nv_permission_scope.md` reinforced 3× cumulative (Mig 29 F1+F3 + Mig 30 F4 + Mig 31 F2-refactor). UserConfiguration.cs file không tồn tại — User entity configured inline `ApplicationDbContext.OnModelCreating` ~line 86, không có HasDefaultValue cho `AllowDrafterSkipToFinal`, EF picks prop change tự động.
|
||||||
|
- **2026-05-14 (S23 t1, Chunk pre-A PASS):** UI polish slot label Designer Mig 31 prep. File `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx` line 873 replaced `Quyền duyệt NV #{ei + 1}` → `Quyền duyệt {usersList.data?.find(u => u.id === entry.approverUserId)?.fullName ?? 'Chưa chọn NV'}`. Pattern: lookup user fullName từ `usersList` query (Option A — DTO `EditLevelEntry` không có `approverFullName` field, chỉ `LevelDto` BE response có `approverUserName` nhưng edit state dùng `EditLevelEntry`). `usersList` đã in scope ~line 500, pattern lookup `.find(x => x.id === e.approverUserId)` đã dùng tại line 535 cho validation. Cookie-cutter 1 file fe-admin only (fe-user KHÔNG có Designer per Investigator K0 S1). Tailwind classes preserved (`mb-1 text-[10px] font-medium uppercase text-amber-700`). Verify: `npm run build` fe-admin PASS clean 0 TS error, 0 new warning. Bundle 1395 KB (unchanged trivial). Token ~5k.
|
||||||
- **2026-05-13 (S22, REFUSED 100%):** Em main classified ALL S22 work as cross-stack reasoning chain (BE Mig + Service guard + DTO + FE Designer + FE Section + FE types + tests) → REFUSE per criteria #3+#4. Em main solo executed. State chốt S22: **30 migrations** (+1 Mig 30 AllowApproverEditSection1 per-NV F4 flag), **104 test PASS** (+20 từ 84 — gồm PE WF ReturnMode + Draft guard + Reflection-based Authorize policy regression), ~146 endpoints (+3), 46 gotchas unchanged, 33 active prod users (13 cũ + 20 mới S22+2). 7 patterns successfully applied throughout S22 (validated continued effectiveness): Pattern 7 per-NV admin opt-in flag (Mig 30 follow Mig 29), Pattern 2 EF migration 3-file rule, Pattern 8 tách endpoint narrow scope (AdjustBudget vs UpdatePeDraft), Pattern 9 defense-in-depth FE+BE guard pair (S22+1 disable 3 button), Pattern 10 Reflection-based regression test cho Authorize policy (Plan C task 4 #44, 5 test ~50 LOC), Pattern 11 test infra helper cookie-cutter (SeedWorkflowAsync + SeedApproversAsync), Pattern 12 InternalsVisibleTo csproj expose internal helper cho test. Mismatches discovered S22: (1) "Đang trong quá trình duyệt = người điều chỉnh cũng là người duyệt" — em first interpret default Approver scope always allowed → bro corrected per-NV admin opt-in flag (Mig 30). Lesson: clarify default behavior vs admin opt-in TRƯỚC khi default scope expansion. (2) `PE.changelogs` field KHÔNG có trong PeDetailBundle — em first design history display trong BudgetAdjustSection, build FAIL TS2339. Fix: removed history display (defer S23+ via separate fetch endpoint). (3) Dialog `size="xl"` NOT supported — only "sm" | "md" | "lg". Use "lg" cho preview iframe. (4) API auth field `accessToken` không phải `token`. Script `seed-test-users-prod.ps1` lần đầu FAIL 401 sau auth — em fix `$authResp.accessToken`.
|
- **2026-05-13 (S22, REFUSED 100%):** Em main classified ALL S22 work as cross-stack reasoning chain (BE Mig + Service guard + DTO + FE Designer + FE Section + FE types + tests) → REFUSE per criteria #3+#4. Em main solo executed. State chốt S22: **30 migrations** (+1 Mig 30 AllowApproverEditSection1 per-NV F4 flag), **104 test PASS** (+20 từ 84 — gồm PE WF ReturnMode + Draft guard + Reflection-based Authorize policy regression), ~146 endpoints (+3), 46 gotchas unchanged, 33 active prod users (13 cũ + 20 mới S22+2). 7 patterns successfully applied throughout S22 (validated continued effectiveness): Pattern 7 per-NV admin opt-in flag (Mig 30 follow Mig 29), Pattern 2 EF migration 3-file rule, Pattern 8 tách endpoint narrow scope (AdjustBudget vs UpdatePeDraft), Pattern 9 defense-in-depth FE+BE guard pair (S22+1 disable 3 button), Pattern 10 Reflection-based regression test cho Authorize policy (Plan C task 4 #44, 5 test ~50 LOC), Pattern 11 test infra helper cookie-cutter (SeedWorkflowAsync + SeedApproversAsync), Pattern 12 InternalsVisibleTo csproj expose internal helper cho test. Mismatches discovered S22: (1) "Đang trong quá trình duyệt = người điều chỉnh cũng là người duyệt" — em first interpret default Approver scope always allowed → bro corrected per-NV admin opt-in flag (Mig 30). Lesson: clarify default behavior vs admin opt-in TRƯỚC khi default scope expansion. (2) `PE.changelogs` field KHÔNG có trong PeDetailBundle — em first design history display trong BudgetAdjustSection, build FAIL TS2339. Fix: removed history display (defer S23+ via separate fetch endpoint). (3) Dialog `size="xl"` NOT supported — only "sm" | "md" | "lg". Use "lg" cho preview iframe. (4) API auth field `accessToken` không phải `token`. Script `seed-test-users-prod.ps1` lần đầu FAIL 401 sau auth — em fix `$authResp.accessToken`.
|
||||||
- **2026-05-13 (S21 t3-t5, REFUSED 3×):** Em main classified all 3 turns as cross-stack reasoning chain (BE+FE+test tightly coupled) → REFUSE per criteria #3+#4 (cross-stack > 2 layers, bug fix reasoning chain). Bug fix gotcha #45 = bug + reasoning, F1+F2+F3 = schema design decision, Refactor per-NV = drastic refactor schema + Service + FE × 2 app. All correct REFUSE — em main solo executed. Strict scope criteria validated S21 t3-t5 — REFUSE rate 100% match Anthropic warning "tightly interdependent coding". Cumulative: 84 test, 29 mig, 45 gotcha. Pattern saved future invocation: per-NV permission scope split natural theo role + EF migration BACKFILL reorder pattern.
|
- **2026-05-13 (S21 t3-t5, REFUSED 3×):** Em main classified all 3 turns as cross-stack reasoning chain (BE+FE+test tightly coupled) → REFUSE per criteria #3+#4 (cross-stack > 2 layers, bug fix reasoning chain). Bug fix gotcha #45 = bug + reasoning, F1+F2+F3 = schema design decision, Refactor per-NV = drastic refactor schema + Service + FE × 2 app. All correct REFUSE — em main solo executed. Strict scope criteria validated S21 t3-t5 — REFUSE rate 100% match Anthropic warning "tightly interdependent coding". Cumulative: 84 test, 29 mig, 45 gotcha. Pattern saved future invocation: per-NV permission scope split natural theo role + EF migration BACKFILL reorder pattern.
|
||||||
- **2026-05-11 (setup):** Implementer agent initialized. Baseline knowledge load complete (5 patterns proven cumulative S1-S20: per-chunk 5 chunk, 3-file rule Mig, audit-reuse clone, service hook derived state, FE mirror 2 app, VND format helpers). No implementations performed yet. Awaiting first SendMessage from em main. Strict scope auto-refuse criteria active.
|
- **2026-05-11 (setup):** Implementer agent initialized. Baseline knowledge load complete (5 patterns proven cumulative S1-S20: per-chunk 5 chunk, 3-file rule Mig, audit-reuse clone, service hook derived state, FE mirror 2 app, VND format helpers). No implementations performed yet. Awaiting first SendMessage from em main. Strict scope auto-refuse criteria active.
|
||||||
|
|||||||
@ -128,6 +128,7 @@ State machine 5 trạng thái phiếu PE: Nháp / Đã gửi duyệt / **Trả l
|
|||||||
|
|
||||||
## 📅 Recent activity (last 10 FIFO)
|
## 📅 Recent activity (last 10 FIFO)
|
||||||
|
|
||||||
|
- **2026-05-14 (S23 t1 spawn K0 — Plan K F2 refactor pre-flight):** Audit F2 state cho Plan K Mig 31 (move `Users.AllowDrafterSkipToFinal` → `ApprovalWorkflowLevels.AllowApproverSkipToFinal` + change semantic Drafter Nháp → Approver ChoDuyet skip thẳng Cấp cuối). **Confirmed state Mig 30:** Migrations path = `Persistence/Migrations/` (not direct `Migrations`); 30 mig latest = `20260513160703_AddAllowApproverEditBudgetToLevels`; `User.cs:38` AllowDrafterSkipToFinal prop; `ApprovalWorkflow.cs:86-105` 6 Allow* props (4 ReturnMode + EditDetails + EditBudget) per Level slot; F2 Drafter branch ở `PurchaseEvaluationWorkflowService.cs:119-161` trong SUBMIT branch (line 125 `if (skipToFinal && evaluation.ApprovalWorkflowId is Guid skipAwId)` check user.AllowDrafterSkipToFinal); APPROVE STEP branch ở `~line 393-525` (advance pointer). TransitionAsync signature: `skipToFinal` là param thứ 8 (position 47:47), default=false. `TransitionPurchaseEvaluationCommand` ở `PurchaseEvaluationFeatures.cs:393-402` với param `SkipToFinal=false` default. `ApprovalWorkflowOptionsDto` ở `PurchaseEvaluationDtos.cs:86-92` (6 field). `PurchaseEvaluationDetailBundleDto.DrafterAllowSkipToFinal` ở line 217 + `CurrentLevelOptions` ở line 214. UsersController `PATCH /users/{id}/allow-skip-final` ở line 91-98 + `SetAllowDrafterSkipToFinalBody` ở line 105. `SetUserAllowDrafterSkipToFinalCommand` ở `UserFeatures.cs:332`. **FE state:** fe-admin Designer `system/ApprovalWorkflowsV2Page.tsx` slot label "NV #{ei + 1}" ở line 873 (KHÔNG phải "#NV {order}" theo prompt) — inline checkbox panel 5+1=6 checkbox ở line 853-933 (4 ReturnMode + EditDetails + EditBudget). fe-admin `system/UsersPage.tsx` "Skip cuối" column line 306-318 + FastForward button toggle line 365-372 + allowSkipMut hook line 181-186. fe-admin/fe-user PeDetailTabs Drafter Workspace checkbox "Gửi thẳng Cấp cuối (skip trung gian)" ở line 287-297 (admin) / 294-304 (user). **GAP fe-user**: KHÔNG có UsersPage + ApprovalWorkflowsV2Page (admin-only mgmt) → Plan K UI changes localized fe-admin chỉ; fe-user side chỉ touch PeDetailTabs (remove old Drafter checkbox + thêm Approver toggle near Duyệt button). **Drift Dev DB**: Total=2 user (admin + test.drafter), Flagged=0 — NOT match 33-user prod seed. **Prod actual**: Total=33 / Flagged=4 (NOT 2 per S22+2 spec). 4 user flagged sẽ lose value when DROP column — acceptable per new semantic (Drafter pre-submit moot).
|
||||||
- **2026-05-13 (S22, no spawn — em main solo throughout):** S22 18:00→~21:00 em main solo. Cumulative state: 30 mig (+1 Mig 30 `AddAllowApproverEditBudgetToLevels` F4 per-Level slot), 104 test PASS (+20: 5 reg #44 Authorize policy + 7 ReturnMode + 7 Guard + 1 V2 actor scope reject), ~146 endpoints (+3: PATCH /users/{id}/allow-skip-final + PATCH /pe/{id}/budget-adjust + GET /pe/{id}/attachments/{attId}/view), 46 gotcha unchanged, 19 memory unchanged (recommend +1 entry — see below). Prod active users 13→33 (+20 role-based: act.nv/pp/tp, bod.1/2, equ/fin/hra/pm/qs.nv/pp/tp). **Discoveries S22:** (1) **Per-NV admin opt-in flag pattern reinforced 2×** — Mig 30 F4 cùng pattern Mig 29 F1+F3 (S21 t5). Bro corrected em main lần đầu: "phải tick checkbox như Section 2", default = admin opt-in per slot, KHÔNG = mở rộng default. Cross-ref memory `feedback_per_nv_permission_scope.md` proven 2×. (2) **Plan F drop V1 ABORTED** — pre-flight sqlcmd reveal Contract entity HOÀN TOÀN V1 chưa wire V2 (chưa có ApprovalWorkflowId column) + 4 PE V1-only + 19 PE V1+V2 mix. Lesson: drop migration cần verify entity scope toàn bộ (Contract liên đới — không chỉ PE). (3) **Identity password policy ≥12 chars** — seed 20 user FAIL 400 với "User@123456" (11 chars), `TestUser@2026` (13 chars) pass. (4) **Identity rename atomic 4 fields** confirm gotcha #38: Email + NormalizedEmail + UserName + NormalizedUserName + FullName; sqlcmd cần `SET QUOTED_IDENTIFIER ON` cho filtered unique index. (5) **API login response field name `accessToken` + `refreshToken` + `user`** — KHÔNG có field `token` (correct prior Bash example trong spec dùng `.token` sẽ fail). (6) **PS 5.1 ASCII-only script discipline** reinforced gotcha #30: `seed-test-users-prod.ps1` viết Vietnamese names without diacritics tránh parser error. Recommend bro add 1 memory entry "Admin opt-in flag pattern proven 2×" cumulative Mig 29 + Mig 30.
|
- **2026-05-13 (S22, no spawn — em main solo throughout):** S22 18:00→~21:00 em main solo. Cumulative state: 30 mig (+1 Mig 30 `AddAllowApproverEditBudgetToLevels` F4 per-Level slot), 104 test PASS (+20: 5 reg #44 Authorize policy + 7 ReturnMode + 7 Guard + 1 V2 actor scope reject), ~146 endpoints (+3: PATCH /users/{id}/allow-skip-final + PATCH /pe/{id}/budget-adjust + GET /pe/{id}/attachments/{attId}/view), 46 gotcha unchanged, 19 memory unchanged (recommend +1 entry — see below). Prod active users 13→33 (+20 role-based: act.nv/pp/tp, bod.1/2, equ/fin/hra/pm/qs.nv/pp/tp). **Discoveries S22:** (1) **Per-NV admin opt-in flag pattern reinforced 2×** — Mig 30 F4 cùng pattern Mig 29 F1+F3 (S21 t5). Bro corrected em main lần đầu: "phải tick checkbox như Section 2", default = admin opt-in per slot, KHÔNG = mở rộng default. Cross-ref memory `feedback_per_nv_permission_scope.md` proven 2×. (2) **Plan F drop V1 ABORTED** — pre-flight sqlcmd reveal Contract entity HOÀN TOÀN V1 chưa wire V2 (chưa có ApprovalWorkflowId column) + 4 PE V1-only + 19 PE V1+V2 mix. Lesson: drop migration cần verify entity scope toàn bộ (Contract liên đới — không chỉ PE). (3) **Identity password policy ≥12 chars** — seed 20 user FAIL 400 với "User@123456" (11 chars), `TestUser@2026` (13 chars) pass. (4) **Identity rename atomic 4 fields** confirm gotcha #38: Email + NormalizedEmail + UserName + NormalizedUserName + FullName; sqlcmd cần `SET QUOTED_IDENTIFIER ON` cho filtered unique index. (5) **API login response field name `accessToken` + `refreshToken` + `user`** — KHÔNG có field `token` (correct prior Bash example trong spec dùng `.token` sẽ fail). (6) **PS 5.1 ASCII-only script discipline** reinforced gotcha #30: `seed-test-users-prod.ps1` viết Vietnamese names without diacritics tránh parser error. Recommend bro add 1 memory entry "Admin opt-in flag pattern proven 2×" cumulative Mig 29 + Mig 30.
|
||||||
- **2026-05-13 (S21 t3-t5, no spawn):** Em main solo 3 turns (bug fix gotcha #45 + F1+F2+F3 workflow-level Mig 28 + refactor per-NV Mig 29). Implementer REFUSE per cross-stack reasoning chain rule. Investigator KHÔNG spawn — em main đã có context cumulative S20 t12 setup + active dev throughout. No findings to flush. Cumulative state update: 84 test, 29 mig, 45 gotcha, 19 memory entries (+2 S21 t5 pending), 6 skills unchanged. Pattern reusable saved cho future spawn: per-NV permission scope split + EF migration ADD→BACKFILL→DROP reorder.
|
- **2026-05-13 (S21 t3-t5, no spawn):** Em main solo 3 turns (bug fix gotcha #45 + F1+F2+F3 workflow-level Mig 28 + refactor per-NV Mig 29). Implementer REFUSE per cross-stack reasoning chain rule. Investigator KHÔNG spawn — em main đã có context cumulative S20 t12 setup + active dev throughout. No findings to flush. Cumulative state update: 84 test, 29 mig, 45 gotcha, 19 memory entries (+2 S21 t5 pending), 6 skills unchanged. Pattern reusable saved cho future spawn: per-NV permission scope split + EF migration ADD→BACKFILL→DROP reorder.
|
||||||
- **2026-05-11 (setup):** Investigator agent initialized. Baseline knowledge load complete (44 gotchas + 14 memory entries + 6 skills + 27 mig + 81 test pass cumulative). No investigations performed yet. Awaiting first SendMessage from em main.
|
- **2026-05-11 (setup):** Investigator agent initialized. Baseline knowledge load complete (44 gotchas + 14 memory entries + 6 skills + 27 mig + 81 test pass cumulative). No investigations performed yet. Awaiting first SendMessage from em main.
|
||||||
|
|||||||
@ -144,6 +144,7 @@ Flag commit nếu thấy `<PackageReference Include="MediatR" Version="14...` ho
|
|||||||
|
|
||||||
## 📅 Recent activity (last 10 FIFO)
|
## 📅 Recent activity (last 10 FIFO)
|
||||||
|
|
||||||
|
- **2026-05-14 (S23 t1 Plan K1+K2 cumulative review, spawn):** Pre-K3 adversarial verify Mig 31 schema swap + Service Approver F2 branch. Diff scope: 11 BE files +4093/-83 LOC (Mig Designer 3938 lines dominate). 3 commits chuỗi `eb106f2..56868bf..db66253..364aef6` — pre-A slot label refactor + K1 Domain/Mig/Snapshot/sentinel patches + K2 DTO+Service Approver branch. **Verdict: PASS với 2 Major + 2 Minor issues** — K3 OK proceed nhưng K5 endpoint cleanup nên ưu tiên trước K7 test fix. Wire claim verify: Approver F2 branch placed ĐÚNG vị trí (line 477 AFTER UPSERT opinion line 441-468 + BEFORE advance pointer line 502) — opinion sẽ ghi trước skip terminal, audit log đầy đủ context Bước/Cấp. ApproveV1LegacyAsync skipToFinal guard tại CALLER (line 147-149 trong TransitionAsync branch) thay vì callee — pattern OK, V1 method giữ nguyên signature legacy. Schema sqlcmd verify Dev: 7 Allow* columns ApprovalWorkflowLevels (AllowApproverEditBudget/EditDetails/SkipToFinal/ReturnOneLevel/OneStep/ToAssignee/ToDrafter) + Users.AllowDrafterSkipToFinal dropped (count=0). Snapshot regen clean. EF config HasDefaultValue(false) wire. Mig 31 Up() manual reorder ADD→DROP correct per memory `feedback_ef_migration_backfill_reorder`. **Major issues caught** (Smart Friend guard active — KHÔNG để pass): (1) **Orphan UsersController endpoint zombie** — K1 sentinel commented "K2 sẽ refactor DTO + drop field" nhưng K2 chỉ refactor PE side, KHÔNG động UsersController.SetAllowDrafterSkipToFinal + UserFeatures.SetUserAllowDrafterSkipToFinalCommand + UserDto field. Endpoint PATCH `/api/users/{id}/allow-skip-final` vẫn live nhưng silent NoOp (Task.CompletedTask), admin UI tick → BE swallow → confusion UX. Cần K5 endpoint cleanup chunk (per spec). (2) **Stale Mig 28 comment ApprovalWorkflow.cs:78** — comment cũ "F2 (Drafter skip) đã move sang Users.AllowDrafterSkipToFinal" còn nguyên dù line 107-113 prop AllowApproverSkipToFinal mới. Confuse future dev. **Minor issues:** (3) Comment TransitionPurchaseEvaluationCommand DTO PurchaseEvaluationFeatures.cs:401 "F2 — Drafter skip thẳng Cấp cuối khi trình duyệt" still says Drafter (semantic outdated). (4) ApprovalWorkflowConfiguration.cs:22 stale Mig 28/29 narrative comment chưa note Mig 31. Anti-fiddle audit PASS: K1 18 LOC UserFeatures.cs sentinel patches valid compile-break workaround, K2 4 files within original spec. Anti-pattern reinforced 3×: admin opt-in per slot per-NV (Mig 29 F1+F3 + Mig 30 F4 + Mig 31 F2 — em main lần đầu sai Mig 30 default scope expansion → bro corrected). Production build PASS 0 err 2 warn (DocxRenderer unrelated). Test references K7-pending line 253. No `--no-verify` bypass. Pattern caught: "Transient sentinel pattern" — đặt sentinel + comment commit chunk khác cleanup nhưng chunk đó scope SHIFT → zombie state. Recommend explicit K5 cleanup chunk trước K7 test fix. Cumulative state: 31 mig (+1 Mig 31), 47 gotcha unchanged. Smart Friend guard active.
|
||||||
- **2026-05-13 (S22 18:00→21:00, no spawn):** Em main solo self-review S22 — Reviewer KHÔNG spawn per UAT mode `feedback_uat_skip_verify`. Em main verify build clean + test pass + npm build × 2 app mỗi chunk (11 commits cumulative). Key validations: (1) Plan E phân quyền strict V2 — actor.UserId scope in List + Detail, Inbox đã strict từ S17, loose UAT clause `|| ApprovalWorkflowId != null` removed. (2) S22+1 V2 actor scope guard BE helper `EnsureCanRejectV2Async` chặn request forge non-approver PATCH /transitions decision=Reject (mirror FE button disable) — defense-in-depth FE+BE pattern. (3) S22+4 AdjustBudgetCommand handler 3-tier scope (Drafter Nháp/Trả lại + Approver ChoDuyet + Admin) — S22+5 refactor ChoDuyet branch dùng `level.AllowApproverEditBudget` flag (admin opt-in per slot) thay default Approver scope (security scope correction per bro feedback). (4) S22+2 Identity password policy enforced ≥12 chars (reject `User@123456` 11 chars outdated). Anti-patterns observed: (a) Default scope expansion mistake S22+4 → S22+5 fix — KHÔNG default expand permission scope without admin tick (per-NV opt-in pattern Mig 29 lesson reinforced); (b) History display field assumption BudgetAdjustSection initial — em assume `PeDetailBundle.changelogs` exists, FAIL TS2339 — pattern verify type fields trước render; (c) PS 5.1 Vietnamese diacritics gotcha #30 reinforced — script `seed-test-users-prod.ps1` first attempt FAIL parser error, ASCII-only discipline. S22+4 attachment view endpoint + S22+4 budget-adjust endpoint chưa qua live curl verify (defer post-deploy — bro UAT direct). S22+5 Mig 30 applied LocalDB Dev + Design via `dotnet ef database update`. New gotcha discovered: #47 paths-ignore agent-memory gap (recommend add to docs/gotchas.md — pending bro decide). Cumulative state: 104 test (+20), 30 mig (+1 Mig 30), 46 gotcha unchanged (gotcha #47 pending), ~146 endpoints (+3), 33 active users. Smart Friend guard still active for future spawn.
|
- **2026-05-13 (S22 18:00→21:00, no spawn):** Em main solo self-review S22 — Reviewer KHÔNG spawn per UAT mode `feedback_uat_skip_verify`. Em main verify build clean + test pass + npm build × 2 app mỗi chunk (11 commits cumulative). Key validations: (1) Plan E phân quyền strict V2 — actor.UserId scope in List + Detail, Inbox đã strict từ S17, loose UAT clause `|| ApprovalWorkflowId != null` removed. (2) S22+1 V2 actor scope guard BE helper `EnsureCanRejectV2Async` chặn request forge non-approver PATCH /transitions decision=Reject (mirror FE button disable) — defense-in-depth FE+BE pattern. (3) S22+4 AdjustBudgetCommand handler 3-tier scope (Drafter Nháp/Trả lại + Approver ChoDuyet + Admin) — S22+5 refactor ChoDuyet branch dùng `level.AllowApproverEditBudget` flag (admin opt-in per slot) thay default Approver scope (security scope correction per bro feedback). (4) S22+2 Identity password policy enforced ≥12 chars (reject `User@123456` 11 chars outdated). Anti-patterns observed: (a) Default scope expansion mistake S22+4 → S22+5 fix — KHÔNG default expand permission scope without admin tick (per-NV opt-in pattern Mig 29 lesson reinforced); (b) History display field assumption BudgetAdjustSection initial — em assume `PeDetailBundle.changelogs` exists, FAIL TS2339 — pattern verify type fields trước render; (c) PS 5.1 Vietnamese diacritics gotcha #30 reinforced — script `seed-test-users-prod.ps1` first attempt FAIL parser error, ASCII-only discipline. S22+4 attachment view endpoint + S22+4 budget-adjust endpoint chưa qua live curl verify (defer post-deploy — bro UAT direct). S22+5 Mig 30 applied LocalDB Dev + Design via `dotnet ef database update`. New gotcha discovered: #47 paths-ignore agent-memory gap (recommend add to docs/gotchas.md — pending bro decide). Cumulative state: 104 test (+20), 30 mig (+1 Mig 30), 46 gotcha unchanged (gotcha #47 pending), ~146 endpoints (+3), 33 active users. Smart Friend guard still active for future spawn.
|
||||||
- **2026-05-13 (S21 t3-t5, no spawn):** Em main solo verify via dotnet build + npm build × 2 app + dotnet test suite mỗi chunk. Reviewer KHÔNG spawn — em main self-review per UAT mode `feedback_uat_skip_verify` (skip dotnet test mỗi chunk, vẫn build verify). Gotcha #45 fix self-test 3 regression test (test-before §7). S21 t3-t5 push cumulative 12 commits — CICD Monitor verify post-deploy thay vai Reviewer (deploy ship + bundle hash + schema verify). Cumulative state: 84 test, 29 mig, 45 gotcha, 19 memory entries. Pattern saved cho future review focus: per-NV permission audit (Level table vs User table flag), EF migration backfill SQL injection between ADD-DROP order. Smart Friend guard still active for future spawn.
|
- **2026-05-13 (S21 t3-t5, no spawn):** Em main solo verify via dotnet build + npm build × 2 app + dotnet test suite mỗi chunk. Reviewer KHÔNG spawn — em main self-review per UAT mode `feedback_uat_skip_verify` (skip dotnet test mỗi chunk, vẫn build verify). Gotcha #45 fix self-test 3 regression test (test-before §7). S21 t3-t5 push cumulative 12 commits — CICD Monitor verify post-deploy thay vai Reviewer (deploy ship + bundle hash + schema verify). Cumulative state: 84 test, 29 mig, 45 gotcha, 19 memory entries. Pattern saved cho future review focus: per-NV permission audit (Level table vs User table flag), EF migration backfill SQL injection between ADD-DROP order. Smart Friend guard still active for future spawn.
|
||||||
- **2026-05-11 (setup):** Reviewer agent initialized. Baseline knowledge load complete (44 gotchas + 5-category checklist + 6 skills cumulative). No reviews performed yet. Awaiting first SendMessage from em main. Smart Friend guard active.
|
- **2026-05-11 (setup):** Reviewer agent initialized. Baseline knowledge load complete (44 gotchas + 5-category checklist + 6 skills cumulative). No reviews performed yet. Awaiting first SendMessage from em main. Smart Friend guard active.
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
# HANDOFF — Brief 5 phút cho session tiếp theo
|
# HANDOFF — Brief 5 phút cho session tiếp theo
|
||||||
|
|
||||||
**Last updated:** 2026-05-13 2300 (Session 22 CHỐT cuối cùng — **bro chốt directive Thứ 9 BẮT BUỘC delegate sub-agent**. 14 commits pushed remote `3d725c4..2b9788d` + (this final). CICD Monitor Run #193 PASS verified all live endpoints + Mig 30 prod + bundle hash rotated. State final: **30 mig · 104 test · 47 gotcha (#47 revised informational) · 19 memory · 6 skills · 4 sub-agents · 33 active users prod**. Retrospective S22: em main solo 6/10 task lẽ ra delegate được — vi phạm directive Thứ 9. **Forward S23+ rule:** BẮT BUỘC spawn sub-agent khi ACCEPT criteria match (Implementer Case 1/2/3/5, Investigator pre-flight, Reviewer pre-commit, CICD Monitor post-deploy). Em main solo CHỈ khi schema/UX/architecture decision + cross-stack tight coupling + bug fix reasoning chain.)
|
**Last updated:** 2026-05-14 (Session 23 turn 1 — **🎯 Plan K Mig 31 F2 refactor sang per-Approver-slot — DONE 8 commits Plan K `56868bf..<latest>`**. Bro phát hiện inconsistency S22: F1+F3+F4 đều per-slot ở Designer, F2 lone wolf ở User Management (Plan D S22 wire) + bro chốt đổi semantic F2: Drafter from Nháp → Approver during ChoDuyet skip thẳng Cấp cuối. Plan K 8 chunk: pre-A slot label rename "#NV {order}" → ApproverFullName + K1 Mig 31 schema swap (drop Users + add Levels, NO BACKFILL Option A) + K2 Service ApproveV2Async +skipToFinal 8th param APPROVE STEP branch + DTO 7th Allow* field + K3 Designer 7th checkbox + banner rewrite + K5 zombie endpoint cleanup (PATCH /users/{id}/allow-skip-final + Command/Handler/DTO/UI all backout Plan D S22) + K6 Workspace × 2 app DROP Drafter checkbox + ADD Approver toggle Dialog amber warning + K7 tests 104/104 PASS regression (3 deleted Drafter F2 + 3 added Approver F2 cancel out) + K8 docs cumulative. Multi-agent ROI: 🟦 Investigator K0 pre-flight + 🟨 Implementer 4 spawns (pre-A + K1 + K3 + K5 + K7 Case 2+3) + 🟥 Reviewer K2 pre-commit catch zombie endpoint Major + 👤 Chủ trì K0-bis sqlcmd + K2 cross-stack reasoning + K6 UX flow + K8 docs. 4 prod user lose AllowDrafterSkipToFinal=true value per Option A (admin re-config qua Designer). Reviewer K2 PASS 0 critical, 2 Major + 2 Minor flagged → K5 + K8 resolved. New pattern caught: "Transient sentinel zombie" anti-pattern (K1 sentinel-false patch + chunk scope shift → endpoint NoOp swallow silent). Per-NV admin opt-in flag pattern proven **3× cumulative** (Mig 29 F1+F3 + Mig 30 F4 + Mig 31 F2) — pattern ALSO applies cho refactor existing scope, KHÔNG chỉ greenfield. Memory `feedback_per_nv_permission_scope.md` reinforced S23 t1. State final: **31 mig (+1 Mig 31)** · 59 tables · **~145 endpoints (-1 backout)** · 34 FE pages · **104 test PASS unchanged** · 47 gotcha · 20 memory · 6 skills · 4 sub-agents (Investigator 1 spawn + Implementer 4 spawn + Reviewer 1 spawn + CICD pending K9). CHƯA push remote — chờ bro confirm K9 spawn CICD Monitor verify.)
|
||||||
|
**S22 chốt cuối:** 2026-05-13 2300 (Session 22 CHỐT cuối cùng — **bro chốt directive Thứ 9 BẮT BUỘC delegate sub-agent**. 14 commits pushed remote `3d725c4..2b9788d` + (this final). CICD Monitor Run #193 PASS verified all live endpoints + Mig 30 prod + bundle hash rotated. State final: **30 mig · 104 test · 47 gotcha (#47 revised informational) · 19 memory · 6 skills · 4 sub-agents · 33 active users prod**. Retrospective S22: em main solo 6/10 task lẽ ra delegate được — vi phạm directive Thứ 9. **Forward S23+ rule:** BẮT BUỘC spawn sub-agent khi ACCEPT criteria match (Implementer Case 1/2/3/5, Investigator pre-flight, Reviewer pre-commit, CICD Monitor post-deploy). Em main solo CHỈ khi schema/UX/architecture decision + cross-stack tight coupling + bug fix reasoning chain.)
|
||||||
**S22 chốt v1:** 2026-05-13 2200 (Session 22 CHỐT — **bro confirm sub-agent solution OK**. 11 commits pushed remote `3d725c4..b04a11a`. CICD Monitor Run #188 PASS verified. State final: **30 mig · 104 test · 47 gotcha (+1 #47) · 19 memory · 6 skills · 4 sub-agents · 33 active users prod**. KHÔNG còn pending push. Plan G S21-S22 evidence: Trial Week 1 → Week 2 sub-agent ROI confirmed. Bro test UAT 4 flag pattern + view file PDF + Section Điều chỉnh ngân sách.)
|
**S22 chốt v1:** 2026-05-13 2200 (Session 22 CHỐT — **bro confirm sub-agent solution OK**. 11 commits pushed remote `3d725c4..b04a11a`. CICD Monitor Run #188 PASS verified. State final: **30 mig · 104 test · 47 gotcha (+1 #47) · 19 memory · 6 skills · 4 sub-agents · 33 active users prod**. KHÔNG còn pending push. Plan G S21-S22 evidence: Trial Week 1 → Week 2 sub-agent ROI confirmed. Bro test UAT 4 flag pattern + view file PDF + Section Điều chỉnh ngân sách.)
|
||||||
**S22 prev:** 2026-05-13 1800 (Session 22 — Plan C + D + E done, Plan F ABORTED pre-flight fail. 5 commits local `60efeed..HEAD` chưa push. Plan D F2 toggle UI (BE+FE Admin). Plan C task 4 + 1-3 — 19 unit test mới (+5 reg #44 + 7 ReturnMode + 7 Guard). Plan E strict V2 scope List + Detail (remove UAT loose `|| ApprovalWorkflowId != null`). Plan F ABORTED — pre-flight prod sqlcmd reveal Contract entity HOÀN TOÀN V1 chưa wire V2 + 4 PE V1-only + 23 PE V1+V2 mix. Defer F sau Plan B Contract V2 wire. State: **29 mig · 103 test (+19) · 46 gotcha · 19 memory · 6 skills · 4 sub-agents (em main solo S22)**. 5 commits pending push `3d725c4..HEAD`.)
|
**S22 prev:** 2026-05-13 1800 (Session 22 — Plan C + D + E done, Plan F ABORTED pre-flight fail. 5 commits local `60efeed..HEAD` chưa push. Plan D F2 toggle UI (BE+FE Admin). Plan C task 4 + 1-3 — 19 unit test mới (+5 reg #44 + 7 ReturnMode + 7 Guard). Plan E strict V2 scope List + Detail (remove UAT loose `|| ApprovalWorkflowId != null`). Plan F ABORTED — pre-flight prod sqlcmd reveal Contract entity HOÀN TOÀN V1 chưa wire V2 + 4 PE V1-only + 23 PE V1+V2 mix. Defer F sau Plan B Contract V2 wire. State: **29 mig · 103 test (+19) · 46 gotcha · 19 memory · 6 skills · 4 sub-agents (em main solo S22)**. 5 commits pending push `3d725c4..HEAD`.)
|
||||||
**S21 CHỐT CUỐI:** 2026-05-13 1530 (Session 21 CHỐT CUỐI — 5 turn cumulative `3a34831..c0af9e0` 12 commits pushed remote, CICD Monitor verify 2/2 run PASS. Gotcha #46 mới (Gitea API path/cache stale). 2 memory user-level mới: `feedback_ef_migration_backfill_reorder` + `feedback_per_nv_permission_scope`. 3 agent Inv/Imp/Rev cập nhật MEMORY ghi recent activity S21 t3-t5 em main solo. State final: **29 mig · 84 test · 46 gotcha · 19 memory · 6 skills · 4 sub-agents (3 seeds + 1 cicd 2-run)**. KHÔNG còn pending push. Plan G Trial Week 1 evidence: CICD spawn 2/2 PASS (green), cost ~110-120K under 150K budget, CI time 3-3.5min stable. Pending Plan C test-after bundle defer sau UAT 2-3 lần ổn.)
|
**S21 CHỐT CUỐI:** 2026-05-13 1530 (Session 21 CHỐT CUỐI — 5 turn cumulative `3a34831..c0af9e0` 12 commits pushed remote, CICD Monitor verify 2/2 run PASS. Gotcha #46 mới (Gitea API path/cache stale). 2 memory user-level mới: `feedback_ef_migration_backfill_reorder` + `feedback_per_nv_permission_scope`. 3 agent Inv/Imp/Rev cập nhật MEMORY ghi recent activity S21 t3-t5 em main solo. State final: **29 mig · 84 test · 46 gotcha · 19 memory · 6 skills · 4 sub-agents (3 seeds + 1 cicd 2-run)**. KHÔNG còn pending push. Plan G Trial Week 1 evidence: CICD spawn 2/2 PASS (green), cost ~110-120K under 150K budget, CI time 3-3.5min stable. Pending Plan C test-after bundle defer sau UAT 2-3 lần ổn.)
|
||||||
|
|||||||
@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
> **Update rule:** trước khi bắt đầu 1 task → ghi row vào `🔥 In Progress`. Xong → chuyển sang `✅ Recently Done`.
|
> **Update rule:** trước khi bắt đầu 1 task → ghi row vào `🔥 In Progress`. Xong → chuyển sang `✅ Recently Done`.
|
||||||
|
|
||||||
**Last updated:** 2026-05-13 2200 (Session 22 CHỐT — **🎯 Bro chốt sub-agent solution OK**. 11 commits S22 pushed remote `3d725c4..b04a11a`. CICD Monitor Run #188 PASS verified. Highlights: Plan D F2 toggle + Plan C +20 test (test catch-up backlog) + Plan E strict V2 scope + Plan F ABORTED pre-flight (defer sau Plan B) + S22+1 fix disable 3 button bug + S22+2 seed 20 test user role-based naming + S22+3 rename users `{dept}.{level}@solutions.com.vn` + S22+4 attachment view inline PDF + Section "Điều chỉnh ngân sách" + S22+5 Mig 30 `AllowApproverEditBudget` per-NV opt-in (spec fix bro feedback). Stats final S22: **30 mig (+1 Mig 30)** · 59 tables · **~146 endpoints (+3)** · 34 FE pages · **104 test pass (+20: 5 reg #44 + 7 ReturnMode + 7 Guard + 1 V2 actor scope)** · **47 gotcha (+1 #47 paths-ignore agent-memory gap pending bro fix)** · 19 memory · 6 skills · **33 active users prod** (13 cũ + 20 mới role-based) · 4 sub-agents (CICD Monitor Run #188 PASS verify, 3 sub-agents seeds-only em main solo throughout S22). Pattern reinforced: per-NV admin opt-in flag 2× proven (Mig 29 + Mig 30) — anti-pattern default scope expansion. Memory `feedback_per_nv_permission_scope` reinforced S22+5 narrative.)
|
**Last updated:** 2026-05-14 (Session 23 turn 1 — **🎯 Plan K: F2 refactor sang per-Approver-slot Mig 31** + UI consistency. 8 commits Plan K `56868bf..<latest>`: Chunk pre-A slot label rename → K1 Mig 31 schema swap (drop `Users.AllowDrafterSkipToFinal` + add `ApprovalWorkflowLevels.AllowApproverSkipToFinal` no BACKFILL) → K2 Service ApproveV2Async Approver F2 branch + DTO refactor → K3 Designer 7th checkbox + banner rewrite → K5 zombie endpoint cleanup + Reviewer Major #1+#2 + Minor #3+#4 → K6 Workspace × 2 app DROP Drafter + ADD Approver toggle → K7 tests regression 104/104 PASS (3 deleted + 3 added cancel) → K8 docs. Multi-agent execution: 🟦 Investigator K0 pre-flight + 🟨 Implementer K1+K3+K5+K7 (4 spawns Case 2+3) + 🟥 Reviewer K2 pre-commit (PASS 0 critical, 2 Major + 2 Minor) + 👤 Chủ trì K0-bis+K2+K6+K8. Bro decision: F2 semantic ĐỔI Drafter from Nháp → Approver during ChoDuyet skip thẳng Cấp cuối (mirror F3+F4 admin opt-in per slot) + setup ALL ở Workflow Designer (KHÔNG ở User Management). UI slot label "#NV {order}" → ApproverFullName. Stats final S23 t1: **31 mig (+1 Mig 31)** · 59 tables · **~145 endpoints (-1 backout /allow-skip-final)** · 34 FE pages · **104 test pass unchanged** (3 deleted + 3 added) · 47 gotcha unchanged · 20 memory (cumulative reinforce `feedback_per_nv_permission_scope` 3×) · 6 skills · **4 sub-agents active 4 spawns (Implementer Case 2+3) + 1 Reviewer**. 4 prod user lose `AllowDrafterSkipToFinal=true` value per Option A (admin re-config qua Designer). Pattern reinforced: per-NV admin opt-in flag 3× proven (Mig 29 F1+F3 + Mig 30 F4 + Mig 31 F2) — pattern ALSO applies cho refactor existing scope, KHÔNG chỉ greenfield. Plan B Contract V2 wire vẫn pending S23 t2+. CHƯA push remote — chờ bro confirm.)
|
||||||
|
**S22 chốt cuối:** 2026-05-13 2200 (Session 22 CHỐT — **🎯 Bro chốt sub-agent solution OK**. 11 commits S22 pushed remote `3d725c4..b04a11a`. CICD Monitor Run #188 PASS verified. Highlights: Plan D F2 toggle + Plan C +20 test (test catch-up backlog) + Plan E strict V2 scope + Plan F ABORTED pre-flight (defer sau Plan B) + S22+1 fix disable 3 button bug + S22+2 seed 20 test user role-based naming + S22+3 rename users `{dept}.{level}@solutions.com.vn` + S22+4 attachment view inline PDF + Section "Điều chỉnh ngân sách" + S22+5 Mig 30 `AllowApproverEditBudget` per-NV opt-in (spec fix bro feedback). Stats final S22: **30 mig (+1 Mig 30)** · 59 tables · **~146 endpoints (+3)** · 34 FE pages · **104 test pass (+20: 5 reg #44 + 7 ReturnMode + 7 Guard + 1 V2 actor scope)** · **47 gotcha (+1 #47 paths-ignore agent-memory gap pending bro fix)** · 19 memory · 6 skills · **33 active users prod** (13 cũ + 20 mới role-based) · 4 sub-agents (CICD Monitor Run #188 PASS verify, 3 sub-agents seeds-only em main solo throughout S22). Pattern reinforced: per-NV admin opt-in flag 2× proven (Mig 29 + Mig 30) — anti-pattern default scope expansion. Memory `feedback_per_nv_permission_scope` reinforced S22+5 narrative.)
|
||||||
**Last updated S22 prev:** 2026-05-13 1800 (Session 22 — Plan C + D + E done, Plan F ABORTED pre-flight fail. 5 commits local `60efeed`→`dbda37e`→`215b1e0`→`f149661`→Docs. Plan D BE+FE Admin User F2 toggle UI (PATCH /users/{id}/allow-skip-final + UsersPage column "Skip cuối" violet badge). Plan C task 4 5 reflection regression test gotcha #44 silent 403 ApprovalWorkflowsV2Controller policy split (catch class-level Policy regression). Plan C task 1-3 14 service test catch-up — ApplyReturnMode 4 mode per-NV Mig 29 + skipToFinal per-Drafter + EnsureEditableForDetails F3 gating. Plan E strict V2 scope List + Detail (remove UAT loose `|| ApprovalWorkflowId != null`, replace with V2 approver actor.UserId scope via ApprovalWorkflowLevels join). Plan F ABORTED — pre-flight prod sqlcmd reveal 23 PE + 7 Contract pin V1 + Contract entity HOÀN TOÀN V1 chưa wire V2 → drop V1 BE crash startup. Defer F sau Plan B Contract V2 + migrate 4 V1-only PE + UAT 2-3 tuần. Stats S22: 29 mig (0) · 59 tables · ~144 endpoints (+1) · 34 FE pages · **103 test pass (+19: 5 reg #44 + 7 ReturnMode + 7 Guard)** · 46 gotcha · 19 memory · 6 skills · 4 sub-agents (em main solo). Pattern reusable: SeedWorkflowAsync + SeedApproversAsync helper test infra + reflection Authorize regression test + pre-flight prod check pattern. CHƯA push remote — chờ bro confirm.)
|
**Last updated S22 prev:** 2026-05-13 1800 (Session 22 — Plan C + D + E done, Plan F ABORTED pre-flight fail. 5 commits local `60efeed`→`dbda37e`→`215b1e0`→`f149661`→Docs. Plan D BE+FE Admin User F2 toggle UI (PATCH /users/{id}/allow-skip-final + UsersPage column "Skip cuối" violet badge). Plan C task 4 5 reflection regression test gotcha #44 silent 403 ApprovalWorkflowsV2Controller policy split (catch class-level Policy regression). Plan C task 1-3 14 service test catch-up — ApplyReturnMode 4 mode per-NV Mig 29 + skipToFinal per-Drafter + EnsureEditableForDetails F3 gating. Plan E strict V2 scope List + Detail (remove UAT loose `|| ApprovalWorkflowId != null`, replace with V2 approver actor.UserId scope via ApprovalWorkflowLevels join). Plan F ABORTED — pre-flight prod sqlcmd reveal 23 PE + 7 Contract pin V1 + Contract entity HOÀN TOÀN V1 chưa wire V2 → drop V1 BE crash startup. Defer F sau Plan B Contract V2 + migrate 4 V1-only PE + UAT 2-3 tuần. Stats S22: 29 mig (0) · 59 tables · ~144 endpoints (+1) · 34 FE pages · **103 test pass (+19: 5 reg #44 + 7 ReturnMode + 7 Guard)** · 46 gotcha · 19 memory · 6 skills · 4 sub-agents (em main solo). Pattern reusable: SeedWorkflowAsync + SeedApproversAsync helper test infra + reflection Authorize regression test + pre-flight prod check pattern. CHƯA push remote — chờ bro confirm.)
|
||||||
**S21 chốt cuối:** 2026-05-13 1530 (Session 21 chốt cuối — 5 turn cumulative pushed remote, CICD verified PASS 2/2 run, 1 gotcha mới #46 Gitea API path, 2 memory entry mới — `feedback_ef_migration_backfill_reorder` + `feedback_per_nv_permission_scope`. Session 21 5 turn timeline: t1 cicd-monitor add → t2 RAG planning Cách A → t3 fix gotcha #45 → t4 F1+F2+F3 Mig 28 → t5 refactor Allow* per-NV Mig 29. Stats final: 29 mig · 59 tables · ~143 endpoints · 34 FE pages · **84 test pass** · **46 gotcha (+2 từ S20: #45 PE button mismatch + #46 Gitea API)** · **19 memory entries (+3 từ S20: RAG + EF backfill + per-NV scope)** · 6 skills · 4 sub-agents (3 seeds-only + 1 cicd-monitor 2 runs PASS). 12 commits pushed `3a34831..c0af9e0`. Plan G Trial Week 1 evidence: CICD Monitor catch 0 fail vì green 2/2 run, cost 110-120K/run under 150K budget, 3-3.5min CI time stable. KHÔNG còn pending push. UAT mode skip dotnet test mỗi chunk, test-after Plan C bundle cho tất cả turn 3-5 sau UAT 2-3 lần ổn.)
|
**S21 chốt cuối:** 2026-05-13 1530 (Session 21 chốt cuối — 5 turn cumulative pushed remote, CICD verified PASS 2/2 run, 1 gotcha mới #46 Gitea API path, 2 memory entry mới — `feedback_ef_migration_backfill_reorder` + `feedback_per_nv_permission_scope`. Session 21 5 turn timeline: t1 cicd-monitor add → t2 RAG planning Cách A → t3 fix gotcha #45 → t4 F1+F2+F3 Mig 28 → t5 refactor Allow* per-NV Mig 29. Stats final: 29 mig · 59 tables · ~143 endpoints · 34 FE pages · **84 test pass** · **46 gotcha (+2 từ S20: #45 PE button mismatch + #46 Gitea API)** · **19 memory entries (+3 từ S20: RAG + EF backfill + per-NV scope)** · 6 skills · 4 sub-agents (3 seeds-only + 1 cicd-monitor 2 runs PASS). 12 commits pushed `3a34831..c0af9e0`. Plan G Trial Week 1 evidence: CICD Monitor catch 0 fail vì green 2/2 run, cost 110-120K/run under 150K budget, 3-3.5min CI time stable. KHÔNG còn pending push. UAT mode skip dotnet test mỗi chunk, test-after Plan C bundle cho tất cả turn 3-5 sau UAT 2-3 lần ổn.)
|
||||||
**S21 turn 5:** 2026-05-13 1400 (Session 21 turn 5 — **🎯 Refactor F1+F2+F3 sang PER-NV (Mig 29 drop Mig 28 column workflow-level). 4 chunk per-commit `0366946` (A BE schema+Service refactor) → `63234b2` (B FE Admin Designer 5 checkbox per-Level row) → `5ccb2a7` (C FE eOffice rename currentLevelOptions + drafterAllowSkipToFinal) → this Chunk D Docs. **F1+F3** 5 flag MOVED xuống `ApprovalWorkflowLevels` (per slot Approver, mỗi NV có riêng quyền duyệt). **F2** AllowDrafterSkipToFinal MOVED xuống `Users` (per-Drafter user, admin config ở User Management). Mig 29 4-stage: ADD 5 column Levels + 1 column Users + BACKFILL bulk SQL (copy workflow → all Levels, set TRUE cho Drafter user nào từng dùng workflow Allow) + DROP 6 column workflow `ApprovalWorkflows`. Service `ApplyReturnModeAsync` refactor đọc `currentLevel.AllowXxx` thay vì `workflow.AllowXxx`. Helper `EnsureEditableForDetailsAsync` read `level.AllowApproverEditDetails`. DRAFTER trình branch read `userManager.FindByIdAsync(actorUserId).AllowDrafterSkipToFinal`. DTO refactor: `AwLevelDto +5 Allow*`, `AwDefinitionDto -6 Allow*`, `CreateAwLevelInput +5 Allow*`, `PeDetailBundle.workflowOptions → currentLevelOptions + drafterAllowSkipToFinal`. FE Admin Designer drop section "Cấu hình nâng cao" workflow-level, replace 5 checkbox grid-cols-2 inline mỗi Level entry row (5 flag per slot). FE eOffice rename `wfOptions → levelOptions` đọc `currentLevelOptions`. Backward compat: backfill preserve admin config S21 t4. Test 84/84 PASS unchanged. Stats: **29 mig (+1) · 59 tables · ~143 endpoints · 34 FE pages · 84 test pass · 45 gotcha · 17 memory · 6 skills · 4 sub-agents seeds-only.**)
|
**S21 turn 5:** 2026-05-13 1400 (Session 21 turn 5 — **🎯 Refactor F1+F2+F3 sang PER-NV (Mig 29 drop Mig 28 column workflow-level). 4 chunk per-commit `0366946` (A BE schema+Service refactor) → `63234b2` (B FE Admin Designer 5 checkbox per-Level row) → `5ccb2a7` (C FE eOffice rename currentLevelOptions + drafterAllowSkipToFinal) → this Chunk D Docs. **F1+F3** 5 flag MOVED xuống `ApprovalWorkflowLevels` (per slot Approver, mỗi NV có riêng quyền duyệt). **F2** AllowDrafterSkipToFinal MOVED xuống `Users` (per-Drafter user, admin config ở User Management). Mig 29 4-stage: ADD 5 column Levels + 1 column Users + BACKFILL bulk SQL (copy workflow → all Levels, set TRUE cho Drafter user nào từng dùng workflow Allow) + DROP 6 column workflow `ApprovalWorkflows`. Service `ApplyReturnModeAsync` refactor đọc `currentLevel.AllowXxx` thay vì `workflow.AllowXxx`. Helper `EnsureEditableForDetailsAsync` read `level.AllowApproverEditDetails`. DRAFTER trình branch read `userManager.FindByIdAsync(actorUserId).AllowDrafterSkipToFinal`. DTO refactor: `AwLevelDto +5 Allow*`, `AwDefinitionDto -6 Allow*`, `CreateAwLevelInput +5 Allow*`, `PeDetailBundle.workflowOptions → currentLevelOptions + drafterAllowSkipToFinal`. FE Admin Designer drop section "Cấu hình nâng cao" workflow-level, replace 5 checkbox grid-cols-2 inline mỗi Level entry row (5 flag per slot). FE eOffice rename `wfOptions → levelOptions` đọc `currentLevelOptions`. Backward compat: backfill preserve admin config S21 t4. Test 84/84 PASS unchanged. Stats: **29 mig (+1) · 59 tables · ~143 endpoints · 34 FE pages · 84 test pass · 45 gotcha · 17 memory · 6 skills · 4 sub-agents seeds-only.**)
|
||||||
|
|||||||
@ -0,0 +1,250 @@
|
|||||||
|
# Session 23 turn 1 — 2026-05-14 — Plan K Mig 31 F2 refactor sang per-Approver-slot
|
||||||
|
|
||||||
|
**Dev:** Claude Opus 4.7 1M (4 sub-agents active + em main coordinator)
|
||||||
|
**Duration:** ~4h
|
||||||
|
**Base commit:** `eb106f2` (S22 chốt v2)
|
||||||
|
**Final HEAD:** `<latest>` (K8 docs)
|
||||||
|
**Total commits Plan K:** **8** (CHƯA push remote — chờ bro confirm K9 spawn CICD Monitor verify)
|
||||||
|
|
||||||
|
## 🎯 Trigger session
|
||||||
|
|
||||||
|
Bro nhận ra inconsistency S22+5:
|
||||||
|
- Mig 29 F1+F3 (5 flag) → per-Approver-slot ở Workflow Designer ✅
|
||||||
|
- Mig 30 F4 AllowApproverEditBudget → per-slot ở Designer ✅
|
||||||
|
- Mig 29 F2 AllowDrafterSkipToFinal → per-User flag ở **User Management page** (Plan D S22) ❌
|
||||||
|
|
||||||
|
Bro chốt 4 điểm refactor:
|
||||||
|
1. "Cấu hình cho phép edit → thực hiện trong trạng thái đang duyệt" (= F3+F4 OK pattern hiện tại)
|
||||||
|
2. "Cấu hình cho phép skip → duyệt thẳng cho phép trong trạng thái đang duyệt" (= F2 đổi semantic: Drafter from Nháp → Approver during ChoDuyet)
|
||||||
|
3. "Tất cả đều cấu hình ngay trong chỗ setup quy trình duyệt" (move F2 từ Users → Levels per-slot)
|
||||||
|
4. "Chỗ quy trình duyệt #NV 1 - Họ tên luôn" (slot label hiển thị ApproverFullName thay vì "NV 1")
|
||||||
|
|
||||||
|
→ Plan K refactor F2 storage + semantic + UI consistency.
|
||||||
|
|
||||||
|
## 🌳 Plan K 8 chunk execution
|
||||||
|
|
||||||
|
### Pre-flight (K0 + K0-bis)
|
||||||
|
|
||||||
|
- **K0 — 🟦 Investigator pre-flight audit F2 state** (~28K tokens spawn). Output: 12 file paths verified (Service + Entity + Config + DTO + FE × 2 app) + 5 surprises catch:
|
||||||
|
- S1: fe-user KHÔNG có Designer + UsersPage (admin-only) → K3/K5 scope hẹp hơn estimate
|
||||||
|
- S2: Prod 4/33 user flagged (drift S22+2 seed) → cần bro decision
|
||||||
|
- S3: Slot label thực tế `NV #{ei + 1}` (per-entry index, KHÔNG phải `#NV {order}`)
|
||||||
|
- S4: Audit comment string hardcoded line 149-150 cần rename
|
||||||
|
- S5: F2 default false consistent với F1 mode 1-3 + F3 + F4
|
||||||
|
|
||||||
|
- **K0-bis — 👤 Chủ trì solo sqlcmd 5s** prod 4 user list: `fin.pp` + `pm.nv` (S22+2 seed) + `nv.test` (legacy UAT) + `truong.nguyen` (real user Solutions admin tick manual).
|
||||||
|
|
||||||
|
**Bro decisions chốt 2 AskUserQuestion:**
|
||||||
|
- F2 semantic: **Option B** Approver ChoDuyet skip cuối (đổi semantic, mirror F3+F4)
|
||||||
|
- Slot label: **Bỏ '#NV {order}' hẳn**, chỉ "Họ tên" user pin
|
||||||
|
- Prod 4 user: **Option A accept lose** (log audit trail, admin re-config qua Designer)
|
||||||
|
- Slot label chunk: **Pre-Mig 31 chunk riêng** (Chunk pre-A)
|
||||||
|
|
||||||
|
### Chunk pre-A — Slot label refactor (`56868bf`)
|
||||||
|
|
||||||
|
🟨 Implementer Case 2 (~5K tokens spawn). 1 file fe-admin Designer line 873:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- Quyền duyệt NV #{ei + 1}
|
||||||
|
+ Quyền duyệt {usersList.data?.find(u => u.id === entry.approverUserId)?.fullName ?? 'Chưa chọn NV'}
|
||||||
|
```
|
||||||
|
|
||||||
|
Lookup pattern reuse precedent line 535. Bundle size unchanged trivial.
|
||||||
|
|
||||||
|
### K1 — Mig 31 schema swap (`db66253`)
|
||||||
|
|
||||||
|
🟨 Implementer Case 2 (~22K tokens spawn). 6 BE files atomic:
|
||||||
|
|
||||||
|
- `User.cs` REMOVE `AllowDrafterSkipToFinal` prop (line 38)
|
||||||
|
- `ApprovalWorkflow.cs` ADD `AllowApproverSkipToFinal` prop trên `ApprovalWorkflowLevel` (sau AllowApproverEditBudget)
|
||||||
|
- `ApprovalWorkflowConfiguration.cs` ADD `HasDefaultValue(false)` (match 6 dòng existing)
|
||||||
|
- `PurchaseEvaluationWorkflowService.cs` surgical remove F2 Drafter SUBMIT skipToFinal block line 125-161 + K2 marker comment
|
||||||
|
- Mig 31 `RefactorSkipToFinalToApproverLevel` — 2 stage:
|
||||||
|
- Stage 1: ADD Levels column FIRST
|
||||||
|
- Stage 2: DROP Users column SECOND (manual reorder per memory `feedback_ef_migration_backfill_reorder`)
|
||||||
|
- NO BACKFILL (Option A — 4 prod user accept lose)
|
||||||
|
- Apply Dev + Design DB
|
||||||
|
|
||||||
|
Implementer flagged 4 ambiguities:
|
||||||
|
1. UserConfiguration.cs KHÔNG exist (config inline trong ApplicationDbContext.OnModelCreating line 86) — no action
|
||||||
|
2. Application layer compile-break (UserFeatures.cs + PurchaseEvaluationFeatures.cs reference removed prop) → minimal sentinel-`false` patches + K2 marker
|
||||||
|
3. Test file line 253 still references removed prop → K7 fix
|
||||||
|
4. Service IF block scope precision — remove only `if (skipToFinal)` true-branch + flatten `else`
|
||||||
|
|
||||||
|
Test count 4039 insertions / 63 deletions (Designer + Snapshot dominate).
|
||||||
|
|
||||||
|
### K2 — BE Service Approver F2 branch + DTO refactor (`364aef6`)
|
||||||
|
|
||||||
|
👤 Chủ trì Solo (cross-stack reasoning + state machine integration). 4 files:
|
||||||
|
|
||||||
|
- `PurchaseEvaluationWorkflowService.cs`:
|
||||||
|
- `ApproveV2Async` +`bool skipToFinal` 8th param
|
||||||
|
- APPROVE STEP branch sau UPSERT opinion line 477:
|
||||||
|
- if (!isAdmin && !isSystem && !matchingLevel.AllowApproverSkipToFinal) → ConflictException with helpful message
|
||||||
|
- else → set Phase=DaDuyet + clear pointer + SLA + LogTransition "[Approver duyệt thẳng Cấp cuối — Bước X Cấp Y → DaDuyet]"
|
||||||
|
- Caller TransitionAsync line ~144 pass skipToFinal vào ApproveV2Async
|
||||||
|
- V1 ApproveV1LegacyAsync caller throw nếu skipToFinal=true non-admin
|
||||||
|
- Drafter SUBMIT branch comment cleanup K1 marker → Mig 31 deprecation note
|
||||||
|
- `ApprovalWorkflowOptionsDto` +7th field `AllowApproverSkipToFinal`
|
||||||
|
- `PurchaseEvaluationDetailBundleDto` REMOVE `DrafterAllowSkipToFinal` field
|
||||||
|
- `IPurchaseEvaluationWorkflowService.cs` comment update Mig 28-31 semantic refactor
|
||||||
|
- `PurchaseEvaluationFeatures.cs` GetPe handler populate 7 Allow* + REMOVE sentinel `drafterAllowSkipToFinal`
|
||||||
|
|
||||||
|
Build PASS 0 err, test K1 transient FAIL expected → K7 fix.
|
||||||
|
|
||||||
|
### Reviewer K2 pre-K3 verify (PASS)
|
||||||
|
|
||||||
|
🟥 Reviewer K2 (~22K tokens spawn). 5-category checklist verdict:
|
||||||
|
|
||||||
|
| Category | Result | Issues |
|
||||||
|
|---|---|---|
|
||||||
|
| 1. Wire BE Approver F2 | ✅ PASS | 0 critical (branch placement đúng) |
|
||||||
|
| 2. Schema Mig 31 | ✅ PASS | 3-file rule + Up() ADD-DROP + sqlcmd verify 7 cols Levels + 0 Users.AllowDrafterSkipToFinal |
|
||||||
|
| 3. Security | ✅ PASS | admin bypass + V1 guard + clear error |
|
||||||
|
| 4. Code quality | ✅ PASS | 0 err build, anti-fiddle clean |
|
||||||
|
| 5. Tests | ✅ PASS (UAT defer K7) | — |
|
||||||
|
|
||||||
|
**2 Major + 2 Minor flagged:**
|
||||||
|
- Major #1 **Zombie endpoint** PATCH /users/{id}/allow-skip-final Admin tick = NoOp swallow silent → K5 cleanup priority
|
||||||
|
- Major #2 Stale narrative `ApprovalWorkflow.cs:78-80` F2 cũ → K5 + K8 polish
|
||||||
|
- Minor #3 `PurchaseEvaluationFeatures.cs:401` Command DTO comment → K5 polish
|
||||||
|
- Minor #4 `ApprovalWorkflowConfiguration.cs:22-24` Mig narrative → K5 polish
|
||||||
|
|
||||||
|
**New pattern caught Reviewer:** "Transient sentinel zombie" — K1 sentinel-`false` patch + chunk scope shift → endpoint NoOp silent. Mitigation: Reviewer pre-commit Smart Friend guard catch + recommend cleanup priority.
|
||||||
|
|
||||||
|
### K3 — FE Admin Designer 7th checkbox + banner rewrite (`dd52d16`)
|
||||||
|
|
||||||
|
🟨 Implementer Case 2 (~6K tokens spawn). 1 file fe-admin Designer:
|
||||||
|
|
||||||
|
- Type `LevelDto +allowApproverSkipToFinal` (7th)
|
||||||
|
- Type `EditLevelEntry +allowApproverSkipToFinal`
|
||||||
|
- Helper `makeDefaultLevelEntry` default false
|
||||||
|
- Helper `copyFromDefinition` propagate
|
||||||
|
- Inline checkbox panel mỗi Level entry: "Cho phép duyệt thẳng Cấp cuối khi đang duyệt" (cùng group amber-50/30 với 5 F1+F3 + 1 F4 → 7 checkbox grid-cols-2)
|
||||||
|
- Banner line ~623-631 rewrite: "F2 cấu hình ở User Management" (Plan D S22 stale) → "Cấu hình quyền duyệt riêng cho từng NV trong slot Approver bên dưới"
|
||||||
|
- POST/PATCH mutation body propagate 7th flag
|
||||||
|
|
||||||
|
Build PASS 0 TS err, bundle 1395.74 KB unchanged trivial.
|
||||||
|
|
||||||
|
### K5 — Zombie endpoint cleanup + Reviewer Major #1+#2 + Minor #3+#4 (`2ea8977`)
|
||||||
|
|
||||||
|
🟨 Implementer Case 2 (~12K tokens spawn). Full backout Plan D S22:
|
||||||
|
|
||||||
|
**BE drop (7 files):**
|
||||||
|
- `UsersController.cs` DELETE PATCH /allow-skip-final endpoint + Body record
|
||||||
|
- `UserFeatures.cs` DELETE SetUserAllowDrafterSkipToFinalCommand + Handler + UserDto.AllowDrafterSkipToFinal field + sentinel mapping references
|
||||||
|
- `ApprovalWorkflow.cs` rewrite stale narrative line 78-80 (Major #2)
|
||||||
|
- `PurchaseEvaluationFeatures.cs` rewrite Command DTO comment (Minor #3)
|
||||||
|
- `ApprovalWorkflowConfiguration.cs` APPEND Mig 31 narrative (Minor #4)
|
||||||
|
- `ApprovalWorkflowV2AdminFeatures.cs` clean stale DTO comment
|
||||||
|
- `IPurchaseEvaluationWorkflowService.cs` + `PurchaseEvaluationDtos.cs` clean storage references
|
||||||
|
|
||||||
|
**FE Admin drop (2 files):**
|
||||||
|
- `UsersPage.tsx` DELETE "Skip cuối" column + FastForward badge + button toggle + mutation hook
|
||||||
|
- `types/users.ts` DELETE allowDrafterSkipToFinal field
|
||||||
|
|
||||||
|
**Grep verify:** `AllowDrafterSkipToFinal` + `allow-skip-final` + `allowDrafterSkipToFinal` + `Skip cuối` + `FastForward` → 0 results src/Backend + fe-admin/src.
|
||||||
|
|
||||||
|
Build PASS BE + fe-admin. Implementer flagged 4 stale comments fe-user PeDetailTabs (cosmetic, K6 cleanup).
|
||||||
|
|
||||||
|
### K6 — Workspace × 2 app DROP Drafter + ADD Approver toggle (`ebe2469`)
|
||||||
|
|
||||||
|
👤 Chủ trì Solo (UX flow decision — Approver workflow trigger point). 6 files (3 fe-admin + 3 fe-user mirror):
|
||||||
|
|
||||||
|
**DROP Drafter Workspace checkbox:**
|
||||||
|
- `PeDetailTabs.tsx` × 2 app: REMOVE state skipToFinal + allowSkipToFinal + violet label "Gửi thẳng Cấp cuối (skip trung gian)" + mutation skipToFinal payload
|
||||||
|
- Simplify mutation signature: `opts: { skipToFinal: boolean }` → void
|
||||||
|
- Drop conditional button label + confirm dialog text
|
||||||
|
|
||||||
|
**ADD Approver Workspace toggle (Dialog approach — UX consistent với Trả lại Mode picker):**
|
||||||
|
- `PeWorkflowPanel.tsx` × 2 app: state `skipToFinalApprover` default false
|
||||||
|
- Visible khi Approve forward (NOT Cancel + NOT SendBack) + `currentLevelOptions?.allowApproverSkipToFinal === true`
|
||||||
|
- Checkbox violet panel + Amber warning "Hành động KHÔNG quay lại được"
|
||||||
|
- Mutation payload +`skipToFinal: !isReject && skipToFinalApprover`
|
||||||
|
- onSuccess reset state
|
||||||
|
|
||||||
|
**Type cleanup × 2 app:**
|
||||||
|
- `ApprovalWorkflowOptions` +`allowApproverSkipToFinal: boolean` (7th)
|
||||||
|
- `PeDetailBundle` REMOVE `drafterAllowSkipToFinal` field + Mig 29+30+31 cumulative comment
|
||||||
|
|
||||||
|
Build PASS × 2 app, bundle rotated.
|
||||||
|
|
||||||
|
### K7 — Tests regression (`6b1e2d9`)
|
||||||
|
|
||||||
|
🟨 Implementer Case 3 (~14K tokens spawn). 1 test file:
|
||||||
|
|
||||||
|
**Delete 3 deprecated Drafter F2 tests** (semantic deprecated Mig 31, no value).
|
||||||
|
|
||||||
|
**Add 3 Approver F2 service tests** pattern Implementer memory Pattern 11:
|
||||||
|
- `ApproveV2_SkipToFinal_AdminTickFlag_SetsPhaseDaDuyet` (happy path)
|
||||||
|
- `ApproveV2_SkipToFinal_FlagOff_NonAdmin_ThrowsConflictException` (denied)
|
||||||
|
- `ApproveV2_SkipToFinal_FlagOff_Admin_BypassesFlagCheck` (admin bypass)
|
||||||
|
|
||||||
|
Helper `SeedApproverF2WorkflowAsync` 2-Step variant (test multi-step verify skip → terminal NOT fallthrough).
|
||||||
|
|
||||||
|
Test count **104/104 PASS** baseline preserved (3 deleted + 3 added cancel out).
|
||||||
|
|
||||||
|
### K8 — Docs + commit + push (this commit)
|
||||||
|
|
||||||
|
- `docs/database/schema-diagram.md §14` update Mig 22-31 title + add Mig 30 + Mig 31 blocks + DROP Users column note
|
||||||
|
- `docs/STATUS.md` Last updated S23 t1 entry
|
||||||
|
- `docs/HANDOFF.md` TL;DR S23 t1 đầy đủ (top)
|
||||||
|
- Session log file này
|
||||||
|
- Memory `feedback_per_nv_permission_scope.md` reinforce 3× cumulative S23 t1
|
||||||
|
- MEMORY index update
|
||||||
|
- 2 stale FE Designer comments fe-admin (lines 73-75 + 502-504) rewrite Mig 29+30+31 cumulative
|
||||||
|
- 3 dirty agent MEMORY.md commit cùng (Investigator + Reviewer + CICD Monitor from S22 chốt + S23 t1)
|
||||||
|
|
||||||
|
## 📊 Stats S23 t1 chốt
|
||||||
|
|
||||||
|
| Metric | Trước (S22 chốt) | Sau (S23 t1) | Δ |
|
||||||
|
|---|---|---|---|
|
||||||
|
| DB tables | 59 | 59 | 0 |
|
||||||
|
| **Migrations** | 30 | **31** | **+1** (Mig 31 refactor F2) |
|
||||||
|
| Endpoints | ~146 | **~145** | **-1** (backout /allow-skip-final) |
|
||||||
|
| FE pages | 34 | 34 | 0 |
|
||||||
|
| **Unit tests** | 104 | **104** | 0 (3 deleted + 3 added cancel) |
|
||||||
|
| Gotchas | 47 | 47 | 0 |
|
||||||
|
| Memory entries | 19 | **20** | +1 reinforcement S23 t1 (cumulative entry update) |
|
||||||
|
| Skills | 6 | 6 | 0 |
|
||||||
|
| Sub-agents | 4 (seeds-only S22 cumulative) | 4 (1 Inv + 4 Imp + 1 Rev spawn) | Plan K active execution |
|
||||||
|
| Active prod users | 33 | 33 | 0 (4 user lose flag, KHÔNG remove user) |
|
||||||
|
| **Commits Plan K** | — | **8** (`56868bf..<latest>`) | pre-A + K1-K3 + K5-K8 |
|
||||||
|
|
||||||
|
## 🎯 Multi-agent ROI evidence Plan K
|
||||||
|
|
||||||
|
| Spawn | Agent | Cost (tokens) | Output | Catch |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| K0 | 🟦 Investigator | ~28K | 12 file paths + 5 surprises (S1-S5) + LOC estimate | S1 fe-user no Designer → scope giảm + S2 4 prod user drift + S3 slot label real format |
|
||||||
|
| K0-bis | 👤 Chủ trì | <1K | 4 user list (fin.pp+pm.nv+nv.test+truong.nguyen) | — |
|
||||||
|
| Chunk pre-A | 🟨 Implementer Case 2 | ~5K | 1 commit slot label rename | — |
|
||||||
|
| K1 | 🟨 Implementer Case 2 | ~22K | 1 commit Mig 31 schema swap 6 files | 3 ambiguities (UserConfig inline + compile-break workaround + test file fix K7 + IF block precision) |
|
||||||
|
| K2 | 👤 Chủ trì | ~self | 1 commit Service + DTO refactor | — |
|
||||||
|
| Reviewer K2 | 🟥 Reviewer | ~22K | PASS 0 critical + 2 Major + 2 Minor | Major #1 zombie endpoint + Major #2 stale narrative + new pattern "transient sentinel zombie" |
|
||||||
|
| K3 | 🟨 Implementer Case 2 | ~6K | 1 commit Designer 7th checkbox + banner | — |
|
||||||
|
| K5 | 🟨 Implementer Case 2 | ~12K | 1 commit zombie cleanup BE 7 + FE 2 | — |
|
||||||
|
| K6 | 👤 Chủ trì | ~self | 1 commit Workspace × 2 app refactor | — |
|
||||||
|
| K7 | 🟨 Implementer Case 3 | ~14K | 1 commit tests regression 104/104 PASS | — |
|
||||||
|
| K8 | 👤 Chủ trì | ~self | 1 commit docs + memory + push | — |
|
||||||
|
| K9 (pending) | 🟩 CICD Monitor | ~150K | post-deploy verify Mig 31 prod + bundle hash | — |
|
||||||
|
|
||||||
|
**Total Plan K spawn cost (excl K9):** ~109K tokens — under 200K budget per session.
|
||||||
|
|
||||||
|
**Multi-agent value caught:**
|
||||||
|
1. **Investigator K0 S2 4 prod user drift** — em main solo dễ bỏ qua, audit qua sqlcmd K0-bis catch real user `truong.nguyen` admin tick manual S22+ (cần follow-up post-deploy)
|
||||||
|
2. **Reviewer K2 Major #1 zombie endpoint** — em main solo K2 đã pass build clean nhưng KHÔNG nhớ cleanup Plan D S22 wire → Reviewer catch + K5 priority cleanup → giữ UX clean cho admin
|
||||||
|
3. **Reviewer K2 new pattern "transient sentinel zombie"** — pattern reusable cross-project saved Reviewer memory
|
||||||
|
|
||||||
|
## ⚠️ Pending S23 t2+
|
||||||
|
|
||||||
|
- **K9 CICD Monitor post-deploy verify** — chờ bro confirm spawn sau push remote
|
||||||
|
- **Plan B Contract V2 wire Mig 32+33** — chưa kick off (PE V2 + per-NV + F2 refactor đã proven 3×, ready mirror sang Contract entity per HANDOFF S22 pre-allocation)
|
||||||
|
- **Plan C carry test-after bundle** — defer UAT 2-3 lần ổn (Service ApplyReturnMode test catch-up Mig 29 + Mig 30 F4 service test)
|
||||||
|
- **Admin re-config `truong.nguyen` qua Designer** — real user lose flag, cần bro UAT note hoặc ping user
|
||||||
|
|
||||||
|
## 📋 Pattern reinforced
|
||||||
|
|
||||||
|
- **Per-NV admin opt-in flag pattern 3× cumulative** — Mig 29 F1+F3 (5 flag) + Mig 30 F4 (1 flag) + Mig 31 F2 (1 flag refactor). Pattern ALSO applies cho refactor existing scope, KHÔNG chỉ greenfield. Memory `feedback_per_nv_permission_scope.md` reinforced 3×.
|
||||||
|
- **EF Migration ADD→DROP no-BACKFILL pattern** cho semantic refactor (Mig 29 cumulative 3× với Mig 31). Decision lose vs preserve based on semantic similarity old vs new.
|
||||||
|
- **Multi-agent atomic per-chunk discipline** — Implementer Case 2 cookie-cutter 4 spawns, Reviewer pre-commit catch zombie, Chủ trì cross-stack reasoning. ROI evidence Week 2 trial.
|
||||||
|
- **UAT mode Phase 9 test-after** — bro confirm K7 OK sau 7 chunk UAT iteration (Implementer K1+K2+K3+K5 + Chủ trì K2+K6 build verify mỗi chunk, test verify K7 cuối).
|
||||||
@ -715,14 +715,15 @@ CREATE TABLE PurchaseEvaluationDepartmentOpinions (
|
|||||||
CREATE UNIQUE INDEX IX_PEDeptOpinions_PEId_Kind ON PurchaseEvaluationDepartmentOpinions (PurchaseEvaluationId, Kind);
|
CREATE UNIQUE INDEX IX_PEDeptOpinions_PEId_Kind ON PurchaseEvaluationDepartmentOpinions (PurchaseEvaluationId, Kind);
|
||||||
```
|
```
|
||||||
|
|
||||||
## 14. ApprovalWorkflow V2 schema (Migration 22-29, Session 17-21 — 3 bảng mới + 5 column Level + 1 column User)
|
## 14. ApprovalWorkflow V2 schema (Migration 22-31, Session 17-23 — 3 bảng mới + 7 Allow* options per slot Approver)
|
||||||
|
|
||||||
Schema riêng song song WorkflowDefinition V1 (Mig 21) — pin per phiếu PE.
|
Schema riêng song song WorkflowDefinition V1 (Mig 21) — pin per phiếu PE.
|
||||||
V1 vẫn giữ cho phiếu cũ; V2 mới là active cho phiếu tạo từ Session 17 trở đi.
|
V1 vẫn giữ cho phiếu cũ; V2 mới là active cho phiếu tạo từ Session 17 trở đi.
|
||||||
|
|
||||||
Mig 29 (S21 t5) — Refactor 6 Allow* options từ workflow-level (Mig 28 S21 t4)
|
**Pattern cumulative cho admin opt-in flag per slot — proven 3× (memory `feedback_per_nv_permission_scope.md`):**
|
||||||
sang PER-NV: 5 flag F1+F3 xuống Level slot (Approver), 1 flag F2 xuống User
|
- Mig 29 (S21 t5) — Refactor 6 Allow* options từ workflow-level (Mig 28 S21 t4) sang PER-NV: 5 flag F1+F3 xuống Level slot, 1 flag F2 xuống User. Backfill bulk SQL preserve admin config S21 t4.
|
||||||
(per-Drafter). Backfill bulk SQL preserve admin config S21 t4.
|
- Mig 30 (S22+5) — F4 `AllowApproverEditBudget` per slot Approver — admin tick cho NV chỉnh Section "Điều chỉnh ngân sách" khi đang duyệt.
|
||||||
|
- Mig 31 (S23 t1) — F2 refactor sang Approver scope: ADD `AllowApproverSkipToFinal` per Level slot, DROP `Users.AllowDrafterSkipToFinal`. Semantic cũ Drafter-from-Nháp deprecated. **NO BACKFILL** (4 prod user lose flag value — admin re-config qua Designer).
|
||||||
|
|
||||||
### Core (3 bảng):
|
### Core (3 bảng):
|
||||||
|
|
||||||
@ -751,12 +752,18 @@ ApprovalWorkflowLevels (FK Cascade ApprovalWorkflowStepId, FK Restrict Approver
|
|||||||
├── AllowReturnToDrafter bit DEFAULT 1 — F1 mode: Trả về Drafter (S17 backward compat)
|
├── AllowReturnToDrafter bit DEFAULT 1 — F1 mode: Trả về Drafter (S17 backward compat)
|
||||||
├── AllowApproverEditDetails bit DEFAULT 0 — F3: NV này chỉnh Section 2 lúc đang duyệt
|
├── AllowApproverEditDetails bit DEFAULT 0 — F3: NV này chỉnh Section 2 lúc đang duyệt
|
||||||
│
|
│
|
||||||
|
├── Mig 30 (S22+5) — F4 admin opt-in per slot:
|
||||||
|
├── AllowApproverEditBudget bit DEFAULT 0 — F4: NV này chỉnh Section "Điều chỉnh ngân sách"
|
||||||
|
│
|
||||||
|
├── Mig 31 (S23 t1) — F2 refactor admin opt-in per slot:
|
||||||
|
├── AllowApproverSkipToFinal bit DEFAULT 0 — F2: NV này duyệt thẳng Cấp cuối (skip trung gian)
|
||||||
|
│ (storage cũ Users.AllowDrafterSkipToFinal đã DROP)
|
||||||
|
│
|
||||||
└── INDEX (ApprovalWorkflowStepId, Order) + INDEX ApproverUserId
|
└── INDEX (ApprovalWorkflowStepId, Order) + INDEX ApproverUserId
|
||||||
|
|
||||||
Users (AspNetUsers extension)
|
Users (AspNetUsers extension)
|
||||||
├── ... existing columns (FullName, DepartmentId, PositionLevel, CanBypassReview, etc)
|
├── ... existing columns (FullName, DepartmentId, PositionLevel, CanBypassReview, etc)
|
||||||
└── Mig 29 (S21 t5) — F2 per-Drafter:
|
└── (Mig 29 AllowDrafterSkipToFinal column đã DROP trong Mig 31 — F2 refactor sang Approver scope)
|
||||||
AllowDrafterSkipToFinal bit DEFAULT 0 — User được Drafter gửi PE thẳng Cấp cuối
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Convention quan trọng:** nhiều `ApprovalWorkflowLevel` rows cùng `Order` trong cùng Step = **same Cấp với N approvers** (OR-of-N). Ví dụ Cấp 1 có 2 NV: 2 row Level cùng `Order=1` khác `ApproverUserId`.
|
**Convention quan trọng:** nhiều `ApprovalWorkflowLevel` rows cùng `Order` trong cùng Step = **same Cấp với N approvers** (OR-of-N). Ví dụ Cấp 1 có 2 NV: 2 row Level cùng `Order=1` khác `ApproverUserId`.
|
||||||
|
|||||||
@ -117,19 +117,17 @@ export function PeDetailTabs({
|
|||||||
// "Lưu & Gửi Duyệt" workspace mode (user 2026-05-07): trigger transition
|
// "Lưu & Gửi Duyệt" workspace mode (user 2026-05-07): trigger transition
|
||||||
// sang phase tiếp theo (= Đã gửi duyệt). nextPhases[0] thường là ChoPurchasing
|
// sang phase tiếp theo (= Đã gửi duyệt). nextPhases[0] thường là ChoPurchasing
|
||||||
// (skip TuChoi). Sau success → toast + invalidate + onBack đóng workspace.
|
// (skip TuChoi). Sau success → toast + invalidate + onBack đóng workspace.
|
||||||
// Mig 29 (S21 t5) — F2: per-Drafter user flag (User Management page).
|
// Mig 31 (S23 t1) — F2 Drafter-from-Nháp semantic deprecated. skipToFinal moved
|
||||||
const [skipToFinal, setSkipToFinal] = useState(false)
|
// sang Approver scope ChoDuyet (per-Level slot — xem PeWorkflowPanel).
|
||||||
const allowSkipToFinal = evaluation.drafterAllowSkipToFinal ?? false
|
|
||||||
|
|
||||||
const submitForApproval = useMutation({
|
const submitForApproval = useMutation({
|
||||||
mutationFn: async (opts: { skipToFinal: boolean }) => {
|
mutationFn: async () => {
|
||||||
const next = evaluation.workflow.nextPhases.find(p => p !== PurchaseEvaluationPhase.TuChoi && p !== PurchaseEvaluationPhase.TraLai)
|
const next = evaluation.workflow.nextPhases.find(p => p !== PurchaseEvaluationPhase.TuChoi && p !== PurchaseEvaluationPhase.TraLai)
|
||||||
if (!next) throw new Error('Không có phase tiếp theo để gửi duyệt')
|
if (!next) throw new Error('Không có phase tiếp theo để gửi duyệt')
|
||||||
return api.post(`/purchase-evaluations/${evaluation.id}/transitions`, {
|
return api.post(`/purchase-evaluations/${evaluation.id}/transitions`, {
|
||||||
targetPhase: next,
|
targetPhase: next,
|
||||||
decision: 1,
|
decision: 1,
|
||||||
comment: null,
|
comment: null,
|
||||||
skipToFinal: opts.skipToFinal,
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@ -283,33 +281,19 @@ export function PeDetailTabs({
|
|||||||
>
|
>
|
||||||
Lưu
|
Lưu
|
||||||
</Button>
|
</Button>
|
||||||
{/* Mig 28 (S21 t4) — F2: Drafter skip checkbox */}
|
|
||||||
{allowSkipToFinal && canSubmitForApproval && (
|
|
||||||
<label className="flex cursor-pointer items-center gap-1.5 rounded border border-violet-200 bg-violet-50 px-2 py-1 text-[11px] text-violet-800">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="h-3 w-3"
|
|
||||||
checked={skipToFinal}
|
|
||||||
onChange={e => setSkipToFinal(e.target.checked)}
|
|
||||||
/>
|
|
||||||
<span>Gửi thẳng Cấp cuối (skip trung gian)</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!forwardPhase) return
|
if (!forwardPhase) return
|
||||||
const confirmMsg = skipToFinal
|
const confirmMsg = `Gửi phiếu vào quy trình duyệt? Sẽ chuyển sang "${PurchaseEvaluationPhaseLabel[forwardPhase]}". Sau khi gửi sẽ KHÔNG sửa được nữa (trừ khi approver Trả lại).`
|
||||||
? `Gửi THẲNG CẤP CUỐI bỏ qua các Cấp trung gian? Hệ thống sẽ ghi audit "Drafter skip" — không quay lại được trừ khi approver Trả lại.`
|
|
||||||
: `Gửi phiếu vào quy trình duyệt? Sẽ chuyển sang "${PurchaseEvaluationPhaseLabel[forwardPhase]}". Sau khi gửi sẽ KHÔNG sửa được nữa (trừ khi approver Trả lại).`
|
|
||||||
if (confirm(confirmMsg)) {
|
if (confirm(confirmMsg)) {
|
||||||
submitForApproval.mutate({ skipToFinal })
|
submitForApproval.mutate()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!canSubmitForApproval || submitForApproval.isPending}
|
disabled={!canSubmitForApproval || submitForApproval.isPending}
|
||||||
title={submitDisabledReason ?? `Gửi phiếu sang "${forwardPhase ? PurchaseEvaluationPhaseLabel[forwardPhase] : '?'}"`}
|
title={submitDisabledReason ?? `Gửi phiếu sang "${forwardPhase ? PurchaseEvaluationPhaseLabel[forwardPhase] : '?'}"`}
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
>
|
>
|
||||||
{submitForApproval.isPending ? 'Đang gửi…' : skipToFinal ? 'Lưu & Gửi thẳng CẤP CUỐI →' : 'Lưu & Gửi Duyệt →'}
|
{submitForApproval.isPending ? 'Đang gửi…' : 'Lưu & Gửi Duyệt →'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -37,6 +37,9 @@ export function PeWorkflowPanel({
|
|||||||
// Mig 28 (S21 t4) — F1 mode Trả lại. Default Drafter (backward compat S17).
|
// Mig 28 (S21 t4) — F1 mode Trả lại. Default Drafter (backward compat S17).
|
||||||
const [returnMode, setReturnMode] = useState<WorkflowReturnMode>(WorkflowReturnMode.Drafter)
|
const [returnMode, setReturnMode] = useState<WorkflowReturnMode>(WorkflowReturnMode.Drafter)
|
||||||
const [returnTargetUserId, setReturnTargetUserId] = useState<string | null>(null)
|
const [returnTargetUserId, setReturnTargetUserId] = useState<string | null>(null)
|
||||||
|
// Mig 31 (S23 t1) — F2 Approver duyệt thẳng Cấp cuối. Default false (admin opt-in
|
||||||
|
// per slot tick → checkbox visible trong dialog Approve, default unchecked).
|
||||||
|
const [skipToFinalApprover, setSkipToFinalApprover] = useState(false)
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const { user: currentUser } = useAuth()
|
const { user: currentUser } = useAuth()
|
||||||
const isAdmin = currentUser?.roles?.includes('Admin') ?? false
|
const isAdmin = currentUser?.roles?.includes('Admin') ?? false
|
||||||
@ -97,6 +100,9 @@ export function PeWorkflowPanel({
|
|||||||
returnMode: isTraLaiAction ? returnMode : null,
|
returnMode: isTraLaiAction ? returnMode : null,
|
||||||
returnTargetUserId: isTraLaiAction && returnMode === WorkflowReturnMode.Assignee
|
returnTargetUserId: isTraLaiAction && returnMode === WorkflowReturnMode.Assignee
|
||||||
? returnTargetUserId : null,
|
? returnTargetUserId : null,
|
||||||
|
// Mig 31 (S23 t1) — F2 Approver scope ChoDuyet duyệt thẳng Cấp cuối.
|
||||||
|
// BE check matchingLevel.AllowApproverSkipToFinal (admin opt-in per slot).
|
||||||
|
skipToFinal: !isReject && skipToFinalApprover,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@ -108,6 +114,7 @@ export function PeWorkflowPanel({
|
|||||||
setComment('')
|
setComment('')
|
||||||
setReturnMode(WorkflowReturnMode.Drafter)
|
setReturnMode(WorkflowReturnMode.Drafter)
|
||||||
setReturnTargetUserId(null)
|
setReturnTargetUserId(null)
|
||||||
|
setSkipToFinalApprover(false)
|
||||||
},
|
},
|
||||||
onError: e => toast.error(getErrorMessage(e)),
|
onError: e => toast.error(getErrorMessage(e)),
|
||||||
})
|
})
|
||||||
@ -397,6 +404,32 @@ export function PeWorkflowPanel({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{/* Mig 31 (S23 t1) — F2 Approver toggle: chỉ visible khi Approve forward
|
||||||
|
+ admin tick AllowApproverSkipToFinal cho slot Cấp hiện tại. */}
|
||||||
|
{!isCancel && !isSendBack && levelOptions?.allowApproverSkipToFinal && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="flex cursor-pointer items-start gap-2 rounded border border-violet-200 bg-violet-50 px-3 py-2 text-[12px] text-violet-800 hover:bg-violet-100/60">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mt-0.5"
|
||||||
|
checked={skipToFinalApprover}
|
||||||
|
onChange={e => setSkipToFinalApprover(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<span className="font-medium">Duyệt thẳng Cấp cuối (skip Bước/Cấp trung gian)</span>
|
||||||
|
<span className="mt-0.5 block text-[11px] text-violet-700/80">
|
||||||
|
Phiếu sẽ tiến thẳng tới "Đã duyệt" (terminal) — bỏ qua mọi Cấp/Bước còn lại.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isCancel && !isSendBack && skipToFinalApprover && (
|
||||||
|
<div className="mb-3 rounded border border-amber-300 bg-amber-50 px-3 py-2 text-[11px] text-amber-800">
|
||||||
|
⚠ Hành động KHÔNG quay lại được (trừ khi Drafter reset toàn bộ). Phiếu sẽ
|
||||||
|
skip qua tất cả Cấp/Bước còn lại và chuyển thẳng "Đã duyệt".
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Label>Ghi chú (tùy chọn)</Label>
|
<Label>Ghi chú (tùy chọn)</Label>
|
||||||
<Textarea value={comment} onChange={e => setComment(e.target.value)} rows={3} />
|
<Textarea value={comment} onChange={e => setComment(e.target.value)} rows={3} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@ -43,12 +43,14 @@ type LevelDto = {
|
|||||||
approverEmail: string | null
|
approverEmail: string | null
|
||||||
// Mig 29 (S21 t5) — 5 Allow* options per slot Approver
|
// Mig 29 (S21 t5) — 5 Allow* options per slot Approver
|
||||||
// Mig 30 (S22+5) — +AllowApproverEditBudget cho Section ngân sách
|
// Mig 30 (S22+5) — +AllowApproverEditBudget cho Section ngân sách
|
||||||
|
// Mig 31 (S23 t1) — +AllowApproverSkipToFinal F2 storage swap Users→Level (per-Approver-slot)
|
||||||
allowReturnOneLevel: boolean
|
allowReturnOneLevel: boolean
|
||||||
allowReturnOneStep: boolean
|
allowReturnOneStep: boolean
|
||||||
allowReturnToAssignee: boolean
|
allowReturnToAssignee: boolean
|
||||||
allowReturnToDrafter: boolean
|
allowReturnToDrafter: boolean
|
||||||
allowApproverEditDetails: boolean
|
allowApproverEditDetails: boolean
|
||||||
allowApproverEditBudget: boolean
|
allowApproverEditBudget: boolean
|
||||||
|
allowApproverSkipToFinal: boolean
|
||||||
}
|
}
|
||||||
type StepDto = {
|
type StepDto = {
|
||||||
id: string
|
id: string
|
||||||
@ -68,9 +70,10 @@ type DefinitionDto = {
|
|||||||
description: string | null
|
description: string | null
|
||||||
isActive: boolean
|
isActive: boolean
|
||||||
isUserSelectable: boolean // Mig 25 — admin toggle cho user pick
|
isUserSelectable: boolean // Mig 25 — admin toggle cho user pick
|
||||||
// Mig 29 (S21 t5) — 6 Allow* options MOVED:
|
// Mig 29 (S21 t5) + Mig 30 (S22+5) + Mig 31 (S23 t1) — 7 Allow* options
|
||||||
// - 5 flag F1+F3 xuống per slot Level (xem LevelDto)
|
// ALL xuống per slot Level (xem LevelDto). Admin opt-in per-Approver-slot
|
||||||
// - 1 flag F2 AllowDrafterSkipToFinal xuống per User (User Management)
|
// pattern proven 3× cumulative: F1+F3 (5 flag Mig 29) + F4 (Mig 30) + F2
|
||||||
|
// (Mig 31 refactor sang Approver scope, storage cũ Users đã drop).
|
||||||
activatedAt: string | null
|
activatedAt: string | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
steps: StepDto[]
|
steps: StepDto[]
|
||||||
@ -89,12 +92,14 @@ type EditLevelEntry = {
|
|||||||
// Mig 29 (S21 t5) — 5 Allow* per slot (default backward compat S17: chỉ
|
// Mig 29 (S21 t5) — 5 Allow* per slot (default backward compat S17: chỉ
|
||||||
// AllowReturnToDrafter=true, 4 còn lại false).
|
// AllowReturnToDrafter=true, 4 còn lại false).
|
||||||
// Mig 30 (S22+5) — +AllowApproverEditBudget cho Section ngân sách (default false).
|
// Mig 30 (S22+5) — +AllowApproverEditBudget cho Section ngân sách (default false).
|
||||||
|
// Mig 31 (S23 t1) — +AllowApproverSkipToFinal F2 admin opt-in per-Approver-slot (default false).
|
||||||
allowReturnOneLevel: boolean
|
allowReturnOneLevel: boolean
|
||||||
allowReturnOneStep: boolean
|
allowReturnOneStep: boolean
|
||||||
allowReturnToAssignee: boolean
|
allowReturnToAssignee: boolean
|
||||||
allowReturnToDrafter: boolean
|
allowReturnToDrafter: boolean
|
||||||
allowApproverEditDetails: boolean
|
allowApproverEditDetails: boolean
|
||||||
allowApproverEditBudget: boolean
|
allowApproverEditBudget: boolean
|
||||||
|
allowApproverSkipToFinal: boolean
|
||||||
}
|
}
|
||||||
type EditStep = { name: string; departmentId: string | null; levelEntries: EditLevelEntry[] }
|
type EditStep = { name: string; departmentId: string | null; levelEntries: EditLevelEntry[] }
|
||||||
|
|
||||||
@ -142,6 +147,7 @@ function copyFromDefinition(d: DefinitionDto): EditStep[] {
|
|||||||
allowReturnToDrafter: l.allowReturnToDrafter ?? true,
|
allowReturnToDrafter: l.allowReturnToDrafter ?? true,
|
||||||
allowApproverEditDetails: l.allowApproverEditDetails ?? false,
|
allowApproverEditDetails: l.allowApproverEditDetails ?? false,
|
||||||
allowApproverEditBudget: l.allowApproverEditBudget ?? false,
|
allowApproverEditBudget: l.allowApproverEditBudget ?? false,
|
||||||
|
allowApproverSkipToFinal: l.allowApproverSkipToFinal ?? false,
|
||||||
})),
|
})),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@ -158,6 +164,7 @@ function makeDefaultLevelEntry(order: LevelOrder, approverUserId: string): EditL
|
|||||||
allowReturnToDrafter: true,
|
allowReturnToDrafter: true,
|
||||||
allowApproverEditDetails: false,
|
allowApproverEditDetails: false,
|
||||||
allowApproverEditBudget: false,
|
allowApproverEditBudget: false,
|
||||||
|
allowApproverSkipToFinal: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -493,9 +500,9 @@ function Designer({
|
|||||||
const [description, setDescription] = useState(cloneFrom?.description ?? '')
|
const [description, setDescription] = useState(cloneFrom?.description ?? '')
|
||||||
const [steps, setSteps] = useState<EditStep[]>(initialSteps)
|
const [steps, setSteps] = useState<EditStep[]>(initialSteps)
|
||||||
|
|
||||||
// Mig 29 (S21 t5) — 6 Allow* options MOVED:
|
// Mig 29 (S21 t5) + Mig 30 (S22+5) + Mig 31 (S23 t1) — 7 Allow* options
|
||||||
// - 5 flag F1+F3 xuống per Level slot (xem EditLevelEntry, render mỗi Level row)
|
// ALL xuống per Level slot (xem EditLevelEntry, render inline mỗi Level row).
|
||||||
// - 1 flag F2 AllowDrafterSkipToFinal xuống per User (User Management page)
|
// Admin opt-in per-Approver-slot pattern proven 3× cumulative.
|
||||||
|
|
||||||
const usersList = useQuery({
|
const usersList = useQuery({
|
||||||
queryKey: ['users-for-approver-v2'],
|
queryKey: ['users-for-approver-v2'],
|
||||||
@ -550,6 +557,8 @@ function Designer({
|
|||||||
// Mỗi entry → 1 Level row. Multiple rows cùng Order = same Cấp với
|
// Mỗi entry → 1 Level row. Multiple rows cùng Order = same Cấp với
|
||||||
// N approvers (BE iterate group by Order).
|
// N approvers (BE iterate group by Order).
|
||||||
// Mig 29 (S21 t5) — 5 Allow* options per slot Approver.
|
// Mig 29 (S21 t5) — 5 Allow* options per slot Approver.
|
||||||
|
// Mig 30 (S22+5) — +AllowApproverEditBudget.
|
||||||
|
// Mig 31 (S23 t1) — +AllowApproverSkipToFinal F2 storage swap per-slot.
|
||||||
levels: s.levelEntries.map(e => ({
|
levels: s.levelEntries.map(e => ({
|
||||||
order: e.order,
|
order: e.order,
|
||||||
name: `Cấp ${e.order}`,
|
name: `Cấp ${e.order}`,
|
||||||
@ -560,6 +569,7 @@ function Designer({
|
|||||||
allowReturnToDrafter: e.allowReturnToDrafter,
|
allowReturnToDrafter: e.allowReturnToDrafter,
|
||||||
allowApproverEditDetails: e.allowApproverEditDetails,
|
allowApproverEditDetails: e.allowApproverEditDetails,
|
||||||
allowApproverEditBudget: e.allowApproverEditBudget,
|
allowApproverEditBudget: e.allowApproverEditBudget,
|
||||||
|
allowApproverSkipToFinal: e.allowApproverSkipToFinal,
|
||||||
})),
|
})),
|
||||||
})),
|
})),
|
||||||
})
|
})
|
||||||
@ -620,14 +630,13 @@ function Designer({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Mig 29 (S21 t5) — 6 Allow* options MOVED per-NV:
|
{/* Mig 29 (S21 t5) — 5 Allow* F1+F3 per slot Approver.
|
||||||
- 5 flag F1+F3 xuống mỗi Level row (xem level entry inline below).
|
Mig 30 (S22+5) — +AllowApproverEditBudget per slot.
|
||||||
- 1 flag F2 AllowDrafterSkipToFinal xuống Users page (System → Users).
|
Mig 31 (S23 t1) — F2 storage swap Users→Level: per-Approver-slot.
|
||||||
Section "Cấu hình nâng cao" workflow-level cũ Mig 28 đã DROP. */}
|
ALL Allow* options now configured PER NV trong slot Approver dưới đây. */}
|
||||||
<div className="rounded-lg border border-violet-200 bg-violet-50/30 px-3 py-2 text-[11px] leading-relaxed text-violet-800">
|
<div className="rounded-lg border border-violet-200 bg-violet-50/30 px-3 py-2 text-[11px] leading-relaxed text-violet-800">
|
||||||
ⓘ Cấu hình quyền duyệt (Trả lại modes + Edit Section 2) đặt RIÊNG cho từng NV ở mỗi
|
ⓘ Cấu hình quyền duyệt riêng cho từng NV trong slot Approver bên dưới
|
||||||
Cấp dưới đây. F2 "Gửi thẳng Cấp cuối" (Drafter) cấu hình ở
|
(Trả lại / Edit Section 2 / Edit Budget / Duyệt thẳng Cấp cuối).
|
||||||
<span className="font-medium"> User Management</span> (mỗi NV global).
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 rounded-lg border border-slate-200 p-3">
|
<div className="space-y-2 rounded-lg border border-slate-200 p-3">
|
||||||
@ -870,7 +879,7 @@ function Designer({
|
|||||||
className="ml-4 mt-1 rounded border border-amber-100 bg-amber-50/30 px-2 py-1.5"
|
className="ml-4 mt-1 rounded border border-amber-100 bg-amber-50/30 px-2 py-1.5"
|
||||||
>
|
>
|
||||||
<div className="mb-1 text-[10px] font-medium uppercase text-amber-700">
|
<div className="mb-1 text-[10px] font-medium uppercase text-amber-700">
|
||||||
Quyền duyệt NV #{ei + 1}
|
Quyền duyệt {usersList.data?.find(u => u.id === entry.approverUserId)?.fullName ?? 'Chưa chọn NV'}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-1">
|
<div className="grid grid-cols-2 gap-1">
|
||||||
<label className="flex items-center gap-1 text-[11px] text-slate-700">
|
<label className="flex items-center gap-1 text-[11px] text-slate-700">
|
||||||
@ -927,6 +936,17 @@ function Designer({
|
|||||||
/>
|
/>
|
||||||
<span>Cho phép chỉnh sửa Section ngân sách lúc đang duyệt</span>
|
<span>Cho phép chỉnh sửa Section ngân sách lúc đang duyệt</span>
|
||||||
</label>
|
</label>
|
||||||
|
{/* Mig 31 (S23 t1) — F2 AllowApproverSkipToFinal admin opt-in per-slot.
|
||||||
|
Approver tick = skip thẳng Cấp cuối khi đang ChoDuyet. */}
|
||||||
|
<label className="col-span-2 flex items-center gap-1 text-[11px] text-slate-700">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="h-3 w-3"
|
||||||
|
checked={entry.allowApproverSkipToFinal}
|
||||||
|
onChange={e => updateField('allowApproverSkipToFinal', e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>Cho phép duyệt thẳng Cấp cuối khi đang duyệt</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useState, type FormEvent } from 'react'
|
import { useState, type FormEvent } from 'react'
|
||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
import { Building2, KeyRound, Pencil, Plus, Shield, Unlock, Users, CheckCircle2, XCircle, ShieldCheck, FastForward } from 'lucide-react'
|
import { Building2, KeyRound, Pencil, Plus, Shield, Unlock, Users, CheckCircle2, XCircle, ShieldCheck } from 'lucide-react'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { PageHeader } from '@/components/PageHeader'
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
import { DataTable, Pagination, type Column } from '@/components/DataTable'
|
import { DataTable, Pagination, type Column } from '@/components/DataTable'
|
||||||
@ -175,18 +175,9 @@ export function UsersPage() {
|
|||||||
onError: err => toast.error(getErrorMessage(err)),
|
onError: err => toast.error(getErrorMessage(err)),
|
||||||
})
|
})
|
||||||
|
|
||||||
// F2 per-Drafter (Mig 29): toggle AllowDrafterSkipToFinal. Khi true, Drafter
|
// Mig 31 (S23 t1 Plan K Chunk D) — DELETED allowSkipMut. F2 semantic + storage
|
||||||
// được tick "Gửi thẳng Cấp cuối" trong PE Workspace để skip Bước/Cấp trung
|
// refactor sang ApprovalWorkflowLevels (per-Approver slot, admin opt-in qua
|
||||||
// gian và bay thẳng tới Cấp cuối workflow.
|
// Workflow Designer page). KHÔNG còn per-Drafter user toggle ở User Management.
|
||||||
const allowSkipMut = useMutation({
|
|
||||||
mutationFn: (u: User) =>
|
|
||||||
api.patch(`/users/${u.id}/allow-skip-final`, { allowDrafterSkipToFinal: !u.allowDrafterSkipToFinal }),
|
|
||||||
onSuccess: () => {
|
|
||||||
qc.invalidateQueries({ queryKey: ['users'] })
|
|
||||||
toast.success('Đã cập nhật quyền gửi thẳng Cấp cuối')
|
|
||||||
},
|
|
||||||
onError: err => toast.error(getErrorMessage(err)),
|
|
||||||
})
|
|
||||||
|
|
||||||
function nextPositionLevel(current: number | null): number | null {
|
function nextPositionLevel(current: number | null): number | null {
|
||||||
// Cycle null → 1 (NV) → 2 (PP) → 3 (TP) → null
|
// Cycle null → 1 (NV) → 2 (PP) → 3 (TP) → null
|
||||||
@ -302,21 +293,6 @@ export function UsersPage() {
|
|||||||
<span className="text-xs text-slate-400">—</span>
|
<span className="text-xs text-slate-400">—</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: 'allowDrafterSkipToFinal',
|
|
||||||
header: 'Skip cuối',
|
|
||||||
width: 'w-20',
|
|
||||||
align: 'center',
|
|
||||||
render: u =>
|
|
||||||
u.allowDrafterSkipToFinal ? (
|
|
||||||
<span title="Drafter được gửi PE thẳng Cấp cuối workflow (skip Bước/Cấp trung gian)" className="inline-flex items-center gap-1 rounded bg-violet-100 px-1.5 py-0.5 text-[10px] text-violet-700">
|
|
||||||
<FastForward className="h-3 w-3" />
|
|
||||||
skip
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-xs text-slate-400">—</span>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{ key: 'createdAt', header: 'Ngày tạo', width: 'w-24', render: u => fmtDate(u.createdAt) },
|
{ key: 'createdAt', header: 'Ngày tạo', width: 'w-24', render: u => fmtDate(u.createdAt) },
|
||||||
{
|
{
|
||||||
key: 'actions',
|
key: 'actions',
|
||||||
@ -362,14 +338,6 @@ export function UsersPage() {
|
|||||||
{u.positionLevel != null ? PositionLevelShort[u.positionLevel] : '—'}
|
{u.positionLevel != null ? PositionLevelShort[u.positionLevel] : '—'}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() => allowSkipMut.mutate(u)}
|
|
||||||
title={u.allowDrafterSkipToFinal ? 'Tắt quyền skip — Drafter phải tuần tự qua mọi Bước/Cấp' : 'Bật quyền skip — Drafter được gửi PE thẳng Cấp cuối workflow'}
|
|
||||||
>
|
|
||||||
<FastForward className={`h-3.5 w-3.5 ${u.allowDrafterSkipToFinal ? 'text-violet-600' : 'text-slate-400'}`} />
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="ghost" onClick={() => toggleActiveMut.mutate(u)} title={u.isActive ? 'Vô hiệu hóa' : 'Kích hoạt'}>
|
<Button size="sm" variant="ghost" onClick={() => toggleActiveMut.mutate(u)} title={u.isActive ? 'Vô hiệu hóa' : 'Kích hoạt'}>
|
||||||
{u.isActive ? <XCircle className="h-3.5 w-3.5 text-red-500" /> : <CheckCircle2 className="h-3.5 w-3.5 text-emerald-600" />}
|
{u.isActive ? <XCircle className="h-3.5 w-3.5 text-red-500" /> : <CheckCircle2 className="h-3.5 w-3.5 text-emerald-600" />}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -347,8 +347,9 @@ export type PeDepartmentApproval = {
|
|||||||
isBypassed: boolean
|
isBypassed: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mig 29 (S21 t5) — 5 Allow* options của Cấp Approver hiện tại (per-NV slot).
|
// Mig 29 (S21 t5) + Mig 30 (S22+5) + Mig 31 (S23 t1) — 7 Allow* options của
|
||||||
// F2 (Drafter skip) MOVED sang per-User (xem PeDetailBundle.drafterAllowSkipToFinal).
|
// Cấp Approver hiện tại (per-NV slot). F2 refactor sang Approver scope ChoDuyet
|
||||||
|
// (storage cũ Users.AllowDrafterSkipToFinal đã drop).
|
||||||
export type ApprovalWorkflowOptions = {
|
export type ApprovalWorkflowOptions = {
|
||||||
allowReturnOneLevel: boolean
|
allowReturnOneLevel: boolean
|
||||||
allowReturnOneStep: boolean
|
allowReturnOneStep: boolean
|
||||||
@ -356,6 +357,7 @@ export type ApprovalWorkflowOptions = {
|
|||||||
allowReturnToDrafter: boolean
|
allowReturnToDrafter: boolean
|
||||||
allowApproverEditDetails: boolean
|
allowApproverEditDetails: boolean
|
||||||
allowApproverEditBudget: boolean // Mig 30 (S22+5) — F4 Section ngân sách
|
allowApproverEditBudget: boolean // Mig 30 (S22+5) — F4 Section ngân sách
|
||||||
|
allowApproverSkipToFinal: boolean // Mig 31 (S23 t1) — F2 Approver duyệt thẳng Cấp cuối
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mig 28 (S21 t4) — F1 mode Trả lại payload gửi BE
|
// Mig 28 (S21 t4) — F1 mode Trả lại payload gửi BE
|
||||||
@ -398,10 +400,9 @@ export type PeDetailBundle = {
|
|||||||
approvalWorkflowCode: string | null
|
approvalWorkflowCode: string | null
|
||||||
approvalWorkflowName: string | null
|
approvalWorkflowName: string | null
|
||||||
approvalWorkflowVersion: number | null
|
approvalWorkflowVersion: number | null
|
||||||
// Mig 29 (S21 t5) — 5 Allow* options của Cấp hiện tại (per-NV slot).
|
// Mig 29 (S21 t5) + Mig 30 (S22+5) + Mig 31 (S23 t1) — 7 Allow* options của
|
||||||
|
// Cấp hiện tại (per-NV slot). Null nếu V1 legacy hoặc pointer chưa init.
|
||||||
currentLevelOptions: ApprovalWorkflowOptions | null
|
currentLevelOptions: ApprovalWorkflowOptions | null
|
||||||
// Mig 29 — F2 per-Drafter flag
|
|
||||||
drafterAllowSkipToFinal: boolean
|
|
||||||
// Mig 24 — info Bước/Cấp đang chờ duyệt (chỉ populate khi pin V2 + Phase=ChoDuyet)
|
// Mig 24 — info Bước/Cấp đang chờ duyệt (chỉ populate khi pin V2 + Phase=ChoDuyet)
|
||||||
currentApproval: PeCurrentApproval | null
|
currentApproval: PeCurrentApproval | null
|
||||||
// Mig 24 — full flow snapshot (Bước → Cấp → NV) với Status per level
|
// Mig 24 — full flow snapshot (Bước → Cấp → NV) với Status per level
|
||||||
|
|||||||
@ -11,7 +11,6 @@ export type User = {
|
|||||||
position: string | null
|
position: string | null
|
||||||
canBypassReview: boolean
|
canBypassReview: boolean
|
||||||
positionLevel: number | null // Mig 18 — 1=NV, 2=PP, 3=TP, null=admin/external
|
positionLevel: number | null // Mig 18 — 1=NV, 2=PP, 3=TP, null=admin/external
|
||||||
allowDrafterSkipToFinal: boolean // Mig 29 — F2: Drafter được gửi thẳng Cấp cuối workflow PE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cấp chức danh trong phòng (Mig 18) — phục vụ N-stage workflow inner step.
|
// Cấp chức danh trong phòng (Mig 18) — phục vụ N-stage workflow inner step.
|
||||||
|
|||||||
@ -117,24 +117,21 @@ export function PeDetailTabs({
|
|||||||
// itemsReadOnly = readOnly trừ khi approver mode F3 mở
|
// itemsReadOnly = readOnly trừ khi approver mode F3 mở
|
||||||
const itemsReadOnly = readOnly && !approverEditMode
|
const itemsReadOnly = readOnly && !approverEditMode
|
||||||
|
|
||||||
// Mig 29 (S21 t5) — F2: per-Drafter user flag (KHÔNG còn workflow-level).
|
// Mig 31 (S23 t1) — F2 Drafter-from-Nháp semantic deprecated. skipToFinal
|
||||||
// Admin cấu hình ở User Management page → BE resolve qua DrafterUserId.
|
// moved sang Approver scope ChoDuyet (per-Level slot — xem PeWorkflowPanel).
|
||||||
const [skipToFinal, setSkipToFinal] = useState(false)
|
// Drafter SUBMIT chạy normal init pointer Step 0 Cấp 1.
|
||||||
const allowSkipToFinal = evaluation.drafterAllowSkipToFinal ?? false
|
|
||||||
|
|
||||||
// "Lưu & Gửi Duyệt" workspace mode (user 2026-05-07): trigger transition
|
// "Lưu & Gửi Duyệt" workspace mode (user 2026-05-07): trigger transition
|
||||||
// sang phase tiếp theo (= Đã gửi duyệt). nextPhases[0] thường là ChoPurchasing
|
// sang phase tiếp theo (= Đã gửi duyệt). nextPhases[0] thường là ChoPurchasing
|
||||||
// (skip TuChoi). Sau success → toast + invalidate + onBack đóng workspace.
|
// (skip TuChoi). Sau success → toast + invalidate + onBack đóng workspace.
|
||||||
const submitForApproval = useMutation({
|
const submitForApproval = useMutation({
|
||||||
mutationFn: async (opts: { skipToFinal: boolean }) => {
|
mutationFn: async () => {
|
||||||
const next = evaluation.workflow.nextPhases.find(p => p !== PurchaseEvaluationPhase.TuChoi && p !== PurchaseEvaluationPhase.TraLai)
|
const next = evaluation.workflow.nextPhases.find(p => p !== PurchaseEvaluationPhase.TuChoi && p !== PurchaseEvaluationPhase.TraLai)
|
||||||
if (!next) throw new Error('Không có phase tiếp theo để gửi duyệt')
|
if (!next) throw new Error('Không có phase tiếp theo để gửi duyệt')
|
||||||
return api.post(`/purchase-evaluations/${evaluation.id}/transitions`, {
|
return api.post(`/purchase-evaluations/${evaluation.id}/transitions`, {
|
||||||
targetPhase: next,
|
targetPhase: next,
|
||||||
decision: 1,
|
decision: 1,
|
||||||
comment: null,
|
comment: null,
|
||||||
// Mig 28 (S21 t4) — F2: Drafter skip thẳng Cấp cuối
|
|
||||||
skipToFinal: opts.skipToFinal,
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@ -289,34 +286,19 @@ export function PeDetailTabs({
|
|||||||
>
|
>
|
||||||
Lưu
|
Lưu
|
||||||
</Button>
|
</Button>
|
||||||
{/* Mig 28 (S21 t4) — F2: Drafter skip thẳng Cấp cuối checkbox.
|
|
||||||
Chỉ hiện khi workflow.AllowDrafterSkipToFinal=true. */}
|
|
||||||
{allowSkipToFinal && canSubmitForApproval && (
|
|
||||||
<label className="flex cursor-pointer items-center gap-1.5 rounded border border-violet-200 bg-violet-50 px-2 py-1 text-[11px] text-violet-800">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="h-3 w-3"
|
|
||||||
checked={skipToFinal}
|
|
||||||
onChange={e => setSkipToFinal(e.target.checked)}
|
|
||||||
/>
|
|
||||||
<span>Gửi thẳng Cấp cuối (skip trung gian)</span>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!forwardPhase) return
|
if (!forwardPhase) return
|
||||||
const confirmMsg = skipToFinal
|
const confirmMsg = `Gửi phiếu vào quy trình duyệt? Sẽ chuyển sang "${PurchaseEvaluationPhaseLabel[forwardPhase]}". Sau khi gửi sẽ KHÔNG sửa được nữa (trừ khi approver Trả lại).`
|
||||||
? `Gửi THẲNG CẤP CUỐI bỏ qua các Cấp trung gian? Hệ thống sẽ ghi audit "Drafter skip" — không quay lại được trừ khi approver Trả lại.`
|
|
||||||
: `Gửi phiếu vào quy trình duyệt? Sẽ chuyển sang "${PurchaseEvaluationPhaseLabel[forwardPhase]}". Sau khi gửi sẽ KHÔNG sửa được nữa (trừ khi approver Trả lại).`
|
|
||||||
if (confirm(confirmMsg)) {
|
if (confirm(confirmMsg)) {
|
||||||
submitForApproval.mutate({ skipToFinal })
|
submitForApproval.mutate()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={!canSubmitForApproval || submitForApproval.isPending}
|
disabled={!canSubmitForApproval || submitForApproval.isPending}
|
||||||
title={submitDisabledReason ?? `Gửi phiếu sang "${forwardPhase ? PurchaseEvaluationPhaseLabel[forwardPhase] : '?'}"`}
|
title={submitDisabledReason ?? `Gửi phiếu sang "${forwardPhase ? PurchaseEvaluationPhaseLabel[forwardPhase] : '?'}"`}
|
||||||
className="text-xs"
|
className="text-xs"
|
||||||
>
|
>
|
||||||
{submitForApproval.isPending ? 'Đang gửi…' : skipToFinal ? 'Lưu & Gửi thẳng CẤP CUỐI →' : 'Lưu & Gửi Duyệt →'}
|
{submitForApproval.isPending ? 'Đang gửi…' : 'Lưu & Gửi Duyệt →'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -37,6 +37,9 @@ export function PeWorkflowPanel({
|
|||||||
// Mig 28 (S21 t4) — F1 mode Trả lại. Default Drafter (backward compat S17).
|
// Mig 28 (S21 t4) — F1 mode Trả lại. Default Drafter (backward compat S17).
|
||||||
const [returnMode, setReturnMode] = useState<WorkflowReturnMode>(WorkflowReturnMode.Drafter)
|
const [returnMode, setReturnMode] = useState<WorkflowReturnMode>(WorkflowReturnMode.Drafter)
|
||||||
const [returnTargetUserId, setReturnTargetUserId] = useState<string | null>(null)
|
const [returnTargetUserId, setReturnTargetUserId] = useState<string | null>(null)
|
||||||
|
// Mig 31 (S23 t1) — F2 Approver duyệt thẳng Cấp cuối. Default false (admin opt-in
|
||||||
|
// per slot tick → checkbox visible trong dialog Approve, default unchecked).
|
||||||
|
const [skipToFinalApprover, setSkipToFinalApprover] = useState(false)
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const { user: currentUser } = useAuth()
|
const { user: currentUser } = useAuth()
|
||||||
const isAdmin = currentUser?.roles?.includes('Admin') ?? false
|
const isAdmin = currentUser?.roles?.includes('Admin') ?? false
|
||||||
@ -96,6 +99,9 @@ export function PeWorkflowPanel({
|
|||||||
returnMode: isTraLaiAction ? returnMode : null,
|
returnMode: isTraLaiAction ? returnMode : null,
|
||||||
returnTargetUserId: isTraLaiAction && returnMode === WorkflowReturnMode.Assignee
|
returnTargetUserId: isTraLaiAction && returnMode === WorkflowReturnMode.Assignee
|
||||||
? returnTargetUserId : null,
|
? returnTargetUserId : null,
|
||||||
|
// Mig 31 (S23 t1) — F2 Approver scope ChoDuyet duyệt thẳng Cấp cuối.
|
||||||
|
// BE check matchingLevel.AllowApproverSkipToFinal (admin opt-in per slot).
|
||||||
|
skipToFinal: !isReject && skipToFinalApprover,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@ -107,6 +113,7 @@ export function PeWorkflowPanel({
|
|||||||
setComment('')
|
setComment('')
|
||||||
setReturnMode(WorkflowReturnMode.Drafter)
|
setReturnMode(WorkflowReturnMode.Drafter)
|
||||||
setReturnTargetUserId(null)
|
setReturnTargetUserId(null)
|
||||||
|
setSkipToFinalApprover(false)
|
||||||
},
|
},
|
||||||
onError: e => toast.error(getErrorMessage(e)),
|
onError: e => toast.error(getErrorMessage(e)),
|
||||||
})
|
})
|
||||||
@ -394,6 +401,32 @@ export function PeWorkflowPanel({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{/* Mig 31 (S23 t1) — F2 Approver toggle: chỉ visible khi Approve forward
|
||||||
|
+ admin tick AllowApproverSkipToFinal cho slot Cấp hiện tại. */}
|
||||||
|
{!isCancel && !isSendBack && levelOptions?.allowApproverSkipToFinal && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<label className="flex cursor-pointer items-start gap-2 rounded border border-violet-200 bg-violet-50 px-3 py-2 text-[12px] text-violet-800 hover:bg-violet-100/60">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mt-0.5"
|
||||||
|
checked={skipToFinalApprover}
|
||||||
|
onChange={e => setSkipToFinalApprover(e.target.checked)}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<span className="font-medium">Duyệt thẳng Cấp cuối (skip Bước/Cấp trung gian)</span>
|
||||||
|
<span className="mt-0.5 block text-[11px] text-violet-700/80">
|
||||||
|
Phiếu sẽ tiến thẳng tới "Đã duyệt" (terminal) — bỏ qua mọi Cấp/Bước còn lại.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isCancel && !isSendBack && skipToFinalApprover && (
|
||||||
|
<div className="mb-3 rounded border border-amber-300 bg-amber-50 px-3 py-2 text-[11px] text-amber-800">
|
||||||
|
⚠ Hành động KHÔNG quay lại được (trừ khi Drafter reset toàn bộ). Phiếu sẽ
|
||||||
|
skip qua tất cả Cấp/Bước còn lại và chuyển thẳng "Đã duyệt".
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<Label>Ghi chú (tùy chọn)</Label>
|
<Label>Ghi chú (tùy chọn)</Label>
|
||||||
<Textarea value={comment} onChange={e => setComment(e.target.value)} rows={3} />
|
<Textarea value={comment} onChange={e => setComment(e.target.value)} rows={3} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@ -354,6 +354,7 @@ export type ApprovalWorkflowOptions = {
|
|||||||
allowReturnToDrafter: boolean
|
allowReturnToDrafter: boolean
|
||||||
allowApproverEditDetails: boolean
|
allowApproverEditDetails: boolean
|
||||||
allowApproverEditBudget: boolean // Mig 30 (S22+5) — F4 Section ngân sách
|
allowApproverEditBudget: boolean // Mig 30 (S22+5) — F4 Section ngân sách
|
||||||
|
allowApproverSkipToFinal: boolean // Mig 31 (S23 t1) — F2 Approver duyệt thẳng Cấp cuối
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mig 28 (S21 t4) — F1 mode Trả lại payload gửi BE
|
// Mig 28 (S21 t4) — F1 mode Trả lại payload gửi BE
|
||||||
@ -396,13 +397,11 @@ export type PeDetailBundle = {
|
|||||||
approvalWorkflowCode: string | null
|
approvalWorkflowCode: string | null
|
||||||
approvalWorkflowName: string | null
|
approvalWorkflowName: string | null
|
||||||
approvalWorkflowVersion: number | null
|
approvalWorkflowVersion: number | null
|
||||||
// Mig 29 (S21 t5) — 5 Allow* options của Cấp hiện tại (per-NV slot). Null
|
// Mig 29 (S21 t5) + Mig 30 (S22+5) + Mig 31 (S23 t1) — 7 Allow* options của
|
||||||
// nếu V1 legacy hoặc pointer chưa init. FE render Trả lại dropdown + Edit
|
// Cấp hiện tại (per-NV slot). Null nếu V1 legacy hoặc pointer chưa init. FE
|
||||||
// Section 2 conditional theo flag của slot Approver đang duyệt.
|
// render Trả lại dropdown + Edit Section 2/4 + Duyệt thẳng Cấp cuối
|
||||||
|
// conditional theo flag của slot Approver đang duyệt.
|
||||||
currentLevelOptions: ApprovalWorkflowOptions | null
|
currentLevelOptions: ApprovalWorkflowOptions | null
|
||||||
// Mig 29 — F2 per-Drafter: cờ AllowDrafterSkipToFinal của Drafter user pin
|
|
||||||
// phiếu. Workspace conditional render checkbox "Gửi thẳng Cấp cuối".
|
|
||||||
drafterAllowSkipToFinal: boolean
|
|
||||||
// Mig 24 — info Bước/Cấp đang chờ duyệt (chỉ populate khi pin V2 + Phase=ChoDuyet)
|
// Mig 24 — info Bước/Cấp đang chờ duyệt (chỉ populate khi pin V2 + Phase=ChoDuyet)
|
||||||
currentApproval: PeCurrentApproval | null
|
currentApproval: PeCurrentApproval | null
|
||||||
// Mig 24 — full flow snapshot (Bước → Cấp → NV) với Status per level
|
// Mig 24 — full flow snapshot (Bước → Cấp → NV) với Status per level
|
||||||
|
|||||||
@ -85,21 +85,13 @@ public class UsersController(IMediator mediator) : ControllerBase
|
|||||||
return NoContent();
|
return NoContent();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mig 29 F2 per-Drafter: admin toggle AllowDrafterSkipToFinal cho user. Khi
|
// Mig 31 (S23 t1 Plan K Chunk D) — DELETED PATCH F2 endpoint per-Drafter.
|
||||||
// true, Drafter có thể tick "Gửi thẳng Cấp cuối" trong PE Workspace để bay
|
// F2 semantic + storage refactor sang ApprovalWorkflowLevels per-Approver
|
||||||
// thẳng tới Cấp cuối workflow. Mặc định false.
|
// slot (AllowApproverSkipToFinal, admin opt-in qua Workflow Designer).
|
||||||
[HttpPatch("{id:guid}/allow-skip-final")]
|
// Plan K backout zombie endpoint NoOp.
|
||||||
[Authorize(Policy = "Users.Update")]
|
|
||||||
public async Task<IActionResult> SetAllowDrafterSkipToFinal(
|
|
||||||
Guid id, [FromBody] SetAllowDrafterSkipToFinalBody body, CancellationToken ct)
|
|
||||||
{
|
|
||||||
await mediator.Send(new SetUserAllowDrafterSkipToFinalCommand(id, body.AllowDrafterSkipToFinal), ct);
|
|
||||||
return NoContent();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public record AssignRolesBody(List<string> Roles);
|
public record AssignRolesBody(List<string> Roles);
|
||||||
public record ResetPasswordBody(string NewPassword);
|
public record ResetPasswordBody(string NewPassword);
|
||||||
public record SetBypassReviewBody(bool CanBypassReview);
|
public record SetBypassReviewBody(bool CanBypassReview);
|
||||||
public record SetPositionLevelBody(int? PositionLevel);
|
public record SetPositionLevelBody(int? PositionLevel);
|
||||||
public record SetAllowDrafterSkipToFinalBody(bool AllowDrafterSkipToFinal);
|
|
||||||
|
|||||||
@ -55,9 +55,10 @@ public record AwDefinitionDto(
|
|||||||
string? Description,
|
string? Description,
|
||||||
bool IsActive,
|
bool IsActive,
|
||||||
bool IsUserSelectable,
|
bool IsUserSelectable,
|
||||||
// Mig 29 (S21 t5) — 6 advanced options đã MOVE per-NV: 5 flag (F1+F3) xuống
|
// Mig 29 (S21 t5) — 6 advanced options đã MOVE per-NV: F1+F3 xuống AwLevelDto
|
||||||
// AwLevelDto (per slot Approver), F2 AllowDrafterSkipToFinal xuống User table
|
// (per slot Approver). Workflow-level Mig 28 dropped.
|
||||||
// (per-Drafter user). Workflow-level Mig 28 dropped.
|
// Mig 31 (S23 t1) — F2 cũng refactor xuống Level slot (AllowApproverSkipToFinal
|
||||||
|
// trong AwLevelDto) per-Approver scope ChoDuyet — KHÔNG còn per-Drafter user scope.
|
||||||
DateTime? ActivatedAt,
|
DateTime? ActivatedAt,
|
||||||
DateTime CreatedAt,
|
DateTime CreatedAt,
|
||||||
List<AwStepDto> Steps);
|
List<AwStepDto> Steps);
|
||||||
|
|||||||
@ -81,15 +81,18 @@ public record PurchaseEvaluationChangelogDto(
|
|||||||
// Mig 29 (S21 t5) — Approver options của slot Level hiện tại (per-NV).
|
// Mig 29 (S21 t5) — Approver options của slot Level hiện tại (per-NV).
|
||||||
// FE eOffice filter Trả lại dropdown + Edit Section 2 enabled theo flag của
|
// FE eOffice filter Trả lại dropdown + Edit Section 2 enabled theo flag của
|
||||||
// Cấp hiện tại NV đang duyệt. Null nếu phiếu V1 legacy hoặc không ChoDuyet.
|
// Cấp hiện tại NV đang duyệt. Null nếu phiếu V1 legacy hoặc không ChoDuyet.
|
||||||
// F2 (Drafter skip) đã move sang `PeDetailBundleDto.DrafterAllowSkipToFinal`.
|
|
||||||
// Mig 30 (S22+5) — F4 +AllowApproverEditBudget cho Section "Điều chỉnh ngân sách".
|
// Mig 30 (S22+5) — F4 +AllowApproverEditBudget cho Section "Điều chỉnh ngân sách".
|
||||||
|
// Mig 31 (S23 t1) — F2 refactor sang Approver scope ChoDuyet: +AllowApproverSkipToFinal
|
||||||
|
// cho phép Approver duyệt thẳng Cấp cuối (admin opt-in per slot). Storage cũ
|
||||||
|
// trên Users table đã drop, semantic Drafter-from-Nháp deprecated.
|
||||||
public record ApprovalWorkflowOptionsDto(
|
public record ApprovalWorkflowOptionsDto(
|
||||||
bool AllowReturnOneLevel,
|
bool AllowReturnOneLevel,
|
||||||
bool AllowReturnOneStep,
|
bool AllowReturnOneStep,
|
||||||
bool AllowReturnToAssignee,
|
bool AllowReturnToAssignee,
|
||||||
bool AllowReturnToDrafter,
|
bool AllowReturnToDrafter,
|
||||||
bool AllowApproverEditDetails,
|
bool AllowApproverEditDetails,
|
||||||
bool AllowApproverEditBudget);
|
bool AllowApproverEditBudget,
|
||||||
|
bool AllowApproverSkipToFinal);
|
||||||
|
|
||||||
public record PurchaseEvaluationWorkflowSummaryDto(
|
public record PurchaseEvaluationWorkflowSummaryDto(
|
||||||
string PolicyName,
|
string PolicyName,
|
||||||
@ -207,14 +210,12 @@ public record PurchaseEvaluationDetailBundleDto(
|
|||||||
string? ApprovalWorkflowCode,
|
string? ApprovalWorkflowCode,
|
||||||
string? ApprovalWorkflowName,
|
string? ApprovalWorkflowName,
|
||||||
int? ApprovalWorkflowVersion,
|
int? ApprovalWorkflowVersion,
|
||||||
// Mig 29 (S21 t5) — 5 Allow* options của Cấp hiện tại (per-NV slot). Null
|
// Mig 29 (S21 t5) + Mig 30 (S22+5) + Mig 31 (S23 t1) — 7 Allow* options
|
||||||
// nếu V1 legacy hoặc không ChoDuyet. FE render Trả lại dropdown + Edit
|
// của Cấp hiện tại (per-NV slot). Null nếu V1 legacy hoặc không ChoDuyet.
|
||||||
// Section 2 conditional. Field rename "WorkflowOptions" → "CurrentLevelOptions"
|
// FE render Trả lại dropdown + Edit Section 2 + Section 4 ngân sách +
|
||||||
// để rõ semantic per-slot không phải workflow-wide.
|
// Duyệt thẳng Cấp cuối conditional. Field rename "WorkflowOptions" →
|
||||||
|
// "CurrentLevelOptions" để rõ semantic per-slot không phải workflow-wide.
|
||||||
ApprovalWorkflowOptionsDto? CurrentLevelOptions,
|
ApprovalWorkflowOptionsDto? CurrentLevelOptions,
|
||||||
// Mig 29 — F2 per-Drafter: cờ AllowDrafterSkipToFinal của Drafter user pin
|
|
||||||
// phiếu. Workspace conditional render checkbox "Gửi thẳng Cấp cuối".
|
|
||||||
bool DrafterAllowSkipToFinal,
|
|
||||||
PurchaseEvaluationCurrentApprovalDto? CurrentApproval,
|
PurchaseEvaluationCurrentApprovalDto? CurrentApproval,
|
||||||
PurchaseEvaluationApprovalFlowDto? ApprovalFlow,
|
PurchaseEvaluationApprovalFlowDto? ApprovalFlow,
|
||||||
List<PurchaseEvaluationSupplierDto> Suppliers,
|
List<PurchaseEvaluationSupplierDto> Suppliers,
|
||||||
|
|||||||
@ -398,7 +398,8 @@ public record TransitionPurchaseEvaluationCommand(
|
|||||||
// Mig 28 (S21 t4) — F1 mode Trả lại (optional, null = default Drafter)
|
// Mig 28 (S21 t4) — F1 mode Trả lại (optional, null = default Drafter)
|
||||||
WorkflowReturnMode? ReturnMode = null,
|
WorkflowReturnMode? ReturnMode = null,
|
||||||
Guid? ReturnTargetUserId = null,
|
Guid? ReturnTargetUserId = null,
|
||||||
// F2 — Drafter skip thẳng Cấp cuối khi trình duyệt (optional, default false)
|
// F2 — Approver skip thẳng Cấp cuối lúc duyệt ChoDuyet (Mig 31 admin opt-in
|
||||||
|
// per slot, AllowApproverSkipToFinal). Default false.
|
||||||
bool SkipToFinal = false) : IRequest;
|
bool SkipToFinal = false) : IRequest;
|
||||||
|
|
||||||
public class TransitionPurchaseEvaluationCommandValidator : AbstractValidator<TransitionPurchaseEvaluationCommand>
|
public class TransitionPurchaseEvaluationCommandValidator : AbstractValidator<TransitionPurchaseEvaluationCommand>
|
||||||
@ -728,15 +729,6 @@ public class GetPurchaseEvaluationQueryHandler(
|
|||||||
// hiển thị FE detail card "QT-DN-V2-001 - Tên (v01)").
|
// hiển thị FE detail card "QT-DN-V2-001 - Tên (v01)").
|
||||||
// Mig 24 — populate CurrentApproval (cấp hiện tại) + ApprovalFlow (full
|
// Mig 24 — populate CurrentApproval (cấp hiện tại) + ApprovalFlow (full
|
||||||
// Bước/Cấp tree với Status) cho FE render flow vertical thay phase cards.
|
// Bước/Cấp tree với Status) cho FE render flow vertical thay phase cards.
|
||||||
// Mig 29 (S21 t5) — F2 drafter flag từ User entity (per-Drafter user
|
|
||||||
// AllowDrafterSkipToFinal). Default false nếu DrafterUserId null.
|
|
||||||
var drafterAllowSkipToFinal = false;
|
|
||||||
if (e.DrafterUserId is Guid drafterId)
|
|
||||||
{
|
|
||||||
var drafterUser = await userManager.Users.AsNoTracking()
|
|
||||||
.FirstOrDefaultAsync(u => u.Id == drafterId, ct);
|
|
||||||
drafterAllowSkipToFinal = drafterUser?.AllowDrafterSkipToFinal ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
string? awCode = null, awName = null;
|
string? awCode = null, awName = null;
|
||||||
int? awVersion = null;
|
int? awVersion = null;
|
||||||
@ -755,8 +747,9 @@ public class GetPurchaseEvaluationQueryHandler(
|
|||||||
awName = aw.Name;
|
awName = aw.Name;
|
||||||
awVersion = aw.Version;
|
awVersion = aw.Version;
|
||||||
|
|
||||||
// Mig 29 (S21 t5) — Resolve Cấp hiện tại + populate 5 Allow* flag
|
// Mig 29 (S21 t5) + Mig 30 (S22+5) + Mig 31 (S23 t1) — Resolve
|
||||||
// của slot Approver đang duyệt. Null nếu pointer chưa init.
|
// Cấp hiện tại + populate 7 Allow* flag của slot Approver đang
|
||||||
|
// duyệt. Null nếu pointer chưa init.
|
||||||
if (e.CurrentWorkflowStepIndex is int curStepIdx
|
if (e.CurrentWorkflowStepIndex is int curStepIdx
|
||||||
&& curStepIdx >= 0 && curStepIdx < aw.Steps.Count
|
&& curStepIdx >= 0 && curStepIdx < aw.Steps.Count
|
||||||
&& e.CurrentApprovalLevelOrder is int curLevelOrder)
|
&& e.CurrentApprovalLevelOrder is int curLevelOrder)
|
||||||
@ -771,7 +764,8 @@ public class GetPurchaseEvaluationQueryHandler(
|
|||||||
curLevel.AllowReturnToAssignee,
|
curLevel.AllowReturnToAssignee,
|
||||||
curLevel.AllowReturnToDrafter,
|
curLevel.AllowReturnToDrafter,
|
||||||
curLevel.AllowApproverEditDetails,
|
curLevel.AllowApproverEditDetails,
|
||||||
curLevel.AllowApproverEditBudget);
|
curLevel.AllowApproverEditBudget,
|
||||||
|
curLevel.AllowApproverSkipToFinal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -894,8 +888,6 @@ public class GetPurchaseEvaluationQueryHandler(
|
|||||||
e.BudgetId, budgetSummary,
|
e.BudgetId, budgetSummary,
|
||||||
e.BudgetManualName, e.BudgetManualAmount,
|
e.BudgetManualName, e.BudgetManualAmount,
|
||||||
e.ApprovalWorkflowId, awCode, awName, awVersion, currentLevelOptions,
|
e.ApprovalWorkflowId, awCode, awName, awVersion, currentLevelOptions,
|
||||||
// Mig 29 (S21 t5) — F2 drafter flag từ User entity
|
|
||||||
drafterAllowSkipToFinal,
|
|
||||||
currentApproval, approvalFlow,
|
currentApproval, approvalFlow,
|
||||||
e.Suppliers
|
e.Suppliers
|
||||||
.OrderBy(s => s.Order)
|
.OrderBy(s => s.Order)
|
||||||
|
|||||||
@ -8,13 +8,15 @@ public interface IPurchaseEvaluationWorkflowService
|
|||||||
// Kiểm tra + thực hiện transition. Throw ForbiddenException nếu không hợp lệ.
|
// Kiểm tra + thực hiện transition. Throw ForbiddenException nếu không hợp lệ.
|
||||||
// Tự tạo PurchaseEvaluationApproval + update Phase + SlaDeadline.
|
// Tự tạo PurchaseEvaluationApproval + update Phase + SlaDeadline.
|
||||||
//
|
//
|
||||||
// Optional params Mig 28 (S21 t4 — F1+F2 advanced workflow options):
|
// Optional params Mig 28-31 (S21-S23 advanced workflow options per-NV slot):
|
||||||
// - returnMode: mode Trả lại (F1). Null = default Drafter behavior khi Reject+TraLai.
|
// - returnMode: mode Trả lại (F1). Null = default Drafter behavior khi Reject+TraLai.
|
||||||
// OneLevel/OneStep/Assignee → giữ Phase=ChoDuyet, lùi pointer (peer review).
|
// OneLevel/OneStep/Assignee → giữ Phase=ChoDuyet, lùi pointer (peer review).
|
||||||
// Drafter → Phase=TraLai clear pointer như S17.
|
// Drafter → Phase=TraLai clear pointer như S17. Flag check tại level.Allow*.
|
||||||
// - returnTargetUserId: required khi returnMode=Assignee — pick từ list NV đã duyệt.
|
// - returnTargetUserId: required khi returnMode=Assignee — pick từ list NV đã duyệt.
|
||||||
// - skipToFinal: F2 Drafter trình duyệt → skip mọi Bước/Cấp trung gian, set pointer
|
// - skipToFinal: F2 Approver during ChoDuyet duyệt thẳng Cấp cuối → set Phase=DaDuyet
|
||||||
// = max Step + max Level. Workflow phải AllowDrafterSkipToFinal=true.
|
// terminal trực tiếp, clear pointer. Mig 31 (S23 t1) refactor sang Approver scope:
|
||||||
|
// matchingLevel.AllowApproverSkipToFinal phải true (admin opt-in per slot).
|
||||||
|
// Semantic cũ Drafter-from-Nháp đã deprecated + storage cũ trên Users table đã drop.
|
||||||
Task TransitionAsync(
|
Task TransitionAsync(
|
||||||
PurchaseEvaluation evaluation,
|
PurchaseEvaluation evaluation,
|
||||||
PurchaseEvaluationPhase targetPhase,
|
PurchaseEvaluationPhase targetPhase,
|
||||||
|
|||||||
@ -22,8 +22,7 @@ public record UserDto(
|
|||||||
string? DepartmentName,
|
string? DepartmentName,
|
||||||
string? Position,
|
string? Position,
|
||||||
bool CanBypassReview,
|
bool CanBypassReview,
|
||||||
int? PositionLevel, // Mig 18 — 1=NV, 2=PP, 3=TP. Null cho admin/system/external user.
|
int? PositionLevel); // Mig 18 — 1=NV, 2=PP, 3=TP. Null cho admin/system/external user.
|
||||||
bool AllowDrafterSkipToFinal); // Mig 29 — F2 per-Drafter: cho phép Drafter gửi thẳng Cấp cuối khi tạo PE.
|
|
||||||
|
|
||||||
// ========== LIST ==========
|
// ========== LIST ==========
|
||||||
public record ListUsersQuery : PagedRequest, IRequest<PagedResult<UserDto>>;
|
public record ListUsersQuery : PagedRequest, IRequest<PagedResult<UserDto>>;
|
||||||
@ -62,7 +61,7 @@ public class ListUsersQueryHandler(UserManager<User> userManager, IApplicationDb
|
|||||||
var roles = await userManager.GetRolesAsync(u);
|
var roles = await userManager.GetRolesAsync(u);
|
||||||
var isLocked = u.LockoutEnd.HasValue && u.LockoutEnd.Value.UtcDateTime > now;
|
var isLocked = u.LockoutEnd.HasValue && u.LockoutEnd.Value.UtcDateTime > now;
|
||||||
string? deptName = u.DepartmentId is { } did && deptNames.TryGetValue(did, out var dn) ? dn : null;
|
string? deptName = u.DepartmentId is { } did && deptNames.TryGetValue(did, out var dn) ? dn : null;
|
||||||
items.Add(new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview, (int?)u.PositionLevel, u.AllowDrafterSkipToFinal));
|
items.Add(new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview, (int?)u.PositionLevel));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PagedResult<UserDto>(items, total, request.Page, request.PageSize);
|
return new PagedResult<UserDto>(items, total, request.Page, request.PageSize);
|
||||||
@ -84,7 +83,7 @@ public class GetUserQueryHandler(UserManager<User> userManager, IApplicationDbCo
|
|||||||
string? deptName = null;
|
string? deptName = null;
|
||||||
if (u.DepartmentId is { } did)
|
if (u.DepartmentId is { } did)
|
||||||
deptName = await db.Departments.AsNoTracking().Where(d => d.Id == did).Select(d => d.Name).FirstOrDefaultAsync(ct);
|
deptName = await db.Departments.AsNoTracking().Where(d => d.Id == did).Select(d => d.Name).FirstOrDefaultAsync(ct);
|
||||||
return new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview, (int?)u.PositionLevel, u.AllowDrafterSkipToFinal);
|
return new UserDto(u.Id, u.Email!, u.FullName, u.IsActive, isLocked, u.CreatedAt, roles.ToList(), u.DepartmentId, deptName, u.Position, u.CanBypassReview, (int?)u.PositionLevel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,23 +323,7 @@ public class SetUserPositionLevelCommandHandler(UserManager<User> userManager)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== SET ALLOW DRAFTER SKIP TO FINAL (Mig 29 — F2 per-Drafter) ==========
|
// Mig 31 (S23 t1 Plan K Chunk D) — DELETED F2 Command + Handler per-Drafter.
|
||||||
// Admin toggle AllowDrafterSkipToFinal cho 1 user. Khi true, user (Drafter) được
|
// F2 semantic + storage refactor sang ApprovalWorkflowLevels (per-Approver
|
||||||
// dùng checkbox "Gửi thẳng Cấp cuối" trong PE Workspace để skip toàn bộ Bước/Cấp
|
// slot, admin opt-in qua Workflow Designer). Plan K backout zombie Command.
|
||||||
// trung gian và bay thẳng tới Cấp cuối. Mặc định false (an toàn — Drafter phải
|
// Replaced bởi Workflow Designer per-Level toggle trong ApprovalWorkflowV2AdminFeatures.
|
||||||
// tuần tự qua mọi Bước/Cấp theo workflow).
|
|
||||||
public record SetUserAllowDrafterSkipToFinalCommand(Guid Id, bool AllowDrafterSkipToFinal) : IRequest;
|
|
||||||
|
|
||||||
public class SetUserAllowDrafterSkipToFinalCommandHandler(UserManager<User> userManager)
|
|
||||||
: IRequestHandler<SetUserAllowDrafterSkipToFinalCommand>
|
|
||||||
{
|
|
||||||
public async Task Handle(SetUserAllowDrafterSkipToFinalCommand request, CancellationToken ct)
|
|
||||||
{
|
|
||||||
var user = await userManager.FindByIdAsync(request.Id.ToString())
|
|
||||||
?? throw new NotFoundException("User", request.Id);
|
|
||||||
user.AllowDrafterSkipToFinal = request.AllowDrafterSkipToFinal;
|
|
||||||
var result = await userManager.UpdateAsync(user);
|
|
||||||
if (!result.Succeeded)
|
|
||||||
throw new ConflictException(string.Join("; ", result.Errors.Select(e => e.Description)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -75,8 +75,9 @@ public class ApprovalWorkflowLevel : BaseEntity
|
|||||||
// tick stick per Level row (KHÔNG còn workflow-level cũ Mig 28).
|
// tick stick per Level row (KHÔNG còn workflow-level cũ Mig 28).
|
||||||
//
|
//
|
||||||
// F1 (4 mode Trả lại) + F3 (Edit Section 2) = quyền của Approver Level.
|
// F1 (4 mode Trả lại) + F3 (Edit Section 2) = quyền của Approver Level.
|
||||||
// F2 (Drafter skip) đã move sang Users.AllowDrafterSkipToFinal (per-Drafter
|
// F2 đã move xuống Level slot này (Mig 31 — xem prop AllowApproverSkipToFinal).
|
||||||
// user — không liên quan slot Approver).
|
// Semantic mới: Approver during ChoDuyet skip thẳng Cấp cuối (admin opt-in
|
||||||
|
// per slot, KHÔNG còn per-Drafter user scope cũ).
|
||||||
//
|
//
|
||||||
// Backfill Mig 29: copy từ workflow-level Allow* cũ → all Levels của workflow.
|
// Backfill Mig 29: copy từ workflow-level Allow* cũ → all Levels của workflow.
|
||||||
// Default backward compat: AllowReturnToDrafter=true (S17 fallback). 4 flag
|
// Default backward compat: AllowReturnToDrafter=true (S17 fallback). 4 flag
|
||||||
@ -104,5 +105,13 @@ public class ApprovalWorkflowLevel : BaseEntity
|
|||||||
/// Nháp/Trả lại — flag này CHỈ mở thêm scope cho Approver ChoDuyet.
|
/// Nháp/Trả lại — flag này CHỈ mở thêm scope cho Approver ChoDuyet.
|
||||||
public bool AllowApproverEditBudget { get; set; }
|
public bool AllowApproverEditBudget { get; set; }
|
||||||
|
|
||||||
|
/// F2 (Mig 31 — S23 t1 Plan K) — REFACTOR semantic + storage từ per-Drafter
|
||||||
|
/// (cũ Mig 29 ở Users table, đã DROP) sang per-Approver slot. Cho phép NV slot
|
||||||
|
/// này (khi đang duyệt ChoDuyet) Approve skip thẳng Cấp cuối, bỏ qua mọi
|
||||||
|
/// Bước/Cấp trung gian còn lại. Default false (admin opt-in per slot).
|
||||||
|
/// Mirror F3+F4 admin opt-in per-Approver pattern (Mig 29 + Mig 30)
|
||||||
|
/// reinforced 3× cumulative. NO BACKFILL — 4 prod user lose value cũ per bro Option A.
|
||||||
|
public bool AllowApproverSkipToFinal { get; set; }
|
||||||
|
|
||||||
public ApprovalWorkflowStep? Step { get; set; }
|
public ApprovalWorkflowStep? Step { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,12 +28,9 @@ public class User : IdentityUser<Guid>
|
|||||||
// được sub-step đó. Null cho admin/system/external user.
|
// được sub-step đó. Null cho admin/system/external user.
|
||||||
public PositionLevel? PositionLevel { get; set; }
|
public PositionLevel? PositionLevel { get; set; }
|
||||||
|
|
||||||
// Mig 29 (Session 21 turn 5) — F2 per-Drafter: cho phép user này (khi đóng
|
// Mig 31 (S23 t1 Plan K Chunk A) — F2 semantic + storage REFACTOR:
|
||||||
// vai Drafter) gửi PE thẳng Cấp cuối, skip mọi Bước/Cấp trung gian. Workspace
|
// Drafter-skip-from-Nháp (cũ Mig 29) → Approver-skip-during-ChoDuyet (mới).
|
||||||
// hiện checkbox "Gửi thẳng Cấp cuối" conditional theo flag này.
|
// Cờ chuyển sang `ApprovalWorkflowLevels.AllowApproverSkipToFinal` per-slot
|
||||||
//
|
// Approver (mirror F3+F4 admin opt-in per-Approver pattern). NO BACKFILL —
|
||||||
// Mặc định false (an toàn). Admin set ở User Management page. Backfill
|
// 4 prod user accept lose value per bro Option A, admin re-config qua Designer.
|
||||||
// Mig 29: bulk set TRUE cho user nào từng Drafter PE link workflow có
|
|
||||||
// workflow.AllowDrafterSkipToFinal=true (preserve admin config S21 t4).
|
|
||||||
public bool AllowDrafterSkipToFinal { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,9 @@ public class ApprovalWorkflowConfiguration : IEntityTypeConfiguration<ApprovalWo
|
|||||||
// Mig 28 cũ 6 column Allow* đã DROP trong Mig 29 (S21 t5) — refactor sang
|
// Mig 28 cũ 6 column Allow* đã DROP trong Mig 29 (S21 t5) — refactor sang
|
||||||
// per-NV (Level table cho F1+F3, Users table cho F2). Backfill bulk SQL
|
// per-NV (Level table cho F1+F3, Users table cho F2). Backfill bulk SQL
|
||||||
// preserve config admin từ S21 t4 trước khi drop.
|
// preserve config admin từ S21 t4 trước khi drop.
|
||||||
|
// + Mig 31 (S23 t1) F2 refactor sang Approver scope per-Level slot —
|
||||||
|
// AllowApproverSkipToFinal trên Level table (admin opt-in per slot,
|
||||||
|
// KHÔNG còn per-Drafter user scope cũ Mig 29).
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,5 +84,10 @@ public class ApprovalWorkflowLevelConfiguration : IEntityTypeConfiguration<Appro
|
|||||||
// Mig 30 (S22+5) — F4 per-NV: cho phép edit Section "Điều chỉnh ngân sách"
|
// Mig 30 (S22+5) — F4 per-NV: cho phép edit Section "Điều chỉnh ngân sách"
|
||||||
// lúc đang duyệt. Default false (admin opt-in).
|
// lúc đang duyệt. Default false (admin opt-in).
|
||||||
e.Property(x => x.AllowApproverEditBudget).HasDefaultValue(false);
|
e.Property(x => x.AllowApproverEditBudget).HasDefaultValue(false);
|
||||||
|
|
||||||
|
// Mig 31 (S23 t1 Plan K Chunk A) — F2 per-NV REFACTOR: cho phép Approver
|
||||||
|
// slot này skip thẳng Cấp cuối lúc đang duyệt ChoDuyet. Default false
|
||||||
|
// (admin opt-in). Semantic + storage cũ ở Users table per-Drafter đã drop.
|
||||||
|
e.Property(x => x.AllowApproverSkipToFinal).HasDefaultValue(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,52 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
// Mig 31 (S23 t1 Plan K Chunk A) — F2 storage swap Users → ApprovalWorkflowLevels.
|
||||||
|
// Manual reorder Up(): ADD col first → DROP col second (per memory rule
|
||||||
|
// feedback_ef_migration_backfill_reorder.md). NO BACKFILL block — Option A bro
|
||||||
|
// accept 4 prod user (fin.pp + pm.nv + nv.test + truong.nguyen) lose value cũ
|
||||||
|
// AllowDrafterSkipToFinal=true. Admin re-config qua Workflow Designer per slot.
|
||||||
|
//
|
||||||
|
// Down() reverse order với data loss accepted: cờ AllowApproverSkipToFinal sẽ
|
||||||
|
// mất khi rollback (Users.AllowDrafterSkipToFinal restore default false).
|
||||||
|
public partial class RefactorSkipToFinalToApproverLevel : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
// Step 1 — ADD new column on ApprovalWorkflowLevels (per-slot Approver scope).
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "AllowApproverSkipToFinal",
|
||||||
|
table: "ApprovalWorkflowLevels",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
// Step 2 — DROP old column on Users (NO BACKFILL — Option A accept lose).
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AllowDrafterSkipToFinal",
|
||||||
|
table: "Users");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
// Step 1 — DROP new column on ApprovalWorkflowLevels.
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "AllowApproverSkipToFinal",
|
||||||
|
table: "ApprovalWorkflowLevels");
|
||||||
|
|
||||||
|
// Step 2 — ADD old column on Users (data loss accepted — default false).
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "AllowDrafterSkipToFinal",
|
||||||
|
table: "Users",
|
||||||
|
type: "bit",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -198,6 +198,11 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
.HasColumnType("bit")
|
.HasColumnType("bit")
|
||||||
.HasDefaultValue(false);
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
|
b.Property<bool>("AllowApproverSkipToFinal")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bit")
|
||||||
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
b.Property<bool>("AllowReturnOneLevel")
|
b.Property<bool>("AllowReturnOneLevel")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("bit")
|
.HasColumnType("bit")
|
||||||
@ -1945,9 +1950,6 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.Property<int>("AccessFailedCount")
|
b.Property<int>("AccessFailedCount")
|
||||||
.HasColumnType("int");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<bool>("AllowDrafterSkipToFinal")
|
|
||||||
.HasColumnType("bit");
|
|
||||||
|
|
||||||
b.Property<bool>("CanBypassReview")
|
b.Property<bool>("CanBypassReview")
|
||||||
.HasColumnType("bit");
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
|||||||
@ -118,43 +118,13 @@ public class PurchaseEvaluationWorkflowService(
|
|||||||
}
|
}
|
||||||
evaluation.Phase = PurchaseEvaluationPhase.ChoDuyet;
|
evaluation.Phase = PurchaseEvaluationPhase.ChoDuyet;
|
||||||
|
|
||||||
// F2 (Mig 29 — S21 t5) — Drafter skip thẳng Cấp cuối. Permission
|
// Mig 31 (S23 t1 Plan K) — F2 Drafter-skip-from-Nháp semantic deprecated.
|
||||||
// check moved sang `User.AllowDrafterSkipToFinal` (per-Drafter user,
|
// skipToFinal param 8th repurpose sang Approver scope ChoDuyet (xem
|
||||||
// không còn workflow-level Mig 28).
|
// ApproveV2Async branch). Drafter SUBMIT chạy normal init pointer Step 0
|
||||||
// Admin bypass user flag check.
|
// Cấp 1, ignore skipToFinal flag.
|
||||||
if (skipToFinal && evaluation.ApprovalWorkflowId is Guid skipAwId)
|
|
||||||
{
|
|
||||||
if (!isAdmin)
|
|
||||||
{
|
|
||||||
if (actorUserId is null)
|
|
||||||
throw new ConflictException("skipToFinal yêu cầu authenticated user.");
|
|
||||||
var drafterUser = await userManager.FindByIdAsync(actorUserId.Value.ToString())
|
|
||||||
?? throw new ConflictException("User không tồn tại.");
|
|
||||||
if (!drafterUser.AllowDrafterSkipToFinal)
|
|
||||||
throw new ConflictException(
|
|
||||||
$"User '{drafterUser.FullName}' không được phép gửi thẳng Cấp cuối. " +
|
|
||||||
"Liên hệ Admin để cấp quyền ở User Management.");
|
|
||||||
}
|
|
||||||
var wfSkip = await db.ApprovalWorkflows
|
|
||||||
.Include(w => w.Steps).ThenInclude(s => s.Levels)
|
|
||||||
.FirstOrDefaultAsync(w => w.Id == skipAwId, ct)
|
|
||||||
?? throw new ConflictException("Workflow không tồn tại.");
|
|
||||||
var finalStep = wfSkip.Steps.OrderBy(s => s.Order).LastOrDefault()
|
|
||||||
?? throw new ConflictException("Workflow chưa có Bước nào.");
|
|
||||||
var finalLevelOrder = finalStep.Levels.OrderBy(l => l.Order).LastOrDefault()?.Order
|
|
||||||
?? throw new ConflictException($"Bước {finalStep.Order} chưa có Cấp nào.");
|
|
||||||
evaluation.CurrentWorkflowStepIndex = wfSkip.Steps.Count - 1; // 0-based last step
|
|
||||||
evaluation.CurrentApprovalLevelOrder = finalLevelOrder;
|
|
||||||
comment = string.IsNullOrWhiteSpace(comment)
|
|
||||||
? "[Drafter gửi thẳng Cấp cuối — skip Bước/Cấp trung gian]"
|
|
||||||
: $"{comment} [Drafter gửi thẳng Cấp cuối — skip Bước/Cấp trung gian]";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
evaluation.CurrentWorkflowStepIndex = 0;
|
evaluation.CurrentWorkflowStepIndex = 0;
|
||||||
// Chỉ init levelOrder=1 nếu pin schema V2 (ApprovalWorkflowId set).
|
// Chỉ init levelOrder=1 nếu pin schema V2 (ApprovalWorkflowId set).
|
||||||
evaluation.CurrentApprovalLevelOrder = evaluation.ApprovalWorkflowId is not null ? 1 : null;
|
evaluation.CurrentApprovalLevelOrder = evaluation.ApprovalWorkflowId is not null ? 1 : null;
|
||||||
}
|
|
||||||
evaluation.SlaDeadline = dateTime.UtcNow.AddDays(7);
|
evaluation.SlaDeadline = dateTime.UtcNow.AddDays(7);
|
||||||
await LogTransitionAsync(evaluation, fromPhase, PurchaseEvaluationPhase.ChoDuyet, actorUserId, decision, comment, ct);
|
await LogTransitionAsync(evaluation, fromPhase, PurchaseEvaluationPhase.ChoDuyet, actorUserId, decision, comment, ct);
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
@ -166,12 +136,17 @@ public class PurchaseEvaluationWorkflowService(
|
|||||||
{
|
{
|
||||||
// Branch: V2 schema mới (ApprovalWorkflowId pin) hay V1 legacy
|
// Branch: V2 schema mới (ApprovalWorkflowId pin) hay V1 legacy
|
||||||
// (WorkflowDefinitionId pin Mig 21).
|
// (WorkflowDefinitionId pin Mig 21).
|
||||||
|
// Mig 31 (S23 t1 Plan K) — skipToFinal repurpose Approver scope ChoDuyet.
|
||||||
|
// V2 path nhận flag, V1 legacy throw nếu non-admin gọi skipToFinal=true.
|
||||||
if (evaluation.ApprovalWorkflowId is Guid awId)
|
if (evaluation.ApprovalWorkflowId is Guid awId)
|
||||||
{
|
{
|
||||||
await ApproveV2Async(evaluation, awId, actorUserId, actorRoles, isAdmin, isSystem, comment, ct);
|
await ApproveV2Async(evaluation, awId, actorUserId, actorRoles, isAdmin, isSystem, comment, skipToFinal, ct);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
if (skipToFinal && !isAdmin && !isSystem)
|
||||||
|
throw new ConflictException(
|
||||||
|
"skipToFinal chỉ hỗ trợ phiếu V2 (ApprovalWorkflowsV2). Phiếu V1 legacy không có per-Approver-slot flag.");
|
||||||
await ApproveV1LegacyAsync(evaluation, actorUserId, actorRoles, isAdmin, isSystem, comment, ct);
|
await ApproveV1LegacyAsync(evaluation, actorUserId, actorRoles, isAdmin, isSystem, comment, ct);
|
||||||
}
|
}
|
||||||
await db.SaveChangesAsync(ct);
|
await db.SaveChangesAsync(ct);
|
||||||
@ -391,6 +366,8 @@ public class PurchaseEvaluationWorkflowService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== V2 schema (Mig 22-24) — iterate ApprovalWorkflowSteps + Levels =====
|
// ===== V2 schema (Mig 22-24) — iterate ApprovalWorkflowSteps + Levels =====
|
||||||
|
// Mig 31 (S23 t1 Plan K) — `skipToFinal` 8th param: F2 Approver scope ChoDuyet.
|
||||||
|
// Admin opt-in flag per slot tại matchingLevel.AllowApproverSkipToFinal.
|
||||||
private async Task ApproveV2Async(
|
private async Task ApproveV2Async(
|
||||||
PurchaseEvaluation evaluation,
|
PurchaseEvaluation evaluation,
|
||||||
Guid awId,
|
Guid awId,
|
||||||
@ -399,6 +376,7 @@ public class PurchaseEvaluationWorkflowService(
|
|||||||
bool isAdmin,
|
bool isAdmin,
|
||||||
bool isSystem,
|
bool isSystem,
|
||||||
string? comment,
|
string? comment,
|
||||||
|
bool skipToFinal,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var aw = await db.ApprovalWorkflows.AsNoTracking()
|
var aw = await db.ApprovalWorkflows.AsNoTracking()
|
||||||
@ -489,6 +467,38 @@ public class PurchaseEvaluationWorkflowService(
|
|||||||
existingOpinion.SignedByFullName = actorFullName;
|
existingOpinion.SignedByFullName = actorFullName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mig 31 (S23 t1 Plan K) — F2 Approver scope ChoDuyet: duyệt thẳng Cấp cuối.
|
||||||
|
// Admin opt-in per slot tại matchingLevel.AllowApproverSkipToFinal. Khi
|
||||||
|
// Approver tick checkbox "Duyệt thẳng Cấp cuối" trong Workspace + admin
|
||||||
|
// đã enable flag cho slot này → bỏ qua mọi Bước/Cấp trung gian còn lại,
|
||||||
|
// set Phase=DaDuyet terminal trực tiếp. Mirror F3+F4 admin opt-in per-
|
||||||
|
// Approver-slot pattern (Mig 29 + Mig 30) reinforced 3× cumulative.
|
||||||
|
// Non-admin + flag off → ConflictException. Admin bypass flag.
|
||||||
|
if (skipToFinal)
|
||||||
|
{
|
||||||
|
if (!isAdmin && !isSystem && !matchingLevel.AllowApproverSkipToFinal)
|
||||||
|
{
|
||||||
|
throw new ConflictException(
|
||||||
|
$"Cấp Approver hiện tại (Bước {currentIdx + 1} Cấp {currentLevelOrder}) " +
|
||||||
|
"chưa được phép duyệt thẳng Cấp cuối. Admin phải tick checkbox " +
|
||||||
|
"'Duyệt thẳng Cấp cuối' trong Workflow Designer cho slot này.");
|
||||||
|
}
|
||||||
|
|
||||||
|
evaluation.Phase = PurchaseEvaluationPhase.DaDuyet;
|
||||||
|
evaluation.CurrentWorkflowStepIndex = null;
|
||||||
|
evaluation.CurrentApprovalLevelOrder = null;
|
||||||
|
evaluation.SlaDeadline = null;
|
||||||
|
await LogTransitionAsync(
|
||||||
|
evaluation,
|
||||||
|
PurchaseEvaluationPhase.ChoDuyet,
|
||||||
|
PurchaseEvaluationPhase.DaDuyet,
|
||||||
|
actorUserId,
|
||||||
|
ApprovalDecision.Approve,
|
||||||
|
$"[Approver duyệt thẳng Cấp cuối — Bước {currentIdx + 1} Cấp {currentLevelOrder} → DaDuyet] {comment ?? ""}".Trim(),
|
||||||
|
ct);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Advance: nếu còn cấp tiếp trong Step → levelOrder++; else → next Step + level 1
|
// Advance: nếu còn cấp tiếp trong Step → levelOrder++; else → next Step + level 1
|
||||||
if (currentLevelOrder < maxLevelOrder)
|
if (currentLevelOrder < maxLevelOrder)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using SolutionErp.Application.Common.Exceptions;
|
using SolutionErp.Application.Common.Exceptions;
|
||||||
using SolutionErp.Application.Notifications;
|
using SolutionErp.Application.Notifications;
|
||||||
@ -14,9 +15,12 @@ using SolutionErp.Infrastructure.Tests.Common;
|
|||||||
|
|
||||||
namespace SolutionErp.Infrastructure.Tests.Services;
|
namespace SolutionErp.Infrastructure.Tests.Services;
|
||||||
|
|
||||||
// Plan C task 1-2 catch-up cho S21 t4-t5 feature:
|
// Plan C task 1-2 catch-up cho S21 t4-t5 feature + Plan K Chunk F (S23 t1 Mig 31):
|
||||||
// - ApplyReturnModeAsync 4 mode đọc level.Allow* per-NV (Mig 29 refactor từ workflow-level Mig 28)
|
// - ApplyReturnModeAsync 4 mode đọc level.Allow* per-NV (Mig 29 refactor từ workflow-level Mig 28)
|
||||||
// - skipToFinal đọc user.AllowDrafterSkipToFinal per-Drafter (Mig 29 split scope theo role)
|
// - skipToFinal (Mig 31 refactor) đọc matchingLevel.AllowApproverSkipToFinal per-Approver-slot
|
||||||
|
// trong ApproveV2Async branch APPROVE STEP. Semantic mới: Approver during ChoDuyet
|
||||||
|
// skip thẳng Cấp cuối → Phase=DaDuyet terminal. Semantic cũ Drafter-from-Nháp
|
||||||
|
// (Mig 29) đã deprecated + storage trên Users table đã drop trong Mig 31 Plan K.
|
||||||
//
|
//
|
||||||
// Focus: defensive boundary check + admin bypass invariant. KHÔNG cover toàn bộ
|
// Focus: defensive boundary check + admin bypass invariant. KHÔNG cover toàn bộ
|
||||||
// edge case (Bước 1 Cấp 1 fallback, Assignee runtime pick, V1 legacy fallback) —
|
// edge case (Bước 1 Cấp 1 fallback, Assignee runtime pick, V1 legacy fallback) —
|
||||||
@ -236,33 +240,124 @@ public class PurchaseEvaluationWorkflowServiceReturnModeTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============ Task 2: skipToFinal (Drafter trình branch) ============
|
// ============ Task 2: skipToFinal — Mig 31 (S23 t1 Plan K Chunk F) ============
|
||||||
|
// Semantic mới: Approver during ChoDuyet duyệt thẳng Cấp cuối → Phase=DaDuyet terminal.
|
||||||
|
// Storage: matchingLevel.AllowApproverSkipToFinal (per-Approver-slot, admin opt-in).
|
||||||
|
// 3 tests cover happy path + denied + admin bypass — mirror F1 Drafter pattern.
|
||||||
|
//
|
||||||
|
// SeedApproverF2WorkflowAsync helper: 2 Steps × 2 Levels (multi-step để verify
|
||||||
|
// skip thẳng terminal, KHÔNG fallthrough advance pointer next Step). Slot Cấp 1
|
||||||
|
// Bước 1 set AllowApproverSkipToFinal per param.
|
||||||
|
|
||||||
|
private static async Task<(ApprovalWorkflow wf, ApprovalWorkflowStep s1, ApprovalWorkflowLevel s1l1, ApprovalWorkflowLevel s1l2, ApprovalWorkflowStep s2, ApprovalWorkflowLevel s2l1, ApprovalWorkflowLevel s2l2)>
|
||||||
|
SeedApproverF2WorkflowAsync(
|
||||||
|
TestApplicationDbContext db,
|
||||||
|
Guid s1l1Approver,
|
||||||
|
Guid s1l2Approver,
|
||||||
|
Guid s2l1Approver,
|
||||||
|
Guid s2l2Approver,
|
||||||
|
bool allowApproverSkipToFinalSlotS1L1 = false)
|
||||||
|
{
|
||||||
|
var wf = new ApprovalWorkflow
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Code = "QT-TEST-F2-001",
|
||||||
|
Version = 1,
|
||||||
|
Name = "Test Workflow Approver F2",
|
||||||
|
ApplicableType = ApprovalWorkflowApplicableType.DuyetNcc,
|
||||||
|
IsActive = true,
|
||||||
|
IsUserSelectable = true,
|
||||||
|
};
|
||||||
|
var s1 = new ApprovalWorkflowStep
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
ApprovalWorkflowId = wf.Id,
|
||||||
|
Order = 1,
|
||||||
|
DepartmentId = null,
|
||||||
|
Name = "Bước 1 CCM",
|
||||||
|
};
|
||||||
|
var s2 = new ApprovalWorkflowStep
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
ApprovalWorkflowId = wf.Id,
|
||||||
|
Order = 2,
|
||||||
|
DepartmentId = null,
|
||||||
|
Name = "Bước 2 GĐ",
|
||||||
|
};
|
||||||
|
var s1l1 = new ApprovalWorkflowLevel
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
ApprovalWorkflowStepId = s1.Id,
|
||||||
|
Order = 1,
|
||||||
|
ApproverUserId = s1l1Approver,
|
||||||
|
AllowApproverSkipToFinal = allowApproverSkipToFinalSlotS1L1,
|
||||||
|
};
|
||||||
|
var s1l2 = new ApprovalWorkflowLevel
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
ApprovalWorkflowStepId = s1.Id,
|
||||||
|
Order = 2,
|
||||||
|
ApproverUserId = s1l2Approver,
|
||||||
|
};
|
||||||
|
var s2l1 = new ApprovalWorkflowLevel
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
ApprovalWorkflowStepId = s2.Id,
|
||||||
|
Order = 1,
|
||||||
|
ApproverUserId = s2l1Approver,
|
||||||
|
};
|
||||||
|
var s2l2 = new ApprovalWorkflowLevel
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
ApprovalWorkflowStepId = s2.Id,
|
||||||
|
Order = 2,
|
||||||
|
ApproverUserId = s2l2Approver,
|
||||||
|
};
|
||||||
|
db.ApprovalWorkflows.Add(wf);
|
||||||
|
db.ApprovalWorkflowSteps.Add(s1);
|
||||||
|
db.ApprovalWorkflowSteps.Add(s2);
|
||||||
|
db.ApprovalWorkflowLevels.Add(s1l1);
|
||||||
|
db.ApprovalWorkflowLevels.Add(s1l2);
|
||||||
|
db.ApprovalWorkflowLevels.Add(s2l1);
|
||||||
|
db.ApprovalWorkflowLevels.Add(s2l2);
|
||||||
|
await db.SaveChangesAsync(CancellationToken.None);
|
||||||
|
return (wf, s1, s1l1, s1l2, s2, s2l1, s2l2);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SkipToFinal_DrafterAllowed_SetsPointerToFinalLevel()
|
public async Task ApproveV2_SkipToFinal_AdminTickFlag_SetsPhaseDaDuyet()
|
||||||
{
|
{
|
||||||
// Drafter user có AllowDrafterSkipToFinal=true → init pointer cuối step + cuối level.
|
// Happy path: workflow 2 Step × 2 Level. Slot Cấp 1 Bước 1 admin tick
|
||||||
|
// AllowApproverSkipToFinal=true. Actor = userA (Cấp 1 Bước 1 approver,
|
||||||
|
// non-admin role). PE pin workflow + Phase=ChoDuyet + pointer init Step 0 Cấp 1.
|
||||||
|
// → Phase=DaDuyet, pointer cleared, opinion + PEA + Changelog logged.
|
||||||
var (svc, fix, db, _) = CreateService();
|
var (svc, fix, db, _) = CreateService();
|
||||||
using (fix)
|
using (fix)
|
||||||
{
|
{
|
||||||
var (a1, a2) = await SeedApproversAsync(fix, "skip1");
|
var userA = await fix.CreateUserAsync("usera-f2-skip@test.local", "User A F2 Skip",
|
||||||
var (wf, _, _, _) = await SeedWorkflowAsync(db, a1.Id, a2.Id);
|
departmentId: null, roles: new[] { AppRoles.CostControl });
|
||||||
var drafter = await fix.CreateUserAsync(
|
var userB = await fix.CreateUserAsync("userb-f2-skip@test.local", "User B F2 Skip",
|
||||||
"drafter.skip@test.local", "Drafter Skip", departmentId: null,
|
departmentId: null, roles: new[] { AppRoles.CostControl });
|
||||||
roles: new[] { AppRoles.Drafter });
|
var userC = await fix.CreateUserAsync("userc-f2-skip@test.local", "User C F2 Skip",
|
||||||
drafter.AllowDrafterSkipToFinal = true;
|
departmentId: null, roles: new[] { AppRoles.CostControl });
|
||||||
await fix.Services.GetRequiredService<UserManager<User>>().UpdateAsync(drafter);
|
var userD = await fix.CreateUserAsync("userd-f2-skip@test.local", "User D F2 Skip",
|
||||||
|
departmentId: null, roles: new[] { AppRoles.CostControl });
|
||||||
|
var (wf, _, s1l1, _, _, _, _) = await SeedApproverF2WorkflowAsync(
|
||||||
|
db, userA.Id, userB.Id, userC.Id, userD.Id,
|
||||||
|
allowApproverSkipToFinalSlotS1L1: true);
|
||||||
|
|
||||||
var pe = new PurchaseEvaluation
|
var pe = new PurchaseEvaluation
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
Type = PurchaseEvaluationType.DuyetNcc,
|
Type = PurchaseEvaluationType.DuyetNcc,
|
||||||
Phase = PurchaseEvaluationPhase.DangSoanThao,
|
Phase = PurchaseEvaluationPhase.ChoDuyet,
|
||||||
MaPhieu = "PE-SKIP-001",
|
MaPhieu = "PE-F2-001",
|
||||||
TenGoiThau = "Skip to final",
|
TenGoiThau = "Approver F2 happy",
|
||||||
ProjectId = Guid.NewGuid(),
|
ProjectId = Guid.NewGuid(),
|
||||||
DrafterUserId = drafter.Id,
|
DrafterUserId = Guid.NewGuid(),
|
||||||
ApprovalWorkflowId = wf.Id,
|
ApprovalWorkflowId = wf.Id,
|
||||||
|
CurrentWorkflowStepIndex = 0,
|
||||||
|
CurrentApprovalLevelOrder = 1,
|
||||||
};
|
};
|
||||||
db.PurchaseEvaluations.Add(pe);
|
db.PurchaseEvaluations.Add(pe);
|
||||||
await db.SaveChangesAsync(CancellationToken.None);
|
await db.SaveChangesAsync(CancellationToken.None);
|
||||||
@ -270,44 +365,74 @@ public class PurchaseEvaluationWorkflowServiceReturnModeTests
|
|||||||
await svc.TransitionAsync(
|
await svc.TransitionAsync(
|
||||||
evaluation: pe,
|
evaluation: pe,
|
||||||
targetPhase: PurchaseEvaluationPhase.ChoDuyet,
|
targetPhase: PurchaseEvaluationPhase.ChoDuyet,
|
||||||
actorUserId: drafter.Id,
|
actorUserId: userA.Id,
|
||||||
actorRoles: new[] { AppRoles.Drafter },
|
actorRoles: new[] { AppRoles.CostControl },
|
||||||
decision: ApprovalDecision.Approve,
|
decision: ApprovalDecision.Approve,
|
||||||
comment: "gửi thẳng cấp cuối",
|
comment: "duyệt thẳng cấp cuối",
|
||||||
skipToFinal: true,
|
skipToFinal: true,
|
||||||
ct: CancellationToken.None);
|
ct: CancellationToken.None);
|
||||||
|
|
||||||
pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet);
|
pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet, "Skip → terminal trực tiếp");
|
||||||
pe.CurrentWorkflowStepIndex.Should().Be(0, "Step duy nhất (chỉ 1 Step) = index 0");
|
pe.CurrentWorkflowStepIndex.Should().BeNull("Pointer cleared khi terminal");
|
||||||
pe.CurrentApprovalLevelOrder.Should().Be(2,
|
pe.CurrentApprovalLevelOrder.Should().BeNull("Pointer cleared khi terminal");
|
||||||
"Final Level Order = 2 (Cấp cuối Bước cuối)");
|
pe.SlaDeadline.Should().BeNull("SLA cleared khi terminal");
|
||||||
|
|
||||||
|
// 1 PEL opinion UPSERT cho slot Cấp 1 Bước 1 trước skip
|
||||||
|
var opinions = await db.PurchaseEvaluationLevelOpinions
|
||||||
|
.Where(o => o.PurchaseEvaluationId == pe.Id).ToListAsync();
|
||||||
|
opinions.Should().HaveCount(1);
|
||||||
|
opinions[0].ApprovalWorkflowLevelId.Should().Be(s1l1.Id, "Opinion ghi vào slot Cấp 1 Bước 1");
|
||||||
|
opinions[0].SignedByUserId.Should().Be(userA.Id);
|
||||||
|
|
||||||
|
// 1 PEA approval audit
|
||||||
|
var approvals = await db.PurchaseEvaluationApprovals
|
||||||
|
.Where(a => a.PurchaseEvaluationId == pe.Id).ToListAsync();
|
||||||
|
approvals.Should().HaveCount(1);
|
||||||
|
approvals[0].ApproverUserId.Should().Be(userA.Id);
|
||||||
|
approvals[0].Decision.Should().Be(ApprovalDecision.Approve);
|
||||||
|
|
||||||
|
// Changelog entry với context note chứa "Approver duyệt thẳng Cấp cuối"
|
||||||
|
var changelogs = await db.PurchaseEvaluationChangelogs
|
||||||
|
.Where(c => c.PurchaseEvaluationId == pe.Id).ToListAsync();
|
||||||
|
changelogs.Should().Contain(c => c.ContextNote != null
|
||||||
|
&& c.ContextNote.Contains("Approver duyệt thẳng Cấp cuối"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SkipToFinal_DrafterDenied_NonAdmin_Throws()
|
public async Task ApproveV2_SkipToFinal_FlagOff_NonAdmin_ThrowsConflictException()
|
||||||
{
|
{
|
||||||
// Drafter user có AllowDrafterSkipToFinal=false (default) + non-admin → throw.
|
// Denied: workflow same nhưng AllowApproverSkipToFinal=false cho slot Cấp 1 Bước 1.
|
||||||
|
// Actor = userA (non-admin, trong slot Cấp 1 Bước 1 approvers).
|
||||||
|
// → throw ConflictException "chưa được phép duyệt thẳng Cấp cuối".
|
||||||
|
// State unchanged: Phase still ChoDuyet, pointer unchanged.
|
||||||
var (svc, fix, db, _) = CreateService();
|
var (svc, fix, db, _) = CreateService();
|
||||||
using (fix)
|
using (fix)
|
||||||
{
|
{
|
||||||
var (a1, a2) = await SeedApproversAsync(fix, "skip2");
|
var userA = await fix.CreateUserAsync("usera-f2-deny@test.local", "User A F2 Deny",
|
||||||
var (wf, _, _, _) = await SeedWorkflowAsync(db, a1.Id, a2.Id);
|
departmentId: null, roles: new[] { AppRoles.CostControl });
|
||||||
var drafter = await fix.CreateUserAsync(
|
var userB = await fix.CreateUserAsync("userb-f2-deny@test.local", "User B F2 Deny",
|
||||||
"drafter.noskip@test.local", "Drafter NoSkip", departmentId: null,
|
departmentId: null, roles: new[] { AppRoles.CostControl });
|
||||||
roles: new[] { AppRoles.Drafter });
|
var userC = await fix.CreateUserAsync("userc-f2-deny@test.local", "User C F2 Deny",
|
||||||
// drafter.AllowDrafterSkipToFinal = false (default)
|
departmentId: null, roles: new[] { AppRoles.CostControl });
|
||||||
|
var userD = await fix.CreateUserAsync("userd-f2-deny@test.local", "User D F2 Deny",
|
||||||
|
departmentId: null, roles: new[] { AppRoles.CostControl });
|
||||||
|
var (wf, _, _, _, _, _, _) = await SeedApproverF2WorkflowAsync(
|
||||||
|
db, userA.Id, userB.Id, userC.Id, userD.Id,
|
||||||
|
allowApproverSkipToFinalSlotS1L1: false);
|
||||||
|
|
||||||
var pe = new PurchaseEvaluation
|
var pe = new PurchaseEvaluation
|
||||||
{
|
{
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
Type = PurchaseEvaluationType.DuyetNcc,
|
Type = PurchaseEvaluationType.DuyetNcc,
|
||||||
Phase = PurchaseEvaluationPhase.DangSoanThao,
|
Phase = PurchaseEvaluationPhase.ChoDuyet,
|
||||||
MaPhieu = "PE-SKIP-002",
|
MaPhieu = "PE-F2-002",
|
||||||
TenGoiThau = "Skip denied",
|
TenGoiThau = "Approver F2 denied",
|
||||||
ProjectId = Guid.NewGuid(),
|
ProjectId = Guid.NewGuid(),
|
||||||
DrafterUserId = drafter.Id,
|
DrafterUserId = Guid.NewGuid(),
|
||||||
ApprovalWorkflowId = wf.Id,
|
ApprovalWorkflowId = wf.Id,
|
||||||
|
CurrentWorkflowStepIndex = 0,
|
||||||
|
CurrentApprovalLevelOrder = 1,
|
||||||
};
|
};
|
||||||
db.PurchaseEvaluations.Add(pe);
|
db.PurchaseEvaluations.Add(pe);
|
||||||
await db.SaveChangesAsync(CancellationToken.None);
|
await db.SaveChangesAsync(CancellationToken.None);
|
||||||
@ -315,22 +440,87 @@ public class PurchaseEvaluationWorkflowServiceReturnModeTests
|
|||||||
var act = async () => await svc.TransitionAsync(
|
var act = async () => await svc.TransitionAsync(
|
||||||
evaluation: pe,
|
evaluation: pe,
|
||||||
targetPhase: PurchaseEvaluationPhase.ChoDuyet,
|
targetPhase: PurchaseEvaluationPhase.ChoDuyet,
|
||||||
actorUserId: drafter.Id,
|
actorUserId: userA.Id,
|
||||||
actorRoles: new[] { AppRoles.Drafter },
|
actorRoles: new[] { AppRoles.CostControl },
|
||||||
decision: ApprovalDecision.Approve,
|
decision: ApprovalDecision.Approve,
|
||||||
comment: "test denied",
|
comment: "test denied",
|
||||||
skipToFinal: true,
|
skipToFinal: true,
|
||||||
ct: CancellationToken.None);
|
ct: CancellationToken.None);
|
||||||
|
|
||||||
await act.Should().ThrowAsync<ConflictException>()
|
await act.Should().ThrowAsync<ConflictException>()
|
||||||
.WithMessage("*không được phép gửi thẳng Cấp cuối*");
|
.WithMessage("*chưa được phép duyệt thẳng Cấp cuối*");
|
||||||
|
|
||||||
// Service mutate Phase=ChoDuyet TRƯỚC khi validate skipToFinal flag,
|
// State: in-memory PE chưa mutate phase (throw chặn trước assign).
|
||||||
// throw chặn SaveChangesAsync → DB không persist. Test focus contract
|
// Note: service log PEA + Opinion TRƯỚC khi validate skipToFinal flag,
|
||||||
// throw, không assert in-memory rollback (note: nếu future refactor
|
// throw chặn SaveChangesAsync → DB không persist. Focus test contract
|
||||||
// move validate trước mutate, test này vẫn pass).
|
// throw + Phase invariant unchanged.
|
||||||
pe.CurrentWorkflowStepIndex.Should().BeNull("Skip flow throw trước khi init pointer");
|
pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet, "Throw chặn trước mutate Phase");
|
||||||
pe.CurrentApprovalLevelOrder.Should().BeNull("Pointer chưa init khi throw");
|
pe.CurrentWorkflowStepIndex.Should().Be(0, "Pointer unchanged");
|
||||||
|
pe.CurrentApprovalLevelOrder.Should().Be(1, "Pointer unchanged");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ApproveV2_SkipToFinal_FlagOff_Admin_BypassesFlagCheck()
|
||||||
|
{
|
||||||
|
// Admin bypass: workflow same nhưng AllowApproverSkipToFinal=false cho slot Cấp 1 Bước 1.
|
||||||
|
// Actor = adminUser (actorRoles contains "Admin"), trong slot Cấp 1 Bước 1.
|
||||||
|
// → DaDuyet (admin bypass flag), pointer cleared, opinion logged, Changelog "Approver duyệt thẳng Cấp cuối".
|
||||||
|
var (svc, fix, db, _) = CreateService();
|
||||||
|
using (fix)
|
||||||
|
{
|
||||||
|
var adminUser = await fix.CreateUserAsync("admin-f2-bypass@test.local", "Admin F2 Bypass",
|
||||||
|
departmentId: null, roles: new[] { AppRoles.Admin });
|
||||||
|
var userB = await fix.CreateUserAsync("userb-f2-bypass@test.local", "User B F2 Bypass",
|
||||||
|
departmentId: null, roles: new[] { AppRoles.CostControl });
|
||||||
|
var userC = await fix.CreateUserAsync("userc-f2-bypass@test.local", "User C F2 Bypass",
|
||||||
|
departmentId: null, roles: new[] { AppRoles.CostControl });
|
||||||
|
var userD = await fix.CreateUserAsync("userd-f2-bypass@test.local", "User D F2 Bypass",
|
||||||
|
departmentId: null, roles: new[] { AppRoles.CostControl });
|
||||||
|
var (wf, _, _, _, _, _, _) = await SeedApproverF2WorkflowAsync(
|
||||||
|
db, adminUser.Id, userB.Id, userC.Id, userD.Id,
|
||||||
|
allowApproverSkipToFinalSlotS1L1: false);
|
||||||
|
|
||||||
|
var pe = new PurchaseEvaluation
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Type = PurchaseEvaluationType.DuyetNcc,
|
||||||
|
Phase = PurchaseEvaluationPhase.ChoDuyet,
|
||||||
|
MaPhieu = "PE-F2-003",
|
||||||
|
TenGoiThau = "Approver F2 admin bypass",
|
||||||
|
ProjectId = Guid.NewGuid(),
|
||||||
|
DrafterUserId = Guid.NewGuid(),
|
||||||
|
ApprovalWorkflowId = wf.Id,
|
||||||
|
CurrentWorkflowStepIndex = 0,
|
||||||
|
CurrentApprovalLevelOrder = 1,
|
||||||
|
};
|
||||||
|
db.PurchaseEvaluations.Add(pe);
|
||||||
|
await db.SaveChangesAsync(CancellationToken.None);
|
||||||
|
|
||||||
|
await svc.TransitionAsync(
|
||||||
|
evaluation: pe,
|
||||||
|
targetPhase: PurchaseEvaluationPhase.ChoDuyet,
|
||||||
|
actorUserId: adminUser.Id,
|
||||||
|
actorRoles: new[] { AppRoles.Admin },
|
||||||
|
decision: ApprovalDecision.Approve,
|
||||||
|
comment: "admin duyệt thẳng",
|
||||||
|
skipToFinal: true,
|
||||||
|
ct: CancellationToken.None);
|
||||||
|
|
||||||
|
pe.Phase.Should().Be(PurchaseEvaluationPhase.DaDuyet, "Admin bypass flag → terminal");
|
||||||
|
pe.CurrentWorkflowStepIndex.Should().BeNull("Pointer cleared");
|
||||||
|
pe.CurrentApprovalLevelOrder.Should().BeNull("Pointer cleared");
|
||||||
|
|
||||||
|
// Opinion logged (UPSERT trước skip)
|
||||||
|
var opinions = await db.PurchaseEvaluationLevelOpinions
|
||||||
|
.Where(o => o.PurchaseEvaluationId == pe.Id).ToListAsync();
|
||||||
|
opinions.Should().HaveCount(1);
|
||||||
|
|
||||||
|
// Changelog entry với "Approver duyệt thẳng Cấp cuối"
|
||||||
|
var changelogs = await db.PurchaseEvaluationChangelogs
|
||||||
|
.Where(c => c.PurchaseEvaluationId == pe.Id).ToListAsync();
|
||||||
|
changelogs.Should().Contain(c => c.ContextNote != null
|
||||||
|
&& c.ContextNote.Contains("Approver duyệt thẳng Cấp cuối"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -372,45 +562,4 @@ public class PurchaseEvaluationWorkflowServiceReturnModeTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task SkipToFinal_AdminBypass_Succeeds()
|
|
||||||
{
|
|
||||||
// Admin role bypass user.AllowDrafterSkipToFinal flag check.
|
|
||||||
var (svc, fix, db, _) = CreateService();
|
|
||||||
using (fix)
|
|
||||||
{
|
|
||||||
var (a1, a2) = await SeedApproversAsync(fix, "skip3");
|
|
||||||
var (wf, _, _, _) = await SeedWorkflowAsync(db, a1.Id, a2.Id);
|
|
||||||
var adminUser = await fix.CreateUserAsync(
|
|
||||||
"admin.skip@test.local", "Admin Skip", departmentId: null,
|
|
||||||
roles: new[] { AppRoles.Admin });
|
|
||||||
|
|
||||||
var pe = new PurchaseEvaluation
|
|
||||||
{
|
|
||||||
Id = Guid.NewGuid(),
|
|
||||||
Type = PurchaseEvaluationType.DuyetNcc,
|
|
||||||
Phase = PurchaseEvaluationPhase.DangSoanThao,
|
|
||||||
MaPhieu = "PE-SKIP-003",
|
|
||||||
TenGoiThau = "Admin skip",
|
|
||||||
ProjectId = Guid.NewGuid(),
|
|
||||||
DrafterUserId = adminUser.Id,
|
|
||||||
ApprovalWorkflowId = wf.Id,
|
|
||||||
};
|
|
||||||
db.PurchaseEvaluations.Add(pe);
|
|
||||||
await db.SaveChangesAsync(CancellationToken.None);
|
|
||||||
|
|
||||||
await svc.TransitionAsync(
|
|
||||||
evaluation: pe,
|
|
||||||
targetPhase: PurchaseEvaluationPhase.ChoDuyet,
|
|
||||||
actorUserId: adminUser.Id,
|
|
||||||
actorRoles: new[] { AppRoles.Admin },
|
|
||||||
decision: ApprovalDecision.Approve,
|
|
||||||
comment: "admin gửi thẳng",
|
|
||||||
skipToFinal: true,
|
|
||||||
ct: CancellationToken.None);
|
|
||||||
|
|
||||||
pe.Phase.Should().Be(PurchaseEvaluationPhase.ChoDuyet);
|
|
||||||
pe.CurrentApprovalLevelOrder.Should().Be(2, "Admin bypass + skip → pointer cuối");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user