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"; });