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 ? `
Kelner stolika: ${escapeHtml(operator)}
` : ''; const pending = Number(row.status_kds) === 0; const cardClass = [ 'feed-card', isWaiter ? 'waiter' : 'bill', pending ? '' : 'done', ].filter(Boolean).join(' '); return `
${escapeHtml(typeLabel)}
${escapeHtml(formatTime(row.created_at))}
Stolik ${escapeHtml(row.table_id || '?')}
${operatorHtml}
${escapeHtml(row.message_text || '')}
${pending ? 'Aktywne w KDS' : 'Obsłużone w KDS'}
`; }).join(''); } function updateStats(summary, total) { statPending.textContent = String(summary?.pending ?? 0); statWaiter.textContent = String(summary?.waiter_calls ?? 0); statBill.textContent = String(summary?.bill_requests ?? 0); statTotal.textContent = String(total ?? 0); } function setSyncState(ok, message) { syncDot.classList.toggle('ok', ok); syncDot.classList.toggle('err', !ok); syncLabel.textContent = message; } async function pollFeed() { try { const response = await fetch(API_URL, { cache: 'no-store' }); const result = await response.json(); if (result.status !== 'success') { setSyncState(false, 'Błąd API'); return; } const payload = JSON.stringify(result.data || []); const rows = result.data || []; handleNewRows(rows); if (payload !== lastPayload) { renderFeed(rows); lastPayload = payload; } updateStats(result.summary, result.count); const now = new Date(); setSyncState(true, `Sync ${now.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit', second: '2-digit' })}`); } catch { setSyncState(false, 'Brak połączenia'); } } function startPolling() { pollFeed(); if (pollTimer) clearInterval(pollTimer); pollTimer = setInterval(pollFeed, POLL_MS); } enableNotifyBtn?.addEventListener('click', () => { requestNotifications(); }); document.addEventListener('visibilitychange', () => { if (!document.hidden) { pollFeed(); } }); (async function init() { updateNotifyBanner(); if ('Notification' in window && Notification.permission === 'granted') { await registerServiceWorker(); } else if ('Notification' in window && Notification.permission === 'default') { notifyBanner.classList.remove('hidden'); } startPolling(); })();