Naprawa lokalizacji - odczytu
This commit is contained in:
249
public/waiter/app.js
Normal file
249
public/waiter/app.js
Normal file
@@ -0,0 +1,249 @@
|
||||
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();
|
||||
})();
|
||||
Reference in New Issue
Block a user