import { useEffect, useMemo, useRef, useState } from 'react' import { Minus, Plus, Search, X } from 'lucide-react' import { useLocale } from '@meshkee/dashboard-ui' import { useT } from '../i18n/useT' import { Tooltip } from './Tooltip' import type { CategoryTreeItem } from './CategoryTreeSelect' import treeStyles from './CategoryTree.module.css' import selectStyles from './CategoryTreeSelect.module.css' import styles from './CategoryTreeMultiSelect.module.css' interface CategoryTreeMultiSelectProps { categories: CategoryTreeItem[] selectedIds: string[] onToggle: (id: string) => void placeholder?: string disabled?: boolean id?: string } function getChildren(categories: CategoryTreeItem[], parentId: string | null) { return categories .filter((category) => category.parentId === parentId) .sort((a, b) => a.nameEn.localeCompare(b.nameEn)) } function getAncestorIds(categories: CategoryTreeItem[], id: string) { const byId = new Map(categories.map((category) => [category.id, category])) const ids: string[] = [] let current = byId.get(id) while (current?.parentId) { ids.push(current.parentId) current = byId.get(current.parentId) } return ids } function categoryMatches(category: CategoryTreeItem, query: string) { const q = query.trim().toLowerCase() if (!q) return true return ( category.nameEn.toLowerCase().includes(q) || category.nameFa.toLowerCase().includes(q) || category.nameFa.includes(query.trim()) ) } function visibleIdsForQuery( categories: CategoryTreeItem[], query: string, ): Set | null { const q = query.trim() if (!q) return null const matched = categories.filter((category) => categoryMatches(category, q)) const visible = new Set() for (const category of matched) { visible.add(category.id) for (const ancestorId of getAncestorIds(categories, category.id)) { visible.add(ancestorId) } } return visible } function CategoryNamePair({ category }: { category: CategoryTreeItem }) { const nameEn = category.nameEn?.trim() || '' const nameFa = category.nameFa?.trim() || '' return ( {nameEn ? ( {nameEn} ) : null} {nameEn && nameFa ? · : null} {nameFa ? ( {nameFa} ) : null} ) } function TreeNodes({ categories, parentId, selectedIds, expandedIds, visibleIds, forceExpanded, disabled, onToggleExpand, onToggleSelect, }: { categories: CategoryTreeItem[] parentId: string | null selectedIds: Set expandedIds: Set visibleIds: Set | null forceExpanded: boolean disabled: boolean onToggleExpand: (id: string) => void onToggleSelect: (id: string) => void }) { const t = useT() const nodes = getChildren(categories, parentId).filter( (category) => !visibleIds || visibleIds.has(category.id), ) return ( <> {nodes.map((category) => { const childCount = getChildren(categories, category.id).some( (child) => !visibleIds || visibleIds.has(child.id), ) const expanded = forceExpanded || expandedIds.has(category.id) const selected = selectedIds.has(category.id) return (
{childCount ? ( ) : ( )}
{childCount ? (
) : null}
) })} ) } export function CategoryTreeMultiSelect({ categories, selectedIds, onToggle, placeholder, disabled = false, id, }: CategoryTreeMultiSelectProps) { const t = useT() const { locale } = useLocale() const isFa = locale === 'fa' const containerRef = useRef(null) const [open, setOpen] = useState(false) const [query, setQuery] = useState('') const [expandedIds, setExpandedIds] = useState>(() => new Set()) const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]) const searchPlaceholder = placeholder ?? t('categories.select.search') const visibleIds = useMemo( () => visibleIdsForQuery(categories, query), [categories, query], ) const selectedCategories = useMemo( () => selectedIds .map((id) => categories.find((category) => category.id === id)) .filter((category): category is CategoryTreeItem => Boolean(category)), [categories, selectedIds], ) useEffect(() => { setExpandedIds((prev) => { const next = new Set(prev) for (const id of selectedIds) { for (const ancestorId of getAncestorIds(categories, id)) { next.add(ancestorId) } } return next }) }, [categories, selectedIds]) useEffect(() => { if (!open) return function onClickOutside(e: MouseEvent) { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setOpen(false) setQuery('') } } document.addEventListener('mousedown', onClickOutside) return () => document.removeEventListener('mousedown', onClickOutside) }, [open]) function handleToggleExpand(toggleId: string) { setExpandedIds((prev) => { const next = new Set(prev) if (next.has(toggleId)) next.delete(toggleId) else next.add(toggleId) return next }) } const searching = query.trim().length > 0 const hasVisible = searching ? (visibleIds?.size ?? 0) > 0 : categories.length > 0 return (
{ setQuery(e.target.value) if (!open) setOpen(true) }} onFocus={() => { if (!disabled) setOpen(true) }} onClick={() => { if (!disabled) setOpen(true) }} autoComplete="off" role="combobox" aria-expanded={open} aria-autocomplete="list" /> {query ? ( ) : null}
{open && !disabled ? (
{!hasVisible ? (

{t('categories.select.empty')}

) : ( )}
) : null} {selectedCategories.length > 0 ? (
{selectedCategories.map((category) => (
))}
) : null}
) }