From b1755d415cf5a5bfdf3035c2d8c97e35632f1d02 Mon Sep 17 00:00:00 2001 From: Eric Gullickson <16152721+ericgullickson@users.noreply.github.com> Date: Sun, 9 Nov 2025 10:37:27 -0600 Subject: [PATCH] Bug Fixes --- .../features/vehicles/api/vehicles.routes.ts | 42 +++--- .../vehicles/components/VehicleForm.tsx | 137 ++++++++++++++++-- 2 files changed, 145 insertions(+), 34 deletions(-) diff --git a/backend/src/features/vehicles/api/vehicles.routes.ts b/backend/src/features/vehicles/api/vehicles.routes.ts index 4d7cc6b..2491b7e 100644 --- a/backend/src/features/vehicles/api/vehicles.routes.ts +++ b/backend/src/features/vehicles/api/vehicles.routes.ts @@ -30,32 +30,15 @@ export const vehiclesRoutes: FastifyPluginAsync = async ( handler: vehiclesController.createVehicle.bind(vehiclesController) }); - // GET /api/vehicles/:id - Get specific vehicle - fastify.get<{ Params: VehicleParams }>('/vehicles/:id', { - preHandler: [fastify.authenticate], - handler: vehiclesController.getVehicle.bind(vehiclesController) - }); - - // PUT /api/vehicles/:id - Update vehicle - fastify.put<{ Params: VehicleParams; Body: UpdateVehicleBody }>('/vehicles/:id', { - preHandler: [fastify.authenticate], - handler: vehiclesController.updateVehicle.bind(vehiclesController) - }); - - // DELETE /api/vehicles/:id - Delete vehicle - fastify.delete<{ Params: VehicleParams }>('/vehicles/:id', { - preHandler: [fastify.authenticate], - handler: vehiclesController.deleteVehicle.bind(vehiclesController) - }); - // Hierarchical Vehicle API - mirrors MVP Platform Vehicles Service structure - + // IMPORTANT: Register specific routes BEFORE dynamic :id routes to avoid conflicts + // GET /api/vehicles/dropdown/years - Available model years fastify.get('/vehicles/dropdown/years', { preHandler: [fastify.authenticate], handler: vehiclesController.getDropdownYears.bind(vehiclesController) }); - + // GET /api/vehicles/dropdown/makes?year=2024 - Get makes for year (Level 1) fastify.get<{ Querystring: { year: number } }>('/vehicles/dropdown/makes', { preHandler: [fastify.authenticate], @@ -91,6 +74,25 @@ export const vehiclesRoutes: FastifyPluginAsync = async ( preHandler: [fastify.authenticate], handler: vehiclesController.decodeVIN.bind(vehiclesController) }); + + // Dynamic :id routes MUST come last to avoid matching specific paths like "dropdown" + // GET /api/vehicles/:id - Get specific vehicle + fastify.get<{ Params: VehicleParams }>('/vehicles/:id', { + preHandler: [fastify.authenticate], + handler: vehiclesController.getVehicle.bind(vehiclesController) + }); + + // PUT /api/vehicles/:id - Update vehicle + fastify.put<{ Params: VehicleParams; Body: UpdateVehicleBody }>('/vehicles/:id', { + preHandler: [fastify.authenticate], + handler: vehiclesController.updateVehicle.bind(vehiclesController) + }); + + // DELETE /api/vehicles/:id - Delete vehicle + fastify.delete<{ Params: VehicleParams }>('/vehicles/:id', { + preHandler: [fastify.authenticate], + handler: vehiclesController.deleteVehicle.bind(vehiclesController) + }); }; // For backward compatibility during migration diff --git a/frontend/src/features/vehicles/components/VehicleForm.tsx b/frontend/src/features/vehicles/components/VehicleForm.tsx index a84b0bf..07e0c64 100644 --- a/frontend/src/features/vehicles/components/VehicleForm.tsx +++ b/frontend/src/features/vehicles/components/VehicleForm.tsx @@ -2,7 +2,7 @@ * @ai-summary Vehicle form component for create/edit with dropdown cascades */ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; @@ -67,15 +67,6 @@ export const VehicleForm: React.FC = ({ initialData, loading, }) => { - const formatVehicleLabel = (value?: string): string => { - if (!value) return ''; - return value - .split(' ') - .filter(Boolean) - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - }; - const [years, setYears] = useState([]); const [makes, setMakes] = useState([]); const [models, setModels] = useState([]); @@ -89,6 +80,9 @@ export const VehicleForm: React.FC = ({ const [selectedTrim, setSelectedTrim] = useState(); const [decodingVIN, setDecodingVIN] = useState(false); const [decodeSuccess, setDecodeSuccess] = useState(false); + const hasInitialized = useRef(false); + const isInitializing = useRef(false); + const [dropdownsReady, setDropdownsReady] = useState(false); const { register, @@ -96,6 +90,7 @@ export const VehicleForm: React.FC = ({ formState: { errors }, watch, setValue, + reset, } = useForm({ resolver: zodResolver(vehicleSchema), defaultValues: initialData, @@ -151,8 +146,113 @@ export const VehicleForm: React.FC = ({ 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); + + // Set year and load makes + setSelectedYear(initialData.year); + const makesData = await vehiclesApi.getMakes(initialData.year); + setMakes(makesData); + + if (initialData.make) { + const makeOption = makesData.find(m => m.name === initialData.make); + if (makeOption) { + setSelectedMake(makeOption); + + // Load models + const modelsData = await vehiclesApi.getModels(initialData.year, makeOption.id); + setModels(modelsData); + + if (initialData.model) { + const modelOption = modelsData.find(m => m.name === initialData.model); + if (modelOption) { + setSelectedModel(modelOption); + + // Load trims and transmissions in parallel + const [trimsData, transmissionsData] = await Promise.all([ + vehiclesApi.getTrims(initialData.year, makeOption.id, modelOption.id), + vehiclesApi.getTransmissions(initialData.year, makeOption.id, modelOption.id) + ]); + setTrims(trimsData); + setTransmissions(transmissionsData); + + if (initialData.trimLevel) { + const trimOption = trimsData.find(t => t.name === initialData.trimLevel); + if (trimOption) { + setSelectedTrim(trimOption); + + // Load engines + const enginesData = await vehiclesApi.getEngines( + initialData.year, + makeOption.id, + modelOption.id, + trimOption.id + ); + setEngines(enginesData); + } + } + } + } + } + } + + // Signal that dropdowns are ready + setDropdownsReady(true); + } catch (error) { + console.error('Failed to initialize edit mode:', error); + } finally { + setLoadingDropdowns(false); + } + }; + + initializeEditMode(); + }, [initialData]); // Run when initialData is available + + // Reset form values after dropdowns are loaded and rendered + useEffect(() => { + if (!dropdownsReady || !initialData) return; + + let timer2: NodeJS.Timeout; + + // Use setTimeout to ensure React has rendered the dropdown options + const timer1 = setTimeout(() => { + // Normalize the data to match dropdown option values (lowercase) + const normalizedData = { + ...initialData, + make: initialData.make?.toLowerCase(), + model: initialData.model?.toLowerCase(), + trimLevel: initialData.trimLevel, + transmission: initialData.transmission, + engine: initialData.engine + }; + + reset(normalizedData); + + // Mark initialization complete after a delay to allow effects to process + timer2 = setTimeout(() => { + isInitializing.current = false; + }, 100); + }, 50); + + return () => { + clearTimeout(timer1); + if (timer2) clearTimeout(timer2); + }; + }, [dropdownsReady, initialData, reset]); + // Load makes when year changes useEffect(() => { + // Skip during initialization + if (isInitializing.current) return; + if (watchedYear && watchedYear !== selectedYear) { const loadMakes = async () => { setLoadingDropdowns(true); @@ -160,7 +260,7 @@ export const VehicleForm: React.FC = ({ const makesData = await vehiclesApi.getMakes(watchedYear); setMakes(makesData); setSelectedYear(watchedYear); - + // Clear dependent selections setModels([]); setEngines([]); @@ -187,6 +287,9 @@ export const VehicleForm: React.FC = ({ // Load models when make changes useEffect(() => { + // Skip during initialization + if (isInitializing.current) return; + if (watchedMake && watchedYear && watchedMake !== selectedMake?.name) { const makeOption = makes.find(make => make.name === watchedMake); if (makeOption) { @@ -196,7 +299,7 @@ export const VehicleForm: React.FC = ({ const modelsData = await vehiclesApi.getModels(watchedYear, makeOption.id); setModels(modelsData); setSelectedMake(makeOption); - + // Clear dependent selections setEngines([]); setTrims([]); @@ -221,6 +324,9 @@ export const VehicleForm: React.FC = ({ // Load trims when model changes useEffect(() => { + // Skip during initialization + if (isInitializing.current) return; + if (watchedModel && watchedYear && selectedMake && watchedModel !== selectedModel?.name) { const modelOption = models.find(model => model.name === watchedModel); if (modelOption) { @@ -257,6 +363,9 @@ export const VehicleForm: React.FC = ({ // Load engines when trim changes useEffect(() => { + // Skip during initialization + if (isInitializing.current) return; + const trimName = watch('trimLevel'); if (trimName && watchedYear && selectedMake && selectedModel) { const trimOption = trims.find(t => t.name === trimName); @@ -347,7 +456,7 @@ export const VehicleForm: React.FC = ({ {makes.map((make) => ( ))} @@ -366,7 +475,7 @@ export const VehicleForm: React.FC = ({ {models.map((model) => ( ))}