[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 @@
# Schema Diagram — Luồng DB SOLUTION_ERP
> ERD đầy đủ + mối quan hệ 19 table sau Phase 3. Mermaid render ở VS Code / GitHub / Gitea.
> ERD đầy đủ + mối quan hệ **24 table** sau Tier 3 (Notifications + Versioned workflows). Mermaid render ở VS Code / GitHub / Gitea.
## 1. Full ERD
@ -22,6 +22,7 @@ erDiagram
Departments ||--o{ Contracts : "drafted-in"
Users ||--o{ Contracts : "drafter"
ContractTemplates ||--o{ Contracts : "uses"
WorkflowDefinitions ||--o{ Contracts : "pinned-policy"
Contracts ||--o{ ContractApprovals : "history"
Contracts ||--o{ ContractComments : "thread"
@ -29,6 +30,11 @@ erDiagram
Users ||--o{ ContractApprovals : "approved-by"
Users ||--o{ ContractComments : "author"
WorkflowDefinitions ||--o{ WorkflowSteps : "has"
WorkflowSteps ||--o{ WorkflowStepApprovers : "allowed-by"
Users ||--o{ Notifications : "recipient"
Users {
uniqueidentifier Id PK
nvarchar FullName "200"
@ -126,6 +132,7 @@ erDiagram
uniqueidentifier DepartmentId FK
uniqueidentifier DrafterUserId FK
uniqueidentifier TemplateId FK
uniqueidentifier WorkflowDefinitionId FK "pinned policy, nullable"
decimal GiaTri "18,2"
bit BypassProcurementAndCCM
datetime2 SlaDeadline
@ -170,6 +177,54 @@ erDiagram
int LastSeq
datetime2 UpdatedAt
}
Notifications {
uniqueidentifier Id PK
uniqueidentifier RecipientUserId FK
int Type "ContractTransition/CommentAdded/SlaExpired/..."
nvarchar Title "200"
nvarchar Body "2000"
nvarchar Link "500"
bit IsRead
datetime2 ReadAt
datetime2 CreatedAt
}
WorkflowTypeAssignments {
uniqueidentifier Id PK
int ContractType "UK"
nvarchar PolicyName "50 Standard/SkipCcm"
datetime2 UpdatedAt
uniqueidentifier UpdatedBy FK
}
WorkflowDefinitions {
uniqueidentifier Id PK
nvarchar Code "100 QT-MB / QT-TP / ..."
int Version "auto-increment per Code"
bit IsActive "chi 1 active per ContractType"
int ContractType
nvarchar Name "200"
nvarchar Description "500"
datetime2 CreatedAt
uniqueidentifier CreatedBy FK
}
WorkflowSteps {
uniqueidentifier Id PK
uniqueidentifier WorkflowDefinitionId FK
int Order
int Phase "target ContractPhase int"
nvarchar Name "200"
int SlaDays
}
WorkflowStepApprovers {
uniqueidentifier Id PK
uniqueidentifier WorkflowStepId FK
int Kind "1=Role, 2=User"
nvarchar AssignmentValue "200 RoleName or UserId Guid string"
}
```
## 2. Luồng dữ liệu chính (data flow diagram)
@ -182,18 +237,27 @@ flowchart TB
P --> MI[MenuItems]
end
subgraph MASTER ["📋 Master data (admin CRUD)"]
subgraph MASTER ["📋 Master data (admin CRUD + seed demo)"]
S[Suppliers]
PR[Projects]
DE[Departments]
end
subgraph FORMS ["📄 Form templates (seed)"]
subgraph FORMS ["📄 Form templates (admin upload)"]
CT[ContractTemplates]
CC[ContractClauses]
end
subgraph CONTRACT ["📝 Contract workflow (Phase 3 core)"]
subgraph WORKFLOW ["⚙️ Versioned workflow (admin designer)"]
WD[WorkflowDefinitions]
WS[WorkflowSteps]
WSA[WorkflowStepApprovers]
WTA[WorkflowTypeAssignments legacy override]
WD --> WS
WS --> WSA
end
subgraph CONTRACT ["📝 Contract workflow"]
C[Contracts]
CA[ContractApprovals]
CCM[ContractComments]
@ -201,39 +265,48 @@ flowchart TB
CCS[ContractCodeSequences]
end
subgraph NOTIFY ["🔔 Notification module"]
N[Notifications]
end
U -.Drafter.-> C
S --> C
PR --> C
DE --> C
CT --> C
WD -.pinned at create.-> C
C --> CA
C --> CCM
C --> CAT
C -.gen when DangDongDau.-> CCS
C -.transition event.-> N
CCM -.comment added.-> N
U -.recipient.-> N
```
## 3. Vòng đời 1 HĐ — data changes
## 3. Vòng đời 1 HĐ — data changes (với versioned workflow)
```mermaid
flowchart LR
Create[POST /contracts]
Create --> C1["Contracts INSERT<br/>Phase=2, SLA=+7d"]
Create[POST /contracts type=5]
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 --> 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 --> C3[INSERT ContractComments]
Comment --> C3["INSERT ContractComments<br/>INSERT Notifications"]
Transition2[Transition 3→4→5→6→7]
Transition2 --> C4["UPDATE Phase + SlaDeadline<br/>INSERT ContractApprovals"]
NewVersion[Admin creates QT-MB-v03]
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 --> CG["ContractCodeGenerator<br/>SERIALIZABLE tran<br/>UPSERT ContractCodeSequences"]
CG --> C5["UPDATE Contract<br/>SET MaHopDong='FLOCK 01/HĐGK/SOL&PVL/01',<br/>Phase=8"]
Transition4[Transition 8→9 HRA phát hành]
Transition4 --> C6["UPDATE Phase=9, SlaDeadline=NULL<br/>INSERT ContractApprovals"]
Transition3 --> CG["ContractCodeGenerator SERIALIZABLE<br/>UPSERT ContractCodeSequences"]
CG --> C5["UPDATE Contract<br/>SET MaHopDong, Phase=8<br/>INSERT Notifications"]
```
## 4. Index strategy
@ -245,12 +318,19 @@ flowchart LR
| Contracts | `IX_Contracts_SupplierId` | Filter HĐ theo NCC |
| Contracts | `IX_Contracts_ProjectId` | Filter HĐ theo dự án |
| Contracts | `IX_Contracts_SlaDeadline` | SLA expiry job query |
| Contracts | `IX_Contracts_WorkflowDefinitionId` | Pinned policy lookup |
| ContractApprovals | `IX_ContractApprovals_ContractId_ApprovedAt` | Timeline query theo HĐ |
| ContractComments | `IX_ContractComments_ContractId_CreatedAt` | Thread load |
| ContractAttachments | `IX_ContractAttachments_ContractId` | Attachments load |
| Suppliers/Projects/Departments | `UX_{Table}_Code` | Unique business code |
| Permissions | `UX_Permissions_RoleId_MenuKey` | 1 row / role / menu |
| MenuItems | `IX_MenuItems_ParentKey` | Tree query |
| Notifications | `IX_Notifications_RecipientUserId_IsRead_CreatedAt` | Bell badge unread count + list |
| WorkflowDefinitions | `UX_WorkflowDefinitions_Code_Version` | Unique version per code |
| WorkflowDefinitions | `IX_WorkflowDefinitions_ContractType_IsActive` | Active policy lookup |
| WorkflowSteps | `IX_WorkflowSteps_WorkflowDefinitionId_Order` | Load steps ordered |
| WorkflowStepApprovers | `IX_WorkflowStepApprovers_WorkflowStepId` | Approver load |
| WorkflowTypeAssignments | `UX_WorkflowTypeAssignments_ContractType` | 1 override per type (legacy) |
Chi tiết + cheatsheet SQL: [`database-guide.md`](database-guide.md).
@ -263,9 +343,13 @@ Chi tiết + cheatsheet SQL: [`database-guide.md`](database-guide.md).
| Contract → ContractAttachment | 1 - N | Cascade | File vật lý vẫn còn trong `wwwroot/uploads/`, cleanup riêng |
| Supplier → Contract | 1 - N | Restrict | Không xóa Supplier nếu còn HĐ tham chiếu |
| Project → Contract | 1 - N | Restrict | Tương tự |
| **WorkflowDefinition → Contract** | 1 - N | **Restrict** | **KHÔNG cascade** → HĐ cũ pin version cũ không bị xóa khi admin archive |
| Role → Permission | 1 - N | Cascade | Xóa role → clear permissions |
| MenuItem → Permission | 1 - N | Cascade | — |
| MenuItem → MenuItem (self-ref) | 1 - N | Restrict | Parent-child menu |
| **WorkflowDefinition → WorkflowStep** | 1 - N | Cascade | Delete def → remove steps (chỉ khi no Contract tham chiếu) |
| **WorkflowStep → WorkflowStepApprover** | 1 - N | Cascade | — |
| **User → Notification (RecipientUserId)** | 1 - N | Cascade | — |
## 6. Soft delete behavior
@ -278,47 +362,89 @@ Entity list áp dụng:
- Supplier, Project, Department
- Contract
- ContractTemplate, ContractClause
- WorkflowDefinition (admin archive = `IsActive=false`, xóa logic chỉ khi muốn scrub)
KHÔNG soft delete (cascade hoặc keep):
- ContractApproval, ContractComment, ContractAttachment — cascade khi Contract xóa
- Permission, MenuItem — cascade khi Role xóa
- ContractCodeSequence — không bao giờ xóa (giữ history seq)
- Notifications — không soft delete, chỉ `IsRead` flag (giữ history ngắn hạn, cleanup job sau)
- WorkflowStep / WorkflowStepApprover — cascade khi WorkflowDefinition xóa
- Identity tables (Users, Roles, ...) — Identity không support soft delete built-in
## 7. Truy vấn tiêu biểu
### Inbox HĐ chờ role của tôi
### Inbox HĐ chờ role của tôi (với versioned workflow)
```sql
-- Tương đương GetMyInboxQuery
-- Server filter (API): HĐ chờ role eligible phase theo pinned policy
-- Thực tế ContractsController resolve policy runtime:
-- def = db.WorkflowDefinitions.Include(Steps.Approvers).Where(Id == contract.WorkflowDefinitionId).FirstOrDefault()
-- policy = def != null ? Registry.FromDefinition(def) : Registry.For(contract.Type)
-- phase eligible = policy.Transitions.Where(t => t.From == contract.Phase && t.AllowedRoles.Intersect(myRoles).Any())
-- SQL tương đương:
SELECT c.Id, c.MaHopDong, c.TenHopDong, c.Phase, c.SlaDeadline, s.Name AS SupplierName, p.Name AS ProjectName
FROM Contracts c
INNER JOIN Suppliers s ON c.SupplierId = s.Id
INNER JOIN Projects p ON c.ProjectId = p.Id
WHERE c.IsDeleted = 0
AND c.Phase IN (/* phase eligible cho role hiện tại */)
AND c.Phase IN (/* phase eligible computed theo pinned workflow */)
ORDER BY c.SlaDeadline ASC;
```
### Dashboard stats
### Pick active workflow tại create-time
```sql
SELECT TOP 1 Id
FROM WorkflowDefinitions
WHERE ContractType = @Type AND IsActive = 1
ORDER BY [Version] DESC;
```
### Tạo version mới (atomic)
```sql
BEGIN TRAN;
-- Step 1: deactivate current active
UPDATE WorkflowDefinitions
SET IsActive = 0
WHERE ContractType = @Type AND IsActive = 1;
-- Step 2: compute next version
DECLARE @NextVersion INT = (SELECT ISNULL(MAX([Version]), 0) + 1 FROM WorkflowDefinitions WHERE Code = @Code);
-- Step 3: insert new active version
INSERT WorkflowDefinitions (Id, Code, [Version], IsActive, ContractType, Name, Description, CreatedAt, CreatedBy)
VALUES (NEWID(), @Code, @NextVersion, 1, @Type, @Name, @Description, GETUTCDATE(), @UserId);
-- Step 4: insert steps + approvers (batch)
-- ...
COMMIT;
```
### Dashboard stats (MyDashboard user-specific)
```sql
-- Tổng + active + overdue
SELECT
(SELECT COUNT(*) FROM Contracts WHERE IsDeleted = 0) AS Total,
(SELECT COUNT(*) FROM Contracts WHERE IsDeleted = 0 AND Phase NOT IN (9, 99)) AS Active,
(SELECT COUNT(*) FROM Contracts WHERE IsDeleted = 0 AND Phase NOT IN (9, 99) AND SlaDeadline < GETUTCDATE()) AS Overdue;
(SELECT COUNT(*) FROM Contracts
WHERE DrafterUserId = @Me AND IsDeleted = 0 AND Phase NOT IN (9, 99)) AS DraftsInProgress,
(SELECT COUNT(*) FROM Contracts
WHERE Phase IN (/* eligible phases cho role tôi */) AND IsDeleted = 0) AS PendingMyApproval,
(SELECT COUNT(*) FROM Contracts
WHERE IsDeleted = 0 AND SlaDeadline BETWEEN GETUTCDATE() AND DATEADD(DAY, 2, GETUTCDATE())) AS DueSoon,
(SELECT COUNT(*) FROM Contracts
WHERE IsDeleted = 0 AND SlaDeadline < GETUTCDATE() AND Phase NOT IN (9, 99)) AS Overdue,
(SELECT ISNULL(SUM(GiaTri), 0) FROM Contracts
WHERE DrafterUserId = @Me AND Phase = 2) AS DraftsTotalValue;
```
-- By phase
SELECT Phase, COUNT(*) FROM Contracts WHERE IsDeleted = 0 GROUP BY Phase;
### Notifications unread count (bell badge)
-- Top 5 NCC
SELECT TOP 5 c.SupplierId, s.Name, COUNT(*) AS Cnt, SUM(c.GiaTri) AS TotalValue
FROM Contracts c
INNER JOIN Suppliers s ON c.SupplierId = s.Id
WHERE c.IsDeleted = 0
GROUP BY c.SupplierId, s.Name
ORDER BY COUNT(*) DESC;
```sql
SELECT COUNT(*) FROM Notifications
WHERE RecipientUserId = @Me AND IsRead = 0;
```
### Gen mã HĐ atomic
@ -327,32 +453,63 @@ ORDER BY COUNT(*) DESC;
BEGIN TRAN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
MERGE ContractCodeSequences AS tgt
USING (SELECT @Prefix AS Prefix) AS src ON tgt.Prefix = src.Prefix
WHEN MATCHED THEN UPDATE SET LastSeq = LastSeq + 1, UpdatedAt = GETUTCDATE()
WHEN NOT MATCHED THEN INSERT (Prefix, LastSeq, UpdatedAt) VALUES (@Prefix, 1, GETUTCDATE());
UPDATE ContractCodeSequences
SET LastSeq = LastSeq + 1, UpdatedAt = GETUTCDATE()
WHERE Prefix = @Prefix;
IF @@ROWCOUNT = 0
INSERT ContractCodeSequences (Prefix, LastSeq, UpdatedAt)
VALUES (@Prefix, 1, GETUTCDATE());
SELECT LastSeq FROM ContractCodeSequences WHERE Prefix = @Prefix;
COMMIT;
```
(EF Core impl dùng `UPDATE + IF ROWCOUNT = 0 INSERT` thay MERGE — tương đương nhưng an toàn hơn với SERIALIZABLE).
## 8. Migration lịch sử
| # | Migration | Tables added |
| # | Migration | Tables added / changed |
|---|---|---|
| 1 | `Init` | 7 Identity tables |
| 2 | `AddMasterData` | Suppliers, Projects, Departments |
| 3 | `AddPermissions` | MenuItems, Permissions |
| 4 | `AddForms` | ContractTemplates, ContractClauses |
| 5 | `AddContractsWorkflow` | Contracts, ContractApprovals, ContractComments, ContractAttachments, ContractCodeSequences |
| 6 | `AddNotifications` | Notifications |
| 7 | `AddWorkflowTypeAssignments` | WorkflowTypeAssignments (admin override legacy) |
| 8 | `AddVersionedWorkflows` | WorkflowDefinitions, WorkflowSteps, WorkflowStepApprovers + Contracts.WorkflowDefinitionId FK |
Tổng: **19 bảng** (+ `__EFMigrationsHistory` hệ thống).
Tổng: **24 bảng** (+ `__EFMigrationsHistory` hệ thống).
## 9. Liên quan
## 9. Versioned workflow invariants
```
1. UNIQUE (WorkflowDefinitions.Code, Version)
→ không 2 row cùng Code + Version (enforce qua IX unique)
2. Chỉ 1 WorkflowDefinition.IsActive = true per ContractType tại 1 thời điểm
→ enforce qua CreateWorkflowDefinitionCommand: UPDATE deactivate trước INSERT, cùng transaction
3. Contract.WorkflowDefinitionId pinned at create → không update sau đó
→ CreateContractCommandHandler pick active version 1 lần, save
4. ON DELETE Restrict FK Contract.WorkflowDefinitionId → WorkflowDefinitions.Id
→ admin không thể DELETE WorkflowDefinition nếu còn Contract pin
→ admin archive = set IsActive=false thôi, row vẫn tồn tại
5. Runtime policy resolution order (ContractWorkflowService):
a. If contract.WorkflowDefinitionId NOT NULL → load def → FromDefinition
b. Else if admin override ở WorkflowTypeAssignments for contract.Type → Registry.ByName
c. Else → Registry.For(contract.Type) (hardcoded Standard/SkipCcm)
6. WorkflowStepApprover.Kind
- 1=Role: AssignmentValue là RoleName (Domain/Identity/AppRoles constants)
- 2=User: AssignmentValue là UserId Guid string
- Runtime guard hiện tại chỉ dùng Role-kind (User-kind data model ready, enable iter sau)
```
## 10. Liên quan
- [`database-guide.md`](database-guide.md) — conventions + migration workflow + cheatsheet đầy đủ
- [`../architecture.md`](../architecture.md) — layered architecture + data flow
- [`../workflow-contract.md`](../workflow-contract.md) — state machine spec
- [`../workflow-contract.md`](../workflow-contract.md) — state machine spec + versioned
- [`../flows/`](../flows/) — sequence diagrams