'use client'
import { useState, useEffect, useMemo } 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 { Skeleton } from '@/components/ui/skeleton'
import { Input } from '@/components/ui/input'
import { useToast } from '@/components/ui/use-toast'
import { cn, formatCurrency, formatDate } from '@/lib/utils'
import { FileText, ImageIcon, Loader2, Search, Inbox, Eye } from 'lucide-react'
// InboxDocumentPicker
//
// Lists invoice-inbox documents that have not yet been consumed (no supplier
// invoice, no journal entry, not matched to a transaction, document not
// already linked) so the user can attach one as underlag to a verifikat.
//
// Two modes:
// - Link mode (JournalEntryAttachments, "Välj från inkorgen"): `journalEntryId`
// is set and picking a document immediately links it to the journal entry AND
// stamps the inbox item so it drops out of the active inbox: see
// app/api/documents/[id]/link/route.ts.
// - Select mode (TransactionBookingDialog, "Välj befintligt underlag"): the
// journal entry does not exist yet, so `onSelect` is provided instead and the
// pick is reported to the parent, which links after the entry is created.
//
// Each row carries a preview button (eye) that opens a quick dialog rendering
// the document inline, so the user can confirm the right file before attaching.
// Attaching is the row's primary click (fast path) and is also offered from
// inside the preview dialog (preview → confirm).
export interface AvailableInboxDoc {
inbox_item_id: string
document_id: string
file_name: string
mime_type: string | null
file_size_bytes: number
source: string | null
created_at: string
supplier_name: string | null
amount: number | null
currency: string | null
invoice_date: string | null
}
interface Props {
open: boolean
onClose: () => void
/** Link mode: the journal entry to link the picked document to. */
journalEntryId?: string
/** Link mode: called after a successful link so the parent can refresh its document list. */
onLinked?: () => void
/** Select mode: report the picked document to the parent instead of linking. */
onSelect?: (doc: AvailableInboxDoc) => void
}
function isImageType(type: string | null): boolean {
return type?.startsWith('image/') ?? false
}
function isPdfType(type: string | null): boolean {
return type === 'application/pdf'
}
function DocIcon({ mime }: { mime: string | null }) {
if (isImageType(mime)) {
return
}
return
}
export default function InboxDocumentPicker({ open, onClose, journalEntryId, onLinked, onSelect }: Props) {
const t = useTranslations('journal_attachments')
const { toast } = useToast()
const [loading, setLoading] = useState(true)
const [items, setItems] = useState([])
const [search, setSearch] = useState('')
const [linkingId, setLinkingId] = useState(null)
const [previewItem, setPreviewItem] = useState(null)
// Reset + fetch each time the dialog opens.
useEffect(() => {
if (!open) return
setSearch('')
setItems([])
setPreviewItem(null)
setLoading(true)
let cancelled = false
;(async () => {
try {
const res = await fetch('/api/documents/inbox-available')
const json = (await res.json().catch(() => ({}))) as { data?: AvailableInboxDoc[] }
if (cancelled) return
setItems(json.data ?? [])
} catch {
if (!cancelled) setItems([])
} finally {
if (!cancelled) setLoading(false)
}
})()
return () => {
cancelled = true
}
}, [open])
const filtered = useMemo(() => {
const q = search.trim().toLowerCase()
if (!q) return items
return items.filter((it) =>
`${it.supplier_name ?? ''} ${it.file_name}`.toLowerCase().includes(q),
)
}, [items, search])
async function handlePick(item: AvailableInboxDoc) {
// Select mode: the journal entry doesn't exist yet: hand the pick to the
// parent and let it link after creation. Clear the preview first: the
// preview dialog's open state is `previewItem !== null`, so leaving it set
// would strand a floating preview after the picker closes (the component
// stays mounted; the on-open reset only runs on the next open).
if (onSelect) {
setPreviewItem(null)
onSelect(item)
onClose()
return
}
if (!journalEntryId) return
setLinkingId(item.document_id)
try {
const res = await fetch(`/api/documents/${item.document_id}/link`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
journal_entry_id: journalEntryId,
inbox_item_id: item.inbox_item_id,
}),
})
if (!res.ok) {
const json = (await res.json().catch(() => ({}))) as {
error?: string | { message?: string }
}
const description =
typeof json.error === 'string' ? json.error : json.error?.message
toast({ title: t('picker_link_failed'), description, variant: 'destructive' })
return
}
toast({ title: t('picker_linked') })
setPreviewItem(null)
onLinked?.()
onClose()
} catch {
toast({ title: t('picker_link_failed'), variant: 'destructive' })
} finally {
setLinkingId(null)
}
}
const hasSearch = search.trim().length > 0
const previewSrc = previewItem ? `/api/documents/${previewItem.document_id}/inline` : null
return (
<>
>
)
}