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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user