organization($request); abort_unless($modules->memberCanAccess($organization, $this->member($request), 'dentistry'), 403); } protected function assertDentistryManage(Request $request, SpecialtyModuleService $modules): void { $organization = $this->organization($request); abort_unless($modules->memberCanManage($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->assertDentistryManage($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->assertDentistryManage($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->assertDentistryManage($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->assertDentistryManage($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->assertDentistryManage($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->assertDentistryManage($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); $from = $request->query('from'); $to = $request->query('to'); return view('care.specialty.dentistry.reports', [ 'moduleKey' => 'dentistry', 'definition' => $modules->definition('dentistry'), 'report' => $analytics->report($organization, $this->ownerRef($request), $branchScope, $from, $to), '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(), ]); } public function setDentition( Request $request, Visit $visit, SpecialtyModuleService $modules, DentalChartService $charts, ): RedirectResponse { $this->authorizeAbility($request, 'consultations.manage'); $this->assertDentistryManage($request, $modules); $this->assertVisit($request, $visit); $validated = $request->validate([ 'dentition' => ['required', 'string', 'in:adult,primary,mixed'], ]); $organization = $this->organization($request); $owner = $this->ownerRef($request); $visit->loadMissing('patient'); $chart = $charts->chartForPatient($organization, $visit->patient, $owner); try { $charts->setDentition( $chart, $validated['dentition'], $owner, $this->actorRef($request), ); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return redirect() ->route('care.specialty.workspace', ['module' => 'dentistry', 'visit' => $visit, 'tab' => 'odontogram']) ->with('success', 'Dentition updated.'); } public function updatePlanItem( Request $request, Visit $visit, DentalPlanItem $item, SpecialtyModuleService $modules, DentalTreatmentPlanService $plans, ): RedirectResponse { $this->authorizeAbility($request, 'consultations.manage'); $this->assertDentistryManage($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->activePlanForPatient($organization, $visit->patient, $owner); abort_unless($plan && $item->treatment_plan_id === $plan->id, 404); try { $plans->updateItem($item, $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', 'Plan item updated.'); } public function cancelPlanItem( Request $request, Visit $visit, DentalPlanItem $item, SpecialtyModuleService $modules, DentalTreatmentPlanService $plans, ): RedirectResponse { $this->authorizeAbility($request, 'consultations.manage'); $this->assertDentistryManage($request, $modules); $this->assertVisit($request, $visit); $organization = $this->organization($request); $owner = $this->ownerRef($request); $visit->loadMissing('patient'); $plan = $plans->activePlanForPatient($organization, $visit->patient, $owner); abort_unless($plan && $item->treatment_plan_id === $plan->id, 404); try { $plans->cancelItem($item, $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', 'Plan item cancelled.'); } public function rejectPlan( Request $request, Visit $visit, SpecialtyModuleService $modules, DentalTreatmentPlanService $plans, ): RedirectResponse { $this->authorizeAbility($request, 'consultations.manage'); $this->assertDentistryManage($request, $modules); $this->assertVisit($request, $visit); $validated = $request->validate([ 'rejection_reason' => ['nullable', 'string', 'max:2000'], ]); $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.'); } try { $plans->rejectPlan( $plan, $owner, $validated['rejection_reason'] ?? null, $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', 'Treatment plan rejected.'); } public function cancelPlan( Request $request, Visit $visit, SpecialtyModuleService $modules, DentalTreatmentPlanService $plans, ): RedirectResponse { $this->authorizeAbility($request, 'consultations.manage'); $this->assertDentistryManage($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.'); } try { $plans->cancelPlan($plan, $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', 'Treatment plan cancelled.'); } public function voidImage( Request $request, Visit $visit, DentalImage $image, SpecialtyModuleService $modules, DentalImagingService $imaging, ): RedirectResponse { $this->authorizeAbility($request, 'consultations.manage'); $this->assertDentistryManage($request, $modules); $this->assertVisit($request, $visit); $organization = $this->organization($request); $visit->loadMissing('patient'); abort_unless( $image->organization_id === $organization->id && ($image->visit_id === $visit->id || $image->patient_id === $visit->patient_id), 404, ); try { $imaging->void($image, $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 removed.'); } public function setStage( Request $request, Visit $visit, SpecialtyModuleService $modules, SpecialtyVisitStageService $stages, SpecialtyShellService $shell, ): RedirectResponse { $this->authorizeAbility($request, 'consultations.manage'); $this->assertDentistryManage($request, $modules); $this->assertVisit($request, $visit); $validated = $request->validate([ 'stage' => ['required', 'string', 'max:32'], ]); try { $stages->setStage( $this->organization($request), $visit, 'dentistry', $validated['stage'], $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' => $shell->workspaceTabForStage('dentistry', $validated['stage']), ]) ->with('success', 'Visit stage updated.'); } public function savePerio( Request $request, Visit $visit, SpecialtyModuleService $modules, DentalPerioService $perio, ): RedirectResponse { $this->authorizeAbility($request, 'consultations.manage'); $this->assertDentistryManage($request, $modules); $this->assertVisit($request, $visit); $validated = $request->validate([ 'sites' => ['nullable', 'array'], 'sites.*.tooth_code' => ['required', 'string', 'max:8'], 'sites.*.surface' => ['required', 'string', 'max:8'], 'sites.*.probing_mm' => ['nullable', 'integer', 'min:0', 'max:20'], 'sites.*.bleeding' => ['nullable', 'boolean'], 'sites.*.plaque' => ['nullable', 'boolean'], 'sites.*.recession_mm' => ['nullable', 'integer', 'min:0', 'max:20'], 'notes' => ['nullable', 'string', 'max:5000'], ]); $organization = $this->organization($request); $owner = $this->ownerRef($request); $visit->loadMissing('patient'); try { $perio->saveExam( $organization, $visit->patient, $owner, $validated['sites'] ?? [], $visit, $validated['notes'] ?? null, $this->actorRef($request), ); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return redirect() ->route('care.specialty.workspace', ['module' => 'dentistry', 'visit' => $visit, 'tab' => 'perio']) ->with('success', 'Perio exam saved.'); } public function createLabCase( Request $request, Visit $visit, SpecialtyModuleService $modules, DentalLabCaseService $labCases, ): RedirectResponse { $this->authorizeAbility($request, 'consultations.manage'); $this->assertDentistryManage($request, $modules); $this->assertVisit($request, $visit); $validated = $request->validate([ 'case_type' => ['required', 'string', 'in:crown,bridge,denture,veneer,ortho,other'], 'lab_name' => ['nullable', 'string', 'max:255'], 'tooth_codes' => ['nullable', 'array'], 'tooth_codes.*' => ['string', 'max:8'], 'due_at' => ['nullable', 'date'], 'notes' => ['nullable', 'string', 'max:2000'], 'status' => ['nullable', 'string', 'in:draft,ordered,sent,received,fitted,cancelled'], ]); $organization = $this->organization($request); $owner = $this->ownerRef($request); $visit->loadMissing('patient'); try { $labCases->create( $organization, $visit->patient, $owner, $validated, $visit, $this->actorRef($request), ); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return redirect() ->route('care.specialty.workspace', ['module' => 'dentistry', 'visit' => $visit, 'tab' => 'lab']) ->with('success', 'Lab case created.'); } public function transitionLabCase( Request $request, Visit $visit, DentalLabCase $labCase, SpecialtyModuleService $modules, DentalLabCaseService $labCases, ): RedirectResponse { $this->authorizeAbility($request, 'consultations.manage'); $this->assertDentistryManage($request, $modules); $this->assertVisit($request, $visit); $validated = $request->validate([ 'status' => ['required', 'string', 'in:draft,ordered,sent,received,fitted,cancelled'], ]); $organization = $this->organization($request); $visit->loadMissing('patient'); abort_unless( $labCase->organization_id === $organization->id && $labCase->patient_id === $visit->patient_id, 404, ); try { $labCases->transition( $labCase, $validated['status'], $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' => 'lab']) ->with('success', 'Lab case updated.'); } public function scheduleRecall( Request $request, Visit $visit, SpecialtyModuleService $modules, DentalRecallService $recalls, ): RedirectResponse { $this->authorizeAbility($request, 'consultations.manage'); $this->assertDentistryManage($request, $modules); $this->assertVisit($request, $visit); $validated = $request->validate([ 'due_on' => ['required', 'date'], 'reason' => ['nullable', 'string', 'max:255'], 'notes' => ['nullable', 'string', 'max:2000'], ]); $organization = $this->organization($request); $owner = $this->ownerRef($request); $visit->loadMissing('patient'); try { $recalls->schedule( $organization, $visit->patient, $owner, $validated, $this->actorRef($request), ); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return redirect() ->route('care.specialty.workspace', ['module' => 'dentistry', 'visit' => $visit, 'tab' => 'recalls']) ->with('success', 'Recall scheduled.'); } public function completeRecall( Request $request, Visit $visit, DentalRecall $recall, SpecialtyModuleService $modules, DentalRecallService $recalls, ): RedirectResponse { $this->authorizeAbility($request, 'consultations.manage'); $this->assertDentistryManage($request, $modules); $this->assertVisit($request, $visit); $organization = $this->organization($request); $visit->loadMissing('patient'); abort_unless( $recall->organization_id === $organization->id && $recall->patient_id === $visit->patient_id, 404, ); try { $recalls->complete( $recall, $this->ownerRef($request), $visit, $this->actorRef($request), ); } catch (\InvalidArgumentException $e) { return back()->with('error', $e->getMessage()); } return redirect() ->route('care.specialty.workspace', ['module' => 'dentistry', 'visit' => $visit, 'tab' => 'recalls']) ->with('success', 'Recall completed.'); } public function cancelRecall( Request $request, Visit $visit, DentalRecall $recall, SpecialtyModuleService $modules, DentalRecallService $recalls, ): RedirectResponse { $this->authorizeAbility($request, 'consultations.manage'); $this->assertDentistryManage($request, $modules); $this->assertVisit($request, $visit); $organization = $this->organization($request); $visit->loadMissing('patient'); abort_unless( $recall->organization_id === $organization->id && $recall->patient_id === $visit->patient_id, 404, ); try { $recalls->cancel($recall, $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' => 'recalls']) ->with('success', 'Recall cancelled.'); } }