Add ambulance New call flow under ambulance.manage.
Deploy Ladill Care / deploy (push) Successful in 1m41s

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 <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-20 12:20:29 +00:00
co-authored by Cursor
parent 4e753e68c0
commit 527a469b78
5 changed files with 246 additions and 2 deletions
@@ -4,10 +4,17 @@ namespace App\Http\Controllers\Care;
use App\Http\Controllers\Care\Concerns\ScopesToAccount; use App\Http\Controllers\Care\Concerns\ScopesToAccount;
use App\Http\Controllers\Controller; 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\SpecialtyClinicalRecord;
use App\Models\Visit; use App\Models\Visit;
use App\Services\Care\Ambulance\AmbulanceAnalyticsService; use App\Services\Care\Ambulance\AmbulanceAnalyticsService;
use App\Services\Care\Ambulance\AmbulanceWorkflowService; 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\SpecialtyClinicalRecordService;
use App\Services\Care\SpecialtyModuleService; use App\Services\Care\SpecialtyModuleService;
use App\Services\Care\SpecialtyShellService; use App\Services\Care\SpecialtyShellService;
@@ -38,6 +45,115 @@ class AmbulanceWorkspaceController extends Controller
$this->authorizeBranch($request, (int) $visit->branch_id); $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( public function setStage(
Request $request, Request $request,
Visit $visit, Visit $visit,
@@ -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
<x-app-layout title="New ambulance call">
<div class="mx-auto max-w-xl space-y-4">
<div>
<p class="text-sm text-slate-500">
<a href="{{ route('care.specialty.show', 'ambulance') }}" class="text-indigo-600 hover:text-indigo-800">Ambulance</a>
<span class="text-slate-300">/</span>
New call
</p>
<h1 class="mt-1 text-xl font-semibold text-slate-900">Log a new call</h1>
<p class="mt-1 text-sm text-slate-500">Creates a dispatch board entry not a desk walk-in queue ticket for Call next.</p>
</div>
@if (session('error'))
<div class="rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">{{ session('error') }}</div>
@endif
<form method="POST" action="{{ route('care.specialty.ambulance.new-call.store') }}" class="space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
@csrf
<div>
<label class="block text-sm font-medium text-slate-700">Branch</label>
<select name="branch_id" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@foreach ($branches as $branch)
<option value="{{ $branch->id }}" @selected(old('branch_id', $defaultBranch) == $branch->id)>{{ $branch->name }}</option>
@endforeach
</select>
@error('branch_id')<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Patient</label>
<x-searchable-select
name="patient_id"
:options="$patientOptions"
:selected="old('patient_id')"
:required="true"
placeholder="Search name, number, phone…"
empty-label="Select patient…"
/>
@error('patient_id')<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
<p class="mt-1 text-xs text-slate-500">Register the patient first if they are not in the system yet.</p>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Call nature / reason</label>
<input type="text" name="reason" value="{{ old('reason') }}" placeholder="e.g. Chest pain, trauma pickup, facility transfer"
class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@error('reason')<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Scene / pickup location</label>
<input type="text" name="location" value="{{ old('location') }}" placeholder="Address or landmark"
class="mt-1 w-full rounded-lg border-slate-300 text-sm">
@error('location')<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Notes</label>
<textarea name="notes" rows="3" class="mt-1 w-full rounded-lg border-slate-300 text-sm">{{ old('notes') }}</textarea>
@error('notes')<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
</div>
<div class="flex flex-wrap gap-3">
<button type="submit" class="btn-primary">Log call</button>
<a href="{{ route('care.specialty.show', 'ambulance') }}" class="rounded-lg border border-slate-200 px-4 py-2 text-sm text-slate-600">Cancel</a>
</div>
</form>
</div>
</x-app-layout>
@@ -220,7 +220,7 @@
]"> ]">
@if ($canManageQueue ?? false) @if ($canManageQueue ?? false)
<x-slot name="actions"> <x-slot name="actions">
<a href="{{ route('care.appointments.walk-in.create') }}" class="btn-primary">New call</a> <a href="{{ route('care.specialty.ambulance.new-call') }}" class="btn-primary">New call</a>
</x-slot> </x-slot>
@endif @endif
</x-care.page-hero> </x-care.page-hero>
+2
View File
@@ -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/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/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/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::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}/clinical', [SpecialtyModuleController::class, 'saveClinical'])->name('care.specialty.clinical.save');
Route::post('/specialty/{module}/workspace/{visit}/documents', [SpecialtyModuleController::class, 'uploadDocument'])->name('care.specialty.documents.upload'); Route::post('/specialty/{module}/workspace/{visit}/documents', [SpecialtyModuleController::class, 'uploadDocument'])->name('care.specialty.documents.upload');
+49 -1
View File
@@ -254,6 +254,54 @@ class CareAmbulanceSuiteTest extends TestCase
->assertSee('Handover') ->assertSee('Handover')
->assertSee($this->patient->fullName()) ->assertSee($this->patient->fullName())
->assertDontSee('Call next') ->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);
} }
} }