Skip to content

Commit 5d45942

Browse files
committed
feat(hr): Phase 100 — Work Schedules & Shift Management
https://claude.ai/code/session_01RdUGwo74JXChRCF88Yu27d
1 parent 3fb9455 commit 5d45942

17 files changed

Lines changed: 990 additions & 142 deletions
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace App\Modules\HR\Http\Controllers;
4+
5+
use App\Http\Controllers\Controller;
6+
use App\Modules\HR\Models\EmployeeSchedule;
7+
use Illuminate\Http\RedirectResponse;
8+
use Illuminate\Http\Request;
9+
use Inertia\Inertia;
10+
use Inertia\Response;
11+
12+
class EmployeeScheduleController extends Controller
13+
{
14+
public function index(Request $request): Response
15+
{
16+
$this->authorize('viewAny', EmployeeSchedule::class);
17+
18+
$assignments = EmployeeSchedule::with(['employee', 'schedule'])
19+
->when($request->employee_id, fn ($q) => $q->where('employee_id', $request->employee_id))
20+
->orderByDesc('created_at')
21+
->paginate(20)
22+
->withQueryString();
23+
24+
return Inertia::render('HR/EmployeeSchedules/Index', [
25+
'assignments' => $assignments,
26+
'filters' => $request->only(['employee_id']),
27+
]);
28+
}
29+
30+
public function store(Request $request): RedirectResponse
31+
{
32+
$this->authorize('create', EmployeeSchedule::class);
33+
34+
$validated = $request->validate([
35+
'employee_id' => 'required|exists:employees,id',
36+
'work_schedule_id' => 'required|exists:work_schedules,id',
37+
'effective_from' => 'required|date',
38+
'effective_to' => 'nullable|date',
39+
'is_active' => 'nullable|boolean',
40+
]);
41+
42+
EmployeeSchedule::create([
43+
'tenant_id' => auth()->user()->tenant_id,
44+
'employee_id' => $validated['employee_id'],
45+
'work_schedule_id' => $validated['work_schedule_id'],
46+
'effective_from' => $validated['effective_from'],
47+
'effective_to' => $validated['effective_to'] ?? null,
48+
'is_active' => $validated['is_active'] ?? true,
49+
]);
50+
51+
return redirect()->back();
52+
}
53+
54+
public function destroy(EmployeeSchedule $employeeSchedule): RedirectResponse
55+
{
56+
$this->authorize('delete', $employeeSchedule);
57+
58+
$employeeSchedule->delete();
59+
60+
return redirect()->back();
61+
}
62+
}

erp/app/Modules/HR/Http/Controllers/WorkScheduleController.php

Lines changed: 54 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,65 +4,86 @@
44

55
use App\Http\Controllers\Controller;
66
use App\Modules\HR\Models\WorkSchedule;
7+
use App\Modules\HR\Models\WorkScheduleShift;
78
use Illuminate\Http\RedirectResponse;
89
use Illuminate\Http\Request;
910
use Inertia\Inertia;
1011
use Inertia\Response;
1112

