Switch biometric agent to staging JSON cache flow

This commit is contained in:
2026-05-19 16:27:54 +08:00
parent ceb03224b5
commit 7f92eff3eb
2 changed files with 71 additions and 71 deletions

View File

@@ -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}.",
]);
}

View File

@@ -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) {