'use client' import { useState, useRef, useEffect, useMemo, useCallback } from 'react' import { Input } from '@/components/ui/input' import { getAccountClassName } from '@/lib/bookkeeping/account-descriptions' import type { BASAccount } from '@/types' interface AccountComboboxProps { value: string accounts: BASAccount[] onChange: (accountNumber: string) => void } const MAX_RESULTS = 50 export default function AccountCombobox({ value, accounts, onChange }: AccountComboboxProps) { const [search, setSearch] = useState(value) const [isOpen, setIsOpen] = useState(false) const [highlightedIndex, setHighlightedIndex] = useState(0) const containerRef = useRef(null) const inputRef = useRef(null) const listRef = useRef(null) // Sync external value changes into the search field useEffect(() => { setSearch(value) }, [value]) // Filter accounts based on search input const filteredAccounts = useMemo(() => { if (!search) return accounts.slice(0, MAX_RESULTS) const trimmed = search.trim() if (!trimmed) return accounts.slice(0, MAX_RESULTS) const startsWithDigit = /^\d/.test(trimmed) if (startsWithDigit) { return accounts .filter((a) => a.account_number.startsWith(trimmed)) .slice(0, MAX_RESULTS) } const lowerSearch = trimmed.toLowerCase() return accounts .filter((a) => a.account_name.toLowerCase().includes(lowerSearch)) .slice(0, MAX_RESULTS) }, [accounts, search]) // Group filtered accounts by class const groupedAccounts = useMemo(() => { const groups: { className: string; accounts: BASAccount[] }[] = [] const groupMap = new Map() for (const account of filteredAccounts) { const className = getAccountClassName(account.account_class) if (!groupMap.has(className)) { groupMap.set(className, []) } groupMap.get(className)!.push(account) } for (const [className, accts] of groupMap) { groups.push({ className, accounts: accts }) } return groups }, [filteredAccounts]) // Flat list for keyboard navigation const flatList = useMemo(() => filteredAccounts, [filteredAccounts]) // Reset highlight when filtered results change useEffect(() => { setHighlightedIndex(0) }, [filteredAccounts]) // Scroll highlighted item into view useEffect(() => { if (!isOpen || !listRef.current) return const highlighted = listRef.current.querySelector('[data-highlighted="true"]') if (highlighted) { highlighted.scrollIntoView({ block: 'nearest' }) } }, [highlightedIndex, isOpen]) // Close dropdown when clicking outside useEffect(() => { function handleClickOutside(e: MouseEvent) { if (containerRef.current && !containerRef.current.contains(e.target as Node)) { setIsOpen(false) } } document.addEventListener('mousedown', handleClickOutside) return () => document.removeEventListener('mousedown', handleClickOutside) }, []) const selectAccount = useCallback( (accountNumber: string) => { onChange(accountNumber) setSearch(accountNumber) setIsOpen(false) inputRef.current?.blur() }, [onChange] ) const handleKeyDown = (e: React.KeyboardEvent) => { if (!isOpen) { if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { setIsOpen(true) e.preventDefault() } return } switch (e.key) { case 'ArrowDown': e.preventDefault() setHighlightedIndex((prev) => Math.min(prev + 1, flatList.length - 1)) break case 'ArrowUp': e.preventDefault() setHighlightedIndex((prev) => Math.max(prev - 1, 0)) break case 'Enter': e.preventDefault() if (flatList[highlightedIndex]) { selectAccount(flatList[highlightedIndex].account_number) } break case 'Escape': e.preventDefault() setIsOpen(false) break } } const handleInputChange = (e: React.ChangeEvent) => { const newValue = e.target.value setSearch(newValue) // Only emit valid account numbers to parent if (/^\d{4}$/.test(newValue) && accounts.some(a => a.account_number === newValue)) { onChange(newValue) } if (!isOpen) { setIsOpen(true) } } const handleFocus = () => { setIsOpen(true) } const handleBlur = () => { // Small delay to allow dropdown click to fire first setTimeout(() => { if (!accounts.some(a => a.account_number === search)) { setSearch(value) } }, 150) } return (
{/* Dropdown */} {isOpen && flatList.length > 0 && (
{groupedAccounts.map((group) => (
{group.className}
{group.accounts.map((account) => { const flatIndex = flatList.indexOf(account) const isHighlighted = flatIndex === highlightedIndex return ( ) })}
))}
)} {/* Empty state */} {isOpen && search.trim() && flatList.length === 0 && (

Hittade inget konto som matchar.

Kontot kan behöva aktiveras i din kontoplan.

)}
) }