pmtool / src /components /custom /employee-selector.tsx
devarshia5's picture
Upload 487 files
d97b8f9 verified
Raw
History Blame Contribute Delete
6.42 kB
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;