[CLAUDE] App+Api+Docs: Chunk E1 — List endpoint + Bypass-review + Notify TPB + chốt session 8
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m15s
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 3m15s
3 endpoint mới + Notify TPB + Docs update để chốt session 8.
Application:
- PurchaseEvaluationDepartmentApprovalFeatures.cs (NEW):
* ListPeDepartmentApprovalsQuery + DTO PeDepartmentApprovalDto
* Join Departments (lấy Name) + lookup Users.FullName denorm cho FE timeline
- UserFeatures.cs: SetUserBypassReviewCommand + Handler dùng UserManager.UpdateAsync
- IApplicationDbContext: thêm DbSet<User> Users + DbSet<Role> Roles (cần cho lookup)
Api:
- PurchaseEvaluationsController: GET /api/purchase-evaluations/{id}/department-approvals
- UsersController: PATCH /api/users/{id}/bypass-review (Authorize Users.Update)
Infra:
- PurchaseEvaluationWorkflowService: notify TPB cùng dept khi NV review.
Query db.Users.Where(DeptId match + IsActive) → UserManager.GetRolesAsync
filter DeptManager → notifications.NotifyAsync. Best effort fail non-critical.
Docs:
- STATUS.md: Recently Done thêm row session 8 + Phase header update
count 52→55 tables, 15→16 migrations, 128→131 endpoints
- HANDOFF.md: TL;DR session 8 + 8 cảnh báo session 9 (FE chưa làm,
test flow anh Kiệt, smart reject test, lock edit test, ...)
- migration-todos.md: Phase 9 done section đầy đủ 3 ràng buộc + pending Chunk E-bis
- CLAUDE.md: count 52→55 + migration 16 description
- session log: 2026-05-04-1230-chot-session-8-2-stage-dept-approval.md (full report)
Verify final:
- Build pass 0 warning 0 error
- 77 unit test pass (54 Domain + 23 Infra)
- Migration 16 applied LocalDB OK + schema verified
Total session 8 cumulative: 5 commit per-chunk:
- 5fe61cc (A: Migration 16 schema)
- 14f3c9f (B: Lock edit guards 17 handler)
- 9747f8c (C: Smart reject + Resume 3 module)
- a532ba6 (D: PE 2-stage logic)
- (current E1: List + Notify + Bypass + Docs)
Pending Chunk E-bis (defer cho session 9 sau UAT PE):
- FE Workflow Panel hiển thị 2-stage timeline
- FE UserManager toggle CanBypassReview
- HĐ + Budget 2-stage extension
- Tests Phase 3 mini cho 2-stage Service-layer logic
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@ -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`)
|
- Audit fields: `CreatedAt`, `UpdatedAt`, `CreatedBy`, `UpdatedBy` (`BaseEntity`)
|
||||||
- Soft delete: `IsDeleted`, `DeletedAt`, `DeletedBy` (`AuditableEntity`)
|
- Soft delete: `IsDeleted`, `DeletedAt`, `DeletedBy` (`AuditableEntity`)
|
||||||
- Migrations: `dotnet ef migrations add <Name> --project src/Backend/SolutionErp.Infrastructure --startup-project src/Backend/SolutionErp.Api`
|
- Migrations: `dotnet ef migrations add <Name> --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
|
### Modules
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,38 @@
|
|||||||
# HANDOFF — Brief 5 phút cho session tiếp theo
|
# 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 <admin>
|
||||||
|
{ "canBypassReview": true }
|
||||||
|
```
|
||||||
|
4. **Test thực tế bug fix flow**:
|
||||||
|
- Login `phuong.nguyen` (NV.PRO, role=Procurement, DeptId=PRO) tạo phiếu PE type A
|
||||||
|
- Trình DangSoanThao → ChoPurchasing
|
||||||
|
- phuong.nguyen click Duyệt phase ChoPurchasing → expect: phase KHÔNG đổi, có row Stage=Review
|
||||||
|
- `tra.bui` (TPB.PRO, role=DeptManager) click Duyệt → expect: phase chuyển ChoCCM
|
||||||
|
5. **Notify TPB cùng dept** dùng `UserManager.GetRolesAsync` filter `DeptManager`. Best effort, fail OK.
|
||||||
|
6. **Cron audit định kỳ 2026-05-01** đã quá hạn 3 ngày, vẫn EMPTY runtime. Cần manual trigger.
|
||||||
|
7. **Smart reject test**: Reject phase ChoCCM → DangSoanThao + RejectedFromPhase=ChoCCM. Drafter sửa Detail + trình lại → jump straight tới ChoCCM (skip ChoPurchasing).
|
||||||
|
8. **Lock edit test**: HĐ ở Phase=DangGopY → cố sửa Detail → expect 409 Conflict "đã trình duyệt, không thể chỉnh sửa".
|
||||||
|
|
||||||
## TL;DR Session 6 (30/04 — không code, chỉ docs)
|
## TL;DR Session 6 (30/04 — không code, chỉ docs)
|
||||||
|
|
||||||
|
|||||||
@ -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`.
|
> **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
|
### 🌐 Production URLs
|
||||||
|
|
||||||
@ -44,14 +44,23 @@
|
|||||||
- [ ] **Phase 4** — API smoke tests qua WebApplicationFactory ~7 test
|
- [ ] **Phase 4** — API smoke tests qua WebApplicationFactory ~7 test
|
||||||
- [ ] **Phase 5** — FE Vitest cho lib utility (queryMatches, fmtMoney) ~10 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)
|
## ✅ Recently Done (newest on top)
|
||||||
|
|
||||||
| Ngày | Ai | Task | Commit |
|
| 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-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 | **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` |
|
| 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` |
|
||||||
|
|||||||
@ -157,6 +157,35 @@ Session log: `2026-04-28-chot-session-4-budget.md`.
|
|||||||
|
|
||||||
## 📝 Phase 9 — UAT + Ops + carry over (Session 6+ active)
|
## 📝 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)
|
### ✅ 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ũ
|
- [x] **MD audit + compact** — STATUS -27%, HANDOFF -32%, migration-todos -35%, archive 51 row Phase 0-7 cũ
|
||||||
|
|||||||
@ -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<PeDepartmentApprovalDto> (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 <admin token>
|
||||||
|
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** |
|
||||||
@ -220,6 +220,15 @@ public class PurchaseEvaluationsController(IMediator mediator) : ControllerBase
|
|||||||
await mediator.Send(new DeletePeDepartmentOpinionCommand(id, kind), ct);
|
await mediator.Send(new DeletePeDepartmentOpinionCommand(id, kind), ct);
|
||||||
return NoContent();
|
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<ActionResult<List<PeDepartmentApprovalDto>>> ListDepartmentApprovals(
|
||||||
|
Guid id, CancellationToken ct)
|
||||||
|
=> Ok(await mediator.Send(new ListPeDepartmentApprovalsQuery(id), ct));
|
||||||
}
|
}
|
||||||
|
|
||||||
public record OpinionBody(PeDepartmentKind Kind, string? Opinion, bool Sign);
|
public record OpinionBody(PeDepartmentKind Kind, string? Opinion, bool Sign);
|
||||||
|
|||||||
@ -62,7 +62,18 @@ public class UsersController(IMediator mediator) : ControllerBase
|
|||||||
await mediator.Send(new UnlockUserCommand(id), ct);
|
await mediator.Send(new UnlockUserCommand(id), ct);
|
||||||
return NoContent();
|
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<IActionResult> SetBypassReview(
|
||||||
|
Guid id, [FromBody] SetBypassReviewBody body, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await mediator.Send(new SetUserBypassReviewCommand(id, body.CanBypassReview), ct);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record AssignRolesBody(List<string> Roles);
|
public record AssignRolesBody(List<string> Roles);
|
||||||
public record ResetPasswordBody(string NewPassword);
|
public record ResetPasswordBody(string NewPassword);
|
||||||
|
public record SetBypassReviewBody(bool CanBypassReview);
|
||||||
|
|||||||
@ -23,6 +23,8 @@ public interface IApplicationDbContext
|
|||||||
DbSet<WorkItem> WorkItems { get; }
|
DbSet<WorkItem> WorkItems { get; }
|
||||||
DbSet<MenuItem> MenuItems { get; }
|
DbSet<MenuItem> MenuItems { get; }
|
||||||
DbSet<Permission> Permissions { get; }
|
DbSet<Permission> Permissions { get; }
|
||||||
|
DbSet<User> Users { get; }
|
||||||
|
DbSet<Role> Roles { get; }
|
||||||
DbSet<ContractTemplate> ContractTemplates { get; }
|
DbSet<ContractTemplate> ContractTemplates { get; }
|
||||||
DbSet<ContractClause> ContractClauses { get; }
|
DbSet<ContractClause> ContractClauses { get; }
|
||||||
DbSet<Contract> Contracts { get; }
|
DbSet<Contract> Contracts { get; }
|
||||||
|
|||||||
@ -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<List<PeDepartmentApprovalDto>>;
|
||||||
|
|
||||||
|
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<ListPeDepartmentApprovalsQuery, List<PeDepartmentApprovalDto>>
|
||||||
|
{
|
||||||
|
public async Task<List<PeDepartmentApprovalDto>> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -267,3 +267,23 @@ public class UnlockUserCommandHandler(UserManager<User> userManager) : IRequestH
|
|||||||
await userManager.ResetAccessFailedCountAsync(user);
|
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<User> userManager)
|
||||||
|
: IRequestHandler<SetUserBypassReviewCommand>
|
||||||
|
{
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -176,7 +176,38 @@ public class PurchaseEvaluationWorkflowService(
|
|||||||
ContextNote = comment,
|
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);
|
await db.SaveChangesAsync(ct);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user