import { endpoints, loaderMinMs } from "./config.js"; import { getLang, onLangChange, t } from "./i18n.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(); /** @type {Array} */ let lastBillsForUi = []; let hasRenderedOrders = false; function escapeHtml(value) { return String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function getLoaderMsgs() { return [t("loader.msg1"), t("loader.msg2"), t("loader.msg3"), t("loader.msg4")]; } let msgIdx = 0; const msgInterval = setInterval(() => { const msgs = getLoaderMsgs(); msgIdx = (msgIdx + 1) % msgs.length; if (loaderMsg) loaderMsg.textContent = msgs[msgIdx]; }, 4000); function formatTableLabel(name) { const raw = String(name || "").trim(); if (!raw) return t("table.label", { name: "" }).trim(); if (raw.toUpperCase().startsWith("STOLIK") || raw.toUpperCase().startsWith("TABLE") || raw.toUpperCase().startsWith("TISCH")) { return raw; } return t("table.label", { name: raw }); } // Initial State if (getTableParam()) { tableLabel.textContent = formatTableLabel(getTableParam()); } onLangChange(() => { if (tableLabel && getTableParam()) { tableLabel.textContent = formatTableLabel(getTableParam()); } if (loaderMsg && loadingScreen && !loadingScreen.classList.contains("hidden")) { loaderMsg.textContent = getLoaderMsgs()[msgIdx]; } if (hasRenderedOrders) { updateUI(lastBillsForUi, { manageLoader: false }); } }); /** 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.hidden = false; } 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 = formatTableLabel(result.tableName); setTableParam(result.tableName); } } catch { // best effort } } export function updateUI(bills, { manageLoader = true } = {}) { lastBillsForUi = Array.isArray(bills) ? bills : []; hasRenderedOrders = true; if (manageLoader) { hideLoader(); } const allArticles = lastBillsForUi.flatMap((b) => (Array.isArray(b?.Articles) ? b.Articles : [])); const items = mergeWithPersistedItems(allArticles); renderGlobalHistory(); if (items.length === 0) { showEmptyState(); return; } renderItems(items); updateStatus(lastBillsForUi, items); } function showEmptyState() { const title = document.getElementById("ordersTitle"); if (title) title.classList.add("hidden"); emptyState.classList.remove("hidden"); itemsList.innerHTML = ""; prepStatus.textContent = t("status.none"); statusIcon.textContent = "馃崈"; progressBar.style.width = "0%"; statusMeta.textContent = t("status.none_meta"); // Historia mo偶e istnie膰 nawet gdy brak bie偶膮cych pozycji renderGlobalHistory(); } function normalizeArticleName(rawName) { const name = String(rawName || t("orders.item_fallback")); // 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() || t("orders.item_fallback"); } 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 lang = getLang(); 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 = `
${escapeHtml(entry.name)} ${escapeHtml(t("history.entry_meta", { table: formatTableLabel(entry.sourceTable || "?"), date: dt.toLocaleDateString(lang), time: dt.toLocaleTimeString(lang, { hour: "2-digit", minute: "2-digit" }), }))}
x${escapeHtml(entry.qty)}
`; historyList.appendChild(div); }); } export function clearGlobalHistory(e) { if (e) e.preventDefault(); if (confirm(t("history.clear_confirm"))) { 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, getLang()); }); 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 = t("orders.item_preparing"); if (isReady && item.completedByDisappear) { meta = t("orders.item_ready_done"); } else if (isReady) { meta = t("orders.item_ready"); } div.innerHTML = `
${escapeHtml(item.name)} ${escapeHtml(meta)}
x${escapeHtml(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 = t("status.ready"); statusIcon.innerHTML = "馃構"; statusMeta.textContent = t("status.ready_meta"); } else if (pct > 0) { prepStatus.textContent = t("status.partial"); statusIcon.innerHTML = "馃嵆"; statusMeta.textContent = t("status.partial_meta"); } else { prepStatus.textContent = t("status.preparing"); if (!window.selectedAnimationHtml) { window.selectedAnimationHtml = window.kitchenAnimations[Math.floor(Math.random() * window.kitchenAnimations.length)]; } statusIcon.innerHTML = window.selectedAnimationHtml; statusMeta.textContent = t("status.preparing_meta"); } // 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(getLang(), { hour: "2-digit", minute: "2-digit" }) : "--:--"; metaFooter.textContent = t("status.footer", { time, table: 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 = formatTableLabel(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 = t("loader.api_error", { message: result.message }); } } catch (err) { loaderMsg.textContent = t("loader.connection"); } }