109 lines
4.0 KiB
PHP
109 lines
4.0 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. Save directly to biometric_attendances table
|
|
$processed = 0;
|
|
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) {
|
|
// Insert or ignore based on biometric_emp_id and punch_time to prevent duplicates
|
|
$exists = \App\Models\BiometricAttendance::where('biometric_emp_id', $empCode)
|
|
->where('punch_time', $punchTime->toDateTimeString())
|
|
->exists();
|
|
|
|
if (!$exists) {
|
|
\App\Models\BiometricAttendance::create([
|
|
'branch_id' => $branch->id,
|
|
'biometric_emp_id' => $empCode,
|
|
'punch_time' => $punchTime->toDateTimeString(),
|
|
'punch_type' => $type,
|
|
'terminal_alias' => $branch->name . ' (Agent)',
|
|
'sync_status' => 'pending',
|
|
]);
|
|
$processed++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => "Successfully pushed {$processed} new records to the Biometric Attendance database for Branch {$branch->id}.",
|
|
]);
|
|
}
|
|
|
|
private function parseDateSafe($dateStr)
|
|
{
|
|
if (!$dateStr) {
|
|
return null;
|
|
}
|
|
|
|
// Handle numeric timestamps (Unix seconds or milliseconds)
|
|
if (is_numeric($dateStr)) {
|
|
if (strlen((string)$dateStr) > 10) {
|
|
// It's likely in milliseconds
|
|
return \Carbon\Carbon::createFromTimestampMs($dateStr)->setTimezone('Asia/Manila');
|
|
}
|
|
return \Carbon\Carbon::createFromTimestamp($dateStr)->setTimezone('Asia/Manila');
|
|
}
|
|
|
|
// Remove JS-specific timezone names like "(China Standard Time)" to prevent Carbon parse errors
|
|
$cleanDateStr = trim(explode('(', $dateStr)[0]);
|
|
|
|
try {
|
|
return \Carbon\Carbon::parse($cleanDateStr, 'Asia/Manila')->setTimezone('Asia/Manila');
|
|
} catch (\Exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
}
|