import { Head, router } from '@inertiajs/react'; import { addMonths, format, subMonths } from 'date-fns'; import { id } from 'date-fns/locale'; import { CalendarCheck, CalendarDays, CheckCircle2, ChevronLeft, ChevronRight, Clock, LogIn, LogOut, UserX, Wallet, } from 'lucide-react'; import { useMemo, useState } from 'react'; import { toast } from 'sonner'; import { CameraCapture } from '@/components/camera-capture'; import { LocationMap } from '@/components/location-map'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { index as attendanceIndex, store, update, } from '@/routes/admin/hr/attendances'; type Attendance = { id: number; attendance_date: string; check_in_at: string | null; check_out_at: string | null; check_in_photo: string | null; check_out_photo: string | null; check_in_latitude: number; check_in_longitude: number; check_out_latitude: number | null; check_out_longitude: number | null; work_duration_minutes: number | null; }; type MonthStats = { working_days: number; present: number; absent: number; leave: number; }; type Props = { attendances: Attendance[]; todayAttendance: Attendance | null; currentYear: number; currentMonth: number; monthStats: MonthStats; hrSettings: { scheduled_check_in_time: string; scheduled_check_out_time: string; }; }; const WEEKDAYS = ['Min', 'Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab']; function getDaysInMonth(year: number, month: number): number { return new Date(year, month, 0).getDate(); } function getFirstDayOfMonth(year: number, month: number): number { return new Date(year, month - 1, 1).getDay(); } function isSameDay(d1: Date, d2: Date): boolean { return ( d1.getDate() === d2.getDate() && d1.getMonth() === d2.getMonth() && d1.getFullYear() === d2.getFullYear() ); } function checkIsToday(date: Date): boolean { const t = new Date(); return ( date.getDate() === t.getDate() && date.getMonth() === t.getMonth() && date.getFullYear() === t.getFullYear() ); } function isWeekend(date: Date): boolean { const day = date.getDay(); return day === 0 || day === 6; } function isLate( checkInAt: string | null, officeHour: number, officeMinute: number, ): boolean { if (!checkInAt) { return false; } const d = new Date(checkInAt); const h = d.getHours(); const m = d.getMinutes(); return h > officeHour || (h === officeHour && m > officeMinute); } function getLateMinutes( checkInAt: string | null, officeHour: number, officeMinute: number, ): number { if (!checkInAt || !isLate(checkInAt, officeHour, officeMinute)) { return 0; } const d = new Date(checkInAt); const officeStart = new Date(d); officeStart.setHours(officeHour, officeMinute, 0, 0); return Math.ceil((d.getTime() - officeStart.getTime()) / 60000); } function formatMinutes(minutes: number | null): string { if (!minutes) { return '-'; } const hours = Math.floor(minutes / 60); const mins = Math.floor(minutes % 60); if (mins === 0) { return `${hours} jam`; } return `${hours} jam ${mins} menit`; } export default function AttendanceIndex({ attendances, todayAttendance, currentYear, currentMonth, monthStats, hrSettings, }: Props) { const [officeHour, officeMinute] = hrSettings.scheduled_check_in_time .split(':') .map(Number); const [viewDate, setViewDate] = useState( new Date(currentYear, currentMonth - 1), ); const [selectedDate, setSelectedDate] = useState(new Date()); const [showCamera, setShowCamera] = useState(false); const [actionType, setActionType] = useState<'check-in' | 'check-out'>( 'check-in', ); const [locationLoading, setLocationLoading] = useState(false); const [detailAttendance, setDetailAttendance] = useState( null, ); const viewYear = viewDate.getFullYear(); const viewMonth = viewDate.getMonth() + 1; const attendanceDates = useMemo(() => { const dates = new Map(); attendances.forEach((att) => { dates.set(att.attendance_date, att); }); return dates; }, [attendances]); const selectedAttendance = useMemo(() => { const dateStr = format(selectedDate, 'yyyy-MM-dd'); return attendanceDates.get(dateStr) ?? null; }, [selectedDate, attendanceDates]); const calendarDays = useMemo(() => { const daysInMonth = getDaysInMonth(viewYear, viewMonth); const firstDay = getFirstDayOfMonth(viewYear, viewMonth); const prevMonthDays = getDaysInMonth( viewYear, viewMonth === 1 ? 12 : viewMonth - 1, ); const days: { day: number; isCurrentMonth: boolean; date: Date }[] = []; for (let i = firstDay - 1; i >= 0; i--) { const d = prevMonthDays - i; const m = viewMonth === 1 ? 12 : viewMonth - 1; const y = viewMonth === 1 ? viewYear - 1 : viewYear; days.push({ day: d, isCurrentMonth: false, date: new Date(y, m - 1, d), }); } for (let i = 1; i <= daysInMonth; i++) { days.push({ day: i, isCurrentMonth: true, date: new Date(viewYear, viewMonth - 1, i), }); } const remaining = 42 - days.length; for (let i = 1; i <= remaining; i++) { const m = viewMonth === 12 ? 1 : viewMonth + 1; const y = viewMonth === 12 ? viewYear + 1 : viewYear; days.push({ day: i, isCurrentMonth: false, date: new Date(y, m - 1, i), }); } return days; }, [viewYear, viewMonth]); const handlePrevMonth = () => setViewDate((d) => subMonths(d, 1)); const handleNextMonth = () => setViewDate((d) => addMonths(d, 1)); const handleCameraCapture = (dataUrl: string) => { setShowCamera(false); setLocationLoading(true); if (!navigator.geolocation) { setLocationLoading(false); toast.error('Geolocation tidak didukung di browser ini.'); return; } navigator.geolocation.getCurrentPosition( (position) => { setLocationLoading(false); const formData = new FormData(); formData.append('photo', dataUrl); formData.append( 'latitude', position.coords.latitude.toString(), ); formData.append( 'longitude', position.coords.longitude.toString(), ); if (actionType === 'check-in') { router.post(store(), formData, { preserveScroll: true }); } else if (actionType === 'check-out' && todayAttendance) { router.put(update(todayAttendance.id), formData, { preserveScroll: true, }); } }, () => { setLocationLoading(false); toast.error( 'Gagal mendapatkan lokasi. Pastikan izin lokasi diberikan.', ); }, { enableHighAccuracy: true, timeout: 10000 }, ); }; const handleCheckIn = () => { setActionType('check-in'); setShowCamera(true); }; const handleCheckOut = () => { setActionType('check-out'); setShowCamera(true); }; function formatTime(dateStr: string | null): string { if (!dateStr) { return '-'; } return format(new Date(dateStr), 'HH:mm'); } const hasCheckedIn = !!todayAttendance; const hasCheckedOut = !!todayAttendance?.check_out_at; const today = new Date(); return ( <>

Presensi

{/* Alert Status Presensi */} Presensi Hari Ini
{hasCheckedIn ? (
Masuk: {formatTime( todayAttendance?.check_in_at, )}
{hasCheckedOut ? ( <> Pulang: {formatTime( todayAttendance?.check_out_at, )} ) : ( <> Pulang: Belum )}
) : (

Anda belum melakukan presensi hari ini

)}
{locationLoading && ( Mendapatkan lokasi... )}
{/* Summary Stats */}

Hari Kerja

{monthStats.working_days}

Hadir

{monthStats.present}

Tidak Hadir

{monthStats.absent}

Cuti

{monthStats.leave}

{/* Calendar Header */}
{format(today, 'MMM', { locale: id, })}
{format(today, 'dd')}

{format(viewDate, 'MMMM yyyy', { locale: id, })}

{/* Weekday Headers */}
{WEEKDAYS.map((day) => (
{day}
))}
{/* Calendar Grid */}
{calendarDays.map((cell, idx) => { const dateStr = format(cell.date, 'yyyy-MM-dd'); const attendance = attendanceDates.get(dateStr); const isSelected = isSameDay( cell.date, selectedDate, ); const isTodayDate = isSameDay( cell.date, new Date(), ); const today = new Date(); today.setHours(0, 0, 0, 0); const cellDate = new Date(cell.date); cellDate.setHours(0, 0, 0, 0); const isPastDate = cellDate < today; const showAbsent = cell.isCurrentMonth && isPastDate && !isWeekend(cell.date) && !attendance; const late = attendance ? isLate( attendance.check_in_at, officeHour, officeMinute, ) : false; const lateMins = attendance ? getLateMinutes( attendance.check_in_at, officeHour, officeMinute, ) : 0; return ( ); })}
{showCamera && ( setShowCamera(false)} /> )} !open && setDetailAttendance(null)} > Detail Presensi -{' '} {detailAttendance && format( new Date(detailAttendance.attendance_date), 'dd MMMM yyyy', { locale: id }, )} {detailAttendance && (
Foto Masuk {detailAttendance.check_in_photo ? ( Foto Masuk ) : (
Tidak ada foto
)}
{formatTime( detailAttendance.check_in_at, )}
Foto Pulang {detailAttendance.check_out_photo ? ( Foto Pulang ) : (
Tidak ada foto
)}
{detailAttendance.check_out_at ? ( <> {formatTime( detailAttendance.check_out_at, )} ) : ( <> Belum pulang )}
Lokasi Presensi
)}
); }