Gate ward admin nav to nursing roles and add My shifts for staff.
Deploy Ladill Care / deploy (push) Successful in 52s

Ambulance and other non-nursing vitals roles no longer see Care units / Nursing Services; staff get a personal duty roster view of assigned shifts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-20 11:26:52 +00:00
co-authored by Cursor
parent 76241ac2b5
commit 7c32b34715
8 changed files with 410 additions and 9 deletions
@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Care;
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
use App\Http\Controllers\Controller;
use App\Services\Care\RosterService;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\View\View;
class MyShiftsController extends Controller
{
use ScopesToAccount;
public function index(Request $request, RosterService $roster): View
{
$this->authorizeAbility($request, 'nursing.my_shifts.view');
$member = $this->member($request);
abort_unless($member, 403);
$weekStart = $request->filled('week')
? Carbon::parse($request->input('week'))->startOfWeek(Carbon::MONDAY)
: now()->startOfWeek(Carbon::MONDAY);
$grid = $roster->memberWeek($member, $this->ownerRef($request), $weekStart);
$byDate = [];
foreach ($grid['entries'] as $entry) {
$byDate[$entry->duty_date->toDateString()][] = $entry;
}
return view('care.nursing.my-shifts', [
'member' => $member,
'weekStart' => $grid['week_start'],
'days' => $grid['days'],
'entriesByDate' => $byDate,
'todayEntries' => $grid['today'],
'prevWeek' => $grid['week_start']->copy()->subWeek()->toDateString(),
'nextWeek' => $grid['week_start']->copy()->addWeek()->toDateString(),
'roles' => config('care.roles'),
]);
}
}
+49 -7
View File
@@ -498,6 +498,17 @@ class CarePermissions
],
];
/** Ward / nursing-ops roles — not every clinical role with vitals (e.g. ambulance, GP). */
protected array $nursingOpsRoles = [
'nurse',
'ed_nurse',
'theatre_nurse',
'labour_ward_nurse',
'fertility_nurse',
'dialysis_nurse',
'midwife',
];
/** Clinical roles that staff the floor (queues, specialty desks, counters). */
protected array $floorCareRoles = [
'emergency_physician', 'ed_nurse', 'general_physician', 'doctor',
@@ -566,11 +577,14 @@ class CarePermissions
}
}
return $this->withDerivedAbilities(array_keys($merged));
return $this->withNursingOpsAbilities(
$this->withDerivedAbilities(array_keys($merged)),
$role,
);
}
/**
* Floor clinicians who record vitals may also place patients on care units/beds.
* Clinical vitals unlock charting surfaces; ward nursing hub abilities are role-gated.
*
* @param list<string> $abilities
* @return list<string>
@@ -581,9 +595,8 @@ class CarePermissions
return $abilities;
}
// Clinical vitals → charting surfaces (not the Nursing Services admin hub).
if (in_array('vitals.manage', $abilities, true)) {
$abilities[] = 'inpatient.placement.view';
$abilities[] = 'inpatient.placement.manage';
$abilities[] = 'mar.view';
$abilities[] = 'mar.administer';
$abilities[] = 'nursing.notes.view';
@@ -593,9 +606,7 @@ class CarePermissions
$abilities[] = 'nursing.assessments.view';
$abilities[] = 'nursing.assessments.manage';
$abilities[] = 'nursing.performance.view';
$abilities[] = 'nursing.roster.view';
$abilities[] = 'nursing.roster.manage';
$abilities[] = 'nursing.services.view';
$abilities[] = 'nursing.my_shifts.view';
}
if (in_array('analytics.department.view', $abilities, true)) {
@@ -607,11 +618,42 @@ class CarePermissions
$abilities[] = 'nursing.performance.view';
$abilities[] = 'nursing.roster.view';
$abilities[] = 'nursing.services.view';
$abilities[] = 'nursing.my_shifts.view';
}
return array_values(array_unique($abilities));
}
/**
* Ward nursing abilities (care units, roster manage, Nursing Services hub).
* Applied only for nursing-ops roles not ambulance / GP / specialty MDs.
*
* @param list<string> $abilities
* @return list<string>
*/
protected function withNursingOpsAbilities(array $abilities, ?string $role): array
{
if (in_array('*', $abilities, true)) {
return $abilities;
}
if ($role === null || ! in_array($role, $this->nursingOpsRoles, true)) {
return $abilities;
}
if (! in_array('vitals.manage', $abilities, true)) {
return $abilities;
}
$abilities[] = 'inpatient.placement.view';
$abilities[] = 'inpatient.placement.manage';
$abilities[] = 'nursing.roster.view';
$abilities[] = 'nursing.roster.manage';
$abilities[] = 'nursing.services.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);
@@ -20,7 +20,6 @@ class NursingServicesService
'fertility_nurse',
'dialysis_nurse',
'midwife',
'ambulance_staff',
];
/**
+38
View File
@@ -240,6 +240,44 @@ class RosterService
->get();
}
/**
* Roster entries for one staff member over a week (or open-ended upcoming window).
*
* @return array{week_start: Carbon, days: list<Carbon>, entries: Collection<int, RosterEntry>, today: Collection<int, RosterEntry>}
*/
public function memberWeek(
Member $member,
string $ownerRef,
?CarbonInterface $weekStart = null,
): array {
$start = Carbon::parse($weekStart ?? now())->startOfWeek(Carbon::MONDAY)->startOfDay();
$end = $start->copy()->addDays(6);
$days = [];
for ($i = 0; $i < 7; $i++) {
$days[] = $start->copy()->addDays($i);
}
$entries = RosterEntry::owned($ownerRef)
->where('member_id', $member->id)
->whereBetween('duty_date', [$start->toDateString(), $end->toDateString()])
->where('status', '!=', RosterEntry::STATUS_CANCELLED)
->with(['shift', 'careUnit.department'])
->orderBy('duty_date')
->orderBy('shift_id')
->get();
$today = $entries->filter(
fn (RosterEntry $entry) => $entry->duty_date->isSameDay(now())
)->values();
return [
'week_start' => $start,
'days' => $days,
'entries' => $entries,
'today' => $today,
];
}
/**
* @param iterable<Member> $members
* @return array<int, string>