Zmiana aplikacji na wersje lang
This commit is contained in:
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+213
@@ -0,0 +1,213 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
|
||||||
|
http_response_code(405);
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Method not allowed',
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = require __DIR__ . '/../config/menu.php';
|
||||||
|
$languages = is_array($config['languages'] ?? null) ? $config['languages'] : [];
|
||||||
|
$lang = strtolower(trim((string) ($_GET['lang'] ?? 'pl')));
|
||||||
|
|
||||||
|
if ($lang === '' || !isset($languages[$lang])) {
|
||||||
|
http_response_code(422);
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Unsupported language',
|
||||||
|
'allowed' => array_keys($languages),
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$langConfig = $languages[$lang];
|
||||||
|
$source = (string) ($langConfig['source'] ?? 'upstream');
|
||||||
|
$ttl = max(60, (int) ($config['ttl_seconds'] ?? 1800));
|
||||||
|
$timeout = max(3, (int) ($config['timeout_seconds'] ?? 12));
|
||||||
|
$cacheDir = __DIR__ . '/cache';
|
||||||
|
$cacheFile = $cacheDir . '/menu_' . $lang . '.json';
|
||||||
|
|
||||||
|
if (!is_dir($cacheDir)) {
|
||||||
|
@mkdir($cacheDir, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function readMenuCache(string $cacheFile): ?array
|
||||||
|
{
|
||||||
|
if (!is_file($cacheFile)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = file_get_contents($cacheFile);
|
||||||
|
if ($raw === false || $raw === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($raw, true);
|
||||||
|
return is_array($decoded) ? $decoded : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeMenuCache(string $cacheFile, array $payload): void
|
||||||
|
{
|
||||||
|
file_put_contents(
|
||||||
|
$cacheFile,
|
||||||
|
json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||||
|
LOCK_EX
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchUpstreamMenu(string $url, int $timeout): ?array
|
||||||
|
{
|
||||||
|
$body = false;
|
||||||
|
$httpCode = 0;
|
||||||
|
|
||||||
|
if (function_exists('curl_init')) {
|
||||||
|
$ch = curl_init($url);
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_FOLLOWLOCATION => true,
|
||||||
|
CURLOPT_TIMEOUT => $timeout,
|
||||||
|
CURLOPT_HTTPHEADER => ['Accept: application/json'],
|
||||||
|
]);
|
||||||
|
$body = curl_exec($ch);
|
||||||
|
$errno = curl_errno($ch);
|
||||||
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||||
|
curl_close($ch);
|
||||||
|
if ($errno !== 0) {
|
||||||
|
$body = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$context = stream_context_create([
|
||||||
|
'http' => [
|
||||||
|
'method' => 'GET',
|
||||||
|
'timeout' => $timeout,
|
||||||
|
'header' => "Accept: application/json\r\n",
|
||||||
|
'ignore_errors' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$body = @file_get_contents($url, false, $context);
|
||||||
|
if (isset($http_response_header[0]) && preg_match('/\s(\d{3})\s/', $http_response_header[0], $m)) {
|
||||||
|
$httpCode = (int) $m[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($body === false || ($httpCode > 0 && ($httpCode < 200 || $httpCode >= 300))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($body, true);
|
||||||
|
return is_array($decoded) ? $decoded : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapUpstreamMenu(array $raw, string $lang, string $source): array
|
||||||
|
{
|
||||||
|
$categories = [];
|
||||||
|
foreach ($raw['categories'] ?? [] as $cat) {
|
||||||
|
if (!is_array($cat)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$id = trim((string) ($cat['id'] ?? ''));
|
||||||
|
$name = trim((string) ($cat['name'] ?? ''));
|
||||||
|
if ($id === '' || $name === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$categories[] = ['id' => $id, 'name' => $name];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sections = [];
|
||||||
|
foreach ($raw['tables'] ?? [] as $table) {
|
||||||
|
if (!is_array($table)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$sectionName = trim((string) ($table['name'] ?? ''));
|
||||||
|
if ($sectionName === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = [];
|
||||||
|
foreach ($table['items'] ?? [] as $item) {
|
||||||
|
if (!is_array($item)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$items[] = [
|
||||||
|
'position' => (string) ($item['id'] ?? ''),
|
||||||
|
'categoryId' => (string) ($item['category'] ?? ''),
|
||||||
|
'tag' => '',
|
||||||
|
'image' => (string) ($item['image'] ?? ''),
|
||||||
|
'title' => trim((string) ($item['name'] ?? '')),
|
||||||
|
'description' => trim((string) ($item['name_extra'] ?? '')),
|
||||||
|
'price' => (string) ($item['price'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$sections[] = [
|
||||||
|
'categoryName' => $sectionName,
|
||||||
|
'items' => $items,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 'success',
|
||||||
|
'lang' => $lang,
|
||||||
|
'source' => $source,
|
||||||
|
'cachedAt' => gmdate('c'),
|
||||||
|
'categories' => $categories,
|
||||||
|
'sections' => $sections,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$cached = readMenuCache($cacheFile);
|
||||||
|
$now = time();
|
||||||
|
$cachedAtTs = 0;
|
||||||
|
if ($cached && !empty($cached['cachedAt'])) {
|
||||||
|
$cachedAtTs = strtotime((string) $cached['cachedAt']) ?: 0;
|
||||||
|
}
|
||||||
|
$isFresh = $cached && $cachedAtTs > 0 && ($now - $cachedAtTs) < $ttl;
|
||||||
|
|
||||||
|
if ($isFresh) {
|
||||||
|
echo json_encode($cached, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($source === 'ai') {
|
||||||
|
if ($cached) {
|
||||||
|
echo json_encode($cached, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
http_response_code(503);
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'AI menu not available yet',
|
||||||
|
'lang' => $lang,
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$base = rtrim((string) ($config['upstream_base'] ?? ''), '/');
|
||||||
|
$url = $base . '/' . rawurlencode($lang);
|
||||||
|
$raw = fetchUpstreamMenu($url, $timeout);
|
||||||
|
|
||||||
|
if ($raw === null) {
|
||||||
|
if ($cached) {
|
||||||
|
echo json_encode($cached, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
http_response_code(502);
|
||||||
|
echo json_encode([
|
||||||
|
'status' => 'error',
|
||||||
|
'message' => 'Failed to fetch menu',
|
||||||
|
'lang' => $lang,
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$mapped = mapUpstreamMenu($raw, $lang, 'upstream');
|
||||||
|
writeMenuCache($cacheFile, $mapped);
|
||||||
|
|
||||||
|
echo json_encode($mapped, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Konfiguracja menu z upstreamu Karczmy + lista języków aplikacji.
|
||||||
|
*/
|
||||||
|
return [
|
||||||
|
'upstream_base' => 'https://www.karczmabiesiada.eu/restaurant_menu/export',
|
||||||
|
'ttl_seconds' => 1800,
|
||||||
|
'timeout_seconds' => 12,
|
||||||
|
'languages' => [
|
||||||
|
'pl' => [
|
||||||
|
'label' => 'Polski',
|
||||||
|
'flag' => '🇵🇱',
|
||||||
|
'source' => 'upstream',
|
||||||
|
],
|
||||||
|
'en' => [
|
||||||
|
'label' => 'English',
|
||||||
|
'flag' => '🇬🇧',
|
||||||
|
'source' => 'upstream',
|
||||||
|
],
|
||||||
|
'de' => [
|
||||||
|
'label' => 'Deutsch',
|
||||||
|
'flag' => '🇩🇪',
|
||||||
|
'source' => 'upstream',
|
||||||
|
],
|
||||||
|
// później: 'uk' => ['label' => 'Українська', 'flag' => '🇺🇦', 'source' => 'ai', 'from' => 'pl'],
|
||||||
|
],
|
||||||
|
];
|
||||||
+87
-116
@@ -6,10 +6,19 @@ header('Expires: 0');
|
|||||||
|
|
||||||
require_once __DIR__ . '/includes/asset_version.php';
|
require_once __DIR__ . '/includes/asset_version.php';
|
||||||
require_once __DIR__ . '/../api/request_ip.php';
|
require_once __DIR__ . '/../api/request_ip.php';
|
||||||
|
$menuConfig = require __DIR__ . '/../config/menu.php';
|
||||||
$publicDir = __DIR__;
|
$publicDir = __DIR__;
|
||||||
$vCss = publicAssetVersion($publicDir, 'assets/css/app.css');
|
$vCss = publicAssetVersion($publicDir, 'assets/css/app.css');
|
||||||
$vJs = publicJsBundleVersion($publicDir, 'assets/js/app.js');
|
$vJs = publicJsBundleVersion($publicDir, 'assets/js/app.js');
|
||||||
$vMenu = publicAssetVersion($publicDir, 'menu.json');
|
|
||||||
|
$languagesForClient = [];
|
||||||
|
foreach (($menuConfig['languages'] ?? []) as $code => $meta) {
|
||||||
|
$languagesForClient[] = [
|
||||||
|
'code' => (string) $code,
|
||||||
|
'label' => (string) ($meta['label'] ?? strtoupper((string) $code)),
|
||||||
|
'flag' => (string) ($meta['flag'] ?? '🏳️'),
|
||||||
|
];
|
||||||
|
}
|
||||||
?><!doctype html>
|
?><!doctype html>
|
||||||
<html lang="pl">
|
<html lang="pl">
|
||||||
|
|
||||||
@@ -34,42 +43,45 @@ $vMenu = publicAssetVersion($publicDir, 'menu.json');
|
|||||||
<div class="loader-icon"></div>
|
<div class="loader-icon"></div>
|
||||||
<div class="loader-text">
|
<div class="loader-text">
|
||||||
<h2>Karczma Biesiada</h2>
|
<h2>Karczma Biesiada</h2>
|
||||||
<div class="loader-msg" id="loaderMsg">Łączenie z kuchnią...</div>
|
<div class="loader-msg" id="loaderMsg" data-i18n="loader.connecting">Łączenie z kuchnią...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="geoScreen" class="hidden">
|
<div id="geoScreen" class="hidden">
|
||||||
<div class="geo-shell">
|
<div class="geo-shell">
|
||||||
|
<div class="geo-lang-row">
|
||||||
|
<div id="geoLangPickerMount"></div>
|
||||||
|
</div>
|
||||||
<div class="geo-icon">📍</div>
|
<div class="geo-icon">📍</div>
|
||||||
<h2 class="geo-title">Witamy w Karcznie</h2>
|
<h2 class="geo-title" data-i18n="geo.title">Witamy w Karcznie</h2>
|
||||||
<p class="geo-lead" id="geoLead">
|
<p class="geo-lead" id="geoLead" data-i18n="geo.lead">
|
||||||
Przeglądaj menu od razu — albo potwierdź, że jesteś u nas, aby wezwać kelnera, śledzić zamówienie i poprosić o rachunek.
|
Przeglądaj menu od razu — albo potwierdź, że jesteś u nas, aby wezwać kelnera, śledzić zamówienie i poprosić o rachunek.
|
||||||
</p>
|
</p>
|
||||||
<p class="geo-status" id="geoMsg"></p>
|
<p class="geo-status" id="geoMsg"></p>
|
||||||
|
|
||||||
<div class="geo-actions" id="geoActions">
|
<div class="geo-actions" id="geoActions">
|
||||||
<button type="button" id="geoMenuOnlyBtn" class="geo-btn geo-btn-menu">
|
<button type="button" id="geoMenuOnlyBtn" class="geo-btn geo-btn-menu">
|
||||||
<span class="geo-btn-main">Przejdź do menu</span>
|
<span class="geo-btn-main" data-i18n="geo.btn.menu_main">Przejdź do menu</span>
|
||||||
<span class="geo-btn-sub">bez lokalizacji</span>
|
<span class="geo-btn-sub" data-i18n="geo.btn.menu_sub">bez lokalizacji</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" id="geoActionBtn" class="geo-btn geo-btn-locate btn btn-primary">
|
<button type="button" id="geoActionBtn" class="geo-btn geo-btn-locate btn btn-primary">
|
||||||
<span class="geo-btn-main">Zgoda, sprawdź lokalizację</span>
|
<span class="geo-btn-main" data-i18n="geo.btn.locate">Zgoda, sprawdź lokalizację</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="geo-instructions hidden" id="geoInstructions" aria-live="polite"></div>
|
<div class="geo-instructions hidden" id="geoInstructions" aria-live="polite"></div>
|
||||||
|
|
||||||
<div class="geo-wifi-callout">
|
<div class="geo-wifi-callout">
|
||||||
<p class="geo-wifi-callout-title">📶 Wejdź bez zgody na lokalizację</p>
|
<p class="geo-wifi-callout-title" data-i18n="geo.wifi.title">📶 Wejdź bez zgody na lokalizację</p>
|
||||||
<p>Połącz telefon z siecią Wi‑Fi restauracji:</p>
|
<p data-i18n="geo.wifi.connect">Połącz telefon z siecią Wi‑Fi restauracji:</p>
|
||||||
<p class="geo-wifi-network"><strong>HotSpot Karczmy</strong></p>
|
<p class="geo-wifi-network"><strong>HotSpot Karczmy</strong></p>
|
||||||
<p class="geo-wifi-password">Hasło: <strong>karczmabiesiada</strong></p>
|
<p class="geo-wifi-password">Hasło: <strong>karczmabiesiada</strong></p>
|
||||||
<p>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:</p>
|
<p data-i18n="geo.wifi.after" data-i18n-html>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:</p>
|
||||||
<ul class="geo-wifi-list">
|
<ul class="geo-wifi-list">
|
||||||
<li>wezwania kelnera</li>
|
<li data-i18n="geo.wifi.li1">wezwania kelnera</li>
|
||||||
<li>prośby o rachunek</li>
|
<li data-i18n="geo.wifi.li2">prośby o rachunek</li>
|
||||||
<li>statusu zamówienia</li>
|
<li data-i18n="geo.wifi.li3">statusu zamówienia</li>
|
||||||
<li>całej aplikacji przy stoliku</li>
|
<li data-i18n="geo.wifi.li4">całej aplikacji przy stoliku</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,12 +89,12 @@ $vMenu = publicAssetVersion($publicDir, 'menu.json');
|
|||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<header id="mainHeader">
|
<header id="mainHeader">
|
||||||
<h1 class="logo-text">Karczma Biesiada</h1>
|
<div class="header-top">
|
||||||
<div id="tableLabel" class="table-badge">Wybierz stolik</div>
|
<h1 class="logo-text">Karczma Biesiada</h1>
|
||||||
|
<div id="headerLangPickerMount"></div>
|
||||||
|
</div>
|
||||||
|
<div id="tableLabel" class="table-badge" data-i18n="table.choose">Wybierz stolik</div>
|
||||||
</header>
|
</header>
|
||||||
<!-- <div id="greetingBanner"
|
|
||||||
style="display:none; text-align:center; padding: 10px; font-weight:600; color:var(--primary); font-family:'Playfair Display', serif; font-size:18px;">
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<main id="mainContent">
|
<main id="mainContent">
|
||||||
<div id="statusView" class="view-section active">
|
<div id="statusView" class="view-section active">
|
||||||
@@ -90,8 +102,8 @@ $vMenu = publicAssetVersion($publicDir, 'menu.json');
|
|||||||
<section class="status-card">
|
<section class="status-card">
|
||||||
<div class="status-header">
|
<div class="status-header">
|
||||||
<div>
|
<div>
|
||||||
<span class="status-title">Aktualny status</span>
|
<span class="status-title" data-i18n="status.title">Aktualny status</span>
|
||||||
<div id="prepStatus" class="status-value">Oczekiwanie...</div>
|
<div id="prepStatus" class="status-value" data-i18n="status.waiting">Oczekiwanie...</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="statusIcon" style="font-size: 28px;">⏳</div>
|
<div id="statusIcon" style="font-size: 28px;">⏳</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -100,61 +112,51 @@ $vMenu = publicAssetVersion($publicDir, 'menu.json');
|
|||||||
<div id="progressBar" class="progress-bar"></div>
|
<div id="progressBar" class="progress-bar"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="statusMeta" style="font-size: 12px; color: var(--text-muted);">
|
<div id="statusMeta" style="font-size: 12px; color: var(--text-muted);" data-i18n="status.checking">
|
||||||
Sprawdzamy co pysznego się przygotowuje...
|
Sprawdzamy co pysznego się przygotowuje...
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="items-container" id="ordersContainer">
|
<section class="items-container" id="ordersContainer">
|
||||||
<h3 id="ordersTitle">Twoje zamówione dania</h3>
|
<h3 id="ordersTitle" data-i18n="orders.title">Twoje zamówione dania</h3>
|
||||||
<div id="emptyState" class="empty-state hidden">
|
<div id="emptyState" class="empty-state hidden">
|
||||||
<div class="empty-icon">📖</div>
|
<div class="empty-icon">📖</div>
|
||||||
<p style="color: var(--text-muted)">Jeśli właśnie złożyłeś zamówienie, daj nam chwilkę na jego
|
<p style="color: var(--text-muted)" data-i18n="orders.empty">Jeśli właśnie złożyłeś zamówienie, daj nam chwilkę na jego
|
||||||
przetworzenie.</p>
|
przetworzenie.</p>
|
||||||
<button class="btn btn-primary" style="margin-top: 15px; padding: 12px 20px; font-size: 15px;"
|
<button class="btn btn-primary" style="margin-top: 15px; padding: 12px 20px; font-size: 15px;"
|
||||||
onclick="switchTab('menu')">Przeglądaj menu</button>
|
onclick="switchTab('menu')" data-i18n="orders.browse_menu">Przeglądaj menu</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="itemsList"></div>
|
<div id="itemsList"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="historySection" class="items-container history-section hidden">
|
<section id="historySection" class="items-container history-section hidden">
|
||||||
<h3>Twoje poprzednie zamówienia</h3>
|
<h3 data-i18n="history.title">Twoje poprzednie zamówienia</h3>
|
||||||
<p class="history-note">To są pozycje z innych wizyt, które były widoczne na tym telefonie po zeskanowaniu
|
<p class="history-note" data-i18n="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 🙂</p>
|
kodów QR. Jeśli kiedyś byłeś w restauracji bez skanowania kodu, tych pozycji tu nie będzie 🙂</p>
|
||||||
<div id="historyList"></div>
|
<div id="historyList"></div>
|
||||||
<div style="text-align: center; margin-top: 15px;">
|
<div style="text-align: center; margin-top: 15px;">
|
||||||
<a href="#" onclick="clearGlobalHistory(event)"
|
<a href="#" onclick="clearGlobalHistory(event)"
|
||||||
style="font-size: 12px; color: var(--text-muted); text-decoration: underline;">Usuń historię</a>
|
style="font-size: 12px; color: var(--text-muted); text-decoration: underline;" data-i18n="history.clear">Usuń historię</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div id="metaFooter" class="meta-footer"></div>
|
<div id="metaFooter" class="meta-footer"></div>
|
||||||
</div> <!-- Koniec statusView -->
|
</div>
|
||||||
|
|
||||||
<div id="menuView" class="view-section hidden">
|
<div id="menuView" class="view-section hidden">
|
||||||
<div id="menuOnlyBanner" class="menu-only-banner is-hidden">
|
<div id="menuOnlyBanner" class="menu-only-banner is-hidden">
|
||||||
<span>Potwierdź lokalizację, aby wezwać kelnera lub poprosić o rachunek.</span>
|
<span data-i18n="menu.banner">Potwierdź lokalizację, aby wezwać kelnera lub poprosić o rachunek.</span>
|
||||||
<button type="button" class="menu-only-banner-btn" onclick="promptGeoForFullAccess()">Sprawdź teraz</button>
|
<button type="button" class="menu-only-banner-btn" onclick="promptGeoForFullAccess()" data-i18n="menu.banner_btn">Sprawdź teraz</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="menu-search-container">
|
<div class="menu-search-container">
|
||||||
<input type="text" id="menuSearchInput" placeholder="Szukaj dania..." oninput="filterMenu()" />
|
<input type="text" id="menuSearchInput" data-i18n-placeholder="menu.search" placeholder="Szukaj dania..." oninput="filterMenu()" />
|
||||||
|
<div id="menuLangPickerMount"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="restaurant-menu-container">
|
<div class="restaurant-menu-container">
|
||||||
<nav class="menu-categories-nav">
|
<nav class="menu-categories-nav">
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="#" class="active" data-category-badge="0" onclick="showCategory(0)">Wszystko</a></li>
|
<!-- Dynamic categories -->
|
||||||
<li><a href="#" onclick="showCategory(1)" data-category-badge="1">Przystawki</a></li>
|
|
||||||
<li><a href="#" onclick="showCategory(2)" data-category-badge="2">Zupy</a></li>
|
|
||||||
<li><a href="#" onclick="showCategory(3)" data-category-badge="3">Dania główne</a></li>
|
|
||||||
<li><a href="#" onclick="showCategory(4)" data-category-badge="4">Dania swojskie</a></li>
|
|
||||||
<li><a href="#" onclick="showCategory(5)" data-category-badge="5">Ryby</a></li>
|
|
||||||
<li><a href="#" onclick="showCategory(7)" data-category-badge="7">Sałatki</a></li>
|
|
||||||
<li><a href="#" onclick="showCategory(6)" data-category-badge="6">Makarony</a></li>
|
|
||||||
<li><a href="#" onclick="showCategory(9)" data-category-badge="9">Dla dzieci</a></li>
|
|
||||||
<li><a href="#" onclick="showCategory(8)" data-category-badge="8">Dodatki</a></li>
|
|
||||||
<li><a href="#" onclick="showCategory(10)" data-category-badge="10">Desery</a></li>
|
|
||||||
<li><a href="#" onclick="showCategory(11)" data-category-badge="11">Napoje</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@@ -162,7 +164,7 @@ $vMenu = publicAssetVersion($publicDir, 'menu.json');
|
|||||||
<!-- Dynamiczne menu załaduje się tutaj -->
|
<!-- Dynamiczne menu załaduje się tutaj -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- Koniec menuView -->
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -174,69 +176,45 @@ $vMenu = publicAssetVersion($publicDir, 'menu.json');
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bottom Navigation Bar -->
|
|
||||||
<nav class="bottom-nav" id="bottomNav" style="display: none;">
|
<nav class="bottom-nav" id="bottomNav" style="display: none;">
|
||||||
<div class="nav-item active" onclick="switchTab('status')" id="navStatus">
|
<div class="nav-item active" onclick="switchTab('status')" id="navStatus">
|
||||||
<span class="nav-icon">🍽️</span>
|
<span class="nav-icon">🍽️</span>
|
||||||
<span class="nav-label">Zamówienie</span>
|
<span class="nav-label" data-i18n="nav.order">Zamówienie</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item" onclick="switchTab('menu')" id="navMenu">
|
<div class="nav-item" onclick="switchTab('menu')" id="navMenu">
|
||||||
<span class="nav-icon">📖</span>
|
<span class="nav-icon">📖</span>
|
||||||
<span class="nav-label">Menu</span>
|
<span class="nav-label" data-i18n="nav.menu">Menu</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item action-call" onclick="openWaiterDialog()" id="navWaiter">
|
<div class="nav-item action-call" onclick="openWaiterDialog()" id="navWaiter">
|
||||||
<span class="nav-icon">🛎️</span>
|
<span class="nav-icon">🛎️</span>
|
||||||
<span class="nav-label">Kelner</span>
|
<span class="nav-label" data-i18n="nav.waiter">Kelner</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="nav-item action-bill" onclick="openBillDialog()" id="navBill">
|
<div class="nav-item action-bill" onclick="openBillDialog()" id="navBill">
|
||||||
<span class="nav-icon">💳</span>
|
<span class="nav-icon">💳</span>
|
||||||
<span class="nav-label">Rachunek</span>
|
<span class="nav-label" data-i18n="nav.bill">Rachunek</span>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- WAITER DIALOG -->
|
|
||||||
<div class="modal-overlay" id="waiterModal">
|
<div class="modal-overlay" id="waiterModal">
|
||||||
<div class="modal-content" style="text-align: center;">
|
<div class="modal-content" style="text-align: center;">
|
||||||
<div style="font-size: 48px; margin-bottom: 15px;">🛎️</div>
|
<div style="font-size: 48px; margin-bottom: 15px;">🛎️</div>
|
||||||
<h3 style="margin-top: 0; color: var(--text-main); font-family: 'Playfair Display', serif; font-size: 24px;">
|
<h3 style="margin-top: 0; color: var(--text-main); font-family: 'Playfair Display', serif; font-size: 24px;" data-i18n="waiter.title">
|
||||||
Przywołać obsługę?</h3>
|
Przywołać obsługę?</h3>
|
||||||
<p style="color: var(--text-muted); font-size: 15px; margin-bottom: 25px; line-height: 1.5;">
|
<p style="color: var(--text-muted); font-size: 15px; margin-bottom: 25px; line-height: 1.5;" data-i18n="waiter.body">
|
||||||
Kelner otrzyma natychmiastowe powiadomienie na swoim panelu i podejdzie do Twojego stolika najszybciej jak to
|
Kelner otrzyma natychmiastowe powiadomienie na swoim panelu i podejdzie do Twojego stolika najszybciej jak to
|
||||||
możliwe.
|
możliwe.
|
||||||
</p>
|
</p>
|
||||||
<div style="display: flex; gap: 12px; flex-direction: column;">
|
<div style="display: flex; gap: 12px; flex-direction: column;">
|
||||||
<button class="btn btn-primary" onclick="confirmCallWaiter()" style="padding: 14px; font-size: 16px;">Tak,
|
<button class="btn btn-primary" onclick="confirmCallWaiter()" style="padding: 14px; font-size: 16px;" data-i18n="waiter.confirm">Tak,
|
||||||
poproś kelnera</button>
|
poproś kelnera</button>
|
||||||
<button class="btn btn-secondary" onclick="closeWaiterDialog()" style="padding: 14px;">Anuluj</button>
|
<button class="btn btn-secondary" onclick="closeWaiterDialog()" style="padding: 14px;" data-i18n="waiter.cancel">Anuluj</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- NAME DIALOG (Tymczasowo wyłączone)
|
|
||||||
<div class="modal-overlay" id="nameModal">
|
|
||||||
<div class="modal-content" style="text-align: center;">
|
|
||||||
<div style="font-size: 48px; margin-bottom: 15px;">👋</div>
|
|
||||||
<h3 style="margin-top: 0; color: var(--text-main); font-family: 'Playfair Display', serif; font-size: 24px;">Podaj
|
|
||||||
swoje imię</h3>
|
|
||||||
<p style="color: var(--text-muted); font-size: 15px; margin-bottom: 20px; line-height: 1.5;">
|
|
||||||
Dzięki temu będziemy mogli powitać Cię osobiście podczas Twojej wizyty!
|
|
||||||
</p>
|
|
||||||
<div class="input-group" style="margin-bottom: 25px; text-align: left;">
|
|
||||||
<input type="text" id="userNameInput" class="input-field" placeholder="Twoje imię..." autocomplete="off" />
|
|
||||||
</div>
|
|
||||||
<div style="display: flex; gap: 12px; flex-direction: column;">
|
|
||||||
<button class="btn btn-primary" onclick="saveUserName()" style="padding: 14px; font-size: 16px;">Idę
|
|
||||||
dalej</button>
|
|
||||||
<button class="btn btn-secondary" onclick="declineUserName()" style="padding: 14px;">Nie chcę podawać</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<!-- ITEM MODAL -->
|
|
||||||
<div class="modal-overlay" id="itemModal">
|
<div class="modal-overlay" id="itemModal">
|
||||||
<div class="modal-content item-modal-content" id="itemModalContent">
|
<div class="modal-content item-modal-content" id="itemModalContent">
|
||||||
<button type="button" class="close-btn item-modal-close" onclick="closeItemModal()" aria-label="Zamknij">×</button>
|
<button type="button" class="close-btn item-modal-close" onclick="closeItemModal()" data-i18n-aria="menu.close" aria-label="Zamknij">×</button>
|
||||||
<div class="item-modal-pane" id="itemModalPane">
|
<div class="item-modal-pane" id="itemModalPane">
|
||||||
<div class="item-modal-image-wrap">
|
<div class="item-modal-image-wrap">
|
||||||
<img id="itemModalImage" class="item-modal-image" src="" alt="">
|
<img id="itemModalImage" class="item-modal-image" src="" alt="">
|
||||||
@@ -245,7 +223,7 @@ $vMenu = publicAssetVersion($publicDir, 'menu.json');
|
|||||||
<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"/>
|
<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"/>
|
<circle cx="12" cy="13" r="4"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Brak zdjęcia</span>
|
<span data-i18n="menu.no_image">Brak zdjęcia</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="item-modal-body">
|
<div class="item-modal-body">
|
||||||
@@ -255,120 +233,113 @@ $vMenu = publicAssetVersion($publicDir, 'menu.json');
|
|||||||
<h3 id="itemModalTitle" class="item-modal-title"></h3>
|
<h3 id="itemModalTitle" class="item-modal-title"></h3>
|
||||||
<div id="itemModalPrice" class="item-modal-price"></div>
|
<div id="itemModalPrice" class="item-modal-price"></div>
|
||||||
<p id="itemModalDesc" class="item-modal-desc"></p>
|
<p id="itemModalDesc" class="item-modal-desc"></p>
|
||||||
<button type="button" class="btn btn-secondary item-modal-close-btn" onclick="closeItemModal()">Zamknij</button>
|
<button type="button" class="btn btn-secondary item-modal-close-btn" onclick="closeItemModal()" data-i18n="menu.close">Zamknij</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- BILL DIALOG -->
|
|
||||||
<div class="modal-overlay" id="billModal">
|
<div class="modal-overlay" id="billModal">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h3 id="modalTitle">Rozliczenie</h3>
|
<h3 id="modalTitle" data-i18n="bill.title">Rozliczenie</h3>
|
||||||
<button class="close-btn" onclick="closeBillDialog()">×</button>
|
<button class="close-btn" onclick="closeBillDialog()">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 0: Loading or List of Bills -->
|
|
||||||
<div class="step active" id="stepBillList">
|
<div class="step active" id="stepBillList">
|
||||||
<div id="billLoading" style="text-align:center; padding: 20px;">
|
<div id="billLoading" style="text-align:center; padding: 20px;" data-i18n="bill.loading">
|
||||||
⏳ Pobieranie rachunków...
|
⏳ Pobieranie rachunków...
|
||||||
</div>
|
</div>
|
||||||
<div id="billListContainer" class="hidden">
|
<div id="billListContainer" class="hidden">
|
||||||
<p style="margin-top:0; color:var(--text-muted); font-size:14px; margin-bottom: 20px;">Mamy kilka otwartych
|
<p style="margin-top:0; color:var(--text-muted); font-size:14px; margin-bottom: 20px;" data-i18n="bill.multi">Mamy kilka otwartych
|
||||||
rachunków na tym stoliku. Który chcesz opłacić?</p>
|
rachunków na tym stoliku. Który chcesz opłacić?</p>
|
||||||
<div id="billListItems" style="display:flex; flex-direction:column; gap:10px;"></div>
|
<div id="billListItems" style="display:flex; flex-direction:column; gap:10px;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 0.5: Bill Review -->
|
|
||||||
<div class="step" id="stepBillReview">
|
<div class="step" id="stepBillReview">
|
||||||
<p style="margin-top:0; color:var(--text-muted); font-size:14px; margin-bottom: 20px;">Podsumowanie rachunku:
|
<p style="margin-top:0; color:var(--text-muted); font-size:14px; margin-bottom: 20px;" data-i18n="bill.review">Podsumowanie rachunku:
|
||||||
</p>
|
</p>
|
||||||
<div id="billReviewContent"
|
<div id="billReviewContent"
|
||||||
style="background: var(--surface-light); padding: 15px; border-radius: 12px; max-height: 40vh; overflow-y: auto; margin-bottom: 15px;">
|
style="background: var(--surface-light); padding: 15px; border-radius: 12px; max-height: 40vh; overflow-y: auto; margin-bottom: 15px;">
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; justify-content:space-between; font-weight:700; font-size:18px; margin-bottom: 20px;">
|
<div style="display:flex; justify-content:space-between; font-weight:700; font-size:18px; margin-bottom: 20px;">
|
||||||
<span>Do zapłaty:</span>
|
<span data-i18n="bill.total">Do zapłaty:</span>
|
||||||
<span id="billTotalAmount" style="color:var(--primary);">0.00 PLN</span>
|
<span id="billTotalAmount" style="color:var(--primary);">0.00 PLN</span>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; gap:12px;">
|
<div style="display:flex; gap:12px;">
|
||||||
<button class="btn btn-secondary" style="flex:1;" onclick="goBackToBillList()"
|
<button class="btn btn-secondary" style="flex:1;" onclick="goBackToBillList()"
|
||||||
id="btnBackToBills">Wróć</button>
|
id="btnBackToBills" data-i18n="bill.back">Wróć</button>
|
||||||
<button class="btn btn-primary" style="flex:2;" onclick="proceedToBillPayment()">Poproś rachunek</button>
|
<button class="btn btn-primary" style="flex:2;" onclick="proceedToBillPayment()" data-i18n="bill.request">Poproś rachunek</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 1: Payment Method -->
|
|
||||||
<div class="step" id="stepPayment">
|
<div class="step" id="stepPayment">
|
||||||
<p style="margin-top:0; color:var(--text-muted); font-size:14px; margin-bottom: 20px;">Wybierz preferowaną formę
|
<p style="margin-top:0; color:var(--text-muted); font-size:14px; margin-bottom: 20px;" data-i18n="bill.payment">Wybierz preferowaną formę
|
||||||
płatności:</p>
|
płatności:</p>
|
||||||
<div class="option-grid">
|
<div class="option-grid">
|
||||||
<div class="option-card" onclick="selectPayment('karta')">
|
<div class="option-card" onclick="selectPayment('karta')">
|
||||||
<span class="option-icon">💳</span>
|
<span class="option-icon">💳</span>
|
||||||
<span class="option-label">Karta</span>
|
<span class="option-label" data-i18n="bill.card">Karta</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="option-card" onclick="selectPayment('gotówka')">
|
<div class="option-card" onclick="selectPayment('gotówka')">
|
||||||
<span class="option-icon">💵</span>
|
<span class="option-icon">💵</span>
|
||||||
<span class="option-label">Gotówka</span>
|
<span class="option-label" data-i18n="bill.cash">Gotówka</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary" onclick="goToStep('stepBillReview')" style="margin-top: 15px;">Wróć do
|
<button class="btn btn-secondary" onclick="goToStep('stepBillReview')" style="margin-top: 15px;" data-i18n="bill.back_summary">Wróć do
|
||||||
podsumowania</button>
|
podsumowania</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 2: Document Type -->
|
|
||||||
<div class="step" id="stepDocument">
|
<div class="step" id="stepDocument">
|
||||||
<p style="margin-top:0; color:var(--text-muted); font-size:14px; margin-bottom: 20px;">Jakiego dokumentu
|
<p style="margin-top:0; color:var(--text-muted); font-size:14px; margin-bottom: 20px;" data-i18n="bill.doc">Jakiego dokumentu
|
||||||
potrzebujesz?</p>
|
potrzebujesz?</p>
|
||||||
<div class="option-grid">
|
<div class="option-grid">
|
||||||
<div class="option-card" onclick="selectDocument('paragon')">
|
<div class="option-card" onclick="selectDocument('paragon')">
|
||||||
<span class="option-icon">🧾</span>
|
<span class="option-icon">🧾</span>
|
||||||
<span class="option-label">Paragon</span>
|
<span class="option-label" data-i18n="bill.receipt">Paragon</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="option-card" onclick="selectDocument('faktura')">
|
<div class="option-card" onclick="selectDocument('faktura')">
|
||||||
<span class="option-icon">📄</span>
|
<span class="option-icon">📄</span>
|
||||||
<span class="option-label">Faktura</span>
|
<span class="option-label" data-i18n="bill.invoice">Faktura</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary" onclick="goToStep('stepPayment')" style="margin-top: 8px;">Wróć</button>
|
<button class="btn btn-secondary" onclick="goToStep('stepPayment')" style="margin-top: 8px;" data-i18n="bill.back">Wróć</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 3: NIP Input -->
|
|
||||||
<div class="step" id="stepNIP">
|
<div class="step" id="stepNIP">
|
||||||
<p style="margin-top:0; color:var(--text-muted); font-size:14px; margin-bottom: 20px;">Wprowadź NIP firmy,
|
<p style="margin-top:0; color:var(--text-muted); font-size:14px; margin-bottom: 20px;" data-i18n="bill.nip_intro">Wprowadź NIP firmy,
|
||||||
abyśmy mogli automatycznie pobrać dane.</p>
|
abyśmy mogli automatycznie pobrać dane.</p>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<label class="input-label">Numer NIP</label>
|
<label class="input-label" data-i18n="bill.nip_label">Numer NIP</label>
|
||||||
<input type="number" id="nipInput" class="input-field" placeholder="np. 1234567890" autocomplete="off" />
|
<input type="number" id="nipInput" class="input-field" placeholder="np. 1234567890" autocomplete="off" />
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; gap:12px; margin-top: 24px;">
|
<div style="display:flex; gap:12px; margin-top: 24px;">
|
||||||
<button class="btn btn-secondary" style="flex:1;" onclick="goToStep('stepDocument')">Wróć</button>
|
<button class="btn btn-secondary" style="flex:1;" onclick="goToStep('stepDocument')" data-i18n="bill.back">Wróć</button>
|
||||||
<button class="btn btn-primary" style="flex:2;" onclick="fetchGUS()" id="btnGUS">Pobierz z GUS</button>
|
<button class="btn btn-primary" style="flex:2;" onclick="fetchGUS()" id="btnGUS" data-i18n="bill.gus">Pobierz z GUS</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Step 4: Verify Data -->
|
|
||||||
<div class="step" id="stepVerify">
|
<div class="step" id="stepVerify">
|
||||||
<p style="margin-top:0; color:var(--text-muted); font-size:14px; margin-bottom: 20px;">Czy poniższe dane do
|
<p style="margin-top:0; color:var(--text-muted); font-size:14px; margin-bottom: 20px;" data-i18n="bill.verify">Czy poniższe dane do
|
||||||
faktury są prawidłowe?</p>
|
faktury są prawidłowe?</p>
|
||||||
|
|
||||||
<div class="company-details">
|
<div class="company-details">
|
||||||
<input type="text" id="cmpName" class="company-input" style="font-weight:700; margin-bottom:4px;" readonly />
|
<input type="text" id="cmpName" class="company-input" style="font-weight:700; margin-bottom:4px;" readonly />
|
||||||
<input type="text" id="cmpStreet" class="company-input" placeholder="Ulica i numer" style="margin-bottom:4px;"
|
<input type="text" id="cmpStreet" class="company-input" data-i18n-placeholder="bill.street" placeholder="Ulica i numer" style="margin-bottom:4px;"
|
||||||
readonly />
|
readonly />
|
||||||
<div style="display: flex; gap: 8px; margin-bottom: 4px;">
|
<div style="display: flex; gap: 8px; margin-bottom: 4px;">
|
||||||
<input type="text" id="cmpZip" class="company-input" placeholder="Kod" style="flex: 1;" readonly />
|
<input type="text" id="cmpZip" class="company-input" data-i18n-placeholder="bill.zip" placeholder="Kod" style="flex: 1;" readonly />
|
||||||
<input type="text" id="cmpCity" class="company-input" placeholder="Miasto" style="flex: 2;" readonly />
|
<input type="text" id="cmpCity" class="company-input" data-i18n-placeholder="bill.city" placeholder="Miasto" style="flex: 2;" readonly />
|
||||||
</div>
|
</div>
|
||||||
<input type="text" id="cmpNip" class="company-input muted" readonly />
|
<input type="text" id="cmpNip" class="company-input muted" readonly />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="display:flex; gap:12px; flex-direction:column;">
|
<div style="display:flex; gap:12px; flex-direction:column;">
|
||||||
<button class="btn btn-primary" onclick="confirmInvoice()">Tak, poproszę fakturę!</button>
|
<button class="btn btn-primary" onclick="confirmInvoice()" data-i18n="bill.confirm_invoice">Tak, poproszę fakturę!</button>
|
||||||
<div style="display:flex; gap:12px;">
|
<div style="display:flex; gap:12px;">
|
||||||
<button class="btn btn-secondary" onclick="goToStep('stepNIP')">Zmień NIP</button>
|
<button class="btn btn-secondary" onclick="goToStep('stepNIP')" data-i18n="bill.change_nip">Zmień NIP</button>
|
||||||
<button class="btn btn-secondary" onclick="editCompanyData()" id="btnEditCompany">Popraw ręcznie</button>
|
<button class="btn btn-secondary" onclick="editCompanyData()" id="btnEditCompany" data-i18n="bill.edit_company">Popraw ręcznie</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -376,16 +347,15 @@ $vMenu = publicAssetVersion($publicDir, 'menu.json');
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Toast -->
|
|
||||||
<div class="toast" id="toastMsg">
|
<div class="toast" id="toastMsg">
|
||||||
<span style="font-size:20px;">✓</span> <span id="toastText">Wysłano!</span>
|
<span style="font-size:20px;">✓</span> <span id="toastText" data-i18n="toast.sent">Wysłano!</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window.MENU_ASSET_VERSION = <?= json_encode($vMenu, JSON_UNESCAPED_UNICODE) ?>;
|
|
||||||
window.APP_JS_VERSION = <?= json_encode($vJs, JSON_UNESCAPED_UNICODE) ?>;
|
window.APP_JS_VERSION = <?= json_encode($vJs, JSON_UNESCAPED_UNICODE) ?>;
|
||||||
window.APP_CONFIG = <?= json_encode([
|
window.APP_CONFIG = <?= json_encode([
|
||||||
'geoBypassHosts' => getGeoBypassTrustedHosts(),
|
'geoBypassHosts' => getGeoBypassTrustedHosts(),
|
||||||
|
'languages' => $languagesForClient,
|
||||||
'endpoints' => [
|
'endpoints' => [
|
||||||
'analytics' => '../api/analytics.php',
|
'analytics' => '../api/analytics.php',
|
||||||
'guestActionQueue' => '../api/guest_action_queue.php',
|
'guestActionQueue' => '../api/guest_action_queue.php',
|
||||||
@@ -393,6 +363,7 @@ $vMenu = publicAssetVersion($publicDir, 'menu.json');
|
|||||||
'gusLookup' => '../api/gus_lookup.php',
|
'gusLookup' => '../api/gus_lookup.php',
|
||||||
'kds' => '../api/kds.php',
|
'kds' => '../api/kds.php',
|
||||||
'bills' => '../api/bills.php',
|
'bills' => '../api/bills.php',
|
||||||
|
'menu' => '../api/menu.php',
|
||||||
],
|
],
|
||||||
'loaderMinMs' => 10000,
|
'loaderMinMs' => 10000,
|
||||||
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?>;
|
||||||
|
|||||||
@@ -82,6 +82,89 @@ body {
|
|||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.geo-lang-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-picker {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-picker-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: var(--surface, #1e293b);
|
||||||
|
border: 1px solid var(--surface-light, #334155);
|
||||||
|
color: var(--text-main, #f8fafc);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-picker-btn:hover {
|
||||||
|
border-color: var(--primary, #e2b07e);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-flag-img {
|
||||||
|
display: block;
|
||||||
|
width: 28px;
|
||||||
|
height: 20px;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-picker-menu {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: calc(100% + 6px);
|
||||||
|
min-width: 168px;
|
||||||
|
background: var(--surface, #1e293b);
|
||||||
|
border: 1px solid var(--surface-light, #334155);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 6px;
|
||||||
|
z-index: 80;
|
||||||
|
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-picker-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-main, #f8fafc);
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: inherit;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-picker-option:hover,
|
||||||
|
.lang-picker-option.is-active {
|
||||||
|
background: rgba(226, 176, 126, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-top .logo-text {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.geo-icon {
|
.geo-icon {
|
||||||
font-size: 64px;
|
font-size: 64px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
@@ -1104,6 +1187,9 @@ header {
|
|||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#menuOnlyBanner.is-hidden + .menu-search-container {
|
#menuOnlyBanner.is-hidden + .menu-search-container {
|
||||||
@@ -1111,7 +1197,9 @@ header {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#menuSearchInput {
|
#menuSearchInput {
|
||||||
width: 100%;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
width: auto;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border: 1px solid var(--surface-light);
|
border: 1px solid var(--surface-light);
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480">
|
||||||
|
<rect width="640" height="160" y="0" fill="#000"/>
|
||||||
|
<rect width="640" height="160" y="160" fill="#dd0000"/>
|
||||||
|
<rect width="640" height="160" y="320" fill="#ffce00"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 239 B |
@@ -0,0 +1,7 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480">
|
||||||
|
<rect width="640" height="480" fill="#012169"/>
|
||||||
|
<path fill="#FFF" d="M75 0l244 181L562 0h78v62L400 241l240 178v61h-80L318 301 81 480H0v-60l239-178L0 64V0h75z"/>
|
||||||
|
<path fill="#C8102E" d="M424 281l216 159v40L369 281h55zm-184 20l6 35L54 480H0l246-180zM640 0v3L391 191l2-44L590 0h50zM0 0l239 176h-60L0 42V0z"/>
|
||||||
|
<path fill="#FFF" d="M241 0v480h160V0H241zM0 160v160h640V160H0z"/>
|
||||||
|
<path fill="#C8102E" d="M0 193v96h640v-96H0zM273 0v480h96V0h-96z"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 521 B |
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 480">
|
||||||
|
<rect width="640" height="240" y="0" fill="#fff"/>
|
||||||
|
<rect width="640" height="240" y="240" fill="#dc143c"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 181 B |
+25
-3
@@ -43,6 +43,10 @@ import {
|
|||||||
saveUserName,
|
saveUserName,
|
||||||
startGeoBootstrap,
|
startGeoBootstrap,
|
||||||
} from "./modules/geo.js";
|
} from "./modules/geo.js";
|
||||||
|
import {
|
||||||
|
createLanguagePicker,
|
||||||
|
initI18n,
|
||||||
|
} from "./modules/i18n.js";
|
||||||
import {
|
import {
|
||||||
getHashParam,
|
getHashParam,
|
||||||
setHashParam,
|
setHashParam,
|
||||||
@@ -51,13 +55,32 @@ import {
|
|||||||
} from "./modules/state.js";
|
} from "./modules/state.js";
|
||||||
import { showToast } from "./modules/toast.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
|
// Parse URL into shared state
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
setHashParam((params.get("h") || "").trim());
|
setHashParam((params.get("h") || "").trim());
|
||||||
setIsStaffPreview(params.get("preview") === "staff");
|
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()) {
|
if (!getHashParam()) {
|
||||||
const input = prompt("Podaj bezpieczny hash stolika (wymagane):");
|
const input = prompt("Podaj bezpieczny hash stolika (wymagane):");
|
||||||
const trimmed = (input || "").trim();
|
const trimmed = (input || "").trim();
|
||||||
@@ -73,7 +96,6 @@ setProtectedActionHandlers({
|
|||||||
bill: openBillDialogInternal,
|
bill: openBillDialogInternal,
|
||||||
});
|
});
|
||||||
|
|
||||||
// window.* for inline onclick in app.php
|
|
||||||
window.saveUserName = saveUserName;
|
window.saveUserName = saveUserName;
|
||||||
window.declineUserName = declineUserName;
|
window.declineUserName = declineUserName;
|
||||||
window.handleGeoMenuClick = handleGeoMenuClick;
|
window.handleGeoMenuClick = handleGeoMenuClick;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
};
|
||||||
@@ -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",
|
||||||
|
};
|
||||||
@@ -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",
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { endpoints } from "./config.js";
|
import { endpoints } from "./config.js";
|
||||||
|
import { t } from "./i18n.js";
|
||||||
import { trackEvent } from "./analytics.js";
|
import { trackEvent } from "./analytics.js";
|
||||||
import { requireFullAccess } from "./access.js";
|
import { requireFullAccess } from "./access.js";
|
||||||
import {
|
import {
|
||||||
@@ -41,14 +42,14 @@ export async function callWaiter(type) {
|
|||||||
if (queued.reason === "pending") {
|
if (queued.reason === "pending") {
|
||||||
showToast(guestActionBlockedMessage("waiter_call"));
|
showToast(guestActionBlockedMessage("waiter_call"));
|
||||||
} else {
|
} else {
|
||||||
showToast("Nie udało się wysłać wezwania. Spróbuj ponownie za chwilę.");
|
showToast(t("waiter.toast_fail"));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
trackEvent("waiter_call_requested", { waiterType: "order" });
|
trackEvent("waiter_call_requested", { waiterType: "order" });
|
||||||
sendApiSimulated("CallWaiter_Order", { table: getTableParam() });
|
sendApiSimulated("CallWaiter_Order", { table: getTableParam() });
|
||||||
showToast("Kelner wkrótce do Ciebie podejdzie!");
|
showToast(t("waiter.toast_ok"));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openWaiterDialog() {
|
export function openWaiterDialog() {
|
||||||
@@ -96,6 +97,7 @@ export async function openBillDialogInternal() {
|
|||||||
document.body.style.overflow = "hidden"; // Zablokuj scroll tła
|
document.body.style.overflow = "hidden"; // Zablokuj scroll tła
|
||||||
|
|
||||||
document.getElementById("billLoading").classList.remove("hidden");
|
document.getElementById("billLoading").classList.remove("hidden");
|
||||||
|
document.getElementById("billLoading").textContent = t("bill.loading");
|
||||||
document.getElementById("billListContainer").classList.add("hidden");
|
document.getElementById("billListContainer").classList.add("hidden");
|
||||||
goToStep("stepBillList");
|
goToStep("stepBillList");
|
||||||
|
|
||||||
@@ -115,10 +117,10 @@ export async function openBillDialogInternal() {
|
|||||||
document.getElementById("btnBackToBills").style.display = "block";
|
document.getElementById("btnBackToBills").style.display = "block";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
document.getElementById("billLoading").innerHTML = "Brak otwartych rachunków do opłacenia.";
|
document.getElementById("billLoading").innerHTML = t("bill.loading_empty");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} 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.style.padding = "15px";
|
||||||
div.onclick = () => showBillReview(b);
|
div.onclick = () => showBillReview(b);
|
||||||
|
|
||||||
const numerFormat = b.numer ? `#${b.numer}` : "Rachunek";
|
const numerFormat = b.numer ? `#${b.numer}` : t("bill.bill_fallback");
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight:bold;">${numerFormat}</div>
|
<div style="font-weight:bold;">${numerFormat}</div>
|
||||||
@@ -212,7 +214,7 @@ export async function selectDocument(docType) {
|
|||||||
if (queued.reason === "pending") {
|
if (queued.reason === "pending") {
|
||||||
showToast(guestActionBlockedMessage("bill_request"));
|
showToast(guestActionBlockedMessage("bill_request"));
|
||||||
} else {
|
} else {
|
||||||
showToast("Nie udało się wysłać prośby o rachunek. Spróbuj ponownie za chwilę.");
|
showToast(t("bill.toast_fail"));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -224,7 +226,7 @@ export async function selectDocument(docType) {
|
|||||||
payment: billState.payment,
|
payment: billState.payment,
|
||||||
doc: "paragon",
|
doc: "paragon",
|
||||||
});
|
});
|
||||||
showToast("Kelner przyniesie paragon do opłacenia!");
|
showToast(t("bill.toast_receipt"));
|
||||||
} else {
|
} else {
|
||||||
goToStep("stepNIP");
|
goToStep("stepNIP");
|
||||||
document.getElementById("nipInput").value = "";
|
document.getElementById("nipInput").value = "";
|
||||||
@@ -235,11 +237,11 @@ export async function selectDocument(docType) {
|
|||||||
export async function fetchGUS() {
|
export async function fetchGUS() {
|
||||||
const nip = document.getElementById("nipInput").value.replace(/[\s-]/g, "");
|
const nip = document.getElementById("nipInput").value.replace(/[\s-]/g, "");
|
||||||
if (nip.length < 10) {
|
if (nip.length < 10) {
|
||||||
alert("Wprowadź poprawny numer NIP.");
|
alert(t("bill.nip_invalid"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const btn = document.getElementById("btnGUS");
|
const btn = document.getElementById("btnGUS");
|
||||||
btn.textContent = "Szukam...";
|
btn.textContent = t("bill.gus_searching");
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -267,24 +269,24 @@ export async function fetchGUS() {
|
|||||||
document.getElementById("cmpStreet").value = billState.company.street;
|
document.getElementById("cmpStreet").value = billState.company.street;
|
||||||
document.getElementById("cmpZip").value = billState.company.zip;
|
document.getElementById("cmpZip").value = billState.company.zip;
|
||||||
document.getElementById("cmpCity").value = billState.company.city;
|
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
|
// reset do readonly
|
||||||
document.getElementById("cmpName").readOnly = true;
|
document.getElementById("cmpName").readOnly = true;
|
||||||
document.getElementById("cmpStreet").readOnly = true;
|
document.getElementById("cmpStreet").readOnly = true;
|
||||||
document.getElementById("cmpZip").readOnly = true;
|
document.getElementById("cmpZip").readOnly = true;
|
||||||
document.getElementById("cmpCity").readOnly = true;
|
document.getElementById("cmpCity").readOnly = true;
|
||||||
document.getElementById("btnEditCompany").textContent = "Popraw ręcznie";
|
document.getElementById("btnEditCompany").textContent = t("bill.edit_company");
|
||||||
|
|
||||||
goToStep("stepVerify");
|
goToStep("stepVerify");
|
||||||
} else {
|
} else {
|
||||||
alert("Nie udało się pobrać danych z GUS dla podanego NIP-u.");
|
alert(t("bill.gus_fail"));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Błąd pobierania danych z GUS:", error);
|
console.error("Błąd pobierania danych z GUS:", error);
|
||||||
alert("Błąd połączenia z API GUS.");
|
alert(t("bill.gus_error"));
|
||||||
} finally {
|
} finally {
|
||||||
btn.textContent = "Pobierz z GUS";
|
btn.textContent = t("bill.gus");
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,13 +304,13 @@ export function editCompanyData() {
|
|||||||
z.readOnly = false;
|
z.readOnly = false;
|
||||||
c.readOnly = false;
|
c.readOnly = false;
|
||||||
n.focus();
|
n.focus();
|
||||||
btn.textContent = "Zakończ edycję";
|
btn.textContent = t("bill.edit_done");
|
||||||
} else {
|
} else {
|
||||||
n.readOnly = true;
|
n.readOnly = true;
|
||||||
s.readOnly = true;
|
s.readOnly = true;
|
||||||
z.readOnly = true;
|
z.readOnly = true;
|
||||||
c.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") {
|
if (queued.reason === "pending") {
|
||||||
showToast(guestActionBlockedMessage("bill_request"));
|
showToast(guestActionBlockedMessage("bill_request"));
|
||||||
} else {
|
} else {
|
||||||
showToast("Nie udało się wysłać prośby o rachunek. Spróbuj ponownie za chwilę.");
|
showToast(t("bill.toast_fail"));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -349,5 +351,5 @@ export async function confirmInvoice() {
|
|||||||
nip: billState.nip,
|
nip: billState.nip,
|
||||||
company: billState.company,
|
company: billState.company,
|
||||||
});
|
});
|
||||||
showToast("Dziękujemy! Prośba o fakturę została wysłana.");
|
showToast(t("bill.toast_invoice"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,15 @@ export const endpoints = {
|
|||||||
gusLookup: cfg.endpoints?.gusLookup || "../api/gus_lookup.php",
|
gusLookup: cfg.endpoints?.gusLookup || "../api/gus_lookup.php",
|
||||||
kds: cfg.endpoints?.kds || "../api/kds.php",
|
kds: cfg.endpoints?.kds || "../api/kds.php",
|
||||||
bills: cfg.endpoints?.bills || "../api/bills.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 =
|
export const loaderMinMs = Number(cfg.loaderMinMs) || 10_000;
|
||||||
window.MENU_ASSET_VERSION || window.APP_ASSET_VERSION || "1";
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { endpoints, geoBypassHosts } from "./config.js";
|
import { endpoints, geoBypassHosts } from "./config.js";
|
||||||
|
import { t } from "./i18n.js";
|
||||||
import { trackEvent } from "./analytics.js";
|
import { trackEvent } from "./analytics.js";
|
||||||
import {
|
import {
|
||||||
runPendingProtectedAction,
|
runPendingProtectedAction,
|
||||||
@@ -109,21 +110,15 @@ function showGreeting(name, firstVisitTime) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isIOSDevice() {
|
function GEO_GATE_LABELS() {
|
||||||
return (
|
return {
|
||||||
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
status: t("geo.feature.status"),
|
||||||
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1)
|
waiter: t("geo.feature.waiter"),
|
||||||
);
|
bill: t("geo.feature.bill"),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const GEO_GATE_LABELS = {
|
const GEO_DEFAULT_LEAD = () => t("geo.lead");
|
||||||
status: "status zamówienia",
|
|
||||||
waiter: "wezwanie kelnera",
|
|
||||||
bill: "prośbę o rachunek",
|
|
||||||
};
|
|
||||||
|
|
||||||
const GEO_DEFAULT_LEAD =
|
|
||||||
"Przeglądaj menu od razu — albo potwierdź, że jesteś u nas, aby wezwać kelnera, śledzić zamówienie i poprosić o rachunek.";
|
|
||||||
|
|
||||||
function setGeoLead(html) {
|
function setGeoLead(html) {
|
||||||
const el = document.getElementById("geoLead");
|
const el = document.getElementById("geoLead");
|
||||||
@@ -155,7 +150,7 @@ function setGeoActionBusy(busy) {
|
|||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.setAttribute("aria-busy", busy ? "true" : "false");
|
btn.setAttribute("aria-busy", busy ? "true" : "false");
|
||||||
if (busy) {
|
if (busy) {
|
||||||
setGeoActionLabel("Sprawdzanie…");
|
setGeoActionLabel(t("geo.btn.checking"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,19 +167,7 @@ function setGeoActionLabel(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getGeoPermissionInstructions() {
|
function getGeoPermissionInstructions() {
|
||||||
if (isIOSDevice()) {
|
return t("geo.hint.permission");
|
||||||
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ę.`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let geoMenuButtonMode = "menu_only";
|
let geoMenuButtonMode = "menu_only";
|
||||||
@@ -231,7 +214,7 @@ function configureGeoSecondaryButton(mode) {
|
|||||||
|
|
||||||
if (mode === "back_to_menu") {
|
if (mode === "back_to_menu") {
|
||||||
menuOnlyBtn.style.display = "";
|
menuOnlyBtn.style.display = "";
|
||||||
if (mainEl) mainEl.textContent = "Wróć do menu";
|
if (mainEl) mainEl.textContent = t("geo.btn.back_menu");
|
||||||
if (subEl) {
|
if (subEl) {
|
||||||
subEl.textContent = "";
|
subEl.textContent = "";
|
||||||
subEl.style.display = "none";
|
subEl.style.display = "none";
|
||||||
@@ -240,9 +223,9 @@ function configureGeoSecondaryButton(mode) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
menuOnlyBtn.style.display = "";
|
menuOnlyBtn.style.display = "";
|
||||||
if (mainEl) mainEl.textContent = "Przejdź do menu";
|
if (mainEl) mainEl.textContent = t("geo.btn.menu_main");
|
||||||
if (subEl) {
|
if (subEl) {
|
||||||
subEl.textContent = "bez lokalizacji";
|
subEl.textContent = t("geo.btn.menu_sub");
|
||||||
subEl.style.display = "";
|
subEl.style.display = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,13 +238,13 @@ export function showGeoGateForAction(action) {
|
|||||||
if (loadingScreen) loadingScreen.classList.add("hidden");
|
if (loadingScreen) loadingScreen.classList.add("hidden");
|
||||||
if (geoScreen) geoScreen.classList.remove("hidden");
|
if (geoScreen) geoScreen.classList.remove("hidden");
|
||||||
|
|
||||||
const feature = GEO_GATE_LABELS[action] || "tę funkcję";
|
const feature = GEO_GATE_LABELS()[action] || t("geo.feature.other");
|
||||||
setGeoLead(`Menu masz już otwarte. Aby skorzystać z <b>${feature}</b>, potwierdź krótko, że jesteś w restauracji.`);
|
setGeoLead(t("geo.lead_action", { feature }));
|
||||||
setGeoStatus("");
|
setGeoStatus("");
|
||||||
hideGeoInstructions();
|
hideGeoInstructions();
|
||||||
if (geoActionBtn) {
|
if (geoActionBtn) {
|
||||||
setGeoActionBusy(false);
|
setGeoActionBusy(false);
|
||||||
setGeoActionLabel("Sprawdź lokalizację");
|
setGeoActionLabel(t("geo.btn.check"));
|
||||||
}
|
}
|
||||||
configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only");
|
configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only");
|
||||||
}
|
}
|
||||||
@@ -425,24 +408,22 @@ function showGeoConsentScreen() {
|
|||||||
loadingScreen.classList.add("hidden");
|
loadingScreen.classList.add("hidden");
|
||||||
geoScreen.classList.remove("hidden");
|
geoScreen.classList.remove("hidden");
|
||||||
|
|
||||||
setGeoLead(GEO_DEFAULT_LEAD);
|
setGeoLead(GEO_DEFAULT_LEAD());
|
||||||
setGeoStatus("");
|
setGeoStatus("");
|
||||||
hideGeoInstructions();
|
hideGeoInstructions();
|
||||||
|
|
||||||
if (geoActionBtn) {
|
if (geoActionBtn) {
|
||||||
setGeoActionBusy(false);
|
setGeoActionBusy(false);
|
||||||
setGeoActionLabel("Zgoda, sprawdź lokalizację");
|
setGeoActionLabel(t("geo.btn.locate"));
|
||||||
}
|
}
|
||||||
configureGeoSecondaryButton("menu_only");
|
configureGeoSecondaryButton("menu_only");
|
||||||
}
|
}
|
||||||
|
|
||||||
function showGeoPermissionBlockedState() {
|
function showGeoPermissionBlockedState() {
|
||||||
setGeoStatus("Przeglądarka zablokowała dostęp do lokalizacji.", { error: true });
|
setGeoStatus(t("geo.status.blocked"), { error: true });
|
||||||
showGeoInstructions(
|
showGeoInstructions(getGeoPermissionInstructions());
|
||||||
`${getGeoPermissionInstructions()}<br><br>Po zmianie ustawień <b>odśwież stronę</b>, a potem kliknij „Spróbuj ponownie”.`
|
|
||||||
);
|
|
||||||
setGeoActionBusy(false);
|
setGeoActionBusy(false);
|
||||||
setGeoActionLabel("Spróbuj ponownie");
|
setGeoActionLabel(t("geo.btn.retry"));
|
||||||
configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only");
|
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");
|
configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only");
|
||||||
|
|
||||||
if (!window.isSecureContext) {
|
if (!window.isSecureContext) {
|
||||||
setGeoLead(GEO_DEFAULT_LEAD);
|
setGeoLead(GEO_DEFAULT_LEAD());
|
||||||
setGeoStatus("Ta strona wymaga bezpiecznego połączenia HTTPS.", { error: true });
|
setGeoStatus(t("geo.status.https"), { error: true });
|
||||||
showGeoInstructions(
|
showGeoInstructions(t("geo.hint.https"));
|
||||||
"Przeglądarki mobilne blokują geolokalizację bez <b>https://</b>. Otwórz aplikację przez HTTPS i spróbuj ponownie."
|
|
||||||
);
|
|
||||||
setGeoActionBusy(false);
|
setGeoActionBusy(false);
|
||||||
setGeoActionLabel("Spróbuj ponownie");
|
setGeoActionLabel(t("geo.btn.retry"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!navigator.geolocation) {
|
if (!navigator.geolocation) {
|
||||||
setGeoLead(GEO_DEFAULT_LEAD);
|
setGeoLead(GEO_DEFAULT_LEAD());
|
||||||
setGeoStatus("Twoja przeglądarka nie wspiera geolokalizacji.", { error: true });
|
setGeoStatus(t("geo.status.unsupported"), { error: true });
|
||||||
hideGeoInstructions();
|
hideGeoInstructions();
|
||||||
setGeoActionBusy(false);
|
setGeoActionBusy(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setGeoLead(GEO_DEFAULT_LEAD);
|
setGeoLead(GEO_DEFAULT_LEAD());
|
||||||
setGeoStatus("Sprawdzamy Twoją lokalizację…", { info: true });
|
setGeoStatus(t("geo.status.checking"), { info: true });
|
||||||
hideGeoInstructions();
|
hideGeoInstructions();
|
||||||
setGeoActionBusy(true);
|
setGeoActionBusy(true);
|
||||||
|
|
||||||
@@ -595,16 +574,16 @@ export async function initGeolocationAfterBypassChecks(options = {}) {
|
|||||||
accuracyMeters: Math.round(accuracy),
|
accuracyMeters: Math.round(accuracy),
|
||||||
});
|
});
|
||||||
setGeoActionBusy(false);
|
setGeoActionBusy(false);
|
||||||
setGeoActionLabel("Spróbuj ponownie");
|
setGeoActionLabel(t("geo.btn.retry"));
|
||||||
configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only");
|
configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only");
|
||||||
setGeoStatus(
|
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 }
|
{ error: true }
|
||||||
);
|
);
|
||||||
showGeoInstructions(
|
showGeoInstructions(t("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."
|
|
||||||
);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
trackEvent("geo_check_failed", {
|
trackEvent("geo_check_failed", {
|
||||||
reason: "browser_error",
|
reason: "browser_error",
|
||||||
@@ -612,18 +591,18 @@ export async function initGeolocationAfterBypassChecks(options = {}) {
|
|||||||
message: String(error.message || ""),
|
message: String(error.message || ""),
|
||||||
});
|
});
|
||||||
setGeoActionBusy(false);
|
setGeoActionBusy(false);
|
||||||
setGeoActionLabel("Spróbuj ponownie");
|
setGeoActionLabel(t("geo.btn.retry"));
|
||||||
configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only");
|
configureGeoSecondaryButton(getAppAccessLevel() === "menu" ? "back_to_menu" : "menu_only");
|
||||||
const deniedBecauseInsecure = /secure origins|only secure|https/i.test(String(error.message || ""));
|
const deniedBecauseInsecure = /secure origins|only secure|https/i.test(String(error.message || ""));
|
||||||
|
|
||||||
if (deniedBecauseInsecure) {
|
if (deniedBecauseInsecure) {
|
||||||
setGeoStatus("Geolokalizacja wymaga HTTPS.", { error: true });
|
setGeoStatus(t("geo.status.https_short"), { error: true });
|
||||||
showGeoInstructions("Otwórz aplikację przez bezpieczny adres <b>https://</b> i spróbuj ponownie.");
|
showGeoInstructions(t("geo.hint.https"));
|
||||||
} else if (isGeoPermissionDenied(error)) {
|
} else if (isGeoPermissionDenied(error)) {
|
||||||
showGeoPermissionBlockedState();
|
showGeoPermissionBlockedState();
|
||||||
} else {
|
} else {
|
||||||
setGeoStatus("Nie udało się pobrać lokalizacji.", { error: true });
|
setGeoStatus(t("geo.status.failed"), { error: true });
|
||||||
showGeoInstructions("Sprawdź zasięg, włącz GPS i spróbuj ponownie.");
|
showGeoInstructions(t("geo.hint.retry"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
}
|
||||||
@@ -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";
|
import { trackEvent } from "./analytics.js";
|
||||||
|
|
||||||
let itemModalKeys = [];
|
let itemModalKeys = [];
|
||||||
@@ -6,6 +7,7 @@ let itemModalIndex = -1;
|
|||||||
let itemModalTouchStart = null;
|
let itemModalTouchStart = null;
|
||||||
let itemModalDragging = false;
|
let itemModalDragging = false;
|
||||||
let itemModalAnimating = false;
|
let itemModalAnimating = false;
|
||||||
|
let menuLangChangeBound = false;
|
||||||
|
|
||||||
function resetItemModalPane() {
|
function resetItemModalPane() {
|
||||||
const pane = document.getElementById("itemModalPane");
|
const pane = document.getElementById("itemModalPane");
|
||||||
@@ -78,13 +80,13 @@ function renderMenuListImage(url) {
|
|||||||
const srcAttr = hasUrl ? ` src="${url}"` : "";
|
const srcAttr = hasUrl ? ` src="${url}"` : "";
|
||||||
const onerror = hasUrl ? ' onerror="handleMenuImageError(this)"' : "";
|
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) {
|
function findMenuItem(categoryId, position) {
|
||||||
if (!window.menuDataRaw) return null;
|
if (!window.menuDataRaw) return null;
|
||||||
for (const cat of window.menuDataRaw) {
|
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) {
|
if (item.categoryId == categoryId && item.position == position) {
|
||||||
return item;
|
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 {
|
try {
|
||||||
const response = await fetch(`menu.json?v=${encodeURIComponent(MENU_ASSET_VERSION)}`);
|
const response = await fetch(`${endpoints.menu}?lang=${encodeURIComponent(lang)}`);
|
||||||
if (!response.ok) throw new Error("Nie udało się załadować menu");
|
if (!response.ok) throw new Error(t("menu.load_error"));
|
||||||
const menuData = await response.json();
|
const payload = await response.json();
|
||||||
window.menuDataRaw = menuData;
|
|
||||||
const container = document.getElementById("menuContainer");
|
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;
|
if (!container) return;
|
||||||
|
|
||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
|
|
||||||
menuData.forEach((category) => {
|
sections.forEach((section) => {
|
||||||
const catId = category.items.length > 0 ? category.items[0].categoryId : "";
|
const items = Array.isArray(section.items) ? section.items : [];
|
||||||
|
const catId = items.length > 0 ? items[0].categoryId : "";
|
||||||
|
|
||||||
const catDiv = document.createElement("div");
|
const catDiv = document.createElement("div");
|
||||||
catDiv.className = "rm-category";
|
catDiv.className = "rm-category";
|
||||||
catDiv.setAttribute("data-cat-id", catId);
|
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">`;
|
<div class="rmc-positions">`;
|
||||||
|
|
||||||
category.items.forEach((item) => {
|
items.forEach((item) => {
|
||||||
html += `
|
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="${item.position}" data-category-id="${item.categoryId}" onclick="openItemModal('${item.categoryId}', '${item.position}')" style="cursor: pointer;">
|
||||||
${renderMenuListImage(item.image)}
|
${renderMenuListImage(item.image)}
|
||||||
<div class="rmc-title">
|
<div class="rmc-title">
|
||||||
<h4>${item.title}<span>${item.description}</span></h4>
|
<h4>${item.title}<span>${item.description || ""}</span></h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="rmc-other"><span>${item.price}</span></div>
|
<div class="rmc-other"><span>${item.price}</span></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -280,16 +311,28 @@ export async function loadMenu() {
|
|||||||
catDiv.innerHTML = html;
|
catDiv.innerHTML = html;
|
||||||
container.appendChild(catDiv);
|
container.appendChild(catDiv);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
renderCategoryNav(payload.categories);
|
||||||
|
showCategory(0);
|
||||||
|
|
||||||
|
const searchInput = document.getElementById("menuSearchInput");
|
||||||
|
if (searchInput?.value) {
|
||||||
|
filterMenu();
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Błąd ładowania menu:", err);
|
console.error("Błąd ładowania menu:", err);
|
||||||
const container = document.getElementById("menuContainer");
|
showMenuLoadError(container);
|
||||||
if (container) {
|
|
||||||
container.innerHTML =
|
|
||||||
'<p style="text-align:center; padding: 20px; color: var(--text-muted);">Nie udało się załadować menu.</p>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setMenuLanguageReload() {
|
||||||
|
if (menuLangChangeBound) return;
|
||||||
|
menuLangChangeBound = true;
|
||||||
|
onLangChange(() => loadMenu());
|
||||||
|
}
|
||||||
|
|
||||||
|
setMenuLanguageReload();
|
||||||
|
|
||||||
export function openItemModal(categoryId, position) {
|
export function openItemModal(categoryId, position) {
|
||||||
itemModalKeys = buildVisibleMenuItemKeys();
|
itemModalKeys = buildVisibleMenuItemKeys();
|
||||||
itemModalIndex = itemModalKeys.findIndex(
|
itemModalIndex = itemModalKeys.findIndex(
|
||||||
@@ -366,17 +409,16 @@ export function showCategory(categoryId) {
|
|||||||
clickedLink.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
|
clickedLink.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const categories = document.querySelectorAll(".rm-category");
|
const showAll = categoryId === 0 || categoryId === "0";
|
||||||
categories.forEach((cat) => {
|
const targetId = String(categoryId);
|
||||||
if (categoryId === 0) {
|
|
||||||
cat.style.display = "";
|
document.querySelectorAll(".rm-category").forEach((section) => {
|
||||||
} else {
|
let hasVisible = false;
|
||||||
const catId = parseInt(cat.getAttribute("data-cat-id"), 10);
|
section.querySelectorAll(".rmc-position").forEach((item) => {
|
||||||
if (catId === categoryId) {
|
const match = showAll || String(item.getAttribute("data-category-id")) === targetId;
|
||||||
cat.style.display = "";
|
item.style.display = match ? "" : "none";
|
||||||
} else {
|
if (match) hasVisible = true;
|
||||||
cat.style.display = "none";
|
});
|
||||||
}
|
section.style.display = hasVisible ? "" : "none";
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { endpoints, loaderMinMs } from "./config.js";
|
import { endpoints, loaderMinMs } from "./config.js";
|
||||||
|
import { getLang, t } from "./i18n.js";
|
||||||
import { runPendingProtectedAction, updateNavAccessState } from "./access.js";
|
import { runPendingProtectedAction, updateNavAccessState } from "./access.js";
|
||||||
import {
|
import {
|
||||||
refreshGuestPendingActions,
|
refreshGuestPendingActions,
|
||||||
@@ -30,17 +31,29 @@ const SIX_MONTHS_MS = 180 * 24 * 60 * 60 * 1000;
|
|||||||
const LOADER_MIN_MS = loaderMinMs;
|
const LOADER_MIN_MS = loaderMinMs;
|
||||||
const loadStartTime = Date.now();
|
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;
|
let msgIdx = 0;
|
||||||
const msgInterval = setInterval(() => {
|
const msgInterval = setInterval(() => {
|
||||||
|
const msgs = getLoaderMsgs();
|
||||||
msgIdx = (msgIdx + 1) % msgs.length;
|
msgIdx = (msgIdx + 1) % msgs.length;
|
||||||
if (loaderMsg) loaderMsg.textContent = msgs[msgIdx];
|
if (loaderMsg) loaderMsg.textContent = msgs[msgIdx];
|
||||||
}, 4000);
|
}, 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
|
// Initial State
|
||||||
if (getTableParam()) {
|
if (getTableParam()) {
|
||||||
const tableParam = getTableParam();
|
tableLabel.textContent = formatTableLabel(getTableParam());
|
||||||
tableLabel.textContent = tableParam.toUpperCase().startsWith("STOLIK") ? tableParam : `Stolik ${tableParam}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Storage key must follow current hash (fallback: table), not a frozen empty tableParam at init. */
|
/** 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 response = await fetch(`${endpoints.kds}?h=${encodeURIComponent(hashParam)}`);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (result.status === "success" && result.tableName && result.tableName !== "") {
|
if (result.status === "success" && result.tableName && result.tableName !== "") {
|
||||||
tableLabel.textContent = result.tableName.toUpperCase().startsWith("STOLIK")
|
tableLabel.textContent = formatTableLabel(result.tableName);
|
||||||
? result.tableName
|
|
||||||
: `Stolik ${result.tableName}`;
|
|
||||||
setTableParam(result.tableName);
|
setTableParam(result.tableName);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -106,16 +117,16 @@ function showEmptyState() {
|
|||||||
|
|
||||||
emptyState.classList.remove("hidden");
|
emptyState.classList.remove("hidden");
|
||||||
itemsList.innerHTML = "";
|
itemsList.innerHTML = "";
|
||||||
prepStatus.textContent = "Brak aktywnych zamówień";
|
prepStatus.textContent = t("status.none");
|
||||||
statusIcon.textContent = "🍃";
|
statusIcon.textContent = "🍃";
|
||||||
progressBar.style.width = "0%";
|
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
|
// Historia może istnieć nawet gdy brak bieżących pozycji
|
||||||
renderGlobalHistory();
|
renderGlobalHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeArticleName(rawName) {
|
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".
|
// Usuwa gramatury typu: "300G", "250 G", "500/200/150G".
|
||||||
const withoutWeight = name.replace(
|
const withoutWeight = name.replace(
|
||||||
@@ -126,7 +137,7 @@ function normalizeArticleName(rawName) {
|
|||||||
return withoutWeight
|
return withoutWeight
|
||||||
.replace(/\s{2,}/g, " ")
|
.replace(/\s{2,}/g, " ")
|
||||||
.replace(/\s+([,.;:!?])/g, "$1")
|
.replace(/\s+([,.;:!?])/g, "$1")
|
||||||
.trim() || "Pozycja";
|
.trim() || t("orders.item_fallback");
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadPersistedItems() {
|
function loadPersistedItems() {
|
||||||
@@ -202,6 +213,7 @@ function addItemsToGlobalHistory(items, sourceTable) {
|
|||||||
|
|
||||||
export function renderGlobalHistory() {
|
export function renderGlobalHistory() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
const lang = getLang();
|
||||||
const history = loadGlobalHistory()
|
const history = loadGlobalHistory()
|
||||||
.filter((e) => now - (e.archivedAt || 0) <= SIX_MONTHS_MS)
|
.filter((e) => now - (e.archivedAt || 0) <= SIX_MONTHS_MS)
|
||||||
.sort((a, b) => (b.archivedAt || 0) - (a.archivedAt || 0));
|
.sort((a, b) => (b.archivedAt || 0) - (a.archivedAt || 0));
|
||||||
@@ -224,7 +236,11 @@ export function renderGlobalHistory() {
|
|||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<div class="item-info">
|
<div class="item-info">
|
||||||
<span class="item-name">${entry.name}</span>
|
<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>
|
||||||
<div class="item-qty">x${entry.qty}</div>
|
<div class="item-qty">x${entry.qty}</div>
|
||||||
`;
|
`;
|
||||||
@@ -234,7 +250,7 @@ export function renderGlobalHistory() {
|
|||||||
|
|
||||||
export function clearGlobalHistory(e) {
|
export function clearGlobalHistory(e) {
|
||||||
if (e) e.preventDefault();
|
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);
|
localStorage.removeItem(historyKey);
|
||||||
renderGlobalHistory();
|
renderGlobalHistory();
|
||||||
}
|
}
|
||||||
@@ -282,7 +298,7 @@ function mergeWithPersistedItems(articles) {
|
|||||||
// Aktywne na górze, gotowe (zniknięte) na dole
|
// Aktywne na górze, gotowe (zniknięte) na dole
|
||||||
merged.sort((a, b) => {
|
merged.sort((a, b) => {
|
||||||
if (a.present !== b.present) return a.present ? -1 : 1;
|
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);
|
savePersistedItems(merged);
|
||||||
@@ -301,11 +317,11 @@ function renderItems(items) {
|
|||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
div.className = `item-card ${isReady ? "ready" : ""} ${item.present ? "" : "archived"}`;
|
div.className = `item-card ${isReady ? "ready" : ""} ${item.present ? "" : "archived"}`;
|
||||||
|
|
||||||
let meta = "🔥 W przygotowaniu";
|
let meta = t("orders.item_preparing");
|
||||||
if (isReady && item.completedByDisappear) {
|
if (isReady && item.completedByDisappear) {
|
||||||
meta = "✅ Gotowe (zrealizowane)";
|
meta = t("orders.item_ready_done");
|
||||||
} else if (isReady) {
|
} else if (isReady) {
|
||||||
meta = "✅ Gotowe";
|
meta = t("orders.item_ready");
|
||||||
}
|
}
|
||||||
|
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
@@ -333,29 +349,29 @@ function updateStatus(bills, items) {
|
|||||||
progressBar.style.width = `${pct}%`;
|
progressBar.style.width = `${pct}%`;
|
||||||
|
|
||||||
if (pct >= 100) {
|
if (pct >= 100) {
|
||||||
prepStatus.textContent = "Gotowe do podania!";
|
prepStatus.textContent = t("status.ready");
|
||||||
statusIcon.innerHTML = "😋";
|
statusIcon.innerHTML = "😋";
|
||||||
statusMeta.textContent = "Wszystkie Twoje dania opuściły już kuchnię.";
|
statusMeta.textContent = t("status.ready_meta");
|
||||||
} else if (pct > 0) {
|
} else if (pct > 0) {
|
||||||
prepStatus.textContent = "Częściowo gotowe";
|
prepStatus.textContent = t("status.partial");
|
||||||
statusIcon.innerHTML = "🍳";
|
statusIcon.innerHTML = "🍳";
|
||||||
statusMeta.textContent = "Pierwsze pyszności już na Ciebie czekają!";
|
statusMeta.textContent = t("status.partial_meta");
|
||||||
} else {
|
} else {
|
||||||
prepStatus.textContent = "W przygotowaniu";
|
prepStatus.textContent = t("status.preparing");
|
||||||
if (!window.selectedAnimationHtml) {
|
if (!window.selectedAnimationHtml) {
|
||||||
window.selectedAnimationHtml =
|
window.selectedAnimationHtml =
|
||||||
window.kitchenAnimations[Math.floor(Math.random() * window.kitchenAnimations.length)];
|
window.kitchenAnimations[Math.floor(Math.random() * window.kitchenAnimations.length)];
|
||||||
}
|
}
|
||||||
statusIcon.innerHTML = window.selectedAnimationHtml;
|
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
|
// Footer meta
|
||||||
const newest = [...bills].sort((a, b) => new Date(b?.Date || 0) - new Date(a?.Date || 0))[0];
|
const newest = [...bills].sort((a, b) => new Date(b?.Date || 0) - new Date(a?.Date || 0))[0];
|
||||||
const time = newest?.Date
|
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() {
|
export async function fetchOrders() {
|
||||||
@@ -371,9 +387,7 @@ export async function fetchOrders() {
|
|||||||
|
|
||||||
if (result.status === "success") {
|
if (result.status === "success") {
|
||||||
if (result.tableName && result.tableName !== "") {
|
if (result.tableName && result.tableName !== "") {
|
||||||
tableLabel.textContent = result.tableName.toUpperCase().startsWith("STOLIK")
|
tableLabel.textContent = formatTableLabel(result.tableName);
|
||||||
? result.tableName
|
|
||||||
: `Stolik ${result.tableName}`;
|
|
||||||
setTableParam(result.tableName); // Aktualizacja do właściwej nazwy na poczet innych zapytań
|
setTableParam(result.tableName); // Aktualizacja do właściwej nazwy na poczet innych zapytań
|
||||||
refreshGuestPendingActions();
|
refreshGuestPendingActions();
|
||||||
startGuestPendingPoll();
|
startGuestPendingPoll();
|
||||||
@@ -419,9 +433,9 @@ export async function fetchOrders() {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} else {
|
} else {
|
||||||
loaderMsg.textContent = "Błąd API: " + result.message;
|
loaderMsg.textContent = t("loader.api_error", { message: result.message });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
loaderMsg.textContent = "Problem z połączeniem. Próbujemy ponownie...";
|
loaderMsg.textContent = t("loader.connection");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { endpoints } from "./config.js";
|
import { endpoints } from "./config.js";
|
||||||
|
import { t } from "./i18n.js";
|
||||||
import { showToast } from "./toast.js";
|
import { showToast } from "./toast.js";
|
||||||
import {
|
import {
|
||||||
getBillState,
|
getBillState,
|
||||||
@@ -75,36 +76,42 @@ export function formatGuestQueueMessage(title, lines = []) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildWaiterCallQueueMessage() {
|
export function buildWaiterCallQueueMessage() {
|
||||||
return "Przywołanie kelnera";
|
return t("waiter.queue_title");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildBillRequestQueueMessage(docType) {
|
export function buildBillRequestQueueMessage(docType) {
|
||||||
const billState = getBillState();
|
const billState = getBillState();
|
||||||
const lines = [
|
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") {
|
if (docType === "faktura") {
|
||||||
lines.push({ label: "NIP:", value: billState.nip || "—" });
|
lines.push({ label: t("bill.queue_nip"), value: billState.nip || "—" });
|
||||||
lines.push({ label: "Firma:", value: billState.company?.name || "—" });
|
lines.push({ label: t("bill.queue_company"), value: billState.company?.name || "—" });
|
||||||
const addressParts = [
|
const addressParts = [
|
||||||
billState.company?.street,
|
billState.company?.street,
|
||||||
[billState.company?.zip, billState.company?.city].filter(Boolean).join(" "),
|
[billState.company?.zip, billState.company?.city].filter(Boolean).join(" "),
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
if (addressParts.length) {
|
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) {
|
export function guestActionBlockedMessage(messageType) {
|
||||||
if (messageType === "waiter_call") {
|
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() {
|
export function updateGuestActionNavState() {
|
||||||
|
|||||||
@@ -18,14 +18,22 @@ function publicAssetVersion(string $publicDir, string $relativePath): string
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Najnowszy mtime wśród pliku głównego i wszystkich .js w katalogu modules/.
|
* Najnowszy mtime wśród pliku głównego i wszystkich .js w katalogach modules/ i locales/.
|
||||||
*/
|
*/
|
||||||
function publicJsBundleVersion(string $publicDir, string $entryRelativePath, string $modulesRelativeDir = 'assets/js/modules'): string
|
function publicJsBundleVersion(string $publicDir, string $entryRelativePath): string
|
||||||
{
|
{
|
||||||
$versions = [(int) publicAssetVersion($publicDir, $entryRelativePath)];
|
$versions = [(int) publicAssetVersion($publicDir, $entryRelativePath)];
|
||||||
$modulesDir = $publicDir . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, ltrim($modulesRelativeDir, '/'));
|
$dirs = [
|
||||||
|
'assets/js/modules',
|
||||||
|
'assets/js/locales',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($dirs as $relativeDir) {
|
||||||
|
$modulesDir = $publicDir . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, ltrim($relativeDir, '/'));
|
||||||
|
if (!is_dir($modulesDir)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (is_dir($modulesDir)) {
|
|
||||||
$iterator = new RecursiveIteratorIterator(
|
$iterator = new RecursiveIteratorIterator(
|
||||||
new RecursiveDirectoryIterator($modulesDir, FilesystemIterator::SKIP_DOTS)
|
new RecursiveDirectoryIterator($modulesDir, FilesystemIterator::SKIP_DOTS)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,828 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"categoryName": "Na dobry początek",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"position": "331",
|
|
||||||
"categoryId": "1",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/losos-na-placku.webp",
|
|
||||||
"title": "Wędzony łosoś na chrupkim placku ziemniaczanym",
|
|
||||||
"description": "ze śmietaną i sosem a'la duńskim",
|
|
||||||
"price": "31,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "11",
|
|
||||||
"categoryId": "1",
|
|
||||||
"tag": "kiszone,smalec",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/smalczyk.webp",
|
|
||||||
"title": "Skiśnięte ogórki i smolec swojej roboty z pieczywem",
|
|
||||||
"description": "",
|
|
||||||
"price": "28,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "12",
|
|
||||||
"categoryId": "1",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/przystawki-Tatar-siekany.webp",
|
|
||||||
"title": "Tatar siekany z wołowiny z piklami i pieczywem",
|
|
||||||
"description": "",
|
|
||||||
"price": "62,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "332",
|
|
||||||
"categoryId": "1",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/400_320_crop/sledz-w-oleju.webp",
|
|
||||||
"title": "Śledź w oleju z kwaśnym dżemem morelowo-żurawinowym podany z pieczywem",
|
|
||||||
"description": "",
|
|
||||||
"price": "29,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "260",
|
|
||||||
"categoryId": "1",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/oscypki.webp",
|
|
||||||
"title": "Grillowane serki góralskie",
|
|
||||||
"description": "z żurawiną z Podhala",
|
|
||||||
"price": "21,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "187",
|
|
||||||
"categoryId": "1",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/400_320_crop/rydze-z-cebula-na-grzance.webp",
|
|
||||||
"title": "Rydze z cebulą na grzance",
|
|
||||||
"description": "",
|
|
||||||
"price": "33,00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"categoryName": "Polywki",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"position": "18",
|
|
||||||
"categoryId": "2",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/wielkanocne-2020/400_320_crop/Karczma-biesiada-stalowa-wola-dania-na-wielkanoc.webp",
|
|
||||||
"title": "Żurek na wędzonce z jajkiem w chlebie",
|
|
||||||
"description": "",
|
|
||||||
"price": "31,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "22",
|
|
||||||
"categoryId": "2",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/rosol-z-makaronem.webp",
|
|
||||||
"title": "Rosół wiejski z trzech rodzajów mięs z makaronem",
|
|
||||||
"description": "",
|
|
||||||
"price": "19,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "25",
|
|
||||||
"categoryId": "2",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/zupy-Obszar-kompozycji-5.webp",
|
|
||||||
"title": "Barszcz czerwony ze swojskimi uszkami",
|
|
||||||
"description": "",
|
|
||||||
"price": "26,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "374",
|
|
||||||
"categoryId": "2",
|
|
||||||
"tag": "",
|
|
||||||
"image": "/themes/karczmabiesiadanew/images/icon-menu.jpg",
|
|
||||||
"title": "Krem z pomidorów z mozzarellą",
|
|
||||||
"description": "",
|
|
||||||
"price": "22,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "377",
|
|
||||||
"categoryId": "2",
|
|
||||||
"tag": "",
|
|
||||||
"image": "/themes/karczmabiesiadanew/images/icon-menu.jpg",
|
|
||||||
"title": "Kwaśnica z żeberkiem",
|
|
||||||
"description": "",
|
|
||||||
"price": "26,00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"categoryName": "Co przez gospodarza lubiane, a i Wam polecane",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"position": "363",
|
|
||||||
"categoryId": "3",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/aktualnosci/nowe-smaki-lato-2024/400_320_crop/pyszne-jedzenie-karczma-biesiada-6.webp",
|
|
||||||
"title": "Schab po zbóju z ogórkiem kiszonym podany z białym sosem i ziemniakami opiekanymi",
|
|
||||||
"description": "",
|
|
||||||
"price": "53,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "324",
|
|
||||||
"categoryId": "3",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/miesa-Roladki-z-suszonymi-pomidorami.webp",
|
|
||||||
"title": "Kurczak z mozarellą i suszonymi pomidorami",
|
|
||||||
"description": "zawinięty boczkiem z dipem buraczkowym podany z kopytkami",
|
|
||||||
"price": "41,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "379",
|
|
||||||
"categoryId": "",
|
|
||||||
"tag": "",
|
|
||||||
"image": "/themes/karczmabiesiadanew/images/icon-menu.jpg",
|
|
||||||
"title": "Polędwiczki w sosie kurkowym z kopytkami i ogórkiem kiszonym",
|
|
||||||
"description": "",
|
|
||||||
"price": "54,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "188",
|
|
||||||
"categoryId": "3",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/400_320_crop/kaczka.webp",
|
|
||||||
"title": "Soczysta pierś z kaczki",
|
|
||||||
"description": "z sosem porzeczkowym z kopytkami i bukietem surówek",
|
|
||||||
"price": "69,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "351",
|
|
||||||
"categoryId": "3",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/nowosci-2024/400_320_crop/1-1.webp",
|
|
||||||
"title": "Stek z polędwicy wołowej",
|
|
||||||
"description": "z puree ziemniaczanym i sosem z zielonego pieprzu",
|
|
||||||
"price": "99,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "238",
|
|
||||||
"categoryId": "3",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/syte-koryto.webp",
|
|
||||||
"title": "Syte koryto (micha mięs na stół)",
|
|
||||||
"description": "W zestawie podwójna porcja: golonki, karkówki, schabu z kością, kiełbaski baraniej, szaszłyka, bekonu, ziemniaków opiekanych i kapusty zasmażanej,",
|
|
||||||
"price": "275,00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"categoryName": "Dania mięsne",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"position": "325",
|
|
||||||
"categoryId": "3",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/miesa-karczek-grillowany.webp",
|
|
||||||
"title": "Karczek grillowany z sosem BBQ",
|
|
||||||
"description": "z ziemniakami opiekanymi i sałatką wiosenną",
|
|
||||||
"price": "51,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "272",
|
|
||||||
"categoryId": "3",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/miesa-filet-w-sosie-smietanowo_.webp",
|
|
||||||
"title": "Filet drobiowy w sosie śmietanowo-koperkowym",
|
|
||||||
"description": "z ziemniakami gotowanymi i bukietem surówek",
|
|
||||||
"price": "42,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "273",
|
|
||||||
"categoryId": "3",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/filet.webp",
|
|
||||||
"title": "Filet z kurczaka panierowany",
|
|
||||||
"description": "z frytkami i surówką z białej kapusty",
|
|
||||||
"price": "39,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "274",
|
|
||||||
"categoryId": "3",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/miesa-schabowy2.webp",
|
|
||||||
"title": "Kotlet schabowy panierowany",
|
|
||||||
"description": "z ziemniakami opiekanymi i kapustą zasmażaną",
|
|
||||||
"price": "40,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "326",
|
|
||||||
"categoryId": "3",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/miesa-golonka1.webp",
|
|
||||||
"title": "Golonka po chłopsku z rusztu",
|
|
||||||
"description": "(z kością) z ziemniakami opiekanymi i kapustą zasmażaną",
|
|
||||||
"price": "58,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "364",
|
|
||||||
"categoryId": "3",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/aktualnosci/nowe-smaki-lato-2024/400_320_crop/pyszne-jedzenie-karczma-biesiada-8.webp",
|
|
||||||
"title": "Żeberka pieczone podane na kapuście zasmażanej z ziemniakami opiekanymi",
|
|
||||||
"description": "",
|
|
||||||
"price": "56,00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"categoryName": "To co w wodzie pływo",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"position": "39",
|
|
||||||
"categoryId": "5",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/pstrag_nowy.webp",
|
|
||||||
"title": "Pstrąg pieczony",
|
|
||||||
"description": "z warzywami sezonowymi i ziemniakami",
|
|
||||||
"price": "57,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "284",
|
|
||||||
"categoryId": "5",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/morszczuk.webp",
|
|
||||||
"title": "Morszczuk w panierce",
|
|
||||||
"description": "z frytkami i surówką z marchwii i ananasa",
|
|
||||||
"price": "40,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "352",
|
|
||||||
"categoryId": "",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/sandacz-na-sosie-cytrynowym-karczma-biesiada-stalowa-wola.webp",
|
|
||||||
"title": "Sandacz podany na sosie śmietanowo-cytrynowym",
|
|
||||||
"description": "z zapiekanką ziemniaczaną i warzywami sezonowymi",
|
|
||||||
"price": "75,00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"categoryName": "Dania swojskie",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"position": "45",
|
|
||||||
"categoryId": "4",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/miesa-placek-po-wiejsku.webp",
|
|
||||||
"title": "Placek po wiejsku z gulaszem",
|
|
||||||
"description": "i surówką z białej kapusty",
|
|
||||||
"price": "40,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "46",
|
|
||||||
"categoryId": "4",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/nalesniki-ze-szpinakiem.webp",
|
|
||||||
"title": "Naleśniki ze szpinakiem",
|
|
||||||
"description": "",
|
|
||||||
"price": "27,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "47",
|
|
||||||
"categoryId": "4",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/pierogi-ruskie.webp",
|
|
||||||
"title": "Pierogi ruskie",
|
|
||||||
"description": "z omastą lub z patelni",
|
|
||||||
"price": "27,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "365",
|
|
||||||
"categoryId": "4",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/aktualnosci/nowe-smaki-lato-2024/400_320_crop/pyszne-jedzenie-karczma-biesiada-3.webp",
|
|
||||||
"title": "Pierogi z oscypkiem, żurawiną i chipsami z cebulki",
|
|
||||||
"description": "",
|
|
||||||
"price": "31,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "353",
|
|
||||||
"categoryId": "4",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/400_320_crop/pierogi-z-miesem.webp",
|
|
||||||
"title": "Pierogi z mięsem z omastą lub z patelni",
|
|
||||||
"description": "",
|
|
||||||
"price": "29,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "51",
|
|
||||||
"categoryId": "4",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/golabki-w-sosie-pomidorowym.webp",
|
|
||||||
"title": "Gołąbki w sosie pomidorowym",
|
|
||||||
"description": "",
|
|
||||||
"price": "31,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "52",
|
|
||||||
"categoryId": "4",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/placki_z_wedzonka.webp",
|
|
||||||
"title": "Placki ziemniaczane ze śmietaną",
|
|
||||||
"description": "",
|
|
||||||
"price": "31,00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"categoryName": "Makarony",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"position": "373",
|
|
||||||
"categoryId": "6",
|
|
||||||
"tag": "",
|
|
||||||
"image": "/themes/karczmabiesiadanew/images/icon-menu.jpg",
|
|
||||||
"title": "Makaron tagliatelle z kurczakiem i szpinakiem w sosie śmietanowym",
|
|
||||||
"description": "",
|
|
||||||
"price": "41,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "355",
|
|
||||||
"categoryId": "6",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/nowosci-2024/400_320_crop/5.webp",
|
|
||||||
"title": "Makaron tagliatelle z podgrzybkami, gorgonzolą i boczkiem",
|
|
||||||
"description": "",
|
|
||||||
"price": "44,00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"categoryName": "Sałatki",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"position": "321",
|
|
||||||
"categoryId": "7",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/Salatka-z-ananasem-grillowanym-kurczakiem.webp",
|
|
||||||
"title": "Sałatka z grillowanym kurczakiem i ananasem",
|
|
||||||
"description": "Kurczak, ananas, sałata lodowa, jajo, ogórek, pomidor, sos ogrodowy, sos słodkie chili",
|
|
||||||
"price": "39,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "320",
|
|
||||||
"categoryId": "7",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/salatka-grecka.webp",
|
|
||||||
"title": "Sałatka Grecka",
|
|
||||||
"description": "Ser favita, oliwki, cebula, ogórek, pomidor, sałata lodowa, sos ogrodowy",
|
|
||||||
"price": "37,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "322",
|
|
||||||
"categoryId": "7",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/salatka-cezar.webp",
|
|
||||||
"title": "Sałatka Cezar",
|
|
||||||
"description": "Z kurczakiem, sosem czosnkowym, sałatą lodową, ser corregio i grzankami",
|
|
||||||
"price": "44,00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"categoryName": "Burgery",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"position": "361",
|
|
||||||
"categoryId": "",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/Burger-wolowy-karczma-biesiada-stalowa-wola.webp",
|
|
||||||
"title": "Buła z wołowiną",
|
|
||||||
"description": "",
|
|
||||||
"price": "41,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "362",
|
|
||||||
"categoryId": "",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/nowosci-2024/400_320_crop/3.webp",
|
|
||||||
"title": "Buła z kurczakiem",
|
|
||||||
"description": "",
|
|
||||||
"price": "38,00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"categoryName": "Dania dla dzieci",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"position": "287",
|
|
||||||
"categoryId": "9",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/400_320_crop/rosol-dla-dzieci.webp",
|
|
||||||
"title": "Rosół z makaronem",
|
|
||||||
"description": "porcja dziecięca",
|
|
||||||
"price": "14,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "293",
|
|
||||||
"categoryId": "9",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/dzieci-nuggetsy-z-kurczaka.webp",
|
|
||||||
"title": "Nuggetsy z kurczaka",
|
|
||||||
"description": "z frytkami i surówką z marchwii i ananasa",
|
|
||||||
"price": "33,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "294",
|
|
||||||
"categoryId": "9",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/400_320_crop/filet-z-frytkami-dla-dzieci.webp",
|
|
||||||
"title": "Filet z kurczaka z frytkami i bukietem surówek",
|
|
||||||
"description": "porcja dziecięca",
|
|
||||||
"price": "27,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "295",
|
|
||||||
"categoryId": "9",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/400_320_crop/morszczuk-w-panierce-dla-dzieci.webp",
|
|
||||||
"title": "Morszczuk w panierce",
|
|
||||||
"description": "z ziemniakami i surówką z białej kapusty (porcja dziecięca)",
|
|
||||||
"price": "27,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "296",
|
|
||||||
"categoryId": "9",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/400_320_crop/filet-w-sosie-dla-dzieci.webp",
|
|
||||||
"title": "Filet w sosie śmietanowo-koperkowym",
|
|
||||||
"description": "z ziemniakami gotowanymi (porcja dziecięca)",
|
|
||||||
"price": "28,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "298",
|
|
||||||
"categoryId": "9",
|
|
||||||
"tag": "deser",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/nalesniki.webp",
|
|
||||||
"title": "Naleśniki z serem",
|
|
||||||
"description": "",
|
|
||||||
"price": "24,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "299",
|
|
||||||
"categoryId": "9",
|
|
||||||
"tag": "deser",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/dzieci-Nalesniki-z-nutella.webp",
|
|
||||||
"title": "Naleśniki z nutellą",
|
|
||||||
"description": "",
|
|
||||||
"price": "28,00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"categoryName": "Dodatki do drugich dań",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"position": "56",
|
|
||||||
"categoryId": "8",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/400_320_crop/DSC088432.webp",
|
|
||||||
"title": "Ziemniaki gotowane",
|
|
||||||
"description": "",
|
|
||||||
"price": "10,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "57",
|
|
||||||
"categoryId": "8",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/dodatki-ziemniaki-opiekane.webp",
|
|
||||||
"title": "Ziemniaki opiekane",
|
|
||||||
"description": "",
|
|
||||||
"price": "11,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "58",
|
|
||||||
"categoryId": "8",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/400_320_crop/frytki.webp",
|
|
||||||
"title": "Frytki",
|
|
||||||
"description": "",
|
|
||||||
"price": "12,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "380",
|
|
||||||
"categoryId": "",
|
|
||||||
"tag": "",
|
|
||||||
"image": "/themes/karczmabiesiadanew/images/icon-menu.jpg",
|
|
||||||
"title": "Kopytka",
|
|
||||||
"description": "",
|
|
||||||
"price": "11,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "61",
|
|
||||||
"categoryId": "8",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/bukiet.webp",
|
|
||||||
"title": "Bukiet surówek",
|
|
||||||
"description": "",
|
|
||||||
"price": "12,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "279",
|
|
||||||
"categoryId": "8",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/salatka-wiosenna.webp",
|
|
||||||
"title": "Sałatka wiosenna",
|
|
||||||
"description": "Pomidor, ogórek, papryka na liściu sałaty z lekkim sosem ogrodowym",
|
|
||||||
"price": "11,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "62",
|
|
||||||
"categoryId": "8",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/3.webp",
|
|
||||||
"title": "Surówka z białej kapusty",
|
|
||||||
"description": "",
|
|
||||||
"price": "10,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "67",
|
|
||||||
"categoryId": "8",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/buraczki.webp",
|
|
||||||
"title": "Surówka z buraczków",
|
|
||||||
"description": "",
|
|
||||||
"price": "10,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "63",
|
|
||||||
"categoryId": "8",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/2.webp",
|
|
||||||
"title": "Surówka z marchwi i ananasa",
|
|
||||||
"description": "",
|
|
||||||
"price": "10,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "65",
|
|
||||||
"categoryId": "8",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/kapusta-zasmazana.webp",
|
|
||||||
"title": "Kapusta zasmażana",
|
|
||||||
"description": "",
|
|
||||||
"price": "12,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "381",
|
|
||||||
"categoryId": "",
|
|
||||||
"tag": "",
|
|
||||||
"image": "/themes/karczmabiesiadanew/images/icon-menu.jpg",
|
|
||||||
"title": "Mizeria",
|
|
||||||
"description": "",
|
|
||||||
"price": "11,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "66",
|
|
||||||
"categoryId": "8",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/ogorek-kiszony.webp",
|
|
||||||
"title": "Ogórek kiszony",
|
|
||||||
"description": "",
|
|
||||||
"price": "10,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "68",
|
|
||||||
"categoryId": "8",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/dodatki-Chlebek-swojski-z-ziarnami.webp",
|
|
||||||
"title": "Chlebek swojski",
|
|
||||||
"description": "",
|
|
||||||
"price": "6,00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"categoryName": "Cosik na słodko",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"position": "77",
|
|
||||||
"categoryId": "10",
|
|
||||||
"tag": "deser",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/400_320_crop/szarlotka.webp",
|
|
||||||
"title": "Szarlotka na gorąco z gałką lodów",
|
|
||||||
"description": "",
|
|
||||||
"price": "26,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "78",
|
|
||||||
"categoryId": "10",
|
|
||||||
"tag": "deser",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/photo_2024-07-17_13-24-30.webp",
|
|
||||||
"title": "Sernik na musie truskawkowym",
|
|
||||||
"description": "",
|
|
||||||
"price": "24,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "382",
|
|
||||||
"categoryId": "",
|
|
||||||
"tag": "",
|
|
||||||
"image": "/themes/karczmabiesiadanew/images/icon-menu.jpg",
|
|
||||||
"title": "Fondant czekoladowy na ciepło podany na sosie wiśniowym z gałką lodów",
|
|
||||||
"description": "",
|
|
||||||
"price": "31,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "327",
|
|
||||||
"categoryId": "10",
|
|
||||||
"tag": "deser",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/deser-3.webp",
|
|
||||||
"title": "Deser lodowy",
|
|
||||||
"description": "3 gałki, smaki do wyboru",
|
|
||||||
"price": "20,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "292",
|
|
||||||
"categoryId": "10",
|
|
||||||
"tag": "deser",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/deser-lodowy-dla-dzieci.webp",
|
|
||||||
"title": "Mini deser lodowy",
|
|
||||||
"description": "2 gałki, smaki do wyboru",
|
|
||||||
"price": "17,00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"categoryName": "Napitki",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"position": "81",
|
|
||||||
"categoryId": "11",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/photo_2024-07-17_13-24-04.webp",
|
|
||||||
"title": "Espresso",
|
|
||||||
"description": "",
|
|
||||||
"price": "9,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "82",
|
|
||||||
"categoryId": "11",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/400_320_crop/kawa-czarna.webp",
|
|
||||||
"title": "Kawa czarna",
|
|
||||||
"description": "",
|
|
||||||
"price": "10,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "83",
|
|
||||||
"categoryId": "11",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/kawa-biala1.webp",
|
|
||||||
"title": "Kawa biała",
|
|
||||||
"description": "",
|
|
||||||
"price": "11,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "84",
|
|
||||||
"categoryId": "11",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/napoje-kawa-cappuccino.webp",
|
|
||||||
"title": "Cappuccino",
|
|
||||||
"description": "",
|
|
||||||
"price": "14,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "86",
|
|
||||||
"categoryId": "11",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/400_320_crop/latte.webp",
|
|
||||||
"title": "Latte Machiato",
|
|
||||||
"description": "",
|
|
||||||
"price": "14,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "90",
|
|
||||||
"categoryId": "11",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/400_320_crop/herbata-czarna.webp",
|
|
||||||
"title": "Herbata czarna",
|
|
||||||
"description": "",
|
|
||||||
"price": "10,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "202",
|
|
||||||
"categoryId": "11",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/herbata1.webp",
|
|
||||||
"title": "Herbata Richmont",
|
|
||||||
"description": "",
|
|
||||||
"price": "14,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "329",
|
|
||||||
"categoryId": "11",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/herbata-owocowa1.webp",
|
|
||||||
"title": "Herbata owocowa Richmont",
|
|
||||||
"description": "",
|
|
||||||
"price": "14,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "330",
|
|
||||||
"categoryId": "11",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/menu/400_320_crop/photo_2024-07-19_08-00-49.webp",
|
|
||||||
"title": "Herbata zielona Richmont",
|
|
||||||
"description": "",
|
|
||||||
"price": "14,00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"categoryName": "Napitki dla wysusonyk",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"position": "301",
|
|
||||||
"categoryId": "11",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/potrawy/400_320_crop/napoje-lemoniady.webp",
|
|
||||||
"title": "Orzeźwiająca lemoniada",
|
|
||||||
"description": "",
|
|
||||||
"price": "18,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "302",
|
|
||||||
"categoryId": "11",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/400_320_crop/modzajto-light.webp",
|
|
||||||
"title": "Modżajto light",
|
|
||||||
"description": "Drink bezalkoholowy",
|
|
||||||
"price": "20,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "304",
|
|
||||||
"categoryId": "11",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/400_320_crop/sok-wyciskany.webp",
|
|
||||||
"title": "Sok wyciskany ze świeżych pomarańczy lub grejpfrutów",
|
|
||||||
"description": "",
|
|
||||||
"price": "20,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "305",
|
|
||||||
"categoryId": "11",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/logos/400_320_crop/logo-cocacola.webp",
|
|
||||||
"title": "Coca-Cola, Coca-Cola zero, Fanta, Sprite",
|
|
||||||
"description": "",
|
|
||||||
"price": "11,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "309",
|
|
||||||
"categoryId": "11",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/logos/400_320_crop/Kinley.webp",
|
|
||||||
"title": "Tonic Kinley",
|
|
||||||
"description": "Tonic Water",
|
|
||||||
"price": "11,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "310",
|
|
||||||
"categoryId": "11",
|
|
||||||
"tag": "woda",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/logos/400_320_crop/logo-kropla.webp",
|
|
||||||
"title": "Kropla Beskidu 330ml",
|
|
||||||
"description": "gazowana, niegazowana",
|
|
||||||
"price": "9,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "359",
|
|
||||||
"categoryId": "",
|
|
||||||
"tag": "",
|
|
||||||
"image": "/themes/karczmabiesiadanew/images/icon-menu.jpg",
|
|
||||||
"title": "Dzbanek wody gazowana/niegazowana",
|
|
||||||
"description": "",
|
|
||||||
"price": "19,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "307",
|
|
||||||
"categoryId": "11",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/logos/400_320_crop/logo-fuzetea.webp",
|
|
||||||
"title": "Fuzetea",
|
|
||||||
"description": "cytrynowa z trawą cytrynową, brzoskwiniowa z hibiskusem",
|
|
||||||
"price": "10,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "308",
|
|
||||||
"categoryId": "11",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/logos/400_320_crop/logo-cappy.webp",
|
|
||||||
"title": "Cappy",
|
|
||||||
"description": "pomarańcza, jabłko, multiwitamina, czarna porzeczka, grejpfrut",
|
|
||||||
"price": "10,00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"position": "312",
|
|
||||||
"categoryId": "11",
|
|
||||||
"tag": "",
|
|
||||||
"image": "https://www.karczmabiesiada.eu/cache/images/files/logos/400_320_crop/logo-burn.webp",
|
|
||||||
"title": "Burn",
|
|
||||||
"description": "napój energetyczny",
|
|
||||||
"price": "13,00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?php
|
||||||
|
$_SERVER['REQUEST_METHOD'] = 'GET';
|
||||||
|
$_GET['lang'] = $argv[1] ?? 'pl';
|
||||||
|
include __DIR__ . '/../api/menu.php';
|
||||||
Reference in New Issue
Block a user