Przebudowa działania geolokalizacji. Zgoda na menu bez lokalizacji.

This commit is contained in:
2026-05-31 00:00:39 +02:00
parent 8de221ba79
commit 04aaa6e321
7 changed files with 517 additions and 28 deletions

View File

@@ -881,6 +881,44 @@ header {
font-weight: 500;
}
.nav-item.nav-locked {
opacity: 0.48;
filter: grayscale(0.35);
}
.nav-item.nav-locked .nav-label::after {
content: " 🔒";
font-size: 10px;
}
.menu-only-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 12px;
padding: 12px 14px;
border-radius: 12px;
background: rgba(226, 176, 126, 0.12);
border: 1px solid rgba(226, 176, 126, 0.28);
font-size: 13px;
color: var(--text-muted);
line-height: 1.45;
}
.menu-only-banner-btn {
flex-shrink: 0;
border: 1px solid rgba(226, 176, 126, 0.45);
background: rgba(226, 176, 126, 0.15);
color: var(--primary);
border-radius: 999px;
padding: 8px 14px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.nav-item:active .nav-icon {
transform: scale(0.9);
}

View File

@@ -9,6 +9,7 @@ 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) {
@@ -22,6 +23,8 @@ if (!hashParam) {
}
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";
@@ -69,6 +72,10 @@ function deriveZoneFromTable(tableValue) {
}
function trackEvent(eventName, payload = {}) {
if (isStaffPreview) {
return;
}
const body = {
eventName,
sessionId: analyticsSessionId,
@@ -433,9 +440,155 @@ function hideLoader() {
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 <b>${feature}</b>, potwierdź, że jesteś w restauracji.<br><br>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();
@@ -835,7 +988,13 @@ window.callWaiter = async function (type) {
showToast("Kelner wkrótce do Ciebie podejdzie!");
};
window.openWaiterDialog = async function () {
window.openWaiterDialog = function () {
requireFullAccess("waiter", () => {
openWaiterDialogInternal();
});
};
async function openWaiterDialogInternal() {
if (!(await ensureGuestActionAllowed("waiter_call"))) {
return;
}
@@ -860,7 +1019,13 @@ window.proceedToBillPayment = async function () {
goToStep("stepPayment");
};
window.openBillDialog = async function () {
window.openBillDialog = function () {
requireFullAccess("bill", () => {
openBillDialogInternal();
});
};
async function openBillDialogInternal() {
await refreshGuestPendingActions();
trackEvent("bill_dialog_opened");
billState = { payment: '', doc: '', nip: '', company: null, selectedBillId: null };
@@ -964,7 +1129,7 @@ window.goToStep = function (stepId) {
};
// --- SPA NAVIGATION LOGIC ---
window.switchTab = function (tabName) {
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'));
@@ -983,11 +1148,10 @@ window.switchTab = function (tabName) {
view.classList.add('active');
document.getElementById('navStatus').classList.add('active');
if (header) header.style.display = '';
// Show greeting banner only if it was rendered initially or has content
if (greetingBanner && greetingBanner.innerHTML.trim() !== '') greetingBanner.style.display = '';
}
else if (tabName === 'menu') {
trackEvent("view_menu");
trackEvent("view_menu", { flow: appAccessLevel === "menu" ? "menu_only" : "full_app" });
const view = document.getElementById('menuView');
view.classList.remove('hidden');
view.classList.add('active');
@@ -996,6 +1160,16 @@ window.switchTab = function (tabName) {
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 ---
@@ -1312,6 +1486,8 @@ function haversineDistance(lat1, lon1, lat2, lon2) {
}
function startApp() {
appAccessLevel = "full";
updateNavAccessState();
document.getElementById("geoScreen").classList.add("hidden");
document.getElementById("loadingScreen").classList.remove("hidden");
trackEvent("session_start", { flow: "start_app" });
@@ -1338,6 +1514,7 @@ function showGeoConsentScreen() {
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");
@@ -1350,6 +1527,9 @@ function showGeoConsentScreen() {
geoActionBtn.disabled = false;
geoActionBtn.textContent = "Udziel zgody / Sprawdź";
}
if (menuOnlyBtn) {
configureGeoSecondaryButton("menu_only");
}
}
function isIOSDevice() {
@@ -1365,7 +1545,7 @@ window.initGeolocation = function () {
if (shouldBypassGeolocationHost()) {
console.warn("Bypassing geolocation for trusted host.");
trackEvent("geo_bypass_host", { host: window.location.hostname, reason: "trusted_host" });
startApp();
unlockFullApp();
return;
}
@@ -1373,12 +1553,13 @@ window.initGeolocation = function () {
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" });
startApp();
unlockFullApp();
return;
}
@@ -1392,11 +1573,13 @@ window.initGeolocation = function () {
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.";
@@ -1422,7 +1605,7 @@ window.initGeolocation = function () {
if (dist <= MAX_DISTANCE_METERS) {
trackEvent("geo_check_passed", { distanceMeters: Math.round(dist), accuracyMeters: Math.round(accuracy) });
startApp();
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) });
@@ -1430,6 +1613,7 @@ window.initGeolocation = function () {
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).<br>Nasza aplikacja działa tylko na miejscu.<br><br>
<small style="color: #888;">Debug: Twoja odległość: ${Math.round(dist)}m, Dokładność sygnału: ${Math.round(accuracy)}m</small><br><br>
Jeśli to błąd GPS lub słaby sygnał, spróbuj ponownie za chwilę.`;
@@ -1441,6 +1625,7 @@ window.initGeolocation = function () {
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 = `<b style="color: #ff6b6b;">Przeglądarka zablokowała lokalizację z powodu braku HTTPS.</b><br><br>