All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m47s
11 commit feature work (b75448e→ae59cfe) → docs sync. ## STATUS.md - Last updated 2026-04-23 16:00 - Phase header: Tier 3 + 4-bảng + master catalogs + roles VN - 7 Recently Done row mới (3-panel layout, 4-bảng overhaul, Thao tác 2-panel, Mã HĐ gen Create, master catalogs, roles VN demo users) - Cumulative table thêm cột "+Toolkit/Catalogs/Roles" (DB 36, endpoints ~80, migrations 11, commits ~47) - Session log link mới - Skill list count = 13 file ## HANDOFF.md - TL;DR cập nhật: 36 tables, 80 endpoints, 11 migrations - Phase table thêm 5 row Done (3-panel, 4-bảng, mã HĐ, master, roles) - Git state 18 commit gần nhất - Credentials block thêm 13 demo user (User@123456) — warn rotate trước UAT ## migration-todos.md Section "Session 2026-04-23 (chiều)" với 14 ticked checkbox + commit refs. ## schema-diagram.md - Header: 24 → 36 bảng - Migration table thêm row 9-11 (highlighted) - Section 8bis mới: chi tiết 7 Details + ContractChangelogs + 4 Catalogs + Role.ShortName + User.DepartmentId/Position ## Session log mới `docs/changelog/sessions/2026-04-23-1500-toolkit-data-roles.md` (~270 dòng) — outcome A→I, stats cumulative, 6 architectural decisions, next session priority. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
575 lines
20 KiB
Markdown
575 lines
20 KiB
Markdown
# Schema Diagram — Luồng DB SOLUTION_ERP
|
|
|
|
> ERD đầy đủ + mối quan hệ **36 table** sau Migration 11 (Tier 3 + 4-bảng overhaul + 4 master catalogs + Role/User VN). Mermaid render ở VS Code / GitHub / Gitea.
|
|
|
|
## 1. Full ERD
|
|
|
|
```mermaid
|
|
erDiagram
|
|
Users ||--o{ UserRoles : "has"
|
|
Roles ||--o{ UserRoles : "assigned to"
|
|
Users ||--o{ UserClaims : "has"
|
|
Users ||--o{ UserLogins : "has"
|
|
Users ||--o{ UserTokens : "has"
|
|
Roles ||--o{ RoleClaims : "has"
|
|
|
|
Roles ||--o{ Permissions : "grants"
|
|
MenuItems ||--o{ Permissions : "controlled by"
|
|
MenuItems ||--o{ MenuItems : "parent-of"
|
|
|
|
Suppliers ||--o{ Contracts : "party-B"
|
|
Projects ||--o{ Contracts : "belongs-to"
|
|
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"
|
|
Contracts ||--o{ ContractAttachments : "files"
|
|
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"
|
|
nvarchar Email "UK"
|
|
nvarchar PasswordHash
|
|
nvarchar RefreshToken "512"
|
|
datetime2 RefreshTokenExpiresAt
|
|
bit IsActive
|
|
datetime2 CreatedAt
|
|
}
|
|
|
|
Roles {
|
|
uniqueidentifier Id PK
|
|
nvarchar Name "UK"
|
|
nvarchar Description "500"
|
|
datetime2 CreatedAt
|
|
}
|
|
|
|
MenuItems {
|
|
nvarchar Key PK "50"
|
|
nvarchar Label "200"
|
|
nvarchar ParentKey FK "NULL if root"
|
|
int Order
|
|
nvarchar Icon "50"
|
|
}
|
|
|
|
Permissions {
|
|
uniqueidentifier Id PK
|
|
uniqueidentifier RoleId FK
|
|
nvarchar MenuKey FK "50"
|
|
bit CanRead
|
|
bit CanCreate
|
|
bit CanUpdate
|
|
bit CanDelete
|
|
}
|
|
|
|
Suppliers {
|
|
uniqueidentifier Id PK
|
|
nvarchar Code "UK 50"
|
|
nvarchar Name "200"
|
|
int Type "NCC/NTP/TD/DVDV/CDT"
|
|
nvarchar TaxCode "20"
|
|
nvarchar Phone "30"
|
|
nvarchar Email "100"
|
|
nvarchar Address "500"
|
|
bit IsDeleted
|
|
}
|
|
|
|
Projects {
|
|
uniqueidentifier Id PK
|
|
nvarchar Code "UK 50"
|
|
nvarchar Name "200"
|
|
date StartDate
|
|
date EndDate
|
|
uniqueidentifier ManagerUserId FK
|
|
decimal BudgetTotal "18,2"
|
|
}
|
|
|
|
Departments {
|
|
uniqueidentifier Id PK
|
|
nvarchar Code "UK 50"
|
|
nvarchar Name "200"
|
|
uniqueidentifier ManagerUserId FK
|
|
}
|
|
|
|
ContractTemplates {
|
|
uniqueidentifier Id PK
|
|
nvarchar FormCode "UK 50"
|
|
nvarchar Name "200"
|
|
int ContractType "nullable"
|
|
nvarchar FileName "255"
|
|
nvarchar StoragePath "500"
|
|
nvarchar Format "docx/xlsx"
|
|
nvarchar FieldSpec "max JSON"
|
|
bit IsActive
|
|
}
|
|
|
|
ContractClauses {
|
|
uniqueidentifier Id PK
|
|
nvarchar Code "UK 50"
|
|
nvarchar Name "200"
|
|
nvarchar Content "max rich text"
|
|
int Version
|
|
bit IsActive
|
|
}
|
|
|
|
Contracts {
|
|
uniqueidentifier Id PK
|
|
nvarchar MaHopDong "UK filtered 100"
|
|
nvarchar TenHopDong "500"
|
|
int Type "ContractType enum"
|
|
int Phase "9 state"
|
|
uniqueidentifier SupplierId FK
|
|
uniqueidentifier ProjectId FK
|
|
uniqueidentifier DepartmentId FK
|
|
uniqueidentifier DrafterUserId FK
|
|
uniqueidentifier TemplateId FK
|
|
uniqueidentifier WorkflowDefinitionId FK "pinned policy, nullable"
|
|
decimal GiaTri "18,2"
|
|
bit BypassProcurementAndCCM
|
|
datetime2 SlaDeadline
|
|
bit SlaWarningSent
|
|
nvarchar DraftData "max JSON"
|
|
bit IsDeleted
|
|
}
|
|
|
|
ContractApprovals {
|
|
uniqueidentifier Id PK
|
|
uniqueidentifier ContractId FK
|
|
int FromPhase
|
|
int ToPhase
|
|
uniqueidentifier ApproverUserId FK "NULL=system"
|
|
int Decision "Pending/Approve/Reject/AutoApprove"
|
|
nvarchar Comment "1000"
|
|
datetime2 ApprovedAt
|
|
}
|
|
|
|
ContractComments {
|
|
uniqueidentifier Id PK
|
|
uniqueidentifier ContractId FK
|
|
uniqueidentifier UserId FK
|
|
int Phase "phase luc comment"
|
|
nvarchar Content "2000"
|
|
datetime2 CreatedAt
|
|
}
|
|
|
|
ContractAttachments {
|
|
uniqueidentifier Id PK
|
|
uniqueidentifier ContractId FK
|
|
nvarchar FileName "255"
|
|
nvarchar StoragePath "500"
|
|
bigint FileSize
|
|
nvarchar ContentType "100"
|
|
int Purpose "DraftExport/ScannedSigned/SealedCopy"
|
|
nvarchar Note "500"
|
|
}
|
|
|
|
ContractCodeSequences {
|
|
nvarchar Prefix PK "200"
|
|
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)
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
subgraph IDENTITY ["🔐 Identity domain (seed lần đầu)"]
|
|
U[Users] -->|N-M| R[Roles]
|
|
R --> P[Permissions]
|
|
P --> MI[MenuItems]
|
|
end
|
|
|
|
subgraph MASTER ["📋 Master data (admin CRUD + seed demo)"]
|
|
S[Suppliers]
|
|
PR[Projects]
|
|
DE[Departments]
|
|
end
|
|
|
|
subgraph FORMS ["📄 Form templates (admin upload)"]
|
|
CT[ContractTemplates]
|
|
CC[ContractClauses]
|
|
end
|
|
|
|
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]
|
|
CAT[ContractAttachments]
|
|
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 (với versioned workflow)
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
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 --> 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<br/>INSERT Notifications"]
|
|
|
|
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 SERIALIZABLE<br/>UPSERT ContractCodeSequences"]
|
|
CG --> C5["UPDATE Contract<br/>SET MaHopDong, Phase=8<br/>INSERT Notifications"]
|
|
```
|
|
|
|
## 4. Index strategy
|
|
|
|
| Table | Index | Purpose |
|
|
|---|---|---|
|
|
| Contracts | `UX_Contracts_MaHopDong` filtered `WHERE [MaHopDong] IS NOT NULL` | Unique mã HĐ |
|
|
| Contracts | `IX_Contracts_Phase_IsDeleted` | Inbox query theo phase |
|
|
| 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).
|
|
|
|
## 5. Relationship cardinality
|
|
|
|
| Parent → Child | Cardinality | OnDelete | Ghi chú |
|
|
|---|---|---|---|
|
|
| Contract → ContractApproval | 1 - N | Cascade | Xóa HĐ → xóa lịch sử (nhưng mức delete bị chặn sau DangInKy) |
|
|
| Contract → ContractComment | 1 - N | Cascade | — |
|
|
| 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
|
|
|
|
Mọi entity extend `AuditableEntity`:
|
|
- Delete qua `db.X.Remove(entity)` → `AuditingInterceptor` chuyển `EntityState.Deleted` → `Modified`, set `IsDeleted=true, DeletedAt, DeletedBy`
|
|
- Query filter `HasQueryFilter(x => !x.IsDeleted)` tự động filter ra
|
|
- Để query bao gồm soft-deleted: `.IgnoreQueryFilters()`
|
|
|
|
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 (với versioned workflow)
|
|
|
|
```sql
|
|
-- 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 computed theo pinned workflow */)
|
|
ORDER BY c.SlaDeadline ASC;
|
|
```
|
|
|
|
### 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
|
|
SELECT
|
|
(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;
|
|
```
|
|
|
|
### Notifications unread count (bell badge)
|
|
|
|
```sql
|
|
SELECT COUNT(*) FROM Notifications
|
|
WHERE RecipientUserId = @Me AND IsRead = 0;
|
|
```
|
|
|
|
### Gen mã HĐ atomic
|
|
|
|
```sql
|
|
BEGIN TRAN;
|
|
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
|
|
|
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;
|
|
```
|
|
|
|
## 8. Migration lịch sử
|
|
|
|
| # | 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 |
|
|
| **9** | **`AddContractDetailsAndChangelog`** | **7 ContractType-specific Details + ContractChangelogs (unified audit log)** |
|
|
| **10** | **`AddMasterCatalogs`** | **UnitsOfMeasure, MaterialItems, ServiceItems, WorkItems** |
|
|
| **11** | **`AddRoleShortNameAndUserDepartment`** | **+Role.ShortName + User.DepartmentId/Position (cột thêm, không bảng mới)** |
|
|
|
|
Tổng: **36 bảng** (+ `__EFMigrationsHistory` hệ thống).
|
|
|
|
## 8bis. Bảng mới sau Migration 9-11
|
|
|
|
### Per-type Details (7 bảng — Migration 9)
|
|
|
|
Mỗi loại HĐ có bảng riêng. Common base: `Id`, `ContractId` FK Cascade,
|
|
`Order` int, `ThanhTien` decimal(18,2), `GhiChu` nvarchar(1000) +
|
|
`AuditableEntity` (CreatedAt/By/UpdatedAt/By).
|
|
|
|
| Bảng | Loại HĐ (Type) | Field đặc trưng |
|
|
|---|---|---|
|
|
| `ThauPhuDetails` | 1 | HangMuc, DonViTinh, KhoiLuong, DonGia, ThoiGianHoanThanh |
|
|
| `GiaoKhoanDetails` | 2 | MaCongViec, TenCongViec, KhoiLuong, DonGia, YeuCauKyThuat |
|
|
| `NhaCungCapDetails` | 3 | MaSP, TenSP, ThongSoKyThuat, SoLuong, DonGia, ThoiGianGiao, XuatXu |
|
|
| `DichVuDetails` | 4 | MaDichVu, TenDichVu, ThoiGian, DonGia, TuNgay/DenNgay |
|
|
| `MuaBanDetails` | 5 | MaSP, TenSP, SoLuong, DonGia, **ThueVAT** decimal(5,2), XuatXu |
|
|
| `NguyenTacNccDetails` | 6 | NhomSP, TenSP, **DonGiaToiThieu/ToiDa** (range), DieuKien |
|
|
| `NguyenTacDvDetails` | 7 | LoaiDichVu, TenDichVu, DonGiaToiThieu/ToiDa, PhamViDichVu, SLA |
|
|
|
|
### ContractChangelogs (Migration 9)
|
|
|
|
Unified audit log cho mọi entity HĐ:
|
|
- `Id`, `ContractId` FK Cascade
|
|
- `EntityType` int enum: 1=Contract, 2=Detail, 3=Workflow, 4=Comment, 5=Attachment
|
|
- `EntityId` Guid? (PK của child entity, null nếu là Header)
|
|
- `Action` int enum: 1=Insert, 2=Update, 3=Delete, 4=Transition
|
|
- `PhaseAtChange` int? (snapshot Phase tại thời điểm change)
|
|
- `UserId` Guid? + `UserName` nvarchar(200) (denormalize cho readable)
|
|
- `Summary` nvarchar(500), `FieldChangesJson` nvarchar(max), `ContextNote` nvarchar(2000)
|
|
- Indexes: (ContractId, CreatedAt), (ContractId, EntityType)
|
|
|
|
### 4 Master Catalogs (Migration 10)
|
|
|
|
Phục vụ autocomplete trong Details add form (FE HTML5 datalist).
|
|
|
|
| Bảng | Field | Dùng cho HĐ Detail |
|
|
|---|---|---|
|
|
| `UnitsOfMeasure` | Code, Name, Description | Tất cả 7 type (donViTinh) |
|
|
| `MaterialItems` | Code, Name, Category, DefaultUnit, Specification, OriginCountry, IsActive | NCC + Mua bán + Nguyên tắc NCC |
|
|
| `ServiceItems` | Code, Name, Category, DefaultUnit, Description, IsActive | Dịch vụ + Nguyên tắc DV |
|
|
| `WorkItems` | Code, Name, Category, DefaultUnit, Description, IsActive | Thầu phụ + Giao khoán |
|
|
|
|
Common: `AuditableEntity`, `IX_<Table>_Code` UNIQUE filtered `IsDeleted=0`,
|
|
`IX_<Table>_Category`, `HasQueryFilter !IsDeleted`.
|
|
|
|
### Identity columns thêm (Migration 11)
|
|
|
|
**Role:**
|
|
- `ShortName` nvarchar(50) — Mã viết tắt VN (QTV/BOD/CCM/PRO/FIN/...)
|
|
- `Description` đã có — full Vietnamese label
|
|
|
|
**User:**
|
|
- `DepartmentId` Guid? FK Departments **OnDelete Restrict** (không xóa
|
|
dept nếu còn user reference)
|
|
- `Position` nvarchar(200) — chức vụ free text
|
|
- `IX_Users_DepartmentId`
|
|
|
|
## 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 + versioned
|
|
- [`../flows/`](../flows/) — sequence diagrams
|