with(['employee.user.profile', 'media']) ->when(! $hasScopedAccess, function (Builder $query): void { $query->whereRaw('1 = 0'); }) ->when($hasScopedAccess && $scopedEmployeeId !== null, function (Builder $query) use ($scopedEmployeeId): void { $query->where('employee_id', $scopedEmployeeId); }) ->whereDate('attendance_date', '>=', $start->toDateString()) ->whereDate('attendance_date', '<', $end->toDateString()) ->orderBy('attendance_date') ->orderBy('check_in_at') ->get(); } public function todayAttendanceForEmployee(Employee $employee): ?array { $attendance = Attendance::query() ->with('media') ->where('employee_id', $employee->id) ->whereDate('attendance_date', today()) ->first(); return $attendance?->toArray(); } public function isOnLeaveToday(Employee $employee): bool { return LeaveRequest::query() ->approved() ->where('employee_id', $employee->id) ->whereDate('start_date', '<=', today()) ->whereDate('end_date', '>=', today()) ->exists(); } public function checkIn(array $validated, User $user): void { $employee = $this->resolveAuthenticatedEmployee($user); $this->runInTransaction( function () use ($employee, $validated): void { $existing = Attendance::query() ->where('employee_id', $employee->id) ->whereDate('attendance_date', today()) ->exists(); if ($existing) { throw ValidationException::withMessages([ 'attendance' => 'Anda sudah melakukan presensi masuk hari ini.', ]); } $attendance = Attendance::create([ 'employee_id' => $employee->id, 'attendance_date' => today(), 'check_in_at' => now(), 'check_in_latitude' => $validated['latitude'], 'check_in_longitude' => $validated['longitude'], ]); $this->mediaService->addBase64Image( $attendance, $validated['photo'], 'checkin', 'checkin', ); }, 'Gagal mencatat presensi masuk', ); $this->pushNotificationService->sendToRoles( '⏰ Presensi Masuk', "Karyawan {$user->profile?->full_name} melakukan presensi masuk.", ['owner', 'developer', 'direktur'], route('admin.hr.attendances.index'), ); } public function checkOut(array $validated, User $user): void { $employee = $this->resolveAuthenticatedEmployee($user); $workDurationMinutes = $this->runInTransaction( function () use ($employee, $validated): int { $attendance = Attendance::query() ->where('employee_id', $employee->id) ->whereDate('attendance_date', today()) ->first(); if ($attendance === null) { throw ValidationException::withMessages([ 'attendance' => 'Anda belum melakukan presensi masuk hari ini.', ]); } if ($attendance->check_out_at !== null) { throw ValidationException::withMessages([ 'attendance' => 'Anda sudah melakukan presensi pulang hari ini.', ]); } $checkOutAt = now(); $workDurationMinutes = (int) $attendance->check_in_at->diffInMinutes($checkOutAt); $attendance->update([ 'check_out_at' => $checkOutAt, 'check_out_latitude' => $validated['latitude'], 'check_out_longitude' => $validated['longitude'], 'work_duration_minutes' => $workDurationMinutes, ]); $this->mediaService->addBase64Image( $attendance, $validated['photo'], 'checkout', 'checkout', ); return $workDurationMinutes; }, 'Gagal mencatat presensi pulang', ); $this->pushNotificationService->sendToRoles( '⏰ Presensi Pulang', "Karyawan {$user->profile?->full_name} melakukan presensi pulang (Durasi kerja: ".round($workDurationMinutes / 60, 1).' jam).', ['owner', 'developer', 'direktur'], route('admin.hr.attendances.index'), ); } public function delete(Attendance $attendance, User $user): void { $attendance->loadMissing('employee.user.profile'); $employeeName = $attendance->employee?->user?->profile?->full_name; $date = $attendance->attendance_date; $this->runInTransaction( function () use ($attendance): void { $attendance->delete(); }, 'Gagal menghapus presensi', ); $this->pushNotificationService->sendToRoles( '🗑️ Presensi Dihapus', "Data presensi {$employeeName} tanggal {$date?->toDateString()} telah dihapus oleh {$user->profile?->full_name}.", ['owner', 'developer', 'direktur'], route('admin.hr.attendances.index'), ); } public function resolveAttendanceScope(User $user): array { $employee = $user->employee; $isManager = $user->hasAnyRole([Role::OWNER->value, Role::DEVELOPER->value, Role::DIREKTUR->value]); return [ 'scopedEmployeeId' => $isManager ? null : $employee?->id, 'hasScopedAccess' => $isManager || $employee !== null, 'isManager' => $isManager, 'employee' => $employee, 'canCheckIn' => $user->can(Permission::ATTENDANCES_CREATE->value) && $employee !== null, ]; } public function indexPageData(User $user, CarbonInterface $start, CarbonInterface $end): array { $scope = $this->resolveAttendanceScope($user); $employee = $scope['employee']; return [ 'attendances' => $this->listForCalendar( $start, $end, $scope['scopedEmployeeId'], $scope['hasScopedAccess'], ), 'todayAttendance' => $employee ? $this->todayAttendanceForEmployee($employee) : null, 'isOnLeave' => $employee ? $this->isOnLeaveToday($employee) : false, 'canCheckIn' => $scope['canCheckIn'], 'canManageAll' => $scope['isManager'], 'calendarRange' => [ 'start' => $start->toDateString(), 'end' => $end->toDateString(), ], ]; } }