import { CategoryIcon } from '@/components/shared/category-combobox'; import { Command, CommandGroup, CommandInput, CommandItem, CommandList, } from '@/components/ui/command'; import { Kbd } from '@/components/ui/kbd'; import { type AnimationState } from '@/hooks/use-categorize-transactions'; import { buildCategoryTree, flattenCategoryTree, getCategoryPath, } from '@/lib/category-tree'; import { cn } from '@/lib/utils'; import { type Category, getCategoryColorClasses } from '@/types/category'; import { type DecryptedTransaction } from '@/types/transaction'; import { __ } from '@/utils/i18n'; import { ArrowDown, ArrowUp } from 'lucide-react'; import { type RefObject, useMemo } from 'react'; interface CategorizerCommandProps { sortedCategories: Category[]; animationState: AnimationState; currentTransaction: DecryptedTransaction | undefined; searchValue: string; onSearchChange: (value: string) => void; onCategorySelect: (category: Category) => void; commandInputRef: RefObject; disabled?: boolean; } export function CategorizerCommand({ sortedCategories, animationState, currentTransaction, searchValue, onSearchChange, onCategorySelect, commandInputRef, disabled = false, }: CategorizerCommandProps) { const treeCategories = useMemo( () => flattenCategoryTree(buildCategoryTree(sortedCategories)), [sortedCategories], ); const query = searchValue.trim().toLowerCase(); // Tree-aware search: keep each match together with its ancestors so a // matching child still shows its parent chain for context. const visibleCategories = useMemo(() => { if (!query) { return treeCategories; } const parentOf = new Map( sortedCategories.map((c) => [c.id, c.parent_id]), ); const include = new Set(); for (const category of sortedCategories) { if (!category.name.toLowerCase().includes(query)) { continue; } let id: string | null | undefined = category.id; let guard = 0; while (id != null && guard++ < 10) { include.add(id); id = parentOf.get(id); } } return treeCategories.filter((node) => include.has(node.id)); }, [treeCategories, sortedCategories, query]); if (animationState === 'success' || !currentTransaction) { return null; } return (

{__('Assign a new category')}

{__('Search, move')}{' '} {__(', and press')}

{visibleCategories.length === 0 && (
{__('No categories found.')}
)} {visibleCategories.map((category) => { const colorClasses = getCategoryColorClasses( category.color, ); return ( onCategorySelect(category)} disabled={ animationState !== 'idle' || disabled } className="group cursor-pointer gap-3 p-2" style={{ paddingLeft: `${0.5 + category.depth * 1.25}rem`, }} >
{category.name}
); })}
); }