*/ 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) ->where('status', Payment::STATUS_PAID) ->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, ]; } /** * Today's payment collections for billing dashboards (cashiers / bills.view). * * @return array{ * collected_minor: int, * payment_count: int, * cash_minor: int, * other_minor: int * } */ public function todayPaymentStats( string $ownerRef, int $organizationId, ?int $branchId = null, ): array { $today = now()->startOfDay(); $base = Payment::owned($ownerRef) ->where('status', Payment::STATUS_PAID) ->whereHas('bill', function (Builder $q) use ($organizationId, $branchId) { $q->where('organization_id', $organizationId); if ($branchId) { $q->where('branch_id', $branchId); } }) ->whereDate('paid_at', $today); $collected = (int) (clone $base)->sum('amount_minor'); $count = (clone $base)->count(); $cash = (int) (clone $base)->where('method', Payment::METHOD_CASH)->sum('amount_minor'); return [ 'collected_minor' => $collected, 'payment_count' => $count, 'cash_minor' => $cash, 'other_minor' => max(0, $collected - $cash), ]; } /** * Recently paid invoices for billing dashboards. * * @return Collection */ public function recentPaidBills( string $ownerRef, int $organizationId, ?int $branchId = null, int $limit = 10, ): Collection { return Bill::owned($ownerRef) ->where('organization_id', $organizationId) ->when($branchId, fn ($q) => $q->where('branch_id', $branchId)) ->where('status', Bill::STATUS_PAID) ->with('patient') ->withMax([ 'payments as last_paid_at' => fn ($q) => $q->where('status', Payment::STATUS_PAID), ], 'paid_at') ->orderByDesc('last_paid_at') ->orderByDesc('id') ->limit($limit) ->get(); } /** * @return array */ 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 */ 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 */ 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 */ 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) ->where('status', Payment::STATUS_PAID) ->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 */ 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(); } /** * Org-level assessment analytics (Enterprise / assessment_analytics). * * @return array{ * summary: array, * by_template: Collection, * by_pathway: Collection, * score_averages: Collection * } */ public function assessmentsReport( string $ownerRef, int $organizationId, Carbon $from, Carbon $to, ?int $branchId = null, ): array { $base = Assessment::query() ->from('care_assessments') ->where('care_assessments.owner_ref', $ownerRef) ->where('care_assessments.organization_id', $organizationId) ->when($branchId, fn ($q) => $q->where('care_assessments.branch_id', $branchId)) ->whereBetween('care_assessments.created_at', [$from, $to]) ->whereNull('care_assessments.deleted_at'); $summary = [ 'total_started' => (clone $base)->count(), 'completed' => (clone $base)->where('care_assessments.status', Assessment::STATUS_COMPLETED)->count(), 'draft' => (clone $base)->where('care_assessments.status', Assessment::STATUS_DRAFT)->count(), 'cancelled' => (clone $base)->where('care_assessments.status', Assessment::STATUS_CANCELLED)->count(), 'patients_with_assessments' => (clone $base)->distinct('care_assessments.patient_id')->count('care_assessments.patient_id'), 'pathways_activated' => PatientPathway::query() ->where('care_patient_pathways.owner_ref', $ownerRef) ->where('care_patient_pathways.organization_id', $organizationId) ->whereBetween('care_patient_pathways.activated_at', [$from, $to]) ->whereNull('care_patient_pathways.deleted_at') ->count(), ]; $byTemplate = (clone $base) ->join('care_assessment_templates', 'care_assessments.template_id', '=', 'care_assessment_templates.id') ->select( 'care_assessment_templates.code as template_code', 'care_assessment_templates.name as template_name', DB::raw('count(*) as total'), DB::raw("sum(case when care_assessments.status = 'completed' then 1 else 0 end) as completed"), ) ->groupBy('care_assessment_templates.code', 'care_assessment_templates.name') ->orderByDesc('total') ->limit(30) ->get(); $byPathway = PatientPathway::query() ->from('care_patient_pathways') ->where('care_patient_pathways.owner_ref', $ownerRef) ->where('care_patient_pathways.organization_id', $organizationId) ->whereBetween('care_patient_pathways.activated_at', [$from, $to]) ->whereNull('care_patient_pathways.deleted_at') ->join('care_clinical_pathways', 'care_patient_pathways.pathway_id', '=', 'care_clinical_pathways.id') ->select( 'care_clinical_pathways.code as pathway_code', 'care_clinical_pathways.name as pathway_name', DB::raw('count(*) as total'), DB::raw("sum(case when care_patient_pathways.status = 'active' then 1 else 0 end) as still_active"), ) ->groupBy('care_clinical_pathways.code', 'care_clinical_pathways.name') ->orderByDesc('total') ->limit(20) ->get(); $scoreAverages = AssessmentScore::query() ->from('care_assessment_scores') ->where('care_assessment_scores.owner_ref', $ownerRef) ->whereHas('assessment', function (Builder $q) use ($organizationId, $branchId, $from, $to) { $q->where('care_assessments.organization_id', $organizationId) ->when($branchId, fn ($qq) => $qq->where('care_assessments.branch_id', $branchId)) ->whereBetween('care_assessments.completed_at', [$from, $to]); }) ->select( 'care_assessment_scores.template_code', DB::raw('count(*) as completions'), DB::raw('avg(care_assessment_scores.total_score) as avg_total'), DB::raw('avg(care_assessment_scores.max_score) as avg_max'), ) ->groupBy('care_assessment_scores.template_code') ->orderByDesc('completions') ->limit(20) ->get(); return [ 'summary' => $summary, 'by_template' => $byTemplate, 'by_pathway' => $byPathway, 'score_averages' => $scoreAverages, ]; } }