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
parent
9ac64de030
commit
0b6a043f19
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Reference in New Issue