Deploy Ladill Care / deploy (push) Successful in 30s
Roster no longer writes temporary staff assignments; unit assignment UI drops shift fields and labels clarify the two workflows. Co-authored-by: Cursor <cursoragent@cursor.com>
197 lines
7.4 KiB
PHP
197 lines
7.4 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' => 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;
|
|
}
|
|
}
|