From b7aca6ee2b2f74c46d51d4374a2e3fa40144608b Mon Sep 17 00:00:00 2001 From: isaacclad Date: Mon, 20 Jul 2026 11:49:18 +0000 Subject: [PATCH] Separate unit placements from shift duty assignments. Roster no longer writes temporary staff assignments; unit assignment UI drops shift fields and labels clarify the two workflows. Co-authored-by: Cursor --- .../Care/NursingServicesController.php | 1 - .../Care/StaffAssignmentController.php | 14 +++---- app/Models/StaffAssignment.php | 15 +++++++ app/Services/Care/NursePerformanceService.php | 1 + app/Services/Care/NursingServicesService.php | 1 + app/Services/Care/RosterService.php | 32 +-------------- config/care.php | 3 +- .../care/admin/care-units/show.blade.php | 4 +- .../admin/staff-assignments/create.blade.php | 39 +++++++------------ .../admin/staff-assignments/index.blade.php | 12 +++--- resources/views/care/nursing/roster.blade.php | 6 +-- .../views/care/nursing/services-hub.blade.php | 14 +++---- resources/views/partials/sidebar.blade.php | 2 +- .../CareRosterAndNursingServicesTest.php | 19 +++------ tests/Feature/CareStaffManagementTest.php | 6 +-- 15 files changed, 68 insertions(+), 101 deletions(-) diff --git a/app/Http/Controllers/Care/NursingServicesController.php b/app/Http/Controllers/Care/NursingServicesController.php index 2a2e4f1..97e9225 100644 --- a/app/Http/Controllers/Care/NursingServicesController.php +++ b/app/Http/Controllers/Care/NursingServicesController.php @@ -52,7 +52,6 @@ class NursingServicesController extends Controller 'canRoster' => $permissions->can($member, 'nursing.roster.manage'), 'canViewRoster' => $permissions->can($member, 'nursing.roster.view'), 'assignmentRoles' => config('care.staff_assignment_roles'), - 'shiftCodes' => config('care.staff_shift_codes'), ]); } } diff --git a/app/Http/Controllers/Care/StaffAssignmentController.php b/app/Http/Controllers/Care/StaffAssignmentController.php index d87ecd5..365931d 100644 --- a/app/Http/Controllers/Care/StaffAssignmentController.php +++ b/app/Http/Controllers/Care/StaffAssignmentController.php @@ -27,6 +27,7 @@ class StaffAssignmentController extends Controller $assignments = StaffAssignment::owned($owner) ->where('organization_id', $organization->id) + ->unitPlacements() ->with(['member.branch', 'department.branch', 'careUnit']) ->orderByDesc('starts_on') ->orderByDesc('id') @@ -45,7 +46,6 @@ class StaffAssignmentController extends Controller 'kinds' => config('care.staff_assignment_kinds'), 'statuses' => config('care.staff_assignment_statuses'), 'assignmentRoles' => config('care.staff_assignment_roles'), - 'shiftCodes' => config('care.staff_shift_codes'), 'memberLabels' => $this->memberLabels($assignments->pluck('member')->filter()), 'heroStats' => $heroStats, ]); @@ -74,7 +74,7 @@ class StaffAssignmentController extends Controller AuditLogger::record($owner, 'staff_assignment.created', $organization->id, $owner, StaffAssignment::class, $assignment->id); - return redirect()->route('care.staff-assignments.index')->with('success', 'Staff assignment created.'); + return redirect()->route('care.staff-assignments.index')->with('success', 'Unit assignment created.'); } public function edit(Request $request, StaffAssignment $staffAssignment): View @@ -100,7 +100,7 @@ class StaffAssignmentController extends Controller AuditLogger::record($owner, 'staff_assignment.updated', $organization->id, $owner, StaffAssignment::class, $staffAssignment->id); - return redirect()->route('care.staff-assignments.index')->with('success', 'Staff assignment updated.'); + return redirect()->route('care.staff-assignments.index')->with('success', 'Unit assignment updated.'); } public function destroy(Request $request, StaffAssignment $staffAssignment): RedirectResponse @@ -119,7 +119,7 @@ class StaffAssignmentController extends Controller $staffAssignment->id, ); - return redirect()->route('care.staff-assignments.index')->with('success', 'Staff assignment removed.'); + return redirect()->route('care.staff-assignments.index')->with('success', 'Unit assignment removed.'); } /** @@ -159,7 +159,6 @@ class StaffAssignmentController extends Controller 'kinds' => config('care.staff_assignment_kinds'), 'statuses' => config('care.staff_assignment_statuses'), 'assignmentRoles' => config('care.staff_assignment_roles'), - 'shiftCodes' => config('care.staff_shift_codes'), 'rbacRoles' => config('care.roles'), 'selectedMemberId' => $request->integer('member_id') ?: null, ]; @@ -181,13 +180,12 @@ class StaffAssignmentController extends Controller ], 'kind' => ['required', 'string', Rule::in(array_keys(config('care.staff_assignment_kinds')))], 'assignment_role' => ['nullable', 'string', Rule::in(array_keys(config('care.staff_assignment_roles')))], - 'shift_code' => ['nullable', 'string', Rule::in(array_keys(config('care.staff_shift_codes')))], 'starts_on' => ['required', 'date'], 'ends_on' => ['nullable', 'date', 'after_or_equal:starts_on'], 'status' => ['required', 'string', Rule::in(array_keys(config('care.staff_assignment_statuses')))], 'notes' => ['nullable', 'string', 'max:2000'], ], [ - 'care_unit_id.required' => 'Temporary assignments require a care unit.', + 'care_unit_id.required' => 'Temporary unit assignments require a care unit.', ]); $member = Member::owned($owner)->findOrFail($validated['member_id']); @@ -221,7 +219,7 @@ class StaffAssignmentController extends Controller 'care_unit_id' => $validated['care_unit_id'] ? (int) $validated['care_unit_id'] : null, 'kind' => $validated['kind'], 'assignment_role' => $validated['assignment_role'] ?? null, - 'shift_code' => $validated['shift_code'] ?? null, + 'shift_code' => null, 'starts_on' => $validated['starts_on'], 'ends_on' => $validated['ends_on'] ?? null, 'status' => $validated['status'], diff --git a/app/Models/StaffAssignment.php b/app/Models/StaffAssignment.php index 5c843e5..e6e1c2c 100644 --- a/app/Models/StaffAssignment.php +++ b/app/Models/StaffAssignment.php @@ -7,6 +7,7 @@ use Carbon\CarbonInterface; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\SoftDeletes; class StaffAssignment extends Model @@ -58,6 +59,20 @@ class StaffAssignment extends Model return $this->belongsTo(CareUnit::class, 'care_unit_id'); } + /** + * Legacy link from when duty roster auto-created unit placements. + * Shift assignments live on RosterEntry; new roster rows do not set this. + */ + public function rosterEntries(): HasMany + { + return $this->hasMany(RosterEntry::class, 'staff_assignment_id'); + } + + public function scopeUnitPlacements(Builder $query): Builder + { + return $query->whereDoesntHave('rosterEntries'); + } + public function scopeEffectiveOn(Builder $query, CarbonInterface|string|null $date = null): Builder { $day = $date diff --git a/app/Services/Care/NursePerformanceService.php b/app/Services/Care/NursePerformanceService.php index 983c864..97b32d2 100644 --- a/app/Services/Care/NursePerformanceService.php +++ b/app/Services/Care/NursePerformanceService.php @@ -85,6 +85,7 @@ class NursePerformanceService $assignedStaff = StaffAssignment::owned($ownerRef) ->where('care_unit_id', $unit->id) ->where('status', 'active') + ->unitPlacements() ->effectiveOn($to) ->with('member') ->get(); diff --git a/app/Services/Care/NursingServicesService.php b/app/Services/Care/NursingServicesService.php index a32fd59..28876b2 100644 --- a/app/Services/Care/NursingServicesService.php +++ b/app/Services/Care/NursingServicesService.php @@ -45,6 +45,7 @@ class NursingServicesService return StaffAssignment::owned($ownerRef) ->where('organization_id', $organization->id) ->where('status', 'active') + ->unitPlacements() ->effectiveOn(now()) ->whereHas('member', fn ($q) => $q->whereIn('role', self::NURSING_ROLES)) ->with(['member', 'careUnit.department', 'department']) diff --git a/app/Services/Care/RosterService.php b/app/Services/Care/RosterService.php index 9383fd3..642f036 100644 --- a/app/Services/Care/RosterService.php +++ b/app/Services/Care/RosterService.php @@ -7,7 +7,6 @@ use App\Models\Member; use App\Models\Organization; use App\Models\RosterEntry; use App\Models\Shift; -use App\Models\StaffAssignment; use App\Models\User; use Carbon\Carbon; use Carbon\CarbonInterface; @@ -114,7 +113,6 @@ class RosterService string $ownerRef, ?string $actorRef = null, ?string $notes = null, - bool $linkAssignment = true, ): RosterEntry { if ((int) $shift->organization_id !== (int) $unit->organization_id || (int) $member->organization_id !== (int) $unit->organization_id) { @@ -138,32 +136,13 @@ class RosterService ]); } - $assignmentId = null; - if ($linkAssignment) { - $assignment = StaffAssignment::create([ - 'owner_ref' => $ownerRef, - 'organization_id' => $unit->organization_id, - 'member_id' => $member->id, - 'department_id' => $unit->department_id, - 'care_unit_id' => $unit->id, - 'kind' => 'temporary', - 'assignment_role' => null, - 'shift_code' => $shift->code, - 'starts_on' => $date, - 'ends_on' => $date, - 'status' => 'active', - 'notes' => $notes ?? 'Duty roster', - ]); - $assignmentId = $assignment->id; - } - if ($existing) { if ($existing->trashed()) { $existing->restore(); } $existing->update([ 'status' => RosterEntry::STATUS_SCHEDULED, - 'staff_assignment_id' => $assignmentId, + 'staff_assignment_id' => null, 'notes' => $notes, 'created_by' => $actorRef, ]); @@ -177,7 +156,7 @@ class RosterService 'member_id' => $member->id, 'duty_date' => $date, 'status' => RosterEntry::STATUS_SCHEDULED, - 'staff_assignment_id' => $assignmentId, + 'staff_assignment_id' => null, 'notes' => $notes, 'created_by' => $actorRef, ]); @@ -202,13 +181,6 @@ class RosterService $entry->update(['status' => RosterEntry::STATUS_CANCELLED]); - if ($entry->staff_assignment_id) { - StaffAssignment::query()->where('id', $entry->staff_assignment_id)->update([ - 'status' => 'ended', - 'ends_on' => $entry->duty_date, - ]); - } - // Soft-delete so the unique (unit, shift, member, date) can be reused. $entry->delete(); diff --git a/config/care.php b/config/care.php index 40f1a33..be0e0a6 100644 --- a/config/care.php +++ b/config/care.php @@ -134,7 +134,8 @@ return [ ], /* - | Lightweight shift codes until full roster entities (Phase 3). + | Shift labels for nursing notes / handovers (not unit placement). + | Duty scheduling uses care_shifts + roster entries. */ 'staff_shift_codes' => [ 'day' => 'Day shift', diff --git a/resources/views/care/admin/care-units/show.blade.php b/resources/views/care/admin/care-units/show.blade.php index 3c5b8c4..38f7521 100644 --- a/resources/views/care/admin/care-units/show.blade.php +++ b/resources/views/care/admin/care-units/show.blade.php @@ -35,7 +35,7 @@ MAR board @endif @if ($canRoster) - Duty roster + Shift assignments @endif @if ($canAssess) Assessments @@ -51,7 +51,7 @@ @endif @if ($canManageUnit) Edit unit - Assign staff + Unit assignment @endif diff --git a/resources/views/care/admin/staff-assignments/create.blade.php b/resources/views/care/admin/staff-assignments/create.blade.php index c952790..93efb83 100644 --- a/resources/views/care/admin/staff-assignments/create.blade.php +++ b/resources/views/care/admin/staff-assignments/create.blade.php @@ -2,10 +2,10 @@ $assignment = $assignment ?? null; @endphp - +
-

