@php /** * In-restaurant self-order touch panel. * @var array $restaurant @var array $categories @var array $items * @var array $featured @var array $deals @var array $optJson @var array $ingJson * @var string $cuisineName @var string $tableLabel @var bool $dineIn * @var bool $orderingOn @var bool $codOn @var bool $offlineOn @var array $cart * @var bool $headerEnabled @var bool $sidebarEnabled * @var string $closedNotice @var float $minOrder * * The basket is the session cart (/cart/add) and "place order" submits the * standard checkout, so kiosk orders are priced, recorded and tracked exactly * like storefront orders. A table-mounted kiosk (?t={qr_token}) is bound to * its table and places dine-in orders; a counter kiosk places pickup orders. * * $headerEnabled (Vendor Settings > Dine-in, default on) shows the standard * site header + cuisines panel above the kiosk shell, so a guest - or a * store reviewer - can still leave the kiosk and browse the rest of the site. * An owner running a dedicated in-restaurant tablet may turn it off for a * fully immersive, chrome-free shell. * * $sidebarEnabled (same tab, default OFF) adds the storefront's left store-type * rail. It is separate because a fixed in-restaurant panel is ordering from THIS * kitchen, so a rail that navigates away is usually unwanted even when the header * is kept. The header's burger opens that rail, so it is only rendered when the * rail is - otherwise it would control nothing. */ use App\Services\PreviewMenu; use App\Support\App; $curSym = preg_replace('/[0-9.,\s]/', '', money(0)) ?: '$'; // Seconds a guest has to intervene before the kiosk resets itself. One number for both // of the shell's resets - the order-placed screen and the idle warning - so a guest // never meets two different countdowns on the same panel. $resetCountdown = 20; $itemsByCat = []; foreach ($items as $it) { $itemsByCat[(int) $it['menu_category_id']][] = $it; } $catIcon = function (string $name): string { $slug = PreviewMenu::categoryIcon3d($name) ?: 'diner-grill'; return ''; }; $czAttrs = function (array $it) use ($optJson, $ingJson, $__env): string { $id = (int) $it['id']; return 'data-cz' . ' data-cz-id="' . $id . '"' . ' data-cz-name="' . e($it['name']) . '"' . ' data-cz-price="' . e(number_format((float) $it['price'], 2, '.', '')) . '"' . ' data-cz-img="' . e(media($it['image'] ?? '', 'assets/client/dish/dish-1.png')) . '"' . ' data-cz-desc="' . e((string) ($it['description'] ?? '')) . '"' . ' data-cz-options="' . e($optJson[$id] ?? '[]') . '"' . ' data-cz-ings="' . e($ingJson[$id] ?? '[]') . '"'; }; $dealBadge = function (array $d) use ($curSym, $__env): string { $value = rtrim(rtrim(number_format((float) $d['value'], 2), '0'), '.'); return $d['type'] === 'percent' ? t_raw('common.deal_percent_off', [':n' => $value]) : t_raw('common.deal_amount_off', [':amount' => $curSym . $value]); }; @endphp {{ $title ?? t('kiosk.menu_fallback_name') }} @php $favicon = setting('favicon', 'assets/admin/favicon.png'); $favExt = strtolower(pathinfo((string) $favicon, PATHINFO_EXTENSION)); $favType = $favExt === 'svg' ? 'image/svg+xml' : ($favExt === 'ico' ? 'image/x-icon' : 'image/png'); @endphp @if ($headerEnabled) @endif @php $primary = setting('primary_color', ''); @endphp @if ($primary) @endif @if (is_rtl()) @endif @if ($headerEnabled) @endif @php /* The kiosk installs as its own app, scoped to this restaurant's kiosk alone — so it never captures the till or the storefront into its window, and vice versa. Linked only when offline terminals are on, because that is when the manifest exists: advertising one the server 404s makes the browser offer an install that cannot complete. */ @endphp @php $kioskOffline = \App\Services\Capability::has(\App\Services\Capability::OFFLINE); @endphp @if ($kioskOffline) @endif @php // app.js loads in BOTH kiosk shells (with the site header, and chrome-free), so its // base / cart / label bootstrap - and the translated strings it writes into the DOM - // are emitted unconditionally rather than only alongside the header. @endphp @if ($kioskOffline) @php /* Shown only while the line is down. A kiosk stands in a dining room, so the failure has to read as "this machine, not you" at a glance and from across the room — a basket that silently stops responding is the worst version of this, because the guest keeps trying and staff never notice. Hidden by default so it costs nothing on a healthy screen; the browser's own online/offline events drive it, no polling. */ @endphp @endif @if ($headerEnabled) @partial('header', ['sidebarEnabled' => $sidebarEnabled]) @if ($sidebarEnabled)@partial('side-panel')@endif @partial('cuisines-panel') @partial('flash') @partial('cookie-consent') @endif
{!! restaurant_logo_html($restaurant, 'md', 'qm-kiosk-logo') !!} @php // Masthead lockup beside the logo, not a section heading - the page's //

