Compare commits
4 Commits
21ee36390e
...
12daa7f6b0
| Author | SHA1 | Date | |
|---|---|---|---|
| 12daa7f6b0 | |||
| 2781c7ea09 | |||
| f6047d5218 | |||
| c847dc0b24 |
@ -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ó 21 migration → 55 bảng** (Phase 9+ — Mig 21 `RefactorWorkflowToFlatModel` DRASTIC REFACTOR Session 16. Bỏ phase enum legacy (2-9 + 98 deprecated giữ data cũ), dùng ChoDuyet=10 đơn nhất + CurrentWorkflowStepIndex tracking. Workflow flat list (Phòng × Cấp × Approvers). WorkflowStep + DepartmentId+PositionLevel. Drop InnerStep entities (Mig 18+20). Service rewrite iterate steps OrderBy Order, advance pointer per approve. Match approver Dept+PositionLevel OR Approvers Role/User. PE 3-button Duyệt forward / Trả lại smart-reject jump-back / Từ chối khoá phiếu)
|
- **Hiện có 22 migration → 58 bảng** (Phase 9+ — Mig 22 `AddApprovalWorkflowsV2` Session 17 schema mới UAT trước khi drop legacy: Quy trình > Bước (Phòng) > Cấp (NV cụ thể qua ApproverUserId — KHÔNG OR-of-many). Schema riêng `ApprovalWorkflows` + `ApprovalWorkflowSteps` + `ApprovalWorkflowLevels`. PE/Contract Service CHƯA wire — vẫn pin Mig 21 legacy. Mig 21 `RefactorWorkflowToFlatModel` DRASTIC REFACTOR Session 16. Phase enum simplify ChoDuyet=10 đơn nhất + CurrentWorkflowStepIndex tracking. Workflow flat list (Phòng × Cấp × Approvers). PE 3-button Duyệt/Trả lại/Từ chối)
|
||||||
|
|
||||||
### Modules
|
### Modules
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,70 @@
|
|||||||
# HANDOFF — Brief 5 phút cho session tiếp theo
|
# HANDOFF — Brief 5 phút cho session tiếp theo
|
||||||
|
|
||||||
**Last updated:** 2026-05-08 (Session 16 — **🎯 DRASTIC REFACTOR DONE: flat workflow Phòng × Cấp + Mig 21 + Service rewrite + FE Designer rewrite. 2 commit Chunk A+B. 96 → 77 test pass (drop 19 legacy). Backward compat: legacy phase 2-9+98 enum giữ cho data cũ.**)
|
**Last updated:** 2026-05-08 (Session 17 — **🎯 SCHEMA MỚI ApprovalWorkflowsV2 + Menu "Duyệt NCC (Mới)" UAT. 4 commit. Schema riêng trên Mig 22. Cấu trúc Bước (Phòng) > Cấp (NV cụ thể qua ApproverUserId — KHÔNG OR-of-many). Chỉ admin Designer xong, Service PE/Contract chưa wire qua schema mới — UAT UIUX trước.**)
|
||||||
|
|
||||||
|
## TL;DR Session 17 (08/05 — Schema mới UAT trước khi drop legacy)
|
||||||
|
|
||||||
|
User chốt sau Session 16: schema flat Mig 21 vẫn không đúng intent. Yêu cầu **viết lại toàn bộ chỗ Quy trình Duyệt + thêm Menu mới "Duyệt NCC (Mới)"** với cấu trúc rõ ràng:
|
||||||
|
|
||||||
|
```
|
||||||
|
Mã Quy trình - Tên Quy trình
|
||||||
|
* Bước 1 - Phòng A
|
||||||
|
* Cấp 1 - NV X ← 1 user CỤ THỂ qua ApproverUserId
|
||||||
|
* Cấp 2 - NV Y
|
||||||
|
* Bước 2 - Phòng B
|
||||||
|
* Cấp 1 - NV Z
|
||||||
|
```
|
||||||
|
|
||||||
|
Khác Mig 21: mỗi Cấp = 1 NV chính xác, KHÔNG OR-of-many group Dept+PositionLevel/Role/User.
|
||||||
|
|
||||||
|
**4 commit (3 chunk per-commit + docs):**
|
||||||
|
|
||||||
|
### Chunk A (`c847dc0`) — Domain + EF + Mig 22 + Menu
|
||||||
|
|
||||||
|
- Domain `ApprovalWorkflowsV2/ApprovalWorkflow.cs` — 3 entity (ApprovalWorkflow + Step + Level) + enum `ApprovalWorkflowApplicableType` (DuyetNcc=1 / DuyetNccPhuongAn=2 / Contract=3)
|
||||||
|
- EF `ApprovalWorkflowConfiguration.cs` — UNIQUE (Code, Version), FK Cascade Step→Workflow + Level→Step, FK Restrict Department + ApproverUserId
|
||||||
|
- ApplicationDbContext +3 DbSet
|
||||||
|
- **Migration 22** `AddApprovalWorkflowsV2` — 3 CREATE TABLE + 1 UNIQUE + 4 INDEX. Applied cả `_Design` + `_Dev` LocalDB
|
||||||
|
- DbInitializer SeedMenusAsync: +menu `ApprovalWorkflowsV2` root dưới System (icon Workflow) + leaf `AwV2_DuyetNcc` (icon FileCheck, label "Duyệt NCC (Mới)")
|
||||||
|
- MenuKeys.cs +2 const trong All array
|
||||||
|
|
||||||
|
### Chunk B (`f6047d5`) — Application CQRS + API
|
||||||
|
|
||||||
|
- `Application/ApprovalWorkflowsV2/ApprovalWorkflowV2AdminFeatures.cs`:
|
||||||
|
- `GetAwAdminOverviewQuery(ApplicableType?)` — load 3-level Include + dept/user names map
|
||||||
|
- `CreateAwDefinitionCommand` + Validator — auto-increment Version theo Code, deactivate active version cùng ApplicableType
|
||||||
|
- `DeleteAwDefinitionCommand` — UAT helper unconditional (chưa pin)
|
||||||
|
- DTO AwDefinition/AwStep/AwLevel + AwTypeSummary
|
||||||
|
- IApplicationDbContext +3 DbSet
|
||||||
|
- `Api/Controllers/ApprovalWorkflowsV2Controller` — route `/api/approval-workflows-v2`, GET ?applicableType=N | POST | DELETE/{id}, reuse policy `Workflows.Read` + `Workflows.Create`
|
||||||
|
|
||||||
|
### Chunk C (`2781c7e`) — FE Designer
|
||||||
|
|
||||||
|
- `fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx` (~480 LOC)
|
||||||
|
- Overview cards Active+History per ApplicableType
|
||||||
|
- DefinitionCard read-only: Bước (badge phòng emerald) → Cấp (badge violet C1/C2 + tên NV + email)
|
||||||
|
- Designer dialog: Mã/Tên/Mô tả + Add/Remove Step + reorder (chevron up/down) + Add/Remove Level + Select Phòng + Select NV duyệt
|
||||||
|
- Validate: mỗi Step ≥1 Level, mỗi Level phải có ApproverUserId
|
||||||
|
- Auto-assign code mặc định theo type: `QT-DN-V2-001` / `QT-DN-PA-V2-001` / `QT-HD-V2-001`
|
||||||
|
- Layout.tsx resolver +ApprovalWorkflowsV2 root → `/system/approval-workflows-v2`, +AwV2_<TypeCode> leaf → `/system/approval-workflows-v2/<code>`
|
||||||
|
- App.tsx +2 route
|
||||||
|
- menuKeys.ts +2 const sync với BE
|
||||||
|
|
||||||
|
### Chunk D — Docs (current)
|
||||||
|
|
||||||
|
STATUS + HANDOFF + project_solution_erp.md memory.
|
||||||
|
|
||||||
|
## ⚠️ Điều quan trọng cho Session 18+
|
||||||
|
|
||||||
|
1. **PE/Contract Service CHƯA wire** qua schema mới — vẫn pin `WorkflowDefinitionId` từ Mig 21 (legacy). Sau khi user UAT UIUX OK, Session sau cần:
|
||||||
|
- Thêm pinning `ApprovalWorkflowId` vào PE/Contract entity (nullable, song song WorkflowDefinitionId)
|
||||||
|
- Service rewrite: nếu pin V2 → iterate ApprovalWorkflowSteps OrderBy Order → ApprovalWorkflowLevels OrderBy Order → match `actor.Id == level.ApproverUserId` (chính xác 1-1)
|
||||||
|
- Migration data từ legacy → V2 (manual hoặc seed sample)
|
||||||
|
- Drop legacy WorkflowDefinition + WorkflowStep + Approver tables sau khi không phiếu nào pin
|
||||||
|
2. **Backward compat 100%** — schema mới chỉ thêm vào, không sửa cũ. Phiếu PE/Contract đang chạy không bị ảnh hưởng.
|
||||||
|
3. **77 test pass giữ nguyên** — không thêm test mới (Designer UI chưa critical, defer khi Service wire xong → test policy + match approver).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## TL;DR Session 16 (08/05 — Drastic refactor flat workflow EXECUTE)
|
## TL;DR Session 16 (08/05 — Drastic refactor flat workflow EXECUTE)
|
||||||
|
|
||||||
|
|||||||
@ -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-05-08 (Session 16 — **🎯 DRASTIC REFACTOR: flat workflow Phòng × Cấp + Migration 21. Bỏ phase enum legacy 2-9, dùng `ChoDuyet=10` đơn nhất + `CurrentWorkflowStepIndex` tracking + `RejectedAtStepIndex` resume. WorkflowStep + DepartmentId/PositionLevel. Drop InnerStep entities (Mig 18+20 deprecated). Service rewrite PE + Contract. App CQRS DTOs simplified. Tests 96 → 77 (drop 19 N-stage/2-stage legacy). FE Designer rewrite flat UI. 2 commit Chunk A + B (`dbb0089`, `88a5be1`). Backward compat: legacy phase values 2-9 + 98 giữ enum cho data cũ.**)
|
**Last updated:** 2026-05-08 (Session 17 — **🎯 SCHEMA MỚI hoàn toàn `ApprovalWorkflowsV2` (Mig 22) + Menu mới "Duyệt NCC (Mới)" UAT trước khi drop legacy. User chốt sau Session 16: "Thấy vẫn không đúng, viết lại toàn bộ chỗ Quy trình Duyệt + thêm Menu mới". Cấu trúc Quy trình > Bước (Phòng) > Cấp (NV cụ thể qua ApproverUserId — KHÔNG OR-of-many như Mig 21). 4 commit Chunk A+B+C (`c847dc0`, `f6047d5`, `2781c7e`) + Chunk D docs.**)
|
||||||
|
|
||||||
## 📍 Phase hiện tại: **Phase 9 active — UAT** — **55 DB tables, 21 migrations, ~134 API endpoints, 32 FE pages. 77 unit test pass** (54 Domain + 23 Infra). 41 gotcha. 30 demo user. 6 skill. **5 PE display status** (Bản nháp / Đã gửi duyệt / Trả lại / Đã duyệt / Từ chối). **Phase enum simplified post-Mig 21**: DangSoanThao=1, ChoDuyet=10 (NEW generic), DaDuyet=7, TuChoi=99. Legacy 2-6 + 98 deprecated. **Flat workflow Mig 21**: WorkflowStep + DepartmentId+PositionLevel + Approvers. PE/Contract + CurrentWorkflowStepIndex/RejectedAtStepIndex tracking. Service iterate steps OrderBy Order, advance pointer per approve. Match approver Dept+PositionLevel (OR cùng cấp/dept). Hết step → DaDuyet/DaPhatHanh.
|
## 📍 Phase hiện tại: **Phase 9 active — UAT (workflow schema mới Mig 22 đợi user test)** — **58 DB tables (+3 Mig 22 ApprovalWorkflows + Steps + Levels), 22 migrations, ~137 API endpoints (+3 V2), 33 FE pages (+1 Designer V2). 77 unit test pass** (54 Domain + 23 Infra). 41 gotcha. 30 demo user. 6 skill. **5 PE display status** (Bản nháp / Đã gửi duyệt / Trả lại / Đã duyệt / Từ chối). **Phase enum simplified post-Mig 21**: DangSoanThao=1, ChoDuyet=10 (NEW generic), DaDuyet=7, TuChoi=99. Legacy 2-6 + 98 deprecated. **Workflow schemas đang đồng tồn tại**: (1) Mig 21 `WorkflowDefinition` flat — pin với PE/Contract đang chạy. (2) Mig 22 `ApprovalWorkflow` mới (UAT) — Bước (Phòng) > Cấp (NV ApproverUserId). Sau UAT → migrate data + drop legacy.
|
||||||
|
|
||||||
### 🌐 Production URLs
|
### 🌐 Production URLs
|
||||||
|
|
||||||
@ -61,6 +61,7 @@
|
|||||||
|
|
||||||
| Ngày | Ai | Task | Commit |
|
| Ngày | Ai | Task | Commit |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
| 2026-05-08 | Claude | **🎯 SESSION 17 — Schema mới `ApprovalWorkflowsV2` (Mig 22) + Menu "Duyệt NCC (Mới)" UAT (3 commit per-chunk)** — User chốt sau Session 16 drastic refactor: "Thấy vẫn không đúng, viết lại toàn bộ chỗ Quy trình Duyệt + thêm 1 Menu chỗ quy trình duyệt nữa là Duyệt NCC (Mới)" với cấu trúc explicit `Mã Quy trình - Tên Quy trình / * Bước 1 - Phòng A / * Cấp 1 - NV X / * Cấp 2 - NV Y`. Schema riêng UAT trước khi drop legacy. Khác Mig 21: mỗi Cấp = 1 NV CỤ THỂ qua `ApproverUserId` (KHÔNG OR-of-many). **Chunk A (`c847dc0`)** Domain `ApprovalWorkflowsV2/ApprovalWorkflow.cs` — 3 entity (ApprovalWorkflow + Step + Level) + enum `ApprovalWorkflowApplicableType` (DuyetNcc/DuyetNccPhuongAn/Contract). EF `ApprovalWorkflowConfiguration.cs` 3 IEntityTypeConfiguration: ToTable + UNIQUE (Code, Version) + FK Cascade Step→Workflow + FK Cascade Level→Step + FK Restrict Department + FK Restrict User. ApplicationDbContext 3 DbSet. **Migration 22** `AddApprovalWorkflowsV2` — 3 CREATE TABLE + 1 UNIQUE + 4 INDEX, applied cả `_Design` + `_Dev` LocalDB. DbInitializer SeedMenusAsync +2 menu row (`ApprovalWorkflowsV2` root dưới System icon Workflow + `AwV2_DuyetNcc` leaf icon FileCheck). MenuKeys.cs +2 const + All array. **Chunk B (`f6047d5`)** Application `ApprovalWorkflowsV2/ApprovalWorkflowV2AdminFeatures.cs` — 3 handler (GetAwAdminOverviewQuery filter optional ApplicableType, CreateAwDefinitionCommand auto-increment Version + deactivate active version cùng type, DeleteAwDefinitionCommand UAT helper unconditional) + Validator child rules NotEmpty Levels + ApproverUserId required. DTO AwDefinition/AwStep/AwLevel + AwTypeSummary. IApplicationDbContext +3 DbSet. Api Controller `ApprovalWorkflowsV2Controller` — GET ?applicableType=N | POST | DELETE/{id}, reuse policy Workflows.Read/Workflows.Create. **Chunk C (`2781c7e`)** FE-Admin `ApprovalWorkflowsV2Page.tsx` (~480 LOC) Designer mới mirror PE pattern nhưng theo schema V2: Overview cards Active+History per ApplicableType, DefinitionCard read-only render Bước → Cấp với approver name + email, Designer dialog Mã/Tên/Mô tả + reorder Step/Level (chevron up/down), Add/Remove Step + Level, Select Phòng + Select NV duyệt, validate level required NV. Auto-assign code QT-DN-V2-001 / QT-DN-PA-V2-001 / QT-HD-V2-001. Layout.tsx resolver +ApprovalWorkflowsV2/AwV2_<TypeCode>. App.tsx +2 route. menuKeys.ts +2 const sync BE. **KHÔNG đụng** PE/Contract Service (legacy WorkflowDefinition vẫn pin). Verify: dotnet build pass + 77 test pass (no regression) + npm build fe-admin pass. Backward compat 100% — schema mới chỉ thêm vào, không sửa cũ. **Next**: User UAT menu mới → confirm UX → Session sau migrate PE/Contract Service iterate ApprovalWorkflowSteps → drop legacy WorkflowDefinition. | `c847dc0` (A) · `f6047d5` (B) · `2781c7e` (C) · (current D docs) |
|
||||||
| 2026-05-08 | Claude | **🎯 SESSION 16 — DRASTIC REFACTOR flat workflow Phòng × Cấp (Mig 21, 2 commit Chunk A+B)** — Resume từ Session 15 defer plan. User chốt "bỏ phase enum hoàn toàn, dùng ChoDuyet=10 đơn nhất + currentStepIndex tracking". Per memory `feedback_drastic_refactor_scope`: dedicated session với context fresh, scope conservative 2x buffer (~8-10h estimate, actual ~3h). **Chunk A (`dbb0089`)** — Domain enum simplify (DangSoanThao=1, ChoDuyet=10 NEW, DaDuyet=7, TuChoi=99; legacy 2-6 + 98 deprecated giữ cho data cũ). WorkflowStep + DepartmentId Guid? FK Restrict + PositionLevel int? (PE + Contract mirror). PE/Contract entity + CurrentWorkflowStepIndex int? + RejectedAtStepIndex int?. Drop class WorkflowStepInnerStep + nav (PE + Contract). Drop *DepartmentApproval.InnerStepId column. EF Configurations: drop InnerStep config + restore simple unique non-filtered (Mig 19/20 filtered split reverse). DbContext drop DbSet<*WorkflowStepInnerStep> × 2. **Migration 21** `RefactorWorkflowToFlatModel` GỘP: 4 ALTER cols (PE/Contract CurrentStepIndex+RejectedAtStepIndex) + 2 ALTER (WorkflowStep DeptId+PositionLevel) + DROP TABLE x 2 (PEWorkflowStepInnerSteps + WorkflowStepInnerSteps Mig 18+20) + DROP InnerStepId column x 2 (PE+Contract DeptApproval) + DROP filtered indexes x 2 + restore simple unique x 2. PE + Contract Service rewrite TransitionAsync: phase transitions DangSoanThao→ChoDuyet (Drafter trình init idx=0) / ChoDuyet→ChoDuyet (advance idx) / ChoDuyet→DaDuyet/DaPhatHanh (last step done) / ChoDuyet→DangSoanThao (Trả lại save RejectedAtStepIndex) / ChoDuyet→TuChoi (Từ chối khoá vĩnh viễn). Match approver: actor.Dept==step.Dept AND actor.PositionLevel>=step.PositionLevel (OR cùng cấp/dept) OR Approvers.Kind=User match OR Kind=Role match. Admin role bypass policy. Last step done → gen mã HĐ (Contract only). App CQRS WorkflowStepDto + WorkflowStepInput drop InnerStep, add DepartmentId/DepartmentName/PositionLevel (PE + Contract mirror). Tests rewrite: DROP `PeNStageApprovalTests.cs` (6) + `ContractNStageApprovalTests.cs` (6) + `PeTwoStageApprovalTests.cs` (7) — legacy N-stage/2-stage no longer applicable. UPDATE `PeWorkflowAdminTests` signature. **96 → 77 test pass** (-19 legacy). 3-file rule Mig 21 (.cs + Designer + Snapshot) commit đủ. **Chunk B (`88a5be1`)** — FE-Admin Designer rewrite (PeWorkflowsPage + WorkflowsPage): drop InnerStepDto + EditInnerStep types, drop PHASE_OPTIONS auto-assign ChoDuyet=10, StepDto + EditStep + departmentId/positionLevel, copyFromDefinition simplified, Designer step UI rewrite (Tên + Phòng Select + Cấp Select + SLA + Approvers Role/User optional fallback, drop entire InnerSteps sub-section), DefinitionCard view hiển thị badge Phòng emerald + Cấp NV/PP/TP violet, save payload phase=10. types/purchaseEvaluation.ts (fe-admin + fe-user mirror) + ChoDuyet=10 enum + label "Đang duyệt" + color amber. **Chunk C (FE PeWorkflowPanel) SKIP** — existing UI compatible (workflow.nextPhases driven by BE simplified policy), reuse 3-button Trả lại/Từ chối logic Session 14 hoạt động trên ChoDuyet phase tự động. **KHÔNG đụng** Service Notify pattern + Changelog pattern (giữ hành vi Mig 16). Verify: dotnet build pass + Mig 21 LocalDB applied + 77 test pass + npm build × 2 pass. Memory `feedback_drastic_refactor_scope.md` validated: dedicated session approach hoạt động đúng dự đoán. | `dbb0089` (A) · `88a5be1` (B) |
|
| 2026-05-08 | Claude | **🎯 SESSION 16 — DRASTIC REFACTOR flat workflow Phòng × Cấp (Mig 21, 2 commit Chunk A+B)** — Resume từ Session 15 defer plan. User chốt "bỏ phase enum hoàn toàn, dùng ChoDuyet=10 đơn nhất + currentStepIndex tracking". Per memory `feedback_drastic_refactor_scope`: dedicated session với context fresh, scope conservative 2x buffer (~8-10h estimate, actual ~3h). **Chunk A (`dbb0089`)** — Domain enum simplify (DangSoanThao=1, ChoDuyet=10 NEW, DaDuyet=7, TuChoi=99; legacy 2-6 + 98 deprecated giữ cho data cũ). WorkflowStep + DepartmentId Guid? FK Restrict + PositionLevel int? (PE + Contract mirror). PE/Contract entity + CurrentWorkflowStepIndex int? + RejectedAtStepIndex int?. Drop class WorkflowStepInnerStep + nav (PE + Contract). Drop *DepartmentApproval.InnerStepId column. EF Configurations: drop InnerStep config + restore simple unique non-filtered (Mig 19/20 filtered split reverse). DbContext drop DbSet<*WorkflowStepInnerStep> × 2. **Migration 21** `RefactorWorkflowToFlatModel` GỘP: 4 ALTER cols (PE/Contract CurrentStepIndex+RejectedAtStepIndex) + 2 ALTER (WorkflowStep DeptId+PositionLevel) + DROP TABLE x 2 (PEWorkflowStepInnerSteps + WorkflowStepInnerSteps Mig 18+20) + DROP InnerStepId column x 2 (PE+Contract DeptApproval) + DROP filtered indexes x 2 + restore simple unique x 2. PE + Contract Service rewrite TransitionAsync: phase transitions DangSoanThao→ChoDuyet (Drafter trình init idx=0) / ChoDuyet→ChoDuyet (advance idx) / ChoDuyet→DaDuyet/DaPhatHanh (last step done) / ChoDuyet→DangSoanThao (Trả lại save RejectedAtStepIndex) / ChoDuyet→TuChoi (Từ chối khoá vĩnh viễn). Match approver: actor.Dept==step.Dept AND actor.PositionLevel>=step.PositionLevel (OR cùng cấp/dept) OR Approvers.Kind=User match OR Kind=Role match. Admin role bypass policy. Last step done → gen mã HĐ (Contract only). App CQRS WorkflowStepDto + WorkflowStepInput drop InnerStep, add DepartmentId/DepartmentName/PositionLevel (PE + Contract mirror). Tests rewrite: DROP `PeNStageApprovalTests.cs` (6) + `ContractNStageApprovalTests.cs` (6) + `PeTwoStageApprovalTests.cs` (7) — legacy N-stage/2-stage no longer applicable. UPDATE `PeWorkflowAdminTests` signature. **96 → 77 test pass** (-19 legacy). 3-file rule Mig 21 (.cs + Designer + Snapshot) commit đủ. **Chunk B (`88a5be1`)** — FE-Admin Designer rewrite (PeWorkflowsPage + WorkflowsPage): drop InnerStepDto + EditInnerStep types, drop PHASE_OPTIONS auto-assign ChoDuyet=10, StepDto + EditStep + departmentId/positionLevel, copyFromDefinition simplified, Designer step UI rewrite (Tên + Phòng Select + Cấp Select + SLA + Approvers Role/User optional fallback, drop entire InnerSteps sub-section), DefinitionCard view hiển thị badge Phòng emerald + Cấp NV/PP/TP violet, save payload phase=10. types/purchaseEvaluation.ts (fe-admin + fe-user mirror) + ChoDuyet=10 enum + label "Đang duyệt" + color amber. **Chunk C (FE PeWorkflowPanel) SKIP** — existing UI compatible (workflow.nextPhases driven by BE simplified policy), reuse 3-button Trả lại/Từ chối logic Session 14 hoạt động trên ChoDuyet phase tự động. **KHÔNG đụng** Service Notify pattern + Changelog pattern (giữ hành vi Mig 16). Verify: dotnet build pass + Mig 21 LocalDB applied + 77 test pass + npm build × 2 pass. Memory `feedback_drastic_refactor_scope.md` validated: dedicated session approach hoạt động đúng dự đoán. | `dbb0089` (A) · `88a5be1` (B) |
|
||||||
| 2026-05-07 | Claude | **🎯 SESSION 15 — Tooltip diagnose "Lưu & Gửi Duyệt" + Plan drastic refactor flat workflow → DEFER** — User UAT live screenshot phiếu PE Bản nháp + báo "Lưu & Gửi Duyệt" KHÔNG hoạt động + suy đoán "trùng ID với phiếu khác". Chẩn đoán: button silent disabled khi `evaluation.workflow.nextPhases` không có forward phase (chỉ TuChoi/TraLai). FE chưa có visual feedback → user không biết. Improvement (commit `835cc7f`): compute `forwardPhase` once + add `submitDisabledReason` string giải thích reason (canEditPhase=false / readOnly / !forwardPhase với hint admin kiểm tra cấu hình quy trình) + button title attribute show reason hover hoặc forward phase label khi enabled + Dialog confirm show forward phase explicit "Sẽ chuyển sang Chờ Purchasing". Mirror fe-admin + fe-user. Build pass cả 2. **"Trùng ID" KHÔNG phải bug FE** — `PurchaseEvaluationWorkspacePage` URL state đúng (`+ Thêm mới` clear `id`, save set new), mỗi PE row unique GUID + MaPhieu. **Tiếp theo plan drastic refactor**: User chốt "bỏ phase enum hoàn toàn, dùng ChoDuyet=10 đơn nhất + currentStepIndex tracking" + workflow flat list (Phòng × Cấp × Users[]) thay InnerStep model. Surface 6 chunk plan + start Chunk A: edit Domain entities (Phase enum +ChoDuyet=10, WorkflowStep +DeptId/PositionLevel, drop InnerStep class+nav, PE/Contract +CurrentWorkflowStepIndex/RejectedAtStepIndex, *DeptApproval drop InnerStepId) + EF Configurations (drop InnerStep config + nav, restore simple unique non-filtered) + DbContext drop DbSets — 12 files trong working tree. Realize scope realistic ~8-10h (PolicyRegistry rewrite + 2 Service rewrite + App CQRS + 12 tests rewrite + Designer FE + Migration 21 + Docs) vượt session boundary + risk session context deep ~30 commits. **REVERT working tree** về `835cc7f` clean. Add memory `feedback_drastic_refactor_scope` decision rule: drastic refactor cần dedicated session, ước tính conservative (2x buffer), tránh mid-session big refactor. **Stats unchanged**: 96 test pass, 20 mig, 57 bảng. | `835cc7f` |
|
| 2026-05-07 | Claude | **🎯 SESSION 15 — Tooltip diagnose "Lưu & Gửi Duyệt" + Plan drastic refactor flat workflow → DEFER** — User UAT live screenshot phiếu PE Bản nháp + báo "Lưu & Gửi Duyệt" KHÔNG hoạt động + suy đoán "trùng ID với phiếu khác". Chẩn đoán: button silent disabled khi `evaluation.workflow.nextPhases` không có forward phase (chỉ TuChoi/TraLai). FE chưa có visual feedback → user không biết. Improvement (commit `835cc7f`): compute `forwardPhase` once + add `submitDisabledReason` string giải thích reason (canEditPhase=false / readOnly / !forwardPhase với hint admin kiểm tra cấu hình quy trình) + button title attribute show reason hover hoặc forward phase label khi enabled + Dialog confirm show forward phase explicit "Sẽ chuyển sang Chờ Purchasing". Mirror fe-admin + fe-user. Build pass cả 2. **"Trùng ID" KHÔNG phải bug FE** — `PurchaseEvaluationWorkspacePage` URL state đúng (`+ Thêm mới` clear `id`, save set new), mỗi PE row unique GUID + MaPhieu. **Tiếp theo plan drastic refactor**: User chốt "bỏ phase enum hoàn toàn, dùng ChoDuyet=10 đơn nhất + currentStepIndex tracking" + workflow flat list (Phòng × Cấp × Users[]) thay InnerStep model. Surface 6 chunk plan + start Chunk A: edit Domain entities (Phase enum +ChoDuyet=10, WorkflowStep +DeptId/PositionLevel, drop InnerStep class+nav, PE/Contract +CurrentWorkflowStepIndex/RejectedAtStepIndex, *DeptApproval drop InnerStepId) + EF Configurations (drop InnerStep config + nav, restore simple unique non-filtered) + DbContext drop DbSets — 12 files trong working tree. Realize scope realistic ~8-10h (PolicyRegistry rewrite + 2 Service rewrite + App CQRS + 12 tests rewrite + Designer FE + Migration 21 + Docs) vượt session boundary + risk session context deep ~30 commits. **REVERT working tree** về `835cc7f` clean. Add memory `feedback_drastic_refactor_scope` decision rule: drastic refactor cần dedicated session, ước tính conservative (2x buffer), tránh mid-session big refactor. **Stats unchanged**: 96 test pass, 20 mig, 57 bảng. | `835cc7f` |
|
||||||
| 2026-05-07 | Claude | **🎯 SESSION 14 — PE 3-button workflow Duyệt/Trả lại/Từ chối + Task 2 sample seed in-progress** — User chỉ thị thay 2-button approval bằng 3 hành động rõ ràng cho approver: **Duyệt** (forward), **Trả lại** (về DangSoanThao + Drafter sửa, smart reject Mig 16 + clear N-stage rows + Drafter resume jump-back), **Từ chối** (Phase=TuChoi, phiếu khoá vĩnh viễn 17 handler Mig 16 lock edit, Drafter phải tạo phiếu mới). 1 commit (`0d77698`): Domain `PurchaseEvaluationPolicy.cs` NccOnly + NccWithPlan thêm `(X → TuChoi)` transition cho mọi phase trung gian (ChoPurchasing/ChoCCM/ChoDuAn/ChoCEODuyetPA/ChoCEODuyetNCC) với roles của phase. FromDefinition expand: mỗi step (trừ DangSoanThao) thêm (step.Phase → TuChoi) với roles step. Service `PurchaseEvaluationWorkflowService.TransitionAsync` — Reject branch tách 2 case: target=TuChoi giữ nguyên (KHÔNG override + KHÔNG set RejectedFromPhase + KHÔNG clear N-stage); target khác (DangSoanThao) → smart reject (force DangSoanThao + RejectedFromPhase + clear N-stage). FE PeWorkflowPanel (admin + user mirror): render 3 button rõ ràng "✓ Duyệt → X" brand / "← Trả lại (về Drafter sửa)" red / "✗ Hủy / Từ chối" red. Decision logic: target=TuChoi || isSendBack → Reject (2), else Approve (1). Dialog confirm: title rõ + Cancel case warning red "phiếu sẽ bị khoá hoàn toàn" + SendBack case hint amber "Phiếu về DangSoanThao, Drafter sửa rồi trình lại — workflow tự jump tới phase này". Tests: rename `Reject_Sets_RejectedFromPhase_And_Forces_DangSoanThao` → `Reject_To_DangSoanThao_Sets_RejectedFromPhase_TraLai` (target từ TuChoi → DangSoanThao). NEW `Reject_To_TuChoi_Locks_Permanently_No_RejectedFromPhase`. Update `NStage_Reject_Clears_InnerStep_Rows_At_Phase` target → DangSoanThao. **95 → 96 test pass** (+1 Từ chối). **Task 2 sample seed in-progress**: dotnet ef database update applied Mig 9-20 lên LocalDB SolutionErp_Dev (trước đó chỉ Mig 1-8 vì DesignTimeDbContextFactory hardcoded SolutionErp_Design ≠ runtime SolutionErp_Dev — gotcha tooling distinction). API start để DbInitializer auto-seed 30 demo user nhưng exit 255 sớm khi log buffer full → seed dở dang. Defer Task 2 cho session sau (cần guidance: seed manual SQL hoặc manual API run + sample N-stage workflow def + Update PositionLevel cho 30 users existing). | `0d77698` |
|
| 2026-05-07 | Claude | **🎯 SESSION 14 — PE 3-button workflow Duyệt/Trả lại/Từ chối + Task 2 sample seed in-progress** — User chỉ thị thay 2-button approval bằng 3 hành động rõ ràng cho approver: **Duyệt** (forward), **Trả lại** (về DangSoanThao + Drafter sửa, smart reject Mig 16 + clear N-stage rows + Drafter resume jump-back), **Từ chối** (Phase=TuChoi, phiếu khoá vĩnh viễn 17 handler Mig 16 lock edit, Drafter phải tạo phiếu mới). 1 commit (`0d77698`): Domain `PurchaseEvaluationPolicy.cs` NccOnly + NccWithPlan thêm `(X → TuChoi)` transition cho mọi phase trung gian (ChoPurchasing/ChoCCM/ChoDuAn/ChoCEODuyetPA/ChoCEODuyetNCC) với roles của phase. FromDefinition expand: mỗi step (trừ DangSoanThao) thêm (step.Phase → TuChoi) với roles step. Service `PurchaseEvaluationWorkflowService.TransitionAsync` — Reject branch tách 2 case: target=TuChoi giữ nguyên (KHÔNG override + KHÔNG set RejectedFromPhase + KHÔNG clear N-stage); target khác (DangSoanThao) → smart reject (force DangSoanThao + RejectedFromPhase + clear N-stage). FE PeWorkflowPanel (admin + user mirror): render 3 button rõ ràng "✓ Duyệt → X" brand / "← Trả lại (về Drafter sửa)" red / "✗ Hủy / Từ chối" red. Decision logic: target=TuChoi || isSendBack → Reject (2), else Approve (1). Dialog confirm: title rõ + Cancel case warning red "phiếu sẽ bị khoá hoàn toàn" + SendBack case hint amber "Phiếu về DangSoanThao, Drafter sửa rồi trình lại — workflow tự jump tới phase này". Tests: rename `Reject_Sets_RejectedFromPhase_And_Forces_DangSoanThao` → `Reject_To_DangSoanThao_Sets_RejectedFromPhase_TraLai` (target từ TuChoi → DangSoanThao). NEW `Reject_To_TuChoi_Locks_Permanently_No_RejectedFromPhase`. Update `NStage_Reject_Clears_InnerStep_Rows_At_Phase` target → DangSoanThao. **95 → 96 test pass** (+1 Từ chối). **Task 2 sample seed in-progress**: dotnet ef database update applied Mig 9-20 lên LocalDB SolutionErp_Dev (trước đó chỉ Mig 1-8 vì DesignTimeDbContextFactory hardcoded SolutionErp_Design ≠ runtime SolutionErp_Dev — gotcha tooling distinction). API start để DbInitializer auto-seed 30 demo user nhưng exit 255 sớm khi log buffer full → seed dở dang. Defer Task 2 cho session sau (cần guidance: seed manual SQL hoặc manual API run + sample N-stage workflow def + Update PositionLevel cho 30 users existing). | `0d77698` |
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import { PermissionsPage } from '@/pages/system/PermissionsPage'
|
|||||||
import { RolesPage } from '@/pages/system/RolesPage'
|
import { RolesPage } from '@/pages/system/RolesPage'
|
||||||
import { WorkflowsPage } from '@/pages/system/WorkflowsPage'
|
import { WorkflowsPage } from '@/pages/system/WorkflowsPage'
|
||||||
import { PeWorkflowsPage } from '@/pages/system/PeWorkflowsPage'
|
import { PeWorkflowsPage } from '@/pages/system/PeWorkflowsPage'
|
||||||
|
import { ApprovalWorkflowsV2Page } from '@/pages/system/ApprovalWorkflowsV2Page'
|
||||||
import { FormsPage } from '@/pages/forms/FormsPage'
|
import { FormsPage } from '@/pages/forms/FormsPage'
|
||||||
import { ContractsListPage } from '@/pages/contracts/ContractsListPage'
|
import { ContractsListPage } from '@/pages/contracts/ContractsListPage'
|
||||||
import { ContractDetailPage } from '@/pages/contracts/ContractDetailPage'
|
import { ContractDetailPage } from '@/pages/contracts/ContractDetailPage'
|
||||||
@ -51,6 +52,9 @@ function App() {
|
|||||||
<Route path="/system/workflows/:typeCode" element={<WorkflowsPage />} />
|
<Route path="/system/workflows/:typeCode" element={<WorkflowsPage />} />
|
||||||
<Route path="/system/pe-workflows" element={<PeWorkflowsPage />} />
|
<Route path="/system/pe-workflows" element={<PeWorkflowsPage />} />
|
||||||
<Route path="/system/pe-workflows/:typeCode" element={<PeWorkflowsPage />} />
|
<Route path="/system/pe-workflows/:typeCode" element={<PeWorkflowsPage />} />
|
||||||
|
{/* Quy trình duyệt MỚI (Mig 22 — UAT) */}
|
||||||
|
<Route path="/system/approval-workflows-v2" element={<ApprovalWorkflowsV2Page />} />
|
||||||
|
<Route path="/system/approval-workflows-v2/:typeCode" element={<ApprovalWorkflowsV2Page />} />
|
||||||
<Route path="/forms" element={<FormsPage />} />
|
<Route path="/forms" element={<FormsPage />} />
|
||||||
<Route path="/contracts" element={<ContractsListPage />} />
|
<Route path="/contracts" element={<ContractsListPage />} />
|
||||||
<Route path="/contracts/new" element={<ContractCreatePage />} />
|
<Route path="/contracts/new" element={<ContractCreatePage />} />
|
||||||
|
|||||||
@ -91,6 +91,17 @@ function resolvePath(key: string): string | null {
|
|||||||
if (code === 'DuyetNcc' || code === 'DuyetNccPhuongAn') return `/system/pe-workflows/${code}`
|
if (code === 'DuyetNcc' || code === 'DuyetNccPhuongAn') return `/system/pe-workflows/${code}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Quy trình duyệt MỚI (Mig 22 — Session 17): root = group bowed, leaf =
|
||||||
|
// type-specific designer. Sau UAT thay thế PeWorkflows + Workflows cũ.
|
||||||
|
if (key === 'ApprovalWorkflowsV2') return '/system/approval-workflows-v2'
|
||||||
|
const awV2Match = key.match(/^AwV2_(.+)$/)
|
||||||
|
if (awV2Match) {
|
||||||
|
const code = awV2Match[1]
|
||||||
|
if (code === 'DuyetNcc' || code === 'DuyetNccPhuongAn' || code === 'Contract') {
|
||||||
|
return `/system/approval-workflows-v2/${code}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,9 @@ export const MenuKeys = {
|
|||||||
Permissions: 'Permissions',
|
Permissions: 'Permissions',
|
||||||
PurchaseEvaluations: 'PurchaseEvaluations',
|
PurchaseEvaluations: 'PurchaseEvaluations',
|
||||||
PeWorkflows: 'PeWorkflows',
|
PeWorkflows: 'PeWorkflows',
|
||||||
|
// Quy trình duyệt MỚI (Mig 22 — Session 17, 2026-05-08)
|
||||||
|
ApprovalWorkflowsV2: 'ApprovalWorkflowsV2',
|
||||||
|
AwV2_DuyetNcc: 'AwV2_DuyetNcc',
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type MenuKey = typeof MenuKeys[keyof typeof MenuKeys]
|
export type MenuKey = typeof MenuKeys[keyof typeof MenuKeys]
|
||||||
|
|||||||
638
fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx
Normal file
638
fe-admin/src/pages/system/ApprovalWorkflowsV2Page.tsx
Normal file
@ -0,0 +1,638 @@
|
|||||||
|
// Quy trình duyệt MỚI (Mig 22 — Session 17, 2026-05-08).
|
||||||
|
// Schema riêng UAT trước khi drop legacy. Cấu trúc:
|
||||||
|
// Quy trình (Mã + Tên + ApplicableType)
|
||||||
|
// Bước 1 — Phòng A
|
||||||
|
// Cấp 1 — NV X (1 user CỤ THỂ qua ApproverUserId)
|
||||||
|
// Cấp 2 — NV Y
|
||||||
|
//
|
||||||
|
// Khác Designer cũ (PE workflow): Levels match 1 NV chính xác (KHÔNG OR-of-many).
|
||||||
|
import { useMemo, useState, type FormEvent } from 'react'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { GitBranch, Plus, Trash2, CheckCircle2, History, Workflow, ChevronUp, ChevronDown } from 'lucide-react'
|
||||||
|
import { toast } from 'sonner'
|
||||||
|
import { PageHeader } from '@/components/PageHeader'
|
||||||
|
import { Button } from '@/components/ui/Button'
|
||||||
|
import { Dialog } from '@/components/ui/Dialog'
|
||||||
|
import { Input } from '@/components/ui/Input'
|
||||||
|
import { Label } from '@/components/ui/Label'
|
||||||
|
import { Select } from '@/components/ui/Select'
|
||||||
|
import { Textarea } from '@/components/ui/Textarea'
|
||||||
|
import { api } from '@/lib/api'
|
||||||
|
import { getErrorMessage } from '@/lib/apiError'
|
||||||
|
import type { Department, Paged } from '@/types/master'
|
||||||
|
|
||||||
|
// ===== Types (mirror BE AwAdminOverviewDto) =====
|
||||||
|
|
||||||
|
type LevelDto = {
|
||||||
|
id: string
|
||||||
|
order: number
|
||||||
|
name: string | null
|
||||||
|
approverUserId: string
|
||||||
|
approverUserName: string | null
|
||||||
|
approverEmail: string | null
|
||||||
|
}
|
||||||
|
type StepDto = {
|
||||||
|
id: string
|
||||||
|
order: number
|
||||||
|
name: string // "Phòng A" — display
|
||||||
|
departmentId: string | null
|
||||||
|
departmentName: string | null
|
||||||
|
levels: LevelDto[]
|
||||||
|
}
|
||||||
|
type DefinitionDto = {
|
||||||
|
id: string
|
||||||
|
code: string
|
||||||
|
version: number
|
||||||
|
applicableType: number
|
||||||
|
applicableTypeLabel: string
|
||||||
|
name: string
|
||||||
|
description: string | null
|
||||||
|
isActive: boolean
|
||||||
|
activatedAt: string | null
|
||||||
|
createdAt: string
|
||||||
|
steps: StepDto[]
|
||||||
|
}
|
||||||
|
type TypeSummaryDto = {
|
||||||
|
applicableType: number
|
||||||
|
applicableTypeLabel: string
|
||||||
|
active: DefinitionDto | null
|
||||||
|
history: DefinitionDto[]
|
||||||
|
}
|
||||||
|
|
||||||
|
type EditLevel = { name: string; approverUserId: string }
|
||||||
|
type EditStep = { name: string; departmentId: string | null; levels: EditLevel[] }
|
||||||
|
|
||||||
|
// FE typeCode → BE int (giống MenuKeys ApplicableType)
|
||||||
|
const TYPE_CODE_TO_INT: Record<string, number> = {
|
||||||
|
DuyetNcc: 1,
|
||||||
|
DuyetNccPhuongAn: 2,
|
||||||
|
Contract: 3,
|
||||||
|
}
|
||||||
|
const DEFAULT_CODE_BY_TYPE: Record<number, string> = {
|
||||||
|
1: 'QT-DN-V2-001',
|
||||||
|
2: 'QT-DN-PA-V2-001',
|
||||||
|
3: 'QT-HD-V2-001',
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyFromDefinition(d: DefinitionDto): EditStep[] {
|
||||||
|
return d.steps.map(s => ({
|
||||||
|
name: s.name,
|
||||||
|
departmentId: s.departmentId,
|
||||||
|
levels: s.levels.map(l => ({ name: l.name ?? '', approverUserId: l.approverUserId })),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApprovalWorkflowsV2Page() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
const { typeCode } = useParams<{ typeCode?: string }>()
|
||||||
|
const selectedTypeInt = typeCode ? TYPE_CODE_TO_INT[typeCode] : null
|
||||||
|
|
||||||
|
const overview = useQuery({
|
||||||
|
queryKey: ['approval-workflow-v2-overview', selectedTypeInt],
|
||||||
|
queryFn: async () => {
|
||||||
|
const params = selectedTypeInt ? { applicableType: selectedTypeInt } : {}
|
||||||
|
return (await api.get<{ types: TypeSummaryDto[] }>('/approval-workflows-v2', { params })).data
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentType = selectedTypeInt
|
||||||
|
? overview.data?.types.find(t => t.applicableType === selectedTypeInt)
|
||||||
|
: null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<PageHeader
|
||||||
|
title={
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<Workflow className="h-5 w-5" />
|
||||||
|
{currentType
|
||||||
|
? `Quy trình duyệt (Mới): ${currentType.applicableTypeLabel}`
|
||||||
|
: 'Quy trình duyệt (Mới)'}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
description={
|
||||||
|
currentType
|
||||||
|
? 'Mỗi Bước = 1 Phòng. Mỗi Cấp trong Bước = 1 nhân viên cụ thể duyệt. Tuần tự: Cấp 1 → Cấp 2 → ... → Bước kế.'
|
||||||
|
: 'Schema mới UAT — chọn loại quy trình từ menu bên trái.'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{overview.isLoading && <div className="text-sm text-slate-500">Đang tải…</div>}
|
||||||
|
|
||||||
|
{overview.data && !currentType && (
|
||||||
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{overview.data.types.map(t => (
|
||||||
|
<div key={t.applicableType} className="rounded-xl border border-slate-200 bg-white p-4 shadow-sm">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-800">{t.applicableTypeLabel}</h3>
|
||||||
|
{t.active && (
|
||||||
|
<span className="rounded bg-brand-50 px-2 py-0.5 font-mono text-[10px] font-medium text-brand-700">
|
||||||
|
{t.active.code} v{String(t.active.version).padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-xs text-slate-500">
|
||||||
|
{t.active
|
||||||
|
? `${t.active.steps.length} bước · ${t.active.steps.reduce((s, x) => s + x.levels.length, 0)} cấp · ${t.history.length} version`
|
||||||
|
: 'Chưa có quy trình'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentType && (
|
||||||
|
<TypePanel
|
||||||
|
type={currentType}
|
||||||
|
onSaved={() => qc.invalidateQueries({ queryKey: ['approval-workflow-v2-overview'] })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Per-type panel =====
|
||||||
|
|
||||||
|
function TypePanel({ type, onSaved }: { type: TypeSummaryDto; onSaved: () => void }) {
|
||||||
|
const [designerOpen, setDesignerOpen] = useState(false)
|
||||||
|
const [cloneFrom, setCloneFrom] = useState<DefinitionDto | null>(null)
|
||||||
|
const qc = useQueryClient()
|
||||||
|
|
||||||
|
const del = useMutation({
|
||||||
|
mutationFn: async (id: string) => api.delete(`/approval-workflows-v2/${id}`),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Đã xoá version')
|
||||||
|
qc.invalidateQueries({ queryKey: ['approval-workflow-v2-overview'] })
|
||||||
|
},
|
||||||
|
onError: err => toast.error(getErrorMessage(err)),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{type.active ? (
|
||||||
|
<DefinitionCard
|
||||||
|
def={type.active}
|
||||||
|
isActive
|
||||||
|
onClone={d => { setCloneFrom(d); setDesignerOpen(true) }}
|
||||||
|
onDelete={() => {
|
||||||
|
if (confirm(`Xoá version đang áp dụng "${type.active!.code} v${type.active!.version}"?`)) {
|
||||||
|
del.mutate(type.active!.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-xl border border-dashed border-slate-300 p-8 text-center text-sm text-slate-500">
|
||||||
|
Chưa có quy trình cho loại này. Tạo version đầu tiên bên dưới.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-sm font-semibold text-slate-700">Lịch sử versions</h3>
|
||||||
|
<Button onClick={() => { setCloneFrom(type.active); setDesignerOpen(true) }}>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Tạo quy trình mới
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{type.history.filter(d => !d.isActive).length === 0 && (
|
||||||
|
<div className="rounded-md border border-slate-200 bg-slate-50 p-4 text-xs text-slate-500">
|
||||||
|
Chưa có version cũ. Khi tạo version mới, version hiện tại tự động archive.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
{type.history.filter(d => !d.isActive).map(d => (
|
||||||
|
<DefinitionCard
|
||||||
|
key={d.id}
|
||||||
|
def={d}
|
||||||
|
isActive={false}
|
||||||
|
onClone={dd => { setCloneFrom(dd); setDesignerOpen(true) }}
|
||||||
|
onDelete={() => {
|
||||||
|
if (confirm(`Xoá version "${d.code} v${d.version}"?`)) del.mutate(d.id)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{designerOpen && (
|
||||||
|
<Designer
|
||||||
|
applicableType={type.applicableType}
|
||||||
|
applicableTypeLabel={type.applicableTypeLabel}
|
||||||
|
cloneFrom={cloneFrom}
|
||||||
|
onClose={() => { setDesignerOpen(false); setCloneFrom(null) }}
|
||||||
|
onSaved={() => { setDesignerOpen(false); setCloneFrom(null); onSaved() }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Definition card (read-only) =====
|
||||||
|
|
||||||
|
function DefinitionCard({
|
||||||
|
def,
|
||||||
|
isActive,
|
||||||
|
onClone,
|
||||||
|
onDelete,
|
||||||
|
}: {
|
||||||
|
def: DefinitionDto
|
||||||
|
isActive: boolean
|
||||||
|
onClone: (d: DefinitionDto) => void
|
||||||
|
onDelete: () => void
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className={`rounded-xl border bg-white p-5 shadow-sm ${isActive ? 'border-brand-200' : 'border-slate-200'}`}>
|
||||||
|
<div className="flex items-start justify-between gap-4">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="text-[15px] font-semibold text-slate-900">{def.name}</h3>
|
||||||
|
<span className="rounded bg-slate-100 px-2 py-0.5 font-mono text-[11px] text-slate-600">
|
||||||
|
{def.code} v{String(def.version).padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
{isActive ? (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full bg-emerald-100 px-2 py-0.5 text-[10px] font-medium text-emerald-700">
|
||||||
|
<CheckCircle2 className="h-3 w-3" />
|
||||||
|
Đang áp dụng
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-medium text-slate-600">
|
||||||
|
<History className="h-3 w-3" />
|
||||||
|
Archived
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{def.description && <p className="mt-1 text-xs leading-relaxed text-slate-500">{def.description}</p>}
|
||||||
|
|
||||||
|
<ol className="mt-3 space-y-2">
|
||||||
|
{def.steps.map(s => (
|
||||||
|
<li key={s.id} className="rounded-lg border border-slate-100 bg-slate-50/30 p-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-brand-600 text-[11px] font-bold text-white">
|
||||||
|
{s.order}
|
||||||
|
</div>
|
||||||
|
<span className="text-sm font-medium text-slate-800">Bước {s.order} — {s.name}</span>
|
||||||
|
{s.departmentName && (
|
||||||
|
<span className="rounded bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium text-emerald-700">
|
||||||
|
{s.departmentName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ul className="mt-2 ml-9 space-y-1">
|
||||||
|
{s.levels.length === 0 ? (
|
||||||
|
<li className="text-[11px] italic text-slate-400">Chưa có cấp duyệt</li>
|
||||||
|
) : (
|
||||||
|
s.levels.map(l => (
|
||||||
|
<li key={l.id} className="flex items-center gap-2 text-xs">
|
||||||
|
<span className="rounded-full bg-violet-100 px-2 py-0.5 font-mono text-[10px] font-bold text-violet-700">
|
||||||
|
C{l.order}
|
||||||
|
</span>
|
||||||
|
<span className="text-slate-700">{l.name || `Cấp ${l.order}`}</span>
|
||||||
|
<span className="text-slate-400">—</span>
|
||||||
|
<span className="font-medium text-slate-800">
|
||||||
|
{l.approverUserName ?? l.approverUserId}
|
||||||
|
</span>
|
||||||
|
{l.approverEmail && (
|
||||||
|
<span className="text-[10px] text-slate-400">({l.approverEmail})</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Button variant="outline" size="sm" onClick={() => onClone(def)}>
|
||||||
|
<Plus className="h-3.5 w-3.5" />
|
||||||
|
Tạo từ bản này
|
||||||
|
</Button>
|
||||||
|
<Button variant="outline" size="sm" onClick={onDelete}>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
Xoá version
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Designer dialog =====
|
||||||
|
|
||||||
|
function Designer({
|
||||||
|
applicableType,
|
||||||
|
applicableTypeLabel,
|
||||||
|
cloneFrom,
|
||||||
|
onClose,
|
||||||
|
onSaved,
|
||||||
|
}: {
|
||||||
|
applicableType: number
|
||||||
|
applicableTypeLabel: string
|
||||||
|
cloneFrom: DefinitionDto | null
|
||||||
|
onClose: () => void
|
||||||
|
onSaved: () => void
|
||||||
|
}) {
|
||||||
|
const initialSteps: EditStep[] = useMemo(
|
||||||
|
() =>
|
||||||
|
cloneFrom
|
||||||
|
? copyFromDefinition(cloneFrom)
|
||||||
|
: [{ name: 'Phòng 1', departmentId: null, levels: [{ name: 'Cấp 1', approverUserId: '' }] }],
|
||||||
|
[cloneFrom],
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultCode = DEFAULT_CODE_BY_TYPE[applicableType] ?? 'QT-V2-001'
|
||||||
|
const [code, setCode] = useState(cloneFrom?.code ?? defaultCode)
|
||||||
|
const [name, setName] = useState(cloneFrom ? `${cloneFrom.name} (clone)` : `Quy trình ${applicableTypeLabel}`)
|
||||||
|
const [description, setDescription] = useState(cloneFrom?.description ?? '')
|
||||||
|
const [steps, setSteps] = useState<EditStep[]>(initialSteps)
|
||||||
|
|
||||||
|
const usersList = useQuery({
|
||||||
|
queryKey: ['users-for-approver-v2'],
|
||||||
|
queryFn: async () =>
|
||||||
|
(await api.get<{ items: { id: string; fullName: string; email: string }[] }>('/users', {
|
||||||
|
params: { page: 1, pageSize: 200 },
|
||||||
|
})).data.items,
|
||||||
|
})
|
||||||
|
|
||||||
|
const departmentsList = useQuery({
|
||||||
|
queryKey: ['departments-list-v2'],
|
||||||
|
queryFn: async () =>
|
||||||
|
(await api.get<Paged<Department>>('/departments', { params: { page: 1, pageSize: 200 } })).data.items,
|
||||||
|
})
|
||||||
|
|
||||||
|
const save = useMutation({
|
||||||
|
mutationFn: async () => {
|
||||||
|
// Validate có user trong tất cả Cấp
|
||||||
|
for (const s of steps) {
|
||||||
|
if (s.levels.length === 0) throw new Error(`Bước "${s.name}" chưa có cấp duyệt nào.`)
|
||||||
|
for (const l of s.levels) {
|
||||||
|
if (!l.approverUserId) throw new Error(`Bước "${s.name}" có cấp chưa chọn nhân viên duyệt.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await api.post('/approval-workflows-v2', {
|
||||||
|
applicableType,
|
||||||
|
code,
|
||||||
|
name,
|
||||||
|
description: description || null,
|
||||||
|
steps: steps.map((s, i) => ({
|
||||||
|
order: i + 1,
|
||||||
|
name: s.name,
|
||||||
|
departmentId: s.departmentId,
|
||||||
|
levels: s.levels.map((l, j) => ({
|
||||||
|
order: j + 1,
|
||||||
|
name: l.name || null,
|
||||||
|
approverUserId: l.approverUserId,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('Đã lưu quy trình mới. Version cũ đã archive.')
|
||||||
|
onSaved()
|
||||||
|
},
|
||||||
|
onError: err => toast.error(getErrorMessage(err)),
|
||||||
|
})
|
||||||
|
|
||||||
|
function submit(e: FormEvent) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (steps.length === 0) {
|
||||||
|
toast.error('Phải có ít nhất 1 bước')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
save.mutate()
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveStep(idx: number, dir: -1 | 1) {
|
||||||
|
const newIdx = idx + dir
|
||||||
|
if (newIdx < 0 || newIdx >= steps.length) return
|
||||||
|
const next = [...steps]
|
||||||
|
;[next[idx], next[newIdx]] = [next[newIdx], next[idx]]
|
||||||
|
setSteps(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveLevel(stepIdx: number, levelIdx: number, dir: -1 | 1) {
|
||||||
|
const step = steps[stepIdx]
|
||||||
|
const newIdx = levelIdx + dir
|
||||||
|
if (newIdx < 0 || newIdx >= step.levels.length) return
|
||||||
|
const next = [...step.levels]
|
||||||
|
;[next[levelIdx], next[newIdx]] = [next[newIdx], next[levelIdx]]
|
||||||
|
setSteps(steps.map((x, i) => (i === stepIdx ? { ...x, levels: next } : x)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open
|
||||||
|
onClose={onClose}
|
||||||
|
title={`Tạo quy trình mới — ${applicableTypeLabel}`}
|
||||||
|
size="lg"
|
||||||
|
footer={
|
||||||
|
<>
|
||||||
|
<Button variant="outline" onClick={onClose}>Hủy</Button>
|
||||||
|
<Button onClick={submit} disabled={save.isPending} form="aw-v2-form">
|
||||||
|
{save.isPending ? 'Đang lưu…' : 'Lưu + kích hoạt'}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<form id="aw-v2-form" onSubmit={submit} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Mã quy trình *</Label>
|
||||||
|
<Input value={code} onChange={e => setCode(e.target.value)} required className="font-mono" />
|
||||||
|
<div className="text-[11px] text-slate-400">Vd QT-DN-V2-001. Version auto-tăng mỗi lần lưu.</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Tên hiển thị *</Label>
|
||||||
|
<Input value={name} onChange={e => setName(e.target.value)} required />
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2 space-y-1.5">
|
||||||
|
<Label>Mô tả</Label>
|
||||||
|
<Textarea rows={2} value={description} onChange={e => setDescription(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 rounded-lg border border-slate-200 p-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Label>Các bước duyệt — mỗi bước = 1 Phòng × N cấp NV ({steps.length} bước)</Label>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setSteps([...steps, {
|
||||||
|
name: `Phòng ${steps.length + 1}`,
|
||||||
|
departmentId: departmentsList.data?.[0]?.id ?? null,
|
||||||
|
levels: [{ name: 'Cấp 1', approverUserId: usersList.data?.[0]?.id ?? '' }],
|
||||||
|
}])}
|
||||||
|
>
|
||||||
|
<Plus className="h-3.5 w-3.5" />
|
||||||
|
Thêm bước
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{steps.map((s, idx) => (
|
||||||
|
<div key={idx} className="rounded-md border border-slate-200 bg-slate-50/40 p-3">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<div className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-brand-600 text-[11px] font-bold text-white">
|
||||||
|
{idx + 1}
|
||||||
|
</div>
|
||||||
|
<div className="grid flex-1 grid-cols-3 gap-2">
|
||||||
|
<div>
|
||||||
|
<Label className="text-[11px]">Tên bước</Label>
|
||||||
|
<Input
|
||||||
|
value={s.name}
|
||||||
|
onChange={e => setSteps(steps.map((x, i) => (i === idx ? { ...x, name: e.target.value } : x)))}
|
||||||
|
placeholder="Phòng A"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">
|
||||||
|
<Label className="text-[11px]">Phòng (hint hiển thị)</Label>
|
||||||
|
<Select
|
||||||
|
value={s.departmentId ?? ''}
|
||||||
|
onChange={e =>
|
||||||
|
setSteps(steps.map((x, i) => (i === idx ? { ...x, departmentId: e.target.value || null } : x)))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<option value="">— Không —</option>
|
||||||
|
{departmentsList.data?.map(d => (
|
||||||
|
<option key={d.id} value={d.id}>{d.name}</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-0.5">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => moveStep(idx, -1)}
|
||||||
|
disabled={idx === 0}
|
||||||
|
className="flex h-6 w-6 items-center justify-center rounded text-slate-400 hover:bg-slate-100 hover:text-slate-700 disabled:opacity-30"
|
||||||
|
title="Lên"
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => moveStep(idx, 1)}
|
||||||
|
disabled={idx === steps.length - 1}
|
||||||
|
className="flex h-6 w-6 items-center justify-center rounded text-slate-400 hover:bg-slate-100 hover:text-slate-700 disabled:opacity-30"
|
||||||
|
title="Xuống"
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSteps(steps.filter((_, i) => i !== idx))}
|
||||||
|
className="flex h-6 w-6 items-center justify-center rounded text-slate-400 hover:bg-red-50 hover:text-red-600"
|
||||||
|
title="Xóa bước"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Levels */}
|
||||||
|
<div className="mt-2 ml-9 space-y-1.5 border-l-2 border-violet-200 pl-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-[11px] font-semibold uppercase tracking-wide text-violet-700">
|
||||||
|
Cấp duyệt ({s.levels.length})
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setSteps(steps.map((x, i) =>
|
||||||
|
i === idx
|
||||||
|
? { ...x, levels: [...x.levels, { name: `Cấp ${x.levels.length + 1}`, approverUserId: usersList.data?.[0]?.id ?? '' }] }
|
||||||
|
: x,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
className="rounded bg-violet-50 px-2 py-1 text-[11px] font-medium text-violet-700 hover:bg-violet-100"
|
||||||
|
>
|
||||||
|
+ Thêm cấp
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{s.levels.map((l, li) => (
|
||||||
|
<div key={li} className="flex items-center gap-1.5">
|
||||||
|
<span className="rounded-full bg-violet-100 px-2 py-1 font-mono text-[10px] font-bold text-violet-700">
|
||||||
|
C{li + 1}
|
||||||
|
</span>
|
||||||
|
<Input
|
||||||
|
value={l.name}
|
||||||
|
onChange={e =>
|
||||||
|
setSteps(steps.map((x, i) =>
|
||||||
|
i === idx ? { ...x, levels: x.levels.map((y, j) => (j === li ? { ...y, name: e.target.value } : y)) } : x,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
placeholder={`Cấp ${li + 1}`}
|
||||||
|
className="h-7 max-w-[120px] text-xs"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
value={l.approverUserId}
|
||||||
|
onChange={e =>
|
||||||
|
setSteps(steps.map((x, i) =>
|
||||||
|
i === idx
|
||||||
|
? { ...x, levels: x.levels.map((y, j) => (j === li ? { ...y, approverUserId: e.target.value } : y)) }
|
||||||
|
: x,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
className="h-7 flex-1 text-xs"
|
||||||
|
>
|
||||||
|
<option value="">— Chọn NV duyệt —</option>
|
||||||
|
{usersList.data?.map(u => (
|
||||||
|
<option key={u.id} value={u.id}>
|
||||||
|
{u.fullName} ({u.email})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => moveLevel(idx, li, -1)}
|
||||||
|
disabled={li === 0}
|
||||||
|
className="flex h-6 w-6 items-center justify-center rounded text-slate-400 hover:bg-slate-100 hover:text-slate-700 disabled:opacity-30"
|
||||||
|
title="Lên"
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => moveLevel(idx, li, 1)}
|
||||||
|
disabled={li === s.levels.length - 1}
|
||||||
|
className="flex h-6 w-6 items-center justify-center rounded text-slate-400 hover:bg-slate-100 hover:text-slate-700 disabled:opacity-30"
|
||||||
|
title="Xuống"
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() =>
|
||||||
|
setSteps(steps.map((x, i) =>
|
||||||
|
i === idx ? { ...x, levels: x.levels.filter((_, j) => j !== li) } : x,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
className="flex h-6 w-6 items-center justify-center rounded text-slate-400 hover:bg-red-50 hover:text-red-600"
|
||||||
|
title="Xóa cấp"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{s.levels.length === 0 && (
|
||||||
|
<div className="rounded border border-dashed border-slate-300 px-2 py-1.5 text-[11px] italic text-slate-400">
|
||||||
|
Chưa có cấp. Bấm "+ Thêm cấp" để chỉ định NV duyệt.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-2 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-xs text-blue-800">
|
||||||
|
<GitBranch className="mt-0.5 h-3.5 w-3.5 shrink-0" />
|
||||||
|
<div>
|
||||||
|
Quy tắc duyệt: tuần tự trong cùng Bước (Cấp 1 → Cấp 2 → ...), hết Cấp thì sang Bước kế.
|
||||||
|
Mỗi Cấp = 1 nhân viên cụ thể (KHÔNG OR-of-many). Hết tất cả Bước = Đã duyệt.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using SolutionErp.Application.ApprovalWorkflowsV2;
|
||||||
|
|
||||||
|
namespace SolutionErp.Api.Controllers;
|
||||||
|
|
||||||
|
// Quy trình duyệt MỚI (Mig 22 — Session 17, 2026-05-08).
|
||||||
|
// Schema riêng để UAT, KHÔNG đụng WorkflowDefinition cũ.
|
||||||
|
// Reuse policy "Workflows.Read"/"Workflows.Create" giống PE/Contract designer
|
||||||
|
// — admin đã có quyền quản lý workflow nói chung.
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/approval-workflows-v2")]
|
||||||
|
[Authorize(Policy = "Workflows.Read")]
|
||||||
|
public class ApprovalWorkflowsV2Controller(IMediator mediator) : ControllerBase
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<ActionResult<AwAdminOverviewDto>> Overview(
|
||||||
|
[FromQuery] int? applicableType,
|
||||||
|
CancellationToken ct)
|
||||||
|
=> Ok(await mediator.Send(new GetAwAdminOverviewQuery(applicableType), ct));
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
[Authorize(Policy = "Workflows.Create")]
|
||||||
|
public async Task<ActionResult<object>> Create([FromBody] CreateAwDefinitionCommand cmd, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var id = await mediator.Send(cmd, ct);
|
||||||
|
return Ok(new { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{id:guid}")]
|
||||||
|
[Authorize(Policy = "Workflows.Create")]
|
||||||
|
public async Task<IActionResult> Delete(Guid id, CancellationToken ct)
|
||||||
|
{
|
||||||
|
await mediator.Send(new DeleteAwDefinitionCommand(id), ct);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,279 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using MediatR;
|
||||||
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SolutionErp.Application.Common.Interfaces;
|
||||||
|
using SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||||
|
using SolutionErp.Domain.Identity;
|
||||||
|
|
||||||
|
namespace SolutionErp.Application.ApprovalWorkflowsV2;
|
||||||
|
|
||||||
|
// Quy trình duyệt MỚI (Mig 22 — Session 17, 2026-05-08).
|
||||||
|
// Schema riêng để UAT trước khi drop legacy WorkflowDefinition cũ.
|
||||||
|
//
|
||||||
|
// Cấu trúc:
|
||||||
|
// Quy trình (Code + Name + ApplicableType)
|
||||||
|
// Bước 1 - Phòng A
|
||||||
|
// Cấp 1 - NV X (ApproverUserId 1 user cụ thể)
|
||||||
|
// Cấp 2 - NV Y
|
||||||
|
//
|
||||||
|
// Khác Mig 21: Levels match 1 NV CHÍNH XÁC qua ApproverUserId, không match
|
||||||
|
// group Dept+PositionLevel/Role/User.
|
||||||
|
|
||||||
|
public record AwLevelDto(
|
||||||
|
Guid Id,
|
||||||
|
int Order,
|
||||||
|
string? Name,
|
||||||
|
Guid ApproverUserId,
|
||||||
|
string? ApproverUserName,
|
||||||
|
string? ApproverEmail);
|
||||||
|
|
||||||
|
public record AwStepDto(
|
||||||
|
Guid Id,
|
||||||
|
int Order,
|
||||||
|
string Name,
|
||||||
|
Guid? DepartmentId,
|
||||||
|
string? DepartmentName,
|
||||||
|
List<AwLevelDto> Levels);
|
||||||
|
|
||||||
|
public record AwDefinitionDto(
|
||||||
|
Guid Id,
|
||||||
|
string Code,
|
||||||
|
int Version,
|
||||||
|
int ApplicableType,
|
||||||
|
string ApplicableTypeLabel,
|
||||||
|
string Name,
|
||||||
|
string? Description,
|
||||||
|
bool IsActive,
|
||||||
|
DateTime? ActivatedAt,
|
||||||
|
DateTime CreatedAt,
|
||||||
|
List<AwStepDto> Steps);
|
||||||
|
|
||||||
|
public record AwTypeSummaryDto(
|
||||||
|
int ApplicableType,
|
||||||
|
string ApplicableTypeLabel,
|
||||||
|
AwDefinitionDto? Active,
|
||||||
|
List<AwDefinitionDto> History);
|
||||||
|
|
||||||
|
public record AwAdminOverviewDto(List<AwTypeSummaryDto> Types);
|
||||||
|
|
||||||
|
internal static class AwLabels
|
||||||
|
{
|
||||||
|
public static readonly Dictionary<ApprovalWorkflowApplicableType, string> Type = new()
|
||||||
|
{
|
||||||
|
[ApprovalWorkflowApplicableType.DuyetNcc] = "Duyệt NCC",
|
||||||
|
[ApprovalWorkflowApplicableType.DuyetNccPhuongAn] = "Duyệt NCC + Giải pháp",
|
||||||
|
[ApprovalWorkflowApplicableType.Contract] = "Hợp đồng",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== GET overview ==========
|
||||||
|
// Filter `applicableType=null` → return tất cả, `=N` → chỉ type đó.
|
||||||
|
|
||||||
|
public record GetAwAdminOverviewQuery(int? ApplicableType = null) : IRequest<AwAdminOverviewDto>;
|
||||||
|
|
||||||
|
public class GetAwAdminOverviewQueryHandler(
|
||||||
|
IApplicationDbContext db,
|
||||||
|
UserManager<User> userManager) : IRequestHandler<GetAwAdminOverviewQuery, AwAdminOverviewDto>
|
||||||
|
{
|
||||||
|
public async Task<AwAdminOverviewDto> Handle(GetAwAdminOverviewQuery request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var query = db.ApprovalWorkflows.AsNoTracking()
|
||||||
|
.Include(d => d.Steps.OrderBy(s => s.Order))
|
||||||
|
.ThenInclude(s => s.Levels.OrderBy(l => l.Order))
|
||||||
|
.OrderByDescending(d => d.Version)
|
||||||
|
.AsQueryable();
|
||||||
|
|
||||||
|
if (request.ApplicableType is int t)
|
||||||
|
{
|
||||||
|
var typeEnum = (ApprovalWorkflowApplicableType)t;
|
||||||
|
query = query.Where(d => d.ApplicableType == typeEnum);
|
||||||
|
}
|
||||||
|
|
||||||
|
var definitions = await query.ToListAsync(ct);
|
||||||
|
|
||||||
|
// Resolve dept names
|
||||||
|
var deptIds = definitions
|
||||||
|
.SelectMany(d => d.Steps)
|
||||||
|
.Where(s => s.DepartmentId != null)
|
||||||
|
.Select(s => s.DepartmentId!.Value)
|
||||||
|
.Distinct().ToList();
|
||||||
|
var deptNames = deptIds.Count == 0
|
||||||
|
? new Dictionary<Guid, string>()
|
||||||
|
: await db.Departments.AsNoTracking()
|
||||||
|
.Where(d => deptIds.Contains(d.Id))
|
||||||
|
.ToDictionaryAsync(d => d.Id, d => d.Name, ct);
|
||||||
|
|
||||||
|
// Resolve user names
|
||||||
|
var userIds = definitions
|
||||||
|
.SelectMany(d => d.Steps)
|
||||||
|
.SelectMany(s => s.Levels)
|
||||||
|
.Select(l => l.ApproverUserId)
|
||||||
|
.Distinct().ToList();
|
||||||
|
var users = userIds.Count == 0
|
||||||
|
? new Dictionary<Guid, (string FullName, string? Email)>()
|
||||||
|
: await userManager.Users.AsNoTracking()
|
||||||
|
.Where(u => userIds.Contains(u.Id))
|
||||||
|
.Select(u => new { u.Id, u.FullName, u.Email })
|
||||||
|
.ToDictionaryAsync(u => u.Id, u => (u.FullName, u.Email), ct);
|
||||||
|
|
||||||
|
AwDefinitionDto ToDto(ApprovalWorkflow d) => new(
|
||||||
|
d.Id,
|
||||||
|
d.Code,
|
||||||
|
d.Version,
|
||||||
|
(int)d.ApplicableType,
|
||||||
|
AwLabels.Type.GetValueOrDefault(d.ApplicableType, d.ApplicableType.ToString()),
|
||||||
|
d.Name,
|
||||||
|
d.Description,
|
||||||
|
d.IsActive,
|
||||||
|
d.ActivatedAt,
|
||||||
|
d.CreatedAt,
|
||||||
|
d.Steps.OrderBy(s => s.Order).Select(s => new AwStepDto(
|
||||||
|
s.Id,
|
||||||
|
s.Order,
|
||||||
|
s.Name,
|
||||||
|
s.DepartmentId,
|
||||||
|
s.DepartmentId != null ? deptNames.GetValueOrDefault(s.DepartmentId.Value) : null,
|
||||||
|
s.Levels.OrderBy(l => l.Order).Select(l =>
|
||||||
|
{
|
||||||
|
users.TryGetValue(l.ApproverUserId, out var info);
|
||||||
|
return new AwLevelDto(l.Id, l.Order, l.Name, l.ApproverUserId, info.FullName, info.Email);
|
||||||
|
}).ToList()
|
||||||
|
)).ToList());
|
||||||
|
|
||||||
|
var typesToReturn = request.ApplicableType is int onlyT
|
||||||
|
? new[] { (ApprovalWorkflowApplicableType)onlyT }
|
||||||
|
: Enum.GetValues<ApprovalWorkflowApplicableType>();
|
||||||
|
|
||||||
|
var result = typesToReturn
|
||||||
|
.Select(type =>
|
||||||
|
{
|
||||||
|
var versions = definitions.Where(d => d.ApplicableType == type).Select(ToDto).ToList();
|
||||||
|
return new AwTypeSummaryDto(
|
||||||
|
(int)type,
|
||||||
|
AwLabels.Type.GetValueOrDefault(type, type.ToString()),
|
||||||
|
versions.FirstOrDefault(v => v.IsActive),
|
||||||
|
versions);
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new AwAdminOverviewDto(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== POST new version ==========
|
||||||
|
|
||||||
|
public record CreateAwLevelInput(int Order, string? Name, Guid ApproverUserId);
|
||||||
|
|
||||||
|
public record CreateAwStepInput(
|
||||||
|
int Order,
|
||||||
|
string Name,
|
||||||
|
Guid? DepartmentId,
|
||||||
|
List<CreateAwLevelInput> Levels);
|
||||||
|
|
||||||
|
public record CreateAwDefinitionCommand(
|
||||||
|
int ApplicableType,
|
||||||
|
string Code,
|
||||||
|
string Name,
|
||||||
|
string? Description,
|
||||||
|
List<CreateAwStepInput> Steps) : IRequest<Guid>;
|
||||||
|
|
||||||
|
public class CreateAwDefinitionCommandValidator : AbstractValidator<CreateAwDefinitionCommand>
|
||||||
|
{
|
||||||
|
public CreateAwDefinitionCommandValidator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.ApplicableType).Must(t => Enum.IsDefined(typeof(ApprovalWorkflowApplicableType), t))
|
||||||
|
.WithMessage("ApplicableType không hợp lệ.");
|
||||||
|
RuleFor(x => x.Code).NotEmpty().MaximumLength(100)
|
||||||
|
.Matches("^[A-Za-z0-9._-]+$")
|
||||||
|
.WithMessage("Code chỉ dùng chữ, số, và các ký tự . _ -");
|
||||||
|
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
||||||
|
RuleFor(x => x.Description).MaximumLength(1000);
|
||||||
|
RuleFor(x => x.Steps).NotEmpty()
|
||||||
|
.WithMessage("Quy trình phải có ít nhất 1 bước.");
|
||||||
|
RuleForEach(x => x.Steps).ChildRules(step =>
|
||||||
|
{
|
||||||
|
step.RuleFor(s => s.Order).GreaterThanOrEqualTo(1);
|
||||||
|
step.RuleFor(s => s.Name).NotEmpty().MaximumLength(200);
|
||||||
|
step.RuleFor(s => s.Levels).NotEmpty()
|
||||||
|
.WithMessage("Mỗi bước phải có ít nhất 1 cấp duyệt.");
|
||||||
|
step.RuleForEach(s => s.Levels).ChildRules(level =>
|
||||||
|
{
|
||||||
|
level.RuleFor(l => l.Order).GreaterThanOrEqualTo(1);
|
||||||
|
level.RuleFor(l => l.Name).MaximumLength(200);
|
||||||
|
level.RuleFor(l => l.ApproverUserId).NotEmpty()
|
||||||
|
.WithMessage("Cấp duyệt phải chỉ định 1 nhân viên cụ thể.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CreateAwDefinitionCommandHandler(IApplicationDbContext db)
|
||||||
|
: IRequestHandler<CreateAwDefinitionCommand, Guid>
|
||||||
|
{
|
||||||
|
public async Task<Guid> Handle(CreateAwDefinitionCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var typeEnum = (ApprovalWorkflowApplicableType)request.ApplicableType;
|
||||||
|
|
||||||
|
// Auto-increment version theo Code (cùng Code = cùng "logical" workflow)
|
||||||
|
var nextVersion = await db.ApprovalWorkflows
|
||||||
|
.Where(w => w.Code == request.Code)
|
||||||
|
.MaxAsync(w => (int?)w.Version, ct) ?? 0;
|
||||||
|
nextVersion++;
|
||||||
|
|
||||||
|
// Deactivate active version cho ApplicableType này (only ONE active per type)
|
||||||
|
var actives = await db.ApprovalWorkflows
|
||||||
|
.Where(w => w.ApplicableType == typeEnum && w.IsActive)
|
||||||
|
.ToListAsync(ct);
|
||||||
|
foreach (var old in actives) old.IsActive = false;
|
||||||
|
|
||||||
|
var def = new ApprovalWorkflow
|
||||||
|
{
|
||||||
|
Code = request.Code,
|
||||||
|
Version = nextVersion,
|
||||||
|
ApplicableType = typeEnum,
|
||||||
|
Name = request.Name,
|
||||||
|
Description = request.Description,
|
||||||
|
IsActive = true,
|
||||||
|
ActivatedAt = DateTime.UtcNow,
|
||||||
|
Steps = request.Steps.OrderBy(s => s.Order)
|
||||||
|
.Select(s => new ApprovalWorkflowStep
|
||||||
|
{
|
||||||
|
Order = s.Order,
|
||||||
|
Name = s.Name,
|
||||||
|
DepartmentId = s.DepartmentId,
|
||||||
|
Levels = s.Levels.OrderBy(l => l.Order)
|
||||||
|
.Select(l => new ApprovalWorkflowLevel
|
||||||
|
{
|
||||||
|
Order = l.Order,
|
||||||
|
Name = l.Name,
|
||||||
|
ApproverUserId = l.ApproverUserId,
|
||||||
|
}).ToList(),
|
||||||
|
})
|
||||||
|
.ToList(),
|
||||||
|
};
|
||||||
|
db.ApprovalWorkflows.Add(def);
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
return def.Id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== DELETE version (chỉ khi chưa có phiếu pin) ==========
|
||||||
|
// Hiện chưa có phiếu nào pin schema mới → unconditional delete OK cho UAT.
|
||||||
|
// Sau UAT khi link với PE/Contract thật cần check usage trước khi delete.
|
||||||
|
|
||||||
|
public record DeleteAwDefinitionCommand(Guid Id) : IRequest;
|
||||||
|
|
||||||
|
public class DeleteAwDefinitionCommandHandler(IApplicationDbContext db)
|
||||||
|
: IRequestHandler<DeleteAwDefinitionCommand>
|
||||||
|
{
|
||||||
|
public async Task Handle(DeleteAwDefinitionCommand request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var def = await db.ApprovalWorkflows
|
||||||
|
.FirstOrDefaultAsync(d => d.Id == request.Id, ct)
|
||||||
|
?? throw new KeyNotFoundException($"ApprovalWorkflow {request.Id} không tồn tại.");
|
||||||
|
|
||||||
|
db.ApprovalWorkflows.Remove(def);
|
||||||
|
await db.SaveChangesAsync(ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||||
using SolutionErp.Domain.Budgets;
|
using SolutionErp.Domain.Budgets;
|
||||||
using SolutionErp.Domain.Contracts;
|
using SolutionErp.Domain.Contracts;
|
||||||
using SolutionErp.Domain.Contracts.Details;
|
using SolutionErp.Domain.Contracts.Details;
|
||||||
@ -64,6 +65,12 @@ public interface IApplicationDbContext
|
|||||||
DbSet<PurchaseEvaluationDepartmentOpinion> PurchaseEvaluationDepartmentOpinions { get; }
|
DbSet<PurchaseEvaluationDepartmentOpinion> PurchaseEvaluationDepartmentOpinions { get; }
|
||||||
DbSet<PurchaseEvaluationDepartmentApproval> PurchaseEvaluationDepartmentApprovals { get; }
|
DbSet<PurchaseEvaluationDepartmentApproval> PurchaseEvaluationDepartmentApprovals { get; }
|
||||||
|
|
||||||
|
// Quy trình duyệt MỚI (Mig 22 — Session 17): schema riêng UAT trước khi
|
||||||
|
// drop legacy WorkflowDefinition. Cấu trúc: Quy trình > Bước (Phòng) > Cấp (NV cụ thể).
|
||||||
|
DbSet<ApprovalWorkflow> ApprovalWorkflows { get; }
|
||||||
|
DbSet<ApprovalWorkflowStep> ApprovalWorkflowSteps { get; }
|
||||||
|
DbSet<ApprovalWorkflowLevel> ApprovalWorkflowLevels { get; }
|
||||||
|
|
||||||
// Module Ngân sách (Phase 7)
|
// Module Ngân sách (Phase 7)
|
||||||
DbSet<Budget> Budgets { get; }
|
DbSet<Budget> Budgets { get; }
|
||||||
DbSet<BudgetDetail> BudgetDetails { get; }
|
DbSet<BudgetDetail> BudgetDetails { get; }
|
||||||
|
|||||||
@ -0,0 +1,63 @@
|
|||||||
|
using SolutionErp.Domain.Common;
|
||||||
|
|
||||||
|
namespace SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||||
|
|
||||||
|
// Quy trình duyệt mới (Mig 22 — Session 17, 2026-05-08).
|
||||||
|
// Schema riêng để UAT, KHÔNG đụng WorkflowDefinition cũ (Mig 21 flat). Sau UAT
|
||||||
|
// OK → migrate data PE/HĐ pin sang ApprovalWorkflowId + drop bảng cũ.
|
||||||
|
//
|
||||||
|
// Cấu trúc:
|
||||||
|
// Quy trình (Code + Name + ApplicableType)
|
||||||
|
// Bước 1 - Phòng A (DepartmentId optional hint)
|
||||||
|
// Cấp 1 - NV X (ApproverUserId specific)
|
||||||
|
// Cấp 2 - NV Y
|
||||||
|
// Bước 2 - Phòng B
|
||||||
|
// Cấp 1 - NV Z
|
||||||
|
// ...
|
||||||
|
//
|
||||||
|
// Service (sau khi UAT chốt): iterate Steps OrderBy Order. Mỗi step iterate
|
||||||
|
// Levels OrderBy Order. Mỗi level = 1 NV cụ thể duyệt. Hết level → next step.
|
||||||
|
// Hết step → DaDuyet.
|
||||||
|
public class ApprovalWorkflow : BaseEntity
|
||||||
|
{
|
||||||
|
public string Code { get; set; } = string.Empty; // Mã quy trình "QT-DN-V2-001"
|
||||||
|
public int Version { get; set; } // monotonically increases per Code
|
||||||
|
public ApprovalWorkflowApplicableType ApplicableType { get; set; } // module áp dụng
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
public DateTime? ActivatedAt { get; set; }
|
||||||
|
|
||||||
|
public List<ApprovalWorkflowStep> Steps { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ApprovalWorkflowApplicableType
|
||||||
|
{
|
||||||
|
DuyetNcc = 1, // PE module — Duyệt NCC (default test target)
|
||||||
|
DuyetNccPhuongAn = 2, // PE — Duyệt NCC + Giải pháp
|
||||||
|
Contract = 3, // HĐ general (any ContractType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bước = Phòng. 1 quy trình có nhiều bước theo Order.
|
||||||
|
public class ApprovalWorkflowStep : BaseEntity
|
||||||
|
{
|
||||||
|
public Guid ApprovalWorkflowId { get; set; }
|
||||||
|
public int Order { get; set; } // 1-based
|
||||||
|
public string Name { get; set; } = string.Empty; // "Phòng A", "Phòng B" — display
|
||||||
|
public Guid? DepartmentId { get; set; } // hint phòng (optional, không strict match)
|
||||||
|
|
||||||
|
public ApprovalWorkflow? ApprovalWorkflow { get; set; }
|
||||||
|
public List<ApprovalWorkflowLevel> Levels { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cấp = 1 NV cụ thể. 1 bước có nhiều cấp theo Order. Approver = ApproverUserId
|
||||||
|
// chính xác (KHÔNG OR-of-many). Sequential trong cùng bước: cấp 1 → cấp 2 → ...
|
||||||
|
public class ApprovalWorkflowLevel : BaseEntity
|
||||||
|
{
|
||||||
|
public Guid ApprovalWorkflowStepId { get; set; }
|
||||||
|
public int Order { get; set; } // 1-based trong cùng step
|
||||||
|
public string? Name { get; set; } // "Cấp 1" — display optional
|
||||||
|
public Guid ApproverUserId { get; set; } // 1 NV cụ thể duyệt cấp này
|
||||||
|
|
||||||
|
public ApprovalWorkflowStep? Step { get; set; }
|
||||||
|
}
|
||||||
@ -51,6 +51,20 @@ public static class MenuKeys
|
|||||||
public const string PurchaseEvaluations = "PurchaseEvaluations"; // root group
|
public const string PurchaseEvaluations = "PurchaseEvaluations"; // root group
|
||||||
public const string PeWorkflows = "PeWorkflows"; // workflow admin root
|
public const string PeWorkflows = "PeWorkflows"; // workflow admin root
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Quy trình duyệt MỚI (Mig 22 — Session 17, 2026-05-08):
|
||||||
|
// Schema riêng `ApprovalWorkflow` để UAT trước khi migrate hoàn toàn.
|
||||||
|
// Cấu trúc: Quy trình > Bước (Phòng) > Cấp (NV cụ thể).
|
||||||
|
// Mã + Tên Quy trình
|
||||||
|
// Bước 1 - Phòng A
|
||||||
|
// Cấp 1 - NV X
|
||||||
|
// Cấp 2 - NV Y
|
||||||
|
// Bước 2 - Phòng B
|
||||||
|
// ...
|
||||||
|
// ============================================================
|
||||||
|
public const string ApprovalWorkflowsV2 = "ApprovalWorkflowsV2"; // root admin (mới)
|
||||||
|
public const string ApprovalWorkflowDuyetNccV2 = "AwV2_DuyetNcc"; // leaf cho Duyệt NCC mới
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Module Ngân sách (Phase 7) — 4 bảng quản lý ngân sách dự án/gói thầu.
|
// Module Ngân sách (Phase 7) — 4 bảng quản lý ngân sách dự án/gói thầu.
|
||||||
// 1 root + 3 leaf action (Danh sách / Thao tác / Duyệt).
|
// 1 root + 3 leaf action (Danh sách / Thao tác / Duyệt).
|
||||||
@ -81,6 +95,7 @@ public static class MenuKeys
|
|||||||
PurchaseEvaluations,
|
PurchaseEvaluations,
|
||||||
Budgets, BudgetList, BudgetCreate, BudgetPending,
|
Budgets, BudgetList, BudgetCreate, BudgetPending,
|
||||||
System, Users, Roles, Permissions, Workflows, PeWorkflows,
|
System, Users, Roles, Permissions, Workflows, PeWorkflows,
|
||||||
|
ApprovalWorkflowsV2, ApprovalWorkflowDuyetNccV2, // Mig 22
|
||||||
];
|
];
|
||||||
|
|
||||||
public static readonly string[] Actions = ["Read", "Create", "Update", "Delete"];
|
public static readonly string[] Actions = ["Read", "Create", "Update", "Delete"];
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using SolutionErp.Application.Common.Interfaces;
|
using SolutionErp.Application.Common.Interfaces;
|
||||||
|
using SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||||
using SolutionErp.Domain.Budgets;
|
using SolutionErp.Domain.Budgets;
|
||||||
using SolutionErp.Domain.Contracts;
|
using SolutionErp.Domain.Contracts;
|
||||||
using SolutionErp.Domain.Contracts.Details;
|
using SolutionErp.Domain.Contracts.Details;
|
||||||
@ -63,6 +64,11 @@ public class ApplicationDbContext
|
|||||||
public DbSet<PurchaseEvaluationDepartmentOpinion> PurchaseEvaluationDepartmentOpinions => Set<PurchaseEvaluationDepartmentOpinion>();
|
public DbSet<PurchaseEvaluationDepartmentOpinion> PurchaseEvaluationDepartmentOpinions => Set<PurchaseEvaluationDepartmentOpinion>();
|
||||||
public DbSet<PurchaseEvaluationDepartmentApproval> PurchaseEvaluationDepartmentApprovals => Set<PurchaseEvaluationDepartmentApproval>();
|
public DbSet<PurchaseEvaluationDepartmentApproval> PurchaseEvaluationDepartmentApprovals => Set<PurchaseEvaluationDepartmentApproval>();
|
||||||
|
|
||||||
|
// Quy trình duyệt mới (Mig 22 — Session 17): schema riêng UAT.
|
||||||
|
public DbSet<ApprovalWorkflow> ApprovalWorkflows => Set<ApprovalWorkflow>();
|
||||||
|
public DbSet<ApprovalWorkflowStep> ApprovalWorkflowSteps => Set<ApprovalWorkflowStep>();
|
||||||
|
public DbSet<ApprovalWorkflowLevel> ApprovalWorkflowLevels => Set<ApprovalWorkflowLevel>();
|
||||||
|
|
||||||
// Module Ngân sách (Phase 7) — 4 bảng: Budget header + Details + Approvals + Changelogs.
|
// Module Ngân sách (Phase 7) — 4 bảng: Budget header + Details + Approvals + Changelogs.
|
||||||
public DbSet<Budget> Budgets => Set<Budget>();
|
public DbSet<Budget> Budgets => Set<Budget>();
|
||||||
public DbSet<BudgetDetail> BudgetDetails => Set<BudgetDetail>();
|
public DbSet<BudgetDetail> BudgetDetails => Set<BudgetDetail>();
|
||||||
|
|||||||
@ -0,0 +1,69 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
using SolutionErp.Domain.ApprovalWorkflowsV2;
|
||||||
|
|
||||||
|
namespace SolutionErp.Infrastructure.Persistence.Configurations;
|
||||||
|
|
||||||
|
// EF config Mig 22 — schema mới riêng cho Quy trình duyệt v2 (UAT before drop legacy).
|
||||||
|
|
||||||
|
public class ApprovalWorkflowConfiguration : IEntityTypeConfiguration<ApprovalWorkflow>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<ApprovalWorkflow> e)
|
||||||
|
{
|
||||||
|
e.ToTable("ApprovalWorkflows");
|
||||||
|
e.Property(x => x.Code).HasMaxLength(100).IsRequired();
|
||||||
|
e.Property(x => x.Name).HasMaxLength(200).IsRequired();
|
||||||
|
e.Property(x => x.Description).HasMaxLength(1000);
|
||||||
|
e.Property(x => x.ApplicableType).HasConversion<int>();
|
||||||
|
|
||||||
|
e.HasIndex(x => new { x.Code, x.Version }).IsUnique();
|
||||||
|
e.HasIndex(x => new { x.ApplicableType, x.IsActive });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApprovalWorkflowStepConfiguration : IEntityTypeConfiguration<ApprovalWorkflowStep>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<ApprovalWorkflowStep> e)
|
||||||
|
{
|
||||||
|
e.ToTable("ApprovalWorkflowSteps");
|
||||||
|
e.Property(x => x.Name).HasMaxLength(200).IsRequired();
|
||||||
|
|
||||||
|
e.HasOne(x => x.ApprovalWorkflow)
|
||||||
|
.WithMany(d => d.Steps)
|
||||||
|
.HasForeignKey(x => x.ApprovalWorkflowId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
// Department FK Restrict (optional hint).
|
||||||
|
e.HasOne<SolutionErp.Domain.Master.Department>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.DepartmentId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
e.HasIndex(x => new { x.ApprovalWorkflowId, x.Order });
|
||||||
|
e.HasIndex(x => x.DepartmentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ApprovalWorkflowLevelConfiguration : IEntityTypeConfiguration<ApprovalWorkflowLevel>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<ApprovalWorkflowLevel> e)
|
||||||
|
{
|
||||||
|
e.ToTable("ApprovalWorkflowLevels");
|
||||||
|
e.Property(x => x.Name).HasMaxLength(200);
|
||||||
|
|
||||||
|
e.HasOne(x => x.Step)
|
||||||
|
.WithMany(s => s.Levels)
|
||||||
|
.HasForeignKey(x => x.ApprovalWorkflowStepId)
|
||||||
|
.OnDelete(DeleteBehavior.Cascade);
|
||||||
|
|
||||||
|
// ApproverUserId FK Restrict — không cấu hình nav để giữ nhẹ
|
||||||
|
// (1 chiều, query qua join nếu cần admin xem detail).
|
||||||
|
e.HasOne<SolutionErp.Domain.Identity.User>()
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey(x => x.ApproverUserId)
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
e.HasIndex(x => new { x.ApprovalWorkflowStepId, x.Order });
|
||||||
|
e.HasIndex(x => x.ApproverUserId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1308,6 +1308,10 @@ public static class DbInitializer
|
|||||||
// Module Duyệt NCC (tiền-HĐ)
|
// Module Duyệt NCC (tiền-HĐ)
|
||||||
(MenuKeys.PurchaseEvaluations, "Quy trình chọn Thầu phụ - NCC", null, 25, "ClipboardCheck"),
|
(MenuKeys.PurchaseEvaluations, "Quy trình chọn Thầu phụ - NCC", null, 25, "ClipboardCheck"),
|
||||||
(MenuKeys.PeWorkflows, "Quy trình Duyệt NCC", MenuKeys.System, 95, "GitCompareArrows"),
|
(MenuKeys.PeWorkflows, "Quy trình Duyệt NCC", MenuKeys.System, 95, "GitCompareArrows"),
|
||||||
|
// Quy trình duyệt MỚI (Mig 22 — Session 17, 2026-05-08): schema riêng
|
||||||
|
// UAT trước khi migrate hoàn toàn. Cấu trúc: Quy trình > Bước (Phòng) > Cấp (NV cụ thể).
|
||||||
|
(MenuKeys.ApprovalWorkflowsV2, "Quy trình duyệt (Mới)", MenuKeys.System, 96, "Workflow"),
|
||||||
|
(MenuKeys.ApprovalWorkflowDuyetNccV2, "Duyệt NCC (Mới)", MenuKeys.ApprovalWorkflowsV2, 1, "FileCheck"),
|
||||||
// Module Ngân sách (Phase 7)
|
// Module Ngân sách (Phase 7)
|
||||||
(MenuKeys.Budgets, "Ngân sách", null, 27, "Wallet"),
|
(MenuKeys.Budgets, "Ngân sách", null, 27, "Wallet"),
|
||||||
(MenuKeys.BudgetList, "Danh sách", MenuKeys.Budgets, 1, "List"),
|
(MenuKeys.BudgetList, "Danh sách", MenuKeys.Budgets, 1, "List"),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,143 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace SolutionErp.Infrastructure.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddApprovalWorkflowsV2 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ApprovalWorkflows",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
Code = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||||
|
Version = table.Column<int>(type: "int", nullable: false),
|
||||||
|
ApplicableType = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||||
|
Description = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: true),
|
||||||
|
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||||
|
ActivatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
|
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ApprovalWorkflows", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ApprovalWorkflowSteps",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
ApprovalWorkflowId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
Order = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
|
||||||
|
DepartmentId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
|
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ApprovalWorkflowSteps", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ApprovalWorkflowSteps_ApprovalWorkflows_ApprovalWorkflowId",
|
||||||
|
column: x => x.ApprovalWorkflowId,
|
||||||
|
principalTable: "ApprovalWorkflows",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ApprovalWorkflowSteps_Departments_DepartmentId",
|
||||||
|
column: x => x.DepartmentId,
|
||||||
|
principalTable: "Departments",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "ApprovalWorkflowLevels",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
ApprovalWorkflowStepId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
Order = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||||
|
ApproverUserId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||||
|
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||||
|
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||||
|
CreatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||||
|
UpdatedBy = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_ApprovalWorkflowLevels", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ApprovalWorkflowLevels_ApprovalWorkflowSteps_ApprovalWorkflowStepId",
|
||||||
|
column: x => x.ApprovalWorkflowStepId,
|
||||||
|
principalTable: "ApprovalWorkflowSteps",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_ApprovalWorkflowLevels_Users_ApproverUserId",
|
||||||
|
column: x => x.ApproverUserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Restrict);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApprovalWorkflowLevels_ApprovalWorkflowStepId_Order",
|
||||||
|
table: "ApprovalWorkflowLevels",
|
||||||
|
columns: new[] { "ApprovalWorkflowStepId", "Order" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApprovalWorkflowLevels_ApproverUserId",
|
||||||
|
table: "ApprovalWorkflowLevels",
|
||||||
|
column: "ApproverUserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApprovalWorkflows_ApplicableType_IsActive",
|
||||||
|
table: "ApprovalWorkflows",
|
||||||
|
columns: new[] { "ApplicableType", "IsActive" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApprovalWorkflows_Code_Version",
|
||||||
|
table: "ApprovalWorkflows",
|
||||||
|
columns: new[] { "Code", "Version" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApprovalWorkflowSteps_ApprovalWorkflowId_Order",
|
||||||
|
table: "ApprovalWorkflowSteps",
|
||||||
|
columns: new[] { "ApprovalWorkflowId", "Order" });
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_ApprovalWorkflowSteps_DepartmentId",
|
||||||
|
table: "ApprovalWorkflowSteps",
|
||||||
|
column: "DepartmentId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ApprovalWorkflowLevels");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ApprovalWorkflowSteps");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "ApprovalWorkflows");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -125,6 +125,141 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.ToTable("UserTokens", (string)null);
|
b.ToTable("UserTokens", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("ActivatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<int>("ApplicableType")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<string>("Code")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("CreatedBy")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.HasMaxLength(1000)
|
||||||
|
.HasColumnType("nvarchar(1000)");
|
||||||
|
|
||||||
|
b.Property<bool>("IsActive")
|
||||||
|
.HasColumnType("bit");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UpdatedBy")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<int>("Version")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ApplicableType", "IsActive");
|
||||||
|
|
||||||
|
b.HasIndex("Code", "Version")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("ApprovalWorkflows", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("ApprovalWorkflowStepId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("ApproverUserId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("CreatedBy")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
|
b.Property<int>("Order")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UpdatedBy")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ApproverUserId");
|
||||||
|
|
||||||
|
b.HasIndex("ApprovalWorkflowStepId", "Order");
|
||||||
|
|
||||||
|
b.ToTable("ApprovalWorkflowLevels", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowStep", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid>("ApprovalWorkflowId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("CreatedBy")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<Guid?>("DepartmentId")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)");
|
||||||
|
|
||||||
|
b.Property<int>("Order")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2");
|
||||||
|
|
||||||
|
b.Property<Guid?>("UpdatedBy")
|
||||||
|
.HasColumnType("uniqueidentifier");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("DepartmentId");
|
||||||
|
|
||||||
|
b.HasIndex("ApprovalWorkflowId", "Order");
|
||||||
|
|
||||||
|
b.ToTable("ApprovalWorkflowSteps", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SolutionErp.Domain.Budgets.Budget", b =>
|
modelBuilder.Entity("SolutionErp.Domain.Budgets.Budget", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@ -3154,6 +3289,39 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
.IsRequired();
|
.IsRequired();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowLevel", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowStep", "Step")
|
||||||
|
.WithMany("Levels")
|
||||||
|
.HasForeignKey("ApprovalWorkflowStepId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("SolutionErp.Domain.Identity.User", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("ApproverUserId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Step");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowStep", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", "ApprovalWorkflow")
|
||||||
|
.WithMany("Steps")
|
||||||
|
.HasForeignKey("ApprovalWorkflowId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("SolutionErp.Domain.Master.Department", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("DepartmentId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
|
b.Navigation("ApprovalWorkflow");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetApproval", b =>
|
modelBuilder.Entity("SolutionErp.Domain.Budgets.BudgetApproval", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("SolutionErp.Domain.Budgets.Budget", "Budget")
|
b.HasOne("SolutionErp.Domain.Budgets.Budget", "Budget")
|
||||||
@ -3521,6 +3689,16 @@ namespace SolutionErp.Infrastructure.Persistence.Migrations
|
|||||||
b.Navigation("Step");
|
b.Navigation("Step");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflow", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Steps");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("SolutionErp.Domain.ApprovalWorkflowsV2.ApprovalWorkflowStep", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Levels");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("SolutionErp.Domain.Budgets.Budget", b =>
|
modelBuilder.Entity("SolutionErp.Domain.Budgets.Budget", b =>
|
||||||
{
|
{
|
||||||
b.Navigation("Approvals");
|
b.Navigation("Approvals");
|
||||||
|
|||||||
Reference in New Issue
Block a user