From 690f260b71736a111b2bd14c1b51e7fdf3b6030c Mon Sep 17 00:00:00 2001 From: Matthias Hochmeister Date: Mon, 23 Mar 2026 16:47:36 +0100 Subject: [PATCH] new features --- backend/src/models/booking.model.ts | 2 + backend/src/services/bestellung.service.ts | 2 +- backend/src/services/booking.service.ts | 9 ++- backend/src/services/events.service.ts | 4 +- backend/src/services/vehicle.service.ts | 6 +- .../VehicleBookingQuickAddWidget.tsx | 11 +++- frontend/src/pages/FahrzeugBuchungen.tsx | 20 ++++-- frontend/src/pages/Kalender.tsx | 61 +++++++++++++------ frontend/src/types/booking.types.ts | 1 + 9 files changed, 80 insertions(+), 36 deletions(-) diff --git a/backend/src/models/booking.model.ts b/backend/src/models/booking.model.ts index ee94f91..e5b6646 100644 --- a/backend/src/models/booking.model.ts +++ b/backend/src/models/booking.model.ts @@ -19,6 +19,7 @@ export interface FahrzeugBuchung { beginn: Date; ende: Date; buchungs_art: BuchungsArt; + ganztaegig: boolean; gebucht_von: string; kontakt_person?: string | null; kontakt_telefon?: string | null; @@ -42,6 +43,7 @@ export interface FahrzeugBuchungListItem { buchungs_art: BuchungsArt; beginn: Date; ende: Date; + ganztaegig: boolean; abgesagt: boolean; gebucht_von: string; gebucht_von_name?: string | null; diff --git a/backend/src/services/bestellung.service.ts b/backend/src/services/bestellung.service.ts index d451673..02482ee 100644 --- a/backend/src/services/bestellung.service.ts +++ b/backend/src/services/bestellung.service.ts @@ -162,7 +162,7 @@ async function getOrderById(id: number) { ]); return { - ...orderResult.rows[0], + bestellung: orderResult.rows[0], positionen: positionen.rows, dateien: dateien.rows, erinnerungen: erinnerungen.rows, diff --git a/backend/src/services/booking.service.ts b/backend/src/services/booking.service.ts index 3b1aba4..beea60e 100644 --- a/backend/src/services/booking.service.ts +++ b/backend/src/services/booking.service.ts @@ -26,6 +26,7 @@ function rowToListItem(row: any): FahrzeugBuchungListItem { buchungs_art: row.buchungs_art, beginn: new Date(row.beginn), ende: new Date(row.ende), + ganztaegig: row.ganztaegig ?? false, abgesagt: row.abgesagt, gebucht_von: row.gebucht_von, gebucht_von_name: row.gebucht_von_name ?? null, @@ -41,6 +42,7 @@ function rowToBuchung(row: any): FahrzeugBuchung { beginn: new Date(row.beginn), ende: new Date(row.ende), buchungs_art: row.buchungs_art, + ganztaegig: row.ganztaegig ?? false, gebucht_von: row.gebucht_von, kontakt_person: row.kontakt_person ?? null, kontakt_telefon: row.kontakt_telefon ?? null, @@ -78,7 +80,7 @@ class BookingService { const query = ` SELECT b.id, b.fahrzeug_id, b.titel, b.buchungs_art::text AS buchungs_art, - b.beginn, b.ende, b.abgesagt, b.gebucht_von, + b.beginn, b.ende, b.abgesagt, b.gebucht_von, b.ganztaegig, f.bezeichnung AS fahrzeug_name, f.amtliches_kennzeichen AS fahrzeug_kennzeichen, u.name AS gebucht_von_name FROM fahrzeug_buchungen b @@ -103,7 +105,7 @@ class BookingService { const query = ` SELECT b.id, b.fahrzeug_id, b.titel, b.buchungs_art::text AS buchungs_art, - b.beginn, b.ende, b.abgesagt, b.gebucht_von, + b.beginn, b.ende, b.abgesagt, b.gebucht_von, b.ganztaegig, f.bezeichnung AS fahrzeug_name, f.amtliches_kennzeichen AS fahrzeug_kennzeichen, u.name AS gebucht_von_name FROM fahrzeug_buchungen b @@ -125,7 +127,7 @@ class BookingService { SELECT b.id, b.fahrzeug_id, b.titel, b.beschreibung, b.buchungs_art::text AS buchungs_art, - b.beginn, b.ende, + b.beginn, b.ende, b.ganztaegig, b.gebucht_von, b.kontakt_person, b.kontakt_telefon, b.abgesagt, b.abgesagt_grund, b.erstellt_am, b.aktualisiert_am, @@ -300,6 +302,7 @@ class BookingService { if (data.buchungsArt !== undefined) addField('buchungs_art', data.buchungsArt, 'fahrzeug_buchung_art'); if (data.kontaktPerson !== undefined) addField('kontakt_person', data.kontaktPerson); if (data.kontaktTelefon !== undefined) addField('kontakt_telefon', data.kontaktTelefon); + if (data.ganztaegig !== undefined) addField('ganztaegig', data.ganztaegig); if (setClauses.length === 0) { throw new Error('No fields to update'); diff --git a/backend/src/services/events.service.ts b/backend/src/services/events.service.ts index cbaaf4d..6446982 100644 --- a/backend/src/services/events.service.ts +++ b/backend/src/services/events.service.ts @@ -393,7 +393,9 @@ class EventsService { * Capped at 100 instances and 2 years from the start date. */ private generateRecurrenceDates(startDate: Date, _endDate: Date, config: WiederholungConfig): Date[] { const dates: Date[] = []; - const limitDate = config.bis ? new Date(config.bis + 'T23:59:59Z') : new Date(0); + const defaultLimit = new Date(startDate); + defaultLimit.setUTCFullYear(defaultLimit.getUTCFullYear() + 2); + const limitDate = config.bis ? new Date(config.bis + 'T23:59:59Z') : defaultLimit; const interval = config.intervall ?? 1; // Cap at 100 instances max, and 2 years const maxDate = new Date(startDate); diff --git a/backend/src/services/vehicle.service.ts b/backend/src/services/vehicle.service.ts index 29f90fb..b1de760 100644 --- a/backend/src/services/vehicle.service.ts +++ b/backend/src/services/vehicle.service.ts @@ -396,8 +396,9 @@ class VehicleService { // Auto-update next service date on the vehicle when result is 'bestanden' if (data.ergebnis === 'bestanden' && data.naechste_faelligkeit) { + const column = data.art === '§57a Prüfung' ? 'paragraph57a_faellig_am' : 'naechste_wartung_am'; await pool.query( - `UPDATE fahrzeuge SET naechste_wartung_am = $1 WHERE id = $2`, + `UPDATE fahrzeuge SET ${column} = $1 WHERE id = $2`, [data.naechste_faelligkeit, fahrzeugId] ); } @@ -443,8 +444,9 @@ class VehicleService { // Auto-update next service date on the vehicle when result is 'bestanden' if (data.ergebnis === 'bestanden' && data.naechste_faelligkeit) { + const column = data.art === '§57a Prüfung' ? 'paragraph57a_faellig_am' : 'naechste_wartung_am'; await pool.query( - `UPDATE fahrzeuge SET naechste_wartung_am = $1 WHERE id = $2`, + `UPDATE fahrzeuge SET ${column} = $1 WHERE id = $2`, [data.naechste_faelligkeit, fahrzeugId] ); } diff --git a/frontend/src/components/dashboard/VehicleBookingQuickAddWidget.tsx b/frontend/src/components/dashboard/VehicleBookingQuickAddWidget.tsx index 597e737..0c463b9 100644 --- a/frontend/src/components/dashboard/VehicleBookingQuickAddWidget.tsx +++ b/frontend/src/components/dashboard/VehicleBookingQuickAddWidget.tsx @@ -56,12 +56,17 @@ const VehicleBookingQuickAddWidget: React.FC = () => { const mutation = useMutation({ mutationFn: () => { + const beginnDate = new Date(beginn); + const endeDate = new Date(ende); + if (isNaN(beginnDate.getTime()) || isNaN(endeDate.getTime())) { + return Promise.reject(new Error('Ungültiges Datum')); + } const data: CreateBuchungInput = { fahrzeugId, titel: titel.trim(), beschreibung: beschreibung.trim() || null, - beginn: new Date(beginn).toISOString(), - ende: new Date(ende).toISOString(), + beginn: beginnDate.toISOString(), + ende: endeDate.toISOString(), buchungsArt: 'intern', }; return bookingApi.create(data); @@ -120,7 +125,7 @@ const VehicleBookingQuickAddWidget: React.FC = () => { > {(vehicles ?? []).map((v) => ( - {v.bezeichnung}{v.amtliches_kennzeichen ? ` (${v.amtliches_kennzeichen})` : ''} + {v.bezeichnung} ))} diff --git a/frontend/src/pages/FahrzeugBuchungen.tsx b/frontend/src/pages/FahrzeugBuchungen.tsx index 76cb24e..2ed5394 100644 --- a/frontend/src/pages/FahrzeugBuchungen.tsx +++ b/frontend/src/pages/FahrzeugBuchungen.tsx @@ -287,10 +287,17 @@ function FahrzeugBuchungen() { setDialogLoading(true); setDialogError(null); try { + const beginnDate = new Date(form.beginn); + const endeDate = new Date(form.ende); + if (isNaN(beginnDate.getTime()) || isNaN(endeDate.getTime())) { + setDialogError('Ungültiges Datum. Bitte Beginn und Ende prüfen.'); + setDialogLoading(false); + return; + } const payload: CreateBuchungInput = { ...form, - beginn: new Date(form.beginn).toISOString(), - ende: new Date(form.ende).toISOString(), + beginn: beginnDate.toISOString(), + ende: endeDate.toISOString(), ganztaegig: form.ganztaegig || false, }; if (editingBooking) { @@ -374,6 +381,7 @@ function FahrzeugBuchungen() { buchungsArt: detailBooking.buchungs_art, kontaktPerson: '', kontaktTelefon: '', + ganztaegig: detailBooking.ganztaegig || false, }); setDialogError(null); setAvailability(null); @@ -698,9 +706,10 @@ function FahrzeugBuchungen() { }} /> - {format(parseISO(detailBooking.beginn), 'dd.MM.yyyy HH:mm')} - {' – '} - {format(parseISO(detailBooking.ende), 'dd.MM.yyyy HH:mm')} + {detailBooking.ganztaegig + ? `${format(parseISO(detailBooking.beginn), 'dd.MM.yyyy')} – ${format(parseISO(detailBooking.ende), 'dd.MM.yyyy')} (Ganztägig)` + : `${format(parseISO(detailBooking.beginn), 'dd.MM.yyyy HH:mm')} – ${format(parseISO(detailBooking.ende), 'dd.MM.yyyy HH:mm')}` + } {detailBooking.gebucht_von_name && ( @@ -778,7 +787,6 @@ function FahrzeugBuchungen() { {vehicles.map((v) => ( {v.bezeichnung} - {v.amtliches_kennzeichen ? ` (${v.amtliches_kennzeichen})` : ''} ))} diff --git a/frontend/src/pages/Kalender.tsx b/frontend/src/pages/Kalender.tsx index 309f23b..3d977c3 100644 --- a/frontend/src/pages/Kalender.tsx +++ b/frontend/src/pages/Kalender.tsx @@ -158,6 +158,7 @@ const EMPTY_BOOKING_FORM: CreateBuchungInput = { buchungsArt: 'intern', kontaktPerson: '', kontaktTelefon: '', + ganztaegig: false, }; // ────────────────────────────────────────────────────────────────────────────── @@ -1446,7 +1447,7 @@ function VeranstaltungFormDialog({ try { const createPayload: CreateVeranstaltungInput = { ...form, - wiederholung: (!editingEvent && wiederholungAktiv && wiederholungBis) + wiederholung: ((!editingEvent || (editingEvent.wiederholung && !editingEvent.wiederholung_parent_id)) && wiederholungAktiv && wiederholungBis) ? { typ: wiederholungTyp, bis: wiederholungBis, @@ -1458,7 +1459,7 @@ function VeranstaltungFormDialog({ : null, }; if (editingEvent) { - await eventsApi.updateEvent(editingEvent.id, form); + await eventsApi.updateEvent(editingEvent.id, createPayload); notification.showSuccess('Veranstaltung aktualisiert'); } else { await eventsApi.createEvent(createPayload); @@ -1636,10 +1637,10 @@ function VeranstaltungFormDialog({ {(!editingEvent || (editingEvent && editingEvent.wiederholung)) && ( <> - {editingEvent && editingEvent.wiederholung ? ( + {editingEvent && editingEvent.wiederholung && editingEvent.wiederholung_parent_id ? ( <> - Wiederholung kann nicht bearbeitet werden + Wiederholung kann nicht bearbeitet werden (Einzeltermin einer Serie) } @@ -1659,7 +1660,7 @@ function VeranstaltungFormDialog({ )} {wiederholungAktiv && ( - + Wiederholung setWiederholungBis(e.target.value)} InputLabelProps={{ shrink: true }} fullWidth - disabled={!!editingEvent} + disabled={!!editingEvent?.wiederholung_parent_id} helperText="Letztes Datum für Wiederholungen" /> @@ -2085,6 +2086,7 @@ export default function Kalender() { buchungsArt: detailBooking.buchungs_art, kontaktPerson: '', kontaktTelefon: '', + ganztaegig: (detailBooking as any).ganztaegig || false, }); setBookingDialogError(null); setAvailability(null); @@ -2128,7 +2130,17 @@ export default function Kalender() { } const beginnIso = fromGermanDateTime(bookingForm.beginn)!; const endeIso = fromGermanDateTime(bookingForm.ende)!; - if (new Date(endeIso) <= new Date(beginnIso)) { + const beginnDate = new Date(beginnIso); + const endeDate = new Date(endeIso); + if (isNaN(beginnDate.getTime())) { + setBookingDialogError('Ungültiges Beginn-Datum'); + return; + } + if (isNaN(endeDate.getTime())) { + setBookingDialogError('Ungültiges Ende-Datum'); + return; + } + if (endeDate <= beginnDate) { setBookingDialogError('Ende muss nach dem Beginn liegen'); return; } @@ -2137,8 +2149,8 @@ export default function Kalender() { try { const payload: CreateBuchungInput = { ...bookingForm, - beginn: (() => { const iso = fromGermanDateTime(bookingForm.beginn); return iso ? new Date(iso).toISOString() : new Date(bookingForm.beginn).toISOString(); })(), - ende: (() => { const iso = fromGermanDateTime(bookingForm.ende); return iso ? new Date(iso).toISOString() : new Date(bookingForm.ende).toISOString(); })(), + beginn: beginnDate.toISOString(), + ende: endeDate.toISOString(), }; if (editingBooking) { await bookingApi.update(editingBooking.id, payload); @@ -2874,11 +2886,6 @@ export default function Kalender() { {vehicle.bezeichnung} - {vehicle.amtliches_kennzeichen && ( - - {vehicle.amtliches_kennzeichen} - - )} {weekDays.map((day) => { const cellBookings = getBookingsForCell(vehicle.id, day); @@ -3010,9 +3017,10 @@ export default function Kalender() { }} /> - {fnsFormat(parseISO(detailBooking.beginn), 'dd.MM.yyyy HH:mm')} - {' – '} - {fnsFormat(parseISO(detailBooking.ende), 'dd.MM.yyyy HH:mm')} + {(detailBooking as any).ganztaegig + ? `${fnsFormat(parseISO(detailBooking.beginn), 'dd.MM.yyyy')} – ${fnsFormat(parseISO(detailBooking.ende), 'dd.MM.yyyy')} (Ganztägig)` + : `${fnsFormat(parseISO(detailBooking.beginn), 'dd.MM.yyyy HH:mm')} – ${fnsFormat(parseISO(detailBooking.ende), 'dd.MM.yyyy HH:mm')}` + } {detailBooking.gebucht_von_name && ( @@ -3074,7 +3082,7 @@ export default function Kalender() { > {vehicles.map((v) => ( - {v.bezeichnung}{v.amtliches_kennzeichen ? ` (${v.amtliches_kennzeichen})` : ''} + {v.bezeichnung} ))} @@ -3088,6 +3096,19 @@ export default function Kalender() { } /> + { + const checked = e.target.checked; + setBookingForm((f) => ({ ...f, ganztaegig: checked })); + }} + /> + } + label="Ganztägig" + /> +