Commit Graph

127 Commits

Author SHA1 Message Date
48a99e14e7 [CLAUDE] Domain+App+Infra: Plan B G-H1 Mig 34 EmployeeProfile + seed 30 demo
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m38s
Phase 10.1 G-H1 Hồ sơ Nhân sự Foundation — port từ NamGroup CT_NHANSU
(1675 NV, 10 bảng TblNhanVien*) sau anh main chốt S32 4 quyết định
(scope FULL 11 module + DB single schema dbo + reuse Workflow V2 +
chunk per-module Plan riêng). G-H1 CRITICAL FIRST vì depend by 8/11
module Phase 10 sau (Đề xuất/Đơn từ/OT/Đặt xe/Ticket/Dashboard NS/
Chấm công đều cần EmployeeProfile data).

Investigator pre-flight (a103d20) audit NamGroup confirm:
- Main TblNhanVien 105 cols (drop 35 cols duplicate User/UX legacy)
- 5 satellite Phase 10.1 (defer 3 HĐLĐ Plan H2): WorkHistory + Education
  + FamilyRelation + Skill polymorphic Kind + Document
- 6 enum thay catalog FK (Gender/MaritalStatus/EmployeeStatus/...)
- DiaChi dual-write FK + freetext lesson Plan C NamGroup 1675 NV drift

Em main 4 decision chốt:
1. 5 satellite Phase 10.1 (defer 3 HĐLĐ Plan H2)
2. Skill polymorphic Kind enum (gộp 3 NamGroup table)
3. DiaChi 6 FK Province/District/Ward declare nullable + freetext dual-write
   ngày đầu (FK constraint defer G-H2 khi catalog scaffold — Implementer
   smart decision documented EmployeeProfile.cs comment line 14-17)
4. MaNhanVien format NV/{YYYY}/{Seq:D4} atomic Serializable reset/year

Implementer Case 2 (a8f4567) Pattern 12-bis cross-module mirror PE → Hrm
cookie-cutter scaffold 17 file mới + 4 modified + 3-file mig rule:

Domain (8 file SolutionErp.Domain.Hrm):
- EmployeeProfile.cs (main ~70 cols inherit AuditableEntity, 1-1 UNIQUE User)
- EmployeeWorkHistory.cs + EmployeeEducation.cs + EmployeeFamilyRelation.cs
- EmployeeSkill.cs (polymorphic Kind=Computer/Language/Other)
- EmployeeDocument.cs (IdCard/Passport/Degree/Certificate/LaborContract/Other)
- EmployeeCodeSequence.cs (PK string Prefix, NOT BaseEntity Id Guid)
- Enums.cs (10 enum gọn 1 file)

Application (1 file):
- IEmployeeCodeGenerator.cs interface (mirror IContractCodeGenerator)

Infrastructure (8 file):
- EmployeeCodeGenerator.cs impl IsolationLevel.Serializable transaction
- 7 EF Configuration file (HasIndex UNIQUE UserId/EmployeeCode/Phone +
  HasMaxLength + HasColumnType decimal(18,2) + FK Cascade satellite)
- DependencyInjection.cs (M): register IEmployeeCodeGenerator → impl

Persistence (3 file modified + 2 new mig + 1 snapshot):
- IApplicationDbContext.cs (M): +7 DbSet<EmployeeProfile/...>
- ApplicationDbContext.cs (M): +7 DbSet impl
- ApplicationDbContextModelSnapshot.cs (M): EF auto-update
- 20260526110207_AddEmployeeProfiles.cs (NEW, EF auto-gen)
- 20260526110207_AddEmployeeProfiles.Designer.cs (NEW, EF auto-gen)

