Add visit placement on care units and beds.
Deploy Ladill Care / deploy (push) Successful in 38s

Patients can be admitted, transferred, and discharged from unit/bed stays with occupancy sync, so MAR and ward boards have inpatient context.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-20 10:10:37 +00:00
co-authored by Cursor
parent 3735be3425
commit 45a1a95142
16 changed files with 1036 additions and 22 deletions
@@ -18,11 +18,106 @@
</p>
</div>
<div class="flex flex-wrap gap-2">
<a href="{{ route('care.care-units.edit', $unit) }}" class="btn-secondary">Edit</a>
<a href="{{ route('care.staff-assignments.create', ['care_unit_id' => $unit->id]) }}" class="btn-primary">Assign staff</a>
@php
$member = auth()->user()
? app(\App\Services\Care\OrganizationResolver::class)->memberFor(auth()->user())
: null;
$permissions = app(\App\Services\Care\CarePermissions::class);
$canManageUnit = $permissions->can($member, 'admin.departments.manage');
@endphp
@if ($canManageUnit)
<a href="{{ route('care.care-units.edit', $unit) }}" class="btn-secondary">Edit unit</a>
<a href="{{ route('care.staff-assignments.create', ['care_unit_id' => $unit->id]) }}" class="btn-secondary">Assign staff</a>
@endif
</div>
</div>
{{-- Current patients --}}
<div class="rounded-2xl border border-slate-200 bg-white p-6">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 class="text-base font-semibold text-slate-900">Patients on this unit</h2>
<p class="text-sm text-slate-500">Active visit placements foundation for MAR and ward boards.</p>
</div>
<p class="text-sm font-medium text-slate-700">{{ $placedVisits->count() }} placed</p>
</div>
<div class="mt-4 overflow-x-auto">
<table class="min-w-full text-sm">
<thead class="text-left text-xs uppercase text-slate-500">
<tr>
<th class="pb-2 pr-4">Patient</th>
<th class="pb-2 pr-4">Bed</th>
<th class="pb-2 pr-4">Placed</th>
<th class="pb-2"></th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
@forelse ($placedVisits as $visit)
<tr>
<td class="py-3 pr-4 font-medium">
{{ $visit->patient?->fullName() ?? '—' }}
@if ($visit->patient?->patient_number)
<span class="text-xs text-slate-400">{{ $visit->patient->patient_number }}</span>
@endif
</td>
<td class="py-3 pr-4">{{ $visit->bed?->label ?? '—' }}</td>
<td class="py-3 pr-4 whitespace-nowrap">{{ $visit->placed_at?->format('d M Y H:i') ?? '—' }}</td>
<td class="py-3 text-right whitespace-nowrap">
@if ($canPlace)
<form method="POST" action="{{ route('care.visits.placement.destroy', $visit) }}" class="inline" onsubmit="return confirm('Discharge placement and free the bed?')">
@csrf @method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-800">Discharge bed</button>
</form>
@endif
</td>
</tr>
@empty
<tr><td colspan="4" class="py-4 text-slate-500">No patients placed on this unit.</td></tr>
@endforelse
</tbody>
</table>
</div>
@if ($canPlace && $unit->kind !== 'float_pool')
<form method="POST" action="{{ route('care.care-units.placements.store', $unit) }}" class="mt-6 grid gap-3 border-t border-slate-100 pt-5 sm:grid-cols-4">
@csrf
<div class="sm:col-span-2">
<label class="block text-xs font-medium uppercase text-slate-500">Open visit</label>
<select name="visit_id" required class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<option value="">Select patient visit…</option>
@foreach ($placeableVisits as $visit)
<option value="{{ $visit->id }}">
{{ $visit->patient?->fullName() ?? 'Patient' }}
· {{ $visit->checked_in_at?->format('d M H:i') ?? $visit->status }}
@if ($visit->careUnit) (now: {{ $visit->careUnit->name ?? 'placed' }}) @endif
</option>
@endforeach
</select>
@error('visit_id')<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
@error('care_unit_id')<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
@error('bed_id')<p class="mt-1 text-xs text-red-600">{{ $message }}</p>@enderror
</div>
@if ($unit->supportsBeds())
<div>
<label class="block text-xs font-medium uppercase text-slate-500">Bed</label>
<select name="bed_id" class="mt-1 w-full rounded-lg border-slate-300 text-sm">
<option value="">No bed yet</option>
@foreach ($availableBeds as $bed)
<option value="{{ $bed->id }}">{{ $bed->label }}@if ($bed->room) ({{ $bed->room }}) @endif</option>
@endforeach
</select>
</div>
@else
<div></div>
@endif
<div class="flex items-end">
<button type="submit" class="btn-primary w-full">Place on unit</button>
</div>
</form>
@endif
</div>
@if ($unit->supportsBeds())
<div class="rounded-2xl border border-slate-200 bg-white p-6">
<div class="flex items-center justify-between gap-3">
@@ -32,17 +127,19 @@
</div>
</div>
<form method="POST" action="{{ route('care.care-units.beds.store', $unit) }}" class="mt-4 grid gap-3 sm:grid-cols-4">
@csrf
<input type="text" name="label" required placeholder="Bed label" class="rounded-lg border-slate-300 text-sm" value="{{ old('label') }}">
<input type="text" name="room" placeholder="Room" class="rounded-lg border-slate-300 text-sm" value="{{ old('room') }}">
<select name="status" class="rounded-lg border-slate-300 text-sm">
@foreach ($bedStatuses as $value => $label)
<option value="{{ $value }}" @selected(old('status', 'available') === $value)>{{ $label }}</option>
@endforeach
</select>
<button type="submit" class="btn-primary">Add bed</button>
</form>
@if ($canManageUnit ?? false)
<form method="POST" action="{{ route('care.care-units.beds.store', $unit) }}" class="mt-4 grid gap-3 sm:grid-cols-4">
@csrf
<input type="text" name="label" required placeholder="Bed label" class="rounded-lg border-slate-300 text-sm" value="{{ old('label') }}">
<input type="text" name="room" placeholder="Room" class="rounded-lg border-slate-300 text-sm" value="{{ old('room') }}">
<select name="status" class="rounded-lg border-slate-300 text-sm">
@foreach ($bedStatuses as $value => $label)
<option value="{{ $value }}" @selected(old('status', 'available') === $value)>{{ $label }}</option>
@endforeach
</select>
<button type="submit" class="btn-primary">Add bed</button>
</form>
@endif
<div class="mt-4 overflow-x-auto">
<table class="min-w-full text-sm">
@@ -51,24 +148,31 @@
<th class="pb-2 pr-4">Label</th>
<th class="pb-2 pr-4">Room</th>
<th class="pb-2 pr-4">Status</th>
<th class="pb-2 pr-4">Occupant</th>
<th class="pb-2"></th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
@forelse ($unit->beds as $bed)
@php
$occupant = $placedVisits->firstWhere('bed_id', $bed->id);
@endphp
<tr class="{{ $bed->is_active ? '' : 'opacity-50' }}">
<td class="py-3 pr-4 font-medium">{{ $bed->label }}</td>
<td class="py-3 pr-4">{{ $bed->room ?: '—' }}</td>
<td class="py-3 pr-4">{{ $bedStatuses[$bed->status] ?? $bed->status }}</td>
<td class="py-3 pr-4">{{ $occupant?->patient?->fullName() ?? '—' }}</td>
<td class="py-3 text-right">
<form method="POST" action="{{ route('care.care-units.beds.destroy', [$unit, $bed]) }}" class="inline" onsubmit="return confirm('Remove this bed?')">
@csrf @method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-800">Remove</button>
</form>
@if ($canManageUnit ?? false)
<form method="POST" action="{{ route('care.care-units.beds.destroy', [$unit, $bed]) }}" class="inline" onsubmit="return confirm('Remove this bed?')">
@csrf @method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-800">Remove</button>
</form>
@endif
</td>
</tr>
@empty
<tr><td colspan="4" class="py-4 text-slate-500">No beds yet.</td></tr>
<tr><td colspan="5" class="py-4 text-slate-500">No beds yet.</td></tr>
@endforelse
</tbody>
</table>
@@ -77,6 +181,9 @@
@else
<div class="rounded-2xl border border-dashed border-slate-200 bg-slate-50 px-6 py-5 text-sm text-slate-600">
This unit kind does not use beds. Outpatient clinics, service units, and float pools assign staff without bed inventory.
@if ($unit->kind === 'float_pool')
Patients cannot be placed on a float pool.
@endif
</div>
@endif
</div>
@@ -93,6 +93,9 @@
{{ $visit->checked_in_at?->format('d M Y H:i') ?? '—' }}
· {{ config('care.visit_statuses')[$visit->status] ?? $visit->status }}
@if ($visit->branch) · {{ $visit->branch->name }} @endif
@if ($visit->careUnit)
· {{ $visit->careUnit->name }}@if ($visit->bed) / {{ $visit->bed->label }}@endif
@endif
</p>
@empty
<p class="mt-1 text-slate-400">No visits yet</p>
@@ -114,6 +114,8 @@
if ($permissions->can($member, 'admin.departments.view')) {
$adminNav[] = ['name' => 'Departments', 'route' => route('care.departments.index'), 'active' => request()->routeIs('care.departments.*'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25A2.25 2.25 0 0 1 13.5 18v-2.25Z" />'];
}
if ($permissions->can($member, 'admin.departments.view') || $permissions->can($member, 'inpatient.placement.view')) {
$adminNav[] = ['name' => 'Care units', 'route' => route('care.care-units.index'), 'active' => request()->routeIs('care.care-units.*'),
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 21h19.5m-18-18v18m10.5-18v18m6-13.5V21M6.75 6.75h.75m-.75 3h.75m-.75 3h.75m3-6h.75m-.75 3h.75m-.75 3h.75M6.75 21v-3.375c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21M3 3h12m-.75 4.5H21m-3.75 3h.008v.008h-.008v-.008Zm0 3h.008v.008h-.008v-.008Zm0 3h.008v.008h-.008v-.008Z" />'];
}