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
@@ -11,6 +11,7 @@ use App\Models\Patient;
use App\Models\Practitioner;
use App\Services\Care\AppointmentService;
use App\Services\Care\OrganizationResolver;
use App\Services\Care\ServiceQueuePresenter;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
@@ -21,13 +22,15 @@ class AppointmentController extends Controller
public function __construct(
protected AppointmentService $appointments,
protected ServiceQueuePresenter $serviceQueues,
) {}
public function index(Request $request): View
{
$this->authorizeAbility($request, 'appointments.view');
$organization = $this->organization($request);
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
$member = $this->member($request);
$branchScope = app(OrganizationResolver::class)->branchScope($member);
$appointments = $this->appointments->list(
$this->ownerRef($request),
@@ -58,6 +61,13 @@ class AppointmentController extends Controller
'practitioners' => $practitioners,
'statuses' => config('care.appointment_statuses'),
'heroStats' => $heroStats,
'serviceQueuePanel' => $this->serviceQueues->panel(
$request,
$organization,
$member,
'clinical',
$request->input('counter_uuid'),
),
]);
}
@@ -12,6 +12,8 @@ use App\Services\Care\AppointmentService;
use App\Services\Care\CarePermissions;
use App\Services\Care\OrganizationResolver;
use App\Services\Care\ReportService;
use App\Services\Care\ServiceQueuePresenter;
use App\Services\Care\SpecialtyModuleService;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\View\View;
@@ -24,6 +26,8 @@ class DashboardController extends Controller
protected ReportService $reports,
protected CarePermissions $permissions,
protected AppointmentService $appointments,
protected ServiceQueuePresenter $serviceQueues,
protected SpecialtyModuleService $specialtyModules,
) {}
public function index(Request $request): View
@@ -76,6 +80,12 @@ class DashboardController extends Controller
? $this->patientQueueForMember($request, $organization->id, $owner, $member, $branchScope)
: [collect(), collect(), null];
$serviceQueueContext = match ($member?->role) {
'pharmacist' => 'pharmacy',
'lab_technician' => 'laboratory',
default => 'clinical',
};
return view('care.dashboard', [
'organization' => $organization,
'member' => $member,
@@ -93,6 +103,14 @@ class DashboardController extends Controller
'patientQueue' => $queue,
'inConsultation' => $inConsultation,
'practitionerId' => $practitionerId,
'serviceQueuePanel' => $this->serviceQueues->panel(
$request,
$organization,
$member,
$serviceQueueContext,
$request->input('counter_uuid'),
),
'specialtyModules' => $this->specialtyModules->enabledModules($organization),
]);
}
@@ -11,6 +11,7 @@ use App\Models\InvestigationType;
use App\Models\Member;
use App\Services\Care\InvestigationService;
use App\Services\Care\OrganizationResolver;
use App\Services\Care\ServiceQueuePresenter;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
@@ -21,6 +22,7 @@ class InvestigationController extends Controller
public function __construct(
protected InvestigationService $investigations,
protected ServiceQueuePresenter $serviceQueues,
) {}
public function index(Request $request): View
@@ -74,6 +76,13 @@ class InvestigationController extends Controller
->where('organization_id', $organization->id)
->orderBy('role')
->get(),
'serviceQueuePanel' => $this->serviceQueues->panel(
$request,
$organization,
$this->member($request),
'laboratory',
$request->input('counter_uuid'),
),
]);
}
@@ -10,6 +10,7 @@ use App\Models\Practitioner;
use App\Services\Care\OrganizationResolver;
use App\Services\Care\PharmacyService;
use App\Services\Care\PrescriptionService;
use App\Services\Care\ServiceQueuePresenter;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
@@ -21,6 +22,7 @@ class PrescriptionController extends Controller
public function __construct(
protected PrescriptionService $prescriptions,
protected PharmacyService $pharmacy,
protected ServiceQueuePresenter $serviceQueues,
) {}
public function index(Request $request): View
@@ -65,6 +67,13 @@ class PrescriptionController extends Controller
'queue' => $queue,
'drugs' => $drugs,
'canDispense' => $canDispense,
'serviceQueuePanel' => $this->serviceQueues->panel(
$request,
$organization,
$this->member($request),
'pharmacy',
$request->input('counter_uuid'),
),
]);
}
+11 -1
View File
@@ -10,6 +10,7 @@ use App\Models\Practitioner;
use App\Services\Care\AppointmentService;
use App\Services\Care\ConsultationService;
use App\Services\Care\OrganizationResolver;
use App\Services\Care\ServiceQueuePresenter;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
@@ -21,13 +22,15 @@ class QueueController extends Controller
public function __construct(
protected AppointmentService $appointments,
protected ConsultationService $consultations,
protected ServiceQueuePresenter $serviceQueues,
) {}
public function index(Request $request): View
{
$this->authorizeAbility($request, 'appointments.view');
$organization = $this->organization($request);
$branchScope = app(OrganizationResolver::class)->branchScope($this->member($request));
$member = $this->member($request);
$branchScope = app(OrganizationResolver::class)->branchScope($member);
$branches = Branch::owned($this->ownerRef($request))
->where('organization_id', $organization->id)
@@ -82,6 +85,13 @@ class QueueController extends Controller
'canManageQueue' => $canManageQueue,
'canConsult' => $canConsult,
'heroStats' => $heroStats,
'serviceQueuePanel' => $this->serviceQueues->panel(
$request,
$organization,
$member,
'clinical',
$request->input('counter_uuid'),
),
]);
}
@@ -4,7 +4,7 @@ namespace App\Http\Controllers\Care;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
use App\Models\Branch;
use App\Services\Care\ServiceQueuePresenter;
use App\Services\Queue\QueueClient;
use Illuminate\Http\Client\RequestException;
use Illuminate\Http\RedirectResponse;
@@ -15,40 +15,19 @@ class ServiceQueueController extends Controller
{
use ScopesToAccount;
public function index(Request $request, QueueClient $queue): View|RedirectResponse
public function index(Request $request): RedirectResponse
{
$this->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) {
if ($queue->configured() && $e->response?->status() === 403) {
try {
$queue->syncIntegration($owner, true);
$counters = $queue->counters($owner);
} catch (RequestException) {
return redirect()->route('care.settings')->with('error', 'Ladill Queue integration is not enabled. Save settings with Queue integration checked, and enable Care in Queue settings.');
}
} else {
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'));
// Natural Queue: counters live on role pages (clinical queue, pharmacy, lab, specialties).
return redirect()->route('care.queue.index');
}
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);
$owner = (string) $organization->owner_ref;
if (! $this->integrationEnabled($organization)) {
return redirect()->route('care.settings');
@@ -57,7 +36,7 @@ class ServiceQueueController extends Controller
try {
$state = $queue->console($owner, $counterUuid);
} catch (RequestException) {
return redirect()->route('care.service-queues.index')->with('error', 'Counter console unavailable.');
return redirect()->route('care.queue.index')->with('error', 'Counter console unavailable.');
}
return view('care.service-queues.console', [
@@ -70,7 +49,8 @@ class ServiceQueueController extends Controller
public function action(Request $request, string $counterUuid, QueueClient $queue): RedirectResponse
{
$this->authorizeAbility($request, 'service_queues.console');
$owner = $this->ownerRef($request);
$organization = $this->organization($request);
$owner = (string) $organization->owner_ref;
$validated = $request->validate([
'action' => ['required', 'string'],
@@ -58,6 +58,7 @@ class SettingsController extends Controller
'hasBranchesFeature' => $plans->hasFeature($organization, 'branches'),
'hasClinicalDevicesFeature' => $plans->hasFeature($organization, 'clinical_devices'),
'canUseQueueIntegration' => $plans->canUseQueueIntegration($organization),
'canUseSpecialtyModules' => $plans->canUseSpecialtyModules($organization),
'assessmentsEngineEnabled' => $careFeatures->enabled($organization, CareFeatures::ASSESSMENTS_ENGINE),
'messagingSmsReady' => $suiteSmsReady || $credential->hasValidSms(),
'messagingEmailReady' => $suiteEmailReady || $credential->hasValidBird(),
@@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers\Care;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
use App\Services\Care\PlanService;
use App\Services\Care\SpecialtyModuleService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class SettingsModulesController extends Controller
{
use ScopesToAccount;
public function edit(Request $request, SpecialtyModuleService $modules, PlanService $plans): View
{
$this->authorizeAbility($request, 'settings.view');
$organization = $this->organization($request);
$member = $this->member($request);
$canManage = app(\App\Services\Care\CarePermissions::class)->can($member, 'settings.manage');
return view('care.settings.modules', [
'organization' => $organization,
'canManage' => $canManage,
'canUseSpecialtyModules' => $modules->canManage($organization),
'catalog' => $modules->catalog(),
'enabled' => collect($modules->enabledKeys($organization))->flip()->all(),
'planKey' => $plans->planKey($organization),
]);
}
public function update(Request $request, SpecialtyModuleService $modules): RedirectResponse
{
$this->authorizeAbility($request, 'settings.manage');
$organization = $this->organization($request);
if (! $modules->canManage($organization)) {
return redirect()
->route('care.pro.index')
->with('upsell', 'Specialty modules are part of Care Pro. Upgrade to activate dentistry, eye care, and other specialty practice surfaces.');
}
$keys = array_keys($modules->catalog());
$desired = [];
foreach ($keys as $key) {
$desired[$key] = $request->boolean("modules.{$key}");
}
$result = $modules->sync($organization, $this->ownerRef($request), $desired);
if ($result['errors'] !== []) {
return back()->with('error', 'Some modules could not be updated: '.implode(' ', $result['errors']));
}
$parts = [];
if ($result['activated'] !== []) {
$parts[] = 'Activated '.count($result['activated']).' module(s)';
}
if ($result['deactivated'] !== []) {
$parts[] = 'Deactivated '.count($result['deactivated']).' module(s)';
}
return back()->with('success', $parts !== [] ? implode('. ', $parts).'.' : 'Modules saved.');
}
}
@@ -0,0 +1,80 @@
<?php
namespace App\Http\Controllers\Care;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
use App\Models\Appointment;
use App\Models\Department;
use App\Services\Care\OrganizationResolver;
use App\Services\Care\ServiceQueuePresenter;
use App\Services\Care\SpecialtyModuleService;
use Illuminate\Http\Request;
use Illuminate\View\View;
class SpecialtyModuleController extends Controller
{
use ScopesToAccount;
public function show(
Request $request,
string $module,
SpecialtyModuleService $modules,
ServiceQueuePresenter $presenter,
): View {
$definition = $modules->definition($module);
abort_unless($definition, 404);
$organization = $this->organization($request);
abort_unless($modules->isEnabled($organization, $module), 404);
$roles = $definition['roles'] ?? [];
$member = $this->member($request);
if (is_array($roles) && $roles !== [] && $member && ! in_array($member->role, $roles, true)) {
abort(403);
}
$owner = $this->ownerRef($request);
$branchScope = app(OrganizationResolver::class)->branchScope($member);
$departments = Department::owned($owner)
->where('type', $definition['department_type'] ?? 'general')
->where('is_active', true)
->when($branchScope, fn ($q) => $q->where('branch_id', $branchScope))
->with('branch')
->orderBy('name')
->get();
$departmentIds = $departments->pluck('id')->all();
$waiting = Appointment::owned($owner)
->where('organization_id', $organization->id)
->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN])
->when($departmentIds !== [], fn ($q) => $q->whereIn('department_id', $departmentIds))
->when($departmentIds === [], fn ($q) => $q->whereRaw('1 = 0'))
->when($branchScope, fn ($q) => $q->where('branch_id', $branchScope))
->with(['patient', 'practitioner', 'department'])
->orderBy('queue_position')
->orderBy('checked_in_at')
->limit(25)
->get();
$serviceQueuePanel = $presenter->panel(
$request,
$organization,
$member,
$module,
$request->input('counter_uuid'),
);
return view('care.specialty.show', [
'organization' => $organization,
'moduleKey' => $module,
'definition' => $definition,
'departments' => $departments,
'waiting' => $waiting,
'queueStubs' => $modules->queueStubsFor($organization, $module),
'serviceQueuePanel' => $serviceQueuePanel,
]);
}
}