Add full Oncology, Renal Care, and Surgery specialty clinical suites.
Deploy Ladill Care / deploy (push) Successful in 32s
Deploy Ladill Care / deploy (push) Successful in 32s
Match Pediatrics/ENT depth with stages, workspace tabs, clinical records, reports/print, demo seed, and suite tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,215 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Care;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\SpecialtyClinicalRecord;
|
||||||
|
use App\Models\Visit;
|
||||||
|
use App\Services\Care\Oncology\OncologyAnalyticsService;
|
||||||
|
use App\Services\Care\Oncology\OncologyWorkflowService;
|
||||||
|
use App\Services\Care\SpecialtyClinicalRecordService;
|
||||||
|
use App\Services\Care\SpecialtyModuleService;
|
||||||
|
use App\Services\Care\SpecialtyShellService;
|
||||||
|
use App\Services\Care\SpecialtyVisitStageService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class OncologyWorkspaceController extends Controller
|
||||||
|
{
|
||||||
|
use ScopesToAccount;
|
||||||
|
|
||||||
|
protected function assertOncologyAccess(Request $request, SpecialtyModuleService $modules): void
|
||||||
|
{
|
||||||
|
$organization = $this->organization($request);
|
||||||
|
abort_unless($modules->memberCanAccess($organization, $this->member($request), 'oncology'), 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertOncologyManage(Request $request, SpecialtyModuleService $modules): void
|
||||||
|
{
|
||||||
|
$organization = $this->organization($request);
|
||||||
|
abort_unless($modules->memberCanManage($organization, $this->member($request), 'oncology'), 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->assertOncologyManage($request, $modules);
|
||||||
|
$this->assertVisit($request, $visit);
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'stage' => ['required', 'string', 'max:32'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stages->setStage(
|
||||||
|
$this->organization($request),
|
||||||
|
$visit,
|
||||||
|
'oncology',
|
||||||
|
$validated['stage'],
|
||||||
|
$this->ownerRef($request),
|
||||||
|
$this->actorRef($request),
|
||||||
|
);
|
||||||
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
return back()->with('error', $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('care.specialty.workspace', [
|
||||||
|
'module' => 'oncology',
|
||||||
|
'visit' => $visit,
|
||||||
|
'tab' => $shell->workspaceTabForStage('oncology', $validated['stage']),
|
||||||
|
])
|
||||||
|
->with('success', 'Visit stage updated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveTreatment(
|
||||||
|
Request $request,
|
||||||
|
Visit $visit,
|
||||||
|
SpecialtyModuleService $modules,
|
||||||
|
SpecialtyClinicalRecordService $clinical,
|
||||||
|
SpecialtyVisitStageService $stages,
|
||||||
|
OncologyWorkflowService $workflow,
|
||||||
|
): RedirectResponse {
|
||||||
|
$this->authorizeAbility($request, 'consultations.manage');
|
||||||
|
$this->assertOncologyManage($request, $modules);
|
||||||
|
$this->assertVisit($request, $visit);
|
||||||
|
|
||||||
|
$organization = $this->organization($request);
|
||||||
|
$owner = $this->ownerRef($request);
|
||||||
|
|
||||||
|
$payload = (array) $request->input('payload', []);
|
||||||
|
foreach ($clinical->fieldsFor('oncology', 'onc_treatment') as $field) {
|
||||||
|
if (($field['type'] ?? '') === 'boolean') {
|
||||||
|
$payload[$field['name']] = $request->boolean('payload.'.$field['name']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$request->merge(['payload' => $payload, 'tab' => 'treatment']);
|
||||||
|
|
||||||
|
$validated = $request->validate(array_merge([
|
||||||
|
'tab' => ['required', 'string'],
|
||||||
|
], $clinical->validationRules('oncology', 'onc_treatment')));
|
||||||
|
|
||||||
|
$record = $clinical->upsert(
|
||||||
|
$organization,
|
||||||
|
$visit,
|
||||||
|
'oncology',
|
||||||
|
'onc_treatment',
|
||||||
|
$clinical->payloadFromRequest($validated),
|
||||||
|
$owner,
|
||||||
|
$owner,
|
||||||
|
OncologyWorkflowService::STAGE_TREATMENT,
|
||||||
|
SpecialtyClinicalRecord::STATUS_COMPLETED,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stages->setStage(
|
||||||
|
$organization,
|
||||||
|
$visit,
|
||||||
|
'oncology',
|
||||||
|
OncologyWorkflowService::STAGE_TREATMENT,
|
||||||
|
$owner,
|
||||||
|
$owner,
|
||||||
|
);
|
||||||
|
} catch (\InvalidArgumentException) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($workflow->shouldCompleteVisit($record->payload ?? [])) {
|
||||||
|
try {
|
||||||
|
$stages->setStage(
|
||||||
|
$organization,
|
||||||
|
$visit,
|
||||||
|
'oncology',
|
||||||
|
OncologyWorkflowService::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' => 'oncology', 'visit' => $visit, 'tab' => 'treatment'])
|
||||||
|
->with('success', 'Oncology treatment saved.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reports(
|
||||||
|
Request $request,
|
||||||
|
SpecialtyModuleService $modules,
|
||||||
|
SpecialtyShellService $shell,
|
||||||
|
OncologyAnalyticsService $analytics,
|
||||||
|
): View {
|
||||||
|
$this->authorizeAbility($request, 'consultations.view');
|
||||||
|
$this->assertOncologyAccess($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.oncology.reports', [
|
||||||
|
'moduleKey' => 'oncology',
|
||||||
|
'definition' => $modules->definition('oncology'),
|
||||||
|
'shellNav' => $shell->navItems('oncology'),
|
||||||
|
'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->assertOncologyAccess($request, $modules);
|
||||||
|
$this->assertVisit($request, $visit);
|
||||||
|
|
||||||
|
$visit->loadMissing(['patient', 'appointment.practitioner', 'branch']);
|
||||||
|
|
||||||
|
return view('care.specialty.oncology.print', [
|
||||||
|
'visit' => $visit,
|
||||||
|
'patient' => $visit->patient,
|
||||||
|
'history' => $clinical->findForVisit($visit, 'oncology', 'onc_history'),
|
||||||
|
'staging' => $clinical->findForVisit($visit, 'oncology', 'onc_staging'),
|
||||||
|
'investigation' => $clinical->findForVisit($visit, 'oncology', 'onc_investigation'),
|
||||||
|
'plan' => $clinical->findForVisit($visit, 'oncology', 'onc_plan'),
|
||||||
|
'treatment' => $clinical->findForVisit($visit, 'oncology', 'onc_treatment'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Care;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\SpecialtyClinicalRecord;
|
||||||
|
use App\Models\Visit;
|
||||||
|
use App\Services\Care\Renal\RenalAnalyticsService;
|
||||||
|
use App\Services\Care\Renal\RenalWorkflowService;
|
||||||
|
use App\Services\Care\SpecialtyClinicalRecordService;
|
||||||
|
use App\Services\Care\SpecialtyModuleService;
|
||||||
|
use App\Services\Care\SpecialtyShellService;
|
||||||
|
use App\Services\Care\SpecialtyVisitStageService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class RenalWorkspaceController extends Controller
|
||||||
|
{
|
||||||
|
use ScopesToAccount;
|
||||||
|
|
||||||
|
protected function assertRenalAccess(Request $request, SpecialtyModuleService $modules): void
|
||||||
|
{
|
||||||
|
$organization = $this->organization($request);
|
||||||
|
abort_unless($modules->memberCanAccess($organization, $this->member($request), 'renal'), 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertRenalManage(Request $request, SpecialtyModuleService $modules): void
|
||||||
|
{
|
||||||
|
$organization = $this->organization($request);
|
||||||
|
abort_unless($modules->memberCanManage($organization, $this->member($request), 'renal'), 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->assertRenalManage($request, $modules);
|
||||||
|
$this->assertVisit($request, $visit);
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'stage' => ['required', 'string', 'max:32'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stages->setStage(
|
||||||
|
$this->organization($request),
|
||||||
|
$visit,
|
||||||
|
'renal',
|
||||||
|
$validated['stage'],
|
||||||
|
$this->ownerRef($request),
|
||||||
|
$this->actorRef($request),
|
||||||
|
);
|
||||||
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
return back()->with('error', $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('care.specialty.workspace', [
|
||||||
|
'module' => 'renal',
|
||||||
|
'visit' => $visit,
|
||||||
|
'tab' => $shell->workspaceTabForStage('renal', $validated['stage']),
|
||||||
|
])
|
||||||
|
->with('success', 'Visit stage updated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function savePlan(
|
||||||
|
Request $request,
|
||||||
|
Visit $visit,
|
||||||
|
SpecialtyModuleService $modules,
|
||||||
|
SpecialtyClinicalRecordService $clinical,
|
||||||
|
SpecialtyVisitStageService $stages,
|
||||||
|
RenalWorkflowService $workflow,
|
||||||
|
): RedirectResponse {
|
||||||
|
$this->authorizeAbility($request, 'consultations.manage');
|
||||||
|
$this->assertRenalManage($request, $modules);
|
||||||
|
$this->assertVisit($request, $visit);
|
||||||
|
|
||||||
|
$organization = $this->organization($request);
|
||||||
|
$owner = $this->ownerRef($request);
|
||||||
|
|
||||||
|
$payload = (array) $request->input('payload', []);
|
||||||
|
foreach ($clinical->fieldsFor('renal', 'renal_plan') as $field) {
|
||||||
|
if (($field['type'] ?? '') === 'boolean') {
|
||||||
|
$payload[$field['name']] = $request->boolean('payload.'.$field['name']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$request->merge(['payload' => $payload, 'tab' => 'plan']);
|
||||||
|
|
||||||
|
$validated = $request->validate(array_merge([
|
||||||
|
'tab' => ['required', 'string'],
|
||||||
|
], $clinical->validationRules('renal', 'renal_plan')));
|
||||||
|
|
||||||
|
$record = $clinical->upsert(
|
||||||
|
$organization,
|
||||||
|
$visit,
|
||||||
|
'renal',
|
||||||
|
'renal_plan',
|
||||||
|
$clinical->payloadFromRequest($validated),
|
||||||
|
$owner,
|
||||||
|
$owner,
|
||||||
|
RenalWorkflowService::STAGE_PLAN,
|
||||||
|
SpecialtyClinicalRecord::STATUS_COMPLETED,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stages->setStage(
|
||||||
|
$organization,
|
||||||
|
$visit,
|
||||||
|
'renal',
|
||||||
|
RenalWorkflowService::STAGE_PLAN,
|
||||||
|
$owner,
|
||||||
|
$owner,
|
||||||
|
);
|
||||||
|
} catch (\InvalidArgumentException) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($workflow->shouldCompleteVisit($record->payload ?? [])) {
|
||||||
|
try {
|
||||||
|
$stages->setStage(
|
||||||
|
$organization,
|
||||||
|
$visit,
|
||||||
|
'renal',
|
||||||
|
RenalWorkflowService::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' => 'renal', 'visit' => $visit, 'tab' => 'plan'])
|
||||||
|
->with('success', 'Renal care plan saved.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reports(
|
||||||
|
Request $request,
|
||||||
|
SpecialtyModuleService $modules,
|
||||||
|
SpecialtyShellService $shell,
|
||||||
|
RenalAnalyticsService $analytics,
|
||||||
|
): View {
|
||||||
|
$this->authorizeAbility($request, 'consultations.view');
|
||||||
|
$this->assertRenalAccess($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.renal.reports', [
|
||||||
|
'moduleKey' => 'renal',
|
||||||
|
'definition' => $modules->definition('renal'),
|
||||||
|
'shellNav' => $shell->navItems('renal'),
|
||||||
|
'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->assertRenalAccess($request, $modules);
|
||||||
|
$this->assertVisit($request, $visit);
|
||||||
|
|
||||||
|
$visit->loadMissing(['patient', 'appointment.practitioner', 'branch']);
|
||||||
|
|
||||||
|
return view('care.specialty.renal.print', [
|
||||||
|
'visit' => $visit,
|
||||||
|
'patient' => $visit->patient,
|
||||||
|
'history' => $clinical->findForVisit($visit, 'renal', 'renal_history'),
|
||||||
|
'exam' => $clinical->findForVisit($visit, 'renal', 'renal_exam'),
|
||||||
|
'dialysis' => $clinical->findForVisit($visit, 'renal', 'renal_session'),
|
||||||
|
'plan' => $clinical->findForVisit($visit, 'renal', 'renal_plan'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -162,6 +162,9 @@ class SpecialtyModuleController extends Controller
|
|||||||
'pediatrics' => 'history',
|
'pediatrics' => 'history',
|
||||||
'orthopedics' => 'history',
|
'orthopedics' => 'history',
|
||||||
'ent' => 'history',
|
'ent' => 'history',
|
||||||
|
'oncology' => 'history',
|
||||||
|
'renal' => 'history',
|
||||||
|
'surgery' => 'history',
|
||||||
default => (collect(array_keys($shellTabs))
|
default => (collect(array_keys($shellTabs))
|
||||||
->first(fn (string $tab) => $clinical->recordTypeForTab($module, $tab) !== null)
|
->first(fn (string $tab) => $clinical->recordTypeForTab($module, $tab) !== null)
|
||||||
?? 'clinical_notes'),
|
?? 'clinical_notes'),
|
||||||
@@ -242,6 +245,9 @@ class SpecialtyModuleController extends Controller
|
|||||||
'pediatrics' => 'history',
|
'pediatrics' => 'history',
|
||||||
'orthopedics' => 'history',
|
'orthopedics' => 'history',
|
||||||
'ent' => 'history',
|
'ent' => 'history',
|
||||||
|
'oncology' => 'history',
|
||||||
|
'renal' => 'history',
|
||||||
|
'surgery' => 'history',
|
||||||
default => (collect(array_keys($shellTabs))
|
default => (collect(array_keys($shellTabs))
|
||||||
->first(fn (string $tab) => $clinical->recordTypeForTab($module, $tab) !== null)
|
->first(fn (string $tab) => $clinical->recordTypeForTab($module, $tab) !== null)
|
||||||
?? 'clinical_notes'),
|
?? 'clinical_notes'),
|
||||||
@@ -806,6 +812,109 @@ class SpecialtyModuleController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if ($module === 'oncology' && in_array($recordType, ['onc_history', 'onc_staging', 'onc_investigation', 'onc_plan'], true)) {
|
||||||
|
$workflow = app(\App\Services\Care\Oncology\OncologyWorkflowService::class);
|
||||||
|
$stageService = app(\App\Services\Care\SpecialtyVisitStageService::class);
|
||||||
|
$suggested = match ($recordType) {
|
||||||
|
'onc_history' => $workflow->stageFromHistory($record->payload ?? []),
|
||||||
|
'onc_staging' => $workflow->stageFromStaging($record->payload ?? []),
|
||||||
|
'onc_investigation' => $workflow->stageFromInvestigation($record->payload ?? []),
|
||||||
|
'onc_plan' => $workflow->stageFromPlan($record->payload ?? []),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
$current = $visit->specialty_stage;
|
||||||
|
$early = ['check_in', 'waiting', null, ''];
|
||||||
|
$order = [
|
||||||
|
'check_in' => 0,
|
||||||
|
'history' => 1,
|
||||||
|
'staging' => 2,
|
||||||
|
'investigation' => 3,
|
||||||
|
'plan' => 4,
|
||||||
|
'treatment' => 5,
|
||||||
|
'completed' => 6,
|
||||||
|
];
|
||||||
|
if ($suggested && (! $current || in_array($current, $early, true))) {
|
||||||
|
try {
|
||||||
|
$stageService->setStage($organization, $visit, 'oncology', $suggested, $this->ownerRef($request), $this->ownerRef($request));
|
||||||
|
} catch (\InvalidArgumentException) {
|
||||||
|
}
|
||||||
|
} elseif ($suggested && $suggested !== $current && ($order[$suggested] ?? 0) > ($order[$current] ?? 0)) {
|
||||||
|
try {
|
||||||
|
$stageService->setStage($organization, $visit, 'oncology', $suggested, $this->ownerRef($request), $this->ownerRef($request));
|
||||||
|
} catch (\InvalidArgumentException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($module === 'renal' && in_array($recordType, ['renal_history', 'renal_exam', 'renal_session'], true)) {
|
||||||
|
$workflow = app(\App\Services\Care\Renal\RenalWorkflowService::class);
|
||||||
|
$stageService = app(\App\Services\Care\SpecialtyVisitStageService::class);
|
||||||
|
$suggested = match ($recordType) {
|
||||||
|
'renal_history' => $workflow->stageFromHistory($record->payload ?? []),
|
||||||
|
'renal_exam' => $workflow->stageFromExam($record->payload ?? []),
|
||||||
|
'renal_session' => $workflow->stageFromDialysis($record->payload ?? []),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
$current = $visit->specialty_stage;
|
||||||
|
$early = ['check_in', 'waiting', null, ''];
|
||||||
|
$order = [
|
||||||
|
'check_in' => 0,
|
||||||
|
'history' => 1,
|
||||||
|
'exam' => 2,
|
||||||
|
'dialysis' => 3,
|
||||||
|
'plan' => 4,
|
||||||
|
'completed' => 5,
|
||||||
|
];
|
||||||
|
if ($suggested && (! $current || in_array($current, $early, true))) {
|
||||||
|
try {
|
||||||
|
$stageService->setStage($organization, $visit, 'renal', $suggested, $this->ownerRef($request), $this->ownerRef($request));
|
||||||
|
} catch (\InvalidArgumentException) {
|
||||||
|
}
|
||||||
|
} elseif ($suggested && $suggested !== $current && ($order[$suggested] ?? 0) > ($order[$current] ?? 0)) {
|
||||||
|
try {
|
||||||
|
$stageService->setStage($organization, $visit, 'renal', $suggested, $this->ownerRef($request), $this->ownerRef($request));
|
||||||
|
} catch (\InvalidArgumentException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($module === 'surgery' && in_array($recordType, ['surg_history', 'surg_exam', 'surg_investigation', 'surg_plan', 'surg_procedure'], true)) {
|
||||||
|
$workflow = app(\App\Services\Care\Surgery\SurgeryWorkflowService::class);
|
||||||
|
$stageService = app(\App\Services\Care\SpecialtyVisitStageService::class);
|
||||||
|
$suggested = match ($recordType) {
|
||||||
|
'surg_history' => $workflow->stageFromHistory($record->payload ?? []),
|
||||||
|
'surg_exam' => $workflow->stageFromExam($record->payload ?? []),
|
||||||
|
'surg_investigation' => $workflow->stageFromInvestigation($record->payload ?? []),
|
||||||
|
'surg_plan' => $workflow->stageFromPlan($record->payload ?? []),
|
||||||
|
'surg_procedure' => $workflow->stageFromProcedure($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,
|
||||||
|
'postop' => 6,
|
||||||
|
'completed' => 7,
|
||||||
|
];
|
||||||
|
if ($suggested && (! $current || in_array($current, $early, true))) {
|
||||||
|
try {
|
||||||
|
$stageService->setStage($organization, $visit, 'surgery', $suggested, $this->ownerRef($request), $this->ownerRef($request));
|
||||||
|
} catch (\InvalidArgumentException) {
|
||||||
|
}
|
||||||
|
} elseif ($suggested && $suggested !== $current && ($order[$suggested] ?? 0) > ($order[$current] ?? 0)) {
|
||||||
|
try {
|
||||||
|
$stageService->setStage($organization, $visit, 'surgery', $suggested, $this->ownerRef($request), $this->ownerRef($request));
|
||||||
|
} catch (\InvalidArgumentException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()
|
return redirect()
|
||||||
->route('care.specialty.workspace', [
|
->route('care.specialty.workspace', [
|
||||||
'module' => $module,
|
'module' => $module,
|
||||||
@@ -1262,6 +1371,27 @@ class SpecialtyModuleController extends Controller
|
|||||||
$entProcedure = null;
|
$entProcedure = null;
|
||||||
$entStageCodes = [];
|
$entStageCodes = [];
|
||||||
$entStageFlow = [];
|
$entStageFlow = [];
|
||||||
|
$oncologyHistory = null;
|
||||||
|
$oncologyStaging = null;
|
||||||
|
$oncologyInvestigation = null;
|
||||||
|
$oncologyPlan = null;
|
||||||
|
$oncologyTreatment = null;
|
||||||
|
$oncologyStageCodes = [];
|
||||||
|
$oncologyStageFlow = [];
|
||||||
|
$renalHistory = null;
|
||||||
|
$renalExam = null;
|
||||||
|
$renalDialysis = null;
|
||||||
|
$renalPlan = null;
|
||||||
|
$renalStageCodes = [];
|
||||||
|
$renalStageFlow = [];
|
||||||
|
$surgeryHistory = null;
|
||||||
|
$surgeryExam = null;
|
||||||
|
$surgeryInvestigation = null;
|
||||||
|
$surgeryPlan = null;
|
||||||
|
$surgeryProcedure = null;
|
||||||
|
$surgeryPostop = null;
|
||||||
|
$surgeryStageCodes = [];
|
||||||
|
$surgeryStageFlow = [];
|
||||||
$activeTab = (string) $request->query('tab', array_key_first($shell->workspaceTabs($module)) ?: 'overview');
|
$activeTab = (string) $request->query('tab', array_key_first($shell->workspaceTabs($module)) ?: 'overview');
|
||||||
$clinical = app(SpecialtyClinicalRecordService::class);
|
$clinical = app(SpecialtyClinicalRecordService::class);
|
||||||
|
|
||||||
@@ -1456,6 +1586,36 @@ class SpecialtyModuleController extends Controller
|
|||||||
$entStageFlow = app(\App\Services\Care\Ent\EntWorkflowService::class)->stageFlow();
|
$entStageFlow = app(\App\Services\Care\Ent\EntWorkflowService::class)->stageFlow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($module === 'oncology') {
|
||||||
|
$oncologyHistory = $clinical->findForVisit($workspaceVisit, 'oncology', 'onc_history');
|
||||||
|
$oncologyStaging = $clinical->findForVisit($workspaceVisit, 'oncology', 'onc_staging');
|
||||||
|
$oncologyInvestigation = $clinical->findForVisit($workspaceVisit, 'oncology', 'onc_investigation');
|
||||||
|
$oncologyPlan = $clinical->findForVisit($workspaceVisit, 'oncology', 'onc_plan');
|
||||||
|
$oncologyTreatment = $clinical->findForVisit($workspaceVisit, 'oncology', 'onc_treatment');
|
||||||
|
$oncologyStageCodes = collect($shell->stages('oncology'))->pluck('code')->all();
|
||||||
|
$oncologyStageFlow = app(\App\Services\Care\Oncology\OncologyWorkflowService::class)->stageFlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($module === 'renal') {
|
||||||
|
$renalHistory = $clinical->findForVisit($workspaceVisit, 'renal', 'renal_history');
|
||||||
|
$renalExam = $clinical->findForVisit($workspaceVisit, 'renal', 'renal_exam');
|
||||||
|
$renalDialysis = $clinical->findForVisit($workspaceVisit, 'renal', 'renal_session');
|
||||||
|
$renalPlan = $clinical->findForVisit($workspaceVisit, 'renal', 'renal_plan');
|
||||||
|
$renalStageCodes = collect($shell->stages('renal'))->pluck('code')->all();
|
||||||
|
$renalStageFlow = app(\App\Services\Care\Renal\RenalWorkflowService::class)->stageFlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($module === 'surgery') {
|
||||||
|
$surgeryHistory = $clinical->findForVisit($workspaceVisit, 'surgery', 'surg_history');
|
||||||
|
$surgeryExam = $clinical->findForVisit($workspaceVisit, 'surgery', 'surg_exam');
|
||||||
|
$surgeryInvestigation = $clinical->findForVisit($workspaceVisit, 'surgery', 'surg_investigation');
|
||||||
|
$surgeryPlan = $clinical->findForVisit($workspaceVisit, 'surgery', 'surg_plan');
|
||||||
|
$surgeryProcedure = $clinical->findForVisit($workspaceVisit, 'surgery', 'surg_procedure');
|
||||||
|
$surgeryPostop = $clinical->findForVisit($workspaceVisit, 'surgery', 'surg_postop');
|
||||||
|
$surgeryStageCodes = collect($shell->stages('surgery'))->pluck('code')->all();
|
||||||
|
$surgeryStageFlow = app(\App\Services\Care\Surgery\SurgeryWorkflowService::class)->stageFlow();
|
||||||
|
}
|
||||||
|
|
||||||
if ($patientHeader !== null) {
|
if ($patientHeader !== null) {
|
||||||
$patientHeader['clinical_alerts'] = $clinicalAlerts;
|
$patientHeader['clinical_alerts'] = $clinicalAlerts;
|
||||||
}
|
}
|
||||||
@@ -1604,6 +1764,27 @@ class SpecialtyModuleController extends Controller
|
|||||||
'entProcedure' => $entProcedure,
|
'entProcedure' => $entProcedure,
|
||||||
'entStageCodes' => $entStageCodes,
|
'entStageCodes' => $entStageCodes,
|
||||||
'entStageFlow' => $entStageFlow,
|
'entStageFlow' => $entStageFlow,
|
||||||
|
'oncologyHistory' => $oncologyHistory,
|
||||||
|
'oncologyStaging' => $oncologyStaging,
|
||||||
|
'oncologyInvestigation' => $oncologyInvestigation,
|
||||||
|
'oncologyPlan' => $oncologyPlan,
|
||||||
|
'oncologyTreatment' => $oncologyTreatment,
|
||||||
|
'oncologyStageCodes' => $oncologyStageCodes,
|
||||||
|
'oncologyStageFlow' => $oncologyStageFlow,
|
||||||
|
'renalHistory' => $renalHistory,
|
||||||
|
'renalExam' => $renalExam,
|
||||||
|
'renalDialysis' => $renalDialysis,
|
||||||
|
'renalPlan' => $renalPlan,
|
||||||
|
'renalStageCodes' => $renalStageCodes,
|
||||||
|
'renalStageFlow' => $renalStageFlow,
|
||||||
|
'surgeryHistory' => $surgeryHistory,
|
||||||
|
'surgeryExam' => $surgeryExam,
|
||||||
|
'surgeryInvestigation' => $surgeryInvestigation,
|
||||||
|
'surgeryPlan' => $surgeryPlan,
|
||||||
|
'surgeryProcedure' => $surgeryProcedure,
|
||||||
|
'surgeryPostop' => $surgeryPostop,
|
||||||
|
'surgeryStageCodes' => $surgeryStageCodes,
|
||||||
|
'surgeryStageFlow' => $surgeryStageFlow,
|
||||||
'queueStubs' => $modules->queueStubsFor($organization, $module),
|
'queueStubs' => $modules->queueStubsFor($organization, $module),
|
||||||
'branchId' => $branchId,
|
'branchId' => $branchId,
|
||||||
'branchLabel' => $branchLabel,
|
'branchLabel' => $branchLabel,
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Care;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\SpecialtyClinicalRecord;
|
||||||
|
use App\Models\Visit;
|
||||||
|
use App\Services\Care\SpecialtyClinicalRecordService;
|
||||||
|
use App\Services\Care\SpecialtyModuleService;
|
||||||
|
use App\Services\Care\SpecialtyShellService;
|
||||||
|
use App\Services\Care\SpecialtyVisitStageService;
|
||||||
|
use App\Services\Care\Surgery\SurgeryAnalyticsService;
|
||||||
|
use App\Services\Care\Surgery\SurgeryWorkflowService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class SurgeryWorkspaceController extends Controller
|
||||||
|
{
|
||||||
|
use ScopesToAccount;
|
||||||
|
|
||||||
|
protected function assertSurgeryAccess(Request $request, SpecialtyModuleService $modules): void
|
||||||
|
{
|
||||||
|
$organization = $this->organization($request);
|
||||||
|
abort_unless($modules->memberCanAccess($organization, $this->member($request), 'surgery'), 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertSurgeryManage(Request $request, SpecialtyModuleService $modules): void
|
||||||
|
{
|
||||||
|
$organization = $this->organization($request);
|
||||||
|
abort_unless($modules->memberCanManage($organization, $this->member($request), 'surgery'), 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->assertSurgeryManage($request, $modules);
|
||||||
|
$this->assertVisit($request, $visit);
|
||||||
|
|
||||||
|
$validated = $request->validate([
|
||||||
|
'stage' => ['required', 'string', 'max:32'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stages->setStage(
|
||||||
|
$this->organization($request),
|
||||||
|
$visit,
|
||||||
|
'surgery',
|
||||||
|
$validated['stage'],
|
||||||
|
$this->ownerRef($request),
|
||||||
|
$this->actorRef($request),
|
||||||
|
);
|
||||||
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
return back()->with('error', $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('care.specialty.workspace', [
|
||||||
|
'module' => 'surgery',
|
||||||
|
'visit' => $visit,
|
||||||
|
'tab' => $shell->workspaceTabForStage('surgery', $validated['stage']),
|
||||||
|
])
|
||||||
|
->with('success', 'Visit stage updated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function savePostop(
|
||||||
|
Request $request,
|
||||||
|
Visit $visit,
|
||||||
|
SpecialtyModuleService $modules,
|
||||||
|
SpecialtyClinicalRecordService $clinical,
|
||||||
|
SpecialtyVisitStageService $stages,
|
||||||
|
SurgeryWorkflowService $workflow,
|
||||||
|
): RedirectResponse {
|
||||||
|
$this->authorizeAbility($request, 'consultations.manage');
|
||||||
|
$this->assertSurgeryManage($request, $modules);
|
||||||
|
$this->assertVisit($request, $visit);
|
||||||
|
|
||||||
|
$organization = $this->organization($request);
|
||||||
|
$owner = $this->ownerRef($request);
|
||||||
|
|
||||||
|
$payload = (array) $request->input('payload', []);
|
||||||
|
foreach ($clinical->fieldsFor('surgery', 'surg_postop') as $field) {
|
||||||
|
if (($field['type'] ?? '') === 'boolean') {
|
||||||
|
$payload[$field['name']] = $request->boolean('payload.'.$field['name']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$request->merge(['payload' => $payload, 'tab' => 'postop']);
|
||||||
|
|
||||||
|
$validated = $request->validate(array_merge([
|
||||||
|
'tab' => ['required', 'string'],
|
||||||
|
], $clinical->validationRules('surgery', 'surg_postop')));
|
||||||
|
|
||||||
|
$record = $clinical->upsert(
|
||||||
|
$organization,
|
||||||
|
$visit,
|
||||||
|
'surgery',
|
||||||
|
'surg_postop',
|
||||||
|
$clinical->payloadFromRequest($validated),
|
||||||
|
$owner,
|
||||||
|
$owner,
|
||||||
|
SurgeryWorkflowService::STAGE_POSTOP,
|
||||||
|
SpecialtyClinicalRecord::STATUS_COMPLETED,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$stages->setStage(
|
||||||
|
$organization,
|
||||||
|
$visit,
|
||||||
|
'surgery',
|
||||||
|
SurgeryWorkflowService::STAGE_POSTOP,
|
||||||
|
$owner,
|
||||||
|
$owner,
|
||||||
|
);
|
||||||
|
} catch (\InvalidArgumentException) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($workflow->shouldCompleteVisit($record->payload ?? [])) {
|
||||||
|
try {
|
||||||
|
$stages->setStage(
|
||||||
|
$organization,
|
||||||
|
$visit,
|
||||||
|
'surgery',
|
||||||
|
SurgeryWorkflowService::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' => 'surgery', 'visit' => $visit, 'tab' => 'postop'])
|
||||||
|
->with('success', 'Post-op note saved.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reports(
|
||||||
|
Request $request,
|
||||||
|
SpecialtyModuleService $modules,
|
||||||
|
SpecialtyShellService $shell,
|
||||||
|
SurgeryAnalyticsService $analytics,
|
||||||
|
): View {
|
||||||
|
$this->authorizeAbility($request, 'consultations.view');
|
||||||
|
$this->assertSurgeryAccess($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.surgery.reports', [
|
||||||
|
'moduleKey' => 'surgery',
|
||||||
|
'definition' => $modules->definition('surgery'),
|
||||||
|
'shellNav' => $shell->navItems('surgery'),
|
||||||
|
'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->assertSurgeryAccess($request, $modules);
|
||||||
|
$this->assertVisit($request, $visit);
|
||||||
|
|
||||||
|
$visit->loadMissing(['patient', 'appointment.practitioner', 'branch']);
|
||||||
|
|
||||||
|
return view('care.specialty.surgery.print', [
|
||||||
|
'visit' => $visit,
|
||||||
|
'patient' => $visit->patient,
|
||||||
|
'history' => $clinical->findForVisit($visit, 'surgery', 'surg_history'),
|
||||||
|
'exam' => $clinical->findForVisit($visit, 'surgery', 'surg_exam'),
|
||||||
|
'investigation' => $clinical->findForVisit($visit, 'surgery', 'surg_investigation'),
|
||||||
|
'plan' => $clinical->findForVisit($visit, 'surgery', 'surg_plan'),
|
||||||
|
'procedure' => $clinical->findForVisit($visit, 'surgery', 'surg_procedure'),
|
||||||
|
'postop' => $clinical->findForVisit($visit, 'surgery', 'surg_postop'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1023,6 +1023,9 @@ class DemoTenantSeeder
|
|||||||
$this->seedPediatricsClinicalDemo($organization, $ownerRef);
|
$this->seedPediatricsClinicalDemo($organization, $ownerRef);
|
||||||
$this->seedOrthopedicsClinicalDemo($organization, $ownerRef);
|
$this->seedOrthopedicsClinicalDemo($organization, $ownerRef);
|
||||||
$this->seedEntClinicalDemo($organization, $ownerRef);
|
$this->seedEntClinicalDemo($organization, $ownerRef);
|
||||||
|
$this->seedOncologyClinicalDemo($organization, $ownerRef);
|
||||||
|
$this->seedRenalClinicalDemo($organization, $ownerRef);
|
||||||
|
$this->seedSurgeryClinicalDemo($organization, $ownerRef);
|
||||||
|
|
||||||
return $count;
|
return $count;
|
||||||
}
|
}
|
||||||
@@ -2222,6 +2225,226 @@ class DemoTenantSeeder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private function seedOncologyClinicalDemo(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', 'oncology')
|
||||||
|
->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, 'oncology', 'onc_history', [
|
||||||
|
'history' => $highRisk
|
||||||
|
? 'New diagnosis work-up; progressive fatigue and weight loss'
|
||||||
|
: 'On adjuvant regimen; here for cycle review',
|
||||||
|
'cancer_history' => $highRisk ? 'Breast mass biopsied last month' : 'Stage II breast Ca — surgery 4 months ago',
|
||||||
|
'medications' => $highRisk ? 'Analgesia PRN' : 'Current chemo cycle medications',
|
||||||
|
'allergies' => 'NKDA',
|
||||||
|
'comorbidities' => $highRisk ? 'Hypertension' : 'None significant',
|
||||||
|
'social' => 'Family support available',
|
||||||
|
], $ownerRef, $ownerRef, 'history');
|
||||||
|
$clinical->upsert($organization, $visit, 'oncology', 'onc_staging', [
|
||||||
|
'diagnosis' => $highRisk ? 'Suspected metastatic breast carcinoma' : 'Breast carcinoma — adjuvant',
|
||||||
|
'stage' => $highRisk ? 'cT4 N2 M1' : 'pT2 N1 M0',
|
||||||
|
'histology' => 'Invasive ductal carcinoma',
|
||||||
|
'performance_status' => $highRisk ? '3' : '1',
|
||||||
|
'exam_findings' => $highRisk ? 'Cachexia; axillary nodes; hepatic tenderness' : 'Well; surgical scar healed',
|
||||||
|
'sites' => $highRisk ? 'Breast, axilla, liver' : 'None residual known',
|
||||||
|
], $ownerRef, $ownerRef, 'staging');
|
||||||
|
if ($highRisk) {
|
||||||
|
$clinical->upsert($organization, $visit, 'oncology', 'onc_plan', [
|
||||||
|
'diagnosis' => 'Metastatic breast carcinoma',
|
||||||
|
'plan' => 'Staging imaging; MDT; supportive care; consider palliative systemic therapy',
|
||||||
|
'intent' => 'Palliative',
|
||||||
|
'medications' => 'Analgesia; antiemetics PRN',
|
||||||
|
'treatment_planned' => true,
|
||||||
|
'follow_up' => 'Chemo day-care after MDT',
|
||||||
|
'advice' => 'Return if fever or uncontrolled pain',
|
||||||
|
], $ownerRef, $ownerRef, 'plan');
|
||||||
|
}
|
||||||
|
if (! $visit->specialty_stage) {
|
||||||
|
$visit->update(['specialty_stage' => $highRisk ? 'plan' : 'staging']);
|
||||||
|
}
|
||||||
|
$index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function seedRenalClinicalDemo(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', 'renal')
|
||||||
|
->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, 'renal', 'renal_history', [
|
||||||
|
'history' => $highRisk
|
||||||
|
? 'Missed last dialysis; progressive dyspnoea and oedema'
|
||||||
|
: 'Routine thrice-weekly haemodialysis review',
|
||||||
|
'ckd_stage' => $highRisk ? 'CKD 5D — 2 years on HD' : 'CKD 5D — 8 months on HD',
|
||||||
|
'access_history' => 'Left AV fistula',
|
||||||
|
'medications' => 'EPO; phosphate binders; antihypertensives',
|
||||||
|
'allergies' => 'NKDA',
|
||||||
|
'comorbidities' => $highRisk ? 'Diabetes; heart failure' : 'Hypertension',
|
||||||
|
], $ownerRef, $ownerRef, 'history');
|
||||||
|
$clinical->upsert($organization, $visit, 'renal', 'renal_exam', [
|
||||||
|
'chief_complaint' => $highRisk ? 'Fluid overload / missed dialysis' : 'Routine dialysis session',
|
||||||
|
'bp' => $highRisk ? '168/98' : '132/78',
|
||||||
|
'weight_kg' => $highRisk ? 78.5 : 65.2,
|
||||||
|
'fluid_status' => $highRisk ? 'Overload' : 'Euvolemic',
|
||||||
|
'access' => 'Left AVF thrills present',
|
||||||
|
'exam_findings' => $highRisk ? 'Bilateral basal crackles; sacral oedema' : 'Chest clear; no oedema',
|
||||||
|
'uremic_symptoms' => $highRisk ? 'Nausea; pruritus' : 'None',
|
||||||
|
], $ownerRef, $ownerRef, 'exam');
|
||||||
|
if ($highRisk) {
|
||||||
|
$clinical->upsert($organization, $visit, 'renal', 'renal_session', [
|
||||||
|
'session_type' => 'Hemodialysis',
|
||||||
|
'pre_weight_kg' => 78.5,
|
||||||
|
'uf_goal_l' => 3.5,
|
||||||
|
'access' => 'Left AVF',
|
||||||
|
'bp_pre' => '168/98',
|
||||||
|
'labs_summary' => 'K 5.8; Cr elevated; Hb 9.1',
|
||||||
|
'complications' => 'Hypotension mid-session anticipated',
|
||||||
|
'notes' => 'Urgent HD for fluid overload after missed session',
|
||||||
|
], $ownerRef, $ownerRef, 'dialysis');
|
||||||
|
}
|
||||||
|
if (! $visit->specialty_stage) {
|
||||||
|
$visit->update(['specialty_stage' => $highRisk ? 'dialysis' : 'exam']);
|
||||||
|
}
|
||||||
|
$index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function seedSurgeryClinicalDemo(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', 'surgery')
|
||||||
|
->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, 'surgery', 'surg_history', [
|
||||||
|
'history' => $highRisk
|
||||||
|
? 'Right inguinal hernia with intermittent pain; electing repair'
|
||||||
|
: 'Post-appendicectomy wound check',
|
||||||
|
'past_surgical' => $highRisk ? 'None' : 'Laparoscopic appendicectomy 10 days ago',
|
||||||
|
'medications' => $highRisk ? 'None' : 'Analgesia; antibiotics completed',
|
||||||
|
'allergies' => 'NKDA',
|
||||||
|
'comorbidities' => $highRisk ? 'Well-controlled diabetes' : 'None',
|
||||||
|
'anaesthesia_history' => 'No prior issues',
|
||||||
|
], $ownerRef, $ownerRef, 'history');
|
||||||
|
$clinical->upsert($organization, $visit, 'surgery', 'surg_exam', [
|
||||||
|
'chief_complaint' => $highRisk ? 'Inguinal hernia — pre-op' : 'Wound review',
|
||||||
|
'exam_findings' => $highRisk
|
||||||
|
? 'Right reducible inguinal hernia; no strangulation'
|
||||||
|
: 'Wound clean; mild erythema; no discharge',
|
||||||
|
'asa_class' => $highRisk ? 'II' : 'I',
|
||||||
|
'fitness' => 'Fit for day-case anaesthesia',
|
||||||
|
'site_marking' => $highRisk,
|
||||||
|
], $ownerRef, $ownerRef, 'exam');
|
||||||
|
if ($highRisk) {
|
||||||
|
$clinical->upsert($organization, $visit, 'surgery', 'surg_plan', [
|
||||||
|
'diagnosis' => 'Right inguinal hernia',
|
||||||
|
'proposed_procedure' => 'Open mesh hernia repair',
|
||||||
|
'plan' => 'Day-case repair; consent discussed including mesh risks',
|
||||||
|
'consent_obtained' => true,
|
||||||
|
'procedure_planned' => true,
|
||||||
|
'follow_up' => 'Wound check 7–10 days',
|
||||||
|
'advice' => 'NPO from midnight; bring meds list',
|
||||||
|
], $ownerRef, $ownerRef, 'plan');
|
||||||
|
}
|
||||||
|
if (! $visit->specialty_stage) {
|
||||||
|
$visit->update(['specialty_stage' => $highRisk ? 'plan' : 'exam']);
|
||||||
|
}
|
||||||
|
$index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private function seedDentistryClinicalDemo(Organization $organization, string $ownerRef): void
|
private function seedDentistryClinicalDemo(Organization $organization, string $ownerRef): void
|
||||||
{
|
{
|
||||||
// Specialty departments are provisioned without organization_id; scope via branches.
|
// Specialty departments are provisioned without organization_id; scope via branches.
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Care\Oncology;
|
||||||
|
|
||||||
|
use App\Models\Appointment;
|
||||||
|
use App\Models\Bill;
|
||||||
|
use App\Models\BillLineItem;
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Models\SpecialtyClinicalRecord;
|
||||||
|
use App\Models\Visit;
|
||||||
|
use App\Services\Care\SpecialtyShellService;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class OncologyAnalyticsService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected SpecialtyShellService $shell,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* arrivals_today: int,
|
||||||
|
* completed_today: int,
|
||||||
|
* chemo_open: int,
|
||||||
|
* diagnosis_breakdown: Collection,
|
||||||
|
* regimen_breakdown: Collection,
|
||||||
|
* avg_los_minutes: ?float,
|
||||||
|
* revenue_by_service: Collection,
|
||||||
|
* from: string,
|
||||||
|
* to: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public function report(
|
||||||
|
Organization $organization,
|
||||||
|
string $ownerRef,
|
||||||
|
?int $branchId = null,
|
||||||
|
?string $from = null,
|
||||||
|
?string $to = null,
|
||||||
|
): array {
|
||||||
|
$todayStart = now()->startOfDay();
|
||||||
|
$rangeFrom = $from ? Carbon::parse($from)->startOfDay() : now()->startOfMonth();
|
||||||
|
$rangeTo = $to ? Carbon::parse($to)->endOfDay() : now()->endOfDay();
|
||||||
|
|
||||||
|
$departmentIds = $this->shell->departmentIds($organization, 'oncology', $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');
|
||||||
|
|
||||||
|
$chemoOpen = SpecialtyClinicalRecord::owned($ownerRef)
|
||||||
|
->where('organization_id', $organization->id)
|
||||||
|
->where('module_key', 'oncology')
|
||||||
|
->where('record_type', 'onc_treatment')
|
||||||
|
->whereIn('visit_id', $openVisitIds->isEmpty() ? [0] : $openVisitIds)
|
||||||
|
->where('status', '!=', SpecialtyClinicalRecord::STATUS_COMPLETED)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$stagingRecords = SpecialtyClinicalRecord::owned($ownerRef)
|
||||||
|
->where('organization_id', $organization->id)
|
||||||
|
->where('module_key', 'oncology')
|
||||||
|
->where('record_type', 'onc_staging')
|
||||||
|
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
|
||||||
|
->whereBetween('recorded_at', [$rangeFrom, $rangeTo])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$diagnosisBreakdown = $stagingRecords
|
||||||
|
->groupBy(fn (SpecialtyClinicalRecord $r) => (string) ($r->payload['diagnosis'] ?? 'Unknown'))
|
||||||
|
->map(fn (Collection $rows, string $diagnosis) => [
|
||||||
|
'diagnosis' => $diagnosis,
|
||||||
|
'total' => $rows->count(),
|
||||||
|
])
|
||||||
|
->values()
|
||||||
|
->sortByDesc('total')
|
||||||
|
->values();
|
||||||
|
|
||||||
|
$treatmentRecords = SpecialtyClinicalRecord::owned($ownerRef)
|
||||||
|
->where('organization_id', $organization->id)
|
||||||
|
->where('module_key', 'oncology')
|
||||||
|
->where('record_type', 'onc_treatment')
|
||||||
|
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
|
||||||
|
->whereBetween('recorded_at', [$rangeFrom, $rangeTo])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$regimenBreakdown = $treatmentRecords
|
||||||
|
->groupBy(fn (SpecialtyClinicalRecord $r) => (string) ($r->payload['regimen'] ?? ($r->payload['treatment'] ?? 'Unknown')))
|
||||||
|
->map(fn (Collection $rows, string $regimen) => [
|
||||||
|
'regimen' => $regimen,
|
||||||
|
'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, 'oncology'))
|
||||||
|
->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,
|
||||||
|
'chemo_open' => $chemoOpen,
|
||||||
|
'diagnosis_breakdown' => $diagnosisBreakdown,
|
||||||
|
'regimen_breakdown' => $regimenBreakdown,
|
||||||
|
'avg_los_minutes' => $avgLos,
|
||||||
|
'revenue_by_service' => $revenueByService,
|
||||||
|
'from' => $rangeFrom->toDateString(),
|
||||||
|
'to' => $rangeTo->toDateString(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Care\Oncology;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map oncology clinical progress onto visit stages.
|
||||||
|
*/
|
||||||
|
class OncologyWorkflowService
|
||||||
|
{
|
||||||
|
public const STAGE_CHECK_IN = 'check_in';
|
||||||
|
|
||||||
|
public const STAGE_HISTORY = 'history';
|
||||||
|
|
||||||
|
public const STAGE_STAGING = 'staging';
|
||||||
|
|
||||||
|
public const STAGE_INVESTIGATION = 'investigation';
|
||||||
|
|
||||||
|
public const STAGE_PLAN = 'plan';
|
||||||
|
|
||||||
|
public const STAGE_TREATMENT = 'treatment';
|
||||||
|
|
||||||
|
public const STAGE_COMPLETED = 'completed';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $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<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
public function stageFromStaging(array $payload): string
|
||||||
|
{
|
||||||
|
if (! empty($payload['diagnosis'])
|
||||||
|
|| ! empty($payload['stage'])
|
||||||
|
|| ! empty($payload['exam_findings'])) {
|
||||||
|
return self::STAGE_STAGING;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::STAGE_HISTORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
public function stageFromInvestigation(array $payload): string
|
||||||
|
{
|
||||||
|
$findings = trim((string) ($payload['findings'] ?? ''));
|
||||||
|
if ($findings !== '') {
|
||||||
|
return self::STAGE_INVESTIGATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::STAGE_STAGING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
public function stageFromPlan(array $payload): string
|
||||||
|
{
|
||||||
|
if (! empty($payload['treatment_planned'])) {
|
||||||
|
return self::STAGE_TREATMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
$plan = trim((string) ($payload['plan'] ?? ''));
|
||||||
|
if ($plan !== '') {
|
||||||
|
return self::STAGE_PLAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::STAGE_INVESTIGATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
public function shouldCompleteVisit(array $payload): bool
|
||||||
|
{
|
||||||
|
$outcome = strtolower((string) ($payload['outcome'] ?? ''));
|
||||||
|
|
||||||
|
return str_contains($outcome, 'completed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array{next: string, label: string}>
|
||||||
|
*/
|
||||||
|
public function stageFlow(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::STAGE_CHECK_IN => ['next' => self::STAGE_HISTORY, 'label' => 'Start history'],
|
||||||
|
self::STAGE_HISTORY => ['next' => self::STAGE_STAGING, 'label' => 'Move to staging / exam'],
|
||||||
|
self::STAGE_STAGING => ['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_TREATMENT, 'label' => 'Move to treatment'],
|
||||||
|
self::STAGE_TREATMENT => ['next' => self::STAGE_COMPLETED, 'label' => 'Complete visit'],
|
||||||
|
self::STAGE_COMPLETED => ['next' => self::STAGE_COMPLETED, 'label' => 'Completed'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Care\Renal;
|
||||||
|
|
||||||
|
use App\Models\Appointment;
|
||||||
|
use App\Models\Bill;
|
||||||
|
use App\Models\BillLineItem;
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Models\SpecialtyClinicalRecord;
|
||||||
|
use App\Models\Visit;
|
||||||
|
use App\Services\Care\SpecialtyShellService;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class RenalAnalyticsService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected SpecialtyShellService $shell,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* arrivals_today: int,
|
||||||
|
* completed_today: int,
|
||||||
|
* dialysis_open: int,
|
||||||
|
* session_breakdown: Collection,
|
||||||
|
* diagnosis_breakdown: Collection,
|
||||||
|
* avg_los_minutes: ?float,
|
||||||
|
* revenue_by_service: Collection,
|
||||||
|
* from: string,
|
||||||
|
* to: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public function report(
|
||||||
|
Organization $organization,
|
||||||
|
string $ownerRef,
|
||||||
|
?int $branchId = null,
|
||||||
|
?string $from = null,
|
||||||
|
?string $to = null,
|
||||||
|
): array {
|
||||||
|
$todayStart = now()->startOfDay();
|
||||||
|
$rangeFrom = $from ? Carbon::parse($from)->startOfDay() : now()->startOfMonth();
|
||||||
|
$rangeTo = $to ? Carbon::parse($to)->endOfDay() : now()->endOfDay();
|
||||||
|
|
||||||
|
$departmentIds = $this->shell->departmentIds($organization, 'renal', $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');
|
||||||
|
|
||||||
|
$dialysisOpen = Visit::owned($ownerRef)
|
||||||
|
->where('organization_id', $organization->id)
|
||||||
|
->whereIn('id', $openVisitIds->isEmpty() ? [0] : $openVisitIds)
|
||||||
|
->where('specialty_stage', 'dialysis')
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$sessionRecords = SpecialtyClinicalRecord::owned($ownerRef)
|
||||||
|
->where('organization_id', $organization->id)
|
||||||
|
->where('module_key', 'renal')
|
||||||
|
->where('record_type', 'renal_session')
|
||||||
|
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
|
||||||
|
->whereBetween('recorded_at', [$rangeFrom, $rangeTo])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$sessionBreakdown = $sessionRecords
|
||||||
|
->groupBy(fn (SpecialtyClinicalRecord $r) => (string) ($r->payload['session_type'] ?? 'Unknown'))
|
||||||
|
->map(fn (Collection $rows, string $type) => [
|
||||||
|
'session_type' => $type,
|
||||||
|
'total' => $rows->count(),
|
||||||
|
])
|
||||||
|
->values()
|
||||||
|
->sortByDesc('total')
|
||||||
|
->values();
|
||||||
|
|
||||||
|
$planRecords = SpecialtyClinicalRecord::owned($ownerRef)
|
||||||
|
->where('organization_id', $organization->id)
|
||||||
|
->where('module_key', 'renal')
|
||||||
|
->where('record_type', 'renal_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();
|
||||||
|
|
||||||
|
$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, 'renal'))
|
||||||
|
->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,
|
||||||
|
'dialysis_open' => $dialysisOpen,
|
||||||
|
'session_breakdown' => $sessionBreakdown,
|
||||||
|
'diagnosis_breakdown' => $diagnosisBreakdown,
|
||||||
|
'avg_los_minutes' => $avgLos,
|
||||||
|
'revenue_by_service' => $revenueByService,
|
||||||
|
'from' => $rangeFrom->toDateString(),
|
||||||
|
'to' => $rangeTo->toDateString(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Care\Renal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map renal / nephrology clinical progress onto visit stages.
|
||||||
|
*/
|
||||||
|
class RenalWorkflowService
|
||||||
|
{
|
||||||
|
public const STAGE_CHECK_IN = 'check_in';
|
||||||
|
|
||||||
|
public const STAGE_HISTORY = 'history';
|
||||||
|
|
||||||
|
public const STAGE_EXAM = 'exam';
|
||||||
|
|
||||||
|
public const STAGE_DIALYSIS = 'dialysis';
|
||||||
|
|
||||||
|
public const STAGE_PLAN = 'plan';
|
||||||
|
|
||||||
|
public const STAGE_COMPLETED = 'completed';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $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<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
public function stageFromExam(array $payload): string
|
||||||
|
{
|
||||||
|
if (! empty($payload['chief_complaint'])
|
||||||
|
|| ! empty($payload['exam_findings'])
|
||||||
|
|| ! empty($payload['fluid_status'])) {
|
||||||
|
return self::STAGE_EXAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::STAGE_HISTORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
public function stageFromDialysis(array $payload): string
|
||||||
|
{
|
||||||
|
if (! empty($payload['session_type']) || ! empty($payload['notes'])) {
|
||||||
|
return self::STAGE_DIALYSIS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::STAGE_EXAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
public function stageFromPlan(array $payload): string
|
||||||
|
{
|
||||||
|
$plan = trim((string) ($payload['plan'] ?? ''));
|
||||||
|
if ($plan !== '') {
|
||||||
|
return self::STAGE_PLAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::STAGE_DIALYSIS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
public function shouldCompleteVisit(array $payload): bool
|
||||||
|
{
|
||||||
|
$outcome = strtolower((string) ($payload['outcome'] ?? ''));
|
||||||
|
|
||||||
|
return str_contains($outcome, 'completed') || str_contains($outcome, 'discharged');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array{next: string, label: string}>
|
||||||
|
*/
|
||||||
|
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'],
|
||||||
|
self::STAGE_EXAM => ['next' => self::STAGE_DIALYSIS, 'label' => 'Move to dialysis / labs'],
|
||||||
|
self::STAGE_DIALYSIS => ['next' => self::STAGE_PLAN, 'label' => 'Move to care plan'],
|
||||||
|
self::STAGE_PLAN => ['next' => self::STAGE_COMPLETED, 'label' => 'Complete visit'],
|
||||||
|
self::STAGE_COMPLETED => ['next' => self::STAGE_COMPLETED, 'label' => 'Completed'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -471,6 +471,39 @@ class SpecialtyClinicalRecordService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if ($moduleKey === 'oncology' && $recordType === 'onc_staging') {
|
||||||
|
$ecog = (string) ($payload['performance_status'] ?? '');
|
||||||
|
if (in_array($ecog, ['3', '4'], true)) {
|
||||||
|
$alerts[] = [
|
||||||
|
'code' => 'onc.ecog_high',
|
||||||
|
'severity' => 'warning',
|
||||||
|
'message' => 'High ECOG performance status ('.$ecog.') — review treatment fitness.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($moduleKey === 'renal' && $recordType === 'renal_exam') {
|
||||||
|
$fluid = strtolower((string) ($payload['fluid_status'] ?? ''));
|
||||||
|
if (str_contains($fluid, 'overload')) {
|
||||||
|
$alerts[] = [
|
||||||
|
'code' => 'ren.fluid_overload',
|
||||||
|
'severity' => 'warning',
|
||||||
|
'message' => 'Fluid overload recorded on renal exam.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($moduleKey === 'surgery' && $recordType === 'surg_plan') {
|
||||||
|
if (empty($payload['consent_obtained']) && ! empty($payload['procedure_planned'])) {
|
||||||
|
$alerts[] = [
|
||||||
|
'code' => 'sur.consent_missing',
|
||||||
|
'severity' => 'critical',
|
||||||
|
'message' => 'Procedure planned without documented consent.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($moduleKey === 'psychiatry' && $recordType === 'risk_assessment') {
|
if ($moduleKey === 'psychiatry' && $recordType === 'risk_assessment') {
|
||||||
$level = strtolower((string) ($payload['overall_risk'] ?? ''));
|
$level = strtolower((string) ($payload['overall_risk'] ?? ''));
|
||||||
if (str_contains($level, 'imminent')) {
|
if (str_contains($level, 'imminent')) {
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ use App\Services\Care\Psychiatry\PsychiatryWorkflowService;
|
|||||||
use App\Services\Care\Pediatrics\PediatricsWorkflowService;
|
use App\Services\Care\Pediatrics\PediatricsWorkflowService;
|
||||||
use App\Services\Care\Orthopedics\OrthopedicsWorkflowService;
|
use App\Services\Care\Orthopedics\OrthopedicsWorkflowService;
|
||||||
use App\Services\Care\Ent\EntWorkflowService;
|
use App\Services\Care\Ent\EntWorkflowService;
|
||||||
|
use App\Services\Care\Oncology\OncologyWorkflowService;
|
||||||
|
use App\Services\Care\Renal\RenalWorkflowService;
|
||||||
|
use App\Services\Care\Surgery\SurgeryWorkflowService;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,6 +75,9 @@ class SpecialtyShellService
|
|||||||
'pediatrics' => 'care.specialty.pediatrics.stage',
|
'pediatrics' => 'care.specialty.pediatrics.stage',
|
||||||
'orthopedics' => 'care.specialty.orthopedics.stage',
|
'orthopedics' => 'care.specialty.orthopedics.stage',
|
||||||
'ent' => 'care.specialty.ent.stage',
|
'ent' => 'care.specialty.ent.stage',
|
||||||
|
'oncology' => 'care.specialty.oncology.stage',
|
||||||
|
'renal' => 'care.specialty.renal.stage',
|
||||||
|
'surgery' => 'care.specialty.surgery.stage',
|
||||||
default => null,
|
default => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -95,6 +101,9 @@ class SpecialtyShellService
|
|||||||
'pediatrics' => app(PediatricsWorkflowService::class)->stageFlow(),
|
'pediatrics' => app(PediatricsWorkflowService::class)->stageFlow(),
|
||||||
'orthopedics' => app(OrthopedicsWorkflowService::class)->stageFlow(),
|
'orthopedics' => app(OrthopedicsWorkflowService::class)->stageFlow(),
|
||||||
'ent' => app(EntWorkflowService::class)->stageFlow(),
|
'ent' => app(EntWorkflowService::class)->stageFlow(),
|
||||||
|
'oncology' => app(OncologyWorkflowService::class)->stageFlow(),
|
||||||
|
'renal' => app(RenalWorkflowService::class)->stageFlow(),
|
||||||
|
'surgery' => app(SurgeryWorkflowService::class)->stageFlow(),
|
||||||
'dentistry' => [
|
'dentistry' => [
|
||||||
'waiting' => ['next' => 'chair', 'label' => 'Seat at chair'],
|
'waiting' => ['next' => 'chair', 'label' => 'Seat at chair'],
|
||||||
'chair' => ['next' => 'procedure', 'label' => 'Start procedure'],
|
'chair' => ['next' => 'procedure', 'label' => 'Start procedure'],
|
||||||
@@ -394,7 +403,7 @@ class SpecialtyShellService
|
|||||||
) {
|
) {
|
||||||
$code = (string) ($stage['code'] ?? '');
|
$code = (string) ($stage['code'] ?? '');
|
||||||
|
|
||||||
if (in_array($moduleKey, ['dentistry', 'emergency', 'blood_bank', 'ophthalmology', 'physiotherapy', 'maternity', 'radiology', 'cardiology', 'psychiatry', 'pediatrics', 'orthopedics', 'ent'], true) && $visitIds->isNotEmpty()) {
|
if (in_array($moduleKey, ['dentistry', 'emergency', 'blood_bank', 'ophthalmology', 'physiotherapy', 'maternity', 'radiology', 'cardiology', 'psychiatry', 'pediatrics', 'orthopedics', 'ent', 'oncology', 'renal', 'surgery'], true) && $visitIds->isNotEmpty()) {
|
||||||
$count = Visit::owned($ownerRef)
|
$count = Visit::owned($ownerRef)
|
||||||
->where('organization_id', $organization->id)
|
->where('organization_id', $organization->id)
|
||||||
->whereIn('id', $visitIds)
|
->whereIn('id', $visitIds)
|
||||||
@@ -433,6 +442,9 @@ class SpecialtyShellService
|
|||||||
'pediatrics' => $clinical->countOpenByType($organization, 'pediatrics', 'pediatric_exam', $branchScope),
|
'pediatrics' => $clinical->countOpenByType($organization, 'pediatrics', 'pediatric_exam', $branchScope),
|
||||||
'orthopedics' => $clinical->countOpenByType($organization, 'orthopedics', 'ortho_exam', $branchScope),
|
'orthopedics' => $clinical->countOpenByType($organization, 'orthopedics', 'ortho_exam', $branchScope),
|
||||||
'ent' => $clinical->countOpenByType($organization, 'ent', 'ent_exam', $branchScope),
|
'ent' => $clinical->countOpenByType($organization, 'ent', 'ent_exam', $branchScope),
|
||||||
|
'oncology' => $clinical->countOpenByType($organization, 'oncology', 'onc_staging', $branchScope),
|
||||||
|
'renal' => $clinical->countOpenByType($organization, 'renal', 'renal_exam', $branchScope),
|
||||||
|
'surgery' => $clinical->countOpenByType($organization, 'surgery', 'surg_exam', $branchScope),
|
||||||
'dentistry' => \App\Models\DentalPlanItem::query()
|
'dentistry' => \App\Models\DentalPlanItem::query()
|
||||||
->whereHas('plan', function ($q) use ($organization, $ownerRef) {
|
->whereHas('plan', function ($q) use ($organization, $ownerRef) {
|
||||||
$q->owned($ownerRef)
|
$q->owned($ownerRef)
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Care\Surgery;
|
||||||
|
|
||||||
|
use App\Models\Appointment;
|
||||||
|
use App\Models\Bill;
|
||||||
|
use App\Models\BillLineItem;
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Models\SpecialtyClinicalRecord;
|
||||||
|
use App\Models\Visit;
|
||||||
|
use App\Services\Care\SpecialtyShellService;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class SurgeryAnalyticsService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected SpecialtyShellService $shell,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* arrivals_today: int,
|
||||||
|
* completed_today: int,
|
||||||
|
* preop_open: int,
|
||||||
|
* diagnosis_breakdown: Collection,
|
||||||
|
* procedure_breakdown: Collection,
|
||||||
|
* avg_los_minutes: ?float,
|
||||||
|
* revenue_by_service: Collection,
|
||||||
|
* from: string,
|
||||||
|
* to: string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public function report(
|
||||||
|
Organization $organization,
|
||||||
|
string $ownerRef,
|
||||||
|
?int $branchId = null,
|
||||||
|
?string $from = null,
|
||||||
|
?string $to = null,
|
||||||
|
): array {
|
||||||
|
$todayStart = now()->startOfDay();
|
||||||
|
$rangeFrom = $from ? Carbon::parse($from)->startOfDay() : now()->startOfMonth();
|
||||||
|
$rangeTo = $to ? Carbon::parse($to)->endOfDay() : now()->endOfDay();
|
||||||
|
|
||||||
|
$departmentIds = $this->shell->departmentIds($organization, 'surgery', $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');
|
||||||
|
|
||||||
|
$preopOpen = Visit::owned($ownerRef)
|
||||||
|
->where('organization_id', $organization->id)
|
||||||
|
->whereIn('id', $openVisitIds->isEmpty() ? [0] : $openVisitIds)
|
||||||
|
->whereIn('specialty_stage', ['history', 'exam', 'investigation', 'plan'])
|
||||||
|
->count();
|
||||||
|
|
||||||
|
$planRecords = SpecialtyClinicalRecord::owned($ownerRef)
|
||||||
|
->where('organization_id', $organization->id)
|
||||||
|
->where('module_key', 'surgery')
|
||||||
|
->where('record_type', 'surg_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', 'surgery')
|
||||||
|
->where('record_type', 'surg_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, 'surgery'))
|
||||||
|
->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,
|
||||||
|
'preop_open' => $preopOpen,
|
||||||
|
'diagnosis_breakdown' => $diagnosisBreakdown,
|
||||||
|
'procedure_breakdown' => $procedureBreakdown,
|
||||||
|
'avg_los_minutes' => $avgLos,
|
||||||
|
'revenue_by_service' => $revenueByService,
|
||||||
|
'from' => $rangeFrom->toDateString(),
|
||||||
|
'to' => $rangeTo->toDateString(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Care\Surgery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map surgical OPD / pre-op clinic progress onto visit stages.
|
||||||
|
*/
|
||||||
|
class SurgeryWorkflowService
|
||||||
|
{
|
||||||
|
public const STAGE_CHECK_IN = 'check_in';
|
||||||
|
|
||||||
|
public const STAGE_HISTORY = 'history';
|
||||||
|
|
||||||
|
public const STAGE_EXAM = 'exam';
|
||||||
|
|
||||||
|
public const STAGE_INVESTIGATION = 'investigation';
|
||||||
|
|
||||||
|
public const STAGE_PLAN = 'plan';
|
||||||
|
|
||||||
|
public const STAGE_PROCEDURE = 'procedure';
|
||||||
|
|
||||||
|
public const STAGE_POSTOP = 'postop';
|
||||||
|
|
||||||
|
public const STAGE_COMPLETED = 'completed';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $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<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
public function stageFromExam(array $payload): string
|
||||||
|
{
|
||||||
|
if (! empty($payload['chief_complaint']) || ! empty($payload['exam_findings'])) {
|
||||||
|
return self::STAGE_EXAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::STAGE_HISTORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
public function stageFromInvestigation(array $payload): string
|
||||||
|
{
|
||||||
|
$findings = trim((string) ($payload['findings'] ?? ''));
|
||||||
|
if ($findings !== '') {
|
||||||
|
return self::STAGE_INVESTIGATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::STAGE_EXAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $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<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
public function stageFromProcedure(array $payload): string
|
||||||
|
{
|
||||||
|
if (! empty($payload['procedure']) || ! empty($payload['outcome'])) {
|
||||||
|
return self::STAGE_PROCEDURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::STAGE_PLAN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
public function shouldCompleteVisit(array $payload): bool
|
||||||
|
{
|
||||||
|
$outcome = strtolower((string) ($payload['outcome'] ?? ''));
|
||||||
|
|
||||||
|
return str_contains($outcome, 'completed') || str_contains($outcome, 'discharged');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, array{next: string, label: string}>
|
||||||
|
*/
|
||||||
|
public function stageFlow(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::STAGE_CHECK_IN => ['next' => self::STAGE_HISTORY, 'label' => 'Start pre-op history'],
|
||||||
|
self::STAGE_HISTORY => ['next' => self::STAGE_EXAM, 'label' => 'Move to exam'],
|
||||||
|
self::STAGE_EXAM => ['next' => self::STAGE_INVESTIGATION, 'label' => 'Move to investigations'],
|
||||||
|
self::STAGE_INVESTIGATION => ['next' => self::STAGE_PLAN, 'label' => 'Move to consent / plan'],
|
||||||
|
self::STAGE_PLAN => ['next' => self::STAGE_PROCEDURE, 'label' => 'Move to procedure'],
|
||||||
|
self::STAGE_PROCEDURE => ['next' => self::STAGE_POSTOP, 'label' => 'Move to post-op'],
|
||||||
|
self::STAGE_POSTOP => ['next' => self::STAGE_COMPLETED, 'label' => 'Complete visit'],
|
||||||
|
self::STAGE_COMPLETED => ['next' => self::STAGE_COMPLETED, 'label' => 'Completed'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -87,16 +87,25 @@ return [
|
|||||||
'procedure' => 'ent_procedure',
|
'procedure' => 'ent_procedure',
|
||||||
],
|
],
|
||||||
'oncology' => [
|
'oncology' => [
|
||||||
'review' => 'oncology_review',
|
'history' => 'onc_history',
|
||||||
'clinical_notes' => 'clinical_note',
|
'staging' => 'onc_staging',
|
||||||
|
'investigations' => 'onc_investigation',
|
||||||
|
'plan' => 'onc_plan',
|
||||||
|
'treatment' => 'onc_treatment',
|
||||||
],
|
],
|
||||||
'renal' => [
|
'renal' => [
|
||||||
'session' => 'renal_session',
|
'history' => 'renal_history',
|
||||||
'clinical_notes' => 'clinical_note',
|
'exam' => 'renal_exam',
|
||||||
|
'dialysis' => 'renal_session',
|
||||||
|
'plan' => 'renal_plan',
|
||||||
],
|
],
|
||||||
'surgery' => [
|
'surgery' => [
|
||||||
'preop' => 'preop',
|
'history' => 'surg_history',
|
||||||
'clinical_notes' => 'clinical_note',
|
'exam' => 'surg_exam',
|
||||||
|
'investigations' => 'surg_investigation',
|
||||||
|
'plan' => 'surg_plan',
|
||||||
|
'procedure' => 'surg_procedure',
|
||||||
|
'postop' => 'surg_postop',
|
||||||
],
|
],
|
||||||
'vaccination' => [
|
'vaccination' => [
|
||||||
'screening' => 'vax_screening',
|
'screening' => 'vax_screening',
|
||||||
@@ -620,58 +629,133 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
'oncology' => [
|
'oncology' => [
|
||||||
'oncology_review' => [
|
'onc_history' => [
|
||||||
|
['name' => 'history', 'label' => 'History of present illness', 'type' => 'textarea', 'required' => true, 'rows' => 4],
|
||||||
|
['name' => 'cancer_history', 'label' => 'Cancer / treatment history', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
['name' => 'medications', 'label' => 'Medications', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
['name' => 'allergies', 'label' => 'Allergies', 'type' => 'text'],
|
||||||
|
['name' => 'comorbidities', 'label' => 'Comorbidities', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
['name' => 'social', 'label' => 'Social / support context', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
],
|
||||||
|
'onc_staging' => [
|
||||||
['name' => 'diagnosis', 'label' => 'Oncology diagnosis', 'type' => 'text', 'required' => true],
|
['name' => 'diagnosis', 'label' => 'Oncology diagnosis', 'type' => 'text', 'required' => true],
|
||||||
['name' => 'stage', 'label' => 'Stage', 'type' => 'text'],
|
['name' => 'stage', 'label' => 'Stage / TNM', 'type' => 'text'],
|
||||||
|
['name' => 'histology', 'label' => 'Histology', 'type' => 'text'],
|
||||||
['name' => 'performance_status', 'label' => 'Performance status (ECOG)', 'type' => 'select', 'options' => ['0', '1', '2', '3', '4']],
|
['name' => 'performance_status', 'label' => 'Performance status (ECOG)', 'type' => 'select', 'options' => ['0', '1', '2', '3', '4']],
|
||||||
['name' => 'current_regimen', 'label' => 'Current regimen', 'type' => 'textarea', 'rows' => 2],
|
['name' => 'exam_findings', 'label' => 'Exam findings', 'type' => 'textarea', 'rows' => 3],
|
||||||
|
['name' => 'sites', 'label' => 'Disease sites', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
],
|
||||||
|
'onc_investigation' => [
|
||||||
|
['name' => 'investigation', 'label' => 'Investigation', 'type' => 'text', 'required' => true],
|
||||||
|
['name' => 'findings', 'label' => 'Findings', 'type' => 'textarea', 'required' => true, 'rows' => 3],
|
||||||
|
['name' => 'impression', 'label' => 'Impression', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
],
|
||||||
|
'onc_plan' => [
|
||||||
|
['name' => 'diagnosis', 'label' => 'Diagnosis', 'type' => 'text', 'required' => true],
|
||||||
|
['name' => 'plan', 'label' => 'Plan', 'type' => 'textarea', 'required' => true, 'rows' => 3],
|
||||||
|
['name' => 'intent', 'label' => 'Treatment intent', 'type' => 'select', 'options' => ['Curative', 'Adjuvant', 'Neoadjuvant', 'Palliative', 'Supportive']],
|
||||||
|
['name' => 'medications', 'label' => 'Medications', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
['name' => 'treatment_planned', 'label' => 'Treatment / chemo planned', 'type' => 'boolean'],
|
||||||
|
['name' => 'follow_up', 'label' => 'Follow-up', 'type' => 'text'],
|
||||||
|
['name' => 'advice', 'label' => 'Patient advice', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
],
|
||||||
|
'onc_treatment' => [
|
||||||
|
['name' => 'treatment', 'label' => 'Treatment / chemo note', 'type' => 'text', 'required' => true],
|
||||||
|
['name' => 'regimen', 'label' => 'Regimen', 'type' => 'text'],
|
||||||
['name' => 'cycle', 'label' => 'Cycle / day', 'type' => 'text'],
|
['name' => 'cycle', 'label' => 'Cycle / day', 'type' => 'text'],
|
||||||
['name' => 'toxicity', 'label' => 'Toxicity / side effects', 'type' => 'textarea', 'rows' => 2],
|
['name' => 'toxicity', 'label' => 'Toxicity / side effects', 'type' => 'textarea', 'rows' => 2],
|
||||||
['name' => 'response', 'label' => 'Treatment response', 'type' => 'textarea', 'rows' => 2],
|
['name' => 'outcome', 'label' => 'Outcome', 'type' => 'select', 'required' => true, 'options' => ['Completed uneventfully', 'Completed with complications', 'Deferred', 'Aborted']],
|
||||||
['name' => 'plan', 'label' => 'Plan', 'type' => 'textarea', 'required' => true, 'rows' => 2],
|
['name' => 'post_treatment_plan', 'label' => 'Post-treatment plan', 'type' => 'textarea', 'rows' => 2],
|
||||||
],
|
['name' => 'notes', 'label' => 'Notes', '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],
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'renal' => [
|
'renal' => [
|
||||||
|
'renal_history' => [
|
||||||
|
['name' => 'history', 'label' => 'History of present illness', 'type' => 'textarea', 'required' => true, 'rows' => 4],
|
||||||
|
['name' => 'ckd_stage', 'label' => 'CKD stage / dialysis vintage', 'type' => 'text'],
|
||||||
|
['name' => 'access_history', 'label' => 'Access history', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
['name' => 'medications', 'label' => 'Medications', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
['name' => 'allergies', 'label' => 'Allergies', 'type' => 'text'],
|
||||||
|
['name' => 'comorbidities', 'label' => 'Comorbidities', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
],
|
||||||
|
'renal_exam' => [
|
||||||
|
['name' => 'chief_complaint', 'label' => 'Chief complaint', 'type' => 'text', 'required' => true],
|
||||||
|
['name' => 'bp', 'label' => 'Blood pressure', 'type' => 'text'],
|
||||||
|
['name' => 'weight_kg', 'label' => 'Weight (kg)', 'type' => 'number'],
|
||||||
|
['name' => 'fluid_status', 'label' => 'Fluid status', 'type' => 'select', 'options' => ['Euvolemic', 'Overload', 'Dehydrated', 'Not assessed']],
|
||||||
|
['name' => 'access', 'label' => 'Access assessment', 'type' => 'text'],
|
||||||
|
['name' => 'exam_findings', 'label' => 'Exam findings', 'type' => 'textarea', 'rows' => 3],
|
||||||
|
['name' => 'uremic_symptoms', 'label' => 'Uremic symptoms', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
],
|
||||||
'renal_session' => [
|
'renal_session' => [
|
||||||
['name' => 'session_type', 'label' => 'Session type', 'type' => 'select', 'required' => true, 'options' => ['Hemodialysis', 'Peritoneal dialysis', 'Clinic review', 'Other']],
|
['name' => 'session_type', 'label' => 'Session / labs type', 'type' => 'select', 'required' => true, 'options' => ['Hemodialysis', 'Peritoneal dialysis', 'Clinic labs review', 'Other']],
|
||||||
['name' => 'pre_weight_kg', 'label' => 'Pre-weight (kg)', 'type' => 'number'],
|
['name' => 'pre_weight_kg', 'label' => 'Pre-weight (kg)', 'type' => 'number'],
|
||||||
['name' => 'post_weight_kg', 'label' => 'Post-weight (kg)', 'type' => 'number'],
|
['name' => 'post_weight_kg', 'label' => 'Post-weight (kg)', 'type' => 'number'],
|
||||||
['name' => 'uf_goal_l', 'label' => 'UF goal (L)', 'type' => 'number'],
|
['name' => 'uf_goal_l', 'label' => 'UF goal (L)', 'type' => 'number'],
|
||||||
['name' => 'access', 'label' => 'Access', 'type' => 'text'],
|
['name' => 'access', 'label' => 'Access', 'type' => 'text'],
|
||||||
['name' => 'bp_pre', 'label' => 'BP pre', 'type' => 'text'],
|
['name' => 'bp_pre', 'label' => 'BP pre', 'type' => 'text'],
|
||||||
['name' => 'bp_post', 'label' => 'BP post', 'type' => 'text'],
|
['name' => 'bp_post', 'label' => 'BP post', 'type' => 'text'],
|
||||||
|
['name' => 'labs_summary', 'label' => 'Labs summary', 'type' => 'textarea', 'rows' => 2],
|
||||||
['name' => 'complications', 'label' => 'Complications', 'type' => 'textarea', 'rows' => 2],
|
['name' => 'complications', 'label' => 'Complications', 'type' => 'textarea', 'rows' => 2],
|
||||||
['name' => 'notes', 'label' => 'Session notes', 'type' => 'textarea', 'required' => true, 'rows' => 3],
|
['name' => 'notes', 'label' => 'Session / labs notes', 'type' => 'textarea', 'required' => true, 'rows' => 3],
|
||||||
],
|
],
|
||||||
'clinical_note' => [
|
'renal_plan' => [
|
||||||
['name' => 'history', 'label' => 'History of present illness', 'type' => 'textarea', 'required' => true, 'rows' => 4],
|
['name' => 'diagnosis', 'label' => 'Diagnosis', 'type' => 'text', 'required' => true],
|
||||||
['name' => 'examination', 'label' => 'Examination', 'type' => 'textarea', 'rows' => 3],
|
['name' => 'plan', 'label' => 'Care plan', 'type' => 'textarea', 'required' => true, 'rows' => 3],
|
||||||
['name' => 'working_diagnosis', 'label' => 'Working diagnosis', 'type' => 'text'],
|
['name' => 'dialysis_prescription', 'label' => 'Dialysis prescription notes', 'type' => 'textarea', 'rows' => 2],
|
||||||
['name' => 'plan', 'label' => 'Plan', 'type' => 'textarea', 'rows' => 3],
|
['name' => 'medications', 'label' => 'Medications', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
['name' => 'follow_up', 'label' => 'Follow-up', 'type' => 'text'],
|
||||||
|
['name' => 'outcome', 'label' => 'Visit outcome', 'type' => 'select', 'required' => true, 'options' => ['In progress', 'Completed — continue plan', 'Completed — discharged', 'Referred', 'Admitted']],
|
||||||
|
['name' => 'advice', 'label' => 'Patient advice', 'type' => 'textarea', 'rows' => 2],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'surgery' => [
|
'surgery' => [
|
||||||
'preop' => [
|
'surg_history' => [
|
||||||
['name' => 'proposed_procedure', 'label' => 'Proposed procedure', 'type' => 'text', 'required' => true],
|
|
||||||
['name' => 'indication', 'label' => 'Indication', 'type' => 'textarea', 'required' => true, 'rows' => 2],
|
|
||||||
['name' => 'asa_class', 'label' => 'ASA class', 'type' => 'select', 'options' => ['I', 'II', 'III', 'IV', 'V', 'E']],
|
|
||||||
['name' => 'allergies_reviewed', 'label' => 'Allergies reviewed', 'type' => 'boolean'],
|
|
||||||
['name' => 'consent_obtained', 'label' => 'Consent obtained', 'type' => 'boolean'],
|
|
||||||
['name' => 'fitness', 'label' => 'Fitness for anaesthesia', 'type' => 'textarea', 'rows' => 2],
|
|
||||||
['name' => 'investigations', 'label' => 'Relevant investigations', 'type' => 'textarea', 'rows' => 2],
|
|
||||||
['name' => 'plan', 'label' => 'Peri-op plan', 'type' => 'textarea', 'required' => true, 'rows' => 2],
|
|
||||||
],
|
|
||||||
'clinical_note' => [
|
|
||||||
['name' => 'history', 'label' => 'History of present illness', 'type' => 'textarea', 'required' => true, 'rows' => 4],
|
['name' => 'history', 'label' => 'History of present illness', 'type' => 'textarea', 'required' => true, 'rows' => 4],
|
||||||
['name' => 'examination', 'label' => 'Examination', 'type' => 'textarea', 'rows' => 3],
|
['name' => 'past_surgical', 'label' => 'Past surgical history', 'type' => 'textarea', 'rows' => 2],
|
||||||
['name' => 'working_diagnosis', 'label' => 'Working diagnosis', 'type' => 'text'],
|
['name' => 'medications', 'label' => 'Medications', 'type' => 'textarea', 'rows' => 2],
|
||||||
['name' => 'plan', 'label' => 'Plan', 'type' => 'textarea', 'rows' => 3],
|
['name' => 'allergies', 'label' => 'Allergies', 'type' => 'text'],
|
||||||
|
['name' => 'comorbidities', 'label' => 'Comorbidities', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
['name' => 'anaesthesia_history', 'label' => 'Anaesthesia history', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
],
|
||||||
|
'surg_exam' => [
|
||||||
|
['name' => 'chief_complaint', 'label' => 'Chief complaint', 'type' => 'text', 'required' => true],
|
||||||
|
['name' => 'exam_findings', 'label' => 'Examination findings', 'type' => 'textarea', 'required' => true, 'rows' => 3],
|
||||||
|
['name' => 'asa_class', 'label' => 'ASA class', 'type' => 'select', 'options' => ['I', 'II', 'III', 'IV', 'V', 'E']],
|
||||||
|
['name' => 'fitness', 'label' => 'Fitness notes', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
['name' => 'site_marking', 'label' => 'Site marking reviewed', 'type' => 'boolean'],
|
||||||
|
],
|
||||||
|
'surg_investigation' => [
|
||||||
|
['name' => 'investigation', 'label' => 'Investigation', 'type' => 'text', 'required' => true],
|
||||||
|
['name' => 'findings', 'label' => 'Findings', 'type' => 'textarea', 'required' => true, 'rows' => 3],
|
||||||
|
['name' => 'impression', 'label' => 'Impression', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
],
|
||||||
|
'surg_plan' => [
|
||||||
|
['name' => 'diagnosis', 'label' => 'Diagnosis', 'type' => 'text', 'required' => true],
|
||||||
|
['name' => 'proposed_procedure', 'label' => 'Proposed procedure', 'type' => 'text'],
|
||||||
|
['name' => 'plan', 'label' => 'Consent / peri-op plan', 'type' => 'textarea', 'required' => true, 'rows' => 3],
|
||||||
|
['name' => 'consent_obtained', 'label' => 'Consent obtained', 'type' => 'boolean'],
|
||||||
|
['name' => 'procedure_planned', 'label' => 'Procedure planned today', 'type' => 'boolean'],
|
||||||
|
['name' => 'follow_up', 'label' => 'Follow-up', 'type' => 'text'],
|
||||||
|
['name' => 'advice', 'label' => 'Patient advice', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
],
|
||||||
|
'surg_procedure' => [
|
||||||
|
['name' => 'procedure', 'label' => 'Procedure', 'type' => 'text', 'required' => true],
|
||||||
|
['name' => 'indication', 'label' => 'Indication', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
['name' => 'technique', 'label' => 'Technique / findings', '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' => 'notes', 'label' => 'Notes', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
],
|
||||||
|
'surg_postop' => [
|
||||||
|
['name' => 'review_type', 'label' => 'Review type', 'type' => 'select', 'required' => true, 'options' => ['Immediate post-op', 'Clinic wound check', 'Follow-up review', 'Discharge review']],
|
||||||
|
['name' => 'progress', 'label' => 'Progress / wound status', 'type' => 'textarea', 'required' => true, 'rows' => 3],
|
||||||
|
['name' => 'pain_score', 'label' => 'Pain score (0–10)', 'type' => 'number'],
|
||||||
|
['name' => 'outcome', 'label' => 'Outcome', 'type' => 'select', 'required' => true, 'options' => ['In progress', 'Completed — continue plan', 'Completed — discharged', 'Referred', 'Admitted']],
|
||||||
|
['name' => 'next_steps', 'label' => 'Next steps', 'type' => 'textarea', 'rows' => 2],
|
||||||
|
['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea', 'rows' => 2],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
'vaccination' => [
|
'vaccination' => [
|
||||||
|
|||||||
@@ -513,62 +513,114 @@ return [
|
|||||||
],
|
],
|
||||||
'oncology' => [
|
'oncology' => [
|
||||||
'stages' => [
|
'stages' => [
|
||||||
['code' => 'waiting', 'label' => 'Waiting', 'queue_point' => 'waiting'],
|
['code' => 'check_in', 'label' => 'Check-in', 'queue_point' => 'waiting'],
|
||||||
['code' => 'review', 'label' => 'Oncology review', 'queue_point' => 'chair'],
|
['code' => 'history', 'label' => 'History', 'queue_point' => 'waiting'],
|
||||||
['code' => 'treatment', 'label' => 'Treatment', 'queue_point' => 'chair'],
|
['code' => 'staging', 'label' => 'Staging / exam', 'queue_point' => 'chair'],
|
||||||
|
['code' => 'investigation', 'label' => 'Investigations', 'queue_point' => 'chair'],
|
||||||
|
['code' => 'plan', 'label' => 'Diagnosis & plan', 'queue_point' => 'chair'],
|
||||||
|
['code' => 'treatment', 'label' => 'Treatment / chemo', 'queue_point' => 'chair'],
|
||||||
['code' => 'completed', 'label' => 'Completed', 'queue_point' => null],
|
['code' => 'completed', 'label' => 'Completed', 'queue_point' => null],
|
||||||
],
|
],
|
||||||
'services' => [
|
'services' => [
|
||||||
['code' => 'onc.consult', 'label' => 'Oncology consultation', 'amount_minor' => 10000, 'type' => 'consultation'],
|
['code' => 'onc.consult', 'label' => 'Oncology consultation', 'amount_minor' => 10000, 'type' => 'consultation'],
|
||||||
|
['code' => 'onc.staging', 'label' => 'Staging assessment', 'amount_minor' => 8000, 'type' => 'consultation'],
|
||||||
['code' => 'onc.chemo', 'label' => 'Chemo day-care session', 'amount_minor' => 50000, 'type' => 'procedure'],
|
['code' => 'onc.chemo', 'label' => 'Chemo day-care session', 'amount_minor' => 50000, 'type' => 'procedure'],
|
||||||
|
['code' => 'onc.supportive', 'label' => 'Supportive care visit', 'amount_minor' => 6000, 'type' => 'consultation'],
|
||||||
],
|
],
|
||||||
'workspace_tabs' => [
|
'workspace_tabs' => [
|
||||||
'overview' => 'Overview',
|
'overview' => 'Overview',
|
||||||
'review' => 'Oncology review',
|
'history' => 'History',
|
||||||
'clinical_notes' => 'Clinical notes',
|
'staging' => 'Staging / exam',
|
||||||
|
'investigations' => 'Investigations',
|
||||||
|
'plan' => 'Diagnosis & plan',
|
||||||
|
'treatment' => 'Treatment',
|
||||||
'orders' => 'Orders',
|
'orders' => 'Orders',
|
||||||
'billing' => 'Billing',
|
'billing' => 'Billing',
|
||||||
'documents' => 'Documents',
|
'documents' => 'Documents',
|
||||||
],
|
],
|
||||||
|
'stage_tabs' => [
|
||||||
|
'check_in' => 'overview',
|
||||||
|
'history' => 'history',
|
||||||
|
'staging' => 'staging',
|
||||||
|
'investigation' => 'investigations',
|
||||||
|
'plan' => 'plan',
|
||||||
|
'treatment' => 'treatment',
|
||||||
|
'completed' => 'overview',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'renal' => [
|
'renal' => [
|
||||||
'stages' => [
|
'stages' => [
|
||||||
['code' => 'waiting', 'label' => 'Waiting', 'queue_point' => 'waiting'],
|
['code' => 'check_in', 'label' => 'Check-in', 'queue_point' => 'waiting'],
|
||||||
['code' => 'dialysis', 'label' => 'Dialysis / clinic', 'queue_point' => 'chair'],
|
['code' => 'history', 'label' => 'History', 'queue_point' => 'waiting'],
|
||||||
|
['code' => 'exam', 'label' => 'Exam', 'queue_point' => 'chair'],
|
||||||
|
['code' => 'dialysis', 'label' => 'Dialysis / labs', 'queue_point' => 'chair'],
|
||||||
|
['code' => 'plan', 'label' => 'Care plan', 'queue_point' => 'chair'],
|
||||||
['code' => 'completed', 'label' => 'Completed', 'queue_point' => null],
|
['code' => 'completed', 'label' => 'Completed', 'queue_point' => null],
|
||||||
],
|
],
|
||||||
'services' => [
|
'services' => [
|
||||||
['code' => 'ren.consult', 'label' => 'Nephrology consultation', 'amount_minor' => 8000, 'type' => 'consultation'],
|
['code' => 'ren.consult', 'label' => 'Nephrology consultation', 'amount_minor' => 8000, 'type' => 'consultation'],
|
||||||
['code' => 'ren.dialysis', 'label' => 'Dialysis session', 'amount_minor' => 35000, 'type' => 'procedure'],
|
['code' => 'ren.dialysis', 'label' => 'Dialysis session', 'amount_minor' => 35000, 'type' => 'procedure'],
|
||||||
|
['code' => 'ren.labs', 'label' => 'Renal labs panel', 'amount_minor' => 6000, 'type' => 'lab'],
|
||||||
|
['code' => 'ren.review', 'label' => 'Clinic review', 'amount_minor' => 5000, 'type' => 'consultation'],
|
||||||
],
|
],
|
||||||
'workspace_tabs' => [
|
'workspace_tabs' => [
|
||||||
'overview' => 'Overview',
|
'overview' => 'Overview',
|
||||||
'session' => 'Renal session',
|
'history' => 'History',
|
||||||
'clinical_notes' => 'Clinical notes',
|
'exam' => 'Exam',
|
||||||
|
'dialysis' => 'Dialysis / labs',
|
||||||
|
'plan' => 'Care plan',
|
||||||
'orders' => 'Orders',
|
'orders' => 'Orders',
|
||||||
'billing' => 'Billing',
|
'billing' => 'Billing',
|
||||||
'documents' => 'Documents',
|
'documents' => 'Documents',
|
||||||
],
|
],
|
||||||
|
'stage_tabs' => [
|
||||||
|
'check_in' => 'overview',
|
||||||
|
'history' => 'history',
|
||||||
|
'exam' => 'exam',
|
||||||
|
'dialysis' => 'dialysis',
|
||||||
|
'plan' => 'plan',
|
||||||
|
'completed' => 'overview',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'surgery' => [
|
'surgery' => [
|
||||||
'stages' => [
|
'stages' => [
|
||||||
['code' => 'waiting', 'label' => 'Waiting', 'queue_point' => 'waiting'],
|
['code' => 'check_in', 'label' => 'Check-in', 'queue_point' => 'waiting'],
|
||||||
['code' => 'preop', 'label' => 'Pre-op assessment', 'queue_point' => 'chair'],
|
['code' => 'history', 'label' => 'Pre-op history', 'queue_point' => 'waiting'],
|
||||||
['code' => 'postop', 'label' => 'Post-op review', 'queue_point' => 'chair'],
|
['code' => 'exam', 'label' => 'Exam', 'queue_point' => 'chair'],
|
||||||
|
['code' => 'investigation', 'label' => 'Investigations', 'queue_point' => 'chair'],
|
||||||
|
['code' => 'plan', 'label' => 'Consent / plan', 'queue_point' => 'chair'],
|
||||||
|
['code' => 'procedure', 'label' => 'Procedure', 'queue_point' => 'chair'],
|
||||||
|
['code' => 'postop', 'label' => 'Post-op', 'queue_point' => 'chair'],
|
||||||
['code' => 'completed', 'label' => 'Completed', 'queue_point' => null],
|
['code' => 'completed', 'label' => 'Completed', 'queue_point' => null],
|
||||||
],
|
],
|
||||||
'services' => [
|
'services' => [
|
||||||
['code' => 'sur.consult', 'label' => 'Surgical consultation', 'amount_minor' => 8000, 'type' => 'consultation'],
|
['code' => 'sur.consult', 'label' => 'Surgical consultation', 'amount_minor' => 8000, 'type' => 'consultation'],
|
||||||
['code' => 'sur.preop', 'label' => 'Pre-op assessment', 'amount_minor' => 5000, 'type' => 'consultation'],
|
['code' => 'sur.preop', 'label' => 'Pre-op assessment', 'amount_minor' => 5000, 'type' => 'consultation'],
|
||||||
|
['code' => 'sur.procedure', 'label' => 'Minor procedure / clinic', 'amount_minor' => 25000, 'type' => 'procedure'],
|
||||||
|
['code' => 'sur.postop', 'label' => 'Post-op review', 'amount_minor' => 4500, 'type' => 'consultation'],
|
||||||
],
|
],
|
||||||
'workspace_tabs' => [
|
'workspace_tabs' => [
|
||||||
'overview' => 'Overview',
|
'overview' => 'Overview',
|
||||||
'preop' => 'Pre-op',
|
'history' => 'History',
|
||||||
'clinical_notes' => 'Clinical notes',
|
'exam' => 'Exam',
|
||||||
|
'investigations' => 'Investigations',
|
||||||
|
'plan' => 'Consent / plan',
|
||||||
|
'procedure' => 'Procedure',
|
||||||
|
'postop' => 'Post-op',
|
||||||
'orders' => 'Orders',
|
'orders' => 'Orders',
|
||||||
'billing' => 'Billing',
|
'billing' => 'Billing',
|
||||||
'documents' => 'Documents',
|
'documents' => 'Documents',
|
||||||
],
|
],
|
||||||
|
'stage_tabs' => [
|
||||||
|
'check_in' => 'overview',
|
||||||
|
'history' => 'history',
|
||||||
|
'exam' => 'exam',
|
||||||
|
'investigation' => 'investigations',
|
||||||
|
'plan' => 'plan',
|
||||||
|
'procedure' => 'procedure',
|
||||||
|
'postop' => 'postop',
|
||||||
|
'completed' => 'overview',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
'vaccination' => [
|
'vaccination' => [
|
||||||
'stages' => [
|
'stages' => [
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Oncology summary · {{ $patient->fullName() }}</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: system-ui, sans-serif; color: #0f172a; margin: 2rem; }
|
||||||
|
h1 { font-size: 1.25rem; margin: 0 0 .25rem; }
|
||||||
|
h2 { font-size: .95rem; margin: 1.25rem 0 .5rem; border-bottom: 1px solid #e2e8f0; padding-bottom: .25rem; }
|
||||||
|
.meta { color: #64748b; font-size: .875rem; }
|
||||||
|
dl { display: grid; grid-template-columns: 10rem 1fr; gap: .35rem .75rem; font-size: .875rem; }
|
||||||
|
dt { color: #64748b; }
|
||||||
|
dd { margin: 0; font-weight: 500; }
|
||||||
|
@media print { .no-print { display: none; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<p class="no-print"><a href="{{ route('care.specialty.workspace', ['module' => 'oncology', 'visit' => $visit]) }}">Back</a></p>
|
||||||
|
<h1>{{ $patient->fullName() }}</h1>
|
||||||
|
<p class="meta">{{ $patient->patient_number }} · Visit #{{ $visit->id }} · Stage {{ $visit->specialty_stage ?? '—' }}</p>
|
||||||
|
<h2>History</h2>
|
||||||
|
@if ($history)<dl><dt>HPI</dt><dd>{{ $history->payload['history'] ?? '—' }}</dd><dt>Cancer history</dt><dd>{{ $history->payload['cancer_history'] ?? '—' }}</dd></dl>@else<p class="meta">No history recorded.</p>@endif
|
||||||
|
<h2>Staging / exam</h2>
|
||||||
|
@if ($staging)<dl><dt>Diagnosis</dt><dd>{{ $staging->payload['diagnosis'] ?? '—' }}</dd><dt>Stage</dt><dd>{{ $staging->payload['stage'] ?? '—' }}</dd><dt>ECOG</dt><dd>{{ $staging->payload['performance_status'] ?? '—' }}</dd></dl>@else<p class="meta">No staging recorded.</p>@endif
|
||||||
|
<h2>Investigations</h2>
|
||||||
|
@if ($investigation)<dl><dt>Investigation</dt><dd>{{ $investigation->payload['investigation'] ?? '—' }}</dd><dt>Findings</dt><dd>{{ $investigation->payload['findings'] ?? '—' }}</dd></dl>@else<p class="meta">No investigation recorded.</p>@endif
|
||||||
|
<h2>Plan</h2>
|
||||||
|
@if ($plan)<dl><dt>Diagnosis</dt><dd>{{ $plan->payload['diagnosis'] ?? '—' }}</dd><dt>Plan</dt><dd>{{ $plan->payload['plan'] ?? '—' }}</dd></dl>@else<p class="meta">No plan recorded.</p>@endif
|
||||||
|
<h2>Treatment</h2>
|
||||||
|
@if ($treatment)<dl><dt>Treatment</dt><dd>{{ $treatment->payload['treatment'] ?? '—' }}</dd><dt>Outcome</dt><dd>{{ $treatment->payload['outcome'] ?? '—' }}</dd></dl>@else<p class="meta">No treatment recorded.</p>@endif
|
||||||
|
<script>window.print();</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
<x-app-layout title="Oncology reports">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-xl font-semibold text-slate-900">Oncology reports</h1>
|
||||||
|
<p class="mt-1 text-sm text-slate-500">Arrivals, open chemo notes, diagnoses, regimens, length of stay, and oncology revenue.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('care.specialty.show', 'oncology') }}" class="text-sm font-medium text-indigo-600">← Specialty home</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="GET" class="flex flex-wrap items-end gap-3 rounded-2xl border border-slate-200 bg-white p-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-slate-600">From</label>
|
||||||
|
<input type="date" name="from" value="{{ $report['from'] ?? '' }}" class="mt-1 rounded-xl border border-slate-200 px-3 py-2 text-sm">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-slate-600">To</label>
|
||||||
|
<input type="date" name="to" value="{{ $report['to'] ?? '' }}" class="mt-1 rounded-xl border border-slate-200 px-3 py-2 text-sm">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="rounded-xl bg-indigo-600 px-4 py-2 text-sm font-semibold text-white">Apply</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="grid gap-4 sm:grid-cols-4">
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white p-4">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Arrivals today</p>
|
||||||
|
<p class="mt-2 text-2xl font-semibold tabular-nums text-slate-900">{{ number_format($report['arrivals_today']) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white p-4">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Completed today</p>
|
||||||
|
<p class="mt-2 text-2xl font-semibold tabular-nums text-slate-900">{{ number_format($report['completed_today']) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white p-4">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Chemo notes open</p>
|
||||||
|
<p class="mt-2 text-2xl font-semibold tabular-nums text-rose-700">{{ number_format($report['chemo_open']) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white p-4">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Avg LOS (range)</p>
|
||||||
|
<p class="mt-2 text-2xl font-semibold tabular-nums text-slate-900">
|
||||||
|
{{ $report['avg_los_minutes'] !== null ? $report['avg_los_minutes'].' min' : '—' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid gap-4 lg:grid-cols-2">
|
||||||
|
<section class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||||
|
<h2 class="text-sm font-semibold text-slate-900">Diagnoses</h2>
|
||||||
|
<ul class="mt-3 space-y-2 text-sm">
|
||||||
|
@forelse ($report['diagnosis_breakdown'] as $row)
|
||||||
|
<li class="flex justify-between gap-3">
|
||||||
|
<span>{{ $row['diagnosis'] }}</span>
|
||||||
|
<span class="tabular-nums text-slate-600">{{ $row['total'] }}</span>
|
||||||
|
</li>
|
||||||
|
@empty
|
||||||
|
<li class="text-slate-500">No staging records in range.</li>
|
||||||
|
@endforelse
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||||
|
<h2 class="text-sm font-semibold text-slate-900">Regimens / treatments</h2>
|
||||||
|
<ul class="mt-3 space-y-2 text-sm">
|
||||||
|
@forelse ($report['regimen_breakdown'] as $row)
|
||||||
|
<li class="flex justify-between gap-3">
|
||||||
|
<span>{{ $row['regimen'] }}</span>
|
||||||
|
<span class="tabular-nums text-slate-600">{{ $row['total'] }}</span>
|
||||||
|
</li>
|
||||||
|
@empty
|
||||||
|
<li class="text-slate-500">No treatments in range.</li>
|
||||||
|
@endforelse
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||||
|
<h2 class="text-sm font-semibold text-slate-900">Oncology service revenue</h2>
|
||||||
|
<ul class="mt-3 space-y-2 text-sm">
|
||||||
|
@forelse ($report['revenue_by_service'] as $row)
|
||||||
|
<li class="flex justify-between gap-3">
|
||||||
|
<span>{{ $row->description }} <span class="text-slate-400">×{{ $row->total }}</span></span>
|
||||||
|
<span class="tabular-nums">{{ $currency }} {{ number_format($row->amount_minor / 100, 2) }}</span>
|
||||||
|
</li>
|
||||||
|
@empty
|
||||||
|
<li class="text-slate-500">No billed oncology services in range.</li>
|
||||||
|
@endforelse
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</x-app-layout>
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
@php
|
||||||
|
$hist = $oncologyHistory?->payload ?? [];
|
||||||
|
$staging = $oncologyStaging?->payload ?? [];
|
||||||
|
$plan = $oncologyPlan?->payload ?? [];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<section class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-slate-900">Oncology overview</h3>
|
||||||
|
<p class="mt-0.5 text-xs text-slate-500">History, staging, investigations, plan, and treatment for this episode.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-3 text-sm">
|
||||||
|
<a href="{{ route('care.specialty.oncology.print', $workspaceVisit) }}" target="_blank" class="font-medium text-indigo-600">Print summary</a>
|
||||||
|
<a href="{{ route('care.specialty.oncology.reports') }}" class="font-medium text-indigo-600">Oncology reports</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4 grid gap-3 sm:grid-cols-3">
|
||||||
|
<div class="rounded-xl border border-slate-100 bg-slate-50 px-3 py-3">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Diagnosis</p>
|
||||||
|
<p class="mt-1 text-sm font-semibold text-slate-900">{{ $staging['diagnosis'] ?? ($plan['diagnosis'] ?? '—') }}</p>
|
||||||
|
<p class="mt-1 text-xs text-slate-500">Stage {{ $staging['stage'] ?? '—' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-slate-100 bg-slate-50 px-3 py-3">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">ECOG</p>
|
||||||
|
<p class="mt-1 text-sm font-semibold text-slate-900">{{ $staging['performance_status'] ?? '—' }}</p>
|
||||||
|
<p class="mt-1 text-xs text-slate-500">{{ $plan['intent'] ?? '—' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-slate-100 bg-slate-50 px-3 py-3">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Plan</p>
|
||||||
|
<p class="mt-1 text-sm font-semibold text-slate-900">{{ \Illuminate\Support\Str::limit($plan['plan'] ?? 'Not set', 40) }}</p>
|
||||||
|
<p class="mt-1 text-xs text-slate-500">{{ $plan['follow_up'] ?? '—' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dl class="mt-4 grid gap-3 text-sm sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<dt class="text-slate-500">History</dt>
|
||||||
|
<dd class="font-medium text-slate-900">{{ $hist['history'] ?? '—' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-slate-500">Queue</dt>
|
||||||
|
<dd class="font-medium text-slate-900">
|
||||||
|
{{ $workspaceVisit->appointment?->queue_ticket_number ?? '—' }}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
@if (! empty($clinicalAlerts))
|
||||||
|
<div class="mt-4 space-y-1.5 border-t border-slate-100 pt-4">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Alerts</p>
|
||||||
|
@foreach ($clinicalAlerts as $alert)
|
||||||
|
<p class="text-sm {{ ($alert['severity'] ?? '') === 'critical' ? 'text-rose-700' : 'text-amber-800' }}">
|
||||||
|
{{ $alert['message'] ?? '' }}
|
||||||
|
</p>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
@php
|
||||||
|
$record = $oncologyTreatment;
|
||||||
|
$payload = $record?->payload ?? [];
|
||||||
|
$canManage = $canManageClinical ?? $canConsult ?? false;
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<section class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-slate-900">Treatment / chemo</h3>
|
||||||
|
<p class="mt-0.5 text-xs text-slate-500">Completed treatments can close the oncology visit.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('care.specialty.oncology.print', $workspaceVisit) }}" target="_blank" class="text-sm font-medium text-indigo-600">Print summary</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($canManage)
|
||||||
|
<form method="POST" action="{{ route('care.specialty.oncology.treatment', $workspaceVisit) }}" class="mt-4 space-y-4">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="tab" value="treatment">
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
@foreach ($clinicalFields ?? [] as $field)
|
||||||
|
@php
|
||||||
|
$name = $field['name'];
|
||||||
|
$value = old('payload.'.$name, $payload[$name] ?? '');
|
||||||
|
$type = $field['type'] ?? 'text';
|
||||||
|
@endphp
|
||||||
|
<div @class(['sm:col-span-2' => in_array($type, ['textarea'], true)])>
|
||||||
|
<label class="block text-xs font-medium text-slate-600">
|
||||||
|
{{ $field['label'] }}
|
||||||
|
@if (! empty($field['required'])) <span class="text-rose-600">*</span> @endif
|
||||||
|
</label>
|
||||||
|
@if ($type === 'textarea')
|
||||||
|
<textarea name="payload[{{ $name }}]" rows="{{ $field['rows'] ?? 3 }}" @required(! empty($field['required']))
|
||||||
|
class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ $value }}</textarea>
|
||||||
|
@elseif ($type === 'select')
|
||||||
|
<select name="payload[{{ $name }}]" @required(! empty($field['required']))
|
||||||
|
class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
|
||||||
|
<option value="">Select…</option>
|
||||||
|
@foreach ($field['options'] ?? [] as $option)
|
||||||
|
<option value="{{ $option }}" @selected((string) $value === (string) $option)>{{ $option }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@elseif ($type === 'boolean')
|
||||||
|
<label class="mt-2 inline-flex items-center gap-2 text-sm text-slate-700">
|
||||||
|
<input type="checkbox" name="payload[{{ $name }}]" value="1" @checked(old('payload.'.$name, ! empty($payload[$name])))>
|
||||||
|
Yes
|
||||||
|
</label>
|
||||||
|
@else
|
||||||
|
<input type="{{ $type === 'number' ? 'number' : 'text' }}" name="payload[{{ $name }}]" value="{{ $value }}"
|
||||||
|
@required(! empty($field['required']))
|
||||||
|
class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-primary">Save treatment</button>
|
||||||
|
</form>
|
||||||
|
@elseif ($record)
|
||||||
|
<dl class="mt-4 grid gap-3 text-sm sm:grid-cols-2">
|
||||||
|
@foreach ($clinicalFields ?? [] as $field)
|
||||||
|
<div>
|
||||||
|
<dt class="text-slate-500">{{ $field['label'] }}</dt>
|
||||||
|
<dd class="font-medium text-slate-900">{{ $payload[$field['name']] ?? '—' }}</dd>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</dl>
|
||||||
|
@else
|
||||||
|
<p class="mt-4 text-sm text-slate-500">No treatment recorded yet.</p>
|
||||||
|
@endif
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en"><head><meta charset="utf-8"><title>Renal summary · {{ $patient->fullName() }}</title>
|
||||||
|
<style>body{font-family:system-ui,sans-serif;color:#0f172a;margin:2rem}h1{font-size:1.25rem}h2{font-size:.95rem;margin:1.25rem 0 .5rem;border-bottom:1px solid #e2e8f0;padding-bottom:.25rem}.meta{color:#64748b;font-size:.875rem}dl{display:grid;grid-template-columns:10rem 1fr;gap:.35rem .75rem;font-size:.875rem}dt{color:#64748b}dd{margin:0;font-weight:500}@media print{.no-print{display:none}}</style></head>
|
||||||
|
<body>
|
||||||
|
<p class="no-print"><a href="{{ route('care.specialty.workspace', ['module' => 'renal', 'visit' => $visit]) }}">Back</a></p>
|
||||||
|
<h1>{{ $patient->fullName() }}</h1>
|
||||||
|
<p class="meta">{{ $patient->patient_number }} · Visit #{{ $visit->id }} · Stage {{ $visit->specialty_stage ?? '—' }}</p>
|
||||||
|
<h2>History</h2>@if($history)<dl><dt>HPI</dt><dd>{{ $history->payload['history'] ?? '—' }}</dd><dt>CKD</dt><dd>{{ $history->payload['ckd_stage'] ?? '—' }}</dd></dl>@else<p class="meta">No history.</p>@endif
|
||||||
|
<h2>Exam</h2>@if($exam)<dl><dt>Complaint</dt><dd>{{ $exam->payload['chief_complaint'] ?? '—' }}</dd><dt>Fluid</dt><dd>{{ $exam->payload['fluid_status'] ?? '—' }}</dd></dl>@else<p class="meta">No exam.</p>@endif
|
||||||
|
<h2>Dialysis / labs</h2>@if($dialysis)<dl><dt>Type</dt><dd>{{ $dialysis->payload['session_type'] ?? '—' }}</dd><dt>Notes</dt><dd>{{ $dialysis->payload['notes'] ?? '—' }}</dd></dl>@else<p class="meta">No session.</p>@endif
|
||||||
|
<h2>Plan</h2>@if($plan)<dl><dt>Diagnosis</dt><dd>{{ $plan->payload['diagnosis'] ?? '—' }}</dd><dt>Plan</dt><dd>{{ $plan->payload['plan'] ?? '—' }}</dd><dt>Outcome</dt><dd>{{ $plan->payload['outcome'] ?? '—' }}</dd></dl>@else<p class="meta">No plan.</p>@endif
|
||||||
|
<script>window.print();</script></body></html>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<x-app-layout title="Renal Care reports">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-xl font-semibold text-slate-900">Renal Care reports</h1>
|
||||||
|
<p class="mt-1 text-sm text-slate-500">Arrivals, dialysis sessions, diagnoses, length of stay, and renal revenue.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('care.specialty.show', 'renal') }}" class="text-sm font-medium text-indigo-600">← Specialty home</a>
|
||||||
|
</div>
|
||||||
|
<form method="GET" class="flex flex-wrap items-end gap-3 rounded-2xl border border-slate-200 bg-white p-4">
|
||||||
|
<div><label class="block text-xs font-medium text-slate-600">From</label><input type="date" name="from" value="{{ $report['from'] ?? '' }}" class="mt-1 rounded-xl border border-slate-200 px-3 py-2 text-sm"></div>
|
||||||
|
<div><label class="block text-xs font-medium text-slate-600">To</label><input type="date" name="to" value="{{ $report['to'] ?? '' }}" class="mt-1 rounded-xl border border-slate-200 px-3 py-2 text-sm"></div>
|
||||||
|
<button type="submit" class="rounded-xl bg-indigo-600 px-4 py-2 text-sm font-semibold text-white">Apply</button>
|
||||||
|
</form>
|
||||||
|
<div class="grid gap-4 sm:grid-cols-4">
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white p-4"><p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Arrivals today</p><p class="mt-2 text-2xl font-semibold tabular-nums text-slate-900">{{ number_format($report['arrivals_today']) }}</p></div>
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white p-4"><p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Completed today</p><p class="mt-2 text-2xl font-semibold tabular-nums text-slate-900">{{ number_format($report['completed_today']) }}</p></div>
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white p-4"><p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Dialysis stage open</p><p class="mt-2 text-2xl font-semibold tabular-nums text-rose-700">{{ number_format($report['dialysis_open']) }}</p></div>
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white p-4"><p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Avg LOS</p><p class="mt-2 text-2xl font-semibold tabular-nums text-slate-900">{{ $report['avg_los_minutes'] !== null ? $report['avg_los_minutes'].' min' : '—' }}</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-4 lg:grid-cols-2">
|
||||||
|
<section class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||||
|
<h2 class="text-sm font-semibold text-slate-900">Session types</h2>
|
||||||
|
<ul class="mt-3 space-y-2 text-sm">
|
||||||
|
@forelse ($report['session_breakdown'] as $row)
|
||||||
|
<li class="flex justify-between gap-3"><span>{{ $row['session_type'] }}</span><span class="tabular-nums text-slate-600">{{ $row['total'] }}</span></li>
|
||||||
|
@empty<li class="text-slate-500">No sessions in range.</li>@endforelse
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||||
|
<h2 class="text-sm font-semibold text-slate-900">Diagnoses</h2>
|
||||||
|
<ul class="mt-3 space-y-2 text-sm">
|
||||||
|
@forelse ($report['diagnosis_breakdown'] as $row)
|
||||||
|
<li class="flex justify-between gap-3"><span>{{ $row['diagnosis'] }}</span><span class="tabular-nums text-slate-600">{{ $row['total'] }}</span></li>
|
||||||
|
@empty<li class="text-slate-500">No care plans in range.</li>@endforelse
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<section class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||||
|
<h2 class="text-sm font-semibold text-slate-900">Renal service revenue</h2>
|
||||||
|
<ul class="mt-3 space-y-2 text-sm">
|
||||||
|
@forelse ($report['revenue_by_service'] as $row)
|
||||||
|
<li class="flex justify-between gap-3"><span>{{ $row->description }} <span class="text-slate-400">×{{ $row->total }}</span></span><span class="tabular-nums">{{ $currency }} {{ number_format($row->amount_minor / 100, 2) }}</span></li>
|
||||||
|
@empty<li class="text-slate-500">No billed renal services in range.</li>@endforelse
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</x-app-layout>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
@php
|
||||||
|
$hist = $renalHistory?->payload ?? [];
|
||||||
|
$exam = $renalExam?->payload ?? [];
|
||||||
|
$plan = $renalPlan?->payload ?? [];
|
||||||
|
@endphp
|
||||||
|
<section class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-slate-900">Renal Care overview</h3>
|
||||||
|
<p class="mt-0.5 text-xs text-slate-500">History, exam, dialysis/labs, and care plan for this episode.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-3 text-sm">
|
||||||
|
<a href="{{ route('care.specialty.renal.print', $workspaceVisit) }}" target="_blank" class="font-medium text-indigo-600">Print summary</a>
|
||||||
|
<a href="{{ route('care.specialty.renal.reports') }}" class="font-medium text-indigo-600">Renal reports</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 grid gap-3 sm:grid-cols-3">
|
||||||
|
<div class="rounded-xl border border-slate-100 bg-slate-50 px-3 py-3">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Complaint</p>
|
||||||
|
<p class="mt-1 text-sm font-semibold text-slate-900">{{ $exam['chief_complaint'] ?? '—' }}</p>
|
||||||
|
<p class="mt-1 text-xs text-slate-500">Fluid {{ $exam['fluid_status'] ?? '—' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-slate-100 bg-slate-50 px-3 py-3">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">CKD / access</p>
|
||||||
|
<p class="mt-1 text-sm font-semibold text-slate-900">{{ $hist['ckd_stage'] ?? '—' }}</p>
|
||||||
|
<p class="mt-1 text-xs text-slate-500">{{ $exam['access'] ?? '—' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-slate-100 bg-slate-50 px-3 py-3">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Diagnosis / plan</p>
|
||||||
|
<p class="mt-1 text-sm font-semibold text-slate-900">{{ $plan['diagnosis'] ?? 'Not set' }}</p>
|
||||||
|
<p class="mt-1 text-xs text-slate-500">{{ $plan['follow_up'] ?? '—' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<dl class="mt-4 grid gap-3 text-sm sm:grid-cols-2">
|
||||||
|
<div><dt class="text-slate-500">History</dt><dd class="font-medium text-slate-900">{{ $hist['history'] ?? '—' }}</dd></div>
|
||||||
|
<div><dt class="text-slate-500">Plan</dt><dd class="font-medium text-slate-900">{{ $plan['plan'] ?? '—' }}</dd></div>
|
||||||
|
</dl>
|
||||||
|
@if (! empty($clinicalAlerts))
|
||||||
|
<div class="mt-4 space-y-1.5 border-t border-slate-100 pt-4">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Alerts</p>
|
||||||
|
@foreach ($clinicalAlerts as $alert)
|
||||||
|
<p class="text-sm {{ ($alert['severity'] ?? '') === 'critical' ? 'text-rose-700' : 'text-amber-800' }}">{{ $alert['message'] ?? '' }}</p>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
@php
|
||||||
|
$record = $renalPlan;
|
||||||
|
$payload = $record?->payload ?? [];
|
||||||
|
$canManage = $canManageClinical ?? $canConsult ?? false;
|
||||||
|
@endphp
|
||||||
|
<section class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-slate-900">Care plan</h3>
|
||||||
|
<p class="mt-0.5 text-xs text-slate-500">Completed / discharged outcomes can close the renal visit.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('care.specialty.renal.print', $workspaceVisit) }}" target="_blank" class="text-sm font-medium text-indigo-600">Print summary</a>
|
||||||
|
</div>
|
||||||
|
@if ($canManage)
|
||||||
|
<form method="POST" action="{{ route('care.specialty.renal.plan', $workspaceVisit) }}" class="mt-4 space-y-4">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="tab" value="plan">
|
||||||
|
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
@foreach ($clinicalFields ?? [] as $field)
|
||||||
|
@php
|
||||||
|
$name = $field['name'];
|
||||||
|
$value = old('payload.'.$name, $payload[$name] ?? '');
|
||||||
|
$type = $field['type'] ?? 'text';
|
||||||
|
@endphp
|
||||||
|
<div @class(['sm:col-span-2' => in_array($type, ['textarea'], true)])>
|
||||||
|
<label class="block text-xs font-medium text-slate-600">
|
||||||
|
{{ $field['label'] }}
|
||||||
|
@if (! empty($field['required'])) <span class="text-rose-600">*</span> @endif
|
||||||
|
</label>
|
||||||
|
@if ($type === 'textarea')
|
||||||
|
<textarea name="payload[{{ $name }}]" rows="{{ $field['rows'] ?? 3 }}" @required(! empty($field['required']))
|
||||||
|
class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ $value }}</textarea>
|
||||||
|
@elseif ($type === 'select')
|
||||||
|
<select name="payload[{{ $name }}]" @required(! empty($field['required']))
|
||||||
|
class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
|
||||||
|
<option value="">Select…</option>
|
||||||
|
@foreach ($field['options'] ?? [] as $option)
|
||||||
|
<option value="{{ $option }}" @selected((string) $value === (string) $option)>{{ $option }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@elseif ($type === 'boolean')
|
||||||
|
<label class="mt-2 inline-flex items-center gap-2 text-sm text-slate-700">
|
||||||
|
<input type="checkbox" name="payload[{{ $name }}]" value="1" @checked(old('payload.'.$name, ! empty($payload[$name])))>
|
||||||
|
Yes
|
||||||
|
</label>
|
||||||
|
@else
|
||||||
|
<input type="{{ $type === 'number' ? 'number' : 'text' }}" name="payload[{{ $name }}]" value="{{ $value }}"
|
||||||
|
@required(! empty($field['required']))
|
||||||
|
class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn-primary">Save care plan</button>
|
||||||
|
</form>
|
||||||
|
@elseif ($record)
|
||||||
|
<dl class="mt-4 grid gap-3 text-sm sm:grid-cols-2">
|
||||||
|
@foreach ($clinicalFields ?? [] as $field)
|
||||||
|
<div><dt class="text-slate-500">{{ $field['label'] }}</dt><dd class="font-medium text-slate-900">{{ $payload[$field['name']] ?? '—' }}</dd></div>
|
||||||
|
@endforeach
|
||||||
|
</dl>
|
||||||
|
@else
|
||||||
|
<p class="mt-4 text-sm text-slate-500">No care plan recorded yet.</p>
|
||||||
|
@endif
|
||||||
|
</section>
|
||||||
@@ -79,6 +79,12 @@
|
|||||||
@include('care.specialty.orthopedics.workspace-'.$activeTab)
|
@include('care.specialty.orthopedics.workspace-'.$activeTab)
|
||||||
@elseif ($moduleKey === 'ent' && in_array($activeTab, ['overview', 'procedure'], true))
|
@elseif ($moduleKey === 'ent' && in_array($activeTab, ['overview', 'procedure'], true))
|
||||||
@include('care.specialty.ent.workspace-'.$activeTab)
|
@include('care.specialty.ent.workspace-'.$activeTab)
|
||||||
|
@elseif ($moduleKey === 'oncology' && in_array($activeTab, ['overview', 'treatment'], true))
|
||||||
|
@include('care.specialty.oncology.workspace-'.$activeTab)
|
||||||
|
@elseif ($moduleKey === 'renal' && in_array($activeTab, ['overview', 'plan'], true))
|
||||||
|
@include('care.specialty.renal.workspace-'.$activeTab)
|
||||||
|
@elseif ($moduleKey === 'surgery' && in_array($activeTab, ['overview', 'postop'], true))
|
||||||
|
@include('care.specialty.surgery.workspace-'.$activeTab)
|
||||||
@elseif ($activeTab === 'billing')
|
@elseif ($activeTab === 'billing')
|
||||||
<section class="rounded-2xl border border-slate-200 bg-white p-5">
|
<section class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||||
<h3 class="text-sm font-semibold text-slate-900">
|
<h3 class="text-sm font-semibold text-slate-900">
|
||||||
|
|||||||
@@ -61,6 +61,15 @@
|
|||||||
@if ($moduleKey === 'ent')
|
@if ($moduleKey === 'ent')
|
||||||
<a href="{{ route('care.specialty.ent.reports') }}" class="text-slate-600 hover:text-slate-900">Reports</a>
|
<a href="{{ route('care.specialty.ent.reports') }}" class="text-slate-600 hover:text-slate-900">Reports</a>
|
||||||
@endif
|
@endif
|
||||||
|
@if ($moduleKey === 'oncology')
|
||||||
|
<a href="{{ route('care.specialty.oncology.reports') }}" class="text-slate-600 hover:text-slate-900">Reports</a>
|
||||||
|
@endif
|
||||||
|
@if ($moduleKey === 'renal')
|
||||||
|
<a href="{{ route('care.specialty.renal.reports') }}" class="text-slate-600 hover:text-slate-900">Reports</a>
|
||||||
|
@endif
|
||||||
|
@if ($moduleKey === 'surgery')
|
||||||
|
<a href="{{ route('care.specialty.surgery.reports') }}" class="text-slate-600 hover:text-slate-900">Reports</a>
|
||||||
|
@endif
|
||||||
<a href="{{ route('care.specialty.history', $moduleKey) }}" class="text-slate-600 hover:text-slate-900">History</a>
|
<a href="{{ route('care.specialty.history', $moduleKey) }}" class="text-slate-600 hover:text-slate-900">History</a>
|
||||||
@if ($canManageClinical ?? $canManageSpecialty ?? true)
|
@if ($canManageClinical ?? $canManageSpecialty ?? true)
|
||||||
<a href="{{ route('care.specialty.billing', $moduleKey) }}" class="text-slate-600 hover:text-slate-900">Billing</a>
|
<a href="{{ route('care.specialty.billing', $moduleKey) }}" class="text-slate-600 hover:text-slate-900">Billing</a>
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en"><head><meta charset="utf-8"><title>Surgery summary · {{ $patient->fullName() }}</title>
|
||||||
|
<style>body{font-family:system-ui,sans-serif;color:#0f172a;margin:2rem}h1{font-size:1.25rem}h2{font-size:.95rem;margin:1.25rem 0 .5rem;border-bottom:1px solid #e2e8f0;padding-bottom:.25rem}.meta{color:#64748b;font-size:.875rem}dl{display:grid;grid-template-columns:10rem 1fr;gap:.35rem .75rem;font-size:.875rem}dt{color:#64748b}dd{margin:0;font-weight:500}@media print{.no-print{display:none}}</style></head>
|
||||||
|
<body>
|
||||||
|
<p class="no-print"><a href="{{ route('care.specialty.workspace', ['module' => 'surgery', 'visit' => $visit]) }}">Back</a></p>
|
||||||
|
<h1>{{ $patient->fullName() }}</h1>
|
||||||
|
<p class="meta">{{ $patient->patient_number }} · Visit #{{ $visit->id }} · Stage {{ $visit->specialty_stage ?? '—' }}</p>
|
||||||
|
<h2>History</h2>@if($history)<dl><dt>HPI</dt><dd>{{ $history->payload['history'] ?? '—' }}</dd></dl>@else<p class="meta">No history.</p>@endif
|
||||||
|
<h2>Exam</h2>@if($exam)<dl><dt>Complaint</dt><dd>{{ $exam->payload['chief_complaint'] ?? '—' }}</dd><dt>ASA</dt><dd>{{ $exam->payload['asa_class'] ?? '—' }}</dd></dl>@else<p class="meta">No exam.</p>@endif
|
||||||
|
<h2>Investigations</h2>@if($investigation)<dl><dt>Investigation</dt><dd>{{ $investigation->payload['investigation'] ?? '—' }}</dd><dt>Findings</dt><dd>{{ $investigation->payload['findings'] ?? '—' }}</dd></dl>@else<p class="meta">No investigation.</p>@endif
|
||||||
|
<h2>Consent / plan</h2>@if($plan)<dl><dt>Diagnosis</dt><dd>{{ $plan->payload['diagnosis'] ?? '—' }}</dd><dt>Procedure</dt><dd>{{ $plan->payload['proposed_procedure'] ?? '—' }}</dd><dt>Consent</dt><dd>{{ !empty($plan->payload['consent_obtained']) ? 'Yes' : 'No' }}</dd></dl>@else<p class="meta">No plan.</p>@endif
|
||||||
|
<h2>Procedure</h2>@if($procedure)<dl><dt>Procedure</dt><dd>{{ $procedure->payload['procedure'] ?? '—' }}</dd><dt>Outcome</dt><dd>{{ $procedure->payload['outcome'] ?? '—' }}</dd></dl>@else<p class="meta">No procedure.</p>@endif
|
||||||
|
<h2>Post-op</h2>@if($postop)<dl><dt>Progress</dt><dd>{{ $postop->payload['progress'] ?? '—' }}</dd><dt>Outcome</dt><dd>{{ $postop->payload['outcome'] ?? '—' }}</dd></dl>@else<p class="meta">No post-op.</p>@endif
|
||||||
|
<script>window.print();</script></body></html>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<x-app-layout title="Surgery reports">
|
||||||
|
<div class="space-y-6">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-xl font-semibold text-slate-900">Surgery reports</h1>
|
||||||
|
<p class="mt-1 text-sm text-slate-500">Arrivals, pre-op pipeline, diagnoses, procedures, length of stay, and surgery revenue.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('care.specialty.show', 'surgery') }}" class="text-sm font-medium text-indigo-600">← Specialty home</a>
|
||||||
|
</div>
|
||||||
|
<form method="GET" class="flex flex-wrap items-end gap-3 rounded-2xl border border-slate-200 bg-white p-4">
|
||||||
|
<div><label class="block text-xs font-medium text-slate-600">From</label><input type="date" name="from" value="{{ $report['from'] ?? '' }}" class="mt-1 rounded-xl border border-slate-200 px-3 py-2 text-sm"></div>
|
||||||
|
<div><label class="block text-xs font-medium text-slate-600">To</label><input type="date" name="to" value="{{ $report['to'] ?? '' }}" class="mt-1 rounded-xl border border-slate-200 px-3 py-2 text-sm"></div>
|
||||||
|
<button type="submit" class="rounded-xl bg-indigo-600 px-4 py-2 text-sm font-semibold text-white">Apply</button>
|
||||||
|
</form>
|
||||||
|
<div class="grid gap-4 sm:grid-cols-4">
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white p-4"><p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Arrivals today</p><p class="mt-2 text-2xl font-semibold tabular-nums text-slate-900">{{ number_format($report['arrivals_today']) }}</p></div>
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white p-4"><p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Completed today</p><p class="mt-2 text-2xl font-semibold tabular-nums text-slate-900">{{ number_format($report['completed_today']) }}</p></div>
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white p-4"><p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Pre-op pipeline</p><p class="mt-2 text-2xl font-semibold tabular-nums text-rose-700">{{ number_format($report['preop_open']) }}</p></div>
|
||||||
|
<div class="rounded-2xl border border-slate-200 bg-white p-4"><p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Avg LOS</p><p class="mt-2 text-2xl font-semibold tabular-nums text-slate-900">{{ $report['avg_los_minutes'] !== null ? $report['avg_los_minutes'].' min' : '—' }}</p></div>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-4 lg:grid-cols-2">
|
||||||
|
<section class="rounded-2xl border border-slate-200 bg-white p-5"><h2 class="text-sm font-semibold text-slate-900">Diagnoses</h2>
|
||||||
|
<ul class="mt-3 space-y-2 text-sm">@forelse ($report['diagnosis_breakdown'] as $row)<li class="flex justify-between gap-3"><span>{{ $row['diagnosis'] }}</span><span class="tabular-nums text-slate-600">{{ $row['total'] }}</span></li>@empty<li class="text-slate-500">No plans in range.</li>@endforelse</ul>
|
||||||
|
</section>
|
||||||
|
<section class="rounded-2xl border border-slate-200 bg-white p-5"><h2 class="text-sm font-semibold text-slate-900">Procedures</h2>
|
||||||
|
<ul class="mt-3 space-y-2 text-sm">@forelse ($report['procedure_breakdown'] as $row)<li class="flex justify-between gap-3"><span>{{ $row['procedure'] }}</span><span class="tabular-nums text-slate-600">{{ $row['total'] }}</span></li>@empty<li class="text-slate-500">No procedures in range.</li>@endforelse</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<section class="rounded-2xl border border-slate-200 bg-white p-5"><h2 class="text-sm font-semibold text-slate-900">Surgery service revenue</h2>
|
||||||
|
<ul class="mt-3 space-y-2 text-sm">@forelse ($report['revenue_by_service'] as $row)<li class="flex justify-between gap-3"><span>{{ $row->description }} <span class="text-slate-400">×{{ $row->total }}</span></span><span class="tabular-nums">{{ $currency }} {{ number_format($row->amount_minor / 100, 2) }}</span></li>@empty<li class="text-slate-500">No billed surgery services in range.</li>@endforelse</ul>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</x-app-layout>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
@php
|
||||||
|
$hist = $surgeryHistory?->payload ?? [];
|
||||||
|
$exam = $surgeryExam?->payload ?? [];
|
||||||
|
$plan = $surgeryPlan?->payload ?? [];
|
||||||
|
@endphp
|
||||||
|
<section class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-slate-900">Surgery overview</h3>
|
||||||
|
<p class="mt-0.5 text-xs text-slate-500">Pre-op history, exam, investigations, consent/plan, procedure, and post-op.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-3 text-sm">
|
||||||
|
<a href="{{ route('care.specialty.surgery.print', $workspaceVisit) }}" target="_blank" class="font-medium text-indigo-600">Print summary</a>
|
||||||
|
<a href="{{ route('care.specialty.surgery.reports') }}" class="font-medium text-indigo-600">Surgery reports</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 grid gap-3 sm:grid-cols-3">
|
||||||
|
<div class="rounded-xl border border-slate-100 bg-slate-50 px-3 py-3">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Complaint</p>
|
||||||
|
<p class="mt-1 text-sm font-semibold text-slate-900">{{ $exam['chief_complaint'] ?? '—' }}</p>
|
||||||
|
<p class="mt-1 text-xs text-slate-500">ASA {{ $exam['asa_class'] ?? '—' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-slate-100 bg-slate-50 px-3 py-3">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Proposed procedure</p>
|
||||||
|
<p class="mt-1 text-sm font-semibold text-slate-900">{{ $plan['proposed_procedure'] ?? '—' }}</p>
|
||||||
|
<p class="mt-1 text-xs text-slate-500">{{ ! empty($plan['consent_obtained']) ? 'Consent obtained' : 'Consent pending' }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl border border-slate-100 bg-slate-50 px-3 py-3">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Diagnosis / plan</p>
|
||||||
|
<p class="mt-1 text-sm font-semibold text-slate-900">{{ $plan['diagnosis'] ?? 'Not set' }}</p>
|
||||||
|
<p class="mt-1 text-xs text-slate-500">{{ $plan['follow_up'] ?? '—' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<dl class="mt-4 grid gap-3 text-sm sm:grid-cols-2">
|
||||||
|
<div><dt class="text-slate-500">History</dt><dd class="font-medium text-slate-900">{{ $hist['history'] ?? '—' }}</dd></div>
|
||||||
|
<div><dt class="text-slate-500">Plan</dt><dd class="font-medium text-slate-900">{{ $plan['plan'] ?? '—' }}</dd></div>
|
||||||
|
</dl>
|
||||||
|
@if (! empty($clinicalAlerts))
|
||||||
|
<div class="mt-4 space-y-1.5 border-t border-slate-100 pt-4">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500">Alerts</p>
|
||||||
|
@foreach ($clinicalAlerts as $alert)
|
||||||
|
<p class="text-sm {{ ($alert['severity'] ?? '') === 'critical' ? 'text-rose-700' : 'text-amber-800' }}">{{ $alert['message'] ?? '' }}</p>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
@php
|
||||||
|
$record = $surgeryPostop;
|
||||||
|
$payload = $record?->payload ?? [];
|
||||||
|
$canManage = $canManageClinical ?? $canConsult ?? false;
|
||||||
|
@endphp
|
||||||
|
<section class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-slate-900">Post-op</h3>
|
||||||
|
<p class="mt-0.5 text-xs text-slate-500">Completed / discharged outcomes can close the surgery visit.</p>
|
||||||
|
</div>
|
||||||
|
<a href="{{ route('care.specialty.surgery.print', $workspaceVisit) }}" target="_blank" class="text-sm font-medium text-indigo-600">Print summary</a>
|
||||||
|
</div>
|
||||||
|
@if ($canManage)
|
||||||
|
<form method="POST" action="{{ route('care.specialty.surgery.postop', $workspaceVisit) }}" class="mt-4 space-y-4">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="tab" value="postop">
|
||||||
|
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2">
|
||||||
|
@foreach ($clinicalFields ?? [] as $field)
|
||||||
|
@php
|
||||||
|
$name = $field['name'];
|
||||||
|
$value = old('payload.'.$name, $payload[$name] ?? '');
|
||||||
|
$type = $field['type'] ?? 'text';
|
||||||
|
@endphp
|
||||||
|
<div @class(['sm:col-span-2' => in_array($type, ['textarea'], true)])>
|
||||||
|
<label class="block text-xs font-medium text-slate-600">
|
||||||
|
{{ $field['label'] }}
|
||||||
|
@if (! empty($field['required'])) <span class="text-rose-600">*</span> @endif
|
||||||
|
</label>
|
||||||
|
@if ($type === 'textarea')
|
||||||
|
<textarea name="payload[{{ $name }}]" rows="{{ $field['rows'] ?? 3 }}" @required(! empty($field['required']))
|
||||||
|
class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">{{ $value }}</textarea>
|
||||||
|
@elseif ($type === 'select')
|
||||||
|
<select name="payload[{{ $name }}]" @required(! empty($field['required']))
|
||||||
|
class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
|
||||||
|
<option value="">Select…</option>
|
||||||
|
@foreach ($field['options'] ?? [] as $option)
|
||||||
|
<option value="{{ $option }}" @selected((string) $value === (string) $option)>{{ $option }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
@elseif ($type === 'boolean')
|
||||||
|
<label class="mt-2 inline-flex items-center gap-2 text-sm text-slate-700">
|
||||||
|
<input type="checkbox" name="payload[{{ $name }}]" value="1" @checked(old('payload.'.$name, ! empty($payload[$name])))>
|
||||||
|
Yes
|
||||||
|
</label>
|
||||||
|
@else
|
||||||
|
<input type="{{ $type === 'number' ? 'number' : 'text' }}" name="payload[{{ $name }}]" value="{{ $value }}"
|
||||||
|
@required(! empty($field['required']))
|
||||||
|
class="mt-1 w-full rounded-xl border border-slate-200 px-3 py-2 text-sm">
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn-primary">Save post-op</button>
|
||||||
|
</form>
|
||||||
|
@elseif ($record)
|
||||||
|
<dl class="mt-4 grid gap-3 text-sm sm:grid-cols-2">
|
||||||
|
@foreach ($clinicalFields ?? [] as $field)
|
||||||
|
<div><dt class="text-slate-500">{{ $field['label'] }}</dt><dd class="font-medium text-slate-900">{{ $payload[$field['name']] ?? '—' }}</dd></div>
|
||||||
|
@endforeach
|
||||||
|
</dl>
|
||||||
|
@else
|
||||||
|
<p class="mt-4 text-sm text-slate-500">No post-op note recorded yet.</p>
|
||||||
|
@endif
|
||||||
|
</section>
|
||||||
@@ -50,6 +50,9 @@ use App\Http\Controllers\Care\PsychiatryWorkspaceController;
|
|||||||
use App\Http\Controllers\Care\PediatricsWorkspaceController;
|
use App\Http\Controllers\Care\PediatricsWorkspaceController;
|
||||||
use App\Http\Controllers\Care\OrthopedicsWorkspaceController;
|
use App\Http\Controllers\Care\OrthopedicsWorkspaceController;
|
||||||
use App\Http\Controllers\Care\EntWorkspaceController;
|
use App\Http\Controllers\Care\EntWorkspaceController;
|
||||||
|
use App\Http\Controllers\Care\OncologyWorkspaceController;
|
||||||
|
use App\Http\Controllers\Care\RenalWorkspaceController;
|
||||||
|
use App\Http\Controllers\Care\SurgeryWorkspaceController;
|
||||||
use App\Http\Controllers\Care\SpecialtyModuleController;
|
use App\Http\Controllers\Care\SpecialtyModuleController;
|
||||||
use App\Http\Controllers\Care\VisitWorkflowController;
|
use App\Http\Controllers\Care\VisitWorkflowController;
|
||||||
use App\Http\Controllers\NotificationController;
|
use App\Http\Controllers\NotificationController;
|
||||||
@@ -141,6 +144,9 @@ Route::middleware(['auth', 'platform.session'])->group(function () {
|
|||||||
Route::get('/specialty/pediatrics/reports', [PediatricsWorkspaceController::class, 'reports'])->name('care.specialty.pediatrics.reports');
|
Route::get('/specialty/pediatrics/reports', [PediatricsWorkspaceController::class, 'reports'])->name('care.specialty.pediatrics.reports');
|
||||||
Route::get('/specialty/orthopedics/reports', [OrthopedicsWorkspaceController::class, 'reports'])->name('care.specialty.orthopedics.reports');
|
Route::get('/specialty/orthopedics/reports', [OrthopedicsWorkspaceController::class, 'reports'])->name('care.specialty.orthopedics.reports');
|
||||||
Route::get('/specialty/ent/reports', [EntWorkspaceController::class, 'reports'])->name('care.specialty.ent.reports');
|
Route::get('/specialty/ent/reports', [EntWorkspaceController::class, 'reports'])->name('care.specialty.ent.reports');
|
||||||
|
Route::get('/specialty/oncology/reports', [OncologyWorkspaceController::class, 'reports'])->name('care.specialty.oncology.reports');
|
||||||
|
Route::get('/specialty/renal/reports', [RenalWorkspaceController::class, 'reports'])->name('care.specialty.renal.reports');
|
||||||
|
Route::get('/specialty/surgery/reports', [SurgeryWorkspaceController::class, 'reports'])->name('care.specialty.surgery.reports');
|
||||||
Route::get('/specialty/{module}/workspace/{visit?}', [SpecialtyModuleController::class, 'workspace'])->name('care.specialty.workspace');
|
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}/clinical', [SpecialtyModuleController::class, 'saveClinical'])->name('care.specialty.clinical.save');
|
||||||
Route::post('/specialty/{module}/workspace/{visit}/documents', [SpecialtyModuleController::class, 'uploadDocument'])->name('care.specialty.documents.upload');
|
Route::post('/specialty/{module}/workspace/{visit}/documents', [SpecialtyModuleController::class, 'uploadDocument'])->name('care.specialty.documents.upload');
|
||||||
@@ -201,6 +207,15 @@ Route::middleware(['auth', 'platform.session'])->group(function () {
|
|||||||
Route::post('/specialty/ent/workspace/{visit}/stage', [EntWorkspaceController::class, 'setStage'])->name('care.specialty.ent.stage');
|
Route::post('/specialty/ent/workspace/{visit}/stage', [EntWorkspaceController::class, 'setStage'])->name('care.specialty.ent.stage');
|
||||||
Route::post('/specialty/ent/workspace/{visit}/procedure', [EntWorkspaceController::class, 'saveProcedure'])->name('care.specialty.ent.procedure');
|
Route::post('/specialty/ent/workspace/{visit}/procedure', [EntWorkspaceController::class, 'saveProcedure'])->name('care.specialty.ent.procedure');
|
||||||
Route::get('/specialty/ent/workspace/{visit}/print', [EntWorkspaceController::class, 'printSummary'])->name('care.specialty.ent.print');
|
Route::get('/specialty/ent/workspace/{visit}/print', [EntWorkspaceController::class, 'printSummary'])->name('care.specialty.ent.print');
|
||||||
|
Route::post('/specialty/oncology/workspace/{visit}/stage', [OncologyWorkspaceController::class, 'setStage'])->name('care.specialty.oncology.stage');
|
||||||
|
Route::post('/specialty/oncology/workspace/{visit}/treatment', [OncologyWorkspaceController::class, 'saveTreatment'])->name('care.specialty.oncology.treatment');
|
||||||
|
Route::get('/specialty/oncology/workspace/{visit}/print', [OncologyWorkspaceController::class, 'printSummary'])->name('care.specialty.oncology.print');
|
||||||
|
Route::post('/specialty/renal/workspace/{visit}/stage', [RenalWorkspaceController::class, 'setStage'])->name('care.specialty.renal.stage');
|
||||||
|
Route::post('/specialty/renal/workspace/{visit}/plan', [RenalWorkspaceController::class, 'savePlan'])->name('care.specialty.renal.plan');
|
||||||
|
Route::get('/specialty/renal/workspace/{visit}/print', [RenalWorkspaceController::class, 'printSummary'])->name('care.specialty.renal.print');
|
||||||
|
Route::post('/specialty/surgery/workspace/{visit}/stage', [SurgeryWorkspaceController::class, 'setStage'])->name('care.specialty.surgery.stage');
|
||||||
|
Route::post('/specialty/surgery/workspace/{visit}/postop', [SurgeryWorkspaceController::class, 'savePostop'])->name('care.specialty.surgery.postop');
|
||||||
|
Route::get('/specialty/surgery/workspace/{visit}/print', [SurgeryWorkspaceController::class, 'printSummary'])->name('care.specialty.surgery.print');
|
||||||
Route::get('/specialty/{module}', [SpecialtyModuleController::class, 'show'])->name('care.specialty.show');
|
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}/call-next', [SpecialtyModuleController::class, 'callNext'])->name('care.specialty.call-next');
|
||||||
Route::post('/specialty/{module}/refer', [SpecialtyModuleController::class, 'refer'])->name('care.specialty.refer');
|
Route::post('/specialty/{module}/refer', [SpecialtyModuleController::class, 'refer'])->name('care.specialty.refer');
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Http\Middleware\EnsurePlatformSession;
|
||||||
|
use App\Models\Appointment;
|
||||||
|
use App\Models\Branch;
|
||||||
|
use App\Models\Department;
|
||||||
|
use App\Models\Member;
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Models\Patient;
|
||||||
|
use App\Models\SpecialtyClinicalRecord;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Visit;
|
||||||
|
use App\Services\Care\SpecialtyModuleService;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class CareOncologySuiteTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected User $owner;
|
||||||
|
|
||||||
|
protected Organization $organization;
|
||||||
|
|
||||||
|
protected Branch $branch;
|
||||||
|
|
||||||
|
protected Patient $patient;
|
||||||
|
|
||||||
|
protected Visit $visit;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||||
|
|
||||||
|
$this->owner = User::create([
|
||||||
|
'public_id' => 'onc-owner',
|
||||||
|
'name' => 'Owner',
|
||||||
|
'email' => 'onc-owner@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->organization = Organization::create([
|
||||||
|
'owner_ref' => $this->owner->public_id,
|
||||||
|
'name' => 'Oncology Clinic',
|
||||||
|
'slug' => 'onc-owner-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,
|
||||||
|
'oncology',
|
||||||
|
);
|
||||||
|
|
||||||
|
$department = Department::query()
|
||||||
|
->where('branch_id', $this->branch->id)
|
||||||
|
->where('type', 'oncology')
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$this->patient = Patient::create([
|
||||||
|
'owner_ref' => $this->owner->public_id,
|
||||||
|
'organization_id' => $this->organization->id,
|
||||||
|
'branch_id' => $this->branch->id,
|
||||||
|
'patient_number' => 'P-ONC-SUITE',
|
||||||
|
'first_name' => 'Ama',
|
||||||
|
'last_name' => 'Cancer',
|
||||||
|
'gender' => 'female',
|
||||||
|
'date_of_birth' => '1988-09-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_clinical_tabs_render(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->get(route('care.specialty.workspace', [
|
||||||
|
'module' => 'oncology',
|
||||||
|
'visit' => $this->visit,
|
||||||
|
'tab' => 'overview',
|
||||||
|
]))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Oncology overview')
|
||||||
|
->assertSee('data-care-stage-bar', false)
|
||||||
|
->assertSee('Start history')
|
||||||
|
->assertSee('Oncology reports');
|
||||||
|
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->get(route('care.specialty.workspace', [
|
||||||
|
'module' => 'oncology',
|
||||||
|
'visit' => $this->visit,
|
||||||
|
'tab' => 'staging',
|
||||||
|
]))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Oncology diagnosis');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_clinical_save_sets_stage_and_alerts(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->post(route('care.specialty.clinical.save', [
|
||||||
|
'module' => 'oncology',
|
||||||
|
'visit' => $this->visit,
|
||||||
|
]), [
|
||||||
|
'tab' => 'staging',
|
||||||
|
'payload' => [
|
||||||
|
'diagnosis' => 'Breast carcinoma',
|
||||||
|
'stage' => 'IIA',
|
||||||
|
'performance_status' => '3',
|
||||||
|
'exam_findings' => 'Palpable mass',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertRedirect();
|
||||||
|
|
||||||
|
$this->assertSame('staging', $this->visit->fresh()->specialty_stage);
|
||||||
|
|
||||||
|
$record = SpecialtyClinicalRecord::query()
|
||||||
|
->where('visit_id', $this->visit->id)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$codes = collect($record->alerts)->pluck('code')->all();
|
||||||
|
$this->assertContains('onc.ecog_high', $codes);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_stage_advance_and_terminal_completes_visit(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->post(route('care.specialty.oncology.stage', $this->visit), [
|
||||||
|
'stage' => 'treatment',
|
||||||
|
])
|
||||||
|
->assertRedirect(route('care.specialty.workspace', [
|
||||||
|
'module' => 'oncology',
|
||||||
|
'visit' => $this->visit,
|
||||||
|
'tab' => 'treatment',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->assertSame('treatment', $this->visit->fresh()->specialty_stage);
|
||||||
|
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->post(route('care.specialty.oncology.treatment', $this->visit), [
|
||||||
|
'tab' => 'treatment',
|
||||||
|
'payload' => [
|
||||||
|
'treatment' => 'AC cycle 1 day 1',
|
||||||
|
'regimen' => 'AC',
|
||||||
|
'cycle' => '1/4',
|
||||||
|
'outcome' => 'Completed uneventfully',
|
||||||
|
'post_treatment_plan' => 'Return cycle 2 in 21 days',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertRedirect(route('care.specialty.workspace', [
|
||||||
|
'module' => 'oncology',
|
||||||
|
'visit' => $this->visit,
|
||||||
|
'tab' => 'treatment',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$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' => 'onc_treatment',
|
||||||
|
'status' => SpecialtyClinicalRecord::STATUS_COMPLETED,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_reports_and_print(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->get(route('care.specialty.oncology.reports'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Oncology reports')
|
||||||
|
->assertSee('Arrivals today');
|
||||||
|
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->get(route('care.specialty.oncology.print', $this->visit))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee($this->patient->fullName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_index_does_not_auto_open_patient(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->get(route('care.specialty.show', 'oncology'))
|
||||||
|
->assertOk()
|
||||||
|
->assertDontSee('Oncology overview');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Http\Middleware\EnsurePlatformSession;
|
||||||
|
use App\Models\Appointment;
|
||||||
|
use App\Models\Branch;
|
||||||
|
use App\Models\Department;
|
||||||
|
use App\Models\Member;
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Models\Patient;
|
||||||
|
use App\Models\SpecialtyClinicalRecord;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Visit;
|
||||||
|
use App\Services\Care\SpecialtyModuleService;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class CareRenalSuiteTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected User $owner;
|
||||||
|
|
||||||
|
protected Organization $organization;
|
||||||
|
|
||||||
|
protected Branch $branch;
|
||||||
|
|
||||||
|
protected Patient $patient;
|
||||||
|
|
||||||
|
protected Visit $visit;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||||
|
|
||||||
|
$this->owner = User::create([
|
||||||
|
'public_id' => 'renal-owner',
|
||||||
|
'name' => 'Owner',
|
||||||
|
'email' => 'renal-owner@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->organization = Organization::create([
|
||||||
|
'owner_ref' => $this->owner->public_id,
|
||||||
|
'name' => 'Renal Clinic',
|
||||||
|
'slug' => 'renal-owner-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,
|
||||||
|
'renal',
|
||||||
|
);
|
||||||
|
|
||||||
|
$department = Department::query()
|
||||||
|
->where('branch_id', $this->branch->id)
|
||||||
|
->where('type', 'renal')
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$this->patient = Patient::create([
|
||||||
|
'owner_ref' => $this->owner->public_id,
|
||||||
|
'organization_id' => $this->organization->id,
|
||||||
|
'branch_id' => $this->branch->id,
|
||||||
|
'patient_number' => 'P-REN-SUITE',
|
||||||
|
'first_name' => 'Kojo',
|
||||||
|
'last_name' => 'Kidney',
|
||||||
|
'gender' => 'female',
|
||||||
|
'date_of_birth' => '1988-09-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_clinical_tabs_render(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->get(route('care.specialty.workspace', [
|
||||||
|
'module' => 'renal',
|
||||||
|
'visit' => $this->visit,
|
||||||
|
'tab' => 'overview',
|
||||||
|
]))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Renal Care overview')
|
||||||
|
->assertSee('data-care-stage-bar', false)
|
||||||
|
->assertSee('Start history')
|
||||||
|
->assertSee('Renal reports');
|
||||||
|
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->get(route('care.specialty.workspace', [
|
||||||
|
'module' => 'renal',
|
||||||
|
'visit' => $this->visit,
|
||||||
|
'tab' => 'exam',
|
||||||
|
]))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Chief complaint', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_clinical_save_sets_stage_and_alerts(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->post(route('care.specialty.clinical.save', [
|
||||||
|
'module' => 'renal',
|
||||||
|
'visit' => $this->visit,
|
||||||
|
]), [
|
||||||
|
'tab' => 'exam',
|
||||||
|
'payload' => [
|
||||||
|
'chief_complaint' => 'Missed dialysis with oedema',
|
||||||
|
'fluid_status' => 'Overload',
|
||||||
|
'exam_findings' => 'Bilateral crackles',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertRedirect();
|
||||||
|
|
||||||
|
$this->assertSame('exam', $this->visit->fresh()->specialty_stage);
|
||||||
|
|
||||||
|
$record = SpecialtyClinicalRecord::query()
|
||||||
|
->where('visit_id', $this->visit->id)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$codes = collect($record->alerts)->pluck('code')->all();
|
||||||
|
$this->assertContains('ren.fluid_overload', $codes);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_stage_advance_and_terminal_completes_visit(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->post(route('care.specialty.renal.stage', $this->visit), [
|
||||||
|
'stage' => 'plan',
|
||||||
|
])
|
||||||
|
->assertRedirect(route('care.specialty.workspace', [
|
||||||
|
'module' => 'renal',
|
||||||
|
'visit' => $this->visit,
|
||||||
|
'tab' => 'plan',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->assertSame('plan', $this->visit->fresh()->specialty_stage);
|
||||||
|
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->post(route('care.specialty.renal.plan', $this->visit), [
|
||||||
|
'tab' => 'plan',
|
||||||
|
'payload' => [
|
||||||
|
'diagnosis' => 'CKD 5D with fluid overload',
|
||||||
|
'plan' => 'Urgent HD; adjust dry weight',
|
||||||
|
'outcome' => 'Completed — discharged',
|
||||||
|
'follow_up' => 'Next HD in 2 days',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertRedirect(route('care.specialty.workspace', [
|
||||||
|
'module' => 'renal',
|
||||||
|
'visit' => $this->visit,
|
||||||
|
'tab' => 'plan',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$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' => 'renal_plan',
|
||||||
|
'status' => SpecialtyClinicalRecord::STATUS_COMPLETED,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_reports_and_print(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->get(route('care.specialty.renal.reports'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Renal Care reports')
|
||||||
|
->assertSee('Arrivals today');
|
||||||
|
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->get(route('care.specialty.renal.print', $this->visit))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee($this->patient->fullName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_index_does_not_auto_open_patient(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->get(route('care.specialty.show', 'renal'))
|
||||||
|
->assertOk()
|
||||||
|
->assertDontSee('Renal Care overview');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Http\Middleware\EnsurePlatformSession;
|
||||||
|
use App\Models\Appointment;
|
||||||
|
use App\Models\Branch;
|
||||||
|
use App\Models\Department;
|
||||||
|
use App\Models\Member;
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Models\Patient;
|
||||||
|
use App\Models\SpecialtyClinicalRecord;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Visit;
|
||||||
|
use App\Services\Care\SpecialtyModuleService;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class CareSurgerySuiteTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected User $owner;
|
||||||
|
|
||||||
|
protected Organization $organization;
|
||||||
|
|
||||||
|
protected Branch $branch;
|
||||||
|
|
||||||
|
protected Patient $patient;
|
||||||
|
|
||||||
|
protected Visit $visit;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
$this->withoutMiddleware(EnsurePlatformSession::class);
|
||||||
|
|
||||||
|
$this->owner = User::create([
|
||||||
|
'public_id' => 'surg-owner',
|
||||||
|
'name' => 'Owner',
|
||||||
|
'email' => 'surg-owner@example.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->organization = Organization::create([
|
||||||
|
'owner_ref' => $this->owner->public_id,
|
||||||
|
'name' => 'Surgery Clinic',
|
||||||
|
'slug' => 'surg-owner-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,
|
||||||
|
'surgery',
|
||||||
|
);
|
||||||
|
|
||||||
|
$department = Department::query()
|
||||||
|
->where('branch_id', $this->branch->id)
|
||||||
|
->where('type', 'surgery')
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
$this->patient = Patient::create([
|
||||||
|
'owner_ref' => $this->owner->public_id,
|
||||||
|
'organization_id' => $this->organization->id,
|
||||||
|
'branch_id' => $this->branch->id,
|
||||||
|
'patient_number' => 'P-SUR-SUITE',
|
||||||
|
'first_name' => 'Efua',
|
||||||
|
'last_name' => 'Hernia',
|
||||||
|
'gender' => 'female',
|
||||||
|
'date_of_birth' => '1988-09-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_clinical_tabs_render(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->get(route('care.specialty.workspace', [
|
||||||
|
'module' => 'surgery',
|
||||||
|
'visit' => $this->visit,
|
||||||
|
'tab' => 'overview',
|
||||||
|
]))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Surgery overview')
|
||||||
|
->assertSee('data-care-stage-bar', false)
|
||||||
|
->assertSee('Start pre-op history')
|
||||||
|
->assertSee('Surgery reports');
|
||||||
|
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->get(route('care.specialty.workspace', [
|
||||||
|
'module' => 'surgery',
|
||||||
|
'visit' => $this->visit,
|
||||||
|
'tab' => 'exam',
|
||||||
|
]))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Chief complaint', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_clinical_save_sets_stage_and_alerts(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->post(route('care.specialty.clinical.save', [
|
||||||
|
'module' => 'surgery',
|
||||||
|
'visit' => $this->visit,
|
||||||
|
]), [
|
||||||
|
'tab' => 'exam',
|
||||||
|
'payload' => [
|
||||||
|
'chief_complaint' => 'Right inguinal hernia',
|
||||||
|
'exam_findings' => 'Reducible hernia',
|
||||||
|
'asa_class' => 'II',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertRedirect();
|
||||||
|
|
||||||
|
$this->assertSame('exam', $this->visit->fresh()->specialty_stage);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_stage_advance_and_terminal_completes_visit(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->post(route('care.specialty.surgery.stage', $this->visit), [
|
||||||
|
'stage' => 'postop',
|
||||||
|
])
|
||||||
|
->assertRedirect(route('care.specialty.workspace', [
|
||||||
|
'module' => 'surgery',
|
||||||
|
'visit' => $this->visit,
|
||||||
|
'tab' => 'postop',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$this->assertSame('postop', $this->visit->fresh()->specialty_stage);
|
||||||
|
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->post(route('care.specialty.surgery.postop', $this->visit), [
|
||||||
|
'tab' => 'postop',
|
||||||
|
'payload' => [
|
||||||
|
'review_type' => 'Immediate post-op',
|
||||||
|
'progress' => 'Stable; wound dry',
|
||||||
|
'outcome' => 'Completed — discharged',
|
||||||
|
'next_steps' => 'Wound check in 7 days',
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertRedirect(route('care.specialty.workspace', [
|
||||||
|
'module' => 'surgery',
|
||||||
|
'visit' => $this->visit,
|
||||||
|
'tab' => 'postop',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$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' => 'surg_postop',
|
||||||
|
'status' => SpecialtyClinicalRecord::STATUS_COMPLETED,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_reports_and_print(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->get(route('care.specialty.surgery.reports'))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Surgery reports')
|
||||||
|
->assertSee('Arrivals today');
|
||||||
|
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->get(route('care.specialty.surgery.print', $this->visit))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee($this->patient->fullName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_index_does_not_auto_open_patient(): void
|
||||||
|
{
|
||||||
|
$this->actingAs($this->owner)
|
||||||
|
->get(route('care.specialty.show', 'surgery'))
|
||||||
|
->assertOk()
|
||||||
|
->assertDontSee('Surgery overview');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user