From 94e53d05bf22aea7147feb71eacc84088585b327 Mon Sep 17 00:00:00 2001 From: isaacclad Date: Fri, 17 Jul 2026 17:48:34 +0000 Subject: [PATCH] Keep consultation context on nested clinical pages and add Call again on patient queue cards. Doctors can return to the active consultation from forms, lab, prescriptions, pathways, and bills; queue cards can re-announce called/serving tickets via the existing Queue recall bridge. Co-authored-by: Cursor --- .../Controllers/Care/AssessmentController.php | 35 ++- app/Http/Controllers/Care/BillController.php | 18 +- .../Care/ConsultationController.php | 5 + .../Care/InvestigationController.php | 6 + .../Controllers/Care/PathwayController.php | 9 +- .../Care/PrescriptionController.php | 16 +- app/Http/Controllers/Care/QueueController.php | 15 ++ .../Care/ConsultationReturnContext.php | 146 ++++++++++++ .../views/care/assessments/index.blade.php | 2 + .../views/care/assessments/show.blade.php | 7 +- resources/views/care/bills/show.blade.php | 2 + .../views/care/consultations/show.blade.php | 23 +- .../views/care/lab/requests/show.blade.php | 2 + resources/views/care/pathways/index.blade.php | 2 + .../views/care/prescriptions/create.blade.php | 3 + .../views/care/prescriptions/show.blade.php | 2 + resources/views/care/queue/index.blade.php | 38 ++- .../care/consultation-return.blade.php | 41 ++++ routes/web.php | 1 + .../CareConsultationReturnContextTest.php | 224 ++++++++++++++++++ tests/Feature/CareQueueBridgeTest.php | 48 ++++ 21 files changed, 615 insertions(+), 30 deletions(-) create mode 100644 app/Services/Care/ConsultationReturnContext.php create mode 100644 resources/views/components/care/consultation-return.blade.php create mode 100644 tests/Feature/CareConsultationReturnContextTest.php diff --git a/app/Http/Controllers/Care/AssessmentController.php b/app/Http/Controllers/Care/AssessmentController.php index 2c9dc8b..fe22fbd 100644 --- a/app/Http/Controllers/Care/AssessmentController.php +++ b/app/Http/Controllers/Care/AssessmentController.php @@ -12,6 +12,7 @@ use App\Models\Visit; use App\Services\Care\AssessmentService; use App\Services\Care\CareFeatures; use App\Services\Care\CarePermissions; +use App\Services\Care\ConsultationReturnContext; use App\Services\Care\FhirAssessmentExporter; use App\Services\Care\OrganizationResolver; use Illuminate\Http\JsonResponse; @@ -54,6 +55,7 @@ class AssessmentController extends Controller 'statuses' => config('care.assessment_statuses'), 'categories' => config('care.assessment_template_categories'), 'filters' => $request->only(['status', 'template_code', 'category']), + 'returnConsultation' => app(ConsultationReturnContext::class)->resolve($request, (int) $patient->id), ]); } @@ -111,8 +113,13 @@ class AssessmentController extends Controller $context, ); + $return = app(ConsultationReturnContext::class); + return redirect() - ->route('care.assessments.show', $assessment) + ->route('care.assessments.show', array_filter([ + 'assessment' => $assessment, + ...$return->routeQuery($request), + ])) ->with('success', 'Assessment started.'); } @@ -141,8 +148,14 @@ class AssessmentController extends Controller ], ); + $return = app(ConsultationReturnContext::class); + $return->remember($consultation); + return redirect() - ->route('care.assessments.show', $assessment) + ->route('care.assessments.show', array_filter([ + 'assessment' => $assessment, + ...$return->query($consultation), + ])) ->with('success', 'Assessment started.'); } @@ -177,11 +190,15 @@ class AssessmentController extends Controller } } + $return = app(ConsultationReturnContext::class); + return view('care.assessments.show', [ 'assessment' => $assessment, 'answersByCode' => $answersByCode, 'canEdit' => $canEdit, 'statuses' => config('care.assessment_statuses'), + 'returnConsultation' => $return->resolve($request, (int) $assessment->patient_id) + ?? $return->resolveLinked($request, $assessment->consultation), ]); } @@ -205,8 +222,13 @@ class AssessmentController extends Controller array_key_exists('notes', $validated) ? $validated['notes'] : null, ); + $return = app(ConsultationReturnContext::class); + return redirect() - ->route('care.assessments.show', $assessment) + ->route('care.assessments.show', array_filter([ + 'assessment' => $assessment, + ...$return->routeQuery($request), + ])) ->with('success', 'Assessment saved.'); } @@ -240,8 +262,13 @@ class AssessmentController extends Controller $this->ownerRef($request), ); + $return = app(ConsultationReturnContext::class); + return redirect() - ->route('care.assessments.show', $assessment) + ->route('care.assessments.show', array_filter([ + 'assessment' => $assessment, + ...$return->routeQuery($request), + ])) ->with('success', 'Assessment completed.'); } diff --git a/app/Http/Controllers/Care/BillController.php b/app/Http/Controllers/Care/BillController.php index 0ceba65..8c48ab0 100644 --- a/app/Http/Controllers/Care/BillController.php +++ b/app/Http/Controllers/Care/BillController.php @@ -5,10 +5,12 @@ namespace App\Http\Controllers\Care; use App\Http\Controllers\Controller; use App\Http\Controllers\Care\Concerns\ScopesToAccount; use App\Models\Bill; +use App\Models\Consultation; use App\Models\Visit; use App\Services\Care\BillService; use App\Services\Care\CareQueueBridge; use App\Services\Care\CareQueueContexts; +use App\Services\Care\ConsultationReturnContext; use App\Services\Care\OrganizationResolver; use App\Services\Payments\MerchantGatewayService; use Illuminate\Http\RedirectResponse; @@ -100,8 +102,12 @@ class BillController extends Controller $bill = $this->bills->generateFromVisit($visit, $this->ownerRef($request), $this->ownerRef($request)); - return redirect()->route('care.bills.show', $bill) - ->with('success', 'Bill generated from visit.'); + $return = app(ConsultationReturnContext::class); + + return redirect()->route('care.bills.show', array_filter([ + 'bill' => $bill, + ...$return->routeQuery($request), + ]))->with('success', 'Bill generated from visit.'); } public function show(Request $request, Bill $bill): View @@ -124,6 +130,12 @@ class BillController extends Controller ->except(['gateway']) ->all(); + $return = app(ConsultationReturnContext::class); + $linked = Consultation::query() + ->where('visit_id', $bill->visit_id) + ->orderByDesc('id') + ->first(); + return view('care.bills.show', [ 'bill' => $bill, 'statuses' => config('care.bill_statuses'), @@ -135,6 +147,8 @@ class BillController extends Controller 'gatewayConfigured' => $gatewayConfigured, 'gatewayProvider' => $gateway?->provider, 'gatewayLabels' => config('care.payment_gateways'), + 'returnConsultation' => $return->resolve($request, (int) $bill->patient_id) + ?? $return->resolveLinked($request, $linked), ]); } diff --git a/app/Http/Controllers/Care/ConsultationController.php b/app/Http/Controllers/Care/ConsultationController.php index f526bd8..c88e3de 100644 --- a/app/Http/Controllers/Care/ConsultationController.php +++ b/app/Http/Controllers/Care/ConsultationController.php @@ -11,6 +11,7 @@ use App\Models\Assessment; use App\Models\AssessmentTemplate; use App\Services\Care\CareFeatures; use App\Services\Care\CarePermissions; +use App\Services\Care\ConsultationReturnContext; use App\Services\Care\ConsultationService; use App\Services\Care\OrganizationResolver; use App\Services\Care\PathwayService; @@ -37,6 +38,8 @@ class ConsultationController extends Controller 'investigationRequests.investigationType', 'prescriptions.items', ]); + app(ConsultationReturnContext::class)->remember($consultation); + $permissions = app(CarePermissions::class); $member = $this->member($request); $organization = $this->organization($request); @@ -235,6 +238,8 @@ class ConsultationController extends Controller $this->ownerRef($request), ); + app(ConsultationReturnContext::class)->forget(); + return redirect()->route('care.patients.show', $consultation->patient) ->with('success', 'Consultation completed.'); } diff --git a/app/Http/Controllers/Care/InvestigationController.php b/app/Http/Controllers/Care/InvestigationController.php index 67453c5..62c3663 100644 --- a/app/Http/Controllers/Care/InvestigationController.php +++ b/app/Http/Controllers/Care/InvestigationController.php @@ -11,6 +11,7 @@ use App\Models\InvestigationType; use App\Models\Member; use App\Services\Care\CareQueueBridge; use App\Services\Care\CareQueueContexts; +use App\Services\Care\ConsultationReturnContext; use App\Services\Care\InvestigationService; use App\Services\Care\OrganizationResolver; use Illuminate\Http\RedirectResponse; @@ -149,6 +150,7 @@ class InvestigationController extends Controller $investigation->load([ 'patient', 'investigationType', 'practitioner', 'branch', 'result.values', 'result.attachments', 'assignedMember', + 'consultation.patient', 'consultation.appointment', ]); $canManage = app(\App\Services\Care\CarePermissions::class) @@ -156,11 +158,15 @@ class InvestigationController extends Controller $canViewResults = app(\App\Services\Care\CarePermissions::class) ->can($this->member($request), 'lab.results.view'); + $return = app(ConsultationReturnContext::class); + return view('care.lab.requests.show', [ 'investigation' => $investigation, 'statuses' => config('care.investigation_statuses'), 'canManage' => $canManage, 'canViewResults' => $canViewResults, + 'returnConsultation' => $return->resolve($request, (int) $investigation->patient_id) + ?? $return->resolveLinked($request, $investigation->consultation), ]); } diff --git a/app/Http/Controllers/Care/PathwayController.php b/app/Http/Controllers/Care/PathwayController.php index b1084bb..61734d6 100644 --- a/app/Http/Controllers/Care/PathwayController.php +++ b/app/Http/Controllers/Care/PathwayController.php @@ -10,6 +10,7 @@ use App\Models\Patient; use App\Models\PatientPathway; use App\Services\Care\CareFeatures; use App\Services\Care\CarePermissions; +use App\Services\Care\ConsultationReturnContext; use App\Services\Care\OrganizationResolver; use App\Services\Care\PathwayService; use Illuminate\Http\JsonResponse; @@ -51,6 +52,7 @@ class PathwayController extends Controller 'catalog' => $catalog, 'canManage' => $canManage, 'statuses' => config('care.patient_pathway_statuses'), + 'returnConsultation' => app(ConsultationReturnContext::class)->resolve($request, (int) $patient->id), ]); } @@ -91,8 +93,13 @@ class PathwayController extends Controller ], ); + $return = app(ConsultationReturnContext::class); + return redirect() - ->route('care.pathways.index', $patient) + ->route('care.pathways.index', array_filter([ + 'patient' => $patient, + ...$return->routeQuery($request), + ])) ->with('success', "Pathway “{$pathway->name}” activated."); } diff --git a/app/Http/Controllers/Care/PrescriptionController.php b/app/Http/Controllers/Care/PrescriptionController.php index d462d89..0eabfc9 100644 --- a/app/Http/Controllers/Care/PrescriptionController.php +++ b/app/Http/Controllers/Care/PrescriptionController.php @@ -9,6 +9,7 @@ use App\Models\Prescription; use App\Models\Practitioner; use App\Services\Care\CareQueueBridge; use App\Services\Care\CareQueueContexts; +use App\Services\Care\ConsultationReturnContext; use App\Services\Care\OrganizationResolver; use App\Services\Care\PharmacyService; use App\Services\Care\PrescriptionService; @@ -126,6 +127,8 @@ class PrescriptionController extends Controller $consultation->load(['patient', 'practitioner']); + app(ConsultationReturnContext::class)->remember($consultation); + $practitioners = Practitioner::owned($this->ownerRef($request)) ->where('organization_id', $this->organization($request)->id) ->where('is_active', true) @@ -151,8 +154,13 @@ class PrescriptionController extends Controller $this->ownerRef($request), ); - return redirect()->route('care.prescriptions.show', $prescription) - ->with('success', 'Prescription created.'); + $return = app(ConsultationReturnContext::class); + $return->remember($consultation); + + return redirect()->route('care.prescriptions.show', array_filter([ + 'prescription' => $prescription, + ...$return->query($consultation), + ]))->with('success', 'Prescription created.'); } public function show(Request $request, Prescription $prescription): View @@ -167,12 +175,16 @@ class PrescriptionController extends Controller $canDispense = app(\App\Services\Care\CarePermissions::class) ->can($this->member($request), 'prescriptions.dispense'); + $return = app(ConsultationReturnContext::class); + return view('care.prescriptions.show', [ 'prescription' => $prescription, 'statuses' => config('care.prescription_statuses'), 'routes' => config('care.medication_routes'), 'canManage' => $canManage, 'canDispense' => $canDispense, + 'returnConsultation' => $return->resolve($request, (int) $prescription->patient_id) + ?? $return->resolveLinked($request, $prescription->consultation), ]); } diff --git a/app/Http/Controllers/Care/QueueController.php b/app/Http/Controllers/Care/QueueController.php index 4f256a6..7e89718 100644 --- a/app/Http/Controllers/Care/QueueController.php +++ b/app/Http/Controllers/Care/QueueController.php @@ -138,6 +138,21 @@ class QueueController extends Controller return back()->with('success', 'Completed '.$ticket['ticket_number'].'.'); } + public function recall(Request $request, Appointment $appointment): RedirectResponse + { + $this->authorizeAbility($request, 'consultations.manage'); + $this->authorizeAppointment($request, $appointment); + $organization = $this->organization($request); + abort_unless($this->queueBridge->isEnabled($organization), 404); + + $ticket = $this->queueBridge->recall($organization, $appointment); + if (! $ticket) { + return back()->with('error', 'Could not call patient again.'); + } + + return back()->with('success', 'Called again '.$ticket['ticket_number'].'.'); + } + public function start(Request $request, Appointment $appointment): RedirectResponse { $this->authorizeAbility($request, 'consultations.manage'); diff --git a/app/Services/Care/ConsultationReturnContext.php b/app/Services/Care/ConsultationReturnContext.php new file mode 100644 index 0000000..f999cca --- /dev/null +++ b/app/Services/Care/ConsultationReturnContext.php @@ -0,0 +1,146 @@ +status === Consultation::STATUS_COMPLETED) { + $this->forget(); + + return; + } + + session([self::SESSION_KEY => $consultation->uuid]); + } + + public function forget(): void + { + session()->forget(self::SESSION_KEY); + } + + public function uuidFrom(Request $request): ?string + { + foreach ([ + $request->query(self::QUERY_KEY), + $request->input(self::QUERY_KEY), + session(self::SESSION_KEY), + ] as $candidate) { + if (is_string($candidate) && Str::isUuid($candidate)) { + return $candidate; + } + } + + return null; + } + + /** + * Capture an explicit from_consultation posted/queried value into session + * so subsequent redirects keep the serving flow. + */ + public function capture(Request $request): void + { + $uuid = $request->query(self::QUERY_KEY) ?? $request->input(self::QUERY_KEY); + if (is_string($uuid) && Str::isUuid($uuid)) { + session([self::SESSION_KEY => $uuid]); + } + } + + public function resolve(Request $request, ?int $patientId = null): ?Consultation + { + $this->capture($request); + + $uuid = $this->uuidFrom($request); + if ($uuid === null) { + return null; + } + + $consultation = Consultation::query() + ->where('uuid', $uuid) + ->with(['patient', 'appointment']) + ->first(); + + if (! $consultation) { + return null; + } + + if ($patientId !== null && (int) $consultation->patient_id !== $patientId) { + return null; + } + + return $consultation; + } + + /** + * Resolve from an explicit linked consultation (e.g. prescription.consultation) + * only when the doctor is already in a serving flow for that consultation. + */ + public function resolveLinked(Request $request, ?Consultation $linked): ?Consultation + { + if (! $linked) { + return null; + } + + $active = $this->resolve($request, (int) $linked->patient_id); + if ($active && $active->id === $linked->id) { + return $active; + } + + // Nested under consultation route (create prescription) or still in progress + // with an explicit query/session match already handled above. For linked + // records opened mid-flow without query (POST redirect), prefer the linked + // consultation when session matches its uuid. + $uuid = $this->uuidFrom($request); + if ($uuid !== null && $linked->uuid === $uuid) { + $linked->loadMissing(['patient', 'appointment']); + + return $linked; + } + + return null; + } + + /** + * @return array{from_consultation: string} + */ + public function query(Consultation $consultation): array + { + return [self::QUERY_KEY => $consultation->uuid]; + } + + public function withQuery(string $url, Consultation $consultation): string + { + $separator = str_contains($url, '?') ? '&' : '?'; + + return $url.$separator.self::QUERY_KEY.'='.urlencode($consultation->uuid); + } + + /** + * Route parameters to keep return context on redirects (empty when none). + * + * @return array + */ + public function routeQuery(Request $request): array + { + $this->capture($request); + $uuid = $this->uuidFrom($request); + + return $uuid ? [self::QUERY_KEY => $uuid] : []; + } +} diff --git a/resources/views/care/assessments/index.blade.php b/resources/views/care/assessments/index.blade.php index f873ecd..3498766 100644 --- a/resources/views/care/assessments/index.blade.php +++ b/resources/views/care/assessments/index.blade.php @@ -1,4 +1,6 @@ + +

Clinical assessments

diff --git a/resources/views/care/assessments/show.blade.php b/resources/views/care/assessments/show.blade.php index c8feb6c..77047cc 100644 --- a/resources/views/care/assessments/show.blade.php +++ b/resources/views/care/assessments/show.blade.php @@ -1,4 +1,6 @@ + +

@@ -8,7 +10,7 @@

{{ $assessment->patient->fullName() }} · {{ $assessment->template->code }} v{{ $assessment->template->version }} - @if ($assessment->consultation) + @if ($assessment->consultation && ! ($returnConsultation ?? null)) · Consultation @endif

@@ -58,6 +60,9 @@ @csrf @method('PUT') @include('care.assessments._form', ['assessment' => $assessment, 'answersByCode' => $answersByCode]) + @if (($returnConsultation ?? null)?->uuid) + + @endif
@endif @@ -62,7 +63,7 @@

Requested tests

@foreach ($consultation->investigationRequests as $req)

- {{ $req->investigationType->name }} + {{ $req->investigationType->name }} · {{ config('care.investigation_statuses')[$req->status] ?? $req->status }}

@endforeach @@ -77,7 +78,7 @@
@if ($universalAssessment) - + {{ $universalAssessment->isDraft() ? 'Continue form' : 'View form' }} @elseif (($canCaptureUniversal ?? false) && ! $isCompleted)
@csrf +
@endif @@ -149,7 +151,7 @@

Condition pathways

- View all + View all
@if (($activePathways ?? collect())->isNotEmpty()) @@ -187,13 +189,14 @@ Form not available yet @elseif ($instrument['assessment']) {{ $assessmentStatuses[$instrument['assessment']->status] ?? $instrument['assessment']->status }} - + {{ $instrument['assessment']->isDraft() ? 'Continue' : 'View' }} @elseif (($canCaptureAssessment ?? false) && ! $isCompleted)
@csrf +
@else @@ -222,6 +225,7 @@ @csrf + @endif @@ -242,14 +246,14 @@

Clinical forms this visit

- All patient forms + All patient forms
@if (($consultationAssessments ?? collect())->isNotEmpty())
    @foreach ($consultationAssessments as $item)
  • - + {{ $item->template->name }} · {{ ($assessmentStatuses[$item->status] ?? $item->status) }} @@ -263,6 +267,7 @@ @if (($canCaptureAssessment ?? false) && ! $isCompleted && ($startableTemplates ?? collect())->isNotEmpty())
    @csrf +
    +
diff --git a/resources/views/care/prescriptions/show.blade.php b/resources/views/care/prescriptions/show.blade.php index 8eb4c57..36ac1e6 100644 --- a/resources/views/care/prescriptions/show.blade.php +++ b/resources/views/care/prescriptions/show.blade.php @@ -1,4 +1,6 @@ + +

{{ $statuses[$prescription->status] ?? $prescription->status }}

diff --git a/resources/views/care/queue/index.blade.php b/resources/views/care/queue/index.blade.php index 0ce03d1..74c8c4f 100644 --- a/resources/views/care/queue/index.blade.php +++ b/resources/views/care/queue/index.blade.php @@ -67,14 +67,22 @@

{{ $appointment->patient->patient_number }} · {{ $appointment->reason ?? '—' }}

- @if ($canConsult) - - @csrf - - - @endif +
+ @if (! empty($queueIntegration['enabled']) && $appointment->queue_ticket_uuid && in_array($appointment->queue_ticket_status ?? '', ['called', 'serving'], true) && $canConsult) +
+ @csrf + +
+ @endif + @if ($canConsult) +
+ @csrf + +
+ @endif +
@empty

No patients waiting.

@@ -99,9 +107,17 @@

{{ $appointment->practitioner?->name ?? 'Unassigned' }}

- @if ($appointment->consultation) - Open - @endif +
+ @if (! empty($queueIntegration['enabled']) && $appointment->queue_ticket_uuid && in_array($appointment->queue_ticket_status ?? '', ['called', 'serving'], true) && $canConsult) +
+ @csrf + +
+ @endif + @if ($appointment->consultation) + Open + @endif +
@empty

No active consultations.

diff --git a/resources/views/components/care/consultation-return.blade.php b/resources/views/components/care/consultation-return.blade.php new file mode 100644 index 0000000..3fbd027 --- /dev/null +++ b/resources/views/components/care/consultation-return.blade.php @@ -0,0 +1,41 @@ +@props([ + 'consultation' => null, +]) + +@php + use App\Services\Care\ConsultationReturnContext; + + /** @var \App\Models\Consultation|null $consultation */ + $consultation = $consultation + ?? app(ConsultationReturnContext::class)->resolve(request()); + + $appointment = $consultation?->appointment; + $queueOn = filled($appointment?->queue_ticket_number); +@endphp + +@if ($consultation) +
merge(['class' => 'mb-6 flex flex-wrap items-center justify-between gap-3 rounded-xl border border-amber-200 bg-amber-50/80 px-4 py-3']) }}> +
+

