Refactor biometric attendance to use database
This commit is contained in:
@@ -17,218 +17,97 @@ class BiometricAttendanceController extends Controller
|
||||
{
|
||||
public function index(Request $request)
|
||||
{
|
||||
if (Auth::user()->can('manage-biometric-attendance')) {
|
||||
$company_setting = settings();
|
||||
$api_urls = !empty($company_setting['zkteco_api_url']) ? $company_setting['zkteco_api_url'] : '';
|
||||
$username = !empty($company_setting['zkteco_username']) ? $company_setting['zkteco_username'] : '';
|
||||
$password = !empty($company_setting['zkteco_password']) ? $company_setting['zkteco_password'] : '';
|
||||
$token = !empty($company_setting['zkteco_auth_token']) ? $company_setting['zkteco_auth_token'] : '';
|
||||
$isZktecoSync = !empty($company_setting['isZktecoSync']) && $company_setting['isZktecoSync'] == '1';
|
||||
|
||||
$isDirectIP = filter_var($api_urls, FILTER_VALIDATE_IP);
|
||||
|
||||
// Check if any branches are configured for local IP (which the agent uses)
|
||||
$branches = \App\Models\Branch::whereNotNull('zkteco_ip')->whereIn('created_by', getCompanyAndUsersId())->get();
|
||||
|
||||
$configurationMissing = empty($api_urls) || (!$isDirectIP && (empty($username) || empty($password) || empty($token) || !$isZktecoSync));
|
||||
|
||||
// If branches have IPs configured, we don't need the global API settings
|
||||
if ($branches->isNotEmpty()) {
|
||||
$configurationMissing = false;
|
||||
}
|
||||
|
||||
if (!empty($request->start_date) && !empty($request->end_date)) {
|
||||
$start_date = date('Y-m-d H:i:s', strtotime($request->start_date));
|
||||
$end_date = date('Y-m-d H:i:s', strtotime($request->end_date) + 86400 - 1);
|
||||
} else {
|
||||
$start_date = date('Y-m-d', strtotime('-7 days'));
|
||||
$end_date = date('Y-m-d');
|
||||
}
|
||||
|
||||
$attendances = [];
|
||||
|
||||
$branches = \App\Models\Branch::whereNotNull('zkteco_ip')->whereIn('created_by', getCompanyAndUsersId())->get();
|
||||
if ($branches->isNotEmpty()) {
|
||||
foreach ($branches as $branch) {
|
||||
try {
|
||||
$zk = new \App\Library\ZKTeco\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) {}
|
||||
|
||||
// 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) {
|
||||
try {
|
||||
$zk = new \App\Library\ZKTeco\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) {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (isDemo()) {
|
||||
$attendances = isSaas() ? $this->getSaasDemoData() : $this->getNonSaasDemoData();
|
||||
}
|
||||
|
||||
|
||||
// Group by employee code and date
|
||||
$groupedAttendances = collect($attendances)
|
||||
->groupBy(function ($item) {
|
||||
return $item['emp_code'] . '_' . date('Y-m-d', strtotime($item['punch_time']));
|
||||
})
|
||||
->map(function ($dayEntries) {
|
||||
$sorted = $dayEntries->sortBy('punch_time');
|
||||
$firstEntry = $sorted->first();
|
||||
$lastEntry = $sorted->last();
|
||||
$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'],
|
||||
'employee_code' => $firstEntry['emp_code'],
|
||||
'name' => $employee && $employee->user ? $employee->user->name : 'Unknown',
|
||||
'date' => date('Y-m-d', strtotime($firstEntry['punch_time'])),
|
||||
'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'
|
||||
];
|
||||
})->filter()->values();
|
||||
$query = $groupedAttendances;
|
||||
|
||||
// Handle search
|
||||
if ($request->has('search') && !empty($request->search)) {
|
||||
$query = $query->filter(function ($item) use ($request) {
|
||||
return stripos($item['name'], $request->search) !== false ||
|
||||
stripos($item['employee_code'], $request->search) !== false;
|
||||
});
|
||||
}
|
||||
|
||||
// Handle date range filter
|
||||
if ($request->has('start_date') && !empty($request->start_date)) {
|
||||
$query = $query->filter(function ($item) use ($request) {
|
||||
return $item['date'] = $request->start_date;
|
||||
});
|
||||
}
|
||||
|
||||
if ($request->has('end_date') && !empty($request->end_date)) {
|
||||
$query = $query->filter(function ($item) use ($request) {
|
||||
return $item['date'] <= $request->end_date;
|
||||
});
|
||||
}
|
||||
|
||||
// Handle sorting
|
||||
if ($request->has('sort_field') && !empty($request->sort_field)) {
|
||||
$direction = $request->sort_direction ?? 'asc';
|
||||
$query = $query->sortBy($request->sort_field, SORT_REGULAR, $direction === 'desc');
|
||||
} else {
|
||||
$query = $query->sortByDesc('id');
|
||||
}
|
||||
|
||||
// Manual pagination for collection
|
||||
$perPage = $request->per_page ?? 10;
|
||||
$currentPage = $request->page ?? 1;
|
||||
$total = $query->count();
|
||||
$items = $query->forPage($currentPage, $perPage)->values();
|
||||
|
||||
$biometricData = new LengthAwarePaginator(
|
||||
$items,
|
||||
$total,
|
||||
$perPage,
|
||||
$currentPage,
|
||||
[
|
||||
'path' => $request->url(),
|
||||
'pageName' => 'page',
|
||||
]
|
||||
);
|
||||
return Inertia::render('hr/biometric-attendance/index', [
|
||||
'biometricData' => $biometricData,
|
||||
'filters' => $request->all(['search', 'start_date', 'end_date', 'sort_field', 'sort_direction', 'per_page']),
|
||||
'configurationMissing' => $configurationMissing,
|
||||
]);
|
||||
} else {
|
||||
if (!Auth::user()->can('manage-biometric-attendance')) {
|
||||
return redirect()->back()->with('error', __('Permission Denied.'));
|
||||
}
|
||||
|
||||
$query = \App\Models\BiometricAttendance::with(['branch']);
|
||||
|
||||
// Filter by dates
|
||||
if ($request->has('start_date') && !empty($request->start_date)) {
|
||||
$query->whereDate('punch_time', '>=', $request->start_date);
|
||||
} else {
|
||||
$query->whereDate('punch_time', '>=', Carbon::now()->subDays(7));
|
||||
}
|
||||
|
||||
if ($request->has('end_date') && !empty($request->end_date)) {
|
||||
$query->whereDate('punch_time', '<=', $request->end_date);
|
||||
} else {
|
||||
$query->whereDate('punch_time', '<=', Carbon::now());
|
||||
}
|
||||
|
||||
$records = $query->get();
|
||||
|
||||
$groupedAttendances = collect($records)
|
||||
->groupBy(function ($item) {
|
||||
return $item->biometric_emp_id . '_' . $item->punch_time->format('Y-m-d');
|
||||
})
|
||||
->map(function ($dayEntries) {
|
||||
$sorted = $dayEntries->sortBy('punch_time');
|
||||
$firstEntry = $sorted->first();
|
||||
$lastEntry = $sorted->last();
|
||||
|
||||
return [
|
||||
'id' => $firstEntry->id,
|
||||
'employee_code' => $firstEntry->biometric_emp_id,
|
||||
'date' => $firstEntry->punch_time->format('Y-m-d'),
|
||||
'clock_in' => $firstEntry->punch_time->format('H:i:s'),
|
||||
'clock_out' => $sorted->count() > 1 ? $lastEntry->punch_time->format('H:i:s') : null,
|
||||
'total_entries' => $sorted->count(),
|
||||
'terminal' => $firstEntry->terminal_alias ?? 'Agent',
|
||||
'sync_status' => $firstEntry->sync_status,
|
||||
];
|
||||
})->filter()->values();
|
||||
|
||||
// Attach employee names
|
||||
$empIds = $groupedAttendances->pluck('employee_code')->unique();
|
||||
$employees = \App\Models\Employee::with('user')->whereIn('biometric_emp_id', $empIds)->get()->keyBy('biometric_emp_id');
|
||||
|
||||
$groupedAttendances = $groupedAttendances->map(function($item) use ($employees) {
|
||||
$emp = $employees->get($item['employee_code']);
|
||||
$item['name'] = $emp && $emp->user ? $emp->user->name : 'Unknown';
|
||||
return $item;
|
||||
});
|
||||
|
||||
// Search filter
|
||||
if ($request->has('search') && !empty($request->search)) {
|
||||
$search = strtolower($request->search);
|
||||
$groupedAttendances = $groupedAttendances->filter(function ($item) use ($search) {
|
||||
return str_contains(strtolower($item['name']), $search) ||
|
||||
str_contains(strtolower($item['employee_code']), $search);
|
||||
});
|
||||
}
|
||||
|
||||
// Sorting
|
||||
if ($request->has('sort_field') && !empty($request->sort_field)) {
|
||||
$direction = $request->sort_direction ?? 'asc';
|
||||
$groupedAttendances = $groupedAttendances->sortBy($request->sort_field, SORT_REGULAR, $direction === 'desc');
|
||||
} else {
|
||||
$groupedAttendances = $groupedAttendances->sortByDesc('date');
|
||||
}
|
||||
|
||||
// Pagination
|
||||
$perPage = $request->per_page ?? 10;
|
||||
$currentPage = $request->page ?? 1;
|
||||
$total = $groupedAttendances->count();
|
||||
$items = $groupedAttendances->forPage($currentPage, $perPage)->values();
|
||||
|
||||
$biometricData = new LengthAwarePaginator(
|
||||
$items,
|
||||
$total,
|
||||
$perPage,
|
||||
$currentPage,
|
||||
[
|
||||
'path' => $request->url(),
|
||||
'pageName' => 'page',
|
||||
]
|
||||
);
|
||||
|
||||
return Inertia::render('hr/biometric-attendance/index', [
|
||||
'biometricData' => $biometricData,
|
||||
'filters' => $request->all(['search', 'start_date', 'end_date', 'sort_field', 'sort_direction', 'per_page']),
|
||||
'configurationMissing' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function show(Request $request, $employeeCode, $date)
|
||||
@@ -241,107 +120,21 @@ class BiometricAttendanceController extends Controller
|
||||
], 403);
|
||||
}
|
||||
|
||||
if (isDemo()) {
|
||||
$allDemoData = isSaas() ? $this->getSaasDemoData() : $this->getNonSaasDemoData();
|
||||
$attendances = collect($allDemoData)
|
||||
->filter(function ($item) use ($employeeCode, $date) {
|
||||
return $item['emp_code'] === $employeeCode &&
|
||||
date('Y-m-d', strtotime($item['punch_time'])) === $date;
|
||||
})
|
||||
->values()
|
||||
->toArray();
|
||||
} else {
|
||||
$company_setting = settings();
|
||||
$token = $company_setting['zkteco_auth_token'] ?? '';
|
||||
$api_urls = $company_setting['zkteco_api_url'] ?? '';
|
||||
$attendances = \App\Models\BiometricAttendance::where('biometric_emp_id', $employeeCode)
|
||||
->whereDate('punch_time', $date)
|
||||
->orderBy('punch_time', 'asc')
|
||||
->get();
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
if ($isDirectIP) {
|
||||
try {
|
||||
$zk = new \App\Library\ZKTeco\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,
|
||||
]);
|
||||
|
||||
$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);
|
||||
|
||||
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'] ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Process entries (already filtered by API)
|
||||
$dayEntries = collect($attendances)
|
||||
->sortBy('punch_time')
|
||||
->map(function ($item) {
|
||||
return [
|
||||
'id' => $item['id'],
|
||||
'punch_time' => $item['punch_time'],
|
||||
'time' => date('H:i:s', strtotime($item['punch_time'])),
|
||||
'punch_state_display' => $item['punch_state_display'] ?? 'Unknown',
|
||||
'verify_type_display' => $item['verify_type_display'] ?? 'Unknown',
|
||||
'terminal_alias' => $item['terminal_alias'] ?? 'Unknown'
|
||||
];
|
||||
})
|
||||
->values();
|
||||
$dayEntries = $attendances->map(function ($item) {
|
||||
return [
|
||||
'id' => $item->id,
|
||||
'punch_time' => $item->punch_time->format('Y-m-d H:i:s'),
|
||||
'time' => $item->punch_time->format('H:i:s'),
|
||||
'punch_state_display' => $item->punch_type == 0 ? 'Clock In' : 'Clock Out',
|
||||
'verify_type_display' => 'Unknown',
|
||||
'terminal_alias' => $item->terminal_alias ?? 'Unknown'
|
||||
];
|
||||
});
|
||||
|
||||
if ($dayEntries->isEmpty()) {
|
||||
return response()->json([
|
||||
@@ -368,221 +161,38 @@ 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 \App\Library\ZKTeco\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) {}
|
||||
|
||||
// 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) {
|
||||
try {
|
||||
$zk = new \App\Library\ZKTeco\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."));
|
||||
if (!Auth::user()->can('sync-biometric-attendance')) {
|
||||
return redirect()->back()->with('error', __('Permission Denied.'));
|
||||
}
|
||||
return redirect()->back()->with('error', __('Permission Denied.'));
|
||||
}
|
||||
|
||||
public function sync(Request $request, $id)
|
||||
{
|
||||
if (Auth::user()->can('sync-biometric-attendance')) {
|
||||
$biometricEmpId = $request->biometric_emp_id;
|
||||
$biometricId = $request->biometric_id;
|
||||
// $punchTime = $request->punch_time;
|
||||
$attedanceDate = $request->date;
|
||||
$clockInTime = $request->clock_in;
|
||||
$clockOutTime = $request->clock_out;
|
||||
|
||||
$company_setting = settings();
|
||||
|
||||
$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.'));
|
||||
}
|
||||
|
||||
$employee = Employee::with('user')->whereIn('created_by', getCompanyAndUsersId())->where('biometric_emp_id', $biometricEmpId)->first();
|
||||
|
||||
if ($employee) {
|
||||
|
||||
if (is_null($clockOutTime)) {
|
||||
return redirect()->back()->with('error', __("Still Employee is not Clock Out. So You Can't Sync That Attedance."));
|
||||
}
|
||||
|
||||
// Check Attendance Already Sync or not
|
||||
$attendance = AttendanceRecord::where('biometric_id', $biometricId)
|
||||
->where('date', $attedanceDate)
|
||||
->whereIn('created_by', getCompanyAndUsersId())
|
||||
->first();
|
||||
|
||||
if ($attendance) {
|
||||
return redirect()->back()->with('error', __('Attendance Is Alredady Sync.'));
|
||||
}
|
||||
|
||||
// Check if record already exists
|
||||
$exists = AttendanceRecord::where('employee_id', $employee->user_id)
|
||||
->where('date', $attedanceDate)
|
||||
->whereIn('created_by', getCompanyAndUsersId())
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
return redirect()->back()->with('error', __('Attendance record already exists for this employee and date.'));
|
||||
} else {
|
||||
$shift = Shift::where('id', $employee->shift_id)
|
||||
->where('status', 'active')
|
||||
->first();
|
||||
|
||||
if (!$shift) {
|
||||
$shift = Shift::whereIn('created_by', getCompanyAndUsersId())
|
||||
->where('status', 'active')
|
||||
->first();
|
||||
}
|
||||
|
||||
$policy = AttendancePolicy::where('id', $employee->attendance_policy_id)
|
||||
->where('status', 'active')
|
||||
->first();
|
||||
|
||||
if (!$policy) {
|
||||
$policy = AttendancePolicy::whereIn('created_by', getCompanyAndUsersId())
|
||||
->where('status', 'active')
|
||||
->first();
|
||||
}
|
||||
|
||||
|
||||
// Fetch all pending records
|
||||
$pendingRecords = \App\Models\BiometricAttendance::where('sync_status', 'pending')->get();
|
||||
|
||||
$groupedAttendances = $pendingRecords->groupBy(function ($item) {
|
||||
return $item->biometric_emp_id . '_' . $item->punch_time->format('Y-m-d');
|
||||
});
|
||||
|
||||
$syncedCount = 0;
|
||||
foreach ($groupedAttendances as $key => $dayEntries) {
|
||||
$sorted = $dayEntries->sortBy('punch_time');
|
||||
$firstEntry = $sorted->first();
|
||||
$lastEntry = $sorted->last();
|
||||
|
||||
$employee = Employee::with('user')->whereIn('created_by', getCompanyAndUsersId())->where('biometric_emp_id', $firstEntry->biometric_emp_id)->first();
|
||||
|
||||
if ($employee && $sorted->count() > 1) {
|
||||
$attedanceDate = $firstEntry->punch_time->format('Y-m-d');
|
||||
$clockInTime = $firstEntry->punch_time->format('H:i:s');
|
||||
$clockOutTime = $lastEntry->punch_time->format('H:i:s');
|
||||
|
||||
$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 = $biometricId;
|
||||
$attendance->biometric_id = $firstEntry->id;
|
||||
$attendance->shift_id = $shift?->id;
|
||||
$attendance->attendance_policy_id = $policy?->id;
|
||||
$attendance->date = $attedanceDate;
|
||||
@@ -590,17 +200,100 @@ class BiometricAttendanceController extends Controller
|
||||
$attendance->clock_out = $clockOutTime;
|
||||
$attendance->created_by = creatorId();
|
||||
$attendance->save();
|
||||
|
||||
$attendance->fresh(); // Reload to get relationships
|
||||
|
||||
$attendance->fresh();
|
||||
$attendance->processAttendance();
|
||||
|
||||
return redirect()->back()->with('success', __('Biometric data synced successfully.'));
|
||||
|
||||
// Mark as synced
|
||||
foreach ($dayEntries as $entry) {
|
||||
$entry->update(['sync_status' => 'synced']);
|
||||
}
|
||||
|
||||
$syncedCount++;
|
||||
} else {
|
||||
// Already exists, just mark as synced so they don't pile up in pending
|
||||
foreach ($dayEntries as $entry) {
|
||||
$entry->update(['sync_status' => 'synced']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return redirect()->back()->with('success', __("Bulk sync completed. Synced {$syncedCount} new records."));
|
||||
}
|
||||
|
||||
public function sync(Request $request, $id)
|
||||
{
|
||||
if (!Auth::user()->can('sync-biometric-attendance')) {
|
||||
return redirect()->back()->with('error', __('Permission Denied.'));
|
||||
}
|
||||
|
||||
$biometricEmpId = $request->biometric_emp_id;
|
||||
$biometricId = $request->biometric_id;
|
||||
$attedanceDate = $request->date;
|
||||
$clockInTime = $request->clock_in;
|
||||
$clockOutTime = $request->clock_out;
|
||||
|
||||
$employee = Employee::with('user')->whereIn('created_by', getCompanyAndUsersId())->where('biometric_emp_id', $biometricEmpId)->first();
|
||||
|
||||
if ($employee) {
|
||||
if (is_null($clockOutTime)) {
|
||||
return redirect()->back()->with('error', __("Still Employee is not Clock Out. So You Can't Sync That Attedance."));
|
||||
}
|
||||
|
||||
// Check if record already exists
|
||||
$exists = AttendanceRecord::where('employee_id', $employee->user_id)
|
||||
->where('date', $attedanceDate)
|
||||
->whereIn('created_by', getCompanyAndUsersId())
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
// Just mark it as synced so it doesn't show up again
|
||||
\App\Models\BiometricAttendance::where('biometric_emp_id', $biometricEmpId)->whereDate('punch_time', $attedanceDate)->update(['sync_status' => 'synced']);
|
||||
return redirect()->back()->with('error', __('Attendance record already exists for this employee and date.'));
|
||||
} else {
|
||||
return redirect()->back()->with('error', __('Employee not found'));
|
||||
$shift = Shift::where('id', $employee->shift_id)
|
||||
->where('status', 'active')
|
||||
->first();
|
||||
|
||||
if (!$shift) {
|
||||
$shift = Shift::whereIn('created_by', getCompanyAndUsersId())
|
||||
->where('status', 'active')
|
||||
->first();
|
||||
}
|
||||
|
||||
$policy = AttendancePolicy::where('id', $employee->attendance_policy_id)
|
||||
->where('status', 'active')
|
||||
->first();
|
||||
|
||||
if (!$policy) {
|
||||
$policy = AttendancePolicy::whereIn('created_by', getCompanyAndUsersId())
|
||||
->where('status', 'active')
|
||||
->first();
|
||||
}
|
||||
|
||||
$attendance = new AttendanceRecord();
|
||||
$attendance->employee_id = $employee->user_id;
|
||||
$attendance->biometric_id = $biometricId;
|
||||
$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(); // Reload to get relationships
|
||||
$attendance->processAttendance();
|
||||
|
||||
// Mark DB entries as synced
|
||||
\App\Models\BiometricAttendance::where('biometric_emp_id', $biometricEmpId)
|
||||
->whereDate('punch_time', $attedanceDate)
|
||||
->update(['sync_status' => 'synced']);
|
||||
|
||||
return redirect()->back()->with('success', __('Biometric data synced successfully.'));
|
||||
}
|
||||
} else {
|
||||
return redirect()->back()->with('error', __('Permission Denied.'));
|
||||
return redirect()->back()->with('error', __('Employee not found'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user