Files
ladill-care/app/Services/Care/ReportService.php
T
isaaccladandCursor 6c9c742ed8
Deploy Ladill Care / deploy (push) Failing after 13s
Initial Ladill Care release.
Healthcare management app: patients, appointments, consultations, lab,
pharmacy inventory, billing, and reports at care.ladill.com.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 11:36:22 +00:00

194 lines
7.4 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): 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 = 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();
}
}