Files
ladill-care/resources/views/care/dashboard.blade.php
T
isaaccladandCursor dfb66369d0
Deploy Ladill Care / deploy (push) Successful in 30s
Hide Laboratory nav from doctors who only have lab.view.
The sidebar and dashboard linked to the lab work queue (lab.manage) while
gating on lab.view, so physio/GP doctors hit 403 on open.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-19 17:31:14 +00:00

361 lines
22 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.manage')) {
$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)
@php
$toSettings = $permissions->can($member, 'settings.view');
$href = $toSettings
? route('care.settings.modules.show', $item['key'])
: route('care.specialty.show', $item['key']);
@endphp
<a href="{{ $href }}"
class="rounded-2xl border border-slate-200 bg-white px-4 py-4 transition hover:border-indigo-300 hover:shadow-sm">
<div class="flex items-start justify-between gap-2">
<div class="flex min-w-0 items-start gap-2.5">
<x-care.specialty-icon :module="$item['key']" boxed />
<div class="min-w-0">
<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>
@if ($toSettings)
<p class="mt-2 text-[11px] font-medium text-indigo-600">Settings &amp; pricing </p>
@endif
</div>
</div>
@if (! empty($item['access_level']) && $item['access_level'] !== 'manage' && ! $toSettings)
<span class="shrink-0 rounded-full bg-amber-50 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-800">
{{ $item['access_level'] === 'refer' ? 'View + refer' : 'View' }}
</span>
@endif
</div>
</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
@if (! empty($showBillingPanel) && $paymentStats)
@php
$money = fn (int $minor) => $currency.' '.number_format($minor / 100, 2);
@endphp
<section class="mt-6">
<div class="flex flex-wrap items-end justify-between gap-3">
<div>
<h2 class="text-sm font-semibold text-slate-900">Payments today</h2>
<p class="mt-0.5 text-xs text-slate-500">Collections at your branch</p>
</div>
<a href="{{ route('care.bills.index') }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800">Open billing</a>
</div>
<div class="mt-3 grid grid-cols-2 gap-4 lg:grid-cols-4">
<div class="rounded-2xl border border-slate-200 bg-white p-5">
<p class="text-xs font-medium uppercase tracking-wide text-slate-500">Collected</p>
<p class="mt-3 text-2xl font-semibold text-emerald-700">{{ $money($paymentStats['collected_minor']) }}</p>
</div>
<div class="rounded-2xl border border-slate-200 bg-white p-5">
<p class="text-xs font-medium uppercase tracking-wide text-slate-500">Payments</p>
<p class="mt-3 text-2xl font-semibold text-slate-900">{{ number_format($paymentStats['payment_count']) }}</p>
</div>
<div class="rounded-2xl border border-slate-200 bg-white p-5">
<p class="text-xs font-medium uppercase tracking-wide text-slate-500">Cash</p>
<p class="mt-3 text-2xl font-semibold text-slate-900">{{ $money($paymentStats['cash_minor']) }}</p>
</div>
<div class="rounded-2xl border border-slate-200 bg-white p-5">
<p class="text-xs font-medium uppercase tracking-wide text-slate-500">Other methods</p>
<p class="mt-3 text-2xl font-semibold text-slate-900">{{ $money($paymentStats['other_minor']) }}</p>
</div>
</div>
</section>
<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">Recently paid bills</h2>
<p class="mt-0.5 text-xs text-slate-500">Latest settled invoices</p>
</div>
<a href="{{ route('care.bills.index', ['status' => \App\Models\Bill::STATUS_PAID]) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800">View paid</a>
</div>
<div class="overflow-x-auto">
<table class="min-w-full text-sm">
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
<tr>
<th class="px-5 py-3">Invoice</th>
<th class="px-5 py-3">Patient</th>
<th class="px-5 py-3">Total</th>
<th class="px-5 py-3">Paid at</th>
<th class="px-5 py-3 text-right"> </th>
</tr>
</thead>
<tbody class="divide-y divide-slate-50">
@forelse ($recentPaidBills as $bill)
@php
$billUrl = route('care.bills.show', $bill);
$paidAt = $bill->last_paid_at
? \Illuminate\Support\Carbon::parse($bill->last_paid_at)->format('d M Y, H:i')
: '—';
@endphp
<tr class="hover:bg-slate-50">
<td class="px-5 py-3 font-mono text-xs">
<a href="{{ $billUrl }}" class="font-medium text-sky-600 hover:text-sky-700">{{ $bill->invoice_number }}</a>
</td>
<td class="px-5 py-3">{{ $bill->patient?->fullName() ?? 'Patient' }}</td>
<td class="px-5 py-3 tabular-nums">{{ $money((int) $bill->total_minor) }}</td>
<td class="px-5 py-3 text-slate-500">{{ $paidAt }}</td>
<td class="px-5 py-3 text-right">
<a href="{{ $billUrl }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800">View</a>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="px-5 py-8 text-center text-sm text-slate-500">No paid bills yet.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</section>
@endif
</x-app-layout>