Separate unit placements from shift duty assignments.
Deploy Ladill Care / deploy (push) Successful in 30s
Deploy Ladill Care / deploy (push) Successful in 30s
Roster no longer writes temporary staff assignments; unit assignment UI drops shift fields and labels clarify the two workflows. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user