From a3521247e0a145a9fbf6ad7bdb92293906a7f497 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Fri, 17 Jul 2026 19:06:36 +0000 Subject: [PATCH] Route pharmacy, lab, and billing tickets through a shared FIFO pool. Stop pre-assigning prescriptions to a default counter so Call next follows arrival order and each pharmacist only serves one active patient at a time. Co-authored-by: Cursor --- .../Care/PrescriptionController.php | 35 ++++++++++++++++--- app/Services/Care/CareQueueBridge.php | 23 ++++-------- app/Services/Care/CareQueueContexts.php | 12 ++++--- app/Services/Care/CareQueueProvisioner.php | 7 +++- .../views/care/prescriptions/queue.blade.php | 16 ++++++--- 5 files changed, 62 insertions(+), 31 deletions(-) diff --git a/app/Http/Controllers/Care/PrescriptionController.php b/app/Http/Controllers/Care/PrescriptionController.php index 0eabfc9..f739db7 100644 --- a/app/Http/Controllers/Care/PrescriptionController.php +++ b/app/Http/Controllers/Care/PrescriptionController.php @@ -64,6 +64,32 @@ class PrescriptionController extends Controller $queue = $queue->filter(fn (Prescription $rx) => (int) ($rx->visit?->branch_id) === $branchScope)->values(); } + $queueIntegration = $branchId + ? $this->queueBridge->listMeta($organization, CareQueueContexts::PHARMACY, $branchId) + : ['enabled' => false]; + + $memberPoint = $branchId + ? app(\App\Services\Care\CareQueueProvisioner::class) + ->pointForMember($organization, CareQueueContexts::PHARMACY, $branchId, $member) + : null; + $memberCounterUuid = $memberPoint['counter_uuid'] ?? null; + + if (! empty($queueIntegration['enabled'])) { + $queue = $queue->sortBy(function (Prescription $rx) use ($memberCounterUuid) { + $status = (string) ($rx->queue_ticket_status ?? ''); + $mine = $memberCounterUuid + && (string) ($rx->queue_service_point_uuid ?? '') === (string) $memberCounterUuid; + $rank = match (true) { + $mine && $status === 'serving' => 0, + $mine && $status === 'called' => 1, + $status === 'waiting' || $status === '' => 2, + default => 3, + }; + + return sprintf('%d-%s', $rank, $rx->created_at?->format('Y-m-d H:i:s') ?? ''); + })->values(); + } + $drugs = \App\Models\Drug::owned($this->ownerRef($request)) ->where('organization_id', $organization->id) ->where('is_active', true) @@ -80,9 +106,8 @@ class PrescriptionController extends Controller 'drugs' => $drugs, 'canDispense' => $canDispense, 'branchId' => $branchId, - 'queueIntegration' => $branchId - ? $this->queueBridge->listMeta($organization, CareQueueContexts::PHARMACY, $branchId) - : ['enabled' => false], + 'queueIntegration' => $queueIntegration, + 'memberCounterUuid' => $memberCounterUuid, ]); } @@ -99,7 +124,9 @@ class PrescriptionController extends Controller $result = $this->queueBridge->callNext($organization, CareQueueContexts::PHARMACY, $branchId, $member); $ticket = $result['ticket'] ?? null; if (! $ticket) { - return back()->with('info', 'No prescriptions waiting at your pharmacy service point.'); + // Distinguish empty queue vs mid-dispense block via a follow-up check is hard over HTTP; + // prefer a clear action message for pharmacists. + return back()->with('info', 'No waiting patients — or finish the patient you are already serving before calling next.'); } return back()->with('success', 'Called '.$ticket['ticket_number'].'.'); diff --git a/app/Services/Care/CareQueueBridge.php b/app/Services/Care/CareQueueBridge.php index 682b90e..36fd6f2 100644 --- a/app/Services/Care/CareQueueBridge.php +++ b/app/Services/Care/CareQueueBridge.php @@ -109,7 +109,6 @@ class CareQueueBridge return $prescription; } - $point = $this->provisioner->defaultPoint($organization, CareQueueContexts::PHARMACY, $branchId); $ticket = $this->issue( $organization, CareQueueContexts::PHARMACY, @@ -122,14 +121,14 @@ class CareQueueBridge ], 'walk_in', 'api', - $point, + null, ); if (! $ticket) { return $prescription; } - $this->applyTicket($prescription, $ticket, $point); + $this->applyTicket($prescription, $ticket, null); return $prescription->fresh(); } @@ -141,11 +140,6 @@ class CareQueueBridge } $request->loadMissing('patient'); - $point = $this->provisioner->defaultPoint( - $organization, - CareQueueContexts::LABORATORY, - (int) $request->branch_id, - ); $ticket = $this->issue( $organization, CareQueueContexts::LABORATORY, @@ -158,14 +152,14 @@ class CareQueueBridge ], 'walk_in', 'api', - $point, + null, ); if (! $ticket) { return $request; } - $this->applyTicket($request, $ticket, $point); + $this->applyTicket($request, $ticket, null); return $request->fresh(); } @@ -181,11 +175,6 @@ class CareQueueBridge } $bill->loadMissing('patient'); - $point = $this->provisioner->defaultPoint( - $organization, - CareQueueContexts::BILLING, - (int) $bill->branch_id, - ); $ticket = $this->issue( $organization, CareQueueContexts::BILLING, @@ -198,14 +187,14 @@ class CareQueueBridge ], 'walk_in', 'api', - $point, + null, ); if (! $ticket) { return $bill; } - $this->applyTicket($bill, $ticket, $point); + $this->applyTicket($bill, $ticket, null); return $bill->fresh(); } diff --git a/app/Services/Care/CareQueueContexts.php b/app/Services/Care/CareQueueContexts.php index 79bd7c6..5768077 100644 --- a/app/Services/Care/CareQueueContexts.php +++ b/app/Services/Care/CareQueueContexts.php @@ -32,7 +32,8 @@ class CareQueueContexts /** * Core department queues provisioned for every branch when Queue integration is on. - * Consultation/pharmacy/lab/billing use assigned_only routing (per service point). + * Consultation/triage stay assigned_only (doctor/nurse-specific). + * Pharmacy/lab/billing/imaging use a shared FIFO pool claimed by each counter on Call next. * * @return array */ @@ -60,25 +61,26 @@ class CareQueueContexts self::PHARMACY => [ 'name' => 'Pharmacy', 'prefix' => 'P', - 'routing_mode' => 'assigned_only', + // Shared FIFO pool — any pharmacist counter claims the next waiting ticket. + 'routing_mode' => 'shared_pool', 'type' => 'pharmacy', ], self::LABORATORY => [ 'name' => 'Laboratory', 'prefix' => 'L', - 'routing_mode' => 'assigned_only', + 'routing_mode' => 'shared_pool', 'type' => 'laboratory', ], self::BILLING => [ 'name' => 'Billing', 'prefix' => 'B', - 'routing_mode' => 'assigned_only', + 'routing_mode' => 'shared_pool', 'type' => 'cashier', ], self::IMAGING => [ 'name' => 'Imaging', 'prefix' => 'I', - 'routing_mode' => 'assigned_only', + 'routing_mode' => 'shared_pool', 'type' => 'laboratory', ], ]; diff --git a/app/Services/Care/CareQueueProvisioner.php b/app/Services/Care/CareQueueProvisioner.php index ade6ca0..141188e 100644 --- a/app/Services/Care/CareQueueProvisioner.php +++ b/app/Services/Care/CareQueueProvisioner.php @@ -484,9 +484,14 @@ class CareQueueProvisioner int $branchId, ): ?array { $existing = $this->resourcesFor($organization, $context, $branchId); + $expectedMode = CareQueueContexts::departments()[$context]['routing_mode'] + ?? (CareQueueContexts::isSpecialty($context) ? 'assigned_only' : null); + $modeMatches = ! $expectedMode + || ($existing['routing_mode'] ?? null) === $expectedMode; + if ($existing && ! empty($existing['queue_uuid']) && ( ! empty($existing['points']) || ! empty($existing['counter_uuid']) - )) { + ) && $modeMatches) { return $existing; } diff --git a/resources/views/care/prescriptions/queue.blade.php b/resources/views/care/prescriptions/queue.blade.php index 21a8096..ffe8d67 100644 --- a/resources/views/care/prescriptions/queue.blade.php +++ b/resources/views/care/prescriptions/queue.blade.php @@ -34,10 +34,18 @@ @if ($canDispense)
@if (! empty($queueIntegration['enabled']) && $rx->queue_ticket_uuid && ($rx->queue_ticket_status ?? '') !== 'serving') -
- @csrf - -
+ @php + $pointUuid = (string) ($rx->queue_service_point_uuid ?? ''); + $mine = empty($memberCounterUuid) + || $pointUuid === '' + || $pointUuid === (string) $memberCounterUuid; + @endphp + @if ($mine) +
+ @csrf + +
+ @endif @endif