feat(assessments): layered clinical assessment engine end-to-end
Deploy Ladill Care / deploy (push) Successful in 1m26s
Deploy Ladill Care / deploy (push) Successful in 1m26s
Add a template-driven assessment system with universal intake, clinical pathways, disease instruments (stroke MVP + extended, diabetes, and ten specialty packs), scoring, patient outcome trends, REST/API + FHIR export, and Enterprise org-level assessment analytics. Seed packs and design/licensing docs ship for deploy and pre-GA review.
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Api\Concerns\ScopesApiToAccount;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ClinicalPathway;
|
||||
use App\Models\Consultation;
|
||||
use App\Models\Patient;
|
||||
use App\Models\PatientPathway;
|
||||
use App\Services\Care\CareFeatures;
|
||||
use App\Services\Care\CarePermissions;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use App\Services\Care\PathwayService;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PathwayController extends Controller
|
||||
{
|
||||
use ScopesApiToAccount;
|
||||
|
||||
public function __construct(
|
||||
protected PathwayService $pathways,
|
||||
protected CareFeatures $features,
|
||||
protected CarePermissions $permissions,
|
||||
) {}
|
||||
|
||||
public function catalog(Request $request): JsonResponse
|
||||
{
|
||||
$this->assertEngineEnabled($request);
|
||||
$this->authorizeAbility($request, 'assessments.view');
|
||||
|
||||
$pathways = ClinicalPathway::query()
|
||||
->active()
|
||||
->with('templates')
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
|
||||
return response()->json(['data' => $pathways]);
|
||||
}
|
||||
|
||||
public function index(Request $request, Patient $patient): JsonResponse
|
||||
{
|
||||
$this->assertEngineEnabled($request);
|
||||
$this->authorizeAbility($request, 'assessments.view');
|
||||
$this->authorizePatient($request, $patient);
|
||||
|
||||
$active = $this->pathways->activeFor($patient)->load('pathway.templates');
|
||||
$history = PatientPathway::query()
|
||||
->owned($this->ownerRef($request))
|
||||
->where('patient_id', $patient->id)
|
||||
->with('pathway')
|
||||
->orderByDesc('activated_at')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
return response()->json([
|
||||
'active' => $active->map(fn (PatientPathway $pp) => $this->serializePatientPathway($pp)),
|
||||
'history' => $history->map(fn (PatientPathway $pp) => $this->serializePatientPathway($pp)),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request, Patient $patient): JsonResponse
|
||||
{
|
||||
$this->assertEngineEnabled($request);
|
||||
$this->authorizeAbility($request, 'pathways.manage');
|
||||
$this->authorizePatient($request, $patient);
|
||||
|
||||
$validated = $request->validate([
|
||||
'pathway_code' => ['required', 'string', 'max:100'],
|
||||
'consultation_uuid' => ['nullable', 'uuid'],
|
||||
'activation_diagnosis_text' => ['nullable', 'string', 'max:500'],
|
||||
]);
|
||||
|
||||
$pathway = ClinicalPathway::findByCode($validated['pathway_code']);
|
||||
abort_unless($pathway, 422, 'Unknown or inactive pathway.');
|
||||
|
||||
$consultation = null;
|
||||
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', 'diagnoses']);
|
||||
}
|
||||
|
||||
$patientPathway = $this->pathways->activate(
|
||||
$patient,
|
||||
$pathway,
|
||||
$this->ownerRef($request),
|
||||
$this->member($request),
|
||||
[
|
||||
'consultation' => $consultation,
|
||||
'activation_diagnosis_text' => $validated['activation_diagnosis_text'] ?? null,
|
||||
'actor' => $this->ownerRef($request),
|
||||
],
|
||||
);
|
||||
|
||||
return response()->json($this->serializePatientPathway($patientPathway->load(['pathway.templates', 'assessments.template'])), 201);
|
||||
}
|
||||
|
||||
public function deactivate(Request $request, Patient $patient, PatientPathway $patientPathway): JsonResponse
|
||||
{
|
||||
$this->assertEngineEnabled($request);
|
||||
$this->authorizeAbility($request, 'pathways.manage');
|
||||
$this->authorizePatient($request, $patient);
|
||||
abort_unless($patientPathway->patient_id === $patient->id, 404);
|
||||
$this->authorizeOwner($request, $patientPathway);
|
||||
|
||||
$updated = $this->pathways->deactivate(
|
||||
$patientPathway,
|
||||
$this->ownerRef($request),
|
||||
$this->member($request),
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
return response()->json($this->serializePatientPathway($updated->load('pathway')));
|
||||
}
|
||||
|
||||
public function suggestions(Request $request, Consultation $consultation): JsonResponse
|
||||
{
|
||||
$this->assertEngineEnabled($request);
|
||||
$this->authorizeAbility($request, 'consultations.view');
|
||||
$this->authorizeConsultation($request, $consultation);
|
||||
|
||||
$consultation->loadMissing(['diagnoses', 'patient']);
|
||||
$suggestions = $this->pathways->suggest($consultation->patient, $consultation->diagnoses);
|
||||
|
||||
return response()->json([
|
||||
'data' => $suggestions->map(fn (array $row) => [
|
||||
'pathway_code' => $row['pathway_code'],
|
||||
'pathway_name' => $row['pathway_name'],
|
||||
'rank' => $row['rank'],
|
||||
'match_reason' => $row['match_reason'],
|
||||
'already_active' => $row['already_active'],
|
||||
])->values()->all(),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function serializePatientPathway(PatientPathway $pp): array
|
||||
{
|
||||
return [
|
||||
'uuid' => $pp->uuid,
|
||||
'status' => $pp->status,
|
||||
'pathway_code' => $pp->pathway?->code,
|
||||
'pathway_name' => $pp->pathway?->name,
|
||||
'activated_at' => $pp->activated_at?->toIso8601String(),
|
||||
'activation_diagnosis_text' => $pp->activation_diagnosis_text,
|
||||
'resolved_at' => $pp->resolved_at?->toIso8601String(),
|
||||
'assessments' => $pp->relationLoaded('assessments')
|
||||
? $pp->assessments->map(fn ($a) => [
|
||||
'uuid' => $a->uuid,
|
||||
'status' => $a->status,
|
||||
'template_code' => $a->template?->code,
|
||||
])->values()->all()
|
||||
: null,
|
||||
];
|
||||
}
|
||||
|
||||
protected function assertEngineEnabled(Request $request): void
|
||||
{
|
||||
abort_unless(
|
||||
$this->features->enabled($this->organization($request), CareFeatures::ASSESSMENTS_ENGINE),
|
||||
404,
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user