From 8e23cdeae30310369ea285a9b2ecb35a80bcafb8 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Sun, 19 Jul 2026 18:41:55 +0000 Subject: [PATCH] Add full Radiology, Cardiology, and Psychiatry specialty clinical suites. Mirror the Ophthalmology/Maternity shell pattern with stages, stage_tabs, workspace clinical records, reports/print, demo seeds, and suite tests. Co-authored-by: Cursor --- .../Care/CardiologyWorkspaceController.php | 215 +++++++++++ .../Care/PsychiatryWorkspaceController.php | 214 +++++++++++ .../Care/RadiologyWorkspaceController.php | 214 +++++++++++ .../Care/SpecialtyModuleController.php | 212 +++++++++++ .../Cardiology/CardiologyAnalyticsService.php | 168 +++++++++ .../Cardiology/CardiologyWorkflowService.php | 104 ++++++ app/Services/Care/DemoTenantSeeder.php | 349 ++++++++++++++++++ .../Psychiatry/PsychiatryAnalyticsService.php | 168 +++++++++ .../Psychiatry/PsychiatryWorkflowService.php | 86 +++++ .../Radiology/RadiologyAnalyticsService.php | 168 +++++++++ .../Radiology/RadiologyWorkflowService.php | 85 +++++ .../Care/SpecialtyClinicalRecordService.php | 71 ++++ app/Services/Care/SpecialtyShellService.php | 14 +- .../Care/SpecialtyVisitStageService.php | 24 ++ config/care_specialty_clinical.php | 110 +++++- config/care_specialty_modules.php | 5 +- config/care_specialty_shell.php | 76 +++- .../care/specialty/cardiology/print.blade.php | 89 +++++ .../specialty/cardiology/reports.blade.php | 88 +++++ .../cardiology/workspace-overview.blade.php | 71 ++++ .../cardiology/workspace-procedures.blade.php | 70 ++++ .../specialty/partials/actions-menu.blade.php | 6 + .../care/specialty/psychiatry/print.blade.php | 81 ++++ .../specialty/psychiatry/reports.blade.php | 88 +++++ .../psychiatry/workspace-followup.blade.php | 70 ++++ .../psychiatry/workspace-overview.blade.php | 71 ++++ .../care/specialty/radiology/print.blade.php | 79 ++++ .../specialty/radiology/reports.blade.php | 88 +++++ .../radiology/workspace-overview.blade.php | 72 ++++ .../workspace-verification.blade.php | 70 ++++ .../specialty/sections/workspace.blade.php | 6 + .../views/care/specialty/shell.blade.php | 9 + routes/web.php | 15 + tests/Feature/CareCardiologySuiteTest.php | 237 ++++++++++++ tests/Feature/CarePsychiatrySuiteTest.php | 237 ++++++++++++ tests/Feature/CareRadiologySuiteTest.php | 236 ++++++++++++ 36 files changed, 3932 insertions(+), 34 deletions(-) create mode 100644 app/Http/Controllers/Care/CardiologyWorkspaceController.php create mode 100644 app/Http/Controllers/Care/PsychiatryWorkspaceController.php create mode 100644 app/Http/Controllers/Care/RadiologyWorkspaceController.php create mode 100644 app/Services/Care/Cardiology/CardiologyAnalyticsService.php create mode 100644 app/Services/Care/Cardiology/CardiologyWorkflowService.php create mode 100644 app/Services/Care/Psychiatry/PsychiatryAnalyticsService.php create mode 100644 app/Services/Care/Psychiatry/PsychiatryWorkflowService.php create mode 100644 app/Services/Care/Radiology/RadiologyAnalyticsService.php create mode 100644 app/Services/Care/Radiology/RadiologyWorkflowService.php create mode 100644 resources/views/care/specialty/cardiology/print.blade.php create mode 100644 resources/views/care/specialty/cardiology/reports.blade.php create mode 100644 resources/views/care/specialty/cardiology/workspace-overview.blade.php create mode 100644 resources/views/care/specialty/cardiology/workspace-procedures.blade.php create mode 100644 resources/views/care/specialty/psychiatry/print.blade.php create mode 100644 resources/views/care/specialty/psychiatry/reports.blade.php create mode 100644 resources/views/care/specialty/psychiatry/workspace-followup.blade.php create mode 100644 resources/views/care/specialty/psychiatry/workspace-overview.blade.php create mode 100644 resources/views/care/specialty/radiology/print.blade.php create mode 100644 resources/views/care/specialty/radiology/reports.blade.php create mode 100644 resources/views/care/specialty/radiology/workspace-overview.blade.php create mode 100644 resources/views/care/specialty/radiology/workspace-verification.blade.php create mode 100644 tests/Feature/CareCardiologySuiteTest.php create mode 100644 tests/Feature/CarePsychiatrySuiteTest.php create mode 100644 tests/Feature/CareRadiologySuiteTest.php diff --git a/app/Http/Controllers/Care/CardiologyWorkspaceController.php b/app/Http/Controllers/Care/CardiologyWorkspaceController.php new file mode 100644 index 0000000..83f9ccb --- /dev/null +++ b/app/Http/Controllers/Care/CardiologyWorkspaceController.php @@ -0,0 +1,215 @@ +organization($request); + abort_unless($modules->memberCanAccess($organization, $this->member($request), 'cardiology'), 403); + } + + protected function assertCardiologyManage(Request $request, SpecialtyModuleService $modules): void + { + $organization = $this->organization($request); + abort_unless($modules->memberCanManage($organization, $this->member($request), 'cardiology'), 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 setStage( + Request $request, + Visit $visit, + SpecialtyModuleService $modules, + SpecialtyVisitStageService $stages, + SpecialtyShellService $shell, + ): RedirectResponse { + $this->authorizeAbility($request, 'consultations.manage'); + $this->assertCardiologyManage($request, $modules); + $this->assertVisit($request, $visit); + + $validated = $request->validate([ + 'stage' => ['required', 'string', 'max:32'], + ]); + + try { + $stages->setStage( + $this->organization($request), + $visit, + 'cardiology', + $validated['stage'], + $this->ownerRef($request), + $this->actorRef($request), + ); + } catch (\InvalidArgumentException $e) { + return back()->with('error', $e->getMessage()); + } + + return redirect() + ->route('care.specialty.workspace', [ + 'module' => 'cardiology', + 'visit' => $visit, + 'tab' => $shell->workspaceTabForStage('cardiology', $validated['stage']), + ]) + ->with('success', 'Visit stage updated.'); + } + + public function saveProcedure( + Request $request, + Visit $visit, + SpecialtyModuleService $modules, + SpecialtyClinicalRecordService $clinical, + SpecialtyVisitStageService $stages, + CardiologyWorkflowService $workflow, + ): RedirectResponse { + $this->authorizeAbility($request, 'consultations.manage'); + $this->assertCardiologyManage($request, $modules); + $this->assertVisit($request, $visit); + + $organization = $this->organization($request); + $owner = $this->ownerRef($request); + + $payload = (array) $request->input('payload', []); + foreach ($clinical->fieldsFor('cardiology', 'cardio_procedure') as $field) { + if (($field['type'] ?? '') === 'boolean') { + $payload[$field['name']] = $request->boolean('payload.'.$field['name']); + } + } + $request->merge(['payload' => $payload, 'tab' => 'procedures']); + + $validated = $request->validate(array_merge([ + 'tab' => ['required', 'string'], + ], $clinical->validationRules('cardiology', 'cardio_procedure'))); + + $record = $clinical->upsert( + $organization, + $visit, + 'cardiology', + 'cardio_procedure', + $clinical->payloadFromRequest($validated), + $owner, + $owner, + CardiologyWorkflowService::STAGE_PROCEDURE, + SpecialtyClinicalRecord::STATUS_COMPLETED, + ); + + try { + $stages->setStage( + $organization, + $visit, + 'cardiology', + CardiologyWorkflowService::STAGE_PROCEDURE, + $owner, + $owner, + ); + } catch (\InvalidArgumentException) { + } + + if ($workflow->shouldCompleteVisit($record->payload ?? [])) { + try { + $stages->setStage( + $organization, + $visit, + 'cardiology', + CardiologyWorkflowService::STAGE_COMPLETED, + $owner, + $owner, + ); + } catch (\InvalidArgumentException) { + } + + $visit->refresh(); + if ($visit->status !== Visit::STATUS_COMPLETED) { + $visit->update([ + 'status' => Visit::STATUS_COMPLETED, + 'completed_at' => $visit->completed_at ?? now(), + ]); + } + + $appointment = $visit->appointment; + if ($appointment && $appointment->status !== \App\Models\Appointment::STATUS_COMPLETED) { + $appointment->update([ + 'status' => \App\Models\Appointment::STATUS_COMPLETED, + 'completed_at' => $appointment->completed_at ?? now(), + ]); + } + } + + return redirect() + ->route('care.specialty.workspace', ['module' => 'cardiology', 'visit' => $visit, 'tab' => 'procedures']) + ->with('success', 'Procedure / intervention saved.'); + } + + public function reports( + Request $request, + SpecialtyModuleService $modules, + SpecialtyShellService $shell, + CardiologyAnalyticsService $analytics, + ): View { + $this->authorizeAbility($request, 'consultations.view'); + $this->assertCardiologyAccess($request, $modules); + + $organization = $this->organization($request); + $branchScope = app(\App\Services\Care\OrganizationResolver::class)->branchScope($this->member($request)); + + $report = $analytics->report( + $organization, + $this->ownerRef($request), + $branchScope, + $request->query('from'), + $request->query('to'), + ); + + return view('care.specialty.cardiology.reports', [ + 'moduleKey' => 'cardiology', + 'definition' => $modules->definition('cardiology'), + 'shellNav' => $shell->navItems('cardiology'), + 'report' => $report, + 'currency' => strtoupper((string) config('care.billing.currency', config('care.currency', 'GHS'))), + ]); + } + + public function printSummary( + Request $request, + Visit $visit, + SpecialtyModuleService $modules, + SpecialtyClinicalRecordService $clinical, + ): View { + $this->authorizeAbility($request, 'consultations.view'); + $this->assertCardiologyAccess($request, $modules); + $this->assertVisit($request, $visit); + + $visit->loadMissing(['patient', 'appointment.practitioner', 'branch']); + + return view('care.specialty.cardiology.print', [ + 'visit' => $visit, + 'patient' => $visit->patient, + 'history' => $clinical->findForVisit($visit, 'cardiology', 'cardio_history'), + 'exam' => $clinical->findForVisit($visit, 'cardiology', 'cardiac_exam'), + 'investigation' => $clinical->findForVisit($visit, 'cardiology', 'cardio_investigation'), + 'plan' => $clinical->findForVisit($visit, 'cardiology', 'cardio_plan'), + 'procedure' => $clinical->findForVisit($visit, 'cardiology', 'cardio_procedure'), + ]); + } +} diff --git a/app/Http/Controllers/Care/PsychiatryWorkspaceController.php b/app/Http/Controllers/Care/PsychiatryWorkspaceController.php new file mode 100644 index 0000000..bb5cfaf --- /dev/null +++ b/app/Http/Controllers/Care/PsychiatryWorkspaceController.php @@ -0,0 +1,214 @@ +organization($request); + abort_unless($modules->memberCanAccess($organization, $this->member($request), 'psychiatry'), 403); + } + + protected function assertPsychiatryManage(Request $request, SpecialtyModuleService $modules): void + { + $organization = $this->organization($request); + abort_unless($modules->memberCanManage($organization, $this->member($request), 'psychiatry'), 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 setStage( + Request $request, + Visit $visit, + SpecialtyModuleService $modules, + SpecialtyVisitStageService $stages, + SpecialtyShellService $shell, + ): RedirectResponse { + $this->authorizeAbility($request, 'consultations.manage'); + $this->assertPsychiatryManage($request, $modules); + $this->assertVisit($request, $visit); + + $validated = $request->validate([ + 'stage' => ['required', 'string', 'max:32'], + ]); + + try { + $stages->setStage( + $this->organization($request), + $visit, + 'psychiatry', + $validated['stage'], + $this->ownerRef($request), + $this->actorRef($request), + ); + } catch (\InvalidArgumentException $e) { + return back()->with('error', $e->getMessage()); + } + + return redirect() + ->route('care.specialty.workspace', [ + 'module' => 'psychiatry', + 'visit' => $visit, + 'tab' => $shell->workspaceTabForStage('psychiatry', $validated['stage']), + ]) + ->with('success', 'Visit stage updated.'); + } + + public function saveFollowUp( + Request $request, + Visit $visit, + SpecialtyModuleService $modules, + SpecialtyClinicalRecordService $clinical, + SpecialtyVisitStageService $stages, + PsychiatryWorkflowService $workflow, + ): RedirectResponse { + $this->authorizeAbility($request, 'consultations.manage'); + $this->assertPsychiatryManage($request, $modules); + $this->assertVisit($request, $visit); + + $organization = $this->organization($request); + $owner = $this->ownerRef($request); + + $payload = (array) $request->input('payload', []); + foreach ($clinical->fieldsFor('psychiatry', 'psych_followup') as $field) { + if (($field['type'] ?? '') === 'boolean') { + $payload[$field['name']] = $request->boolean('payload.'.$field['name']); + } + } + $request->merge(['payload' => $payload, 'tab' => 'followup']); + + $validated = $request->validate(array_merge([ + 'tab' => ['required', 'string'], + ], $clinical->validationRules('psychiatry', 'psych_followup'))); + + $record = $clinical->upsert( + $organization, + $visit, + 'psychiatry', + 'psych_followup', + $clinical->payloadFromRequest($validated), + $owner, + $owner, + PsychiatryWorkflowService::STAGE_REVIEW, + SpecialtyClinicalRecord::STATUS_COMPLETED, + ); + + try { + $stages->setStage( + $organization, + $visit, + 'psychiatry', + PsychiatryWorkflowService::STAGE_REVIEW, + $owner, + $owner, + ); + } catch (\InvalidArgumentException) { + } + + if ($workflow->shouldCompleteVisit($record->payload ?? [])) { + try { + $stages->setStage( + $organization, + $visit, + 'psychiatry', + PsychiatryWorkflowService::STAGE_COMPLETED, + $owner, + $owner, + ); + } catch (\InvalidArgumentException) { + } + + $visit->refresh(); + if ($visit->status !== Visit::STATUS_COMPLETED) { + $visit->update([ + 'status' => Visit::STATUS_COMPLETED, + 'completed_at' => $visit->completed_at ?? now(), + ]); + } + + $appointment = $visit->appointment; + if ($appointment && $appointment->status !== \App\Models\Appointment::STATUS_COMPLETED) { + $appointment->update([ + 'status' => \App\Models\Appointment::STATUS_COMPLETED, + 'completed_at' => $appointment->completed_at ?? now(), + ]); + } + } + + return redirect() + ->route('care.specialty.workspace', ['module' => 'psychiatry', 'visit' => $visit, 'tab' => 'followup']) + ->with('success', 'Follow-up review saved.'); + } + + public function reports( + Request $request, + SpecialtyModuleService $modules, + SpecialtyShellService $shell, + PsychiatryAnalyticsService $analytics, + ): View { + $this->authorizeAbility($request, 'consultations.view'); + $this->assertPsychiatryAccess($request, $modules); + + $organization = $this->organization($request); + $branchScope = app(\App\Services\Care\OrganizationResolver::class)->branchScope($this->member($request)); + + $report = $analytics->report( + $organization, + $this->ownerRef($request), + $branchScope, + $request->query('from'), + $request->query('to'), + ); + + return view('care.specialty.psychiatry.reports', [ + 'moduleKey' => 'psychiatry', + 'definition' => $modules->definition('psychiatry'), + 'shellNav' => $shell->navItems('psychiatry'), + 'report' => $report, + 'currency' => strtoupper((string) config('care.billing.currency', config('care.currency', 'GHS'))), + ]); + } + + public function printSummary( + Request $request, + Visit $visit, + SpecialtyModuleService $modules, + SpecialtyClinicalRecordService $clinical, + ): View { + $this->authorizeAbility($request, 'consultations.view'); + $this->assertPsychiatryAccess($request, $modules); + $this->assertVisit($request, $visit); + + $visit->loadMissing(['patient', 'appointment.practitioner', 'branch']); + + return view('care.specialty.psychiatry.print', [ + 'visit' => $visit, + 'patient' => $visit->patient, + 'history' => $clinical->findForVisit($visit, 'psychiatry', 'mental_status'), + 'risk' => $clinical->findForVisit($visit, 'psychiatry', 'risk_assessment'), + 'plan' => $clinical->findForVisit($visit, 'psychiatry', 'psych_plan'), + 'followup' => $clinical->findForVisit($visit, 'psychiatry', 'psych_followup'), + ]); + } +} diff --git a/app/Http/Controllers/Care/RadiologyWorkspaceController.php b/app/Http/Controllers/Care/RadiologyWorkspaceController.php new file mode 100644 index 0000000..b1070b8 --- /dev/null +++ b/app/Http/Controllers/Care/RadiologyWorkspaceController.php @@ -0,0 +1,214 @@ +organization($request); + abort_unless($modules->memberCanAccess($organization, $this->member($request), 'radiology'), 403); + } + + protected function assertRadiologyManage(Request $request, SpecialtyModuleService $modules): void + { + $organization = $this->organization($request); + abort_unless($modules->memberCanManage($organization, $this->member($request), 'radiology'), 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 setStage( + Request $request, + Visit $visit, + SpecialtyModuleService $modules, + SpecialtyVisitStageService $stages, + SpecialtyShellService $shell, + ): RedirectResponse { + $this->authorizeAbility($request, 'consultations.manage'); + $this->assertRadiologyManage($request, $modules); + $this->assertVisit($request, $visit); + + $validated = $request->validate([ + 'stage' => ['required', 'string', 'max:32'], + ]); + + try { + $stages->setStage( + $this->organization($request), + $visit, + 'radiology', + $validated['stage'], + $this->ownerRef($request), + $this->actorRef($request), + ); + } catch (\InvalidArgumentException $e) { + return back()->with('error', $e->getMessage()); + } + + return redirect() + ->route('care.specialty.workspace', [ + 'module' => 'radiology', + 'visit' => $visit, + 'tab' => $shell->workspaceTabForStage('radiology', $validated['stage']), + ]) + ->with('success', 'Visit stage updated.'); + } + + public function saveVerification( + Request $request, + Visit $visit, + SpecialtyModuleService $modules, + SpecialtyClinicalRecordService $clinical, + SpecialtyVisitStageService $stages, + RadiologyWorkflowService $workflow, + ): RedirectResponse { + $this->authorizeAbility($request, 'consultations.manage'); + $this->assertRadiologyManage($request, $modules); + $this->assertVisit($request, $visit); + + $organization = $this->organization($request); + $owner = $this->ownerRef($request); + + $payload = (array) $request->input('payload', []); + foreach ($clinical->fieldsFor('radiology', 'imaging_verification') as $field) { + if (($field['type'] ?? '') === 'boolean') { + $payload[$field['name']] = $request->boolean('payload.'.$field['name']); + } + } + $request->merge(['payload' => $payload, 'tab' => 'verification']); + + $validated = $request->validate(array_merge([ + 'tab' => ['required', 'string'], + ], $clinical->validationRules('radiology', 'imaging_verification'))); + + $record = $clinical->upsert( + $organization, + $visit, + 'radiology', + 'imaging_verification', + $clinical->payloadFromRequest($validated), + $owner, + $owner, + RadiologyWorkflowService::STAGE_VERIFIED, + SpecialtyClinicalRecord::STATUS_COMPLETED, + ); + + try { + $stages->setStage( + $organization, + $visit, + 'radiology', + RadiologyWorkflowService::STAGE_VERIFIED, + $owner, + $owner, + ); + } catch (\InvalidArgumentException) { + } + + if ($workflow->shouldCompleteVisit($record->payload ?? [])) { + try { + $stages->setStage( + $organization, + $visit, + 'radiology', + RadiologyWorkflowService::STAGE_COMPLETED, + $owner, + $owner, + ); + } catch (\InvalidArgumentException) { + } + + $visit->refresh(); + if ($visit->status !== Visit::STATUS_COMPLETED) { + $visit->update([ + 'status' => Visit::STATUS_COMPLETED, + 'completed_at' => $visit->completed_at ?? now(), + ]); + } + + $appointment = $visit->appointment; + if ($appointment && $appointment->status !== \App\Models\Appointment::STATUS_COMPLETED) { + $appointment->update([ + 'status' => \App\Models\Appointment::STATUS_COMPLETED, + 'completed_at' => $appointment->completed_at ?? now(), + ]); + } + } + + return redirect() + ->route('care.specialty.workspace', ['module' => 'radiology', 'visit' => $visit, 'tab' => 'verification']) + ->with('success', 'Verification saved.'); + } + + public function reports( + Request $request, + SpecialtyModuleService $modules, + SpecialtyShellService $shell, + RadiologyAnalyticsService $analytics, + ): View { + $this->authorizeAbility($request, 'consultations.view'); + $this->assertRadiologyAccess($request, $modules); + + $organization = $this->organization($request); + $branchScope = app(\App\Services\Care\OrganizationResolver::class)->branchScope($this->member($request)); + + $report = $analytics->report( + $organization, + $this->ownerRef($request), + $branchScope, + $request->query('from'), + $request->query('to'), + ); + + return view('care.specialty.radiology.reports', [ + 'moduleKey' => 'radiology', + 'definition' => $modules->definition('radiology'), + 'shellNav' => $shell->navItems('radiology'), + 'report' => $report, + 'currency' => strtoupper((string) config('care.billing.currency', config('care.currency', 'GHS'))), + ]); + } + + public function printSummary( + Request $request, + Visit $visit, + SpecialtyModuleService $modules, + SpecialtyClinicalRecordService $clinical, + ): View { + $this->authorizeAbility($request, 'consultations.view'); + $this->assertRadiologyAccess($request, $modules); + $this->assertVisit($request, $visit); + + $visit->loadMissing(['patient', 'appointment.practitioner', 'branch']); + + return view('care.specialty.radiology.print', [ + 'visit' => $visit, + 'patient' => $visit->patient, + 'imagingRequest' => $clinical->findForVisit($visit, 'radiology', 'imaging_request'), + 'acquisition' => $clinical->findForVisit($visit, 'radiology', 'imaging_acquisition'), + 'imagingReport' => $clinical->findForVisit($visit, 'radiology', 'imaging_report'), + 'verification' => $clinical->findForVisit($visit, 'radiology', 'imaging_verification'), + ]); + } +} diff --git a/app/Http/Controllers/Care/SpecialtyModuleController.php b/app/Http/Controllers/Care/SpecialtyModuleController.php index 6fdde2d..9f3faac 100644 --- a/app/Http/Controllers/Care/SpecialtyModuleController.php +++ b/app/Http/Controllers/Care/SpecialtyModuleController.php @@ -156,6 +156,9 @@ class SpecialtyModuleController extends Controller 'ophthalmology' => 'refraction', 'physiotherapy' => 'assessment', 'maternity' => 'history', + 'radiology' => 'protocol', + 'cardiology' => 'history', + 'psychiatry' => 'history', default => (collect(array_keys($shellTabs)) ->first(fn (string $tab) => $clinical->recordTypeForTab($module, $tab) !== null) ?? 'clinical_notes'), @@ -230,6 +233,9 @@ class SpecialtyModuleController extends Controller 'ophthalmology' => 'refraction', 'physiotherapy' => 'assessment', 'maternity' => 'history', + 'radiology' => 'protocol', + 'cardiology' => 'history', + 'psychiatry' => 'history', default => (collect(array_keys($shellTabs)) ->first(fn (string $tab) => $clinical->recordTypeForTab($module, $tab) !== null) ?? 'clinical_notes'), @@ -510,6 +516,146 @@ class SpecialtyModuleController extends Controller } } + if ($module === 'radiology' && in_array($recordType, ['imaging_request', 'imaging_acquisition', 'imaging_report'], true)) { + $workflow = app(\App\Services\Care\Radiology\RadiologyWorkflowService::class); + $stageService = app(\App\Services\Care\SpecialtyVisitStageService::class); + $suggested = match ($recordType) { + 'imaging_request' => $workflow->stageFromRequest($record->payload ?? []), + 'imaging_acquisition' => $workflow->stageFromAcquisition($record->payload ?? []), + 'imaging_report' => $workflow->stageFromReport($record->payload ?? []), + default => null, + }; + $current = $visit->specialty_stage; + $early = ['check_in', 'waiting', 'request', null, '']; + $order = [ + 'check_in' => 0, + 'protocol' => 1, + 'acquisition' => 2, + 'reporting' => 3, + 'verified' => 4, + 'completed' => 5, + ]; + if ($suggested && (! $current || in_array($current, $early, true))) { + try { + $stageService->setStage( + $organization, + $visit, + 'radiology', + $suggested, + $this->ownerRef($request), + $this->ownerRef($request), + ); + } catch (\InvalidArgumentException) { + } + } elseif ($suggested && $suggested !== $current && ($order[$suggested] ?? 0) > ($order[$current] ?? 0)) { + try { + $stageService->setStage( + $organization, + $visit, + 'radiology', + $suggested, + $this->ownerRef($request), + $this->ownerRef($request), + ); + } catch (\InvalidArgumentException) { + } + } + } + + if ($module === 'cardiology' && in_array($recordType, ['cardio_history', 'cardiac_exam', 'cardio_investigation', 'cardio_plan'], true)) { + $workflow = app(\App\Services\Care\Cardiology\CardiologyWorkflowService::class); + $stageService = app(\App\Services\Care\SpecialtyVisitStageService::class); + $suggested = match ($recordType) { + 'cardio_history' => $workflow->stageFromHistory($record->payload ?? []), + 'cardiac_exam' => $workflow->stageFromExam($record->payload ?? []), + 'cardio_investigation' => $workflow->stageFromInvestigation($record->payload ?? []), + 'cardio_plan' => $workflow->stageFromPlan($record->payload ?? []), + default => null, + }; + $current = $visit->specialty_stage; + $early = ['check_in', 'waiting', null, '']; + $order = [ + 'check_in' => 0, + 'history' => 1, + 'exam' => 2, + 'investigation' => 3, + 'plan' => 4, + 'procedure' => 5, + 'completed' => 6, + ]; + if ($suggested && (! $current || in_array($current, $early, true))) { + try { + $stageService->setStage( + $organization, + $visit, + 'cardiology', + $suggested, + $this->ownerRef($request), + $this->ownerRef($request), + ); + } catch (\InvalidArgumentException) { + } + } elseif ($suggested && $suggested !== $current && ($order[$suggested] ?? 0) > ($order[$current] ?? 0)) { + try { + $stageService->setStage( + $organization, + $visit, + 'cardiology', + $suggested, + $this->ownerRef($request), + $this->ownerRef($request), + ); + } catch (\InvalidArgumentException) { + } + } + } + + if ($module === 'psychiatry' && in_array($recordType, ['mental_status', 'risk_assessment', 'psych_plan'], true)) { + $workflow = app(\App\Services\Care\Psychiatry\PsychiatryWorkflowService::class); + $stageService = app(\App\Services\Care\SpecialtyVisitStageService::class); + $suggested = match ($recordType) { + 'mental_status' => $workflow->stageFromHistory($record->payload ?? []), + 'risk_assessment' => $workflow->stageFromRisk($record->payload ?? []), + 'psych_plan' => $workflow->stageFromFormulation($record->payload ?? []), + default => null, + }; + $current = $visit->specialty_stage; + $early = ['check_in', 'waiting', null, '']; + $order = [ + 'check_in' => 0, + 'history' => 1, + 'risk' => 2, + 'formulation' => 3, + 'review' => 4, + 'completed' => 5, + ]; + if ($suggested && (! $current || in_array($current, $early, true))) { + try { + $stageService->setStage( + $organization, + $visit, + 'psychiatry', + $suggested, + $this->ownerRef($request), + $this->ownerRef($request), + ); + } catch (\InvalidArgumentException) { + } + } elseif ($suggested && $suggested !== $current && ($order[$suggested] ?? 0) > ($order[$current] ?? 0)) { + try { + $stageService->setStage( + $organization, + $visit, + 'psychiatry', + $suggested, + $this->ownerRef($request), + $this->ownerRef($request), + ); + } catch (\InvalidArgumentException) { + } + } + } + return redirect() ->route('care.specialty.workspace', [ 'module' => $module, @@ -926,6 +1072,25 @@ class SpecialtyModuleController extends Controller $maternityPostnatal = null; $maternityStageCodes = []; $maternityStageFlow = []; + $radiologyRequest = null; + $radiologyAcquisition = null; + $radiologyReport = null; + $radiologyVerification = null; + $radiologyStageCodes = []; + $radiologyStageFlow = []; + $cardiologyHistory = null; + $cardiologyExam = null; + $cardiologyInvestigation = null; + $cardiologyPlan = null; + $cardiologyProcedure = null; + $cardiologyStageCodes = []; + $cardiologyStageFlow = []; + $psychiatryHistory = null; + $psychiatryRisk = null; + $psychiatryPlan = null; + $psychiatryFollowup = null; + $psychiatryStageCodes = []; + $psychiatryStageFlow = []; $activeTab = (string) $request->query('tab', array_key_first($shell->workspaceTabs($module)) ?: 'overview'); $clinical = app(SpecialtyClinicalRecordService::class); @@ -1062,6 +1227,34 @@ class SpecialtyModuleController extends Controller $maternityStageFlow = app(\App\Services\Care\Maternity\MaternityWorkflowService::class)->stageFlow(); } + if ($module === 'radiology') { + $radiologyRequest = $clinical->findForVisit($workspaceVisit, 'radiology', 'imaging_request'); + $radiologyAcquisition = $clinical->findForVisit($workspaceVisit, 'radiology', 'imaging_acquisition'); + $radiologyReport = $clinical->findForVisit($workspaceVisit, 'radiology', 'imaging_report'); + $radiologyVerification = $clinical->findForVisit($workspaceVisit, 'radiology', 'imaging_verification'); + $radiologyStageCodes = collect($shell->stages('radiology'))->pluck('code')->all(); + $radiologyStageFlow = app(\App\Services\Care\Radiology\RadiologyWorkflowService::class)->stageFlow(); + } + + if ($module === 'cardiology') { + $cardiologyHistory = $clinical->findForVisit($workspaceVisit, 'cardiology', 'cardio_history'); + $cardiologyExam = $clinical->findForVisit($workspaceVisit, 'cardiology', 'cardiac_exam'); + $cardiologyInvestigation = $clinical->findForVisit($workspaceVisit, 'cardiology', 'cardio_investigation'); + $cardiologyPlan = $clinical->findForVisit($workspaceVisit, 'cardiology', 'cardio_plan'); + $cardiologyProcedure = $clinical->findForVisit($workspaceVisit, 'cardiology', 'cardio_procedure'); + $cardiologyStageCodes = collect($shell->stages('cardiology'))->pluck('code')->all(); + $cardiologyStageFlow = app(\App\Services\Care\Cardiology\CardiologyWorkflowService::class)->stageFlow(); + } + + if ($module === 'psychiatry') { + $psychiatryHistory = $clinical->findForVisit($workspaceVisit, 'psychiatry', 'mental_status'); + $psychiatryRisk = $clinical->findForVisit($workspaceVisit, 'psychiatry', 'risk_assessment'); + $psychiatryPlan = $clinical->findForVisit($workspaceVisit, 'psychiatry', 'psych_plan'); + $psychiatryFollowup = $clinical->findForVisit($workspaceVisit, 'psychiatry', 'psych_followup'); + $psychiatryStageCodes = collect($shell->stages('psychiatry'))->pluck('code')->all(); + $psychiatryStageFlow = app(\App\Services\Care\Psychiatry\PsychiatryWorkflowService::class)->stageFlow(); + } + if ($patientHeader !== null) { $patientHeader['clinical_alerts'] = $clinicalAlerts; } @@ -1170,6 +1363,25 @@ class SpecialtyModuleController extends Controller 'maternityPostnatal' => $maternityPostnatal, 'maternityStageCodes' => $maternityStageCodes, 'maternityStageFlow' => $maternityStageFlow, + 'radiologyRequest' => $radiologyRequest, + 'radiologyAcquisition' => $radiologyAcquisition, + 'radiologyReport' => $radiologyReport, + 'radiologyVerification' => $radiologyVerification, + 'radiologyStageCodes' => $radiologyStageCodes, + 'radiologyStageFlow' => $radiologyStageFlow, + 'cardiologyHistory' => $cardiologyHistory, + 'cardiologyExam' => $cardiologyExam, + 'cardiologyInvestigation' => $cardiologyInvestigation, + 'cardiologyPlan' => $cardiologyPlan, + 'cardiologyProcedure' => $cardiologyProcedure, + 'cardiologyStageCodes' => $cardiologyStageCodes, + 'cardiologyStageFlow' => $cardiologyStageFlow, + 'psychiatryHistory' => $psychiatryHistory, + 'psychiatryRisk' => $psychiatryRisk, + 'psychiatryPlan' => $psychiatryPlan, + 'psychiatryFollowup' => $psychiatryFollowup, + 'psychiatryStageCodes' => $psychiatryStageCodes, + 'psychiatryStageFlow' => $psychiatryStageFlow, 'queueStubs' => $modules->queueStubsFor($organization, $module), 'branchId' => $branchId, 'branchLabel' => $branchLabel, diff --git a/app/Services/Care/Cardiology/CardiologyAnalyticsService.php b/app/Services/Care/Cardiology/CardiologyAnalyticsService.php new file mode 100644 index 0000000..b5f3c2c --- /dev/null +++ b/app/Services/Care/Cardiology/CardiologyAnalyticsService.php @@ -0,0 +1,168 @@ +startOfDay(); + $rangeFrom = $from ? Carbon::parse($from)->startOfDay() : now()->startOfMonth(); + $rangeTo = $to ? Carbon::parse($to)->endOfDay() : now()->endOfDay(); + + $departmentIds = $this->shell->departmentIds($organization, 'cardiology', $ownerRef, $branchId); + + $appointmentBase = Appointment::owned($ownerRef) + ->where('organization_id', $organization->id) + ->when($departmentIds !== [], fn ($q) => $q->whereIn('department_id', $departmentIds)) + ->when($departmentIds === [], fn ($q) => $q->whereRaw('1 = 0')) + ->when($branchId, fn ($q) => $q->where('branch_id', $branchId)); + + $arrivalsToday = (clone $appointmentBase) + ->where('checked_in_at', '>=', $todayStart) + ->count(); + + $completedToday = (clone $appointmentBase) + ->where('status', Appointment::STATUS_COMPLETED) + ->where('completed_at', '>=', $todayStart) + ->count(); + + $visitIds = (clone $appointmentBase)->whereNotNull('visit_id')->pluck('visit_id'); + + $openVisitIds = Visit::owned($ownerRef) + ->where('organization_id', $organization->id) + ->whereIn('id', $visitIds->isEmpty() ? [0] : $visitIds) + ->whereIn('status', [Visit::STATUS_OPEN, Visit::STATUS_IN_PROGRESS]) + ->pluck('id'); + + $highRiskOpen = SpecialtyClinicalRecord::owned($ownerRef) + ->where('organization_id', $organization->id) + ->where('module_key', 'cardiology') + ->where('record_type', 'cardiac_exam') + ->whereIn('visit_id', $openVisitIds->isEmpty() ? [0] : $openVisitIds) + ->get() + ->filter(function (SpecialtyClinicalRecord $r) { + $nyha = (string) ($r->payload['nyha_class'] ?? ''); + + return in_array($nyha, ['III', 'IV'], true); + }) + ->count(); + + $planRecords = SpecialtyClinicalRecord::owned($ownerRef) + ->where('organization_id', $organization->id) + ->where('module_key', 'cardiology') + ->where('record_type', 'cardio_plan') + ->when($branchId, fn ($q) => $q->where('branch_id', $branchId)) + ->whereBetween('recorded_at', [$rangeFrom, $rangeTo]) + ->get(); + + $diagnosisBreakdown = $planRecords + ->groupBy(fn (SpecialtyClinicalRecord $r) => (string) ($r->payload['diagnosis'] ?? 'Unknown')) + ->map(fn (Collection $rows, string $diagnosis) => [ + 'diagnosis' => $diagnosis, + 'total' => $rows->count(), + ]) + ->values() + ->sortByDesc('total') + ->values(); + + $procedureRecords = SpecialtyClinicalRecord::owned($ownerRef) + ->where('organization_id', $organization->id) + ->where('module_key', 'cardiology') + ->where('record_type', 'cardio_procedure') + ->when($branchId, fn ($q) => $q->where('branch_id', $branchId)) + ->whereBetween('recorded_at', [$rangeFrom, $rangeTo]) + ->get(); + + $procedureBreakdown = $procedureRecords + ->groupBy(fn (SpecialtyClinicalRecord $r) => (string) ($r->payload['procedure'] ?? 'Unknown')) + ->map(fn (Collection $rows, string $procedure) => [ + 'procedure' => $procedure, + 'total' => $rows->count(), + ]) + ->values() + ->sortByDesc('total') + ->values(); + + $completedVisits = Visit::owned($ownerRef) + ->where('organization_id', $organization->id) + ->whereIn('id', $visitIds->isEmpty() ? [0] : $visitIds) + ->where('status', Visit::STATUS_COMPLETED) + ->whereBetween('completed_at', [$rangeFrom, $rangeTo]) + ->whereNotNull('checked_in_at') + ->whereNotNull('completed_at') + ->get(); + + $avgLos = null; + if ($completedVisits->isNotEmpty()) { + $avgLos = round($completedVisits->avg(function (Visit $visit) { + return $visit->checked_in_at->diffInMinutes($visit->completed_at); + }), 1); + } + + $serviceLabels = collect($this->shell->provisionedServices($organization, 'cardiology')) + ->pluck('label') + ->filter() + ->values() + ->all(); + + $revenueByService = BillLineItem::query() + ->where('owner_ref', $ownerRef) + ->where('source_type', 'specialty_service') + ->whereIn('description', $serviceLabels ?: ['__none__']) + ->whereBetween('created_at', [$rangeFrom, $rangeTo]) + ->whereHas('bill', function ($q) use ($organization, $visitIds) { + $q->where('organization_id', $organization->id) + ->whereIn('visit_id', $visitIds->isEmpty() ? [0] : $visitIds) + ->whereNotIn('status', [Bill::STATUS_VOID]); + }) + ->selectRaw('description, count(*) as total, sum(total_minor) as amount_minor') + ->groupBy('description') + ->orderByDesc('amount_minor') + ->get(); + + return [ + 'arrivals_today' => $arrivalsToday, + 'completed_today' => $completedToday, + 'high_risk_open' => $highRiskOpen, + 'diagnosis_breakdown' => $diagnosisBreakdown, + 'procedure_breakdown' => $procedureBreakdown, + 'avg_los_minutes' => $avgLos, + 'revenue_by_service' => $revenueByService, + 'from' => $rangeFrom->toDateString(), + 'to' => $rangeTo->toDateString(), + ]; + } +} diff --git a/app/Services/Care/Cardiology/CardiologyWorkflowService.php b/app/Services/Care/Cardiology/CardiologyWorkflowService.php new file mode 100644 index 0000000..f760d68 --- /dev/null +++ b/app/Services/Care/Cardiology/CardiologyWorkflowService.php @@ -0,0 +1,104 @@ + $payload + */ + public function stageFromHistory(array $payload): string + { + $history = trim((string) ($payload['history'] ?? '')); + if ($history !== '') { + return self::STAGE_HISTORY; + } + + return self::STAGE_CHECK_IN; + } + + /** + * @param array $payload + */ + public function stageFromExam(array $payload): string + { + if (! empty($payload['ecg_findings']) || ! empty($payload['chief_complaint'])) { + return self::STAGE_EXAM; + } + + return self::STAGE_HISTORY; + } + + /** + * @param array $payload + */ + public function stageFromInvestigation(array $payload): string + { + $findings = trim((string) ($payload['findings'] ?? '')); + if ($findings !== '') { + return self::STAGE_INVESTIGATION; + } + + return self::STAGE_EXAM; + } + + /** + * @param array $payload + */ + public function stageFromPlan(array $payload): string + { + if (! empty($payload['procedure_planned'])) { + return self::STAGE_PROCEDURE; + } + + $plan = trim((string) ($payload['plan'] ?? '')); + if ($plan !== '') { + return self::STAGE_PLAN; + } + + return self::STAGE_INVESTIGATION; + } + + /** + * @param array $payload + */ + public function shouldCompleteVisit(array $payload): bool + { + $outcome = strtolower((string) ($payload['outcome'] ?? '')); + + return str_contains($outcome, 'completed'); + } + + /** + * @return array + */ + public function stageFlow(): array + { + return [ + self::STAGE_CHECK_IN => ['next' => self::STAGE_HISTORY, 'label' => 'Start history'], + self::STAGE_HISTORY => ['next' => self::STAGE_EXAM, 'label' => 'Move to exam / ECG'], + self::STAGE_EXAM => ['next' => self::STAGE_INVESTIGATION, 'label' => 'Move to investigations'], + self::STAGE_INVESTIGATION => ['next' => self::STAGE_PLAN, 'label' => 'Move to diagnosis & plan'], + self::STAGE_PLAN => ['next' => self::STAGE_PROCEDURE, 'label' => 'Move to procedure'], + self::STAGE_PROCEDURE => ['next' => self::STAGE_COMPLETED, 'label' => 'Complete visit'], + self::STAGE_COMPLETED => ['next' => self::STAGE_COMPLETED, 'label' => 'Completed'], + ]; + } +} diff --git a/app/Services/Care/DemoTenantSeeder.php b/app/Services/Care/DemoTenantSeeder.php index b366272..a553c57 100644 --- a/app/Services/Care/DemoTenantSeeder.php +++ b/app/Services/Care/DemoTenantSeeder.php @@ -1017,6 +1017,9 @@ class DemoTenantSeeder $this->seedOphthalmologyClinicalDemo($organization, $ownerRef); $this->seedPhysiotherapyClinicalDemo($organization, $ownerRef); $this->seedMaternityClinicalDemo($organization, $ownerRef); + $this->seedRadiologyClinicalDemo($organization, $ownerRef); + $this->seedCardiologyClinicalDemo($organization, $ownerRef); + $this->seedPsychiatryClinicalDemo($organization, $ownerRef); return $count; } @@ -1507,6 +1510,352 @@ class DemoTenantSeeder } } + private function seedRadiologyClinicalDemo(Organization $organization, string $ownerRef): void + { + $branchIds = Branch::owned($ownerRef) + ->where('organization_id', $organization->id) + ->pluck('id'); + + if ($branchIds->isEmpty()) { + return; + } + + $departmentIds = Department::owned($ownerRef) + ->whereIn('branch_id', $branchIds) + ->where('type', 'radiology') + ->where('is_active', true) + ->pluck('id'); + + if ($departmentIds->isEmpty()) { + return; + } + + $appointments = Appointment::owned($ownerRef) + ->where('organization_id', $organization->id) + ->whereIn('department_id', $departmentIds) + ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) + ->whereNotNull('visit_id') + ->with('visit') + ->orderBy('branch_id') + ->orderBy('id') + ->get(); + + if ($appointments->isEmpty()) { + return; + } + + $clinical = app(SpecialtyClinicalRecordService::class); + $index = 0; + + foreach ($appointments as $appointment) { + $visit = $appointment->visit; + if (! $visit) { + continue; + } + + $urgent = $index === 0; + $clinical->upsert( + $organization, + $visit, + 'radiology', + 'imaging_request', + [ + 'modality' => $urgent ? 'CT' : 'X-ray', + 'body_part' => $urgent ? 'Chest' : 'Left ankle', + 'clinical_info' => $urgent + ? 'Shortness of breath; rule out PE / infiltrates' + : 'Trauma — query fracture after fall', + 'urgency' => $urgent ? 'STAT' : 'Routine', + 'contrast' => $urgent ? 'IV' : 'None', + 'protocol' => $urgent ? 'CTPA protocol' : 'AP / lateral', + 'referrer' => 'Demo ER / Ortho', + ], + $ownerRef, + $ownerRef, + 'protocol', + ); + + $clinical->upsert( + $organization, + $visit, + 'radiology', + 'imaging_acquisition', + [ + 'technologist' => 'Demo Tech', + 'equipment' => $urgent ? 'CT 1' : 'XR Bay A', + 'acquisition_status' => 'Acquired', + 'contrast_given' => $urgent, + 'dose_notes' => $urgent ? 'Standard CTPA dose' : 'Low dose extremity', + 'quality' => 'Diagnostic', + 'notes' => 'Demo acquisition for radiology suite.', + ], + $ownerRef, + $ownerRef, + 'acquisition', + ); + + if ($urgent) { + $clinical->upsert( + $organization, + $visit, + 'radiology', + 'imaging_report', + [ + 'technique' => 'CT pulmonary angiogram with IV contrast', + 'comparison' => 'None available', + 'findings' => 'No filling defect in main or segmental pulmonary arteries. Mild basilar atelectasis.', + 'impression' => 'No CT evidence of pulmonary embolism.', + 'recommendations' => 'Clinical correlation; consider alternative causes of dyspnoea.', + 'critical_result' => false, + ], + $ownerRef, + $ownerRef, + 'reporting', + ); + } + + if (! $visit->specialty_stage) { + $visit->update(['specialty_stage' => $urgent ? 'reporting' : 'acquisition']); + } + + $index++; + } + } + + private function seedCardiologyClinicalDemo(Organization $organization, string $ownerRef): void + { + $branchIds = Branch::owned($ownerRef) + ->where('organization_id', $organization->id) + ->pluck('id'); + + if ($branchIds->isEmpty()) { + return; + } + + $departmentIds = Department::owned($ownerRef) + ->whereIn('branch_id', $branchIds) + ->where('type', 'cardiology') + ->where('is_active', true) + ->pluck('id'); + + if ($departmentIds->isEmpty()) { + return; + } + + $appointments = Appointment::owned($ownerRef) + ->where('organization_id', $organization->id) + ->whereIn('department_id', $departmentIds) + ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) + ->whereNotNull('visit_id') + ->with('visit') + ->orderBy('branch_id') + ->orderBy('id') + ->get(); + + if ($appointments->isEmpty()) { + return; + } + + $clinical = app(SpecialtyClinicalRecordService::class); + $index = 0; + + foreach ($appointments as $appointment) { + $visit = $appointment->visit; + if (! $visit) { + continue; + } + + $highRisk = $index === 0; + $clinical->upsert( + $organization, + $visit, + 'cardiology', + 'cardio_history', + [ + 'history' => $highRisk + ? 'Progressive dyspnoea and orthopnoea for 2 weeks' + : 'Palpitations intermittent for 1 month', + 'past_cardiac' => $highRisk ? 'Prior MI 2019' : 'None', + 'medications' => $highRisk ? 'Aspirin, atorvastatin, bisoprolol' : 'None', + 'allergies' => 'NKDA', + 'risk_factors' => $highRisk ? 'Hypertension, diabetes, smoking history' : 'Family history of AF', + 'family_history' => $highRisk ? 'Father CAD' : 'Mother AF', + ], + $ownerRef, + $ownerRef, + 'history', + ); + + $clinical->upsert( + $organization, + $visit, + 'cardiology', + 'cardiac_exam', + [ + 'chief_complaint' => $highRisk ? 'Dyspnoea / heart failure symptoms' : 'Palpitations', + 'nyha_class' => $highRisk ? 'III' : 'I', + 'bp' => $highRisk ? '168/98' : '124/78', + 'hr' => $highRisk ? 104 : 72, + 'heart_sounds' => $highRisk ? 'S3; soft pansystolic murmur' : 'Normal S1 S2; no murmur', + 'ecg_findings' => $highRisk + ? 'Sinus tach; Q waves inferiorly; no acute STEMI' + : 'Sinus rhythm; occasional PACs', + 'exam_findings' => $highRisk ? 'Bilateral basal crackles; mild ankle oedema' : 'CVD exam normal', + ], + $ownerRef, + $ownerRef, + 'exam', + ); + + if ($highRisk) { + $clinical->upsert( + $organization, + $visit, + 'cardiology', + 'cardio_plan', + [ + 'diagnosis' => 'Decompensated heart failure (NYHA III)', + 'plan' => 'Diuresis; echo; review meds; admit if not improving', + 'medications' => 'Increase loop diuretic; continue GDMT', + 'procedure_planned' => false, + 'follow_up' => '48-hour review or sooner if worse', + 'advice' => 'Daily weights; fluid restriction counselling', + ], + $ownerRef, + $ownerRef, + 'plan', + ); + } + + if (! $visit->specialty_stage) { + $visit->update(['specialty_stage' => $highRisk ? 'plan' : 'exam']); + } + + $index++; + } + } + + private function seedPsychiatryClinicalDemo(Organization $organization, string $ownerRef): void + { + $branchIds = Branch::owned($ownerRef) + ->where('organization_id', $organization->id) + ->pluck('id'); + + if ($branchIds->isEmpty()) { + return; + } + + $departmentIds = Department::owned($ownerRef) + ->whereIn('branch_id', $branchIds) + ->where('type', 'psychiatry') + ->where('is_active', true) + ->pluck('id'); + + if ($departmentIds->isEmpty()) { + return; + } + + $appointments = Appointment::owned($ownerRef) + ->where('organization_id', $organization->id) + ->whereIn('department_id', $departmentIds) + ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) + ->whereNotNull('visit_id') + ->with('visit') + ->orderBy('branch_id') + ->orderBy('id') + ->get(); + + if ($appointments->isEmpty()) { + return; + } + + $clinical = app(SpecialtyClinicalRecordService::class); + $index = 0; + + foreach ($appointments as $appointment) { + $visit = $appointment->visit; + if (! $visit) { + continue; + } + + $highRisk = $index === 0; + $clinical->upsert( + $organization, + $visit, + 'psychiatry', + 'mental_status', + [ + 'chief_complaint' => $highRisk ? 'Low mood with suicidal ideation' : 'Anxiety and poor sleep', + 'history' => $highRisk + ? '2-week worsening depression; sleep and appetite reduced' + : 'Generalised worry for 3 months; work stress', + 'appearance' => $highRisk ? 'Dishevelled; limited eye contact' : 'Well kempt; cooperative', + 'mood_affect' => $highRisk ? 'Depressed / flat' : 'Anxious / congruent', + 'thought' => $highRisk ? 'Hopeless themes; no psychosis' : 'Worried ruminations; no delusions', + 'perception' => 'No hallucinations', + 'cognition' => 'Oriented ×3', + 'insight' => $highRisk ? 'Partial' : 'Good', + 'mse_summary' => $highRisk + ? 'Depressed MSE with active suicidal ideation — high vigilance' + : 'Anxious MSE without psychosis or self-harm intent', + ], + $ownerRef, + $ownerRef, + 'history', + ); + + $clinical->upsert( + $organization, + $visit, + 'psychiatry', + 'risk_assessment', + [ + 'overall_risk' => $highRisk ? 'High' : 'Low', + 'self_harm' => $highRisk ? 'Ideation' : 'None identified', + 'harm_others' => 'None identified', + 'safeguarding' => $highRisk ? 'Lives alone; limited support tonight' : 'Supportive partner', + 'protective_factors' => $highRisk ? 'Engaged with clinic; no prior attempts' : 'Good insight; coping strategies', + 'risk_summary' => $highRisk + ? 'High risk of self-harm — safety planning and close follow-up required' + : 'Low risk; outpatient management appropriate', + 'immediate_actions' => $highRisk + ? 'Safety plan; remove means; same-day senior review' + : 'Psychoeducation; follow-up in clinic', + ], + $ownerRef, + $ownerRef, + 'risk', + ); + + if ($highRisk) { + $clinical->upsert( + $organization, + $visit, + 'psychiatry', + 'psych_plan', + [ + 'formulation' => 'Major depressive episode with high self-harm risk in context of isolation', + 'diagnosis' => 'Major depressive disorder — severe', + 'plan' => 'Crisis safety plan; consider short admission vs intensive outpatient; start/adjust antidepressant', + 'medications' => 'Review current AD; discuss options', + 'therapy' => 'CBT referral when risk settles', + 'follow_up' => '24–48 hours or sooner if crisis', + 'crisis_plan' => 'Emergency contacts; crisis line; return to ED if ideation intensifies', + ], + $ownerRef, + $ownerRef, + 'formulation', + ); + } + + if (! $visit->specialty_stage) { + $visit->update(['specialty_stage' => $highRisk ? 'formulation' : 'risk']); + } + + $index++; + } + } + private function seedDentistryClinicalDemo(Organization $organization, string $ownerRef): void { // Specialty departments are provisioned without organization_id; scope via branches. diff --git a/app/Services/Care/Psychiatry/PsychiatryAnalyticsService.php b/app/Services/Care/Psychiatry/PsychiatryAnalyticsService.php new file mode 100644 index 0000000..18de2cd --- /dev/null +++ b/app/Services/Care/Psychiatry/PsychiatryAnalyticsService.php @@ -0,0 +1,168 @@ +startOfDay(); + $rangeFrom = $from ? Carbon::parse($from)->startOfDay() : now()->startOfMonth(); + $rangeTo = $to ? Carbon::parse($to)->endOfDay() : now()->endOfDay(); + + $departmentIds = $this->shell->departmentIds($organization, 'psychiatry', $ownerRef, $branchId); + + $appointmentBase = Appointment::owned($ownerRef) + ->where('organization_id', $organization->id) + ->when($departmentIds !== [], fn ($q) => $q->whereIn('department_id', $departmentIds)) + ->when($departmentIds === [], fn ($q) => $q->whereRaw('1 = 0')) + ->when($branchId, fn ($q) => $q->where('branch_id', $branchId)); + + $arrivalsToday = (clone $appointmentBase) + ->where('checked_in_at', '>=', $todayStart) + ->count(); + + $completedToday = (clone $appointmentBase) + ->where('status', Appointment::STATUS_COMPLETED) + ->where('completed_at', '>=', $todayStart) + ->count(); + + $visitIds = (clone $appointmentBase)->whereNotNull('visit_id')->pluck('visit_id'); + + $openVisitIds = Visit::owned($ownerRef) + ->where('organization_id', $organization->id) + ->whereIn('id', $visitIds->isEmpty() ? [0] : $visitIds) + ->whereIn('status', [Visit::STATUS_OPEN, Visit::STATUS_IN_PROGRESS]) + ->pluck('id'); + + $highRiskOpen = SpecialtyClinicalRecord::owned($ownerRef) + ->where('organization_id', $organization->id) + ->where('module_key', 'psychiatry') + ->where('record_type', 'risk_assessment') + ->whereIn('visit_id', $openVisitIds->isEmpty() ? [0] : $openVisitIds) + ->get() + ->filter(function (SpecialtyClinicalRecord $r) { + $level = strtolower((string) ($r->payload['overall_risk'] ?? '')); + + return str_contains($level, 'high') || str_contains($level, 'imminent'); + }) + ->count(); + + $riskRecords = SpecialtyClinicalRecord::owned($ownerRef) + ->where('organization_id', $organization->id) + ->where('module_key', 'psychiatry') + ->where('record_type', 'risk_assessment') + ->when($branchId, fn ($q) => $q->where('branch_id', $branchId)) + ->whereBetween('recorded_at', [$rangeFrom, $rangeTo]) + ->get(); + + $riskBreakdown = $riskRecords + ->groupBy(fn (SpecialtyClinicalRecord $r) => (string) ($r->payload['overall_risk'] ?? 'Unknown')) + ->map(fn (Collection $rows, string $risk) => [ + 'risk' => $risk, + 'total' => $rows->count(), + ]) + ->values() + ->sortByDesc('total') + ->values(); + + $reviewRecords = SpecialtyClinicalRecord::owned($ownerRef) + ->where('organization_id', $organization->id) + ->where('module_key', 'psychiatry') + ->where('record_type', 'psych_followup') + ->when($branchId, fn ($q) => $q->where('branch_id', $branchId)) + ->whereBetween('recorded_at', [$rangeFrom, $rangeTo]) + ->get(); + + $reviewOutcomes = $reviewRecords + ->groupBy(fn (SpecialtyClinicalRecord $r) => (string) ($r->payload['outcome'] ?? 'Unknown')) + ->map(fn (Collection $rows, string $outcome) => [ + 'outcome' => $outcome, + 'total' => $rows->count(), + ]) + ->values() + ->sortByDesc('total') + ->values(); + + $completedVisits = Visit::owned($ownerRef) + ->where('organization_id', $organization->id) + ->whereIn('id', $visitIds->isEmpty() ? [0] : $visitIds) + ->where('status', Visit::STATUS_COMPLETED) + ->whereBetween('completed_at', [$rangeFrom, $rangeTo]) + ->whereNotNull('checked_in_at') + ->whereNotNull('completed_at') + ->get(); + + $avgLos = null; + if ($completedVisits->isNotEmpty()) { + $avgLos = round($completedVisits->avg(function (Visit $visit) { + return $visit->checked_in_at->diffInMinutes($visit->completed_at); + }), 1); + } + + $serviceLabels = collect($this->shell->provisionedServices($organization, 'psychiatry')) + ->pluck('label') + ->filter() + ->values() + ->all(); + + $revenueByService = BillLineItem::query() + ->where('owner_ref', $ownerRef) + ->where('source_type', 'specialty_service') + ->whereIn('description', $serviceLabels ?: ['__none__']) + ->whereBetween('created_at', [$rangeFrom, $rangeTo]) + ->whereHas('bill', function ($q) use ($organization, $visitIds) { + $q->where('organization_id', $organization->id) + ->whereIn('visit_id', $visitIds->isEmpty() ? [0] : $visitIds) + ->whereNotIn('status', [Bill::STATUS_VOID]); + }) + ->selectRaw('description, count(*) as total, sum(total_minor) as amount_minor') + ->groupBy('description') + ->orderByDesc('amount_minor') + ->get(); + + return [ + 'arrivals_today' => $arrivalsToday, + 'completed_today' => $completedToday, + 'high_risk_open' => $highRiskOpen, + 'risk_breakdown' => $riskBreakdown, + 'review_outcomes' => $reviewOutcomes, + 'avg_los_minutes' => $avgLos, + 'revenue_by_service' => $revenueByService, + 'from' => $rangeFrom->toDateString(), + 'to' => $rangeTo->toDateString(), + ]; + } +} diff --git a/app/Services/Care/Psychiatry/PsychiatryWorkflowService.php b/app/Services/Care/Psychiatry/PsychiatryWorkflowService.php new file mode 100644 index 0000000..c197714 --- /dev/null +++ b/app/Services/Care/Psychiatry/PsychiatryWorkflowService.php @@ -0,0 +1,86 @@ + $payload + */ + public function stageFromHistory(array $payload): string + { + if (! empty($payload['chief_complaint']) || ! empty($payload['mse_summary'])) { + return self::STAGE_HISTORY; + } + + return self::STAGE_CHECK_IN; + } + + /** + * @param array $payload + */ + public function stageFromRisk(array $payload): string + { + $risk = trim((string) ($payload['risk_summary'] ?? $payload['risk'] ?? '')); + if ($risk !== '') { + return self::STAGE_RISK; + } + + return self::STAGE_HISTORY; + } + + /** + * @param array $payload + */ + public function stageFromFormulation(array $payload): string + { + $plan = trim((string) ($payload['plan'] ?? '')); + if ($plan !== '') { + return self::STAGE_FORMULATION; + } + + return self::STAGE_RISK; + } + + /** + * @param array $payload + */ + public function shouldCompleteVisit(array $payload): bool + { + $outcome = strtolower((string) ($payload['outcome'] ?? '')); + + return str_contains($outcome, 'discharged') + || str_contains($outcome, 'follow-up arranged') + || str_contains($outcome, 'completed'); + } + + /** + * @return array + */ + public function stageFlow(): array + { + return [ + self::STAGE_CHECK_IN => ['next' => self::STAGE_HISTORY, 'label' => 'Start history / MSE'], + self::STAGE_HISTORY => ['next' => self::STAGE_RISK, 'label' => 'Move to risk assessment'], + self::STAGE_RISK => ['next' => self::STAGE_FORMULATION, 'label' => 'Move to formulation / plan'], + self::STAGE_FORMULATION => ['next' => self::STAGE_REVIEW, 'label' => 'Move to follow-up review'], + self::STAGE_REVIEW => ['next' => self::STAGE_COMPLETED, 'label' => 'Complete visit'], + self::STAGE_COMPLETED => ['next' => self::STAGE_COMPLETED, 'label' => 'Completed'], + ]; + } +} diff --git a/app/Services/Care/Radiology/RadiologyAnalyticsService.php b/app/Services/Care/Radiology/RadiologyAnalyticsService.php new file mode 100644 index 0000000..ea65b40 --- /dev/null +++ b/app/Services/Care/Radiology/RadiologyAnalyticsService.php @@ -0,0 +1,168 @@ +startOfDay(); + $rangeFrom = $from ? Carbon::parse($from)->startOfDay() : now()->startOfMonth(); + $rangeTo = $to ? Carbon::parse($to)->endOfDay() : now()->endOfDay(); + + $departmentIds = $this->shell->departmentIds($organization, 'radiology', $ownerRef, $branchId); + + $appointmentBase = Appointment::owned($ownerRef) + ->where('organization_id', $organization->id) + ->when($departmentIds !== [], fn ($q) => $q->whereIn('department_id', $departmentIds)) + ->when($departmentIds === [], fn ($q) => $q->whereRaw('1 = 0')) + ->when($branchId, fn ($q) => $q->where('branch_id', $branchId)); + + $arrivalsToday = (clone $appointmentBase) + ->where('checked_in_at', '>=', $todayStart) + ->count(); + + $completedToday = (clone $appointmentBase) + ->where('status', Appointment::STATUS_COMPLETED) + ->where('completed_at', '>=', $todayStart) + ->count(); + + $visitIds = (clone $appointmentBase)->whereNotNull('visit_id')->pluck('visit_id'); + + $openVisitIds = Visit::owned($ownerRef) + ->where('organization_id', $organization->id) + ->whereIn('id', $visitIds->isEmpty() ? [0] : $visitIds) + ->whereIn('status', [Visit::STATUS_OPEN, Visit::STATUS_IN_PROGRESS]) + ->pluck('id'); + + $urgentOpen = SpecialtyClinicalRecord::owned($ownerRef) + ->where('organization_id', $organization->id) + ->where('module_key', 'radiology') + ->where('record_type', 'imaging_request') + ->whereIn('visit_id', $openVisitIds->isEmpty() ? [0] : $openVisitIds) + ->get() + ->filter(function (SpecialtyClinicalRecord $r) { + $urgency = strtolower((string) ($r->payload['urgency'] ?? '')); + + return str_contains($urgency, 'urgent') || str_contains($urgency, 'stat'); + }) + ->count(); + + $requestRecords = SpecialtyClinicalRecord::owned($ownerRef) + ->where('organization_id', $organization->id) + ->where('module_key', 'radiology') + ->where('record_type', 'imaging_request') + ->when($branchId, fn ($q) => $q->where('branch_id', $branchId)) + ->whereBetween('recorded_at', [$rangeFrom, $rangeTo]) + ->get(); + + $modalityBreakdown = $requestRecords + ->groupBy(fn (SpecialtyClinicalRecord $r) => (string) ($r->payload['modality'] ?? 'Unknown')) + ->map(fn (Collection $rows, string $modality) => [ + 'modality' => $modality, + 'total' => $rows->count(), + ]) + ->values() + ->sortByDesc('total') + ->values(); + + $verificationRecords = SpecialtyClinicalRecord::owned($ownerRef) + ->where('organization_id', $organization->id) + ->where('module_key', 'radiology') + ->where('record_type', 'imaging_verification') + ->when($branchId, fn ($q) => $q->where('branch_id', $branchId)) + ->whereBetween('recorded_at', [$rangeFrom, $rangeTo]) + ->get(); + + $verificationOutcomes = $verificationRecords + ->groupBy(fn (SpecialtyClinicalRecord $r) => (string) ($r->payload['verification_status'] ?? 'Unknown')) + ->map(fn (Collection $rows, string $status) => [ + 'status' => $status, + 'total' => $rows->count(), + ]) + ->values() + ->sortByDesc('total') + ->values(); + + $completedVisits = Visit::owned($ownerRef) + ->where('organization_id', $organization->id) + ->whereIn('id', $visitIds->isEmpty() ? [0] : $visitIds) + ->where('status', Visit::STATUS_COMPLETED) + ->whereBetween('completed_at', [$rangeFrom, $rangeTo]) + ->whereNotNull('checked_in_at') + ->whereNotNull('completed_at') + ->get(); + + $avgLos = null; + if ($completedVisits->isNotEmpty()) { + $avgLos = round($completedVisits->avg(function (Visit $visit) { + return $visit->checked_in_at->diffInMinutes($visit->completed_at); + }), 1); + } + + $serviceLabels = collect($this->shell->provisionedServices($organization, 'radiology')) + ->pluck('label') + ->filter() + ->values() + ->all(); + + $revenueByService = BillLineItem::query() + ->where('owner_ref', $ownerRef) + ->where('source_type', 'specialty_service') + ->whereIn('description', $serviceLabels ?: ['__none__']) + ->whereBetween('created_at', [$rangeFrom, $rangeTo]) + ->whereHas('bill', function ($q) use ($organization, $visitIds) { + $q->where('organization_id', $organization->id) + ->whereIn('visit_id', $visitIds->isEmpty() ? [0] : $visitIds) + ->whereNotIn('status', [Bill::STATUS_VOID]); + }) + ->selectRaw('description, count(*) as total, sum(total_minor) as amount_minor') + ->groupBy('description') + ->orderByDesc('amount_minor') + ->get(); + + return [ + 'arrivals_today' => $arrivalsToday, + 'completed_today' => $completedToday, + 'urgent_open' => $urgentOpen, + 'modality_breakdown' => $modalityBreakdown, + 'verification_outcomes' => $verificationOutcomes, + 'avg_los_minutes' => $avgLos, + 'revenue_by_service' => $revenueByService, + 'from' => $rangeFrom->toDateString(), + 'to' => $rangeTo->toDateString(), + ]; + } +} diff --git a/app/Services/Care/Radiology/RadiologyWorkflowService.php b/app/Services/Care/Radiology/RadiologyWorkflowService.php new file mode 100644 index 0000000..92eb7fd --- /dev/null +++ b/app/Services/Care/Radiology/RadiologyWorkflowService.php @@ -0,0 +1,85 @@ + $payload + */ + public function stageFromRequest(array $payload): string + { + if (! empty($payload['modality']) || ! empty($payload['body_part'])) { + return self::STAGE_PROTOCOL; + } + + return self::STAGE_CHECK_IN; + } + + /** + * @param array $payload + */ + public function stageFromAcquisition(array $payload): string + { + $status = strtolower((string) ($payload['acquisition_status'] ?? '')); + if (str_contains($status, 'complete') || str_contains($status, 'acquired')) { + return self::STAGE_ACQUISITION; + } + + return self::STAGE_PROTOCOL; + } + + /** + * @param array $payload + */ + public function stageFromReport(array $payload): string + { + $findings = trim((string) ($payload['findings'] ?? '')); + $impression = trim((string) ($payload['impression'] ?? '')); + if ($findings !== '' || $impression !== '') { + return self::STAGE_REPORTING; + } + + return self::STAGE_ACQUISITION; + } + + /** + * @param array $payload + */ + public function shouldCompleteVisit(array $payload): bool + { + $status = strtolower((string) ($payload['verification_status'] ?? '')); + + return str_contains($status, 'verified') || str_contains($status, 'final'); + } + + /** + * @return array + */ + public function stageFlow(): array + { + return [ + self::STAGE_CHECK_IN => ['next' => self::STAGE_PROTOCOL, 'label' => 'Start request / protocol'], + self::STAGE_PROTOCOL => ['next' => self::STAGE_ACQUISITION, 'label' => 'Move to acquisition'], + self::STAGE_ACQUISITION => ['next' => self::STAGE_REPORTING, 'label' => 'Move to reporting'], + self::STAGE_REPORTING => ['next' => self::STAGE_VERIFIED, 'label' => 'Move to verification'], + self::STAGE_VERIFIED => ['next' => self::STAGE_COMPLETED, 'label' => 'Complete study'], + self::STAGE_COMPLETED => ['next' => self::STAGE_COMPLETED, 'label' => 'Completed'], + ]; + } +} diff --git a/app/Services/Care/SpecialtyClinicalRecordService.php b/app/Services/Care/SpecialtyClinicalRecordService.php index 0dcf703..6c0652c 100644 --- a/app/Services/Care/SpecialtyClinicalRecordService.php +++ b/app/Services/Care/SpecialtyClinicalRecordService.php @@ -373,6 +373,77 @@ class SpecialtyClinicalRecordService } } + if ($moduleKey === 'radiology' && $recordType === 'imaging_request') { + $urgency = strtolower((string) ($payload['urgency'] ?? '')); + if (str_contains($urgency, 'stat')) { + $alerts[] = [ + 'code' => 'rad.stat', + 'severity' => 'critical', + 'message' => 'STAT imaging request.', + ]; + } elseif (str_contains($urgency, 'urgent')) { + $alerts[] = [ + 'code' => 'rad.urgent', + 'severity' => 'warning', + 'message' => 'Urgent imaging request.', + ]; + } + } + + if ($moduleKey === 'radiology' && $recordType === 'imaging_report') { + if (! empty($payload['critical_result'])) { + $alerts[] = [ + 'code' => 'rad.critical_result', + 'severity' => 'critical', + 'message' => 'Critical / unexpected imaging finding recorded.', + ]; + } + } + + if ($moduleKey === 'cardiology' && $recordType === 'cardiac_exam') { + $nyha = (string) ($payload['nyha_class'] ?? ''); + if (in_array($nyha, ['III', 'IV'], true)) { + $alerts[] = [ + 'code' => 'car.nyha_high', + 'severity' => $nyha === 'IV' ? 'critical' : 'warning', + 'message' => 'NYHA class '.$nyha.' on cardiac exam.', + ]; + } + $ecg = strtolower((string) ($payload['ecg_findings'] ?? '')); + if (str_contains($ecg, 'stemi') || str_contains($ecg, 'st elevation') || str_contains($ecg, 'vt') || str_contains($ecg, 'vf')) { + $alerts[] = [ + 'code' => 'car.ecg_critical', + 'severity' => 'critical', + 'message' => 'Critical ECG findings recorded.', + ]; + } + } + + if ($moduleKey === 'psychiatry' && $recordType === 'risk_assessment') { + $level = strtolower((string) ($payload['overall_risk'] ?? '')); + if (str_contains($level, 'imminent')) { + $alerts[] = [ + 'code' => 'psy.risk_imminent', + 'severity' => 'critical', + 'message' => 'Imminent risk — escalate immediately.', + ]; + } elseif (str_contains($level, 'high')) { + $alerts[] = [ + 'code' => 'psy.risk_high', + 'severity' => 'critical', + 'message' => 'High psychiatric risk recorded.', + ]; + } + $selfHarm = strtolower((string) ($payload['self_harm'] ?? '')); + if (str_contains($selfHarm, 'intent') || str_contains($selfHarm, 'attempt')) { + $alerts[] = [ + 'code' => 'psy.self_harm', + 'severity' => 'critical', + 'message' => 'Self-harm intent or recent attempt recorded.', + ]; + } + } + return $alerts; } diff --git a/app/Services/Care/SpecialtyShellService.php b/app/Services/Care/SpecialtyShellService.php index e6b4a53..73abd87 100644 --- a/app/Services/Care/SpecialtyShellService.php +++ b/app/Services/Care/SpecialtyShellService.php @@ -14,6 +14,9 @@ use App\Services\Care\Emergency\EmergencyWorkflowService; use App\Services\Care\Ophthalmology\OphthalmologyWorkflowService; use App\Services\Care\Physiotherapy\PhysiotherapyWorkflowService; use App\Services\Care\Maternity\MaternityWorkflowService; +use App\Services\Care\Radiology\RadiologyWorkflowService; +use App\Services\Care\Cardiology\CardiologyWorkflowService; +use App\Services\Care\Psychiatry\PsychiatryWorkflowService; use Illuminate\Support\Collection; /** @@ -60,6 +63,9 @@ class SpecialtyShellService 'ophthalmology' => 'care.specialty.ophthalmology.stage', 'physiotherapy' => 'care.specialty.physiotherapy.stage', 'maternity' => 'care.specialty.maternity.stage', + 'radiology' => 'care.specialty.radiology.stage', + 'cardiology' => 'care.specialty.cardiology.stage', + 'psychiatry' => 'care.specialty.psychiatry.stage', default => null, }; } @@ -77,6 +83,9 @@ class SpecialtyShellService 'ophthalmology' => app(OphthalmologyWorkflowService::class)->stageFlow(), 'physiotherapy' => app(PhysiotherapyWorkflowService::class)->stageFlow(), 'maternity' => app(MaternityWorkflowService::class)->stageFlow(), + 'radiology' => app(RadiologyWorkflowService::class)->stageFlow(), + 'cardiology' => app(CardiologyWorkflowService::class)->stageFlow(), + 'psychiatry' => app(PsychiatryWorkflowService::class)->stageFlow(), 'dentistry' => [ 'waiting' => ['next' => 'chair', 'label' => 'Seat at chair'], 'chair' => ['next' => 'procedure', 'label' => 'Start procedure'], @@ -376,7 +385,7 @@ class SpecialtyShellService ) { $code = (string) ($stage['code'] ?? ''); - if (in_array($moduleKey, ['dentistry', 'emergency', 'blood_bank', 'ophthalmology', 'physiotherapy', 'maternity'], true) && $visitIds->isNotEmpty()) { + if (in_array($moduleKey, ['dentistry', 'emergency', 'blood_bank', 'ophthalmology', 'physiotherapy', 'maternity', 'radiology', 'cardiology', 'psychiatry'], true) && $visitIds->isNotEmpty()) { $count = Visit::owned($ownerRef) ->where('organization_id', $organization->id) ->whereIn('id', $visitIds) @@ -409,6 +418,9 @@ class SpecialtyShellService 'ophthalmology' => $clinical->countOpenByType($organization, 'ophthalmology', 'eye_exam', $branchScope), 'physiotherapy' => $clinical->countOpenByType($organization, 'physiotherapy', 'pt_assessment', $branchScope), 'maternity' => $clinical->countOpenByType($organization, 'maternity', 'anc_history', $branchScope), + 'radiology' => $clinical->countOpenByType($organization, 'radiology', 'imaging_request', $branchScope), + 'cardiology' => $clinical->countOpenByType($organization, 'cardiology', 'cardiac_exam', $branchScope), + 'psychiatry' => $clinical->countOpenByType($organization, 'psychiatry', 'mental_status', $branchScope), 'dentistry' => \App\Models\DentalPlanItem::query() ->whereHas('plan', function ($q) use ($organization, $ownerRef) { $q->owned($ownerRef) diff --git a/app/Services/Care/SpecialtyVisitStageService.php b/app/Services/Care/SpecialtyVisitStageService.php index 3492c2a..083b736 100644 --- a/app/Services/Care/SpecialtyVisitStageService.php +++ b/app/Services/Care/SpecialtyVisitStageService.php @@ -120,6 +120,30 @@ class SpecialtyVisitStageService : ($stages[1] ?? ($stages[0] ?? null)); } + if ($moduleKey === 'radiology') { + $stages = $this->allowedStages($moduleKey); + + return in_array(\App\Services\Care\Radiology\RadiologyWorkflowService::STAGE_PROTOCOL, $stages, true) + ? \App\Services\Care\Radiology\RadiologyWorkflowService::STAGE_PROTOCOL + : ($stages[1] ?? ($stages[0] ?? null)); + } + + if ($moduleKey === 'cardiology') { + $stages = $this->allowedStages($moduleKey); + + return in_array(\App\Services\Care\Cardiology\CardiologyWorkflowService::STAGE_HISTORY, $stages, true) + ? \App\Services\Care\Cardiology\CardiologyWorkflowService::STAGE_HISTORY + : ($stages[1] ?? ($stages[0] ?? null)); + } + + if ($moduleKey === 'psychiatry') { + $stages = $this->allowedStages($moduleKey); + + return in_array(\App\Services\Care\Psychiatry\PsychiatryWorkflowService::STAGE_HISTORY, $stages, true) + ? \App\Services\Care\Psychiatry\PsychiatryWorkflowService::STAGE_HISTORY + : ($stages[1] ?? ($stages[0] ?? null)); + } + $stages = $this->allowedStages($moduleKey); foreach (['chair', 'in_care', 'exam', 'assessment', 'treatment'] as $preferred) { if (in_array($preferred, $stages, true)) { diff --git a/config/care_specialty_clinical.php b/config/care_specialty_clinical.php index 1192fde..ab14e60 100644 --- a/config/care_specialty_clinical.php +++ b/config/care_specialty_clinical.php @@ -47,16 +47,23 @@ return [ 'postnatal' => 'postnatal_note', ], 'radiology' => [ - 'request' => 'imaging_request', - 'report' => 'imaging_report', + 'protocol' => 'imaging_request', + 'acquisition' => 'imaging_acquisition', + 'findings' => 'imaging_report', + 'verification' => 'imaging_verification', ], 'cardiology' => [ + 'history' => 'cardio_history', 'exam' => 'cardiac_exam', - 'clinical_notes' => 'clinical_note', + 'investigations' => 'cardio_investigation', + 'plan' => 'cardio_plan', + 'procedures' => 'cardio_procedure', ], 'psychiatry' => [ - 'assessment' => 'mental_status', - 'clinical_notes' => 'clinical_note', + 'history' => 'mental_status', + 'risk' => 'risk_assessment', + 'plan' => 'psych_plan', + 'followup' => 'psych_followup', ], 'pediatrics' => [ 'exam' => 'pediatric_exam', @@ -363,48 +370,113 @@ return [ ['name' => 'clinical_info', 'label' => 'Clinical information', 'type' => 'textarea', 'required' => true, 'rows' => 3], ['name' => 'urgency', 'label' => 'Urgency', 'type' => 'select', 'options' => ['Routine', 'Urgent', 'STAT']], ['name' => 'contrast', 'label' => 'Contrast', 'type' => 'select', 'options' => ['None', 'IV', 'Oral', 'Both']], + ['name' => 'protocol', 'label' => 'Protocol / sequences', 'type' => 'textarea', 'rows' => 2], + ['name' => 'referrer', 'label' => 'Referring clinician', 'type' => 'text'], + ], + 'imaging_acquisition' => [ + ['name' => 'technologist', 'label' => 'Technologist', 'type' => 'text'], + ['name' => 'equipment', 'label' => 'Equipment / room', 'type' => 'text'], + ['name' => 'acquisition_status', 'label' => 'Acquisition status', 'type' => 'select', 'required' => true, 'options' => ['Scheduled', 'In progress', 'Acquired', 'Incomplete', 'Cancelled']], + ['name' => 'contrast_given', 'label' => 'Contrast administered', 'type' => 'boolean'], + ['name' => 'dose_notes', 'label' => 'Dose / exposure notes', 'type' => 'textarea', 'rows' => 2], + ['name' => 'quality', 'label' => 'Image quality', 'type' => 'select', 'options' => ['Diagnostic', 'Limited', 'Non-diagnostic', 'Repeat needed']], + ['name' => 'notes', 'label' => 'Acquisition notes', 'type' => 'textarea', 'rows' => 2], ], 'imaging_report' => [ ['name' => 'technique', 'label' => 'Technique', 'type' => 'textarea', 'rows' => 2], + ['name' => 'comparison', 'label' => 'Comparison', 'type' => 'text'], ['name' => 'findings', 'label' => 'Findings', 'type' => 'textarea', 'required' => true, 'rows' => 4], ['name' => 'impression', 'label' => 'Impression', 'type' => 'textarea', 'required' => true, 'rows' => 3], ['name' => 'recommendations', 'label' => 'Recommendations', 'type' => 'textarea', 'rows' => 2], + ['name' => 'critical_result', 'label' => 'Critical / unexpected finding', 'type' => 'boolean'], + ], + 'imaging_verification' => [ + ['name' => 'verification_status', 'label' => 'Verification status', 'type' => 'select', 'required' => true, 'options' => ['Draft', 'Preliminary', 'Verified / final', 'Amended']], + ['name' => 'verified_by', 'label' => 'Verified by', 'type' => 'text', 'required' => true], + ['name' => 'critical_communicated', 'label' => 'Critical result communicated', 'type' => 'boolean'], + ['name' => 'communication_notes', 'label' => 'Communication notes', 'type' => 'textarea', 'rows' => 2], + ['name' => 'notes', 'label' => 'Verification notes', 'type' => 'textarea', 'rows' => 2], ], ], 'cardiology' => [ + 'cardio_history' => [ + ['name' => 'history', 'label' => 'History of present illness', 'type' => 'textarea', 'required' => true, 'rows' => 4], + ['name' => 'past_cardiac', 'label' => 'Past cardiac history', 'type' => 'textarea', 'rows' => 2], + ['name' => 'medications', 'label' => 'Medications', 'type' => 'textarea', 'rows' => 2], + ['name' => 'allergies', 'label' => 'Allergies', 'type' => 'text'], + ['name' => 'risk_factors', 'label' => 'Cardiac risk factors', 'type' => 'textarea', 'rows' => 2], + ['name' => 'family_history', 'label' => 'Family history', 'type' => 'textarea', 'rows' => 2], + ], 'cardiac_exam' => [ ['name' => 'chief_complaint', 'label' => 'Chief complaint', 'type' => 'text', 'required' => true], ['name' => 'nyha_class', 'label' => 'NYHA class', 'type' => 'select', 'options' => ['I', 'II', 'III', 'IV', 'N/A']], ['name' => 'bp', 'label' => 'Blood pressure', 'type' => 'text'], ['name' => 'hr', 'label' => 'Heart rate', 'type' => 'number'], ['name' => 'heart_sounds', 'label' => 'Heart sounds / murmurs', 'type' => 'textarea', 'rows' => 2], - ['name' => 'ecg_findings', 'label' => 'ECG findings', 'type' => 'textarea', 'rows' => 2], - ['name' => 'echo_summary', 'label' => 'Echo summary', 'type' => 'textarea', 'rows' => 2], - ['name' => 'risk_factors', 'label' => 'Cardiac risk factors', 'type' => 'textarea', 'rows' => 2], + ['name' => 'ecg_findings', 'label' => 'ECG findings', 'type' => 'textarea', 'required' => true, 'rows' => 2], + ['name' => 'exam_findings', 'label' => 'Exam findings', 'type' => 'textarea', 'rows' => 2], ], - 'clinical_note' => [ - ['name' => 'history', 'label' => 'History of present illness', 'type' => 'textarea', 'required' => true, 'rows' => 4], - ['name' => 'examination', 'label' => 'Examination', 'type' => 'textarea', 'rows' => 3], - ['name' => 'working_diagnosis', 'label' => 'Working diagnosis', 'type' => 'text'], - ['name' => 'plan', 'label' => 'Plan', 'type' => 'textarea', 'rows' => 3], + 'cardio_investigation' => [ + ['name' => 'modality', 'label' => 'Investigation', 'type' => 'select', 'required' => true, 'options' => ['Echo', 'Stress test', 'Holter', 'Troponin / labs', 'CT angio', 'Other']], + ['name' => 'indication', 'label' => 'Indication', 'type' => 'textarea', 'rows' => 2], + ['name' => 'findings', 'label' => 'Findings', 'type' => 'textarea', 'required' => true, 'rows' => 3], + ['name' => 'impression', 'label' => 'Impression', 'type' => 'textarea', 'rows' => 2], + ['name' => 'recommendations', 'label' => 'Recommendations', 'type' => 'textarea', 'rows' => 2], + ], + 'cardio_plan' => [ + ['name' => 'diagnosis', 'label' => 'Diagnosis', 'type' => 'text', 'required' => true], + ['name' => 'plan', 'label' => 'Plan', 'type' => 'textarea', 'required' => true, 'rows' => 3], + ['name' => 'medications', 'label' => 'Medications', 'type' => 'textarea', 'rows' => 2], + ['name' => 'procedure_planned', 'label' => 'Procedure / intervention planned', 'type' => 'boolean'], + ['name' => 'follow_up', 'label' => 'Follow-up', 'type' => 'text'], + ['name' => 'advice', 'label' => 'Patient advice', 'type' => 'textarea', 'rows' => 2], + ], + 'cardio_procedure' => [ + ['name' => 'procedure', 'label' => 'Procedure / intervention', 'type' => 'text', 'required' => true], + ['name' => 'indication', 'label' => 'Indication', 'type' => 'textarea', 'rows' => 2], + ['name' => 'outcome', 'label' => 'Outcome', 'type' => 'select', 'required' => true, 'options' => ['Completed uneventfully', 'Completed with complications', 'Deferred', 'Aborted']], + ['name' => 'complications', 'label' => 'Complications', 'type' => 'textarea', 'rows' => 2], + ['name' => 'post_procedure_plan', 'label' => 'Post-procedure plan', 'type' => 'textarea', 'rows' => 2], + ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea', 'rows' => 2], ], ], 'psychiatry' => [ 'mental_status' => [ ['name' => 'chief_complaint', 'label' => 'Presenting complaint', 'type' => 'text', 'required' => true], + ['name' => 'history', 'label' => 'History of present illness', 'type' => 'textarea', 'rows' => 3], ['name' => 'appearance', 'label' => 'Appearance / behaviour', 'type' => 'textarea', 'rows' => 2], ['name' => 'mood_affect', 'label' => 'Mood / affect', 'type' => 'text'], ['name' => 'thought', 'label' => 'Thought content / process', 'type' => 'textarea', 'rows' => 2], ['name' => 'perception', 'label' => 'Perception', 'type' => 'text'], + ['name' => 'cognition', 'label' => 'Cognition / orientation', 'type' => 'text'], ['name' => 'insight', 'label' => 'Insight / judgment', 'type' => 'text'], - ['name' => 'risk', 'label' => 'Risk assessment', 'type' => 'textarea', 'required' => true, 'rows' => 2], ['name' => 'mse_summary', 'label' => 'MSE summary', 'type' => 'textarea', 'required' => true, 'rows' => 3], ], - 'clinical_note' => [ - ['name' => 'history', 'label' => 'History of present illness', 'type' => 'textarea', 'required' => true, 'rows' => 4], - ['name' => 'examination', 'label' => 'Examination', 'type' => 'textarea', 'rows' => 3], - ['name' => 'working_diagnosis', 'label' => 'Working diagnosis', 'type' => 'text'], - ['name' => 'plan', 'label' => 'Plan', 'type' => 'textarea', 'rows' => 3], + 'risk_assessment' => [ + ['name' => 'overall_risk', 'label' => 'Overall risk level', 'type' => 'select', 'required' => true, 'options' => ['Low', 'Moderate', 'High', 'Imminent']], + ['name' => 'self_harm', 'label' => 'Self-harm / suicide risk', 'type' => 'select', 'options' => ['None identified', 'Ideation', 'Plan', 'Intent / recent attempt']], + ['name' => 'harm_others', 'label' => 'Risk to others', 'type' => 'select', 'options' => ['None identified', 'Ideation', 'Plan', 'Intent']], + ['name' => 'safeguarding', 'label' => 'Safeguarding concerns', 'type' => 'textarea', 'rows' => 2], + ['name' => 'protective_factors', 'label' => 'Protective factors', 'type' => 'textarea', 'rows' => 2], + ['name' => 'risk_summary', 'label' => 'Risk summary', 'type' => 'textarea', 'required' => true, 'rows' => 3], + ['name' => 'immediate_actions', 'label' => 'Immediate actions', 'type' => 'textarea', 'rows' => 2], + ], + 'psych_plan' => [ + ['name' => 'formulation', 'label' => 'Formulation', 'type' => 'textarea', 'required' => true, 'rows' => 3], + ['name' => 'diagnosis', 'label' => 'Working diagnosis', 'type' => 'text'], + ['name' => 'plan', 'label' => 'Management plan', 'type' => 'textarea', 'required' => true, 'rows' => 3], + ['name' => 'medications', 'label' => 'Medications', 'type' => 'textarea', 'rows' => 2], + ['name' => 'therapy', 'label' => 'Therapy / psychosocial', 'type' => 'textarea', 'rows' => 2], + ['name' => 'follow_up', 'label' => 'Follow-up', 'type' => 'text'], + ['name' => 'crisis_plan', 'label' => 'Crisis / safety plan', 'type' => 'textarea', 'rows' => 2], + ], + 'psych_followup' => [ + ['name' => 'review_type', 'label' => 'Review type', 'type' => 'select', 'required' => true, 'options' => ['Clinic follow-up', 'Medication review', 'Crisis review', 'Discharge review']], + ['name' => 'progress', 'label' => 'Progress since last contact', 'type' => 'textarea', 'required' => true, 'rows' => 3], + ['name' => 'current_risk', 'label' => 'Current risk', 'type' => 'select', 'options' => ['Low', 'Moderate', 'High', 'Imminent']], + ['name' => 'outcome', 'label' => 'Outcome', 'type' => 'select', 'required' => true, 'options' => ['In progress', 'Follow-up arranged', 'Discharged', 'Referred', 'Admitted']], + ['name' => 'next_steps', 'label' => 'Next steps', 'type' => 'textarea', 'rows' => 2], + ['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea', 'rows' => 2], ], ], 'pediatrics' => [ diff --git a/config/care_specialty_modules.php b/config/care_specialty_modules.php index 2168b14..2b4f9c4 100644 --- a/config/care_specialty_modules.php +++ b/config/care_specialty_modules.php @@ -118,6 +118,7 @@ return [ 'queue_keywords' => ['radio', 'imaging', 'x-ray', 'xray', 'ultrasound', 'ct', 'mri'], 'access' => 'general', 'roles' => ['doctor', 'nurse', 'lab_technician', 'receptionist'], + 'specialist_keywords' => ['radio', 'radiolog', 'imaging'], ], 'cardiology' => [ 'label' => 'Cardiology', @@ -134,7 +135,7 @@ return [ 'support_roles' => ['nurse'], 'view_roles' => ['doctor', 'nurse', 'lab_technician'], 'refer_roles' => ['doctor', 'nurse'], - 'specialist_keywords' => ['cardio', 'cardiology', 'heart'], + 'specialist_keywords' => ['cardio', 'cardiology', 'heart', 'ecg'], ], 'psychiatry' => [ 'label' => 'Psychiatry', @@ -151,7 +152,7 @@ return [ 'support_roles' => ['nurse'], 'view_roles' => ['doctor', 'nurse'], 'refer_roles' => ['doctor', 'nurse'], - 'specialist_keywords' => ['psych', 'psychiatry', 'mental'], + 'specialist_keywords' => ['psych', 'psychiatry', 'mental', 'behaviour'], ], 'pediatrics' => [ 'label' => 'Pediatrics', diff --git a/config/care_specialty_shell.php b/config/care_specialty_shell.php index dcdc3b0..c66a632 100644 --- a/config/care_specialty_shell.php +++ b/config/care_specialty_shell.php @@ -292,61 +292,113 @@ return [ ], 'radiology' => [ 'stages' => [ - ['code' => 'request', 'label' => 'Request received', 'queue_point' => 'waiting'], - ['code' => 'imaging', 'label' => 'Imaging', 'queue_point' => 'chair'], + ['code' => 'check_in', 'label' => 'Check-in', 'queue_point' => 'waiting'], + ['code' => 'protocol', 'label' => 'Request / protocol', 'queue_point' => 'waiting'], + ['code' => 'acquisition', 'label' => 'Acquisition', 'queue_point' => 'chair'], ['code' => 'reporting', 'label' => 'Reporting', 'queue_point' => 'chair'], + ['code' => 'verified', 'label' => 'Verified', 'queue_point' => null], ['code' => 'completed', 'label' => 'Completed', 'queue_point' => null], ], 'services' => [ ['code' => 'rad.xray', 'label' => 'X-ray', 'amount_minor' => 8000, 'type' => 'imaging'], ['code' => 'rad.ultrasound', 'label' => 'Ultrasound', 'amount_minor' => 15000, 'type' => 'imaging'], ['code' => 'rad.ct', 'label' => 'CT scan', 'amount_minor' => 45000, 'type' => 'imaging'], + ['code' => 'rad.mri', 'label' => 'MRI', 'amount_minor' => 65000, 'type' => 'imaging'], + ['code' => 'rad.fluoro', 'label' => 'Fluoroscopy', 'amount_minor' => 20000, 'type' => 'imaging'], + ['code' => 'rad.contrast', 'label' => 'Contrast administration', 'amount_minor' => 5000, 'type' => 'procedure'], + ['code' => 'rad.report', 'label' => 'Radiology report', 'amount_minor' => 4000, 'type' => 'consultation'], ], 'workspace_tabs' => [ 'overview' => 'Overview', - 'request' => 'Imaging request', - 'report' => 'Report', + 'protocol' => 'Request / protocol', + 'acquisition' => 'Acquisition', + 'findings' => 'Findings / report', + 'verification' => 'Verification', + 'orders' => 'Orders', 'billing' => 'Billing', 'documents' => 'Documents', ], + 'stage_tabs' => [ + 'check_in' => 'overview', + 'protocol' => 'protocol', + 'acquisition' => 'acquisition', + 'reporting' => 'findings', + 'verified' => 'verification', + 'completed' => 'overview', + ], ], 'cardiology' => [ 'stages' => [ - ['code' => 'waiting', 'label' => 'Waiting', 'queue_point' => 'waiting'], - ['code' => 'exam', 'label' => 'Cardiac exam', 'queue_point' => 'chair'], + ['code' => 'check_in', 'label' => 'Check-in', 'queue_point' => 'waiting'], + ['code' => 'history', 'label' => 'History', 'queue_point' => 'waiting'], + ['code' => 'exam', 'label' => 'Exam / ECG', 'queue_point' => 'chair'], + ['code' => 'investigation', 'label' => 'Investigations', 'queue_point' => 'chair'], + ['code' => 'plan', 'label' => 'Diagnosis & plan', 'queue_point' => 'chair'], + ['code' => 'procedure', 'label' => 'Procedure / intervention', 'queue_point' => 'chair'], ['code' => 'completed', 'label' => 'Completed', 'queue_point' => null], ], 'services' => [ ['code' => 'car.consult', 'label' => 'Cardiology consultation', 'amount_minor' => 8000, 'type' => 'consultation'], ['code' => 'car.ecg', 'label' => 'ECG', 'amount_minor' => 5000, 'type' => 'procedure'], ['code' => 'car.echo', 'label' => 'Echocardiogram', 'amount_minor' => 25000, 'type' => 'imaging'], + ['code' => 'car.stress', 'label' => 'Stress test', 'amount_minor' => 18000, 'type' => 'procedure'], + ['code' => 'car.holter', 'label' => 'Holter monitoring', 'amount_minor' => 15000, 'type' => 'procedure'], + ['code' => 'car.procedure', 'label' => 'Cardiac procedure', 'amount_minor' => 35000, 'type' => 'procedure'], ], 'workspace_tabs' => [ 'overview' => 'Overview', - 'exam' => 'Cardiac exam', - 'clinical_notes' => 'Clinical notes', + 'history' => 'History', + 'exam' => 'Exam / ECG', + 'investigations' => 'Investigations', + 'plan' => 'Diagnosis & plan', + 'procedures' => 'Procedures', 'orders' => 'Orders', 'billing' => 'Billing', 'documents' => 'Documents', ], + 'stage_tabs' => [ + 'check_in' => 'overview', + 'history' => 'history', + 'exam' => 'exam', + 'investigation' => 'investigations', + 'plan' => 'plan', + 'procedure' => 'procedures', + 'completed' => 'overview', + ], ], 'psychiatry' => [ 'stages' => [ - ['code' => 'waiting', 'label' => 'Waiting', 'queue_point' => 'waiting'], - ['code' => 'assessment', 'label' => 'Mental health assessment', 'queue_point' => 'chair'], + ['code' => 'check_in', 'label' => 'Check-in', 'queue_point' => 'waiting'], + ['code' => 'history', 'label' => 'History / MSE', 'queue_point' => 'waiting'], + ['code' => 'risk', 'label' => 'Risk assessment', 'queue_point' => 'chair'], + ['code' => 'formulation', 'label' => 'Formulation / plan', 'queue_point' => 'chair'], + ['code' => 'review', 'label' => 'Follow-up review', 'queue_point' => 'chair'], ['code' => 'completed', 'label' => 'Completed', 'queue_point' => null], ], 'services' => [ ['code' => 'psy.consult', 'label' => 'Psychiatry consultation', 'amount_minor' => 8000, 'type' => 'consultation'], + ['code' => 'psy.mse', 'label' => 'Mental state examination', 'amount_minor' => 5000, 'type' => 'consultation'], + ['code' => 'psy.risk', 'label' => 'Risk assessment', 'amount_minor' => 4500, 'type' => 'consultation'], + ['code' => 'psy.followup', 'label' => 'Follow-up review', 'amount_minor' => 6000, 'type' => 'consultation'], ], 'workspace_tabs' => [ 'overview' => 'Overview', - 'assessment' => 'Assessment', - 'clinical_notes' => 'Clinical notes', + 'history' => 'History / MSE', + 'risk' => 'Risk', + 'plan' => 'Formulation / plan', + 'followup' => 'Follow-up', 'orders' => 'Orders', 'billing' => 'Billing', 'documents' => 'Documents', ], + 'stage_tabs' => [ + 'check_in' => 'overview', + 'history' => 'history', + 'risk' => 'risk', + 'formulation' => 'plan', + 'review' => 'followup', + 'completed' => 'overview', + ], ], 'pediatrics' => [ 'stages' => [ diff --git a/resources/views/care/specialty/cardiology/print.blade.php b/resources/views/care/specialty/cardiology/print.blade.php new file mode 100644 index 0000000..51f7302 --- /dev/null +++ b/resources/views/care/specialty/cardiology/print.blade.php @@ -0,0 +1,89 @@ + + + + + Cardiology summary · {{ $patient->fullName() }} + + + +

Back

+ +

{{ $patient->fullName() }}

+

+ {{ $patient->patient_number }} + · Visit #{{ $visit->id }} + · Stage {{ $visit->specialty_stage ?? '—' }} + · {{ $visit->checked_in_at?->format('d M Y H:i') ?? '—' }} +

+ +

History

+ @if ($history) +
+
HPI
{{ $history->payload['history'] ?? '—' }}
+
Past cardiac
{{ $history->payload['past_cardiac'] ?? '—' }}
+
Risk factors
{{ $history->payload['risk_factors'] ?? '—' }}
+
Medications
{{ $history->payload['medications'] ?? '—' }}
+
+ @else +

No history recorded.

+ @endif + +

Exam / ECG

+ @if ($exam) +
+
Complaint
{{ $exam->payload['chief_complaint'] ?? '—' }}
+
NYHA
{{ $exam->payload['nyha_class'] ?? '—' }}
+
BP / HR
{{ $exam->payload['bp'] ?? '—' }} · {{ $exam->payload['hr'] ?? '—' }}
+
ECG
{{ $exam->payload['ecg_findings'] ?? '—' }}
+
Exam
{{ $exam->payload['exam_findings'] ?? '—' }}
+
+ @else +

No exam recorded.

+ @endif + +

Investigations

+ @if ($investigation) +
+
Investigation
{{ $investigation->payload['modality'] ?? '—' }}
+
Findings
{{ $investigation->payload['findings'] ?? '—' }}
+
Impression
{{ $investigation->payload['impression'] ?? '—' }}
+
+ @else +

No investigation recorded.

+ @endif + +

Diagnosis & plan

+ @if ($plan) +
+
Diagnosis
{{ $plan->payload['diagnosis'] ?? '—' }}
+
Plan
{{ $plan->payload['plan'] ?? '—' }}
+
Medications
{{ $plan->payload['medications'] ?? '—' }}
+
Follow-up
{{ $plan->payload['follow_up'] ?? '—' }}
+
+ @else +

No plan recorded.

+ @endif + +

Procedure

+ @if ($procedure) +
+
Procedure
{{ $procedure->payload['procedure'] ?? '—' }}
+
Outcome
{{ $procedure->payload['outcome'] ?? '—' }}
+
Plan
{{ $procedure->payload['post_procedure_plan'] ?? '—' }}
+
+ @else +

No procedure recorded.

+ @endif + + + + diff --git a/resources/views/care/specialty/cardiology/reports.blade.php b/resources/views/care/specialty/cardiology/reports.blade.php new file mode 100644 index 0000000..ad098e9 --- /dev/null +++ b/resources/views/care/specialty/cardiology/reports.blade.php @@ -0,0 +1,88 @@ + +
+
+
+

Cardiology reports

+

Arrivals, NYHA III–IV open cases, diagnoses, procedures, length of stay, and cardiology revenue.

+
+ ← Specialty home +
+ +
+
+ + +
+
+ + +
+ +
+ +
+
+

Arrivals today

+

{{ number_format($report['arrivals_today']) }}

+
+
+

Completed today

+

{{ number_format($report['completed_today']) }}

+
+
+

NYHA III–IV open

+

{{ number_format($report['high_risk_open']) }}

+
+
+

Avg LOS (range)

+

+ {{ $report['avg_los_minutes'] !== null ? $report['avg_los_minutes'].' min' : '—' }} +

+
+
+ +
+
+

Diagnoses

+
    + @forelse ($report['diagnosis_breakdown'] as $row) +
  • + {{ $row['diagnosis'] }} + {{ $row['total'] }} +
  • + @empty +
  • No care plans in range.
  • + @endforelse +
+
+ +
+

Procedures

+
    + @forelse ($report['procedure_breakdown'] as $row) +
  • + {{ $row['procedure'] }} + {{ $row['total'] }} +
  • + @empty +
  • No procedures in range.
  • + @endforelse +
+
+
+ +
+

Cardiology service revenue

+
    + @forelse ($report['revenue_by_service'] as $row) +
  • + {{ $row->description }} ×{{ $row->total }} + {{ $currency }} {{ number_format($row->amount_minor / 100, 2) }} +
  • + @empty +
  • No billed cardiology services in range.
  • + @endforelse +
+
+
+
diff --git a/resources/views/care/specialty/cardiology/workspace-overview.blade.php b/resources/views/care/specialty/cardiology/workspace-overview.blade.php new file mode 100644 index 0000000..ef040f0 --- /dev/null +++ b/resources/views/care/specialty/cardiology/workspace-overview.blade.php @@ -0,0 +1,71 @@ +@php + $hist = $cardiologyHistory?->payload ?? []; + $exam = $cardiologyExam?->payload ?? []; + $plan = $cardiologyPlan?->payload ?? []; +@endphp + +
+
+
+

Cardiology overview

+

History, exam / ECG, investigations, and care plan for this episode.

+
+ +
+ +
+
+

Complaint

+

{{ $exam['chief_complaint'] ?? '—' }}

+

NYHA {{ $exam['nyha_class'] ?? '—' }}

+
+
+

Vitals / ECG

+

BP {{ $exam['bp'] ?? '—' }} · HR {{ $exam['hr'] ?? '—' }}

+

{{ \Illuminate\Support\Str::limit($exam['ecg_findings'] ?? '—', 50) }}

+
+
+

Diagnosis / plan

+

{{ $plan['diagnosis'] ?? 'Not set' }}

+

{{ $plan['follow_up'] ?? '—' }}

+
+
+ +
+
+
History
+
{{ $hist['history'] ?? '—' }}
+
+
+
Plan
+
{{ $plan['plan'] ?? '—' }}
+
+
+
Queue
+
+ {{ $workspaceVisit->appointment?->queue_ticket_number ?? '—' }} + @if ($workspaceVisit->appointment?->queue_ticket_status) + ({{ $workspaceVisit->appointment->queue_ticket_status }}) + @endif +
+
+
+
Checked in
+
{{ $workspaceVisit->checked_in_at?->format('d M Y H:i') ?? '—' }}
+
+
+ + @if (! empty($clinicalAlerts)) +
+

Alerts

+ @foreach ($clinicalAlerts as $alert) +

+ {{ $alert['message'] ?? '' }} +

+ @endforeach +
+ @endif +
diff --git a/resources/views/care/specialty/cardiology/workspace-procedures.blade.php b/resources/views/care/specialty/cardiology/workspace-procedures.blade.php new file mode 100644 index 0000000..993f285 --- /dev/null +++ b/resources/views/care/specialty/cardiology/workspace-procedures.blade.php @@ -0,0 +1,70 @@ +@php + $record = $cardiologyProcedure; + $payload = $record?->payload ?? []; + $canManage = $canManageClinical ?? $canConsult ?? false; +@endphp + +
+
+
+

Procedure / intervention

+

Completed interventions can close the cardiology visit.

+
+ Print summary +
+ + @if ($canManage) +
+ @csrf + +
+ @foreach ($clinicalFields ?? [] as $field) + @php + $name = $field['name']; + $value = old('payload.'.$name, $payload[$name] ?? ''); + $type = $field['type'] ?? 'text'; + @endphp +
in_array($type, ['textarea'], true)])> + + @if ($type === 'textarea') + + @elseif ($type === 'select') + + @elseif ($type === 'boolean') + + @else + + @endif +
+ @endforeach +
+ +
+ @elseif ($record) +
+ @foreach ($clinicalFields ?? [] as $field) +
+
{{ $field['label'] }}
+
{{ $payload[$field['name']] ?? '—' }}
+
+ @endforeach +
+ @else +

