@extends('layouts.app') @section('title', __('prescriptions::messages.prescriptions.title')) @php use App\Modules\Prescriptions\Enums\PrescriptionStatus; use App\Modules\Prescriptions\Models\Prescription; /* | The brief names five states; the record stores four, and two of the five | are the same status seen at different points in the calendar. | | issued + still valid → Active | issued + past its date → Expired | draft → Pending | superseded → Renewed (it was reissued, and this is the | prescription that was replaced) | cancelled → Cancelled | | Expiry is derived rather than stored, so it is worked out once here | rather than in three places that can disagree. */ $presentation = static function (Prescription $p): array { if ($p->status === PrescriptionStatus::Issued && $p->hasExpired()) { return ['px-pill--red', __('prescriptions::messages.prescriptions.state_expired')]; } return match ($p->status) { PrescriptionStatus::Issued => ['px-pill--green', __('prescriptions::messages.prescriptions.state_active')], PrescriptionStatus::Draft => ['px-pill--amber', __('prescriptions::messages.prescriptions.state_pending')], PrescriptionStatus::Superseded => ['px-pill--blue', __('prescriptions::messages.prescriptions.state_renewed')], PrescriptionStatus::Cancelled => ['px-pill--grey', $p->status->label()], }; }; $cards = [ [ 'key' => 'active', 'label' => __('prescriptions::messages.prescriptions.card_active'), 'icon' => 'bi-capsule', 'tone' => 'violet', ], [ 'key' => 'expiring', 'label' => __('prescriptions::messages.prescriptions.card_expiring'), 'icon' => 'bi-hourglass-bottom', 'tone' => 'pink', ], [ 'key' => 'renewed', 'label' => __('prescriptions::messages.prescriptions.card_renewed'), 'icon' => 'bi-arrow-repeat', 'tone' => 'blue', ], [ 'key' => 'controlled', 'label' => __('prescriptions::messages.prescriptions.card_controlled'), 'icon' => 'bi-shield-lock-fill', 'tone' => 'green', ], ]; /* | Whether anything is actually narrowing the list. | | search_field is excluded: it always carries a value — the pill shown as | selected — so counting it would make a screen nobody has filtered offer | to clear filters that were never applied. It narrows nothing on its own; | it only says where the term beside it is aimed. */ $hasFilters = collect($filters) ->except(['search_field']) ->filter(fn ($v): bool => $v !== '') ->isNotEmpty(); @endphp @section('content') {{-- ── Page head ──────────────────────────────────────────────────────── --}}

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

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

