user(); $isAdmin = self::hasAnyRole([Role::DEVELOPER, Role::OWNER]); $hrSettings = app(HRSettings::class); $employeeId = $isAdmin ? null : $user->employee?->id; return [ 'attendances' => $this->getByMonth($year, $month, $employeeId), 'leaves' => $this->getLeavesByMonth($year, $month, $employeeId), 'employees' => $isAdmin ? $this->employeeService->getAll() : [], 'todayAttendance' => $isAdmin ? null : $this->getToday(), 'currentYear' => $year, 'currentMonth' => $month, 'monthStats' => $this->getMonthStats($year, $month, $employeeId), 'hrSettings' => [ 'scheduled_check_in_time' => $hrSettings->scheduled_check_in_time, 'scheduled_check_out_time' => $hrSettings->scheduled_check_out_time, ], 'isOnLeave' => $isAdmin ? false : $this->isOnLeave($user), 'canCheckIn' => $isAdmin ? false : $user->employee !== null, 'isAdmin' => $isAdmin, ]; } 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 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 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); } 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 checkIn(array $data): Attendance { $employee = auth()->user()->employee; if (! $employee) { throw ValidationException::withMessages([ 'employee' => 'Anda tidak terdaftar sebagai karyawan.', ]); } $today = now()->toDateString(); $existing = Attendance::where('employee_id', $employee->id) ->where('attendance_date', $today) ->first(); if ($existing) { throw ValidationException::withMessages([ 'attendance' => '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'], 'checkin', 'attendances'); } NotificationService::notify( roles: [Role::OWNER, Role::DEVELOPER, Role::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'], 'checkout', 'attendances'); } NotificationService::notify( roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR], title: 'Presensi Pulang', body: 'Presensi pulang oleh '.auth()->user()->full_name.'.', url: route('admin.hr.attendances.index'), ); return $attendance; } 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, ]; } 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('checkin'); $checkOutMedia = $attendance->getFirstMedia('checkout'); $toArray['check_in_photo'] = $checkInMedia ? Cache::remember("attendance_check_in_{$checkInMedia->id}", now()->addMinutes(55), fn () => $this->s3Service->getTemporaryUrl($checkInMedia->getPath())) : null; $toArray['check_out_photo'] = $checkOutMedia ? Cache::remember("attendance_check_out_{$checkOutMedia->id}", now()->addMinutes(55), fn () => $this->s3Service->getTemporaryUrl($checkOutMedia->getPath())) : null; return $toArray; } }