No procedure recorded yet.

+ @endif +
diff --git a/resources/views/care/specialty/partials/actions-menu.blade.php b/resources/views/care/specialty/partials/actions-menu.blade.php index 6e5a3d8..00b41b9 100644 --- a/resources/views/care/specialty/partials/actions-menu.blade.php +++ b/resources/views/care/specialty/partials/actions-menu.blade.php @@ -47,6 +47,9 @@ 'ophthalmology' => 'check_in', 'physiotherapy' => 'check_in', 'maternity' => 'check_in', + 'radiology' => 'check_in', + 'cardiology' => 'check_in', + 'psychiatry' => 'check_in', default => collect($stages ?? [])->pluck('code')->first(), }; $defaultStartLabel = match ($moduleKey) { @@ -56,6 +59,9 @@ 'ophthalmology' => 'Start check-in', 'physiotherapy' => 'Start check-in', 'maternity' => 'Start check-in', + 'radiology' => 'Start check-in', + 'cardiology' => 'Start check-in', + 'psychiatry' => 'Start check-in', default => 'Start', }; $currentStage = $workspaceVisit?->specialty_stage; diff --git a/resources/views/care/specialty/psychiatry/print.blade.php b/resources/views/care/specialty/psychiatry/print.blade.php new file mode 100644 index 0000000..b91472d --- /dev/null +++ b/resources/views/care/specialty/psychiatry/print.blade.php @@ -0,0 +1,81 @@ + + + + + Psychiatry summary · {{ $patient->fullName() }} + + + +

