Files
karczma-aplikacja-stoliki/public/waiter/app.js
2026-06-10 20:31:48 +02:00

250 lines
6.5 KiB
JavaScript

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, '&lt;')
.replace(/>/g, '&gt;');
}
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
? `<div class="feed-operator">Kelner stolika: <strong>${escapeHtml(operator)}</strong></div>`
: '';
const pending = Number(row.status_kds) === 0;
const cardClass = [
'feed-card',
isWaiter ? 'waiter' : 'bill',
pending ? '' : 'done',
].filter(Boolean).join(' ');
return `
<article class="${cardClass}" data-id="${row.id}">
<div class="feed-head">
<div class="feed-title">${escapeHtml(typeLabel)}</div>
<div class="feed-time">${escapeHtml(formatTime(row.created_at))}</div>
</div>
<div class="feed-table">Stolik ${escapeHtml(row.table_id || '?')}</div>
${operatorHtml}
<div class="feed-msg">${escapeHtml(row.message_text || '')}</div>
<div class="feed-badges">
<span class="badge ${pending ? 'badge-pending' : 'badge-done'}">
${pending ? 'Aktywne w KDS' : 'Obsłużone w KDS'}
</span>
</div>
</article>
`;
}).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();
})();