/* * ------------------------------------------------------- * Shared Templates Across Hierarchy * * 1. Add shared_with_hierarchy flag to newsletter_templates * 2. Create document_templates table with hierarchy sharing * 3. Template cloning function for child orgs * 4. Hierarchy-aware RLS for template reads * ------------------------------------------------------- */ -- ===================================================== -- 1. Newsletter Templates: add sharing flag -- ===================================================== ALTER TABLE public.newsletter_templates ADD COLUMN IF NOT EXISTS shared_with_hierarchy boolean NOT NULL DEFAULT false; -- Allow child orgs to read parent's shared templates DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'newsletter_templates' AND policyname = 'newsletter_templates_hierarchy_read' ) THEN EXECUTE 'CREATE POLICY newsletter_templates_hierarchy_read ON public.newsletter_templates FOR SELECT TO authenticated USING ( shared_with_hierarchy = true AND public.has_role_on_account_or_ancestor(account_id) )'; END IF; END $$; -- ===================================================== -- 2. Document Templates table -- ===================================================== CREATE TABLE IF NOT EXISTS public.document_templates ( id uuid PRIMARY KEY DEFAULT gen_random_uuid(), account_id uuid NOT NULL REFERENCES public.accounts(id) ON DELETE CASCADE, name text NOT NULL, description text, template_type text NOT NULL DEFAULT 'generic' CHECK (template_type IN ( 'generic', 'member_card', 'invoice', 'receipt', 'certificate', 'letter', 'label', 'report' )), -- Template content: HTML with variable placeholders like {{member.first_name}} body_html text NOT NULL DEFAULT '', -- Available variables for this template variables jsonb NOT NULL DEFAULT '[]'::jsonb, -- Page settings page_format text NOT NULL DEFAULT 'A4' CHECK (page_format IN ('A4', 'A5', 'A6', 'letter', 'label')), orientation text NOT NULL DEFAULT 'portrait' CHECK (orientation IN ('portrait', 'landscape')), -- Hierarchy sharing shared_with_hierarchy boolean NOT NULL DEFAULT false, -- Meta is_default boolean NOT NULL DEFAULT false, sort_order integer NOT NULL DEFAULT 0, created_by uuid REFERENCES auth.users(id) ON DELETE SET NULL, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS ix_document_templates_account ON public.document_templates(account_id); CREATE INDEX IF NOT EXISTS ix_document_templates_type ON public.document_templates(account_id, template_type); ALTER TABLE public.document_templates ENABLE ROW LEVEL SECURITY; REVOKE ALL ON public.document_templates FROM authenticated, service_role; GRANT SELECT, INSERT, UPDATE, DELETE ON public.document_templates TO authenticated; GRANT ALL ON public.document_templates TO service_role; -- Own account read/write CREATE POLICY document_templates_select ON public.document_templates FOR SELECT TO authenticated USING (public.has_role_on_account(account_id)); CREATE POLICY document_templates_mutate ON public.document_templates FOR ALL TO authenticated USING (public.has_permission(auth.uid(), account_id, 'documents.generate'::public.app_permissions)); -- Hierarchy: child orgs can read parent's shared templates DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_policies WHERE schemaname = 'public' AND tablename = 'document_templates' AND policyname = 'document_templates_hierarchy_read' ) THEN EXECUTE 'CREATE POLICY document_templates_hierarchy_read ON public.document_templates FOR SELECT TO authenticated USING ( shared_with_hierarchy = true AND public.has_role_on_account_or_ancestor(account_id) )'; END IF; END $$; CREATE TRIGGER trg_document_templates_updated_at BEFORE UPDATE ON public.document_templates FOR EACH ROW EXECUTE FUNCTION public.update_account_settings_timestamp(); -- ===================================================== -- 3. Template cloning function -- ===================================================== -- Clone a shared template into a child org's own templates CREATE OR REPLACE FUNCTION public.clone_template( p_template_type text, -- 'newsletter' or 'document' p_template_id uuid, p_target_account_id uuid, p_new_name text DEFAULT NULL ) RETURNS uuid LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$ DECLARE v_new_id uuid; v_caller_id uuid; v_source_account_id uuid; BEGIN v_caller_id := (SELECT auth.uid()); -- Verify caller has a role on the target account IF NOT public.has_role_on_account(p_target_account_id) THEN RAISE EXCEPTION 'Keine Berechtigung für die Zielorganisation'; END IF; IF p_template_type = 'newsletter' THEN -- Get source account for verification SELECT account_id INTO v_source_account_id FROM public.newsletter_templates WHERE id = p_template_id; IF v_source_account_id IS NULL THEN RAISE EXCEPTION 'Vorlage nicht gefunden'; END IF; -- Must be shared or from own account IF v_source_account_id != p_target_account_id THEN IF NOT EXISTS ( SELECT 1 FROM public.newsletter_templates WHERE id = p_template_id AND shared_with_hierarchy = true ) THEN RAISE EXCEPTION 'Vorlage ist nicht für die Hierarchie freigegeben'; END IF; END IF; INSERT INTO public.newsletter_templates ( account_id, name, subject, body_html, body_text, variables ) SELECT p_target_account_id, COALESCE(p_new_name, name || ' (Kopie)'), subject, body_html, body_text, variables FROM public.newsletter_templates WHERE id = p_template_id RETURNING id INTO v_new_id; ELSIF p_template_type = 'document' THEN SELECT account_id INTO v_source_account_id FROM public.document_templates WHERE id = p_template_id; IF v_source_account_id IS NULL THEN RAISE EXCEPTION 'Vorlage nicht gefunden'; END IF; IF v_source_account_id != p_target_account_id THEN IF NOT EXISTS ( SELECT 1 FROM public.document_templates WHERE id = p_template_id AND shared_with_hierarchy = true ) THEN RAISE EXCEPTION 'Vorlage ist nicht für die Hierarchie freigegeben'; END IF; END IF; INSERT INTO public.document_templates ( account_id, name, description, template_type, body_html, variables, page_format, orientation, created_by ) SELECT p_target_account_id, COALESCE(p_new_name, name || ' (Kopie)'), description, template_type, body_html, variables, page_format, orientation, v_caller_id FROM public.document_templates WHERE id = p_template_id RETURNING id INTO v_new_id; ELSE RAISE EXCEPTION 'Ungültiger Vorlagentyp: %', p_template_type; END IF; RETURN v_new_id; END; $$; GRANT EXECUTE ON FUNCTION public.clone_template(text, uuid, uuid, text) TO authenticated, service_role; -- ===================================================== -- 4. List shared templates across hierarchy -- ===================================================== CREATE OR REPLACE FUNCTION public.list_hierarchy_shared_templates( root_account_id uuid, p_template_type text DEFAULT NULL -- 'newsletter', 'document', or NULL for both ) RETURNS TABLE ( id uuid, account_id uuid, account_name varchar, template_source text, -- 'newsletter' or 'document' name text, description text, template_type text, shared_with_hierarchy boolean, created_at timestamptz ) LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$ BEGIN IF NOT public.has_role_on_account(root_account_id) THEN RETURN; END IF; RETURN QUERY -- Newsletter templates SELECT nt.id, nt.account_id, a.name AS account_name, 'newsletter'::text AS template_source, nt.name, NULL::text AS description, 'newsletter'::text AS template_type, nt.shared_with_hierarchy, nt.created_at FROM public.newsletter_templates nt JOIN public.accounts a ON a.id = nt.account_id WHERE nt.account_id IN (SELECT d FROM public.get_account_descendants(root_account_id) d) AND nt.shared_with_hierarchy = true AND (p_template_type IS NULL OR p_template_type = 'newsletter') UNION ALL -- Document templates SELECT dt.id, dt.account_id, a.name AS account_name, 'document'::text AS template_source, dt.name, dt.description, dt.template_type, dt.shared_with_hierarchy, dt.created_at FROM public.document_templates dt JOIN public.accounts a ON a.id = dt.account_id WHERE dt.account_id IN (SELECT d FROM public.get_account_descendants(root_account_id) d) AND dt.shared_with_hierarchy = true AND (p_template_type IS NULL OR p_template_type = 'document') ORDER BY created_at DESC; END; $$; GRANT EXECUTE ON FUNCTION public.list_hierarchy_shared_templates(uuid, text) TO authenticated, service_role;