Bug Fixes

This commit is contained in:
Eric Gullickson
2025-11-09 10:37:27 -06:00
parent 408a0736c0
commit b1755d415c
2 changed files with 145 additions and 34 deletions

View File

@@ -30,32 +30,15 @@ export const vehiclesRoutes: FastifyPluginAsync = async (
handler: vehiclesController.createVehicle.bind(vehiclesController) 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 // 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 // GET /api/vehicles/dropdown/years - Available model years
fastify.get('/vehicles/dropdown/years', { fastify.get('/vehicles/dropdown/years', {
preHandler: [fastify.authenticate], preHandler: [fastify.authenticate],
handler: vehiclesController.getDropdownYears.bind(vehiclesController) handler: vehiclesController.getDropdownYears.bind(vehiclesController)
}); });
// GET /api/vehicles/dropdown/makes?year=2024 - Get makes for year (Level 1) // GET /api/vehicles/dropdown/makes?year=2024 - Get makes for year (Level 1)
fastify.get<{ Querystring: { year: number } }>('/vehicles/dropdown/makes', { fastify.get<{ Querystring: { year: number } }>('/vehicles/dropdown/makes', {
preHandler: [fastify.authenticate], preHandler: [fastify.authenticate],
@@ -91,6 +74,25 @@ export const vehiclesRoutes: FastifyPluginAsync = async (
preHandler: [fastify.authenticate], preHandler: [fastify.authenticate],
handler: vehiclesController.decodeVIN.bind(vehiclesController) 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 // For backward compatibility during migration

View File

@@ -2,7 +2,7 @@
* @ai-summary Vehicle form component for create/edit with dropdown cascades * @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 { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod'; import { z } from 'zod';
@@ -67,15 +67,6 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
initialData, initialData,
loading, 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<number[]>([]); const [years, setYears] = useState<number[]>([]);
const [makes, setMakes] = useState<DropdownOption[]>([]); const [makes, setMakes] = useState<DropdownOption[]>([]);
const [models, setModels] = useState<DropdownOption[]>([]); const [models, setModels] = useState<DropdownOption[]>([]);
@@ -89,6 +80,9 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
const [selectedTrim, setSelectedTrim] = useState<DropdownOption | undefined>(); const [selectedTrim, setSelectedTrim] = useState<DropdownOption | undefined>();
const [decodingVIN, setDecodingVIN] = useState(false); const [decodingVIN, setDecodingVIN] = useState(false);
const [decodeSuccess, setDecodeSuccess] = useState(false); const [decodeSuccess, setDecodeSuccess] = useState(false);
const hasInitialized = useRef(false);
const isInitializing = useRef(false);
const [dropdownsReady, setDropdownsReady] = useState(false);
const { const {
register, register,
@@ -96,6 +90,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
formState: { errors }, formState: { errors },
watch, watch,
setValue, setValue,
reset,
} = useForm<CreateVehicleRequest>({ } = useForm<CreateVehicleRequest>({
resolver: zodResolver(vehicleSchema), resolver: zodResolver(vehicleSchema),
defaultValues: initialData, defaultValues: initialData,
@@ -151,8 +146,113 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
loadYears(); 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 // Load makes when year changes
useEffect(() => { useEffect(() => {
// Skip during initialization
if (isInitializing.current) return;
if (watchedYear && watchedYear !== selectedYear) { if (watchedYear && watchedYear !== selectedYear) {
const loadMakes = async () => { const loadMakes = async () => {
setLoadingDropdowns(true); setLoadingDropdowns(true);
@@ -160,7 +260,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
const makesData = await vehiclesApi.getMakes(watchedYear); const makesData = await vehiclesApi.getMakes(watchedYear);
setMakes(makesData); setMakes(makesData);
setSelectedYear(watchedYear); setSelectedYear(watchedYear);
// Clear dependent selections // Clear dependent selections
setModels([]); setModels([]);
setEngines([]); setEngines([]);
@@ -187,6 +287,9 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
// Load models when make changes // Load models when make changes
useEffect(() => { useEffect(() => {
// Skip during initialization
if (isInitializing.current) return;
if (watchedMake && watchedYear && watchedMake !== selectedMake?.name) { if (watchedMake && watchedYear && watchedMake !== selectedMake?.name) {
const makeOption = makes.find(make => make.name === watchedMake); const makeOption = makes.find(make => make.name === watchedMake);
if (makeOption) { if (makeOption) {
@@ -196,7 +299,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
const modelsData = await vehiclesApi.getModels(watchedYear, makeOption.id); const modelsData = await vehiclesApi.getModels(watchedYear, makeOption.id);
setModels(modelsData); setModels(modelsData);
setSelectedMake(makeOption); setSelectedMake(makeOption);
// Clear dependent selections // Clear dependent selections
setEngines([]); setEngines([]);
setTrims([]); setTrims([]);
@@ -221,6 +324,9 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
// Load trims when model changes // Load trims when model changes
useEffect(() => { useEffect(() => {
// Skip during initialization
if (isInitializing.current) return;
if (watchedModel && watchedYear && selectedMake && watchedModel !== selectedModel?.name) { if (watchedModel && watchedYear && selectedMake && watchedModel !== selectedModel?.name) {
const modelOption = models.find(model => model.name === watchedModel); const modelOption = models.find(model => model.name === watchedModel);
if (modelOption) { if (modelOption) {
@@ -257,6 +363,9 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
// Load engines when trim changes // Load engines when trim changes
useEffect(() => { useEffect(() => {
// Skip during initialization
if (isInitializing.current) return;
const trimName = watch('trimLevel'); const trimName = watch('trimLevel');
if (trimName && watchedYear && selectedMake && selectedModel) { if (trimName && watchedYear && selectedMake && selectedModel) {
const trimOption = trims.find(t => t.name === trimName); const trimOption = trims.find(t => t.name === trimName);
@@ -347,7 +456,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
<option value="">Select Make</option> <option value="">Select Make</option>
{makes.map((make) => ( {makes.map((make) => (
<option key={make.id} value={make.name}> <option key={make.id} value={make.name}>
{formatVehicleLabel(make.name)} {make.name}
</option> </option>
))} ))}
</select> </select>
@@ -366,7 +475,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
<option value="">Select Model</option> <option value="">Select Model</option>
{models.map((model) => ( {models.map((model) => (
<option key={model.id} value={model.name}> <option key={model.id} value={model.name}>
{formatVehicleLabel(model.name)} {model.name}
</option> </option>
))} ))}
</select> </select>