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');
|
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
|
// Rules
|
||||||
if (space.type === SpaceType.DESK) {
|
if (space.type === SpaceType.DESK) {
|
||||||
// Desks: Full day (setting to midnight of start and end of that day)
|
// 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');
|
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
|
// Rules
|
||||||
if (space.type === SpaceType.DESK) {
|
if (space.type === SpaceType.DESK) {
|
||||||
start.setHours(0, 0, 0, 0);
|
start.setHours(0, 0, 0, 0);
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,14 @@
|
||||||
start.setHours(0, 0, 0, 0);
|
start.setHours(0, 0, 0, 0);
|
||||||
end = new Date(bookingDate);
|
end = new Date(bookingDate);
|
||||||
end.setHours(23, 59, 59, 999);
|
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 {
|
} else {
|
||||||
const [sh, sm] = startTime.split(':').map(Number);
|
const [sh, sm] = startTime.split(':').map(Number);
|
||||||
const [eh, em] = endTime.split(':').map(Number);
|
const [eh, em] = endTime.split(':').map(Number);
|
||||||
|
|
@ -82,6 +90,12 @@
|
||||||
end = new Date(bookingDate);
|
end = new Date(bookingDate);
|
||||||
end.setHours(eh, em, 0, 0);
|
end.setHours(eh, em, 0, 0);
|
||||||
|
|
||||||
|
if (start < new Date()) {
|
||||||
|
error = $t('booking.errorPast');
|
||||||
|
loading = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (end <= start) {
|
if (end <= start) {
|
||||||
error = $t('booking.errorTime');
|
error = $t('booking.errorTime');
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|
@ -143,6 +157,10 @@
|
||||||
const slotStart = new Date(bookingDate);
|
const slotStart = new Date(bookingDate);
|
||||||
slotStart.setHours(h, m, 0, 0);
|
slotStart.setHours(h, m, 0, 0);
|
||||||
|
|
||||||
|
if (slotStart < new Date()) {
|
||||||
|
return { isPast: true };
|
||||||
|
}
|
||||||
|
|
||||||
return dayReservations.find(r => {
|
return dayReservations.find(r => {
|
||||||
const rStart = new Date(r.startTime);
|
const rStart = new Date(r.startTime);
|
||||||
const rEnd = new Date(r.endTime);
|
const rEnd = new Date(r.endTime);
|
||||||
|
|
@ -287,12 +305,13 @@
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => selectSlot(time)}
|
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
|
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' :
|
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'}"
|
'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}
|
{time}
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,8 @@
|
||||||
"errorCreating": "Failed to create booking",
|
"errorCreating": "Failed to create booking",
|
||||||
"errorDeleting": "Failed to delete booking",
|
"errorDeleting": "Failed to delete booking",
|
||||||
"errorTime": "End time must be after start time",
|
"errorTime": "End time must be after start time",
|
||||||
|
"errorPast": "Cannot book in the past",
|
||||||
|
"past": "This time has already passed",
|
||||||
"spaceType": {
|
"spaceType": {
|
||||||
"DESK": "Desk",
|
"DESK": "Desk",
|
||||||
"MEETING_ROOM": "Meeting Room",
|
"MEETING_ROOM": "Meeting Room",
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,8 @@
|
||||||
"errorCreating": "Error al crear la reserva",
|
"errorCreating": "Error al crear la reserva",
|
||||||
"errorDeleting": "Error al eliminar la reserva",
|
"errorDeleting": "Error al eliminar la reserva",
|
||||||
"errorTime": "La hora de finalización debe ser posterior a la de inicio",
|
"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": {
|
"spaceType": {
|
||||||
"DESK": "Puesto",
|
"DESK": "Puesto",
|
||||||
"MEETING_ROOM": "Sala de reuniones",
|
"MEETING_ROOM": "Sala de reuniones",
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@
|
||||||
id="date-select"
|
id="date-select"
|
||||||
type="date"
|
type="date"
|
||||||
bind:value={selectedDateString}
|
bind:value={selectedDateString}
|
||||||
|
min={new Date().toISOString().split('T')[0]}
|
||||||
class="absolute inset-0 opacity-0 cursor-pointer z-10 w-full h-full"
|
class="absolute inset-0 opacity-0 cursor-pointer z-10 w-full h-full"
|
||||||
on:click={(e) => e.currentTarget.showPicker()}
|
on:click={(e) => e.currentTarget.showPicker()}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@
|
||||||
id="date-select"
|
id="date-select"
|
||||||
type="date"
|
type="date"
|
||||||
bind:value={selectedDateString}
|
bind:value={selectedDateString}
|
||||||
|
min={new Date().toLocaleDateString('en-CA')}
|
||||||
class="absolute inset-0 opacity-0 cursor-pointer z-10 w-full h-full"
|
class="absolute inset-0 opacity-0 cursor-pointer z-10 w-full h-full"
|
||||||
on:click={(e) => e.currentTarget.showPicker()}
|
on:click={(e) => e.currentTarget.showPicker()}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,21 @@
|
||||||
const start = new Date(`${editStartTimeDate}T${editStartTimeTime}`);
|
const start = new Date(`${editStartTimeDate}T${editStartTimeTime}`);
|
||||||
const end = new Date(`${editEndTimeDate}T${editEndTimeTime}`);
|
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}`, {
|
await apiFetch(`/reservations/${editingReservation.id}`, {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
@ -268,6 +283,7 @@
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
bind:value={editStartTimeDate}
|
bind:value={editStartTimeDate}
|
||||||
|
min={new Date().toISOString().split('T')[0]}
|
||||||
class="absolute inset-0 opacity-0 cursor-pointer w-full z-10"
|
class="absolute inset-0 opacity-0 cursor-pointer w-full z-10"
|
||||||
on:click={(e) => e.currentTarget.showPicker()}
|
on:click={(e) => e.currentTarget.showPicker()}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue