[CLAUDE] Docs: chốt session Tier 3 feature-complete + versioned workflow
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m33s

- Session log 2026-04-22-0300 (A→K): attachment, SignalR, form builder,
  PDF, dynamic + versioned workflow, nested menu, 3-panel permissions,
  seed master, brand identity, content polish, Gitea fix
- STATUS: Tier 3 feature-complete snapshot + cumulative stats (24 tables,
  ~50 endpoints, 8 migrations); next-up = UAT + Email SMTP (blocked) +
  rotate creds + SQL backup schedule
- HANDOFF: rewrite brief cho session mới — phase 5 prod done, Tier 3
  đóng gói, quick sanity-check 2 app, versioned workflow quick ref,
  file active hiện trạng, git state
- migration-todos: tick Tier 3 items (attachment/realtime/form builder/
  PDF/dynamic+versioned workflow/nested menu) + thêm iter-3 versioned
  workflow section + post-launch list
- schema-diagram: +5 table (Notifications, WorkflowTypeAssignments,
  WorkflowDefinitions, WorkflowSteps, WorkflowStepApprovers); indexes
  mới, cardinality FK restrict cho pinned policy, truy vấn tiêu biểu
- workflow-contract: +section 7bis resolution order, 7ter admin
  designer flow, updated data model + code pointers Tier 3
- PROJECT-MAP: module map post-Tier-3 (3 box mới Notification/
  Attachment/Branding + Infra/DevOps box), API namespace đầy đủ,
  architectural wins 5 điểm
- contract-workflow skill: versioned workflow section, policy
  resolution code snippet, admin designer flow, code pointers Tier 3,
  tier 4+ backlog
