Files
Reservair/assets/js/elements/RsvTimeline.js
2026-06-14 10:01:24 +02:00

154 lines
5.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);