Implement Care RBAC role→permission→app matrix.
Deploy Ladill Care / deploy (push) Successful in 57s
Deploy Ladill Care / deploy (push) Successful in 57s
Replace broad doctor/nurse specialty access with granular roles and primary apps, permission inheritance for lab/BB managers, and cannot-rules for discharge, lab approve, and cashier vs billing officer. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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 modules.
|
||||
*
|
||||
* @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'],
|
||||
'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<string, list<string>>
|
||||
*/
|
||||
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<string> */
|
||||
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<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 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<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];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<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',
|
||||
], true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+87
-31
@@ -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<string, string> module key => display name
|
||||
* @var array<string, array{name: string, role: string}>
|
||||
*/
|
||||
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<array{key: string, name: string, role: string}>
|
||||
*/
|
||||
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']],
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user