Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect, useRef } from "react"; | |
| import { useLocation } from "react-router-dom"; | |
| import { useAuth } from "@/lib/auth-context"; | |
| import { | |
| PlusCircle, | |
| Search, | |
| Loader2, | |
| FileEdit, | |
| Trash2, | |
| Check, | |
| X, | |
| CalendarRange, | |
| ChevronsUpDown | |
| } from "lucide-react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| import { | |
| Table, | |
| TableBody, | |
| TableCell, | |
| TableHead, | |
| TableHeader, | |
| TableRow | |
| } from "@/components/ui/table"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { | |
| Card, | |
| CardContent, | |
| CardDescription, | |
| CardHeader, | |
| CardTitle, | |
| } from "@/components/ui/card"; | |
| import { Label } from "@/components/ui/label"; | |
| import { Switch } from "@/components/ui/switch"; | |
| import { Textarea } from "@/components/ui/textarea"; | |
| import { toast } from "sonner"; | |
| import { | |
| LeaveMaster, | |
| LeaveMasterCreateRequest, | |
| LeaveMasterUpdateRequest | |
| } from "@/types"; | |
| import { leaveMasterApi } from "@/services/leaveMasterApi"; | |
| import { roleService } from "@/lib/api"; | |
| import Header from "@/components/layout/header"; | |
| import Sidebar from "@/components/layout/sidebar"; | |
| import { useIsMobile } from "@/hooks/use-mobile"; | |
| import CustomDialog from "@/components/CustomDialog"; | |
| import { cn } from "@/lib/utils"; | |
| // Multi-select roles component | |
| interface Role { | |
| roleId: number; | |
| roleName: string; | |
| } | |
| interface MultiSelectRolesProps { | |
| roles: Role[]; | |
| selectedRoleIds: number[]; | |
| onChange: (roleIds: number[]) => void; | |
| label?: string; | |
| placeholder?: string; | |
| } | |
| const MultiSelectRoles: React.FC<MultiSelectRolesProps> = ({ | |
| roles, | |
| selectedRoleIds, | |
| onChange, | |
| label = "Roles", | |
| placeholder = "Select roles..." | |
| }) => { | |
| const [isOpen, setIsOpen] = useState(false); | |
| const [searchTerm, setSearchTerm] = useState(""); | |
| const containerRef = useRef<HTMLDivElement>(null); | |
| // Close dropdown when clicking outside | |
| useEffect(() => { | |
| const handleClickOutside = (event: MouseEvent) => { | |
| if (containerRef.current && !containerRef.current.contains(event.target as Node)) { | |
| setIsOpen(false); | |
| } | |
| }; | |
| document.addEventListener("mousedown", handleClickOutside); | |
| return () => document.removeEventListener("mousedown", handleClickOutside); | |
| }, []); | |
| // Filter roles based on search term | |
| const filteredRoles = roles.filter(role => | |
| role.roleName.toLowerCase().includes(searchTerm.toLowerCase()) | |
| ); | |
| // Toggle role selection | |
| const toggleRole = (roleId: number) => { | |
| if (selectedRoleIds.includes(roleId)) { | |
| onChange(selectedRoleIds.filter(id => id !== roleId)); | |
| } else { | |
| onChange([...selectedRoleIds, roleId]); | |
| } | |
| }; | |
| // Get selected role names for display | |
| const getSelectedRoleNames = () => { | |
| return selectedRoleIds | |
| .map(id => roles.find(role => role.roleId === id)?.roleName) | |
| .filter(Boolean) | |
| .join(", "); | |
| }; | |
| return ( | |
| <div className="space-y-2"> | |
| {label && <Label>{label}</Label>} | |
| <div ref={containerRef} className="relative"> | |
| <div | |
| className={cn( | |
| "flex min-h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background cursor-pointer", | |
| isOpen && "ring-2 ring-ring ring-offset-2" | |
| )} | |
| onClick={() => setIsOpen(!isOpen)} | |
| > | |
| {selectedRoleIds.length > 0 ? ( | |
| <div className="flex-1 truncate">{getSelectedRoleNames()}</div> | |
| ) : ( | |
| <div className="text-muted-foreground">{placeholder}</div> | |
| )} | |
| <ChevronsUpDown className="h-4 w-4 shrink-0 opacity-50" /> | |
| </div> | |
| {isOpen && ( | |
| <div className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md"> | |
| <div className="sticky top-0 bg-popover p-2 border-b"> | |
| <Input | |
| placeholder="Search roles..." | |
| value={searchTerm} | |
| onChange={(e) => setSearchTerm(e.target.value)} | |
| className="h-8" | |
| onClick={(e) => e.stopPropagation()} | |
| /> | |
| </div> | |
| <div className="pt-1"> | |
| {filteredRoles.length === 0 ? ( | |
| <div className="py-2 px-2 text-sm text-center text-muted-foreground"> | |
| No roles found | |
| </div> | |
| ) : ( | |
| filteredRoles.map((role) => ( | |
| <div | |
| key={role.roleId} | |
| className={cn( | |
| "flex items-center justify-between py-1.5 px-2 text-sm rounded-sm cursor-pointer", | |
| selectedRoleIds.includes(role.roleId) | |
| ? "bg-primary text-primary-foreground" | |
| : "hover:bg-accent hover:text-accent-foreground" | |
| )} | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| toggleRole(role.roleId); | |
| }} | |
| > | |
| <span>{role.roleName}</span> | |
| {selectedRoleIds.includes(role.roleId) && ( | |
| <Check className="h-4 w-4" /> | |
| )} | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| {selectedRoleIds.length > 0 && ( | |
| <div className="flex flex-wrap gap-1 mt-2"> | |
| {selectedRoleIds.map(roleId => { | |
| const role = roles.find(r => r.roleId === roleId); | |
| if (!role) return null; | |
| return ( | |
| <Badge | |
| key={roleId} | |
| variant="secondary" | |
| className="flex items-center gap-1" | |
| > | |
| {role.roleName} | |
| <button | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| toggleRole(roleId); | |
| }} | |
| className="ml-1 rounded-full hover:bg-accent p-0.5" | |
| > | |
| <X className="h-3 w-3" /> | |
| <span className="sr-only">Remove {role.roleName}</span> | |
| </button> | |
| </Badge> | |
| ); | |
| })} | |
| </div> | |
| )} | |
| </div> | |
| {selectedRoleIds.length === 0 && ( | |
| <p className="text-xs text-muted-foreground"> | |
| Leave empty to apply to all roles | |
| </p> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| const LeaveTypesPage: React.FC = () => { | |
| const { userData } = useAuth(); | |
| const location = useLocation(); | |
| const isMobile = useIsMobile(); | |
| const [isSidebarOpen, setIsSidebarOpen] = useState(!isMobile); | |
| // Close sidebar on mobile when route changes | |
| useEffect(() => { | |
| if (isMobile) { | |
| setIsSidebarOpen(false); | |
| } | |
| }, [location, isMobile]); | |
| // Update sidebar state when screen size changes | |
| useEffect(() => { | |
| setIsSidebarOpen(!isMobile); | |
| }, [isMobile]); | |
| const [isLoading, setIsLoading] = useState(true); | |
| const [leaveTypes, setLeaveTypes] = useState<LeaveMaster[]>([]); | |
| const [filteredLeaveTypes, setFilteredLeaveTypes] = useState<LeaveMaster[]>([]); | |
| const [searchQuery, setSearchQuery] = useState(""); | |
| const [roles, setRoles] = useState<{roleId: number, roleName: string}[]>([]); | |
| // Dialog states | |
| const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); | |
| const [isEditDialogOpen, setIsEditDialogOpen] = useState(false); | |
| const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); | |
| const [selectedLeaveType, setSelectedLeaveType] = useState<LeaveMaster | null>(null); | |
| // Form state | |
| const [newLeaveType, setNewLeaveType] = useState<LeaveMasterCreateRequest>({ | |
| leaveType: "", | |
| description: "", | |
| defaultDays: 0, | |
| carryForwardLimit: 0, | |
| isActive: true, | |
| minDaysBeforeApply: 0, | |
| requiresApproval: true, | |
| requiresDocumentation: false, | |
| applyToRoles: "", | |
| }); | |
| const [editLeaveType, setEditLeaveType] = useState<LeaveMasterUpdateRequest>({ | |
| id: 0, | |
| leaveType: "", | |
| description: "", | |
| defaultDays: 0, | |
| carryForwardLimit: 0, | |
| isActive: true, | |
| minDaysBeforeApply: 0, | |
| requiresApproval: true, | |
| requiresDocumentation: false, | |
| applyToRoles: "", | |
| }); | |
| // Toggle sidebar | |
| const toggleSidebar = () => { | |
| setIsSidebarOpen(!isSidebarOpen); | |
| }; | |
| // Fetch leave types | |
| useEffect(() => { | |
| const fetchLeaveTypes = async () => { | |
| try { | |
| setIsLoading(true); | |
| const leaveTypes = await leaveMasterApi.getAll(); | |
| setLeaveTypes(leaveTypes); | |
| setFilteredLeaveTypes(leaveTypes); | |
| } catch (error) { | |
| console.error("Error fetching leave types:", error); | |
| toast.error("Failed to fetch leave types"); | |
| } finally { | |
| setIsLoading(false); | |
| } | |
| }; | |
| const fetchRoles = async () => { | |
| try { | |
| const roles = await roleService.getAll(); | |
| setRoles(roles); | |
| } catch (error) { | |
| console.error("Error fetching roles:", error); | |
| toast.error("Failed to fetch roles"); | |
| } | |
| }; | |
| fetchLeaveTypes(); | |
| fetchRoles(); | |
| }, []); | |
| // Filter leave types when search query changes | |
| useEffect(() => { | |
| if (!searchQuery) { | |
| setFilteredLeaveTypes(leaveTypes); | |
| return; | |
| } | |
| const query = searchQuery.toLowerCase(); | |
| const filtered = leaveTypes.filter( | |
| (type) => | |
| type.leaveType.toLowerCase().includes(query) || | |
| type.description.toLowerCase().includes(query) | |
| ); | |
| setFilteredLeaveTypes(filtered); | |
| }, [searchQuery, leaveTypes]); | |
| // Handle create form submission | |
| const handleCreateSubmit = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| try { | |
| const createdType = await leaveMasterApi.create(newLeaveType); | |
| setLeaveTypes([...leaveTypes, createdType]); | |
| setIsCreateDialogOpen(false); | |
| resetCreateForm(); | |
| toast.success("Leave type created successfully"); | |
| } catch (error) { | |
| console.error("Error creating leave type:", error); | |
| toast.error("Failed to create leave type"); | |
| } | |
| }; | |
| // Handle edit form submission | |
| const handleEditSubmit = async (e: React.FormEvent) => { | |
| e.preventDefault(); | |
| if (!selectedLeaveType) return; | |
| try { | |
| const updatedType = await leaveMasterApi.update( | |
| selectedLeaveType.id, | |
| editLeaveType | |
| ); | |
| // Check if the response is not null (i.e., it's not a 204 No Content response) | |
| if (updatedType) { | |
| setLeaveTypes( | |
| leaveTypes.map((type) => | |
| type.id === updatedType.id ? updatedType : type | |
| ) | |
| ); | |
| } else { | |
| // Handle the case when there's no content (204 No Content) | |
| console.log('Leave type update successful, but no data returned.'); | |
| // Optionally handle this case differently, depending on your requirements | |
| } | |
| setIsEditDialogOpen(false); | |
| setSelectedLeaveType(null); | |
| toast.success("Leave type updated successfully"); | |
| } catch (error) { | |
| console.error("Error updating leave type:", error); | |
| toast.error("Failed to update leave type"); | |
| } | |
| }; | |
| // Handle delete confirmation | |
| const handleDeleteConfirm = async () => { | |
| if (!selectedLeaveType) return; | |
| try { | |
| await leaveMasterApi.delete(selectedLeaveType.id); | |
| setLeaveTypes(leaveTypes.filter((type) => type.id !== selectedLeaveType.id)); | |
| setIsDeleteDialogOpen(false); | |
| setSelectedLeaveType(null); | |
| toast.success("Leave type deleted successfully"); | |
| } catch (error) { | |
| console.error("Error deleting leave type:", error); | |
| toast.error("Failed to delete leave type"); | |
| } | |
| }; | |
| // Toggle leave type active status | |
| const handleToggleStatus = async (leaveType: LeaveMaster) => { | |
| try { | |
| const updatedType = await leaveMasterApi.toggleStatus( | |
| leaveType.id, | |
| !leaveType.isActive | |
| ); | |
| setLeaveTypes( | |
| leaveTypes.map((type) => | |
| type.id === updatedType.id ? updatedType : type | |
| ) | |
| ); | |
| toast.success(`Leave type ${updatedType.isActive ? "activated" : "deactivated"} successfully`); | |
| } catch (error) { | |
| console.error("Error toggling leave type status:", error); | |
| toast.error("Failed to update leave type status"); | |
| } | |
| }; | |
| // Set up edit form | |
| const handleEditClick = (leaveType: LeaveMaster) => { | |
| setSelectedLeaveType(leaveType); | |
| setEditLeaveType({ | |
| id: leaveType.id, | |
| leaveType: leaveType.leaveType, | |
| description: leaveType.description, | |
| defaultDays: leaveType.defaultDays, | |
| carryForwardLimit: leaveType.carryForwardLimit, | |
| isActive: leaveType.isActive, | |
| maxConsecutiveDays: leaveType.maxConsecutiveDays, | |
| minDaysBeforeApply: leaveType.minDaysBeforeApply, | |
| requiresApproval: leaveType.requiresApproval, | |
| requiresDocumentation: leaveType.requiresDocumentation, | |
| applyToRoles: leaveType.applyToRoles, | |
| }); | |
| setIsEditDialogOpen(true); | |
| }; | |
| // Set up delete dialog | |
| const handleDeleteClick = (leaveType: LeaveMaster) => { | |
| setSelectedLeaveType(leaveType); | |
| setIsDeleteDialogOpen(true); | |
| }; | |
| // Reset create form | |
| const resetCreateForm = () => { | |
| setNewLeaveType({ | |
| leaveType: "", | |
| description: "", | |
| defaultDays: 0, | |
| carryForwardLimit: 0, | |
| isActive: true, | |
| minDaysBeforeApply: 0, | |
| requiresApproval: true, | |
| requiresDocumentation: false, | |
| applyToRoles: "", | |
| }); | |
| }; | |
| // Handle form input changes for create | |
| const handleCreateChange = ( | |
| e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement> | |
| ) => { | |
| const { name, value } = e.target; | |
| setNewLeaveType((prev) => ({ | |
| ...prev, | |
| [name]: value, | |
| })); | |
| }; | |
| // Handle create form switch changes | |
| const handleCreateSwitchChange = (name: string, checked: boolean) => { | |
| setNewLeaveType((prev) => ({ | |
| ...prev, | |
| [name]: checked, | |
| })); | |
| }; | |
| // Handle form input changes for edit | |
| const handleEditChange = ( | |
| e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement> | |
| ) => { | |
| const { name, value } = e.target; | |
| setEditLeaveType((prev) => ({ | |
| ...prev, | |
| [name]: value, | |
| })); | |
| }; | |
| // Handle edit form switch changes | |
| const handleEditSwitchChange = (name: string, checked: boolean) => { | |
| setEditLeaveType((prev) => ({ | |
| ...prev, | |
| [name]: checked, | |
| })); | |
| }; | |
| // Handle numeric input to ensure they're numbers | |
| const handleNumericChange = ( | |
| e: React.ChangeEvent<HTMLInputElement>, | |
| isEditForm: boolean | |
| ) => { | |
| const { name, value } = e.target; | |
| const numValue = value === "" ? 0 : parseInt(value, 10); | |
| if (isEditForm) { | |
| setEditLeaveType((prev) => ({ | |
| ...prev, | |
| [name]: numValue, | |
| })); | |
| } else { | |
| setNewLeaveType((prev) => ({ | |
| ...prev, | |
| [name]: numValue, | |
| })); | |
| } | |
| }; | |
| // Format roles string for display | |
| const formatRoles = (rolesString: string) => { | |
| if (!rolesString || !roles.length) return "All roles"; | |
| const roleIds = rolesString.split(",").map(id => parseInt(id.trim(), 10)); | |
| const roleNames = roleIds.map( | |
| id => roles.find(r => r.roleId === id)?.roleName || `Role ${id}` | |
| ); | |
| return roleNames.join(", "); | |
| }; | |
| // Convert string role IDs to array of numbers | |
| const parseRoleIds = (rolesString: string): number[] => { | |
| if (!rolesString || rolesString.trim() === "") return []; | |
| return rolesString.split(',').map(id => parseInt(id.trim(), 10)); | |
| }; | |
| // Convert array of role IDs to comma-separated string | |
| const stringifyRoleIds = (roleIds: number[]): string => { | |
| return roleIds.join(','); | |
| }; | |
| // New state for multi-select | |
| const [selectedRoleIds, setSelectedRoleIds] = useState<number[]>([]); | |
| const [editSelectedRoleIds, setEditSelectedRoleIds] = useState<number[]>([]); | |
| // Update the Role IDs when form state changes | |
| useEffect(() => { | |
| setSelectedRoleIds(parseRoleIds(newLeaveType.applyToRoles)); | |
| }, [newLeaveType.applyToRoles]); | |
| useEffect(() => { | |
| setEditSelectedRoleIds(parseRoleIds(editLeaveType.applyToRoles)); | |
| }, [editLeaveType.applyToRoles]); | |
| // Handle role selection for create form | |
| const handleRoleSelectionChange = (roleIds: number[]) => { | |
| setSelectedRoleIds(roleIds); | |
| setNewLeaveType(prev => ({ | |
| ...prev, | |
| applyToRoles: stringifyRoleIds(roleIds) | |
| })); | |
| }; | |
| // Handle role selection for edit form | |
| const handleEditRoleSelectionChange = (roleIds: number[]) => { | |
| setEditSelectedRoleIds(roleIds); | |
| setEditLeaveType(prev => ({ | |
| ...prev, | |
| applyToRoles: stringifyRoleIds(roleIds) | |
| })); | |
| }; | |
| return ( | |
| <div className="flex min-h-screen flex-col"> | |
| <Header toggleSidebar={toggleSidebar} isSidebarOpen={isSidebarOpen} /> | |
| <Sidebar isOpen={isSidebarOpen} setIsOpen={setIsSidebarOpen} /> | |
| <main className={`flex-1 p-6 pt-20 md:p-10 md:pt-24 ${isSidebarOpen ? "md:ml-64" : ""} transition-all duration-300`}> | |
| <div className="mx-auto max-w-6xl"> | |
| <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between mb-8 gap-4"> | |
| <div> | |
| <h2 className="text-3xl font-bold tracking-tight">Leave Types</h2> | |
| <p className="text-muted-foreground"> | |
| Manage leave types and policies | |
| </p> | |
| </div> | |
| <Button onClick={(e) => { | |
| e.stopPropagation(); | |
| setIsCreateDialogOpen(true); | |
| }}> | |
| <PlusCircle className="mr-2 h-4 w-4" /> | |
| Add Leave Type | |
| </Button> | |
| <CustomDialog | |
| isOpen={isCreateDialogOpen} | |
| onClose={() => setIsCreateDialogOpen(false)} | |
| title="Add Leave Type" | |
| description="Create a new leave type and define its policies" | |
| allowClose={true} | |
| isPending={false} | |
| > | |
| <form onSubmit={handleCreateSubmit} className="space-y-6"> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="leaveType">Leave Type Name</Label> | |
| <Input | |
| id="leaveType" | |
| name="leaveType" | |
| placeholder="Annual Leave" | |
| value={newLeaveType.leaveType} | |
| onChange={handleCreateChange} | |
| required | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="defaultDays">Default Days Per Year</Label> | |
| <Input | |
| id="defaultDays" | |
| name="defaultDays" | |
| type="number" | |
| min="0" | |
| placeholder="21" | |
| value={newLeaveType.defaultDays} | |
| onChange={(e) => handleNumericChange(e, false)} | |
| required | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="carryForwardLimit">Carry Forward Limit</Label> | |
| <Input | |
| id="carryForwardLimit" | |
| name="carryForwardLimit" | |
| type="number" | |
| min="0" | |
| placeholder="5" | |
| value={newLeaveType.carryForwardLimit} | |
| onChange={(e) => handleNumericChange(e, false)} | |
| required | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="minDaysBeforeApply"> | |
| Minimum Days Before Apply | |
| </Label> | |
| <Input | |
| id="minDaysBeforeApply" | |
| name="minDaysBeforeApply" | |
| type="number" | |
| min="0" | |
| placeholder="3" | |
| value={newLeaveType.minDaysBeforeApply} | |
| onChange={(e) => handleNumericChange(e, false)} | |
| required | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="maxConsecutiveDays"> | |
| Maximum Consecutive Days (Optional) | |
| </Label> | |
| <Input | |
| id="maxConsecutiveDays" | |
| name="maxConsecutiveDays" | |
| type="number" | |
| min="0" | |
| placeholder="30" | |
| value={newLeaveType.maxConsecutiveDays || ""} | |
| onChange={(e) => | |
| handleNumericChange(e, false) | |
| } | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <MultiSelectRoles | |
| roles={roles || []} | |
| selectedRoleIds={selectedRoleIds} | |
| onChange={handleRoleSelectionChange} | |
| label="Apply To Roles" | |
| placeholder="Select roles..." | |
| /> | |
| </div> | |
| <div className="space-y-2 flex items-center"> | |
| <div className="flex items-center space-x-2"> | |
| <Switch | |
| id="isActive" | |
| checked={newLeaveType.isActive} | |
| onCheckedChange={(checked) => | |
| handleCreateSwitchChange("isActive", checked) | |
| } | |
| /> | |
| <Label htmlFor="isActive">Active</Label> | |
| </div> | |
| </div> | |
| <div className="space-y-2 flex items-center"> | |
| <div className="flex items-center space-x-2"> | |
| <Switch | |
| id="requiresApproval" | |
| checked={newLeaveType.requiresApproval} | |
| onCheckedChange={(checked) => | |
| handleCreateSwitchChange("requiresApproval", checked) | |
| } | |
| /> | |
| <Label htmlFor="requiresApproval">Requires Approval</Label> | |
| </div> | |
| </div> | |
| <div className="space-y-2 flex items-center"> | |
| <div className="flex items-center space-x-2"> | |
| <Switch | |
| id="requiresDocumentation" | |
| checked={newLeaveType.requiresDocumentation} | |
| onCheckedChange={(checked) => | |
| handleCreateSwitchChange("requiresDocumentation", checked) | |
| } | |
| /> | |
| <Label htmlFor="requiresDocumentation">Requires Documentation</Label> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="description">Description</Label> | |
| <Textarea | |
| id="description" | |
| name="description" | |
| placeholder="Annual leave entitlement for employees" | |
| value={newLeaveType.description} | |
| onChange={handleCreateChange} | |
| className="min-h-[100px]" | |
| required | |
| /> | |
| </div> | |
| <div className="flex justify-end gap-2 mt-4"> | |
| <Button | |
| variant="outline" | |
| type="button" | |
| onClick={() => { | |
| resetCreateForm(); | |
| setIsCreateDialogOpen(false); | |
| }} | |
| > | |
| Cancel | |
| </Button> | |
| <Button type="submit">Create Leave Type</Button> | |
| </div> | |
| </form> | |
| </CustomDialog> | |
| <CustomDialog | |
| isOpen={isEditDialogOpen} | |
| onClose={() => setIsEditDialogOpen(false)} | |
| title="Edit Leave Type" | |
| description="Update leave type and its policies" | |
| allowClose={true} | |
| isPending={false} | |
| > | |
| <form onSubmit={handleEditSubmit} className="space-y-6"> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div className="space-y-2"> | |
| <Label htmlFor="edit-leaveType">Leave Type Name</Label> | |
| <Input | |
| id="edit-leaveType" | |
| name="leaveType" | |
| placeholder="Annual Leave" | |
| value={editLeaveType.leaveType} | |
| onChange={handleEditChange} | |
| required | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="edit-defaultDays">Default Days Per Year</Label> | |
| <Input | |
| id="edit-defaultDays" | |
| name="defaultDays" | |
| type="number" | |
| min="0" | |
| placeholder="21" | |
| value={editLeaveType.defaultDays} | |
| onChange={(e) => handleNumericChange(e, true)} | |
| required | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="edit-carryForwardLimit">Carry Forward Limit</Label> | |
| <Input | |
| id="edit-carryForwardLimit" | |
| name="carryForwardLimit" | |
| type="number" | |
| min="0" | |
| placeholder="5" | |
| value={editLeaveType.carryForwardLimit} | |
| onChange={(e) => handleNumericChange(e, true)} | |
| required | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="edit-minDaysBeforeApply"> | |
| Minimum Days Before Apply | |
| </Label> | |
| <Input | |
| id="edit-minDaysBeforeApply" | |
| name="minDaysBeforeApply" | |
| type="number" | |
| min="0" | |
| placeholder="3" | |
| value={editLeaveType.minDaysBeforeApply} | |
| onChange={(e) => handleNumericChange(e, true)} | |
| required | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="edit-maxConsecutiveDays"> | |
| Maximum Consecutive Days (Optional) | |
| </Label> | |
| <Input | |
| id="edit-maxConsecutiveDays" | |
| name="maxConsecutiveDays" | |
| type="number" | |
| min="0" | |
| placeholder="30" | |
| value={editLeaveType.maxConsecutiveDays || ""} | |
| onChange={(e) => | |
| handleNumericChange(e, true) | |
| } | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <MultiSelectRoles | |
| roles={roles || []} | |
| selectedRoleIds={editSelectedRoleIds} | |
| onChange={handleEditRoleSelectionChange} | |
| label="Apply To Roles" | |
| placeholder="Select roles..." | |
| /> | |
| </div> | |
| <div className="space-y-2 flex items-center"> | |
| <div className="flex items-center space-x-2"> | |
| <Switch | |
| id="edit-isActive" | |
| checked={editLeaveType.isActive} | |
| onCheckedChange={(checked) => | |
| handleEditSwitchChange("isActive", checked) | |
| } | |
| /> | |
| <Label htmlFor="edit-isActive">Active</Label> | |
| </div> | |
| </div> | |
| <div className="space-y-2 flex items-center"> | |
| <div className="flex items-center space-x-2"> | |
| <Switch | |
| id="edit-requiresApproval" | |
| checked={editLeaveType.requiresApproval} | |
| onCheckedChange={(checked) => | |
| handleEditSwitchChange("requiresApproval", checked) | |
| } | |
| /> | |
| <Label htmlFor="edit-requiresApproval">Requires Approval</Label> | |
| </div> | |
| </div> | |
| <div className="space-y-2 flex items-center"> | |
| <div className="flex items-center space-x-2"> | |
| <Switch | |
| id="edit-requiresDocumentation" | |
| checked={editLeaveType.requiresDocumentation} | |
| onCheckedChange={(checked) => | |
| handleEditSwitchChange("requiresDocumentation", checked) | |
| } | |
| /> | |
| <Label htmlFor="edit-requiresDocumentation">Requires Documentation</Label> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="edit-description">Description</Label> | |
| <Textarea | |
| id="edit-description" | |
| name="description" | |
| placeholder="Annual leave entitlement for employees" | |
| value={editLeaveType.description} | |
| onChange={handleEditChange} | |
| className="min-h-[100px]" | |
| required | |
| /> | |
| </div> | |
| <div className="flex justify-end gap-2 mt-4"> | |
| <Button | |
| variant="outline" | |
| type="button" | |
| onClick={() => { | |
| setIsEditDialogOpen(false); | |
| setSelectedLeaveType(null); | |
| }} | |
| > | |
| Cancel | |
| </Button> | |
| <Button type="submit">Update Leave Type</Button> | |
| </div> | |
| </form> | |
| </CustomDialog> | |
| <CustomDialog | |
| isOpen={isDeleteDialogOpen} | |
| onClose={() => setIsDeleteDialogOpen(false)} | |
| title="Delete Leave Type" | |
| description="Are you sure you want to delete this leave type? This action cannot be undone." | |
| allowClose={true} | |
| isPending={false} | |
| > | |
| {selectedLeaveType && ( | |
| <div className="py-4"> | |
| <p className="font-medium">{selectedLeaveType.leaveType}</p> | |
| <p className="text-sm text-muted-foreground mt-1"> | |
| {selectedLeaveType.description} | |
| </p> | |
| </div> | |
| )} | |
| <div className="flex justify-end gap-2 mt-4"> | |
| <Button | |
| variant="outline" | |
| onClick={() => { | |
| setIsDeleteDialogOpen(false); | |
| setSelectedLeaveType(null); | |
| }} | |
| > | |
| Cancel | |
| </Button> | |
| <Button variant="destructive" onClick={handleDeleteConfirm}> | |
| Delete | |
| </Button> | |
| </div> | |
| </CustomDialog> | |
| </div> | |
| <Card> | |
| <CardHeader> | |
| <CardTitle>All Leave Types</CardTitle> | |
| <CardDescription> | |
| Configure leave types, balances, and approval policies | |
| </CardDescription> | |
| <div className="flex items-center gap-4 mt-4"> | |
| <div className="relative flex-1"> | |
| <Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" /> | |
| <Input | |
| placeholder="Search leave types..." | |
| className="pl-8" | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| /> | |
| </div> | |
| </div> | |
| </CardHeader> | |
| <CardContent> | |
| {isLoading ? ( | |
| <div className="flex items-center justify-center p-8"> | |
| <Loader2 className="h-8 w-8 animate-spin text-primary" /> | |
| </div> | |
| ) : ( | |
| <div className="rounded-md border"> | |
| <Table> | |
| <TableHeader> | |
| <TableRow> | |
| <TableHead>Leave Type</TableHead> | |
| <TableHead>Description</TableHead> | |
| <TableHead>Balance (Days)</TableHead> | |
| <TableHead>Carry Forward</TableHead> | |
| <TableHead>Applies To</TableHead> | |
| <TableHead>Status</TableHead> | |
| <TableHead className="text-right">Actions</TableHead> | |
| </TableRow> | |
| </TableHeader> | |
| <TableBody> | |
| {filteredLeaveTypes.length === 0 ? ( | |
| <TableRow> | |
| <TableCell | |
| colSpan={7} | |
| className="h-24 text-center" | |
| > | |
| No leave types found. | |
| </TableCell> | |
| </TableRow> | |
| ) : ( | |
| filteredLeaveTypes.map((leaveType) => ( | |
| <TableRow key={leaveType.id}> | |
| <TableCell className="font-medium"> | |
| {leaveType.leaveType} | |
| </TableCell> | |
| <TableCell className="max-w-xs truncate"> | |
| {leaveType.description} | |
| </TableCell> | |
| <TableCell>{leaveType.defaultDays} days</TableCell> | |
| <TableCell>{leaveType.carryForwardLimit} days</TableCell> | |
| <TableCell> | |
| {formatRoles(leaveType.applyToRoles)} | |
| </TableCell> | |
| <TableCell> | |
| <Badge | |
| variant={leaveType.isActive ? "default" : "outline"} | |
| className="flex w-fit items-center gap-1" | |
| > | |
| {leaveType.isActive ? | |
| <Check className="h-3 w-3"/> : | |
| <X className="h-3 w-3"/> | |
| } | |
| <span>{leaveType.isActive ? "Active" : "Inactive"}</span> | |
| </Badge> | |
| </TableCell> | |
| <TableCell className="text-right"> | |
| <div className="flex justify-end gap-2"> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| onClick={() => handleEditClick(leaveType)} | |
| > | |
| <FileEdit className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| variant={leaveType.isActive ? "outline" : "default"} | |
| size="sm" | |
| onClick={() => handleToggleStatus(leaveType)} | |
| > | |
| {leaveType.isActive ? | |
| <X className="h-4 w-4"/> : | |
| <Check className="h-4 w-4"/> | |
| } | |
| </Button> | |
| <Button | |
| variant="destructive" | |
| size="sm" | |
| onClick={() => handleDeleteClick(leaveType)} | |
| > | |
| <Trash2 className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| </TableCell> | |
| </TableRow> | |
| )) | |
| )} | |
| </TableBody> | |
| </Table> | |
| </div> | |
| )} | |
| </CardContent> | |
| </Card> | |
| </div> | |
| </main> | |
| </div> | |
| ); | |
| }; | |
| export default LeaveTypesPage; |