@extends('layouts.app') @section('title', 'Duty Roster') @section('content') {{-- DUTY ROSTER — REDESIGN (May 3 2026) ---------------------------------------------------------------------- Layout: 7-day weekly table with 8 fixed rows DAY SHIFT × { Clinical Officer, Nurse, Receptionist, Support Staff } NIGHT SHIFT × { Clinical Officer, Nurse, Receptionist, Support Staff } Database is unchanged — DutyRoster still stores by (date, shift, staff_record_id|locum_id, department_id). The role row is derived at render time from the staff member's `designation` (or the locum's `role_designation`). Anything that doesn't match the four canonical buckets falls into a single "Other" group rendered below the main table only when it actually has entries — preserves data visibility for existing rosters that include other roles. Print/PDF: print uses the inline @media print rules below. Export PDF triggers the same print dialog (browser-side "Save as PDF"). --}} @php /** * Edit permission: only HR User and admins can add/remove entries. */ $rosterCanEdit = Auth::user() && Auth::user()->hasAnyRole('Super Admin', 'Hospital Admin', 'HR User'); /** * Map a free-text designation/locum role string to one of the four * canonical role buckets. Anything that doesn't match returns 'other' * — those entries are rendered in a separate group below the main * table so existing roster data with non-canonical roles is not * silently dropped from the UI. * * Order of checks matters: most specific first ("Clinical Officer" * before generic "Officer"; "Nursing Assistant" before "Nurse"). */ $roleBucket = function (?string $designation): string { $d = strtolower(trim($designation ?? '')); if ($d === '') return 'other'; // Volunteer Nurse: nursing coverage is nursing coverage regardless // of pay status. Must come BEFORE the generic volunteer catch-all // below so "volunteer nurse" doesn't get swallowed into Support. if (str_contains($d, 'volunteer nurse')) return 'nurse'; // CNA / Nursing Assistant — must come BEFORE the Nurse check if (str_contains($d, 'cna') || str_contains($d, 'nursing assistant') || str_contains($d, 'care assistant') || str_contains($d, 'patient attendant')) return 'cna'; // Clinical Officer / Doctor / Medical Officer if (str_contains($d, 'clinical officer') || str_contains($d, 'medical officer') || str_contains($d, 'doctor') || str_contains($d, 'clinician') || str_contains($d, 'physician') || str_contains($d, 'consultant')) return 'clinical'; // Nurse (after CNA so it doesn't swallow nursing assistants) if (str_contains($d, 'nurse') || str_contains($d, 'midwife')) return 'nurse'; // Receptionist / Front desk / Admin reception if (str_contains($d, 'reception') || str_contains($d, 'front desk') || str_contains($d, 'cashier')) return 'receptionist'; // Volunteer roles & other support designations all fold into the // Support Staff row. Volunteer Nurse already routed to 'nurse' // above so it's the one volunteer designation that doesn't land // here. Cleaners, porters, drivers, interns, records clerks, // community health volunteers, generic "Volunteer" all match. if (str_contains($d, 'volunteer') || str_contains($d, 'cleaner') || str_contains($d, 'porter') || str_contains($d, 'driver') || str_contains($d, 'intern') || str_contains($d, 'records clerk') || str_contains($d, 'support')) return 'cna'; return 'other'; }; /** * Bucket every roster entry into [shift][roleKey][date] => [entries…] * so the table render is a clean nested loop without per-cell filtering. */ $bucketed = [ 'day' => ['clinical' => [], 'nurse' => [], 'receptionist' => [], 'cna' => [], 'other' => []], 'night' => ['clinical' => [], 'nurse' => [], 'receptionist' => [], 'cna' => [], 'other' => []], ]; foreach (['day', 'night'] as $shiftKey) { $byDate = $rosterGrid[$shiftKey] ?? []; foreach ($byDate as $dateStr => $entries) { foreach ($entries as $entry) { $designation = optional($entry->staffRecord)->designation ?? optional($entry->locum)->role_designation ?? ''; $bucket = $roleBucket($designation); $bucketed[$shiftKey][$bucket][$dateStr][] = $entry; } } } // Should we render the "Other" group? Only if there's actually data // in it on either shift. Keeps the canonical 8-row table clean when // the whole week maps neatly. $showOtherDay = ! empty($bucketed['day']['other']); $showOtherNight = ! empty($bucketed['night']['other']); $showOtherGroup = $showOtherDay || $showOtherNight; // Roles to render in the canonical table (always all four). $roles = [ 'clinical' => 'Clinician', 'nurse' => 'Nurse', 'receptionist' => 'Receptionist', 'cna' => 'Support Staff', ]; // Shift metadata. $shifts = [ 'day' => ['label' => 'DAY', 'time' => '08:00 – 20:00'], 'night' => ['label' => 'NIGHT', 'time' => '20:00 – 08:00'], ]; @endphp
Week of {{ $weekStart->format('d M Y') }} – {{ $weekEnd->format('d M Y') }}
Week of {{ $weekStart->format('d M') }} – {{ $weekEnd->format('d M Y') }}
| Shift | Role | @foreach($weekDates as $d) @php $isToday = $d->isToday(); @endphp{{ $d->format('D') }} {{ $d->format('d M') }} | @endforeach
|---|---|---|
| {{ $shiftMeta['label'] }} {{ $shiftMeta['time'] }} | @endif {{-- Role cell (column 2) — tinted per spec --}}{{ $roleLabel }} | {{-- 7 day cells --}} @foreach($weekDates as $d) @php $isToday = $d->isToday(); $dateStr = $d->toDateString(); $entries = $bucketed[$shiftKey][$roleKey][$dateStr] ?? []; @endphp
@if(empty($entries))
@if($rosterCanEdit)
+ Assign
@else
—
@endif
@else
@foreach($entries as $entry)
@php
$isLocum = $entry->assignee_type === 'locum';
$isVolunteer = $entry->assignee_type === 'volunteer';
$name = $entry->assignee_name;
@endphp
@if($rosterCanEdit)
{{ $name }}
@if($isLocum)L@endif
@if($isVolunteer)V@endif
@else
{{ $name }}
@if($isLocum)L@endif
@if($isVolunteer)V@endif
@endif
@endforeach
{{-- "+ add another" affordance removed by request:
with one person per role per shift the typical case,
the inline plus on populated cells just adds visual
noise. To add a second assignee to a filled slot,
use the full "Add Rota Entry" button at the top. --}}
@endif
|
@endforeach
Other Roles
Staff whose designation doesn't map to the canonical four role rows above.
| {{ $shiftMeta['label'] }} {{ $shiftMeta['time'] }} | Other | @foreach($weekDates as $d) @php $isToday = $d->isToday(); $dateStr = $d->toDateString(); $entries = $bucketed[$shiftKey]['other'][$dateStr] ?? []; @endphp
@if(empty($entries))
—
@else
@foreach($entries as $entry)
@php
$isLocum = $entry->assignee_type === 'locum';
$isVolunteer = $entry->assignee_type === 'volunteer';
$name = $entry->assignee_name;
$desig = optional($entry->staffRecord)->designation
?? optional($entry->locum)->role_designation
?? '';
@endphp
@if($rosterCanEdit)
{{ $name }}
@if($isLocum)L@endif
@if($isVolunteer)V@endif
@else
{{ $name }}
@if($isLocum)L@endif
@if($isVolunteer)V@endif
@endif
@endforeach
@endif
|
@endforeach
Pick a date, shift, and staff member.