> */ protected array $roleInherits = [ 'lab_manager' => ['lab_technician'], 'blood_bank_manager' => ['blood_bank_officer'], ]; /** * Specialty module keys each role may open (manage). Null = no specialty * matrix entry (fall back to module config allowlists + desk keywords). * Empty list = no specialty manage. * * @var array|null> */ protected array $rolePrimaryApps = [ 'super_admin' => null, // all via admin 'hospital_admin' => null, 'emergency_physician' => ['emergency', 'radiology', 'pathology', 'blood_bank', 'cardiology'], 'ed_nurse' => ['emergency', 'blood_bank', 'radiology', 'pathology'], 'general_physician' => [ 'emergency', 'cardiology', 'pediatrics', 'ent', 'dermatology', 'ophthalmology', 'vaccination', 'child_welfare', ], 'doctor' => [ 'emergency', 'cardiology', 'pediatrics', 'ent', 'dermatology', 'ophthalmology', 'vaccination', 'child_welfare', ], // legacy → GP (desk specialists narrowed elsewhere) 'surgeon' => ['surgery', 'radiology', 'pathology', 'blood_bank'], 'theatre_nurse' => ['surgery', 'blood_bank'], 'radiologist' => ['radiology'], 'radiographer' => ['radiology'], 'lab_technician' => ['pathology', 'blood_bank'], 'lab_manager' => ['pathology', 'blood_bank'], 'pathologist' => ['pathology'], 'blood_bank_officer' => ['blood_bank'], 'blood_bank_manager' => ['blood_bank'], 'pharmacist' => [], // Pharmacy is core, not a specialty module 'dentist' => ['dentistry'], 'psychiatrist' => ['psychiatry'], 'physiotherapist' => ['physiotherapy'], 'midwife' => ['maternity'], 'pediatrician' => ['pediatrics'], 'oncologist' => ['oncology', 'infusion'], 'cardiologist' => ['cardiology'], 'ent_specialist' => ['ent'], 'dermatologist' => ['dermatology'], 'ophthalmologist' => ['ophthalmology'], 'orthopedist' => ['orthopedics'], 'podiatrist' => ['podiatry'], 'fertility_specialist' => ['fertility'], 'dialysis_nurse' => ['renal'], 'ambulance_staff' => ['ambulance', 'emergency'], 'receptionist' => [], // refer only — see roleReferApps 'billing_officer' => [], 'cashier' => [], 'accountant' => [], 'department_manager' => null, // department analytics — all enabled modules view 'nurse' => ['vaccination', 'child_welfare', 'infusion', 'maternity'], ]; /** * Specialty modules a role may refer into (beyond manage). Empty / missing = none * beyond primary apps. Manage always implies refer. * * @var array> */ protected array $roleReferApps = [ 'general_physician' => [ 'dentistry', 'physiotherapy', 'maternity', 'psychiatry', 'orthopedics', 'oncology', 'renal', 'surgery', 'radiology', 'pathology', 'blood_bank', 'infusion', 'podiatry', 'fertility', 'ambulance', ], 'doctor' => [ 'dentistry', 'physiotherapy', 'maternity', 'psychiatry', 'orthopedics', 'oncology', 'renal', 'surgery', 'radiology', 'pathology', 'blood_bank', 'infusion', 'podiatry', 'fertility', 'ambulance', ], 'nurse' => ['emergency', 'pediatrics', 'blood_bank'], 'pharmacist' => ['oncology', 'infusion', 'maternity', 'emergency'], 'receptionist' => [ 'emergency', 'pediatrics', 'vaccination', 'child_welfare', 'dentistry', 'maternity', 'ambulance', 'ophthalmology', 'physiotherapy', ], ]; /** * Specialty modules a role may view without refer/manage. Empty / missing = none * beyond primary + refer. Manage and refer always imply view. * * @var array> */ protected array $roleViewApps = [ 'lab_technician' => ['radiology'], 'lab_manager' => ['radiology'], 'pathologist' => ['blood_bank'], 'pharmacist' => ['oncology', 'infusion', 'maternity', 'emergency'], ]; /** * @var array> */ protected array $roleAbilities = [ 'super_admin' => ['*'], 'hospital_admin' => ['*'], 'emergency_physician' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'investigations.request', 'prescriptions.manage', 'lab.view', 'lab.results.view', 'vitals.manage', 'assessments.view', 'assessments.capture', 'assessments.manage', 'pathways.manage', 'emergency.queue.view', 'emergency.discharge', 'radiology.request', 'pathology.request', 'bloodbank.request', 'cardiology.view', 'queue.patient_board', 'service_queues.console', ], 'ed_nurse' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'vitals.manage', 'assessments.view', 'assessments.capture', 'emergency.queue.view', 'radiology.request', 'pathology.request', 'bloodbank.request', // cannot: emergency.discharge, consultations.manage 'service_queues.console', ], 'general_physician' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'investigations.request', 'prescriptions.manage', 'lab.view', 'lab.results.view', 'vitals.manage', 'assessments.view', 'assessments.capture', 'assessments.manage', 'pathways.manage', 'emergency.queue.view', 'emergency.discharge', 'cardiology.view', 'pediatrics.view', 'ent.view', 'dermatology.view', 'ophthalmology.view', 'queue.patient_board', 'service_queues.console', ], // Legacy alias — same abilities as general_physician (desk specialists // still narrowed by SpecialtyModuleService keyword matching). 'doctor' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'investigations.request', 'prescriptions.manage', 'lab.view', 'lab.results.view', 'vitals.manage', 'assessments.view', 'assessments.capture', 'assessments.manage', 'pathways.manage', 'emergency.queue.view', 'emergency.discharge', 'cardiology.view', 'pediatrics.view', 'ent.view', 'dermatology.view', 'ophthalmology.view', 'queue.patient_board', 'service_queues.console', ], 'surgeon' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'investigations.request', 'prescriptions.manage', 'lab.view', 'lab.results.view', 'vitals.manage', 'assessments.view', 'assessments.capture', 'assessments.manage', 'pathways.manage', 'radiology.request', 'pathology.request', 'bloodbank.request', 'service_queues.console', ], 'theatre_nurse' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'vitals.manage', 'assessments.view', 'assessments.capture', 'bloodbank.request', 'service_queues.console', ], 'radiologist' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'investigations.request', 'lab.view', 'lab.results.view', 'radiology.view', 'radiology.report', 'assessments.view', 'assessments.capture', 'service_queues.console', ], 'radiographer' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'radiology.view', 'radiology.capture', 'service_queues.console', ], 'lab_technician' => [ 'dashboard.view', 'patients.view', 'lab.view', 'lab.manage', 'pathology.view', 'pathology.result.enter', 'blood_bank.view', 'blood_bank.issue', // cannot: pathology.result.approve, lab.catalog.manage 'service_queues.console', ], 'lab_manager' => [ 'lab.catalog.manage', 'pathology.result.approve', 'analytics.lab.view', 'lab.admin', 'blood_bank.manage', ], 'pathologist' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'lab.view', 'lab.manage', 'lab.results.view', 'pathology.view', 'pathology.result.enter', 'pathology.result.approve', 'assessments.view', 'assessments.capture', 'service_queues.console', ], 'blood_bank_officer' => [ 'dashboard.view', 'patients.view', 'blood_bank.view', 'blood_bank.issue', 'service_queues.console', ], 'blood_bank_manager' => [ 'blood_bank.manage', 'blood_bank.inventory', 'blood_bank.reports', ], 'pharmacist' => [ 'dashboard.view', 'patients.view', 'prescriptions.view', 'prescriptions.dispense', 'pharmacy.view', 'pharmacy.manage', 'service_queues.console', ], 'dentist' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'investigations.request', 'prescriptions.manage', 'vitals.manage', 'assessments.view', 'assessments.capture', 'assessments.manage', 'pathways.manage', 'dentistry.view', 'dentistry.manage', 'service_queues.console', ], 'psychiatrist' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'prescriptions.manage', 'assessments.view', 'assessments.capture', 'assessments.manage', 'pathways.manage', 'psychiatry.view', 'psychiatry.manage', 'service_queues.console', ], 'physiotherapist' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'vitals.manage', 'assessments.view', 'assessments.capture', 'physiotherapy.view', 'physiotherapy.manage', 'service_queues.console', ], 'midwife' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'vitals.manage', 'assessments.view', 'assessments.capture', 'assessments.manage', 'maternity.view', 'maternity.manage', 'service_queues.console', ], 'pediatrician' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'investigations.request', 'prescriptions.manage', 'vitals.manage', 'assessments.view', 'assessments.capture', 'assessments.manage', 'pathways.manage', 'pediatrics.view', 'pediatrics.manage', 'queue.patient_board', 'service_queues.console', ], 'oncologist' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'investigations.request', 'prescriptions.manage', 'vitals.manage', 'assessments.view', 'assessments.capture', 'assessments.manage', 'pathways.manage', 'oncology.view', 'oncology.manage', 'infusion.view', 'infusion.manage', 'service_queues.console', ], 'cardiologist' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'investigations.request', 'prescriptions.manage', 'lab.view', 'lab.results.view', 'vitals.manage', 'assessments.view', 'assessments.capture', 'assessments.manage', 'pathways.manage', 'cardiology.view', 'cardiology.manage', 'service_queues.console', ], 'ent_specialist' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'investigations.request', 'prescriptions.manage', 'vitals.manage', 'assessments.view', 'assessments.capture', 'assessments.manage', 'pathways.manage', 'ent.view', 'ent.manage', 'service_queues.console', ], 'dermatologist' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'investigations.request', 'prescriptions.manage', 'vitals.manage', 'assessments.view', 'assessments.capture', 'assessments.manage', 'pathways.manage', 'dermatology.view', 'dermatology.manage', 'service_queues.console', ], 'ophthalmologist' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'investigations.request', 'prescriptions.manage', 'vitals.manage', 'assessments.view', 'assessments.capture', 'assessments.manage', 'pathways.manage', 'ophthalmology.view', 'ophthalmology.manage', 'service_queues.console', ], 'orthopedist' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'investigations.request', 'prescriptions.manage', 'vitals.manage', 'assessments.view', 'assessments.capture', 'assessments.manage', 'pathways.manage', 'orthopedics.view', 'orthopedics.manage', 'service_queues.console', ], 'podiatrist' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'vitals.manage', 'assessments.view', 'assessments.capture', 'podiatry.view', 'podiatry.manage', 'service_queues.console', ], 'fertility_specialist' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'consultations.manage', 'investigations.request', 'prescriptions.manage', 'vitals.manage', 'assessments.view', 'assessments.capture', 'assessments.manage', 'pathways.manage', 'fertility.view', 'fertility.manage', 'service_queues.console', ], 'dialysis_nurse' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'vitals.manage', 'assessments.view', 'assessments.capture', 'renal.view', 'renal.manage', 'service_queues.console', ], 'ambulance_staff' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'vitals.manage', 'assessments.view', 'assessments.capture', 'ambulance.view', 'ambulance.manage', 'emergency.queue.view', 'service_queues.console', ], 'receptionist' => [ 'dashboard.view', 'patients.view', 'patients.manage', 'appointments.view', 'appointments.manage', 'queue.manage', 'service_queues.console', ], 'billing_officer' => [ 'dashboard.view', 'patients.view', 'bills.view', 'bills.manage', 'insurance.view', 'patient_accounts.view', // cannot: payments.manage (receive payments) 'service_queues.console', ], // Cashiers collect payments — cannot edit invoices (no bills.manage). 'cashier' => [ 'dashboard.view', 'patients.view', 'bills.view', 'payments.manage', 'payments.refund', 'receipts.view', 'service_queues.console', ], 'accountant' => [ 'dashboard.view', 'bills.view', 'reports.finance.view', 'reports.finance.export', 'accounting.view', 'accounting.gl', 'accounting.reconcile', 'audit.view', 'audit.export', ], 'department_manager' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'analytics.department.view', 'reports.finance.view', 'service_queues.console', ], // Legacy floor nurse — vaccination / CWC / infusion / maternity manage. 'nurse' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'vitals.manage', 'assessments.view', 'assessments.capture', 'service_queues.console', ], ]; /** Clinical roles that staff the floor (queues, specialty desks, counters). */ protected array $floorCareRoles = [ 'emergency_physician', 'ed_nurse', 'general_physician', 'doctor', 'surgeon', 'theatre_nurse', 'radiologist', 'radiographer', 'lab_technician', 'lab_manager', 'pathologist', 'blood_bank_officer', 'blood_bank_manager', 'pharmacist', 'dentist', 'psychiatrist', 'physiotherapist', 'midwife', 'pediatrician', 'oncologist', 'cardiologist', 'ent_specialist', 'dermatologist', 'ophthalmologist', 'orthopedist', 'podiatrist', 'fertility_specialist', 'dialysis_nurse', 'ambulance_staff', 'receptionist', 'cashier', 'billing_officer', 'nurse', ]; /** Roles that get practitioner desks / multi-branch clinical assignment. */ protected array $clinicalPractitionerRoles = [ 'emergency_physician', 'general_physician', 'doctor', 'surgeon', 'radiologist', 'pathologist', 'dentist', 'psychiatrist', 'physiotherapist', 'midwife', 'pediatrician', 'oncologist', 'cardiologist', 'ent_specialist', 'dermatologist', 'ophthalmologist', 'orthopedist', 'podiatrist', 'fertility_specialist', 'ed_nurse', 'theatre_nurse', 'dialysis_nurse', 'ambulance_staff', 'radiographer', 'lab_technician', 'lab_manager', 'blood_bank_officer', 'blood_bank_manager', 'pharmacist', 'nurse', ]; public function can(?Member $member, string $ability): bool { if ($member === null) { return false; } $abilities = $this->resolvedAbilities((string) $member->role); if (in_array('*', $abilities, true)) { return true; } return in_array($ability, $abilities, true); } /** * @return list */ public function resolvedAbilities(string $role): array { $seen = []; $merged = []; $stack = [$role]; while ($stack !== []) { $current = array_pop($stack); if (isset($seen[$current])) { continue; } $seen[$current] = true; foreach ($this->roleInherits[$current] ?? [] as $parent) { $stack[] = $parent; } foreach ($this->roleAbilities[$current] ?? [] as $ability) { $merged[$ability] = true; } } return array_keys($merged); } public function isAdmin(?Member $member): bool { return $member !== null && in_array($member->role, ['super_admin', 'hospital_admin'], true); } /** * Primary specialty module keys for this member's role (manage). * * @return list|null null = unrestricted specialty (admins / dept manager use other rules) */ public function primaryAppsFor(?Member $member): ?array { if ($member === null) { return []; } if ($this->isAdmin($member)) { return null; } $role = (string) $member->role; if (! array_key_exists($role, $this->rolePrimaryApps)) { return []; } return $this->rolePrimaryApps[$role]; } /** * Specialty modules this role may refer into (beyond manage). * * @return list */ public function referAppsFor(?Member $member): array { if ($member === null || $this->isAdmin($member)) { return []; } $role = (string) $member->role; if ($role === 'department_manager') { return []; } return array_values(array_unique($this->roleReferApps[$role] ?? [])); } /** * Specialty modules this role may view without refer/manage. * * @return list */ public function viewAppsFor(?Member $member): array { if ($member === null || $this->isAdmin($member)) { return []; } $role = (string) $member->role; return array_values(array_unique($this->roleViewApps[$role] ?? [])); } /** * Whether the role matrix lists this specialty module as a primary app. */ public function roleHasPrimaryApp(?Member $member, string $moduleKey): bool { $apps = $this->primaryAppsFor($member); if ($apps === null) { return true; } return in_array($moduleKey, $apps, true); } /** * Single-desk specialist roles (hide Specialty nav group). */ public function isSingleAppSpecialist(?Member $member): bool { $apps = $this->primaryAppsFor($member); if ($apps === null) { return false; } return count($apps) === 1; } /** * 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, $this->floorCareRoles, true); } /** * Dedicated patient-flow board (GP Queue home / call-next board). * Permission-gated — not a role string check. Facility admins do not staff this board. */ public function canAccessPatientQueue(?Member $member): bool { if ($member === null || $this->isAdmin($member)) { return false; } return $this->can($member, 'queue.patient_board'); } /** * @return list */ public function clinicalPractitionerRoles(): array { return $this->clinicalPractitionerRoles; } /** * @return list */ public function floorCareRoles(): array { return $this->floorCareRoles; } /** * Roles that may switch / multi-site like legacy doctors (practitioner branches). */ public function usesPractitionerBranchScope(?Member $member): bool { if ($member === null) { return false; } return $this->can($member, 'queue.patient_board') || in_array($member->role, [ 'emergency_physician', 'general_physician', 'doctor', 'surgeon', 'radiologist', 'pathologist', 'dentist', 'psychiatrist', 'pediatrician', 'oncologist', 'physiotherapist', 'midwife', 'cardiologist', 'ent_specialist', 'dermatologist', 'ophthalmologist', 'orthopedist', 'podiatrist', 'fertility_specialist', ], true); } /** * Care cashiers collect via Billing, not Ladill POS. Strip POS from * Identity app grants so the suite launcher hides it. * * @param list|null $slugs Null = unrestricted owner access. * @return list|null */ public function filterAllowedAppSlugs(?Member $member, ?array $slugs): ?array { if ($slugs === null || $member?->role !== 'cashier') { return $slugs; } return array_values(array_filter( array_map('strval', $slugs), fn (string $slug) => $slug !== 'pos', )); } /** * @param array{full_access?: bool, apps?: list, show_hub?: bool, show_billing?: bool} $access * @return array{full_access?: bool, apps?: list, show_hub?: bool, show_billing?: bool} */ public function filterStaffAppAccess(?Member $member, array $access): array { if ($member?->role !== 'cashier') { return $access; } $apps = $this->filterAllowedAppSlugs($member, array_values(array_map( 'strval', (array) ($access['apps'] ?? []), ))) ?? []; $access['apps'] = $apps; if (empty($access['full_access']) && count($apps) <= 1) { $access['show_hub'] = false; } return $access; } }