import React, { useEffect, useState, useCallback } from 'react'; import { Alert, Box, Button, Chip, CircularProgress, Container, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, FormControl, Grid, IconButton, InputLabel, Link, MenuItem, Paper, Select, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Tooltip, Typography, } from '@mui/material'; import { Add, ArrowBack, Assignment, Build, CheckCircle, DeleteOutline, Edit, Error as ErrorIcon, History, LocalFireDepartment, MoreHoriz, PauseCircle, ReportProblem, School, Star, Verified, Warning, } from '@mui/icons-material'; import { useNavigate, useParams } from 'react-router-dom'; import DashboardLayout from '../components/dashboard/DashboardLayout'; import { DetailLayout } from '../components/templates'; import ChatAwareFab from '../components/shared/ChatAwareFab'; import { vehiclesApi } from '../services/vehicles'; import GermanDateField from '../components/shared/GermanDateField'; import { fromGermanDate } from '../utils/dateInput'; import { equipmentApi } from '../services/equipment'; import { FahrzeugDetail as FahrzeugDetailType, FahrzeugWartungslog, FahrzeugStatus, FahrzeugStatusLabel, CreateWartungslogPayload, UpdateWartungslogPayload, UpdateStatusPayload, WartungslogArt, WartungslogErgebnis, WartungslogErgebnisLabel, WartungslogErgebnisColor, OverlappingBooking, } from '../types/vehicle.types'; import type { AusruestungListItem } from '../types/equipment.types'; import { AusruestungStatus, AusruestungStatusLabel } from '../types/equipment.types'; import { usePermissions } from '../hooks/usePermissions'; import { usePermissionContext } from '../contexts/PermissionContext'; import { useNotification } from '../contexts/NotificationContext'; import FahrzeugChecklistTab from '../components/fahrzeuge/FahrzeugChecklistTab'; // ── Status config ───────────────────────────────────────────────────────────── const STATUS_ICONS: Record = { [FahrzeugStatus.Einsatzbereit]: , [FahrzeugStatus.AusserDienstWartung]: , [FahrzeugStatus.AusserDienstSchaden]: , }; const STATUS_CHIP_COLOR: Record = { [FahrzeugStatus.Einsatzbereit]: 'success', [FahrzeugStatus.AusserDienstWartung]: 'warning', [FahrzeugStatus.AusserDienstSchaden]: 'error', }; // ── Date helpers ────────────────────────────────────────────────────────────── function fmtDate(iso: string | null | undefined): string { if (!iso) return '—'; return new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', }); } function inspectionBadgeColor(tage: number | null): 'success' | 'warning' | 'error' | 'default' { if (tage === null) return 'default'; if (tage < 0) return 'error'; if (tage <= 30) return 'warning'; return 'success'; } function fmtDatetime(iso: string | Date | null | undefined): string { return fmtDate(iso ? new Date(iso).toISOString() : null); } // ── Status History Section ──────────────────────────────────────────────────── const StatusHistorySection: React.FC<{ vehicleId: string }> = ({ vehicleId }) => { const [history, setHistory] = useState<{ alter_status: string; neuer_status: string; bemerkung?: string; geaendert_von_name?: string; erstellt_am: string }[]>([]); const [loading, setLoading] = useState(true); useEffect(() => { vehiclesApi.getStatusHistory(vehicleId) .then(setHistory) .catch(() => setHistory([])) .finally(() => setLoading(false)); }, [vehicleId]); if (loading || history.length === 0) return null; return ( <> Status-Historie Datum Von Nach Bemerkung Geändert von {history.map((h, idx) => ( {fmtDatetime(h.erstellt_am)} {h.bemerkung || '—'} {h.geaendert_von_name || '—'} ))}
); }; // ── Übersicht Tab ───────────────────────────────────────────────────────────── interface UebersichtTabProps { vehicle: FahrzeugDetailType; onStatusUpdated: () => void; canChangeStatus: boolean; canEdit: boolean; } const UebersichtTab: React.FC = ({ vehicle, onStatusUpdated, canChangeStatus, canEdit: _canEdit }) => { const [statusDialogOpen, setStatusDialogOpen] = useState(false); const [newStatus, setNewStatus] = useState(vehicle.status); const [bemerkung, setBemerkung] = useState(vehicle.status_bemerkung ?? ''); const [ausserDienstVon, setAusserDienstVon] = useState( vehicle.ausser_dienst_von ? new Date(vehicle.ausser_dienst_von).toISOString().slice(0, 16) : '' ); const [ausserDienstBis, setAusserDienstBis] = useState( vehicle.ausser_dienst_bis ? new Date(vehicle.ausser_dienst_bis).toISOString().slice(0, 16) : '' ); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); const [overlappingBookings, setOverlappingBookings] = useState([]); const isAusserDienst = (s: FahrzeugStatus) => s === FahrzeugStatus.AusserDienstWartung || s === FahrzeugStatus.AusserDienstSchaden; const openDialog = () => { setNewStatus(vehicle.status); setBemerkung(''); setAusserDienstVon( vehicle.ausser_dienst_von ? new Date(vehicle.ausser_dienst_von).toISOString().slice(0, 16) : '' ); setAusserDienstBis( vehicle.ausser_dienst_bis ? new Date(vehicle.ausser_dienst_bis).toISOString().slice(0, 16) : '' ); setSaveError(null); setOverlappingBookings([]); setStatusDialogOpen(true); }; const closeDialog = () => { setSaveError(null); setStatusDialogOpen(false); }; const handleSaveStatus = async () => { try { setSaving(true); setSaveError(null); const payload: UpdateStatusPayload = { status: newStatus, bemerkung, ...(isAusserDienst(newStatus) && ausserDienstVon && ausserDienstBis ? { ausserDienstVon: new Date(ausserDienstVon).toISOString(), ausserDienstBis: new Date(ausserDienstBis).toISOString(), } : {}), }; const result = await vehiclesApi.updateStatus(vehicle.id, payload); setStatusDialogOpen(false); setOverlappingBookings(result.overlappingBookings ?? []); onStatusUpdated(); } catch (err: any) { setSaveError(err?.response?.data?.message || 'Status konnte nicht gespeichert werden.'); } finally { setSaving(false); } }; const canSave = !isAusserDienst(newStatus) || (!!ausserDienstVon && !!ausserDienstBis); const isSchaden = vehicle.status === FahrzeugStatus.AusserDienstSchaden; // Inspection deadline badges const inspItems: { label: string; faelligAm: string | null; tage: number | null }[] = [ { label: '§57a Periodische Prüfung', faelligAm: vehicle.paragraph57a_faellig_am, tage: vehicle.paragraph57a_tage_bis_faelligkeit }, { label: 'Nächste Wartung / Service', faelligAm: vehicle.naechste_wartung_am, tage: vehicle.wartung_tage_bis_faelligkeit }, ]; return ( {isSchaden && ( } sx={{ mb: 2 }}> Schaden gemeldet — dieses Fahrzeug ist nicht einsatzbereit. {vehicle.status_bemerkung && ` Bemerkung: ${vehicle.status_bemerkung}`} )} {overlappingBookings.length > 0 && ( Folgende Buchungen überschneiden sich mit dem Außer-Dienst-Zeitraum: {overlappingBookings.map((b) => (
  • {b.titel} · {fmtDate(b.beginn as unknown as string)} – {fmtDate(b.ende as unknown as string)} · {b.gebucht_von_name}
  • ))}
    Bitte prüfe, ob diese storniert werden müssen.
    )} {/* Status panel */} {STATUS_ICONS[vehicle.status]} Aktueller Status {vehicle.aktiver_lehrgang && ( } label="In Lehrgang" color="info" size="small" /> )} {(vehicle.status === FahrzeugStatus.AusserDienstWartung || vehicle.status === FahrzeugStatus.AusserDienstSchaden) && vehicle.ausser_dienst_von && vehicle.ausser_dienst_bis && ( Außer Dienst: {fmtDatetime(vehicle.ausser_dienst_von)} – {fmtDatetime(vehicle.ausser_dienst_bis)} (geschätzt) )} {vehicle.status_bemerkung && ( {vehicle.status_bemerkung} )} {canChangeStatus && ( )} {/* Vehicle data grid */} {[ { label: 'Bezeichnung', value: vehicle.bezeichnung }, { label: 'Kurzname', value: vehicle.kurzname }, { label: 'Kennzeichen', value: vehicle.amtliches_kennzeichen }, ].map(({ label, value }) => ( {label} {value ?? '—'} ))} {/* Inspection deadline quick view */} Prüf- und Wartungsfristen {inspItems.map(({ label, faelligAm, tage }) => { const color = inspectionBadgeColor(tage); return ( {label} {faelligAm ? ( <> : undefined} sx={{ mt: 0.5 }} /> {tage !== null && tage >= 0 && ( in {tage} Tagen )} ) : ( Kein Datum erfasst )} ); })} {/* Status history */} {/* Status change dialog */} Fahrzeugstatus ändern {saveError && {saveError}} Neuer Status {isAusserDienst(newStatus) && ( <> setAusserDienstVon(iso)} /> setAusserDienstBis(iso)} /> Zeitangabe ist eine Schätzung )} setBemerkung(e.target.value)} placeholder="z.B. Fahrzeug in Werkstatt, voraussichtlich ab 01.03. wieder einsatzbereit" />
    ); }; // ── Wartung Tab ─────────────────────────────────────────────────────────────── interface WartungTabProps { fahrzeugId: string; wartungslog: FahrzeugWartungslog[]; onAdded: () => void; canWrite: boolean; } const WARTUNG_ART_ICONS: Record = { '§57a Prüfung': , 'Service': , 'Sonstiges': , default: , }; const WartungTab: React.FC = ({ fahrzeugId, wartungslog, onAdded, canWrite }) => { const [dialogOpen, setDialogOpen] = useState(false); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); const [editingWartungId, setEditingWartungId] = useState(null); const emptyForm: CreateWartungslogPayload = { datum: '', art: undefined, beschreibung: '', km_stand: undefined, kraftstoff_liter: undefined, kosten: undefined, externe_werkstatt: '', ergebnis: undefined, naechste_faelligkeit: '', }; const [form, setForm] = useState(emptyForm); const openCreateDialog = () => { setEditingWartungId(null); setForm(emptyForm); setSaveError(null); setDialogOpen(true); }; const openEditDialog = (entry: FahrzeugWartungslog) => { setEditingWartungId(entry.id); setForm({ datum: entry.datum ? new Date(entry.datum).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) : '', art: entry.art ?? undefined, beschreibung: entry.beschreibung, km_stand: entry.km_stand ?? undefined, kraftstoff_liter: entry.kraftstoff_liter ?? undefined, kosten: entry.kosten ?? undefined, externe_werkstatt: entry.externe_werkstatt ?? '', ergebnis: entry.ergebnis ?? undefined, naechste_faelligkeit: entry.naechste_faelligkeit ? entry.naechste_faelligkeit.slice(0, 10) : '', }); setSaveError(null); setDialogOpen(true); }; const handleSubmit = async () => { if (!form.datum || !form.beschreibung.trim()) { setSaveError('Datum und Beschreibung sind erforderlich.'); return; } try { setSaving(true); setSaveError(null); const isoDate = fromGermanDate(form.datum) || form.datum; if (editingWartungId) { const payload: UpdateWartungslogPayload = { datum: isoDate, art: form.art, beschreibung: form.beschreibung, km_stand: form.km_stand, externe_werkstatt: form.externe_werkstatt || undefined, ergebnis: form.ergebnis, naechste_faelligkeit: form.naechste_faelligkeit || undefined, }; await vehiclesApi.updateWartungslog(fahrzeugId, editingWartungId, payload); } else { await vehiclesApi.addWartungslog(fahrzeugId, { ...form, datum: isoDate, externe_werkstatt: form.externe_werkstatt || undefined, naechste_faelligkeit: form.naechste_faelligkeit || undefined, }); } setDialogOpen(false); setForm(emptyForm); setEditingWartungId(null); onAdded(); } catch { setSaveError('Wartungseintrag konnte nicht gespeichert werden.'); } finally { setSaving(false); } }; return ( {wartungslog.length === 0 ? ( Noch keine Wartungseinträge erfasst. ) : ( } spacing={0}> {wartungslog.map((entry) => { const artIcon = WARTUNG_ART_ICONS[entry.art ?? ''] ?? WARTUNG_ART_ICONS.default; return ( {artIcon} {fmtDate(entry.datum)} {entry.art && } {entry.ergebnis && ( )} {entry.beschreibung} {[ entry.km_stand != null && `${entry.km_stand.toLocaleString('de-DE')} km`, entry.externe_werkstatt && entry.externe_werkstatt, entry.naechste_faelligkeit && `Nächste Fälligkeit: ${fmtDate(entry.naechste_faelligkeit)}`, ].filter(Boolean).join(' · ')} {entry.dokument_url ? ( ) : canWrite ? ( ) : null} {canWrite && ( openEditDialog(entry)} aria-label="Bearbeiten"> )} ); })} )} {canWrite && ( )} setDialogOpen(false)} maxWidth="sm" fullWidth> {editingWartungId ? 'Wartungseintrag bearbeiten' : 'Wartung / Service eintragen'} {saveError && {saveError}} setForm((f) => ({ ...f, datum: e.target.value }))} InputLabelProps={{ shrink: true }} /> Art setForm((f) => ({ ...f, beschreibung: e.target.value }))} /> setForm((f) => ({ ...f, km_stand: e.target.value ? Number(e.target.value) : undefined }))} inputProps={{ min: 0 }} /> setForm((f) => ({ ...f, externe_werkstatt: e.target.value }))} placeholder="Name der Werkstatt (wenn extern)" /> Ergebnis setForm((f) => ({ ...f, naechste_faelligkeit: iso }))} /> ); }; // ── Ausrüstung Tab ─────────────────────────────────────────────────────────── const EQUIPMENT_STATUS_COLOR: Record = { [AusruestungStatus.Einsatzbereit]: 'success', [AusruestungStatus.Beschaedigt]: 'error', [AusruestungStatus.InWartung]: 'warning', [AusruestungStatus.AusserDienst]: 'default', }; function pruefungBadgeColor(tage: number | null): 'success' | 'warning' | 'error' | 'default' { if (tage === null) return 'default'; if (tage < 0) return 'error'; if (tage <= 30) return 'warning'; return 'success'; } interface AusruestungTabProps { equipment: AusruestungListItem[]; vehicleId: string; } const AusruestungTab: React.FC = ({ equipment, vehicleId: _vehicleId }) => { const navigate = useNavigate(); const hasProblems = equipment.some( (e) => e.status === AusruestungStatus.Beschaedigt || e.status === AusruestungStatus.InWartung ); if (equipment.length === 0) { return ( Keine Ausrüstung zugewiesen Diesem Fahrzeug ist derzeit keine Ausrüstung zugeordnet. ); } return ( {hasProblems && ( } sx={{ mb: 2 }}> Achtung: Eine oder mehrere Ausrüstungen dieses Fahrzeugs sind beschädigt oder in Wartung. )} Bezeichnung Kategorie Status Wichtig Nächste Prüfung {equipment.map((item) => { const statusColor = EQUIPMENT_STATUS_COLOR[item.status] ?? 'default'; const pruefTage = item.pruefung_tage_bis_faelligkeit; const pruefColor = pruefungBadgeColor(pruefTage); return ( navigate(`/ausruestung/${item.id}`)} sx={{ textAlign: 'left' }} > {item.bezeichnung} {item.ist_wichtig && ( )} {item.naechste_pruefung_am ? ( : undefined} /> ) : ( )} ); })}
    ); }; // ── Main Page ───────────────────────────────────────────────────────────────── function FahrzeugDetail() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { isAdmin, canChangeStatus, canManageMaintenance } = usePermissions(); const { hasPermission } = usePermissionContext(); const notification = useNotification(); const [vehicle, setVehicle] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false); const [vehicleEquipment, setVehicleEquipment] = useState([]); const fetchVehicle = useCallback(async () => { if (!id) return; try { setLoading(true); setError(null); const data = await vehiclesApi.getById(id); setVehicle(data); // Fetch equipment separately — failure must not break the page try { const eq = await equipmentApi.getByVehicle(id); setVehicleEquipment(eq); } catch { setVehicleEquipment([]); } } catch { setError('Fahrzeug konnte nicht geladen werden.'); } finally { setLoading(false); } }, [id]); useEffect(() => { fetchVehicle(); }, [fetchVehicle]); const handleDeleteVehicle = async () => { if (!id) return; try { setDeleteLoading(true); await vehiclesApi.delete(id); notification.showSuccess('Fahrzeug wurde erfolgreich gelöscht.'); navigate('/fahrzeuge'); } catch { notification.showError('Fahrzeug konnte nicht gelöscht werden.'); setDeleteDialogOpen(false); setDeleteLoading(false); } }; if (loading) { return ( ); } if (error || !vehicle) { return ( {error ?? 'Fahrzeug nicht gefunden.'} ); } const hasOverdue = (vehicle.paragraph57a_tage_bis_faelligkeit !== null && vehicle.paragraph57a_tage_bis_faelligkeit < 0) || (vehicle.wartung_tage_bis_faelligkeit !== null && vehicle.wartung_tage_bis_faelligkeit < 0); const titleText = vehicle.kurzname ? `${vehicle.bezeichnung} ${vehicle.kurzname}` : vehicle.bezeichnung; const tabs = [ { label: 'Übersicht', content: ( ), }, { label: hasOverdue ? ( Wartung ) : 'Wartung', content: ( ), }, { label: 'Einsätze', content: ( Einsatzhistorie Die Einsatzverknüpfung wird in Tier 2 (Einsatzverwaltung) implementiert. ), }, { label: `Ausrüstung${vehicleEquipment.length > 0 ? ` (${vehicleEquipment.length})` : ''}`, content: , }, ...(hasPermission('checklisten:view') ? [{ label: 'Checklisten', content: , }] : []), ]; return ( {isAdmin && ( navigate(`/fahrzeuge/${vehicle.id}/bearbeiten`)} aria-label="Fahrzeug bearbeiten" > )} {isAdmin && ( setDeleteDialogOpen(true)} aria-label="Fahrzeug löschen" > )} } /> {/* Delete confirmation dialog */} !deleteLoading && setDeleteDialogOpen(false)}> Fahrzeug löschen Möchten Sie das Fahrzeug '{vehicle.bezeichnung}' wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden. ); } export default FahrzeugDetail;