Gate Queue to doctors and land specialty modules on list indexes.
Deploy Ladill Care / deploy (push) Successful in 34s

Hide the patient-flow Queue board from nurses, reception, and other
non-doctors; specialty home routes now render the queue board list
instead of auto-opening the first open visit.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-18 22:53:12 +00:00
co-authored by Cursor
parent 244bb8fba6
commit d422797050
22 changed files with 277 additions and 122 deletions
@@ -150,7 +150,12 @@ class AppointmentController extends Controller
->with('success', 'Walk-in checked in. Complete the current patient journey stage before Queue release.');
}
return redirect()->route('care.queue.index')
if (app(CarePermissions::class)->canAccessPatientQueue($this->member($request))) {
return redirect()->route('care.queue.index')
->with('success', 'Walk-in added to queue.');
}
return redirect()->route('care.appointments.show', $appointment)
->with('success', 'Walk-in added to queue.');
}
@@ -217,7 +222,12 @@ class AppointmentController extends Controller
->with('success', 'Patient checked in. Complete the current patient journey stage before Queue release.');
}
return redirect()->route('care.queue.index')
if (app(CarePermissions::class)->canAccessPatientQueue($this->member($request))) {
return redirect()->route('care.queue.index')
->with('success', 'Patient checked in and added to queue.');
}
return redirect()->route('care.appointments.show', $appointment)
->with('success', 'Patient checked in and added to queue.');
}
@@ -278,8 +278,11 @@ class ConsultationController extends Controller
->with('success', 'Consultation completed. Continue the patient journey from the next stage.');
}
return redirect()->route('care.queue.index')
->with('success', 'Consultation completed.');
return redirect()->route(
app(CarePermissions::class)->canAccessPatientQueue($this->member($request))
? 'care.queue.index'
: 'care.appointments.index'
)->with('success', 'Consultation completed.');
}
protected function authorizeConsultation(Request $request, Consultation $consultation): void
@@ -8,7 +8,8 @@ use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
/**
* Legacy Ladill Queue console routes redirect to the Care patient queue.
* Legacy Ladill Queue console routes redirect to the Care patient queue (doctors)
* or appointments list when the actor cannot open the patient-flow board.
*/
class ServiceQueueController extends Controller
{
@@ -18,20 +19,29 @@ class ServiceQueueController extends Controller
{
$this->authorizeAbility($request, 'service_queues.console');
return redirect()->route('care.queue.index');
return $this->redirectAwayFromLegacyConsole($request);
}
public function console(Request $request, string $counterUuid): RedirectResponse
{
$this->authorizeAbility($request, 'service_queues.console');
return redirect()->route('care.queue.index');
return $this->redirectAwayFromLegacyConsole($request);
}
public function action(Request $request, string $counterUuid): RedirectResponse
{
$this->authorizeAbility($request, 'service_queues.console');
return redirect()->route('care.queue.index');
return $this->redirectAwayFromLegacyConsole($request);
}
protected function redirectAwayFromLegacyConsole(Request $request): RedirectResponse
{
if (app(\App\Services\Care\CarePermissions::class)->canAccessPatientQueue($this->member($request))) {
return redirect()->route('care.queue.index');
}
return redirect()->route('care.appointments.index');
}
}
@@ -31,20 +31,9 @@ class SpecialtyModuleController extends Controller
SpecialtyModuleService $modules,
SpecialtyShellService $shell,
CareQueueBridge $queueBridge,
): RedirectResponse {
$definition = $modules->definition($module);
abort_unless($definition, 404);
abort_unless(
$modules->memberCanAccess($this->organization($request), $this->member($request), $module),
403,
);
// Specialty patient flow lives on Queue; nurses stay on the specialty workspace.
if (app(\App\Services\Care\CarePermissions::class)->canAccessPatientQueue($this->member($request))) {
return redirect()->route('care.queue.index');
}
return redirect()->route('care.specialty.workspace', $module);
): View {
// Everyone lands on the specialty list/index — never auto-open a patient.
return $this->renderShell($request, $module, 'visits', $modules, $shell, $queueBridge);
}
public function visits(
@@ -53,19 +42,8 @@ class SpecialtyModuleController extends Controller
SpecialtyModuleService $modules,
SpecialtyShellService $shell,
CareQueueBridge $queueBridge,
): RedirectResponse {
$definition = $modules->definition($module);
abort_unless($definition, 404);
abort_unless(
$modules->memberCanAccess($this->organization($request), $this->member($request), $module),
403,
);
if (app(\App\Services\Care\CarePermissions::class)->canAccessPatientQueue($this->member($request))) {
return redirect()->route('care.queue.index');
}
return redirect()->route('care.specialty.workspace', $module);
): View {
return $this->renderShell($request, $module, 'visits', $modules, $shell, $queueBridge);
}
public function history(
@@ -95,7 +73,12 @@ class SpecialtyModuleController extends Controller
SpecialtyShellService $shell,
CareQueueBridge $queueBridge,
?Visit $visit = null,
): View {
): View|RedirectResponse {
// Bare /workspace (no visit) is the old auto-open entry — send users to the list.
if ($visit === null) {
return redirect()->route('care.specialty.show', $module);
}
return $this->renderShell($request, $module, 'workspace', $modules, $shell, $queueBridge, $visit);
}
@@ -589,14 +572,7 @@ class SpecialtyModuleController extends Controller
);
return redirect()
->route(
app(\App\Services\Care\CarePermissions::class)->canAccessPatientQueue($member)
? 'care.queue.index'
: 'care.specialty.workspace',
app(\App\Services\Care\CarePermissions::class)->canAccessPatientQueue($member)
? []
: ['module' => $module],
)
->route('care.specialty.show', $module)
->with('success', 'Referred '.$patient->fullName().' to '.($definition['label'] ?? $module).'.');
}
@@ -792,25 +768,6 @@ class SpecialtyModuleController extends Controller
);
$timeline = $shell->patientTimeline($visit->patient);
}
} elseif ($section === 'workspace') {
$first = $openVisits->first();
if ($first?->patient) {
$first->load([
'patient.allergies',
'patient.emergencyContacts',
'patient.attachments',
'appointment.practitioner',
'appointment.consultation.investigationRequests.investigationType',
'bill.lineItems',
'consultations.investigationRequests.investigationType',
]);
$workspaceVisit = $first;
$patientHeader = array_merge(
['patient' => $first->patient, 'visit' => $first, 'appointment' => $first->appointment],
$shell->patientHeaderMeta($first->patient),
);
$timeline = $shell->patientTimeline($first->patient);
}
}
if ($workspaceVisit && $section === 'workspace') {
+5 -7
View File
@@ -111,15 +111,13 @@ class CarePermissions
}
/**
* Dedicated patient-flow board (GP / specialty Queue homes).
* Nurses keep specialty workspaces and vitals not the call-next board.
* 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.
*/
public function canAccessPatientQueue(?Member $member): bool
{
if (! $this->handlesFloorCare($member)) {
return false;
}
return $member->role !== 'nurse';
return $member !== null && $member->role === 'doctor';
}
}
+15 -4
View File
@@ -284,6 +284,8 @@ class OrganizationResolver
/**
* Explicit branch IDs for a doctor's linked practitioner desks (pivot + legacy branch_id).
* GPs with no practitioner desk fall back to their member branch so they can still
* open the shared consultation Queue board.
*
* @return list<int>
*/
@@ -304,7 +306,13 @@ class OrganizationResolver
$ids = array_merge($ids, $practitioner->assignedBranchIds());
}
return array_values(array_unique(array_map('intval', $ids)));
$ids = array_values(array_unique(array_map('intval', $ids)));
if ($ids === [] && $member->branch_id) {
return [(int) $member->branch_id];
}
return $ids;
}
public function mayAccessBranch(?Member $member, ?int $branchId): bool
@@ -357,8 +365,9 @@ class OrganizationResolver
}
/**
* Practitioner IDs a doctor is locked to. Null = no practitioner lock.
* Empty array = doctor with no linked desk.
* Practitioner IDs a doctor is locked to. Null = no practitioner lock
* (non-doctors, or GPs with no linked desk they work the shared consultation board).
* Non-empty = desk-locked specialist / assigned GP.
*
* @return list<int>|null
*/
@@ -368,7 +377,7 @@ class OrganizationResolver
return null;
}
return Practitioner::owned((string) $organization->owner_ref)
$ids = Practitioner::owned((string) $organization->owner_ref)
->where('organization_id', $organization->id)
->where('is_active', true)
->where(function ($query) use ($member) {
@@ -380,5 +389,7 @@ class OrganizationResolver
->map(fn ($id) => (int) $id)
->values()
->all();
return $ids === [] ? null : $ids;
}
}