Files
karczma-aplikacja-stoliki/public/assets/js/app.js

1263 lines
43 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 params = new URLSearchParams(location.search);
let hashParam = (params.get("h") || "").trim();
// 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
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 = {}) {
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
});
}
function queueGuestAction(messageType, messageText, extra = {}) {
const body = {
tableId: tableParam || null,
qrHash: hashParam || null,
messageType,
messageText,
extra
};
fetch(guestActionQueueEndpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
keepalive: true
}).catch(() => {
// best effort - ignore queue errors in UI
});
}
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 = "";
}
}, remaining);
}
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ń
}
// 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 = function (type) {
if (type === 'order') {
trackEvent("waiter_call_requested", { waiterType: "order" });
queueGuestAction("waiter_call", "Przywołanie kelnera", { waiterType: "order" });
sendApiSimulated("CallWaiter_Order", { table: tableParam });
showToast("Kelner wkrótce do Ciebie podejdzie!");
}
};
window.openWaiterDialog = function () {
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 = function () {
closeWaiterDialog();
callWaiter('order');
};
window.openBillDialog = async function () {
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;
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 ---
window.switchTab = function (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 = '';
// 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");
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);
};
// --- 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 = function (docType) {
billState.doc = docType;
if (docType === 'paragon') {
trackEvent("bill_request_sent", { docType: "paragon" });
const queueMessage = `Prośba o rachunek | forma płatności: ${billState.payment || "nieznana"} | dokument: paragon`;
queueGuestAction("bill_request", queueMessage, {
payment: billState.payment || null,
docType: "paragon"
});
closeBillDialog();
sendApiSimulated("CallWaiter_Bill", { table: tableParam, billId: billState.selectedBillId, payment: billState.payment, doc: 'paragon' });
showToast("Kelner przyniesie paragon do opłacenia!");
} else {
goToStep("stepNIP");
document.getElementById("nipInput").value = '';
setTimeout(() => document.getElementById("nipInput").focus(), 100);
}
};
window.fetchGUS = async function () {
const nip = document.getElementById("nipInput").value.replace(/[\s-]/g, '');
if (nip.length < 10) {
alert("Wprowadź poprawny numer NIP.");
return;
}
const btn = document.getElementById("btnGUS");
btn.textContent = "Szukam...";
btn.disabled = true;
try {
const apiKey = "d8bdc252-e0f1-4863-97a1-826faddbc49c";
const response = await fetch(`https://api.magico.pro/v1/gus/${nip}?api_key=${apiKey}`);
const result = await response.json();
if (result.status && result.data) {
const data = result.data;
let fullStreet = data.street || '';
if (data.propertyNumber) fullStreet += ' ' + data.propertyNumber;
if (data.apartmentNumber) fullStreet += '/' + data.apartmentNumber;
billState.nip = nip;
billState.company = {
name: data.name,
street: fullStreet.trim(),
zip: data.zipCode,
city: data.city,
nip: data.nip
};
document.getElementById("cmpName").value = billState.company.name;
document.getElementById("cmpStreet").value = billState.company.street;
document.getElementById("cmpZip").value = billState.company.zip;
document.getElementById("cmpCity").value = billState.company.city;
document.getElementById("cmpNip").value = "NIP: " + billState.company.nip;
// reset do readonly
document.getElementById("cmpName").readOnly = true;
document.getElementById("cmpStreet").readOnly = true;
document.getElementById("cmpZip").readOnly = true;
document.getElementById("cmpCity").readOnly = true;
document.getElementById("btnEditCompany").textContent = "Popraw ręcznie";
goToStep("stepVerify");
} else {
alert("Nie udało się pobrać danych z GUS dla podanego NIP-u.");
}
} catch (error) {
console.error("Błąd pobierania danych z GUS:", error);
alert("Błąd połączenia z API GUS.");
} finally {
btn.textContent = "Pobierz z GUS";
btn.disabled = false;
}
};
// --- DYNAMIC MENU LOADING ---
async function loadMenu() {
try {
const response = await fetch('menu.json');
if (!response.ok) throw new Error('Nie udało się załadować menu');
const menuData = await response.json();
window.menuDataRaw = menuData;
const container = document.getElementById('menuContainer');
if (!container) return;
container.innerHTML = '';
menuData.forEach(category => {
const catId = category.items.length > 0 ? category.items[0].categoryId : '';
const catDiv = document.createElement('div');
catDiv.className = 'rm-category';
catDiv.setAttribute('data-cat-id', catId);
let html = `<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 = 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;
closeBillDialog();
trackEvent("bill_request_sent", { docType: "faktura" });
const queueMessage = `Prośba o rachunek | forma płatności: ${billState.payment || "nieznana"} | dokument: faktura | NIP: ${billState.nip || "-"} | firma: ${billState.company?.name || "-"}`;
queueGuestAction("bill_request", queueMessage, {
payment: billState.payment || null,
docType: "faktura",
nip: billState.nip || null,
company: billState.company || null
});
sendApiSimulated("CallWaiter_Bill", {
table: tableParam,
billId: billState.selectedBillId,
payment: billState.payment,
doc: 'faktura',
nip: billState.nip,
company: billState.company
});
showToast("Dziękujemy! Prośba o fakturę została wysłana.");
};
// --- GEOLOCATION LOGIC ---
const RESTAURANT_LAT = 50.5624963;
const RESTAURANT_LNG = 22.0608059;
const MAX_DISTANCE_METERS = 200;
function haversineDistance(lat1, lon1, lat2, lon2) {
const R = 6371e3;
const p1 = lat1 * Math.PI / 180;
const p2 = lat2 * Math.PI / 180;
const dp = (lat2 - lat1) * Math.PI / 180;
const dl = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dp / 2) * Math.sin(dp / 2) +
Math.cos(p1) * Math.cos(p2) *
Math.sin(dl / 2) * Math.sin(dl / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
function startApp() {
document.getElementById("geoScreen").classList.add("hidden");
document.getElementById("loadingScreen").classList.remove("hidden");
trackEvent("session_start", { flow: "start_app" });
initUserProfile();
fetchOrders();
if (!window.ordersInterval) {
window.ordersInterval = setInterval(fetchOrders, 10000);
}
// Fallback: If no data after 25s, show empty state anyway
setTimeout(() => {
if (!document.getElementById("loadingScreen").classList.contains("hidden")) {
updateUI([]);
}
}, 25000);
}
function showGeoConsentScreen() {
const geoScreen = document.getElementById("geoScreen");
const loadingScreen = document.getElementById("loadingScreen");
const geoMsg = document.getElementById("geoMsg");
const geoActionBtn = document.getElementById("geoActionBtn");
loadingScreen.classList.add("hidden");
geoScreen.classList.remove("hidden");
if (geoMsg) {
geoMsg.innerHTML = `Aby zapewnić bezpieczeństwo Twojego zamówienia, musimy upewnić się, że znajdujesz się na terenie restauracji.<br><br>Prosimy o udzielenie zgody na dostęp do lokalizacji w przeglądarce.`;
}
if (geoActionBtn) {
geoActionBtn.disabled = false;
geoActionBtn.textContent = "Udziel zgody / Sprawdź";
}
}
function isIOSDevice() {
return /iPad|iPhone|iPod/.test(navigator.userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
}
function shouldBypassGeolocationHost() {
const bypassHosts = ['82.160.190.247'];
return bypassHosts.includes(window.location.hostname);
}
window.initGeolocation = function () {
if (shouldBypassGeolocationHost()) {
console.warn("Bypassing geolocation for trusted host.");
trackEvent("geo_bypass_host", { host: window.location.hostname, reason: "trusted_host" });
startApp();
return;
}
const geoScreen = document.getElementById("geoScreen");
const loadingScreen = document.getElementById("loadingScreen");
const geoMsg = document.getElementById("geoMsg");
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.");
trackEvent("geo_bypass_host", { host: window.location.hostname, reason: "local_http" });
startApp();
return;
}
if (!window.isSecureContext) {
geoMsg.innerHTML = `<b style="color: #ff6b6b;">Ta strona nie jest uruchomiona w bezpiecznym trybie HTTPS.</b><br><br>
Przeglądarki mobilne blokują geolokalizację bez pytania, jeśli adres nie zaczyna się od <b>https://</b>.<br><br>
Otwórz aplikację przez HTTPS i spróbuj ponownie.<br><br>
<b>Masz problem z lokalizacją?</b> Połącz się z <b>HotSpot Karczmy</b>, a wtedy wejdziesz do aplikacji bez geolokalizacji.<br>
Hasło: <b>karczmabiesiada</b>`;
if (geoActionBtn) {
geoActionBtn.disabled = false;
geoActionBtn.textContent = "Spróbuj ponownie";
}
return;
}
loadingScreen.classList.add("hidden");
geoScreen.classList.remove("hidden");
if (!navigator.geolocation) {
geoMsg.innerHTML = "Twoja przeglądarka nie wspiera geolokalizacji. Aplikacja wymaga nowszej przeglądarki.";
return;
}
geoMsg.innerHTML = "Sprawdzamy Twoją lokalizację...";
trackEvent("geo_check_started");
if (geoActionBtn) {
geoActionBtn.disabled = true;
geoActionBtn.textContent = "Sprawdzanie...";
}
navigator.geolocation.getCurrentPosition(
(position) => {
const dist = haversineDistance(
RESTAURANT_LAT, RESTAURANT_LNG,
position.coords.latitude, position.coords.longitude
);
const accuracy = position.coords.accuracy;
console.log(`[GEO] Lat: ${position.coords.latitude}, Lng: ${position.coords.longitude}, Dist: ${dist}m, Accuracy: ${accuracy}m`);
if (dist <= MAX_DISTANCE_METERS) {
trackEvent("geo_check_passed", { distanceMeters: Math.round(dist), accuracyMeters: Math.round(accuracy) });
startApp();
// setTimeout(() => showToast(`Lokalizacja zweryfikowana (Dystans: ${Math.round(dist)}m, Dokładność: ${Math.round(accuracy)}m)`), 2000);
} else {
trackEvent("geo_check_failed", { reason: "outside_restaurant", distanceMeters: Math.round(dist), accuracyMeters: Math.round(accuracy) });
if (geoActionBtn) {
geoActionBtn.disabled = false;
geoActionBtn.textContent = "Spróbuj ponownie";
}
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ę.`;
}
},
(error) => {
trackEvent("geo_check_failed", { reason: "browser_error", code: error.code || null, message: String(error.message || "") });
if (geoActionBtn) {
geoActionBtn.disabled = false;
geoActionBtn.textContent = "Spróbuj ponownie";
}
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>
Geolokalizacja działa tylko na bezpiecznym adresie <b>https://</b> (lub localhost).<br>
Otwórz aplikację przez HTTPS i spróbuj ponownie.<br><br>
<b>Masz problem z lokalizacją?</b> Połącz się z <b>HotSpot Karczmy</b>, a wtedy wejdziesz do aplikacji bez geolokalizacji.<br>
Hasło: <b>karczmabiesiada</b>`;
} else if (error.code === error.PERMISSION_DENIED) {
const isIOS = isIOSDevice();
let instructions = '';
if (isIOS) {
instructions = `<b>Instrukcja dla iPhone (Safari):</b><br>
1. Kliknij ikonę <b>"aA"</b> po lewej stronie paska adresu.<br>
2. Wybierz <b>"Ustawienia witryny"</b> (Website Settings).<br>
3. Zmień opcję <b>"Położenie"</b> (Location) na "Zapytaj" lub "Pozwalaj".<br>
4. Odśwież stronę.<br><br>
<i>Uwaga: Na urządzeniach Apple lokalizacja działa WYŁĄCZNIE, gdy adres strony zaczyna się od bezpiecznego <b>https://</b>. Jeżeli jesteś na http://, system zablokuje to automatycznie.</i><br><br>
<b>Masz problem z lokalizacją?</b> Połącz się z <b>HotSpot Karczmy</b>, a wtedy wejdziesz do aplikacji bez geolokalizacji.<br>
Hasło: <b>karczmabiesiada</b>`;
} else {
instructions = `<b>Instrukcja dla Android / Chrome:</b><br>
1. Kliknij ikonkę <b>kłódki / ustawień</b> 🔒 obok adresu strony na górze przeglądarki.<br>
2. Znajdź <b>Uprawnienia</b> (Lokalizacja) i zmień z "Zablokuj" na "Zezwalaj".<br>
3. Odśwież stronę.<br><br>
<b>Masz problem z lokalizacją?</b> Połącz się z <b>HotSpot Karczmy</b>, a wtedy wejdziesz do aplikacji bez geolokalizacji.<br>
Hasło: <b>karczmabiesiada</b>`;
}
geoMsg.innerHTML = `<b style="color: #ff6b6b;">Przeglądarka zablokowała dostęp do lokalizacji.</b><br><br>
Bez tego nie możemy zweryfikować, czy jesteś w restauracji.<br><br>
${instructions}`;
} else {
geoMsg.innerHTML = "Nie udało się pobrać lokalizacji. Sprawdź zasięg lub włącz GPS i spróbuj ponownie.";
}
},
{ enableHighAccuracy: true, timeout: 15000, maximumAge: 0 }
);
};
if (shouldBypassGeolocationHost()) {
startApp();
} else if (isIOSDevice()) {
showGeoConsentScreen();
} else {
initGeolocation();
}