Back

+ +

{{ $patient->fullName() }}

+

+ {{ $patient->patient_number }} + · Visit #{{ $visit->id }} + · Stage {{ $visit->specialty_stage ?? '—' }} + · {{ $visit->checked_in_at?->format('d M Y H:i') ?? '—' }} +

+ +

History / MSE

+ @if ($history) +
+
Presenting
{{ $history->payload['chief_complaint'] ?? '—' }}
+
History
{{ $history->payload['history'] ?? '—' }}
+
Mood / affect
{{ $history->payload['mood_affect'] ?? '—' }}
+
Thought
{{ $history->payload['thought'] ?? '—' }}
+
MSE summary
{{ $history->payload['mse_summary'] ?? '—' }}
+
+ @else +

No MSE recorded.

+ @endif + +

Risk assessment

+ @if ($risk) +
+
Overall risk
{{ $risk->payload['overall_risk'] ?? '—' }}
+
Self-harm
{{ $risk->payload['self_harm'] ?? '—' }}
+
Risk to others
{{ $risk->payload['harm_others'] ?? '—' }}
+
Summary
{{ $risk->payload['risk_summary'] ?? '—' }}
+
Actions
{{ $risk->payload['immediate_actions'] ?? '—' }}
+
+ @else +

No risk assessment recorded.

