diff --git a/app/Domains/Management/Attendance/HTTP/Controllers/V1/AttendanceReportController.php b/app/Domains/Management/Attendance/HTTP/Controllers/V1/AttendanceReportController.php new file mode 100644 index 000000000..471223e99 --- /dev/null +++ b/app/Domains/Management/Attendance/HTTP/Controllers/V1/AttendanceReportController.php @@ -0,0 +1,81 @@ +applyPermissions( + 'attendances', + [], + [ + 'getMonthlyReport' => 'list', + 'getCalendarDays' => 'list', + ] + ); + } + + /** + * Get monthly attendance report for employees (paginated) + * Includes per-employee counts and day-by-day breakdown + */ + public function getMonthlyReport(Request $request) + { + $request->validate([ + 'month' => 'required|integer|between:1,12', + 'year' => 'required|integer|min:2000|max:2100', + 'user_id' => 'nullable|integer|exists:users,id', + 'search' => 'nullable|string|max:100', + 'per_page' => 'nullable|integer|min:1|max:100', + ]); + + try { + $params = $request->only(['month', 'year', 'user_id', 'search', 'per_page']); + + // Re-format month for service if needed, but our service handles the params array + $report = $this->reportService->getMonthlyReport($params); + + return $this->sendPaginatedResponse($report); + } catch (Exception $exception) { + Log::error("Attendance Report Error: " . $exception->getMessage()); + return $this->sendFailedResponse($exception->getMessage(), 400); + } + } + + /** + * Get the calendar days for a specific month (for calendar headers) + */ + public function getCalendarDays(Request $request) + { + $request->validate([ + 'month' => 'required|integer|between:1,12', + 'year' => 'required|integer|min:2000|max:2100', + ]); + + try { + $startDate = Carbon::createFromDate($request->year, $request->month, 1)->startOfMonth(); + $endDate = $startDate->copy()->endOfMonth(); + + $days = $this->reportService->getCalendarDays($startDate, $endDate); + + return $this->sendSuccessResponse([ + 'days' => $days, + 'month_name' => $startDate->format('F Y'), + 'total_days' => count($days) + ]); + } catch (Exception $exception) { + Log::error("Calendar Days Error: " . $exception->getMessage()); + return $this->sendFailedResponse($exception->getMessage(), 400); + } + } +} diff --git a/app/Domains/Management/Attendance/Services/AttendanceReportService.php b/app/Domains/Management/Attendance/Services/AttendanceReportService.php new file mode 100644 index 000000000..d1e9e6fbd --- /dev/null +++ b/app/Domains/Management/Attendance/Services/AttendanceReportService.php @@ -0,0 +1,197 @@ +startOfMonth(); + } else { + // Assume $month is "Y-m" + try { + $startDate = Carbon::parse($month)->startOfMonth(); + } catch (\Exception $e) { + // Fallback or handle invalid format + $startDate = now()->startOfMonth(); + } + } + + $endDate = $startDate->copy()->endOfMonth(); + $userId = $params['user_id'] ?? null; + $search = $params['search'] ?? null; + $perPage = $params['per_page'] ?? 15; + + // 1. Get calendar days for the month + $calendarDays = $this->getCalendarDays($startDate, $endDate); + + // 2. Base query for users + $usersQuery = User::query() + ->select(['id', 'name', 'job_number']) + ->where('status', 'active'); + + if (function_exists('company') && company() && company()->id) { + $usersQuery->where('main_company_id', company()->id); + } + + if ($userId) { + $usersQuery->where('id', $userId); + } + + if ($search) { + $usersQuery->where('name', 'like', "%{$search}%"); + } + + // 3. Paginate users + $users = $usersQuery->paginate($perPage); + + // 4. Fetch attendances and leaves for these users + $userIds = $users->pluck('id')->toArray(); + + $attendancesQuery = Attendance::whereIn('user_id', $userIds) + ->whereBetween('date', [$startDate->toDateString(), $endDate->toDateString()]); + + if (function_exists('company') && company() && company()->id) { + $attendancesQuery->where('company_id', company()->id); + } + + $attendances = $attendancesQuery->get()->groupBy('user_id'); + + + $leaves = Leave::whereIn('user_id', $userIds) + ->where('status', LeaveEnum::Approved->value) + ->where(function ($query) use ($startDate, $endDate) { + $query->whereBetween('start_date', [$startDate->toDateString(), $endDate->toDateString()]) + ->orWhereBetween('end_date', [$startDate->toDateString(), $endDate->toDateString()]) + ->orWhere(function ($q) use ($startDate, $endDate) { + $q->where('start_date', '<=', $startDate->toDateString()) + ->where('end_date', '>=', $endDate->toDateString()); + }); + }) + ->get() + ->groupBy('user_id'); + + // 5. Process data per user + $reportData = $users->getCollection()->map(function ($user) use ($attendances, $leaves, $calendarDays) { + $userAttendances = $attendances->get($user->id, collect())->keyBy(function($a) { + return Carbon::parse($a->date)->toDateString(); + }); + $userLeaves = $leaves->get($user->id, collect()); + + $presentCount = 0; + $leaveCount = 0; + $absentCount = 0; + $dayBreakdown = []; + + foreach ($calendarDays as $day) { + $dateString = $day['date']; + $status = 'absent'; + + $attendance = $userAttendances->get($dateString); + + if ($attendance && $attendance->check_in) { + $status = 'present'; + $presentCount++; + } else { + $isOnLeave = $userLeaves->contains(function ($leave) use ($dateString) { + return $dateString >= Carbon::parse($leave->start_date)->toDateString() && + $dateString <= Carbon::parse($leave->end_date)->toDateString(); + }); + + if ($isOnLeave) { + $status = 'leave'; + $leaveCount++; + } else { + if (!$day['is_weekend'] && !$day['is_holiday']) { + $absentCount++; + } else { + $status = $day['is_holiday'] ? 'holiday' : 'weekend'; + } + } + } + + $dayBreakdown[] = [ + 'date' => $dateString, + 'status' => $status, + 'check_in' => $attendance->check_in ?? null, + 'check_out' => $attendance->check_out ?? null, + 'is_weekend' => $day['is_weekend'], + 'is_holiday' => $day['is_holiday'], + 'holiday_name' => $day['holiday_name'], + ]; + } + + return [ + 'employee_id' => $user->id, + 'full_name' => $user->name, + 'job_number' => $user->job_number, + 'present_days_count' => $presentCount, + 'leave_days_count' => $leaveCount, + 'absent_days_count' => $absentCount, + 'day_breakdown' => $dayBreakdown, + ]; + }); + + // 6. Return paginated result + $users->setCollection($reportData); + + return $users; + } + + /** + * Get list of days in the month with their metadata + */ + public function getCalendarDays(Carbon $startDate, Carbon $endDate): array + { + $calendarDays = []; + $period = CarbonPeriod::create($startDate, $endDate); + + // Fetch all active official holidays in this range at once for efficiency + $holidays = OfficialHoliday::where('is_active', true) + ->where(function ($q) use ($startDate, $endDate) { + $q->whereBetween('start_date', [$startDate->toDateString(), $endDate->toDateString()]) + ->orWhereBetween('end_date', [$startDate->toDateString(), $endDate->toDateString()]) + ->orWhere(function ($subQ) use ($startDate, $endDate) { + $subQ->where('start_date', '<=', $startDate->toDateString()) + ->where('end_date', '>=', $endDate->toDateString()); + }); + }) + ->get(); + + foreach ($period as $date) { + $dateString = $date->toDateString(); + $holiday = $holidays->first(function ($h) use ($dateString) { + return $dateString >= $h->start_date->toDateString() && + $dateString <= $h->end_date->toDateString(); + }); + + $calendarDays[] = [ + 'date' => $dateString, + 'day' => $date->day, + 'day_name' => $date->format('l'), + 'is_weekend' => $date->isWeekend(), + 'is_holiday' => !is_null($holiday), + 'holiday_name' => $holiday?->name, + ]; + } + + return $calendarDays; + } +} diff --git a/routes/apis/management.php b/routes/apis/management.php index aeddf4317..25b319adc 100644 --- a/routes/apis/management.php +++ b/routes/apis/management.php @@ -42,6 +42,8 @@ use App\Domains\Management\EmployeeRequests\Http\Controllers\V1\EmployeeRequestController; use App\Domains\Management\EmployeeRequests\Http\Controllers\V1\RequestTypeController; use App\Domains\Management\EmployeeRequests\Http\Controllers\V1\RequestSettingsController; +use App\Domains\Management\Attendance\HTTP\Controllers\V1\AttendanceReportController; + // Rewards and Penalties Route::get('rewards/export', [RewardAndPenaltyController::class, 'export']); Route::post('rewards/import', [RewardAndPenaltyController::class, 'import']); @@ -80,6 +82,9 @@ Route::prefix('attendances')->group(function () { Route::prefix('dashboard')->group(function () { Route::get('/', [DashboardController::class, 'index']); + Route::get('monthly-report-detailed', [AttendanceReportController::class, 'getMonthlyReport']); + Route::get('calendar-days', [AttendanceReportController::class, 'getCalendarDays']); + Route::get('/attendance-spreadsheet', [DashboardController::class, 'attendanceSpreadsheet']); Route::get('/employees-on-leave', [DashboardController::class, 'employeesOnLeave']); Route::get('/daily-statistics', [DashboardController::class, 'dailyStatistics']);