Files
ladill-pos/app/Http/Controllers/Public/KitchenDisplayController.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

117 lines
3.7 KiB
PHP

<?php
namespace App\Http\Controllers\Public;
use App\Http\Controllers\Controller;
use App\Models\PosDevice;
use App\Models\PosSaleLine;
use App\Services\Pos\DeviceService;
use App\Services\Pos\KitchenBoardService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
use Symfony\Component\HttpFoundation\StreamedResponse;
/**
* Token-gated kitchen display for unattended tablets (no staff login).
*/
class KitchenDisplayController extends Controller
{
public function __construct(
private DeviceService $devices,
private KitchenBoardService $board,
) {}
public function show(string $token): View
{
$device = $this->findKitchenDevice($token);
$this->devices->recordHeartbeat($device);
return view('pos.kitchen.public', [
'device' => $device,
'locationName' => $device->location?->name ?? 'Kitchen',
'stations' => $this->board->stations($device->owner_ref),
'feedUrl' => route('pos.kitchen-display.feed', ['token' => $token]),
'streamUrl' => route('pos.kitchen-display.stream', ['token' => $token]),
'bumpUrl' => route('pos.kitchen-display.bump', ['token' => $token, 'line' => '__ID__']),
]);
}
public function feed(string $token): JsonResponse
{
$device = $this->findKitchenDevice($token);
$this->devices->recordHeartbeat($device);
return response()->json($this->board->payload($device->owner_ref));
}
public function stream(Request $request, string $token): StreamedResponse
{
$device = $this->findKitchenDevice($token);
$this->devices->recordHeartbeat($device);
$owner = $device->owner_ref;
$deviceId = $device->id;
$request->session()->save();
$response = new StreamedResponse(function () use ($owner, $deviceId) {
while (ob_get_level() > 0) {
@ob_end_flush();
}
@set_time_limit(0);
$start = time();
$lastHash = null;
while (true) {
if ($deviceId) {
$live = PosDevice::query()->find($deviceId);
if ($live) {
app(DeviceService::class)->recordHeartbeat($live);
}
}
$payload = $this->board->payload($owner);
$json = json_encode($payload);
$hash = md5((string) $json);
if ($hash !== $lastHash) {
echo 'data: '.$json."\n\n";
$lastHash = $hash;
} else {
echo ": ping\n\n";
}
@ob_flush();
@flush();
if (connection_aborted() || (time() - $start) >= 25) {
break;
}
sleep(1);
}
});
$response->headers->set('Content-Type', 'text/event-stream');
$response->headers->set('Cache-Control', 'no-cache');
$response->headers->set('Connection', 'keep-alive');
$response->headers->set('X-Accel-Buffering', 'no');
return $response;
}
public function bump(string $token, PosSaleLine $line): JsonResponse
{
$device = $this->findKitchenDevice($token);
$this->devices->recordHeartbeat($device);
return response()->json($this->board->bumpLine($line, $device->owner_ref));
}
private function findKitchenDevice(string $token): PosDevice
{
$device = $this->devices->findByToken($token);
abort_unless($device && $device->type === 'kitchen_display', 404);
return $device->loadMissing('location');
}
}