1921 lines
59 KiB
JavaScript
1921 lines
59 KiB
JavaScript
window.kitchenAnimations = [
|
||
`<div class="kitchen-anim"><div class="fire-emoji">🔥</div><div class="pan-emoji">🍳</div></div>`,
|
||
`<div class="anim-run"><div class="chicken">🐓</div><div class="chef">👨🍳</div></div>`,
|
||
`<div class="anim-pot"><div class="chicken-head">🐓</div><div class="pot-emoji">🍲</div></div>`,
|
||
`<div class="anim-pizza"><div class="chef-pizza">👨🍳</div><div class="pizza-emoji">🍕</div></div>`,
|
||
`<div class="anim-pig"><div class="pig-emoji">🐷</div><div class="sweat-emoji">💦</div><div class="knife-emoji">🍴</div></div>`
|
||
];
|
||
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 `<b>iPhone (Safari):</b><br>
|
||
1. Kliknij <b>aA</b> po lewej stronie paska adresu.<br>
|
||
2. Wybierz <b>Ustawienia witryny</b>.<br>
|
||
3. Ustaw <b>Położenie</b> na „Zapytaj” lub „Pozwalaj”.<br>
|
||
4. Odśwież stronę.<br><br>
|
||
<i>Lokalizacja działa tylko przez bezpieczne <b>https://</b>.</i>`;
|
||
}
|
||
|
||
return `<b>Android / Chrome:</b><br>
|
||
1. Kliknij ikonę <b>kłódki</b> obok adresu strony.<br>
|
||
2. W <b>Uprawnieniach</b> zmień Lokalizację na „Zezwalaj”.<br>
|
||
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 <b>${feature}</b>, 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 = `
|
||
<div class="item-info">
|
||
<span class="item-name">${entry.name}</span>
|
||
<span class="item-meta">Stolik ${entry.sourceTable || "?"} • ${dt.toLocaleDateString("pl-PL")} ${dt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||
</div>
|
||
<div class="item-qty">x${entry.qty}</div>
|
||
`;
|
||
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 = `
|
||
<div class="item-info">
|
||
<span class="item-name">${item.name}</span>
|
||
<span class="item-meta">${meta}</span>
|
||
</div>
|
||
<div class="item-qty">x${item.qty}</div>
|
||
`;
|
||
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 = `
|
||
<div>
|
||
<div style="font-weight:bold;">${numerFormat}</div>
|
||
<div style="font-size:12px; color:var(--text-muted);">${b.opis}</div>
|
||
</div>
|
||
<div style="font-weight:bold; color:var(--primary);">${b.suma.toFixed(2)} PLN</div>
|
||
`;
|
||
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 = `
|
||
<div style="flex:1;">
|
||
<div style="font-weight:600; font-size: 14px;">${p.nazwa}</div>
|
||
<div style="font-size:12px; color:var(--text-muted);">${p.ilosc} x ${p.cena.toFixed(2)} PLN</div>
|
||
</div>
|
||
<div style="font-weight:600;">${p.wartosc.toFixed(2)} PLN</div>
|
||
`;
|
||
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?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 = `<div class="restaurant-menu-category">${category.categoryName}</div>
|
||
<div class="rmc-positions">`;
|
||
|
||
category.items.forEach(item => {
|
||
html += `
|
||
<div class="rmc-position" data-position="${item.position}" data-category-id="${item.categoryId}" onclick="openItemModal('${item.categoryId}', '${item.position}')" style="cursor: pointer;">
|
||
<img class="rmc-image" src="${item.image}" alt="" loading="lazy">
|
||
<div class="rmc-title">
|
||
<h4>${item.title}<span>${item.description}</span></h4>
|
||
</div>
|
||
<div class="rmc-other"><span>${item.price}</span></div>
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
html += `</div>`;
|
||
catDiv.innerHTML = html;
|
||
container.appendChild(catDiv);
|
||
});
|
||
} catch (err) {
|
||
console.error('Błąd ładowania menu:', err);
|
||
const container = document.getElementById('menuContainer');
|
||
if (container) {
|
||
container.innerHTML = '<p style="text-align:center; padding: 20px; color: var(--text-muted);">Nie udało się załadować menu.</p>';
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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 ---
|
||
// 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()}<br><br>Po zmianie ustawień <b>odśwież stronę</b>, 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 <b>https://</b>. 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 <b>https://</b> 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();
|
||
} |