/** * @ai-summary Vehicle form component for create/edit with dropdown cascades */ import React, { useState, useEffect, useRef } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Button } from '../../../shared-minimal/components/Button'; import { CreateVehicleRequest } from '../types/vehicles.types'; import { vehiclesApi } from '../api/vehicles.api'; const vehicleSchema = z .object({ vin: z.string().optional(), year: z.number().min(1980).max(new Date().getFullYear() + 1).optional(), make: z.string().optional(), model: z.string().optional(), engine: z.string().optional(), transmission: z.string().optional(), trimLevel: z.string().optional(), driveType: z.string().optional(), fuelType: z.string().optional(), nickname: z.string().optional(), color: z.string().optional(), licensePlate: z.string().optional(), odometerReading: z.number().min(0).optional(), }) .refine( (data) => { const vin = (data.vin || '').trim(); const plate = (data.licensePlate || '').trim(); // Must have either a valid 17-char VIN or a non-empty license plate if (vin.length === 17) return true; if (plate.length > 0) return true; return false; }, { message: 'Either a valid 17-character VIN or a license plate is required', path: ['vin'], } ) .refine( (data) => { const vin = (data.vin || '').trim(); const plate = (data.licensePlate || '').trim(); // If VIN provided but not 17 and no plate, fail; if plate exists, allow any VIN (or empty) if (plate.length > 0) return true; return vin.length === 17 || vin.length === 0; }, { message: 'VIN must be exactly 17 characters when license plate is not provided', path: ['vin'], } ); interface VehicleFormProps { onSubmit: (data: CreateVehicleRequest) => void; onCancel: () => void; initialData?: Partial; loading?: boolean; } export const VehicleForm: React.FC = ({ onSubmit, onCancel, initialData, loading, }) => { const [years, setYears] = useState([]); const [makes, setMakes] = useState([]); const [models, setModels] = useState([]); const [engines, setEngines] = useState([]); const [trims, setTrims] = useState([]); const [transmissions, setTransmissions] = useState([]); const [selectedYear, setSelectedYear] = useState(); const [selectedMake, setSelectedMake] = useState(''); const [selectedModel, setSelectedModel] = useState(''); const [selectedTrim, setSelectedTrim] = useState(''); const [loadingDropdowns, setLoadingDropdowns] = useState(false); const [decodingVIN, setDecodingVIN] = useState(false); const [decodeSuccess, setDecodeSuccess] = useState(false); const hasInitialized = useRef(false); const isInitializing = useRef(false); const { register, handleSubmit, formState: { errors }, watch, setValue, reset, } = useForm({ resolver: zodResolver(vehicleSchema), defaultValues: initialData, }); const watchedYear = watch('year'); const watchedMake = watch('make'); const watchedModel = watch('model'); const watchedVIN = watch('vin'); // VIN decode handler const handleDecodeVIN = async () => { const vin = watchedVIN; if (!vin || vin.length !== 17) { return; } setDecodingVIN(true); setDecodeSuccess(false); try { const result = await vehiclesApi.decodeVIN(vin); if (result.success) { // Auto-populate fields with decoded values if (result.year) setValue('year', result.year); if (result.make) setValue('make', result.make); if (result.model) setValue('model', result.model); if (result.transmission) setValue('transmission', result.transmission); if (result.engine) setValue('engine', result.engine); if (result.trimLevel) setValue('trimLevel', result.trimLevel); setDecodeSuccess(true); setTimeout(() => setDecodeSuccess(false), 3000); // Hide success after 3 seconds } } catch (error) { console.error('VIN decode failed:', error); } finally { setDecodingVIN(false); } }; // Load years on component mount useEffect(() => { const loadYears = async () => { try { const yearsData = await vehiclesApi.getYears(); setYears(yearsData); } catch (error) { console.error('Failed to load years:', error); } }; loadYears(); }, []); // Initialize dropdowns when editing existing vehicle useEffect(() => { const initializeEditMode = async () => { // Only run once and only if we have initialData if (hasInitialized.current || !initialData || !initialData.year) return; hasInitialized.current = true; isInitializing.current = true; try { setLoadingDropdowns(true); // Step 1: Set year and load makes setSelectedYear(initialData.year); const makesData = await vehiclesApi.getMakes(initialData.year); setMakes(makesData); if (!initialData.make) { isInitializing.current = false; return; } // Step 2: Set make and load models setSelectedMake(initialData.make); const modelsData = await vehiclesApi.getModels(initialData.year, initialData.make); setModels(modelsData); if (!initialData.model) { isInitializing.current = false; return; } // Step 3: Set model and load trims + transmissions setSelectedModel(initialData.model); const [trimsData, transmissionsData] = await Promise.all([ vehiclesApi.getTrims(initialData.year, initialData.make, initialData.model), vehiclesApi.getTransmissions(initialData.year, initialData.make, initialData.model) ]); setTrims(trimsData); setTransmissions(transmissionsData); if (initialData.trimLevel) { // Step 4: Set trim and load engines setSelectedTrim(initialData.trimLevel); const enginesData = await vehiclesApi.getEngines( initialData.year, initialData.make, initialData.model, initialData.trimLevel ); setEngines(enginesData); } isInitializing.current = false; } catch (error) { console.error('Failed to initialize edit mode:', error); isInitializing.current = false; } finally { setLoadingDropdowns(false); } }; initializeEditMode(); }, [initialData]); // Run when initialData is available // Reset form values after initialization useEffect(() => { if (!isInitializing.current && initialData) { reset(initialData); } }, [initialData, reset]); // Load makes when year changes useEffect(() => { // Skip during initialization if (isInitializing.current) return; if (watchedYear && watchedYear !== selectedYear) { const loadMakes = async () => { setLoadingDropdowns(true); try { const makesData = await vehiclesApi.getMakes(watchedYear); setMakes(makesData); setSelectedYear(watchedYear); // Clear dependent selections setSelectedMake(''); setSelectedModel(''); setSelectedTrim(''); setModels([]); setTrims([]); setEngines([]); setTransmissions([]); setValue('make', ''); setValue('model', ''); setValue('trimLevel', ''); setValue('transmission', ''); setValue('engine', ''); } catch (error) { console.error('Failed to load makes:', error); setMakes([]); } finally { setLoadingDropdowns(false); } }; loadMakes(); } }, [watchedYear, selectedYear, setValue]); // Load models when make changes useEffect(() => { // Skip during initialization if (isInitializing.current) return; if (watchedMake && watchedYear && watchedMake !== selectedMake) { const loadModels = async () => { setLoadingDropdowns(true); try { const modelsData = await vehiclesApi.getModels(watchedYear, watchedMake); setModels(modelsData); setSelectedMake(watchedMake); // Clear dependent selections setSelectedModel(''); setSelectedTrim(''); setTrims([]); setEngines([]); setTransmissions([]); setValue('model', ''); setValue('trimLevel', ''); setValue('transmission', ''); setValue('engine', ''); } catch (error) { console.error('Failed to load models:', error); setModels([]); } finally { setLoadingDropdowns(false); } }; loadModels(); } }, [watchedMake, watchedYear, selectedMake, setValue]); // Load trims and transmissions when model changes useEffect(() => { // Skip during initialization if (isInitializing.current) return; if (watchedModel && watchedYear && selectedMake && watchedModel !== selectedModel) { const loadTrimsAndTransmissions = async () => { setLoadingDropdowns(true); try { const [trimsData, transmissionsData] = await Promise.all([ vehiclesApi.getTrims(watchedYear, selectedMake, watchedModel), vehiclesApi.getTransmissions(watchedYear, selectedMake, watchedModel) ]); setTrims(trimsData); setTransmissions(transmissionsData); setSelectedModel(watchedModel); // Clear deeper selections setSelectedTrim(''); setEngines([]); setValue('trimLevel', ''); setValue('engine', ''); } catch (error) { console.error('Failed to load trims and transmissions:', error); setTrims([]); setTransmissions([]); } finally { setLoadingDropdowns(false); } }; loadTrimsAndTransmissions(); } }, [watchedModel, watchedYear, selectedMake, selectedModel, setValue]); // Load engines when trim changes useEffect(() => { // Skip during initialization if (isInitializing.current) return; const trimName = watch('trimLevel'); if (trimName && watchedYear && selectedMake && selectedModel) { const loadEngines = async () => { setLoadingDropdowns(true); try { const enginesData = await vehiclesApi.getEngines( watchedYear, selectedMake, selectedModel, trimName ); setEngines(enginesData); setSelectedTrim(trimName); } catch (error) { console.error('Failed to load engines:', error); setEngines([]); } finally { setLoadingDropdowns(false); } }; loadEngines(); } }, [watchedYear, selectedMake, selectedModel, watch('trimLevel')]); return (

Enter VIN to auto-fill vehicle details OR manually select from dropdowns below

{decodeSuccess && (

VIN decoded successfully! Fields populated.

)} {errors.vin && (

{errors.vin.message}

)}
{/* Vehicle Specification Dropdowns */}
{/* Trim (left) */}
{/* Engine (middle) */}
{/* Transmission (right) */}
{errors.licensePlate && (

{errors.licensePlate.message}

)}
); };