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,135 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Care;
|
||||
|
||||
use App\Models\ClinicalPathway;
|
||||
use App\Models\Diagnosis;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Match persisted diagnosis rows to clinical pathways (v1 algorithm).
|
||||
*/
|
||||
class PathwayMatcher
|
||||
{
|
||||
/**
|
||||
* @param iterable<Diagnosis|array{code?: ?string, description?: ?string}> $diagnoses
|
||||
* @return Collection<int, array{
|
||||
* pathway: ClinicalPathway,
|
||||
* pathway_code: string,
|
||||
* pathway_name: string,
|
||||
* rank: int,
|
||||
* match_reason: string
|
||||
* }>
|
||||
*/
|
||||
public function match(iterable $diagnoses): Collection
|
||||
{
|
||||
$dxList = collect($diagnoses)->map(function ($d) {
|
||||
if ($d instanceof Diagnosis) {
|
||||
return [
|
||||
'code' => $d->code,
|
||||
'description' => $d->description,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => $d['code'] ?? null,
|
||||
'description' => $d['description'] ?? null,
|
||||
];
|
||||
})->filter(fn ($d) => filled($d['code']) || filled($d['description']));
|
||||
|
||||
if ($dxList->isEmpty()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
$pathways = ClinicalPathway::query()
|
||||
->active()
|
||||
->orderBy('sort_order')
|
||||
->get();
|
||||
|
||||
$matches = [];
|
||||
|
||||
foreach ($pathways as $pathway) {
|
||||
$rules = $pathway->match_rules ?? [];
|
||||
$bestRank = null;
|
||||
$bestReason = null;
|
||||
|
||||
foreach ($dxList as $dx) {
|
||||
$result = $this->matchDiagnosis($dx, $rules);
|
||||
if ($result === null) {
|
||||
continue;
|
||||
}
|
||||
if ($bestRank === null || $result['rank'] > $bestRank) {
|
||||
$bestRank = $result['rank'];
|
||||
$bestReason = $result['reason'];
|
||||
}
|
||||
}
|
||||
|
||||
if ($bestRank !== null) {
|
||||
$matches[] = [
|
||||
'pathway' => $pathway,
|
||||
'pathway_code' => $pathway->code,
|
||||
'pathway_name' => $pathway->name,
|
||||
'rank' => $bestRank,
|
||||
'match_reason' => $bestReason,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return collect($matches)
|
||||
->sort(function ($a, $b) {
|
||||
if ($a['rank'] !== $b['rank']) {
|
||||
return $b['rank'] <=> $a['rank'];
|
||||
}
|
||||
|
||||
return $a['pathway']->sort_order <=> $b['pathway']->sort_order;
|
||||
})
|
||||
->values();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{code?: ?string, description?: ?string} $dx
|
||||
* @param array<string, mixed> $rules
|
||||
* @return array{rank: int, reason: string}|null
|
||||
*/
|
||||
protected function matchDiagnosis(array $dx, array $rules): ?array
|
||||
{
|
||||
$desc = $this->normalize((string) ($dx['description'] ?? ''));
|
||||
$codeStripped = $this->stripCode((string) ($dx['code'] ?? ''));
|
||||
|
||||
foreach ($rules['exclude_keywords'] ?? [] as $exclude) {
|
||||
$ex = $this->normalize((string) $exclude);
|
||||
if ($ex !== '' && $desc !== '' && str_contains($desc, $ex)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($rules['icd_prefixes'] ?? [] as $prefix) {
|
||||
$p = $this->stripCode((string) $prefix);
|
||||
if ($p !== '' && $codeStripped !== '' && str_starts_with($codeStripped, $p)) {
|
||||
return ['rank' => 100, 'reason' => 'icd_prefix:'.$prefix];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($rules['keywords'] ?? [] as $keyword) {
|
||||
$kw = $this->normalize((string) $keyword);
|
||||
if ($kw !== '' && $desc !== '' && str_contains($desc, $kw)) {
|
||||
return ['rank' => 50, 'reason' => 'keyword:'.$keyword];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function normalize(string $s): string
|
||||
{
|
||||
$s = mb_strtolower(trim($s));
|
||||
$s = preg_replace('/\s+/u', ' ', $s) ?? $s;
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
protected function stripCode(string $code): string
|
||||
{
|
||||
return strtoupper(preg_replace('/[^A-Za-z0-9]/', '', $code) ?? '');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user