Complete Dentistry commercial suite with PMS depth.
Deploy Ladill Care / deploy (push) Successful in 31s

Add plan lifecycle, visit stages with chair/recovery points, primary dentition charting, imaging void, revenue reports, perio exams, lab cases, and recalls.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-18 17:11:15 +00:00
co-authored by Cursor
parent e227f8705f
commit ce782382c5
37 changed files with 2458 additions and 204 deletions
@@ -7,9 +7,11 @@ use App\Models\BillLineItem;
use App\Models\DentalImage;
use App\Models\DentalPlanItem;
use App\Models\DentalProcedure;
use App\Models\DentalRecall;
use App\Models\DentalTreatmentPlan;
use App\Models\Organization;
use App\Models\Patient;
use Carbon\Carbon;
use Illuminate\Support\Collection;
class DentalAnalyticsService
@@ -21,16 +23,21 @@ class DentalAnalyticsService
* revenue_by_code: Collection,
* unfinished_items: int,
* avg_chair_minutes: ?float,
* imaging_by_modality: Collection
* imaging_by_modality: Collection,
* from: string,
* to: string
* }
*/
public function report(
Organization $organization,
string $ownerRef,
?int $branchId = null,
?string $from = null,
?string $to = null,
): array {
$todayStart = now()->startOfDay();
$monthStart = now()->startOfMonth();
$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)
@@ -44,7 +51,7 @@ class DentalAnalyticsService
->get();
$proceduresMonth = (clone $procBase)
->where('performed_at', '>=', $monthStart)
->whereBetween('performed_at', [$rangeFrom, $rangeTo])
->selectRaw('procedure_code, label, count(*) as total, sum(amount_minor) as amount_minor')
->groupBy('procedure_code', 'label')
->orderByDesc('total')
@@ -53,7 +60,7 @@ class DentalAnalyticsService
$revenueByCode = BillLineItem::query()
->where('owner_ref', $ownerRef)
->where('source_type', 'dental_procedure')
->where('created_at', '>=', $monthStart)
->whereBetween('created_at', [$rangeFrom, $rangeTo])
->selectRaw('description, sum(total_minor) as amount_minor, count(*) as total')
->groupBy('description')
->orderByDesc('amount_minor')
@@ -77,7 +84,7 @@ class DentalAnalyticsService
->where('status', Appointment::STATUS_COMPLETED)
->whereNotNull('started_at')
->whereNotNull('completed_at')
->where('completed_at', '>=', $monthStart)
->whereBetween('completed_at', [$rangeFrom, $rangeTo])
->when($branchId, fn ($q) => $q->where('branch_id', $branchId));
$avgChair = null;
@@ -88,7 +95,7 @@ class DentalAnalyticsService
$imaging = DentalImage::owned($ownerRef)
->where('organization_id', $organization->id)
->where('captured_at', '>=', $monthStart)
->whereBetween('captured_at', [$rangeFrom, $rangeTo])
->selectRaw('modality, count(*) as total')
->groupBy('modality')
->orderByDesc('total')
@@ -101,6 +108,8 @@ class DentalAnalyticsService
'unfinished_items' => $unfinished,
'avg_chair_minutes' => $avgChair,
'imaging_by_modality' => $imaging,
'from' => $rangeFrom->toDateString(),
'to' => $rangeTo->toDateString(),
];
}
@@ -150,6 +159,24 @@ class DentalAnalyticsService
}
}
$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;
}
}