Backend Report: - Application/Reports/Dtos/DashboardStatsDto: 5 KPI + PhaseCount + SupplierCount + ProjectCount + MonthlyValue - Application/Reports/Queries/GetDashboardStats handler: total/active/overdue/published this month/totalValueActive + byPhase + top 5 NCC/du an + 12 thang monthly (fill zero khi thang empty) - Application/Reports/Services/IContractExcelExporter interface - Infrastructure/Reports/ContractExcelExporter: ClosedXML workbook 10 cot, header style bold+blue, number format #,##0, formula SUM, auto-fit, freeze header - Application/Reports/Commands/ExportContractsToExcelCommand: filter phase/supplier/project/date range - Api/Controllers/ReportsController: GET /reports/dashboard, GET /reports/contracts/export - DI register IContractExcelExporter (Scoped) Frontend fe-admin: - types/reports.ts: DashboardStats type - components/BarChart.tsx: generic horizontal bar chart — chi Tailwind, khong thu vien ngoai - pages/DashboardPage.tsx REWRITE: 5 KPI card (FileText/TrendingUp/AlertTriangle/CheckCircle2/Coins) + by-phase bar + monthly 12-month chart + top 5 NCC + top 5 du an + skeleton loader - pages/ReportsPage.tsx MOI: filter phase/fromDate/toDate → export Excel button - Route /reports vao App.tsx E2E verified: - GET /api/reports/dashboard → 200 voi day du KPI + monthly fill 12 thang - GET /api/reports/contracts/export → 200 xlsx 7229 bytes (Microsoft Excel 2007+) Docs consolidation (theo yeu cau user): - docs/rules.md MOI: 9 section coding conventions (ngon ngu UI/code/DB/docs, BE Clean Arch, CQRS+MediatR, Validation FluentValidation, Error handling, Async, Entity rules, DI, Package pinning, FE React/TS erasableSyntaxOnly, path alias, TanStack Query, Permission guard, Toast+error, DB convention, Git commit format, Docs structure, Testing, Security) - docs/architecture.md MOI: layered overview ASCII art, request lifecycle (1 POST/api/contracts qua 10 step), workflow state machine 9 phase, permission model, data flow sequence diagram 4 actor (Drafter/Manager/CCM/BOD/HRA), deployment architecture Phase 5, skill library, non-functional table - docs/database/schema-diagram.md MOI: full ERD 19 table mermaid + data flow diagram + vong doi 1 HD (create → 7 transition → gen ma → publish) + index strategy table + relationship cardinality + soft delete behavior + SQL queries (inbox/dashboard/gen ma) + migration history - docs/gotchas.md UPDATE: 17 → 26 pitfalls, them section "Claude Code harness quirks" (Edit File not read, DI build pass nhung runtime fail) + "Contract workflow" (ma HD gen 2 lan, BE-FE NEXT_PHASES sync, race condition) + "Permission matrix" (cache real-time, MenuKey typo) - docs/STATUS.md: Phase 4 MVP done, docs entry points section liet ke het, next Phase 5 Production - docs/HANDOFF.md: phase table them Phase 4 row, file tree update voi Reports, test points day du, git state commit 7 - docs/changelog/migration-todos.md: tick Phase 4 MVP items + them iteration 2 list - docs/changelog/sessions/2026-04-21-1430-phase4-report.md: session log voi thong so cumulative (BE 3100 LOC, 30 docs) - CLAUDE.md root: update Tai lieu quan trong section them rules.md, architecture.md, schema-diagram.md, .claude/skills (13 links now) Bug fix: - TS unused import ContractPhaseLabel trong DashboardPage - DI thieu register IContractExcelExporter — build pass but runtime would fail (added) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
359 lines
11 KiB
Markdown
359 lines
11 KiB
Markdown
# Schema Diagram — Luồng DB SOLUTION_ERP
|
|
|
|
> ERD đầy đủ + mối quan hệ 19 table sau Phase 3. 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"
|
|
|
|
Contracts ||--o{ ContractApprovals : "history"
|
|
Contracts ||--o{ ContractComments : "thread"
|
|
Contracts ||--o{ ContractAttachments : "files"
|
|
Users ||--o{ ContractApprovals : "approved-by"
|
|
Users ||--o{ ContractComments : "author"
|
|
|
|
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
|
|
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
|
|
}
|
|
```
|
|
|
|
## 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)"]
|
|
S[Suppliers]
|
|
PR[Projects]
|
|
DE[Departments]
|
|
end
|
|
|
|
subgraph FORMS ["📄 Form templates (seed)"]
|
|
CT[ContractTemplates]
|
|
CC[ContractClauses]
|
|
end
|
|
|
|
subgraph CONTRACT ["📝 Contract workflow (Phase 3 core)"]
|
|
C[Contracts]
|
|
CA[ContractApprovals]
|
|
CCM[ContractComments]
|
|
CAT[ContractAttachments]
|
|
CCS[ContractCodeSequences]
|
|
end
|
|
|
|
U -.Drafter.-> C
|
|
S --> C
|
|
PR --> C
|
|
DE --> C
|
|
CT --> C
|
|
C --> CA
|
|
C --> CCM
|
|
C --> CAT
|
|
C -.gen when DangDongDau.-> CCS
|
|
```
|
|
|
|
## 3. Vòng đời 1 HĐ — data changes
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
Create[POST /contracts]
|
|
Create --> C1["Contracts INSERT<br/>Phase=2, SLA=+7d"]
|
|
|
|
Transition1[Transition 2→3]
|
|
Transition1 --> C2["UPDATE Phase=3<br/>INSERT ContractApprovals"]
|
|
|
|
Comment[POST /comments]
|
|
Comment --> C3[INSERT ContractComments]
|
|
|
|
Transition2[Transition 3→4→5→6→7]
|
|
Transition2 --> C4["UPDATE Phase + SlaDeadline<br/>INSERT ContractApprovals"]
|
|
|
|
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"]
|
|
```
|
|
|
|
## 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 |
|
|
| 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 |
|
|
|
|
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ự |
|
|
| 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 |
|
|
|
|
## 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
|
|
|
|
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)
|
|
- 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
|
|
|
|
```sql
|
|
-- Tương đương GetMyInboxQuery
|
|
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 */)
|
|
ORDER BY c.SlaDeadline ASC;
|
|
```
|
|
|
|
### Dashboard stats
|
|
|
|
```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;
|
|
|
|
-- By phase
|
|
SELECT Phase, COUNT(*) FROM Contracts WHERE IsDeleted = 0 GROUP BY Phase;
|
|
|
|
-- 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;
|
|
```
|
|
|
|
### Gen mã HĐ atomic
|
|
|
|
```sql
|
|
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());
|
|
|
|
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 |
|
|
|---|---|---|
|
|
| 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 |
|
|
|
|
Tổng: **19 bảng** (+ `__EFMigrationsHistory` hệ thống).
|
|
|
|
## 9. 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
|
|
- [`../flows/`](../flows/) — sequence diagrams
|