diff --git a/.env.example b/.env.example index 0abfc39..ab5a9be 100644 --- a/.env.example +++ b/.env.example @@ -19,6 +19,10 @@ SESSION_LIFETIME=1440 CACHE_STORE=database QUEUE_CONNECTION=database +# Ladill Queue service API (in-app service queue console) +QUEUE_API_URL=https://queue.ladill.com/api/v1 +QUEUE_API_KEY_CARE= + # --- Ladill SSO (Sign in with Ladill — auth.ladill.com OIDC client) --- LADILL_SSO_CLIENT_ID= LADILL_SSO_CLIENT_SECRET= diff --git a/app/Http/Controllers/Care/ServiceQueueController.php b/app/Http/Controllers/Care/ServiceQueueController.php new file mode 100644 index 0000000..5ec573e --- /dev/null +++ b/app/Http/Controllers/Care/ServiceQueueController.php @@ -0,0 +1,87 @@ +authorizeAbility($request, 'service_queues.console'); + $organization = $this->organization($request); + + if (! $this->integrationEnabled($organization)) { + return redirect()->route('care.settings')->with('error', 'Enable Ladill Queue integration in settings first.'); + } + + $owner = $this->ownerRef($request); + + try { + $counters = $queue->counters($owner); + } catch (RequestException $e) { + return redirect()->route('care.settings')->with('error', 'Could not reach Ladill Queue. Check API keys and enable integration in Queue settings.'); + } + + return view('care.service-queues.index', compact('counters', 'organization')); + } + + public function console(Request $request, string $counterUuid, QueueClient $queue): View|RedirectResponse + { + $this->authorizeAbility($request, 'service_queues.console'); + $organization = $this->organization($request); + $owner = $this->ownerRef($request); + + if (! $this->integrationEnabled($organization)) { + return redirect()->route('care.settings'); + } + + try { + $state = $queue->console($owner, $counterUuid); + } catch (RequestException) { + return redirect()->route('care.service-queues.index')->with('error', 'Counter console unavailable.'); + } + + return view('care.service-queues.console', [ + 'state' => $state, + 'counterUuid' => $counterUuid, + 'organization' => $organization, + ]); + } + + public function action(Request $request, string $counterUuid, QueueClient $queue): RedirectResponse + { + $this->authorizeAbility($request, 'service_queues.console'); + $owner = $this->ownerRef($request); + + $validated = $request->validate([ + 'action' => ['required', 'string'], + 'queue_uuid' => ['nullable', 'uuid'], + 'ticket_uuid' => ['nullable', 'uuid'], + 'to_queue_uuid' => ['nullable', 'uuid'], + 'reason' => ['nullable', 'string', 'max:500'], + ]); + + try { + $queue->consoleAction($owner, $counterUuid, $validated); + } catch (RequestException) { + return back()->with('error', 'Queue action failed. Try again.'); + } + + return back()->with('success', 'Queue updated.'); + } + + protected function integrationEnabled(\App\Models\Organization $organization): bool + { + return (bool) data_get($organization->settings, 'queue_integration_enabled', false); + } +} diff --git a/app/Http/Controllers/Care/SettingsController.php b/app/Http/Controllers/Care/SettingsController.php index 7c232c7..48635d9 100644 --- a/app/Http/Controllers/Care/SettingsController.php +++ b/app/Http/Controllers/Care/SettingsController.php @@ -7,6 +7,7 @@ use App\Http\Controllers\Care\Concerns\ScopesToAccount; use App\Models\Branch; use App\Services\Care\AuditLogger; use App\Services\Care\CarePermissions; +use App\Services\Queue\QueueClient; use App\Support\OrganizationBranding; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; @@ -39,7 +40,7 @@ class SettingsController extends Controller ]); } - public function update(Request $request): RedirectResponse + public function update(Request $request, QueueClient $queue): RedirectResponse { $this->authorizeAbility($request, 'settings.manage'); $organization = $this->organization($request); @@ -51,12 +52,26 @@ class SettingsController extends Controller 'facility_type' => ['required', 'string', 'in:clinic,hospital,diagnostic,specialist'], 'logo' => ['nullable', 'image', 'mimes:jpeg,png,jpg,webp,svg', 'max:2048'], 'remove_logo' => ['nullable', 'boolean'], + 'queue_integration_enabled' => ['nullable', 'boolean'], ]); $settings = $organization->settings ?? []; $settings['onboarded'] = true; $settings['facility_type'] = $validated['facility_type']; + $wantsQueue = $request->boolean('queue_integration_enabled'); + $hadQueue = (bool) data_get($settings, 'queue_integration_enabled', false); + $settings['queue_integration_enabled'] = $wantsQueue; + + if ($wantsQueue && ! $hadQueue && $queue->configured()) { + $branch = Branch::owned($owner)->where('organization_id', $organization->id)->orderBy('name')->first(); + try { + $queue->enable($owner, $organization->name, $branch?->name); + } catch (\Throwable) { + return back()->withInput()->with('error', 'Could not provision Ladill Queue for this account. Ensure Queue API keys are configured and Queue allows Care integration.'); + } + } + $logoPath = $organization->logo_path; if ($request->boolean('remove_logo')) { diff --git a/app/Services/Care/CarePermissions.php b/app/Services/Care/CarePermissions.php index 98158da..28cb21f 100644 --- a/app/Services/Care/CarePermissions.php +++ b/app/Services/Care/CarePermissions.php @@ -13,6 +13,7 @@ class CarePermissions 'receptionist' => [ 'dashboard.view', 'patients.view', 'patients.manage', 'appointments.view', 'appointments.manage', 'queue.manage', + 'service_queues.console', ], 'doctor' => [ 'dashboard.view', 'patients.view', 'appointments.view', @@ -22,6 +23,7 @@ class CarePermissions 'nurse' => [ 'dashboard.view', 'patients.view', 'appointments.view', 'consultations.view', 'vitals.manage', 'queue.manage', + 'service_queues.console', ], 'lab_technician' => [ 'dashboard.view', 'patients.view', 'lab.view', 'lab.manage', diff --git a/app/Services/Queue/QueueClient.php b/app/Services/Queue/QueueClient.php new file mode 100644 index 0000000..84a0719 --- /dev/null +++ b/app/Services/Queue/QueueClient.php @@ -0,0 +1,113 @@ +token()) + ->acceptJson() + ->timeout(12) + ->withQueryParameters(['owner' => $owner]); + } + + public function configured(): bool + { + return $this->token() !== '' && $this->base() !== ''; + } + + /** + * @return array + */ + public function status(string $owner): array + { + $response = $this->http($owner)->get($this->base().'/integrations/status'); + $response->throw(); + + return (array) ($response->json('data') ?? []); + } + + /** + * @return array + */ + public function enable(string $owner, string $organizationName, ?string $branchName = null): array + { + $response = $this->http($owner)->post($this->base().'/integrations/provision', array_filter([ + 'organization_name' => $organizationName, + 'branch_name' => $branchName, + 'industry' => 'healthcare', + ])); + $response->throw(); + + return (array) ($response->json('data') ?? []); + } + + public function disable(string $owner): void + { + // Local Care setting only; Queue must be disabled separately in Queue settings. + } + + public function isLinked(string $owner): bool + { + if (! $this->configured()) { + return false; + } + + try { + $status = $this->status($owner); + + return (bool) ($status['provisioned'] ?? false) + && (bool) data_get($status, 'integrations.care', false); + } catch (RequestException) { + return false; + } + } + + /** + * @return list> + */ + public function counters(string $owner): array + { + $response = $this->http($owner)->get($this->base().'/counters'); + $response->throw(); + + return (array) ($response->json('data') ?? []); + } + + /** + * @return array + */ + public function console(string $owner, string $counterUuid): array + { + $response = $this->http($owner)->get($this->base().'/counters/'.$counterUuid.'/console'); + $response->throw(); + + return (array) ($response->json('data') ?? []); + } + + /** + * @param array $payload + * @return array + */ + public function consoleAction(string $owner, string $counterUuid, array $payload): array + { + $response = $this->http($owner)->post($this->base().'/counters/'.$counterUuid.'/console', $payload); + $response->throw(); + + return (array) ($response->json('data') ?? []); + } +} diff --git a/config/care.php b/config/care.php index 99bb7bc..e76685f 100644 --- a/config/care.php +++ b/config/care.php @@ -217,4 +217,9 @@ return [ ], ], + 'queue' => [ + 'api_url' => env('QUEUE_API_URL', 'https://queue.ladill.com/api/v1'), + 'api_key' => env('QUEUE_API_KEY_CARE'), + ], + ]; diff --git a/resources/views/care/service-queues/console.blade.php b/resources/views/care/service-queues/console.blade.php new file mode 100644 index 0000000..b86293c --- /dev/null +++ b/resources/views/care/service-queues/console.blade.php @@ -0,0 +1,97 @@ +@php + $counter = $state['counter'] ?? []; + $current = $state['current_ticket'] ?? null; + $queues = $state['queues'] ?? []; + $waiting = $state['waiting_by_queue'] ?? []; + $transferQueues = $state['transfer_queues'] ?? []; +@endphp + + +
+
+ ← Service queues +

