mirror of
https://github.com/renee-png/acmcc.git
synced 2026-06-21 01:40:01 +00:00
Bids & Quotes: PDF attachment upload (board-accessible)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -25,15 +25,34 @@ export default function BidsQuotesPage({ boardAssociationIds, boardCanManage = f
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<any>(null);
|
||||
const [selectedBid, setSelectedBid] = useState<any>(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
|
||||
<div><Label>Status</Label><Select value={form.status} onValueChange={(v) => setForm({ ...form, status: v })}><SelectTrigger><SelectValue /></SelectTrigger><SelectContent><SelectItem value="pending">Pending</SelectItem><SelectItem value="accepted">Accepted</SelectItem><SelectItem value="rejected">Rejected</SelectItem><SelectItem value="expired">Expired</SelectItem></SelectContent></Select></div></div>
|
||||
<div className="grid grid-cols-2 gap-4"><div><Label>Received</Label><Input type="date" value={form.received_date} onChange={(e) => setForm({ ...form, received_date: e.target.value })} /></div><div><Label>Expires</Label><Input type="date" value={form.expiry_date} onChange={(e) => setForm({ ...form, expiry_date: e.target.value })} /></div></div>
|
||||
<div><Label>Description</Label><Textarea value={form.description} onChange={(e) => setForm({ ...form, description: e.target.value })} /></div>
|
||||
</div><DialogFooter><Button onClick={handleSave}>{editing ? "Update" : "Create"}</Button></DialogFooter></DialogContent></Dialog>
|
||||
<div>
|
||||
<Label>Attachment (PDF)</Label>
|
||||
{form.document_url ? (
|
||||
<div className="flex items-center justify-between gap-2 rounded-md border bg-muted/30 p-2 mt-1">
|
||||
<a href={form.document_url} target="_blank" rel="noopener noreferrer" className="text-sm text-primary underline truncate">
|
||||
{form.document_name || "View PDF"}
|
||||
</a>
|
||||
<Button type="button" variant="ghost" size="sm" onClick={() => setForm({ ...form, document_url: "", document_name: "" })}>Remove</Button>
|
||||
</div>
|
||||
) : (
|
||||
<Input type="file" accept="application/pdf" className="mt-1" disabled={uploading}
|
||||
onChange={(e) => handleFileUpload(e.target.files?.[0])} />
|
||||
)}
|
||||
{uploading && <p className="text-xs text-muted-foreground mt-1">Uploading…</p>}
|
||||
</div>
|
||||
</div><DialogFooter><Button onClick={handleSave} disabled={uploading}>{editing ? "Update" : "Create"}</Button></DialogFooter></DialogContent></Dialog>
|
||||
|
||||
{/* Detail/Vote Dialog */}
|
||||
<Dialog open={!!selectedBid} onOpenChange={(open) => !open && setSelectedBid(null)}>
|
||||
@@ -121,6 +155,13 @@ export default function BidsQuotesPage({ boardAssociationIds, boardCanManage = f
|
||||
{selectedBid.description && (
|
||||
<div><p className="text-sm text-muted-foreground">Description</p><p className="bg-muted p-3 rounded-md mt-1 border whitespace-pre-wrap">{selectedBid.description}</p></div>
|
||||
)}
|
||||
{selectedBid.document_url && (
|
||||
<div><p className="text-sm text-muted-foreground">Attachment</p>
|
||||
<a href={selectedBid.document_url} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-sm text-primary underline mt-1">
|
||||
{selectedBid.document_name || "View PDF"}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<VotingAndComments entityType="bid_quote" entityId={selectedBid.id} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user