Files
ladill-care/app/Services/Care/ReportService.php
T
isaaccladandCursor 50f515cf6a
Deploy Ladill Care / deploy (push) Successful in 33s
Hide finance and admin KPIs from roles that lack them.
Doctors no longer see revenue, open bills, or org admin cards on the Care dashboard.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 22:32:41 +00:00

202 lines
7.6 KiB
PHP

<?php
namespace App\Services\Care;
use App\Models\Appointment;
use App\Models\Bill;
use App\Models\Diagnosis;
use App\Models\InvestigationRequest;
use App\Models\Patient;
use App\Models\Payment;
use App\Models\Visit;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class ReportService
{
/**
* @return array<string, mixed>
*/
public function dashboardStats(
string $ownerRef,
int $organizationId,
?int $branchId = null,
bool $includeBilling = true,
): array {
$today = now()->startOfDay();
$patientsToday = Patient::owned($ownerRef)
->where('organization_id', $organizationId)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereDate('created_at', $today)
->count();
$appointmentsToday = Appointment::owned($ownerRef)
->where('organization_id', $organizationId)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereDate('scheduled_at', $today)
->count();
$openBills = 0;
$revenueToday = 0;
if ($includeBilling) {
$openBills = Bill::owned($ownerRef)
->where('organization_id', $organizationId)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereIn('status', [Bill::STATUS_OPEN, Bill::STATUS_PARTIAL])
->count();
$revenueToday = Payment::owned($ownerRef)
->whereHas('bill', function (Builder $q) use ($organizationId, $branchId) {
$q->where('organization_id', $organizationId);
if ($branchId) {
$q->where('branch_id', $branchId);
}
})
->whereDate('paid_at', $today)
->sum('amount_minor');
}
$pendingLab = InvestigationRequest::owned($ownerRef)
->where('organization_id', $organizationId)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereIn('status', InvestigationRequest::activeStatuses())
->count();
return [
'patients_today' => $patientsToday,
'appointments_today' => $appointmentsToday,
'open_bills' => $openBills,
'revenue_today_minor' => (int) $revenueToday,
'pending_lab' => $pendingLab,
];
}
/**
* @return array<string, mixed>
*/
public function patientsReport(string $ownerRef, int $organizationId, Carbon $from, Carbon $to, ?int $branchId = null): array
{
$newPatients = Patient::owned($ownerRef)
->where('organization_id', $organizationId)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereBetween('created_at', [$from, $to])
->count();
$returningPatients = Visit::owned($ownerRef)
->where('organization_id', $organizationId)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereBetween('checked_in_at', [$from, $to])
->distinct('patient_id')
->count('patient_id');
$visits = Visit::owned($ownerRef)
->where('organization_id', $organizationId)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereBetween('checked_in_at', [$from, $to])
->count();
return [
'new_patients' => $newPatients,
'returning_patients' => $returningPatients,
'total_visits' => $visits,
];
}
/**
* @return array<string, mixed>
*/
public function appointmentsReport(string $ownerRef, int $organizationId, Carbon $from, Carbon $to, ?int $branchId = null): array
{
$base = Appointment::owned($ownerRef)
->where('organization_id', $organizationId)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereBetween('scheduled_at', [$from, $to]);
return [
'total' => (clone $base)->count(),
'completed' => (clone $base)->where('status', Appointment::STATUS_COMPLETED)->count(),
'cancelled' => (clone $base)->where('status', Appointment::STATUS_CANCELLED)->count(),
'no_show' => (clone $base)->where('status', Appointment::STATUS_NO_SHOW)->count(),
];
}
/**
* @return array<string, mixed>
*/
public function laboratoryReport(string $ownerRef, int $organizationId, Carbon $from, Carbon $to, ?int $branchId = null): array
{
$base = InvestigationRequest::owned($ownerRef)
->where('organization_id', $organizationId)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereBetween('created_at', [$from, $to]);
$completed = (clone $base)
->whereIn('status', [InvestigationRequest::STATUS_COMPLETED, InvestigationRequest::STATUS_DELIVERED])
->get();
$turnaroundHours = $completed
->filter(fn ($r) => $r->completed_at && $r->created_at)
->map(fn ($r) => $r->created_at->diffInHours($r->completed_at))
->avg();
return [
'requested' => (clone $base)->count(),
'completed' => $completed->count(),
'pending' => (clone $base)->whereIn('status', InvestigationRequest::activeStatuses())->count(),
'avg_turnaround_hours' => $turnaroundHours ? round($turnaroundHours, 1) : null,
];
}
/**
* @return array<string, mixed>
*/
public function financeReport(string $ownerRef, int $organizationId, Carbon $from, Carbon $to, ?int $branchId = null): array
{
$billed = Bill::owned($ownerRef)
->where('organization_id', $organizationId)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereBetween('created_at', [$from, $to])
->where('status', '!=', Bill::STATUS_VOID);
$payments = Payment::owned($ownerRef)
->whereHas('bill', function (Builder $q) use ($organizationId, $branchId) {
$q->where('organization_id', $organizationId);
if ($branchId) {
$q->where('branch_id', $branchId);
}
})
->whereBetween('paid_at', [$from, $to]);
return [
'invoiced_minor' => (int) (clone $billed)->sum('total_minor'),
'collected_minor' => (int) (clone $payments)->sum('amount_minor'),
'outstanding_minor' => (int) Bill::owned($ownerRef)
->where('organization_id', $organizationId)
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
->whereIn('status', [Bill::STATUS_OPEN, Bill::STATUS_PARTIAL])
->sum('balance_minor'),
'invoice_count' => (clone $billed)->count(),
];
}
/**
* @return Collection<int, object>
*/
public function clinicalReport(string $ownerRef, int $organizationId, Carbon $from, Carbon $to): Collection
{
return Diagnosis::owned($ownerRef)
->whereHas('consultation.visit', function (Builder $q) use ($organizationId, $from, $to) {
$q->where('organization_id', $organizationId)
->whereBetween('checked_in_at', [$from, $to]);
})
->select('description', DB::raw('count(*) as total'))
->groupBy('description')
->orderByDesc('total')
->limit(20)
->get();
}
}