+ @endif + +

Formulation / plan

+ @if ($plan) +
+
Formulation
{{ $plan->payload['formulation'] ?? '—' }}
+
Diagnosis
{{ $plan->payload['diagnosis'] ?? '—' }}
+
Plan
{{ $plan->payload['plan'] ?? '—' }}
+
Medications
{{ $plan->payload['medications'] ?? '—' }}
+
Follow-up
{{ $plan->payload['follow_up'] ?? '—' }}
+
+ @else +

No formulation recorded.

+ @endif + +

Follow-up

+ @if ($followup) +
+
Review type
{{ $followup->payload['review_type'] ?? '—' }}
+
Progress
{{ $followup->payload['progress'] ?? '—' }}
+
Outcome
{{ $followup->payload['outcome'] ?? '—' }}
+
Next steps
{{ $followup->payload['next_steps'] ?? '—' }}
+
+ @else +

No follow-up recorded.

+ @endif + + + + diff --git a/resources/views/care/specialty/psychiatry/reports.blade.php b/resources/views/care/specialty/psychiatry/reports.blade.php new file mode 100644 index 0000000..c788499 --- /dev/null +++ b/resources/views/care/specialty/psychiatry/reports.blade.php @@ -0,0 +1,88 @@ + +
+
+
+

Psychiatry reports

