import { Button } from "@/components/ui/button" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command" import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover" import { useCurrentCompany } from "@/hooks/useCurrentCompany" import _ from "@/lib/translate" import { cn } from "@/lib/utils" import { useFrappeGetDocList } from "frappe-react-sdk" import Fuse from "fuse.js" import { ChevronDownIcon } from "lucide-react" import { useLayoutEffect, useMemo, useRef, useState } from "react" import { FormControl } from "../ui/form" export interface AccountsDropdownProps { root_type?: ('Asset' | 'Liability' | 'Equity' | 'Income' | 'Expense')[], report_type?: 'Balance Sheet' | 'Profit and Loss', account_type?: string[], value?: string, onChange?: (value: string) => void, readOnly?: boolean, disabled?: boolean, company?: string, filterFunction?: (account: Account) => boolean, // If true, the component will be wrapped in a FormControl component useInForm?: boolean, buttonClassName?: string, size?: 'sm' | 'md' | 'lg', } /** * Component to select an account - supports fuzzy search * @param root_type - The root type of the account * @param report_type - The report type of the account * @param account_type - The type of the account * @param value - The value of the account field * @param onChange - The function to call when the value changes * @returns */ const AccountsDropdown = ({ root_type, report_type, account_type, value, onChange, readOnly, disabled, company, filterFunction, useInForm, buttonClassName, size = 'md' }: AccountsDropdownProps) => { const { data } = useGetAccounts(root_type, report_type, account_type, company, filterFunction) const groupedAccounts = useMemo(() => { if (!data) return [] const grouped: Record = data.reduce((acc, account) => { const parentAccount = account.parent_account if (!parentAccount) return acc if (!acc[parentAccount]) { acc[parentAccount] = [] } acc[parentAccount].push(account) return acc }, {} as Record) return Object.entries(grouped).map(([parentAccount, accounts]) => ({ // Remove the last abbreviation from the parent account name like "Assets - TCC" should be "Assets", and "Assets - USD - TCC" should be "Assets - USD" parentAccount: parentAccount.split(" - ").slice(0, -1).join(" - "), accounts })) }, [data]) const searchIndex = useMemo(() => { if (!data) { return null } return new Fuse(data, { keys: ['name'], threshold: 0.5, includeScore: true }) }, [data]) const [search, setSearch] = useState("") const recommendedAccounts = useMemo(() => { if (!searchIndex || !search) { return [] } return searchIndex.search(search).map((result) => result.item) }, [searchIndex, search]) const [open, setOpen] = useState(false) const onOpenChange = (open: boolean) => { if (readOnly) return setOpen(open) // setSearch("") } const onSelect = (value: string) => { onChange?.(value) setOpen(false) setSearch(value) } const buttonRef = useRef(null) const [width, setWidth] = useState(320) useLayoutEffect(() => { if (buttonRef.current) { setWidth(buttonRef.current.getBoundingClientRect().width) } }, []) return ( {useInForm ? : } {_("No accounts found.")} {recommendedAccounts.length > 0 && ( {recommendedAccounts.map((account) => ( onSelect(account.name)}>{account.name} ))} )} {!search && groupedAccounts.map((group) => ( {group.accounts.map((account) => ( onSelect(account.name)}>{account.name} ))} ))} ) } interface Account { name: string root_type: 'Asset' | 'Liability' | 'Equity' | 'Income' | 'Expense' report_type: 'Balance Sheet' | 'Profit and Loss' account_type: string account_currency: string parent_account: string } export const useGetAccounts = (root_type?: ('Asset' | 'Liability' | 'Equity' | 'Income' | 'Expense')[], report_type?: 'Balance Sheet' | 'Profit and Loss', account_type?: string[], company?: string, filterFunction?: (account: Account) => boolean) => { const currentCompany = useCurrentCompany() const { data, isLoading, error, mutate } = useFrappeGetDocList("Account", { fields: ["name", "root_type", "report_type", "account_type", "account_currency", "parent_account"], filters: [["is_group", "=", 0], ["disabled", "=", 0], ["company", "=", company ?? currentCompany]], limit: 1000, orderBy: { "field": "root_type", // @ts-expect-error - we can pass in additional fields to orderBy "order": "asc, account_number asc" } }, `accounts-${company ?? currentCompany}`, { revalidateIfStale: false, revalidateOnFocus: false, revalidateOnReconnect: false, }) const filteredData = useMemo(() => { return data?.filter((account) => { if (root_type && !root_type.includes(account.root_type)) return false if (report_type && account.report_type !== report_type) return false if (account_type && !account_type.includes(account.account_type)) return false if (filterFunction) return filterFunction(account) return true }) ?? [] }, [data, root_type, report_type, account_type, filterFunction]) return { data: filteredData, isLoading, error, mutate } } export default AccountsDropdown