From c670ca7e0e3a97abafc9bc414d0118369d89f594 Mon Sep 17 00:00:00 2001 From: renee-png Date: Tue, 16 Jun 2026 20:53:19 -0400 Subject: [PATCH] Bids & Quotes: PDF attachment upload (board-accessible) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a PDF upload to the bid/quote dialog (stored in the bid-attachments bucket, saved to document_url/document_name) and shows the attachment in the detail view. Board members with can_upload can attach PDFs — table RLS and the storage bucket already permit it; only the UI was missing. Co-Authored-By: Claude Opus 4.8 --- src/pages/BidsQuotesPage.tsx | 49 +++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/src/pages/BidsQuotesPage.tsx b/src/pages/BidsQuotesPage.tsx index 8ccca35..d162b4d 100644 --- a/src/pages/BidsQuotesPage.tsx +++ b/src/pages/BidsQuotesPage.tsx @@ -25,15 +25,34 @@ export default function BidsQuotesPage({ boardAssociationIds, boardCanManage = f const [dialogOpen, setDialogOpen] = useState(false); const [editing, setEditing] = useState(null); const [selectedBid, setSelectedBid] = useState(null); - const [form, setForm] = useState({ vendor_name: "", amount: "", description: "", status: "pending", received_date: "", expiry_date: "" }); + const [form, setForm] = useState({ vendor_name: "", amount: "", description: "", status: "pending", received_date: "", expiry_date: "", document_url: "", document_name: "" }); + const [uploading, setUploading] = useState(false); const isBoardView = !!boardAssociationIds?.length; // Staff always manage; board members only when granted the upload permission. const canManage = !isBoardView || boardCanManage; const fetchData = async () => { setLoading(true); let query = supabase.from("bids_quotes").select("*, associations(name)").order("created_at", { ascending: false }); if (isBoardView) query = query.in("association_id", boardAssociationIds!); const { data } = await query; setBids(data || []); setLoading(false); }; useEffect(() => { fetchData(); }, []); - const openNew = () => { setEditing(null); setForm({ vendor_name: "", amount: "", description: "", status: "pending", received_date: "", expiry_date: "" }); setDialogOpen(true); }; - const openEdit = (b: any) => { setEditing(b); setForm({ vendor_name: b.vendor_name, amount: b.amount?.toString() || "", description: b.description || "", status: b.status, received_date: b.received_date || "", expiry_date: b.expiry_date || "" }); setDialogOpen(true); }; + const openNew = () => { setEditing(null); setForm({ vendor_name: "", amount: "", description: "", status: "pending", received_date: "", expiry_date: "", document_url: "", document_name: "" }); setDialogOpen(true); }; + const openEdit = (b: any) => { setEditing(b); setForm({ vendor_name: b.vendor_name, amount: b.amount?.toString() || "", description: b.description || "", status: b.status, received_date: b.received_date || "", expiry_date: b.expiry_date || "", document_url: b.document_url || "", document_name: b.document_name || "" }); setDialogOpen(true); }; + + const handleFileUpload = async (file?: File) => { + if (!file) return; + if (file.type !== "application/pdf") { toast({ variant: "destructive", title: "PDF only", description: "Please choose a PDF file." }); return; } + setUploading(true); + try { + const path = `${crypto.randomUUID()}.pdf`; + const { error } = await supabase.storage.from("bid-attachments").upload(path, file, { contentType: "application/pdf", upsert: false }); + if (error) throw error; + const { data } = supabase.storage.from("bid-attachments").getPublicUrl(path); + setForm((f) => ({ ...f, document_url: data.publicUrl, document_name: file.name })); + toast({ title: "PDF uploaded" }); + } catch (e: any) { + toast({ variant: "destructive", title: "Upload failed", description: e.message }); + } finally { + setUploading(false); + } + }; const handleSave = async () => { const payload = { ...form, amount: parseFloat(form.amount) || 0, received_date: form.received_date || null, expiry_date: form.expiry_date || null }; if (editing) { await supabase.from("bids_quotes").update(payload).eq("id", editing.id); toast({ title: "Updated" }); } @@ -104,7 +123,22 @@ export default function BidsQuotesPage({ boardAssociationIds, boardCanManage = f
setForm({ ...form, received_date: e.target.value })} />
setForm({ ...form, expiry_date: e.target.value })} />