new features

This commit is contained in:
Matthias Hochmeister
2026-03-23 14:01:39 +01:00
parent d2dc64d54a
commit 3326156b15
35 changed files with 1341 additions and 257 deletions

View File

@@ -21,11 +21,13 @@ import {
Select,
MenuItem,
FormControl,
FormControlLabel,
InputLabel,
CircularProgress,
Alert,
Popover,
Stack,
Switch,
Tooltip,
} from '@mui/material';
import {
@@ -83,6 +85,7 @@ const EMPTY_FORM: CreateBuchungInput = {
buchungsArt: 'intern',
kontaktPerson: '',
kontaktTelefon: '',
ganztaegig: false,
};
// ---------------------------------------------------------------------------
@@ -218,6 +221,7 @@ function FahrzeugBuchungen() {
const [form, setForm] = useState<CreateBuchungInput>({ ...EMPTY_FORM });
const [dialogLoading, setDialogLoading] = useState(false);
const [dialogError, setDialogError] = useState<string | null>(null);
const [overrideOutOfService, setOverrideOutOfService] = useState(false);
const [availability, setAvailability] = useState<{
available: boolean;
reason?: string;
@@ -254,6 +258,7 @@ function FahrzeugBuchungen() {
setForm({ ...EMPTY_FORM });
setDialogError(null);
setAvailability(null);
setOverrideOutOfService(false);
setDialogOpen(true);
};
@@ -265,6 +270,7 @@ function FahrzeugBuchungen() {
setForm({ ...EMPTY_FORM, fahrzeugId: vehicleId, beginn: dateStr, ende: dateEndStr });
setDialogError(null);
setAvailability(null);
setOverrideOutOfService(false);
setDialogOpen(true);
};
@@ -276,27 +282,33 @@ function FahrzeugBuchungen() {
...form,
beginn: new Date(form.beginn).toISOString(),
ende: new Date(form.ende).toISOString(),
ganztaegig: form.ganztaegig || false,
};
if (editingBooking) {
await bookingApi.update(editingBooking.id, payload);
notification.showSuccess('Buchung aktualisiert');
} else {
await bookingApi.create(payload);
await bookingApi.create({ ...payload, ignoreOutOfService: overrideOutOfService } as any);
notification.showSuccess('Buchung erstellt');
}
setDialogOpen(false);
loadData();
} catch (e: unknown) {
const axiosError = e as { response?: { status?: number; data?: { message?: string; reason?: string } }; message?: string };
if (axiosError?.response?.status === 409) {
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');
try {
const axiosError = e as { response?: { status?: number; data?: { message?: string; reason?: string } }; message?: string };
if (axiosError?.response?.status === 409) {
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(axiosError?.response?.data?.message || 'Fahrzeug ist im gewählten Zeitraum bereits gebucht');
}
} else {
setDialogError('Fahrzeug ist im gewählten Zeitraum bereits gebucht');
const msg = axiosError?.response?.data?.message || axiosError?.message || 'Fehler beim Speichern';
setDialogError(msg);
}
} else {
setDialogError(axiosError?.message || 'Fehler beim Speichern');
} catch {
setDialogError(e instanceof Error ? e.message : 'Fehler beim Speichern');
}
} finally {
setDialogLoading(false);
@@ -495,11 +507,6 @@ function FahrzeugBuchungen() {
<Typography variant="body2" fontWeight={600}>
{vehicle.bezeichnung}
</Typography>
{vehicle.amtliches_kennzeichen && (
<Typography variant="caption" color="text.secondary">
{vehicle.amtliches_kennzeichen}
</Typography>
)}
</TableCell>
{weekDays.map((day) => {
const cellBookings = getBookingsForCell(vehicle.id, day);
@@ -774,16 +781,39 @@ function FahrzeugBuchungen() {
}
/>
<FormControlLabel
control={
<Switch
checked={form.ganztaegig || false}
onChange={(e) => {
const checked = e.target.checked;
setForm((f) => {
if (checked && f.beginn) {
const dateStr = f.beginn.split('T')[0];
return { ...f, ganztaegig: true, beginn: `${dateStr}T00:00`, ende: f.ende ? `${(f.ende.split('T')[0])}T23:59` : `${dateStr}T23:59` };
}
return { ...f, ganztaegig: checked };
});
}}
/>
}
label="Ganztägig"
/>
<TextField
fullWidth
size="small"
label="Beginn"
type="datetime-local"
type={form.ganztaegig ? 'date' : 'datetime-local'}
required
value={form.beginn}
onChange={(e) =>
setForm((f) => ({ ...f, beginn: e.target.value }))
}
value={form.ganztaegig ? (form.beginn?.split('T')[0] || '') : form.beginn}
onChange={(e) => {
if (form.ganztaegig) {
setForm((f) => ({ ...f, beginn: `${e.target.value}T00:00` }));
} else {
setForm((f) => ({ ...f, beginn: e.target.value }));
}
}}
InputLabelProps={{ shrink: true }}
/>
@@ -791,12 +821,16 @@ function FahrzeugBuchungen() {
fullWidth
size="small"
label="Ende"
type="datetime-local"
type={form.ganztaegig ? 'date' : 'datetime-local'}
required
value={form.ende}
onChange={(e) =>
setForm((f) => ({ ...f, ende: e.target.value }))
}
value={form.ganztaegig ? (form.ende?.split('T')[0] || '') : form.ende}
onChange={(e) => {
if (form.ganztaegig) {
setForm((f) => ({ ...f, ende: `${e.target.value}T23:59` }));
} else {
setForm((f) => ({ ...f, ende: e.target.value }));
}
}}
InputLabelProps={{ shrink: true }}
/>
@@ -818,16 +852,34 @@ function FahrzeugBuchungen() {
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"
/>
<Box>
<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"
/>
<FormControlLabel
control={
<Switch
checked={overrideOutOfService}
onChange={(e) => setOverrideOutOfService(e.target.checked)}
color="warning"
size="small"
/>
}
label={
<Typography variant="body2" color="warning.main">
Trotz Außer-Dienst-Status buchen
</Typography>
}
sx={{ mt: 0.5 }}
/>
</Box>
) : (
<Chip
icon={<Warning />}