'use client' import { useState, useEffect, useCallback, useRef } 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 { useToast } from '@/components/ui/use-toast' import { FileText, ImageIcon, Download, ChevronDown, ChevronUp, Plus, Trash2, RefreshCw, Loader2, Lock, AlertTriangle, Inbox, } from 'lucide-react' import DocumentUploadZone from '@/components/bookkeeping/DocumentUploadZone' import type { UploadedFile } from '@/components/bookkeeping/DocumentUploadZone' import InboxDocumentPicker from '@/components/bookkeeping/InboxDocumentPicker' interface DocumentRecord { id: string file_name: string file_size_bytes: number mime_type: string | null storage_path: string created_at: string download_url?: string } interface JournalEntryAttachmentsProps { journalEntryId: string onCountChange?: (count: number) => void } function formatFileSize(bytes: number): string { if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` return `${(bytes / (1024 * 1024)).toFixed(1)} MB` } function isImageType(type: string | null): boolean { return type?.startsWith('image/') ?? false } function isPdfType(type: string | null): boolean { return type === 'application/pdf' } function isPreviewable(type: string | null): boolean { return isImageType(type) || isPdfType(type) } export default function JournalEntryAttachments({ journalEntryId, onCountChange, }: JournalEntryAttachmentsProps) { const t = useTranslations('journal_attachments') const { toast } = useToast() const [documents, setDocuments] = useState([]) const [loading, setLoading] = useState(true) const [expandedDoc, setExpandedDoc] = useState(null) const [showUpload, setShowUpload] = useState(false) const [showInboxPicker, setShowInboxPicker] = useState(false) const [uploadFiles, setUploadFiles] = useState([]) // Docs listed here are filtered by journal_entry_id, so every row is bound // to a verifikation — BFL 7 kap 2§ blocks deletion. "Ta bort" therefore // surfaces the educational modal; "Ersätt" goes through createNewVersion() // so the original stays in the version chain. const [blockedDoc, setBlockedDoc] = useState(null) const [replacingDocId, setReplacingDocId] = useState(null) const replaceFileInputRef = useRef(null) const replaceTargetIdRef = useRef(null) const onCountChangeRef = useRef(onCountChange) onCountChangeRef.current = onCountChange const fetchDocuments = useCallback(async () => { try { const res = await fetch( `/api/documents?journal_entry_id=${journalEntryId}¤t_only=true` ) const { data } = await res.json() setDocuments(data || []) onCountChangeRef.current?.(data?.length || 0) } catch { // Non-critical — silently ignore } finally { setLoading(false) } }, [journalEntryId]) useEffect(() => { fetchDocuments() }, [fetchDocuments]) // Refresh documents when uploads complete useEffect(() => { const allDone = uploadFiles.length > 0 && uploadFiles.every((f) => f.status !== 'uploading') const hasUploaded = uploadFiles.some((f) => f.status === 'uploaded') if (allDone && hasUploaded) { fetchDocuments() setUploadFiles([]) setShowUpload(false) } }, [uploadFiles, fetchDocuments]) const handleDownload = async (docId: string) => { try { const res = await fetch(`/api/documents/${docId}`) const { data } = await res.json() if (data?.download_url) { window.open(data.download_url, '_blank') } } catch { // Non-critical — silently ignore } } const handlePreviewToggle = async (doc: DocumentRecord) => { if (expandedDoc === doc.id) { setExpandedDoc(null) return } if (!doc.download_url) { try { const res = await fetch(`/api/documents/${doc.id}`) const { data } = await res.json() if (data?.download_url) { setDocuments((prev) => prev.map((d) => (d.id === doc.id ? { ...d, download_url: data.download_url } : d)) ) } } catch { return } } setExpandedDoc(doc.id) } const handleRequestRemove = (doc: DocumentRecord) => { setBlockedDoc(doc) } const handleOpenReplacePicker = (docId: string) => { replaceTargetIdRef.current = docId replaceFileInputRef.current?.click() } const handleReplaceFileSelected = async (file: File | null) => { const docId = replaceTargetIdRef.current replaceTargetIdRef.current = null if (replaceFileInputRef.current) { replaceFileInputRef.current.value = '' } if (!file || !docId) return setReplacingDocId(docId) try { const fd = new FormData() fd.append('file', file) const res = await fetch(`/api/documents/${docId}/versions`, { method: 'POST', body: fd, }) if (!res.ok) { const { error } = await res.json().catch(() => ({ error: undefined })) toast({ title: t('replace_failed'), description: error || undefined, variant: 'destructive', }) } else { await fetchDocuments() setBlockedDoc(null) } } catch { toast({ title: t('replace_failed'), variant: 'destructive' }) } finally { setReplacingDocId(null) } } if (loading) { return (
{t('loading')}
) } return (
handleReplaceFileSelected(e.target.files?.[0] ?? null)} />

{t('title')} {documents.length > 0 && `(${documents.length})`}

{showUpload && (
)} {documents.length === 0 && !showUpload ? (

{t('empty')}

) : (
{documents.map((doc) => { const isReplacing = replacingDocId === doc.id return (
{isPreviewable(doc.mime_type) ? ( ) : ( )} {isPreviewable(doc.mime_type) && expandedDoc !== doc.id && ( isImageType(doc.mime_type) ? ( ) : ( ) )} {doc.file_name} {formatFileSize(doc.file_size_bytes)}
{expandedDoc === doc.id && doc.download_url && isImageType(doc.mime_type) && (
{doc.file_name}
)} {expandedDoc === doc.id && doc.download_url && isPdfType(doc.mime_type) && (
{/* + type="application/pdf" invokes Chrome's PDF plugin directly.