Zmiana aplikacji na wersje lang

This commit is contained in:
2026-09-25 16:59:37 +02:00
parent 9d25de0d89
commit 3056eb6c3d
24 changed files with 1308 additions and 1105 deletions
+25 -3
View File
@@ -43,6 +43,10 @@ import {
saveUserName,
startGeoBootstrap,
} from "./modules/geo.js";
import {
createLanguagePicker,
initI18n,
} from "./modules/i18n.js";
import {
getHashParam,
setHashParam,
@@ -51,13 +55,32 @@ import {
} from "./modules/state.js";
import { showToast } from "./modules/toast.js";
initI18n();
function mountLanguagePickers() {
const mounts = [
["geoLangPickerMount", "geoLang"],
["headerLangPickerMount", "headerLang"],
["menuLangPickerMount", "menuLang"],
];
mounts.forEach(([mountId, prefix]) => {
const mount = document.getElementById(mountId);
if (!mount || mount.dataset.ready) return;
const picker = createLanguagePicker({ idPrefix: prefix });
mount.appendChild(picker.el);
mount.dataset.ready = "1";
});
}
mountLanguagePickers();
// Parse URL into shared state
const params = new URLSearchParams(location.search);
setHashParam((params.get("h") || "").trim());
setIsStaffPreview(params.get("preview") === "staff");
setTableParam(""); // Puste, zostanie uzupełnione przez backend
setTableParam("");
// Jeśli brak hasha w URL – zapytaj użytkownika (np. do testów)
if (!getHashParam()) {
const input = prompt("Podaj bezpieczny hash stolika (wymagane):");
const trimmed = (input || "").trim();
@@ -73,7 +96,6 @@ setProtectedActionHandlers({
bill: openBillDialogInternal,
});
// window.* for inline onclick in app.php
window.saveUserName = saveUserName;
window.declineUserName = declineUserName;
window.handleGeoMenuClick = handleGeoMenuClick;
+145
View File
@@ -0,0 +1,145 @@
export default {
"html.lang": "de",
"doc.title": "Karczma Biesiada – Ihre Bestellung",
"loader.connecting": "Verbindung zur Küche...",
"loader.msg1": "Öfen werden angeheizt...",
"loader.msg2": "Der Küchenchef prüft die Zutaten...",
"loader.msg3": "Verbindung zum Herzen des Restaurants...",
"loader.msg4": "Gleich fertig...",
"loader.api_error": "API-Fehler: {message}",
"loader.connection": "Verbindungsproblem. Wir versuchen es erneut...",
"geo.title": "Willkommen in der Karczma",
"geo.lead": "Durchstöbern Sie gleich die Speisekarte — oder bestätigen Sie, dass Sie bei uns sind, um den Kellner zu rufen, die Bestellung zu verfolgen und die Rechnung anzufordern.",
"geo.lead_action": "Die Speisekarte ist bereits geöffnet. Um <b>{feature}</b> zu nutzen, bestätigen Sie kurz, dass Sie im Restaurant sind.",
"geo.btn.menu_main": "Zur Speisekarte",
"geo.btn.menu_sub": "ohne Standort",
"geo.btn.back_menu": "Zurück zur Speisekarte",
"geo.btn.locate": "OK, Standort prüfen",
"geo.btn.retry": "Erneut versuchen",
"geo.btn.check": "Standort prüfen",
"geo.btn.checking": "Prüfung…",
"geo.feature.other": "diese Funktion",
"geo.wifi.title": "📶 Ohne Standortfreigabe eintreten",
"geo.wifi.connect": "Verbinden Sie Ihr Telefon mit dem WLAN des Restaurants:",
"geo.wifi.after": "Nach der Verbindung <strong>Seite aktualisieren</strong> (oder QR-Code schließen und erneut öffnen). Die App lässt Sie <strong>ohne Standortabfrage</strong> hinein — mit vollem Zugang zu:",
"geo.wifi.li1": "Kellner rufen",
"geo.wifi.li2": "Rechnung anfordern",
"geo.wifi.li3": "Bestellstatus",
"geo.wifi.li4": "der gesamten Tisch-App",
"geo.feature.status": "Bestellstatus",
"geo.feature.waiter": "Kellner rufen",
"geo.feature.bill": "Rechnung anfordern",
"geo.status.checking": "Wir prüfen Ihren Standort…",
"geo.status.blocked": "Der Browser hat den Standortzugriff blockiert.",
"geo.status.https": "Diese Seite erfordert eine sichere HTTPS-Verbindung.",
"geo.status.https_short": "Geolokalisierung erfordert HTTPS.",
"geo.status.unsupported": "Ihr Browser unterstützt keine Geolokalisierung.",
"geo.status.failed": "Standort konnte nicht ermittelt werden.",
"geo.status.outside": "Sie scheinen außerhalb des Restaurants zu sein (ca. {dist} m, GPS-Genauigkeit: ±{accuracy} m).",
"geo.hint.permission": "Aktivieren Sie den Standort für diese Seite in den Browsereinstellungen und versuchen Sie es erneut.",
"geo.hint.https": "Öffnen Sie die App über eine sichere <b>https://</b>-Adresse und versuchen Sie es erneut.",
"geo.hint.retry": "Prüfen Sie den Empfang, schalten Sie GPS ein und versuchen Sie es erneut.",
"geo.hint.outside": "Browser melden oft einen anderen Standort als Google Maps. Versuchen Sie es im Freien oder näher am Fenster — oder gehen Sie ohne Standort zur Speisekarte.",
"table.choose": "Tisch wählen",
"table.label": "Tisch {name}",
"status.title": "Aktueller Status",
"status.waiting": "Warten...",
"status.checking": "Wir prüfen, was gekocht wird...",
"status.none": "Keine aktiven Bestellungen",
"status.none_meta": "Schauen Sie gern in unsere Speisekarte.",
"status.ready": "Bereit zum Servieren!",
"status.ready_meta": "Alle Ihre Gerichte haben die Küche verlassen.",
"status.partial": "Teilweise fertig",
"status.partial_meta": "Die ersten Köstlichkeiten warten schon auf Sie!",
"status.preparing": "In Zubereitung",
"status.preparing_meta": "Ihre Bestellung wird gerade von unseren Köchen zubereitet.",
"status.footer": "Bestellung um {time} • Tisch {table}",
"orders.title": "Ihre bestellten Gerichte",
"orders.empty": "Wenn Sie gerade bestellt haben, geben Sie uns einen Moment zur Verarbeitung.",
"orders.browse_menu": "Speisekarte ansehen",
"history.title": "Ihre früheren Bestellungen",
"history.note": "Das sind Positionen von anderen Besuchen, die auf diesem Telefon nach dem Scannen von QR-Codes sichtbar waren. Ohne Scan erscheinen sie hier nicht 🙂",
"history.clear": "Verlauf löschen",
"history.clear_confirm": "Möchten Sie den Verlauf Ihrer früheren Bestellungen wirklich löschen?",
"history.entry_meta": "{table} • {date} {time}",
"orders.item_preparing": "🔥 In Zubereitung",
"orders.item_ready": "✅ Fertig",
"orders.item_ready_done": "✅ Fertig (erledigt)",
"orders.item_fallback": "Position",
"menu.banner": "Bestätigen Sie Ihren Standort, um den Kellner zu rufen oder die Rechnung anzufordern.",
"menu.banner_btn": "Jetzt prüfen",
"menu.search": "Gericht suchen...",
"menu.all": "Alles",
"menu.load_error": "Speisekarte konnte nicht geladen werden.",
"menu.no_image": "Kein Foto",
"menu.close": "Schließen",
"menu.lang": "Sprache",
"nav.order": "Bestellung",
"nav.menu": "Karte",
"nav.waiter": "Kellner",
"nav.bill": "Rechnung",
"waiter.title": "Personal rufen?",
"waiter.body": "Der Kellner erhält sofort eine Benachrichtigung und kommt so schnell wie möglich an Ihren Tisch.",
"waiter.confirm": "Ja, Kellner rufen",
"waiter.cancel": "Abbrechen",
"waiter.toast_ok": "Ein Kellner kommt gleich zu Ihnen!",
"waiter.toast_fail": "Anfrage konnte nicht gesendet werden. Bitte versuchen Sie es gleich erneut.",
"waiter.blocked": "Ein Kellner wurde bereits gerufen. Bitte warten Sie, bis das Personal dies bestätigt.",
"waiter.queue_title": "Kellner rufen",
"bill.title": "Abrechnung",
"bill.loading": "⏳ Rechnungen werden geladen...",
"bill.loading_empty": "Keine offenen Rechnungen zum Bezahlen.",
"bill.loading_error": "Fehler beim Laden der Rechnungen.",
"bill.multi": "An diesem Tisch gibt es mehrere offene Rechnungen. Welche möchten Sie bezahlen?",
"bill.review": "Rechnungsübersicht:",
"bill.total": "Zu zahlen:",
"bill.back": "Zurück",
"bill.request": "Rechnung anfordern",
"bill.payment": "Wählen Sie die bevorzugte Zahlungsart:",
"bill.card": "Karte",
"bill.cash": "Bargeld",
"bill.back_summary": "Zurück zur Übersicht",
"bill.doc": "Welches Dokument benötigen Sie?",
"bill.receipt": "Bon",
"bill.invoice": "Rechnung",
"bill.nip_intro": "Geben Sie die Steuernummer (NIP) der Firma ein, damit wir die Daten automatisch abrufen können.",
"bill.nip_label": "NIP-Nummer",
"bill.gus": "Aus Register abrufen",
"bill.gus_searching": "Suche...",
"bill.verify": "Sind die Rechnungsdaten unten korrekt?",
"bill.confirm_invoice": "Ja, Rechnung anfordern!",
"bill.change_nip": "NIP ändern",
"bill.edit_company": "Manuell korrigieren",
"bill.edit_done": "Bearbeitung beenden",
"bill.street": "Straße und Nummer",
"bill.zip": "PLZ",
"bill.city": "Stadt",
"bill.toast_receipt": "Der Kellner bringt den Bon zur Zahlung!",
"bill.toast_invoice": "Danke! Ihre Rechnungsanfrage wurde gesendet.",
"bill.toast_fail": "Rechnungsanfrage konnte nicht gesendet werden. Bitte versuchen Sie es gleich erneut.",
"bill.blocked": "Eine Rechnungsanfrage wurde bereits gesendet. Bitte warten Sie, bis das Personal sie bearbeitet.",
"bill.nip_invalid": "Geben Sie eine gültige NIP-Nummer ein.",
"bill.gus_fail": "Firmendaten für diese NIP konnten nicht abgerufen werden.",
"bill.gus_error": "Verbindungsfehler zum Firmenregister.",
"bill.payment_unknown": "unbekannt",
"bill.queue_title": "Rechnungsanfrage",
"bill.queue_payment": "Zahlungsart:",
"bill.queue_doc": "Dokument:",
"bill.queue_nip": "NIP:",
"bill.queue_company": "Firma:",
"bill.queue_address": "Adresse:",
"bill.doc_receipt": "Bon",
"bill.doc_invoice": "Rechnung",
"bill.bill_fallback": "Rechnung",
"toast.sent": "Gesendet!",
"lang.aria": "Sprache wählen",
};
+145
View File
@@ -0,0 +1,145 @@
export default {
"html.lang": "en",
"doc.title": "Karczma Biesiada – Your Order",
"loader.connecting": "Connecting to the kitchen...",
"loader.msg1": "Firing up the ovens...",
"loader.msg2": "Chef is checking the ingredients...",
"loader.msg3": "Connecting to the heart of the restaurant...",
"loader.msg4": "Almost ready...",
"loader.api_error": "API error: {message}",
"loader.connection": "Connection problem. Retrying...",
"geo.title": "Welcome to Karczma",
"geo.lead": "Browse the menu right away — or confirm you are with us to call a waiter, track your order and request the bill.",
"geo.lead_action": "You already have the menu open. To use <b>{feature}</b>, briefly confirm you are at the restaurant.",
"geo.btn.menu_main": "Go to menu",
"geo.btn.menu_sub": "without location",
"geo.btn.back_menu": "Back to menu",
"geo.btn.locate": "OK, check my location",
"geo.btn.retry": "Try again",
"geo.btn.check": "Check location",
"geo.btn.checking": "Checking…",
"geo.feature.other": "this feature",
"geo.wifi.title": "📶 Enter without location permission",
"geo.wifi.connect": "Connect your phone to the restaurant Wi‑Fi:",
"geo.wifi.after": "After connecting, <strong>refresh the page</strong> (or close and open the QR code again). The app will let you in <strong>without asking for location</strong> — with full access to:",
"geo.wifi.li1": "calling a waiter",
"geo.wifi.li2": "requesting the bill",
"geo.wifi.li3": "order status",
"geo.wifi.li4": "the full table-side app",
"geo.feature.status": "order status",
"geo.feature.waiter": "calling a waiter",
"geo.feature.bill": "requesting the bill",
"geo.status.checking": "Checking your location…",
"geo.status.blocked": "The browser blocked location access.",
"geo.status.https": "This page requires a secure HTTPS connection.",
"geo.status.https_short": "Geolocation requires HTTPS.",
"geo.status.unsupported": "Your browser does not support geolocation.",
"geo.status.failed": "Could not get your location.",
"geo.status.outside": "It looks like you are outside the restaurant (about {dist} m, GPS accuracy: ±{accuracy} m).",
"geo.hint.permission": "Enable location for this site in your browser settings and try again.",
"geo.hint.https": "Open the app via a secure <b>https://</b> address and try again.",
"geo.hint.retry": "Check signal, turn on GPS and try again.",
"geo.hint.outside": "Browsers often report a different location than Google Maps. Try again outdoors or closer to a window — or continue to the menu without location.",
"table.choose": "Select a table",
"table.label": "Table {name}",
"status.title": "Current status",
"status.waiting": "Waiting...",
"status.checking": "Checking what’s cooking...",
"status.none": "No active orders",
"status.none_meta": "Feel free to browse our menu.",
"status.ready": "Ready to serve!",
"status.ready_meta": "All your dishes have left the kitchen.",
"status.partial": "Partially ready",
"status.partial_meta": "The first tasty bites are waiting for you!",
"status.preparing": "Being prepared",
"status.preparing_meta": "Your order is being prepared by our chefs.",
"status.footer": "Order placed at {time} • Table {table}",
"orders.title": "Your ordered dishes",
"orders.empty": "If you just placed an order, give us a moment to process it.",
"orders.browse_menu": "Browse menu",
"history.title": "Your previous orders",
"history.note": "These are items from other visits visible on this phone after scanning QR codes. If you visited without scanning a code, they won’t appear here 🙂",
"history.clear": "Clear history",
"history.clear_confirm": "Are you sure you want to delete your previous order history?",
"history.entry_meta": "{table} • {date} {time}",
"orders.item_preparing": "🔥 Being prepared",
"orders.item_ready": "✅ Ready",
"orders.item_ready_done": "✅ Ready (completed)",
"orders.item_fallback": "Item",
"menu.banner": "Confirm your location to call a waiter or request the bill.",
"menu.banner_btn": "Check now",
"menu.search": "Search dishes...",
"menu.all": "All",
"menu.load_error": "Could not load the menu.",
"menu.no_image": "No photo",
"menu.close": "Close",
"menu.lang": "Language",
"nav.order": "Order",
"nav.menu": "Menu",
"nav.waiter": "Waiter",
"nav.bill": "Bill",
"waiter.title": "Call the staff?",
"waiter.body": "The waiter will get an instant notification and come to your table as soon as possible.",
"waiter.confirm": "Yes, call the waiter",
"waiter.cancel": "Cancel",
"waiter.toast_ok": "A waiter will be with you shortly!",
"waiter.toast_fail": "Could not send the request. Please try again in a moment.",
"waiter.blocked": "A waiter has already been called. Please wait until staff confirms it on the panel.",
"waiter.queue_title": "Waiter call",
"bill.title": "Checkout",
"bill.loading": "⏳ Loading bills...",
"bill.loading_empty": "No open bills to pay.",
"bill.loading_error": "Error loading bills.",
"bill.multi": "There are several open bills at this table. Which one would you like to pay?",
"bill.review": "Bill summary:",
"bill.total": "Total due:",
"bill.back": "Back",
"bill.request": "Request bill",
"bill.payment": "Choose your preferred payment method:",
"bill.card": "Card",
"bill.cash": "Cash",
"bill.back_summary": "Back to summary",
"bill.doc": "What document do you need?",
"bill.receipt": "Receipt",
"bill.invoice": "Invoice",
"bill.nip_intro": "Enter the company tax ID (NIP) so we can fetch the details automatically.",
"bill.nip_label": "Tax ID (NIP)",
"bill.gus": "Fetch from registry",
"bill.gus_searching": "Searching...",
"bill.verify": "Are the invoice details below correct?",
"bill.confirm_invoice": "Yes, request an invoice!",
"bill.change_nip": "Change NIP",
"bill.edit_company": "Edit manually",
"bill.edit_done": "Done editing",
"bill.street": "Street and number",
"bill.zip": "ZIP",
"bill.city": "City",
"bill.toast_receipt": "The waiter will bring the receipt for payment!",
"bill.toast_invoice": "Thank you! Your invoice request has been sent.",
"bill.toast_fail": "Could not send the bill request. Please try again in a moment.",
"bill.blocked": "A bill request has already been sent. Please wait until staff handles it.",
"bill.nip_invalid": "Enter a valid tax ID (NIP).",
"bill.gus_fail": "Could not fetch company data for this NIP.",
"bill.gus_error": "Connection error to the company registry.",
"bill.payment_unknown": "unknown",
"bill.queue_title": "Bill request",
"bill.queue_payment": "Payment method:",
"bill.queue_doc": "Document:",
"bill.queue_nip": "NIP:",
"bill.queue_company": "Company:",
"bill.queue_address": "Address:",
"bill.doc_receipt": "receipt",
"bill.doc_invoice": "invoice",
"bill.bill_fallback": "Bill",
"toast.sent": "Sent!",
"lang.aria": "Choose language",
};
+145
View File
@@ -0,0 +1,145 @@
export default {
"html.lang": "pl",
"doc.title": "Karczma Biesiada – Twoje Zamówienie",
"loader.connecting": "Łączenie z kuchnią...",
"loader.msg1": "Rozgrzewamy piece...",
"loader.msg2": "Szef kuchni sprawdza składniki...",
"loader.msg3": "Łączenie z sercem restauracji...",
"loader.msg4": "Prawie gotowe...",
"loader.api_error": "Błąd API: {message}",
"loader.connection": "Problem z połączeniem. Próbujemy ponownie...",
"geo.title": "Witamy w Karcznie",
"geo.lead": "Przeglądaj menu od razu — albo potwierdź, że jesteś u nas, aby wezwać kelnera, śledzić zamówienie i poprosić o rachunek.",
"geo.lead_action": "Menu masz już otwarte. Aby skorzystać z <b>{feature}</b>, potwierdź krótko, że jesteś w restauracji.",
"geo.btn.menu_main": "Przejdź do menu",
"geo.btn.menu_sub": "bez lokalizacji",
"geo.btn.back_menu": "Wróć do menu",
"geo.btn.locate": "Zgoda, sprawdź lokalizację",
"geo.btn.retry": "Spróbuj ponownie",
"geo.btn.check": "Sprawdź lokalizację",
"geo.btn.checking": "Sprawdzanie…",
"geo.feature.other": "tę funkcję",
"geo.wifi.title": "📶 Wejdź bez zgody na lokalizację",
"geo.wifi.connect": "Połącz telefon z siecią Wi‑Fi restauracji:",
"geo.wifi.after": "Po połączeniu <strong>odśwież stronę</strong> (lub zamknij i otwórz ponownie kod QR). Aplikacja wpuści Cię <strong>bez pytania o lokalizację</strong> — z pełnym dostępem do:",
"geo.wifi.li1": "wezwania kelnera",
"geo.wifi.li2": "prośby o rachunek",
"geo.wifi.li3": "statusu zamówienia",
"geo.wifi.li4": "całej aplikacji przy stoliku",
"geo.feature.status": "statusu zamówienia",
"geo.feature.waiter": "wezwania kelnera",
"geo.feature.bill": "prośby o rachunek",
"geo.status.checking": "Sprawdzamy Twoją lokalizację…",
"geo.status.blocked": "Przeglądarka zablokowała dostęp do lokalizacji.",
"geo.status.https": "Ta strona wymaga bezpiecznego połączenia HTTPS.",
"geo.status.https_short": "Geolokalizacja wymaga HTTPS.",
"geo.status.unsupported": "Twoja przeglądarka nie wspiera geolokalizacji.",
"geo.status.failed": "Nie udało się pobrać lokalizacji.",
"geo.status.outside": "Wygląda na to, że jesteś poza restauracją (ok. {dist} m, dokładność GPS: ±{accuracy} m).",
"geo.hint.permission": "Włącz lokalizację dla tej strony w ustawieniach przeglądarki i spróbuj ponownie.",
"geo.hint.https": "Otwórz aplikację przez bezpieczny adres <b>https://</b> i spróbuj ponownie.",
"geo.hint.retry": "Sprawdź zasięg, włącz GPS i spróbuj ponownie.",
"geo.hint.outside": "Przeglądarka często podaje inną lokalizację niż aplikacja Map Google. Spróbuj ponownie na zewnątrz lub bliżej okna — albo przejdź do menu bez lokalizacji.",
"table.choose": "Wybierz stolik",
"table.label": "Stolik {name}",
"status.title": "Aktualny status",
"status.waiting": "Oczekiwanie...",
"status.checking": "Sprawdzamy co pysznego się przygotowuje...",
"status.none": "Brak aktywnych zamówień",
"status.none_meta": "Zapraszamy do sprawdzenia naszego menu.",
"status.ready": "Gotowe do podania!",
"status.ready_meta": "Wszystkie Twoje dania opuściły już kuchnię.",
"status.partial": "Częściowo gotowe",
"status.partial_meta": "Pierwsze pyszności już na Ciebie czekają!",
"status.preparing": "W przygotowaniu",
"status.preparing_meta": "Twoje zamówienie jest właśnie tworzone przez naszych kucharzy.",
"status.footer": "Zamówienie złożone o godzinie {time} • Stolik {table}",
"orders.title": "Twoje zamówione dania",
"orders.empty": "Jeśli właśnie złożyłeś zamówienie, daj nam chwilkę na jego przetworzenie.",
"orders.browse_menu": "Przeglądaj menu",
"history.title": "Twoje poprzednie zamówienia",
"history.note": "To są pozycje z innych wizyt, które były widoczne na tym telefonie po zeskanowaniu kodów QR. Jeśli kiedyś byłeś w restauracji bez skanowania kodu, tych pozycji tu nie będzie 🙂",
"history.clear": "Usuń historię",
"history.clear_confirm": "Czy na pewno chcesz usunąć historię swoich poprzednich zamówień?",
"history.entry_meta": "{table} • {date} {time}",
"orders.item_preparing": "🔥 W przygotowaniu",
"orders.item_ready": "✅ Gotowe",
"orders.item_ready_done": "✅ Gotowe (zrealizowane)",
"orders.item_fallback": "Pozycja",
"menu.banner": "Potwierdź lokalizację, aby wezwać kelnera lub poprosić o rachunek.",
"menu.banner_btn": "Sprawdź teraz",
"menu.search": "Szukaj dania...",
"menu.all": "Wszystko",
"menu.load_error": "Nie udało się załadować menu.",
"menu.no_image": "Brak zdjęcia",
"menu.close": "Zamknij",
"menu.lang": "Język",
"nav.order": "Zamówienie",
"nav.menu": "Menu",
"nav.waiter": "Kelner",
"nav.bill": "Rachunek",
"waiter.title": "Przywołać obsługę?",
"waiter.body": "Kelner otrzyma natychmiastowe powiadomienie na swoim panelu i podejdzie do Twojego stolika najszybciej jak to możliwe.",
"waiter.confirm": "Tak, poproś kelnera",
"waiter.cancel": "Anuluj",
"waiter.toast_ok": "Kelner wkrótce do Ciebie podejdzie!",
"waiter.toast_fail": "Nie udało się wysłać wezwania. Spróbuj ponownie za chwilę.",
"waiter.blocked": "Kelner został już wezwany. Poczekaj, aż obsługa potwierdzi zgłoszenie na panelu.",
"waiter.queue_title": "Przywołanie kelnera",
"bill.title": "Rozliczenie",
"bill.loading": "⏳ Pobieranie rachunków...",
"bill.loading_empty": "Brak otwartych rachunków do opłacenia.",
"bill.loading_error": "Błąd pobierania rachunków.",
"bill.multi": "Mamy kilka otwartych rachunków na tym stoliku. Który chcesz opłacić?",
"bill.review": "Podsumowanie rachunku:",
"bill.total": "Do zapłaty:",
"bill.back": "Wróć",
"bill.request": "Poproś rachunek",
"bill.payment": "Wybierz preferowaną formę płatności:",
"bill.card": "Karta",
"bill.cash": "Gotówka",
"bill.back_summary": "Wróć do podsumowania",
"bill.doc": "Jakiego dokumentu potrzebujesz?",
"bill.receipt": "Paragon",
"bill.invoice": "Faktura",
"bill.nip_intro": "Wprowadź NIP firmy, abyśmy mogli automatycznie pobrać dane.",
"bill.nip_label": "Numer NIP",
"bill.gus": "Pobierz z GUS",
"bill.gus_searching": "Szukam...",
"bill.verify": "Czy poniższe dane do faktury są prawidłowe?",
"bill.confirm_invoice": "Tak, poproszę fakturę!",
"bill.change_nip": "Zmień NIP",
"bill.edit_company": "Popraw ręcznie",
"bill.edit_done": "Zakończ edycję",
"bill.street": "Ulica i numer",
"bill.zip": "Kod",
"bill.city": "Miasto",
"bill.toast_receipt": "Kelner przyniesie paragon do opłacenia!",
"bill.toast_invoice": "Dziękujemy! Prośba o fakturę została wysłana.",
"bill.toast_fail": "Nie udało się wysłać prośby o rachunek. Spróbuj ponownie za chwilę.",
"bill.blocked": "Prośba o rachunek została już wysłana. Poczekaj, aż obsługa ją obsłuży.",
"bill.nip_invalid": "Wprowadź poprawny numer NIP.",
"bill.gus_fail": "Nie udało się pobrać danych z GUS dla podanego NIP-u.",
"bill.gus_error": "Błąd połączenia z API GUS.",
"bill.payment_unknown": "nieznana",
"bill.queue_title": "Prośba o rachunek",
"bill.queue_payment": "Forma płatności:",
"bill.queue_doc": "Dokument:",
"bill.queue_nip": "NIP:",
"bill.queue_company": "Firma:",
"bill.queue_address": "Adres:",
"bill.doc_receipt": "paragon",
"bill.doc_invoice": "faktura",
"bill.bill_fallback": "Rachunek",
"toast.sent": "Wysłano!",
"lang.aria": "Wybierz język",
};
+20 -18
View File
@@ -1,4 +1,5 @@
import { endpoints } from "./config.js";
import { t } from "./i18n.js";
import { trackEvent } from "./analytics.js";
import { requireFullAccess } from "./access.js";
import {
@@ -41,14 +42,14 @@ export async function callWaiter(type) {
if (queued.reason === "pending") {
showToast(guestActionBlockedMessage("waiter_call"));
} else {
showToast("Nie udało się wysłać wezwania. Spróbuj ponownie za chwilę.");
showToast(t("waiter.toast_fail"));
}
return;
}
trackEvent("waiter_call_requested", { waiterType: "order" });
sendApiSimulated("CallWaiter_Order", { table: getTableParam() });
showToast("Kelner wkrótce do Ciebie podejdzie!");
showToast(t("waiter.toast_ok"));
}
export function openWaiterDialog() {
@@ -96,6 +97,7 @@ export async function openBillDialogInternal() {
document.body.style.overflow = "hidden"; // Zablokuj scroll tła
document.getElementById("billLoading").classList.remove("hidden");
document.getElementById("billLoading").textContent = t("bill.loading");
document.getElementById("billListContainer").classList.add("hidden");
goToStep("stepBillList");
@@ -115,10 +117,10 @@ export async function openBillDialogInternal() {
document.getElementById("btnBackToBills").style.display = "block";
}
} else {
document.getElementById("billLoading").innerHTML = "Brak otwartych rachunków do opłacenia.";
document.getElementById("billLoading").innerHTML = t("bill.loading_empty");
}
} catch (err) {
document.getElementById("billLoading").innerHTML = "Błąd pobierania rachunków.";
document.getElementById("billLoading").innerHTML = t("bill.loading_error");
}
}
@@ -137,7 +139,7 @@ function renderBillList(bills) {
div.style.padding = "15px";
div.onclick = () => showBillReview(b);
const numerFormat = b.numer ? `#${b.numer}` : "Rachunek";
const numerFormat = b.numer ? `#${b.numer}` : t("bill.bill_fallback");
div.innerHTML = `
<div>
<div style="font-weight:bold;">${numerFormat}</div>
@@ -212,7 +214,7 @@ export async function selectDocument(docType) {
if (queued.reason === "pending") {
showToast(guestActionBlockedMessage("bill_request"));
} else {
showToast("Nie udało się wysłać prośby o rachunek. Spróbuj ponownie za chwilę.");
showToast(t("bill.toast_fail"));
}
return;
}
@@ -224,7 +226,7 @@ export async function selectDocument(docType) {
payment: billState.payment,
doc: "paragon",
});
showToast("Kelner przyniesie paragon do opłacenia!");
showToast(t("bill.toast_receipt"));
} else {
goToStep("stepNIP");
document.getElementById("nipInput").value = "";
@@ -235,11 +237,11 @@ export async function selectDocument(docType) {
export async function fetchGUS() {
const nip = document.getElementById("nipInput").value.replace(/[\s-]/g, "");
if (nip.length < 10) {
alert("Wprowadź poprawny numer NIP.");
alert(t("bill.nip_invalid"));
return;
}
const btn = document.getElementById("btnGUS");
btn.textContent = "Szukam...";
btn.textContent = t("bill.gus_searching");
btn.disabled = true;
try {
@@ -267,24 +269,24 @@ export async function fetchGUS() {
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;
document.getElementById("cmpNip").value = `${t("bill.queue_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";
document.getElementById("btnEditCompany").textContent = t("bill.edit_company");
goToStep("stepVerify");
} else {
alert("Nie udało się pobrać danych z GUS dla podanego NIP-u.");
alert(t("bill.gus_fail"));
}
} catch (error) {
console.error("Błąd pobierania danych z GUS:", error);
alert("Błąd połączenia z API GUS.");
alert(t("bill.gus_error"));
} finally {
btn.textContent = "Pobierz z GUS";
btn.textContent = t("bill.gus");
btn.disabled = false;
}
}
@@ -302,13 +304,13 @@ export function editCompanyData() {
z.readOnly = false;
c.readOnly = false;
n.focus();
btn.textContent = "Zakończ edycję";
btn.textContent = t("bill.edit_done");
} else {
n.readOnly = true;
s.readOnly = true;
z.readOnly = true;
c.readOnly = true;
btn.textContent = "Popraw ręcznie";
btn.textContent = t("bill.edit_company");
}
}
@@ -334,7 +336,7 @@ export async function confirmInvoice() {
if (queued.reason === "pending") {
showToast(guestActionBlockedMessage("bill_request"));
} else {
showToast("Nie udało się wysłać prośby o rachunek. Spróbuj ponownie za chwilę.");
showToast(t("bill.toast_fail"));
}
return;
}
@@ -349,5 +351,5 @@ export async function confirmInvoice() {
nip: billState.nip,
company: billState.company,
});
showToast("Dziękujemy! Prośba o fakturę została wysłana.");
showToast(t("bill.toast_invoice"));
}
+9 -3
View File
@@ -11,9 +11,15 @@ export const endpoints = {
gusLookup: cfg.endpoints?.gusLookup || "../api/gus_lookup.php",
kds: cfg.endpoints?.kds || "../api/kds.php",
bills: cfg.endpoints?.bills || "../api/bills.php",
menu: cfg.endpoints?.menu || "../api/menu.php",
};
export const loaderMinMs = Number(cfg.loaderMinMs) || 10_000;
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 MENU_ASSET_VERSION =
window.MENU_ASSET_VERSION || window.APP_ASSET_VERSION || "1";
export const loaderMinMs = Number(cfg.loaderMinMs) || 10_000;
+40 -61
View File
@@ -1,4 +1,5 @@
import { endpoints, geoBypassHosts } from "./config.js";
import { t } from "./i18n.js";
import { trackEvent } from "./analytics.js";
import {
runPendingProtectedAction,
@@ -109,21 +110,15 @@ function showGreeting(name, firstVisitTime) {
}
}
function isIOSDevice() {
return (
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1)
);
function GEO_GATE_LABELS() {
return {
status: t("geo.feature.status"),
waiter: t("geo.feature.waiter"),
bill: t("geo.feature.bill"),
};
}
const GEO_GATE_LABELS = {
status: "status zamówienia",
waiter: "wezwanie kelnera",
bill: "prośbę o rachunek",
};
const GEO_DEFAULT_LEAD =
"Przeglądaj menu od razu — albo potwierdź, że jesteś u nas, aby wezwać kelnera, śledzić zamówienie i poprosić o rachunek.";
const GEO_DEFAULT_LEAD = () => t("geo.lead");
function setGeoLead(html) {
const el = document.getElementById("geoLead");
@@ -155,7 +150,7 @@ function setGeoActionBusy(busy) {
btn.disabled = false;
btn.setAttribute("aria-busy", busy ? "true" : "false");
if (busy) {
setGeoActionLabel("Sprawdzanie…");
setGeoActionLabel(t("geo.btn.checking"));
}
}
@@ -172,19 +167,7 @@ function setGeoActionLabel(text) {
}
function getGeoPermissionInstructions() {
if (isIOSDevice()) {
return `<b>iPhone (Safari):</b><br>
1. Kliknij <b>aA</b> po lewej stronie paska adresu.<br>
2. Wybierz <b>Ustawienia witryny</b>.<br>
3. Ustaw <b>Położenie</b> na „Zapytaj” lub „Pozwalaj”.<br>
4. Odśwież stronę.<br><br>
<i>Lokalizacja działa tylko przez bezpieczne <b>https://</b>.</i>`;
}
return `<b>Android / Chrome:</b><br>
1. Kliknij ikonę <b>kłódki</b> obok adresu strony.<br>
2. W <b>Uprawnieniach</b> zmień Lokalizację na „Zezwalaj”.<br>
3. Odśwież stronę.`;
return t("geo.hint.permission");
}
let geoMenuButtonMode = "menu_only";
@@ -231,7 +214,7 @@ function configureGeoSecondaryButton(mode) {
if (mode === "back_to_menu") {
menuOnlyBtn.style.display = "";
if (mainEl) mainEl.textContent = "Wróć do menu";
if (mainEl) mainEl.textContent = t("geo.btn.back_menu");
if (subEl) {
subEl.textContent = "";
subEl.style.display = "none";
@@ -240,9 +223,9 @@ function configureGeoSecondaryButton(mode) {
}
menuOnlyBtn.style.display = "";
if (mainEl) mainEl.textContent = "Przejdź do menu";
if (mainEl) mainEl.textContent = t("geo.btn.menu_main");
if (subEl) {
subEl.textContent = "bez lokalizacji";
subEl.textContent = t("geo.btn.menu_sub");
subEl.style.display = "";
}
}
@@ -255,13 +238,13 @@ export function showGeoGateForAction(action) {
if (loadingScreen) loadingScreen.classList.add("hidden");
if (geoScreen) geoScreen.classList.remove("hidden");
const feature = GEO_GATE_LABELS[action] || "tę funkcję";
setGeoLead(`Menu masz już otwarte. Aby skorzystać z <b>${feature}</b>, potwierdź krótko, że jesteś w restauracji.`);
const feature = GEO_GATE_LABELS()[action] || t("geo.feature.other");
setGeoLead(t("geo.lead_action", { feature }));
setGeoStatus("");
hideGeoInstructions();
if (geoActionBtn) {
setGeoActionBusy(false);
setGeoActionLabel("Sprawdź lokalizację");
setGeoActionLabel(t("geo.btn.check"));
}
configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only");
}
@@ -425,24 +408,22 @@ function showGeoConsentScreen() {
loadingScreen.classList.add("hidden");
geoScreen.classList.remove("hidden");
setGeoLead(GEO_DEFAULT_LEAD);
setGeoLead(GEO_DEFAULT_LEAD());
setGeoStatus("");
hideGeoInstructions();
if (geoActionBtn) {
setGeoActionBusy(false);
setGeoActionLabel("Zgoda, sprawdź lokalizację");
setGeoActionLabel(t("geo.btn.locate"));
}
configureGeoSecondaryButton("menu_only");
}
function showGeoPermissionBlockedState() {
setGeoStatus("Przeglądarka zablokowała dostęp do lokalizacji.", { error: true });
showGeoInstructions(
`${getGeoPermissionInstructions()}<br><br>Po zmianie ustawień <b>odśwież stronę</b>, a potem kliknij „Spróbuj ponownie”.`
);
setGeoStatus(t("geo.status.blocked"), { error: true });
showGeoInstructions(getGeoPermissionInstructions());
setGeoActionBusy(false);
setGeoActionLabel("Spróbuj ponownie");
setGeoActionLabel(t("geo.btn.retry"));
configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only");
}
@@ -539,26 +520,24 @@ export async function initGeolocationAfterBypassChecks(options = {}) {
configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only");
if (!window.isSecureContext) {
setGeoLead(GEO_DEFAULT_LEAD);
setGeoStatus("Ta strona wymaga bezpiecznego połączenia HTTPS.", { error: true });
showGeoInstructions(
"Przeglądarki mobilne blokują geolokalizację bez <b>https://</b>. Otwórz aplikację przez HTTPS i spróbuj ponownie."
);
setGeoLead(GEO_DEFAULT_LEAD());
setGeoStatus(t("geo.status.https"), { error: true });
showGeoInstructions(t("geo.hint.https"));
setGeoActionBusy(false);
setGeoActionLabel("Spróbuj ponownie");
setGeoActionLabel(t("geo.btn.retry"));
return;
}
if (!navigator.geolocation) {
setGeoLead(GEO_DEFAULT_LEAD);
setGeoStatus("Twoja przeglądarka nie wspiera geolokalizacji.", { error: true });
setGeoLead(GEO_DEFAULT_LEAD());
setGeoStatus(t("geo.status.unsupported"), { error: true });
hideGeoInstructions();
setGeoActionBusy(false);
return;
}
setGeoLead(GEO_DEFAULT_LEAD);
setGeoStatus("Sprawdzamy Twoją lokalizację…", { info: true });
setGeoLead(GEO_DEFAULT_LEAD());
setGeoStatus(t("geo.status.checking"), { info: true });
hideGeoInstructions();
setGeoActionBusy(true);
@@ -595,16 +574,16 @@ export async function initGeolocationAfterBypassChecks(options = {}) {
accuracyMeters: Math.round(accuracy),
});
setGeoActionBusy(false);
setGeoActionLabel("Spróbuj ponownie");
setGeoActionLabel(t("geo.btn.retry"));
configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only");
setGeoStatus(
`Wygląda na to, że jesteś poza restauracją (ok. ${Math.round(dist)} m, dokładność GPS: ±${Math.round(accuracy)} m).`,
t("geo.status.outside", {
dist: Math.round(dist),
accuracy: Math.round(accuracy),
}),
{ error: true }
);
showGeoInstructions(
"Przeglądarka często podaje inną lokalizację niż aplikacja Map Google. " +
"Spróbuj ponownie na zewnątrz lub bliżej okna — albo przejdź do menu bez lokalizacji."
);
showGeoInstructions(t("geo.hint.outside"));
} catch (error) {
trackEvent("geo_check_failed", {
reason: "browser_error",
@@ -612,18 +591,18 @@ export async function initGeolocationAfterBypassChecks(options = {}) {
message: String(error.message || ""),
});
setGeoActionBusy(false);
setGeoActionLabel("Spróbuj ponownie");
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("Geolokalizacja wymaga HTTPS.", { error: true });
showGeoInstructions("Otwórz aplikację przez bezpieczny adres <b>https://</b> i spróbuj ponownie.");
setGeoStatus(t("geo.status.https_short"), { error: true });
showGeoInstructions(t("geo.hint.https"));
} else if (isGeoPermissionDenied(error)) {
showGeoPermissionBlockedState();
} else {
setGeoStatus("Nie udało się pobrać lokalizacji.", { error: true });
showGeoInstructions("Sprawdź zasięg, włącz GPS i spróbuj ponownie.");
setGeoStatus(t("geo.status.failed"), { error: true });
showGeoInstructions(t("geo.hint.retry"));
}
}
}
+191
View File
@@ -0,0 +1,191 @@
import pl from "../locales/pl.js";
import en from "../locales/en.js";
import de from "../locales/de.js";
const STORAGE_KEY = "karczma_lang";
const catalogs = { pl, en, de };
let currentLang = "pl";
/** @type {Set<(lang: string) => void>} */
const listeners = new Set();
function interpolate(template, vars = {}) {
return String(template).replace(/\{(\w+)\}/g, (_, key) => {
return vars[key] != null ? String(vars[key]) : `{${key}}`;
});
}
export function getAvailableLanguages() {
const fromConfig = window.APP_CONFIG?.languages;
if (Array.isArray(fromConfig) && fromConfig.length) {
return fromConfig;
}
return [
{ code: "pl", label: "Polski", flag: "🇵🇱" },
{ code: "en", label: "English", flag: "🇬🇧" },
{ code: "de", label: "Deutsch", flag: "🇩🇪" },
];
}
export function getLang() {
return currentLang;
}
export function t(key, vars = {}) {
const catalog = catalogs[currentLang] || catalogs.pl;
const fallback = catalogs.pl;
const value = catalog[key] ?? fallback[key] ?? key;
return interpolate(value, vars);
}
export function applyTranslations(root = document) {
root.querySelectorAll("[data-i18n]").forEach((el) => {
const key = el.getAttribute("data-i18n");
if (!key) return;
const html = el.hasAttribute("data-i18n-html");
const text = t(key);
if (html) {
el.innerHTML = text;
} else {
el.textContent = text;
}
});
root.querySelectorAll("[data-i18n-placeholder]").forEach((el) => {
const key = el.getAttribute("data-i18n-placeholder");
if (!key) return;
el.setAttribute("placeholder", t(key));
});
root.querySelectorAll("[data-i18n-aria]").forEach((el) => {
const key = el.getAttribute("data-i18n-aria");
if (!key) return;
el.setAttribute("aria-label", t(key));
});
document.documentElement.lang = t("html.lang");
document.title = t("doc.title");
}
export function onLangChange(callback) {
listeners.add(callback);
return () => listeners.delete(callback);
}
export function setLang(lang, { persist = true, notify = true } = {}) {
const next = catalogs[lang] ? lang : "pl";
const changed = next !== currentLang;
currentLang = next;
if (persist) {
try {
localStorage.setItem(STORAGE_KEY, currentLang);
} catch {
// ignore
}
}
applyTranslations();
if (notify && changed) {
listeners.forEach((cb) => {
try {
cb(currentLang);
} catch (err) {
console.warn("[i18n] listener failed", err);
}
});
}
return currentLang;
}
export function initI18n() {
let stored = "";
try {
stored = localStorage.getItem(STORAGE_KEY) || "";
} catch {
stored = "";
}
const initial = catalogs[stored] ? stored : "pl";
return setLang(initial, { persist: true, notify: false });
}
function flagImgHtml(code) {
const safe = String(code || "pl").toLowerCase().replace(/[^a-z]/g, "") || "pl";
return `<img class="lang-flag-img" src="assets/img/flags/${safe}.svg" alt="" width="28" height="20" decoding="async">`;
}
export function createLanguagePicker({ idPrefix = "lang" } = {}) {
const wrap = document.createElement("div");
wrap.className = "lang-picker";
wrap.dataset.langPicker = "1";
const btn = document.createElement("button");
btn.type = "button";
btn.className = "lang-picker-btn";
btn.id = `${idPrefix}Btn`;
btn.setAttribute("aria-haspopup", "listbox");
btn.setAttribute("aria-expanded", "false");
const menu = document.createElement("div");
menu.className = "lang-picker-menu hidden";
menu.id = `${idPrefix}Menu`;
menu.setAttribute("role", "listbox");
function refresh() {
const langs = getAvailableLanguages();
const current = langs.find((l) => l.code === getLang()) || langs[0];
const code = current?.code || "pl";
btn.innerHTML = flagImgHtml(code);
btn.setAttribute("aria-label", `${t("lang.aria")}: ${current?.label || code}`);
menu.innerHTML = "";
langs.forEach((lang) => {
const option = document.createElement("button");
option.type = "button";
option.className = "lang-picker-option" + (lang.code === getLang() ? " is-active" : "");
option.setAttribute("role", "option");
option.setAttribute("aria-selected", lang.code === getLang() ? "true" : "false");
option.innerHTML = `${flagImgHtml(lang.code)}<span>${lang.label}</span>`;
option.addEventListener("click", (e) => {
e.stopPropagation();
close();
if (lang.code !== getLang()) {
setLang(lang.code);
}
});
menu.appendChild(option);
});
}
function open() {
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");
}
btn.addEventListener("click", (e) => {
e.stopPropagation();
if (menu.classList.contains("hidden")) open();
else close();
});
document.addEventListener("click", (e) => {
if (!wrap.contains(e.target)) close();
});
wrap.appendChild(btn);
wrap.appendChild(menu);
refresh();
onLangChange(() => refresh());
return { el: wrap, refresh, close };
}
+73 -31
View File
@@ -1,4 +1,5 @@
import { MENU_ASSET_VERSION } from "./config.js";
import { endpoints } from "./config.js";
import { getLang, onLangChange, t } from "./i18n.js";
import { trackEvent } from "./analytics.js";
let itemModalKeys = [];
@@ -6,6 +7,7 @@ let itemModalIndex = -1;
let itemModalTouchStart = null;
let itemModalDragging = false;
let itemModalAnimating = false;
let menuLangChangeBound = false;
function resetItemModalPane() {
const pane = document.getElementById("itemModalPane");
@@ -78,13 +80,13 @@ function renderMenuListImage(url) {
const srcAttr = hasUrl ? ` src="${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>Brak zdjęcia</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>${t("menu.no_image")}</span></div></div>`;
}
function findMenuItem(categoryId, position) {
if (!window.menuDataRaw) return null;
for (const cat of window.menuDataRaw) {
for (const item of cat.items) {
for (const item of cat.items || []) {
if (item.categoryId == categoryId && item.position == position) {
return item;
}
@@ -243,33 +245,62 @@ export function bindItemModalSwipe() {
});
}
export async function loadMenu() {
function renderCategoryNav(categories) {
const ul = document.querySelector(".menu-categories-nav ul");
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>`;
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>`;
});
ul.innerHTML = html;
}
function showMenuLoadError(container) {
if (!container) return;
container.innerHTML = `<p style="text-align:center; padding: 20px; color: var(--text-muted);">${t("menu.load_error")}</p>`;
}
export async function loadMenu(lang = getLang()) {
const container = document.getElementById("menuContainer");
try {
const response = await fetch(`menu.json?v=${encodeURIComponent(MENU_ASSET_VERSION)}`);
if (!response.ok) throw new Error("Nie udało się załadować menu");
const menuData = await response.json();
window.menuDataRaw = menuData;
const container = document.getElementById("menuContainer");
const response = await fetch(`${endpoints.menu}?lang=${encodeURIComponent(lang)}`);
if (!response.ok) throw new Error(t("menu.load_error"));
const payload = await response.json();
if (payload.status !== "success" || !Array.isArray(payload.sections)) {
throw new Error(t("menu.load_error"));
}
const sections = payload.sections;
window.menuDataRaw = sections;
if (!container) return;
container.innerHTML = "";
menuData.forEach((category) => {
const catId = category.items.length > 0 ? category.items[0].categoryId : "";
sections.forEach((section) => {
const items = Array.isArray(section.items) ? section.items : [];
const catId = items.length > 0 ? 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>
let html = `<div class="restaurant-menu-category">${section.categoryName || ""}</div>
<div class="rmc-positions">`;
category.items.forEach((item) => {
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;">
${renderMenuListImage(item.image)}
<div class="rmc-title">
<h4>${item.title}<span>${item.description}</span></h4>
<h4>${item.title}<span>${item.description || ""}</span></h4>
</div>
<div class="rmc-other"><span>${item.price}</span></div>
</div>
@@ -280,16 +311,28 @@ export async function loadMenu() {
catDiv.innerHTML = html;
container.appendChild(catDiv);
});
renderCategoryNav(payload.categories);
showCategory(0);
const searchInput = document.getElementById("menuSearchInput");
if (searchInput?.value) {
filterMenu();
}
} 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>';
}
showMenuLoadError(container);
}
}
export function setMenuLanguageReload() {
if (menuLangChangeBound) return;
menuLangChangeBound = true;
onLangChange(() => loadMenu());
}
setMenuLanguageReload();
export function openItemModal(categoryId, position) {
itemModalKeys = buildVisibleMenuItemKeys();
itemModalIndex = itemModalKeys.findIndex(
@@ -366,17 +409,16 @@ export function showCategory(categoryId) {
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";
}
}
const showAll = categoryId === 0 || categoryId === "0";
const targetId = String(categoryId);
document.querySelectorAll(".rm-category").forEach((section) => {
let hasVisible = false;
section.querySelectorAll(".rmc-position").forEach((item) => {
const match = showAll || String(item.getAttribute("data-category-id")) === targetId;
item.style.display = match ? "" : "none";
if (match) hasVisible = true;
});
section.style.display = hasVisible ? "" : "none";
});
}
+43 -29
View File
@@ -1,4 +1,5 @@
import { endpoints, loaderMinMs } from "./config.js";
import { getLang, t } from "./i18n.js";
import { runPendingProtectedAction, updateNavAccessState } from "./access.js";
import {
refreshGuestPendingActions,
@@ -30,17 +31,29 @@ const SIX_MONTHS_MS = 180 * 24 * 60 * 60 * 1000;
const LOADER_MIN_MS = loaderMinMs;
const loadStartTime = Date.now();
const msgs = ["Rozgrzewamy piece...", "Szef kuchni sprawdza składniki...", "Łączenie z sercem restauracji...", "Prawie gotowe..."];
function getLoaderMsgs() {
return [t("loader.msg1"), t("loader.msg2"), t("loader.msg3"), t("loader.msg4")];
}
let msgIdx = 0;
const msgInterval = setInterval(() => {
const msgs = getLoaderMsgs();
msgIdx = (msgIdx + 1) % msgs.length;
if (loaderMsg) loaderMsg.textContent = msgs[msgIdx];
}, 4000);
function formatTableLabel(name) {
const raw = String(name || "").trim();
if (!raw) return t("table.label", { name: "" }).trim();
if (raw.toUpperCase().startsWith("STOLIK") || raw.toUpperCase().startsWith("TABLE") || raw.toUpperCase().startsWith("TISCH")) {
return raw;
}
return t("table.label", { name: raw });
}
// Initial State
if (getTableParam()) {
const tableParam = getTableParam();
tableLabel.textContent = tableParam.toUpperCase().startsWith("STOLIK") ? tableParam : `Stolik ${tableParam}`;
tableLabel.textContent = formatTableLabel(getTableParam());
}
/** Storage key must follow current hash (fallback: table), not a frozen empty tableParam at init. */
@@ -73,9 +86,7 @@ export async function resolveTableLabel() {
const response = await fetch(`${endpoints.kds}?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}`;
tableLabel.textContent = formatTableLabel(result.tableName);
setTableParam(result.tableName);
}
} catch {
@@ -106,16 +117,16 @@ function showEmptyState() {
emptyState.classList.remove("hidden");
itemsList.innerHTML = "";
prepStatus.textContent = "Brak aktywnych zamówień";
prepStatus.textContent = t("status.none");
statusIcon.textContent = "🍃";
progressBar.style.width = "0%";
statusMeta.textContent = "Zapraszamy do sprawdzenia naszego menu.";
statusMeta.textContent = t("status.none_meta");
// Historia może istnieć nawet gdy brak bieżących pozycji
renderGlobalHistory();
}
function normalizeArticleName(rawName) {
const name = String(rawName || "Pozycja");
const name = String(rawName || t("orders.item_fallback"));
// Usuwa gramatury typu: "300G", "250 G", "500/200/150G".
const withoutWeight = name.replace(
@@ -126,7 +137,7 @@ function normalizeArticleName(rawName) {
return withoutWeight
.replace(/\s{2,}/g, " ")
.replace(/\s+([,.;:!?])/g, "$1")
.trim() || "Pozycja";
.trim() || t("orders.item_fallback");
}
function loadPersistedItems() {
@@ -202,6 +213,7 @@ function addItemsToGlobalHistory(items, sourceTable) {
export function renderGlobalHistory() {
const now = Date.now();
const lang = getLang();
const history = loadGlobalHistory()
.filter((e) => now - (e.archivedAt || 0) <= SIX_MONTHS_MS)
.sort((a, b) => (b.archivedAt || 0) - (a.archivedAt || 0));
@@ -224,7 +236,11 @@ export function renderGlobalHistory() {
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>
<span class="item-meta">${t("history.entry_meta", {
table: formatTableLabel(entry.sourceTable || "?"),
date: dt.toLocaleDateString(lang),
time: dt.toLocaleTimeString(lang, { hour: "2-digit", minute: "2-digit" }),
})}</span>
</div>
<div class="item-qty">x${entry.qty}</div>
`;
@@ -234,7 +250,7 @@ export function renderGlobalHistory() {
export function clearGlobalHistory(e) {
if (e) e.preventDefault();
if (confirm("Czy na pewno chcesz usunąć historię swoich poprzednich zamówień?")) {
if (confirm(t("history.clear_confirm"))) {
localStorage.removeItem(historyKey);
renderGlobalHistory();
}
@@ -282,7 +298,7 @@ function mergeWithPersistedItems(articles) {
// 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");
return a.name.localeCompare(b.name, getLang());
});
savePersistedItems(merged);
@@ -301,11 +317,11 @@ function renderItems(items) {
const div = document.createElement("div");
div.className = `item-card ${isReady ? "ready" : ""} ${item.present ? "" : "archived"}`;
let meta = "🔥 W przygotowaniu";
let meta = t("orders.item_preparing");
if (isReady && item.completedByDisappear) {
meta = "✅ Gotowe (zrealizowane)";
meta = t("orders.item_ready_done");
} else if (isReady) {
meta = "✅ Gotowe";
meta = t("orders.item_ready");
}
div.innerHTML = `
@@ -333,29 +349,29 @@ function updateStatus(bills, items) {
progressBar.style.width = `${pct}%`;
if (pct >= 100) {
prepStatus.textContent = "Gotowe do podania!";
prepStatus.textContent = t("status.ready");
statusIcon.innerHTML = "😋";
statusMeta.textContent = "Wszystkie Twoje dania opuściły już kuchnię.";
statusMeta.textContent = t("status.ready_meta");
} else if (pct > 0) {
prepStatus.textContent = "Częściowo gotowe";
prepStatus.textContent = t("status.partial");
statusIcon.innerHTML = "🍳";
statusMeta.textContent = "Pierwsze pyszności już na Ciebie czekają!";
statusMeta.textContent = t("status.partial_meta");
} else {
prepStatus.textContent = "W przygotowaniu";
prepStatus.textContent = t("status.preparing");
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.";
statusMeta.textContent = t("status.preparing_meta");
}
// 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" })
? new Date(newest.Date).toLocaleTimeString(getLang(), { hour: "2-digit", minute: "2-digit" })
: "--:--";
metaFooter.textContent = `Zamówienie złożone o godzinie ${time} • Stolik ${getTableParam()}`;
metaFooter.textContent = t("status.footer", { time, table: getTableParam() });
}
export async function fetchOrders() {
@@ -371,9 +387,7 @@ export async function fetchOrders() {
if (result.status === "success") {
if (result.tableName && result.tableName !== "") {
tableLabel.textContent = result.tableName.toUpperCase().startsWith("STOLIK")
? result.tableName
: `Stolik ${result.tableName}`;
tableLabel.textContent = formatTableLabel(result.tableName);
setTableParam(result.tableName); // Aktualizacja do właściwej nazwy na poczet innych zapytań
refreshGuestPendingActions();
startGuestPendingPoll();
@@ -419,9 +433,9 @@ export async function fetchOrders() {
},
]);
} else {
loaderMsg.textContent = "Błąd API: " + result.message;
loaderMsg.textContent = t("loader.api_error", { message: result.message });
}
} catch (err) {
loaderMsg.textContent = "Problem z połączeniem. Próbujemy ponownie...";
loaderMsg.textContent = t("loader.connection");
}
}
+16 -9
View File
@@ -1,4 +1,5 @@
import { endpoints } from "./config.js";
import { t } from "./i18n.js";
import { showToast } from "./toast.js";
import {
getBillState,
@@ -75,36 +76,42 @@ export function formatGuestQueueMessage(title, lines = []) {
}
export function buildWaiterCallQueueMessage() {
return "Przywołanie kelnera";
return t("waiter.queue_title");
}
export function buildBillRequestQueueMessage(docType) {
const billState = getBillState();
const lines = [
{ label: "Forma płatności:", value: billState.payment || "nieznana" },
{ label: "Dokument:", value: docType === "faktura" ? "faktura" : "paragon" },
{
label: t("bill.queue_payment"),
value: billState.payment || t("bill.payment_unknown"),
},
{
label: t("bill.queue_doc"),
value: docType === "faktura" ? t("bill.doc_invoice") : t("bill.doc_receipt"),
},
];
if (docType === "faktura") {
lines.push({ label: "NIP:", value: billState.nip || "—" });
lines.push({ label: "Firma:", value: billState.company?.name || "—" });
lines.push({ label: t("bill.queue_nip"), value: billState.nip || "—" });
lines.push({ label: t("bill.queue_company"), value: billState.company?.name || "—" });
const addressParts = [
billState.company?.street,
[billState.company?.zip, billState.company?.city].filter(Boolean).join(" "),
].filter(Boolean);
if (addressParts.length) {
lines.push({ label: "Adres:", value: addressParts.join(", ") });
lines.push({ label: t("bill.queue_address"), value: addressParts.join(", ") });
}
}
return formatGuestQueueMessage("Prośba o rachunek", lines);
return formatGuestQueueMessage(t("bill.queue_title"), lines);
}
export function guestActionBlockedMessage(messageType) {
if (messageType === "waiter_call") {
return "Kelner został już wezwany. Poczekaj, aż obsługa potwierdzi zgłoszenie na panelu.";
return t("waiter.blocked");
}
return "Prośba o rachunek została już wysłana. Poczekaj, aż obsługa ją obsłuży.";
return t("bill.blocked");
}
export function updateGuestActionNavState() {