Files
ladill-queue/app/Http/Controllers/Api/ConsoleApiController.php
T
isaaccladandCursor 0854b431bc
Deploy Ladill Queue / deploy (push) Successful in 2m17s
Add service-point routing so call-next cannot steal assigned tickets.
Counters gain destination/staff metadata; tickets can be pre-assigned to a
service point with assigned_only queues for healthcare while shared_pool
preserves generic bank/government behavior. Display and announcements expose
ticket, staff, and destination clearly for Care and public boards.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 17:21:56 +00:00

164 lines
6.1 KiB
PHP

<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Api\Concerns\ScopesApiToAccount;
use App\Http\Controllers\Controller;
use App\Models\Counter;
use App\Models\Organization;
use App\Models\ServiceQueue;
use App\Models\Ticket;
use App\Services\Qms\PlanService;
use App\Services\Qms\QueueEngine;
use App\Services\Qms\TicketPresenter;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ConsoleApiController extends Controller
{
use ScopesApiToAccount;
public function __construct(
protected QueueEngine $engine,
protected PlanService $plans,
) {}
public function show(Request $request, Counter $counter): JsonResponse
{
$this->authorizeAbility($request, 'console.use');
$this->authorizeOwner($request, $counter);
$owner = $this->ownerRef($request);
$organization = $this->organization($request);
$canTransfer = $this->plans->hasFeature($organization, 'transfer');
$counter->load(['serviceQueues', 'branch']);
$currentTicket = Ticket::owned($owner)
->where('counter_id', $counter->id)
->whereIn('status', ['called', 'serving', 'on_hold'])
->with('serviceQueue')
->latest('called_at')
->first();
$waitingByQueue = collect($counter->serviceQueues)->mapWithKeys(function (ServiceQueue $queue) use ($counter) {
return [$queue->uuid => array_map(
fn ($t) => TicketPresenter::toArray($t),
$this->engine->waitingTickets($queue, 10, $counter),
)];
});
$allQueues = $canTransfer
? ServiceQueue::owned($owner)
->where('organization_id', $counter->organization_id)
->where('branch_id', $counter->branch_id)
->where('is_active', true)
->orderBy('name')
->get(['uuid', 'name', 'prefix'])
: collect();
return response()->json([
'data' => [
'counter' => [
'uuid' => $counter->uuid,
'name' => $counter->name,
'destination' => $counter->displayDestination(),
'staff_display_name' => $counter->staff_display_name,
'status' => $counter->status,
'branch' => $counter->branch?->name,
],
'current_ticket' => $currentTicket ? TicketPresenter::toArray($currentTicket) : null,
'queues' => $counter->serviceQueues->map(fn (ServiceQueue $q) => [
'uuid' => $q->uuid,
'name' => $q->name,
'prefix' => $q->prefix,
'routing_mode' => $q->routingMode(),
]),
'waiting_by_queue' => $waitingByQueue,
'transfer_queues' => $allQueues,
'can_transfer' => $canTransfer,
],
]);
}
public function action(Request $request, Counter $counter): JsonResponse
{
$this->authorizeAbility($request, 'console.use');
$this->authorizeOwner($request, $counter);
$actor = $this->ownerRef($request);
$validated = $request->validate([
'action' => ['required', 'string'],
'queue_uuid' => ['nullable', 'uuid'],
'ticket_uuid' => ['nullable', 'uuid'],
'to_queue_uuid' => ['nullable', 'uuid'],
'reason' => ['nullable', 'string', 'max:500'],
]);
$result = match ($validated['action']) {
'call_next' => $this->handleCallNext($counter, $validated['queue_uuid'] ?? '', $actor),
'available' => tap($counter)->update(['status' => 'available']),
'offline' => tap($counter)->update(['status' => 'offline']),
default => $this->handleTicketAction($validated, $counter, $actor),
};
if ($result instanceof Ticket) {
return response()->json(['data' => TicketPresenter::toArray($result->load(['serviceQueue', 'counter']))]);
}
return $this->show($request, $counter->fresh());
}
protected function handleCallNext(Counter $counter, string $queueUuid, string $actor): ?Ticket
{
abort_if($queueUuid === '', 422, 'queue_uuid is required.');
$queue = ServiceQueue::where('uuid', $queueUuid)->firstOrFail();
return $this->engine->callNext($queue, $counter, $actor);
}
protected function handleTicketAction(array $validated, Counter $counter, string $actor): Ticket|Counter|null
{
$ticketUuid = $validated['ticket_uuid'] ?? '';
abort_if($ticketUuid === '', 422, 'ticket_uuid is required for this action.');
$ticket = Ticket::where('uuid', $ticketUuid)->firstOrFail();
return match ($validated['action']) {
'recall' => $this->engine->recall($ticket, $actor),
'start' => $this->engine->startServing($ticket, $actor),
'hold' => $this->engine->hold($ticket, $actor),
'skip' => $this->engine->skip($ticket, $actor),
'complete' => $this->engine->completeTicket($ticket, $actor),
'no_show' => $this->engine->markNoShow($ticket, $actor),
'transfer' => $this->handleTransfer(
$ticket,
$counter,
$validated['to_queue_uuid'] ?? '',
$validated['reason'] ?? null,
$actor,
),
default => abort(422, 'Unknown action.'),
};
}
protected function handleTransfer(
Ticket $ticket,
Counter $counter,
string $toQueueUuid,
?string $reason,
string $actor,
): Ticket {
$organization = Organization::query()->findOrFail($counter->organization_id);
abort_unless(
$this->plans->hasFeature($organization, 'transfer'),
403,
'Handing off tickets between queues requires Queue Pro.',
);
abort_if($toQueueUuid === '', 422, 'to_queue_uuid is required for transfer.');
$toQueue = ServiceQueue::where('uuid', $toQueueUuid)->firstOrFail();
return $this->engine->transfer($ticket, $toQueue, $counter, $reason, $actor);
}
}