@extends('layouts.dashboard')
@section('content')
@php
/**
* Kitchen Pipeline (KDS) board.
*
* @var string $serverNow DB clock anchor (Y-m-d H:i:s)
* @var int $prepMinutes Target prep time for late highlighting
* @var array $columns ['new'=>[...], 'preparing'=>[...], 'ready'=>[...]]
* @var array $stations the kitchen's active stations (empty ⇒ one undivided board)
* @var string $activeStation '' = whole kitchen, a station id, or 'unassigned'
*/
$columns = $columns ?? ['new' => [], 'preparing' => [], 'ready' => []];
$prepMinutes = (int) ($prepMinutes ?? 30);
$serverNow = (string) ($serverNow ?? '');
$stations = $stations ?? [];
$activeStation = (string) ($activeStation ?? '');
/** Render one order card; markup must mirror kitchen.js so a refresh is seamless. */
$renderCard = static function (array $c) use ($activeStation, $__env): void {
$late = !empty($c['late']);
$items = $c['items'] ?? [];
$next = $c['next'] ?? null;
@endphp
@php $sched = !empty($c['scheduled_at']) ? fmt_clock($c['scheduled_at']) : '';
@endphp
@php /* When the ticket is DUE — the pre-order's slot when it has one, otherwise when it was
placed. The same instant buildBoard() measures `minutes` from, carried here in
machine-readable form because kitchen.js re-derives every card's timer from the DOM
every 15 seconds and had only data-placed to work from: fifteen seconds after load a
pre-order's card was rewritten to the hours since it was ORDERED and flagged late — a
false late alarm on a ticket that is not due yet, clearing only on a reload. */
@endphp
@php $due = !empty($c['scheduled_at']) ? (string) $c['scheduled_at'] : (string) $c['placed_at'];
@endphp
#{{ (string) $c['order_number'] }}
@php /* A dine-in ticket names its TABLE, so the pass knows where to take it — the order
carried table_id all along and no staff-facing screen ever showed it. kitchen.js
builds the same badge on a live refresh, so the card is identical either way. */
@endphp
{{ t_raw('common.' . (string) $c['service_mode']) }}{!! !empty($c['table_label']) ? ' · ' . e((string) $c['table_label']) : '' !!}
@if (!empty($c['paid'])) {{ t_raw('status.payment.paid') }}@else {{ t_raw('status.payment.unpaid') }}@endif
@foreach ($items as $it)
@php $itReady = !empty($it['ready']); @endphp
@php /* One line, one cook. The tick is the control; the whole ticket's state
follows from these, so it is the primary gesture on the card rather than a
decoration beside the name. */
@endphp
{{ (string) $it['name'] }}
@if (!empty($it['station'])){{ (string) $it['station'] }}@endif
×{{ (int) $it['qty'] }}
@php /* Recall — a plain form, so a ticket can always be pulled back even on a
station whose scripts never loaded. The board's poller rebuilds cards from
JSON and knows nothing about this control, so the page script below puts it
straight back after every repaint (see the observer). */
@endphp
@php /* Only where the whole ticket is on screen — a station cannot pull back
a ticket it can only see part of. */
@endphp
@if ($activeStation === '' && in_array((string) $c['status'], ['preparing', 'out_for_delivery'], true))
@endif
@endif
@php
};
// The same two readings the page script maintains in the browser, painted here from the
// snapshot the server already holds. Without this the board would open showing an empty
// age chip and four zeroed health chips until the first script pass — and would keep
// showing them on a station running without JavaScript.
$colAge = [];
foreach ($columns as $colKey => $colCards) {
$oldest = -1;
foreach ($colCards as $card) {
$mins = (int) ($card['minutes'] ?? 0);
if ($mins > $oldest) {
$oldest = $mins;
}
}
$colAge[$colKey] = $oldest;
}
/** The chip tone an age wears: within target, past it, or half again past it. */
$ageTone = static function (int $mins) use ($prepMinutes, $__env): string {
return $mins > $prepMinutes * 1.5 ? 'red' : ($mins > $prepMinutes ? 'amber' : 'green');
};
$cols = [
['key' => 'new', 'label' => t_raw('vendor.kitchen.col_new'), 'extra' => ' is-new'],
['key' => 'preparing', 'label' => t_raw('vendor.kitchen.col_preparing'), 'extra' => ''],
['key' => 'ready', 'label' => t_raw('vendor.kitchen.col_ready'), 'extra' => ''],
];
@endphp
{{ t_raw('vendor.kitchen.board_heading') }}
{{ t_raw('vendor.kitchen.board_subtitle') }}
@php /* Which part of the kitchen this screen is. Plain links, the same period-bar
control the reports use to scope a view — a station screen is left on one choice
for a whole service, so it must survive a reload and be bookmarkable rather than
live in a script's memory. Absent entirely until a kitchen has stations, which is
what keeps every install that never adopts routing exactly as it was. */
@endphp
@if (count($stations))
@php /* Two readings the column of cards can never give on its own: how much of each dish
is outstanding across every ticket, and how the board is running against the target
prep time — not just "late or not", but how late. */
@endphp
@php
$kdsI18n = [
'paid' => t_raw('status.payment.paid'),
'unpaid' => t_raw('status.payment.unpaid'),
'modes' => [
'delivery' => t_raw('common.delivery'),
'pickup' => t_raw('common.pickup'),
'dine_in' => t_raw('common.dine_in'),
],
'cols' => [
'new' => t_raw('vendor.kitchen.col_new'),
'preparing' => t_raw('vendor.kitchen.col_preparing'),
'ready' => t_raw('vendor.kitchen.col_ready'),
],
'noOrders' => t_raw('vendor.kitchen.no_orders_empty'),
'loyaltyPrefix' => t_raw('vendor.kitchen.loyalty_reward_prefix'),
'ptsAbbrev' => t_raw('vendor.kitchen.pts_abbrev'),
'minsSuffix' => t_raw('vendor.kitchen.minutes_suffix'),
'advanceFallback' => t_raw('vendor.kitchen.advance_fallback'),
'staleRetrying' => t_raw('vendor.kitchen.stale_badge'),
'sessionExpired' => t_raw('vendor.kitchen.err_session_expired'),
'boardUpdateFailed' => t_raw('vendor.kitchen.err_board_update_failed'),
'announceUpdated' => t_raw('vendor.kitchen.announce_board_updated'),
'announceNotLive' => t_raw('vendor.kitchen.announce_not_live'),
'toastRefreshed' => t_raw('vendor.kitchen.toast_refreshed'),
'advanceFailed' => t_raw('vendor.kitchen.err_advance_failed'),
'networkAdvance' => t_raw('vendor.kitchen.err_network_advance'),
'tickReady' => t_raw('vendor.kitchen.tick_ready_aria'),
'tickReopen' => t_raw('vendor.kitchen.tick_reopen_aria'),
'itemsProgress' => t_raw('vendor.kitchen.items_progress'),
'prepFailed' => t_raw('vendor.kitchen.err_prep_failed'),
'networkPrep' => t_raw('vendor.kitchen.err_network_prep'),
// The clock format the scheduled chip above is drawn with. That chip goes through
// fmt_clock(), i.e. the pattern the operator chose in Settings → Time; kitchen.js
// rebuilds the same chip from JSON on every poll and had no way to learn it, so the
// slot silently changed shape after the first poll. Not copy, but the same kind of
// thing: something the script cannot resolve for itself.
'timePattern' => qm_pattern('time'),
];
@endphp
@php /* Column age — how long the OLDEST ticket in this column has been
waiting. A column of ten fresh tickets and a column of ten stalled ones look
identical when all the header carries is a count. The board's poller only
rewrites .qm-kds-count, so this chip survives every repaint and is kept
current by the page script below. */
@endphp
@php $age = $colAge[$col['key']] ?? -1;
@endphp
{!! $age < 0 ? t('vendor.kitchen.col_age_none') : t('vendor.kitchen.col_age', [':n' => (string) $age]) !!}{{ count($cards) }}