Files
ladill-care/resources/views/care/consultations/show.blade.php
T
isaacclad 2ce4bc8993
Deploy Ladill Care / deploy (push) Successful in 1m26s
feat(assessments): layered clinical assessment engine end-to-end
Add a template-driven assessment system with universal intake, clinical
pathways, disease instruments (stroke MVP + extended, diabetes, and ten
specialty packs), scoring, patient outcome trends, REST/API + FHIR
export, and Enterprise org-level assessment analytics. Seed packs and
design/licensing docs ship for deploy and pre-GA review.
2026-07-16 22:58:09 +00:00

472 lines
31 KiB
PHP

<x-app-layout :title="'Consultation · '.$consultation->patient->fullName()">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<p class="text-xs font-medium uppercase tracking-wide {{ $isCompleted ? 'text-emerald-600' : 'text-amber-600' }}">
{{ $isCompleted ? 'Completed' : 'In progress' }}
</p>
<h1 class="text-2xl font-semibold text-slate-900">{{ $consultation->patient->fullName() }}</h1>
<p class="mt-1 text-sm text-slate-500">
{{ $consultation->patient->patient_number }}
@if ($consultation->practitioner) · {{ $consultation->practitioner->name }} @endif
</p>
</div>
@if ($canManage && ! $isCompleted)
<x-confirm-dialog
:name="'complete-consultation-'.$consultation->id"
title="Complete this consultation?"
message="Mark the consultation as finished. You can still prescribe or generate a bill afterward."
:action="route('care.consultations.complete', $consultation)"
confirm-label="Complete consultation"
variant="primary"
>
<x-slot:trigger>
<button type="button" class="btn-primary">Complete consultation</button>
</x-slot:trigger>
</x-confirm-dialog>
@endif
@if ($canPrescribe)
<a href="{{ route('care.prescriptions.create', $consultation) }}" class="rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Prescribe</a>
@endif
@if ($canGenerateBill && $isCompleted && $consultation->visit)
<form method="POST" action="{{ route('care.bills.generate', $consultation->visit) }}">
@csrf
<button type="submit" class="rounded-lg bg-slate-800 px-4 py-2 text-sm font-medium text-white hover:bg-slate-900">Generate bill</button>
</form>
@endif
</div>
@if ($canRequestInvestigations && $investigationTypes->isNotEmpty())
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Request investigations</h2>
<form method="POST" action="{{ route('care.lab.requests.store', $consultation) }}" class="mt-4 space-y-4">
@csrf
<div class="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
@foreach ($investigationTypes as $type)
<label class="flex items-center gap-2 rounded-lg border border-slate-100 bg-slate-50 px-3 py-2 text-sm">
<input type="checkbox" name="investigation_type_ids[]" value="{{ $type->id }}" class="rounded border-slate-300">
<span>{{ $type->name }} <span class="text-slate-400">({{ config('care.investigation_categories')[$type->category] ?? $type->category }})</span></span>
</label>
@endforeach
</div>
<div class="grid gap-4 sm:grid-cols-2">
<input type="text" name="clinical_notes" placeholder="Clinical notes for lab" class="rounded-lg border-slate-300 text-sm">
<select name="priority" class="rounded-lg border-slate-300 text-sm">
<option value="routine">Routine</option>
<option value="urgent">Urgent</option>
</select>
</div>
<button type="submit" class="btn-primary">Submit requests</button>
</form>
@if ($consultation->investigationRequests->isNotEmpty())
<div class="mt-4 border-t border-slate-100 pt-4 text-sm">
<p class="font-medium text-slate-700">Requested tests</p>
@foreach ($consultation->investigationRequests as $req)
<p class="mt-1">
<a href="{{ route('care.lab.requests.show', $req) }}" class="text-sky-600">{{ $req->investigationType->name }}</a>
· {{ config('care.investigation_statuses')[$req->status] ?? $req->status }}
</p>
@endforeach
</div>
@endif
</section>
@endif
@if ($consultation->prescriptions->isNotEmpty())
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Prescriptions</h2>
<ul class="mt-4 space-y-2 text-sm">
@foreach ($consultation->prescriptions as $rx)
<li>
<a href="{{ route('care.prescriptions.show', $rx) }}" class="text-sky-600 hover:text-sky-700">
{{ $rx->created_at->format('d M Y H:i') }}
</a>
· {{ config('care.prescription_statuses')[$rx->status] ?? $rx->status }}
· {{ $rx->items->pluck('name')->join(', ') }}
</li>
@endforeach
</ul>
</section>
@endif
@if ($canViewAssessments ?? false)
{{-- Layer 1: universal intake (structured overlay; does not replace free-text symptoms) --}}
@if ($universalTemplate)
<section class="mt-6 rounded-2xl border border-sky-100 bg-sky-50/40 p-6">
<div class="flex flex-wrap items-start justify-between gap-3">
<div>
<h2 class="text-sm font-semibold uppercase tracking-wide text-sky-800">Universal intake</h2>
<p class="mt-1 text-xs text-slate-500">
Structured Layer 1 assessment (presenting complaint, social history, function).
Free-text <span class="font-medium">Symptoms</span> and <span class="font-medium">Clinical notes</span> below remain the narrative source of truth they are not auto-copied either way.
</p>
</div>
@if ($universalAssessment)
<a href="{{ route('care.assessments.show', $universalAssessment) }}" class="rounded-lg bg-sky-600 px-3 py-2 text-sm font-medium text-white hover:bg-sky-700">
{{ $universalAssessment->isDraft() ? 'Continue intake' : 'View intake' }}
</a>
@elseif (($canCaptureUniversal ?? false) && ! $isCompleted)
<form method="POST" action="{{ route('care.consultations.assessments.store', $consultation) }}">
@csrf
<input type="hidden" name="template_code" value="universal_intake">
<button type="submit" class="rounded-lg bg-sky-600 px-3 py-2 text-sm font-medium text-white hover:bg-sky-700">Start universal intake</button>
</form>
@endif
</div>
@if ($universalAssessment)
<p class="mt-3 text-sm text-slate-600">
Status:
<span class="font-medium">{{ $assessmentStatuses[$universalAssessment->status] ?? $universalAssessment->status }}</span>
@if ($universalAssessment->consultation_id !== $consultation->id)
<span class="text-slate-400">(from another visit open to review)</span>
@endif
</p>
@php
$cc = $universalAssessment->answers->first(fn ($a) => $a->question?->code === 'chief_complaint');
$pain = $universalAssessment->answers->first(fn ($a) => $a->question?->code === 'pain_score');
@endphp
@if ($cc || $pain)
<dl class="mt-3 grid gap-2 text-sm sm:grid-cols-2">
@if ($cc)
<div>
<dt class="text-slate-500">Chief complaint</dt>
<dd class="font-medium text-slate-800">{{ \Illuminate\Support\Str::limit((string) $cc->authoritativeValue(), 160) }}</dd>
</div>
@endif
@if ($pain)
<div>
<dt class="text-slate-500">Pain score</dt>
<dd class="font-medium text-slate-800">{{ $pain->authoritativeValue() ?? '—' }}</dd>
</div>
@endif
</dl>
@endif
@elseif (! ($canCaptureUniversal ?? false))
<p class="mt-3 text-sm text-slate-400">No universal intake for this patient yet.</p>
@endif
</section>
@endif
{{-- Layer 2: pathways (suggest from persisted diagnoses; clinician confirms) --}}
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Clinical pathways</h2>
<a href="{{ route('care.pathways.index', $consultation->patient) }}" class="text-sm text-sky-600 hover:text-sky-700">Manage pathways</a>
</div>
@if (($activePathways ?? collect())->isNotEmpty())
<div class="mt-3 flex flex-wrap gap-2">
@foreach ($activePathways as $pp)
<span class="inline-flex items-center rounded-full bg-emerald-50 px-3 py-1 text-xs font-medium text-emerald-800 ring-1 ring-emerald-100">
{{ $pp->pathway->name }}
</span>
@endforeach
</div>
@endif
@if (($pathwayInstruments ?? collect())->isNotEmpty())
<div class="mt-4 overflow-hidden rounded-xl border border-slate-100">
<div class="bg-slate-50 px-3 py-2 text-xs font-semibold uppercase tracking-wide text-slate-500">
Pathway instruments
</div>
<ul class="divide-y divide-slate-100 text-sm">
@foreach ($pathwayInstruments as $instrument)
<li class="flex flex-wrap items-center justify-between gap-2 px-3 py-2.5">
<div>
<span class="font-medium text-slate-900">{{ $instrument['template_name'] }}</span>
<span class="text-xs text-slate-400">
· {{ $instrument['pathway_name'] }}
· {{ $instrument['template_code'] }}
@if ($instrument['required']) · required @endif
</span>
@if ($instrument['assessment']?->score)
<span class="ml-1 text-xs font-medium text-emerald-700">
score {{ $instrument['assessment']->score->total_score }}@if($instrument['assessment']->score->max_score !== null)/{{ $instrument['assessment']->score->max_score }}@endif
</span>
@endif
</div>
<div class="flex items-center gap-2">
@if ($instrument['pack_missing'])
<span class="text-xs text-amber-600">Content pack not seeded</span>
@elseif ($instrument['assessment'])
<span class="text-xs text-slate-500">{{ $assessmentStatuses[$instrument['assessment']->status] ?? $instrument['assessment']->status }}</span>
<a href="{{ route('care.assessments.show', $instrument['assessment']) }}" class="text-sky-600 hover:text-sky-700">
{{ $instrument['assessment']->isDraft() ? 'Continue' : 'View' }}
</a>
@elseif (($canCaptureAssessment ?? false) && ! $isCompleted)
<form method="POST" action="{{ route('care.consultations.assessments.store', $consultation) }}">
@csrf
<input type="hidden" name="template_code" value="{{ $instrument['template_code'] }}">
<button type="submit" class="text-sm font-medium text-sky-600 hover:text-sky-700">Start</button>
</form>
@else
<span class="text-xs text-slate-400">Not started</span>
@endif
</div>
</li>
@endforeach
</ul>
</div>
@endif
@if (($pathwaySuggestions ?? collect())->isNotEmpty())
<ul class="mt-4 space-y-2 text-sm">
@foreach ($pathwaySuggestions as $suggestion)
<li class="flex flex-wrap items-center justify-between gap-2 rounded-xl border border-slate-100 bg-slate-50 px-3 py-2">
<div>
<span class="font-medium text-slate-900">{{ $suggestion['pathway_name'] }}</span>
<span class="text-xs text-slate-400">
· {{ $suggestion['match_reason'] }}
@if ($suggestion['already_active']) · already active @endif
</span>
</div>
@if (($canManagePathways ?? false) && ! $suggestion['already_active'] && ! $isCompleted)
<form method="POST" action="{{ route('care.pathways.store', $consultation->patient) }}">
@csrf
<input type="hidden" name="pathway_code" value="{{ $suggestion['pathway_code'] }}">
<input type="hidden" name="consultation_uuid" value="{{ $consultation->uuid }}">
<button type="submit" class="text-sm font-medium text-sky-600 hover:text-sky-700">Activate</button>
</form>
@endif
</li>
@endforeach
</ul>
@else
<p class="mt-3 text-sm text-slate-400">
@if ($consultation->diagnoses->isEmpty())
Save diagnoses to see pathway suggestions.
@else
No pathway matches for saved diagnoses.
@endif
</p>
@endif
</section>
<section class="mt-6 rounded-2xl border border-slate-200 bg-white p-6">
<div class="flex flex-wrap items-center justify-between gap-2">
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Clinical assessments</h2>
<a href="{{ route('care.assessments.index', $consultation->patient) }}" class="text-sm text-sky-600 hover:text-sky-700">Patient assessments</a>
</div>
@if (($consultationAssessments ?? collect())->isNotEmpty())
<ul class="mt-4 space-y-2 text-sm">
@foreach ($consultationAssessments as $item)
<li>
<a href="{{ route('care.assessments.show', $item) }}" class="text-sky-600 hover:text-sky-700">
{{ $item->template->name }}
</a>
· {{ ($assessmentStatuses[$item->status] ?? $item->status) }}
</li>
@endforeach
</ul>
@else
<p class="mt-4 text-sm text-slate-400">No assessments linked to this consultation yet.</p>
@endif
@if (($canCaptureAssessment ?? false) && ! $isCompleted && ($startableTemplates ?? collect())->isNotEmpty())
<form method="POST" action="{{ route('care.consultations.assessments.store', $consultation) }}" class="mt-4 flex flex-wrap items-end gap-3 border-t border-slate-100 pt-4">
@csrf
<div class="min-w-[12rem] flex-1">
<label class="block text-xs text-slate-500">Start other assessment</label>
<select name="template_code" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<option value="">Select template…</option>
@foreach ($startableTemplates as $template)
<option value="{{ $template->code }}">{{ $template->name }} ({{ $template->code }})</option>
@endforeach
</select>
</div>
<button type="submit" class="btn-primary">Start</button>
</form>
@endif
</section>
@endif
@if (! $isCompleted && ($canManage || $canVitals))
<form method="POST" action="{{ route('care.consultations.update', $consultation) }}" enctype="multipart/form-data" class="mt-6 space-y-6">
@csrf @method('PUT')
@if ($canVitals)
<section class="rounded-2xl border border-slate-200 bg-white p-6">
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Vital signs</h2>
@php $latestVitals = $consultation->vitalSigns->last(); @endphp
<div class="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div>
<label class="block text-xs text-slate-500">BP systolic</label>
<input type="number" name="vitals[bp_systolic]" value="{{ old('vitals.bp_systolic', $latestVitals?->bp_systolic) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-xs text-slate-500">BP diastolic</label>
<input type="number" name="vitals[bp_diastolic]" value="{{ old('vitals.bp_diastolic', $latestVitals?->bp_diastolic) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-xs text-slate-500">Pulse</label>
<input type="number" name="vitals[pulse]" value="{{ old('vitals.pulse', $latestVitals?->pulse) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-xs text-slate-500">Temperature (°C)</label>
<input type="number" step="0.1" name="vitals[temperature]" value="{{ old('vitals.temperature', $latestVitals?->temperature) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-xs text-slate-500">Weight (kg)</label>
<input type="number" step="0.01" name="vitals[weight_kg]" value="{{ old('vitals.weight_kg', $latestVitals?->weight_kg) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-xs text-slate-500">Height (cm)</label>
<input type="number" step="0.1" name="vitals[height_cm]" value="{{ old('vitals.height_cm', $latestVitals?->height_cm) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-xs text-slate-500">SpO₂ (%)</label>
<input type="number" name="vitals[spo2]" value="{{ old('vitals.spo2', $latestVitals?->spo2) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-xs text-slate-500">Resp. rate</label>
<input type="number" name="vitals[respiratory_rate]" value="{{ old('vitals.respiratory_rate', $latestVitals?->respiratory_rate) }}" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
</div>
</section>
@endif
@if ($canManage)
<section class="rounded-2xl border border-slate-200 bg-white p-6">
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Clinical notes (narrative)</h2>
@if ($canViewAssessments ?? false)
<p class="mt-1 text-xs text-slate-400">
Narrative source of truth for this encounter. Structured universal intake is a separate overlay above and is not auto-synced with these fields.
</p>
@endif
<div class="mt-4 space-y-4">
<div>
<label class="block text-sm font-medium text-slate-700">Symptoms</label>
<textarea name="symptoms" rows="3" class="mt-1 w-full rounded-lg border-slate-300 text-sm">{{ old('symptoms', $consultation->symptoms) }}</textarea>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Clinical notes</label>
<textarea name="clinical_notes" rows="5" class="mt-1 w-full rounded-lg border-slate-300 text-sm">{{ old('clinical_notes', $consultation->clinical_notes) }}</textarea>
</div>
<div>
<label class="block text-sm font-medium text-slate-700">Practitioner</label>
<select name="practitioner_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<option value=""></option>
@foreach ($practitioners as $practitioner)
<option value="{{ $practitioner->id }}" @selected(old('practitioner_id', $consultation->practitioner_id) == $practitioner->id)>{{ $practitioner->name }}</option>
@endforeach
</select>
</div>
</div>
</section>
<section class="rounded-2xl border border-slate-200 bg-white p-6" x-data="{ diagnoses: {{ json_encode(old('diagnoses', $consultation->diagnoses->map(fn ($d) => ['code' => $d->code, 'description' => $d->description, 'is_primary' => $d->is_primary, 'notes' => $d->notes])->values()->all() ?: [['code' => '', 'description' => '', 'is_primary' => true, 'notes' => '']])) }} }">
<div class="flex items-center justify-between">
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Diagnoses</h2>
<button type="button" @click="diagnoses.push({code:'',description:'',is_primary:false,notes:''})" class="text-sm text-sky-600 hover:text-sky-700">Add diagnosis</button>
</div>
<template x-for="(dx, index) in diagnoses" :key="index">
<div class="mt-4 grid gap-3 rounded-xl border border-slate-100 bg-slate-50 p-4 sm:grid-cols-2">
<div>
<label class="block text-xs text-slate-500">ICD code</label>
<input type="text" :name="'diagnoses['+index+'][code]'" x-model="dx.code" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div>
<label class="block text-xs text-slate-500">Description</label>
<input type="text" :name="'diagnoses['+index+'][description]'" x-model="dx.description" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
</div>
<div class="sm:col-span-2">
<label class="inline-flex items-center gap-2 text-sm">
<input type="checkbox" :name="'diagnoses['+index+'][is_primary]'" value="1" x-model="dx.is_primary" class="rounded border-slate-300">
Primary diagnosis
</label>
</div>
</div>
</template>
</section>
<section class="rounded-2xl border border-slate-200 bg-white p-6">
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Documents</h2>
<input type="file" name="documents[]" multiple accept=".pdf,.jpg,.jpeg,.png,.webp" class="mt-4 block w-full text-sm text-slate-500">
</section>
@endif
<div class="flex flex-wrap gap-3">
<button type="submit" class="btn-primary">Save</button>
@if ($canManage && ($canViewAssessments ?? false))
<button type="submit" name="suggest_pathways" value="1" class="rounded-lg border border-sky-200 bg-sky-50 px-4 py-2 text-sm font-medium text-sky-800 hover:bg-sky-100">
Save diagnoses &amp; suggest pathways
</button>
@endif
<a href="{{ route('care.queue.index') }}" class="rounded-lg border border-slate-200 px-4 py-2 text-sm text-slate-600">Back to queue</a>
</div>
</form>
@else
<div class="mt-6 space-y-6">
@if ($consultation->vitalSigns->isNotEmpty())
<section class="rounded-2xl border border-slate-200 bg-white p-6">
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Vital signs</h2>
@foreach ($consultation->vitalSigns as $vitals)
<dl class="mt-4 grid gap-3 text-sm sm:grid-cols-4">
<div><dt class="text-slate-500">BP</dt><dd class="font-medium">{{ $vitals->bp_systolic }}/{{ $vitals->bp_diastolic }}</dd></div>
<div><dt class="text-slate-500">Pulse</dt><dd class="font-medium">{{ $vitals->pulse ?? '—' }}</dd></div>
<div><dt class="text-slate-500">Temp</dt><dd class="font-medium">{{ $vitals->temperature ?? '—' }}°C</dd></div>
<div><dt class="text-slate-500">SpO₂</dt><dd class="font-medium">{{ $vitals->spo2 ? $vitals->spo2.'%' : '—' }}</dd></div>
</dl>
@endforeach
</section>
@endif
@if ($consultation->symptoms || $consultation->clinical_notes)
<section class="rounded-2xl border border-slate-200 bg-white p-6">
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Clinical notes (narrative)</h2>
@if ($consultation->symptoms)
<p class="mt-4 text-sm"><span class="font-medium text-slate-700">Symptoms:</span> {{ $consultation->symptoms }}</p>
@endif
@if ($consultation->clinical_notes)
<p class="mt-2 whitespace-pre-wrap text-sm text-slate-600">{{ $consultation->clinical_notes }}</p>
@endif
</section>
@endif
@if (($canViewAssessments ?? false) && ($universalAssessment ?? null)?->isCompleted())
<section class="rounded-2xl border border-sky-100 bg-white p-6">
<div class="flex items-center justify-between gap-2">
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Universal intake (structured)</h2>
<a href="{{ route('care.assessments.show', $universalAssessment) }}" class="text-sm text-sky-600 hover:text-sky-700">Open</a>
</div>
<dl class="mt-4 space-y-2 text-sm">
@foreach ($universalAssessment->answers->sortBy(fn ($a) => $a->question?->sort_order ?? 0) as $answer)
@if ($answer->question)
<div class="grid gap-1 sm:grid-cols-3">
<dt class="text-slate-500">{{ $answer->question->label }}</dt>
<dd class="sm:col-span-2 font-medium text-slate-800">
@php $v = $answer->authoritativeValue(); @endphp
@if (is_array($v))
{{ implode(', ', $v) }}
@elseif (is_bool($v))
{{ $v ? 'Yes' : 'No' }}
@else
{{ $v ?? '—' }}
@endif
</dd>
</div>
@endif
@endforeach
</dl>
</section>
@endif
@if ($consultation->diagnoses->isNotEmpty())
<section class="rounded-2xl border border-slate-200 bg-white p-6">
<h2 class="text-sm font-semibold uppercase tracking-wide text-slate-500">Diagnoses</h2>
<ul class="mt-4 space-y-2 text-sm">
@foreach ($consultation->diagnoses as $diagnosis)
<li>
<span class="font-medium">{{ $diagnosis->description }}</span>
@if ($diagnosis->code) <span class="text-slate-400">({{ $diagnosis->code }})</span> @endif
@if ($diagnosis->is_primary) <span class="text-sky-600">primary</span> @endif
</li>
@endforeach
</ul>
</section>
@endif
</div>
@endif
</x-app-layout>