Switch biometric agent to staging JSON cache flow
This commit is contained in:
@@ -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}.",
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user