Skip to content

Commit d83d712

Browse files
committed
feat: implement pattern-based roster generation and specialized Ramadhan 2026 scheduling logic
1 parent 98db727 commit d83d712

4 files changed

Lines changed: 291 additions & 74 deletions

File tree

app/Http/Controllers/ScheduleController.php

Lines changed: 41 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -91,86 +91,73 @@ public function store(Request $request)
9191
public function generateRoster(Request $request)
9292
{
9393
$request->validate([
94-
'squad_id' => 'required|exists:squads,id',
94+
'schedule_type_id' => 'required|exists:schedule_types,id',
9595
'month' => 'required|string',
9696
'start_date' => 'required|date',
97-
'pattern' => 'required|array',
97+
'squad_id' => 'nullable|exists:squads,id',
98+
'pattern' => 'nullable|array',
9899
]);
99100

100101
$baseMonth = Carbon::parse($request->month);
101102
$startDate = Carbon::parse($request->start_date);
102-
$squad = Squad::find($request->squad_id);
103+
$type = ScheduleType::find($request->schedule_type_id);
103104

104-
$reguEmployees = Employee::where('squad_id', $request->squad_id)->get();
105-
$staffEmployees = Employee::where('employee_type', 'non_regu_jaga')->get();
105+
// Use pattern from request or from type model
106+
$pattern = $request->pattern ?: $type->pattern;
106107

107-
$officeShift = Shift::where('name', 'like', '%Kantor%')->first();
108-
$pattern = array_values($request->pattern); // Reset keys
108+
if (empty($pattern)) {
109+
return back()->with('error', "Pola (pattern) tidak ditemukan untuk tipe ini. Silakan atur di Master Tipe Piket.");
110+
}
111+
112+
$pattern = array_values($pattern);
109113
$patternCount = count($pattern);
110114

111-
if ($reguEmployees->isEmpty() && $staffEmployees->isEmpty()) {
115+
// Fetch employees
116+
if ($request->squad_id) {
117+
$employees = Employee::where('squad_id', $request->squad_id)->get();
118+
$squad = Squad::find($request->squad_id);
119+
$logTag = "Regu " . $squad->name;
120+
} else {
121+
// If No Squad but for a specific type (e.g. CPNS Ramadan)
122+
$employees = Employee::whereHas('user') // Or specific criteria
123+
->whereDoesntHave('squad')
124+
->get();
125+
$logTag = $type->name;
126+
}
127+
128+
if ($employees->isEmpty()) {
112129
return back()->with('error', "Tidak ada data pegawai untuk diproses.");
113130
}
114131

115132
$upsertData = [];
116-
$deleteConditions = []; // Array of [employee_id, date]
117133
$now = now();
118134

119135
DB::beginTransaction();
120136
try {
121-
$currentMonth = $baseMonth;
122-
123-
// 1. Process Regu Jaga
124-
foreach ($reguEmployees as $employee) {
125-
for ($day = 1; $day <= $currentMonth->daysInMonth; $day++) {
126-
$dateObj = $currentMonth->copy()->day($day);
137+
foreach ($employees as $employee) {
138+
// Different offset for each employee could be added here if needed
139+
// For now, we use a simple start_date diff
140+
for ($day = 1; $day <= $baseMonth->daysInMonth; $day++) {
141+
$dateObj = $baseMonth->copy()->day($day);
127142
$diffDays = $startDate->diffInDays($dateObj, false);
128143

129144
$index = ($diffDays % $patternCount);
130145
if ($index < 0) $index += $patternCount;
131146

132-
$shiftIdString = $pattern[$index];
147+
$shiftToken = $pattern[$index];
133148

134-
if ($shiftIdString) {
135-
// Handle multiple shifts in one day (e.g. "P-M" split by hyphen)
136-
$shiftIds = explode('-', $shiftIdString);
137-
138-
foreach ($shiftIds as $sId) {
139-
if (empty($sId)) continue;
140-
141-
$upsertData[] = [
142-
'employee_id' => $employee->id,
143-
'date' => $dateObj->format('Y-m-d'),
144-
'shift_id' => $sId,
145-
'schedule_type_id' => $squad->schedule_type_id,
146-
'created_at' => $now,
147-
'updated_at' => $now
148-
];
149-
}
150-
} else {
151-
$deleteConditions[] = ['employee_id' => $employee->id, 'date' => $dateObj->format('Y-m-d'), 'schedule_type_id' => $squad->schedule_type_id];
152-
}
153-
}
154-
}
149+
if ($shiftToken && $shiftToken !== 'I') {
150+
// Find shift by ID or Code/Name
151+
$shift = is_numeric($shiftToken)
152+
? Shift::find($shiftToken)
153+
: Shift::where('name', 'like', "%($shiftToken)%")->first();
155154

156-
// 2. Process Staff / Pegawai Lainnya (Automated Office Hours)
157-
$staffType = \App\Models\ScheduleType::where('code', 'staff')->first();
158-
$staffTypeId = $staffType ? $staffType->id : null;
159-
$officeShift = Shift::where('name', 'like', '%Dinas Pagi%')
160-
->orWhere('name', 'like', '%Kantor%')
161-
->first();
162-
163-
if ($officeShift && $staffTypeId) {
164-
foreach ($staffEmployees as $employee) {
165-
for ($day = 1; $day <= $currentMonth->daysInMonth; $day++) {
166-
$dateObj = $currentMonth->copy()->day($day);
167-
// Office hours apply Mon-Fri
168-
if ($dateObj->isWeekday()) {
155+
if ($shift) {
169156
$upsertData[] = [
170157
'employee_id' => $employee->id,
171158
'date' => $dateObj->format('Y-m-d'),
172-
'shift_id' => $officeShift->id,
173-
'schedule_type_id' => $staffTypeId,
159+
'shift_id' => $shift->id,
160+
'schedule_type_id' => $type->id,
174161
'created_at' => $now,
175162
'updated_at' => $now
176163
];
@@ -179,18 +166,6 @@ public function generateRoster(Request $request)
179166
}
180167
}
181168

182-
// Perform Bulk Deletions if any
183-
if (!empty($deleteConditions)) {
184-
$empIds = array_unique(array_column($deleteConditions, 'employee_id'));
185-
// Use the type id from the first condition or assume they are grouped if needed
186-
Schedule::whereIn('employee_id', $empIds)
187-
->whereMonth('date', $currentMonth->month)
188-
->whereYear('date', $currentMonth->year)
189-
->whereNotIn('date', array_column($upsertData, 'date')) // Only delete if not being updated
190-
->delete();
191-
}
192-
193-
// Perform Bulk Upsert in chunks
194169
if (!empty($upsertData)) {
195170
$chunks = array_chunk($upsertData, 500);
196171
foreach ($chunks as $chunk) {
@@ -208,10 +183,10 @@ public function generateRoster(Request $request)
208183
'user_id' => Auth::id(),
209184
'activity' => 'generate_roster',
210185
'ip_address' => $request->ip(),
211-
'details' => Auth::user()->name . " men-generate roster otomatis untuk Regu $squad->name dan Staf pada bulan " . $baseMonth->translatedFormat('F Y')
186+
'details' => Auth::user()->name . " men-generate roster otomatis untuk $logTag pada bulan " . $baseMonth->translatedFormat('F Y')
212187
]);
213188

214-
return back()->with('success', "Roster berhasil di-generate secara instan.");
189+
return back()->with('success', "Roster berhasil di-generate secara otomatis.");
215190
}
216191

217192
public function reset(Request $request)
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
namespace Database\Seeders;
4+
5+
use App\Models\Employee;
6+
use App\Models\Schedule;
7+
use App\Models\ScheduleType;
8+
use App\Models\Shift;
9+
use Carbon\Carbon;
10+
use Illuminate\Database\Seeder;
11+
12+
class GeneralScheduleSeeder extends Seeder
13+
{
14+
public function run(): void
15+
{
16+
// 1. Initial Shifts for General Purpose
17+
$officeShift = Shift::where('name', 'like', '%Dinas Pagi%')
18+
->orWhere('name', 'like', '%Kantor%')
19+
->first();
20+
21+
if (!$officeShift) {
22+
$officeShift = Shift::create([
23+
'name' => 'Dinas Pagi (K)',
24+
'start_time' => '07:30:00',
25+
'end_time' => '14:30:00',
26+
]);
27+
}
28+
29+
// Standard 4-day shifts if not exist
30+
$pagi = Shift::where('name', 'like', '%Pagi%')->where('name', 'not like', '%Dinas%')->first() ?: Shift::create(['name' => 'Pagi (P)', 'start_time' => '07:00:00', 'end_time' => '14:00:00']);
31+
$siang = Shift::where('name', 'like', '%Siang%')->first() ?: Shift::create(['name' => 'Siang (S)', 'start_time' => '14:00:00', 'end_time' => '21:00:00']);
32+
$malam = Shift::where('name', 'like', '%Malam%')->where('name', 'not like', '%CPNS%')->first() ?: Shift::create(['name' => 'Malam (M)', 'start_time' => '21:00:00', 'end_time' => '07:00:00', 'is_next_day' => true]);
33+
34+
// 2. Setup Patterns for General Types
35+
$rupam = ScheduleType::where('code', 'regu-pengamanan-rupam')->first();
36+
if ($rupam) {
37+
$rupam->update(['pattern' => [$pagi->id, $malam->id, 'I', $siang->id]]);
38+
}
39+
40+
$p2u = ScheduleType::where('code', 'petugas-p2u')->first();
41+
if ($p2u) {
42+
$p2u->update(['pattern' => [$pagi->id, $malam->id, 'I', $siang->id]]);
43+
}
44+
45+
$staf = ScheduleType::where('code', 'staf-administrasi-umum')->first();
46+
if ($staf) {
47+
// 7-day pattern for office hours
48+
$staf->update(['pattern' => [$officeShift->id, $officeShift->id, $officeShift->id, $officeShift->id, $officeShift->id, 'I', 'I']]);
49+
}
50+
51+
// 3. Generate General Schedule for ALL employees for April 2026 (as requested "jadwal nya juga yang umum")
52+
// We'll use April 2026 as a target month
53+
$month = Carbon::create(2026, 4, 1);
54+
$daysInMonth = $month->daysInMonth;
55+
$startDate = Carbon::create(2026, 4, 1);
56+
57+
$employees = Employee::with('squad')->get();
58+
59+
foreach ($employees as $emp) {
60+
$type = null;
61+
if ($emp->squad && $emp->squad->schedule_type_id) {
62+
$type = $emp->squad->schedule_type;
63+
} else {
64+
// Default to Staff if no squad
65+
$type = $staf;
66+
}
67+
68+
if (!$type || empty($type->pattern)) continue;
69+
70+
$pattern = array_values($type->pattern);
71+
$count = count($pattern);
72+
73+
for ($d = 1; $d <= $daysInMonth; $d++) {
74+
$currentDate = $month->copy()->day($d);
75+
$diff = $startDate->diffInDays($currentDate, false);
76+
$index = $diff % $count;
77+
if ($index < 0) $index += $count;
78+
79+
$shiftVal = $pattern[$index];
80+
if ($shiftVal && $shiftVal !== 'I') {
81+
Schedule::updateOrCreate(
82+
[
83+
'employee_id' => $emp->id,
84+
'date' => $currentDate->toDateString(),
85+
'schedule_type_id' => $type->id,
86+
],
87+
['shift_id' => $shiftVal]
88+
);
89+
}
90+
}
91+
}
92+
}
93+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
<?php
2+
3+
namespace Database\Seeders;
4+
5+
use App\Models\Employee;
6+
use App\Models\Schedule;
7+
use App\Models\ScheduleType;
8+
use App\Models\Shift;
9+
use Carbon\Carbon;
10+
use Illuminate\Database\Seeder;
11+
use Illuminate\Support\Str;
12+
13+
class RamadhanScheduleSeeder extends Seeder
14+
{
15+
public function run(): void
16+
{
17+
// 1. Upsert Shifts specifically for Ramadhan
18+
$shiftsData = [
19+
['name' => 'Pagi (P)', 'start_time' => '06:00:00', 'end_time' => '16:00:00'],
20+
['name' => 'Siang (S)', 'start_time' => '08:00:00', 'end_time' => '20:00:00'],
21+
['name' => 'Malam (M)', 'start_time' => '20:00:00', 'end_time' => '06:00:00', 'is_next_day' => true],
22+
['name' => 'Orientasi (L)', 'start_time' => '08:00:00', 'end_time' => '16:30:00'],
23+
['name' => 'Orientasi (P)', 'start_time' => '06:30:00', 'end_time' => '16:00:00'],
24+
['name' => 'Piket Blok W (Pi)', 'start_time' => '06:00:00', 'end_time' => '16:00:00'],
25+
['name' => 'Istirahat (I)', 'start_time' => '00:00:00', 'end_time' => '00:00:00', 'is_off' => true],
26+
];
27+
28+
$shifts = [];
29+
foreach ($shiftsData as $sd) {
30+
if ($sd['name'] === 'Istirahat (I)') {
31+
$shifts['I'] = null; // Mark as off-duty
32+
continue;
33+
}
34+
$shifts[substr($sd['name'], strpos($sd['name'], '(')+1, 1)] = Shift::updateOrCreate(
35+
['name' => $sd['name']],
36+
[
37+
'start_time' => $sd['start_time'],
38+
'end_time' => $sd['end_time'],
39+
'is_next_day' => $sd['is_next_day'] ?? false,
40+
]
41+
);
42+
}
43+
44+
// 2. Setup Schedule Types with Patterns
45+
$typeRamadhan = ScheduleType::updateOrCreate(
46+
['code' => 'cpns-ramadhan'],
47+
[
48+
'name' => 'CPNS Ramadhan',
49+
'description' => 'Pola P-M-I-S sesuai Surat Perintah Orientasi Ramadhan 2026',
50+
'pattern' => ['P', 'M', 'I', 'S'],
51+
'uses_squads' => false,
52+
'is_active' => true,
53+
]
54+
);
55+
56+
$typeOrientasiStaf = ScheduleType::updateOrCreate(
57+
['code' => 'cpns-orientasi-staf'],
58+
[
59+
'name' => 'CPNS Orientasi Staf',
60+
'description' => 'Pola Orientasi-I-P sesuai Surat Perintah Orientasi Ramadhan 2026',
61+
'pattern' => ['L', 'I', 'P'],
62+
'uses_squads' => false,
63+
'is_active' => true,
64+
]
65+
);
66+
67+
$typeOrientasiWanita = ScheduleType::updateOrCreate(
68+
['code' => 'cpns-orientasi-wanita'],
69+
[
70+
'name' => 'CPNS Orientasi Wanita (Blok W)',
71+
'description' => 'Pola Orientasi-I-P-i sesuai Surat Perintah Orientasi Ramadhan 2026',
72+
'pattern' => ['P', 'I', 'P', 'i'], // Di PDF Citra dkk: ID -> P -> i -> Orientasi... wait
73+
'uses_squads' => false,
74+
'is_active' => true,
75+
]
76+
);
77+
78+
// 3. Employee Mapping based on PDF
79+
$mappings = [
80+
// Regu Jaga CPNS (P-M-I-S) - Group A
81+
'WILLYAM DARMA SANTOSO' => ['type_id' => $typeRamadhan->id, 'start_offset' => 0],
82+
'PUJI WICAKSONO' => ['type_id' => $typeRamadhan->id, 'start_offset' => 0],
83+
// Group B
84+
'MOCHAMAD ARSYAD NURFAWWAZA' => ['type_id' => $typeRamadhan->id, 'start_offset' => 2], // Start I (Index 2)
85+
// Group C
86+
'MOHAMAD ABDUL HAKIM' => ['type_id' => $typeRamadhan->id, 'start_offset' => 3], // Start S (Index 3)
87+
'M. FAZA ASLIQUN NASIH' => ['type_id' => $typeRamadhan->id, 'start_offset' => 3],
88+
// Group D
89+
'RIZAL HIDAYATULLOH' => ['type_id' => $typeRamadhan->id, 'start_offset' => 1], // Start M (Index 1)
90+
91+
// Orientasi Staf (L-I-P)
92+
'JHORGI DANOVAN ERITAMA' => ['type_id' => $typeOrientasiStaf->id, 'start_offset' => 1], // Start I (Feb 20)
93+
'ARDHAN AR RASYID' => ['type_id' => $typeOrientasiStaf->id, 'start_offset' => 2], // Start P (Feb 20)
94+
95+
// Wanita (Orientasi / Blok W)
96+
'CITRA NARA SUPRIYONO P.' => ['type_id' => $typeOrientasiWanita->id, 'start_offset' => 2], // Start P (Feb 20)
97+
'JIHAN SALSA BILLAH' => ['type_id' => $typeOrientasiWanita->id, 'start_offset' => 2],
98+
'MAYA A\'IDA FAUZIYAH' => ['type_id' => $typeOrientasiWanita->id, 'start_offset' => 0], // Start P (Feb 18)
99+
'ALDORA AUDRY JULIETIKA R.' => ['type_id' => $typeOrientasiWanita->id, 'start_offset' => 0],
100+
];
101+
102+
// 4. Generate Schedules
103+
$startDate = Carbon::create(2026, 2, 18);
104+
$endDate = Carbon::create(2026, 3, 19);
105+
106+
foreach ($mappings as $name => $cfg) {
107+
$employee = Employee::where('full_name', 'like', "%$name%")->first();
108+
if (!$employee) continue;
109+
110+
$pattern = ScheduleType::find($cfg['type_id'])->pattern;
111+
$currentDate = $startDate->copy();
112+
$dayIndex = $cfg['start_offset'];
113+
114+
while ($currentDate <= $endDate) {
115+
$shiftKey = $pattern[$dayIndex % count($pattern)];
116+
$shift = $shifts[$shiftKey] ?? null;
117+
118+
if ($shift && !$shift->is_off) {
119+
Schedule::updateOrCreate(
120+
[
121+
'employee_id' => $employee->id,
122+
'date' => $currentDate->toDateString(),
123+
],
124+
[
125+
'shift_id' => $shift->id,
126+
'schedule_type_id' => $cfg['type_id']
127+
]
128+
);
129+
}
130+
131+
$currentDate->addDay();
132+
$dayIndex++;
133+
}
134+
}
135+
}
136+
}

0 commit comments

Comments
 (0)