Lock doctors to a single branch — never a branch picker.
Deploy Ladill Care / deploy (push) Successful in 1m3s
Deploy Ladill Care / deploy (push) Successful in 1m3s
Doctors were treated as multi-branch when Identity cleared branch_id; now they stay on their assigned/desk site, specialty desks are home-branch only, and the queue shows site name as text only. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -73,7 +73,11 @@ class QueueController extends Controller
|
||||
}
|
||||
|
||||
if ($lockToPractitioner) {
|
||||
$queue = $this->appointments->queueForPractitioners($owner, $practitionerScope ?? []);
|
||||
$queue = $this->appointments->queueForPractitioners(
|
||||
$owner,
|
||||
$practitionerScope ?? [],
|
||||
$branchId > 0 ? $branchId : null,
|
||||
);
|
||||
$inConsultation = Appointment::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('status', Appointment::STATUS_IN_CONSULTATION)
|
||||
|
||||
@@ -25,6 +25,22 @@ class BranchContext
|
||||
|
||||
public function canSwitch(?Member $member): bool
|
||||
{
|
||||
if ($member === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Site clinicians are assigned to one branch — never a branch picker.
|
||||
if (in_array($member->role, [
|
||||
'doctor',
|
||||
'nurse',
|
||||
'pharmacist',
|
||||
'lab_technician',
|
||||
'cashier',
|
||||
'receptionist',
|
||||
], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->organizations->branchScope($member) === null;
|
||||
}
|
||||
|
||||
@@ -40,7 +56,8 @@ class BranchContext
|
||||
return Branch::owned((string) $organization->owner_ref)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->when($scope, fn ($q) => $q->where('id', $scope))
|
||||
// Scope 0 = no site assigned — must not fall through to every branch.
|
||||
->when($scope !== null, fn ($q) => $q->where('id', $scope))
|
||||
->orderBy('name')
|
||||
->get();
|
||||
}
|
||||
|
||||
@@ -822,6 +822,9 @@ class DemoTenantSeeder
|
||||
|
||||
$member = $moduleMembers[$key];
|
||||
$staff = $member ? \App\Support\DemoWorld::staffByEmail($staffEmail) : null;
|
||||
$homeBranchId = (int) ($member?->branch_id ?? 0);
|
||||
// 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);
|
||||
$practitioner = Practitioner::withTrashed()->updateOrCreate(
|
||||
[
|
||||
@@ -832,8 +835,8 @@ class DemoTenantSeeder
|
||||
'owner_ref' => $ownerRef,
|
||||
'branch_id' => $branch->id,
|
||||
'department_id' => $department->id,
|
||||
'member_id' => $member?->id,
|
||||
'name' => is_array($staff)
|
||||
'member_id' => $assignMember ? $member->id : null,
|
||||
'name' => $assignMember && is_array($staff)
|
||||
? (string) $staff['name']
|
||||
: (($definition['label'] ?? ucfirst($key)).' Clinic'),
|
||||
'specialty' => (string) ($definition['label'] ?? $key),
|
||||
|
||||
@@ -180,7 +180,7 @@ class OrganizationResolver
|
||||
};
|
||||
}
|
||||
|
||||
/** Branch ID the member may access; null = all branches. */
|
||||
/** Branch ID the member may access; null = all branches (admins only). */
|
||||
public function branchScope(?Member $member): ?int
|
||||
{
|
||||
if ($member === null) {
|
||||
@@ -191,12 +191,30 @@ class OrganizationResolver
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($member->branch_id) {
|
||||
return (int) $member->branch_id;
|
||||
}
|
||||
|
||||
// Doctors are always one site. Prefer linked desk when Identity omitted branch_id.
|
||||
if ($member->role === 'doctor') {
|
||||
$deskBranch = Practitioner::query()
|
||||
->where('is_active', true)
|
||||
->where(function ($query) use ($member) {
|
||||
$query->where('member_id', $member->id)
|
||||
->orWhere('user_ref', $member->user_ref);
|
||||
})
|
||||
->orderBy('id')
|
||||
->value('branch_id');
|
||||
|
||||
return $deskBranch ? (int) $deskBranch : 0;
|
||||
}
|
||||
|
||||
return $member->branch_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Practitioner IDs a doctor is locked to. Null = no practitioner lock (non-doctors).
|
||||
* Empty array = doctor with no linked practitioner desk (see no clinical queue).
|
||||
* Practitioner IDs a doctor is locked to (single branch). Null = no practitioner lock.
|
||||
* Empty array = doctor with no linked desk on their branch.
|
||||
*
|
||||
* @return list<int>|null
|
||||
*/
|
||||
@@ -206,9 +224,12 @@ class OrganizationResolver
|
||||
return null;
|
||||
}
|
||||
|
||||
$branchId = $this->branchScope($member);
|
||||
|
||||
return Practitioner::owned((string) $organization->owner_ref)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
|
||||
->where(function ($query) use ($member) {
|
||||
$query->where('member_id', $member->id)
|
||||
->orWhere('user_ref', $member->user_ref);
|
||||
|
||||
@@ -139,9 +139,12 @@ class SpecialtyModuleService
|
||||
? array_map('intval', $provisionedIds)
|
||||
: [];
|
||||
|
||||
$branchId = app(OrganizationResolver::class)->branchScope($member);
|
||||
|
||||
$practitioners = Practitioner::owned((string) $organization->owner_ref)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('is_active', true)
|
||||
->when($branchId, fn ($q) => $q->where('branch_id', $branchId))
|
||||
->where(function ($query) use ($member) {
|
||||
$query->where('member_id', $member->id)
|
||||
->orWhere('user_ref', $member->user_ref);
|
||||
|
||||
@@ -53,7 +53,12 @@ class TeamMemberProvisioner
|
||||
[
|
||||
'owner_ref' => $ownerRef !== '' ? $ownerRef : $user->ownerRef(),
|
||||
'role' => (string) ($appMeta['role'] ?? 'member'),
|
||||
'branch_id' => $appMeta['branch_id'] ?? null,
|
||||
'branch_id' => array_key_exists('branch_id', $appMeta)
|
||||
? $appMeta['branch_id']
|
||||
: Member::query()
|
||||
->where('organization_id', $organizationId)
|
||||
->where('user_ref', $user->ownerRef())
|
||||
->value('branch_id'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,11 +13,16 @@
|
||||
</div>
|
||||
|
||||
<form method="GET" class="mt-4 flex flex-wrap gap-3 rounded-2xl border border-slate-200 bg-white p-4">
|
||||
<select name="branch_id" class="rounded-lg border-slate-300 text-sm" onchange="this.form.submit()">
|
||||
@foreach ($branches as $branch)
|
||||
<option value="{{ $branch->id }}" @selected($branchId == $branch->id)>{{ $branch->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if (($canSwitchBranch ?? false) && isset($branches) && $branches->count() > 1)
|
||||
<select name="branch_id" class="rounded-lg border-slate-300 text-sm" onchange="this.form.submit()">
|
||||
@foreach ($branches as $branch)
|
||||
<option value="{{ $branch->id }}" @selected($branchId == $branch->id)>{{ $branch->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@elseif (isset($branches) && $branches->isNotEmpty())
|
||||
<input type="hidden" name="branch_id" value="{{ $branchId }}">
|
||||
<span class="flex items-center text-sm font-medium text-slate-700">{{ $branches->first()->name }}</span>
|
||||
@endif
|
||||
<select name="status" class="rounded-lg border-slate-300 text-sm">
|
||||
<option value="">All active</option>
|
||||
@foreach ($statuses as $value => $label)
|
||||
|
||||
@@ -17,18 +17,20 @@
|
||||
</x-care.page-hero>
|
||||
|
||||
<form method="GET" class="flex flex-wrap gap-3 rounded-2xl border border-slate-200 bg-white p-4">
|
||||
@if ($canSwitchBranch ?? false)
|
||||
@if ($lockToPractitioner ?? false)
|
||||
<p class="text-sm text-slate-500">
|
||||
@if (isset($branches) && $branches->isNotEmpty())
|
||||
<span class="font-medium text-slate-700">{{ $branches->first()->name }}</span>
|
||||
<span class="text-slate-300"> · </span>
|
||||
@endif
|
||||
Showing patients assigned to you
|
||||
</p>
|
||||
@elseif ($canSwitchBranch ?? false)
|
||||
<select name="branch_id" class="rounded-lg border-slate-300 text-sm" onchange="this.form.submit()">
|
||||
@foreach ($branches as $branch)
|
||||
<option value="{{ $branch->id }}" @selected($branchId == $branch->id)>{{ $branch->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@elseif (($lockToPractitioner ?? false) === false && isset($branches) && $branches->isNotEmpty())
|
||||
<input type="hidden" name="branch_id" value="{{ $branchId }}">
|
||||
<span class="flex items-center text-sm text-slate-500">{{ $branches->first()->name }}</span>
|
||||
@endif
|
||||
|
||||
@unless ($lockToPractitioner ?? false)
|
||||
<select name="practitioner_id" class="rounded-lg border-slate-300 text-sm">
|
||||
<option value="">All practitioners</option>
|
||||
@foreach ($practitioners as $practitioner)
|
||||
@@ -37,8 +39,18 @@
|
||||
</select>
|
||||
<button type="submit" class="btn-primary">Filter</button>
|
||||
@else
|
||||
<span class="flex items-center text-sm text-slate-500">Showing patients assigned to you</span>
|
||||
@endunless
|
||||
@if (isset($branches) && $branches->isNotEmpty())
|
||||
<input type="hidden" name="branch_id" value="{{ $branchId }}">
|
||||
<span class="flex items-center text-sm font-medium text-slate-700">{{ $branches->first()->name }}</span>
|
||||
@endif
|
||||
<select name="practitioner_id" class="rounded-lg border-slate-300 text-sm">
|
||||
<option value="">All practitioners</option>
|
||||
@foreach ($practitioners as $practitioner)
|
||||
<option value="{{ $practitioner->id }}" @selected($practitionerId == $practitioner->id)>{{ $practitioner->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<button type="submit" class="btn-primary">Filter</button>
|
||||
@endif
|
||||
</form>
|
||||
|
||||
@include('care.partials.queue-ops', [
|
||||
|
||||
@@ -142,7 +142,15 @@ class CareAppointmentTest extends TestCase
|
||||
$this->assertNotNull($appointment->visit_id);
|
||||
$this->assertDatabaseHas('care_visits', ['patient_id' => $this->patient->id]);
|
||||
|
||||
Member::where('user_ref', $this->user->public_id)->update(['role' => 'doctor']);
|
||||
$member = Member::where('user_ref', $this->user->public_id)->firstOrFail();
|
||||
$member->update([
|
||||
'role' => 'doctor',
|
||||
'branch_id' => $this->branch->id,
|
||||
]);
|
||||
$this->practitioner->update([
|
||||
'member_id' => $member->id,
|
||||
'user_ref' => $this->user->public_id,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('care.queue.start', $appointment))
|
||||
|
||||
@@ -142,13 +142,32 @@ class CareStaffTenantScopeTest extends TestCase
|
||||
->get(route('care.queue.index'))
|
||||
->assertOk()
|
||||
->assertSee('Showing patients assigned to you')
|
||||
->assertSee('East Legon Care')
|
||||
->assertDontSee('All practitioners')
|
||||
->assertDontSee('name="branch_id"', false)
|
||||
->assertDontSee('<select', false)
|
||||
->assertSee('Ama Mensah')
|
||||
->assertSee('Toothache')
|
||||
->assertDontSee('Kofi Boateng')
|
||||
->assertDontSee('Fever');
|
||||
|
||||
// Even if Identity clears member.branch_id, doctors stay on their desk's branch — no picker.
|
||||
$doctorMember->update(['branch_id' => null]);
|
||||
Branch::create([
|
||||
'owner_ref' => $owner->public_id,
|
||||
'organization_id' => $organization->id,
|
||||
'name' => 'West Hills Care',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
$this->actingAs($doctor)
|
||||
->get(route('care.queue.index'))
|
||||
->assertOk()
|
||||
->assertSee('East Legon Care')
|
||||
->assertDontSee('West Hills Care')
|
||||
->assertDontSee('name="branch_id"', false)
|
||||
->assertDontSee('<select', false);
|
||||
|
||||
$this->actingAs($doctor)
|
||||
->get(route('care.patients.index'))
|
||||
->assertOk()
|
||||
|
||||
Reference in New Issue
Block a user