@if (Route::has('formulary.index')) {{ __('prescriptions::messages.prescriptions.library') }} @endif {{-- Every prescription belongs to a patient. Arrived from that patient's record, the question is already answered and this starts the draft outright; arrived from the sidebar, it asks first. Either way the button finishes the job it names. --}} @can('create', Prescription::class) @if ($forPatient !== null)
@csrf
@else {{ __('prescriptions::messages.prescriptions.new') }} @endif @endcan
{{-- ── Filter panel ───────────────────────────────────────────────────── --}} {{-- Outside the swapped region: that region is replaced wholesale on every filter change, which would destroy the field being typed into. --}}
@php $activeField = \App\Modules\Patients\Enums\PatientSearchField::tryFrom($filters['search_field'] ?? '') ?? $defaultSearchField; // What the box should say it is expecting, per field. Read by // Clinic.searchField in app.js so the placeholder follows the pill // without a round trip. $searchRules = []; foreach ($searchFields as $searchField) { $searchRules[$searchField->value] = ['placeholder' => $searchField->placeholder()]; } @endphp {{-- One line of filters, not a grid of them. The twelve-column layout gave each control a fixed share of the row, so six filters became three ragged lines with the buttons stranded below. .px-filters lets every control take what it needs and wrap only when the line is genuinely full — the same bar Appointments uses, so the two screens filter the same way. --}}
{{-- Which identifier the term is aimed at, shown flat rather than in a dropdown: three options hidden behind a click is a clerk wondering whether they can search by phone at all. --}}
@foreach ($searchFields as $searchField) @endforeach
{{-- Hidden, not deleted: the pills above already say "search by", and the placeholder repeats it. It stays in the markup so a screen reader still gets the field a name rather than reading the hint as one. --}}
@if ($departments !== [])
@endif
{{ __('common.reset') }}
{{-- Date windows, collapsed. Four date inputs open by default is where people stop reading a filter bar; opened automatically when one is set, so an active filter is never hidden from the person it affects. Below the line rather than in it: expanded inside a row of single controls, they would push everything else onto a second line every time somebody opened them. --}} @php $hasDates = collect($filters) ->only(['issued_from', 'issued_to', 'expires_from', 'expires_to']) ->filter(fn ($v): bool => $v !== '') ->isNotEmpty(); @endphp
@foreach ([ 'issued_from' => __('prescriptions::messages.prescriptions.issued_from'), 'issued_to' => __('prescriptions::messages.prescriptions.issued_to'), 'expires_from' => __('prescriptions::messages.prescriptions.expires_from'), 'expires_to' => __('prescriptions::messages.prescriptions.expires_to'), ] as $field => $label) {{-- The one date component in the product. The column class goes on it rather than on a wrapper: it renders its own label and field, so a wrapper would only exist to hold a class. --}} @endforeach
{{-- ── Statistics ─────────────────────────────────────────── --}}
@foreach ($cards as $card) @php $value = $summary['tally'][$card['key']]; $delta = $summary['today'][$card['key']] - $summary['previous'][$card['key']]; @endphp

{{ $card['label'] }}

{{ number_format($value) }}

@if ($delta !== 0) @endif {{ __('prescriptions::messages.prescriptions.vs_previous', [ 'delta' => $delta > 0 ? '+'.$delta : (string) $delta, ]) }}

@endforeach
{{-- ── Prescriptions ──────────────────────────────────────── --}}

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

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

@if ($prescriptions->isEmpty()) {{-- ── Empty state ────────────────────────────────── --}}

{{ __('prescriptions::messages.prescriptions.empty_title') }}

{{ $hasFilters ? __('prescriptions::messages.prescriptions.empty_filtered') : __('prescriptions::messages.prescriptions.empty_text') }}

@can('create', Prescription::class) @if (Route::has('patients.index')) {{ __('prescriptions::messages.prescriptions.new') }} @endif @endcan @if ($hasFilters) {{ __('prescriptions::messages.prescriptions.clear_filters') }} @endif
@else
@foreach ($prescriptions as $prescription) @php $patient = $participants['patients'][$prescription->patient_id] ?? null; $doctor = $participants['doctors'][$prescription->doctor_id] ?? null; $patientName = $patient?->name ?? __('prescriptions::messages.prescriptions.unknown_patient'); [$pill, $stateLabel] = $presentation($prescription); $remaining = $prescription->daysUntilExpiry(); @endphp {{-- Name, MRN and reference in one column, and no avatar. The circle carried no information the name did not, and the reference held a whole column of its own to show one line of text. --}} @endforeach
{{ __('prescriptions::messages.prescriptions.patient') }} {{ __('prescriptions::messages.prescriptions.prescriber') }} {{ __('prescriptions::messages.items.title') }} {{ __('prescriptions::messages.prescriptions.issued_at') }} {{ __('prescriptions::messages.prescriptions.valid_until') }} {{ __('common.status') }} {{ __('common.actions') }}
@if ($patient !== null && Route::has('patients.show')) {{ $patientName }} @else {{ $patientName }} @endif {{-- Guarded: a prescription written before the patient was matched has no MRN, and an unconditional line left a blank gap between the name and the reference. --}} @if ($patient?->mrn) {{ $patient->mrn }} @if ($patient->ageLabel) · {{ $patient->ageLabel }} @endif @endif {{-- Still the link to the prescription: it was what the removed column linked from, and it is how staff refer to one aloud. --}} {{ $prescription->reference }}
{{ $doctor?->name ?? '—' }} @if ($doctor?->specialty) {{ $doctor->specialty }} @endif
@foreach ($prescription->items->take(2) as $item) {{ $item->drug_name }} @endforeach @if ($prescription->items_count > 2) +{{ $prescription->items_count - 2 }} @endif {{-- The prescriber went ahead over an allergy warning. Visible from the list, because by the time somebody has opened the record they are already committed. --}} @if ($prescription->allergy_warnings) @endif
{{ $prescription->issued_at?->format('d M Y') ?? '—' }} @if ($prescription->valid_until === null) — @else {{ $prescription->valid_until->format('d M Y') }} @if ($prescription->status === PrescriptionStatus::Issued) {{ $remaining < 0 ? __('prescriptions::messages.prescriptions.expired_ago', ['days' => abs($remaining)]) : trans_choice('prescriptions::messages.prescriptions.days_left', $remaining, ['count' => $remaining]) }} @endif @endif {{ $stateLabel }}
@if ($prescriptions->hasPages())
{{ $prescriptions->links() }}
@endif @endif
{{-- ── Context ────────────────────────────────────────────────── --}}
@endsection