{{ $assignment ? 'Edit staff assignment' : 'Add staff assignment' }}

-

RBAC role stays on the team member. This record is their unit placement over a date range.

+

{{ $assignment ? 'Edit unit assignment' : 'Add unit assignment' }}

+

Home or float placement on a unit. Day/evening/night duty is scheduled separately on the shift roster.

{{ $message }}

@enderror
-
-
- - -
-
- - -
+
+ +
@@ -109,13 +98,13 @@
- + @if ($assignment) -
+ @csrf @method('DELETE') - +
@endif
diff --git a/resources/views/care/admin/staff-assignments/index.blade.php b/resources/views/care/admin/staff-assignments/index.blade.php index e0e87c4..796c3ce 100644 --- a/resources/views/care/admin/staff-assignments/index.blade.php +++ b/resources/views/care/admin/staff-assignments/index.blade.php @@ -1,9 +1,9 @@ - +
- Add assignment + Add unit assignment @@ -23,7 +23,6 @@ Kind Post Unit / Dept - Shift Dates Status @@ -41,7 +40,6 @@
{{ $assignment->department->name }}
@endif - {{ $shiftCodes[$assignment->shift_code] ?? ($assignment->shift_code ?: '—') }} {{ $assignment->starts_on->format('Y-m-d') }} → @@ -58,7 +56,7 @@ @empty - No staff assignments yet. + No unit assignments yet. @endforelse diff --git a/resources/views/care/nursing/roster.blade.php b/resources/views/care/nursing/roster.blade.php index 1dc1e66..e55be42 100644 --- a/resources/views/care/nursing/roster.blade.php +++ b/resources/views/care/nursing/roster.blade.php @@ -7,7 +7,7 @@ / Duty roster

