Replace ambulance Call next queue with an EMS dispatch board.
Deploy Ladill Care / deploy (push) Successful in 35s
Deploy Ladill Care / deploy (push) Successful in 35s
Home columns follow New call → Dispatched → On scene → Transport → Handover; walk-ins land as New call and Call next is disabled for this module. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
{{--
|
||||
Ambulance / EMS dispatch board — stage columns, not Call next desk queue.
|
||||
|
||||
Expected: $ambulanceDispatchBoard (from AmbulanceDispatchBoardService),
|
||||
$canAdvanceStage, $specialtyStageRoute, $canManageQueue (for New call CTA context)
|
||||
--}}
|
||||
@php
|
||||
$board = $ambulanceDispatchBoard ?? ['columns' => [], 'active_count' => 0, 'completed_today' => 0];
|
||||
$columns = $board['columns'] ?? [];
|
||||
$canAdvanceStage = $canAdvanceStage ?? false;
|
||||
$specialtyStageRoute = $specialtyStageRoute ?? 'care.specialty.ambulance.stage';
|
||||
@endphp
|
||||
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm shadow-slate-100/80">
|
||||
<div class="border-b border-slate-100 bg-gradient-to-r from-amber-50/90 via-white to-slate-50 px-4 py-3 sm:px-5">
|
||||
<p class="text-sm font-semibold text-slate-900">Dispatch board</p>
|
||||
<p class="mt-0.5 text-sm text-slate-500">Track calls from intake through dispatch, scene, transport, and facility handover — not a desk call-next line.</p>
|
||||
</div>
|
||||
|
||||
<div class="flex snap-x snap-mandatory gap-0 overflow-x-auto xl:grid xl:grid-cols-6 xl:overflow-visible" style="-ms-overflow-style:none;scrollbar-width:none;">
|
||||
@foreach ($columns as $index => $column)
|
||||
<section @class([
|
||||
'flex w-[min(78vw,18rem)] shrink-0 snap-start flex-col border-slate-100 xl:w-auto',
|
||||
'border-r' => $index < count($columns) - 1,
|
||||
])>
|
||||
<header class="sticky top-0 z-[1] flex items-center justify-between gap-2 border-b border-slate-100 bg-white/95 px-3 py-3 backdrop-blur-sm">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<span class="h-2 w-2 shrink-0 rounded-full {{ $column['dot'] }}"></span>
|
||||
<h2 class="truncate text-xs font-semibold uppercase tracking-wide {{ $column['accent'] }}">{{ $column['label'] }}</h2>
|
||||
</div>
|
||||
<span class="rounded-md bg-slate-100 px-2 py-0.5 text-xs font-semibold tabular-nums text-slate-700">{{ $column['count'] }}</span>
|
||||
</header>
|
||||
<div class="flex-1 space-y-2 p-3 sm:min-h-[12rem]">
|
||||
@forelse ($column['items'] as $visit)
|
||||
@include('care.specialty.ambulance.dispatch-card', [
|
||||
'visit' => $visit,
|
||||
'column' => $column,
|
||||
'canAdvanceStage' => $canAdvanceStage && $column['key'] !== 'completed',
|
||||
'specialtyStageRoute' => $specialtyStageRoute,
|
||||
])
|
||||
@empty
|
||||
<p class="rounded-xl border border-dashed border-slate-200 px-3 py-8 text-center text-xs text-slate-400">{{ $column['empty'] }}</p>
|
||||
@endforelse
|
||||
</div>
|
||||
</section>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,58 @@
|
||||
{{-- Single EMS call card on the ambulance dispatch board. --}}
|
||||
@php
|
||||
$appointment = $visit->appointment;
|
||||
$patient = $visit->patient;
|
||||
$openUrl = route('care.specialty.workspace', ['module' => 'ambulance', 'visit' => $visit]);
|
||||
$ticketNumber = $appointment?->queue_ticket_number;
|
||||
$reason = $appointment?->reason;
|
||||
$nextStage = $column['next_stage'] ?? null;
|
||||
$nextLabel = $column['next_label'] ?? null;
|
||||
$isCompleted = ($column['key'] ?? '') === 'completed';
|
||||
@endphp
|
||||
<article
|
||||
role="link"
|
||||
tabindex="0"
|
||||
data-href="{{ $openUrl }}"
|
||||
onclick="if (! event.target.closest('a, button, form, input, select, textarea, label')) { window.location.href = this.dataset.href }"
|
||||
onkeydown="if ((event.key === 'Enter' || event.key === ' ') && ! event.target.closest('a, button, form, input, select, textarea, label')) { event.preventDefault(); window.location.href = this.dataset.href }"
|
||||
@class([
|
||||
'group relative flex gap-3 rounded-xl border px-3 py-3 transition cursor-pointer hover:shadow-sm',
|
||||
'border-amber-200 bg-amber-50/70' => ($column['key'] ?? '') === 'check_in',
|
||||
'border-indigo-200 bg-indigo-50/50' => ($column['key'] ?? '') === 'dispatch',
|
||||
'border-sky-200 bg-sky-50/50' => ($column['key'] ?? '') === 'on_scene',
|
||||
'border-violet-200 bg-violet-50/50' => ($column['key'] ?? '') === 'transport',
|
||||
'border-emerald-200 bg-emerald-50/50' => ($column['key'] ?? '') === 'handover',
|
||||
'border-slate-100 bg-white opacity-90' => $isCompleted,
|
||||
])
|
||||
>
|
||||
<div class="min-w-0 flex-1 space-y-1.5">
|
||||
@if ($ticketNumber)
|
||||
<span class="inline-flex rounded-md bg-slate-900 px-2 py-0.5 font-mono text-[11px] font-semibold tracking-wide text-white">{{ $ticketNumber }}</span>
|
||||
@endif
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-sm font-semibold text-slate-900">{{ $patient?->fullName() ?? 'Patient' }}</p>
|
||||
<p class="mt-0.5 truncate text-xs text-slate-500">
|
||||
{{ $patient?->patient_number ?? '—' }}
|
||||
@if ($reason)
|
||||
<span class="text-slate-300"> · </span>{{ \Illuminate\Support\Str::limit($reason, 40) }}
|
||||
@endif
|
||||
</p>
|
||||
@if ($appointment?->practitioner?->name)
|
||||
<p class="mt-0.5 truncate text-xs text-slate-400">{{ $appointment->practitioner->name }}</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative z-10 flex shrink-0 flex-col items-end justify-center gap-1.5" @click.stop>
|
||||
<a href="{{ $openUrl }}" class="rounded-lg border border-slate-200 bg-white px-2.5 py-1 text-[11px] font-medium text-slate-700 hover:bg-slate-50">Open</a>
|
||||
@if ($canAdvanceStage && $nextStage && $nextLabel)
|
||||
<form method="POST" action="{{ route($specialtyStageRoute, $visit) }}">
|
||||
@csrf
|
||||
<input type="hidden" name="stage" value="{{ $nextStage }}">
|
||||
<button type="submit" class="rounded-lg bg-slate-900 px-2.5 py-1 text-[11px] font-semibold text-white hover:bg-slate-800">
|
||||
{{ $nextLabel }}
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</article>
|
||||
@@ -83,7 +83,7 @@
|
||||
$currentStage = $workspaceVisit?->specialty_stage;
|
||||
@endphp
|
||||
|
||||
@if ($onWorkspace && $canCallNext)
|
||||
@if ($onWorkspace && $canCallNext && ($moduleKey ?? '') !== 'ambulance')
|
||||
@include('care.partials.queue-ops', [
|
||||
'queueIntegration' => $queueIntegration ?? null,
|
||||
'queueCallNextRoute' => 'care.specialty.call-next',
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
'workspace' => ($patientHeader['patient'] ?? null)?->fullName() ?: 'Workspace',
|
||||
'history' => 'Visit history',
|
||||
'billing' => 'Billing',
|
||||
'visits' => 'Patient queue',
|
||||
default => 'Patient queue',
|
||||
'visits' => ($moduleKey ?? '') === 'ambulance' ? 'Dispatch board' : 'Patient queue',
|
||||
default => ($moduleKey ?? '') === 'ambulance' ? 'Dispatch board' : 'Patient queue',
|
||||
};
|
||||
@endphp
|
||||
<x-app-layout :title="$pageTitle">
|
||||
@@ -197,66 +197,96 @@
|
||||
</div>
|
||||
@include('care.specialty.sections.billing')
|
||||
@else
|
||||
{{-- Queue home (overview / visits) — same UI as GP Queue --}}
|
||||
{{-- Queue / dispatch home --}}
|
||||
<div class="lg:grid lg:grid-cols-[minmax(0,1fr)_16rem] lg:items-start lg:gap-6">
|
||||
<div class="min-w-0 space-y-6">
|
||||
@php
|
||||
$specialtyLabel = $definition['nav_label'] ?? $definition['label'] ?? ucfirst($moduleKey);
|
||||
$isAmbulanceHome = $moduleKey === 'ambulance';
|
||||
@endphp
|
||||
<div class="mb-1 flex items-center gap-2.5">
|
||||
<x-care.specialty-icon :module="$moduleKey" boxed />
|
||||
<p class="text-sm font-semibold text-slate-700">{{ $specialtyLabel }}</p>
|
||||
</div>
|
||||
<x-care.page-hero
|
||||
badge="Patient flow · Call next"
|
||||
:title="$specialtyLabel.' queue'"
|
||||
description="Move patients from waiting through called, in care, and done."
|
||||
:stats="[
|
||||
['value' => number_format($kpis['waiting'] ?? $queue->count()), 'label' => 'Waiting'],
|
||||
['value' => number_format($kpis['in_progress'] ?? $inConsultation->count()), 'label' => 'In care'],
|
||||
['value' => number_format($kpis['completed_today'] ?? 0), 'label' => 'Today'],
|
||||
]">
|
||||
@if ($canManageQueue ?? false)
|
||||
<x-slot name="actions">
|
||||
<a href="{{ route('care.appointments.walk-in.create') }}" class="btn-primary">Walk-in</a>
|
||||
</x-slot>
|
||||
@endif
|
||||
</x-care.page-hero>
|
||||
@if ($isAmbulanceHome)
|
||||
<x-care.page-hero
|
||||
badge="EMS · Dispatch"
|
||||
:title="$specialtyLabel.' dispatch'"
|
||||
description="Respond to call-ins — track New call through dispatch, scene, transport, and handover."
|
||||
:stats="[
|
||||
['value' => number_format($ambulanceDispatchBoard['active_count'] ?? 0), 'label' => 'Active calls'],
|
||||
['value' => number_format($ambulanceDispatchBoard['completed_today'] ?? 0), 'label' => 'Completed today'],
|
||||
['value' => number_format($kpis['clinical_open'] ?? 0), 'label' => 'On scene open'],
|
||||
]">
|
||||
@if ($canManageQueue ?? false)
|
||||
<x-slot name="actions">
|
||||
<a href="{{ route('care.appointments.walk-in.create') }}" class="btn-primary">New call</a>
|
||||
</x-slot>
|
||||
@endif
|
||||
</x-care.page-hero>
|
||||
@else
|
||||
<x-care.page-hero
|
||||
badge="Patient flow · Call next"
|
||||
:title="$specialtyLabel.' queue'"
|
||||
description="Move patients from waiting through called, in care, and done."
|
||||
:stats="[
|
||||
['value' => number_format($kpis['waiting'] ?? $queue->count()), 'label' => 'Waiting'],
|
||||
['value' => number_format($kpis['in_progress'] ?? $inConsultation->count()), 'label' => 'In care'],
|
||||
['value' => number_format($kpis['completed_today'] ?? 0), 'label' => 'Today'],
|
||||
]">
|
||||
@if ($canManageQueue ?? false)
|
||||
<x-slot name="actions">
|
||||
<a href="{{ route('care.appointments.walk-in.create') }}" class="btn-primary">Walk-in</a>
|
||||
</x-slot>
|
||||
@endif
|
||||
</x-care.page-hero>
|
||||
@endif
|
||||
|
||||
<div class="flex flex-wrap gap-3 text-sm">
|
||||
<a href="{{ route('care.specialty.history', $moduleKey) }}" class="rounded-lg border border-slate-200 bg-white px-3 py-1.5 font-medium text-slate-700 hover:bg-slate-50">History</a>
|
||||
@if ($canManageClinical ?? $canManageSpecialty ?? true)
|
||||
<a href="{{ route('care.specialty.billing', $moduleKey) }}" class="rounded-lg border border-slate-200 bg-white px-3 py-1.5 font-medium text-slate-700 hover:bg-slate-50">Billing</a>
|
||||
@endif
|
||||
@if ($isAmbulanceHome)
|
||||
<a href="{{ route('care.specialty.ambulance.reports') }}" class="rounded-lg border border-slate-200 bg-white px-3 py-1.5 font-medium text-slate-700 hover:bg-slate-50">Reports</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@include('care.partials.queue-board', [
|
||||
'queue' => $queue,
|
||||
'inConsultation' => $inConsultation,
|
||||
'done' => $done ?? collect(),
|
||||
'queueIntegration' => $queueIntegration ?? null,
|
||||
'branchId' => $branchId,
|
||||
'branchLabel' => $branchLabel ?? null,
|
||||
'canConsult' => $canConsult ?? false,
|
||||
'lockToPractitioner' => $lockToPractitioner ?? false,
|
||||
'showFilters' => true,
|
||||
'queueCallNextRoute' => ($canCallNext ?? $canManageSpecialty ?? false) ? 'care.specialty.call-next' : null,
|
||||
'queueCallNextParams' => ['module' => $moduleKey],
|
||||
'startRouteName' => 'care.queue.start',
|
||||
'inCareLabel' => 'In care',
|
||||
'openInCareUrl' => function ($appointment) use ($moduleKey) {
|
||||
if ($appointment->visit) {
|
||||
return route('care.specialty.workspace', [
|
||||
'module' => $moduleKey,
|
||||
'visit' => $appointment->visit,
|
||||
]);
|
||||
}
|
||||
@if ($isAmbulanceHome)
|
||||
@include('care.specialty.ambulance.dispatch-board', [
|
||||
'ambulanceDispatchBoard' => $ambulanceDispatchBoard ?? ['columns' => [], 'active_count' => 0, 'completed_today' => 0],
|
||||
'canAdvanceStage' => $canAdvanceStage ?? false,
|
||||
'specialtyStageRoute' => $specialtyStageRoute ?? 'care.specialty.ambulance.stage',
|
||||
])
|
||||
@else
|
||||
@include('care.partials.queue-board', [
|
||||
'queue' => $queue,
|
||||
'inConsultation' => $inConsultation,
|
||||
'done' => $done ?? collect(),
|
||||
'queueIntegration' => $queueIntegration ?? null,
|
||||
'branchId' => $branchId,
|
||||
'branchLabel' => $branchLabel ?? null,
|
||||
'canConsult' => $canConsult ?? false,
|
||||
'lockToPractitioner' => $lockToPractitioner ?? false,
|
||||
'showFilters' => true,
|
||||
'queueCallNextRoute' => ($canCallNext ?? $canManageSpecialty ?? false) ? 'care.specialty.call-next' : null,
|
||||
'queueCallNextParams' => ['module' => $moduleKey],
|
||||
'startRouteName' => 'care.queue.start',
|
||||
'inCareLabel' => 'In care',
|
||||
'openInCareUrl' => function ($appointment) use ($moduleKey) {
|
||||
if ($appointment->visit) {
|
||||
return route('care.specialty.workspace', [
|
||||
'module' => $moduleKey,
|
||||
'visit' => $appointment->visit,
|
||||
]);
|
||||
}
|
||||
|
||||
return $appointment->consultation
|
||||
? route('care.consultations.show', $appointment->consultation)
|
||||
: null;
|
||||
},
|
||||
])
|
||||
return $appointment->consultation
|
||||
? route('care.consultations.show', $appointment->consultation)
|
||||
: null;
|
||||
},
|
||||
])
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@include('care.partials.clinical-actions-panel', [
|
||||
|
||||
Reference in New Issue
Block a user