+

Arrivals, high-risk open cases, risk levels, review outcomes, length of stay, and psychiatry revenue.

+
+ ← Specialty home +
+ +
+
+ + +
+
+ + +
+ +
+ +
+
+

Arrivals today

+

{{ number_format($report['arrivals_today']) }}

+
+
+

Completed today

+

{{ number_format($report['completed_today']) }}

+
+
+

High risk open

+

{{ number_format($report['high_risk_open']) }}

+
+
+

Avg LOS (range)

+

+ {{ $report['avg_los_minutes'] !== null ? $report['avg_los_minutes'].' min' : '—' }} +

+
+
+ +
+
+

Risk levels

+
    + @forelse ($report['risk_breakdown'] as $row) +
  • + {{ $row['risk'] }} + {{ $row['total'] }} +
  • + @empty +
  • No risk assessments in range.
  • + @endforelse +
+
+ +
+

Review outcomes

+
    + @forelse ($report['review_outcomes'] as $row) +
  • + {{ $row['outcome'] }} + {{ $row['total'] }} +
  • + @empty +
  • No follow-up reviews in range.
  • + @endforelse +
+
+
+ +
+

Psychiatry service revenue

+
    + @forelse ($report['revenue_by_service'] as $row) +
  • + {{ $row->description }} ×{{ $row->total }} + {{ $currency }} {{ number_format($row->amount_minor / 100, 2) }} +
  • + @empty +
  • No billed psychiatry services in range.
  • + @endforelse +
