diff --git a/Windows-Sync-Agent.zip b/Windows-Sync-Agent.zip index 145b29b5f..6c23a61f9 100644 Binary files a/Windows-Sync-Agent.zip and b/Windows-Sync-Agent.zip differ diff --git a/app/Http/Controllers/Api/AttendanceSyncController.php b/app/Http/Controllers/Api/AttendanceSyncController.php index fbb88ac40..a4a4cf20e 100644 --- a/app/Http/Controllers/Api/AttendanceSyncController.php +++ b/app/Http/Controllers/Api/AttendanceSyncController.php @@ -46,60 +46,84 @@ class AttendanceSyncController extends Controller return response()->json(['error' => 'Branch not found'], 404); } - // 3. Process the logs (same logic as BiometricAttendanceController) + // 3. Process the logs (match BiometricAttendanceController logic) + // Ensure we handle arrays with 'recordTime' or 'timestamp' + $formattedAttendances = []; + foreach ($attendances as $record) { + $empCode = $record['deviceUserId'] ?? $record['id'] ?? null; + $punchTime = $record['recordTime'] ?? $record['timestamp'] ?? null; + $uid = $record['userSn'] ?? $record['uid'] ?? null; + + if ($empCode && $punchTime) { + $formattedAttendances[] = [ + 'id' => $uid, + 'emp_code' => (string) $empCode, + 'punch_time' => $punchTime, + 'branch_id' => $branch->id + ]; + } + } + + $groupedAttendances = collect($formattedAttendances)->groupBy(function ($item) { + return $item['emp_code'] . '_' . date('Y-m-d', strtotime($item['punch_time'])); + }); + $processed = 0; $inserted = 0; - foreach ($attendances as $record) { + foreach ($groupedAttendances as $key => $dayEntries) { $processed++; - // Typical payload from zklib-js: { userSn: 1, deviceUserId: '1', recordTime: '2023-10-14 08:30:00' } - $empCode = $record['deviceUserId'] ?? $record['id'] ?? null; - $punchTime = $record['recordTime'] ?? $record['timestamp'] ?? null; - - if (!$empCode || !$punchTime) { - continue; - } - - // Find employee by emp_code inside this branch's company - $employee = \App\Models\Employee::where('emp_code', $empCode) + $sorted = $dayEntries->sortBy('punch_time'); + $firstEntry = $sorted->first(); + $lastEntry = $sorted->last(); + + // Note: Since this is an API, there is no Auth::user(). We use the Branch's creator. + // BiometricAttendanceController uses whereIn('created_by', getCompanyAndUsersId()) + // We just use where('created_by', $branch->created_by) or similar. + + $employee = \App\Models\Employee::with('user') ->where('created_by', $branch->created_by) + ->where('biometric_emp_id', $firstEntry['emp_code']) ->first(); - - if (!$employee) { - continue; - } - - // Parse date and time - $ts = strtotime($punchTime); - $date = date('Y-m-d', $ts); - $time = date('H:i:s', $ts); - - // Check if record already exists to prevent duplicates - $exists = AttendanceEmployee::where('employee_id', $employee->id) - ->where('date', $date) - ->where('clock_in', $time) - ->exists(); - - if (!$exists) { - AttendanceEmployee::create([ - 'employee_id' => $employee->id, - 'date' => $date, - 'status' => 'Present', - 'clock_in' => $time, - 'clock_out' => '00:00:00', - 'late' => '00:00:00', - 'early_leaving' => '00:00:00', - 'overtime' => '00:00:00', - 'total_rest' => '00:00:00', - 'created_by' => $branch->created_by, - ]); - $inserted++; + + if ($employee && $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 = $branch->created_by; + $attendance->save(); + + $attendance->fresh(); + $attendance->processAttendance(); + + $inserted++; + } } } return response()->json([ 'success' => true, - 'message' => "Processed {$processed} records, Inserted {$inserted} new attendances.", + 'message' => "Processed {$processed} unique daily records, Inserted {$inserted} new attendances.", ]); } } diff --git a/branch-agent/sync-agent.exe b/branch-agent/sync-agent.exe index 7f0b4b95e..c87635cd9 100644 Binary files a/branch-agent/sync-agent.exe and b/branch-agent/sync-agent.exe differ diff --git a/branch-agent/sync.js b/branch-agent/sync.js index 1428a2870..f06c01c46 100644 --- a/branch-agent/sync.js +++ b/branch-agent/sync.js @@ -15,33 +15,38 @@ async function syncAttendance() { const zkInstance = new ZKLib(ZKTECO_IP, ZKTECO_PORT, 10000, 4000); try { - // Connect to the device await zkInstance.createSocket(); console.log('✅ Connected to ZKTeco device.'); - // Get Attendance records console.log('Fetching attendance records...'); const attendances = await zkInstance.getAttendances(); if (attendances && attendances.data && attendances.data.length > 0) { - console.log(`✅ Found ${attendances.data.length} records.`); + console.log(`✅ Found total ${attendances.data.length} records on device.`); - // Push to Server - console.log(`Pushing to Cloud API: ${SERVER_API_URL}`); - const response = await axios.post(SERVER_API_URL, { - api_key: API_KEY, - branch_id: BRANCH_ID, - zkteco_ip: ZKTECO_IP, - attendances: attendances.data + // Filter to last 15 days + const fifteenDaysAgo = new Date(); + fifteenDaysAgo.setDate(fifteenDaysAgo.getDate() - 15); + + const recentLogs = attendances.data.filter(log => { + const logDate = new Date(log.recordTime); + return logDate >= fifteenDaysAgo; }); - - console.log('✅ Sync Successful:', response.data); - // Optional: Clear attendance on device to avoid re-sending old logs - // await zkInstance.clearAttendanceLog(); - // console.log('✅ Cleared attendance logs from device.'); + console.log(`✅ Filtered to ${recentLogs.length} records from the last 15 days.`); + + if (recentLogs.length > 0) { + console.log(`Pushing to Cloud API: ${SERVER_API_URL}`); + const response = await axios.post(SERVER_API_URL, { + api_key: API_KEY, + branch_id: BRANCH_ID, + zkteco_ip: ZKTECO_IP, + attendances: recentLogs + }); + console.log('✅ Sync Successful:', response.data); + } } else { - console.log('ℹ️ No new attendance records found.'); + console.log('ℹ️ No attendance records found on device.'); } } catch (e) { @@ -54,6 +59,12 @@ async function syncAttendance() { await zkInstance.disconnect(); console.log('Disconnected from ZKTeco device.'); } catch (err) {} + + // Keep window open for user to read logs + console.log('\nPress any key to exit...'); + process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.on('data', process.exit.bind(process, 0)); } }