Compare commits

...

8 Commits

Author SHA1 Message Date
098baa6da6 [CLAUDE] Docs: Chunk G — K8 Plan K wrap S23 t1: docs + session log + Designer comment cleanup + 3 agent MEMORY drift
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m30s
Plan K Mig 31 F2 refactor sang per-Approver-slot DONE — 8 commits cumulative
S23 t1 (`56868bf..<this>`). K8 wrap docs + dirty MEMORY.md commit:

Docs updates:
- docs/STATUS.md: Last updated S23 t1 entry với Plan K summary 8 chunk
- docs/HANDOFF.md: TL;DR S23 t1 đầy đủ (top) — multi-agent ROI evidence
- docs/database/schema-diagram.md §14: title Mig 22-31 (was 22-29) + add
  Mig 30 F4 + Mig 31 F2 blocks per slot Approver + DROP Users column note
- NEW docs/changelog/sessions/2026-05-14-s23-turn1-plan-k-mig31-f2-refactor.md
  session log đầy đủ 8 chunk timeline + multi-agent spawn cost table + pattern
  reinforced 3×

FE Admin Designer comment cleanup (Reviewer K2 follow-up):
- ApprovalWorkflowsV2Page.tsx lines 73-75 + 502-504: 2 stale narratives "F2
  AllowDrafterSkipToFinal xuống per User (User Management)" rewrite Mig 29+30+31
  cumulative narrative "7 Allow* ALL xuống per Level slot, pattern proven 3×"

