'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import {
Bot,
ChevronDown,
ImagePlus,
MapPin,
Search,
Sparkles,
Star,
X,
} from 'lucide-react';
import { useCallback, useRef, useState } from 'react';
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 { useProjectSearch } from '@/lib/hooks/use-valuation';
import {
valuationFormSchema,
type ValuationFormData,
VALUATION_PROPERTY_TYPES,
CITIES,
FLOOD_RISK_OPTIONS,
QUALITY_LABELS,
} from '@/lib/validations/valuation';
import type { ValuationRequest } from '@/lib/valuation-api';
interface ValuationFormProps {
onSubmit: (data: ValuationRequest) => void;
isLoading?: boolean;
}
function CollapsibleSection({
title,
icon,
description,
children,
}: {
title: string;
icon: React.ReactNode;
description: string;
children: React.ReactNode;
}) {
const [open, setOpen] = useState(false);
return (
{open &&
{children}
}
);
}
function QualitySlider({
id,
label,
register,
isInverted,
}: {
id: string;
label: string;
register: ReturnType>['register'];
isInverted?: boolean;
}) {
const [value, setValue] = useState(50);
const displayValue = Math.round(value * 100) / 100;
const normalizedForApi = value / 100;
return (
);
}
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,
setValue,
formState: { errors },
} = useForm({
resolver: zodResolver(valuationFormSchema),
defaultValues: {
city: 'Ho Chi Minh',
hasLegalPaper: true,
deepAnalysis: false,
},
});
// Project autocomplete state
const [projectQuery, setProjectQuery] = useState('');
const [projectName, setProjectName] = useState('');
const [showProjectDropdown, setShowProjectDropdown] = useState(false);
const { data: projectResults } = useProjectSearch(projectQuery);
const projectInputRef = useRef(null);
// Image upload state
const [imagePreview, setImagePreview] = useState(null);
const [imageUrl, setImageUrl] = useState(null);
const [uploadProgress, setUploadProgress] = useState(null);
const [uploadError, setUploadError] = useState(null);
const fileInputRef = useRef(null);
const MAX_IMAGE_SIZE_BYTES = 5 * 1024 * 1024; // 5MB
const handleProjectSearch = useCallback((e: React.ChangeEvent) => {
const value = e.target.value;
setProjectQuery(value);
setProjectName(value);
setShowProjectDropdown(value.length >= 2);
if (!value) {
setValue('projectId', '');
}
}, [setValue]);
const handleSelectProject = useCallback(
(id: string, name: string) => {
setValue('projectId', id);
setProjectName(name);
setProjectQuery('');
setShowProjectDropdown(false);
},
[setValue],
);
const handleImageChange = useCallback(
(e: React.ChangeEvent) => {
const file = e.target.files?.[0];
if (!file) return;
setUploadError(null);
if (!file.type.startsWith('image/')) {
setUploadError('Định dạng không hợp lệ. Vui lòng chọn ảnh JPG hoặc PNG.');
return;
}
if (file.size > MAX_IMAGE_SIZE_BYTES) {
setUploadError('Ảnh vượt quá giới hạn 5MB.');
return;
}
// Show local preview
const reader = new FileReader();
reader.onload = (ev) => {
setImagePreview(ev.target?.result as string);
};
reader.readAsDataURL(file);
// Simulated upload progress for local preview flow — the valuation
// endpoint accepts a public imageUrl, so the real upload is a no-op
// here, but users still get feedback for files being processed.
setUploadProgress(0);
const start = Date.now();
const tick = () => {
const elapsed = Date.now() - start;
const pct = Math.min(100, Math.round((elapsed / 400) * 100));
setUploadProgress(pct);
if (pct < 100) {
requestAnimationFrame(tick);
} else {
setTimeout(() => setUploadProgress(null), 500);
}
};
requestAnimationFrame(tick);
// In production, upload to server and get URL
// For now we store as object URL for preview purposes
setImageUrl(URL.createObjectURL(file));
},
[],
);
const handleClearImage = useCallback(() => {
setImagePreview(null);
setImageUrl(null);
setUploadProgress(null);
setUploadError(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
}, []);
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,
projectId: data.projectId || undefined,
description: data.description || undefined,
deepAnalysis: data.deepAnalysis,
imageUrl: imageUrl || undefined,
// v2 fields
useV2: data.useV2,
distanceToCbdKm: toNum(data.distanceToCbdKm),
distanceToMetroKm: toNum(data.distanceToMetroKm),
distanceToSchoolKm: toNum(data.distanceToSchoolKm),
distanceToHospitalKm: toNum(data.distanceToHospitalKm),
distanceToParkKm: toNum(data.distanceToParkKm),
distanceToMallKm: toNum(data.distanceToMallKm),
floodZoneRisk: toNum(data.floodZoneRisk),
hasElevator: data.hasElevator,
hasParking: data.hasParking,
hasPool: data.hasPool,
renovationScore: toNum(data.renovationScore),
viewQuality: toNum(data.viewQuality),
interiorQuality: toNum(data.interiorQuality),
noiseLevel: toNum(data.noiseLevel),
naturalLight: toNum(data.naturalLight),
});
};
return (
Định giá bất động sản
Nhập thông tin bất động sản để nhận ước tính giá từ AI
);
}