feat: vehicle/equipment type system, equipment checklist support, and checklist overview redesign

This commit is contained in:
Matthias Hochmeister
2026-03-28 17:27:01 +01:00
parent 692093cc85
commit 6b46e97eb6
25 changed files with 2230 additions and 494 deletions

View File

@@ -1,6 +1,7 @@
import React, { useEffect, useState, useCallback } from 'react';
import {
Alert,
Autocomplete,
Box,
Button,
Chip,
@@ -60,6 +61,8 @@ import { vehiclesApi } from '../services/vehicles';
import GermanDateField from '../components/shared/GermanDateField';
import { fromGermanDate } from '../utils/dateInput';
import { equipmentApi } from '../services/equipment';
import { fahrzeugTypenApi } from '../services/fahrzeugTypen';
import type { FahrzeugTyp } from '../types/checklist.types';
import {
FahrzeugDetail as FahrzeugDetailType,
FahrzeugWartungslog,
@@ -187,9 +190,10 @@ interface UebersichtTabProps {
vehicle: FahrzeugDetailType;
onStatusUpdated: () => void;
canChangeStatus: boolean;
canEdit: boolean;
}
const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated, canChangeStatus }) => {
const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated, canChangeStatus, canEdit }) => {
const [statusDialogOpen, setStatusDialogOpen] = useState(false);
const [newStatus, setNewStatus] = useState<FahrzeugStatus>(vehicle.status);
const [bemerkung, setBemerkung] = useState(vehicle.status_bemerkung ?? '');
@@ -203,6 +207,43 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated,
const [saveError, setSaveError] = useState<string | null>(null);
const [overlappingBookings, setOverlappingBookings] = useState<OverlappingBooking[]>([]);
// ── Fahrzeugtypen ──
const [allTypes, setAllTypes] = useState<FahrzeugTyp[]>([]);
const [vehicleTypes, setVehicleTypes] = useState<FahrzeugTyp[]>([]);
const [typesLoading, setTypesLoading] = useState(true);
const [editingTypes, setEditingTypes] = useState(false);
const [selectedTypes, setSelectedTypes] = useState<FahrzeugTyp[]>([]);
const [typesSaving, setTypesSaving] = useState(false);
useEffect(() => {
let cancelled = false;
Promise.all([
fahrzeugTypenApi.getAll(),
fahrzeugTypenApi.getTypesForVehicle(vehicle.id),
])
.then(([all, assigned]) => {
if (cancelled) return;
setAllTypes(all);
setVehicleTypes(assigned);
})
.catch(() => {})
.finally(() => { if (!cancelled) setTypesLoading(false); });
return () => { cancelled = true; };
}, [vehicle.id]);
const handleSaveTypes = async () => {
try {
setTypesSaving(true);
await fahrzeugTypenApi.setTypesForVehicle(vehicle.id, selectedTypes.map((t) => t.id));
setVehicleTypes(selectedTypes);
setEditingTypes(false);
} catch {
// silent — keep dialog open
} finally {
setTypesSaving(false);
}
};
const isAusserDienst = (s: FahrzeugStatus) =>
s === FahrzeugStatus.AusserDienstWartung || s === FahrzeugStatus.AusserDienstSchaden;
@@ -342,6 +383,50 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated,
))}
</Grid>
{/* Fahrzeugtypen */}
<Typography variant="h6" sx={{ mt: 3, mb: 1.5 }}>
Fahrzeugtypen
</Typography>
{typesLoading ? (
<CircularProgress size={20} />
) : editingTypes ? (
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-start' }}>
<Autocomplete
multiple
options={allTypes}
getOptionLabel={(o) => o.name}
value={selectedTypes}
onChange={(_e, val) => setSelectedTypes(val)}
isOptionEqualToValue={(a, b) => a.id === b.id}
renderInput={(params) => <TextField {...params} label="Fahrzeugtypen" size="small" />}
sx={{ minWidth: 300, flexGrow: 1 }}
/>
<Button variant="contained" size="small" onClick={handleSaveTypes} disabled={typesSaving}>
{typesSaving ? <CircularProgress size={16} /> : 'Speichern'}
</Button>
<Button size="small" onClick={() => setEditingTypes(false)}>Abbrechen</Button>
</Box>
) : (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
{vehicleTypes.length === 0 ? (
<Typography variant="body2" color="text.disabled">Keine Typen zugewiesen</Typography>
) : (
vehicleTypes.map((t) => (
<Chip key={t.id} label={t.name} size="small" variant="outlined" />
))
)}
{canEdit && (
<IconButton
size="small"
onClick={() => { setSelectedTypes(vehicleTypes); setEditingTypes(true); }}
aria-label="Fahrzeugtypen bearbeiten"
>
<Edit fontSize="small" />
</IconButton>
)}
</Box>
)}
{/* Inspection deadline quick view */}
<Typography variant="h6" sx={{ mt: 3, mb: 1.5 }}>
Prüf- und Wartungsfristen
@@ -1047,6 +1132,7 @@ function FahrzeugDetail() {
vehicle={vehicle}
onStatusUpdated={fetchVehicle}
canChangeStatus={canChangeStatus}
canEdit={hasPermission('fahrzeuge:edit')}
/>
</TabPanel>