- 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>
216 lines
6.9 KiB
TypeScript
216 lines
6.9 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>Định giá bất động sản</CardTitle>
|
|
<CardDescription>
|
|
Nhập thông tin bất động sản để nhận ước tính giá từ 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">Loại bất động sản *</Label>
|
|
<Select id="propertyType" {...register('propertyType')}>
|
|
<option value="">-- Chọn loại --</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">Tỉnh/Thành phố *</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">Quận/Huyện *</Label>
|
|
<Input
|
|
id="district"
|
|
placeholder="VD: Quận 1, Bình Thạnh..."
|
|
{...register('district')}
|
|
/>
|
|
{errors.district && (
|
|
<p className="text-sm text-destructive">{errors.district.message}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="area">Diện tích (m²) *</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">Phòng ngủ</Label>
|
|
<Input
|
|
id="bedrooms"
|
|
type="number"
|
|
placeholder="VD: 3"
|
|
{...register('bedrooms')}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="bathrooms">Phòng tắm</Label>
|
|
<Input
|
|
id="bathrooms"
|
|
type="number"
|
|
placeholder="VD: 2"
|
|
{...register('bathrooms')}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="floors">Số tầng</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">Mặt tiền (m)</Label>
|
|
<Input
|
|
id="frontage"
|
|
type="number"
|
|
step="0.1"
|
|
placeholder="VD: 5"
|
|
{...register('frontage')}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="roadWidth">Độ rộng đường (m)</Label>
|
|
<Input
|
|
id="roadWidth"
|
|
type="number"
|
|
step="0.1"
|
|
placeholder="VD: 8"
|
|
{...register('roadWidth')}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="yearBuilt">Năm xây dựng</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">Có sổ đỏ/giấy tờ hợp pháp</Label>
|
|
</div>
|
|
|
|
<Button type="submit" disabled={isLoading} className="w-full sm:w-auto">
|
|
{isLoading ? 'Đang định giá...' : 'Định giá ngay'}
|
|
</Button>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|