Restore Care queue FIFO after demo skip left tickets Unassigned.
Deploy Ladill Care / deploy (push) Successful in 1m2s

Provision one branch/context at a time instead of the whole org, fall back to a desk when doctor counters are catching up, sync pharmacy tickets in Care list order on page load, and seed waiting_at/queue_position for demos.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-17 23:05:42 +00:00
co-authored by Cursor
parent 640b62010a
commit 86d79b5099
6 changed files with 111 additions and 23 deletions
@@ -64,9 +64,16 @@ 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];
$queueIntegration = ['enabled' => false];
if ($branchId && $this->queueBridge->isEnabled($organization)) {
// Issue tickets in Care FIFO order so Queue Call Next matches this list.
$this->queueBridge->syncMissingTickets($organization, CareQueueContexts::PHARMACY, $branchId, 50);
$queueIntegration = $this->queueBridge->listMeta($organization, CareQueueContexts::PHARMACY, $branchId);
$queue = $this->prescriptions->pharmacyQueue($this->ownerRef($request), $organization->id);
if ($branchScope) {
$queue = $queue->filter(fn (Prescription $rx) => (int) ($rx->visit?->branch_id) === $branchScope)->values();
}
}
$memberPoint = $branchId
? app(\App\Services\Care\CareQueueProvisioner::class)
@@ -47,7 +47,7 @@ class QueueController extends Controller
$queueIntegration = ['enabled' => false];
if ($branchId && $this->queueBridge->isEnabled($organization)) {
// Catch up tickets for waiters created before Queue was linked (demo / pre-enable).
$this->queueBridge->syncMissingTickets($organization, CareQueueContexts::CONSULTATION, $branchId, 25);
$this->queueBridge->syncMissingTickets($organization, CareQueueContexts::CONSULTATION, $branchId, 50);
$queueIntegration = $this->queueBridge->listMeta($organization, CareQueueContexts::CONSULTATION, $branchId);
}
+33 -17
View File
@@ -407,26 +407,41 @@ class CareQueueBridge
*/
protected function resolveConsultationPoint(Organization $organization, Appointment $appointment): ?array
{
if ($appointment->practitioner_id) {
$point = $this->provisioner->pointForPractitioner(
$organization,
(int) $appointment->branch_id,
(int) $appointment->practitioner_id,
);
if ($point) {
return $point;
}
// Practitioner assigned but point not provisioned yet — refresh once.
$this->provisioner->ensure($organization, CareQueueContexts::CONSULTATION, (int) $appointment->branch_id);
$branchId = (int) $appointment->branch_id;
return $this->provisioner->pointForPractitioner(
$organization->fresh(),
(int) $appointment->branch_id,
(int) $appointment->practitioner_id,
);
// Assigned-only consultation requires a doctor — leave Unassigned until one is chosen.
if (! $appointment->practitioner_id) {
return null;
}
return null;
$point = $this->provisioner->pointForPractitioner(
$organization,
$branchId,
(int) $appointment->practitioner_id,
);
if ($point) {
return $point;
}
// Practitioner assigned but point not provisioned yet — refresh this branch only.
$this->provisioner->ensure($organization, CareQueueContexts::CONSULTATION, $branchId);
$point = $this->provisioner->pointForPractitioner(
$organization->fresh(),
$branchId,
(int) $appointment->practitioner_id,
);
if ($point) {
return $point;
}
// Prefer a desk ticket over leaving waiters Unassigned while counters catch up
// after demo seed / deferred settings provision.
return $this->provisioner->defaultPoint(
$organization->fresh(),
CareQueueContexts::CONSULTATION,
$branchId,
);
}
/**
@@ -659,6 +674,7 @@ class CareQueueBridge
})
->whereHas('visit', fn ($q) => $q->where('branch_id', $branchId))
->with(['patient', 'visit'])
->orderBy('created_at')
->orderBy('id')
->limit($limit)
->get();
+54 -1
View File
@@ -476,6 +476,9 @@ class CareQueueProvisioner
}
/**
* Ensure Queue resources exist for one context + branch only.
* Never fans out across every department × branch (that hung demo queues).
*
* @return array{queue_uuid: ?string, counter_uuid: ?string, queue_external_key: string, counter_external_key: string, synced: bool, points: list<array<string, mixed>>, routing_mode: string}|null
*/
public function ensure(
@@ -507,8 +510,58 @@ class CareQueueProvisioner
return null;
}
$this->provisionOrganization($organization->fresh());
$this->provisionContextBranch($organization->fresh(), $context, $branchId);
return $this->resourcesFor($organization->fresh(), $context, $branchId);
}
/**
* Provision a single department queue for one branch (idempotent).
*/
public function provisionContextBranch(Organization $organization, string $context, int $branchId): ?array
{
if (! $this->shouldProvision($organization)) {
return $this->resourcesFor($organization, $context, $branchId);
}
$meta = CareQueueContexts::departments()[$context] ?? null;
if ($meta === null) {
return null;
}
$branch = Branch::owned((string) $organization->owner_ref)
->where('organization_id', $organization->id)
->whereKey($branchId)
->first();
if (! $branch) {
return null;
}
$provisioning = (array) data_get($organization->settings, 'department_queue_provisioning', []);
$queues = is_array($provisioning[$context]['queues'] ?? null)
? $provisioning[$context]['queues']
: [];
$byBranch = collect($queues)->keyBy(fn ($q) => (string) ($q['branch_id'] ?? ''));
$prior = $byBranch->get((string) $branch->id, []);
$stub = $this->provisionBranchDepartment(
$organization,
(string) $organization->owner_ref,
$context,
$meta,
$branch,
is_array($prior) ? $prior : [],
);
$byBranch->put((string) $branch->id, $stub);
$provisioning[$context] = [
'queues' => $byBranch->values()->all(),
];
$settings = $organization->settings ?? [];
$settings['department_queue_provisioning'] = $provisioning;
$organization->update(['settings' => $settings]);
return $stub;
}
}
+6
View File
@@ -896,6 +896,12 @@ class DemoTenantSeeder
'status' => $status,
'scheduled_at' => $scheduledAt,
'checked_in_at' => $visit?->checked_in_at,
'waiting_at' => in_array($status, [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN], true)
? $scheduledAt->copy()->addMinutes(5)
: null,
'queue_position' => in_array($status, [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN], true)
? ($i + 1)
: null,
'completed_at' => $status === Appointment::STATUS_COMPLETED
? $scheduledAt->copy()->addHour()
: null,
@@ -8,7 +8,13 @@
<div class="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-sky-100 bg-sky-50/70 px-4 py-3">
<div>
<p class="text-xs font-semibold uppercase tracking-wide text-sky-700">Queue</p>
<p class="text-sm text-slate-600">Call next operates on your assigned service point only tickets routed to other rooms or desks stay there.</p>
<p class="text-sm text-slate-600">
@if (($qi['routing_mode'] ?? null) === 'shared_pool')
Call next takes the next waiting patient in line for this queue (shared pool).
@else
Call next operates on your assigned service point only tickets routed to other rooms or desks stay there.
@endif
</p>
</div>
<form method="POST" action="{{ route($callNextRoute, $callNextParams) }}">
@csrf