feat: map shapes, image persistence, grab tool, layer rename/delete, i18n, page metadata

This commit is contained in:
AlacrisDevs
2026-02-08 23:11:09 +02:00
parent 75a2aefadb
commit f2384bceb8
125 changed files with 22605 additions and 3902 deletions

View File

@@ -0,0 +1,7 @@
-- Add event_id and department_id to documents for auto-folder linking
ALTER TABLE documents ADD COLUMN IF NOT EXISTS event_id uuid REFERENCES events(id) ON DELETE SET NULL;
ALTER TABLE documents ADD COLUMN IF NOT EXISTS department_id uuid REFERENCES event_departments(id) ON DELETE SET NULL;
-- Index for quick lookups
CREATE INDEX IF NOT EXISTS idx_documents_event_id ON documents(event_id) WHERE event_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_documents_department_id ON documents(department_id) WHERE department_id IS NOT NULL;

View File

@@ -0,0 +1,28 @@
-- Create storage bucket for uploaded files
INSERT INTO storage.buckets (id, name, public, file_size_limit)
VALUES ('files', 'files', true, 52428800) -- 50MB max file size
ON CONFLICT (id) DO NOTHING;
-- Allow authenticated users to upload files (organized by org_id/folder)
CREATE POLICY "Authenticated users can upload files"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (bucket_id = 'files');
-- Allow authenticated users to update their files
CREATE POLICY "Authenticated users can update files"
ON storage.objects FOR UPDATE
TO authenticated
USING (bucket_id = 'files');
-- Allow public read access to all files
CREATE POLICY "Public read access for files"
ON storage.objects FOR SELECT
TO public
USING (bucket_id = 'files');
-- Allow authenticated users to delete files
CREATE POLICY "Authenticated users can delete files"
ON storage.objects FOR DELETE
TO authenticated
USING (bucket_id = 'files');

View File

@@ -0,0 +1,5 @@
-- Add 'triple' to layout_preset enum for 3-column layout
ALTER TYPE layout_preset ADD VALUE IF NOT EXISTS 'triple';
-- Migrate any existing 'focus_sidebar' or 'custom' layouts to 'split'
UPDATE department_dashboards SET layout = 'split' WHERE layout IN ('focus_sidebar', 'custom');

View File

@@ -0,0 +1,7 @@
-- Add receipt/invoice document linking to budget items
-- Each budget item can optionally link to a document (uploaded file) as its receipt/invoice
ALTER TABLE budget_items
ADD COLUMN receipt_document_id UUID REFERENCES documents(id) ON DELETE SET NULL;
CREATE INDEX idx_budget_items_receipt ON budget_items(receipt_document_id);

View File

