Embed Ladill Queue on role pages and add specialty modules under Settings.
Deploy Ladill Care / deploy (push) Successful in 53s

Remove the standalone Service Queues nav so call-next/now-serving lives on clinical queue, appointments, pharmacy, lab, and specialty surfaces; Pro orgs can activate dentistry and related modules with branch departments and queue stubs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-17 15:55:47 +00:00
co-authored by Cursor
parent e0a7a64d38
commit dec282d25d
30 changed files with 1552 additions and 35 deletions
@@ -85,5 +85,7 @@
</div>
<div>{{ $appointments->links() }}</div>
@include('care.service-queues._panel')
</div>
</x-app-layout>
+21
View File
@@ -192,6 +192,27 @@
</section>
@endif
@if (! empty($serviceQueuePanel['enabled']))
<div class="mt-6">
@include('care.service-queues._panel')
</div>
@endif
@if (! empty($specialtyModules))
<section class="mt-6">
<h2 class="text-sm font-semibold text-slate-900">Specialty modules</h2>
<div class="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
@foreach ($specialtyModules as $item)
<a href="{{ route('care.specialty.show', $item['key']) }}"
class="rounded-2xl border border-slate-200 bg-white px-4 py-4 transition hover:border-indigo-300 hover:shadow-sm">
<p class="text-sm font-semibold text-slate-900">{{ $item['definition']['nav_label'] ?? $item['definition']['label'] }}</p>
<p class="mt-1 text-xs text-slate-500">{{ $item['definition']['description'] ?? '' }}</p>
</a>
@endforeach
</div>
</section>
@endif
@if (count($organizationCards) > 0)
@php
$orgCols = match (count($organizationCards)) {
@@ -42,4 +42,8 @@
<p class="rounded-2xl border border-dashed border-slate-200 bg-white p-8 text-center text-sm text-slate-500">Queue is empty.</p>
@endforelse
</div>
<div class="mt-6">
@include('care.service-queues._panel')
</div>
</x-app-layout>
@@ -56,4 +56,8 @@
<p class="rounded-2xl border border-dashed border-slate-200 bg-white p-8 text-center text-sm text-slate-500">No prescriptions in queue.</p>
@endforelse
</div>
<div class="mt-6">
@include('care.service-queues._panel')
</div>
</x-app-layout>
@@ -78,5 +78,7 @@
</div>
</section>
</div>
@include('care.service-queues._panel')
</div>
</x-app-layout>
@@ -0,0 +1,145 @@
{{-- Compact Ladill Queue panel for role-native pages (not a standalone Service Queues nav). --}}
@php
$panel = $serviceQueuePanel ?? null;
@endphp
@if (! empty($panel['enabled']))
@php
$counterUuid = $panel['counter_uuid'] ?? null;
$state = $panel['state'] ?? null;
$counters = $panel['counters'] ?? [];
$current = $state['current_ticket'] ?? null;
$queues = $state['queues'] ?? [];
$waiting = $state['waiting_by_queue'] ?? [];
$counter = $state['counter'] ?? [];
$stubs = $panel['stubs'] ?? [];
$statusLabel = match ($current['status'] ?? null) {
'called' => 'Called',
'serving' => 'Serving',
'on_hold' => 'On hold',
default => $current['status'] ?? null,
};
@endphp
<section class="rounded-2xl border border-indigo-100 bg-gradient-to-br from-indigo-50/80 via-white to-white shadow-sm">
<div class="flex flex-wrap items-start justify-between gap-3 border-b border-indigo-100 px-5 py-4">
<div>
<p class="text-xs font-semibold uppercase tracking-wider text-indigo-600">Service counter</p>
<h2 class="mt-0.5 text-sm font-semibold text-slate-900">
{{ $counter['name'] ?? 'Ladill Queue' }}
@if (! empty($counter['branch']))
<span class="font-normal text-slate-500">· {{ $counter['branch'] }}</span>
@endif
</h2>
</div>
<div class="flex flex-wrap items-center gap-2">
@if (count($counters) > 1 && $counterUuid)
<form method="GET" class="flex items-center gap-2">
@foreach (request()->except('counter_uuid') as $key => $value)
@if (is_scalar($value))
<input type="hidden" name="{{ $key }}" value="{{ $value }}">
@endif
@endforeach
<select name="counter_uuid" class="rounded-lg border-slate-300 text-xs" onchange="this.form.submit()">
@foreach ($counters as $option)
<option value="{{ $option['uuid'] }}" @selected(($option['uuid'] ?? '') === $counterUuid)>{{ $option['name'] ?? 'Counter' }}</option>
@endforeach
</select>
</form>
@endif
@if ($counterUuid)
<a href="{{ route('care.service-queues.console', $counterUuid) }}" class="text-xs font-medium text-indigo-600 hover:text-indigo-800">Full console</a>
@endif
</div>
</div>
<div class="space-y-4 px-5 py-4">
@if (! empty($panel['error']))
<p class="rounded-lg bg-amber-50 px-3 py-2 text-sm text-amber-900">{{ $panel['error'] }}</p>
@endif
@if ($current && $counterUuid)
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<p class="text-xs font-medium uppercase tracking-wide text-slate-500">Now serving</p>
<p class="mt-1 text-3xl font-bold tracking-tight text-slate-900">{{ $current['ticket_number'] }}</p>
<p class="mt-1 text-sm text-slate-600">{{ $current['customer_name'] ?? 'Walk-in' }} · {{ $current['queue']['name'] ?? '' }}</p>
</div>
@if ($statusLabel)
<span class="rounded-full bg-indigo-100 px-2.5 py-1 text-xs font-semibold text-indigo-700">{{ $statusLabel }}</span>
@endif
</div>
<div class="flex flex-wrap gap-2">
@foreach ([
'start' => 'Start',
'complete' => 'Complete',
'recall' => 'Recall',
'skip' => 'Skip',
] as $action => $label)
<form method="POST" action="{{ route('care.service-queues.action', $counterUuid) }}">
@csrf
<input type="hidden" name="action" value="{{ $action }}">
<input type="hidden" name="ticket_uuid" value="{{ $current['uuid'] }}">
<button type="submit" class="{{ $action === 'complete' || $action === 'start' ? 'btn-primary' : 'btn-secondary' }} text-xs">{{ $label }}</button>
</form>
@endforeach
</div>
@elseif ($counterUuid)
<p class="text-sm text-slate-600">No active ticket. Call the next patient from a queue below.</p>
@elseif (count($stubs))
<div class="rounded-xl border border-dashed border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-600">
<p class="font-medium text-slate-800">Specialty queues ready</p>
<ul class="mt-2 space-y-1 text-xs">
@foreach ($stubs as $stub)
<li>
{{ $stub['name'] ?? 'Queue' }}
@if (! empty($stub['branch_name'])) · {{ $stub['branch_name'] }} @endif
@if (! empty($stub['prefix']))
<span class="font-mono text-slate-500">({{ $stub['prefix'] }})</span>
@endif
@if (! empty($stub['synced']))
<span class="text-emerald-600">· linked</span>
@else
<span class="text-slate-400">· create in Queue admin if missing</span>
@endif
</li>
@endforeach
</ul>
</div>
@elseif (! $panel['error'])
<p class="text-sm text-slate-500">No counters found for this branch. Create counters in Queue admin, then refresh.</p>
@endif
@if ($counterUuid && count($queues))
<div class="grid gap-3 sm:grid-cols-2">
@foreach ($queues as $queue)
@php $waitingTickets = $waiting[$queue['uuid']] ?? []; @endphp
<div class="rounded-xl border border-slate-200 bg-white px-4 py-3">
<div class="flex items-center justify-between gap-2">
<div>
<p class="text-sm font-semibold text-slate-900">{{ $queue['name'] }}</p>
<p class="text-xs text-slate-500">{{ count($waitingTickets) }} waiting</p>
</div>
<form method="POST" action="{{ route('care.service-queues.action', $counterUuid) }}">
@csrf
<input type="hidden" name="action" value="call_next">
<input type="hidden" name="queue_uuid" value="{{ $queue['uuid'] }}">
<button type="submit" class="btn-primary text-xs">Call next</button>
</form>
</div>
@if (count($waitingTickets))
<ul class="mt-2 space-y-1 border-t border-slate-50 pt-2">
@foreach (array_slice($waitingTickets, 0, 3) as $ticket)
<li class="flex justify-between text-xs text-slate-600">
<span class="font-mono font-semibold text-slate-800">{{ $ticket['ticket_number'] }}</span>
<span class="truncate pl-2">{{ $ticket['customer_name'] ?? 'Walk-in' }}</span>
</li>
@endforeach
</ul>
@endif
</div>
@endforeach
</div>
@endif
</div>
</section>
@endif
@@ -18,7 +18,7 @@
<x-app-layout title="Console · {{ $counter['name'] ?? 'Counter' }}">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<a href="{{ route('care.service-queues.index') }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800"> Service queues</a>
<a href="{{ route('care.queue.index') }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800"> Patient queue</a>
<h1 class="mt-2 text-xl font-semibold text-slate-900">{{ $counter['name'] ?? 'Counter' }}</h1>
<p class="mt-1 text-sm text-slate-500">{{ $counter['branch'] ?? '' }} · Ladill Queue console</p>
</div>
+14 -1
View File
@@ -101,6 +101,19 @@
</a>
</li>
@endif
<li>
<a href="{{ route('care.settings.modules') }}"
class="flex items-center justify-between rounded-xl border border-slate-100 bg-slate-50 px-4 py-3 text-sm text-slate-700 hover:text-indigo-700">
<span>
Modules
@if (empty($canUseSpecialtyModules))
<span class="ml-2 rounded-full bg-amber-50 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-700">Pro</span>
@endif
<span class="mt-0.5 block text-xs text-slate-500">Dentistry, eye care, physiotherapy, maternity, radiology</span>
</span>
<span class="text-slate-400">&rarr;</span>
</a>
</li>
</ul>
</x-settings.card>
@endif
@@ -194,7 +207,7 @@
</div>
</x-settings.card>
<x-settings.card title="Integrations" description="Connect Ladill Queue for service counters. Requires Care Pro or Enterprise — and Care must also be enabled in Queue settings.">
<x-settings.card title="Integrations" description="Connect Ladill Queue for service counters on clinical, pharmacy, and lab pages. Requires Care Pro or Enterprise — and Care must also be enabled in Queue settings.">
@if (! empty($canUseQueueIntegration))
<label class="flex items-center gap-2 text-sm">
<input type="checkbox" name="queue_integration_enabled" value="1" @checked(data_get($organization->settings, 'queue_integration_enabled'))>
@@ -0,0 +1,53 @@
<x-app-layout title="Modules · Settings">
<x-settings.page title="Specialty modules" description="Activate specialty practice surfaces for {{ $organization->name }}. Departments and queues are provisioned per branch; deactivating hides UI without deleting clinical history.">
<p class="text-sm">
<a href="{{ route('care.settings') }}" class="font-medium text-indigo-600 hover:text-indigo-800"> Facility settings</a>
</p>
@if (session('success'))
<p class="rounded-lg bg-emerald-50 px-3 py-2 text-sm text-emerald-800">{{ session('success') }}</p>
@endif
@if (session('error'))
<p class="rounded-lg bg-rose-50 px-3 py-2 text-sm text-rose-800">{{ session('error') }}</p>
@endif
@if (! $canUseSpecialtyModules)
<x-settings.card title="Pro feature" description="Specialty modules require Care Pro or Enterprise.">
<p class="text-sm text-slate-600">Unlock dentistry, eye care, physiotherapy, maternity, and radiology practice pages with matching departments and queue stubs.</p>
<a href="{{ route('care.pro.index') }}" class="btn-primary mt-4 inline-flex text-sm">View plans</a>
</x-settings.card>
@else
<form method="POST" action="{{ route('care.settings.modules.update') }}" class="space-y-6">
@csrf
@method('PUT')
<div class="grid gap-4 sm:grid-cols-2">
@foreach ($catalog as $key => $module)
<label class="flex cursor-pointer flex-col rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition hover:border-indigo-200 {{ ! empty($enabled[$key]) ? 'ring-1 ring-indigo-200' : '' }}">
<div class="flex items-start justify-between gap-3">
<div>
<p class="text-sm font-semibold text-slate-900">{{ $module['label'] }}</p>
<p class="mt-1 text-xs text-slate-500">{{ $module['description'] }}</p>
</div>
<input type="checkbox" name="modules[{{ $key }}]" value="1"
@checked(old("modules.{$key}", ! empty($enabled[$key])))
@disabled(! $canManage)
class="mt-0.5 rounded border-slate-300">
</div>
<p class="mt-3 text-[11px] uppercase tracking-wide text-slate-400">
Dept · {{ $module['department_name'] ?? $module['label'] }}
· Queue · {{ $module['queue_name'] ?? $module['label'] }}
</p>
</label>
@endforeach
</div>
@if ($canManage)
<div class="flex justify-end">
<button type="submit" class="btn-primary">Save modules</button>
</div>
@endif
</form>
@endif
</x-settings.page>
</x-app-layout>
@@ -0,0 +1,67 @@
<x-app-layout title="{{ $definition['label'] }}">
<div class="space-y-6">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<p class="text-xs font-semibold uppercase tracking-wider text-indigo-600">Specialty module</p>
<h1 class="mt-1 text-xl font-semibold text-slate-900">{{ $definition['label'] }}</h1>
<p class="mt-1 text-sm text-slate-500">{{ $definition['description'] ?? '' }}</p>
</div>
<a href="{{ route('care.settings.modules') }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800">Manage modules</a>
</div>
<div class="grid gap-4 lg:grid-cols-2">
<section class="rounded-2xl border border-slate-200 bg-white p-5">
<h2 class="text-sm font-semibold text-slate-900">Departments</h2>
<ul class="mt-3 space-y-2">
@forelse ($departments as $department)
<li class="flex items-center justify-between rounded-xl border border-slate-100 bg-slate-50 px-3 py-2 text-sm">
<span class="font-medium text-slate-800">{{ $department->name }}</span>
<span class="text-xs text-slate-500">{{ $department->branch?->name }}</span>
</li>
@empty
<li class="text-sm text-slate-500">No active departments for this module at your branch.</li>
@endforelse
</ul>
</section>
<section class="rounded-2xl border border-slate-200 bg-white p-5">
<h2 class="text-sm font-semibold text-slate-900">Queue stubs</h2>
<ul class="mt-3 space-y-2 text-sm">
@forelse ($queueStubs as $stub)
<li class="rounded-xl border border-slate-100 bg-slate-50 px-3 py-2">
<span class="font-medium text-slate-800">{{ $stub['name'] ?? 'Queue' }}</span>
@if (! empty($stub['prefix']))
<span class="font-mono text-xs text-slate-500">({{ $stub['prefix'] }})</span>
@endif
<span class="mt-0.5 block text-xs text-slate-500">{{ $stub['branch_name'] ?? '' }}</span>
</li>
@empty
<li class="text-sm text-slate-500">No queue stubs for your branch.</li>
@endforelse
</ul>
</section>
</div>
<section class="rounded-2xl border border-slate-200 bg-white p-5">
<div class="flex items-center justify-between gap-3">
<h2 class="text-sm font-semibold text-slate-900">Waiting patients</h2>
<a href="{{ route('care.appointments.index') }}" class="text-xs font-medium text-indigo-600">Appointments</a>
</div>
<div class="mt-3 space-y-2">
@forelse ($waiting as $appointment)
<div class="flex items-center justify-between rounded-xl border border-slate-100 bg-slate-50 px-3 py-3 text-sm">
<div>
<p class="font-medium text-slate-900">{{ $appointment->patient?->fullName() }}</p>
<p class="text-xs text-slate-500">{{ $appointment->department?->name }} · {{ $appointment->practitioner?->name ?? 'Unassigned' }}</p>
</div>
<a href="{{ route('care.appointments.show', $appointment) }}" class="text-sky-600">View</a>
</div>
@empty
<p class="rounded-xl border border-dashed border-slate-200 px-4 py-8 text-center text-sm text-slate-500">No patients waiting in this specialty.</p>
@endforelse
</div>
</section>
@include('care.service-queues._panel')
</div>
</x-app-layout>
+14 -3
View File
@@ -74,9 +74,20 @@
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 0 1 3 19.875v-6.75ZM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V8.625ZM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 0 1-1.125-1.125V4.125Z" />'];
}
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' => '<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 6v.75m0 3v.75m0 3v.75m0 3V18m-9-5.25h5.25M7.5 15h3M3.375 5.25c-.621 0-1.125.504-1.125 1.125v3.026a2.999 2.999 0 0 1 0 5.198v3.026c0 .621.504 1.125 1.125 1.125h17.25c.621 0 1.125-.504 1.125-1.125v-3.026a2.999 2.999 0 0 1 0-5.198V6.375c0-.621-.504-1.125-1.125-1.125H3.375Z" />'];
// Specialty modules (Settings → Modules) — no separate Service Queues nav;
// Ladill Queue embeds on clinical / pharmacy / lab / specialty pages.
$specialtyModules = app(\App\Services\Care\SpecialtyModuleService::class)->enabledModules($organization);
foreach ($specialtyModules as $item) {
$roles = $item['definition']['roles'] ?? [];
if (is_array($roles) && $roles !== [] && $member && ! in_array($member->role, $roles, true)) {
continue;
}
$nav[] = [
'name' => $item['definition']['nav_label'] ?? $item['definition']['label'],
'route' => route('care.specialty.show', $item['key']),
'active' => request()->routeIs('care.specialty.show') && request()->route('module') === $item['key'],
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />',
];
}
}