Files
ladill-pos/app/Services/Pos/KitchenBoardService.php
T
isaacclad ede4aaa10c
Deploy Ladill POS / deploy (push) Successful in 46s
Add public token-gated kitchen display for POS devices.
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.
2026-07-15 22:52:28 +00:00

97 lines
3.4 KiB
PHP

<?php
namespace App\Services\Pos;
use App\Models\PosSale;
use App\Models\PosSaleLine;
use App\Models\PosStation;
use Illuminate\Support\Collection;
class KitchenBoardService
{
/** @return array{tickets: Collection, server_time: string} */
public function payload(string $ownerRef): array
{
return [
'tickets' => $this->buildTickets($ownerRef),
'server_time' => now()->toIso8601String(),
];
}
public function stations(string $ownerRef): Collection
{
return PosStation::owned($ownerRef)->orderBy('name')->get(['id', 'name']);
}
private function buildTickets(string $owner): Collection
{
// Payment-agnostic: dine-in tabs (pending) and paid online orders both
// sit on the board while the kitchen is still working them.
$sales = PosSale::owned($owner)
->where('kitchen_status', PosSale::KITCHEN_ACTIVE)
->with(['table', 'lines' => fn ($q) => $q->orderBy('position')->with('modifiers', 'station')])
->orderBy('kitchen_sent_at')
->get();
return $sales->map(function (PosSale $sale) {
$lines = $sale->lines
->filter(fn (PosSaleLine $l) => in_array($l->kitchen_state, [
PosSaleLine::KITCHEN_QUEUED,
PosSaleLine::KITCHEN_PREPARING,
PosSaleLine::KITCHEN_READY,
], true))
->values();
if ($lines->isEmpty()) {
return null;
}
return [
'id' => $sale->id,
'reference' => $sale->reference,
'order_type' => $sale->order_type,
'table' => $sale->table?->label,
'sent_at' => optional($sale->kitchen_sent_at)->toIso8601String(),
'lines' => $lines->map(fn (PosSaleLine $l) => [
'id' => $l->id,
'name' => $l->name,
'quantity' => $l->quantity,
'notes' => $l->notes,
'course' => $l->course,
'station' => $l->station?->name,
'station_id' => $l->station_id,
'source' => $l->source,
'modifiers' => $l->modifiers->map(fn ($m) => $m->name)->all(),
'state' => $l->kitchen_state,
])->all(),
];
})->filter()->values();
}
/**
* Advance one line to the next kitchen state (queued → preparing → ready → served).
*
* @return array{state: string}
*/
public function bumpLine(PosSaleLine $line, string $ownerRef): array
{
abort_unless($line->sale && $line->sale->owner_ref === $ownerRef, 404);
$flow = PosSaleLine::KITCHEN_FLOW;
$idx = array_search($line->kitchen_state, $flow, true);
$next = $idx === false ? PosSaleLine::KITCHEN_PREPARING : ($flow[$idx + 1] ?? PosSaleLine::KITCHEN_SERVED);
$line->forceFill(['kitchen_state' => $next])->save();
$sale = $line->sale;
$remaining = $sale->lines()
->whereIn('kitchen_state', [PosSaleLine::KITCHEN_QUEUED, PosSaleLine::KITCHEN_PREPARING, PosSaleLine::KITCHEN_READY])
->count();
if ($remaining === 0) {
$sale->forceFill(['kitchen_status' => PosSale::KITCHEN_SERVED])->save();
}
return ['state' => $next];
}
}