Continue consultation

+

+ {{ $consultation->patient->fullName() }} + @if ($queueOn) + + @include('care.partials.queue-ticket', [ + 'ticketNumber' => $appointment->queue_ticket_number, + 'ticketStatus' => $appointment->queue_ticket_status, + 'destination' => $appointment->queue_destination, + 'staffDisplayName' => $appointment->queue_staff_display_name, + ]) + + @endif +

+
+ + Back to consultation + +
+@endif diff --git a/routes/web.php b/routes/web.php index 4144a15..fb1e7e7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -90,6 +90,7 @@ Route::middleware(['auth', 'platform.session'])->group(function () { Route::get('/queue', [QueueController::class, 'index'])->name('care.queue.index'); Route::post('/queue/call-next', [QueueController::class, 'callNext'])->name('care.queue.call-next'); Route::post('/queue/{appointment}/start', [QueueController::class, 'start'])->name('care.queue.start'); + Route::post('/queue/{appointment}/recall', [QueueController::class, 'recall'])->name('care.queue.recall'); Route::post('/queue/{appointment}/complete-ticket', [QueueController::class, 'completeTicket'])->name('care.queue.complete-ticket'); Route::middleware('care.paid')->group(function () { diff --git a/tests/Feature/CareConsultationReturnContextTest.php b/tests/Feature/CareConsultationReturnContextTest.php new file mode 100644 index 0000000..1155938 --- /dev/null +++ b/tests/Feature/CareConsultationReturnContextTest.php @@ -0,0 +1,224 @@ +withoutMiddleware(EnsurePlatformSession::class); + + $this->user = User::create([ + 'public_id' => 'return-ctx-user', + 'name' => 'Doctor', + 'email' => 'return-ctx@example.com', + ]); + + $this->organization = Organization::create([ + 'owner_ref' => $this->user->public_id, + 'name' => 'Return Clinic', + 'slug' => 'return-clinic', + 'timezone' => 'UTC', + 'settings' => [ + 'onboarded' => true, + 'plan' => 'pro', + 'plan_expires_at' => now()->addMonth()->toIso8601String(), + 'rollout' => [ + CareFeatures::ASSESSMENTS_ENGINE => true, + ], + ], + ]); + + Member::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'user_ref' => $this->user->public_id, + 'role' => 'hospital_admin', + ]); + + $this->branch = Branch::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'name' => 'Main', + 'is_active' => true, + ]); + + $this->patient = Patient::create([ + 'uuid' => (string) Str::uuid(), + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_number' => 'P-RET-1', + 'first_name' => 'Ada', + 'last_name' => 'Lovelace', + ]); + + $this->visit = Visit::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_id' => $this->patient->id, + 'status' => Visit::STATUS_OPEN, + 'checked_in_at' => now(), + ]); + + $this->consultation = Consultation::create([ + 'owner_ref' => $this->user->public_id, + 'visit_id' => $this->visit->id, + 'patient_id' => $this->patient->id, + 'status' => Consultation::STATUS_DRAFT, + 'started_at' => now(), + ]); + } + + public function test_consultation_show_remembers_return_context(): void + { + $this->actingAs($this->user) + ->get(route('care.consultations.show', $this->consultation)) + ->assertOk() + ->assertSessionHas(ConsultationReturnContext::SESSION_KEY, $this->consultation->uuid); + } + + public function test_nested_pages_show_back_to_consultation_when_context_present(): void + { + $type = InvestigationType::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'name' => 'CBC', + 'category' => 'haematology', + 'code' => 'cbc', + 'is_active' => true, + ]); + + $lab = InvestigationRequest::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_id' => $this->patient->id, + 'visit_id' => $this->visit->id, + 'consultation_id' => $this->consultation->id, + 'investigation_type_id' => $type->id, + 'status' => InvestigationRequest::STATUS_PENDING, + 'priority' => 'routine', + ]); + + $rx = Prescription::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'visit_id' => $this->visit->id, + 'consultation_id' => $this->consultation->id, + 'patient_id' => $this->patient->id, + 'status' => Prescription::STATUS_ACTIVE, + ]); + + $this->actingAs($this->user) + ->withSession([ConsultationReturnContext::SESSION_KEY => $this->consultation->uuid]) + ->get(route('care.lab.requests.show', [ + 'investigation' => $lab, + 'from_consultation' => $this->consultation->uuid, + ])) + ->assertOk() + ->assertSee('Back to consultation') + ->assertSee('Continue consultation') + ->assertSee('Ada Lovelace'); + + $this->actingAs($this->user) + ->withSession([ConsultationReturnContext::SESSION_KEY => $this->consultation->uuid]) + ->get(route('care.prescriptions.show', [ + 'prescription' => $rx, + 'from_consultation' => $this->consultation->uuid, + ])) + ->assertOk() + ->assertSee('Back to consultation'); + + $this->actingAs($this->user) + ->withSession([ConsultationReturnContext::SESSION_KEY => $this->consultation->uuid]) + ->get(route('care.pathways.index', [ + 'patient' => $this->patient, + 'from_consultation' => $this->consultation->uuid, + ])) + ->assertOk() + ->assertSee('Back to consultation'); + + $this->actingAs($this->user) + ->get(route('care.prescriptions.create', $this->consultation)) + ->assertOk() + ->assertSee('Back to consultation'); + } + + public function test_direct_access_without_context_hides_consultation_chrome(): void + { + $type = InvestigationType::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'name' => 'CBC', + 'category' => 'haematology', + 'code' => 'cbc2', + 'is_active' => true, + ]); + + $lab = InvestigationRequest::create([ + 'owner_ref' => $this->user->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_id' => $this->patient->id, + 'visit_id' => $this->visit->id, + 'consultation_id' => $this->consultation->id, + 'investigation_type_id' => $type->id, + 'status' => InvestigationRequest::STATUS_PENDING, + 'priority' => 'routine', + ]); + + $this->actingAs($this->user) + ->get(route('care.lab.requests.show', $lab)) + ->assertOk() + ->assertDontSee('Back to consultation') + ->assertDontSee('Continue consultation'); + + $this->actingAs($this->user) + ->get(route('care.pathways.index', $this->patient)) + ->assertOk() + ->assertDontSee('Back to consultation'); + } + + public function test_completing_consultation_clears_return_context(): void + { + $this->actingAs($this->user) + ->withSession([ConsultationReturnContext::SESSION_KEY => $this->consultation->uuid]) + ->post(route('care.consultations.complete', $this->consultation)) + ->assertRedirect(route('care.patients.show', $this->patient)) + ->assertSessionMissing(ConsultationReturnContext::SESSION_KEY); + } +} diff --git a/tests/Feature/CareQueueBridgeTest.php b/tests/Feature/CareQueueBridgeTest.php index dff2dbb..8bb28e8 100644 --- a/tests/Feature/CareQueueBridgeTest.php +++ b/tests/Feature/CareQueueBridgeTest.php @@ -320,5 +320,53 @@ class CareQueueBridgeTest extends TestCase ->assertSessionHas('info', 'No patients waiting at your consultation service point.') ->assertSessionMissing('error'); } + + public function test_recall_reannounces_called_ticket_from_patient_queue(): void + { + $appointment = Appointment::create([ + 'owner_ref' => $this->owner->public_id, + 'organization_id' => $this->organization->id, + 'branch_id' => $this->branch->id, + 'patient_id' => $this->patient->id, + 'type' => Appointment::TYPE_WALK_IN, + 'status' => Appointment::STATUS_WAITING, + 'scheduled_at' => now(), + 'waiting_at' => now(), + 'queue_position' => 1, + 'queue_ticket_uuid' => 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', + 'queue_ticket_number' => 'C055', + 'queue_ticket_status' => 'called', + ]); + + Http::fake([ + '*/tickets/*/action*' => Http::response([ + 'data' => [ + 'uuid' => 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', + 'ticket_number' => 'C055', + 'status' => 'called', + 'customer_name' => 'Ada Lovelace', + ], + ], 200), + ]); + + $this->actingAs($this->owner) + ->from(route('care.queue.index')) + ->post(route('care.queue.recall', $appointment)) + ->assertRedirect(route('care.queue.index')) + ->assertSessionHas('success'); + + Http::assertSent(function ($request) { + return str_contains($request->url(), '/tickets/eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee/action') + && ($request['action'] ?? null) === 'recall'; + }); + + $this->assertSame('called', $appointment->fresh()->queue_ticket_status); + + $this->actingAs($this->owner) + ->get(route('care.queue.index', ['branch_id' => $this->branch->id])) + ->assertOk() + ->assertSee('Call again') + ->assertSee('C055'); + } }