3 agent MEMORY.md drift commit (dirty từ session start S23 + S22 chốt):
- Investigator: K0 pre-flight findings + 5 surprises catch
- Reviewer: K2 PASS report + new pattern "transient sentinel zombie" anti-pattern
- CICD Monitor: S22 chốt verify cumulative (Run #193 + S23 t1 pending K9 spawn)

User-level memory updates (cross-project diary persisted ngoài repo):
- feedback_per_nv_permission_scope.md: reinforcement S23 t1 — Pattern 3×
  cumulative (Mig 29 + Mig 30 + Mig 31). Pattern ALSO applies cho refactor existing
  scope, KHÔNG chỉ greenfield. Cross-ref discoveries Plan K (compile-break workaround,
  stale narrative drift, transient sentinel zombie anti-pattern caught Reviewer).
- MEMORY.md index: cumulative reinforcement note 3× Mig 31

Verify:
- dotnet build production projects clean
- npm run build fe-admin pass 17.76s, 0 TS err
- Test 104/104 PASS (S23 t1 K7 chunk maintained baseline)

Plan K state final: 31 mig · 59 tables · ~145 endpoints · 104 test · 47 gotcha
· 20 memory · 6 skills · 4 sub-agents active. CHƯA push remote — chờ bro confirm
K9 spawn CICD Monitor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:48:41 +07:00
6b1e2d9220 [CLAUDE] Tests: Chunk F — K7 Mig 31 Approver F2 service regression + delete deprecated Drafter F2 tests
Sub-task 1: Fix broken references
- Tests reading removed User.AllowDrafterSkipToFinal prop -> delete entire test methods (semantic deprecated, no value)
- 3 deleted: SkipToFinal_DrafterAllowed_SetsPointerToFinalLevel + SkipToFinal_DrafterDenied_NonAdmin_Throws + SkipToFinal_AdminBypass_Succeeds

Sub-task 2: Add 3 Approver F2 service tests (PurchaseEvaluationWorkflowServiceReturnModeTests)
- ApproveV2_SkipToFinal_AdminTickFlag_SetsPhaseDaDuyet (happy path)
- ApproveV2_SkipToFinal_FlagOff_NonAdmin_ThrowsConflictException (denied)
- ApproveV2_SkipToFinal_FlagOff_Admin_BypassesFlagCheck (admin bypass)

Pattern reusable: SeedApproverF2WorkflowAsync 2 Step x 2 Level cookie-cutter
(Implementer memory Pattern 11 S22 SeedWorkflowAsync). PE init Phase=ChoDuyet
+ pointer Step 0 Cap 1. TestApplicationDbContext SQLite. Add EntityFrameworkCore
using for ToListAsync queries on PEL/PEA/Changelog audit assertions.

Verify:
- dotnet build SolutionErp.slnx 0 err 2 pre-existing DocxRenderer warn
- 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

Plan K Chunk F test-after carry per Phase 9 UAT mode bro confirm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:41:26 +07:00
ebe2469470 [CLAUDE] FE-Admin FE-User: Chunk E — K6 Workspace DROP Drafter checkbox + ADD Approver toggle (Mig 31 F2 refactor)
Plan K Chunk E mirror 2 app rule §3.9. Refactor F2 UX flow:

DROP fe-admin + fe-user Workspace Drafter checkbox:
- PeDetailTabs.tsx Workspace action bar: REMOVE "Gửi thẳng Cấp cuối (skip trung gian)"
  violet label + state skipToFinal + allowSkipToFinal lookup + skipToFinal payload
- submitForApproval mutation signature simplify: opts: { skipToFinal: boolean } → void
- Confirm dialog text + button label drop skipToFinal conditional

ADD fe-admin + fe-user Approver toggle trong PeWorkflowPanel dialog:
- State skipToFinalApprover default false
- Visible khi Approve forward (NOT Cancel + NOT SendBack) + currentLevelOptions?.allowApproverSkipToFinal
- Checkbox violet panel với description "Phiếu sẽ tiến thẳng tới Đã duyệt (terminal)"
- Amber warning khi checked: "Hành động KHÔNG quay lại được"
- Mutation payload +skipToFinal: !isReject && skipToFinalApprover
- onSuccess reset state

Type ApprovalWorkflowOptions × 2 app: +allowApproverSkipToFinal: boolean (7th)
Type PeDetailBundle × 2 app: REMOVE drafterAllowSkipToFinal field + comment Mig 29+30+31

UX design Dialog approach (consistent với Trả lại Mode picker pattern):
- Skip thẳng Cấp cuối = destructive action → confirm dialog amber warning
- Mirror Mig 28 Trả lại 4 mode picker UX consistency
- Em main solo K6 per UX flow decision criteria

Per bro decision Plan K S23 t1: "Chỗ cấu hình cho phép skip → duyệt thẳng cho phép
trong trạng thái đang duyệt" + "Tất cả đều cấu hình ngay trong chỗ setup quy trình duyệt".

Verify:
- npm run build × 2 app pass clean (0 TS err)
- Pre-existing warnings unchanged (chunk size + INEFFECTIVE_DYNAMIC_IMPORT)
- Bundle hash rotated × 2 app

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:34:39 +07:00
2ea8977d0f [CLAUDE] Backout: Chunk D — K5 cleanup F2 zombie endpoint + UsersPage column + DTO field
Reviewer K2 Major #1: PATCH /api/users/{id}/allow-skip-final endpoint Admin tick =
NoOp swallow silent (K1 sentinel → confusion UX). Full backout Plan D S22 stack:

BE drop (7 files):
- UsersController.cs: DELETE PATCH /allow-skip-final endpoint + SetAllowDrafterSkipToFinalBody record
- UserFeatures.cs: DELETE SetUserAllowDrafterSkipToFinalCommand + Handler
                    + UserDto.AllowDrafterSkipToFinal field
                    + list/get DTO mapping sentinel-false references
- ApprovalWorkflow.cs: REWRITE stale narrative line 78-80 (Reviewer Major #2 Mig 31 semantic)
                       + docstring AllowApproverSkipToFinal line 108 clean stale Users storage ref
- PurchaseEvaluationFeatures.cs: REWRITE Command DTO comment line 401 (Reviewer Minor #3)
- ApprovalWorkflowConfiguration.cs: APPEND Mig 31 narrative line 22-24 (Reviewer Minor #4)
                                     + clean storage move comment line 87
- ApprovalWorkflowV2AdminFeatures.cs: clean DTO comment line 58 stale "F2 xuống User table"
- IPurchaseEvaluationWorkflowService.cs + PurchaseEvaluationDtos.cs: clean stale
  "storage Users.AllowDrafterSkipToFinal" comments

FE Admin drop (2 files):
- UsersPage.tsx: DELETE "Skip cuối" column + FastForward badge + 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 leftover deferred K6).

Plan K refactor F2 storage Users → Levels (Mig 31) complete cumulative cleanup.
Pattern reusable: post-refactor full cleanup (BE endpoint + Command + DTO + FE column
+ types + stale narratives) atomic 1 commit thay vì leak zombie state.

Verify:
- dotnet build production projects 0 err (2 pre-existing DocxRenderer warn)
- npm build fe-admin 0 TS err (no new warning)
- Grep AllowDrafterSkipToFinal + allow-skip-final + allowDrafterSkipToFinal zero results
  across src/Backend (excl Migrations history) + fe-admin/src

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:27:12 +07:00
dd52d16ca9 [CLAUDE] FE-Admin: Chunk C — Mig 31 K3 Designer 7th checkbox AllowApproverSkipToFinal + banner rewrite
ApprovalWorkflowsV2Page Designer inline panel mỗi Level entry thêm checkbox thứ 7:
"Cho phép duyệt thẳng Cấp cuối khi đang duyệt" (F2 admin opt-in per-slot Approver).
Group cuối list sau F4 AllowApproverEditBudget (Mig 30) — pattern mirror Mig 29/30
admin opt-in reinforced 3× cumulative.

Types LevelDto + EditLevelEntry +allowApproverSkipToFinal: boolean field.
Helper makeDefaultLevelEntry default false (opt-out — admin tick explicit).
Helper copyFromDefinition propagate flag từ workflow cũ.
POST/PATCH mutation body propagate 7th flag mỗi Level entry.

Banner line ~623-631 rewrite: "F2 cấu hình ở User Management" (Plan D S22 wire) →
"Cấu hình quyền duyệt riêng cho từng NV trong slot Approver bên dưới" — phản ánh
schema Mig 31 (F2 storage moved per-slot).

Per bro decision S23 t1 Plan K: "Tất cả đều cấu hình ngay trong chỗ setup quy trình duyệt".

Verify:
- npm run build fe-admin pass clean
- 0 TS error
- Bundle size 1395.74 KB (unchanged trivial)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:19:48 +07:00
364aef63fd [CLAUDE] PurchaseEvaluation: Chunk B — Mig 31 K2 Approver F2 branch APPROVE STEP + DTO refactor
Service ApproveV2Async +skipToFinal 8th param. APPROVE STEP branch sau UPSERT
PEL opinion: check admin OR matchingLevel.AllowApproverSkipToFinal → set
Phase=DaDuyet terminal directly, clear pointer + SLA, audit "[Approver duyệt
thẳng Cấp cuối — Bước X Cấp Y → DaDuyet]". Non-admin + flag off → ConflictException.

ApproveV1LegacyAsync: throw nếu skipToFinal=true non-admin (V1 legacy không
hỗ trợ per-Approver-slot flag).

Caller TransitionAsync line ~144 pass skipToFinal vào ApproveV2Async.
Drafter SUBMIT branch ignore skipToFinal (K1 đã remove F2 Drafter semantic
stub) — Mig 31 marker comment cleanup.

DTO ApprovalWorkflowOptionsDto +bool AllowApproverSkipToFinal (7th field).
DTO PurchaseEvaluationDetailBundleDto -DrafterAllowSkipToFinal field.
GetPe handler populate 7 Allow* từ curLevel (Mig 29+30+31 cumulative).
Sentinel `var drafterAllowSkipToFinal = false;` cleanup từ K1.

IPurchaseEvaluationWorkflowService.cs comment skipToFinal semantic refactor:
Drafter from Nháp → Approver during ChoDuyet skip thẳng Cấp cuối.

Pattern reusable: feedback_per_nv_permission_scope.md reinforced 3× cumulative
(Mig 29 F1+F3 + Mig 30 F4 + Mig 31 F2).

Verify:
- dotnet build production projects clean (0 err, 2 warnings pre-existing DocxRenderer)
- Test fail at K1 expected (test file references removed prop, K7 sẽ fix)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:08:11 +07:00
db6625304a [CLAUDE] Domain: Chunk A — Mig 31 swap F2 storage Users→ApprovalWorkflowLevels (Approver scope ChoDuyet)
Mig 31 RefactorSkipToFinalToApproverLevel — 2 stage manual reorder:
- ADD ApprovalWorkflowLevels.AllowApproverSkipToFinal bit NOT NULL DEFAULT 0
- DROP Users.AllowDrafterSkipToFinal (semantic mới khác hẳn — admin re-config qua Designer)
- NO BACKFILL (Option A — accept lose 4 prod user value per K0-bis audit)

Plan K refactor F2 semantic: Drafter from Nháp → Approver during ChoDuyet skip thẳng Cấp cuối.
Mirror F3+F4 admin opt-in per-Approver-slot pattern (Mig 29 + Mig 30) reinforced 3× cumulative.

Service line 121-157 F2 Drafter SUBMIT branch REMOVED stub (K2 sẽ add Approver F2 branch
trong APPROVE STEP line ~393-525). TransitionAsync skipToFinal param 8th KEPT cho K2 repurpose.

Application layer compile-break fix transient: UserDto field mapping + GET handler + LIST
handler + SetUserAllowDrafterSkipToFinalCommandHandler NoOp + PurchaseEvaluationFeatures
drafter flag → sentinel false. DTO + Command signature UNCHANGED (K2 chunk Chủ trì sẽ
refactor DTO/Command theo plan).

4 prod user (fin.pp + pm.nv + nv.test + truong.nguyen) lose AllowDrafterSkipToFinal=true
per bro Option A. Audit trail trong session log K8.

Verify:
- dotnet ef migrations add pass
- dotnet ef database update Dev + Design pass (Mig 31 applied both DB)
- dotnet build src/Backend/SolutionErp.Api production projects clean (0 err, 0 warn)
- dotnet test SKIPPED per UAT mode (memory feedback_uat_skip_verify) — K7 chunk fix
  remaining PurchaseEvaluationWorkflowServiceReturnModeTests.cs:253 reference

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:03:05 +07:00
56868bfd7f [CLAUDE] FE-Admin: Chunk pre-A — Mig 31 prep: rename slot label "#NV {order}" -> "Họ tên" user pin
UX polish trong ApprovalWorkflowsV2Page Designer slot label hiển thị tên user pin
(lookup từ usersList query) thay vì số thứ tự "NV #{ei + 1}". Fallback "Chưa chọn NV"
khi slot chưa pick user.

Plan K pre-Mig 31 first chunk. Mig 31 sẽ thêm AllowApproverSkipToFinal 7th checkbox
inline panel (K3 chunk sau).

Per bro decision S23 t1: "Chỗ quy trình duyệt #NV 1 - Họ tên luôn".

Verify:
- npm run build fe-admin pass
- 0 TS error

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:53:17 +07:00
31 changed files with 4745 additions and 322 deletions

View File

@ -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.)

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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.)

View File

@ -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.**)

View File

@ -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 ADDDROP 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).

View File

@ -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`.

View File

@ -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>

View File

@ -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 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>

View File

@ -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>
) )

View File

@ -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>

View File

@ -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

View File

@ -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.

View File

@ -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>

View File

@ -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 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>

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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,

View File

@ -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)

View File

@ -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,

View File

@ -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)));
}
}

View File

@ -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; }
} }

View File

@ -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; }
} }

View File

@ -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);
} }
} }

View File

@ -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);
}
}
}

View File

@ -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");

View File

@ -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) evaluation.CurrentWorkflowStepIndex = 0;
{ // Chỉ init levelOrder=1 nếu pin schema V2 (ApprovalWorkflowId set).
if (!isAdmin) evaluation.CurrentApprovalLevelOrder = evaluation.ApprovalWorkflowId is not null ? 1 : null;
{
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;
// Chỉ init levelOrder=1 nếu pin schema V2 (ApprovalWorkflowId set).
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)
{ {

View File

@ -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");
}
}
} }