@extends('layouts.dashboard') @section('content') @php /** * POS (waiter / cashier) inside the vendor dashboard. * @var array $categories @var array $items @var iterable $zones @var iterable $tables * @var array $deliveryZones @var array $optJson @var array $ingJson * @var string $taxMode @var float $taxRate @var string $taxLabel @var float $serviceChargePercent @var bool $tipsEnabled * * The ticket is built client-side (assets/js/pos.js) and submitted in one * request; the server re-validates and re-prices every line before the order * is committed. Sent orders appear on the kitchen display immediately. */ $curSym = preg_replace('/[0-9.,\s]/', '', money(0)) ?: '$'; /* Till chrome the operator switched off on POS settings. Absent column = nothing hidden. */ $posHidden = $posHidden ?? []; $posShow = static fn (string $k): bool => \App\Services\PosChrome::shows($posHidden, $k); $zoneName = []; foreach ($zones as $z) { $zoneName[(int) $z['id']] = (string) $z['name']; } $tablesByZone = []; foreach ($tables as $t) { $tablesByZone[(int) $t['zone_id']][] = $t; } /** Render the data-* attributes that open the shared customize modal. */ $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] ?? '[]') . '"'; }; @endphp @php // The till opens on the two things a cashier works with continuously — the CART and the // additional-options deck — so neither costs a tap to reach. Floor zones and the pinned // quick keys are occasional, so they start collapsed and their pill in the bar opens them. // The toggles in pos-modals.js drive these same root classes. @endphp @php /* The three chrome switches ride on this root as classes; the bars themselves are drawn by the dashboard layout, outside this element, so the CSS reaches them with :has() - the same mechanism the header-offset rules already use. Photos are hidden by class too, rather than by dropping the , so the tile keeps its width either way. */ @endphp
@php $drawerShift = $drawerShift ?? null; @endphp {!! $drawerShift ? t('vendor.pos.drawer_open_label') : t('vendor.pos.drawer_closed_label') !!} @if ($tablesByZone && $posShow('tables')) @php // Toggle the dine-in floor-zones section (indoor / patio / upper level…). @endphp @endif @if ($posShow('funcs')) @php // Toggles the function deck below (guests, discounts, print…) — same pill as the drawer // chip. Open by default on desktop, collapsed on tablet/phone (CSS default + JS). @endphp @endif @if (!empty($quickKeys)) @php /* Show/hide the pinned strip. Guarded like the floor-zones toggle above: a till with nothing pinned has nothing to collapse, so the button is not drawn at all. Reuses the strip's own label key rather than minting a second wording for one thing. */ @endphp @endif @if ($posShow('shortcuts')) @endif @php // Collapse the New-Order panel away to the right (the menu takes the full width) and back. // Sits last so the cart control docks beside the panel it collapses. @endphp @php /* Both belong to offline mode: a connection pill on a till that cannot hold a sale only tells staff something they can do nothing with, and there is nothing to install when the manifest 404s. */ @endphp @php $offlineCap = \App\Services\Capability::has(\App\Services\Capability::OFFLINE); @endphp @if ($offlineCap) @endif @if ($offlineCap && $posShow('install')) @endif
@if ($tablesByZone && $posShow('tables'))
@foreach ($tablesByZone as $zid => $zTables)
{{ $zoneName[$zid] ?? t_raw('vendor.pos.zone_fallback') }} @foreach ($zTables as $t) @php $st = (string) $t['status']; @endphp @endforeach
@endforeach
@endif @php /* Quick keys — the dishes this restaurant rings all day, pinned by the operator in Terminals → Quick keys and resolved here against the live menu, so a key for a dish that has been deleted or taken off simply is not drawn. Rendered into the page, not fetched: the deck has to keep working through an outage, which it cannot do if pressing a key needs the network. Each key carries the same data-cz attributes as the grid tile below, so the tap runs through the one customize path. */ @endphp @if (!empty($quickKeys))
{{ t_raw('vendor.pos.quick_keys_label') }} @foreach ($quickKeys as $qk) @php /* Same wrapper as the grid tile below, image included. A quick key is a tile for a dish the cashier rings by sight; without the photo it was the one tile on the till identified by name alone — and these are the dishes being reached for fastest, so they are the last ones that should be harder to spot. */ @endphp @endforeach
@endif
@foreach ($categories as $c) @endforeach
@foreach ($items as $it) @php $avail = (int) ($it['is_available'] ?? 1) === 1; @endphp @php // data-barcode is read by the scanner bridge in pos-hardware.js: the whole menu is // already in this page, so a scan resolves from the DOM with no round trip and // cannot reach another restaurant's items. @endphp @endforeach
@partial('order-customize-modal', ['cur' => $curSym]) @partial('pos/modals', ['cur' => $curSym, 'tablesByZone' => $tablesByZone, 'zoneName' => $zoneName, 'restaurant' => $restaurant ?? [], 'discountReasons' => $discountReasons ?? [], 'voidReasons' => $voidReasons ?? [], 'payQrEnabled' => $payQrEnabled ?? false]) @php /* * Preview-only starter ticket. * * On the public demo the till is the first screen most visitors open, and an empty * New-Order panel shows none of the line anatomy, the tax line or the totals. Four real * lines drawn from this restaurant's own menu render the panel exactly as a working * service would, with genuine names, prices and tax. * * Gated on demo_mode(): a restaurant that bought the product always opens a clean ticket, * because starting a shift with phantom items on the bill would be a serious defect. * It also seeds ONLY when the ticket is empty, so it never overwrites real work. */ $demoTicket = []; if (function_exists('demo_mode') && demo_mode()) { foreach ($items as $it) { if (count($demoTicket) >= 4) { break; } if ((float) ($it['price'] ?? 0) <= 0 || (int) ($it['is_available'] ?? 1) !== 1) { continue; } $demoTicket[] = ['id' => (int) $it['id'], 'name' => (string) $it['name'], 'price' => (float) $it['price']]; } } @endphp @if ($demoTicket) @endif @php // POS device bridge (card reader / receipt printer / cash drawer). QMPOS is read by // pos-reader.js + pos-hardware.js, which the controller enqueues only when the bound device // has the capability. Card data never touches this server — the Terminal SDK talks to Stripe. @endphp @if (!empty($readerOn)) @endif @php // Keyboard shortcuts: F2 search · F3 discount · F4 hold · F9 pay · F1 this reference. @endphp @endsection