Deploy Ladill Care / deploy (push) Failing after 13s
Healthcare management app: patients, appointments, consultations, lab, pharmacy inventory, billing, and reports at care.ladill.com. Co-authored-by: Cursor <cursoragent@cursor.com>
194 lines
7.4 KiB
PHP
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();
|
|
}
|
|
}
|