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.
main
Pau Costa Ferrer 2026-03-28 18:19:57 +01:00
parent 9ac64de030
commit 0b6a043f19
7 changed files with 66 additions and 3 deletions

View File

@ -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);

View File

@ -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 @@
<button
type="button"
on:click={() => selectSlot(time)}
disabled={!!reservation && reservation.userId !== $user?.id && reservation.user?.id !== $user?.id && $user?.role !== 'ADMIN'}
disabled={(!!reservation && reservation.userId !== $user?.id && reservation.user?.id !== $user?.id && $user?.role !== 'ADMIN') || (reservation && reservation.isPast)}
class="w-full py-2 text-[10px] font-bold rounded-md transition-all border
{reservation ? 'bg-red-100 text-red-700 border-red-200' :
{reservation?.isPast ? 'bg-gray-100 text-gray-400 border-gray-200 cursor-not-allowed' :
reservation ? 'bg-red-100 text-red-700 border-red-200' :
startTime === time ? 'bg-indigo-600 text-white border-indigo-700 ring-2 ring-indigo-200' :
'bg-white text-gray-600 border-gray-200 hover:border-indigo-400'}"
title={reservation ? $t('booking.bookedBy', { values: { user: reservation.user.email } }) : $t('booking.availableAt', { values: { time: time } })}
title={reservation?.isPast ? $t('booking.past') : reservation ? $t('booking.bookedBy', { values: { user: reservation.user.email } }) : $t('booking.availableAt', { values: { time: time } })}
>
{time}
</button>

View File

@ -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",

View File

@ -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",

View File

@ -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()}
/>

View File

@ -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()}
/>

View File

@ -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 @@
<input
type="date"
bind:value={editStartTimeDate}
min={new Date().toISOString().split('T')[0]}
class="absolute inset-0 opacity-0 cursor-pointer w-full z-10"
on:click={(e) => e.currentTarget.showPicker()}
/>