'use client' import { useEffect, useMemo, useState } from 'react' import { createClient } from '@/lib/supabase/client' // `createClient()` returns a fresh object on every call, so we keep the // instance creation inside the effect: listing it as a dep would re-fire // the fetch on every render and create an infinite loop. import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog' import { useCompany } from '@/contexts/CompanyContext' import { formatCurrency, formatDate } from '@/lib/utils' import { Loader2, Search, ArrowDownRight } from 'lucide-react' interface BankTransaction { id: string date: string description: string amount: number currency: string } interface BankTransactionPickerProps { open: boolean onOpenChange: (open: boolean) => void /** Invoice total in invoice currency. Used to rank transactions by amount proximity. */ targetAmount: number /** Invoice currency. Used to filter the candidate list when set. */ targetCurrency: string onPick: (transactionId: string) => void } /** * Lightweight picker for unmatched outgoing bank transactions. Used by * `/supplier-invoices/new` when the user wants to register an invoice and * mark it paid against a specific bank transaction in one go. * * Filters: negative amount, no journal entry, no supplier_invoice link. * Ranks by absolute amount-difference vs `targetAmount`. */ export default function BankTransactionPicker({ open, onOpenChange, targetAmount, targetCurrency, onPick, }: BankTransactionPickerProps) { const { company } = useCompany() const [transactions, setTransactions] = useState([]) const [isLoading, setIsLoading] = useState(false) const [searchTerm, setSearchTerm] = useState('') useEffect(() => { if (!open || !company) return let cancelled = false const supabase = createClient() ;(async () => { setIsLoading(true) // No strict currency filter: the bank transaction that pays a // foreign-currency invoice is almost always in the company's domestic // currency (e.g. SEK bank account paying a USD invoice). The user // would see "no matches" if we filtered to the invoice currency. // Cross-currency amount diff is hidden in the row below so the user // isn't shown a misleading "Diff X kr" against a USD target. const { data, error } = await supabase .from('transactions') .select('id, date, description, amount, currency') .eq('company_id', company.id) .lt('amount', 0) .is('journal_entry_id', null) .is('supplier_invoice_id', null) .is('invoice_id', null) .order('date', { ascending: false }) .limit(200) if (!cancelled) { if (!error) setTransactions(data || []) setIsLoading(false) } })() return () => { cancelled = true } }, [open, company]) const filtered = useMemo(() => { const term = searchTerm.trim().toLowerCase() const matches = transactions.filter((t) => term === '' ? true : (t.description || '').toLowerCase().includes(term), ) // Rank by amount proximity only when currencies match: comparing a USD // target to a SEK transaction numerically would produce a meaningless // ranking. Cross-currency rows fall back to date-desc order. return matches.sort((a, b) => { const aSame = a.currency === targetCurrency const bSame = b.currency === targetCurrency if (aSame !== bSame) return aSame ? -1 : 1 if (!aSame) return 0 const da = Math.abs(Math.abs(a.amount) - targetAmount) const db = Math.abs(Math.abs(b.amount) - targetAmount) return da - db }) }, [transactions, searchTerm, targetAmount, targetCurrency]) return ( Välj banktransaktion Välj den utgående banktransaktion som motsvarar denna faktura. Fakturan registreras och markeras som betald i ett steg.
setSearchTerm(e.target.value)} className="pl-10" />
{isLoading ? (
Laddar transaktioner…
) : filtered.length === 0 ? (
{transactions.length === 0 ? 'Inga okategoriserade utgifts­transaktioner hittades.' : `Inga transaktioner matchar "${searchTerm}".`}
) : ( filtered.map((tx) => { const sameCurrency = tx.currency === targetCurrency const absAmount = Math.abs(tx.amount) const diff = sameCurrency ? Math.abs(absAmount - targetAmount) : null const isExact = diff != null && diff < 0.01 return ( ) }) )}
) }