@@ -0,0 +1,210 @@
-- Finance enhancements: planned budgets per department, sponsor allocations,
-- org-wide contacts, and department contact pinning
-- ============================================================
-- 1. Add planned_budget to event_departments
-- ============================================================
ALTER TABLE event_departments
ADD COLUMN IF NOT EXISTS planned_budget NUMERIC(12,2) NOT NULL DEFAULT 0;
-- ============================================================
-- 2. Sponsor Allocations
-- Tracks how sponsor money is allocated to departments.
-- A sponsor (confirmed/active) has an amount; the finance lead
-- distributes portions of that to specific departments.
-- ============================================================
CREATE TABLE sponsor_allocations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
sponsor_id UUID NOT NULL REFERENCES sponsors(id) ON DELETE CASCADE,
department_id UUID NOT NULL REFERENCES event_departments(id) ON DELETE CASCADE,
allocated_amount NUMERIC(12,2) NOT NULL DEFAULT 0,
used_amount NUMERIC(12,2) NOT NULL DEFAULT 0,
notes TEXT,
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 idx_sponsor_alloc_sponsor ON sponsor_allocations(sponsor_id);
CREATE INDEX idx_sponsor_alloc_dept ON sponsor_allocations(department_id);
-- Prevent duplicate allocations of the same sponsor to the same department
CREATE UNIQUE INDEX idx_sponsor_alloc_unique ON sponsor_allocations(sponsor_id, department_id);
-- ============================================================
-- 3. Org-wide Contacts
-- Contacts that belong to the organization, not a department.
-- Departments can "pin" contacts from this list.
-- ============================================================
CREATE TABLE org_contacts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
name TEXT NOT NULL,
role TEXT,
company TEXT,
email TEXT,
phone TEXT,
website TEXT,
notes TEXT,
category TEXT DEFAULT 'general',
color TEXT DEFAULT '#00A3E0',
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 idx_org_contacts_org ON org_contacts(org_id);
CREATE INDEX idx_org_contacts_category ON org_contacts(category);
-- ============================================================
-- 4. Department pinned contacts (junction table)
-- Links org_contacts to departments for quick access
-- ============================================================
CREATE TABLE department_pinned_contacts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
department_id UUID NOT NULL REFERENCES event_departments(id) ON DELETE CASCADE,
contact_id UUID NOT NULL REFERENCES org_contacts(id) ON DELETE CASCADE,
pinned_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX idx_dept_pinned_unique ON department_pinned_contacts(department_id, contact_id);
CREATE INDEX idx_dept_pinned_dept ON department_pinned_contacts(department_id);
CREATE INDEX idx_dept_pinned_contact ON department_pinned_contacts(contact_id);
-- ============================================================
-- 5. RLS Policies
-- ============================================================
-- Sponsor Allocations
ALTER TABLE sponsor_allocations ENABLE ROW LEVEL SECURITY;
CREATE POLICY "sponsor_allocations_select" ON sponsor_allocations FOR SELECT
USING (
EXISTS (
SELECT 1 FROM event_departments ed
JOIN events e ON e.id = ed.event_id
JOIN org_members om ON om.org_id = e.org_id
WHERE ed.id = sponsor_allocations.department_id
AND om.user_id = auth.uid()
)
);
CREATE POLICY "sponsor_allocations_insert" ON sponsor_allocations FOR INSERT
WITH CHECK (
EXISTS (
SELECT 1 FROM event_departments ed
JOIN events e ON e.id = ed.event_id
JOIN org_members om ON om.org_id = e.org_id
WHERE ed.id = sponsor_allocations.department_id
AND om.user_id = auth.uid()
AND om.role IN ('owner', 'admin', 'editor')
)
);
CREATE POLICY "sponsor_allocations_update" ON sponsor_allocations FOR UPDATE
USING (
EXISTS (
SELECT 1 FROM event_departments ed
JOIN events e ON e.id = ed.event_id
JOIN org_members om ON om.org_id = e.org_id
WHERE ed.id = sponsor_allocations.department_id
AND om.user_id = auth.uid()
AND om.role IN ('owner', 'admin', 'editor')
)
);
CREATE POLICY "sponsor_allocations_delete" ON sponsor_allocations FOR DELETE
USING (
EXISTS (
SELECT 1 FROM event_departments ed
JOIN events e ON e.id = ed.event_id
JOIN org_members om ON om.org_id = e.org_id
WHERE ed.id = sponsor_allocations.department_id
AND om.user_id = auth.uid()
AND om.role IN ('owner', 'admin', 'editor')
)
);
-- Org Contacts
ALTER TABLE org_contacts ENABLE ROW LEVEL SECURITY;
CREATE POLICY "org_contacts_select" ON org_contacts FOR SELECT
USING (
EXISTS (
SELECT 1 FROM org_members om
WHERE om.org_id = org_contacts.org_id
AND om.user_id = auth.uid()
)
);
CREATE POLICY "org_contacts_insert" ON org_contacts FOR INSERT
WITH CHECK (
EXISTS (
SELECT 1 FROM org_members om
WHERE om.org_id = org_contacts.org_id
AND om.user_id = auth.uid()
AND om.role IN ('owner', 'admin', 'editor')
)
);
CREATE POLICY "org_contacts_update" ON org_contacts FOR UPDATE
USING (
EXISTS (
SELECT 1 FROM org_members om
WHERE om.org_id = org_contacts.org_id
AND om.user_id = auth.uid()
AND om.role IN ('owner', 'admin', 'editor')
)
);
CREATE POLICY "org_contacts_delete" ON org_contacts FOR DELETE
USING (
EXISTS (
SELECT 1 FROM org_members om
WHERE om.org_id = org_contacts.org_id
AND om.user_id = auth.uid()
AND om.role IN ('owner', 'admin', 'editor')
)
);
-- Department Pinned Contacts
ALTER TABLE department_pinned_contacts ENABLE ROW LEVEL SECURITY;
CREATE POLICY "dept_pinned_contacts_select" ON department_pinned_contacts FOR SELECT
USING (
EXISTS (
SELECT 1 FROM event_departments ed
JOIN events e ON e.id = ed.event_id
JOIN org_members om ON om.org_id = e.org_id
WHERE ed.id = department_pinned_contacts.department_id
AND om.user_id = auth.uid()
)
);
CREATE POLICY "dept_pinned_contacts_insert" ON department_pinned_contacts FOR INSERT
WITH CHECK (
EXISTS (
SELECT 1 FROM event_departments ed
JOIN events e ON e.id = ed.event_id
JOIN org_members om ON om.org_id = e.org_id
WHERE ed.id = department_pinned_contacts.department_id
AND om.user_id = auth.uid()
AND om.role IN ('owner', 'admin', 'editor')
)
);
CREATE POLICY "dept_pinned_contacts_delete" ON department_pinned_contacts FOR DELETE
USING (
EXISTS (
SELECT 1 FROM event_departments ed
JOIN events e ON e.id = ed.event_id
JOIN org_members om ON om.org_id = e.org_id
WHERE ed.id = department_pinned_contacts.department_id
AND om.user_id = auth.uid()
AND om.role IN ('owner', 'admin', 'editor')
)
);
-- ============================================================
-- 6. Enable realtime for sponsor allocations
-- ============================================================
ALTER PUBLICATION supabase_realtime ADD TABLE sponsor_allocations;

View File

@@ -0,0 +1,34 @@
-- Organization settings: currency, locale, defaults, feature toggles, branding
-- Adds configurable preferences to the organizations table
-- ============================================================
-- 1. Preferences (currency, locale, calendar)
-- ============================================================
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS currency TEXT NOT NULL DEFAULT 'EUR';
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS date_format TEXT NOT NULL DEFAULT 'DD/MM/YYYY';
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS timezone TEXT NOT NULL DEFAULT 'Europe/Tallinn';
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS week_start_day TEXT NOT NULL DEFAULT 'monday'
CHECK (week_start_day IN ('monday', 'sunday'));
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS default_calendar_view TEXT NOT NULL DEFAULT 'month'
CHECK (default_calendar_view IN ('month', 'week', 'day'));
-- ============================================================
-- 2. Event defaults
-- ============================================================
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS default_event_color TEXT NOT NULL DEFAULT '#7986cb';
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS default_event_status TEXT NOT NULL DEFAULT 'planning';
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS default_dept_modules TEXT[] NOT NULL DEFAULT ARRAY['kanban', 'files', 'checklist'];
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS default_dept_layout TEXT NOT NULL DEFAULT 'split';
-- ============================================================
-- 3. Feature toggles
-- ============================================================
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS feature_chat BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS feature_sponsors BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS feature_contacts BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS feature_budget BOOLEAN NOT NULL DEFAULT true;
-- ============================================================
-- 4. Branding / description
-- ============================================================
ALTER TABLE organizations ADD COLUMN IF NOT EXISTS description TEXT DEFAULT '';

