window.kitchenAnimations = [
`
`,
``,
``,
``,
``
];
window.selectedAnimationHtml = null;
const MENU_ASSET_VERSION =
window.MENU_ASSET_VERSION || window.APP_ASSET_VERSION || "1";
const params = new URLSearchParams(location.search);
let hashParam = (params.get("h") || "").trim();
const isStaffPreview = params.get("preview") === "staff";
// 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
let appAccessLevel = "none"; // "none" | "menu" | "full"
let pendingProtectedAction = null; // "status" | "waiter" | "bill"
const analyticsEndpoint = "../api/analytics.php";
const guestActionQueueEndpoint = "../api/guest_action_queue.php";
const analyticsSessionKey = "karczma_analytics_session_id";
function getOrCreateAnalyticsSessionId() {
let existing = localStorage.getItem(analyticsSessionKey);
if (existing) return existing;
let newId = "";
if (window.crypto && crypto.randomUUID) {
newId = crypto.randomUUID();
} else {
newId = `sess_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
}
localStorage.setItem(analyticsSessionKey, newId);
return newId;
}
const analyticsSessionId = getOrCreateAnalyticsSessionId();
function detectDeviceType() {
const ua = navigator.userAgent || "";
if (/iPad|iPhone|iPod/.test(ua) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1)) return "ios";
if (/Android/i.test(ua)) return "android";
return "other";
}
function detectBrowser() {
const ua = navigator.userAgent || "";
if (/Edg\//.test(ua)) return "edge";
if (/OPR\//.test(ua)) return "opera";
if (/Chrome\//.test(ua)) return "chrome";
if (/Safari\//.test(ua)) return "safari";
if (/Firefox\//.test(ua)) return "firefox";
return "other";
}
function deriveZoneFromTable(tableValue) {
const raw = String(tableValue || "").trim().toLowerCase();
if (!raw) return null;
if (raw.startsWith("t") || raw.includes("taras")) return "taras";
if (raw.startsWith("k") || raw.includes("karczma")) return "karczma";
return null;
}
function trackEvent(eventName, payload = {}) {
if (isStaffPreview) {
return;
}
const body = {
eventName,
sessionId: analyticsSessionId,
tableId: tableParam || null,
zone: deriveZoneFromTable(tableParam),
qrHash: hashParam || null,
deviceType: detectDeviceType(),
browser: detectBrowser(),
payload
};
const bodyString = JSON.stringify(body);
try {
if (navigator.sendBeacon) {
const blob = new Blob([bodyString], { type: "application/json" });
navigator.sendBeacon(analyticsEndpoint, blob);
return;
}
} catch {
// fallback to fetch below
}
fetch(analyticsEndpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: bodyString,
keepalive: true
}).catch(() => {
// best effort - ignore analytics errors
});
}
let cachedOpenBills = [];
let guestPendingActions = {
waiter_call: false,
bill_request: false,
};
let guestPendingPollTimer = null;
const GUEST_PENDING_POLL_MS = 15000;
function cacheOpenBills(bills) {
cachedOpenBills = Array.isArray(bills) ? bills : [];
}
function getQueueOperatorFields() {
const bills = cachedOpenBills;
if (!bills.length) {
return { otwierajacyImie: "", otwierajacyNazwisko: "" };
}
let bill = bills[0];
if (billState.selectedBillId) {
const selected = bills.find((b) => b.id === billState.selectedBillId);
if (selected) bill = selected;
}
const o = bill.otwierajacy || {};
return {
otwierajacyImie: String(o.imie || "").trim(),
otwierajacyNazwisko: String(o.nazwisko || "").trim(),
};
}
async function prefetchOpenBills() {
if (!hashParam) return;
try {
const res = await fetch(`../api/bills.php?h=${encodeURIComponent(hashParam)}`);
const result = await res.json();
if (result.status === "success") {
cacheOpenBills(result.data);
}
} catch {
// best effort
}
}
/**
* Komunikat do kolejki KDS — zwykły tekst, wiersze oddzielone \n (w JSON jako entery).
* @param {string} title
* @param {{ label?: string, value?: string }[]} lines
*/
function formatGuestQueueMessage(title, lines = []) {
const rows = (Array.isArray(lines) ? lines : [])
.map((line) => {
const label = String(line?.label ?? "").trim();
const value = String(line?.value ?? "").trim();
if (!label && !value) return "";
if (label && value) return `${label} ${value}`;
return label || value;
})
.filter(Boolean);
if (!rows.length) {
return title;
}
return `${title}\n${rows.join("\n")}`;
}
function buildWaiterCallQueueMessage() {
return "Przywołanie kelnera";
}
function buildBillRequestQueueMessage(docType) {
const lines = [
{ label: "Forma płatności:", value: billState.payment || "nieznana" },
{ label: "Dokument:", value: docType === "faktura" ? "faktura" : "paragon" },
];
if (docType === "faktura") {
lines.push({ label: "NIP:", value: billState.nip || "—" });
lines.push({ label: "Firma:", value: billState.company?.name || "—" });
const addressParts = [
billState.company?.street,
[billState.company?.zip, billState.company?.city].filter(Boolean).join(" "),
].filter(Boolean);
if (addressParts.length) {
lines.push({ label: "Adres:", value: addressParts.join(", ") });
}
}
return formatGuestQueueMessage("Prośba o rachunek", lines);
}
function guestActionBlockedMessage(messageType) {
if (messageType === "waiter_call") {
return "Kelner został już wezwany. Poczekaj, aż obsługa potwierdzi zgłoszenie na panelu.";
}
return "Prośba o rachunek została już wysłana. Poczekaj, aż obsługa ją obsłuży.";
}
function updateGuestActionNavState() {
const waiterNav = document.querySelector(".bottom-nav .action-call");
const billNav = document.querySelector(".bottom-nav .action-bill");
if (waiterNav) {
waiterNav.classList.toggle("nav-action-pending", guestPendingActions.waiter_call);
}
if (billNav) {
billNav.classList.toggle("nav-action-pending", guestPendingActions.bill_request);
}
}
async function refreshGuestPendingActions() {
if (!hashParam && !tableParam) {
return guestPendingActions;
}
const params = new URLSearchParams();
if (hashParam) params.set("h", hashParam);
if (tableParam) params.set("tableId", tableParam);
try {
const res = await fetch(`${guestActionQueueEndpoint}?${params.toString()}`);
const result = await res.json();
if (result.status === "success" && result.pending) {
guestPendingActions.waiter_call = !!result.pending.waiter_call;
guestPendingActions.bill_request = !!result.pending.bill_request;
updateGuestActionNavState();
}
} catch {
// best effort
}
return guestPendingActions;
}
function startGuestPendingPoll() {
if (guestPendingPollTimer) return;
guestPendingPollTimer = setInterval(() => {
if (!hashParam && !tableParam) return;
refreshGuestPendingActions();
}, GUEST_PENDING_POLL_MS);
}
async function ensureGuestActionAllowed(messageType) {
await refreshGuestPendingActions();
if (!guestPendingActions[messageType]) {
return true;
}
showToast(guestActionBlockedMessage(messageType));
return false;
}
async function queueGuestAction(messageType, messageText, extra = {}) {
const operator = getQueueOperatorFields();
const body = {
tableId: tableParam || null,
qrHash: hashParam || null,
messageType,
messageText,
otwierajacyImie: operator.otwierajacyImie,
otwierajacyNazwisko: operator.otwierajacyNazwisko,
extra,
};
try {
const res = await fetch(guestActionQueueEndpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
keepalive: true,
});
const result = await res.json().catch(() => ({}));
if (res.status === 409 || result.code === "pending_on_kds") {
guestPendingActions[messageType] = true;
updateGuestActionNavState();
return { ok: false, reason: "pending" };
}
if (!res.ok || result.status !== "success") {
return { ok: false, reason: "error" };
}
guestPendingActions[messageType] = true;
updateGuestActionNavState();
return { ok: true };
} catch {
return { ok: false, reason: "error" };
}
}
if (hashParam) {
trackEvent("qr_scan", { source: "qr_link_open" });
}
// USER PROFILE LOGIC
const userProfileKey = "karczma_user_profile";
const USER_PROFILE_EXPIRE_MS = 180 * 24 * 60 * 60 * 1000; // ~6 months
function initUserProfile() {
return; // Funkcja tymczasowo wyłączona
try {
const raw = localStorage.getItem(userProfileKey);
let profile = null;
if (raw) {
profile = JSON.parse(raw);
}
const now = Date.now();
// Check if profile exists and is valid
if (profile) {
if (profile.declined) {
// User declined in the past, don't ask again.
return;
}
// If expired, maybe we want to ask again or we could just keep it if they didn't decline.
// The user said: "trzymamy przez wiele miesięcy (odnawiamy datę przy każdej wizycie)"
if (now - profile.lastVisit > USER_PROFILE_EXPIRE_MS) {
// Profile expired. Ask again.
showNameDialog();
} else {
// Profile valid, renew date and show greeting
profile.lastVisit = now;
localStorage.setItem(userProfileKey, JSON.stringify(profile));
showGreeting(profile.name, profile.firstVisit || profile.lastVisit);
}
} else {
// No profile, ask for name
showNameDialog();
}
} catch (err) {
// If error parsing, ask again
showNameDialog();
}
}
function showNameDialog() {
const modal = document.getElementById("nameModal");
if (modal) {
modal.classList.add("active");
document.body.style.overflow = 'hidden';
}
}
function hideNameDialog() {
const modal = document.getElementById("nameModal");
if (modal) {
modal.classList.remove("active");
document.body.style.overflow = '';
}
}
window.saveUserName = function () {
const input = document.getElementById("userNameInput").value.trim();
if (input) {
const now = Date.now();
const profile = { name: input, firstVisit: now, lastVisit: now, declined: false };
localStorage.setItem(userProfileKey, JSON.stringify(profile));
hideNameDialog();
showGreeting(profile.name, profile.firstVisit);
}
};
window.declineUserName = function () {
const profile = { name: null, firstVisit: Date.now(), lastVisit: Date.now(), declined: true };
localStorage.setItem(userProfileKey, JSON.stringify(profile));
hideNameDialog();
};
function showGreeting(name, firstVisitTime) {
const banner = document.getElementById("greetingBanner");
if (banner && name) {
const isToday = new Date(firstVisitTime).toDateString() === new Date().toDateString();
if (isToday) {
banner.innerHTML = `Cześć ${name}, życzymy pysznego posiłku!`;
} else {
banner.innerHTML = `Witaj ${name}, super że do nas wracasz!`;
}
banner.style.display = "block";
}
}
// Call init is now delayed until geolocation succeeds
// initUserProfile();
// 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 = tableParam.toUpperCase().startsWith("STOLIK") ? tableParam : `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);
const bottomNav = document.getElementById("bottomNav");
if (bottomNav) {
bottomNav.style.display = "";
}
updateNavAccessState();
if (pendingProtectedAction && appAccessLevel === "full") {
runPendingProtectedAction();
}
}, remaining);
}
function updateNavAccessState() {
const locked = appAccessLevel === "menu";
["navStatus", "navWaiter", "navBill"].forEach((id) => {
const el = document.getElementById(id);
if (el) el.classList.toggle("nav-locked", locked);
});
const banner = document.getElementById("menuOnlyBanner");
if (banner) {
banner.classList.remove("hidden");
banner.classList.toggle("is-hidden", appAccessLevel !== "menu");
}
}
function showBottomNav() {
const bottomNav = document.getElementById("bottomNav");
if (bottomNav) bottomNav.style.display = "";
updateNavAccessState();
}
async function resolveTableLabel() {
if (!hashParam) return;
try {
const response = await fetch(`../api/kds.php?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}`;
tableParam = result.tableName;
}
} catch {
// best effort
}
}
function isIOSDevice() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
}
const GEO_GATE_LABELS = {
status: "status zamówienia",
waiter: "wezwanie kelnera",
bill: "prośbę o rachunek",
};
const GEO_DEFAULT_LEAD =
"Przeglądaj menu od razu — albo potwierdź, że jesteś u nas, aby wezwać kelnera, śledzić zamówienie i poprosić o rachunek.";
function setGeoLead(html) {
const el = document.getElementById("geoLead");
if (el) el.innerHTML = html;
}
function setGeoStatus(html, { error = false, info = false } = {}) {
const el = document.getElementById("geoMsg");
if (!el) return;
el.innerHTML = html || "";
el.classList.toggle("is-error", error);
el.classList.toggle("is-info", info);
}
function showGeoInstructions(html) {
const el = document.getElementById("geoInstructions");
if (!el) return;
el.innerHTML = html || "";
el.classList.toggle("hidden", !html);
}
function hideGeoInstructions() {
showGeoInstructions("");
}
function setGeoActionBusy(busy) {
const btn = document.getElementById("geoActionBtn");
if (!btn) return;
btn.disabled = false;
btn.setAttribute("aria-busy", busy ? "true" : "false");
if (busy) {
setGeoActionLabel("Sprawdzanie…");
}
}
function isGeoPermissionDenied(error) {
return Number(error?.code) === 1;
}
function setGeoActionLabel(text) {
const btn = document.getElementById("geoActionBtn");
if (!btn) return;
const main = btn.querySelector(".geo-btn-main");
if (main) main.textContent = text;
else btn.textContent = text;
}
function getGeoPermissionInstructions() {
if (isIOSDevice()) {
return `iPhone (Safari):
1. Kliknij aA po lewej stronie paska adresu.
2. Wybierz Ustawienia witryny.
3. Ustaw Położenie na „Zapytaj” lub „Pozwalaj”.
4. Odśwież stronę.
Lokalizacja działa tylko przez bezpieczne https://.`;
}
return `Android / Chrome:
1. Kliknij ikonę kłódki obok adresu strony.
2. W Uprawnieniach zmień Lokalizację na „Zezwalaj”.
3. Odśwież stronę.`;
}
let geoMenuButtonMode = "menu_only";
window.handleGeoMenuClick = function () {
if (geoMenuButtonMode === "back_to_menu") {
document.getElementById("geoScreen")?.classList.add("hidden");
return;
}
enterMenuOnlyMode();
};
window.retryGeolocation = function () {
if (shouldBypassGeolocationHost()) {
bypassGeolocation("trusted_host", { host: window.location.hostname });
return;
}
initGeolocationAfterBypassChecks({ userInitiated: true }).catch((err) => {
console.error("[GEO] retry failed", err);
showGeoPermissionBlockedState();
});
};
function bindGeoScreenButtons() {
document.getElementById("geoMenuOnlyBtn")?.addEventListener("click", (event) => {
event.preventDefault();
handleGeoMenuClick();
});
document.getElementById("geoActionBtn")?.addEventListener("click", (event) => {
event.preventDefault();
retryGeolocation();
});
}
function configureGeoSecondaryButton(mode) {
const menuOnlyBtn = document.getElementById("geoMenuOnlyBtn");
if (!menuOnlyBtn) return;
geoMenuButtonMode = mode;
const mainEl = menuOnlyBtn.querySelector(".geo-btn-main");
const subEl = menuOnlyBtn.querySelector(".geo-btn-sub");
if (mode === "back_to_menu") {
menuOnlyBtn.style.display = "";
if (mainEl) mainEl.textContent = "Wróć do menu";
if (subEl) {
subEl.textContent = "";
subEl.style.display = "none";
}
return;
}
menuOnlyBtn.style.display = "";
if (mainEl) mainEl.textContent = "Przejdź do menu";
if (subEl) {
subEl.textContent = "bez lokalizacji";
subEl.style.display = "";
}
}
function showGeoGateForAction(action) {
const geoScreen = document.getElementById("geoScreen");
const loadingScreen = document.getElementById("loadingScreen");
const geoActionBtn = document.getElementById("geoActionBtn");
if (loadingScreen) loadingScreen.classList.add("hidden");
if (geoScreen) geoScreen.classList.remove("hidden");
const feature = GEO_GATE_LABELS[action] || "tę funkcję";
setGeoLead(`Menu masz już otwarte. Aby skorzystać z ${feature}, potwierdź krótko, że jesteś w restauracji.`);
setGeoStatus("");
hideGeoInstructions();
if (geoActionBtn) {
setGeoActionBusy(false);
setGeoActionLabel("Sprawdź lokalizację");
}
configureGeoSecondaryButton(appAccessLevel === "menu" ? "back_to_menu" : "menu_only");
}
function requireFullAccess(action, onGranted) {
if (appAccessLevel === "full") {
onGranted();
return;
}
pendingProtectedAction = action;
trackEvent("geo_gate_prompted", { action });
if (appAccessLevel === "menu") {
trackEvent("geo_retry_from_menu", { action });
}
showGeoGateForAction(action);
}
window.promptGeoForFullAccess = function () {
pendingProtectedAction = pendingProtectedAction || "status";
trackEvent("geo_gate_prompted", { action: pendingProtectedAction, source: "menu_banner" });
trackEvent("geo_retry_from_menu", { action: pendingProtectedAction, source: "menu_banner" });
showGeoGateForAction(pendingProtectedAction);
};
function runPendingProtectedAction() {
const action = pendingProtectedAction;
pendingProtectedAction = null;
if (!action) return;
setTimeout(() => {
if (action === "status") switchTabInternal("status");
else if (action === "waiter") openWaiterDialogInternal();
else if (action === "bill") openBillDialogInternal();
}, 150);
}
function unlockFullApp() {
appAccessLevel = "full";
updateNavAccessState();
document.getElementById("geoScreen")?.classList.add("hidden");
const loaderVisible = !document.getElementById("loadingScreen")?.classList.contains("hidden");
const alreadyStarted = !!window.ordersInterval;
if (alreadyStarted && !loaderVisible) {
trackEvent("session_start", { flow: "unlock_from_menu" });
initUserProfile();
fetchOrders();
prefetchOpenBills();
refreshGuestPendingActions();
startGuestPendingPoll();
runPendingProtectedAction();
return;
}
startApp();
}
window.enterMenuOnlyMode = function () {
trackEvent("menu_only_entered");
appAccessLevel = "menu";
pendingProtectedAction = null;
document.getElementById("geoScreen")?.classList.add("hidden");
document.getElementById("loadingScreen")?.classList.add("hidden");
showBottomNav();
resolveTableLabel();
switchTabInternal("menu");
};
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() {
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(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);
});
}
window.clearGlobalHistory = function (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 }], 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) {
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 ${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 = result.tableName.toUpperCase().startsWith("STOLIK") ? result.tableName : `Stolik ${result.tableName}`;
tableParam = 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...";
}
}
// fetchOrders();
// setInterval(fetchOrders, 10000);
// --- 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 = async function (type) {
if (type !== "order") return;
if (!(await ensureGuestActionAllowed("waiter_call"))) {
return;
}
const queued = await queueGuestAction("waiter_call", buildWaiterCallQueueMessage(), {
waiterType: "order",
});
if (!queued.ok) {
if (queued.reason === "pending") {
showToast(guestActionBlockedMessage("waiter_call"));
} else {
showToast("Nie udało się wysłać wezwania. Spróbuj ponownie za chwilę.");
}
return;
}
trackEvent("waiter_call_requested", { waiterType: "order" });
sendApiSimulated("CallWaiter_Order", { table: tableParam });
showToast("Kelner wkrótce do Ciebie podejdzie!");
};
window.openWaiterDialog = function () {
requireFullAccess("waiter", () => {
openWaiterDialogInternal();
});
};
async function openWaiterDialogInternal() {
if (!(await ensureGuestActionAllowed("waiter_call"))) {
return;
}
document.getElementById("waiterModal").classList.add("active");
document.body.style.overflow = 'hidden';
};
window.closeWaiterDialog = function () {
document.getElementById("waiterModal").classList.remove("active");
document.body.style.overflow = '';
};
window.confirmCallWaiter = async function () {
closeWaiterDialog();
await callWaiter("order");
};
window.proceedToBillPayment = async function () {
if (!(await ensureGuestActionAllowed("bill_request"))) {
return;
}
goToStep("stepPayment");
};
window.openBillDialog = function () {
requireFullAccess("bill", () => {
openBillDialogInternal();
});
};
async function openBillDialogInternal() {
await refreshGuestPendingActions();
trackEvent("bill_dialog_opened");
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;
cacheOpenBills(bills);
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 = `
${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(el => el.classList.remove('active'));
document.getElementById(stepId).classList.add('active');
};
// --- SPA NAVIGATION LOGIC ---
function switchTabInternal(tabName) {
// 1. Zdejmij .active z widoków i ikonek nav
document.querySelectorAll('.view-section').forEach(el => el.classList.add('hidden'));
document.querySelectorAll('.view-section').forEach(el => el.classList.remove('active'));
document.getElementById('navStatus').classList.remove('active');
document.getElementById('navMenu').classList.remove('active');
const header = document.getElementById('mainHeader');
const greetingBanner = document.getElementById('greetingBanner');
// 2. Nadaj .active wybranym elementom
if (tabName === 'status') {
trackEvent("view_status");
const view = document.getElementById('statusView');
view.classList.remove('hidden');
view.classList.add('active');
document.getElementById('navStatus').classList.add('active');
if (header) header.style.display = '';
if (greetingBanner && greetingBanner.innerHTML.trim() !== '') greetingBanner.style.display = '';
}
else if (tabName === 'menu') {
trackEvent("view_menu", { flow: appAccessLevel === "menu" ? "menu_only" : "full_app" });
const view = document.getElementById('menuView');
view.classList.remove('hidden');
view.classList.add('active');
document.getElementById('navMenu').classList.add('active');
if (header) header.style.display = 'none';
if (greetingBanner) greetingBanner.style.display = 'none';
}
window.scrollTo(0, 0);
}
window.switchTab = function (tabName) {
if (tabName === "menu") {
switchTabInternal("menu");
return;
}
if (tabName === "status") {
requireFullAccess("status", () => switchTabInternal("status"));
}
};
// --- MENU LOGIC ---
window.filterMenu = function () {
const query = document.getElementById('menuSearchInput').value.toLowerCase();
const now = Date.now();
if (query.length >= 2 && (!window.lastMenuSearchEventAt || now - window.lastMenuSearchEventAt > 8000)) {
window.lastMenuSearchEventAt = now;
trackEvent("menu_search", { queryLength: query.length });
}
const categories = document.querySelectorAll('.rm-category');
categories.forEach(category => {
let hasVisibleItems = false;
const items = category.querySelectorAll('.rmc-position');
items.forEach(item => {
const title = item.querySelector('.rmc-title h4').textContent.toLowerCase();
if (title.includes(query)) {
item.style.display = '';
hasVisibleItems = true;
} else {
item.style.display = 'none';
}
});
if (hasVisibleItems) {
category.style.display = '';
} else {
category.style.display = 'none';
}
});
};
window.showCategory = function (categoryId) {
document.querySelectorAll('.menu-categories-nav a').forEach(a => a.classList.remove('active'));
const clickedLink = document.querySelector(`.menu-categories-nav a[data-category-badge="${categoryId}"]`);
if (clickedLink) {
clickedLink.classList.add('active');
clickedLink.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
}
const categories = document.querySelectorAll('.rm-category');
categories.forEach(cat => {
if (categoryId === 0) {
cat.style.display = '';
} else {
const catId = parseInt(cat.getAttribute('data-cat-id'), 10);
if (catId === categoryId) {
cat.style.display = '';
} else {
cat.style.display = 'none';
}
}
});
};
window.selectPayment = function (method) {
billState.payment = method;
goToStep("stepDocument");
};
window.selectDocument = async function (docType) {
billState.doc = docType;
if (docType === 'paragon') {
if (!(await ensureGuestActionAllowed("bill_request"))) {
return;
}
const queued = await queueGuestAction("bill_request", buildBillRequestQueueMessage("paragon"), {
payment: billState.payment || null,
docType: "paragon",
});
if (!queued.ok) {
if (queued.reason === "pending") {
showToast(guestActionBlockedMessage("bill_request"));
} else {
showToast("Nie udało się wysłać prośby o rachunek. Spróbuj ponownie za chwilę.");
}
return;
}
trackEvent("bill_request_sent", { 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 = async 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;
try {
const apiKey = "d8bdc252-e0f1-4863-97a1-826faddbc49c";
const response = await fetch(`https://api.magico.pro/v1/gus/${nip}?api_key=${apiKey}`);
const result = await response.json();
if (result.status && result.data) {
const data = result.data;
let fullStreet = data.street || '';
if (data.propertyNumber) fullStreet += ' ' + data.propertyNumber;
if (data.apartmentNumber) fullStreet += '/' + data.apartmentNumber;
billState.nip = nip;
billState.company = {
name: data.name,
street: fullStreet.trim(),
zip: data.zipCode,
city: data.city,
nip: data.nip
};
document.getElementById("cmpName").value = billState.company.name;
document.getElementById("cmpStreet").value = billState.company.street;
document.getElementById("cmpZip").value = billState.company.zip;
document.getElementById("cmpCity").value = billState.company.city;
document.getElementById("cmpNip").value = "NIP: " + billState.company.nip;
// reset do readonly
document.getElementById("cmpName").readOnly = true;
document.getElementById("cmpStreet").readOnly = true;
document.getElementById("cmpZip").readOnly = true;
document.getElementById("cmpCity").readOnly = true;
document.getElementById("btnEditCompany").textContent = "Popraw ręcznie";
goToStep("stepVerify");
} else {
alert("Nie udało się pobrać danych z GUS dla podanego NIP-u.");
}
} catch (error) {
console.error("Błąd pobierania danych z GUS:", error);
alert("Błąd połączenia z API GUS.");
} finally {
btn.textContent = "Pobierz z GUS";
btn.disabled = false;
}
};
// --- DYNAMIC MENU LOADING ---
let itemModalKeys = [];
let itemModalIndex = -1;
let itemModalTouchStart = null;
let itemModalDragging = false;
let itemModalAnimating = false;
function resetItemModalPane() {
const pane = document.getElementById("itemModalPane");
if (!pane) return;
pane.classList.remove(
"is-dragging",
"is-exiting-left",
"is-exiting-right",
"is-entering-from-left",
"is-entering-from-right"
);
pane.style.transform = "";
pane.style.opacity = "";
}
function waitForPaneTransition(pane, timeoutMs = 400) {
return new Promise((resolve) => {
let settled = false;
const finish = () => {
if (settled) return;
settled = true;
pane.removeEventListener("transitionend", onEnd);
resolve();
};
const onEnd = (e) => {
if (e.target === pane) finish();
};
pane.addEventListener("transitionend", onEnd);
setTimeout(finish, timeoutMs);
});
}
function isValidMenuImageUrl(url) {
return typeof url === "string" && url.trim().length > 0;
}
window.handleMenuImageError = function (imgEl) {
if (!imgEl) return;
imgEl.onerror = null;
imgEl.classList.add("hidden");
const wrap = imgEl.closest(".rmc-image-wrap, .item-modal-image-wrap");
const placeholder = wrap?.querySelector(".menu-image-placeholder");
if (placeholder) placeholder.classList.remove("hidden");
};
function applyMenuItemImage(imgEl, placeholderEl, url) {
if (!imgEl || !placeholderEl) return;
if (!isValidMenuImageUrl(url)) {
imgEl.src = "";
imgEl.classList.add("hidden");
placeholderEl.classList.remove("hidden");
return;
}
imgEl.onload = () => {
imgEl.classList.remove("hidden");
placeholderEl.classList.add("hidden");
};
imgEl.onerror = () => handleMenuImageError(imgEl);
imgEl.classList.remove("hidden");
placeholderEl.classList.add("hidden");
imgEl.src = url;
}
function renderMenuListImage(url) {
const hasUrl = isValidMenuImageUrl(url);
const imgClass = hasUrl ? "rmc-image" : "rmc-image hidden";
const placeholderClass = hasUrl ? "menu-image-placeholder hidden" : "menu-image-placeholder";
const srcAttr = hasUrl ? ` src="${url}"` : "";
const onerror = hasUrl ? ' onerror="handleMenuImageError(this)"' : "";
return ``;
}
function findMenuItem(categoryId, position) {
if (!window.menuDataRaw) return null;
for (const cat of window.menuDataRaw) {
for (const item of cat.items) {
if (item.categoryId == categoryId && item.position == position) {
return item;
}
}
}
return null;
}
function buildVisibleMenuItemKeys() {
const keys = [];
document.querySelectorAll(".rmc-position").forEach((el) => {
if (el.style.display === "none") return;
const category = el.closest(".rm-category");
if (category && category.style.display === "none") return;
keys.push({
categoryId: el.getAttribute("data-category-id"),
position: el.getAttribute("data-position"),
});
});
return keys;
}
function updateItemModalNavigation() {
const total = itemModalKeys.length;
const counter = document.getElementById("itemModalCounter");
if (counter) counter.textContent = total > 1 ? `${itemModalIndex + 1} / ${total}` : "";
}
function populateItemModal(item) {
const imgEl = document.getElementById("itemModalImage");
const placeholderEl = document.getElementById("itemModalImagePlaceholder");
const titleEl = document.getElementById("itemModalTitle");
const descEl = document.getElementById("itemModalDesc");
const priceEl = document.getElementById("itemModalPrice");
applyMenuItemImage(imgEl, placeholderEl, item.image);
if (titleEl) titleEl.textContent = item.title;
if (descEl) descEl.textContent = item.description || "";
if (priceEl) priceEl.textContent = item.price;
updateItemModalNavigation();
}
window.navigateItemModal = async function (delta) {
if (!itemModalKeys.length || !delta || itemModalAnimating) return;
const nextIndex = itemModalIndex + delta;
if (nextIndex < 0 || nextIndex >= itemModalKeys.length) return;
const key = itemModalKeys[nextIndex];
const item = findMenuItem(key.categoryId, key.position);
const pane = document.getElementById("itemModalPane");
if (!item || !pane) return;
itemModalAnimating = true;
resetItemModalPane();
const exitClass = delta > 0 ? "is-exiting-left" : "is-exiting-right";
const enterClass = delta > 0 ? "is-entering-from-right" : "is-entering-from-left";
pane.classList.add(exitClass);
await waitForPaneTransition(pane);
itemModalIndex = nextIndex;
populateItemModal(item);
pane.classList.remove(exitClass);
pane.classList.add(enterClass);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
pane.classList.remove(enterClass);
});
});
await waitForPaneTransition(pane);
itemModalAnimating = false;
};
function bindItemModalSwipe() {
const content = document.getElementById("itemModalContent");
const pane = document.getElementById("itemModalPane");
const modal = document.getElementById("itemModal");
if (!content || !pane || !modal || content.dataset.swipeBound) return;
content.dataset.swipeBound = "1";
content.addEventListener(
"touchstart",
(e) => {
if (itemModalAnimating || itemModalKeys.length <= 1 || e.touches.length !== 1) return;
if (e.target.closest("button")) return;
const touch = e.touches[0];
itemModalTouchStart = { x: touch.clientX, y: touch.clientY };
itemModalDragging = false;
},
{ passive: true }
);
content.addEventListener(
"touchmove",
(e) => {
if (!itemModalTouchStart || itemModalAnimating || itemModalKeys.length <= 1) return;
const touch = e.touches[0];
const dx = touch.clientX - itemModalTouchStart.x;
const dy = touch.clientY - itemModalTouchStart.y;
if (!itemModalDragging) {
if (Math.abs(dx) > 12 && Math.abs(dx) > Math.abs(dy)) {
itemModalDragging = true;
pane.classList.add("is-dragging");
} else if (Math.abs(dy) > 12) {
itemModalTouchStart = null;
return;
} else {
return;
}
}
let translateX = dx;
if (itemModalIndex <= 0 && dx > 0) translateX = dx * 0.3;
if (itemModalIndex >= itemModalKeys.length - 1 && dx < 0) translateX = dx * 0.3;
pane.style.transform = `translateX(${translateX}px)`;
pane.style.opacity = String(1 - Math.min(Math.abs(translateX) / 320, 0.18));
},
{ passive: true }
);
content.addEventListener(
"touchend",
(e) => {
if (!itemModalTouchStart) return;
const touch = e.changedTouches[0];
const dx = touch.clientX - itemModalTouchStart.x;
const dy = touch.clientY - itemModalTouchStart.y;
const wasDragging = itemModalDragging;
itemModalTouchStart = null;
itemModalDragging = false;
if (!wasDragging) return;
pane.classList.remove("is-dragging");
pane.style.transform = "";
pane.style.opacity = "";
if (Math.abs(dx) >= 50 && Math.abs(dx) > Math.abs(dy)) {
navigateItemModal(dx < 0 ? 1 : -1);
}
},
{ passive: true }
);
document.addEventListener("keydown", (e) => {
if (!modal.classList.contains("active")) return;
if (e.key === "ArrowRight") navigateItemModal(1);
if (e.key === "ArrowLeft") navigateItemModal(-1);
if (e.key === "Escape") closeItemModal();
});
}
async function loadMenu() {
try {
const response = await fetch(`menu.json?v=${encodeURIComponent(MENU_ASSET_VERSION)}`);
if (!response.ok) throw new Error('Nie udało się załadować menu');
const menuData = await response.json();
window.menuDataRaw = menuData;
const container = document.getElementById('menuContainer');
if (!container) return;
container.innerHTML = '';
menuData.forEach(category => {
const catId = category.items.length > 0 ? category.items[0].categoryId : '';
const catDiv = document.createElement('div');
catDiv.className = 'rm-category';
catDiv.setAttribute('data-cat-id', catId);
let html = `
`;
category.items.forEach(item => {
html += `
${renderMenuListImage(item.image)}
${item.title}${item.description}
${item.price}
`;
});
html += `
`;
catDiv.innerHTML = html;
container.appendChild(catDiv);
});
} catch (err) {
console.error('Błąd ładowania menu:', err);
const container = document.getElementById('menuContainer');
if (container) {
container.innerHTML = 'Nie udało się załadować menu.
';
}
}
}
// Inicjalizacja ładowania menu
loadMenu();
bindItemModalSwipe();
window.openItemModal = function(categoryId, position) {
itemModalKeys = buildVisibleMenuItemKeys();
itemModalIndex = itemModalKeys.findIndex(
(key) => key.categoryId == categoryId && key.position == position
);
if (itemModalIndex < 0) {
itemModalKeys = [{ categoryId, position }];
itemModalIndex = 0;
}
const foundItem = findMenuItem(categoryId, position);
if (!foundItem) return;
populateItemModal(foundItem);
resetItemModalPane();
const modal = document.getElementById('itemModal');
if (modal) {
modal.classList.add('active');
document.body.style.overflow = 'hidden';
}
};
window.closeItemModal = function() {
const modal = document.getElementById('itemModal');
if (modal) {
modal.classList.remove('active');
document.body.style.overflow = '';
}
itemModalTouchStart = null;
itemModalDragging = false;
itemModalAnimating = false;
resetItemModalPane();
};
window.editCompanyData = function () {
const n = document.getElementById("cmpName");
const s = document.getElementById("cmpStreet");
const z = document.getElementById("cmpZip");
const c = document.getElementById("cmpCity");
const btn = document.getElementById("btnEditCompany");
if (n.readOnly) {
n.readOnly = false;
s.readOnly = false;
z.readOnly = false;
c.readOnly = false;
n.focus();
btn.textContent = "Zakończ edycję";
} else {
n.readOnly = true;
s.readOnly = true;
z.readOnly = true;
c.readOnly = true;
btn.textContent = "Popraw ręcznie";
}
};
window.confirmInvoice = async function () {
billState.company.name = document.getElementById("cmpName").value;
billState.company.street = document.getElementById("cmpStreet").value;
billState.company.zip = document.getElementById("cmpZip").value;
billState.company.city = document.getElementById("cmpCity").value;
if (!(await ensureGuestActionAllowed("bill_request"))) {
return;
}
const queued = await queueGuestAction("bill_request", buildBillRequestQueueMessage("faktura"), {
payment: billState.payment || null,
docType: "faktura",
nip: billState.nip || null,
company: billState.company || null,
});
if (!queued.ok) {
if (queued.reason === "pending") {
showToast(guestActionBlockedMessage("bill_request"));
} else {
showToast("Nie udało się wysłać prośby o rachunek. Spróbuj ponownie za chwilę.");
}
return;
}
closeBillDialog();
trackEvent("bill_request_sent", { docType: "faktura" });
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.");
};
// --- GEOLOCATION LOGIC ---
// Dwa punkty odniesienia: OSM (adres budynku) i pin Google Maps (z nim porównują goście w Maps).
const RESTAURANT_LOCATIONS = [
{ lat: 50.5622609, lng: 22.0606303, source: "osm" },
{ lat: 50.567953, lng: 22.061045, source: "google_maps" },
];
const MAX_DISTANCE_METERS = 300;
const MAX_ACCURACY_BONUS = 150;
function haversineDistance(lat1, lon1, lat2, lon2) {
const R = 6371e3;
const p1 = lat1 * Math.PI / 180;
const p2 = lat2 * Math.PI / 180;
const dp = (lat2 - lat1) * Math.PI / 180;
const dl = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dp / 2) * Math.sin(dp / 2) +
Math.cos(p1) * Math.cos(p2) *
Math.sin(dl / 2) * Math.sin(dl / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
function distanceToRestaurant(lat, lng) {
return Math.min(
...RESTAURANT_LOCATIONS.map(({ lat: rLat, lng: rLng }) =>
haversineDistance(rLat, rLng, lat, lng)
)
);
}
function isInsideRestaurantGeofence(distanceMeters, accuracyMeters) {
const accuracyBonus = Math.min(Math.max(Number(accuracyMeters) || 0, 0), MAX_ACCURACY_BONUS);
return distanceMeters <= MAX_DISTANCE_METERS + accuracyBonus;
}
function requestRestaurantGeolocation() {
const geoOptions = {
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 0,
};
return new Promise((resolve, reject) => {
let bestSample = null;
let watchId = null;
let settled = false;
const finish = (result) => {
if (settled) return;
settled = true;
if (watchId != null) navigator.geolocation.clearWatch(watchId);
clearTimeout(timeoutId);
resolve(result);
};
const fail = (error) => {
if (settled) return;
settled = true;
if (watchId != null) navigator.geolocation.clearWatch(watchId);
clearTimeout(timeoutId);
reject(error);
};
const handlePosition = (position) => {
const dist = distanceToRestaurant(position.coords.latitude, position.coords.longitude);
const accuracy = position.coords.accuracy;
const sample = { position, dist, accuracy };
if (!bestSample || dist < bestSample.dist) {
bestSample = sample;
}
if (isInsideRestaurantGeofence(dist, accuracy)) {
finish({ passed: true, ...sample });
}
};
const timeoutId = setTimeout(() => {
if (bestSample) {
finish({
passed: isInsideRestaurantGeofence(bestSample.dist, bestSample.accuracy),
...bestSample,
});
return;
}
fail({ code: 3, message: "Geolocation timeout" });
}, 12000);
watchId = navigator.geolocation.watchPosition(handlePosition, fail, geoOptions);
});
}
function startApp() {
appAccessLevel = "full";
updateNavAccessState();
document.getElementById("geoScreen").classList.add("hidden");
document.getElementById("loadingScreen").classList.remove("hidden");
trackEvent("session_start", { flow: "start_app" });
initUserProfile();
fetchOrders();
prefetchOpenBills();
refreshGuestPendingActions();
startGuestPendingPoll();
if (!window.ordersInterval) {
window.ordersInterval = setInterval(fetchOrders, 10000);
}
// Fallback: If no data after 25s, show empty state anyway
setTimeout(() => {
if (!document.getElementById("loadingScreen").classList.contains("hidden")) {
updateUI([]);
}
}, 25000);
}
function showGeoConsentScreen() {
const geoScreen = document.getElementById("geoScreen");
const loadingScreen = document.getElementById("loadingScreen");
const geoActionBtn = document.getElementById("geoActionBtn");
loadingScreen.classList.add("hidden");
geoScreen.classList.remove("hidden");
setGeoLead(GEO_DEFAULT_LEAD);
setGeoStatus("");
hideGeoInstructions();
if (geoActionBtn) {
setGeoActionBusy(false);
setGeoActionLabel("Zgoda, sprawdź lokalizację");
}
configureGeoSecondaryButton("menu_only");
}
function showGeoPermissionBlockedState() {
setGeoStatus("Przeglądarka zablokowała dostęp do lokalizacji.", { error: true });
showGeoInstructions(
`${getGeoPermissionInstructions()}
Po zmianie ustawień odśwież stronę, a potem kliknij „Spróbuj ponownie”.`
);
setGeoActionBusy(false);
setGeoActionLabel("Spróbuj ponownie");
configureGeoSecondaryButton(appAccessLevel === "menu" ? "back_to_menu" : "menu_only");
}
function shouldBypassGeolocationHost() {
const bypassHosts = ['82.160.190.247'];
return bypassHosts.includes(window.location.hostname);
}
async function checkGeoBypassByClientIp() {
try {
const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
const timeoutId = controller
? setTimeout(() => controller.abort(), 4000)
: null;
const res = await fetch("../api/geo_bypass.php", {
credentials: "same-origin",
cache: "no-store",
signal: controller?.signal,
});
if (timeoutId) clearTimeout(timeoutId);
const data = await res.json();
return data.status === "success" && data.bypassGeo === true;
} catch {
return false;
}
}
function bypassGeolocation(reason, extra = {}) {
trackEvent('geo_bypass_host', { reason, ...extra });
if (appAccessLevel === 'menu') {
unlockFullApp();
} else {
startApp();
}
}
async function queryGeolocationPermissionState() {
if (!navigator.permissions?.query) {
return "unknown";
}
try {
const status = await navigator.permissions.query({ name: "geolocation" });
return status.state;
} catch {
return "unknown";
}
}
async function bootstrapGeolocation() {
if (shouldBypassGeolocationHost()) {
bypassGeolocation('trusted_host', { host: window.location.hostname });
return;
}
if (await checkGeoBypassByClientIp()) {
bypassGeolocation('trusted_ip');
return;
}
showGeoConsentScreen();
}
window.initGeolocation = function () {
if (shouldBypassGeolocationHost()) {
console.warn("Bypassing geolocation for trusted host.");
bypassGeolocation("trusted_host", { host: window.location.hostname });
return;
}
checkGeoBypassByClientIp().then((bypassByIp) => {
if (bypassByIp) {
console.warn("Bypassing geolocation for trusted client IP.");
bypassGeolocation("trusted_ip");
return;
}
initGeolocationAfterBypassChecks();
});
};
async function initGeolocationAfterBypassChecks(options = {}) {
const geoScreen = document.getElementById("geoScreen");
const loadingScreen = document.getElementById("loadingScreen");
const geoActionBtn = document.getElementById("geoActionBtn");
const bypassHosts = ['localhost', '127.0.0.1', '192.168.20.84'];
if (window.location.protocol === 'http:' && bypassHosts.includes(window.location.hostname)) {
console.warn("Bypassing geolocation on local HTTP environment.");
bypassGeolocation("local_http", { host: window.location.hostname });
return;
}
loadingScreen?.classList.add("hidden");
geoScreen?.classList.remove("hidden");
configureGeoSecondaryButton(appAccessLevel === "menu" ? "back_to_menu" : "menu_only");
if (!window.isSecureContext) {
setGeoLead(GEO_DEFAULT_LEAD);
setGeoStatus("Ta strona wymaga bezpiecznego połączenia HTTPS.", { error: true });
showGeoInstructions("Przeglądarki mobilne blokują geolokalizację bez https://. Otwórz aplikację przez HTTPS i spróbuj ponownie.");
setGeoActionBusy(false);
setGeoActionLabel("Spróbuj ponownie");
return;
}
if (!navigator.geolocation) {
setGeoLead(GEO_DEFAULT_LEAD);
setGeoStatus("Twoja przeglądarka nie wspiera geolokalizacji.", { error: true });
hideGeoInstructions();
setGeoActionBusy(false);
return;
}
setGeoLead(GEO_DEFAULT_LEAD);
setGeoStatus("Sprawdzamy Twoją lokalizację…", { info: true });
hideGeoInstructions();
setGeoActionBusy(true);
const permissionState = await queryGeolocationPermissionState();
if (permissionState === "denied") {
showGeoPermissionBlockedState();
return;
}
trackEvent("geo_check_started");
try {
const result = await requestRestaurantGeolocation();
const dist = result.dist;
const accuracy = result.accuracy;
console.log(
`[GEO] Lat: ${result.position.coords.latitude}, Lng: ${result.position.coords.longitude}, ` +
`MinDist: ${Math.round(dist)}m, Accuracy: ${Math.round(accuracy)}m`
);
if (result.passed) {
trackEvent("geo_check_passed", {
distanceMeters: Math.round(dist),
accuracyMeters: Math.round(accuracy),
});
unlockFullApp();
return;
}
trackEvent("geo_check_failed", {
reason: "outside_restaurant",
distanceMeters: Math.round(dist),
accuracyMeters: Math.round(accuracy),
});
setGeoActionBusy(false);
setGeoActionLabel("Spróbuj ponownie");
configureGeoSecondaryButton(appAccessLevel === "menu" ? "back_to_menu" : "menu_only");
setGeoStatus(
`Wygląda na to, że jesteś poza restauracją (ok. ${Math.round(dist)} m, dokładność GPS: ±${Math.round(accuracy)} m).`,
{ error: true }
);
showGeoInstructions(
"Przeglądarka często podaje inną lokalizację niż aplikacja Map Google. " +
"Spróbuj ponownie na zewnątrz lub bliżej okna — albo przejdź do menu bez lokalizacji."
);
} catch (error) {
trackEvent("geo_check_failed", {
reason: "browser_error",
code: error.code || null,
message: String(error.message || ""),
});
setGeoActionBusy(false);
setGeoActionLabel("Spróbuj ponownie");
configureGeoSecondaryButton(appAccessLevel === "menu" ? "back_to_menu" : "menu_only");
const deniedBecauseInsecure = /secure origins|only secure|https/i.test(String(error.message || ""));
if (deniedBecauseInsecure) {
setGeoStatus("Geolokalizacja wymaga HTTPS.", { error: true });
showGeoInstructions("Otwórz aplikację przez bezpieczny adres https:// i spróbuj ponownie.");
} else if (isGeoPermissionDenied(error)) {
showGeoPermissionBlockedState();
} else {
setGeoStatus("Nie udało się pobrać lokalizacji.", { error: true });
showGeoInstructions("Sprawdź zasięg, włącz GPS i spróbuj ponownie.");
}
}
};
bindGeoScreenButtons();
if (shouldBypassGeolocationHost()) {
bypassGeolocation("trusted_host", { host: window.location.hostname });
} else {
bootstrapGeolocation();
}