import { useEffect, useRef, useState } from "react"; import { ChevronDown, UserCircle, X } from "lucide-react"; import { cn } from "@/lib/utils"; import type { TaskAssignment, ProjectMember } from "@/types"; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function initials(name: string | null | undefined): string { if (!name) return "?"; return name .split(" ") .slice(0, 2) .map((w) => w[0]?.toUpperCase() ?? "") .join(""); } interface AvatarCircleProps { name: string | null | undefined; className?: string; } function AvatarCircle({ name, className }: AvatarCircleProps) { return ( {initials(name)} ); } function roleBadge( userId: number, ownerId: number, permission: ProjectMember["permission"] ): { label: string; className: string } { if (userId === ownerId) return { label: "Owner", className: "text-accent" }; if (permission === "create_modify") return { label: "Editor", className: "text-muted-foreground" }; return { label: "Viewer", className: "text-muted-foreground/60" }; } // --------------------------------------------------------------------------- // AssigneeAvatars — stacked display for TaskRow / KanbanBoard // --------------------------------------------------------------------------- export function AssigneeAvatars({ assignments, max = 3, }: { assignments: TaskAssignment[]; max?: number; }) { const visible = assignments.slice(0, max); const overflow = assignments.length - max; const allNames = assignments.map((a) => a.user_name ?? "Unknown").join(", "); return ( {visible.map((a) => ( {initials(a.user_name)} ))} {overflow > 0 && ( +{overflow} )} ); } // --------------------------------------------------------------------------- // AssignmentPicker // --------------------------------------------------------------------------- interface AssignmentPickerProps { currentAssignments: TaskAssignment[]; members: ProjectMember[]; currentUserId: number; ownerId: number; onAssign: (userIds: number[]) => void; onUnassign: (userId: number) => void; disabled?: boolean; } export function AssignmentPicker({ currentAssignments, members, currentUserId, ownerId, onAssign, onUnassign, disabled = false, }: AssignmentPickerProps) { const [open, setOpen] = useState(false); const containerRef = useRef(null); // Close on outside click useEffect(() => { if (!open) return; function handle(e: MouseEvent) { if ( containerRef.current && !containerRef.current.contains(e.target as Node) ) { setOpen(false); } } document.addEventListener("mousedown", handle); return () => document.removeEventListener("mousedown", handle); }, [open]); const assignedIds = new Set(currentAssignments.map((a) => a.user_id)); // Build ordered member list: current user first, then rest alphabetically const sortedMembers = [...members].sort((a, b) => { if (a.user_id === currentUserId) return -1; if (b.user_id === currentUserId) return 1; return (a.user_name ?? "").localeCompare(b.user_name ?? ""); }); const availableMembers = sortedMembers.filter( (m) => !assignedIds.has(m.user_id) ); function handleSelect(userId: number) { onAssign([userId]); setOpen(false); } function handleRemove(e: React.MouseEvent, userId: number) { e.stopPropagation(); onUnassign(userId); } function handleTriggerClick() { if (disabled) return; setOpen((prev) => !prev); } function handleTriggerKeyDown(e: React.KeyboardEvent) { if (disabled) return; if (e.key === "Enter" || e.key === " ") { e.preventDefault(); setOpen((prev) => !prev); } if (e.key === "Escape") setOpen(false); } const hasAssignees = currentAssignments.length > 0; return ( {/* Trigger area */} {hasAssignees ? ( <> {currentAssignments.map((a) => ( {a.user_id === currentUserId ? "Me" : (a.user_name ?? "Unknown")} {!disabled && ( handleRemove(e, a.user_id)} className="text-muted-foreground hover:text-foreground transition-colors ml-0.5 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring rounded-full" > )} ))} {!disabled && availableMembers.length > 0 && ( )} > ) : ( Assign... )} {/* Dropdown */} {open && availableMembers.length > 0 && ( {availableMembers.map((m) => { const badge = roleBadge(m.user_id, ownerId, m.permission); const displayName = m.user_id === currentUserId ? "Me" : (m.user_name ?? "Unknown"); return ( handleSelect(m.user_id)} className="w-full flex items-center gap-2 px-3 py-1.5 text-sm hover:bg-muted/50 transition-colors text-left focus-visible:outline-none focus-visible:bg-muted/50" > {displayName} {badge.label} ); })} )} {/* Empty state when all members assigned */} {open && availableMembers.length === 0 && ( All members assigned )} ); }
All members assigned