154 lines
5.1 KiB
JavaScript
154 lines
5.1 KiB
JavaScript
import { RsvTimetableService } from '../services/RsvTimetableService.js';
|
||
|
||
class RsvTimeline extends HTMLElement {
|
||
static get observedAttributes() {
|
||
return ['timetable-id', 'date'];
|
||
}
|
||
|
||
// ---- Attribute accessors ------------------------------------------------
|
||
|
||
get timetableId() { return parseInt(this.getAttribute('timetable-id')); }
|
||
|
||
get date() {
|
||
const attr = this.getAttribute('date');
|
||
// Parse as local midnight so setHours() in block rendering stays in local time.
|
||
return attr ? new Date(attr + 'T12:00:00') : new Date();
|
||
}
|
||
|
||
set date(value) {
|
||
const d = new Date(value);
|
||
const str = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||
this.setAttribute('date', str);
|
||
}
|
||
|
||
// ---- Lifecycle ----------------------------------------------------------
|
||
|
||
connectedCallback() {
|
||
this._version = 0;
|
||
this.classList.add('rsv-slots-list');
|
||
this.addEventListener('click', this._on_click.bind(this));
|
||
this._render();
|
||
}
|
||
|
||
attributeChangedCallback(_attr, oldVal, newVal) {
|
||
if (oldVal === newVal || !this.isConnected) return;
|
||
this._render();
|
||
}
|
||
|
||
// ---- Public API ---------------------------------------------------------
|
||
|
||
// Re-fetch availability for the current date, e.g. after a booking occupied
|
||
// some slots. The date is unchanged, so attributeChangedCallback won't fire.
|
||
refresh() {
|
||
this._render();
|
||
}
|
||
|
||
// ---- Private ------------------------------------------------------------
|
||
|
||
_on_click(event) {
|
||
const slot = event.target.closest('.rsv-slots-slot');
|
||
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 }));
|
||
}
|
||
}
|
||
|
||
async _render() {
|
||
// Version guard: discard renders that were superseded by a newer call.
|
||
const v = ++this._version;
|
||
const s = ReservairStrings.timeline;
|
||
|
||
if (this.timetableId === null) {
|
||
this.replaceChildren(this._notice(s.not_reservable));
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const occupancy = await RsvTimetableService.get_availability_for_date(this.timetableId, this.date);
|
||
if (v !== this._version) return;
|
||
|
||
|
||
const header = document.createElement('div');
|
||
header.classList.add('rsv-slots-label');
|
||
header.textContent = this.date.toLocaleDateString(navigator.language, {
|
||
weekday: 'long', day: 'numeric', month: 'long',
|
||
}).replace(',', '');
|
||
|
||
if(occupancy.length === 0) {
|
||
this.replaceChildren(header, this._notice(s.no_blocks));
|
||
return;
|
||
}
|
||
|
||
const blocks = [];
|
||
|
||
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;
|
||
}
|
||
|
||
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, lead_time_minutes?.[i] ?? 0)
|
||
);
|
||
|
||
const time_slot_group = document.createElement('div');
|
||
time_slot_group.classList.add('rsv-slots-group');
|
||
time_slot_group.replaceChildren(...time_slots);
|
||
blocks.push(time_slot_group);
|
||
}
|
||
|
||
this.replaceChildren(header, ...blocks);
|
||
} catch (_e) {
|
||
if (v !== this._version) return;
|
||
this.replaceChildren(this._notice(s.no_blocks));
|
||
}
|
||
}
|
||
|
||
_block(date, left, block_size, idx, min_lead_time_minutes = 0) {
|
||
const from = new Date(date);
|
||
from.setHours(0, idx * block_size, 0, 0);
|
||
|
||
const to = new Date(from);
|
||
to.setMinutes(to.getMinutes() + block_size);
|
||
|
||
const cell = document.createElement('div');
|
||
cell.classList.add('rsv-slots-slot', 'rsv-slots-slot-available');
|
||
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');
|
||
time_el.textContent = `${this._fmt(from)} – ${this._fmt(to)}`;
|
||
|
||
const badge = document.createElement('span');
|
||
badge.classList.add('rsv-slots-slot-badge');
|
||
const remaining_seats = left;
|
||
|
||
if (remaining_seats > 0) badge.classList.add('rsv-slots-slot-badge-available');
|
||
|
||
if (remaining_seats === 1) badge.textContent = `${remaining_seats} místo`;
|
||
else if (remaining_seats >= 2 && remaining_seats <= 4) badge.textContent = `${remaining_seats} místa`;
|
||
else badge.textContent = `${remaining_seats} míst`;
|
||
|
||
|
||
cell.append(time_el, badge);
|
||
return cell;
|
||
}
|
||
|
||
_notice(text) {
|
||
const p = document.createElement('p');
|
||
p.classList.add('rsv-slots-notice');
|
||
p.textContent = text;
|
||
return p;
|
||
}
|
||
|
||
_fmt(dt) {
|
||
return dt.getHours() + ':' + String(dt.getMinutes()).padStart(2, '0');
|
||
}
|
||
}
|
||
|
||
customElements.define('rsv-timeline', RsvTimeline);
|