182 lines
6.3 KiB
JavaScript
182 lines
6.3 KiB
JavaScript
import { useState, useEffect, useRef } from "react";
|
||
import { useEmployeesName } from "../../hooks/useEmployees";
|
||
import { useDebounce } from "../../utils/appUtils";
|
||
import { useController } from "react-hook-form";
|
||
import Avatar from "../common/Avatar";
|
||
|
||
const MultiEmployeeSearchInput = ({
|
||
control,
|
||
name,
|
||
projectId,
|
||
placeholder,
|
||
forAll,
|
||
}) => {
|
||
const {
|
||
field: { onChange, value, ref },
|
||
fieldState: { error },
|
||
} = useController({ name, control });
|
||
|
||
const [search, setSearch] = useState("");
|
||
const [showDropdown, setShowDropdown] = useState(false);
|
||
const [selectedEmployees, setSelectedEmployees] = useState([]);
|
||
const dropdownRef = useRef(null);
|
||
|
||
const debouncedSearch = useDebounce(search, 500);
|
||
|
||
const { data: employees, isLoading } = useEmployeesName(
|
||
projectId,
|
||
debouncedSearch,
|
||
forAll
|
||
);
|
||
|
||
useEffect(() => {
|
||
if (value && employees?.data) {
|
||
// Ensure value is a string (sometimes it may come as array/object)
|
||
const stringValue =
|
||
typeof value === "string"
|
||
? value
|
||
: Array.isArray(value)
|
||
? value.join(",")
|
||
: "";
|
||
|
||
const emails = stringValue.split(",").filter(Boolean);
|
||
const foundEmps = employees.data.filter((emp) =>
|
||
emails.includes(emp.email)
|
||
);
|
||
|
||
setSelectedEmployees(foundEmps);
|
||
|
||
if (forAll && foundEmps.length > 0) {
|
||
setSearch(""); // clear search field
|
||
}
|
||
}
|
||
}, [value, employees?.data, forAll]);
|
||
|
||
|
||
const handleSelect = (employee) => {
|
||
if (!selectedEmployees.find((emp) => emp.email === employee.email)) {
|
||
const newSelected = [...selectedEmployees, employee];
|
||
setSelectedEmployees(newSelected);
|
||
// Store emails instead of IDs
|
||
onChange(
|
||
newSelected
|
||
.map((e) => e.email)
|
||
.filter(Boolean)
|
||
.join(",")
|
||
);
|
||
setSearch("");
|
||
setShowDropdown(false);
|
||
}
|
||
};
|
||
|
||
const handleRemove = (email) => {
|
||
const newSelected = selectedEmployees.filter((e) => e.email !== email);
|
||
setSelectedEmployees(newSelected);
|
||
onChange(
|
||
newSelected
|
||
.map((e) => e.email)
|
||
.filter(Boolean)
|
||
.join(",")
|
||
);
|
||
};
|
||
|
||
useEffect(() => {
|
||
const handleClickOutside = (event) => {
|
||
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
||
setShowDropdown(false);
|
||
}
|
||
};
|
||
const handleEsc = (event) => {
|
||
if (event.key === "Escape") setShowDropdown(false);
|
||
};
|
||
|
||
document.addEventListener("mousedown", handleClickOutside);
|
||
document.addEventListener("keydown", handleEsc);
|
||
|
||
return () => {
|
||
document.removeEventListener("mousedown", handleClickOutside);
|
||
document.removeEventListener("keydown", handleEsc);
|
||
};
|
||
}, []);
|
||
|
||
return (
|
||
<div className="position-relative" ref={dropdownRef}>
|
||
<div className="d-flex flex-wrap gap-1 mb-1">
|
||
{selectedEmployees.map((emp) => (
|
||
<div
|
||
key={emp.email}
|
||
className="badge bg-label-secondary d-flex align-items-center py-0 px-1"
|
||
style={{ fontSize: "0.75rem" }}
|
||
>
|
||
<Avatar
|
||
size="xs"
|
||
classAvatar="m-0 me-1"
|
||
firstName={emp.firstName}
|
||
lastName={emp.lastName}
|
||
/>
|
||
{emp.firstName} {emp.lastName}
|
||
<span
|
||
className="ms-1"
|
||
style={{ cursor: "pointer", fontSize: "0.75rem" }}
|
||
onClick={() => handleRemove(emp.email)}
|
||
>
|
||
×
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
|
||
<input
|
||
type="search"
|
||
ref={ref}
|
||
className="form-control form-control-sm"
|
||
placeholder={placeholder}
|
||
value={search}
|
||
onChange={(e) => {
|
||
setSearch(e.target.value);
|
||
setShowDropdown(true);
|
||
}}
|
||
onFocus={() => setShowDropdown(true)}
|
||
/>
|
||
|
||
{showDropdown && (employees?.data?.length > 0 || isLoading) && (
|
||
<ul
|
||
className="list-group position-absolute bg-white w-100 shadow z-3 rounded-1 px-0"
|
||
style={{ maxHeight: 200, overflowY: "auto", zIndex: 1050 }}
|
||
>
|
||
{isLoading ? (
|
||
<li className="list-group-item py-1 px-2 text-muted">Searching...</li>
|
||
) : (
|
||
employees?.data
|
||
?.filter((emp) => !selectedEmployees.find((e) => e.email === emp.email))
|
||
.map((emp) => (
|
||
<li
|
||
key={emp.email}
|
||
className="list-group-item list-group-item-action py-1 px-2"
|
||
style={{ cursor: "pointer" }}
|
||
onClick={() => handleSelect(emp)}
|
||
>
|
||
<div className="d-flex align-items-center">
|
||
<Avatar
|
||
size="xs"
|
||
classAvatar="m-0 me-2"
|
||
firstName={emp.firstName}
|
||
lastName={emp.lastName}
|
||
/>
|
||
<span className="text-muted">{`${emp.firstName} ${emp.lastName}`}</span>
|
||
</div>
|
||
</li>
|
||
))
|
||
)}
|
||
</ul>
|
||
)}
|
||
|
||
{error && <small className="text-danger">{error.message}</small>}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default MultiEmployeeSearchInput;
|
||
|