@extends('layouts.app') @section('title', __('patients::messages.patients.title')) @php use App\Modules\Patients\Enums\Gender; use App\Modules\Patients\Models\Patient; /* | Metrics are counted here rather than in the controller, deliberately. | | The controller is shared with the API vertical and its contract is | settled; the brief was a redesign that leaves the backend alone. The | dashboard already establishes the same pattern for read-only display | counts, and the branch scope applies through the model either way. | | Skipped on a *filter* request, not on every partial one. | | Filtering re-renders this view on every keystroke, and these four counts | do not change while somebody types — recomputing them would put four | queries behind each letter, and the cards sit outside the region a filter | replaces anyway. | | A navigation is the opposite case: the whole of
is being replaced, | so leaving the cards out means arriving at the screen without them and | having to reload the page to see them. */ $showMetrics = \App\Support\Http\PartialRequest::needsFullContent(); if ($showMetrics) { $totalPatients = Patient::query()->count(); $newThisMonth = Patient::query()->where('registered_at', '>=', now()->startOfMonth())->count(); $malePatients = Patient::query()->where('gender', Gender::Male->value)->count(); $femalePatients = Patient::query()->where('gender', Gender::Female->value)->count(); $pct = static fn (int $part): int => $totalPatients > 0 ? (int) round($part / $totalPatients * 100) : 0; } @endphp @section('content') {{-- ── Page head ──────────────────────────────────────────────────────── --}}

{{ __('patients::messages.patients.title') }}

{{ __('patients::messages.patients.subtitle') }}

@can('create', Patient::class) {{ __('patients::messages.patients.register') }} @endcan
{{-- ── Metrics ────────────────────────────────────────────────────────── --}} @if ($showMetrics)

{{ __('patients::messages.patients.total') }}

{{ number_format($totalPatients) }}

{{ __('patients::messages.patients.new_this_month', ['count' => number_format($newThisMonth)]) }}

{{ __('patients::messages.patients.new_patients') }}

{{ number_format($newThisMonth) }}

{{ __('patients::messages.patients.this_month') }}

{{ __('patients::messages.patients.male') }}

{{ number_format($malePatients) }}

{{ $pct($malePatients) }}% {{ __('patients::messages.patients.of_total') }}

{{ __('patients::messages.patients.female') }}

{{ number_format($femalePatients) }}

{{ $pct($femalePatients) }}% {{ __('patients::messages.patients.of_total') }}

@endif {{-- ── Filters ────────────────────────────────────────────────────────── --}}
{{-- data-clinic-filter keeps the live filtering: typing swaps the table below without reloading. Without JS it is an ordinary GET form. --}} @php $activeField = \App\Modules\Patients\Enums\PatientSearchField::tryFrom($filters['search_field'] ?? '') ?? $defaultSearchField; // Placeholder per field, handed to the script so the box re-labels // itself the moment a pill changes. $searchRules = []; foreach ($searchFields as $searchField) { $searchRules[$searchField->value] = ['placeholder' => $searchField->placeholder()]; } @endphp
{{-- Which column the term is aimed at. Radios rather than buttons, inside the filter form, so the choice travels with the search as an ordinary form field: changing it re-runs the live filter through the same path typing does, and the whole thing still works with JavaScript off. The labels and minimum lengths belong to the register, so they come from Patients' own enum. Outside the grid below, not a full-width row inside it: the pills and the box they aim are one control, and a grid gutter between them read as two separate settings. --}}
@foreach ($searchFields as $searchField) @endforeach
{{-- Hidden, not deleted. "Search" above a box whose placeholder already reads "Search by Mobile no.", under pills that already say "Search by", is the same word three times. It stays in the markup for anyone using a screen reader, who gets the placeholder read as a hint rather than as the field's name. --}}
{{ __('common.reset') }}
{{-- Registration window. Collapsed because most searches never need it, and an eight-control filter bar is where people stop reading. Opened automatically when a value is already set, so an active filter is never hidden from the person it affects. --}} {{-- Block form deliberately: the single-line inline variant mis-parses the nested parentheses below and silently drops everything after it. --}} @php $hasDateFilter = ($filters['registered_from'] ?? '') !== '' || ($filters['registered_to'] ?? '') !== ''; @endphp
{{-- ── Table ──────────────────────────────────────────────────────────── --}}

{{ __('patients::messages.patients.showing', [ 'from' => number_format($patients->firstItem() ?? 0), 'to' => number_format($patients->lastItem() ?? 0), 'total' => number_format($patients->total()), ]) }}

{{-- The MRN sits under the name rather than in a column of its own. It is an identifier people read off a card or repeat down a phone, not a value anyone scans a whole column of, and a fixed-width column of it was pushing the clinical fields off the right of narrower screens. --}} @forelse ($patients as $patient) @php $genderPill = match ($patient->gender?->value) { 'male' => 'px-pill--blue', 'female' => 'px-pill--pink', default => 'px-pill--grey', }; // blood_group is a plain string column, not an enum. $bloodGroup = (string) $patient->blood_group; $bloodPill = match (mb_substr($bloodGroup, 0, 1)) { 'A' => mb_strlen($bloodGroup) > 2 ? 'px-pill--amber' : 'px-pill--blue', 'B' => 'px-pill--violet', 'O' => 'px-pill--green', default => 'px-pill--grey', }; @endphp @empty @endforelse
{{ __('patients::messages.patients.singular') }} {{ __('patients::messages.fields.age') }} {{ __('patients::messages.fields.gender') }} {{ __('patients::messages.fields.blood_group') }} {{ __('patients::messages.patients.registered_on') }} {{ __('common.status') }} {{ __('common.actions') }}
{{-- No avatar. A coloured pair of initials beside every row was decoration competing with the name being scanned for, and the allergy marker beside it is the one thing here that genuinely needs to catch the eye. --}}
{{ $patient->fullName() }} {{-- Name, then phone, then MRN — each on its own line. Stacked rather than run together because two monospace strings side by side read as one long number, and the phone is what reception reaches for most often, so it sits nearest the name. --}} @if ($patient->phone) {{ $patient->phone }} @endif {{ $patient->mrn }}
{{-- An allergy marker on the listing itself. By the time someone has opened the record they are already committed to a course of action; the warning is cheapest here. --}} @if (($patient->active_allergy_count ?? 0) > 0) {{ $patient->active_allergy_count }} @endif
{{ $patient->ageLabel() }} {{ $patient->gender->label() }} @if ($bloodGroup !== '') {{ $bloodGroup }} @else — @endif {{ $patient->registered_at?->format('d M Y') }} {{ $patient->is_active ? __('common.active') : __('common.inactive') }}

{{ __('patients::messages.patients.empty') }}

{{ __('patients::messages.patients.empty_text') }}

@if ($patients->hasPages())
{{ $patients->links() }}
@endif
@endsection