Add My ward nurse station for bedside clinical work.
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:
isaacclad
2026-07-20 15:54:26 +00:00
co-authored by Cursor
parent 96f36a60d6
commit 45da731561
15 changed files with 515 additions and 24 deletions
@@ -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'),
]);
}
}
+41 -2
View File
@@ -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);
+121
View File
@@ -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,
];
}
}