rework vehicle handling

This commit is contained in:
Matthias Hochmeister
2026-02-28 13:34:16 +01:00
parent 84cf505511
commit 41fc41bee4
13 changed files with 931 additions and 1228 deletions

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useState, useCallback } from 'react';
import {
Alert,
Box,
Card,
CardActionArea,
@@ -15,7 +16,6 @@ import {
TextField,
Tooltip,
Typography,
Alert,
} from '@mui/material';
import {
Add,
@@ -35,8 +35,6 @@ import {
FahrzeugListItem,
FahrzeugStatus,
FahrzeugStatusLabel,
PruefungArt,
PruefungArtLabel,
} from '../types/vehicle.types';
import { usePermissions } from '../hooks/usePermissions';
@@ -64,13 +62,23 @@ function inspBadgeColor(tage: number | null): InspBadgeColor {
}
function inspBadgeLabel(art: string, tage: number | null, faelligAm: string | null): string {
const artShort = art; // 'HU', 'AU', etc.
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 `${artShort}: ${date}`;
if (tage < 0) return `${artShort}: ÜBERFÄLLIG (${date})`;
if (tage === 0) return `${artShort}: heute (${date})`;
return `${artShort}: ${date}`;
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 ──────────────────────────────────────────────────────────────
@@ -81,15 +89,23 @@ interface VehicleCardProps {
}
const VehicleCard: React.FC<VehicleCardProps> = ({ vehicle, onClick }) => {
const status = vehicle.status as FahrzeugStatus;
const status = vehicle.status as FahrzeugStatus;
const statusCfg = STATUS_CONFIG[status] ?? STATUS_CONFIG[FahrzeugStatus.Einsatzbereit];
const isSchaden = status === FahrzeugStatus.AusserDienstSchaden;
// Collect inspection badges (only for types where a faellig_am exists)
const inspBadges: { art: string; tage: number | null; faelligAm: string | null }[] = [
{ art: '§57a', tage: vehicle.paragraph57a_tage_bis_faelligkeit, faelligAm: vehicle.paragraph57a_faellig_am },
{ art: 'Wartung', tage: vehicle.wartung_tage_bis_faelligkeit, faelligAm: vehicle.naechste_wartung_am },
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 (
@@ -116,7 +132,6 @@ const VehicleCard: React.FC<VehicleCardProps> = ({ vehicle, onClick }) => {
onClick={() => onClick(vehicle.id)}
sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', alignItems: 'stretch' }}
>
{/* Vehicle image / placeholder */}
{vehicle.bild_url ? (
<CardMedia
component="img"
@@ -140,7 +155,6 @@ const VehicleCard: React.FC<VehicleCardProps> = ({ vehicle, onClick }) => {
)}
<CardContent sx={{ flexGrow: 1, pb: '8px !important' }}>
{/* Title row */}
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', mb: 0.5 }}>
<Box>
<Typography variant="h6" component="div" lineHeight={1.2}>
@@ -159,7 +173,6 @@ const VehicleCard: React.FC<VehicleCardProps> = ({ vehicle, onClick }) => {
</Box>
</Box>
{/* Status badge */}
<Box sx={{ mb: 1 }}>
<Chip
icon={statusCfg.icon}
@@ -170,7 +183,6 @@ const VehicleCard: React.FC<VehicleCardProps> = ({ vehicle, onClick }) => {
/>
</Box>
{/* Crew config */}
{vehicle.besatzung_soll && (
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
Besatzung: {vehicle.besatzung_soll}
@@ -178,7 +190,6 @@ const VehicleCard: React.FC<VehicleCardProps> = ({ vehicle, onClick }) => {
</Typography>
)}
{/* Inspection badges */}
{inspBadges.length > 0 && (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{inspBadges.map((b) => {
@@ -188,11 +199,7 @@ const VehicleCard: React.FC<VehicleCardProps> = ({ vehicle, onClick }) => {
return (
<Tooltip
key={b.art}
title={`${PruefungArtLabel[b.art as PruefungArt] ?? b.art}: ${
b.tage !== null && b.tage < 0
? `Seit ${Math.abs(b.tage)} Tagen überfällig!`
: `Fällig am ${new Date(b.faelligAm!).toLocaleDateString('de-DE')}`
}`}
title={inspTooltipTitle(b.fullLabel, b.tage, b.faelligAm)}
>
<Chip
size="small"
@@ -249,16 +256,18 @@ function Fahrzeuge() {
);
});
// Summary counts
const einsatzbereit = vehicles.filter((v) => v.status === FahrzeugStatus.Einsatzbereit).length;
// An overdue inspection exists if §57a OR Wartung is past due
const hasOverdue = vehicles.some(
(v) => v.naechste_pruefung_tage !== null && v.naechste_pruefung_tage < 0
(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 (
<DashboardLayout>
<Container maxWidth="xl">
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
<Box>
<Typography variant="h4" gutterBottom sx={{ mb: 0 }}>
@@ -268,12 +277,7 @@ function Fahrzeuge() {
<Typography variant="body2" color="text.secondary">
{vehicles.length} Fahrzeug{vehicles.length !== 1 ? 'e' : ''} gesamt
{' · '}
<Typography
component="span"
variant="body2"
color="success.main"
fontWeight={600}
>
<Typography component="span" variant="body2" color="success.main" fontWeight={600}>
{einsatzbereit} einsatzbereit
</Typography>
</Typography>
@@ -281,15 +285,12 @@ function Fahrzeuge() {
</Box>
</Box>
{/* Overdue inspection global warning */}
{hasOverdue && (
<Alert severity="error" sx={{ mb: 2 }} icon={<Warning />}>
<strong>Achtung:</strong> Mindestens ein Fahrzeug hat eine überfällige Prüfungsfrist.
Betroffene Fahrzeuge dürfen bis zur Durchführung der Prüfung ggf. nicht eingesetzt werden.
<strong>Achtung:</strong> Mindestens ein Fahrzeug hat eine überfällige Prüfungs- oder Wartungsfrist.
</Alert>
)}
{/* Search bar */}
<TextField
placeholder="Fahrzeug suchen (Bezeichnung, Kennzeichen, Hersteller…)"
value={search}
@@ -306,21 +307,18 @@ function Fahrzeuge() {
}}
/>
{/* Loading state */}
{loading && (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<CircularProgress />
</Box>
)}
{/* Error state */}
{!loading && error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
{/* Empty state */}
{!loading && !error && filtered.length === 0 && (
<Box sx={{ textAlign: 'center', py: 8 }}>
<DirectionsCar sx={{ fontSize: 64, color: 'text.disabled', mb: 2 }} />
@@ -332,7 +330,6 @@ function Fahrzeuge() {
</Box>
)}
{/* Vehicle grid */}
{!loading && !error && filtered.length > 0 && (
<Grid container spacing={3}>
{filtered.map((vehicle) => (
@@ -346,7 +343,6 @@ function Fahrzeuge() {
</Grid>
)}
{/* FAB — add vehicle (shown to write-role users only; role check done server-side) */}
{isAdmin && (
<Fab
color="primary"