Route pharmacy, lab, and billing tickets through a shared FIFO pool.
Deploy Ladill Care / deploy (push) Successful in 57s

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 <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-17 19:06:36 +00:00
co-authored by Cursor
parent 5e8c082e02
commit a3521247e0
5 changed files with 62 additions and 31 deletions
@@ -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'].'.');
+6 -17
View File
@@ -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();
}
+7 -5
View File
@@ -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<string, array{name: string, prefix: string, routing_mode: string, type: string}>
*/
@@ -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',
],
];
+6 -1
View File
@@ -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;
}
@@ -34,10 +34,18 @@
@if ($canDispense)
<div class="mt-3 flex flex-wrap gap-2">
@if (! empty($queueIntegration['enabled']) && $rx->queue_ticket_uuid && ($rx->queue_ticket_status ?? '') !== 'serving')
<form method="POST" action="{{ route('care.prescriptions.serve', $rx) }}">
@csrf
<button type="submit" class="rounded-lg bg-sky-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-sky-700">Serve</button>
</form>
@php
$pointUuid = (string) ($rx->queue_service_point_uuid ?? '');
$mine = empty($memberCounterUuid)
|| $pointUuid === ''
|| $pointUuid === (string) $memberCounterUuid;
@endphp
@if ($mine)
<form method="POST" action="{{ route('care.prescriptions.serve', $rx) }}">
@csrf
<button type="submit" class="rounded-lg bg-sky-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-sky-700">Serve</button>
</form>
@endif
@endif
</div>
<form method="POST" action="{{ route('care.prescriptions.dispense', $rx) }}" class="mt-4 border-t border-slate-100 pt-4">