Spaces:
Sleeping
Sleeping
| import React, { useState, useRef, useEffect } from "react"; | |
| import { Check, ChevronsUpDown, Search, X } from "lucide-react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Avatar, AvatarFallback } from "@/components/ui/avatar"; | |
| import { cn } from "@/lib/utils"; | |
| import { Employee } from "@/types"; | |
| interface EmployeeSelectorProps { | |
| employees: Employee[]; | |
| selectedEmployeeId: string; | |
| onSelect: (employeeId: string) => void; | |
| placeholder?: string; | |
| disabled?: boolean; | |
| className?: string; | |
| } | |
| const EmployeeSelector = ({ | |
| employees, | |
| selectedEmployeeId, | |
| onSelect, | |
| placeholder = "Select an employee", | |
| disabled = false, | |
| className | |
| }: EmployeeSelectorProps) => { | |
| const [isOpen, setIsOpen] = useState(false); | |
| const [searchQuery, setSearchQuery] = useState(""); | |
| const dropdownRef = useRef<HTMLDivElement>(null); | |
| // Find the currently selected employee | |
| const selected = employees.find(employee => employee.id.toString() === selectedEmployeeId); | |
| // Get full name of employee | |
| const getFullName = (employee: Employee): string => { | |
| return [employee.firstName, employee.middleName, employee.lastName] | |
| .filter(Boolean) | |
| .join(" "); | |
| }; | |
| // Get initials for avatar | |
| const getInitials = (employee: Employee): string => { | |
| return `${employee.firstName.charAt(0)}${employee.lastName.charAt(0)}`.toUpperCase(); | |
| }; | |
| // Filter employees based on search query | |
| const filteredEmployees = employees.filter(employee => { | |
| const fullName = getFullName(employee).toLowerCase(); | |
| const query = searchQuery.toLowerCase(); | |
| return ( | |
| fullName.includes(query) || | |
| employee.email.toLowerCase().includes(query) || | |
| employee.empCode.toLowerCase().includes(query) | |
| ); | |
| }); | |
| // Handle click outside to close dropdown | |
| useEffect(() => { | |
| function handleClickOutside(event: MouseEvent) { | |
| if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { | |
| setIsOpen(false); | |
| } | |
| } | |
| document.addEventListener("mousedown", handleClickOutside); | |
| return () => { | |
| document.removeEventListener("mousedown", handleClickOutside); | |
| }; | |
| }, []); | |
| // Handle selecting an employee | |
| const handleSelectEmployee = (employeeId: string) => { | |
| onSelect(employeeId); | |
| setIsOpen(false); | |
| setSearchQuery(""); | |
| }; | |
| // Handle clearing selection | |
| const handleClearSelection = (e: React.MouseEvent) => { | |
| e.stopPropagation(); | |
| onSelect(""); | |
| }; | |
| return ( | |
| <div className={cn("relative w-full", className)} ref={dropdownRef}> | |
| <Button | |
| type="button" | |
| variant="outline" | |
| role="combobox" | |
| aria-expanded={isOpen} | |
| className={cn( | |
| "w-full justify-between font-normal", | |
| !selectedEmployeeId && "text-muted-foreground" | |
| )} | |
| onClick={() => setIsOpen(!isOpen)} | |
| disabled={disabled} | |
| > | |
| {selected ? ( | |
| <div className="flex items-center gap-2"> | |
| <Avatar className="h-6 w-6"> | |
| <AvatarFallback> | |
| {getInitials(selected)} | |
| </AvatarFallback> | |
| </Avatar> | |
| <span>{getFullName(selected)}</span> | |
| </div> | |
| ) : ( | |
| <span>{placeholder}</span> | |
| )} | |
| <div className="flex items-center"> | |
| {selectedEmployeeId && ( | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="h-4 w-4 p-0 mr-1 opacity-70 hover:opacity-100" | |
| onClick={handleClearSelection} | |
| > | |
| <X className="h-3 w-3" /> | |
| <span className="sr-only">Clear selection</span> | |
| </Button> | |
| )} | |
| <ChevronsUpDown className="h-4 w-4 opacity-50" /> | |
| </div> | |
| </Button> | |
| {isOpen && ( | |
| <div className="absolute top-full z-[9999] mt-1 w-full rounded-md border border-input bg-popover shadow-md animate-in fade-in-0 zoom-in-95"> | |
| <div className="flex items-center border-b px-3 py-2"> | |
| <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" /> | |
| <Input | |
| placeholder="Search employees..." | |
| className="flex h-8 w-full rounded-md bg-transparent py-2 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50 border-0 focus-visible:ring-0 focus-visible:ring-offset-0" | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| onClick={(e) => e.stopPropagation()} | |
| /> | |
| </div> | |
| <div className="max-h-[15rem] overflow-y-auto py-1"> | |
| {filteredEmployees.length === 0 ? ( | |
| <div className="px-2 py-2 text-sm text-center text-muted-foreground"> | |
| No employees found | |
| </div> | |
| ) : ( | |
| filteredEmployees.map(employee => ( | |
| <div | |
| key={employee.id} | |
| className={cn( | |
| "flex items-center justify-between px-3 py-2 text-sm cursor-pointer hover:bg-accent", | |
| employee.id.toString() === selectedEmployeeId && "bg-accent/50" | |
| )} | |
| onClick={() => handleSelectEmployee(employee.id.toString())} | |
| > | |
| <div className="flex items-center gap-2"> | |
| <Avatar className="h-6 w-6"> | |
| <AvatarFallback> | |
| {getInitials(employee)} | |
| </AvatarFallback> | |
| </Avatar> | |
| <div> | |
| <p className="font-medium">{getFullName(employee)}</p> | |
| <p className="text-xs text-muted-foreground truncate max-w-[200px]"> | |
| {employee.email} • {employee.empCode} | |
| </p> | |
| </div> | |
| </div> | |
| {employee.id.toString() === selectedEmployeeId && ( | |
| <Check className="h-4 w-4 text-primary" /> | |
| )} | |
| </div> | |
| )) | |
| )} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| }; | |
| export default EmployeeSelector; |