Files
goodgo-platform/apps/web/components/valuation/valuation-history-chart.tsx
Ho Ngoc Hai 8da488711b feat(analytics): AVM v2 batch valuation, comparison, history + frontend upgrade
Add batch valuation (POST /analytics/valuation/batch, max 50 properties),
valuation comparison (POST /analytics/valuation/compare, 2-5 properties),
and history endpoint (GET /analytics/valuation/history/:propertyId) with
confidence explanation helper. Frontend: enhanced valuation form with project
autocomplete and deep analysis toggle, results with confidence badges and
price range visualization, comparables table, history chart, market context
card, and PDF export.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-16 05:08:05 +07:00

111 lines
3.4 KiB
TypeScript

'use client';
import {
Area,
AreaChart,
CartesianGrid,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from 'recharts';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { formatPrice } from '@/lib/currency';
import type { ValuationHistoryPoint } from '@/lib/valuation-api';
interface ValuationHistoryChartProps {
data: ValuationHistoryPoint[];
}
function formatChartDate(dateStr: string): string {
const date = new Date(dateStr);
return date.toLocaleDateString('vi-VN', { month: 'short', year: '2-digit' });
}
function formatTooltipDate(dateStr: string): string {
const date = new Date(dateStr);
return date.toLocaleDateString('vi-VN', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});
}
export function ValuationHistoryChart({ data }: ValuationHistoryChartProps) {
if (data.length < 2) return null;
const chartData = data.map((point) => ({
date: point.date,
price: point.estimatedPriceVND,
confidence: Math.round(point.confidence * 100),
}));
return (
<Card>
<CardHeader>
<CardTitle className="text-lg">Lịch sử đnh giá</CardTitle>
<CardDescription>
Biến đng giá ưc tính theo thời gian
</CardDescription>
</CardHeader>
<CardContent>
<div className="h-64">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData} margin={{ top: 5, right: 10, left: 10, bottom: 5 }}>
<defs>
<linearGradient id="priceGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor="hsl(var(--primary))" stopOpacity={0.3} />
<stop offset="95%" stopColor="hsl(var(--primary))" stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="date"
tickFormatter={formatChartDate}
className="fill-muted-foreground"
fontSize={12}
/>
<YAxis
tickFormatter={(val: number) => formatPrice(val)}
className="fill-muted-foreground"
fontSize={12}
width={80}
/>
<Tooltip
contentStyle={{
backgroundColor: 'hsl(var(--card))',
border: '1px solid hsl(var(--border))',
borderRadius: '0.5rem',
fontSize: '0.875rem',
}}
labelFormatter={(label) =>
typeof label === 'string' ? formatTooltipDate(label) : label
}
formatter={(value, name) => {
const num = Number(value);
if (name === 'price') return [formatPrice(num) + ' VNĐ', 'Giá ước tính'];
if (name === 'confidence') return [`${num}%`, 'Độ tin cậy'];
return [String(value), name];
}}
/>
<Area
type="monotone"
dataKey="price"
stroke="hsl(var(--primary))"
strokeWidth={2}
fill="url(#priceGradient)"
/>
</AreaChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
);
}