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
Add tests for GenerateReport, GetReport, DeleteReport command/query handlers and Report entity domain logic. Co-Authored-By: Paperclip <noreply@paperclip.ing>
153 lines
3.9 KiB
TypeScript
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>
|
|
);
|
|
}
|