Bug Fixes
This commit is contained in:
@@ -30,25 +30,8 @@ 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', {
|
||||
@@ -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
|
||||
|
||||
@@ -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<VehicleFormProps> = ({
|
||||
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<number[]>([]);
|
||||
const [makes, setMakes] = useState<DropdownOption[]>([]);
|
||||
const [models, setModels] = useState<DropdownOption[]>([]);
|
||||
@@ -89,6 +80,9 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
|
||||
const [selectedTrim, setSelectedTrim] = useState<DropdownOption | undefined>();
|
||||
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<VehicleFormProps> = ({
|
||||
formState: { errors },
|
||||
watch,
|
||||
setValue,
|
||||
reset,
|
||||
} = useForm<CreateVehicleRequest>({
|
||||
resolver: zodResolver(vehicleSchema),
|
||||
defaultValues: initialData,
|
||||
@@ -151,8 +146,113 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
|
||||
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);
|
||||
@@ -187,6 +287,9 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
|
||||
|
||||
// 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) {
|
||||
@@ -221,6 +324,9 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
|
||||
|
||||
// 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<VehicleFormProps> = ({
|
||||
|
||||
// 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<VehicleFormProps> = ({
|
||||
<option value="">Select Make</option>
|
||||
{makes.map((make) => (
|
||||
<option key={make.id} value={make.name}>
|
||||
{formatVehicleLabel(make.name)}
|
||||
{make.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -366,7 +475,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
|
||||
<option value="">Select Model</option>
|
||||
{models.map((model) => (
|
||||
<option key={model.id} value={model.name}>
|
||||
{formatVehicleLabel(model.name)}
|
||||
{model.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
Reference in New Issue
Block a user