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

@@ -40,6 +40,7 @@ import {
IosShare,
CheckCircle,
Warning,
Block,
} from '@mui/icons-material';
import DashboardLayout from '../components/dashboard/DashboardLayout';
import { useAuth } from '../contexts/AuthContext';
@@ -62,6 +63,7 @@ import {
isToday,
parseISO,
isSameDay,
isWithinInterval,
} from 'date-fns';
import { de } from 'date-fns/locale';
@@ -156,6 +158,18 @@ function FahrzeugBuchungen() {
});
};
const isOutOfService = (vehicle: Fahrzeug, day: Date): boolean => {
if (!vehicle.ausser_dienst_von || !vehicle.ausser_dienst_bis) return false;
try {
return isWithinInterval(day, {
start: parseISO(vehicle.ausser_dienst_von),
end: parseISO(vehicle.ausser_dienst_bis),
});
} catch {
return false;
}
};
// ── Create / Edit dialog ──────────────────────────────────────────────────
const [dialogOpen, setDialogOpen] = useState(false);
const [editingBooking, setEditingBooking] =
@@ -163,7 +177,11 @@ function FahrzeugBuchungen() {
const [form, setForm] = useState<CreateBuchungInput>({ ...EMPTY_FORM });
const [dialogLoading, setDialogLoading] = useState(false);
const [dialogError, setDialogError] = useState<string | null>(null);
const [availability, setAvailability] = useState<boolean | null>(null);
const [availability, setAvailability] = useState<{
available: boolean;
reason?: string;
ausserDienstBis?: string;
} | null>(null);
// Check availability whenever the relevant form fields change
useEffect(() => {
@@ -178,8 +196,8 @@ function FahrzeugBuchungen() {
new Date(form.beginn),
new Date(form.ende)
)
.then(({ available }) => {
if (!cancelled) setAvailability(available);
.then((result) => {
if (!cancelled) setAvailability(result);
})
.catch(() => {
if (!cancelled) setAvailability(null);
@@ -227,9 +245,14 @@ function FahrzeugBuchungen() {
setDialogOpen(false);
loadData();
} catch (e: unknown) {
const axiosError = e as { response?: { status?: number }; message?: string };
const axiosError = e as { response?: { status?: number; data?: { message?: string; reason?: string } }; message?: string };
if (axiosError?.response?.status === 409) {
setDialogError('Fahrzeug ist im gewählten Zeitraum bereits gebucht');
const reason = axiosError?.response?.data?.reason;
if (reason === 'out_of_service') {
setDialogError(axiosError?.response?.data?.message || 'Fahrzeug ist im gewählten Zeitraum außer Dienst');
} else {
setDialogError('Fahrzeug ist im gewählten Zeitraum bereits gebucht');
}
} else {
setDialogError(axiosError?.message || 'Fehler beim Speichern');
}
@@ -437,7 +460,8 @@ function FahrzeugBuchungen() {
</TableCell>
{weekDays.map((day) => {
const cellBookings = getBookingsForCell(vehicle.id, day);
const isFree = cellBookings.length === 0;
const oos = isOutOfService(vehicle, day);
const isFree = cellBookings.length === 0 && !oos;
return (
<TableCell
key={day.toISOString()}
@@ -445,8 +469,12 @@ function FahrzeugBuchungen() {
isFree ? handleCellClick(vehicle.id, day) : undefined
}
sx={{
bgcolor: isFree ? (theme) => theme.palette.mode === 'dark' ? 'success.900' : 'success.50' : undefined,
cursor: isFree && canCreate ? 'pointer' : 'default',
bgcolor: oos
? (theme) => theme.palette.mode === 'dark' ? 'error.900' : 'error.50'
: isFree
? (theme) => theme.palette.mode === 'dark' ? 'success.900' : 'success.50'
: undefined,
cursor: isFree && canCreate ? 'pointer' : oos ? 'not-allowed' : 'default',
'&:hover': isFree && canCreate
? { bgcolor: (theme) => theme.palette.mode === 'dark' ? 'success.800' : 'success.100' }
: {},
@@ -454,6 +482,18 @@ function FahrzeugBuchungen() {
verticalAlign: 'top',
}}
>
{oos && (
<Tooltip title="Fahrzeug außer Dienst">
<Chip
icon={<Block fontSize="small" />}
label="Außer Dienst"
size="small"
color="error"
variant="outlined"
sx={{ fontSize: '0.6rem', height: 18, mb: 0.25, width: '100%' }}
/>
</Tooltip>
)}
{cellBookings.map((b) => (
<Tooltip
key={b.id}
@@ -519,6 +559,19 @@ function FahrzeugBuchungen() {
/>
<Typography variant="caption">Frei</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Box
sx={{
width: 16,
height: 16,
bgcolor: (theme) => theme.palette.mode === 'dark' ? 'error.900' : 'error.50',
border: '1px solid',
borderColor: (theme) => theme.palette.mode === 'dark' ? 'error.700' : 'error.300',
borderRadius: 0.5,
}}
/>
<Typography variant="caption">Außer Dienst</Typography>
</Box>
{(Object.entries(BUCHUNGS_ART_LABELS) as [BuchungsArt, string][]).map(
([art, label]) => (
<Box key={art} sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
@@ -702,15 +755,29 @@ function FahrzeugBuchungen() {
Verfügbarkeit wird geprüft...
</Typography>
</Box>
) : availability.available ? (
<Chip
icon={<CheckCircle />}
label="Fahrzeug verfügbar"
color="success"
size="small"
/>
) : availability.reason === 'out_of_service' ? (
<Chip
icon={<Block />}
label={
availability.ausserDienstBis
? `Außer Dienst bis ${new Date(availability.ausserDienstBis).toLocaleDateString('de-DE')} (geschätzt)`
: 'Fahrzeug ist außer Dienst'
}
color="error"
size="small"
/>
) : (
<Chip
icon={availability ? <CheckCircle /> : <Warning />}
label={
availability
? 'Fahrzeug verfügbar'
: 'Konflikt: bereits gebucht'
}
color={availability ? 'success' : 'error'}
icon={<Warning />}
label="Konflikt: bereits gebucht"
color="error"
size="small"
/>
)}