const API_URL = '../../api/waiter_feed.php'; const POLL_MS = 15000; const feedList = document.getElementById('feedList'); const emptyState = document.getElementById('emptyState'); const syncDot = document.getElementById('syncDot'); const syncLabel = document.getElementById('syncLabel'); const notifyBanner = document.getElementById('notifyBanner'); const enableNotifyBtn = document.getElementById('enableNotifyBtn'); const statPending = document.getElementById('statPending'); const statWaiter = document.getElementById('statWaiter'); const statBill = document.getElementById('statBill'); const statTotal = document.getElementById('statTotal'); let knownIds = new Set(); let feedInitialized = false; let pollTimer = null; let lastPayload = ''; let swRegistration = null; function escapeHtml(value) { return String(value ?? '') .replace(/&/g, '&') .replace(//g, '>'); } function formatTime(isoLike) { if (!isoLike) return ''; const dt = new Date(String(isoLike).replace(' ', 'T')); if (Number.isNaN(dt.getTime())) return ''; return dt.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' }); } function formatOperator(row) { const imie = (row.otwierajacy_imie || '').trim(); const nazwisko = (row.otwierajacy_nazwisko || '').trim(); return `${imie} ${nazwisko}`.trim(); } function updateNotifyBanner() { if (!('Notification' in window)) { notifyBanner.classList.add('hidden'); return; } if (Notification.permission === 'default') { notifyBanner.classList.remove('hidden'); return; } notifyBanner.classList.add('hidden'); } async function registerServiceWorker() { if (!('serviceWorker' in navigator)) { return null; } try { swRegistration = await navigator.serviceWorker.register('sw.js'); await navigator.serviceWorker.ready; return swRegistration; } catch (err) { console.warn('[waiter] SW registration failed', err); return null; } } async function requestNotifications() { if (!('Notification' in window)) { alert('Ta przeglądarka nie obsługuje powiadomień.'); return; } const permission = await Notification.requestPermission(); updateNotifyBanner(); if (permission === 'granted') { await registerServiceWorker(); } } function showNotification(row) { const isWaiter = row.message_type === 'waiter_call'; const title = isWaiter ? 'Wezwanie kelnera' : 'Prośba o rachunek'; const firstLine = (row.message_text || '').split('\n')[0].trim(); const body = `Stolik ${row.table_id || '?'}` + (firstLine ? `\n${firstLine}` : ''); const payload = { type: 'notify', title, body, tag: `waiter-${row.id}`, }; if (swRegistration && swRegistration.active) { swRegistration.active.postMessage(payload); return; } if (navigator.serviceWorker && navigator.serviceWorker.controller) { navigator.serviceWorker.controller.postMessage(payload); return; } if (Notification.permission === 'granted') { new Notification(title, { body, tag: payload.tag, renotify: true, }); } } function handleNewRows(rows) { const fresh = []; for (const row of rows) { const id = Number(row.id); if (!feedInitialized) { knownIds.add(id); continue; } if (!knownIds.has(id)) { knownIds.add(id); fresh.push(row); } } feedInitialized = true; for (const row of fresh) { showNotification(row); } } function renderFeed(rows) { if (!rows.length) { feedList.innerHTML = ''; emptyState.classList.remove('hidden'); return; } emptyState.classList.add('hidden'); feedList.innerHTML = rows.map((row) => { const isWaiter = row.message_type === 'waiter_call'; const typeLabel = isWaiter ? 'Wezwanie kelnera' : 'Prośba o rachunek'; const operator = formatOperator(row); const operatorHtml = operator ? `