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>
171 lines
5.3 KiB
PHP
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;
|
|
}
|
|
}
|