{{ $counter['name'] ?? 'Counter' }}

+

{{ $counter['branch'] ?? '' }} · Ladill Queue console

+
+
+
@csrf
+
@csrf
+
+
+ + @if ($current) +
+
+

Current customer

+
+
+

{{ $current['ticket_number'] }}

+

{{ $current['queue']['name'] ?? '' }}

+

{{ $current['customer_name'] ?? 'Walk-in customer' }}

+
+ @foreach ([ + 'recall' => 'Recall', + 'start' => 'Start serving', + 'hold' => 'Hold', + 'skip' => 'Skip', + 'complete' => 'Complete', + 'no_show' => 'No show', + ] as $action => $label) +
+ @csrf + + + +
+ @endforeach +
+ @if (count($transferQueues) > 1) +
+ @csrf + + + + + +
+ @endif +
+
+ @else +
+ No active customer — call the next ticket from a queue below. +
+ @endif + +
+ @foreach ($queues as $queue) +
+
+

{{ $queue['name'] }}

+
+ @csrf + + + +
+
+
    + @forelse ($waiting[$queue['uuid']] ?? [] as $ticket) +
  • + {{ $ticket['ticket_number'] }} + {{ $ticket['customer_name'] ?? 'Walk-in' }} +
  • + @empty +
  • Queue is empty
  • + @endforelse +
