(#8) - Minimum lead time #10

Merged
Martas merged 1 commits from feat/claude/#8-minimum-lead-time into main 2026-06-14 08:03:34 +00:00
5 changed files with 30 additions and 11 deletions
Showing only changes of commit 3225ff1e10 - Show all commits
+7 -1
View File
@@ -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;
+5 -4
View File
@@ -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');
+8 -5
View File
@@ -5,17 +5,20 @@
*/
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 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;
}
}
@@ -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);
+2 -1
View File
@@ -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++;
}