View File

@@ -0,0 +1,5 @@
-- Add social_links JSONB column to organizations
ALTER TABLE public.organizations
ADD COLUMN IF NOT EXISTS social_links jsonb DEFAULT '{}'::jsonb;
COMMENT ON COLUMN public.organizations.social_links IS 'JSON object storing social media URLs: website, instagram, facebook, discord, linkedin, x, youtube, tiktok, fienta';

View File

@@ -0,0 +1,368 @@
-- ============================================================
-- Migration 039: Security & Performance fixes from Supabase linter reports
-- ============================================================
-- ============================================================
-- 1. CRITICAL: Enable RLS on organizations table (policies exist but RLS was off)
-- ============================================================
ALTER TABLE public.organizations ENABLE ROW LEVEL SECURITY;
-- ============================================================
-- 2. Fix mutable search_path on all public functions
-- Adds SET search_path = '' to make them immutable to search_path attacks
-- ============================================================
-- is_platform_admin
CREATE OR REPLACE FUNCTION public.is_platform_admin()
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = ''
AS $$
SELECT EXISTS (
SELECT 1 FROM public.profiles
WHERE id = (select auth.uid()) AND is_platform_admin = true
);
$$;
-- is_org_member (used heavily in RLS)
CREATE OR REPLACE FUNCTION public.is_org_member(org_id uuid)
RETURNS boolean
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = ''
AS $$
SELECT EXISTS (
SELECT 1 FROM public.org_members
WHERE org_members.org_id = is_org_member.org_id
AND user_id = (select auth.uid())
);
$$;
-- handle_new_user
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
BEGIN
INSERT INTO public.profiles (id, email, full_name, avatar_url)
VALUES (
NEW.id,
NEW.email,
COALESCE(NEW.raw_user_meta_data->>'full_name', NEW.raw_user_meta_data->>'name'),
NEW.raw_user_meta_data->>'avatar_url'
);
RETURN NEW;
END;
$$;
-- handle_new_org
CREATE OR REPLACE FUNCTION public.handle_new_org()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
BEGIN
-- Add creator as owner
INSERT INTO public.org_members (org_id, user_id, role)
VALUES (NEW.id, NEW.created_by, 'owner');
-- Create default roles
PERFORM public.create_default_org_roles(NEW.id);
RETURN NEW;
END;
$$;
-- create_default_org_roles
CREATE OR REPLACE FUNCTION public.create_default_org_roles(p_org_id uuid)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
BEGIN
INSERT INTO public.org_roles (org_id, name, color, permissions, is_system, is_default)
VALUES
(p_org_id, 'Admin', '#ef4444', ARRAY['manage_members','manage_roles','manage_settings','manage_content','view_content','comment'], true, false),
(p_org_id, 'Editor', '#3b82f6', ARRAY['manage_content','view_content','comment'], true, true),
(p_org_id, 'Commenter', '#8b5cf6', ARRAY['view_content','comment'], true, false),
(p_org_id, 'Viewer', '#6b7280', ARRAY['view_content'], true, false)
ON CONFLICT DO NOTHING;
END;
$$;
-- compute_document_path
CREATE OR REPLACE FUNCTION public.compute_document_path(doc_id uuid)
RETURNS text
LANGUAGE plpgsql
STABLE
SET search_path = ''
AS $$
DECLARE
result text := '';
current_id uuid := doc_id;
current_name text;
parent uuid;
BEGIN
LOOP
SELECT d.name, d.parent_id INTO current_name, parent
FROM public.documents d WHERE d.id = current_id;
IF NOT FOUND THEN EXIT; END IF;
IF result = '' THEN result := current_name;
ELSE result := current_name || '/' || result;
END IF;
IF parent IS NULL THEN EXIT; END IF;
current_id := parent;
END LOOP;
RETURN '/' || result;
END;
$$;
-- update_document_path
CREATE OR REPLACE FUNCTION public.update_document_path()
RETURNS trigger
LANGUAGE plpgsql
SET search_path = ''
AS $$
BEGIN
NEW.path := public.compute_document_path(NEW.id);
RETURN NEW;
END;
$$;
-- get_next_document_position
CREATE OR REPLACE FUNCTION public.get_next_document_position(p_org_id uuid, p_parent_id uuid)
RETURNS integer
LANGUAGE plpgsql
SET search_path = ''
AS $$
DECLARE
max_pos integer;
BEGIN
IF p_parent_id IS NULL THEN
SELECT COALESCE(MAX(position), -1) + 1 INTO max_pos
FROM public.documents WHERE org_id = p_org_id AND parent_id IS NULL;
ELSE
SELECT COALESCE(MAX(position), -1) + 1 INTO max_pos
FROM public.documents WHERE org_id = p_org_id AND parent_id = p_parent_id;
END IF;
RETURN max_pos;
END;
$$;
-- update_matrix_credentials_updated_at
CREATE OR REPLACE FUNCTION public.update_matrix_credentials_updated_at()
RETURNS trigger
LANGUAGE plpgsql
SET search_path = ''
AS $$
BEGIN
NEW.updated_at = now();
RETURN NEW;
END;
$$;
-- handle_new_event
CREATE OR REPLACE FUNCTION public.handle_new_event()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
BEGIN
PERFORM public.seed_event_task_columns(NEW.id);
RETURN NEW;
END;
$$;
-- seed_event_task_columns
CREATE OR REPLACE FUNCTION public.seed_event_task_columns(p_event_id uuid)
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
BEGIN
INSERT INTO public.event_task_columns (event_id, name, position, color)
VALUES
(p_event_id, 'To Do', 0, '#6b7280'),
(p_event_id, 'In Progress', 1, '#3b82f6'),
(p_event_id, 'Done', 2, '#22c55e')
ON CONFLICT DO NOTHING;
END;
$$;
-- create_department_dashboard
CREATE OR REPLACE FUNCTION public.create_department_dashboard()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
dash_id uuid;
checklist_id uuid;
note_id uuid;
BEGIN
INSERT INTO public.department_dashboards (department_id, created_by)
VALUES (NEW.id, NEW.created_by)
RETURNING id INTO dash_id;
INSERT INTO public.dashboard_panels (dashboard_id, module_type, position, config)
VALUES
(dash_id, 'kanban', 0, '{}'),
(dash_id, 'files', 1, '{}'),
(dash_id, 'checklist', 2, '{}'),
(dash_id, 'notes', 3, '{}');
INSERT INTO public.department_checklists (department_id, title, created_by)
VALUES (NEW.id, 'Getting Started', NEW.created_by)
RETURNING id INTO checklist_id;
INSERT INTO public.department_checklist_items (checklist_id, title, position)
VALUES
(checklist_id, 'Set up department goals', 0),
(checklist_id, 'Assign team members', 1),
(checklist_id, 'Create initial tasks', 2);
INSERT INTO public.department_notes (department_id, title, content, created_by)
VALUES (NEW.id, 'Welcome', 'Welcome to your department! Use this space for notes and documentation.', NEW.created_by);
RETURN NEW;
END;
$$;
-- ============================================================
-- 3. Fix overly permissive RLS policy: org_members "Allow member inserts"
-- Replace WITH CHECK (true) with proper check
-- ============================================================
DROP POLICY IF EXISTS "Allow member inserts" ON public.org_members;
CREATE POLICY "Allow member inserts" ON public.org_members
FOR INSERT
WITH CHECK (
-- Only allow if user is an admin/owner of the org, or it's the system (via trigger)
EXISTS (
SELECT 1 FROM public.org_members om
WHERE om.org_id = org_members.org_id
AND om.user_id = (select auth.uid())
AND om.role IN ('owner', 'admin')
)
OR
-- Allow the handle_new_org trigger (creator becomes owner)
org_members.user_id = (select auth.uid())
);
-- ============================================================
-- 4. Add missing RLS policies for tables with RLS enabled but no policies
-- ============================================================
-- card_assignees: inherit access from the card's board's org
CREATE POLICY "Card assignees inherit card access" ON public.card_assignees
FOR ALL
USING (
EXISTS (
SELECT 1 FROM public.kanban_cards kc
JOIN public.kanban_columns kcol ON kcol.id = kc.column_id
JOIN public.kanban_boards kb ON kb.id = kcol.board_id
JOIN public.org_members om ON om.org_id = kb.org_id
WHERE kc.id = card_assignees.card_id
AND om.user_id = (select auth.uid())
)
);
-- event_attendees: org members can access
CREATE POLICY "Org members can manage event attendees" ON public.event_attendees
FOR ALL
USING (
EXISTS (
SELECT 1 FROM public.events e
JOIN public.org_members om ON om.org_id = e.org_id
WHERE e.id = event_attendees.event_id
AND om.user_id = (select auth.uid())
)
);
-- ============================================================
-- 5. Add indexes on unindexed foreign keys (created_by, user_id, role_id, org_id)
-- ============================================================
CREATE INDEX IF NOT EXISTS idx_activity_log_user_id ON public.activity_log (user_id);
CREATE INDEX IF NOT EXISTS idx_budget_items_created_by ON public.budget_items (created_by);
CREATE INDEX IF NOT EXISTS idx_calendar_events_created_by ON public.calendar_events (created_by);
CREATE INDEX IF NOT EXISTS idx_card_assignees_user_id ON public.card_assignees (user_id);
CREATE INDEX IF NOT EXISTS idx_department_checklists_created_by ON public.department_checklists (created_by);
CREATE INDEX IF NOT EXISTS idx_department_contacts_created_by ON public.department_contacts (created_by);
CREATE INDEX IF NOT EXISTS idx_department_dashboards_created_by ON public.department_dashboards (created_by);
CREATE INDEX IF NOT EXISTS idx_department_notes_created_by ON public.department_notes (created_by);
CREATE INDEX IF NOT EXISTS idx_document_locks_user_id ON public.document_locks (user_id);
CREATE INDEX IF NOT EXISTS idx_documents_created_by ON public.documents (created_by);
CREATE INDEX IF NOT EXISTS idx_event_attendees_user_id ON public.event_attendees (user_id);
CREATE INDEX IF NOT EXISTS idx_event_members_role_id ON public.event_members (role_id);
CREATE INDEX IF NOT EXISTS idx_event_tasks_created_by ON public.event_tasks (created_by);
CREATE INDEX IF NOT EXISTS idx_events_created_by ON public.events (created_by);
CREATE INDEX IF NOT EXISTS idx_kanban_boards_created_by ON public.kanban_boards (created_by);
CREATE INDEX IF NOT EXISTS idx_kanban_cards_created_by ON public.kanban_cards (created_by);
CREATE INDEX IF NOT EXISTS idx_kanban_checklist_items_card_id ON public.kanban_checklist_items (card_id);
CREATE INDEX IF NOT EXISTS idx_matrix_credentials_org_id ON public.matrix_credentials (org_id);
CREATE INDEX IF NOT EXISTS idx_org_contacts_created_by ON public.org_contacts (created_by);
CREATE INDEX IF NOT EXISTS idx_org_google_calendars_connected_by ON public.org_google_calendars (connected_by);
CREATE INDEX IF NOT EXISTS idx_org_invites_invited_by ON public.org_invites (invited_by);
CREATE INDEX IF NOT EXISTS idx_org_invites_role_id ON public.org_invites (role_id);
CREATE INDEX IF NOT EXISTS idx_org_members_role_id ON public.org_members (role_id);
CREATE INDEX IF NOT EXISTS idx_schedule_blocks_created_by ON public.schedule_blocks (created_by);
CREATE INDEX IF NOT EXISTS idx_sponsor_allocations_created_by ON public.sponsor_allocations (created_by);
CREATE INDEX IF NOT EXISTS idx_sponsors_created_by ON public.sponsors (created_by);
-- ============================================================
-- 6. Drop unused indexes (never been used per Supabase linter)
-- ============================================================
DROP INDEX IF EXISTS public.idx_kanban_cards_assignee;
DROP INDEX IF EXISTS public.idx_kanban_cards_due_date;
DROP INDEX IF EXISTS public.idx_calendar_events_google;
DROP INDEX IF EXISTS public.idx_activity_log_entity;
DROP INDEX IF EXISTS public.idx_team_members_team;
DROP INDEX IF EXISTS public.idx_team_members_user;
DROP INDEX IF EXISTS public.idx_kanban_boards_team;
DROP INDEX IF EXISTS public.idx_kanban_boards_personal;
DROP INDEX IF EXISTS public.idx_kanban_comments_user;
DROP INDEX IF EXISTS public.idx_kanban_comments_created;
DROP INDEX IF EXISTS public.idx_document_locks_heartbeat;
DROP INDEX IF EXISTS public.idx_event_members_user;
DROP INDEX IF EXISTS public.idx_events_org;
DROP INDEX IF EXISTS public.idx_event_tasks_assignee;
DROP INDEX IF EXISTS public.idx_dept_checklist_items_assigned;
DROP INDEX IF EXISTS public.idx_schedule_blocks_time;
DROP INDEX IF EXISTS public.idx_department_contacts_category;
DROP INDEX IF EXISTS public.idx_budget_items_category;
DROP INDEX IF EXISTS public.idx_sponsors_tier;
DROP INDEX IF EXISTS public.idx_sponsors_status;
DROP INDEX IF EXISTS public.idx_sponsor_alloc_sponsor;
DROP INDEX IF EXISTS public.idx_org_contacts_category;
-- ============================================================
-- 7. NOTE: "Leaked password protection" is an Auth dashboard setting,
-- not fixable via migration. Enable it in:
-- Supabase Dashboard → Authentication → Settings → Password Security
-- ============================================================
-- ============================================================
-- 8. NOTE: RLS initplan warnings (auth.uid() → (select auth.uid()))
-- There are ~90 policies that use auth.uid() directly instead of
-- (select auth.uid()). This causes per-row re-evaluation.
-- The fix is mechanical: wrap auth.uid() in a subselect.
-- This is addressed in the new policies above. For existing policies
-- across all tables, a comprehensive rewrite would be very large.
-- We recommend addressing these incrementally per table as needed,
-- or in a dedicated follow-up migration.
-- ============================================================

