Files
dashboard/frontend/src/components/equipment/EquipmentAlerts.tsx
Matthias Hochmeister 5b8f40ab9a add now features
2026-03-01 14:41:45 +01:00

252 lines
7.5 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import {
Alert,
AlertTitle,
Box,
CircularProgress,
Link,
Typography,
} from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import { equipmentApi } from '../../services/equipment';
import {
AusruestungStatusLabel,
AusruestungStatus,
} from '../../types/equipment.types';
import type {
EquipmentStats,
AusruestungListItem,
} from '../../types/equipment.types';
function formatDate(iso: string): string {
return new Date(iso).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
});
}
interface EquipmentAlertsProps {
daysAhead?: number;
hideWhenEmpty?: boolean;
}
interface AlertGroup {
key: string;
severity: 'error' | 'warning';
title: string;
content: React.ReactNode;
}
const EquipmentAlerts: React.FC<EquipmentAlertsProps> = ({
daysAhead = 30,
hideWhenEmpty = true,
}) => {
const [stats, setStats] = useState<EquipmentStats | null>(null);
const [alerts, setAlerts] = useState<AusruestungListItem[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let mounted = true;
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const [statsData, alertsData] = await Promise.all([
equipmentApi.getStats(),
equipmentApi.getAlerts(daysAhead),
]);
if (mounted) {
setStats(statsData);
setAlerts(alertsData);
}
} catch {
if (mounted) setError('Ausrüstungshinweise konnten nicht geladen werden.');
} finally {
if (mounted) setLoading(false);
}
};
fetchData();
return () => { mounted = false; };
}, [daysAhead]);
if (loading) {
return (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 1 }}>
<CircularProgress size={16} />
<Typography variant="body2" color="text.secondary">
Ausrüstungsstatus wird geprüft...
</Typography>
</Box>
);
}
if (error) {
return <Alert severity="error">{error}</Alert>;
}
if (!stats) return null;
// Separate alerts into overdue vs. upcoming inspections
const overdueItems = alerts.filter(
(a) => a.pruefung_tage_bis_faelligkeit !== null && a.pruefung_tage_bis_faelligkeit < 0
);
const upcomingItems = alerts.filter(
(a) => a.pruefung_tage_bis_faelligkeit !== null && a.pruefung_tage_bis_faelligkeit >= 0
);
// Build alert groups based on stats
const groups: AlertGroup[] = [];
// 1. Overdue inspections
if (stats.inspectionsOverdue > 0 && overdueItems.length > 0) {
groups.push({
key: 'overdue',
severity: 'error',
title: `Überfällige Prüfungen (${stats.inspectionsOverdue})`,
content: (
<Box component="ul" sx={{ m: 0, pl: 2 }}>
{overdueItems.map((item) => {
const tage = Math.abs(item.pruefung_tage_bis_faelligkeit!);
const tageText = `seit ${tage} Tag${tage === 1 ? '' : 'en'} überfällig`;
return (
<Box key={item.id} component="li" sx={{ mb: 0.5 }}>
<Link
component={RouterLink}
to={`/ausruestung/${item.id}`}
color="inherit"
underline="hover"
sx={{ fontWeight: 500 }}
>
{item.bezeichnung}
</Link>
{item.kategorie_kurzname ? ` (${item.kategorie_kurzname})` : ''}
{' — '}
<Typography component="span" variant="body2">
{tageText}
{item.naechste_pruefung_am
? ` (${formatDate(item.naechste_pruefung_am)})`
: ''}
</Typography>
</Box>
);
})}
</Box>
),
});
}
// 2. Important equipment not ready (affects vehicle readiness)
if (stats.wichtigNichtBereit > 0) {
groups.push({
key: 'wichtig',
severity: 'error',
title: `Wichtige Ausrüstung nicht einsatzbereit (${stats.wichtigNichtBereit})`,
content: (
<Typography variant="body2">
{stats.wichtigNichtBereit} wichtige{stats.wichtigNichtBereit === 1 ? 's' : ''}
{' '}Ausrüstungsteil{stats.wichtigNichtBereit === 1 ? '' : 'e'} nicht
einsatzbereit Fahrzeugbereitschaft kann beeinträchtigt sein.
</Typography>
),
});
}
// 3. Damaged equipment
if (stats.beschaedigt > 0) {
groups.push({
key: 'beschaedigt',
severity: 'error',
title: `${AusruestungStatusLabel[AusruestungStatus.Beschaedigt]} (${stats.beschaedigt})`,
content: (
<Typography variant="body2">
{stats.beschaedigt} Ausrüstungsteil{stats.beschaedigt === 1 ? '' : 'e'}{' '}
{stats.beschaedigt === 1 ? 'ist' : 'sind'} als{' '}
{AusruestungStatusLabel[AusruestungStatus.Beschaedigt].toLowerCase()} gemeldet.
</Typography>
),
});
}
// 4. Upcoming inspections
if (stats.inspectionsDue > 0 && upcomingItems.length > 0) {
groups.push({
key: 'upcoming',
severity: 'warning',
title: `Prüfungen fällig in den nächsten ${daysAhead} Tagen (${stats.inspectionsDue})`,
content: (
<Box component="ul" sx={{ m: 0, pl: 2 }}>
{upcomingItems.map((item) => {
const tage = item.pruefung_tage_bis_faelligkeit!;
const tageText =
tage === 0
? 'heute fällig'
: `fällig in ${tage} Tag${tage === 1 ? '' : 'en'}`;
return (
<Box key={item.id} component="li" sx={{ mb: 0.5 }}>
<Link
component={RouterLink}
to={`/ausruestung/${item.id}`}
color="inherit"
underline="hover"
sx={{ fontWeight: 500 }}
>
{item.bezeichnung}
</Link>
{item.kategorie_kurzname ? ` (${item.kategorie_kurzname})` : ''}
{' — '}
<Typography component="span" variant="body2">
{tageText}
{item.naechste_pruefung_am
? ` (${formatDate(item.naechste_pruefung_am)})`
: ''}
</Typography>
</Box>
);
})}
</Box>
),
});
}
// 5. In maintenance
if (stats.inWartung > 0) {
groups.push({
key: 'wartung',
severity: 'warning',
title: `${AusruestungStatusLabel[AusruestungStatus.InWartung]} (${stats.inWartung})`,
content: (
<Typography variant="body2">
{stats.inWartung} Ausrüstungsteil{stats.inWartung === 1 ? '' : 'e'}{' '}
{stats.inWartung === 1 ? 'befindet' : 'befinden'} sich derzeit in Wartung.
</Typography>
),
});
}
// Nothing to show
if (groups.length === 0) {
if (hideWhenEmpty) return null;
return (
<Alert severity="success">
Alle Ausrüstung ist einsatzbereit. Keine Auffälligkeiten in den nächsten{' '}
{daysAhead} Tagen.
</Alert>
);
}
return (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
{groups.map(({ key, severity, title, content }) => (
<Alert key={key} severity={severity} variant="outlined">
<AlertTitle sx={{ fontWeight: 600 }}>{title}</AlertTitle>
{content}
</Alert>
))}
</Box>
);
};
export default EquipmentAlerts;