Deploy Ladill Care / deploy (push) Successful in 43s
Mirror Cardiology-depth stage flows, workspace tabs, reports/print, clinical alerts, demo seeds, and PHPUnit coverage for child health, fracture clinic, and ENT pathways. Co-authored-by: Cursor <cursoragent@cursor.com>
165 lines
6.3 KiB
PHP
165 lines
6.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Care\Ent;
|
|
|
|
use App\Models\Appointment;
|
|
use App\Models\Bill;
|
|
use App\Models\BillLineItem;
|
|
use App\Models\Organization;
|
|
use App\Models\SpecialtyClinicalRecord;
|
|
use App\Models\Visit;
|
|
use App\Services\Care\SpecialtyShellService;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Collection;
|
|
|
|
class EntAnalyticsService
|
|
{
|
|
public function __construct(
|
|
protected SpecialtyShellService $shell,
|
|
) {}
|
|
|
|
/**
|
|
* @return array{
|
|
* arrivals_today: int,
|
|
* completed_today: int,
|
|
* airway_open: int,
|
|
* diagnosis_breakdown: Collection,
|
|
* procedure_breakdown: Collection,
|
|
* avg_los_minutes: ?float,
|
|
* revenue_by_service: Collection,
|
|
* from: string,
|
|
* to: string
|
|
* }
|
|
*/
|
|
public function report(
|
|
Organization $organization,
|
|
string $ownerRef,
|
|
?int $branchId = null,
|
|
?string $from = null,
|
|
?string $to = null,
|
|
): array {
|
|
$todayStart = now()->startOfDay();
|
|
$rangeFrom = $from ? Carbon::parse($from)->startOfDay() : now()->startOfMonth();
|
|
$rangeTo = $to ? Carbon::parse($to)->endOfDay() : now()->endOfDay();
|
|
|
|
$departmentIds = $this->shell->departmentIds($organization, 'ent', $ownerRef, $branchId);
|
|
|
|
$appointmentBase = Appointment::owned($ownerRef)
|
|
->where('organization_id', $organization->id)
|
|
->when($departmentIds !== [], fn ($q) => $q->whereIn('department_id', $departmentIds))
|
|
->when($departmentIds === [], fn ($q) => $q->whereRaw('1 = 0'))
|
|
->when($branchId, fn ($q) => $q->where('branch_id', $branchId));
|
|
|
|
$arrivalsToday = (clone $appointmentBase)
|
|
->where('checked_in_at', '>=', $todayStart)
|
|
->count();
|
|
|
|
$completedToday = (clone $appointmentBase)
|
|
->where('status', Appointment::STATUS_COMPLETED)
|
|
->where('completed_at', '>=', $todayStart)
|
|
->count();
|
|
|
|
$visitIds = (clone $appointmentBase)->whereNotNull('visit_id')->pluck('visit_id');
|
|
|
|
$openVisitIds = Visit::owned($ownerRef)
|
|
->where('organization_id', $organization->id)
|
|
->whereIn('id', $visitIds->isEmpty() ? [0] : $visitIds)
|
|
->whereIn('status', [Visit::STATUS_OPEN, Visit::STATUS_IN_PROGRESS])
|
|
->pluck('id');
|
|
|
|
$airwayOpen = SpecialtyClinicalRecord::owned($ownerRef)
|
|
->where('organization_id', $organization->id)
|
|
->where('module_key', 'ent')
|
|
->where('record_type', 'ent_exam')
|
|
->whereIn('visit_id', $openVisitIds->isEmpty() ? [0] : $openVisitIds)
|
|
->get()
|
|
->filter(fn (SpecialtyClinicalRecord $r) => ! empty($r->payload['airway_concern']))
|
|
->count();
|
|
|
|
$planRecords = SpecialtyClinicalRecord::owned($ownerRef)
|
|
->where('organization_id', $organization->id)
|
|
->where('module_key', 'ent')
|
|
->where('record_type', 'ent_plan')
|
|
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
|
|
->whereBetween('recorded_at', [$rangeFrom, $rangeTo])
|
|
->get();
|
|
|
|
$diagnosisBreakdown = $planRecords
|
|
->groupBy(fn (SpecialtyClinicalRecord $r) => (string) ($r->payload['diagnosis'] ?? 'Unknown'))
|
|
->map(fn (Collection $rows, string $diagnosis) => [
|
|
'diagnosis' => $diagnosis,
|
|
'total' => $rows->count(),
|
|
])
|
|
->values()
|
|
->sortByDesc('total')
|
|
->values();
|
|
|
|
$procedureRecords = SpecialtyClinicalRecord::owned($ownerRef)
|
|
->where('organization_id', $organization->id)
|
|
->where('module_key', 'ent')
|
|
->where('record_type', 'ent_procedure')
|
|
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
|
|
->whereBetween('recorded_at', [$rangeFrom, $rangeTo])
|
|
->get();
|
|
|
|
$procedureBreakdown = $procedureRecords
|
|
->groupBy(fn (SpecialtyClinicalRecord $r) => (string) ($r->payload['procedure'] ?? 'Unknown'))
|
|
->map(fn (Collection $rows, string $procedure) => [
|
|
'procedure' => $procedure,
|
|
'total' => $rows->count(),
|
|
])
|
|
->values()
|
|
->sortByDesc('total')
|
|
->values();
|
|
|
|
$completedVisits = Visit::owned($ownerRef)
|
|
->where('organization_id', $organization->id)
|
|
->whereIn('id', $visitIds->isEmpty() ? [0] : $visitIds)
|
|
->where('status', Visit::STATUS_COMPLETED)
|
|
->whereBetween('completed_at', [$rangeFrom, $rangeTo])
|
|
->whereNotNull('checked_in_at')
|
|
->whereNotNull('completed_at')
|
|
->get();
|
|
|
|
$avgLos = null;
|
|
if ($completedVisits->isNotEmpty()) {
|
|
$avgLos = round($completedVisits->avg(function (Visit $visit) {
|
|
return $visit->checked_in_at->diffInMinutes($visit->completed_at);
|
|
}), 1);
|
|
}
|
|
|
|
$serviceLabels = collect($this->shell->provisionedServices($organization, 'ent'))
|
|
->pluck('label')
|
|
->filter()
|
|
->values()
|
|
->all();
|
|
|
|
$revenueByService = BillLineItem::query()
|
|
->where('owner_ref', $ownerRef)
|
|
->where('source_type', 'specialty_service')
|
|
->whereIn('description', $serviceLabels ?: ['__none__'])
|
|
->whereBetween('created_at', [$rangeFrom, $rangeTo])
|
|
->whereHas('bill', function ($q) use ($organization, $visitIds) {
|
|
$q->where('organization_id', $organization->id)
|
|
->whereIn('visit_id', $visitIds->isEmpty() ? [0] : $visitIds)
|
|
->whereNotIn('status', [Bill::STATUS_VOID]);
|
|
})
|
|
->selectRaw('description, count(*) as total, sum(total_minor) as amount_minor')
|
|
->groupBy('description')
|
|
->orderByDesc('amount_minor')
|
|
->get();
|
|
|
|
return [
|
|
'arrivals_today' => $arrivalsToday,
|
|
'completed_today' => $completedToday,
|
|
'airway_open' => $airwayOpen,
|
|
'diagnosis_breakdown' => $diagnosisBreakdown,
|
|
'procedure_breakdown' => $procedureBreakdown,
|
|
'avg_los_minutes' => $avgLos,
|
|
'revenue_by_service' => $revenueByService,
|
|
'from' => $rangeFrom->toDateString(),
|
|
'to' => $rangeTo->toDateString(),
|
|
];
|
|
}
|
|
}
|