Reserve floor queues for clinical staff and deepen specialty forms.
Deploy Ladill Care / deploy (push) Successful in 41s

Hospital admins no longer see specialty/queue floor nav; Call next is a full-width button; specialty encounters open module-specific clinical forms instead of the general consultation page.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-18 12:04:05 +00:00
co-authored by Cursor
parent bd8218fb45
commit 612e021e85
18 changed files with 1145 additions and 183 deletions
@@ -31,6 +31,7 @@ class QueueController extends Controller
public function index(Request $request): View
{
$this->authorizeAbility($request, 'appointments.view');
abort_unless(app(CarePermissions::class)->handlesFloorCare($this->member($request)), 403);
$organization = $this->organization($request);
$member = $this->member($request);
$owner = $this->ownerRef($request);
@@ -143,6 +144,7 @@ class QueueController extends Controller
public function callNext(Request $request): RedirectResponse
{
abort_unless(app(CarePermissions::class)->handlesFloorCare($this->member($request)), 403);
$this->authorizeAbility($request, 'appointments.view');
$organization = $this->organization($request);
abort_unless($this->queueBridge->isEnabled($organization), 404);
@@ -158,7 +158,7 @@ class SpecialtyModuleController extends Controller
return back()->with('error', 'This appointment cannot start a consultation from its current status.');
}
$consultation = $consultations->startFromAppointment(
$consultations->startFromAppointment(
$appointment,
$owner,
$owner,
@@ -167,9 +167,20 @@ class SpecialtyModuleController extends Controller
return back()->with('error', $e->getMessage());
}
$visit = $visit->fresh() ?? $appointment->visit;
$clinical = app(SpecialtyClinicalRecordService::class);
$shellTabs = app(SpecialtyShellService::class)->workspaceTabs($module);
$preferredTab = collect(array_keys($shellTabs))
->first(fn (string $tab) => $clinical->recordTypeForTab($module, $tab) !== null)
?? 'clinical_notes';
return redirect()
->route('care.consultations.show', $consultation)
->with('success', 'Consultation started.');
->route('care.specialty.workspace', [
'module' => $module,
'visit' => $visit,
'tab' => $preferredTab,
])
->with('success', 'Specialty encounter started.');
}
public function saveClinical(
+20
View File
@@ -79,4 +79,24 @@ class CarePermissions
{
return $member !== null && in_array($member->role, ['super_admin', 'hospital_admin'], true);
}
/**
* Floor care: queues, specialty clinics, lab/pharmacy counters.
* Facility admins manage settings/staff they do not staff waiting lines.
*/
public function handlesFloorCare(?Member $member): bool
{
if ($member === null || $this->isAdmin($member)) {
return false;
}
return in_array($member->role, [
'doctor',
'nurse',
'lab_technician',
'pharmacist',
'receptionist',
'cashier',
], true);
}
}
@@ -19,23 +19,14 @@ class SpecialtyClinicalRecordService
*/
public function recordTypesForModule(string $moduleKey): array
{
return match ($moduleKey) {
'emergency' => [
'triage' => 'triage',
'clinical_notes' => 'clinical_note',
],
'blood_bank' => [
'requests' => 'blood_request',
'inventory' => 'inventory_note',
],
'dentistry' => [
'odontogram' => 'odontogram',
'clinical_notes' => 'clinical_note',
],
default => [
'clinical_notes' => 'clinical_note',
],
};
$configured = config("care_specialty_clinical.record_types.{$moduleKey}");
if (is_array($configured) && $configured !== []) {
return $configured;
}
return [
'clinical_notes' => 'clinical_note',
];
}
public function recordTypeForTab(string $moduleKey, string $tab): ?string
@@ -50,65 +41,14 @@ class SpecialtyClinicalRecordService
*/
public function fieldsFor(string $moduleKey, string $recordType): array
{
$key = "{$moduleKey}.{$recordType}";
$fields = config("care_specialty_clinical.fields.{$moduleKey}.{$recordType}");
if (is_array($fields) && $fields !== []) {
return $fields;
}
return match ($key) {
'emergency.triage' => [
['name' => 'acuity', 'label' => 'Triage acuity', 'type' => 'select', 'required' => true, 'options' => ['1 - Resuscitation', '2 - Emergency', '3 - Urgent', '4 - Semi-urgent', '5 - Non-urgent']],
['name' => 'chief_complaint', 'label' => 'Chief complaint', 'type' => 'text', 'required' => true],
['name' => 'onset', 'label' => 'Onset', 'type' => 'text'],
['name' => 'airway_ok', 'label' => 'Airway patent', 'type' => 'boolean'],
['name' => 'breathing_ok', 'label' => 'Breathing adequate', 'type' => 'boolean'],
['name' => 'circulation_ok', 'label' => 'Circulation stable', 'type' => 'boolean'],
['name' => 'gcs', 'label' => 'GCS', 'type' => 'number'],
['name' => 'pain_score', 'label' => 'Pain score (010)', 'type' => 'number'],
['name' => 'mode_of_arrival', 'label' => 'Mode of arrival', 'type' => 'select', 'options' => ['Walk-in', 'Ambulance', 'Police', 'Referral', 'Other']],
['name' => 'disposition_intent', 'label' => 'Disposition intent', 'type' => 'select', 'options' => ['Resus', 'Treatment bay', 'Observation', 'Discharge', 'Admit', 'Transfer']],
['name' => 'notes', 'label' => 'Triage notes', 'type' => 'textarea', 'rows' => 3],
],
'emergency.clinical_note' => [
['name' => 'history', 'label' => 'History of present illness', 'type' => 'textarea', 'rows' => 4, 'required' => true],
['name' => 'examination', 'label' => 'Examination', 'type' => 'textarea', 'rows' => 3],
['name' => 'working_diagnosis', 'label' => 'Working diagnosis', 'type' => 'text'],
['name' => 'plan', 'label' => 'Plan', 'type' => 'textarea', 'rows' => 3],
['name' => 'procedures', 'label' => 'Procedures performed', 'type' => 'textarea', 'rows' => 2],
],
'blood_bank.blood_request' => [
['name' => 'product', 'label' => 'Product', 'type' => 'select', 'required' => true, 'options' => ['Whole blood', 'Packed RBC', 'Platelets', 'FFP', 'Cryoprecipitate']],
['name' => 'units', 'label' => 'Units requested', 'type' => 'number', 'required' => true],
['name' => 'urgency', 'label' => 'Urgency', 'type' => 'select', 'required' => true, 'options' => ['Routine', 'Urgent', 'Emergency / massive']],
['name' => 'patient_blood_group', 'label' => 'Patient blood group', 'type' => 'select', 'options' => ['A+', 'A-', 'B+', 'B-', 'AB+', 'AB-', 'O+', 'O-', 'Unknown']],
['name' => 'indication', 'label' => 'Clinical indication', 'type' => 'textarea', 'rows' => 2, 'required' => true],
['name' => 'crossmatch_status', 'label' => 'Cross-match status', 'type' => 'select', 'options' => ['Not started', 'In progress', 'Compatible', 'Incompatible', 'Issued']],
['name' => 'issued_units', 'label' => 'Units issued', 'type' => 'number'],
['name' => 'notes', 'label' => 'Notes', 'type' => 'textarea', 'rows' => 2],
],
'blood_bank.inventory_note' => [
['name' => 'whole_blood_units', 'label' => 'Whole blood on hand', 'type' => 'number'],
['name' => 'packed_rbc_units', 'label' => 'Packed RBC on hand', 'type' => 'number'],
['name' => 'platelet_units', 'label' => 'Platelets on hand', 'type' => 'number'],
['name' => 'ffp_units', 'label' => 'FFP on hand', 'type' => 'number'],
['name' => 'low_stock_alert', 'label' => 'Flag low stock', 'type' => 'boolean'],
['name' => 'notes', 'label' => 'Inventory notes', 'type' => 'textarea', 'rows' => 3],
],
'dentistry.odontogram' => [
['name' => 'teeth_affected', 'label' => 'Teeth affected (FDI numbers, comma-separated)', 'type' => 'text', 'required' => true],
['name' => 'findings', 'label' => 'Chart findings', 'type' => 'textarea', 'rows' => 3, 'required' => true],
['name' => 'planned_procedures', 'label' => 'Planned procedures', 'type' => 'textarea', 'rows' => 2],
['name' => 'completed_procedures', 'label' => 'Completed this visit', 'type' => 'textarea', 'rows' => 2],
['name' => 'anesthesia', 'label' => 'Anesthesia', 'type' => 'select', 'options' => ['None', 'Local', 'Sedation', 'GA']],
['name' => 'occlusion_notes', 'label' => 'Occlusion / bite notes', 'type' => 'text'],
],
'dentistry.clinical_note' => [
['name' => 'history', 'label' => 'History', 'type' => 'textarea', 'rows' => 3, 'required' => true],
['name' => 'examination', 'label' => 'Examination', 'type' => 'textarea', 'rows' => 3],
['name' => 'working_diagnosis', 'label' => 'Diagnosis', 'type' => 'text'],
['name' => 'plan', 'label' => 'Plan', 'type' => 'textarea', 'rows' => 2],
],
default => [
['name' => 'notes', 'label' => 'Clinical notes', 'type' => 'textarea', 'rows' => 4, 'required' => true],
],
};
return [
['name' => 'notes', 'label' => 'Clinical notes', 'type' => 'textarea', 'rows' => 4, 'required' => true],
];
}
public function findForVisit(Visit $visit, string $moduleKey, string $recordType): ?SpecialtyClinicalRecord
+1 -1
View File
@@ -150,7 +150,7 @@ class SpecialtyModuleService
/**
* Whether the member may see/serve this specialty module.
* Doctors must be linked to a practitioner desk in that specialty department.
* Admins/receptionists keep org-wide visibility for operations.
* Receptionists keep queue/ops visibility; facility admins do not staff specialty floors.
*/
public function memberCanAccess(Organization $organization, ?Member $member, string $key): bool
{