feat(attendance): add GPS summary dashboard card, punch source filter, and per-card GPS Google Maps indicators

This commit is contained in:
2026-09-14 17:37:10 +08:00
parent 66d43ca78a
commit b8d4ea9d12
421 changed files with 15065 additions and 14780 deletions

View File

@@ -119,6 +119,26 @@ class AttendanceRecordController extends Controller
$query->where('date', '<=', $request->date_to);
}
// Handle punch source / GPS filter
if ($request->has('source') && ! empty($request->source) && $request->source !== 'all') {
if ($request->source === 'gps') {
$query->where(function ($q) {
$q->whereNotNull('clock_in_latitude')
->orWhereNotNull('clock_out_latitude');
});
} elseif ($request->source === 'zkteco') {
$query->where(function ($q) {
$q->where('clock_in_source', 'zkteco')
->orWhere('clock_out_source', 'zkteco');
});
} else {
$query->where(function ($q) use ($request) {
$q->where('clock_in_source', $request->source)
->orWhere('clock_out_source', $request->source);
});
}
}
// Handle sorting
if ($request->has('sort_field') && ! empty($request->sort_field)) {
$sortField = $request->sort_field;
@@ -138,9 +158,11 @@ class AttendanceRecordController extends Controller
$attendanceRecords->getCollection()->transform(function ($record) {
if ($record->employee) {
$rawAvatar = $record->employee->getRawOriginal('avatar');
$record->employee->avatar = check_file($rawAvatar)
? get_file($rawAvatar)
: asset('storage/avatars/avatar.png');
$avatarUrl = null;
if ($rawAvatar) {
$avatarUrl = getImagePath($rawAvatar);
}
$record->employee->avatar_url = $avatarUrl;
}
// Add leave type information for on_leave records
@@ -169,12 +191,18 @@ class AttendanceRecordController extends Controller
$statsRecords = AttendanceRecord::whereIn('created_by', $companyUserIds)->get();
$todayStats = [
'present' => $statsRecords->where('status', 'present')->count(),
'on_leave' => LeaveApplication::whereIn('employee_id', function ($q) use ($companyUserIds) {
$q->select('user_id')->from('employees')->whereIn('created_by', $companyUserIds);
})->where('status', 'approved')->count(),
'late_arrivals' => $statsRecords->where('is_late', true)->count(),
'overtime' => $statsRecords->where('overtime_hours', '>', 0)->count(),
'present' => $statsRecords->where('status', 'present')->count(),
'on_leave' => LeaveApplication::whereIn('employee_id', function ($q) use ($companyUserIds) {
$q->select('user_id')->from('employees')->whereIn('created_by', $companyUserIds);
})->where('status', 'approved')->count(),
'late_arrivals' => $statsRecords->where('is_late', true)->count(),
'overtime' => $statsRecords->where('overtime_hours', '>', 0)->count(),
'gps_clock_ins' => $statsRecords->filter(function ($r) {
return !empty($r->clock_in_latitude) || !empty($r->clock_out_latitude);
})->count(),
'biometric_clock_ins' => $statsRecords->filter(function ($r) {
return $r->clock_in_source === 'zkteco' || $r->clock_out_source === 'zkteco';
})->count(),
];
} else {
$today = Carbon::today();
@@ -184,12 +212,18 @@ class AttendanceRecordController extends Controller
->get();
$todayStats = [
'present' => $todayRecords->where('status', 'present')->count(),
'on_leave' => LeaveApplication::whereIn('employee_id', function ($q) use ($companyUserIds) {
$q->select('user_id')->from('employees')->whereIn('created_by', $companyUserIds);
})->where('status', 'approved')->whereDate('start_date', '<=', $today)->whereDate('end_date', '>=', $today)->count(),
'late_arrivals' => $todayRecords->where('is_late', true)->count(),
'overtime' => $todayRecords->where('overtime_hours', '>', 0)->count(),
'present' => $todayRecords->where('status', 'present')->count(),
'on_leave' => LeaveApplication::whereIn('employee_id', function ($q) use ($companyUserIds) {
$q->select('user_id')->from('employees')->whereIn('created_by', $companyUserIds);
})->where('status', 'approved')->whereDate('start_date', '<=', $today)->whereDate('end_date', '>=', $today)->count(),
'late_arrivals' => $todayRecords->where('is_late', true)->count(),
'overtime' => $todayRecords->where('overtime_hours', '>', 0)->count(),
'gps_clock_ins' => $todayRecords->filter(function ($r) {
return !empty($r->clock_in_latitude) || !empty($r->clock_out_latitude);
})->count(),
'biometric_clock_ins' => $todayRecords->filter(function ($r) {
return $r->clock_in_source === 'zkteco' || $r->clock_out_source === 'zkteco';
})->count(),
];
}
@@ -204,7 +238,7 @@ class AttendanceRecordController extends Controller
'employees' => $this->getFilteredEmployees(),
'branches' => $branches,
'hasSampleFile' => file_exists(storage_path('uploads/sample/sample-attendance-record.xlsx')),
'filters' => $request->all(['search', 'employee_id', 'branch_id', 'status', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']),
'filters' => $request->all(['search', 'employee_id', 'branch_id', 'status', 'source', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']),
'todayStats' => $todayStats,
'leaveTypes' => \App\Models\LeaveType::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->get(['id', 'name']),
]);