Add self-service kiosk for queue ticket issuance.
Deploy Ladill Queue / deploy (push) Successful in 36s
Deploy Ladill Queue / deploy (push) Successful in 36s
Customers can pick a service, optionally enter details, and receive a ticket on a branded touchscreen flow with admin-configurable queues and auto-reset. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -56,6 +56,7 @@ class DeviceController extends Controller
|
||||
return view('qms.devices.create', [
|
||||
'organization' => $organization,
|
||||
'branches' => $branches,
|
||||
'queues' => ServiceQueue::owned($owner)->where('organization_id', $organization->id)->orderBy('name')->get(),
|
||||
'deviceTypes' => config('qms.device_types'),
|
||||
]);
|
||||
}
|
||||
@@ -70,8 +71,37 @@ class DeviceController extends Controller
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'type' => ['required', 'string', 'in:'.implode(',', array_keys(config('qms.device_types')))],
|
||||
'branch_id' => ['nullable', 'exists:queue_branches,id'],
|
||||
'queue_ids' => ['nullable', 'array'],
|
||||
'queue_ids.*' => ['integer', 'exists:queue_service_queues,id'],
|
||||
'collect_name' => ['nullable', 'boolean'],
|
||||
'collect_phone' => ['nullable', 'boolean'],
|
||||
'reset_seconds' => ['nullable', 'integer', 'min:5', 'max:120'],
|
||||
'welcome_message' => ['nullable', 'string', 'max:255'],
|
||||
]);
|
||||
|
||||
$config = null;
|
||||
if ($validated['type'] === 'kiosk') {
|
||||
$queueIds = array_map('intval', $validated['queue_ids'] ?? []);
|
||||
if ($queueIds !== []) {
|
||||
$allowed = ServiceQueue::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->whereIn('id', $queueIds)
|
||||
->pluck('id')
|
||||
->all();
|
||||
$queueIds = array_values(array_intersect($queueIds, $allowed));
|
||||
}
|
||||
|
||||
$config = [
|
||||
'service_queue_ids' => $queueIds,
|
||||
'collect_name' => $request->boolean('collect_name'),
|
||||
'collect_phone' => $request->boolean('collect_phone'),
|
||||
'reset_seconds' => $validated['reset_seconds'] ?? 15,
|
||||
'welcome_message' => filled($validated['welcome_message'] ?? null)
|
||||
? $validated['welcome_message']
|
||||
: null,
|
||||
];
|
||||
}
|
||||
|
||||
$device = Device::create([
|
||||
'owner_ref' => $owner,
|
||||
'organization_id' => $organization->id,
|
||||
@@ -79,6 +109,7 @@ class DeviceController extends Controller
|
||||
'name' => $validated['name'],
|
||||
'type' => $validated['type'],
|
||||
'status' => 'offline',
|
||||
'config' => $config,
|
||||
]);
|
||||
|
||||
$token = $this->devices->generateToken($device);
|
||||
|
||||
@@ -5,8 +5,10 @@ namespace App\Http\Controllers\Qms;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ServiceQueue;
|
||||
use App\Services\Qms\DeviceService;
|
||||
use App\Services\Qms\KioskService;
|
||||
use App\Services\Qms\QueueEngine;
|
||||
use App\Services\Qms\TicketPresenter;
|
||||
use App\Support\OrganizationBranding;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
@@ -16,21 +18,31 @@ class KioskDeviceController extends Controller
|
||||
public function __construct(
|
||||
protected QueueEngine $engine,
|
||||
protected DeviceService $devices,
|
||||
protected KioskService $kiosk,
|
||||
) {}
|
||||
|
||||
public function show(Request $request): View
|
||||
{
|
||||
$device = $request->attributes->get('qms.device');
|
||||
$queues = ServiceQueue::query()
|
||||
->where('organization_id', $device->organization_id)
|
||||
->when($device->branch_id, fn ($q) => $q->where('branch_id', $device->branch_id))
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
$device->loadMissing('organization');
|
||||
$queues = $this->kiosk->queuesFor($device);
|
||||
$settings = $this->kiosk->settings($device);
|
||||
$organization = $device->organization;
|
||||
|
||||
$this->devices->recordHeartbeat($device);
|
||||
|
||||
return view('qms.kiosk.device', compact('device', 'queues'));
|
||||
return view('qms.kiosk.device', [
|
||||
'device' => $device,
|
||||
'queues' => $queues,
|
||||
'settings' => $settings,
|
||||
'logoUrl' => $organization
|
||||
? OrganizationBranding::logoUrl($organization)
|
||||
: OrganizationBranding::assetUrl(),
|
||||
'logoAlt' => $organization
|
||||
? OrganizationBranding::logoAlt($organization)
|
||||
: 'Ladill Queue',
|
||||
'poweredByLogoUrl' => OrganizationBranding::assetUrl(OrganizationBranding::POWERED_BY_LOGO),
|
||||
]);
|
||||
}
|
||||
|
||||
public function issue(Request $request): JsonResponse
|
||||
@@ -45,8 +57,10 @@ class KioskDeviceController extends Controller
|
||||
'priority' => ['nullable', 'string'],
|
||||
]);
|
||||
|
||||
abort_unless($this->kiosk->queueAllowedForIssue($device, (int) $validated['queue_id']), 403);
|
||||
|
||||
$queue = ServiceQueue::findOrFail($validated['queue_id']);
|
||||
abort_unless($queue->organization_id === $device->organization_id, 403);
|
||||
abort_if($queue->is_paused || ! $queue->is_active, 422, 'This queue is not accepting tickets right now.');
|
||||
|
||||
$ticket = $this->engine->issueTicket($queue, $device->owner_ref, [
|
||||
'customer_name' => $validated['customer_name'] ?? null,
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Qms;
|
||||
|
||||
use App\Models\Device;
|
||||
use App\Models\ServiceQueue;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class KioskService
|
||||
{
|
||||
/**
|
||||
* @return array{service_queue_ids: list<int>, collect_name: bool, collect_phone: bool, reset_seconds: int, welcome_message: ?string}
|
||||
*/
|
||||
public function settings(Device $device): array
|
||||
{
|
||||
$config = $device->config ?? [];
|
||||
|
||||
return [
|
||||
'service_queue_ids' => array_values(array_filter(array_map(
|
||||
'intval',
|
||||
$config['service_queue_ids'] ?? [],
|
||||
))),
|
||||
'collect_name' => (bool) ($config['collect_name'] ?? false),
|
||||
'collect_phone' => (bool) ($config['collect_phone'] ?? false),
|
||||
'reset_seconds' => max(5, (int) ($config['reset_seconds'] ?? 15)),
|
||||
'welcome_message' => filled($config['welcome_message'] ?? null)
|
||||
? (string) $config['welcome_message']
|
||||
: null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue IDs configured on the device (empty = all branch queues).
|
||||
*
|
||||
* @return list<int>
|
||||
*/
|
||||
public function assignedQueueIds(Device $device): array
|
||||
{
|
||||
return $this->settings($device)['service_queue_ids'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, ServiceQueue>
|
||||
*/
|
||||
public function queuesFor(Device $device): Collection
|
||||
{
|
||||
$ids = $this->assignedQueueIds($device);
|
||||
|
||||
$query = ServiceQueue::query()
|
||||
->where('organization_id', $device->organization_id)
|
||||
->where('is_active', true)
|
||||
->where('is_paused', false)
|
||||
->when($device->branch_id, fn ($q) => $q->where('branch_id', $device->branch_id))
|
||||
->when($ids !== [], fn ($q) => $q->whereIn('id', $ids))
|
||||
->orderBy('name');
|
||||
|
||||
return $query->get();
|
||||
}
|
||||
|
||||
public function queueAllowedForIssue(Device $device, int $queueId): bool
|
||||
{
|
||||
$ids = $this->assignedQueueIds($device);
|
||||
|
||||
$query = ServiceQueue::query()
|
||||
->where('organization_id', $device->organization_id)
|
||||
->where('id', $queueId)
|
||||
->when($device->branch_id, fn ($q) => $q->where('branch_id', $device->branch_id))
|
||||
->when($ids !== [], fn ($q) => $q->whereIn('id', $ids));
|
||||
|
||||
return $query->exists();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user