Board-member upload permission for documents & bids/quotes

Add a "Allow document & bid/quote uploads" toggle on board member profiles
(board_members.can_upload). When enabled, that board member can upload
association documents and create/manage bids & quotes for their association(s);
otherwise the board portal stays read-only for them.

- Migration (prod): board_members.can_upload column; tighten the documents
  insert + storage 'files' upload policies to require can_upload; add a
  bids_quotes board policy gated on can_upload.
- BoardMembersPage: permission switch (load/save).
- BoardAssociationContext: expose canUpload for the selected association.
- DocumentsPage: board upload gated by the flag (was always-on for board).
- BidsQuotesPage: permitted board members can add/manage bids (was hidden);
  board inserts target the board's association.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-01 23:33:09 -04:00
parent 2fd311c8a2
commit a3a0b706a1
7 changed files with 105 additions and 21 deletions
+10 -5
View File
@@ -16,11 +16,11 @@ import type { Tables } from "@/integrations/supabase/types";
type BoardMember = {
id: string; association_id: string; member_name: string; member_email: string | null;
phone: string | null; role: string | null; approval_authority: boolean;
phone: string | null; role: string | null; approval_authority: boolean; can_upload: boolean;
created_at: string; associations?: { name: string } | null;
};
const emptyForm = { member_name: "", member_email: "", phone: "", role: "Member", approval_authority: false, association_id: "" };
const emptyForm = { member_name: "", member_email: "", phone: "", role: "Member", approval_authority: false, can_upload: false, association_id: "" };
export default function BoardMembersPage() {
const { toast } = useToast();
@@ -55,7 +55,7 @@ export default function BoardMembersPage() {
const openAdd = () => { setEditingId(null); setForm({ ...emptyForm }); setFormOpen(true); };
const openEdit = (m: BoardMember) => {
setEditingId(m.id);
setForm({ member_name: m.member_name, member_email: m.member_email || "", phone: m.phone || "", role: m.role || "Member", approval_authority: m.approval_authority, association_id: m.association_id });
setForm({ member_name: m.member_name, member_email: m.member_email || "", phone: m.phone || "", role: m.role || "Member", approval_authority: m.approval_authority, can_upload: m.can_upload ?? false, association_id: m.association_id });
setFormOpen(true);
};
@@ -68,10 +68,11 @@ export default function BoardMembersPage() {
association_id: form.association_id, member_name: form.member_name,
member_email: form.member_email || null, phone: form.phone || null,
role: form.role || "Member", approval_authority: form.approval_authority,
can_upload: form.can_upload,
};
const { error } = editingId
? await supabase.from("board_members").update(payload).eq("id", editingId)
: await supabase.from("board_members").insert(payload);
? await supabase.from("board_members").update(payload as any).eq("id", editingId)
: await supabase.from("board_members").insert(payload as any);
if (error) toast({ variant: "destructive", title: "Error", description: error.message });
else { toast({ title: editingId ? "Member updated" : "Member added" }); setFormOpen(false); fetchData(); }
setSaving(false);
@@ -208,6 +209,10 @@ export default function BoardMembersPage() {
<Switch checked={form.approval_authority} onCheckedChange={v => setForm({ ...form, approval_authority: v })} />
<Label>Approval Authority</Label>
</div>
<div className="flex items-center gap-3">
<Switch checked={form.can_upload} onCheckedChange={v => setForm({ ...form, can_upload: v })} />
<Label>Allow document & bid/quote uploads</Label>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setFormOpen(false)}>Cancel</Button>