Add shift templates, unit duty roster grid, and Nursing Services hub.
Deploy Ladill Care / deploy (push) Successful in 1m0s
Deploy Ladill Care / deploy (push) Successful in 1m0s
Phase 3 staff management: real shift entities, week roster linked to temporary assignments, and a nursing ops module surface (registry, today’s allocation, unit shortcuts). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Services\Care\CarePermissions;
|
||||
use App\Services\Care\NursingServicesService;
|
||||
use App\Services\Care\RosterService;
|
||||
use App\Services\Care\SpecialtyModuleService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class NursingServicesController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function index(
|
||||
Request $request,
|
||||
NursingServicesService $nursing,
|
||||
RosterService $roster,
|
||||
SpecialtyModuleService $modules,
|
||||
): View {
|
||||
$this->authorizeAbility($request, 'nursing.services.view');
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$nurses = $nursing->nurseRegistry($organization, $owner);
|
||||
$allocations = $nursing->activeAllocations($organization, $owner);
|
||||
$todayRoster = $roster->todayAllocation($organization, $owner);
|
||||
$units = $nursing->careUnits($organization, $owner);
|
||||
$moduleEnabled = $modules->isEnabled($organization, 'nursing_services');
|
||||
|
||||
$labelMembers = $nurses
|
||||
->merge($allocations->pluck('member')->filter())
|
||||
->merge($todayRoster->pluck('member')->filter())
|
||||
->unique('id')
|
||||
->values();
|
||||
|
||||
$permissions = app(CarePermissions::class);
|
||||
$member = $this->member($request);
|
||||
|
||||
return view('care.nursing.services-hub', [
|
||||
'organization' => $organization,
|
||||
'nurses' => $nurses,
|
||||
'nurseLabels' => $nursing->memberLabels($labelMembers),
|
||||
'allocations' => $allocations,
|
||||
'todayRoster' => $todayRoster,
|
||||
'units' => $units,
|
||||
'moduleEnabled' => $moduleEnabled,
|
||||
'roles' => config('care.roles'),
|
||||
'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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?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\Member;
|
||||
use App\Models\RosterEntry;
|
||||
use App\Models\Shift;
|
||||
use App\Services\Care\CarePermissions;
|
||||
use App\Services\Care\RosterService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class RosterController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function show(Request $request, CareUnit $careUnit, RosterService $roster): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'nursing.roster.view');
|
||||
$this->authorizeOwner($request, $careUnit);
|
||||
abort_unless((int) $careUnit->organization_id === (int) $this->organization($request)->id, 404);
|
||||
|
||||
$weekStart = $request->filled('week')
|
||||
? Carbon::parse($request->input('week'))->startOfWeek(Carbon::MONDAY)
|
||||
: now()->startOfWeek(Carbon::MONDAY);
|
||||
|
||||
$grid = $roster->unitWeekGrid($careUnit, $this->ownerRef($request), $weekStart);
|
||||
$canManage = app(CarePermissions::class)->can($this->member($request), 'nursing.roster.manage');
|
||||
|
||||
return view('care.nursing.roster', [
|
||||
'unit' => $careUnit->load('department.branch'),
|
||||
'weekStart' => $grid['week_start'],
|
||||
'days' => $grid['days'],
|
||||
'shifts' => $grid['shifts'],
|
||||
'cells' => $grid['cells'],
|
||||
'members' => $grid['members'],
|
||||
'memberLabels' => $grid['member_labels'],
|
||||
'canManage' => $canManage,
|
||||
'statuses' => config('care.roster_entry_statuses'),
|
||||
'prevWeek' => $grid['week_start']->copy()->subWeek()->toDateString(),
|
||||
'nextWeek' => $grid['week_start']->copy()->addWeek()->toDateString(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request, CareUnit $careUnit, RosterService $roster): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'nursing.roster.manage');
|
||||
$this->authorizeOwner($request, $careUnit);
|
||||
$organization = $this->organization($request);
|
||||
abort_unless((int) $careUnit->organization_id === (int) $organization->id, 404);
|
||||
|
||||
$validated = $request->validate([
|
||||
'shift_id' => ['required', 'integer', 'exists:care_shifts,id'],
|
||||
'member_id' => ['required', 'integer', 'exists:care_members,id'],
|
||||
'duty_date' => ['required', 'date'],
|
||||
'notes' => ['nullable', 'string', 'max:1000'],
|
||||
]);
|
||||
|
||||
$shift = Shift::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->findOrFail($validated['shift_id']);
|
||||
$member = Member::owned($this->ownerRef($request))
|
||||
->where('organization_id', $organization->id)
|
||||
->findOrFail($validated['member_id']);
|
||||
|
||||
$roster->assign(
|
||||
$careUnit,
|
||||
$shift,
|
||||
$member,
|
||||
$validated['duty_date'],
|
||||
$this->ownerRef($request),
|
||||
$this->actorRef($request),
|
||||
$validated['notes'] ?? null,
|
||||
);
|
||||
|
||||
return redirect()
|
||||
->route('care.care-units.roster', [
|
||||
'careUnit' => $careUnit,
|
||||
'week' => Carbon::parse($validated['duty_date'])->startOfWeek(Carbon::MONDAY)->toDateString(),
|
||||
])
|
||||
->with('success', 'Staff added to roster.');
|
||||
}
|
||||
|
||||
public function destroy(Request $request, CareUnit $careUnit, RosterEntry $rosterEntry, RosterService $roster): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'nursing.roster.manage');
|
||||
$this->authorizeOwner($request, $careUnit);
|
||||
$this->authorizeOwner($request, $rosterEntry);
|
||||
abort_unless((int) $rosterEntry->care_unit_id === (int) $careUnit->id, 404);
|
||||
|
||||
$week = $rosterEntry->duty_date->copy()->startOfWeek(Carbon::MONDAY)->toDateString();
|
||||
$roster->cancel($rosterEntry, $this->ownerRef($request), $this->actorRef($request));
|
||||
|
||||
return redirect()
|
||||
->route('care.care-units.roster', ['careUnit' => $careUnit, 'week' => $week])
|
||||
->with('success', 'Roster entry cancelled.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Care;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Care\Concerns\ScopesToAccount;
|
||||
use App\Models\Shift;
|
||||
use App\Services\Care\AuditLogger;
|
||||
use App\Services\Care\RosterService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ShiftController extends Controller
|
||||
{
|
||||
use ScopesToAccount;
|
||||
|
||||
public function index(Request $request, RosterService $roster): View
|
||||
{
|
||||
$this->authorizeAbility($request, 'nursing.roster.manage');
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$roster->ensureDefaultShifts($organization, $owner);
|
||||
|
||||
$shifts = Shift::owned($owner)
|
||||
->where('organization_id', $organization->id)
|
||||
->orderBy('sort_order')
|
||||
->orderBy('label')
|
||||
->get();
|
||||
|
||||
return view('care.nursing.shifts-index', [
|
||||
'shifts' => $shifts,
|
||||
'organization' => $organization,
|
||||
]);
|
||||
}
|
||||
|
||||
public function store(Request $request): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'nursing.roster.manage');
|
||||
$organization = $this->organization($request);
|
||||
$owner = $this->ownerRef($request);
|
||||
|
||||
$validated = $request->validate([
|
||||
'code' => [
|
||||
'required', 'string', 'max:50', 'alpha_dash',
|
||||
Rule::unique('care_shifts', 'code')->where(fn ($q) => $q->where('organization_id', $organization->id)->whereNull('deleted_at')),
|
||||
],
|
||||
'label' => ['required', 'string', 'max:100'],
|
||||
'start_time' => ['required', 'date_format:H:i'],
|
||||
'end_time' => ['required', 'date_format:H:i'],
|
||||
'color' => ['nullable', 'string', 'max:20'],
|
||||
]);
|
||||
|
||||
$shift = Shift::create([
|
||||
'owner_ref' => $owner,
|
||||
'organization_id' => $organization->id,
|
||||
'code' => strtolower($validated['code']),
|
||||
'label' => $validated['label'],
|
||||
'start_time' => $validated['start_time'].':00',
|
||||
'end_time' => $validated['end_time'].':00',
|
||||
'color' => $validated['color'] ?? '#64748b',
|
||||
'sort_order' => ((int) Shift::query()->where('organization_id', $organization->id)->max('sort_order')) + 10,
|
||||
'is_active' => true,
|
||||
]);
|
||||
|
||||
AuditLogger::record($owner, 'shift.created', $organization->id, $owner, Shift::class, $shift->id);
|
||||
|
||||
return redirect()->route('care.shifts.index')->with('success', 'Shift created.');
|
||||
}
|
||||
|
||||
public function update(Request $request, Shift $shift): RedirectResponse
|
||||
{
|
||||
$this->authorizeAbility($request, 'nursing.roster.manage');
|
||||
$this->authorizeOwner($request, $shift);
|
||||
|
||||
$validated = $request->validate([
|
||||
'label' => ['required', 'string', 'max:100'],
|
||||
'start_time' => ['required', 'date_format:H:i'],
|
||||
'end_time' => ['required', 'date_format:H:i'],
|
||||
'color' => ['nullable', 'string', 'max:20'],
|
||||
'is_active' => ['boolean'],
|
||||
]);
|
||||
|
||||
$shift->update([
|
||||
'label' => $validated['label'],
|
||||
'start_time' => $validated['start_time'].':00',
|
||||
'end_time' => $validated['end_time'].':00',
|
||||
'color' => $validated['color'] ?? $shift->color,
|
||||
'is_active' => $request->boolean('is_active', true),
|
||||
]);
|
||||
|
||||
AuditLogger::record($this->ownerRef($request), 'shift.updated', $shift->organization_id, $this->ownerRef($request), Shift::class, $shift->id);
|
||||
|
||||
return redirect()->route('care.shifts.index')->with('success', 'Shift updated.');
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,12 @@ class SpecialtyModuleController extends Controller
|
||||
SpecialtyModuleService $modules,
|
||||
SpecialtyShellService $shell,
|
||||
CareQueueBridge $queueBridge,
|
||||
): View {
|
||||
): View|RedirectResponse {
|
||||
// Nursing Services is an ops hub (roster / registry), not a patient specialty shell.
|
||||
if ($module === 'nursing_services') {
|
||||
return redirect()->route('care.nursing.services');
|
||||
}
|
||||
|
||||
// Everyone lands on the specialty list/index — never auto-open a patient.
|
||||
return $this->renderShell($request, $module, 'visits', $modules, $shell, $queueBridge);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user