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>
45 lines
1.5 KiB
TypeScript
45 lines
1.5 KiB
TypeScript
import { cn } from '@/lib/cn'
|
|
|
|
type BarDatum = {
|
|
label: string
|
|
value: number
|
|
sublabel?: string
|
|
}
|
|
|
|
type Props = {
|
|
data: BarDatum[]
|
|
maxBars?: number
|
|
formatValue?: (v: number) => string
|
|
barClassName?: string
|
|
}
|
|
|
|
// Horizontal bar chart using plain Tailwind — không cần thư viện ngoài.
|
|
export function BarChart({ data, maxBars = 12, formatValue = v => v.toLocaleString('vi-VN'), barClassName }: Props) {
|
|
const shown = data.slice(0, maxBars)
|
|
const max = Math.max(1, ...shown.map(d => d.value))
|
|
|
|
return (
|
|
<div className="space-y-1.5">
|
|
{shown.length === 0 && <div className="py-6 text-center text-sm text-slate-400">Chưa có dữ liệu</div>}
|
|
{shown.map((d, i) => {
|
|
const pct = (d.value / max) * 100
|
|
return (
|
|
<div key={i} className="group">
|
|
<div className="flex items-center justify-between text-xs text-slate-600">
|
|
<span className="truncate pr-2">{d.label}</span>
|
|
<span className="font-mono tabular-nums">{formatValue(d.value)}</span>
|
|
</div>
|
|
<div className="h-2 rounded-full bg-slate-100">
|
|
<div
|
|
className={cn('h-full rounded-full bg-brand-500 transition-all group-hover:bg-brand-600', barClassName)}
|
|
style={{ width: `${pct}%` }}
|
|
/>
|
|
</div>
|
|
{d.sublabel && <div className="mt-0.5 text-[10px] text-slate-400">{d.sublabel}</div>}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|