Files
erp-nutzfahrzeuge/frontend/components/vehicles/VehicleForm.tsx
T

341 lines
11 KiB
TypeScript
Raw Normal View History

'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>
);
}