feat: add cross-organization member search and template cloning functionality
This commit is contained in:
282
apps/web/supabase/migrations/20260414000006_shared_templates.sql
Normal file
282
apps/web/supabase/migrations/20260414000006_shared_templates.sql
Normal file
@@ -0,0 +1,282 @@
|
||||
/*
|
||||
* -------------------------------------------------------
|
||||
* 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;
|
||||
Reference in New Issue
Block a user