View File

@@ -0,0 +1,2 @@
-- Add 'map' to the module_type enum
ALTER TYPE module_type ADD VALUE IF NOT EXISTS 'map';

View File

@@ -0,0 +1,42 @@
-- Fix: event_departments has no created_by column.
-- Use auth.uid() instead of NEW.created_by in the auto-create trigger.
CREATE OR REPLACE FUNCTION public.create_department_dashboard()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
dash_id uuid;
checklist_id uuid;
note_id uuid;
current_user_id uuid := auth.uid();
BEGIN
INSERT INTO public.department_dashboards (department_id, created_by)
VALUES (NEW.id, current_user_id)
RETURNING id INTO dash_id;
INSERT INTO public.dashboard_panels (dashboard_id, module_type, position, config)
VALUES
(dash_id, 'kanban', 0, '{}'),
(dash_id, 'files', 1, '{}'),
(dash_id, 'checklist', 2, '{}'),
(dash_id, 'notes', 3, '{}');
INSERT INTO public.department_checklists (department_id, title, created_by)
VALUES (NEW.id, 'Getting Started', current_user_id)
RETURNING id INTO checklist_id;
INSERT INTO public.department_checklist_items (checklist_id, title, position)
VALUES
(checklist_id, 'Set up department goals', 0),
(checklist_id, 'Assign team members', 1),
(checklist_id, 'Create initial tasks', 2);
INSERT INTO public.department_notes (department_id, title, content, created_by)
VALUES (NEW.id, 'Welcome', 'Welcome to your department! Use this space for notes and documentation.', current_user_id);
RETURN NEW;
END;
$$;

