Files
ladill-care/app/Services/Care/CarePermissions.php
T
isaaccladandCursor 45da731561
Deploy Ladill Care / deploy (push) Successful in 39s
Add My ward nurse station for bedside clinical work.
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>
2026-07-20 15:54:26 +00:00

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;
}
}