add features
This commit is contained in:
643
frontend/src/pages/EinsatzDetail.tsx
Normal file
643
frontend/src/pages/EinsatzDetail.tsx
Normal file
@@ -0,0 +1,643 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Box,
|
||||
Container,
|
||||
Typography,
|
||||
Button,
|
||||
Chip,
|
||||
Card,
|
||||
CardContent,
|
||||
Grid,
|
||||
Divider,
|
||||
Avatar,
|
||||
Skeleton,
|
||||
Alert,
|
||||
Stack,
|
||||
Tooltip,
|
||||
Paper,
|
||||
TextField,
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
ArrowBack,
|
||||
Edit,
|
||||
Save,
|
||||
Cancel,
|
||||
LocalFireDepartment,
|
||||
AccessTime,
|
||||
DirectionsCar,
|
||||
People,
|
||||
LocationOn,
|
||||
Description,
|
||||
PictureAsPdf,
|
||||
} from '@mui/icons-material';
|
||||
import { format, parseISO, differenceInMinutes } from 'date-fns';
|
||||
import { de } from 'date-fns/locale';
|
||||
import DashboardLayout from '../components/dashboard/DashboardLayout';
|
||||
import {
|
||||
incidentsApi,
|
||||
EinsatzDetail,
|
||||
EinsatzStatus,
|
||||
EINSATZ_ART_LABELS,
|
||||
EINSATZ_STATUS_LABELS,
|
||||
EinsatzArt,
|
||||
} from '../services/incidents';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// COLOUR MAPS
|
||||
// ---------------------------------------------------------------------------
|
||||
const ART_CHIP_COLOR: Record<
|
||||
EinsatzArt,
|
||||
'error' | 'primary' | 'secondary' | 'warning' | 'success' | 'default' | 'info'
|
||||
> = {
|
||||
Brand: 'error',
|
||||
THL: 'primary',
|
||||
ABC: 'secondary',
|
||||
BMA: 'warning',
|
||||
Hilfeleistung: 'success',
|
||||
Fehlalarm: 'default',
|
||||
Brandsicherheitswache: 'info',
|
||||
};
|
||||
|
||||
const STATUS_CHIP_COLOR: Record<EinsatzStatus, 'warning' | 'success' | 'default'> = {
|
||||
aktiv: 'warning',
|
||||
abgeschlossen: 'success',
|
||||
archiviert: 'default',
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HELPERS
|
||||
// ---------------------------------------------------------------------------
|
||||
function formatDE(iso: string | null | undefined, fmt = 'dd.MM.yyyy HH:mm'): string {
|
||||
if (!iso) return '—';
|
||||
try {
|
||||
return format(parseISO(iso), fmt, { locale: de });
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
function minuteDiff(start: string | null, end: string | null): string {
|
||||
if (!start || !end) return '—';
|
||||
try {
|
||||
const mins = differenceInMinutes(parseISO(end), parseISO(start));
|
||||
if (mins < 0) return '—';
|
||||
if (mins < 60) return `${mins} min`;
|
||||
const h = Math.floor(mins / 60);
|
||||
const m = mins % 60;
|
||||
return m === 0 ? `${h} h` : `${h} h ${m} min`;
|
||||
} catch {
|
||||
return '—';
|
||||
}
|
||||
}
|
||||
|
||||
function initials(givenName: string | null, familyName: string | null, name: string | null): string {
|
||||
if (givenName && familyName) return `${givenName[0]}${familyName[0]}`.toUpperCase();
|
||||
if (name) return name.slice(0, 2).toUpperCase();
|
||||
return '??';
|
||||
}
|
||||
|
||||
function displayName(p: EinsatzDetail['personal'][0]): string {
|
||||
if (p.given_name && p.family_name) return `${p.given_name} ${p.family_name}`;
|
||||
if (p.name) return p.name;
|
||||
return p.email;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// TIMELINE STEP
|
||||
// ---------------------------------------------------------------------------
|
||||
interface TimelineStepProps {
|
||||
label: string;
|
||||
time: string | null;
|
||||
duration?: string;
|
||||
isFirst?: boolean;
|
||||
}
|
||||
|
||||
const TimelineStep: React.FC<TimelineStepProps> = ({ label, time, duration, isFirst }) => (
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', minWidth: 24 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: '50%',
|
||||
bgcolor: time ? 'primary.main' : 'action.disabled',
|
||||
border: '2px solid',
|
||||
borderColor: time ? 'primary.main' : 'action.disabled',
|
||||
flexShrink: 0,
|
||||
mt: 0.5,
|
||||
}}
|
||||
/>
|
||||
{!isFirst && (
|
||||
<Box
|
||||
sx={{
|
||||
width: 2,
|
||||
flexGrow: 1,
|
||||
minHeight: 32,
|
||||
bgcolor: time ? 'primary.light' : 'action.disabled',
|
||||
my: 0.25,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ pb: 2 }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ textTransform: 'uppercase', fontSize: '0.68rem' }}>
|
||||
{label}
|
||||
</Typography>
|
||||
<Typography variant="body1" fontWeight={time ? 600 : 400} color={time ? 'text.primary' : 'text.disabled'}>
|
||||
{time ? formatDE(time) : 'Nicht erfasst'}
|
||||
</Typography>
|
||||
{duration && (
|
||||
<Typography variant="caption" color="primary.main" sx={{ fontWeight: 500 }}>
|
||||
+{duration}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// MAIN PAGE
|
||||
// ---------------------------------------------------------------------------
|
||||
function EinsatzDetail() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const notification = useNotification();
|
||||
|
||||
const [einsatz, setEinsatz] = useState<EinsatzDetail | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Edit mode for bericht fields
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [berichtKurz, setBerichtKurz] = useState('');
|
||||
const [berichtText, setBerichtText] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// FETCH
|
||||
// -------------------------------------------------------------------------
|
||||
const fetchEinsatz = useCallback(async () => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const data = await incidentsApi.getById(id);
|
||||
setEinsatz(data);
|
||||
setBerichtKurz(data.bericht_kurz ?? '');
|
||||
setBerichtText(data.bericht_text ?? '');
|
||||
} catch (err) {
|
||||
setError('Einsatz konnte nicht geladen werden.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchEinsatz();
|
||||
}, [fetchEinsatz]);
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SAVE BERICHT
|
||||
// -------------------------------------------------------------------------
|
||||
const handleSaveBericht = async () => {
|
||||
if (!id) return;
|
||||
setSaving(true);
|
||||
try {
|
||||
await incidentsApi.update(id, {
|
||||
bericht_kurz: berichtKurz || null,
|
||||
bericht_text: berichtText || null,
|
||||
});
|
||||
notification.showSuccess('Einsatzbericht gespeichert');
|
||||
setEditing(false);
|
||||
fetchEinsatz();
|
||||
} catch (err) {
|
||||
notification.showError('Fehler beim Speichern des Berichts');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setEditing(false);
|
||||
setBerichtKurz(einsatz?.bericht_kurz ?? '');
|
||||
setBerichtText(einsatz?.bericht_text ?? '');
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// PDF EXPORT (placeholder)
|
||||
// -------------------------------------------------------------------------
|
||||
const handleExportPdf = () => {
|
||||
notification.showInfo('PDF-Export wird in Kürze verfügbar sein.');
|
||||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// LOADING STATE
|
||||
// -------------------------------------------------------------------------
|
||||
if (loading) {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Container maxWidth="lg">
|
||||
<Skeleton width={120} height={36} sx={{ mb: 2 }} />
|
||||
<Skeleton width={300} height={48} sx={{ mb: 3 }} />
|
||||
<Grid container spacing={3}>
|
||||
{[0, 1, 2].map((i) => (
|
||||
<Grid item xs={12} md={4} key={i}>
|
||||
<Skeleton variant="rectangular" height={200} sx={{ borderRadius: 2 }} />
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</Container>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
if (error || !einsatz) {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Container maxWidth="lg">
|
||||
<Button
|
||||
startIcon={<ArrowBack />}
|
||||
onClick={() => navigate('/einsaetze')}
|
||||
sx={{ mb: 3 }}
|
||||
>
|
||||
Zurück zur Übersicht
|
||||
</Button>
|
||||
<Alert severity="error">{error ?? 'Einsatz nicht gefunden.'}</Alert>
|
||||
</Container>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
const address = [einsatz.strasse, einsatz.hausnummer, einsatz.ort]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<Container maxWidth="lg">
|
||||
{/* Back + Actions */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
||||
<Button
|
||||
startIcon={<ArrowBack />}
|
||||
onClick={() => navigate('/einsaetze')}
|
||||
variant="text"
|
||||
>
|
||||
Zurück
|
||||
</Button>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Tooltip title="PDF exportieren (Vorschau)">
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<PictureAsPdf />}
|
||||
onClick={handleExportPdf}
|
||||
size="small"
|
||||
>
|
||||
PDF Export
|
||||
</Button>
|
||||
</Tooltip>
|
||||
{!editing ? (
|
||||
<Button
|
||||
variant="contained"
|
||||
startIcon={<Edit />}
|
||||
onClick={() => setEditing(true)}
|
||||
size="small"
|
||||
>
|
||||
Bearbeiten
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<Cancel />}
|
||||
onClick={handleCancelEdit}
|
||||
size="small"
|
||||
disabled={saving}
|
||||
>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
startIcon={<Save />}
|
||||
onClick={handleSaveBericht}
|
||||
size="small"
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? 'Speichere...' : 'Speichern'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
{/* HEADER */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flexWrap: 'wrap', mb: 1 }}>
|
||||
<Chip
|
||||
icon={<LocalFireDepartment />}
|
||||
label={EINSATZ_ART_LABELS[einsatz.einsatz_art]}
|
||||
color={ART_CHIP_COLOR[einsatz.einsatz_art]}
|
||||
sx={{ fontWeight: 600 }}
|
||||
/>
|
||||
<Chip
|
||||
label={EINSATZ_STATUS_LABELS[einsatz.status]}
|
||||
color={STATUS_CHIP_COLOR[einsatz.status]}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
/>
|
||||
{einsatz.einsatz_stichwort && (
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
{einsatz.einsatz_stichwort}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Typography variant="h4" fontWeight={700}>
|
||||
Einsatz {einsatz.einsatz_nr}
|
||||
</Typography>
|
||||
{address && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, mt: 0.5 }}>
|
||||
<LocationOn fontSize="small" color="action" />
|
||||
<Typography variant="body1" color="text.secondary">
|
||||
{address}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{/* LEFT COLUMN: Timeline + Vehicles */}
|
||||
<Grid item xs={12} md={4}>
|
||||
{/* Timeline card */}
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<AccessTime color="primary" />
|
||||
<Typography variant="h6">Zeitlinie</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{/* Reversed order: last step first (top = Alarm) */}
|
||||
<TimelineStep
|
||||
label="Alarmzeit"
|
||||
time={einsatz.alarm_time}
|
||||
/>
|
||||
<TimelineStep
|
||||
label="Ausrückzeit"
|
||||
time={einsatz.ausrueck_time}
|
||||
duration={minuteDiff(einsatz.alarm_time, einsatz.ausrueck_time)}
|
||||
/>
|
||||
<TimelineStep
|
||||
label="Ankunftszeit (Hilfsfrist)"
|
||||
time={einsatz.ankunft_time}
|
||||
duration={minuteDiff(einsatz.alarm_time, einsatz.ankunft_time)}
|
||||
/>
|
||||
<TimelineStep
|
||||
isFirst
|
||||
label="Einrückzeit"
|
||||
time={einsatz.einrueck_time}
|
||||
duration={minuteDiff(einsatz.alarm_time, einsatz.einrueck_time)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{(einsatz.hilfsfrist_min !== null || einsatz.dauer_min !== null) && (
|
||||
<>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Grid container spacing={1}>
|
||||
{einsatz.hilfsfrist_min !== null && (
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Hilfsfrist
|
||||
</Typography>
|
||||
<Typography variant="body1" fontWeight={700} color={einsatz.hilfsfrist_min > 10 ? 'error.main' : 'success.main'}>
|
||||
{einsatz.hilfsfrist_min} min
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
{einsatz.dauer_min !== null && (
|
||||
<Grid item xs={6}>
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
Gesamtdauer
|
||||
</Typography>
|
||||
<Typography variant="body1" fontWeight={700}>
|
||||
{einsatz.dauer_min < 60
|
||||
? `${einsatz.dauer_min} min`
|
||||
: `${Math.floor(einsatz.dauer_min / 60)} h ${einsatz.dauer_min % 60} min`}
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Vehicles card */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<DirectionsCar color="primary" />
|
||||
<Typography variant="h6">Fahrzeuge</Typography>
|
||||
<Chip
|
||||
label={einsatz.fahrzeuge.length}
|
||||
size="small"
|
||||
color="primary"
|
||||
sx={{ ml: 'auto' }}
|
||||
/>
|
||||
</Box>
|
||||
{einsatz.fahrzeuge.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Keine Fahrzeuge zugewiesen
|
||||
</Typography>
|
||||
) : (
|
||||
<Stack spacing={1}>
|
||||
{einsatz.fahrzeuge.map((f) => (
|
||||
<Paper
|
||||
key={f.fahrzeug_id}
|
||||
variant="outlined"
|
||||
sx={{ p: 1.25, borderRadius: 2 }}
|
||||
>
|
||||
<Typography variant="subtitle2" fontWeight={600}>
|
||||
{f.bezeichnung}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{f.kennzeichen}
|
||||
{f.fahrzeug_typ ? ` · ${f.fahrzeug_typ}` : ''}
|
||||
</Typography>
|
||||
{(f.ausrueck_time || f.einrueck_time) && (
|
||||
<Typography variant="caption" color="text.secondary" display="block">
|
||||
{formatDE(f.ausrueck_time, 'HH:mm')} → {formatDE(f.einrueck_time, 'HH:mm')}
|
||||
</Typography>
|
||||
)}
|
||||
</Paper>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
|
||||
{/* RIGHT COLUMN: Personnel + Bericht */}
|
||||
<Grid item xs={12} md={8}>
|
||||
{/* Personnel card */}
|
||||
<Card sx={{ mb: 3 }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<People color="primary" />
|
||||
<Typography variant="h6">Einsatzkräfte</Typography>
|
||||
<Chip
|
||||
label={einsatz.personal.length}
|
||||
size="small"
|
||||
color="primary"
|
||||
sx={{ ml: 'auto' }}
|
||||
/>
|
||||
</Box>
|
||||
{einsatz.einsatzleiter_name && (
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ textTransform: 'uppercase', fontSize: '0.68rem' }}>
|
||||
Einsatzleiter
|
||||
</Typography>
|
||||
<Typography variant="body1" fontWeight={600}>
|
||||
{einsatz.einsatzleiter_name}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{einsatz.personal.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Keine Einsatzkräfte zugewiesen
|
||||
</Typography>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1.5 }}>
|
||||
{einsatz.personal.map((p) => (
|
||||
<Box
|
||||
key={p.user_id}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
p: 1,
|
||||
borderRadius: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
minWidth: 200,
|
||||
maxWidth: 260,
|
||||
flex: '1 1 200px',
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
sx={{ width: 36, height: 36, bgcolor: 'primary.main', fontSize: '0.8rem' }}
|
||||
>
|
||||
{initials(p.given_name, p.family_name, p.name)}
|
||||
</Avatar>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
noWrap
|
||||
title={displayName(p)}
|
||||
>
|
||||
{displayName(p)}
|
||||
</Typography>
|
||||
<Chip
|
||||
label={p.funktion}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
sx={{ fontSize: '0.68rem', height: 18 }}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Bericht card */}
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<Description color="primary" />
|
||||
<Typography variant="h6">Einsatzbericht</Typography>
|
||||
</Box>
|
||||
|
||||
{editing ? (
|
||||
<Stack spacing={2}>
|
||||
<TextField
|
||||
label="Kurzbeschreibung"
|
||||
value={berichtKurz}
|
||||
onChange={(e) => setBerichtKurz(e.target.value)}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={2}
|
||||
inputProps={{ maxLength: 255 }}
|
||||
helperText={`${berichtKurz.length}/255`}
|
||||
/>
|
||||
<TextField
|
||||
label="Ausführlicher Bericht"
|
||||
value={berichtText}
|
||||
onChange={(e) => setBerichtText(e.target.value)}
|
||||
fullWidth
|
||||
multiline
|
||||
rows={8}
|
||||
placeholder="Detaillierter Einsatzbericht..."
|
||||
helperText="Nur für Kommandant und Admin sichtbar"
|
||||
/>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack spacing={2}>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ textTransform: 'uppercase', fontSize: '0.68rem' }}>
|
||||
Kurzbeschreibung
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
{einsatz.bericht_kurz ?? (
|
||||
<Typography component="span" color="text.disabled" fontStyle="italic">
|
||||
Keine Kurzbeschreibung erfasst
|
||||
</Typography>
|
||||
)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{einsatz.bericht_text !== undefined && (
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ textTransform: 'uppercase', fontSize: '0.68rem' }}>
|
||||
Ausführlicher Bericht
|
||||
</Typography>
|
||||
{einsatz.bericht_text ? (
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ whiteSpace: 'pre-wrap', mt: 0.5, lineHeight: 1.7 }}
|
||||
>
|
||||
{einsatz.bericht_text}
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.disabled" fontStyle="italic">
|
||||
Kein Bericht erfasst
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* Footer meta */}
|
||||
<Box sx={{ mt: 3, pt: 2, borderTop: 1, borderColor: 'divider' }}>
|
||||
<Typography variant="caption" color="text.disabled">
|
||||
Angelegt: {formatDE(einsatz.created_at, 'dd.MM.yyyy HH:mm')}
|
||||
{' · '}
|
||||
Zuletzt geändert: {formatDE(einsatz.updated_at, 'dd.MM.yyyy HH:mm')}
|
||||
{' · '}
|
||||
Alarmierung via {einsatz.alarmierung_art}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Container>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default EinsatzDetail;
|
||||
Reference in New Issue
Block a user