import React, { useState, useEffect, useCallback } from 'react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; import { Search, Users, FolderOpen, Check, Loader2 } from 'lucide-react'; import { T, translate } from '@/i18n'; import { ContactGroup, fetchContactGroups, fetchGroupMembers, } from '@/modules/contacts/client-contacts'; // ─── Props ──────────────────────────────────────────────────────────────────── export interface ContactsPickerProps { /** Currently selected group IDs (controlled). */ value?: string[]; /** Called whenever the selection changes. */ onChange?: (groupIds: string[]) => void; /** If provided, wraps the picker in a Dialog triggered by `trigger`. */ trigger?: React.ReactNode; /** Called when the dialog confirms selection. Only used in dialog mode. */ onConfirm?: (groupIds: string[]) => void; /** Disable all interactions. */ disabled?: boolean; /** Placeholder shown when nothing is selected. */ placeholder?: string; } // ─── Component ──────────────────────────────────────────────────────────────── export const ContactsPicker = ({ value, onChange, trigger, onConfirm, disabled, placeholder, }: ContactsPickerProps) => { const [groups, setGroups] = useState([]); const [memberCounts, setMemberCounts] = useState>(new Map()); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); // Dialog mode state const [open, setOpen] = useState(false); const [pendingIds, setPendingIds] = useState>(new Set(value ?? [])); const selectedIds = new Set(value ?? []); const isDialog = !!trigger; const load = useCallback(async () => { setLoading(true); try { const [g, members] = await Promise.all([ fetchContactGroups(), fetchGroupMembers(), ]); setGroups(g); // Count contacts per group const counts = new Map(); for (const m of members) { counts.set(m.group_id, (counts.get(m.group_id) || 0) + 1); } setMemberCounts(counts); } catch (e: any) { toast.error(e.message); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); // Sync pendingIds when value changes externally (dialog mode) useEffect(() => { setPendingIds(new Set(value ?? [])); }, [value]); const toggle = (id: string) => { if (disabled) return; if (isDialog) { setPendingIds(prev => { const next = new Set(prev); next.has(id) ? next.delete(id) : next.add(id); return next; }); } else { const next = new Set(selectedIds); next.has(id) ? next.delete(id) : next.add(id); onChange?.(Array.from(next)); } }; const handleConfirm = () => { const ids = Array.from(pendingIds); onChange?.(ids); onConfirm?.(ids); setOpen(false); }; const filtered = groups.filter(g => { if (!search) return true; return g.name.toLowerCase().includes(search.toLowerCase()); }); const activeSet = isDialog ? pendingIds : selectedIds; // ─── Render list ────────────────────────────────────────────────────────── const listContent = (
{groups.length > 5 && (
setSearch(e.target.value)} />
)} {loading ? (
) : filtered.length === 0 ? (

{groups.length === 0 ? 'No groups yet.' : 'No matching groups.'}

) : (
{filtered.map(g => { const checked = activeSet.has(g.id); const count = memberCounts.get(g.id) || 0; return (
!disabled && toggle(g.id)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(g.id); } }} className={`flex items-center w-full gap-3 px-3 py-2.5 text-sm text-left transition-colors ${checked ? 'bg-primary/5' : 'hover:bg-muted/50'} ${disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`} > {g.name} {count}
); })}
)} {activeSet.size > 0 && (

{activeSet.size} {translate(activeSet.size !== 1 ? 'groups selected' : 'group selected')}

)}
); // ─── Dialog mode ────────────────────────────────────────────────────────── if (isDialog) { return ( <> !disabled && setOpen(true)}>{trigger} Select Groups {listContent} ); } // ─── Inline mode ────────────────────────────────────────────────────────── return listContent; };