Files
goodgo-platform/apps/web/components/reports/report-chart.tsx
Ho Ngoc Hai 25f415f3bc
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 21s
CI / E2E Tests (push) Has been skipped
Deploy / Build API Image (push) Failing after 3m40s
Deploy / Build Web Image (push) Failing after 15s
Deploy / Build AI Services Image (push) Failing after 16s
E2E Tests / Playwright E2E (push) Failing after 2m3s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 23m49s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 16s
Security Scanning / Trivy Scan — API Image (push) Failing after 1m24s
Security Scanning / Trivy Scan — Web Image (push) Failing after 34s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 22s
Security Scanning / Trivy Filesystem Scan (push) Failing after 18s
Security Scanning / Security Gate (push) Failing after 1s
test(reports): add unit tests for report handlers and domain entity
Add tests for GenerateReport, GetReport, DeleteReport command/query
handlers and Report entity domain logic.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-16 09:18:32 +07:00

153 lines
3.9 KiB
TypeScript

'use client';
import * as React from 'react';
import {
Area,
AreaChart,
Bar,
BarChart,
CartesianGrid,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
interface DataPoint {
period: string;
value: number;
unit: string;
}
interface ReportChartProps {
data: DataPoint[];
title: string;
variant?: 'area' | 'bar';
color?: string;
height?: number;
}
const COLORS = {
primary: '#2563eb',
secondary: '#16a34a',
accent: '#d97706',
};
function formatValue(value: number, unit: string): string {
if (unit === 'tỷ USD' || unit === 'tỷ VND') {
return `${value.toLocaleString('vi-VN')} ${unit}`;
}
if (unit === '%') {
return `${value}%`;
}
if (unit === 'người' || unit === 'triệu người') {
return value.toLocaleString('vi-VN');
}
return `${value.toLocaleString('vi-VN')} ${unit}`;
}
export function ReportChart({
data,
title,
variant = 'area',
color = COLORS.primary,
height = 240,
}: ReportChartProps) {
if (!data || data.length === 0) return null;
const unit = data[0]?.unit ?? '';
const chartData = data.map((d) => ({
name: d.period,
value: d.value,
}));
return (
<div className="rounded-lg border bg-card p-4">
<h4 className="mb-3 text-sm font-medium text-muted-foreground">{title}</h4>
<ResponsiveContainer width="100%" height={height}>
{variant === 'bar' ? (
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis dataKey="name" tick={{ fontSize: 11 }} />
<YAxis tick={{ fontSize: 11 }} />
<Tooltip
formatter={(value) => [formatValue(Number(value), unit), title]}
contentStyle={{ fontSize: 12 }}
/>
<Bar dataKey="value" fill={color} radius={[4, 4, 0, 0]} />
</BarChart>
) : (
<AreaChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis dataKey="name" tick={{ fontSize: 11 }} />
<YAxis tick={{ fontSize: 11 }} />
<Tooltip
formatter={(value) => [formatValue(Number(value), unit), title]}
contentStyle={{ fontSize: 12 }}
/>
<Area
type="monotone"
dataKey="value"
stroke={color}
fill={color}
fillOpacity={0.1}
strokeWidth={2}
/>
</AreaChart>
)}
</ResponsiveContainer>
</div>
);
}
interface ReportChartsGridProps {
charts: Record<string, DataPoint[]>;
labels?: Record<string, string>;
}
const DEFAULT_CHART_LABELS: Record<string, string> = {
gdp_trend: 'GDP',
fdi_trend: 'Vốn FDI',
population: 'Dân số',
urbanization: 'Đô thị hóa',
labor_force: 'Lực lượng lao động',
avg_wage: 'Lương bình quân',
industrial_output: 'Sản lượng công nghiệp',
cpi: 'Chỉ số giá tiêu dùng',
mortgage_rate: 'Lãi suất vay',
};
const CHART_COLORS: Record<string, string> = {
gdp_trend: COLORS.primary,
fdi_trend: COLORS.secondary,
population: COLORS.accent,
urbanization: COLORS.primary,
labor_force: COLORS.secondary,
avg_wage: COLORS.accent,
};
export function ReportChartsGrid({ charts, labels }: ReportChartsGridProps) {
const mergedLabels = { ...DEFAULT_CHART_LABELS, ...labels };
const validCharts = Object.entries(charts).filter(
([, data]) => Array.isArray(data) && data.length > 0,
);
if (validCharts.length === 0) return null;
return (
<div className="mt-4 grid gap-4 sm:grid-cols-2">
{validCharts.map(([key, data]) => (
<ReportChart
key={key}
data={data as DataPoint[]}
title={mergedLabels[key] ?? key}
color={CHART_COLORS[key] ?? COLORS.primary}
variant={key.includes('trend') ? 'area' : 'bar'}
/>
))}
</div>
);
}