# STATUS — Snapshot hiện tại > **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 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 (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 - https://api.solutions.com.vn — API (Let's Encrypt, auto-renew via win-acme) - https://admin.solutions.com.vn — Admin FE (HTTP→HTTPS auto-redirect) - https://eoffice.solutions.com.vn — User FE (HTTP→HTTPS auto-redirect) - https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp — Gitea repo + Actions - Default admin: `admin@solutionerp.local` / `Admin@123456` ⚠️ **RE-ROTATE sau login đầu** ## 🔥 In Progress — Session 7+ (Phase 9 active) ### A. Hard blockers (chờ user / ops) - [ ] **UAT thật 1 tuần** với 2-3 user (Drafter / CCM / BOD) — hard requirement Phase 5 roadmap - [ ] **SMTP config** → Email outbox (BLOCKED chờ user cấp host/user/pass) - [ ] **Rotate creds** — admin + 30 demo + SA + vrapp + JWT secret + Gitea runner token - [ ] **Schedule SQL backup daily** — `scripts/backup-sql.ps1` đã sẵn, chưa schedule Task Scheduler - [ ] **Remove binding cũ `.huypham.vn`** sau verify stable: `ssh vietreport-vps ; .\migrate-domains.ps1 -RemoveOld -SkipCert` - [ ] **win-acme scheduled task "unhealthy"** — auto-renew fix trước 2026-06-18 ### B. Carry over feature gap - [ ] **Export phiếu PDF/Excel** PE — tái dùng `IDocumentConverter` + template `PE-TrinhDuyet.docx` (user pending — không quan trọng lắm) ### C. Optional polish (khi UAT phát sinh bug) - [ ] Budget MaNganSach atomic sequence (hiện Random.Shared → migration `AddBudgetCodeSequences`) - [ ] Budget versioned workflow (admin config UI thay hardcoded `BudgetPolicy.Default`) - [ ] Payment terms PE tách field (JSON blob → 6 column riêng) - [ ] Auto-map PE Details → Contract per-type Details khi gen HĐ - [ ] Matrix Quotes bulk paste từ Excel - [ ] fe-user Inbox thêm section "Phiếu Duyệt NCC chờ tôi" ### D. Tests Phase 3-5 (làm khi gặp bug recurring để justify ROI — rule §7) - [ ] **Phase 3 full** — Application handler tests cần UserManager DI helper (PE Opinion Upsert, Budget validate, CreateContractFromEvaluation BudgetId carry) ~15 test - [ ] **Phase 4** — API smoke tests qua WebApplicationFactory ~7 test - [ ] **Phase 5** — FE Vitest cho lib utility (queryMatches, fmtMoney) ~10 test ### E. 2-stage dept approval — Chunk E-bis ✅ DONE (Session 9) - [x] **FE Workflow Panel** PE — section "Tiến trình duyệt 2-cấp phòng ban" group by phase × dept, highlight amber khi chờ TPB confirm - [x] **FE UserManager toggle** `CanBypassReview` — column "Bypass" badge fuchsia + button toggle ShieldCheck (UsersPage) - [x] **HĐ 2-stage** mở rộng — `ContractWorkflowService` thêm UserManager DI + 2-stage logic mirror PE + `ContractDepartmentApprovalFeatures.cs` + endpoint `GET /contracts/{id}/department-approvals` + FE WorkflowHistoryPanel section mới - [x] **Budget 2-stage** mở rộng — `TransitionBudgetCommandHandler` thêm INotificationService + IDateTime + 2-stage logic + `BudgetDepartmentApprovalFeatures.cs` + endpoint + FE BudgetWorkflowPanel section - [x] **Tests 2-stage** (6 test) — `IdentityFixture` setup full Identity stack + 6 test PE workflow service: NV review block / TPB confirm allow / NV bypass / Admin skip / Reject set / Resume jump-back. Pattern reusable. ### F. Audit định kỳ (cron tự fire) - [x] **2026-05-01** (audit 2026-05-04 manual trigger sau trễ 4 ngày) — combined audit log `docs/changelog/skill-audit-2026-05.md`. Cron Claude SDK KHÔNG fit monthly cadence (auto-expire 7d) → user setup OS Task Scheduler nếu cần auto-remind. - [ ] **2026-06-01** — combined audit kế. Trigger thủ công khi đến ngày hoặc user nói "audit MD" / "kiểm tra docs" / "định kỳ kiểm tra". ## ✅ Recently Done (newest on top) | 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 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 14 — PE 3-button workflow Duyệt/Trả lại/Từ chối + Task 2 sample seed in-progress** — User chỉ thị thay 2-button approval bằng 3 hành động rõ ràng cho approver: **Duyệt** (forward), **Trả lại** (về DangSoanThao + Drafter sửa, smart reject Mig 16 + clear N-stage rows + Drafter resume jump-back), **Từ chối** (Phase=TuChoi, phiếu khoá vĩnh viễn 17 handler Mig 16 lock edit, Drafter phải tạo phiếu mới). 1 commit (`0d77698`): Domain `PurchaseEvaluationPolicy.cs` NccOnly + NccWithPlan thêm `(X → TuChoi)` transition cho mọi phase trung gian (ChoPurchasing/ChoCCM/ChoDuAn/ChoCEODuyetPA/ChoCEODuyetNCC) với roles của phase. FromDefinition expand: mỗi step (trừ DangSoanThao) thêm (step.Phase → TuChoi) với roles step. Service `PurchaseEvaluationWorkflowService.TransitionAsync` — Reject branch tách 2 case: target=TuChoi giữ nguyên (KHÔNG override + KHÔNG set RejectedFromPhase + KHÔNG clear N-stage); target khác (DangSoanThao) → smart reject (force DangSoanThao + RejectedFromPhase + clear N-stage). FE PeWorkflowPanel (admin + user mirror): render 3 button rõ ràng "✓ Duyệt → X" brand / "← Trả lại (về Drafter sửa)" red / "✗ Hủy / Từ chối" red. Decision logic: target=TuChoi || isSendBack → Reject (2), else Approve (1). Dialog confirm: title rõ + Cancel case warning red "phiếu sẽ bị khoá hoàn toàn" + SendBack case hint amber "Phiếu về DangSoanThao, Drafter sửa rồi trình lại — workflow tự jump tới phase này". Tests: rename `Reject_Sets_RejectedFromPhase_And_Forces_DangSoanThao` → `Reject_To_DangSoanThao_Sets_RejectedFromPhase_TraLai` (target từ TuChoi → DangSoanThao). NEW `Reject_To_TuChoi_Locks_Permanently_No_RejectedFromPhase`. Update `NStage_Reject_Clears_InnerStep_Rows_At_Phase` target → DangSoanThao. **95 → 96 test pass** (+1 Từ chối). **Task 2 sample seed in-progress**: dotnet ef database update applied Mig 9-20 lên LocalDB SolutionErp_Dev (trước đó chỉ Mig 1-8 vì DesignTimeDbContextFactory hardcoded SolutionErp_Design ≠ runtime SolutionErp_Dev — gotcha tooling distinction). API start để DbInitializer auto-seed 30 demo user nhưng exit 255 sớm khi log buffer full → seed dở dang. Defer Task 2 cho session sau (cần guidance: seed manual SQL hoặc manual API run + sample N-stage workflow def + Update PositionLevel cho 30 users existing). | `0d77698` | | 2026-05-07 | Claude | **🎯 SESSION 13 — Mirror N-stage workflow sang Contract (Mig 20, 5 commit per-chunk + skip Chunk E API)** — User chỉ thị mirror N-stage từ PE sang Contract. Budget defer (cần versioned WF migration trước, hardcoded BudgetPolicy hiện tại chưa có WorkflowDefinition). 5 chunk per-commit (build + ef + test pass mỗi chunk): **Chunk A (`951ffa3`)** Domain entity `WorkflowStepInnerStep` (Domain/Contracts/) + nav WorkflowStep.InnerSteps + ALTER ContractDepartmentApproval.InnerStepId Guid? + EF config FK Cascade Step / Restrict Dept+InnerStep + **Migration 20** `AddContractWorkflowInnerStepsAndAlterDeptApprovalUnique` GỘP 1 (CREATE TABLE WorkflowStepInnerSteps + ALTER InnerStepId + DropIndex old + Recreate filtered legacy `WHERE InnerStepId IS NULL` + new filtered N-stage `WHERE InnerStepId IS NOT NULL` + 3 IX + 3 FK). **Chunk B (`04cf2a0`)** Application CQRS DTO — `WorkflowStepInnerStepDto` + extend `WorkflowStepDto` + GetWorkflowAdminOverview Include InnerSteps + DeptNames map + `CreateWorkflowStepInnerStepInput` + `CreateWorkflowStepInput` extend (default null backward compat) + Validator child rules + Handler atomic batch insert. **Chunk C (`e247b67`)** ContractWorkflowService refactor mirror PE — load definition InnerSteps eager, reject branch clear N-stage rows tại fromPhase, dept approval block split hasInnerSteps→N-stage logic / else→legacy 2-stage. N-stage flow giống PE: yêu cầu actor có DeptId+PositionLevel, match firstPending Order asc + (exact level OR canBypass + level≥), exact upsert 1 row InnerStepId, bypass batch upsert NV+PP+TP cùng dept ≤ actor (audit IsBypassed cho cấp dưới), recheck stillPending → BLOCK + log "duyệt cấp X (còn Y pending)". **Chunk D (`7c0772a`)** Tests 6 N-stage Contract mirror PE pattern + helper SeedWorkflowDefinitionAsync 2 step adjacent (DangGopY + DangDamPhan) + SeedContractAsync với Project + Supplier seed + FakeChangelogService + FakeContractCodeGenerator stubs. Bug fix: legacy fallback test ban đầu fail (Standard policy DangGopY → DangDamPhan chỉ cho [Drafter, DeptManager], không Procurement) → switched phase pair sang DangKiemTraCCM → DangTrinhKy + role CostControl khớp. Total 89 → **95 test pass**. **Chunk E SKIP** — WorkflowsController auto-bind `[FromBody] CreateWorkflowDefinitionCommand` record qua JSON, no code change cần. **Chunk F (current)** FE-Admin types/users.ts đã có PositionLevel const từ Session 12 reuse. WorkflowsPage Designer extend mirror PeWorkflowsPage Chunk F: InnerStepDto + EditInnerStep types + copyFromDefinition include + departmentsList query + sub-section "Cấp duyệt nhỏ trong phòng" drag-list { Phòng × Cấp + required } + button "+ Thêm cấp duyệt" emerald + payload include Order asc. Empty state hint fallback 2-cấp legacy. KHÔNG đụng fe-user (admin-only). Docs/Skill update. **Backward compat 100%**: workflow Contract no InnerSteps → fallback legacy 2-stage Mig 16. Data legacy InnerStepId=null vẫn enforce unique cũ qua filtered index. Defer Budget mirror cho session sau (cần migration `AddBudgetVersionedWorkflow` trước). | `951ffa3` (A) · `04cf2a0` (B) · `e247b67` (C) · `7c0772a` (D) · (current F) | | 2026-05-07 | Claude | **🎯 SESSION 12 — N-stage workflow approval Phòng × PositionLevel cấu hình động (PE-only first, 6 commit per-chunk + Mig 18+19)** — User yêu cầu mở rộng từ 2-stage Mig 16 (NV.Review/TPB.Confirm) sang N-stage cấu hình động: mỗi WorkflowStep cha (= 1 phase) có thể cấu hình chuỗi InnerSteps con theo Department × PositionLevel với Order sequential. Spec defaults chốt 6 câu (PositionLevel int 1=NV/2=PP/3=TP, sequential pure, bypass cùng dept TP skip NV+PP, smart reject reset N-stage rows về DangSoanThao, PE-only first, designer 1 sub-section InnerSteps). 6 chunk per-commit (build + ef + test pass mỗi chunk per `feedback_per_chunk_commit.md`): **Chunk A (`13ab533`)** Domain enum `PositionLevel` (NV/PP/TP) + entity `PurchaseEvaluationWorkflowStepInnerStep` + ALTER User.PositionLevel int? + ALTER PEDeptApprovals.InnerStepId Guid? + EF config + **Migration 18** `AddPeWorkflowInnerStepsAndPositionLevel` (1 CREATE TABLE + 2 ALTER + 3 index + FK Cascade Step / Restrict Dept/InnerStep). 3-file rule. **Chunk B (`0e56bd0`)** Application CQRS DTO — `PeWorkflowStepInnerStepDto` + extend `PeWorkflowStepDto` + `CreatePeWorkflowStepInnerStepInput` (default null backward compat existing PeWorkflowAdminTests) + Validator child rules + Handler atomic batch insert + UserDto +PositionLevel field + `SetUserPositionLevelCommand` mirror SetBypassReview. **Chunk C (`0c62e24`)** Service N-stage logic — **Migration 19** `AlterPeDeptApprovalsUniqueFilteredForInnerSteps` (filtered unique: legacy `WHERE InnerStepId IS NULL` + N-stage `WHERE InnerStepId IS NOT NULL`) cho phép multi-row cùng dept khác inner step. PurchaseEvaluationWorkflowService refactor: load definition InnerSteps eager + reject branch clear N-stage rows + dept block split hasInnerSteps→N-stage logic / else→legacy 2-stage. N-stage flow: yêu cầu actor có DeptId+PositionLevel, match firstPending (Order asc IsRequired) same dept + (exact level OR canBypass + level≥), exact match upsert 1 row InnerStepId, bypass batch upsert NV+PP+TP cùng dept ≤ actor level (audit IsBypassed cho cấp dưới skip), recheck stillPending → BLOCK + log "duyệt cấp X (còn Y pending)" / all done → fall through phase transition. Backward compat: workflow no InnerSteps fallback legacy + InnerStepId=null filter unique cũ vẫn enforce. **Chunk D (`3d76c6b`)** Tests N-stage 6 test mới (NV first blocks / 3-level sequential pass / TP bypass skips / wrong dept throws 403 / reject clears rows / legacy fallback no inner) + IdentityFixture extend `+positionLevel` arg + helper SeedWorkflowDefinitionAsync 2 step adjacent (ChoPurchasing+ChoCCM) cho FromDefinition build transition policy guard pass. Total 83→**89 test pass**. **Chunk E (`83ffabd`)** API `PATCH /users/{id}/position-level` mirror SetBypassReview pattern + body `{positionLevel:int?}` Authorize Users.Update. **Chunk F (current)** FE-Admin types/users.ts +positionLevel field + PositionLevel const + Label/Short maps. PeWorkflowsPage Designer extend: InnerStepDto type + EditInnerStep type + copyFromDefinition include + departmentsList query + sub-section "Cấp duyệt nhỏ trong phòng" per step card với drag-drop list { Phòng × Cấp + required checkbox } + button "+ Thêm cấp duyệt" (xanh emerald) + payload include innerSteps Order asc. UsersPage column "Cấp" badge NV/PP/TP emerald + action button cycle null→1→2→3→null call positionLevelMut PATCH. KHÔNG đụng fe-user (admin-only feature). Docs/Skill update. PE-only first. Backward compat 100%: workflow no InnerSteps + data legacy 2-stage rows không phá. | `13ab533` (A) · `0e56bd0` (B) · `0c62e24` (C) · `3d76c6b` (D) · `83ffabd` (E) · (current F) | | 2026-05-08 00:30 | Claude | **🎯 SESSION PHASE 2 WRAP-UP — B12-B14 PE detail polish iterate (3 commit FE-only sau wrap-up `6e7a6db`)** — User UAT iteration tiếp, áp rule strict verify khi rename/remove (lesson hotfix CI). 3 batch nhỏ: **B12 (`378c993`)** "Lưu" no-close (chỉ toast + invalidate, KHÔNG đóng workspace) + nút "Xóa phiếu" red bottom CHỈ Bản nháp (soft-delete `IsDeleted=true` qua AuditableEntity, không xóa hoàn toàn DB) + bỏ header bar workspace mode "Sửa header"/"Xóa"/"Đóng" (chuyển hết xuống bottom action bar) + Section 4 column header dùng `s.supplierName` thay `displayName` (NCC master) + Section 3 row chặn xóa NCC khi đã có quotes (`hasQuotes` computed) + tooltip "xóa báo giá trước". **B13 (`e320027`)** InfoTab `useEffect` watch `[autoEdit, canEdit, ev.id, ...]` → re-trigger edit mode khi pencil click phiếu khác (fix useState mount-time only) + sync values từ ev mới (tránh stale state) + Pencil "sáng lên" active state khi `editingRowId === p.id` (bg-brand-100 + text-brand-700 + ring-brand-300 + shadow-sm + tooltip cập nhật) + wire `editingRowId={autoEditHeader ? selectedId : null}` từ Workspace → PeListPanel. **B14 (`d2306b8`)** QuoteDialog bỏ checkbox "Chọn NCC này cho hạng mục" (consolidate winner ở Section 2.a NccSelectorRow, isSelected vẫn gửi BE giữ trạng thái cũ) + winner column Section 4 matrix highlight emerald (header `bg-emerald-50` + `✓ ` prefix + cells `bg-emerald-50 font-semibold` cho ENTIRE column, không chỉ cell có quote) + loading overlay full-screen QuoteDialog (`bg-white/70 backdrop-blur-sm` + spinner ring brand-600 + text "Đang lưu báo giá…"/"Đang xóa…") + NccSelectorRow inline spinner "Đang chọn NCC + sync cột giá Section 4…" + disable Hủy/Xóa/Lưu buttons khi `isSaving`. Verify: `npm run build` × 2 app pass mỗi commit · `dotnet test` 83 pass (KHÔNG regression). | `378c993` (B12) · `e320027` (B13) · `d2306b8` (B14) | | 2026-05-07 | Claude | **🎯 SESSION WRAP-UP S10-11+++++++ — PE Workspace UX overhaul đầy đủ (23 commit / ~3500 LOC FE + Mig 17 BE)** — User UAT live mode iterate liên tục, áp rule `feedback_uat_skip_verify` (skip dotnet test sau mỗi chunk, push ngay). 7 batch chính: **B1 (S10) PE Thao tác 2-panel workspace** — leaf `Pe_*_Create` từ page Create header riêng → workspace 2-panel `[320px_1fr]` mirror HĐ Thầu phụ; PeListPanel pure picker + sticky "+ Thêm mới"; PeDetailTabs `mode='workspace'` ẩn Workflow/Approvals/History + Section 5 disabled "nhập khi duyệt" (4 commit). **B2 (S11) Migration 17** `AddManualBudgetFieldsToPeAndContract` — 4 ALTER (PE + HĐ × `BudgetManualName` nvarchar(200) + `BudgetManualAmount` decimal(18,2)) cho fallback "user nhập tay khi không link Budget entity approved". Domain + EF config + App CQRS Create/Update + DTO + Validator + carry-forward `CreateContractFromEvaluation`. FE toggle "Nhập tay" trong PeHeaderForm + ContractCreatePage NewForm/EditForm × 2 app (5 commit). **B3 (S11+) BudgetFieldRow inline editor** — Section 2 "b. Ngân sách" thay FormRow tĩnh → editable component (toggle + Select OR 2 input + Save dirty + Hủy). canEdit cho cả 3 view (Workspace/Danh sách/Duyệt mode), readOnly chỉ display (3 commit). **B4 (S11++) InfoTab inline edit + PeListPanel pencil hover** — Section 1 "✎ Sửa" button flip display↔inputs (Tên/Địa điểm/Mô tả/Payment editable, Dự án locked). PeListPanel thêm pencil icon group-hover absolute right + URL `?editHeader=1` chain → `autoEditHeader` prop trigger mount-time edit (3 commit). **B5 (S11+++) Workspace "new" sectioned create view** — `PeWorkspaceCreateView.tsx` ~230 LOC layout 5 sections giống PeDetailTabs visual. S1 + S2.b editable, S3-5 LockedHint "Lưu phiếu trước". POST trigger create. Replace `PeHeaderForm` trong workspace mode='new' (1 commit). **B6 (S11++++) Danh sách disable toàn bộ interactions** — PurchaseEvaluationsListPage `readOnly={true}` hardcoded cho PeDetailTabs + `readOnly={!pendingMe}` cho PeWorkflowPanel (List view → ẩn Chuyển tiếp + show hint "Vào menu Duyệt"; Pending vẫn approve được) (2 commit). **B7 (S11+++++) Lock Loại quy trình + payment preset** — workspace `` theo URL `?type=N`. `