Clarify patient vs nurse unit placement on nursing charts.
Deploy Ladill Care / deploy (push) Successful in 55s

The empty state referred to the patient but read like the nurse was unassigned; show duty units and patient-specific copy instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-20 16:36:35 +00:00
co-authored by Cursor
parent cebc9c8e4c
commit 5c394cec2e
4 changed files with 155 additions and 10 deletions
@@ -214,6 +214,7 @@ class PatientController extends Controller
$nursingAssessments = collect(); $nursingAssessments = collect();
$activePlacedVisit = null; $activePlacedVisit = null;
$latestVitals = null; $latestVitals = null;
$nurseDutyUnits = collect();
if ($canViewAssessments) { if ($canViewAssessments) {
$branchScope = app(OrganizationResolver::class)->branchScope($member); $branchScope = app(OrganizationResolver::class)->branchScope($member);
@@ -243,10 +244,13 @@ class PatientController extends Controller
} }
if ($isNurseStation) { if ($isNurseStation) {
$nurseDutyUnits = app(\App\Services\Care\NurseStationService::class)
->dutyUnits($organization, $member, $this->ownerRef($request));
$activePlacedVisit = $patient->visits() $activePlacedVisit = $patient->visits()
->whereIn('status', [Visit::STATUS_OPEN, Visit::STATUS_IN_PROGRESS]) ->whereIn('status', [Visit::STATUS_OPEN, Visit::STATUS_IN_PROGRESS])
->whereNotNull('care_unit_id') ->whereNotNull('care_unit_id')
->with(['careUnit', 'bed']) ->with(['careUnit.department.branch', 'bed'])
->orderByDesc('placed_at') ->orderByDesc('placed_at')
->first(); ->first();
@@ -269,6 +273,7 @@ class PatientController extends Controller
'canViewPathways' => $canViewPathways, 'canViewPathways' => $canViewPathways,
'activePlacedVisit' => $activePlacedVisit, 'activePlacedVisit' => $activePlacedVisit,
'latestVitals' => $latestVitals, 'latestVitals' => $latestVitals,
'nurseDutyUnits' => $nurseDutyUnits,
'nursingAssessments' => $nursingAssessments, 'nursingAssessments' => $nursingAssessments,
'messagingSmsReady' => $suiteSmsReady || $credential->hasValidSms(), 'messagingSmsReady' => $suiteSmsReady || $credential->hasValidSms(),
'messagingEmailReady' => $suiteEmailReady || $credential->hasValidBird(), 'messagingEmailReady' => $suiteEmailReady || $credential->hasValidBird(),
+61
View File
@@ -118,4 +118,65 @@ class NurseStationService
'selected_unit' => $selected, 'selected_unit' => $selected,
]; ];
} }
/**
* Units the nurse is on duty for today (roster) or has an active home placement.
*
* @return Collection<int, CareUnit>
*/
public function dutyUnits(Organization $organization, Member $member, string $ownerRef): Collection
{
$dutyToday = RosterEntry::owned($ownerRef)
->where('organization_id', $organization->id)
->where('member_id', $member->id)
->whereDate('duty_date', now()->toDateString())
->where('status', '!=', RosterEntry::STATUS_CANCELLED)
->orderBy('shift_id')
->get(['care_unit_id']);
$placements = StaffAssignment::owned($ownerRef)
->where('organization_id', $organization->id)
->where('member_id', $member->id)
->where('status', 'active')
->unitPlacements()
->effectiveOn(now())
->whereNotNull('care_unit_id')
->get(['care_unit_id']);
$unitIds = $dutyToday->pluck('care_unit_id')
->merge($placements->pluck('care_unit_id'))
->filter()
->map(fn ($id) => (int) $id)
->unique()
->values();
if ($unitIds->isEmpty()) {
return collect();
}
$units = CareUnit::owned($ownerRef)
->where('organization_id', $organization->id)
->whereIn('id', $unitIds->all())
->where('is_active', true)
->with('department.branch')
->get()
->keyBy('id');
// Preserve roster order, then placement-only units.
$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));
}
}
return $ordered->values();
}
} }
+29 -6
View File
@@ -107,9 +107,12 @@
@if (($isNurseStation ?? false) && ! ($canConsult ?? false)) @if (($isNurseStation ?? false) && ! ($canConsult ?? false))
@if ($activePlacedVisit ?? null) @if ($activePlacedVisit ?? null)
<div class="rounded-xl border border-teal-100 bg-teal-50/60 px-3 py-3"> <div class="rounded-xl border border-teal-100 bg-teal-50/60 px-3 py-3">
<p class="text-xs font-semibold uppercase tracking-wide text-teal-800">On unit now</p> <p class="text-xs font-semibold uppercase tracking-wide text-teal-800">Patient on unit</p>
<p class="mt-1 font-medium text-slate-900"> <p class="mt-1 font-medium text-slate-900">
{{ $activePlacedVisit->careUnit?->name ?? 'Care unit' }} {{ $activePlacedVisit->careUnit?->name ?? 'Care unit' }}
@if ($activePlacedVisit->careUnit?->department?->branch)
· {{ $activePlacedVisit->careUnit->department->branch->name }}
@endif
@if ($activePlacedVisit->bed) @if ($activePlacedVisit->bed)
· Bed {{ $activePlacedVisit->bed->label }} · Bed {{ $activePlacedVisit->bed->label }}
@endif @endif
@@ -130,14 +133,34 @@
@if ($canViewMar ?? false) @if ($canViewMar ?? false)
<a href="{{ route('care.visits.mar', $activePlacedVisit) }}" class="text-sm font-medium text-teal-700 hover:text-teal-900">MAR chart</a> <a href="{{ route('care.visits.mar', $activePlacedVisit) }}" class="text-sm font-medium text-teal-700 hover:text-teal-900">MAR chart</a>
@endif @endif
<a href="{{ route('care.care-units.assessments', $activePlacedVisit->careUnit) }}" class="text-sm font-medium text-teal-700 hover:text-teal-900">Vitals &amp; assess</a> @if ($activePlacedVisit->careUnit)
<a href="{{ route('care.care-units.notes', $activePlacedVisit->careUnit) }}" class="text-sm font-medium text-teal-700 hover:text-teal-900">Nursing notes</a> <a href="{{ route('care.care-units.assessments', $activePlacedVisit->careUnit) }}" class="text-sm font-medium text-teal-700 hover:text-teal-900">Vitals &amp; assess</a>
<a href="{{ route('care.care-units.notes', $activePlacedVisit->careUnit) }}" class="text-sm font-medium text-teal-700 hover:text-teal-900">Nursing notes</a>
@endif
</p> </p>
</div> </div>
@else @else
<div class="rounded-xl border border-dashed border-slate-200 px-3 py-3 text-slate-500"> <div class="rounded-xl border border-dashed border-slate-200 px-3 py-3 text-slate-600">
Not placed on a care unit right now. <p class="font-medium text-slate-800">This patient is not on a care unit right now.</p>
<a href="{{ route('care.nursing.station') }}" class="ml-1 font-medium text-indigo-600 hover:text-indigo-800">My care unit</a> @if (($nurseDutyUnits ?? collect())->isNotEmpty())
<p class="mt-1 text-sm text-slate-500">
You are on duty at
{{ $nurseDutyUnits->pluck('name')->join(', ') }}
this chart is about the patients placement, not yours.
</p>
<p class="mt-2 flex flex-wrap gap-3">
@foreach ($nurseDutyUnits as $dutyUnit)
<a href="{{ route('care.nursing.station', ['unit' => $dutyUnit->id]) }}" class="text-sm font-medium text-indigo-600 hover:text-indigo-800">
Open {{ $dutyUnit->name }}
</a>
@endforeach
</p>
@else
<p class="mt-1 text-sm text-slate-500">
Ask a charge nurse to place them, or open
<a href="{{ route('care.nursing.station') }}" class="font-medium text-indigo-600 hover:text-indigo-800">My care unit</a>.
</p>
@endif
</div> </div>
@endif @endif
@@ -349,12 +349,68 @@ class CareMyShiftsAndNavPermissionsTest extends TestCase
->get(route('care.patients.show', $patient)) ->get(route('care.patients.show', $patient))
->assertOk() ->assertOk()
->assertSee('Visit &amp; nursing care', false) ->assertSee('Visit &amp; nursing care', false)
->assertSee('On unit now') ->assertSee('Patient on unit')
->assertSee('Medical Ward')
->assertSee('Nursing assessments') ->assertSee('Nursing assessments')
->assertSee('My care unit')
->assertDontSee('Visit &amp; clinical history', false) ->assertDontSee('Visit &amp; clinical history', false)
->assertDontSee('Condition pathways') ->assertDontSee('Condition pathways')
->assertDontSee('Follow-up scores') ->assertDontSee('Follow-up scores')
->assertDontSee('Start assessment'); ->assertDontSee('Start assessment')
->assertDontSee('This patient is not on a care unit');
}
public function test_nurse_patient_chart_clarifies_unplaced_patient_vs_nurse_duty(): void
{
$user = User::create([
'public_id' => 'nurse-chart-unplaced',
'name' => 'Nurse Duty',
'email' => 'nurse-chart-unplaced@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([
'uuid' => (string) \Illuminate\Support\Str::uuid(),
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_number' => 'P-CHART-2',
'first_name' => 'Kofi',
'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,
'status' => \App\Models\Visit::STATUS_IN_PROGRESS,
'checked_in_at' => now(),
]);
$this->actingAs($user)
->get(route('care.patients.show', $patient))
->assertOk()
->assertSee('This patient is not on a care unit right now.')
->assertSee('You are on duty at')
->assertSee('Medical Ward')
->assertSee('Open Medical Ward')
->assertDontSee('Not placed on a care unit right now.');
} }
} }