@extends('layouts.app') @section('title', __('laboratory::messages.orders.title')) @php use App\Modules\Laboratory\Enums\ItemStatus; use App\Modules\Laboratory\Enums\OrderPriority; use App\Modules\Laboratory\Enums\OrderStatus; use App\Modules\Laboratory\Models\LabOrder; /* | Ordered is "waiting to be drawn", collected is "on the bench but not | started" — grey and blue respectively, which is how a laboratory reads | its own queue. The brief lists "critical" among the statuses; it is not | one. A critical value is a property of a result, and an order carrying | one is usually also completed, so it is surfaced as its own marker | rather than by overwriting the status. */ $statusPill = static fn (OrderStatus $status): string => match ($status) { OrderStatus::Ordered => 'px-pill--grey', OrderStatus::Collected => 'px-pill--blue', OrderStatus::InProgress => 'px-pill--amber', OrderStatus::Completed => 'px-pill--green', OrderStatus::Cancelled => 'px-pill--red', }; $priorityPill = static fn (OrderPriority $priority): string => match ($priority) { OrderPriority::Routine => 'px-pill--grey', OrderPriority::Urgent => 'px-pill--amber', OrderPriority::Stat => 'px-pill--red', }; $cards = [ [ 'key' => 'pending', 'label' => __('laboratory::messages.orders.card_pending'), 'icon' => 'bi-inboxes-fill', 'tone' => 'blue', ], [ 'key' => 'processing', 'label' => __('laboratory::messages.orders.card_processing'), 'icon' => 'bi-arrow-repeat', 'tone' => 'violet', ], [ 'key' => 'completed', 'label' => __('laboratory::messages.orders.card_completed'), 'icon' => 'bi-check2-circle', 'tone' => 'green', ], [ 'key' => 'critical', 'label' => __('laboratory::messages.orders.card_critical'), 'icon' => 'bi-exclamation-octagon-fill', 'tone' => 'pink', ], ]; /* | Whether anything is actually narrowing the worklist. | | 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 ──────────────────────────────────────────────────────── --}}

{{ __('laboratory::messages.orders.title') }}

{{ __('laboratory::messages.orders.subtitle') }}

{{-- The collection round is the worklist filtered to what has been ordered but not yet drawn — not a separate screen. --}} {{ __('laboratory::messages.orders.collection_round') }} {{-- An order always belongs to a patient, so the first step is choosing one — there is no useful blank form before that. This once sent people to the patient list and left them to work out the rest, which read as a wrong turn rather than as step one. It now opens the ordering steps, which ask the same question and then carry on to the answer. --}} @can('create', LabOrder::class) {{ __('laboratory::messages.orders.new') }} @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 patient 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
{{-- Which identifier the patient box is aimed at. Outside the bar below, so the pills sit on the box they aim rather than in a gutter above it. --}}
@foreach ($searchFields as $searchField) @endforeach
{{-- One row, not a grid. On the twelve-column grid these eight controls came to nineteen columns and wrapped, so the filters took two rows and more vertical space than the worklist they filter. Flexed, they share whatever width there is and fall onto a second row only when there is genuinely no room. The accession box is gone from here. It was the second search box on a row that already had one, and the reference is on every line of the table below. `?search=` still filters by it, so a barcode scanner that types a URL keeps working. --}}
{{-- Hidden, not deleted: the pills directly above name this box — that is what "search by" is choosing for — and the placeholder repeats it. It stays in the markup so a screen reader still gets the field a name. --}}
@if ($departments !== [])
@endif
{{ __('common.reset') }}
{{-- ── 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 {{ __('laboratory::messages.orders.vs_previous', [ 'delta' => $delta > 0 ? '+'.$delta : (string) $delta, ]) }}

@endforeach
{{-- ── Worklist ───────────────────────────────────────────── --}}

{{ __('laboratory::messages.orders.title') }}

{{ __('laboratory::messages.orders.showing', [ 'from' => number_format($orders->firstItem() ?? 0), 'to' => number_format($orders->lastItem() ?? 0), 'total' => number_format($orders->total()), ]) }} @if ($filters['status'] === '') · {{ __('laboratory::messages.orders.open_only') }} @endif

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

{{ __('laboratory::messages.orders.empty_title') }}

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

@else
{{-- No accession column. The order is opened from the row's own actions menu; the reference and the accession number are both on the order screen itself. --}} @foreach ($orders as $order) @php $patient = $participants['patients'][$order->patient_id] ?? null; $doctor = $participants['doctors'][$order->ordered_by] ?? null; $patientName = $patient?->name ?? __('laboratory::messages.orders.unknown_patient'); $specimen = $order->specimens->first(); /* | Progress across the lines, which is what the bench | actually tracks: an order is not "half done" — its | individual tests are resulted one at a time. */ $resulted = $order->items ->filter(fn ($item): bool => in_array( $item->status, [ItemStatus::Resulted, ItemStatus::Verified], true, )) ->count(); $total = max(1, $order->items_count); $percent = (int) round($resulted / $total * 100); @endphp {{-- The patient and what was ordered for them, in one column: the name is what somebody scans this list for, and the tests are what they read next. The name carries no record number, age or avatar for the same reason — all of them competed with it. --}} {{-- The actions sit on the row rather than behind a menu. There are at most three, they are the same three on every row, and hiding them behind a three-dot button cost a click and a guess for somebody working through a list of orders. --}} @endforeach
{{ __('laboratory::messages.orders.patient') }} {{ __('laboratory::messages.orders.ordered_by') }} {{ __('laboratory::messages.orders.collected_at') }} {{ __('laboratory::messages.orders.priority') }} {{ __('laboratory::messages.orders.specimen') }} {{ __('common.status') }} {{ __('common.actions') }}
@if ($patient !== null && Route::has('patients.show')) {{ $patientName }} @else {{ $patientName }} @endif
{{-- Fasting travels with the row because it is the one thing that invalidates a sample if missed. --}} @if ($order->fasting) @endif
@foreach ($order->items->take(2) as $item) {{ $item->test_name }} @endforeach @if ($order->items_count > 2) +{{ $order->items_count - 2 }} @endif
{{ $resulted }}/{{ $order->items_count }}
@php /* | The doctor if one is on record, otherwise whoever | placed the order. Most orders here are raised by | staff with no doctor profile, and naming nobody | while the printed report names somebody is the | worse of the two answers. */ $requestedBy = $doctor?->name ?? $order->orderedBy?->name; @endphp {{-- No avatar here. A coloured disc adds nothing to a name already written beside it, and this column is scanned for the name. The patient column keeps its avatar, where the initial is doing the work of telling one row from another at a glance. --}}
{{ $requestedBy ?? '—' }} @if ($doctor?->specialty) {{ $doctor->specialty }} @endif
@if ($specimen !== null) {{ $specimen->collected_at->format('d M') }} {{ $specimen->collected_at->format('H:i') }} @else {{ __('laboratory::messages.orders.awaiting_collection') }} @endif @if ($order->priority === OrderPriority::Stat) @endif {{ $order->priority->label() }} @if ($specimen !== null) {{ $specimen->specimen_type }} @else — @endif {{ $order->status->label() }}
{{-- Icon-only, so every one carries its own name with the order reference in it: nine links all called "View order" are no use to somebody reading by screen reader. --}} @if ($order->status === OrderStatus::Completed) @endif @if ($patient !== null && Route::has('patients.show')) @endif
@if ($orders->hasPages())
{{ $orders->links() }}
@endif @endif
{{-- ── Bench context ──────────────────────────────────────────── --}}
@endsection