feat(roster): add legacy roster sync and propagation seeders

This commit is contained in:
2026-04-28 22:08:13 +08:00
parent caa7c19fec
commit 2c940ee7fd
3 changed files with 446 additions and 0 deletions

View File

@@ -0,0 +1,302 @@
{
"SEB2018-2008T": {
"1": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"2": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"4": {
"shift": "OPS 05:30PM-02:30AM",
"is_rd": false
},
"5": {
"shift": "OPS 05:30PM-02:30AM",
"is_rd": false
},
"3": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"6": {
"shift": null,
"is_rd": true
},
"0": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
}
},
"SEB2023-2068H": {
"2": {
"shift": "OPS 08:00AM-05:00PM",
"is_rd": false
},
"4": {
"shift": "OPS 08:00AM-05:00PM",
"is_rd": false
},
"6": {
"shift": null,
"is_rd": true
},
"0": {
"shift": "OPS 08:00AM-05:00PM",
"is_rd": false
},
"1": {
"shift": "OPS 08:00AM-05:00PM",
"is_rd": false
},
"3": {
"shift": "OPS 08:00AM-05:00PM",
"is_rd": false
},
"5": {
"shift": null,
"is_rd": true
}
},
"EMP-001": {
"1": {
"shift": "OPS 08:00AM-05:00PM",
"is_rd": false
},
"6": {
"shift": null,
"is_rd": true
},
"5": {
"shift": null,
"is_rd": true
},
"0": {
"shift": "OPS 08:00AM-05:00PM",
"is_rd": false
},
"2": {
"shift": "OPS 08:00AM-05:00PM",
"is_rd": false
},
"3": {
"shift": "OPS 08:00AM-05:00PM",
"is_rd": false
},
"4": {
"shift": "OPS 08:00AM-05:00PM",
"is_rd": false
}
},
"SEB2024-2109T": {
"1": {
"shift": null,
"is_rd": true
},
"6": {
"shift": null,
"is_rd": true
},
"0": {
"shift": null,
"is_rd": true
},
"2": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"3": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"4": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"5": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
}
},
"SEB2018-2024T": {
"6": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"0": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"1": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"2": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"3": {
"shift": null,
"is_rd": true
},
"4": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"5": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
}
},
"SEB2024-2101T": {
"6": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"0": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"1": {
"shift": null,
"is_rd": true
},
"2": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"3": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"4": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"5": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
}
},
"SEB2018-2023T": {
"6": {
"shift": null,
"is_rd": true
},
"0": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"1": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"2": {
"shift": "OPS 05:30PM-02:30AM",
"is_rd": false
},
"3": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"4": {
"shift": "OPS 05:30PM-02:30AM",
"is_rd": false
},
"5": {
"shift": "OPS 05:30PM-02:30AM",
"is_rd": false
}
},
"SEB2018-2013T": {
"6": {
"shift": null,
"is_rd": true
},
"0": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"1": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"2": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"3": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"4": {
"shift": "OPS 05:30PM-02:30AM",
"is_rd": false
},
"5": {
"shift": null,
"is_rd": true
}
},
"SEB2018-2033T": {
"6": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"0": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"1": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"2": {
"shift": null,
"is_rd": true
},
"3": {
"shift": null,
"is_rd": true
},
"4": {
"shift": "OPS 05:30PM-02:30AM",
"is_rd": false
},
"5": {
"shift": "OPS 05:30PM-02:30AM",
"is_rd": false
}
},
"SEB2024-2111T": {
"6": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"0": {
"shift": null,
"is_rd": true
},
"1": {
"shift": null,
"is_rd": true
},
"2": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"3": {
"shift": "OPS 03:30PM-12:30AM",
"is_rd": false
},
"4": {
"shift": "OPS 05:30PM-02:30AM",
"is_rd": false
},
"5": {
"shift": "OPS 05:30PM-02:30AM",
"is_rd": false
}
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\User;
use App\Models\Employee;
use App\Models\Shift;
use App\Models\AttendanceRecord;
use Illuminate\Support\Facades\File;
class LegacyRosterSyncSeeder extends Seeder
{
public function run()
{
$jsonPath = base_path('scratch/legacy_roster.json');
if (!File::exists($jsonPath)) {
$this->command->error("JSON file not found at $jsonPath");
return;
}
$roster = json_decode(File::get($jsonPath), true);
$total = count($roster);
$count = 0;
$this->command->getOutput()->progressStart($total);
foreach ($roster as $item) {
$employee = Employee::where('employee_id', $item['code'])->first();
if ($employee) {
$shift = null;
if ($item['shift_name']) {
$shift = Shift::where('name', $item['shift_name'])->first();
}
// Create or update the attendance record (Roster)
AttendanceRecord::updateOrCreate(
[
'employee_id' => $employee->user_id, // Attendance uses user_id as employee_id relationship
'date' => $item['date']
],
[
'shift_id' => $shift ? $shift->id : null,
'is_rest_day' => $item['is_rest_day'],
'status' => 'absent', // Roster placeholder
'created_by' => 1
]
);
}
$this->command->getOutput()->progressAdvance();
$count++;
}
$this->command->getOutput()->progressFinish();
$this->command->info("Synced $count roster records successfully.");
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\User;
use App\Models\Employee;
use App\Models\Shift;
use App\Models\AttendanceRecord;
use Illuminate\Support\Facades\File;
use Carbon\Carbon;
class PropagateRosterSeeder extends Seeder
{
public function run()
{
$jsonPath = database_path('data/employee_patterns.json');
if (!File::exists($jsonPath)) {
$this->command->error("Patterns file not found.");
return;
}
$patterns = json_decode(File::get($jsonPath), true);
// Months to seed: April and May 2026
$months = [
['year' => 2026, 'month' => 4],
['year' => 2026, 'month' => 5],
];
$employees = Employee::all();
$total = $employees->count();
foreach ($months as $m) {
$startDate = Carbon::create($m['year'], $m['month'], 1);
$endDate = $startDate->copy()->endOfMonth();
$this->command->info("Propagating patterns into " . $startDate->format('F Y') . "...");
$this->command->getOutput()->progressStart($total);
foreach ($employees as $emp) {
$pattern = $patterns[$emp->employee_id] ?? null;
if (!$pattern) {
$this->command->getOutput()->progressAdvance();
continue;
}
$current = $startDate->copy();
while ($current->lte($endDate)) {
$pyDow = $current->dayOfWeekIso - 1; // 0=Mon, 6=Sun
$assignment = $pattern[$pyDow] ?? null;
if ($assignment) {
$shift = null;
if ($assignment['shift']) {
$shift = Shift::where('name', $assignment['shift'])->first();
}
AttendanceRecord::updateOrCreate(
[
'employee_id' => $emp->user_id,
'date' => $current->toDateString()
],
[
'shift_id' => $shift ? $shift->id : null,
'is_rest_day' => $assignment['is_rd'],
'status' => 'absent',
'created_by' => 1
]
);
}
$current->addDay();
}
$this->command->getOutput()->progressAdvance();
}
$this->command->getOutput()->progressFinish();
}
$this->command->info("Propagated roster for April and May 2026 successfully.");
}
}