Add ambulance New call flow under ambulance.manage.
Deploy Ladill Care / deploy (push) Successful in 1m41s
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:
@@ -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>
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user