Commit Graph

92 Commits

Author SHA1 Message Date
f45090b654 [CLAUDE] Domain+App+Infra+Api+FE-Admin+FE-User: S36 Plan G-O2 Phòng họp Mig 36 + BE CRUD + FE 2 app
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m55s
Phase 10.2 G-O2 Phòng họp BookingCalendar — Mig 36 schema + BE CQRS + FE 2 app
mirror cookie-cutter G-H2 HrmConfig pattern. Standalone không depend workflow.

BE schema (Mig 36 — em main solo Step 4):
- 4 Domain new: MeetingRoom catalog + MeetingBooking header + MeetingBookingAttendee
  join table N-to-N (NOT JSON per Investigator verdict) + Enums (MeetingBookingStatus
  3-state: Confirmed/Cancelled/Completed)
- 3 EF Config: UNIQUE Code + composite index (RoomId, StartAt) range query + UNIQUE
  composite (BookingId, UserId) join
- FK strategy: Room→Restrict (preserve history) + Booking→Cascade attendees +
  User→Restrict (denorm FullName+Email tránh cascade wipe)
- Mig 36 3-file rule + ApplicationDbContextModelSnapshot updated + apply Dev+Design DB

BE CQRS (~584 LOC — Implementer Case 2):
- MeetingFeatures.cs 479 LOC 9 handler: 4 Room CRUD + 5 Booking (List + GetById +
  Create + Update + Cancel)
- SERIALIZABLE transaction overlap check via EXISTS query — throw 409 Conflict
  "Phòng đã được đặt trong khoảng thời gian này"
- MeetingRoomsController 49 LOC + MeetingBookingsController 56 LOC — class-level
  [Authorize] + Roles="Admin" for write
