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>
96 lines
2.5 KiB
TypeScript
96 lines
2.5 KiB
TypeScript
'use client';
|
|
|
|
import {
|
|
Building,
|
|
CalendarDays,
|
|
TrendingDown,
|
|
TrendingUp,
|
|
Users,
|
|
Warehouse,
|
|
} from 'lucide-react';
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card';
|
|
import { formatPrice, formatPricePerM2 } from '@/lib/currency';
|
|
import type { MarketContext } from '@/lib/valuation-api';
|
|
|
|
interface MarketContextCardProps {
|
|
context: MarketContext;
|
|
}
|
|
|
|
export function MarketContextCard({ context }: MarketContextCardProps) {
|
|
const isGrowthPositive = context.priceGrowthYoY >= 0;
|
|
|
|
const stats = [
|
|
{
|
|
label: 'Giá trung bình/m²',
|
|
value: formatPricePerM2(context.avgPricePerM2),
|
|
icon: Building,
|
|
},
|
|
{
|
|
label: 'Giá trung vị',
|
|
value: formatPrice(context.medianPrice),
|
|
icon: Warehouse,
|
|
},
|
|
{
|
|
label: 'Tăng trưởng YoY',
|
|
value: `${isGrowthPositive ? '+' : ''}${context.priceGrowthYoY.toFixed(1)}%`,
|
|
icon: isGrowthPositive ? TrendingUp : TrendingDown,
|
|
color: isGrowthPositive ? 'text-green-600' : 'text-red-600',
|
|
},
|
|
{
|
|
label: 'Chỉ số nhu cầu',
|
|
value: `${context.demandIndex.toFixed(0)}/100`,
|
|
icon: Users,
|
|
},
|
|
{
|
|
label: 'Nguồn cung',
|
|
value: `${context.supplyCount.toLocaleString('vi-VN')} BĐS`,
|
|
icon: Building,
|
|
},
|
|
{
|
|
label: 'Thời gian bán TB',
|
|
value: `${context.avgDaysOnMarket} ngày`,
|
|
icon: CalendarDays,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Bối cảnh thị trường</CardTitle>
|
|
<CardDescription>
|
|
{context.district}, {context.city} — {context.period}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
{stats.map((stat) => {
|
|
const Icon = stat.icon;
|
|
return (
|
|
<div
|
|
key={stat.label}
|
|
className="flex items-start gap-3 rounded-lg border p-3"
|
|
>
|
|
<div className="rounded-md bg-muted p-2">
|
|
<Icon className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
<div>
|
|
<p className="text-xs text-muted-foreground">{stat.label}</p>
|
|
<p className={`text-sm font-semibold ${stat.color ?? ''}`}>
|
|
{stat.value}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|