'use client' import { useState } from 'react' import { useTranslations } from 'next-intl' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { useToast } from '@/components/ui/use-toast' import { formatCurrency, formatDate } from '@/lib/utils' import { ArrowUpRight, ArrowDownRight, FileText, Inbox, Loader2, X } from 'lucide-react' import DocumentUploadZone from '@/components/bookkeeping/DocumentUploadZone' import type { UploadedFile } from '@/components/bookkeeping/DocumentUploadZone' import InboxDocumentPicker from '@/components/bookkeeping/InboxDocumentPicker' import type { AvailableInboxDoc } from '@/components/bookkeeping/InboxDocumentPicker' import type { TransactionWithInvoice } from './transaction-types' interface TransactionAttachDocumentDialogProps { open: boolean onOpenChange: (open: boolean) => void transaction: TransactionWithInvoice | null onAttached: (transactionId: string, documentId: string) => void } /** * Standalone "Matcha mot underlag" dialog for the /transactions view: the * mirror of the Documents view's TransactionMatchPicker (doc → tx direction). * Pick an unconsumed inbox document or upload a new file, then pin it to the * transaction via POST /api/transactions/[id]/attach-document. The pin is * single-valued (transactions.document_id); for booked rows the route * propagates the link onto the verifikation immediately. */ export default function TransactionAttachDocumentDialog({ open, onOpenChange, transaction, onAttached, }: TransactionAttachDocumentDialogProps) { const t = useTranslations('tx_attach_dialog') const { toast } = useToast() const [uploadedFiles, setUploadedFiles] = useState([]) const [pickedDoc, setPickedDoc] = useState(null) const [inboxPickerOpen, setInboxPickerOpen] = useState(false) const [isAttaching, setIsAttaching] = useState(false) if (!transaction) return null const isIncome = transaction.amount > 0 // Single selection: transactions.document_id pins exactly one doc, so an // inbox pick replaces any upload and vice versa. const selectedDocumentId = pickedDoc?.document_id ?? uploadedFiles.find((f) => f.status === 'uploaded' && f.id)?.id ?? null const reset = () => { setUploadedFiles([]) setPickedDoc(null) setInboxPickerOpen(false) } const handleAttach = async () => { if (!selectedDocumentId || isAttaching) return setIsAttaching(true) try { const res = await fetch(`/api/transactions/${transaction.id}/attach-document`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ document_id: selectedDocumentId }), }) if (!res.ok) { const json = (await res.json().catch(() => ({}))) as { error?: unknown } // The route returns Swedish domain messages (immutability, locked // period) as a plain string: surface them verbatim. toast({ title: t('error_toast'), description: typeof json.error === 'string' ? json.error : undefined, variant: 'destructive', }) return } toast({ title: t('success_toast') }) // Same event AgentChat and the booking dialog dispatch: flips the inbox // card's indicator optimistically without a refetch. window.dispatchEvent( new CustomEvent('Accounted:transaction-document-linked', { detail: { transaction_id: transaction.id, document_id: selectedDocumentId }, }), ) onAttached(transaction.id, selectedDocumentId) reset() onOpenChange(false) } catch { // Network-level failure: fetch rejected before a response existed. toast({ title: t('error_toast'), variant: 'destructive' }) } finally { setIsAttaching(false) } } return ( { if (!o) reset() onOpenChange(o) }} > {t('title')} {t('description')} {/* Transaction summary: same block as TransactionBookingDialog */}
{isIncome ? ( ) : ( )}

{transaction.description}

{formatDate(transaction.date)}

{isIncome ? '+' : ''} {formatCurrency(transaction.amount, transaction.currency)}

{transaction.document_id && (

{t('already_attached_hint')}

)}
{ setUploadedFiles(files) if (files.length > 0) setPickedDoc(null) }} maxFiles={1} compact disabled={isAttaching} /> {pickedDoc && (
{pickedDoc.supplier_name ?? pickedDoc.file_name} {pickedDoc.amount != null && ( {formatCurrency(pickedDoc.amount, pickedDoc.currency ?? 'SEK')} )}
)}
setInboxPickerOpen(false)} onSelect={(doc) => { setPickedDoc(doc) setUploadedFiles([]) }} />
) }