From 527a469b780166be11c1860bfb30509c450f4118 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Mon, 20 Jul 2026 12:20:29 +0000 Subject: [PATCH] Add ambulance New call flow under ambulance.manage. Walk-in appointments require appointments.manage, which blocked EMS staff; New call now uses a dedicated specialty route that lands on the dispatch board. Co-authored-by: Cursor --- .../Care/AmbulanceWorkspaceController.php | 116 ++++++++++++++++++ .../specialty/ambulance/new-call.blade.php | 78 ++++++++++++ .../views/care/specialty/shell.blade.php | 2 +- routes/web.php | 2 + tests/Feature/CareAmbulanceSuiteTest.php | 50 +++++++- 5 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 resources/views/care/specialty/ambulance/new-call.blade.php diff --git a/app/Http/Controllers/Care/AmbulanceWorkspaceController.php b/app/Http/Controllers/Care/AmbulanceWorkspaceController.php index b8dc44e..c00c26c 100644 --- a/app/Http/Controllers/Care/AmbulanceWorkspaceController.php +++ b/app/Http/Controllers/Care/AmbulanceWorkspaceController.php @@ -4,10 +4,17 @@ namespace App\Http\Controllers\Care; use App\Http\Controllers\Care\Concerns\ScopesToAccount; use App\Http\Controllers\Controller; +use App\Models\Appointment; +use App\Models\Branch; +use App\Models\Department; +use App\Models\Patient; use App\Models\SpecialtyClinicalRecord; use App\Models\Visit; use App\Services\Care\Ambulance\AmbulanceAnalyticsService; use App\Services\Care\Ambulance\AmbulanceWorkflowService; +use App\Services\Care\AppointmentService; +use App\Services\Care\AuditLogger; +use App\Services\Care\OrganizationResolver; use App\Services\Care\SpecialtyClinicalRecordService; use App\Services\Care\SpecialtyModuleService; use App\Services\Care\SpecialtyShellService; @@ -38,6 +45,115 @@ class AmbulanceWorkspaceController extends Controller $this->authorizeBranch($request, (int) $visit->branch_id); } + public function newCallCreate( + Request $request, + SpecialtyModuleService $modules, + ): View { + $this->authorizeAbility($request, 'ambulance.manage'); + $this->assertAmbulanceManage($request, $modules); + + $organization = $this->organization($request); + $owner = $this->ownerRef($request); + $member = $this->member($request); + $branchScope = app(OrganizationResolver::class)->branchScope($member); + + $branches = Branch::owned($owner) + ->where('organization_id', $organization->id) + ->where('is_active', true) + ->when($branchScope, fn ($q) => $q->where('id', $branchScope)) + ->orderBy('name') + ->get(); + + $defaultBranch = $branchScope ?? $branches->first()?->id; + + $patients = Patient::owned($owner) + ->where('organization_id', $organization->id) + ->when($branchScope, fn ($q) => $q->where('branch_id', $branchScope)) + ->orderBy('first_name') + ->limit(200) + ->get(); + + return view('care.specialty.ambulance.new-call', [ + 'organization' => $organization, + 'branches' => $branches, + 'defaultBranch' => $defaultBranch, + 'patients' => $patients, + ]); + } + + public function newCallStore( + Request $request, + SpecialtyModuleService $modules, + AppointmentService $appointments, + ): RedirectResponse { + $this->authorizeAbility($request, 'ambulance.manage'); + $this->assertAmbulanceManage($request, $modules); + + $organization = $this->organization($request); + $owner = $this->ownerRef($request); + $member = $this->member($request); + $branchScope = app(OrganizationResolver::class)->branchScope($member); + + $validated = $request->validate([ + 'branch_id' => ['required', 'integer', 'exists:care_branches,id'], + 'patient_id' => ['required', 'integer', 'exists:care_patients,id'], + 'reason' => ['nullable', 'string', 'max:500'], + 'notes' => ['nullable', 'string', 'max:2000'], + 'location' => ['nullable', 'string', 'max:500'], + ]); + + $branchId = (int) $validated['branch_id']; + if ($branchScope !== null) { + abort_unless($branchId === (int) $branchScope, 403); + } + $this->authorizeBranch($request, $branchId); + + $department = Department::owned($owner) + ->where('type', 'ambulance') + ->where('branch_id', $branchId) + ->where('is_active', true) + ->orderBy('id') + ->first(); + + if (! $department) { + return back()->withInput()->with('error', 'No active ambulance department at this branch.'); + } + + $patient = Patient::owned($owner) + ->where('organization_id', $organization->id) + ->findOrFail((int) $validated['patient_id']); + + $reason = trim((string) ($validated['reason'] ?? '')); + if ($reason === '') { + $reason = 'Ambulance call'; + } + if (! empty($validated['location'])) { + $reason .= ' · '.$validated['location']; + } + + $appointment = $appointments->walkIn($organization, $owner, [ + 'patient_id' => $patient->id, + 'branch_id' => $branchId, + 'department_id' => $department->id, + 'reason' => $reason, + 'notes' => $validated['notes'] ?? null, + ], $this->actorRef($request) ?? $owner); + + AuditLogger::record( + $owner, + 'ambulance.new_call', + $organization->id, + $this->actorRef($request) ?? $owner, + Appointment::class, + $appointment->id, + ['department_id' => $department->id], + ); + + return redirect() + ->route('care.specialty.show', 'ambulance') + ->with('success', 'New call logged for '.$patient->fullName().'.'); + } + public function setStage( Request $request, Visit $visit, diff --git a/resources/views/care/specialty/ambulance/new-call.blade.php b/resources/views/care/specialty/ambulance/new-call.blade.php new file mode 100644 index 0000000..fe80550 --- /dev/null +++ b/resources/views/care/specialty/ambulance/new-call.blade.php @@ -0,0 +1,78 @@ +@php + $patientOptions = collect($patients)->map(fn ($patient) => [ + 'value' => (string) $patient->id, + 'label' => $patient->fullName().' ('.$patient->patient_number.')', + 'search' => trim($patient->fullName().' '.$patient->patient_number.' '.($patient->phone ?? '').' '.($patient->email ?? '')), + ])->all(); +@endphp + + +
+
+

+ Ambulance + / + New call +

+

Log a new call

+

Creates a dispatch board entry — not a desk walk-in queue ticket for Call next.

+
+ + @if (session('error')) +
{{ session('error') }}
+ @endif + +
+ @csrf + +
+ + + @error('branch_id')

{{ $message }}

@enderror +
+ +
+ + + @error('patient_id')

{{ $message }}

@enderror +

Register the patient first if they are not in the system yet.

+
+ +
+ + + @error('reason')

{{ $message }}

@enderror +
+ +
+ + + @error('location')

{{ $message }}

@enderror +
+ +
+ + + @error('notes')

{{ $message }}

@enderror +
+ +
+ + Cancel +
+
+
+
diff --git a/resources/views/care/specialty/shell.blade.php b/resources/views/care/specialty/shell.blade.php index 9695acd..ee289d9 100644 --- a/resources/views/care/specialty/shell.blade.php +++ b/resources/views/care/specialty/shell.blade.php @@ -220,7 +220,7 @@ ]"> @if ($canManageQueue ?? false) - New call + New call @endif diff --git a/routes/web.php b/routes/web.php index 0ac925c..47bf8b4 100644 --- a/routes/web.php +++ b/routes/web.php @@ -181,6 +181,8 @@ Route::middleware(['auth', 'platform.session'])->group(function () { Route::get('/specialty/fertility/reports', [FertilityWorkspaceController::class, 'reports'])->name('care.specialty.fertility.reports'); Route::get('/specialty/child-welfare/reports', [ChildWelfareWorkspaceController::class, 'reports'])->name('care.specialty.child-welfare.reports'); Route::get('/specialty/ambulance/reports', [AmbulanceWorkspaceController::class, 'reports'])->name('care.specialty.ambulance.reports'); + Route::get('/specialty/ambulance/new-call', [AmbulanceWorkspaceController::class, 'newCallCreate'])->name('care.specialty.ambulance.new-call'); + Route::post('/specialty/ambulance/new-call', [AmbulanceWorkspaceController::class, 'newCallStore'])->name('care.specialty.ambulance.new-call.store'); Route::get('/specialty/{module}/workspace/{visit?}', [SpecialtyModuleController::class, 'workspace'])->name('care.specialty.workspace'); Route::post('/specialty/{module}/workspace/{visit}/clinical', [SpecialtyModuleController::class, 'saveClinical'])->name('care.specialty.clinical.save'); Route::post('/specialty/{module}/workspace/{visit}/documents', [SpecialtyModuleController::class, 'uploadDocument'])->name('care.specialty.documents.upload'); diff --git a/tests/Feature/CareAmbulanceSuiteTest.php b/tests/Feature/CareAmbulanceSuiteTest.php index 49ed0ea..b67fb66 100644 --- a/tests/Feature/CareAmbulanceSuiteTest.php +++ b/tests/Feature/CareAmbulanceSuiteTest.php @@ -254,6 +254,54 @@ class CareAmbulanceSuiteTest extends TestCase ->assertSee('Handover') ->assertSee($this->patient->fullName()) ->assertDontSee('Call next') - ->assertDontSee('Bring the next waiting patient to your desk'); + ->assertDontSee('Bring the next waiting patient to your desk') + ->assertSee(route('care.specialty.ambulance.new-call'), false); + } + + public function test_ambulance_staff_can_log_new_call_without_appointments_manage(): void + { + $user = User::create([ + 'public_id' => 'amb-crew', + 'name' => 'Kojo Ambulance', + 'email' => 'kojo-amb@example.com', + ]); + Member::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'user_ref' => $user->public_id, + 'role' => 'ambulance_staff', + 'branch_id' => $this->branch->id, + ]); + + $this->actingAs($user) + ->get(route('care.appointments.walk-in.create')) + ->assertForbidden(); + + $this->actingAs($user) + ->get(route('care.specialty.ambulance.new-call')) + ->assertOk() + ->assertSee('Log a new call'); + + $this->actingAs($user) + ->post(route('care.specialty.ambulance.new-call.store'), [ + 'branch_id' => $this->branch->id, + 'patient_id' => $this->patient->id, + 'reason' => 'Chest pain', + 'location' => 'Spintex Road', + ]) + ->assertRedirect(route('care.specialty.show', 'ambulance')); + + $this->assertDatabaseHas('care_appointments', [ + 'patient_id' => $this->patient->id, + 'branch_id' => $this->branch->id, + 'type' => Appointment::TYPE_WALK_IN, + ]); + + $visit = Visit::query() + ->where('patient_id', $this->patient->id) + ->where('specialty_stage', 'check_in') + ->latest('id') + ->first(); + $this->assertNotNull($visit); } }