import { useState, useEffect, useRef } from "react"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { format, parseISO, isBefore, isAfter } from "date-fns"; import { Calendar as CalendarIcon, ChevronDown, ChevronUp, Edit, Loader2, MoreHorizontal, Plus, Trash2, Gift, FilterX, Search, CalendarDays, CalendarPlus, X } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter, } from "@/components/ui/card"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Textarea } from "@/components/ui/textarea"; import { toast } from "sonner"; import { holidayApi } from "@/services/holidayApi"; import { Holiday, HolidayCreateRequest, HolidayUpdateRequest } from "@/types"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; import Layout from "@/components/layout/layout"; import CustomDialog from "@/components/CustomDialog"; import { Badge } from "@/components/ui/badge"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ScrollArea } from "@/components/ui/scroll-area"; import { useMediaQuery } from "@/hooks/use-media-query"; import { useNavigate } from "react-router-dom"; import { useAuth } from "@/lib/auth-context"; // Form validation schema const holidaySchema = z.object({ id: z.number().optional(), name: z.string().min(2, { message: "Name must be at least 2 characters" }), date: z.date(), description: z.string().min(3, { message: "Description must be at least 3 characters" }), isRecurring: z.boolean().default(false), appliesTo: z.string() }); type HolidayFormValues = z.infer; // Helper function to get seasonal color based on date const getSeasonalColor = (date: Date): string => { const month = date.getMonth(); // Winter (December, January, February) if (month === 11 || month === 0 || month === 1) return "bg-blue-50 border-blue-200 text-blue-700 dark:bg-blue-950 dark:border-blue-800 dark:text-blue-300"; // Spring (March, April, May) if (month >= 2 && month <= 4) return "bg-green-50 border-green-200 text-green-700 dark:bg-green-950 dark:border-green-800 dark:text-green-300"; // Summer (June, July, August) if (month >= 5 && month <= 7) return "bg-amber-50 border-amber-200 text-amber-700 dark:bg-amber-950 dark:border-amber-800 dark:text-amber-300"; // Fall (September, October, November) return "bg-orange-50 border-orange-200 text-orange-700 dark:bg-orange-950 dark:border-orange-800 dark:text-orange-300"; }; // Add the custom filter options array const filterOptions = [ { value: "all", label: "All Holidays" }, { value: "upcoming", label: "Upcoming" }, { value: "past", label: "Past" }, { value: "recurring", label: "Recurring" } ]; // Custom Year Select Component function CustomYearSelect({ selectedYear, availableYears, handleYearChange }) { const [open, setOpen] = useState(false); const dropdownRef = useRef(null); useEffect(() => { if (!open) return; const handleOutsideClick = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setOpen(false); } }; document.addEventListener('mousedown', handleOutsideClick); return () => document.removeEventListener('mousedown', handleOutsideClick); }, [open]); const handleButtonClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setOpen(!open); }; const displayYear = selectedYear === "all" ? "All Years" : selectedYear.toString(); const currentYear = new Date().getFullYear(); return (
{open && (
{availableYears.map((year) => ( ))}
)}
); } // Add the custom filter component before the Holidays component function CustomFilterDropdown({ filterType, handleFilterChange, resetFilters, hasFilters }) { const [open, setOpen] = useState(false); const dropdownRef = useRef(null); // Handle outside clicks to close the dropdown useEffect(() => { if (!open) return; const handleOutsideClick = (e: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setOpen(false); } }; document.addEventListener('mousedown', handleOutsideClick); return () => document.removeEventListener('mousedown', handleOutsideClick); }, [open]); const handleButtonClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); setOpen(!open); }; const selectedOption = filterOptions.find(option => option.value === filterType) || filterOptions[0]; return (
{open && (
{filterOptions.map((option) => ( ))}
)}
); } const Holidays = () => { const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [selectedHoliday, setSelectedHoliday] = useState(null); const [searchQuery, setSearchQuery] = useState(""); const [filterType, setFilterType] = useState<"all" | "upcoming" | "past" | "recurring">("upcoming"); const [selectedYear, setSelectedYear] = useState(new Date().getFullYear()); const [activeTab, setActiveTab] = useState("cards"); const queryClient = useQueryClient(); const isMobile = useMediaQuery("(max-width: 768px)"); const navigate = useNavigate(); const { userData } = useAuth(); // Check if user is admin (has permissions to add/edit/delete holidays) // In a real app, this would be based on user role or permissions const isAdmin = userData?.roleId === 1 || userData?.roleId === 2; // Assuming roleId 1 and 2 are admin roles // Fetch holidays const { data: holidays = [], isLoading, error, } = useQuery({ queryKey: ["holidays"], queryFn: () => holidayApi.getAll(), }); // Get unique years from holidays const availableYears = Array.from( new Set(holidays.map(h => new Date(h.date).getFullYear())) ).sort((a, b) => b - a); // Filter and search holidays const filteredHolidays = holidays.filter(holiday => { // Apply search filter const matchesSearch = searchQuery === "" || holiday.name.toLowerCase().includes(searchQuery.toLowerCase()) || holiday.description.toLowerCase().includes(searchQuery.toLowerCase()); // Apply type filter let matchesType = true; const today = new Date(); if (filterType === "upcoming") { matchesType = isAfter(parseISO(holiday.date), today); } else if (filterType === "past") { matchesType = isBefore(parseISO(holiday.date), today); } else if (filterType === "recurring") { matchesType = holiday.isRecurring; } // Apply year filter const matchesYear = selectedYear === "all" || new Date(holiday.date).getFullYear() === selectedYear; return matchesSearch && matchesType && matchesYear; }); // Create form const createForm = useForm({ resolver: zodResolver(holidaySchema), defaultValues: { name: "", date: new Date(), description: "", isRecurring: false, appliesTo: "All" }, }); // Edit form const editForm = useForm({ resolver: zodResolver(holidaySchema), defaultValues: { id: 0, name: "", date: new Date(), description: "", isRecurring: false, appliesTo: "All" }, }); // Reset create form when dialog opens/closes useEffect(() => { if (!isCreateDialogOpen) { setTimeout(() => { createForm.reset({ name: "", date: new Date(), description: "", isRecurring: false, appliesTo: "All" }); }, 100); } }, [isCreateDialogOpen, createForm]); // Set edit form values when a holiday is selected for editing useEffect(() => { if (selectedHoliday && isEditDialogOpen) { editForm.reset({ id: selectedHoliday.id, name: selectedHoliday.name, date: parseISO(selectedHoliday.date), description: selectedHoliday.description, isRecurring: selectedHoliday.isRecurring, appliesTo: selectedHoliday.appliesTo }); } }, [selectedHoliday, isEditDialogOpen, editForm]); // Create mutation const createMutation = useMutation({ mutationFn: (data: HolidayCreateRequest) => holidayApi.create(data), onSuccess: () => { toast.success("Holiday created successfully"); setIsCreateDialogOpen(false); queryClient.invalidateQueries({ queryKey: ["holidays"] }); }, onError: (error) => { toast.error("Failed to create holiday"); console.error("Error creating holiday:", error); }, }); // Update mutation const updateMutation = useMutation({ mutationFn: (data: HolidayUpdateRequest) => holidayApi.update(data.id, data), onSuccess: () => { toast.success("Holiday updated successfully"); setIsEditDialogOpen(false); queryClient.invalidateQueries({ queryKey: ["holidays"] }); }, onError: (error) => { toast.error("Failed to update holiday"); console.error("Error updating holiday:", error); }, }); // Delete mutation const deleteMutation = useMutation({ mutationFn: (id: number) => holidayApi.delete(id), onSuccess: () => { toast.success("Holiday deleted successfully"); setIsDeleteDialogOpen(false); queryClient.invalidateQueries({ queryKey: ["holidays"] }); }, onError: (error) => { toast.error("Failed to delete holiday"); console.error("Error deleting holiday:", error); }, }); // Handle create form submission const onCreateSubmit = (values: HolidayFormValues) => { createMutation.mutate({ name: values.name, date: values.date.toISOString(), description: values.description, isRecurring: values.isRecurring, appliesTo: values.appliesTo }); }; // Handle edit form submission const onEditSubmit = (values: HolidayFormValues) => { if (!values.id) return; updateMutation.mutate({ id: values.id, name: values.name, date: values.date.toISOString(), description: values.description, isRecurring: values.isRecurring, appliesTo: values.appliesTo }); }; // Handle delete confirmation const onDeleteConfirm = () => { if (selectedHoliday) { deleteMutation.mutate(selectedHoliday.id); } }; // Open edit dialog with selected holiday const handleEdit = (holiday: Holiday) => { setSelectedHoliday(holiday); setIsEditDialogOpen(true); }; // Open delete dialog with selected holiday const handleDelete = (holiday: Holiday) => { setSelectedHoliday(holiday); setIsDeleteDialogOpen(true); }; // Handler for filter change const handleFilterChange = (type: "all" | "upcoming" | "past" | "recurring") => { setFilterType(type); }; // Handler for year change const handleYearChange = (year: number | "all") => { setSelectedYear(year); }; // Reset filters const resetFilters = () => { setSearchQuery(""); setFilterType("all"); setSelectedYear("all"); }; if (error) { return (
Error Loading Holidays

An unexpected error occurred. Please try again later or contact support if the problem persists.

); } return (

Company Holidays

View and manage company-wide holiday schedule

Holiday Calendar
setSearchQuery(e.target.value)} className="pl-8 max-w-[200px] bg-white dark:bg-gray-950" />
{isLoading ? (
) : error ? (

Failed to load holiday data. Please try again.

) : filteredHolidays.length === 0 ? (

No holidays found

Try adjusting your filters or add new holidays if you have permission.

) : ( Cards Table
{isAdmin && ( )}
{filteredHolidays.map((holiday) => { const holidayDate = parseISO(holiday.date); const colorClass = getSeasonalColor(holidayDate); return (

{holiday.name}

{format(holidayDate, "EEEE, MMMM d, yyyy")}

{holiday.isRecurring && ( Recurring )}

{holiday.description}

{holiday.appliesTo} {isAdmin && (
)}
); })}
Name Date Description Recurring Applies To {isAdmin && Actions} {filteredHolidays.map((holiday) => ( {holiday.name} {format(parseISO(holiday.date), "MMM d, yyyy")} {holiday.description} {holiday.isRecurring ? "Yes" : "No"} {holiday.appliesTo} {isAdmin && (
)}
))}
)}
Holiday Stats {selectedYear !== "all" && `(${selectedYear})`}
Total Holidays {filteredHolidays.length}
Upcoming Holidays {filteredHolidays.filter(h => isAfter(parseISO(h.date), new Date())).length}
Recurring Holidays {filteredHolidays.filter(h => h.isRecurring).length}
{/* Create Dialog */} !createMutation.isPending && setIsCreateDialogOpen(false)} title="Add New Holiday" description="Create a new holiday or important date for the organization" allowClose={!createMutation.isPending} isPending={createMutation.isPending} >
( Holiday Name )} /> ( Date { if (e.target.value) { field.onChange(new Date(e.target.value)); } }} /> )} /> ( Description