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

216 lines
6.8 KiB
TypeScript

'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select } from '@/components/ui/select';
import {
valuationFormSchema,
type ValuationFormData,
VALUATION_PROPERTY_TYPES,
CITIES,
} from '@/lib/validations/valuation';
import type { ValuationRequest } from '@/lib/valuation-api';
interface ValuationFormProps {
onSubmit: (data: ValuationRequest) => void;
isLoading?: boolean;
}
function toNum(val: string | undefined): number | undefined {
if (!val || val === '') return undefined;
const n = Number(val);
return isNaN(n) ? undefined : n;
}
export function ValuationForm({ onSubmit, isLoading }: ValuationFormProps) {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<ValuationFormData>({
resolver: zodResolver(valuationFormSchema),
defaultValues: {
city: 'Ho Chi Minh',
hasLegalPaper: true,
},
});
const handleFormSubmit = (data: ValuationFormData) => {
onSubmit({
propertyType: data.propertyType,
area: Number(data.area),
district: data.district,
city: data.city,
bedrooms: toNum(data.bedrooms),
bathrooms: toNum(data.bathrooms),
floors: toNum(data.floors),
frontage: toNum(data.frontage),
roadWidth: toNum(data.roadWidth),
yearBuilt: toNum(data.yearBuilt),
hasLegalPaper: data.hasLegalPaper,
});
};
return (
<Card>
<CardHeader>
<CardTitle>Dinh gia bat dong san</CardTitle>
<CardDescription>
Nhap thong tin bat dong san de nhan uoc tinh gia tu AI
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-6">
{/* Row 1: Property type + City */}
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="propertyType">Loai bat dong san *</Label>
<Select id="propertyType" {...register('propertyType')}>
<option value="">-- Chon loai --</option>
{VALUATION_PROPERTY_TYPES.map((t) => (
<option key={t.value} value={t.value}>
{t.label}
</option>
))}
</Select>
{errors.propertyType && (
<p className="text-sm text-destructive">{errors.propertyType.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="city">Tinh/Thanh pho *</Label>
<Select id="city" {...register('city')}>
{CITIES.map((c) => (
<option key={c.value} value={c.value}>
{c.label}
</option>
))}
</Select>
{errors.city && (
<p className="text-sm text-destructive">{errors.city.message}</p>
)}
</div>
</div>
{/* Row 2: District + Area */}
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="district">Quan/Huyen *</Label>
<Input
id="district"
placeholder="VD: Quan 1, Binh Thanh..."
{...register('district')}
/>
{errors.district && (
<p className="text-sm text-destructive">{errors.district.message}</p>
)}
</div>
<div className="space-y-2">
<Label htmlFor="area">Dien tich (m2) *</Label>
<Input
id="area"
type="number"
step="0.1"
placeholder="VD: 80"
{...register('area')}
/>
{errors.area && (
<p className="text-sm text-destructive">{errors.area.message}</p>
)}
</div>
</div>
{/* Row 3: Bedrooms + Bathrooms + Floors */}
<div className="grid gap-4 sm:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="bedrooms">Phong ngu</Label>
<Input
id="bedrooms"
type="number"
placeholder="VD: 3"
{...register('bedrooms')}
/>
</div>
<div className="space-y-2">
<Label htmlFor="bathrooms">Phong tam</Label>
<Input
id="bathrooms"
type="number"
placeholder="VD: 2"
{...register('bathrooms')}
/>
</div>
<div className="space-y-2">
<Label htmlFor="floors">So tang</Label>
<Input
id="floors"
type="number"
placeholder="VD: 4"
{...register('floors')}
/>
</div>
</div>
{/* Row 4: Frontage + Road Width + Year Built */}
<div className="grid gap-4 sm:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="frontage">Mat tien (m)</Label>
<Input
id="frontage"
type="number"
step="0.1"
placeholder="VD: 5"
{...register('frontage')}
/>
</div>
<div className="space-y-2">
<Label htmlFor="roadWidth">Do rong duong (m)</Label>
<Input
id="roadWidth"
type="number"
step="0.1"
placeholder="VD: 8"
{...register('roadWidth')}
/>
</div>
<div className="space-y-2">
<Label htmlFor="yearBuilt">Nam xay dung</Label>
<Input
id="yearBuilt"
type="number"
placeholder="VD: 2020"
{...register('yearBuilt')}
/>
</div>
</div>
{/* Legal paper checkbox */}
<div className="flex items-center gap-2">
<input
id="hasLegalPaper"
type="checkbox"
className="h-4 w-4 rounded border-input"
{...register('hasLegalPaper')}
/>
<Label htmlFor="hasLegalPaper">Co so do/giay to hop phap</Label>
</div>
<Button type="submit" disabled={isLoading} className="w-full sm:w-auto">
{isLoading ? 'Dang dinh gia...' : 'Dinh gia ngay'}
</Button>
</form>
</CardContent>
</Card>
);
}