Limit specialty module visibility to matching desk specialists.
Deploy Ladill Care / deploy (push) Successful in 35s

GPs keep broad general/limited manage and restricted view/refer; dentists and other desk specialists only see modules that match their specialty.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-18 22:09:15 +00:00
co-authored by Cursor
parent 9ecc3ad7f0
commit d02760c527
4 changed files with 119 additions and 16 deletions
+52 -11
View File
@@ -214,16 +214,11 @@ class SpecialtyModuleService
return 'manage';
}
$canRefer = $this->roleListed($member, $definition['refer_roles'] ?? []);
$canView = $this->roleListed($member, $definition['view_roles'] ?? []);
if ($canRefer && $canView) {
// Use gated helpers so desk specialists do not inherit GP view/refer on other modules.
if ($this->memberCanRefer($organization, $member, $key)) {
return 'refer';
}
if ($canRefer) {
return 'refer';
}
if ($canView) {
if ($this->memberCanView($organization, $member, $key)) {
return 'view';
}
@@ -256,6 +251,12 @@ class SpecialtyModuleService
return false;
}
// Desk specialists only manage modules that match their specialty / assignment.
if ($this->isDeskSpecialist($organization, $member)
&& ! $this->specialistBelongsToModule($organization, $member, $key)) {
return false;
}
$tier = $this->accessTier($key);
$role = (string) $member->role;
$manageRoles = $definition['roles'] ?? [];
@@ -266,14 +267,14 @@ class SpecialtyModuleService
return true;
}
if ($role === 'doctor' && $this->roleListed($member, $manageRoles)) {
return $this->doctorAssignedToModule($organization, $member, $key)
|| $this->doctorSpecialtyMatchesModule($organization, $member, $key);
return $this->specialistBelongsToModule($organization, $member, $key);
}
return false;
}
// general + limited: role allowlist is enough (GPs = doctor without specialty desk).
// general + limited: role allowlist is enough for GPs and non-doctor roles.
// Desk specialists already passed the match gate above.
return $this->roleListed($member, $manageRoles);
}
@@ -290,6 +291,12 @@ class SpecialtyModuleService
return false;
}
// Desk specialists do not get cross-module view; only their matching desks.
if ($this->isDeskSpecialist($organization, $member)
&& ! $this->specialistBelongsToModule($organization, $member, $key)) {
return false;
}
$definition = $this->definition($key);
if (! $definition) {
return false;
@@ -311,6 +318,12 @@ class SpecialtyModuleService
return false;
}
// Desk specialists do not get cross-module referral nav; GPs keep refer_roles.
if ($this->isDeskSpecialist($organization, $member)
&& ! $this->specialistBelongsToModule($organization, $member, $key)) {
return false;
}
$definition = $this->definition($key);
if (! $definition) {
return false;
@@ -319,6 +332,34 @@ class SpecialtyModuleService
return $this->roleListed($member, $definition['refer_roles'] ?? []);
}
/**
* 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.
*/
public function isDeskSpecialist(Organization $organization, Member $member): bool
{
if ((string) $member->role !== 'doctor') {
return false;
}
foreach (array_keys($this->catalog()) as $key) {
if ($this->specialistBelongsToModule($organization, $member, $key)) {
return true;
}
}
return false;
}
/**
* Whether this doctor belongs to the module via department assignment or specialty keywords.
*/
public function specialistBelongsToModule(Organization $organization, Member $member, string $key): bool
{
return $this->doctorAssignedToModule($organization, $member, $key)
|| $this->doctorSpecialtyMatchesModule($organization, $member, $key);
}
/**
* @param list<string>|mixed $roles
*/