Files
ladill-care/app/Services/Care/Dentistry/DentalChartService.php
T
isaaccladandCursor 0edd653187
Deploy Ladill Care / deploy (push) Failing after 36s
Ship commercial Dentistry suite with odontogram and treatment plans.
Replace placeholder clinical forms with patient-level FDI charting, multi-visit plans, procedure-to-bill linking, imaging, reports, and demo seed data.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-18 16:33:34 +00:00

171 lines
5.3 KiB
PHP

<?php
namespace App\Services\Care\Dentistry;
use App\Models\DentalChart;
use App\Models\DentalChartEvent;
use App\Models\DentalTooth;
use App\Models\Organization;
use App\Models\Patient;
use App\Models\Visit;
use App\Services\Care\AuditLogger;
use Illuminate\Support\Facades\DB;
class DentalChartService
{
public function chartForPatient(Organization $organization, Patient $patient, string $ownerRef): DentalChart
{
$chart = DentalChart::owned($ownerRef)
->where('organization_id', $organization->id)
->where('patient_id', $patient->id)
->with('teeth')
->first();
if ($chart) {
return $chart;
}
return DB::transaction(function () use ($organization, $patient, $ownerRef) {
$chart = DentalChart::create([
'owner_ref' => $ownerRef,
'organization_id' => $organization->id,
'patient_id' => $patient->id,
'notation' => 'fdi',
'charted_at' => now(),
'charted_by' => $ownerRef,
]);
foreach (DentalCatalog::adultToothCodes() as $code) {
DentalTooth::create([
'dental_chart_id' => $chart->id,
'tooth_code' => $code,
'status' => DentalTooth::STATUS_PRESENT,
'surfaces' => [],
'conditions' => [],
]);
}
return $chart->fresh('teeth');
});
}
/**
* @param array{status?: string, surfaces?: list<string>, conditions?: list<string>, notes?: ?string} $data
*/
public function updateTooth(
DentalChart $chart,
string $toothCode,
array $data,
string $ownerRef,
?string $actorRef = null,
?Visit $visit = null,
): DentalTooth {
if (! DentalCatalog::isValidToothCode($toothCode)) {
throw new \InvalidArgumentException("Invalid FDI tooth code [{$toothCode}].");
}
$tooth = $chart->teeth()->where('tooth_code', $toothCode)->first();
if (! $tooth) {
$tooth = DentalTooth::create([
'dental_chart_id' => $chart->id,
'tooth_code' => $toothCode,
'status' => DentalTooth::STATUS_PRESENT,
'surfaces' => [],
'conditions' => [],
]);
}
$before = [
'status' => $tooth->status,
'surfaces' => $tooth->surfaces ?? [],
'conditions' => $tooth->conditions ?? [],
'notes' => $tooth->notes,
];
$status = $data['status'] ?? $tooth->status;
if (! array_key_exists($status, DentalCatalog::toothStatuses())) {
throw new \InvalidArgumentException("Invalid tooth status [{$status}].");
}
$surfaces = array_values(array_intersect(
array_map('strtoupper', $data['surfaces'] ?? ($tooth->surfaces ?? [])),
DentalCatalog::surfaces(),
));
$allowedConditions = array_keys(DentalCatalog::conditions());
$conditions = array_values(array_intersect(
$data['conditions'] ?? ($tooth->conditions ?? []),
$allowedConditions,
));
if (in_array($status, [DentalTooth::STATUS_MISSING, DentalTooth::STATUS_EXTRACTED], true)) {
$surfaces = [];
if (! in_array('missing', $conditions, true)) {
$conditions[] = 'missing';
}
}
$tooth->update([
'status' => $status,
'surfaces' => $surfaces,
'conditions' => $conditions,
'notes' => $data['notes'] ?? $tooth->notes,
]);
$chart->update([
'charted_at' => now(),
'charted_by' => $actorRef ?? $ownerRef,
]);
$after = [
'status' => $tooth->status,
'surfaces' => $tooth->surfaces ?? [],
'conditions' => $tooth->conditions ?? [],
'notes' => $tooth->notes,
];
DentalChartEvent::create([
'owner_ref' => $ownerRef,
'organization_id' => $chart->organization_id,
'dental_chart_id' => $chart->id,
'patient_id' => $chart->patient_id,
'visit_id' => $visit?->id,
'tooth_code' => $toothCode,
'event_type' => 'tooth_updated',
'payload' => ['before' => $before, 'after' => $after],
'recorded_by' => $actorRef ?? $ownerRef,
'recorded_at' => now(),
]);
AuditLogger::record(
$ownerRef,
'dental.chart.tooth_updated',
$chart->organization_id,
$actorRef,
DentalTooth::class,
$tooth->id,
['tooth_code' => $toothCode],
);
return $tooth->fresh();
}
/**
* @return array<string, array{status: string, surfaces: list<string>, conditions: list<string>, notes: ?string}>
*/
public function teethMap(DentalChart $chart): array
{
$map = [];
foreach ($chart->teeth as $tooth) {
$map[$tooth->tooth_code] = [
'status' => $tooth->status,
'surfaces' => $tooth->surfaces ?? [],
'conditions' => $tooth->conditions ?? [],
'notes' => $tooth->notes,
];
}
return $map;
}
}