Deploy Ladill POS / deploy (push) Successful in 23s
The "links" slice — a guest scans a table's QR, browses the menu (with
modifiers), and submits an order that lands on that table's open tab and fires
straight to the Kitchen Display.
- Public, auth-free flow scoped by an unguessable table short_code:
GET /t/{code} (menu + client cart), POST /t/{code}/order (throttled),
GET /t/{code}/done. Orders open/append the table's dine-in tab, add lines as
source=guest, and send to the kitchen.
- Staff print a per-table QR (Settings → table → QR; renders client-side to the
public menu URL). short_code is generated lazily.
- Guest lines are badged on the ticket and the KDS so staff can tell them apart;
staff still settle the tab as usual (cash / Ladill Pay).
- Extracted PosSaleService::buildProductLine as the single product+modifier
price resolver, now shared by staff and guest ordering (client prices never
trusted).
Schema additive: pos_tables.short_code, pos_sale_lines.source. New
PosRestaurantTest covers the guest order firing to the kitchen; suite green (12).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
3.6 KiB
PHP
102 lines
3.6 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Pos;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Controllers\Pos\Concerns\ScopesToAccount;
|
|
use App\Models\PosSale;
|
|
use App\Models\PosSaleLine;
|
|
use App\Models\PosStation;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\View\View;
|
|
|
|
class KitchenController extends Controller
|
|
{
|
|
use ScopesToAccount;
|
|
|
|
public function index(Request $request): View
|
|
{
|
|
return view('pos.kitchen.index', [
|
|
'stations' => PosStation::owned($this->ownerRef($request))->orderBy('name')->get(['id', 'name']),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Active kitchen tickets — open sales with fired-but-not-served lines.
|
|
*/
|
|
public function feed(Request $request): JsonResponse
|
|
{
|
|
$owner = $this->ownerRef($request);
|
|
|
|
$sales = PosSale::owned($owner)
|
|
->openTickets()
|
|
->where('kitchen_status', PosSale::KITCHEN_ACTIVE)
|
|
->with(['table', 'lines' => fn ($q) => $q->orderBy('position')->with('modifiers', 'station')])
|
|
->orderBy('kitchen_sent_at')
|
|
->get();
|
|
|
|
$tickets = $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();
|
|
|
|
return response()->json(['tickets' => $tickets, 'server_time' => now()->toIso8601String()]);
|
|
}
|
|
|
|
/**
|
|
* Advance one line to the next kitchen state (queued → preparing → ready → served).
|
|
*/
|
|
public function bump(Request $request, PosSaleLine $line): JsonResponse
|
|
{
|
|
$owner = $this->ownerRef($request);
|
|
abort_unless($line->sale && $line->sale->owner_ref === $owner, 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();
|
|
|
|
// When every fired line is served, the ticket leaves the kitchen board.
|
|
$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 response()->json(['state' => $next]);
|
|
}
|
|
}
|