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.
200 lines
6.8 KiB
PHP
200 lines
6.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Care;
|
|
|
|
use App\Models\ClinicalPathway;
|
|
use App\Models\Consultation;
|
|
use App\Models\Member;
|
|
use App\Models\Patient;
|
|
use App\Models\PatientPathway;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
|
|
class PathwayService
|
|
{
|
|
public function __construct(
|
|
protected PathwayMatcher $matcher,
|
|
protected AssessmentService $assessments,
|
|
protected CarePermissions $permissions,
|
|
) {}
|
|
|
|
/**
|
|
* @param iterable<\App\Models\Diagnosis|array{code?: string, description?: string}> $diagnoses
|
|
* @return Collection<int, array{pathway: ClinicalPathway, pathway_code: string, pathway_name: string, rank: int, match_reason: string, already_active: bool}>
|
|
*/
|
|
public function suggest(Patient $patient, iterable $diagnoses): Collection
|
|
{
|
|
$activeIds = $this->activeFor($patient)->pluck('pathway_id')->all();
|
|
|
|
return $this->matcher->match($diagnoses)->map(function (array $row) use ($activeIds) {
|
|
$row['already_active'] = in_array($row['pathway']->id, $activeIds, true);
|
|
|
|
return $row;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param array{
|
|
* consultation?: ?Consultation,
|
|
* activation_diagnosis_text?: ?string,
|
|
* actor?: ?string,
|
|
* notes?: ?string,
|
|
* } $context
|
|
*/
|
|
public function activate(
|
|
Patient $patient,
|
|
ClinicalPathway $pathway,
|
|
string $ownerRef,
|
|
?Member $member,
|
|
array $context = [],
|
|
): PatientPathway {
|
|
if (! $this->permissions->can($member, 'pathways.manage')) {
|
|
throw new HttpException(403, 'Not authorized to manage clinical pathways.');
|
|
}
|
|
|
|
if (! $pathway->is_active) {
|
|
throw ValidationException::withMessages([
|
|
'pathway_code' => 'Pathway is inactive.',
|
|
]);
|
|
}
|
|
|
|
$consultation = $context['consultation'] ?? null;
|
|
$actor = $context['actor'] ?? null;
|
|
$diagnosisText = $context['activation_diagnosis_text']
|
|
?? $this->snapshotDiagnosisText($consultation);
|
|
|
|
return DB::transaction(function () use ($patient, $pathway, $ownerRef, $member, $consultation, $actor, $diagnosisText, $context) {
|
|
$existingActive = PatientPathway::query()
|
|
->where('patient_id', $patient->id)
|
|
->where('pathway_id', $pathway->id)
|
|
->where('status', PatientPathway::STATUS_ACTIVE)
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if ($existingActive) {
|
|
return $existingActive->load(['pathway.templates', 'assessments.template']);
|
|
}
|
|
|
|
$patientPathway = PatientPathway::create([
|
|
'owner_ref' => $ownerRef,
|
|
'organization_id' => $patient->organization_id,
|
|
'patient_id' => $patient->id,
|
|
'pathway_id' => $pathway->id,
|
|
'status' => PatientPathway::STATUS_ACTIVE,
|
|
'activated_at' => now(),
|
|
'activated_by' => $actor,
|
|
'activation_consultation_id' => $consultation?->id,
|
|
'activation_diagnosis_text' => $diagnosisText ? mb_substr($diagnosisText, 0, 500) : null,
|
|
'notes' => $context['notes'] ?? null,
|
|
]);
|
|
|
|
$pathway->loadMissing('templates');
|
|
foreach ($pathway->templates->where('is_required_on_activation', true) as $binding) {
|
|
try {
|
|
$this->assessments->start(
|
|
$patient,
|
|
$binding->template_code,
|
|
$ownerRef,
|
|
$member,
|
|
[
|
|
'consultation' => $consultation,
|
|
'visit' => $consultation?->visit,
|
|
'patient_pathway' => $patientPathway,
|
|
'actor' => $actor,
|
|
],
|
|
);
|
|
} catch (ValidationException $e) {
|
|
// Template may not be seeded yet (content pack PR); skip missing packs.
|
|
if (isset($e->errors()['template_code'])) {
|
|
continue;
|
|
}
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
AuditLogger::record(
|
|
$ownerRef,
|
|
'pathway.activated',
|
|
$patient->organization_id,
|
|
$actor,
|
|
PatientPathway::class,
|
|
$patientPathway->id,
|
|
[
|
|
'pathway_code' => $pathway->code,
|
|
'activation_diagnosis_text' => $patientPathway->activation_diagnosis_text,
|
|
],
|
|
);
|
|
|
|
return $patientPathway->load(['pathway.templates', 'assessments.template']);
|
|
});
|
|
}
|
|
|
|
public function deactivate(
|
|
PatientPathway $patientPathway,
|
|
string $ownerRef,
|
|
?Member $member,
|
|
?string $actorRef = null,
|
|
): PatientPathway {
|
|
if (! $this->permissions->can($member, 'pathways.manage')) {
|
|
throw new HttpException(403, 'Not authorized to manage clinical pathways.');
|
|
}
|
|
|
|
if ($patientPathway->owner_ref !== $ownerRef) {
|
|
throw new HttpException(404, 'Pathway enrollment not found.');
|
|
}
|
|
|
|
if ($patientPathway->status !== PatientPathway::STATUS_ACTIVE) {
|
|
return $patientPathway;
|
|
}
|
|
|
|
$patientPathway->update([
|
|
'status' => PatientPathway::STATUS_INACTIVE,
|
|
'resolved_at' => now(),
|
|
]);
|
|
|
|
AuditLogger::record(
|
|
$ownerRef,
|
|
'pathway.deactivated',
|
|
$patientPathway->organization_id,
|
|
$actorRef,
|
|
PatientPathway::class,
|
|
$patientPathway->id,
|
|
['pathway_code' => $patientPathway->pathway?->code ?? $patientPathway->pathway_id],
|
|
);
|
|
|
|
return $patientPathway->fresh(['pathway']);
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, PatientPathway>
|
|
*/
|
|
public function activeFor(Patient $patient): Collection
|
|
{
|
|
return PatientPathway::query()
|
|
->where('patient_id', $patient->id)
|
|
->where('status', PatientPathway::STATUS_ACTIVE)
|
|
->with('pathway')
|
|
->orderByDesc('activated_at')
|
|
->get();
|
|
}
|
|
|
|
protected function snapshotDiagnosisText(?Consultation $consultation): ?string
|
|
{
|
|
if (! $consultation) {
|
|
return null;
|
|
}
|
|
|
|
$consultation->loadMissing('diagnoses');
|
|
if ($consultation->diagnoses->isEmpty()) {
|
|
return null;
|
|
}
|
|
|
|
return $consultation->diagnoses
|
|
->map(fn ($d) => trim(($d->code ? $d->code.' ' : '').$d->description))
|
|
->filter()
|
|
->implode('; ');
|
|
}
|
|
}
|