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.
149 lines
4.4 KiB
PHP
149 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Pos;
|
|
|
|
use App\Models\PosCustomerDisplay;
|
|
use App\Models\PosDevice;
|
|
use App\Models\PosLocation;
|
|
use Illuminate\Support\Str;
|
|
|
|
class DeviceService
|
|
{
|
|
public function __construct(private CustomerDisplayService $displays) {}
|
|
|
|
public function generateToken(): string
|
|
{
|
|
return Str::random(48);
|
|
}
|
|
|
|
public function findByToken(string $token): ?PosDevice
|
|
{
|
|
return PosDevice::query()->where('device_token', $token)->first();
|
|
}
|
|
|
|
public function recordHeartbeat(PosDevice $device): PosDevice
|
|
{
|
|
$device->update([
|
|
'status' => PosDevice::STATUS_ONLINE,
|
|
'last_online_at' => now(),
|
|
]);
|
|
|
|
return $device->fresh();
|
|
}
|
|
|
|
/** Mark token-bearing devices online when their public endpoints are hit. */
|
|
public function touchByToken(?string $token): void
|
|
{
|
|
if (! $token) {
|
|
return;
|
|
}
|
|
|
|
$device = $this->findByToken($token);
|
|
if ($device) {
|
|
$this->recordHeartbeat($device);
|
|
}
|
|
}
|
|
|
|
public function markStaleDevicesOffline(int $minutes = 10): int
|
|
{
|
|
return PosDevice::query()
|
|
->where('status', PosDevice::STATUS_ONLINE)
|
|
->where(function ($q) use ($minutes) {
|
|
$q->whereNull('last_online_at')
|
|
->orWhere('last_online_at', '<', now()->subMinutes($minutes));
|
|
})
|
|
->update(['status' => PosDevice::STATUS_OFFLINE]);
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
public function defaultConfigForType(string $type): array
|
|
{
|
|
return match ($type) {
|
|
'customer_display' => ['role' => 'customer_facing'],
|
|
'kitchen_display' => ['role' => 'kitchen'],
|
|
'receipt_printer' => ['paper_mm' => 80, 'driver' => 'browser'],
|
|
'register' => ['role' => 'till'],
|
|
default => [],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a device; customer_display types share the branch display token.
|
|
*
|
|
* @param array{name: string, type: string, location_id?: ?int, owner_ref: string} $data
|
|
*/
|
|
public function register(array $data): PosDevice
|
|
{
|
|
$type = $data['type'];
|
|
$locationId = isset($data['location_id']) ? (int) $data['location_id'] : null;
|
|
$token = null;
|
|
|
|
if (in_array($type, config('pos.device_token_types', []), true)) {
|
|
$token = $this->tokenForNewDevice($type, $locationId, $data['owner_ref']);
|
|
}
|
|
|
|
return PosDevice::create([
|
|
'owner_ref' => $data['owner_ref'],
|
|
'location_id' => $locationId ?: null,
|
|
'name' => $data['name'],
|
|
'type' => $type,
|
|
'status' => PosDevice::STATUS_OFFLINE,
|
|
'device_token' => $token,
|
|
'config' => $this->defaultConfigForType($type),
|
|
]);
|
|
}
|
|
|
|
public function regenerateToken(PosDevice $device): PosDevice
|
|
{
|
|
if (! $device->usesToken()) {
|
|
return $device;
|
|
}
|
|
|
|
$token = $this->generateToken();
|
|
|
|
if ($device->type === 'customer_display' && $device->location_id) {
|
|
$location = PosLocation::query()->find($device->location_id);
|
|
if ($location) {
|
|
$display = $this->displays->ensureForLocation($location);
|
|
$display->update(['token' => $token]);
|
|
}
|
|
}
|
|
|
|
$device->update(['device_token' => $token]);
|
|
|
|
return $device->fresh();
|
|
}
|
|
|
|
public function openUrl(PosDevice $device): ?string
|
|
{
|
|
if (! $device->device_token) {
|
|
return null;
|
|
}
|
|
|
|
return match ($device->type) {
|
|
'customer_display' => route('pos.customer-display.show', ['token' => $device->device_token]),
|
|
'kitchen_display' => route('pos.kitchen-display.show', ['token' => $device->device_token]),
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
private function tokenForNewDevice(string $type, ?int $locationId, string $ownerRef): string
|
|
{
|
|
if ($type === 'customer_display' && $locationId) {
|
|
$location = PosLocation::query()
|
|
->where('owner_ref', $ownerRef)
|
|
->whereKey($locationId)
|
|
->first();
|
|
|
|
if ($location) {
|
|
$display = $this->displays->ensureForLocation($location);
|
|
|
|
// Keep a single active display token per branch; reuse it for the device.
|
|
return $display->token;
|
|
}
|
|
}
|
|
|
|
return $this->generateToken();
|
|
}
|
|
}
|