import { endpoints, loaderMinMs } from "./config.js"; import { runPendingProtectedAction, updateNavAccessState } from "./access.js"; import { refreshGuestPendingActions, startGuestPendingPoll, } from "./queue.js"; import { getAppAccessLevel, getHashParam, getPendingProtectedAction, getTableParam, setTableParam, } from "./state.js"; 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 historyKey = "stolik2_global_history"; const historySection = document.getElementById("historySection"); const historyList = document.getElementById("historyList"); const SIX_MONTHS_MS = 180 * 24 * 60 * 60 * 1000; const LOADER_MIN_MS = loaderMinMs; 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; if (loaderMsg) loaderMsg.textContent = msgs[msgIdx]; }, 4000); // Initial State if (getTableParam()) { const tableParam = getTableParam(); tableLabel.textContent = tableParam.toUpperCase().startsWith("STOLIK") ? tableParam : `Stolik ${tableParam}`; } /** Storage key must follow current hash (fallback: table), not a frozen empty tableParam at init. */ export function getOrderStorageKey() { const key = (getHashParam() || getTableParam() || "unknown").toLowerCase(); return `stolik2_state_${key}`; } export function hideLoader() { const elapsed = Date.now() - loadStartTime; const remaining = Math.max(0, LOADER_MIN_MS - elapsed); setTimeout(() => { loadingScreen.classList.add("hidden"); clearInterval(msgInterval); const bottomNav = document.getElementById("bottomNav"); if (bottomNav) { bottomNav.style.display = ""; } updateNavAccessState(); if (getPendingProtectedAction() && getAppAccessLevel() === "full") { runPendingProtectedAction(); } }, remaining); } export async function resolveTableLabel() { const hashParam = getHashParam(); if (!hashParam) return; try { const response = await fetch(`${endpoints.kds}?h=${encodeURIComponent(hashParam)}`); const result = await response.json(); if (result.status === "success" && result.tableName && result.tableName !== "") { tableLabel.textContent = result.tableName.toUpperCase().startsWith("STOLIK") ? result.tableName : `Stolik ${result.tableName}`; setTableParam(result.tableName); } } catch { // best effort } } export 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 showEmptyState() { const title = document.getElementById("ordersTitle"); if (title) title.classList.add("hidden"); emptyState.classList.remove("hidden"); itemsList.innerHTML = ""; prepStatus.textContent = "Brak aktywnych zamówień"; statusIcon.textContent = "🍃"; progressBar.style.width = "0%"; statusMeta.textContent = "Zapraszamy do sprawdzenia naszego menu."; // 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(getOrderStorageKey()); if (!raw) return []; const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed : []; } catch { return []; } } function savePersistedItems(items) { try { localStorage.setItem(getOrderStorageKey(), 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]); } export 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); }); } export function clearGlobalHistory(e) { if (e) e.preventDefault(); if (confirm("Czy na pewno chcesz usunąć historię swoich poprzednich zamówień?")) { localStorage.removeItem(historyKey); renderGlobalHistory(); } } 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 }], getTableParam()); }); // 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) { const title = document.getElementById("ordersTitle"); if (title) title.classList.remove("hidden"); 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 ${getTableParam()}`; } export async function fetchOrders() { try { const hashParam = getHashParam(); if (!hashParam) { updateUI([]); return; } const response = await fetch(`${endpoints.kds}?h=${encodeURIComponent(hashParam)}`); const result = await response.json(); if (result.status === "success") { if (result.tableName && result.tableName !== "") { tableLabel.textContent = result.tableName.toUpperCase().startsWith("STOLIK") ? result.tableName : `Stolik ${result.tableName}`; setTableParam(result.tableName); // Aktualizacja do właściwej nazwy na poczet innych zapytań refreshGuestPendingActions(); startGuestPendingPoll(); } // 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..."; } }