import React, { useEffect, useState, useCallback } from 'react'; import { Alert, Box, Button, Chip, CircularProgress, Container, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Fab, FormControl, Grid, IconButton, InputLabel, MenuItem, Paper, Select, Stack, Tab, Tabs, TextField, Tooltip, Typography, } from '@mui/material'; import { Add, ArrowBack, Build, CheckCircle, DeleteOutline, Edit, Error as ErrorIcon, 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 { equipmentApi } from '../services/equipment'; 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', }); } // -- 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 { setSaveError('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, 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 && `${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, canChangeStatus } = 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; const subtitle = [ equipment.kategorie_name, equipment.seriennummer ? `SN: ${equipment.seriennummer}` : null, ].filter(Boolean).join(' · '); return ( {equipment.bezeichnung} {subtitle && ( {subtitle} )} {canChangeStatus && ( 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" > 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;