From 0b6a043f19d46e05e733dfb646be80e218f1b505 Mon Sep 17 00:00:00 2001 From: Pau Costa Date: Sat, 28 Mar 2026 18:19:57 +0100 Subject: [PATCH] Prevent past bookings and updates across web and API - Add validation logic to block creating or updating reservations in the past. - Update date pickers to enforce minimum selectable date as today. - Extend i18n files (`en.json`, `es-ES.json`) with new error messages. - Refine UI to disable interaction with past time slots and display appropriate messages. --- .../src/reservations/reservations.service.ts | 22 ++++++++++++++++ .../src/lib/components/BookingModal.svelte | 25 ++++++++++++++++--- apps/web/src/lib/i18n/locales/en.json | 2 ++ apps/web/src/lib/i18n/locales/es-ES.json | 2 ++ .../dashboard/building/[id]/+page.svelte | 1 + .../routes/dashboard/floor/[id]/+page.svelte | 1 + .../routes/dashboard/my-bookings/+page.svelte | 16 ++++++++++++ 7 files changed, 66 insertions(+), 3 deletions(-) diff --git a/apps/api/src/reservations/reservations.service.ts b/apps/api/src/reservations/reservations.service.ts index 2aef008..20c405a 100644 --- a/apps/api/src/reservations/reservations.service.ts +++ b/apps/api/src/reservations/reservations.service.ts @@ -31,6 +31,17 @@ export class ReservationsService { throw new BadRequestException('Start time must be before end time'); } + const now = new Date(); + if (space.type === SpaceType.DESK) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + if (start < today) { + throw new BadRequestException('Cannot book desks in the past'); + } + } else if (start < now) { + throw new BadRequestException('Cannot book in the past'); + } + // Rules if (space.type === SpaceType.DESK) { // Desks: Full day (setting to midnight of start and end of that day) @@ -204,6 +215,17 @@ export class ReservationsService { throw new BadRequestException('Start time must be before end time'); } + const now = new Date(); + if (space.type === SpaceType.DESK) { + const today = new Date(); + today.setHours(0, 0, 0, 0); + if (start < today) { + throw new BadRequestException('Cannot update desk reservation to the past'); + } + } else if (start < now) { + throw new BadRequestException('Cannot update reservation to the past'); + } + // Rules if (space.type === SpaceType.DESK) { start.setHours(0, 0, 0, 0); diff --git a/apps/web/src/lib/components/BookingModal.svelte b/apps/web/src/lib/components/BookingModal.svelte index d3fae87..d8ce122 100644 --- a/apps/web/src/lib/components/BookingModal.svelte +++ b/apps/web/src/lib/components/BookingModal.svelte @@ -72,6 +72,14 @@ start.setHours(0, 0, 0, 0); end = new Date(bookingDate); end.setHours(23, 59, 59, 999); + + const today = new Date(); + today.setHours(0, 0, 0, 0); + if (start < today) { + error = $t('booking.errorPast'); + loading = false; + return; + } } else { const [sh, sm] = startTime.split(':').map(Number); const [eh, em] = endTime.split(':').map(Number); @@ -82,6 +90,12 @@ end = new Date(bookingDate); end.setHours(eh, em, 0, 0); + if (start < new Date()) { + error = $t('booking.errorPast'); + loading = false; + return; + } + if (end <= start) { error = $t('booking.errorTime'); loading = false; @@ -143,6 +157,10 @@ const slotStart = new Date(bookingDate); slotStart.setHours(h, m, 0, 0); + if (slotStart < new Date()) { + return { isPast: true }; + } + return dayReservations.find(r => { const rStart = new Date(r.startTime); const rEnd = new Date(r.endTime); @@ -287,12 +305,13 @@ diff --git a/apps/web/src/lib/i18n/locales/en.json b/apps/web/src/lib/i18n/locales/en.json index 20dd97c..c34fadd 100644 --- a/apps/web/src/lib/i18n/locales/en.json +++ b/apps/web/src/lib/i18n/locales/en.json @@ -77,6 +77,8 @@ "errorCreating": "Failed to create booking", "errorDeleting": "Failed to delete booking", "errorTime": "End time must be after start time", + "errorPast": "Cannot book in the past", + "past": "This time has already passed", "spaceType": { "DESK": "Desk", "MEETING_ROOM": "Meeting Room", diff --git a/apps/web/src/lib/i18n/locales/es-ES.json b/apps/web/src/lib/i18n/locales/es-ES.json index cc08f65..0c7947f 100644 --- a/apps/web/src/lib/i18n/locales/es-ES.json +++ b/apps/web/src/lib/i18n/locales/es-ES.json @@ -77,6 +77,8 @@ "errorCreating": "Error al crear la reserva", "errorDeleting": "Error al eliminar la reserva", "errorTime": "La hora de finalización debe ser posterior a la de inicio", + "errorPast": "No se puede reservar en el pasado", + "past": "Esta hora ya ha pasado", "spaceType": { "DESK": "Puesto", "MEETING_ROOM": "Sala de reuniones", diff --git a/apps/web/src/routes/dashboard/building/[id]/+page.svelte b/apps/web/src/routes/dashboard/building/[id]/+page.svelte index dededd5..b22ecef 100644 --- a/apps/web/src/routes/dashboard/building/[id]/+page.svelte +++ b/apps/web/src/routes/dashboard/building/[id]/+page.svelte @@ -108,6 +108,7 @@ id="date-select" type="date" bind:value={selectedDateString} + min={new Date().toISOString().split('T')[0]} class="absolute inset-0 opacity-0 cursor-pointer z-10 w-full h-full" on:click={(e) => e.currentTarget.showPicker()} /> diff --git a/apps/web/src/routes/dashboard/floor/[id]/+page.svelte b/apps/web/src/routes/dashboard/floor/[id]/+page.svelte index e559c20..82fc302 100644 --- a/apps/web/src/routes/dashboard/floor/[id]/+page.svelte +++ b/apps/web/src/routes/dashboard/floor/[id]/+page.svelte @@ -69,6 +69,7 @@ id="date-select" type="date" bind:value={selectedDateString} + min={new Date().toLocaleDateString('en-CA')} class="absolute inset-0 opacity-0 cursor-pointer z-10 w-full h-full" on:click={(e) => e.currentTarget.showPicker()} /> diff --git a/apps/web/src/routes/dashboard/my-bookings/+page.svelte b/apps/web/src/routes/dashboard/my-bookings/+page.svelte index 163b83f..ce7ef2b 100644 --- a/apps/web/src/routes/dashboard/my-bookings/+page.svelte +++ b/apps/web/src/routes/dashboard/my-bookings/+page.svelte @@ -74,6 +74,21 @@ const start = new Date(`${editStartTimeDate}T${editStartTimeTime}`); const end = new Date(`${editEndTimeDate}T${editEndTimeTime}`); + const now = new Date(); + if (editingReservation.space.type === 'DESK') { + const today = new Date(); + today.setHours(0, 0, 0, 0); + if (start < today) { + alert($t('booking.errorPast')); + saving = false; + return; + } + } else if (start < now) { + alert($t('booking.errorPast')); + saving = false; + return; + } + await apiFetch(`/reservations/${editingReservation.id}`, { method: "PATCH", body: JSON.stringify({ @@ -268,6 +283,7 @@ e.currentTarget.showPicker()} />