assertEngineEnabled($request); $this->authorizeAbility($request, 'assessments.view'); $this->authorizePatient($request, $patient); $branchScope = app(OrganizationResolver::class)->branchScope($this->member($request)); $assessments = $this->assessments->listForPatient( $patient, $this->ownerRef($request), $request->only(['status', 'template_code', 'category', 'from', 'to', 'per_page']), $branchScope, ); $canCapture = $this->canCapture($request); return view('care.assessments.index', [ 'patient' => $patient, 'assessments' => $assessments, 'canCapture' => $canCapture, 'statuses' => config('care.assessment_statuses'), 'categories' => config('care.assessment_template_categories'), 'filters' => $request->only(['status', 'template_code', 'category']), 'returnConsultation' => app(ConsultationReturnContext::class)->resolve($request, (int) $patient->id), ]); } public function create(Request $request, Patient $patient): View { $this->assertEngineEnabled($request); $this->authorizeCapture($request); $this->authorizePatient($request, $patient); $templates = AssessmentTemplate::query() ->system() ->current() ->orderBy('category') ->orderBy('name') ->get(); $member = $this->member($request); $capturable = $templates->filter(function (AssessmentTemplate $template) use ($member) { try { $this->assessments->assertCaptureAllowed($member, $template); return true; } catch (\Throwable) { return false; } })->values(); 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'), ]); } public function store(Request $request, Patient $patient): RedirectResponse { $this->assertEngineEnabled($request); $this->authorizeCapture($request); $this->authorizePatient($request, $patient); $validated = $request->validate([ 'template_code' => ['required', 'string', 'max:100'], 'consultation_uuid' => ['nullable', 'uuid'], 'visit_uuid' => ['nullable', 'uuid'], ]); $context = $this->resolveContext($request, $patient, $validated); $assessment = $this->assessments->start( $patient, $validated['template_code'], $this->ownerRef($request), $this->member($request), $context, ); $return = app(ConsultationReturnContext::class); return redirect() ->route('care.assessments.show', array_filter([ 'assessment' => $assessment, ...$return->routeQuery($request), ])) ->with('success', 'Assessment started.'); } public function storeForConsultation(Request $request, Consultation $consultation): RedirectResponse { $this->assertEngineEnabled($request); $this->authorizeCapture($request); $this->authorizeConsultation($request, $consultation); $validated = $request->validate([ 'template_code' => ['required', 'string', 'max:100'], ]); $consultation->loadMissing(['patient', 'visit']); $this->authorizePatient($request, $consultation->patient); $assessment = $this->assessments->start( $consultation->patient, $validated['template_code'], $this->ownerRef($request), $this->member($request), [ 'consultation' => $consultation, 'visit' => $consultation->visit, 'actor' => $this->ownerRef($request), ], ); $return = app(ConsultationReturnContext::class); $return->remember($consultation); return redirect() ->route('care.assessments.show', array_filter([ 'assessment' => $assessment, ...$return->query($consultation), ])) ->with('success', 'Assessment started.'); } public function show(Request $request, Assessment $assessment): View { $this->assertEngineEnabled($request); $this->authorizeAbility($request, 'assessments.view'); $this->authorizeAssessment($request, $assessment); $assessment->load([ 'template.questions', 'answers.question', 'patient', 'consultation', 'visit', 'practitioner', 'score', ]); $answersByCode = $assessment->answers ->mapWithKeys(fn ($answer) => [ $answer->question->code => $answer->authoritativeValue($answer->question->answer_type), ]); $canEdit = false; if ($assessment->isDraft() && $this->canCapture($request)) { try { $this->assessments->assertCaptureAllowed($this->member($request), $assessment->template); $canEdit = true; } catch (\Throwable) { $canEdit = false; } } $return = app(ConsultationReturnContext::class); return view('care.assessments.show', [ 'assessment' => $assessment, 'answersByCode' => $answersByCode, 'canEdit' => $canEdit, 'statuses' => config('care.assessment_statuses'), 'returnConsultation' => $return->resolve($request, (int) $assessment->patient_id) ?? $return->resolveLinked($request, $assessment->consultation), ]); } public function update(Request $request, Assessment $assessment): RedirectResponse { $this->assertEngineEnabled($request); $this->authorizeCapture($request); $this->authorizeAssessment($request, $assessment); $validated = $request->validate([ 'answers' => ['nullable', 'array'], 'notes' => ['nullable', 'string', 'max:10000'], ]); $this->assessments->saveAnswers( $assessment, $this->ownerRef($request), $validated['answers'] ?? [], $this->member($request), $this->ownerRef($request), array_key_exists('notes', $validated) ? $validated['notes'] : null, ); $return = app(ConsultationReturnContext::class); return redirect() ->route('care.assessments.show', array_filter([ 'assessment' => $assessment, ...$return->routeQuery($request), ])) ->with('success', 'Assessment saved.'); } public function complete(Request $request, Assessment $assessment): RedirectResponse { $this->assertEngineEnabled($request); $this->authorizeCapture($request); $this->authorizeAssessment($request, $assessment); // Persist any posted answers first (complete form can include them). if ($request->has('answers') || $request->has('notes')) { $validated = $request->validate([ 'answers' => ['nullable', 'array'], 'notes' => ['nullable', 'string', 'max:10000'], ]); $this->assessments->saveAnswers( $assessment, $this->ownerRef($request), $validated['answers'] ?? [], $this->member($request), $this->ownerRef($request), array_key_exists('notes', $validated) ? $validated['notes'] : null, ); $assessment->refresh(); } $this->assessments->complete( $assessment, $this->ownerRef($request), $this->member($request), $this->ownerRef($request), ); $return = app(ConsultationReturnContext::class); return redirect() ->route('care.assessments.show', array_filter([ 'assessment' => $assessment, ...$return->routeQuery($request), ])) ->with('success', 'Assessment completed.'); } public function cancel(Request $request, Assessment $assessment): RedirectResponse { $this->assertEngineEnabled($request); $this->authorizeCapture($request); $this->authorizeAssessment($request, $assessment); $this->assessments->cancel( $assessment, $this->ownerRef($request), $this->member($request), $this->ownerRef($request), ); return redirect() ->route('care.assessments.index', $assessment->patient) ->with('success', 'Assessment cancelled.'); } /** * FHIR R4 Bundle (Questionnaire + QuestionnaireResponse) download. */ public function fhirExport(Request $request, Assessment $assessment): StreamedResponse|JsonResponse { $this->assertEngineEnabled($request); $this->authorizeAbility($request, 'assessments.view'); $this->authorizeAssessment($request, $assessment); $bundle = $this->fhir->exportBundle($assessment); $filename = 'assessment-'.$assessment->uuid.'-fhir.json'; if ($request->wantsJson() && ! $request->boolean('download')) { return response()->json($bundle); } return response()->streamDownload( function () use ($bundle) { echo json_encode($bundle, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); }, $filename, ['Content-Type' => 'application/fhir+json'], ); } protected function assertEngineEnabled(Request $request): void { abort_unless( $this->features->enabled($this->organization($request), CareFeatures::ASSESSMENTS_ENGINE), 404, ); $this->features->ensureAssessmentCatalog(); } protected function authorizeCapture(Request $request): void { abort_unless($this->canCapture($request), 403); } protected function canCapture(Request $request): bool { $member = $this->member($request); return $this->permissions->can($member, 'assessments.capture') || $this->permissions->can($member, 'assessments.manage'); } protected function authorizePatient(Request $request, Patient $patient): void { $this->authorizeOwner($request, $patient); abort_unless($patient->organization_id === $this->organization($request)->id, 404); $branchId = app(OrganizationResolver::class)->branchScope($this->member($request)); if ($branchId !== null && $patient->branch_id !== $branchId) { abort(404); } } protected function authorizeConsultation(Request $request, Consultation $consultation): void { $this->authorizeOwner($request, $consultation); $consultation->loadMissing('visit'); abort_unless($consultation->visit->organization_id === $this->organization($request)->id, 404); $branchId = app(OrganizationResolver::class)->branchScope($this->member($request)); if ($branchId !== null && $consultation->visit->branch_id !== $branchId) { abort(404); } } protected function authorizeAssessment(Request $request, Assessment $assessment): void { $this->authorizeOwner($request, $assessment); abort_unless($assessment->organization_id === $this->organization($request)->id, 404); $branchId = app(OrganizationResolver::class)->branchScope($this->member($request)); if ($branchId !== null && $assessment->branch_id !== $branchId) { abort(404); } } /** * @param array{consultation_uuid?: ?string, visit_uuid?: ?string} $validated * @return array{consultation?: Consultation, visit?: Visit, actor: string} */ protected function resolveContext(Request $request, Patient $patient, array $validated): array { $context = ['actor' => $this->ownerRef($request)]; if (! empty($validated['consultation_uuid'])) { $consultation = Consultation::query() ->where('uuid', $validated['consultation_uuid']) ->where('patient_id', $patient->id) ->firstOrFail(); $this->authorizeConsultation($request, $consultation); $consultation->loadMissing('visit'); $context['consultation'] = $consultation; $context['visit'] = $consultation->visit; } elseif (! empty($validated['visit_uuid'])) { $visit = Visit::query() ->where('uuid', $validated['visit_uuid']) ->where('patient_id', $patient->id) ->firstOrFail(); $this->authorizeOwner($request, $visit); abort_unless($visit->organization_id === $this->organization($request)->id, 404); $branchId = app(OrganizationResolver::class)->branchScope($this->member($request)); if ($branchId !== null && $visit->branch_id !== $branchId) { abort(404); } $context['visit'] = $visit; } return $context; } }