Fix Call next by advancing Care waiters and reclaiming desk tickets.
Deploy Ladill Care / deploy (push) Successful in 51s
Deploy Ladill Care / deploy (push) Successful in 51s
Call next now picks the next board appointment, issues or reclaims its ticket onto the calling desk, and still fires display/voice announcements — so patients are not left stuck when tickets sit on the wrong point after sync-on-GET was removed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,6 +4,7 @@ namespace App\Services\Care;
|
||||
|
||||
use App\Models\Appointment;
|
||||
use App\Models\Bill;
|
||||
use App\Models\CareQueueTicket;
|
||||
use App\Models\InvestigationRequest;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
@@ -376,6 +377,22 @@ class CareQueueBridge
|
||||
return ['ticket' => null, 'entity' => null, 'resources' => $resources];
|
||||
}
|
||||
|
||||
// GP / specialty: advance the next Care waiter (board order), even when their
|
||||
// ticket sits on another desk or was never issued (sync-on-GET was removed).
|
||||
if ($this->isAppointmentQueueContext($context)) {
|
||||
$fromCare = $this->callNextAppointment(
|
||||
$organization,
|
||||
$context,
|
||||
$branchId,
|
||||
$practitionerId,
|
||||
$resources,
|
||||
$point,
|
||||
);
|
||||
if ($fromCare['ticket']) {
|
||||
return $fromCare;
|
||||
}
|
||||
}
|
||||
|
||||
$callResources = [
|
||||
'queue_uuid' => $resources['queue_uuid'],
|
||||
'counter_uuid' => $point['counter_uuid'],
|
||||
@@ -415,6 +432,157 @@ class CareQueueBridge
|
||||
return ['ticket' => $ticket, 'entity' => $entity?->fresh(), 'resources' => $resources];
|
||||
}
|
||||
|
||||
protected function isAppointmentQueueContext(string $context): bool
|
||||
{
|
||||
return $context === CareQueueContexts::CONSULTATION
|
||||
|| CareQueueContexts::isSpecialty($context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the next waiting appointment for this context — matching board order.
|
||||
*
|
||||
* @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 callNextAppointment(
|
||||
Organization $organization,
|
||||
string $context,
|
||||
int $branchId,
|
||||
?int $practitionerId,
|
||||
array $resources,
|
||||
array $point,
|
||||
): array {
|
||||
$empty = ['ticket' => null, 'entity' => null, 'resources' => $resources];
|
||||
|
||||
$appointment = $this->nextCallableAppointment($organization, $context, $branchId, $practitionerId);
|
||||
if (! $appointment) {
|
||||
$this->syncMissingTickets($organization, $context, $branchId, 25);
|
||||
$appointment = $this->nextCallableAppointment($organization, $context, $branchId, $practitionerId);
|
||||
}
|
||||
|
||||
if (! $appointment) {
|
||||
return $empty;
|
||||
}
|
||||
|
||||
if (blank($appointment->queue_ticket_uuid)) {
|
||||
$appointment = $this->issueForAppointment(
|
||||
$organization,
|
||||
$appointment->fresh(['patient', 'organization', 'practitioner', 'visit']) ?? $appointment,
|
||||
) ?? $appointment;
|
||||
}
|
||||
|
||||
if (blank($appointment->queue_ticket_uuid)) {
|
||||
// Last resort: issue directly onto the calling desk.
|
||||
$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) {
|
||||
$this->applyTicket($appointment, $ticket, $point);
|
||||
$appointment = $appointment->fresh() ?? $appointment;
|
||||
}
|
||||
}
|
||||
|
||||
if (blank($appointment->queue_ticket_uuid)) {
|
||||
return $empty;
|
||||
}
|
||||
|
||||
$ticket = $this->attemptCallWaitingTicket(
|
||||
$organization,
|
||||
(string) $appointment->queue_ticket_uuid,
|
||||
(string) $point['counter_uuid'],
|
||||
);
|
||||
|
||||
if (! $ticket) {
|
||||
return $empty;
|
||||
}
|
||||
|
||||
$this->applyTicket($appointment, $ticket, $point);
|
||||
|
||||
return [
|
||||
'ticket' => $ticket,
|
||||
'entity' => $appointment->fresh(),
|
||||
'resources' => $resources,
|
||||
];
|
||||
}
|
||||
|
||||
protected function nextCallableAppointment(
|
||||
Organization $organization,
|
||||
string $context,
|
||||
int $branchId,
|
||||
?int $practitionerId,
|
||||
): ?Appointment {
|
||||
$owner = (string) $organization->owner_ref;
|
||||
|
||||
$query = Appointment::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('branch_id', $branchId)
|
||||
->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']);
|
||||
})
|
||||
->with(['patient', 'organization', 'practitioner', 'visit'])
|
||||
->orderBy('queue_position')
|
||||
->orderBy('waiting_at')
|
||||
->orderBy('checked_in_at')
|
||||
->orderBy('id')
|
||||
->limit(50);
|
||||
|
||||
if ($practitionerId) {
|
||||
$query->where('practitioner_id', $practitionerId);
|
||||
}
|
||||
|
||||
foreach ($query->get() as $appointment) {
|
||||
if ($this->contextForAppointment($organization, $appointment) !== $context) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Already ticketed waiters stay callable; gate only blocks new issuance.
|
||||
if (blank($appointment->queue_ticket_uuid)
|
||||
&& ! $this->workflowGate->canRelease($organization, $appointment->visit, $context)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return $appointment;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
protected function attemptCallWaitingTicket(
|
||||
Organization $organization,
|
||||
string $ticketUuid,
|
||||
string $counterUuid,
|
||||
): ?array {
|
||||
try {
|
||||
return $this->engine->callWaitingTicket($organization, $ticketUuid, $counterUuid);
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('care.queue_call_waiting_ticket_failed', [
|
||||
'ticket_uuid' => $ticketUuid,
|
||||
'message' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{queue_uuid?: ?string, counter_uuid?: ?string, points?: list<array<string, mixed>>} $resources
|
||||
* @param array<string, mixed> $preferredPoint
|
||||
@@ -736,7 +904,7 @@ class CareQueueBridge
|
||||
|
||||
$owner = (string) $organization->owner_ref;
|
||||
|
||||
return match (true) {
|
||||
$entity = match (true) {
|
||||
$context === CareQueueContexts::PHARMACY => Prescription::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('queue_ticket_uuid', $ticketUuid)
|
||||
@@ -754,6 +922,40 @@ class CareQueueBridge
|
||||
->where('queue_ticket_uuid', $ticketUuid)
|
||||
->first(),
|
||||
};
|
||||
|
||||
if ($entity) {
|
||||
return $entity;
|
||||
}
|
||||
|
||||
// Recover when the Care row lost its uuid mirror but the ticket still points at it.
|
||||
$ticket = CareQueueTicket::query()
|
||||
->where('organization_id', $organization->id)
|
||||
->where('uuid', $ticketUuid)
|
||||
->first();
|
||||
|
||||
if (! $ticket || ! $ticket->care_entity_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return match ($ticket->care_entity) {
|
||||
'prescription' => Prescription::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->whereKey($ticket->care_entity_id)
|
||||
->first(),
|
||||
'investigation_request' => InvestigationRequest::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->whereKey($ticket->care_entity_id)
|
||||
->first(),
|
||||
'bill' => Bill::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->whereKey($ticket->care_entity_id)
|
||||
->first(),
|
||||
'appointment' => Appointment::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->whereKey($ticket->care_entity_id)
|
||||
->first(),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
protected function syncMissingAppointments(Organization $organization, string $context, int $branchId, int $limit): int
|
||||
|
||||
Reference in New Issue
Block a user