diff --git a/app/(dashboard)/settings/page.tsx b/app/(dashboard)/settings/page.tsx index a78c6096..55d592e3 100644 --- a/app/(dashboard)/settings/page.tsx +++ b/app/(dashboard)/settings/page.tsx @@ -34,6 +34,7 @@ import { import { useTheme } from 'next-themes' import type { CompanySettings } from '@/types' import { validateBankgiroNumber, formatBankgiroNumber } from '@/lib/bankgiro/luhn' +import { BankNameCombobox } from '@/components/settings/BankNameCombobox' import { CalendarFeedSettings } from '@/components/settings/CalendarFeedSettings' import { getSettingsPanel } from '@/lib/extensions/settings-panel-registry' import { SecuritySettings } from '@/components/settings/SecuritySettings' @@ -420,13 +421,10 @@ export default function SettingsPage() {
- - Bank +
diff --git a/components/settings/BankNameCombobox.tsx b/components/settings/BankNameCombobox.tsx new file mode 100644 index 00000000..f28b30fe --- /dev/null +++ b/components/settings/BankNameCombobox.tsx @@ -0,0 +1,191 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' +import { Input } from '@/components/ui/input' +import { cn } from '@/lib/utils' + +interface BankOption { + name: string + logo?: string + bic?: string +} + +const FALLBACK_BANKS: BankOption[] = [ + { name: 'Nordea', bic: 'NDEASESS' }, + { name: 'SEB', bic: 'ESSESESS' }, + { name: 'Swedbank', bic: 'SWEDSESS' }, + { name: 'Handelsbanken', bic: 'HANDSESS' }, + { name: 'Danske Bank', bic: 'DABASES' }, + { name: 'Länsförsäkringar', bic: 'ELLFSESS' }, + { name: 'Skandiabanken', bic: 'SKIASESS' }, + { name: 'ICA Banken' }, + { name: 'Avanza Bank' }, + { name: 'Sparbanken' }, +] + +interface BankNameComboboxProps { + defaultValue?: string + enableBankingEnabled?: boolean +} + +export function BankNameCombobox({ defaultValue = '', enableBankingEnabled = false }: BankNameComboboxProps) { + const [value, setValue] = useState(defaultValue) + const [banks, setBanks] = useState(FALLBACK_BANKS) + const [isOpen, setIsOpen] = useState(false) + const [highlightedIndex, setHighlightedIndex] = useState(-1) + const containerRef = useRef(null) + const inputRef = useRef(null) + const listRef = useRef(null) + + useEffect(() => { + if (!enableBankingEnabled) return + + let cancelled = false + async function fetchBanks() { + try { + const res = await fetch('/api/extensions/ext/enable-banking/banks') + if (!res.ok) return + const data = await res.json() + if (!cancelled && data.banks?.length > 0) { + setBanks(data.banks) + } + } catch { + // Keep fallback list + } + } + fetchBanks() + return () => { cancelled = true } + }, [enableBankingEnabled]) + + const filtered = value.trim() + ? banks.filter((b) => b.name.toLowerCase().includes(value.toLowerCase())) + : banks + + useEffect(() => { + setHighlightedIndex(-1) + }, [value]) + + useEffect(() => { + if (!isOpen) return + 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) + }, [isOpen]) + + // Scroll highlighted item into view + useEffect(() => { + if (highlightedIndex < 0 || !listRef.current) return + const item = listRef.current.children[highlightedIndex] as HTMLElement + item?.scrollIntoView({ block: 'nearest' }) + }, [highlightedIndex]) + + function selectBank(bank: BankOption) { + setValue(bank.name) + setIsOpen(false) + inputRef.current?.focus() + } + + function handleKeyDown(e: React.KeyboardEvent) { + if (!isOpen && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) { + setIsOpen(true) + e.preventDefault() + return + } + + if (!isOpen) return + + switch (e.key) { + case 'ArrowDown': + e.preventDefault() + setHighlightedIndex((i) => (i < filtered.length - 1 ? i + 1 : 0)) + break + case 'ArrowUp': + e.preventDefault() + setHighlightedIndex((i) => (i > 0 ? i - 1 : filtered.length - 1)) + break + case 'Enter': + e.preventDefault() + if (highlightedIndex >= 0 && filtered[highlightedIndex]) { + selectBank(filtered[highlightedIndex]) + } + break + case 'Escape': + setIsOpen(false) + break + } + } + + return ( +
+ + { + setValue(e.target.value) + if (!isOpen) setIsOpen(true) + }} + onFocus={() => setIsOpen(true)} + onKeyDown={handleKeyDown} + role="combobox" + aria-expanded={isOpen} + aria-autocomplete="list" + aria-controls="bank-name-listbox" + autoComplete="off" + /> + {isOpen && filtered.length > 0 && ( +
    + {filtered.map((bank, i) => ( +
  • setHighlightedIndex(i)} + onMouseDown={(e) => { + e.preventDefault() // prevent blur before click registers + selectBank(bank) + }} + > + {bank.logo ? ( + + ) : ( + + + + )} + {bank.name} +
  • + ))} +
+ )} +
+ ) +}