250 lines
6.5 KiB
JavaScript
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, '<')
|
|
.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
|
|
? `<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();
|
|
})();
|