latest('attendance_date') ->get() ->map(fn (Attendance $attendance) => $this->formatAttendance($attendance)); } public function getByMonth(int $year, int $month): Collection { return Attendance::with(['employee.user.userProfile', 'media']) ->whereYear('attendance_date', $year) ->whereMonth('attendance_date', $month) ->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 getMonthStats(int $year, int $month): 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(); } $attendanceCount = Attendance::whereYear('attendance_date', $year) ->whereMonth('attendance_date', $month) ->count(); $leaveDays = LeaveRequest::approved() ->where('start_date', '<=', $endOfMonth) ->where('end_date', '>=', $startOfMonth) ->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(); 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; } }