feat: add byMonth method to AttendanceController and corresponding service logic
This commit is contained in:
parent
86cc8637bd
commit
4758b19d2a
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
@ -57,9 +57,9 @@ public function getByMonth(int $year, int $month, ?int $employeeId = null): Coll
|
|||||||
return Attendance::with(['employee.user.userProfile', 'media'])
|
return Attendance::with(['employee.user.userProfile', 'media'])
|
||||||
->whereYear('attendance_date', $year)
|
->whereYear('attendance_date', $year)
|
||||||
->whereMonth('attendance_date', $month)
|
->whereMonth('attendance_date', $month)
|
||||||
->when($employeeId, fn ($q) => $q->where('employee_id', $employeeId))
|
->when($employeeId, fn($q) => $q->where('employee_id', $employeeId))
|
||||||
->get()
|
->get()
|
||||||
->map(fn (Attendance $attendance) => $this->formatAttendance($attendance));
|
->map(fn(Attendance $attendance) => $this->formatAttendance($attendance));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getByDate(string $date): ?array
|
public function getByDate(string $date): ?array
|
||||||
@ -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());
|
||||||
@ -86,9 +98,9 @@ public function getLeavesByMonth(int $year, int $month, ?int $employeeId = null)
|
|||||||
->with('employee.user.userProfile')
|
->with('employee.user.userProfile')
|
||||||
->where('start_date', '<=', $endOfMonth)
|
->where('start_date', '<=', $endOfMonth)
|
||||||
->where('end_date', '>=', $startOfMonth)
|
->where('end_date', '>=', $startOfMonth)
|
||||||
->when($employeeId, fn ($q) => $q->where('employee_id', $employeeId))
|
->when($employeeId, fn($q) => $q->where('employee_id', $employeeId))
|
||||||
->get()
|
->get()
|
||||||
->map(fn (LeaveRequest $leave) => [
|
->map(fn(LeaveRequest $leave) => [
|
||||||
'id' => $leave->id,
|
'id' => $leave->id,
|
||||||
'employee_id' => $leave->employee_id,
|
'employee_id' => $leave->employee_id,
|
||||||
'start_date' => $leave->start_date->toDateString(),
|
'start_date' => $leave->start_date->toDateString(),
|
||||||
@ -174,7 +186,7 @@ public function checkIn(array $data): Attendance
|
|||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR],
|
||||||
title: 'Presensi Masuk',
|
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]),
|
url: route('admin.hr.attendances.index', ['highlight' => $attendance->id]),
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -196,7 +208,7 @@ public function checkOut(Attendance $attendance, array $data): Attendance
|
|||||||
NotificationService::notify(
|
NotificationService::notify(
|
||||||
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR],
|
roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR],
|
||||||
title: 'Presensi Pulang',
|
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]),
|
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);
|
->where('employee_id', $employeeId);
|
||||||
|
|
||||||
$attendanceDates = $attendanceQuery->pluck('attendance_date')
|
$attendanceDates = $attendanceQuery->pluck('attendance_date')
|
||||||
->map(fn ($d) => Carbon::parse($d)->toDateString())
|
->map(fn($d) => Carbon::parse($d)->toDateString())
|
||||||
->filter(fn ($d) => ! in_array(Carbon::parse($d)->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY]))
|
->filter(fn($d) => ! in_array(Carbon::parse($d)->dayOfWeek, [Carbon::SATURDAY, Carbon::SUNDAY]))
|
||||||
->unique()
|
->unique()
|
||||||
->values();
|
->values();
|
||||||
$attendanceCount = $attendanceDates->count();
|
$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
|
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;
|
||||||
@ -307,11 +332,11 @@ private function formatAttendance(Attendance $attendance): array
|
|||||||
$checkOutMedia = $attendance->getFirstMedia('checkout');
|
$checkOutMedia = $attendance->getFirstMedia('checkout');
|
||||||
|
|
||||||
$toArray['check_in_photo'] = $checkInMedia
|
$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;
|
: null;
|
||||||
|
|
||||||
$toArray['check_out_photo'] = $checkOutMedia
|
$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;
|
: null;
|
||||||
|
|
||||||
return $toArray;
|
return $toArray;
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user