Files
solution-erp/docs/HANDOFF.md
pqhuy1987 21ee36390e [CLAUDE] Docs+Skill: chốt Session 16 wrap-up — drastic refactor flat workflow (Chunk D)
Session 16 (2026-05-08) docs/skill/memory wrap-up:

STATUS.md:
- Last updated Session 16 (2 commit Chunk A+B)
- Phase summary count (20→21 mig, 96→77 test, 57→55 bảng)
- Recently Done row Session 16 chi tiết (drastic refactor hoàn tất)
- Phase enum simplified semantic post-Mig 21

HANDOFF.md:
- TL;DR Session 16 prepend với 2 chunk + Chunk C SKIP rationale
- Per-chunk implementation chi tiết (Domain + Mig 21 + Service + Tests
  + FE Designer)
- 8 cảnh báo Session 17+: UAT live test / old data migration / Sample
  seed / Budget N-stage / schema-diagram / skill refresh / tests flat /
  Hard blockers Ops

migration-todos.md: Phase 9 + Session 16 block 2 chunk done + 7 defer task

Session log NEW `2026-05-08-0500-drastic-refactor-flat-workflow.md`:
- Bối cảnh resume từ S15 defer
- Spec implementation (Phase enum, state machine, schema Mig 21)
- Per-chunk Chunk A + Chunk B detail
- Chunk C skip rationale
- Memory `feedback_drastic_refactor_scope` validation:
  scope estimate 30% accurate (3h actual vs 10h conservative)
- Plan organization sau S16

Skill ef-core-migration:
- description + heading: 20→21 migration
- + Mig 21 row "RefactorWorkflowToFlatModel" với detail (4 ALTER + 2
  ALTER + DROP TABLE × 2 + DROP COLUMN × 2 + restore simple unique × 2)
- Total 57→55 bảng (-2 InnerStep tables)
- Tests: 96→77 (drop 19 legacy)

CLAUDE.md root:
- Migration count 20→21
- DB tables 57→55
- Workflow flat description thay N-stage description

docs/rules.md §7: 96→77 test (Mig 21 simplified)

Memory `project_solution_erp.md`:
- Add "Tổng sau session 16" block với drastic refactor details

🎉 Session 16 wrap-up. Cumulative since session start (S15 wrap-up
`38d10b7`): 4 commit (S15 wrap-up was final S15) + 3 commit Session 16
(A `dbb0089` + B `88a5be1` + D current).

Defer Session 17+ priority:
1. UAT live test workflow flat (3 phòng × N cấp realistic)
2. Old PE/HĐ legacy phase data migration
3. Sample data seed (Task 2 carry-over)
4. Hard blockers Ops

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:16:32 +07:00

