"use client"; import { useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { UploadCloud, FileText, Loader2, CheckCircle2, AlertCircle } from "lucide-react"; const MAX_BYTES = 20 * 1024 * 1024; // Drag-and-drop / tap-to-browse PDF upload against an invoice's `source_pdf` field. // Accepts PDF only, matching the server route — the client check is here to fail fast, // the server check is the one that actually guards the data. export function InvoiceUploader({ invoiceNo }: { invoiceNo: string }) { const router = useRouter(); const inputRef = useRef(null); const [dragging, setDragging] = useState(false); const [state, setState] = useState<{ status: "idle" | "uploading" | "done" | "error"; message?: string }>({ status: "idle", }); async function send(file: File) { if (file.type !== "application/pdf" && !file.name.toLowerCase().endsWith(".pdf")) { setState({ status: "error", message: "Only PDF invoices are accepted" }); return; } if (file.size > MAX_BYTES) { setState({ status: "error", message: `${(file.size / 1048576).toFixed(1)} MB — limit is 20 MB` }); return; } setState({ status: "uploading" }); const body = new FormData(); body.append("file", file); body.append("invoiceNo", invoiceNo); try { const res = await fetch("/api/invoices/upload", { method: "POST", body }); const data = await res.json().catch(() => ({ ok: false, error: "Unexpected response" })); if (!data.ok) setState({ status: "error", message: data.error || "Upload failed" }); else { setState({ status: "done", message: `${data.file} attached` }); router.refresh(); } } catch { setState({ status: "error", message: "Could not reach the server" }); } } return (
{/* Whole zone is the tap target — button semantics on a div, so it stays one 44px-tall control instead of a small inner button. */}
inputRef.current?.click()} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); inputRef.current?.click(); } }} onDragOver={(e) => { e.preventDefault(); setDragging(true); }} onDragLeave={() => setDragging(false)} onDrop={(e) => { e.preventDefault(); setDragging(false); const file = e.dataTransfer.files?.[0]; if (file) send(file); }} className={`flex items-center gap-3 min-h-[44px] px-4 py-3 rounded-xl border-2 border-dashed cursor-pointer transition ${ dragging ? "border-[#00798C] bg-[#00798C]/10" : "border-gray-200 dark:border-white/10 hover:border-[#00798C]/50 hover:bg-gray-50 dark:hover:bg-white/[0.02]" }`} > {state.status === "uploading" ? ( ) : state.status === "done" ? ( ) : ( )}
{state.status === "uploading" ? "Uploading…" : state.status === "done" ? state.message : "Drop the invoice PDF here, or tap to browse"}
PDF only · up to 20 MB · attaches to {invoiceNo}
{ const file = e.target.files?.[0]; if (file) send(file); e.target.value = ""; // allow re-picking the same file }} /> {state.status === "error" && (

{state.message}

)}
); } // Shown once a document is attached, with a link that opens it. The link points at our // own authenticated proxy, not at PocketBase's /api/files — the invoices collection is // rule-locked, so a direct PB link 404s in the browser. export function AttachedDocument({ invoiceNo, invoiceId, }: { invoiceNo: string; invoiceId: string; }) { const [showUploader, setShowUploader] = useState(false); return (
Open invoice PDF {showUploader && (
)}
); }