From 0e5ce80ed3f25e67f9262910b165dbda92dbcd69 Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 30 May 2026 11:22:28 +0800 Subject: [PATCH] feat: add fix_production_attendance.php script --- fix_production_attendance.php | 128 ++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 fix_production_attendance.php diff --git a/fix_production_attendance.php b/fix_production_attendance.php new file mode 100644 index 000000000..669bd9bc1 --- /dev/null +++ b/fix_production_attendance.php @@ -0,0 +1,128 @@ +make(Illuminate\Contracts\Console\Kernel::class); +$kernel->bootstrap(); + +use Illuminate\Support\Facades\DB; +use App\Models\Employee; +use App\Models\BiometricAttendance; +use App\Models\AttendanceRecord; +use App\Models\Shift; +use App\Models\AttendancePolicy; +use App\Http\Controllers\BiometricAttendanceController; +use Carbon\Carbon; + +DB::transaction(function() { + echo "=== 1. Running Timezone Correction Migration logic ===\n"; + // Check if migration has run. We can check if we need to shift. + // If we have punch times before May 29 that are in early morning (UTC), we shift them. + // To prevent double-shifting, we can check a flag or just run the update on records + // that have punch_time hours that don't match local time, or we can check if the migration ran. + $migrationRun = DB::table('migrations')->where('migration', '2026_05_30_030000_fix_biometric_timezone_offset')->exists(); + + if (!$migrationRun) { + echo "Migration has not run yet. Shifting raw biometric punches by +8 hours...\n"; + DB::statement("UPDATE biometric_attendances SET punch_time = DATE_ADD(punch_time, INTERVAL 8 HOUR)"); + // Insert migration record so Laravel knows it ran + DB::table('migrations')->insert([ + 'migration' => '2026_05_30_030000_fix_biometric_timezone_offset', + 'batch' => (DB::table('migrations')->max('batch') ?? 0) + 1 + ]); + echo "Raw punches shifted successfully.\n"; + } else { + echo "Migration already ran. Raw punches are already in local time.\n"; + } + + echo "\n=== 2. Re-syncing and Repairing Attendance Records ===\n"; + + // Fetch all biometric attendances + $allBioRecords = BiometricAttendance::all(); + + // Eager-load employees with shifts + $empIds = $allBioRecords->pluck('biometric_emp_id')->unique(); + $employees = Employee::with(['user', 'shift']) + ->whereIn('biometric_emp_id', $empIds) + ->get() + ->keyBy('biometric_emp_id'); + + $grouped = $allBioRecords->groupBy(function ($item) use ($employees) { + $employee = $employees->get($item->biometric_emp_id); + $workDate = BiometricAttendanceController::getWorkDateForPunch($item->punch_time, $employee); + return $item->biometric_emp_id . '_' . $workDate; + }); + + $updatedCount = 0; + $createdCount = 0; + + foreach ($grouped as $key => $dayEntries) { + $sorted = $dayEntries->sortBy('punch_time'); + $firstEntry = $sorted->first(); + $lastEntry = $sorted->last(); + + $employee = $employees->get($firstEntry->biometric_emp_id); + if (!$employee) continue; + + $workDate = BiometricAttendanceController::getWorkDateForPunch($firstEntry->punch_time, $employee); + $clockInTime = $firstEntry->punch_time->format('H:i:s'); + $clockOutTime = $sorted->count() > 1 ? $lastEntry->punch_time->format('H:i:s') : null; + + // Find existing attendance record + $attendance = AttendanceRecord::where('employee_id', $employee->user_id) + ->where('date', $workDate) + ->first(); + + if ($attendance) { + // Keep existing shift/policy if set + if (empty($attendance->shift_id)) { + $shift = Shift::where('id', $employee->shift_id)->where('status', 'active')->first(); + $attendance->shift_id = $shift?->id; + } + if (empty($attendance->attendance_policy_id)) { + $policy = AttendancePolicy::where('id', $employee->attendance_policy_id)->where('status', 'active')->first(); + $attendance->attendance_policy_id = $policy?->id; + } + + $attendance->biometric_id = $firstEntry->id; + $attendance->branch_id = $firstEntry->branch_id; + $attendance->clock_in = $clockInTime; + $attendance->clock_out = $clockOutTime; + $attendance->save(); + + $attendance->fresh(); + $attendance->processAttendance(); + + echo "Updated record for Employee User ID {$employee->user_id} ({$employee->user->name}) on Date {$workDate}: In {$clockInTime} | Out " . ($clockOutTime ?? 'NULL') . "\n"; + $updatedCount++; + } else { + $shift = Shift::where('id', $employee->shift_id)->where('status', 'active')->first(); + $policy = AttendancePolicy::where('id', $employee->attendance_policy_id)->where('status', 'active')->first(); + + $attendance = new AttendanceRecord(); + $attendance->employee_id = $employee->user_id; + $attendance->branch_id = $firstEntry->branch_id; + $attendance->biometric_id = $firstEntry->id; + $attendance->shift_id = $shift?->id; + $attendance->attendance_policy_id = $policy?->id; + $attendance->date = $workDate; + $attendance->clock_in = $clockInTime; + $attendance->clock_out = $clockOutTime; + $attendance->created_by = $employee->user_id; // fallback creator + $attendance->save(); + + $attendance->fresh(); + $attendance->processAttendance(); + + echo "Created record for Employee User ID {$employee->user_id} ({$employee->user->name}) on Date {$workDate}: In {$clockInTime} | Out " . ($clockOutTime ?? 'NULL') . "\n"; + $createdCount++; + } + + // Mark raw entries as synced + foreach ($dayEntries as $entry) { + $entry->update(['sync_status' => 'synced']); + } + } + + echo "\nSync Complete! Created: {$createdCount} | Updated: {$updatedCount}\n"; +});