- Add proper Vietnamese diacritics to all valuation components (form, results, history) and their test assertions - Fix valuation API client to use /analytics/valuation endpoint - Return empty history gracefully (no server endpoint yet) Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
143 lines
5.5 KiB
TypeScript
143 lines
5.5 KiB
TypeScript
'use client';
|
||
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import type { ValuationResult } from '@/lib/valuation-api';
|
||
|
||
interface ValuationResultsProps {
|
||
result: ValuationResult;
|
||
}
|
||
|
||
function formatPrice(num: number): string {
|
||
if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(2)} tỷ`;
|
||
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} triệu`;
|
||
return num.toLocaleString('vi-VN');
|
||
}
|
||
|
||
function formatPriceM2(price: number): string {
|
||
if (price >= 1_000_000) return `${(price / 1_000_000).toFixed(1)} tr/m²`;
|
||
return `${price.toLocaleString('vi-VN')} đ/m²`;
|
||
}
|
||
|
||
export function ValuationResults({ result }: ValuationResultsProps) {
|
||
const confidencePct = Math.round(result.confidence * 100);
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Main estimate */}
|
||
<Card className="border-primary/20 bg-primary/5">
|
||
<CardHeader className="pb-3">
|
||
<CardDescription>Giá ước tính bởi AI</CardDescription>
|
||
<CardTitle className="text-3xl text-primary">
|
||
{formatPrice(result.estimatedPriceVND)} VNĐ
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="grid gap-4 sm:grid-cols-3">
|
||
<div>
|
||
<p className="text-sm text-muted-foreground">Độ tin cậy</p>
|
||
<div className="mt-1 flex items-center gap-2">
|
||
<div className="h-2 flex-1 rounded-full bg-muted">
|
||
<div
|
||
className="h-2 rounded-full bg-primary transition-all"
|
||
style={{ width: `${confidencePct}%` }}
|
||
/>
|
||
</div>
|
||
<span className="text-sm font-medium">{confidencePct}%</span>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-muted-foreground">Giá/m²</p>
|
||
<p className="mt-1 text-lg font-semibold">{formatPriceM2(result.pricePerM2)}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-sm text-muted-foreground">Khoảng giá</p>
|
||
<p className="mt-1 text-lg font-semibold">
|
||
{formatPrice(result.priceRangeLow)} – {formatPrice(result.priceRangeHigh)}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* Price drivers */}
|
||
{result.priceDrivers.length > 0 && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-lg">Yếu tố ảnh hưởng giá</CardTitle>
|
||
<CardDescription>Các yếu tố chính tác động đến giá trị bất động sản</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-3">
|
||
{result.priceDrivers.map((driver) => (
|
||
<div key={driver.feature} className="flex items-center gap-3">
|
||
<span
|
||
className={`text-sm font-medium ${
|
||
driver.direction === 'positive' ? 'text-green-600' : 'text-red-600'
|
||
}`}
|
||
>
|
||
{driver.direction === 'positive' ? '+' : '-'}
|
||
{Math.abs(driver.impact).toFixed(1)}%
|
||
</span>
|
||
<div className="flex-1">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-sm">{driver.feature}</span>
|
||
</div>
|
||
<div className="mt-1 h-1.5 rounded-full bg-muted">
|
||
<div
|
||
className={`h-1.5 rounded-full ${
|
||
driver.direction === 'positive' ? 'bg-green-500' : 'bg-red-500'
|
||
}`}
|
||
style={{ width: `${Math.min(Math.abs(driver.impact), 100)}%` }}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
|
||
{/* Comparables */}
|
||
{result.comparables.length > 0 && (
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-lg">Bất động sản tương tự</CardTitle>
|
||
<CardDescription>
|
||
{result.comparables.length} bất động sản có đặc điểm tương tự trong khu vực
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="space-y-3">
|
||
{result.comparables.map((comp) => (
|
||
<div
|
||
key={comp.id}
|
||
className="flex items-center gap-4 rounded-lg border p-3"
|
||
>
|
||
<div className="min-w-0 flex-1">
|
||
<p className="truncate font-medium">{comp.title}</p>
|
||
<p className="text-sm text-muted-foreground">
|
||
{comp.district} · {comp.areaM2} m²
|
||
</p>
|
||
</div>
|
||
<div className="text-right">
|
||
<p className="font-semibold text-primary">{formatPrice(Number(comp.priceVND))}</p>
|
||
<p className="text-sm text-muted-foreground">
|
||
{formatPriceM2(comp.pricePerM2)}
|
||
</p>
|
||
</div>
|
||
<div className="hidden sm:block">
|
||
<span className="rounded-full bg-accent px-2 py-1 text-xs font-medium">
|
||
{Math.round(comp.similarity * 100)}% tương tự
|
||
</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|