-- 1. Add new role value ALTER TYPE public.app_role ADD VALUE IF NOT EXISTS 'master_board_member'; -- 2. Assignments table CREATE TABLE IF NOT EXISTS public.master_board_assignments ( id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, user_id UUID NOT NULL, association_id UUID NOT NULL REFERENCES public.associations(id) ON DELETE CASCADE, created_by UUID, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), UNIQUE (user_id, association_id) ); CREATE INDEX IF NOT EXISTS idx_master_board_assignments_user ON public.master_board_assignments(user_id); CREATE INDEX IF NOT EXISTS idx_master_board_assignments_assoc ON public.master_board_assignments(association_id); ALTER TABLE public.master_board_assignments ENABLE ROW LEVEL SECURITY; DROP POLICY IF EXISTS "Staff manage master board assignments" ON public.master_board_assignments; CREATE POLICY "Staff manage master board assignments" ON public.master_board_assignments FOR ALL TO authenticated USING (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'manager'::public.app_role)) WITH CHECK (public.has_role(auth.uid(), 'admin'::public.app_role) OR public.has_role(auth.uid(), 'manager'::public.app_role)); DROP POLICY IF EXISTS "Master board members view own assignments" ON public.master_board_assignments; CREATE POLICY "Master board members view own assignments" ON public.master_board_assignments FOR SELECT TO authenticated USING (user_id = auth.uid()); -- 3. Helper: is master board member of a given association CREATE OR REPLACE FUNCTION public.is_master_board_member_of_association(_user_id uuid, _association_id uuid) RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$ SELECT EXISTS ( SELECT 1 FROM public.master_board_assignments mba WHERE mba.user_id = _user_id AND mba.association_id = _association_id ) $$; -- 4. Helper: list master board association ids for a user CREATE OR REPLACE FUNCTION public.get_master_board_association_ids(_user_id uuid DEFAULT auth.uid()) RETURNS SETOF uuid LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$ SELECT mba.association_id FROM public.master_board_assignments mba WHERE mba.user_id = _user_id $$; -- 5. Update is_board_member_of_association to also accept master board assignment CREATE OR REPLACE FUNCTION public.is_board_member_of_association(_user_id uuid, _association_id uuid) RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$ SELECT EXISTS ( SELECT 1 FROM public.board_members bm WHERE bm.user_id = _user_id AND bm.association_id = _association_id ) OR EXISTS ( SELECT 1 FROM public.master_board_assignments mba WHERE mba.user_id = _user_id AND mba.association_id = _association_id ) $$; -- 6. Update user_belongs_to_association to include master board assignments CREATE OR REPLACE FUNCTION public.user_belongs_to_association(_user_id uuid, _association_id uuid) RETURNS boolean LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$ SELECT public.has_role(_user_id, 'admin'::public.app_role) OR public.has_role(_user_id, 'manager'::public.app_role) OR EXISTS ( SELECT 1 FROM public.owners o WHERE o.user_id = _user_id AND o.association_id = _association_id ) OR EXISTS ( SELECT 1 FROM public.board_members bm WHERE bm.user_id = _user_id AND bm.association_id = _association_id ) OR EXISTS ( SELECT 1 FROM public.master_board_assignments mba WHERE mba.user_id = _user_id AND mba.association_id = _association_id ) $$; -- 7. Update get_user_association_ids to include master board assignments CREATE OR REPLACE FUNCTION public.get_user_association_ids() RETURNS SETOF uuid LANGUAGE sql STABLE SECURITY DEFINER SET search_path TO 'public' AS $$ SELECT DISTINCT association_id FROM ( SELECT association_id FROM public.owners WHERE user_id = auth.uid() UNION SELECT association_id FROM public.board_members WHERE user_id = auth.uid() UNION SELECT association_id FROM public.master_board_assignments WHERE user_id = auth.uid() ) sub $$;