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({
-
+
+
{visibleStatsCount > 0 && (
{can('dashboard.attendance') && (