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,319 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Visit;
|
||||
use App\Services\Care\Dentistry\DentalAnalyticsService;
|
||||
use App\Services\Care\Dentistry\DentalCatalog;
|
||||
use App\Services\Care\Dentistry\DentalChartService;
|
||||
use App\Services\Care\Dentistry\DentalImagingService;
|
||||
use App\Services\Care\Dentistry\DentalProcedureService;
|
||||
use App\Services\Care\Dentistry\DentalTreatmentPlanService;
|
||||
use App\Services\Care\Dentistry\DentalVisitNoteService;
|
||||
use App\Services\Care\SpecialtyModuleService;
|
||||
use App\Services\Care\SpecialtyShellService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class DentistryWorkspaceController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
protected function assertDentistryAccess(Request $request, SpecialtyModuleService $modules): void
|
||||
{
|
||||
$organization = $this->organization($request);
|
||||
abort_unless($modules->memberCanAccess($organization, $this->member($request), 'dentistry'), 403);
|
||||
}
|
||||
|
||||
protected function assertVisit(Request $request, Visit $visit): void
|
||||
{
|
||||
abort_unless($visit->organization_id === $this->organization($request)->id, 404);
|
||||
$this->authorizeBranch($request, (int) $visit->branch_id);
|
||||
}
|
||||
|
||||
public function updateTooth(
|
||||
Request $request,
|
||||
Visit $visit,
|
||||
SpecialtyModuleService $modules,
|
||||
DentalChartService $charts,
|
||||
): RedirectResponse {
|
||||
$this->authorizeAbility($request, 'consultations.manage');
|
||||
$this->assertDentistryAccess($request, $modules);
|
||||
$this->assertVisit($request, $visit);
|
||||
|
||||
$validated = $request->validate([
|
||||
'tooth_code' => ['required', 'string', 'max:8'],
|
||||
'status' => ['required', 'string', 'in:present,missing,extracted,implant'],
|
||||
'surfaces' => ['nullable', 'array'],
|
||||
'surfaces.*' => ['string', 'max:2'],
|
||||
'conditions' => ['nullable', 'array'],
|
||||
'conditions.*' => ['string', 'max:32'],
|
||||
'notes' => ['nullable', 'string', 'max:2000'],
|
||||
]);
|
||||
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
$visit->loadMissing('patient');
|
||||
$chart = $charts->chartForPatient($organization, $visit->patient, $owner);
|
||||
|
||||
try {
|
||||
$charts->updateTooth(
|
||||
$chart,
|
||||
$validated['tooth_code'],
|
||||
$validated,
|
||||
$owner,
|
||||
$this->actorRef($request),
|
||||
$visit,
|
||||
);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return back()->with('error', $e->getMessage());
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('care.specialty.workspace', ['module' => 'dentistry', 'visit' => $visit, 'tab' => 'odontogram'])
|
||||
->with('success', 'Tooth '.$validated['tooth_code'].' updated.');
|
||||
}
|
||||
|
||||
public function addPlanItem(
|
||||
Request $request,
|
||||
Visit $visit,
|
||||
SpecialtyModuleService $modules,
|
||||
DentalTreatmentPlanService $plans,
|
||||
): RedirectResponse {
|
||||
$this->authorizeAbility($request, 'consultations.manage');
|
||||
$this->assertDentistryAccess($request, $modules);
|
||||
$this->assertVisit($request, $visit);
|
||||
|
||||
$validated = $request->validate([
|
||||
'procedure_code' => ['required', 'string', 'max:64'],
|
||||
'tooth_code' => ['nullable', 'string', 'max:8'],
|
||||
'surfaces' => ['nullable', 'array'],
|
||||
'surfaces.*' => ['string', 'max:2'],
|
||||
'priority' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||
'notes' => ['nullable', 'string', 'max:2000'],
|
||||
]);
|
||||
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
$visit->loadMissing('patient');
|
||||
$plan = $plans->ensurePlan($organization, $visit->patient, $owner, $visit, $this->actorRef($request));
|
||||
|
||||
try {
|
||||
$plans->addItem($plan, $organization, $validated, $owner, $this->actorRef($request));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return back()->with('error', $e->getMessage());
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('care.specialty.workspace', ['module' => 'dentistry', 'visit' => $visit, 'tab' => 'plan'])
|
||||
->with('success', 'Added to treatment plan.');
|
||||
}
|
||||
|
||||
public function acceptPlan(
|
||||
Request $request,
|
||||
Visit $visit,
|
||||
SpecialtyModuleService $modules,
|
||||
DentalTreatmentPlanService $plans,
|
||||
): RedirectResponse {
|
||||
$this->authorizeAbility($request, 'consultations.manage');
|
||||
$this->assertDentistryAccess($request, $modules);
|
||||
$this->assertVisit($request, $visit);
|
||||
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
$visit->loadMissing('patient');
|
||||
$plan = $plans->activePlanForPatient($organization, $visit->patient, $owner);
|
||||
if (! $plan) {
|
||||
return back()->with('error', 'No open treatment plan.');
|
||||
}
|
||||
|
||||
$plans->accept($plan, $owner, $this->actorRef($request));
|
||||
|
||||
return redirect()
|
||||
->route('care.specialty.workspace', ['module' => 'dentistry', 'visit' => $visit, 'tab' => 'plan'])
|
||||
->with('success', 'Treatment plan accepted.');
|
||||
}
|
||||
|
||||
public function completeProcedure(
|
||||
Request $request,
|
||||
Visit $visit,
|
||||
SpecialtyModuleService $modules,
|
||||
DentalProcedureService $procedures,
|
||||
): RedirectResponse {
|
||||
$this->authorizeAbility($request, 'consultations.manage');
|
||||
$this->assertDentistryAccess($request, $modules);
|
||||
$this->assertVisit($request, $visit);
|
||||
|
||||
$validated = $request->validate([
|
||||
'procedure_code' => ['required', 'string', 'max:64'],
|
||||
'tooth_code' => ['nullable', 'string', 'max:8'],
|
||||
'surfaces' => ['nullable', 'array'],
|
||||
'surfaces.*' => ['string', 'max:2'],
|
||||
'anesthesia' => ['nullable', 'string', 'max:32'],
|
||||
'notes' => ['nullable', 'string', 'max:2000'],
|
||||
'plan_item_id' => ['nullable', 'integer'],
|
||||
'bill' => ['nullable', 'boolean'],
|
||||
]);
|
||||
|
||||
try {
|
||||
$procedures->complete(
|
||||
$this->organization($request),
|
||||
$visit,
|
||||
[
|
||||
...$validated,
|
||||
'bill' => $request->boolean('bill', true),
|
||||
],
|
||||
$this->ownerRef($request),
|
||||
$this->actorRef($request),
|
||||
);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return back()->with('error', $e->getMessage());
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('care.specialty.workspace', ['module' => 'dentistry', 'visit' => $visit, 'tab' => 'treat'])
|
||||
->with('success', 'Procedure recorded and billed.');
|
||||
}
|
||||
|
||||
public function saveNotes(
|
||||
Request $request,
|
||||
Visit $visit,
|
||||
SpecialtyModuleService $modules,
|
||||
DentalVisitNoteService $notes,
|
||||
): RedirectResponse {
|
||||
$this->authorizeAbility($request, 'consultations.manage');
|
||||
$this->assertDentistryAccess($request, $modules);
|
||||
$this->assertVisit($request, $visit);
|
||||
|
||||
$validated = $request->validate([
|
||||
'chief_complaint' => ['nullable', 'string', 'max:5000'],
|
||||
'soft_tissue' => ['nullable', 'string', 'max:5000'],
|
||||
'occlusion' => ['nullable', 'string', 'max:5000'],
|
||||
'notes' => ['nullable', 'string', 'max:5000'],
|
||||
]);
|
||||
|
||||
$notes->upsert(
|
||||
$this->organization($request),
|
||||
$visit,
|
||||
$validated,
|
||||
$this->ownerRef($request),
|
||||
$this->actorRef($request),
|
||||
);
|
||||
|
||||
return redirect()
|
||||
->route('care.specialty.workspace', ['module' => 'dentistry', 'visit' => $visit, 'tab' => 'notes'])
|
||||
->with('success', 'Visit notes saved.');
|
||||
}
|
||||
|
||||
public function uploadImage(
|
||||
Request $request,
|
||||
Visit $visit,
|
||||
SpecialtyModuleService $modules,
|
||||
DentalImagingService $imaging,
|
||||
DentalProcedureService $procedures,
|
||||
): RedirectResponse {
|
||||
$this->authorizeAbility($request, 'consultations.manage');
|
||||
$this->assertDentistryAccess($request, $modules);
|
||||
$this->assertVisit($request, $visit);
|
||||
|
||||
$validated = $request->validate([
|
||||
'attachment' => ['required', 'file', 'max:10240', 'mimes:pdf,jpeg,jpg,png,webp'],
|
||||
'modality' => ['required', 'string', 'in:pa,bw,opg,photo'],
|
||||
'tooth_codes' => ['nullable', 'array'],
|
||||
'tooth_codes.*' => ['string', 'max:8'],
|
||||
'caption' => ['nullable', 'string', 'max:255'],
|
||||
'add_fee' => ['nullable', 'boolean'],
|
||||
]);
|
||||
|
||||
try {
|
||||
$imaging->upload(
|
||||
$this->organization($request),
|
||||
$visit,
|
||||
$validated['attachment'],
|
||||
$validated,
|
||||
$this->ownerRef($request),
|
||||
$this->actorRef($request),
|
||||
);
|
||||
|
||||
if ($request->boolean('add_fee')) {
|
||||
$feeCode = match ($validated['modality']) {
|
||||
'pa' => 'den.xray_pa',
|
||||
'bw' => 'den.xray_bw',
|
||||
'opg' => 'den.xray_opg',
|
||||
default => 'den.photo',
|
||||
};
|
||||
$procedures->complete(
|
||||
$this->organization($request),
|
||||
$visit,
|
||||
[
|
||||
'procedure_code' => $feeCode,
|
||||
'tooth_code' => ($validated['tooth_codes'][0] ?? null),
|
||||
'bill' => true,
|
||||
'notes' => 'Imaging fee',
|
||||
],
|
||||
$this->ownerRef($request),
|
||||
$this->actorRef($request),
|
||||
);
|
||||
}
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return back()->with('error', $e->getMessage());
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('care.specialty.workspace', ['module' => 'dentistry', 'visit' => $visit, 'tab' => 'imaging'])
|
||||
->with('success', 'Image uploaded.');
|
||||
}
|
||||
|
||||
public function reports(
|
||||
Request $request,
|
||||
SpecialtyModuleService $modules,
|
||||
SpecialtyShellService $shell,
|
||||
DentalAnalyticsService $analytics,
|
||||
): View {
|
||||
$this->assertDentistryAccess($request, $modules);
|
||||
$organization = $this->organization($request);
|
||||
$member = $this->member($request);
|
||||
$branchScope = app(\App\Services\Care\OrganizationResolver::class)->branchScope($member);
|
||||
|
||||
return view('care.specialty.dentistry.reports', [
|
||||
'moduleKey' => 'dentistry',
|
||||
'definition' => $modules->definition('dentistry'),
|
||||
'report' => $analytics->report($organization, $this->ownerRef($request), $branchScope),
|
||||
'modalities' => DentalCatalog::modalities(),
|
||||
'currency' => config('care.billing.currency', 'GHS'),
|
||||
'shellNav' => $shell->navItems('dentistry'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function printChart(
|
||||
Request $request,
|
||||
Visit $visit,
|
||||
SpecialtyModuleService $modules,
|
||||
DentalChartService $charts,
|
||||
DentalTreatmentPlanService $plans,
|
||||
): StreamedResponse|View {
|
||||
$this->authorizeAbility($request, 'consultations.view');
|
||||
$this->assertDentistryAccess($request, $modules);
|
||||
$this->assertVisit($request, $visit);
|
||||
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
$visit->loadMissing('patient');
|
||||
$chart = $charts->chartForPatient($organization, $visit->patient, $owner);
|
||||
$plan = $plans->activePlanForPatient($organization, $visit->patient, $owner);
|
||||
|
||||
return view('care.specialty.dentistry.print', [
|
||||
'visit' => $visit,
|
||||
'patient' => $visit->patient,
|
||||
'chart' => $chart,
|
||||
'teethMap' => $charts->teethMap($chart),
|
||||
'plan' => $plan,
|
||||
'rows' => DentalCatalog::odontogramRows(),
|
||||
'conditions' => DentalCatalog::conditions(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user