latest('attendance_date') ->get() ->map(fn (Attendance $attendance) => $this->formatAttendance($attendance)); } public function getByMonth(int $year, int $month, ?int $employeeId = null): Collection { return Attendance::with(['employee.user.userProfile', 'media']) ->whereYear('attendance_date', $year) ->whereMonth('attendance_date', $month) ->when($employeeId, fn ($q) => $q->where('employee_id', $employeeId)) ->get() ->map(fn (Attendance $attendance) => $this->formatAttendance($attendance)); } public function getByDate(string $date): ?array { $attendance = Attendance::with(['employee.user.userProfile', 'media']) ->where('attendance_date', $date) ->where('employee_id', auth()->user()->employee?->id) ->first(); return $attendance ? $this->formatAttendance($attendance) : null; } public function getToday(): ?array { return $this->getByDate(now()->toDateString()); } public function getAllEmployees(): Collection { return Employee::with(['user.userProfile', 'user.roles']) ->whereHas('user', fn ($q) => $q->where('is_active', true)) ->get(); } public function getMonthStats(int $year, int $month, ?int $employeeId = null): array { $startOfMonth = Carbon::createFromDate($year, $month, 1)->startOfMonth(); $endOfMonth = $startOfMonth->copy()->endOfMonth(); $workingDays = 0; $current = $startOfMonth->copy(); while ($current->lte($endOfMonth)) { if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) { $workingDays++; } $current->addDay(); } $attendanceQuery = Attendance::whereYear('attendance_date', $year) ->whereMonth('attendance_date', $month); if ($employeeId) { $attendanceQuery->where('employee_id', $employeeId); } $attendanceCount = $attendanceQuery->count(); $leaveQuery = LeaveRequest::approved() ->where('start_date', '<=', $endOfMonth) ->where('end_date', '>=', $startOfMonth); if ($employeeId) { $leaveQuery->where('employee_id', $employeeId); } $leaveDays = $leaveQuery->get() ->reduce(function ($carry, $leave) use ($startOfMonth, $endOfMonth) { $leaveStart = max($leave->start_date->timestamp, $startOfMonth->timestamp); $leaveEnd = min($leave->end_date->timestamp, $endOfMonth->timestamp); $days = Carbon::createFromTimestamp($leaveStart)->diffInDays(Carbon::createFromTimestamp($leaveEnd)) + 1; return $carry + max(0, $days); }, 0); return [ 'working_days' => $workingDays, 'present' => $attendanceCount, 'absent' => max(0, $workingDays - $attendanceCount - $leaveDays), 'leave' => $leaveDays, ]; } public function checkIn(array $data): Attendance { $employee = auth()->user()->employee; if (! $employee) { throw new \Exception('Anda tidak terdaftar sebagai karyawan.'); } $today = now()->toDateString(); $existing = Attendance::where('employee_id', $employee->id) ->where('attendance_date', $today) ->first(); if ($existing) { throw new \Exception('Anda sudah melakukan presensi hari ini.'); } $attendance = Attendance::create([ 'employee_id' => $employee->id, 'attendance_date' => $today, 'check_in_at' => now(), 'check_in_latitude' => $data['latitude'], 'check_in_longitude' => $data['longitude'], ]); if (! empty($data['photo'])) { $this->registerMediaFromBase64($attendance, $data['photo'], 'check-in', 'attendances'); } NotificationService::notify( roles: ['Owner', 'Developer', 'Direktur'], title: 'Presensi Masuk', body: 'Presensi masuk oleh '.auth()->user()->full_name.'.', url: route('admin.hr.attendances.index'), ); return $attendance; } public function checkOut(Attendance $attendance, array $data): Attendance { $attendance->update([ 'check_out_at' => now(), 'check_out_latitude' => $data['latitude'], 'check_out_longitude' => $data['longitude'], ]); if (! empty($data['photo'])) { $this->registerMediaFromBase64($attendance, $data['photo'], 'check-out', 'attendances'); } NotificationService::notify( roles: ['Owner', 'Developer', 'Direktur'], title: 'Presensi Pulang', body: 'Presensi pulang oleh '.auth()->user()->full_name.'.', url: route('admin.hr.attendances.index'), ); return $attendance; } private function formatAttendance(Attendance $attendance): array { $toArray = $attendance->toArray(); $toArray['employee_name'] = $attendance->employee?->user?->userProfile?->full_name ?? '-'; if (is_null($toArray['work_duration_minutes']) && $attendance->check_in_at) { $checkIn = Carbon::parse($attendance->check_in_at); $end = $checkIn->toDateString() === now()->toDateString() ? now() : $checkIn->copy()->endOfDay(); $toArray['work_duration_minutes'] = $checkIn->diffInMinutes($end); } $checkInMedia = $attendance->getFirstMedia('check-in'); $checkOutMedia = $attendance->getFirstMedia('check-out'); $toArray['check_in_photo'] = $checkInMedia ? Cache::remember("attendance_check_in_{$checkInMedia->id}", now()->addMinutes(55), fn () => $this->s3Service->getTemporaryUrl($checkInMedia->file_name)) : null; $toArray['check_out_photo'] = $checkOutMedia ? Cache::remember("attendance_check_out_{$checkOutMedia->id}", now()->addMinutes(55), fn () => $this->s3Service->getTemporaryUrl($checkOutMedia->file_name)) : null; return $toArray; } }