146 lines
6.4 KiB
PHP
146 lines
6.4 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use Illuminate\Http\Request;
|
|
use App\Models\Branch;
|
|
use App\Models\AttendanceEmployee;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class AttendanceSyncController extends Controller
|
|
{
|
|
public function sync(Request $request)
|
|
{
|
|
// 1. Verify API Key
|
|
$apiKey = $request->header('X-API-Key') ?? $request->input('api_key');
|
|
// You should store a master API key in your .env like ZKTECO_SYNC_KEY=xxx
|
|
$validKey = env('ZKTECO_SYNC_KEY', 'default_secure_sync_key_123');
|
|
|
|
if ($apiKey !== $validKey) {
|
|
Log::warning('Unauthorized ZKTeco sync attempt', ['ip' => $request->ip()]);
|
|
return response()->json(['error' => 'Unauthorized'], 401);
|
|
}
|
|
|
|
$branchId = $request->input('branch_id');
|
|
$zktecoIp = $request->input('zkteco_ip');
|
|
$attendances = $request->input('attendances', []);
|
|
|
|
if (empty($attendances)) {
|
|
return response()->json(['message' => 'No attendance records provided'], 400);
|
|
}
|
|
|
|
// 2. Find the branch
|
|
$branchQuery = Branch::query();
|
|
if ($branchId) {
|
|
$branchQuery->where('id', $branchId);
|
|
} elseif ($zktecoIp) {
|
|
$branchQuery->where('zkteco_ip', $zktecoIp);
|
|
} else {
|
|
return response()->json(['error' => 'Must provide branch_id or zkteco_ip'], 400);
|
|
}
|
|
|
|
$branch = $branchQuery->first();
|
|
|
|
if (!$branch) {
|
|
return response()->json(['error' => 'Branch not found'], 404);
|
|
}
|
|
|
|
// 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 ($groupedAttendances as $key => $dayEntries) {
|
|
$processed++;
|
|
$sorted = $dayEntries->sortBy('punch_time');
|
|
$firstEntry = $sorted->first();
|
|
$lastEntry = $sorted->last();
|
|
|
|
// 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 (!$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();
|
|
}
|
|
|
|
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)";
|
|
}
|
|
} else {
|
|
$skippedReasons[] = "Employee with Biometric ID {$firstEntry['emp_code']} not found in Branch {$branch->id}";
|
|
}
|
|
}
|
|
|
|
$debugMsg = empty($skippedReasons) ? "" : " | Skipped reasons: " . implode(', ', array_unique($skippedReasons));
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => "Processed {$processed} unique daily records, Inserted {$inserted} new attendances." . $debugMsg,
|
|
]);
|
|
}
|
|
}
|