View File

@@ -0,0 +1,41 @@
-- Fix: dashboard_panels column is "module" not "module_type"
CREATE OR REPLACE FUNCTION public.create_department_dashboard()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
dash_id uuid;
checklist_id uuid;
note_id uuid;
current_user_id uuid := auth.uid();
BEGIN
INSERT INTO public.department_dashboards (department_id, created_by)
VALUES (NEW.id, current_user_id)
RETURNING id INTO dash_id;
INSERT INTO public.dashboard_panels (dashboard_id, module, position, width)
VALUES
(dash_id, 'kanban', 0, '1'),
(dash_id, 'files', 1, '1'),
(dash_id, 'checklist', 2, '1'),
(dash_id, 'notes', 3, '1');
INSERT INTO public.department_checklists (department_id, title, created_by)
VALUES (NEW.id, 'Getting Started', current_user_id)
RETURNING id INTO checklist_id;
INSERT INTO public.department_checklist_items (checklist_id, title, position)
VALUES
(checklist_id, 'Set up department goals', 0),
(checklist_id, 'Assign team members', 1),
(checklist_id, 'Create initial tasks', 2);
INSERT INTO public.department_notes (department_id, title, content, created_by)
VALUES (NEW.id, 'Welcome', 'Welcome to your department! Use this space for notes and documentation.', current_user_id);
RETURN NEW;
END;
$$;

