Deploy Ladill Care / deploy (push) Successful in 39s
Floor nurses keep Nursing Services admin-only, but now get a My ward home keyed off today's roster or unit placement with patient lists and direct links to MAR, vitals/assessments, notes, and handovers. Care unit show and board crumbs open for station users so clinical pages are reachable without department admin rights. Co-authored-by: Cursor <cursoragent@cursor.com>
889 lines
33 KiB
PHP
889 lines
33 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Care;
|
|
|
|
use App\Models\Member;
|
|
|
|
/**
|
|
* Care RBAC: role → abilities + specialty manage / refer / view apps.
|
|
*
|
|
* Authorization must use can() / primaryAppsFor() / referAppsFor() /
|
|
* viewAppsFor() — never role string checks like isDoctor. Legacy `doctor`
|
|
* aliases to general_physician for abilities; desk specialists are narrowed
|
|
* by SpecialtyModuleService keyword matching. `nurse` is a limited floor role
|
|
* (ED / theatre / dialysis nurses are separate).
|
|
*
|
|
* Inheritance: lab_manager ⊃ lab_technician; blood_bank_manager ⊃ blood_bank_officer.
|
|
*/
|
|
class CarePermissions
|
|
{
|
|
/**
|
|
* Child role abilities are merged into the parent (parent wins on duplicates).
|
|
*
|
|
* @var array<string, list<string>>
|
|
*/
|
|
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<string, list<string>|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', 'nursing_services'],
|
|
'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', 'nursing_services'],
|
|
'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' => ['womens_health', 'nursing_services'],
|
|
'obstetrician' => ['womens_health'],
|
|
'gynecologist' => ['womens_health'],
|
|
'obgyn' => ['womens_health'],
|
|
'labour_ward_nurse' => ['womens_health', 'nursing_services'],
|
|
'pediatrician' => ['pediatrics'],
|
|
'oncologist' => ['oncology', 'infusion'],
|
|
'cardiologist' => ['cardiology'],
|
|
'ent_specialist' => ['ent'],
|
|
'dermatologist' => ['dermatology'],
|
|
'ophthalmologist' => ['ophthalmology'],
|
|
'orthopedist' => ['orthopedics'],
|
|
'podiatrist' => ['podiatry'],
|
|
'fertility_specialist' => ['fertility'],
|
|
'embryologist' => ['fertility'],
|
|
'fertility_nurse' => ['fertility', 'nursing_services'],
|
|
'dialysis_nurse' => ['renal', 'nursing_services'],
|
|
'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', 'womens_health'],
|
|
];
|
|
|
|
/**
|
|
* Specialty modules a role may refer into (beyond manage). Empty / missing = none
|
|
* beyond primary apps. Manage always implies refer.
|
|
*
|
|
* @var array<string, list<string>>
|
|
*/
|
|
protected array $roleReferApps = [
|
|
'general_physician' => [
|
|
'dentistry', 'physiotherapy', 'womens_health', 'psychiatry', 'orthopedics',
|
|
'oncology', 'renal', 'surgery', 'radiology', 'pathology', 'blood_bank',
|
|
'infusion', 'podiatry', 'fertility', 'ambulance',
|
|
],
|
|
'doctor' => [
|
|
'dentistry', 'physiotherapy', 'womens_health', 'psychiatry', 'orthopedics',
|
|
'oncology', 'renal', 'surgery', 'radiology', 'pathology', 'blood_bank',
|
|
'infusion', 'podiatry', 'fertility', 'ambulance',
|
|
],
|
|
'nurse' => ['emergency', 'pediatrics', 'blood_bank'],
|
|
'pharmacist' => ['oncology', 'infusion', 'womens_health', 'emergency'],
|
|
'receptionist' => [
|
|
'emergency', 'pediatrics', 'vaccination', 'child_welfare', 'dentistry',
|
|
'womens_health', 'ambulance', 'ophthalmology', 'physiotherapy',
|
|
],
|
|
'obstetrician' => ['fertility'],
|
|
'gynecologist' => ['fertility'],
|
|
'obgyn' => ['fertility'],
|
|
'midwife' => ['fertility'],
|
|
];
|
|
|
|
/**
|
|
* Specialty modules a role may view without refer/manage. Empty / missing = none
|
|
* beyond primary + refer. Manage and refer always imply view.
|
|
*
|
|
* @var array<string, list<string>>
|
|
*/
|
|
protected array $roleViewApps = [
|
|
'lab_technician' => ['radiology'],
|
|
'lab_manager' => ['radiology'],
|
|
'pathologist' => ['blood_bank'],
|
|
'pharmacist' => ['oncology', 'infusion', 'womens_health', 'emergency'],
|
|
];
|
|
|
|
/**
|
|
* @var array<string, list<string>>
|
|
*/
|
|
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',
|
|
'womens_health.view', 'womens_health.manage',
|
|
'service_queues.console',
|
|
],
|
|
'obstetrician' => [
|
|
'dashboard.view', 'patients.view', 'appointments.view',
|
|
'consultations.view', 'consultations.manage',
|
|
'investigations.request', 'prescriptions.manage',
|
|
'vitals.manage',
|
|
'assessments.view', 'assessments.capture', 'assessments.manage',
|
|
'pathways.manage',
|
|
'womens_health.view', 'womens_health.manage',
|
|
'service_queues.console',
|
|
],
|
|
'gynecologist' => [
|
|
'dashboard.view', 'patients.view', 'appointments.view',
|
|
'consultations.view', 'consultations.manage',
|
|
'investigations.request', 'prescriptions.manage',
|
|
'vitals.manage',
|
|
'assessments.view', 'assessments.capture', 'assessments.manage',
|
|
'pathways.manage',
|
|
'womens_health.view', 'womens_health.manage',
|
|
'service_queues.console',
|
|
],
|
|
'obgyn' => [
|
|
'dashboard.view', 'patients.view', 'appointments.view',
|
|
'consultations.view', 'consultations.manage',
|
|
'investigations.request', 'prescriptions.manage',
|
|
'vitals.manage',
|
|
'assessments.view', 'assessments.capture', 'assessments.manage',
|
|
'pathways.manage',
|
|
'womens_health.view', 'womens_health.manage',
|
|
'service_queues.console',
|
|
],
|
|
'labour_ward_nurse' => [
|
|
'dashboard.view', 'patients.view', 'appointments.view',
|
|
'consultations.view',
|
|
'vitals.manage',
|
|
'assessments.view', 'assessments.capture',
|
|
'womens_health.view', 'womens_health.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',
|
|
],
|
|
'embryologist' => [
|
|
'dashboard.view', 'patients.view', 'appointments.view',
|
|
'consultations.view',
|
|
'fertility.view', 'fertility.manage',
|
|
'service_queues.console',
|
|
],
|
|
'fertility_nurse' => [
|
|
'dashboard.view', 'patients.view', 'appointments.view',
|
|
'consultations.view',
|
|
'vitals.manage',
|
|
'assessments.view', 'assessments.capture',
|
|
'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',
|
|
],
|
|
];
|
|
|
|
/** Ward / nursing-ops roles (charge / unit leads) — not the legacy floor `nurse`. */
|
|
protected array $nursingOpsRoles = [
|
|
'ed_nurse',
|
|
'theatre_nurse',
|
|
'labour_ward_nurse',
|
|
'fertility_nurse',
|
|
'dialysis_nurse',
|
|
'midwife',
|
|
];
|
|
|
|
/** 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', 'obstetrician', 'gynecologist', 'obgyn', 'labour_ward_nurse',
|
|
'pediatrician', 'oncologist',
|
|
'cardiologist', 'ent_specialist', 'dermatologist', 'ophthalmologist',
|
|
'orthopedist', 'podiatrist', 'fertility_specialist', 'embryologist', 'fertility_nurse',
|
|
'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', 'obstetrician', 'gynecologist', 'obgyn',
|
|
'labour_ward_nurse', 'pediatrician', 'oncologist',
|
|
'cardiologist', 'ent_specialist', 'dermatologist', 'ophthalmologist',
|
|
'orthopedist', 'podiatrist', 'fertility_specialist', 'embryologist', 'fertility_nurse',
|
|
'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<string>
|
|
*/
|
|
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 $this->withNursingStationAbility(
|
|
$this->withNursingOpsAbilities(
|
|
$this->withDerivedAbilities(array_keys($merged)),
|
|
$role,
|
|
),
|
|
$role,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Clinical vitals unlock charting surfaces; ward nursing hub abilities are role-gated.
|
|
*
|
|
* @param list<string> $abilities
|
|
* @return list<string>
|
|
*/
|
|
protected function withDerivedAbilities(array $abilities): array
|
|
{
|
|
if (in_array('*', $abilities, true)) {
|
|
return $abilities;
|
|
}
|
|
|
|
// Clinical vitals → charting surfaces (not the Nursing Services admin hub).
|
|
if (in_array('vitals.manage', $abilities, true)) {
|
|
$abilities[] = 'mar.view';
|
|
$abilities[] = 'mar.administer';
|
|
$abilities[] = 'nursing.notes.view';
|
|
$abilities[] = 'nursing.notes.manage';
|
|
$abilities[] = 'nursing.handover.view';
|
|
$abilities[] = 'nursing.handover.manage';
|
|
$abilities[] = 'nursing.assessments.view';
|
|
$abilities[] = 'nursing.assessments.manage';
|
|
$abilities[] = 'nursing.performance.view';
|
|
$abilities[] = 'nursing.my_shifts.view';
|
|
}
|
|
|
|
if (in_array('analytics.department.view', $abilities, true)) {
|
|
$abilities[] = 'inpatient.placement.view';
|
|
$abilities[] = 'mar.view';
|
|
$abilities[] = 'nursing.notes.view';
|
|
$abilities[] = 'nursing.handover.view';
|
|
$abilities[] = 'nursing.assessments.view';
|
|
$abilities[] = 'nursing.performance.view';
|
|
$abilities[] = 'nursing.roster.view';
|
|
$abilities[] = 'nursing.services.view';
|
|
$abilities[] = 'nursing.my_shifts.view';
|
|
}
|
|
|
|
return array_values(array_unique($abilities));
|
|
}
|
|
|
|
/**
|
|
* Ward nursing abilities (care units, roster manage, Nursing Services hub).
|
|
* Applied only for nursing-ops roles — not ambulance / GP / specialty MDs.
|
|
*
|
|
* @param list<string> $abilities
|
|
* @return list<string>
|
|
*/
|
|
protected function withNursingOpsAbilities(array $abilities, ?string $role): array
|
|
{
|
|
if (in_array('*', $abilities, true)) {
|
|
return $abilities;
|
|
}
|
|
|
|
if ($role === null || ! in_array($role, $this->nursingOpsRoles, true)) {
|
|
return $abilities;
|
|
}
|
|
|
|
if (! in_array('vitals.manage', $abilities, true)) {
|
|
return $abilities;
|
|
}
|
|
|
|
$abilities[] = 'inpatient.placement.view';
|
|
$abilities[] = 'inpatient.placement.manage';
|
|
$abilities[] = 'nursing.roster.view';
|
|
$abilities[] = 'nursing.roster.manage';
|
|
$abilities[] = 'nursing.services.view';
|
|
|
|
return array_values(array_unique($abilities));
|
|
}
|
|
|
|
/**
|
|
* Bedside nurse station (My ward) — floor + ward nursing roles with clinical vitals.
|
|
* Not granted to GPs / ambulance solely because they also chart vitals.
|
|
*
|
|
* @param list<string> $abilities
|
|
* @return list<string>
|
|
*/
|
|
protected function withNursingStationAbility(array $abilities, ?string $role): array
|
|
{
|
|
if (in_array('*', $abilities, true)) {
|
|
return $abilities;
|
|
}
|
|
|
|
$nursingRoles = [
|
|
'nurse',
|
|
'ed_nurse',
|
|
'theatre_nurse',
|
|
'labour_ward_nurse',
|
|
'fertility_nurse',
|
|
'dialysis_nurse',
|
|
'midwife',
|
|
];
|
|
|
|
if ($role === null || ! in_array($role, $nursingRoles, true)) {
|
|
return $abilities;
|
|
}
|
|
|
|
if (! in_array('vitals.manage', $abilities, true)) {
|
|
return $abilities;
|
|
}
|
|
|
|
$abilities[] = 'nursing.station.view';
|
|
|
|
return array_values(array_unique($abilities));
|
|
}
|
|
|
|
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<string>|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<string>
|
|
*/
|
|
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<string>
|
|
*/
|
|
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<string>
|
|
*/
|
|
public function clinicalPractitionerRoles(): array
|
|
{
|
|
return $this->clinicalPractitionerRoles;
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
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',
|
|
'obstetrician', 'gynecologist', 'obgyn', 'fertility_specialist',
|
|
'cardiologist', 'ent_specialist', 'dermatologist', 'ophthalmologist',
|
|
'orthopedist', 'podiatrist',
|
|
], true);
|
|
}
|
|
|
|
/**
|
|
* Care cashiers collect via Billing, not Ladill POS. Strip POS from
|
|
* Identity app grants so the suite launcher hides it.
|
|
*
|
|
* @param list<string>|null $slugs Null = unrestricted owner access.
|
|
* @return list<string>|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<string>, show_hub?: bool, show_billing?: bool} $access
|
|
* @return array{full_access?: bool, apps?: list<string>, 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;
|
|
}
|
|
}
|