-

Week roster

+

Shift assignments

{{ $weekStart->format('d M Y') }} – {{ $weekStart->copy()->addDays(6)->format('d M Y') }} · {{ $unit->department?->name }} @@ -80,8 +80,8 @@ @if ($canManage)

-

Add to roster

-

Creates a temporary staff assignment for that duty date.

+

Add shift assignment

+

Schedules who works this unit on a specific date and shift. Does not change their unit placement.

@csrf
diff --git a/resources/views/care/nursing/services-hub.blade.php b/resources/views/care/nursing/services-hub.blade.php index 5c68c01..ef3df04 100644 --- a/resources/views/care/nursing/services-hub.blade.php +++ b/resources/views/care/nursing/services-hub.blade.php @@ -15,7 +15,7 @@ Shift templates @endif Care units - Staff assignments + Unit assignments
@@ -35,7 +35,7 @@
-

Today’s roster

+

Today’s shift assignments

@@ -64,7 +64,7 @@ @empty - + @endforelse
{{ $nurseLabels[$entry->member_id] ?? ($entry->member?->user_ref ?? '—') }}
No duty roster entries for today.
No shift assignments for today.
@@ -73,8 +73,8 @@
-

Active allocations

-

Dated staff assignments effective today (primary, temporary, specialty).

