From 3854fc647158db9f7d4d070a7235ce959edd5959 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sat, 18 Jul 2026 20:47:57 +0000 Subject: [PATCH] Add consultation-nested assessment create and show routes. Index alone still left /consultations/{uuid}/assessments/create and /consultations/{uuid}/assessments/{uuid} unmatched (404). Wire those paths, redirect store into the nested show, and cover the doctor form open flow with feature tests. Co-authored-by: Cursor --- .../Controllers/Care/AssessmentController.php | 104 +++++++++++++++--- .../views/care/assessments/create.blade.php | 13 ++- .../views/care/assessments/index.blade.php | 12 +- .../views/care/assessments/show.blade.php | 4 +- .../views/care/consultations/show.blade.php | 8 +- resources/views/partials/sidebar.blade.php | 1 + routes/web.php | 3 + tests/Feature/CareAssessmentCaptureTest.php | 74 +++++++++++++ 8 files changed, 194 insertions(+), 25 deletions(-) diff --git a/app/Http/Controllers/Care/AssessmentController.php b/app/Http/Controllers/Care/AssessmentController.php index bcad35d..a901da9 100644 --- a/app/Http/Controllers/Care/AssessmentController.php +++ b/app/Http/Controllers/Care/AssessmentController.php @@ -75,6 +75,42 @@ class AssessmentController extends Controller return $this->index($request, $consultation->patient); } + /** + * Start-form picker under a consultation (parity with prescriptions create). + * GET /consultations/{uuid}/assessments/create + */ + public function createForConsultation(Request $request, Consultation $consultation): View + { + $this->authorizeConsultation($request, $consultation); + $consultation->loadMissing(['patient', 'visit']); + + $return = app(ConsultationReturnContext::class); + $return->remember($consultation); + $request->query->set('consultation_uuid', $consultation->uuid); + if ($consultation->visit?->uuid) { + $request->query->set('visit_uuid', $consultation->visit->uuid); + } + $request->query->set(ConsultationReturnContext::QUERY_KEY, $consultation->uuid); + + return $this->create($request, $consultation->patient); + } + + /** + * Open an assessment while staying on the consultation URL tree. + * GET /consultations/{uuid}/assessments/{assessment} + */ + public function showForConsultation(Request $request, Consultation $consultation, Assessment $assessment): View + { + $this->authorizeConsultation($request, $consultation); + abort_unless((int) $assessment->patient_id === (int) $consultation->patient_id, 404); + + $return = app(ConsultationReturnContext::class); + $return->remember($consultation); + $request->query->set(ConsultationReturnContext::QUERY_KEY, $consultation->uuid); + + return $this->show($request, $assessment); + } + public function create(Request $request, Patient $patient): View { $this->assertEngineEnabled($request); @@ -99,15 +135,19 @@ class AssessmentController extends Controller } })->values(); + $returnConsultation = app(ConsultationReturnContext::class) + ->resolve($request, (int) $patient->id); + $returnConsultation?->loadMissing('visit'); + return view('care.assessments.create', [ 'patient' => $patient, 'templates' => $capturable, 'categories' => config('care.assessment_template_categories'), - 'consultationUuid' => $request->query('consultation_uuid'), - 'visitUuid' => $request->query('visit_uuid'), + 'consultationUuid' => $request->query('consultation_uuid') ?? $returnConsultation?->uuid, + 'visitUuid' => $request->query('visit_uuid') ?? $returnConsultation?->visit?->uuid, + 'returnConsultation' => $returnConsultation, ]); } - public function store(Request $request, Patient $patient): RedirectResponse { $this->assertEngineEnabled($request); @@ -130,6 +170,16 @@ class AssessmentController extends Controller ); $return = app(ConsultationReturnContext::class); + if (! empty($context['consultation'])) { + $return->remember($context['consultation']); + + return redirect() + ->route('care.consultations.assessments.show', [ + 'consultation' => $context['consultation'], + 'assessment' => $assessment, + ]) + ->with('success', 'Assessment started.'); + } return redirect() ->route('care.assessments.show', array_filter([ @@ -168,10 +218,10 @@ class AssessmentController extends Controller $return->remember($consultation); return redirect() - ->route('care.assessments.show', array_filter([ + ->route('care.consultations.assessments.show', [ + 'consultation' => $consultation, 'assessment' => $assessment, - ...$return->query($consultation), - ])) + ]) ->with('success', 'Assessment started.'); } @@ -240,11 +290,7 @@ class AssessmentController extends Controller $return = app(ConsultationReturnContext::class); - return redirect() - ->route('care.assessments.show', array_filter([ - 'assessment' => $assessment, - ...$return->routeQuery($request), - ])) + return $this->redirectToAssessmentShow($request, $assessment, $return) ->with('success', 'Assessment saved.'); } @@ -280,11 +326,7 @@ class AssessmentController extends Controller $return = app(ConsultationReturnContext::class); - return redirect() - ->route('care.assessments.show', array_filter([ - 'assessment' => $assessment, - ...$return->routeQuery($request), - ])) + return $this->redirectToAssessmentShow($request, $assessment, $return) ->with('success', 'Assessment completed.'); } @@ -301,11 +343,41 @@ class AssessmentController extends Controller $this->ownerRef($request), ); + $return = app(ConsultationReturnContext::class); + $consultation = $return->resolve($request, (int) $assessment->patient_id); + + if ($consultation) { + return redirect() + ->route('care.consultations.assessments.index', $consultation) + ->with('success', 'Assessment cancelled.'); + } + return redirect() ->route('care.assessments.index', $assessment->patient) ->with('success', 'Assessment cancelled.'); } + /** + * Prefer consultation-nested show when the doctor is mid-consult. + */ + protected function redirectToAssessmentShow( + Request $request, + Assessment $assessment, + ConsultationReturnContext $return, + ): RedirectResponse { + $consultation = $return->resolve($request, (int) $assessment->patient_id) + ?? $return->resolveLinked($request, $assessment->consultation); + + if ($consultation) { + return redirect()->route('care.consultations.assessments.show', [ + 'consultation' => $consultation, + 'assessment' => $assessment, + ]); + } + + return redirect()->route('care.assessments.show', $assessment); + } + /** * FHIR R4 Bundle (Questionnaire + QuestionnaireResponse) download. */ diff --git a/resources/views/care/assessments/create.blade.php b/resources/views/care/assessments/create.blade.php index fb58420..70086cc 100644 --- a/resources/views/care/assessments/create.blade.php +++ b/resources/views/care/assessments/create.blade.php @@ -1,4 +1,6 @@ + +

