From 8a5330d3d26c9c03daecc1cca8ea013257a4b0c6 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sat, 18 Jul 2026 18:45:00 +0000 Subject: [PATCH] Gate specialty mutate CTAs on Care abilities, not only module manage. Nurses can manage limited specialties but lack consultations.manage, so hide Start/stage/chart edits that 403 while keeping queue/vitals/billing aligned with what those routes actually authorize. Co-authored-by: Cursor --- .../Care/EmergencyWorkspaceController.php | 8 +- .../Care/SpecialtyModuleController.php | 20 ++ .../dentistry/workspace-imaging.blade.php | 2 +- .../dentistry/workspace-lab.blade.php | 2 +- .../dentistry/workspace-notes.blade.php | 2 +- .../dentistry/workspace-odontogram.blade.php | 2 +- .../dentistry/workspace-overview.blade.php | 2 +- .../dentistry/workspace-perio.blade.php | 2 +- .../dentistry/workspace-plan.blade.php | 2 +- .../dentistry/workspace-recalls.blade.php | 2 +- .../dentistry/workspace-treat.blade.php | 2 +- .../emergency/workspace-disposition.blade.php | 2 +- .../emergency/workspace-overview.blade.php | 2 +- .../emergency/workspace-vitals.blade.php | 2 +- .../specialty/partials/actions-menu.blade.php | 40 +-- .../partials/clinical-form.blade.php | 2 +- .../specialty/sections/workspace.blade.php | 10 +- .../views/care/specialty/shell.blade.php | 14 +- tests/Feature/CareSpecialtyAccessTest.php | 228 ++++++++++++++++++ tests/Feature/CareSpecialtyShellTest.php | 4 + 20 files changed, 312 insertions(+), 38 deletions(-) diff --git a/app/Http/Controllers/Care/EmergencyWorkspaceController.php b/app/Http/Controllers/Care/EmergencyWorkspaceController.php index cac3638..67df83e 100644 --- a/app/Http/Controllers/Care/EmergencyWorkspaceController.php +++ b/app/Http/Controllers/Care/EmergencyWorkspaceController.php @@ -77,7 +77,13 @@ class EmergencyWorkspaceController extends Controller SpecialtyModuleService $modules, EmergencyVitalsService $vitals, ): RedirectResponse { - $this->authorizeAbility($request, 'consultations.manage'); + $permissions = app(\App\Services\Care\CarePermissions::class); + $member = $this->member($request); + abort_unless( + $permissions->can($member, 'vitals.manage') + || $permissions->can($member, 'consultations.manage'), + 403, + ); $this->assertEmergencyManage($request, $modules); $this->assertVisit($request, $visit); diff --git a/app/Http/Controllers/Care/SpecialtyModuleController.php b/app/Http/Controllers/Care/SpecialtyModuleController.php index ca5a90d..2972898 100644 --- a/app/Http/Controllers/Care/SpecialtyModuleController.php +++ b/app/Http/Controllers/Care/SpecialtyModuleController.php @@ -637,8 +637,20 @@ class SpecialtyModuleController extends Controller $services = $shell->provisionedServices($organization, $module); $permissions = app(\App\Services\Care\CarePermissions::class); + // Match every ability that mutate routes actually check — module canManage alone is not enough. $canConsult = $permissions->can($member, 'consultations.manage') && $canManageSpecialty; + $canAdvanceStage = $canConsult; + $canStartConsultation = $canConsult; + $canCallNext = $canManageSpecialty; + $canManageClinical = $canManageSpecialty; + $canManageVitals = $canManageSpecialty && ( + $permissions->can($member, 'vitals.manage') + || $permissions->can($member, 'consultations.manage') + ); $canManageQueue = $permissions->can($member, 'appointments.manage') && $canManageSpecialty; + $canBookAppointments = $permissions->can($member, 'appointments.manage'); + $canViewPatients = $permissions->can($member, 'patients.view'); + $canViewAppointments = $permissions->can($member, 'appointments.view'); $branchLabel = $branchId ? (string) (\App\Models\Branch::owned($owner)->whereKey($branchId)->value('name') ?? '') : ''; @@ -880,7 +892,15 @@ class SpecialtyModuleController extends Controller 'practitionerScope' => $practitionerScope, 'lockToPractitioner' => $practitionerScope !== null, 'canConsult' => $canConsult, + 'canAdvanceStage' => $canAdvanceStage, + 'canStartConsultation' => $canStartConsultation, + 'canCallNext' => $canCallNext, + 'canManageClinical' => $canManageClinical, + 'canManageVitals' => $canManageVitals, 'canManageQueue' => $canManageQueue, + 'canBookAppointments' => $canBookAppointments, + 'canViewPatients' => $canViewPatients, + 'canViewAppointments' => $canViewAppointments, 'canManageSpecialty' => $canManageSpecialty, 'canViewSpecialty' => $canViewSpecialty, 'canReferSpecialty' => $canReferSpecialty, diff --git a/resources/views/care/specialty/dentistry/workspace-imaging.blade.php b/resources/views/care/specialty/dentistry/workspace-imaging.blade.php index 50a695b..5f013c9 100644 --- a/resources/views/care/specialty/dentistry/workspace-imaging.blade.php +++ b/resources/views/care/specialty/dentistry/workspace-imaging.blade.php @@ -1,4 +1,4 @@ -@php $canManage = $canManageSpecialty ?? true; @endphp +@php $canManage = $canConsult ?? false; @endphp

Dental imaging

diff --git a/resources/views/care/specialty/dentistry/workspace-lab.blade.php b/resources/views/care/specialty/dentistry/workspace-lab.blade.php index 29e6d35..bc089bf 100644 --- a/resources/views/care/specialty/dentistry/workspace-lab.blade.php +++ b/resources/views/care/specialty/dentistry/workspace-lab.blade.php @@ -1,4 +1,4 @@ -@php $canManage = $canManageSpecialty ?? true; @endphp +@php $canManage = $canConsult ?? false; @endphp

Lab cases

diff --git a/resources/views/care/specialty/dentistry/workspace-notes.blade.php b/resources/views/care/specialty/dentistry/workspace-notes.blade.php index d48531a..2a5ac66 100644 --- a/resources/views/care/specialty/dentistry/workspace-notes.blade.php +++ b/resources/views/care/specialty/dentistry/workspace-notes.blade.php @@ -1,4 +1,4 @@ -@php $canManage = $canManageSpecialty ?? true; @endphp +@php $canManage = $canConsult ?? false; @endphp

Dental visit notes

diff --git a/resources/views/care/specialty/dentistry/workspace-odontogram.blade.php b/resources/views/care/specialty/dentistry/workspace-odontogram.blade.php index 0747449..e3f68a6 100644 --- a/resources/views/care/specialty/dentistry/workspace-odontogram.blade.php +++ b/resources/views/care/specialty/dentistry/workspace-odontogram.blade.php @@ -3,7 +3,7 @@ $selected = old('tooth_code', request('tooth', ($dentalRows['upper'][0] ?? '16'))); $selectedData = $teethMap[$selected] ?? ['status' => 'present', 'surfaces' => [], 'conditions' => [], 'notes' => null]; $dentition = $dentalChart->dentition ?? 'adult'; - $canManage = $canManageSpecialty ?? true; + $canManage = $canConsult ?? false; @endphp
diff --git a/resources/views/care/specialty/dentistry/workspace-overview.blade.php b/resources/views/care/specialty/dentistry/workspace-overview.blade.php index b600f9f..3659c0f 100644 --- a/resources/views/care/specialty/dentistry/workspace-overview.blade.php +++ b/resources/views/care/specialty/dentistry/workspace-overview.blade.php @@ -2,7 +2,7 @@ $plan = $dentalPlan; $openItems = $plan?->items?->whereIn('status', ['proposed', 'accepted']) ?? collect(); $currentStage = $workspaceVisit->specialty_stage; - $canManage = $canManageSpecialty ?? true; + $canManage = $canConsult ?? false; $stageFlow = [ 'waiting' => ['next' => 'chair', 'label' => 'Seat at chair'], 'chair' => ['next' => 'procedure', 'label' => 'Start procedure'], diff --git a/resources/views/care/specialty/dentistry/workspace-perio.blade.php b/resources/views/care/specialty/dentistry/workspace-perio.blade.php index e633256..e104358 100644 --- a/resources/views/care/specialty/dentistry/workspace-perio.blade.php +++ b/resources/views/care/specialty/dentistry/workspace-perio.blade.php @@ -6,7 +6,7 @@ } $teeth = array_merge($dentalRows['upper'] ?? [], $dentalRows['lower'] ?? []); $probeTeeth = array_slice($teeth, 0, 8); - $canManage = $canManageSpecialty ?? true; + $canManage = $canConsult ?? false; @endphp
diff --git a/resources/views/care/specialty/dentistry/workspace-plan.blade.php b/resources/views/care/specialty/dentistry/workspace-plan.blade.php index e30d054..e59c7f0 100644 --- a/resources/views/care/specialty/dentistry/workspace-plan.blade.php +++ b/resources/views/care/specialty/dentistry/workspace-plan.blade.php @@ -1,7 +1,7 @@ @php $plan = $dentalPlan; $history = ($dentalPlanHistory ?? collect())->filter(fn ($p) => ! $plan || $p->id !== $plan->id); - $canManage = $canManageSpecialty ?? true; + $canManage = $canConsult ?? false; @endphp
diff --git a/resources/views/care/specialty/dentistry/workspace-recalls.blade.php b/resources/views/care/specialty/dentistry/workspace-recalls.blade.php index f4bd69e..db4b244 100644 --- a/resources/views/care/specialty/dentistry/workspace-recalls.blade.php +++ b/resources/views/care/specialty/dentistry/workspace-recalls.blade.php @@ -1,4 +1,4 @@ -@php $canManage = $canManageSpecialty ?? true; @endphp +@php $canManage = $canConsult ?? false; @endphp

Recalls

diff --git a/resources/views/care/specialty/dentistry/workspace-treat.blade.php b/resources/views/care/specialty/dentistry/workspace-treat.blade.php index 83f7354..a6ae345 100644 --- a/resources/views/care/specialty/dentistry/workspace-treat.blade.php +++ b/resources/views/care/specialty/dentistry/workspace-treat.blade.php @@ -1,4 +1,4 @@ -@php $canManage = $canManageSpecialty ?? true; @endphp +@php $canManage = $canConsult ?? false; @endphp

Treat today

diff --git a/resources/views/care/specialty/emergency/workspace-disposition.blade.php b/resources/views/care/specialty/emergency/workspace-disposition.blade.php index a46affb..f3639bf 100644 --- a/resources/views/care/specialty/emergency/workspace-disposition.blade.php +++ b/resources/views/care/specialty/emergency/workspace-disposition.blade.php @@ -1,7 +1,7 @@ @php $record = $emergencyDisposition; $payload = old('payload', $record?->payload ?? []); - $canManage = $canManageSpecialty ?? true; + $canManage = $canConsult ?? false; @endphp
diff --git a/resources/views/care/specialty/emergency/workspace-overview.blade.php b/resources/views/care/specialty/emergency/workspace-overview.blade.php index 9c7d910..dc939f8 100644 --- a/resources/views/care/specialty/emergency/workspace-overview.blade.php +++ b/resources/views/care/specialty/emergency/workspace-overview.blade.php @@ -4,7 +4,7 @@ $currentStage = $workspaceVisit->specialty_stage; $stageFlow = $emergencyStageFlow ?? []; $latest = $emergencyLatestVitals; - $canManage = $canManageSpecialty ?? true; + $canManage = $canConsult ?? false; @endphp
diff --git a/resources/views/care/specialty/emergency/workspace-vitals.blade.php b/resources/views/care/specialty/emergency/workspace-vitals.blade.php index a935731..b84469c 100644 --- a/resources/views/care/specialty/emergency/workspace-vitals.blade.php +++ b/resources/views/care/specialty/emergency/workspace-vitals.blade.php @@ -1,4 +1,4 @@ -@php $canManage = $canManageSpecialty ?? true; @endphp +@php $canManage = $canManageVitals ?? false; @endphp
diff --git a/resources/views/care/specialty/partials/actions-menu.blade.php b/resources/views/care/specialty/partials/actions-menu.blade.php index 014c4f9..1858601 100644 --- a/resources/views/care/specialty/partials/actions-menu.blade.php +++ b/resources/views/care/specialty/partials/actions-menu.blade.php @@ -8,7 +8,14 @@ $appointment = $workspaceVisit?->appointment; $openConsultation = $appointment?->consultation; $ticketCalled = $appointment && $appointment->queue_ticket_status === 'called'; - $canStartConsultation = $appointment && in_array($appointment->status, [ + $canCallNext = $canCallNext ?? ($canManageSpecialty ?? false); + $canConsult = $canConsult ?? false; + $canAdvanceStage = $canAdvanceStage ?? $canConsult; + $canStartConsultationFlag = $canStartConsultation ?? $canConsult; + $canBookAppointments = $canBookAppointments ?? false; + $canViewPatients = $canViewPatients ?? true; + $canViewAppointments = $canViewAppointments ?? true; + $appointmentReadyToStart = $appointment && in_array($appointment->status, [ \App\Models\Appointment::STATUS_WAITING, \App\Models\Appointment::STATUS_CHECKED_IN, ], true); @@ -17,7 +24,8 @@ $appointment->status === \App\Models\Appointment::STATUS_IN_CONSULTATION || ($openConsultation && $openConsultation->status !== \App\Models\Consultation::STATUS_COMPLETED) ); - $canCompleteConsultation = $consultationInProgress + $canCompleteConsultation = $canConsult + && $consultationInProgress && $openConsultation && $openConsultation->status !== \App\Models\Consultation::STATUS_COMPLETED; $chartTab = collect($workspaceTabs ?? []) @@ -40,7 +48,7 @@ $currentStage = $workspaceVisit?->specialty_stage; @endphp -@if ($onWorkspace && ($canManageSpecialty ?? true)) +@if ($onWorkspace && $canCallNext) @include('care.partials.queue-ops', [ 'queueIntegration' => $queueIntegration ?? null, 'queueCallNextRoute' => 'care.specialty.call-next', @@ -50,7 +58,7 @@ ]) @endif -@if (($canManageSpecialty ?? true) && $ticketCalled && $appointment) +@if ($canConsult && $ticketCalled && $appointment)
@csrf @@ -58,25 +66,25 @@ @endif @if ($workspaceVisit) - @if (($canManageSpecialty ?? true) && $moduleKey === 'dentistry' && $currentStage && isset($stageFlow[$currentStage])) + @if ($canAdvanceStage && $moduleKey === 'dentistry' && $currentStage && isset($stageFlow[$currentStage])) @csrf
- @elseif (($canManageSpecialty ?? true) && $moduleKey === 'dentistry' && ! $currentStage) + @elseif ($canAdvanceStage && $moduleKey === 'dentistry' && ! $currentStage)
@csrf
- @elseif (($canManageSpecialty ?? true) && $moduleKey === 'emergency' && $currentStage && isset($stageFlow[$currentStage]) && ($stageFlow[$currentStage]['next'] ?? null) !== $currentStage) + @elseif ($canAdvanceStage && $moduleKey === 'emergency' && $currentStage && isset($stageFlow[$currentStage]) && ($stageFlow[$currentStage]['next'] ?? null) !== $currentStage)
@csrf
- @elseif (($canManageSpecialty ?? true) && $moduleKey === 'emergency' && ! $currentStage) + @elseif ($canAdvanceStage && $moduleKey === 'emergency' && ! $currentStage)
@csrf @@ -84,7 +92,7 @@
@endif - @if (($canManageSpecialty ?? true) && $canCompleteConsultation) + @if ($canCompleteConsultation) @endif - @if ($workspaceVisit->patient) + @if ($canViewPatients && $workspaceVisit->patient) Patient chart @endif - @if ($workspaceVisit->appointment) + @if ($canViewAppointments && $workspaceVisit->appointment) Appointment @@ -132,6 +140,8 @@ Back to queue - - Register / book - +@if ($canBookAppointments) + + Register / book + +@endif diff --git a/resources/views/care/specialty/partials/clinical-form.blade.php b/resources/views/care/specialty/partials/clinical-form.blade.php index a3cd896..3bc5ead 100644 --- a/resources/views/care/specialty/partials/clinical-form.blade.php +++ b/resources/views/care/specialty/partials/clinical-form.blade.php @@ -29,7 +29,7 @@
@endif - @if (! ($canManageSpecialty ?? true)) + @if (! ($canManageClinical ?? $canManageSpecialty ?? true))
@forelse ($clinicalFields as $field) @php diff --git a/resources/views/care/specialty/sections/workspace.blade.php b/resources/views/care/specialty/sections/workspace.blade.php index 2e59196..09ba0ae 100644 --- a/resources/views/care/specialty/sections/workspace.blade.php +++ b/resources/views/care/specialty/sections/workspace.blade.php @@ -43,9 +43,9 @@ @elseif ($activeTab === 'billing')

- {{ ($canManageSpecialty ?? true) ? 'Add specialty service' : 'Visit billing' }} + {{ ($canManageClinical ?? $canManageSpecialty ?? true) ? 'Add specialty service' : 'Visit billing' }}

- @if ($canManageSpecialty ?? true) + @if ($canManageClinical ?? $canManageSpecialty ?? true)
@csrf diff --git a/resources/views/care/specialty/shell.blade.php b/resources/views/care/specialty/shell.blade.php index 6bcdea9..30a2fd3 100644 --- a/resources/views/care/specialty/shell.blade.php +++ b/resources/views/care/specialty/shell.blade.php @@ -20,7 +20,7 @@ Reports @endif History - @if ($canManageSpecialty ?? true) + @if ($canManageClinical ?? $canManageSpecialty ?? true) Billing @endif @@ -34,6 +34,10 @@ You have view-only access. Results and history are available; clinical edits require specialty access. @endif + @elseif (($canManageSpecialty ?? false) && ! ($canConsult ?? false)) +
+ You can work this specialty floor (queue, vitals, billing support) but starting encounters and clinical chart edits require a clinician with consultation access. +
@endif
@@ -51,7 +55,9 @@ ]) @php - $completeConsultation = $workspaceVisit?->appointment?->consultation; + $completeConsultation = ($canConsult ?? false) + ? $workspaceVisit?->appointment?->consultation + : null; @endphp @if ($completeConsultation && $completeConsultation->status !== \App\Models\Consultation::STATUS_COMPLETED) History - @if ($canManageSpecialty ?? true) + @if ($canManageClinical ?? $canManageSpecialty ?? true) Billing @endif
@@ -130,7 +136,7 @@ 'canConsult' => $canConsult ?? false, 'lockToPractitioner' => $lockToPractitioner ?? false, 'showFilters' => true, - 'queueCallNextRoute' => ($canManageSpecialty ?? true) ? 'care.specialty.call-next' : null, + 'queueCallNextRoute' => ($canCallNext ?? $canManageSpecialty ?? false) ? 'care.specialty.call-next' : null, 'queueCallNextParams' => ['module' => $moduleKey], 'startRouteName' => 'care.queue.start', 'inCareLabel' => 'In care', diff --git a/tests/Feature/CareSpecialtyAccessTest.php b/tests/Feature/CareSpecialtyAccessTest.php index eba4859..87cd975 100644 --- a/tests/Feature/CareSpecialtyAccessTest.php +++ b/tests/Feature/CareSpecialtyAccessTest.php @@ -340,4 +340,232 @@ class CareSpecialtyAccessTest extends TestCase $this->assertTrue($this->modules->memberCanManage($this->organization, $admin, 'cardiology')); $this->assertTrue($this->modules->memberCanManage($this->organization, $admin, 'emergency')); } + + /** + * @return array{0: \App\Models\User, 1: Member, 2: \App\Models\Visit} + */ + protected function dentistryVisitForRole(string $role, string $suffix): array + { + [$user, $member] = $this->makeStaff($role, $suffix); + if ($role === 'doctor') { + Practitioner::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'member_id' => $member->id, + 'user_ref' => $user->public_id, + 'name' => 'Dr '.$suffix, + 'specialty' => 'Dentistry', + 'is_active' => true, + ]); + } + + $dept = Department::query()->where('type', 'dental')->firstOrFail(); + $patient = Patient::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_number' => 'P-DEN-'.$suffix, + 'first_name' => 'Efua', + 'last_name' => 'Boateng', + 'gender' => 'female', + 'date_of_birth' => '1992-06-01', + ]); + $visit = \App\Models\Visit::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_id' => $patient->id, + 'status' => \App\Models\Visit::STATUS_IN_PROGRESS, + 'checked_in_at' => now(), + 'specialty_stage' => 'waiting', + ]); + Appointment::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_id' => $patient->id, + 'department_id' => $dept->id, + 'visit_id' => $visit->id, + 'type' => Appointment::TYPE_WALK_IN, + 'status' => Appointment::STATUS_WAITING, + 'scheduled_at' => now(), + 'checked_in_at' => now(), + 'waiting_at' => now(), + ]); + + return [$user, $member, $visit]; + } + + public function test_nurse_on_dentistry_does_not_see_consultation_mutate_actions(): void + { + [$nurseUser, $nurseMember, $visit] = $this->dentistryVisitForRole('nurse', 'den-nurse'); + + $this->assertTrue($this->modules->memberCanManage($this->organization, $nurseMember, 'dentistry')); + $this->assertFalse(app(\App\Services\Care\CarePermissions::class)->can($nurseMember, 'consultations.manage')); + + $html = $this->actingAs($nurseUser) + ->get(route('care.specialty.workspace', ['module' => 'dentistry', 'visit' => $visit, 'tab' => 'overview'])) + ->assertOk() + ->assertSee('Efua Boateng') + ->assertSee('consultation access') + ->assertDontSee('Seat at chair') + ->assertDontSee('Register / book') + ->assertSee('Back to queue') + ->assertSee('Patient chart') + ->getContent(); + + $this->assertStringContainsString('Waiting', $html); + $this->assertStringNotContainsString( + route('care.specialty.dentistry.stage', $visit, absolute: false), + $html, + ); + $this->assertStringNotContainsString( + route('care.specialty.consultation.start', ['module' => 'dentistry', 'visit' => $visit], absolute: false), + $html, + ); + } + + public function test_nurse_dentistry_mutate_posts_are_forbidden(): void + { + [$nurseUser, , $visit] = $this->dentistryVisitForRole('nurse', 'den-nurse-post'); + + $this->actingAs($nurseUser) + ->post(route('care.specialty.dentistry.stage', $visit), ['stage' => 'chair']) + ->assertForbidden(); + + $this->actingAs($nurseUser) + ->post(route('care.specialty.consultation.start', ['module' => 'dentistry', 'visit' => $visit])) + ->assertForbidden(); + + $this->actingAs($nurseUser) + ->post(route('care.specialty.dentistry.tooth', $visit), [ + 'tooth_code' => '16', + 'status' => 'present', + ]) + ->assertForbidden(); + } + + public function test_doctor_on_dentistry_sees_and_can_advance_stage(): void + { + [$doctorUser, $doctorMember, $visit] = $this->dentistryVisitForRole('doctor', 'den-doc'); + + $this->assertTrue($this->modules->memberCanManage($this->organization, $doctorMember, 'dentistry')); + $this->assertTrue(app(\App\Services\Care\CarePermissions::class)->can($doctorMember, 'consultations.manage')); + + $this->actingAs($doctorUser) + ->get(route('care.specialty.workspace', ['module' => 'dentistry', 'visit' => $visit, 'tab' => 'overview'])) + ->assertOk() + ->assertSee('Seat at chair') + ->assertSee('Start') + ->assertDontSee('consultation access'); + + $this->actingAs($doctorUser) + ->post(route('care.specialty.dentistry.stage', $visit), ['stage' => 'chair']) + ->assertRedirect(); + + $this->assertSame('chair', $visit->fresh()->specialty_stage); + } + + public function test_admin_on_dentistry_sees_mutate_actions(): void + { + $dept = Department::query()->where('type', 'dental')->firstOrFail(); + $patient = Patient::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_number' => 'P-DEN-ADMIN', + 'first_name' => 'Admin', + 'last_name' => 'Patient', + 'gender' => 'male', + 'date_of_birth' => '1980-01-01', + ]); + $visit = \App\Models\Visit::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_id' => $patient->id, + 'status' => \App\Models\Visit::STATUS_IN_PROGRESS, + 'checked_in_at' => now(), + 'specialty_stage' => 'waiting', + ]); + Appointment::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_id' => $patient->id, + 'department_id' => $dept->id, + 'visit_id' => $visit->id, + 'type' => Appointment::TYPE_WALK_IN, + 'status' => Appointment::STATUS_WAITING, + 'scheduled_at' => now(), + 'checked_in_at' => now(), + 'waiting_at' => now(), + ]); + + $this->actingAs($this->owner) + ->get(route('care.specialty.workspace', ['module' => 'dentistry', 'visit' => $visit, 'tab' => 'overview'])) + ->assertOk() + ->assertSee('Seat at chair') + ->assertSee('Start'); + } + + public function test_nurse_can_save_emergency_vitals_with_vitals_manage(): void + { + [$nurseUser, $nurseMember] = $this->makeStaff('nurse', 'er-vitals'); + $this->assertTrue(app(\App\Services\Care\CarePermissions::class)->can($nurseMember, 'vitals.manage')); + $this->assertFalse(app(\App\Services\Care\CarePermissions::class)->can($nurseMember, 'consultations.manage')); + + $dept = Department::query()->where('type', 'emergency')->firstOrFail(); + $patient = Patient::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_number' => 'P-ER-VITALS', + 'first_name' => 'Kojo', + 'last_name' => 'Mensah', + 'gender' => 'male', + 'date_of_birth' => '1988-01-01', + ]); + $visit = \App\Models\Visit::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_id' => $patient->id, + 'status' => \App\Models\Visit::STATUS_IN_PROGRESS, + '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' => $dept->id, + 'visit_id' => $visit->id, + 'type' => Appointment::TYPE_WALK_IN, + 'status' => Appointment::STATUS_IN_CONSULTATION, + 'scheduled_at' => now(), + 'checked_in_at' => now(), + 'started_at' => now(), + ]); + + $this->actingAs($nurseUser) + ->get(route('care.specialty.workspace', ['module' => 'emergency', 'visit' => $visit, 'tab' => 'vitals'])) + ->assertOk() + ->assertSee('Save vitals') + ->assertDontSee('Seat at chair') + ->assertDontSee('Start triage'); + + $this->actingAs($nurseUser) + ->post(route('care.specialty.emergency.vitals', $visit), [ + 'bp_systolic' => 120, + 'bp_diastolic' => 80, + 'pulse' => 72, + ]) + ->assertRedirect(); + + $this->actingAs($nurseUser) + ->post(route('care.specialty.emergency.stage', $visit), ['stage' => 'treatment']) + ->assertForbidden(); + } } diff --git a/tests/Feature/CareSpecialtyShellTest.php b/tests/Feature/CareSpecialtyShellTest.php index c6ec230..ef3cde6 100644 --- a/tests/Feature/CareSpecialtyShellTest.php +++ b/tests/Feature/CareSpecialtyShellTest.php @@ -294,6 +294,8 @@ class CareSpecialtyShellTest extends TestCase public function test_workspace_includes_timeline_tab_and_complete_action(): void { + Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'doctor']); + app(SpecialtyModuleService::class)->activate( $this->organization, $this->owner->public_id, @@ -378,6 +380,8 @@ class CareSpecialtyShellTest extends TestCase public function test_called_workspace_shows_call_again_until_session_starts(): void { + Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'doctor']); + app(SpecialtyModuleService::class)->activate( $this->organization, $this->owner->public_id,