+
+
+
diff --git a/resources/views/care/specialty/psychiatry/workspace-followup.blade.php b/resources/views/care/specialty/psychiatry/workspace-followup.blade.php new file mode 100644 index 0000000..e447a1b --- /dev/null +++ b/resources/views/care/specialty/psychiatry/workspace-followup.blade.php @@ -0,0 +1,70 @@ +@php + $record = $psychiatryFollowup; + $payload = $record?->payload ?? []; + $canManage = $canManageClinical ?? $canConsult ?? false; +@endphp + +
+
+
+

Follow-up review

+

Discharge and follow-up outcomes can close the psychiatry visit.

+
+ Print summary +
+ + @if ($canManage) +
+ @csrf + +
+ @foreach ($clinicalFields ?? [] as $field) + @php + $name = $field['name']; + $value = old('payload.'.$name, $payload[$name] ?? ''); + $type = $field['type'] ?? 'text'; + @endphp +
in_array($type, ['textarea'], true)])> + + @if ($type === 'textarea') + + @elseif ($type === 'select') + + @elseif ($type === 'boolean') + + @else + + @endif +
+ @endforeach +
+ +
+ @elseif ($record) +
+ @foreach ($clinicalFields ?? [] as $field) +
+
{{ $field['label'] }}
+
{{ $payload[$field['name']] ?? '—' }}
+
+ @endforeach +
+ @else +

