Poprawka botto bar i wersje językowe
This commit is contained in:
@@ -34,7 +34,7 @@ export function updateNavAccessState() {
|
||||
|
||||
export function showBottomNav() {
|
||||
const bottomNav = document.getElementById("bottomNav");
|
||||
if (bottomNav) bottomNav.style.display = "";
|
||||
if (bottomNav) bottomNav.hidden = false;
|
||||
updateNavAccessState();
|
||||
}
|
||||
|
||||
@@ -93,6 +93,8 @@ export function switchTabInternal(tabName) {
|
||||
if (greetingBanner) greetingBanner.style.display = "none";
|
||||
}
|
||||
window.scrollTo(0, 0);
|
||||
const appScroll = document.getElementById("appScroll");
|
||||
if (appScroll) appScroll.scrollTop = 0;
|
||||
}
|
||||
|
||||
export function switchTab(tabName) {
|
||||
|
||||
@@ -15,16 +15,16 @@ import { showToast } from "./toast.js";
|
||||
import {
|
||||
getBillState,
|
||||
getHashParam,
|
||||
getTableParam,
|
||||
setBillState,
|
||||
} from "./state.js";
|
||||
|
||||
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 } }));
|
||||
// }
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
export async function callWaiter(type) {
|
||||
@@ -48,7 +48,6 @@ export async function callWaiter(type) {
|
||||
}
|
||||
|
||||
trackEvent("waiter_call_requested", { waiterType: "order" });
|
||||
sendApiSimulated("CallWaiter_Order", { table: getTableParam() });
|
||||
showToast(t("waiter.toast_ok"));
|
||||
}
|
||||
|
||||
@@ -142,10 +141,10 @@ function renderBillList(bills) {
|
||||
const numerFormat = b.numer ? `#${b.numer}` : t("bill.bill_fallback");
|
||||
div.innerHTML = `
|
||||
<div>
|
||||
<div style="font-weight:bold;">${numerFormat}</div>
|
||||
<div style="font-size:12px; color:var(--text-muted);">${b.opis}</div>
|
||||
<div style="font-weight:bold;">${escapeHtml(numerFormat)}</div>
|
||||
<div style="font-size:12px; color:var(--text-muted);">${escapeHtml(b.opis)}</div>
|
||||
</div>
|
||||
<div style="font-weight:bold; color:var(--primary);">${b.suma.toFixed(2)} PLN</div>
|
||||
<div style="font-weight:bold; color:var(--primary);">${Number(b.suma).toFixed(2)} PLN</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
});
|
||||
@@ -172,10 +171,10 @@ export function showBillReview(bill) {
|
||||
|
||||
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 style="font-weight:600; font-size: 14px;">${escapeHtml(p.nazwa)}</div>
|
||||
<div style="font-size:12px; color:var(--text-muted);">${escapeHtml(p.ilosc)} x ${Number(p.cena).toFixed(2)} PLN</div>
|
||||
</div>
|
||||
<div style="font-weight:600;">${p.wartosc.toFixed(2)} PLN</div>
|
||||
<div style="font-weight:600;">${Number(p.wartosc).toFixed(2)} PLN</div>
|
||||
`;
|
||||
content.appendChild(div);
|
||||
});
|
||||
@@ -220,12 +219,6 @@ export async function selectDocument(docType) {
|
||||
}
|
||||
trackEvent("bill_request_sent", { docType: "paragon" });
|
||||
closeBillDialog();
|
||||
sendApiSimulated("CallWaiter_Bill", {
|
||||
table: getTableParam(),
|
||||
billId: billState.selectedBillId,
|
||||
payment: billState.payment,
|
||||
doc: "paragon",
|
||||
});
|
||||
showToast(t("bill.toast_receipt"));
|
||||
} else {
|
||||
goToStep("stepNIP");
|
||||
@@ -343,13 +336,5 @@ export async function confirmInvoice() {
|
||||
|
||||
closeBillDialog();
|
||||
trackEvent("bill_request_sent", { docType: "faktura" });
|
||||
sendApiSimulated("CallWaiter_Bill", {
|
||||
table: getTableParam(),
|
||||
billId: billState.selectedBillId,
|
||||
payment: billState.payment,
|
||||
doc: "faktura",
|
||||
nip: billState.nip,
|
||||
company: billState.company,
|
||||
});
|
||||
showToast(t("bill.toast_invoice"));
|
||||
}
|
||||
|
||||
@@ -14,12 +14,4 @@ export const endpoints = {
|
||||
menu: cfg.endpoints?.menu || "../api/menu.php",
|
||||
};
|
||||
|
||||
export const availableLanguages = Array.isArray(cfg.languages)
|
||||
? cfg.languages
|
||||
: [
|
||||
{ code: "pl", label: "Polski", flag: "🇵🇱" },
|
||||
{ code: "en", label: "English", flag: "🇬🇧" },
|
||||
{ code: "de", label: "Deutsch", flag: "🇩🇪" },
|
||||
];
|
||||
|
||||
export const loaderMinMs = Number(cfg.loaderMinMs) || 10_000;
|
||||
|
||||
+195
-183
@@ -1,5 +1,5 @@
|
||||
import { endpoints, geoBypassHosts } from "./config.js";
|
||||
import { t } from "./i18n.js";
|
||||
import { onLangChange, t } from "./i18n.js";
|
||||
import { trackEvent } from "./analytics.js";
|
||||
import {
|
||||
runPendingProtectedAction,
|
||||
@@ -20,95 +20,20 @@ import {
|
||||
setPendingProtectedAction,
|
||||
} from "./state.js";
|
||||
|
||||
// USER PROFILE LOGIC
|
||||
const userProfileKey = "karczma_user_profile";
|
||||
const USER_PROFILE_EXPIRE_MS = 180 * 24 * 60 * 60 * 1000; // ~6 months
|
||||
/** @typedef {'consent'|'gate'|'blocked'|'checking'|'outside'|'https'|'unsupported'|'failed'} GeoUiMode */
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
const geoUi = {
|
||||
/** @type {GeoUiMode} */
|
||||
mode: "consent",
|
||||
pendingAction: null,
|
||||
outsideDist: 0,
|
||||
outsideAccuracy: 0,
|
||||
actionBusy: false,
|
||||
/** @type {'locate'|'check'|'retry'|'checking'} */
|
||||
actionLabel: "locate",
|
||||
/** @type {'menu_only'|'back_to_menu'} */
|
||||
secondaryMode: "menu_only",
|
||||
};
|
||||
|
||||
function GEO_GATE_LABELS() {
|
||||
return {
|
||||
@@ -118,8 +43,6 @@ function GEO_GATE_LABELS() {
|
||||
};
|
||||
}
|
||||
|
||||
const GEO_DEFAULT_LEAD = () => t("geo.lead");
|
||||
|
||||
function setGeoLead(html) {
|
||||
const el = document.getElementById("geoLead");
|
||||
if (el) el.innerHTML = html;
|
||||
@@ -144,20 +67,6 @@ 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(t("geo.btn.checking"));
|
||||
}
|
||||
}
|
||||
|
||||
function isGeoPermissionDenied(error) {
|
||||
return Number(error?.code) === 1;
|
||||
}
|
||||
|
||||
function setGeoActionLabel(text) {
|
||||
const btn = document.getElementById("geoActionBtn");
|
||||
if (!btn) return;
|
||||
@@ -166,14 +75,145 @@ function setGeoActionLabel(text) {
|
||||
else btn.textContent = text;
|
||||
}
|
||||
|
||||
function getGeoPermissionInstructions() {
|
||||
return t("geo.hint.permission");
|
||||
function applyGeoActionLabel() {
|
||||
const keys = {
|
||||
locate: "geo.btn.locate",
|
||||
check: "geo.btn.check",
|
||||
retry: "geo.btn.retry",
|
||||
checking: "geo.btn.checking",
|
||||
};
|
||||
setGeoActionLabel(t(keys[geoUi.actionLabel] || keys.locate));
|
||||
}
|
||||
|
||||
let geoMenuButtonMode = "menu_only";
|
||||
function setGeoActionBusy(busy) {
|
||||
const btn = document.getElementById("geoActionBtn");
|
||||
if (!btn) return;
|
||||
btn.disabled = false;
|
||||
btn.setAttribute("aria-busy", busy ? "true" : "false");
|
||||
geoUi.actionBusy = !!busy;
|
||||
if (busy) {
|
||||
geoUi.actionLabel = "checking";
|
||||
}
|
||||
applyGeoActionLabel();
|
||||
}
|
||||
|
||||
function applyGeoSecondaryButton() {
|
||||
const menuOnlyBtn = document.getElementById("geoMenuOnlyBtn");
|
||||
if (!menuOnlyBtn) return;
|
||||
|
||||
const mainEl = menuOnlyBtn.querySelector(".geo-btn-main");
|
||||
const subEl = menuOnlyBtn.querySelector(".geo-btn-sub");
|
||||
menuOnlyBtn.style.display = "";
|
||||
|
||||
if (geoUi.secondaryMode === "back_to_menu") {
|
||||
if (mainEl) mainEl.textContent = t("geo.btn.back_menu");
|
||||
if (subEl) {
|
||||
subEl.textContent = "";
|
||||
subEl.style.display = "none";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (mainEl) mainEl.textContent = t("geo.btn.menu_main");
|
||||
if (subEl) {
|
||||
subEl.textContent = t("geo.btn.menu_sub");
|
||||
subEl.style.display = "";
|
||||
}
|
||||
}
|
||||
|
||||
function refreshGeoCopy() {
|
||||
const secondary =
|
||||
getAppAccessLevel() === "menu" && geoUi.secondaryMode === "back_to_menu"
|
||||
? "back_to_menu"
|
||||
: geoUi.secondaryMode;
|
||||
|
||||
geoUi.secondaryMode = secondary;
|
||||
|
||||
switch (geoUi.mode) {
|
||||
case "gate": {
|
||||
const feature = GEO_GATE_LABELS()[geoUi.pendingAction] || t("geo.feature.other");
|
||||
setGeoLead(t("geo.lead_action", { feature }));
|
||||
break;
|
||||
}
|
||||
case "blocked":
|
||||
setGeoLead(t("geo.lead"));
|
||||
setGeoStatus(t("geo.status.blocked"), { error: true });
|
||||
showGeoInstructions(t("geo.hint.permission"));
|
||||
break;
|
||||
case "checking":
|
||||
setGeoLead(t("geo.lead"));
|
||||
setGeoStatus(t("geo.status.checking"), { info: true });
|
||||
hideGeoInstructions();
|
||||
break;
|
||||
case "outside":
|
||||
setGeoLead(t("geo.lead"));
|
||||
setGeoStatus(
|
||||
t("geo.status.outside", {
|
||||
dist: geoUi.outsideDist,
|
||||
accuracy: geoUi.outsideAccuracy,
|
||||
}),
|
||||
{ error: true }
|
||||
);
|
||||
showGeoInstructions(t("geo.hint.outside"));
|
||||
break;
|
||||
case "https":
|
||||
setGeoLead(t("geo.lead"));
|
||||
setGeoStatus(t("geo.status.https"), { error: true });
|
||||
showGeoInstructions(t("geo.hint.https"));
|
||||
break;
|
||||
case "unsupported":
|
||||
setGeoLead(t("geo.lead"));
|
||||
setGeoStatus(t("geo.status.unsupported"), { error: true });
|
||||
hideGeoInstructions();
|
||||
break;
|
||||
case "failed":
|
||||
setGeoLead(t("geo.lead"));
|
||||
setGeoStatus(t("geo.status.failed"), { error: true });
|
||||
showGeoInstructions(t("geo.hint.retry"));
|
||||
break;
|
||||
case "consent":
|
||||
default:
|
||||
setGeoLead(t("geo.lead"));
|
||||
break;
|
||||
}
|
||||
|
||||
applyGeoActionLabel();
|
||||
applyGeoSecondaryButton();
|
||||
}
|
||||
|
||||
onLangChange(() => {
|
||||
refreshGeoCopy();
|
||||
});
|
||||
|
||||
/* --- Disabled user profile (name modal) — kept for possible re-enable --- */
|
||||
const userProfileKey = "karczma_user_profile";
|
||||
const USER_PROFILE_EXPIRE_MS = 180 * 24 * 60 * 60 * 1000;
|
||||
|
||||
export function initUserProfile() {
|
||||
return;
|
||||
}
|
||||
|
||||
export function saveUserName() {
|
||||
const input = document.getElementById("userNameInput");
|
||||
if (!input) return;
|
||||
const name = input.value.trim();
|
||||
if (!name) return;
|
||||
const now = Date.now();
|
||||
localStorage.setItem(
|
||||
userProfileKey,
|
||||
JSON.stringify({ name, firstVisit: now, lastVisit: now, declined: false })
|
||||
);
|
||||
}
|
||||
|
||||
export function declineUserName() {
|
||||
localStorage.setItem(
|
||||
userProfileKey,
|
||||
JSON.stringify({ name: null, firstVisit: Date.now(), lastVisit: Date.now(), declined: true })
|
||||
);
|
||||
}
|
||||
|
||||
export function handleGeoMenuClick() {
|
||||
if (geoMenuButtonMode === "back_to_menu") {
|
||||
if (geoUi.secondaryMode === "back_to_menu") {
|
||||
document.getElementById("geoScreen")?.classList.add("hidden");
|
||||
return;
|
||||
}
|
||||
@@ -204,49 +244,22 @@ export function bindGeoScreenButtons() {
|
||||
});
|
||||
}
|
||||
|
||||
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 = t("geo.btn.back_menu");
|
||||
if (subEl) {
|
||||
subEl.textContent = "";
|
||||
subEl.style.display = "none";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
menuOnlyBtn.style.display = "";
|
||||
if (mainEl) mainEl.textContent = t("geo.btn.menu_main");
|
||||
if (subEl) {
|
||||
subEl.textContent = t("geo.btn.menu_sub");
|
||||
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("geo.feature.other");
|
||||
setGeoLead(t("geo.lead_action", { feature }));
|
||||
geoUi.mode = "gate";
|
||||
geoUi.pendingAction = action;
|
||||
geoUi.actionBusy = false;
|
||||
geoUi.actionLabel = "check";
|
||||
geoUi.secondaryMode = getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only";
|
||||
setGeoStatus("");
|
||||
hideGeoInstructions();
|
||||
if (geoActionBtn) {
|
||||
setGeoActionBusy(false);
|
||||
setGeoActionLabel(t("geo.btn.check"));
|
||||
}
|
||||
configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only");
|
||||
setGeoActionBusy(false);
|
||||
refreshGeoCopy();
|
||||
}
|
||||
|
||||
setGeoGateHandler(showGeoGateForAction);
|
||||
@@ -400,31 +413,33 @@ export function startApp() {
|
||||
}, 25000);
|
||||
}
|
||||
|
||||
function isGeoPermissionDenied(error) {
|
||||
return Number(error?.code) === 1;
|
||||
}
|
||||
|
||||
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());
|
||||
geoUi.mode = "consent";
|
||||
geoUi.pendingAction = null;
|
||||
geoUi.actionLabel = "locate";
|
||||
geoUi.secondaryMode = "menu_only";
|
||||
setGeoStatus("");
|
||||
hideGeoInstructions();
|
||||
|
||||
if (geoActionBtn) {
|
||||
setGeoActionBusy(false);
|
||||
setGeoActionLabel(t("geo.btn.locate"));
|
||||
}
|
||||
configureGeoSecondaryButton("menu_only");
|
||||
setGeoActionBusy(false);
|
||||
refreshGeoCopy();
|
||||
}
|
||||
|
||||
function showGeoPermissionBlockedState() {
|
||||
setGeoStatus(t("geo.status.blocked"), { error: true });
|
||||
showGeoInstructions(getGeoPermissionInstructions());
|
||||
geoUi.mode = "blocked";
|
||||
geoUi.actionLabel = "retry";
|
||||
geoUi.secondaryMode = getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only";
|
||||
setGeoActionBusy(false);
|
||||
setGeoActionLabel(t("geo.btn.retry"));
|
||||
configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only");
|
||||
refreshGeoCopy();
|
||||
}
|
||||
|
||||
export function shouldBypassGeolocationHost() {
|
||||
@@ -517,29 +532,27 @@ export async function initGeolocationAfterBypassChecks(options = {}) {
|
||||
|
||||
loadingScreen?.classList.add("hidden");
|
||||
geoScreen?.classList.remove("hidden");
|
||||
configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only");
|
||||
geoUi.secondaryMode = getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only";
|
||||
|
||||
if (!window.isSecureContext) {
|
||||
setGeoLead(GEO_DEFAULT_LEAD());
|
||||
setGeoStatus(t("geo.status.https"), { error: true });
|
||||
showGeoInstructions(t("geo.hint.https"));
|
||||
geoUi.mode = "https";
|
||||
geoUi.actionLabel = "retry";
|
||||
setGeoActionBusy(false);
|
||||
setGeoActionLabel(t("geo.btn.retry"));
|
||||
refreshGeoCopy();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!navigator.geolocation) {
|
||||
setGeoLead(GEO_DEFAULT_LEAD());
|
||||
setGeoStatus(t("geo.status.unsupported"), { error: true });
|
||||
hideGeoInstructions();
|
||||
geoUi.mode = "unsupported";
|
||||
geoUi.actionLabel = "retry";
|
||||
setGeoActionBusy(false);
|
||||
refreshGeoCopy();
|
||||
return;
|
||||
}
|
||||
|
||||
setGeoLead(GEO_DEFAULT_LEAD());
|
||||
setGeoStatus(t("geo.status.checking"), { info: true });
|
||||
hideGeoInstructions();
|
||||
geoUi.mode = "checking";
|
||||
setGeoActionBusy(true);
|
||||
refreshGeoCopy();
|
||||
|
||||
const permissionState = await queryGeolocationPermissionState();
|
||||
if (permissionState === "denied") {
|
||||
@@ -573,36 +586,35 @@ export async function initGeolocationAfterBypassChecks(options = {}) {
|
||||
distanceMeters: Math.round(dist),
|
||||
accuracyMeters: Math.round(accuracy),
|
||||
});
|
||||
geoUi.mode = "outside";
|
||||
geoUi.outsideDist = Math.round(dist);
|
||||
geoUi.outsideAccuracy = Math.round(accuracy);
|
||||
geoUi.actionLabel = "retry";
|
||||
geoUi.secondaryMode = getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only";
|
||||
setGeoActionBusy(false);
|
||||
setGeoActionLabel(t("geo.btn.retry"));
|
||||
configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only");
|
||||
setGeoStatus(
|
||||
t("geo.status.outside", {
|
||||
dist: Math.round(dist),
|
||||
accuracy: Math.round(accuracy),
|
||||
}),
|
||||
{ error: true }
|
||||
);
|
||||
showGeoInstructions(t("geo.hint.outside"));
|
||||
refreshGeoCopy();
|
||||
} catch (error) {
|
||||
trackEvent("geo_check_failed", {
|
||||
reason: "browser_error",
|
||||
code: error.code || null,
|
||||
message: String(error.message || ""),
|
||||
});
|
||||
setGeoActionBusy(false);
|
||||
setGeoActionLabel(t("geo.btn.retry"));
|
||||
configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only");
|
||||
const deniedBecauseInsecure = /secure origins|only secure|https/i.test(String(error.message || ""));
|
||||
|
||||
if (deniedBecauseInsecure) {
|
||||
setGeoStatus(t("geo.status.https_short"), { error: true });
|
||||
showGeoInstructions(t("geo.hint.https"));
|
||||
geoUi.mode = "https";
|
||||
geoUi.actionLabel = "retry";
|
||||
geoUi.secondaryMode = getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only";
|
||||
setGeoActionBusy(false);
|
||||
refreshGeoCopy();
|
||||
} else if (isGeoPermissionDenied(error)) {
|
||||
showGeoPermissionBlockedState();
|
||||
} else {
|
||||
setGeoStatus(t("geo.status.failed"), { error: true });
|
||||
showGeoInstructions(t("geo.hint.retry"));
|
||||
geoUi.mode = "failed";
|
||||
geoUi.actionLabel = "retry";
|
||||
geoUi.secondaryMode = getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only";
|
||||
setGeoActionBusy(false);
|
||||
refreshGeoCopy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,18 @@ import de from "../locales/de.js";
|
||||
|
||||
const STORAGE_KEY = "karczma_lang";
|
||||
const catalogs = { pl, en, de };
|
||||
const LANG_BUSY_MIN_MS = 280;
|
||||
|
||||
let currentLang = "pl";
|
||||
/** @type {Set<(lang: string) => void>} */
|
||||
let langBusy = false;
|
||||
/** @type {Set<(lang: string) => void | Promise<void>>} */
|
||||
const listeners = new Set();
|
||||
/** @type {Set<(busy: boolean) => void>} */
|
||||
const busyListeners = new Set();
|
||||
|
||||
/** @type {Set<{ el: HTMLElement, close: () => void }>} */
|
||||
const openPickerClosers = new Set();
|
||||
let documentClickBound = false;
|
||||
|
||||
function interpolate(template, vars = {}) {
|
||||
return String(template).replace(/\{(\w+)\}/g, (_, key) => {
|
||||
@@ -15,6 +23,30 @@ function interpolate(template, vars = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
function ensureDocumentClickHandler() {
|
||||
if (documentClickBound) return;
|
||||
documentClickBound = true;
|
||||
document.addEventListener("click", (e) => {
|
||||
openPickerClosers.forEach((picker) => {
|
||||
if (!picker.el.contains(e.target)) {
|
||||
picker.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setLangBusy(busy) {
|
||||
langBusy = !!busy;
|
||||
document.documentElement.classList.toggle("is-lang-loading", langBusy);
|
||||
busyListeners.forEach((cb) => {
|
||||
try {
|
||||
cb(langBusy);
|
||||
} catch (err) {
|
||||
console.warn("[i18n] busy listener failed", err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getAvailableLanguages() {
|
||||
const fromConfig = window.APP_CONFIG?.languages;
|
||||
if (Array.isArray(fromConfig) && fromConfig.length) {
|
||||
@@ -31,6 +63,10 @@ export function getLang() {
|
||||
return currentLang;
|
||||
}
|
||||
|
||||
export function isLangBusy() {
|
||||
return langBusy;
|
||||
}
|
||||
|
||||
export function t(key, vars = {}) {
|
||||
const catalog = catalogs[currentLang] || catalogs.pl;
|
||||
const fallback = catalogs.pl;
|
||||
@@ -72,7 +108,12 @@ export function onLangChange(callback) {
|
||||
return () => listeners.delete(callback);
|
||||
}
|
||||
|
||||
export function setLang(lang, { persist = true, notify = true } = {}) {
|
||||
export function onLangBusyChange(callback) {
|
||||
busyListeners.add(callback);
|
||||
return () => busyListeners.delete(callback);
|
||||
}
|
||||
|
||||
export async function setLang(lang, { persist = true, notify = true } = {}) {
|
||||
const next = catalogs[lang] ? lang : "pl";
|
||||
const changed = next !== currentLang;
|
||||
currentLang = next;
|
||||
@@ -87,14 +128,38 @@ export function setLang(lang, { persist = true, notify = true } = {}) {
|
||||
|
||||
applyTranslations();
|
||||
|
||||
if (notify && changed) {
|
||||
if (!(notify && changed)) {
|
||||
return currentLang;
|
||||
}
|
||||
|
||||
if (langBusy) {
|
||||
return currentLang;
|
||||
}
|
||||
|
||||
const startedAt = Date.now();
|
||||
setLangBusy(true);
|
||||
|
||||
try {
|
||||
const tasks = [];
|
||||
listeners.forEach((cb) => {
|
||||
try {
|
||||
cb(currentLang);
|
||||
const result = cb(currentLang);
|
||||
if (result != null && typeof result.then === "function") {
|
||||
tasks.push(result);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("[i18n] listener failed", err);
|
||||
}
|
||||
});
|
||||
if (tasks.length) {
|
||||
await Promise.allSettled(tasks);
|
||||
}
|
||||
} finally {
|
||||
const wait = Math.max(0, LANG_BUSY_MIN_MS - (Date.now() - startedAt));
|
||||
if (wait > 0) {
|
||||
await new Promise((resolve) => setTimeout(resolve, wait));
|
||||
}
|
||||
setLangBusy(false);
|
||||
}
|
||||
|
||||
return currentLang;
|
||||
@@ -116,7 +181,13 @@ function flagImgHtml(code) {
|
||||
return `<img class="lang-flag-img" src="assets/img/flags/${safe}.svg" alt="" width="28" height="20" decoding="async">`;
|
||||
}
|
||||
|
||||
function spinnerHtml() {
|
||||
return `<span class="lang-picker-spinner" aria-hidden="true"></span>`;
|
||||
}
|
||||
|
||||
export function createLanguagePicker({ idPrefix = "lang" } = {}) {
|
||||
ensureDocumentClickHandler();
|
||||
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "lang-picker";
|
||||
wrap.dataset.langPicker = "1";
|
||||
@@ -133,13 +204,41 @@ export function createLanguagePicker({ idPrefix = "lang" } = {}) {
|
||||
menu.id = `${idPrefix}Menu`;
|
||||
menu.setAttribute("role", "listbox");
|
||||
|
||||
function refresh() {
|
||||
const pickerApi = {
|
||||
el: wrap,
|
||||
close: () => {
|
||||
menu.classList.add("hidden");
|
||||
btn.setAttribute("aria-expanded", "false");
|
||||
wrap.classList.remove("is-open");
|
||||
openPickerClosers.delete(pickerApi);
|
||||
},
|
||||
};
|
||||
|
||||
function paintButton() {
|
||||
const langs = getAvailableLanguages();
|
||||
const current = langs.find((l) => l.code === getLang()) || langs[0];
|
||||
const code = current?.code || "pl";
|
||||
|
||||
if (langBusy) {
|
||||
btn.innerHTML = spinnerHtml();
|
||||
btn.disabled = true;
|
||||
btn.setAttribute("aria-busy", "true");
|
||||
btn.setAttribute("aria-label", t("lang.loading"));
|
||||
wrap.classList.add("is-loading");
|
||||
return;
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.setAttribute("aria-busy", "false");
|
||||
btn.innerHTML = flagImgHtml(code);
|
||||
btn.setAttribute("aria-label", `${t("lang.aria")}: ${current?.label || code}`);
|
||||
wrap.classList.remove("is-loading");
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
paintButton();
|
||||
|
||||
const langs = getAvailableLanguages();
|
||||
menu.innerHTML = "";
|
||||
langs.forEach((lang) => {
|
||||
const option = document.createElement("button");
|
||||
@@ -147,12 +246,14 @@ export function createLanguagePicker({ idPrefix = "lang" } = {}) {
|
||||
option.className = "lang-picker-option" + (lang.code === getLang() ? " is-active" : "");
|
||||
option.setAttribute("role", "option");
|
||||
option.setAttribute("aria-selected", lang.code === getLang() ? "true" : "false");
|
||||
option.disabled = langBusy;
|
||||
option.innerHTML = `${flagImgHtml(lang.code)}<span>${lang.label}</span>`;
|
||||
option.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
close();
|
||||
if (langBusy) return;
|
||||
pickerApi.close();
|
||||
if (lang.code !== getLang()) {
|
||||
setLang(lang.code);
|
||||
void setLang(lang.code);
|
||||
}
|
||||
});
|
||||
menu.appendChild(option);
|
||||
@@ -160,25 +261,21 @@ export function createLanguagePicker({ idPrefix = "lang" } = {}) {
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (langBusy) return;
|
||||
openPickerClosers.forEach((p) => {
|
||||
if (p !== pickerApi) p.close();
|
||||
});
|
||||
menu.classList.remove("hidden");
|
||||
btn.setAttribute("aria-expanded", "true");
|
||||
wrap.classList.add("is-open");
|
||||
}
|
||||
|
||||
function close() {
|
||||
menu.classList.add("hidden");
|
||||
btn.setAttribute("aria-expanded", "false");
|
||||
wrap.classList.remove("is-open");
|
||||
openPickerClosers.add(pickerApi);
|
||||
}
|
||||
|
||||
btn.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
if (langBusy) return;
|
||||
if (menu.classList.contains("hidden")) open();
|
||||
else close();
|
||||
});
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
if (!wrap.contains(e.target)) close();
|
||||
else pickerApi.close();
|
||||
});
|
||||
|
||||
wrap.appendChild(btn);
|
||||
@@ -186,6 +283,10 @@ export function createLanguagePicker({ idPrefix = "lang" } = {}) {
|
||||
refresh();
|
||||
|
||||
onLangChange(() => refresh());
|
||||
onLangBusyChange(() => {
|
||||
if (langBusy) pickerApi.close();
|
||||
paintButton();
|
||||
});
|
||||
|
||||
return { el: wrap, refresh, close };
|
||||
return { el: wrap, refresh, close: pickerApi.close };
|
||||
}
|
||||
|
||||
@@ -7,7 +7,19 @@ let itemModalIndex = -1;
|
||||
let itemModalTouchStart = null;
|
||||
let itemModalDragging = false;
|
||||
let itemModalAnimating = false;
|
||||
let menuLangChangeBound = false;
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function escapeAttr(value) {
|
||||
return escapeHtml(value).replace(/`/g, "`");
|
||||
}
|
||||
|
||||
function resetItemModalPane() {
|
||||
const pane = document.getElementById("itemModalPane");
|
||||
@@ -77,10 +89,10 @@ function renderMenuListImage(url) {
|
||||
const hasUrl = isValidMenuImageUrl(url);
|
||||
const imgClass = hasUrl ? "rmc-image" : "rmc-image hidden";
|
||||
const placeholderClass = hasUrl ? "menu-image-placeholder hidden" : "menu-image-placeholder";
|
||||
const srcAttr = hasUrl ? ` src="${url}"` : "";
|
||||
const srcAttr = hasUrl ? ` src="${escapeAttr(url)}"` : "";
|
||||
const onerror = hasUrl ? ' onerror="handleMenuImageError(this)"' : "";
|
||||
|
||||
return `<div class="rmc-image-wrap"><img class="${imgClass}"${srcAttr} alt="" loading="lazy"${onerror}><div class="${placeholderClass}" aria-hidden="true"><svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span>${t("menu.no_image")}</span></div></div>`;
|
||||
return `<div class="rmc-image-wrap"><img class="${imgClass}"${srcAttr} alt="" loading="lazy"${onerror}><div class="${placeholderClass}" aria-hidden="true"><svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg><span>${escapeHtml(t("menu.no_image"))}</span></div></div>`;
|
||||
}
|
||||
|
||||
function findMenuItem(categoryId, position) {
|
||||
@@ -250,13 +262,13 @@ function renderCategoryNav(categories) {
|
||||
if (!ul) return;
|
||||
|
||||
const list = Array.isArray(categories) ? categories : [];
|
||||
let html = `<li><a href="#" class="active" data-category-badge="0" onclick="showCategory(0); return false;">${t("menu.all")}</a></li>`;
|
||||
let html = `<li><a href="#" class="active" data-category-badge="0" onclick="showCategory(0); return false;">${escapeHtml(t("menu.all"))}</a></li>`;
|
||||
|
||||
list.forEach((cat) => {
|
||||
const id = String(cat?.id ?? "").trim();
|
||||
const name = String(cat?.name ?? "").trim();
|
||||
if (!id || !name) return;
|
||||
html += `<li><a href="#" data-category-badge="${id}" onclick="showCategory('${id}'); return false;">${name}</a></li>`;
|
||||
html += `<li><a href="#" data-category-badge="${escapeAttr(id)}" onclick="showCategory('${escapeAttr(id)}'); return false;">${escapeHtml(name)}</a></li>`;
|
||||
});
|
||||
|
||||
ul.innerHTML = html;
|
||||
@@ -264,11 +276,19 @@ function renderCategoryNav(categories) {
|
||||
|
||||
function showMenuLoadError(container) {
|
||||
if (!container) return;
|
||||
container.innerHTML = `<p style="text-align:center; padding: 20px; color: var(--text-muted);">${t("menu.load_error")}</p>`;
|
||||
container.innerHTML = `<p style="text-align:center; padding: 20px; color: var(--text-muted);">${escapeHtml(t("menu.load_error"))}</p>`;
|
||||
}
|
||||
|
||||
function getActiveCategoryId() {
|
||||
return (
|
||||
document.querySelector(".menu-categories-nav a.active")?.getAttribute("data-category-badge") ||
|
||||
"0"
|
||||
);
|
||||
}
|
||||
|
||||
export async function loadMenu(lang = getLang()) {
|
||||
const container = document.getElementById("menuContainer");
|
||||
const previousCategoryId = getActiveCategoryId();
|
||||
try {
|
||||
const response = await fetch(`${endpoints.menu}?lang=${encodeURIComponent(lang)}`);
|
||||
if (!response.ok) throw new Error(t("menu.load_error"));
|
||||
@@ -292,17 +312,19 @@ export async function loadMenu(lang = getLang()) {
|
||||
catDiv.className = "rm-category";
|
||||
catDiv.setAttribute("data-cat-id", catId);
|
||||
|
||||
let html = `<div class="restaurant-menu-category">${section.categoryName || ""}</div>
|
||||
let html = `<div class="restaurant-menu-category">${escapeHtml(section.categoryName || "")}</div>
|
||||
<div class="rmc-positions">`;
|
||||
|
||||
items.forEach((item) => {
|
||||
const categoryId = escapeAttr(item.categoryId);
|
||||
const position = escapeAttr(item.position);
|
||||
html += `
|
||||
<div class="rmc-position" data-position="${item.position}" data-category-id="${item.categoryId}" onclick="openItemModal('${item.categoryId}', '${item.position}')" style="cursor: pointer;">
|
||||
<div class="rmc-position" data-position="${position}" data-category-id="${categoryId}" onclick="openItemModal('${categoryId}', '${position}')" style="cursor: pointer;">
|
||||
${renderMenuListImage(item.image)}
|
||||
<div class="rmc-title">
|
||||
<h4>${item.title}<span>${item.description || ""}</span></h4>
|
||||
<h4>${escapeHtml(item.title)}<span>${escapeHtml(item.description || "")}</span></h4>
|
||||
</div>
|
||||
<div class="rmc-other"><span>${item.price}</span></div>
|
||||
<div class="rmc-other"><span>${escapeHtml(item.price)}</span></div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
@@ -313,7 +335,12 @@ export async function loadMenu(lang = getLang()) {
|
||||
});
|
||||
|
||||
renderCategoryNav(payload.categories);
|
||||
showCategory(0);
|
||||
|
||||
const safePrev = String(previousCategoryId).replace(/"/g, "");
|
||||
const categoryStillExists =
|
||||
safePrev === "0" ||
|
||||
!!document.querySelector(`.menu-categories-nav a[data-category-badge="${safePrev}"]`);
|
||||
showCategory(categoryStillExists ? previousCategoryId : 0);
|
||||
|
||||
const searchInput = document.getElementById("menuSearchInput");
|
||||
if (searchInput?.value) {
|
||||
@@ -325,13 +352,7 @@ export async function loadMenu(lang = getLang()) {
|
||||
}
|
||||
}
|
||||
|
||||
export function setMenuLanguageReload() {
|
||||
if (menuLangChangeBound) return;
|
||||
menuLangChangeBound = true;
|
||||
onLangChange(() => loadMenu());
|
||||
}
|
||||
|
||||
setMenuLanguageReload();
|
||||
onLangChange((lang) => loadMenu(lang));
|
||||
|
||||
export function openItemModal(categoryId, position) {
|
||||
itemModalKeys = buildVisibleMenuItemKeys();
|
||||
@@ -370,7 +391,8 @@ export function closeItemModal() {
|
||||
}
|
||||
|
||||
export function filterMenu() {
|
||||
const query = document.getElementById("menuSearchInput").value.toLowerCase();
|
||||
const searchInput = document.getElementById("menuSearchInput");
|
||||
const query = (searchInput?.value || "").toLowerCase();
|
||||
const now = Date.now();
|
||||
if (query.length >= 2 && (!window.lastMenuSearchEventAt || now - window.lastMenuSearchEventAt > 8000)) {
|
||||
window.lastMenuSearchEventAt = now;
|
||||
@@ -383,7 +405,7 @@ export function filterMenu() {
|
||||
const items = category.querySelectorAll(".rmc-position");
|
||||
|
||||
items.forEach((item) => {
|
||||
const title = item.querySelector(".rmc-title h4").textContent.toLowerCase();
|
||||
const title = item.querySelector(".rmc-title h4")?.textContent?.toLowerCase() || "";
|
||||
if (title.includes(query)) {
|
||||
item.style.display = "";
|
||||
hasVisibleItems = true;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { endpoints, loaderMinMs } from "./config.js";
|
||||
import { getLang, t } from "./i18n.js";
|
||||
import { getLang, onLangChange, t } from "./i18n.js";
|
||||
import { runPendingProtectedAction, updateNavAccessState } from "./access.js";
|
||||
import {
|
||||
refreshGuestPendingActions,
|
||||
@@ -31,6 +31,19 @@ const SIX_MONTHS_MS = 180 * 24 * 60 * 60 * 1000;
|
||||
const LOADER_MIN_MS = loaderMinMs;
|
||||
const loadStartTime = Date.now();
|
||||
|
||||
/** @type {Array} */
|
||||
let lastBillsForUi = [];
|
||||
let hasRenderedOrders = false;
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? "")
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
function getLoaderMsgs() {
|
||||
return [t("loader.msg1"), t("loader.msg2"), t("loader.msg3"), t("loader.msg4")];
|
||||
}
|
||||
@@ -56,6 +69,20 @@ if (getTableParam()) {
|
||||
tableLabel.textContent = formatTableLabel(getTableParam());
|
||||
}
|
||||
|
||||
onLangChange(() => {
|
||||
if (tableLabel && getTableParam()) {
|
||||
tableLabel.textContent = formatTableLabel(getTableParam());
|
||||
}
|
||||
|
||||
if (loaderMsg && loadingScreen && !loadingScreen.classList.contains("hidden")) {
|
||||
loaderMsg.textContent = getLoaderMsgs()[msgIdx];
|
||||
}
|
||||
|
||||
if (hasRenderedOrders) {
|
||||
updateUI(lastBillsForUi, { manageLoader: false });
|
||||
}
|
||||
});
|
||||
|
||||
/** Storage key must follow current hash (fallback: table), not a frozen empty tableParam at init. */
|
||||
export function getOrderStorageKey() {
|
||||
const key = (getHashParam() || getTableParam() || "unknown").toLowerCase();
|
||||
@@ -70,7 +97,7 @@ export function hideLoader() {
|
||||
clearInterval(msgInterval);
|
||||
const bottomNav = document.getElementById("bottomNav");
|
||||
if (bottomNav) {
|
||||
bottomNav.style.display = "";
|
||||
bottomNav.hidden = false;
|
||||
}
|
||||
updateNavAccessState();
|
||||
if (getPendingProtectedAction() && getAppAccessLevel() === "full") {
|
||||
@@ -94,11 +121,15 @@ export async function resolveTableLabel() {
|
||||
}
|
||||
}
|
||||
|
||||
export function updateUI(bills) {
|
||||
// Hide loader after minimum display time
|
||||
hideLoader();
|
||||
export function updateUI(bills, { manageLoader = true } = {}) {
|
||||
lastBillsForUi = Array.isArray(bills) ? bills : [];
|
||||
hasRenderedOrders = true;
|
||||
|
||||
const allArticles = bills.flatMap((b) => (Array.isArray(b?.Articles) ? b.Articles : []));
|
||||
if (manageLoader) {
|
||||
hideLoader();
|
||||
}
|
||||
|
||||
const allArticles = lastBillsForUi.flatMap((b) => (Array.isArray(b?.Articles) ? b.Articles : []));
|
||||
const items = mergeWithPersistedItems(allArticles);
|
||||
renderGlobalHistory();
|
||||
|
||||
@@ -108,7 +139,7 @@ export function updateUI(bills) {
|
||||
}
|
||||
|
||||
renderItems(items);
|
||||
updateStatus(bills, items);
|
||||
updateStatus(lastBillsForUi, items);
|
||||
}
|
||||
|
||||
function showEmptyState() {
|
||||
@@ -235,14 +266,14 @@ export function renderGlobalHistory() {
|
||||
div.className = "item-card archived ready";
|
||||
div.innerHTML = `
|
||||
<div class="item-info">
|
||||
<span class="item-name">${entry.name}</span>
|
||||
<span class="item-meta">${t("history.entry_meta", {
|
||||
<span class="item-name">${escapeHtml(entry.name)}</span>
|
||||
<span class="item-meta">${escapeHtml(t("history.entry_meta", {
|
||||
table: formatTableLabel(entry.sourceTable || "?"),
|
||||
date: dt.toLocaleDateString(lang),
|
||||
time: dt.toLocaleTimeString(lang, { hour: "2-digit", minute: "2-digit" }),
|
||||
})}</span>
|
||||
}))}</span>
|
||||
</div>
|
||||
<div class="item-qty">x${entry.qty}</div>
|
||||
<div class="item-qty">x${escapeHtml(entry.qty)}</div>
|
||||
`;
|
||||
historyList.appendChild(div);
|
||||
});
|
||||
@@ -326,10 +357,10 @@ function renderItems(items) {
|
||||
|
||||
div.innerHTML = `
|
||||
<div class="item-info">
|
||||
<span class="item-name">${item.name}</span>
|
||||
<span class="item-meta">${meta}</span>
|
||||
<span class="item-name">${escapeHtml(item.name)}</span>
|
||||
<span class="item-meta">${escapeHtml(meta)}</span>
|
||||
</div>
|
||||
<div class="item-qty">x${item.qty}</div>
|
||||
<div class="item-qty">x${escapeHtml(item.qty)}</div>
|
||||
`;
|
||||
itemsList.appendChild(div);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user