This commit is contained in:
Matthias Hochmeister
2026-03-13 19:23:39 +01:00
parent 02d9d808b2
commit bc6d09200a
15 changed files with 610 additions and 74 deletions

View File

@@ -66,6 +66,7 @@ import {
CreateWartungslogPayload,
UpdateStatusPayload,
WartungslogArt,
OverlappingBooking,
} from '../types/vehicle.types';
import type { AusruestungListItem } from '../types/equipment.types';
import { AusruestungStatus, AusruestungStatusLabel } from '../types/equipment.types';
@@ -92,14 +93,12 @@ const STATUS_ICONS: Record<FahrzeugStatus, React.ReactElement> = {
[FahrzeugStatus.Einsatzbereit]: <CheckCircle color="success" />,
[FahrzeugStatus.AusserDienstWartung]: <PauseCircle color="warning" />,
[FahrzeugStatus.AusserDienstSchaden]: <ErrorIcon color="error" />,
[FahrzeugStatus.InLehrgang]: <School color="info" />,
};
const STATUS_CHIP_COLOR: Record<FahrzeugStatus, 'success' | 'warning' | 'error' | 'info'> = {
[FahrzeugStatus.Einsatzbereit]: 'success',
[FahrzeugStatus.AusserDienstWartung]: 'warning',
[FahrzeugStatus.AusserDienstSchaden]: 'error',
[FahrzeugStatus.InLehrgang]: 'info',
};
// ── Date helpers ──────────────────────────────────────────────────────────────
@@ -118,6 +117,10 @@ function inspectionBadgeColor(tage: number | null): 'success' | 'warning' | 'err
return 'success';
}
function fmtDatetime(iso: string | Date | null | undefined): string {
return fmtDate(iso ? new Date(iso).toISOString() : null);
}
// ── Übersicht Tab ─────────────────────────────────────────────────────────────
interface UebersichtTabProps {
@@ -130,13 +133,30 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated,
const [statusDialogOpen, setStatusDialogOpen] = useState(false);
const [newStatus, setNewStatus] = useState<FahrzeugStatus>(vehicle.status);
const [bemerkung, setBemerkung] = useState(vehicle.status_bemerkung ?? '');
const [ausserDienstVon, setAusserDienstVon] = useState(
vehicle.ausser_dienst_von ? new Date(vehicle.ausser_dienst_von).toISOString().slice(0, 16) : ''
);
const [ausserDienstBis, setAusserDienstBis] = useState(
vehicle.ausser_dienst_bis ? new Date(vehicle.ausser_dienst_bis).toISOString().slice(0, 16) : ''
);
const [saving, setSaving] = useState(false);
const [saveError, setSaveError] = useState<string | null>(null);
const [overlappingBookings, setOverlappingBookings] = useState<OverlappingBooking[]>([]);
const isAusserDienst = (s: FahrzeugStatus) =>
s === FahrzeugStatus.AusserDienstWartung || s === FahrzeugStatus.AusserDienstSchaden;
const openDialog = () => {
setNewStatus(vehicle.status);
setBemerkung(vehicle.status_bemerkung ?? '');
setAusserDienstVon(
vehicle.ausser_dienst_von ? new Date(vehicle.ausser_dienst_von).toISOString().slice(0, 16) : ''
);
setAusserDienstBis(
vehicle.ausser_dienst_bis ? new Date(vehicle.ausser_dienst_bis).toISOString().slice(0, 16) : ''
);
setSaveError(null);
setOverlappingBookings([]);
setStatusDialogOpen(true);
};
@@ -149,9 +169,19 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated,
try {
setSaving(true);
setSaveError(null);
const payload: UpdateStatusPayload = { status: newStatus, bemerkung };
await vehiclesApi.updateStatus(vehicle.id, payload);
const payload: UpdateStatusPayload = {
status: newStatus,
bemerkung,
...(isAusserDienst(newStatus) && ausserDienstVon && ausserDienstBis
? {
ausserDienstVon: new Date(ausserDienstVon).toISOString(),
ausserDienstBis: new Date(ausserDienstBis).toISOString(),
}
: {}),
};
const result = await vehiclesApi.updateStatus(vehicle.id, payload);
setStatusDialogOpen(false);
setOverlappingBookings(result.overlappingBookings ?? []);
onStatusUpdated();
} catch (err: any) {
setSaveError(err?.response?.data?.message || 'Status konnte nicht gespeichert werden.');
@@ -160,6 +190,8 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated,
}
};
const canSave = !isAusserDienst(newStatus) || (!!ausserDienstVon && !!ausserDienstBis);
const isSchaden = vehicle.status === FahrzeugStatus.AusserDienstSchaden;
// Inspection deadline badges
@@ -177,6 +209,20 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated,
</Alert>
)}
{overlappingBookings.length > 0 && (
<Alert severity="warning" sx={{ mb: 2 }}>
<strong>Folgende Buchungen überschneiden sich mit dem Außer-Dienst-Zeitraum:</strong>
<Box component="ul" sx={{ mt: 1, mb: 0, pl: 2 }}>
{overlappingBookings.map((b) => (
<li key={b.id}>
<strong>{b.titel}</strong> · {fmtDate(b.beginn as unknown as string)} {fmtDate(b.ende as unknown as string)} · {b.gebucht_von_name}
</li>
))}
</Box>
Bitte prüfe, ob diese storniert werden müssen.
</Alert>
)}
{/* Status panel */}
<Paper variant="outlined" sx={{ p: 2, mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
@@ -184,11 +230,27 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated,
{STATUS_ICONS[vehicle.status]}
<Box>
<Typography variant="subtitle1" fontWeight={600}>Aktueller Status</Typography>
<Chip
label={FahrzeugStatusLabel[vehicle.status]}
color={STATUS_CHIP_COLOR[vehicle.status]}
size="small"
/>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
<Chip
label={FahrzeugStatusLabel[vehicle.status]}
color={STATUS_CHIP_COLOR[vehicle.status]}
size="small"
/>
{vehicle.aktiver_lehrgang && (
<Chip
icon={<School fontSize="small" />}
label="In Lehrgang"
color="info"
size="small"
/>
)}
</Box>
{(vehicle.status === FahrzeugStatus.AusserDienstWartung || vehicle.status === FahrzeugStatus.AusserDienstSchaden) &&
vehicle.ausser_dienst_von && vehicle.ausser_dienst_bis && (
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
Außer Dienst: {fmtDatetime(vehicle.ausser_dienst_von)} {fmtDatetime(vehicle.ausser_dienst_bis)} (geschätzt)
</Typography>
)}
{vehicle.status_bemerkung && (
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
{vehicle.status_bemerkung}
@@ -279,6 +341,33 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated,
))}
</Select>
</FormControl>
{isAusserDienst(newStatus) && (
<>
<TextField
label="Außer Dienst von *"
type="datetime-local"
fullWidth
sx={{ mb: 2 }}
value={ausserDienstVon}
onChange={(e) => setAusserDienstVon(e.target.value)}
InputLabelProps={{ shrink: true }}
/>
<TextField
label="Geschätztes Ende *"
type="datetime-local"
fullWidth
sx={{ mb: 1 }}
value={ausserDienstBis}
onChange={(e) => setAusserDienstBis(e.target.value)}
InputLabelProps={{ shrink: true }}
/>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 2 }}>
Zeitangabe ist eine Schätzung
</Typography>
</>
)}
<TextField
label="Bemerkung (optional)"
fullWidth
@@ -294,7 +383,7 @@ const UebersichtTab: React.FC<UebersichtTabProps> = ({ vehicle, onStatusUpdated,
<Button
variant="contained"
onClick={handleSaveStatus}
disabled={saving}
disabled={saving || !canSave}
startIcon={saving ? <CircularProgress size={16} /> : undefined}
>
Speichern