Files
goodgo-platform/apps/web/components/valuation/valuation-results.tsx
Ho Ngoc Hai 3c6ed4c82a feat(web): add Property Valuation UI with AVM integration
Build the valuation page at /dashboard/valuation with form input,
AI-powered price estimation results, comparable properties display,
and valuation history. Add "Dinh gia AI" button to listing detail
sidebar for quick per-listing estimates.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-04-09 00:17:12 +07:00

143 lines
5.4 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)} ty`;
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(0)} trieu`;
return num.toLocaleString('vi-VN');
}
function formatPriceM2(price: number): string {
if (price >= 1_000_000) return `${(price / 1_000_000).toFixed(1)} tr/m2`;
return `${price.toLocaleString('vi-VN')} d/m2`;
}
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>Gia uoc tinh boi AI</CardDescription>
<CardTitle className="text-3xl text-primary">
{formatPrice(result.estimatedPriceVND)} VND
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-4 sm:grid-cols-3">
<div>
<p className="text-sm text-muted-foreground">Do tin cay</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">Gia/m2</p>
<p className="mt-1 text-lg font-semibold">{formatPriceM2(result.pricePerM2)}</p>
</div>
<div>
<p className="text-sm text-muted-foreground">Khoang gia</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">Yeu to anh huong gia</CardTitle>
<CardDescription>Cac yeu to chinh tac dong den gia tri bat dong san</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">Bat dong san tuong tu</CardTitle>
<CardDescription>
{result.comparables.length} bat dong san co dac diem tuong tu trong khu vuc
</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} &middot; {comp.areaM2} m2
</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)}% tuong tu
</span>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
</div>
);
}