import { endpoints, geoBypassHosts } from "./config.js"; import { trackEvent } from "./analytics.js"; import { runPendingProtectedAction, setGeoGateHandler, showBottomNav, switchTabInternal, updateNavAccessState, } from "./access.js"; import { prefetchOpenBills, refreshGuestPendingActions, startGuestPendingPoll, } from "./queue.js"; import { fetchOrders, resolveTableLabel, updateUI } from "./orders.js"; import { getAppAccessLevel, setAppAccessLevel, setPendingProtectedAction, } from "./state.js"; // USER PROFILE LOGIC const userProfileKey = "karczma_user_profile"; const USER_PROFILE_EXPIRE_MS = 180 * 24 * 60 * 60 * 1000; // ~6 months export 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 = ""; } } export function saveUserName() { 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); } } export function declineUserName() { 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"; } } function isIOSDevice() { return ( /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1) ); } const GEO_GATE_LABELS = { status: "status zamówienia", waiter: "wezwanie kelnera", bill: "prośbę o rachunek", }; const GEO_DEFAULT_LEAD = "Przeglądaj menu od razu — albo potwierdź, że jesteś u nas, aby wezwać kelnera, śledzić zamówienie i poprosić o rachunek."; function setGeoLead(html) { const el = document.getElementById("geoLead"); if (el) el.innerHTML = html; } function setGeoStatus(html, { error = false, info = false } = {}) { const el = document.getElementById("geoMsg"); if (!el) return; el.innerHTML = html || ""; el.classList.toggle("is-error", error); el.classList.toggle("is-info", info); } function showGeoInstructions(html) { const el = document.getElementById("geoInstructions"); if (!el) return; el.innerHTML = html || ""; el.classList.toggle("hidden", !html); } function hideGeoInstructions() { showGeoInstructions(""); } function setGeoActionBusy(busy) { const btn = document.getElementById("geoActionBtn"); if (!btn) return; btn.disabled = false; btn.setAttribute("aria-busy", busy ? "true" : "false"); if (busy) { setGeoActionLabel("Sprawdzanie…"); } } function isGeoPermissionDenied(error) { return Number(error?.code) === 1; } function setGeoActionLabel(text) { const btn = document.getElementById("geoActionBtn"); if (!btn) return; const main = btn.querySelector(".geo-btn-main"); if (main) main.textContent = text; else btn.textContent = text; } function getGeoPermissionInstructions() { if (isIOSDevice()) { return `iPhone (Safari):
1. Kliknij aA po lewej stronie paska adresu.
2. Wybierz Ustawienia witryny.
3. Ustaw Położenie na „Zapytaj” lub „Pozwalaj”.
4. Odśwież stronę.

Lokalizacja działa tylko przez bezpieczne https://.`; } return `Android / Chrome:
1. Kliknij ikonę kłódki obok adresu strony.
2. W Uprawnieniach zmień Lokalizację na „Zezwalaj”.
3. Odśwież stronę.`; } let geoMenuButtonMode = "menu_only"; export function handleGeoMenuClick() { if (geoMenuButtonMode === "back_to_menu") { document.getElementById("geoScreen")?.classList.add("hidden"); return; } enterMenuOnlyMode(); } export function retryGeolocation() { if (shouldBypassGeolocationHost()) { bypassGeolocation("trusted_host", { host: window.location.hostname }); return; } initGeolocationAfterBypassChecks({ userInitiated: true }).catch((err) => { console.error("[GEO] retry failed", err); showGeoPermissionBlockedState(); }); } export 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 = ""; } } export function showGeoGateForAction(action) { const geoScreen = document.getElementById("geoScreen"); const loadingScreen = document.getElementById("loadingScreen"); const geoActionBtn = document.getElementById("geoActionBtn"); if (loadingScreen) loadingScreen.classList.add("hidden"); if (geoScreen) geoScreen.classList.remove("hidden"); const feature = GEO_GATE_LABELS[action] || "tę funkcję"; setGeoLead(`Menu masz już otwarte. Aby skorzystać z ${feature}, potwierdź krótko, że jesteś w restauracji.`); setGeoStatus(""); hideGeoInstructions(); if (geoActionBtn) { setGeoActionBusy(false); setGeoActionLabel("Sprawdź lokalizację"); } configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only"); } setGeoGateHandler(showGeoGateForAction); export function unlockFullApp() { setAppAccessLevel("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(); } export function enterMenuOnlyMode() { trackEvent("menu_only_entered"); setAppAccessLevel("menu"); setPendingProtectedAction(null); document.getElementById("geoScreen")?.classList.add("hidden"); document.getElementById("loadingScreen")?.classList.add("hidden"); showBottomNav(); resolveTableLabel(); switchTabInternal("menu"); } // --- 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); }); } export function startApp() { setAppAccessLevel("full"); updateNavAccessState(); document.getElementById("geoScreen").classList.add("hidden"); document.getElementById("loadingScreen").classList.remove("hidden"); trackEvent("session_start", { flow: "start_app" }); initUserProfile(); fetchOrders(); prefetchOpenBills(); refreshGuestPendingActions(); startGuestPendingPoll(); if (!window.ordersInterval) { window.ordersInterval = setInterval(fetchOrders, 10000); } // Fallback: If no data after 25s, show empty state anyway setTimeout(() => { if (!document.getElementById("loadingScreen").classList.contains("hidden")) { updateUI([]); } }, 25000); } function showGeoConsentScreen() { const geoScreen = document.getElementById("geoScreen"); const loadingScreen = document.getElementById("loadingScreen"); const geoActionBtn = document.getElementById("geoActionBtn"); loadingScreen.classList.add("hidden"); geoScreen.classList.remove("hidden"); setGeoLead(GEO_DEFAULT_LEAD); setGeoStatus(""); hideGeoInstructions(); if (geoActionBtn) { setGeoActionBusy(false); setGeoActionLabel("Zgoda, sprawdź lokalizację"); } configureGeoSecondaryButton("menu_only"); } function showGeoPermissionBlockedState() { setGeoStatus("Przeglądarka zablokowała dostęp do lokalizacji.", { error: true }); showGeoInstructions( `${getGeoPermissionInstructions()}

