From d648d48632a9a3b3ce11b801703945a310d98b54 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Fri, 7 Aug 2026 13:25:41 +0700 Subject: [PATCH] feat: implement TodayAttendanceAlert component for improved attendance display and interaction --- .../card/today-attendance-alert.tsx | 136 ++++++++++++++++++ .../js/pages/admin/hr/attendance/index.tsx | 95 +----------- resources/js/pages/dashboard.tsx | 73 +++++++++- 3 files changed, 213 insertions(+), 91 deletions(-) create mode 100644 resources/js/components/card/today-attendance-alert.tsx diff --git a/resources/js/components/card/today-attendance-alert.tsx b/resources/js/components/card/today-attendance-alert.tsx new file mode 100644 index 0000000..b369bc9 --- /dev/null +++ b/resources/js/components/card/today-attendance-alert.tsx @@ -0,0 +1,136 @@ +import { CalendarCheck, CheckCircle2, Clock, LogIn, LogOut } from 'lucide-react'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Button } from '@/components/ui/button'; + +type TodayAttendance = { + id: number; + check_in_at: string | null; + check_out_at: string | null; +} | null; + +type TodayAttendanceAlertProps = { + todayAttendance: TodayAttendance; + canCheckIn?: boolean; + isOnLeave?: boolean; + locationLoading?: boolean; + onCheckIn?: () => void; + onCheckOut?: () => void; + showButtons?: boolean; +}; + +function formatTime(dateStr: string | null): string { + if (!dateStr) { + return '-'; + } + + return new Date(dateStr).toLocaleTimeString('id-ID', { + hour: '2-digit', + minute: '2-digit', + hour12: false, + }); +} + +export function TodayAttendanceAlert({ + todayAttendance, + canCheckIn = true, + isOnLeave = false, + locationLoading = false, + onCheckIn, + onCheckOut, + showButtons = true, +}: TodayAttendanceAlertProps) { + const hasCheckedIn = !!todayAttendance?.check_in_at; + const hasCheckedOut = !!todayAttendance?.check_out_at; + + return ( + + + Presensi Hari Ini + +
+
+ {hasCheckedIn ? ( +
+
+ + + Masuk: + + + {formatTime(todayAttendance?.check_in_at)} + +
+
+ {hasCheckedOut ? ( + <> + + + Pulang: + + + {formatTime(todayAttendance?.check_out_at)} + + + ) : ( + <> + + + Pulang: + + + Belum + + + )} +
+
+ ) : isOnLeave ? ( +

+ Anda sedang cuti hari ini. +

+ ) : !canCheckIn ? ( +

+ Anda tidak memiliki akses presensi. +

+ ) : ( +

+ Anda belum melakukan presensi hari ini +

+ )} +
+ + {showButtons && canCheckIn && !isOnLeave && ( +
+ {locationLoading && ( + + Mendapatkan lokasi... + + )} + + +
+ )} +
+
+
+ ); +} diff --git a/resources/js/pages/admin/hr/attendance/index.tsx b/resources/js/pages/admin/hr/attendance/index.tsx index 8dfce5a..a724169 100644 --- a/resources/js/pages/admin/hr/attendance/index.tsx +++ b/resources/js/pages/admin/hr/attendance/index.tsx @@ -2,7 +2,6 @@ import { Head, router } from '@inertiajs/react'; import { addMonths, format, subMonths } from 'date-fns'; import { id } from 'date-fns/locale'; import { - CalendarCheck, CalendarDays, CheckCircle2, ChevronLeft, @@ -17,7 +16,7 @@ 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 { TodayAttendanceAlert } from '@/components/card/today-attendance-alert'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; @@ -329,9 +328,6 @@ export default function AttendanceIndex({ return format(new Date(dateStr), 'HH:mm'); } - const hasCheckedIn = !!todayAttendance; - const hasCheckedOut = !!todayAttendance?.check_out_at; - const today = new Date(); return ( @@ -346,89 +342,12 @@ export default function AttendanceIndex({ {!isAdmin && ( - - - 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... - - )} - - -
-
-
-
+ )} {monthStats && ( diff --git a/resources/js/pages/dashboard.tsx b/resources/js/pages/dashboard.tsx index 32aabd3..cea7aaf 100644 --- a/resources/js/pages/dashboard.tsx +++ b/resources/js/pages/dashboard.tsx @@ -1,4 +1,4 @@ -import { Head, usePage } from '@inertiajs/react'; +import { Head, router, usePage } from '@inertiajs/react'; import { Clock, Moon, @@ -9,12 +9,19 @@ import { UserCheck, } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; +import { toast } from 'sonner'; import { Pie, PieChart, Cell, ResponsiveContainer, Tooltip } from 'recharts'; -import { AttendanceCard } from '@/components/card/attendance-card'; +import { CameraCapture } from '@/components/camera-capture'; +import { TodayAttendanceAlert } from '@/components/card/today-attendance-alert'; import { StatCard } from '@/components/card/stat-card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { + Dialog, + DialogContent, +} from '@/components/ui/dialog'; import { useCan } from '@/hooks/use-can'; import { formatRupiah } from '@/lib/rupiah'; +import { store, update } from '@/routes/admin/hr/attendances'; type TodayAttendance = { id: number; @@ -125,6 +132,9 @@ export default function Dashboard({ const { can } = useCan(); const { auth } = usePage().props as { auth: { user?: { username?: string } } }; const [currentTime, setCurrentTime] = useState(new Date()); + const [showCamera, setShowCamera] = useState(false); + const [actionType, setActionType] = useState<'check-in' | 'check-out'>('check-in'); + const [locationLoading, setLocationLoading] = useState(false); useEffect(() => { const timer = setInterval(() => setCurrentTime(new Date()), 1000); @@ -238,6 +248,51 @@ export default function Dashboard({ const GreetingIcon = greeting.icon; + const handleCheckIn = () => { + setActionType('check-in'); + setShowCamera(true); + }; + + const handleCheckOut = () => { + setActionType('check-out'); + setShowCamera(true); + }; + + 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 }, + ); + }; + return ( <> @@ -272,12 +327,24 @@ export default function Dashboard({ - + + + setShowCamera(false)} + /> + + + {visibleStatsCount > 0 && (
{can('dashboard.attendance') && (