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);
|
||||
|
||||
@@ -26,6 +26,13 @@ return [
|
||||
'midwife' => 'Midwife',
|
||||
'pediatrician' => 'Pediatrician',
|
||||
'oncologist' => 'Oncologist',
|
||||
'cardiologist' => 'Cardiologist',
|
||||
'ent_specialist' => 'ENT Specialist',
|
||||
'dermatologist' => 'Dermatologist',
|
||||
'ophthalmologist' => 'Ophthalmologist',
|
||||
'orthopedist' => 'Orthopedist',
|
||||
'podiatrist' => 'Podiatrist',
|
||||
'fertility_specialist' => 'Fertility Specialist',
|
||||
'dialysis_nurse' => 'Dialysis Nurse',
|
||||
'ambulance_staff' => 'Ambulance Staff',
|
||||
'nurse' => 'Nurse (legacy)',
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
* Specialty practice modules (Settings → Modules).
|
||||
* Pro-gated via plans.*.features specialty_modules.
|
||||
*
|
||||
* Primary specialty visibility is driven by CarePermissions::primaryAppsFor()
|
||||
* (role → module keys). Config roles / support_roles / view_roles / refer_roles
|
||||
* remain for legacy doctor desk-specialist keyword matching and GP refer/view
|
||||
* outside the primary-app matrix.
|
||||
* Primary specialty visibility is driven by CarePermissions::primaryAppsFor() /
|
||||
* referAppsFor() / viewAppsFor() (role → module keys). Config roles /
|
||||
* support_roles / view_roles / refer_roles remain for legacy doctor desk-specialist
|
||||
* keyword matching and as a secondary allowlist aligned with the matrix.
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
@@ -23,7 +23,17 @@ return [
|
||||
'icon' => 'bolt',
|
||||
'queue_keywords' => ['emerg', 'casualty', 'trauma', 'er', 'a&e'],
|
||||
'access' => 'general',
|
||||
'roles' => ['doctor', 'nurse', 'lab_technician', 'pharmacist', 'receptionist'],
|
||||
'roles' => [
|
||||
'emergency_physician', 'ed_nurse', 'general_physician', 'doctor',
|
||||
'ambulance_staff',
|
||||
],
|
||||
'view_roles' => [
|
||||
'emergency_physician', 'ed_nurse', 'general_physician', 'doctor',
|
||||
'ambulance_staff', 'nurse', 'pharmacist', 'receptionist',
|
||||
],
|
||||
'refer_roles' => [
|
||||
'general_physician', 'doctor', 'nurse', 'pharmacist', 'receptionist',
|
||||
],
|
||||
'default_on_paid_plans' => true,
|
||||
],
|
||||
'blood_bank' => [
|
||||
@@ -37,7 +47,18 @@ return [
|
||||
'icon' => 'droplet',
|
||||
'queue_keywords' => ['blood', 'transfusion', 'crossmatch', 'donor'],
|
||||
'access' => 'general',
|
||||
'roles' => ['blood_bank_manager', 'lab_technician', 'nurse', 'doctor', 'receptionist'],
|
||||
'roles' => [
|
||||
'blood_bank_officer', 'blood_bank_manager', 'lab_technician', 'lab_manager',
|
||||
'emergency_physician', 'ed_nurse', 'surgeon', 'theatre_nurse',
|
||||
],
|
||||
'view_roles' => [
|
||||
'blood_bank_officer', 'blood_bank_manager', 'lab_technician', 'lab_manager',
|
||||
'pathologist', 'emergency_physician', 'ed_nurse', 'surgeon', 'theatre_nurse',
|
||||
'general_physician', 'doctor', 'nurse',
|
||||
],
|
||||
'refer_roles' => [
|
||||
'general_physician', 'doctor', 'nurse',
|
||||
],
|
||||
'default_on_paid_plans' => true,
|
||||
],
|
||||
'dentistry' => [
|
||||
@@ -51,7 +72,9 @@ return [
|
||||
'icon' => 'face-smile',
|
||||
'queue_keywords' => ['dent', 'dental', 'oral'],
|
||||
'access' => 'limited',
|
||||
'roles' => ['doctor', 'nurse', 'receptionist'],
|
||||
'roles' => ['dentist'],
|
||||
'view_roles' => ['dentist', 'general_physician', 'doctor', 'receptionist'],
|
||||
'refer_roles' => ['general_physician', 'doctor', 'receptionist'],
|
||||
'specialist_keywords' => ['dent', 'dental', 'oral', 'odont'],
|
||||
],
|
||||
'ophthalmology' => [
|
||||
@@ -65,7 +88,11 @@ return [
|
||||
'icon' => 'eye',
|
||||
'queue_keywords' => ['eye', 'ophthal', 'optom'],
|
||||
'access' => 'limited',
|
||||
'roles' => ['doctor', 'nurse', 'receptionist'],
|
||||
'roles' => ['ophthalmologist', 'general_physician', 'doctor'],
|
||||
'view_roles' => [
|
||||
'ophthalmologist', 'general_physician', 'doctor', 'receptionist',
|
||||
],
|
||||
'refer_roles' => ['general_physician', 'doctor', 'receptionist'],
|
||||
'specialist_keywords' => ['eye', 'ophthal', 'optom'],
|
||||
],
|
||||
'physiotherapy' => [
|
||||
@@ -79,7 +106,11 @@ return [
|
||||
'icon' => 'arrow-path',
|
||||
'queue_keywords' => ['physio', 'rehab', 'therapy'],
|
||||
'access' => 'limited',
|
||||
'roles' => ['doctor', 'nurse', 'receptionist'],
|
||||
'roles' => ['physiotherapist'],
|
||||
'view_roles' => [
|
||||
'physiotherapist', 'general_physician', 'doctor', 'receptionist',
|
||||
],
|
||||
'refer_roles' => ['general_physician', 'doctor', 'receptionist'],
|
||||
'specialist_keywords' => ['physio', 'rehab', 'therapy'],
|
||||
],
|
||||
'maternity' => [
|
||||
@@ -90,10 +121,16 @@ return [
|
||||
'queue_name' => 'Maternity',
|
||||
'queue_prefix' => 'MAT',
|
||||
'nav_label' => 'Maternity',
|
||||
'icon' => 'heart',
|
||||
'icon' => 'home',
|
||||
'queue_keywords' => ['matern', 'antenatal', 'obstetric'],
|
||||
'access' => 'limited',
|
||||
'roles' => ['doctor', 'nurse', 'pharmacist', 'receptionist'],
|
||||
'roles' => ['midwife', 'nurse'],
|
||||
'view_roles' => [
|
||||
'midwife', 'nurse', 'general_physician', 'doctor', 'pharmacist', 'receptionist',
|
||||
],
|
||||
'refer_roles' => [
|
||||
'general_physician', 'doctor', 'pharmacist', 'receptionist',
|
||||
],
|
||||
'specialist_keywords' => ['matern', 'antenatal', 'obstetric', 'midwif'],
|
||||
],
|
||||
'radiology' => [
|
||||
@@ -107,7 +144,16 @@ return [
|
||||
'icon' => 'camera',
|
||||
'queue_keywords' => ['radio', 'imaging', 'x-ray', 'xray', 'ultrasound', 'ct', 'mri'],
|
||||
'access' => 'general',
|
||||
'roles' => ['doctor', 'nurse', 'lab_technician', 'receptionist'],
|
||||
'roles' => [
|
||||
'radiologist', 'radiographer', 'emergency_physician', 'ed_nurse', 'surgeon',
|
||||
],
|
||||
'view_roles' => [
|
||||
'radiologist', 'radiographer', 'emergency_physician', 'ed_nurse', 'surgeon',
|
||||
'lab_technician', 'lab_manager', 'general_physician', 'doctor',
|
||||
],
|
||||
'refer_roles' => [
|
||||
'general_physician', 'doctor',
|
||||
],
|
||||
'specialist_keywords' => ['radio', 'radiolog', 'imaging'],
|
||||
],
|
||||
'cardiology' => [
|
||||
@@ -121,10 +167,13 @@ return [
|
||||
'icon' => 'heart',
|
||||
'queue_keywords' => ['cardio', 'heart', 'ecg', 'echo'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
'support_roles' => ['nurse'],
|
||||
'view_roles' => ['doctor', 'general_physician', 'nurse', 'lab_technician'],
|
||||
'refer_roles' => ['doctor', 'general_physician', 'nurse'],
|
||||
'roles' => ['cardiologist', 'emergency_physician', 'general_physician', 'doctor'],
|
||||
'support_roles' => ['ed_nurse'],
|
||||
'view_roles' => [
|
||||
'cardiologist', 'emergency_physician', 'general_physician', 'doctor',
|
||||
'ed_nurse',
|
||||
],
|
||||
'refer_roles' => ['general_physician', 'doctor'],
|
||||
'specialist_keywords' => ['cardio', 'cardiology', 'heart', 'ecg'],
|
||||
],
|
||||
'psychiatry' => [
|
||||
@@ -138,10 +187,12 @@ return [
|
||||
'icon' => 'chat-bubble-left-right',
|
||||
'queue_keywords' => ['psych', 'mental', 'behaviour'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
'support_roles' => ['nurse'],
|
||||
'view_roles' => ['doctor', 'general_physician', 'nurse'],
|
||||
'refer_roles' => ['doctor', 'general_physician', 'nurse'],
|
||||
'roles' => ['psychiatrist'],
|
||||
'support_roles' => [],
|
||||
'view_roles' => [
|
||||
'psychiatrist', 'general_physician', 'doctor',
|
||||
],
|
||||
'refer_roles' => ['general_physician', 'doctor'],
|
||||
'specialist_keywords' => ['psych', 'psychiatry', 'mental', 'behaviour'],
|
||||
],
|
||||
'pediatrics' => [
|
||||
@@ -155,7 +206,13 @@ return [
|
||||
'icon' => 'user-group',
|
||||
'queue_keywords' => ['pedia', 'paedia', 'child', 'infant'],
|
||||
'access' => 'general',
|
||||
'roles' => ['doctor', 'nurse', 'receptionist'],
|
||||
'roles' => ['pediatrician', 'general_physician', 'doctor'],
|
||||
'view_roles' => [
|
||||
'pediatrician', 'general_physician', 'doctor', 'nurse', 'receptionist',
|
||||
],
|
||||
'refer_roles' => [
|
||||
'general_physician', 'doctor', 'nurse', 'receptionist',
|
||||
],
|
||||
// Avoid bare "child" — collides with Child Welfare Clinic specialty labels.
|
||||
'specialist_keywords' => ['pedia', 'paedia', 'pediatric', 'paediatric', 'infant'],
|
||||
],
|
||||
@@ -170,10 +227,12 @@ return [
|
||||
'icon' => 'cube',
|
||||
'queue_keywords' => ['ortho', 'fracture', 'bone', 'joint'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
'support_roles' => ['nurse'],
|
||||
'view_roles' => ['doctor', 'general_physician', 'nurse'],
|
||||
'refer_roles' => ['doctor', 'general_physician', 'nurse'],
|
||||
'roles' => ['orthopedist'],
|
||||
'support_roles' => [],
|
||||
'view_roles' => [
|
||||
'orthopedist', 'general_physician', 'doctor',
|
||||
],
|
||||
'refer_roles' => ['general_physician', 'doctor'],
|
||||
'specialist_keywords' => ['ortho', 'orthopedic', 'orthopaedic', 'fracture'],
|
||||
],
|
||||
'ent' => [
|
||||
@@ -187,10 +246,12 @@ return [
|
||||
'icon' => 'speaker-wave',
|
||||
'queue_keywords' => ['ent', 'ear', 'nose', 'throat', 'otolaryng'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
'support_roles' => ['nurse'],
|
||||
'view_roles' => ['doctor', 'general_physician', 'nurse'],
|
||||
'refer_roles' => ['doctor', 'general_physician', 'nurse'],
|
||||
'roles' => ['ent_specialist', 'general_physician', 'doctor'],
|
||||
'support_roles' => [],
|
||||
'view_roles' => [
|
||||
'ent_specialist', 'general_physician', 'doctor',
|
||||
],
|
||||
'refer_roles' => ['general_physician', 'doctor'],
|
||||
'specialist_keywords' => ['ent', 'otolaryng', 'ear', 'nose', 'throat'],
|
||||
],
|
||||
'oncology' => [
|
||||
@@ -204,10 +265,14 @@ return [
|
||||
'icon' => 'shield-exclamation',
|
||||
'queue_keywords' => ['oncol', 'cancer', 'chemo'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
'roles' => ['oncologist'],
|
||||
'support_roles' => ['nurse'],
|
||||
'view_roles' => ['doctor', 'general_physician', 'nurse', 'pharmacist'],
|
||||
'refer_roles' => ['doctor', 'general_physician', 'nurse'],
|
||||
'view_roles' => [
|
||||
'oncologist', 'general_physician', 'doctor', 'pharmacist',
|
||||
],
|
||||
'refer_roles' => [
|
||||
'general_physician', 'doctor', 'pharmacist',
|
||||
],
|
||||
'specialist_keywords' => ['oncol', 'cancer', 'chemo'],
|
||||
],
|
||||
'renal' => [
|
||||
@@ -221,10 +286,12 @@ return [
|
||||
'icon' => 'circle-stack',
|
||||
'queue_keywords' => ['renal', 'dialysis', 'nephro', 'kidney'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
'support_roles' => ['nurse'],
|
||||
'view_roles' => ['doctor', 'general_physician', 'nurse'],
|
||||
'refer_roles' => ['doctor', 'general_physician', 'nurse'],
|
||||
'roles' => ['dialysis_nurse'],
|
||||
'support_roles' => [],
|
||||
'view_roles' => [
|
||||
'dialysis_nurse', 'general_physician', 'doctor',
|
||||
],
|
||||
'refer_roles' => ['general_physician', 'doctor'],
|
||||
'specialist_keywords' => ['renal', 'nephro', 'dialysis', 'kidney'],
|
||||
],
|
||||
'surgery' => [
|
||||
@@ -238,10 +305,12 @@ return [
|
||||
'icon' => 'wrench-screwdriver',
|
||||
'queue_keywords' => ['surg', 'theatre', 'operat', 'pre-op'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
'support_roles' => ['nurse'],
|
||||
'view_roles' => ['doctor', 'general_physician', 'nurse'],
|
||||
'refer_roles' => ['doctor', 'general_physician', 'nurse'],
|
||||
'roles' => ['surgeon', 'theatre_nurse'],
|
||||
'support_roles' => [],
|
||||
'view_roles' => [
|
||||
'surgeon', 'theatre_nurse', 'general_physician', 'doctor',
|
||||
],
|
||||
'refer_roles' => ['general_physician', 'doctor'],
|
||||
'specialist_keywords' => ['surg', 'theatre', 'operat'],
|
||||
],
|
||||
'vaccination' => [
|
||||
@@ -255,7 +324,11 @@ return [
|
||||
'icon' => 'shield-check',
|
||||
'queue_keywords' => ['vaccin', 'immun', 'epi', 'injection'],
|
||||
'access' => 'general',
|
||||
'roles' => ['doctor', 'nurse', 'receptionist'],
|
||||
'roles' => ['general_physician', 'doctor', 'nurse'],
|
||||
'view_roles' => [
|
||||
'general_physician', 'doctor', 'nurse', 'receptionist',
|
||||
],
|
||||
'refer_roles' => ['general_physician', 'doctor', 'receptionist'],
|
||||
'specialist_keywords' => ['vaccin', 'immun', 'epi'],
|
||||
],
|
||||
'pathology' => [
|
||||
@@ -269,7 +342,18 @@ return [
|
||||
'icon' => 'document-magnifying-glass',
|
||||
'queue_keywords' => ['pathol', 'histo', 'cytol', 'biopsy'],
|
||||
'access' => 'general',
|
||||
'roles' => ['lab_technician', 'doctor', 'nurse', 'receptionist'],
|
||||
'roles' => [
|
||||
'lab_technician', 'lab_manager', 'pathologist',
|
||||
'emergency_physician', 'ed_nurse', 'surgeon',
|
||||
],
|
||||
'view_roles' => [
|
||||
'lab_technician', 'lab_manager', 'pathologist',
|
||||
'emergency_physician', 'ed_nurse', 'surgeon',
|
||||
'general_physician', 'doctor',
|
||||
],
|
||||
'refer_roles' => [
|
||||
'general_physician', 'doctor',
|
||||
],
|
||||
'specialist_keywords' => ['pathol', 'histo', 'cytol'],
|
||||
],
|
||||
'infusion' => [
|
||||
@@ -283,7 +367,13 @@ return [
|
||||
'icon' => 'beaker',
|
||||
'queue_keywords' => ['infusion', 'iv', 'drip', 'day care'],
|
||||
'access' => 'general',
|
||||
'roles' => ['doctor', 'nurse', 'pharmacist', 'receptionist'],
|
||||
'roles' => ['oncologist', 'nurse'],
|
||||
'view_roles' => [
|
||||
'oncologist', 'nurse', 'general_physician', 'doctor', 'pharmacist',
|
||||
],
|
||||
'refer_roles' => [
|
||||
'general_physician', 'doctor', 'pharmacist',
|
||||
],
|
||||
'specialist_keywords' => ['infusion', 'iv therapy', 'day care'],
|
||||
],
|
||||
'dermatology' => [
|
||||
@@ -297,10 +387,12 @@ return [
|
||||
'icon' => 'swatch',
|
||||
'queue_keywords' => ['derma', 'skin', 'rash'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
'roles' => ['dermatologist', 'general_physician', 'doctor'],
|
||||
'support_roles' => [],
|
||||
'view_roles' => ['doctor', 'general_physician', 'nurse'],
|
||||
'refer_roles' => ['doctor', 'general_physician', 'nurse'],
|
||||
'view_roles' => [
|
||||
'dermatologist', 'general_physician', 'doctor',
|
||||
],
|
||||
'refer_roles' => ['general_physician', 'doctor'],
|
||||
'specialist_keywords' => ['derma', 'skin'],
|
||||
],
|
||||
'podiatry' => [
|
||||
@@ -314,10 +406,12 @@ return [
|
||||
'icon' => 'finger-print',
|
||||
'queue_keywords' => ['podiat', 'foot', 'diabetic foot'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
'roles' => ['podiatrist'],
|
||||
'support_roles' => [],
|
||||
'view_roles' => ['doctor', 'general_physician', 'nurse'],
|
||||
'refer_roles' => ['doctor', 'general_physician', 'nurse'],
|
||||
'view_roles' => [
|
||||
'podiatrist', 'general_physician', 'doctor',
|
||||
],
|
||||
'refer_roles' => ['general_physician', 'doctor'],
|
||||
'specialist_keywords' => ['podiat', 'foot'],
|
||||
],
|
||||
'fertility' => [
|
||||
@@ -331,10 +425,12 @@ return [
|
||||
'icon' => 'sparkles',
|
||||
'queue_keywords' => ['fertil', 'ivf', 'reproduct'],
|
||||
'access' => 'restricted',
|
||||
'roles' => ['doctor'],
|
||||
'roles' => ['fertility_specialist'],
|
||||
'support_roles' => [],
|
||||
'view_roles' => ['doctor', 'general_physician', 'nurse'],
|
||||
'refer_roles' => ['doctor', 'general_physician', 'nurse'],
|
||||
'view_roles' => [
|
||||
'fertility_specialist', 'general_physician', 'doctor',
|
||||
],
|
||||
'refer_roles' => ['general_physician', 'doctor'],
|
||||
'specialist_keywords' => ['fertil', 'ivf', 'reproduct'],
|
||||
],
|
||||
'child_welfare' => [
|
||||
@@ -348,7 +444,11 @@ return [
|
||||
'icon' => 'gift',
|
||||
'queue_keywords' => ['cwc', 'child welfare', 'well baby', 'well-child', 'welfare clinic'],
|
||||
'access' => 'general',
|
||||
'roles' => ['doctor', 'nurse', 'receptionist'],
|
||||
'roles' => ['general_physician', 'doctor', 'nurse'],
|
||||
'view_roles' => [
|
||||
'general_physician', 'doctor', 'nurse', 'receptionist',
|
||||
],
|
||||
'refer_roles' => ['general_physician', 'doctor', 'receptionist'],
|
||||
// Prefer multi-word / CWC stems — do not use bare "child" (steals pediatrics).
|
||||
'specialist_keywords' => ['child welfare', 'cwc', 'welfare clinic', 'well baby', 'well-child'],
|
||||
],
|
||||
@@ -364,7 +464,13 @@ return [
|
||||
// Avoid ED stems (emergency, casualty, trauma, er, a&e) — those belong to emergency.
|
||||
'queue_keywords' => ['ambulance', 'ems', 'paramedic', 'prehospital', 'ambulance service'],
|
||||
'access' => 'general',
|
||||
'roles' => ['doctor', 'nurse', 'receptionist'],
|
||||
'roles' => ['ambulance_staff'],
|
||||
'view_roles' => [
|
||||
'ambulance_staff', 'general_physician', 'doctor', 'receptionist',
|
||||
],
|
||||
'refer_roles' => [
|
||||
'general_physician', 'doctor', 'receptionist',
|
||||
],
|
||||
'specialist_keywords' => ['ambulance', 'ems', 'paramedic', 'prehospital'],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -123,7 +123,7 @@ class CareNurseOrgAccessTest extends TestCase
|
||||
'owner_ref' => $owner->public_id,
|
||||
'organization_id' => $employer->id,
|
||||
'user_ref' => strtolower($nurse->email),
|
||||
'role' => 'nurse',
|
||||
'role' => 'ed_nurse',
|
||||
]);
|
||||
|
||||
$branch = Branch::create([
|
||||
@@ -195,7 +195,7 @@ class CareNurseOrgAccessTest extends TestCase
|
||||
|
||||
$nurse = User::create([
|
||||
'public_id' => 'er-docs-nurse',
|
||||
'name' => 'Nurse',
|
||||
'name' => 'ED Nurse',
|
||||
'email' => 'er-docs-nurse@example.com',
|
||||
]);
|
||||
|
||||
@@ -222,7 +222,7 @@ class CareNurseOrgAccessTest extends TestCase
|
||||
'owner_ref' => $owner->public_id,
|
||||
'organization_id' => $organization->id,
|
||||
'user_ref' => $nurse->public_id,
|
||||
'role' => 'nurse',
|
||||
'role' => 'ed_nurse',
|
||||
]);
|
||||
|
||||
$branch = Branch::create([
|
||||
@@ -299,8 +299,8 @@ class CareNurseOrgAccessTest extends TestCase
|
||||
|
||||
$organization = Organization::create([
|
||||
'owner_ref' => $owner->public_id,
|
||||
'name' => 'Cardio Clinic',
|
||||
'slug' => 'cardio-docs-clinic',
|
||||
'name' => 'Rad Clinic',
|
||||
'slug' => 'rad-docs-clinic',
|
||||
'settings' => [
|
||||
'onboarded' => true,
|
||||
'plan' => 'pro',
|
||||
@@ -330,9 +330,9 @@ class CareNurseOrgAccessTest extends TestCase
|
||||
]);
|
||||
|
||||
$modules = app(SpecialtyModuleService::class);
|
||||
$modules->activate($organization, $owner->public_id, 'cardiology');
|
||||
$modules->activate($organization, $owner->public_id, 'radiology');
|
||||
|
||||
$dept = Department::query()->where('type', 'cardiology')->where('branch_id', $branch->id)->firstOrFail();
|
||||
$dept = Department::query()->where('type', 'radiology')->where('branch_id', $branch->id)->firstOrFail();
|
||||
|
||||
$patient = Patient::create([
|
||||
'owner_ref' => $owner->public_id,
|
||||
@@ -369,11 +369,12 @@ class CareNurseOrgAccessTest extends TestCase
|
||||
]);
|
||||
|
||||
$labMember = Member::query()->where('user_ref', $lab->public_id)->firstOrFail();
|
||||
$this->assertSame('view', $modules->memberAccessLevel($organization, $labMember, 'cardiology'));
|
||||
// Lab manages pathology/blood bank; radiology is view-only.
|
||||
$this->assertSame('view', $modules->memberAccessLevel($organization, $labMember, 'radiology'));
|
||||
|
||||
$this->actingAs($lab)
|
||||
->get(route('care.specialty.workspace', [
|
||||
'module' => 'cardiology',
|
||||
'module' => 'radiology',
|
||||
'visit' => $visit,
|
||||
'tab' => 'documents',
|
||||
]))
|
||||
|
||||
@@ -95,22 +95,40 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
|
||||
public function test_nurse_and_pharmacist_manage_general_modules(): void
|
||||
{
|
||||
foreach (['vaccination', 'child_welfare', 'maternity'] as $key) {
|
||||
$this->modules->activate($this->organization->fresh(), $this->owner->public_id, $key);
|
||||
}
|
||||
$this->organization->refresh();
|
||||
|
||||
[, $nurse] = $this->makeStaff('nurse', 'nurse1');
|
||||
[, $edNurse] = $this->makeStaff('ed_nurse', 'edn1');
|
||||
[, $pharmacist] = $this->makeStaff('pharmacist', 'rx1');
|
||||
[, $lab] = $this->makeStaff('lab_technician', 'lab1');
|
||||
[, $receptionist] = $this->makeStaff('receptionist', 'rec1');
|
||||
|
||||
// Legacy nurse is limited — no specialty modules.
|
||||
// Legacy floor nurse: vaccination / CWC / infusion / maternity manage; emergency refer only.
|
||||
$this->assertFalse($this->modules->memberCanManage($this->organization, $nurse, 'emergency'));
|
||||
$this->assertTrue($this->modules->memberCanRefer($this->organization, $nurse, 'emergency'));
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $nurse, 'vaccination'));
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $nurse, 'infusion'));
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $nurse, 'maternity'));
|
||||
// ED nurse matrix apps: Emergency, Blood Bank, Radiology, Pathology.
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $edNurse, 'emergency'));
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $edNurse, 'pathology'));
|
||||
// Pharmacist is pharmacy-only (core app) — no specialty modules.
|
||||
// Pharmacist: no specialty manage; refer/view pharmacy-adjacent modules.
|
||||
$this->assertFalse($this->modules->memberCanManage($this->organization, $pharmacist, 'emergency'));
|
||||
$this->assertFalse($this->modules->memberCanManage($this->organization, $pharmacist, 'infusion'));
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $lab, 'pathology'));
|
||||
$this->assertFalse($this->modules->memberCanManage($this->organization, $lab, 'blood_bank'));
|
||||
$this->assertTrue($this->modules->memberCanRefer($this->organization, $pharmacist, 'infusion'));
|
||||
$this->assertTrue($this->modules->memberCanView($this->organization, $pharmacist, 'emergency'));
|
||||
$this->assertFalse($this->modules->memberCanManage($this->organization, $pharmacist, 'cardiology'));
|
||||
// Lab: pathology + blood bank manage; radiology view.
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $lab, 'pathology'));
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $lab, 'blood_bank'));
|
||||
$this->assertSame('view', $this->modules->memberAccessLevel($this->organization, $lab, 'radiology'));
|
||||
// Receptionist: refer only (no specialty manage).
|
||||
$this->assertFalse($this->modules->memberCanManage($this->organization, $receptionist, 'emergency'));
|
||||
$this->assertTrue($this->modules->memberCanRefer($this->organization, $receptionist, 'emergency'));
|
||||
$this->assertTrue($this->modules->memberCanRefer($this->organization, $receptionist, 'dentistry'));
|
||||
}
|
||||
|
||||
public function test_gp_doctor_manages_general_but_only_views_restricted(): void
|
||||
@@ -129,17 +147,33 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
|
||||
$this->assertFalse($this->modules->isDeskSpecialist($this->organization, $member));
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $member, 'emergency'));
|
||||
// Dentistry is not a GP primary app.
|
||||
// Dentistry is not a GP primary app — refer only.
|
||||
$this->assertFalse($this->modules->memberCanManage($this->organization, $member, 'dentistry'));
|
||||
// Cardiology is a GP primary app (manage).
|
||||
$this->assertTrue($this->modules->memberCanRefer($this->organization, $member, 'dentistry'));
|
||||
// Cardiology / ophthalmology are GP primary apps (manage).
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $member, 'cardiology'));
|
||||
// Psychiatry is outside GP apps — refer/view via legacy config for GP.
|
||||
$this->modules->activate($this->organization->fresh(), $this->owner->public_id, 'ophthalmology');
|
||||
$this->organization->refresh();
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $member, 'ophthalmology'));
|
||||
// Psychiatry is outside GP manage — refer/view via roleReferApps.
|
||||
$this->assertFalse($this->modules->memberCanManage($this->organization, $member, 'psychiatry'));
|
||||
$this->assertTrue($this->modules->memberCanView($this->organization, $member, 'psychiatry'));
|
||||
$this->assertTrue($this->modules->memberCanRefer($this->organization, $member, 'psychiatry'));
|
||||
$this->assertSame('refer', $this->modules->memberAccessLevel($this->organization, $member, 'psychiatry'));
|
||||
}
|
||||
|
||||
public function test_dedicated_cardiologist_role_manages_cardiology_only(): void
|
||||
{
|
||||
[, $member] = $this->makeStaff('cardiologist', 'cardio-role');
|
||||
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $member, 'cardiology'));
|
||||
$this->assertSame('manage', $this->modules->memberAccessLevel($this->organization, $member, 'cardiology'));
|
||||
$this->assertSame('none', $this->modules->memberAccessLevel($this->organization, $member, 'emergency'));
|
||||
$this->assertSame('none', $this->modules->memberAccessLevel($this->organization, $member, 'dentistry'));
|
||||
$this->assertTrue(app(\App\Services\Care\CarePermissions::class)->isSingleAppSpecialist($member));
|
||||
$this->assertFalse($this->modules->shouldShowSpecialtyNav($this->organization, $member));
|
||||
}
|
||||
|
||||
public function test_cardiologist_manages_restricted_cardiology(): void
|
||||
{
|
||||
[$doctor, $member] = $this->makeStaff('doctor', 'cardio1');
|
||||
@@ -237,7 +271,7 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
->assertSee('>Specialty</p>', false)
|
||||
->assertSee('Emergency')
|
||||
->assertSee('Cardiology')
|
||||
->assertDontSee('Dentistry');
|
||||
->assertSee('Dentistry'); // GP refer access
|
||||
}
|
||||
|
||||
public function test_dentistry_specialty_does_not_match_ent_keyword(): void
|
||||
@@ -382,9 +416,11 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
->pluck('key')
|
||||
->all();
|
||||
|
||||
// Pharmacist: pharmacy core only — no specialty modules.
|
||||
$this->assertSame([], $enabledForPharmacist);
|
||||
$this->assertFalse($this->modules->shouldShowSpecialtyNav($this->organization, $pharmacistMember));
|
||||
// Pharmacist: pharmacy-adjacent specialty refer/view (multi-app → Specialty nav).
|
||||
$this->assertContains('emergency', $enabledForPharmacist);
|
||||
$this->assertContains('infusion', $enabledForPharmacist);
|
||||
$this->assertNotContains('cardiology', $enabledForPharmacist);
|
||||
$this->assertTrue($this->modules->shouldShowSpecialtyNav($this->organization, $pharmacistMember));
|
||||
|
||||
$this->assertContains('emergency', $enabledForEp);
|
||||
$this->assertContains('radiology', $enabledForEp);
|
||||
@@ -394,7 +430,7 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
$this->actingAs($pharmacistUser)
|
||||
->get(route('care.dashboard'))
|
||||
->assertOk()
|
||||
->assertDontSee('>Specialty</p>', false);
|
||||
->assertSee('>Specialty</p>', false);
|
||||
}
|
||||
|
||||
public function test_manage_role_still_sees_mutate_actions_on_restricted_module(): void
|
||||
@@ -778,8 +814,18 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
$this->assertTrue($permissions->can($lab, 'lab.manage'));
|
||||
$this->assertTrue($permissions->can($lab, 'pathology.result.enter'));
|
||||
$this->assertFalse($permissions->can($lab, 'pathology.result.approve'));
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $lab, 'blood_bank'));
|
||||
$this->assertTrue($permissions->can($labMgr, 'pathology.result.approve'));
|
||||
$this->assertTrue($permissions->can($labMgr, 'lab.catalog.manage'));
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $labMgr, 'blood_bank'));
|
||||
|
||||
[, $cardiologist] = $this->makeStaff('cardiologist', 'matrix-cardio');
|
||||
[, $ophthalmologist] = $this->makeStaff('ophthalmologist', 'matrix-oph');
|
||||
$this->modules->activate($this->organization->fresh(), $this->owner->public_id, 'ophthalmology');
|
||||
$this->organization->refresh();
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $cardiologist, 'cardiology'));
|
||||
$this->assertFalse($this->modules->memberCanManage($this->organization, $cardiologist, 'emergency'));
|
||||
$this->assertTrue($this->modules->memberCanManage($this->organization, $ophthalmologist, 'ophthalmology'));
|
||||
|
||||
$this->assertTrue($permissions->can($billing, 'bills.manage'));
|
||||
$this->assertFalse($permissions->can($billing, 'payments.manage'));
|
||||
|
||||
@@ -54,7 +54,7 @@ class CareSpecialtyClinicalTest extends TestCase
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->owner->public_id,
|
||||
'role' => 'nurse',
|
||||
'role' => 'hospital_admin',
|
||||
]);
|
||||
|
||||
$this->branch = Branch::create([
|
||||
|
||||
@@ -216,16 +216,16 @@ class CareSpecialtyModulesTest extends TestCase
|
||||
Http::fake();
|
||||
app(SpecialtyModuleService::class)->activate($this->organization, $this->owner->public_id, 'dentistry');
|
||||
|
||||
$nurse = User::create([
|
||||
'public_id' => 'modules-nurse',
|
||||
'name' => 'Floor Nurse',
|
||||
'email' => 'modules-nurse@example.com',
|
||||
$receptionist = User::create([
|
||||
'public_id' => 'modules-reception',
|
||||
'name' => 'Front Desk',
|
||||
'email' => 'modules-reception@example.com',
|
||||
]);
|
||||
Member::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $nurse->public_id,
|
||||
'role' => 'nurse',
|
||||
'user_ref' => $receptionist->public_id,
|
||||
'role' => 'receptionist',
|
||||
'branch_id' => $this->branch->id,
|
||||
]);
|
||||
|
||||
@@ -234,7 +234,8 @@ class CareSpecialtyModulesTest extends TestCase
|
||||
->assertOk()
|
||||
->assertSee('Patient flow');
|
||||
|
||||
$this->actingAs($nurse)
|
||||
// Receptionist has refer access to dentistry (registration into specialty queues).
|
||||
$this->actingAs($receptionist)
|
||||
->get(route('care.specialty.show', 'dentistry'))
|
||||
->assertOk()
|
||||
->assertSee('Patient flow');
|
||||
|
||||
@@ -55,7 +55,7 @@ class CareSpecialtyShellTest extends TestCase
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->owner->public_id,
|
||||
'role' => 'nurse',
|
||||
'role' => 'dentist',
|
||||
]);
|
||||
|
||||
$this->branch = Branch::create([
|
||||
@@ -99,6 +99,8 @@ class CareSpecialtyShellTest extends TestCase
|
||||
|
||||
public function test_specialty_shell_overview_and_sections_render(): void
|
||||
{
|
||||
Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'emergency_physician']);
|
||||
|
||||
app(SpecialtyModuleService::class)->activate(
|
||||
$this->organization,
|
||||
$this->owner->public_id,
|
||||
@@ -153,6 +155,8 @@ class CareSpecialtyShellTest extends TestCase
|
||||
|
||||
public function test_workspace_can_add_catalog_service_to_visit_bill(): void
|
||||
{
|
||||
Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'emergency_physician']);
|
||||
|
||||
app(SpecialtyModuleService::class)->activate(
|
||||
$this->organization,
|
||||
$this->owner->public_id,
|
||||
@@ -307,7 +311,7 @@ class CareSpecialtyShellTest extends TestCase
|
||||
|
||||
public function test_workspace_includes_timeline_tab_and_complete_action(): void
|
||||
{
|
||||
Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'doctor']);
|
||||
Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'dentist']);
|
||||
|
||||
app(SpecialtyModuleService::class)->activate(
|
||||
$this->organization,
|
||||
@@ -393,7 +397,7 @@ class CareSpecialtyShellTest extends TestCase
|
||||
|
||||
public function test_called_workspace_shows_call_again_until_session_starts(): void
|
||||
{
|
||||
Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'doctor']);
|
||||
Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'dentist']);
|
||||
|
||||
app(SpecialtyModuleService::class)->activate(
|
||||
$this->organization,
|
||||
|
||||
Reference in New Issue
Block a user