Expand specialty RBAC with dedicated specialist roles and support access.
Deploy Ladill Care / deploy (push) Successful in 41s
Deploy Ladill Care / deploy (push) Successful in 41s
Add cardiologist/ENT/derm/ophthalmology/ortho/podiatry/fertility roles and manage/refer/view matrices so GPs, nurses, lab, pharmacy, and reception get realistic module access beyond single-app specialists. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -5,11 +5,13 @@ namespace App\Services\Care;
|
||||
use App\Models\Member;
|
||||
|
||||
/**
|
||||
* Care RBAC: role → abilities + primary specialty apps.
|
||||
* Care RBAC: role → abilities + specialty manage / refer / view apps.
|
||||
*
|
||||
* Authorization must use can() / primaryAppsFor() — never role string checks
|
||||
* like isDoctor. Legacy `doctor` aliases to general_physician; `nurse` is a
|
||||
* limited floor role (ED / theatre / dialysis nurses are separate).
|
||||
* 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.
|
||||
*/
|
||||
@@ -28,7 +30,7 @@ class CarePermissions
|
||||
/**
|
||||
* 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 modules.
|
||||
* Empty list = no specialty manage.
|
||||
*
|
||||
* @var array<string, list<string>|null>
|
||||
*/
|
||||
@@ -37,14 +39,20 @@ class CarePermissions
|
||||
'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'],
|
||||
'doctor' => ['emergency', 'cardiology', 'pediatrics', 'ent', 'dermatology'], // legacy → GP
|
||||
'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'],
|
||||
'lab_manager' => ['pathology'],
|
||||
'lab_technician' => ['pathology', 'blood_bank'],
|
||||
'lab_manager' => ['pathology', 'blood_bank'],
|
||||
'pathologist' => ['pathology'],
|
||||
'blood_bank_officer' => ['blood_bank'],
|
||||
'blood_bank_manager' => ['blood_bank'],
|
||||
@@ -55,14 +63,59 @@ class CarePermissions
|
||||
'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' => [], // registration / queues — refer via queue.manage
|
||||
'receptionist' => [], // refer only — see roleReferApps
|
||||
'billing_officer' => [],
|
||||
'cashier' => [],
|
||||
'accountant' => [],
|
||||
'department_manager' => null, // department analytics — all enabled modules view
|
||||
'nurse' => [], // legacy limited — no specialty manage
|
||||
'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<string, list<string>>
|
||||
*/
|
||||
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<string, list<string>>
|
||||
*/
|
||||
protected array $roleViewApps = [
|
||||
'lab_technician' => ['radiology'],
|
||||
'lab_manager' => ['radiology'],
|
||||
'pathologist' => ['blood_bank'],
|
||||
'pharmacist' => ['oncology', 'infusion', 'maternity', 'emergency'],
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -106,6 +159,7 @@ class CarePermissions
|
||||
'pathways.manage',
|
||||
'emergency.queue.view', 'emergency.discharge',
|
||||
'cardiology.view', 'pediatrics.view', 'ent.view', 'dermatology.view',
|
||||
'ophthalmology.view',
|
||||
'queue.patient_board',
|
||||
'service_queues.console',
|
||||
],
|
||||
@@ -121,6 +175,7 @@ class CarePermissions
|
||||
'pathways.manage',
|
||||
'emergency.queue.view', 'emergency.discharge',
|
||||
'cardiology.view', 'pediatrics.view', 'ent.view', 'dermatology.view',
|
||||
'ophthalmology.view',
|
||||
'queue.patient_board',
|
||||
'service_queues.console',
|
||||
],
|
||||
@@ -162,6 +217,7 @@ class CarePermissions
|
||||
'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',
|
||||
],
|
||||
@@ -170,6 +226,7 @@ class CarePermissions
|
||||
'pathology.result.approve',
|
||||
'analytics.lab.view',
|
||||
'lab.admin',
|
||||
'blood_bank.manage',
|
||||
],
|
||||
'pathologist' => [
|
||||
'dashboard.view', 'patients.view', 'appointments.view',
|
||||
@@ -252,6 +309,75 @@ class CarePermissions
|
||||
'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',
|
||||
@@ -301,7 +427,7 @@ class CarePermissions
|
||||
'reports.finance.view',
|
||||
'service_queues.console',
|
||||
],
|
||||
// Legacy nurse — floor vitals / assessments only; no specialty modules.
|
||||
// Legacy floor nurse — vaccination / CWC / infusion / maternity manage.
|
||||
'nurse' => [
|
||||
'dashboard.view', 'patients.view', 'appointments.view',
|
||||
'consultations.view', 'vitals.manage',
|
||||
@@ -317,8 +443,10 @@ class CarePermissions
|
||||
'lab_technician', 'lab_manager', 'pathologist',
|
||||
'blood_bank_officer', 'blood_bank_manager',
|
||||
'pharmacist', 'dentist', 'psychiatrist', 'physiotherapist',
|
||||
'midwife', 'pediatrician', 'oncologist', 'dialysis_nurse',
|
||||
'ambulance_staff', 'receptionist', 'cashier', 'billing_officer',
|
||||
'midwife', 'pediatrician', 'oncologist',
|
||||
'cardiologist', 'ent_specialist', 'dermatologist', 'ophthalmologist',
|
||||
'orthopedist', 'podiatrist', 'fertility_specialist',
|
||||
'dialysis_nurse', 'ambulance_staff', 'receptionist', 'cashier', 'billing_officer',
|
||||
'nurse',
|
||||
];
|
||||
|
||||
@@ -327,6 +455,8 @@ class CarePermissions
|
||||
'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',
|
||||
@@ -381,7 +511,7 @@ class CarePermissions
|
||||
}
|
||||
|
||||
/**
|
||||
* Primary specialty module keys for this member's role.
|
||||
* Primary specialty module keys for this member's role (manage).
|
||||
*
|
||||
* @return list<string>|null null = unrestricted specialty (admins / dept manager use other rules)
|
||||
*/
|
||||
@@ -403,6 +533,41 @@ class CarePermissions
|
||||
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.
|
||||
*/
|
||||
@@ -485,6 +650,8 @@ class CarePermissions
|
||||
'emergency_physician', 'general_physician', 'doctor', 'surgeon',
|
||||
'radiologist', 'pathologist', 'dentist', 'psychiatrist',
|
||||
'pediatrician', 'oncologist', 'physiotherapist', 'midwife',
|
||||
'cardiologist', 'ent_specialist', 'dermatologist', 'ophthalmologist',
|
||||
'orthopedist', 'podiatrist', 'fertility_specialist',
|
||||
], true);
|
||||
}
|
||||
|
||||
|
||||
@@ -358,7 +358,8 @@ class SpecialtyModuleService
|
||||
*/
|
||||
public function memberCanView(Organization $organization, ?Member $member, string $key): bool
|
||||
{
|
||||
if ($this->memberCanManage($organization, $member, $key)) {
|
||||
if ($this->memberCanManage($organization, $member, $key)
|
||||
|| $this->memberCanRefer($organization, $member, $key)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -380,14 +381,14 @@ class SpecialtyModuleService
|
||||
return $this->specialistBelongsToModule($organization, $member, $key);
|
||||
}
|
||||
|
||||
// Legacy doctor (non-desk) / GP: keep refer/view on restricted modules
|
||||
// outside primary manage via config view_roles when role is doctor/GP.
|
||||
if (in_array((string) $member->role, ['doctor', 'general_physician'], true)
|
||||
&& ! in_array($key, $primaryApps, true)) {
|
||||
$definition = $this->definition($key);
|
||||
if ($definition && $this->roleListed($member, $definition['view_roles'] ?? [])) {
|
||||
return true;
|
||||
}
|
||||
if (in_array($key, $permissions->viewAppsFor($member), true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Secondary: config view_roles (docs + legacy allowlists).
|
||||
$definition = $this->definition($key);
|
||||
if ($definition && $this->roleListed($member, $definition['view_roles'] ?? [])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array($key, $primaryApps, true);
|
||||
@@ -428,12 +429,14 @@ class SpecialtyModuleService
|
||||
return $this->specialistBelongsToModule($organization, $member, $key);
|
||||
}
|
||||
|
||||
if (in_array((string) $member->role, ['doctor', 'general_physician'], true)
|
||||
&& ! in_array($key, $primaryApps, true)) {
|
||||
$definition = $this->definition($key);
|
||||
if ($definition && $this->roleListed($member, $definition['refer_roles'] ?? [])) {
|
||||
return true;
|
||||
}
|
||||
if (in_array($key, $permissions->referAppsFor($member), true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Secondary: config refer_roles (docs + legacy allowlists).
|
||||
$definition = $this->definition($key);
|
||||
if ($definition && $this->roleListed($member, $definition['refer_roles'] ?? [])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return in_array($key, $primaryApps, true);
|
||||
|
||||
Reference in New Issue
Block a user