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.
133 lines
4.3 KiB
PHP
133 lines
4.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Care;
|
|
|
|
use App\Models\Assessment;
|
|
use App\Models\AssessmentScore;
|
|
use App\Models\AssessmentTemplate;
|
|
use App\Models\Patient;
|
|
use App\Models\VitalSign;
|
|
use Illuminate\Support\Collection;
|
|
|
|
/**
|
|
* Per-patient longitudinal trends (Layer 4) — free for all plans.
|
|
* Org-wide multi-patient analytics remain Enterprise (KD-18).
|
|
*/
|
|
class OutcomeTrendService
|
|
{
|
|
public const OUTCOME_TEMPLATE_CODE = 'outcome_core';
|
|
|
|
/**
|
|
* Completed outcome_core assessments newest-first with answer maps.
|
|
*
|
|
* @return Collection<int, array{assessment: Assessment, answers: array<string, mixed>, assessed_at: ?\Carbon\Carbon}>
|
|
*/
|
|
public function outcomeSeries(Patient $patient, string $ownerRef, ?int $branchScope = null, int $limit = 50): Collection
|
|
{
|
|
$template = AssessmentTemplate::currentSystemByCode(self::OUTCOME_TEMPLATE_CODE);
|
|
|
|
$query = Assessment::query()
|
|
->owned($ownerRef)
|
|
->where('patient_id', $patient->id)
|
|
->where('status', Assessment::STATUS_COMPLETED)
|
|
->with(['answers.question', 'template'])
|
|
->orderByDesc('assessed_at')
|
|
->orderByDesc('completed_at')
|
|
->limit($limit);
|
|
|
|
if ($template) {
|
|
$query->where('template_id', $template->id);
|
|
} else {
|
|
$query->whereHas('template', fn ($q) => $q->where('code', self::OUTCOME_TEMPLATE_CODE));
|
|
}
|
|
|
|
if ($branchScope !== null) {
|
|
$query->where('branch_id', $branchScope);
|
|
}
|
|
|
|
return $query->get()->map(function (Assessment $assessment) {
|
|
$answers = [];
|
|
foreach ($assessment->answers as $answer) {
|
|
if ($answer->question) {
|
|
$answers[$answer->question->code] = $answer->authoritativeValue($answer->question->answer_type);
|
|
}
|
|
}
|
|
|
|
return [
|
|
'assessment' => $assessment,
|
|
'answers' => $answers,
|
|
'assessed_at' => $assessment->assessed_at ?? $assessment->completed_at,
|
|
];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Recent instrument scores for chartable disease templates.
|
|
*
|
|
* @return Collection<int, AssessmentScore>
|
|
*/
|
|
public function instrumentScores(
|
|
Patient $patient,
|
|
string $ownerRef,
|
|
?int $branchScope = null,
|
|
?string $templateCode = null,
|
|
int $limit = 50,
|
|
): Collection {
|
|
$query = AssessmentScore::query()
|
|
->owned($ownerRef)
|
|
->whereHas('assessment', function ($q) use ($patient, $branchScope) {
|
|
$q->where('patient_id', $patient->id)
|
|
->where('status', Assessment::STATUS_COMPLETED)
|
|
->when($branchScope !== null, fn ($qq) => $qq->where('branch_id', $branchScope));
|
|
})
|
|
->with(['assessment.template'])
|
|
->orderByDesc('computed_at')
|
|
->limit($limit);
|
|
|
|
if ($templateCode) {
|
|
$query->where('template_code', $templateCode);
|
|
}
|
|
|
|
return $query->get();
|
|
}
|
|
|
|
/**
|
|
* Latest vitals from consultations (typed path — not outcome form).
|
|
*
|
|
* @return Collection<int, VitalSign>
|
|
*/
|
|
public function recentVitals(Patient $patient, string $ownerRef, int $limit = 10): Collection
|
|
{
|
|
return VitalSign::query()
|
|
->where('owner_ref', $ownerRef)
|
|
->whereHas('consultation', fn ($q) => $q->where('patient_id', $patient->id))
|
|
->orderByDesc('recorded_at')
|
|
->limit($limit)
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* Sparkline-friendly series for a single outcome numeric code (oldest → newest).
|
|
*
|
|
* @return list<array{date: string, value: float|int|null}>
|
|
*/
|
|
public function numericOutcomeSeries(Patient $patient, string $ownerRef, string $questionCode, ?int $branchScope = null): array
|
|
{
|
|
$rows = $this->outcomeSeries($patient, $ownerRef, $branchScope)->reverse()->values();
|
|
|
|
$out = [];
|
|
foreach ($rows as $row) {
|
|
$val = $row['answers'][$questionCode] ?? null;
|
|
if ($val === null || $val === '') {
|
|
continue;
|
|
}
|
|
$out[] = [
|
|
'date' => $row['assessed_at']?->format('Y-m-d') ?? '',
|
|
'value' => is_numeric($val) ? $val + 0 : null,
|
|
];
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
}
|