import { useCallback, useRef, useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { format } from "date-fns"; import { X, Paperclip, Upload, Trash2, FileText, ImageIcon, Loader2, ArrowUpCircle, ArrowDownCircle, ArrowLeftRight, TrendingUp, } from "lucide-react"; import { cn } from "@/utils/cn"; import { formatCurrency } from "@/utils/currency"; import type { Transaction, AttachmentRef } from "@/api/transactions"; import { uploadAttachment, deleteAttachment, getAttachmentUrl } from "@/api/transactions"; const TYPE_COLORS = { income: "text-success", expense: "text-destructive", transfer: "text-muted-foreground", investment: "text-primary", }; const TYPE_ICONS = { income: ArrowUpCircle, expense: ArrowDownCircle, transfer: ArrowLeftRight, investment: TrendingUp, }; const TYPE_BG = { income: "bg-success/10", expense: "bg-destructive/10", transfer: "bg-secondary", investment: "bg-primary/10", }; function humanFileSize(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 FileIcon({ mimeType }: { mimeType: string }) { if (mimeType === "application/pdf") return ; return ; } interface Props { transaction: Transaction; accountName?: string; categoryName?: string; onClose: () => void; } export default function TransactionDetailDrawer({ transaction, accountName, categoryName, onClose }: Props) { const qc = useQueryClient(); const [attachments, setAttachments] = useState(transaction.attachment_refs ?? []); const [dragging, setDragging] = useState(false); const [uploadError, setUploadError] = useState(null); const fileInputRef = useRef(null); const Icon = TYPE_ICONS[transaction.type] ?? ArrowDownCircle; const uploadMutation = useMutation({ mutationFn: (file: File) => uploadAttachment(transaction.id, file), onSuccess: (ref) => { setAttachments((prev) => [...prev, ref]); qc.invalidateQueries({ queryKey: ["transactions"] }); setUploadError(null); }, onError: (err: any) => { setUploadError(err?.response?.data?.detail ?? "Upload failed"); }, }); const deleteMutation = useMutation({ mutationFn: (attachmentId: string) => deleteAttachment(transaction.id, attachmentId), onSuccess: (_data, attachmentId) => { setAttachments((prev) => prev.filter((a) => a.id !== attachmentId)); qc.invalidateQueries({ queryKey: ["transactions"] }); }, }); const handleFiles = useCallback((files: FileList | null) => { if (!files) return; setUploadError(null); for (const file of Array.from(files)) { uploadMutation.mutate(file); } }, [uploadMutation]); const onDragOver = (e: React.DragEvent) => { e.preventDefault(); setDragging(true); }; const onDragLeave = () => setDragging(false); const onDrop = (e: React.DragEvent) => { e.preventDefault(); setDragging(false); handleFiles(e.dataTransfer.files); }; return ( <> {/* Backdrop */}
{/* Drawer */}
{/* Header */}

{transaction.description}

{format(new Date(transaction.date), "dd MMMM yyyy")}

{/* Body */}
{/* Amount */}

{transaction.amount >= 0 ? "+" : ""} {formatCurrency(transaction.amount, transaction.currency)}

{transaction.amount_base !== null && transaction.currency !== transaction.base_currency && (

≈ {formatCurrency(transaction.amount_base, transaction.base_currency)}

)}
{/* Detail rows */}
{[ ["Account", accountName ?? "—"], ["Category", categoryName ?? "Uncategorised"], ["Status", transaction.status.charAt(0).toUpperCase() + transaction.status.slice(1)], ["Type", transaction.type.charAt(0).toUpperCase() + transaction.type.slice(1)], ...(transaction.merchant ? [["Merchant", transaction.merchant]] : []), ...(transaction.notes ? [["Notes", transaction.notes]] : []), ].map(([label, value]) => (
{label} {value}
))} {transaction.tags.length > 0 && (
Tags
{transaction.tags.map((t) => ( {t} ))}
)}
{/* Attachments */}

Receipts & Attachments

{attachments.length}/10
{/* Existing attachments */} {attachments.length > 0 && (
{attachments.map((att) => (
{att.filename}

{humanFileSize(att.size)}

))}
)} {/* Drop zone */} {attachments.length < 10 && (
fileInputRef.current?.click()} className={cn( "border-2 border-dashed rounded-xl p-5 text-center cursor-pointer transition-colors select-none", dragging ? "border-primary bg-primary/5" : "border-border hover:border-primary/50 hover:bg-secondary/30" )} > {uploadMutation.isPending ? (

Uploading…

) : (

Drop files here or click to browse

JPEG, PNG, WebP, PDF — max 10 MB

)}
)} handleFiles(e.target.files)} /> {uploadError && (

{uploadError}

)}
); }