// components/generic/Sheet/AddUsersSheet.jsx import { useState, useEffect, useMemo } from "react"; import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { Spinner } from "@/components/ui/spinner"; import { AlertTriangle } from "lucide-react"; /** * Generic sheet for selecting and adding users to any entity * (groups, tasks, projects, etc.) * * @param {Object} props * @param {boolean} props.open * @param {Function} props.onOpenChange * * @param {string} [props.title] Sheet heading. Default: "Add members" * @param {string} [props.submitLabel] Submit button label. Default: "Add" * * @param {Array} props.users [{ [idKey], [labelKey] }] — list to display * @param {boolean} props.loading * @param {Function} props.onFetch Called on open to (re)load the list * * @param {string} [props.idKey] Key for the user id. Default: "user_id" * @param {string} [props.labelKey] Key for the display name. Default: "full_name" * @param {string} [props.subLabelKey] Optional secondary line (e.g. "email") * @param {string} [props.warningKey] If set, users with a truthy value at this key show * a "will be moved" warning (e.g. "current_group") * * @param {Function} props.onSubmit Called with selected ids[] * * @example — groups * fetchUsersNotInGroup(gid)} * onSubmit={(ids) => addUsersToGroup(gid, ids)} * ... * /> * * @example — tasks * fetchUnassignedUsers(taskId)} * onSubmit={(ids) => assignUsersToTask(taskId, ids)} * idKey="user_id" * labelKey="full_name" * subLabelKey="email" * ... * /> */ export function AddSheet({ open, onOpenChange, title = "Add members", submitLabel = "Add", users = [], loading = false, onFetch, idKey = "user_id", labelKey = "full_name", subLabelKey = null, warningKey = null, onSubmit, }) { const [search, setSearch] = useState(""); const [selected, setSelected] = useState([]); useEffect(() => { if (open) { onFetch?.(); setSearch(""); setSelected([]); } }, [open]); const filtered = useMemo(() => { if (!search.trim()) return users; return users.filter((u) => String(u[labelKey] ?? "").toLowerCase().includes(search.toLowerCase()) ); }, [search, users, labelKey]); const toggle = (id) => setSelected((prev) => prev.includes(id) ? prev.filter((v) => v !== id) : [...prev, id] ); const toggleAll = () => { const allIds = filtered.map((u) => u[idKey]); const allSelected = allIds.every((id) => selected.includes(id)); setSelected((prev) => allSelected ? prev.filter((id) => !allIds.includes(id)) : [...new Set([...prev, ...allIds])] ); }; const allFilteredSelected = filtered.length > 0 && filtered.every((u) => selected.includes(u[idKey])); const movingCount = useMemo(() => { if (!warningKey) return 0; return selected.filter((id) => { const user = users.find((u) => u[idKey] === id); return !!user?.[warningKey]; }).length; }, [selected, users, warningKey, idKey]); async function handleSubmit() { if (!selected.length) return; await onSubmit(selected); onOpenChange(false); } function handleClose() { setSearch(""); setSelected([]); onOpenChange(false); } return ( {/* SheetContent is a flex column with fixed height (100dvh). We split it into 3 rows: header (shrink-0), body (flex-1 overflow-hidden), footer (shrink-0). The body itself is a flex column — search and select-all shrink, list overflows. */} {/* Header */} {title} {/* Body — fills remaining space, clips overflow */}
{loading && (
)} {/* Search — fixed height */}
setSearch(e.target.value)} />
{/* Select all — fixed height */} {filtered.length > 0 && ( )} {/* Scrollable list — takes all remaining space */}
{!loading && filtered.length === 0 ? (

{search ? `No results for "${search}".` : "No users available."}

) : ( filtered.map((user) => { const id = user[idKey]; const label = user[labelKey]; const sub = subLabelKey ? user[subLabelKey] : null; const warning = warningKey ? user[warningKey] : null; return ( ); }) )}
{/* Footer */}
{movingCount > 0 && (

{movingCount} user{movingCount > 1 ? 's' : ''} already belong{movingCount === 1 ? 's' : ''} to other group{movingCount > 1 ? 's' : ''}.

)}
); }