341 lines
11 KiB
TypeScript
341 lines
11 KiB
TypeScript
|
|
'use client';
|
||
|
|
|
||
|
|
import { useState } from 'react';
|
||
|
|
import { useRouter } from 'next/navigation';
|
||
|
|
import { Button } from '@/components/ui/Button';
|
||
|
|
import { Input } from '@/components/ui/Input';
|
||
|
|
import {
|
||
|
|
createVehicle,
|
||
|
|
updateVehicle,
|
||
|
|
type VehicleResponse,
|
||
|
|
type VehicleCreateData,
|
||
|
|
} from '@/lib/vehicles';
|
||
|
|
|
||
|
|
const VEHICLE_TYPES = ['lkw', 'pkw', 'baumaschine', 'stapler', 'transporter'];
|
||
|
|
const CONDITIONS = ['new', 'used'];
|
||
|
|
const AVAILABILITY = ['available', 'reserved', 'sold'];
|
||
|
|
const FUEL_TYPES = ['Diesel', 'Petrol', 'Electric', 'Hybrid', 'Gas'];
|
||
|
|
const TRANSMISSIONS = ['Manual', 'Automatic', 'Semi-Automatic'];
|
||
|
|
const HOURS_UNITS = ['h', 'min'];
|
||
|
|
|
||
|
|
interface VehicleFormProps {
|
||
|
|
vehicle?: VehicleResponse;
|
||
|
|
mode?: 'create' | 'edit';
|
||
|
|
}
|
||
|
|
|
||
|
|
interface FormErrors {
|
||
|
|
[key: string]: string | undefined;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function VehicleForm({ vehicle, mode = 'create' }: VehicleFormProps) {
|
||
|
|
const router = useRouter();
|
||
|
|
const [loading, setLoading] = useState(false);
|
||
|
|
const [errors, setErrors] = useState<FormErrors>({});
|
||
|
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
||
|
|
|
||
|
|
const [formData, setFormData] = useState<VehicleCreateData>({
|
||
|
|
make: vehicle?.make || '',
|
||
|
|
model: vehicle?.model || '',
|
||
|
|
fin: vehicle?.fin || '',
|
||
|
|
year: vehicle?.year,
|
||
|
|
first_registration: vehicle?.first_registration,
|
||
|
|
power_kw: vehicle?.power_kw,
|
||
|
|
fuel_type: vehicle?.fuel_type,
|
||
|
|
transmission: vehicle?.transmission,
|
||
|
|
color: vehicle?.color,
|
||
|
|
condition: vehicle?.condition || 'used',
|
||
|
|
location: vehicle?.location,
|
||
|
|
availability: vehicle?.availability || 'available',
|
||
|
|
price: vehicle?.price || 0,
|
||
|
|
vehicle_type: vehicle?.vehicle_type || 'lkw',
|
||
|
|
lkw_type: vehicle?.lkw_type,
|
||
|
|
machine_type: vehicle?.machine_type,
|
||
|
|
body_type: vehicle?.body_type,
|
||
|
|
operating_hours: vehicle?.operating_hours,
|
||
|
|
operating_hours_unit: vehicle?.operating_hours_unit,
|
||
|
|
mileage_km: vehicle?.mileage_km,
|
||
|
|
description: vehicle?.description,
|
||
|
|
});
|
||
|
|
|
||
|
|
const validate = (): boolean => {
|
||
|
|
const newErrors: FormErrors = {};
|
||
|
|
|
||
|
|
if (!formData.make || formData.make.trim().length === 0) {
|
||
|
|
newErrors.make = 'Make is required';
|
||
|
|
}
|
||
|
|
if (!formData.model || formData.model.trim().length === 0) {
|
||
|
|
newErrors.model = 'Model is required';
|
||
|
|
}
|
||
|
|
if (!formData.fin) {
|
||
|
|
newErrors.fin = 'FIN is required';
|
||
|
|
} else if (formData.fin.length !== 17) {
|
||
|
|
newErrors.fin = 'FIN must be exactly 17 characters';
|
||
|
|
}
|
||
|
|
if (!formData.price || formData.price <= 0) {
|
||
|
|
newErrors.price = 'Price must be greater than 0';
|
||
|
|
}
|
||
|
|
if (!formData.vehicle_type) {
|
||
|
|
newErrors.vehicle_type = 'Vehicle type is required';
|
||
|
|
}
|
||
|
|
|
||
|
|
setErrors(newErrors);
|
||
|
|
return Object.keys(newErrors).length === 0;
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleChange = (field: keyof VehicleCreateData, value: string | number | undefined) => {
|
||
|
|
setFormData(prev => ({ ...prev, [field]: value }));
|
||
|
|
if (errors[field]) {
|
||
|
|
setErrors(prev => { const next = { ...prev }; delete next[field]; return next; });
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
||
|
|
e.preventDefault();
|
||
|
|
if (!validate()) return;
|
||
|
|
|
||
|
|
setLoading(true);
|
||
|
|
setSubmitError(null);
|
||
|
|
try {
|
||
|
|
if (mode === 'edit' && vehicle) {
|
||
|
|
await updateVehicle(vehicle.id, formData);
|
||
|
|
router.push(`/de/fahrzeuge/${vehicle.id}`);
|
||
|
|
} else {
|
||
|
|
const created = await createVehicle(formData);
|
||
|
|
router.push(`/de/fahrzeuge/${created.id}`);
|
||
|
|
}
|
||
|
|
} catch (err: unknown) {
|
||
|
|
const apiErr = err as { error?: { message?: string } };
|
||
|
|
setSubmitError(apiErr?.error?.message || 'Failed to save vehicle');
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const inputClass = 'w-full px-3 py-2 border rounded-lg bg-surface text-text border-border focus:outline-none focus:ring-2 focus:ring-primary';
|
||
|
|
|
||
|
|
return (
|
||
|
|
<form data-testid="vehicle-form" onSubmit={handleSubmit} className="space-y-6">
|
||
|
|
{submitError && (
|
||
|
|
<div data-testid="form-submit-error" className="p-4 bg-error/10 text-error rounded-lg">
|
||
|
|
{submitError}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="grid grid-cols-2 gap-4">
|
||
|
|
<Input
|
||
|
|
label="Make *"
|
||
|
|
data-testid="input-make"
|
||
|
|
value={formData.make}
|
||
|
|
onChange={e => handleChange('make', e.target.value)}
|
||
|
|
error={errors.make}
|
||
|
|
/>
|
||
|
|
<Input
|
||
|
|
label="Model *"
|
||
|
|
data-testid="input-model"
|
||
|
|
value={formData.model}
|
||
|
|
onChange={e => handleChange('model', e.target.value)}
|
||
|
|
error={errors.model}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-2 gap-4">
|
||
|
|
<Input
|
||
|
|
label="FIN * (17 characters)"
|
||
|
|
data-testid="input-fin"
|
||
|
|
value={formData.fin}
|
||
|
|
maxLength={17}
|
||
|
|
onChange={e => handleChange('fin', e.target.value.toUpperCase())}
|
||
|
|
error={errors.fin}
|
||
|
|
/>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-text mb-1">Vehicle Type *</label>
|
||
|
|
<select
|
||
|
|
data-testid="input-vehicle_type"
|
||
|
|
className={inputClass}
|
||
|
|
value={formData.vehicle_type}
|
||
|
|
onChange={e => handleChange('vehicle_type', e.target.value)}
|
||
|
|
>
|
||
|
|
{VEHICLE_TYPES.map(t => <option key={t} value={t}>{t}</option>)}
|
||
|
|
</select>
|
||
|
|
{errors.vehicle_type && <p className="mt-1 text-sm text-error">{errors.vehicle_type}</p>}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-2 gap-4">
|
||
|
|
<Input
|
||
|
|
label="Price (EUR) *"
|
||
|
|
data-testid="input-price"
|
||
|
|
type="number"
|
||
|
|
step="0.01"
|
||
|
|
min="0"
|
||
|
|
value={formData.price || ''}
|
||
|
|
onChange={e => handleChange('price', parseFloat(e.target.value) || 0)}
|
||
|
|
error={errors.price}
|
||
|
|
/>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-text mb-1">Availability</label>
|
||
|
|
<select
|
||
|
|
data-testid="input-availability"
|
||
|
|
className={inputClass}
|
||
|
|
value={formData.availability}
|
||
|
|
onChange={e => handleChange('availability', e.target.value)}
|
||
|
|
>
|
||
|
|
{AVAILABILITY.map(a => <option key={a} value={a}>{a}</option>)}
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-3 gap-4">
|
||
|
|
<Input
|
||
|
|
label="Year"
|
||
|
|
data-testid="input-year"
|
||
|
|
type="number"
|
||
|
|
min="1900"
|
||
|
|
max="2100"
|
||
|
|
value={formData.year || ''}
|
||
|
|
onChange={e => handleChange('year', e.target.value ? parseInt(e.target.value) : undefined)}
|
||
|
|
/>
|
||
|
|
<Input
|
||
|
|
label="First Registration"
|
||
|
|
data-testid="input-first_registration"
|
||
|
|
type="date"
|
||
|
|
value={formData.first_registration || ''}
|
||
|
|
onChange={e => handleChange('first_registration', e.target.value || undefined)}
|
||
|
|
/>
|
||
|
|
<Input
|
||
|
|
label="Power (kW)"
|
||
|
|
data-testid="input-power_kw"
|
||
|
|
type="number"
|
||
|
|
min="0"
|
||
|
|
value={formData.power_kw || ''}
|
||
|
|
onChange={e => handleChange('power_kw', e.target.value ? parseInt(e.target.value) : undefined)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-3 gap-4">
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-text mb-1">Fuel Type</label>
|
||
|
|
<select
|
||
|
|
data-testid="input-fuel_type"
|
||
|
|
className={inputClass}
|
||
|
|
value={formData.fuel_type || ''}
|
||
|
|
onChange={e => handleChange('fuel_type', e.target.value || undefined)}
|
||
|
|
>
|
||
|
|
<option value="">—</option>
|
||
|
|
{FUEL_TYPES.map(f => <option key={f} value={f}>{f}</option>)}
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-text mb-1">Transmission</label>
|
||
|
|
<select
|
||
|
|
data-testid="input-transmission"
|
||
|
|
className={inputClass}
|
||
|
|
value={formData.transmission || ''}
|
||
|
|
onChange={e => handleChange('transmission', e.target.value || undefined)}
|
||
|
|
>
|
||
|
|
<option value="">—</option>
|
||
|
|
{TRANSMISSIONS.map(t => <option key={t} value={t}>{t}</option>)}
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-text mb-1">Condition</label>
|
||
|
|
<select
|
||
|
|
data-testid="input-condition"
|
||
|
|
className={inputClass}
|
||
|
|
value={formData.condition}
|
||
|
|
onChange={e => handleChange('condition', e.target.value)}
|
||
|
|
>
|
||
|
|
{CONDITIONS.map(c => <option key={c} value={c}>{c}</option>)}
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="grid grid-cols-2 gap-4">
|
||
|
|
<Input
|
||
|
|
label="Color"
|
||
|
|
data-testid="input-color"
|
||
|
|
value={formData.color || ''}
|
||
|
|
onChange={e => handleChange('color', e.target.value || undefined)}
|
||
|
|
/>
|
||
|
|
<Input
|
||
|
|
label="Location"
|
||
|
|
data-testid="input-location"
|
||
|
|
value={formData.location || ''}
|
||
|
|
onChange={e => handleChange('location', e.target.value || undefined)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{formData.vehicle_type === 'lkw' && (
|
||
|
|
<Input
|
||
|
|
label="LKW Type"
|
||
|
|
data-testid="input-lkw_type"
|
||
|
|
value={formData.lkw_type || ''}
|
||
|
|
onChange={e => handleChange('lkw_type', e.target.value || undefined)}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{formData.vehicle_type === 'baumaschine' && (
|
||
|
|
<Input
|
||
|
|
label="Machine Type"
|
||
|
|
data-testid="input-machine_type"
|
||
|
|
value={formData.machine_type || ''}
|
||
|
|
onChange={e => handleChange('machine_type', e.target.value || undefined)}
|
||
|
|
/>
|
||
|
|
)}
|
||
|
|
|
||
|
|
<div className="grid grid-cols-2 gap-4">
|
||
|
|
<Input
|
||
|
|
label="Mileage (km)"
|
||
|
|
data-testid="input-mileage_km"
|
||
|
|
type="number"
|
||
|
|
min="0"
|
||
|
|
value={formData.mileage_km || ''}
|
||
|
|
onChange={e => handleChange('mileage_km', e.target.value ? parseInt(e.target.value) : undefined)}
|
||
|
|
/>
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-text mb-1">Operating Hours</label>
|
||
|
|
<div className="flex gap-2">
|
||
|
|
<Input
|
||
|
|
data-testid="input-operating_hours"
|
||
|
|
type="number"
|
||
|
|
step="0.1"
|
||
|
|
min="0"
|
||
|
|
value={formData.operating_hours || ''}
|
||
|
|
onChange={e => handleChange('operating_hours', e.target.value ? parseFloat(e.target.value) : undefined)}
|
||
|
|
/>
|
||
|
|
<select
|
||
|
|
data-testid="input-operating_hours_unit"
|
||
|
|
className={inputClass}
|
||
|
|
style={{ maxWidth: '80px' }}
|
||
|
|
value={formData.operating_hours_unit || 'h'}
|
||
|
|
onChange={e => handleChange('operating_hours_unit', e.target.value || undefined)}
|
||
|
|
>
|
||
|
|
{HOURS_UNITS.map(u => <option key={u} value={u}>{u}</option>)}
|
||
|
|
</select>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div>
|
||
|
|
<label className="block text-sm font-medium text-text mb-1">Description</label>
|
||
|
|
<textarea
|
||
|
|
data-testid="input-description"
|
||
|
|
className={inputClass}
|
||
|
|
rows={4}
|
||
|
|
value={formData.description || ''}
|
||
|
|
onChange={e => handleChange('description', e.target.value || undefined)}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="flex gap-3">
|
||
|
|
<Button type="submit" loading={loading} data-testid="submit-button">
|
||
|
|
{mode === 'edit' ? 'Update Vehicle' : 'Create Vehicle'}
|
||
|
|
</Button>
|
||
|
|
<Button type="button" variant="ghost" onClick={() => router.back()}>
|
||
|
|
Cancel
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</form>
|
||
|
|
);
|
||
|
|
}
|