No follow-up review recorded yet.

+ @endif +
diff --git a/resources/views/care/specialty/psychiatry/workspace-overview.blade.php b/resources/views/care/specialty/psychiatry/workspace-overview.blade.php new file mode 100644 index 0000000..fa6dc7b --- /dev/null +++ b/resources/views/care/specialty/psychiatry/workspace-overview.blade.php @@ -0,0 +1,71 @@ +@php + $mse = $psychiatryHistory?->payload ?? []; + $risk = $psychiatryRisk?->payload ?? []; + $plan = $psychiatryPlan?->payload ?? []; +@endphp + +
+
+
+

Psychiatry overview

+

MSE, risk, formulation, and follow-up for this episode. Clinical notes use the same access controls as other specialty records.

+
+ +
+ +
+
+

Presenting

+

{{ $mse['chief_complaint'] ?? '—' }}

+

{{ $mse['mood_affect'] ?? '—' }}

+
+
+

Risk

+

{{ $risk['overall_risk'] ?? 'Not assessed' }}

+

{{ \Illuminate\Support\Str::limit($risk['risk_summary'] ?? '—', 50) }}

+
+
+

Plan

+

{{ $plan['diagnosis'] ?? 'Not set' }}

+

{{ $plan['follow_up'] ?? '—' }}

+
+
+ +
+
+
MSE summary
+
{{ $mse['mse_summary'] ?? '—' }}
+
+
+
Management plan
+
{{ $plan['plan'] ?? '—' }}
+
+
+
Queue
+
+ {{ $workspaceVisit->appointment?->queue_ticket_number ?? '—' }} + @if ($workspaceVisit->appointment?->queue_ticket_status) + ({{ $workspaceVisit->appointment->queue_ticket_status }}) + @endif +
+
+
+
Checked in
+
{{ $workspaceVisit->checked_in_at?->format('d M Y H:i') ?? '—' }}
+
+
+ + @if (! empty($clinicalAlerts)) +
+

Alerts

+ @foreach ($clinicalAlerts as $alert) +

+ {{ $alert['message'] ?? '' }} +

+ @endforeach +
+ @endif +
diff --git a/resources/views/care/specialty/radiology/print.blade.php b/resources/views/care/specialty/radiology/print.blade.php new file mode 100644 index 0000000..c79f39c --- /dev/null +++ b/resources/views/care/specialty/radiology/print.blade.php @@ -0,0 +1,79 @@ + + + + + Radiology summary · {{ $patient->fullName() }} + + + +

Back

+ +

{{ $patient->fullName() }}

+

+ {{ $patient->patient_number }} + · Visit #{{ $visit->id }} + · Stage {{ $visit->specialty_stage ?? '—' }} + · {{ $visit->checked_in_at?->format('d M Y H:i') ?? '—' }} +

+ +

Request / protocol

+ @if ($imagingRequest) +
+
Modality
{{ $imagingRequest->payload['modality'] ?? '—' }}
+
Body part
{{ $imagingRequest->payload['body_part'] ?? '—' }}
+
Urgency
{{ $imagingRequest->payload['urgency'] ?? '—' }}
+
Contrast
{{ $imagingRequest->payload['contrast'] ?? '—' }}
+
Clinical info
{{ $imagingRequest->payload['clinical_info'] ?? '—' }}
+
Protocol
{{ $imagingRequest->payload['protocol'] ?? '—' }}
+
+ @else +

No imaging request recorded.

+ @endif + +

Acquisition

+ @if ($acquisition) +
+
Status
{{ $acquisition->payload['acquisition_status'] ?? '—' }}
+
Quality
{{ $acquisition->payload['quality'] ?? '—' }}
+
Technologist
{{ $acquisition->payload['technologist'] ?? '—' }}
+
Notes
{{ $acquisition->payload['notes'] ?? '—' }}
+
+ @else +

No acquisition recorded.

+ @endif + +

Findings / report

+ @if ($imagingReport) +
+
Technique
{{ $imagingReport->payload['technique'] ?? '—' }}
+
Findings
{{ $imagingReport->payload['findings'] ?? '—' }}
+
Impression
{{ $imagingReport->payload['impression'] ?? '—' }}
+
Recommendations
{{ $imagingReport->payload['recommendations'] ?? '—' }}
+
+ @else +

No report recorded.

+ @endif + +

Verification

+ @if ($verification) +
+
Status
{{ $verification->payload['verification_status'] ?? '—' }}
+
Verified by
{{ $verification->payload['verified_by'] ?? '—' }}
+
Notes
{{ $verification->payload['notes'] ?? '—' }}
+
+ @else +

No verification recorded.

+ @endif + + + + diff --git a/resources/views/care/specialty/radiology/reports.blade.php b/resources/views/care/specialty/radiology/reports.blade.php new file mode 100644 index 0000000..1539611 --- /dev/null +++ b/resources/views/care/specialty/radiology/reports.blade.php @@ -0,0 +1,88 @@ + +
+
+
+

Radiology reports

+

Arrivals, urgent studies, modalities, verification outcomes, length of stay, and imaging revenue.

+
+ ← Specialty home +
+ +
+
+ + +
+
+ + +
+ +
+ +
+
+

Arrivals today

+

{{ number_format($report['arrivals_today']) }}

+
+
+

Completed today

+

{{ number_format($report['completed_today']) }}

+
+
+

Urgent open

+

{{ number_format($report['urgent_open']) }}

+
+
+

Avg LOS (range)

+

+ {{ $report['avg_los_minutes'] !== null ? $report['avg_los_minutes'].' min' : '—' }} +

+
+
+ +
+
+

Modalities

+
    + @forelse ($report['modality_breakdown'] as $row) +
  • + {{ $row['modality'] }} + {{ $row['total'] }} +
  • + @empty +
  • No imaging requests in range.
  • + @endforelse +
+
+ +
+

Verification outcomes

+
    + @forelse ($report['verification_outcomes'] as $row) +
  • + {{ $row['status'] }} + {{ $row['total'] }} +
  • + @empty +
  • No verifications in range.
  • + @endforelse +
+
+
+ +
+

Radiology service revenue

+
    + @forelse ($report['revenue_by_service'] as $row) +
  • + {{ $row->description }} ×{{ $row->total }} + {{ $currency }} {{ number_format($row->amount_minor / 100, 2) }} +
  • + @empty +
  • No billed radiology services in range.
  • + @endforelse +
+
+
+
diff --git a/resources/views/care/specialty/radiology/workspace-overview.blade.php b/resources/views/care/specialty/radiology/workspace-overview.blade.php new file mode 100644 index 0000000..7756941 --- /dev/null +++ b/resources/views/care/specialty/radiology/workspace-overview.blade.php @@ -0,0 +1,72 @@ +@php + $req = $radiologyRequest?->payload ?? []; + $acq = $radiologyAcquisition?->payload ?? []; + $rep = $radiologyReport?->payload ?? []; + $ver = $radiologyVerification?->payload ?? []; +@endphp + +
+
+
+

Radiology overview

+

Request, acquisition, report, and verification for this study.

+
+ +
+ +
+
+

Request

+

{{ $req['modality'] ?? '—' }} · {{ $req['body_part'] ?? '—' }}

+

{{ $req['urgency'] ?? 'Routine' }}

+
+
+

Acquisition

+

{{ $acq['acquisition_status'] ?? 'Not started' }}

+

{{ $acq['quality'] ?? '—' }}

+
+
+

Report / verify

+

{{ $ver['verification_status'] ?? 'Draft' }}

+

{{ \Illuminate\Support\Str::limit($rep['impression'] ?? '—', 60) }}

+
+
+ +
+
+
Clinical info
+
{{ $req['clinical_info'] ?? '—' }}
+
+
+
Impression
+
{{ $rep['impression'] ?? '—' }}
+
+
+
Queue
+
+ {{ $workspaceVisit->appointment?->queue_ticket_number ?? '—' }} + @if ($workspaceVisit->appointment?->queue_ticket_status) + ({{ $workspaceVisit->appointment->queue_ticket_status }}) + @endif +
+
+
+
Checked in
+
{{ $workspaceVisit->checked_in_at?->format('d M Y H:i') ?? '—' }}
+
+
+ + @if (! empty($clinicalAlerts)) +
+

Alerts

+ @foreach ($clinicalAlerts as $alert) +

+ {{ $alert['message'] ?? '' }} +

+ @endforeach +
+ @endif +
diff --git a/resources/views/care/specialty/radiology/workspace-verification.blade.php b/resources/views/care/specialty/radiology/workspace-verification.blade.php new file mode 100644 index 0000000..c594f80 --- /dev/null +++ b/resources/views/care/specialty/radiology/workspace-verification.blade.php @@ -0,0 +1,70 @@ +@php + $record = $radiologyVerification; + $payload = $record?->payload ?? []; + $canManage = $canManageClinical ?? $canConsult ?? false; +@endphp + +
+
+
+

Verification

+

Final / verified reports can close the imaging episode.

+
+ Print summary +
+ + @if ($canManage) +
+ @csrf + +
+ @foreach ($clinicalFields ?? [] as $field) + @php + $name = $field['name']; + $value = old('payload.'.$name, $payload[$name] ?? ''); + $type = $field['type'] ?? 'text'; + @endphp +
in_array($type, ['textarea'], true)])> + + @if ($type === 'textarea') + + @elseif ($type === 'select') + + @elseif ($type === 'boolean') + + @else + + @endif +
+ @endforeach +
+ +
+ @elseif ($record) +
+ @foreach ($clinicalFields ?? [] as $field) +
+
{{ $field['label'] }}
+
{{ $payload[$field['name']] ?? '—' }}
+
+ @endforeach +
+ @else +

No verification recorded yet.

+ @endif +
diff --git a/resources/views/care/specialty/sections/workspace.blade.php b/resources/views/care/specialty/sections/workspace.blade.php index aeed64d..5287239 100644 --- a/resources/views/care/specialty/sections/workspace.blade.php +++ b/resources/views/care/specialty/sections/workspace.blade.php @@ -67,6 +67,12 @@ @include('care.specialty.physiotherapy.workspace-'.$activeTab) @elseif ($moduleKey === 'maternity' && in_array($activeTab, ['overview', 'postnatal'], true)) @include('care.specialty.maternity.workspace-'.$activeTab) + @elseif ($moduleKey === 'radiology' && in_array($activeTab, ['overview', 'verification'], true)) + @include('care.specialty.radiology.workspace-'.$activeTab) + @elseif ($moduleKey === 'cardiology' && in_array($activeTab, ['overview', 'procedures'], true)) + @include('care.specialty.cardiology.workspace-'.$activeTab) + @elseif ($moduleKey === 'psychiatry' && in_array($activeTab, ['overview', 'followup'], true)) + @include('care.specialty.psychiatry.workspace-'.$activeTab) @elseif ($activeTab === 'billing')

