Very minimal MVP
This commit is contained in:
@@ -3,7 +3,18 @@
|
||||
*/
|
||||
|
||||
import { apiClient } from '../../../core/api/client';
|
||||
import { Vehicle, CreateVehicleRequest, UpdateVehicleRequest } from '../types/vehicles.types';
|
||||
import axios from 'axios';
|
||||
import { Vehicle, CreateVehicleRequest, UpdateVehicleRequest, DropdownOption } from '../types/vehicles.types';
|
||||
|
||||
// Unauthenticated client for dropdown data
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api';
|
||||
const dropdownClient = axios.create({
|
||||
baseURL: API_BASE_URL,
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
export const vehiclesApi = {
|
||||
getAll: async (): Promise<Vehicle[]> => {
|
||||
@@ -29,4 +40,30 @@ export const vehiclesApi = {
|
||||
delete: async (id: string): Promise<void> => {
|
||||
await apiClient.delete(`/vehicles/${id}`);
|
||||
},
|
||||
|
||||
// Dropdown API methods (unauthenticated)
|
||||
getMakes: async (): Promise<DropdownOption[]> => {
|
||||
const response = await dropdownClient.get('/vehicles/dropdown/makes');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getModels: async (make: string): Promise<DropdownOption[]> => {
|
||||
const response = await dropdownClient.get(`/vehicles/dropdown/models/${encodeURIComponent(make)}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getTransmissions: async (): Promise<DropdownOption[]> => {
|
||||
const response = await dropdownClient.get('/vehicles/dropdown/transmissions');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getEngines: async (): Promise<DropdownOption[]> => {
|
||||
const response = await dropdownClient.get('/vehicles/dropdown/engines');
|
||||
return response.data;
|
||||
},
|
||||
|
||||
getTrims: async (): Promise<DropdownOption[]> => {
|
||||
const response = await dropdownClient.get('/vehicles/dropdown/trims');
|
||||
return response.data;
|
||||
},
|
||||
};
|
||||
@@ -1,16 +1,24 @@
|
||||
/**
|
||||
* @ai-summary Vehicle form component for create/edit
|
||||
* @ai-summary Vehicle form component for create/edit with dropdown cascades
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState, useEffect } 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 { CreateVehicleRequest, DropdownOption } from '../types/vehicles.types';
|
||||
import { vehiclesApi } from '../api/vehicles.api';
|
||||
|
||||
const vehicleSchema = z.object({
|
||||
vin: z.string().length(17, 'VIN must be exactly 17 characters'),
|
||||
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(),
|
||||
@@ -30,15 +38,74 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
|
||||
initialData,
|
||||
loading,
|
||||
}) => {
|
||||
const [makes, setMakes] = useState<DropdownOption[]>([]);
|
||||
const [models, setModels] = useState<DropdownOption[]>([]);
|
||||
const [transmissions, setTransmissions] = useState<DropdownOption[]>([]);
|
||||
const [engines, setEngines] = useState<DropdownOption[]>([]);
|
||||
const [trims, setTrims] = useState<DropdownOption[]>([]);
|
||||
const [selectedMake, setSelectedMake] = useState<string>('');
|
||||
const [loadingDropdowns, setLoadingDropdowns] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
watch,
|
||||
setValue,
|
||||
} = useForm<CreateVehicleRequest>({
|
||||
resolver: zodResolver(vehicleSchema),
|
||||
defaultValues: initialData,
|
||||
});
|
||||
|
||||
const watchedMake = watch('make');
|
||||
|
||||
// Load dropdown data on component mount
|
||||
useEffect(() => {
|
||||
const loadInitialData = async () => {
|
||||
setLoadingDropdowns(true);
|
||||
try {
|
||||
const [makesData, transmissionsData, enginesData, trimsData] = await Promise.all([
|
||||
vehiclesApi.getMakes(),
|
||||
vehiclesApi.getTransmissions(),
|
||||
vehiclesApi.getEngines(),
|
||||
vehiclesApi.getTrims(),
|
||||
]);
|
||||
|
||||
setMakes(makesData);
|
||||
setTransmissions(transmissionsData);
|
||||
setEngines(enginesData);
|
||||
setTrims(trimsData);
|
||||
} catch (error) {
|
||||
console.error('Failed to load dropdown data:', error);
|
||||
} finally {
|
||||
setLoadingDropdowns(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadInitialData();
|
||||
}, []);
|
||||
|
||||
// Load models when make changes
|
||||
useEffect(() => {
|
||||
if (watchedMake && watchedMake !== selectedMake) {
|
||||
const loadModels = async () => {
|
||||
try {
|
||||
const modelsData = await vehiclesApi.getModels(watchedMake);
|
||||
setModels(modelsData);
|
||||
setSelectedMake(watchedMake);
|
||||
|
||||
// Clear model selection when make changes
|
||||
setValue('model', '');
|
||||
} catch (error) {
|
||||
console.error('Failed to load models:', error);
|
||||
setModels([]);
|
||||
}
|
||||
};
|
||||
|
||||
loadModels();
|
||||
}
|
||||
}, [watchedMake, selectedMake, setValue]);
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div>
|
||||
@@ -55,6 +122,101 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Vehicle Specification Dropdowns */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Make
|
||||
</label>
|
||||
<select
|
||||
{...register('make')}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
disabled={loadingDropdowns}
|
||||
>
|
||||
<option value="">Select Make</option>
|
||||
{makes.map((make) => (
|
||||
<option key={make.id} value={make.name}>
|
||||
{make.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Model
|
||||
</label>
|
||||
<select
|
||||
{...register('model')}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
disabled={!watchedMake || models.length === 0}
|
||||
>
|
||||
<option value="">Select Model</option>
|
||||
{models.map((model) => (
|
||||
<option key={model.id} value={model.name}>
|
||||
{model.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Engine
|
||||
</label>
|
||||
<select
|
||||
{...register('engine')}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
disabled={loadingDropdowns}
|
||||
>
|
||||
<option value="">Select Engine</option>
|
||||
{engines.map((engine) => (
|
||||
<option key={engine.id} value={engine.name}>
|
||||
{engine.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Transmission
|
||||
</label>
|
||||
<select
|
||||
{...register('transmission')}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
disabled={loadingDropdowns}
|
||||
>
|
||||
<option value="">Select Transmission</option>
|
||||
{transmissions.map((transmission) => (
|
||||
<option key={transmission.id} value={transmission.name}>
|
||||
{transmission.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Trim Level
|
||||
</label>
|
||||
<select
|
||||
{...register('trimLevel')}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
disabled={loadingDropdowns}
|
||||
>
|
||||
<option value="">Select Trim</option>
|
||||
{trims.map((trim) => (
|
||||
<option key={trim.id} value={trim.name}>
|
||||
{trim.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nickname
|
||||
@@ -106,7 +268,7 @@ export const VehicleForm: React.FC<VehicleFormProps> = ({
|
||||
<Button variant="secondary" onClick={onCancel} type="button">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" loading={loading}>
|
||||
<Button type="submit" loading={loading || loadingDropdowns}>
|
||||
{initialData ? 'Update Vehicle' : 'Add Vehicle'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,11 @@ export interface Vehicle {
|
||||
make?: string;
|
||||
model?: string;
|
||||
year?: number;
|
||||
engine?: string;
|
||||
transmission?: string;
|
||||
trimLevel?: string;
|
||||
driveType?: string;
|
||||
fuelType?: string;
|
||||
nickname?: string;
|
||||
color?: string;
|
||||
licensePlate?: string;
|
||||
@@ -20,6 +25,13 @@ export interface Vehicle {
|
||||
|
||||
export interface CreateVehicleRequest {
|
||||
vin: string;
|
||||
make?: string;
|
||||
model?: string;
|
||||
engine?: string;
|
||||
transmission?: string;
|
||||
trimLevel?: string;
|
||||
driveType?: string;
|
||||
fuelType?: string;
|
||||
nickname?: string;
|
||||
color?: string;
|
||||
licensePlate?: string;
|
||||
@@ -27,8 +39,20 @@ export interface CreateVehicleRequest {
|
||||
}
|
||||
|
||||
export interface UpdateVehicleRequest {
|
||||
make?: string;
|
||||
model?: string;
|
||||
engine?: string;
|
||||
transmission?: string;
|
||||
trimLevel?: string;
|
||||
driveType?: string;
|
||||
fuelType?: string;
|
||||
nickname?: string;
|
||||
color?: string;
|
||||
licensePlate?: string;
|
||||
odometerReading?: number;
|
||||
}
|
||||
|
||||
export interface DropdownOption {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
Reference in New Issue
Block a user