startOfDay(); $rangeFrom = $from ? Carbon::parse($from)->startOfDay() : now()->startOfMonth(); $rangeTo = $to ? Carbon::parse($to)->endOfDay() : now()->endOfDay(); $procBase = DentalProcedure::owned($ownerRef) ->where('organization_id', $organization->id) ->where('status', DentalProcedure::STATUS_COMPLETED); $proceduresToday = (clone $procBase) ->where('performed_at', '>=', $todayStart) ->selectRaw('procedure_code, label, count(*) as total, sum(amount_minor) as amount_minor') ->groupBy('procedure_code', 'label') ->orderByDesc('total') ->get(); $proceduresMonth = (clone $procBase) ->whereBetween('performed_at', [$rangeFrom, $rangeTo]) ->selectRaw('procedure_code, label, count(*) as total, sum(amount_minor) as amount_minor') ->groupBy('procedure_code', 'label') ->orderByDesc('total') ->get(); $revenueByCode = BillLineItem::query() ->where('owner_ref', $ownerRef) ->where('source_type', 'dental_procedure') ->whereBetween('created_at', [$rangeFrom, $rangeTo]) ->selectRaw('description, sum(total_minor) as amount_minor, count(*) as total') ->groupBy('description') ->orderByDesc('amount_minor') ->limit(20) ->get(); $unfinished = DentalPlanItem::query() ->whereHas('plan', function ($q) use ($organization, $ownerRef) { $q->owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('status', [ DentalTreatmentPlan::STATUS_ACCEPTED, DentalTreatmentPlan::STATUS_IN_PROGRESS, ]); }) ->whereIn('status', [DentalPlanItem::STATUS_ACCEPTED, DentalPlanItem::STATUS_PROPOSED]) ->count(); $chairQuery = Appointment::owned($ownerRef) ->where('organization_id', $organization->id) ->where('status', Appointment::STATUS_COMPLETED) ->whereNotNull('started_at') ->whereNotNull('completed_at') ->whereBetween('completed_at', [$rangeFrom, $rangeTo]) ->when($branchId, fn ($q) => $q->where('branch_id', $branchId)); $avgChair = null; $samples = (clone $chairQuery)->limit(200)->get(['started_at', 'completed_at']); if ($samples->isNotEmpty()) { $avgChair = round($samples->avg(fn ($a) => $a->started_at->diffInMinutes($a->completed_at)), 1); } $imaging = DentalImage::owned($ownerRef) ->where('organization_id', $organization->id) ->whereBetween('captured_at', [$rangeFrom, $rangeTo]) ->selectRaw('modality, count(*) as total') ->groupBy('modality') ->orderByDesc('total') ->get(); return [ 'procedures_today' => $proceduresToday, 'procedures_month' => $proceduresMonth, 'revenue_by_code' => $revenueByCode, 'unfinished_items' => $unfinished, 'avg_chair_minutes' => $avgChair, 'imaging_by_modality' => $imaging, 'from' => $rangeFrom->toDateString(), 'to' => $rangeTo->toDateString(), ]; } /** * @return list */ public function alertsForPatient(Organization $organization, Patient $patient, string $ownerRef): array { $alerts = []; $plan = DentalTreatmentPlan::owned($ownerRef) ->where('organization_id', $organization->id) ->where('patient_id', $patient->id) ->whereIn('status', [ DentalTreatmentPlan::STATUS_ACCEPTED, DentalTreatmentPlan::STATUS_IN_PROGRESS, ]) ->with('items') ->latest('id') ->first(); if ($plan && $plan->accepted_at && $plan->accepted_at->lt(now()->subDays(30))) { $open = $plan->items->whereIn('status', [ DentalPlanItem::STATUS_ACCEPTED, DentalPlanItem::STATUS_PROPOSED, ])->count(); if ($open > 0) { $alerts[] = [ 'code' => 'dental.plan.stale', 'severity' => 'warning', 'message' => "Accepted treatment plan has {$open} unfinished item(s) older than 30 days.", ]; } } if ($plan) { $high = $plan->items ->where('priority', '<=', 20) ->whereIn('status', [DentalPlanItem::STATUS_ACCEPTED, DentalPlanItem::STATUS_PROPOSED]); foreach ($high->take(3) as $item) { $tooth = $item->tooth_code ? " tooth {$item->tooth_code}" : ''; $alerts[] = [ 'code' => 'dental.plan.high_priority', 'severity' => 'warning', 'message' => "High-priority planned: {$item->label}{$tooth}.", ]; } } $overdue = DentalRecall::owned($ownerRef) ->where('organization_id', $organization->id) ->where('patient_id', $patient->id) ->whereIn('status', [DentalRecall::STATUS_SCHEDULED, DentalRecall::STATUS_DUE]) ->where('due_on', '<', now()->toDateString()) ->orderBy('due_on') ->limit(3) ->get(); foreach ($overdue as $recall) { $reason = $recall->reason ?: 'Recall'; $alerts[] = [ 'code' => 'dental.recall.overdue', 'severity' => 'warning', 'message' => "Overdue recall ({$reason}) due {$recall->due_on->format('d M Y')}.", ]; } return $alerts; } }