diff --git a/resources/views/care/specialty/shell.blade.php b/resources/views/care/specialty/shell.blade.php index 689f4d2..d2eead4 100644 --- a/resources/views/care/specialty/shell.blade.php +++ b/resources/views/care/specialty/shell.blade.php @@ -43,6 +43,15 @@ @if ($moduleKey === 'maternity') Reports @endif + @if ($moduleKey === 'radiology') + Reports + @endif + @if ($moduleKey === 'cardiology') + Reports + @endif + @if ($moduleKey === 'psychiatry') + Reports + @endif History @if ($canManageClinical ?? $canManageSpecialty ?? true) Billing diff --git a/routes/web.php b/routes/web.php index 5366523..142ec02 100644 --- a/routes/web.php +++ b/routes/web.php @@ -44,6 +44,9 @@ use App\Http\Controllers\Care\EmergencyWorkspaceController; use App\Http\Controllers\Care\OphthalmologyWorkspaceController; use App\Http\Controllers\Care\PhysiotherapyWorkspaceController; use App\Http\Controllers\Care\MaternityWorkspaceController; +use App\Http\Controllers\Care\RadiologyWorkspaceController; +use App\Http\Controllers\Care\CardiologyWorkspaceController; +use App\Http\Controllers\Care\PsychiatryWorkspaceController; use App\Http\Controllers\Care\SpecialtyModuleController; use App\Http\Controllers\Care\VisitWorkflowController; use App\Http\Controllers\NotificationController; @@ -129,6 +132,9 @@ Route::middleware(['auth', 'platform.session'])->group(function () { Route::get('/specialty/ophthalmology/reports', [OphthalmologyWorkspaceController::class, 'reports'])->name('care.specialty.ophthalmology.reports'); Route::get('/specialty/physiotherapy/reports', [PhysiotherapyWorkspaceController::class, 'reports'])->name('care.specialty.physiotherapy.reports'); Route::get('/specialty/maternity/reports', [MaternityWorkspaceController::class, 'reports'])->name('care.specialty.maternity.reports'); + Route::get('/specialty/radiology/reports', [RadiologyWorkspaceController::class, 'reports'])->name('care.specialty.radiology.reports'); + Route::get('/specialty/cardiology/reports', [CardiologyWorkspaceController::class, 'reports'])->name('care.specialty.cardiology.reports'); + Route::get('/specialty/psychiatry/reports', [PsychiatryWorkspaceController::class, 'reports'])->name('care.specialty.psychiatry.reports'); Route::get('/specialty/{module}/workspace/{visit?}', [SpecialtyModuleController::class, 'workspace'])->name('care.specialty.workspace'); Route::post('/specialty/{module}/workspace/{visit}/clinical', [SpecialtyModuleController::class, 'saveClinical'])->name('care.specialty.clinical.save'); Route::post('/specialty/{module}/workspace/{visit}/documents', [SpecialtyModuleController::class, 'uploadDocument'])->name('care.specialty.documents.upload'); @@ -171,6 +177,15 @@ Route::middleware(['auth', 'platform.session'])->group(function () { Route::post('/specialty/maternity/workspace/{visit}/stage', [MaternityWorkspaceController::class, 'setStage'])->name('care.specialty.maternity.stage'); Route::post('/specialty/maternity/workspace/{visit}/postnatal', [MaternityWorkspaceController::class, 'savePostnatal'])->name('care.specialty.maternity.postnatal'); Route::get('/specialty/maternity/workspace/{visit}/print', [MaternityWorkspaceController::class, 'printSummary'])->name('care.specialty.maternity.print'); + Route::post('/specialty/radiology/workspace/{visit}/stage', [RadiologyWorkspaceController::class, 'setStage'])->name('care.specialty.radiology.stage'); + Route::post('/specialty/radiology/workspace/{visit}/verification', [RadiologyWorkspaceController::class, 'saveVerification'])->name('care.specialty.radiology.verification'); + Route::get('/specialty/radiology/workspace/{visit}/print', [RadiologyWorkspaceController::class, 'printSummary'])->name('care.specialty.radiology.print'); + Route::post('/specialty/cardiology/workspace/{visit}/stage', [CardiologyWorkspaceController::class, 'setStage'])->name('care.specialty.cardiology.stage'); + Route::post('/specialty/cardiology/workspace/{visit}/procedure', [CardiologyWorkspaceController::class, 'saveProcedure'])->name('care.specialty.cardiology.procedure'); + Route::get('/specialty/cardiology/workspace/{visit}/print', [CardiologyWorkspaceController::class, 'printSummary'])->name('care.specialty.cardiology.print'); + Route::post('/specialty/psychiatry/workspace/{visit}/stage', [PsychiatryWorkspaceController::class, 'setStage'])->name('care.specialty.psychiatry.stage'); + Route::post('/specialty/psychiatry/workspace/{visit}/followup', [PsychiatryWorkspaceController::class, 'saveFollowUp'])->name('care.specialty.psychiatry.followup'); + Route::get('/specialty/psychiatry/workspace/{visit}/print', [PsychiatryWorkspaceController::class, 'printSummary'])->name('care.specialty.psychiatry.print'); Route::get('/specialty/{module}', [SpecialtyModuleController::class, 'show'])->name('care.specialty.show'); Route::post('/specialty/{module}/call-next', [SpecialtyModuleController::class, 'callNext'])->name('care.specialty.call-next'); Route::post('/specialty/{module}/refer', [SpecialtyModuleController::class, 'refer'])->name('care.specialty.refer'); diff --git a/tests/Feature/CareCardiologySuiteTest.php b/tests/Feature/CareCardiologySuiteTest.php new file mode 100644 index 0000000..9f79917 --- /dev/null +++ b/tests/Feature/CareCardiologySuiteTest.php @@ -0,0 +1,237 @@ +withoutMiddleware(EnsurePlatformSession::class); + + $this->owner = User::create([ + 'public_id' => 'car-owner', + 'name' => 'Owner', + 'email' => 'car-owner@example.com', + ]); + + $this->organization = Organization::create([ + 'owner_ref' => $this->owner->public_id, + 'name' => 'Cardiology Clinic', + 'slug' => 'cardiology-clinic', + 'settings' => [ + 'onboarded' => true, + 'facility_type' => 'clinic', + 'plan' => 'pro', + 'plan_expires_at' => now()->addMonth()->toIso8601String(), + 'queue_integration_enabled' => true, + ], + ]); + + Member::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'user_ref' => $this->owner->public_id, + 'role' => 'hospital_admin', + 'branch_id' => null, + ]); + + $this->branch = Branch::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'name' => 'Main', + 'is_active' => true, + ]); + + app(SpecialtyModuleService::class)->activate( + $this->organization, + $this->owner->public_id, + 'cardiology', + ); + + $department = Department::query() + ->where('branch_id', $this->branch->id) + ->where('type', 'cardiology') + ->firstOrFail(); + + $this->patient = Patient::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_number' => 'P-CAR-SUITE', + 'first_name' => 'Kwesi', + 'last_name' => 'Mensah', + 'gender' => 'male', + 'date_of_birth' => '1975-03-20', + ]); + + $this->visit = Visit::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_id' => $this->patient->id, + 'status' => Visit::STATUS_IN_PROGRESS, + 'checked_in_at' => now(), + 'specialty_stage' => 'check_in', + ]); + + Appointment::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_id' => $this->patient->id, + 'department_id' => $department->id, + 'visit_id' => $this->visit->id, + 'type' => Appointment::TYPE_WALK_IN, + 'status' => Appointment::STATUS_IN_CONSULTATION, + 'scheduled_at' => now(), + 'waiting_at' => now(), + 'checked_in_at' => now(), + 'started_at' => now(), + ]); + } + + public function test_overview_and_exam_tabs_render(): void + { + $this->actingAs($this->owner) + ->get(route('care.specialty.workspace', [ + 'module' => 'cardiology', + 'visit' => $this->visit, + 'tab' => 'overview', + ])) + ->assertOk() + ->assertSee('Cardiology overview') + ->assertSee('data-care-stage-bar', false) + ->assertSee('Exam / ECG') + ->assertSee('Start history') + ->assertSee('Cardiology reports'); + + $this->actingAs($this->owner) + ->get(route('care.specialty.workspace', [ + 'module' => 'cardiology', + 'visit' => $this->visit, + 'tab' => 'exam', + ])) + ->assertOk() + ->assertSee('Chief complaint') + ->assertSee('ECG findings'); + } + + public function test_exam_sets_stage_and_nyha_alert(): void + { + $this->actingAs($this->owner) + ->post(route('care.specialty.clinical.save', [ + 'module' => 'cardiology', + 'visit' => $this->visit, + ]), [ + 'tab' => 'exam', + 'payload' => [ + 'chief_complaint' => 'Dyspnoea', + 'nyha_class' => 'III', + 'bp' => '160/95', + 'hr' => 98, + 'ecg_findings' => 'Sinus tach; no STEMI', + 'exam_findings' => 'Basal crackles', + ], + ]) + ->assertRedirect(); + + $this->assertSame('exam', $this->visit->fresh()->specialty_stage); + + $record = SpecialtyClinicalRecord::query() + ->where('visit_id', $this->visit->id) + ->where('record_type', 'cardiac_exam') + ->firstOrFail(); + + $codes = collect($record->alerts)->pluck('code')->all(); + $this->assertContains('car.nyha_high', $codes); + } + + public function test_stage_advance_and_procedure_completes_visit(): void + { + $this->actingAs($this->owner) + ->post(route('care.specialty.cardiology.stage', $this->visit), [ + 'stage' => 'procedure', + ]) + ->assertRedirect(route('care.specialty.workspace', [ + 'module' => 'cardiology', + 'visit' => $this->visit, + 'tab' => 'procedures', + ])); + + $this->assertSame('procedure', $this->visit->fresh()->specialty_stage); + + $this->actingAs($this->owner) + ->post(route('care.specialty.cardiology.procedure', $this->visit), [ + 'tab' => 'procedures', + 'payload' => [ + 'procedure' => 'Cardioversion', + 'indication' => 'AF with RVR', + 'outcome' => 'Completed uneventfully', + 'post_procedure_plan' => 'Monitor 4 hours; continue anticoagulation', + ], + ]) + ->assertRedirect(route('care.specialty.workspace', [ + 'module' => 'cardiology', + 'visit' => $this->visit, + 'tab' => 'procedures', + ])); + + $this->assertSame('completed', $this->visit->fresh()->specialty_stage); + $this->assertSame(Visit::STATUS_COMPLETED, $this->visit->fresh()->status); + $this->assertDatabaseHas('care_specialty_clinical_records', [ + 'visit_id' => $this->visit->id, + 'record_type' => 'cardio_procedure', + 'status' => SpecialtyClinicalRecord::STATUS_COMPLETED, + ]); + } + + public function test_reports_and_print(): void + { + $this->actingAs($this->owner) + ->get(route('care.specialty.cardiology.reports')) + ->assertOk() + ->assertSee('Cardiology reports') + ->assertSee('Arrivals today'); + + $this->actingAs($this->owner) + ->get(route('care.specialty.cardiology.print', $this->visit)) + ->assertOk() + ->assertSee('Cardiology summary') + ->assertSee($this->patient->fullName()); + } + + public function test_list_index_does_not_auto_open_patient(): void + { + $this->actingAs($this->owner) + ->get(route('care.specialty.show', 'cardiology')) + ->assertOk() + ->assertDontSee('Cardiology overview'); + } +} diff --git a/tests/Feature/CarePsychiatrySuiteTest.php b/tests/Feature/CarePsychiatrySuiteTest.php new file mode 100644 index 0000000..93e1984 --- /dev/null +++ b/tests/Feature/CarePsychiatrySuiteTest.php @@ -0,0 +1,237 @@ +withoutMiddleware(EnsurePlatformSession::class); + + $this->owner = User::create([ + 'public_id' => 'psy-owner', + 'name' => 'Owner', + 'email' => 'psy-owner@example.com', + ]); + + $this->organization = Organization::create([ + 'owner_ref' => $this->owner->public_id, + 'name' => 'Psychiatry Clinic', + 'slug' => 'psychiatry-clinic', + 'settings' => [ + 'onboarded' => true, + 'facility_type' => 'clinic', + 'plan' => 'pro', + 'plan_expires_at' => now()->addMonth()->toIso8601String(), + 'queue_integration_enabled' => true, + ], + ]); + + Member::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'user_ref' => $this->owner->public_id, + 'role' => 'hospital_admin', + 'branch_id' => null, + ]); + + $this->branch = Branch::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'name' => 'Main', + 'is_active' => true, + ]); + + app(SpecialtyModuleService::class)->activate( + $this->organization, + $this->owner->public_id, + 'psychiatry', + ); + + $department = Department::query() + ->where('branch_id', $this->branch->id) + ->where('type', 'psychiatry') + ->firstOrFail(); + + $this->patient = Patient::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_number' => 'P-PSY-SUITE', + 'first_name' => 'Adwoa', + 'last_name' => 'Mensah', + 'gender' => 'female', + 'date_of_birth' => '1988-11-02', + ]); + + $this->visit = Visit::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_id' => $this->patient->id, + 'status' => Visit::STATUS_IN_PROGRESS, + 'checked_in_at' => now(), + 'specialty_stage' => 'check_in', + ]); + + Appointment::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_id' => $this->patient->id, + 'department_id' => $department->id, + 'visit_id' => $this->visit->id, + 'type' => Appointment::TYPE_WALK_IN, + 'status' => Appointment::STATUS_IN_CONSULTATION, + 'scheduled_at' => now(), + 'waiting_at' => now(), + 'checked_in_at' => now(), + 'started_at' => now(), + ]); + } + + public function test_overview_and_history_tabs_render(): void + { + $this->actingAs($this->owner) + ->get(route('care.specialty.workspace', [ + 'module' => 'psychiatry', + 'visit' => $this->visit, + 'tab' => 'overview', + ])) + ->assertOk() + ->assertSee('Psychiatry overview') + ->assertSee('data-care-stage-bar', false) + ->assertSee('History / MSE') + ->assertSee('Start history / MSE') + ->assertSee('Psychiatry reports'); + + $this->actingAs($this->owner) + ->get(route('care.specialty.workspace', [ + 'module' => 'psychiatry', + 'visit' => $this->visit, + 'tab' => 'history', + ])) + ->assertOk() + ->assertSee('Presenting complaint') + ->assertSee('MSE summary'); + } + + public function test_risk_sets_stage_and_alerts(): void + { + $this->actingAs($this->owner) + ->post(route('care.specialty.clinical.save', [ + 'module' => 'psychiatry', + 'visit' => $this->visit, + ]), [ + 'tab' => 'risk', + 'payload' => [ + 'overall_risk' => 'High', + 'self_harm' => 'Ideation', + 'harm_others' => 'None identified', + 'risk_summary' => 'High risk — safety planning required', + 'immediate_actions' => 'Safety plan; senior review', + ], + ]) + ->assertRedirect(); + + $this->assertSame('risk', $this->visit->fresh()->specialty_stage); + + $record = SpecialtyClinicalRecord::query() + ->where('visit_id', $this->visit->id) + ->where('record_type', 'risk_assessment') + ->firstOrFail(); + + $codes = collect($record->alerts)->pluck('code')->all(); + $this->assertContains('psy.risk_high', $codes); + } + + public function test_stage_advance_and_followup_completes_visit(): void + { + $this->actingAs($this->owner) + ->post(route('care.specialty.psychiatry.stage', $this->visit), [ + 'stage' => 'review', + ]) + ->assertRedirect(route('care.specialty.workspace', [ + 'module' => 'psychiatry', + 'visit' => $this->visit, + 'tab' => 'followup', + ])); + + $this->assertSame('review', $this->visit->fresh()->specialty_stage); + + $this->actingAs($this->owner) + ->post(route('care.specialty.psychiatry.followup', $this->visit), [ + 'tab' => 'followup', + 'payload' => [ + 'review_type' => 'Clinic follow-up', + 'progress' => 'Mood improved; ideation resolved', + 'current_risk' => 'Low', + 'outcome' => 'Follow-up arranged', + 'next_steps' => 'Return in 2 weeks', + ], + ]) + ->assertRedirect(route('care.specialty.workspace', [ + 'module' => 'psychiatry', + 'visit' => $this->visit, + 'tab' => 'followup', + ])); + + $this->assertSame('completed', $this->visit->fresh()->specialty_stage); + $this->assertSame(Visit::STATUS_COMPLETED, $this->visit->fresh()->status); + $this->assertDatabaseHas('care_specialty_clinical_records', [ + 'visit_id' => $this->visit->id, + 'record_type' => 'psych_followup', + 'status' => SpecialtyClinicalRecord::STATUS_COMPLETED, + ]); + } + + public function test_reports_and_print(): void + { + $this->actingAs($this->owner) + ->get(route('care.specialty.psychiatry.reports')) + ->assertOk() + ->assertSee('Psychiatry reports') + ->assertSee('Arrivals today'); + + $this->actingAs($this->owner) + ->get(route('care.specialty.psychiatry.print', $this->visit)) + ->assertOk() + ->assertSee('Psychiatry summary') + ->assertSee($this->patient->fullName()); + } + + public function test_list_index_does_not_auto_open_patient(): void + { + $this->actingAs($this->owner) + ->get(route('care.specialty.show', 'psychiatry')) + ->assertOk() + ->assertDontSee('Psychiatry overview'); + } +} diff --git a/tests/Feature/CareRadiologySuiteTest.php b/tests/Feature/CareRadiologySuiteTest.php new file mode 100644 index 0000000..f08b88e --- /dev/null +++ b/tests/Feature/CareRadiologySuiteTest.php @@ -0,0 +1,236 @@ +withoutMiddleware(EnsurePlatformSession::class); + + $this->owner = User::create([ + 'public_id' => 'rad-owner', + 'name' => 'Owner', + 'email' => 'rad-owner@example.com', + ]); + + $this->organization = Organization::create([ + 'owner_ref' => $this->owner->public_id, + 'name' => 'Radiology Clinic', + 'slug' => 'radiology-clinic', + 'settings' => [ + 'onboarded' => true, + 'facility_type' => 'clinic', + 'plan' => 'pro', + 'plan_expires_at' => now()->addMonth()->toIso8601String(), + 'queue_integration_enabled' => true, + ], + ]); + + Member::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'user_ref' => $this->owner->public_id, + 'role' => 'hospital_admin', + 'branch_id' => null, + ]); + + $this->branch = Branch::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'name' => 'Main', + 'is_active' => true, + ]); + + app(SpecialtyModuleService::class)->activate( + $this->organization, + $this->owner->public_id, + 'radiology', + ); + + $department = Department::query() + ->where('branch_id', $this->branch->id) + ->where('type', 'radiology') + ->firstOrFail(); + + $this->patient = Patient::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_number' => 'P-RAD-SUITE', + 'first_name' => 'Ama', + 'last_name' => 'Mensah', + 'gender' => 'female', + 'date_of_birth' => '1990-05-12', + ]); + + $this->visit = Visit::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_id' => $this->patient->id, + 'status' => Visit::STATUS_IN_PROGRESS, + 'checked_in_at' => now(), + 'specialty_stage' => 'check_in', + ]); + + Appointment::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_id' => $this->patient->id, + 'department_id' => $department->id, + 'visit_id' => $this->visit->id, + 'type' => Appointment::TYPE_WALK_IN, + 'status' => Appointment::STATUS_IN_CONSULTATION, + 'scheduled_at' => now(), + 'waiting_at' => now(), + 'checked_in_at' => now(), + 'started_at' => now(), + ]); + } + + public function test_overview_and_protocol_tabs_render(): void + { + $this->actingAs($this->owner) + ->get(route('care.specialty.workspace', [ + 'module' => 'radiology', + 'visit' => $this->visit, + 'tab' => 'overview', + ])) + ->assertOk() + ->assertSee('Radiology overview') + ->assertSee('data-care-stage-bar', false) + ->assertSee('Check-in') + ->assertSee('Start request / protocol') + ->assertSee('Radiology reports'); + + $this->actingAs($this->owner) + ->get(route('care.specialty.workspace', [ + 'module' => 'radiology', + 'visit' => $this->visit, + 'tab' => 'protocol', + ])) + ->assertOk() + ->assertSee('Modality') + ->assertSee('Body part / region'); + } + + public function test_request_sets_stage_and_stat_alert(): void + { + $this->actingAs($this->owner) + ->post(route('care.specialty.clinical.save', [ + 'module' => 'radiology', + 'visit' => $this->visit, + ]), [ + 'tab' => 'protocol', + 'payload' => [ + 'modality' => 'CT', + 'body_part' => 'Chest', + 'clinical_info' => 'Rule out PE', + 'urgency' => 'STAT', + 'contrast' => 'IV', + ], + ]) + ->assertRedirect(); + + $this->assertSame('protocol', $this->visit->fresh()->specialty_stage); + + $record = SpecialtyClinicalRecord::query() + ->where('visit_id', $this->visit->id) + ->where('record_type', 'imaging_request') + ->firstOrFail(); + + $codes = collect($record->alerts)->pluck('code')->all(); + $this->assertContains('rad.stat', $codes); + } + + public function test_stage_advance_and_verification_completes_visit(): void + { + $this->actingAs($this->owner) + ->post(route('care.specialty.radiology.stage', $this->visit), [ + 'stage' => 'verified', + ]) + ->assertRedirect(route('care.specialty.workspace', [ + 'module' => 'radiology', + 'visit' => $this->visit, + 'tab' => 'verification', + ])); + + $this->assertSame('verified', $this->visit->fresh()->specialty_stage); + + $this->actingAs($this->owner) + ->post(route('care.specialty.radiology.verification', $this->visit), [ + 'tab' => 'verification', + 'payload' => [ + 'verification_status' => 'Verified / final', + 'verified_by' => 'Dr. Demo Radiologist', + 'critical_communicated' => false, + 'notes' => 'Final report verified', + ], + ]) + ->assertRedirect(route('care.specialty.workspace', [ + 'module' => 'radiology', + 'visit' => $this->visit, + 'tab' => 'verification', + ])); + + $this->assertSame('completed', $this->visit->fresh()->specialty_stage); + $this->assertSame(Visit::STATUS_COMPLETED, $this->visit->fresh()->status); + $this->assertDatabaseHas('care_specialty_clinical_records', [ + 'visit_id' => $this->visit->id, + 'record_type' => 'imaging_verification', + 'status' => SpecialtyClinicalRecord::STATUS_COMPLETED, + ]); + } + + public function test_reports_and_print(): void + { + $this->actingAs($this->owner) + ->get(route('care.specialty.radiology.reports')) + ->assertOk() + ->assertSee('Radiology reports') + ->assertSee('Arrivals today'); + + $this->actingAs($this->owner) + ->get(route('care.specialty.radiology.print', $this->visit)) + ->assertOk() + ->assertSee('Radiology summary') + ->assertSee($this->patient->fullName()); + } + + public function test_list_index_does_not_auto_open_patient(): void + { + $this->actingAs($this->owner) + ->get(route('care.specialty.show', 'radiology')) + ->assertOk() + ->assertDontSee('Radiology overview'); + } +}