'use client' import { useEffect, useState } from 'react' import { useTranslations } from 'next-intl' import { Button } from '@/components/ui/button' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog' import { formatCurrency, formatDate } from '@/lib/utils' import { CheckCircle2, AlertTriangle } from 'lucide-react' import type { TransactionWithInvoice } from './transaction-types' interface DuplicateCandidate { journal_entry_id: string voucher_label: string entry_date: string description: string | null amount: number bank_account_number: string reason: 'exact_amount_same_date' | 'exact_amount_within_window' } interface InvoiceMatchDialogProps { open: boolean onOpenChange: (open: boolean) => void transaction: TransactionWithInvoice | null isConfirming: boolean onConfirm: (opts?: { force?: boolean; expected_journal_entry_id?: string }) => void onLinkToExisting?: (journalEntryId: string) => void } export default function InvoiceMatchDialog({ open, onOpenChange, transaction, isConfirming, onConfirm, onLinkToExisting, }: InvoiceMatchDialogProps) { const t = useTranslations('tx_invoice_match') const isSupplierInvoice = !!transaction?.potential_supplier_invoice const isCustomerInvoice = !!transaction?.potential_invoice const transactionId = transaction?.id ?? null // Customer-side only: pre-flight check for a manual verifikation that // already books this receipt. Supplier-side duplicate-payment surfacing // is handled by the mark-paid guard on the supplier-invoice side; here // we only need the customer flow for the reported issue. const [candidate, setCandidate] = useState(null) const [isCheckingDuplicate, setIsCheckingDuplicate] = useState(false) useEffect(() => { if (!open || !transactionId || !isCustomerInvoice || !onLinkToExisting) { setCandidate(null) return } let cancelled = false async function check() { setIsCheckingDuplicate(true) try { const res = await fetch(`/api/transactions/${transactionId}/duplicate-payment-check`) if (!res.ok) return const data = (await res.json()) as { candidate: DuplicateCandidate | null } if (!cancelled) setCandidate(data.candidate ?? null) } catch { // Fail-open: hide the warning panel; the server still enforces the guard. } finally { if (!cancelled) setIsCheckingDuplicate(false) } } check() return () => { cancelled = true } }, [open, transactionId, isCustomerInvoice, onLinkToExisting]) // The invoice candidate the dialog is about, normalized to a single shape. // Supplier invoices show the negative-amount paid-out match; customer // invoices show the positive-amount paid-in match. Each side carries its // own follow-up action language. const matchTitle = isSupplierInvoice ? t('title_supplier') : t('title_customer') const matchDescription = isSupplierInvoice ? t('description_supplier') : t('description_customer') return ( {matchTitle} {matchDescription} {transaction && (isCustomerInvoice || isSupplierInvoice) && (
{/* Duplicate-payment warning — customer-side only, only when a candidate exists */} {candidate && isCustomerInvoice && (

{t('duplicate_title')}

{candidate.reason === 'exact_amount_same_date' ? t('duplicate_body_same_date', { label: candidate.voucher_label, amount: formatCurrency(candidate.amount, transaction.currency), }) : t('duplicate_body_window', { label: candidate.voucher_label, amount: formatCurrency(candidate.amount, transaction.currency), date: formatDate(candidate.entry_date), })}

{candidate.description && ( // Truncate to a short head before render. The // description is free-text and may carry a customer // name or note that's not strictly required to // identify the verifikation (voucher_label + amount + // date already do that). Cap length to keep the // dialog tight and limit incidental PII surfacing // in the rendered DOM. GDPR Art.5(1)(c).

{candidate.description.length > 80 ? `${candidate.description.slice(0, 80).trimEnd()}…` : candidate.description}

)}
{onLinkToExisting && (
)}
)} {/* Transaction details */}

{t('transaction_label')}

{transaction.description}

{formatDate(transaction.date)} 0 ? 'text-success' : ''}`}> {transaction.amount > 0 ? '+' : ''} {formatCurrency(transaction.amount, transaction.currency)}
{/* Invoice details */} {isCustomerInvoice && (

{t('invoice_label')}

{t('invoice_number', { number: transaction.potential_invoice!.invoice_number ?? '' })}

{transaction.potential_invoice!.customer?.name || t('unknown_customer')}

{t('due_date', { date: formatDate(transaction.potential_invoice!.due_date) })} {formatCurrency( transaction.potential_invoice!.total, transaction.potential_invoice!.currency, )}
)} {isSupplierInvoice && (

{t('supplier_invoice_label')}

{t('invoice_number', { number: transaction.potential_supplier_invoice!.supplier_invoice_number ?? '' })}

{t('arrival_number', { number: transaction.potential_supplier_invoice!.arrival_number ?? '' })}

{t('due_date', { date: formatDate(transaction.potential_supplier_invoice!.due_date) })} {formatCurrency( transaction.potential_supplier_invoice!.total, transaction.potential_supplier_invoice!.currency, )}
)} {/* Amount comparison */} {(() => { const txAbs = Math.abs(transaction.amount) const invTotal = isSupplierInvoice ? transaction.potential_supplier_invoice!.remaining_amount ?? transaction.potential_supplier_invoice!.total : transaction.potential_invoice!.total const invCurrency = isSupplierInvoice ? transaction.potential_supplier_invoice!.currency : transaction.potential_invoice!.currency const sameCurrency = transaction.currency === invCurrency const amountsMatch = sameCurrency && Math.abs(txAbs - invTotal) < 0.01 if (amountsMatch) { return (

{t('amounts_match')}

) } const diff = Math.abs(txAbs - invTotal) return (

{t('amounts_differ')}

{t('amount_diff', { amount: formatCurrency(diff, transaction.currency) })} {!sameCurrency && t('different_currencies')} {isSupplierInvoice && diff > 0.01 && sameCurrency && t('partial_payment_note')}

) })()} {/* What will happen */}

{t('on_confirm_title')}

  • • {isSupplierInvoice ? t('on_confirm_link_supplier') : t('on_confirm_link_customer')}
  • • {isSupplierInvoice ? t('on_confirm_mark_paid_supplier') : t('on_confirm_mark_paid_customer')}
  • • {t('on_confirm_voucher')}
)}
) }