'use client' import { useState } from 'react' import { useTranslations } from 'next-intl' import Link from 'next/link' import { Paperclip, Loader2 } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { cn } from '@/lib/utils' import { useToast } from '@/components/ui/use-toast' import { openDeferredTab } from '@/lib/browser/deferred-tab' import { useBranding } from '@/lib/branding/brand-context' interface Props { documentId: string | null | undefined /** The booked tx's journal entry: link target when the underlag lives only * at verifikat level (multi-doc entries, booking-dialog uploads). */ journalEntryId?: string | null /** Underlag exists on the journal entry even though no doc is pinned to the * transaction row (computeJeUnderlagStatus === 'has'). */ hasJeDoc?: boolean /** Booked, requires underlag, has none (computeJeUnderlagStatus === 'missing'). */ missing?: boolean /** Opens the attach dialog from the negative state. Omit for viewers: * the badge then renders non-interactive. */ onAttach?: () => void className?: string } const badgeClass = 'h-4 gap-1 px-1.5 py-0 text-[10px] font-normal' // Enlarges the hit area beyond the 16px badge without shifting layout. const hitAreaClass = 'shrink-0 p-1 -m-1' /** * Per-row underlag status for the /transactions lists. * * - Pinned doc (transactions.document_id): clickable badge that fetches a * signed URL and opens the document in a new tab. * - Verifikat-level doc only: same badge, links to the verifikat page (which * lists all attachments: handles multi-doc without a per-row fetch). * - Missing on a booked row: discreet outline badge that doubles as the * attach affordance when onAttach is provided. */ export function TransactionAttachmentIndicator({ documentId, journalEntryId, hasJeDoc, missing, onAttach, className, }: Props) { const t = useTranslations('tx_underlag') const tCommon = useTranslations('common') const { appName } = useBranding() const { toast } = useToast() const [isLoading, setIsLoading] = useState(false) const handleOpen = async (e: React.MouseEvent) => { e.stopPropagation() e.preventDefault() if (isLoading || !documentId) return setIsLoading(true) // Pre-open inside the click's user activation: a window.open after the // await is popup-blocked when the signed-URL fetch is slow. const tab = openDeferredTab(tCommon('loading')) try { const res = await fetch(`/api/documents/${documentId}`) if (!res.ok) { tab.close() toast({ title: t('open_failed'), variant: 'destructive' }) return } const { data } = await res.json() if (!data?.download_url || !tab.navigate(data.download_url)) { tab.close() toast({ title: t('open_failed'), description: tab.blocked ? tCommon('popup_blocked_description', { appName }) : undefined, variant: 'destructive', }) } } catch { tab.close() toast({ title: t('open_failed'), variant: 'destructive' }) } finally { setIsLoading(false) } } if (documentId) { return ( ) } if (hasJeDoc && journalEntryId) { return ( e.stopPropagation()} className={cn(hitAreaClass, className)} > {t('attached_label')} ) } if (missing) { const badge = ( {t('missing_label')} ) if (!onAttach) return {badge} return ( ) } return null }