rework vehicle handling
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user