Add clinical device registry, browser wedge scan, and local agent API.
Deploy Ladill Care / deploy (push) Successful in 1m39s
Deploy Ladill Care / deploy (push) Successful in 1m39s
Branch-scoped Care devices with hashed agent tokens, patient barcode lookup/labels, and provenance-aware vitals from a minimal device agent (Pro for agent hardware; wedge free). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -417,6 +417,7 @@
|
||||
<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>
|
||||
<div><dt class="text-slate-500">Source</dt><dd class="font-medium">{{ config('care.vital_sources.'.$vitals->source, $vitals->source ?? 'manual') }}@if ($vitals->device_id) · device #{{ $vitals->device_id }}@endif</dd></div>
|
||||
</dl>
|
||||
@endforeach
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<x-app-layout title="Add device">
|
||||
<div class="mx-auto max-w-lg space-y-4">
|
||||
<div>
|
||||
<a href="{{ route('care.devices.index') }}" class="text-sm text-slate-500 hover:text-slate-800">← Devices</a>
|
||||
<h1 class="mt-2 text-xl font-semibold text-slate-900">Register device</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">Inventory is branch-scoped. Agent devices get a token shown once after create.</p>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ route('care.devices.store') }}" class="space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
@csrf
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Name</label>
|
||||
<input type="text" name="name" value="{{ old('name') }}" required placeholder="Nurse station scanner"
|
||||
class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Type</label>
|
||||
<select name="type" id="device-type" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($deviceTypes as $value => $label)
|
||||
@php
|
||||
$needsPro = in_array($value, $agentTypes, true);
|
||||
@endphp
|
||||
<option value="{{ $value }}"
|
||||
data-mode="{{ $defaultModes[$value] ?? 'manual' }}"
|
||||
data-pro="{{ $needsPro ? '1' : '0' }}"
|
||||
@selected(old('type', 'barcode_scanner') === $value)
|
||||
@disabled($needsPro && ! $hasAgentDevices)>
|
||||
{{ $label }}@if ($needsPro && ! $hasAgentDevices) (Pro)@endif
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@if (! $hasAgentDevices)
|
||||
<p class="mt-1 text-xs text-amber-700">Clinical agent devices need Care Pro. USB barcode/QR scanners work on Free.</p>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Connection mode</label>
|
||||
<select name="connection_mode" id="connection-mode" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($connectionModes as $value => $label)
|
||||
<option value="{{ $value }}" @selected(old('connection_mode', 'browser_wedge') === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<p class="mt-1 text-xs text-slate-500" id="connection-hint">USB scanners act as a keyboard in the browser. Serial/BLE needs the local agent.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Branch</label>
|
||||
<select name="branch_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
<option value="">All branches</option>
|
||||
@foreach ($branches as $branch)
|
||||
<option value="{{ $branch->id }}" @selected(old('branch_id') == $branch->id)>{{ $branch->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary w-full">Register device</button>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
(() => {
|
||||
const typeEl = document.getElementById('device-type');
|
||||
const modeEl = document.getElementById('connection-mode');
|
||||
const hintEl = document.getElementById('connection-hint');
|
||||
const hints = {
|
||||
browser_wedge: 'Works in the browser today as a USB keyboard wedge. No local agent required.',
|
||||
agent: 'Needs the Care Device Agent on a local machine (serial / BLE / vendor SDK).',
|
||||
web_bluetooth: 'Experimental Web Bluetooth — prefer the local agent for clinical devices.',
|
||||
manual: 'Manual / inventory only — no automated capture path.',
|
||||
};
|
||||
function sync() {
|
||||
const opt = typeEl.selectedOptions[0];
|
||||
if (!opt) return;
|
||||
const mode = opt.dataset.mode || 'manual';
|
||||
modeEl.value = mode;
|
||||
hintEl.textContent = hints[mode] || hints.manual;
|
||||
}
|
||||
typeEl.addEventListener('change', sync);
|
||||
modeEl.addEventListener('change', () => {
|
||||
hintEl.textContent = hints[modeEl.value] || hints.manual;
|
||||
});
|
||||
sync();
|
||||
})();
|
||||
</script>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,89 @@
|
||||
<x-app-layout title="Edit device">
|
||||
<div class="mx-auto max-w-lg space-y-4">
|
||||
<div>
|
||||
<a href="{{ route('care.devices.index') }}" class="text-sm text-slate-500 hover:text-slate-800">← Devices</a>
|
||||
<h1 class="mt-2 text-xl font-semibold text-slate-900">{{ $device->name }}</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">{{ $connectionHint }}</p>
|
||||
</div>
|
||||
|
||||
@if ($plainToken)
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-950">
|
||||
<p class="font-medium">Device token (copy now — shown once)</p>
|
||||
<code class="mt-2 block break-all rounded-lg bg-white px-3 py-2 font-mono text-xs text-slate-800">{{ $plainToken }}</code>
|
||||
<p class="mt-2 text-xs text-amber-800">Send as <code class="font-mono">X-Care-Device-Token</code> or <code class="font-mono">Authorization: Bearer …</code> from the Care Device Agent.</p>
|
||||
</div>
|
||||
@elseif ($device->hasToken())
|
||||
<div class="rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-700">
|
||||
<p>A device token is active (hash stored). Regenerate to rotate; the previous token stops working immediately.</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form method="POST" action="{{ route('care.devices.update', $device) }}" class="space-y-4 rounded-2xl border border-slate-200 bg-white p-6">
|
||||
@csrf @method('PUT')
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Name</label>
|
||||
<input type="text" name="name" value="{{ old('name', $device->name) }}" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Type</label>
|
||||
<select name="type" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($deviceTypes as $value => $label)
|
||||
@php $needsPro = in_array($value, $agentTypes, true); @endphp
|
||||
<option value="{{ $value }}" @selected(old('type', $device->type) === $value)
|
||||
@disabled($needsPro && ! $hasAgentDevices && $device->type !== $value)>
|
||||
{{ $label }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Connection mode</label>
|
||||
<select name="connection_mode" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($connectionModes as $value => $label)
|
||||
<option value="{{ $value }}" @selected(old('connection_mode', $device->connection_mode) === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Status</label>
|
||||
<select name="status" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
@foreach ($deviceStatuses as $value => $label)
|
||||
<option value="{{ $value }}" @selected(old('status', $device->status) === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-700">Branch</label>
|
||||
<select name="branch_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
|
||||
<option value="">All branches</option>
|
||||
@foreach ($branches as $branch)
|
||||
<option value="{{ $branch->id }}" @selected(old('branch_id', $device->branch_id) == $branch->id)>{{ $branch->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<button type="submit" class="btn-primary">Save</button>
|
||||
<a href="{{ route('care.devices.index') }}" class="rounded-lg border border-slate-200 px-4 py-2 text-sm text-slate-600">Cancel</a>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<form method="POST" action="{{ route('care.devices.token.regenerate', $device) }}">
|
||||
@csrf
|
||||
<button type="submit" class="rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm text-slate-700 hover:bg-slate-50">
|
||||
{{ $device->hasToken() ? 'Regenerate token' : 'Issue agent token' }}
|
||||
</button>
|
||||
</form>
|
||||
@if ($device->hasToken())
|
||||
<form method="POST" action="{{ route('care.devices.token.revoke', $device) }}">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="rounded-lg border border-rose-200 bg-white px-4 py-2 text-sm text-rose-700 hover:bg-rose-50">Revoke token</button>
|
||||
</form>
|
||||
@endif
|
||||
<form method="POST" action="{{ route('care.devices.destroy', $device) }}" onsubmit="return confirm('Remove this device?')">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="rounded-lg border border-rose-200 bg-white px-4 py-2 text-sm text-rose-700 hover:bg-rose-50">Delete device</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,71 @@
|
||||
<x-app-layout title="Devices">
|
||||
<div class="space-y-6">
|
||||
<x-care.page-hero
|
||||
badge="Scanners · Vitals · Lab agents"
|
||||
title="Devices"
|
||||
description="Branch inventory for barcode scanners (browser wedge) and agent-connected clinical devices. USB scanners work in the browser; serial/BLE hardware needs the Care Device Agent."
|
||||
:stats="[
|
||||
['value' => number_format($heroStats['total']), 'label' => 'Devices'],
|
||||
['value' => number_format($heroStats['online']), 'label' => 'Online'],
|
||||
['value' => number_format($heroStats['agent']), 'label' => 'Agent-linked'],
|
||||
]">
|
||||
@if ($canManage)
|
||||
<x-slot name="actions">
|
||||
<a href="{{ route('care.devices.create') }}" class="btn-primary">Add device</a>
|
||||
</x-slot>
|
||||
@endif
|
||||
</x-care.page-hero>
|
||||
|
||||
<div class="rounded-2xl border border-sky-100 bg-sky-50 px-4 py-3 text-sm text-sky-900">
|
||||
<p class="font-medium">How devices connect</p>
|
||||
<ul class="mt-1 list-disc space-y-1 pl-5 text-sky-800/90">
|
||||
<li><strong>Barcode / QR scanners</strong> — USB keyboard wedge works in Patients and Lab today. Free on all plans.</li>
|
||||
<li><strong>Thermometers, BP, SpO₂, scales, analyzers</strong> — need a local Care Device Agent (Pro / Enterprise). See docs/devices.md.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
|
||||
<tr>
|
||||
<th class="px-4 py-3">Name</th>
|
||||
<th class="px-4 py-3">Type</th>
|
||||
<th class="px-4 py-3">Connection</th>
|
||||
<th class="px-4 py-3">Status</th>
|
||||
<th class="px-4 py-3">Branch</th>
|
||||
<th class="px-4 py-3">Last seen</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
@forelse ($devices as $device)
|
||||
<tr>
|
||||
<td class="px-4 py-3 font-medium text-slate-900">{{ $device->name }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ $deviceTypes[$device->type] ?? $device->type }}</td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ $connectionModes[$device->connection_mode] ?? $device->connection_mode }}</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="inline-flex rounded-full px-2 py-0.5 text-xs font-medium {{ $device->isOnline() ? 'bg-emerald-100 text-emerald-800' : 'bg-slate-100 text-slate-600' }}">
|
||||
{{ $device->isOnline() ? 'Online' : ucfirst($device->status) }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-slate-600">{{ $device->branch?->name ?? 'All branches' }}</td>
|
||||
<td class="px-4 py-3 text-slate-500">{{ $device->last_seen_at?->diffForHumans() ?? 'Never' }}</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
@if ($canManage)
|
||||
<a href="{{ route('care.devices.edit', $device) }}" class="text-sky-600 hover:text-sky-700">Edit</a>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="7" class="px-4 py-8 text-center text-slate-500">No devices registered yet.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
@if ($devices->hasPages())
|
||||
<div class="border-t border-slate-100 px-5 py-3">{{ $devices->links() }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -7,7 +7,17 @@
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@if ($canManage && $investigation->status === \App\Models\InvestigationRequest::STATUS_PENDING)
|
||||
<form method="POST" action="{{ route('care.lab.requests.collect-sample', $investigation) }}">@csrf<button class="btn-primary">Collect sample</button></form>
|
||||
<form method="POST" action="{{ route('care.lab.requests.collect-sample', $investigation) }}" class="flex flex-wrap items-end gap-2">
|
||||
@csrf
|
||||
<div>
|
||||
<label for="sample_barcode" class="block text-xs font-medium text-slate-600">Sample barcode</label>
|
||||
<input type="text" id="sample_barcode" name="sample_barcode" autocomplete="off"
|
||||
placeholder="Scan or type barcode…"
|
||||
class="mt-1 w-48 rounded-lg border-slate-300 font-mono text-sm"
|
||||
autofocus>
|
||||
</div>
|
||||
<button class="btn-primary">Collect sample</button>
|
||||
</form>
|
||||
@endif
|
||||
@if ($canManage && $investigation->status === \App\Models\InvestigationRequest::STATUS_SAMPLE_COLLECTED)
|
||||
<form method="POST" action="{{ route('care.lab.requests.start', $investigation) }}">@csrf<button class="btn-primary">Start processing</button></form>
|
||||
|
||||
@@ -45,6 +45,22 @@
|
||||
@endif
|
||||
</form>
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-4"
|
||||
x-data="patientScan({ lookupUrl: @js(route('care.patients.scan')) })"
|
||||
@keydown.window="onGlobalKeydown($event)">
|
||||
<label for="patient-barcode-scan" class="text-sm font-medium text-slate-700">Barcode / QR scan</label>
|
||||
<p class="mt-0.5 text-xs text-slate-400">USB scanners work as a keyboard wedge. Scan a patient ID label or type the number and press Enter.</p>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<input type="text" id="patient-barcode-scan" x-ref="scanInput" x-model="scanCode"
|
||||
@keydown.enter.prevent="submitScan()"
|
||||
placeholder="Scan patient ID…" autocomplete="off"
|
||||
class="min-w-[220px] flex-1 rounded-lg border-slate-300 font-mono text-sm">
|
||||
<button type="button" class="btn-primary" @click="submitScan()">Open patient</button>
|
||||
</div>
|
||||
<p x-show="scanError" x-text="scanError" class="mt-2 text-sm text-rose-600" x-cloak></p>
|
||||
<p x-show="scanOk" x-text="scanOk" class="mt-2 text-sm text-emerald-600" x-cloak></p>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="bg-slate-50 text-left text-xs uppercase text-slate-500">
|
||||
@@ -85,4 +101,64 @@
|
||||
|
||||
<div>{{ $patients->links() }}</div>
|
||||
</div>
|
||||
<script>
|
||||
function patientScan(config) {
|
||||
return {
|
||||
lookupUrl: config.lookupUrl,
|
||||
scanCode: '',
|
||||
scanError: '',
|
||||
scanOk: '',
|
||||
scanBuffer: '',
|
||||
scanTimer: null,
|
||||
submitScan() {
|
||||
const code = (this.scanCode || '').trim();
|
||||
this.scanError = '';
|
||||
this.scanOk = '';
|
||||
if (!code) return;
|
||||
fetch(this.lookupUrl + '?' + new URLSearchParams({ code }), {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
}).then(async (res) => {
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
this.scanError = data.message || 'No patient found for that barcode.';
|
||||
return;
|
||||
}
|
||||
this.scanOk = 'Opening ' + (data.patient?.name || 'patient') + '…';
|
||||
window.location.href = data.patient.url;
|
||||
}).catch(() => {
|
||||
this.scanError = 'Lookup failed. Try again.';
|
||||
});
|
||||
},
|
||||
onGlobalKeydown(e) {
|
||||
if (this.shouldIgnoreScan(e)) return;
|
||||
if (e.key === 'Enter') {
|
||||
const code = (this.scanBuffer || '').trim();
|
||||
this.scanBuffer = '';
|
||||
if (code.length >= 3) {
|
||||
e.preventDefault();
|
||||
this.scanCode = code;
|
||||
this.submitScan();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
this.scanBuffer += e.key;
|
||||
clearTimeout(this.scanTimer);
|
||||
this.scanTimer = setTimeout(() => { this.scanBuffer = ''; }, 120);
|
||||
}
|
||||
},
|
||||
shouldIgnoreScan(e) {
|
||||
const tag = (e.target?.tagName || '').toLowerCase();
|
||||
if (tag === 'textarea') return true;
|
||||
if (tag === 'input' && e.target?.id !== 'patient-barcode-scan') return true;
|
||||
if (e.target?.isContentEditable) return true;
|
||||
return false;
|
||||
},
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</x-app-layout>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Patient label · {{ $patient->patient_number }}</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
color: #0f172a;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 1rem;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
.toolbar a, .toolbar button {
|
||||
font-family: system-ui, sans-serif;
|
||||
font-size: 0.875rem;
|
||||
padding: 0.5rem 0.9rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid #cbd5e1;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
color: #334155;
|
||||
}
|
||||
.toolbar button.primary {
|
||||
background: #0ea5e9;
|
||||
border-color: #0284c7;
|
||||
color: #fff;
|
||||
}
|
||||
.sheet {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
.label {
|
||||
width: 90mm;
|
||||
min-height: 40mm;
|
||||
background: #fff;
|
||||
border: 1px solid #94a3b8;
|
||||
padding: 4mm 5mm;
|
||||
}
|
||||
.org { font-size: 9pt; color: #64748b; margin: 0 0 2mm; }
|
||||
.id { font-size: 16pt; font-weight: 700; letter-spacing: 0.02em; margin: 0; }
|
||||
.name { font-size: 12pt; margin: 2mm 0 0; font-family: system-ui, sans-serif; }
|
||||
.meta { font-size: 9pt; color: #475569; margin: 1.5mm 0 0; font-family: system-ui, sans-serif; }
|
||||
@media print {
|
||||
body { background: #fff; }
|
||||
.toolbar { display: none !important; }
|
||||
.sheet { padding: 0; }
|
||||
.label { border: none; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="toolbar">
|
||||
<button type="button" class="primary" onclick="window.print()">Print label</button>
|
||||
<a href="{{ route('care.patients.show', $patient) }}">Back to patient</a>
|
||||
</div>
|
||||
<div class="sheet">
|
||||
<div class="label">
|
||||
<p class="org">{{ $organization->name }}</p>
|
||||
<p class="id">{{ $patient->patient_number }}</p>
|
||||
<p class="name">{{ $patient->fullName() }}</p>
|
||||
<p class="meta">
|
||||
{{ $genders[$patient->gender] ?? '' }}
|
||||
@if ($patient->date_of_birth)
|
||||
· {{ $patient->date_of_birth->format('d M Y') }}
|
||||
@endif
|
||||
@if ($patient->branch)
|
||||
· {{ $patient->branch->name }}
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -14,6 +14,7 @@
|
||||
</div>
|
||||
@if ($canManage)
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ route('care.patients.label', $patient) }}" target="_blank" class="rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Print ID label</a>
|
||||
<a href="{{ route('care.patients.edit', $patient) }}" class="btn-primary">Edit</a>
|
||||
<x-confirm-dialog
|
||||
:name="'archive-patient-'.$patient->id"
|
||||
@@ -28,6 +29,10 @@
|
||||
</x-slot:trigger>
|
||||
</x-confirm-dialog>
|
||||
</div>
|
||||
@else
|
||||
<div class="flex gap-2">
|
||||
<a href="{{ route('care.patients.label', $patient) }}" target="_blank" class="rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Print ID label</a>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@
|
||||
</div>
|
||||
</x-settings.card>
|
||||
|
||||
@if ($canViewBranches || $canViewTeam)
|
||||
<x-settings.card title="Branches & team" description="Locations and staff access for this facility. Multi-branch management requires Care Pro.">
|
||||
@if ($canViewBranches || $canViewTeam || $canViewDevices)
|
||||
<x-settings.card title="Branches & team" description="Locations, staff access, and clinical hardware for this facility. Multi-branch management requires Care Pro.">
|
||||
<ul class="space-y-3">
|
||||
@if ($canViewBranches)
|
||||
<li>
|
||||
@@ -84,6 +84,23 @@
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
@if ($canViewDevices)
|
||||
<li>
|
||||
<a href="{{ route('care.devices.index') }}"
|
||||
class="flex items-center justify-between rounded-xl border border-slate-100 bg-slate-50 px-4 py-3 text-sm text-slate-700 hover:text-indigo-700">
|
||||
<span>
|
||||
Devices
|
||||
@if (! $hasClinicalDevicesFeature)
|
||||
<span class="ml-2 rounded-full bg-amber-50 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-amber-700">Agent · Pro</span>
|
||||
@endif
|
||||
<span class="mt-0.5 block text-xs text-slate-500">
|
||||
Barcode scanners (browser) · agent-connected vitals / lab hardware
|
||||
</span>
|
||||
</span>
|
||||
<span class="text-slate-400">→</span>
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</x-settings.card>
|
||||
@endif
|
||||
|
||||
@@ -97,7 +97,8 @@
|
||||
$settingsActive = (request()->routeIs('care.settings*') && ! request()->routeIs('care.pro.*'))
|
||||
|| request()->routeIs('care.branches.*')
|
||||
|| request()->routeIs('care.members.*')
|
||||
|| request()->routeIs('care.integrations*');
|
||||
|| request()->routeIs('care.integrations*')
|
||||
|| request()->routeIs('care.devices.*');
|
||||
@endphp
|
||||
|
||||
<nav class="flex-1 space-y-0.5 overflow-y-auto px-3 py-4">
|
||||
|
||||
Reference in New Issue
Block a user