'use client' import { useState, useEffect, useMemo } from 'react' import { createClient } from '@/lib/supabase/client' import { Input } from '@/components/ui/input' import { formatCurrency, formatDate, cn } from '@/lib/utils' import { Search, FileText, Loader2 } from 'lucide-react' import { useCompany } from '@/contexts/CompanyContext' import type { SupplierInvoice, Supplier } from '@/types' import type { TransactionWithInvoice } from './transaction-types' import { DOMESTIC_CURRENCY, normalizeCurrency, rankInvoicesByAmountProximity, } from './invoice-candidate-ranking' type OpenSupplierInvoice = SupplierInvoice & { supplier?: Supplier } interface SupplierInvoicePickerProps { transaction: TransactionWithInvoice onSelect: (invoice: OpenSupplierInvoice) => void isProcessing: boolean } export default function SupplierInvoicePicker({ transaction, onSelect, isProcessing, }: SupplierInvoicePickerProps) { const { company } = useCompany() const supabase = useMemo(() => createClient(), []) const [invoices, setInvoices] = useState([]) const [isLoading, setIsLoading] = useState(true) const [search, setSearch] = useState('') useEffect(() => { if (!company) return const companyId = company.id let cancelled = false async function load() { setIsLoading(true) // Status filter mirrors match-supplier-invoice route expectations: only // approved/overdue/partially_paid invoices can take a payment. Registered // invoices haven't passed the approval gate yet; paid/credited/reversed // are terminal. const { data } = await supabase .from('supplier_invoices') .select('*, supplier:suppliers(*)') .eq('company_id', companyId) .in('status', ['approved', 'overdue', 'partially_paid']) .gt('remaining_amount', 0) .order('invoice_date', { ascending: false }) .limit(200) if (cancelled) return const all = ((data as OpenSupplierInvoice[]) || []) // Status-leak guard: if a supplier invoice still says 'approved'/'overdue' // but already has a payment voucher attached, hide it. Partially-paid // invoices intentionally pass through: they may take more payments. // Mirrors the customer-side guard in InvoicePicker. const fullIds = all .filter((inv) => inv.status === 'approved' || inv.status === 'overdue') .map((inv) => inv.id) let visible = all if (fullIds.length > 0) { const { data: paid } = await supabase .from('supplier_invoice_payments') .select('supplier_invoice_id') .eq('company_id', companyId) .in('supplier_invoice_id', fullIds) .not('journal_entry_id', 'is', null) if (cancelled) return const paidSet = new Set( ((paid as { supplier_invoice_id: string }[] | null) ?? []).map( (r) => r.supplier_invoice_id, ), ) visible = all.filter((inv) => !paidSet.has(inv.id)) } setInvoices(visible) setIsLoading(false) } load() return () => { cancelled = true } }, [company, supabase]) const sorted = useMemo(() => { const filtered = !search ? invoices : invoices.filter((inv) => { const q = search.toLowerCase() return ( (inv.supplier_invoice_number ?? '').toLowerCase().includes(q) || (inv.supplier?.name ?? '').toLowerCase().includes(q) ) }) // Amount proximity is only meaningful between comparable amounts: ranking // a 1 000 EUR invoice as a perfect hit for a 1 000 SEK payment put the // wrong row first. Foreign invoices stay in the list either way; see // ./invoice-candidate-ranking. return rankInvoicesByAmountProximity(filtered, { amount: transaction.amount, currency: transaction.currency, amountSek: transaction.amount_sek, }) }, [invoices, search, transaction.amount, transaction.currency, transaction.amount_sek]) if (isLoading) { return (
Laddar leverantörsfakturor...
) } if (invoices.length === 0) { return (

Inga öppna leverantörsfakturor att matcha mot.

) } return (
setSearch(e.target.value)} className="pl-9" autoFocus />
{sorted.map(({ invoice, proximity }) => { const remaining = invoice.remaining_amount ?? invoice.total const { exact, close, candidateSek } = proximity const invoiceCurrency = normalizeCurrency(invoice.currency) // The currency earns a marker only when it deviates from the bank // row's: the same marker on every row would say nothing (design.md, // "chips mark exceptions"). It is what explains why a row is or is // not ranked as close. const foreignCurrency = proximity.basis !== 'same_currency' return ( ) })} {sorted.length === 0 && (

Ingen faktura matchar "{search}"

)}
) }