import React, { useEffect, useState, useCallback } from 'react'; import { Alert, Box, Button, Chip, CircularProgress, Container, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, FormControl, Grid, IconButton, InputLabel, MenuItem, Paper, Select, Stack, Tab, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Tabs, TextField, Tooltip, Typography, } from '@mui/material'; import { Add, ArrowBack, Build, CheckCircle, DeleteOutline, Edit, Error as ErrorIcon, History, MoreHoriz, PauseCircle, RemoveCircle, Star, Verified, Warning, } from '@mui/icons-material'; import { Link as RouterLink, useNavigate, useParams } from 'react-router-dom'; import DashboardLayout from '../components/dashboard/DashboardLayout'; import ChatAwareFab from '../components/shared/ChatAwareFab'; import { equipmentApi } from '../services/equipment'; import { fromGermanDate } from '../utils/dateInput'; import { AusruestungDetail, AusruestungWartungslog, AusruestungWartungslogArt, AusruestungStatus, AusruestungStatusLabel, UpdateAusruestungStatusPayload, CreateAusruestungWartungslogPayload, } from '../types/equipment.types'; import { usePermissions } from '../hooks/usePermissions'; import { useNotification } from '../contexts/NotificationContext'; // -- Tab Panel ---------------------------------------------------------------- interface TabPanelProps { children?: React.ReactNode; index: number; value: number; } const TabPanel: React.FC = ({ children, value, index }) => ( ); // -- Status config ------------------------------------------------------------ const STATUS_ICONS: Record = { [AusruestungStatus.Einsatzbereit]: , [AusruestungStatus.Beschaedigt]: , [AusruestungStatus.InWartung]: , [AusruestungStatus.AusserDienst]: , }; const STATUS_CHIP_COLOR: Record = { [AusruestungStatus.Einsatzbereit]: 'success', [AusruestungStatus.Beschaedigt]: 'error', [AusruestungStatus.InWartung]: 'warning', [AusruestungStatus.AusserDienst]: 'default', }; // -- 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 fmtDatetime(iso: string | null | undefined): string { if (!iso) return '---'; return new Date(iso).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } // -- Status History Section --------------------------------------------------- const StatusHistorySection: React.FC<{ equipmentId: string }> = ({ equipmentId }) => { 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(() => { equipmentApi.getStatusHistory(equipmentId) .then(setHistory) .catch(() => setHistory([])) .finally(() => setLoading(false)); }, [equipmentId]); if (loading || history.length === 0) return null; return ( <> Status-Verlauf Datum Von Nach Bemerkung Geändert von {history.map((h, idx) => ( {fmtDatetime(h.erstellt_am)} {h.bemerkung || '—'} {h.geaendert_von_name || '—'} ))}
); }; // -- Wartungslog Art config --------------------------------------------------- const WARTUNG_ART_CHIP_COLOR: Record = { 'Prüfung': 'info', 'Reparatur': 'warning', 'Sonstiges': 'default', }; const WARTUNG_ART_ICONS: Record = { 'Prüfung': , 'Reparatur': , 'Sonstiges': , default: , }; const ERGEBNIS_CHIP_COLOR: Record = { bestanden: 'success', bestanden_mit_maengeln: 'warning', nicht_bestanden: 'error', }; const ERGEBNIS_LABEL: Record = { bestanden: 'Bestanden', bestanden_mit_maengeln: 'Bestanden (mit Mängeln)', nicht_bestanden: 'Nicht bestanden', }; // -- Uebersicht Tab ----------------------------------------------------------- interface UebersichtTabProps { equipment: AusruestungDetail; onStatusUpdated: () => void; canChangeStatus: boolean; } const UebersichtTab: React.FC = ({ equipment, onStatusUpdated, canChangeStatus }) => { const [statusDialogOpen, setStatusDialogOpen] = useState(false); const [newStatus, setNewStatus] = useState(equipment.status); const [bemerkung, setBemerkung] = useState(equipment.status_bemerkung ?? ''); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); const openDialog = () => { setNewStatus(equipment.status); setBemerkung(equipment.status_bemerkung ?? ''); setSaveError(null); setStatusDialogOpen(true); }; const closeDialog = () => { setSaveError(null); setStatusDialogOpen(false); }; const handleSaveStatus = async () => { try { setSaving(true); setSaveError(null); const payload: UpdateAusruestungStatusPayload = { status: newStatus, bemerkung }; await equipmentApi.updateStatus(equipment.id, payload); setStatusDialogOpen(false); onStatusUpdated(); } catch (err: any) { setSaveError(err?.response?.data?.message || 'Status konnte nicht gespeichert werden.'); } finally { setSaving(false); } }; const isBeschaedigt = equipment.status === AusruestungStatus.Beschaedigt; // Inspection deadline const pruefTage = equipment.pruefung_tage_bis_faelligkeit; // Data grid fields const dataFields: { label: string; value: React.ReactNode }[] = [ { label: 'Kategorie', value: equipment.kategorie_name }, { label: 'Seriennummer', value: equipment.seriennummer ?? '---' }, { label: 'Inventarnummer', value: equipment.inventarnummer ?? '---' }, { label: 'Hersteller', value: equipment.hersteller ?? '---' }, { label: 'Baujahr', value: equipment.baujahr ?? '---' }, { label: 'Fahrzeug', value: equipment.fahrzeug_id ? ( {equipment.fahrzeug_bezeichnung} ) : '---', }, ...(!equipment.fahrzeug_id ? [{ label: 'Standort', value: equipment.standort || '---' }] : []), { label: 'Wichtig', value: equipment.ist_wichtig ? ( Ja ) : 'Nein', }, { label: 'Prüfintervall', value: equipment.pruef_intervall_monate ? `${equipment.pruef_intervall_monate} Monate` : '---', }, { label: 'Letzte Prüfung', value: fmtDate(equipment.letzte_pruefung_am) }, { label: 'Nächste Prüfung', value: equipment.naechste_pruefung_am ? ( {fmtDate(equipment.naechste_pruefung_am)} {pruefTage !== null && pruefTage < 0 && ( } label={`${Math.abs(pruefTage)} Tage überfällig`} sx={{ mt: 0.5 }} /> )} {pruefTage !== null && pruefTage >= 0 && pruefTage <= 30 && ( )} {pruefTage !== null && pruefTage > 30 && ( )} ) : ( ), }, { label: 'Bemerkung', value: equipment.bemerkung ?? '---' }, ]; return ( {isBeschaedigt && ( } sx={{ mb: 2 }}> Beschädigt --- dieses Gerät ist nicht einsatzbereit. {equipment.status_bemerkung && ` Bemerkung: ${equipment.status_bemerkung}`} )} {/* Status panel */} {STATUS_ICONS[equipment.status]} Aktueller Status {equipment.status_bemerkung && ( {equipment.status_bemerkung} )} {canChangeStatus && ( )} {/* Equipment data grid */} {dataFields.map(({ label, value }) => ( {label} {typeof value === 'string' || typeof value === 'number' ? ( {value} ) : ( {value} )} ))} {/* Status change dialog */} Gerätestatus ändern {saveError && {saveError}} Neuer Status setBemerkung(e.target.value)} placeholder="z.B. Gerät zur Reparatur eingeschickt, voraussichtlich ab 01.03. wieder einsatzbereit" /> ); }; // -- Wartung Tab -------------------------------------------------------------- interface WartungTabProps { equipmentId: string; wartungslog: AusruestungWartungslog[]; onAdded: () => void; canWrite: boolean; } const WartungTab: React.FC = ({ equipmentId, wartungslog, onAdded, canWrite }) => { const [dialogOpen, setDialogOpen] = useState(false); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(null); const emptyForm: CreateAusruestungWartungslogPayload = { datum: '', art: 'Prüfung' as AusruestungWartungslogArt, beschreibung: '', ergebnis: undefined, kosten: undefined, pruefende_stelle: undefined, }; const [form, setForm] = useState(emptyForm); const handleSubmit = async () => { if (!form.datum || !form.art || !form.beschreibung.trim()) { setSaveError('Datum, Art und Beschreibung sind erforderlich.'); return; } try { setSaving(true); setSaveError(null); await equipmentApi.addWartungslog(equipmentId, { ...form, datum: fromGermanDate(form.datum) || form.datum, pruefende_stelle: form.pruefende_stelle || undefined, ergebnis: form.ergebnis || undefined, }); setDialogOpen(false); setForm(emptyForm); onAdded(); } catch { setSaveError('Wartungseintrag konnte nicht gespeichert werden.'); } finally { setSaving(false); } }; // Sort wartungslog by datum DESC const sorted = [...wartungslog].sort( (a, b) => new Date(b.datum).getTime() - new Date(a.datum).getTime() ); return ( {sorted.length === 0 ? ( Keine Wartungseinträge vorhanden. ) : ( } spacing={0}> {sorted.map((entry) => { const artIcon = WARTUNG_ART_ICONS[entry.art ?? ''] ?? WARTUNG_ART_ICONS.default; const ergebnisColor = entry.ergebnis ? ERGEBNIS_CHIP_COLOR[entry.ergebnis] : undefined; const ergebnisLabel = entry.ergebnis ? ERGEBNIS_LABEL[entry.ergebnis] : undefined; return ( {artIcon} {fmtDate(entry.datum)} {entry.art && ( )} {ergebnisLabel && ergebnisColor && ( )} {entry.beschreibung} {[ entry.kosten != null && `${Number(entry.kosten).toFixed(2)} EUR`, entry.pruefende_stelle && entry.pruefende_stelle, ].filter(Boolean).join(' · ')} ); })} )} {canWrite && ( { setForm(emptyForm); setSaveError(null); setDialogOpen(true); }} > )} setDialogOpen(false)} maxWidth="sm" fullWidth> Wartung / Prüfung eintragen {saveError && {saveError}} setForm((f) => ({ ...f, datum: e.target.value }))} InputLabelProps={{ shrink: true }} /> Art * setForm((f) => ({ ...f, beschreibung: e.target.value }))} /> Ergebnis setForm((f) => ({ ...f, kosten: e.target.value ? Number(e.target.value) : undefined, })) } inputProps={{ min: 0, step: 0.01 }} /> setForm((f) => ({ ...f, pruefende_stelle: e.target.value }))} placeholder="Name der prüfenden Stelle oder Person" /> ); }; // -- Main Page ---------------------------------------------------------------- function AusruestungDetailPage() { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { isAdmin, canManageCategory, canManageEquipmentMaintenance } = usePermissions(); const notification = useNotification(); const [equipment, setEquipment] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState(0); const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false); const fetchEquipment = useCallback(async () => { if (!id) return; try { setLoading(true); setError(null); const data = await equipmentApi.getById(id); setEquipment(data); } catch { setError('Gerät konnte nicht geladen werden.'); } finally { setLoading(false); } }, [id]); useEffect(() => { fetchEquipment(); }, [fetchEquipment]); const handleDelete = async () => { if (!id) return; try { setDeleteLoading(true); await equipmentApi.delete(id); notification.showSuccess('Gerät wurde erfolgreich gelöscht.'); navigate('/ausruestung'); } catch { notification.showError('Gerät konnte nicht gelöscht werden.'); setDeleteDialogOpen(false); setDeleteLoading(false); } }; if (loading) { return ( ); } if (error || !equipment) { return ( {error ?? 'Gerät nicht gefunden.'} ); } const hasOverdue = equipment.pruefung_tage_bis_faelligkeit !== null && equipment.pruefung_tage_bis_faelligkeit < 0; // Derive an inline category object so canManageCategory can do the motorisiert check const equipmentKategorie = { id: equipment.kategorie_id, name: equipment.kategorie_name, kurzname: equipment.kategorie_kurzname, sortierung: 0, motorisiert: equipment.kategorie_motorisiert, }; const canWrite = canManageCategory(equipmentKategorie); const subtitle = [ equipment.kategorie_name, equipment.seriennummer ? `SN: ${equipment.seriennummer}` : null, ].filter(Boolean).join(' · '); return ( {equipment.bezeichnung} {subtitle && ( {subtitle} )} {canWrite && ( navigate(`/ausruestung/${equipment.id}/bearbeiten`)} aria-label="Gerät bearbeiten" > )} {isAdmin && ( setDeleteDialogOpen(true)} aria-label="Gerät löschen" > )} setActiveTab(v)} aria-label="Ausrüstung Detailansicht" variant="scrollable" scrollButtons="auto" > Wartung : 'Wartung' } /> {/* Delete confirmation dialog */} !deleteLoading && setDeleteDialogOpen(false)}> Gerät löschen Möchten Sie '{equipment.bezeichnung}' wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden. ); } export default AusruestungDetailPage;