diff --git a/app/Http/Controllers/Api/InvestigationController.php b/app/Http/Controllers/Api/InvestigationController.php index a841d14..2bc0cad 100644 --- a/app/Http/Controllers/Api/InvestigationController.php +++ b/app/Http/Controllers/Api/InvestigationController.php @@ -143,7 +143,7 @@ class InvestigationController extends Controller public function approve(Request $request, InvestigationRequest $investigation): JsonResponse { - $this->authorizeAbility($request, 'lab.manage'); + $this->authorizeAbility($request, 'pathology.result.approve'); $this->authorizeInvestigation($request, $investigation); $updated = $this->investigations->approve($investigation, $this->ownerRef($request), $this->ownerRef($request)); diff --git a/app/Http/Controllers/Care/BillController.php b/app/Http/Controllers/Care/BillController.php index 2938f45..d84a3ea 100644 --- a/app/Http/Controllers/Care/BillController.php +++ b/app/Http/Controllers/Care/BillController.php @@ -87,9 +87,14 @@ class BillController extends Controller public function callNext(Request $request): RedirectResponse { - $this->authorizeAbility($request, 'bills.manage'); - $organization = $this->organization($request); + $permissions = app(CarePermissions::class); $member = $this->member($request); + abort_unless( + $permissions->can($member, 'payments.manage') + || $permissions->can($member, 'bills.manage'), + 403, + ); + $organization = $this->organization($request); $branchId = $this->branchContext->resolve($request, $organization, $member); abort_unless($branchId > 0, 422); abort_unless($this->queueBridge->isEnabled($organization), 404); @@ -108,7 +113,13 @@ class BillController extends Controller public function serve(Request $request, Bill $bill): RedirectResponse { - $this->authorizeAbility($request, 'bills.manage'); + $permissions = app(CarePermissions::class); + $member = $this->member($request); + abort_unless( + $permissions->can($member, 'payments.manage') + || $permissions->can($member, 'bills.manage'), + 403, + ); $this->authorizeBill($request, $bill); $organization = $this->organization($request); abort_unless($this->queueBridge->isEnabled($organization), 404); diff --git a/app/Http/Controllers/Care/EmergencyWorkspaceController.php b/app/Http/Controllers/Care/EmergencyWorkspaceController.php index 1b327cb..d0dc9d9 100644 --- a/app/Http/Controllers/Care/EmergencyWorkspaceController.php +++ b/app/Http/Controllers/Care/EmergencyWorkspaceController.php @@ -128,7 +128,7 @@ class EmergencyWorkspaceController extends Controller SpecialtyVisitStageService $stages, EmergencyWorkflowService $workflow, ): RedirectResponse { - $this->authorizeAbility($request, 'consultations.manage'); + $this->authorizeAbility($request, 'emergency.discharge'); $this->assertEmergencyManage($request, $modules); $this->assertVisit($request, $visit); diff --git a/app/Http/Controllers/Care/InvestigationController.php b/app/Http/Controllers/Care/InvestigationController.php index 26ddd88..0cacd58 100644 --- a/app/Http/Controllers/Care/InvestigationController.php +++ b/app/Http/Controllers/Care/InvestigationController.php @@ -228,7 +228,7 @@ class InvestigationController extends Controller public function approve(Request $request, InvestigationRequest $investigation): RedirectResponse { - $this->authorizeAbility($request, 'lab.manage'); + $this->authorizeAbility($request, 'pathology.result.approve'); $this->authorizeInvestigation($request, $investigation); $this->investigations->approve($investigation, $this->ownerRef($request), $this->ownerRef($request)); diff --git a/app/Http/Controllers/Care/MemberController.php b/app/Http/Controllers/Care/MemberController.php index 66380e2..4a4004c 100644 --- a/app/Http/Controllers/Care/MemberController.php +++ b/app/Http/Controllers/Care/MemberController.php @@ -8,6 +8,7 @@ use App\Models\Branch; use App\Models\Member; use App\Models\Practitioner; use App\Services\Care\AuditLogger; +use App\Services\Care\CarePermissions; use App\Services\Identity\IdentityTeamClient; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -31,7 +32,7 @@ class MemberController extends Controller ->get(); $adminRoles = ['super_admin', 'hospital_admin']; - $clinicalRoles = ['doctor', 'nurse', 'lab_technician', 'lab_manager', 'pharmacist', 'blood_bank_manager']; + $clinicalRoles = app(CarePermissions::class)->clinicalPractitionerRoles(); $heroStats = [ 'total' => $members->count(), @@ -71,6 +72,7 @@ class MemberController extends Controller 'organization' => $organization, 'branches' => $branches, 'roles' => config('care.roles'), + 'practitionerRoles' => app(CarePermissions::class)->clinicalPractitionerRoles(), 'mailboxOptions' => $mailboxOptions, 'specialties' => Practitioner::specialtyOptions(), ]); @@ -82,11 +84,16 @@ class MemberController extends Controller $organization = $this->organization($request); $owner = $this->ownerRef($request); + $permissions = app(CarePermissions::class); + $needsPractitionerDesk = $permissions->usesPractitionerBranchScope( + new Member(['role' => $request->input('role')]) + ); + $validated = $request->validate([ 'email' => ['required', 'email', 'max:255'], 'role' => ['required', 'string', 'in:'.implode(',', array_keys(config('care.roles')))], 'branch_id' => [ - Rule::requiredIf(fn () => $request->input('role') === 'doctor'), + Rule::requiredIf(fn () => $needsPractitionerDesk), 'nullable', 'integer', 'exists:care_branches,id', @@ -138,7 +145,10 @@ class MemberController extends Controller AuditLogger::record($owner, 'member.invited', $organization->id, $owner, Member::class, $member->id); - $createPractitioner = $request->boolean('create_practitioner', $validated['role'] === 'doctor'); + $createPractitioner = $request->boolean( + 'create_practitioner', + app(CarePermissions::class)->usesPractitionerBranchScope(new Member(['role' => $validated['role']])), + ); if ($createPractitioner) { $name = trim((string) ($validated['practitioner_name'] ?? '')) ?: Str::headline(Str::before($email, '@')); $practitioner = Practitioner::query()->firstOrCreate( diff --git a/app/Http/Controllers/Care/PractitionerController.php b/app/Http/Controllers/Care/PractitionerController.php index 12ba8ad..2164c33 100644 --- a/app/Http/Controllers/Care/PractitionerController.php +++ b/app/Http/Controllers/Care/PractitionerController.php @@ -210,7 +210,7 @@ class PractitionerController extends Controller { return Member::owned($owner) ->where('organization_id', $organizationId) - ->whereIn('role', ['doctor', 'nurse', 'lab_technician', 'lab_manager', 'pharmacist', 'blood_bank_manager', 'hospital_admin', 'super_admin']) + ->whereIn('role', app(\App\Services\Care\CarePermissions::class)->clinicalPractitionerRoles()) ->orderBy('user_ref') ->get(); } diff --git a/app/Services/Care/AssessmentService.php b/app/Services/Care/AssessmentService.php index 591dcd9..0b4353e 100644 --- a/app/Services/Care/AssessmentService.php +++ b/app/Services/Care/AssessmentService.php @@ -311,8 +311,16 @@ class AssessmentService public function defaultCaptureRoles(string $category): array { return match ($category) { - AssessmentTemplate::CATEGORY_DISEASE => ['doctor'], - default => ['doctor', 'nurse'], + AssessmentTemplate::CATEGORY_DISEASE => [ + 'doctor', 'general_physician', 'emergency_physician', 'surgeon', + 'pediatrician', 'oncologist', 'psychiatrist', 'dentist', 'pathologist', + ], + default => [ + 'doctor', 'general_physician', 'emergency_physician', 'surgeon', + 'pediatrician', 'oncologist', 'psychiatrist', 'dentist', 'pathologist', + 'nurse', 'ed_nurse', 'theatre_nurse', 'dialysis_nurse', 'midwife', + 'physiotherapist', 'ambulance_staff', 'radiographer', + ], }; } diff --git a/app/Services/Care/BranchContext.php b/app/Services/Care/BranchContext.php index 02efa15..81c8fd5 100644 --- a/app/Services/Care/BranchContext.php +++ b/app/Services/Care/BranchContext.php @@ -29,20 +29,13 @@ class BranchContext return false; } - if ($member->role === 'doctor') { + if ($member->role === 'doctor' || app(CarePermissions::class)->usesPractitionerBranchScope($member)) { return count($this->organizations->doctorAssignedBranchIds($member)) > 1; } // Other site clinicians stay on their assigned branch — never a picker. - if (in_array($member->role, [ - 'nurse', - 'pharmacist', - 'lab_technician', - 'lab_manager', - 'blood_bank_manager', - 'cashier', - 'receptionist', - ], true)) { + if (in_array($member->role, app(CarePermissions::class)->floorCareRoles(), true) + && ! app(CarePermissions::class)->usesPractitionerBranchScope($member)) { return false; } diff --git a/app/Services/Care/CarePermissions.php b/app/Services/Care/CarePermissions.php index 2be5d0b..1c357a3 100644 --- a/app/Services/Care/CarePermissions.php +++ b/app/Services/Care/CarePermissions.php @@ -4,86 +4,332 @@ namespace App\Services\Care; use App\Models\Member; +/** + * Care RBAC: role → abilities + primary specialty 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). + * + * Inheritance: lab_manager ⊃ lab_technician; blood_bank_manager ⊃ blood_bank_officer. + */ class CarePermissions { /** - * Role abilities. + * Child role abilities are merged into the parent (parent wins on duplicates). * - * Lab catalog pricing uses `lab.catalog.manage` (admins via `*`, plus `lab_manager`). - * Lab technicians keep `lab.manage` for queue / sample / process / results — - * not catalog CRUD or price edits. - * - * Blood Bank operational inventory uses `blood_bank.manage` (managers + admins). - * Clinical visit stage/issue for managers also requires specialty module manage. + * @var array> + */ + 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 modules. * + * @var array|null> + */ + protected array $rolePrimaryApps = [ + 'super_admin' => null, // all via admin + 'hospital_admin' => null, + 'emergency_physician' => ['emergency', 'radiology', 'pathology', 'blood_bank', 'cardiology'], + 'ed_nurse' => ['emergency', 'blood_bank', 'radiology', 'pathology'], + 'general_physician' => ['emergency', 'cardiology', 'pediatrics', 'ent', 'dermatology'], + 'doctor' => ['emergency', 'cardiology', 'pediatrics', 'ent', 'dermatology'], // legacy → GP + 'surgeon' => ['surgery', 'radiology', 'pathology', 'blood_bank'], + 'theatre_nurse' => ['surgery', 'blood_bank'], + 'radiologist' => ['radiology'], + 'radiographer' => ['radiology'], + 'lab_technician' => ['pathology'], + 'lab_manager' => ['pathology'], + 'pathologist' => ['pathology'], + 'blood_bank_officer' => ['blood_bank'], + 'blood_bank_manager' => ['blood_bank'], + 'pharmacist' => [], // Pharmacy is core, not a specialty module + 'dentist' => ['dentistry'], + 'psychiatrist' => ['psychiatry'], + 'physiotherapist' => ['physiotherapy'], + 'midwife' => ['maternity'], + 'pediatrician' => ['pediatrics'], + 'oncologist' => ['oncology', 'infusion'], + 'dialysis_nurse' => ['renal'], + 'ambulance_staff' => ['ambulance', 'emergency'], + 'receptionist' => [], // registration / queues — refer via queue.manage + 'billing_officer' => [], + 'cashier' => [], + 'accountant' => [], + 'department_manager' => null, // department analytics — all enabled modules view + 'nurse' => [], // legacy limited — no specialty manage + ]; + + /** * @var array> */ protected array $roleAbilities = [ 'super_admin' => ['*'], 'hospital_admin' => ['*'], - 'receptionist' => [ - 'dashboard.view', 'patients.view', 'patients.manage', - 'appointments.view', 'appointments.manage', 'queue.manage', + + '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', + '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', + 'queue.patient_board', 'service_queues.console', ], - 'nurse' => [ + 'surgeon' => [ 'dashboard.view', 'patients.view', 'appointments.view', - 'consultations.view', 'vitals.manage', + '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', + 'dashboard.view', 'patients.view', + 'lab.view', 'lab.manage', + 'pathology.view', 'pathology.result.enter', + // cannot: pathology.result.approve, lab.catalog.manage 'service_queues.console', ], 'lab_manager' => [ - 'dashboard.view', 'patients.view', 'lab.view', 'lab.manage', 'lab.catalog.manage', + 'pathology.result.approve', + 'analytics.lab.view', + 'lab.admin', + ], + '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', ], - 'pharmacist' => [ - 'dashboard.view', 'patients.view', 'prescriptions.view', 'prescriptions.dispense', - 'pharmacy.view', 'pharmacy.manage', + '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', - 'blood_bank.view', 'blood_bank.manage', + 'prescriptions.view', 'prescriptions.dispense', + 'pharmacy.view', 'pharmacy.manage', 'service_queues.console', ], - // Cashiers run the Billing desk (bills / payments). They do not get - // Ladill POS app access — see filterAllowedAppSlugs() / DemoWorld. + 'dentist' => [ + 'dashboard.view', 'patients.view', 'appointments.view', + 'consultations.view', 'consultations.manage', + 'investigations.request', 'prescriptions.manage', + 'vitals.manage', + 'assessments.view', 'assessments.capture', 'assessments.manage', + 'pathways.manage', + 'dentistry.view', 'dentistry.manage', + 'service_queues.console', + ], + 'psychiatrist' => [ + 'dashboard.view', 'patients.view', 'appointments.view', + 'consultations.view', 'consultations.manage', + 'prescriptions.manage', + 'assessments.view', 'assessments.capture', 'assessments.manage', + 'pathways.manage', + 'psychiatry.view', 'psychiatry.manage', + 'service_queues.console', + ], + 'physiotherapist' => [ + 'dashboard.view', 'patients.view', 'appointments.view', + 'consultations.view', 'consultations.manage', + 'vitals.manage', + 'assessments.view', 'assessments.capture', + 'physiotherapy.view', 'physiotherapy.manage', + 'service_queues.console', + ], + 'midwife' => [ + 'dashboard.view', 'patients.view', 'appointments.view', + 'consultations.view', 'consultations.manage', + 'vitals.manage', + 'assessments.view', 'assessments.capture', 'assessments.manage', + 'maternity.view', 'maternity.manage', + 'service_queues.console', + ], + 'pediatrician' => [ + 'dashboard.view', 'patients.view', 'appointments.view', + 'consultations.view', 'consultations.manage', + 'investigations.request', 'prescriptions.manage', + 'vitals.manage', + 'assessments.view', 'assessments.capture', 'assessments.manage', + 'pathways.manage', + 'pediatrics.view', 'pediatrics.manage', + 'queue.patient_board', + 'service_queues.console', + ], + 'oncologist' => [ + 'dashboard.view', 'patients.view', 'appointments.view', + 'consultations.view', 'consultations.manage', + 'investigations.request', 'prescriptions.manage', + 'vitals.manage', + 'assessments.view', 'assessments.capture', 'assessments.manage', + 'pathways.manage', + 'oncology.view', 'oncology.manage', + 'infusion.view', 'infusion.manage', + 'service_queues.console', + ], + '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', 'bills.manage', 'payments.manage', + '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', + '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 nurse — floor vitals / assessments only; no specialty modules. + 'nurse' => [ + 'dashboard.view', 'patients.view', 'appointments.view', + 'consultations.view', 'vitals.manage', + 'assessments.view', 'assessments.capture', + 'service_queues.console', + ], ]; - /** @var list */ - protected array $adminAbilities = [ - 'admin.branches.view', 'admin.branches.manage', - 'admin.departments.view', 'admin.departments.manage', - 'admin.practitioners.view', 'admin.practitioners.manage', - 'admin.members.view', 'admin.members.manage', - 'settings.view', 'settings.manage', - 'lab.catalog.manage', - 'blood_bank.view', 'blood_bank.manage', - 'devices.view', 'devices.manage', - 'displays.view', 'displays.manage', - 'audit.view', 'audit.export', + /** Clinical roles that staff the floor (queues, specialty desks, counters). */ + protected array $floorCareRoles = [ + 'emergency_physician', 'ed_nurse', 'general_physician', 'doctor', + 'surgeon', 'theatre_nurse', 'radiologist', 'radiographer', + 'lab_technician', 'lab_manager', 'pathologist', + 'blood_bank_officer', 'blood_bank_manager', + 'pharmacist', 'dentist', 'psychiatrist', 'physiotherapist', + 'midwife', 'pediatrician', 'oncologist', 'dialysis_nurse', + 'ambulance_staff', 'receptionist', 'cashier', 'billing_officer', + 'nurse', + ]; + + /** Roles that get practitioner desks / multi-branch clinical assignment. */ + protected array $clinicalPractitionerRoles = [ + 'emergency_physician', 'general_physician', 'doctor', 'surgeon', + 'radiologist', 'pathologist', 'dentist', 'psychiatrist', + 'physiotherapist', 'midwife', 'pediatrician', 'oncologist', + '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 @@ -92,7 +338,7 @@ class CarePermissions return false; } - $abilities = $this->roleAbilities[$member->role] ?? []; + $abilities = $this->resolvedAbilities((string) $member->role); if (in_array('*', $abilities, true)) { return true; @@ -101,11 +347,88 @@ class CarePermissions return in_array($ability, $abilities, true); } + /** + * @return list + */ + public function resolvedAbilities(string $role): array + { + $seen = []; + $merged = []; + + $stack = [$role]; + while ($stack !== []) { + $current = array_pop($stack); + if (isset($seen[$current])) { + continue; + } + $seen[$current] = true; + + foreach ($this->roleInherits[$current] ?? [] as $parent) { + $stack[] = $parent; + } + + foreach ($this->roleAbilities[$current] ?? [] as $ability) { + $merged[$ability] = true; + } + } + + return array_keys($merged); + } + public function isAdmin(?Member $member): bool { return $member !== null && in_array($member->role, ['super_admin', 'hospital_admin'], true); } + /** + * Primary specialty module keys for this member's role. + * + * @return list|null null = unrestricted specialty (admins / dept manager use other rules) + */ + public function primaryAppsFor(?Member $member): ?array + { + if ($member === null) { + return []; + } + + if ($this->isAdmin($member)) { + return null; + } + + $role = (string) $member->role; + if (! array_key_exists($role, $this->rolePrimaryApps)) { + return []; + } + + return $this->rolePrimaryApps[$role]; + } + + /** + * 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. @@ -116,27 +439,53 @@ class CarePermissions return false; } - return in_array($member->role, [ - 'doctor', - 'nurse', - 'lab_technician', - 'lab_manager', - 'pharmacist', - 'blood_bank_manager', - 'receptionist', - 'cashier', - ], true); + return in_array($member->role, $this->floorCareRoles, true); } /** * Dedicated patient-flow board (GP Queue home / call-next board). - * Doctors only — nurses, reception, pharmacy, and other floor roles use - * specialty module lists and service queues instead. - * Facility admins manage settings/staff and do not staff this board. + * Permission-gated — not a role string check. Facility admins do not staff this board. */ public function canAccessPatientQueue(?Member $member): bool { - return $member !== null && $member->role === 'doctor'; + if ($member === null || $this->isAdmin($member)) { + return false; + } + + return $this->can($member, 'queue.patient_board'); + } + + /** + * @return list + */ + public function clinicalPractitionerRoles(): array + { + return $this->clinicalPractitionerRoles; + } + + /** + * @return list + */ + public function floorCareRoles(): array + { + return $this->floorCareRoles; + } + + /** + * Roles that may switch / multi-site like legacy doctors (practitioner branches). + */ + public function usesPractitionerBranchScope(?Member $member): bool + { + if ($member === null) { + return false; + } + + return $this->can($member, 'queue.patient_board') + || in_array($member->role, [ + 'emergency_physician', 'general_physician', 'doctor', 'surgeon', + 'radiologist', 'pathologist', 'dentist', 'psychiatrist', + 'pediatrician', 'oncologist', 'physiotherapist', 'midwife', + ], true); } /** diff --git a/app/Services/Care/CareQueueBridge.php b/app/Services/Care/CareQueueBridge.php index efb2836..b8a523e 100644 --- a/app/Services/Care/CareQueueBridge.php +++ b/app/Services/Care/CareQueueBridge.php @@ -820,7 +820,7 @@ class CareQueueBridge fn ($p) => ($p['kind'] ?? '') === 'practitioner' && (int) ($p['ref_id'] ?? 0) === $practitionerId ); } - if ($member && $member->role === 'doctor') { + if ($member && app(CarePermissions::class)->usesPractitionerBranchScope($member)) { $prac = Practitioner::owned($organization->owner_ref) ->where('organization_id', $organization->id) ->where('branch_id', $branchId) diff --git a/app/Services/Care/DemoTenantSeeder.php b/app/Services/Care/DemoTenantSeeder.php index 2e4c356..f3c7a6b 100644 --- a/app/Services/Care/DemoTenantSeeder.php +++ b/app/Services/Care/DemoTenantSeeder.php @@ -920,8 +920,10 @@ class DemoTenantSeeder // One doctor = one branch. Only link the specialty desk on their home site. $assignMember = $member && $homeBranchId > 0 && (int) $branch->id === $homeBranchId; $stableRef = sprintf('demo-specialty-%s-%s', $key, $branch->id); - $doctorName = (string) (\App\Support\DemoWorld::SPECIALTY_DOCTORS[$key] - ?? ('Dr. '.($definition['label'] ?? ucfirst($key)))); + $specialtyMeta = \App\Support\DemoWorld::SPECIALTY_DOCTORS[$key] ?? null; + $doctorName = is_array($specialtyMeta) + ? (string) ($specialtyMeta['name'] ?? ('Dr. '.($definition['label'] ?? ucfirst($key)))) + : (string) ($specialtyMeta ?? ('Dr. '.($definition['label'] ?? ucfirst($key)))); if ($assignMember && is_array($staff) && filled($staff['name'] ?? null)) { $practitionerName = (string) $staff['name']; } elseif ($assignMember) { diff --git a/app/Services/Care/OrganizationResolver.php b/app/Services/Care/OrganizationResolver.php index 59a749f..87d6eb0 100644 --- a/app/Services/Care/OrganizationResolver.php +++ b/app/Services/Care/OrganizationResolver.php @@ -282,7 +282,7 @@ class OrganizationResolver return null; } - if ($member->role === 'doctor') { + if (app(CarePermissions::class)->usesPractitionerBranchScope($member)) { return $this->doctorAssignedBranchIds($member); } @@ -356,7 +356,7 @@ class OrganizationResolver return null; } - if ($member->role === 'doctor') { + if (app(CarePermissions::class)->usesPractitionerBranchScope($member)) { $ids = $this->doctorAssignedBranchIds($member); if ($ids === []) { return 0; diff --git a/app/Services/Care/SpecialtyModuleService.php b/app/Services/Care/SpecialtyModuleService.php index ced5f63..a89ca5e 100644 --- a/app/Services/Care/SpecialtyModuleService.php +++ b/app/Services/Care/SpecialtyModuleService.php @@ -200,10 +200,10 @@ class SpecialtyModuleService /** * Whether the sidebar should show the Specialty nav group. * - * Desk specialists only receive their own matching module(s), so a - * Specialty → Dentistry (etc.) group is redundant — they use dashboard - * cards / direct routes instead. GPs, nurses, and other multi-module - * roles keep the group for referral and cross-module access. + * Single-app specialists (dentist, radiologist, …) and legacy desk + * specialists only receive their own module(s), so a Specialty group is + * redundant — they use dashboard cards / direct routes instead. + * Multi-app clinical roles keep the group. */ public function shouldShowSpecialtyNav(Organization $organization, ?Member $member): bool { @@ -216,6 +216,11 @@ class SpecialtyModuleService return false; } + $permissions = app(CarePermissions::class); + if ($permissions->isSingleAppSpecialist($member)) { + return false; + } + if ($this->isDeskSpecialist($organization, $member)) { return false; } @@ -283,6 +288,9 @@ class SpecialtyModuleService /** * Full clinical / queue edit access for the module. + * + * Role primary apps (CarePermissions) are the source of truth for the + * RBAC matrix. Legacy `doctor` desk specialists still use keyword matching. */ public function memberCanManage(Organization $organization, ?Member $member, string $key): bool { @@ -290,7 +298,8 @@ class SpecialtyModuleService return false; } - if (app(CarePermissions::class)->isAdmin($member)) { + $permissions = app(CarePermissions::class); + if ($permissions->isAdmin($member)) { return true; } @@ -299,6 +308,24 @@ class SpecialtyModuleService return false; } + // Department managers get analytics view, not clinical manage. + if ((string) $member->role === 'department_manager') { + return false; + } + + $primaryApps = $permissions->primaryAppsFor($member); + + // Matrix-driven roles (including GP / EP / specialists with app lists). + if (is_array($primaryApps)) { + // Legacy doctor + specialty desk: only matching modules. + if ((string) $member->role === 'doctor' + && $this->isDeskSpecialist($organization, $member)) { + return $this->specialistBelongsToModule($organization, $member, $key); + } + + return in_array($key, $primaryApps, true); + } + // Desk specialists only manage modules that match their specialty / assignment. if ($this->isDeskSpecialist($organization, $member) && ! $this->specialistBelongsToModule($organization, $member, $key)) { @@ -314,7 +341,7 @@ class SpecialtyModuleService if ($this->roleListed($member, $supportRoles)) { return true; } - if ($role === 'doctor' && $this->roleListed($member, $manageRoles)) { + if (in_array($role, ['doctor', 'general_physician'], true) && $this->roleListed($member, $manageRoles)) { return $this->specialistBelongsToModule($organization, $member, $key); } @@ -339,6 +366,33 @@ class SpecialtyModuleService return false; } + $permissions = app(CarePermissions::class); + if ((string) $member->role === 'department_manager') { + return true; + } + + $primaryApps = $permissions->primaryAppsFor($member); + if (is_array($primaryApps)) { + // Matrix roles: view only what they can manage (no cross-module GP view + // unless listed — general_physician gets cardiology manage via apps). + if ((string) $member->role === 'doctor' + && $this->isDeskSpecialist($organization, $member)) { + 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; + } + } + + return in_array($key, $primaryApps, true); + } + // Desk specialists do not get cross-module view; only their matching desks. if ($this->isDeskSpecialist($organization, $member) && ! $this->specialistBelongsToModule($organization, $member, $key)) { @@ -366,6 +420,25 @@ class SpecialtyModuleService return false; } + $permissions = app(CarePermissions::class); + $primaryApps = $permissions->primaryAppsFor($member); + if (is_array($primaryApps)) { + if ((string) $member->role === 'doctor' + && $this->isDeskSpecialist($organization, $member)) { + 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; + } + } + + return in_array($key, $primaryApps, true); + } + // Desk specialists do not get cross-module referral nav; GPs keep refer_roles. if ($this->isDeskSpecialist($organization, $member) && ! $this->specialistBelongsToModule($organization, $member, $key)) { @@ -381,8 +454,9 @@ class SpecialtyModuleService } /** - * Doctor with a specialty desk: non-GP specialty matching a catalog module, or - * assigned to a provisioned specialty department. GPs and non-doctors are false. + * Legacy doctor with a specialty desk: non-GP specialty matching a catalog + * module, or assigned to a provisioned specialty department. + * Dedicated specialist roles use primary apps instead. */ public function isDeskSpecialist(Organization $organization, Member $member): bool { @@ -391,7 +465,7 @@ class SpecialtyModuleService return $this->deskSpecialistCache[$cacheKey]; } - if ((string) $member->role !== 'doctor') { + if (! in_array((string) $member->role, ['doctor'], true)) { return $this->deskSpecialistCache[$cacheKey] = false; } diff --git a/app/Support/DemoWorld.php b/app/Support/DemoWorld.php index 5dd8a57..dc0a15b 100644 --- a/app/Support/DemoWorld.php +++ b/app/Support/DemoWorld.php @@ -187,34 +187,52 @@ final class DemoWorld * }>> */ /** - * Care specialty module doctors (Pro / Enterprise). Emails: demo-{tier}-{key}@ladill.com + * Care specialty module clinicians (Pro / Enterprise). + * Emails: demo-{tier}-{key}@ladill.com + * Roles follow the Care RBAC matrix when a dedicated slug exists; + * remaining modules stay on legacy `doctor` + desk specialty keywords. * - * @var array module key => display name + * @var array */ public const SPECIALTY_DOCTORS = [ - 'emergency' => 'Dr. Kojo Emergency', - 'blood_bank' => 'Dr. Ama Blood Bank', - 'dentistry' => 'Dr. Ama Dental', - 'ophthalmology' => 'Dr. Kofi Eye', - 'physiotherapy' => 'Dr. Efua Physio', - 'maternity' => 'Dr. Abena Maternity', - 'radiology' => 'Dr. Yaw Radiology', - 'cardiology' => 'Dr. Kwesi Cardiology', - 'psychiatry' => 'Dr. Adwoa Psychiatry', - 'pediatrics' => 'Dr. Fiifi Pediatrics', - 'orthopedics' => 'Dr. Kojo Orthopedics', - 'ent' => 'Dr. Esi ENT', - 'oncology' => 'Dr. Mansa Oncology', - 'renal' => 'Dr. Nana Renal', - 'surgery' => 'Dr. Kwadwo Surgery', - 'vaccination' => 'Dr. Afia Vaccination', - 'pathology' => 'Dr. Yaw Pathology', - 'infusion' => 'Dr. Akosua Infusion', - 'dermatology' => 'Dr. Ama Dermatology', - 'podiatry' => 'Dr. Kofi Podiatry', - 'fertility' => 'Dr. Abena Fertility', - 'child_welfare' => 'Dr. Efua Child Welfare', - 'ambulance' => 'Dr. Kojo Ambulance', + 'emergency' => ['name' => 'Dr. Kojo Emergency', 'role' => 'emergency_physician'], + 'blood_bank' => ['name' => 'Ama Blood Bank Officer', 'role' => 'blood_bank_officer'], + 'dentistry' => ['name' => 'Dr. Ama Dental', 'role' => 'dentist'], + 'ophthalmology' => ['name' => 'Dr. Kofi Eye', 'role' => 'doctor'], + 'physiotherapy' => ['name' => 'Dr. Efua Physio', 'role' => 'physiotherapist'], + 'maternity' => ['name' => 'Abena Midwife', 'role' => 'midwife'], + 'radiology' => ['name' => 'Dr. Yaw Radiology', 'role' => 'radiologist'], + 'cardiology' => ['name' => 'Dr. Kwesi Cardiology', 'role' => 'doctor'], + 'psychiatry' => ['name' => 'Dr. Adwoa Psychiatry', 'role' => 'psychiatrist'], + 'pediatrics' => ['name' => 'Dr. Fiifi Pediatrics', 'role' => 'pediatrician'], + 'orthopedics' => ['name' => 'Dr. Kojo Orthopedics', 'role' => 'doctor'], + 'ent' => ['name' => 'Dr. Esi ENT', 'role' => 'doctor'], + 'oncology' => ['name' => 'Dr. Mansa Oncology', 'role' => 'oncologist'], + 'renal' => ['name' => 'Dr. Nana Renal', 'role' => 'doctor'], + 'surgery' => ['name' => 'Dr. Kwadwo Surgery', 'role' => 'surgeon'], + 'vaccination' => ['name' => 'Dr. Afia Vaccination', 'role' => 'doctor'], + 'pathology' => ['name' => 'Dr. Yaw Pathology', 'role' => 'pathologist'], + 'infusion' => ['name' => 'Dr. Akosua Infusion', 'role' => 'doctor'], + 'dermatology' => ['name' => 'Dr. Ama Dermatology', 'role' => 'doctor'], + 'podiatry' => ['name' => 'Dr. Kofi Podiatry', 'role' => 'doctor'], + 'fertility' => ['name' => 'Dr. Abena Fertility', 'role' => 'doctor'], + 'child_welfare' => ['name' => 'Dr. Efua Child Welfare', 'role' => 'doctor'], + 'ambulance' => ['name' => 'Kojo Ambulance', 'role' => 'ambulance_staff'], + ]; + + /** + * Extra Care matrix roles (not covered by SPECIALTY_DOCTORS keys). + * + * @var list + */ + public const CARE_MATRIX_STAFF = [ + ['key' => 'ed-nurse', 'name' => 'Ama ED Nurse', 'role' => 'ed_nurse'], + ['key' => 'theatre-nurse', 'name' => 'Efua Theatre Nurse', 'role' => 'theatre_nurse'], + ['key' => 'radiographer', 'name' => 'Kofi Radiographer', 'role' => 'radiographer'], + ['key' => 'dialysis-nurse', 'name' => 'Abena Dialysis Nurse', 'role' => 'dialysis_nurse'], + ['key' => 'billing-officer', 'name' => 'Yaw Billing Officer', 'role' => 'billing_officer'], + ['key' => 'department-manager', 'name' => 'Adwoa Department Manager', 'role' => 'department_manager'], + ['key' => 'hospital-admin', 'name' => 'Kojo Hospital Admin', 'role' => 'hospital_admin'], ]; public const STAFF = [ @@ -243,7 +261,14 @@ final class DemoWorld 'email' => 'demo-pro-doctor@ladill.com', 'name' => 'Dr. Kwame Mensah (Pro)', 'apps' => ['care'], - 'roles' => ['care' => 'doctor'], + 'roles' => ['care' => 'general_physician'], + ], + [ + 'key' => 'general-physician', + 'email' => 'demo-pro-general-physician@ladill.com', + 'name' => 'Dr. Ama General Physician (Pro)', + 'apps' => ['care'], + 'roles' => ['care' => 'general_physician'], ], [ 'key' => 'nurse', @@ -345,7 +370,14 @@ final class DemoWorld 'email' => 'demo-enterprise-doctor@ladill.com', 'name' => 'Dr. Kwame Mensah', 'apps' => ['care'], - 'roles' => ['care' => 'doctor'], + 'roles' => ['care' => 'general_physician'], + ], + [ + 'key' => 'general-physician', + 'email' => 'demo-enterprise-general-physician@ladill.com', + 'name' => 'Dr. Ama General Physician', + 'apps' => ['care'], + 'roles' => ['care' => 'general_physician'], ], [ 'key' => 'nurse', @@ -572,7 +604,7 @@ final class DemoWorld return $roster; } - foreach (self::SPECIALTY_DOCTORS as $key => $name) { + foreach (self::SPECIALTY_DOCTORS as $key => $meta) { $email = sprintf('demo-%s-%s@ladill.com', $tier, $key); $already = false; foreach ($roster as $row) { @@ -584,13 +616,37 @@ final class DemoWorld if ($already) { continue; } + $name = is_array($meta) ? (string) ($meta['name'] ?? $key) : (string) $meta; + $role = is_array($meta) ? (string) ($meta['role'] ?? 'doctor') : 'doctor'; + $suffix = $tier === 'pro' ? ' (Pro)' : ''; + $roster[] = [ + 'key' => $key, + 'email' => $email, + 'name' => $name.$suffix, + 'apps' => ['care'], + 'roles' => ['care' => $role], + ]; + } + + foreach (self::CARE_MATRIX_STAFF as $extra) { + $email = sprintf('demo-%s-%s@ladill.com', $tier, $extra['key']); + $already = false; + foreach ($roster as $row) { + if (strtolower((string) ($row['email'] ?? '')) === $email) { + $already = true; + break; + } + } + if ($already) { + continue; + } $suffix = $tier === 'pro' ? ' (Pro)' : ''; $roster[] = [ - 'key' => $key, + 'key' => $extra['key'], 'email' => $email, - 'name' => $name.$suffix, + 'name' => $extra['name'].$suffix, 'apps' => ['care'], - 'roles' => ['care' => 'doctor'], + 'roles' => ['care' => $extra['role']], ]; } diff --git a/config/care.php b/config/care.php index 51beb3f..2d8e956 100644 --- a/config/care.php +++ b/config/care.php @@ -5,13 +5,32 @@ return [ 'roles' => [ 'super_admin' => 'Super Administrator', 'hospital_admin' => 'Hospital Administrator', - 'receptionist' => 'Receptionist', - 'doctor' => 'Doctor', - 'nurse' => 'Nurse', + 'department_manager' => 'Department Manager', + 'emergency_physician' => 'Emergency Physician', + 'ed_nurse' => 'ED Nurse', + 'general_physician' => 'General Physician', + 'doctor' => 'Doctor (legacy)', + 'surgeon' => 'Surgeon', + 'theatre_nurse' => 'Theatre Nurse', + 'radiologist' => 'Radiologist', + 'radiographer' => 'Radiographer', 'lab_technician' => 'Laboratory Technician', 'lab_manager' => 'Laboratory Manager', - 'pharmacist' => 'Pharmacist', + 'pathologist' => 'Pathologist', + 'blood_bank_officer' => 'Blood Bank Officer', 'blood_bank_manager' => 'Blood Bank Manager', + 'pharmacist' => 'Pharmacist', + 'dentist' => 'Dentist', + 'psychiatrist' => 'Psychiatrist', + 'physiotherapist' => 'Physiotherapist', + 'midwife' => 'Midwife', + 'pediatrician' => 'Pediatrician', + 'oncologist' => 'Oncologist', + 'dialysis_nurse' => 'Dialysis Nurse', + 'ambulance_staff' => 'Ambulance Staff', + 'nurse' => 'Nurse (legacy)', + 'receptionist' => 'Receptionist', + 'billing_officer' => 'Billing Officer', 'cashier' => 'Cashier', 'accountant' => 'Accountant', ], diff --git a/config/care_specialty_modules.php b/config/care_specialty_modules.php index 2de4bf3..851d5af 100644 --- a/config/care_specialty_modules.php +++ b/config/care_specialty_modules.php @@ -4,20 +4,10 @@ * Specialty practice modules (Settings → Modules). * Pro-gated via plans.*.features specialty_modules. * - * Access tiers (RBAC via Care member roles): - * - general: broad day-to-day access for listed roles (GPs = doctor without specialty desk) - * - limited: GPs + nurses (and listed roles); desk specialists only when specialty keywords match - * - restricted: specialist doctors (department / specialty keywords) + optional support_roles; - * GPs use view_roles / refer_roles; desk specialists only see matching modules - * - * Prefer existing clinical role slugs on allowlists. Dedicated managers (e.g. blood_bank_manager, - * lab_manager) are intentional product roles — add them to the relevant module roles lists. - * - * Desk specialists (doctor whose practitioner specialty matches specialist_keywords, or who is - * assigned to the module department) only see modules relevant to their work — non-matching - * modules are access "none" (hidden from sidebar). Nurses / receptionists keep role allowlists. - * - * default_on_paid_plans: always on for Pro/Enterprise (sidebar + access). + * 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. * * @return array> */ @@ -133,8 +123,8 @@ return [ 'access' => 'restricted', 'roles' => ['doctor'], 'support_roles' => ['nurse'], - 'view_roles' => ['doctor', 'nurse', 'lab_technician'], - 'refer_roles' => ['doctor', 'nurse'], + 'view_roles' => ['doctor', 'general_physician', 'nurse', 'lab_technician'], + 'refer_roles' => ['doctor', 'general_physician', 'nurse'], 'specialist_keywords' => ['cardio', 'cardiology', 'heart', 'ecg'], ], 'psychiatry' => [ @@ -150,8 +140,8 @@ return [ 'access' => 'restricted', 'roles' => ['doctor'], 'support_roles' => ['nurse'], - 'view_roles' => ['doctor', 'nurse'], - 'refer_roles' => ['doctor', 'nurse'], + 'view_roles' => ['doctor', 'general_physician', 'nurse'], + 'refer_roles' => ['doctor', 'general_physician', 'nurse'], 'specialist_keywords' => ['psych', 'psychiatry', 'mental', 'behaviour'], ], 'pediatrics' => [ @@ -182,8 +172,8 @@ return [ 'access' => 'restricted', 'roles' => ['doctor'], 'support_roles' => ['nurse'], - 'view_roles' => ['doctor', 'nurse'], - 'refer_roles' => ['doctor', 'nurse'], + 'view_roles' => ['doctor', 'general_physician', 'nurse'], + 'refer_roles' => ['doctor', 'general_physician', 'nurse'], 'specialist_keywords' => ['ortho', 'orthopedic', 'orthopaedic', 'fracture'], ], 'ent' => [ @@ -199,8 +189,8 @@ return [ 'access' => 'restricted', 'roles' => ['doctor'], 'support_roles' => ['nurse'], - 'view_roles' => ['doctor', 'nurse'], - 'refer_roles' => ['doctor', 'nurse'], + 'view_roles' => ['doctor', 'general_physician', 'nurse'], + 'refer_roles' => ['doctor', 'general_physician', 'nurse'], 'specialist_keywords' => ['ent', 'otolaryng', 'ear', 'nose', 'throat'], ], 'oncology' => [ @@ -216,8 +206,8 @@ return [ 'access' => 'restricted', 'roles' => ['doctor'], 'support_roles' => ['nurse'], - 'view_roles' => ['doctor', 'nurse', 'pharmacist'], - 'refer_roles' => ['doctor', 'nurse'], + 'view_roles' => ['doctor', 'general_physician', 'nurse', 'pharmacist'], + 'refer_roles' => ['doctor', 'general_physician', 'nurse'], 'specialist_keywords' => ['oncol', 'cancer', 'chemo'], ], 'renal' => [ @@ -233,8 +223,8 @@ return [ 'access' => 'restricted', 'roles' => ['doctor'], 'support_roles' => ['nurse'], - 'view_roles' => ['doctor', 'nurse'], - 'refer_roles' => ['doctor', 'nurse'], + 'view_roles' => ['doctor', 'general_physician', 'nurse'], + 'refer_roles' => ['doctor', 'general_physician', 'nurse'], 'specialist_keywords' => ['renal', 'nephro', 'dialysis', 'kidney'], ], 'surgery' => [ @@ -250,8 +240,8 @@ return [ 'access' => 'restricted', 'roles' => ['doctor'], 'support_roles' => ['nurse'], - 'view_roles' => ['doctor', 'nurse'], - 'refer_roles' => ['doctor', 'nurse'], + 'view_roles' => ['doctor', 'general_physician', 'nurse'], + 'refer_roles' => ['doctor', 'general_physician', 'nurse'], 'specialist_keywords' => ['surg', 'theatre', 'operat'], ], 'vaccination' => [ @@ -309,8 +299,8 @@ return [ 'access' => 'restricted', 'roles' => ['doctor'], 'support_roles' => [], - 'view_roles' => ['doctor', 'nurse'], - 'refer_roles' => ['doctor', 'nurse'], + 'view_roles' => ['doctor', 'general_physician', 'nurse'], + 'refer_roles' => ['doctor', 'general_physician', 'nurse'], 'specialist_keywords' => ['derma', 'skin'], ], 'podiatry' => [ @@ -326,8 +316,8 @@ return [ 'access' => 'restricted', 'roles' => ['doctor'], 'support_roles' => [], - 'view_roles' => ['doctor', 'nurse'], - 'refer_roles' => ['doctor', 'nurse'], + 'view_roles' => ['doctor', 'general_physician', 'nurse'], + 'refer_roles' => ['doctor', 'general_physician', 'nurse'], 'specialist_keywords' => ['podiat', 'foot'], ], 'fertility' => [ @@ -343,8 +333,8 @@ return [ 'access' => 'restricted', 'roles' => ['doctor'], 'support_roles' => [], - 'view_roles' => ['doctor', 'nurse'], - 'refer_roles' => ['doctor', 'nurse'], + 'view_roles' => ['doctor', 'general_physician', 'nurse'], + 'refer_roles' => ['doctor', 'general_physician', 'nurse'], 'specialist_keywords' => ['fertil', 'ivf', 'reproduct'], ], 'child_welfare' => [ diff --git a/resources/views/care/admin/members/create.blade.php b/resources/views/care/admin/members/create.blade.php index f01383b..d010a41 100644 --- a/resources/views/care/admin/members/create.blade.php +++ b/resources/views/care/admin/members/create.blade.php @@ -9,7 +9,12 @@ -
+ @csrf @if (! empty($mailboxOptions)) @@ -41,28 +46,28 @@
-

- Required for doctors. Add more sites later under Practitioners (telemedicine). +

+ Required for clinical desks. Add more sites later under Practitioners (telemedicine).

-
+