import { useState, useCallback, useMemo } from 'react'; import { Text, Loader, Group, Badge, TextInput, UnstyledButton, Collapse, Stack, Box, rem, Center, useComputedColorScheme, } from '@mantine/core'; import { IconChevronRight, IconChevronDown, IconSearch, IconMapPin, } from '@tabler/icons-react'; import { useGetLocationsQuery } from '../api/location-api'; import type { Location } from '../types/location'; import { useTranslation } from 'react-i18next'; import { useLocalized } from '@ema-platform/api'; import { PageLoader } from '@ema-platform/ui'; interface LocationTreeProps { selectedId: string | null; onSelect: (location: Location) => void; } function flatListToTree( items: Location[], parentId: string | null, ): Location[] { return items .filter((item) => item.parentId === parentId) .map((item) => ({ ...item, children: flatListToTree(items, item.id), })); } function TreeNode({ location, selectedId, onSelect, depth, }: { location: Location; selectedId: string | null; onSelect: (location: Location) => void; depth: number; }) { const localized = useLocalized(); const [opened, setOpened] = useState(depth < 1); const isSelected = selectedId === location.id; const hasChildren = Array.isArray(location.children) && location.children.length > 0; const colorScheme = useComputedColorScheme('light', { getInitialValueInEffect: true }); const hoverBg = colorScheme === 'dark' ? 'var(--mantine-color-dark-6)' : 'var(--mantine-color-gray-0)'; const toggle = useCallback(() => { setOpened((prev) => !prev); }, []); const select = useCallback(() => { onSelect(location); }, [onSelect, location]); return ( { if (!isSelected) e.currentTarget.style.backgroundColor = hoverBg; }} onMouseLeave={(e) => { if (!isSelected) e.currentTarget.style.backgroundColor = 'transparent'; }} > {hasChildren && ( { e.stopPropagation(); toggle(); }} style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', width: rem(18), height: rem(18), border: 'none', background: 'none', cursor: 'pointer', padding: 0, flexShrink: 0, }} > {opened ? ( ) : ( )} )} {!hasChildren && } {localized(location.names)} {hasChildren && ( {location.children!.map((child) => ( ))} )} ); } export function LocationTree({ selectedId, onSelect, }: LocationTreeProps) { const { t } = useTranslation(); const [search, setSearch] = useState(''); const { data: allLocationsRes, isLoading } = useGetLocationsQuery({ take: 10000 }); const tree = useMemo(() => { if (!allLocationsRes?.items) return []; const roots = flatListToTree(allLocationsRes.items, null); return roots; }, [allLocationsRes]); const filteredTree = useMemo(() => { if (!search) return tree; const matches = (loc: Location): boolean => { const nameMatch = (loc.names.en ?? '') .toLowerCase() .includes(search.toLowerCase()); const childMatch = Array.isArray(loc.children) && loc.children.some(matches); return nameMatch || childMatch; }; const filterNodes = (nodes: Location[]): Location[] => nodes .filter(matches) .map((node) => ({ ...node, children: Array.isArray(node.children) ? filterNodes(node.children) : [], })); return filterNodes(tree); }, [tree, search]); const handleSelect = useCallback( (loc: Location) => { onSelect(loc); }, [onSelect], ); if (isLoading) { return ; } return ( <> } value={search} onChange={(e) => setSearch(e.currentTarget.value)} mb="sm" size="sm" /> {tree.length === 0 && ( {t('location.noLocations')} )} {filteredTree.map((root) => ( ))} ); }