feat: add byMonth method to AttendanceController and corresponding service logic

This commit is contained in:
Yoga Pangestu 2026-08-15 13:46:30 +07:00
parent 86cc8637bd
commit 4758b19d2a
3 changed files with 81 additions and 63 deletions

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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<Attendance | null>(
null,
);
const [attendances, setAttendances] = useState<Attendance[]>(initialAttendances);
const [leaves, setLeaves] = useState<Leave[]>(initialLeaves);
const [employees, setEmployees] = useState<Employee[]>(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<string, Attendance[]>();
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.
<button
onClick={() => {
router.get(
attendanceIndex.url(),
{
year: currentYear,
month: currentMonth,
},
{
replace: true,
preserveState: true,
},
);
const now = new Date();
setViewDate(now);
setSelectedDate(now);
fetchMonthData(currentYear, currentMonth);
}}
className="ml-1 text-primary underline underline-offset-4 hover:text-primary/80"
>
@ -495,8 +481,7 @@ export default function AttendanceIndex({
const todayMidnight = new Date();
todayMidnight.setHours(0, 0, 0, 0);
const [cYear, cMonth, cDay] = String(cell.date).split('-').map(Number);
const cellDate = new Date(cYear, cMonth - 1, cDay);
const cellDate = new Date(cell.date);
cellDate.setHours(0, 0, 0, 0);
const isPastDate = cellDate < todayMidnight;
@ -543,7 +528,7 @@ export default function AttendanceIndex({
<div className="mt-1 flex flex-col gap-0.5 overflow-hidden">
{isAdmin ? (
<>
{[...employees].sort((a, b) => a.name.localeCompare(b.name)).map((emp) => {
{cell.isCurrentMonth && !isFutureDate && [...employees].sort((a, b) => a.name.localeCompare(b.name)).map((emp) => {
const att = dayAttendances.find((a) => a.employee_id === emp.id);
const leave = dayLeaves.find((l) => l.employee_id === emp.id);