- gotchas +7 bẫy mới (#26-32): SignalR WebSocket headers, interceptor
  2-phase pattern, LibreOffice mirror 404, PS 5.1 UTF-16 GITHUB_PATH,
  PS 5.1 diacritics parse, Dialog size TS, NavLink end query-params

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-22 10:25:02 +07:00
parent 91b2da147f
commit fbca83264c
9 changed files with 1363 additions and 479 deletions

View File

@ -1,6 +1,6 @@
--- ---
name: contract-workflow 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: when-to-use:
- "transition contract" - "transition contract"
- "chuyển phase hợp đồng" - "chuyển phase hợp đồng"
@ -9,11 +9,15 @@ when-to-use:
- "reject contract về draft" - "reject contract về draft"
- "mã HĐ sai format" - "mã HĐ sai format"
- "bypass CCM chủ đầu tư" - "bypass CCM chủ đầu tư"
- "versioned workflow"
- "quy trình mới HĐ cũ giữ cũ"
- "WorkflowDefinition pin"
- "admin workflow designer"
--- ---
# Contract Workflow Skill # Contract Workflow Skill
> **Status:** Phase 3 IMPLEMENTED (MVPstate transitions + code gen). Còn thiếu: SLA hosted service, email notify, in-app realtime. > **Status:** Tier 3 FEATURE-COMPLETEState 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) ## Domain entities (implemented)
@ -21,8 +25,14 @@ when-to-use:
Contract ─────< ContractApproval (lịch sử mỗi transition) Contract ─────< ContractApproval (lịch sử mỗi transition)
─────< ContractComment (thread góp ý) ─────< ContractComment (thread góp ý)
─────< ContractAttachment (scan signed/sealed) ─────< ContractAttachment (scan signed/sealed)
─────> WorkflowDefinition (PINNED at create-time, nullable FK)
ContractCodeSequence (Prefix PK, LastSeq) — gen mã HĐ atomic 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 ## 9 phase state machine
@ -39,11 +49,69 @@ Alternates:
Bypass (HĐ Chủ đầu tư, BypassProcurementAndCCM=true): Bypass (HĐ Chủ đầu tư, BypassProcurementAndCCM=true):
DangInKy → DangTrinhKy (skip CCM) 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 | | DangSoanThao | 7d |
| DangGopY | 7d | | DangGopY | 7d |
@ -54,9 +122,11 @@ Bypass (HĐ Chủ đầu tư, BypassProcurementAndCCM=true):
| DangDongDau | none | | DangDongDau | none |
| DaPhatHanh | 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 | | 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. **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) ## Mã HĐ gen (RG-001)
Xem `ContractCodeGenerator.GenerateAsync()`. Format theo loại HĐ: 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 | | HopDongNguyenTacNCC | `{Year}/NCC/SOL&{SupplierCode}/{Seq:D2}` ← framework |
| HopDongNguyenTacDichVu | `{Year}/HĐDV/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. **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:** **Backend Domain:**
- `Domain/Contracts/Contract.cs` — aggregate root - `Domain/Contracts/Contract.cs` — aggregate root (+ `WorkflowDefinitionId?`)
- `Domain/Contracts/ContractApproval.cs` — history - `Domain/Contracts/ContractApproval.cs` — history
- `Domain/Contracts/ContractComment.cs` — thread - `Domain/Contracts/ContractComment.cs` — thread
- `Domain/Contracts/ContractAttachment.cs` — files - `Domain/Contracts/ContractAttachment.cs` — files
- `Domain/Contracts/ContractCodeSequence.cs` — seq table - `Domain/Contracts/ContractCodeSequence.cs` — seq table
- `Application/Contracts/Services/IContractWorkflowService.cs` + `IContractCodeGenerator.cs` - `Domain/Contracts/WorkflowPolicy.cs` — record + `WorkflowPolicies.Standard/SkipCcm` + `WorkflowPolicyRegistry.{For, FromDefinition, ByName}`
- `Infrastructure/Services/ContractWorkflowService.cs`state + role guard - **`Domain/Contracts/WorkflowDefinition.cs`**versioned policy header
- `Infrastructure/Services/ContractCodeGenerator.cs`transactional gen - **`Domain/Contracts/WorkflowStep.cs`**step trong definition
- `Application/Contracts/ContractFeatures.cs`CQRS (Create, Update draft, Transition, AddComment, List, Inbox, GetDetail, Delete) - **`Domain/Contracts/WorkflowStepApprover.cs`**Role/User approver (+ `ApproverKind` enum)
- `Api/Controllers/ContractsController.cs` — REST endpoints - `Domain/Contracts/WorkflowTypeAssignment.cs` — legacy admin override
**Frontend:** **Backend Application:**
- `fe-admin/src/pages/contracts/ContractsListPage.tsx` — full list admin view - `Application/Contracts/Services/IContractWorkflowService.cs` + `IContractCodeGenerator.cs`
- `fe-admin/src/pages/contracts/ContractDetailPage.tsx` — detail + timeline + action - `Application/Contracts/ContractFeatures.cs` — CQRS (Create pin WorkflowDefId, Update draft, Transition, AddComment, List, Inbox, GetDetail, Delete)
- `fe-user/src/pages/InboxPage.tsx` — HĐ chờ role tôi xử lý - `Application/Contracts/ContractAttachmentFeatures.cs` — Upload/Download/Delete CQRS
- `fe-user/src/pages/contracts/ContractCreatePage.tsx` — tạo HĐ draft - **`Application/Contracts/WorkflowAdminFeatures.cs`** — `GetWorkflowAdminOverviewQuery` + `CreateWorkflowDefinitionCommand`
- `fe-user/src/pages/contracts/ContractDetailPage.tsx` — duplicate có chủ đích
- `fe-user/src/pages/contracts/MyContractsPage.tsx` — HĐ của tôi **Backend Infrastructure:**
- `fe-admin/src/types/contracts.ts` + `fe-user/src/types/contracts.ts` — type mirror - `Infrastructure/Services/ContractWorkflowService.cs` — resolve policy (pinned → override → fallback), state + role guard
- `fe-admin/src/components/PhaseBadge.tsx` — badge màu theo phase - `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 ## API endpoints
| Method | Path | Purpose | | 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/inbox` | HĐ chờ role của user xử lý |
| GET | `/api/contracts/{id}` | Detail + approvals + comments + attachments | | GET | `/api/contracts/{id}` | Detail + approvals + comments + attachments + pinned workflow |
| POST | `/api/contracts` | Tạo draft (Phase = DangSoanThao) | | POST | `/api/contracts` | Tạo draft — pin `WorkflowDefinitionId = active version for type` |
| PUT | `/api/contracts/{id}` | Update draft (chỉ khi Phase = DangSoanThao) | | 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}/transitions` | Chuyển phase (body: `{targetPhase, decision, comment}`) |
| POST | `/api/contracts/{id}/comments` | Thêm comment vào thread | | 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) | | DELETE | `/api/contracts/{id}` | Soft delete (chỉ < DangInKy) |
| **GET** | **`/api/workflows`** | Admin: overview per ContractType (active + history + "N 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 ## Guard Rules đã implement
- **State adjacency:** chỉ cho chuyển giữa các (from, to) đã khai báo trong `Transitions` dict - **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 đó - **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 - **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) - **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). Default false phải qua CCM - **Bypass CCM:** `Contract.BypassProcurementAndCCM=true` cho phép `DangInKy → DangTrinhKy` (skip CCM)
- **Self-delete:** không cho xóa đã qua `DangInKy` - **Self-delete:** không cho xóa đã qua `DangInKy`
- **Versioned pin:** `Contract.WorkflowDefinitionId` pinned at create không update sau đó. FK restrict admin không xóa được def nếu còn tham chiếu
## Workflow tạo HĐ end-to-end (testable) ## Workflow tạo HĐ end-to-end (testable, Tier 3)
```bash ```bash
# 1. Setup master data # 1. Setup master data (auto-seeded: 5 supplier + 3 project + 9 dept)
POST /api/suppliers { code: "PVL", name: "...", type: 1 } # 2. Admin tạo version mới cho HĐ Mua bán
POST /api/projects { code: "FLOCK 01", name: "..." } 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Đ # 3. User tạo HĐ Mua bán → pin WorkflowDefinitionId = v02.Id
POST /api/contracts { type: 2, supplierId, projectId, giaTri: 150000000, tenHopDong: "..." } POST /api/contracts { type: 5, supplierId, projectId, giaTri: ..., tenHopDong: "..." }
# → Phase = DangSoanThao, SlaDeadline = +7d # → Phase=DangSoanThao, SlaDeadline=+7d, WorkflowDefinitionId=v02
# 3. Submit góp ý # 4. Transition — guard load từ v02.Steps.Approvers
POST /api/contracts/{id}/transitions { targetPhase: 3, decision: 1, comment: "..." } POST /api/contracts/{id}/transitions { targetPhase: 3, decision: 1, comment: "..." }
# 4. Chuyển qua các phase (với admin) # 5. Chuyển qua các phase
4 DangDamPhan → 5 DangInKy → 6 DangKiemTraCCM7 DangTrinhKy 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 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 9 DaPhatHanh
``` ```
@ -171,15 +281,17 @@ POST /api/contracts/{id}/transitions { targetPhase: 3, decision: 1, comment: ".
- **Race condition gen song song** dùng `IsolationLevel.Serializable`, không skip. - **Race condition gen 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. - **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. - **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 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 20% SLA (`SlaWarningSent` flag đã )
- [ ] Warning notification khi còn 20% SLA - [ ] User-kind approver runtime guard (data model ready)
- [ ] Email notification (MailKit) khi chuyển phase - [ ] Email notification (MailKit) khi chuyển phase BLOCKED SMTP
- [ ] In-app notification badge SignalR push - [ ] RowVersion optimistic concurrency (2 user cùng duyệt 409)
- [ ] Upload attachment endpoint + FE (multipart)
- [ ] RowVersion optimistic concurrency (2 user cùng duyệt)
- [ ] ContractClause appendix attach khi export trọn gói - [ ] ContractClause appendix attach khi export trọn gói
- [ ] Audit log riêng (`AuditLogs` table) ngoài `ContractApprovals` - [ ] Audit log riêng (`AuditLogs` table) ngoài `ContractApprovals`
- [ ] MediatR `AuditBehavior` log mọi command
- [ ] E-signature integration (VNPT/FPT CA)

View File

@ -1,28 +1,29 @@
# HANDOFF — Brief 5 phút cho session tiếp theo # 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? ## Ở đâu rồi?
| Phase | Trạng thái | | Phase | Trạng thái |
|---|---| |---|---|
| 0 Draft | ✅ Done | | 0 Draft | ✅ Done |
| 1 Alpha Core foundation | ✅ Done | | 1 Alpha Core (foundation + đợt 2 CRUD + Permission) | ✅ Done |
| 1 Alpha Core đợt 2 (CRUD + Permission) | ✅ Done | | 2 Form Engine MVP + iter 2 (upload UI + .doc auto-convert + PDF export) | ✅ Done |
| 2 Form Engine MVP | ✅ Done | | 3 Workflow MVP (9 phase + code gen) + iter 2 (SLA job + attachment + notify) | ✅ Done |
| 2 Form Engine iteration 2 | 📝 Optional | | 4 Report MVP (Dashboard + Excel) + user-specific dashboard | ✅ Done |
| 3 Workflow MVP (9 phase + code gen) | ✅ Done | | 5 Prep + 5.1 Security + Users Mgmt | ✅ Done |
| 3 Workflow iteration 2 (SLA + notify + attachment) | 📝 Optional | | **5 Deploy prod** (3 domain HTTPS live) | ✅ Done |
| 4 Report MVP (Dashboard + Excel) | ✅ Done | | **Tier 3 (Attach + Realtime + Form builder + PDF + Versioned WF + Nested menu + Permission 3-panel)** | ✅ Done |
| 4 Report iteration 2 | 📝 Optional | | 6+ Post-launch (E-signature, Bravo/SAP, Mobile, AI) | 📝 Future |
| 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 |
## Run nhanh ## Run nhanh
```powershell ```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 dotnet run --project src\Backend\SolutionErp.Api
# Terminal 2 — Admin FE # Terminal 2 — Admin FE
@ -34,144 +35,218 @@ cd fe-user && npm run dev # → http://localhost:8080
Login: `admin@solutionerp.local` / `Admin@123456` Login: `admin@solutionerp.local` / `Admin@123456`
Điểm cần test ngay (Phase 4 MVP): ## Quick sanity-check
- **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
## 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 ## Cần làm kế tiếp
- [ ] `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
### 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 - E-signature (VNPT CA / FPT CA) — Phase 6
- Field spec JSON per template + dynamic form builder FE - Bravo/SAP import NCC — Phase 6
- `{{#loop}}...{{/loop}}` block support cho table lặp - Mobile app — Phase 6
- PDF convert via LibreOffice headless - AI OCR scan HĐ — Phase 6+
- 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)
## Lưu ý kỹ thuật quan trọng ## Lưu ý kỹ thuật quan trọng
**Đọc [`gotchas.md`](gotchas.md) trước khi:** **Đọc [`gotchas.md`](gotchas.md) (26 bẫy) 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)
## 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/ SOLUTION_ERP/
├── src/Backend/ (Clean Arch, 4 project, .NET 10) ├── src/Backend/ (Clean Arch, 4 project, .NET 10)
│ ├── SolutionErp.Domain/ │ ├── SolutionErp.Domain/
│ │ ├── Common/ BaseEntity, AuditableEntity │ │ ├── Common/ BaseEntity, AuditableEntity
│ │ ├── Contracts/ ContractType, ContractPhase, ApprovalDecision + **Contract, ContractApproval, ContractComment, ContractAttachment, ContractCodeSequence** ← Phase 3 │ │ ├── Contracts/ ContractType, ContractPhase, ApprovalDecision,
│ │ ├── Forms/ ContractTemplate, ContractClause ← Phase 2 │ │ Contract (+WorkflowDefinitionId), ContractApproval,
│ │ ├── Identity/ User, Role, MenuItem, Permission, AppRoles, MenuKeys │ │ │ ContractComment, ContractAttachment, ContractCodeSequence,
│ │ └── Master/ Supplier, Project, Department, SupplierType │ │ │ **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/ │ ├── SolutionErp.Application/
│ │ ├── Auth/ Login, Refresh, Me │ │ ├── Auth/ Login, Refresh, Me
│ │ ├── Common/ Exceptions, Behaviors, Interfaces, Models │ │ ├── Common/
│ │ ── Contracts/ ContractFeatures (8 CQRS), IContractWorkflowService, IContractCodeGenerator, DTOs ← Phase 3 │ │ ── Interfaces/ IApplicationDbContext, ICurrentUser, IDateTime,
│ │ ├── Forms/ FormFeatures (List/Get/Render) ← Phase 2 │ │ │ IJwtTokenService, **IFileStorage**, **IDocumentConverter**,
│ │ ├── Master/ Suppliers, Projects, Departments CQRS │ │ │ **IRealtimeNotifier**, **INotificationService**
│ │ ├── Permissions/ GetMyMenuTree, matrix upsert │ │ ├── Contracts/ ContractFeatures, IContractWorkflowService,
│ │ └── Reports/ **DashboardStats, ExportContractsToExcel, IContractExcelExporter** ← Phase 4 │ │ │ **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/ │ ├── SolutionErp.Infrastructure/
│ │ ├── Forms/ DocxRenderer, XlsxRenderer, FormRenderer ← Phase 2 │ │ ├── Forms/ DocxRenderer, XlsxRenderer, FormRenderer,
│ │ ├── Identity/ JwtSettings, JwtTokenService │ │ │ **LibreOfficeDocumentConverter**
│ │ ├── Persistence/ DbContext, DbInitializer, Interceptors, Migrations (**5**) │ │ ├── Identity/ JwtSettings, JwtTokenService
│ │ ├── Reports/ **ContractExcelExporter** ← Phase 4 │ │ ├── Persistence/
│ │ └── Services/ DateTimeService, ContractWorkflowService, ContractCodeGenerator ← Phase 3 │ │ │ ├── Interceptors/ AuditingInterceptor, **NotificationPushInterceptor**
│ │ │ └── Migrations/ 8 migrations
│ │ ├── Reports/ ContractExcelExporter
│ │ ├── **Storage/** LocalFileStorage (path-traversal guard)
│ │ └── Services/ DateTimeService, **ContractWorkflowService (load pinned def)**,
│ │ ContractCodeGenerator, **NotificationService**
│ └── SolutionErp.Api/ │ └── SolutionErp.Api/
│ ├── Authorization/ MenuPermissionHandler + Requirement │ ├── Authorization/ MenuPermissionHandler + Requirement
│ ├── Controllers/ Auth, Suppliers, Projects, Departments, Menus, Roles, Permissions, Forms, Contracts, **Reports** (10 controller) │ ├── Controllers/ Auth, Suppliers, Projects, Departments, Menus,
├── Middleware/ GlobalExceptionMiddleware │ Roles, Permissions, Forms, Contracts, Reports,
├── Services/ CurrentUserService, WebHostEnvironmentLocator │ Users, **Notifications**, **Workflows**
── wwwroot/templates/ 5 file .docx/.xlsx ← Phase 2 ── **Hubs/** NotificationHub (/hubs/notifications)
├── fe-admin/ (11 page) │ ├── Middleware/ GlobalExceptionMiddleware
└── src/pages/ ├── **Realtime/** SignalRNotifier
│ ├── LoginPage │ ├── Services/ CurrentUserService, WebHostEnvironmentLocator
── DashboardPage ← Phase 4 rewrite (KPI cards + BarChart) ── wwwroot/templates/ .docx/.xlsx templates
│ ├── master/SuppliersPage, ProjectsPage, DepartmentsPage ├── fe-admin/ (~18 page)
│ ├── system/PermissionsPage │ └── src/
│ ├── forms/FormsPage ← Phase 2 │ ├── pages/
├── contracts/ContractsListPage, ContractDetailPage ← Phase 3 │ ├── LoginPage, DashboardPage (MyDashboardRow)
└── ReportsPage ← Phase 4 │ ├── master/ Suppliers, Projects, Departments
├── fe-user/ (5 page) │ │ ├── system/ **PermissionsPage (3-panel)**, **WorkflowsPage (URL-driven)**, Users
└── src/pages/ │ ├── forms/ FormsPage (upload + Form/JSON + PDF)
├── LoginPage │ ├── contracts/ List, Detail (+Attachments), **Create**
├── InboxPage ← Phase 3 │ └── ReportsPage
── contracts/ContractCreatePage, ContractDetailPage, MyContractsPage ← Phase 3 ── components/ Layout (recursive menu + filterForAdmin),
├── docs/ (35 file) TopBar, NotificationBell, UserMenu, SlaTimer,
├── STATUS.md, HANDOFF.md, rules.md, architecture.md │ EmptyState, PhaseBadge, WorkflowSummaryCard,
├── CLAUDE.md, PROJECT-MAP.md │ ContractAttachmentsSection, DynamicForm,
├── workflow-contract.md, forms-spec.md │ **WorkflowDesigner** (Steps + Approvers modal)
├── database/{database-guide, schema-diagram}.md └── 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) │ ├── flows/ (7 file — README + 6 flow)
│ ├── guides/ (4 file) — deployment-iis, cicd, security-checklist, runbook ← Phase 5 prep │ ├── guides/ (4 file)
│ ├── changelog/migration-todos.md + sessions/ (7 session log) │ ├── changelog/migration-todos + sessions/ (8 session log)
│ └── gotchas.md │ └── gotchas (26 pitfall)
├── scripts/ (5 file PS + py) ├── scripts/ (5 PS + py)
│ ├── parse_forms.py, parse_workflow.py (Phase 0) │ ├── parse_forms, parse_workflow (Phase 0)
│ ├── convert-doc-to-docx.ps1 (Phase 2) │ ├── convert-doc-to-docx (Phase 2)
── deploy-iis.ps1, backup-sql.ps1 ← Phase 5 prep ── deploy-iis, backup-sql (Phase 5)
├── .gitea/workflows/deploy.yml ← Phase 5 prep CI/CD template │ └── install-libreoffice (Tier 3)
── .claude/skills/ (3 skill — all full spec) ── .gitea/workflows/deploy.yml CI/CD Windows self-hosted runner
└── .claude/skills/ 3 skill (contract-workflow, form-engine, permission-matrix)
``` ```
## Git state ## Git state
``` ```
(sẽ là commit 8) — Phase 5 Prep (infra + scripts + guides + refresh token) HEAD → main
fe7ad8e — Phase 4 Report MVP + docs consolidation 91b2da1 — PermissionsPage 3-panel layout (LATEST)
7e957a7 — Phase 3 Workflow MVP f216169 — Admin Workflows tabs → sidebar menu items
5113e4c — Phase 2 Form Engine MVP 355bbe3 — Fix Dialog size TS (xl → lg)
54d6c9b — Phase 1.2 CRUD + Permission e7e5f2d — Versioned workflow entities + migration + designer
49a5f57 — Docs database-guide + flows 4 session trước đó nằm trong STATUS table
702411f — Phase 1 foundation
25dad7f — Phase 0 scaffold
Branch: main Branch: main (tracking origin/main)
Remote: chưa (Gitea URL CẦN NGAY để Phase 5 go-live) Remote: https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp.git
``` ```
## Credentials + URLs ## Credentials + URLs
@ -180,23 +255,27 @@ Remote: chưa (Gitea URL CẦN NGAY để Phase 5 go-live)
admin@solutionerp.local / Admin@123456 admin@solutionerp.local / Admin@123456
``` ```
- API: http://localhost:5443 (swagger `/swagger`) - API prod: https://api.huypham.vn — `/health/live`, `/health/ready`
- Admin FE: http://localhost:8082 - Admin FE prod: https://admin.huypham.vn
- User FE: http://localhost:8080 - User FE prod: https://user.huypham.vn
- SQL LocalDB: `(localdb)\MSSQLLocalDB` / Database=`SolutionErp_Dev` - 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 ## Đánh giá nhanh
**Tốt:** **Tốt:**
- Build pass 100% cả BE + FE - 3 domain HTTPS prod live, CI/CD xanh
- E2E test full 9-phase workflow end-to-end — mã HĐ gen đúng format RG-001 - 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
- Docs đầy đủ: 26 file, session log mỗi chunk, gotchas tích lũy 17 pitfall - Clean-arch 3-project split đúng cho 2 cross-cutting service (realtime + document-converter)
- Cả 2 FE đều có Contract detail page + timeline + comment thread + state machine action - 26 gotchas tích lũy, 8 session log, 40 docs agent onboard nhanh
- Invariant critical: " giữ quy trình " guaranteed by pinning (reference-based immutability, không snapshot copy)
**Rủi ro còn:** **Rủi ro còn:**
- SLA chỉ set deadline — không có job auto-approve (Phase 3.2) - UAT thật chưa chạy thể phát hiện edge case missing
- Không có notification (email + in-app) — user phải F5 inbox - SMTP chưa notification chỉ in-app (toast + bell), không email
- Form render chỉ MVP — loop table + PDF chưa có - User-kind approver chưa enable guard runtime (designer cho pick, nhưng transition dùng Role fall-back)
- Permission matrix chưa test thực với non-admin user - Credentials chưa rotate
- 3 file .doc chưa convert (carryover Phase 2) - SQL backup chưa schedule Task Scheduler
- Không có upload attachment endpoint (chỉ có entity + DTO)

View File

@ -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). > Đọ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 │ │ SOLUTION_ERP │
│ 🌐 Prod live: api/admin/user.huypham.vn (HTTPS Let's Encrypt) │
└─────────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────────┘
╔════════════════╗ ╔════════════════╗ ╔════════════════╗ ╔════════════════╗ ╔════════════════╗ ╔════════════════╗
║ IDENTITY ║ ║ DANH MỤC ║ ║ QUẢN LÝ HĐ ║ ║ 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 ║ ║ User (+Mgmt) ║ ║ Supplier ║ ║ Contract ║
║ Role ║ ║ Project ║ ║ ContractType ║ Role (12 seed) ║ ║ Project ║ ║ + WorkflowDefId
║ Permission ║ ║ Department ║ ║ ContractForm ║ Permission ║ ║ Department ║ ║ Approval
MenuKey ║ ║ ContractClause ║ ║ Approval (3-panel UI) ║ + seed demo ║ ║ Comment
AuditLog ║ ║ (điều kiện ║ ║ Comment MenuKey ║ ║ ContractClause ║ ║ Attachment
chung - 002.04)║ ║ Attachment + nested tree ║ ║ (E2E upload)
╚════════════════╝ ╚════════════════╝ ║ AuditTrail ║ ╚════════════════╝ ╚════════════════╝ ╚════════════════╝
╚════════════════╝
╔════════════════╗ ╔════════════════╗ ╔════════════════╗ ╔════════════════╗ ╔════════════════╗ ╔════════════════╗
║ FORM ENGINE ║ ║ WORKFLOW ║ ║ BÁO CÁO ║ ║ FORM ENGINE ║ ║ WORKFLOW ║ ║ BÁO CÁO ║
║ (Phase 2) ║ ║ (Phase 3) ║ ║ (Phase 4) ║ (Phase 2) ║ ║ (Phase 3) ║ ║ (Phase 4)
╠════════════════╣ ╠════════════════╣ ╠════════════════╣ ╠════════════════╣ ╠════════════════╣ ╠════════════════╣
║ Template ║ ║ StateMachine ║ ║ Dashboard ║ ║ Template CRUD ║ ║ StateMachine ║ ║ Dashboard ║
Renderer ║ ║ Transition ║ ║ ExcelExport ║ DynamicForm ✅ ║ ║ Transition ║ ║ ExcelExport ║
║ (DOCX/XLSX) ║ ║ SlaTimer ║ ║ FilterQuery ║ (DOCX/XLSX) ║ ║ SlaTimer ║ ║ MyDashboard ✅
║ Field mapping ║ ║ Notification ║ ║ ║ FieldSpec JSON ║ ║ SlaExpiryJob ✅ ║ ║ (role-aware)
║ PO gen (F.07) ║ ║ CodeGenerator ║ ║ ║ ║ PDF export ✅ ║ ║ CodeGen RG-001 ║ ║ ║
║ ║ (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 ────< UserRoles >──── Role ────< Permission (Role × MenuKey × CRUD)
User ────< AuditLog
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) Project (Dự án)
Department (Phòng ban) Department (9 phòng từ QT docx)
Contract Contract
├── Type: HĐTP | HĐGK | NCC | HĐDV | HĐ Mua bán | ... ├── 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 — xem workflow-contract.md) ├── Phase (9 state)
├── Supplier, Project, Drafter ├── WorkflowDefinitionId (pinned policy at create-time)
├── MaHopDong (gen theo RG-001) ├── Supplier, Project, Drafter, Template
├── Approvals[] (audit ai ký phase nào) ├── MaHopDong (gen theo RG-001 khi DangDongDau)
├── Comments[] (thread góp ý Phase 3 của workflow) ├── SlaDeadline + SlaWarningSent flag
├── Attachments[] (scan bản gốc, file export) ├── Approvals[] (history)
── TemplateData (JSON — field đã điền khi render form) ── Comments[] (thread)
└── Attachments[] (scan + upload)
ContractTemplate (ánh xạ Type → File mẫu FO-002.xx) WorkflowDefinition (versioned per ContractType)
ContractClause (điều khoản chung FO-002.04 — rich text) ├── Code (QT-MB, QT-TP, ...) + Version (v01, v02, ...)
PurchaseOrder (có thể đính với Contract hoặc standalone) ├── 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/auth — login, refresh, me, logout
/api/users — CRUD user, assign role, reset password /api/users — CRUD + assign roles + reset password + unlock + toggle active
/api/roles — CRUD role, permission matrix /api/roles — list (CRUD Create/Rename/Delete: TODO)
/api/menus — menu tree + permission resolution /api/menus /me tree với inherit Contracts/Workflows
/api/suppliers — CRUD NCC /api/suppliers — CRUD NCC
/api/projects — CRUD dự án /api/projects — CRUD dự án
/api/departments — CRUD phòng ban /api/departments — CRUD phòng ban
/api/permissions — get matrix + bulk upsert
/api/contracts — CRUD + query by phase/project/supplier /api/contracts — CRUD + query by phase/supplier/project/type + pendingMe
/api/contracts/{id}/transitions — state machine action /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}/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 — CRUD templates (upload/update/delete + render + PDF export)
/api/forms/{id}/render — render template → docx/xlsx (Phase 2) /api/forms/templates/{id}/export-pdf — LibreOffice conversion
/api/reports/dashboard — KPI tổng hợp /api/workflows — admin GET overview + POST create new version
/api/reports/export — Excel download /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ý ### fe-admin (:8082) — cho Admin + Role quản lý
- `/login` - `/login`
- `/dashboard`KPI system - `/dashboard`MyDashboard row (4 card) + KPI + charts
- `/master/users` + `/master/roles` + `/master/permissions` - `/master/suppliers|projects|departments` — CRUD
- `/master/suppliers` + `/master/projects` + `/master/departments` - `/system/users` — Users Mgmt (create/reset/unlock/assign-roles/toggle-active)
- `/master/contract-templates` + `/master/contract-clauses` - `/system/permissions`**3-panel layout** (Role list | Menu×CRUD matrix | Granted stats)
- `/contracts`danh sách toàn bộ, filter phase/dự án - `/system/workflows`landing 7-card grid per ContractType
- `/contracts/{id}` — detail + approval panel + audit log - `/system/workflows/:typeCode` — Definition card (active + history) + Designer modal
- `/reports` + `/system/audit-log` - `/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` - `/login`
- `/inbox` — HĐ đang chờ tôi xử lý (filter theo role × phase) - `/inbox` — HĐ chờ role tôi xử lý (lọc theo `?type=X`)
- `/contracts/new`chọn template + điền form + submit - `/my-contracts`HĐ tôi đã tạo/tham gia (lọc theo `?type=X`)
- `/contracts/{id}`detail, comment, approve/reject - `/contracts/new`tạo HĐ draft (pre-select type)
- `/my-contracts`HĐ tôi đã tạo/tham gia - `/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) Drafter (QS/NV.PB) ← pin WorkflowDefinitionId = v02 active
├─ [POST /api/contracts] tạo draft + chọn template ├─ [POST /api/contracts] tạo draft Phase=DangSoanThao, pin v02
├─ [POST /api/forms/{id}/render] fill + preview ├─ [POST /api/forms/templates/{id}/render] fill FieldSpec + preview
├─ (upload scan đính kèm qua /attachments)
├─ [POST /api/contracts/{id}/transitions] DangSoanThao → DangGopY ├─ [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) PD/PM/PRO/CCM/FIN/ACT (song song)
└─ [POST /api/contracts/{id}/comments] góp ý └─ [POST /api/contracts/{id}/comments] góp ý → Notification push Drafter
Drafter Drafter
├─ [PATCH /api/contracts/{id}] revise ├─ [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) NTP/NCC/TĐ (external — Drafter update thay)
└─ upload scan có chữ ký đối tác └─ upload scan có chữ ký đối tác
Drafter Drafter → [transitions] → DangKiemTraCCM
└─ [POST /api/contracts/{id}/transitions] → DangKiemTraCCM CCM → [transitions] → DangTrinhKy
BOD → [transitions] → DangDongDau
└─ BE: ContractCodeGenerator SERIALIZABLE → gen MaHopDong RG-001
CCM HRA → [transitions] → DaPhatHanh (final)
└─ [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)
``` ```
## External dependencies ## External dependencies (hiện trạng)
- **SQL Server** — chính thức, dev có thể LocalDB hoặc Docker (`docker-compose.yml`) - **SQL Server** — prod SQLEXPRESS trên VPS, dev LocalDB hoặc Docker
- **IIS** — deploy target (Windows Server 2019+) - **IIS** — Windows Server (VPS shared VIETREPORT), URL Rewrite + ARR + WebSockets module
- **Gitea** — git remote (URL chờ user cấp) - **Gitea** — https://git.baocaogiaoduc.vn (self-hosted, shared runner)
- **Aspose.Words / DocumentFormat.OpenXml** — render .docx (Phase 2, quyết định khi đó) - **win-acme** — Let's Encrypt auto-renew
- **EPPlus hoặc ClosedXML** — render .xlsx (Phase 2) - **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) ## Non-goals (KHÔNG làm)
- ❌ Python AI service (user đã quyết gác vô thời hạn) - ❌ 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) - ❌ Multi-tenant (1 instance / 1 công ty)
- ❌ Tích hợp e-signature (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+ nếu có) - ❌ Tích hợp SAP/Bravo ERP Phase 6+
- ❌ Public API / external webhooks - ❌ 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_<Code>`.

View File

@ -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`. > **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 ### 🌐 Production URLs
- https://api.huypham.vn — API (Let's Encrypt, auto-renew via win-acme) - https://api.huypham.vn — API (Let's Encrypt, auto-renew via win-acme)
- https://admin.huypham.vn — Admin FE (HTTP→HTTPS auto-redirect) - https://admin.huypham.vn — Admin FE (HTTP→HTTPS auto-redirect)
- https://user.huypham.vn — User FE (HTTP→HTTPS auto-redirect) - https://user.huypham.vn — User FE (HTTP→HTTPS auto-redirect)
@ -15,125 +16,106 @@
## 🔥 In Progress ## 🔥 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) ## ✅ Recently Done (newest on top)
| Ngày | Ai | Task | Commit | | 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-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-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-22 | Claude | **Admin Workflows tabs → sidebar menu items** — Seed 7 `Wf_<Code>` leaf dưới group `Workflows`. Layout resolvePath `Wf_<Code>``/system/workflows/<code>`. 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-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-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 | **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 | **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 | **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 | **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 | **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 | **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 | **Fix Gitea 500 sau Install Web-WebSockets** — Feature install khóa section `<webSocket>` ở applicationHost.config → tất cả IIS site fail. Fix: `appcmd unlock config -section:system.webServer/webSocket`. Gotcha #25 | `c52186b` | | 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 | **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 | **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 | **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 | **Fix Gitea 500 sau Install Web-WebSockets** — appcmd unlock section webSocket. Gotcha #25 | `c52186b` |
| 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 | **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 | **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 | **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 | **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 | **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 | **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 | **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 | **PermissionsPage improved** — search, stats badge, bulk column toggle, empty state icon | `6c0e206` | | 2026-04-21 | Claude | **Fix login Network Error** — SPA web.config HTTP→HTTPS redirect rule (CORS chỉ https) | `397eb36` |
| 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 | **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 | **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 | **PermissionsPage iter 1** — search, stats badge, bulk column toggle, empty state | `6c0e206` |
| 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 | **ERP shell** — TopBar + NotificationBell + UserMenu (avatar + role badges). Layout `[sidebar] [topbar + content]` | `2b6f91c` |
| 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 | **Tier 1 UI polish** — SlaTimer (inline + full variant, 5 chỗ), Inbox stat cards, DataTable skeleton rows, EmptyState | `290936a`..`2e43799` |
| 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-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.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 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 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 2 Form Engine MVP** | `5113e4c` |
| 2026-04-21 | Claude | **Phase 1.2** — CRUD Master + Permission Matrix | `54d6c9b` | | 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** + Docs addition | `702411f` + `49a5f57` |
| 2026-04-21 | Claude | **Phase 1 foundation** | `702411f` |
| 2026-04-21 | Claude | **Phase 0** | `25dad7f` | | 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:** **Docs entry points:**
- [`rules.md`](rules.md) · [`architecture.md`](architecture.md) · [`HANDOFF.md`](HANDOFF.md) - [`rules.md`](rules.md) · [`architecture.md`](architecture.md) · [`HANDOFF.md`](HANDOFF.md)
- [`workflow-contract.md`](workflow-contract.md) · [`forms-spec.md`](forms-spec.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) - [`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) - [`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 ## 🎯 Next up
### Phase 5 (prod go-live) ### Hard blockers (chờ user / ops)
- [x] Gitea remote + push all commits - [ ] **UAT 1 tuần 2-3 user thật** — hard requirement từ roadmap Phase 5
- [x] Gitea Actions runner (self-hosted Windows, shared VIETREPORT runner) - [ ] **Email outbox** — MailKit + SMTP (BLOCKED chờ user cấp SMTP host/user/pass)
- [x] Secrets Gitea (JWT_SECRET, DB_CONNECTION — IIS_* deprecated sau rewrite workflow) - [ ] **Rotate credentials** — SA, vrapp, JWT secret, runner token (đã post chat)
- [x] CI/CD workflow xanh end-to-end - [ ] **SQL backup daily** — Task Scheduler (script `scripts/backup-sql.ps1` đã có, chưa schedule)
- [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
### 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 ✓ - [ ] Roles CRUD — admin tạo custom role ngoài 12 hardcoded (schema sẵn, chỉ cần CQRS + FE)
- [x] SignalR real-time push (auto-push interceptor + client auto-reconnect) ✓ - [ ] User-level approver targeting runtime — data model đã có (`WorkflowStepApprover.Kind=User`), chỉ cần wire User-kind vào `ContractWorkflowService.TransitionAsync` guard
- [x] Form template builder CRUD (admin upload .docx/.xlsx + FieldSpec JSON editor) ✓ - [ ] PermissionsPage: grant `Workflows.Read` cho non-admin role → menu Wf_* visible
- [x] Form builder iteration 2: DynamicForm render UI từ FieldSpec ✓ - [ ] 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] PDF export qua LibreOffice headless ✓
- [x] .doc → .docx auto-conversion khi upload template - [x] .doc/.xls → .docx/.xlsx auto-conversion ✓
- [x] Dynamic workflow policy per ContractType (Standard/SkipCcm) - [x] Dynamic workflow policy per ContractType ✓
- [ ] Email outbox cho Notification (MailKit, SMTP config — cần user config) - [x] **Versioned workflow (WorkflowDefinition pinned per Contract)**
- [x] **Admin workflow designer UI (per-type, per-step approvers)**
### Phase 5.1 Security — hầu như xong - [x] **Nested sidebar menu per ContractType (fe-user) + menu split admin/user**
- [x] **PermissionsPage 3-panel layout**
- [x] Security headers middleware (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, CSP) - [ ] Email outbox for Notification (blocked — SMTP config)
- [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)
## 📊 Thông số cumulative ## 📊 Thông số cumulative
| | P0 | P1f | P1.2 | P2 | P3 | P4 | **P5 prep** | | | P0 | P1f | P1.2 | P2 | P3 | P4 | P5prep | **Tier3** |
|---|---:|---:|---:|---:|---:|---:|---:| |---|---:|---:|---:|---:|---:|---:|---:|---:|
| BE LOC | 0 | ~400 | ~1500 | ~1900 | ~2700 | ~3100 | **~3300** | | BE LOC | 0 | ~400 | ~1500 | ~1900 | ~2700 | ~3100 | ~3300 | **~4800** |
| DB tables | 0 | 7 | 12 | 14 | 19 | 19 | 19 | | DB tables | 0 | 7 | 12 | 14 | 19 | 19 | 19 | **24** (+Notifications, +WorkflowTypeAssignments, +WorkflowDefinitions, +WorkflowSteps, +WorkflowStepApprovers) |
| API endpoints | 0 | 4 | 20 | 23 | 31 | 33 | **35** (+health) | | 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 | | Migrations | 0 | 1 | 3 | 4 | 5 | 5 | 5 | **8** (+AddNotifications, +AddWorkflowTypeAssignments, +AddVersionedWorkflows) |
| FE pages | 0 | 2 | 6 | 7 | 14 | 16 | 16 | | FE pages | 0 | 2 | 6 | 7 | 14 | 16 | 16 | **~20** (admin Users/Workflows per-type + user nested menu) |
| Scripts PS | 0 | 0 | 0 | 1 (convert-doc) | 1 | 1 | **3** (+deploy-iis, backup-sql) | | Scripts PS | 0 | 0 | 0 | 1 | 1 | 1 | 3 | **4** (+install-libreoffice) |
| CI/CD workflow | 0 | 0 | 0 | 0 | 0 | 0 | **1** | | CI/CD workflow | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
| Docs | 10 | 13 | 14 | 24 | 26 | 30 | **35** (+4 guides + session log) | | Docs | 10 | 13 | 14 | 24 | 26 | 30 | 35 | **~40** (+session log + updated MDs) |
| Commits | 1 | 2 | 3 | 5 | 6 | 7 | **8** (sắp) | | Commits | 1 | 2 | 3 | 5 | 6 | 7 | 8 | **~25** |
## 🚨 Blockers / risks ## 🚨 Blockers / risks
- **Gitea remote URL** — ĐANG CẦN để push + setup CI/CD - ⚠️ **Email SMTP chưa có** — blocker cho notification outbound
- ⚠️ **Phase 5.1 security hardening** chưa làm (headers, account lockout, IDOR check) - ⚠️ **UAT real user chưa chạy** — risk phát sinh bug edge-case quan trọng
- ⚠️ **3 file .doc chưa convert** (Phase 2 carryover) - ⚠️ **Credentials leaked trong chat** — cần rotate trước go-live thật
- ⚠️ **SLA không tự auto-approve** (Phase 3.2) - ⚠️ **SQL backup không auto** — risk data loss nếu VPS crash
- ⚠️ **Email/in-app notification** chưa có - ⚠️ **Permission `Workflows.Read` cho non-admin** — cần grant để họ thấy menu Wf_* (hiện chỉ admin thấy)
- ⚠️ **FE Users management chưa có** — khó test permission non-admin - ⚠️ **User-kind approver chưa enable runtime** — designer cho chọn User nhưng guard fall back DeptManager
- ⚠️ **Rate limit global 300/min/IP** — OK cho dev, cần tăng cho prod nếu nhiều user
## Credentials + URLs ## Credentials + URLs
@ -141,6 +123,9 @@ Session logs: [P0](changelog/sessions/2026-04-21-1045-phase0-scaffold.md) · [P1
admin@solutionerp.local / Admin@123456 admin@solutionerp.local / Admin@123456
``` ```
- API: http://localhost:5443 — Swagger `/swagger` (dev only) — Health `/health/live` + `/health/ready` - API prod: https://api.huypham.vn — Health `/health/live` + `/health/ready`
- Admin FE: http://localhost:8082 — `/dashboard`, `/contracts`, `/reports`, `/master/*`, `/forms`, `/system/permissions` - API dev: http://localhost:5443 — Swagger `/swagger`
- User FE: http://localhost:8080 — `/inbox`, `/contracts/new`, `/my-contracts` - 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`

View File

@ -58,7 +58,7 @@
- [x] FE: `main.tsx` với QueryClient (TanStack Query) - [x] FE: `main.tsx` với QueryClient (TanStack Query)
- [x] E2E verified: login qua Vite proxy cả 2 app → JWT + user info - [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] `Domain/Master/Supplier` (+ SupplierType enum 5 loại) / `Project` / `Department` (AuditableEntity)
- [x] EF `IEntityTypeConfiguration<T>` cho mỗi entity (unique Code + query filter IsDeleted) - [x] EF `IEntityTypeConfiguration<T>` cho mỗi entity (unique Code + query filter IsDeleted)
@ -67,27 +67,25 @@
- [x] Migration 2: `AddMasterData` - [x] Migration 2: `AddMasterData`
- [x] `Domain/Identity/MenuItem` (Key PK, Label, ParentKey, Order, Icon) + `MenuKeys` const class - [x] `Domain/Identity/MenuItem` (Key PK, Label, ParentKey, Order, Icon) + `MenuKeys` const class
- [x] `Domain/Identity/Permission` (RoleId, MenuKey, CanRead/Create/Update/Delete) - [x] `Domain/Identity/Permission` (RoleId, MenuKey, CanRead/Create/Update/Delete)
- [x] Seed default menu tree (12 menu) + admin full access trong DbInitializer - [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, union OR, tree filter - [x] `Application/Permissions/Queries/GetMyMenuTreeQuery` — resolve per-user + inherit Contracts/Workflows root
- [x] `Api/Controllers/{MenusController, RolesController, PermissionsController}` - [x] `Api/Controllers/{MenusController, RolesController, PermissionsController}`
- [x] Migration 3: `AddPermissions` - [x] Migration 3: `AddPermissions`
- [x] Authorization handler `MenuPermissionHandler` + register 48 policy `{menu}.{action}` - [x] Authorization handler `MenuPermissionHandler` + register 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] FE: `<PermissionGuard menuKey="Suppliers" action="Update">` + `usePermission()` hook - [x] FE: `<PermissionGuard menuKey="Suppliers" action="Update">` + `usePermission()` hook
- [x] FE Admin: 3 trang CRUD Supplier/Project/Department với DataTable + Dialog modal + search/sort/paging - [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) - [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` - [x] FE Admin: Layout menu động từ `/api/menus/me` + recursive nested + filterForAdmin
- [ ] FE User: trang "HĐ của tôi" list + filter — Phase 3 - [x] FE User: trang "HĐ của tôi" list + filter `?type=X` — Tier 3
- [ ] FE Admin: Users management page (tạo user + gán role) — sắp tới - [x] FE Admin: Users management page (tạo user + gán role + reset password + unlock)
- [ ] FE Admin: Roles CRUD — sắp tới - [ ] FE Admin: Roles CRUD — optional (12 role seed đủ dùng)
- [ ] Route guard theo role admin-only — PermissionGuard ở button, route cần thêm - [x] Route guard theo role admin-only — PermissionGuard ở button level
### Exit criteria Phase 1 ### Exit criteria Phase 1
- [ ] Admin login → tạo NCC/Project → tạo role "Nhân viên CCM" → gán permission menu "Contracts.Read" - [x] Admin login → tạo NCC/Project → gán permission menu
- [ ] User CCM login → thấy menu Contracts, không thấy menu Admin - [x] User non-admin login → thấy menu theo role, không bị 403
- [ ] Tạo Contract draft → list hiển thị, không bị 403 sai - [x] Tạo Contract draft → list hiển thị, filter role-aware
## Phase 2 — Form Engine (T5-6) ## Phase 2 — Form Engine (T5-6)
@ -107,18 +105,17 @@
- [x] FE admin: `FormsPage` — list + render dialog điền JSON + download - [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 - [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) - [x] Convert `.doc``.docx` / `.xls``.xlsx` qua `IDocumentConverter` + LibreOffice headless (thay Word COM, auto-convert khi admin upload)
- [ ] Parse chi tiết field của 5 template HĐ — mỗi form thành JSON `FieldSpec` - [x] FE user: form builder dynamic — `DynamicForm` component render từ `FieldSpec` JSON (text/textarea/number/date/currency/select)
- [ ] Support `{{#loop}}...{{/loop}}` block cho table lặp (hạng mục HĐ giao khoán, PO) - [x] FE admin: upload template mới qua UI (POST multipart) + edit FieldSpec + delete (soft via IsActive)
- [ ] FE user: form builder dynamic — render từ fieldSpec thay vì điền JSON tay - [x] PDF convert via LibreOffice headless (`soffice --headless --convert-to pdf`) — `LibreOfficeDocumentConverter` (timeout + per-request temp + isolated UserInstallation)
- [ ] FE admin: upload template mới qua UI (POST multipart) + edit field mapping - [x] Format helpers: number → `VND`, date → `dd/MM/yyyy` (render layer)
- [ ] Lưu `ContractClause` (FO-002.04) dạng rich text, admin edit qua TipTap/TinyMCE - [ ] Support `{{#loop}}...{{/loop}}` block cho table lặp (hạng mục HĐ giao khoán, PO) — optional
- [ ] PDF convert via LibreOffice headless (`soffice --headless --convert-to pdf`) - [ ] Lưu `ContractClause` (FO-002.04) dạng rich text + TipTap/TinyMCE editor — optional
- [ ] Import/export template (backup/restore) - [ ] Import/export template (backup/restore) — optional
- [ ] Format helpers: number → `150,000,000 VND`, date → `dd/MM/yyyy` - [ ] Content preservation test (render → diff layout) — optional
- [ ] Content preservation test: render → diff layout với template gốc
## Phase 3 — Workflow State Machine (T7-9) ## Phase 3 — Workflow State Machine (T7-9)
@ -141,21 +138,39 @@
- [x] PhaseBadge component + color map - [x] PhaseBadge component + color map
- [x] E2E verified: tạo HĐ → chạy 9 phase → gen mã `FLOCK 01/HĐGK/SOL&PVL2026/01` - [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] `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] E2E test với non-admin user (Drafter role) — IDOR filter verified
- [x] Admin password warning log khi vẫn dùng default - [x] Admin password warning log khi vẫn dùng default
- [ ] Warning notification khi còn 20% SLA (track `SlaWarningSent` flag đã có) - [x] `Infrastructure/Services/NotificationService` — in-app + emit (email đợi SMTP)
- [ ] `Infrastructure/Services/NotificationService` — email (MailKit) + in-app - [x] SignalR hub cho real-time notification badge — `/hubs/notifications` + interceptor auto-push
- [ ] SignalR hub cho real-time notification badge - [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) - [ ] 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) - [ ] RowVersion optimistic concurrency (2 user race → 409)
- [ ] Render HĐ docx lúc tạo (merge TemplateId + DraftData + ContractClause appendix) - [ ] Render HĐ docx lúc tạo (merge TemplateId + DraftData + ContractClause appendix)
- [ ] Filter Inbox theo phase ở FE - [ ] E2E test: reject → quay về DangSoanThao với multi-role
- [ ] E2E test: reject → quay về DangSoanThao - [ ] Email notification (MailKit + SMTP) — blocked chờ user config
- [ ] E2E test: SLA expired → auto-approve + log (test thật qua set SlaDeadline past)
### 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) ## Phase 4 — Reporting + Polish (T10-11)
@ -168,13 +183,15 @@
- [x] FE `ReportsPage` filter + export - [x] FE `ReportsPage` filter + export
- [x] Docs consolidation: `rules.md` + `architecture.md` + `database/schema-diagram.md` + gotchas update - [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) - [ ] SLA overdue report (by role / phase, export Excel)
- [ ] Contract audit log export (từng HĐ ra PDF) - [ ] 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) - [ ] 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 - [ ] Accessibility: keyboard nav, focus trap modal, aria labels
- [ ] Dark mode - [ ] Dark mode
- [ ] Performance: explicit index DB cho query hot đã identify - [ ] Performance: explicit index DB cho query hot đã identify
@ -200,17 +217,18 @@
- [x] `docs/guides/runbook.md` — operations (restart, rollback, restore) - [x] `docs/guides/runbook.md` — operations (restart, rollback, restore)
- [x] FE refresh token auto interceptor (queue pattern cả 2 app) - [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) - [x] Windows Server setup: IIS + URL Rewrite + ARR (reverse proxy FE → IIS)
- [ ] SQL Server prod + Task Scheduler trigger backup-sql.ps1 - [x] SQL Server prod (SQLEXPRESS) + vrapp db_owner
- [ ] HTTPS certificate (Let's Encrypt qua win-acme) - [x] HTTPS certificate (Let's Encrypt qua win-acme — 3 cert + auto-renew)
- [ ] Gitea remote setup + push all commits - [x] Gitea remote setup + push all commits
- [ ] Set 5 Gitea Actions secrets (IIS_HOST/USER/PASSWORD/JWT_SECRET/DB_CONNECTION) - [x] Set Gitea Actions secrets (JWT_SECRET, DB_CONNECTION — deploy local via runner)
- [ ] Enable Gitea runner (Windows + Ubuntu) - [x] Enable Gitea runner (Windows self-hosted, shared với VIETREPORT)
- [ ] Test CI/CD workflow lần đầu staging - [x] Test CI/CD workflow — xanh E2E, /health/live 200 sau deploy
- [ ] UAT production 1 tuần với 2-3 user thật - [ ] **UAT production 1 tuần với 2-3 user thật** ← hard blocker còn lại
- [ ] Go-live checklist: backup, rollback plan, on-call contact - [ ] 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 ### 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`) - [ ] 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 - [ ] 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) ## 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) - [ ] E-signature integration (VNPT CA hoặc FPT CA)
- [ ] Tích hợp Bravo / SAP ERP import NCC - [ ] Tích hợp Bravo / SAP ERP import NCC
- [ ] Mobile app (React Native?) cho BOD duyệt ngoài giờ - [ ] Mobile app (React Native?) cho BOD duyệt ngoài giờ

View File

@ -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_<Code>` group + `Ct_<Code>_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_<Code>` leaves dưới `Workflows` group.
- Layout resolvePath `Wf_<Code>``/system/workflows/<code>`.
- 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 `<webSocket>`
applicationHost → all IIS sites with `<webSocket enabled="true">` 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ó).

View File

@ -1,6 +1,6 @@
# Schema Diagram — Luồng DB SOLUTION_ERP # 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 ## 1. Full ERD
@ -22,6 +22,7 @@ erDiagram
Departments ||--o{ Contracts : "drafted-in" Departments ||--o{ Contracts : "drafted-in"
Users ||--o{ Contracts : "drafter" Users ||--o{ Contracts : "drafter"
ContractTemplates ||--o{ Contracts : "uses" ContractTemplates ||--o{ Contracts : "uses"
WorkflowDefinitions ||--o{ Contracts : "pinned-policy"
Contracts ||--o{ ContractApprovals : "history" Contracts ||--o{ ContractApprovals : "history"
Contracts ||--o{ ContractComments : "thread" Contracts ||--o{ ContractComments : "thread"
@ -29,6 +30,11 @@ erDiagram
Users ||--o{ ContractApprovals : "approved-by" Users ||--o{ ContractApprovals : "approved-by"
Users ||--o{ ContractComments : "author" Users ||--o{ ContractComments : "author"
WorkflowDefinitions ||--o{ WorkflowSteps : "has"
WorkflowSteps ||--o{ WorkflowStepApprovers : "allowed-by"
Users ||--o{ Notifications : "recipient"
Users { Users {
uniqueidentifier Id PK uniqueidentifier Id PK
nvarchar FullName "200" nvarchar FullName "200"
@ -126,6 +132,7 @@ erDiagram
uniqueidentifier DepartmentId FK uniqueidentifier DepartmentId FK
uniqueidentifier DrafterUserId FK uniqueidentifier DrafterUserId FK
uniqueidentifier TemplateId FK uniqueidentifier TemplateId FK
uniqueidentifier WorkflowDefinitionId FK "pinned policy, nullable"
decimal GiaTri "18,2" decimal GiaTri "18,2"
bit BypassProcurementAndCCM bit BypassProcurementAndCCM
datetime2 SlaDeadline datetime2 SlaDeadline
@ -170,6 +177,54 @@ erDiagram
int LastSeq int LastSeq
datetime2 UpdatedAt 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) ## 2. Luồng dữ liệu chính (data flow diagram)
@ -182,18 +237,27 @@ flowchart TB
P --> MI[MenuItems] P --> MI[MenuItems]
end end
subgraph MASTER ["📋 Master data (admin CRUD)"] subgraph MASTER ["📋 Master data (admin CRUD + seed demo)"]
S[Suppliers] S[Suppliers]
PR[Projects] PR[Projects]
DE[Departments] DE[Departments]
end end
subgraph FORMS ["📄 Form templates (seed)"] subgraph FORMS ["📄 Form templates (admin upload)"]
CT[ContractTemplates] CT[ContractTemplates]
CC[ContractClauses] CC[ContractClauses]
end 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] C[Contracts]
CA[ContractApprovals] CA[ContractApprovals]
CCM[ContractComments] CCM[ContractComments]
@ -201,39 +265,48 @@ flowchart TB
CCS[ContractCodeSequences] CCS[ContractCodeSequences]
end end
subgraph NOTIFY ["🔔 Notification module"]
N[Notifications]
end
U -.Drafter.-> C U -.Drafter.-> C
S --> C S --> C
PR --> C PR --> C
DE --> C DE --> C
CT --> C CT --> C
WD -.pinned at create.-> C
C --> CA C --> CA
C --> CCM C --> CCM
C --> CAT C --> CAT
C -.gen when DangDongDau.-> CCS 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 ```mermaid
flowchart LR flowchart LR
Create[POST /contracts] Create[POST /contracts type=5]
Create --> C1["Contracts INSERT<br/>Phase=2, SLA=+7d"] Create --> PickWD["SELECT TOP 1 WorkflowDefinition<br/>WHERE ContractType=5 AND IsActive=1<br/>→ Id=wf-v02"]
PickWD --> C1["Contracts INSERT<br/>Phase=2, SLA=+7d, WorkflowDefinitionId=wf-v02"]
Transition1[Transition 2→3] Transition1[Transition 2→3]
Transition1 --> C2["UPDATE Phase=3<br/>INSERT ContractApprovals"] Transition1 --> LoadPolicy["Load wf-v02.Steps.Approvers<br/>WorkflowPolicyRegistry.FromDefinition(def)"]
LoadPolicy --> Guard["Check allowed roles for<br/>(from=2, to=3)"]
Guard --> C2["UPDATE Phase=3<br/>INSERT ContractApprovals<br/>INSERT Notifications bulk"]
Comment[POST /comments] Comment[POST /comments]
Comment --> C3[INSERT ContractComments] Comment --> C3["INSERT ContractComments<br/>INSERT Notifications"]
Transition2[Transition 3→4→5→6→7] NewVersion[Admin creates QT-MB-v03]
Transition2 --> C4["UPDATE Phase + SlaDeadline<br/>INSERT ContractApprovals"] NewVersion --> NV1["INSERT WorkflowDefinition v03 IsActive=1<br/>UPDATE v02 SET IsActive=0 (atomic)"]
NV1 -.->|HĐ cũ không ảnh hưởng| C1
Transition3[Transition 7→8 BOD ký] Transition3[Transition 7→8 BOD ký]
Transition3 --> CG["ContractCodeGenerator<br/>SERIALIZABLE tran<br/>UPSERT ContractCodeSequences"] Transition3 --> CG["ContractCodeGenerator SERIALIZABLE<br/>UPSERT ContractCodeSequences"]
CG --> C5["UPDATE Contract<br/>SET MaHopDong='FLOCK 01/HĐGK/SOL&PVL/01',<br/>Phase=8"] CG --> C5["UPDATE Contract<br/>SET MaHopDong, Phase=8<br/>INSERT Notifications"]
Transition4[Transition 8→9 HRA phát hành]
Transition4 --> C6["UPDATE Phase=9, SlaDeadline=NULL<br/>INSERT ContractApprovals"]
``` ```
## 4. Index strategy ## 4. Index strategy
@ -245,12 +318,19 @@ flowchart LR
| Contracts | `IX_Contracts_SupplierId` | Filter HĐ theo NCC | | Contracts | `IX_Contracts_SupplierId` | Filter HĐ theo NCC |
| Contracts | `IX_Contracts_ProjectId` | Filter HĐ theo dự án | | Contracts | `IX_Contracts_ProjectId` | Filter HĐ theo dự án |
| Contracts | `IX_Contracts_SlaDeadline` | SLA expiry job query | | Contracts | `IX_Contracts_SlaDeadline` | SLA expiry job query |
| Contracts | `IX_Contracts_WorkflowDefinitionId` | Pinned policy lookup |
| ContractApprovals | `IX_ContractApprovals_ContractId_ApprovedAt` | Timeline query theo HĐ | | ContractApprovals | `IX_ContractApprovals_ContractId_ApprovedAt` | Timeline query theo HĐ |
| ContractComments | `IX_ContractComments_ContractId_CreatedAt` | Thread load | | ContractComments | `IX_ContractComments_ContractId_CreatedAt` | Thread load |
| ContractAttachments | `IX_ContractAttachments_ContractId` | Attachments load | | ContractAttachments | `IX_ContractAttachments_ContractId` | Attachments load |
| Suppliers/Projects/Departments | `UX_{Table}_Code` | Unique business code | | Suppliers/Projects/Departments | `UX_{Table}_Code` | Unique business code |
| Permissions | `UX_Permissions_RoleId_MenuKey` | 1 row / role / menu | | Permissions | `UX_Permissions_RoleId_MenuKey` | 1 row / role / menu |
| MenuItems | `IX_MenuItems_ParentKey` | Tree query | | 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). 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 | | 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 | | 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ự | | 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 | | Role → Permission | 1 - N | Cascade | Xóa role → clear permissions |
| MenuItem → Permission | 1 - N | Cascade | — | | MenuItem → Permission | 1 - N | Cascade | — |
| MenuItem → MenuItem (self-ref) | 1 - N | Restrict | Parent-child menu | | 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 ## 6. Soft delete behavior
@ -278,47 +362,89 @@ Entity list áp dụng:
- Supplier, Project, Department - Supplier, Project, Department
- Contract - Contract
- ContractTemplate, ContractClause - ContractTemplate, ContractClause
- WorkflowDefinition (admin archive = `IsActive=false`, xóa logic chỉ khi muốn scrub)
KHÔNG soft delete (cascade hoặc keep): KHÔNG soft delete (cascade hoặc keep):
- ContractApproval, ContractComment, ContractAttachment — cascade khi Contract xóa - ContractApproval, ContractComment, ContractAttachment — cascade khi Contract xóa
- Permission, MenuItem — cascade khi Role xóa - Permission, MenuItem — cascade khi Role xóa
- ContractCodeSequence — không bao giờ xóa (giữ history seq) - 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 - Identity tables (Users, Roles, ...) — Identity không support soft delete built-in
## 7. Truy vấn tiêu biểu ## 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 ```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 SELECT c.Id, c.MaHopDong, c.TenHopDong, c.Phase, c.SlaDeadline, s.Name AS SupplierName, p.Name AS ProjectName
FROM Contracts c FROM Contracts c
INNER JOIN Suppliers s ON c.SupplierId = s.Id INNER JOIN Suppliers s ON c.SupplierId = s.Id
INNER JOIN Projects p ON c.ProjectId = p.Id INNER JOIN Projects p ON c.ProjectId = p.Id
WHERE c.IsDeleted = 0 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; 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 ```sql
-- Tổng + active + overdue
SELECT SELECT
(SELECT COUNT(*) FROM Contracts WHERE IsDeleted = 0) AS Total, (SELECT COUNT(*) FROM Contracts
(SELECT COUNT(*) FROM Contracts WHERE IsDeleted = 0 AND Phase NOT IN (9, 99)) AS Active, WHERE DrafterUserId = @Me AND IsDeleted = 0 AND Phase NOT IN (9, 99)) AS DraftsInProgress,
(SELECT COUNT(*) FROM Contracts WHERE IsDeleted = 0 AND Phase NOT IN (9, 99) AND SlaDeadline < GETUTCDATE()) AS Overdue; (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 ### Notifications unread count (bell badge)
SELECT Phase, COUNT(*) FROM Contracts WHERE IsDeleted = 0 GROUP BY Phase;
-- Top 5 NCC ```sql
SELECT TOP 5 c.SupplierId, s.Name, COUNT(*) AS Cnt, SUM(c.GiaTri) AS TotalValue SELECT COUNT(*) FROM Notifications
FROM Contracts c WHERE RecipientUserId = @Me AND IsRead = 0;
INNER JOIN Suppliers s ON c.SupplierId = s.Id
WHERE c.IsDeleted = 0
GROUP BY c.SupplierId, s.Name
ORDER BY COUNT(*) DESC;
``` ```
### Gen mã HĐ atomic ### Gen mã HĐ atomic
@ -327,32 +453,63 @@ ORDER BY COUNT(*) DESC;
BEGIN TRAN; BEGIN TRAN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
MERGE ContractCodeSequences AS tgt UPDATE ContractCodeSequences
USING (SELECT @Prefix AS Prefix) AS src ON tgt.Prefix = src.Prefix SET LastSeq = LastSeq + 1, UpdatedAt = GETUTCDATE()
WHEN MATCHED THEN UPDATE SET LastSeq = LastSeq + 1, UpdatedAt = GETUTCDATE() WHERE Prefix = @Prefix;
WHEN NOT MATCHED THEN INSERT (Prefix, LastSeq, UpdatedAt) VALUES (@Prefix, 1, GETUTCDATE());
IF @@ROWCOUNT = 0
INSERT ContractCodeSequences (Prefix, LastSeq, UpdatedAt)
VALUES (@Prefix, 1, GETUTCDATE());
SELECT LastSeq FROM ContractCodeSequences WHERE Prefix = @Prefix; SELECT LastSeq FROM ContractCodeSequences WHERE Prefix = @Prefix;
COMMIT; 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ử ## 8. Migration lịch sử
| # | Migration | Tables added | | # | Migration | Tables added / changed |
|---|---|---| |---|---|---|
| 1 | `Init` | 7 Identity tables | | 1 | `Init` | 7 Identity tables |
| 2 | `AddMasterData` | Suppliers, Projects, Departments | | 2 | `AddMasterData` | Suppliers, Projects, Departments |
| 3 | `AddPermissions` | MenuItems, Permissions | | 3 | `AddPermissions` | MenuItems, Permissions |
| 4 | `AddForms` | ContractTemplates, ContractClauses | | 4 | `AddForms` | ContractTemplates, ContractClauses |
| 5 | `AddContractsWorkflow` | Contracts, ContractApprovals, ContractComments, ContractAttachments, ContractCodeSequences | | 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 đủ - [`database-guide.md`](database-guide.md) — conventions + migration workflow + cheatsheet đầy đủ
- [`../architecture.md`](../architecture.md) — layered architecture + data flow - [`../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 - [`../flows/`](../flows/) — sequence diagrams

View File

@ -197,6 +197,109 @@ Tương tự khi dùng URL Rewrite `<serverVariables>` 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. **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<InterceptionResult<int>> SavingChangesAsync(...) {
_pending = eventData.Context.ChangeTracker.Entries<Notification>()
.Where(e => e.State == EntityState.Added)
.Select(e => e.Entity).ToList();
return base.SavingChangesAsync(...);
}
public override async ValueTask<int> 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:** `<Dialog size="xl">``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
<NavLink to={path} end={path.includes('?')}>
```
## Checklist debug bug mới ## Checklist debug bug mới
1. Build pass không? → fail → check using + package version compat 1. Build pass không? → fail → check using + package version compat
@ -207,4 +310,6 @@ Tương tự khi dùng URL Rewrite `<serverVariables>` cần unlock `system.webS
6. Nếu TS error → check `erasableSyntaxOnly`, `verbatimModuleSyntax` 6. Nếu TS error → check `erasableSyntaxOnly`, `verbatimModuleSyntax`
7. Nếu EF expression tree → tách logic ra ngoài query 7. Nếu EF expression tree → tách logic ra ngoài query
8. Nếu Unicode CLI → dùng file payload 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)

View File

@ -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) | | Quá SLA → auto-approve | Drafter + role giữ phase | email + in-app (log audit) |
| Reject (quay về `DangSoanThao`) | Drafter | email + in-app | | 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 ```csharp
// Domain // Domain
@ -117,40 +117,157 @@ public enum ContractPhase {
public class Contract : AuditableEntity { public class Contract : AuditableEntity {
public Guid Id { get; set; } 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 ContractType Type { get; set; } // HDTP, HDGK, NCC, HDDV...
public ContractPhase Phase { get; set; } public ContractPhase Phase { get; set; }
public Guid SupplierId { get; set; } public Guid SupplierId { get; set; }
public Guid ProjectId { get; set; } public Guid ProjectId { get; set; }
public decimal GiaTri { get; set; } public decimal GiaTri { get; set; }
public bool BypassProcurementAndCCM { 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 DateTime? SlaDeadline { get; set; }
// ... public bool SlaWarningSent { get; set; }
public List<ContractComment> Comments { get; set; } // thread góp ý phase 3
public List<ContractApproval> Approvals { get; set; } // ai ký phase nào, lúc nào // Tier 3: pin policy version at create-time cho immutability
public List<ContractAttachment> Attachments { get; set; } // scan bản gốc, file export public Guid? WorkflowDefinitionId { get; set; }
public List<ContractComment> Comments { get; set; }
public List<ContractApproval> Approvals { get; set; }
public List<ContractAttachment> Attachments { get; set; }
} }
public class ContractApproval { public class ContractApproval {
public Guid ContractId { get; set; } public Guid ContractId { get; set; }
public ContractPhase Phase { get; set; } public ContractPhase FromPhase { get; set; }
public Guid ApproverUserId { get; set; } public ContractPhase ToPhase { get; set; }
public Guid? ApproverUserId { get; set; } // null = system (SLA auto)
public DateTime? ApprovedAt { get; set; } 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; } 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<WorkflowStep> 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<WorkflowStepApprover> 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:** **Service chính:**
- `IContractWorkflowService.TransitionAsync(contractId, targetPhase, userId, comment)` — check guard + update state + tạo approval + notify - `IContractWorkflowService.TransitionAsync(contractId, targetPhase, userId, comment)` resolve policy, check guard, update state, tạo approval, emit notification
- `IContractCodeGenerator.GenerateAsync(projectId, type, supplierId)`dùng SEMAPHORE/transaction tránh race condition - `IContractCodeGenerator.GenerateAsync(projectId, type, supplierId)`SERIALIZABLE transaction tránh race
- `ISlaExpiryJob` — hosted service chạy mỗi 15 phút, auto-approve các HĐ quá hạn - `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 ## 8. Business rules summary
1. **Một role chỉ có 1 phase active tại 1 thời điểm** cho 1 HĐ. 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). 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. 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. 5. **Mã HĐ** gen theo `forms-spec.md § RG-001` — chỉ gen khi transition sang phase `DangDongDau` (8).
6. **Audit log đầy đủ** — mọi transition đều ghi `AuditLog(entityId, action, oldPhase, newPhase, userId, timestamp, diff)`. 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