Fix specialty Call next vs WAITING KPI mismatch.
Deploy Ladill Care / deploy (push) Successful in 1m44s
Deploy Ladill Care / deploy (push) Successful in 1m44s
Admins without a desk now scan all specialty service points, KPIs use the same branch as queue ops, and empty Call next flashes explain tickets vs other branches. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -304,10 +304,50 @@ class SpecialtyModuleController extends Controller
|
||||
$result = $queueBridge->callNext($organization, $module, $branchId, $member, $practitionerId);
|
||||
$ticket = $result['ticket'] ?? null;
|
||||
if (! $ticket) {
|
||||
$owner = $this->ownerRef($request);
|
||||
$departmentIds = Department::owned($owner)
|
||||
->where('type', $definition['department_type'] ?? 'general')
|
||||
->where('is_active', true)
|
||||
->when($branchScope, fn ($q) => $q->where('branch_id', $branchScope))
|
||||
->pluck('id');
|
||||
|
||||
$waitingAtBranch = Appointment::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('branch_id', $branchId)
|
||||
->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN])
|
||||
->when($departmentIds->isNotEmpty(), fn ($q) => $q->whereIn('department_id', $departmentIds))
|
||||
->when($departmentIds->isEmpty(), fn ($q) => $q->whereRaw('1 = 0'))
|
||||
->count();
|
||||
|
||||
$waitingOrg = Appointment::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN])
|
||||
->when($departmentIds->isNotEmpty(), fn ($q) => $q->whereIn('department_id', $departmentIds))
|
||||
->when($departmentIds->isEmpty(), fn ($q) => $q->whereRaw('1 = 0'))
|
||||
->count();
|
||||
|
||||
$branchName = \App\Models\Branch::owned($owner)->whereKey($branchId)->value('name') ?: 'this branch';
|
||||
|
||||
if ($waitingAtBranch > 0) {
|
||||
return back()->with(
|
||||
'info',
|
||||
"{$waitingAtBranch} patient(s) are waiting in {$definition['label']} at {$branchName}, but none have a callable ticket at a service desk yet. Open Visits and use Start consultation, or check in again so a ticket is issued."
|
||||
);
|
||||
}
|
||||
|
||||
if ($waitingOrg > $waitingAtBranch) {
|
||||
return back()->with(
|
||||
'info',
|
||||
"No patients waiting in {$definition['label']} at {$branchName}. {$waitingOrg} are waiting at other branches — open Visits or switch branch."
|
||||
);
|
||||
}
|
||||
|
||||
return back()->with('info', 'No patients waiting in this specialty queue.');
|
||||
}
|
||||
|
||||
return back()->with('success', 'Called '.$ticket['ticket_number'].'.');
|
||||
$name = $ticket['customer_name'] ?? 'patient';
|
||||
|
||||
return back()->with('success', 'Called '.$ticket['ticket_number'].' — '.$name.'.');
|
||||
}
|
||||
|
||||
protected function renderShell(
|
||||
@@ -341,22 +381,6 @@ class SpecialtyModuleController extends Controller
|
||||
|
||||
$departmentIds = $departments->pluck('id')->all();
|
||||
|
||||
$waiting = Appointment::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN])
|
||||
->when($departmentIds !== [], fn ($q) => $q->whereIn('department_id', $departmentIds))
|
||||
->when($departmentIds === [], fn ($q) => $q->whereRaw('1 = 0'))
|
||||
->when($branchScope, fn ($q) => $q->where('branch_id', $branchScope))
|
||||
->when(
|
||||
$practitionerScope !== null,
|
||||
fn ($q) => $q->whereIn('practitioner_id', $practitionerScope ?: [0]),
|
||||
)
|
||||
->with(['patient', 'practitioner', 'department', 'visit'])
|
||||
->orderBy('queue_position')
|
||||
->orderBy('checked_in_at')
|
||||
->limit(25)
|
||||
->get();
|
||||
|
||||
$branchId = (int) ($branchScope ?: $departments->first()?->branch_id);
|
||||
if ($practitionerScope !== null && $practitionerScope !== []) {
|
||||
$pracBranch = (int) \App\Models\Practitioner::owned($owner)
|
||||
@@ -367,11 +391,32 @@ class SpecialtyModuleController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
// Match Call next: when the member has no branch scope (e.g. hospital admin),
|
||||
// KPIs / waiting lists use the same branch as queue ops so WAITING isn't org-wide
|
||||
// while Call next is local.
|
||||
$kpiBranch = $branchScope ?? ($branchId > 0 ? $branchId : null);
|
||||
|
||||
$waiting = Appointment::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN])
|
||||
->when($departmentIds !== [], fn ($q) => $q->whereIn('department_id', $departmentIds))
|
||||
->when($departmentIds === [], fn ($q) => $q->whereRaw('1 = 0'))
|
||||
->when($kpiBranch, fn ($q) => $q->where('branch_id', $kpiBranch))
|
||||
->when(
|
||||
$practitionerScope !== null,
|
||||
fn ($q) => $q->whereIn('practitioner_id', $practitionerScope ?: [0]),
|
||||
)
|
||||
->with(['patient', 'practitioner', 'department', 'visit'])
|
||||
->orderBy('queue_position')
|
||||
->orderBy('checked_in_at')
|
||||
->limit(25)
|
||||
->get();
|
||||
|
||||
$shellDefinition = $shell->definition($module);
|
||||
$kpis = $shell->kpis($organization, $module, $owner, $branchScope, $practitionerScope);
|
||||
$openVisits = $shell->openVisits($organization, $module, $owner, $branchScope, $practitionerScope);
|
||||
$kpis = $shell->kpis($organization, $module, $owner, $kpiBranch, $practitionerScope);
|
||||
$openVisits = $shell->openVisits($organization, $module, $owner, $kpiBranch, $practitionerScope);
|
||||
$visitHistory = $section === 'history'
|
||||
? $shell->visitHistory($organization, $module, $owner, $branchScope, $practitionerScope)
|
||||
? $shell->visitHistory($organization, $module, $owner, $kpiBranch, $practitionerScope)
|
||||
: collect();
|
||||
$services = $shell->provisionedServices($organization, $module);
|
||||
|
||||
@@ -472,6 +517,7 @@ class SpecialtyModuleController extends Controller
|
||||
'visitDocuments' => $visitDocuments,
|
||||
'queueStubs' => $modules->queueStubsFor($organization, $module),
|
||||
'branchId' => $branchId,
|
||||
'practitionerScope' => $practitionerScope,
|
||||
'queueIntegration' => $branchId
|
||||
? $queueBridge->listMeta($organization, $module, $branchId)
|
||||
: ['enabled' => false],
|
||||
|
||||
@@ -346,18 +346,72 @@ class CareQueueBridge
|
||||
$ticket = $this->attemptCallNext($organization, $callResources);
|
||||
}
|
||||
|
||||
// Admins / reception without a desk: try every service point on this queue so
|
||||
// Call next matches the specialty waiting list instead of one empty desk.
|
||||
if (! $ticket && ! $practitionerId && (
|
||||
CareQueueContexts::isSpecialty($context)
|
||||
|| $context === CareQueueContexts::CONSULTATION
|
||||
)) {
|
||||
$ticket = $this->attemptCallNextAcrossPoints($organization, $resources, $point);
|
||||
}
|
||||
|
||||
if (! $ticket) {
|
||||
return ['ticket' => null, 'entity' => null, 'resources' => $resources];
|
||||
}
|
||||
|
||||
$entity = $this->findEntityByTicket($organization, $context, (string) ($ticket['uuid'] ?? ''));
|
||||
if ($entity) {
|
||||
$this->applyTicket($entity, $ticket, $point);
|
||||
$matchedPoint = $point;
|
||||
$ticketPointUuid = (string) data_get($ticket, 'assigned_counter.uuid', data_get($ticket, 'counter.uuid', ''));
|
||||
if ($ticketPointUuid !== '') {
|
||||
$matchedPoint = collect($resources['points'] ?? [])->first(
|
||||
fn ($p) => ($p['counter_uuid'] ?? null) === $ticketPointUuid
|
||||
) ?? $point;
|
||||
}
|
||||
$this->applyTicket($entity, $ticket, $matchedPoint);
|
||||
}
|
||||
|
||||
return ['ticket' => $ticket, 'entity' => $entity?->fresh(), 'resources' => $resources];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{queue_uuid?: ?string, counter_uuid?: ?string, points?: list<array<string, mixed>>} $resources
|
||||
* @param array<string, mixed> $preferredPoint
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
protected function attemptCallNextAcrossPoints(
|
||||
Organization $organization,
|
||||
array $resources,
|
||||
array $preferredPoint,
|
||||
): ?array {
|
||||
$queueUuid = (string) ($resources['queue_uuid'] ?? '');
|
||||
if ($queueUuid === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$preferredUuid = (string) ($preferredPoint['counter_uuid'] ?? '');
|
||||
$points = collect($resources['points'] ?? [])
|
||||
->filter(fn ($p) => ! empty($p['counter_uuid']))
|
||||
->sortBy(fn ($p) => ($p['counter_uuid'] ?? '') === $preferredUuid ? 0 : 1)
|
||||
->values();
|
||||
|
||||
foreach ($points as $candidate) {
|
||||
$uuid = (string) $candidate['counter_uuid'];
|
||||
if ($uuid === $preferredUuid) {
|
||||
continue;
|
||||
}
|
||||
$ticket = $this->attemptCallNext($organization, [
|
||||
'queue_uuid' => $queueUuid,
|
||||
'counter_uuid' => $uuid,
|
||||
]);
|
||||
if ($ticket) {
|
||||
return $ticket;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{queue_uuid?: ?string, counter_uuid?: ?string} $resources
|
||||
* @return array<string, mixed>|null
|
||||
|
||||
Reference in New Issue
Block a user