Fix queue handoff so tickets keep their number across services.
Deploy Ladill Queue / deploy (push) Successful in 40s

Handoff now frees the counter, ends the service session, and requeues
the same ticket for the next department — critical for hospital flows
like doctor → lab.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-14 21:22:01 +00:00
co-authored by Cursor
parent d938acf909
commit d2f5ff11d2
12 changed files with 333 additions and 74 deletions
+42 -21
View File
@@ -61,37 +61,45 @@ class ConsoleController extends Controller
'action' => ['required', 'string'],
'queue_id' => ['nullable', 'integer'],
'ticket_id' => ['nullable', 'integer'],
'to_queue_id' => ['nullable', 'integer'],
'to_queue_id' => ['required_if:action,transfer', 'nullable', 'integer'],
'reason' => ['nullable', 'string', 'max:500'],
]);
match ($validated['action']) {
'call_next' => $this->handleCallNext($counter, (int) $validated['queue_id'], $actor),
'recall' => $this->handleTicketAction($validated['ticket_id'], 'recall', $actor),
'start' => $this->handleTicketAction($validated['ticket_id'], 'start', $actor),
'hold' => $this->handleTicketAction($validated['ticket_id'], 'hold', $actor),
'skip' => $this->handleTicketAction($validated['ticket_id'], 'skip', $actor),
'complete' => $this->handleTicketAction($validated['ticket_id'], 'complete', $actor),
'no_show' => $this->handleTicketAction($validated['ticket_id'], 'no_show', $actor),
'transfer' => $this->handleTransfer($validated['ticket_id'], (int) $validated['to_queue_id'], $counter, $validated['reason'] ?? null, $actor),
'available' => $counter->update(['status' => 'available']),
'offline' => $counter->update(['status' => 'offline']),
default => null,
};
try {
$message = match ($validated['action']) {
'call_next' => $this->handleCallNext($counter, (int) $validated['queue_id'], $actor),
'recall' => $this->handleTicketAction($validated['ticket_id'], 'recall', $actor),
'start' => $this->handleTicketAction($validated['ticket_id'], 'start', $actor),
'hold' => $this->handleTicketAction($validated['ticket_id'], 'hold', $actor),
'skip' => $this->handleTicketAction($validated['ticket_id'], 'skip', $actor),
'complete' => $this->handleTicketAction($validated['ticket_id'], 'complete', $actor),
'no_show' => $this->handleTicketAction($validated['ticket_id'], 'no_show', $actor),
'transfer' => $this->handleTransfer($validated['ticket_id'], (int) $validated['to_queue_id'], $counter, $validated['reason'] ?? null, $actor),
'available' => tap(null, fn () => $counter->update(['status' => 'available'])),
'offline' => tap(null, fn () => $counter->update(['status' => 'offline'])),
default => null,
};
} catch (\RuntimeException $e) {
return back()->with('error', $e->getMessage());
}
return back();
return $message
? back()->with('success', $message)
: back();
}
protected function handleCallNext(Counter $counter, int $queueId, string $actor): void
protected function handleCallNext(Counter $counter, int $queueId, string $actor): ?string
{
$queue = ServiceQueue::findOrFail($queueId);
$this->engine->callNext($queue, $counter, $actor);
$ticket = $this->engine->callNext($queue, $counter, $actor);
return $ticket ? "Called {$ticket->ticket_number}." : 'No tickets waiting in that queue.';
}
protected function handleTicketAction(?int $ticketId, string $action, string $actor): void
protected function handleTicketAction(?int $ticketId, string $action, string $actor): ?string
{
if (! $ticketId) {
return;
return null;
}
$ticket = Ticket::findOrFail($ticketId);
match ($action) {
@@ -103,15 +111,28 @@ class ConsoleController extends Controller
'no_show' => $this->engine->markNoShow($ticket, $actor),
default => null,
};
return null;
}
protected function handleTransfer(?int $ticketId, int $toQueueId, Counter $counter, ?string $reason, string $actor): void
protected function handleTransfer(?int $ticketId, int $toQueueId, Counter $counter, ?string $reason, string $actor): ?string
{
if (! $ticketId || ! $toQueueId) {
return;
return null;
}
$ticket = Ticket::findOrFail($ticketId);
$toQueue = ServiceQueue::findOrFail($toQueueId);
if (
$toQueue->organization_id !== $counter->organization_id
|| $toQueue->branch_id !== $counter->branch_id
|| $toQueue->owner_ref !== $counter->owner_ref
) {
throw new \RuntimeException('You can only hand off to queues at this branch.');
}
$this->engine->transfer($ticket, $toQueue, $counter, $reason, $actor);
return "Handed off {$ticket->ticket_number} to {$toQueue->name}. Ticket number unchanged.";
}
}
@@ -92,7 +92,13 @@ class TicketController extends Controller
$this->authorizeAbility($request, 'tickets.view');
$this->authorizeOwner($request, $ticket);
$ticket->load(['serviceQueue', 'counter', 'events']);
$ticket->load([
'serviceQueue',
'counter',
'events',
'transfers.fromQueue',
'transfers.toQueue',
]);
return view('qms.tickets.show', [
'ticket' => $ticket,