Files
ladill-care/app/Http/Controllers/Care/StaffAssignmentController.php
T
isaaccladandCursor b7aca6ee2b
Deploy Ladill Care / deploy (push) Successful in 30s
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 <cursoragent@cursor.com>
2026-07-20 11:49:18 +00:00

253 lines
9.8 KiB
PHP

<?php
namespace App\Http\Controllers\Care;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
use App\Models\CareUnit;
use App\Models\Department;
use App\Models\Member;
use App\Models\StaffAssignment;
use App\Models\User;
use App\Services\Care\AuditLogger;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\View\View;
class StaffAssignmentController extends Controller
{
use ScopesToAccount;
public function index(Request $request): View
{
$this->authorizeAbility($request, 'admin.members.view');
$organization = $this->organization($request);
$owner = $this->ownerRef($request);
$assignments = StaffAssignment::owned($owner)
->where('organization_id', $organization->id)
->unitPlacements()
->with(['member.branch', 'department.branch', 'careUnit'])
->orderByDesc('starts_on')
->orderByDesc('id')
->get();
$heroStats = [
'total' => $assignments->count(),
'active' => $assignments->where('status', 'active')->filter(fn (StaffAssignment $a) => $a->isEffectiveOn())->count(),
'temporary' => $assignments->where('kind', 'temporary')->count(),
'primary' => $assignments->where('kind', 'primary')->count(),
];
return view('care.admin.staff-assignments.index', [
'assignments' => $assignments,
'organization' => $organization,
'kinds' => config('care.staff_assignment_kinds'),
'statuses' => config('care.staff_assignment_statuses'),
'assignmentRoles' => config('care.staff_assignment_roles'),
'memberLabels' => $this->memberLabels($assignments->pluck('member')->filter()),
'heroStats' => $heroStats,
]);
}
public function create(Request $request): View
{
$this->authorizeAbility($request, 'admin.members.manage');
return view('care.admin.staff-assignments.create', $this->formData($request));
}
public function store(Request $request): RedirectResponse
{
$this->authorizeAbility($request, 'admin.members.manage');
$organization = $this->organization($request);
$owner = $this->ownerRef($request);
$validated = $this->validated($request, $organization->id, $owner);
$assignment = StaffAssignment::create([
'owner_ref' => $owner,
'organization_id' => $organization->id,
...$validated,
]);
AuditLogger::record($owner, 'staff_assignment.created', $organization->id, $owner, StaffAssignment::class, $assignment->id);
return redirect()->route('care.staff-assignments.index')->with('success', 'Unit assignment created.');
}
public function edit(Request $request, StaffAssignment $staffAssignment): View
{
$this->authorizeAbility($request, 'admin.members.manage');
$this->authorizeOwner($request, $staffAssignment);
return view('care.admin.staff-assignments.edit', [
...$this->formData($request),
'assignment' => $staffAssignment,
]);
}
public function update(Request $request, StaffAssignment $staffAssignment): RedirectResponse
{
$this->authorizeAbility($request, 'admin.members.manage');
$this->authorizeOwner($request, $staffAssignment);
$organization = $this->organization($request);
$owner = $this->ownerRef($request);
$validated = $this->validated($request, $organization->id, $owner);
$staffAssignment->update($validated);
AuditLogger::record($owner, 'staff_assignment.updated', $organization->id, $owner, StaffAssignment::class, $staffAssignment->id);
return redirect()->route('care.staff-assignments.index')->with('success', 'Unit assignment updated.');
}
public function destroy(Request $request, StaffAssignment $staffAssignment): RedirectResponse
{
$this->authorizeAbility($request, 'admin.members.manage');
$this->authorizeOwner($request, $staffAssignment);
$staffAssignment->delete();
AuditLogger::record(
$this->ownerRef($request),
'staff_assignment.deleted',
$staffAssignment->organization_id,
$this->ownerRef($request),
StaffAssignment::class,
$staffAssignment->id,
);
return redirect()->route('care.staff-assignments.index')->with('success', 'Unit assignment removed.');
}
/**
* @return array<string, mixed>
*/
protected function formData(Request $request): array
{
$organization = $this->organization($request);
$owner = $this->ownerRef($request);
$members = Member::owned($owner)
->where('organization_id', $organization->id)
->with('branch')
->orderBy('role')
->get();
$departments = Department::owned($owner)
->whereHas('branch', fn ($q) => $q->where('organization_id', $organization->id))
->with('branch')
->where('is_active', true)
->orderBy('name')
->get();
$units = CareUnit::owned($owner)
->where('organization_id', $organization->id)
->where('is_active', true)
->with('department')
->orderBy('name')
->get();
return [
'organization' => $organization,
'members' => $members,
'memberLabels' => $this->memberLabels($members),
'departments' => $departments,
'units' => $units,
'kinds' => config('care.staff_assignment_kinds'),
'statuses' => config('care.staff_assignment_statuses'),
'assignmentRoles' => config('care.staff_assignment_roles'),
'rbacRoles' => config('care.roles'),
'selectedMemberId' => $request->integer('member_id') ?: null,
];
}
/**
* @return array<string, mixed>
*/
protected function validated(Request $request, int $organizationId, string $owner): array
{
$validated = $request->validate([
'member_id' => ['required', 'integer', 'exists:care_members,id'],
'department_id' => ['nullable', 'integer', 'exists:care_departments,id'],
'care_unit_id' => [
'nullable',
'integer',
'exists:care_care_units,id',
Rule::requiredIf(fn () => $request->input('kind') === 'temporary'),
],
'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')))],
'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 unit assignments require a care unit.',
]);
$member = Member::owned($owner)->findOrFail($validated['member_id']);
abort_unless((int) $member->organization_id === $organizationId, 404);
if (! empty($validated['department_id'])) {
$department = Department::owned($owner)->with('branch')->findOrFail($validated['department_id']);
abort_unless((int) $department->branch?->organization_id === $organizationId, 404);
} else {
$validated['department_id'] = null;
}
if (! empty($validated['care_unit_id'])) {
$unit = CareUnit::owned($owner)->findOrFail($validated['care_unit_id']);
abort_unless((int) $unit->organization_id === $organizationId, 404);
if (! empty($validated['department_id']) && (int) $unit->department_id !== (int) $validated['department_id']) {
throw \Illuminate\Validation\ValidationException::withMessages([
'care_unit_id' => 'Care unit must belong to the selected department.',
]);
}
if (empty($validated['department_id'])) {
$validated['department_id'] = $unit->department_id;
}
} else {
$validated['care_unit_id'] = null;
}
return [
'member_id' => (int) $validated['member_id'],
'department_id' => $validated['department_id'] ? (int) $validated['department_id'] : null,
'care_unit_id' => $validated['care_unit_id'] ? (int) $validated['care_unit_id'] : null,
'kind' => $validated['kind'],
'assignment_role' => $validated['assignment_role'] ?? null,
'shift_code' => null,
'starts_on' => $validated['starts_on'],
'ends_on' => $validated['ends_on'] ?? null,
'status' => $validated['status'],
'notes' => $validated['notes'] ?? null,
];
}
/**
* @param iterable<Member|null> $members
* @return array<int, string>
*/
protected function memberLabels(iterable $members): array
{
$list = collect($members)->filter()->unique('id')->values();
$refs = $list->pluck('user_ref')->all();
$emails = User::query()->whereIn('public_id', $refs)->pluck('email', 'public_id');
$roles = config('care.roles');
$labels = [];
foreach ($list as $member) {
$display = str_contains((string) $member->user_ref, '@')
? $member->user_ref
: ($emails[$member->user_ref] ?? $member->user_ref);
$roleLabel = $roles[$member->role] ?? $member->role;
$labels[(int) $member->id] = $display.' · '.$roleLabel;
}
return $labels;
}
}