is the hero title below. Kept as plain text so the kiosk has // exactly one h1. @endphp

{{ $restaurant['name'] ?? t('kiosk.menu_fallback_name') }}

{{ $restaurant['tagline'] ?? $cuisineName }}

@if (!empty($kioskName)) {{ $kioskName }} @endif @if (!empty($dineIn) && $tableLabel !== '') {{ t_raw('kiosk.dine_in_table', [':n' => $tableLabel]) }} @else {{ t_raw('kiosk.counter_order') }} @endif @if (\App\Services\PosChrome::shows($posHidden ?? [], 'install')) @if ($kioskOffline)@endif @endif
@if (!empty($bindingBroken))
{!! t_raw('kiosk.staff_notice_body') !!}
@endif @if (!empty($closedNotice))
{{ $closedNotice }}
@endif
{{ t_raw('kiosk.hero_kicker') }}

{{ t_raw('kiosk.hero_title_pre') }} {{ t_raw('kiosk.hero_title_hl') }}

{{ t_raw('kiosk.hero_text', [':name' => $restaurant['name'] ?? '']) }}

@if ($deals)

{{ t_raw('kiosk.offers_heading') }}

@foreach ($deals as $d)
{!! $dealBadge($d) !!}

{!! isset($d['title']) ? e($d['title']) : t('kiosk.special_offer_fallback') !!}

@if (!empty($d['description']))

{{ $d['description'] }}

@endif {{ $d['code'] }}
@endforeach
@endif @if ($featured)

@if (is_file(public_path() . '/assets/admin/svg/005-chef.svg')) @endif{{ t_raw('kiosk.chefs_picks_heading') }}

@foreach ($featured as $it) @endforeach
@endif
@foreach ($categories as $ci => $c) @php $catId = (int) $c['id']; $catCount = count($itemsByCat[$catId] ?? []); @endphp

{!! $catIcon((string) $c['name']) !!} {{ $c['name'] }} @if ($catCount){{ $catCount }}@endif

@foreach (($itemsByCat[$catId] ?? []) as $it) @php $avail = (int) ($it['is_available'] ?? 1) === 1; @endphp @php $veg = (int) ($it['is_veg'] ?? 0) === 1; @endphp

{{ $it['name'] }}

@if ((int) ($it['is_popular'] ?? 0) === 1){{ t_raw('vendor.menu.popular_pill') }}@endif

{{ $it['description'] ?? '' }}

@partial('cards/dish-attrs', ['item' => $it])
{{ money($it['price']) }} @if ($avail) @else {{ t_raw('common.sold_out') }} @endif
@endforeach
@endforeach
@if ($idleSeconds > 0) @endif @partial('order-customize-modal', ['cur' => $curSym]) @if ($headerEnabled) @partial('scripts', ['pageScripts' => ['js/order-customize.js']]) @else @endif @php /* OUTSIDE the header/chrome-free branch, for the same reason app.js is emitted unconditionally above: a kiosk runs in two shells, and the offline cache belongs to both. Inside the else it loaded only on the chrome-free one, so a venue running the kiosk with the site header got the panel markup and nothing to drive it. */ @endphp @if (!empty($canSelfCheckout)) @php /* The till's own reader module, fed kiosk endpoints: pos-reader.js only reads window.QMPOS, so the one file drives both surfaces and a reader fix lands everywhere at once. Merged into the QMPOS the offline module may already carry, never over it. */ @endphp @endif @if ($kioskOffline) @php /* The SAME module the till uses, with the queue switched off — cache only. The kiosk gets the app-shell cache so a dropped line shows this restaurant's own panel instead of the browser's error page, and recovers by itself when the line returns. It does NOT get the order queue, and that is a decision rather than an omission: a till is staff-operated, so a held sale is held by someone who knows it is held, while a guest who "orders" into a queue walks away expecting food from a kitchen that never got a ticket. The basket is server-authoritative here in any case — every action reconciles against the server's snapshot — so there is nothing client-side to build an order from. */ @endphp @endif @if ($kioskOffline && \App\Services\PosChrome::shows($posHidden ?? [], 'install')) @endif @if (($kioskOsk ?? 'auto') !== 'off') @php /* ON-SCREEN KEYBOARD (KioskBoard, MIT, shipped under assets/vendor/kioskboard). A standalone kiosk - a Windows touch screen in kiosk mode above all - does not reliably raise the OS keyboard, so the review step's name and phone fields open their own: letters for the names, a number pad for the phone, in the venue's language. auto (default) leaves tablets to their own keyboard; always forces it everywhere; off (a hardware keyboard) loads nothing. Set per kiosk on Devices. */ @endphp @endif