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);