feat: implement multi-branch biometric IP configuration and auto-sync
This commit is contained in:
@@ -25,7 +25,8 @@ class BiometricAttendanceController extends Controller
|
||||
$token = !empty($company_setting['zkteco_auth_token']) ? $company_setting['zkteco_auth_token'] : '';
|
||||
$isZktecoSync = !empty($company_setting['isZktecoSync']) && $company_setting['isZktecoSync'] == '1';
|
||||
|
||||
$configurationMissing = empty($api_urls) || empty($username) || empty($password) || empty($token) || !$isZktecoSync;
|
||||
$isDirectIP = filter_var($api_urls, FILTER_VALIDATE_IP);
|
||||
$configurationMissing = empty($api_urls) || (!$isDirectIP && (empty($username) || empty($password) || empty($token) || !$isZktecoSync));
|
||||
|
||||
if (!empty($request->start_date) && !empty($request->end_date)) {
|
||||
$start_date = date('Y-m-d H:i:s', strtotime($request->start_date));
|
||||
@@ -37,38 +38,74 @@ class BiometricAttendanceController extends Controller
|
||||
|
||||
$attendances = [];
|
||||
|
||||
if (!empty($token) && !empty($api_urls)) {
|
||||
$api_url = rtrim($api_urls, '/');
|
||||
$url = $api_url . '/iclock/api/transactions/?' . http_build_query([
|
||||
'start_time' => $start_date,
|
||||
'end_time' => $end_date,
|
||||
'page_size' => 10000,
|
||||
]);
|
||||
|
||||
$curl = curl_init();
|
||||
try {
|
||||
curl_setopt_array($curl, array(
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_ENCODING => '',
|
||||
CURLOPT_MAXREDIRS => 10,
|
||||
CURLOPT_TIMEOUT => 0,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
|
||||
CURLOPT_CUSTOMREQUEST => 'GET',
|
||||
CURLOPT_HTTPHEADER => array(
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Token ' . $token
|
||||
),
|
||||
));
|
||||
|
||||
$response = curl_exec($curl);
|
||||
curl_close($curl);
|
||||
|
||||
$json_attendance = json_decode($response, true);
|
||||
$attendances = $json_attendance['data'] ?? [];
|
||||
} catch (\Throwable $th) {
|
||||
$attendances = [];
|
||||
$branches = \App\Models\Branch::whereNotNull('zkteco_ip')->whereIn('created_by', getCompanyAndUsersId())->get();
|
||||
if ($branches->isNotEmpty()) {
|
||||
foreach ($branches as $branch) {
|
||||
try {
|
||||
$zk = new \Rats\Zkteco\Lib\Zkteco($branch->zkteco_ip);
|
||||
if ($zk->connect()) {
|
||||
$raw_attendance = $zk->getAttendance();
|
||||
$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 ($raw_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,
|
||||
'branch_id' => $branch->id
|
||||
];
|
||||
}
|
||||
}
|
||||
$zk->disconnect();
|
||||
}
|
||||
} catch (\Throwable $th) {}
|
||||
}
|
||||
} else if (!empty($api_urls)) {
|
||||
if ($isDirectIP) {
|
||||
try {
|
||||
$zk = new \Rats\Zkteco\Lib\Zkteco($api_urls);
|
||||
if ($zk->connect()) {
|
||||
$raw_attendance = $zk->getAttendance();
|
||||
$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 ($raw_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' => 'Local ZKTeco'
|
||||
];
|
||||
}
|
||||
}
|
||||
$zk->disconnect();
|
||||
}
|
||||
} catch (\Throwable $th) {}
|
||||
} else if (!empty($token)) {
|
||||
$api_url = rtrim($api_urls, '/');
|
||||
$url = $api_url . '/iclock/api/transactions/?' . http_build_query([
|
||||
'start_time' => $start_date,
|
||||
'end_time' => $end_date,
|
||||
'page_size' => 10000,
|
||||
]);
|
||||
$curl = curl_init();
|
||||
try {
|
||||
curl_setopt_array($curl, array(
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => array('Content-Type: application/json', 'Authorization: Token ' . $token)
|
||||
));
|
||||
$response = curl_exec($curl);
|
||||
curl_close($curl);
|
||||
$json = json_decode($response, true);
|
||||
$attendances = $json['data'] ?? [];
|
||||
} catch (\Throwable $th) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +124,12 @@ class BiometricAttendanceController extends Controller
|
||||
$sorted = $dayEntries->sortBy('punch_time');
|
||||
$firstEntry = $sorted->first();
|
||||
$lastEntry = $sorted->last();
|
||||
$employee = Employee::with('user')->where('biometric_emp_id', $firstEntry['emp_code'])->first();
|
||||
$q = Employee::with('user')->where('biometric_emp_id', $firstEntry['emp_code']);
|
||||
if (isset($firstEntry['branch_id'])) {
|
||||
$q->where('branch_id', $firstEntry['branch_id']);
|
||||
}
|
||||
$employee = $q->first();
|
||||
if (!$employee) return null;
|
||||
|
||||
return [
|
||||
'id' => $firstEntry['id'],
|
||||
@@ -97,9 +139,10 @@ class BiometricAttendanceController extends Controller
|
||||
'clock_in' => date('H:i:s', strtotime($firstEntry['punch_time'])),
|
||||
'clock_out' => $sorted->count() > 1 ? date('H:i:s', strtotime($lastEntry['punch_time'])) : null,
|
||||
'total_entries' => $sorted->count(),
|
||||
'terminal' => $firstEntry['terminal_alias'] ?? 'Unknown'
|
||||
];
|
||||
});
|
||||
$query = $groupedAttendances->values();
|
||||
})->filter()->values();
|
||||
$query = $groupedAttendances;
|
||||
|
||||
// Handle search
|
||||
if ($request->has('search') && !empty($request->search)) {
|
||||
@@ -180,48 +223,76 @@ class BiometricAttendanceController extends Controller
|
||||
$token = $company_setting['zkteco_auth_token'] ?? '';
|
||||
$api_urls = $company_setting['zkteco_api_url'] ?? '';
|
||||
|
||||
if (empty($token) || empty($api_urls)) {
|
||||
$isDirectIP = filter_var($api_urls, FILTER_VALIDATE_IP);
|
||||
|
||||
if (empty($api_urls) || (!$isDirectIP && empty($token))) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'ZKTeco API configuration missing'
|
||||
], 400);
|
||||
}
|
||||
|
||||
$start_date = $date . ' 00:00:00';
|
||||
$end_date = $date . ' 23:59:59';
|
||||
if ($isDirectIP) {
|
||||
try {
|
||||
$zk = new \Rats\Zkteco\Lib\Zkteco($api_urls);
|
||||
if ($zk->connect()) {
|
||||
$raw_attendance = $zk->getAttendance();
|
||||
foreach ($raw_attendance as $record) {
|
||||
if ((string)$record['id'] === (string)$employeeCode && str_starts_with($record['timestamp'], $date)) {
|
||||
$attendances[] = [
|
||||
'id' => $record['uid'],
|
||||
'punch_time' => $record['timestamp'],
|
||||
'punch_state_display' => $record['type'] == 0 ? 'Clock In' : 'Clock Out',
|
||||
'verify_type_display' => 'Unknown',
|
||||
'terminal_alias' => 'Local ZKTeco'
|
||||
];
|
||||
}
|
||||
}
|
||||
$zk->disconnect();
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to connect to local ZKTeco'
|
||||
], 500);
|
||||
}
|
||||
} else {
|
||||
$start_date = $date . ' 00:00:00';
|
||||
$end_date = $date . ' 23:59:59';
|
||||
|
||||
$api_url = rtrim($api_urls, '/');
|
||||
$url = $api_url . '/iclock/api/transactions/?' . http_build_query([
|
||||
'emp_code' => $employeeCode,
|
||||
'start_time' => $start_date,
|
||||
'end_time' => $end_date,
|
||||
'page_size' => 1000,
|
||||
]);
|
||||
$api_url = rtrim($api_urls, '/');
|
||||
$url = $api_url . '/iclock/api/transactions/?' . http_build_query([
|
||||
'emp_code' => $employeeCode,
|
||||
'start_time' => $start_date,
|
||||
'end_time' => $end_date,
|
||||
'page_size' => 1000,
|
||||
]);
|
||||
|
||||
$curl = curl_init();
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Token ' . $token
|
||||
],
|
||||
]);
|
||||
$curl = curl_init();
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Token ' . $token
|
||||
],
|
||||
]);
|
||||
|
||||
$response = curl_exec($curl);
|
||||
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
curl_close($curl);
|
||||
$response = curl_exec($curl);
|
||||
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
curl_close($curl);
|
||||
|
||||
if ($httpCode !== 200 || !$response) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to fetch data from ZKTeco API'
|
||||
], 500);
|
||||
if ($httpCode !== 200 || !$response) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Failed to fetch data from ZKTeco API'
|
||||
], 500);
|
||||
}
|
||||
|
||||
$json_attendance = json_decode($response, true);
|
||||
$attendances = $json_attendance['data'] ?? [];
|
||||
}
|
||||
|
||||
$json_attendance = json_decode($response, true);
|
||||
$attendances = $json_attendance['data'] ?? [];
|
||||
}
|
||||
|
||||
|
||||
@@ -263,6 +334,133 @@ class BiometricAttendanceController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
public function syncAll(Request $request)
|
||||
{
|
||||
if (Auth::user()->can('sync-biometric-attendance')) {
|
||||
$company_setting = settings();
|
||||
$api_urls = !empty($company_setting['zkteco_api_url']) ? $company_setting['zkteco_api_url'] : '';
|
||||
$token = !empty($company_setting['zkteco_auth_token']) ? $company_setting['zkteco_auth_token'] : '';
|
||||
|
||||
$start_date = date('Y-m-d', strtotime('-7 days'));
|
||||
$end_date = date('Y-m-d');
|
||||
|
||||
$isDirectIP = filter_var($api_urls, FILTER_VALIDATE_IP);
|
||||
$attendances = [];
|
||||
|
||||
$branches = \App\Models\Branch::whereNotNull('zkteco_ip')->whereIn('created_by', getCompanyAndUsersId())->get();
|
||||
if ($branches->isNotEmpty()) {
|
||||
foreach ($branches as $branch) {
|
||||
try {
|
||||
$zk = new \Rats\Zkteco\Lib\Zkteco($branch->zkteco_ip);
|
||||
if ($zk->connect()) {
|
||||
$raw_attendance = $zk->getAttendance();
|
||||
$startTs = strtotime($start_date . ' 00:00:00');
|
||||
$endTs = strtotime($end_date . ' 23:59:59');
|
||||
foreach ($raw_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
|
||||
];
|
||||
}
|
||||
}
|
||||
$zk->disconnect();
|
||||
}
|
||||
} catch (\Throwable $th) {}
|
||||
}
|
||||
} else if (!empty($api_urls)) {
|
||||
if ($isDirectIP) {
|
||||
try {
|
||||
$zk = new \Rats\Zkteco\Lib\Zkteco($api_urls);
|
||||
if ($zk->connect()) {
|
||||
$raw_attendance = $zk->getAttendance();
|
||||
$startTs = strtotime($start_date . ' 00:00:00');
|
||||
$endTs = strtotime($end_date . ' 23:59:59');
|
||||
foreach ($raw_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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
$zk->disconnect();
|
||||
}
|
||||
} catch (\Throwable $th) {}
|
||||
} else if (!empty($token)) {
|
||||
$api_url = rtrim($api_urls, '/');
|
||||
$url = $api_url . '/iclock/api/transactions/?' . http_build_query([
|
||||
'start_time' => date('Y-m-d H:i:s', strtotime($start_date . ' 00:00:00')),
|
||||
'end_time' => date('Y-m-d H:i:s', strtotime($end_date . ' 23:59:59')),
|
||||
'page_size' => 10000,
|
||||
]);
|
||||
$curl = curl_init();
|
||||
try {
|
||||
curl_setopt_array($curl, array(
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => array('Content-Type: application/json', 'Authorization: Token ' . $token)
|
||||
));
|
||||
$response = curl_exec($curl);
|
||||
curl_close($curl);
|
||||
$json = json_decode($response, true);
|
||||
$attendances = $json['data'] ?? [];
|
||||
} catch (\Throwable $th) {}
|
||||
}
|
||||
}
|
||||
|
||||
$groupedAttendances = collect($attendances)->groupBy(function ($item) {
|
||||
return $item['emp_code'] . '_' . date('Y-m-d', strtotime($item['punch_time']));
|
||||
});
|
||||
|
||||
$syncedCount = 0;
|
||||
foreach ($groupedAttendances as $key => $dayEntries) {
|
||||
$sorted = $dayEntries->sortBy('punch_time');
|
||||
$firstEntry = $sorted->first();
|
||||
$lastEntry = $sorted->last();
|
||||
|
||||
$q = Employee::with('user')->whereIn('created_by', getCompanyAndUsersId())->where('biometric_emp_id', $firstEntry['emp_code']);
|
||||
if (isset($firstEntry['branch_id'])) {
|
||||
$q->where('branch_id', $firstEntry['branch_id']);
|
||||
}
|
||||
$employee = $q->first();
|
||||
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 = AttendanceRecord::where('employee_id', $employee->user_id)->where('date', $attedanceDate)->whereIn('created_by', getCompanyAndUsersId())->exists();
|
||||
if (!$exists) {
|
||||
$shift = Shift::where('id', $employee->shift_id)->where('status', 'active')->first() ?? Shift::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->first();
|
||||
$policy = AttendancePolicy::where('id', $employee->attendance_policy_id)->where('status', 'active')->first() ?? AttendancePolicy::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->first();
|
||||
|
||||
$attendance = new 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 = creatorId();
|
||||
$attendance->save();
|
||||
|
||||
$attendance->fresh();
|
||||
$attendance->processAttendance();
|
||||
$syncedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return redirect()->back()->with('success', __("Bulk sync completed. Synced {$syncedCount} new records."));
|
||||
}
|
||||
return redirect()->back()->with('error', __('Permission Denied.'));
|
||||
}
|
||||
|
||||
public function sync(Request $request, $id)
|
||||
{
|
||||
if (Auth::user()->can('sync-biometric-attendance')) {
|
||||
@@ -275,7 +473,9 @@ class BiometricAttendanceController extends Controller
|
||||
|
||||
$company_setting = settings();
|
||||
|
||||
if (empty($company_setting['zkteco_auth_token'])) {
|
||||
$isDirectIP = filter_var($company_setting['zkteco_api_url'] ?? '', FILTER_VALIDATE_IP);
|
||||
|
||||
if (!$isDirectIP && empty($company_setting['zkteco_auth_token'])) {
|
||||
return redirect()->back()->with('error', __('Create the Auth Token From the Setting page.'));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user