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 isOnLeave(User $user): bool { $employee = $user->employee; if (! $employee) { return false; } return LeaveRequest::approved() ->where('employee_id', $employee->id) ->where('start_date', '<=', now()->toDateString()) ->where('end_date', '>=', now()->toDateString()) ->exists(); } public function getLeavesByMonth(int $year, int $month, ?int $employeeId = null): Collection { $startOfMonth = Carbon::createFromDate($year, $month, 1)->startOfMonth(); $endOfMonth = $startOfMonth->copy()->endOfMonth(); return LeaveRequest::approved() ->with('employee.user.userProfile') ->where('start_date', '<=', $endOfMonth) ->where('end_date', '>=', $startOfMonth) ->when($employeeId, fn ($q) => $q->where('employee_id', $employeeId)) ->get() ->map(fn (LeaveRequest $leave) => [ 'id' => $leave->id, 'employee_id' => $leave->employee_id, 'start_date' => $leave->start_date->toDateString(), 'end_date' => $leave->end_date->toDateString(), 'total_days' => $leave->total_days, 'status' => $leave->status->value, 'employee_name' => $leave->employee?->user?->userProfile?->full_name ?? '-', ]); } public function getAllEmployees(): Collection { return Employee::with(['user.userProfile', 'user.roles']) ->whereHas('user', fn ($q) => $q->where('is_active', true)) ->get() ->map(fn (Employee $employee) => [ 'id' => $employee->id, 'name' => $employee->user?->userProfile?->full_name ?? '-', ]); } public function getMonthStats(int $year, int $month, ?int $employeeId = null): array { $startOfMonth = Carbon::createFromDate($year, $month, 1)->startOfMonth(); $endOfMonth = $startOfMonth->copy()->endOfMonth(); $today = Carbon::today(); $statEnd = $endOfMonth->lte($today) ? $endOfMonth : $today; $workingDays = 0; $current = $startOfMonth->copy(); while ($current->lte($statEnd)) { if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) { $workingDays++; } $current->addDay(); } if ($employeeId) { return $this->getMonthStatsForEmployee($year, $month, $startOfMonth, $statEnd, $workingDays, $employeeId); } return $this->getMonthStatsForAll($year, $month, $startOfMonth, $statEnd, $workingDays); } private function getMonthStatsForEmployee(int $year, int $month, Carbon $startOfMonth, Carbon $statEnd, int $workingDays, int $employeeId): array { $attendanceQuery = Attendance::whereYear('attendance_date', $year) ->whereMonth('attendance_date', $month) ->where('attendance_date', '<=', $statEnd->toDateString()) ->where('employee_id', $employeeId); $attendanceDates = $attendanceQuery->pluck('attendance_date') ->map(fn ($d) => Carbon::parse($d)->toDateString()) ->filter(fn ($d) => ! in_array(Carbon::parse($d)->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) ->unique() ->values(); $attendanceCount = $attendanceDates->count(); $leaveQuery = LeaveRequest::approved() ->where('start_date', '<=', $statEnd) ->where('end_date', '>=', $startOfMonth) ->where('employee_id', $employeeId); $leaveRequests = $leaveQuery->get(); $leaveCount = $leaveRequests->count(); $leaveDates = collect(); $leaveRequests->each(function ($leave) use (&$leaveDates, $startOfMonth, $statEnd) { $leaveStart = Carbon::parse($leave->start_date)->startOfDay()->lte($startOfMonth) ? $startOfMonth->copy() : Carbon::parse($leave->start_date)->startOfDay(); $leaveEnd = Carbon::parse($leave->end_date)->startOfDay()->gte($statEnd) ? $statEnd->copy() : Carbon::parse($leave->end_date)->startOfDay(); $current = $leaveStart->copy(); while ($current->lte($leaveEnd)) { if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) { $leaveDates->push($current->toDateString()); } $current->addDay(); } }); $leaveDates = $leaveDates->unique()->values(); $coveredDates = $attendanceDates->merge($leaveDates)->unique()->count(); $absent = max(0, $workingDays - $coveredDates); return [ 'working_days' => $workingDays, 'present' => $attendanceCount, 'absent' => $absent, 'leave' => $leaveCount, ]; } private function getMonthStatsForAll(int $year, int $month, Carbon $startOfMonth, Carbon $statEnd, int $workingDays): array { $totalEmployees = Employee::whereHas('user', fn ($q) => $q->where('is_active', true))->count(); $presentCount = Attendance::whereYear('attendance_date', $year) ->whereMonth('attendance_date', $month) ->where('attendance_date', '<=', $statEnd->toDateString()) ->whereHas('employee', fn ($q) => $q->whereHas('user', fn ($uq) => $uq->where('is_active', true))) ->count(); $leaveRequests = LeaveRequest::approved() ->where('start_date', '<=', $statEnd) ->where('end_date', '>=', $startOfMonth) ->whereHas('employee', fn ($q) => $q->whereHas('user', fn ($uq) => $uq->where('is_active', true))) ->get(); $leaveDays = 0; $leaveRequests->each(function ($leave) use (&$leaveDays, $startOfMonth, $statEnd) { $leaveStart = Carbon::parse($leave->start_date)->startOfDay()->lte($startOfMonth) ? $startOfMonth->copy() : Carbon::parse($leave->start_date)->startOfDay(); $leaveEnd = Carbon::parse($leave->end_date)->startOfDay()->gte($statEnd) ? $statEnd->copy() : Carbon::parse($leave->end_date)->startOfDay(); $current = $leaveStart->copy(); while ($current->lte($leaveEnd)) { if (! in_array($current->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY])) { $leaveDays++; } $current->addDay(); } }); $absent = max(0, ($totalEmployees * $workingDays) - $presentCount - $leaveDays); return [ 'working_days' => $workingDays, 'present' => $presentCount, 'absent' => $absent, 'leave' => $leaveDays, 'total_employees' => $totalEmployees, ]; } 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; } }