From 3225ff1e10a5c509f77774508dc2582c2f681ad5 Mon Sep 17 00:00:00 2001 From: Martin Slachta Date: Sun, 14 Jun 2026 10:01:24 +0200 Subject: [PATCH] (#8) - Minimum lead time --- assets/css/components/RsvTimeSlotsStyles.css | 8 +++++++- assets/js/elements/RsvTimeline.js | 9 +++++---- includes/Models/RsvTimetableAvailability.php | 13 ++++++++----- .../Services/RsvTimetableReservationService.php | 8 ++++++++ includes/Services/RsvTimetableService.php | 3 ++- 5 files changed, 30 insertions(+), 11 deletions(-) diff --git a/assets/css/components/RsvTimeSlotsStyles.css b/assets/css/components/RsvTimeSlotsStyles.css index 8af4771..04b1242 100644 --- a/assets/css/components/RsvTimeSlotsStyles.css +++ b/assets/css/components/RsvTimeSlotsStyles.css @@ -87,7 +87,7 @@ label.rsv-slots-slot-time>input:checked + .content>.capacity { font-weight: 600; } -.rsv-slots-slot:hover:not(.rsv-slots-slot-full):not(.rsv-slots-slot-selected) { +.rsv-slots-slot:hover:not(.rsv-slots-slot-full):not(.rsv-slots-slot-too-soon):not(.rsv-slots-slot-selected) { border-color: #2563eb; background: #f5f8ff; } @@ -115,6 +115,12 @@ label.rsv-slots-slot-time>input:checked + .content>.capacity { cursor: not-allowed; } +/* Within minimum lead time — available but not yet bookable */ +.rsv-slots-slot-too-soon { + opacity: 0.45; + cursor: not-allowed; +} + /* Selected */ .rsv-slots-slot-selected { background: #2563eb; diff --git a/assets/js/elements/RsvTimeline.js b/assets/js/elements/RsvTimeline.js index 8b7189e..0a47774 100644 --- a/assets/js/elements/RsvTimeline.js +++ b/assets/js/elements/RsvTimeline.js @@ -47,7 +47,7 @@ class RsvTimeline extends HTMLElement { _on_click(event) { const slot = event.target.closest('.rsv-slots-slot'); - if (slot && !slot.classList.contains('rsv-slots-slot-full')) { + if (slot && !slot.classList.contains('rsv-slots-slot-full') && !slot.classList.contains('rsv-slots-slot-too-soon')) { slot.classList.toggle('rsv-slots-slot-selected'); slot.dispatchEvent(new Event('input', { bubbles: true })); } @@ -81,7 +81,7 @@ class RsvTimeline extends HTMLElement { const blocks = []; - for (const { from_minutes, to_minutes, block_size_in_minutes, occupancy: block_occ } of occupancy) { + for (const { from_minutes, to_minutes, block_size_in_minutes, occupancy: block_occ, lead_time_minutes } of occupancy) { if (from_minutes === to_minutes || block_occ.length === 0) { continue; } @@ -89,7 +89,7 @@ class RsvTimeline extends HTMLElement { const from_block = parseInt(from_minutes) / block_size_in_minutes; const time_slots = block_occ.map((occ, i) => - this._block(this.date, occ, block_size_in_minutes, from_block + i) + this._block(this.date, occ, block_size_in_minutes, from_block + i, lead_time_minutes?.[i] ?? 0) ); const time_slot_group = document.createElement('div'); @@ -105,7 +105,7 @@ class RsvTimeline extends HTMLElement { } } - _block(date, left, block_size, idx) { + _block(date, left, block_size, idx, min_lead_time_minutes = 0) { const from = new Date(date); from.setHours(0, idx * block_size, 0, 0); @@ -117,6 +117,7 @@ class RsvTimeline extends HTMLElement { cell.dataset.start_utc = from.toISOString(); cell.dataset.end_utc = to.toISOString(); if (left <= 0) cell.classList.add('rsv-slots-slot-full'); + else if (from < new Date(Date.now() + min_lead_time_minutes * 60_000)) cell.classList.add('rsv-slots-slot-too-soon'); const time_el = document.createElement('span'); time_el.classList.add('rsv-slots-slot-time'); diff --git a/includes/Models/RsvTimetableAvailability.php b/includes/Models/RsvTimetableAvailability.php index 9f9e644..a8bc0fd 100644 --- a/includes/Models/RsvTimetableAvailability.php +++ b/includes/Models/RsvTimetableAvailability.php @@ -5,17 +5,20 @@ */ class RsvTimetableAvailability { /** - * @param array $occupancy Number of available seats for each time block + * @param array $occupancy Number of available seats for each time block + * @param array $lead_time_minutes Minimum lead time in minutes required for each block */ public function __construct( public int $from_minutes, public int $to_minutes, public int $block_size_in_minutes, - public array $occupancy + public array $occupancy, + public array $lead_time_minutes = [] ) { } - public function push_block(int $capacity) { - $this->occupancy[] = $capacity; - $this->to_minutes += $this->block_size_in_minutes; + public function push_block(int $capacity, int $min_lead_time_minutes = 0) { + $this->occupancy[] = $capacity; + $this->lead_time_minutes[] = $min_lead_time_minutes; + $this->to_minutes += $this->block_size_in_minutes; } } diff --git a/includes/Services/RsvTimetableReservationService.php b/includes/Services/RsvTimetableReservationService.php index dfceb4d..98b9d73 100644 --- a/includes/Services/RsvTimetableReservationService.php +++ b/includes/Services/RsvTimetableReservationService.php @@ -39,6 +39,14 @@ class RsvTimetableReservationService { return false; } + $max_lead_time = max(array_map(fn($c) => (int) $c->min_lead_time_minutes, $overlapping_capacity)); + $earliest_allowed = new DateTime('now', new DateTimeZone('UTC')); + $earliest_allowed->modify("+{$max_lead_time} minutes"); + if ($start_utc < $earliest_allowed) { + Logger::error("Reservation rejected: minimum lead time of {$max_lead_time} minutes not met for timetable_id: $timetable_id"); + return false; + } + $start_min = $this->time_of_day_minutes($start_utc); $end_min = $this->time_of_day_minutes($end_utc); diff --git a/includes/Services/RsvTimetableService.php b/includes/Services/RsvTimetableService.php index d4d75b9..c85c5e3 100644 --- a/includes/Services/RsvTimetableService.php +++ b/includes/Services/RsvTimetableService.php @@ -98,7 +98,8 @@ class RsvTimetableService { $availabilities[] = new RsvTimetableAvailability($i * $block_length, ($i + 1) * $block_length, $block_length, []); } - $availabilities[$availability_idx]->push_block($total_capacity - count($reservation_stack)); + $max_lead_time = empty($capacity_stack) ? 0 : max(array_map(fn($x) => $x->min_lead_time_minutes, $capacity_stack)); + $availabilities[$availability_idx]->push_block($total_capacity - count($reservation_stack), $max_lead_time); } else if($total_capacity === 0 && count($availabilities) !== $availability_idx) { $availability_idx++; } -- 2.52.0