From d4227970503a2ebb992abbdac9d5b5cb7fca0708 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sat, 18 Jul 2026 22:53:12 +0000 Subject: [PATCH] Gate Queue to doctors and land specialty modules on list indexes. Hide the patient-flow Queue board from nurses, reception, and other non-doctors; specialty home routes now render the queue board list instead of auto-opening the first open visit. Co-authored-by: Cursor --- .../Care/AppointmentController.php | 14 +- .../Care/ConsultationController.php | 7 +- .../Care/ServiceQueueController.php | 18 ++- .../Care/SpecialtyModuleController.php | 67 ++-------- app/Services/Care/CarePermissions.php | 12 +- app/Services/Care/OrganizationResolver.php | 19 ++- .../specialty/blood-bank/reports.blade.php | 2 +- .../specialty/dentistry/reports.blade.php | 2 +- .../specialty/emergency/reports.blade.php | 2 +- .../specialty/partials/actions-menu.blade.php | 2 +- .../care/specialty/sections/billing.blade.php | 2 +- .../specialty/sections/workspace.blade.php | 6 +- .../views/care/specialty/shell.blade.php | 4 +- resources/views/partials/sidebar.blade.php | 2 +- tests/Feature/CareAppointmentTest.php | 13 +- .../CareConsultationReturnContextTest.php | 2 +- tests/Feature/CareNaturalQueueTest.php | 13 ++ tests/Feature/CarePatientQueueAccessTest.php | 126 ++++++++++++++++-- tests/Feature/CareQueueBridgeTest.php | 55 ++++++-- tests/Feature/CareSpecialtyAccessTest.php | 2 +- tests/Feature/CareSpecialtyModulesTest.php | 18 ++- tests/Feature/CareSpecialtyShellTest.php | 11 +- 22 files changed, 277 insertions(+), 122 deletions(-) diff --git a/app/Http/Controllers/Care/AppointmentController.php b/app/Http/Controllers/Care/AppointmentController.php index 95e4e09..e698c7c 100644 --- a/app/Http/Controllers/Care/AppointmentController.php +++ b/app/Http/Controllers/Care/AppointmentController.php @@ -150,7 +150,12 @@ class AppointmentController extends Controller ->with('success', 'Walk-in checked in. Complete the current patient journey stage before Queue release.'); } - return redirect()->route('care.queue.index') + if (app(CarePermissions::class)->canAccessPatientQueue($this->member($request))) { + return redirect()->route('care.queue.index') + ->with('success', 'Walk-in added to queue.'); + } + + return redirect()->route('care.appointments.show', $appointment) ->with('success', 'Walk-in added to queue.'); } @@ -217,7 +222,12 @@ class AppointmentController extends Controller ->with('success', 'Patient checked in. Complete the current patient journey stage before Queue release.'); } - return redirect()->route('care.queue.index') + if (app(CarePermissions::class)->canAccessPatientQueue($this->member($request))) { + return redirect()->route('care.queue.index') + ->with('success', 'Patient checked in and added to queue.'); + } + + return redirect()->route('care.appointments.show', $appointment) ->with('success', 'Patient checked in and added to queue.'); } diff --git a/app/Http/Controllers/Care/ConsultationController.php b/app/Http/Controllers/Care/ConsultationController.php index e5c9b4c..b0cce94 100644 --- a/app/Http/Controllers/Care/ConsultationController.php +++ b/app/Http/Controllers/Care/ConsultationController.php @@ -278,8 +278,11 @@ class ConsultationController extends Controller ->with('success', 'Consultation completed. Continue the patient journey from the next stage.'); } - return redirect()->route('care.queue.index') - ->with('success', 'Consultation completed.'); + return redirect()->route( + app(CarePermissions::class)->canAccessPatientQueue($this->member($request)) + ? 'care.queue.index' + : 'care.appointments.index' + )->with('success', 'Consultation completed.'); } protected function authorizeConsultation(Request $request, Consultation $consultation): void diff --git a/app/Http/Controllers/Care/ServiceQueueController.php b/app/Http/Controllers/Care/ServiceQueueController.php index 9694402..10a216b 100644 --- a/app/Http/Controllers/Care/ServiceQueueController.php +++ b/app/Http/Controllers/Care/ServiceQueueController.php @@ -8,7 +8,8 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; /** - * Legacy Ladill Queue console routes — redirect to the Care patient queue. + * Legacy Ladill Queue console routes — redirect to the Care patient queue (doctors) + * or appointments list when the actor cannot open the patient-flow board. */ class ServiceQueueController extends Controller { @@ -18,20 +19,29 @@ class ServiceQueueController extends Controller { $this->authorizeAbility($request, 'service_queues.console'); - return redirect()->route('care.queue.index'); + return $this->redirectAwayFromLegacyConsole($request); } public function console(Request $request, string $counterUuid): RedirectResponse { $this->authorizeAbility($request, 'service_queues.console'); - return redirect()->route('care.queue.index'); + return $this->redirectAwayFromLegacyConsole($request); } public function action(Request $request, string $counterUuid): RedirectResponse { $this->authorizeAbility($request, 'service_queues.console'); - return redirect()->route('care.queue.index'); + return $this->redirectAwayFromLegacyConsole($request); + } + + protected function redirectAwayFromLegacyConsole(Request $request): RedirectResponse + { + if (app(\App\Services\Care\CarePermissions::class)->canAccessPatientQueue($this->member($request))) { + return redirect()->route('care.queue.index'); + } + + return redirect()->route('care.appointments.index'); } } diff --git a/app/Http/Controllers/Care/SpecialtyModuleController.php b/app/Http/Controllers/Care/SpecialtyModuleController.php index 9ced2a4..f2b53b4 100644 --- a/app/Http/Controllers/Care/SpecialtyModuleController.php +++ b/app/Http/Controllers/Care/SpecialtyModuleController.php @@ -31,20 +31,9 @@ class SpecialtyModuleController extends Controller SpecialtyModuleService $modules, SpecialtyShellService $shell, CareQueueBridge $queueBridge, - ): RedirectResponse { - $definition = $modules->definition($module); - abort_unless($definition, 404); - abort_unless( - $modules->memberCanAccess($this->organization($request), $this->member($request), $module), - 403, - ); - - // 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); + ): View { + // Everyone lands on the specialty list/index — never auto-open a patient. + return $this->renderShell($request, $module, 'visits', $modules, $shell, $queueBridge); } public function visits( @@ -53,19 +42,8 @@ class SpecialtyModuleController extends Controller SpecialtyModuleService $modules, SpecialtyShellService $shell, CareQueueBridge $queueBridge, - ): RedirectResponse { - $definition = $modules->definition($module); - abort_unless($definition, 404); - abort_unless( - $modules->memberCanAccess($this->organization($request), $this->member($request), $module), - 403, - ); - - if (app(\App\Services\Care\CarePermissions::class)->canAccessPatientQueue($this->member($request))) { - return redirect()->route('care.queue.index'); - } - - return redirect()->route('care.specialty.workspace', $module); + ): View { + return $this->renderShell($request, $module, 'visits', $modules, $shell, $queueBridge); } public function history( @@ -95,7 +73,12 @@ class SpecialtyModuleController extends Controller SpecialtyShellService $shell, CareQueueBridge $queueBridge, ?Visit $visit = null, - ): View { + ): View|RedirectResponse { + // Bare /workspace (no visit) is the old auto-open entry — send users to the list. + if ($visit === null) { + return redirect()->route('care.specialty.show', $module); + } + return $this->renderShell($request, $module, 'workspace', $modules, $shell, $queueBridge, $visit); } @@ -589,14 +572,7 @@ class SpecialtyModuleController extends Controller ); return redirect() - ->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], - ) + ->route('care.specialty.show', $module) ->with('success', 'Referred '.$patient->fullName().' to '.($definition['label'] ?? $module).'.'); } @@ -792,25 +768,6 @@ class SpecialtyModuleController extends Controller ); $timeline = $shell->patientTimeline($visit->patient); } - } elseif ($section === 'workspace') { - $first = $openVisits->first(); - if ($first?->patient) { - $first->load([ - 'patient.allergies', - 'patient.emergencyContacts', - 'patient.attachments', - 'appointment.practitioner', - 'appointment.consultation.investigationRequests.investigationType', - 'bill.lineItems', - 'consultations.investigationRequests.investigationType', - ]); - $workspaceVisit = $first; - $patientHeader = array_merge( - ['patient' => $first->patient, 'visit' => $first, 'appointment' => $first->appointment], - $shell->patientHeaderMeta($first->patient), - ); - $timeline = $shell->patientTimeline($first->patient); - } } if ($workspaceVisit && $section === 'workspace') { diff --git a/app/Services/Care/CarePermissions.php b/app/Services/Care/CarePermissions.php index d6994f8..f911f55 100644 --- a/app/Services/Care/CarePermissions.php +++ b/app/Services/Care/CarePermissions.php @@ -111,15 +111,13 @@ class CarePermissions } /** - * Dedicated patient-flow board (GP / specialty Queue homes). - * Nurses keep specialty workspaces and vitals — not the call-next board. + * Dedicated patient-flow board (GP Queue home / call-next board). + * Doctors only — nurses, reception, pharmacy, and other floor roles use + * specialty module lists and service queues instead. + * Facility admins manage settings/staff and do not staff this board. */ public function canAccessPatientQueue(?Member $member): bool { - if (! $this->handlesFloorCare($member)) { - return false; - } - - return $member->role !== 'nurse'; + return $member !== null && $member->role === 'doctor'; } } diff --git a/app/Services/Care/OrganizationResolver.php b/app/Services/Care/OrganizationResolver.php index 13ac5b6..13a47ce 100644 --- a/app/Services/Care/OrganizationResolver.php +++ b/app/Services/Care/OrganizationResolver.php @@ -284,6 +284,8 @@ class OrganizationResolver /** * Explicit branch IDs for a doctor's linked practitioner desks (pivot + legacy branch_id). + * GPs with no practitioner desk fall back to their member branch so they can still + * open the shared consultation Queue board. * * @return list */ @@ -304,7 +306,13 @@ class OrganizationResolver $ids = array_merge($ids, $practitioner->assignedBranchIds()); } - return array_values(array_unique(array_map('intval', $ids))); + $ids = array_values(array_unique(array_map('intval', $ids))); + + if ($ids === [] && $member->branch_id) { + return [(int) $member->branch_id]; + } + + return $ids; } public function mayAccessBranch(?Member $member, ?int $branchId): bool @@ -357,8 +365,9 @@ class OrganizationResolver } /** - * Practitioner IDs a doctor is locked to. Null = no practitioner lock. - * Empty array = doctor with no linked desk. + * Practitioner IDs a doctor is locked to. Null = no practitioner lock + * (non-doctors, or GPs with no linked desk — they work the shared consultation board). + * Non-empty = desk-locked specialist / assigned GP. * * @return list|null */ @@ -368,7 +377,7 @@ class OrganizationResolver return null; } - return Practitioner::owned((string) $organization->owner_ref) + $ids = Practitioner::owned((string) $organization->owner_ref) ->where('organization_id', $organization->id) ->where('is_active', true) ->where(function ($query) use ($member) { @@ -380,5 +389,7 @@ class OrganizationResolver ->map(fn ($id) => (int) $id) ->values() ->all(); + + return $ids === [] ? null : $ids; } } diff --git a/resources/views/care/specialty/blood-bank/reports.blade.php b/resources/views/care/specialty/blood-bank/reports.blade.php index 75477e5..f9c5437 100644 --- a/resources/views/care/specialty/blood-bank/reports.blade.php +++ b/resources/views/care/specialty/blood-bank/reports.blade.php @@ -5,7 +5,7 @@

Blood Bank reports

Requests, urgency mix, products issued, low stock, and bb.* revenue.

- ← Specialty home + ← Specialty home
diff --git a/resources/views/care/specialty/dentistry/reports.blade.php b/resources/views/care/specialty/dentistry/reports.blade.php index f75f926..901e4fa 100644 --- a/resources/views/care/specialty/dentistry/reports.blade.php +++ b/resources/views/care/specialty/dentistry/reports.blade.php @@ -5,7 +5,7 @@

Dentistry reports

Procedures, revenue, unfinished plans, and imaging volume.

- ← Specialty home + ← Specialty home diff --git a/resources/views/care/specialty/emergency/reports.blade.php b/resources/views/care/specialty/emergency/reports.blade.php index b595d1d..dde11a8 100644 --- a/resources/views/care/specialty/emergency/reports.blade.php +++ b/resources/views/care/specialty/emergency/reports.blade.php @@ -5,7 +5,7 @@

Emergency reports

Arrivals, acuity mix, dispositions, length of stay, and ED service revenue.

- ← Specialty home + ← Specialty home diff --git a/resources/views/care/specialty/partials/actions-menu.blade.php b/resources/views/care/specialty/partials/actions-menu.blade.php index 5bd9b46..2644be5 100644 --- a/resources/views/care/specialty/partials/actions-menu.blade.php +++ b/resources/views/care/specialty/partials/actions-menu.blade.php @@ -160,7 +160,7 @@ Back to queue @else - + Specialty home @endif diff --git a/resources/views/care/specialty/sections/billing.blade.php b/resources/views/care/specialty/sections/billing.blade.php index 9d55bb5..881bed2 100644 --- a/resources/views/care/specialty/sections/billing.blade.php +++ b/resources/views/care/specialty/sections/billing.blade.php @@ -36,6 +36,6 @@

- Add a service to a visit invoice from the specialty workspace Billing tab. + Add a service to a visit invoice from a specialty visit workspace Billing tab.

diff --git a/resources/views/care/specialty/sections/workspace.blade.php b/resources/views/care/specialty/sections/workspace.blade.php index 5a3f622..a738778 100644 --- a/resources/views/care/specialty/sections/workspace.blade.php +++ b/resources/views/care/specialty/sections/workspace.blade.php @@ -17,13 +17,15 @@

No open visit selected

@if (! empty($canAccessPatientQueue)) - Open a visit from the queue, or call next from the waiting list. + Open a visit from the specialty list or GP queue, or call next from the waiting list. @else - Open an assigned visit, or ask reception to call the next patient. + Open a patient from the specialty list to enter their workspace. @endif

@if (! empty($canAccessPatientQueue)) Back to queue + @else + Specialty home @endif @else diff --git a/resources/views/care/specialty/shell.blade.php b/resources/views/care/specialty/shell.blade.php index bd6c727..f0354f0 100644 --- a/resources/views/care/specialty/shell.blade.php +++ b/resources/views/care/specialty/shell.blade.php @@ -85,7 +85,7 @@

Completed encounters for this service line.

- ← Specialty home + ← Specialty home @include('care.specialty.sections.history') @elseif ($section === 'billing') @@ -97,7 +97,7 @@

Service catalog for this module.

- ← Specialty home + ← Specialty home @include('care.specialty.sections.billing') @else diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index 7f84716..efc9f72 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -91,7 +91,7 @@ $label = $item['definition']['nav_label'] ?? $item['definition']['label'] ?? $key; $nav[] = [ 'name' => $label, - 'route' => route('care.specialty.workspace', $key), + 'route' => route('care.specialty.show', $key), 'active' => request()->routeIs('care.specialty.*') && request()->route('module') === $key, 'icon' => $specialtyService->iconSvgPath($key), 'group' => 'specialty', diff --git a/tests/Feature/CareAppointmentTest.php b/tests/Feature/CareAppointmentTest.php index 33c3fc4..68280f5 100644 --- a/tests/Feature/CareAppointmentTest.php +++ b/tests/Feature/CareAppointmentTest.php @@ -55,6 +55,7 @@ class CareAppointmentTest extends TestCase 'organization_id' => $this->organization->id, 'user_ref' => $this->user->public_id, 'role' => 'receptionist', + 'branch_id' => null, ]); $this->branch = Branch::create([ @@ -64,6 +65,10 @@ class CareAppointmentTest extends TestCase 'is_active' => true, ]); + Member::query()->where('user_ref', $this->user->public_id)->update([ + 'branch_id' => $this->branch->id, + ]); + Department::create([ 'owner_ref' => $this->user->public_id, 'branch_id' => $this->branch->id, @@ -135,7 +140,7 @@ class CareAppointmentTest extends TestCase $this->actingAs($this->user) ->post(route('care.appointments.check-in', $appointment)) - ->assertRedirect(route('care.queue.index')); + ->assertRedirect(route('care.appointments.show', $appointment)); $appointment->refresh(); $this->assertSame(Appointment::STATUS_WAITING, $appointment->status); @@ -201,7 +206,7 @@ class CareAppointmentTest extends TestCase 'practitioner_id' => $this->practitioner->id, 'reason' => 'Urgent visit', ]) - ->assertRedirect(route('care.queue.index')); + ->assertRedirect(route('care.appointments.show', Appointment::query()->first())); $appointment = Appointment::first(); $this->assertSame(Appointment::TYPE_WALK_IN, $appointment->type); @@ -211,6 +216,8 @@ class CareAppointmentTest extends TestCase public function test_queue_page_loads(): void { + Member::query()->where('user_ref', $this->user->public_id)->update(['role' => 'doctor']); + $this->actingAs($this->user) ->get(route('care.queue.index', ['branch_id' => $this->branch->id])) ->assertOk() @@ -219,6 +226,8 @@ class CareAppointmentTest extends TestCase public function test_queue_repairs_duplicate_positions(): void { + Member::query()->where('user_ref', $this->user->public_id)->update(['role' => 'doctor']); + $second = Patient::create([ 'uuid' => (string) \Illuminate\Support\Str::uuid(), 'owner_ref' => $this->user->public_id, diff --git a/tests/Feature/CareConsultationReturnContextTest.php b/tests/Feature/CareConsultationReturnContextTest.php index 5fa59a1..620ea6d 100644 --- a/tests/Feature/CareConsultationReturnContextTest.php +++ b/tests/Feature/CareConsultationReturnContextTest.php @@ -218,7 +218,7 @@ class CareConsultationReturnContextTest extends TestCase $this->actingAs($this->user) ->withSession([ConsultationReturnContext::SESSION_KEY => $this->consultation->uuid]) ->post(route('care.consultations.complete', $this->consultation)) - ->assertRedirect(route('care.queue.index')) + ->assertRedirect(route('care.appointments.index')) ->assertSessionMissing(ConsultationReturnContext::SESSION_KEY); } } diff --git a/tests/Feature/CareNaturalQueueTest.php b/tests/Feature/CareNaturalQueueTest.php index 4162a60..d794cd1 100644 --- a/tests/Feature/CareNaturalQueueTest.php +++ b/tests/Feature/CareNaturalQueueTest.php @@ -58,6 +58,7 @@ class CareNaturalQueueTest extends TestCase 'organization_id' => $this->organization->id, 'user_ref' => $this->owner->public_id, 'role' => 'receptionist', + 'branch_id' => null, ]); $this->branch = Branch::create([ @@ -66,6 +67,10 @@ class CareNaturalQueueTest extends TestCase 'name' => 'Main', 'is_active' => true, ]); + + Member::query()->where('user_ref', $this->owner->public_id)->update([ + 'branch_id' => $this->branch->id, + ]); } public function test_sidebar_does_not_show_service_queues_nav(): void @@ -78,6 +83,8 @@ class CareNaturalQueueTest extends TestCase public function test_service_queues_index_redirects_to_clinical_queue(): void { + Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'doctor']); + $this->actingAs($this->owner) ->get(route('care.service-queues.index')) ->assertRedirect(route('care.queue.index')); @@ -85,6 +92,8 @@ class CareNaturalQueueTest extends TestCase public function test_clinical_queue_shows_inline_ops_not_service_counter(): void { + Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'doctor']); + $this->actingAs($this->owner) ->get(route('care.queue.index')) ->assertOk() @@ -119,6 +128,8 @@ class CareNaturalQueueTest extends TestCase public function test_integration_off_hides_queue_controls(): void { + Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'doctor']); + $settings = $this->organization->settings; $settings['queue_integration_enabled'] = false; $this->organization->update(['settings' => $settings]); @@ -134,6 +145,8 @@ class CareNaturalQueueTest extends TestCase { Http::fake(); + Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'doctor']); + $practitioner = Practitioner::create([ 'owner_ref' => $this->owner->public_id, 'organization_id' => $this->organization->id, diff --git a/tests/Feature/CarePatientQueueAccessTest.php b/tests/Feature/CarePatientQueueAccessTest.php index c20db25..330b252 100644 --- a/tests/Feature/CarePatientQueueAccessTest.php +++ b/tests/Feature/CarePatientQueueAccessTest.php @@ -3,11 +3,16 @@ namespace Tests\Feature; use App\Http\Middleware\EnsurePlatformSession; +use App\Models\Appointment; use App\Models\Branch; +use App\Models\Department; use App\Models\Member; use App\Models\Organization; +use App\Models\Patient; use App\Models\User; +use App\Models\Visit; use App\Services\Care\CarePermissions; +use App\Services\Care\SpecialtyModuleService; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -100,15 +105,36 @@ class CarePatientQueueAccessTest extends TestCase ->assertDontSee('href="'.e(route('care.queue.index')).'"', false); } - public function test_doctor_and_receptionist_can_open_redesigned_queue_board(): void + public function test_receptionist_and_other_non_doctors_cannot_open_patient_queue(): void + { + [$receptionistUser, $receptionistMember] = $this->makeStaff('receptionist', 'rec1'); + [$pharmacistUser, $pharmacistMember] = $this->makeStaff('pharmacist', 'rx1'); + $permissions = app(CarePermissions::class); + + $this->assertFalse($permissions->canAccessPatientQueue($receptionistMember)); + $this->assertFalse($permissions->canAccessPatientQueue($pharmacistMember)); + $this->assertTrue($permissions->can($receptionistMember, 'queue.manage')); + + $this->actingAs($receptionistUser) + ->get(route('care.queue.index')) + ->assertForbidden(); + + $this->actingAs($receptionistUser) + ->get(route('care.dashboard')) + ->assertOk() + ->assertDontSee('href="'.e(route('care.queue.index')).'"', false); + + $this->actingAs($pharmacistUser) + ->get(route('care.queue.index')) + ->assertForbidden(); + } + + public function test_doctor_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')) @@ -118,29 +144,101 @@ class CarePatientQueueAccessTest extends TestCase ->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 + public function test_hospital_admin_does_not_get_patient_queue_board(): void + { + $adminMember = Member::query() + ->where('user_ref', $this->owner->public_id) + ->firstOrFail(); + $permissions = app(CarePermissions::class); + + $this->assertFalse($permissions->canAccessPatientQueue($adminMember)); + $this->assertFalse($permissions->handlesFloorCare($adminMember)); + + $this->actingAs($this->owner) + ->get(route('care.queue.index')) + ->assertForbidden(); + } + + public function test_nurse_specialty_show_renders_list_not_auto_open_visit(): void { [$nurseUser] = $this->makeStaff('nurse', 'nurse-spec'); - app(\App\Services\Care\SpecialtyModuleService::class) + $modules = app(SpecialtyModuleService::class); + $modules->ensureDefaultModulesProvisioned($this->organization, $this->owner->public_id); + $modules->activate($this->organization->fresh(), $this->owner->public_id, 'blood_bank'); + + $department = Department::query() + ->where('branch_id', $this->branch->id) + ->where('type', 'blood_bank') + ->firstOrFail(); + + $patient = Patient::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_number' => 'P-BB-LIST', + 'first_name' => 'Kwame', + 'last_name' => 'Donor', + 'gender' => 'male', + 'date_of_birth' => '1988-01-01', + ]); + + $visit = Visit::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_id' => $patient->id, + 'status' => Visit::STATUS_OPEN, + 'checked_in_at' => now(), + ]); + + Appointment::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_id' => $patient->id, + 'department_id' => $department->id, + 'visit_id' => $visit->id, + 'type' => Appointment::TYPE_WALK_IN, + 'status' => Appointment::STATUS_WAITING, + 'scheduled_at' => now(), + 'waiting_at' => now(), + 'checked_in_at' => now(), + ]); + + $this->actingAs($nurseUser) + ->get(route('care.specialty.show', 'blood_bank')) + ->assertOk() + ->assertSee('Blood Bank') + ->assertSee('Patient flow') + ->assertSee('Waiting') + ->assertSee('Kwame Donor') + ->assertDontSee('No open visit selected'); + + $this->actingAs($nurseUser) + ->get(route('care.specialty.workspace', 'blood_bank')) + ->assertRedirect(route('care.specialty.show', 'blood_bank')); + } + + public function test_nurse_specialty_show_redirects_to_workspace_not_queue(): void + { + [$nurseUser] = $this->makeStaff('nurse', 'nurse-spec-er'); + + app(SpecialtyModuleService::class) ->ensureDefaultModulesProvisioned($this->organization, $this->owner->public_id); - app(\App\Services\Care\SpecialtyModuleService::class) + app(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')); + ->assertOk() + ->assertSee('Patient flow') + ->assertDontSee('href="'.e(route('care.queue.index')).'"', false); } } diff --git a/tests/Feature/CareQueueBridgeTest.php b/tests/Feature/CareQueueBridgeTest.php index 3fb3e13..e8bc565 100644 --- a/tests/Feature/CareQueueBridgeTest.php +++ b/tests/Feature/CareQueueBridgeTest.php @@ -78,6 +78,29 @@ class CareQueueBridgeTest extends TestCase ]); } + /** + * Patient-flow board is doctor-only; reception keeps check-in / walk-in helpers. + * + * @return array{0: User, 1: Member} + */ + protected function makeDoctor(string $suffix = 'doc'): array + { + $user = User::create([ + 'public_id' => 'bridge-'.$suffix, + 'name' => 'Bridge Doctor '.$suffix, + 'email' => $suffix.'@bridge.example.com', + ]); + $member = Member::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'user_ref' => $user->public_id, + 'role' => 'doctor', + 'branch_id' => $this->branch->id, + ]); + + return [$user, $member]; + } + public function test_check_in_issues_consultation_ticket_when_integration_on(): void { Http::fake(); @@ -164,7 +187,9 @@ class CareQueueBridgeTest extends TestCase 'queue_ticket_status' => 'waiting', ]); - $this->actingAs($this->owner) + [$doctor] = $this->makeDoctor('wait'); + + $this->actingAs($doctor) ->get(route('care.queue.index')) ->assertOk() ->assertSee('C042') @@ -198,7 +223,9 @@ class CareQueueBridgeTest extends TestCase 'queue_position' => 1, ]); - $response = $this->actingAs($this->owner) + [$doctor] = $this->makeDoctor('backfill'); + + $response = $this->actingAs($doctor) ->from(route('care.queue.index')) ->post(route('care.queue.call-next'), [ 'branch_id' => $this->branch->id, @@ -221,7 +248,9 @@ class CareQueueBridgeTest extends TestCase { Http::fake(); - $this->actingAs($this->owner) + [$doctor] = $this->makeDoctor('empty'); + + $this->actingAs($doctor) ->from(route('care.queue.index')) ->post(route('care.queue.call-next'), ['branch_id' => $this->branch->id]) ->assertRedirect(route('care.queue.index')) @@ -304,7 +333,9 @@ class CareQueueBridgeTest extends TestCase $this->assertSame('called', $appointment->fresh()->queue_ticket_status); - $this->actingAs($this->owner) + [$viewer] = $this->makeDoctor('recall-view'); + + $this->actingAs($viewer) ->get(route('care.queue.index', ['branch_id' => $this->branch->id])) ->assertOk() ->assertSee($appointment->queue_ticket_number); @@ -368,7 +399,9 @@ class CareQueueBridgeTest extends TestCase ]); // Board GET must remain read-only; ticket backfill is explicit (Call next / artisan). - $this->actingAs($this->owner) + [$doctor] = $this->makeDoctor('sync'); + + $this->actingAs($doctor) ->get(route('care.queue.index', ['branch_id' => $this->branch->id])) ->assertOk() ->assertSee('Toothache'); @@ -484,7 +517,9 @@ class CareQueueBridgeTest extends TestCase $next->fresh(['patient', 'practitioner', 'organization']), ); - $response = $this->actingAs($this->owner) + [$doctor] = $this->makeDoctor('advance'); + + $response = $this->actingAs($doctor) ->post(route('care.queue.call-next'), [ 'branch_id' => $this->branch->id, 'practitioner_id' => $practitioner->id, @@ -499,7 +534,7 @@ class CareQueueBridgeTest extends TestCase $this->assertSame('called', $next->queue_ticket_status); $response->assertRedirect(route('care.appointments.show', $next)); - $this->actingAs($this->owner) + $this->actingAs($doctor) ->get(route('care.appointments.show', $next)) ->assertOk() ->assertSee('Call again'); @@ -675,7 +710,9 @@ class CareQueueBridgeTest extends TestCase 'care_entity_uuid' => $appointment->uuid, ]); - $this->actingAs($this->owner) + [$caller] = $this->makeDoctor('reclaim'); + + $this->actingAs($caller) ->from(route('care.queue.index')) ->post(route('care.queue.call-next'), [ 'branch_id' => $this->branch->id, @@ -700,7 +737,7 @@ class CareQueueBridgeTest extends TestCase 'status' => 'pending', ]); - $this->actingAs($this->owner) + $this->actingAs($caller) ->get(route('care.queue.index', [ 'branch_id' => $this->branch->id, 'practitioner_id' => $doctor->id, diff --git a/tests/Feature/CareSpecialtyAccessTest.php b/tests/Feature/CareSpecialtyAccessTest.php index 0036e5a..a60618e 100644 --- a/tests/Feature/CareSpecialtyAccessTest.php +++ b/tests/Feature/CareSpecialtyAccessTest.php @@ -278,7 +278,7 @@ class CareSpecialtyAccessTest extends TestCase 'branch_id' => $this->branch->id, 'reason' => 'Chest pain review', ]) - ->assertRedirect(route('care.queue.index')); + ->assertRedirect(route('care.specialty.show', 'cardiology')); $this->assertDatabaseHas('care_appointments', [ 'patient_id' => $patient->id, diff --git a/tests/Feature/CareSpecialtyModulesTest.php b/tests/Feature/CareSpecialtyModulesTest.php index 961e8b6..e77b913 100644 --- a/tests/Feature/CareSpecialtyModulesTest.php +++ b/tests/Feature/CareSpecialtyModulesTest.php @@ -231,11 +231,13 @@ class CareSpecialtyModulesTest extends TestCase $this->actingAs($this->owner) ->get(route('care.specialty.show', 'dentistry')) - ->assertRedirect(route('care.specialty.workspace', 'dentistry')); + ->assertOk() + ->assertSee('Patient flow'); $this->actingAs($nurse) ->get(route('care.specialty.show', 'dentistry')) - ->assertRedirect(route('care.specialty.workspace', 'dentistry')); + ->assertOk() + ->assertSee('Patient flow'); } public function test_unassigned_gp_can_open_limited_specialty_module(): void @@ -258,7 +260,8 @@ class CareSpecialtyModulesTest extends TestCase $this->actingAs($doctor) ->get(route('care.specialty.show', 'dentistry')) - ->assertRedirect(route('care.queue.index')); + ->assertOk() + ->assertSee('Patient flow'); } public function test_assigned_specialty_doctor_can_open_module(): void @@ -293,7 +296,8 @@ class CareSpecialtyModulesTest extends TestCase $this->actingAs($doctor) ->get(route('care.specialty.show', 'dentistry')) - ->assertRedirect(route('care.queue.index')); + ->assertOk() + ->assertSee('Patient flow'); $this->assertTrue( app(SpecialtyModuleService::class)->memberCanAccess( @@ -365,11 +369,13 @@ class CareSpecialtyModulesTest extends TestCase $this->actingAs($this->owner) ->get(route('care.specialty.show', 'emergency')) - ->assertRedirect(route('care.specialty.workspace', 'emergency')); + ->assertOk() + ->assertSee('Patient flow'); $this->actingAs($nurse) ->get(route('care.specialty.show', 'emergency')) - ->assertRedirect(route('care.specialty.workspace', 'emergency')); + ->assertOk() + ->assertSee('Patient flow'); $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 c9a0eb2..aeb1477 100644 --- a/tests/Feature/CareSpecialtyShellTest.php +++ b/tests/Feature/CareSpecialtyShellTest.php @@ -96,11 +96,14 @@ class CareSpecialtyShellTest extends TestCase $this->actingAs($this->owner) ->get(route('care.specialty.show', 'emergency')) - ->assertRedirect(route('care.specialty.workspace', 'emergency')); + ->assertOk() + ->assertSee('Patient flow') + ->assertSee('Waiting'); $this->actingAs($this->owner) ->get(route('care.specialty.visits', 'emergency')) - ->assertRedirect(route('care.specialty.workspace', 'emergency')); + ->assertOk() + ->assertSee('Patient flow'); $this->actingAs($this->owner) ->get(route('care.specialty.history', 'emergency')) @@ -115,9 +118,7 @@ class CareSpecialtyShellTest extends TestCase $this->actingAs($this->owner) ->get(route('care.specialty.workspace', 'emergency')) - ->assertOk() - ->assertSee('No open visit selected') - ->assertDontSee('Back to queue'); + ->assertRedirect(route('care.specialty.show', 'emergency')); } public function test_dentistry_shell_has_chair_stage_and_catalog(): void