import { StatCard } from '@/components/card/stat-card'; import { TodayAttendanceAlert } from '@/components/card/today-attendance-alert'; import { CameraCapture } from '@/components/inputs'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, type ChartConfig } from '@/components/ui/chart'; 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'; import { Head, router, usePage } from '@inertiajs/react'; import { Clock, Moon, Sun, Sunrise, TrendingDown, TrendingUp, UserCheck, } from 'lucide-react'; import { useEffect, useMemo, useState } from 'react'; import { Pie, PieChart } from 'recharts'; import { toast } from 'sonner'; type TodayAttendance = { 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; } | null; type DashboardProps = { attendance: { total_employees: number; percentage: number; present: number; absent: number; on_leave: number; }; revenueSummary: { total_revenue: number; total_discount: number; total_cogs: number; total_deduction: number; total_orders: number; }; expenseSummary: { total: number; expense_total: number; advance_total: number; }; orderStats: { by_channel: Array<{ channel: string; label: string; count: number; total: number; }>; by_payment_type: Array<{ payment_type: string; label: string; count: number; total: number; }>; by_marketing: Array<{ name: string; count: number; total: number; }>; by_status: Array<{ status: string; label: string; count: number; }>; }; todayAttendance: TodayAttendance; isOnLeave: boolean; canCheckIn: boolean; }; export default function Dashboard({ attendance: attendanceStats, revenueSummary, expenseSummary, orderStats, todayAttendance, isOnLeave, canCheckIn, }: DashboardProps) { const { can, hasRole } = 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); return () => clearInterval(timer); }, []); const greeting = useMemo(() => { const hour = currentTime.getHours(); if (hour >= 4 && hour < 11) { return { text: 'Selamat Pagi', icon: Sunrise }; } if (hour >= 11 && hour < 15) { return { text: 'Selamat Siang', icon: Sun }; } if (hour >= 15 && hour < 18) { return { text: 'Selamat Sore', icon: Sun }; } return { text: 'Selamat Malam', icon: Moon }; }, [currentTime]); const timeStr = currentTime.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false, }); const dateStr = currentTime.toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', }); const visibleStatsCount = useMemo(() => { let count = 0; if (can('dashboard.attendance')) { count++; } if (can('dashboard.revenue')) { count++; } if (can('dashboard.expense')) { count++; } return count; }, [can]); const statsGridClass = useMemo(() => { if (visibleStatsCount === 1) { return 'grid gap-4 grid-cols-1 md:max-w-md'; } if (visibleStatsCount === 2) { return 'grid gap-4 grid-cols-1 md:grid-cols-2'; } if (visibleStatsCount === 3) { return 'grid gap-4 grid-cols-1 md:grid-cols-3'; } return 'grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-4'; }, [visibleStatsCount]); const visibleChartsCount = useMemo(() => { let count = 0; if (can('dashboard.orders_channel')) { count++; } if (can('dashboard.orders_payment')) { count++; } if (can('dashboard.orders_marketing')) { count++; } if (can('dashboard.orders_status')) { count++; } return count; }, [can]); const chartsGridClass = useMemo(() => { if (visibleChartsCount === 1) { return 'grid gap-4 grid-cols-1 md:max-w-xl'; } if (visibleChartsCount === 2) { return 'grid gap-4 grid-cols-1 md:grid-cols-2'; } if (visibleChartsCount === 3) { return 'grid gap-4 grid-cols-1 md:grid-cols-3'; } return 'grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-4'; }, [visibleChartsCount]); 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 ( <>
{greeting.text}, {auth.user?.username}! Selamat datang di dasbor aplikasi.
{dateStr} - {timeStr}
{can('attendances.create') && !hasRole('developer') && !hasRole('owner') && ( <> setShowCamera(false)} /> )} {visibleStatsCount > 0 && (
{can('dashboard.attendance') && ( )} {can('dashboard.revenue') && ( )} {can('dashboard.expense') && ( )}
)} {visibleChartsCount > 0 && (
{can('dashboard.orders_channel') && ( ({ name: item.label, count: item.count, }))} dataKey="count" nameKey="name" /> )} {can('dashboard.orders_payment') && ( ({ name: item.label, count: item.count, }))} dataKey="count" nameKey="name" /> )} {can('dashboard.orders_marketing') && ( ({ name: item.name, count: item.count, }))} dataKey="count" nameKey="name" /> )} {can('dashboard.orders_status') && ( ({ name: item.label, count: item.count, }))} dataKey="count" nameKey="name" /> )}
)}
); } type PieChartItem = { name: string; count: number; }; type DashboardPieChartProps = { title: string; data: PieChartItem[]; dataKey: string; nameKey: string; }; const PIE_COLORS = [ 'var(--chart-1)', 'var(--chart-2)', 'var(--chart-3)', 'var(--chart-4)', 'var(--chart-5)', ]; function DashboardPieChart({ title, data, dataKey, nameKey }: DashboardPieChartProps) { const hasData = data.length > 0 && data.some((d) => d.count > 0); const chartConfig = useMemo(() => { const config: ChartConfig = {}; data.forEach((item, index) => { config[item.name] = { label: item.name, color: PIE_COLORS[index % PIE_COLORS.length], }; }); return config; }, [data]); const chartData = useMemo(() => { return data.map((item) => ({ ...item, fill: PIE_COLORS[data.indexOf(item) % PIE_COLORS.length], })); }, [data]); return ( {title} {hasData ? ( } /> } className="-translate-y-2 flex-wrap gap-2 *:basis-1/4 *:justify-center" /> ) : (
Belum ada data
)}
); }