Add My ward nurse station for bedside clinical work.
Deploy Ladill Care / deploy (push) Successful in 39s
Deploy Ladill Care / deploy (push) Successful in 39s
Floor nurses keep Nursing Services admin-only, but now get a My ward home keyed off today's roster or unit placement with patient lists and direct links to MAR, vitals/assessments, notes, and handovers. Care unit show and board crumbs open for station users so clinical pages are reachable without department admin rights. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -155,7 +155,8 @@ class CareUnitController extends Controller
|
||||
$member = $this->member($request);
|
||||
abort_unless(
|
||||
$permissions->can($member, 'admin.departments.view')
|
||||
|| $permissions->can($member, 'inpatient.placement.view'),
|
||||
|| $permissions->can($member, 'inpatient.placement.view')
|
||||
|| $permissions->can($member, 'nursing.station.view'),
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\Care\CarePermissions;
|
||||
use App\Services\Care\NurseStationService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class NurseStationController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function index(Request $request, NurseStationService $station): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'nursing.station.view');
|
||||
$member = $this->member($request);
|
||||
abort_unless($member, 403);
|
||||
|
||||
$organization = $this->organization($request);
|
||||
$unitId = $request->filled('unit') ? (int) $request->input('unit') : null;
|
||||
$board = $station->board($organization, $member, $this->ownerRef($request), $unitId);
|
||||
|
||||
$permissions = app(CarePermissions::class);
|
||||
|
||||
return view('care.nursing.station', [
|
||||
'units' => $board['units'],
|
||||
'patientsByUnit' => $board['patients_by_unit'],
|
||||
'dutyByUnit' => $board['duty_by_unit'],
|
||||
'placementByUnit' => $board['placement_by_unit'],
|
||||
'selectedUnit' => $board['selected_unit'],
|
||||
'canMar' => $permissions->can($member, 'mar.view'),
|
||||
'canAdminister' => $permissions->can($member, 'mar.administer'),
|
||||
'canNotes' => $permissions->can($member, 'nursing.notes.view'),
|
||||
'canHandover' => $permissions->can($member, 'nursing.handover.view'),
|
||||
'canAssess' => $permissions->can($member, 'nursing.assessments.view'),
|
||||
'canVitals' => $permissions->can($member, 'vitals.manage'),
|
||||
'canServicesHub' => $permissions->can($member, 'nursing.services.view'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -576,8 +576,11 @@ class CarePermissions
|
||||
}
|
||||
}
|
||||
|
||||
return $this->withNursingOpsAbilities(
|
||||
$this->withDerivedAbilities(array_keys($merged)),
|
||||
return $this->withNursingStationAbility(
|
||||
$this->withNursingOpsAbilities(
|
||||
$this->withDerivedAbilities(array_keys($merged)),
|
||||
$role,
|
||||
),
|
||||
$role,
|
||||
);
|
||||
}
|
||||
@@ -653,6 +656,42 @@ class CarePermissions
|
||||
return array_values(array_unique($abilities));
|
||||
}
|
||||
|
||||
/**
|
||||
* Bedside nurse station (My ward) — floor + ward nursing roles with clinical vitals.
|
||||
* Not granted to GPs / ambulance solely because they also chart vitals.
|
||||
*
|
||||
* @param list<string> $abilities
|
||||
* @return list<string>
|
||||
*/
|
||||
protected function withNursingStationAbility(array $abilities, ?string $role): array
|
||||
{
|
||||
if (in_array('*', $abilities, true)) {
|
||||
return $abilities;
|
||||
}
|
||||
|
||||
$nursingRoles = [
|
||||
'nurse',
|
||||
'ed_nurse',
|
||||
'theatre_nurse',
|
||||
'labour_ward_nurse',
|
||||
'fertility_nurse',
|
||||
'dialysis_nurse',
|
||||
'midwife',
|
||||
];
|
||||
|
||||
if ($role === null || ! in_array($role, $nursingRoles, true)) {
|
||||
return $abilities;
|
||||
}
|
||||
|
||||
if (! in_array('vitals.manage', $abilities, true)) {
|
||||
return $abilities;
|
||||
}
|
||||
|
||||
$abilities[] = 'nursing.station.view';
|
||||
|
||||
return array_values(array_unique($abilities));
|
||||
}
|
||||
|
||||
public function isAdmin(?Member $member): bool
|
||||
{
|
||||
return $member !== null && in_array($member->role, ['super_admin', 'hospital_admin'], true);
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Care;
|
||||
|
||||
use App\Models\CareUnit;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\RosterEntry;
|
||||
use App\Models\StaffAssignment;
|
||||
use App\Models\Visit;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Bedside nurse workplace: units from today's duty + home placements, with placed patients.
|
||||
*/
|
||||
class NurseStationService
|
||||
{
|
||||
/**
|
||||
* @return array{
|
||||
* units: Collection<int, CareUnit>,
|
||||
* patients_by_unit: array<int, Collection<int, Visit>>,
|
||||
* duty_by_unit: array<int, Collection<int, RosterEntry>>,
|
||||
* placement_by_unit: array<int, StaffAssignment>,
|
||||
* selected_unit: ?CareUnit,
|
||||
* }
|
||||
*/
|
||||
public function board(
|
||||
Organization $organization,
|
||||
Member $member,
|
||||
string $ownerRef,
|
||||
?int $unitId = null,
|
||||
): array {
|
||||
$dutyToday = RosterEntry::owned($ownerRef)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('member_id', $member->id)
|
||||
->whereDate('duty_date', now()->toDateString())
|
||||
->where('status', '!=', RosterEntry::STATUS_CANCELLED)
|
||||
->with(['shift', 'careUnit.department.branch'])
|
||||
->orderBy('shift_id')
|
||||
->get();
|
||||
|
||||
$placements = StaffAssignment::owned($ownerRef)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('member_id', $member->id)
|
||||
->where('status', 'active')
|
||||
->unitPlacements()
|
||||
->effectiveOn(now())
|
||||
->whereNotNull('care_unit_id')
|
||||
->with(['careUnit.department.branch'])
|
||||
->get();
|
||||
|
||||
$unitIds = $dutyToday->pluck('care_unit_id')
|
||||
->merge($placements->pluck('care_unit_id'))
|
||||
->filter()
|
||||
->map(fn ($id) => (int) $id)
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
$units = CareUnit::owned($ownerRef)
|
||||
->where('organization_id', $organization->id)
|
||||
->whereIn('id', $unitIds->all() ?: [0])
|
||||
->where('is_active', true)
|
||||
->with('department.branch')
|
||||
->orderBy('name')
|
||||
->get()
|
||||
->keyBy('id');
|
||||
|
||||
// Preserve duty-first ordering, then remaining placements.
|
||||
$ordered = collect();
|
||||
foreach ($dutyToday as $entry) {
|
||||
$id = (int) $entry->care_unit_id;
|
||||
if ($units->has($id) && ! $ordered->has($id)) {
|
||||
$ordered->put($id, $units->get($id));
|
||||
}
|
||||
}
|
||||
foreach ($placements as $assignment) {
|
||||
$id = (int) $assignment->care_unit_id;
|
||||
if ($units->has($id) && ! $ordered->has($id)) {
|
||||
$ordered->put($id, $units->get($id));
|
||||
}
|
||||
}
|
||||
|
||||
$selected = null;
|
||||
if ($unitId && $ordered->has($unitId)) {
|
||||
$selected = $ordered->get($unitId);
|
||||
} elseif ($ordered->isNotEmpty()) {
|
||||
$selected = $ordered->first();
|
||||
}
|
||||
|
||||
$patientsByUnit = [];
|
||||
foreach ($ordered as $id => $unit) {
|
||||
$patientsByUnit[$id] = Visit::owned($ownerRef)
|
||||
->where('organization_id', $organization->id)
|
||||
->where('care_unit_id', $id)
|
||||
->whereIn('status', [Visit::STATUS_OPEN, Visit::STATUS_IN_PROGRESS])
|
||||
->with(['patient', 'bed'])
|
||||
->orderBy('placed_at')
|
||||
->get();
|
||||
}
|
||||
|
||||
$dutyByUnit = [];
|
||||
foreach ($dutyToday as $entry) {
|
||||
$id = (int) $entry->care_unit_id;
|
||||
$dutyByUnit[$id] ??= collect();
|
||||
$dutyByUnit[$id]->push($entry);
|
||||
}
|
||||
|
||||
$placementByUnit = [];
|
||||
foreach ($placements as $assignment) {
|
||||
$placementByUnit[(int) $assignment->care_unit_id] = $assignment;
|
||||
}
|
||||
|
||||
return [
|
||||
'units' => $ordered->values(),
|
||||
'patients_by_unit' => $patientsByUnit,
|
||||
'duty_by_unit' => $dutyByUnit,
|
||||
'placement_by_unit' => $placementByUnit,
|
||||
'selected_unit' => $selected,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,29 @@
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
@php
|
||||
$member = auth()->user()
|
||||
? app(\App\Services\Care\OrganizationResolver::class)->memberFor(auth()->user())
|
||||
: null;
|
||||
$permissions = app(\App\Services\Care\CarePermissions::class);
|
||||
$canAdminUnits = $permissions->can($member, 'admin.departments.view');
|
||||
$canStation = $permissions->can($member, 'nursing.station.view');
|
||||
$canManageUnit = $permissions->can($member, 'admin.departments.manage');
|
||||
$canMar = $permissions->can($member, 'mar.view');
|
||||
$canNotes = $permissions->can($member, 'nursing.notes.view');
|
||||
$canHandover = $permissions->can($member, 'nursing.handover.view');
|
||||
$canAssess = $permissions->can($member, 'nursing.assessments.view');
|
||||
$canPerf = $permissions->can($member, 'nursing.performance.view');
|
||||
$canRoster = $permissions->can($member, 'nursing.roster.view');
|
||||
@endphp
|
||||
<p class="text-sm text-slate-500">
|
||||
<a href="{{ route('care.care-units.index') }}" class="text-indigo-600 hover:text-indigo-800">Care units</a>
|
||||
@if ($canAdminUnits)
|
||||
<a href="{{ route('care.care-units.index') }}" class="text-indigo-600 hover:text-indigo-800">Care units</a>
|
||||
@elseif ($canStation)
|
||||
<a href="{{ route('care.nursing.station', ['unit' => $unit->id]) }}" class="text-indigo-600 hover:text-indigo-800">My ward</a>
|
||||
@else
|
||||
Care units
|
||||
@endif
|
||||
<span class="text-slate-300">/</span>
|
||||
{{ $unit->department?->name }}
|
||||
</p>
|
||||
@@ -18,19 +39,6 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@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');
|
||||
$canMar = $permissions->can($member, 'mar.view');
|
||||
$canNotes = $permissions->can($member, 'nursing.notes.view');
|
||||
$canHandover = $permissions->can($member, 'nursing.handover.view');
|
||||
$canAssess = $permissions->can($member, 'nursing.assessments.view');
|
||||
$canPerf = $permissions->can($member, 'nursing.performance.view');
|
||||
$canRoster = $permissions->can($member, 'nursing.roster.view');
|
||||
@endphp
|
||||
@if ($canMar)
|
||||
<a href="{{ route('care.care-units.mar', $unit) }}" class="btn-primary">MAR board</a>
|
||||
@endif
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-sm text-slate-500">
|
||||
<a href="{{ route('care.care-units.show', $unit) }}" class="text-indigo-600 hover:text-indigo-800">{{ $unit->name }}</a>
|
||||
@include('care.nursing.partials.unit-crumb', ['unit' => $unit])
|
||||
<span class="text-slate-300">/</span>
|
||||
Nursing assessments
|
||||
</p>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-sm text-slate-500">
|
||||
<a href="{{ route('care.care-units.show', $unit) }}" class="text-indigo-600 hover:text-indigo-800">{{ $unit->name }}</a>
|
||||
@include('care.nursing.partials.unit-crumb', ['unit' => $unit])
|
||||
<span class="text-slate-300">/</span>
|
||||
Ward handovers
|
||||
</p>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-sm text-slate-500">
|
||||
<a href="{{ route('care.care-units.show', $unit) }}" class="text-indigo-600 hover:text-indigo-800">{{ $unit->name }}</a>
|
||||
@include('care.nursing.partials.unit-crumb', ['unit' => $unit])
|
||||
<span class="text-slate-300">/</span>
|
||||
Medication round
|
||||
</p>
|
||||
|
||||
@@ -31,9 +31,17 @@
|
||||
@endif
|
||||
</p>
|
||||
</div>
|
||||
<span class="rounded-full bg-teal-100 px-2.5 py-0.5 text-xs font-medium text-teal-800">
|
||||
{{ config('care.roster_entry_statuses.'.$entry->status, $entry->status) }}
|
||||
</span>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
@if ($entry->careUnit && app(\App\Services\Care\CarePermissions::class)->can(
|
||||
auth()->user() ? app(\App\Services\Care\OrganizationResolver::class)->memberFor(auth()->user()) : null,
|
||||
'nursing.station.view'
|
||||
))
|
||||
<a href="{{ route('care.nursing.station', ['unit' => $entry->care_unit_id]) }}" class="rounded-lg bg-slate-900 px-2.5 py-1 text-xs font-semibold text-white hover:bg-slate-800">Open ward</a>
|
||||
@endif
|
||||
<span class="rounded-full bg-teal-100 px-2.5 py-0.5 text-xs font-medium text-teal-800">
|
||||
{{ config('care.roster_entry_statuses.'.$entry->status, $entry->status) }}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<p class="text-sm text-slate-500">
|
||||
<a href="{{ route('care.care-units.show', $unit) }}" class="text-indigo-600 hover:text-indigo-800">{{ $unit->name }}</a>
|
||||
@include('care.nursing.partials.unit-crumb', ['unit' => $unit])
|
||||
<span class="text-slate-300">/</span>
|
||||
Nursing notes
|
||||
</p>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
@php
|
||||
$crumbMember = auth()->user()
|
||||
? app(\App\Services\Care\OrganizationResolver::class)->memberFor(auth()->user())
|
||||
: null;
|
||||
$permissions = app(\App\Services\Care\CarePermissions::class);
|
||||
$crumbToStation = $permissions->can($crumbMember, 'nursing.station.view')
|
||||
&& ! $permissions->can($crumbMember, 'admin.departments.view');
|
||||
@endphp
|
||||
<a href="{{ $crumbToStation ? route('care.nursing.station', ['unit' => $unit->id]) : route('care.care-units.show', $unit) }}" class="text-indigo-600 hover:text-indigo-800">{{ $unit->name }}</a>
|
||||
@@ -0,0 +1,153 @@
|
||||
<x-app-layout title="My ward">
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-slate-900">My ward</h1>
|
||||
<p class="mt-1 text-sm text-slate-500">
|
||||
Patients on units you’re rostered or placed on today — chart meds, vitals, notes, and handovers.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="{{ route('care.my-shifts') }}" class="btn-secondary">My shifts</a>
|
||||
@if ($canServicesHub)
|
||||
<a href="{{ route('care.nursing.services') }}" class="btn-secondary">Nursing Services</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($units->isEmpty())
|
||||
<div class="rounded-2xl border border-dashed border-slate-200 bg-white px-6 py-12 text-center">
|
||||
<p class="text-base font-medium text-slate-900">No unit assigned for today</p>
|
||||
<p class="mt-2 text-sm text-slate-500">
|
||||
Ask a charge nurse or administrator to put you on the duty roster or a unit placement.
|
||||
You can still check
|
||||
<a href="{{ route('care.my-shifts') }}" class="font-medium text-indigo-600 hover:text-indigo-800">My shifts</a>.
|
||||
</p>
|
||||
</div>
|
||||
@else
|
||||
@if ($units->count() > 1)
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@foreach ($units as $unit)
|
||||
<a
|
||||
href="{{ route('care.nursing.station', ['unit' => $unit->id]) }}"
|
||||
@class([
|
||||
'rounded-lg px-3 py-1.5 text-sm font-medium transition',
|
||||
'bg-slate-900 text-white' => $selectedUnit && (int) $selectedUnit->id === (int) $unit->id,
|
||||
'border border-slate-200 bg-white text-slate-700 hover:bg-slate-50' => ! $selectedUnit || (int) $selectedUnit->id !== (int) $unit->id,
|
||||
])
|
||||
>
|
||||
{{ $unit->name }}
|
||||
<span class="ml-1 tabular-nums opacity-70">{{ ($patientsByUnit[$unit->id] ?? collect())->count() }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@php
|
||||
$unit = $selectedUnit;
|
||||
$patients = $patientsByUnit[$unit->id] ?? collect();
|
||||
$duty = $dutyByUnit[$unit->id] ?? collect();
|
||||
$placement = $placementByUnit[$unit->id] ?? null;
|
||||
@endphp
|
||||
|
||||
<div class="rounded-2xl border border-slate-200 bg-white p-5">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-slate-900">{{ $unit->name }}</h2>
|
||||
<p class="mt-0.5 text-sm text-slate-500">
|
||||
{{ $unit->department?->name }}
|
||||
@if ($unit->department?->branch)
|
||||
· {{ $unit->department->branch->name }}
|
||||
@endif
|
||||
</p>
|
||||
<div class="mt-2 flex flex-wrap gap-2 text-xs text-slate-600">
|
||||
@foreach ($duty as $entry)
|
||||
<span class="inline-flex items-center gap-1.5 rounded-md bg-teal-50 px-2 py-1 font-medium text-teal-800">
|
||||
<span class="h-1.5 w-1.5 rounded-full" style="background: {{ $entry->shift?->color ?? '#14b8a6' }}"></span>
|
||||
On duty · {{ $entry->shift?->label ?? 'Shift' }}
|
||||
@if ($entry->shift)
|
||||
({{ $entry->shift->timeLabel() }})
|
||||
@endif
|
||||
</span>
|
||||
@endforeach
|
||||
@if ($placement && $duty->isEmpty())
|
||||
<span class="inline-flex rounded-md bg-slate-100 px-2 py-1 font-medium text-slate-700">
|
||||
Unit placement · {{ str_replace('_', ' ', $placement->kind) }}
|
||||
</span>
|
||||
@elseif ($placement && $duty->isNotEmpty())
|
||||
<span class="inline-flex rounded-md bg-slate-100 px-2 py-1 text-slate-600">
|
||||
Also placed · {{ str_replace('_', ' ', $placement->kind) }}
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@if ($canMar)
|
||||
<a href="{{ route('care.care-units.mar', $unit) }}" class="btn-primary">MAR round</a>
|
||||
@endif
|
||||
@if ($canAssess || $canVitals)
|
||||
<a href="{{ route('care.care-units.assessments', $unit) }}" class="btn-secondary">Vitals & assess</a>
|
||||
@endif
|
||||
@if ($canNotes)
|
||||
<a href="{{ route('care.care-units.notes', $unit) }}" class="btn-secondary">Notes</a>
|
||||
@endif
|
||||
@if ($canHandover)
|
||||
<a href="{{ route('care.care-units.handovers', $unit) }}" class="btn-secondary">Handover</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 overflow-x-auto">
|
||||
<table class="min-w-full text-sm">
|
||||
<thead class="border-b border-slate-100 text-left text-xs uppercase tracking-wide 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 text-right">Chart</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-50">
|
||||
@forelse ($patients as $visit)
|
||||
<tr>
|
||||
<td class="py-3 pr-4">
|
||||
<p class="font-medium text-slate-900">{{ $visit->patient?->fullName() ?? 'Patient' }}</p>
|
||||
@if ($visit->patient?->patient_number)
|
||||
<p class="text-xs text-slate-400">{{ $visit->patient->patient_number }}</p>
|
||||
@endif
|
||||
</td>
|
||||
<td class="py-3 pr-4 text-slate-600">{{ $visit->bed?->label ?? '—' }}</td>
|
||||
<td class="py-3 pr-4 whitespace-nowrap text-slate-500">
|
||||
{{ $visit->placed_at?->format('d M H:i') ?? '—' }}
|
||||
</td>
|
||||
<td class="py-3 text-right whitespace-nowrap">
|
||||
@if ($canMar)
|
||||
<a href="{{ route('care.visits.mar', $visit) }}" class="mr-3 font-medium text-indigo-600 hover:text-indigo-800">MAR</a>
|
||||
@endif
|
||||
@if ($canAssess || $canVitals)
|
||||
<a href="{{ route('care.care-units.assessments', $unit) }}#visit-{{ $visit->id }}" class="mr-3 text-slate-600 hover:text-slate-900">Vitals</a>
|
||||
@endif
|
||||
@if ($canNotes)
|
||||
<a href="{{ route('care.care-units.notes', $unit) }}" class="text-slate-600 hover:text-slate-900">Notes</a>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="py-10 text-center text-slate-500">
|
||||
No patients placed on this unit yet.
|
||||
@if ($canServicesHub)
|
||||
Placements are managed from the care unit board.
|
||||
@else
|
||||
Ask a charge nurse to place patients on this unit.
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -32,6 +32,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
if ($permissions->can($member, 'nursing.station.view')) {
|
||||
$nav[] = ['name' => 'My ward', 'route' => route('care.nursing.station'), 'active' => request()->routeIs('care.nursing.station')
|
||||
|| (request()->routeIs('care.care-units.mar')
|
||||
|| request()->routeIs('care.care-units.notes')
|
||||
|| request()->routeIs('care.care-units.handovers*')
|
||||
|| request()->routeIs('care.care-units.assessments')
|
||||
|| request()->routeIs('care.visits.mar')),
|
||||
'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" />'];
|
||||
}
|
||||
|
||||
if ($permissions->can($member, 'nursing.my_shifts.view')) {
|
||||
$nav[] = ['name' => 'My shifts', 'route' => route('care.my-shifts'), 'active' => request()->routeIs('care.my-shifts'),
|
||||
'icon' => '<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />'];
|
||||
|
||||
@@ -23,6 +23,7 @@ use App\Http\Controllers\Care\NursePerformanceController;
|
||||
use App\Http\Controllers\Care\ShiftController;
|
||||
use App\Http\Controllers\Care\RosterController;
|
||||
use App\Http\Controllers\Care\NursingServicesController;
|
||||
use App\Http\Controllers\Care\NurseStationController;
|
||||
use App\Http\Controllers\Care\MyShiftsController;
|
||||
use App\Http\Controllers\Care\DeviceController;
|
||||
use App\Http\Controllers\Care\DisplayPublicController;
|
||||
@@ -474,6 +475,7 @@ Route::middleware(['auth', 'platform.session'])->group(function () {
|
||||
Route::put('/shifts/{shift}', [ShiftController::class, 'update'])->name('care.shifts.update');
|
||||
|
||||
Route::get('/nursing/services', [NursingServicesController::class, 'index'])->name('care.nursing.services');
|
||||
Route::get('/nursing/station', [NurseStationController::class, 'index'])->name('care.nursing.station');
|
||||
Route::get('/my-shifts', [MyShiftsController::class, 'index'])->name('care.my-shifts');
|
||||
|
||||
Route::get('/staff-assignments', [StaffAssignmentController::class, 'index'])->name('care.staff-assignments.index');
|
||||
|
||||
@@ -101,6 +101,7 @@ class CareMyShiftsAndNavPermissionsTest extends TestCase
|
||||
$this->assertFalse($permissions->can($member, 'inpatient.placement.view'));
|
||||
$this->assertFalse($permissions->can($member, 'admin.departments.view'));
|
||||
$this->assertTrue($permissions->can($member, 'nursing.my_shifts.view'));
|
||||
$this->assertFalse($permissions->can($member, 'nursing.station.view'));
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('care.care-units.index'))
|
||||
@@ -110,11 +111,16 @@ class CareMyShiftsAndNavPermissionsTest extends TestCase
|
||||
->get(route('care.nursing.services'))
|
||||
->assertForbidden();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('care.nursing.station'))
|
||||
->assertForbidden();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('care.dashboard'))
|
||||
->assertOk()
|
||||
->assertDontSee('>Care units<', false)
|
||||
->assertDontSee('>Nursing Services<', false)
|
||||
->assertDontSee('>My ward<', false)
|
||||
->assertSee('My shifts', false);
|
||||
}
|
||||
|
||||
@@ -174,6 +180,9 @@ class CareMyShiftsAndNavPermissionsTest extends TestCase
|
||||
$this->assertFalse($permissions->can($member, 'inpatient.placement.view'));
|
||||
$this->assertFalse($permissions->can($member, 'nursing.roster.manage'));
|
||||
$this->assertTrue($permissions->can($member, 'nursing.my_shifts.view'));
|
||||
$this->assertTrue($permissions->can($member, 'nursing.station.view'));
|
||||
$this->assertTrue($permissions->can($member, 'mar.view'));
|
||||
$this->assertTrue($permissions->can($member, 'nursing.notes.manage'));
|
||||
$this->assertFalse($permissions->can($member, 'admin.departments.view'));
|
||||
$this->assertFalse($permissions->can($member, 'admin.departments.manage'));
|
||||
|
||||
@@ -201,6 +210,12 @@ class CareMyShiftsAndNavPermissionsTest extends TestCase
|
||||
->get(route('care.specialty.show', 'nursing_services'))
|
||||
->assertForbidden();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('care.nursing.station'))
|
||||
->assertOk()
|
||||
->assertSee('My ward')
|
||||
->assertSee('No unit assigned for today');
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('care.dashboard'))
|
||||
->assertOk()
|
||||
@@ -208,6 +223,88 @@ class CareMyShiftsAndNavPermissionsTest extends TestCase
|
||||
->assertDontSee('>Departments<', false)
|
||||
->assertDontSee('>Nursing Services<', false)
|
||||
->assertDontSee('>Nursing<', false)
|
||||
->assertSee('>My ward<', false)
|
||||
->assertSee('My shifts', false);
|
||||
}
|
||||
|
||||
public function test_regular_nurse_station_shows_rostered_unit_patients_and_clinical_boards(): void
|
||||
{
|
||||
$user = User::create([
|
||||
'public_id' => 'nurse-station',
|
||||
'name' => 'Nurse Kojo',
|
||||
'email' => 'nurse-station@example.com',
|
||||
]);
|
||||
$member = Member::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'user_ref' => $user->public_id,
|
||||
'role' => 'nurse',
|
||||
'branch_id' => $this->branch->id,
|
||||
]);
|
||||
|
||||
$roster = app(RosterService::class);
|
||||
$day = $roster->ensureDefaultShifts($this->organization, $this->owner->public_id)->firstWhere('code', 'day');
|
||||
$roster->assign(
|
||||
$this->unit,
|
||||
$day,
|
||||
$member,
|
||||
now()->toDateString(),
|
||||
$this->owner->public_id,
|
||||
$this->owner->public_id,
|
||||
);
|
||||
|
||||
$patient = \App\Models\Patient::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_number' => 'P-WARD-1',
|
||||
'first_name' => 'Ama',
|
||||
'last_name' => 'Mensah',
|
||||
]);
|
||||
\App\Models\Visit::create([
|
||||
'owner_ref' => $this->owner->public_id,
|
||||
'organization_id' => $this->organization->id,
|
||||
'branch_id' => $this->branch->id,
|
||||
'patient_id' => $patient->id,
|
||||
'care_unit_id' => $this->unit->id,
|
||||
'status' => \App\Models\Visit::STATUS_OPEN,
|
||||
'checked_in_at' => now(),
|
||||
'placed_at' => now(),
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('care.nursing.station'))
|
||||
->assertOk()
|
||||
->assertSee('Medical Ward')
|
||||
->assertSee('Ama Mensah')
|
||||
->assertSee('MAR round')
|
||||
->assertSee('Vitals & assess', false)
|
||||
->assertSee('Notes')
|
||||
->assertSee('Handover');
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('care.care-units.mar', $this->unit))
|
||||
->assertOk()
|
||||
->assertSee('MAR board');
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('care.care-units.notes', $this->unit))
|
||||
->assertOk()
|
||||
->assertSee('Nursing notes');
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('care.care-units.assessments', $this->unit))
|
||||
->assertOk()
|
||||
->assertSee('Ward assessment board');
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('care.care-units.show', $this->unit))
|
||||
->assertOk()
|
||||
->assertSee('Ama Mensah');
|
||||
|
||||
$this->actingAs($user)
|
||||
->get(route('care.my-shifts'))
|
||||
->assertOk()
|
||||
->assertSee('Open ward');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user