fix: refactor ZKTeco API receiver to group by day and use correct AttendanceRecord table, and limit agent to last 15 days
This commit is contained in:
Binary file not shown.
@@ -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.",
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user