From b12659cd68aee832156803981efb97b45ae694b0 Mon Sep 17 00:00:00 2001 From: Akshay Date: Fri, 12 Dec 2025 17:03:08 +0530 Subject: [PATCH] feat: Introduce staff leave management with application and history pages, update staff navigation, and add an attendance backfill script. --- app/staff/history/page.tsx | 227 ++++++++++++++++++++++++++ app/staff/layout.tsx | 5 + app/staff/leaves/page.tsx | 147 +++++++++++++++++ components/staff/ApplyLeaveDialog.tsx | 157 ++++++++++++++++++ components/staff/DashboardStats.tsx | 106 +++++++++++- components/staff/QuickActions.tsx | 55 ++++--- components/staff/WeeklyOverview.tsx | 122 +++++++++++--- 7 files changed, 773 insertions(+), 46 deletions(-) create mode 100644 app/staff/history/page.tsx create mode 100644 app/staff/leaves/page.tsx create mode 100644 components/staff/ApplyLeaveDialog.tsx diff --git a/app/staff/history/page.tsx b/app/staff/history/page.tsx new file mode 100644 index 00000000..99bbee6f --- /dev/null +++ b/app/staff/history/page.tsx @@ -0,0 +1,227 @@ +'use client' + +import { useEffect, useState } from "react" +import { createClient } from "@/lib/supabase/client" +import { useAuth } from "@/lib/hooks/useAuth" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { + Calendar, + Clock, + CheckCircle2, + XCircle, + Filter, + ArrowDownUp, + Download, + History as HistoryIcon +} from "lucide-react" +import { Button } from "@/components/ui/button" + +type AttendanceRecord = { + id: string + user_id: string + check_in: string + check_out: string | null + total_hours: number | null + status?: string // 'Complete' | 'Pending' + created_at: string +} + +export default function StaffHistoryPage() { + const { user, loading: authLoading } = useAuth() + const [history, setHistory] = useState([]) + const [loading, setLoading] = useState(true) + const [stats, setStats] = useState({ + totalHours: 0, + daysPresent: 0, + averageHours: 0 + }) + + const supabase = createClient() + + useEffect(() => { + if (!user) return + + const fetchData = async () => { + try { + const { data, error } = await supabase + .from("attendance_logs") + .select("*") + .eq("user_id", user.id) + .order("check_in", { ascending: false }) + + if (error) throw error + + const records = data || [] + setHistory(records) + + // Calculate stats + const totalHours = records.reduce((acc, curr) => acc + (curr.total_hours || 0), 0) + const daysPresent = records.length + const averageHours = daysPresent > 0 ? totalHours / daysPresent : 0 + + setStats({ + totalHours, + daysPresent, + averageHours + }) + + } catch (err) { + console.error("Error fetching history:", err) + } finally { + setLoading(false) + } + } + + fetchData() + }, [user, supabase]) + + if (authLoading || (loading && !history.length)) { + return ( +
+
+
+ ) + } + + return ( +
+ {/* Header */} +
+
+

+ Attendance History +

+

+ View and track your past attendance records. +

+
+ {/*
+ + +
*/} +
+ + {/* Stats Overview */} +
+ + +
+ +
+
+

Total Hours

+

{stats.totalHours.toFixed(2)}h

+
+
+
+ + + +
+ +
+
+

Days Present

+

{stats.daysPresent}

+
+
+
+ + + +
+ +
+
+

Avg. Daily Hours

+

{stats.averageHours.toFixed(2)}h

+
+
+
+
+ + {/* History Table */} + + + + + Detailed Logs + + +
+ + + + + + + + + + + + {history.length === 0 ? ( + + + + ) : ( + history.map((record) => { + const date = new Date(record.check_in).toLocaleDateString(undefined, { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric' + }) + const checkInTime = new Date(record.check_in).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + const checkOutTime = record.check_out + ? new Date(record.check_out).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + : '-' + + const isComplete = !!record.check_out + + return ( + + + + + + + + ) + }) + )} + +
DateCheck InCheck OutDurationStatus
+ No attendance records found. +
{date}{checkInTime}{checkOutTime} + = 8 + ? "bg-green-500/10 text-green-400 border border-green-500/20" + : "bg-blue-500/10 text-blue-400 border border-blue-500/20" + }`}> + {record.total_hours ? `${record.total_hours.toFixed(2)}h` : '-'} + + +
+ {isComplete ? ( + <> + + Completed + + ) : ( + <> + + Active + + )} +
+
+
+
+
+ ) +} diff --git a/app/staff/layout.tsx b/app/staff/layout.tsx index c9d500bb..06cb5f3d 100644 --- a/app/staff/layout.tsx +++ b/app/staff/layout.tsx @@ -47,6 +47,11 @@ const sidebarItems: SidebarGroupType[] = [ url: "/staff/tasks", icon: ClipboardList, }, + { + title: "My Leaves", + url: "/staff/leaves", + icon: CalendarDays, + }, ], }, { diff --git a/app/staff/leaves/page.tsx b/app/staff/leaves/page.tsx new file mode 100644 index 00000000..c528b255 --- /dev/null +++ b/app/staff/leaves/page.tsx @@ -0,0 +1,147 @@ +"use client" + +import { useEffect, useState } from "react" +import { createClient } from "@/lib/supabase/client" +import { useAuth } from "@/lib/hooks/useAuth" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { + Calendar, + CalendarClock, + CheckCircle2, + XCircle, + Clock +} from "lucide-react" +import { Badge } from "@/components/ui/badge" +import { format, differenceInBusinessDays, parseISO } from "date-fns" + +type LeaveRequest = { + id: string + leave_type: string + start_date: string + end_date: string + reason: string + status: 'pending' | 'approved' | 'rejected' + created_at: string +} + +export default function MyLeavesPage() { + const { user, loading: authLoading } = useAuth() + const [leaves, setLeaves] = useState([]) + const [loading, setLoading] = useState(true) + const supabase = createClient() + + useEffect(() => { + if (!user) return + + const fetchLeaves = async () => { + try { + const { data, error } = await supabase + .from("leave_requests") + .select("*") + .eq("user_id", user.id) + .order("created_at", { ascending: false }) + + if (error) throw error + setLeaves(data || []) + } catch (err) { + console.error("Error fetching leave requests:", err) + } finally { + setLoading(false) + } + } + + fetchLeaves() + }, [user, supabase]) + + if (authLoading || loading) { + return ( +
+
+
+ ) + } + + return ( +
+
+

+ My Leave Requests +

+

+ Track the status of your leave applications. +

+
+ + + + + + Application History + + +
+ + + + + + + + + + + + {leaves.length === 0 ? ( + + + + ) : ( + leaves.map((leave) => { + const appliedDate = format(parseISO(leave.created_at), "MMM d, yyyy") + const startDate = format(parseISO(leave.start_date), "MMM d") + const endDate = format(parseISO(leave.end_date), "MMM d, yyyy") + const days = differenceInBusinessDays(parseISO(leave.end_date), parseISO(leave.start_date)) + 1 + + return ( + + + + + + + + ) + }) + )} + +
Applied OnLeave TypeDurationStatusReason
+ No leave requests found. start by applying for one! +
{appliedDate} + + {leave.leave_type} + + +
+ {days} Day{days > 1 ? 's' : ''} + {startDate} - {endDate} +
+
+
+ {leave.status === 'approved' && } + {leave.status === 'rejected' && } + {leave.status === 'pending' && } + {leave.status.charAt(0).toUpperCase() + leave.status.slice(1)} +
+
+ {leave.reason} +
+
+
+
+ ) +} diff --git a/components/staff/ApplyLeaveDialog.tsx b/components/staff/ApplyLeaveDialog.tsx new file mode 100644 index 00000000..f1860a26 --- /dev/null +++ b/components/staff/ApplyLeaveDialog.tsx @@ -0,0 +1,157 @@ +"use client" + +import { useState } from "react" +import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog" +import { Button } from "@/components/ui/button" +import { Label } from "@/components/ui/label" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" +import { createClient } from "@/lib/supabase/client" +import { useAuth } from "@/lib/hooks/useAuth" +import { toast } from "sonner" +import { Loader2 } from "lucide-react" + +interface ApplyLeaveDialogProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function ApplyLeaveDialog({ open, onOpenChange }: ApplyLeaveDialogProps) { + const { user } = useAuth() + const [loading, setLoading] = useState(false) + const [formData, setFormData] = useState({ + leaveType: "", + startDate: "", + endDate: "", + reason: "" + }) + + const supabase = createClient() + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!user) return + + if (!formData.leaveType || !formData.startDate || !formData.endDate || !formData.reason) { + toast.error("Please fill in all fields") + return + } + + setLoading(true) + try { + const { error } = await supabase + .from("leave_requests") + .insert({ + user_id: user.id, + leave_type: formData.leaveType, + start_date: formData.startDate, + end_date: formData.endDate, + reason: formData.reason + }) + + if (error) throw error + + toast.success("Leave application submitted successfully") + onOpenChange(false) + setFormData({ leaveType: "", startDate: "", endDate: "", reason: "" }) // Reset + } catch (error) { + console.error("Error applying for leave:", error) + toast.error("Failed to apply for leave") + } finally { + setLoading(false) + } + } + + return ( + + + + Apply for Leave + + Submit a new leave request for approval. + + +
+
+ + +
+ +
+
+ + setFormData(prev => ({ ...prev, startDate: e.target.value }))} + /> +
+
+ + setFormData(prev => ({ ...prev, endDate: e.target.value }))} + /> +
+
+ +
+ +