[CLAUDE] Phase4: Report MVP + Docs Consolidation (rules, architecture, schema-diagram)

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>
This commit is contained in:
pqhuy1987
2026-04-21 12:42:46 +07:00
parent 7e957a7654
commit fe7ad8e4a3
21 changed files with 1817 additions and 212 deletions

View File

@ -0,0 +1,358 @@
# 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