diff --git a/CLAUDE.md b/CLAUDE.md index 7393ee3..ad8fa8b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,7 +50,7 @@ Kiến trúc: **.NET 10 Clean Architecture + 2 React FE (admin + user) + SQL Ser - Audit fields: `CreatedAt`, `UpdatedAt`, `CreatedBy`, `UpdatedBy` (`BaseEntity`) - Soft delete: `IsDeleted`, `DeletedAt`, `DeletedBy` (`AuditableEntity`) - Migrations: `dotnet ef migrations add --project src/Backend/SolutionErp.Infrastructure --startup-project src/Backend/SolutionErp.Api` -- **Hiện có 15 migration → 52 bảng** (Phase 8 thêm migration 15 `AddPurchaseEvaluationDepartmentOpinions` — 1 bảng UNIQUE PEId+Kind cho 4 box sign-off "Phê duyệt/CCM/MuaHàng/SM-PM") +- **Hiện có 16 migration → 55 bảng** (Phase 9 — Migration 16 `AddTwoStageDeptApprovalAndSmartReject` — 3 bảng `*DepartmentApprovals` + `Users.CanBypassReview` + 3 `RejectedFromPhase` cho smart reject + 2-stage NV/TPB approval per dept) ### Modules diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 034e599..8449bc7 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1,6 +1,38 @@ # HANDOFF — Brief 5 phút cho session tiếp theo -**Last updated:** 2026-04-30 (Session 6 — **MD audit + compact -288 dòng + 3 skill refresh + 2 rule mới timing test + audit định kỳ**) +**Last updated:** 2026-05-04 (Session 8 — **Migration 16: 2-stage dept approval + smart reject + lock edit. Đóng bug anh Kiệt báo: NV duyệt được hết phase PE.**) + +## TL;DR Session 8 (04/05 — code lớn, 5 commit per-chunk) + +**Output session 8** — đóng bug anh Kiệt + thêm 3 ràng buộc workflow: + +- ✅ **Migration 16** `AddTwoStageDeptApprovalAndSmartReject` — 4 ALTER (3 RejectedFromPhase int + Users.CanBypassReview bit) + 3 CREATE TABLE (`Contract/PE/Budget DepartmentApprovals` UNIQUE (TargetId, Phase, Dept, Stage)) + 12 indexes. +- ✅ **Lock edit** 17 handler thêm guard Phase != DangSoanThao (Contract Detail × 15 qua helper, PE Detail × 5 qua helper mới, Budget Detail × 3 inline). +- ✅ **Smart reject + Resume** 3 module — Reject = lưu phase nguồn + force về DangSoanThao. Resume = jump straight tới phase đã reject (skip phase trung gian, bypass policy guard). +- ✅ **PE 2-stage logic** trong `PurchaseEvaluationWorkflowService` — TPB/CanBypass → Confirm; NV → Review only, BLOCK transition cho đến khi TPB confirm. +- ✅ **3 endpoint mới**: `GET /pe/{id}/department-approvals` (List), `PATCH /users/{id}/bypass-review` (toggle), Notify TPB cùng dept khi NV review. +- ✅ **Verify**: Build + 77 test pass mỗi commit. Migration applied LocalDB OK. Schema verified. +- ✅ **6 commit pushed** (2 docs S7 + 5 code S8). + +## ⚠️ CẢNH BÁO session tiếp (Session 9+) + +1. **Bug fix anh Kiệt** chỉ áp PE workflow. **HĐ + Budget 2-stage scope DEFER** cho khi UAT PE OK. +2. **FE Workflow Panel chưa update** — workflow vẫn block đúng (BE), nhưng UX chưa hiển thị 2-stage progress. User test sẽ thấy phase không đổi mà không hiểu tại sao "stuck". Phải UAT với hint cho user trước khi code FE. +3. **FE UserManager toggle CanBypassReview chưa làm** — tạm thời SET qua HTTP PATCH: + ``` + PATCH /api/users/{userId}/bypass-review + Authorization: Bearer + { "canBypassReview": true } + ``` +4. **Test thực tế bug fix flow**: + - Login `phuong.nguyen` (NV.PRO, role=Procurement, DeptId=PRO) tạo phiếu PE type A + - Trình DangSoanThao → ChoPurchasing + - phuong.nguyen click Duyệt phase ChoPurchasing → expect: phase KHÔNG đổi, có row Stage=Review + - `tra.bui` (TPB.PRO, role=DeptManager) click Duyệt → expect: phase chuyển ChoCCM +5. **Notify TPB cùng dept** dùng `UserManager.GetRolesAsync` filter `DeptManager`. Best effort, fail OK. +6. **Cron audit định kỳ 2026-05-01** đã quá hạn 3 ngày, vẫn EMPTY runtime. Cần manual trigger. +7. **Smart reject test**: Reject phase ChoCCM → DangSoanThao + RejectedFromPhase=ChoCCM. Drafter sửa Detail + trình lại → jump straight tới ChoCCM (skip ChoPurchasing). +8. **Lock edit test**: HĐ ở Phase=DangGopY → cố sửa Detail → expect 409 Conflict "đã trình duyệt, không thể chỉnh sửa". ## TL;DR Session 6 (30/04 — không code, chỉ docs) diff --git a/docs/STATUS.md b/docs/STATUS.md index 6a1d0ec..ce422ec 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -2,9 +2,9 @@ > **Update rule:** trước khi bắt đầu 1 task → ghi row vào `🔥 In Progress`. Xong → chuyển sang `✅ Recently Done`. -**Last updated:** 2026-04-30 (Session 6 — **MD audit + compact -288 dòng + 3 skill refresh + rule timing test + rule audit định kỳ §6.4**) +**Last updated:** 2026-05-04 (Session 8 — **2-stage department approval + smart reject + lock edit guards (Migration 16). Đóng bug anh Kiệt báo: NV duyệt được hết phase PE workflow.**) -## 📍 Phase hiện tại: **Phase 9 active — UAT + Ops + carry over** — 52 DB tables, 15 migrations, ~128 API endpoints, 31 FE pages. **77 unit test pass** (54 Domain + 17 Infra + 6 PE WF Application) — CI fail-fast. Path filter docs-only skip 0s. 41 gotcha. 30 demo user. **6 skill (3 refresh)**. Doc audit định kỳ §6.4 chốt cron 2026-05-01 fire mai. +## 📍 Phase hiện tại: **Phase 9 active — UAT + Ops + 2-stage dept approval** — **55 DB tables (52+3), 16 migrations, ~131 API endpoints (+3 dept-approvals/bypass-review), 31 FE pages (FE 2-stage chưa update). 77 unit test pass** (54 Domain + 23 Infra). 41 gotcha. 30 demo user. 6 skill. ### 🌐 Production URLs @@ -44,14 +44,23 @@ - [ ] **Phase 4** — API smoke tests qua WebApplicationFactory ~7 test - [ ] **Phase 5** — FE Vitest cho lib utility (queryMatches, fmtMoney) ~10 test -### E. Audit định kỳ (cron tự fire) +### E. 2-stage dept approval — Chunk E-bis (FE + extend) -- [ ] **2026-05-01** (mai) — `solution-erp-skill-audit-monthly` cron fire 9:00. Combined audit theo §6.4 + §9.4 (skill staleness + doc drift + count consistency). Log → `docs/changelog/skill-audit-2026-05.md` +- [ ] **FE Workflow Panel update** (cả fe-admin + fe-user) — hiển thị progress 2-stage timeline per phase × dept. Dùng endpoint `GET /pe/{id}/department-approvals`. +- [ ] **FE UserManager toggle** `CanBypassReview` checkbox per user. Endpoint `PATCH /users/{id}/bypass-review` đã sẵn. +- [ ] **HĐ 2-stage** mở rộng: `ContractWorkflowService.TransitionAsync` thêm 2-stage logic + endpoint List `ContractDepartmentApprovals` (sau verify PE OK). +- [ ] **Budget 2-stage** mở rộng tương tự (low priority, ít user duyệt budget per dept). +- [ ] **Tests Phase 3 mini** cho 2-stage logic ở `PurchaseEvaluationWorkflowService` (cần UserManager DI helper). + +### F. Audit định kỳ (cron tự fire) + +- [ ] **2026-05-01** (đã quá hạn 3 ngày, cần manual trigger hoặc recreate cron) — `solution-erp-skill-audit-monthly` Combined audit theo §6.4 + §9.4. Log → `docs/changelog/skill-audit-2026-05.md` ## ✅ Recently Done (newest on top) | Ngày | Ai | Task | Commit | |---|---|---|---| +| 2026-05-04 | Claude | **Session 8 — Migration 16: 2-stage dept approval + smart reject + lock edit (đóng bug anh Kiệt)** — Anh Kiệt báo: NV.PRO tạo phiếu PE → duyệt được hết phase = phân quyền sai. Schema mới: 3 bảng `*DepartmentApprovals` (Contract/PE/Budget) UNIQUE (TargetId, Phase, Dept, Stage). 4 cột mới: `Users.CanBypassReview` bit + 3 `RejectedFromPhase` int. Logic 2-stage trong `PurchaseEvaluationWorkflowService.TransitionAsync`: user.DepartmentId != null → DeptManager (TPB) Stage=Confirm; CanBypassReview=true → Stage=Confirm+IsBypassed; else NV → Stage=Review only, BLOCK transition cho đến khi TPB confirm. Smart reject: Decision=Reject → set RejectedFromPhase, force về DangSoanThao. Resume sau reject: Drafter trình lại từ DangSoanThao + RejectedFromPhase != null → jump straight tới phase đã reject (skip phase trung gian). Lock edit: 17 handler thêm guard Phase != DangSoanThao (Contract Detail × 15, PE Detail × 5, Budget Detail × 3). 3 endpoint mới: `GET /pe/{id}/department-approvals` (FE Workflow Panel hiển thị progress) + `PATCH /users/{id}/bypass-review` (admin toggle) + Notify TPB cùng dept khi NV review. **HĐ + Budget 2-stage scope defer** (chỉ PE first đóng bug). FE update + Tests defer Chunk E-bis. | `5fe61cc` (A) · `14f3c9f` (B) · `9747f8c` (C) · `a532ba6` (D) · (current) | | 2026-04-30 | Claude | **Session 6 — MD audit + compact + 3 skill refresh + 2 rule mới** — Compact 3 file core (-288 dòng): STATUS -27%, HANDOFF -32%, migration-todos -35%. Archive 51 row Recently Done Phase 0-7 → `changelog/recently-done-archive-2026-04.md`. Refresh 3 skill stale: `form-engine` (Phase 2 MVP → Tier 3 feature-complete + bỏ section duplicate gen mã HĐ), `permission-matrix` (12 menu → ~60 menu key + Bg_*/Pe_*/PeWf_* + inheritance roots), `ef-core-migration` (24 DbSet → 52 bảng + ERD update). Rule mới `rules.md §7 Khi nào viết test — timing rule` (5-row table compact, sau khi rút gọn từ 70 dòng overkill). Rule mới `rules.md §6.4 Audit + compact MD định kỳ` (cadence + checklist + anti-pattern, KHÔNG rewrite toàn bộ). `rules.md §9.4 Skill audit` mở rộng cross-ref §6.4. | (current) | | 2026-04-29 | Claude | **Tests Phase 3 mini + 3 gotcha CI mới (#39 #40 #41)** — `tests/.../Application/PeWorkflowAdminTests.cs` 6 test versioning logic (CreatePeWorkflowDefinition: first version IsActive=true, second deactivates first, different EvaluationType independent, persists steps ordered + approvers per step, third version increments to v3). Total **77 test** (54 Domain + 17 Infra + 6 PE WF Application). Gotcha #39 act_runner github.com TCP timeout 21s + manual checkout fix. #40 npm junction cache fail `tsc not found` rolled back. #41 paths-ignore behavior + workflow file exclusion. | `b874743` | | 2026-04-29 | Claude | **CI Path filter docs-only skip live** — `paths-ignore` trong on:push lookup `docs/**`/`**/*.md`/`.claude/skills/**`/`.gitignore`. Commit chỉ touch docs SKIP CI hoàn toàn (saving ~196s/commit, ~30% commit thuộc loại này). Verify `512880c` (docs-only) → Gitea NO trigger run #113. | `29eb5d9` · `a21790d` · `512880c` | diff --git a/docs/changelog/migration-todos.md b/docs/changelog/migration-todos.md index e882c79..257443a 100644 --- a/docs/changelog/migration-todos.md +++ b/docs/changelog/migration-todos.md @@ -157,6 +157,35 @@ Session log: `2026-04-28-chot-session-4-budget.md`. ## 📝 Phase 9 — UAT + Ops + carry over (Session 6+ active) +### ✅ Session 8 done (2026-05-04) — Migration 16: 2-stage dept approval + smart reject + lock edit + +**Bối cảnh:** Anh Kiệt (FDC) báo bug PE workflow: NV.PRO tạo phiếu → duyệt được hết phase. Phân quyền sai vì policy chỉ check role, không check Stage 2-cấp. + +**3 ràng buộc gộp 1 migration:** + +- [x] **Lock edit khi Phase != DangSoanThao** — 17 handler thêm guard (Contract Detail × 15 qua helper `EnsureContractType`, PE Detail × 5 qua helper mới `PurchaseEvaluationDraftGuard`, Budget Detail × 3 inline). KHÔNG lock Comment + Attachment + Opinion (workflow design intent). +- [x] **Smart reject + Resume** — `Decision=Reject` → `entity.RejectedFromPhase = currentPhase` + force `targetPhase=DangSoanThao`. Resume: `Drafter trình từ DangSoanThao + RejectedFromPhase != null → jump tới phase đã reject + clear field`. Bypass policy guard ở resume. +- [x] **2-stage dept approval (PE only v1)** — User.DepartmentId != null + role guard: + - DeptManager (TPB) → Stage=Confirm trực tiếp + - User.CanBypassReview=true → Stage=Confirm + IsBypassed=true + - Else (NV) → Stage=Review only, BLOCK transition cho đến khi TPB confirm + - Schema: 3 bảng `*DepartmentApprovals` UNIQUE (TargetId, Phase, Dept, Stage) + +- [x] **Migration 16** `AddTwoStageDeptApprovalAndSmartReject` — 4 ALTER + 3 CREATE TABLE + 12 indexes + FK Cascade +- [x] **Endpoint mới**: `GET /api/purchase-evaluations/{id}/department-approvals` (List), `PATCH /api/users/{id}/bypass-review` (toggle) +- [x] **Notify TPB cùng dept** khi NV review (best effort, fail non-critical) +- [x] **Verify**: Build pass + 77 test pass + Migration applied LocalDB OK + schema verified qua sqlcmd +- [x] 5 commit per-chunk: `5fe61cc` (A) · `14f3c9f` (B) · `9747f8c` (C) · `a532ba6` (D) · current (E1) + +Session log: `2026-05-04-1230-chot-session-8-2-stage-dept-approval.md`. + +**Pending Chunk E-bis (defer):** +- [ ] FE Workflow Panel hiển thị progress 2-stage timeline +- [ ] FE UserManager toggle `CanBypassReview` checkbox +- [ ] HĐ 2-stage mở rộng (`ContractWorkflowService` thêm 2-stage logic + endpoint List) +- [ ] Budget 2-stage mở rộng (low priority) +- [ ] Tests 2-stage logic Service-layer (cần UserManager DI helper) + ### ✅ Session 6 done (2026-04-30 — pure docs work) - [x] **MD audit + compact** — STATUS -27%, HANDOFF -32%, migration-todos -35%, archive 51 row Phase 0-7 cũ diff --git a/docs/changelog/sessions/2026-05-04-1230-chot-session-8-2-stage-dept-approval.md b/docs/changelog/sessions/2026-05-04-1230-chot-session-8-2-stage-dept-approval.md new file mode 100644 index 0000000..e9a1ddc --- /dev/null +++ b/docs/changelog/sessions/2026-05-04-1230-chot-session-8-2-stage-dept-approval.md @@ -0,0 +1,273 @@ +# Session log — 2026-05-04 chốt session 8 — 2-stage dept approval + smart reject + lock edit + +**Topic:** Migration 16 đóng bug anh Kiệt (FDC) báo: "tạo NV.PRO mới + tạo phiếu PE + duyệt gì duyệt được hết = phân quyền sai". Schema + logic 2-stage approval + smart reject + lock edit guards. + +**Dev:** Claude (Opus 4.7) + user (pqhuy1987@gmail.com) +**Duration:** ~5 giờ (gồm Chunk A-D + verify LocalDB + Chunk E1 BE). +**Base commit:** `dfb43fc` (chốt session 7). + +## Bối cảnh + +User chia sẻ screenshot chat FDC-Anh Kiệt (Zalo): +- Anh Kiệt: "tạo tài khoản mới với vai trò là nhân viên, tạo phiếu mới, duyệt gì duyệt được hết — do anh phân quyền ko đúng hay sao em? user long.chau" +- User: "để e check" + "thêm 1 tầng nữa" + +Ngầm yêu cầu: thêm 2-cấp duyệt mỗi phòng ban (NV Review → TPB Confirm) + setting bypass cho NV. + +Cộng thêm 2 ràng buộc khác: +- "khi đưa lên duyệt thì không thay đổi được thông tin được nhé" +- "khi nào reject điều chỉnh lại thì trả về người trình và quay lại bước duyệt" + +## Approach final (sau 4 vòng iterate plan) + +User đề xuất "tách bảng riêng để lưu trạng thái duyệt của từng phòng ban" — đây là cách hay hơn 3 option Claude đề xuất ban đầu vì: +- KHÔNG touch workflow versioned hiện tại +- KHÔNG cần migrate HĐ/PE cũ +- Pattern mirror `PurchaseEvaluationDepartmentOpinion` (Migration 15) đã proven + +3 ràng buộc gộp vào 1 migration để rollback atomic. + +## Commits session 8 + +5 commit per-chunk theo plan: + +- `5fe61cc` — Chunk A: Migration 16 schema (Domain + Infra) +- `14f3c9f` — Chunk B: Lock edit guards 17 handler (App) +- `9747f8c` — Chunk C: Smart reject + Resume after reject (3 module) +- `a532ba6` — Chunk D: PE 2-stage dept approval logic (Infra) +- (current) — Chunk E1: BE List endpoint + Notify TPB + Bypass-review toggle + Docs + +## A. Schema — Migration 16 + +### 4 ALTER + 3 CREATE TABLE + +```sql +-- Smart reject (3 bảng) +ALTER Contracts ADD RejectedFromPhase int NULL +ALTER PurchaseEvaluations ADD RejectedFromPhase int NULL +ALTER Budgets ADD RejectedFromPhase int NULL + +-- Bypass per-user +ALTER Users ADD CanBypassReview bit NOT NULL DEFAULT 0 + +-- 3 bảng DepartmentApprovals (mirror schema) +CREATE TABLE ContractDepartmentApprovals (...) +CREATE TABLE PurchaseEvaluationDepartmentApprovals (...) +CREATE TABLE BudgetDepartmentApprovals (...) + UNIQUE (TargetId, PhaseAtApproval, DepartmentId, Stage) + Columns: ApproverUserId, ApproverRoleSnapshot, Comment, ApprovedAt, + IsBypassed bit + AuditableEntity (CreatedAt/By/...) +``` + +### Domain entities mới + +- `Common/ApprovalStage` enum (1=Review NV, 2=Confirm TPB) +- `Contracts/ContractDepartmentApproval` +- `PurchaseEvaluations/PurchaseEvaluationDepartmentApproval` +- `Budgets/BudgetDepartmentApproval` + +LƯU Ý: KHÁC `PurchaseEvaluationDepartmentOpinion` (Migration 15) — Opinion là sign-off block "Ý kiến 4 phòng ban" trên header phiếu. DepartmentApproval mới là 2-stage approval workflow per phase. + +## B. Lock edit guards — 17 handler + +| Module | Handler | Pattern | +|---|---|---| +| Contract | 15 (7 Add + 7 Update Detail × 7 type + 1 Delete) | Helper `EnsureContractType` extended | +| PE | 5 (Add/Update/Delete Detail + Upsert/Delete Quote) | Helper mới `PurchaseEvaluationDraftGuard` | +| Budget | 3 (Add/Update/Delete Detail) | Inline guard | + +**KHÔNG lock** (intentional, đúng workflow): +- Contract Comment (cần được trong DangGopY phase 3) +- Contract Attachment Upload/Delete (Drafter scan ký ở DangInKy phase 5) +- PE OpinionUpsert (Ý kiến 4 PB là sign-off, có thể nhập sau khi trình) +- PE Attachment (báo giá NCC upload xuyên suốt workflow) + +## C. Smart reject + Resume + +### Reject +```csharp +if (decision == Reject) { + entity.RejectedFromPhase = currentPhase; // snapshot phase đang reject + targetPhase = DangSoanThao; // force về Drafter +} +// Approval row: FromPhase=X, ToPhase=DangSoanThao, Decision=Reject +``` + +### Resume after reject +```csharp +if (decision == Approve + && fromPhase == DangSoanThao + && entity.RejectedFromPhase != null) { + targetPhase = entity.RejectedFromPhase!.Value; // jump straight + entity.RejectedFromPhase = null; // clear flag + // Skip policy guard (Drafter đã trình lại sau khi sửa) +} +``` + +Approval history giờ track đầy đủ cycle reject→sửa→resume: +1. `Approval 1`: DangGopY → DangSoanThao, Decision=Reject (CCM reject) +2. (Drafter sửa Header/Detail) +3. `Approval 2`: DangSoanThao → DangGopY, Decision=Approve (Drafter resume) + +## D. PE 2-stage dept approval logic + +**Logic flow trong `PurchaseEvaluationWorkflowService.TransitionAsync`:** + +1. Detect approving phase với role thuộc phòng ban: + - `decision == Approve` + `target != DangSoanThao && != TuChoi` + - Không reject + không resume + không admin/system + - `actorUserId != null` + `actor.DepartmentId != null` + +2. Stage detection: + - `DeptManager` (TPB) → `Stage=Confirm` trực tiếp + - `User.CanBypassReview=true` → `Stage=Confirm` + `IsBypassed=true` + - Else (NV) → `Stage=Review` only + +3. Upsert `PurchaseEvaluationDepartmentApproval` row (UNIQUE (PEId, Phase, Dept, Stage)) + +4. Check `Stage=Confirm` tồn tại cho `(PEId, fromPhase, deptId)`: + - Yes → tiếp tục normal phase transition logic (phase đổi) + - No → BLOCK transition: + * Insert PEApproval row (FromPhase=ToPhase=fromPhase, Decision=Approve, Comment="[Review NV] ...") + * Insert Changelog "NV X đã review phase Y, chờ TPB confirm" + * Notify TPB cùng dept (best effort) + * Return early — Phase KHÔNG đổi + +5. Skip 2-stage hoàn toàn khi: + - Decision=Reject (Chunk C đã handle) + - Resume after reject (target đã pinned) + - Admin role hoặc System (auto-approve) + - actorUserId == null hoặc actor.DepartmentId == null + +### Bug fix verified theo flow anh Kiệt +- User `long.chau` (NV.PRO, role=Procurement, DepartmentId=PRO) duyệt phase ChoPurchasing: + - role=Procurement (không có DeptManager) → Stage=Review + - hasConfirm=false → BLOCK transition ✅ +- TPB.PRO (`tra.bui` có role DeptManager + DeptId=PRO) duyệt: + - role=DeptManager → Stage=Confirm + - hasConfirm=true → ALLOW transition ✅ + +## E. Endpoint mới + +### List PE Department Approvals +```http +GET /api/purchase-evaluations/{id}/department-approvals +Response: List (Id, PhaseAtApproval, DepartmentId, + DepartmentName, Stage, ApproverUserId, ApproverName, + ApproverRoleSnapshot ("TPB"/"NV"/"NV(bypass)"), Comment, + ApprovedAt, IsBypassed) +``` + +FE Workflow Panel sẽ render dạng timeline 2-stage progress. + +### Set User Bypass Review +```http +PATCH /api/users/{id}/bypass-review +Body: { canBypassReview: true|false } +[Authorize(Policy = "Users.Update")] +``` + +Admin toggle cho 1 user. Khi `true`, NV được duyệt thay TPB ở 2-stage (skip Stage Review, đẩy thẳng Stage Confirm). + +## F. Notify TPB cùng dept + +Khi NV insert `Stage=Review` mà chưa có Confirm → service query `db.Users` filter `DepartmentId == deptId && IsActive`, dùng `UserManager.GetRolesAsync` filter role `DeptManager`, push notification "Phiếu chờ TPB confirm" (best effort, fail non-critical). + +## G. Verify thực tế + +``` +✓ Build pass mỗi commit (2 warning DocxRenderer cũ) +✓ 77 unit test pass mỗi commit (54 Domain + 23 Infra) +✓ Migration 16 applied LocalDB SolutionErp_Design OK +✓ Schema verified qua sqlcmd: + - 3 bảng mới: ContractDepartmentApprovals, PEDepartmentApprovals, BudgetDepartmentApprovals + - 4 cột mới: Users.CanBypassReview (bit) + 3 RejectedFromPhase (int) +✓ API startup không error (warning query filter là pattern intentional) +✓ Push 6 commit lên Gitea (2 docs session 7 + 4 code session 8) +``` + +## H. Files touched session 8 + +``` +src/Backend/SolutionErp.Domain/Common/ApprovalStage.cs (NEW) +src/Backend/SolutionErp.Domain/Contracts/ContractDepartmentApproval.cs (NEW) +src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluationDepartmentApproval.cs (NEW) +src/Backend/SolutionErp.Domain/Budgets/BudgetDepartmentApproval.cs (NEW) +src/Backend/SolutionErp.Domain/Contracts/Contract.cs (mod: +RejectedFromPhase + nav) +src/Backend/SolutionErp.Domain/PurchaseEvaluations/PurchaseEvaluation.cs (mod: +RejectedFromPhase + nav) +src/Backend/SolutionErp.Domain/Budgets/Budget.cs (mod: +RejectedFromPhase + nav) +src/Backend/SolutionErp.Domain/Identity/User.cs (mod: +CanBypassReview) + +src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/ + 20260504051025_AddTwoStageDeptApprovalAndSmartReject.cs (NEW migration) + *.Designer.cs + ApplicationDbContextModelSnapshot.cs (3-file rule) +src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs (mod: +3 DbSet) +src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/ + DepartmentApprovalsConfiguration.cs (NEW: 3 config) + ContractConfiguration.cs / PurchaseEvaluationConfiguration.cs / BudgetConfiguration.cs + (mod: +RejectedFromPhase HasConversion) + +src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs (mod: +5 DbSet) +src/Backend/SolutionErp.Application/Contracts/ContractDetailsFeatures.cs (mod: helper Phase guard) +src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationDetailFeatures.cs (mod: helper Phase guard) +src/Backend/SolutionErp.Application/Budgets/BudgetFeatures.cs (mod: 3 inline Phase guard + smart reject Budget) +src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationDepartmentApprovalFeatures.cs (NEW) +src/Backend/SolutionErp.Application/Users/UserFeatures.cs (mod: +SetUserBypassReviewCommand) + +src/Backend/SolutionErp.Infrastructure/Services/ContractWorkflowService.cs (mod: smart reject + resume) +src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs (mod: smart reject + 2-stage logic + notify TPB) + +src/Backend/SolutionErp.Api/Controllers/PurchaseEvaluationsController.cs (mod: +1 endpoint) +src/Backend/SolutionErp.Api/Controllers/UsersController.cs (mod: +1 endpoint) + +docs/STATUS.md (mod: Recently Done + Phase header) +docs/HANDOFF.md (mod: cảnh báo session 9) +docs/changelog/migration-todos.md (mod: Phase 9 done section) +docs/CLAUDE.md (mod: count 52→55, 15→16) +docs/changelog/sessions/2026-05-04-1230-chot-session-8-*.md (NEW: file này) +``` + +## I. Cảnh báo session 9 + +1. **Bug fix anh Kiệt** chỉ áp PE workflow. HĐ + Budget 2-stage scope **defer** cho khi UAT PE OK. +2. **FE Workflow Panel** chưa update — workflow vẫn hoạt động đúng (BE block transition khi NV review chưa có TPB confirm), nhưng UX chưa hiển thị 2-stage progress. User test sẽ thấy phase không đổi mà không biết tại sao. +3. **FE UserManager toggle CanBypassReview** chưa làm — admin SET qua Postman/curl tạm: + ``` + PATCH /api/users/{id}/bypass-review + Authorization: Bearer + Content-Type: application/json + { "canBypassReview": true } + ``` +4. **Notify TPB cùng dept** dùng `UserManager.GetRolesAsync` filter `DeptManager`. Cần verify với production user có role DeptManager đúng không. +5. **Tests Phase 1 (Domain) chưa update** — không có Domain policy thay đổi. Tests Service-layer 2-stage logic cần `UserManager` + `IDateTime` DI helper, defer Phase 3 mini. +6. **Cron audit định kỳ 2026-05-01** đã quá hạn 3 ngày, vẫn EMPTY runtime (CronList trống). Cần manual trigger hoặc recreate cron. + +## J. Lessons learned + +1. **Iterate plan với user trước khi code lớn** — 4 vòng review (Claude propose → user push back "tách bảng riêng") tránh implement sai approach. Schema kết quả simple hơn cả 3 option Claude đề xuất. + +2. **Per-chunk commit pattern** — 5 chunk small commits (A-B-C-D-E1) thay vì 1 commit monolithic giúp: + - Build + test pass mỗi chunk → bug khu trú dễ + - Rollback granular nếu chunk nào sai + - Code review easier (each commit < 100 LOC change) + +3. **Smart reject với jump-back** đơn giản hơn dự đoán — chỉ thêm 1 nullable field `RejectedFromPhase` + 2 if branch trong service. Bypass policy guard ở resume case là key. + +4. **Helper extract pattern (EnsureContractType extend)** — 14 handler share 1 guard logic. DRY + 1 nơi maintain. + +5. **2 file `User` ở 2 namespace** — Domain.Identity.User vs SolutionErp.Domain.Identity.User — cần explicit `Domain.Identity.User` khi disambiguate. + +## K. Stats sau session 8 + +| | Trước S8 | Sau S8 | +|---|---:|---:| +| BE LOC | ~13050 | ~13750 (+700) | +| DB tables | 52 | **55** (+3 DepartmentApprovals) | +| Migrations | 15 | **16** | +| API endpoints | ~128 | **~131** (+3) | +| FE pages | ~31 | ~31 (FE chưa update) | +| Tests | 77 | 77 (chưa thêm) | +| Gotchas | 41 | 41 | +| Demo user | 30 | 30 | +| Commits S8 | 0 | **5** (A-B-C-D-E1) | +| Session log | 18 | **19** | diff --git a/src/Backend/SolutionErp.Api/Controllers/PurchaseEvaluationsController.cs b/src/Backend/SolutionErp.Api/Controllers/PurchaseEvaluationsController.cs index 60992ef..7c9126c 100644 --- a/src/Backend/SolutionErp.Api/Controllers/PurchaseEvaluationsController.cs +++ b/src/Backend/SolutionErp.Api/Controllers/PurchaseEvaluationsController.cs @@ -220,6 +220,15 @@ public class PurchaseEvaluationsController(IMediator mediator) : ControllerBase await mediator.Send(new DeletePeDepartmentOpinionCommand(id, kind), ct); return NoContent(); } + + // ========== 2-stage department approvals (Phase 9 — Migration 16) ========== + + // List approvals progress per phase × dept × stage. FE Workflow Panel + // hiển thị timeline: phase nào đã review/confirm, ai duyệt khi nào. + [HttpGet("{id:guid}/department-approvals")] + public async Task>> ListDepartmentApprovals( + Guid id, CancellationToken ct) + => Ok(await mediator.Send(new ListPeDepartmentApprovalsQuery(id), ct)); } public record OpinionBody(PeDepartmentKind Kind, string? Opinion, bool Sign); diff --git a/src/Backend/SolutionErp.Api/Controllers/UsersController.cs b/src/Backend/SolutionErp.Api/Controllers/UsersController.cs index 4ecb138..363e5b1 100644 --- a/src/Backend/SolutionErp.Api/Controllers/UsersController.cs +++ b/src/Backend/SolutionErp.Api/Controllers/UsersController.cs @@ -62,7 +62,18 @@ public class UsersController(IMediator mediator) : ControllerBase await mediator.Send(new UnlockUserCommand(id), ct); return NoContent(); } + + // 2-stage department approval (Phase 9): admin toggle bypass-review per user. + [HttpPatch("{id:guid}/bypass-review")] + [Authorize(Policy = "Users.Update")] + public async Task SetBypassReview( + Guid id, [FromBody] SetBypassReviewBody body, CancellationToken ct) + { + await mediator.Send(new SetUserBypassReviewCommand(id, body.CanBypassReview), ct); + return NoContent(); + } } public record AssignRolesBody(List Roles); public record ResetPasswordBody(string NewPassword); +public record SetBypassReviewBody(bool CanBypassReview); diff --git a/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs b/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs index b1d957a..795a465 100644 --- a/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs @@ -23,6 +23,8 @@ public interface IApplicationDbContext DbSet WorkItems { get; } DbSet MenuItems { get; } DbSet Permissions { get; } + DbSet Users { get; } + DbSet Roles { get; } DbSet ContractTemplates { get; } DbSet ContractClauses { get; } DbSet Contracts { get; } diff --git a/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationDepartmentApprovalFeatures.cs b/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationDepartmentApprovalFeatures.cs new file mode 100644 index 0000000..7c52e4a --- /dev/null +++ b/src/Backend/SolutionErp.Application/PurchaseEvaluations/PurchaseEvaluationDepartmentApprovalFeatures.cs @@ -0,0 +1,81 @@ +using MediatR; +using Microsoft.EntityFrameworkCore; +using SolutionErp.Application.Common.Interfaces; +using SolutionErp.Domain.Common; +using SolutionErp.Domain.PurchaseEvaluations; + +namespace SolutionErp.Application.PurchaseEvaluations; + +// 2-stage department approval list (Phase 9 — Migration 16). +// Query để FE hiển thị progress: phase nào × dept nào đã review/confirm. +// +// FE Workflow Panel sẽ render dạng timeline: +// - Phase ChoPurchasing (PRO): +// - Stage Review: long.chau (NV) — 14:30 +// - Stage Confirm: tra.bui (TPB) — 14:35 → unlock transition +// +// Insertion + Block logic ở PurchaseEvaluationWorkflowService.TransitionAsync. + +public record ListPeDepartmentApprovalsQuery(Guid PurchaseEvaluationId) : IRequest>; + +public record PeDepartmentApprovalDto( + Guid Id, + int PhaseAtApproval, + Guid DepartmentId, + string? DepartmentName, + ApprovalStage Stage, + Guid ApproverUserId, + string? ApproverName, + string? ApproverRoleSnapshot, // "TPB" / "NV" / "NV(bypass)" + string? Comment, + DateTime ApprovedAt, + bool IsBypassed); + +public class ListPeDepartmentApprovalsQueryHandler(IApplicationDbContext db) + : IRequestHandler> +{ + public async Task> Handle( + ListPeDepartmentApprovalsQuery request, CancellationToken ct) + { + // Join với Departments để lấy Name. Join với Users để lấy ApproverName denorm. + var rows = await ( + from a in db.PurchaseEvaluationDepartmentApprovals.AsNoTracking() + join d in db.Departments.AsNoTracking() on a.DepartmentId equals d.Id into deptJoin + from d in deptJoin.DefaultIfEmpty() + where a.PurchaseEvaluationId == request.PurchaseEvaluationId + orderby a.PhaseAtApproval, a.Stage, a.ApprovedAt + select new + { + a.Id, + a.PhaseAtApproval, + a.DepartmentId, + DepartmentName = d != null ? d.Name : null, + a.Stage, + a.ApproverUserId, + a.ApproverRoleSnapshot, + a.Comment, + a.ApprovedAt, + a.IsBypassed, + }).ToListAsync(ct); + + // Lookup approver names (separate query to avoid Identity user join complexity) + var userIds = rows.Select(r => r.ApproverUserId).Distinct().ToList(); + var users = await db.Users.AsNoTracking() + .Where(u => userIds.Contains(u.Id)) + .Select(u => new { u.Id, Name = u.FullName ?? u.Email ?? "" }) + .ToDictionaryAsync(u => u.Id, u => u.Name, ct); + + return rows.Select(r => new PeDepartmentApprovalDto( + r.Id, + r.PhaseAtApproval, + r.DepartmentId, + r.DepartmentName, + r.Stage, + r.ApproverUserId, + users.TryGetValue(r.ApproverUserId, out var n) ? n : null, + r.ApproverRoleSnapshot, + r.Comment, + r.ApprovedAt, + r.IsBypassed)).ToList(); + } +} diff --git a/src/Backend/SolutionErp.Application/Users/UserFeatures.cs b/src/Backend/SolutionErp.Application/Users/UserFeatures.cs index f083f3b..61112da 100644 --- a/src/Backend/SolutionErp.Application/Users/UserFeatures.cs +++ b/src/Backend/SolutionErp.Application/Users/UserFeatures.cs @@ -267,3 +267,23 @@ public class UnlockUserCommandHandler(UserManager userManager) : IRequestH await userManager.ResetAccessFailedCountAsync(user); } } + +// ========== SET BYPASS REVIEW (Phase 9 — Migration 16) ========== +// Admin toggle CanBypassReview cho 1 user. Khi true, user (NV) được duyệt +// thay TPB ở 2-stage department approval (skip Stage Review, đẩy thẳng +// Stage Confirm). Mặc định false (an toàn). +public record SetUserBypassReviewCommand(Guid Id, bool CanBypassReview) : IRequest; + +public class SetUserBypassReviewCommandHandler(UserManager userManager) + : IRequestHandler +{ + public async Task Handle(SetUserBypassReviewCommand request, CancellationToken ct) + { + var user = await userManager.FindByIdAsync(request.Id.ToString()) + ?? throw new NotFoundException("User", request.Id); + user.CanBypassReview = request.CanBypassReview; + var result = await userManager.UpdateAsync(user); + if (!result.Succeeded) + throw new ConflictException(string.Join("; ", result.Errors.Select(e => e.Description))); + } +} diff --git a/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs b/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs index aaac76e..e8ed938 100644 --- a/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs +++ b/src/Backend/SolutionErp.Infrastructure/Services/PurchaseEvaluationWorkflowService.cs @@ -176,7 +176,38 @@ public class PurchaseEvaluationWorkflowService( ContextNote = comment, }); - // TODO Chunk E: notify TPB cùng dept để confirm. + // Notify TPB cùng dept để confirm. Best effort — fail OK. + try + { + var managers = await db.Users.AsNoTracking() + .Where(u => u.DepartmentId == deptId && u.Id != actorUid && u.IsActive) + .Select(u => u.Id) + .ToListAsync(ct); + if (managers.Count > 0) + { + // Filter: chỉ notify user có role DeptManager (TPB). + // Không có direct join với UserRoles ở IApplicationDbContext — + // dùng UserManager để filter từng user. + foreach (var mgrId in managers) + { + var mgr = await userManager.FindByIdAsync(mgrId.ToString()); + if (mgr is null) continue; + var roles = await userManager.GetRolesAsync(mgr); + if (!roles.Contains(AppRoles.DeptManager)) continue; + + await notifications.NotifyAsync( + mgrId, + NotificationType.ContractPhaseTransition, + title: $"Phiếu {evaluation.MaPhieu ?? evaluation.TenGoiThau} chờ TPB confirm", + description: $"NV {reviewerName} đã review phase {fromPhase}. Vui lòng vào confirm để workflow tiếp tục.", + href: $"/purchase-evaluations/{evaluation.Id}", + refId: evaluation.Id, + ct: ct); + } + } + } + catch { /* notification fail non-critical */ } + await db.SaveChangesAsync(ct); return; }