import React, { useEffect, useState, useCallback } from 'react'; import { Alert, Box, Button, Card, CardActionArea, CardContent, CardMedia, Chip, CircularProgress, Container, Grid, IconButton, InputAdornment, Paper, Tab, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Tabs, TextField, Tooltip, Typography, } from '@mui/material'; import { Add, Add as AddIcon, CheckCircle, Delete as DeleteIcon, DirectionsCar, Edit as EditIcon, Error as ErrorIcon, FileDownload, PauseCircle, School, Search, Warning, ReportProblem, } from '@mui/icons-material'; import { useNavigate, useSearchParams } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import DashboardLayout from '../components/dashboard/DashboardLayout'; import ChatAwareFab from '../components/shared/ChatAwareFab'; import { vehiclesApi } from '../services/vehicles'; import { equipmentApi } from '../services/equipment'; import { fahrzeugTypenApi } from '../services/fahrzeugTypen'; import type { VehicleEquipmentWarning } from '../types/equipment.types'; import { AusruestungStatus, AusruestungStatusLabel } from '../types/equipment.types'; import { FahrzeugListItem, FahrzeugStatus, FahrzeugStatusLabel, } from '../types/vehicle.types'; import type { FahrzeugTyp } from '../types/checklist.types'; import { usePermissions } from '../hooks/usePermissions'; import { usePermissionContext } from '../contexts/PermissionContext'; import { useNotification } from '../contexts/NotificationContext'; import { FormDialog } from '../components/templates'; // ── Status chip config ──────────────────────────────────────────────────────── const STATUS_CONFIG: Record< FahrzeugStatus, { color: 'success' | 'warning' | 'error' | 'info'; icon: React.ReactElement } > = { [FahrzeugStatus.Einsatzbereit]: { color: 'success', icon: }, [FahrzeugStatus.AusserDienstWartung]: { color: 'warning', icon: }, [FahrzeugStatus.AusserDienstSchaden]: { color: 'error', icon: }, }; // ── Inspection badge helpers ────────────────────────────────────────────────── type InspBadgeColor = 'success' | 'warning' | 'error' | 'default'; function inspBadgeColor(tage: number | null): InspBadgeColor { if (tage === null) return 'default'; if (tage < 0) return 'error'; if (tage <= 30) return 'warning'; return 'success'; } function inspBadgeLabel(art: string, tage: number | null, faelligAm: string | null): string { if (faelligAm === null) return ''; const date = new Date(faelligAm).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', }); if (tage === null) return `${art}: ${date}`; if (tage < 0) return `${art}: ÜBERFÄLLIG (${date})`; if (tage === 0) return `${art}: heute (${date})`; return `${art}: ${date}`; } function inspTooltipTitle(fullLabel: string, tage: number | null, faelligAm: string | null): string { if (!faelligAm) return fullLabel; const date = new Date(faelligAm).toLocaleDateString('de-DE'); if (tage !== null && tage < 0) { return `${fullLabel}: Seit ${Math.abs(tage)} Tagen überfällig!`; } return `${fullLabel}: Fällig am ${date}`; } // ── Vehicle Card ────────────────────────────────────────────────────────────── interface VehicleCardProps { vehicle: FahrzeugListItem; onClick: (id: string) => void; warnings?: VehicleEquipmentWarning[]; } const VehicleCard: React.FC = ({ vehicle, onClick, warnings = [] }) => { const status = vehicle.status as FahrzeugStatus; const statusCfg = STATUS_CONFIG[status] ?? STATUS_CONFIG[FahrzeugStatus.Einsatzbereit]; const isSchaden = status === FahrzeugStatus.AusserDienstSchaden; const inspBadges = [ { art: '§57a', fullLabel: '§57a Periodische Prüfung', tage: vehicle.paragraph57a_tage_bis_faelligkeit, faelligAm: vehicle.paragraph57a_faellig_am, }, { art: 'Wartung', fullLabel: 'Nächste Wartung / Service', tage: vehicle.wartung_tage_bis_faelligkeit, faelligAm: vehicle.naechste_wartung_am, }, ].filter((b) => b.faelligAm !== null); return ( {isSchaden && ( )} onClick(vehicle.id)} sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', alignItems: 'stretch' }} > {vehicle.bild_url ? ( ) : ( )} {vehicle.bezeichnung} {vehicle.kurzname && ( ({vehicle.kurzname}) )} {vehicle.amtliches_kennzeichen && ( {vehicle.amtliches_kennzeichen} )} {vehicle.aktiver_lehrgang && ( } label="In Lehrgang" color="info" size="small" variant="outlined" /> )} {(status === FahrzeugStatus.AusserDienstWartung || status === FahrzeugStatus.AusserDienstSchaden) && vehicle.ausser_dienst_bis && ( Bis ca. {new Date(vehicle.ausser_dienst_bis).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' })} )} {inspBadges.length > 0 && ( {inspBadges.map((b) => { const color = inspBadgeColor(b.tage); const label = inspBadgeLabel(b.art, b.tage, b.faelligAm); if (!label) return null; return ( : undefined} sx={{ fontSize: '0.7rem' }} /> ); })} )} {warnings.length > 0 && ( {warnings.slice(0, warnings.length > 3 ? 2 : 3).map((w) => { const isError = w.status === AusruestungStatus.Beschaedigt || w.status === AusruestungStatus.AusserDienst; return ( } label={w.bezeichnung} color={isError ? 'error' : 'warning'} sx={{ fontSize: '0.7rem', maxWidth: 160 }} /> ); })} {warnings.length > 3 && ( `${w.bezeichnung}: ${AusruestungStatusLabel[w.status]}`) .join('\n')} > } label={`+${warnings.length - 2} weitere`} color={ warnings .slice(2) .some( (w) => w.status === AusruestungStatus.Beschaedigt || w.status === AusruestungStatus.AusserDienst ) ? 'error' : 'warning' } sx={{ fontSize: '0.7rem' }} /> )} )} ); }; // ── Fahrzeugtypen-Verwaltung (Einstellungen Tab) ───────────────────────────── function FahrzeugTypenSettings() { const queryClient = useQueryClient(); const { showSuccess, showError } = useNotification(); const { data: fahrzeugTypen = [], isLoading } = useQuery({ queryKey: ['fahrzeug-typen'], queryFn: fahrzeugTypenApi.getAll, }); const [dialogOpen, setDialogOpen] = useState(false); const [editing, setEditing] = useState(null); const [form, setForm] = useState({ name: '', beschreibung: '', icon: '' }); const [deleteError, setDeleteError] = useState(null); const createMutation = useMutation({ mutationFn: (data: Partial) => fahrzeugTypenApi.create(data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['fahrzeug-typen'] }); setDialogOpen(false); showSuccess('Fahrzeugtyp erstellt'); }, onError: () => showError('Fehler beim Erstellen'), }); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: number; data: Partial }) => fahrzeugTypenApi.update(id, data), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['fahrzeug-typen'] }); setDialogOpen(false); showSuccess('Fahrzeugtyp aktualisiert'); }, onError: () => showError('Fehler beim Aktualisieren'), }); const deleteMutation = useMutation({ mutationFn: (id: number) => fahrzeugTypenApi.delete(id), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['fahrzeug-typen'] }); setDeleteError(null); showSuccess('Fahrzeugtyp gelöscht'); }, onError: (err: any) => { const msg = err?.response?.data?.message || 'Fehler beim Löschen — Typ ist möglicherweise noch in Verwendung.'; setDeleteError(msg); }, }); const openCreate = () => { setEditing(null); setForm({ name: '', beschreibung: '', icon: '' }); setDialogOpen(true); }; const openEdit = (t: FahrzeugTyp) => { setEditing(t); setForm({ name: t.name, beschreibung: t.beschreibung ?? '', icon: t.icon ?? '' }); setDialogOpen(true); }; const handleSubmit = () => { if (!form.name.trim()) return; if (editing) { updateMutation.mutate({ id: editing.id, data: form }); } else { createMutation.mutate(form); } }; const isSaving = createMutation.isPending || updateMutation.isPending; return ( Fahrzeugtypen {deleteError && ( setDeleteError(null)}> {deleteError} )} {isLoading ? ( ) : ( <> Name Beschreibung Icon Aktionen {fahrzeugTypen.length === 0 ? ( Keine Fahrzeugtypen vorhanden ) : ( fahrzeugTypen.map((t) => ( {t.name} {t.beschreibung ?? '–'} {t.icon ?? '–'} openEdit(t)}> deleteMutation.mutate(t.id)} > )) )}
)} setDialogOpen(false)} onSubmit={handleSubmit} title={editing ? 'Fahrzeugtyp bearbeiten' : 'Neuer Fahrzeugtyp'} isSubmitting={isSaving} > setForm((f) => ({ ...f, name: e.target.value }))} /> setForm((f) => ({ ...f, beschreibung: e.target.value }))} /> setForm((f) => ({ ...f, icon: e.target.value }))} placeholder="z.B. fire_truck" />
); } // ── Main Page ───────────────────────────────────────────────────────────────── function Fahrzeuge() { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const tab = parseInt(searchParams.get('tab') ?? '0', 10); const { isAdmin } = usePermissions(); const { hasPermission } = usePermissionContext(); const [vehicles, setVehicles] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [search, setSearch] = useState(''); const [equipmentWarnings, setEquipmentWarnings] = useState>(new Map()); const canEditSettings = hasPermission('checklisten:manage_templates'); const fetchVehicles = useCallback(async () => { try { setLoading(true); setError(null); const data = await vehiclesApi.getAll(); setVehicles(data); } catch { setError('Fahrzeuge konnten nicht geladen werden. Bitte versuchen Sie es erneut.'); } finally { setLoading(false); } }, []); useEffect(() => { fetchVehicles(); }, [fetchVehicles]); useEffect(() => { async function fetchWarnings() { try { const warnings = await equipmentApi.getVehicleWarnings(); const warningsMap = new Map(); warnings.forEach(w => { const existing = warningsMap.get(w.fahrzeug_id) || []; existing.push(w); warningsMap.set(w.fahrzeug_id, existing); }); setEquipmentWarnings(warningsMap); } catch { setEquipmentWarnings(new Map()); } } fetchWarnings(); }, []); const handleExportAlerts = useCallback(async () => { try { const blob = await vehiclesApi.exportAlerts(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; const today = new Date(); const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`; a.download = `pruefungen_${dateStr}.csv`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } catch { setError('CSV-Export fehlgeschlagen.'); } }, []); const filtered = vehicles.filter((v) => { if (!search.trim()) return true; const q = search.toLowerCase(); return ( v.bezeichnung.toLowerCase().includes(q) || (v.kurzname?.toLowerCase().includes(q) ?? false) || (v.amtliches_kennzeichen?.toLowerCase().includes(q) ?? false) || (v.hersteller?.toLowerCase().includes(q) ?? false) ); }); const einsatzbereit = vehicles.filter((v) => v.status === FahrzeugStatus.Einsatzbereit).length; const hasOverdue = vehicles.some( (v) => (v.paragraph57a_tage_bis_faelligkeit !== null && v.paragraph57a_tage_bis_faelligkeit < 0) || (v.wartung_tage_bis_faelligkeit !== null && v.wartung_tage_bis_faelligkeit < 0) ); return ( Fahrzeugverwaltung {!loading && ( {vehicles.length} Fahrzeug{vehicles.length !== 1 ? 'e' : ''} gesamt {' · '} {einsatzbereit} einsatzbereit )} {tab === 0 && ( )} setSearchParams({ tab: String(v) })} sx={{ mb: 3, borderBottom: 1, borderColor: 'divider' }} > {canEditSettings && } {tab === 0 && ( <> {hasOverdue && ( }> Achtung: Mindestens ein Fahrzeug hat eine überfällige Prüfungs- oder Wartungsfrist. )} setSearch(e.target.value)} fullWidth size="small" sx={{ mb: 3, maxWidth: 480 }} InputProps={{ startAdornment: ( ), }} /> {loading && ( )} {!loading && error && ( {error} )} {!loading && !error && filtered.length === 0 && ( {vehicles.length === 0 ? 'Noch keine Fahrzeuge erfasst' : 'Kein Fahrzeug entspricht dem Suchbegriff'} )} {!loading && !error && filtered.length > 0 && ( {filtered.map((vehicle) => ( navigate(`/fahrzeuge/${id}`)} warnings={equipmentWarnings.get(vehicle.id) || []} /> ))} )} {isAdmin && ( navigate('/fahrzeuge/neu')} > )} )} {tab === 1 && canEditSettings && ( )} ); } export default Fahrzeuge;