diff --git a/.claude/skills/contract-workflow/SKILL.md b/.claude/skills/contract-workflow/SKILL.md index 93c52af..d29041f 100644 --- a/.claude/skills/contract-workflow/SKILL.md +++ b/.claude/skills/contract-workflow/SKILL.md @@ -1,6 +1,6 @@ --- name: contract-workflow -description: State machine 9 phase cho hợp đồng TP/NCC/Tổ đội — transition guard, role check, SLA deadline, auto-gen mã HĐ RG-001. Dùng khi debug chuyển phase, 403 forbidden, code sai format, bypass Chủ đầu tư. +description: State machine 9 phase cho hợp đồng TP/NCC/Tổ đội — transition guard, role check, SLA deadline, auto-gen mã HĐ RG-001, versioned workflow admin-configurable per ContractType. Dùng khi debug chuyển phase, 403 forbidden, code sai format, bypass Chủ đầu tư, workflow policy resolution. when-to-use: - "transition contract" - "chuyển phase hợp đồng" @@ -9,11 +9,15 @@ when-to-use: - "reject contract về draft" - "mã HĐ sai format" - "bypass CCM chủ đầu tư" + - "versioned workflow" + - "quy trình mới HĐ cũ giữ cũ" + - "WorkflowDefinition pin" + - "admin workflow designer" --- # Contract Workflow Skill -> **Status:** Phase 3 IMPLEMENTED (MVP — state transitions + code gen). Còn thiếu: SLA hosted service, email notify, in-app realtime. +> **Status:** Tier 3 FEATURE-COMPLETE — State transitions + code gen + SLA job + attachment + realtime notify + versioned workflow admin-configurable. Còn thiếu: email outbox (SMTP), User-kind approver runtime, warning 20% SLA. ## Domain entities (implemented) @@ -21,8 +25,14 @@ when-to-use: Contract ─────< ContractApproval (lịch sử mỗi transition) ─────< ContractComment (thread góp ý) ─────< ContractAttachment (scan signed/sealed) + ─────> WorkflowDefinition (PINNED at create-time, nullable FK) ContractCodeSequence (Prefix PK, LastSeq) — gen mã HĐ atomic + +WorkflowDefinition ─────< WorkflowStep ─────< WorkflowStepApprover + (Code + Version + IsActive + ContractType) (Kind=Role|User + AssignmentValue) + +WorkflowTypeAssignment (admin override legacy, fall back khi Contract.WorkflowDefinitionId == null) ``` ## 9 phase state machine @@ -39,11 +49,69 @@ Alternates: Bypass (HĐ Chủ đầu tư, BypassProcurementAndCCM=true): DangInKy → DangTrinhKy (skip CCM) + +Policy variants (hardcoded fallback, dùng khi không có WorkflowDefinition pin): + - Standard (8 phase full CCM) — Thầu phụ, Giao khoán, NCC + - SkipCcm (7 phase bỏ CCM) — Dịch vụ, Mua bán, Nguyên tắc NCC, Nguyên tắc DV ``` -## SLA mặc định (sau transition, set `Contract.SlaDeadline = UtcNow + sla`) +## Versioned workflow (Tier 3) — policy resolution runtime -| Phase | SLA | +```csharp +// ContractWorkflowService.LoadPolicyAsync(contractId): +// 1. Load contract +var c = await db.Contracts.FindAsync(contractId); + +// 2. If pinned — nạp từ DB +if (c.WorkflowDefinitionId != null) { + var def = await db.WorkflowDefinitions + .Include(d => d.Steps).ThenInclude(s => s.Approvers) + .FirstAsync(d => d.Id == c.WorkflowDefinitionId); + return WorkflowPolicyRegistry.FromDefinition(def); + // → xây policy runtime từ Steps.Approvers + // → Role-kind → allowedRoles + // → User-kind (data model ready, iter sau enable guard) +} + +// 3. Nếu không pin — check admin override WorkflowTypeAssignments +var assignment = await db.WorkflowTypeAssignments.FirstOrDefaultAsync(a => a.ContractType == c.Type); +if (assignment != null) return WorkflowPolicyRegistry.ByName(assignment.PolicyName); + +// 4. Fallback hardcoded +return WorkflowPolicyRegistry.For(c.Type); // Standard or SkipCcm +``` + +## Admin designer flow (Tạo version mới) + +``` +Admin → /system/workflows → grid 7 type card + → click "HĐ Mua bán" → /system/workflows/MuaBan + → thấy QT-MB-v01 active + history + → click "Tạo phiên bản mới" (có thể Clone từ v01) + → Designer modal: + Code: QT-MB (auto-fill) + Version: v02 (auto-compute max+1) + Name + Description + Steps (repeatable, reorderable): + Step 1: Phase=2 (DangSoanThao) SLA=7d + Approvers: +Role Drafter, +Role DeptManager + Step 2: Phase=3 (DangGopY) SLA=7d + Approvers: +Role ProjectManager, +User {userId alice} + ... + → Save → POST /api/workflows + BE atomically: + UPDATE WorkflowDefinitions SET IsActive=0 WHERE ContractType=5 AND IsActive=1; + INSERT WorkflowDefinitions (Id, Code='QT-MB', Version=2, IsActive=1, ...); + INSERT WorkflowSteps / WorkflowStepApprovers batch; + → trở về /system/workflows/MuaBan → v02 active badge, v01 archived "N HĐ còn chạy" + + → HĐ cũ pin v01 KHÔNG BỊ ẢNH HƯỞNG (Contract.WorkflowDefinitionId = v01.Id) + → HĐ mới tạo sau đó pick active → pin v02 +``` + +## SLA mặc định (khi pinned def không có SlaDays → fallback) + +| Phase | SLA fallback | |---|---| | DangSoanThao | 7d | | DangGopY | 7d | @@ -54,9 +122,11 @@ Bypass (HĐ Chủ đầu tư, BypassProcurementAndCCM=true): | DangDongDau | none | | DaPhatHanh | none | -## Role × Phase guard matrix +Nếu pinned WorkflowStep có `SlaDays > 0` → ưu tiên value của step đó. -Xem `ContractWorkflowService.Transitions` dictionary. Tóm tắt: +## Role × Phase guard matrix (hardcoded Standard) + +Xem `WorkflowPolicies.Standard.Transitions`. Tóm tắt: | Phase hiện tại → target | Roles được phép | |---|---| @@ -75,6 +145,8 @@ Xem `ContractWorkflowService.Transitions` dictionary. Tóm tắt: **Admin bypass:** user có role `Admin` → pass mọi guard. Dùng để test flow nhanh. +**Versioned override:** Nếu HĐ có `WorkflowDefinitionId` pin → allowed roles sẽ lấy từ `WorkflowStep.Approvers` (Role-kind) thay vì hardcoded. Admin có thể config 2 role bất kỳ cho step 3, guard theo đó. + ## Mã HĐ gen (RG-001) Xem `ContractCodeGenerator.GenerateAsync()`. Format theo loại HĐ: @@ -89,78 +161,116 @@ Xem `ContractCodeGenerator.GenerateAsync()`. Format theo loại HĐ: | HopDongNguyenTacNCC | `{Year}/NCC/SOL&{SupplierCode}/{Seq:D2}` ← framework | | HopDongNguyenTacDichVu | `{Year}/HĐDV/SOL&{SupplierCode}/{Seq:D2}` ← framework | -**Transactional:** `BeginTransactionAsync(IsolationLevel.Serializable)` + `ContractCodeSequences` row UPDATE. Tránh race condition khi 2 HĐ cùng prefix gen song song. +**Transactional:** `BeginTransactionAsync(IsolationLevel.Serializable)` + `ContractCodeSequences` row UPSERT. Tránh race condition khi 2 HĐ cùng prefix gen song song. **Gen khi nào:** transition sang `DangDongDau`. Nếu `MaHopDong` đã có (reject rồi approve lại) → giữ nguyên, không gen lại. -## Code pointers +## Code pointers (Tier 3 updated) -**Backend:** -- `Domain/Contracts/Contract.cs` — aggregate root +**Backend Domain:** +- `Domain/Contracts/Contract.cs` — aggregate root (+ `WorkflowDefinitionId?`) - `Domain/Contracts/ContractApproval.cs` — history - `Domain/Contracts/ContractComment.cs` — thread - `Domain/Contracts/ContractAttachment.cs` — files - `Domain/Contracts/ContractCodeSequence.cs` — seq table -- `Application/Contracts/Services/IContractWorkflowService.cs` + `IContractCodeGenerator.cs` -- `Infrastructure/Services/ContractWorkflowService.cs` — state + role guard -- `Infrastructure/Services/ContractCodeGenerator.cs` — transactional gen -- `Application/Contracts/ContractFeatures.cs` — CQRS (Create, Update draft, Transition, AddComment, List, Inbox, GetDetail, Delete) -- `Api/Controllers/ContractsController.cs` — REST endpoints +- `Domain/Contracts/WorkflowPolicy.cs` — record + `WorkflowPolicies.Standard/SkipCcm` + `WorkflowPolicyRegistry.{For, FromDefinition, ByName}` +- **`Domain/Contracts/WorkflowDefinition.cs`** — versioned policy header +- **`Domain/Contracts/WorkflowStep.cs`** — step trong definition +- **`Domain/Contracts/WorkflowStepApprover.cs`** — Role/User approver (+ `ApproverKind` enum) +- `Domain/Contracts/WorkflowTypeAssignment.cs` — legacy admin override -**Frontend:** -- `fe-admin/src/pages/contracts/ContractsListPage.tsx` — full list admin view -- `fe-admin/src/pages/contracts/ContractDetailPage.tsx` — detail + timeline + action -- `fe-user/src/pages/InboxPage.tsx` — HĐ chờ role tôi xử lý -- `fe-user/src/pages/contracts/ContractCreatePage.tsx` — tạo HĐ draft -- `fe-user/src/pages/contracts/ContractDetailPage.tsx` — duplicate có chủ đích -- `fe-user/src/pages/contracts/MyContractsPage.tsx` — HĐ của tôi -- `fe-admin/src/types/contracts.ts` + `fe-user/src/types/contracts.ts` — type mirror -- `fe-admin/src/components/PhaseBadge.tsx` — badge màu theo phase +**Backend Application:** +- `Application/Contracts/Services/IContractWorkflowService.cs` + `IContractCodeGenerator.cs` +- `Application/Contracts/ContractFeatures.cs` — CQRS (Create pin WorkflowDefId, Update draft, Transition, AddComment, List, Inbox, GetDetail, Delete) +- `Application/Contracts/ContractAttachmentFeatures.cs` — Upload/Download/Delete CQRS +- **`Application/Contracts/WorkflowAdminFeatures.cs`** — `GetWorkflowAdminOverviewQuery` + `CreateWorkflowDefinitionCommand` + +**Backend Infrastructure:** +- `Infrastructure/Services/ContractWorkflowService.cs` — resolve policy (pinned → override → fallback), state + role guard +- `Infrastructure/Services/ContractCodeGenerator.cs` — transactional gen +- `Infrastructure/Services/NotificationService.cs` — write to DbContext (caller atomicity) +- `Infrastructure/Persistence/Interceptors/NotificationPushInterceptor.cs` — auto-push via SignalR + +**Backend Api:** +- `Api/Controllers/ContractsController.cs` — REST endpoints +- **`Api/Controllers/WorkflowsController.cs`** — admin overview + create new version +- `Api/Hubs/NotificationHub.cs` + `Api/Realtime/SignalRNotifier.cs` + +**Frontend Admin:** +- `fe-admin/src/pages/contracts/{ContractsListPage, ContractCreatePage, ContractDetailPage}.tsx` +- `fe-admin/src/pages/system/WorkflowsPage.tsx` — URL-driven landing + per-type +- `fe-admin/src/components/workflow/{WorkflowDesigner, DefinitionCard}.tsx` +- `fe-admin/src/components/{PhaseBadge, WorkflowSummaryCard, ContractAttachmentsSection, DynamicForm, SlaTimer}.tsx` + +**Frontend User:** +- `fe-user/src/pages/InboxPage.tsx` — filter `?type=X` +- `fe-user/src/pages/contracts/{ContractCreatePage, ContractDetailPage, MyContractsPage}.tsx` +- `fe-user/src/components/{Layout, ContractAttachmentsSection, SlaTimer, NotificationBell}.tsx` ## API endpoints | Method | Path | Purpose | |---|---|---| -| GET | `/api/contracts` | List với filter phase/supplier/project + paging | +| GET | `/api/contracts` | List với filter phase/supplier/project/type + paging + pendingMe | | GET | `/api/contracts/inbox` | HĐ chờ role của user xử lý | -| GET | `/api/contracts/{id}` | Detail + approvals + comments + attachments | -| POST | `/api/contracts` | Tạo draft (Phase = DangSoanThao) | +| GET | `/api/contracts/{id}` | Detail + approvals + comments + attachments + pinned workflow | +| POST | `/api/contracts` | Tạo draft — pin `WorkflowDefinitionId = active version for type` | | PUT | `/api/contracts/{id}` | Update draft (chỉ khi Phase = DangSoanThao) | | POST | `/api/contracts/{id}/transitions` | Chuyển phase (body: `{targetPhase, decision, comment}`) | | POST | `/api/contracts/{id}/comments` | Thêm comment vào thread | +| POST | `/api/contracts/{id}/attachments` | Upload file (multipart, 20MB, MIME whitelist) | +| GET | `/api/contracts/{id}/attachments/{aid}` | Download stream | +| DELETE | `/api/contracts/{id}/attachments/{aid}` | Delete (+ cleanup file) | | DELETE | `/api/contracts/{id}` | Soft delete (chỉ < DangInKy) | +| **GET** | **`/api/workflows`** | Admin: overview per ContractType (active + history + "N HĐ còn chạy") | +| **GET** | **`/api/workflows/{type}`** | Per-type definitions + steps + approvers | +| **POST** | **`/api/workflows`** | Create new version (auto-increment + deactivate old) | ## Guard Rules đã implement -- **State adjacency:** chỉ cho chuyển giữa các (from, to) đã khai báo trong `Transitions` dict -- **Role check:** role của actor phải ∈ allowed roles của transition đó +- **State adjacency:** chỉ cho chuyển giữa các (from, to) đã khai báo trong `policy.Transitions` (pinned def hoặc hardcoded) +- **Role check:** role của actor phải ∈ allowed roles của transition đó (từ Role-kind approvers hoặc hardcoded) - **Admin bypass:** role `Admin` pass mọi check -- **System bypass:** `actorUserId == null` + `Decision = AutoApprove` → cho phép (dành cho SLA job Phase 3.2) -- **Bypass CCM:** `Contract.BypassProcurementAndCCM=true` cho phép `DangInKy → DangTrinhKy` (skip CCM). Default false → phải qua CCM +- **System bypass:** `actorUserId == null` + `Decision = AutoApprove` → cho phép (dành cho SLA job) +- **Bypass CCM:** `Contract.BypassProcurementAndCCM=true` cho phép `DangInKy → DangTrinhKy` (skip CCM) - **Self-delete:** không cho xóa HĐ đã qua `DangInKy` +- **Versioned pin:** `Contract.WorkflowDefinitionId` pinned at create → không update sau đó. FK restrict → admin không xóa được def nếu HĐ còn tham chiếu -## Workflow tạo HĐ end-to-end (testable) +## Workflow tạo HĐ end-to-end (testable, Tier 3) ```bash -# 1. Setup master data -POST /api/suppliers { code: "PVL", name: "...", type: 1 } -POST /api/projects { code: "FLOCK 01", name: "..." } +# 1. Setup master data (auto-seeded: 5 supplier + 3 project + 9 dept) +# 2. Admin tạo version mới cho HĐ Mua bán +POST /api/workflows +{ + "code": "QT-MB", + "name": "Quy trình Mua bán v02", + "contractType": 5, + "steps": [ + { "order": 1, "phase": 2, "name": "Soạn thảo", "slaDays": 7, + "approvers": [{ "kind": 1, "assignmentValue": "Drafter" }, { "kind": 1, "assignmentValue": "DeptManager" }] }, + { "order": 2, "phase": 3, "name": "Góp ý", "slaDays": 7, + "approvers": [{ "kind": 1, "assignmentValue": "ProjectManager" }, { "kind": 2, "assignmentValue": "{userId}" }] }, + ... + ] +} +# → Version=02, IsActive=1, v01 deactivated -# 2. Tạo HĐ -POST /api/contracts { type: 2, supplierId, projectId, giaTri: 150000000, tenHopDong: "..." } -# → Phase = DangSoanThao, SlaDeadline = +7d +# 3. User tạo HĐ Mua bán → pin WorkflowDefinitionId = v02.Id +POST /api/contracts { type: 5, supplierId, projectId, giaTri: ..., tenHopDong: "..." } +# → Phase=DangSoanThao, SlaDeadline=+7d, WorkflowDefinitionId=v02 -# 3. Submit góp ý -POST /api/contracts/{id}/transitions { targetPhase: 3, decision: 1, comment: "..." } +# 4. Transition — guard load từ v02.Steps.Approvers +POST /api/contracts/{id}/transitions { targetPhase: 3, decision: 1, comment: "..." } -# 4. Chuyển qua các phase (với admin) -→ 4 DangDamPhan → 5 DangInKy → 6 DangKiemTraCCM → 7 DangTrinhKy +# 5. Chuyển qua các phase +→ 4 DangDamPhan → 5 DangInKy → (skip CCM nếu SkipCcm policy) → 7 DangTrinhKy -# 5. BOD ký → gen mã HĐ +# 6. BOD ký → gen mã HĐ → 8 DangDongDau - # contract.MaHopDong = "FLOCK 01/HĐGK/SOL&PVL/01" + # contract.MaHopDong = "FLOCK 01/MB/SOL&PVL/01" -# 6. HRA đóng dấu + phát hành +# 7. HRA đóng dấu + phát hành → 9 DaPhatHanh ``` @@ -171,15 +281,17 @@ POST /api/contracts/{id}/transitions { targetPhase: 3, decision: 1, comment: ". - **Race condition gen mã song song** → dùng `IsolationLevel.Serializable`, không skip. - **SLA Deadline không reset khi reject** → `TransitionAsync` luôn reset theo target phase, kể cả reject. - **Comment ở phase sai** → `AddCommentCommand` luôn lấy phase hiện tại tại thời điểm comment. -- **FE hiển thị next phase button** → map `NEXT_PHASES` ở FE phải match BE `Transitions`. Nếu BE đổi, FE quên update → user click → 403. +- **FE hiển thị next phase button** → RESOLVED Tier 3. FE dùng `contract.workflow.nextPhases` từ BE (pinned policy → single source of truth). +- **WorkflowDefinition cascade delete** → NÊN restrict FK. Nếu cascade sẽ xóa Contract cũ → data loss. Đã fix trong migration. +- **User-kind approver không enforce runtime** — designer cho chọn nhưng guard v1 chỉ check Role. Iter 2 cần wire `step.Approvers.Where(a => a.Kind == User)` vào check. -## Phase 3 iteration 2 (còn thiếu) +## Tier 4+ (còn thiếu / future) -- [ ] `SlaExpiryJob` BackgroundService — auto-approve khi quá hạn (xem `docs/flows/sla-expiry-flow.md`) -- [ ] Warning notification khi còn 20% SLA -- [ ] Email notification (MailKit) khi chuyển phase -- [ ] In-app notification badge — SignalR push -- [ ] Upload attachment endpoint + FE (multipart) -- [ ] RowVersion optimistic concurrency (2 user cùng duyệt) +- [ ] Warning notification 20% SLA (`SlaWarningSent` flag đã có) +- [ ] User-kind approver runtime guard (data model ready) +- [ ] Email notification (MailKit) khi chuyển phase — BLOCKED SMTP +- [ ] RowVersion optimistic concurrency (2 user cùng duyệt → 409) - [ ] ContractClause appendix attach khi export HĐ trọn gói - [ ] Audit log riêng (`AuditLogs` table) ngoài `ContractApprovals` +- [ ] MediatR `AuditBehavior` — log mọi command +- [ ] E-signature integration (VNPT/FPT CA) diff --git a/docs/HANDOFF.md b/docs/HANDOFF.md index 759b2ce..f2c233a 100644 --- a/docs/HANDOFF.md +++ b/docs/HANDOFF.md @@ -1,28 +1,29 @@ # HANDOFF — Brief 5 phút cho session tiếp theo -**Last updated:** 2026-04-21 16:30 (cuối Phase 5.1 Security + Users Mgmt) +**Last updated:** 2026-04-22 03:00 (post-Tier-3-feature-complete + versioned workflow) + +## TL;DR + +Tier 3 ERP features xong hết (Attachment, SignalR, Form builder, PDF, Versioned workflow, Nested menu, Permission layout). Prod live 3 domain. **Còn lại chủ yếu là UAT + SMTP + rotate creds**, không còn module kỹ thuật lớn nào chưa làm. ## Ở đâu rồi? | Phase | Trạng thái | |---|---| | 0 Draft | ✅ Done | -| 1 Alpha Core foundation | ✅ Done | -| 1 Alpha Core đợt 2 (CRUD + Permission) | ✅ Done | -| 2 Form Engine MVP | ✅ Done | -| 2 Form Engine iteration 2 | 📝 Optional | -| 3 Workflow MVP (9 phase + code gen) | ✅ Done | -| 3 Workflow iteration 2 (SLA + notify + attachment) | 📝 Optional | -| 4 Report MVP (Dashboard + Excel) | ✅ Done | -| 4 Report iteration 2 | 📝 Optional | -| 5 Prep (infra + scripts + guides + refresh token) | ✅ Done | -| **5.1 Security + Users Mgmt (headers, lockout, Users CRUD)** | ✅ Done (IDOR + deps scan còn) | -| 5 Deploy production (cần Gitea URL) | 📋 Next | +| 1 Alpha Core (foundation + đợt 2 CRUD + Permission) | ✅ Done | +| 2 Form Engine MVP + iter 2 (upload UI + .doc auto-convert + PDF export) | ✅ Done | +| 3 Workflow MVP (9 phase + code gen) + iter 2 (SLA job + attachment + notify) | ✅ Done | +| 4 Report MVP (Dashboard + Excel) + user-specific dashboard | ✅ Done | +| 5 Prep + 5.1 Security + Users Mgmt | ✅ Done | +| **5 Deploy prod** (3 domain HTTPS live) | ✅ Done | +| **Tier 3 (Attach + Realtime + Form builder + PDF + Versioned WF + Nested menu + Permission 3-panel)** | ✅ Done | +| 6+ Post-launch (E-signature, Bravo/SAP, Mobile, AI) | 📝 Future | ## Run nhanh ```powershell -# Terminal 1 — API (auto seed 8 template lần đầu) +# Terminal 1 — API (auto seed 12 role + 9 dept + 5 supplier + 3 project + 8 template + 7 workflow definition + 28 ContractType menu + 7 workflow menu) dotnet run --project src\Backend\SolutionErp.Api # Terminal 2 — Admin FE @@ -34,144 +35,218 @@ cd fe-user && npm run dev # → http://localhost:8080 Login: `admin@solutionerp.local` / `Admin@123456` -Điểm cần test ngay (Phase 4 MVP): -- **Admin `/dashboard`** → 5 KPI card + By Phase bar + Monthly chart + Top NCC/dự án -- **Admin `/reports`** → filter phase/date → Export Excel .xlsx 10 cột có formula SUM -- **fe-user `/contracts/new`** → tạo HĐ draft (Phase 2 DangSoanThao, SLA +7d) -- **fe-user `/inbox`** → xem HĐ chờ role mình xử lý -- **`/contracts/{id}`** → click "Duyệt → tiếp" chạy state machine. Phase 8 gen `MaHopDong` RG-001 -- **`/forms`** → render template .docx -- **`/system/permissions`** → ma trận Role × MenuKey -- **`/master/suppliers|projects|departments`** → CRUD +## Quick sanity-check -## Cần làm kế tiếp (ưu tiên) +**Admin (:8082):** +- `/dashboard` → "Của tôi" row 4 card + KPI cards + charts +- `/contracts` → list toàn bộ, filter phase/supplier/project +- `/contracts/new?type=5` → tạo HĐ Mua bán, pre-select type từ URL +- `/contracts/{id}` → timeline + action dialog + attachments drag-drop + WorkflowSummaryCard +- `/system/workflows` → 7-card landing (Thầu phụ/Giao khoán/NCC/Dịch vụ/Mua bán/NguyenTacNcc/NguyenTacDv) +- `/system/workflows/MuaBan` → DefinitionCard active + history + "Tạo phiên bản mới" modal với Steps + Approvers (+Role / +User) +- `/system/permissions` → 3-panel layout (Role list | Menu×CRUD matrix | Granted stats) +- `/system/users` → Users CRUD + assign roles +- `/forms` → upload .docx/.xlsx + render dialog Form↔JSON + Tải PDF -### A. Phase 5 — Production (tuần 12-13, item lớn nhất còn lại) +**User (:8080):** +- `/inbox?type=5` → HĐ Mua bán chờ role mình +- `/my-contracts?type=2` → HĐ Thầu phụ của tôi +- `/contracts/new?type=3` → tạo HĐ NCC +- Sidebar nested: 📄 Hợp đồng → expand 7 type → expand "HĐ Mua bán" → Danh sách / Thao tác / Duyệt -**Đọc trước:** `docs/changelog/migration-todos.md` section Phase 5. +**Realtime check:** +- Login 2 tab (admin + user) → user tạo comment / transition → admin nhận toast + bell +1 -- [ ] `.gitea/workflows/deploy.yml` CI/CD build + deploy IIS -- [ ] `scripts/deploy-iis.ps1` stop app pool → xcopy → start -- [ ] Windows Server IIS + URL Rewrite + ARR (reverse proxy FE → .NET) -- [ ] HTTPS cert via win-acme hoặc mua -- [ ] `appsettings.Production.json` + user secrets + JWT secret rotation -- [ ] Rate limiting middleware (auth endpoint 5 req/min/IP) -- [ ] Security audit OWASP top 10 -- [ ] Health check endpoint `/health` -- [ ] Serilog → file rolling daily retention 30d -- [ ] SQL backup: daily full + 15min log -- [ ] Runbook: restart, rollback migration, restore backup -- [ ] UAT production 1 tuần với 2-3 user thật -- [ ] Go-live checklist +## Cần làm kế tiếp -### B. Polish iterations (optional — khi rảnh) +### A. Hard blockers (chờ user / ops) -**Phase 2 iter 2:** convert 3 .doc qua Word COM `DisplayAlerts=0` hoặc LibreOffice, field spec JSON + form builder FE dynamic, `{{#loop}}` block support, PDF convert, FE upload template multipart. +1. **UAT thật 1 tuần với 2-3 user** — hard requirement từ roadmap. Kiến nghị: + - User A: Drafter (QS/NV.PB) — tạo 3 HĐ mỗi type, đi hết 9 phase + - User B: CCM — duyệt phase 6 + - User C: BOD — duyệt phase 7 + - Ghi bug / friction / đề xuất → backlog iter 2 +2. **SMTP config** để bật Email outbox: + ```json + "Email": { + "Host": "smtp.gmail.com", + "Port": 587, + "Username": "...", + "Password": "...", + "From": "noreply@solutionerp.local" + } + ``` + Khi có → thêm `MailKit`, `IEmailSender`, hook vào `NotificationService.CreateAsync` ngay trước khi enqueue realtime push. +3. **Rotate credentials** — SA SQL password, vrapp password, JWT secret prod, Gitea runner registration token +4. **Schedule SQL backup** — `schtasks /create /tn "SolutionErp Backup" /tr "powershell -File C:\...\scripts\backup-sql.ps1" /sc DAILY /st 03:00` -**Phase 3 iter 2:** `SlaExpiryJob` BackgroundService auto-approve, email (MailKit) + in-app (SignalR) notify, upload attachment endpoint + FE multipart (`wwwroot/uploads/contracts/{id}/`), RowVersion concurrency, render HĐ docx khi tạo (merge TemplateId + DraftData + ContractClause). +### B. Polish iterations (optional — khi UAT phát sinh) -**Phase 4 iter 2:** SLA overdue report by role/phase, PDF HĐ export (LibreOffice), dashboard user-specific (role tôi). +- **Roles CRUD** — admin tạo custom role ngoài 12 hardcoded (`Domain.Identity.AppRoles`) +- **User-kind approver runtime** — data model `WorkflowStepApprover.Kind=User` + `AssignmentValue=userId` đã có, chỉ cần: + ```csharp + // ContractWorkflowService.TransitionAsync (bổ sung): + var userApprovers = step.Approvers.Where(a => a.Kind == ApproverKind.User) + .Select(a => Guid.Parse(a.AssignmentValue)); + if (userApprovers.Any() && !userApprovers.Contains(actorUserId)) + throw new ForbiddenException(); + ``` +- **Grant `Workflows.Read` cho non-admin role** trong PermissionsPage → menu Wf_* auto-visible (inheritance đã có) +- **Warning notification 20% SLA** — job emit khi `SlaDeadline - now < sla * 0.2 && !SlaWarningSent`, set flag +- **Reject → DangSoanThao E2E test** với 3 role khác nhau +- **Deps scan CI** — `dotnet list package --vulnerable` + `npm audit --audit-level=high` -### C. Phase 2 iteration 2 (form engine polish) +### C. Non-goals / parked -- Convert 3 file `.doc` qua Word COM `DisplayAlerts=0` + timeout, hoặc LibreOffice -- Field spec JSON per template + dynamic form builder FE -- `{{#loop}}...{{/loop}}` block support cho table lặp -- PDF convert via LibreOffice headless -- Admin upload template UI (multipart) - -### D. Quick wins (không block phase) - -- FE Users management + Roles CRUD (test permission với non-admin user thật) -- Filter Inbox theo phase FE -- Refresh token auto (FE axios interceptor retry 401) +- E-signature (VNPT CA / FPT CA) — Phase 6 +- Bravo/SAP import NCC — Phase 6 +- Mobile app — Phase 6 +- AI OCR scan HĐ — Phase 6+ ## Lưu ý kỹ thuật quan trọng -**Đọc [`gotchas.md`](gotchas.md) trước khi:** -- Thêm package mới → check compat với .NET 10 (MediatR 14 fail → dùng 12) -- Debug 404 API → kiểm Program.cs có persist không (Dropbox issue) -- Expression tree error → tách switch ra ngoài LINQ -- TS enum error → dùng const-object pattern (`erasableSyntaxOnly`) -- Word COM stuck → kill + fallback LibreOffice -- Migration lỗi → check 3 file đầy đủ (Designer + Migration + Snapshot) +**Đọc [`gotchas.md`](gotchas.md) (26 bẫy) trước khi:** -## File đang active +- Thêm package mới → .NET 10 compat (MediatR 14 fail → dùng 12.4.1) +- Debug TS enum error → dùng const-object pattern (`erasableSyntaxOnly`) +- Expression tree lỗi → tách switch ra ngoài LINQ +- Deploy Windows Feature (WebSockets, etc.) → unlock section ở applicationHost (gotcha #25) +- Workflow transition 403 → check `Contract.WorkflowDefinitionId` pin đúng không +- Migration lỗi → 3 file đầy đủ (Designer + Migration + Snapshot) + +## Versioned workflow — quick reference + +``` +Contract.WorkflowDefinitionId (nullable Guid FK) + → pin tại `CreateContractCommandHandler` = WorkflowDefinitions.Single(d => d.ContractType == c.Type && d.IsActive) + → ContractWorkflowService.LoadAsync(contractId): + if contract.WorkflowDefinitionId != null: + def = db.WorkflowDefinitions.Include(Steps.Approvers).First(wfId) + return WorkflowPolicyRegistry.FromDefinition(def) + else if admin override ở WorkflowTypeAssignments: + return Registry.ByName(override.PolicyName) + else: + return Registry.For(contract.Type) // hardcoded Standard/SkipCcm + +Admin tạo version mới: + POST /api/workflows + body: { code, name, contractType, steps: [{ order, phase, name, slaDays, approvers: [{ kind, assignmentValue }] }] } + → auto increment Version = max(Version where Code==code) + 1 + → deactivate old IsActive trong cùng ContractType (atomic) + → HĐ cũ ĐÃ PIN WorkflowDefinitionId = old Id → vẫn chạy policy cũ ✓ +``` + +Invariants: +- `UNIQUE (Code, Version)` per WorkflowDefinitions +- **Chỉ 1 IsActive=true per ContractType** tại 1 thời điểm +- `Contract.WorkflowDefinitionId` KHÔNG cascade khi xóa WorkflowDefinition → protect history + +## File đang active (hiện trạng) ``` SOLUTION_ERP/ -├── src/Backend/ (Clean Arch, 4 project, .NET 10) +├── src/Backend/ (Clean Arch, 4 project, .NET 10) │ ├── SolutionErp.Domain/ -│ │ ├── Common/ BaseEntity, AuditableEntity -│ │ ├── Contracts/ ContractType, ContractPhase, ApprovalDecision + **Contract, ContractApproval, ContractComment, ContractAttachment, ContractCodeSequence** ← Phase 3 -│ │ ├── Forms/ ContractTemplate, ContractClause ← Phase 2 -│ │ ├── Identity/ User, Role, MenuItem, Permission, AppRoles, MenuKeys -│ │ └── Master/ Supplier, Project, Department, SupplierType +│ │ ├── Common/ BaseEntity, AuditableEntity +│ │ ├── Contracts/ ContractType, ContractPhase, ApprovalDecision, +│ │ │ Contract (+WorkflowDefinitionId), ContractApproval, +│ │ │ ContractComment, ContractAttachment, ContractCodeSequence, +│ │ │ **WorkflowPolicy** (record + registry + FromDefinition), +│ │ │ **WorkflowDefinition** (Code+Version+IsActive+ContractType), +│ │ │ **WorkflowStep** (Order+Phase+Name+SlaDays), +│ │ │ **WorkflowStepApprover** (Kind=Role|User, AssignmentValue), +│ │ │ **WorkflowTypeAssignment** (admin override legacy) +│ │ ├── Forms/ ContractTemplate (+FieldSpec JSON), ContractClause +│ │ ├── Identity/ User, Role, MenuItem, Permission, AppRoles, +│ │ │ **MenuKeys** (+ContractTypeCodes, ContractTypeGroup/List/Create/Pending helpers, WorkflowTypeLeaf) +│ │ ├── Master/ Supplier (+SupplierType), Project, Department +│ │ └── Notifications/ **Notification** (+NotificationType enum) │ ├── SolutionErp.Application/ -│ │ ├── Auth/ Login, Refresh, Me -│ │ ├── Common/ Exceptions, Behaviors, Interfaces, Models -│ │ ├── Contracts/ ContractFeatures (8 CQRS), IContractWorkflowService, IContractCodeGenerator, DTOs ← Phase 3 -│ │ ├── Forms/ FormFeatures (List/Get/Render) ← Phase 2 -│ │ ├── Master/ Suppliers, Projects, Departments CQRS -│ │ ├── Permissions/ GetMyMenuTree, matrix upsert -│ │ └── Reports/ **DashboardStats, ExportContractsToExcel, IContractExcelExporter** ← Phase 4 +│ │ ├── Auth/ Login, Refresh, Me +│ │ ├── Common/ +│ │ │ └── Interfaces/ IApplicationDbContext, ICurrentUser, IDateTime, +│ │ │ IJwtTokenService, **IFileStorage**, **IDocumentConverter**, +│ │ │ **IRealtimeNotifier**, **INotificationService** +│ │ ├── Contracts/ ContractFeatures, IContractWorkflowService, +│ │ │ **ContractAttachmentFeatures** (Upload/Download/Delete CQRS), +│ │ │ **WorkflowAdminFeatures** (Overview + CreateNewVersion) +│ │ ├── Forms/ FormFeatures (List/Get/Render/**Upload/Update/Delete/ExportPdf**) +│ │ ├── Master/ Suppliers, Projects, Departments CQRS +│ │ ├── **Notifications/** NotificationFeatures (List/UnreadCount/MarkRead/MarkAllRead) +│ │ ├── Permissions/ GetMyMenuTree (**+inherit Contracts/Workflows**) +│ │ └── Reports/ DashboardStats, ExportToExcel, **MyDashboard** │ ├── SolutionErp.Infrastructure/ -│ │ ├── Forms/ DocxRenderer, XlsxRenderer, FormRenderer ← Phase 2 -│ │ ├── Identity/ JwtSettings, JwtTokenService -│ │ ├── Persistence/ DbContext, DbInitializer, Interceptors, Migrations (**5**) -│ │ ├── Reports/ **ContractExcelExporter** ← Phase 4 -│ │ └── Services/ DateTimeService, ContractWorkflowService, ContractCodeGenerator ← Phase 3 +│ │ ├── Forms/ DocxRenderer, XlsxRenderer, FormRenderer, +│ │ │ **LibreOfficeDocumentConverter** +│ │ ├── Identity/ JwtSettings, JwtTokenService +│ │ ├── Persistence/ +│ │ │ ├── Interceptors/ AuditingInterceptor, **NotificationPushInterceptor** +│ │ │ └── Migrations/ 8 migrations +│ │ ├── Reports/ ContractExcelExporter +│ │ ├── **Storage/** LocalFileStorage (path-traversal guard) +│ │ └── Services/ DateTimeService, **ContractWorkflowService (load pinned def)**, +│ │ ContractCodeGenerator, **NotificationService** │ └── SolutionErp.Api/ -│ ├── Authorization/ MenuPermissionHandler + Requirement -│ ├── Controllers/ Auth, Suppliers, Projects, Departments, Menus, Roles, Permissions, Forms, Contracts, **Reports** (10 controller) -│ ├── Middleware/ GlobalExceptionMiddleware -│ ├── Services/ CurrentUserService, WebHostEnvironmentLocator -│ └── wwwroot/templates/ 5 file .docx/.xlsx ← Phase 2 -├── fe-admin/ (11 page) -│ └── src/pages/ -│ ├── LoginPage -│ ├── DashboardPage ← Phase 4 rewrite (KPI cards + BarChart) -│ ├── master/SuppliersPage, ProjectsPage, DepartmentsPage -│ ├── system/PermissionsPage -│ ├── forms/FormsPage ← Phase 2 -│ ├── contracts/ContractsListPage, ContractDetailPage ← Phase 3 -│ └── ReportsPage ← Phase 4 -├── fe-user/ (5 page) -│ └── src/pages/ -│ ├── LoginPage -│ ├── InboxPage ← Phase 3 -│ └── contracts/ContractCreatePage, ContractDetailPage, MyContractsPage ← Phase 3 -├── docs/ (35 file) -│ ├── STATUS.md, HANDOFF.md, rules.md, architecture.md -│ ├── CLAUDE.md, PROJECT-MAP.md -│ ├── workflow-contract.md, forms-spec.md -│ ├── database/{database-guide, schema-diagram}.md +│ ├── Authorization/ MenuPermissionHandler + Requirement +│ ├── Controllers/ Auth, Suppliers, Projects, Departments, Menus, +│ │ Roles, Permissions, Forms, Contracts, Reports, +│ │ Users, **Notifications**, **Workflows** +│ ├── **Hubs/** NotificationHub (/hubs/notifications) +│ ├── Middleware/ GlobalExceptionMiddleware +│ ├── **Realtime/** SignalRNotifier +│ ├── Services/ CurrentUserService, WebHostEnvironmentLocator +│ └── wwwroot/templates/ .docx/.xlsx templates +├── fe-admin/ (~18 page) +│ └── src/ +│ ├── pages/ +│ │ ├── LoginPage, DashboardPage (MyDashboardRow) +│ │ ├── master/ Suppliers, Projects, Departments +│ │ ├── system/ **PermissionsPage (3-panel)**, **WorkflowsPage (URL-driven)**, Users +│ │ ├── forms/ FormsPage (upload + Form/JSON + PDF) +│ │ ├── contracts/ List, Detail (+Attachments), **Create** +│ │ └── ReportsPage +│ ├── components/ Layout (recursive menu + filterForAdmin), +│ │ TopBar, NotificationBell, UserMenu, SlaTimer, +│ │ EmptyState, PhaseBadge, WorkflowSummaryCard, +│ │ ContractAttachmentsSection, DynamicForm, +│ │ **WorkflowDesigner** (Steps + Approvers modal) +│ └── lib/ api.ts, realtime.ts, cn.ts +├── fe-user/ (~10 page) +│ └── src/ +│ ├── pages/ Login, Inbox (+?type filter), +│ │ contracts/{Create, Detail, MyContracts} +│ ├── components/ Layout (recursive + filterForUser + USER_FIXED_TOP), +│ │ NotificationBell, ContractAttachmentsSection, SlaTimer +│ └── lib/ realtime.ts (same singleton pattern) +├── docs/ (~40 file) +│ ├── STATUS, HANDOFF, rules, architecture, CLAUDE, PROJECT-MAP (6) +│ ├── workflow-contract, forms-spec (2) +│ ├── database/{database-guide, schema-diagram} (2) │ ├── flows/ (7 file — README + 6 flow) -│ ├── guides/ (4 file) — deployment-iis, cicd, security-checklist, runbook ← Phase 5 prep -│ ├── changelog/migration-todos.md + sessions/ (7 session log) -│ └── gotchas.md -├── scripts/ (5 file PS + py) -│ ├── parse_forms.py, parse_workflow.py (Phase 0) -│ ├── convert-doc-to-docx.ps1 (Phase 2) -│ └── deploy-iis.ps1, backup-sql.ps1 ← Phase 5 prep -├── .gitea/workflows/deploy.yml ← Phase 5 prep CI/CD template -└── .claude/skills/ (3 skill — all full spec) +│ ├── guides/ (4 file) +│ ├── changelog/migration-todos + sessions/ (8 session log) +│ └── gotchas (26 pitfall) +├── scripts/ (5 PS + py) +│ ├── parse_forms, parse_workflow (Phase 0) +│ ├── convert-doc-to-docx (Phase 2) +│ ├── deploy-iis, backup-sql (Phase 5) +│ └── install-libreoffice (Tier 3) +├── .gitea/workflows/deploy.yml CI/CD Windows self-hosted runner +└── .claude/skills/ 3 skill (contract-workflow, form-engine, permission-matrix) ``` ## Git state ``` -(sẽ là commit 8) — Phase 5 Prep (infra + scripts + guides + refresh token) -fe7ad8e — Phase 4 Report MVP + docs consolidation -7e957a7 — Phase 3 Workflow MVP -5113e4c — Phase 2 Form Engine MVP -54d6c9b — Phase 1.2 CRUD + Permission -49a5f57 — Docs database-guide + flows -702411f — Phase 1 foundation -25dad7f — Phase 0 scaffold +HEAD → main +91b2da1 — PermissionsPage 3-panel layout (LATEST) +f216169 — Admin Workflows tabs → sidebar menu items +355bbe3 — Fix Dialog size TS (xl → lg) +e7e5f2d — Versioned workflow entities + migration + designer +4 session trước đó nằm trong STATUS table -Branch: main -Remote: chưa (Gitea URL CẦN NGAY để Phase 5 go-live) +Branch: main (tracking origin/main) +Remote: https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp.git ``` ## Credentials + URLs @@ -180,23 +255,27 @@ Remote: chưa (Gitea URL CẦN NGAY để Phase 5 go-live) admin@solutionerp.local / Admin@123456 ``` -- API: http://localhost:5443 (swagger `/swagger`) -- Admin FE: http://localhost:8082 -- User FE: http://localhost:8080 -- SQL LocalDB: `(localdb)\MSSQLLocalDB` / Database=`SolutionErp_Dev` +- API prod: https://api.huypham.vn — `/health/live`, `/health/ready` +- Admin FE prod: https://admin.huypham.vn +- User FE prod: https://user.huypham.vn +- API dev: http://localhost:5443 — `/swagger` (Dev only) +- Admin FE dev: http://localhost:8082 +- User FE dev: http://localhost:8080 +- SQL dev: `(localdb)\MSSQLLocalDB` / `SolutionErp_Dev` +- SQL prod: `.\SQLEXPRESS` / `SolutionErp` / `vrapp` (⚠️ rotate) ## Đánh giá nhanh **Tốt:** -- Build pass 100% cả BE + FE -- E2E test full 9-phase workflow end-to-end — mã HĐ gen đúng format RG-001 -- Docs đầy đủ: 26 file, session log mỗi chunk, gotchas tích lũy 17 pitfall -- Cả 2 FE đều có Contract detail page + timeline + comment thread + state machine action +- 3 domain HTTPS prod live, CI/CD xanh +- Tier 3 feature-complete: attachment, realtime, form builder (upload + DynamicForm + PDF), versioned workflow (admin-configurable per ContractType, pin per contract), nested menu per type, 3-panel permissions +- Clean-arch 3-project split đúng cho 2 cross-cutting service (realtime + document-converter) +- 26 gotchas tích lũy, 8 session log, 40 docs — agent onboard nhanh +- Invariant critical: "HĐ cũ giữ quy trình cũ" guaranteed by pinning (reference-based immutability, không snapshot copy) **Rủi ro còn:** -- SLA chỉ set deadline — không có job auto-approve (Phase 3.2) -- Không có notification (email + in-app) — user phải F5 inbox -- Form render chỉ MVP — loop table + PDF chưa có -- Permission matrix chưa test thực với non-admin user -- 3 file .doc chưa convert (carryover Phase 2) -- Không có upload attachment endpoint (chỉ có entity + DTO) +- UAT thật chưa chạy → có thể phát hiện edge case missing +- SMTP chưa có → notification chỉ in-app (toast + bell), không email +- User-kind approver chưa enable guard runtime (designer cho pick, nhưng transition dùng Role fall-back) +- Credentials chưa rotate +- SQL backup chưa schedule Task Scheduler diff --git a/docs/PROJECT-MAP.md b/docs/PROJECT-MAP.md index 5703bb6..443aedd 100644 --- a/docs/PROJECT-MAP.md +++ b/docs/PROJECT-MAP.md @@ -2,153 +2,240 @@ > Đọc file này nếu cần deep context (~15 phút). Nếu chỉ cần snapshot → đọc [`STATUS.md`](STATUS.md). -## Module map +## Module map (hiện trạng post-Tier-3) ``` ┌─────────────────────────────────────────────────────────────────┐ │ SOLUTION_ERP │ +│ 🌐 Prod live: api/admin/user.huypham.vn (HTTPS Let's Encrypt) │ └─────────────────────────────────────────────────────────────────┘ ╔════════════════╗ ╔════════════════╗ ╔════════════════╗ ║ IDENTITY ║ ║ DANH MỤC ║ ║ QUẢN LÝ HĐ ║ - ║ (Phase 1) ║ ║ (Phase 1) ║ ║ (Phase 1-3) ║ + ║ (Phase 1) ✅ ║ ║ (Phase 1) ✅ ║ ║ (Phase 1-3) ✅ ║ ╠════════════════╣ ╠════════════════╣ ╠════════════════╣ - ║ User ║ ║ Supplier ║ ║ Contract ║ - ║ Role ║ ║ Project ║ ║ ContractType ║ - ║ Permission ║ ║ Department ║ ║ ContractForm ║ - ║ MenuKey ║ ║ ContractClause ║ ║ Approval ║ - ║ AuditLog ║ ║ (điều kiện ║ ║ Comment ║ - ║ ║ ║ chung - 002.04)║ ║ Attachment ║ - ╚════════════════╝ ╚════════════════╝ ║ AuditTrail ║ - ╚════════════════╝ + ║ User (+Mgmt) ║ ║ Supplier ║ ║ Contract ║ + ║ Role (12 seed) ║ ║ Project ║ ║ + WorkflowDefId║ + ║ Permission ║ ║ Department ║ ║ Approval ║ + ║ (3-panel UI) ║ ║ + seed demo ║ ║ Comment ║ + ║ MenuKey ║ ║ ContractClause ║ ║ Attachment ✅ ║ + ║ + nested tree ║ ║ ║ ║ (E2E upload) ║ + ╚════════════════╝ ╚════════════════╝ ╚════════════════╝ ╔════════════════╗ ╔════════════════╗ ╔════════════════╗ ║ FORM ENGINE ║ ║ WORKFLOW ║ ║ BÁO CÁO ║ - ║ (Phase 2) ║ ║ (Phase 3) ║ ║ (Phase 4) ║ + ║ (Phase 2) ✅ ║ ║ (Phase 3) ✅ ║ ║ (Phase 4) ✅ ║ ╠════════════════╣ ╠════════════════╣ ╠════════════════╣ - ║ Template ║ ║ StateMachine ║ ║ Dashboard ║ - ║ Renderer ║ ║ Transition ║ ║ ExcelExport ║ - ║ (DOCX/XLSX) ║ ║ SlaTimer ║ ║ FilterQuery ║ - ║ Field mapping ║ ║ Notification ║ ║ ║ - ║ PO gen (F.07) ║ ║ CodeGenerator ║ ║ ║ - ║ ║ ║ (RG-001) ║ ║ ║ + ║ Template CRUD ║ ║ StateMachine ║ ║ Dashboard ║ + ║ DynamicForm ✅ ║ ║ Transition ║ ║ ExcelExport ║ + ║ (DOCX/XLSX) ║ ║ SlaTimer ║ ║ MyDashboard ✅ ║ + ║ FieldSpec JSON ║ ║ SlaExpiryJob ✅ ║ ║ (role-aware) ║ + ║ PDF export ✅ ║ ║ CodeGen RG-001 ║ ║ ║ + ║ (LibreOffice) ║ ║ **Versioned ✅**║ ║ ║ + ║ .doc auto-conv ║ ║ (admin design)║ ║ ║ ╚════════════════╝ ╚════════════════╝ ╚════════════════╝ + + ╔════════════════╗ ╔════════════════╗ ╔════════════════╗ + ║ NOTIFICATION ║ ║ ATTACHMENT ║ ║ BRANDING ║ + ║ (Tier 3) ✅ ║ ║ (Tier 3) ✅ ║ ║ (Tier 3) ✅ ║ + ╠════════════════╣ ╠════════════════╣ ╠════════════════╣ + ║ Notification ║ ║ IFileStorage ║ ║ #1F7DC1 palette║ + ║ SignalR Hub ║ ║ LocalFileStore ║ ║ Be Vietnam Pro ║ + ║ Auto-push ║ ║ Path traversal ║ ║ Solutions logo ║ + ║ interceptor ║ ║ guard ║ ║ ERP shell ║ + ║ Toast + Bell ║ ║ 3 endpoint ║ ║ (TopBar + Bell║ + ║ badge ║ ║ REST + FE ║ ║ + UserMenu) ║ + ║ Email (TODO) ║ ║ drag-drop ║ ║ ║ + ╚════════════════╝ ╚════════════════╝ ╚════════════════╝ + + ╔════════════════════════════════════════════════════════════╗ + ║ INFRA / DEVOPS (Phase 5) ✅ ║ + ╠════════════════════════════════════════════════════════════╣ + ║ IIS 3 sites (Api/Admin/User) + URL Rewrite + ARR ║ + ║ win-acme 3 Let's Encrypt cert + auto-renew ║ + ║ Gitea Actions CI/CD (Windows self-hosted runner) ║ + ║ SQL Server 2019 SQLEXPRESS + scripts/backup-sql.ps1 ║ + ║ LibreOffice 25.8.6 headless (PDF/docx converter) ║ + ║ Health check /health/live + /health/ready ║ + ║ Serilog file rolling daily retention 30d ║ + ║ Security headers + HSTS + rate limit 300/min global ║ + ╚════════════════════════════════════════════════════════════╝ ``` -## Domain entities chính (dự kiến) +## Domain entities chính (implemented) ``` -User ────< Role ────< Permission (Role × MenuKey × CRUD) -User ────< AuditLog +User ────< UserRoles >──── Role ────< Permission (Role × MenuKey × CRUD) -Supplier (NCC) +MenuItem ─┬─ Parent-child tree (Contracts → Ct_MuaBan_* → ...) + ├─ 28 Ct_* leaves (7 type × 3 action + 7 group) + └─ 7 Wf_* leaves for workflow admin + +Supplier (NCC / NTP / TD / DVDV / CDT) Project (Dự án) -Department (Phòng ban) +Department (9 phòng từ QT docx) Contract - ├── Type: HĐTP | HĐGK | NCC | HĐDV | HĐ Mua bán | ... - ├── Phase (9 state — xem workflow-contract.md) - ├── Supplier, Project, Drafter - ├── MaHopDong (gen theo RG-001) - ├── Approvals[] (audit ai ký phase nào) - ├── Comments[] (thread góp ý Phase 3 của workflow) - ├── Attachments[] (scan bản gốc, file export) - └── TemplateData (JSON — field đã điền khi render form) + ├── Type: HĐTP | HĐGK | NCC | HĐDV | HĐ Mua bán | Nguyên tắc NCC | Nguyên tắc DV + ├── Phase (9 state) + ├── WorkflowDefinitionId (pinned policy at create-time) + ├── Supplier, Project, Drafter, Template + ├── MaHopDong (gen theo RG-001 khi DangDongDau) + ├── SlaDeadline + SlaWarningSent flag + ├── Approvals[] (history) + ├── Comments[] (thread) + └── Attachments[] (scan + upload) -ContractTemplate (ánh xạ Type → File mẫu FO-002.xx) -ContractClause (điều khoản chung FO-002.04 — rich text) -PurchaseOrder (có thể đính với Contract hoặc standalone) +WorkflowDefinition (versioned per ContractType) + ├── Code (QT-MB, QT-TP, ...) + Version (v01, v02, ...) + ├── IsActive (max 1 per ContractType) + └── Steps[] + ├── Order + Phase + Name + SlaDays + └── Approvers[] (Kind=Role|User + AssignmentValue) + +WorkflowTypeAssignment (legacy admin override — fall back khi chưa có WorkflowDefinition) + +ContractTemplate (FormCode + ContractType + FieldSpec JSON + StoragePath) +ContractClause (điều khoản chung FO-002.04) +ContractCodeSequence (Prefix → LastSeq, atomic gen) + +Notification (RecipientUserId + Type + Title + Body + Link + IsRead) ``` -## API namespace dự kiến +## API namespace (implemented) ``` -/api/auth — login, refresh, logout, register (admin gate) -/api/users — CRUD user, assign role, reset password -/api/roles — CRUD role, permission matrix -/api/menus — menu tree + permission resolution +/api/auth — login, refresh, me, logout +/api/users — CRUD + assign roles + reset password + unlock + toggle active +/api/roles — list (CRUD Create/Rename/Delete: TODO) +/api/menus — /me tree với inherit Contracts/Workflows -/api/suppliers — CRUD NCC -/api/projects — CRUD dự án -/api/departments — CRUD phòng ban +/api/suppliers — CRUD NCC +/api/projects — CRUD dự án +/api/departments — CRUD phòng ban +/api/permissions — get matrix + bulk upsert -/api/contracts — CRUD + query by phase/project/supplier -/api/contracts/{id}/transitions — state machine action +/api/contracts — CRUD + query by phase/supplier/project/type + pendingMe +/api/contracts/inbox — HĐ chờ role tôi +/api/contracts/{id} — detail + pinned WorkflowDefinition policy +/api/contracts/{id}/transitions — state machine action (role guard + versioned policy) /api/contracts/{id}/comments — thread góp ý -/api/contracts/{id}/attachments — upload/download +/api/contracts/{id}/attachments — upload (multipart) + download stream + delete -/api/forms — template catalog -/api/forms/{id}/render — render template → docx/xlsx (Phase 2) +/api/forms — CRUD templates (upload/update/delete + render + PDF export) +/api/forms/templates/{id}/export-pdf — LibreOffice conversion -/api/reports/dashboard — KPI tổng hợp -/api/reports/export — Excel download +/api/workflows — admin GET overview + POST create new version +/api/workflows/{type} — per-type definition + history + +/api/notifications — list + unread count + mark-read + mark-all-read + +/api/reports/dashboard — KPI tổng hợp +/api/reports/my-dashboard — role-aware user-specific stats +/api/reports/export — Excel download + +/hubs/notifications — SignalR hub (JWT qua ?access_token=) + +/health/live + /health/ready — health check ``` -## FE screens dự kiến +## FE screens (implemented) ### fe-admin (:8082) — cho Admin + Role quản lý - `/login` -- `/dashboard` — KPI system -- `/master/users` + `/master/roles` + `/master/permissions` -- `/master/suppliers` + `/master/projects` + `/master/departments` -- `/master/contract-templates` + `/master/contract-clauses` -- `/contracts` — danh sách toàn bộ, filter phase/dự án -- `/contracts/{id}` — detail + approval panel + audit log -- `/reports` + `/system/audit-log` +- `/dashboard` — MyDashboard row (4 card) + KPI + charts +- `/master/suppliers|projects|departments` — CRUD +- `/system/users` — Users Mgmt (create/reset/unlock/assign-roles/toggle-active) +- `/system/permissions` — **3-panel layout** (Role list | Menu×CRUD matrix | Granted stats) +- `/system/workflows` — landing 7-card grid per ContractType +- `/system/workflows/:typeCode` — Definition card (active + history) + Designer modal +- `/forms` — list + upload + update + delete + render dialog (Form↔JSON toggle) + Tải PDF +- `/contracts` — list full + filter phase/supplier/project/type +- `/contracts/new` — Create (pre-select from `?type=X`) +- `/contracts/:id` — detail + timeline + action dialog + **Attachments section** + WorkflowSummaryCard +- `/reports` — filter + export Excel -### fe-user (:8080) — cho Drafter, TBP, PD/PM, BOD, CCM, ... +Sidebar: nested menu + `filterForAdmin` hide `Ct_*`, keep `Wf_*` for admin + +### fe-user (:8080) — cho Drafter, TBP, PD/PM, BOD, CCM, HRA, ... - `/login` -- `/inbox` — HĐ đang chờ tôi xử lý (filter theo role × phase) -- `/contracts/new` — chọn template + điền form + submit -- `/contracts/{id}` — detail, comment, approve/reject -- `/my-contracts` — HĐ tôi đã tạo/tham gia +- `/inbox` — HĐ chờ role tôi xử lý (lọc theo `?type=X`) +- `/my-contracts` — HĐ tôi đã tạo/tham gia (lọc theo `?type=X`) +- `/contracts/new` — tạo HĐ draft (pre-select type) +- `/contracts/:id` — detail + duyệt/comment + Attachments drag-drop -## Flow chính (Phase 3 — trình ký HĐ end-to-end) +Sidebar: nested 3-level, `filterForUser` hide admin items, hiển thị 7 ContractType × 3 action + +## Flow chính (Phase 3 end-to-end, Tier 3 versioned) ``` -Drafter (QS/NV.PB) - ├─ [POST /api/contracts] tạo draft + chọn template - ├─ [POST /api/forms/{id}/render] fill + preview +Drafter (QS/NV.PB) ← pin WorkflowDefinitionId = v02 active + ├─ [POST /api/contracts] tạo draft Phase=DangSoanThao, pin v02 + ├─ [POST /api/forms/templates/{id}/render] fill FieldSpec + preview + ├─ (upload scan đính kèm qua /attachments) ├─ [POST /api/contracts/{id}/transitions] DangSoanThao → DangGopY + │ BE: load wfDef v02 → FromDefinition → Registry policy → guard (role + from/to) + │ → Notification push tới all PD/PM/PRO/CCM/FIN/ACT + SignalR toast │ -PD/PM/PRO/CCM/FIN/ACT (song song) - └─ [POST /api/contracts/{id}/comments] góp ý - +PD/PM/PRO/CCM/FIN/ACT (song song) + └─ [POST /api/contracts/{id}/comments] góp ý → Notification push Drafter + Drafter ├─ [PATCH /api/contracts/{id}] revise - ├─ [POST /api/contracts/{id}/transitions] DangGopY → DangDamPhan → DangInKy + ├─ [POST .../transitions] DangGopY → DangDamPhan → DangInKy + │ (BypassProcurementAndCCM=true → skip CCM → DangInKy → DangTrinhKy) │ NTP/NCC/TĐ (external — Drafter update thay) └─ upload scan có chữ ký đối tác -Drafter - └─ [POST /api/contracts/{id}/transitions] → DangKiemTraCCM +Drafter → [transitions] → DangKiemTraCCM +CCM → [transitions] → DangTrinhKy +BOD → [transitions] → DangDongDau + └─ BE: ContractCodeGenerator SERIALIZABLE → gen MaHopDong RG-001 -CCM - └─ [POST /api/contracts/{id}/transitions] → DangTrinhKy - -BOD/NĐUQ - └─ [POST /api/contracts/{id}/transitions] → DangDongDau (GEN mã HĐ ở đây!) - -HRA - └─ [POST /api/contracts/{id}/transitions] → DaPhatHanh (upload scan có dấu) +HRA → [transitions] → DaPhatHanh (final) ``` -## External dependencies +## External dependencies (hiện trạng) -- **SQL Server** — chính thức, dev có thể LocalDB hoặc Docker (`docker-compose.yml`) -- **IIS** — deploy target (Windows Server 2019+) -- **Gitea** — git remote (URL chờ user cấp) -- **Aspose.Words / DocumentFormat.OpenXml** — render .docx (Phase 2, quyết định khi đó) -- **EPPlus hoặc ClosedXML** — render .xlsx (Phase 2) +- **SQL Server** — prod SQLEXPRESS trên VPS, dev LocalDB hoặc Docker +- **IIS** — Windows Server (VPS shared VIETREPORT), URL Rewrite + ARR + WebSockets module +- **Gitea** — https://git.baocaogiaoduc.vn (self-hosted, shared runner) +- **win-acme** — Let's Encrypt auto-renew +- **LibreOffice 25.8.6** — PDF / docx / xlsx conversion (soffice headless) +- **DocumentFormat.OpenXml** — render .docx (Phase 2) +- **ClosedXML** — render .xlsx + Excel export (Phase 4) +- **MediatR 12.4.1** — CQRS mediator (pin, 14 breaking) +- **@microsoft/signalr 8.0.7** — FE realtime client +- **Be Vietnam Pro** — Google Fonts (Vietnamese diacritics) ## Non-goals (KHÔNG làm) - ❌ Python AI service (user đã quyết gác vô thời hạn) -- ❌ Mobile app +- ❌ Mobile app (React Native) — Phase 6+ - ❌ Multi-tenant (1 instance / 1 công ty) -- ❌ Tích hợp e-signature (Phase 6+ nếu có) -- ❌ Tích hợp SAP/Bravo ERP (Phase 6+ nếu có) +- ❌ Tích hợp e-signature (VNPT/FPT CA) — Phase 6+ +- ❌ Tích hợp SAP/Bravo ERP — Phase 6+ - ❌ Public API / external webhooks + +## Architectural wins (Tier 3) + +1. **Versioned workflow via Contract.WorkflowDefinitionId pin** — zero-cost immutability guarantee. + HĐ cũ protected from policy changes by REFERENCE (FK restrict), không phải snapshot copy. + +2. **Permission inheritance via menu ancestry** — Contracts + Workflows roots inherit CanRead xuống + descendant Ct_* / Wf_* nodes. Không cần per-leaf permission rows → Permissions table nhỏ gọn. + +3. **3-project clean-arch split cho cross-cutting services** (SignalR realtime + LibreOffice converter): + - Abstraction ở Application (`IRealtimeNotifier`, `IDocumentConverter`) + - Implementation ở Api / Infrastructure + - Caller (Application handlers) KHÔNG depend transport / framework + +4. **SaveChangesInterceptor auto-push notifications** — `NotificationPushInterceptor` capture Added + Notifications ở SavingChanges, push via `IRealtimeNotifier` ở SavedChanges. Zero caller changes + khi CQRS handler chỉ `db.Notifications.Add(n)`. + +5. **URL-driven admin UI (workflows per-type)** — thay tabs bằng sidebar menu items + URL param. + Linkable, bookmarkable, mỗi type có permission leaf riêng qua `Wf_`. diff --git a/docs/STATUS.md b/docs/STATUS.md index 71d7da1..9d043bd 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -2,11 +2,12 @@ > **Update rule:** trước khi bắt đầu 1 task → ghi row vào `🔥 In Progress`. Xong → chuyển sang `✅ Recently Done`. -**Last updated:** 2026-04-21 15:30 (post-prod-deploy) +**Last updated:** 2026-04-22 03:00 (post-Tier-3-feature-complete + versioned workflow) -## 📍 Phase hiện tại: **Đã go-live prod** — 3 domain HTTPS live, CI/CD xanh, Notifications module + ERP shell +## 📍 Phase hiện tại: **Tier 3 feature-complete** — Prod live, tất cả module lớn xong. Còn: UAT thật + Email outbox (chờ SMTP) + rotate creds. ### 🌐 Production URLs + - https://api.huypham.vn — API (Let's Encrypt, auto-renew via win-acme) - https://admin.huypham.vn — Admin FE (HTTP→HTTPS auto-redirect) - https://user.huypham.vn — User FE (HTTP→HTTPS auto-redirect) @@ -15,125 +16,106 @@ ## 🔥 In Progress -_(không có — chờ UAT + quyết Tier 3 tiếp theo)_ +_(không có — Tier 3 đóng gói xong, chờ UAT để quyết Tier 4)_ ## ✅ Recently Done (newest on top) | Ngày | Ai | Task | Commit | |---|---|---|---| -| 2026-04-22 | Claude | **Versioned workflow per ContractType** — Domain: WorkflowDefinition (Code+Version+IsActive) + WorkflowStep + WorkflowStepApprover (Role/User). Contract.WorkflowDefinitionId pin tại create. EF migration AddVersionedWorkflows + AddWorkflowTypeAssignments. Seed v01 per 7 ContractType từ hardcoded policies. ContractWorkflowService.FromDefinition build policy runtime từ DB. Admin `/system/workflows` tabs per type, DefinitionCard + Designer modal (add/remove step, pick phase/SLA, +Role hoặc +User approvers). POST /api/workflows tạo v02 → v01 auto-archive (HĐ cũ vẫn chạy v01). E2E verified: seed 7 v01, create QT-MB-v02, new HĐ Mua bán pin v02 `policyName:"QT-MB-v02"` activePhases [2,3,7,8,9,99] | `e7e5f2d` + `355bbe3` | -| 2026-04-21 | Claude | **Nested sidebar menu fe-user** — 7 ContractType × 3 actions (Danh sách/Thao tác/Duyệt), nested 3-level. Admin hide Ct_*. Layout recursive MenuNodeRenderer. MyContracts + Inbox filter `?type=X` | `5e0f380` | -| 2026-04-21 | Claude | **Seed master data + MyDashboard widgets** — DbInitializer seed 9 departments từ QT docx (PM/QS/CCM/PRO/FIN/ACT/EQU/HRA/BOD) + 5 demo suppliers + 3 demo projects idempotent. MyDashboard endpoint `/api/reports/my-dashboard` role-aware: DraftsInProgress / PendingMyApproval / DueSoon / Overdue. FE DashboardPage "Của tôi" row 4 card hover-interactive, Admin auto-hide nếu = 0. E2E verified 9 dept seeded, endpoint trả data thật | `6197c84` | -| 2026-04-21 | Claude | **Dynamic workflow policy per ContractType** — Domain `WorkflowPolicy` + registry (Standard 8-phase cho Thầu phụ/Giao khoán/NCC; SkipCcm 7-phase cho Dịch vụ/Mua bán/Nguyên tắc). `ContractWorkflowService` dùng `policy.ForContract(c)` thay hardcoded dict. FE xóa `NEXT_PHASES` hardcoded, dùng `contract.workflow.nextPhases` từ BE. `WorkflowSummaryCard` timeline visual. E2E verified: HĐ Thầu phụ có phase 6 CCM, HĐ Mua bán skip. Gotcha #21 resolved | `cae4d84` | -| 2026-04-21 | Claude | **PDF export + .doc/.xls auto-convert + DynamicForm** — LibreOffice 25.8.6 install VPS, `IDocumentConverter` shell soffice `--convert-to pdf/docx/xlsx` với timeout+temp isolation, admin upload .doc auto-convert .docx. `DynamicForm` component render từ FieldSpec JSON (text/textarea/number/date/currency/select). FE Form↔JSON toggle. E2E verified PDF 488KB/126 pages | `e459097` + `6bbd894` | -| 2026-04-21 | Claude | **Form template builder CRUD** — Admin tự upload `.docx/.xlsx` templates qua UI (không cần dev). BE: UploadContractTemplate (multipart, 10MB, FormCode regex unique, FieldSpec JSON validation) + UpdateContractTemplate (metadata + FieldSpec + IsActive) + DeleteContractTemplate (soft via IsActive=false). File lưu vào `wwwroot/templates/{formCode}_{guid}.{ext}`. FE: FormsPage với upload dialog (file picker + FormCode + Loại HĐ + FieldSpec JSON textarea) + row actions 3 nút (render/edit/delete). E2E verified upload 200 + update 204 + delete 204 | `166d26c` | -| 2026-04-21 | Claude | **Fix Gitea 500 sau Install Web-WebSockets** — Feature install khóa section `` ở applicationHost.config → tất cả IIS site fail. Fix: `appcmd unlock config -section:system.webServer/webSocket`. Gotcha #25 | `c52186b` | -| 2026-04-21 | Claude | **SignalR realtime notifications E2E** — Clean-arch split (IRealtimeNotifier Application + SignalRNotifier Api + NotificationPushInterceptor Infrastructure). Hub `/hubs/notifications` JWT via `?access_token=` query (WebSocket headers limit). Interceptor SavedChangesAsync auto-push → zero caller changes. FE singleton connection với auto-reconnect + toast trên push + query invalidation. IIS WebSocket module enabled. Hub verified: negotiate 200 với JWT (WebSockets/SSE/LongPolling transports), 401 không auth | `ea9ab5e` | -| 2026-04-21 | Claude | **Attachment upload E2E** — IFileStorage abstraction + LocalFileStorage (path-traversal guard) + CQRS Upload/Download/Delete + 3 controller endpoints (multipart, File stream, DELETE) + FE ContractAttachmentsSection (drag-drop + purpose selector + icon-per-MIME + auth-blob download + confirm delete) + wired vào cả 2 ContractDetailPage. Unblock E2E workflow (scan HĐ ký/đóng dấu) | `c8d0070` + `dc3f09b` | -| 2026-04-21 | Claude | **Content polish** — typography (14px + leading 1.55 + tracking-tight, heading weights) + PageHeader (text-[22px] + border-b) + Button (shadow + active-press + ring-2) + Input/Select/Textarea (inset shadow + border/ring focus) + DataTable (rounded-xl + UPPERCASE tracking header + brand hover) | `346bd5d` | -| 2026-04-21 | Claude | **Brand identity từ Solutions logo** — pixel-sampled #1F7DC1 → full palette brand-50..900 + accent red + Be Vietnam Pro font (Vietnamese-first) + favicon chữ 'S' crop từ logo.png + apple-touch-icon + login page gradient brand + ERP subtitle | `4abb559` + `bf1fbe3` | -| 2026-04-21 | Claude | **Fix login Network Error** — SPA web.config thêm HTTP→HTTPS redirect rule (CORS chỉ allow https origin, user gõ bare domain bị block) | `397eb36` | -| 2026-04-21 | Claude | **Notifications module E2E** — Domain entity + EF migration + Infra service + CQRS (List/UnreadCount/MarkRead/MarkAllRead) + API controller + FE bells wire real endpoint + ContractWorkflowService emit notification cho Drafter khi phase transition. Foundation sẵn cho SignalR/email outbox | `49c0ddc` | -| 2026-04-21 | Claude | **PermissionsPage improved** — search, stats badge, bulk column toggle, empty state icon | `6c0e206` | -| 2026-04-21 | Claude | **ERP shell**: TopBar + NotificationBell + UserMenu (avatar + role badges). Layout tách `[sidebar] [topbar + content]` — foundation cho multi-module ERP | `2b6f91c` | -| 2026-04-21 | Claude | **Tier 1 UI polish** — SlaTimer (inline + full variant, 5 chỗ), Inbox stat cards, DataTable skeleton rows, EmptyState component + MyContracts CTA | `290936a`..`2e43799` | -| 2026-04-21 | Claude | **CI/CD deploy xanh E2E** — self-hosted Windows runner, single job build+deploy local, npm install fresh node_modules (Vite 8 rolldown binding), appsettings rendered từ secrets, /health/live 200 sau deploy | `b40da1e` | -| 2026-04-21 | Claude | **VPS prod setup** — SQL DB (SQLEXPRESS), IIS sites (SolutionErp-Api/Admin/User), win-acme 3 Let's Encrypt certs + auto-renew, shared gitea-runner với VIETREPORT | `169e268`..`519ba85` | -| 2026-04-21 | Claude | **IDOR + SLA Job + Admin warning** — ContractsController List/GetDetail filter theo role (non-admin chỉ thấy HĐ mình là Drafter hoặc role eligible phase). SlaExpiryJob BackgroundService auto-approve quá hạn mỗi 15min với Decision=AutoApprove. DbInitializer warn log khi admin vẫn dùng password default | `fba0754` | +| 2026-04-22 | Claude | **PermissionsPage 3-panel layout** — Grid `lg:grid-cols-[280px_1fr_300px]`: Panel 1 Role list click-to-select (active ring-brand), Panel 2 Menu×CRUD matrix sticky thead + search + column bulk-toggle + brand-tinted hover, Panel 3 Granted progress bar + CRUD breakdown color badges (slate/emerald/amber/red) + Tip | `91b2da1` | +| 2026-04-22 | Claude | **Admin Workflows tabs → sidebar menu items** — Seed 7 `Wf_` leaf dưới group `Workflows`. Layout resolvePath `Wf_` → `/system/workflows/`. WorkflowsPage bỏ tab bar, URL param drives type selection. Landing 7-card grid khi click top-level `Quy trình HĐ`. Inheritance: `Workflows.Read` perm → tất cả 7 leaves auto-visible. | `f216169` | +| 2026-04-22 | Claude | **Versioned workflow per ContractType** — 3 entity mới: WorkflowDefinition (Code+Version+IsActive+ContractType), WorkflowStep (Order+Phase+Name+SlaDays), WorkflowStepApprover (Role/User + AssignmentValue). Contract.WorkflowDefinitionId nullable FK pin tại create. Migration `AddVersionedWorkflows`. Seed v01 per 7 ContractType. `WorkflowPolicyRegistry.FromDefinition()` build runtime policy từ DB. ContractWorkflowService load pinned definition. Admin `/system/workflows/:typeCode` Designer modal (create new version, clone, add/remove step, +Role/+User approvers). POST /api/workflows auto-increment Version + deactivate old. Invariant: HĐ cũ pin v01 giữ nguyên khi v02 active. E2E verified: QT-MB-v02 active, HĐ cũ vẫn chạy v01. | `e7e5f2d` + `355bbe3` | +| 2026-04-21 | Claude | **Nested sidebar menu fe-user** — 7 ContractType × 3 actions (Danh sách/Thao tác/Duyệt), nested 3-level. Admin hide `Ct_*`. Layout recursive MenuNodeRenderer. MyContracts + Inbox filter `?type=X` | `5e0f380` + `48e91fe` | +| 2026-04-21 | Claude | **Seed master data + MyDashboard widgets** — DbInitializer seed 9 departments (PM/QS/CCM/PRO/FIN/ACT/EQU/HRA/BOD) + 5 demo suppliers + 3 demo projects idempotent. MyDashboard endpoint role-aware: DraftsInProgress / PendingMyApproval / DueSoon / Overdue / DraftsTotalValue. FE "Của tôi" row 4 card hover-interactive, admin auto-hide nếu = 0 | `6197c84` | +| 2026-04-21 | Claude | **Dynamic workflow policy per ContractType** — Domain WorkflowPolicy record + registry (Standard 8-phase cho Thầu phụ/Giao khoán/NCC; SkipCcm 7-phase cho Dịch vụ/Mua bán/Nguyên tắc). ContractWorkflowService dùng policy.ForContract(c). FE xóa NEXT_PHASES hardcoded, dùng contract.workflow.nextPhases BE trả. WorkflowSummaryCard timeline visual. Gotcha #21 resolved | `cae4d84` | +| 2026-04-21 | Claude | **PDF export + .doc/.xls auto-convert + DynamicForm** — LibreOffice 25.8.6 VPS, IDocumentConverter shell soffice `--convert-to pdf/docx/xlsx` timeout+temp isolation. Admin upload .doc auto-convert .docx. DynamicForm parse FieldSpec JSON render inputs (text/textarea/number/date/currency/select). Form↔JSON toggle. E2E verified PDF 488KB/126 pages | `e459097` + `6bbd894` | +| 2026-04-21 | Claude | **Form template builder CRUD** — Admin tự upload `.docx/.xlsx` qua UI (không cần dev). BE multipart + FormCode regex unique + FieldSpec JSON validation + soft delete via IsActive. FE FormsPage upload dialog + row actions render/edit/delete. E2E verified | `166d26c` | +| 2026-04-21 | Claude | **Fix Gitea 500 sau Install Web-WebSockets** — appcmd unlock section webSocket. Gotcha #25 | `c52186b` | +| 2026-04-21 | Claude | **SignalR realtime notifications E2E** — 3-project clean-arch: IRealtimeNotifier (App) + SignalRNotifier (Api) + NotificationPushInterceptor (Infra SaveChanges hook). Hub `/hubs/notifications` JWT `?access_token=` query (WebSocket headers limit). FE singleton lib/realtime.ts auto-reconnect + toast + query invalidation. IIS WebSocket module enabled | `ea9ab5e` | +| 2026-04-21 | Claude | **Attachment upload E2E** — IFileStorage + LocalFileStorage (path-traversal guard) + CQRS Upload/Download/Delete + 3 endpoint (multipart, stream, DELETE) + FE ContractAttachmentsSection drag-drop + purpose selector + icon-per-MIME + auth-blob download + confirm delete. Wired 2 ContractDetailPage | `c8d0070` + `dc3f09b` | +| 2026-04-21 | Claude | **Content polish** — typography 14px + leading 1.55 + tracking-tight + PageHeader border-b + Button shadow+active + Input inset shadow + DataTable rounded-xl UPPERCASE header brand hover | `346bd5d` | +| 2026-04-21 | Claude | **Brand identity từ Solutions logo** — pixel-sampled #1F7DC1 → palette brand-50..900 + accent red + Be Vietnam Pro (Vietnamese-first) + favicon 'S' crop + apple-touch-icon + login gradient brand | `4abb559` + `bf1fbe3` | +| 2026-04-21 | Claude | **Fix login Network Error** — SPA web.config HTTP→HTTPS redirect rule (CORS chỉ https) | `397eb36` | +| 2026-04-21 | Claude | **Notifications module E2E** — Domain entity + EF migration + Infra service + CQRS + API controller + FE bells wire real endpoint + ContractWorkflowService emit notification cho Drafter khi phase transition | `49c0ddc` | +| 2026-04-21 | Claude | **PermissionsPage iter 1** — search, stats badge, bulk column toggle, empty state | `6c0e206` | +| 2026-04-21 | Claude | **ERP shell** — TopBar + NotificationBell + UserMenu (avatar + role badges). Layout `[sidebar] [topbar + content]` | `2b6f91c` | +| 2026-04-21 | Claude | **Tier 1 UI polish** — SlaTimer (inline + full variant, 5 chỗ), Inbox stat cards, DataTable skeleton rows, EmptyState | `290936a`..`2e43799` | +| 2026-04-21 | Claude | **CI/CD deploy xanh E2E** — self-hosted Windows runner, single job build+deploy, fresh node_modules (Vite 8 rolldown binding), appsettings từ secrets, /health/live 200 sau deploy | `b40da1e` | +| 2026-04-21 | Claude | **VPS prod setup** — SQL DB (SQLEXPRESS), IIS sites (SolutionErp-Api/Admin/User), win-acme 3 Let's Encrypt + auto-renew, shared gitea-runner với VIETREPORT | `169e268`..`519ba85` | +| 2026-04-21 | Claude | **IDOR + SLA Job + Admin warning** — ContractsController filter theo role. SlaExpiryJob BackgroundService 15min auto-approve Decision=AutoApprove. DbInitializer warn khi admin vẫn default | `fba0754` | | 2026-04-21 | Claude | **Phase 5.1 Security + Users Mgmt** — Security headers + Identity lockout + LoginHandler check + Users CQRS + UsersController + FE `/system/users` | `11e61c9` | | 2026-04-21 | Claude | **Phase 5 Prep** — BE rate limit + health check + Serilog file + HSTS + scripts deploy-iis/backup-sql + .gitea/workflows/deploy.yml + 4 guides + FE refresh token queue pattern | `46a2cab` | -| 2026-04-21 | Claude | **Phase 4 Report MVP + Docs Consolidation** — Dashboard KPI + Excel export + rules.md + architecture.md + schema-diagram.md + gotchas update 26 pitfalls | `fe7ad8e` | +| 2026-04-21 | Claude | **Phase 4 Report MVP** — Dashboard KPI + Excel export + rules.md + architecture.md + schema-diagram.md + gotchas 26 pitfalls | `fe7ad8e` | | 2026-04-21 | Claude | **Phase 3 Workflow MVP** — 9 phase state machine + gen mã HĐ RG-001 | `7e957a7` | | 2026-04-21 | Claude | **Phase 2 Form Engine MVP** | `5113e4c` | | 2026-04-21 | Claude | **Phase 1.2** — CRUD Master + Permission Matrix | `54d6c9b` | -| 2026-04-21 | Claude | **Docs addition** | `49a5f57` | -| 2026-04-21 | Claude | **Phase 1 foundation** | `702411f` | +| 2026-04-21 | Claude | **Phase 1 foundation** + Docs addition | `702411f` + `49a5f57` | | 2026-04-21 | Claude | **Phase 0** | `25dad7f` | -Session logs: [P0](changelog/sessions/2026-04-21-1045-phase0-scaffold.md) · [P1f](changelog/sessions/2026-04-21-1100-phase1-foundation.md) · [P1.2](changelog/sessions/2026-04-21-1130-phase1-cruds-permission.md) · [P2](changelog/sessions/2026-04-21-1200-phase2-form-engine.md) · [P3](changelog/sessions/2026-04-21-1330-phase3-workflow.md) · [P4](changelog/sessions/2026-04-21-1430-phase4-report.md) · [P5prep](changelog/sessions/2026-04-21-1530-phase5-prep.md) +Session logs: [P0](changelog/sessions/2026-04-21-1045-phase0-scaffold.md) · [P1f](changelog/sessions/2026-04-21-1100-phase1-foundation.md) · [P1.2](changelog/sessions/2026-04-21-1130-phase1-cruds-permission.md) · [P2](changelog/sessions/2026-04-21-1200-phase2-form-engine.md) · [P3](changelog/sessions/2026-04-21-1330-phase3-workflow.md) · [P4](changelog/sessions/2026-04-21-1430-phase4-report.md) · [P5prep](changelog/sessions/2026-04-21-1530-phase5-prep.md) · [**Tier 3**](changelog/sessions/2026-04-22-0300-tier3-feature-complete.md) **Docs entry points:** + - [`rules.md`](rules.md) · [`architecture.md`](architecture.md) · [`HANDOFF.md`](HANDOFF.md) - [`workflow-contract.md`](workflow-contract.md) · [`forms-spec.md`](forms-spec.md) - [`database/database-guide.md`](database/database-guide.md) · [`database/schema-diagram.md`](database/schema-diagram.md) - [`flows/`](flows/) (7 file) · [`guides/`](guides/) (4 file) · [`gotchas.md`](gotchas.md) -- [`changelog/migration-todos.md`](changelog/migration-todos.md) · [`changelog/sessions/`](changelog/sessions/) (7 file) +- [`changelog/migration-todos.md`](changelog/migration-todos.md) · [`changelog/sessions/`](changelog/sessions/) (8 file) ## 🎯 Next up -### Phase 5 (prod go-live) +### Hard blockers (chờ user / ops) -- [x] Gitea remote + push all commits -- [x] Gitea Actions runner (self-hosted Windows, shared VIETREPORT runner) -- [x] Secrets Gitea (JWT_SECRET, DB_CONNECTION — IIS_* deprecated sau rewrite workflow) -- [x] CI/CD workflow xanh end-to-end -- [x] Windows Server setup IIS (SolutionErp-Api/Admin/User) -- [x] HTTPS cert (win-acme 3 Let's Encrypt + auto-renew) -- [x] SQL Server prod (SQLEXPRESS) + vrapp db_owner -- [x] Smoke test E2E: /health/ready Healthy, login JWT thật, FE live -- [ ] **UAT 1 tuần 2-3 user thật** ← next -- [ ] SQL backup Task Scheduler (script đã có, chưa schedule) -- [ ] Rotate credentials (SA, vrapp, JWT, runner token) — 1 số đã post chat +- [ ] **UAT 1 tuần 2-3 user thật** — hard requirement từ roadmap Phase 5 +- [ ] **Email outbox** — MailKit + SMTP (BLOCKED chờ user cấp SMTP host/user/pass) +- [ ] **Rotate credentials** — SA, vrapp, JWT secret, runner token (đã post chat) +- [ ] **SQL backup daily** — Task Scheduler (script `scripts/backup-sql.ps1` đã có, chưa schedule) -### Tier 3 ERP roadmap còn (lớn, để dành session sau) +### Optional polish (khi rảnh / UAT phát sinh) -- [x] Attachment upload BE endpoint + FE drag-drop ✓ -- [x] SignalR real-time push (auto-push interceptor + client auto-reconnect) ✓ -- [x] Form template builder CRUD (admin upload .docx/.xlsx + FieldSpec JSON editor) ✓ -- [x] Form builder iteration 2: DynamicForm render UI từ FieldSpec ✓ +- [ ] Roles CRUD — admin tạo custom role ngoài 12 hardcoded (schema sẵn, chỉ cần CQRS + FE) +- [ ] User-level approver targeting runtime — data model đã có (`WorkflowStepApprover.Kind=User`), chỉ cần wire User-kind vào `ContractWorkflowService.TransitionAsync` guard +- [ ] PermissionsPage: grant `Workflows.Read` cho non-admin role → menu Wf_* visible +- [ ] Warning notification khi còn 20% SLA (`SlaWarningSent` flag đã có, chỉ thiếu job emit) +- [ ] E2E test reject → quay về DangSoanThao (multi-role) +- [ ] Dependencies scan CI (`dotnet list package --vulnerable`, `npm audit`) + +### Tier 3 ERP roadmap ✓ (close) + +- [x] Attachment upload BE + FE ✓ +- [x] SignalR real-time push ✓ +- [x] Form template builder CRUD + DynamicForm ✓ - [x] PDF export qua LibreOffice headless ✓ -- [x] .doc → .docx auto-conversion khi upload template ✓ -- [x] Dynamic workflow policy per ContractType (Standard/SkipCcm) ✓ -- [ ] Email outbox cho Notification (MailKit, SMTP config — cần user config) - -### Phase 5.1 Security — hầu như xong - -- [x] Security headers middleware (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, CSP) -- [x] Identity account lockout (5 fail → 15min, config-driven) -- [x] Password policy config-driven -- [x] LoginHandler check lockout + AccessFailedAsync + reset on success -- [x] BE Users management + FE admin UsersPage -- [x] IDOR check ContractsController (non-admin chỉ thấy HĐ mình/role eligible) -- [x] Admin password warning log startup -- [x] SLA Expiry BackgroundService auto-approve -- [ ] Dependencies scan CI (`dotnet list package --vulnerable` + `npm audit`) -- [ ] Roles CRUD — optional - -### Polish iterations - -**Phase 2 iter 2:** convert .doc, field spec JSON + form builder, {{#loop}}, PDF convert -**Phase 3 iter 2:** SLA job auto-approve, email/in-app notify, attachment upload, RowVersion -**Phase 4 iter 2:** SLA overdue report, PDF HĐ export, dashboard user-specific - -### Quick wins - -- FE Users management + Roles CRUD (test permission non-admin) -- Filter Inbox theo phase FE -- Test refresh token flow manual (logout/login flow) +- [x] .doc/.xls → .docx/.xlsx auto-conversion ✓ +- [x] Dynamic workflow policy per ContractType ✓ +- [x] **Versioned workflow (WorkflowDefinition pinned per Contract)** ✓ +- [x] **Admin workflow designer UI (per-type, per-step approvers)** ✓ +- [x] **Nested sidebar menu per ContractType (fe-user) + menu split admin/user** ✓ +- [x] **PermissionsPage 3-panel layout** ✓ +- [ ] Email outbox for Notification (blocked — SMTP config) ## 📊 Thông số cumulative -| | P0 | P1f | P1.2 | P2 | P3 | P4 | **P5 prep** | -|---|---:|---:|---:|---:|---:|---:|---:| -| BE LOC | 0 | ~400 | ~1500 | ~1900 | ~2700 | ~3100 | **~3300** | -| DB tables | 0 | 7 | 12 | 14 | 19 | 19 | 19 | -| API endpoints | 0 | 4 | 20 | 23 | 31 | 33 | **35** (+health) | -| Migrations | 0 | 1 | 3 | 4 | 5 | 5 | 5 | -| FE pages | 0 | 2 | 6 | 7 | 14 | 16 | 16 | -| Scripts PS | 0 | 0 | 0 | 1 (convert-doc) | 1 | 1 | **3** (+deploy-iis, backup-sql) | -| CI/CD workflow | 0 | 0 | 0 | 0 | 0 | 0 | **1** | -| Docs | 10 | 13 | 14 | 24 | 26 | 30 | **35** (+4 guides + session log) | -| Commits | 1 | 2 | 3 | 5 | 6 | 7 | **8** (sắp) | +| | P0 | P1f | P1.2 | P2 | P3 | P4 | P5prep | **Tier3** | +|---|---:|---:|---:|---:|---:|---:|---:|---:| +| BE LOC | 0 | ~400 | ~1500 | ~1900 | ~2700 | ~3100 | ~3300 | **~4800** | +| DB tables | 0 | 7 | 12 | 14 | 19 | 19 | 19 | **24** (+Notifications, +WorkflowTypeAssignments, +WorkflowDefinitions, +WorkflowSteps, +WorkflowStepApprovers) | +| API endpoints | 0 | 4 | 20 | 23 | 31 | 33 | 35 | **~50** (+notifications, +attachments, +forms CRUD, +pdf export, +workflows admin, +my-dashboard) | +| Migrations | 0 | 1 | 3 | 4 | 5 | 5 | 5 | **8** (+AddNotifications, +AddWorkflowTypeAssignments, +AddVersionedWorkflows) | +| FE pages | 0 | 2 | 6 | 7 | 14 | 16 | 16 | **~20** (admin Users/Workflows per-type + user nested menu) | +| Scripts PS | 0 | 0 | 0 | 1 | 1 | 1 | 3 | **4** (+install-libreoffice) | +| CI/CD workflow | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | +| Docs | 10 | 13 | 14 | 24 | 26 | 30 | 35 | **~40** (+session log + updated MDs) | +| Commits | 1 | 2 | 3 | 5 | 6 | 7 | 8 | **~25** | ## 🚨 Blockers / risks -- ⏳ **Gitea remote URL** — ĐANG CẦN để push + setup CI/CD -- ⚠️ **Phase 5.1 security hardening** chưa làm (headers, account lockout, IDOR check) -- ⚠️ **3 file .doc chưa convert** (Phase 2 carryover) -- ⚠️ **SLA không tự auto-approve** (Phase 3.2) -- ⚠️ **Email/in-app notification** chưa có -- ⚠️ **FE Users management chưa có** — khó test permission non-admin -- ⚠️ **Rate limit global 300/min/IP** — OK cho dev, cần tăng cho prod nếu nhiều user +- ⚠️ **Email SMTP chưa có** — blocker cho notification outbound +- ⚠️ **UAT real user chưa chạy** — risk phát sinh bug edge-case quan trọng +- ⚠️ **Credentials leaked trong chat** — cần rotate trước go-live thật +- ⚠️ **SQL backup không auto** — risk data loss nếu VPS crash +- ⚠️ **Permission `Workflows.Read` cho non-admin** — cần grant để họ thấy menu Wf_* (hiện chỉ admin thấy) +- ⚠️ **User-kind approver chưa enable runtime** — designer cho chọn User nhưng guard fall back DeptManager ## Credentials + URLs @@ -141,6 +123,9 @@ Session logs: [P0](changelog/sessions/2026-04-21-1045-phase0-scaffold.md) · [P1 admin@solutionerp.local / Admin@123456 ``` -- API: http://localhost:5443 — Swagger `/swagger` (dev only) — Health `/health/live` + `/health/ready` -- Admin FE: http://localhost:8082 — `/dashboard`, `/contracts`, `/reports`, `/master/*`, `/forms`, `/system/permissions` -- User FE: http://localhost:8080 — `/inbox`, `/contracts/new`, `/my-contracts` +- API prod: https://api.huypham.vn — Health `/health/live` + `/health/ready` +- API dev: http://localhost:5443 — Swagger `/swagger` +- Admin FE prod: https://admin.huypham.vn · dev `http://localhost:8082` +- User FE prod: https://user.huypham.vn · dev `http://localhost:8080` +- SQL prod: `.\SQLEXPRESS` / `SolutionErp` / `vrapp` +- SQL dev: `(localdb)\MSSQLLocalDB` / `SolutionErp_Dev` diff --git a/docs/changelog/migration-todos.md b/docs/changelog/migration-todos.md index fb744f5..3ef7ba5 100644 --- a/docs/changelog/migration-todos.md +++ b/docs/changelog/migration-todos.md @@ -58,7 +58,7 @@ - [x] FE: `main.tsx` với QueryClient (TanStack Query) - [x] E2E verified: login qua Vite proxy cả 2 app → JWT + user info -### Phase 1 đợt 2 — CRUD master + Permission Matrix (sắp tới) +### Phase 1 đợt 2 — CRUD master + Permission Matrix - [x] `Domain/Master/Supplier` (+ SupplierType enum 5 loại) / `Project` / `Department` (AuditableEntity) - [x] EF `IEntityTypeConfiguration` cho mỗi entity (unique Code + query filter IsDeleted) @@ -67,27 +67,25 @@ - [x] Migration 2: `AddMasterData` - [x] `Domain/Identity/MenuItem` (Key PK, Label, ParentKey, Order, Icon) + `MenuKeys` const class - [x] `Domain/Identity/Permission` (RoleId, MenuKey, CanRead/Create/Update/Delete) -- [x] Seed default menu tree (12 menu) + admin full access trong DbInitializer -- [x] `Application/Permissions/Queries/GetMyMenuTreeQuery` — resolve per-user, union OR, tree filter +- [x] Seed default menu tree + admin full access trong DbInitializer (mở rộng Tier 3: 28 Ct_* + 7 Wf_*) +- [x] `Application/Permissions/Queries/GetMyMenuTreeQuery` — resolve per-user + inherit Contracts/Workflows root - [x] `Api/Controllers/{MenusController, RolesController, PermissionsController}` - [x] Migration 3: `AddPermissions` -- [x] Authorization handler `MenuPermissionHandler` + register 48 policy `{menu}.{action}` -- [ ] `Domain/Entities/Contract` skeleton (Id, Type, SupplierId, ProjectId, Phase=DangChon, DraftData JSON) — deferred Phase 2/3 -- [ ] Contract CRUD draft only (không workflow Phase 3) — deferred +- [x] Authorization handler `MenuPermissionHandler` + register policy `{menu}.{action}` - [x] FE: `` + `usePermission()` hook -- [x] FE Admin: 3 trang CRUD Supplier/Project/Department với DataTable + Dialog modal + search/sort/paging -- [x] FE Admin: Permission Matrix grid page (role × menu × CRUD checkbox) -- [x] FE Admin: Layout menu động từ `/api/menus/me` -- [ ] FE User: trang "HĐ của tôi" list + filter — Phase 3 -- [ ] FE Admin: Users management page (tạo user + gán role) — sắp tới -- [ ] FE Admin: Roles CRUD — sắp tới -- [ ] Route guard theo role admin-only — có PermissionGuard ở button, route cần thêm +- [x] FE Admin: 3 trang CRUD Supplier/Project/Department với DataTable + Dialog + search/sort/paging +- [x] FE Admin: Permission Matrix grid page (role × menu × CRUD checkbox) — iter 1 + 3-panel iter 2 +- [x] FE Admin: Layout menu động từ `/api/menus/me` + recursive nested + filterForAdmin +- [x] FE User: trang "HĐ của tôi" list + filter `?type=X` — Tier 3 +- [x] FE Admin: Users management page (tạo user + gán role + reset password + unlock) +- [ ] FE Admin: Roles CRUD — optional (12 role seed đủ dùng) +- [x] Route guard theo role admin-only — PermissionGuard ở button level ### Exit criteria Phase 1 -- [ ] Admin login → tạo NCC/Project → tạo role "Nhân viên CCM" → gán permission menu "Contracts.Read" -- [ ] User CCM login → thấy menu Contracts, không thấy menu Admin -- [ ] Tạo Contract draft → list hiển thị, không bị 403 sai +- [x] Admin login → tạo NCC/Project → gán permission menu +- [x] User non-admin login → thấy menu theo role, không bị 403 +- [x] Tạo Contract draft → list hiển thị, filter role-aware ## Phase 2 — Form Engine (T5-6) @@ -107,18 +105,17 @@ - [x] FE admin: `FormsPage` — list + render dialog điền JSON + download - [x] E2E verified: render FO-002.05 → file .docx 482KB mở được bằng Word -### Iteration 2 (optional — enhance) +### Iteration 2 (Tier 3 — đã làm) -- [ ] Convert 3 file `.doc` → `.docx` (retry Word COM với `DisplayAlerts=0` + timeout, hoặc LibreOffice headless) -- [ ] Parse chi tiết field của 5 template HĐ — mỗi form thành JSON `FieldSpec` -- [ ] Support `{{#loop}}...{{/loop}}` block cho table lặp (hạng mục HĐ giao khoán, PO) -- [ ] FE user: form builder dynamic — render từ fieldSpec thay vì điền JSON tay -- [ ] FE admin: upload template mới qua UI (POST multipart) + edit field mapping -- [ ] Lưu `ContractClause` (FO-002.04) dạng rich text, admin edit qua TipTap/TinyMCE -- [ ] PDF convert via LibreOffice headless (`soffice --headless --convert-to pdf`) -- [ ] Import/export template (backup/restore) -- [ ] Format helpers: number → `150,000,000 VND`, date → `dd/MM/yyyy` -- [ ] Content preservation test: render → diff layout với template gốc +- [x] Convert `.doc` → `.docx` / `.xls` → `.xlsx` qua `IDocumentConverter` + LibreOffice headless (thay Word COM, auto-convert khi admin upload) +- [x] FE user: form builder dynamic — `DynamicForm` component render từ `FieldSpec` JSON (text/textarea/number/date/currency/select) +- [x] FE admin: upload template mới qua UI (POST multipart) + edit FieldSpec + delete (soft via IsActive) +- [x] PDF convert via LibreOffice headless (`soffice --headless --convert-to pdf`) — `LibreOfficeDocumentConverter` (timeout + per-request temp + isolated UserInstallation) +- [x] Format helpers: number → `VND`, date → `dd/MM/yyyy` (render layer) +- [ ] Support `{{#loop}}...{{/loop}}` block cho table lặp (hạng mục HĐ giao khoán, PO) — optional +- [ ] Lưu `ContractClause` (FO-002.04) dạng rich text + TipTap/TinyMCE editor — optional +- [ ] Import/export template (backup/restore) — optional +- [ ] Content preservation test (render → diff layout) — optional ## Phase 3 — Workflow State Machine (T7-9) @@ -141,21 +138,39 @@ - [x] PhaseBadge component + color map - [x] E2E verified: tạo HĐ → chạy 9 phase → gen mã `FLOCK 01/HĐGK/SOL&PVL2026/01` -### Iteration 2 (polish) +### Iteration 2 (polish — Tier 3 + Notification) - [x] `Infrastructure/HostedServices/SlaExpiryJob` — check mỗi 15min, auto-approve quá hạn với Decision=AutoApprove (+30s delay startup) - [x] E2E test với non-admin user (Drafter role) — IDOR filter verified - [x] Admin password warning log khi vẫn dùng default -- [ ] Warning notification khi còn 20% SLA (track `SlaWarningSent` flag đã có) -- [ ] `Infrastructure/Services/NotificationService` — email (MailKit) + in-app -- [ ] SignalR hub cho real-time notification badge +- [x] `Infrastructure/Services/NotificationService` — in-app + emit (email đợi SMTP) +- [x] SignalR hub cho real-time notification badge — `/hubs/notifications` + interceptor auto-push +- [x] Upload attachment endpoint (multipart) + FE drag-drop UI (`wwwroot/uploads/contracts/{id}/`) — IFileStorage + path-traversal guard +- [x] Filter Inbox theo type ở FE (`?type=X`) +- [x] Render HĐ template docx/xlsx → PDF export (LibreOffice) +- [ ] Warning notification khi còn 20% SLA — `SlaWarningSent` flag đã có - [ ] MediatR `AuditBehavior` — log mọi command (ngoài ContractApprovals) -- [ ] Upload attachment endpoint (multipart) + FE upload UI (`wwwroot/uploads/contracts/{id}/`) - [ ] RowVersion optimistic concurrency (2 user race → 409) - [ ] Render HĐ docx lúc tạo (merge TemplateId + DraftData + ContractClause appendix) -- [ ] Filter Inbox theo phase ở FE -- [ ] E2E test: reject → quay về DangSoanThao -- [ ] E2E test: SLA expired → auto-approve + log (test thật qua set SlaDeadline past) +- [ ] E2E test: reject → quay về DangSoanThao với multi-role +- [ ] Email notification (MailKit + SMTP) — blocked chờ user config + +### Iteration 3 (Versioned workflow — Tier 3) + +- [x] `Domain/Contracts/WorkflowDefinition` (Code + Version + IsActive + ContractType + Description) +- [x] `Domain/Contracts/WorkflowStep` (Order + Phase + Name + SlaDays) +- [x] `Domain/Contracts/WorkflowStepApprover` (Kind: Role|User + AssignmentValue) +- [x] `Contract.WorkflowDefinitionId` nullable FK pin tại create time +- [x] Migration `AddVersionedWorkflows` + seed v01 cho 7 ContractType +- [x] `WorkflowPolicyRegistry.FromDefinition()` — runtime policy build từ DB +- [x] `ContractWorkflowService` — load pinned def → FromDefinition → guard +- [x] `WorkflowAdminFeatures` — GetOverview + CreateNewVersion (auto-increment Version + deactivate old) +- [x] FE admin `/system/workflows/:typeCode` — DefinitionCard + history + Designer modal +- [x] Designer: Steps repeatable, per-step phase/name/SLA, +Role / +User approver select +- [x] Clone-from-version button cho starting point +- [x] Invariants: UNIQUE (Code, Version), 1 IsActive per ContractType, no cascade FK +- [x] E2E: create QT-MB-v02 → v01 archived → HĐ mới pin v02 → HĐ cũ pin v01 giữ nguyên +- [ ] Runtime enable User-kind approver trong TransitionAsync guard (data model ready) ## Phase 4 — Reporting + Polish (T10-11) @@ -168,13 +183,15 @@ - [x] FE `ReportsPage` filter + export - [x] Docs consolidation: `rules.md` + `architecture.md` + `database/schema-diagram.md` + gotchas update -### Iteration 2 (polish — optional) +### Iteration 2 (Tier 3 + optional) +- [x] Dashboard user-specific (`MyDashboard` endpoint — DraftsInProgress / PendingMyApproval / DueSoon / Overdue / DraftsTotalValue) + FE "Của tôi" row 4 card +- [x] UX polish: skeleton loader DataTable, empty state có action, error boundary recovery +- [x] Content polish: typography 14px + leading 1.55 + tracking-tight + PageHeader + Button + Input + DataTable +- [x] Brand identity: #1F7DC1 palette + Be Vietnam Pro font + Solutions logo - [ ] SLA overdue report (by role / phase, export Excel) - [ ] Contract audit log export (từng HĐ ra PDF) -- [ ] Dashboard user-specific (HĐ của tôi / role của tôi) - [ ] Chart library recharts (nếu cần chart phức tạp) -- [ ] UX polish: skeleton loader cho mọi list, empty state có action, error boundary recovery - [ ] Accessibility: keyboard nav, focus trap modal, aria labels - [ ] Dark mode - [ ] Performance: explicit index DB cho query hot đã identify @@ -200,17 +217,18 @@ - [x] `docs/guides/runbook.md` — operations (restart, rollback, restore) - [x] FE refresh token auto interceptor (queue pattern cả 2 app) -### Deploy thật (cần Gitea URL) +### Deploy thật -- [ ] Windows Server setup: IIS + URL Rewrite + ARR (reverse proxy FE → IIS) -- [ ] SQL Server prod + Task Scheduler trigger backup-sql.ps1 -- [ ] HTTPS certificate (Let's Encrypt qua win-acme) -- [ ] Gitea remote setup + push all commits -- [ ] Set 5 Gitea Actions secrets (IIS_HOST/USER/PASSWORD/JWT_SECRET/DB_CONNECTION) -- [ ] Enable Gitea runner (Windows + Ubuntu) -- [ ] Test CI/CD workflow lần đầu staging -- [ ] UAT production 1 tuần với 2-3 user thật -- [ ] Go-live checklist: backup, rollback plan, on-call contact +- [x] Windows Server setup: IIS + URL Rewrite + ARR (reverse proxy FE → IIS) +- [x] SQL Server prod (SQLEXPRESS) + vrapp db_owner +- [x] HTTPS certificate (Let's Encrypt qua win-acme — 3 cert + auto-renew) +- [x] Gitea remote setup + push all commits +- [x] Set Gitea Actions secrets (JWT_SECRET, DB_CONNECTION — deploy local via runner) +- [x] Enable Gitea runner (Windows self-hosted, shared với VIETREPORT) +- [x] Test CI/CD workflow — xanh E2E, /health/live 200 sau deploy +- [ ] **UAT production 1 tuần với 2-3 user thật** ← hard blocker còn lại +- [ ] SQL Task Scheduler trigger backup-sql.ps1 (script có sẵn, chưa schedule) +- [ ] Go-live checklist: rotate creds + backup plan + on-call contact ### Phase 5.1 Security hardening + Users Mgmt @@ -225,8 +243,26 @@ - [ ] Dependencies scan vào CI (`dotnet list package --vulnerable --include-transitive`, `npm audit --audit-level=high`) - [ ] BE Roles CRUD (Create/Rename/Delete custom role) + FE `/system/roles` — optional, 12 role seed đủ dùng +## Tier 3 ERP (Session 2026-04-22) — feature-complete + +- [x] **Attachment upload E2E** — IFileStorage + CQRS + FE drag-drop (gotcha path-traversal) — `c8d0070` +- [x] **SignalR realtime notifications** — 3-project clean-arch split + JWT `?access_token=` + auto-reconnect — `ea9ab5e` +- [x] **Form template builder CRUD** — Upload/Update/Delete + FieldSpec JSON editor — `166d26c` +- [x] **PDF export + DynamicForm + .doc auto-convert** — LibreOffice headless per-request temp — `6bbd894` + `e459097` +- [x] **Dynamic workflow policy** — Standard/SkipCcm registry theo ContractType — `cae4d84` +- [x] **Versioned workflow** — WorkflowDefinition + Steps + Approvers pinned per Contract — `e7e5f2d` +- [x] **Admin workflow designer** — per-type page + Designer modal + clone — `e7e5f2d` +- [x] **Nested sidebar menu fe-user** — 7 type × 3 action + admin/user split — `5e0f380` +- [x] **Workflows tabs → sidebar menu** — 7 Wf_ leaves + URL-driven — `f216169` +- [x] **PermissionsPage 3-panel layout** — Role list | Menu×CRUD | Granted stats — `91b2da1` +- [x] **Seed master data** — 9 dept + 5 supplier + 3 project + MyDashboard — `6197c84` +- [x] **Brand identity** — #1F7DC1 palette + Be Vietnam Pro + Solutions logo — `4abb559`..`bf1fbe3` + ## Post-launch (Phase 6+ — future) +- [ ] **Email outbox** (MailKit + SMTP) — blocked chờ SMTP config +- [ ] **Roles CRUD** — admin tạo custom role ngoài 12 hardcoded +- [ ] **User-kind approver runtime** — data model có, guard cần wire - [ ] E-signature integration (VNPT CA hoặc FPT CA) - [ ] Tích hợp Bravo / SAP ERP import NCC - [ ] Mobile app (React Native?) cho BOD duyệt ngoài giờ diff --git a/docs/changelog/sessions/2026-04-22-0300-tier3-feature-complete.md b/docs/changelog/sessions/2026-04-22-0300-tier3-feature-complete.md new file mode 100644 index 0000000..82bdb89 --- /dev/null +++ b/docs/changelog/sessions/2026-04-22-0300-tier3-feature-complete.md @@ -0,0 +1,206 @@ +# Session 2026-04-22 ~03:00 — Tier 3 feature-complete + versioned workflow + +**Focus:** Hoàn thành toàn bộ Tier 3 ERP features, pivot workflow từ hardcoded +policy → versioned DB-backed designer, chia nested menu cho fe-user + admin +workflow management riêng. + +Session kéo dài 2 phiên (21/04 chiều — 22/04 sáng), tổng ~20+ commit. + +## Outcomes + +### A. Attachment upload E2E ✓ +- `IFileStorage` abstraction + `LocalFileStorage` (Application/Infra split, + path-traversal guard, CREATEDIRECTORY-if-missing). +- CQRS: Upload / Download / Delete, validation 20MB + MIME whitelist (pdf/doc + (x)/xls(x)/png/jpg/webp), sanitize filename. +- Endpoints: POST multipart / GET download stream / DELETE. +- FE `ContractAttachmentsSection` (both apps) — drag-drop, purpose selector, + icon-per-MIME, auth-blob download, confirm delete. +- Integrated vào ContractDetailPage cả 2 app. + +### B. SignalR realtime notifications ✓ +- Clean-arch 3-project split: `IRealtimeNotifier` (Application) + + `SignalRNotifier` (Api) + `NotificationPushInterceptor` (Infrastructure + SaveChanges hook). Zero caller changes — `db.Notifications.Add()` auto-push. +- Hub `/hubs/notifications` JWT via `?access_token=` query string (WebSocket + headers limit). +- FE `lib/realtime.ts` singleton connection + auto-reconnect backoff + stop + on logout. NotificationBell subscribe `notification-created` → toast + + invalidate query. +- IIS WebSocket module installed trên VPS. + +### C. Form template builder CRUD + DynamicForm ✓ +- BE: Upload / Update / Delete templates (multipart, FormCode regex + unique, + FieldSpec JSON validation). `.doc`/`.xls` auto-convert sang `.docx`/`.xlsx` + qua `IDocumentConverter` khi upload. +- FE admin FormsPage: upload dialog với file picker + FormCode + Loại HĐ + + FieldSpec JSON textarea. Row actions 3 nút (render / edit / delete). +- `DynamicForm` component: parse FieldSpec JSON (text/textarea/number/date/ + currency/select), render form inputs. Render dialog có tab toggle Form ↔ JSON. + +### D. PDF export (LibreOffice headless) ✓ +- `IDocumentConverter` generalized (docx→pdf, doc→docx, xls→xlsx, etc). +- `LibreOfficeDocumentConverter` shells `soffice.exe --headless --convert-to`, + per-request temp workDir + isolated UserInstallation (concurrent-safe), + 60s timeout, kill process tree. +- Endpoint: POST `/api/forms/templates/{id}/export-pdf` pipe render → PDF. +- FE Tải PDF button cạnh Tải file gốc trong render dialog. +- LibreOffice 25.8.6 installed trên VPS via `scripts/install-libreoffice.ps1`. +- E2E verified: PDF 488KB / 126 pages. + +### E. Dynamic + versioned workflow per ContractType ✓ +**Phase 1 — Dynamic policy selection:** +- `WorkflowPolicy` record (Domain) + registry với 2 policy: Standard (8 phase + full CCM) + SkipCcm (7 phase bỏ CCM). Map ContractType → policy theo QT docx. +- `ContractWorkflowService.ForContract()` dùng registry. +- FE xóa hardcoded `NEXT_PHASES`, dùng `contract.workflow.nextPhases` từ + `ContractDetailDto.Workflow`. `WorkflowSummaryCard` timeline visual. +- Admin `/system/workflows` page (Phase 1) với dropdown Standard/SkipCcm per + ContractType (DB override `WorkflowTypeAssignment`). + +**Phase 2 — Versioned workflow (user request "Khi add quy trình mới → HĐ cũ +giữ quy trình cũ"):** +- 3 entities mới: `WorkflowDefinition` (Code+Version+IsActive+ContractType), + `WorkflowStep` (Order+Phase+Name+SlaDays), `WorkflowStepApprover` + (Kind: Role|User + AssignmentValue). +- `Contract.WorkflowDefinitionId` nullable FK — pinned at create time. +- Migration `AddVersionedWorkflows`. Seed v01 per 7 ContractType từ hardcoded + policies (Role approvers). +- `WorkflowPolicyRegistry.FromDefinition()` — build runtime policy từ + WorkflowDefinition's Steps. Role-based transitions derive từ Role-kind + approvers, User-kind fallback DeptManager (iteration 2 sẽ enable user-level). +- `ContractWorkflowService` + `ContractFeatures.Get()`: load pinned + WorkflowDefinition → FromDefinition → runtime policy. +- CreateContract pin `WorkflowDefinitionId = active version for type`. +- Admin UI `/system/workflows/:typeCode` (URL-driven, sidebar menu replaces + tabs): + - Landing: 3-col grid card per 7 type với active version badge + - Per-type page: DefinitionCard (active + history), "Archived · N HĐ còn + chạy" count, Designer modal cho create-new-version (code/name/desc, + repeatable steps, per-step approvers + Role hoặc + User select). + - Clone-from-version button cho starting point sensible. +- POST `/api/workflows` create-new-version: auto-increment Version, deactivate + old IsActive, atomic. +- Invariants: + - Unique (Code, Version) + - Chỉ 1 IsActive per ContractType tại 1 thời điểm + - HĐ cũ giữ version cũ (WorkflowDefinitionId pinned, not FK cascade) + +- E2E verified: tạo QT-MB-v02 → v01 archived, HĐ mới type=5 pin v02 + `policyName: "QT-MB-v02"`, 5 bước custom [2,3,7,8,9,99]. + +### F. Nested sidebar menu per ContractType (fe-user) ✓ +- BE seed 7 type groups × 3 action leaves (28 entries) dưới `Contracts`: + - `Ct_` group + `Ct__List/Create/Pending` leaves +- `GetMyMenuTreeQuery` generalized inherit-permission: descendants of + `Contracts` hoặc `Workflows` inherit parent CanRead (no per-leaf perm rows). +- fe-user Layout: recursive `MenuNodeRenderer` (top-level expanded, nested + collapsed). Ct_*_List → `/my-contracts?type=X`, Ct_*_Create → + `/contracts/new?type=X`, Ct_*_Pending → `/inbox?type=X`. +- MyContractsPage + InboxPage read `?type=X`, filter client-side. +- **Menu split**: admin hide `Ct_*`, user hide `Master/System/Forms/Reports`. + +### G. Admin Workflows tabs → sidebar menu items ✓ +- Seed 7 `Wf_` leaves dưới `Workflows` group. +- Layout resolvePath `Wf_` → `/system/workflows/`. +- WorkflowsPage bỏ tab bar; URL param drives type selection. Landing 7-card + grid khi click top-level `Quy trình HĐ` without type. +- Inheritance: `Workflows.Read` perm → tất cả 7 leaves auto-visible. + +### H. PermissionsPage 3-panel layout ✓ +- Grid `lg:grid-cols-[280px_1fr_300px]`: + - Panel 1 (trái): Role list click-to-select với active ring-brand + - Panel 2 (giữa): Menu × CRUD matrix + sticky thead + search + column + bulk-toggle + row brand-tinted hover + - Panel 3 (phải): Granted progress bar + CRUD breakdown color-coded badges + (slate/emerald/amber/red) + Tip + +### I. Seed master data + MyDashboard ✓ +- DbInitializer: 9 departments từ QT docx (PM/QS/CCM/PRO/FIN/ACT/EQU/HRA/BOD), + 5 demo suppliers (5 SupplierType), 3 demo projects. Idempotent. +- Endpoint `/api/reports/my-dashboard`: DraftsInProgress / PendingMyApproval / + DueSoon / Overdue / DraftsTotalValue. +- FE DashboardPage "Của tôi" row 4 card, hover-interactive, admin auto-hide + nếu tất cả = 0. + +### J. Brand identity + content polish (earlier in session) ✓ +- Solutions logo cropped (pixel-sampled #1F7DC1) + full palette brand-50..900 + + Be Vietnam Pro font. +- SlaTimer, InboxPage stat cards, DataTable skeleton, EmptyState. +- TopBar + NotificationBell + UserMenu (ERP shell). + +### K. Gitea 500 fix (side-effect) ✓ +- `Install-WindowsFeature Web-WebSockets` khóa section `` ở + applicationHost → all IIS sites with `` sập. +- Fix: `appcmd unlock config -section:system.webServer/webSocket`. +- Documented as gotcha #25. + +## Commits (chronological, partial) + +``` +Earlier (21/04): + c8d0070 — Attachment upload E2E + ea9ab5e — SignalR realtime E2E + 166d26c — Form template builder CRUD + 6bbd894 — PDF export (LibreOffice) + e459097 — DynamicForm + .doc auto-convert + cae4d84 — Dynamic workflow policy per ContractType + 6197c84 — Seed master data + MyDashboard + 48e91fe — Nested sidebar menu (admin) + 5e0f380 — Menu split (admin hide, user show) + workflow config static + 4abb559..bf1fbe3 — Brand identity (Solutions logo + palette + fonts) + 346bd5d — Content polish (typography, PageHeader, Button, Input, DataTable) + 290936a..2e43799 — Tier 1 UI (SlaTimer, Inbox stats, Skeleton, EmptyState) + 2b6f91c — ERP shell (TopBar, NotificationBell, UserMenu) + 6c0e206 — PermissionsPage iter 1 (search + stats + bulk toggle) + +Today (22/04): + e7e5f2d — Versioned workflow entities + migration + designer + 355bbe3 — Fix Dialog size TS (xl → lg) + f216169 — Workflows tabs → sidebar menu items + 91b2da1 — PermissionsPage 3-panel layout +``` + +## Key architectural decisions + +1. **WorkflowPolicy runtime build from WorkflowDefinition DB rows** (not stored + as JSON blob) — allows admin to edit steps/approvers granularly without + JSON parser UX. +2. **WorkflowDefinitionId pinned at contract create** — zero-cost immutability + guarantee. Old contracts protected from workflow changes by reference, not + by snapshot copy. +3. **Permission inheritance via menu ancestry** (Contracts / Workflows roots) + — keeps Permissions table small while supporting deep navigation menus. +4. **3-project clean-arch split for cross-cutting services** (realtime + notifications, document conversion) — each service has abstraction in + Application + implementation in Infra/Api. +5. **Role + User approvers per step** (data model) but only Role-kind drives + runtime guard v1 — user-level targeting deferred to iter 2. + +## Runtime workflow resolution (critical path) + +``` +Contract.TransitionAsync: + if contract.WorkflowDefinitionId not null: + def = db.WorkflowDefinitions.Include(Steps.Approvers).First(wfId) + policy = WorkflowPolicyRegistry.FromDefinition(def) + elif admin has override in WorkflowTypeAssignments for contract.Type: + policy = Registry.ByName(override.PolicyName) + else: + policy = Registry.For(contract.Type) // hardcoded Standard/SkipCcm + + if not policy.Transitions.HasKey((from, to)): throw Forbidden + if not actor.Roles.Any(r => allowed.Contains(r)): throw Forbidden +``` + +## Next session priority + +1. **UAT với 2-3 user thật** (hard requirement từ roadmap Phase 5). +2. Roles CRUD — trường hợp admin muốn tạo custom role ngoài 12 hardcoded. +3. Email outbox (MailKit + SMTP) — BLOCKED on user providing SMTP config. +4. User-level approver targeting trong workflow runtime (data model có sẵn, + chỉ cần wire User-kind approvers vào TransitionAsync guard). +5. PermissionsPage: allow admin grant `Workflows.Read` cho non-admin role so + menu Wf_* visible. +6. Rotate credentials đã leak trong chat (SA, vrapp, JWT). +7. SQL backup daily Task Scheduler (script đã có). diff --git a/docs/database/schema-diagram.md b/docs/database/schema-diagram.md index 2623a21..873a21a 100644 --- a/docs/database/schema-diagram.md +++ b/docs/database/schema-diagram.md @@ -1,6 +1,6 @@ # Schema Diagram — Luồng DB SOLUTION_ERP -> ERD đầy đủ + mối quan hệ 19 table sau Phase 3. Mermaid render ở VS Code / GitHub / Gitea. +> ERD đầy đủ + mối quan hệ **24 table** sau Tier 3 (Notifications + Versioned workflows). Mermaid render ở VS Code / GitHub / Gitea. ## 1. Full ERD @@ -22,6 +22,7 @@ erDiagram Departments ||--o{ Contracts : "drafted-in" Users ||--o{ Contracts : "drafter" ContractTemplates ||--o{ Contracts : "uses" + WorkflowDefinitions ||--o{ Contracts : "pinned-policy" Contracts ||--o{ ContractApprovals : "history" Contracts ||--o{ ContractComments : "thread" @@ -29,6 +30,11 @@ erDiagram Users ||--o{ ContractApprovals : "approved-by" Users ||--o{ ContractComments : "author" + WorkflowDefinitions ||--o{ WorkflowSteps : "has" + WorkflowSteps ||--o{ WorkflowStepApprovers : "allowed-by" + + Users ||--o{ Notifications : "recipient" + Users { uniqueidentifier Id PK nvarchar FullName "200" @@ -126,6 +132,7 @@ erDiagram uniqueidentifier DepartmentId FK uniqueidentifier DrafterUserId FK uniqueidentifier TemplateId FK + uniqueidentifier WorkflowDefinitionId FK "pinned policy, nullable" decimal GiaTri "18,2" bit BypassProcurementAndCCM datetime2 SlaDeadline @@ -170,6 +177,54 @@ erDiagram int LastSeq datetime2 UpdatedAt } + + Notifications { + uniqueidentifier Id PK + uniqueidentifier RecipientUserId FK + int Type "ContractTransition/CommentAdded/SlaExpired/..." + nvarchar Title "200" + nvarchar Body "2000" + nvarchar Link "500" + bit IsRead + datetime2 ReadAt + datetime2 CreatedAt + } + + WorkflowTypeAssignments { + uniqueidentifier Id PK + int ContractType "UK" + nvarchar PolicyName "50 Standard/SkipCcm" + datetime2 UpdatedAt + uniqueidentifier UpdatedBy FK + } + + WorkflowDefinitions { + uniqueidentifier Id PK + nvarchar Code "100 QT-MB / QT-TP / ..." + int Version "auto-increment per Code" + bit IsActive "chi 1 active per ContractType" + int ContractType + nvarchar Name "200" + nvarchar Description "500" + datetime2 CreatedAt + uniqueidentifier CreatedBy FK + } + + WorkflowSteps { + uniqueidentifier Id PK + uniqueidentifier WorkflowDefinitionId FK + int Order + int Phase "target ContractPhase int" + nvarchar Name "200" + int SlaDays + } + + WorkflowStepApprovers { + uniqueidentifier Id PK + uniqueidentifier WorkflowStepId FK + int Kind "1=Role, 2=User" + nvarchar AssignmentValue "200 RoleName or UserId Guid string" + } ``` ## 2. Luồng dữ liệu chính (data flow diagram) @@ -182,18 +237,27 @@ flowchart TB P --> MI[MenuItems] end - subgraph MASTER ["📋 Master data (admin CRUD)"] + subgraph MASTER ["📋 Master data (admin CRUD + seed demo)"] S[Suppliers] PR[Projects] DE[Departments] end - subgraph FORMS ["📄 Form templates (seed)"] + subgraph FORMS ["📄 Form templates (admin upload)"] CT[ContractTemplates] CC[ContractClauses] end - subgraph CONTRACT ["📝 Contract workflow (Phase 3 core)"] + subgraph WORKFLOW ["⚙️ Versioned workflow (admin designer)"] + WD[WorkflowDefinitions] + WS[WorkflowSteps] + WSA[WorkflowStepApprovers] + WTA[WorkflowTypeAssignments legacy override] + WD --> WS + WS --> WSA + end + + subgraph CONTRACT ["📝 Contract workflow"] C[Contracts] CA[ContractApprovals] CCM[ContractComments] @@ -201,39 +265,48 @@ flowchart TB CCS[ContractCodeSequences] end + subgraph NOTIFY ["🔔 Notification module"] + N[Notifications] + end + U -.Drafter.-> C S --> C PR --> C DE --> C CT --> C + WD -.pinned at create.-> C C --> CA C --> CCM C --> CAT C -.gen when DangDongDau.-> CCS + C -.transition event.-> N + CCM -.comment added.-> N + U -.recipient.-> N ``` -## 3. Vòng đời 1 HĐ — data changes +## 3. Vòng đời 1 HĐ — data changes (với versioned workflow) ```mermaid flowchart LR - Create[POST /contracts] - Create --> C1["Contracts INSERT
Phase=2, SLA=+7d"] + Create[POST /contracts type=5] + Create --> PickWD["SELECT TOP 1 WorkflowDefinition
WHERE ContractType=5 AND IsActive=1
→ Id=wf-v02"] + PickWD --> C1["Contracts INSERT
Phase=2, SLA=+7d, WorkflowDefinitionId=wf-v02"] Transition1[Transition 2→3] - Transition1 --> C2["UPDATE Phase=3
INSERT ContractApprovals"] + Transition1 --> LoadPolicy["Load wf-v02.Steps.Approvers
WorkflowPolicyRegistry.FromDefinition(def)"] + LoadPolicy --> Guard["Check allowed roles for
(from=2, to=3)"] + Guard --> C2["UPDATE Phase=3
INSERT ContractApprovals
INSERT Notifications bulk"] Comment[POST /comments] - Comment --> C3[INSERT ContractComments] + Comment --> C3["INSERT ContractComments
INSERT Notifications"] - Transition2[Transition 3→4→5→6→7] - Transition2 --> C4["UPDATE Phase + SlaDeadline
INSERT ContractApprovals"] + NewVersion[Admin creates QT-MB-v03] + NewVersion --> NV1["INSERT WorkflowDefinition v03 IsActive=1
UPDATE v02 SET IsActive=0 (atomic)"] + NV1 -.->|HĐ cũ không ảnh hưởng| C1 Transition3[Transition 7→8 BOD ký] - Transition3 --> CG["ContractCodeGenerator
SERIALIZABLE tran
UPSERT ContractCodeSequences"] - CG --> C5["UPDATE Contract
SET MaHopDong='FLOCK 01/HĐGK/SOL&PVL/01',
Phase=8"] - - Transition4[Transition 8→9 HRA phát hành] - Transition4 --> C6["UPDATE Phase=9, SlaDeadline=NULL
INSERT ContractApprovals"] + Transition3 --> CG["ContractCodeGenerator SERIALIZABLE
UPSERT ContractCodeSequences"] + CG --> C5["UPDATE Contract
SET MaHopDong, Phase=8
INSERT Notifications"] ``` ## 4. Index strategy @@ -245,12 +318,19 @@ flowchart LR | Contracts | `IX_Contracts_SupplierId` | Filter HĐ theo NCC | | Contracts | `IX_Contracts_ProjectId` | Filter HĐ theo dự án | | Contracts | `IX_Contracts_SlaDeadline` | SLA expiry job query | +| Contracts | `IX_Contracts_WorkflowDefinitionId` | Pinned policy lookup | | ContractApprovals | `IX_ContractApprovals_ContractId_ApprovedAt` | Timeline query theo HĐ | | ContractComments | `IX_ContractComments_ContractId_CreatedAt` | Thread load | | ContractAttachments | `IX_ContractAttachments_ContractId` | Attachments load | | Suppliers/Projects/Departments | `UX_{Table}_Code` | Unique business code | | Permissions | `UX_Permissions_RoleId_MenuKey` | 1 row / role / menu | | MenuItems | `IX_MenuItems_ParentKey` | Tree query | +| Notifications | `IX_Notifications_RecipientUserId_IsRead_CreatedAt` | Bell badge unread count + list | +| WorkflowDefinitions | `UX_WorkflowDefinitions_Code_Version` | Unique version per code | +| WorkflowDefinitions | `IX_WorkflowDefinitions_ContractType_IsActive` | Active policy lookup | +| WorkflowSteps | `IX_WorkflowSteps_WorkflowDefinitionId_Order` | Load steps ordered | +| WorkflowStepApprovers | `IX_WorkflowStepApprovers_WorkflowStepId` | Approver load | +| WorkflowTypeAssignments | `UX_WorkflowTypeAssignments_ContractType` | 1 override per type (legacy) | Chi tiết + cheatsheet SQL: [`database-guide.md`](database-guide.md). @@ -263,9 +343,13 @@ Chi tiết + cheatsheet SQL: [`database-guide.md`](database-guide.md). | Contract → ContractAttachment | 1 - N | Cascade | File vật lý vẫn còn trong `wwwroot/uploads/`, cleanup riêng | | Supplier → Contract | 1 - N | Restrict | Không xóa Supplier nếu còn HĐ tham chiếu | | Project → Contract | 1 - N | Restrict | Tương tự | +| **WorkflowDefinition → Contract** | 1 - N | **Restrict** | **KHÔNG cascade** → HĐ cũ pin version cũ không bị xóa khi admin archive | | Role → Permission | 1 - N | Cascade | Xóa role → clear permissions | | MenuItem → Permission | 1 - N | Cascade | — | | MenuItem → MenuItem (self-ref) | 1 - N | Restrict | Parent-child menu | +| **WorkflowDefinition → WorkflowStep** | 1 - N | Cascade | Delete def → remove steps (chỉ khi no Contract tham chiếu) | +| **WorkflowStep → WorkflowStepApprover** | 1 - N | Cascade | — | +| **User → Notification (RecipientUserId)** | 1 - N | Cascade | — | ## 6. Soft delete behavior @@ -278,47 +362,89 @@ Entity list áp dụng: - Supplier, Project, Department - Contract - ContractTemplate, ContractClause +- WorkflowDefinition (admin archive = `IsActive=false`, xóa logic chỉ khi muốn scrub) KHÔNG soft delete (cascade hoặc keep): - ContractApproval, ContractComment, ContractAttachment — cascade khi Contract xóa - Permission, MenuItem — cascade khi Role xóa - ContractCodeSequence — không bao giờ xóa (giữ history seq) +- Notifications — không soft delete, chỉ `IsRead` flag (giữ history ngắn hạn, cleanup job sau) +- WorkflowStep / WorkflowStepApprover — cascade khi WorkflowDefinition xóa - Identity tables (Users, Roles, ...) — Identity không support soft delete built-in ## 7. Truy vấn tiêu biểu -### Inbox HĐ chờ role của tôi +### Inbox HĐ chờ role của tôi (với versioned workflow) ```sql --- Tương đương GetMyInboxQuery +-- Server filter (API): HĐ chờ role eligible phase theo pinned policy +-- Thực tế ContractsController resolve policy runtime: +-- def = db.WorkflowDefinitions.Include(Steps.Approvers).Where(Id == contract.WorkflowDefinitionId).FirstOrDefault() +-- policy = def != null ? Registry.FromDefinition(def) : Registry.For(contract.Type) +-- phase eligible = policy.Transitions.Where(t => t.From == contract.Phase && t.AllowedRoles.Intersect(myRoles).Any()) +-- SQL tương đương: SELECT c.Id, c.MaHopDong, c.TenHopDong, c.Phase, c.SlaDeadline, s.Name AS SupplierName, p.Name AS ProjectName FROM Contracts c INNER JOIN Suppliers s ON c.SupplierId = s.Id INNER JOIN Projects p ON c.ProjectId = p.Id WHERE c.IsDeleted = 0 - AND c.Phase IN (/* phase eligible cho role hiện tại */) + AND c.Phase IN (/* phase eligible computed theo pinned workflow */) ORDER BY c.SlaDeadline ASC; ``` -### Dashboard stats +### Pick active workflow tại create-time + +```sql +SELECT TOP 1 Id +FROM WorkflowDefinitions +WHERE ContractType = @Type AND IsActive = 1 +ORDER BY [Version] DESC; +``` + +### Tạo version mới (atomic) + +```sql +BEGIN TRAN; + +-- Step 1: deactivate current active +UPDATE WorkflowDefinitions +SET IsActive = 0 +WHERE ContractType = @Type AND IsActive = 1; + +-- Step 2: compute next version +DECLARE @NextVersion INT = (SELECT ISNULL(MAX([Version]), 0) + 1 FROM WorkflowDefinitions WHERE Code = @Code); + +-- Step 3: insert new active version +INSERT WorkflowDefinitions (Id, Code, [Version], IsActive, ContractType, Name, Description, CreatedAt, CreatedBy) +VALUES (NEWID(), @Code, @NextVersion, 1, @Type, @Name, @Description, GETUTCDATE(), @UserId); + +-- Step 4: insert steps + approvers (batch) +-- ... + +COMMIT; +``` + +### Dashboard stats (MyDashboard user-specific) ```sql --- Tổng + active + overdue SELECT - (SELECT COUNT(*) FROM Contracts WHERE IsDeleted = 0) AS Total, - (SELECT COUNT(*) FROM Contracts WHERE IsDeleted = 0 AND Phase NOT IN (9, 99)) AS Active, - (SELECT COUNT(*) FROM Contracts WHERE IsDeleted = 0 AND Phase NOT IN (9, 99) AND SlaDeadline < GETUTCDATE()) AS Overdue; + (SELECT COUNT(*) FROM Contracts + WHERE DrafterUserId = @Me AND IsDeleted = 0 AND Phase NOT IN (9, 99)) AS DraftsInProgress, + (SELECT COUNT(*) FROM Contracts + WHERE Phase IN (/* eligible phases cho role tôi */) AND IsDeleted = 0) AS PendingMyApproval, + (SELECT COUNT(*) FROM Contracts + WHERE IsDeleted = 0 AND SlaDeadline BETWEEN GETUTCDATE() AND DATEADD(DAY, 2, GETUTCDATE())) AS DueSoon, + (SELECT COUNT(*) FROM Contracts + WHERE IsDeleted = 0 AND SlaDeadline < GETUTCDATE() AND Phase NOT IN (9, 99)) AS Overdue, + (SELECT ISNULL(SUM(GiaTri), 0) FROM Contracts + WHERE DrafterUserId = @Me AND Phase = 2) AS DraftsTotalValue; +``` --- By phase -SELECT Phase, COUNT(*) FROM Contracts WHERE IsDeleted = 0 GROUP BY Phase; +### Notifications unread count (bell badge) --- Top 5 NCC -SELECT TOP 5 c.SupplierId, s.Name, COUNT(*) AS Cnt, SUM(c.GiaTri) AS TotalValue -FROM Contracts c -INNER JOIN Suppliers s ON c.SupplierId = s.Id -WHERE c.IsDeleted = 0 -GROUP BY c.SupplierId, s.Name -ORDER BY COUNT(*) DESC; +```sql +SELECT COUNT(*) FROM Notifications +WHERE RecipientUserId = @Me AND IsRead = 0; ``` ### Gen mã HĐ atomic @@ -327,32 +453,63 @@ ORDER BY COUNT(*) DESC; BEGIN TRAN; SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; -MERGE ContractCodeSequences AS tgt -USING (SELECT @Prefix AS Prefix) AS src ON tgt.Prefix = src.Prefix -WHEN MATCHED THEN UPDATE SET LastSeq = LastSeq + 1, UpdatedAt = GETUTCDATE() -WHEN NOT MATCHED THEN INSERT (Prefix, LastSeq, UpdatedAt) VALUES (@Prefix, 1, GETUTCDATE()); +UPDATE ContractCodeSequences +SET LastSeq = LastSeq + 1, UpdatedAt = GETUTCDATE() +WHERE Prefix = @Prefix; + +IF @@ROWCOUNT = 0 + INSERT ContractCodeSequences (Prefix, LastSeq, UpdatedAt) + VALUES (@Prefix, 1, GETUTCDATE()); SELECT LastSeq FROM ContractCodeSequences WHERE Prefix = @Prefix; COMMIT; ``` -(EF Core impl dùng `UPDATE + IF ROWCOUNT = 0 INSERT` thay MERGE — tương đương nhưng an toàn hơn với SERIALIZABLE). - ## 8. Migration lịch sử -| # | Migration | Tables added | +| # | Migration | Tables added / changed | |---|---|---| | 1 | `Init` | 7 Identity tables | | 2 | `AddMasterData` | Suppliers, Projects, Departments | | 3 | `AddPermissions` | MenuItems, Permissions | | 4 | `AddForms` | ContractTemplates, ContractClauses | | 5 | `AddContractsWorkflow` | Contracts, ContractApprovals, ContractComments, ContractAttachments, ContractCodeSequences | +| 6 | `AddNotifications` | Notifications | +| 7 | `AddWorkflowTypeAssignments` | WorkflowTypeAssignments (admin override legacy) | +| 8 | `AddVersionedWorkflows` | WorkflowDefinitions, WorkflowSteps, WorkflowStepApprovers + Contracts.WorkflowDefinitionId FK | -Tổng: **19 bảng** (+ `__EFMigrationsHistory` hệ thống). +Tổng: **24 bảng** (+ `__EFMigrationsHistory` hệ thống). -## 9. Liên quan +## 9. Versioned workflow invariants + +``` +1. UNIQUE (WorkflowDefinitions.Code, Version) + → không 2 row cùng Code + Version (enforce qua IX unique) + +2. Chỉ 1 WorkflowDefinition.IsActive = true per ContractType tại 1 thời điểm + → enforce qua CreateWorkflowDefinitionCommand: UPDATE deactivate trước INSERT, cùng transaction + +3. Contract.WorkflowDefinitionId pinned at create → không update sau đó + → CreateContractCommandHandler pick active version 1 lần, save + +4. ON DELETE Restrict FK Contract.WorkflowDefinitionId → WorkflowDefinitions.Id + → admin không thể DELETE WorkflowDefinition nếu còn Contract pin + → admin archive = set IsActive=false thôi, row vẫn tồn tại + +5. Runtime policy resolution order (ContractWorkflowService): + a. If contract.WorkflowDefinitionId NOT NULL → load def → FromDefinition + b. Else if admin override ở WorkflowTypeAssignments for contract.Type → Registry.ByName + c. Else → Registry.For(contract.Type) (hardcoded Standard/SkipCcm) + +6. WorkflowStepApprover.Kind + - 1=Role: AssignmentValue là RoleName (Domain/Identity/AppRoles constants) + - 2=User: AssignmentValue là UserId Guid string + - Runtime guard hiện tại chỉ dùng Role-kind (User-kind data model ready, enable iter sau) +``` + +## 10. Liên quan - [`database-guide.md`](database-guide.md) — conventions + migration workflow + cheatsheet đầy đủ - [`../architecture.md`](../architecture.md) — layered architecture + data flow -- [`../workflow-contract.md`](../workflow-contract.md) — state machine spec +- [`../workflow-contract.md`](../workflow-contract.md) — state machine spec + versioned - [`../flows/`](../flows/) — sequence diagrams diff --git a/docs/gotchas.md b/docs/gotchas.md index 52edbbf..86639c6 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -197,6 +197,109 @@ Tương tự khi dùng URL Rewrite `` cần unlock `system.webS **Cảnh báo co-existence:** Trên VPS shared với project khác, enable feature mới qua `Install-WindowsFeature` có thể làm sập site project khác. Luôn test all site sau mỗi enable. +## SignalR / Realtime + +### 26. SignalR WebSocket không cho custom Authorization header + +**Triệu chứng:** `new HubConnectionBuilder().withUrl('/hubs/x', { headers: { Authorization: ... } })` — WebSocket transport vẫn 401. + +**Nguyên nhân:** Browser WebSocket API không cho set custom headers cho handshake. Chỉ 2 transport khác (SSE / LongPolling) mới dùng headers. + +**Fix:** +- FE: dùng `accessTokenFactory: () => token` — SignalR client tự append `?access_token=` query cho WebSocket +- BE: Wire JWT bearer `OnMessageReceived` để đọc token từ query khi path matches `/hubs/*`: +```csharp +options.Events = new JwtBearerEvents { + OnMessageReceived = ctx => { + var accessToken = ctx.Request.Query["access_token"]; + var path = ctx.HttpContext.Request.Path; + if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs")) + ctx.Token = accessToken; + return Task.CompletedTask; + } +}; +``` + +### 27. SignalR SaveChangesInterceptor — capture Added ở SavingChanges, push ở SavedChanges + +**Lý do:** SavedChanges chỉ có entries sau commit thành công. Nhưng ở SavedChanges thì `EntityEntry.State` đã về `Unchanged` → không thể filter `Added`. + +**Fix:** 2-phase pattern: +```csharp +public override ValueTask> SavingChangesAsync(...) { + _pending = eventData.Context.ChangeTracker.Entries() + .Where(e => e.State == EntityState.Added) + .Select(e => e.Entity).ToList(); + return base.SavingChangesAsync(...); +} + +public override async ValueTask SavedChangesAsync(..., int result, ...) { + foreach (var n in _pending) await _realtimeNotifier.PushAsync(n); + _pending.Clear(); + return result; +} +``` + +## DevOps / CI/CD + +### 28. LibreOffice download URL 404 khi pin wrong version + +**Triệu chứng:** `Invoke-WebRequest https://download.documentfoundation.org/libreoffice/stable/25.2.7/...` → 404. + +**Nguyên nhân:** LibreOffice mirror chỉ giữ vài version mới nhất. 25.2.7, 24.8.7 không có. Chỉ 25.8.6 tồn tại tại thời điểm cài. + +**Fix:** Check mirror URL trước khi pin. Dùng `Invoke-WebRequest -Method Head` verify trước download thật. + +### 29. PowerShell 5.1 `>> $GITHUB_PATH` ghi UTF-16 → NUL byte crash Gitea Actions + +**Triệu chứng:** Gitea Actions job fail với "NUL byte in PATH". `echo "C:\\dotnet" >> $env:GITHUB_PATH`. + +**Nguyên nhân:** PS 5.1 default encoding UTF-16 LE BOM khi redirect `>>`. Gitea reads PATH as UTF-8 → NUL byte xuất hiện sau mỗi ASCII char. + +**Fix:** Dùng `Out-File -Encoding utf8 -Append`: +```powershell +"C:\dotnet" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append +``` + +Hoặc drop step GITHUB_PATH hoàn toàn nếu NSSM PATH đã có sẵn dotnet+node. + +### 30. PS 5.1 scripts với Vietnamese diacritics → parser error + +**Triệu chứng:** `Cannot parse script: Unexpected character` khi chạy PS script có text tiếng Việt inline. + +**Nguyên nhân:** PS 5.1 đọc file script với ANSI codepage (Windows-1258 hoặc default 1252), không phải UTF-8. + +**Fix (1):** Save script với BOM UTF-8 (Write-Host có dấu vẫn work): +```powershell +[System.IO.File]::WriteAllText($path, $content, [System.Text.Encoding]::UTF8) +``` + +**Fix (2, safer):** Rewrite script ASCII-only. Text tiếng Việt nằm trong log messages thay dùng code: +```powershell +Write-Host "Setup IIS sites done" # thay vi "Hoan tat" +``` + +## TypeScript / FE + +### 31. Dialog `size="xl"` TS2322 nếu variant không khai báo + +**Triệu chứng:** `` → `Type '"xl"' is not assignable to type '"sm" | "md" | "lg"'`. + +**Fix:** Sửa usage về `"lg"`, hoặc add `"xl"` vào `DialogSize` type union trong `components/ui/Dialog.tsx`. Đừng lazy `as any`. + +## FE architecture + +### 32. NavLink `end` prop cho query-param URL variants + +**Triệu chứng:** `/contracts?type=1` highlight cả `/contracts` lẫn `/contracts?type=2` cùng lúc. + +**Nguyên nhân:** Default NavLink `startsWith` match. Query string không parse distinct paths. + +**Fix:** `end={path.includes('?')}` trong resolvePath để query-variants match exact: +```tsx + +``` + ## Checklist debug bug mới 1. Build pass không? → fail → check using + package version compat @@ -207,4 +310,6 @@ Tương tự khi dùng URL Rewrite `` cần unlock `system.webS 6. Nếu TS error → check `erasableSyntaxOnly`, `verbatimModuleSyntax` 7. Nếu EF expression tree → tách logic ra ngoài query 8. Nếu Unicode CLI → dùng file payload -9. Nếu workflow 403 → check FE NEXT_PHASES sync BE +9. Nếu workflow 403 → check FE `workflow.nextPhases` sync từ BE pinned policy +10. Nếu SignalR 401 → dùng `accessTokenFactory` + BE OnMessageReceived hook (#26) +11. Nếu PS 5.1 script fail → check encoding UTF-8 / BOM / ASCII-only (#30) diff --git a/docs/workflow-contract.md b/docs/workflow-contract.md index 247c8b7..93e13f6 100644 --- a/docs/workflow-contract.md +++ b/docs/workflow-contract.md @@ -98,7 +98,7 @@ Ký hiệu: `R` = read, `W` = write/update draft, `A` = approve (chuyển phase | Quá SLA → auto-approve | Drafter + role giữ phase | email + in-app (log audit) | | Reject (quay về `DangSoanThao`) | Drafter | email + in-app | -## 7. Data model implication (cho Phase 3) +## 7. Data model implication (cho Phase 3 + Tier 3 versioned) ```csharp // Domain @@ -117,40 +117,157 @@ public enum ContractPhase { public class Contract : AuditableEntity { public Guid Id { get; set; } - public string MaHopDong { get; set; } // tự gen theo RG-001 + public string? MaHopDong { get; set; } // tự gen theo RG-001 public ContractType Type { get; set; } // HDTP, HDGK, NCC, HDDV... public ContractPhase Phase { get; set; } public Guid SupplierId { get; set; } public Guid ProjectId { get; set; } public decimal GiaTri { get; set; } public bool BypassProcurementAndCCM { get; set; } - public DateTime? SlaDeadline { get; set; } // khi nào phase hiện tại hết hạn - // ... - public List Comments { get; set; } // thread góp ý phase 3 - public List Approvals { get; set; } // ai ký phase nào, lúc nào - public List Attachments { get; set; } // scan bản gốc, file export + public DateTime? SlaDeadline { get; set; } + public bool SlaWarningSent { get; set; } + + // Tier 3: pin policy version at create-time cho immutability + public Guid? WorkflowDefinitionId { get; set; } + + public List Comments { get; set; } + public List Approvals { get; set; } + public List Attachments { get; set; } } public class ContractApproval { public Guid ContractId { get; set; } - public ContractPhase Phase { get; set; } - public Guid ApproverUserId { get; set; } + public ContractPhase FromPhase { get; set; } + public ContractPhase ToPhase { get; set; } + public Guid? ApproverUserId { get; set; } // null = system (SLA auto) public DateTime? ApprovedAt { get; set; } - public ApprovalDecision Decision { get; set; } // Approve | Reject | AutoApprove + public ApprovalDecision Decision { get; set; } // Pending | Approve | Reject | AutoApprove public string? Comment { get; set; } } + +// ==================== Tier 3: versioned workflow ==================== + +public class WorkflowDefinition : AuditableEntity { + public Guid Id { get; set; } + public string Code { get; set; } = ""; // "QT-MB", "QT-TP", "QT-NCC", ... + public int Version { get; set; } // 1, 2, 3, ... auto-increment per Code + public bool IsActive { get; set; } // chỉ 1 = true per ContractType + public ContractType ContractType { get; set; } + public string Name { get; set; } = ""; // "Quy trình Mua bán v02" + public string? Description { get; set; } + + public List Steps { get; set; } = new(); +} + +public class WorkflowStep { + public Guid Id { get; set; } + public Guid WorkflowDefinitionId { get; set; } + public int Order { get; set; } // thứ tự step trong định nghĩa + public ContractPhase Phase { get; set; } // target phase + public string Name { get; set; } = ""; // "Kiểm tra CCM" + public int SlaDays { get; set; } // SLA ngày cho phase này + + public List Approvers { get; set; } = new(); +} + +public class WorkflowStepApprover { + public Guid Id { get; set; } + public Guid WorkflowStepId { get; set; } + public ApproverKind Kind { get; set; } // Role | User + public string AssignmentValue { get; set; } = ""; // RoleName hoặc UserId Guid string +} + +public enum ApproverKind { Role = 1, User = 2 } ``` **Service chính:** -- `IContractWorkflowService.TransitionAsync(contractId, targetPhase, userId, comment)` — check guard + update state + tạo approval + notify -- `IContractCodeGenerator.GenerateAsync(projectId, type, supplierId)` — dùng SEMAPHORE/transaction tránh race condition -- `ISlaExpiryJob` — hosted service chạy mỗi 15 phút, auto-approve các HĐ quá hạn +- `IContractWorkflowService.TransitionAsync(contractId, targetPhase, userId, comment)` — resolve policy, check guard, update state, tạo approval, emit notification +- `IContractCodeGenerator.GenerateAsync(projectId, type, supplierId)` — SERIALIZABLE transaction tránh race +- `SlaExpiryJob : BackgroundService` — 15min, auto-approve quá hạn với Decision=AutoApprove +- `IRealtimeNotifier` (SignalR impl) — push vào group User-{Id} khi Notification created + +## 7bis. Policy resolution — versioned workflow + +```mermaid +sequenceDiagram + participant User as Actor + participant API as ContractsController + participant WF as ContractWorkflowService + participant DB as WorkflowDefinitions + + User->>API: POST /contracts/{id}/transitions {targetPhase} + API->>WF: TransitionAsync(id, targetPhase, userId, comment) + + alt Contract.WorkflowDefinitionId != null (Tier 3 pinned) + WF->>DB: Include(Steps.Approvers).First(Id == wfId) + DB-->>WF: def + WF->>WF: policy = Registry.FromDefinition(def) + else Admin override in WorkflowTypeAssignments + WF->>DB: Find(ContractType == c.Type) + DB-->>WF: override + WF->>WF: policy = Registry.ByName(override.PolicyName) + else Legacy fallback + WF->>WF: policy = Registry.For(c.Type) // hardcoded Standard/SkipCcm + end + + WF->>WF: check (from, to) ∈ policy.Transitions + WF->>WF: check actor.Roles ∩ allowedRoles != ∅ + WF->>DB: UPDATE Phase + INSERT ContractApproval + INSERT Notifications + WF-->>API: 200 +``` + +## 7ter. Admin designer flow (tạo version mới) + +``` +Admin → /system/workflows → click type "HĐ Mua bán" +→ /system/workflows/MuaBan +→ thấy active version QT-MB-v01 + history +→ click "Tạo phiên bản mới" → Designer modal (có thể Clone từ v01) + - Code: QT-MB (auto-fill) + - Version: v02 (auto-compute max+1) + - Name + Description + - Steps (repeatable): + [Order 1] Phase=2 (DangSoanThao) SLA=7 days Approvers: +Role Drafter, +Role DeptManager + [Order 2] Phase=3 (DangGopY) SLA=7 days Approvers: +Role ProjectManager, +User {userId} + ... + - Save → POST /api/workflows + BE: auto Version=max+1, deactivate QT-MB-v01.IsActive=0, insert v02.IsActive=1, atomic +→ trở về /system/workflows/MuaBan → v02 active, v01 archived "N HĐ còn chạy" +→ HĐ cũ pin v01 vẫn chạy v01 (Contract.WorkflowDefinitionId không đổi) +→ HĐ mới tạo sau đây sẽ pin v02 +``` ## 8. Business rules summary 1. **Một role chỉ có 1 phase active tại 1 thời điểm** cho 1 HĐ. -2. **Auto-approve nếu quá SLA** — nhưng phải log `Decision=AutoApprove` rõ ràng trong `ContractApproval`. +2. **Auto-approve nếu quá SLA** — phải log `Decision=AutoApprove` rõ ràng trong `ContractApproval`. 3. **Reject → quay về `DangSoanThao`** — Drafter nhận lại, toàn bộ approval trước đó bị invalidate (kept as history). 4. **Không cho xóa HĐ** đã qua phase 5 (`DangInKy`) — chỉ soft delete. -5. **Mã HĐ** gen theo `forms-spec.md § RG-001` — chỉ gen khi transition sang phase 5. -6. **Audit log đầy đủ** — mọi transition đều ghi `AuditLog(entityId, action, oldPhase, newPhase, userId, timestamp, diff)`. +5. **Mã HĐ** gen theo `forms-spec.md § RG-001` — chỉ gen khi transition sang phase `DangDongDau` (8). +6. **Audit log** — mọi transition đều insert `ContractApprovals` row với actor + timestamp + phase before/after. +7. **Versioned workflow** — `Contract.WorkflowDefinitionId` pin tại create-time, **không update sau đó**. Admin tạo version mới ảnh hưởng HĐ tương lai, HĐ cũ giữ version cũ. +8. **Chỉ 1 active version per ContractType** — enforce qua business logic trong `CreateWorkflowDefinitionCommand` (atomic deactivate + insert). + +## 9. Code pointers (Tier 3) + +**Domain:** +- `Domain/Contracts/WorkflowDefinition.cs` +- `Domain/Contracts/WorkflowStep.cs` +- `Domain/Contracts/WorkflowStepApprover.cs` (+ `ApproverKind` enum) +- `Domain/Contracts/WorkflowPolicy.cs` (record + `WorkflowPolicies.Standard/SkipCcm` + `WorkflowPolicyRegistry.FromDefinition` + `ForContract`) + +**Application:** +- `Application/Contracts/WorkflowAdminFeatures.cs`: + - `GetWorkflowAdminOverviewQuery` — landing per-type + active + history + - `CreateWorkflowDefinitionCommand` — auto Version + atomic deactivate old + +**Infrastructure:** +- `Infrastructure/Services/ContractWorkflowService.cs` — `LoadPolicyAsync(contractId)` resolution order + +**Api:** +- `Api/Controllers/WorkflowsController.cs` — GET overview, GET per-type, POST create-version + +**FE-Admin:** +- `fe-admin/src/pages/system/WorkflowsPage.tsx` — URL-driven, landing + per-type +- `fe-admin/src/components/workflow/WorkflowDesigner.tsx` — modal Steps + Approvers +- `fe-admin/src/components/workflow/DefinitionCard.tsx` — active + history card