Version: 1.0
Last updated: December 10, 2025
Shift Management is the core administrative module for creating, managing and assigning work shifts in the DTR System. Administrators can:
Key files involved:
resources/views/users/admin/shift/ (index, create)App\Http\Controllers\Web\Admin\ShiftController.phpApp\Services\ShiftServices.phpApp\Models\Shift, App\Models\UserShiftPurpose:
Scope:
Out of scope:
Required permissions:
view_shifts — view shift list and detailscreate_shifts — create new shiftsedit_shifts — modify existing shiftsdelete_shifts — delete shift recordsassign_shifts — assign shifts to usersview_shift_assignments — view user shift historyRecommended roles:
Implementation note:
authorize() in controller methodsAdmin URLs:
/admin/shift/admin/shift/create/admin/shift/{id} (via AJAX or inline edit)/admin/shift/{id}/assign (POST)/admin/shift/{id}/remove (POST)/admin/shift/search (GET with filter param)Browser requirements: Modern Chromium-based browsers, recent Safari/Firefox. Mobile viewing supported; recommend desktop for full shift management.
Purpose: Display all shifts with filtering, search and bulk actions.
View file: resources/views/users/admin/shift/index.blade.php
UI elements:
Key features:
/admin/shift/search)Controller method: ShiftController::index(ShiftServices $shiftServices)
public function index(ShiftServices $shiftServices)
{
$shifts = $shiftServices->lists();
return view('users.admin.shift.index', compact('shifts'));
}
Purpose: Register a new shift template.
View file: resources/views/users/admin/shift/create.blade.php
Form fields (via ShiftStoreRequest):
Basic info:
Time Configuration:
Break Configuration:
Days Configuration:
Department & Assignment:
Settings:
On submit (store method):
ShiftStoreRequestController method:
public function store(ShiftStoreRequest $request, ShiftServices $shiftServices)
{
$shiftServices->create($request);
return back()->with(['message' => 'Shift Added']);
}
ShiftStoreRequest validation (expected):
namespace App\Http\Requests;
class ShiftStoreRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'code' => 'required|string|unique:shifts|max:50',
'description' => 'nullable|string',
'start_time' => 'required|date_format:H:i',
'end_time' => 'required|date_format:H:i|after:start_time',
'include_break' => 'nullable|boolean',
'break_start' => 'nullable|date_format:H:i|required_if:include_break,1',
'break_end' => 'nullable|date_format:H:i|required_if:include_break,1',
'days' => 'required|array|min:1',
'days.*' => 'in:monday,tuesday,wednesday,thursday,friday,saturday,sunday',
'department_id' => 'nullable|exists:departments,id',
'position_id' => 'nullable|exists:positions,id',
'status' => 'required|in:active,inactive',
'rotation_type' => 'nullable|in:fixed,rotating',
'max_users' => 'nullable|integer|min:1',
];
}
}
Purpose: View complete shift info and manage user assignments.
UI sections:
Shift model fields:
id — Primary key
code — Unique identifier (string, max 50)
name — Display name (string, max 255)
description — Optional details (text)
start_time — Shift start (time)
end_time — Shift end (time)
include_break — Has break (boolean)
break_start — Break start time (time, nullable)
break_end — Break end time (time, nullable)
shift_hours — Total hours (calculated, decimal)
days — JSON array of days (["monday", "tuesday", ...])
department_id — Foreign key to departments (nullable)
position_id — Foreign key to positions (nullable)
rotation_type — 'fixed' or 'rotating' (enum/string)
max_users — Capacity limit (integer, nullable)
priority — Scheduling priority (integer, nullable)
status — 'active' or 'inactive' (enum/string)
created_by — User ID of creator
created_at — Timestamp
updated_at — Timestamp
deleted_at — Soft delete timestamp (nullable)
UserShift model fields (pivot/join table):
id — Primary key
user_id — Foreign key to users
shift_id — Foreign key to shifts
assigned_at — Assignment timestamp
assigned_by — User ID of assigner
assigned_reason — Optional reason (text)
removed_at — Removal timestamp (nullable)
removed_by — User ID of remover (nullable)
removed_reason — Optional reason (text, nullable)
status — 'active', 'inactive', 'suspended' (enum)
created_at — Timestamp
updated_at — Timestamp
Sample records:
Shift:
id: 1
code: "MOR-001"
name: "Morning Shift"
description: "Standard morning shift for customer support"
start_time: "08:00"
end_time: "17:00"
include_break: true
break_start: "12:00"
break_end: "13:00"
shift_hours: 8.0
days: ["monday", "tuesday", "wednesday", "thursday", "friday"]
department_id: 2 (Support)
status: "active"
UserShift:
id: 1
user_id: 42 (Jane Doe)
shift_id: 1 (Morning Shift)
assigned_at: "2025-01-01 09:00:00"
assigned_by: 1 (Admin)
assigned_reason: "Regular assignment"
status: "active"
Standard Shift Types:
Morning Shift (06:00 – 14:00 or 08:00 – 17:00)
├─ Typical break: 30 min – 1 hour (mid-morning or lunch)
├─ Days: Usually Monday – Friday
├─ Departments: All (support, sales, operations, etc.)
└─ Use case: Standard business hours coverage
Evening Shift (14:00 – 22:00 or 17:00 – 01:00)
├─ Typical break: 30 min – 1 hour (mid-shift)
├─ Days: Usually Monday – Friday
├─ Departments: Customer support, operations
└─ Use case: Extended coverage, evening operations
Night Shift (22:00 – 06:00 or 23:00 – 07:00)
├─ Typical break: 30 min – 1 hour
├─ Days: Usually Monday – Friday or rotates
├─ Departments: Support, monitoring, security
└─ Use case: 24/7 coverage, emergency response
Flexible Shift (Customizable)
├─ No fixed start/end time
├─ Hours negotiated per employee
├─ Days: Varies per assignment
└─ Use case: Part-time, hybrid, freelance roles
Rotating Shift (Cycles through schedules)
├─ Week 1: Morning
├─ Week 2: Evening
├─ Week 3: Night
├─ Repeat: Every 3 weeks (configurable)
└─ Use case: Fair coverage distribution
Weekend/Holiday Shifts
├─ Saturday/Sunday only
├─ Higher pay (typically, via payroll)
├─ Days: Saturday, Sunday, holidays
└─ Use case: Retail, hospitality, healthcare
Custom Shift Configuration Example:
Name: "Flex Customer Support"
Code: FLEX-CUST-001
Start Time: 08:00
End Time: 18:00
Break: 12:00 – 13:00 (1 hour)
Days: Monday – Friday (Mon, Tue, Wed, Thu, Fri)
Department: Customer Support
Position: Support Agent
Rotation: Fixed
Max Users: 12
Priority: Medium
Status: Active
Notes:
"Flexible shift for customer support team.
Hours can be adjusted daily based on ticket volume.
Break timing flexible within 12:00-14:00 window."
Scenario: Assign "Morning Shift" to Jane Doe for 30 days.
Steps:
Server-side processing (via ShiftController::assignShift):
shift param requiredShiftServices::assignUserShift($userId, $shiftId)ShiftServices::selectShift($shiftId, $userId) (mark as selected)Controller method:
public function assignShift(Request $request, string $id, ShiftServices $shiftServices)
{
$request->validate([
'shift' => 'required'
]);
$userShift = $shiftServices->assignUserShift($id, $request->shift);
$shiftServices->selectShift($userShift->shift->id, $id);
return back()->with([
'message' => 'Shift Assign Success'
]);
}
Scenario: Remove Jane Doe from "Morning Shift" (e.g., due to leave).
Steps:
Server-side processing (via ShiftController::removeShift):
userShift (shift assignment ID) requiredShiftServices::removeUserShift($userId, $userShiftId)Controller method:
public function removeShift(Request $request, string $id, ShiftServices $shiftServices)
{
$shiftServices->removeUserShift($id, $request->userShift);
return back()->with([
'message' => 'Shift Remove Success'
]);
}
GET /api/admin/shifts
page, search, department_id, status{
"data": [
{
"id": 1,
"code": "MOR-001",
"name": "Morning Shift",
"start_time": "08:00",
"end_time": "17:00",
"shift_hours": 8.0,
"days": [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday"
],
"department": "Support",
"users_assigned": 7,
"status": "active"
}
],
"pagination": { "total": 12, "per_page": 25, "current_page": 1 }
}GET /api/admin/shifts/{id}
{
"id": 1,
"code": "MOR-001",
"name": "Morning Shift",
"description": "Standard morning shift",
"start_time": "08:00",
"end_time": "17:00",
"break_start": "12:00",
"break_end": "13:00",
"shift_hours": 8.0,
"days": ["monday", "tuesday", "wednesday", "thursday", "friday"],
"department": "Support",
"status": "active",
"assigned_users": [
{
"id": 42,
"name": "Jane Doe",
"email": "jane@example.com",
"assigned_at": "2025-01-01T00:00:00Z"
}
]
}POST /api/admin/shifts
{
"name": "New Shift",
"code": "NEW-001",
"start_time": "09:00",
"end_time": "18:00",
"include_break": true,
"break_start": "12:00",
"break_end": "13:00",
"days": ["monday", "tuesday", "wednesday"],
"status": "active"
}PUT /api/admin/shifts/{id}
DELETE /api/admin/shifts/{id}
POST /api/admin/shifts/{id}/assign
{
"user_ids": [42, 43, 44],
"start_date": "2025-01-01",
"end_date": "2025-01-31",
"reason": "Regular assignment"
}POST /api/admin/shifts/{id}/remove
{
"user_shift_id": 123,
"reason": "Leave approved"
}GET /admin/shift/search
filter (search term)public function search(Request $request, ShiftServices $shiftServices)
{
$filter = $request->filter;
$shifts = $shiftServices->get();
if($filter) {
$shifts = $shiftServices->search($filter);
}
return response([
'shifts' => $shifts,
]);
}
Location: app/Http/Controllers/Web/Admin/ShiftController.php
index() — List all shifts
public function index(ShiftServices $shiftServices)
{
$shifts = $shiftServices->lists();
return view('users.admin.shift.index', compact('shifts'));
}
create() — Show create form
public function create()
{
return view('users.admin.shift.create');
}
store() — Save new shift
public function store(ShiftStoreRequest $request, ShiftServices $shiftServices)
{
$shiftServices->create($request);
return back()->with(['message' => 'Shift Added']);
}
update() — Modify shift
public function update(Request $request, string $id, ShiftServices $shiftServices)
{
$shiftServices->update($request, $id);
return back()->with(['message' => 'Shift is Updated']);
}
destroy() — Delete shift
public function destroy(string $id, ShiftServices $shiftServices)
{
$shiftServices->delete($id);
return back()->with(['message' => 'Shift Deleted!']);
}
search() — Find shifts by filter
public function search(Request $request, ShiftServices $shiftServices)
{
$filter = $request->filter;
$shifts = $shiftServices->get();
if($filter) {
$shifts = $shiftServices->search($filter);
}
return response(['shifts' => $shifts]);
}
assignShift() — Assign shift to user
public function assignShift(Request $request, string $id, ShiftServices $shiftServices)
{
$request->validate(['shift' => 'required']);
$userShift = $shiftServices->assignUserShift($id, $request->shift);
$shiftServices->selectShift($userShift->shift->id, $id);
return back()->with(['message' => 'Shift Assign Success']);
}
removeShift() — Remove user from shift
public function removeShift(Request $request, string $id, ShiftServices $shiftServices)
{
$shiftServices->removeUserShift($id, $request->userShift);
return back()->with(['message' => 'Shift Remove Success']);
}
Location: app/Services/ShiftServices.php (expected)
lists() — Retrieve all shifts with pagination
public function lists()
{
return Shift::with('assignedUsers')
->paginate(25);
}
get() — Retrieve all shifts (no pagination)
public function get()
{
return Shift::all();
}
search($filter) — Search shifts by name, code, or time
public function search($filter)
{
return Shift::where('name', 'like', "%{$filter}%")
->orWhere('code', 'like', "%{$filter}%")
->orWhere('description', 'like', "%{$filter}%")
->get();
}
create($request) — Create new shift
public function create($request)
{
$validated = $request->validated();
// Calculate shift hours
$startTime = Carbon::parse($validated['start_time']);
$endTime = Carbon::parse($validated['end_time']);
$shiftHours = $startTime->diffInHours($endTime);
if ($validated['include_break'] ?? false) {
$breakStart = Carbon::parse($validated['break_start']);
$breakEnd = Carbon::parse($validated['break_end']);
$breakDuration = $breakStart->diffInHours($breakEnd);
$shiftHours -= $breakDuration;
}
return Shift::create([
'code' => $validated['code'],
'name' => $validated['name'],
'description' => $validated['description'] ?? null,
'start_time' => $validated['start_time'],
'end_time' => $validated['end_time'],
'include_break' => $validated['include_break'] ?? false,
'break_start' => $validated['break_start'] ?? null,
'break_end' => $validated['break_end'] ?? null,
'shift_hours' => $shiftHours,
'days' => json_encode($validated['days']),
'department_id' => $validated['department_id'] ?? null,
'status' => $validated['status'] ?? 'active',
'created_by' => auth()->id(),
]);
}
update($request, $id) — Modify shift
public function update($request, $id)
{
$shift = Shift::findOrFail($id);
$validated = $request->validated();
// Recalculate hours
$startTime = Carbon::parse($validated['start_time']);
$endTime = Carbon::parse($validated['end_time']);
$shiftHours = $startTime->diffInHours($endTime);
if ($validated['include_break'] ?? false) {
$breakStart = Carbon::parse($validated['break_start']);
$breakEnd = Carbon::parse($validated['break_end']);
$breakDuration = $breakStart->diffInHours($breakEnd);
$shiftHours -= $breakDuration;
}
$shift->update([
'name' => $validated['name'],
'start_time' => $validated['start_time'],
'end_time' => $validated['end_time'],
'include_break' => $validated['include_break'] ?? false,
'break_start' => $validated['break_start'] ?? null,
'break_end' => $validated['break_end'] ?? null,
'shift_hours' => $shiftHours,
'days' => json_encode($validated['days']),
'status' => $validated['status'] ?? 'active',
]);
return $shift;
}
delete($id) — Soft delete shift
public function delete($id)
{
$shift = Shift::findOrFail($id);
// Check if shift has active assignments
if ($shift->assignedUsers()->where('status', 'active')->exists()) {
throw new \Exception('Cannot delete shift with active assignments');
}
$shift->delete();
return true;
}
assignUserShift($userId, $shiftId) — Assign user to shift
public function assignUserShift($userId, $shiftId)
{
// Check for overlapping shifts
$user = User::findOrFail($userId);
$shift = Shift::findOrFail($shiftId);
// Validate no overlap with existing shifts for same days
// (Implementation depends on overlap logic)
return UserShift::create([
'user_id' => $userId,
'shift_id' => $shiftId,
'assigned_at' => now(),
'assigned_by' => auth()->id(),
'status' => 'active',
]);
}
selectShift($shiftId, $userId) — Mark shift as selected (after assignment)
public function selectShift($shiftId, $userId)
{
// Update pivot/join table or trigger related action
$userShift = UserShift::where('user_id', $userId)
->where('shift_id', $shiftId)
->first();
if ($userShift) {
$userShift->update(['status' => 'active']);
}
return $userShift;
}
removeUserShift($userId, $userShiftId) — Remove user from shift
public function removeUserShift($userId, $userShiftId)
{
$userShift = UserShift::findOrFail($userShiftId);
// Verify user matches
if ($userShift->user_id != $userId) {
throw new \Exception('User mismatch');
}
$userShift->update([
'status' => 'inactive',
'removed_at' => now(),
'removed_by' => auth()->id(),
]);
return $userShift;
}
Index filters:
Search functionality:
/admin/shift/search?filter={term}Reports:
Export formats: CSV, Excel, PDF (roster)
DTR Integration:
Scheduling Integration:
Payroll Integration:
Sync behavior:
Task 1: Create Morning Shift
Task 2: Assign Shift to Employee
Task 3: Bulk Assign Shift
Task 4: Remove User from Shift
Task 5: Generate Shift Roster
Audit logging:
Notifications:
Compliance:
Example 1: Support Department Scheduling
Shifts Created:
- MOR-001: Morning (08:00–17:00) Mon–Fri, Break 12:00–13:00, 8 hrs
- EVE-001: Evening (17:00–01:00) Mon–Fri, Break 21:00–22:00, 8 hrs
- NIG-001: Night (01:00–08:00) Mon–Fri, Break 04:00–05:00, 8 hrs
Assigned Users:
- Morning: Jane, John, Sarah, Mike, Anna (5 users)
- Evening: Robert, Lisa, Tom, Diana (4 users)
- Night: Kevin, Maya (2 users)
Total Coverage: 24/7 support for 5 business days
Capacity: 5 users per shift (average)
Utilization: 11 users / 3 shifts = 73% efficiency
Example 2: Rotating Shift Schedule
Shift: ROT-SUPP (Rotating Support)
- Schedule: 3-week rotation
- Week 1: Morning (MOR-001) Mon–Fri
- Week 2: Evening (EVE-001) Mon–Fri
- Week 3: Night (NIG-001) Mon–Fri
- Then repeat
Assigned: 15 users (5 per shift rotation)
- Users 1–5: Week 1 = Morning, Week 2 = Evening, Week 3 = Night
- Users 6–10: Week 2 = Morning, Week 3 = Evening, Week 1 = Night (offset)
- Users 11–15: Week 3 = Morning, Week 1 = Evening, Week 2 = Night (offset)
Fair distribution: Each user works all shifts equally
Unit tests:
Integration tests:
E2E tests (manual):
Performance tests:
Q: "Shift hours calculated incorrectly"
Q: "Cannot delete shift — says it has active assignments"
Q: "User assigned to two overlapping shifts"
Q: "Shift not appearing on /apps or DTR"
Q: "Bulk assign not working"
Q: "User received no notification of shift assignment"
SQL: Find active shifts for a department
SELECT s.* FROM shifts s
WHERE s.status = 'active'
AND s.department_id = 2
AND s.deleted_at IS NULL
ORDER BY s.start_time;
SQL: Find all users assigned to a shift
SELECT u.* FROM users u
JOIN user_shifts us ON u.id = us.user_id
WHERE us.shift_id = 1 AND us.status = 'active'
ORDER BY u.last_name;
SQL: Find overlapping shift assignments for a user
SELECT us1.*, us2.* FROM user_shifts us1
JOIN user_shifts us2 ON us1.user_id = us2.user_id
AND us1.id <> us2.id
AND us1.status = 'active'
AND us2.status = 'active'
WHERE us1.user_id = 42
AND (us1.shift_id <> us2.shift_id);
SQL: Count users per shift
SELECT s.name, s.code, COUNT(us.id) as assigned_users
FROM shifts s
LEFT JOIN user_shifts us ON s.id = us.shift_id AND us.status = 'active'
WHERE s.status = 'active'
GROUP BY s.id
ORDER BY assigned_users DESC;
Laravel Model: Shift with users relationship
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Shift extends Model
{
protected $fillable = [
'code', 'name', 'description', 'start_time', 'end_time',
'include_break', 'break_start', 'break_end', 'shift_hours',
'days', 'department_id', 'status', 'created_by'
];
protected $casts = [
'days' => 'array',
'include_break' => 'boolean',
];
public function assignedUsers(): BelongsToMany
{
return $this->belongsToMany(User::class, 'user_shifts')
->withPivot('assigned_at', 'assigned_by', 'status')
->where('user_shifts.status', 'active');
}
public function userShifts(): HasMany
{
return $this->hasMany(UserShift::class);
}
}
Laravel Model: UserShift
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class UserShift extends Model
{
protected $fillable = [
'user_id', 'shift_id', 'assigned_at', 'assigned_by',
'assigned_reason', 'removed_at', 'removed_by', 'removed_reason', 'status'
];
protected $casts = [
'assigned_at' => 'datetime',
'removed_at' => 'datetime',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function shift(): BelongsTo
{
return $this->belongsTo(Shift::class);
}
public function assignedByUser(): BelongsTo
{
return $this->belongsTo(User::class, 'assigned_by');
}
}
Blade template: Shift index table row
@foreach($shifts as $shift)
<tr>
<td>{{ $shift->code }}</td>
<td>{{ $shift->name }}</td>
<td>{{ $shift->start_time }} – {{ $shift->end_time }}</td>
<td>{{ implode(', ', $shift->days) }}</td>
<td>{{ $shift->assignedUsers()->count() }}</td>
<td>
<span class="badge {{ $shift->status === 'active' ? 'bg-green' : 'bg-gray' }}">
{{ ucfirst($shift->status) }}
</span>
</td>
<td>
<a href="{{ route('admin.shift.show', $shift->id) }}" class="btn btn-sm btn-info">View</a>
<button @click="assignModal($shift)" class="btn btn-sm btn-success">Assign</button>
<a href="{{ route('admin.shift.edit', $shift->id) }}" class="btn btn-sm btn-warning">Edit</a>
<form action="{{ route('admin.shift.destroy', $shift->id) }}" method="POST" style="display:inline;">
@csrf @method('DELETE')
<button type="submit" class="btn btn-sm btn-danger" onclick="return confirm('Delete?')">Delete</button>
</form>
</td>
</tr>
@endforeach
Document version: 1.0
Maintainers: HR / Scheduler / Engineering / Operations
Last updated: December 10, 2025