1213
class WorkScheduleController extends Controller
1314
{
14-
public function index(): Response
15+
public function index(Request $request): Response
1516
{
1617
$this->authorize('viewAny', WorkSchedule::class);
1718

18-
$schedules = WorkSchedule::orderBy('name')->get();
19+
$schedules = WorkSchedule::query()
20+
->when($request->has('is_active') && $request->is_active !== null, fn ($q) => $q->where('is_active', $request->boolean('is_active')))
21+
->orderBy('name')
22+
->paginate(20)
23+
->withQueryString();
1924

20-
return Inertia::render('HR/WorkSchedules/Index', compact('schedules'));
21-
}
22-
23-
public function create(): Response
24-
{
25-
$this->authorize('create', WorkSchedule::class);
26-
27-
return Inertia::render('HR/WorkSchedules/Create');
25+
return Inertia::render('HR/WorkSchedules/Index', [
26+
'schedules' => $schedules,
27+
'filters' => $request->only(['is_active']),
28+
]);
2829
}
2930

3031
public function store(Request $request): RedirectResponse
3132
{
3233
$this->authorize('create', WorkSchedule::class);
3334

34-
$data = $request->validate([
35-
'name' => ['required', 'string', 'max:255'],
36-
'is_default' => ['boolean'],
37-
'monday_start' => ['nullable', 'date_format:H:i'],
38-
'monday_end' => ['nullable', 'date_format:H:i', 'after:monday_start'],
39-
'tuesday_start' => ['nullable', 'date_format:H:i'],
40-
'tuesday_end' => ['nullable', 'date_format:H:i', 'after:tuesday_start'],
41-
'wednesday_start' => ['nullable', 'date_format:H:i'],
42-
'wednesday_end' => ['nullable', 'date_format:H:i', 'after:wednesday_start'],
43-
'thursday_start' => ['nullable', 'date_format:H:i'],
44-
'thursday_end' => ['nullable', 'date_format:H:i', 'after:thursday_start'],
45-
'friday_start' => ['nullable', 'date_format:H:i'],
46-
'friday_end' => ['nullable', 'date_format:H:i', 'after:friday_start'],
47-
'saturday_start' => ['nullable', 'date_format:H:i'],
48-
'saturday_end' => ['nullable', 'date_format:H:i', 'after:saturday_start'],
49-
'sunday_start' => ['nullable', 'date_format:H:i'],
50-
'sunday_end' => ['nullable', 'date_format:H:i', 'after:sunday_start'],
35+
$validated = $request->validate([
36+
'name' => 'required|string|max:255',
37+
'timezone' => 'nullable|string',
38+
'hours_per_week' => 'nullable|integer|min:1|max:168',
39+
'is_active' => 'nullable|boolean',
40+
'description' => 'nullable|string',
5141
]);
5242

53-
$schedule = WorkSchedule::create([
54-
'tenant_id' => auth()->user()->tenant_id,
55-
...$data,
43+
WorkSchedule::create([
44+
'tenant_id' => auth()->user()->tenant_id,
45+
'name' => $validated['name'],
46+
'timezone' => $validated['timezone'] ?? 'UTC',
47+
'hours_per_week' => $validated['hours_per_week'] ?? 40,
48+
'is_active' => $validated['is_active'] ?? true,
49+
'description' => $validated['description'] ?? null,
5650
]);
5751

58-
return redirect()->route('hr.work-schedules.show', $schedule)->with('success', 'Work schedule created.');
52+
return redirect()->back();
5953
}
6054

6155
public function show(WorkSchedule $workSchedule): Response
6256
{
6357
$this->authorize('view', $workSchedule);
6458

65-
return Inertia::render('HR/WorkSchedules/Show', compact('workSchedule'));
59+
$workSchedule->load(['shifts', 'assignments.employee']);
60+
61+
return Inertia::render('HR/WorkSchedules/Show', [
62+
'workSchedule' => $workSchedule,
63+
]);
64+
}
65+
66+
public function addShift(Request $request, WorkSchedule $workSchedule): RedirectResponse
67+
{
68+
$this->authorize('update', $workSchedule);
69+
70+
$validated = $request->validate([
71+
'day_of_week' => 'required|in:monday,tuesday,wednesday,thursday,friday,saturday,sunday',
72+
'start_time' => 'required|date_format:H:i',
73+
'end_time' => 'required|date_format:H:i',
74+
'break_minutes' => 'nullable|numeric|min:0',
75+
]);
76+
77+
WorkScheduleShift::create([
78+
'tenant_id' => auth()->user()->tenant_id,
79+
'work_schedule_id' => $workSchedule->id,
80+
'day_of_week' => $validated['day_of_week'],
81+
'start_time' => $validated['start_time'],
82+
'end_time' => $validated['end_time'],
83+
'break_minutes' => $validated['break_minutes'] ?? 0,
84+
]);
85+
86+
return redirect()->back();
6687
}
6788

6889
public function destroy(WorkSchedule $workSchedule): RedirectResponse
@@ -71,6 +92,6 @@ public function destroy(WorkSchedule $workSchedule): RedirectResponse
7192

7293
$workSchedule->delete();
7394

74-
return redirect()->route('hr.work-schedules.index')->with('success', 'Work schedule deleted.');
95+
return redirect()->back();
7596
}
7697
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace App\Modules\HR\Models;
4+
5+
use App\Modules\Core\Traits\BelongsToTenant;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
8+
use Illuminate\Support\Carbon;
9+
10+
class EmployeeSchedule extends Model
11+
{
12+
use BelongsToTenant;
13+
14+
protected $fillable = [
15+
'tenant_id',
16+
'employee_id',
17+
'work_schedule_id',
18+
'effective_from',
19+
'effective_to',
20+
'is_active',
21+
];
22+
23+
protected $casts = [
24+
'effective_from' => 'date',
25+
'effective_to' => 'date',
26+
'is_active' => 'boolean',
27+
];
28+
29+
public function employee(): BelongsTo
30+
{
31+
return $this->belongsTo(Employee::class);
32+
}
33+
34+
public function schedule(): BelongsTo
35+
{
36+
return $this->belongsTo(WorkSchedule::class, 'work_schedule_id');
37+
}
38+
39+
public function getIsCurrentAttribute(): bool
40+
{
41+
if (! $this->is_active) {
42+
return false;
43+
}
44+
45+
$today = Carbon::today();
46+
47+
if ($this->effective_from->gt($today)) {
48+
return false;
49+
}
50+
51+
if ($this->effective_to !== null && $this->effective_to->lt($today)) {
52+
return false;
53+
}
54+
55+
return true;
56+
}
57+
}

erp/app/Modules/HR/Models/WorkSchedule.php

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use App\Modules\Core\Traits\BelongsToTenant;
66
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Database\Eloquent\Relations\HasMany;
78
use Illuminate\Database\Eloquent\SoftDeletes;
89

910
class WorkSchedule extends Model
@@ -14,6 +15,11 @@ class WorkSchedule extends Model
1415
protected $fillable = [
1516
'tenant_id',
1617
'name',
18+
'timezone',
19+
'hours_per_week',
20+
'is_active',
21+
'description',
22+
// Legacy fields kept for backward compatibility
1723
'monday_start',
1824
'monday_end',
1925
'tuesday_start',
@@ -32,9 +38,26 @@ class WorkSchedule extends Model
3238
];
3339

3440
protected $casts = [
35-
'is_default' => 'boolean',
41+
'hours_per_week' => 'integer',
42+
'is_active' => 'boolean',
43+
'is_default' => 'boolean',
3644
];
3745

46+
public function shifts(): HasMany
47+
{
48+
return $this->hasMany(WorkScheduleShift::class);
49+
}
50+
51+
public function assignments(): HasMany
52+
{
53+
return $this->hasMany(EmployeeSchedule::class);
54+
}
55+
56+
public function getShiftCountAttribute(): int
57+
{
58+
return $this->shifts()->count();
59+
}
60+
3861
public function scopeDefault($query)
3962
{
4063
return $query->where('is_default', true);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace App\Modules\HR\Models;
4+
5+
use App\Modules\Core\Traits\BelongsToTenant;
6+
use Carbon\Carbon;
7+
use Illuminate\Database\Eloquent\Model;
8+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
9+
10+
class WorkScheduleShift extends Model
11+
{
12+
use BelongsToTenant;
13+
14+
protected $fillable = [
15+
'tenant_id',
16+
'work_schedule_id',
17+
'day_of_week',
18+
'start_time',
19+
'end_time',
20+
'break_minutes',
21+
];
22+
23+
protected $casts = [
24+
'break_minutes' => 'float',
25+
];
26+
27+
public function schedule(): BelongsTo
28+
{
29+
return $this->belongsTo(WorkSchedule::class, 'work_schedule_id');
30+
}
31+
32+
public function getHoursAttribute(): float
33+
{
34+
$start = Carbon::createFromFormat('H:i', substr($this->start_time, 0, 5));
35+
$end = Carbon::createFromFormat('H:i', substr($this->end_time, 0, 5));
36+
37+
$totalMinutes = $start->diffInMinutes($end);
38+
$workMinutes = $totalMinutes - (float) $this->break_minutes;
39+
40+
return round($workMinutes / 60, 2);
41+
}
42+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace App\Modules\HR\Policies;
4+
5+
use App\Models\User;
6+
use Illuminate\Database\Eloquent\Model;
7+
8+
class WorkSchedulePolicy
9+
{
10+
public function viewAny(User $user): bool
11+
{
12+
return $user->can('hr.view');
13+
}
14+
15+
public function view(User $user, Model $model): bool
16+
{
17+
return $user->can('hr.view');
18+
}
19+
20+
public function create(User $user): bool
21+
{
22+
return $user->can('hr.create');
23+
}
24+
25+
public function update(User $user, Model $model): bool
26+
{
27+
return $user->can('hr.create');
28+
}
29+
30+
public function delete(User $user, Model $model): bool
31+
{
32+
return $user->can('hr.delete');
33+
}
34+
}

0 commit comments

Comments
 (0)