+
+ @endforeach +
+
diff --git a/resources/views/care/service-queues/index.blade.php b/resources/views/care/service-queues/index.blade.php new file mode 100644 index 0000000..25b8aa4 --- /dev/null +++ b/resources/views/care/service-queues/index.blade.php @@ -0,0 +1,34 @@ + +
+
+

Service queues

+

Powered by Ladill Queue — call tickets and manage counters without leaving Care

+
+ Open Queue admin +
+ + @if (session('error')) +
{{ session('error') }}
+ @endif + +
+ @forelse ($counters as $counter) +
+
+
+

{{ $counter['name'] }}

+

{{ $counter['status'] ?? 'offline' }}

+
+
+

+ Queues: {{ collect($counter['queues'] ?? [])->join(', ') ?: 'None assigned' }} +

+ Open console +
+ @empty +
+ No counters found in Ladill Queue. Create counters in Queue admin, then return here. +
+ @endforelse +
+
diff --git a/resources/views/care/settings/edit.blade.php b/resources/views/care/settings/edit.blade.php index 4ef0ab8..1eced5f 100644 --- a/resources/views/care/settings/edit.blade.php +++ b/resources/views/care/settings/edit.blade.php @@ -36,6 +36,16 @@ @endif + +
+

Ladill Queue

+

Manage service queues and counters in Care. You must also enable Care integration in Ladill Queue settings.

+ +
+ @endif diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index 8b2430c..700c93b 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -9,6 +9,9 @@ ? app(\App\Services\Care\OrganizationResolver::class)->memberFor(auth()->user()) : null; $permissions = app(\App\Services\Care\CarePermissions::class); + $organization = auth()->user() + ? app(\App\Services\Care\OrganizationResolver::class)->resolveForUser(auth()->user()) + : null; $nav = [ ['name' => 'Dashboard', 'route' => route('care.dashboard'), 'active' => request()->routeIs('care.dashboard'), @@ -52,6 +55,11 @@ 'icon' => '']; } + if ($organization && data_get($organization->settings, 'queue_integration_enabled') && $permissions->can($member, 'service_queues.console')) { + $nav[] = ['name' => 'Service queues', 'route' => route('care.service-queues.index'), 'active' => request()->routeIs('care.service-queues.*'), + 'icon' => '']; + } + $adminNav = []; if ($permissions->can($member, 'admin.branches.view')) { $adminNav[] = ['name' => 'Branches', 'route' => route('care.branches.index'), 'active' => request()->routeIs('care.branches.*'), diff --git a/routes/web.php b/routes/web.php index 552b040..955cea8 100644 --- a/routes/web.php +++ b/routes/web.php @@ -19,6 +19,7 @@ use App\Http\Controllers\Care\PatientController; use App\Http\Controllers\Care\PrescriptionController; use App\Http\Controllers\Care\QueueController; use App\Http\Controllers\Care\ReportController; +use App\Http\Controllers\Care\ServiceQueueController; use App\Http\Controllers\Care\SettingsController; use Illuminate\Support\Facades\Route; @@ -71,6 +72,10 @@ Route::middleware(['auth', 'platform.session'])->group(function () { Route::get('/queue', [QueueController::class, 'index'])->name('care.queue.index'); Route::post('/queue/{appointment}/start', [QueueController::class, 'start'])->name('care.queue.start'); + Route::get('/service-queues', [ServiceQueueController::class, 'index'])->name('care.service-queues.index'); + Route::get('/service-queues/console/{counterUuid}', [ServiceQueueController::class, 'console'])->name('care.service-queues.console'); + Route::post('/service-queues/console/{counterUuid}/action', [ServiceQueueController::class, 'action'])->name('care.service-queues.action'); + Route::get('/consultations/{consultation}', [ConsultationController::class, 'show'])->name('care.consultations.show'); Route::put('/consultations/{consultation}', [ConsultationController::class, 'update'])->name('care.consultations.update'); Route::post('/consultations/{consultation}/complete', [ConsultationController::class, 'complete'])->name('care.consultations.complete');