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>
111 lines
3.4 KiB
TypeScript
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>
|
|
);
|
|
}
|