Reserve floor queues for clinical staff and deepen specialty forms.
Deploy Ladill Care / deploy (push) Successful in 41s

Hospital admins no longer see specialty/queue floor nav; Call next is a full-width button; specialty encounters open module-specific clinical forms instead of the general consultation page.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-18 12:04:05 +00:00
co-authored by Cursor
parent bd8218fb45
commit 612e021e85
18 changed files with 1145 additions and 183 deletions
@@ -1,34 +1,20 @@
{{-- Inline Queue ops when Care↔Queue integration is enabled (not a bolted-on Service counter). --}}
{{-- Inline Queue ops when Care Queue Engine is enabled. --}}
@php
$qi = $queueIntegration ?? null;
$callNextRoute = $queueCallNextRoute ?? null;
$callNextParams = $queueCallNextParams ?? [];
@endphp
@if (! empty($qi['enabled']) && $callNextRoute)
<div class="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-sky-100 bg-sky-50/70 px-4 py-3">
<div>
<p class="text-xs font-semibold uppercase tracking-wide text-sky-700">Queue</p>
<p class="text-sm text-slate-600">
@if (($qi['routing_mode'] ?? null) === 'shared_pool')
Call next takes the next waiting patient in line for this queue (shared pool).
@elseif (! empty($queueCallNextScansAllPoints))
Call next picks the next waiting patient across service desks for this specialty at this branch.
@else
Call next operates on your assigned service point only tickets routed to other rooms or desks stay there.
@endif
</p>
</div>
<form method="POST" action="{{ route($callNextRoute, $callNextParams) }}">
@csrf
@foreach ($callNextParams as $key => $value)
@if (is_scalar($value))
<input type="hidden" name="{{ $key }}" value="{{ $value }}">
@endif
@endforeach
@if (! empty($branchId))
<input type="hidden" name="branch_id" value="{{ $branchId }}">
<form method="POST" action="{{ route($callNextRoute, $callNextParams) }}" class="w-full">
@csrf
@foreach ($callNextParams as $key => $value)
@if (is_scalar($value))
<input type="hidden" name="{{ $key }}" value="{{ $value }}">
@endif
<button type="submit" class="btn-primary text-sm">Call next</button>
</form>
</div>
@endforeach
@if (! empty($branchId))
<input type="hidden" name="branch_id" value="{{ $branchId }}">
@endif
<button type="submit" class="btn-primary flex w-full items-center justify-center text-sm">Call next</button>
</form>
@endif
@@ -72,7 +72,6 @@
'queueCallNextRoute' => 'care.specialty.call-next',
'queueCallNextParams' => ['module' => $moduleKey],
'branchId' => $branchId ?? null,
'queueCallNextScansAllPoints' => ($practitionerScope ?? null) === null,
])
@if ($workspaceVisit)
@@ -88,17 +87,21 @@
$appointment->status === \App\Models\Appointment::STATUS_IN_CONSULTATION
|| ($openConsultation && $openConsultation->status !== \App\Models\Consultation::STATUS_COMPLETED)
);
$chartTab = collect($workspaceTabs ?? [])
->keys()
->first(fn ($key) => ! in_array($key, ['overview', 'orders', 'billing', 'documents'], true))
?: 'clinical_notes';
@endphp
@if ($consultationInProgress && $openConsultation)
<a href="{{ route('care.consultations.show', $openConsultation) }}"
@if ($consultationInProgress)
<a href="{{ route('care.specialty.workspace', ['module' => $moduleKey, 'visit' => $workspaceVisit, 'tab' => $chartTab]) }}"
class="flex w-full items-center justify-center rounded-xl bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-700">
Open consultation
Open specialty chart
</a>
@elseif ($canStartConsultation)
<form method="POST" action="{{ route('care.specialty.consultation.start', ['module' => $moduleKey, 'visit' => $workspaceVisit]) }}">
@csrf
<button type="submit" class="flex w-full items-center justify-center rounded-xl bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-700">
{{ $actions['start'] ?? 'Start consultation' }}
{{ $actions['start'] ?? 'Start specialty encounter' }}
</button>
</form>
@endif
+41 -37
View File
@@ -26,8 +26,10 @@
if ($permissions->can($member, 'appointments.view')) {
$nav[] = ['name' => 'Appointments', 'route' => route('care.appointments.index'), 'active' => request()->routeIs('care.appointments.*'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" />'];
$nav[] = ['name' => 'Queue', 'route' => route('care.queue.index'), 'active' => request()->routeIs('care.queue.*') || request()->routeIs('care.consultations.*'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.25 4.5h13.5a.75.75 0 0 1 .75.75v15.75a.75.75 0 0 1-.75.75H5.25a.75.75 0 0 1-.75-.75V5.25a.75.75 0 0 1 .75-.75Z" />'];
if ($permissions->handlesFloorCare($member)) {
$nav[] = ['name' => 'Queue', 'route' => route('care.queue.index'), 'active' => request()->routeIs('care.queue.*') || request()->routeIs('care.consultations.*'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 3.75h16.5M3.75 19.5h16.5M5.25 4.5h13.5a.75.75 0 0 1 .75.75v15.75a.75.75 0 0 1-.75.75H5.25a.75.75 0 0 1-.75-.75V5.25a.75.75 0 0 1 .75-.75Z" />'];
}
}
// Clinical assessments (layered intake / pathways) — on by default; toggle in Settings.
@@ -36,7 +38,7 @@
$organization,
\App\Services\Care\CareFeatures::ASSESSMENTS_ENGINE,
);
if ($assessmentsEngineOn && $permissions->can($member, 'assessments.view') && $permissions->can($member, 'patients.view')) {
if ($assessmentsEngineOn && $permissions->can($member, 'assessments.view') && $permissions->can($member, 'patients.view') && $permissions->handlesFloorCare($member)) {
$nav[] = [
'name' => 'Clinical forms',
'route' => route('care.patients.index', ['focus' => 'assessments']),
@@ -49,12 +51,12 @@
// Lab, pharmacy, billing, and Queue service counters are Pro/Enterprise modules.
if (! empty($hasPaidPlan)) {
if ($permissions->can($member, 'lab.view')) {
if ($permissions->can($member, 'lab.view') && $permissions->handlesFloorCare($member)) {
$nav[] = ['name' => 'Laboratory', 'route' => route('care.lab.queue.index'), 'active' => request()->routeIs('care.lab.*'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714a2.25 2.25 0 0 0 .659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 0 1-1.59.659H9.06a2.25 2.25 0 0 1-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 0 1-2.25 2.25H7.25A2.25 2.25 0 0 1 5 17v-2.5" />'];
}
if ($permissions->can($member, 'prescriptions.view')) {
if ($permissions->can($member, 'prescriptions.view') && $permissions->handlesFloorCare($member)) {
$nav[] = ['name' => 'Pharmacy queue', 'route' => route('care.prescriptions.queue'), 'active' => request()->routeIs('care.prescriptions.*') && ! request()->routeIs('care.pharmacy.*'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714a2.25 2.25 0 0 0 .659 1.591L19 14.5" />'];
}
@@ -75,40 +77,42 @@
}
// Always-on Pro/Enterprise surfaces first, then other specialty modules.
$specialtyService = app(\App\Services\Care\SpecialtyModuleService::class);
if ($organization) {
$specialtyService->ensureDefaultModulesProvisioned(
$organization,
(string) $organization->owner_ref,
);
$organization->refresh();
}
$specialtyModules = $specialtyService->enabledModulesForMember($organization, $member);
$pinnedKeys = $specialtyService->defaultKeysForPaidPlans();
usort($specialtyModules, function (array $a, array $b) use ($pinnedKeys): int {
$aPin = in_array($a['key'], $pinnedKeys, true) ? 0 : 1;
$bPin = in_array($b['key'], $pinnedKeys, true) ? 0 : 1;
if ($aPin !== $bPin) {
return $aPin <=> $bPin;
}
$aOrder = array_search($a['key'], $pinnedKeys, true);
$bOrder = array_search($b['key'], $pinnedKeys, true);
if ($aOrder !== false || $bOrder !== false) {
return ($aOrder === false ? 99 : $aOrder) <=> ($bOrder === false ? 99 : $bOrder);
if ($permissions->handlesFloorCare($member)) {
$specialtyService = app(\App\Services\Care\SpecialtyModuleService::class);
if ($organization) {
$specialtyService->ensureDefaultModulesProvisioned(
$organization,
(string) $organization->owner_ref,
);
$organization->refresh();
}
$specialtyModules = $specialtyService->enabledModulesForMember($organization, $member);
$pinnedKeys = $specialtyService->defaultKeysForPaidPlans();
usort($specialtyModules, function (array $a, array $b) use ($pinnedKeys): int {
$aPin = in_array($a['key'], $pinnedKeys, true) ? 0 : 1;
$bPin = in_array($b['key'], $pinnedKeys, true) ? 0 : 1;
if ($aPin !== $bPin) {
return $aPin <=> $bPin;
}
$aOrder = array_search($a['key'], $pinnedKeys, true);
$bOrder = array_search($b['key'], $pinnedKeys, true);
if ($aOrder !== false || $bOrder !== false) {
return ($aOrder === false ? 99 : $aOrder) <=> ($bOrder === false ? 99 : $bOrder);
}
return strcasecmp(
(string) ($a['definition']['nav_label'] ?? $a['definition']['label'] ?? $a['key']),
(string) ($b['definition']['nav_label'] ?? $b['definition']['label'] ?? $b['key']),
);
});
foreach ($specialtyModules as $item) {
$nav[] = [
'name' => $item['definition']['nav_label'] ?? $item['definition']['label'],
'route' => route('care.specialty.show', $item['key']),
'active' => request()->routeIs('care.specialty.show') && request()->route('module') === $item['key'],
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />',
];
return strcasecmp(
(string) ($a['definition']['nav_label'] ?? $a['definition']['label'] ?? $a['key']),
(string) ($b['definition']['nav_label'] ?? $b['definition']['label'] ?? $b['key']),
);
});
foreach ($specialtyModules as $item) {
$nav[] = [
'name' => $item['definition']['nav_label'] ?? $item['definition']['label'],
'route' => route('care.specialty.show', $item['key']),
'active' => request()->routeIs('care.specialty.show') && request()->route('module') === $item['key'],
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />',
];
}
}
}