844 lines
60 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# HANDOFF — Brief 5 phút cho session tiếp theo
**Last updated:** 2026-05-08 (Session 16 — **🎯 DRASTIC REFACTOR DONE: flat workflow Phòng × Cấp + Mig 21 + Service rewrite + FE Designer rewrite. 2 commit Chunk A+B. 96 → 77 test pass (drop 19 legacy). Backward compat: legacy phase 2-9+98 enum giữ cho data cũ.**)
## TL;DR Session 16 (08/05 — Drastic refactor flat workflow EXECUTE)
Resume từ Session 15 defer plan. Per memory `feedback_drastic_refactor_scope.md`: dedicated session, fresh context, conservative buffer.
**Spec:** Workflow flat list (Phòng × Cấp × Approvers). Mỗi step = 1 (Phòng × Cấp). Service iterate steps OrderBy Order, advance pointer. Phase enum simplify ChoDuyet=10. Pin WorkflowDefinitionId.
**2 chunk per-commit (5-6 chunk plan rút gọn vì BE tightly coupled):**
### Chunk A (`dbb0089`) — All BE: Domain + Mig 21 + Service + Tests
**Domain entities:**
- Phase enum (PE + Contract): + ChoDuyet=10 generic intermediate. Legacy 2-6 + 98 deprecated (giữ enum cho data cũ).
- WorkflowStep + DepartmentId Guid? FK Restrict + PositionLevel int?
- PurchaseEvaluation/Contract + CurrentWorkflowStepIndex int? + RejectedAtStepIndex int?
- DROP class WorkflowStepInnerStep + nav (PE + Contract)
- DROP *DepartmentApproval.InnerStepId column
**EF Configurations:**
- DROP InnerStep config (PE + Contract) → table dropped
- WorkflowStep config + DeptId/PositionLevel + FK Restrict
- DepartmentApprovals: restore simple unique non-filtered (Mig 19/20 filtered split reverse)
**ApplicationDbContext:** DROP DbSet<*WorkflowStepInnerStep> × 2
**Migration 21** `RefactorWorkflowToFlatModel` GỘP:
- 4 ALTER (PE/Contract +CurrentStepIndex +RejectedAtStepIndex)
- 2 ALTER (WorkflowStep +DepartmentId +PositionLevel) PE + Contract
- DROP TABLE × 2 (PEWorkflowStepInnerSteps + WorkflowStepInnerSteps Mig 18+20)
- DROP COLUMN × 2 (*DeptApproval.InnerStepId)
- DROP filtered indexes Mig 19/20
- RESTORE simple UNIQUE (TargetId, Phase, Dept, Stage) non-filtered × 2
**Service rewrite (PE + Contract WorkflowService.TransitionAsync):**
- DangSoanThao → ChoDuyet (Drafter trình, init idx=0)
- ChoDuyet → ChoDuyet (advance idx per approve)
- ChoDuyet → DaDuyet/DaPhatHanh (idx ≥ steps.Count → terminal, gen mã HĐ Contract)
- ChoDuyet → DangSoanThao (Trả lại — save RejectedAtStepIndex)
- ChoDuyet → TuChoi (Từ chối — khoá vĩnh viễn)
- Resume Drafter (DangSoanThao + RejectedAtStepIndex≠null) → ChoDuyet jump-back
- Match approver: actor.Dept == step.Dept AND actor.PositionLevel >= step.PositionLevel (OR-of-many cùng cấp/dept) OR Approvers.Kind=User|Role match
- Admin role bypass policy
**App CQRS:** WorkflowStepDto + WorkflowStepInput drop InnerStep, add DepartmentId/DepartmentName/PositionLevel (PE + Contract mirror).
**Tests:**
- DROP `PeNStageApprovalTests.cs` (6) + `ContractNStageApprovalTests.cs` (6) + `PeTwoStageApprovalTests.cs` (7) — legacy
- UPDATE `PeWorkflowAdminTests` signature for new flat input
- **96 → 77 test pass** (-19 legacy)
**3-file rule** Mig 21 commit đủ (.cs + Designer + Snapshot).
### Chunk B (`88a5be1`) — FE Designer + types
**PeWorkflowsPage + WorkflowsPage rewrite (~210 LOC each):**
- Drop InnerStepDto + EditInnerStep types
- Drop PHASE_OPTIONS (auto-assign ChoDuyet=10 behind scenes)
- StepDto + EditStep + departmentId, departmentName, positionLevel
- Designer step UI rewrite: Tên + Phòng Select + Cấp Select + SLA + Approvers (Role/User optional fallback). Drop InnerSteps sub-section.
- DefinitionCard view: badge Phòng emerald + Cấp NV/PP/TP violet
- Save payload: phase=10 (ChoDuyet)
- Hint amber: "User cùng Phòng + Cấp ≥ step → duyệt được (OR-of-many)"
**types/purchaseEvaluation.ts (fe-admin + fe-user mirror):** + ChoDuyet=10 enum + label "Đang duyệt" + color amber. Legacy 2-6 + 98 keep.
**Chunk C (FE PeWorkflowPanel) SKIPPED** — existing UI compatible (workflow.nextPhases BE-driven, 3-button Trả lại/Từ chối Session 14 reuse với target=DangSoanThao/TuChoi pattern).
### Verify
- ✅ dotnet build SolutionErp.slnx 0 error
- ✅ dotnet ef database update Mig 21 LocalDB applied OK
- ✅ dotnet test 77 pass (54 Domain + 23 Infra)
- ✅ npm build fe-admin + fe-user pass
### Cumulative sau Session 16
| | Trước S16 | Sau S16 |
|---|---:|---:|
| BE LOC | ~15800 | ~15500 (-300 service simplified) |
| Migrations | 20 | **21** |
| DB tables | 57 | **55** (-2 InnerStep tables) |
| Tests | 96 | **77** (-19 legacy N-stage/2-stage) |
| FE pages | 32 | 32 (rewrite existing 2 designer) |
## ⚠️ CẢNH BÁO Session 17+
1. **UAT live test** — workflow flat ready. Tạo new workflow definition qua `/system/pe-workflows/:typeCode` với 3 phòng × N cấp setup. Verify Drafter trình → cấp 1 phòng A → cấp 2 phòng A → cấp 1 phòng B → ... → DaDuyet flow.
2. **Old PE/HĐ pinned legacy workflow definitions** (phase=ChoPurchasing/ChoCCM/etc) — service rewrite chỉ handle ChoDuyet=10 + DangSoanThao/DaDuyet. Old data ở phase 2-6 sẽ stuck (admin manual transition required). Recommend: data migration script convert old workflow → new flat model (defer).
3. **Approver explicit (Role/User Approvers list)** — fallback nếu user không match Dept+PositionLevel của step. Cho phép user external (không thuộc dept) duyệt qua Role match (vd Admin) hoặc User explicit.
4. **Bypass cấp dưới cùng dept** — User TP với CanBypassReview=true cùng dept và PositionLevel cao hơn step.PositionLevel → duyệt qua. KHÔNG batch upsert NV+PP rows như Mig 18 N-stage trước (đơn giản hóa: 1 step approve = 1 row).
5. **N-stage tests dropped** — 19 test legacy (Mig 18, 20 N-stage + Mig 16 2-stage). Có thể viết test mới cho flat workflow flow nếu UAT phát sinh bug. Defer.
6. **Sample data N-stage seed** task vẫn pending (Session 14). Block trên DesignTime vs Runtime DB gotcha + DbInitializer seed flow.
7. **Budget N-stage** vẫn defer (cần versioned WF migration trước).
8. **schema-diagram §17-21 update** defer cron audit 2026-06-01.
## TL;DR Session 15 (07/05 — Tooltip diagnose + drastic refactor DEFER)
User UAT live screenshot báo button "Lưu & Gửi Duyệt" KHÔNG hoạt động + suy đoán "trùng ID" giữa các phiếu.
**Diagnose (commit `835cc7f`):**
- Root cause: button silent disabled khi `evaluation.workflow.nextPhases` không có forward phase (chỉ TuChoi/TraLai). Cause khả năng: workflow definition pinned thiếu adjacent step → `policy.NextPhasesFrom(DangSoanThao)` return empty.
- Improvement: tooltip + dialog hiển thị reason rõ ràng:
- `submitDisabledReason` text: "Phiếu đã ở phase X — chỉ Bản nháp/Trả lại mới sửa+gửi" / "Workflow không có phase tiếp theo từ X. Liên hệ admin kiểm tra cấu hình"
- Button title attribute → hover show reason hoặc forward phase label
- Dialog confirm show forward phase explicit ("Sẽ chuyển sang Chờ Purchasing")
- Mirror fe-admin + fe-user. Build pass cả 2. KHÔNG đụng BE — chỉ FE diagnostic UX.
- "Trùng ID" KHÔNG phải bug FE — `PurchaseEvaluationWorkspacePage` URL state đúng, mỗi PE row unique GUID + MaPhieu. Suy đoán user do button silent.
**Plan drastic refactor → DEFER:**
User confirm "bỏ phase enum hoàn toàn, dùng ChoDuyet=10 đơn nhất + currentStepIndex tracking" — refactor workflow từ phase-based + InnerStep nested model sang flat WorkflowStep model (mỗi step = Phòng × Cấp + Approvers users).
Edit working tree 12 files (Domain entities + EF Configurations + DbContext):
- Phase enum +ChoDuyet=10, legacy values 2-6 deprecated
- WorkflowStep +DepartmentId, +PositionLevel
- Drop class WorkflowStepInnerStep + nav (PE + Contract)
- PE/Contract +CurrentWorkflowStepIndex int?, +RejectedAtStepIndex int?
- *DepartmentApproval drop InnerStepId column
- EF Configurations: drop InnerStep config + nav, restore simple unique non-filtered
- DbContext: drop DbSet<WorkflowStepInnerStep> × 2
Reality check scope realistic ~8-10h:
1. Domain + EF + DbContext (~50min) ✓ done in working tree
2. PolicyRegistry rewrite PE+Contract (~45min)
3. App CQRS DTOs rewrite (~45min)
4. Service rewrite PE+Contract (~2-3h)
5. Tests rewrite — drop 12 N-stage tests + update remaining (~1.5h)
6. Migration 21 + LocalDB apply + verify (~30min)
7. FE Designer rewrite (~1.5h)
8. FE PeWorkflowPanel + workflow timeline (~1h)
9. Docs/Skill update (~45min)
Vượt session boundary + risk session deep ~30 commits → **REVERT working tree** về `835cc7f` clean state. Test 96 pass intact.
**Decision memorized:** add memory `feedback_drastic_refactor_scope.md` — drastic refactor cần dedicated session, scope estimation conservative (2x buffer), tránh mid-session big refactor.
## ⚠️ CẢNH BÁO Session 16+
1. **Drastic refactor flat workflow chưa làm — DEFER** với plan chi tiết. Khi resume:
- Plan kỹ 6 chunk per-commit
- Buffer 2x estimate (~16h thực tế)
- Tests rewrite biggest risk
- Hoặc fall back Approach Y (FE Designer flat UI giới hạn 5 phòng) ROI 1-2h nếu user OK trade-off
2. **Task 2 sample data seed N-stage** vẫn pending (block trên DesignTime vs Runtime DB gotcha + DbInitializer seed flow)
3. **schema-diagram §17-19 Mig 18-20** vẫn defer cron audit 2026-06-01
4. **Hard blockers Ops** giữ nguyên 6 task
## TL;DR Session 14 (07/05 — PE 3-button approval workflow)
User chỉ thị thay 2-button approval (Duyệt + Reject mơ hồ) bằng **3 hành động rõ ràng** cho approver:
- **Duyệt** = forward phase tiếp theo (decision=Approve)
- **Trả lại** = về DangSoanThao + Drafter sửa (decision=Reject + target=DangSoanThao). Smart reject pattern Mig 16 + clear N-stage rows + Drafter resume jump-back tới phase đã reject.
- **Từ chối** = phase=TuChoi (decision=Reject + target=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/ChoDuAn/ChoCCM/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:
```
if (decision == Reject) {
if (target != TuChoi) { // Trả lại
RejectedFromPhase = fromPhase
target = DangSoanThao // force
clear N-stage rows tại fromPhase
}
// else target=TuChoi: giữ nguyên, KHÔNG set RejectedFromPhase, KHÔNG clear
}
```
- **FE PeWorkflowPanel (admin + user mirror)**: render 3 button rõ:
- "✓ Duyệt → <label phase>" brand
- "← Trả lại (về Drafter sửa)" red khi `target=DangSoanThao && fromPhase != DangSoanThao`
- "✗ Hủy / Từ chối" red khi `target=TuChoi`
- Decision logic FE: `isReject = target=TuChoi || isSendBack`
- **Dialog confirm**: title rõ theo loại, Cancel case warning đỏ "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 update + add 1**:
- rename `Reject_Sets_RejectedFromPhase_And_Forces_DangSoanThao` → `Reject_To_DangSoanThao_Sets_RejectedFromPhase_TraLai` (change target TuChoi→DangSoanThao)
- NEW `Reject_To_TuChoi_Locks_Permanently_No_RejectedFromPhase` (Phase=TuChoi + RejectedFromPhase null)
- update `NStage_Reject_Clears_InnerStep_Rows_At_Phase` target → DangSoanThao
- **95 → 96 test pass** (+1 Từ chối).
**Task 2 sample seed in-progress (defer Session 15+):**
- Phát hiện gotcha: `DesignTimeDbContextFactory` hardcoded `SolutionErp_Design` connection — `dotnet ef database update` từ session 12-13 thực ra apply lên `_Design` DB, KHÔNG phải `_Dev` runtime DB. Khi user run API → DbInitializer auto-MigrateAsync apply lên `_Dev`.
- Đã apply Mig 9-20 lên `SolutionErp_Dev` qua `dotnet ef database update --connection`.
- Start API để DbInitializer auto-seed 30 demo user. API exit 255 sớm khi log buffer full (~100 lines) — seeding dở dang (chỉ seed Roles, chưa Users).
- Defer Task 2 cho session sau. Cần: user manual `dotnet run` API 1 lần hoàn thành seed, hoặc Claude resume với output redirect to file.
## ⚠️ CẢNH BÁO Session 15+
1. **Phase TraLai = 98 (orphan)** — Domain enum đã có từ S11+++++++ nhưng KHÔNG dùng (user chốt Session 14 không cần phase trung gian, chỉ DangSoanThao + TuChoi). Để giữ enum không phá nhưng ko wire vào policy → orphan. Có thể remove migration sau (không gây hại).
2. **Task 2 sample seed pending**: 2 việc:
- Update `Users.PositionLevel` (1=NV, 2=PP, 3=TP) cho 30 demo user đã seed (mapping email pattern: `tpb./ccm./pro./fin./act./equ./hra.` → 3 (TP), `pp.*` → 2 (PP), `nv./qs./` else → 1 (NV), `bod./pm./admin./huy.duong/chau.le` → null)
- INSERT N-stage WorkflowDefinition v2 cho DuyetNcc với InnerSteps (PRO + CCM × 3 cấp NV/PP/TP)
3. **DesignTime vs Runtime DB distinction**: dev có 2 DB:
- `SolutionErp_Design` — `dotnet ef migrations add/update` (DesignTimeDbContextFactory hardcode)
- `SolutionErp_Dev` — runtime API (appsettings.Development.json)
Khi muốn apply migrations lên Dev: `dotnet ef database update --connection "...SolutionErp_Dev..."` hoặc `dotnet run` API (DbInitializer auto-MigrateAsync).
4. **Budget N-stage defer** — chưa wire vì Budget chưa có versioned WorkflowDefinition. Cần migration `AddBudgetVersionedWorkflow` + `AddBudgetWorkflowInnerSteps`. User quyết riêng (feature mở rộng module lớn).
5. **schema-diagram §17-19 Mig 18-20** chưa update — defer cron audit 2026-06-01.
## TL;DR Session 13 (07/05 — Mirror N-stage workflow sang Contract)
User chỉ thị mirror N-stage từ PE (Mig 18+19) sang Contract. Budget defer (cần migration `AddBudgetVersionedWorkflow` trước — hardcoded `BudgetPolicy.Default` chưa có WorkflowDefinition entity).
**5 chunk per-commit** (Chunk E skipped — WorkflowsController auto-bind record qua [FromBody]):
- **Chunk A (`951ffa3`)** Domain `WorkflowStepInnerStep` + nav WorkflowStep.InnerSteps + ALTER ContractDeptApproval.InnerStepId + EF config + **Migration 20** GỘP 1 (CREATE TABLE + ALTER + DropIndex old + Recreate filtered legacy/N-stage)
- **Chunk B (`04cf2a0`)** Application CQRS DTO/Input/Validator/Handler mirror PE Chunk B (default null backward compat)
- **Chunk C (`e247b67`)** ContractWorkflowService refactor — load InnerSteps eager + reject clear N-stage rows + dept block split hasInnerSteps→N-stage / else→legacy 2-stage
- **Chunk D (`7c0772a`)** ContractNStageApprovalTests 6 test mirror PE + helper SeedWorkflowDefinitionAsync 2 step adjacent + FakeChangelogService + FakeContractCodeGenerator stubs. **89→95 test pass**
- **Chunk E SKIP** — auto-bind no code change
- **Chunk F (current)** FE WorkflowsPage Designer extend InnerSteps sub-section mirror PeWorkflowsPage + Docs
**Verify:** dotnet build pass + dotnet ef database update Mig 20 LocalDB applied + dotnet test 95 pass + npm build fe-admin pass.
**Cumulative sau Session 13:**
| | Trước S13 | Sau S13 |
|---|---:|---:|
| BE LOC | ~15300 | ~15700 (+400) |
| Migrations | 19 | **20** (+Mig 20) |
| DB tables | 56 | **57** (+1 WorkflowStepInnerSteps) |
| DB columns mới | — | +1 (ContractDeptApproval.InnerStepId) |
| API endpoints | ~134 | ~134 (no change — auto-bind) |
| FE pages | 32 | 32 (extend existing WorkflowsPage) |
| Tests | 89 | **95** (+6 Contract N-stage) |
| Commits | (after S12) | **+5** (A→F per-chunk, E skipped) |
## ⚠️ CẢNH BÁO Session 14+
1. **Budget N-stage defer** — Budget chưa có versioned WorkflowDefinition entity (hardcoded `BudgetPolicy.Default`). Để mirror N-stage Budget cần migration `AddBudgetVersionedWorkflow` trước (4 bảng `BudgetWorkflowDefinitions/Steps/StepApprovers/InnerSteps` + `Budget.WorkflowDefinitionId?`). Defer cho user quyết riêng — feature mở rộng module Budget lớn.
2. **PE-only deploy first** — Contract N-stage giờ đã có nhưng PE đã được test trước. UAT cả 2 module song song để verify pattern reusable.
3. **Sample data N-stage** — pending (Task 2). Khi user yêu cầu sẽ seed PositionLevel cho 30 demo user + 1 N-stage workflow definition active cho DuyetNcc + 1 cho ContractType (ví dụ HopDongThauPhu).
4. **TraLai BE wire** — pending (Task 4). Phase TraLai = 98 enum đã có ở Session S11+++++++ FE side, nhưng BE workflow service chưa wire button "Trả lại" cho approver. Khi user yêu cầu sẽ thêm decision `SendBack` (or reuse Reject với target=TraLai) + Service logic + FE button trong Workflow Panel.
5. **schema-diagram §17 Mig 20** chưa update — defer cron audit 2026-06-01.
6. **Skill ef-core-migration Mig 20 row** chưa add — defer cron audit.
7. **Skill contract-workflow N-stage cross-ref** chưa add — defer cron audit.
## TL;DR Session 12 (07/05 — N-stage workflow approval per phase × dept × cấp)
User yêu cầu mở rộng từ 2-stage Mig 16 sang N-stage cấu hình động: 1 phase (WorkflowStep cha) có thể cấu hình chuỗi InnerSteps con theo Department × PositionLevel sequential. Mỗi phòng có NV → PP → TP duyệt thứ tự, có thể bypass cùng dept khi role cao + CanBypassReview.
**6 câu spec defaults chốt:**
- Q1 enum `PositionLevel { NhanVien=1, PhoPhong=2, TruongPhong=3 }` + `User.PositionLevel int?`
- Q2 Sequential pure (Order asc, mỗi inner step = 1 cấp duyệt)
- Q3 TP có CanBypassReview → skip NV+PP cùng dept (audit IsBypassed=true cho cấp dưới)
- Q4 Smart reject về DangSoanThao + RejectedFromPhase + clear N-stage rows tại phase reject
- Q5 PE first (Contract+Budget mirror sau khi PE stable UAT)
- Q6(a) Designer UI 1 sub-section "Cấp duyệt nhỏ trong phòng" drag-list per step
**6 chunk per-commit:**
- **Chunk A (`13ab533`)** Domain + Migration 18 — enum PositionLevel + entity InnerStep + ALTER User.PositionLevel + ALTER PEDeptApproval.InnerStepId + EF config Cascade/Restrict + 3-file rule
- **Chunk B (`0e56bd0`)** Application CQRS DTO — extend PeWorkflowStepDto + InnerStepDto + Validator + Handler atomic batch + UserDto +PositionLevel + SetUserPositionLevelCommand (default null backward compat existing PeWorkflowAdminTests)
- **Chunk C (`0c62e24`)** Service N-stage logic + **Migration 19** filtered unique (drop UNIQUE Mig 16 → recreate `WHERE InnerStepId IS NULL` legacy + new `WHERE InnerStepId IS NOT NULL` N-stage). Refactor TransitionAsync: load InnerSteps eager + reject clear rows + hasInnerSteps→N-stage / else→legacy 2-stage. Match firstPending Order asc, exact match upsert 1 row, bypass batch upsert NV+PP+TP audit IsBypassed
- **Chunk D (`3d76c6b`)** 6 test PE N-stage (FirstInner blocks / 3-level sequential pass / TP bypass skips / wrong dept 403 / reject clears rows / legacy fallback no inner). IdentityFixture extend +positionLevel arg. Helper SeedWorkflowDefinitionAsync 2 step adjacent. **83→89 test pass**
- **Chunk E (`83ffabd`)** API `PATCH /users/{id}/position-level` mirror SetBypassReview
- **Chunk F (current)** FE-Admin types/users.ts +positionLevel + PositionLevel const + Label/Short. PeWorkflowsPage Designer + sub-section InnerSteps drag-list { Phòng × Cấp + required } + button "+ Thêm cấp duyệt" emerald + departmentsList query + payload include. UsersPage column "Cấp" badge + cycle button. KHÔNG đụng fe-user (admin-only). Docs/Skill update.
**Verify:** dotnet build pass + dotnet ef database update Mig 18+19 LocalDB applied + dotnet test 89 pass + npm build fe-admin pass.
**Cumulative sau Session 12:**
| | Trước S12 | Sau S12 |
|---|---:|---:|
| BE LOC | ~14850 | ~15300 (+450 — Domain enum + entity + EF config + Service N-stage + App CQRS) |
| Migrations | 17 | **19** (+Mig 18 + Mig 19) |
| DB tables | 55 | **56** (+1 PEWorkflowStepInnerSteps) |
| DB columns mới | — | +2 (User.PositionLevel + PEDeptApproval.InnerStepId) |
| API endpoints | ~133 | **~134** (+1 PATCH /users/{id}/position-level) |
| FE pages | 32 | 32 (no change — extend existing 2 page) |
| Tests | 83 | **89** (+6 N-stage PE) |
| Commits | (after S11+++) | **+6** (A→F per-chunk) |
## ⚠️ CẢNH BÁO Session 13+
1. **N-stage chỉ áp PE first** — Contract + Budget vẫn 2-stage Mig 16 cũ. Mirror sau khi UAT PE 2-3 tuần ổn (giữ pattern, lặp lại Domain entity + Service logic + Tests).
2. **User.PositionLevel chưa seed cho 30 demo user hiện có** — admin phải set tay qua UsersPage cycle button. Hoặc seed migration sau (mapping Position text → PositionLevel int).
3. **Designer InnerSteps optional** — workflow definition KHÔNG có InnerSteps fallback logic 2-stage Mig 16 cũ. Backward compat 100%. Khi muốn enable N-stage cho 1 phase: clone version + add inner steps + Save.
4. **Bypass kế thừa cùng dept only** — TP với CanBypassReview chỉ skip NV+PP CÙNG dept của TP. KHÔNG cross-dept (NV.A bypass không skip NV.B).
5. **Reset N-stage rows khi reject** — clear chỉ rows tại phase reject. Phase trung gian khác vẫn giữ approvals nếu có. Resume sẽ jump tới RejectedFromPhase + cần re-approve N-stage tại phase đó.
6. **Wrong-level error message** — đã localize "phòng X cấp Y không khớp". UAT user cần feedback nếu confused.
7. **schema-diagram §15 Mig 18 + §16 Mig 19** chưa update — defer cho audit cron 2026-06-01 (small drift, không major).
8. **Skill `ef-core-migration` Mig 18+19 row** chưa add — defer audit 2026-06-01.
## TL;DR Session phase 2 (08/05 — B12-B14 polish iterate sau wrap-up `6e7a6db`)
User UAT live tiếp tục, áp rule strict verify khi rename/remove (lesson hotfix CI 0ae3fe2).
- **B12 (`378c993`)** PE detail polish 5 changes:
- "Lưu" no-close (chỉ toast + invalidate sync, KHÔNG đóng workspace)
- "Xóa phiếu" red bottom button CHỈ Bản nháp (soft-delete IsDeleted=true)
- Bỏ header bar workspace mode "Sửa header"/"Xóa"/"Đóng" (chuyển hết xuống bottom action bar)
- Section 4 column header: `s.supplierName` (master NCC name) thay `displayName`
- Section 3 row chặn xóa NCC khi đã có quotes + tooltip "xóa báo giá trước"
- **B13 (`e320027`)** InfoTab auto re-edit + pencil active visual:
- useEffect watch `[autoEdit, canEdit, ev.id, ...]` → re-trigger edit khi pencil click phiếu khác
- Sync values từ ev mới (tránh stale state)
- Pencil "sáng lên" active state (`bg-brand-100 + ring`) khi `editingRowId === p.id`
- **B14 (`d2306b8`)** QuoteDialog + winner column highlight + loading feedback:
- Bỏ checkbox "Chọn NCC này cho hạng mục" (consolidate winner ở Section 2.a)
- Winner column Section 4 matrix LUÔN xanh emerald (header `` prefix + cells full column)
- QuoteDialog full overlay loading + spinner khi save có delay
- NccSelectorRow inline spinner "Đang chọn NCC + sync cột giá Section 4…"
**Verify:** `npm run build` × 2 app pass · `dotnet test` 83 pass (KHÔNG regression). Push gitea OK.
## ⚠️ CẢNH BÁO Session 12+ (cập nhật)
Bổ sung từ wrap-up trước (xem TL;DR S10-11+++++++ phía dưới):
8. **`isSelected` per-quote BE field** — vẫn còn trong API payload nhưng UI ẩn. Nếu sau này muốn cho user chọn winner per-item (khác với winner per-phiếu) → re-enable checkbox QuoteDialog. Hiện tại chỉ gửi `existing?.isSelected ?? false` để giữ legacy data nếu có.
9. **Section 4 cell winner highlight** dùng `ev.selectedSupplierId === s.supplierId` (per-supplier check). Nếu BE workflow có multi-winner per-item (ít khả năng), cần điều chỉnh logic.
10. **Loading overlay QuoteDialog** chỉ áp QuoteDialog. Các dialog khác (AddSupplierDialog/EditSupplierDialog/DetailDialog) chỉ có button text feedback. Nếu UAT thấy delay → mở rộng pattern overlay sang các dialog khác.
11. **InfoTab re-edit useEffect deps** include 4 ev fields (tenGoiThau/diaDiem/moTa/paymentTerms). Có thể trigger re-set values nếu BE cập nhật ev (vd auto-save từ chỗ khác). Trade-off: chấp nhận để sync state. Nếu UAT thấy "values mất" → debug deps.
## TL;DR Session S10-11+++++++ (07/05 — PE Workspace UX overhaul đầy đủ)
User UAT live mode iterate liên tục. Áp rule [`feedback_uat_skip_verify`](../../../../Users/pqhuy/.claude/projects/D--Dropbox-CONG-VIEC-SOLUTION/memory/feedback_uat_skip_verify.md) skip dotnet test sau mỗi chunk, push ngay. Lesson hotfix CI `0ae3fe2`: rename/remove → BẮT BUỘC `npm run build` trước commit.
**11 batch deliverable** (23 commit total — xem STATUS Recently Done row đầu tiên cho narrative đầy đủ):
- **B1** PE Thao tác 2-panel workspace (S10)
- **B2** Migration 17 manual budget fields PE + HĐ (S11)
- **B3** BudgetFieldRow inline editor Section 2.b
- **B4** InfoTab inline edit Section 1 + PeListPanel pencil hover
- **B5** Workspace "new" sectioned create view 5 sections
- **B6** Danh sách view disable toàn bộ tương tác
- **B7** Workspace "new" lock Loại quy trình + payment preset Select
- **B8** PE display status meta (Bản nháp / Đã gửi duyệt / Trả lại / Đã duyệt / Từ chối)
- **B9** Phase TraLai = 98 + pencil always visible + edit gating
- **B10** Hotfix CI TS errors (forcedPhase rename + unused import)
- **B11** PE detail polish — NCC selector dropdown + Section 3 winner protect + Bottom action bar Lưu/Gửi Duyệt
## Stats sau wrap-up
| | Trước S10 | Sau S11+++++++ |
|---|---:|---:|
| BE LOC | ~14400 | ~14850 (+450 — Mig 17 + Domain TraLai + App CQRS) |
| Migrations | 16 | **17** (+1 Mig 17) |
| DB columns mới | — | +4 (PE+HĐ × Name+Amount manual budget) |
| FE pages | 31 | **32** (+1 Workspace 2-panel) |
| FE components mới | — | +5 (PeListPanel, PeHeaderForm, PeWorkspaceCreateView, BudgetFieldRow, NccSelectorRow inline) |
| PE phase enum | 8 | **9** (+TraLai = 98) |
| PE display status | — | **5** (gom phase chi tiết) |
| Tests | 83 | 83 (no test added — UAT iteration, áp rule §7 test-after defer) |
| Commits S10-S11+++++++ | — | **+23** |
## ⚠️ CẢNH BÁO Session 12+
1. **Workflow transition vào TraLai** — Domain enum đã có (98), FE label/color/badge sẵn. NHƯNG chưa wire button "← Trả lại" trong PeWorkflowPanel approver view + workflow service BE chưa transition logic vào TraLai. Khi UAT cần → thêm: button "Trả lại" trong Chuyển tiếp (hoặc Reject Dialog), workflow rule `Decision=3 (TraLai)` set phase=TraLai + ghi `RejectedFromPhase`. FE đã ready accept hiển thị TraLai phase.
2. **"Đã gửi duyệt" multi-phase filter** — display status "Đã gửi duyệt" gom 5 phase trung gian (ChoPurchasing/ChoDuAn/ChoCCM/ChoCEODuyetPA/ChoCEODuyetNCC). BE chưa hỗ trợ multi-phase param trong list endpoint → FE filter dropdown ẩn option này (chỉ exact-match Bản nháp/Đã duyệt/Từ chối). TODO: BE thêm `?phases=2,3,4,5,6` query param hoặc derive `displayStatus` field.
3. **`feedback_uat_skip_verify` exception** — khi commit có **rename/remove**, BẮT BUỘC `npm run build` 1 lần trước push (lesson hotfix `0ae3fe2`). Chỉ skip verify khi pure ADD (props, JSX, new file).
4. **Workspace + Danh sách + Duyệt** matrix:
- Workspace (Pe_*_Create): 2-panel, Section 5 disabled, edit only Bản nháp+Trả lại, pencil bright/disabled, bottom bar 2 nút Lưu/Gửi Duyệt
- Danh sách (Pe_*_List): 3-panel, **readOnly=true** toàn bộ (no edit/transition)
- Duyệt (Pe_*_Pending): 3-panel, PeDetailTabs readOnly + PeWorkflowPanel có Chuyển tiếp + Section 5 nhập opinion (chỗ này hiện vẫn disabled do code path cũ — verify nếu UAT user cần sign opinions ở đây)
5. **Mig 17 columns nullable** — backward compat OK. Cả 2 (BudgetId + manual fields) cùng null acceptable. Validation BE chưa XOR.
6. **CreateContractFromEvaluation carry-forward** manual fields PE→HĐ — đã wire. Verify UAT.
7. **CI deploy status** — sau hotfix `0ae3fe2` các run từ commit `4c0625c` trở đi sẽ xanh + deploy. User UAT live trên prod ngay sau push.
## TL;DR Session 11++ housekeeping (07/05 — InfoTab inline edit + pencil hover)
User feedback after BudgetFieldRow: muốn thêm nút edit kế bên row trong Panel 1 list, click sáng nội dung Section 1 lên cho sửa header inline.
- ✅ **InfoTab inline edit** — display mode + button "✎ Sửa" góc phải Section 1. Editing mode: card border brand-200 + 4 input (Tên / Dự án locked / Địa điểm / Mô tả / Payment) + Save (PUT /pe/:id) / Hủy.
- ✅ **PeListPanel pencil hover** — icon absolute right-2 top-2 mỗi row, group-hover opacity-100. Click → callback onEditClick(id).
- ✅ **URL flag `?editHeader=1`** — set bởi pencil click → autoEditHeader prop chain xuống PeDetailTabs → InfoTab tự open editing mode mount-time.
- ✅ **fe-admin** Chunk 1 (`5a89dd2`) · **fe-user mirror** Chunk 2 (this).
**Defer (chưa làm):** Refactor workspace "new" mode wrap PeHeaderForm trong sectioned layout giống detail view (5 sections visible). PeHeaderForm hiện tại single-card đủ dùng — chỉ làm thêm khi user feedback rõ.
## TL;DR Session 11+ housekeeping (07/05 — inline budget editor)
User feedback after Session 11: muốn toggle + 2 fields hiển thị trực tiếp trong Section 2 "b. Ngân sách" của PeDetailTabs, không chỉ ở "Sửa header" page. Empty values cứ empty.
- ✅ **BudgetFieldRow component** (~125 LOC) thay FormRow tĩnh cũ ở `b. Ngân sách`. canEdit=`!readOnly && isDraft`. Save dùng existing PUT endpoint (KHÔNG cần BE thay đổi).
- ✅ **fe-admin** Chunk 1 (commit `19712d8`) · **fe-user mirror** Chunk 2 (commit `d5c6f12`).
- ✅ **Verify**: 2 build pass + 0 TS error.
UX:
- **Workspace + Danh sách + isDraft** → editable inline: toggle + Select OR 2 input + nút Lưu khi dirty
- **Duyệt + !isDraft** → display only (link card / manual values / "—" empty)
- **Empty state**: hiển thị "—" thay vì "(chưa link)" verbose (per user)
## TL;DR Session 11 (07/05 — Migration 17 manual budget fields)
**Output session 11** — UX improvement cho user nhập ngân sách không cần Budget entity approved:
- ✅ **Migration 17** `AddManualBudgetFieldsToPeAndContract` — 4 ALTER (PE + HĐ × BudgetManualName nvarchar(200) + BudgetManualAmount decimal(18,2)). Applied LocalDB OK. Mirror logic PE ↔ HĐ (Q3 user chốt).
- ✅ **BE** Domain 2 entity (PE + Contract) +2 property + EF config HasMaxLength/HasPrecision. App CQRS Create/Update commands + DTO + Validator + diff log + carry-forward CreateContractFromEvaluation pe→contract.
- ✅ **FE** Toggle checkbox "Nhập tay (không link)" cạnh Label Ngân sách trong 4 chỗ: PeHeaderForm (workspace + /new page wrap), ContractCreatePage NewForm + EditForm. Khi ON: hide Select, show 2 input field grid 2-col (Tên + Số tiền formatted VND). Khi OFF (default): Select Budget approved cũ. Auto-detect manual mode khi load existing có manual data + !budgetId. Display fallback ở PeDetailTabs Section 1 "b. Ngân sách" + Contract EditForm read-only branch.
- ✅ **5 chunk per-commit** (build + 83 test pass mỗi chunk): C1 Domain+Infra Migration 17 (`ecd5f7e`) · C2 App CQRS (`0f7901c`) · C3 FE-Admin (`bab5031`) · C4 FE-User mirror (`14f8d9d`) · C5 Docs (current).
**Validation Q2 chốt:** cả BudgetId + manual fields có thể cùng null (PE/HĐ chưa có ngân sách gì). KHÔNG XOR enforce — BE prefer link nếu có (Phase=DaDuyet guarantee), manual fallback only.
**KHÔNG đụng:** Budget entity / Phase=DaDuyet validation (giữ invariant "PE/HĐ link Budget approved only"). Manual fields chỉ là note/display, KHÔNG join với Budget.Details cho per-row comparison ở PE matrix Section 4 (cột "So với ngân sách" vẫn require ev.budgetId — không có detail rows để compare).
## ⚠️ CẢNH BÁO session tiếp (Session 12+)
1. **UAT manual budget flow** — toggle ON/OFF + save + reload + verify auto-detect mode. Đặc biệt edit existing PE/HĐ có manual data → toggle phải auto-checked. Edit existing có Budget link → toggle auto-unchecked.
2. **Section 4 PE matrix "So với ngân sách"** vẫn require Budget link — manual amount KHÔNG render comparison column. Document giới hạn này nếu UAT thắc mắc.
3. **`docs/database/schema-diagram.md`** chưa cập nhật count 16→17 migration + 4 column mới. Defer cho audit định kỳ 2026-06-01 (per §6.4 cron) — nhỏ enough không phải selective rewrite.
4. **CreateContractFromEvaluation** đã carry forward manual fields PE→Contract. Verify khi UAT: PE có manual budget → tạo HĐ từ phiếu → HĐ inherit luôn.
5. **No new test added** (rule §7 — feature mới = test-after, soak UAT 2-3 tuần ổn → viết happy path).
6. **Schema fields nullable** — không phá HĐ/PE cũ (legacy null OK). Backward compatible.
## TL;DR Session 10 (07/05 — PE workspace 2-panel)
**Output session 10** — restructure leaf "Thao tác" PE từ page Create header riêng sang workspace 2-panel mirror HĐ Thầu phụ pattern:
- ✅ **Spec chốt 5 câu trước code** — Q1 Panel 2 chỉ data entry (KHÔNG Workflow/Approvals/History); Panel 1 pure picker (no inline edit/delete); Q2 mirror HĐ Thầu phụ (sticky "+ Thêm mới" + new→edit Panel 2); Q3 leaf "Danh sách" + "Duyệt" giữ 3-panel hiện tại; Q4 route mới `/workspace`; Q5 Section 5 Ý kiến 4PB disable trong workspace (nhập khi duyệt).
- ✅ **Chunk 1 fe-admin** (commit `ee0d360`) — 3 file mới: `PeListPanel.tsx` (~180 LOC pure picker reuse-able + sticky "+ Thêm mới"), `PeHeaderForm.tsx` (~210 LOC extract header form từ CreatePage), `PurchaseEvaluationWorkspacePage.tsx` (~120 LOC 2-panel `[320px_1fr]`). 3 file sửa: `PeDetailTabs.tsx` thêm prop `mode?: 'detail' \| 'workspace'` + Section 5 hint amber khi workspace + force `opinionsReadOnly`. `Layout.tsx` resolver `Pe_*_Create` → `/workspace?type=N`. `App.tsx` route mới.
- ✅ **Chunk 2 fe-user mirror** (commit `ecf3c59`) — 6 file y hệt content (rule §3.9 duplicate có chủ đích).
- ✅ **Verify**: 2 build pass + dotnet test 83 vẫn pass mỗi chunk.
**KHÔNG đụng BE / migration / schema / endpoint.** Route `/new` cũ giữ tồn tại cho deep-link "Sửa header" button.
## ⚠️ CẢNH BÁO session tiếp (Session 11+)
1. **UAT live workspace** với anh Kiệt + 2-3 user — feature mới UX khác (page Create cũ thành Dialog implicit qua sticky button + transition new→edit auto).
2. **Section 5 Ý kiến 4PB hint banner amber** ở workspace mode chỉ là text gợi ý — KHÔNG block. User vẫn thấy existing opinions read-only. Nếu UAT thấy confused → có thể đổi thành collapsible section.
3. **Mobile: workspace KHÔNG có fallback** — màn hình `<lg` (< 1024px) Panel 2 ẩn (lg:block). Cần test mobile redirect → `/purchase-evaluations/:id` nếu UAT phát hiện. Hiện chỉ admin desktop dùng workspace.
4. **Pe_*_Pending vẫn /purchase-evaluations?pendingMe=1** — leaf "Duyệt" 3-panel (giữ Panel 3 Workflow + readOnly Section 5 cho phép sign). UAT verify Drafter trình → TPB duyệt thấy Section 5 enable đúng.
5. **PE WorkflowPanel duplicate ở 2 chỗ** — leaf "Danh sách" + "Duyệt" Panel 3 (3-panel). KHÔNG có ở "Thao tác" workspace. Verify route resolver active state highlight đúng (queryMatches helper từ session 3).
6. **Sửa header trong workspace** — vẫn navigate sang `/new?id=` (button "Sửa header" trong PeDetailTabs). Chưa wire inline edit qua Dialog. Nếu UAT yêu cầu → thêm prop `onEditHeader` cho PeDetailTabs trigger Dialog reuse `PeHeaderForm`.
## Housekeeping today (sau Session 9)
- ✅ **Audit định kỳ 2026-05** — combined skill + doc drift (commit `7dc0233`). 5 drift patch (count 77→83 + 52→55 bảng), 1 skill content patch (contract-workflow Phase 9 cross-ref). KHÔNG tạo skill mới. Log `docs/changelog/skill-audit-2026-05.md`.
- ✅ **Optional polish — fe-user Inbox PE section** (commit `332a90f`). useQuery `/purchase-evaluations/inbox` + Panel 1 chia 2 section sticky header (HĐ + PE). PE click → navigate page riêng.
- ✅ **User Manual 7 file rewrite compact** (commit `16c2c9c`). User feedback "ko cần quá đầy đủ chi tiết, cho end-user họ làm". Bỏ field validation table + error troubleshoot table + FAQ chi tiết. Giữ tổng quan ngắn + numbered steps đơn giản. ~86 KB / 7 file (cũ ~123 KB ↓30%). Setup `package.json` + `npm install docx@9.5.0` + `npm run gen:all`. Lần sau sửa: edit `_gen-*.js` → run gen:all.
**Cron audit định kỳ:** Claude SDK CronCreate auto-expire 7 days → KHÔNG fit monthly cadence. User cần setup OS Task Scheduler `.bat` script ping API hoặc manual trigger mỗi đầu tháng. Lần kế: 2026-06-01.
**User Manual style rule (mới chốt session này):** end-user docs BỎ field/error tables, FAQ chi tiết, phím tắt — GIỮ tổng quan ngắn + numbered steps + note/warn/tip critical. Phân biệt với rule §6.5 (KHÔNG cắt narrative) — đây là rule cho audience end-user, không phải agent dev đọc rationale.
## TL;DR Session 9 (04/05 — Chunk E-bis sau Session 8)
**Output session 9** — đóng tất cả Chunk E-bis defer từ session 8:
- ✅ **FE PE WorkflowPanel** — Section "Tiến trình duyệt 2-cấp phòng ban" group by phase × dept, highlight amber chờ TPB confirm, badge fuchsia bypass (cả fe-admin + fe-user).
- ✅ **FE UsersPage UserManager** — Column "Bypass" + button ShieldCheck toggle CanBypassReview, badge fuchsia khi enabled. UserDto thêm field.
- ✅ **HĐ 2-stage logic** — `ContractWorkflowService` thêm UserManager DI + mirror logic từ PE service. `ContractDepartmentApprovalFeatures.cs` List query. Endpoint `GET /contracts/{id}/department-approvals`. FE `WorkflowHistoryPanel` section mới.
- ✅ **Budget 2-stage logic** — `TransitionBudgetCommandHandler` thêm INotificationService + IDateTime DI + 2-stage. `BudgetDepartmentApprovalFeatures.cs` + endpoint + FE `BudgetWorkflowPanel` section.
- ✅ **6 test PE 2-stage** — `IdentityFixture` setup full Identity stack (DbContext SQLite + AddIdentityCore + AddRoles<Role>) reusable. 6 scenario: NV_Review_Blocks / TPB_Confirm_Allows / NV_Bypass / Admin_Skip / Reject_Sets / Resume_Jumps_Back.
- ✅ **Verify**: Build pass + 83 test pass mỗi commit (54 Domain + 29 Infra: 17 codegen + 6 PE WF Application + 6 PE 2-stage).
- ✅ **5 commit pushed** Gitea (E2 → E6).
## ⚠️ CẢNH BÁO session tiếp (Session 10+)
1. **UAT live ngay** với anh Kiệt + 2-3 user — feature 2-stage đầy đủ cả 3 module + UX.
2. **Tests Contract + Budget 2-stage skipped** — logic identical PE (cùng pattern, cùng entity shape). Pattern `PeTwoStageApprovalTests` reusable nếu UAT phát hiện regression riêng.
3. **Bypass toggle audit** — chưa log Changelog khi admin toggle CanBypassReview. Audit qua Identity standard column UpdatedAt only. Có thể cần thêm audit row riêng nếu UAT yêu cầu.
4. **Notify TPB cùng dept** dùng `UserManager.GetRolesAsync` filter `DeptManager` — verify production có user role DeptManager đúng (data already seeded).
5. **fe-user KHÔNG có UsersPage** — admin-only function. Bypass toggle chỉ ở fe-admin.
6. **3 endpoint mới List dept-approvals** PE/HĐ/Budget cùng pattern, reuse policy authz `*Read`.
7. **Cron audit định kỳ 2026-05-01** vẫn EMPTY (`No scheduled jobs`). Có thể recreate khi user yêu cầu.
## TL;DR Session 8 (04/05 — code lớn, 5 commit per-chunk)
## TL;DR Session 8 (04/05 — code lớn, 5 commit per-chunk)
**Output session 8** — đóng bug anh Kiệt + thêm 3 ràng buộc workflow:
- ✅ **Migration 16** `AddTwoStageDeptApprovalAndSmartReject` — 4 ALTER (3 RejectedFromPhase int + Users.CanBypassReview bit) + 3 CREATE TABLE (`Contract/PE/Budget DepartmentApprovals` UNIQUE (TargetId, Phase, Dept, Stage)) + 12 indexes.
- ✅ **Lock edit** 17 handler thêm guard Phase != DangSoanThao (Contract Detail × 15 qua helper, PE Detail × 5 qua helper mới, Budget Detail × 3 inline).
- ✅ **Smart reject + Resume** 3 module — Reject = lưu phase nguồn + force về DangSoanThao. Resume = jump straight tới phase đã reject (skip phase trung gian, bypass policy guard).
- ✅ **PE 2-stage logic** trong `PurchaseEvaluationWorkflowService` — TPB/CanBypass → Confirm; NV → Review only, BLOCK transition cho đến khi TPB confirm.
- ✅ **3 endpoint mới**: `GET /pe/{id}/department-approvals` (List), `PATCH /users/{id}/bypass-review` (toggle), Notify TPB cùng dept khi NV review.
- ✅ **Verify**: Build + 77 test pass mỗi commit. Migration applied LocalDB OK. Schema verified.
- ✅ **6 commit pushed** (2 docs S7 + 5 code S8).
## ⚠️ CẢNH BÁO session tiếp (Session 9+)
1. **Bug fix anh Kiệt** chỉ áp PE workflow. **HĐ + Budget 2-stage scope DEFER** cho khi UAT PE OK.
2. **FE Workflow Panel chưa update** — workflow vẫn block đúng (BE), nhưng UX chưa hiển thị 2-stage progress. User test sẽ thấy phase không đổi mà không hiểu tại sao "stuck". Phải UAT với hint cho user trước khi code FE.
3. **FE UserManager toggle CanBypassReview chưa làm** — tạm thời SET qua HTTP PATCH:
```
PATCH /api/users/{userId}/bypass-review
Authorization: Bearer <admin>
{ "canBypassReview": true }
```
4. **Test thực tế bug fix flow**:
- Login `phuong.nguyen` (NV.PRO, role=Procurement, DeptId=PRO) tạo phiếu PE type A
- Trình DangSoanThao → ChoPurchasing
- phuong.nguyen click Duyệt phase ChoPurchasing → expect: phase KHÔNG đổi, có row Stage=Review
- `tra.bui` (TPB.PRO, role=DeptManager) click Duyệt → expect: phase chuyển ChoCCM
5. **Notify TPB cùng dept** dùng `UserManager.GetRolesAsync` filter `DeptManager`. Best effort, fail OK.
6. **Cron audit định kỳ 2026-05-01** đã quá hạn 3 ngày, vẫn EMPTY runtime. Cần manual trigger.
7. **Smart reject test**: Reject phase ChoCCM → DangSoanThao + RejectedFromPhase=ChoCCM. Drafter sửa Detail + trình lại → jump straight tới ChoCCM (skip ChoPurchasing).
8. **Lock edit test**: HĐ ở Phase=DangGopY → cố sửa Detail → expect 409 Conflict "đã trình duyệt, không thể chỉnh sửa".
## TL;DR Session 6 (30/04 — không code, chỉ docs)
**Output session 6** — pure docs work, không thay đổi code/test:
- ✅ **Compact 3 file core**: STATUS -27%, HANDOFF -32%, migration-todos -35% (-288 dòng tổng). Archive 51 row Recently Done Phase 0-7 → `changelog/recently-done-archive-2026-04.md`.
- ✅ **Refresh 3 skill stale**: `form-engine` (Phase 2 MVP → Tier 3 feature-complete), `permission-matrix` (12 menu → ~60 + inheritance roots), `ef-core-migration` (24 DbSet → 52 bảng).
- ✅ **Rule mới §7 Timing test**: 1 bảng 5-row compact — feature mới = test-after, bug = test-before, critical algorithm = test-before, spec change = update test cũ, skip 5 loại.
- ✅ **Rule mới §6.4 Audit + compact MD định kỳ**: cadence + checklist + anti-pattern. Cross-ref §9.4 skill audit. KHÔNG rewrite toàn bộ.
- ✅ **77 test vẫn pass** (Domain 54 + Infra 23). 0 thay đổi code.
- ✅ **Cron 2026-05-01 fire mai** — combined audit (skill + doc drift) theo §6.4 + §9.4.
**Session 5 (29/04)** đóng gần hết feature gap + bật test gate:
### 🎯 Headline outcomes
- ✅ **Budget feature-complete** — FE 3-panel pages cả 2 app, BE đã sẵn từ session 4
- ✅ **PE/HD ↔ Budget integration** — form select Budget filter Phase=DaDuyet + Project, cột "So với ngân sách" ở PE matrix với delta indicator
- ✅ **PE Detail UI restructure** match form chính thức PHIẾU TRÌNH KÝ (4 section đánh số)
- ✅ **PE Workflow Designer admin UI** `/system/pe-workflows/:typeCode` versioned với clone/edit/+Role/+User
- ✅ **Ý kiến 4 phòng ban** — migration 15 + section sign-off 2x2 grid (Phê duyệt/CCM/MuaHàng/SM-PM)
- ✅ **Tests Phase 1-2** — 71 unit test pass + CI gate fail-fast (Domain policy + Infra code generator)
**Session 4 (28/04)** đã có:
### A. Module Ngân sách (Budget) BE — migration 14, +4 bảng, +11 endpoint
- 4 entity: `Budget` (Header) + `BudgetDetail` (flat row) + `BudgetApproval` (history) + `BudgetChangelog` (audit log).
- Enum `BudgetPhase` 5 state (DangSoanThao→ChoCCM→ChoCEO→DaDuyet + TuChoi).
- `BudgetPolicy.Default` hardcoded simple 3-step (Drafter→CCM→CEO) — chưa versioned (TODO khi user cần admin config UI).
- Mã `NS-YYYYMM-XXXX` Random.Shared (chưa atomic — TODO khi format chính thức).
- Link nullable: `Contract.BudgetId?` + `PE.BudgetId?` đã có FK + index, FE chưa wire form.
- Menu seed `Budgets` root + 3 leaf (Bg_List/Bg_Create/Bg_Pending) order=27 icon Wallet.
- Application: 11 CQRS handler ~340 LOC (Create/UpdateDraft/Transition/List/GetDetail/Delete + Detail CRUD auto-recompute TongNganSach + ListChangelogs).
- Api: `BudgetsController` 11 endpoint REST.
- **FE chưa làm — Priority 0 session 5.**
### B. 14 demo user Solutions thật
- PRO 5 (TPB tra.bui + 4 NV) + CCM 7 (TPB ngocanh.huynh + 6 NV) + ISO 1 (chau.le) + CEO 1 (huy.duong).
- Pwd `User@123456`. Reconcile pattern (gotcha #38 4-field rename).
- Tổng 30 user (16 sample cũ giữ + 14 Solutions thật mới).
**Tổng cumulative:** 52 DB tables, ~128 endpoints, 15 migrations, **41 gotchas (+3 CI fixes)**,
**77 unit test (+6 PE WF Application)**, 11+ commit session 5 push lên Gitea.
### 🆕 CI/CD optimize (29/04 tối)
- ✅ **Manual checkout bypass github.com** — fix #108/#109 transient TCP timeout 21s. Run #110 pass.
- ✅ **Path filter docs-only skip** — `paths-ignore` trong on:push, commit MD-only KHÔNG trigger CI (verify `512880c` → no run #113).
- ⏸️ **npm junction cache** — thử ở #111 fail `tsc not found`, rollback. Cần debug session sau (gotcha #40 doc rồi).
- ✅ **Tests Phase 3 mini (PE Workflow Designer)** — 6 test versioning logic. Total 77.
## ⚠️ CẢNH BÁO session tiếp (Session 7+)
1. **CI test gate active** — code → `dotnet test SolutionErp.slnx` local → commit → push. Test fail = NO deploy. Workflow user §7 rules.md.
2. **Timing test rule live (§7)**: feature mới = test-after, bug = test-before BẮT BUỘC, critical algorithm = test-before merge, spec change = update test cũ + code chung commit.
3. **Doc audit cadence live (§6.4)**: cuối phase compact, đầu tháng cron audit, KHÔNG rewrite toàn bộ. Cron fire **mai (2026-05-01)** — combined skill + doc drift theo checklist.
4. **Login email** `admin@solutionerp.local` / `Admin@123456` — domain default chưa đổi sang `@solutions.com.vn` (chỉ demo user rebrand).
5. **Chưa xóa binding cũ `.huypham.vn`** — vẫn fallback. Sau verify stable → `.\migrate-domains.ps1 -RemoveOld -SkipCert` trên VPS.
6. **win-acme scheduled task "unhealthy"** — fix trước cert expire 2026-06-18.
7. **Export phiếu PDF/Excel PE** — pending vô thời hạn (user nói không quan trọng).
8. **3 feature mới chưa test** (PE Opinion Upsert / Budget validate / Contract BudgetId carry) — đợi UAT phát sinh bug → áp rule §7 (regression test before fix). Hoặc soak 2-3 tuần ổn → viết happy path.
9. **G-084:** VPS shared VietReport — mọi reverse proxy mới phải `127.0.0.1` + bind loopback IPv4 explicit.
## ⭐ Skills (.claude/skills/) — PHẢI dùng khi task khớp
| Domain (3) | Ops (3) |
|---|---|
| `contract-workflow` — state machine + versioned WF | `dependency-audit-erp` — npm/dotnet CVE scan |
| `form-engine` — render docx/xlsx + PDF | `ef-core-migration` — EF migration + 3-file rule |
| `permission-matrix` — role × menu × CRUD | `iis-deploy-runbook` — 3 site IIS + win-acme + runner |
**Audit cron:** `0 9 1 * *` (9:00 AM ngày 1 mỗi tháng). Workflow: `docs/rules.md §9.4`. Lần kế: **2026-05-01**.
**Quy tắc:** KHÔNG bulk-clone repo skill 3rd party. Chỉ skill PROJECT-SPECIFIC. Đầy đủ: `docs/rules.md §9`.
## Ở đâu rồi?
| Phase | Trạng thái |
|---|---|
| 0 Draft | ✅ Done |
| 1 Alpha Core (foundation + đợt 2 CRUD + Permission) | ✅ Done |
| 2 Form Engine MVP + iter 2 (upload UI + .doc auto-convert + PDF export) | ✅ Done |
| 3 Workflow MVP (9 phase + code gen) + iter 2 (SLA job + attachment + notify) | ✅ Done |
| 4 Report MVP (Dashboard + Excel) + user-specific dashboard | ✅ Done |
| 5 Prep + 5.1 Security + Users Mgmt | ✅ Done |
| **5 Deploy prod** (3 domain HTTPS live) | ✅ Done |
| **Tier 3 (Attach + Realtime + Form builder + PDF + Versioned WF + Nested menu + Permission 3-panel)** | ✅ Done |
| **Skill governance** (6 skill project-level + audit cron 1/tháng) | ✅ Done |
| **3-panel List/Inbox/Thao tác + sidebar accordion + UserDashboard** | ✅ Done |
| **4-bảng data model** (Header + 7 Details + Workflow + Changelog audit) | ✅ Done |
| **Mã HĐ gen tại Create + backfill legacy** | ✅ Done |
| **4 master catalogs** (Units/Materials/Services/WorkItems) + datalist autocomplete | ✅ Done |
| **Roles VN labels (ShortName)** + Users dept/position + 13 demo users | ✅ Done |
| **RolesPage CRUD** `/system/roles` + custom role admin | ✅ Done |
| **7 demo HĐ varied phases** + details + comments + approvals workflow | ✅ Done |
| **User-kind approver runtime guard** + Warning 20% SLA | ✅ Done |
| **Edit detail row inline** (7 typed Update commands + EditRowDialog) | ✅ Done |
| **Master expand 15 NCC + 8 Project** + backfill demo HĐ diverse | ✅ Done |
| **Deps audit script** (`scripts/deps-audit.ps1`) | ✅ Done |
| **Module Duyệt NCC (tiền-HĐ) E2E** — 10 bảng + 2 workflow + Kế thừa HĐ | ✅ Done |
| **PE polish iter 2** — flat layout + per-NCC attachments + readOnly Duyệt + email rebrand | ✅ Done |
| **Domain rebrand huypham.vn → solutions.com.vn** — 3 subdomain + cert + CORS + FE bundle | ✅ Done |
| **Module Ngân sách BE + 30 user** — 4 bảng + 11 endpoint + workflow simple | ✅ Done (S4) |
| **Module Ngân sách FE** — 3-panel pages + Detail tabs + Workflow Panel cả 2 app | ✅ Done (S5) |
| **PE/Contract → Budget integration** — form Select + cột "So với ngân sách" PE matrix | ✅ Done (S5) |
| **PE Workflow designer admin UI** `/system/pe-workflows/:typeCode` | ✅ Done (S5) |
| **Ý kiến 4 phòng ban PE** — migration 15 + section sign-off 2x2 grid | ✅ Done (S5) |
| **Tests Phase 1-2 + CI gate** — 71 test (Domain policy + Infra code generator) | ✅ Done (S5) |
| **CI manual checkout bypass github.com + Path filter docs-only skip** | ✅ Done (S5) |
| **Tests Phase 3 mini** — 6 test PE Workflow Designer versioning (total 77) | ✅ Done (S5) |
| **Export phiếu PDF/Excel PE** — `IDocumentConverter` + template | 📝 Pending (không quan trọng) |
| **Tests Phase 3 full** — Application handlers (UpsertOpinion + Budget link validation, cần UserManager setup) | 📝 Pending |
| **Tests Phase 4-5** — API smoke + FE Vitest | 📝 Pending (làm khi cần) |
| **npm cache CI optimize** (debug junction Move-Item issue #40) | 📝 Pending |
| 9+ Post-launch (E-signature, Bravo/SAP, Mobile, AI) | 📝 Future |
## Run nhanh
```powershell
# Terminal 1 — API (auto seed 12 role + 9 dept + 5 supplier + 3 project + 8 template + 7 workflow definition + 28 ContractType menu + 7 workflow menu)
dotnet run --project src\Backend\SolutionErp.Api
# Terminal 2 — Admin FE
cd fe-admin && npm run dev # → http://localhost:8082
# Terminal 3 — User FE
cd fe-user && npm run dev # → http://localhost:8080
```
Login: `admin@solutionerp.local` / `Admin@123456`
## Quick sanity-check
**Admin (:8082):**
- `/dashboard` → "Của tôi" row 4 card + KPI cards + charts
- `/contracts` → list toàn bộ, filter phase/supplier/project
- `/contracts/new?type=5` → tạo HĐ Mua bán, pre-select type từ URL
- `/contracts/{id}` → timeline + action dialog + attachments drag-drop + WorkflowSummaryCard
- `/system/workflows` → 7-card landing (Thầu phụ/Giao khoán/NCC/Dịch vụ/Mua bán/NguyenTacNcc/NguyenTacDv)
- `/system/workflows/MuaBan` → DefinitionCard active + history + "Tạo phiên bản mới" modal với Steps + Approvers (+Role / +User)
- `/system/permissions` → 3-panel layout (Role list | Menu×CRUD matrix | Granted stats)
- `/system/users` → Users CRUD + assign roles
- `/forms` → upload .docx/.xlsx + render dialog Form↔JSON + Tải PDF
**User (:8080):**
- `/inbox?type=5` → HĐ Mua bán chờ role mình
- `/my-contracts?type=2` → HĐ Thầu phụ của tôi
- `/contracts/new?type=3` → tạo HĐ NCC
- Sidebar nested: 📄 Hợp đồng → expand 7 type → expand "HĐ Mua bán" → Danh sách / Thao tác / Duyệt
**Realtime check:**
- Login 2 tab (admin + user) → user tạo comment / transition → admin nhận toast + bell +1
## Cần làm kế tiếp
### 🔥 Priority 0 — Session 6 (Ops + UAT focus)
Đa số feature gap đã đóng. Còn lại chủ yếu Ops/UAT + optional polish.
**A. Hard blockers (chờ user / ops):**
1. **UAT thật 1 tuần với 2-3 user** — Drafter (QS/NV.PB) + CCM + BOD ghi bug/friction
2. **SMTP config** → bật Email outbox (BLOCKED chờ user cấp host/user/pass)
3. **Rotate credentials** — admin + 30 demo + SA + vrapp + JWT secret
4. **Schedule SQL backup daily** — `scripts/backup-sql.ps1` chưa schedule Task Scheduler
**B. PE feature gap còn lại:**
- **Export phiếu PDF/Excel** PE — pending (user nói không quan trọng lắm). Khi cần: tái dùng `IDocumentConverter` + template `PE-TrinhDuyet.docx`.
**C. Optional polish (làm khi UAT phát sinh):**
- Budget MaNganSach atomic sequence — chốt format → migration `AddBudgetCodeSequences` mirror Contract/PE
- 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):**
- Phase 3 — Application handler tests (CQRS + EF InMemory) ~15 test
- Phase 4 — API smoke tests (WebApplicationFactory) ~7 test
- Phase 5 — FE Vitest cho lib utility (queryMatches, fmtMoney) ~10 test
**Đã xong session 5 (check STATUS Recently Done):**
- ~~Module Ngân sách FE — 3-panel pages cả 2 app~~ ✅
- ~~PE/Contract → Budget integration + cột "So với ngân sách"~~ ✅
- ~~PE Detail UI restructure 4 section đánh số~~ ✅
- ~~PE Workflow Designer admin UI~~ ✅
- ~~Ý kiến 4 phòng ban (migration 15)~~ ✅
- ~~Tests Phase 1-2 + CI gate (71 test)~~ ✅
### A. Hard blockers (chờ user / ops)
1. **UAT thật 1 tuần với 2-3 user** — hard requirement từ roadmap. Kiến nghị:
- User A: Drafter (QS/NV.PB) — tạo 3 HĐ mỗi type, đi hết 9 phase
- User B: CCM — duyệt phase 6
- User C: BOD — duyệt phase 7
- Ghi bug / friction / đề xuất → backlog iter 2
2. **SMTP config** để bật Email outbox:
```json
"Email": {
"Host": "smtp.gmail.com",
"Port": 587,
"Username": "...",
"Password": "...",
"From": "noreply@solutionerp.local"
}
```
Khi có → thêm `MailKit`, `IEmailSender`, hook vào `NotificationService.CreateAsync` ngay trước khi enqueue realtime push.
3. **Rotate credentials** — SA SQL password, vrapp password, JWT secret prod, Gitea runner registration token
4. **Schedule SQL backup** — `schtasks /create /tn "SolutionErp Backup" /tr "powershell -File C:\...\scripts\backup-sql.ps1" /sc DAILY /st 03:00`
### A1. Định kỳ — Skill audit
**Cadence:** Mỗi đầu tháng (4 tuần). Lần audit kế tiếp: **2026-05-01**.
Workflow xem `docs/rules.md §9.4`. Tóm tắt:
1. Cross-check 6 skill hiện có với STATUS / gotchas / migration-todos
2. Check repo nguồn 3rd party (alirezarezvani/claude-skills) có gì mới
3. Update / archive / add skill nếu cần
4. Log vào `docs/changelog/skill-audit-2026-05.md`
Trigger: user nói "audit skill" hoặc tự chạy đầu Phase mới.
### B. Polish iterations (optional — khi UAT phát sinh)
- **Roles CRUD** — admin tạo custom role ngoài 12 hardcoded (`Domain.Identity.AppRoles`)
- **User-kind approver runtime** — data model `WorkflowStepApprover.Kind=User` + `AssignmentValue=userId` đã có, chỉ cần:
```csharp
// ContractWorkflowService.TransitionAsync (bổ sung):
var userApprovers = step.Approvers.Where(a => a.Kind == ApproverKind.User)
.Select(a => Guid.Parse(a.AssignmentValue));
if (userApprovers.Any() && !userApprovers.Contains(actorUserId))
throw new ForbiddenException();
```
- **Grant `Workflows.Read` cho non-admin role** trong PermissionsPage → menu Wf_* auto-visible (inheritance đã có)
- **Warning notification 20% SLA** — job emit khi `SlaDeadline - now < sla * 0.2 && !SlaWarningSent`, set flag
- **Reject → DangSoanThao E2E test** với 3 role khác nhau
- **Deps scan CI** — `dotnet list package --vulnerable` + `npm audit --audit-level=high`
### C. Non-goals / parked
- E-signature (VNPT CA / FPT CA) — Phase 6
- Bravo/SAP import NCC — Phase 6
- Mobile app — Phase 6
- AI OCR scan HĐ — Phase 6+
## Lưu ý kỹ thuật quan trọng
**Đọc [`gotchas.md`](gotchas.md) (41 bẫy) trước khi:**
- Thêm package mới → .NET 10 compat (MediatR 14 fail → dùng 12.4.1)
- Debug TS enum error → dùng const-object pattern (`erasableSyntaxOnly`)
- Expression tree lỗi → tách switch ra ngoài LINQ
- Deploy Windows Feature (WebSockets, etc.) → unlock section ở applicationHost (gotcha #25)
- Workflow transition 403 → check `Contract.WorkflowDefinitionId` pin đúng không
- Migration lỗi → 3 file đầy đủ (Designer + Migration + Snapshot)
## Versioned workflow — quick ref
`Contract.WorkflowDefinitionId` pin tại Create. `ContractWorkflowService.LoadAsync` resolve order: pinned def → admin override → hardcoded Registry.For(type). Invariant: HĐ cũ giữ policy cũ qua FK Restrict. Detail: [`workflow-contract.md §7bis`](workflow-contract.md).
## File đang active
Cấu trúc thư mục + file map: [`PROJECT-MAP.md`](PROJECT-MAP.md). Git state: `git log --oneline -10`.
## Credentials + URLs
```
admin@solutionerp.local / Admin@123456 ← Admin (QTV)
Demo users — 30 user (User@123456):
── 16 sample @solutionerp.local (giữ cho test legacy) ──
bod.huynh, bod.le Tổng GĐ + Phó GĐ NĐUQ
pm.nguyen GĐ Dự án FLOCK 01 (PM)
ccm.tran, pro.pham, fin.do TPB CCM/PRO/FIN
act.vu, equ.bui, hra.dang Kế toán trưởng / TPB EQU / TPB HRA
qs.hoang, qs.ngo QS công trường (NV.PB)
nv.cao, nv.dinh, nv.truong NV Cung ứng/Tài chính/CCM (NV.PB)
bod.tran, pm.le Bonus NĐUQ + PM thứ 2
── 14 Solutions thật @solutions.com.vn (session 4) ──
PRO 5: tra.bui (TPB) + phuong.nguyen, binh.lethanh, danh.huynh, dat.tran (NV)
CCM 7: ngocanh.huynh (TPB) + ha.dao, cuong.do, long.le, ha.nguyen,
dung.nguyen, anh.nguyen (NV)
ISO 1: chau.le
CEO 1: huy.duong
⚠ Rotate ALL passwords trước UAT thật
```
- API prod: https://api.solutions.com.vn — `/health/live`, `/health/ready`
- Admin FE prod: https://admin.solutions.com.vn
- User FE prod: https://eoffice.solutions.com.vn
- API dev: http://localhost:5443 — `/swagger` (Dev only)
- Admin FE dev: http://localhost:8082
- User FE dev: http://localhost:8080
- SQL dev: `(localdb)\MSSQLLocalDB` / `SolutionErp_Dev`
- SQL prod: `.\SQLEXPRESS` / `SolutionErp` / `vrapp` (⚠ rotate)
## Đánh giá nhanh
**Tốt:**
- 3 domain HTTPS prod live, CI/CD xanh
- Tier 3 feature-complete: attachment, realtime, form builder (upload + DynamicForm + PDF), versioned workflow (admin-configurable per ContractType, pin per contract), nested menu per type, 3-panel permissions
- Clean-arch 3-project split đúng cho 2 cross-cutting service (realtime + document-converter)
- 26 gotchas tích lũy, 8 session log, 40 docs agent onboard nhanh
- Invariant critical: " giữ quy trình " guaranteed by pinning (reference-based immutability, không snapshot copy)
**Rủi ro còn:**
- UAT thật chưa chạy thể phát hiện edge case missing
- SMTP chưa notification chỉ in-app (toast + bell), không email
- User-kind approver chưa enable guard runtime (designer cho pick, nhưng transition dùng Role fall-back)
- Credentials chưa rotate
- SQL backup chưa schedule Task Scheduler