diff --git a/app/Http/Controllers/Care/ConsultationController.php b/app/Http/Controllers/Care/ConsultationController.php
index d32f8fc..e5c9b4c 100644
--- a/app/Http/Controllers/Care/ConsultationController.php
+++ b/app/Http/Controllers/Care/ConsultationController.php
@@ -176,7 +176,8 @@ class ConsultationController extends Controller
$queueIntegration = ['enabled' => false];
$queueBranchId = null;
$queueBridge = app(CareQueueBridge::class);
- if ($permissions->handlesFloorCare($member) && $queueBridge->isEnabled($organization)) {
+ $canAccessPatientQueue = $permissions->canAccessPatientQueue($member);
+ if ($canAccessPatientQueue && $queueBridge->isEnabled($organization)) {
$queueBranchId = app(BranchContext::class)->resolve($request, $organization, $member)
?: (int) ($consultation->practitioner?->branch_id
?: $consultation->visit?->branch_id
@@ -207,6 +208,7 @@ class ConsultationController extends Controller
'consultationAssessments' => $consultationAssessments,
'startableTemplates' => $startableTemplates,
'assessmentStatuses' => config('care.assessment_statuses'),
+ 'canAccessPatientQueue' => $canAccessPatientQueue,
'universalTemplate' => $universalTemplate,
'universalAssessment' => $universalAssessment,
'canCaptureUniversal' => $canCaptureUniversal,
diff --git a/app/Http/Controllers/Care/DashboardController.php b/app/Http/Controllers/Care/DashboardController.php
index aa6f894..e937b8e 100644
--- a/app/Http/Controllers/Care/DashboardController.php
+++ b/app/Http/Controllers/Care/DashboardController.php
@@ -41,7 +41,9 @@ class DashboardController extends Controller
$canBills = $this->permissions->can($member, 'bills.view');
$canFinance = $this->permissions->can($member, 'reports.finance.view');
$canConsult = $this->permissions->can($member, 'consultations.manage');
- $showPatientQueue = $this->permissions->can($member, 'appointments.view') && ! $canBranches;
+ $showPatientQueue = $this->permissions->canAccessPatientQueue($member)
+ && $this->permissions->can($member, 'appointments.view')
+ && ! $canBranches;
$branchQuery = Branch::owned($owner)->where('organization_id', $organization->id);
$this->scopeToBranch($request, $branchQuery);
diff --git a/app/Http/Controllers/Care/QueueController.php b/app/Http/Controllers/Care/QueueController.php
index 12e236b..603aac6 100644
--- a/app/Http/Controllers/Care/QueueController.php
+++ b/app/Http/Controllers/Care/QueueController.php
@@ -32,7 +32,7 @@ class QueueController extends Controller
public function index(Request $request): View
{
$this->authorizeAbility($request, 'appointments.view');
- abort_unless(app(CarePermissions::class)->handlesFloorCare($this->member($request)), 403);
+ abort_unless(app(CarePermissions::class)->canAccessPatientQueue($this->member($request)), 403);
$organization = $this->organization($request);
$member = $this->member($request);
$owner = $this->ownerRef($request);
@@ -126,6 +126,24 @@ class QueueController extends Controller
$todayQuery->where('branch_id', $branchId);
}
+ $doneQuery = Appointment::owned($owner)
+ ->where('organization_id', $organization->id)
+ ->where('status', Appointment::STATUS_COMPLETED)
+ ->whereDate('completed_at', today());
+ if ($lockToPractitioner) {
+ $doneQuery->whereIn('practitioner_id', $practitionerScope ?: [0]);
+ } elseif ($branchId) {
+ $doneQuery->where('branch_id', $branchId);
+ }
+ if ($practitionerId && ! $lockToPractitioner) {
+ $doneQuery->where('practitioner_id', $practitionerId);
+ }
+ $done = $doneQuery
+ ->with(['patient', 'practitioner'])
+ ->orderByDesc('completed_at')
+ ->limit(20)
+ ->get();
+
$heroStats = [
'waiting' => $queue->count(),
'in_consultation' => $inConsultation->count(),
@@ -149,6 +167,7 @@ class QueueController extends Controller
'practitionerId' => $practitionerId,
'queue' => $queue,
'inConsultation' => $inConsultation,
+ 'done' => $done,
'canManageQueue' => $canManageQueue,
'canConsult' => $canConsult,
'heroStats' => $heroStats,
@@ -160,7 +179,7 @@ class QueueController extends Controller
public function callNext(Request $request, CareQueueSessionAdvance $advance): RedirectResponse
{
- abort_unless(app(CarePermissions::class)->handlesFloorCare($this->member($request)), 403);
+ abort_unless(app(CarePermissions::class)->canAccessPatientQueue($this->member($request)), 403);
$this->authorizeAbility($request, 'appointments.view');
$organization = $this->organization($request);
abort_unless($this->queueBridge->isEnabled($organization), 404);
diff --git a/app/Http/Controllers/Care/SpecialtyModuleController.php b/app/Http/Controllers/Care/SpecialtyModuleController.php
index 81fee9d..d8e7ba4 100644
--- a/app/Http/Controllers/Care/SpecialtyModuleController.php
+++ b/app/Http/Controllers/Care/SpecialtyModuleController.php
@@ -39,8 +39,12 @@ class SpecialtyModuleController extends Controller
403,
);
- // Specialty patient flow lives on Queue; clinical depth opens from Queue Start.
- return redirect()->route('care.queue.index');
+ // Specialty patient flow lives on Queue; nurses stay on the specialty workspace.
+ if (app(\App\Services\Care\CarePermissions::class)->canAccessPatientQueue($this->member($request))) {
+ return redirect()->route('care.queue.index');
+ }
+
+ return redirect()->route('care.specialty.workspace', $module);
}
public function visits(
@@ -57,7 +61,11 @@ class SpecialtyModuleController extends Controller
403,
);
- return redirect()->route('care.queue.index');
+ if (app(\App\Services\Care\CarePermissions::class)->canAccessPatientQueue($this->member($request))) {
+ return redirect()->route('care.queue.index');
+ }
+
+ return redirect()->route('care.specialty.workspace', $module);
}
public function history(
@@ -543,7 +551,14 @@ class SpecialtyModuleController extends Controller
);
return redirect()
- ->route('care.queue.index')
+ ->route(
+ app(\App\Services\Care\CarePermissions::class)->canAccessPatientQueue($member)
+ ? 'care.queue.index'
+ : 'care.specialty.workspace',
+ app(\App\Services\Care\CarePermissions::class)->canAccessPatientQueue($member)
+ ? []
+ : ['module' => $module],
+ )
->with('success', 'Referred '.$patient->fullName().' to '.($definition['label'] ?? $module).'.');
}
@@ -650,6 +665,7 @@ class SpecialtyModuleController extends Controller
|| $permissions->can($member, 'consultations.manage')
);
$canManageQueue = $permissions->can($member, 'appointments.manage') && $canManageSpecialty;
+ $canAccessPatientQueue = $permissions->canAccessPatientQueue($member);
$canBookAppointments = $permissions->can($member, 'appointments.manage');
$canViewPatients = $permissions->can($member, 'patients.view');
$canViewAppointments = $permissions->can($member, 'appointments.view');
@@ -657,6 +673,22 @@ class SpecialtyModuleController extends Controller
? (string) (\App\Models\Branch::owned($owner)->whereKey($branchId)->value('name') ?? '')
: '';
+ $done = Appointment::owned($owner)
+ ->where('organization_id', $organization->id)
+ ->where('status', Appointment::STATUS_COMPLETED)
+ ->whereDate('completed_at', today())
+ ->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'])
+ ->orderByDesc('completed_at')
+ ->limit(20)
+ ->get();
+
$workspaceVisit = null;
$patientHeader = null;
$timeline = [];
@@ -843,6 +875,7 @@ class SpecialtyModuleController extends Controller
'waiting' => $waiting,
'queue' => $waiting,
'inConsultation' => $inConsultation,
+ 'done' => $done,
'openVisits' => $openVisits,
'visitHistory' => $visitHistory,
'kpis' => $kpis,
@@ -900,6 +933,7 @@ class SpecialtyModuleController extends Controller
'canManageClinical' => $canManageClinical,
'canManageVitals' => $canManageVitals,
'canManageQueue' => $canManageQueue,
+ 'canAccessPatientQueue' => $canAccessPatientQueue,
'canBookAppointments' => $canBookAppointments,
'canViewPatients' => $canViewPatients,
'canViewAppointments' => $canViewAppointments,
diff --git a/app/Services/Care/CarePermissions.php b/app/Services/Care/CarePermissions.php
index 2119997..d6994f8 100644
--- a/app/Services/Care/CarePermissions.php
+++ b/app/Services/Care/CarePermissions.php
@@ -34,7 +34,7 @@ class CarePermissions
],
'nurse' => [
'dashboard.view', 'patients.view', 'appointments.view',
- 'consultations.view', 'vitals.manage', 'queue.manage',
+ 'consultations.view', 'vitals.manage',
'service_queues.console',
'assessments.view', 'assessments.capture',
],
@@ -109,4 +109,17 @@ class CarePermissions
'cashier',
], true);
}
+
+ /**
+ * Dedicated patient-flow board (GP / specialty Queue homes).
+ * Nurses keep specialty workspaces and vitals — not the call-next board.
+ */
+ public function canAccessPatientQueue(?Member $member): bool
+ {
+ if (! $this->handlesFloorCare($member)) {
+ return false;
+ }
+
+ return $member->role !== 'nurse';
+ }
}
diff --git a/resources/views/care/bills/index.blade.php b/resources/views/care/bills/index.blade.php
index bf331c2..c553a72 100644
--- a/resources/views/care/bills/index.blade.php
+++ b/resources/views/care/bills/index.blade.php
@@ -30,12 +30,13 @@
- @include('care.partials.queue-ops', [
- 'queueIntegration' => $queueIntegration ?? null,
- 'queueCallNextRoute' => 'care.bills.call-next',
- 'queueCallNextParams' => [],
- 'branchId' => $branchId ?? null,
- ])
+ @include('care.partials.queue-ops', [
+ 'queueIntegration' => $queueIntegration ?? null,
+ 'queueCallNextRoute' => 'care.bills.call-next',
+ 'queueCallNextParams' => [],
+ 'branchId' => $branchId ?? null,
+ 'variant' => 'bar',
+ ])
diff --git a/resources/views/care/consultations/partials/actions-menu.blade.php b/resources/views/care/consultations/partials/actions-menu.blade.php
index 44d80ef..58f1b8a 100644
--- a/resources/views/care/consultations/partials/actions-menu.blade.php
+++ b/resources/views/care/consultations/partials/actions-menu.blade.php
@@ -15,6 +15,7 @@
'queueCallNextParams' => [],
'branchId' => $queueBranchId ?? null,
'currentAppointmentId' => $appointment?->id,
+ 'variant' => 'button',
])
@if ($ticketCalled && $appointment)
@@ -71,4 +72,6 @@
@endif
+@if (! empty($canAccessPatientQueue))
Back to queue
+@endif
diff --git a/resources/views/care/financial-obligations/index.blade.php b/resources/views/care/financial-obligations/index.blade.php
index f67c0dc..0ad2bf9 100644
--- a/resources/views/care/financial-obligations/index.blade.php
+++ b/resources/views/care/financial-obligations/index.blade.php
@@ -13,6 +13,7 @@
'queueCallNextRoute' => 'care.bills.call-next',
'queueCallNextParams' => [],
'branchId' => $branchId ?? null,
+ 'variant' => 'button',
])
@endif
diff --git a/resources/views/care/lab/queue/index.blade.php b/resources/views/care/lab/queue/index.blade.php
index 79f68d7..84a3937 100644
--- a/resources/views/care/lab/queue/index.blade.php
+++ b/resources/views/care/lab/queue/index.blade.php
@@ -35,12 +35,13 @@
- @include('care.partials.queue-ops', [
- 'queueIntegration' => $queueIntegration ?? null,
- 'queueCallNextRoute' => 'care.lab.queue.call-next',
- 'queueCallNextParams' => [],
- 'branchId' => $branchId,
- ])
+ @include('care.partials.queue-ops', [
+ 'queueIntegration' => $queueIntegration ?? null,
+ 'queueCallNextRoute' => 'care.lab.queue.call-next',
+ 'queueCallNextParams' => [],
+ 'branchId' => $branchId,
+ 'variant' => 'bar',
+ ])
diff --git a/resources/views/care/partials/queue-board-card.blade.php b/resources/views/care/partials/queue-board-card.blade.php
new file mode 100644
index 0000000..6000033
--- /dev/null
+++ b/resources/views/care/partials/queue-board-card.blade.php
@@ -0,0 +1,86 @@
+{{--
+ Single patient-flow card for queue-board stages.
+
+ Expected: $appointment, $stage (waiting|called|in_care|done),
+ $canConsult, $queueIntegration, $startRouteName, $openInCareUrl (callable)
+--}}
+@php
+ $stage = $stage ?? 'waiting';
+ $ticketStatus = $appointment->queue_ticket_status ?? null;
+ $isCalled = in_array($ticketStatus, ['called', 'serving'], true);
+ $openUrl = isset($openInCareUrl) ? $openInCareUrl($appointment) : null;
+@endphp
+
$stage === 'called' || ($stage === 'waiting' && $isCalled),
+ 'border-emerald-200 bg-emerald-50/60' => $stage === 'in_care',
+ 'border-slate-100 bg-slate-50/80' => $stage === 'waiting' && ! $isCalled,
+ 'border-slate-100 bg-white opacity-90' => $stage === 'done',
+])>
+
+ @if (! empty($queueIntegration['enabled']) && $appointment->queue_ticket_number)
+ @include('care.partials.queue-ticket', [
+ 'ticketNumber' => $appointment->queue_ticket_number,
+ 'ticketStatus' => $ticketStatus,
+ 'destination' => $appointment->queue_destination,
+ 'staffDisplayName' => $appointment->queue_staff_display_name,
+ 'showAssignment' => $stage !== 'in_care' && $stage !== 'done',
+ 'prominent' => true,
+ ])
+ @elseif (! empty($queueIntegration['enabled']) && ($appointment->queue_routing_status ?? '') === 'unresolved')
+
Unassigned
+ @elseif ($appointment->queue_position)
+
{{ $appointment->queue_position }}
+ @endif
+
+
+
{{ $appointment->patient?->fullName() ?? 'Patient' }}
+
+ @if ($stage === 'in_care' || $stage === 'done')
+ {{ $appointment->practitioner?->name ?? 'Unassigned' }}
+ @if ($appointment->patient?->patient_number)
+ · {{ $appointment->patient->patient_number }}
+ @endif
+ @else
+ {{ $appointment->patient?->patient_number ?? '—' }}
+ @if ($appointment->reason)
+ · {{ $appointment->reason }}
+ @endif
+ @endif
+
+
+
+
+
+ @if ($stage === 'waiting' || $stage === 'called')
+ @if (! empty($queueIntegration['enabled']) && $appointment->queue_ticket_uuid && $isCalled && $canConsult)
+
+ @endif
+ @if ($canConsult)
+
+ @endif
+ @elseif ($stage === 'in_care')
+ @if (! empty($queueIntegration['enabled']) && $appointment->queue_ticket_uuid && $isCalled && $canConsult)
+
+ @endif
+ @if ($openUrl)
+
Open
+ @endif
+ @endif
+
+
diff --git a/resources/views/care/partials/queue-board.blade.php b/resources/views/care/partials/queue-board.blade.php
index ae9ec61..8a9920b 100644
--- a/resources/views/care/partials/queue-board.blade.php
+++ b/resources/views/care/partials/queue-board.blade.php
@@ -1,21 +1,22 @@
{{--
- Shared Waiting | In care board (GP Queue + specialty Queue homes).
+ Shared patient-flow board (GP Queue + specialty Queue homes).
Expected vars:
- $queue, $inConsultation (collections of Appointment)
+ - $done (optional — completed today)
- $queueIntegration, $branchId
- $canConsult (bool)
- $queueCallNextRoute, $queueCallNextParams (optional — if set, Call next renders above board)
- $startRouteName (default care.queue.start) — route(name, $appointment)
- $openInCareUrl (callable Appointment $a): ?string — link for in-care Open
- - $inCareLabel (default "In consultation")
+ - $inCareLabel (default "In care")
- $lockToPractitioner, $canSwitchBranch, $branches, $practitioners, $practitionerId (filter bar)
- $showFilters (default true)
--}}
@php
$canConsult = $canConsult ?? false;
$showFilters = $showFilters ?? true;
- $inCareLabel = $inCareLabel ?? 'In consultation';
+ $inCareLabel = $inCareLabel ?? 'In care';
$startRouteName = $startRouteName ?? 'care.queue.start';
$openInCareUrl = $openInCareUrl ?? function ($appointment) {
return $appointment->consultation
@@ -24,10 +25,58 @@
};
$queueCallNextRoute = $queueCallNextRoute ?? null;
$queueCallNextParams = $queueCallNextParams ?? [];
+ $done = $done ?? collect();
+
+ $calledStatuses = ['called', 'serving'];
+ $waitingList = $queue->reject(
+ fn ($a) => in_array($a->queue_ticket_status ?? '', $calledStatuses, true)
+ )->values();
+ $calledList = $queue->filter(
+ fn ($a) => in_array($a->queue_ticket_status ?? '', $calledStatuses, true)
+ )->values();
+
+ $stages = [
+ [
+ 'key' => 'waiting',
+ 'label' => 'Waiting',
+ 'count' => $waitingList->count(),
+ 'items' => $waitingList,
+ 'empty' => 'No patients waiting.',
+ 'accent' => 'text-slate-500',
+ 'dot' => 'bg-slate-400',
+ ],
+ [
+ 'key' => 'called',
+ 'label' => 'Called',
+ 'count' => $calledList->count(),
+ 'items' => $calledList,
+ 'empty' => 'No one called yet.',
+ 'accent' => 'text-indigo-600',
+ 'dot' => 'bg-indigo-500',
+ ],
+ [
+ 'key' => 'in_care',
+ 'label' => $inCareLabel,
+ 'count' => $inConsultation->count(),
+ 'items' => $inConsultation,
+ 'empty' => 'No active encounters.',
+ 'accent' => 'text-emerald-700',
+ 'dot' => 'bg-emerald-500',
+ ],
+ [
+ 'key' => 'done',
+ 'label' => 'Done',
+ 'count' => $done->count(),
+ 'items' => $done,
+ 'empty' => 'None completed today.',
+ 'accent' => 'text-slate-400',
+ 'dot' => 'bg-slate-300',
+ ],
+ ];
@endphp
@if ($showFilters)
-
@endif
-@if ($queueCallNextRoute)
- @include('care.partials.queue-ops', [
- 'queueIntegration' => $queueIntegration ?? null,
- 'queueCallNextRoute' => $queueCallNextRoute,
- 'queueCallNextParams' => $queueCallNextParams,
- 'branchId' => $branchId ?? null,
- ])
-@endif
-
-
-
- Waiting ({{ $queue->count() }})
-
- @forelse ($queue as $appointment)
-
($appointment->queue_ticket_status ?? null) === 'called',
- 'border-emerald-200 bg-emerald-50' => ($appointment->queue_ticket_status ?? null) === 'serving',
- 'border-slate-100 bg-slate-50' => ! in_array($appointment->queue_ticket_status ?? null, ['called', 'serving'], true),
- ])>
-
- @if (! empty($queueIntegration['enabled']) && $appointment->queue_ticket_number)
- @include('care.partials.queue-ticket', [
- 'ticketNumber' => $appointment->queue_ticket_number,
- 'ticketStatus' => $appointment->queue_ticket_status,
- 'destination' => $appointment->queue_destination,
- 'staffDisplayName' => $appointment->queue_staff_display_name,
- ])
- @elseif (! empty($queueIntegration['enabled']) && ($appointment->queue_routing_status ?? '') === 'unresolved')
-
Unassigned
- @elseif ($appointment->queue_position)
-
{{ $appointment->queue_position }}
- @endif
-
-
-
{{ $appointment->patient?->fullName() ?? 'Patient' }}
-
{{ $appointment->patient?->patient_number ?? '—' }} · {{ $appointment->reason ?? '—' }}
-
-
-
-
- @if (! empty($queueIntegration['enabled']) && $appointment->queue_ticket_uuid && in_array($appointment->queue_ticket_status ?? '', ['called', 'serving'], true) && $canConsult)
-
- @endif
- @if ($canConsult)
-
- @endif
-
-
- @empty
-
No patients waiting.
- @endforelse
+
+ @if ($queueCallNextRoute)
+
+ @include('care.partials.queue-ops', [
+ 'queueIntegration' => $queueIntegration ?? null,
+ 'queueCallNextRoute' => $queueCallNextRoute,
+ 'queueCallNextParams' => $queueCallNextParams,
+ 'branchId' => $branchId ?? null,
+ 'variant' => 'bar',
+ ])
+ @if (empty($queueIntegration['enabled']))
+
Start patients from the waiting column when they arrive at your desk.
+ @endif
-
+ @endif
-
- {{ $inCareLabel }} ({{ $inConsultation->count() }})
-
- @forelse ($inConsultation as $appointment)
-
-
- @if (! empty($queueIntegration['enabled']) && $appointment->queue_ticket_number)
- @include('care.partials.queue-ticket', [
- 'ticketNumber' => $appointment->queue_ticket_number,
- 'ticketStatus' => $appointment->queue_ticket_status,
- 'showAssignment' => false,
- ])
- @endif
-
-
{{ $appointment->patient?->fullName() ?? 'Patient' }}
-
{{ $appointment->practitioner?->name ?? 'Unassigned' }}
-
-
-
- @if (! empty($queueIntegration['enabled']) && $appointment->queue_ticket_uuid && in_array($appointment->queue_ticket_status ?? '', ['called', 'serving'], true) && $canConsult)
-
- @endif
- @php $openUrl = $openInCareUrl($appointment); @endphp
- @if ($openUrl)
-
Open
- @endif
+
+ @foreach ($stages as $index => $stage)
+
$index < count($stages) - 1,
+ ])>
+
+
+ @forelse ($stage['items'] as $appointment)
+ @include('care.partials.queue-board-card', [
+ 'appointment' => $appointment,
+ 'stage' => $stage['key'],
+ 'canConsult' => $canConsult,
+ 'queueIntegration' => $queueIntegration ?? null,
+ 'startRouteName' => $startRouteName,
+ 'openInCareUrl' => $openInCareUrl,
+ ])
+ @empty
+
{{ $stage['empty'] }}
+ @endforelse
- @empty
- No active encounters.
- @endforelse
-
-
+
+ @endforeach
+
diff --git a/resources/views/care/partials/queue-ops.blade.php b/resources/views/care/partials/queue-ops.blade.php
index f43ff2a..6f4f1d1 100644
--- a/resources/views/care/partials/queue-ops.blade.php
+++ b/resources/views/care/partials/queue-ops.blade.php
@@ -4,6 +4,7 @@
$callNextRoute = $queueCallNextRoute ?? null;
$callNextParams = $queueCallNextParams ?? [];
$currentAppointmentId = $currentAppointmentId ?? null;
+ $variant = $variant ?? 'bar'; // bar | button
@endphp
@if (! empty($qi['enabled']) && $callNextRoute)
@endif
diff --git a/resources/views/care/partials/queue-ticket.blade.php b/resources/views/care/partials/queue-ticket.blade.php
index 271a8da..c6bfadf 100644
--- a/resources/views/care/partials/queue-ticket.blade.php
+++ b/resources/views/care/partials/queue-ticket.blade.php
@@ -4,14 +4,19 @@
$destination = $destination ?? null;
$staffDisplayName = $staffDisplayName ?? null;
$showAssignment = ($showAssignment ?? true) && ($destination || $staffDisplayName);
+ $prominent = $prominent ?? false;
@endphp
@if ($number)
- {{ $number }}
+ $prominent,
+ 'h-7 min-w-[2.75rem] bg-sky-100 px-2 text-xs text-sky-800' => ! $prominent,
+ ])>{{ $number }}
@if ($status)
$status === 'waiting',
'bg-indigo-100 text-indigo-800' => $status === 'called',
'bg-emerald-100 text-emerald-800' => $status === 'serving',
@@ -21,7 +26,7 @@
@if ($showAssignment)
- @if ($destination){{ $destination }}@endif
+ @if ($destination){{ $destination }}@endif
@if ($destination && $staffDisplayName) · @endif
@if ($staffDisplayName){{ $staffDisplayName }}@endif
diff --git a/resources/views/care/prescriptions/queue.blade.php b/resources/views/care/prescriptions/queue.blade.php
index e273d85..7a860e1 100644
--- a/resources/views/care/prescriptions/queue.blade.php
+++ b/resources/views/care/prescriptions/queue.blade.php
@@ -16,12 +16,13 @@
@endif
- @include('care.partials.queue-ops', [
- 'queueIntegration' => $queueIntegration ?? null,
- 'queueCallNextRoute' => 'care.prescriptions.call-next',
- 'queueCallNextParams' => [],
- 'branchId' => $branchId ?? null,
- ])
+ @include('care.partials.queue-ops', [
+ 'queueIntegration' => $queueIntegration ?? null,
+ 'queueCallNextRoute' => 'care.prescriptions.call-next',
+ 'queueCallNextParams' => [],
+ 'branchId' => $branchId ?? null,
+ 'variant' => 'bar',
+ ])
diff --git a/resources/views/care/queue/index.blade.php b/resources/views/care/queue/index.blade.php
index 84e0c63..61a77b2 100644
--- a/resources/views/care/queue/index.blade.php
+++ b/resources/views/care/queue/index.blade.php
@@ -6,12 +6,12 @@
Dentistry reports
Procedures, revenue, unfinished plans, and imaging volume.
-
← Back to queue
+
← Specialty home
-
← Back to queue
+
← Specialty home
- ← Back to queue
+ ← Specialty home
@include('care.specialty.sections.billing')
@else
@@ -105,9 +109,9 @@
{{ $specialtyLabel }}
'];
- if ($permissions->handlesFloorCare($member)) {
+ if ($permissions->canAccessPatientQueue($member)) {
$nav[] = ['name' => 'Queue', 'route' => route('care.queue.index'), 'active' => request()->routeIs('care.queue.*') || request()->routeIs('care.consultations.*'),
'icon' => ''];
}
diff --git a/tests/Feature/CareAppointmentTest.php b/tests/Feature/CareAppointmentTest.php
index dec975e..33c3fc4 100644
--- a/tests/Feature/CareAppointmentTest.php
+++ b/tests/Feature/CareAppointmentTest.php
@@ -214,7 +214,7 @@ class CareAppointmentTest extends TestCase
$this->actingAs($this->user)
->get(route('care.queue.index', ['branch_id' => $this->branch->id]))
->assertOk()
- ->assertSee('Patient queue');
+ ->assertSee('Patient flow');
}
public function test_queue_repairs_duplicate_positions(): void
diff --git a/tests/Feature/CareNaturalQueueTest.php b/tests/Feature/CareNaturalQueueTest.php
index 99ec362..4162a60 100644
--- a/tests/Feature/CareNaturalQueueTest.php
+++ b/tests/Feature/CareNaturalQueueTest.php
@@ -89,7 +89,10 @@ class CareNaturalQueueTest extends TestCase
->get(route('care.queue.index'))
->assertOk()
->assertDontSee('Service counter')
- ->assertSee('Call next');
+ ->assertSee('Call next')
+ ->assertSee('Patient flow')
+ ->assertSee('Called')
+ ->assertSee('Done');
}
public function test_pharmacist_sees_call_next_on_pharmacy_page(): void
diff --git a/tests/Feature/CarePatientQueueAccessTest.php b/tests/Feature/CarePatientQueueAccessTest.php
new file mode 100644
index 0000000..c20db25
--- /dev/null
+++ b/tests/Feature/CarePatientQueueAccessTest.php
@@ -0,0 +1,146 @@
+withoutMiddleware(EnsurePlatformSession::class);
+
+ $this->owner = User::create([
+ 'public_id' => 'pq-access-owner',
+ 'name' => 'Owner',
+ 'email' => 'pq-access-owner@example.com',
+ ]);
+
+ $this->organization = Organization::create([
+ 'owner_ref' => $this->owner->public_id,
+ 'name' => 'PQ Clinic',
+ 'slug' => 'pq-clinic',
+ 'settings' => [
+ 'onboarded' => true,
+ 'facility_type' => 'clinic',
+ 'plan' => 'pro',
+ 'plan_expires_at' => now()->addMonth()->toIso8601String(),
+ 'queue_integration_enabled' => true,
+ ],
+ ]);
+
+ Member::create([
+ 'owner_ref' => $this->owner->public_id,
+ 'organization_id' => $this->organization->id,
+ 'user_ref' => $this->owner->public_id,
+ 'role' => 'hospital_admin',
+ ]);
+
+ $this->branch = Branch::create([
+ 'owner_ref' => $this->owner->public_id,
+ 'organization_id' => $this->organization->id,
+ 'name' => 'Main',
+ 'is_active' => true,
+ ]);
+ }
+
+ /**
+ * @return array{0: User, 1: Member}
+ */
+ protected function makeStaff(string $role, string $suffix): array
+ {
+ $user = User::create([
+ 'public_id' => 'pq-'.$suffix,
+ 'name' => ucfirst($role).' '.$suffix,
+ 'email' => $suffix.'@pq.example.com',
+ ]);
+ $member = Member::create([
+ 'owner_ref' => $this->owner->public_id,
+ 'organization_id' => $this->organization->id,
+ 'user_ref' => $user->public_id,
+ 'role' => $role,
+ 'branch_id' => $this->branch->id,
+ ]);
+
+ return [$user, $member];
+ }
+
+ public function test_nurse_loses_queue_manage_and_cannot_open_patient_queue(): void
+ {
+ [$nurseUser, $nurseMember] = $this->makeStaff('nurse', 'nurse1');
+ $permissions = app(CarePermissions::class);
+
+ $this->assertFalse($permissions->can($nurseMember, 'queue.manage'));
+ $this->assertFalse($permissions->canAccessPatientQueue($nurseMember));
+ $this->assertTrue($permissions->handlesFloorCare($nurseMember));
+
+ $this->actingAs($nurseUser)
+ ->get(route('care.queue.index'))
+ ->assertForbidden();
+
+ $this->actingAs($nurseUser)
+ ->get(route('care.dashboard'))
+ ->assertOk()
+ ->assertDontSee('href="'.e(route('care.queue.index')).'"', false);
+ }
+
+ public function test_doctor_and_receptionist_can_open_redesigned_queue_board(): void
+ {
+ [$doctorUser, $doctorMember] = $this->makeStaff('doctor', 'doc1');
+ [$receptionistUser, $receptionistMember] = $this->makeStaff('receptionist', 'rec1');
+ $permissions = app(CarePermissions::class);
+
+ $this->assertTrue($permissions->canAccessPatientQueue($doctorMember));
+ $this->assertTrue($permissions->canAccessPatientQueue($receptionistMember));
+ $this->assertTrue($permissions->can($receptionistMember, 'queue.manage'));
+
+ $this->actingAs($doctorUser)
+ ->get(route('care.queue.index'))
+ ->assertOk()
+ ->assertSee('Patient flow')
+ ->assertSee('Waiting')
+ ->assertSee('Called')
+ ->assertSee('Done');
+
+ $this->actingAs($receptionistUser)
+ ->get(route('care.queue.index'))
+ ->assertOk()
+ ->assertSee('Patient flow')
+ ->assertSee('Call next');
+
+ $this->actingAs($doctorUser)
+ ->get(route('care.dashboard'))
+ ->assertOk()
+ ->assertSee('Queue', false);
+ }
+
+ public function test_nurse_specialty_show_redirects_to_workspace_not_queue(): void
+ {
+ [$nurseUser] = $this->makeStaff('nurse', 'nurse-spec');
+
+ app(\App\Services\Care\SpecialtyModuleService::class)
+ ->ensureDefaultModulesProvisioned($this->organization, $this->owner->public_id);
+ app(\App\Services\Care\SpecialtyModuleService::class)
+ ->activate($this->organization->fresh(), $this->owner->public_id, 'emergency');
+
+ $this->actingAs($nurseUser)
+ ->get(route('care.specialty.show', 'emergency'))
+ ->assertRedirect(route('care.specialty.workspace', 'emergency'));
+ }
+}
diff --git a/tests/Feature/CarePractitionerBranchesTest.php b/tests/Feature/CarePractitionerBranchesTest.php
index e4543d2..934fa14 100644
--- a/tests/Feature/CarePractitionerBranchesTest.php
+++ b/tests/Feature/CarePractitionerBranchesTest.php
@@ -121,7 +121,7 @@ class CarePractitionerBranchesTest extends TestCase
$this->actingAs($doctor)
->get(route('care.queue.index'))
->assertOk()
- ->assertSee('Showing patients assigned to you')
+ ->assertSee('Assigned to you')
->assertSee('name="branch_id"', false)
->assertSee('East Legon Care')
->assertSee('West Hills Care')
diff --git a/tests/Feature/CareSpecialtyModulesTest.php b/tests/Feature/CareSpecialtyModulesTest.php
index e65b84d..020182d 100644
--- a/tests/Feature/CareSpecialtyModulesTest.php
+++ b/tests/Feature/CareSpecialtyModulesTest.php
@@ -231,11 +231,11 @@ class CareSpecialtyModulesTest extends TestCase
$this->actingAs($this->owner)
->get(route('care.specialty.show', 'dentistry'))
- ->assertRedirect(route('care.queue.index'));
+ ->assertRedirect(route('care.specialty.workspace', 'dentistry'));
$this->actingAs($nurse)
->get(route('care.specialty.show', 'dentistry'))
- ->assertRedirect(route('care.queue.index'));
+ ->assertRedirect(route('care.specialty.workspace', 'dentistry'));
}
public function test_unassigned_gp_can_open_limited_specialty_module(): void
@@ -357,11 +357,11 @@ class CareSpecialtyModulesTest extends TestCase
$this->actingAs($this->owner)
->get(route('care.specialty.show', 'emergency'))
- ->assertRedirect(route('care.queue.index'));
+ ->assertRedirect(route('care.specialty.workspace', 'emergency'));
$this->actingAs($nurse)
->get(route('care.specialty.show', 'emergency'))
- ->assertRedirect(route('care.queue.index'));
+ ->assertRedirect(route('care.specialty.workspace', 'emergency'));
$this->expectException(\RuntimeException::class);
$service->deactivate($this->organization->fresh(), $this->owner->public_id, 'emergency');
diff --git a/tests/Feature/CareSpecialtyShellTest.php b/tests/Feature/CareSpecialtyShellTest.php
index ef3cde6..c9a0eb2 100644
--- a/tests/Feature/CareSpecialtyShellTest.php
+++ b/tests/Feature/CareSpecialtyShellTest.php
@@ -96,11 +96,11 @@ class CareSpecialtyShellTest extends TestCase
$this->actingAs($this->owner)
->get(route('care.specialty.show', 'emergency'))
- ->assertRedirect(route('care.queue.index'));
+ ->assertRedirect(route('care.specialty.workspace', 'emergency'));
$this->actingAs($this->owner)
->get(route('care.specialty.visits', 'emergency'))
- ->assertRedirect(route('care.queue.index'));
+ ->assertRedirect(route('care.specialty.workspace', 'emergency'));
$this->actingAs($this->owner)
->get(route('care.specialty.history', 'emergency'))
@@ -116,7 +116,8 @@ class CareSpecialtyShellTest extends TestCase
$this->actingAs($this->owner)
->get(route('care.specialty.workspace', 'emergency'))
->assertOk()
- ->assertSee('Back to queue');
+ ->assertSee('No open visit selected')
+ ->assertDontSee('Back to queue');
}
public function test_dentistry_shell_has_chair_stage_and_catalog(): void
diff --git a/tests/Feature/CareStaffTenantScopeTest.php b/tests/Feature/CareStaffTenantScopeTest.php
index 555c562..5dde303 100644
--- a/tests/Feature/CareStaffTenantScopeTest.php
+++ b/tests/Feature/CareStaffTenantScopeTest.php
@@ -143,7 +143,7 @@ class CareStaffTenantScopeTest extends TestCase
$this->actingAs($doctor)
->get(route('care.queue.index'))
->assertOk()
- ->assertSee('Showing patients assigned to you')
+ ->assertSee('Assigned to you')
->assertSee('East Legon Care')
->assertDontSee('All practitioners')
->assertDontSee('name="branch_id"', false)