Fix Call next when doctor desk is missing from cached points.
Deploy Ladill Care / deploy (push) Successful in 48s

Assigned doctors could see board waiters while Call next returned empty because ensure() early-returned without provisioning a newly linked desk. Re-provision on point miss, recover stale tickets, and align empty messaging with visible waiters.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-18 20:31:52 +00:00
co-authored by Cursor
parent c123097f3c
commit b077467c4b
4 changed files with 273 additions and 12 deletions
+34 -2
View File
@@ -125,7 +125,12 @@ class QueueController extends Controller
$waitingCountQuery = Appointment::owned($owner)
->where('organization_id', $organization->id)
->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]);
->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN])
->where(function ($q) {
$q->whereNull('queue_ticket_status')
->orWhere('queue_ticket_status', '')
->orWhereNotIn('queue_ticket_status', ['called', 'serving']);
});
if ($lockToPractitioner) {
$waitingCountQuery->whereIn('practitioner_id', $practitionerScope ?: [0]);
if ($branchId > 0) {
@@ -142,6 +147,14 @@ class QueueController extends Controller
$waitingCountQuery->whereRaw('1 = 0');
}
// Hero Waiting must match the Waiting column (same list the doctor acts on).
$waitingHero = $queue->reject(
fn ($a) => in_array($a->queue_ticket_status ?? '', ['called', 'serving'], true)
)->count();
if ($queue->count() >= $boardLimit) {
$waitingHero = max($waitingHero, $waitingCountQuery->count());
}
$inCareCountQuery = Appointment::owned($owner)
->where('organization_id', $organization->id)
->where('status', Appointment::STATUS_IN_CONSULTATION);
@@ -172,7 +185,7 @@ class QueueController extends Controller
->get();
$heroStats = [
'waiting' => $waitingCountQuery->count(),
'waiting' => $waitingHero,
'in_consultation' => $inCareCountQuery->count(),
'today' => $todayQuery->count(),
];
@@ -258,6 +271,25 @@ class QueueController extends Controller
? 'consultation'
: $context;
$waitingVisible = Appointment::owned($owner)
->where('organization_id', $organization->id)
->where('branch_id', $branchId)
->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN])
->when($practitionerId, fn ($q) => $q->where('practitioner_id', $practitionerId))
->where(function ($q) {
$q->whereNull('queue_ticket_status')
->orWhere('queue_ticket_status', '')
->orWhereNotIn('queue_ticket_status', ['called', 'serving']);
})
->count();
if ($waitingVisible > 0) {
return back()->with(
'info',
"{$waitingVisible} waiting but none are callable at your {$label} desk right now (payment hold or specialty mismatch)."
);
}
return back()->with('info', "No patients waiting at your {$label} service point.");
}
+71 -9
View File
@@ -455,16 +455,37 @@ class CareQueueBridge
): array {
$empty = ['ticket' => null, 'entity' => null, 'resources' => $resources];
$appointment = $this->nextCallableAppointment($organization, $context, $branchId, $practitionerId);
if (! $appointment) {
$candidates = $this->callableAppointments($organization, $context, $branchId, $practitionerId);
if ($candidates === []) {
$this->syncMissingTickets($organization, $context, $branchId, 25);
$appointment = $this->nextCallableAppointment($organization, $context, $branchId, $practitionerId);
$candidates = $this->callableAppointments($organization, $context, $branchId, $practitionerId);
}
if (! $appointment) {
return $empty;
foreach ($candidates as $appointment) {
$called = $this->callSpecificAppointment($organization, $context, $branchId, $appointment, $resources, $point);
if ($called['ticket']) {
return $called;
}
}
return $empty;
}
/**
* @param array{queue_uuid?: ?string, counter_uuid?: ?string, points?: list<array<string, mixed>>} $resources
* @param array<string, mixed> $point
* @return array{ticket: ?array<string, mixed>, entity: ?Model, resources: array<string, mixed>}
*/
protected function callSpecificAppointment(
Organization $organization,
string $context,
int $branchId,
Appointment $appointment,
array $resources,
array $point,
): array {
$empty = ['ticket' => null, 'entity' => null, 'resources' => $resources];
if (blank($appointment->queue_ticket_uuid)) {
$appointment = $this->issueForAppointment(
$organization,
@@ -505,6 +526,43 @@ class CareQueueBridge
(string) $point['counter_uuid'],
);
// Stale uuid / wrong queue / non-waiting row — clear and re-issue onto this desk.
if (! $ticket) {
$appointment->forceFill([
'queue_ticket_uuid' => null,
'queue_ticket_number' => null,
'queue_ticket_status' => null,
'queue_service_point_uuid' => null,
])->save();
$ticket = $this->issue(
$organization,
$context,
$branchId,
$appointment->patient,
[
'care_entity' => 'appointment',
'care_entity_uuid' => $appointment->uuid,
'care_entity_id' => $appointment->id,
'practitioner_id' => $appointment->practitioner_id,
],
$appointment->type === Appointment::TYPE_WALK_IN ? 'walk_in' : 'appointment',
'appointment',
$point,
);
if (! $ticket) {
return $empty;
}
$this->applyTicket($appointment, $ticket, $point);
$appointment = $appointment->fresh() ?? $appointment;
$ticket = $this->attemptCallWaitingTicket(
$organization,
(string) $appointment->queue_ticket_uuid,
(string) $point['counter_uuid'],
);
}
if (! $ticket) {
return $empty;
}
@@ -518,12 +576,15 @@ class CareQueueBridge
];
}
protected function nextCallableAppointment(
/**
* @return list<Appointment>
*/
protected function callableAppointments(
Organization $organization,
string $context,
int $branchId,
?int $practitionerId,
): ?Appointment {
): array {
$owner = (string) $organization->owner_ref;
$query = Appointment::owned($owner)
@@ -546,6 +607,7 @@ class CareQueueBridge
$query->where('practitioner_id', $practitionerId);
}
$callable = [];
foreach ($query->get() as $appointment) {
if ($this->contextForAppointment($organization, $appointment) !== $context) {
continue;
@@ -557,10 +619,10 @@ class CareQueueBridge
continue;
}
return $appointment;
$callable[] = $appointment;
}
return null;
return $callable;
}
/**
+81 -1
View File
@@ -475,6 +475,10 @@ class CareQueueProvisioner
/**
* Resolve a service point for a practitioner on a queue context.
*
* When a doctor is linked after queues were first provisioned, ensure() may
* early-return with a stale points list refresh provisioning on miss so
* Call next can still reach board waiters assigned to that desk.
*
* @return array<string, mixed>|null
*/
public function pointForPractitioner(
@@ -482,17 +486,93 @@ class CareQueueProvisioner
int $branchId,
int $practitionerId,
string $context = CareQueueContexts::CONSULTATION,
): ?array {
$point = $this->findPractitionerPoint($organization, $context, $branchId, $practitionerId);
if ($point && ! empty($point['counter_uuid'])) {
return $point;
}
if (! $this->shouldProvision($organization)) {
return $this->practitionerPointFromDb($organization, $context, $branchId, $practitionerId);
}
if (CareQueueContexts::isSpecialty($context)) {
$this->provisionSpecialtyContextBranch($organization->fresh() ?? $organization, $context, $branchId);
} elseif (CareQueueContexts::isDepartment($context)) {
$this->provisionContextBranch($organization->fresh() ?? $organization, $context, $branchId);
} else {
return $this->practitionerPointFromDb($organization, $context, $branchId, $practitionerId);
}
$organization = $organization->fresh() ?? $organization;
$point = $this->findPractitionerPoint($organization, $context, $branchId, $practitionerId);
if ($point && ! empty($point['counter_uuid'])) {
return $point;
}
return $this->practitionerPointFromDb($organization, $context, $branchId, $practitionerId);
}
/**
* @return array<string, mixed>|null
*/
protected function findPractitionerPoint(
Organization $organization,
string $context,
int $branchId,
int $practitionerId,
): ?array {
$resources = $this->resourcesFor($organization, $context, $branchId);
if (! $resources) {
return null;
}
return collect($resources['points'])->first(
return collect($resources['points'] ?? [])->first(
fn ($p) => ($p['kind'] ?? '') === 'practitioner' && (int) ($p['ref_id'] ?? 0) === $practitionerId
);
}
/**
* @return array<string, mixed>|null
*/
protected function practitionerPointFromDb(
Organization $organization,
string $context,
int $branchId,
int $practitionerId,
): ?array {
$row = CareServicePoint::query()
->where('organization_id', $organization->id)
->where('kind', 'practitioner')
->where('ref_id', $practitionerId)
->where('is_active', true)
->whereHas(
'serviceQueue',
fn ($q) => $q
->where('organization_id', $organization->id)
->where('context', $context)
->where('branch_id', $branchId)
->where('is_active', true),
)
->first();
if (! $row) {
return null;
}
return [
'kind' => 'practitioner',
'ref_id' => $practitionerId,
'name' => $row->name,
'destination' => $row->destination,
'staff_ref' => $row->staff_ref,
'staff_display_name' => $row->staff_display_name,
'external_key' => $row->external_key,
'counter_uuid' => $row->uuid,
'synced' => true,
];
}
/**
* @return array<string, mixed>|null
*/