[CLAUDE] Docs: chốt Session 18 wrap-up — PE V2 polish + Clone B + Mig 25 IsUserSelectable + 4 bug fix UAT

Session 18 (16:56 → 19:45, 7 commit `aaa1c6c` → `32a8d4d`):
- B1 Pe Duyệt filter cứng "Đã gửi duyệt"
- B2 HistoryTab filter Trả lại / Gửi lại
- B3 Clone V2 cho B (DuyetNccPhuongAn) — audit reuse pattern
- B4 Fix silent 403 ApprovalWorkflowsV2Controller
- B5 Fix sidebar highlight queryMatches transient keys
- B6 Mig 25 IsUserSelectable + Designer pin toggle + bỏ "(clone)" + Workspace filter
- B7 Cleanup orphan zip files

Updates:
- STATUS — header 24→25 mig + 43→44 gotcha + 1 row Recently Done top + session log link
- HANDOFF — TL;DR S18 đầy đủ + cảnh báo S19+ (giữ S17 narrative §6.5)
- CLAUDE.md root — count 25 mig + Mig 25 description block
- schema-diagram §14 — heading 22→25 + cột IsUserSelectable + filter logic section + Pending S19+ Mig 26/27
- gotchas — +#44 silent 403 + checklist debug 21
- migration-todos — Phase 9 S18 done section
- session log mới đầy đủ E2E narrative

Stats: 25 mig, 58 tables, ~141 endpoints, 81 test pass (no change), 44 gotcha, 14 memory entries, 6 skill, 7 commit S18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-05-08 19:56:42 +07:00
parent 32a8d4db0b
commit daad79d282
7 changed files with 511 additions and 10 deletions

View File

@ -50,7 +50,7 @@ Kiến trúc: **.NET 10 Clean Architecture + 2 React FE (admin + user) + SQL Ser
- Audit fields: `CreatedAt`, `UpdatedAt`, `CreatedBy`, `UpdatedBy` (`BaseEntity`) - Audit fields: `CreatedAt`, `UpdatedAt`, `CreatedBy`, `UpdatedBy` (`BaseEntity`)
- Soft delete: `IsDeleted`, `DeletedAt`, `DeletedBy` (`AuditableEntity`) - Soft delete: `IsDeleted`, `DeletedAt`, `DeletedBy` (`AuditableEntity`)
- Migrations: `dotnet ef migrations add <Name> --project src/Backend/SolutionErp.Infrastructure --startup-project src/Backend/SolutionErp.Api` - Migrations: `dotnet ef migrations add <Name> --project src/Backend/SolutionErp.Infrastructure --startup-project src/Backend/SolutionErp.Api`
- **Hiện có 24 migration → 58 bảng** (Phase 9+ Session 17 wrap-up — Mig 22-24 V2 schema PE end-to-end: Quy trình > Bước (Phòng) > Cấp (N NV cụ thể qua ApproverUserId, OR-of-N cùng cấp). 3 bảng mới `ApprovalWorkflows` + Steps + Levels. Mig 23 pin `PE.ApprovalWorkflowId`. Mig 24 `PE.CurrentApprovalLevelOrder` track Cấp đang chờ. State machine 5 trạng thái: Nháp / Đã gửi duyệt / Trả lại (Phase riêng TraLai=98) / Từ chối / Đã duyệt. PE Service V2 wire: iterate Steps/Levels match `actor.Id == ApproverUserId`. Contract V2 chưa wire (defer session sau). 81 test pass. Mig 21 V1 flat workflow Mig 21 vẫn live cho phiếu cũ.) - **Hiện có 25 migration → 58 bảng** (Phase 9+ Session 18 wrap-up — Mig 25 `AddIsUserSelectableToApprovalWorkflows`: ALTER `ApprovalWorkflows` +`IsUserSelectable bit` (admin pin/unpin workflow nào cho user pick lúc create phiếu, multi-select độc lập IsActive). Backfill `WHERE IsActive=1 SET 1` giữ behavior cũ. Designer +badge "Cho user chọn" + button Ghim/Bỏ ghim. Workspace filter dropdown chỉ workflows `IsUserSelectable=true`. Mig 22-24 V2 schema (Session 17): `ApprovalWorkflows`/Steps/Levels — Quy trình > Bước (Phòng) > Cấp (N NV cụ thể qua ApproverUserId, OR-of-N cùng cấp). PE.ApprovalWorkflowId pin V2. PE.CurrentApprovalLevelOrder track. State machine 5 trạng thái: Nháp / Đã gửi duyệt / Trả lại (Phase riêng TraLai=98) / Từ chối / Đã duyệt. PE Service V2 wire match `actor.Id == ApproverUserId`. Contract V2 chưa wire (Mig 26 defer Session 19+). 81 test pass. Mig 21 V1 flat workflow vẫn live cho phiếu cũ.)
### Modules ### Modules

View File

