window.kitchenAnimations = [ `
🔥
🍳
`, `
🐓
👨‍🍳
`, `
🐓
🍲
`, `
👨‍🍳
🍕
`, `
🐷
💦
🍴
` ]; window.selectedAnimationHtml = null; const params = new URLSearchParams(location.search); let hashParam = (params.get("h") || "").trim(); // Jeśli brak hasha w URL – zapytaj użytkownika (np. do testów) if (!hashParam) { const input = prompt("Podaj bezpieczny hash stolika (wymagane):"); const trimmed = (input || "").trim(); if (trimmed) { const newUrl = new URL(location.href); newUrl.searchParams.set("h", trimmed); location.replace(newUrl.toString()); } } let tableParam = ""; // Puste, zostanie uzupełnione przez backend // UI Elements const loadingScreen = document.getElementById("loadingScreen"); const loaderMsg = document.getElementById("loaderMsg"); const tableLabel = document.getElementById("tableLabel"); const prepStatus = document.getElementById("prepStatus"); const progressBar = document.getElementById("progressBar"); const statusMeta = document.getElementById("statusMeta"); const itemsList = document.getElementById("itemsList"); const emptyState = document.getElementById("emptyState"); const metaFooter = document.getElementById("metaFooter"); const statusIcon = document.getElementById("statusIcon"); const storageKey = `stolik2_state_${(tableParam || "unknown").toLowerCase()}`; const historyKey = "stolik2_global_history"; const historySection = document.getElementById("historySection"); const historyList = document.getElementById("historyList"); const SIX_MONTHS_MS = 180 * 24 * 60 * 60 * 1000; const HOT_WINDOW_MS = 5 * 60 * 60 * 1000; // Dynamic Loader Messages const LOADER_MIN_MS = 10_000; const loadStartTime = Date.now(); const msgs = ["Rozgrzewamy piece...", "Szef kuchni sprawdza składniki...", "Łączenie z sercem restauracji...", "Prawie gotowe..."]; let msgIdx = 0; const msgInterval = setInterval(() => { msgIdx = (msgIdx + 1) % msgs.length; loaderMsg.textContent = msgs[msgIdx]; }, 4000); // Initial State if (tableParam) { tableLabel.textContent = `Stolik ${tableParam}`; } function hideLoader() { const elapsed = Date.now() - loadStartTime; const remaining = Math.max(0, LOADER_MIN_MS - elapsed); setTimeout(() => { loadingScreen.classList.add("hidden"); clearInterval(msgInterval); }, remaining); } function updateUI(bills) { // Hide loader after minimum display time hideLoader(); const allArticles = bills.flatMap(b => Array.isArray(b?.Articles) ? b.Articles : []); const items = mergeWithPersistedItems(allArticles); renderGlobalHistory(); if (items.length === 0) { showEmptyState(); return; } renderItems(items); updateStatus(bills, items); } function detectTableCandidates(bill) { const remark = String(bill?.Remark || ""); const description = String(bill?.Description || "").trim(); // 1) klasyczny format: "STOLIK 9", "STOLIK 11A" const stolikMatch = remark.match(/STOLIK\s*([0-9A-Z]+)/i); const fromRemark = stolikMatch ? stolikMatch[1].toLowerCase() : ""; // 2) czasem numer bywa w samym Description const fromDescription = description.toLowerCase(); return { fromRemark, fromDescription, remark: remark.toLowerCase() }; } function showEmptyState() { emptyState.classList.remove("hidden"); itemsList.innerHTML = ""; prepStatus.textContent = "Brak aktywnych zamówień"; statusIcon.textContent = "🍃"; progressBar.style.width = "0%"; statusMeta.textContent = "Zapraszamy do złożenia zamówienia u kelnera."; // Historia może istnieć nawet gdy brak bieżących pozycji renderGlobalHistory(); } function normalizeArticleName(rawName) { const name = String(rawName || "Pozycja"); // Usuwa gramatury typu: "300G", "250 G", "500/200/150G". const withoutWeight = name.replace( /\b\d+(?:[.,]\d+)?(?:\s*\/\s*\d+(?:[.,]\d+)?)*\s*[gG]\b/g, "" ); return withoutWeight .replace(/\s{2,}/g, " ") .replace(/\s+([,.;:!?])/g, "$1") .trim() || "Pozycja"; } function loadPersistedItems() { try { const raw = localStorage.getItem(storageKey); if (!raw) return []; const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed : []; } catch { return []; } } function savePersistedItems(items) { try { localStorage.setItem(storageKey, JSON.stringify(items)); } catch { // brak miejsca/tryb prywatny } } function loadGlobalHistory() { try { const raw = localStorage.getItem(historyKey); if (!raw) return []; const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed : []; } catch { return []; } } function saveGlobalHistory(entries) { const now = Date.now(); const cleaned = entries .filter((e) => e && e.name && Number.isFinite(e.archivedAt)) .filter((e) => (now - e.archivedAt) <= SIX_MONTHS_MS); try { localStorage.setItem(historyKey, JSON.stringify(cleaned)); } catch { // ignore storage errors } } function addItemsToGlobalHistory(items, sourceTable) { if (!items.length) return; const now = Date.now(); const existing = loadGlobalHistory(); // zapobiegamy dokładnym duplikatom (ta sama nazwa + qty + stół blisko czasu) const dedupWindowMs = 2 * 60 * 1000; const toAdd = items.filter((item) => { return !existing.some((h) => h.name === item.name && Number(h.qty) === Number(item.qty) && String(h.sourceTable || "") === String(sourceTable || "") && Math.abs((h.archivedAt || 0) - now) <= dedupWindowMs ); }).map((item) => ({ name: item.name, qty: item.qty, sourceTable: sourceTable || "?", archivedAt: now })); if (!toAdd.length) return; saveGlobalHistory([...existing, ...toAdd]); } function renderGlobalHistory() { const now = Date.now(); const history = loadGlobalHistory() .filter((e) => (now - (e.archivedAt || 0)) <= SIX_MONTHS_MS) .sort((a, b) => (b.archivedAt || 0) - (a.archivedAt || 0)); saveGlobalHistory(history); if (!history.length) { historySection.classList.add("hidden"); historyList.innerHTML = ""; return; } historySection.classList.remove("hidden"); historyList.innerHTML = ""; history.forEach((entry) => { const dt = new Date(entry.archivedAt || Date.now()); const div = document.createElement("div"); div.className = "item-card archived ready"; div.innerHTML = `
${entry.name} Stolik ${entry.sourceTable || "?"} • ${dt.toLocaleDateString("pl-PL")} ${dt.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'})}
x${entry.qty}
`; historyList.appendChild(div); }); } function mergeWithPersistedItems(articles) { const current = new Map(); articles.forEach(a => { const name = normalizeArticleName(a.Name); const todo = parseFloat(String(a.QuantityToDo || a.QuantitySet || "0").replace(",",".")); const done = parseFloat(String(a.QuantityDone || "0").replace(",",".")); if (!current.has(name)) { current.set(name, { name, qty: 0, done: 0, present: true, completedByDisappear: false }); } const curr = current.get(name); curr.qty += Number.isFinite(todo) ? todo : 0; curr.done += Number.isFinite(done) ? done : 0; }); const persisted = loadPersistedItems(); const persistedMap = new Map(persisted.map(i => [i.name, i])); const merged = []; // Aktualnie obecne pozycje z WS current.forEach((item) => { merged.push({ ...item, present: true, completedByDisappear: false, updatedAt: Date.now() }); }); // Pozycje, które były wcześniej, ale zniknęły z API -> przesyłamy od razu do historii globalnej persistedMap.forEach((oldItem, name) => { if (current.has(name)) return; const qty = Number.isFinite(oldItem?.qty) ? oldItem.qty : 0; // Zniknęło z bieżącego rachunku (np. rachunek został zamknięty), od razu leci do osobnego bloku historii! addItemsToGlobalHistory([{ name, qty }], tableParam); }); // Aktywne na górze, gotowe (zniknięte) na dole merged.sort((a, b) => { if (a.present !== b.present) return a.present ? -1 : 1; return a.name.localeCompare(b.name, "pl"); }); savePersistedItems(merged); return merged; } function renderItems(items) { emptyState.classList.add("hidden"); itemsList.innerHTML = ""; items.forEach((item) => { const isReady = item.done >= item.qty && item.qty > 0; const div = document.createElement("div"); div.className = `item-card ${isReady ? 'ready' : ''} ${item.present ? '' : 'archived'}`; let meta = "🔥 W przygotowaniu"; if (isReady && item.completedByDisappear) { meta = "✅ Gotowe (zrealizowane)"; } else if (isReady) { meta = "✅ Gotowe"; } div.innerHTML = `
${item.name} ${meta}
x${item.qty}
`; itemsList.appendChild(div); }); renderGlobalHistory(); } function updateStatus(bills, items) { let total = 0; let done = 0; items.forEach(i => { total += Number.isFinite(i.qty) ? i.qty : 0; done += Number.isFinite(i.done) ? i.done : 0; }); const pct = total > 0 ? (done / total) * 100 : 0; progressBar.style.width = `${pct}%`; if (pct >= 100) { prepStatus.textContent = "Gotowe do podania!"; statusIcon.innerHTML = "😋"; statusMeta.textContent = "Wszystkie Twoje dania opuściły już kuchnię."; } else if (pct > 0) { prepStatus.textContent = "Częściowo gotowe"; statusIcon.innerHTML = "🍳"; statusMeta.textContent = "Pierwsze pyszności już na Ciebie czekają!"; } else { prepStatus.textContent = "W przygotowaniu"; if (!window.selectedAnimationHtml) { window.selectedAnimationHtml = window.kitchenAnimations[Math.floor(Math.random() * window.kitchenAnimations.length)]; } statusIcon.innerHTML = window.selectedAnimationHtml; statusMeta.textContent = "Twoje zamówienie jest właśnie tworzone przez naszych kucharzy."; } // Footer meta const newest = [...bills].sort((a,b) => new Date(b?.Date || 0) - new Date(a?.Date || 0))[0]; const time = newest?.Date ? new Date(newest.Date).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}) : "--:--"; metaFooter.textContent = `Zamówienie złożone o godzinie ${time} • Stolik ${tableParam}`; } // API Fetch Logic async function fetchOrders() { try { if (!hashParam) { updateUI([]); return; } const response = await fetch(`../api/kds.php?h=${encodeURIComponent(hashParam)}`); const result = await response.json(); if (result.status === 'success') { if (result.tableName && result.tableName !== '') { tableLabel.textContent = `Stolik ${result.tableName}`; tableParam = result.tableName; // Aktualizacja do właściwej nazwy na poczet innych zapytań } // API teraz samo filtruje i zwraca tylko to co nas interesuje (za pomocą mocnego wyrażenia regularnego) const matches = result.data; // Grupowanie składników w główne dania const groups = {}; matches.forEach(item => { const groupId = item.GrupaZestawuID || item.PozycjaID; if (!groups[groupId]) { groups[groupId] = { Name: item.NazwaZestawu || item.NazwaTowaru, QuantitySet: item.GrupaZestawuID ? 1 : parseFloat(item.Ilosc), Done: 0 }; } // StatusRealizacji >= 2 oznacza, że kucharz wcisnął "Gotowe" na swoim ekranie if (parseInt(item.StatusRealizacji, 10) >= 2) { groups[groupId].Done = groups[groupId].QuantitySet; } }); const transformedArticles = Object.values(groups).map(g => ({ Name: g.Name, QuantitySet: g.QuantitySet, QuantityDone: g.Done })); // Najnowszy czas dodania (do pokazania w stopce) const latestDate = matches.length > 0 ? matches.sort((a,b) => new Date(b.DataDodania) - new Date(a.DataDodania))[0].DataDodania : null; // Przekazanie do dotychczasowej logiki aktualizującej UI (w odpowiednim formacie) updateUI([{ Articles: transformedArticles, Date: latestDate }]); } else { loaderMsg.textContent = "Błąd API: " + result.message; } } catch (err) { loaderMsg.textContent = "Problem z połączeniem. Próbujemy ponownie..."; } } fetchOrders(); setInterval(fetchOrders, 3000); // --- CALL WAITER LOGIC --- let billState = { payment: '', doc: '', nip: '', company: null }; function showToast(msg) { const t = document.getElementById("toastMsg"); document.getElementById("toastText").textContent = msg; t.classList.remove("active"); void t.offsetWidth; // trigger reflow t.classList.add("active"); setTimeout(() => t.classList.remove("active"), 3500); } function sendApiSimulated(actionName, details) { console.log(`[SYMULACJA API] Akcja: ${actionName}`, details); // Przykładowe wysłanie docelowo: // if (window.socket && window.socket.readyState === WebSocket.OPEN) { // window.socket.send(JSON.stringify({ action: "sendUpstream", payload: { type: actionName, table: tableParam, ...details } })); // } } window.callWaiter = function(type) { if (type === 'order') { sendApiSimulated("CallWaiter_Order", { table: tableParam }); showToast("Kelner wkrótce do Ciebie podejdzie!"); } }; window.openBillDialog = async function() { billState = { payment: '', doc: '', nip: '', company: null, selectedBillId: null }; document.getElementById("billModal").classList.add("active"); document.body.style.overflow = 'hidden'; // Zablokuj scroll tła document.getElementById("billLoading").classList.remove("hidden"); document.getElementById("billListContainer").classList.add("hidden"); goToStep("stepBillList"); try { const res = await fetch(`../api/bills.php?h=${encodeURIComponent(hashParam)}`); const result = await res.json(); if (result.status === 'success' && result.data.length > 0) { const bills = result.data; if (bills.length === 1) { showBillReview(bills[0]); document.getElementById("btnBackToBills").style.display = 'none'; } else { renderBillList(bills); document.getElementById("btnBackToBills").style.display = 'block'; } } else { document.getElementById("billLoading").innerHTML = "Brak otwartych rachunków do opłacenia."; } } catch (err) { document.getElementById("billLoading").innerHTML = "Błąd pobierania rachunków."; } }; function renderBillList(bills) { document.getElementById("billLoading").classList.add("hidden"); document.getElementById("billListContainer").classList.remove("hidden"); const container = document.getElementById("billListItems"); container.innerHTML = ""; bills.forEach(b => { const div = document.createElement("div"); div.className = "option-card"; div.style.flexDirection = "row"; div.style.justifyContent = "space-between"; div.style.padding = "15px"; div.onclick = () => showBillReview(b); const numerFormat = b.numer ? `#${b.numer}` : "Rachunek"; div.innerHTML = `
${numerFormat}
${b.opis}
${b.suma.toFixed(2)} PLN
`; container.appendChild(div); }); } window.goBackToBillList = function() { goToStep("stepBillList"); }; window.showBillReview = function(bill) { billState.selectedBillId = bill.id; const content = document.getElementById("billReviewContent"); content.innerHTML = ""; bill.pozycje.forEach(p => { const div = document.createElement("div"); div.style.display = "flex"; div.style.justifyContent = "space-between"; div.style.marginBottom = "8px"; div.style.borderBottom = "1px solid rgba(255,255,255,0.05)"; div.style.paddingBottom = "8px"; div.innerHTML = `
${p.nazwa}
${p.ilosc} x ${p.cena.toFixed(2)} PLN
${p.wartosc.toFixed(2)} PLN
`; content.appendChild(div); }); document.getElementById("billTotalAmount").textContent = bill.suma.toFixed(2) + " PLN"; goToStep("stepBillReview"); }; window.closeBillDialog = function() { document.getElementById("billModal").classList.remove("active"); document.body.style.overflow = ''; // Odblokuj scroll tła }; window.goToStep = function(stepId) { document.querySelectorAll(".step").forEach(s => s.classList.remove("active")); document.getElementById(stepId).classList.add("active"); }; window.selectPayment = function(method) { billState.payment = method; goToStep("stepDocument"); }; window.selectDocument = function(docType) { billState.doc = docType; if (docType === 'paragon') { closeBillDialog(); sendApiSimulated("CallWaiter_Bill", { table: tableParam, billId: billState.selectedBillId, payment: billState.payment, doc: 'paragon' }); showToast("Kelner przyniesie paragon do opłacenia!"); } else { goToStep("stepNIP"); document.getElementById("nipInput").value = ''; setTimeout(() => document.getElementById("nipInput").focus(), 100); } }; window.fetchGUS = function() { const nip = document.getElementById("nipInput").value.replace(/[\s-]/g, ''); if (nip.length < 10) { alert("Wprowadź poprawny numer NIP."); return; } const btn = document.getElementById("btnGUS"); btn.textContent = "Szukam..."; btn.disabled = true; // Symulacja pobrania z GUS setTimeout(() => { btn.textContent = "Pobierz z GUS"; btn.disabled = false; billState.nip = nip; billState.company = { name: "Przykładowa Firma Sp. z o.o.", address: "ul. Gastronomiczna 12/4, 00-120 Warszawa", nip: nip }; document.getElementById("cmpName").value = billState.company.name; document.getElementById("cmpAddress").value = billState.company.address; document.getElementById("cmpNip").value = "NIP: " + billState.company.nip; // reset do readonly document.getElementById("cmpName").readOnly = true; document.getElementById("cmpAddress").readOnly = true; document.getElementById("btnEditCompany").textContent = "Popraw ręcznie"; goToStep("stepVerify"); }, 1200); }; window.editCompanyData = function() { const n = document.getElementById("cmpName"); const a = document.getElementById("cmpAddress"); const btn = document.getElementById("btnEditCompany"); if (n.readOnly) { n.readOnly = false; a.readOnly = false; n.focus(); btn.textContent = "Zakończ edycję"; } else { n.readOnly = true; a.readOnly = true; btn.textContent = "Popraw ręcznie"; } }; window.confirmInvoice = function() { billState.company.name = document.getElementById("cmpName").value; billState.company.address = document.getElementById("cmpAddress").value; closeBillDialog(); sendApiSimulated("CallWaiter_Bill", { table: tableParam, billId: billState.selectedBillId, payment: billState.payment, doc: 'faktura', nip: billState.nip, company: billState.company }); showToast("Dziękujemy! Prośba o fakturę została wysłana."); }; // Fallback: If no data after 25s, show empty state anyway setTimeout(() => { if (!loadingScreen.classList.contains("hidden")) { updateUI([]); } }, 25000);