View File

@@ -0,0 +1,41 @@
-- Fix: width must be 'full'|'half'|'third'|'two_thirds', not '1'
CREATE OR REPLACE FUNCTION public.create_department_dashboard()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
dash_id uuid;
checklist_id uuid;
note_id uuid;
current_user_id uuid := auth.uid();
BEGIN
INSERT INTO public.department_dashboards (department_id, created_by)
VALUES (NEW.id, current_user_id)
RETURNING id INTO dash_id;
INSERT INTO public.dashboard_panels (dashboard_id, module, position, width)
VALUES
(dash_id, 'kanban', 0, 'half'),
(dash_id, 'files', 1, 'half'),
(dash_id, 'checklist', 2, 'half'),
(dash_id, 'notes', 3, 'half');
INSERT INTO public.department_checklists (department_id, title, created_by)
VALUES (NEW.id, 'Getting Started', current_user_id)
RETURNING id INTO checklist_id;
INSERT INTO public.department_checklist_items (checklist_id, title, position)
VALUES
(checklist_id, 'Set up department goals', 0),
(checklist_id, 'Assign team members', 1),
(checklist_id, 'Create initial tasks', 2);
INSERT INTO public.department_notes (department_id, title, content, created_by)
VALUES (NEW.id, 'Welcome', 'Welcome to your department! Use this space for notes and documentation.', current_user_id);
RETURN NEW;
END;
$$;

View File

@@ -0,0 +1,40 @@
-- Fix: department_checklist_items uses "content" and "sort_order", not "title" and "position"
CREATE OR REPLACE FUNCTION public.create_department_dashboard()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
dash_id uuid;
checklist_id uuid;
current_user_id uuid := auth.uid();
BEGIN
INSERT INTO public.department_dashboards (department_id, created_by)
VALUES (NEW.id, current_user_id)
RETURNING id INTO dash_id;
INSERT INTO public.dashboard_panels (dashboard_id, module, position, width)
VALUES
(dash_id, 'kanban', 0, 'half'),
(dash_id, 'files', 1, 'half'),
(dash_id, 'checklist', 2, 'half'),
(dash_id, 'notes', 3, 'half');
INSERT INTO public.department_checklists (department_id, title, sort_order, created_by)
VALUES (NEW.id, 'Getting Started', 0, current_user_id)
RETURNING id INTO checklist_id;
INSERT INTO public.department_checklist_items (checklist_id, content, sort_order)
VALUES
(checklist_id, 'Set up department goals', 0),
(checklist_id, 'Assign team members', 1),
(checklist_id, 'Create initial tasks', 2);
INSERT INTO public.department_notes (department_id, title, content, sort_order, created_by)
VALUES (NEW.id, 'Welcome', 'Welcome to your department! Use this space for notes and documentation.', 0, current_user_id);
RETURN NEW;
END;
$$;

View File

@@ -0,0 +1,29 @@
-- Fix: only create dashboard + panels for the modules the user selected (enabled_modules).
-- No example checklists, notes, or other seed data.
CREATE OR REPLACE FUNCTION public.create_department_dashboard()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
dash_id uuid;
i int := 0;
mod text;
BEGIN
-- Create the dashboard
INSERT INTO public.department_dashboards (department_id, created_by)
VALUES (NEW.id, auth.uid())
RETURNING id INTO dash_id;
-- Create a panel for each enabled module
FOREACH mod IN ARRAY NEW.enabled_modules LOOP
INSERT INTO public.dashboard_panels (dashboard_id, module, position, width)
VALUES (dash_id, mod::public.module_type, i, 'half');
i := i + 1;
END LOOP;
RETURN NEW;
END;
$$;

View File

