Files
goodgo-platform/apps/web/components/valuation/comparables-table.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

157 lines
4.8 KiB
TypeScript

'use client';
import {
createColumnHelper,
flexRender,
getCoreRowModel,
getSortedRowModel,
useReactTable,
type SortingState,
} from '@tanstack/react-table';
import { ArrowUpDown, MapPin } from 'lucide-react';
import { useState } from 'react';
import { Badge } from '@/components/ui/badge';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { formatPrice, formatPricePerM2 } from '@/lib/currency';
import type { ValuationComparable } from '@/lib/valuation-api';
interface ComparablesTableProps {
comparables: ValuationComparable[];
}
const columnHelper = createColumnHelper<ValuationComparable>();
function getSimilarityBadge(similarity: number): {
label: string;
variant: 'success' | 'warning' | 'info';
} {
const pct = Math.round(similarity * 100);
if (pct >= 85) return { label: `${pct}% tương tự`, variant: 'success' };
if (pct >= 70) return { label: `${pct}% tương tự`, variant: 'info' };
return { label: `${pct}% tương tự`, variant: 'warning' };
}
const columns = [
columnHelper.accessor('title', {
header: 'Bất động sản',
cell: (info) => {
const row = info.row.original;
return (
<div className="min-w-0">
<p className="truncate font-medium">{info.getValue()}</p>
<p className="flex items-center gap-1 text-xs text-muted-foreground">
<MapPin className="h-3 w-3" />
{row.district}
{row.address ? `${row.address}` : ''}
</p>
</div>
);
},
}),
columnHelper.accessor('areaM2', {
header: 'Diện tích',
cell: (info) => <span className="whitespace-nowrap">{info.getValue()} m²</span>,
}),
columnHelper.accessor('priceVND', {
header: 'Giá',
cell: (info) => (
<span className="whitespace-nowrap font-semibold text-primary">
{formatPrice(info.getValue())}
</span>
),
}),
columnHelper.accessor('pricePerM2', {
header: 'Giá/m²',
cell: (info) => (
<span className="whitespace-nowrap text-muted-foreground">
{formatPricePerM2(info.getValue())}
</span>
),
}),
columnHelper.accessor('similarity', {
header: 'Tương đồng',
cell: (info) => {
const badge = getSimilarityBadge(info.getValue());
return <Badge variant={badge.variant}>{badge.label}</Badge>;
},
sortDescFirst: true,
}),
];
export function ComparablesTable({ comparables }: ComparablesTableProps) {
const [sorting, setSorting] = useState<SortingState>([
{ id: 'similarity', desc: true },
]);
const table = useReactTable({
data: comparables,
columns,
state: { sorting },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
});
if (comparables.length === 0) return null;
return (
<Card>
<CardHeader>
<CardTitle className="text-lg">Bất đng sản tương tự</CardTitle>
<CardDescription>
{comparables.length} bất đng sản đc điểm tương tự trong khu vực
</CardDescription>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id} className="border-b text-left">
{headerGroup.headers.map((header) => (
<th
key={header.id}
className="pb-2 pr-4 font-medium"
>
{header.isPlaceholder ? null : (
<button
type="button"
className="flex items-center gap-1 hover:text-foreground"
onClick={header.column.getToggleSortingHandler()}
>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
<ArrowUpDown className="h-3 w-3 text-muted-foreground" />
</button>
)}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id} className="border-b last:border-0">
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="py-2 pr-4">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
);
}