Ship commercial Dentistry suite with odontogram and treatment plans.
Deploy Ladill Care / deploy (push) Failing after 36s
Deploy Ladill Care / deploy (push) Failing after 36s
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>
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user