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); 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 public function getIndexData(int $year, int $month): array
{ {
$user = auth()->user(); $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); $hrSettings = app(HRSettings::class);
$employeeId = $isAdmin ? null : $user->employee?->id; $employeeId = $isAdmin ? null : $user->employee?->id;
@ -72,6 +72,18 @@ public function getByDate(string $date): ?array
return $attendance ? $this->formatAttendance($attendance) : null; 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 public function getToday(): ?array
{ {
return $this->getByDate(now()->toDateString()); return $this->getByDate(now()->toDateString());
@ -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 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) $presentCount = Attendance::whereYear('attendance_date', $year)
->whereMonth('attendance_date', $month) ->whereMonth('attendance_date', $month)
->where('attendance_date', '<=', $statEnd->toDateString()) ->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(); ->count();
$leaveRequests = LeaveRequest::approved() $leaveRequests = LeaveRequest::approved()
->where('start_date', '<=', $statEnd) ->where('start_date', '<=', $statEnd)
->where('end_date', '>=', $startOfMonth) ->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(); ->get();
$leaveDays = 0; $leaveDays = 0;

View File

@ -10,7 +10,7 @@ import {
DialogTitle, DialogTitle,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { import {
index as attendanceIndex, byMonth as attendanceByMonth,
store, store,
update, update,
} from '@/routes/admin/hr/attendances'; } from '@/routes/admin/hr/attendances';
@ -24,7 +24,7 @@ import {
LogIn, LogIn,
LogOut, LogOut,
} from 'lucide-react'; } from 'lucide-react';
import { useMemo, useState } from 'react'; import { useMemo, useState, useCallback } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
type Attendance = { type Attendance = {
@ -147,9 +147,9 @@ function formatMinutes(minutes: number | null): string {
} }
export default function AttendanceIndex({ export default function AttendanceIndex({
attendances, attendances: initialAttendances,
leaves, leaves: initialLeaves,
employees = [], employees: initialEmployees = [],
todayAttendance, todayAttendance,
currentYear, currentYear,
currentMonth, currentMonth,
@ -174,10 +174,30 @@ export default function AttendanceIndex({
const [detailAttendance, setDetailAttendance] = useState<Attendance | null>( const [detailAttendance, setDetailAttendance] = useState<Attendance | null>(
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 viewYear = viewDate.getFullYear();
const viewMonth = viewDate.getMonth() + 1; 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 attendanceByDate = useMemo(() => {
const map = new Map<string, Attendance[]>(); const map = new Map<string, Attendance[]>();
attendances.forEach((att) => { attendances.forEach((att) => {
@ -254,47 +274,20 @@ export default function AttendanceIndex({
const handlePrevMonth = () => { const handlePrevMonth = () => {
const newDate = subMonths(viewDate, 1); const newDate = subMonths(viewDate, 1);
setViewDate(newDate); setViewDate(newDate);
fetchMonthData(newDate.getFullYear(), newDate.getMonth() + 1);
if (!isAdmin) {
router.get(
attendanceIndex.url(),
{
year: newDate.getFullYear(),
month: newDate.getMonth() + 1,
},
{ preserveState: true, preserveScroll: true },
);
}
}; };
const handleNextMonth = () => { const handleNextMonth = () => {
const newDate = addMonths(viewDate, 1); const newDate = addMonths(viewDate, 1);
setViewDate(newDate); setViewDate(newDate);
fetchMonthData(newDate.getFullYear(), newDate.getMonth() + 1);
if (!isAdmin) {
router.get(
attendanceIndex.url(),
{
year: newDate.getFullYear(),
month: newDate.getMonth() + 1,
},
{ preserveState: true, preserveScroll: true },
);
}
}; };
const handleGoToToday = () => { const handleGoToToday = () => {
const now = new Date(); const now = new Date();
setViewDate(now); setViewDate(now);
setSelectedDate(now); setSelectedDate(now);
fetchMonthData(now.getFullYear(), now.getMonth() + 1);
if (!isAdmin) {
router.get(
attendanceIndex.url(),
{ year: now.getFullYear(), month: now.getMonth() + 1 },
{ preserveState: true, preserveScroll: true },
);
}
}; };
const handleCameraCapture = (dataUrl: string) => { const handleCameraCapture = (dataUrl: string) => {
@ -374,17 +367,10 @@ export default function AttendanceIndex({
Menampilkan presensi dari notifikasi. Menampilkan presensi dari notifikasi.
<button <button
onClick={() => { onClick={() => {
router.get( const now = new Date();
attendanceIndex.url(), setViewDate(now);
{ setSelectedDate(now);
year: currentYear, fetchMonthData(currentYear, currentMonth);
month: currentMonth,
},
{
replace: true,
preserveState: true,
},
);
}} }}
className="ml-1 text-primary underline underline-offset-4 hover:text-primary/80" 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(); const todayMidnight = new Date();
todayMidnight.setHours(0, 0, 0, 0); todayMidnight.setHours(0, 0, 0, 0);
const [cYear, cMonth, cDay] = String(cell.date).split('-').map(Number); const cellDate = new Date(cell.date);
const cellDate = new Date(cYear, cMonth - 1, cDay);
cellDate.setHours(0, 0, 0, 0); cellDate.setHours(0, 0, 0, 0);
const isPastDate = cellDate < todayMidnight; const isPastDate = cellDate < todayMidnight;
@ -543,7 +528,7 @@ export default function AttendanceIndex({
<div className="mt-1 flex flex-col gap-0.5 overflow-hidden"> <div className="mt-1 flex flex-col gap-0.5 overflow-hidden">
{isAdmin ? ( {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 att = dayAttendances.find((a) => a.employee_id === emp.id);
const leave = dayLeaves.find((l) => l.employee_id === emp.id); const leave = dayLeaves.find((l) => l.employee_id === emp.id);