[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,92 @@
import { useState } from 'react'
import { useMutation } from '@tanstack/react-query'
import { Download, FileSpreadsheet } from 'lucide-react'
import { toast } from 'sonner'
import { PageHeader } from '@/components/PageHeader'
import { Button } from '@/components/ui/Button'
import { Label } from '@/components/ui/Label'
import { Select } from '@/components/ui/Select'
import { Input } from '@/components/ui/Input'
import { api } from '@/lib/api'
import { getErrorMessage } from '@/lib/apiError'
import { ContractPhase, ContractPhaseLabel } from '@/types/contracts'
export function ReportsPage() {
const [phase, setPhase] = useState('')
const [fromDate, setFromDate] = useState('')
const [toDate, setToDate] = useState('')
const exportMut = useMutation({
mutationFn: async () => {
const params: Record<string, string> = {}
if (phase) params.phase = phase
if (fromDate) params.fromDate = fromDate
if (toDate) params.toDate = toDate
const res = await api.get('/reports/contracts/export', { params, responseType: 'blob' })
const filename = res.headers['content-disposition']?.match(/filename="?([^";]+)"?/)?.[1] ?? 'contracts.xlsx'
return { blob: res.data as Blob, filename }
},
onSuccess: ({ blob, filename }) => {
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
a.click()
URL.revokeObjectURL(url)
toast.success('Đã tải file')
},
onError: err => toast.error(getErrorMessage(err)),
})
return (
<div className="p-6">
<PageHeader
title="Báo cáo"
description="Xuất danh sách HĐ ra Excel theo bộ lọc."
/>
<div className="max-w-2xl rounded-lg border border-slate-200 bg-white p-6">
<h2 className="mb-4 flex items-center gap-2 text-sm font-semibold text-slate-700">
<FileSpreadsheet className="h-4 w-4" />
Xuất danh sách (.xlsx)
</h2>
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2 space-y-1.5">
<Label>Phase</Label>
<Select value={phase} onChange={e => setPhase(e.target.value)}>
<option value="">Tất cả phase</option>
{Object.values(ContractPhase).map(p => (
<option key={p} value={p}>{ContractPhaseLabel[p]}</option>
))}
</Select>
</div>
<div className="space-y-1.5">
<Label>Từ ngày</Label>
<Input type="date" value={fromDate} onChange={e => setFromDate(e.target.value)} />
</div>
<div className="space-y-1.5">
<Label>Đến ngày</Label>
<Input type="date" value={toDate} onChange={e => setToDate(e.target.value)} />
</div>
</div>
<div className="mt-6 flex gap-2">
<Button onClick={() => exportMut.mutate()} disabled={exportMut.isPending}>
<Download className="h-4 w-4" />
{exportMut.isPending ? 'Đang xuất…' : 'Tải Excel'}
</Button>
</div>
</div>
<div className="mt-6 max-w-2xl rounded-lg border border-dashed border-slate-300 bg-slate-50 p-5 text-sm text-slate-500">
<strong className="text-slate-700">Báo cáo khác (Phase 4 iteration 2):</strong>
<ul className="mt-2 list-disc space-y-1 pl-5">
<li> quá hạn SLA theo phase / role</li>
<li>Biểu đ giá trị theo tháng / dự án</li>
<li>Export Approvals history</li>
<li>Export từng ra PDF</li>
</ul>
</div>
</div>
)
}