Files
ladill-care/resources/views/care/dashboard.blade.php
T
isaaccladandCursor 015a4cc7fe
Deploy Ladill Care / deploy (push) Successful in 1m45s
Wire Care lists into Ladill Queue workflows instead of reception panels.
When Queue integration is on, role pages issue tickets into their own
department queues and drive Call next → Serve → Complete on the native
lists. Integration off leaves existing UI unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 16:27:48 +00:00

259 lines
16 KiB
PHP

<x-app-layout title="Dashboard">
@php
$currency = config('care.billing.currency');
$operationalCards = [];
if ($permissions->can($member, 'patients.view')) {
$operationalCards[] = [
'label' => 'Patients today',
'value' => number_format($operational['patients_today']),
'href' => route('care.patients.index'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" />',
];
}
if ($permissions->can($member, 'appointments.view')) {
$operationalCards[] = [
'label' => 'Appointments today',
'value' => number_format($operational['appointments_today']),
'href' => route('care.appointments.index'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" />',
];
}
if ($canBills) {
$operationalCards[] = [
'label' => 'Open bills',
'value' => number_format($operational['open_bills']),
'href' => route('care.bills.index'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0 1 15.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75A.75.75 0 0 1 3 6h-.75m0 0v-.375c0-.621.504-1.125 1.125-1.125H20.25M2.25 6v9m18-10.5v.75c0 .414.336.75.75.75h.75m-1.5-1.5h.375c.621 0 1.125.504 1.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125h-.375m1.5-1.5H21a.75.75 0 0 0-.75.75v.75m0 0H3.75m0 0h-.375a1.125 1.125 0 0 1-1.125-1.125V15m1.5 1.5v-.75A.75.75 0 0 0 3 15h-.375M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />',
];
}
if ($canFinance) {
$operationalCards[] = [
'label' => 'Revenue today',
'value' => $currency.' '.number_format($operational['revenue_today_minor'] / 100, 2),
'href' => route('care.bills.index'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m-3-2.818.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.878 4.006 0l.415.33M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />',
'valueClass' => 'text-emerald-700',
];
}
if ($permissions->can($member, 'lab.view')) {
$operationalCards[] = [
'label' => 'Pending lab',
'value' => number_format($operational['pending_lab']),
'href' => route('care.lab.queue.index'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714a2.25 2.25 0 0 0 .659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 0 1-1.59.659H9.06a2.25 2.25 0 0 1-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 0 1-2.25 2.25H7.25A2.25 2.25 0 0 1 5 17v-2.5" />',
];
}
$organizationCards = [];
if ($canBranches) {
$organizationCards[] = [
'label' => 'Active branches',
'value' => number_format($stats['branches']),
'href' => route('care.branches.index'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5m-1.5 3h1.5m-1.5 3h1.5m-1.5 3h1.5m3-9H15m-1.5 3H15m-1.5 3H15m-1.5 3H15M9 21v-3.375c0-.621.504-1.125 1.125-1.125h3.75c.621 0 1.125.504 1.125 1.125V21" />',
];
}
if ($canMembers) {
$organizationCards[] = [
'label' => 'Team members',
'value' => number_format($stats['team_members']),
'href' => route('care.members.index'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M18 18.72a9.094 9.094 0 0 0 3.741-.479 3 3 0 0 0-4.682-2.72m.94 3.198.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0 1 12 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 0 1 6 18.719m12 0a5.971 5.971 0 0 0-.941-3.197m0 0A5.995 5.995 0 0 0 12 12.75a5.995 5.995 0 0 0-5.058 2.772m0 0a3 3 0 0 0-4.681 2.72 8.986 8.986 0 0 0 3.74.477m.94-3.197a5.971 5.971 0 0 0-.94 3.197M15 6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm6 3a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Zm-13.5 0a2.25 2.25 0 1 1-4.5 0 2.25 2.25 0 0 1 4.5 0Z" />',
];
}
if ($canDepartments) {
$organizationCards[] = [
'label' => 'Departments',
'value' => number_format($stats['departments']),
'href' => route('care.departments.index'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25A2.25 2.25 0 0 1 13.5 18v-2.25Z" />',
];
}
@endphp
<div class="mb-6 flex items-center justify-between gap-3">
<div class="min-w-0 flex-1">
<h1 class="truncate text-lg font-semibold tracking-tight text-slate-900 lg:text-xl">{{ $organization->name }}</h1>
<p class="hidden text-sm text-slate-500 lg:block">Healthcare management dashboard</p>
</div>
<div class="flex shrink-0 items-center gap-2">
@if ($permissions->can($member, 'patients.manage'))
@include('partials.mobile-header-btn', [
'href' => route('care.patients.create'),
'label' => 'Register patient',
'desktopLabel' => 'Register patient',
'variant' => 'outline',
'showDesktopIcon' => false,
])
@endif
@if ($permissions->can($member, 'appointments.manage'))
@include('partials.mobile-header-btn', [
'href' => route('care.appointments.create'),
'label' => 'Book appointment',
'desktopLabel' => 'Book appointment',
'variant' => 'primary',
])
@endif
</div>
</div>
@include('partials.upgrade-banner')
@if (count($operationalCards) > 0)
@php
$opCols = match (count($operationalCards)) {
1 => 'lg:grid-cols-1',
2 => 'lg:grid-cols-2',
3 => 'lg:grid-cols-3',
4 => 'lg:grid-cols-4',
default => 'lg:grid-cols-5',
};
@endphp
<div class="grid grid-cols-2 gap-4 {{ $opCols }}">
@foreach ($operationalCards as $card)
<a href="{{ $card['href'] }}" class="rounded-2xl border border-slate-200 bg-white p-5 transition hover:border-indigo-300 hover:shadow-sm">
<div class="flex items-center gap-2.5">
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-indigo-50">
<svg class="h-4 w-4 text-indigo-600" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">{!! $card['icon'] !!}</svg>
</div>
<p class="text-xs font-medium uppercase tracking-wide text-slate-500">{{ $card['label'] }}</p>
</div>
<p class="mt-3 text-2xl font-semibold {{ $card['valueClass'] ?? 'text-slate-900' }}">{{ $card['value'] }}</p>
</a>
@endforeach
</div>
@endif
@if (! empty($showPatientQueue))
<section class="mt-6 rounded-2xl border border-slate-200 bg-white">
<div class="flex flex-wrap items-center justify-between gap-3 border-b border-slate-100 px-5 py-4">
<div>
<h2 class="text-sm font-semibold text-slate-900">Patients in queue</h2>
<p class="mt-0.5 text-xs text-slate-500">
{{ $patientQueue->count() }} waiting
· {{ $inConsultation->count() }} in consultation
</p>
</div>
<a href="{{ route('care.queue.index', array_filter(['practitioner_id' => $practitionerId])) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800">Open full queue</a>
</div>
<div class="grid gap-0 lg:grid-cols-2 lg:divide-x lg:divide-slate-100">
<div class="px-5 py-4">
<h3 class="text-xs font-semibold uppercase tracking-wide text-slate-500">Waiting</h3>
<div class="mt-3 space-y-2">
@forelse ($patientQueue->take(8) as $appointment)
<div class="flex items-center justify-between gap-3 rounded-xl border border-slate-100 bg-slate-50 px-3 py-3">
<div class="min-w-0">
<p class="truncate font-medium text-slate-900">
@if ($appointment->queue_position)
<span class="mr-1.5 inline-flex h-5 w-5 items-center justify-center rounded-full bg-sky-100 text-[10px] font-bold text-sky-700">{{ $appointment->queue_position }}</span>
@endif
{{ $appointment->patient?->fullName() ?? 'Patient' }}
</p>
<p class="truncate text-xs text-slate-500">{{ $appointment->patient?->patient_number }} · {{ $appointment->reason ?? 'No reason noted' }}</p>
</div>
@if (! empty($canConsult))
<form method="POST" action="{{ route('care.queue.start', $appointment) }}" class="shrink-0">
@csrf
<button type="submit" class="rounded-lg bg-sky-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-sky-700">Start</button>
</form>
@endif
</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 right now.</p>
@endforelse
</div>
</div>
<div class="border-t border-slate-100 px-5 py-4 lg:border-t-0">
<h3 class="text-xs font-semibold uppercase tracking-wide text-slate-500">In consultation</h3>
<div class="mt-3 space-y-2">
@forelse ($inConsultation->take(8) as $appointment)
<div class="flex items-center justify-between gap-3 rounded-xl border border-emerald-100 bg-emerald-50 px-3 py-3">
<div class="min-w-0">
<p class="truncate font-medium text-slate-900">{{ $appointment->patient?->fullName() ?? 'Patient' }}</p>
<p class="truncate text-xs text-slate-500">{{ $appointment->practitioner?->name ?? 'Unassigned' }}</p>
</div>
@if ($appointment->consultation)
<a href="{{ route('care.consultations.show', $appointment->consultation) }}" class="shrink-0 text-sm font-medium text-sky-600 hover:text-sky-700">Open</a>
@endif
</div>
@empty
<p class="rounded-xl border border-dashed border-slate-200 px-4 py-8 text-center text-sm text-slate-500">No active consultations.</p>
@endforelse
</div>
</div>
</div>
</section>
@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)) {
1 => 'lg:grid-cols-1',
2 => 'lg:grid-cols-2',
default => 'lg:grid-cols-3',
};
@endphp
<div class="mt-4 grid grid-cols-2 gap-4 {{ $orgCols }}">
@foreach ($organizationCards as $card)
<a href="{{ $card['href'] }}" class="rounded-2xl border border-slate-200 bg-white p-5 transition hover:border-indigo-300 hover:shadow-sm">
<div class="flex items-center gap-2.5">
<div class="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-indigo-50">
<svg class="h-4 w-4 text-indigo-600" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">{!! $card['icon'] !!}</svg>
</div>
<p class="text-xs font-medium uppercase tracking-wide text-slate-500">{{ $card['label'] }}</p>
</div>
<p class="mt-3 text-2xl font-semibold text-slate-900">{{ $card['value'] }}</p>
</a>
@endforeach
</div>
@endif
@if ($canBranches)
<section class="mt-6 rounded-2xl border border-slate-200 bg-white">
<div class="flex items-center justify-between border-b border-slate-100 px-5 py-4">
<h2 class="text-sm font-semibold text-slate-900">Branches</h2>
@include('partials.mobile-icon-link', [
'href' => route('care.branches.index'),
'label' => 'Manage branches',
'icon' => 'arrow',
])
</div>
@forelse ($branches as $branch)
<div class="flex items-center justify-between border-b border-slate-50 px-5 py-3 last:border-0">
<div class="min-w-0">
<p class="font-medium text-slate-900">{{ $branch->name }}</p>
<p class="text-sm text-slate-500">{{ $branch->departments_count }} department(s) · {{ $branch->address ?? 'No address' }}</p>
</div>
@unless ($branch->is_active)
<span class="shrink-0 rounded-full bg-slate-100 px-2 py-1 text-xs text-slate-600">Inactive</span>
@endunless
</div>
@empty
<p class="px-5 py-8 text-center text-sm text-slate-400">No branches configured yet.</p>
@endforelse
</section>
@endif
</x-app-layout>