Files
myeasycms-v2/apps/web/supabase/migrations/20260414000006_shared_templates.sql

283 lines
8.9 KiB
PL/PgSQL

/*
* -------------------------------------------------------
* 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;