diff --git a/app/Http/Controllers/Care/QueueController.php b/app/Http/Controllers/Care/QueueController.php
index 2c8ecd7..bf5b04a 100644
--- a/app/Http/Controllers/Care/QueueController.php
+++ b/app/Http/Controllers/Care/QueueController.php
@@ -11,6 +11,7 @@ use App\Services\Care\BranchContext;
use App\Services\Care\CarePermissions;
use App\Services\Care\CareQueueBridge;
use App\Services\Care\CareQueueContexts;
+use App\Services\Care\CareQueueSessionAdvance;
use App\Services\Care\ConsultationService;
use App\Services\Care\OrganizationResolver;
use Illuminate\Http\RedirectResponse;
@@ -157,7 +158,7 @@ class QueueController extends Controller
]);
}
- public function callNext(Request $request): RedirectResponse
+ public function callNext(Request $request, CareQueueSessionAdvance $advance): RedirectResponse
{
abort_unless(app(CarePermissions::class)->handlesFloorCare($this->member($request)), 403);
$this->authorizeAbility($request, 'appointments.view');
@@ -165,6 +166,7 @@ class QueueController extends Controller
abort_unless($this->queueBridge->isEnabled($organization), 404);
$member = $this->member($request);
+ $owner = $this->ownerRef($request);
$practitionerScope = app(OrganizationResolver::class)->practitionerScope($organization, $member);
$branchId = $this->branchContext->resolve($request, $organization, $member);
$practitionerId = $request->input('practitioner_id') ? (int) $request->input('practitioner_id') : null;
@@ -172,7 +174,7 @@ class QueueController extends Controller
if ($practitionerScope !== null) {
abort_unless($practitionerScope !== [], 422);
$practitionerId = $practitionerScope[0];
- $prac = Practitioner::owned($this->ownerRef($request))->find($practitionerId);
+ $prac = Practitioner::owned($owner)->find($practitionerId);
$branchId = (int) ($prac?->branch_id ?: $branchId);
}
@@ -180,13 +182,20 @@ class QueueController extends Controller
$context = CareQueueContexts::CONSULTATION;
if ($practitionerId) {
- $prac = Practitioner::owned($this->ownerRef($request))->find($practitionerId);
+ $prac = Practitioner::owned($owner)->find($practitionerId);
$specialtyContext = $this->queueBridge->contextForPractitioner($organization, $prac);
if ($specialtyContext !== CareQueueContexts::CONSULTATION) {
$context = $specialtyContext;
}
}
+ if ($request->filled('appointment_id')) {
+ $current = Appointment::owned($owner)
+ ->where('organization_id', $organization->id)
+ ->findOrFail((int) $request->input('appointment_id'));
+ $advance->endCurrentEncounter($current, $owner, $owner);
+ }
+
$result = $this->queueBridge->callNext(
$organization,
$context,
@@ -203,6 +212,11 @@ class QueueController extends Controller
return back()->with('info', "No patients waiting at your {$label} service point.");
}
+ $entity = $result['entity'] ?? null;
+ if ($entity instanceof Appointment) {
+ return $advance->redirectToOpenedPatient($entity, $ticket);
+ }
+
return back()->with('success', 'Called '.$ticket['ticket_number'].' — '.($ticket['customer_name'] ?? 'patient').'.');
}
diff --git a/app/Http/Controllers/Care/SpecialtyModuleController.php b/app/Http/Controllers/Care/SpecialtyModuleController.php
index 7b11a79..14c37dc 100644
--- a/app/Http/Controllers/Care/SpecialtyModuleController.php
+++ b/app/Http/Controllers/Care/SpecialtyModuleController.php
@@ -10,6 +10,7 @@ use App\Models\PatientAttachment;
use App\Models\Visit;
use App\Services\Care\AppointmentService;
use App\Services\Care\CareQueueBridge;
+use App\Services\Care\CareQueueSessionAdvance;
use App\Services\Care\ConsultationService;
use App\Services\Care\OrganizationResolver;
use App\Services\Care\SpecialtyClinicalRecordService;
@@ -309,12 +310,14 @@ class SpecialtyModuleController extends Controller
string $module,
SpecialtyModuleService $modules,
CareQueueBridge $queueBridge,
+ CareQueueSessionAdvance $advance,
): RedirectResponse {
$definition = $modules->definition($module);
abort_unless($definition, 404);
$organization = $this->organization($request);
$member = $this->member($request);
+ $owner = $this->ownerRef($request);
abort_unless($modules->memberCanAccess($organization, $member, $module), 403);
abort_unless($queueBridge->isEnabled($organization), 404);
@@ -327,7 +330,7 @@ class SpecialtyModuleController extends Controller
if ($practitionerScope !== null) {
abort_unless($practitionerScope !== [], 422);
$practitionerId = $practitionerScope[0];
- $pracBranch = (int) \App\Models\Practitioner::owned($this->ownerRef($request))
+ $pracBranch = (int) \App\Models\Practitioner::owned($owner)
->whereKey($practitionerId)
->value('branch_id');
if ($pracBranch > 0) {
@@ -337,10 +340,16 @@ class SpecialtyModuleController extends Controller
abort_unless($branchId > 0, 422);
+ if ($request->filled('appointment_id')) {
+ $current = Appointment::owned($owner)
+ ->where('organization_id', $organization->id)
+ ->findOrFail((int) $request->input('appointment_id'));
+ $advance->endCurrentEncounter($current, $owner, $owner);
+ }
+
$result = $queueBridge->callNext($organization, $module, $branchId, $member, $practitionerId);
$ticket = $result['ticket'] ?? null;
if (! $ticket) {
- $owner = $this->ownerRef($request);
$departmentIds = Department::owned($owner)
->where('type', $definition['department_type'] ?? 'general')
->where('is_active', true)
@@ -381,6 +390,15 @@ class SpecialtyModuleController extends Controller
return back()->with('info', 'No patients waiting in this queue.');
}
+ $entity = $result['entity'] ?? null;
+ if ($entity instanceof Appointment) {
+ return $advance->redirectToOpenedPatient(
+ $entity,
+ $ticket,
+ 'care.queue.index',
+ );
+ }
+
$name = $ticket['customer_name'] ?? 'patient';
return back()->with('success', 'Called '.$ticket['ticket_number'].' — '.$name.'.');
diff --git a/app/Services/Care/CareQueueSessionAdvance.php b/app/Services/Care/CareQueueSessionAdvance.php
new file mode 100644
index 0000000..9566b13
--- /dev/null
+++ b/app/Services/Care/CareQueueSessionAdvance.php
@@ -0,0 +1,82 @@
+loadMissing(['consultation', 'organization']);
+
+ $consultation = $appointment->consultation;
+ if ($consultation && $consultation->status !== Consultation::STATUS_COMPLETED) {
+ $this->consultations->complete($consultation, $ownerRef, $actorRef);
+
+ return;
+ }
+
+ if ($appointment->status === Appointment::STATUS_IN_CONSULTATION) {
+ $this->appointments->complete($appointment, $ownerRef, $actorRef);
+
+ return;
+ }
+
+ if ($appointment->organization && in_array($appointment->queue_ticket_status, ['called', 'serving'], true)) {
+ $this->queueBridge->complete($appointment->organization, $appointment);
+ }
+ }
+
+ /**
+ * Open the patient who was just called — without starting the session.
+ * Specialty → workspace overview; GP → appointment (Start + Call again).
+ */
+ public function redirectToOpenedPatient(
+ Appointment|Model|null $entity,
+ array $ticket,
+ string $fallbackRoute = 'care.queue.index',
+ array $fallbackParams = [],
+ ): RedirectResponse {
+ $message = 'Called '.($ticket['ticket_number'] ?? 'next').' — '.($ticket['customer_name'] ?? 'patient').'.';
+
+ if (! $entity instanceof Appointment) {
+ return redirect()->route($fallbackRoute, $fallbackParams)->with('success', $message);
+ }
+
+ $entity->loadMissing(['visit', 'organization', 'department']);
+ $organization = $entity->organization;
+ if (! $organization) {
+ return redirect()->route($fallbackRoute, $fallbackParams)->with('success', $message);
+ }
+
+ $context = $this->queueBridge->contextForAppointment($organization, $entity);
+
+ if ($context !== CareQueueContexts::CONSULTATION && $entity->visit) {
+ return redirect()
+ ->route('care.specialty.workspace', [
+ 'module' => $context,
+ 'visit' => $entity->visit,
+ 'tab' => 'overview',
+ ])
+ ->with('success', $message);
+ }
+
+ return redirect()
+ ->route('care.appointments.show', $entity)
+ ->with('success', $message);
+ }
+}
diff --git a/app/Services/Care/SpecialtyShellService.php b/app/Services/Care/SpecialtyShellService.php
index 3c28965..fe04acc 100644
--- a/app/Services/Care/SpecialtyShellService.php
+++ b/app/Services/Care/SpecialtyShellService.php
@@ -119,8 +119,27 @@ class SpecialtyShellService
public function workspaceTabs(string $moduleKey): array
{
$tabs = $this->definition($moduleKey)['workspace_tabs'] ?? [];
+ if (! is_array($tabs)) {
+ return [];
+ }
- return is_array($tabs) ? $tabs : [];
+ if (array_key_exists('timeline', $tabs)) {
+ return $tabs;
+ }
+
+ $merged = [];
+ foreach ($tabs as $key => $label) {
+ $merged[$key] = $label;
+ if ($key === 'overview') {
+ $merged['timeline'] = 'Timeline';
+ }
+ }
+
+ if (! array_key_exists('timeline', $merged)) {
+ $merged['timeline'] = 'Timeline';
+ }
+
+ return $merged;
}
/**
diff --git a/config/care_specialty_shell.php b/config/care_specialty_shell.php
index 081d5e7..a13f6ed 100644
--- a/config/care_specialty_shell.php
+++ b/config/care_specialty_shell.php
@@ -28,6 +28,7 @@ return [
],
'workspace_tabs' => [
'overview' => 'Overview',
+ 'timeline' => 'Timeline',
'clinical_notes' => 'Clinical notes',
'orders' => 'Orders',
'billing' => 'Billing',
diff --git a/resources/views/care/appointments/show.blade.php b/resources/views/care/appointments/show.blade.php
index 796a3b3..f5f2fe9 100644
--- a/resources/views/care/appointments/show.blade.php
+++ b/resources/views/care/appointments/show.blade.php
@@ -19,6 +19,12 @@