@@ -0,0 +1,108 @@
-- ============================================================
-- Map Layers & Pins: persistent map data for department map module
-- ============================================================
-- 1. Map Layers (each department can have multiple map layers)
CREATE TABLE map_layers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
department_id UUID NOT NULL REFERENCES event_departments(id) ON DELETE CASCADE,
name TEXT NOT NULL DEFAULT 'Map',
layer_type TEXT NOT NULL DEFAULT 'osm' CHECK (layer_type IN ('osm', 'image')),
image_url TEXT,
image_width INT,
image_height INT,
center_lat DOUBLE PRECISION DEFAULT 0,
center_lng DOUBLE PRECISION DEFAULT 0,
zoom_level INT DEFAULT 13,
sort_order INT NOT NULL DEFAULT 0,
created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_map_layers_dept ON map_layers(department_id);
-- 2. Map Pins (belong to a layer)
CREATE TABLE map_pins (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
layer_id UUID NOT NULL REFERENCES map_layers(id) ON DELETE CASCADE,
label TEXT NOT NULL,
description TEXT DEFAULT '',
color TEXT NOT NULL DEFAULT '#EF4444',
lat DOUBLE PRECISION NOT NULL,
lng DOUBLE PRECISION NOT NULL,
bounds_north DOUBLE PRECISION,
bounds_south DOUBLE PRECISION,
bounds_east DOUBLE PRECISION,
bounds_west DOUBLE PRECISION,
sort_order INT NOT NULL DEFAULT 0,
created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_map_pins_layer ON map_pins(layer_id);
-- 3. RLS
ALTER TABLE map_layers ENABLE ROW LEVEL SECURITY;
ALTER TABLE map_pins ENABLE ROW LEVEL SECURITY;
-- Map Layers: org members can view
CREATE POLICY "Org members can view map layers" ON map_layers FOR SELECT
USING (EXISTS (
SELECT 1 FROM event_departments ed
JOIN events e ON ed.event_id = e.id
JOIN org_members om ON e.org_id = om.org_id
WHERE ed.id = map_layers.department_id AND om.user_id = auth.uid()
));
-- Map Layers: dept members + editors can manage
CREATE POLICY "Dept members and editors can manage map layers" ON map_layers FOR ALL
USING (EXISTS (
SELECT 1 FROM event_departments ed
JOIN events e ON ed.event_id = e.id
JOIN org_members om ON e.org_id = om.org_id
WHERE ed.id = map_layers.department_id
AND om.user_id = auth.uid()
AND (
om.role IN ('owner', 'admin', 'editor')
OR EXISTS (
SELECT 1 FROM event_member_departments emd
JOIN event_members em ON emd.event_member_id = em.id
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
)
)
));
-- Map Pins: org members can view
CREATE POLICY "Org members can view map pins" ON map_pins FOR SELECT
USING (EXISTS (
SELECT 1 FROM map_layers ml
JOIN event_departments ed ON ml.department_id = ed.id
JOIN events e ON ed.event_id = e.id
JOIN org_members om ON e.org_id = om.org_id
WHERE ml.id = map_pins.layer_id AND om.user_id = auth.uid()
));
-- Map Pins: dept members + editors can manage
CREATE POLICY "Dept members and editors can manage map pins" ON map_pins FOR ALL
USING (EXISTS (
SELECT 1 FROM map_layers ml
JOIN event_departments ed ON ml.department_id = ed.id
JOIN events e ON ed.event_id = e.id
JOIN org_members om ON e.org_id = om.org_id
WHERE ml.id = map_pins.layer_id
AND om.user_id = auth.uid()
AND (
om.role IN ('owner', 'admin', 'editor')
OR EXISTS (
SELECT 1 FROM event_member_departments emd
JOIN event_members em ON emd.event_member_id = em.id
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
)
)
));
-- 4. Realtime
ALTER PUBLICATION supabase_realtime ADD TABLE map_layers;
ALTER PUBLICATION supabase_realtime ADD TABLE map_pins;

View File

@@ -0,0 +1,148 @@
-- ============================================================
-- Fix Map Layers & Pins RLS: replace FOR ALL with explicit
-- INSERT/UPDATE/DELETE policies that include WITH CHECK clauses
-- ============================================================
-- Drop the broken FOR ALL policies
DROP POLICY IF EXISTS "Dept members and editors can manage map layers" ON map_layers;
DROP POLICY IF EXISTS "Dept members and editors can manage map pins" ON map_pins;
-- ── Map Layers ──
CREATE POLICY "Dept members and editors can insert map layers" ON map_layers FOR INSERT
WITH CHECK (EXISTS (
SELECT 1 FROM event_departments ed
JOIN events e ON ed.event_id = e.id
JOIN org_members om ON e.org_id = om.org_id
WHERE ed.id = map_layers.department_id
AND om.user_id = auth.uid()
AND (
om.role IN ('owner', 'admin', 'editor')
OR EXISTS (
SELECT 1 FROM event_member_departments emd
JOIN event_members em ON emd.event_member_id = em.id
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
)
)
));
CREATE POLICY "Dept members and editors can update map layers" ON map_layers FOR UPDATE
USING (EXISTS (
SELECT 1 FROM event_departments ed
JOIN events e ON ed.event_id = e.id
JOIN org_members om ON e.org_id = om.org_id
WHERE ed.id = map_layers.department_id
AND om.user_id = auth.uid()
AND (
om.role IN ('owner', 'admin', 'editor')
OR EXISTS (
SELECT 1 FROM event_member_departments emd
JOIN event_members em ON emd.event_member_id = em.id
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
)
)
))
WITH CHECK (EXISTS (
SELECT 1 FROM event_departments ed
JOIN events e ON ed.event_id = e.id
JOIN org_members om ON e.org_id = om.org_id
WHERE ed.id = map_layers.department_id
AND om.user_id = auth.uid()
AND (
om.role IN ('owner', 'admin', 'editor')
OR EXISTS (
SELECT 1 FROM event_member_departments emd
JOIN event_members em ON emd.event_member_id = em.id
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
)
)
));
CREATE POLICY "Dept members and editors can delete map layers" ON map_layers FOR DELETE
USING (EXISTS (
SELECT 1 FROM event_departments ed
JOIN events e ON ed.event_id = e.id
JOIN org_members om ON e.org_id = om.org_id
WHERE ed.id = map_layers.department_id
AND om.user_id = auth.uid()
AND (
om.role IN ('owner', 'admin', 'editor')
OR EXISTS (
SELECT 1 FROM event_member_departments emd
JOIN event_members em ON emd.event_member_id = em.id
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
)
)
));
-- ── Map Pins ──
CREATE POLICY "Dept members and editors can insert map pins" ON map_pins FOR INSERT
WITH CHECK (EXISTS (
SELECT 1 FROM map_layers ml
JOIN event_departments ed ON ml.department_id = ed.id
JOIN events e ON ed.event_id = e.id
JOIN org_members om ON e.org_id = om.org_id
WHERE ml.id = map_pins.layer_id
AND om.user_id = auth.uid()
AND (
om.role IN ('owner', 'admin', 'editor')
OR EXISTS (
SELECT 1 FROM event_member_departments emd
JOIN event_members em ON emd.event_member_id = em.id
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
)
)
));
CREATE POLICY "Dept members and editors can update map pins" ON map_pins FOR UPDATE
USING (EXISTS (
SELECT 1 FROM map_layers ml
JOIN event_departments ed ON ml.department_id = ed.id
JOIN events e ON ed.event_id = e.id
JOIN org_members om ON e.org_id = om.org_id
WHERE ml.id = map_pins.layer_id
AND om.user_id = auth.uid()
AND (
om.role IN ('owner', 'admin', 'editor')
OR EXISTS (
SELECT 1 FROM event_member_departments emd
JOIN event_members em ON emd.event_member_id = em.id
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
)
)
))
WITH CHECK (EXISTS (
SELECT 1 FROM map_layers ml
JOIN event_departments ed ON ml.department_id = ed.id
JOIN events e ON ed.event_id = e.id
JOIN org_members om ON e.org_id = om.org_id
WHERE ml.id = map_pins.layer_id
AND om.user_id = auth.uid()
AND (
om.role IN ('owner', 'admin', 'editor')
OR EXISTS (
SELECT 1 FROM event_member_departments emd
JOIN event_members em ON emd.event_member_id = em.id
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
)
)
));
CREATE POLICY "Dept members and editors can delete map pins" ON map_pins FOR DELETE
USING (EXISTS (
SELECT 1 FROM map_layers ml
JOIN event_departments ed ON ml.department_id = ed.id
JOIN events e ON ed.event_id = e.id
JOIN org_members om ON e.org_id = om.org_id
WHERE ml.id = map_pins.layer_id
AND om.user_id = auth.uid()
AND (
om.role IN ('owner', 'admin', 'editor')
OR EXISTS (
SELECT 1 FROM event_member_departments emd
JOIN event_members em ON emd.event_member_id = em.id
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
)
)
));

View File

@@ -0,0 +1,109 @@
-- ============================================================
-- Map Shapes: polygons and rectangles for map layers
-- ============================================================
CREATE TABLE map_shapes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
layer_id UUID NOT NULL REFERENCES map_layers(id) ON DELETE CASCADE,
shape_type TEXT NOT NULL DEFAULT 'polygon' CHECK (shape_type IN ('polygon', 'rectangle')),
label TEXT NOT NULL DEFAULT '',
color TEXT NOT NULL DEFAULT '#6366f1',
fill_opacity DOUBLE PRECISION NOT NULL DEFAULT 0.15,
stroke_width DOUBLE PRECISION NOT NULL DEFAULT 2,
vertices JSONB NOT NULL DEFAULT '[]',
rotation DOUBLE PRECISION NOT NULL DEFAULT 0,
sort_order INT NOT NULL DEFAULT 0,
created_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
CREATE INDEX idx_map_shapes_layer ON map_shapes(layer_id);
-- RLS
ALTER TABLE map_shapes ENABLE ROW LEVEL SECURITY;
-- Select: org members can view
CREATE POLICY "Org members can view map shapes" ON map_shapes FOR SELECT
USING (EXISTS (
SELECT 1 FROM map_layers ml
JOIN event_departments ed ON ml.department_id = ed.id
JOIN events e ON ed.event_id = e.id
JOIN org_members om ON e.org_id = om.org_id
WHERE ml.id = map_shapes.layer_id AND om.user_id = auth.uid()
));
-- Insert
CREATE POLICY "Dept members and editors can insert map shapes" ON map_shapes FOR INSERT
WITH CHECK (EXISTS (
SELECT 1 FROM map_layers ml
JOIN event_departments ed ON ml.department_id = ed.id
JOIN events e ON ed.event_id = e.id
JOIN org_members om ON e.org_id = om.org_id
WHERE ml.id = map_shapes.layer_id
AND om.user_id = auth.uid()
AND (
om.role IN ('owner', 'admin', 'editor')
OR EXISTS (
SELECT 1 FROM event_member_departments emd
JOIN event_members em ON emd.event_member_id = em.id
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
)
)
));
-- Update
CREATE POLICY "Dept members and editors can update map shapes" ON map_shapes FOR UPDATE
USING (EXISTS (
SELECT 1 FROM map_layers ml
JOIN event_departments ed ON ml.department_id = ed.id
JOIN events e ON ed.event_id = e.id
JOIN org_members om ON e.org_id = om.org_id
WHERE ml.id = map_shapes.layer_id
AND om.user_id = auth.uid()
AND (
om.role IN ('owner', 'admin', 'editor')
OR EXISTS (
SELECT 1 FROM event_member_departments emd
JOIN event_members em ON emd.event_member_id = em.id
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
)
)
))
WITH CHECK (EXISTS (
SELECT 1 FROM map_layers ml
JOIN event_departments ed ON ml.department_id = ed.id
JOIN events e ON ed.event_id = e.id
JOIN org_members om ON e.org_id = om.org_id
WHERE ml.id = map_shapes.layer_id
AND om.user_id = auth.uid()
AND (
om.role IN ('owner', 'admin', 'editor')
OR EXISTS (
SELECT 1 FROM event_member_departments emd
JOIN event_members em ON emd.event_member_id = em.id
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
)
)
));
-- Delete
CREATE POLICY "Dept members and editors can delete map shapes" ON map_shapes FOR DELETE
USING (EXISTS (
SELECT 1 FROM map_layers ml
JOIN event_departments ed ON ml.department_id = ed.id
JOIN events e ON ed.event_id = e.id
JOIN org_members om ON e.org_id = om.org_id
WHERE ml.id = map_shapes.layer_id
AND om.user_id = auth.uid()
AND (
om.role IN ('owner', 'admin', 'editor')
OR EXISTS (
SELECT 1 FROM event_member_departments emd
JOIN event_members em ON emd.event_member_id = em.id
WHERE emd.department_id = ed.id AND em.user_id = auth.uid()
)
)
));
ALTER PUBLICATION supabase_realtime ADD TABLE map_shapes;