diff --git a/app/Http/Controllers/Api/AttendanceSyncController.php b/app/Http/Controllers/Api/AttendanceSyncController.php index 984c46878..647f24aa8 100644 --- a/app/Http/Controllers/Api/AttendanceSyncController.php +++ b/app/Http/Controllers/Api/AttendanceSyncController.php @@ -46,101 +46,57 @@ class AttendanceSyncController extends Controller return response()->json(['error' => 'Branch not found'], 404); } - // 3. Process the logs (match BiometricAttendanceController logic) - // Ensure we handle arrays with 'recordTime' or 'timestamp' + // 3. Process the logs into a standardized format for the dashboard $formattedAttendances = []; foreach ($attendances as $record) { $empCode = $record['deviceUserId'] ?? $record['id'] ?? null; $rawPunchTime = $record['recordTime'] ?? $record['timestamp'] ?? null; $punchTime = $this->parseDateSafe($rawPunchTime); $uid = $record['userSn'] ?? $record['uid'] ?? null; + $type = $record['state'] ?? $record['type'] ?? 0; if ($empCode && $punchTime) { + // We format it identically to how ZKTeco hardware outputs `$raw_attendance` + // so BiometricAttendanceController can process it natively. $formattedAttendances[] = [ - 'id' => $uid, - 'emp_code' => (string) $empCode, - 'punch_time' => $punchTime->toDateTimeString(), // Clean Y-m-d H:i:s format - 'branch_id' => $branch->id + 'uid' => $uid, + 'id' => (string) $empCode, + 'timestamp' => $punchTime->toDateTimeString(), // Clean Y-m-d H:i:s format + 'type' => $type ]; } } - $groupedAttendances = collect($formattedAttendances)->groupBy(function ($item) { - return $item['emp_code'] . '_' . date('Y-m-d', strtotime($item['punch_time'])); - }); - - $processed = 0; - $inserted = 0; - - foreach ($groupedAttendances as $key => $dayEntries) { - $processed++; - $sorted = $dayEntries->sortBy('punch_time'); - $firstEntry = $sorted->first(); - $lastEntry = $sorted->last(); + // Save the formatted records to a JSON file cache for the branch + if (!empty($formattedAttendances)) { + $filePath = storage_path("app/biometric_cache_branch_{$branch->id}.json"); - // Note: Since this is an API, there is no Auth::user(). - // We find the employee by branch_id instead of created_by to avoid Admin vs HR mismatch. - $employee = \App\Models\Employee::with('user') - ->where('branch_id', $branch->id) - ->where('biometric_emp_id', $firstEntry['emp_code']) - ->first(); + // If the file exists, we merge the new records with the old ones, keeping only unique punches + if (file_exists($filePath)) { + $existing = json_decode(file_get_contents($filePath), true) ?? []; + $merged = array_merge($existing, $formattedAttendances); - if (!$employee) { - // Try fallback: maybe the employee isn't assigned to the branch but belongs to the same company - $employee = \App\Models\Employee::with('user') - ->where('created_by', $branch->created_by) - ->where('biometric_emp_id', $firstEntry['emp_code']) - ->first(); - } + // Remove exact duplicates by using a unique key + $unique = collect($merged)->unique(function ($item) { + return $item['id'] . '_' . $item['timestamp']; + })->values()->toArray(); - if ($employee) { - if ($sorted->count() > 1) { - $attedanceDate = date('Y-m-d', strtotime($firstEntry['punch_time'])); - $clockInTime = date('H:i:s', strtotime($firstEntry['punch_time'])); - $clockOutTime = date('H:i:s', strtotime($lastEntry['punch_time'])); - - $exists = \App\Models\AttendanceRecord::where('employee_id', $employee->user_id) - ->where('date', $attedanceDate) - ->exists(); - - if (!$exists) { - $shift = \App\Models\Shift::where('id', $employee->shift_id)->where('status', 'active')->first() - ?? \App\Models\Shift::where('created_by', $branch->created_by)->where('status', 'active')->first(); - - $policy = \App\Models\AttendancePolicy::where('id', $employee->attendance_policy_id)->where('status', 'active')->first() - ?? \App\Models\AttendancePolicy::where('created_by', $branch->created_by)->where('status', 'active')->first(); - - $attendance = new \App\Models\AttendanceRecord(); - $attendance->employee_id = $employee->user_id; - $attendance->biometric_id = $firstEntry['id']; - $attendance->shift_id = $shift?->id; - $attendance->attendance_policy_id = $policy?->id; - $attendance->date = $attedanceDate; - $attendance->clock_in = $clockInTime; - $attendance->clock_out = $clockOutTime; - $attendance->created_by = $employee->created_by ?? $branch->created_by; - $attendance->save(); - - $attendance->fresh(); - $attendance->processAttendance(); - - $inserted++; - } else { - $skippedReasons[] = "Record already exists for Date: {$attedanceDate}"; - } - } else { - $skippedReasons[] = "Only 1 punch found for {$firstEntry['emp_code']} on " . date('Y-m-d', strtotime($firstEntry['punch_time'])) . " (Need Clock In and Out)"; + // Keep only the last 3000 records to prevent file bloat + if (count($unique) > 3000) { + $unique = array_slice($unique, -3000); } + + file_put_contents($filePath, json_encode($unique)); } else { - $skippedReasons[] = "Employee with Biometric ID {$firstEntry['emp_code']} not found in Branch {$branch->id}"; + file_put_contents($filePath, json_encode($formattedAttendances)); } } - $debugMsg = empty($skippedReasons) ? "" : " | Skipped reasons: " . implode(', ', array_unique($skippedReasons)); + $processed = count($formattedAttendances); return response()->json([ 'success' => true, - 'message' => "Processed {$processed} unique daily records, Inserted {$inserted} new attendances." . $debugMsg, + 'message' => "Successfully pushed {$processed} records to the Biometric Attendance waiting room for Branch {$branch->id}.", ]); } diff --git a/app/Http/Controllers/BiometricAttendanceController.php b/app/Http/Controllers/BiometricAttendanceController.php index e1d189b72..ff9b164d3 100644 --- a/app/Http/Controllers/BiometricAttendanceController.php +++ b/app/Http/Controllers/BiometricAttendanceController.php @@ -63,6 +63,29 @@ class BiometricAttendanceController extends Controller $zk->disconnect(); } } catch (\Throwable $th) {} + + // Read from local JSON cache if agent pushed data + try { + $cacheFile = storage_path("app/biometric_cache_branch_{$branch->id}.json"); + if (file_exists($cacheFile)) { + $cached_attendance = json_decode(file_get_contents($cacheFile), true) ?? []; + $startTs = strtotime(strlen($start_date) == 10 ? $start_date . ' 00:00:00' : $start_date); + $endTs = strtotime(strlen($end_date) == 10 ? $end_date . ' 23:59:59' : $end_date); + foreach ($cached_attendance as $record) { + $punchTs = strtotime($record['timestamp']); + if ($punchTs >= $startTs && $punchTs <= $endTs) { + $attendances[] = [ + 'id' => $record['uid'], + 'emp_code' => (string) $record['id'], + 'punch_time' => $record['timestamp'], + 'punch_state_display' => $record['type'] == 0 ? 'Clock In' : 'Clock Out', + 'terminal_alias' => $branch->name . ' (Agent)', + 'branch_id' => $branch->id + ]; + } + } + } + } catch (\Throwable $th) {} } } else if (!empty($api_urls)) { if ($isDirectIP) { @@ -370,6 +393,27 @@ class BiometricAttendanceController extends Controller $zk->disconnect(); } } catch (\Throwable $th) {} + + // Read from local JSON cache if agent pushed data + try { + $cacheFile = storage_path("app/biometric_cache_branch_{$branch->id}.json"); + if (file_exists($cacheFile)) { + $cached_attendance = json_decode(file_get_contents($cacheFile), true) ?? []; + $startTs = strtotime(strlen($start_date) == 10 ? $start_date . ' 00:00:00' : $start_date); + $endTs = strtotime(strlen($end_date) == 10 ? $end_date . ' 23:59:59' : $end_date); + foreach ($cached_attendance as $record) { + $punchTs = strtotime($record['timestamp']); + if ($punchTs >= $startTs && $punchTs <= $endTs) { + $attendances[] = [ + 'id' => $record['uid'], + 'emp_code' => (string) $record['id'], + 'punch_time' => $record['timestamp'], + 'branch_id' => $branch->id + ]; + } + } + } + } catch (\Throwable $th) {} } } else if (!empty($api_urls)) { if ($isDirectIP) {