pmtool / src /pages /holidays.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
44.2 kB
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<typeof holidaySchema>;
// 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<HTMLDivElement>(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 (
<div className="relative" ref={dropdownRef}>
<Button
variant="outline"
className="w-[120px] h-10 justify-between bg-white dark:bg-gray-950"
onClick={handleButtonClick}
>
<div className="flex items-center">
<CalendarDays className="h-4 w-4 mr-2 text-blue-500" />
<span>{displayYear}</span>
</div>
<ChevronDown className="h-4 w-4 opacity-50" />
</Button>
{open && (
<div className="absolute z-50 mt-1 w-[140px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md">
<Button
variant="ghost"
className={cn(
"w-full justify-start rounded-sm font-normal hover:bg-blue-50 dark:hover:bg-blue-900/20",
selectedYear === "all" && "bg-blue-50 dark:bg-blue-900/20 font-medium"
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleYearChange("all");
setOpen(false);
}}
>
All Years
</Button>
{availableYears.map((year) => (
<Button
key={year}
variant="ghost"
className={cn(
"w-full justify-start rounded-sm font-normal hover:bg-blue-50 dark:hover:bg-blue-900/20",
selectedYear === year && "bg-blue-50 dark:bg-blue-900/20 font-medium"
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleYearChange(year);
setOpen(false);
}}
>
<span>{year}</span>
{year === currentYear && (
<span className="ml-2 text-xs text-blue-600 dark:text-blue-400">(Current)</span>
)}
</Button>
))}
</div>
)}
</div>
);
}
// Add the custom filter component before the Holidays component
function CustomFilterDropdown({
filterType,
handleFilterChange,
resetFilters,
hasFilters
}) {
const [open, setOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(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 (
<div className="relative" ref={dropdownRef}>
<Button
variant="outline"
className="w-[130px] h-10 justify-between bg-white dark:bg-gray-950"
onClick={handleButtonClick}
>
<div className="flex items-center">
<CalendarIcon className="h-4 w-4 mr-2 text-amber-500" />
<span>{selectedOption.label}</span>
</div>
<div className="flex items-center">
{hasFilters && (
<X
className="h-4 w-4 mr-1 hover:text-red-500"
onClick={(e) => {
e.stopPropagation();
resetFilters();
setOpen(false);
}}
/>
)}
<ChevronDown className="h-4 w-4 opacity-50" />
</div>
</Button>
{open && (
<div className="absolute z-50 mt-1 w-[180px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md">
{filterOptions.map((option) => (
<Button
key={option.value}
variant="ghost"
className={cn(
"w-full justify-start rounded-sm font-normal hover:bg-amber-50 dark:hover:bg-amber-900/20",
filterType === option.value && "bg-amber-50 dark:bg-amber-900/20 font-medium"
)}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleFilterChange(option.value as any);
setOpen(false);
}}
>
{option.label}
</Button>
))}
</div>
)}
</div>
);
}
const Holidays = () => {
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const [isEditDialogOpen, setIsEditDialogOpen] = useState(false);
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
const [selectedHoliday, setSelectedHoliday] = useState<Holiday | null>(null);
const [searchQuery, setSearchQuery] = useState("");
const [filterType, setFilterType] = useState<"all" | "upcoming" | "past" | "recurring">("upcoming");
const [selectedYear, setSelectedYear] = useState<number | "all">(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<Holiday[]>({
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<HolidayFormValues>({
resolver: zodResolver(holidaySchema),
defaultValues: {
name: "",
date: new Date(),
description: "",
isRecurring: false,
appliesTo: "All"
},
});
// Edit form
const editForm = useForm<HolidayFormValues>({
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 (
<Layout>
<div className="flex h-[450px] w-full items-center justify-center p-4">
<Card className="w-full max-w-md border-red-200">
<CardHeader className="bg-red-50 dark:bg-red-900/20">
<div className="flex items-center gap-2">
<div className="rounded-full bg-red-100 p-2 dark:bg-red-900/30">
<FilterX className="h-5 w-5 text-red-600 dark:text-red-400" />
</div>
<CardTitle className="text-red-700 dark:text-red-400">Error Loading Holidays</CardTitle>
</div>
</CardHeader>
<CardContent className="pt-6">
<p className="text-muted-foreground">
An unexpected error occurred. Please try again later or contact support if the problem persists.
</p>
</CardContent>
<CardFooter>
<Button variant="outline" className="w-full" onClick={() => window.location.reload()}>
Reload Page
</Button>
</CardFooter>
</Card>
</div>
</Layout>
);
}
return (
<Layout>
<div className="container p-4 md:p-8">
<div className="mb-6 bg-white dark:bg-gray-800 rounded-xl shadow-sm p-4">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold flex items-center gap-2 text-amber-600 dark:text-amber-400">
<Gift className="h-7 w-7" />
<span>Company Holidays</span>
</h1>
<p className="text-gray-500 dark:text-gray-400 mt-1">
View and manage company-wide holiday schedule
</p>
</div>
<Button
className="flex items-center gap-2 bg-gradient-to-r from-amber-500 to-orange-600 hover:from-amber-600 hover:to-orange-700 text-white shadow-md"
onClick={() => navigate('/leaves')}
>
<CalendarPlus className="h-4 w-4" />
<span>Request Leave</span>
</Button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
<div className="lg:col-span-3">
<Card className="shadow-md border-none">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-lg font-semibold flex items-center gap-2 text-amber-600 dark:text-amber-400">
<CalendarIcon className="h-4 w-4" />
<span>Holiday Calendar</span>
</CardTitle>
<div className="flex items-center gap-2">
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search holidays..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-8 max-w-[200px] bg-white dark:bg-gray-950"
/>
</div>
<CustomYearSelect
selectedYear={selectedYear}
availableYears={availableYears}
handleYearChange={handleYearChange}
/>
<CustomFilterDropdown
filterType={filterType}
handleFilterChange={handleFilterChange}
resetFilters={resetFilters}
hasFilters={searchQuery || filterType !== "upcoming" || selectedYear !== "all"}
/>
</div>
</div>
</CardHeader>
<CardContent className="pb-4 pt-2">
{isLoading ? (
<div className="flex justify-center items-center py-32">
<Loader2 className="h-8 w-8 animate-spin text-amber-500" />
</div>
) : error ? (
<div className="text-center py-32 border rounded-lg bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800">
<p className="text-red-600 dark:text-red-400">Failed to load holiday data. Please try again.</p>
</div>
) : filteredHolidays.length === 0 ? (
<div className="text-center py-32 border rounded-lg bg-gray-50 dark:bg-gray-800/50 border-gray-200 dark:border-gray-700">
<Gift className="h-10 w-10 mx-auto text-gray-400 mb-3" />
<h3 className="font-medium text-lg text-gray-700 dark:text-gray-300">No holidays found</h3>
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto mt-1">
Try adjusting your filters or add new holidays if you have permission.
</p>
</div>
) : (
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="grid grid-cols-2 max-w-[200px] mb-6">
<TabsTrigger value="cards">Cards</TabsTrigger>
<TabsTrigger value="table">Table</TabsTrigger>
</TabsList>
<div className="flex justify-between items-center mb-4">
<Button
variant="outline"
size="sm"
onClick={() => navigate('/leaves')}
className="flex items-center gap-2 text-indigo-600 dark:text-indigo-400 border-indigo-200 dark:border-indigo-800 hover:bg-indigo-50 dark:hover:bg-indigo-900/30"
>
<CalendarPlus className="h-4 w-4" />
<span>Back to Leaves</span>
</Button>
<div className="flex items-center gap-2">
{isAdmin && (
<Button
onClick={() => setIsCreateDialogOpen(true)}
className="flex items-center gap-2 bg-gradient-to-r from-indigo-500 to-purple-600 hover:from-indigo-600 hover:to-purple-700 text-white shadow-md"
>
<Plus className="h-4 w-4" />
<span>Add Holiday</span>
</Button>
)}
</div>
</div>
<TabsContent value="cards" className="mt-0">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredHolidays.map((holiday) => {
const holidayDate = parseISO(holiday.date);
const colorClass = getSeasonalColor(holidayDate);
return (
<div
key={holiday.id}
className={`p-4 rounded-lg border ${colorClass} transition-all hover:shadow-md`}
>
<div className="flex justify-between items-start mb-3">
<div>
<h3 className="font-semibold text-lg">{holiday.name}</h3>
<p className="text-gray-500 dark:text-gray-400 text-sm">
{format(holidayDate, "EEEE, MMMM d, yyyy")}
</p>
</div>
{holiday.isRecurring && (
<Badge className="bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300 hover:bg-purple-200">
Recurring
</Badge>
)}
</div>
<div className="mt-3">
<p className="text-sm text-gray-600 dark:text-gray-300 line-clamp-2">
{holiday.description}
</p>
</div>
<div className="flex justify-between items-center mt-4">
<span className="text-sm text-gray-500 dark:text-gray-400">
{holiday.appliesTo}
</span>
{isAdmin && (
<div className="flex gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(holiday)}
className="h-8 w-8 p-0 text-blue-600 hover:text-blue-800 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-blue-900/30"
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(holiday)}
className="h-8 w-8 p-0 text-red-600 hover:text-red-800 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-900/30"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
)}
</div>
</div>
);
})}
</div>
</TabsContent>
<TabsContent value="table" className="mt-0">
<div className="rounded-md border overflow-hidden">
<Table>
<TableHeader className="bg-gray-50 dark:bg-gray-800">
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Date</TableHead>
<TableHead>Description</TableHead>
<TableHead>Recurring</TableHead>
<TableHead>Applies To</TableHead>
{isAdmin && <TableHead className="w-[100px] text-right">Actions</TableHead>}
</TableRow>
</TableHeader>
<TableBody>
{filteredHolidays.map((holiday) => (
<TableRow key={holiday.id}>
<TableCell className="font-medium">{holiday.name}</TableCell>
<TableCell>{format(parseISO(holiday.date), "MMM d, yyyy")}</TableCell>
<TableCell className="max-w-[300px] truncate">{holiday.description}</TableCell>
<TableCell>{holiday.isRecurring ? "Yes" : "No"}</TableCell>
<TableCell>{holiday.appliesTo}</TableCell>
{isAdmin && (
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(holiday)}
className="h-8 w-8 p-0 text-blue-600 hover:text-blue-800 hover:bg-blue-50"
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleDelete(holiday)}
className="h-8 w-8 p-0 text-red-600 hover:text-red-800 hover:bg-red-50"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
)}
</TableRow>
))}
</TableBody>
</Table>
</div>
</TabsContent>
</Tabs>
)}
</CardContent>
</Card>
</div>
<div className="space-y-6">
<Card className="shadow-md border-none bg-gradient-to-br from-white to-amber-50 dark:from-gray-800 dark:to-gray-900">
<CardHeader className="pb-2">
<CardTitle className="text-lg flex items-center gap-2 text-amber-600 dark:text-amber-400">
<CalendarIcon className="h-5 w-5" />
<span>Holiday Stats {selectedYear !== "all" && `(${selectedYear})`}</span>
</CardTitle>
</CardHeader>
<CardContent className="pt-2">
<div className="space-y-4">
<div className="flex justify-between items-center">
<span>Total Holidays</span>
<span className="font-bold text-amber-600 dark:text-amber-400">{filteredHolidays.length}</span>
</div>
<div className="flex justify-between items-center">
<span>Upcoming Holidays</span>
<span className="font-bold text-green-600 dark:text-green-400">
{filteredHolidays.filter(h => isAfter(parseISO(h.date), new Date())).length}
</span>
</div>
<div className="flex justify-between items-center">
<span>Recurring Holidays</span>
<span className="font-bold text-blue-600 dark:text-blue-400">
{filteredHolidays.filter(h => h.isRecurring).length}
</span>
</div>
<Button
variant="outline"
className="w-full mt-2 justify-center items-center border-amber-200 hover:bg-amber-50 text-amber-700 dark:border-amber-800 dark:hover:bg-amber-900/30 dark:text-amber-400"
onClick={() => navigate('/leaves')}
>
<CalendarPlus className="mr-2 h-4 w-4" />
<span>Request Leave</span>
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
</div>
{/* Create Dialog */}
<CustomDialog
isOpen={isCreateDialogOpen}
onClose={() => !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}
>
<Form {...createForm}>
<form onSubmit={createForm.handleSubmit(onCreateSubmit)} className="space-y-4">
<FormField
control={createForm.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Holiday Name</FormLabel>
<FormControl>
<Input placeholder="e.g. Independence Day" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="date"
render={({ field }) => (
<FormItem>
<FormLabel>Date</FormLabel>
<FormControl>
<Input
type="date"
value={field.value ? format(field.value, "yyyy-MM-dd") : ""}
onChange={(e) => {
if (e.target.value) {
field.onChange(new Date(e.target.value));
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Provide a brief description of this holiday"
className="min-h-[80px]"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<FormField
control={createForm.control}
name="isRecurring"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Recurring Holiday</FormLabel>
<FormDescription>
This holiday repeats annually
</FormDescription>
</div>
</FormItem>
)}
/>
<FormField
control={createForm.control}
name="appliesTo"
render={({ field }) => (
<FormItem>
<FormLabel>Applies To</FormLabel>
<FormControl>
<select
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
value={field.value}
onChange={(e) => field.onChange(e.target.value)}
>
<option value="All">All Employees</option>
<option value="Department">Specific Department</option>
<option value="Location">Specific Location</option>
</select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end gap-2 mt-4">
<Button
type="button"
variant="outline"
onClick={() => !createMutation.isPending && setIsCreateDialogOpen(false)}
>
Cancel
</Button>
<Button type="submit" disabled={createMutation.isPending}>
{createMutation.isPending && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Create Holiday
</Button>
</div>
</form>
</Form>
</CustomDialog>
{/* Edit Dialog */}
<CustomDialog
isOpen={isEditDialogOpen}
onClose={() => !updateMutation.isPending && setIsEditDialogOpen(false)}
title="Edit Holiday"
description="Update the details of the selected holiday"
allowClose={!updateMutation.isPending}
isPending={updateMutation.isPending}
>
<Form {...editForm}>
<form onSubmit={editForm.handleSubmit(onEditSubmit)} className="space-y-4">
<FormField
control={editForm.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Holiday Name</FormLabel>
<FormControl>
<Input placeholder="e.g. Independence Day" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="date"
render={({ field }) => (
<FormItem>
<FormLabel>Date</FormLabel>
<FormControl>
<Input
type="date"
value={field.value ? format(field.value, "yyyy-MM-dd") : ""}
onChange={(e) => {
if (e.target.value) {
field.onChange(new Date(e.target.value));
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Provide a brief description of this holiday"
className="min-h-[80px]"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<FormField
control={editForm.control}
name="isRecurring"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>Recurring Holiday</FormLabel>
<FormDescription>
This holiday repeats annually
</FormDescription>
</div>
</FormItem>
)}
/>
<FormField
control={editForm.control}
name="appliesTo"
render={({ field }) => (
<FormItem>
<FormLabel>Applies To</FormLabel>
<FormControl>
<select
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
value={field.value}
onChange={(e) => field.onChange(e.target.value)}
>
<option value="All">All Employees</option>
<option value="Department">Specific Department</option>
<option value="Location">Specific Location</option>
</select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex justify-end gap-2 mt-4">
<Button
type="button"
variant="outline"
onClick={() => !updateMutation.isPending && setIsEditDialogOpen(false)}
>
Cancel
</Button>
<Button type="submit" disabled={updateMutation.isPending}>
{updateMutation.isPending && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Update Holiday
</Button>
</div>
</form>
</Form>
</CustomDialog>
{/* Delete Confirmation Dialog */}
<CustomDialog
isOpen={isDeleteDialogOpen}
onClose={() => !deleteMutation.isPending && setIsDeleteDialogOpen(false)}
title="Delete Holiday"
description="Are you sure you want to delete this holiday? This action cannot be undone."
allowClose={!deleteMutation.isPending}
isPending={deleteMutation.isPending}
>
{selectedHoliday && (
<div className="mt-4 space-y-4">
<div className="rounded-lg border border-muted p-3">
<div className="font-medium">{selectedHoliday.name}</div>
<div className="mt-1 flex flex-wrap gap-2">
<Badge variant="outline" className={cn(getSeasonalColor(parseISO(selectedHoliday.date)))}>
{format(parseISO(selectedHoliday.date), "MMMM d, yyyy")}
{selectedHoliday.isRecurring && <span className="ml-1">(Recurring)</span>}
</Badge>
</div>
<p className="mt-2 text-sm text-muted-foreground">{selectedHoliday.description}</p>
</div>
</div>
)}
<div className="flex justify-end gap-2 mt-6">
<Button
type="button"
variant="outline"
onClick={() => !deleteMutation.isPending && setIsDeleteDialogOpen(false)}
>
Cancel
</Button>
<Button
type="button"
variant="destructive"
onClick={onDeleteConfirm}
disabled={deleteMutation.isPending}
>
{deleteMutation.isPending && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
Delete Holiday
</Button>
</div>
</CustomDialog>
</Layout>
);
};
export default Holidays;