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
-
@@ -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 @@
@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