@@ -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;
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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++;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user