Bug Fixes
This commit is contained in:
@@ -30,25 +30,8 @@ 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', {
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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) {
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user