Po zmianie ustawień odśwież stronę, a potem kliknij „Spróbuj ponownie”.` ); setGeoActionBusy(false); setGeoActionLabel("Spróbuj ponownie"); configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only"); } export function shouldBypassGeolocationHost() { return geoBypassHosts.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(endpoints.geoBypass, { 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 (getAppAccessLevel() === "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"; } } export async function bootstrapGeolocation() { if (shouldBypassGeolocationHost()) { bypassGeolocation("trusted_host", { host: window.location.hostname }); return; } if (await checkGeoBypassByClientIp()) { bypassGeolocation("trusted_ip"); return; } showGeoConsentScreen(); } export function initGeolocation() { 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(); }); } export async function initGeolocationAfterBypassChecks(options = {}) { const geoScreen = document.getElementById("geoScreen"); const loadingScreen = document.getElementById("loadingScreen"); const localBypassHosts = ["localhost", "127.0.0.1", "192.168.20.84"]; if (window.location.protocol === "http:" && localBypassHosts.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(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only"); if (!window.isSecureContext) { setGeoLead(GEO_DEFAULT_LEAD); setGeoStatus("Ta strona wymaga bezpiecznego połączenia HTTPS.", { error: true }); showGeoInstructions( "Przeglądarki mobilne blokują geolokalizację bez https://. Otwórz aplikację przez HTTPS i spróbuj ponownie." ); setGeoActionBusy(false); setGeoActionLabel("Spróbuj ponownie"); return; } if (!navigator.geolocation) { setGeoLead(GEO_DEFAULT_LEAD); setGeoStatus("Twoja przeglądarka nie wspiera geolokalizacji.", { error: true }); hideGeoInstructions(); setGeoActionBusy(false); return; } setGeoLead(GEO_DEFAULT_LEAD); setGeoStatus("Sprawdzamy Twoją lokalizację…", { info: true }); hideGeoInstructions(); setGeoActionBusy(true); const permissionState = await queryGeolocationPermissionState(); if (permissionState === "denied") { showGeoPermissionBlockedState(); return; } trackEvent("geo_check_started"); try { const result = await requestRestaurantGeolocation(); const dist = result.dist; const accuracy = result.accuracy; console.log( `[GEO] Lat: ${result.position.coords.latitude}, Lng: ${result.position.coords.longitude}, ` + `MinDist: ${Math.round(dist)}m, Accuracy: ${Math.round(accuracy)}m` ); if (result.passed) { trackEvent("geo_check_passed", { distanceMeters: Math.round(dist), accuracyMeters: Math.round(accuracy), }); unlockFullApp(); return; } trackEvent("geo_check_failed", { reason: "outside_restaurant", distanceMeters: Math.round(dist), accuracyMeters: Math.round(accuracy), }); setGeoActionBusy(false); setGeoActionLabel("Spróbuj ponownie"); configureGeoSecondaryButton(getAppAccessLevel() === "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(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only"); const deniedBecauseInsecure = /secure origins|only secure|https/i.test(String(error.message || "")); if (deniedBecauseInsecure) { setGeoStatus("Geolokalizacja wymaga HTTPS.", { error: true }); showGeoInstructions("Otwórz aplikację przez bezpieczny adres https:// i spróbuj ponownie."); } else if (isGeoPermissionDenied(error)) { showGeoPermissionBlockedState(); } else { setGeoStatus("Nie udało się pobrać lokalizacji.", { error: true }); showGeoInstructions("Sprawdź zasięg, włącz GPS i spróbuj ponownie."); } } } export function startGeoBootstrap() { bindGeoScreenButtons(); if (shouldBypassGeolocationHost()) { bypassGeolocation("trusted_host", { host: window.location.hostname }); } else { bootstrapGeolocation(); } }