Add nursing assessment packs, ward board, and nurse performance KPIs.
Deploy Ladill Care / deploy (push) Successful in 55s
Deploy Ladill Care / deploy (push) Successful in 55s
Seeds NEWS2/Braden/Morse/pain packs with visit vitals and a unit dashboard for MAR, assessments, notes, and handover activity. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\CareUnit;
|
||||
use App\Services\Care\CarePermissions;
|
||||
use App\Services\Care\NursePerformanceService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class NursePerformanceController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function show(Request $request, CareUnit $careUnit, NursePerformanceService $performance): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'nursing.performance.view');
|
||||
$this->authorizeOwner($request, $careUnit);
|
||||
abort_unless((int) $careUnit->organization_id === (int) $this->organization($request)->id, 404);
|
||||
|
||||
$from = $request->filled('from')
|
||||
? Carbon::parse($request->input('from'))->startOfDay()
|
||||
: now()->subDays(7)->startOfDay();
|
||||
$to = $request->filled('to')
|
||||
? Carbon::parse($request->input('to'))->endOfDay()
|
||||
: now()->endOfDay();
|
||||
|
||||
$report = $performance->unitReport($careUnit, $this->ownerRef($request), $from, $to);
|
||||
|
||||
return view('care.nursing.performance', [
|
||||
'unit' => $careUnit->load('department.branch'),
|
||||
'report' => $report,
|
||||
'canManage' => app(CarePermissions::class)->can($this->member($request), 'admin.departments.manage'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\CareUnit;
|
||||
use App\Models\Visit;
|
||||
use App\Services\Care\AssessmentService;
|
||||
use App\Services\Care\CareFeatures;
|
||||
use App\Services\Care\CarePermissions;
|
||||
use App\Services\Care\NursingAssessmentService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class NursingAssessmentController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function unitBoard(Request $request, CareUnit $careUnit, NursingAssessmentService $nursing): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'nursing.assessments.view');
|
||||
$this->authorizeOwner($request, $careUnit);
|
||||
abort_unless((int) $careUnit->organization_id === (int) $this->organization($request)->id, 404);
|
||||
|
||||
app(CareFeatures::class)->ensureAssessmentCatalog();
|
||||
|
||||
$permissions = app(CarePermissions::class);
|
||||
$member = $this->member($request);
|
||||
|
||||
return view('care.nursing.assessments-board', [
|
||||
'unit' => $careUnit->load('department.branch'),
|
||||
'templates' => $nursing->nursingTemplates(),
|
||||
'rows' => $nursing->unitBoard($careUnit, $this->ownerRef($request)),
|
||||
'canCapture' => $permissions->can($member, 'nursing.assessments.manage')
|
||||
|| $permissions->can($member, 'assessments.capture')
|
||||
|| $permissions->can($member, 'assessments.manage'),
|
||||
'canVitals' => $permissions->can($member, 'vitals.manage'),
|
||||
'engineEnabled' => app(CareFeatures::class)->enabled(
|
||||
$this->organization($request),
|
||||
CareFeatures::ASSESSMENTS_ENGINE,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
public function start(
|
||||
Request $request,
|
||||
CareUnit $careUnit,
|
||||
Visit $visit,
|
||||
AssessmentService $assessments,
|
||||
CareFeatures $features,
|
||||
): RedirectResponse {
|
||||
$this->authorizeAbility($request, 'nursing.assessments.manage');
|
||||
$this->authorizeOwner($request, $careUnit);
|
||||
$this->authorizeOwner($request, $visit);
|
||||
abort_unless((int) $visit->care_unit_id === (int) $careUnit->id, 404);
|
||||
abort_unless($features->enabled($this->organization($request), CareFeatures::ASSESSMENTS_ENGINE), 404);
|
||||
|
||||
$features->ensureAssessmentCatalog();
|
||||
|
||||
$validated = $request->validate([
|
||||
'template_code' => ['required', 'string', 'max:100'],
|
||||
]);
|
||||
|
||||
$assessment = $assessments->start(
|
||||
$visit->patient,
|
||||
$validated['template_code'],
|
||||
$this->ownerRef($request),
|
||||
$this->member($request),
|
||||
[
|
||||
'visit' => $visit,
|
||||
'actor' => $this->actorRef($request),
|
||||
],
|
||||
);
|
||||
|
||||
return redirect()
|
||||
->route('care.assessments.show', $assessment)
|
||||
->with('success', 'Nursing assessment started.');
|
||||
}
|
||||
|
||||
public function storeVitals(
|
||||
Request $request,
|
||||
CareUnit $careUnit,
|
||||
Visit $visit,
|
||||
NursingAssessmentService $nursing,
|
||||
): RedirectResponse {
|
||||
$this->authorizeAbility($request, 'vitals.manage');
|
||||
$this->authorizeOwner($request, $careUnit);
|
||||
$this->authorizeOwner($request, $visit);
|
||||
abort_unless((int) $visit->care_unit_id === (int) $careUnit->id, 404);
|
||||
|
||||
$validated = $request->validate([
|
||||
'bp_systolic' => ['nullable', 'integer', 'min:40', 'max:300'],
|
||||
'bp_diastolic' => ['nullable', 'integer', 'min:20', 'max:200'],
|
||||
'pulse' => ['nullable', 'integer', 'min:20', 'max:300'],
|
||||
'temperature' => ['nullable', 'numeric', 'min:30', 'max:45'],
|
||||
'spo2' => ['nullable', 'integer', 'min:50', 'max:100'],
|
||||
'respiratory_rate' => ['nullable', 'integer', 'min:4', 'max:60'],
|
||||
'notes' => ['nullable', 'string', 'max:1000'],
|
||||
]);
|
||||
|
||||
$nursing->recordVisitVitals(
|
||||
$visit,
|
||||
$this->ownerRef($request),
|
||||
$this->actorRef($request),
|
||||
$validated,
|
||||
);
|
||||
|
||||
return redirect()
|
||||
->route('care.care-units.assessments', $careUnit)
|
||||
->with('success', 'Ward vitals recorded.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\BelongsToOwner;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class VisitVital extends Model
|
||||
{
|
||||
use BelongsToOwner;
|
||||
|
||||
protected $table = 'care_visit_vitals';
|
||||
|
||||
protected $fillable = [
|
||||
'owner_ref',
|
||||
'organization_id',
|
||||
'visit_id',
|
||||
'patient_id',
|
||||
'care_unit_id',
|
||||
'bp_systolic',
|
||||
'bp_diastolic',
|
||||
'pulse',
|
||||
'temperature',
|
||||
'spo2',
|
||||
'respiratory_rate',
|
||||
'recorded_by',
|
||||
'recorded_at',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'temperature' => 'decimal:1',
|
||||
'recorded_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function visit(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Visit::class, 'visit_id');
|
||||
}
|
||||
|
||||
public function patient(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Patient::class, 'patient_id');
|
||||
}
|
||||
|
||||
public function careUnit(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(CareUnit::class, 'care_unit_id');
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Services\Care;
|
||||
|
||||
use App\Models\AssessmentTemplate;
|
||||
use App\Models\ClinicalPathway;
|
||||
use App\Models\Organization;
|
||||
use Database\Seeders\AssessmentTemplateSeeder;
|
||||
use Database\Seeders\ClinicalPathwaySeeder;
|
||||
@@ -72,16 +73,24 @@ class CareFeatures
|
||||
return;
|
||||
}
|
||||
|
||||
if (AssessmentTemplate::query()->whereNull('organization_id')->exists()) {
|
||||
return;
|
||||
$nursingCodes = ['news2', 'braden', 'morse', 'pain_nrs'];
|
||||
$haveNursing = AssessmentTemplate::query()
|
||||
->whereNull('organization_id')
|
||||
->whereIn('code', $nursingCodes)
|
||||
->where('is_current', true)
|
||||
->count();
|
||||
|
||||
$empty = ! AssessmentTemplate::query()->whereNull('organization_id')->exists();
|
||||
|
||||
if ($empty || $haveNursing < count($nursingCodes)) {
|
||||
Artisan::call('db:seed', [
|
||||
'--class' => AssessmentTemplateSeeder::class,
|
||||
'--force' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
Artisan::call('db:seed', [
|
||||
'--class' => AssessmentTemplateSeeder::class,
|
||||
'--force' => true,
|
||||
]);
|
||||
|
||||
if (Schema::hasTable('care_clinical_pathways')) {
|
||||
if (Schema::hasTable('care_clinical_pathways')
|
||||
&& ! ClinicalPathway::query()->whereNull('organization_id')->exists()) {
|
||||
Artisan::call('db:seed', [
|
||||
'--class' => ClinicalPathwaySeeder::class,
|
||||
'--force' => true,
|
||||
|
||||
@@ -590,6 +590,9 @@ class CarePermissions
|
||||
$abilities[] = 'nursing.notes.manage';
|
||||
$abilities[] = 'nursing.handover.view';
|
||||
$abilities[] = 'nursing.handover.manage';
|
||||
$abilities[] = 'nursing.assessments.view';
|
||||
$abilities[] = 'nursing.assessments.manage';
|
||||
$abilities[] = 'nursing.performance.view';
|
||||
}
|
||||
|
||||
if (in_array('analytics.department.view', $abilities, true)) {
|
||||
@@ -597,6 +600,8 @@ class CarePermissions
|
||||
$abilities[] = 'mar.view';
|
||||
$abilities[] = 'nursing.notes.view';
|
||||
$abilities[] = 'nursing.handover.view';
|
||||
$abilities[] = 'nursing.assessments.view';
|
||||
$abilities[] = 'nursing.performance.view';
|
||||
}
|
||||
|
||||
return array_values(array_unique($abilities));
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Care;
|
||||
|
||||
use App\Models\Assessment;
|
||||
use App\Models\CareUnit;
|
||||
use App\Models\MarAdministration;
|
||||
use App\Models\NursingNote;
|
||||
use App\Models\StaffAssignment;
|
||||
use App\Models\User;
|
||||
use App\Models\Visit;
|
||||
use App\Models\VisitVital;
|
||||
use App\Models\WardHandover;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class NursePerformanceService
|
||||
{
|
||||
/**
|
||||
* Aggregate nursing KPIs for a care unit over a date range.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function unitReport(
|
||||
CareUnit $unit,
|
||||
string $ownerRef,
|
||||
?CarbonInterface $from = null,
|
||||
?CarbonInterface $to = null,
|
||||
): array {
|
||||
$from = Carbon::parse($from ?? now()->subDays(7))->startOfDay();
|
||||
$to = Carbon::parse($to ?? now())->endOfDay();
|
||||
|
||||
$visitIds = Visit::owned($ownerRef)
|
||||
->where('care_unit_id', $unit->id)
|
||||
->pluck('id');
|
||||
|
||||
$mar = MarAdministration::owned($ownerRef)
|
||||
->where('care_unit_id', $unit->id)
|
||||
->whereBetween('recorded_at', [$from, $to])
|
||||
->get();
|
||||
|
||||
$marGiven = $mar->where('status', MarAdministration::STATUS_GIVEN)->count();
|
||||
$marHeld = $mar->where('status', MarAdministration::STATUS_HELD)->count();
|
||||
$marRefused = $mar->where('status', MarAdministration::STATUS_REFUSED)->count();
|
||||
$marMissed = $mar->where('status', MarAdministration::STATUS_MISSED)->count();
|
||||
$marTotal = $mar->count();
|
||||
$marOnTime = $mar->filter(function (MarAdministration $a) {
|
||||
if ($a->status !== MarAdministration::STATUS_GIVEN || ! $a->scheduled_for) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $a->recorded_at->between(
|
||||
$a->scheduled_for->copy()->subMinutes(30),
|
||||
$a->scheduled_for->copy()->addMinutes(60),
|
||||
);
|
||||
})->count();
|
||||
|
||||
$notes = NursingNote::owned($ownerRef)
|
||||
->where('care_unit_id', $unit->id)
|
||||
->whereBetween('recorded_at', [$from, $to])
|
||||
->get();
|
||||
|
||||
$vitals = VisitVital::owned($ownerRef)
|
||||
->where('care_unit_id', $unit->id)
|
||||
->whereBetween('recorded_at', [$from, $to])
|
||||
->count();
|
||||
|
||||
$nursingCodes = NursingAssessmentService::NURSING_PACK_CODES;
|
||||
$assessments = Assessment::owned($ownerRef)
|
||||
->whereIn('visit_id', $visitIds->isEmpty() ? [0] : $visitIds->all())
|
||||
->whereBetween('created_at', [$from, $to])
|
||||
->whereHas('template', fn ($q) => $q->whereIn('code', $nursingCodes))
|
||||
->with('template')
|
||||
->get();
|
||||
|
||||
$assessmentsCompleted = $assessments->where('status', Assessment::STATUS_COMPLETED)->count();
|
||||
$assessmentsDraft = $assessments->where('status', Assessment::STATUS_DRAFT)->count();
|
||||
|
||||
$handovers = WardHandover::owned($ownerRef)
|
||||
->where('care_unit_id', $unit->id)
|
||||
->whereBetween('created_at', [$from, $to])
|
||||
->get();
|
||||
|
||||
$assignedStaff = StaffAssignment::owned($ownerRef)
|
||||
->where('care_unit_id', $unit->id)
|
||||
->where('status', 'active')
|
||||
->effectiveOn($to)
|
||||
->with('member')
|
||||
->get();
|
||||
|
||||
$byActor = $this->byActor(
|
||||
$mar,
|
||||
$notes,
|
||||
$assessments->where('status', Assessment::STATUS_COMPLETED),
|
||||
$handovers->where('status', WardHandover::STATUS_COMPLETED),
|
||||
);
|
||||
|
||||
return [
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'placed_patients' => Visit::owned($ownerRef)
|
||||
->where('care_unit_id', $unit->id)
|
||||
->whereIn('status', [Visit::STATUS_OPEN, Visit::STATUS_IN_PROGRESS])
|
||||
->count(),
|
||||
'mar' => [
|
||||
'total' => $marTotal,
|
||||
'given' => $marGiven,
|
||||
'held' => $marHeld,
|
||||
'refused' => $marRefused,
|
||||
'missed' => $marMissed,
|
||||
'on_time_given' => $marOnTime,
|
||||
'on_time_rate' => $marGiven > 0 ? round(($marOnTime / $marGiven) * 100, 1) : null,
|
||||
'compliance_rate' => $marTotal > 0
|
||||
? round((($marGiven) / $marTotal) * 100, 1)
|
||||
: null,
|
||||
],
|
||||
'notes' => [
|
||||
'total' => $notes->count(),
|
||||
'by_type' => $notes->groupBy('note_type')->map->count()->all(),
|
||||
],
|
||||
'vitals' => [
|
||||
'total' => $vitals,
|
||||
],
|
||||
'assessments' => [
|
||||
'started' => $assessments->count(),
|
||||
'completed' => $assessmentsCompleted,
|
||||
'draft' => $assessmentsDraft,
|
||||
'completion_rate' => $assessments->count() > 0
|
||||
? round(($assessmentsCompleted / $assessments->count()) * 100, 1)
|
||||
: null,
|
||||
'by_template' => $assessments
|
||||
->groupBy(fn (Assessment $a) => $a->template?->code ?? 'unknown')
|
||||
->map(fn (Collection $group, $code) => [
|
||||
'code' => $code,
|
||||
'name' => $group->first()->template?->name ?? $code,
|
||||
'completed' => $group->where('status', Assessment::STATUS_COMPLETED)->count(),
|
||||
'total' => $group->count(),
|
||||
])
|
||||
->values()
|
||||
->all(),
|
||||
],
|
||||
'handovers' => [
|
||||
'total' => $handovers->count(),
|
||||
'completed' => $handovers->where('status', WardHandover::STATUS_COMPLETED)->count(),
|
||||
],
|
||||
'assigned_staff' => $assignedStaff->count(),
|
||||
'by_actor' => $byActor,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection<int, MarAdministration> $mar
|
||||
* @param Collection<int, NursingNote> $notes
|
||||
* @param Collection<int, Assessment> $assessments
|
||||
* @param Collection<int, WardHandover> $handovers
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
protected function byActor(
|
||||
Collection $mar,
|
||||
Collection $notes,
|
||||
Collection $assessments,
|
||||
Collection $handovers,
|
||||
): array {
|
||||
$refs = collect()
|
||||
->merge($mar->pluck('administered_by'))
|
||||
->merge($notes->pluck('recorded_by'))
|
||||
->merge($assessments->pluck('completed_by'))
|
||||
->merge($handovers->pluck('handed_over_by'))
|
||||
->filter()
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
$emails = User::query()->whereIn('public_id', $refs)->pluck('email', 'public_id');
|
||||
$names = User::query()->whereIn('public_id', $refs)->pluck('name', 'public_id');
|
||||
|
||||
$rows = [];
|
||||
foreach ($refs as $ref) {
|
||||
$rows[] = [
|
||||
'actor_ref' => $ref,
|
||||
'label' => $names[$ref] ?? $emails[$ref] ?? $ref,
|
||||
'mar_given' => $mar->where('administered_by', $ref)->where('status', MarAdministration::STATUS_GIVEN)->count(),
|
||||
'mar_total' => $mar->where('administered_by', $ref)->count(),
|
||||
'notes' => $notes->where('recorded_by', $ref)->count(),
|
||||
'assessments_completed' => $assessments->where('completed_by', $ref)->count(),
|
||||
'handovers' => $handovers->where('handed_over_by', $ref)->count(),
|
||||
];
|
||||
}
|
||||
|
||||
usort($rows, fn ($a, $b) => ($b['mar_total'] + $b['notes'] + $b['assessments_completed'])
|
||||
<=> ($a['mar_total'] + $a['notes'] + $a['assessments_completed']));
|
||||
|
||||
return $rows;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Care;
|
||||
|
||||
use App\Models\Assessment;
|
||||
use App\Models\AssessmentTemplate;
|
||||
use App\Models\CareUnit;
|
||||
use App\Models\Visit;
|
||||
use App\Models\VisitVital;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class NursingAssessmentService
|
||||
{
|
||||
/** @var list<string> */
|
||||
public const NURSING_PACK_CODES = ['news2', 'braden', 'morse', 'pain_nrs'];
|
||||
|
||||
/**
|
||||
* @return Collection<int, AssessmentTemplate>
|
||||
*/
|
||||
public function nursingTemplates(): Collection
|
||||
{
|
||||
return AssessmentTemplate::query()
|
||||
->whereNull('organization_id')
|
||||
->where('is_current', true)
|
||||
->where('is_active', true)
|
||||
->whereIn('code', self::NURSING_PACK_CODES)
|
||||
->orderBy('name')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Board rows for placed patients: latest nursing assessments + latest vitals.
|
||||
*
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function unitBoard(CareUnit $unit, string $ownerRef): array
|
||||
{
|
||||
$templates = $this->nursingTemplates();
|
||||
$visits = Visit::owned($ownerRef)
|
||||
->where('care_unit_id', $unit->id)
|
||||
->whereIn('status', [Visit::STATUS_OPEN, Visit::STATUS_IN_PROGRESS])
|
||||
->with(['patient', 'bed'])
|
||||
->orderBy('placed_at')
|
||||
->get();
|
||||
|
||||
$rows = [];
|
||||
foreach ($visits as $visit) {
|
||||
$latestByCode = Assessment::owned($ownerRef)
|
||||
->where('visit_id', $visit->id)
|
||||
->whereIn('status', [Assessment::STATUS_DRAFT, Assessment::STATUS_COMPLETED])
|
||||
->whereHas('template', fn ($q) => $q->whereIn('code', $templates->pluck('code')->all()))
|
||||
->with(['template', 'score'])
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->unique(fn (Assessment $a) => $a->template?->code)
|
||||
->keyBy(fn (Assessment $a) => $a->template?->code);
|
||||
|
||||
$latestVitals = VisitVital::owned($ownerRef)
|
||||
->where('visit_id', $visit->id)
|
||||
->orderByDesc('recorded_at')
|
||||
->first();
|
||||
|
||||
$rows[] = [
|
||||
'visit' => $visit,
|
||||
'patient' => $visit->patient,
|
||||
'bed' => $visit->bed,
|
||||
'assessments' => $latestByCode,
|
||||
'vitals' => $latestVitals,
|
||||
];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
public function recordVisitVitals(
|
||||
Visit $visit,
|
||||
string $ownerRef,
|
||||
?string $actorRef,
|
||||
array $data,
|
||||
): VisitVital {
|
||||
$vital = VisitVital::create([
|
||||
'owner_ref' => $ownerRef,
|
||||
'organization_id' => $visit->organization_id,
|
||||
'visit_id' => $visit->id,
|
||||
'patient_id' => $visit->patient_id,
|
||||
'care_unit_id' => $visit->care_unit_id,
|
||||
'bp_systolic' => $data['bp_systolic'] ?? null,
|
||||
'bp_diastolic' => $data['bp_diastolic'] ?? null,
|
||||
'pulse' => $data['pulse'] ?? null,
|
||||
'temperature' => $data['temperature'] ?? null,
|
||||
'spo2' => $data['spo2'] ?? null,
|
||||
'respiratory_rate' => $data['respiratory_rate'] ?? null,
|
||||
'notes' => $data['notes'] ?? null,
|
||||
'recorded_by' => $actorRef,
|
||||
'recorded_at' => now(),
|
||||
]);
|
||||
|
||||
AuditLogger::record(
|
||||
$ownerRef,
|
||||
'visit_vitals.recorded',
|
||||
$visit->organization_id,
|
||||
$actorRef,
|
||||
VisitVital::class,
|
||||
$vital->id,
|
||||
);
|
||||
|
||||
return $vital;
|
||||
}
|
||||
}
|
||||
@@ -80,16 +80,48 @@ class ScoringService
|
||||
}
|
||||
|
||||
if (str_starts_with($strategy, 'custom:')) {
|
||||
return $this->customScore($assessment, substr($strategy, 7));
|
||||
$result = $this->customScore($assessment, substr($strategy, 7));
|
||||
} else {
|
||||
$result = match ($strategy) {
|
||||
'sum_items' => $this->sumItems($assessment),
|
||||
'single_value' => $this->singleValue($assessment),
|
||||
default => throw ValidationException::withMessages([
|
||||
'scoring' => "Unknown scoring strategy [{$strategy}].",
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
return match ($strategy) {
|
||||
'sum_items' => $this->sumItems($assessment),
|
||||
'single_value' => $this->singleValue($assessment),
|
||||
default => throw ValidationException::withMessages([
|
||||
'scoring' => "Unknown scoring strategy [{$strategy}].",
|
||||
]),
|
||||
};
|
||||
$result['severity_label'] = $result['severity_label']
|
||||
?? $this->severityFromBands($assessment->template?->meta, $result['total'] ?? null);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $meta
|
||||
*/
|
||||
protected function severityFromBands(?array $meta, mixed $total): ?string
|
||||
{
|
||||
if ($total === null || ! is_numeric($total)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$bands = $meta['severity_bands'] ?? null;
|
||||
if (! is_array($bands) || $bands === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$score = (float) $total;
|
||||
foreach ($bands as $band) {
|
||||
if (! is_array($band) || ! isset($band['max'], $band['label'])) {
|
||||
continue;
|
||||
}
|
||||
if ($score <= (float) $band['max']) {
|
||||
return (string) $band['label'];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user