Gate Queue to doctors and land specialty modules on list indexes.
Deploy Ladill Care / deploy (push) Successful in 34s
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:
@@ -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') {
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h1 class="text-xl font-semibold text-slate-900">Blood Bank reports</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Requests, urgency mix, products issued, low stock, and bb.* revenue.</p>
|
||||
</div>
|
||||
<a href="{{ route('care.specialty.workspace', 'blood_bank') }}" class="text-sm font-medium text-indigo-600">← Specialty home</a>
|
||||
<a href="{{ route('care.specialty.show', 'blood_bank') }}" class="text-sm font-medium text-indigo-600">← Specialty home</a>
|
||||
</div>
|
||||
|
||||
<form method="GET" class="flex flex-wrap items-end gap-3 rounded-2xl border border-slate-200 bg-white p-4">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h1 class="text-xl font-semibold text-slate-900">Dentistry reports</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Procedures, revenue, unfinished plans, and imaging volume.</p>
|
||||
</div>
|
||||
<a href="{{ route('care.specialty.workspace', 'dentistry') }}" class="text-sm font-medium text-indigo-600">← Specialty home</a>
|
||||
<a href="{{ route('care.specialty.show', 'dentistry') }}" class="text-sm font-medium text-indigo-600">← Specialty home</a>
|
||||
</div>
|
||||
|
||||
<form method="GET" class="flex flex-wrap items-end gap-3 rounded-2xl border border-slate-200 bg-white p-4">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<h1 class="text-xl font-semibold text-slate-900">Emergency reports</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Arrivals, acuity mix, dispositions, length of stay, and ED service revenue.</p>
|
||||
</div>
|
||||
<a href="{{ route('care.specialty.workspace', 'emergency') }}" class="text-sm font-medium text-indigo-600">← Specialty home</a>
|
||||
<a href="{{ route('care.specialty.show', 'emergency') }}" class="text-sm font-medium text-indigo-600">← Specialty home</a>
|
||||
</div>
|
||||
|
||||
<form method="GET" class="flex flex-wrap items-end gap-3 rounded-2xl border border-slate-200 bg-white p-4">
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
Back to queue
|
||||
</a>
|
||||
@else
|
||||
<a href="{{ route('care.specialty.workspace', $moduleKey) }}" class="{{ $btnSecondary }}">
|
||||
<a href="{{ route('care.specialty.show', $moduleKey) }}" class="{{ $btnSecondary }}">
|
||||
Specialty home
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@@ -36,6 +36,6 @@
|
||||
</div>
|
||||
|
||||
<p class="mt-4 text-xs text-slate-500">
|
||||
Add a service to a visit invoice from the specialty <a href="{{ route('care.specialty.workspace', $moduleKey) }}" class="font-medium text-indigo-600">workspace</a> Billing tab.
|
||||
Add a service to a visit invoice from a specialty <a href="{{ route('care.specialty.show', $moduleKey) }}" class="font-medium text-indigo-600">visit workspace</a> Billing tab.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -17,13 +17,15 @@
|
||||
<h2 class="text-sm font-semibold text-slate-900">No open visit selected</h2>
|
||||
<p class="mt-1 text-sm text-slate-500">
|
||||
@if (! empty($canAccessPatientQueue))
|
||||
Open a visit from the queue, or call next from the waiting list.
|
||||
Open a visit from the specialty list or GP queue, or call next from the waiting list.
|
||||
@else
|
||||
Open an assigned visit, or ask reception to call the next patient.
|
||||
Open a patient from the specialty list to enter their workspace.
|
||||
@endif
|
||||
</p>
|
||||
@if (! empty($canAccessPatientQueue))
|
||||
<a href="{{ route('care.queue.index') }}" class="mt-4 inline-flex text-sm font-medium text-indigo-600">Back to queue</a>
|
||||
@else
|
||||
<a href="{{ route('care.specialty.show', $moduleKey) }}" class="mt-4 inline-flex text-sm font-medium text-indigo-600">Specialty home</a>
|
||||
@endif
|
||||
</section>
|
||||
@else
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
<p class="mt-1 text-sm text-slate-500">Completed encounters for this service line.</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('care.specialty.workspace', $moduleKey) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800">← Specialty home</a>
|
||||
<a href="{{ route('care.specialty.show', $moduleKey) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800">← Specialty home</a>
|
||||
</div>
|
||||
@include('care.specialty.sections.history')
|
||||
@elseif ($section === 'billing')
|
||||
@@ -97,7 +97,7 @@
|
||||
<p class="mt-1 text-sm text-slate-500">Service catalog for this module.</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="{{ route('care.specialty.workspace', $moduleKey) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800">← Specialty home</a>
|
||||
<a href="{{ route('care.specialty.show', $moduleKey) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800">← Specialty home</a>
|
||||
</div>
|
||||
@include('care.specialty.sections.billing')
|
||||
@else
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
$label = $item['definition']['nav_label'] ?? $item['definition']['label'] ?? $key;
|
||||
$nav[] = [
|
||||
'name' => $label,
|
||||
'route' => route('care.specialty.workspace', $key),
|
||||
'route' => route('care.specialty.show', $key),
|
||||
'active' => request()->routeIs('care.specialty.*') && request()->route('module') === $key,
|
||||
'icon' => $specialtyService->iconSvgPath($key),
|
||||
'group' => 'specialty',
|
||||
|
||||
@@ -55,6 +55,7 @@ class CareAppointmentTest extends TestCase
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->user->public_id,
|
||||
'role' => 'receptionist',
|
||||
'branch_id' => null,
|
||||
]);
|
||||
|
||||
$this->branch = Branch::create([
|
||||
@@ -64,6 +65,10 @@ class CareAppointmentTest extends TestCase
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
Member::query()->where('user_ref', $this->user->public_id)->update([
|
||||
'branch_id' => $this->branch->id,
|
||||
]);
|
||||
|
||||
Department::create([
|
||||
'owner_ref' => $this->user->public_id,
|
||||
'branch_id' => $this->branch->id,
|
||||
@@ -135,7 +140,7 @@ class CareAppointmentTest extends TestCase
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->post(route('care.appointments.check-in', $appointment))
|
||||
->assertRedirect(route('care.queue.index'));
|
||||
->assertRedirect(route('care.appointments.show', $appointment));
|
||||
|
||||
$appointment->refresh();
|
||||
$this->assertSame(Appointment::STATUS_WAITING, $appointment->status);
|
||||
@@ -201,7 +206,7 @@ class CareAppointmentTest extends TestCase
|
||||
'practitioner_id' => $this->practitioner->id,
|
||||
'reason' => 'Urgent visit',
|
||||
])
|
||||
->assertRedirect(route('care.queue.index'));
|
||||
->assertRedirect(route('care.appointments.show', Appointment::query()->first()));
|
||||
|
||||
$appointment = Appointment::first();
|
||||
$this->assertSame(Appointment::TYPE_WALK_IN, $appointment->type);
|
||||
@@ -211,6 +216,8 @@ class CareAppointmentTest extends TestCase
|
||||
|
||||
public function test_queue_page_loads(): void
|
||||
{
|
||||
Member::query()->where('user_ref', $this->user->public_id)->update(['role' => 'doctor']);
|
||||
|
||||
$this->actingAs($this->user)
|
||||
->get(route('care.queue.index', ['branch_id' => $this->branch->id]))
|
||||
->assertOk()
|
||||
@@ -219,6 +226,8 @@ class CareAppointmentTest extends TestCase
|
||||
|
||||
public function test_queue_repairs_duplicate_positions(): void
|
||||
{
|
||||
Member::query()->where('user_ref', $this->user->public_id)->update(['role' => 'doctor']);
|
||||
|
||||
$second = Patient::create([
|
||||
'uuid' => (string) \Illuminate\Support\Str::uuid(),
|
||||
'owner_ref' => $this->user->public_id,
|
||||
|
||||
@@ -218,7 +218,7 @@ class CareConsultationReturnContextTest extends TestCase
|
||||
$this->actingAs($this->user)
|
||||
->withSession([ConsultationReturnContext::SESSION_KEY => $this->consultation->uuid])
|
||||
->post(route('care.consultations.complete', $this->consultation))
|
||||
->assertRedirect(route('care.queue.index'))
|
||||
->assertRedirect(route('care.appointments.index'))
|
||||
->assertSessionMissing(ConsultationReturnContext::SESSION_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ class CareNaturalQueueTest extends TestCase
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $this->owner->public_id,
|
||||
'role' => 'receptionist',
|
||||
'branch_id' => null,
|
||||
]);
|
||||
|
||||
$this->branch = Branch::create([
|
||||
@@ -66,6 +67,10 @@ class CareNaturalQueueTest extends TestCase
|
||||
'name' => 'Main',
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
Member::query()->where('user_ref', $this->owner->public_id)->update([
|
||||
'branch_id' => $this->branch->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_sidebar_does_not_show_service_queues_nav(): void
|
||||
@@ -78,6 +83,8 @@ class CareNaturalQueueTest extends TestCase
|
||||
|
||||
public function test_service_queues_index_redirects_to_clinical_queue(): void
|
||||
{
|
||||
Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'doctor']);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.service-queues.index'))
|
||||
->assertRedirect(route('care.queue.index'));
|
||||
@@ -85,6 +92,8 @@ class CareNaturalQueueTest extends TestCase
|
||||
|
||||
public function test_clinical_queue_shows_inline_ops_not_service_counter(): void
|
||||
{
|
||||
Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'doctor']);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.queue.index'))
|
||||
->assertOk()
|
||||
@@ -119,6 +128,8 @@ class CareNaturalQueueTest extends TestCase
|
||||
|
||||
public function test_integration_off_hides_queue_controls(): void
|
||||
{
|
||||
Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'doctor']);
|
||||
|
||||
$settings = $this->organization->settings;
|
||||
$settings['queue_integration_enabled'] = false;
|
||||
$this->organization->update(['settings' => $settings]);
|
||||
@@ -134,6 +145,8 @@ class CareNaturalQueueTest extends TestCase
|
||||
{
|
||||
Http::fake();
|
||||
|
||||
Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'doctor']);
|
||||
|
||||
$practitioner = Practitioner::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Middleware\EnsurePlatformSession;
|
||||
use App\Models\Appointment;
|
||||
use App\Models\Branch;
|
||||
use App\Models\Department;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Patient;
|
||||
use App\Models\User;
|
||||
use App\Models\Visit;
|
||||
use App\Services\Care\CarePermissions;
|
||||
use App\Services\Care\SpecialtyModuleService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -100,15 +105,36 @@ class CarePatientQueueAccessTest extends TestCase
|
||||
->assertDontSee('href="'.e(route('care.queue.index')).'"', false);
|
||||
}
|
||||
|
||||
public function test_doctor_and_receptionist_can_open_redesigned_queue_board(): void
|
||||
public function test_receptionist_and_other_non_doctors_cannot_open_patient_queue(): void
|
||||
{
|
||||
[$receptionistUser, $receptionistMember] = $this->makeStaff('receptionist', 'rec1');
|
||||
[$pharmacistUser, $pharmacistMember] = $this->makeStaff('pharmacist', 'rx1');
|
||||
$permissions = app(CarePermissions::class);
|
||||
|
||||
$this->assertFalse($permissions->canAccessPatientQueue($receptionistMember));
|
||||
$this->assertFalse($permissions->canAccessPatientQueue($pharmacistMember));
|
||||
$this->assertTrue($permissions->can($receptionistMember, 'queue.manage'));
|
||||
|
||||
$this->actingAs($receptionistUser)
|
||||
->get(route('care.queue.index'))
|
||||
->assertForbidden();
|
||||
|
||||
$this->actingAs($receptionistUser)
|
||||
->get(route('care.dashboard'))
|
||||
->assertOk()
|
||||
->assertDontSee('href="'.e(route('care.queue.index')).'"', false);
|
||||
|
||||
$this->actingAs($pharmacistUser)
|
||||
->get(route('care.queue.index'))
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_doctor_can_open_redesigned_queue_board(): void
|
||||
{
|
||||
[$doctorUser, $doctorMember] = $this->makeStaff('doctor', 'doc1');
|
||||
[$receptionistUser, $receptionistMember] = $this->makeStaff('receptionist', 'rec1');
|
||||
$permissions = app(CarePermissions::class);
|
||||
|
||||
$this->assertTrue($permissions->canAccessPatientQueue($doctorMember));
|
||||
$this->assertTrue($permissions->canAccessPatientQueue($receptionistMember));
|
||||
$this->assertTrue($permissions->can($receptionistMember, 'queue.manage'));
|
||||
|
||||
$this->actingAs($doctorUser)
|
||||
->get(route('care.queue.index'))
|
||||
@@ -118,29 +144,101 @@ class CarePatientQueueAccessTest extends TestCase
|
||||
->assertSee('Called')
|
||||
->assertSee('Done');
|
||||
|
||||
$this->actingAs($receptionistUser)
|
||||
->get(route('care.queue.index'))
|
||||
->assertOk()
|
||||
->assertSee('Patient flow')
|
||||
->assertSee('Call next');
|
||||
|
||||
$this->actingAs($doctorUser)
|
||||
->get(route('care.dashboard'))
|
||||
->assertOk()
|
||||
->assertSee('Queue', false);
|
||||
}
|
||||
|
||||
public function test_nurse_specialty_show_redirects_to_workspace_not_queue(): void
|
||||
public function test_hospital_admin_does_not_get_patient_queue_board(): void
|
||||
{
|
||||
$adminMember = Member::query()
|
||||
->where('user_ref', $this->owner->public_id)
|
||||
->firstOrFail();
|
||||
$permissions = app(CarePermissions::class);
|
||||
|
||||
$this->assertFalse($permissions->canAccessPatientQueue($adminMember));
|
||||
$this->assertFalse($permissions->handlesFloorCare($adminMember));
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.queue.index'))
|
||||
->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_nurse_specialty_show_renders_list_not_auto_open_visit(): void
|
||||
{
|
||||
[$nurseUser] = $this->makeStaff('nurse', 'nurse-spec');
|
||||
|
||||
app(\App\Services\Care\SpecialtyModuleService::class)
|
||||
$modules = app(SpecialtyModuleService::class);
|
||||
$modules->ensureDefaultModulesProvisioned($this->organization, $this->owner->public_id);
|
||||
$modules->activate($this->organization->fresh(), $this->owner->public_id, 'blood_bank');
|
||||
|
||||
$department = Department::query()
|
||||
->where('branch_id', $this->branch->id)
|
||||
->where('type', 'blood_bank')
|
||||
->firstOrFail();
|
||||
|
||||
$patient = Patient::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_number' => 'P-BB-LIST',
|
||||
'first_name' => 'Kwame',
|
||||
'last_name' => 'Donor',
|
||||
'gender' => 'male',
|
||||
'date_of_birth' => '1988-01-01',
|
||||
]);
|
||||
|
||||
$visit = Visit::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_id' => $patient->id,
|
||||
'status' => Visit::STATUS_OPEN,
|
||||
'checked_in_at' => now(),
|
||||
]);
|
||||
|
||||
Appointment::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_id' => $patient->id,
|
||||
'department_id' => $department->id,
|
||||
'visit_id' => $visit->id,
|
||||
'type' => Appointment::TYPE_WALK_IN,
|
||||
'status' => Appointment::STATUS_WAITING,
|
||||
'scheduled_at' => now(),
|
||||
'waiting_at' => now(),
|
||||
'checked_in_at' => now(),
|
||||
]);
|
||||
|
||||
$this->actingAs($nurseUser)
|
||||
->get(route('care.specialty.show', 'blood_bank'))
|
||||
->assertOk()
|
||||
->assertSee('Blood Bank')
|
||||
->assertSee('Patient flow')
|
||||
->assertSee('Waiting')
|
||||
->assertSee('Kwame Donor')
|
||||
->assertDontSee('No open visit selected');
|
||||
|
||||
$this->actingAs($nurseUser)
|
||||
->get(route('care.specialty.workspace', 'blood_bank'))
|
||||
->assertRedirect(route('care.specialty.show', 'blood_bank'));
|
||||
}
|
||||
|
||||
public function test_nurse_specialty_show_redirects_to_workspace_not_queue(): void
|
||||
{
|
||||
[$nurseUser] = $this->makeStaff('nurse', 'nurse-spec-er');
|
||||
|
||||
app(SpecialtyModuleService::class)
|
||||
->ensureDefaultModulesProvisioned($this->organization, $this->owner->public_id);
|
||||
app(\App\Services\Care\SpecialtyModuleService::class)
|
||||
app(SpecialtyModuleService::class)
|
||||
->activate($this->organization->fresh(), $this->owner->public_id, 'emergency');
|
||||
|
||||
$this->actingAs($nurseUser)
|
||||
->get(route('care.specialty.show', 'emergency'))
|
||||
->assertRedirect(route('care.specialty.workspace', 'emergency'));
|
||||
->assertOk()
|
||||
->assertSee('Patient flow')
|
||||
->assertDontSee('href="'.e(route('care.queue.index')).'"', false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,29 @@ class CareQueueBridgeTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Patient-flow board is doctor-only; reception keeps check-in / walk-in helpers.
|
||||
*
|
||||
* @return array{0: User, 1: Member}
|
||||
*/
|
||||
protected function makeDoctor(string $suffix = 'doc'): array
|
||||
{
|
||||
$user = User::create([
|
||||
'public_id' => 'bridge-'.$suffix,
|
||||
'name' => 'Bridge Doctor '.$suffix,
|
||||
'email' => $suffix.'@bridge.example.com',
|
||||
]);
|
||||
$member = Member::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $user->public_id,
|
||||
'role' => 'doctor',
|
||||
'branch_id' => $this->branch->id,
|
||||
]);
|
||||
|
||||
return [$user, $member];
|
||||
}
|
||||
|
||||
public function test_check_in_issues_consultation_ticket_when_integration_on(): void
|
||||
{
|
||||
Http::fake();
|
||||
@@ -164,7 +187,9 @@ class CareQueueBridgeTest extends TestCase
|
||||
'queue_ticket_status' => 'waiting',
|
||||
]);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
[$doctor] = $this->makeDoctor('wait');
|
||||
|
||||
$this->actingAs($doctor)
|
||||
->get(route('care.queue.index'))
|
||||
->assertOk()
|
||||
->assertSee('C042')
|
||||
@@ -198,7 +223,9 @@ class CareQueueBridgeTest extends TestCase
|
||||
'queue_position' => 1,
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->owner)
|
||||
[$doctor] = $this->makeDoctor('backfill');
|
||||
|
||||
$response = $this->actingAs($doctor)
|
||||
->from(route('care.queue.index'))
|
||||
->post(route('care.queue.call-next'), [
|
||||
'branch_id' => $this->branch->id,
|
||||
@@ -221,7 +248,9 @@ class CareQueueBridgeTest extends TestCase
|
||||
{
|
||||
Http::fake();
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
[$doctor] = $this->makeDoctor('empty');
|
||||
|
||||
$this->actingAs($doctor)
|
||||
->from(route('care.queue.index'))
|
||||
->post(route('care.queue.call-next'), ['branch_id' => $this->branch->id])
|
||||
->assertRedirect(route('care.queue.index'))
|
||||
@@ -304,7 +333,9 @@ class CareQueueBridgeTest extends TestCase
|
||||
|
||||
$this->assertSame('called', $appointment->fresh()->queue_ticket_status);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
[$viewer] = $this->makeDoctor('recall-view');
|
||||
|
||||
$this->actingAs($viewer)
|
||||
->get(route('care.queue.index', ['branch_id' => $this->branch->id]))
|
||||
->assertOk()
|
||||
->assertSee($appointment->queue_ticket_number);
|
||||
@@ -368,7 +399,9 @@ class CareQueueBridgeTest extends TestCase
|
||||
]);
|
||||
|
||||
// Board GET must remain read-only; ticket backfill is explicit (Call next / artisan).
|
||||
$this->actingAs($this->owner)
|
||||
[$doctor] = $this->makeDoctor('sync');
|
||||
|
||||
$this->actingAs($doctor)
|
||||
->get(route('care.queue.index', ['branch_id' => $this->branch->id]))
|
||||
->assertOk()
|
||||
->assertSee('Toothache');
|
||||
@@ -484,7 +517,9 @@ class CareQueueBridgeTest extends TestCase
|
||||
$next->fresh(['patient', 'practitioner', 'organization']),
|
||||
);
|
||||
|
||||
$response = $this->actingAs($this->owner)
|
||||
[$doctor] = $this->makeDoctor('advance');
|
||||
|
||||
$response = $this->actingAs($doctor)
|
||||
->post(route('care.queue.call-next'), [
|
||||
'branch_id' => $this->branch->id,
|
||||
'practitioner_id' => $practitioner->id,
|
||||
@@ -499,7 +534,7 @@ class CareQueueBridgeTest extends TestCase
|
||||
$this->assertSame('called', $next->queue_ticket_status);
|
||||
$response->assertRedirect(route('care.appointments.show', $next));
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
$this->actingAs($doctor)
|
||||
->get(route('care.appointments.show', $next))
|
||||
->assertOk()
|
||||
->assertSee('Call again');
|
||||
@@ -675,7 +710,9 @@ class CareQueueBridgeTest extends TestCase
|
||||
'care_entity_uuid' => $appointment->uuid,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
[$caller] = $this->makeDoctor('reclaim');
|
||||
|
||||
$this->actingAs($caller)
|
||||
->from(route('care.queue.index'))
|
||||
->post(route('care.queue.call-next'), [
|
||||
'branch_id' => $this->branch->id,
|
||||
@@ -700,7 +737,7 @@ class CareQueueBridgeTest extends TestCase
|
||||
'status' => 'pending',
|
||||
]);
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
$this->actingAs($caller)
|
||||
->get(route('care.queue.index', [
|
||||
'branch_id' => $this->branch->id,
|
||||
'practitioner_id' => $doctor->id,
|
||||
|
||||
@@ -278,7 +278,7 @@ class CareSpecialtyAccessTest extends TestCase
|
||||
'branch_id' => $this->branch->id,
|
||||
'reason' => 'Chest pain review',
|
||||
])
|
||||
->assertRedirect(route('care.queue.index'));
|
||||
->assertRedirect(route('care.specialty.show', 'cardiology'));
|
||||
|
||||
$this->assertDatabaseHas('care_appointments', [
|
||||
'patient_id' => $patient->id,
|
||||
|
||||
@@ -231,11 +231,13 @@ class CareSpecialtyModulesTest extends TestCase
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.specialty.show', 'dentistry'))
|
||||
->assertRedirect(route('care.specialty.workspace', 'dentistry'));
|
||||
->assertOk()
|
||||
->assertSee('Patient flow');
|
||||
|
||||
$this->actingAs($nurse)
|
||||
->get(route('care.specialty.show', 'dentistry'))
|
||||
->assertRedirect(route('care.specialty.workspace', 'dentistry'));
|
||||
->assertOk()
|
||||
->assertSee('Patient flow');
|
||||
}
|
||||
|
||||
public function test_unassigned_gp_can_open_limited_specialty_module(): void
|
||||
@@ -258,7 +260,8 @@ class CareSpecialtyModulesTest extends TestCase
|
||||
|
||||
$this->actingAs($doctor)
|
||||
->get(route('care.specialty.show', 'dentistry'))
|
||||
->assertRedirect(route('care.queue.index'));
|
||||
->assertOk()
|
||||
->assertSee('Patient flow');
|
||||
}
|
||||
|
||||
public function test_assigned_specialty_doctor_can_open_module(): void
|
||||
@@ -293,7 +296,8 @@ class CareSpecialtyModulesTest extends TestCase
|
||||
|
||||
$this->actingAs($doctor)
|
||||
->get(route('care.specialty.show', 'dentistry'))
|
||||
->assertRedirect(route('care.queue.index'));
|
||||
->assertOk()
|
||||
->assertSee('Patient flow');
|
||||
|
||||
$this->assertTrue(
|
||||
app(SpecialtyModuleService::class)->memberCanAccess(
|
||||
@@ -365,11 +369,13 @@ class CareSpecialtyModulesTest extends TestCase
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.specialty.show', 'emergency'))
|
||||
->assertRedirect(route('care.specialty.workspace', 'emergency'));
|
||||
->assertOk()
|
||||
->assertSee('Patient flow');
|
||||
|
||||
$this->actingAs($nurse)
|
||||
->get(route('care.specialty.show', 'emergency'))
|
||||
->assertRedirect(route('care.specialty.workspace', 'emergency'));
|
||||
->assertOk()
|
||||
->assertSee('Patient flow');
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$service->deactivate($this->organization->fresh(), $this->owner->public_id, 'emergency');
|
||||
|
||||
@@ -96,11 +96,14 @@ class CareSpecialtyShellTest extends TestCase
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.specialty.show', 'emergency'))
|
||||
->assertRedirect(route('care.specialty.workspace', 'emergency'));
|
||||
->assertOk()
|
||||
->assertSee('Patient flow')
|
||||
->assertSee('Waiting');
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.specialty.visits', 'emergency'))
|
||||
->assertRedirect(route('care.specialty.workspace', 'emergency'));
|
||||
->assertOk()
|
||||
->assertSee('Patient flow');
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.specialty.history', 'emergency'))
|
||||
@@ -115,9 +118,7 @@ class CareSpecialtyShellTest extends TestCase
|
||||
|
||||
$this->actingAs($this->owner)
|
||||
->get(route('care.specialty.workspace', 'emergency'))
|
||||
->assertOk()
|
||||
->assertSee('No open visit selected')
|
||||
->assertDontSee('Back to queue');
|
||||
->assertRedirect(route('care.specialty.show', 'emergency'));
|
||||
}
|
||||
|
||||
public function test_dentistry_shell_has_chair_stage_and_catalog(): void
|
||||
|
||||
Reference in New Issue
Block a user