- Application.csproj +Microsoft.EntityFrameworkCore.Relational package (em main fix
  IsolationLevel overload — Implementer gotcha #53 4th truncation diagnose mid-task)
- MenuKeys.cs +4 const (Off_PhongHop sub-group + View/Manage/Book leaf)
- DbInitializer +SeedMeetingRoomsAsync 4 sample (PH-A Phòng họp lớn cap=20 + PH-B
  cap=8 + PHG-501 Giám đốc cap=6 + ONL-1 Online Zoom cap=50) — NOT gated DemoSeed
  per gotcha #51 INFRASTRUCTURE seed

FE 2 app (~1770 LOC × 2 — Implementer Case 2):
- types/meeting.ts × 2 SHA256 IDENTICAL (ce0ad9c6d017cde2) — DTO interface mirror
- MeetingCalendarPage.tsx × 2 SHA256 IDENTICAL (d6d160ae1e4f2285) ~530 LOC — custom
  HTML 7-day grid 8h-20h slot, NO FullCalendar dep (~80 KB bundle saved per
  Investigator verdict alternative)
- MeetingRoomsPage.tsx × 2 SHA256 IDENTICAL (ba35a7ef379a5e9c) ~270 LOC — admin
  catalog CRUD table + Dialog
- 4-place mirror Pattern 16-bis 7× cumulative: types + page + App.tsx route +
  menuKeys + Layout staticMap 3 entry (gotcha #50 silent sidebar drop prevention)

Verify:
- dotnet build SolutionErp.slnx PASS 0 error 2 pre-existing DocxRenderer warning
- dotnet test 130/130 PASS baseline preserve (58 Domain + 72 Infra)
- npm build × 2 app PASS 0 TS error (fe-admin 16.91s bundle 1490 KB / fe-user 8.56s
  bundle 1404 KB, +23 KB gzip both)

Pattern reinforced cumulative S36:
- Pattern 12-bis cross-module mirror 10× (PE → Contract V2 → Hrm → Office)
- Pattern 16-bis 4-place mirror cross-app 7×
- Smart Friend Implementer truncation gotcha #53 4th — mitigation tight brief WORK
  (FE 2 app no truncation, BE truncate diagnose mid only)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 15:06:12 +07:00
909655c40d [CLAUDE] App+Api: S35 Plan G-H2 Task 3 — HrmConfig BE CRUD 16 endpoint cookie-cutter
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m43s
Pattern 12-bis cumulative 3× (S29 Plan B + Chunk C + S35 G-H2). Mega scaffold mirror
Master/Catalogs/CatalogsFeatures.cs 334 LOC pattern → SolutionErp.Application/Hrm/
HrmConfigFeatures.cs 439 LOC. 4 region cookie-cutter cho 4 HRM catalog (LeaveTypes +
Holidays + ShiftPatterns + OtPolicies). 16 endpoint qua HrmConfigsController.cs 137 LOC.

## Scope (Implementer Case 2 ~25K spawn)
- HrmConfigFeatures.cs 4 region × (DTO + List + Create cmd/validator/handler + Update cmd/validator/handler + Delete cmd/handler) = 4 DTO + 4 List query + 4 Create + 4 Update + 4 Delete handler
- HrmConfigsController.cs URL `/api/hrm-configs/{kind}` × {leave-types, holidays, shifts, ot-policies} × 4 verb = 16 endpoint
- Authorization: Class `[Authorize]` + write `[Authorize(Roles="Admin")]` 12 attribute (4 × 3 write verb) mirror Catalogs precedent
- Conflict check: 8 AnyAsync IsDeleted (4 Create + 4 Update với composite Year+Date Holiday exclude-self via `x.Id != req.Id`)
- Validator: 8 AbstractValidator + 44 RuleFor — MaxLength match EF source-of-truth (Smart Friend Implementer caught spec drift, aligned)
- HRM entities NO HasQueryFilter (4 vs 9 file Master have it) — explicit `.Where(!IsDeleted)` in List/Conflict-check queries

## Build + Test verify
- dotnet build PASS 0 err (2 pre-existing DocxRenderer warning unrelated)
- dotnet test 130/130 PASS preserve (BE-only, no FE/test touch)
- FE files NOT touched (defer Task 4 G-H2 FE Admin next chunk)
- EF Config + DbInitializer + IApplicationDbContext: S34 done, no re-touch

## Pre-commit Reviewer Smart Friend 8× clean cumulative
S22+S25+S29×2+S33×2+S35×2 (FE forms + BE CRUD)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 09:51:18 +07:00
07b3f3b284 [CLAUDE] Domain+App+Infra+FE-Admin+FE-User: S34 Plan 4 G-H2 Mig 35 schema foundation
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m30s
Phase 10.2 G-H2 Cấu hình HRM — 4 catalog lookup foundation deploy (BE CRUD +
FE forms DEFER S35 cho clean handoff).

Mig 35 `AddHrmConfigs` — 4 table mới:
- LeaveTypes (Code unique + Name + DaysPerYear decimal(5,2) + IsPaid +
  RequiresAttachment) — 5 sample seed (ANNUAL 12d + SICK 30d + MATERNITY 180d
  + COMPASSIONATE 3d + UNPAID 0d)
- Holidays (Year + Date UNIQUE composite + Name + IsRecurring + IsPaid) —
  10 sample VN 2026 (Tết Dương + 5 Tết Nguyên đán placeholder + Giỗ tổ +
  30/4 + 1/5 + 2/9 + Quốc khánh)
- ShiftPatterns (Code unique + Name + StartTime/EndTime TimeOnly + BreakMinutes +
  WorkDays comma string) — 3 sample (HC 8-17 T2-T6 + CA1 6-14 T2-T7 + CA2 14-22)
- OtPolicies (Code unique + 3 Multiplier decimal(4,2) + 3 MaxHours int) —
  1 sample STANDARD (1.5x/2.0x/3.0x weekday/weekend/holiday + 4h/40h/200h cap
  Luật Lao động VN 2019)

Files:
- Domain/Hrm/{LeaveType,Holiday,ShiftPattern,OtPolicy}.cs (4 entity AuditableEntity)
- Infrastructure/Persistence/Configurations/{LeaveType,Holiday,ShiftPattern,OtPolicy}Configuration.cs (4 EF Config UNIQUE indexes)
- IApplicationDbContext + ApplicationDbContext +4 DbSet
- Migrations/20260527075940_AddHrmConfigs.{cs,Designer.cs} + Snapshot updated (3-file rule)
- DbInitializer.SeedHrmConfigsAsync ~120 LOC seed sample (NOT gated DemoSeed per gotcha #51)
- MenuKeys.cs +HrmConfig sub-group + 4 leaf (LeaveTypes/Holidays/Shifts/OtPolicies) Order=2 dưới Hrm
- DbInitializer.SeedMenuTreeAsync +5 entry (sub-group + 4 leaf)
- fe-admin + fe-user menuKeys.ts +5 const mirror BE (Pattern 16-bis sync)

Verify:
- dotnet build PASS (2 warn DocxRenderer baseline, 0 error)
- dotnet test 130/130 PASS baseline preserve
- Mig 35 applied LocalDB SolutionErp_Dev — verified via dotnet ef database update
- 4 catalog table created + 5+10+3+1 = 19 sample row seed

Defer S35:
- Task 2 BE CQRS 4 catalog CRUD (16 endpoint) — Implementer Case 2 cookie-cutter
- Task 4 FE 2 app 4 catalog page (list/create/edit dialog) — Implementer Case 2

Cumulative S34 mig: 34 → 35 (+1). Tables 67 → 71 (+4). Menu keys 64 → 69 (+5).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 15:04:10 +07:00
e506cd8135 [CLAUDE] App+Api: S34 Plan 3 Item 3 BE 5 satellite CRUD scaffold (15 endpoint)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m36s
Phase 1.5 Item 3 BE — Implementer Case 2 cookie-cutter 5-entity scaffold
mirror parent EmployeeProfile CRUD pattern (Pattern 12-ter NEW saved).

New file: Application/Hrm/EmployeeSatelliteFeatures.cs (621 LOC):
- 5 region (#region {EntityName}) — WorkHistory/Education/FamilyRelation/Skill/Document
- Each region: Create/Update/Delete Command + Validator + Handler
- Verify parent EmployeeProfile.Exists pattern AnyAsync(!IsDeleted)
- Soft delete IsDeleted=true + DeletedAt=UtcNow + DeletedBy=currentUser
- Validator nullable enum IsInEnum().When(HasValue) pattern

Modified file: Api/Controllers/EmployeesController.cs (70 → 234 LOC, +15 endpoint):
- 5 region × 3 verb (POST/PUT/DELETE) = 15 satellite endpoint
- Path per satellite:
  - /api/employees/{id}/work-history
  - /api/employees/{id}/education
  - /api/employees/{id}/family-relations
  - /api/employees/{id}/skills
  - /api/employees/{id}/documents
- Per-action policy: Hrm_HoSo.Create/Update/Delete (override class-level Read)
- BadRequest guard: id != cmd.{EmployeeProfileId|Id} → "ID không khớp"

Document satellite metadata-only — file upload IFileStorage body wire defer S35.

Verify:
- dotnet build PASS (2 warn DocxRenderer baseline, 0 error)
- dotnet test 130/130 PASS (58 Domain + 72 Infra baseline preserve, no test add)
- Endpoints count: ~154 → 169 (+15)
- LOC delta: +785 BE (621 features + 164 controller)

Pattern 12-ter NEW (Implementer MEMORY): 5× satellite CRUD scaffold cookie-cutter
within-module (different from Pattern 12-bis cross-module mirror). Reusable
cho future N-satellite parent (vd Contract attachments, PE quotes, Budget details).

Defer S35:
- FE inline edit forms 5 satellite section (~1.5h em main solo)
- Test bundle satellite CRUD (~30 phút Implementer Case 3)
- IFileStorage Document body upload wire (multipart form-data)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:56:14 +07:00
61e9ce5b3b [CLAUDE] Domain+App+Api+Tests+FE-Admin+FE-User: S34 Plan 3 Phase 1.5 batch 4 item
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m48s
Phase 1.5 backlog G-H1 EmployeeProfile hardening batch (Items 6+2+1+4 of 6).

Item 6 — menuKeys FE drift sync × 2 app:
- fe-admin add: Catalogs + 4 Catalog leaves + Workflows + Budgets + Bg_List/Create/Pending (10 key)
- fe-user add: Budgets + Bg_List/Create/Pending + ApprovalWorkflowsV2 + 2 AwV2 leaf + MenuVisibility + Workflows (8 key)
- Cả 2 file giờ identical mirror BE MenuKeys.cs (28 key cumulative)

Item 2 — UpdateEmployeeProfileCommand bool→bool? safe partial update:
- 3 field IsCommunistParty/IsYouthUnion/IsTradeUnion → bool?
- Handler: HasValue check, null = giữ giá trị cũ (Reviewer minor #(b) S33 fixed)
- FE không bắt buộc gửi 3 field every PUT — tránh accidental reset

Item 1 — EmployeesController per-action policy (gotcha #44 mitigation):
- Class-level [Authorize(Policy = "Hrm_HoSo.Read")] — non-admin thiếu Read → 403
- POST [Authorize(Policy = "Hrm_HoSo.Create")]
- PUT  [Authorize(Policy = "Hrm_HoSo.Update")]
- DELETE [Authorize(Policy = "Hrm_HoSo.Delete")]

Item 4 — Test bundle Phase 1.5 (+10 [Fact], baseline 120 → 130/130 PASS):
- EmployeeCodeGeneratorTests (3 [Fact]) — atomic SERIALIZABLE NV/YYYY/NNNN
  + first call + sequential increment + year boundary preserve old year
- CreateEmployeeProfileCommandTests (4 [Fact]) — Create handler edge case
  + first profile + duplicate UserId Conflict + soft-deleted Conflict-restore
  + UserNotFound NotFoundException
- ListEmployeesQueryTests (3 [Fact]) — filter + paging logic
  + status filter + departmentId filter + search by EmployeeCode partial

Implementer Case 3 test gen caught spec mismatch (allow new after soft-delete
vs throws Conflict-restore) — chose CODE source of truth + renamed test
documenting discriminator message branch. Em main verify behavior correct
(admin UX khôi phục thay vì tạo mới — explicit flow defer Phase 1.5+).

Verify:
- dotnet build PASS (2 warn DocxRenderer baseline, 0 error)
- dotnet test 130/130 PASS (58 Domain + 72 Infra = +10)
- 4 endpoint /api/employees policy wired (gotcha #44 active mitigation)
- 4 MEMORY agent updated post-spawn (CICD Run #238 + Implementer test bundle)

Deferred Phase 1.5 next batch:
- Item 3 Satellite CRUD endpoints (WorkHistory/Education/FamilyRelation/Skill/
  Document) + FE inline edit forms — heavy ~2-3h
- Item 5 UAT smoke non-admin role verify silent 403 catch — defer post-deploy

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:57:08 +07:00
ea440da990 [CLAUDE] Domain+App+Api+Infra+FE-Admin+FE-User: S34 Plan 2 G-O1 Danh bạ nội bộ
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m46s
Phase 10.2 Văn phòng số — Internal Directory (1 endpoint reuse Users +
EmployeeProfiles + Departments, FE card grid avatar/dept/email/phone/Ext).

BE Task 1+2 (em main solo):
- Application/Office/DirectoryFeatures.cs — GetDirectoryQuery + DirectoryItemDto
  12 field LEFT JOIN Users.IsActive + Departments + EmployeeProfiles
- Api/Controllers/DirectoryController.cs — GET /api/directory?search=&departmentId=
  class-level [Authorize] (mọi authenticated NV tra cứu danh bạ nội bộ)
- MenuKeys.cs +Off+OffDanhBa const + All[] update
- DbInitializer.SeedMenuTreeAsync Off Order=29 + OffDanhBa Order=1 dưới Off

FE Task 3 (Implementer Case 2 Pattern 16-bis 4-place mirror cross-app — 5×):
- types/directory.ts SHA256 7349d9f64e78 × 2 app IDENTICAL
- pages/office/InternalDirectoryPage.tsx SHA256 2aa7e0eed2c8 × 2 app IDENTICAL
  Card grid responsive 1/2/3/4 col + filter dept dropdown + search input
  Avatar 14×14 initials gradient PALETTE 6 màu (Pattern 14 Tailwind JIT)
  EmployeeCode badge + Department emerald badge + email mailto + phone tel
  Internal phone Ext: amber badge + empty/loading state Vietnamese 100%
- App.tsx route /directory × 2 app
- lib/menuKeys.ts Off+OffDanhBa const × 2 app
- components/Layout.tsx resolvePath staticMap Off_DanhBa:/directory × 2 app
  (gotcha #50 — 5 places mirror crossapp DON'T MISS)

Verify:
- dotnet build PASS (2 warn DocxRenderer existing, 0 error)
- dotnet test 120/120 PASS (58 Domain + 62 Infra baseline preserve)
- npm build × 2 app PASS 0 TS err (fe-admin 1436KB / fe-user 1350KB)

Implementer MEMORY Pattern 16-bis reinforced 5× cumulative (S29 Plan CA HF1 +
S29 Plan B Chunk D + S33 Plan B G-H1 Task 5 + S34 Plan G-O1 Task 3).

Endpoint smoke pending CICD post-deploy Stage 4 (Run #XXX expected ~3m30s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 13:39:10 +07:00
0e191deea5 [CLAUDE] Domain+App+Api+Infra: Plan B G-H1 Task 4+6 — Hrm CQRS 5 endpoint + Permission menu
Phase 10.1 G-H1 Phase 2 — Task 4 (BE CQRS + REST endpoint) + Task 6
(Permission menu seed) cumulative. Foundation BE side complete cho Hồ sơ
Nhân sự module — FE Task 5 + Reviewer Task 7 + CICD verify next.

## Task 4 — BE CQRS + Controller (3 file new, Implementer Case 2)

src/Backend/SolutionErp.Application/Hrm/EmployeeFeatures.cs (~450 LOC):
- CreateEmployeeProfileCommand + Validator + Handler
  - Verify User.Id exists qua UserManager.FindByIdAsync
  - UNIQUE 1-1 check: throw ConflictException nếu User đã có EmployeeProfile
  - EmployeeCode auto-gen qua IEmployeeCodeGenerator (NV/{YYYY}/{Seq:D4})
  - 50+ field assignment từ Command record
- UpdateEmployeeProfileCommand + Validator + Handler (mutable fields, UserId+EmployeeCode immutable)
- DeleteEmployeeProfileCommand + Handler (soft delete IsDeleted=true)
- GetEmployeeProfileQuery + Handler (Include 5 satellite collection)
- ListEmployeesQuery + Handler (paged + JOIN Users+Departments, filter Status/DepartmentId/Search)

src/Backend/SolutionErp.Application/Hrm/Dtos/EmployeeDtos.cs (~110 LOC):
- EmployeeProfileListItemDto (Id, EmployeeCode, UserId, FullName/Email/Department JOIN, Status, Phone, HireDate)
- EmployeeProfileDetailDto (full 50+ field + 5 satellite collection)
- 5 satellite DTO: EmployeeWorkHistoryDto + EmployeeEducationDto +
  EmployeeFamilyRelationDto + EmployeeSkillDto + EmployeeDocumentDto

src/Backend/SolutionErp.Api/Controllers/EmployeesController.cs (~70 LOC):
- 5 REST endpoint: GET list / GET detail / POST / PUT / DELETE
- Class-level [Authorize] only Phase 1 (per-action policy Hrm_HoSo_View/Create/
  Edit/Delete defer Phase 1.5 per Reviewer recommend)
- Route prefix /api/employees

## Task 6 — Permission menu Hrm_HoSo* (em main solo, 2 file mod)

src/Backend/SolutionErp.Domain/Identity/MenuKeys.cs (+10 LOC):
- +2 const: Hrm root group + HrmHoSo leaf
- Update All[] array → SeedAdminPermissionsAsync auto-grant Admin role CRUD

src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs (+4 LOC):
- SeedMenuTreeAsync +2 entry:
  - (Hrm, "Nhân sự", null, 28, "UserCircle") — root group
  - (HrmHoSo, "Hồ sơ Nhân sự", Hrm, 1, "ContactRound") — leaf
- Order=28 between Budgets=27 và Contracts=30+ (no collision)
- INFRASTRUCTURE menu seed (NOT gated DemoSeed:Disabled — em main verified
  outside gate block per gotcha #51 lesson, mirror Plan B Task 3b
  SeedDemoEmployeeProfilesAsync placement)

## Reviewer ae752c0 verdict: PASS Smart Friend 6× clean

- 0 critical, 0 major, 3 minor defer Phase 1.5 (per-action policy + bool
  partial update + IDateTimeProvider injection)
- Cumulative Smart Friend track: S22 #44 + S25 #48 + S29 Plan CA ≥12 + S29
  Plan B ApplicableType + S33 Plan C BW clean + S33 Plan B Phase 2 clean
- gotcha #51 INFRASTRUCTURE seed gate compliance: ✓
- gotcha #50 Layout staticMap mirror: ✓ (Task 5 commit next)

## Verify
- dotnet build: 0 err 0 warn (1.72s)
- dotnet test: 120/120 PASS baseline preserved
- Endpoint claim verified grep 0 mock marker, 5 mediator.Send real

Pattern 12-bis cross-module entity cookie-cutter mirror PE→Hrm reinforced 4×
cumulative (S29 Plan B Contract Chunk C + S33 Task 3 entity scaffold + Task
3b seed + Task 4 CQRS).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 20:26:44 +07:00
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
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
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
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
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
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
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
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
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
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
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
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
9e63e2da10 [CLAUDE] PE: V2-aware Inbox/List + 2 dropdown filter quy trình + trạng thái
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m12s
User báo: "Phiếu chưa thấy lên trong danh sách duyệt — chắc do chưa ăn
vào flow. Tách thành 2 cái dropdown là list quy trình duyệt và list
trạng thái. Debug trước, phân quyền rút gọn lại sau."

BE — V2-aware permission + filter (Application/PurchaseEvaluations/
PurchaseEvaluationFeatures.cs):
- ListPurchaseEvaluationsQuery +ApprovalWorkflowId? Guid? param +
  IDOR loose: phiếu pin V2 → mọi authenticated user thấy được (UAT)
- GetMyPurchaseEvaluationInbox V2-aware: ResolveV2InboxIdsAsync helper
  precompute Set<Guid> phiếu Phase=ChoDuyet pin V2 + actor ∈ Cấp hiện
  tại approvers (CurrentWorkflowStepIndex + CurrentApprovalLevelOrder
  match Step.Order + Level.Order). Inbox where = eligiblePhases.Contains
  || v2InboxIds.Contains. eligiblePhases admin +ChoDuyet.
- GetById Detail loose: V2 pin → cho non-Drafter xem (skip
  eligiblePhases check).

API Controller:
- PurchaseEvaluationsController.List +approvalWorkflowId query param

FE — 2 dropdown filter (cả 2 app mirror):
- PurchaseEvaluationsListPage: +URL param `awId` filter quy trình
- useQuery `approval-workflows-v2-filter` load list V2 active+history
  theo applicableType=typeFilter (chỉ enabled khi có type)
- Render Select riêng "Tất cả quy trình duyệt" (chỉ show !pendingMe vì
  Inbox dùng API endpoint khác) + Select "Tất cả trạng thái" giữ
- Display option: "QT-DN-V2-001 v01 — Tên quy trình"

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

Test luồng eoffice:
1. Drafter trình phiếu V2 → Phase=ChoDuyet
2. Login NV X (approver Cấp 1) vào "Duyệt NCC > Duyệt"
   (?pendingMe=1) → phiếu hiện trong list
3. Login NV Y (không phải approver) → list rỗng (đúng spec)
4. Vào "Duyệt NCC > Danh sách" (không pendingMe) → 2 dropdown:
   - Quy trình duyệt: filter theo workflow specific
   - Trạng thái: filter theo Phase
2026-05-08 15:18:22 +07:00
d814429cee [CLAUDE] PE Workflow V2: disable nút Duyệt nếu actor không trong cấp hiện tại
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m14s
User feedback: "Nếu không đúng bước duyệt thì nút duyệt cho Disable luôn cũng đc."

BE — DTO + Handler populate "Bước/Cấp đang chờ duyệt":
- Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs:
  +PurchaseEvaluationApprovalLevelApproverDto { UserId, FullName, Email }
  +PurchaseEvaluationCurrentApprovalDto { StepIndex, StepName,
    StepDepartmentId/Name, LevelOrder, LevelName, Approvers[] }
  PurchaseEvaluationDetailBundleDto +CurrentApproval? optional field
- Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs handler
  GetById: khi pin V2 + Phase=ChoDuyet → load AW.Steps.Levels Include
  3-level + group by Order = Cấp + resolve user names → populate
  CurrentApproval. Null khi V1 legacy hoặc không phải ChoDuyet.

FE — types + PeWorkflowPanel (cả 2 app mirror):
- types/purchaseEvaluation.ts: +PeCurrentApproval + PeCurrentApprovalLevelApprover
  + PeDetail.currentApproval optional
- PeWorkflowPanel:
  * Banner V2 hiển thị "Đang chờ Bước N (TênBước · Phòng X) — Cấp K"
    + list NV được duyệt + status emerald (đến lượt) / amber (không phải lượt)
  * useAuth() để check currentUser.id ∈ approvers + Admin bypass
  * Button "Duyệt forward" disabled khi V2 pin + actor không khớp.
    Title tooltip "Cấp K chỉ {NV X / NV Y} mới duyệt được."
  * Button "Trả lại" + "Từ chối" vẫn enabled (BE không gating 2 hành
    động này theo Cấp — Approver có thể reject bất cứ lúc nào).
  * Send-back logic update: target = DangSoanThao OR TraLai (V2 dùng TraLai)
- Admin role bypass mọi check.

Verify: 81 test pass · npm build × 2 OK · BE 0 error.

Test thử:
1. NV X (approver Cấp 1 V2) login → banner emerald "Đến lượt bạn duyệt"
   + nút "✓ Duyệt → ChoDuyet" enabled
2. NV Y (không phải approver) login → banner amber "Không phải lượt
   bạn — chỉ NV X mới duyệt được" + nút Duyệt grey disabled, hover tooltip
3. Admin login → bypass, button enabled
2026-05-08 15:03:29 +07:00
b41484b702 [CLAUDE] PE Workflow: wire Service V2 (Mig 24) — fix bug duyệt phiếu pin schema mới
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m14s
User báo bug eoffice: phiếu tạo mới không duyệt được + không bắt đc quy
trình mới. Root cause: Mig 23 pin ApprovalWorkflowId vào entity nhưng
Service vẫn đọc WorkflowDefinitionId legacy → match approver theo schema
cũ (Dept+PositionLevel/Role/User) thay vì ApproverUserId V2.

BE Domain — Migration 24 `AddCurrentApprovalLevelOrderToPe`:
- PurchaseEvaluation +CurrentApprovalLevelOrder int? (track Cấp 1/2/3
  đang chờ duyệt trong Step hiện tại khi pin V2). Null khi terminal/V1.
- RejectedAtStepIndex giữ deprecated DB column cho data cũ.

BE Service PE — branch theo schema pin:
- V2 (`ApprovalWorkflowId` set): ApproveV2Async() — load
  ApprovalWorkflows.Steps.Levels Include 3-level. Group Levels by Order
  = Cấp (OR-of-N approvers). Match `actor.Id ∈ levelGroup.ApproverUserId`
  (KHÔNG match Dept+Level/Role/User như V1). Advance:
    Còn cấp tiếp trong Step → levelOrder++
    Hết cấp → idx++, levelOrder=1
    Hết Step → DaDuyet
- V1 legacy (chỉ `WorkflowDefinitionId` set): ApproveV1LegacyAsync() —
  giữ nguyên logic Mig 21 (Dept+PositionLevel match)
- Drafter trình từ Nháp/Trả lại: init CurrentWorkflowStepIndex=0 +
  CurrentApprovalLevelOrder=1 (chỉ khi V2 pin)
- Reject (Trả lại): clear CurrentApprovalLevelOrder=null
- Reject (Từ chối): clear all tracking

BE Synthetic Policy V2:
- `PurchaseEvaluationPolicyRegistry.ForV2Schema()` — simple state machine
  policy (DangSoanThao/TraLai → ChoDuyet/TuChoi; ChoDuyet → ChoDuyet/
  TraLai/TuChoi). Roles="*" cho ChoDuyet branch — Service tự enforce
  ApproverUserId, Policy chỉ expose 3 nút FE.
- GetPurchaseEvaluationByIdQuery handler: ưu tiên ForV2Schema() khi pin
  V2 (FE đọc workflow.nextPhases để show button).

Verify: 81 test pass · BE 0 error · Mig 24 applied cả 2 LocalDB.

Test thử (Drafter eoffice):
1. Designer V2 tạo quy trình QT-DN-V2-001: Bước 1 (Phòng A), Cấp 1 (NV X)
2. Workspace tạo phiếu mới, Select QT-DN-V2-001 → Lưu phiếu + Gửi duyệt
3. Phiếu Phase=ChoDuyet, idx=0, levelOrder=1. NV X login → thấy phiếu
   trong Inbox + duyệt được. Sau approve → idx++, levelOrder reset 1.
4. Cấu hình level mismatch: NV Y khác → thấy ForbiddenException rõ tên.

Logic Contract V2 chưa wire (chỉ PE), defer Session sau khi user UAT PE OK.
2026-05-08 14:54:51 +07:00
0a40c65421 [CLAUDE] PurchaseEvaluation: User chọn quy trình duyệt V2 lúc tạo phiếu (Mig 23)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m11s
User feedback: thay field "Loại quy trình (theo menu — khóa)" disabled
→ Select dropdown cho User pick quy trình ApprovalWorkflowsV2 (Mig 22)
ngay từ workspace tạo mới. Hiển thị "Mã + Tên + Version".

BE Domain:
- PurchaseEvaluation +ApprovalWorkflowId Guid? (nullable, FK Restrict)
- EF Configuration: Index + FK Restrict to ApprovalWorkflows
- Migration 23 `AddApprovalWorkflowIdToPurchaseEvaluation` (1 ALTER +
  1 IX + 1 FK), applied cả _Design + _Dev LocalDB
- Field WorkflowDefinitionId (Mig 21 legacy) giữ song song để Service
  PE chạy logic cũ tới khi Session sau wire qua schema mới

BE Application:
- CreatePurchaseEvaluationCommand +ApprovalWorkflowId? Guid? optional
  param (default null)
- Validate: nếu set, phải tồn tại + ApplicableType khớp PE.Type
  (DuyetNcc=1 → ApprovalWorkflowApplicableType.DuyetNcc, etc)
- Handler set entity.ApprovalWorkflowId từ request
- UpdatePurchaseEvaluationDraftCommand mirror — cho User đổi quy trình
  khi sửa Nháp/Trả lại (validate same)
- PurchaseEvaluationDetailBundleDto +ApprovalWorkflowId/Code/Name/Version
- GetPurchaseEvaluationByIdQuery handler load workflow info join
- Update Phase guard: cho sửa cả DangSoanThao + TraLai (Trả lại =
  editable per Session 17 spec)

FE (cả 2 app mirror):
- types/purchaseEvaluation.ts: PeDetail +approvalWorkflowId/Code/Name/Version
- PeWorkspaceCreateView.tsx:
  - Replace field disabled "Loại quy trình" → Select bắt buộc
  - useQuery `/api/approval-workflows-v2?applicableType=N` filter theo
    defaultType (1=DuyetNcc / 2=DuyetNccPhuongAn)
  - Display option: "QT-DN-V2-001 v01 — Quy trình Duyệt NCC (đang áp dụng)"
  - List cả version active + archived (UAT cần test compare)
  - Empty state hint amber "Chưa có quy trình, vào /system/approval-workflows-v2"
  - canSubmit require approvalWorkflowId set
  - POST payload include approvalWorkflowId

Verify: dotnet build OK · 81 test pass · npm build × 2 OK · Mig 23 applied
cả 2 LocalDB.

Logic Service PE chưa wire qua ApprovalWorkflowId — vẫn pin
WorkflowDefinitionId Mig 21 legacy chạy. Session sau wire Service iterate
ApprovalWorkflowSteps + match approver theo schema V2 + drop legacy.
2026-05-08 14:34:54 +07:00
ff21120c8c [CLAUDE] Workflow: State machine 5 trạng thái — Trả lại = Phase riêng
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m17s
Session 17 spec: chốt 5 trạng thái phiếu PE/HĐ/Budget theo state diagram:
  Nháp ─trình──► Đã gửi duyệt ─approve cấp cuối──► Đã duyệt (terminal)
                              ├─ Trả lại ────────► Trả lại
                              └─ Từ chối ────────► Từ chối (terminal)
  Trả lại ──Drafter sửa+gửi lại──► Đã gửi duyệt (chạy LẠI từ đầu)

Khác Mig 21 (Session 16): bỏ smart-reject jump-back. Trả lại = Phase
RIÊNG (TraLai=98), không revert về DangSoanThao + không jump-back step.
Drafter từ TraLai gửi lại như case Nháp — workflow chạy lại từ Cấp 1
Bước 1 (Option A diagram chốt với user).

BE Domain:
- ContractPhase + TraLai = 98
- BudgetPhase + TraLai = 98
- PurchaseEvaluationPhase: TraLai=98 đổi từ [LEGACY deprecated] thành
  primary state. Comment update enum docs cho cả 3.

BE Policy (PE/HĐ/Budget):
- Reject transitions trỏ về TraLai (thay DangSoanThao)
- Mirror entry transitions: TraLai → next phase (cho Drafter resubmit)
- ActivePhases thêm TraLai
- FromDefinition mirror: TraLai → step.Phase + reject → TraLai
- DefaultSla cho TraLai = same as DangSoanThao

BE Service (PE + Contract):
- Reject branch: target=TuChoi giữ; else set Phase=TraLai, clear
  CurrentWorkflowStepIndex=null
- Bỏ ResumeAfterReject branch + RejectedAtStepIndex/RejectedFromPhase
  assignment (DB column giữ deprecated cho data cũ)
- Drafter trình branch: từ DangSoanThao HOẶC TraLai → ChoDuyet, init
  CurrentWorkflowStepIndex=0 (cùng entry point, chạy lại từ đầu)
- Notification: TraLai when fromPhase=ChoDuyet → "bị trả lại"
- Budget Handler: simplify reject → TraLai, bỏ smart-reject + isResuming

BE Tests update:
- WorkflowPolicyTests: Standard_RejectFromCCM → TraLai (rename + assert)
  + Standard_TraLai_To_DangGopY_Allowed_For_Drafter (new)
- PurchaseEvaluationPolicyTests: BothPolicies_RejectFromCCM → TraLai
  + BothPolicies_TraLai_To_ChoPurchasing_AllowedForDrafter (new theory)
- BudgetPolicyTests: Default_CostControl_ChoCCM_To_TraLai (rename)
  + ActivePhases All6States (was All5) + NextPhasesFrom_TraLai (new)
  + NextPhasesFrom_ChoCEO_Includes_DaDuyet_And_TraLai (rename)
- 77 → 81 test pass (+4 tests TraLai entry point)

FE rename "Bản nháp" → "Nháp" (cả 2 app + types):
- types/purchaseEvaluation.ts: PurchaseEvaluationPhaseLabel 1=Nháp,
  10=Đã gửi duyệt. PeDisplayStatus.BanNhap → Nhap (key + value).
  PhaseLabel/Color cho TraLai update active.
- types/contracts.ts: +ChoDuyet=10, +TraLai=98 const + label/color.
  Phase 2 'Đang soạn thảo' → 'Nháp'.
- types/budget.ts: +TraLai=98 const + label/color. Phase 1 → 'Nháp'.
- PeListPanel + PurchaseEvaluationsListPage filter dropdown: Nhap +
  TraLai map đúng phase value.

BE label maps update consistent:
- ContractExcelExporter PhaseLabel: DangSoanThao → "Nháp" + add ChoDuyet/
  TraLai entries.
- PeWorkflowAdminFeatures + WorkflowAdminFeatures PhaseLabels: same.

Verify: dotnet test 81 pass · npm build × 2 app pass · BE 0 error.

Field RejectedAtStepIndex/RejectedFromPhase giữ DB column (nullable,
không set value mới). Cleanup migration sau.
2026-05-08 14:12:38 +07:00
f3bea3c616 [CLAUDE] Workflow: Max 3 cấp/bước + N NV/cấp + sequential gating (V2 UAT iter 2)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m16s
User feedback: "tối đa 3 cấp (không có cấp 4)" — không phải bắt buộc 3.
Mỗi cấp = N NV (add bao nhiêu cũng được). Quy trình chạy theo số cấp
thật sự cấu hình (1/2/3). C2 chưa thao tác được khi C1 chưa có NV.

Convention DB: nhiều `ApprovalWorkflowLevel` row cùng Order = same Cấp,
mỗi row = 1 NV. Service iterate group by Order; trong cùng cấp =
OR-of-N (1 NV duyệt → cấp pass).

BE — Application/ApprovalWorkflowsV2/ApprovalWorkflowV2AdminFeatures.cs:
- Validator strict:
  - Order ∈ {1, 2, 3} (`MaxLevelsPerStep`)
  - Sequential gating: HaveSequentialOrders → 1 / 1+2 / 1+2+3, KHÔNG
    cho 2 (thiếu 1) hoặc 1+3 (thiếu 2)
  - HaveNoDuplicateApproverInSameLevel: 1 NV không thêm 2 lần cùng cấp
- Schema KHÔNG đổi (giữ ApprovalWorkflowLevel.ApproverUserId 1-1).
- Handler không đổi — auto handle multiple rows cùng Order.

FE — ApprovalWorkflowsV2Page.tsx rewrite Levels section:
- Type EditStep.levels → levelEntries: { order: 1|2|3; approverUserId }[]
  flat list (group by order trong render).
- 3 SECTION CỐ ĐỊNH C1/C2/C3 trong Designer:
  - Mỗi section: header "Cấp N" + count NV + nút "+ Thêm NV"
  - List rows mỗi NV với Select dropdown filtered theo Phòng + Trash
  - C2 disabled (opacity-60) khi C1 empty. C3 disabled khi C2 empty.
  - Tooltip "+ Thêm NV": "Cấp k-1 phải có ≥1 NV trước"
- Add NV: dropdown chỉ NV thuộc Phòng + chưa được thêm cùng cấp
  (no duplicate same level).
- Xóa NV: chặn xóa NV cuối Cấp k nếu Cấp k+1 còn entries (toast error
  "Hãy xóa hết NV ở Cấp k+1 trước khi rỗng Cấp k").
- Đổi Phòng → clear toàn bộ levelEntries (NV cũ không thuộc Phòng mới).
- DefinitionCard read-only: group s.levels by Order → render mỗi cấp
  là 1 row với badge "Cấp N" + list NV bên dưới.
- Save validate: Phòng required + Cấp 1 ≥1 NV + sequential + NV thuộc
  đúng Phòng (defensive double-check).

Verify: dotnet build BE OK · 77 test pass · npm build fe-admin OK.

Logic Service PE/Contract chưa wire schema mới — vẫn pin Mig 21 legacy.
2026-05-08 13:20:51 +07:00
f6047d5218 [CLAUDE] Workflow: App CQRS + API ApprovalWorkflowsV2 (Chunk B)
3 handler MediatR + Validator + Controller cho schema mới Mig 22.

Files:
- Application/ApprovalWorkflowsV2/ApprovalWorkflowV2AdminFeatures.cs
  - GetAwAdminOverviewQuery (filter optional ApplicableType)
  - CreateAwDefinitionCommand + Validator (auto-increment Version
    theo Code, deactivate active version cùng ApplicableType)
  - DeleteAwDefinitionCommand (UAT helper — chưa pin nên unconditional)
  - DTO: AwDefinition/AwStep/AwLevel + TypeSummary
- Application/Common/Interfaces/IApplicationDbContext.cs (3 DbSet)
- Api/Controllers/ApprovalWorkflowsV2Controller.cs
  - Route /api/approval-workflows-v2
  - GET ?applicableType=N | POST | DELETE/{id}
  - Reuse policy Workflows.Read/Workflows.Create

Verify: build OK 0 error, IApplicationDbContext expose 3 DbSet mới.

Next: Chunk C — FE Designer page + route + Layout resolver.
2026-05-08 12:42:03 +07:00
dbb0089e28 [CLAUDE] Drastic refactor: flat workflow Phòng × Cấp + Migration 21 (Chunk A)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m18s
User chốt drastic refactor — bỏ phase enum hoàn toàn, dùng ChoDuyet=10
đơn nhất + currentStepIndex tracking. Workflow flat list (Phòng × Cấp ×
Approvers). Mỗi PE/HĐ pin WorkflowDefinitionId chạy hết quy trình đó.

Schema (Migration 21 `RefactorWorkflowToFlatModel`):
- Phase enum +ChoDuyet=10 (PE + Contract). Legacy 2-9 + 98 deprecated.
- WorkflowStep + DepartmentId Guid? (FK Restrict) + PositionLevel int?
  (PE + Contract — mirror).
- PE/Contract + CurrentWorkflowStepIndex int? + RejectedAtStepIndex int?
- DROP table PurchaseEvaluationWorkflowStepInnerSteps (Mig 18)
- DROP table WorkflowStepInnerSteps (Mig 20)
- DROP column ContractDeptApproval.InnerStepId (Mig 20)
- DROP column PEDeptApproval.InnerStepId (Mig 18)
- DROP filtered indexes (Mig 19/20) + restore simple unique
  (TargetId, Phase, Dept, Stage) non-filtered

Service rewrite (PE + Contract WorkflowService.TransitionAsync):
- Phase transitions: DangSoanThao → ChoDuyet (Drafter trình, init idx=0)
- ChoDuyet → ChoDuyet (advance idx per approve)
- ChoDuyet → DaDuyet/DaPhatHanh (idx >= steps.Count → terminal)
- ChoDuyet → DangSoanThao (Trả lại — save RejectedAtStepIndex)
- ChoDuyet → TuChoi (Từ chối — khoá vĩnh viễn)
- DangSoanThao + RejectedAtStepIndex → ChoDuyet jump-back to saved idx
- Approver match: actor.Dept == step.Dept AND actor.PositionLevel >=
  step.PositionLevel (OR-of-many cùng cấp/dept = pass) OR
  Approvers.Any(Kind=User AND id match) OR
  Approvers.Any(Kind=Role AND actorRoles contains)
- Admin role bypass policy. Last step done → gen mã HĐ (Contract only)

App CQRS:
- WorkflowStepDto + WorkflowStepInput drop InnerStep, add DepartmentId
  + PositionLevel fields. PE + Contract mirror.

Tests rewrite:
- DROP PeNStageApprovalTests.cs (6 test) + ContractNStageApprovalTests.cs
  (6 test) + PeTwoStageApprovalTests.cs (7 test) — legacy N-stage/2-stage
  no longer applicable
- UPDATE PeWorkflowAdminTests signature to new flat input
- 96 → 77 test pass (drop 19 legacy)

Reference Domain entities removed:
- WorkflowStepInnerStep (Contract)
- PurchaseEvaluationWorkflowStepInnerStep (PE)
- DTOs WorkflowStepInnerStepDto / CreateWorkflowStepInnerStepInput per module

Memory `feedback_drastic_refactor_scope.md` validated: drastic refactor
done in dedicated session với context fresh, scope ~5h actual (planned ~8-10h
with 2x buffer).

Verify:
- dotnet build SolutionErp.slnx 0 error
- dotnet ef database update Mig 21 LocalDB applied OK
- dotnet test 77 pass (54 Domain + 23 Infra)
- 3-file rule: Migration .cs + Designer.cs + Snapshot updated

Pending Chunk B: FE Designer flat UI (PeWorkflowsPage + WorkflowsPage).
Pending Chunk C: FE PeWorkflowPanel + workflow timeline display.
Pending Chunk D: Docs + Skill + Memory + session log.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:04:51 +07:00
04cf2a0385 [CLAUDE] App: Contract workflow inner steps DTO + designer mirror PE (Chunk B)
Some checks failed
Deploy SOLUTION_ERP / build-deploy (push) Has been cancelled
DTO + Validator + Handler mở rộng cho N-stage admin designer Contract
(Mig 20). Mirror PE Mig 18 pattern (PeWorkflowAdminFeatures Chunk B).

WorkflowAdminFeatures:
- WorkflowStepInnerStepDto record (Id, Order, DeptId, DeptName,
  PositionLevel, Name, SlaDays, IsRequired)
- WorkflowStepDto extend +InnerSteps List
- GetWorkflowAdminOverviewQueryHandler — Include InnerSteps OrderBy +
  resolve DeptNames cho display
- CreateWorkflowStepInnerStepInput record
- CreateWorkflowStepInput extend +InnerSteps (nullable, default null —
  backward compat existing test code positional new())
- Validator child rules cho InnerSteps (Order ≥1, DeptId not empty,
  PositionLevel 1-3, SlaDays ≥0)
- CreateWorkflowDefinitionCommandHandler — convert InnerSteps khi build
  entity (atomic batch insert qua nav collection)

Verify: dotnet build 0 error, 89 test pass (no regression).

Pending Chunk C: ContractWorkflowService.TransitionAsync N-stage logic
+ legacy 2-stage fallback mirror PE pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:02:10 +07:00
0e56bd0a67 [CLAUDE] App: PE workflow inner steps DTO + UpdateUserPositionLevel CQRS (Chunk B)
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m42s
DTO + Validator + Handler mở rộng cho N-stage admin designer (Mig 18):

PeWorkflowAdminFeatures:
- PeWorkflowStepInnerStepDto record (Order/Dept/PositionLevel/Name/SlaDays/IsRequired)
- PeWorkflowStepDto extend +InnerSteps List
- GetPeWorkflowAdminOverviewQuery: Include InnerSteps OrderBy Order +
  resolve DeptNames cho display
- CreatePeWorkflowStepInnerStepInput record
- CreatePeWorkflowStepInput extend +InnerSteps (nullable, default null —
  backward compat existing test PeWorkflowAdminTests positional new())
- Validator child rules cho InnerSteps (Order >=1, DeptId not empty,
  PositionLevel 1-3, SlaDays >=0)
- Handler convert InnerSteps khi build entity (atomic batch insert)

UserFeatures:
- UserDto +PositionLevel int? field
- ListUsers + GetUser handlers map (int?)u.PositionLevel
- SetUserPositionLevelCommand + Validator + Handler mirror
  SetUserBypassReviewCommand pattern (admin set qua UserManager UI)

Verify:
- dotnet build SolutionErp.slnx 0 error
- dotnet test 83 pass (54+29) — no regression
- Backward compat: PeWorkflowAdminTests existing 6 test pass (named-arg
  positional record vẫn work với InnerSteps default null)

Pending Chunk C: PurchaseEvaluationWorkflowService.TransitionAsync N-stage
logic + legacy 2-stage fallback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 18:14:39 +07:00
0f7901c19f [CLAUDE] App: PE + Contract Create/Update commands + DTO add manual budget fields
Chunk 2/5 — wire 2 field mới (BudgetManualName + BudgetManualAmount) qua tất cả
CQRS commands + handlers + DTOs cho cả PE và HĐ. Mirror logic per Q3 user.

Files sửa:
  ~ Application/PurchaseEvaluations/PurchaseEvaluationFeatures.cs
    - CreatePurchaseEvaluationCommand record +2 param
    - Validator: MaximumLength(200) + GreaterThanOrEqualTo(0)
    - Handler: wire entity
    - UpdatePurchaseEvaluationDraftCommand record +2 param + handler wire
    - GetPurchaseEvaluationQuery → DTO mapping +2 field
  ~ Application/PurchaseEvaluations/Dtos/PurchaseEvaluationDtos.cs
    - PurchaseEvaluationDetailBundleDto +2 field (BudgetManualName/Amount)
  ~ Application/PurchaseEvaluations/CreateContractFromEvaluationFeatures.cs
    - Carry forward pe.BudgetManualName/Amount → contract khi gen HĐ từ phiếu
  ~ Application/Contracts/ContractFeatures.cs
    - CreateContractCommand record +2 param + Validator + Handler
    - UpdateContractDraftCommand record +2 param + Handler (diff log thêm 2 field)
    - ContractDetailDto mapping +2 field
  ~ Application/Contracts/Dtos/ContractDtos.cs
    - ContractDetailDto +2 field

Validation Q2 chốt: cả 2 cùng null OK. Manual amount có thể null hoặc >= 0.
KHÔNG XOR với BudgetId (BE prefer link BudgetId nếu set, manual fallback only).

Controllers KHÔNG đụng (FromBody bind JSON → record record optional fields gen
auto null cho legacy callers — backward compat).

Verify: dotnet build pass · dotnet test SolutionErp.slnx 83 pass.

Next: Chunk 3 FE-Admin toggle + 2 fields PeHeaderForm + ContractHeaderForm.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 12:34:00 +07:00
1fc439b978 [CLAUDE] App+Api+FE: Chunk E5 — Budget 2-stage dept approval (mirror PE/Contract)
Budget complete the trifecta — đồng bộ pattern 2-stage cho 3 module
(Contract + PE + Budget) cùng UX cho user khi UAT.

BE App:
- TransitionBudgetCommandHandler thêm INotificationService + IDateTime DI
- Mirror logic 2-stage từ ContractWorkflowService:
  - actor.DepartmentId != null + KHÔNG admin/system + KHÔNG resume
    - DeptManager (TPB) hoặc CanBypassReview → Stage=Confirm
    - Else (NV) → Stage=Review only, BLOCK transition
  - Upsert BudgetDepartmentApproval (UNIQUE BudgetId+Phase+Dept+Stage)
  - Block khi !hasConfirm: insert Approval + Changelog + Notify TPB → return early
- BudgetDepartmentApprovalFeatures.cs (List query mirror PE/Contract)

Api:
- BudgetsController endpoint GET /budgets/{id}/department-approvals

FE (cả fe-admin + fe-user):
- types/budget.ts thêm ApprovalStage const + BudgetDepartmentApproval type
- BudgetWorkflowPanel section "Tiến trình duyệt 2-cấp phòng ban":
  - Group by phase × dept, show Review NV + Confirm TPB
  - Highlight amber "chờ TPB confirm" + badge fuchsia bypass

Note: low-priority cho Budget (ít user duyệt budget per dept) nhưng giữ
consistent UX 3 module.

Build: BE pass + FE pass cả 2 + 77 test pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:46:56 +07:00
b6f5a16420 [CLAUDE] Infra+App+Api+FE: Chunk E4 — HĐ 2-stage dept approval (mirror PE)
Mở rộng 2-stage logic từ PE sang Contract workflow (Migration 16 đã có schema):

BE Service:
- ContractWorkflowService thêm UserManager<User> DI
- Mirror logic 2-stage từ PurchaseEvaluationWorkflowService.TransitionAsync
  Sau policy guard, trước gen mã HĐ:
  - User.DepartmentId != null + actor không admin/system + KHÔNG resume
    - DeptManager (TPB) → Stage=Confirm trực tiếp
    - CanBypassReview=true → Stage=Confirm + IsBypassed=true
    - Else (NV) → Stage=Review only, BLOCK transition
  - Insert ContractDepartmentApproval row (UPSERT theo UNIQUE)
  - Block transition khi chưa có Stage=Confirm:
    - Insert ContractApproval (FromPhase=ToPhase=fromPhase, [Review NV] comment)
    - Insert ContractChangelog "đã review, chờ TPB confirm"
    - Notify TPB cùng dept (UserManager filter DeptManager role)
    - Return early — phase KHÔNG đổi

App + Api:
- ContractDepartmentApprovalFeatures.cs (List query mirror PE)
- ContractsController endpoint GET /contracts/{id}/department-approvals

FE (cả fe-admin + fe-user):
- types/contracts.ts thêm ApprovalStage const + ContractDepartmentApproval type
- WorkflowHistoryPanel section "Tiến trình duyệt 2-cấp phòng ban":
  - Group by phase × dept, show Review NV + Confirm TPB
  - Highlight amber "chờ TPB confirm" khi current phase có Review chưa Confirm
  - Badge fuchsia "bypass" khi NV.CanBypassReview=true
  - Insert giữa WorkflowSummaryCard và Lịch sử duyệt
- Mirror cả 2 app (rule §3.9)

Use case mirror PE: HĐ ở phase DangGopY (P.CCM) — nv.cao (NV) duyệt thì
phase KHÔNG đổi (Review only), chờ ccm.tran (TPB) confirm mới sang DangXetDuyet.

Build: BE pass + FE pass cả 2 + 77 test pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:43:05 +07:00