Give specialty modules the same Queue UI as GPs.
Deploy Ladill Care / deploy (push) Successful in 36s

Specialty homes now use the shared Waiting/In care board with Call next, Start routes specialty tickets into specialty workspaces, and redundant Dentistry/Specialty module chrome is removed.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-18 15:52:00 +00:00
co-authored by Cursor
parent dd3e42ebf3
commit ec9135c59d
15 changed files with 429 additions and 246 deletions
+35 -2
View File
@@ -163,16 +163,29 @@ class QueueController extends Controller
abort_unless($branchId > 0, 422);
$context = CareQueueContexts::CONSULTATION;
if ($practitionerId) {
$prac = Practitioner::owned($this->ownerRef($request))->find($practitionerId);
$specialtyContext = $this->queueBridge->contextForPractitioner($organization, $prac);
if ($specialtyContext !== CareQueueContexts::CONSULTATION) {
$context = $specialtyContext;
}
}
$result = $this->queueBridge->callNext(
$organization,
CareQueueContexts::CONSULTATION,
$context,
$branchId,
$member,
$practitionerId,
);
$ticket = $result['ticket'] ?? null;
if (! $ticket) {
return back()->with('info', 'No patients waiting at your consultation service point.');
$label = $context === CareQueueContexts::CONSULTATION
? 'consultation'
: $context;
return back()->with('info', "No patients waiting at your {$label} service point.");
}
return back()->with('success', 'Called '.$ticket['ticket_number'].' — '.($ticket['customer_name'] ?? 'patient').'.');
@@ -234,6 +247,26 @@ class QueueController extends Controller
$this->ownerRef($request),
);
$organization = $this->organization($request);
$appointment = $appointment->fresh(['visit', 'department']);
$specialtyContext = $this->queueBridge->contextForAppointment($organization, $appointment);
if ($specialtyContext !== CareQueueContexts::CONSULTATION && $appointment->visit) {
$clinical = app(\App\Services\Care\SpecialtyClinicalRecordService::class);
$shell = app(\App\Services\Care\SpecialtyShellService::class);
$preferredTab = collect(array_keys($shell->workspaceTabs($specialtyContext)))
->first(fn (string $tab) => $clinical->recordTypeForTab($specialtyContext, $tab) !== null)
?? 'clinical_notes';
return redirect()
->route('care.specialty.workspace', [
'module' => $specialtyContext,
'visit' => $appointment->visit,
'tab' => $preferredTab,
])
->with('success', 'Encounter started.');
}
return redirect()->route('care.consultations.show', $consultation)
->with('success', 'Consultation started.');
}
@@ -137,9 +137,19 @@ class SpecialtyModuleController extends Controller
$owner = $this->ownerRef($request);
$openConsultation = $appointment->consultation;
if ($openConsultation && $openConsultation->status !== \App\Models\Consultation::STATUS_COMPLETED) {
$clinical = app(SpecialtyClinicalRecordService::class);
$shellTabs = app(SpecialtyShellService::class)->workspaceTabs($module);
$preferredTab = collect(array_keys($shellTabs))
->first(fn (string $tab) => $clinical->recordTypeForTab($module, $tab) !== null)
?? 'clinical_notes';
return redirect()
->route('care.consultations.show', $openConsultation)
->with('success', 'Consultation already in progress.');
->route('care.specialty.workspace', [
'module' => $module,
'visit' => $visit,
'tab' => $preferredTab,
])
->with('success', 'Encounter already in progress.');
}
try {
@@ -180,7 +190,7 @@ class SpecialtyModuleController extends Controller
'visit' => $visit,
'tab' => $preferredTab,
])
->with('success', 'Specialty encounter started.');
->with('success', 'Encounter started.');
}
public function saveClinical(
@@ -342,18 +352,18 @@ class SpecialtyModuleController extends Controller
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."
"{$waitingAtBranch} patient(s) are waiting at {$branchName}, but none have a callable ticket at a service desk yet. Use Start on the queue, 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."
"No patients waiting at {$branchName}. {$waitingOrg} are waiting at other branches — switch branch or open History."
);
}
return back()->with('info', 'No patients waiting in this specialty queue.');
return back()->with('info', 'No patients waiting in this queue.');
}
$name = $ticket['customer_name'] ?? 'patient';
@@ -420,7 +430,22 @@ class SpecialtyModuleController extends Controller
->with(['patient', 'practitioner', 'department', 'visit'])
->orderBy('queue_position')
->orderBy('checked_in_at')
->limit(25)
->limit(50)
->get();
$inConsultation = Appointment::owned($owner)
->where('organization_id', $organization->id)
->where('status', Appointment::STATUS_IN_CONSULTATION)
->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', 'consultation', 'visit'])
->orderBy('started_at')
->limit(50)
->get();
$shellDefinition = $shell->definition($module);
@@ -431,6 +456,13 @@ class SpecialtyModuleController extends Controller
: collect();
$services = $shell->provisionedServices($organization, $module);
$permissions = app(\App\Services\Care\CarePermissions::class);
$canConsult = $permissions->can($member, 'consultations.manage');
$canManageQueue = $permissions->can($member, 'appointments.manage');
$branchLabel = $branchId
? (string) (\App\Models\Branch::owned($owner)->whereKey($branchId)->value('name') ?? '')
: '';
$workspaceVisit = null;
$patientHeader = null;
$timeline = [];
@@ -511,6 +543,8 @@ class SpecialtyModuleController extends Controller
'section' => $section,
'navItems' => $shell->navItems($module),
'waiting' => $waiting,
'queue' => $waiting,
'inConsultation' => $inConsultation,
'openVisits' => $openVisits,
'visitHistory' => $visitHistory,
'kpis' => $kpis,
@@ -528,7 +562,11 @@ class SpecialtyModuleController extends Controller
'visitDocuments' => $visitDocuments,
'queueStubs' => $modules->queueStubsFor($organization, $module),
'branchId' => $branchId,
'branchLabel' => $branchLabel,
'practitionerScope' => $practitionerScope,
'lockToPractitioner' => $practitionerScope !== null,
'canConsult' => $canConsult,
'canManageQueue' => $canManageQueue,
'queueIntegration' => $branchId
? $queueBridge->listMeta($organization, $module, $branchId)
: ['enabled' => false],
+18
View File
@@ -40,6 +40,15 @@ class CareQueueBridge
return CareQueueContexts::CONSULTATION;
}
return $this->contextForDepartment($organization, (int) $departmentId);
}
public function contextForDepartment(Organization $organization, int $departmentId): string
{
if ($departmentId <= 0) {
return CareQueueContexts::CONSULTATION;
}
foreach ($this->specialties->enabledKeys($organization) as $key) {
$deptIds = data_get($organization->settings, "specialty_module_provisioning.{$key}.department_ids", []);
if (is_array($deptIds) && in_array($departmentId, array_map('intval', $deptIds), true)) {
@@ -50,6 +59,15 @@ class CareQueueBridge
return CareQueueContexts::CONSULTATION;
}
public function contextForPractitioner(Organization $organization, ?Practitioner $practitioner): string
{
if (! $practitioner?->department_id) {
return CareQueueContexts::CONSULTATION;
}
return $this->contextForDepartment($organization, (int) $practitioner->department_id);
}
public function issueForAppointment(Organization $organization, Appointment $appointment): ?Appointment
{
if (! $this->isEnabled($organization) || filled($appointment->queue_ticket_uuid)) {
+1 -1
View File
@@ -302,7 +302,7 @@ class CareQueueProvisioner
$points = [];
$roomIndex = 1;
$roomPrefix = CareQueueContexts::isSpecialty($context)
? (string) (config("care.specialty_modules.{$context}.label") ?? 'Room')
? 'Bay'
: 'Consultation Room';
foreach ($practitioners as $practitioner) {
+1 -1
View File
@@ -878,7 +878,7 @@ class DemoTenantSeeder
'member_id' => $assignMember ? $member->id : null,
'name' => $practitionerName,
'specialty' => (string) ($definition['label'] ?? $key),
'room' => ($definition['label'] ?? 'Specialty').' Bay',
'room' => 'Bay '.(($branchIndex % 4) + 1),
'is_active' => true,
'deleted_at' => null,
],
+1 -3
View File
@@ -463,12 +463,10 @@ class SpecialtyShellService
public function navItems(string $moduleKey): array
{
// Queue home is the primary surface; history/billing are secondary links in the shell header.
return [
'overview' => ['label' => 'Overview', 'route' => 'care.specialty.show'],
'visits' => ['label' => 'Visits', 'route' => 'care.specialty.visits'],
'history' => ['label' => 'History', 'route' => 'care.specialty.history'],
'billing' => ['label' => 'Billing', 'route' => 'care.specialty.billing'],
'workspace' => ['label' => 'Workspace', 'route' => 'care.specialty.workspace'],
];
}