Complete Dentistry commercial suite with PMS depth.
Deploy Ladill Care / deploy (push) Successful in 31s
Deploy Ladill Care / deploy (push) Successful in 31s
Add plan lifecycle, visit stages with chair/recovery points, primary dentition charting, imaging void, revenue reports, perio exams, lab cases, and recalls. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,16 +4,24 @@ namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\DentalImage;
|
||||
use App\Models\DentalLabCase;
|
||||
use App\Models\DentalPlanItem;
|
||||
use App\Models\DentalRecall;
|
||||
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\DentalLabCaseService;
|
||||
use App\Services\Care\Dentistry\DentalPerioService;
|
||||
use App\Services\Care\Dentistry\DentalProcedureService;
|
||||
use App\Services\Care\Dentistry\DentalRecallService;
|
||||
use App\Services\Care\Dentistry\DentalTreatmentPlanService;
|
||||
use App\Services\Care\Dentistry\DentalVisitNoteService;
|
||||
use App\Services\Care\SpecialtyModuleService;
|
||||
use App\Services\Care\SpecialtyShellService;
|
||||
use App\Services\Care\SpecialtyVisitStageService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
@@ -279,10 +287,13 @@ class DentistryWorkspaceController extends Controller
|
||||
$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),
|
||||
'report' => $analytics->report($organization, $this->ownerRef($request), $branchScope, $from, $to),
|
||||
'modalities' => DentalCatalog::modalities(),
|
||||
'currency' => config('care.billing.currency', 'GHS'),
|
||||
'shellNav' => $shell->navItems('dentistry'),
|
||||
@@ -316,4 +327,460 @@ class DentistryWorkspaceController extends Controller
|
||||
'conditions' => DentalCatalog::conditions(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function setDentition(
|
||||
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([
|
||||
'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->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->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->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);
|
||||
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->assertDentistryAccess($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->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.');
|
||||
}
|
||||
|
||||
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->assertDentistryAccess($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,
|
||||
): RedirectResponse {
|
||||
$this->authorizeAbility($request, 'consultations.manage');
|
||||
$this->assertDentistryAccess($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' => 'overview'])
|
||||
->with('success', 'Visit stage updated.');
|
||||
}
|
||||
|
||||
public function savePerio(
|
||||
Request $request,
|
||||
Visit $visit,
|
||||
SpecialtyModuleService $modules,
|
||||
DentalPerioService $perio,
|
||||
): RedirectResponse {
|
||||
$this->authorizeAbility($request, 'consultations.manage');
|
||||
$this->assertDentistryAccess($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->assertDentistryAccess($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->assertDentistryAccess($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->assertDentistryAccess($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->assertDentistryAccess($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->assertDentistryAccess($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.');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user