window.kitchenAnimations = [
`
`,
``,
``,
``,
``
];
window.selectedAnimationHtml = null;
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.toggle("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
}
}
const GEO_GATE_LABELS = {
status: "status zamówienia",
waiter: "wezwanie kelnera",
bill: "prośbę o rachunek",
};
function configureGeoSecondaryButton(mode) {
const menuOnlyBtn = document.getElementById("geoMenuOnlyBtn");
if (!menuOnlyBtn) return;
if (mode === "back_to_menu") {
menuOnlyBtn.style.display = "";
menuOnlyBtn.textContent = "Wróć do menu";
menuOnlyBtn.onclick = () => {
document.getElementById("geoScreen")?.classList.add("hidden");
};
return;
}
menuOnlyBtn.style.display = "";
menuOnlyBtn.textContent = "Przeglądaj menu bez lokalizacji";
menuOnlyBtn.onclick = () => enterMenuOnlyMode();
}
function showGeoGateForAction(action) {
const geoScreen = document.getElementById("geoScreen");
const loadingScreen = document.getElementById("loadingScreen");
const geoMsg = document.getElementById("geoMsg");
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ę";
if (geoMsg) {
geoMsg.innerHTML = `Aby skorzystać z ${feature}, potwierdź, że jesteś w restauracji.
Prosimy o zgodę na dostęp do lokalizacji.`;
}
if (geoActionBtn) {
geoActionBtn.disabled = false;
geoActionBtn.textContent = "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 ---
async function loadMenu() {
try {
const response = await fetch('menu.json');
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 += `
${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();
window.openItemModal = function(categoryId, position) {
if (!window.menuDataRaw) return;
let foundItem = null;
for (const cat of window.menuDataRaw) {
for (const item of cat.items) {
if (item.categoryId == categoryId && item.position == position) {
foundItem = item;
break;
}
}
if (foundItem) break;
}
if (foundItem) {
document.getElementById('itemModalImage').src = foundItem.image;
document.getElementById('itemModalTitle').textContent = foundItem.title;
document.getElementById('itemModalDesc').textContent = foundItem.description;
document.getElementById('itemModalPrice').textContent = foundItem.price;
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 = '';
}
};
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 ---
const RESTAURANT_LAT = 50.5624963;
const RESTAURANT_LNG = 22.0608059;
const MAX_DISTANCE_METERS = 200;
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 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 geoMsg = document.getElementById("geoMsg");
const geoActionBtn = document.getElementById("geoActionBtn");
const menuOnlyBtn = document.getElementById("geoMenuOnlyBtn");
loadingScreen.classList.add("hidden");
geoScreen.classList.remove("hidden");
if (geoMsg) {
geoMsg.innerHTML = `Aby zapewnić bezpieczeństwo Twojego zamówienia, musimy upewnić się, że znajdujesz się na terenie restauracji.
Prosimy o udzielenie zgody na dostęp do lokalizacji w przeglądarce.`;
}
if (geoActionBtn) {
geoActionBtn.disabled = false;
geoActionBtn.textContent = "Udziel zgody / Sprawdź";
}
if (menuOnlyBtn) {
configureGeoSecondaryButton("menu_only");
}
}
function isIOSDevice() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
}
function shouldBypassGeolocationHost() {
const bypassHosts = ['82.160.190.247'];
return bypassHosts.includes(window.location.hostname);
}
window.initGeolocation = function () {
if (shouldBypassGeolocationHost()) {
console.warn("Bypassing geolocation for trusted host.");
trackEvent("geo_bypass_host", { host: window.location.hostname, reason: "trusted_host" });
unlockFullApp();
return;
}
const geoScreen = document.getElementById("geoScreen");
const loadingScreen = document.getElementById("loadingScreen");
const geoMsg = document.getElementById("geoMsg");
const geoActionBtn = document.getElementById("geoActionBtn");
const menuOnlyBtn = document.getElementById("geoMenuOnlyBtn");
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.");
trackEvent("geo_bypass_host", { host: window.location.hostname, reason: "local_http" });
unlockFullApp();
return;
}
if (!window.isSecureContext) {
geoMsg.innerHTML = `Ta strona nie jest uruchomiona w bezpiecznym trybie HTTPS.
Przeglądarki mobilne blokują geolokalizację bez pytania, jeśli adres nie zaczyna się od https://.
Otwórz aplikację przez HTTPS i spróbuj ponownie.
Masz problem z lokalizacją? Połącz się z HotSpot Karczmy, a wtedy wejdziesz do aplikacji bez geolokalizacji.
Hasło: karczmabiesiada`;
if (geoActionBtn) {
geoActionBtn.disabled = false;
geoActionBtn.textContent = "Spróbuj ponownie";
}
if (menuOnlyBtn) configureGeoSecondaryButton(appAccessLevel === "menu" ? "back_to_menu" : "menu_only");
return;
}
loadingScreen.classList.add("hidden");
geoScreen.classList.remove("hidden");
configureGeoSecondaryButton(appAccessLevel === "menu" ? "back_to_menu" : "menu_only");
if (!navigator.geolocation) {
geoMsg.innerHTML = "Twoja przeglądarka nie wspiera geolokalizacji. Aplikacja wymaga nowszej przeglądarki.";
return;
}
geoMsg.innerHTML = "Sprawdzamy Twoją lokalizację...";
trackEvent("geo_check_started");
if (geoActionBtn) {
geoActionBtn.disabled = true;
geoActionBtn.textContent = "Sprawdzanie...";
}
navigator.geolocation.getCurrentPosition(
(position) => {
const dist = haversineDistance(
RESTAURANT_LAT, RESTAURANT_LNG,
position.coords.latitude, position.coords.longitude
);
const accuracy = position.coords.accuracy;
console.log(`[GEO] Lat: ${position.coords.latitude}, Lng: ${position.coords.longitude}, Dist: ${dist}m, Accuracy: ${accuracy}m`);
if (dist <= MAX_DISTANCE_METERS) {
trackEvent("geo_check_passed", { distanceMeters: Math.round(dist), accuracyMeters: Math.round(accuracy) });
unlockFullApp();
// setTimeout(() => showToast(`Lokalizacja zweryfikowana (Dystans: ${Math.round(dist)}m, Dokładność: ${Math.round(accuracy)}m)`), 2000);
} else {
trackEvent("geo_check_failed", { reason: "outside_restaurant", distanceMeters: Math.round(dist), accuracyMeters: Math.round(accuracy) });
if (geoActionBtn) {
geoActionBtn.disabled = false;
geoActionBtn.textContent = "Spróbuj ponownie";
}
configureGeoSecondaryButton(appAccessLevel === "menu" ? "back_to_menu" : "menu_only");
geoMsg.innerHTML = `Wydaje się, że jesteś poza restauracją (ok. ${Math.round(dist)}m od nas).
Nasza aplikacja działa tylko na miejscu.
Debug: Twoja odległość: ${Math.round(dist)}m, Dokładność sygnału: ${Math.round(accuracy)}m
Jeśli to błąd GPS lub słaby sygnał, spróbuj ponownie za chwilę.`;
}
},
(error) => {
trackEvent("geo_check_failed", { reason: "browser_error", code: error.code || null, message: String(error.message || "") });
if (geoActionBtn) {
geoActionBtn.disabled = false;
geoActionBtn.textContent = "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) {
geoMsg.innerHTML = `Przeglądarka zablokowała lokalizację z powodu braku HTTPS.
Geolokalizacja działa tylko na bezpiecznym adresie https:// (lub localhost).
Otwórz aplikację przez HTTPS i spróbuj ponownie.
Masz problem z lokalizacją? Połącz się z HotSpot Karczmy, a wtedy wejdziesz do aplikacji bez geolokalizacji.
Hasło: karczmabiesiada`;
} else if (error.code === error.PERMISSION_DENIED) {
const isIOS = isIOSDevice();
let instructions = '';
if (isIOS) {
instructions = `Instrukcja dla iPhone (Safari):
1. Kliknij ikonę "aA" po lewej stronie paska adresu.
2. Wybierz "Ustawienia witryny" (Website Settings).
3. Zmień opcję "Położenie" (Location) na "Zapytaj" lub "Pozwalaj".
4. Odśwież stronę.
Uwaga: Na urządzeniach Apple lokalizacja działa WYŁĄCZNIE, gdy adres strony zaczyna się od bezpiecznego https://. Jeżeli jesteś na http://, system zablokuje to automatycznie.
Masz problem z lokalizacją? Połącz się z HotSpot Karczmy, a wtedy wejdziesz do aplikacji bez geolokalizacji.
Hasło: karczmabiesiada`;
} else {
instructions = `Instrukcja dla Android / Chrome:
1. Kliknij ikonkę kłódki / ustawień 🔒 obok adresu strony na górze przeglądarki.
2. Znajdź Uprawnienia (Lokalizacja) i zmień z "Zablokuj" na "Zezwalaj".
3. Odśwież stronę.
Masz problem z lokalizacją? Połącz się z HotSpot Karczmy, a wtedy wejdziesz do aplikacji bez geolokalizacji.
Hasło: karczmabiesiada`;
}
geoMsg.innerHTML = `Przeglądarka zablokowała dostęp do lokalizacji.
Bez tego nie możemy zweryfikować, czy jesteś w restauracji.
${instructions}`;
} else {
geoMsg.innerHTML = "Nie udało się pobrać lokalizacji. Sprawdź zasięg lub włącz GPS i spróbuj ponownie.";
}
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
);
};
if (shouldBypassGeolocationHost()) {
startApp();
} else if (isIOSDevice()) {
showGeoConsentScreen();
} else {
initGeolocation();
}