+

Unit assignments

+

Home and float placements effective today (primary, temporary, specialty) — separate from shift duty.

@@ -102,7 +102,7 @@ @empty - + @endforelse
No active nursing allocations today.
No active unit assignments today.
@@ -118,7 +118,7 @@

{{ $unit->department?->name }} · {{ str_replace('_', ' ', $unit->kind) }}

@if ($canViewRoster) - Roster + Shifts @endif MAR Notes diff --git a/resources/views/partials/sidebar.blade.php b/resources/views/partials/sidebar.blade.php index 7b8bf8c..f9f9164 100644 --- a/resources/views/partials/sidebar.blade.php +++ b/resources/views/partials/sidebar.blade.php @@ -138,7 +138,7 @@ 'icon' => '']; } if ($permissions->can($member, 'admin.members.view')) { - $adminNav[] = ['name' => 'Staff assignments', 'route' => route('care.staff-assignments.index'), 'active' => request()->routeIs('care.staff-assignments.*'), + $adminNav[] = ['name' => 'Unit assignments', 'route' => route('care.staff-assignments.index'), 'active' => request()->routeIs('care.staff-assignments.*'), 'icon' => '']; } if ($permissions->can($member, 'admin.practitioners.view')) { diff --git a/tests/Feature/CareRosterAndNursingServicesTest.php b/tests/Feature/CareRosterAndNursingServicesTest.php index 0ee08e1..d79a6c4 100644 --- a/tests/Feature/CareRosterAndNursingServicesTest.php +++ b/tests/Feature/CareRosterAndNursingServicesTest.php @@ -110,7 +110,7 @@ class CareRosterAndNursingServicesTest extends TestCase $this->assertSame(3, Shift::query()->where('organization_id', $this->organization->id)->count()); } - public function test_roster_assign_links_temporary_staff_assignment(): void + public function test_roster_assign_does_not_create_unit_assignment(): void { $roster = app(RosterService::class); $shifts = $roster->ensureDefaultShifts($this->organization, $this->owner->public_id); @@ -128,15 +128,11 @@ class CareRosterAndNursingServicesTest extends TestCase $entry = RosterEntry::query()->first(); $this->assertNotNull($entry); $this->assertSame(RosterEntry::STATUS_SCHEDULED, $entry->status); - $this->assertNotNull($entry->staff_assignment_id); - - $assignment = StaffAssignment::query()->find($entry->staff_assignment_id); - $this->assertSame('temporary', $assignment->kind); - $this->assertSame('day', $assignment->shift_code); - $this->assertSame((int) $this->unit->id, (int) $assignment->care_unit_id); + $this->assertNull($entry->staff_assignment_id); + $this->assertSame(0, StaffAssignment::query()->count()); } - public function test_roster_cancel_ends_assignment_and_allows_reassign(): void + public function test_roster_cancel_allows_reassign(): void { $roster = app(RosterService::class); $shifts = $roster->ensureDefaultShifts($this->organization, $this->owner->public_id); @@ -157,10 +153,7 @@ class CareRosterAndNursingServicesTest extends TestCase ->assertRedirect(); $this->assertSoftDeleted('care_roster_entries', ['id' => $entry->id]); - $this->assertDatabaseHas('care_staff_assignments', [ - 'id' => $entry->staff_assignment_id, - 'status' => 'ended', - ]); + $this->assertSame(0, StaffAssignment::query()->count()); $this->actingAs($this->owner) ->post(route('care.care-units.roster.store', $this->unit), [ @@ -194,7 +187,7 @@ class CareRosterAndNursingServicesTest extends TestCase $this->actingAs($this->owner) ->get(route('care.care-units.roster', $this->unit)) ->assertOk() - ->assertSee('Week roster') + ->assertSee('Shift assignments') ->assertSee('Day shift'); } } diff --git a/tests/Feature/CareStaffManagementTest.php b/tests/Feature/CareStaffManagementTest.php index 1baf740..6e293ca 100644 --- a/tests/Feature/CareStaffManagementTest.php +++ b/tests/Feature/CareStaffManagementTest.php @@ -176,7 +176,6 @@ class CareStaffManagementTest extends TestCase 'care_unit_id' => $floatPool->id, 'kind' => 'primary', 'assignment_role' => 'float_nurse', - 'shift_code' => 'rotating', 'starts_on' => now()->toDateString(), 'status' => 'active', ]) @@ -189,7 +188,6 @@ class CareStaffManagementTest extends TestCase 'care_unit_id' => $labour->id, 'kind' => 'temporary', 'assignment_role' => 'midwife', - 'shift_code' => 'night', 'starts_on' => now()->toDateString(), 'ends_on' => now()->toDateString(), 'status' => 'active', @@ -209,8 +207,10 @@ class CareStaffManagementTest extends TestCase $this->actingAs($this->owner) ->get(route('care.staff-assignments.index')) ->assertOk() + ->assertSee('Unit assignments') ->assertSee('Float Pool') - ->assertSee('Labour Ward'); + ->assertSee('Labour Ward') + ->assertDontSee('>Shift<', false); } public function test_temporary_assignment_requires_unit(): void