Start assessment

{{ $patient->fullName() }}

@@ -11,7 +13,9 @@ @if ($templates->isEmpty())

No forms are available for your role right now. Ask an administrator to confirm clinical forms are enabled in Settings.

@else -
+ @csrf @if ($consultationUuid) @@ -19,6 +23,9 @@ @if ($visitUuid) @endif + @if (($returnConsultation ?? null)?->uuid) + + @endif
@@ -38,7 +45,9 @@
- Cancel + Cancel
@endif diff --git a/resources/views/care/assessments/index.blade.php b/resources/views/care/assessments/index.blade.php index 3498766..a5effd8 100644 --- a/resources/views/care/assessments/index.blade.php +++ b/resources/views/care/assessments/index.blade.php @@ -10,7 +10,11 @@

@if ($canCapture) - Start assessment + @if ($returnConsultation ?? null) + Start assessment + @else + Start assessment + @endif @endif
@@ -53,7 +57,11 @@ {{ $assessment->assessed_at?->format('d M Y H:i') ?? $assessment->created_at?->format('d M Y H:i') ?? '—' }} - Open + @if ($returnConsultation ?? null) + Open + @else + Open + @endif @empty diff --git a/resources/views/care/assessments/show.blade.php b/resources/views/care/assessments/show.blade.php index 77047cc..666eb94 100644 --- a/resources/views/care/assessments/show.blade.php +++ b/resources/views/care/assessments/show.blade.php @@ -17,7 +17,9 @@ diff --git a/resources/views/care/consultations/show.blade.php b/resources/views/care/consultations/show.blade.php index ba470f5..6f0fe3a 100644 --- a/resources/views/care/consultations/show.blade.php +++ b/resources/views/care/consultations/show.blade.php @@ -57,7 +57,7 @@