DbInitializer.cs (M, em main solo Task 3b ~90 LOC):
- using SolutionErp.Domain.Hrm import added
- SeedDemoEmployeeProfilesAsync method appended (end of class)
- Register call after SeedDemoUsersAsync line 88 (depend user exist first)
- NOT gated DemoSeed:Disabled flag (infrastructure data per gotcha #51 lesson)
- 30 demo profile mirror 30 user @solutions.com.vn + sequential code
  NV/{YYYY}/0001..0030 + placeholder masked CMND/BHXH/Bank (bro UAT update
  qua FE Page Task 5) + EmployeeCodeSequence row LastSeq=30 → production
  gen tiếp 0031+

Verify:
- dotnet build: 0 err 2 unrelated warn DocxRenderer (2.49s + 7.86s rebuild)
- dotnet ef database update _Dev: Mig 34 applied (top of __EFMigrationsHistory)
- dotnet test: **120/120 PASS** baseline preserved (no test add Phase 10
  test-after per §7 UAT mode — test bundle defer Task 4+5+6 done)

Stats target Phase 10 end: 33→42 mig (+9 Mig 34-42), 60→85 tables (+25),
~148→250 endpoint (+100), 38→60 FE pages (+22). Current after this commit:
33→34 mig + 60→66 tables + endpoint/FE unchanged (G-H1 Task 4+5 next).

Pattern reusable cross-project:
- Pattern 12-bis cross-module entity cookie-cutter mirror reinforced 3×
  (S29 Plan B Contract V2 + S33 Plan B G-H1 EmployeeProfile)
- Infrastructure seed OUT of DemoSeed gate (gotcha #51 lesson, mirror Mig 32
  SeedSampleContractWorkflowV2)
- DiaChi dual-write FK + freetext từ ngày đầu (NamGroup 1675 NV drift lesson)

Pending Plan B G-H1 Phase 2 (chờ anh main signal kick off):
- Task 4 — Implementer BE CQRS handler + 6 endpoint controller
- Task 5 — Implementer FE 2 app EmployeesPage 3-panel + 6 section tabs
- Task 6 — Em main Permission menu Hrm_HoSo* seed
- Task 7 — Reviewer pre-commit + CICD post-deploy verify

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:18:57 +07:00
38f1c4d2d9 [CLAUDE] Infra: Plan B Hotfix CICD — SeedSampleContractWorkflowV2 OUT of DemoSeed gate
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m32s
CICD Monitor post-deploy verify (Run #231 SUCCESS) caught CRITICAL:
SeedSampleContractWorkflowV2Async nested inside `if (!demoSeedDisabled)`
branch → prod has DemoSeed:Disabled=true (Plan T S23 t10) → seed SKIP →
QT-HD-V2-001 KHÔNG tồn tại prod → Drafter Workspace dropdown V2 EMPTY
→ V2 contract path BLOCKED end-to-end UAT.

Fix: PROMOTE SeedSampleContractWorkflowV2 ra ngoài DemoSeed gate. Lý do
architectural:
- Sample workflow là INFRASTRUCTURE config (như Roles + Departments + Catalogs)
- KHÔNG phải demo data wipeable
- Production cần để Drafter create V2 contract
- Admin có thể edit/delete/disable qua Designer sau seed (idempotent skip)

Pattern lesson reusable cross-project: phân biệt INFRASTRUCTURE seed (always
run) vs DEMO seed (gated by flag). Bảng phân loại:
- INFRASTRUCTURE always: Roles, Departments, Catalogs, MenuTree, AdminPerms, Templates
- DEMO gated: DemoUsers (30 sample, Plan T disabled prod), DemoContracts, DemoPE, SampleWorkflows
- INFRASTRUCTURE NEW post-S29: SampleContractWorkflowV2 (cần cho V2 path work)

Verify:
- dotnet build PASS 0 err
- Mig 32 + Mig 33 prod đã apply (Run #231 success)
- Sample seed sẽ chạy on next IIS recycle post-push
- Idempotent: skip nếu QT-HD-V2-001 already exists (rare race admin tự seed Designer trước)

Post-deploy expect:
- ApprovalWorkflows table +1 row Code=QT-HD-V2-001 ApplicableType=3 IsActive=1 IsUserSelectable=1
- Drafter login fe-user → /contracts/new → Workspace dropdown "Quy trình duyệt V2" có 1 option

CICD Monitor ROI: caught BEFORE bro UAT 401/empty dropdown experience. Smart
Friend guard pattern proven 4× cumulative S22 #44 + S25 #48 + S29 Plan B
Reviewer ApplicableType + S29 Plan B CICD DemoSeed gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:11:05 +07:00
3e92584238 [CLAUDE] App: Plan B Hotfix Reviewer — CreateContractCommand validate ApplicableType=Contract
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m30s
Reviewer pre-push verify (agentId ace4799) catch MAJOR security gap:
CreateContractCommand thiếu validation guard rằng ApprovalWorkflowId pin
phải có ApplicableType=Contract(3). Attacker forge POST body với V2 PE
workflow ID (ApplicableType=1/2 DuyetNcc) → contract pin sai workflow type
→ Service ApproveV2Async sẽ run pattern PE workflow trên Contract entity
→ behavior nondeterministic + audit log nhầm.

Fix: Mirror PE pattern PurchaseEvaluationFeatures.cs:62-77.

Validation block thêm vào CreateContractCommandHandler.Handle sau activeWfId
query:
1. Load aw entity by Id (throw NotFound nếu invalid Guid)
2. Verify aw.ApplicableType == Contract(3) (throw Conflict nếu mismatch)

Defense-in-depth: FE Workspace dropdown (Chunk D 62b50d1) đã filter
ApplicableType=3 client-side; BE guard chặn request forge.

Verify:
- dotnet build PASS 0 err 2 pre-existing warn
- dotnet test 111/111 PASS — 0 regression
- Mirror PE pattern exact (only switch enum DuyetNcc/PhuongAn → Contract literal)

Smart Friend ROI: Reviewer caught MAJOR before push prod. Cumulative S22 #44
+ S25 #48 + S29 (this Hotfix) — pattern proven 3× Reviewer save UAT 401/403
prod incidents.

Plan B chain COMPLETE 10/10 (9 + 1 hotfix):
- A1 58898e8 / A2 a85e437 / B 138469d / C 26c98d3 / B2 1f199b0
- E1 ef23308 / D 62b50d1 / E2 48f6d22 / E3 14feb69
- Hotfix Rev (this) ApplicableType=Contract guard

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:52:40 +07:00
48f6d22b3d [CLAUDE] App: Plan B Chunk E2 — ContractDetailDto +ApprovalWorkflowId + LevelOpinions[] populate
Mirror PE PeDetailBundle pattern. Expose V2 workflow state cho FE Section 5
Chunk E3 (Implementer pending) render dynamic LevelOpinionsSectionV2.

Changes:
- ContractDtos.cs:
  - ContractDetailDto +3 fields (default null backward compat):
    - Guid? ApprovalWorkflowId (V2 pin)
    - int? CurrentApprovalLevelOrder
    - List<ContractLevelOpinionDto>? LevelOpinions
  - NEW record ContractLevelOpinionDto (mirror PE 12 fields)
- ContractFeatures.cs GetContractQueryHandler:
  - Load LevelOpinions via 3-step JOIN (ContractLevelOpinions + ApprovalWorkflowLevels.Include(Step) + Users)
  - Map to ContractLevelOpinionDto với StepOrder/Name + LevelOrder/Name + Approver/SignedBy resolve
  - OrderBy StepOrder + LevelOrder
  - Null fallback Comment "" (CS8604 silence)
  - Empty list khi V2 pin nhưng KHÔNG có opinion (workflow start lúc Drafter trình)
  - Skip load nếu V1 (ApprovalWorkflowId null) → null marker FE detect

FE Chunk E3 sẽ:
- Detect V2 mode qua `bundle.approvalWorkflowId != null`
- Fetch ApprovalFlow shape via existing /api/approval-workflows-v2/{ApprovalWorkflowId}
- Render Section 5 dynamic forEach Step → forEach Level → 1 OpinionBox với opinion data from LevelOpinions[]

Verify:
- dotnet build PASS 0 err, 0 warn (clean)
- dotnet test 111/111 PASS — 0 regression
- V1 legacy contract Detail unchanged (ApprovalWorkflowId=null + LevelOpinions=null)

Plan B chain status (8/9 chunks done):
- A1 58898e8  Entity
- A2 a85e437  Mig 32 + Seed
- B 138469d  Service ApproveV2 branch
- C 26c98d3  Mig 33 LevelOpinions
- B2 1f199b0  UPSERT block
- E1 ef23308  CreateContractCommand +V2
- D 62b50d1  FE Workspace V2
- E2 (this)  ContractDetailDto +V2 + LevelOpinions populate
- E3 FE Section 5 LevelOpinionsV2 (Implementer next)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:38:38 +07:00
ef2330871d [CLAUDE] App: Plan B Chunk E1 — CreateContractCommand +ApprovalWorkflowId V2 pin field
Mirror PE CreatePurchaseEvaluationCommand pattern. Drafter pick V2 workflow
qua Workspace Select dropdown (Chunk D FE Implementer running parallel) →
ApprovalWorkflowId pin lúc create. Fallback V1 auto activeWfId nếu null
(7 prod contract giữ behavior).

Changes:
- CreateContractCommand record +Guid? ApprovalWorkflowId = null (optional)
- Handler line 96 wire entity.ApprovalWorkflowId = request.ApprovalWorkflowId
- Both V1 + V2 fields persist (Service ApproveV2Async branch dispatch theo V2 first)

Verify:
- dotnet build PASS 0 err
- Backward compat: existing caller (KHÔNG pass ApprovalWorkflowId) → fallback null
- V1 contract path UNCHANGED

Plan B chain status:
- A1 58898e8  Entity
- A2 a85e437  Mig 32 + Seed
- B 138469d  Service ApproveV2 branch
- C 26c98d3  Mig 33 LevelOpinions
- B2 1f199b0  UPSERT block
- E1 (this)  CreateContractCommand +ApprovalWorkflowId
- D FE Workspace V2 (Implementer running parallel)
- E2 ContractDetailDto + GetContractByIdQuery extend (em main pending)
- E3 FE Section 5 LevelOpinionsV2 (Implementer pending sau E2)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:30:06 +07:00
1f199b01a5 [CLAUDE] Infra: Plan B Chunk B2 — UPSERT ContractLevelOpinion + ResolveActorFullName helper
Replace TODO marker trong Chunk B 138469d (line 257-262) bằng UPSERT block
mirror PE Mig 26 line 512-546.

Changes:
- ApproveV2Async: move matchingLevel computation UP (trước UPSERT block)
- +UPSERT ContractLevelOpinion ~25 LOC:
  - Match level theo ApproverUserId (OR-of-N) + fallback first (admin override)
  - Empty comment → "(duyệt — không ý kiến)" placeholder
  - Insert mới hoặc update existing (UPSERT semantic)
  - SignedByUserId + SignedByFullName denormalized cho Section 5 FE
- skipToFinal block reuse matchingLevel (KHÔNG re-compute)
- +ResolveActorFullNameAsync helper (mirror PE line 774-783)

Section 5 FE (Chunk E) sẽ render dynamic theo flow.steps[].levels[] với
opinion data từ table này. Admin override → FE detect SignedByUserId !==
Level.ApproverUserId → banner "Admin duyệt thay".

Verify:
- dotnet build SolutionErp.slnx PASS 0 err, 2 pre-existing DocxRenderer warn
- dotnet test 111/111 PASS — 0 regression
- V1 legacy path UNCHANGED (7 prod contract giữ behavior)

Plan B chain status:
- A1 58898e8  Entity +2 fields
- A2 a85e437  Mig 32 + Config + Seed
- B 138469d  Service ApproveV2Async branch (UPSERT TODO)
- C 26c98d3  Mig 33 ContractLevelOpinions
- B2 (this)  UPSERT block (resolve TODO Chunk B)
- D FE Workspace V2 (Implementer, next)
- E FE Section 5 V2 (Implementer, pending)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:27:46 +07:00
26c98d3c11 [CLAUDE] Domain+App+Infra: Plan B Chunk C — Mig 33 ContractLevelOpinions cookie-cutter mirror PE Mig 26
- Domain/Contracts/ContractLevelOpinion.cs (NEW entity mirror PE — AuditableEntity, 4 field core + 2 nav)
- Domain/Contracts/Contract.cs (+LevelOpinions nav collection)
- Migrations/20260522052240_AddContractLevelOpinions.cs (3-file rule: .cs + .Designer.cs + Snapshot)
- Configurations/ContractLevelOpinionConfiguration.cs (NEW separate file, mirror PE pattern)
- IApplicationDbContext.cs + ApplicationDbContext.cs (+DbSet<ContractLevelOpinion>)

UNIQUE composite (ContractId, ApprovalWorkflowLevelId) — 1 row per HĐ × Level.
FK Cascade Contract + Restrict ApprovalWorkflowLevel.
SignedByUserId KHÔNG nav (denorm SignedByFullName tránh cascade khi xoá user).

Mirror PE Mig 26 pattern (S19 2026-05-09) EXACT — UPSERT row khi Approver duyệt qua
Service ApproveV2Async (Plan B Chunk B em main 138469d đã có TODO marker).
Em main sẽ add UPSERT block sau Chunk C done (Chunk D).

Verify:
- dotnet build PASS 0 err (2 pre-existing warn DocxRenderer unrelated)
- dotnet ef database update PASS (Mig 33 applied SolutionErp_Dev + _Design)
- dotnet test 111/111 PASS (58 Domain + 53 Infra — no regression)

Plan B chain (6 chunks):
- A1 58898e8  ContractApprovalWorkflowV2 entity scaffold
- A2 a85e437  Contract.ApprovalWorkflowId + ContractConfiguration FK
- B 138469d  ContractWorkflowService ApproveV2Async skeleton + TODO LevelOpinion UPSERT
- C (this)  ContractLevelOpinions entity + Mig 33 + config + DbSet
- D FE Workspace V2 (Implementer, pending)
- E FE Section 5 V2 (Implementer, pending)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:24:38 +07:00
138469db4e [CLAUDE] Infra+App: Plan B Chunk B — Service ApproveV2Async branch + gen mã HĐ adapt
Mirror PE PurchaseEvaluationWorkflowService.cs:ApproveV2Async (line 446-634).
V1 legacy giữ behavior cũ — 7 prod contract chạy nhánh này. V2 mới pin
ApprovalWorkflowId chạy ApproveV2Async helper.

Changes:
- ContractWorkflowService.cs:
  - TransitionAsync +skipToFinal=false param F2 (Mig 31 Plan K mirror PE)
  - Drafter trình init CurrentApprovalLevelOrder=1 nếu V2 schema pin
  - APPROVE STEP branch V2/V1 dispatch theo ApprovalWorkflowId
  - +ApproveV2Async helper ~150 LOC (mirror PE pattern):
    - Load AW.Steps.Levels OR-of-N
    - Match approver actor.Id ∈ pendingLevelGroup.ApproverUserId
    - Add ContractApproval row + enrich comment skipPrefix
    - skipToFinal F2: AllowApproverSkipToFinal guard + advance pointer last
    - Advance level/step normal
    - Terminal: gen mã HĐ RG-001 + Phase=DaPhatHanh (khác PE just DaDuyet)
- IContractWorkflowService.cs: TransitionAsync +skipToFinal=false param
- ContractFeatures.cs: caller TransitionAsync use named arg ct: ct (skip optional)

TODO Chunk C: UPSERT ContractLevelOpinion (table chưa tồn tại — Mig 33
sẽ scaffold + entity + EF config). Block UPSERT add ở đây sau Chunk C done.

Verify:
- dotnet build SolutionErp.slnx PASS 0 err, 2 pre-existing DocxRenderer warn
- dotnet test 111/111 PASS (58 Domain + 53 Infra) — 0 regression
- V1 legacy path UNCHANGED (7 prod contract giữ behavior)

Plan B chain (6 chunks):
- A1 58898e8 Contract +2 fields (em main, done)
- A2 a85e437 Mig 32 schema + Config + Seed (Implementer Case 2, done)
- B (this) Service ApproveV2Async branch (em main, done)
- C Mig 33 ContractLevelOpinions (Implementer, next)
- D FE Workspace V2 (Implementer, pending)
- E FE Section 5 V2 (Implementer, pending)

Race condition lesson: em main + Implementer parallel touch BE same plan
→ Implementer stash em main WIP for clean build verify. Solution: SEQUENTIAL
chunks A→B→C, NOT parallel B với A2. Pattern add to Implementer MEMORY.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:18:46 +07:00
a85e437478 [CLAUDE] Infra: Plan B Chunk A2 — Mig 32 Contract V2 schema + Configuration + Seed sample workflow
Cookie-cutter mirror PE Mig 23+24 GỘP thành 1 Mig 32 (ADD 2 column +
FK + IX). Mirror Mig 26 pattern cho FK Restrict.

Files added/modified:
- Migrations/20260522051059_AddApprovalWorkflowToContract.cs (3-file rule )
- Migrations/20260522051059_AddApprovalWorkflowToContract.Designer.cs
- Migrations/ApplicationDbContextModelSnapshot.cs (updated)
- Configurations/ContractConfiguration.cs (+HasIndex + FK Restrict ApprovalWorkflows)
- Persistence/DbInitializer.cs (SeedSampleContractWorkflowV2 idempotent QT-HD-V2-001)

Mig 32 Up():
- ADD COLUMN Contracts.ApprovalWorkflowId Guid? NULL
- ADD COLUMN Contracts.CurrentApprovalLevelOrder int? NULL
- ADD INDEX IX_Contracts_ApprovalWorkflowId (filtered NOT NULL)
- ADD FK FK_Contracts_ApprovalWorkflows_ApprovalWorkflowId Restrict

Seed sample workflow (UAT smoke + admin Designer default):
- Code: QT-HD-V2-001 Name: "Quy trình duyệt HĐ mẫu UAT V2"
- ApplicableType: 3 (Contract) IsActive: true IsUserSelectable: true
- 1 Step "Bước 1 - Phòng CCM" + 1 Level + Approver Lê Văn Bình CCM
- Idempotent: skip nếu Code+Version existing

V1 coexist: 7 prod contract giữ WorkflowDefinitionId; V2 mới pin
ApprovalWorkflowId. Service ApproveV2Async (Chunk B em main) sẽ branch.

Verify (Implementer):
- dotnet build SolutionErp.slnx PASS 0 err (em main WIP stashed for verify)
- dotnet ef database update Dev PASS (Mig 32 applied)
- 3-file rule Mig: mig.cs + Designer.cs + Snapshot.cs

Plan B chain (6 chunks):
- A1 58898e8 Contract +2 fields (em main, done)
- A2 (this) Mig 32 schema + Config + Seed (Implementer Case 2, done)
- B Service ApproveV2Async branch (em main, in progress)
- C Mig 33 ContractLevelOpinions (Implementer, pending)
- D FE Workspace V2 (Implementer, pending)
- E FE Section 5 V2 (Implementer, pending)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:15:11 +07:00
58898e8fbe [CLAUDE] Domain: Plan B Chunk A1 — Contract +2 fields V2 (ApprovalWorkflowId + CurrentApprovalLevelOrder)
Mirror PE Mig 22-24 pattern. V1+V2 coexist (7 V1 contract giữ
WorkflowDefinitionId, V2 mới pin ApprovalWorkflowId).

Fields added:
- ApprovalWorkflowId Guid? — pin schema mới ApprovalWorkflowsV2
- CurrentApprovalLevelOrder int? — Cấp đang chờ duyệt (1/2/3) trong Step

Service ApproveV2Async branch (Chunk B) sẽ dispatch:
- if (contract.ApprovalWorkflowId is Guid awId) ApproveV2Async
- else ApproveV1Legacy (giữ behavior 7 V1 contract)

Verify:
- dotnet build SolutionErp.slnx PASS 0 err, 2 pre-existing DocxRenderer warn
- No migration (Chunk A2 sẽ scaffold Mig 32)

Plan B chain (6 chunks):
- A1 (this) Entity +2 fields (em main)
- A2 Mig 32 schema (Implementer Case 2 cookie-cutter)
- B Service ApproveV2Async branch (em main critical ~200 LOC)
- C Mig 33 ContractLevelOpinions (Implementer)
- D FE Workspace V2 (Implementer)
- E FE Section 5 LevelOpinionsV2 (Implementer)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 12:07:01 +07:00
68bceddabb [CLAUDE] Infra: Plan CA Chunk D2 hotfix — Password ≥12 chars cho catalog.manager (S22+2 policy)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m31s
Reviewer spawn pre-push verify catch CRITICAL bug Chunk D 4a592cf:
- DemoUserPassword = "User@123456" (11 chars)
- Identity password policy S22+2 ≥12 chars enforced
- → New user catalog.manager CreateAsync FAIL prod → user KHÔNG seed
- → Bro UAT login fe-user 401 → Plan CA broken on prod

Fix: per-user password override conditional check trên roles.Contains(CatalogManager).
- CatalogManager role → password = "CatalogMgr@2026" (15 chars, complexity OK)
- Existing 30 demo user → giữ DemoUserPassword "User@123456" (created pre-S22+2, alive)

Pattern reusable: Khi add demo user MỚI sau S22+2 password policy bump, MUST verify
password ≥12 chars OR override per-user. Existing 30 user idempotent skip CreateAsync
nên KHÔNG bị ảnh hưởng (password hashed in DB từ pre-bump).

Verify:
- dotnet build SolutionErp.slnx PASS 0 err
- Idempotent: existing catalog.manager (nếu manual create) skip + KHÔNG đụng password
- Smart Friend Reviewer guard active — caught issue trước push

Plan CA chain (5 commits cumulative):
- A 80d39a0 BE Role + Seed (em main solo)
- B 06a441c FE move 4 master pages (Implementer Case 2)
- C c995f42 Sidebar filter 2 app (em main solo)
- D 4a592cf Seed demo user (em main solo) — INTRODUCED BUG
- D2 (this) Hotfix password policy (em main solo, Reviewer catch)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 11:09:11 +07:00
4a592cfadb [CLAUDE] Infra: Plan CA Chunk D — Seed demo user catalog.manager + role CatalogManager
DbInitializer.cs SeedDemoUsersAsync array thêm 1 entry:
- Email: catalog.manager@solutions.com.vn (password default User@123456 per SeedDemoUsersAsync logic)
- FullName: "NV Quản lý Danh mục"
- Dept: PRO (Cung ứng)
- Position: "Nhân viên Quản lý Danh mục Dùng chung"
- Roles: [AppRoles.CatalogManager]

Cấp 1 demo user mặc định để bro UAT login fe-user verify 9 menu danh mục
(Master + Suppliers + Projects + Departments + Catalogs + 4 sub-catalogs).
Admin có thể tạo thêm user gán role CatalogManager qua /system/users +
/system/permissions Matrix tự reflect 9 menu key.

Verify:
- dotnet build SolutionErp.slnx PASS 0 err, 2 pre-existing DocxRenderer warn
- Idempotent: SeedDemoUsersAsync skip nếu user existing email
- DbInitializer chạy mỗi lần API startup → demo user auto-seed lên prod sau deploy

Plan CA wrap (4 chunk):
- A 80d39a0 BE Role + Seed permissions (em main solo)
- B 06a441c FE move 4 master pages 948 LOC (Implementer Case 2)
- C c995f42 Sidebar filter 2 app (em main solo)
- D (this) Seed demo user (em main solo)

Total LOC: +1,034 / -2 (BE 67 + FE 962 + sidebar 14 - 2 unused)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 11:02:21 +07:00
80d39a06fb [CLAUDE] Domain+Infra: Plan CA Chunk A — Add role CatalogManager + seed 9 menu CRUD
- AppRoles.cs +CatalogManager const + update All array (6 LOC)
- DbInitializer.cs RoleLabels +CatalogManager ("DM", "Nhân viên Quản lý danh mục")
- DbInitializer.cs +SeedCatalogManagerPermissionsAsync() method ~50 LOC
- Wire seed call vào SeedAdminPermissionsAsync chain (idempotent, mirror SeedPePermissionDefaults pattern)

Permission scope: 9 menu key CRUD all true
- Master (root) + Suppliers + Projects + Departments
- Catalogs (root) + CatalogUnits + CatalogMaterials + CatalogServices + CatalogWorkItems

Verify:
- dotnet build SolutionErp.slnx PASS 0 err, 2 pre-existing DocxRenderer warn
- Idempotent: skip per-(role,menuKey) existing row
- 0 FE touch (Chunk B Implementer parallel commit 06a441c)

Plan CA: anh chốt move "Cấu hình danh mục dùng chung" từ fe-admin → fe-user.
Admin tạo role CatalogManager gán user nào cần CRUD; phần phân quyền User
giữ trong fe-admin Permission Matrix (existing /system/permissions).

Pending Chunk C: sidebar filter 2 app (fe-admin HIDE 9 menu, fe-user SHOW)
Pending Chunk D: smoke verify + tạo demo user catalog.manager@solutions.com.vn

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 10:58:18 +07:00
2bf01184ca [CLAUDE] App+FE-User+FE-Admin: Plan AG4 — bổ sung Drafter + Department vào PE List card
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m27s
Anh UAT 2026-05-21: PE card danh sách thiếu người tạo + phòng ban tạo. Bổ sung 4 field
qua BE JOIN Users + Departments LEFT (cả 2 nullable theo PE entity).

BE — 4 file:
- PurchaseEvaluationDtos.cs: +4 fields DrafterUserId/DrafterName/DepartmentId/DepartmentName
- PurchaseEvaluationFeatures.cs ListHandler: JOIN Users + Departments LEFT, projection +4
- PurchaseEvaluationFeatures.cs InboxHandler: mirror JOIN + projection +4
- CreateContractFromEvaluationFeatures.cs ListApproved: mirror JOIN + projection +4

FE — 4 file × 2 app mirror:
- types/purchaseEvaluation.ts: PeListItem +4 fields
- pages/pe/PurchaseEvaluationsListPage.tsx: PE card render thêm dòng "👤 {drafterName} · {departmentName}"
  giữa Mã phiếu và Supplier. Conditional: chỉ render khi có ít nhất 1 field.

Verify:
- dotnet build clean 0 err
- dotnet test SolutionErp.slnx 111/111 PASS (58 Domain + 53 Infra) — no regression
- npm build fe-user PASS 0 TS err 1290.31 KB (gzip 336.79 KB) 1907 modules
- npm build fe-admin PASS 0 TS err 1401.66 KB (gzip 357.30 KB) 1926 modules
- 2 FE PE List file SHA256 IDENTICAL C6996194... (mirror §3.9)
- KHÔNG Mig (chỉ DTO + projection extend)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-21 18:52:04 +07:00
9ea62be6a7 [CLAUDE] PurchaseEvaluation: Plan AE — fix Changelog UserName 9 sites (Budget Adjust + 8 preventive)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m25s
Bro UAT 2026-05-19 post-S25 Plan AD: "Điều chỉnh ngân sách" entry trong
Lịch sử thay đổi show "Hệ thống" thay vì tên user thật (Phan Văn Chương /
NV CCM). Audit phát hiện systemic bug — 9 Changelog.Add sites trong PE
features MISSING UserName field, FE fallback "Hệ thống" toàn bộ.

Fix Plan AE — preventive batch (8 sites khác chắc chắn bro sẽ phát hiện sau):

PurchaseEvaluationFeatures.cs (4 sites):
- line 120 Tạo phiếu
- line 149 Hạng mục mặc định
- line 228 Cập nhật thông tin phiếu (UpdateDraft)
- line 379 Điều chỉnh ngân sách (Budget Adjust) — bro feedback chính

PurchaseEvaluationDetailFeatures.cs (5 sites):
- line 167 Thêm hạng mục (Detail Insert)
- line 225 Cập nhật hạng mục (Detail Update)
- line 257 Xóa hạng mục (Detail Delete)
- line 317 Cập nhật báo giá (Quote Update — inside if block, 16-space indent)
- line 342 Thêm báo giá (Quote Insert)
- line 377 Xóa báo giá (Quote Delete)
- line 416 Chọn NCC trúng thầu (Select Winner)

Pattern: `UserName = currentUser.FullName ?? currentUser.Email` — ICurrentUser
đã có FullName + Email từ JWT claims, KHÔNG cần inject userManager mới.

Verify:
- dotnet build clean 0 err 2 warn (pre-existing DocxRenderer)
- dotnet test 111/111 PASS

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:32:53 +07:00
a734bf2b8b [CLAUDE] PurchaseEvaluation: Plan AC — fix Lịch sử duyệt panel show Trả lại + Duyệt vượt cấp
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m27s
Bro UAT 2026-05-19 screenshot: panel "Lịch sử duyệt" KHÔNG show Return mode
events (Bro Trả lại từ Phan Văn Chương → Trà missing) + KHÔNG distinct
event Duyệt vượt cấp (skipToFinal F2).

Root cause:
- PurchaseEvaluationApprovals.Add() chỉ ở Approve branch (line 472 V2 + 660 V1)
- Reject branch line 75-103 NEVER adds Approval row — chỉ log Changelog
- skipToFinal advance branch line 532-572 dùng existing line 472 row nhưng
  comment KHÔNG distinct "vượt cấp" semantic vs approve thường

Fix Plan AC:

1. BE Service.cs Reject branch (line 75-103): capture pre-call Step/Level
   trước ApplyReturnModeAsync mutate pointer, add Approval row sau khi mutate:
   Decision=Reject + FromPhase + ToPhase=evaluation.Phase + Comment carry
   from-position + mode summary. Cover cả Trả lại (TraLai+pointer-mode) +
   Từ chối (TuChoi terminal).

2. BE Service.cs line 472 Approve branch: enrich Comment với prefix
   "[Duyệt vượt cấp tới Cấp cuối]" khi skipToFinal=true để Lịch sử duyệt
   distinguish vượt cấp với approve thường.

3. FE PeDetailTabs.tsx × 2 app ApprovalsTab: add Decision badge phân biệt
   Approve (emerald) / Trả lại (amber) / Từ chối (rose). Vì 3/4 mode Trả
   lại (OneLevel/OneStep/Assignee) giữ Phase=ChoDuyet → fromPhase→toPhase
   badge giống Approve. Decision badge bù visual phân biệt.

Verify:
- dotnet build clean 0 err 2 warn (pre-existing DocxRenderer)
- dotnet test 111/111 PASS
- npm build × fe-user + fe-admin PASS 0 TS err

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:45:25 +07:00
cdfd54212c [CLAUDE] PurchaseEvaluation: Plan AB Chunk A — fix Changelog visibility Bug 1 Budget Adjust + Bug 2 Return Mode
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Failing after 1m6s
- BE ApplyReturnModeAsync 4 mode add Changelog.Add() common path (refactor Drafter early return)
- FE PeDetailTabs.tsx HistoryTab filter extend cover Header+ngân sách (B1) + Workflow+Trả lại (B2)
- FE empty placeholder + comment update reflect new filter scope
- Mirror 2 app §3.9

Bug 1: Budget Adjust handler đã log (Header+Update) nhưng FE filter strict TraLai-only
Bug 2: Return mode Service không log Changelog — chỉ approval phase transition

Verify:
- Build clean 0 err
- npm build × 2 app pass 0 TS err
- 111 test baseline preserve (UAT skip test-after defer)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:07:44 +07:00
ee776d5745 [CLAUDE] Domain+App+Api+FE-User+FE-Admin: Plan AA Chunk A - BE IsUserSelectable filter + menu seed Pe_DuyetNcc_WfView + sidebar widen w-72 xl:w-80 + revert Plan U truncate
BE changes:
- MenuKeys.cs +helper PurchaseEvaluationWorkflowView(typeCode) => "Pe_{typeCode}_WfView"
- DbInitializer.cs SeedMenuTreeAsync:
  - tree.Add LuongDuyet (Order=2 first child) cho 2 type PE
  - INSERT-only loop -> INSERT-OR-UPDATE-Order (shift existing prod rows Order+1)
  - Idempotent: skip nếu Order match, UPDATE nếu mismatch
- DbInitializer.cs SeedPurchaseEvaluationPermissionDefaultsAsync +WfView leaf cho 7 role Read
- ApprovalWorkflowV2AdminFeatures.cs GetAwAdminOverviewQuery +IsUserSelectable bool? = null
  + handler conditional Where(d => d.IsUserSelectable == ius)
- ApprovalWorkflowsV2Controller.cs Overview signature +[FromQuery] bool? isUserSelectable
  pass-through to mediator (gotcha #44 fix preserved class-level [Authorize] bare)

FE Layout changes (mirror 2 app rule §3.9):
- fe-user resolvePath regex (List|Create|Pending|WfView) + route
  /purchase-evaluations/workflow-matrix?type=N
- fe-user + fe-admin sidebar w-60 xl:w-72 -> w-72 xl:w-80 (+48/+32px gain)
- Revert Plan U S23 t11 truncate × 5 sites (3 fe-user MenuGroup+MenuLeaf+StaticLeaf
  + 2 fe-admin MenuGroup+MenuLeaf). Keep min-w-0 flex-1 + shrink-0 + title
  tooltip (no harm). Bro request hiển thị đầy đủ label custom Mig 27 dài.

Why:
- User UAT request 2026-05-15: thêm menu "Luồng duyệt" trên Danh sách hiển thị
  ma trận phân quyền workflow V2 admin Designer ghim ra cho user xem trước khi
  tạo phiếu. Filter IsUserSelectable=true (Mig 25).
- Sidebar Plan U S23 t11 truncate hiển thị "..." → bro muốn full label.
  Widen sidebar +32-48px + bỏ truncate cho phép wrap natural khi cực dài.

Verify:
- dotnet build SolutionErp.slnx PASS clean 0 err 2 warn pre-existing DocxRenderer
- Investigator Pre-A confirm gotcha #44 đã fix permanent từ 2026-05-08
- Reviewer cumulative PASS 0 critical / 0 major / 0 minor blocker

Pending Chunk B: FE WorkflowMatrixViewPage.tsx ~215 LOC + types + App.tsx route.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 16:34:40 +07:00
0b97840674 [CLAUDE] Infra Api: Chunk T — Disable auto re-seed demo data qua DemoSeed:Disabled flag (appsettings)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m22s
Bro UAT post-Plan R+S phát hiện: 4 phiếu [DEMO]-A/B + 2 V1 workflows + 1
V2 sample TỰ ĐỘNG RE-SEED sau BE deploy commits Plan P+Q+R+S → IIS recycle
→ DbInitializer.InitializeAsync auto-seed lại 5 demo seed methods.

Plan T fix root cause: Config flag `DemoSeed:Disabled` trong
appsettings.json (production default true) → DbInitializer check flag →
skip 5 demo seed methods.

Note: appsettings.Production.json bị .gitignore (chứa secrets prod), nên
em set flag mặc định trong appsettings.json (commit qua git) — production
inherit true. Dev override false trong appsettings.Development.json để
test fresh demo seed local.

Methods SKIP khi DemoSeed:Disabled=true:
1. SeedWorkflowDefinitionsAsync (V1 PE workflow QT-DN-A v1 + QT-DN-B v1)
2. SeedPurchaseEvaluationWorkflowsAsync (V1 PE workflow extended)
3. SeedDemoContractsAsync ([DEMO] HĐ 7-type sample)
4. SeedDemoPurchaseEvaluationsAsync ([DEMO] PE 4 sample with V1 pin)
5. SeedSampleApprovalWorkflowsV2Async (V2 sample mẫu UAT type B)

Methods KEEP (luôn chạy):
- SeedRoles + SeedAdmin + SeedDepartments + SeedDemoUsers (30 UAT users)
- SeedMenuTree + SeedAdminPermissions + SeedDemoMasterData (Supplier/Project)
- SeedContractTemplates + SeedCatalogs + BackfillContractCodes
- BackfillUserEmailDomain (Phase 6 rebrand migration helper)

Files changed:
- src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs:
  + using Microsoft.Extensions.Configuration
  + Read DemoSeed:Disabled flag từ IConfiguration
  + Log "DemoSeed:Disabled=true — skip ..." khi flag true
  + Wrap 5 method seed conditional `if (!demoSeedDisabled) { ... }`

- src/Backend/SolutionErp.Api/appsettings.json:
  + "DemoSeed": { "Disabled": true } + comment narrative

- src/Backend/SolutionErp.Api/appsettings.Development.json:
  + "DemoSeed": { "Disabled": false } override cho dev

Workflow expected sau deploy:
1. CI deploy commit T → IIS recycle app pool
2. BE startup → DbInitializer reads DemoSeed:Disabled=true
3. Skip 5 demo seed methods → DB state preserved
4. Bro UAT clean slate hoàn toàn — Designer setup workflow mới from scratch

Pending T5: Final DELETE current state (4 PE + 2 V1 + 1 V2 mẫu UAT) sau
deploy applied flag. T6 verify no re-seed loop sau re-deploy.

Verify:
- dotnet build SolutionErp.slnx clean (0 err, 2 warn pre-existing)
- dotnet test SolutionErp.slnx **111/111 PASS** unchanged

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 15:07:57 +07:00
1727bd5cd9 [CLAUDE] Api Docs: Chunk P1+P3 — HOTFIX Controller TransitionPeBody record missing 3 fields (ROOT CAUSE F1+F2 fail)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m23s
CICD Monitor Run #202 Plan O verify catch CRITICAL caveat:

PurchaseEvaluationsController.cs:267 `TransitionPeBody` record CHỈ có
3 fields (TargetPhase, Decision, Comment) — MISSING 3 fields có trong
Command record `TransitionPurchaseEvaluationCommand`:
- ReturnMode (F1 mode Trả lại)
- ReturnTargetUserId (F1 Assignee target)
- SkipToFinal (F2 duyệt thẳng Cấp cuối)

Mediator.Send line 70 cũng drop 3 field. → FE × 2 app SEND ĐÚNG 7 fields
qua `api.post(/transitions)` body (Investigator audit confirm wire OK) →
ASP.NET Core deserialization silently DROP 3 fields ở Controller layer →
Handler nhận ReturnMode=null + SkipToFinal=false → fallback default Drafter
mode + F2 không trigger.

Bug present 2 NGÀY PROD từ Mig 28 deploy 2026-05-13 — gây TẤT CẢ F1+F2
wire fail từ FE side. Plan N (S23 t4) + Plan O (S23 t5) fix 5 lookup sites
discrimination NHƯNG controller body record bug block flow TRƯỚC KHI đến
lookup site. Em main + Reviewer + Implementer + Investigator all MISS bug
này xuyên 4 plan vì:
1. Mig 28 Command extend 3 fields (S21 t4) nhưng Controller body NOT extended
2. Plan K K2 add `skipToFinal` 8th param Service nhưng Controller NOT extended
3. Bug silent — no error, no compile fail, no test fail, FE call OK,
   BE return 204 nhưng handler nhận default args → wrong behavior

Plan P fix BE-only ~10 LOC 1 file `PurchaseEvaluationsController.cs`:

1. Add `using SolutionErp.Application.PurchaseEvaluations.Services` cho
   WorkflowReturnMode enum import (line ~7)

2. Extend `TransitionPeBody` record line 267 thêm 3 fields default:
   ```csharp
   public record TransitionPeBody(
       PurchaseEvaluationPhase TargetPhase,
       ApprovalDecision Decision,
       string? Comment,
       WorkflowReturnMode? ReturnMode = null,
       Guid? ReturnTargetUserId = null,
       bool SkipToFinal = false);
   ```

3. Update `mediator.Send` line 70 pass 7 fields:
   ```csharp
   await mediator.Send(new TransitionPurchaseEvaluationCommand(
       id, body.TargetPhase, body.Decision, body.Comment,
       body.ReturnMode, body.ReturnTargetUserId, body.SkipToFinal), ct);
   ```

Investigator (FE wire audit) verify:
- fe-user/src/components/pe/PeWorkflowPanel.tsx:113-124 + fe-admin mirror —
  api.post send ĐẦY ĐỦ 7 fields qua body
- KHÔNG cần fix FE
- Mig 28/31 Domain test đã cover handler logic — không cần test mới

Verify:
- dotnet build SolutionErp.slnx clean (0 err, 2 warn pre-existing DocxRenderer)
- dotnet test SolutionErp.slnx **111/111 PASS** unchanged (no regression)

Docs update:
- docs/STATUS.md Last updated S23 t6
- docs/HANDOFF.md TL;DR S23 t6 ngắn gọn
- .claude/agent-memory/cicd-monitor/MEMORY.md drift (Run #202 entry pre-existing)

Pattern reinforced cross-project:
- Controller body record MUST mirror Command record fields khi Command thêm
  optional params. Silent drop bug class — không test/build catch được.
- Investigator pre-flight audit FE wire trước khi fix BE (Plan P scope
  verify) tránh em main fix sai assumption.

Pending: CICD Monitor verify Plan P deploy + UAT test bro real.
Pending Bug 2 F2 đến Phan Văn Chương: verify workflow v14 DB structure
sau khi Plan P unblock F2 flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:27:41 +07:00
ae01ca56f2 [CLAUDE] PurchaseEvaluation Tests: Chunk O1-O5 — HOTFIX 4 lookup sites cùng pattern per-NV (Plan N point 9 cascade)
Bro UAT 2026-05-15 sau Plan N deploy phát hiện 2 bug mới:
1. Actor NV Test trong OR-of-N slot click "Trả lại Người chỉ định" → toast
   "Không phải lượt bạn — chỉ NV Cấp duyệt hiện tại mới được Trả lại / Từ
   chối phiếu" mặc dù NV Test đúng trong slot.
2. F2 Duyệt thẳng Cấp cuối → trỏ đến Phan Văn Chương Bước 2 Cấp 2 thay vì
   Nguyễn Văn Trường Bước 3 Cấp 1 (BOD) — defer follow-up vì F2 logic line
   483-524 đã đúng (lastStepIdx + lastLevelMaxOrder), cần verify workflow
   v14 DB structure.

Audit em main: Plan N chỉ fix 1/5 lookup sites — còn 4 sites cùng bug pattern:
  1. Service.cs:201 EnsureCanRejectV2Async — bug bro UAT 1 ROOT CAUSE
  2. Service.cs:248 ApplyReturnModeAsync — read Allow flag từ row đầu
  3. DetailFeatures.cs:72 F3 EnsureEditableForDetailsAsync — cùng bug
  4. Features.cs:311 F4 AdjustBudgetCommand — cùng bug

4 fix surgical (~30 LOC BE total):

**Site 1** (`PurchaseEvaluationWorkflowService.cs:201`):
```diff
- var currentLevel = step.Levels.FirstOrDefault(l => l.Order == curLvl);
- if (currentLevel?.ApproverUserId != actorId)
+ var currentLevel = step.Levels.FirstOrDefault(l =>
+     l.Order == curLvl && l.ApproverUserId == actorId);
+ if (currentLevel is null)
    throw new ForbiddenException("Không phải lượt bạn — ...");
```

**Site 2** (`PurchaseEvaluationWorkflowService.cs:248`): ApplyReturnModeAsync
+`Guid? actorUserId` param 4th + caller TransitionAsync:94 update. Filter
`l.ApproverUserId == actorUserId` trong FirstOrDefault. Non-admin actor
KHÔNG match slot → currentLevel=null → validation skip (mode logic switch
KHÔNG dùng currentLevel object — chỉ dùng curStepIdx + curLevel int values).
Admin bypass validation existing line 252.

**Site 3** (`PurchaseEvaluationDetailFeatures.cs:72`):
```diff
- var level = step?.Levels.FirstOrDefault(lv => lv.Order == levelOrder);
- if (level is null) throw ConflictException("schema lỗi");
- if (!level.AllowApproverEditDetails) throw ConflictException(...);
- if (level.ApproverUserId != actorUserId) throw ForbiddenException(...);
+ var level = step?.Levels.FirstOrDefault(lv =>
+     lv.Order == levelOrder && lv.ApproverUserId == actorUserId);
+ if (level is null) throw ForbiddenException(...);
+ if (!level.AllowApproverEditDetails) throw ConflictException(...);
```

**Site 4** (`PurchaseEvaluationFeatures.cs:311`):
```diff
- var level = step.Levels.FirstOrDefault(l => l.Order == curLvl);
- if (level is null) throw ConflictException("schema lỗi");
- if (!level.AllowApproverEditBudget) throw ConflictException(...);
- if (level.ApproverUserId != actorId) throw ForbiddenException(...);
+ var level = step.Levels.FirstOrDefault(l =>
+     l.Order == curLvl && l.ApproverUserId == actorId);
+ if (level is null) throw ForbiddenException(...);
+ if (!level.AllowApproverEditBudget) throw ConflictException(...);
```

**Regression test** (`PurchaseEvaluationPerNvLookupRegressionTests.cs` 3 test):
1. `TransitionReject_ActorD_LastInSlot_AllowsRejectViaDrafterMode` —
   Actor D (non-first-row trong OR-of-N) trả lại Drafter mode → no throw.
   Pre-fix: throw "Không phải lượt bạn" vì handler check row đầu A.
2. `TransitionReject_Outsider_NotInSlot_ThrowsForbidden` — Outsider không
   trong slot → throw đúng intent (verify fix KHÔNG over-permissive).
3. `TransitionRejectOneLevel_ActorC_HasFlagWhileOthersDont_AllowsMode` —
   Actor C only tick AllowReturnOneLevel, 3 NV khác KHÔNG. Actor C click
   "Trả lại 1 Cấp" → mode allowed. Pre-fix: read flag từ row A (false) →
   throw ConflictException "không bật mode OneLevel".

Pattern reinforced: per-NV admin opt-in flag wire **5 lookup sites** đều
phải discriminate ApproverUserId. Plan N chỉ catch 1/5. Plan O catch 4/5
còn lại. Memory user-level cần update danh sách 5 sites cho future audit.

Verify:
- dotnet build SolutionErp.slnx clean (0 err, 2 warn pre-existing DocxRenderer)
- dotnet test SolutionErp.slnx **111/111 PASS** (+3 từ 108 baseline Plan N)

Pending Chunk O7: docs + memory update commit + push.
Pending Chunk O8: CICD Monitor post-deploy verify.
Pending follow-up Bug 2 F2 đến Phan Văn Chương: verify workflow v14 DB.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 13:05:02 +07:00
03264581ff [CLAUDE] PurchaseEvaluation Tests: Chunk N1+N2 — HOTFIX per-NV lookup site discrimination Allow* flag (BE bug 2 ngày prod)
Bro UAT 2026-05-15 screenshot phát hiện: Admin Designer tick TRUE 7 flag cho
NV Test (UAT V2) slot Bước 2 Cấp 1 (4 NV cùng Cấp, OR-of-N Mig 29). Actor
login → dialog ✓ Duyệt KHÔNG có checkbox F2 skipToFinal + dialog ← Trả lại
CHỈ 1 radio Drafter + KHÔNG có F3+F4 Edit options.

Investigator audit confirm Hypothesis B: BE handler
`PurchaseEvaluationFeatures.cs:765` `FirstOrDefault(l => l.Order ==
curLevelOrder)` THIẾU discriminator `ApproverUserId == currentUser.UserId`.
Schema Mig 29 (S21 t5 2026-05-13) refactor: 1 row per ApproverUserId, OR-of-N
cùng Order → handler luôn lấy row đầu DB (Lê Văn Bính / Trần Xuân Lưu —
chỉ Drafter flag), bỏ qua admin tick per-NV của actor thật.

Bug PRESENT từ Mig 29 deploy 2026-05-13 (2 NGÀY PROD) nhưng chỉ bộc lộ khi
lần đầu admin tick selectively per-NV. Trước đây tất cả slot FALSE → mọi
actor đều thấy "không có options", behavior giống nhau, không lộ.

Cumulative gap analysis: Mig 29 + Mig 30 + Mig 31 wire 8 surface points đúng
nhưng MISS point 9 lookup discrimination → 3× refactor cùng bug. Point 9
mới được catch Plan N S23 t4 (em main + Reviewer + Implementer all MISS
xuyên 3 plan).

N1 BE fix (5 LOC line 765-779):
```csharp
var curLevel = curStep?.Levels.FirstOrDefault(l =>
    l.Order == curLevelOrder && l.ApproverUserId == currentUser.UserId)
    ?? curStep?.Levels.FirstOrDefault(l => l.Order == curLevelOrder); // admin/non-approver fallback
```

N2 Regression test (new file `GetPurchaseEvaluationCurrentLevelOptionsTests.cs`):
- `GetPe_PerNvLookup_ActorMatchesSlot_ReturnsActorSpecificFlags`:
  Seed 4 Level cùng Order=1 (mỗi Level distinct flag profile) × 4 actor →
  assert mỗi actor nhận flag riêng (KHÔNG profile khác). Critical assertion:
  Actor C → AllowApproverSkipToFinal=true (bug bro UAT regression).
- `GetPe_PerNvLookup_AdminNonApprover_FallsBackToFirstRow`:
  Admin actor (NON-match) → fallback FirstOrDefault EF SQLite non-deterministic
  → weak assert NOT null + match exactly 1 of 4 distinct profile.

Pattern reusable saved memory `feedback_per_nv_permission_scope.md` CRITICAL
HOTFIX S23 t4 section:
- Wire checklist 9 surface points (NOT 8 — thêm point 9 lookup discrimination)
- Audit cho future flag F5+: grep `FirstOrDefault.*Order ==` enumerate all
  lookup sites, verify discriminator role-context

Verify:
- dotnet build src/Backend/SolutionErp.Application clean (0 warning, 0 error)
- dotnet test SolutionErp.slnx **108/108 PASS** (+2 từ 106: 58 Domain + 50 Infra)
- N2 2 test individual PASS

Pending Chunk N4: docs + memory update commit + push remote.
Pending CICD Monitor post-deploy verify (spawn sau push).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 12:41:56 +07:00
c2042ef956 [CLAUDE] PurchaseEvaluation: Chunk M1 — Fix F1.OneLevel/OneStep edge case Bước 1 → giữ ChoDuyet (KHÔNG fallback Drafter)
Bro UAT S23 t3: "Các tính năng trả lại 1 cấp hoặc chỉ định hoặc edit cho
xử lý ở trạng thái đang gửi duyệt luôn, không về draft."

Investigator audit confirm 4 mode F1.OneLevel/Assignee + F2 + F3+F4 main
path đã giữ Phase=ChoDuyet (Mig 28-31 cumulative). Edge case duy nhất còn
fallback Drafter (Phase=TraLai):

- F1.OneLevel khi đang Bước 1 Cấp 1 (curStepIdx=0, curLevel=1) — no further back
- F1.OneStep khi đang Bước 1 (curStepIdx=0)

Logic cũ (line 303-310 OneLevel + 325-332 OneStep):
```
evaluation.Phase = PurchaseEvaluationPhase.TraLai;  // 98
evaluation.CurrentWorkflowStepIndex = null;
evaluation.CurrentApprovalLevelOrder = null;
evaluation.SlaDeadline = null;
return "Trả về Người soạn thảo (fallback — đang Bước 1 Cấp 1)";
```

Logic mới — reset (0, 1) giữ ChoDuyet:
```
evaluation.CurrentWorkflowStepIndex = 0;
evaluation.CurrentApprovalLevelOrder = 1;
summary = "Action 'Trả lại 1 Cấp/Bước' không lùi được — phiếu reset về Approver Bước 1 Cấp 1";
// SLA reset 7d ở cuối hàm cho 3 mode còn lại
```

Semantic mới (per bro chốt AskUserQuestion Plan M):
- Phase giữ ChoDuyet (KHÔNG TraLai=98)
- Pointer reset về (0, 1) = chính Approver A hiện tại (effectively no-op)
- SLA reset 7d (cuối hàm switch áp dụng cho cả 3 mode F1 non-Drafter)
- Audit log rõ "không lùi được" để Drafter/Admin biết action không hiệu lực

KHÔNG đụng:
- F1.Drafter (line 268-275) giữ nguyên semantic Phase=TraLai
- F1.Assignee (line 335-360) giữ nguyên throw nếu không match
- F2 ApproveV2Async skipToFinal (line 483-524 Plan K L1 vừa fix)
- F3 EnsureEditableForDetailsAsync (PurchaseEvaluationDetailFeatures.cs:42)
- F4 AdjustBudgetCommand handler (PurchaseEvaluationFeatures.cs:272-329)

Verify:
- dotnet build src/Backend/SolutionErp.Infrastructure clean (0 err, 2 warn
  pre-existing DocxRenderer)
- Service.cs 13+/13- LOC change (1 file, surgical edit)
- Pending Chunk M2: 2 edge case test (Implementer Case 3 spawn) + verify
  K7 Approver F2 không cascade
- Pending Chunk M3: FE label rename Phase=TraLai "Trả lại" → "Cần chỉnh sửa lại" (Implementer Case 2 spawn × 2 app)
- Pending Chunk M4: docs + memory update + push + CICD verify

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 11:11:36 +07:00
83c9f7b45d [CLAUDE] PurchaseEvaluation FE-Admin FE-User: Chunk L5 — PE list UX: ngày tạo thay SLA countdown + sort UpdatedAt DESC
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m10s
Bro UAT S23 t2 yêu cầu 2 UX changes PE list:

1. Đổi "Còn N ngày Mh" (SlaTimer countdown) → "DD/MM/YYYY HH:mm" (ngày giờ tạo phiếu).
2. Sort: phiếu vừa update (Tạo / Gửi duyệt / Trả lại) đưa lên đầu, phiếu cũ phía dưới.

BE changes:
- PurchaseEvaluationListItemDto +UpdatedAt: DateTime? field (auto AuditingInterceptor refresh
  mọi SaveChanges — covers Insert/Update/Transition events natural).
- ListPurchaseEvaluationsQueryHandler sort: OrderByDescending(UpdatedAt ?? CreatedAt)
  (was: OrderByDescending(CreatedAt)).
- GetMyPurchaseEvaluationInboxQueryHandler sort: OrderByDescending(UpdatedAt ?? CreatedAt)
  (was: OrderBy(SlaDeadline ?? MaxValue) — SLA priority deprecated).
- CreateContractFromEvaluationFeatures.cs: +UpdatedAt arg trong DTO ctor (compile fix
  consumer downstream).
- Select projection 3 callsites populate UpdatedAt.

FE × 2 app (mirror rule §3.9):
- PeListItem type +updatedAt: string | null (optional — null khi phiếu chưa Update).
- PurchaseEvaluationsListPage: replace <SlaTimer deadline={p.slaDeadline} ... /> với
  Vietnamese date format "{DD/MM/YYYY HH:mm}" qua Intl.DateTimeFormat (vi-VN locale,
  full date+time options). title tooltip hiện full timestamp.
- Remove SlaTimer import (unused warning).

UpdatedAt sort logic insight: AuditingInterceptor (Infrastructure) auto-refresh
UpdatedAt mọi SaveChanges → mọi event tự nhiên (Drafter tạo / Gửi duyệt từ Workspace
/ Approver duyệt Cấp tiếp / Approver trả lại / Admin override) đều bump UpdatedAt
→ phiếu vừa action lên đầu list. Phiếu mới Insert UpdatedAt=null → fallback CreatedAt
→ vẫn lên đầu (vì CreatedAt vừa now).

Verify:
- dotnet build production projects clean (0 err, 2 pre-existing warn)
- dotnet test SolutionErp.slnx 104/104 PASS (DTO change KHÔNG impact test — tests
  don't construct ListItemDto)
- npm run build × 2 app pass clean

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 01:53:19 +07:00
f3db9e6cc0 [CLAUDE] PurchaseEvaluation: Chunk L1 — Fix F2 skipToFinal semantic: skip pointer tới NV cuối (KHÔNG terminate DaDuyet)
Bro UAT S23 t2 catch: Plan K K2 implement F2 SAI semantic — set
Phase=DaDuyet terminal auto-approve. Bro intent: "Duyệt thẳng đến CEO,
bỏ qua các bước khác chứ ko phải chuyển sang đã duyệt."

Refactor Service.cs ApproveV2Async F2 branch:
- Resolve lastStepIdx = steps.Count - 1, lastLevelMaxOrder = max(LevelOrder)
  trong Step cuối
- Advance pointer: CurrentWorkflowStepIndex = lastStepIdx + CurrentApprovalLevelOrder = lastLevelMaxOrder
- Phase GIỮ NGUYÊN ChoDuyet — NV cuối (CEO/last approver) vẫn cần ký thật
  để tiến DaDuyet
- Audit log "Approver skip thẳng tới Bước X Cấp Y (NV cuối) — bỏ qua các Bước/Cấp trung gian"
- Guard no-op: actor đã ở slot cuối → fall through advance logic (normal → DaDuyet)
  (KHÔNG double-advance khi skipToFinal=true ngay slot cuối)
- Reset SLA 7d cho NV cuối nhận lại

FE × 2 app PeWorkflowPanel.tsx (mirror rule §3.9):
- Description text update: "Phiếu sẽ skip tới NV cuối (CEO/cấp ký cuối) —
  NV cuối vẫn cần duyệt thật để hoàn tất."
- Amber warning update: "Bỏ qua mọi Cấp/Bước trung gian, phiếu chuyển thẳng
  tới NV cuối. NV cuối vẫn phải ký duyệt thật để phiếu thành 'Đã duyệt'."

Verify:
- dotnet build production projects clean (0 err, 2 pre-existing warn)
- npm run build × 2 app pass

Pattern lesson saved memory: Service skipToFinal semantic = advance pointer
NOT terminate. K7 tests TODO update: 3 Approver F2 tests assert pointer
moved to last slot, NOT Phase=DaDuyet. Defer test fix sau UAT confirm UX.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 01:39:03 +07:00
0062fcb269 [CLAUDE] ApprovalWorkflowsV2: Chunk H — K10 hotfix AwLevelDto wire AllowApproverSkipToFinal (Mig 31 admin DTO gap)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m23s
CICD Monitor K9 catch: GET /api/approval-workflows-v2 response thiếu field
`allowApproverSkipToFinal` trong levels[]. Mig 31 column added Levels + Service
ApproveV2Async wire (K2) + PE bundle DTO ApprovalWorkflowOptionsDto wire (K2) +
FE Designer 7th checkbox UI (K3) đầy đủ — NHƯNG `AwLevelDto` admin overview
DTO chưa wire field → round-trip Designer create/update fail (em main K2 design
gap, Reviewer K2 cũng miss audit ApprovalWorkflowV2AdminFeatures).

4 edits ApprovalWorkflowV2AdminFeatures.cs:
1. AwLevelDto record +AllowApproverSkipToFinal field (7th — sau AllowApproverEditBudget)
2. ToDto handler (GetAwAdminOverviewQueryHandler) ctor call +l.AllowApproverSkipToFinal
3. CreateAwLevelInput record +AllowApproverSkipToFinal=false default (admin opt-in)
4. CreateAwDefinitionCommandHandler entity init +AllowApproverSkipToFinal = l.AllowApproverSkipToFinal

Pattern lesson: per-NV admin opt-in flag wire 6 surface points required
(entity + EF config + Mig + Service guard + PE bundle DTO + ApprovalWorkflowOptionsDto
+ FE Designer + admin AwLevelDto + Create input). Mig 30 F4 đã có same gap risk
ban đầu (S22+5 needed full wire). Update memory `feedback_per_nv_permission_scope`
checklist add "admin AwLevelDto + Create input wire" cho future flag F5+.

Verify:
- dotnet build production projects clean (0 err, 2 pre-existing DocxRenderer warn)
- Awaiting CICD Monitor K11 verify post-deploy (GET /api/approval-workflows-v2
  levels[].allowApproverSkipToFinal field PRESENT + Designer round-trip OK)

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

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

FE Admin drop (2 files):
- UsersPage.tsx: DELETE "Skip cuối" column + FastForward badge + FastForward import
                  + allowSkipMut mutation hook + FastForward toggle button
- types/users.ts: DELETE allowDrafterSkipToFinal field

fe-user KHÔNG đụng (no UsersPage admin-only; K6 sẽ handle Workspace Drafter checkbox).
FE Designer page KHÔNG đụng (K3 done; 2 stale comment leftover deferred K6).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 23:03:05 +07:00
b079b27343 [CLAUDE] PE-Workflow: S22+5 Chunk A — Mig 30 +AllowApproverEditBudget per-Level slot
Bro clarify spec S22+4:
- KHÔNG đổi logic edit ngân sách (Drafter Nháp/TraLai vẫn duy nhất default)
- Thêm flag per-NV slot trong Designer: "Cho phép NV này edit Section ngân sách
  lúc đang duyệt" (mirror pattern F3 AllowApproverEditDetails Mig 29)

Mig 30 `AddAllowApproverEditBudgetToLevels`:
- ALTER ApprovalWorkflowLevels +AllowApproverEditBudget bit NOT NULL DEFAULT 0
- 3-file rule (mig.cs + Designer.cs + Snapshot.cs)
- Apply LocalDB Dev + Design

Domain entity ApprovalWorkflowLevel +AllowApproverEditBudget (default false).
EF config HasDefaultValue(false). DTO AwLevelDto + ApprovalWorkflowOptionsDto
+ CreateAwLevelInput all extend +AllowApproverEditBudget.

PE GET handler populate currentLevelOptions thêm AllowApproverEditBudget từ
curLevel slot. Admin Designer GET/POST handler propagate flag.

AdjustBudgetCommand handler refactor ChoDuyet branch:
- Trước: check actor match level.ApproverUserId (cho phép mặc định)
- Sau: check level.AllowApproverEditBudget=true AND actor match ApproverUserId
  → throw ConflictException nếu slot chưa được cấp quyền

Verify:
- dotnet build SolutionErp.slnx — 0 err, 2 warn DocxRenderer pre-existing
- Mig 30 applied Dev + Design DB

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:09:48 +07:00
37b51d7f07 [CLAUDE] PurchaseEvaluation: S22+4 Chunk A — BE attachment view endpoint + AdjustBudget command
Feature 1 (attachment preview):
- NEW `GET /api/purchase-evaluations/{id}/attachments/{attId}/view`
- Cùng handler download, override `Content-Disposition: inline` để FE nhúng iframe
- Permission: same scope GET phiếu (Plan E V2 strict scope)

Feature 2 (điều chỉnh ngân sách):
- NEW `AdjustPurchaseEvaluationBudgetCommand` + Handler + Validator
- NEW `PATCH /api/purchase-evaluations/{id}/budget-adjust` body
  `{budgetId, budgetManualName, budgetManualAmount}`
- Phase + actor scope guard:
  * DangSoanThao/TraLai → chỉ Drafter của phiếu
  * ChoDuyet → Approver currentLevel (match ApproverUserId) — V2 only
  * Admin → bypass tất cả
- Audit changelog với diff narrative: "Điều chỉnh ngân sách: link X→Y, số tiền A→Bđ [Drafter/Approver Bước/Cấp/Admin]"
- Tách riêng KHÔNG dùng UpdatePeDraft vì Approver scope KHÔNG nên được edit
  Section 1 fields (TenGoiThau/DiaDiem/MoTa/PaymentTerms)

Verify:
- dotnet build SolutionErp.slnx — 0 err, 2 warn DocxRenderer pre-existing
- Test defer carry Plan C (UAT mode §7) — guard logic critical, ưu tiên cho S23+

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:25:49 +07:00
40f64c6b32 [CLAUDE] PE-Workflow: UAT S22+1 — disable cả 3 button khi không quyền + BE guard
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m29s
User UAT feedback: "Nếu đã không được quyền thao tác thì ko được quyền thao tác
hết tất cả các hành động" — trước đây chỉ "Duyệt" disabled, "Trả lại" + "Từ chối"
vẫn enabled (design intent S17 cũ).

FE 2 app mirror (PeWorkflowPanel.tsx):
- `isDisabled = blockedByV2Level` (drop `isForwardApprove &&` qualifier)
- Tooltip update "mới thao tác được (Duyệt / Trả lại / Từ chối)"
- Comment refresh ghi UAT S22+1 spec + cross-ref BE EnsureCanRejectV2Async

BE defense-in-depth (PurchaseEvaluationWorkflowService.cs):
- Helper mới `EnsureCanRejectV2Async` mirror FE actorInV2Level logic:
  Skip silent khi admin/V1/non-ChoDuyet/no actor/no pointer. Throw
  ForbiddenException khi V2 + ChoDuyet + actor != currentLevel.ApproverUserId.
- Invoke ở top Reject branch (cover cả TuChoi + Trả lại sub-branches).
- Chặn request forge: non-approver gọi PATCH /transitions direct sẽ 403.

Test (test-before §7 — security guard critical algorithm):
- ReturnMode tests existing 7/7 vẫn PASS (a2.Id = currentLevel approver, guard accept)
- +1 NEW test `Reject_NonApprover_V2_Throws_ForbiddenException` — outsider
  Drafter role gọi Reject phiếu V2 → throw + Phase không mutate

Verify:
- dotnet test SolutionErp.slnx — 104/104 PASS (+1 guard regression)
  Δ: 103 → 104
- npm run build × 2 app — pass (482ms + 583ms)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:46:51 +07:00
f149661d36 [CLAUDE] PurchaseEvaluation: Plan E — phân quyền strict V2 scope (List + Detail)
Thắt chặt phân quyền PE V2 từ UAT loose sang strict actor.UserId scope:

Trước (loose): mọi authenticated user thấy mọi phiếu V2 (`ApprovalWorkflowId != null`).
Sau (strict):
- ListPurchaseEvaluationsQuery: phiếu V2 chỉ visible khi actor là approver
  trong any Step.Level.ApproverUserId của workflow đã pin. Pre-compute
  userApprovalWfIds = DISTINCT workflow IDs có user trong Levels.
- GetPurchaseEvaluationQuery: same — actor must be V2 approver in any Level
  của workflow pin để thấy phiếu (ngoài Drafter scope + role eligible phase).

Drafter vẫn thấy phiếu mình tạo (regardless V2/V1). Admin bypass full.
Inbox đã strict từ Session 17 (ResolveV2InboxIdsAsync match current Cấp +
ApproverUserId) — KHÔNG đụng.

Tests defer: Plan C carry — 4 integration tests Strict V2 List + Detail
(Drafter own / V2 approver / non-approver throw 403) khi UAT confirm.

Verify:
- dotnet build SolutionErp.slnx — 0 err, 2 warning DocxRenderer pre-existing
- dotnet test SolutionErp.slnx — 103/103 PASS regression-free

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:16:59 +07:00
215b1e036a [CLAUDE] Tests: Plan C task 1-3 — Service per-NV Allow* test catch-up (S21 t4-t5 Mig 28-29)
14 test cover 3 helper sửa lớn S21 t4-t5 (test-after UAT backlog):

Task 1+2 — PurchaseEvaluationWorkflowServiceReturnModeTests.cs (7 test):
- ApplyReturnModeAsync Drafter allowed/denied/admin bypass (3 test mode flag)
- OneLevel happy path (peer review chain in same Step)
- OneLevel admin bypass (override disabled flag)
- skipToFinal Drafter allowed/denied/admin bypass (3 test per-user F2)

Task 3 — PurchaseEvaluationDraftGuardTests.cs (7 test):
- Drafter scope: DangSoanThao + TraLai → return (2 test)
- F3 Approver scope: ChoDuyet + flag on + actor match → return
- F3 Approver scope: ChoDuyet + flag off → ConflictException
- F3 Approver scope: ChoDuyet + flag on + actor mismatch → ForbiddenException
- Admin bypass ChoDuyet + flag off → return
- DaDuyet any caller → ConflictException (terminal phase)

InternalsVisibleTo: expose PurchaseEvaluationDraftGuard internal helper cho test.

Finding: skipToFinal Service mutate Phase=ChoDuyet TRƯỚC validate user flag.
Throw chặn SaveChanges nên DB không persist nhưng in-memory dirty. Note trong
test — không refactor scope catch-up (defer S22+).

Verify:
- dotnet test SolutionErp.slnx — 103/103 PASS (58 Domain + 45 Infra)
  Δ: 89 → 103 (+14: ReturnMode 7 + Guard 7)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:14:03 +07:00
60efeeda63 [CLAUDE] Users: Plan D — F2 toggle AllowDrafterSkipToFinal per-user (Mig 29 wire UI)
BE: UserDto +AllowDrafterSkipToFinal + SetUserAllowDrafterSkipToFinalCommand
+ Handler + UsersController PATCH /api/users/{id}/allow-skip-final body
{allowDrafterSkipToFinal:bool} Policy=Users.Update.

FE Admin: User type +allowDrafterSkipToFinal. UsersPage column "Skip cuối"
violet FastForward badge + action button toggle mirror bypass-review pattern.

fe-user KHÔNG mirror (UsersPage admin-only).

Verify:
- dotnet build SolutionErp.slnx — 0 err, 2 warning DocxRenderer pre-existing
- npm run build fe-admin — pass 638ms

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:03:27 +07:00
036694638e [CLAUDE] PE-Workflow: S21 t5 Chunk A — Mig 29 refactor Allow* sang per-NV (per-Level + per-Drafter)
Refactor 6 Allow* options từ workflow-level (Mig 28 S21 t4) sang per-NV scope:
- F1 (4 mode Trả lại) + F3 (Edit Section 2) → 5 flag MOVE xuống
  `ApprovalWorkflowLevels` (per slot Approver, cùng table với ApproverUserId).
- F2 (AllowDrafterSkipToFinal) → MOVE xuống `Users` (per-Drafter user, User Mgmt).

Mig 29 `RefactorAdvancedOptionsToPerLevelAndDrafterUser` — 4-stage migration
(EF auto-generated drop-then-add đã được REORDER manual):
1. ADD 5 column trên `ApprovalWorkflowLevels` (AllowReturnOneLevel/OneStep/
   ToAssignee/ToDrafter[default true]/AllowApproverEditDetails)
2. ADD 1 column trên `Users` (AllowDrafterSkipToFinal default false)
3. BACKFILL bulk SQL (preserve admin config Mig 28):
   - Levels: copy workflow.Allow* → all Levels của workflow (JOIN Steps)
   - Users: SET TRUE cho user nào từng Drafter PE link workflow Allow=true
4. DROP 6 column workflow-level (Mig 28 cleanup)
3-file rule complete. Apply LocalDB Dev + Design success.

Domain entity refactor:
- `ApprovalWorkflow.cs` — REMOVE 6 Allow* field (S21 t4 Mig 28 cũ)
- `ApprovalWorkflowLevel.cs` — ADD 5 Allow* field (F1 + F3)
- `User.cs` — ADD 1 Allow* field (F2 AllowDrafterSkipToFinal)

EF config update:
- `ApprovalWorkflowConfiguration.cs` — remove 6 HasDefaultValue workflow-level,
  add 5 HasDefaultValue per-Level (4 false + 1 AllowReturnToDrafter true S17)

Service refactor `ApplyReturnModeAsync` (`PurchaseEvaluationWorkflowService.cs`):
- Resolve currentLevel slot (CurrentWorkflowStepIndex + CurrentApprovalLevelOrder)
- Read 5 Allow* từ `currentLevel.AllowXxx` thay vì workflow.Allow*
- Admin bypass per-Level flag check (unchanged behavior)
- Drafter mode đặc biệt: check AllowReturnToDrafter của currentLevel (vẫn validate)
- V1 legacy (no V2 schema) → fallback Drafter behavior tự động

DRAFTER trình refactor (`TransitionAsync` skipToFinal branch):
- Permission check moved from workflow-level → `drafterUser.AllowDrafterSkipToFinal`
- Use `userManager.FindByIdAsync(actorUserId)` để get current Drafter user entity
- Admin bypass user flag check (unchanged)

Helper `EnsureEditableForDetailsAsync` refactor:
- Read `level.AllowApproverEditDetails` thay vì workflow.AllowApproverEditDetails
- Error message rõ "Cấp Approver hiện tại (Bước X / Cấp Y)" thay vì "Workflow"

DTO refactor:
- `AwLevelDto` ADD 5 Allow* field (admin Designer GET per-Level)
- `AwDefinitionDto` REMOVE 6 Allow* (no longer workflow-level)
- `CreateAwLevelInput` ADD 5 Allow* param (admin Designer POST per-Level)
- `CreateAwDefinitionCommand` REMOVE 6 Allow* (Steps[].Levels[] now has them)
- `ApprovalWorkflowOptionsDto` chỉ còn 5 flag (F2 removed — separate field)
- `PurchaseEvaluationDetailBundleDto`:
  - rename `WorkflowOptions` → `CurrentLevelOptions` (clearer semantic per-slot)
  - ADD `DrafterAllowSkipToFinal bool` (resolve từ DrafterUserId → User entity)

GetPurchaseEvaluationQueryHandler populate:
- `currentLevelOptions` = 5 Allow* của Cấp hiện tại (null nếu V1 legacy / no pointer)
- `drafterAllowSkipToFinal` = User.AllowDrafterSkipToFinal lookup từ DrafterUserId

Backward compat verified:
- Mig 29 backfill preserve admin config S21 t4 — workflow cũ vẫn chạy đúng
  sau deploy. User chưa từng làm Drafter F2 phải opt-in lần đầu (no auto-set).
- 84 test PASS (58 Domain + 26 Infra unchanged, 3 gotcha #45 guard test backward
  compat signature).

Pending Chunk B/C: FE Admin Designer move 5 checkbox xuống per-Level slot + FE
eOffice read currentLevelOptions + drafterAllowSkipToFinal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 20:03:28 +07:00
c56024ba25 [CLAUDE] PE-Workflow: Chunk B — BE Service + handlers + DTOs (F1+F2+F3)
F1 — 4 mode Trả lại (Service.ApplyReturnModeAsync helper):
- WorkflowReturnMode enum (OneLevel / OneStep / Assignee / Drafter)
- OneLevel: lùi 1 Cấp trong cùng Step (peer review). Bước 1 Cấp 1 → fallback Drafter.
- OneStep: lùi sang Bước trước Cấp cuối. Bước 1 → fallback Drafter.
- Assignee: pick runtime → tìm Step+Level match ApproverUserId trong workflow.
- Drafter: Phase=TraLai clear pointer như S17 (backward compat).
- 3 mode đầu giữ Phase=ChoDuyet, reset SLA 7d. Mode Drafter clear SLA.
- Admin bypass workflow.Allow* flag check. Non-admin → throw ConflictException
  với message rõ "Workflow không bật mode X".

F2 — Drafter skipToFinal (extend DRAFTER trình branch):
- Workflow.AllowDrafterSkipToFinal=true required (non-admin)
- Set CurrentWorkflowStepIndex = Steps.Count-1 + CurrentApprovalLevelOrder = max Level
- Audit comment append "[Drafter gửi thẳng Cấp cuối]"

F3 — Approver edit Section 2 (Detail + NCC + Báo giá):
- New helper `EnsureEditableForDetailsAsync` (extend pattern PurchaseEvaluationDraftGuard):
  - Drafter scope: DangSoanThao OR TraLai (any role, Controller [Authorize] handles)
  - F3 Approver scope: ChoDuyet + workflow.AllowApproverEditDetails=true +
    actor.Id match CurrentLevel.ApproverUserId. Admin bypass flag check.
  - Throw ForbiddenException nếu approver Cấp khác nhau (rõ Bước/Cấp trong message).
- 8 handler switch helper + inject ICurrentUser khi cần:
  - Detail: Add (existing ICurrentUser) / Update + Delete (inject new)
  - Quote: Upsert + Delete (inject new)
  - Supplier: Add (existing) / Update + Delete (inject new + add guard, trước
    đây hoàn toàn KHÔNG có phase guard — bonus security fix)
- Audit: thêm changelog Update/Delete handler (trước đây silent). Khi phase=
  ChoDuyet append " [Approver edit khi đang duyệt]" cho lịch sử rõ ai sửa.

Extension Service `TransitionAsync` signature (backward compat — 3 optional
param thêm cuối + default null/false):
- WorkflowReturnMode? returnMode = null
- Guid? returnTargetUserId = null
- bool skipToFinal = false

TransitionPurchaseEvaluationCommand DTO + Validator + Handler — mirror signature.

DTO extensions:
- ApprovalWorkflowOptionsDto NEW sub-record (6 Allow* flag) cho FE filter
- PurchaseEvaluationDetailBundleDto + WorkflowOptions field (null nếu V1 legacy)
- GetPe handler populate awOptions từ ApprovalWorkflow entity load (Mig 23 path)
- AwDefinitionDto + 6 Allow* field (admin Designer GET overview)
- CreateAwDefinitionCommand + 6 Allow* param (admin Designer POST new version)
- Handler ToDto + entity new() — propagate Allow* end-to-end

Default backward compat: workflow cũ → AllowReturnToDrafter=true (Mig 28 DB
default), 5 flag còn lại false. Phiếu cũ V2 vẫn Trả lại Drafter như S17 sau
deploy — no breaking change.

Verify:
- dotnet build SolutionErp.slnx → 0 err, 2 warn pre-existing DocxRenderer
- 3 regression test gotcha #45 vẫn PASS (backward compat signature change)
- LocalDB Dev + Design đã apply Mig 28 (Chunk A)

Pending Chunk C: FE Admin Designer mirror 2 app (6 checkbox + DTO types).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:57:09 +07:00
0294693a4a [CLAUDE] PE-Workflow: Chunk A — Mig 28 +6 Allow* column ApprovalWorkflow (F1+F2+F3 advanced options)
Domain `ApprovalWorkflow` (Mig 22 — Session 17) thêm 6 boolean cấu hình "Cấu
hình nâng cao" cho admin Designer (F1 trả lại modes + F2 skip cấp cuối + F3
approver edit Section 2):

- AllowReturnOneLevel       (default false) — F1 mode 1 lùi 1 Cấp peer review
- AllowReturnOneStep        (default false) — F1 mode 2 lùi 1 Bước
- AllowReturnToAssignee     (default false) — F1 mode 3 pick runtime từ NV đã duyệt
- AllowReturnToDrafter      (default TRUE)  — F1 mode 4 backward compat S17 fallback
- AllowDrafterSkipToFinal   (default false) — F2 Drafter trình thẳng Cấp cuối
- AllowApproverEditDetails  (default false) — F3 Approver edit HangMuc/NCC/Báo giá

Default backward compat S17: AllowReturnToDrafter=true → mọi workflow cũ chạy
đúng "Trả về Drafter" Phase=TraLai. 5 flag còn lại default false → admin
opt-in per workflow để audit nghiêm.

Mig 28 `AddAdvancedOptionsToApprovalWorkflows`:
- AddColumn × 6 bit NOT NULL DEFAULT 0/1 (3-file rule complete + Designer + Snapshot)
- Apply LocalDB SolutionErp_Dev (runtime) + SolutionErp_Design (ef tooling)

EF config ApprovalWorkflowConfiguration thêm 6 HasDefaultValue match Mig 28
default (backfill rows cũ + ef snapshot consistency).

3 mode Trả lại mới giữ Phase=ChoDuyet, chỉ lùi pointer (peer review chain
sequential). Mode Drafter giữ Phase=TraLai + clear pointer như S17. Behavior
implement trong Chunk B (Service.TransitionAsync extend branches).

Verify:
- dotnet ef migrations add success (no compile error)
- 3-file rule complete: 28 mig × 2 + Snapshot = 57 file Migrations dir
- LocalDB Dev + Design both apply success

Pending Chunk B: BE Service branches + handlers + Controller body extend.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:46:01 +07:00
de0088742f [CLAUDE] PurchaseEvaluation: Chunk A — BE guard target TraLai/TuChoi BẮT BUỘC decision=Reject + 3 regression test
Defense-in-depth chặn FE inconsistency (gotcha #45 — Session 21 turn 3).
Bug pattern: button "← Trả lại" trong PeWorkflowPanel.tsx gửi decision=Approve
khi target=TraLai do `isReject` local var thiếu nhánh TraLai → BE skip Reject
branch → enter APPROVE STEP → ApproveV2Async UPSERT opinion = "đã duyệt" +
advance Cấp. User UAT thấy: "Trả về nhưng hệ thống vẫn duyệt".

BE guard:
- Service `TransitionAsync` thêm early check sau set isAdmin/isSystem
- targetPhase ∈ {TraLai, TuChoi} && decision != Reject → throw ConflictException
- Boundary protection cho mọi caller tương lai (API client / mobile / cron)

Tests (Infra suite +3):
- TransitionAsync_TargetTraLai_WithApproveDecision_Throws_AndDoesNotMutateState
- TransitionAsync_TargetTuChoi_WithApproveDecision_Throws_AndDoesNotMutateState
- TransitionAsync_TargetTraLai_WithRejectDecision_SetsPhaseTraLai (happy path)
+ NoOpNotificationService stub reusable cho future PE service tests

Verify:
- dotnet test SolutionErp.slnx → 84 PASS (58 Domain + 26 Infra = +3 from 81 baseline)
- Build pass (0 err, 2 warn CS8602 pre-existing DocxRenderer)

Pending Chunk B: FE fix PeWorkflowPanel.tsx isReject + dialog isSendBack
mirror 2 app (fe-admin + fe-user) — sync với BE guard rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 09:41:14 +07:00
059bfcbe38 [CLAUDE] FE-Admin+Domain: Chunk C — MenuVisibilityPage + menu key + seed
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
Session 20 turn 7 Chunk C. FE Admin page quản lý Ẩn/Hiện + Đổi tên menu
cho fe-user (eOffice). Admin sidebar fe-admin LUÔN dùng Tên gốc — page này
KHÔNG đụng admin navigation (user Q2=b).

Domain MenuKeys.cs:
  +const MenuVisibility = "MenuVisibility"
  All[] thêm MenuVisibility (giữa Permissions + Workflows)

DbInitializer SeedMenuTreeAsync:
  +leaf (MenuVisibility, "Menu eOffice", System, 94, "Eye")
  Workflows shift Order 94 → 95
  Idempotent — chỉ INSERT nếu chưa có trong DB
  Manual seed Mig 27 LocalDB Dev: INSERT MenuItems + Permissions cho Admin role

FE Admin:
  - types/menu.ts: MenuItem/MenuNode +isVisible bool +displayLabel string|null
  - lib/menuKeys.ts: +MenuVisibility const
  - components/Layout.tsx resolver +MenuVisibility → /system/menu-visibility
  - App.tsx +Route + import MenuVisibilityPage

NEW pages/system/MenuVisibilityPage.tsx (~210 LOC):
  - PageHeader + 4 StatCard (Tổng / Hiển thị / Đã ẩn / Đã đổi tên)
  - Search input (key | label | displayLabel)
  - Table: Key (mono + parentKey ↳) | Tên gốc | Input "Tên hiển thị" inline
    (placeholder "Mặc định: ...") | Toggle Hiển thị/Ẩn (emerald/amber) |
    Lưu (khi dirty) / Khôi phục (khi đã custom)
  - PATCH /menus/{key} body { isVisible, displayLabel } — trim whitespace,
    empty string → null
  - onSuccess: invalidate ['menus', 'all'] + ['my-menu'] + clear draft entry
  - "Khôi phục mặc định" button: PATCH isVisible=true, displayLabel=null
  - Footer hint: nhắc admin sidebar luôn dùng Tên gốc, đổi tên áp eOffice

Verify:
- npm run build × fe-admin pass

Pending Chunk D: FE Layout fe-user filter !isVisible + render displayLabel
Pending Chunk E: Docs S20 turn 7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:37:47 +07:00
ef394f8067 [CLAUDE] Api+App: Chunk B — PATCH /menus/{key} + DTO extend isVisible/displayLabel
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m49s
Session 20 turn 7 Chunk B. BE API cho admin Ẩn/Hiện + Đổi tên menu fe-user.

DTO (MenuDtos.cs):
  - MenuNodeDto +IsVisible bool +DisplayLabel string?
  - MenuItemDto +IsVisible bool +DisplayLabel string?

GetMyMenuTreeQueryHandler:
  - Pass m.IsVisible + m.DisplayLabel vào MenuNodeDto record
  - KHÔNG filter IsVisible server-side (FE 2 app tự filter — fe-admin
    render hết, fe-user filter ẩn). Lý do: 1 endpoint serve cả 2 FE.

ListMenuItemsQueryHandler: +IsVisible +DisplayLabel trong Select projection.

NEW UpdateMenuItemCommand + Validator + Handler (PermissionFeatures.cs):
  - Body: { Key, IsVisible, DisplayLabel? }
  - Validator: Key required + max 50, DisplayLabel max 200
  - Handler: load MenuItem by Key (NotFoundException nếu missing), set
    IsVisible + DisplayLabel (whitespace → null normalize), SaveChangesAsync

MenusController +PATCH /api/menus/{key}:
  - [Authorize(Policy = "Permissions.Update")] — reuse policy admin matrix
  - Body: UpdateMenuItemRequest { IsVisible, DisplayLabel? }
  - Send UpdateMenuItemCommand qua MediatR
  - Return 204 NoContent

Verify:
- dotnet build SolutionErp.slnx — 0 err (1 warn cũ DocxRenderer không liên quan)

Pending Chunk C: FE Admin MenuVisibilityPage
Pending Chunk D: FE Layout fe-user filter + render displayLabel
Pending Chunk E: Docs S20 turn 7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:32:25 +07:00
2ea2d27785 [CLAUDE] Infra: Mig 27 — Chunk A MenuItem +IsVisible +DisplayLabel
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
Session 20 turn 7: admin có thể Ẩn/Hiện + Đổi tên hiển thị menu cho fe-user
(eOffice). Admin sidebar luôn giữ Label gốc (user Q2=b "chỉ của eOffice thôi").

Domain MenuItem: +IsVisible bool=true +DisplayLabel string?(200)
EF Configuration: HasDefaultValue(true) + HasMaxLength(200)
Migration 27 AddVisibilityAndDisplayLabelToMenuItems — 3-file rule:
  + AddColumn IsVisible bit NOT NULL DEFAULT 1
  + AddColumn DisplayLabel nvarchar(200) NULL

Verify:
- dotnet build SolutionErp.slnx — 0 warn / 0 err
- dotnet ef database update --connection SolutionErp_Dev — applied OK
- dotnet ef database update SolutionErp_Design — applied OK

Pending: B (BE API) → C (FE admin page) → D (FE user render) → E (Docs)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:29:50 +07:00
9dee00da01 [CLAUDE] PurchaseEvaluation: Chunk A — reorder section Hạng mục lên #2 + auto-tạo 1 row mặc định
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m20s
Session 20 UI restructure (3 yêu cầu user). Chunk A xử lý:

BE — CreatePurchaseEvaluationCommandHandler thêm 1 PurchaseEvaluationDetail
mặc định khi tạo phiếu mới:
  - GroupCode="01", GroupName="Hạng mục chính"
  - NoiDung = TenGoiThau (tên gói thầu)
  - DonGiaNganSach = ThanhTienNganSach = Budget.TongNganSach (nếu link)
    fallback BudgetManualAmount fallback 0
  - DonViTinh="gói", KL=1, Order=1
  - Changelog entry kèm theo (audit Insert Detail)

FE — Đổi thứ tự 5 section trong PeDetailTabs.tsx (mirror 2 app):
  1. Thông tin gói thầu (giữ)
  2. Hạng mục + Báo giá (chuyển từ #4 lên #2)
  3. Chọn NCC / TP (từ #2 xuống #3)
  4. NCC / TP tham gia (từ #3 xuống #4 — Chunk B sẽ gộp vào #2 nested)
  5. Ý kiến cấp duyệt (giữ)

Q1=a: Giữ Section "Chọn NCC TP thắng thầu" riêng (rõ UX).
Q2=a "1 hạng mục trước tiên": auto-seed đủ, multi-hạng-mục defer.

Verify:
- dotnet build SolutionErp.slnx — 0 warning / 0 error
- Test pass mặc định skip (Phase 9 UAT iteration, Q4 user public luôn)

Pending Chunk B: Nested grid Hạng mục → NCC expand inline edit
Pending Chunk C: Section 5 gộp đồng cấp cùng Phòng (1 box / Step)
Pending Chunk D: Docs S20 changelog + STATUS + HANDOFF

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 09:54:12 +07:00
90baa8e73c [CLAUDE] PurchaseEvaluation: Chunk B Service V2 hook UPSERT opinion + DTO + GET include
Service `ApproveV2Async` sau khi log approval (Decision=Approve) → UPSERT
row `PurchaseEvaluationLevelOpinions` cho Cấp hiện tại (auto sync ý kiến
từ comment khi duyệt). Reject KHÔNG sync.

Match level theo ApproverUserId của actor (multi-NV cùng Cấp OR-of-N).
Admin override (actor.Id KHÔNG match) → fallback first level — FE detect
SignedByUserId !== Level.ApproverUserId hiển thị "Admin duyệt thay".

Empty/whitespace comment → "(duyệt — không ý kiến)" placeholder (Q4 bonus).

Helper `ResolveActorFullNameAsync(actorUserId, isSystem, ct)` lookup
denorm SignedByFullName từ Users (fallback "(System)" / "(unknown)").

DTO `PurchaseEvaluationLevelOpinionDto` (15 fields):
- StepOrder/StepName/StepDepartmentId/StepDepartmentName (Bước Phòng)
- LevelOrder/LevelName/ApproverUserId/ApproverFullName (Cấp NV)
- Comment/SignedAt/SignedByUserId/SignedByFullName (sign-off)

GetPurchaseEvaluationQueryHandler:
- Include LevelOpinions
- helper BuildLevelOpinionsAsync JOIN ApprovalWorkflows.Steps.Levels +
  Departments + Users → denorm DTO. Empty list cho phiếu V1 / V2 chưa
  có cấp nào duyệt → FE fallback message.

Verify: dotnet build pass + dotnet test 81 pass (no regression).

Chunk C kế tiếp: FE Section 5 dynamic mirror 2 app.
2026-05-09 11:00:01 +07:00
77a30584fc [CLAUDE] PurchaseEvaluation: Mig 26 PeLevelOpinions V2 dynamic — Chunk A Domain + EF
Schema mới cho Section 5 "Ý kiến cấp duyệt" V2 dynamic theo
ApprovalWorkflowsV2 (Mig 22-25). Thay thế Mig 15 cố định 4 box (V1).

Entity `PurchaseEvaluationLevelOpinion : AuditableEntity`:
- (PEId, ApprovalWorkflowLevelId) UNIQUE composite
- Comment nvarchar(2000) — text ý kiến hoặc "(duyệt — không ý kiến)" placeholder (Q4 bonus)
- SignedAt datetime2 (luôn có khi UPSERT từ ApproveV2Async)
- SignedByUserId Guid (NV chính chủ HOẶC Admin override)
- SignedByFullName nvarchar(200) — denorm tránh user bị xóa/đổi tên

EF: FK Cascade Pe + Restrict Level. SignedByUserId KHÔNG nav (denorm only).
Migration 26 `AddPeLevelOpinionsForV2`: 1 CREATE TABLE + 2 FK + 2 index.
3-file rule commit đủ (.cs + Designer + Snapshot).

Apply LocalDB SolutionErp_Dev OK (Mig 25 + 26 catchup).

Verify: dotnet build pass + dotnet test 81 pass (no regression).

Chunk B kế tiếp: Service V2 hook UPSERT auto trong ApproveV2Async.
2026-05-09 10:56:16 +07:00
2a53107602 [CLAUDE] AwV2: Mig 25 +IsUserSelectable + Designer pin toggle + Workspace filter, bỏ "(clone)"
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
Hai yêu cầu UAT 2026-05-08:
1. Bỏ "(clone)" auto-append khi clone version mới — version đã đủ phân biệt.
2. Thêm pin toggle để admin chọn workflows nào cho user pick lúc tạo phiếu.

Migration 25 AddIsUserSelectableToApprovalWorkflows:
- ALTER ApprovalWorkflows ADD IsUserSelectable bit NOT NULL DEFAULT 0
- UPDATE backfill SET IsUserSelectable=1 WHERE IsActive=1 (giữ behavior cũ
  cho active versions, archived = false default — admin tự pin nếu cần)

BE:
- Domain ApprovalWorkflow +property IsUserSelectable
- DTO AwDefinitionDto +field
- CreateAwDefinitionCommandHandler set default true cho version mới
- New SetAwUserSelectableCommand + Handler
- API PATCH /api/approval-workflows-v2/{id}/user-selectable (Workflows.Create policy)
- DbInitializer SeedSampleApprovalWorkflowsV2Async set IsUserSelectable=true

FE Designer (fe-admin):
- DefinitionDto +isUserSelectable
- Badge amber "Pin Cho user chọn" khi true (cạnh Đang áp dụng/Archived)
- Button "Pin/PinOff Ghim cho user / Bỏ ghim" trong action group + mutation toggle
- Auto-fill name khi clone: bỏ "(clone)" suffix → giữ nguyên name

FE Workspace (fe-admin + fe-user):
- approvalWorkflows query filter w.isUserSelectable === true
- User dropdown chỉ thấy workflows admin đã pin

Verify: dotnet build pass · 81 test pass · npm build × 2 pass · Mig 25 apply LocalDB OK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 19:15:23 +07:00
f77ea3828a [CLAUDE] Fix: ApprovalWorkflowsV2 GET ai authenticated cũng đc — Drafter pick workflow lúc create PE
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m14s
Bug UAT 2026-05-08: user Drafter (nv.test) login Workspace tạo phiếu B,
dropdown "Quy trình duyệt" empty silent. Sample seed B đã chạy đúng
(Designer admin hiển thị sample + clone v02 active) nhưng Workspace empty.

Root cause: class-level [Authorize(Policy = "Workflows.Read")] →
non-admin role 403 Forbidden khi GET /api/approval-workflows-v2.
TanStack Query catch error silent → dropdown empty không có warning.

Fix:
- Class-level [Authorize] only (any authenticated)
- GET inherit class policy (Drafter cần list workflow để pick — read-only)
- POST + DELETE giữ [Authorize(Policy = "Workflows.Create")] — admin-only Designer

Workflow data không nhạy cảm — chỉ là cấu hình quy trình. Validate
ApplicableType match PE.Type ở Create command đã có.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:45:04 +07:00
937eb2449c [CLAUDE] Workflow V2: clone leaf Designer + sample seed cho DuyetNccPhuongAn (B)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m16s
Mở rộng V2 schema cho type B mirror type A đã chốt S17. Phần lớn đã chung
qua ApplicableType discriminator — chỉ thêm menu key + sample seed.

Changes:
- MenuKeys.cs: +const ApprovalWorkflowDuyetNccPhuongAnV2 (AwV2_DuyetNccPhuongAn) + add vào All array
- DbInitializer.SeedMenusAsync: +leaf "Duyệt NCC và Giải pháp (Mới)" dưới root ApprovalWorkflowsV2
- DbInitializer +SeedSampleApprovalWorkflowsV2Async: seed QT-DN-PA-V2-001 v01 (1 Bước Phòng CCM × 1 Cấp NV test)
  Idempotent — skip nếu admin đã tạo bất kỳ workflow B nào hoặc thiếu test user
- fe-admin/lib/menuKeys.ts: +AwV2_DuyetNccPhuongAn

KHÔNG đụng:
- Migration (V2 schema chung qua ApplicableType — Mig 22-24 đã hỗ trợ B)
- Service ApproveV2Async (không hardcode type)
- Designer page ApprovalWorkflowsV2Page (TYPE_CODE_TO_INT đã có B=2)
- Layout/App.tsx (regex AwV2_(.+) match dynamic)
- Permission default (admin bypass + role khác không cần Designer access)

Verify: dotnet build pass · 81 test pass · npm build fe-admin pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:07:56 +07:00
de0f38dd25 [CLAUDE] PE Panel 3: bỏ phase cards + render flow workflow V2 thực tế
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m14s
User feedback: "bỏ luôn cái quy trình phía trên đi nhé, vì nó là trạng
thái rồi (đã có badge), update cái flow quy trình mới vào bên panel 3
đang đến ai".

BE — ApprovalFlow DTO mới (full snapshot Bước → Cấp → NV với Status):
- PurchaseEvaluationApprovalFlowDto { CurrentStepIndex, CurrentLevelOrder,
  Steps[] }
- PurchaseEvaluationApprovalFlowStepDto { Order, Name, DepartmentId/Name,
  Status, Levels[] }
- PurchaseEvaluationApprovalFlowLevelDto { Order, Name, Approvers[], Status }
- Status: "Done" | "Current" | "Pending"

Handler GetById compute Status logic:
  - Phase=DaDuyet  → tất cả Steps/Levels "Done"
  - Phase=Nháp/Trả lại/Từ chối → tất cả "Pending"
  - Phase=ChoDuyet:
    * Step.Index < currentIdx          → all Levels "Done"
    * Step.Index == currentIdx:
        Level.Order < currentLevelOrder → "Done"
        Level.Order == currentLevelOrder → "Current"
        Level.Order > currentLevelOrder → "Pending"
    * Step.Index > currentIdx           → all "Pending"
- Load Approvers info (FullName + Email) qua UserManager batch query

FE (cả 2 app mirror):
- types/purchaseEvaluation.ts: +PeApprovalFlow + Step + Level + Status union
  PeDetail.approvalFlow optional
- PeWorkflowPanel:
  * BỎ phase cards section (4 ô Nháp/TraLai/ChoDuyet/DaDuyet) — đã
    duplicate với status badge ở header
  * Header mới: "Quy trình duyệt" + Code + Version + Name workflow pin
  * Render Flow vertical: Bước (icon ✓/●/○) → border + bg theo status
    + dept badge → list Cấp (icon nhỏ) với label "đang chờ" / "đã
    duyệt" + tên NV duyệt
  * Phiếu V1 legacy (no flow): show note "dùng quy trình cũ — không
    khả dụng chi tiết"
  * Bỏ helper isPastPhase() (orphan sau khi xóa cards)

Verify: BE build 0 error · 2 FE builds OK.

Test eoffice:
1. Mở phiếu V2 đang ChoDuyet → thấy flow Bước 1 (Phòng A):
   ✓ Cấp 1 NV X (đã duyệt)
   ● Cấp 2 NV Y (đang chờ)  ← highlight
   ○ Cấp 3 NV Z (chưa)
2. Phase=DaDuyet → all Steps/Levels green ✓
3. Phase=Nháp/TraLai → all greyed ○
4. V1 legacy → fallback note
2026-05-08 16:16:40 +07:00
d250ae4e71 [CLAUDE] PE Inbox: nhận filter approvalWorkflowId + show dropdown cả 2 view
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m3s
User báo:
- Filter "Tất cả quy trình duyệt" hiện chỉ ở Danh sách → muốn ở cả 2
- Filter chọn quy trình → không thấy phiếu V2 trong Duyệt (Inbox)

BE — wire filter vào Inbox:
- GetMyPurchaseEvaluationInboxQuery +ApprovalWorkflowId? param
- Handler thêm filter `q.Where(x => x.e.ApprovalWorkflowId == awId)`
- PurchaseEvaluationsController.Inbox +approvalWorkflowId query param

FE (cả 2 app mirror):
- PurchaseEvaluationsListPage: bỏ điều kiện `!pendingMe` ở Select dropdown
  → hiển thị filter quy trình duyệt CẢ Duyệt + Danh sách
- Inbox API call: pass approvalWorkflowId từ URL param

Verify: BE 0 error · 2 FE builds OK.

Test luồng eoffice:
1. Vào "Duyệt NCC > Duyệt" → 2 dropdown filter hiện đầy đủ
2. Chọn 1 quy trình V2 từ dropdown → list filter chỉ phiếu pin quy trình đó
3. Vào "Duyệt NCC > Danh sách" → 2 dropdown vẫn show, filter cũng work
2026-05-08 15:37:03 +07:00