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:
@@ -7,8 +7,13 @@ use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Consultation;
|
||||
use App\Models\InvestigationType;
|
||||
use App\Models\Practitioner;
|
||||
use App\Models\Assessment;
|
||||
use App\Models\AssessmentTemplate;
|
||||
use App\Services\Care\CareFeatures;
|
||||
use App\Services\Care\CarePermissions;
|
||||
use App\Services\Care\ConsultationService;
|
||||
use App\Services\Care\OrganizationResolver;
|
||||
use App\Services\Care\PathwayService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
@@ -32,25 +37,127 @@ class ConsultationController extends Controller
|
||||
'investigationRequests.investigationType', 'prescriptions.items',
|
||||
]);
|
||||
|
||||
$canManage = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'consultations.manage');
|
||||
$canVitals = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'vitals.manage');
|
||||
$canRequestInvestigations = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'investigations.request');
|
||||
$canPrescribe = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'prescriptions.manage');
|
||||
$canGenerateBill = app(\App\Services\Care\CarePermissions::class)
|
||||
->can($this->member($request), 'bills.manage');
|
||||
$permissions = app(CarePermissions::class);
|
||||
$member = $this->member($request);
|
||||
$organization = $this->organization($request);
|
||||
|
||||
$canManage = $permissions->can($member, 'consultations.manage');
|
||||
$canVitals = $permissions->can($member, 'vitals.manage');
|
||||
$canRequestInvestigations = $permissions->can($member, 'investigations.request');
|
||||
$canPrescribe = $permissions->can($member, 'prescriptions.manage');
|
||||
$canGenerateBill = $permissions->can($member, 'bills.manage');
|
||||
|
||||
$assessmentsEnabled = app(CareFeatures::class)->enabled($organization, CareFeatures::ASSESSMENTS_ENGINE);
|
||||
$canViewAssessments = $assessmentsEnabled && $permissions->can($member, 'assessments.view');
|
||||
$canCaptureAssessment = $assessmentsEnabled && (
|
||||
$permissions->can($member, 'assessments.capture')
|
||||
|| $permissions->can($member, 'assessments.manage')
|
||||
);
|
||||
|
||||
$consultationAssessments = collect();
|
||||
$startableTemplates = collect();
|
||||
$universalTemplate = null;
|
||||
$universalAssessment = null;
|
||||
$canCaptureUniversal = false;
|
||||
$canManagePathways = $assessmentsEnabled && $permissions->can($member, 'pathways.manage');
|
||||
$pathwaySuggestions = collect();
|
||||
$activePathways = collect();
|
||||
|
||||
if ($canViewAssessments) {
|
||||
$consultationAssessments = Assessment::query()
|
||||
->owned($this->ownerRef($request))
|
||||
->where('consultation_id', $consultation->id)
|
||||
->with(['template', 'answers.question'])
|
||||
->orderByDesc('created_at')
|
||||
->get();
|
||||
|
||||
$universalTemplate = AssessmentTemplate::currentSystemByCode('universal_intake');
|
||||
if ($universalTemplate) {
|
||||
$universalAssessment = $consultationAssessments
|
||||
->first(fn (Assessment $a) => $a->template_id === $universalTemplate->id)
|
||||
?? Assessment::query()
|
||||
->owned($this->ownerRef($request))
|
||||
->where('patient_id', $consultation->patient_id)
|
||||
->where('template_id', $universalTemplate->id)
|
||||
->whereIn('status', [Assessment::STATUS_DRAFT, Assessment::STATUS_COMPLETED])
|
||||
->with(['template', 'answers.question'])
|
||||
->orderByDesc('created_at')
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($canCaptureAssessment) {
|
||||
$startableTemplates = AssessmentTemplate::query()
|
||||
->system()
|
||||
->current()
|
||||
->orderBy('category')
|
||||
->orderBy('name')
|
||||
->get()
|
||||
// Universal has a dedicated CTA; keep other templates in the general list.
|
||||
->reject(fn (AssessmentTemplate $t) => $t->code === 'universal_intake')
|
||||
->values();
|
||||
|
||||
if ($universalTemplate) {
|
||||
try {
|
||||
app(\App\Services\Care\AssessmentService::class)
|
||||
->assertCaptureAllowed($member, $universalTemplate);
|
||||
$canCaptureUniversal = true;
|
||||
} catch (\Throwable) {
|
||||
$canCaptureUniversal = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$pathwayService = app(PathwayService::class);
|
||||
$activePathways = $pathwayService->activeFor($consultation->patient)
|
||||
->load(['pathway.templates', 'assessments.template', 'assessments.score']);
|
||||
// Suggestions from **persisted** diagnoses only (after save).
|
||||
$pathwaySuggestions = $pathwayService->suggest(
|
||||
$consultation->patient,
|
||||
$consultation->diagnoses,
|
||||
);
|
||||
|
||||
// Instruments for active pathways (e.g. stroke → NIHSS, mRS) with draft/complete status.
|
||||
$pathwayInstruments = collect();
|
||||
foreach ($activePathways as $patientPathway) {
|
||||
foreach ($patientPathway->pathway->templates as $binding) {
|
||||
$template = AssessmentTemplate::currentSystemByCode($binding->template_code);
|
||||
$assessment = $patientPathway->assessments
|
||||
->first(fn (Assessment $a) => $a->template?->code === $binding->template_code)
|
||||
?? $consultationAssessments
|
||||
->first(fn (Assessment $a) => $a->template?->code === $binding->template_code)
|
||||
?? ($template
|
||||
? Assessment::query()
|
||||
->owned($this->ownerRef($request))
|
||||
->where('patient_id', $consultation->patient_id)
|
||||
->where('template_id', $template->id)
|
||||
->with(['template', 'score'])
|
||||
->orderByDesc('created_at')
|
||||
->first()
|
||||
: null);
|
||||
|
||||
$pathwayInstruments->push([
|
||||
'pathway_code' => $patientPathway->pathway->code,
|
||||
'pathway_name' => $patientPathway->pathway->name,
|
||||
'template_code' => $binding->template_code,
|
||||
'template_name' => $template?->name ?? $binding->template_code,
|
||||
'required' => $binding->is_required_on_activation,
|
||||
'phase' => $binding->phase,
|
||||
'template' => $template,
|
||||
'assessment' => $assessment,
|
||||
'pack_missing' => $template === null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$practitioners = Practitioner::owned($this->ownerRef($request))
|
||||
->where('organization_id', $this->organization($request)->id)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
|
||||
$investigationTypes = InvestigationType::owned($this->ownerRef($request))
|
||||
->where('organization_id', $this->organization($request)->id)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
@@ -65,6 +172,19 @@ class ConsultationController extends Controller
|
||||
'canPrescribe' => $canPrescribe,
|
||||
'canGenerateBill' => $canGenerateBill,
|
||||
'isCompleted' => $consultation->status === Consultation::STATUS_COMPLETED,
|
||||
'assessmentsEnabled' => $assessmentsEnabled,
|
||||
'canViewAssessments' => $canViewAssessments,
|
||||
'canCaptureAssessment' => $canCaptureAssessment,
|
||||
'consultationAssessments' => $consultationAssessments,
|
||||
'startableTemplates' => $startableTemplates,
|
||||
'assessmentStatuses' => config('care.assessment_statuses'),
|
||||
'universalTemplate' => $universalTemplate,
|
||||
'universalAssessment' => $universalAssessment,
|
||||
'canCaptureUniversal' => $canCaptureUniversal,
|
||||
'canManagePathways' => $canManagePathways,
|
||||
'pathwaySuggestions' => $pathwaySuggestions,
|
||||
'activePathways' => $activePathways,
|
||||
'pathwayInstruments' => $pathwayInstruments ?? collect(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -88,7 +208,16 @@ class ConsultationController extends Controller
|
||||
$this->ownerRef($request),
|
||||
);
|
||||
|
||||
return back()->with('success', 'Consultation saved.');
|
||||
$message = 'Consultation saved.';
|
||||
if ($canManage && $request->boolean('suggest_pathways')) {
|
||||
$consultation->load('diagnoses');
|
||||
$count = $consultation->diagnoses->filter(fn ($d) => filled($d->description) || filled($d->code))->count();
|
||||
$message = $count > 0
|
||||
? 'Diagnoses saved — review pathway suggestions below.'
|
||||
: 'Consultation saved. Add and save diagnoses to see pathway suggestions.';
|
||||
}
|
||||
|
||||
return back()->with('success', $message);
|
||||
}
|
||||
|
||||
public function complete(Request $request, Consultation $consultation): RedirectResponse
|
||||
|
||||
Reference in New Issue
Block a user