@ -1,6 +1,162 @@
# HANDOFF — Brief 5 phút cho session tiếp theo # HANDOFF — Brief 5 phút cho session tiếp theo
**Last updated:** 2026-05-08 (Session 17 wrap-up — **🎯 PE Workflow V2 schema + Service WIRE end-to-end DONE. 13 commit `c847dc0``de0f38d`. Mig 22-24. State machine 5 trạng thái. Service PE iterate Steps/Levels match `ApproverUserId`. Designer max 3 cấp × N NV/cấp. Panel 3 flow render thực tế. Test prod cleaned + user `nv.test@solutions.com.vn` tạo. 81 test pass. Contract V2 wire DEFER session sau.**) **Last updated:** 2026-05-08 19:45 (Session 18 wrap-up — **🎯 PE V2 polish + Clone B (DuyetNccPhuongAn) + 4 bug fix UAT + Mig 25 IsUserSelectable. 7 commit `aaa1c6c``32a8d4d`. Audit reuse pattern (memory `feedback_audit_reuse_before_clone`): clone B chỉ 3 file ~60 LOC vì schema chung qua ApplicableType discriminator. Bug silent 403 từ class-level Authorize policy quá strict — Drafter không list workflow để pick, Workspace dropdown empty không warning. Fix: class-level `[Authorize]` only, GET endpoint không cần `Workflows.Read`. Bug sidebar highlight mất khi click row do queryMatches exact-set vs URL có `id` transient → strip TRANSIENT_QUERY_KEYS trước compare. Mig 25 ALTER ApprovalWorkflows +IsUserSelectable bit (admin pin/unpin per version, multi-select, độc lập IsActive). Designer +badge "Cho user chọn" + button Ghim/Bỏ ghim. Workspace filter only IsUserSelectable. Bỏ "(clone)" auto-suffix khi clone version. Pe Duyệt: bỏ dropdown trạng thái + filter cứng "Đã gửi duyệt". Lịch sử thay đổi: chỉ events Trả lại / Gửi lại / sửa khi phase=TraLai (BE keep audit, FE filter). 81 test pass (no change — UAT defer test §7). 44 gotcha (+1 silent 403).**)
## TL;DR Session 18 — PE V2 polish + Clone B + 4 bug fix UAT
User UAT live tiếp Session 17, 7 batch nhỏ + 1 feature lớn (Mig 25). Áp memory `feedback_uat_skip_verify` (skip dotnet test mỗi chunk, push ngay) + lesson `0ae3fe2`: rename/remove → BẮT BUỘC `npm run build`.
### B1 (`aaa1c6c`) — Pe Duyệt filter cứng "Đã gửi duyệt"
User: "Duyệt bỏ cái trạng thái đi, chỉ load những trạng thái 'Đã gửi duyệt' là đc."
- Bỏ dropdown "Tất cả trạng thái" khỏi UI khi `pendingMe=true`, thay bằng hint amber "Lọc cố định: Đã gửi duyệt (phiếu đang chờ duyệt)"
- Filter cứng client-side: `getPeDisplayStatus(p.phase) === DaGuiDuyet` — loại Nháp/Trả lại/Đã duyệt/Từ chối
- Header count dùng `rows.length` khi `pendingMe` (inbox không paged)
- Workaround BE `/inbox` loose UAT có thể trả phiếu Nháp (phân quyền strict V2 pending Session 19+)
- Mirror fe-admin + fe-user `PurchaseEvaluationsListPage.tsx`
### B2 (`917446d`) — HistoryTab filter Trả lại / Gửi duyệt lại
User: "Lịch sử thay đổi: chỉ bắt các dòng thay đổi khi trả lại và gửi duyệt lại thôi nhé, không cần bắt trạng thái duyệt và các thay đổi trước khi trả lại."
- FE filter trong `PeDetailTabs.HistoryTab`, BE giữ audit data đầy đủ (reversible nếu user đổi ý / cần audit trail compliance)
- Logic giữ:
- Workflow transition về TraLai (`phaseAtChange === 98`)
- Workflow transition từ TraLai (summary chứa `"TraLai →"`)
- Mọi thay đổi nội dung (Header/Detail/Supplier/Quote/Attachment) khi `phaseAtChange === 98`
- Bỏ: workflow Approve cùng cấp (Cấp 1→2→DaDuyet), sửa khi phase=Nháp/ChoDuyet ban đầu
- Empty state: "Chưa có lịch sử trả lại / gửi duyệt lại"
### B3 (`937eb24`) — Clone V2 cho B (DuyetNccPhuongAn)
User: "Quy trình chọn thầu phụ - NCC → Duyệt NCC đúng. Plan kế hoạch clone toàn bộ updates sang Duyệt NCC và Giải pháp."
Audit reuse trước thay vì duplicate. Phát hiện 80% đã chung:
- Schema V2 (Mig 22-24) qua `ApplicableType` enum
- BE Service `ApproveV2Async` không hardcode type
- App CQRS / API `/approval-workflows-v2?applicableType=N` dynamic
- FE Designer `ApprovalWorkflowsV2Page``TYPE_CODE_TO_INT` cả 3 type
- Layout regex `^AwV2_(.+)$` match dynamic typeCode
- App.tsx route `/system/approval-workflows-v2/:typeCode` dynamic
Chỉ thiếu cho B: **menu key + sample seed** (3 file ~60 LOC).
- `MenuKeys.cs` +const `ApprovalWorkflowDuyetNccPhuongAnV2 = "AwV2_DuyetNccPhuongAn"` + add vào `All[]`
- `DbInitializer.SeedMenusAsync` +leaf "Duyệt NCC và Giải pháp (Mới)" dưới root ApprovalWorkflowsV2 (Order=2 cạnh leaf A Order=1)
- `DbInitializer +SeedSampleApprovalWorkflowsV2Async` (idempotent — skip nếu admin đã tạo workflow B nào, hoặc thiếu test user `nv.test`/Phòng CCM): seed `QT-DN-PA-V2-001 v01` 1 Bước Phòng CCM × 1 Cấp NV test
- `fe-admin/lib/menuKeys.ts` +`AwV2_DuyetNccPhuongAn`
KHÔNG migration / Service / Designer page mới. Memory `feedback_audit_reuse_before_clone.md` capture pattern.
User feedback "OK khá tốt, 1 phát chạy luôn :))" sau verify → confirm approach.
### B4 (`f77ea38`) — Fix silent 403 ApprovalWorkflowsV2Controller
Triệu chứng: Drafter `nv.test` Workspace tạo phiếu B → dropdown "Quy trình duyệt" empty mặc dù Admin Designer thấy 2 version (v01 sample + v02 admin clone).
Root cause: Class-level `[Authorize(Policy = "Workflows.Read")]` → non-admin role 403 Forbidden khi GET `/api/approval-workflows-v2`. TanStack Query catch error không hiện UI → dropdown rỗng silent.
Fix:
- Class-level đổi `[Authorize]` only (any authenticated user)
- GET endpoint inherit class policy — Drafter list workflow để pick read-only, không nhạy cảm
- POST + DELETE giữ `[Authorize(Policy = "Workflows.Create")]` admin-only Designer
Pattern reusable cho Contract V2 Mig 26 sau.
### B5 (`a9c0857`) — Fix sidebar highlight queryMatches transient keys
Triệu chứng: Ở leaf "Danh sách" `/purchase-evaluations?type=1`, click chọn 1 phiếu → URL thành `?type=1&id=abc` → leaf bị mất highlight box (gotcha #34 cũ tái phát theo cách khác).
Root cause: `queryMatches` exact-set equality — target `{type}` (1 key) vs current `{type, id}` (2 keys) length mismatch → no match → leaf unhighlight.
Fix: `TRANSIENT_QUERY_KEYS = {id, q, editHeader, page, phase, awId}` — strip trước khi compare. Mọi key navigation identity (`type`, `pendingMe`, `mode`) check exact-set như cũ.
Edge cases verified:
| URL hiện tại | Target leaf | Match |
|---|---|---|
| `?type=1&id=abc` | Danh sách `?type=1` | ✓ giữ highlight |
| `?type=1&pendingMe=1` | Danh sách `?type=1` | ✗ distinct (không cross-highlight Pending) |
| `?type=1&phase=10` | Danh sách `?type=1` | ✓ giữ highlight (filter dropdown) |
| `?type=1&pendingMe=1&awId=xyz` | Duyệt `?type=1&pendingMe=1` | ✓ giữ highlight |
Mirror fe-admin + fe-user `Layout.tsx`.
### B6 (`2a53107`) — Mig 25 IsUserSelectable + Designer pin toggle + bỏ "(clone)"
User feedback xem Admin Designer: "Bỏ chữ Clone đi nhé, ghi v02, v03... là đủ rồi. Thêm cho tao nút stick để chọn các quy trình nào mà User đc select bên ngoài khi tạo phiếu."
**Bỏ "(clone)":** Designer auto-fill `name = cloneFrom.name` (bỏ ` (clone)` suffix). Version số đã đủ phân biệt.
**Pin toggle "Cho user chọn":**
- **Migration 25** `AddIsUserSelectableToApprovalWorkflows`:
```sql
ALTER TABLE ApprovalWorkflows ADD IsUserSelectable bit NOT NULL DEFAULT 0;
-- Backfill (giữ behavior cũ — active workflows vẫn pickable):
UPDATE ApprovalWorkflows SET IsUserSelectable = 1 WHERE IsActive = 1;
```
- **Domain** `ApprovalWorkflow.IsUserSelectable` — independent với `IsActive`, multiple versions có thể cùng selectable (admin có thể "ghim" nhiều version cho user pick).
- **App CQRS:**
- `AwDefinitionDto` +field `IsUserSelectable`
- `CreateAwDefinitionCommand` Handler set default `true` cho version mới (mirror IsActive default)
- New `SetAwUserSelectableCommand(Guid Id, bool IsUserSelectable)` + Handler — toggle
- **API** `PATCH /api/approval-workflows-v2/{id}/user-selectable` policy `Workflows.Create` (admin only)
- **DbInitializer** `SeedSampleApprovalWorkflowsV2Async` +`IsUserSelectable = true`
- **FE Designer** (`fe-admin/ApprovalWorkflowsV2Page.tsx`):
- `DefinitionDto` +`isUserSelectable`
- Badge amber "📌 Cho user chọn" cạnh badge IsActive/Archived khi `isUserSelectable === true`
- Button "📌 Ghim cho user / 🚫 Bỏ ghim" trong action group + mutation `toggleSelectable` (call PATCH endpoint, invalidate query)
- **FE Workspace** (cả fe-admin + fe-user `PeWorkspaceCreateView.tsx`):
- approvalWorkflows query `.filter(w => w.isUserSelectable)` — chỉ workflows admin đã ghim hiện trong dropdown user
### B7 (`32a8d4d`) — Cleanup orphan zip files
`.claude.zip + docs.zip` từ harness session start lỡ tay vào `git add -A` ở B6 commit. Untrack + add `*.zip` rule `.gitignore`.
### Stats Δ Session 18
| | Trước S18 | Sau S18 |
|---|---:|---:|
| Migrations | 24 | **25** (+1) |
| DB tables | 58 | 58 (Mig 25 chỉ ALTER cột) |
| API endpoints | ~140 | **~141** (+1 PATCH user-selectable) |
| FE pages | 33 | 33 (modify existing only) |
| Test pass | 81 | 81 (no change — UAT feature defer test §7) |
| Gotchas | 43 | **44** (+1 silent 403) |
| Memory entries | 13 | **14** (+1 audit reuse pattern) |
| Skills | 6 | 6 (no add) |
| Commits | (after S17) | **+7** |
## ⚠️ Điều quan trọng cho Session 19+
1. **Contract V2 wire (Mig 26) — pending dedicated session.** Pattern audit-reuse áp dụng: phần lớn đã chung. Mirror PE pattern:
- Thêm `Contract.ApprovalWorkflowId` + `CurrentApprovalLevelOrder` (Mig 26)
- `ContractWorkflowService.ApproveV2Async` mirror PE pattern
- `ContractCreatePage` Workspace Select V2
- Pin V2 mặc định cho ContractType
- Permission GET endpoint đã permissive (Session 18 fix), không cần đụng
2. **Phân quyền strict V2 V2** — hiện loose UAT (mọi authenticated thấy mọi phiếu V2). Sau confirm flow:
- List = Drafter + approver any-Step + Admin
- Inbox = chỉ approver Cấp hiện tại (V2 đã đúng — `ResolveV2InboxIdsAsync`)
- Detail = same as List
- Cũng giải quyết được bug "/inbox loose trả phiếu Nháp" — sau khi strict, B1 FE filter có thể relax nếu BE đã filter đúng
3. **Drop legacy V1 cleanup** sau khi không còn phiếu pin `WorkflowDefinitionId` (V1):
- Drop tables `WorkflowDefinitions` + `WorkflowSteps` + `WorkflowStepApprovers` + PE versions
- Mig 27 cleanup drop column `RejectedAtStepIndex` + `RejectedFromPhase` deprecated S17
- Drop `ApproveV1LegacyAsync` branch trong Service
4. **Test V2 wire** (defer khi UAT confirm + có sample data) — Domain test `ApproveV2Async` match logic + transient TraLai entry → Cấp 1 reset.
5. **`feedback_audit_reuse_before_clone` memory** — áp dụng cho mọi "clone X sang Y" / "thêm type Z mới" sau này. List "đã chung" vs "còn thiếu" trước khi propose plan.
6. **Sample seed B sample** chạy với check `hasAnyB` — sau UAT có thể remove sample seed (admin đã tạo workflow thật). Hoặc giữ làm fallback. Idempotent skip nếu admin có workflow B → không clobber.
---
## TL;DR Session 17 — PE V2 schema end-to-end
## TL;DR Session 17 — PE V2 schema end-to-end ## TL;DR Session 17 — PE V2 schema end-to-end

View File

@ -2,9 +2,9 @@
> **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-08 (Session 17 wrap-up — **🎯 PE Workflow V2 SCHEMA + Service WIRE end-to-end DONE. 13 commit từ `c847dc0``de0f38d`. Mig 22-24 (3 migration mới): ApprovalWorkflowsV2 schema (3 bảng) + Pin ApprovalWorkflowId vào PE + CurrentApprovalLevelOrder tracking. State machine 5 trạng thái (Nháp/Đã gửi duyệt/Trả lại/Từ chối/Đã duyệt) — Trả lại = Phase RIÊNG TraLai=98 (Option A: Drafter sửa+gửi lại chạy LẠI từ đầu, KHÔNG jump-back). PE Service V2 wire — iterate ApprovalWorkflowSteps + Levels + match `actor.Id == ApproverUserId` (1 NV cụ thể, không OR-of-many group). Designer FE max 3 Cấp/Bước × N NV/Cấp + sequential gating + filter NV theo Phòng. Workspace Select pin V2. Inbox V2-aware (ResolveV2InboxIdsAsync). Banner "đến lượt bạn" + button Duyệt disable non-approver + tooltip. Panel 3 render flow workflow thực tế thay phase cards (Bước/Cấp với Status Done/Current/Pending). 81 test pass (+4 từ baseline 77). Test prod cleaned (9 PE + 11 HĐ + 19 Notif xóa). Test user `nv.test@solutions.com.vn`/`TestUser@123456` tạo. Contract V2 wire DEFER session sau.**) **Last updated:** 2026-05-08 19:45 (Session 18 wrap-up — **🎯 PE V2 polish + Clone B (DuyetNccPhuongAn) + 4 bug fix UAT + Mig 25 IsUserSelectable. 7 commit từ `aaa1c6c``32a8d4d`. Audit reuse trước khi clone — schema chung qua ApplicableType discriminator → chỉ thêm 3 file (~60 LOC) cho B (memory mới `feedback_audit_reuse_before_clone`). Bug fix: (1) silent 403 từ class-level Authorize policy quá strict (Drafter không list workflow để pick), (2) sidebar highlight mất khi click row do queryMatches exact-set vs URL có id transient. Mig 25 `AddIsUserSelectableToApprovalWorkflows` — admin pin/unpin workflow nào cho user pick (independent IsActive, multiple selectable đồng thời). Designer +badge "Cho user chọn" + button Ghim/Bỏ ghim. Workspace dropdown filter `isUserSelectable=true` only. Bỏ "(clone)" auto-suffix khi clone version. UAT iter B chạy 1 phát: sample seed `QT-DN-PA-V2-001 v01` 1 Bước CCM × 1 Cấp NV test. Filter UI Pe Duyệt: bỏ dropdown trạng thái + lọc cứng "Đã gửi duyệt" client-side. Lịch sử thay đổi: chỉ events Trả lại / Gửi duyệt lại / sửa khi phase=TraLai (BE giữ audit data đầy đủ, FE filter).**)
## 📍 Phase hiện tại: **Phase 9 active — UAT V2 testing với user thật** — **58 DB tables (+3 Mig 22 ApprovalWorkflows + Steps + Levels), 24 migrations (+3: Mig 22/23/24), ~140 API endpoints (+5 V2 routes), 33 FE pages (+1 Designer V2). 81 unit test pass** (58 Domain + 23 Infra). 43 gotcha (+2 Session 17: dual schema branch + Step.Order ≠ index). 30 demo user + 1 test user UAT. 6 skill. **5 trạng thái phiếu** (Nháp/Đã gửi duyệt/Trả lại/Từ chối/Đã duyệt). **2 Workflow schemas đồng tồn tại** post-Session 17: (1) Mig 21 `WorkflowDefinition` flat (V1) — pin với PE/Contract cũ + match Dept+PositionLevel. (2) Mig 22-24 `ApprovalWorkflow` (V2) — pin với PE mới + match ApproverUserId 1-1, Steps/Levels group by Order, Bước (Phòng) > Cấp (N NV OR-of-N). Service PE branch theo `ApprovalWorkflowId` set or null. Sau UAT chốt → migrate + drop V1 + Contract V2 wire. ## 📍 Phase hiện tại: **Phase 9 active — UAT V2 testing với user thật** — **58 DB tables (no new — Mig 25 chỉ ALTER cột IsUserSelectable), 25 migrations (+1 Mig 25), ~141 API endpoints (+1 PATCH /approval-workflows-v2/{id}/user-selectable), 33 FE pages. 81 unit test pass** (58 Domain + 23 Infra — no change S18, feature mới UAT defer test theo §7). 44 gotcha (+1 Session 18: silent 403 từ class-level Authorize policy). 30 demo user + 1 test user UAT. 6 skill. **5 trạng thái phiếu** (Nháp/Đã gửi duyệt/Trả lại/Từ chối/Đã duyệt). **2 Workflow schemas đồng tồn tại** post-Session 17: (1) Mig 21 `WorkflowDefinition` flat (V1) — pin với PE/Contract cũ + match Dept+PositionLevel. (2) Mig 22-25 `ApprovalWorkflow` (V2) — pin với PE mới + match ApproverUserId 1-1, Steps/Levels group by Order, Bước (Phòng) > Cấp (N NV OR-of-N), Mig 25 +IsUserSelectable admin pin per version. Service PE branch theo `ApprovalWorkflowId` set or null. Sau UAT chốt → migrate + drop V1 + Contract V2 wire.
### 🌐 Production URLs ### 🌐 Production URLs
@ -61,6 +61,7 @@
| Ngày | Ai | Task | Commit | | Ngày | Ai | Task | Commit |
|---|---|---|---| |---|---|---|---|
| 2026-05-08 19:45 | Claude | **🎯 SESSION 18 WRAP-UP — PE V2 polish + Clone B (DuyetNccPhuongAn) + 4 bug fix UAT + Mig 25 IsUserSelectable (7 commit `aaa1c6c``32a8d4d`)** — User UAT live tiếp Session 17, request chuỗi polish nhỏ + clone V2 cho type B. Áp memory `feedback_uat_skip_verify` (skip dotnet test mỗi chunk, push ngay) + lesson rename/remove → bắt buộc `npm run build`. **B1 (`aaa1c6c`)** Pe Duyệt (`?pendingMe=1`): bỏ dropdown "Tất cả trạng thái" + filter cứng client-side `getPeDisplayStatus === DaGuiDuyet` (loại Nháp/Trả lại/Đã duyệt/Từ chối). Hint amber "Lọc cố định: Đã gửi duyệt". Header count dùng `rows.length` (inbox không paged). Workaround BE /inbox loose UAT trả phiếu Nháp. Mirror fe-admin + fe-user. **B2 (`917446d`)** PeDetailTabs HistoryTab filter chỉ events Trả lại/Gửi duyệt lại: workflow transition về TraLai (phaseAtChange=98) + transition từ TraLai (summary chứa "TraLai →") + sửa nội dung khi phaseAtChange=TraLai. BE giữ audit data đầy đủ, chỉ FE filter (reversible). Empty state "Chưa có lịch sử trả lại / gửi duyệt lại". Mirror cả 2 app. **B3 (`937eb24`) Clone V2 cho B (DuyetNccPhuongAn)** — User chốt "Quy trình chọn thầu phụ - NCC → Duyệt NCC đúng. Clone toàn bộ updates sang Duyệt NCC và Giải pháp". Audit phát hiện 80% chung qua `ApplicableType` discriminator → chỉ thêm 3 file ~60 LOC: (a) `MenuKeys.cs` +const `ApprovalWorkflowDuyetNccPhuongAnV2` + add vào `All[]`. (b) `DbInitializer.SeedMenusAsync` +leaf "Duyệt NCC và Giải pháp (Mới)" dưới root ApprovalWorkflowsV2 + new method `SeedSampleApprovalWorkflowsV2Async` seed `QT-DN-PA-V2-001 v01` (1 Bước Phòng CCM × 1 Cấp NV test, idempotent). (c) `fe-admin/lib/menuKeys.ts` +`AwV2_DuyetNccPhuongAn`. KHÔNG migration / Service / Designer page (Layout regex `^AwV2_(.+)$` đã match dynamic, ApprovalWorkflowsV2Page có `TYPE_CODE_TO_INT` cả 3 type). Rút memory `feedback_audit_reuse_before_clone.md`. **B4 (`f77ea38`) Fix permission silent 403** — Drafter `nv.test` Workspace dropdown empty mặc dù seed OK. Root: class-level `[Authorize(Policy = "Workflows.Read")]` → non-admin 403, TanStack Query catch silent → UI empty không warning. Fix: class-level `[Authorize]` only (any authenticated). GET = list workflow read-only không nhạy cảm; POST + DELETE giữ `Workflows.Create` admin-only. Pattern reusable cho Contract V2 sau. **B5 (`a9c0857`) Fix sidebar highlight queryMatches** — Click phiếu trong leaf "Danh sách" → URL `?type=1&id=abc` → menu mất highlight (gotcha #34 cũ tái phát). Root: queryMatches exact-set equality {type} vs {type, id} length mismatch. Fix: `TRANSIENT_QUERY_KEYS = {id, q, editHeader, page, phase, awId}` strip trước compare. Edge case verified: Danh sách `?type=1` vs Pending `?type=1&pendingMe=1` distinct (không cross-highlight). Mirror cả 2 app Layout.tsx. **B6 (`2a53107`) Mig 25 + Designer pin toggle + bỏ "(clone)" + Workspace filter** — User feedback Admin Designer: bỏ "(clone)" auto-suffix khi clone version (version đã đủ phân biệt) + thêm pin toggle "Cho user pick lúc create phiếu" (multi-select, độc lập IsActive). Migration 25 `AddIsUserSelectableToApprovalWorkflows`: ALTER ApprovalWorkflows +`IsUserSelectable bit NOT NULL DEFAULT 0` + Sql backfill `UPDATE WHERE IsActive=1 SET 1` (giữ behavior cũ active workflow vẫn pickable). Domain ApprovalWorkflow +property. DTO AwDefinitionDto +field. CreateAwDefinitionCommand set default `true` cho version mới (mirror IsActive). New `SetAwUserSelectableCommand` + Handler. API `PATCH /api/approval-workflows-v2/{id}/user-selectable` policy `Workflows.Create`. DbInitializer SeedSampleApprovalWorkflowsV2Async +`IsUserSelectable=true`. FE Designer: `DefinitionDto` +field; badge amber "📌 Cho user chọn"; button "Ghim cho user / Bỏ ghim" + mutation `toggleSelectable`. Designer `name = cloneFrom.name` (bỏ ` (clone)` suffix). Workspace fetch filter `w.isUserSelectable === true` (cả fe-admin + fe-user). **B7 (`32a8d4d`)** Cleanup orphan `.claude.zip + docs.zip` từ harness session start, +`*.zip` rule .gitignore. **Cumulative Session 18:** 25 mig (+1), 58 tables (no new), ~141 endpoints (+1), 33 FE pages, **81 test pass** (no change — feature mới UAT defer test theo §7), 44 gotcha (+1 silent 403). Memory +1 entry. **Pending Session 19+:** Contract V2 wire (Mig 26 mirror PE), phân quyền strict V2, drop legacy V1 cleanup. | `aaa1c6c` (B1) · `917446d` (B2) · `937eb24` (B3) · `f77ea38` (B4) · `a9c0857` (B5) · `2a53107` (B6) · `32a8d4d` (B7) |
| 2026-05-08 | Claude | **🎯 SESSION 17 WRAP-UP — PE Workflow V2 schema + Service wire end-to-end (13 commit `c847dc0``de0f38d`)** — User chốt sau Session 16 "Thấy vẫn không đúng" → viết lại schema riêng + thêm Menu "Duyệt NCC (Mới)" UAT. Cấu trúc rõ ràng: Quy trình > Bước (Phòng) > Cấp (NV cụ thể qua ApproverUserId). 3 chunk lớn: **Schema design + Designer** (Mig 22 — `c847dc0/f6047d5/2781c7e/12daa7f`): 3 entity ApprovalWorkflow/Step/Level + enum ApplicableType (DuyetNcc/DuyetNccPhuongAn/Contract). Designer page `/system/approval-workflows-v2/:typeCode` — iter 1 lock 3 cấp (`9712778`, sai intent) → iter 2 đúng intent max 3 cấp × N NV/cấp + sequential gating C2/C3 disabled khi cấp trước empty + filter NV theo Phòng + no-dup same level (`f3bea3c`). Validator BE Order∈{1,2,3} + HaveSequentialOrders + HaveNoDuplicateApproverInSameLevel. **State machine 5 trạng thái** (`ff21120`): Nháp→Đã gửi duyệt→Đã duyệt (terminal) | Trả lại (Phase riêng TraLai=98, KHÔNG revert DangSoanThao + KHÔNG jump-back) | Từ chối (terminal). Drafter từ TraLai sửa+gửi lại chạy LẠI từ Cấp 1 Bước 1 (Option A user chốt diagram). PE/Contract/Budget Phase enum +TraLai=98 + Policy + Service Reject branch trỏ → TraLai + bỏ smart-reject (RejectedAtStepIndex giữ DB column deprecated). 4 test mới TraLai entry point. **Pin V2 vào PE + Service wire** (Mig 23-24 — `0a40c65/b41484b`): PE.ApprovalWorkflowId Guid? + PE.CurrentApprovalLevelOrder int? + EF FK Restrict. CreatePurchaseEvaluationCommand+Validate ApplicableType match PE.Type. UpdateDraft cho phép sửa Phase=Nháp/TraLai. Workspace Select bắt buộc (filter ApplicableType=type). Service `ApproveV2Async` + `ApproveV1LegacyAsync` branch theo ApprovalWorkflowId set or null: V2 group Levels by Order = Cấp (OR-of-N approvers cùng cấp), match `actor.Id ∈ ApproverUserId`, advance levelOrder++ trong Step → idx++ + reset levelOrder=1 → DaDuyet. Synthetic Policy `ForV2Schema()` cho FE nextPhases (DangSoanThao/TraLai → ChoDuyet/TuChoi; ChoDuyet → ChoDuyet/TraLai/TuChoi). **UX V2-aware** (`d814429/9e63e2d/d250ae4/74745a7/de0f38d`): DTO `CurrentApproval { stepIdx, levelOrder, approvers[] }` + `ApprovalFlow { steps[]: { Order, Name, Dept, Levels[]: { Order, Approvers[], Status:Done/Current/Pending } } }`. Banner emerald "Đến lượt bạn" / amber "Không phải lượt bạn — chỉ {NV X / Y} duyệt được". Button Duyệt forward disabled khi V2 + actor không trong cấp + tooltip. Trả lại + Từ chối vẫn enabled (BE không gating reject theo cấp). Inbox V2-aware (`ResolveV2InboxIdsAsync` precompute Set IDs khớp actor.Id ∈ Cấp hiện tại). 2 dropdown filter "Quy trình duyệt" + "Trạng thái" (chỉ ở Duyệt sau user feedback, Danh sách giữ 1 dropdown trạng thái). Panel 3 thay 4 phase cards bằng flow workflow thực tế: Bước (icon ✓/●/○ + dept badge) → Cấp (icon nhỏ + label "đang chờ" / "đã duyệt" + tên NV). Phiếu V1 legacy fallback note. **Test setup** (`ac41d5e`): SQL `clean-transactional-uat.sql` xóa 9 PE + 11 HĐ + Budget + 19 Notif + reset CodeSequences trên prod, giữ master (Users/Suppliers/Projects/Departments/Workflows V1+V2). Tạo test user `nv.test@solutions.com.vn`/`TestUser@123456` (Drafter, Phòng CCM) qua API. **77→81 test pass** (+4 TraLai entry point Domain). FE rename "Bản nháp" → "Nháp" + ChoDuyet=10 + TraLai=98 thêm vào types/contracts.ts + types/budget.ts. **Pending session sau:** Contract V2 wire (mirror PE pattern), Budget V2 (defer xa hơn), phân quyền strict V2 (hiện loose UAT cho mọi authenticated user xem phiếu V2), drop legacy V1 sau khi UAT chốt + cleanup migration drop RejectedAtStepIndex/RejectedFromPhase. | 13 commit (xem `git log --since='2026-05-08'`) | | 2026-05-08 | Claude | **🎯 SESSION 17 WRAP-UP — PE Workflow V2 schema + Service wire end-to-end (13 commit `c847dc0``de0f38d`)** — User chốt sau Session 16 "Thấy vẫn không đúng" → viết lại schema riêng + thêm Menu "Duyệt NCC (Mới)" UAT. Cấu trúc rõ ràng: Quy trình > Bước (Phòng) > Cấp (NV cụ thể qua ApproverUserId). 3 chunk lớn: **Schema design + Designer** (Mig 22 — `c847dc0/f6047d5/2781c7e/12daa7f`): 3 entity ApprovalWorkflow/Step/Level + enum ApplicableType (DuyetNcc/DuyetNccPhuongAn/Contract). Designer page `/system/approval-workflows-v2/:typeCode` — iter 1 lock 3 cấp (`9712778`, sai intent) → iter 2 đúng intent max 3 cấp × N NV/cấp + sequential gating C2/C3 disabled khi cấp trước empty + filter NV theo Phòng + no-dup same level (`f3bea3c`). Validator BE Order∈{1,2,3} + HaveSequentialOrders + HaveNoDuplicateApproverInSameLevel. **State machine 5 trạng thái** (`ff21120`): Nháp→Đã gửi duyệt→Đã duyệt (terminal) | Trả lại (Phase riêng TraLai=98, KHÔNG revert DangSoanThao + KHÔNG jump-back) | Từ chối (terminal). Drafter từ TraLai sửa+gửi lại chạy LẠI từ Cấp 1 Bước 1 (Option A user chốt diagram). PE/Contract/Budget Phase enum +TraLai=98 + Policy + Service Reject branch trỏ → TraLai + bỏ smart-reject (RejectedAtStepIndex giữ DB column deprecated). 4 test mới TraLai entry point. **Pin V2 vào PE + Service wire** (Mig 23-24 — `0a40c65/b41484b`): PE.ApprovalWorkflowId Guid? + PE.CurrentApprovalLevelOrder int? + EF FK Restrict. CreatePurchaseEvaluationCommand+Validate ApplicableType match PE.Type. UpdateDraft cho phép sửa Phase=Nháp/TraLai. Workspace Select bắt buộc (filter ApplicableType=type). Service `ApproveV2Async` + `ApproveV1LegacyAsync` branch theo ApprovalWorkflowId set or null: V2 group Levels by Order = Cấp (OR-of-N approvers cùng cấp), match `actor.Id ∈ ApproverUserId`, advance levelOrder++ trong Step → idx++ + reset levelOrder=1 → DaDuyet. Synthetic Policy `ForV2Schema()` cho FE nextPhases (DangSoanThao/TraLai → ChoDuyet/TuChoi; ChoDuyet → ChoDuyet/TraLai/TuChoi). **UX V2-aware** (`d814429/9e63e2d/d250ae4/74745a7/de0f38d`): DTO `CurrentApproval { stepIdx, levelOrder, approvers[] }` + `ApprovalFlow { steps[]: { Order, Name, Dept, Levels[]: { Order, Approvers[], Status:Done/Current/Pending } } }`. Banner emerald "Đến lượt bạn" / amber "Không phải lượt bạn — chỉ {NV X / Y} duyệt được". Button Duyệt forward disabled khi V2 + actor không trong cấp + tooltip. Trả lại + Từ chối vẫn enabled (BE không gating reject theo cấp). Inbox V2-aware (`ResolveV2InboxIdsAsync` precompute Set IDs khớp actor.Id ∈ Cấp hiện tại). 2 dropdown filter "Quy trình duyệt" + "Trạng thái" (chỉ ở Duyệt sau user feedback, Danh sách giữ 1 dropdown trạng thái). Panel 3 thay 4 phase cards bằng flow workflow thực tế: Bước (icon ✓/●/○ + dept badge) → Cấp (icon nhỏ + label "đang chờ" / "đã duyệt" + tên NV). Phiếu V1 legacy fallback note. **Test setup** (`ac41d5e`): SQL `clean-transactional-uat.sql` xóa 9 PE + 11 HĐ + Budget + 19 Notif + reset CodeSequences trên prod, giữ master (Users/Suppliers/Projects/Departments/Workflows V1+V2). Tạo test user `nv.test@solutions.com.vn`/`TestUser@123456` (Drafter, Phòng CCM) qua API. **77→81 test pass** (+4 TraLai entry point Domain). FE rename "Bản nháp" → "Nháp" + ChoDuyet=10 + TraLai=98 thêm vào types/contracts.ts + types/budget.ts. **Pending session sau:** Contract V2 wire (mirror PE pattern), Budget V2 (defer xa hơn), phân quyền strict V2 (hiện loose UAT cho mọi authenticated user xem phiếu V2), drop legacy V1 sau khi UAT chốt + cleanup migration drop RejectedAtStepIndex/RejectedFromPhase. | 13 commit (xem `git log --since='2026-05-08'`) |
| 2026-05-08 | Claude | **🎯 SESSION 16 — DRASTIC REFACTOR flat workflow Phòng × Cấp (Mig 21, 2 commit Chunk A+B)** — Resume từ Session 15 defer plan. User chốt "bỏ phase enum hoàn toàn, dùng ChoDuyet=10 đơn nhất + currentStepIndex tracking". Per memory `feedback_drastic_refactor_scope`: dedicated session với context fresh, scope conservative 2x buffer (~8-10h estimate, actual ~3h). **Chunk A (`dbb0089`)** — Domain enum simplify (DangSoanThao=1, ChoDuyet=10 NEW, DaDuyet=7, TuChoi=99; legacy 2-6 + 98 deprecated giữ cho data cũ). WorkflowStep + DepartmentId Guid? FK Restrict + PositionLevel int? (PE + Contract mirror). PE/Contract entity + CurrentWorkflowStepIndex int? + RejectedAtStepIndex int?. Drop class WorkflowStepInnerStep + nav (PE + Contract). Drop *DepartmentApproval.InnerStepId column. EF Configurations: drop InnerStep config + restore simple unique non-filtered (Mig 19/20 filtered split reverse). DbContext drop DbSet<*WorkflowStepInnerStep> × 2. **Migration 21** `RefactorWorkflowToFlatModel` GỘP: 4 ALTER cols (PE/Contract CurrentStepIndex+RejectedAtStepIndex) + 2 ALTER (WorkflowStep DeptId+PositionLevel) + DROP TABLE x 2 (PEWorkflowStepInnerSteps + WorkflowStepInnerSteps Mig 18+20) + DROP InnerStepId column x 2 (PE+Contract DeptApproval) + DROP filtered indexes x 2 + restore simple unique x 2. PE + Contract Service rewrite TransitionAsync: phase transitions DangSoanThao→ChoDuyet (Drafter trình init idx=0) / ChoDuyet→ChoDuyet (advance idx) / ChoDuyet→DaDuyet/DaPhatHanh (last step done) / ChoDuyet→DangSoanThao (Trả lại save RejectedAtStepIndex) / ChoDuyet→TuChoi (Từ chối khoá vĩnh viễn). Match approver: actor.Dept==step.Dept AND actor.PositionLevel>=step.PositionLevel (OR cùng cấp/dept) OR Approvers.Kind=User match OR Kind=Role match. Admin role bypass policy. Last step done → gen mã HĐ (Contract only). App CQRS WorkflowStepDto + WorkflowStepInput drop InnerStep, add DepartmentId/DepartmentName/PositionLevel (PE + Contract mirror). Tests rewrite: DROP `PeNStageApprovalTests.cs` (6) + `ContractNStageApprovalTests.cs` (6) + `PeTwoStageApprovalTests.cs` (7) — legacy N-stage/2-stage no longer applicable. UPDATE `PeWorkflowAdminTests` signature. **96 → 77 test pass** (-19 legacy). 3-file rule Mig 21 (.cs + Designer + Snapshot) commit đủ. **Chunk B (`88a5be1`)** — FE-Admin Designer rewrite (PeWorkflowsPage + WorkflowsPage): drop InnerStepDto + EditInnerStep types, drop PHASE_OPTIONS auto-assign ChoDuyet=10, StepDto + EditStep + departmentId/positionLevel, copyFromDefinition simplified, Designer step UI rewrite (Tên + Phòng Select + Cấp Select + SLA + Approvers Role/User optional fallback, drop entire InnerSteps sub-section), DefinitionCard view hiển thị badge Phòng emerald + Cấp NV/PP/TP violet, save payload phase=10. types/purchaseEvaluation.ts (fe-admin + fe-user mirror) + ChoDuyet=10 enum + label "Đang duyệt" + color amber. **Chunk C (FE PeWorkflowPanel) SKIP** — existing UI compatible (workflow.nextPhases driven by BE simplified policy), reuse 3-button Trả lại/Từ chối logic Session 14 hoạt động trên ChoDuyet phase tự động. **KHÔNG đụng** Service Notify pattern + Changelog pattern (giữ hành vi Mig 16). Verify: dotnet build pass + Mig 21 LocalDB applied + 77 test pass + npm build × 2 pass. Memory `feedback_drastic_refactor_scope.md` validated: dedicated session approach hoạt động đúng dự đoán. | `dbb0089` (A) · `88a5be1` (B) | | 2026-05-08 | Claude | **🎯 SESSION 16 — DRASTIC REFACTOR flat workflow Phòng × Cấp (Mig 21, 2 commit Chunk A+B)** — Resume từ Session 15 defer plan. User chốt "bỏ phase enum hoàn toàn, dùng ChoDuyet=10 đơn nhất + currentStepIndex tracking". Per memory `feedback_drastic_refactor_scope`: dedicated session với context fresh, scope conservative 2x buffer (~8-10h estimate, actual ~3h). **Chunk A (`dbb0089`)** — Domain enum simplify (DangSoanThao=1, ChoDuyet=10 NEW, DaDuyet=7, TuChoi=99; legacy 2-6 + 98 deprecated giữ cho data cũ). WorkflowStep + DepartmentId Guid? FK Restrict + PositionLevel int? (PE + Contract mirror). PE/Contract entity + CurrentWorkflowStepIndex int? + RejectedAtStepIndex int?. Drop class WorkflowStepInnerStep + nav (PE + Contract). Drop *DepartmentApproval.InnerStepId column. EF Configurations: drop InnerStep config + restore simple unique non-filtered (Mig 19/20 filtered split reverse). DbContext drop DbSet<*WorkflowStepInnerStep> × 2. **Migration 21** `RefactorWorkflowToFlatModel` GỘP: 4 ALTER cols (PE/Contract CurrentStepIndex+RejectedAtStepIndex) + 2 ALTER (WorkflowStep DeptId+PositionLevel) + DROP TABLE x 2 (PEWorkflowStepInnerSteps + WorkflowStepInnerSteps Mig 18+20) + DROP InnerStepId column x 2 (PE+Contract DeptApproval) + DROP filtered indexes x 2 + restore simple unique x 2. PE + Contract Service rewrite TransitionAsync: phase transitions DangSoanThao→ChoDuyet (Drafter trình init idx=0) / ChoDuyet→ChoDuyet (advance idx) / ChoDuyet→DaDuyet/DaPhatHanh (last step done) / ChoDuyet→DangSoanThao (Trả lại save RejectedAtStepIndex) / ChoDuyet→TuChoi (Từ chối khoá vĩnh viễn). Match approver: actor.Dept==step.Dept AND actor.PositionLevel>=step.PositionLevel (OR cùng cấp/dept) OR Approvers.Kind=User match OR Kind=Role match. Admin role bypass policy. Last step done → gen mã HĐ (Contract only). App CQRS WorkflowStepDto + WorkflowStepInput drop InnerStep, add DepartmentId/DepartmentName/PositionLevel (PE + Contract mirror). Tests rewrite: DROP `PeNStageApprovalTests.cs` (6) + `ContractNStageApprovalTests.cs` (6) + `PeTwoStageApprovalTests.cs` (7) — legacy N-stage/2-stage no longer applicable. UPDATE `PeWorkflowAdminTests` signature. **96 → 77 test pass** (-19 legacy). 3-file rule Mig 21 (.cs + Designer + Snapshot) commit đủ. **Chunk B (`88a5be1`)** — FE-Admin Designer rewrite (PeWorkflowsPage + WorkflowsPage): drop InnerStepDto + EditInnerStep types, drop PHASE_OPTIONS auto-assign ChoDuyet=10, StepDto + EditStep + departmentId/positionLevel, copyFromDefinition simplified, Designer step UI rewrite (Tên + Phòng Select + Cấp Select + SLA + Approvers Role/User optional fallback, drop entire InnerSteps sub-section), DefinitionCard view hiển thị badge Phòng emerald + Cấp NV/PP/TP violet, save payload phase=10. types/purchaseEvaluation.ts (fe-admin + fe-user mirror) + ChoDuyet=10 enum + label "Đang duyệt" + color amber. **Chunk C (FE PeWorkflowPanel) SKIP** — existing UI compatible (workflow.nextPhases driven by BE simplified policy), reuse 3-button Trả lại/Từ chối logic Session 14 hoạt động trên ChoDuyet phase tự động. **KHÔNG đụng** Service Notify pattern + Changelog pattern (giữ hành vi Mig 16). Verify: dotnet build pass + Mig 21 LocalDB applied + 77 test pass + npm build × 2 pass. Memory `feedback_drastic_refactor_scope.md` validated: dedicated session approach hoạt động đúng dự đoán. | `dbb0089` (A) · `88a5be1` (B) |
| 2026-05-07 | Claude | **🎯 SESSION 15 — Tooltip diagnose "Lưu & Gửi Duyệt" + Plan drastic refactor flat workflow → DEFER** — User UAT live screenshot phiếu PE Bản nháp + báo "Lưu & Gửi Duyệt" KHÔNG hoạt động + suy đoán "trùng ID với phiếu khác". Chẩn đoán: button silent disabled khi `evaluation.workflow.nextPhases` không có forward phase (chỉ TuChoi/TraLai). FE chưa có visual feedback → user không biết. Improvement (commit `835cc7f`): compute `forwardPhase` once + add `submitDisabledReason` string giải thích reason (canEditPhase=false / readOnly / !forwardPhase với hint admin kiểm tra cấu hình quy trình) + button title attribute show reason hover hoặc forward phase label khi enabled + Dialog confirm show forward phase explicit "Sẽ chuyển sang Chờ Purchasing". Mirror fe-admin + fe-user. Build pass cả 2. **"Trùng ID" KHÔNG phải bug FE** — `PurchaseEvaluationWorkspacePage` URL state đúng (`+ Thêm mới` clear `id`, save set new), mỗi PE row unique GUID + MaPhieu. **Tiếp theo plan drastic refactor**: User chốt "bỏ phase enum hoàn toàn, dùng ChoDuyet=10 đơn nhất + currentStepIndex tracking" + workflow flat list (Phòng × Cấp × Users[]) thay InnerStep model. Surface 6 chunk plan + start Chunk A: edit Domain entities (Phase enum +ChoDuyet=10, WorkflowStep +DeptId/PositionLevel, drop InnerStep class+nav, PE/Contract +CurrentWorkflowStepIndex/RejectedAtStepIndex, *DeptApproval drop InnerStepId) + EF Configurations (drop InnerStep config + nav, restore simple unique non-filtered) + DbContext drop DbSets — 12 files trong working tree. Realize scope realistic ~8-10h (PolicyRegistry rewrite + 2 Service rewrite + App CQRS + 12 tests rewrite + Designer FE + Migration 21 + Docs) vượt session boundary + risk session context deep ~30 commits. **REVERT working tree** về `835cc7f` clean. Add memory `feedback_drastic_refactor_scope` decision rule: drastic refactor cần dedicated session, ước tính conservative (2x buffer), tránh mid-session big refactor. **Stats unchanged**: 96 test pass, 20 mig, 57 bảng. | `835cc7f` | | 2026-05-07 | Claude | **🎯 SESSION 15 — Tooltip diagnose "Lưu & Gửi Duyệt" + Plan drastic refactor flat workflow → DEFER** — User UAT live screenshot phiếu PE Bản nháp + báo "Lưu & Gửi Duyệt" KHÔNG hoạt động + suy đoán "trùng ID với phiếu khác". Chẩn đoán: button silent disabled khi `evaluation.workflow.nextPhases` không có forward phase (chỉ TuChoi/TraLai). FE chưa có visual feedback → user không biết. Improvement (commit `835cc7f`): compute `forwardPhase` once + add `submitDisabledReason` string giải thích reason (canEditPhase=false / readOnly / !forwardPhase với hint admin kiểm tra cấu hình quy trình) + button title attribute show reason hover hoặc forward phase label khi enabled + Dialog confirm show forward phase explicit "Sẽ chuyển sang Chờ Purchasing". Mirror fe-admin + fe-user. Build pass cả 2. **"Trùng ID" KHÔNG phải bug FE** — `PurchaseEvaluationWorkspacePage` URL state đúng (`+ Thêm mới` clear `id`, save set new), mỗi PE row unique GUID + MaPhieu. **Tiếp theo plan drastic refactor**: User chốt "bỏ phase enum hoàn toàn, dùng ChoDuyet=10 đơn nhất + currentStepIndex tracking" + workflow flat list (Phòng × Cấp × Users[]) thay InnerStep model. Surface 6 chunk plan + start Chunk A: edit Domain entities (Phase enum +ChoDuyet=10, WorkflowStep +DeptId/PositionLevel, drop InnerStep class+nav, PE/Contract +CurrentWorkflowStepIndex/RejectedAtStepIndex, *DeptApproval drop InnerStepId) + EF Configurations (drop InnerStep config + nav, restore simple unique non-filtered) + DbContext drop DbSets — 12 files trong working tree. Realize scope realistic ~8-10h (PolicyRegistry rewrite + 2 Service rewrite + App CQRS + 12 tests rewrite + Designer FE + Migration 21 + Docs) vượt session boundary + risk session context deep ~30 commits. **REVERT working tree** về `835cc7f` clean. Add memory `feedback_drastic_refactor_scope` decision rule: drastic refactor cần dedicated session, ước tính conservative (2x buffer), tránh mid-session big refactor. **Stats unchanged**: 96 test pass, 20 mig, 57 bảng. | `835cc7f` |
@ -92,7 +93,7 @@
> **51 row Phase 0-7 (2026-04-21..28) đã archive →** [`changelog/recently-done-archive-2026-04.md`](changelog/recently-done-archive-2026-04.md) > **51 row Phase 0-7 (2026-04-21..28) đã archive →** [`changelog/recently-done-archive-2026-04.md`](changelog/recently-done-archive-2026-04.md)
Session logs: [P0](changelog/sessions/2026-04-21-1045-phase0-scaffold.md) · [P1f](changelog/sessions/2026-04-21-1100-phase1-foundation.md) · [P1.2](changelog/sessions/2026-04-21-1130-phase1-cruds-permission.md) · [P2](changelog/sessions/2026-04-21-1200-phase2-form-engine.md) · [P3](changelog/sessions/2026-04-21-1330-phase3-workflow.md) · [P4](changelog/sessions/2026-04-21-1430-phase4-report.md) · [P5prep](changelog/sessions/2026-04-21-1530-phase5-prep.md) · [Tier 3](changelog/sessions/2026-04-22-0300-tier3-feature-complete.md) · [Skill gov](changelog/sessions/2026-04-23-0900-skill-governance.md) · [Toolkit+4-bảng+Roles VN](changelog/sessions/2026-04-23-1500-toolkit-data-roles.md) · [Roles+Demo+Pending](changelog/sessions/2026-04-23-2200-roles-demo-pending-cleanup.md) · [PE polish iter 2 + rebrand](changelog/sessions/2026-04-24-chot-session-3-pe-polish.md) · [Budget BE + 14 Solutions users](changelog/sessions/2026-04-28-chot-session-4-budget.md) · [**Budget FE + PE/HD-Budget + PE WF Designer + Tests Phase 1-2**](changelog/sessions/2026-04-29-chot-session-5-budget-fe-pe-tests.md) Session logs: [P0](changelog/sessions/2026-04-21-1045-phase0-scaffold.md) · [P1f](changelog/sessions/2026-04-21-1100-phase1-foundation.md) · [P1.2](changelog/sessions/2026-04-21-1130-phase1-cruds-permission.md) · [P2](changelog/sessions/2026-04-21-1200-phase2-form-engine.md) · [P3](changelog/sessions/2026-04-21-1330-phase3-workflow.md) · [P4](changelog/sessions/2026-04-21-1430-phase4-report.md) · [P5prep](changelog/sessions/2026-04-21-1530-phase5-prep.md) · [Tier 3](changelog/sessions/2026-04-22-0300-tier3-feature-complete.md) · [Skill gov](changelog/sessions/2026-04-23-0900-skill-governance.md) · [Toolkit+4-bảng+Roles VN](changelog/sessions/2026-04-23-1500-toolkit-data-roles.md) · [Roles+Demo+Pending](changelog/sessions/2026-04-23-2200-roles-demo-pending-cleanup.md) · [PE polish iter 2 + rebrand](changelog/sessions/2026-04-24-chot-session-3-pe-polish.md) · [Budget BE + 14 Solutions users](changelog/sessions/2026-04-28-chot-session-4-budget.md) · [Budget FE + PE/HD-Budget + PE WF Designer + Tests Phase 1-2](changelog/sessions/2026-04-29-chot-session-5-budget-fe-pe-tests.md) · [Drastic refactor flat workflow Mig 21](changelog/sessions/2026-05-08-0500-drastic-refactor-flat-workflow.md) · [PE V2 schema end-to-end Mig 22-24](changelog/sessions/2026-05-08-1100-pe-workflow-v2-end-to-end.md) · [**S18 PE V2 polish + Clone B + Mig 25 IsUserSelectable**](changelog/sessions/2026-05-08-1945-s18-pe-v2-polish-clone-b.md)
**Docs entry points:** **Docs entry points:**

View File

@ -157,6 +157,38 @@ Session log: `2026-04-28-chot-session-4-budget.md`.
## 📝 Phase 9 — UAT + Ops + carry over (Session 6+ active) ## 📝 Phase 9 — UAT + Ops + carry over (Session 6+ active)
### ✅ Session 18 done (2026-05-08 19:45) — PE V2 polish + Clone B + Mig 25 IsUserSelectable + 4 bug fix UAT (7 commit `aaa1c6c` → `32a8d4d`)
User UAT live tiếp Session 17, chuỗi polish nhỏ + clone V2 cho type B. Áp memory `feedback_uat_skip_verify` (skip dotnet test mỗi chunk, push ngay) + lesson rename/remove → bắt buộc `npm run build`.
- [x] **B1 (`aaa1c6c`) Pe Duyệt filter cứng "Đã gửi duyệt"** — bỏ dropdown trạng thái + filter cứng client-side `getPeDisplayStatus === DaGuiDuyet`. Hint amber "Lọc cố định". Workaround BE /inbox loose UAT trả phiếu Nháp (phân quyền strict V2 pending).
- [x] **B2 (`917446d`) HistoryTab filter Trả lại / Gửi duyệt lại** — FE filter (BE keep audit data đầy đủ): chỉ events Workflow Transition về TraLai (phaseAtChange=98) + từ TraLai (summary "TraLai →") + sửa nội dung khi phaseAtChange=TraLai.
- [x] **B3 (`937eb24`) Clone V2 cho B (DuyetNccPhuongAn)** — Audit reuse trước thay vì duplicate. Schema chung qua ApplicableType discriminator → chỉ 3 file ~60 LOC: MenuKeys.cs +const + All array, DbInitializer.SeedMenusAsync +leaf B (Order=2) + new SeedSampleApprovalWorkflowsV2Async (idempotent skip nếu admin đã tạo workflow B), fe-admin/menuKeys.ts +const. Memory `feedback_audit_reuse_before_clone.md` capture pattern.
- [x] **B4 (`f77ea38`) Fix silent 403 ApprovalWorkflowsV2** — Drafter `nv.test` Workspace dropdown empty silent. Root: class-level `[Authorize(Policy = "Workflows.Read")]` → non-admin 403, TanStack Query catch silent. Fix: class-level `[Authorize]` only, GET cho any authenticated; POST/DELETE giữ `Workflows.Create` admin-only. Gotcha #44.
- [x] **B5 (`a9c0857`) Fix sidebar highlight queryMatches transient keys** — Click row → URL có id transient → exact-set mismatch → menu unhighlight. Fix: `TRANSIENT_QUERY_KEYS = {id, q, editHeader, page, phase, awId}` strip trước compare. Mirror fe-admin + fe-user Layout.tsx.
- [x] **B6 (`2a53107`) Mig 25 + Designer pin toggle + bỏ "(clone)" + Workspace filter** — Migration 25 `AddIsUserSelectableToApprovalWorkflows`: ALTER `IsUserSelectable bit` + Sql backfill `WHERE IsActive=1 SET 1`. Domain +property. DTO +field. CreateAwDefinitionCommand set default true. New SetAwUserSelectableCommand + Handler. API PATCH `/api/approval-workflows-v2/{id}/user-selectable`. DbInitializer SeedSample +`IsUserSelectable=true`. FE Designer +badge "Cho user chọn" + button Ghim/Bỏ ghim + mutation toggleSelectable. Designer name auto-fill bỏ "(clone)" suffix. FE Workspace fetch filter `w.isUserSelectable === true` (cả fe-admin + fe-user).
- [x] **B7 (`32a8d4d`) Cleanup orphan zip**`.claude.zip + docs.zip` lỡ tay vào commit B6 (`git add -A`). Untrack + add `*.zip` rule .gitignore.
**Stats final Session 18:** 25 mig (+1), 58 DB tables (no new — Mig 25 chỉ ALTER cột), ~141 endpoints (+1 PATCH), 33 FE pages, **81 test pass** (no change — feature mới UAT defer test §7), 44 gotcha (+1 #44 silent 403). Memory +1 entry.
**Defer Session 19+:**
- [ ] **Contract V2 wire (Mig 26)** — mirror PE pattern: thêm `Contract.ApprovalWorkflowId` + `CurrentApprovalLevelOrder` + `ContractWorkflowService.ApproveV2Async` + Workspace Select V2 trong ContractCreatePage. Pin V2 mặc định cho ContractType.
- [ ] **Phân quyền strict V2** — hiện loose UAT (mọi authenticated thấy mọi phiếu V2). List = Drafter + approver any-Step + Admin. Cũng giải quyết bug "/inbox loose trả phiếu Nháp" — sau khi BE filter strict, B1 FE filter có thể relax.
- [ ] **Drop legacy V1 (Mig 27 cleanup)** sau khi không còn phiếu pin `WorkflowDefinitionId` (V1): drop `WorkflowDefinitions` + `WorkflowSteps` + `WorkflowStepApprovers` + drop deprecated columns `RejectedAtStepIndex` / `RejectedFromPhase`. Drop `ApproveV1LegacyAsync` branch trong Service.
- [ ] **Test V2 Service wire** (defer khi UAT confirm + có sample data thật) — Domain test ApproveV2Async + match logic + TraLai entry → Cấp 1 reset.
- [ ] **Budget V2 wire** (defer xa hơn — sau Contract V2)
- [ ] **Sample seed B** — sau UAT có thể remove (admin đã tạo workflow thật), hoặc giữ làm fallback. Idempotent skip không clobber.
- [ ] **schema-diagram §17-21 Mig 18-21** vẫn chưa update (defer cron audit 2026-06-01)
- [ ] **Skill `ef-core-migration` frontmatter** "21 migration" stale (thực 25). Defer cron audit 2026-06-01.
- [ ] **Skill `dependency-audit-erp`** "26+/41 bẫy" stale (thực 44). Defer cron audit 2026-06-01.
### ✅ Session 17 done (2026-05-08) — PE Workflow V2 schema + Service wire end-to-end (Mig 22-24, 13 commit `c847dc0` → `de0f38d`) ### ✅ Session 17 done (2026-05-08) — PE Workflow V2 schema + Service wire end-to-end (Mig 22-24, 13 commit `c847dc0` → `de0f38d`)
User chốt sau Session 16 drastic refactor flat (Mig 21) vẫn chưa đúng intent. Yêu cầu schema riêng + Menu mới "Duyệt NCC (Mới)" — Quy trình > Bước (Phòng) > Cấp (NV cụ thể qua ApproverUserId). State machine 5 trạng thái với Trả lại = Phase RIÊNG (Option A user chốt diagram). User chốt sau Session 16 drastic refactor flat (Mig 21) vẫn chưa đúng intent. Yêu cầu schema riêng + Menu mới "Duyệt NCC (Mới)" — Quy trình > Bước (Phòng) > Cấp (NV cụ thể qua ApproverUserId). State machine 5 trạng thái với Trả lại = Phase RIÊNG (Option A user chốt diagram).

View File

@ -0,0 +1,270 @@
# Session 18 — 2026-05-08 (16:56 → 19:45) — PE V2 polish + Clone B + Mig 25 IsUserSelectable + 4 bug fix UAT
**Dev:** Claude
**Duration:** ~2h49m
**Base commit:** `8680f4c` (Session 17 wrap-up)
**Final HEAD:** `32a8d4d`
**Commits:** 7
## Bối cảnh
Tiếp Session 17 (PE V2 schema end-to-end DONE). User UAT live trên prod `eoffice.solutions.com.vn` test phiếu A (DuyetNcc), feedback chuỗi polish nhỏ + clone V2 cho type B (DuyetNccPhuongAn). Áp memory `feedback_uat_skip_verify` (skip dotnet test mỗi chunk, push ngay) + lesson hotfix CI `0ae3fe2` (rename/remove → bắt buộc `npm run build`).
7 batch deliverable theo thứ tự user feedback:
## B1 (`aaa1c6c`) — Pe Duyệt filter cứng "Đã gửi duyệt"
User: "Duyệt bỏ cái trạng thái đi, chỉ load những trạng thái 'Đã gửi duyệt' là đc."
Screenshot UAT cho thấy leaf "Duyệt" hiển thị 3 phiếu: 1 phiếu Nháp + 2 phiếu Đã gửi duyệt. Phiếu Nháp KHÔNG nên hiện ở leaf "Duyệt".
**Root cause:** `/purchase-evaluations/inbox` BE endpoint hiện loose UAT (mọi authenticated user thấy tất cả phiếu, không filter theo actor là approver Cấp hiện tại). Phân quyền strict V2 đã pending Session 19+.
**Fix FE-only (workaround):**
- `PurchaseEvaluationsListPage.tsx` (cả fe-admin + fe-user):
- Khi `pendingMe=true` → ẩn dropdown "Tất cả trạng thái", thay bằng hint amber "Lọc cố định: Đã gửi duyệt (phiếu đang chờ duyệt)"
- Filter cứng client-side: `allRows.filter(p => getPeDisplayStatus(p.phase) === PeDisplayStatus.DaGuiDuyet)` — loại Nháp/Trả lại/Đã duyệt/Từ chối
- Header count dùng `rows.length` khi `pendingMe` (inbox không paged, không phải `list.data?.total`)
- `PurchaseEvaluationsListPage.tsx` cả 2 app: KHÔNG đụng dropdown ở Danh sách (`pendingMe=false`) — vẫn cho user filter mọi trạng thái
Verify: `npm run build` × 2 pass · 0 TS error.
## B2 (`917446d`) — HistoryTab filter Trả lại / Gửi duyệt lại
User: "Lịch sử thay đổi: Chỉ bắt các dòng thay đổi khi trả lại và gửi duyệt lại thôi nhé, không cần bắt trạng thái duyệt và các thay đổi trước khi trả lại."
**Approach:** FE filter (BE giữ audit data đầy đủ — reversible nếu user đổi ý / cần audit trail compliance / legal).
Logic giữ rows match một trong:
- Workflow transition về TraLai: `entityType === 5 (Workflow) && phaseAtChange === 98 (TraLai)`
- Workflow transition từ TraLai: `entityType === 5 && summary?.includes('TraLai →')`
- Mọi thay đổi nội dung (Header/Detail/Supplier/Quote/Attachment) khi `phaseAtChange === 98`
Logic loại:
- Workflow Approve cùng cấp/phase (Cấp 1→2→DaDuyet): `entityType === 5 && phaseAtChange ≠ 98 && summary KHÔNG chứa 'TraLai →'`
- Sửa nội dung khi phase ≠ TraLai (lần soạn thảo gốc, ChoDuyet đầu)
Empty state: "Chưa có lịch sử trả lại / gửi duyệt lại."
Mirror `PeDetailTabs.tsx` HistoryTab fe-admin + fe-user.
## B3 (`937eb24`) — Clone V2 cho B (DuyetNccPhuongAn)
User: "OK đồng ý cứ giữ đúng thiết kế như thế này, hiện tại, tao chốt: Quy trình chọn thầu phụ - NCC → Duyệt NCC → Đã đúng chính xác với yêu cầu của tao. Plan cho tao kế hoạch clone toàn bộ các cập nhật từ lúc đổi lại quy trình → Đến thời điểm hiện tại và clone nó sang: Quy trình chọn thầu phụ - NCC → Duyệt NCC và Giải pháp."
**Audit reuse trước khi clone** — phát hiện 80% đã chung qua `ApplicableType` discriminator:
| Layer | Đã chung cho B? |
|---|---|
| Schema V2 (Mig 22-24) | ✅ `ApplicableType` enum 1=A / 2=B / 3=Contract |
| BE Service `ApproveV2Async` | ✅ không hardcode type |
| App CQRS `GetAwAdminOverview` / `CreateAwDefinition` / Validator | ✅ chung |
| API `/api/approval-workflows-v2?applicableType=N` | ✅ dynamic |
| FE Designer `ApprovalWorkflowsV2Page` | ✅ `TYPE_CODE_TO_INT` cả 3 type |
| FE Layout regex `^AwV2_(.+)$` | ✅ match dynamic typeCode |
| FE App.tsx route `:typeCode` | ✅ dynamic |
| FE Workspace + List + Pending | ✅ chung qua `?type=N` |
| Menu `Pe_DuyetNccPhuongAn_*` (List/Create/Pending) | ✅ đã seed Mig 12 |
Chỉ thiếu (3 file ~60 LOC):
- `Domain/Identity/MenuKeys.cs` +const `ApprovalWorkflowDuyetNccPhuongAnV2 = "AwV2_DuyetNccPhuongAn"` + add vào `All[]`
- `Infrastructure/Persistence/DbInitializer.cs`:
- `SeedMenusAsync` +leaf "Duyệt NCC và Giải pháp (Mới)" (Order=2 cạnh leaf A Order=1) dưới root `ApprovalWorkflowsV2`
- +method `SeedSampleApprovalWorkflowsV2Async` (idempotent — skip nếu admin đã tạo workflow B nào hoặc thiếu test user `nv.test@solutions.com.vn` / Phòng CCM):
- Seed `QT-DN-PA-V2-001 v01` `IsActive=true`
- 1 Bước "Phòng CCM" (DepartmentId resolve từ `Code="CCM"`)
- 1 Cấp 1 NV: test user `nv.test@solutions.com.vn`
- `fe-admin/src/lib/menuKeys.ts` +`AwV2_DuyetNccPhuongAn`
KHÔNG cần migration / Service code mới / Designer page mới. Memory mới `feedback_audit_reuse_before_clone.md` capture pattern.
User feedback: "OK khá tốt, 1 phát chạy luôn :))" → confirm approach worked.
## B4 (`f77ea38`) — Fix silent 403 ApprovalWorkflowsV2Controller
**Triệu chứng:** UAT user `nv.test` (Drafter) login Workspace tạo phiếu B → dropdown "Quy trình duyệt" empty mặc dù Admin Designer cùng URL endpoint thấy v01 sample + v02 admin clone đầy đủ. KHÔNG có toast error / network panel hint.
**Root cause:** `ApprovalWorkflowsV2Controller` class-level `[Authorize(Policy = "Workflows.Read")]` → non-admin role (Drafter chỉ có `PurchaseEvaluations.Read`) bị 403 Forbidden khi GET `/api/approval-workflows-v2`. TanStack Query catch HTTP error trả `data=undefined`, FE component render dropdown empty không có "loading" / "error" state visible → silent fail.
**Fix:**
```csharp
[ApiController]
[Route("api/approval-workflows-v2")]
[Authorize] // ← any authenticated, không hardcode policy
public class ApprovalWorkflowsV2Controller(IMediator mediator) : ControllerBase
{
[HttpGet]
public async Task<...> Overview(...) { ... } // ← inherit class policy = any authenticated
[HttpPost]
[Authorize(Policy = "Workflows.Create")] // ← admin Designer
public async Task<...> Create(...) { ... }
[HttpPatch("{id:guid}/user-selectable")]
[Authorize(Policy = "Workflows.Create")]
public async Task<...> SetUserSelectable(...) { ... } // (added at B6)
[HttpDelete("{id:guid}")]
[Authorize(Policy = "Workflows.Create")]
public async Task<...> Delete(...) { ... }
}
```
Pattern reusable cho Contract V2 Mig 26 sau (cùng controller). Add gotcha #44.
## B5 (`a9c0857`) — Fix sidebar highlight queryMatches transient keys
**Triệu chứng:** Ở leaf "Danh sách" `/purchase-evaluations?type=1`, click chọn 1 phiếu → URL thành `?type=1&id=abc` → leaf bị mất highlight box (gotcha #34 cũ tái phát theo cách khác).
**Root cause:** `queryMatches` exact-set equality:
```ts
if (aKeys.length !== bKeys.length) return false
```
target `{type}` (1 key) vs current `{type, id}` (2 keys) length mismatch → no match.
**Fix:**
```ts
const TRANSIENT_QUERY_KEYS = new Set(['id', 'q', 'editHeader', 'page', 'phase', 'awId'])
function queryMatches(current, target) {
const a = new URLSearchParams(current)
const b = new URLSearchParams(target)
const aKeys = [...a.keys()].filter(k => !TRANSIENT_QUERY_KEYS.has(k)).sort()
const bKeys = [...b.keys()].filter(k => !TRANSIENT_QUERY_KEYS.has(k)).sort()
if (aKeys.length !== bKeys.length) return false
return aKeys.every((k, i) => bKeys[i] === k && a.get(k) === b.get(k))
}
```
Strip transient state keys (selection / search / filter / pagination) → giữ navigation identity (`type`, `pendingMe`, `mode`) check exact-set như cũ.
Edge cases verified:
| URL hiện tại | Target leaf | Match |
|---|---|---|
| `?type=1&id=abc` | Danh sách `?type=1` | ✓ giữ highlight |
| `?type=1&pendingMe=1` | Danh sách `?type=1` | ✗ distinct (không cross-highlight Pending) |
| `?type=1&phase=10` | Danh sách `?type=1` | ✓ giữ highlight (filter dropdown) |
| `?type=1&pendingMe=1&awId=xyz` | Duyệt `?type=1&pendingMe=1` | ✓ giữ highlight |
Mirror fe-admin + fe-user `Layout.tsx`.
## B6 (`2a53107`) — Mig 25 IsUserSelectable + Designer pin toggle + bỏ "(clone)" + Workspace filter
User feedback Admin Designer (screenshot v01 sample + v02 + v03 admin clone): "Bỏ chữ Clone đi nhé, ghi v02, v03... là đủ rồi, khi tao kế thừa tạo quy trình mới. Thêm cho tao nút stick để chọn các quy trình nào mà User đc select bên ngoài khi tạo phiếu."
**Bỏ "(clone)" auto-suffix:**
- `fe-admin/ApprovalWorkflowsV2Page.tsx` Designer line 403: `name = cloneFrom.name` (giữ nguyên), không append " (clone)". Version số (v02, v03, ...) đã đủ phân biệt.
**Pin toggle "Cho user chọn":**
Decision approach: Schema field riêng `IsUserSelectable` thay vì reuse `IsActive`. Lý do:
- `IsActive` semantic = "đang áp dụng (default new version)" — max 1 per ApplicableType (atomic deactivate ở `CreateAwDefinitionCommand`)
- User yêu cầu "chọn các quy trình nào" — implies multiple → cần multi-select field độc lập
- Default behavior không break: backfill `IsUserSelectable=1 WHERE IsActive=1` giữ active workflows visible cho user
**Migration 25** `AddIsUserSelectableToApprovalWorkflows`:
```sql
ALTER TABLE ApprovalWorkflows ADD IsUserSelectable bit NOT NULL DEFAULT 0;
UPDATE ApprovalWorkflows SET IsUserSelectable = 1 WHERE IsActive = 1; -- backfill
```
3-file rule commit đủ (.cs + Designer + Snapshot).
**BE:**
- Domain `ApprovalWorkflow.IsUserSelectable` property — independent với `IsActive`
- DTO `AwDefinitionDto` +field
- `CreateAwDefinitionCommand` Handler set default `IsUserSelectable = true` cho version mới (mirror IsActive default)
- New `SetAwUserSelectableCommand(Guid Id, bool IsUserSelectable)` + Handler — toggle stick/unstick
- API endpoint `PATCH /api/approval-workflows-v2/{id}/user-selectable` (policy `Workflows.Create` admin only)
- `DbInitializer.SeedSampleApprovalWorkflowsV2Async` set `IsUserSelectable = true` cho sample
**FE Designer (`fe-admin/ApprovalWorkflowsV2Page.tsx`):**
- `DefinitionDto` +field `isUserSelectable: boolean`
- `TypePanel` +mutation `toggleSelectable` call PATCH endpoint, invalidate query
- `DefinitionCard` props +`onToggleSelectable: () => void`
- Badge amber "📌 Cho user chọn" (lucide `Pin` icon) cạnh Đang áp dụng/Archived khi `def.isUserSelectable === true`
- Button "📌 Ghim cho user / 🚫 Bỏ ghim" (lucide `Pin` / `PinOff`) trong action group
**FE Workspace (cả fe-admin + fe-user `PeWorkspaceCreateView.tsx`):**
- `approvalWorkflows` useQuery filter `(typeBucket?.history ?? []).filter(w => w.isUserSelectable)` — chỉ workflows admin đã ghim hiển thị dropdown user
## B7 (`32a8d4d`) — Cleanup orphan zip files
`.claude.zip` + `docs.zip` từ Claude harness session start (orphan dump untracked). B6 commit `git add -A` lỡ tay cuốn cả 2 file.
Fix:
- `git rm --cached` 2 file
- `rm -f` xóa local
- Add `*.zip` rule vào `.gitignore`
- Commit cleanup
## E2E verified
-`dotnet build SolutionErp.slnx` 0 error
-`dotnet test SolutionErp.slnx` **81 pass** (58 Domain + 23 Infra) — no change S18 (feature mới UAT defer test §7)
-`dotnet ef migrations add AddIsUserSelectableToApprovalWorkflows` 3-file rule OK
- ✅ Migration 25 apply LocalDB OK qua `DbInitializer.MigrateAsync` startup
-`npm run build` × fe-admin + fe-user pass mỗi commit có FE changes
- ✅ CI deploy commit B6 `2a53107` success (Gitea Actions)
- ✅ User UAT confirm B chạy 1 phát đúng (commit `937eb24`)
- ✅ User UAT confirm Designer toggle pin work (manual test sau commit B6)
## Bug gặp + fix
| Bug | Fix |
|---|---|
| Drafter Workspace dropdown V2 empty silent (Workflows.Read 403) | Class-level `[Authorize]` only, GET cho any authenticated (B4) |
| Sidebar leaf mất highlight khi click row (URL có id transient) | Strip TRANSIENT_QUERY_KEYS trước queryMatches compare (B5) |
| Phiếu Nháp lọt vào leaf "Duyệt" (BE /inbox loose UAT) | FE filter cứng `getPeDisplayStatus === DaGuiDuyet` (B1, workaround) |
| Workspace dropdown hiện cả archived versions (UX không rõ admin pick gì cho user) | Mig 25 `IsUserSelectable` + Designer pin toggle + Workspace filter (B6) |
| Designer auto-fill "(clone)" suffix khi clone version (xấu, version đã đủ phân biệt) | `name = cloneFrom.name` (B6) |
| 2 file `.zip` orphan vào commit B6 (`git add -A`) | `git rm --cached` + `*.zip` .gitignore (B7) |
## Docs updates
- `docs/STATUS.md` — header 24→25 mig + 43→44 gotcha + 1 row Recently Done Session 18 (top)
- `docs/HANDOFF.md` — TL;DR Session 18 mới + cảnh báo Session 19+ (giữ TL;DR S17 nguyên văn theo §6.5 không cắt narrative)
- `CLAUDE.md` (root) — count 24→25 mig + Mig 25 description block
- `docs/database/schema-diagram.md §14` — heading "Migration 22-24" → "22-25" + +cột `IsUserSelectable` + section "IsUserSelectable filter logic" + Pending Session 19+ update Mig 26/27
- `docs/gotchas.md` — +#44 silent 403 from over-restrictive Authorize policy + checklist debug item 21
- `docs/changelog/migration-todos.md` — Phase 9 Session 18 done section đầy đủ + Defer Session 19+ checklist
- `docs/changelog/sessions/2026-05-08-1945-s18-pe-v2-polish-clone-b.md` — file này
KHÔNG đụng (per §6.5 không cố sửa khi không cần):
- `docs/rules.md` — không có rule mới
- `docs/architecture.md` — không có changes structural
- `docs/PROJECT-MAP.md` — không có structural changes
- `docs/workflow-contract.md` — Contract V2 chưa wire (S19+)
- `docs/forms-spec.md` — không liên quan
- `docs/database/database-guide.md` — count migration không hardcode trong header
- Skills (6) — không tạo mới, không refresh; drift ef-core-migration / dependency-audit-erp defer cron audit 2026-06-01
## Memory updates
- ✅ Add `feedback_audit_reuse_before_clone.md` (commit `937eb24` block context, B3 confirm pattern). MEMORY.md index +1 row.
## Handoff to Session 19+
Đọc `docs/HANDOFF.md` "## ⚠️ Điều quan trọng cho Session 19+" cho 6 cảnh báo đầy đủ.
Top priorities:
1. **Contract V2 wire (Mig 26)** — apply audit-reuse pattern, mirror PE
2. **Phân quyền strict V2** — list/inbox/detail filter theo actor role + approver scope
3. **Drop legacy V1 cleanup (Mig 27)** sau khi không còn phiếu pin V1
## Thông số cumulative
| | Trước S18 | Sau S18 | Δ |
|---|---:|---:|---:|
| Migrations | 24 | **25** | +1 |
| DB tables | 58 | 58 | 0 (Mig 25 chỉ ALTER cột) |
| API endpoints | ~140 | **~141** | +1 PATCH user-selectable |
| FE pages | 33 | 33 | 0 (modify existing) |
| Test pass | 81 | 81 | 0 (UAT defer test §7) |
| Gotchas | 43 | **44** | +1 #44 silent 403 |
| Memory entries | 13 | **14** | +1 audit reuse pattern |
| Skills | 6 | 6 | 0 |
| Commits | (after S17) | **+7** | aaa1c6c → 32a8d4d |
| BE LOC | ~16100 | ~16200 | +~100 (Mig 25 + Set command + endpoint + sample seed) |
| FE LOC | ~17500 | ~17600 | +~100 (Designer pin toggle + Workspace filter + filter helpers) |

View File

@ -715,7 +715,7 @@ 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-24, Session 17 — 3 bảng mới + 2 column) ## 14. ApprovalWorkflow V2 schema (Migration 22-25, Session 17-18 — 3 bảng mới + 3 column)
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.
@ -727,6 +727,7 @@ ApprovalWorkflows
├── Id (PK), Code, Version (UNIQUE Code+Version) ├── Id (PK), Code, Version (UNIQUE Code+Version)
├── ApplicableType (1=DuyetNcc, 2=DuyetNccPhuongAn, 3=Contract) ├── ApplicableType (1=DuyetNcc, 2=DuyetNccPhuongAn, 3=Contract)
├── Name, Description, IsActive, ActivatedAt ├── Name, Description, IsActive, ActivatedAt
├── IsUserSelectable (Mig 25, S18) — admin pin/unpin cho user pick lúc create phiếu
└── (audit) CreatedAt, UpdatedAt, CreatedBy, UpdatedBy └── (audit) CreatedAt, UpdatedAt, CreatedBy, UpdatedBy
ApprovalWorkflowSteps (FK Cascade ApprovalWorkflowId, FK Restrict DepartmentId) ApprovalWorkflowSteps (FK Cascade ApprovalWorkflowId, FK Restrict DepartmentId)
@ -761,7 +762,7 @@ Service iterate: `steps[CurrentStepIndex].Levels.Where(l => l.Order == CurrentLe
| Schema | Match logic | Approver type | | Schema | Match logic | Approver type |
|---|---|---| |---|---|---|
| V1 (Mig 21) | `actor.Dept == step.Dept && actor.PositionLevel >= step.PositionLevel` OR Approvers Role/User explicit | Group qua Dept+Level | | V1 (Mig 21) | `actor.Dept == step.Dept && actor.PositionLevel >= step.PositionLevel` OR Approvers Role/User explicit | Group qua Dept+Level |
| V2 (Mig 22-24) | `actor.Id == any level.ApproverUserId where level.Order == currentLevelOrder` | NV cụ thể 1-1 | | V2 (Mig 22-25) | `actor.Id == any level.ApproverUserId where level.Order == currentLevelOrder` | NV cụ thể 1-1 |
### State transitions (V2 + V1 cùng): ### State transitions (V2 + V1 cùng):
@ -792,10 +793,22 @@ else
- Mỗi Cấp có N NV (OR-of-N) - Mỗi Cấp có N NV (OR-of-N)
- Đổi Phòng → clear toàn bộ approvers (NV cũ có thể không thuộc Phòng mới) - Đổi Phòng → clear toàn bộ approvers (NV cũ có thể không thuộc Phòng mới)
### Pending Session 18+: ### IsUserSelectable filter logic (Mig 25, Session 18):
- Contract V2 wire (Mig 25): mirror PE pattern — thêm `Contract.ApprovalWorkflowId` + `CurrentApprovalLevelOrder` `IsUserSelectable` độc lập với `IsActive`:
- Drop legacy V1: sau khi không còn phiếu pin `WorkflowDefinitionId` → drop `WorkflowDefinitions` + `WorkflowSteps` + `WorkflowStepApprovers` + drop deprecated columns `RejectedAtStepIndex` / `RejectedFromPhase` - `IsActive` = "đang áp dụng (default mới)" — max 1 per ApplicableType
- `IsUserSelectable` = "cho user pick lúc create phiếu" — multiple version cùng selectable
Default behavior (sau Mig 25 backfill): active workflows tự động `IsUserSelectable=1` (giữ behavior cũ — Workspace dropdown vẫn hiện active workflow). Archived versions = `0` default, admin có thể toggle qua Designer khi muốn user pick lại version cũ.
`CreateAwDefinitionCommand` set `IsUserSelectable=true` cho version mới (mirror IsActive default). API `PATCH /api/approval-workflows-v2/{id}/user-selectable` — admin toggle stick/unstick (policy `Workflows.Create`).
Workspace `PeWorkspaceCreateView` query `.filter(w => w.isUserSelectable)` — chỉ hiện workflows admin đã ghim.
### Pending Session 19+:
- Contract V2 wire (Mig 26): mirror PE pattern — thêm `Contract.ApprovalWorkflowId` + `CurrentApprovalLevelOrder` + `ContractWorkflowService.ApproveV2Async`
- Drop legacy V1: sau khi không còn phiếu pin `WorkflowDefinitionId` → drop `WorkflowDefinitions` + `WorkflowSteps` + `WorkflowStepApprovers` + drop deprecated columns `RejectedAtStepIndex` / `RejectedFromPhase` (Mig 27 cleanup)
## 15. Liên quan ## 15. Liên quan

View File

@ -609,6 +609,34 @@ session log, etc).
**Reference:** Commit `29eb5d9` add filter, verify ở commit `512880c` **Reference:** Commit `29eb5d9` add filter, verify ở commit `512880c`
(docs-only) → Gitea NO trigger run #113. (docs-only) → Gitea NO trigger run #113.
### 44. Silent 403 từ class-level `[Authorize(Policy = ...)]` quá strict (Session 18)
**Triệu chứng:** UAT 2026-05-08 — Drafter `nv.test` Workspace tạo phiếu B, dropdown "Quy trình duyệt" empty silent. Admin Designer cùng URL endpoint thấy data đầy đủ. Không có toast error / network panel hint.
**Root cause:** `ApprovalWorkflowsV2Controller` class-level `[Authorize(Policy = "Workflows.Read")]` → non-admin role (Drafter chỉ có `PurchaseEvaluations.Read`) bị 403 Forbidden khi GET `/api/approval-workflows-v2`. TanStack Query catch HTTP error trả `data=undefined`, FE component render dropdown empty không có "loading" / "error" state visible.
**Fix (`f77ea38`):** Tách policy theo action — class-level `[Authorize]` only (any authenticated), action-level chỉ POST/DELETE giữ `[Authorize(Policy = "Workflows.Create")]`:
```csharp
[ApiController]
[Route("api/approval-workflows-v2")]
[Authorize] // ← any authenticated, không hardcode policy
public class ApprovalWorkflowsV2Controller(IMediator mediator) : ControllerBase
{
[HttpGet] // ← Drafter pick workflow lúc create — read-only OK
public async Task<...> Overview(...) { ... }
[HttpPost]
[Authorize(Policy = "Workflows.Create")] // ← admin Designer
public async Task<...> Create(...) { ... }
}
```
**Pattern reusable:** Endpoint dùng cho nhiều use case (admin Designer + user list-pick) — split policy per action thay vì class-level uniform. Read-only list workflow KHÔNG nhạy cảm (chỉ là cấu hình quy trình, không expose business data).
**Phòng tránh tương lai:** Khi controller class-level `[Authorize(Policy)]`, audit role nào cần access từng action. Nếu GET cần broader role hơn POST → split policy. Default `[Authorize]` (any authenticated) cho list-pick endpoint.
**FE diagnostic improvement:** TanStack Query error nên hiển thị warning UI (toast hoặc banner) thay vì silent. Hiện tại `useQuery` catch silent → debug khó. Future: wire `onError` handler global show generic error toast.
## Checklist debug bug mới ## Checklist debug bug mới
1. Build pass không? → fail → check using + package version compat 1. Build pass không? → fail → check using + package version compat
@ -631,3 +659,4 @@ session log, etc).
18. Nếu CI fail TCP timeout 21s ở "Set up job" → bypass github.com, manual checkout từ Gitea (#39) 18. Nếu CI fail TCP timeout 21s ở "Set up job" → bypass github.com, manual checkout từ Gitea (#39)
19. Nếu npm install caching fail `tsc not found` → KHÔNG dùng junction Move-Item, thử robocopy/Copy-Item (#40) 19. Nếu npm install caching fail `tsc not found` → KHÔNG dùng junction Move-Item, thử robocopy/Copy-Item (#40)
20. Nếu CI vẫn trigger khi commit MD-only → paths-ignore trong on:push không match patterns đúng (#41) 20. Nếu CI vẫn trigger khi commit MD-only → paths-ignore trong on:push không match patterns đúng (#41)
21. Nếu user phàn nàn "feature work cho admin nhưng user empty/403 silent" → check class-level Authorize policy có over-restrict cho non-admin không, split per action (#44)