From 86d79b5099cf2e170f764b2a261e5df54b076e4b Mon Sep 17 00:00:00 2001 From: isaacclad Date: Fri, 17 Jul 2026 23:05:42 +0000 Subject: [PATCH] Restore Care queue FIFO after demo skip left tickets Unassigned. 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 --- .../Care/PrescriptionController.php | 13 ++++- app/Http/Controllers/Care/QueueController.php | 2 +- app/Services/Care/CareQueueBridge.php | 50 +++++++++++------ app/Services/Care/CareQueueProvisioner.php | 55 ++++++++++++++++++- app/Services/Care/DemoTenantSeeder.php | 6 ++ .../views/care/partials/queue-ops.blade.php | 8 ++- 6 files changed, 111 insertions(+), 23 deletions(-) diff --git a/app/Http/Controllers/Care/PrescriptionController.php b/app/Http/Controllers/Care/PrescriptionController.php index f739db7..82d9270 100644 --- a/app/Http/Controllers/Care/PrescriptionController.php +++ b/app/Http/Controllers/Care/PrescriptionController.php @@ -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) diff --git a/app/Http/Controllers/Care/QueueController.php b/app/Http/Controllers/Care/QueueController.php index 4a70236..33dd3fa 100644 --- a/app/Http/Controllers/Care/QueueController.php +++ b/app/Http/Controllers/Care/QueueController.php @@ -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); } diff --git a/app/Services/Care/CareQueueBridge.php b/app/Services/Care/CareQueueBridge.php index 00f8619..0d30328 100644 --- a/app/Services/Care/CareQueueBridge.php +++ b/app/Services/Care/CareQueueBridge.php @@ -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(); diff --git a/app/Services/Care/CareQueueProvisioner.php b/app/Services/Care/CareQueueProvisioner.php index 141188e..d4d0b61 100644 --- a/app/Services/Care/CareQueueProvisioner.php +++ b/app/Services/Care/CareQueueProvisioner.php @@ -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>, 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; + } } diff --git a/app/Services/Care/DemoTenantSeeder.php b/app/Services/Care/DemoTenantSeeder.php index d363674..36ef2de 100644 --- a/app/Services/Care/DemoTenantSeeder.php +++ b/app/Services/Care/DemoTenantSeeder.php @@ -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, diff --git a/resources/views/care/partials/queue-ops.blade.php b/resources/views/care/partials/queue-ops.blade.php index ebd3bd5..83cafc8 100644 --- a/resources/views/care/partials/queue-ops.blade.php +++ b/resources/views/care/partials/queue-ops.blade.php @@ -8,7 +8,13 @@

Queue

-

Call next operates on your assigned service point only — tickets routed to other rooms or desks stay there.

+

+ @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 +

@csrf