@if ($universalAssessment) - + {{ $universalAssessment->isDraft() ? 'Continue form' : 'View form' }} @elseif (($canCaptureUniversal ?? false) && ! $isCompleted) @@ -145,7 +145,7 @@ Form not available yet @elseif ($instrument['assessment']) {{ $assessmentStatuses[$instrument['assessment']->status] ?? $instrument['assessment']->status }} - + {{ $instrument['assessment']->isDraft() ? 'Continue' : 'View' }} @elseif (($canCaptureAssessment ?? false) && ! $isCompleted) @@ -209,7 +209,7 @@
    @foreach ($consultationAssessments as $item)
  • - + {{ $item->template->name }} · {{ ($assessmentStatuses[$item->status] ?? $item->status) }} @@ -390,7 +390,7 @@

    Patient history form

    - Open + Open
    @foreach ($universalAssessment->answers->sortBy(fn ($a) => $a->question?->sort_order ?? 0) as $answer) diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index e3a18bd..d1aab9f 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -43,6 +43,7 @@ 'name' => 'Clinical forms', 'route' => route('care.patients.index', ['focus' => 'assessments']), 'active' => request()->routeIs('care.assessments.*') + || request()->routeIs('care.consultations.assessments.*') || request()->routeIs('care.pathways.*') || request()->routeIs('care.outcomes.*'), 'icon' => '', diff --git a/routes/web.php b/routes/web.php index 6549598..d4a717e 100644 --- a/routes/web.php +++ b/routes/web.php @@ -219,8 +219,11 @@ Route::middleware(['auth', 'platform.session'])->group(function () { Route::get('/patients/{patient}/assessments', [AssessmentController::class, 'index'])->name('care.assessments.index'); Route::get('/patients/{patient}/assessments/create', [AssessmentController::class, 'create'])->name('care.assessments.create'); Route::post('/patients/{patient}/assessments', [AssessmentController::class, 'store'])->name('care.assessments.store'); + // Consultation-scoped: index / create / show (create before {assessment} so "create" is not bound as uuid) Route::get('/consultations/{consultation}/assessments', [AssessmentController::class, 'indexForConsultation'])->name('care.consultations.assessments.index'); + Route::get('/consultations/{consultation}/assessments/create', [AssessmentController::class, 'createForConsultation'])->name('care.consultations.assessments.create'); Route::post('/consultations/{consultation}/assessments', [AssessmentController::class, 'storeForConsultation'])->name('care.consultations.assessments.store'); + Route::get('/consultations/{consultation}/assessments/{assessment}', [AssessmentController::class, 'showForConsultation'])->name('care.consultations.assessments.show'); Route::get('/assessments/{assessment}', [AssessmentController::class, 'show'])->name('care.assessments.show'); Route::put('/assessments/{assessment}', [AssessmentController::class, 'update'])->name('care.assessments.update'); Route::post('/assessments/{assessment}/complete', [AssessmentController::class, 'complete'])->name('care.assessments.complete'); diff --git a/tests/Feature/CareAssessmentCaptureTest.php b/tests/Feature/CareAssessmentCaptureTest.php index 0cbe1d2..eceb306 100644 --- a/tests/Feature/CareAssessmentCaptureTest.php +++ b/tests/Feature/CareAssessmentCaptureTest.php @@ -400,6 +400,76 @@ class CareAssessmentCaptureTest extends TestCase $this->assertSame($this->branch->id, $assessment->branch_id); } + public function test_consultation_create_show_and_store_open_forms_for_doctor(): void + { + $this->setMemberRole('doctor'); + + $visit = Visit::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_id' => $this->patient->id, + 'status' => Visit::STATUS_OPEN, + 'checked_in_at' => now(), + ]); + + $consultation = Consultation::create([ + 'owner_ref' => $this->user->public_id, + 'visit_id' => $visit->id, + 'patient_id' => $this->patient->id, + 'status' => Consultation::STATUS_DRAFT, + 'started_at' => now(), + ]); + + // Nested create (was unmatched → Laravel 404 before this fix) + $this->actingAs($this->user) + ->get(route('care.consultations.assessments.create', $consultation)) + ->assertOk() + ->assertSee('Start assessment') + ->assertSee('Universal Intake'); + + $this->actingAs($this->user) + ->get('/consultations/'.$consultation->uuid.'/assessments/create') + ->assertOk(); + + $response = $this->actingAs($this->user) + ->post(route('care.consultations.assessments.store', $consultation), [ + 'template_code' => 'universal_intake', + ]); + + $assessment = Assessment::first(); + $this->assertNotNull($assessment); + + $response->assertRedirect(route('care.consultations.assessments.show', [ + 'consultation' => $consultation, + 'assessment' => $assessment, + ])); + + // Nested show (was unmatched → Laravel 404) + $this->actingAs($this->user) + ->get(route('care.consultations.assessments.show', [$consultation, $assessment])) + ->assertOk() + ->assertSee('Universal Intake') + ->assertSee('Back to consultation'); + + $this->actingAs($this->user) + ->get('/consultations/'.$consultation->uuid.'/assessments/'.$assessment->uuid) + ->assertOk(); + + // Flat show still works for patient-chart / bookmarks + $this->actingAs($this->user) + ->get(route('care.assessments.show', $assessment)) + ->assertOk(); + + // Patient create + store with consultation_uuid also lands on nested show + $this->actingAs($this->user) + ->get(route('care.assessments.create', [ + 'patient' => $this->patient, + 'consultation_uuid' => $consultation->uuid, + ])) + ->assertOk(); + } + public function test_public_bodies_use_consultation_uuid(): void { $this->setMemberRole('doctor'); @@ -430,6 +500,10 @@ class CareAssessmentCaptureTest extends TestCase $assessment = Assessment::first(); $this->assertSame($consultation->id, $assessment->consultation_id); + + $this->actingAs($this->user) + ->get(route('care.consultations.assessments.show', [$consultation, $assessment])) + ->assertOk(); } public function test_tenant_isolation_returns_404(): void