Files
ladill-care/app/Services/Care/NursePerformanceService.php
isaaccladandCursor f1bb432324
Deploy Ladill Care / deploy (push) Successful in 2m12s
Include checked-in clinic visits on outpatient nursing boards.
OPD/service units list open department appointments without manual place; nursing actions soft-place the visit onto the unit. Inpatient wards stay placement-only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-20 17:05:13 +00:00

196 lines
7.3 KiB
PHP

<?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')
->unitPlacements()
->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' => app(CareUnitCensusService::class)
->visitsForUnit($unit, $ownerRef)
->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;
}
}