Files
solution-erp/docs/database/schema-diagram.md
pqhuy1987 fe7ad8e4a3 [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>
2026-04-21 12:42:46 +07:00

11 KiB

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

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)

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

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.

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.DeletedModified, 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

-- 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

-- 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

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