(#8) - Minimum lead time

This commit was merged in pull request #10.
This commit is contained in:
Martin Slachta
2026-06-14 10:01:24 +02:00
parent 7d7f748f7a
commit 3225ff1e10
5 changed files with 30 additions and 11 deletions
+7 -1
View File
@@ -87,7 +87,7 @@ label.rsv-slots-slot-time>input:checked + .content>.capacity {
font-weight: 600; 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; border-color: #2563eb;
background: #f5f8ff; background: #f5f8ff;
} }
@@ -115,6 +115,12 @@ label.rsv-slots-slot-time>input:checked + .content>.capacity {
cursor: not-allowed; cursor: not-allowed;
} }
/* Within minimum lead time — available but not yet bookable */
.rsv-slots-slot-too-soon {
opacity: 0.45;
cursor: not-allowed;
}
/* Selected */ /* Selected */
.rsv-slots-slot-selected { .rsv-slots-slot-selected {
background: #2563eb; background: #2563eb;
+5 -4
View File
@@ -47,7 +47,7 @@ class RsvTimeline extends HTMLElement {
_on_click(event) { _on_click(event) {
const slot = event.target.closest('.rsv-slots-slot'); 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.classList.toggle('rsv-slots-slot-selected');
slot.dispatchEvent(new Event('input', { bubbles: true })); slot.dispatchEvent(new Event('input', { bubbles: true }));
} }
@@ -81,7 +81,7 @@ class RsvTimeline extends HTMLElement {
const blocks = []; 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) { if (from_minutes === to_minutes || block_occ.length === 0) {
continue; continue;
} }
@@ -89,7 +89,7 @@ class RsvTimeline extends HTMLElement {
const from_block = parseInt(from_minutes) / block_size_in_minutes; const from_block = parseInt(from_minutes) / block_size_in_minutes;
const time_slots = block_occ.map((occ, i) => 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'); 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); const from = new Date(date);
from.setHours(0, idx * block_size, 0, 0); from.setHours(0, idx * block_size, 0, 0);
@@ -117,6 +117,7 @@ class RsvTimeline extends HTMLElement {
cell.dataset.start_utc = from.toISOString(); cell.dataset.start_utc = from.toISOString();
cell.dataset.end_utc = to.toISOString(); cell.dataset.end_utc = to.toISOString();
if (left <= 0) cell.classList.add('rsv-slots-slot-full'); 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'); const time_el = document.createElement('span');
time_el.classList.add('rsv-slots-slot-time'); time_el.classList.add('rsv-slots-slot-time');
+8 -5
View File
@@ -5,17 +5,20 @@
*/ */
class RsvTimetableAvailability { class RsvTimetableAvailability {
/** /**
* @param array<int,int> $occupancy Number of available seats for each time block * @param array<int,int> $occupancy Number of available seats for each time block
* @param array<int,int> $lead_time_minutes Minimum lead time in minutes required for each block
*/ */
public function __construct( public function __construct(
public int $from_minutes, public int $from_minutes,
public int $to_minutes, public int $to_minutes,
public int $block_size_in_minutes, public int $block_size_in_minutes,
public array $occupancy public array $occupancy,
public array $lead_time_minutes = []
) { } ) { }
public function push_block(int $capacity) { public function push_block(int $capacity, int $min_lead_time_minutes = 0) {
$this->occupancy[] = $capacity; $this->occupancy[] = $capacity;
$this->to_minutes += $this->block_size_in_minutes; $this->lead_time_minutes[] = $min_lead_time_minutes;
$this->to_minutes += $this->block_size_in_minutes;
} }
} }
@@ -39,6 +39,14 @@ class RsvTimetableReservationService {
return false; 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); $start_min = $this->time_of_day_minutes($start_utc);
$end_min = $this->time_of_day_minutes($end_utc); $end_min = $this->time_of_day_minutes($end_utc);
+2 -1
View File
@@ -98,7 +98,8 @@ class RsvTimetableService {
$availabilities[] = new RsvTimetableAvailability($i * $block_length, ($i + 1) * $block_length, $block_length, []); $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) { } else if($total_capacity === 0 && count($availabilities) !== $availability_idx) {
$availability_idx++; $availability_idx++;
} }