diff --git a/app/Http/Controllers/Admin/HR/AttendanceController.php b/app/Http/Controllers/Admin/HR/AttendanceController.php index 25e6591..6d39ad0 100644 --- a/app/Http/Controllers/Admin/HR/AttendanceController.php +++ b/app/Http/Controllers/Admin/HR/AttendanceController.php @@ -52,4 +52,12 @@ public function byDate(Request $request): ?array return $this->service->getByDate($date); } + + public function byMonth(Request $request): array + { + $year = $request->integer('year', now()->year); + $month = $request->integer('month', now()->month); + + return $this->service->getByMonthData($year, $month); + } } diff --git a/app/Services/Admin/HR/AttendanceService.php b/app/Services/Admin/HR/AttendanceService.php index ec5c9e4..d722c3e 100644 --- a/app/Services/Admin/HR/AttendanceService.php +++ b/app/Services/Admin/HR/AttendanceService.php @@ -29,7 +29,7 @@ public function __construct( public function getIndexData(int $year, int $month): array { $user = auth()->user(); - $isAdmin = self::hasAnyRole([Role::DEVELOPER, Role::OWNER]); + $isAdmin = self::hasAnyRole([Role::DEVELOPER, Role::OWNER, Role::ADMIN_TOKO, Role::DIREKTUR]); $hrSettings = app(HRSettings::class); $employeeId = $isAdmin ? null : $user->employee?->id; @@ -57,9 +57,9 @@ public function getByMonth(int $year, int $month, ?int $employeeId = null): Coll return Attendance::with(['employee.user.userProfile', 'media']) ->whereYear('attendance_date', $year) ->whereMonth('attendance_date', $month) - ->when($employeeId, fn ($q) => $q->where('employee_id', $employeeId)) + ->when($employeeId, fn($q) => $q->where('employee_id', $employeeId)) ->get() - ->map(fn (Attendance $attendance) => $this->formatAttendance($attendance)); + ->map(fn(Attendance $attendance) => $this->formatAttendance($attendance)); } public function getByDate(string $date): ?array @@ -72,6 +72,18 @@ public function getByDate(string $date): ?array return $attendance ? $this->formatAttendance($attendance) : null; } + public function getByMonthData(int $year, int $month): array + { + $employeeId = null; + + return [ + 'attendances' => $this->getByMonth($year, $month, $employeeId), + 'leaves' => $this->getLeavesByMonth($year, $month, $employeeId), + 'employees' => $this->employeeService->getAll(), + 'monthStats' => $this->getMonthStats($year, $month, $employeeId), + ]; + } + public function getToday(): ?array { return $this->getByDate(now()->toDateString()); @@ -86,9 +98,9 @@ public function getLeavesByMonth(int $year, int $month, ?int $employeeId = null) ->with('employee.user.userProfile') ->where('start_date', '<=', $endOfMonth) ->where('end_date', '>=', $startOfMonth) - ->when($employeeId, fn ($q) => $q->where('employee_id', $employeeId)) + ->when($employeeId, fn($q) => $q->where('employee_id', $employeeId)) ->get() - ->map(fn (LeaveRequest $leave) => [ + ->map(fn(LeaveRequest $leave) => [ 'id' => $leave->id, 'employee_id' => $leave->employee_id, 'start_date' => $leave->start_date->toDateString(), @@ -174,7 +186,7 @@ public function checkIn(array $data): Attendance NotificationService::notify( roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR], title: 'Presensi Masuk', - body: 'Presensi masuk oleh '.auth()->user()->full_name.'.', + body: 'Presensi masuk oleh ' . auth()->user()->full_name . '.', url: route('admin.hr.attendances.index', ['highlight' => $attendance->id]), ); @@ -196,7 +208,7 @@ public function checkOut(Attendance $attendance, array $data): Attendance NotificationService::notify( roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR], title: 'Presensi Pulang', - body: 'Presensi pulang oleh '.auth()->user()->full_name.'.', + body: 'Presensi pulang oleh ' . auth()->user()->full_name . '.', url: route('admin.hr.attendances.index', ['highlight' => $attendance->id]), ); @@ -211,8 +223,8 @@ private function getMonthStatsForEmployee(int $year, int $month, Carbon $startOf ->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])) + ->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(); @@ -252,18 +264,31 @@ private function getMonthStatsForEmployee(int $year, int $month, Carbon $startOf 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(); + $totalEmployees = Employee::whereHas('user', function ($q) { + $q->active() + ->whereHas('roles', fn($rq) => $rq->whereHas('permissions', fn($pq) => $pq->where('name', 'attendances.view'))); + })->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))) + ->whereHas('employee', function ($q) { + $q->whereHas('user', function ($uq) { + $uq->active() + ->whereHas('roles', fn($rq) => $rq->whereHas('permissions', fn($pq) => $pq->where('name', 'attendances.view'))); + }); + }) ->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))) + ->whereHas('employee', function ($q) { + $q->whereHas('user', function ($uq) { + $uq->active() + ->whereHas('roles', fn($rq) => $rq->whereHas('permissions', fn($pq) => $pq->where('name', 'attendances.view'))); + }); + }) ->get(); $leaveDays = 0; @@ -307,11 +332,11 @@ private function formatAttendance(Attendance $attendance): array $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())) + ? 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())) + ? Cache::remember("attendance_check_out_{$checkOutMedia->id}", now()->addMinutes(55), fn() => $this->s3Service->getTemporaryUrl($checkOutMedia->getPath())) : null; return $toArray; diff --git a/resources/js/pages/admin/hr/attendance/index.tsx b/resources/js/pages/admin/hr/attendance/index.tsx index c9e2479..d8f4b05 100644 --- a/resources/js/pages/admin/hr/attendance/index.tsx +++ b/resources/js/pages/admin/hr/attendance/index.tsx @@ -10,7 +10,7 @@ import { DialogTitle, } from '@/components/ui/dialog'; import { - index as attendanceIndex, + byMonth as attendanceByMonth, store, update, } from '@/routes/admin/hr/attendances'; @@ -24,7 +24,7 @@ import { LogIn, LogOut, } from 'lucide-react'; -import { useMemo, useState } from 'react'; +import { useMemo, useState, useCallback } from 'react'; import { toast } from 'sonner'; type Attendance = { @@ -147,9 +147,9 @@ function formatMinutes(minutes: number | null): string { } export default function AttendanceIndex({ - attendances, - leaves, - employees = [], + attendances: initialAttendances, + leaves: initialLeaves, + employees: initialEmployees = [], todayAttendance, currentYear, currentMonth, @@ -174,10 +174,30 @@ export default function AttendanceIndex({ const [detailAttendance, setDetailAttendance] = useState( null, ); + const [attendances, setAttendances] = useState(initialAttendances); + const [leaves, setLeaves] = useState(initialLeaves); + const [employees, setEmployees] = useState(initialEmployees); + const [loadingMonth, setLoadingMonth] = useState(false); const viewYear = viewDate.getFullYear(); const viewMonth = viewDate.getMonth() + 1; + const fetchMonthData = useCallback(async (year: number, month: number) => { + setLoadingMonth(true); + try { + const url = attendanceByMonth.url({ query: { year, month } }); + const res = await fetch(url); + if (res.ok) { + const data = await res.json(); + setAttendances(data.attendances); + setLeaves(data.leaves); + setEmployees(data.employees); + } + } finally { + setLoadingMonth(false); + } + }, []); + const attendanceByDate = useMemo(() => { const map = new Map(); attendances.forEach((att) => { @@ -254,47 +274,20 @@ export default function AttendanceIndex({ const handlePrevMonth = () => { const newDate = subMonths(viewDate, 1); setViewDate(newDate); - - if (!isAdmin) { - router.get( - attendanceIndex.url(), - { - year: newDate.getFullYear(), - month: newDate.getMonth() + 1, - }, - { preserveState: true, preserveScroll: true }, - ); - } + fetchMonthData(newDate.getFullYear(), newDate.getMonth() + 1); }; const handleNextMonth = () => { const newDate = addMonths(viewDate, 1); setViewDate(newDate); - - if (!isAdmin) { - router.get( - attendanceIndex.url(), - { - year: newDate.getFullYear(), - month: newDate.getMonth() + 1, - }, - { preserveState: true, preserveScroll: true }, - ); - } + fetchMonthData(newDate.getFullYear(), newDate.getMonth() + 1); }; const handleGoToToday = () => { const now = new Date(); setViewDate(now); setSelectedDate(now); - - if (!isAdmin) { - router.get( - attendanceIndex.url(), - { year: now.getFullYear(), month: now.getMonth() + 1 }, - { preserveState: true, preserveScroll: true }, - ); - } + fetchMonthData(now.getFullYear(), now.getMonth() + 1); }; const handleCameraCapture = (dataUrl: string) => { @@ -374,17 +367,10 @@ export default function AttendanceIndex({ Menampilkan presensi dari notifikasi.