Route pharmacy, lab, and billing tickets through a shared FIFO pool.
Deploy Ladill Care / deploy (push) Successful in 57s
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:
@@ -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'].'.');
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user