Add public token-gated kitchen display for POS devices.
Deploy Ladill POS / deploy (push) Successful in 46s

Kitchen display devices open a full-screen KDS without staff login,
with feed/stream/bump over the device token. Shared kitchen board
logic is extracted so the signed-in Kitchen page stays in sync.
This commit is contained in:
isaacclad
2026-07-15 22:52:28 +00:00
parent c01518b0ee
commit ede4aaa10c
8 changed files with 425 additions and 78 deletions
@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>Kitchen · {{ $locationName }}</title>
@include('partials.favicon')
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600,700&display=swap" rel="stylesheet" />
@vite(['resources/css/app.css', 'resources/js/app.js'])
<style>
[x-cloak] { display: none !important; }
</style>
</head>
<body class="min-h-full bg-slate-100 font-sans text-slate-900 antialiased">
<div class="fixed inset-x-0 top-0 z-20 h-1.5 bg-gradient-to-r from-amber-500 to-orange-600"></div>
<div x-data="posKitchen(@js([
'feedUrl' => $feedUrl,
'streamUrl' => $streamUrl,
'bumpUrl' => $bumpUrl,
'csrf' => csrf_token(),
]))"
class="mx-auto min-h-dvh max-w-[1600px] space-y-4 px-4 py-5 sm:px-6 lg:px-8">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<p class="text-xs font-semibold uppercase tracking-wider text-amber-700">Kitchen display</p>
<h1 class="mt-0.5 text-xl font-semibold tracking-tight text-slate-900 sm:text-2xl">{{ $locationName }}</h1>
<p class="mt-1 text-sm text-slate-500">Live tickets tap an item to advance it. No sign-in required on this screen.</p>
</div>
<div class="flex items-center gap-2">
<span class="rounded-full bg-white px-3 py-1 text-xs font-medium text-slate-500 ring-1 ring-slate-200"
x-text="loaded ? 'Live' : 'Connecting…'"></span>
<button type="button" @click="load()"
class="rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm font-medium text-slate-600 hover:bg-slate-50">
Refresh
</button>
</div>
</div>
@if($stations->isNotEmpty())
<div class="flex flex-wrap gap-1.5">
<button type="button" @click="activeStation = ''" :class="activeStation === '' ? 'bg-amber-600 text-white' : 'bg-white text-slate-600 ring-1 ring-slate-200'"
class="rounded-full px-3 py-1 text-xs font-medium">All stations</button>
@foreach($stations as $station)
<button type="button" @click="activeStation = '{{ $station->id }}'"
:class="activeStation === '{{ $station->id }}' ? 'bg-amber-600 text-white' : 'bg-white text-slate-600 ring-1 ring-slate-200'"
class="rounded-full px-3 py-1 text-xs font-medium">{{ $station->name }}</button>
@endforeach
</div>
@endif
<template x-if="loaded && visibleTickets.length === 0">
<div class="rounded-2xl border border-dashed border-slate-200 bg-white px-6 py-16 text-center">
<p class="text-sm text-slate-500">No active tickets. Fired orders will appear here.</p>
</div>
</template>
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<template x-for="t in visibleTickets" :key="t.id">
<div class="flex flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm">
<div class="flex items-center justify-between border-b border-slate-100 bg-slate-50 px-4 py-2.5">
<div>
<p class="text-sm font-semibold text-slate-900" x-text="t.table ? t.table : t.order_type.replace('_',' ')"></p>
<p class="text-[11px] text-slate-400" x-text="t.reference"></p>
</div>
<span class="rounded-full bg-slate-900/5 px-2 py-0.5 text-xs font-medium text-slate-600" x-text="elapsed(t.sent_at)"></span>
</div>
<div class="flex-1 divide-y divide-slate-100">
<template x-for="line in t.lines" :key="line.id">
<div class="flex items-center justify-between gap-2 px-4 py-2.5"
:class="line.state === 'ready' ? 'bg-emerald-50' : (line.state === 'preparing' ? 'bg-amber-50/60' : '')">
<div class="min-w-0">
<p class="text-sm font-medium text-slate-900">
<span x-text="line.quantity + '×'"></span>
<span x-text="line.name"></span>
</p>
<p x-show="line.modifiers.length" x-cloak class="text-xs text-slate-500" x-text="line.modifiers.join(', ')"></p>
<p x-show="line.notes" x-cloak class="text-xs text-rose-600" x-text="line.notes"></p>
<div class="mt-0.5 flex items-center gap-1.5">
<span x-show="line.course" x-cloak class="rounded bg-slate-100 px-1.5 text-[10px] font-medium text-slate-500" x-text="line.course"></span>
<span x-show="line.station" x-cloak class="rounded bg-indigo-50 px-1.5 text-[10px] font-medium text-indigo-600" x-text="line.station"></span>
<span x-show="line.source === 'guest'" x-cloak class="rounded bg-violet-50 px-1.5 text-[10px] font-medium text-violet-700">guest</span>
<span x-show="line.source === 'online'" x-cloak class="rounded bg-blue-50 px-1.5 text-[10px] font-medium text-blue-700">online</span>
<span class="text-[11px] uppercase tracking-wide text-slate-400" x-text="line.state"></span>
</div>
</div>
<button type="button" @click="bump(line)"
class="shrink-0 rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 text-xs font-semibold text-slate-700 hover:bg-slate-50"
x-text="nextLabel(line.state)"></button>
</div>
</template>
</div>
</div>
</template>
</div>
</div>
</body>
</html>