From 1f2484da3d2a716268f188b51208fb6f13383802 Mon Sep 17 00:00:00 2001 From: AlacrisDevs Date: Sat, 7 Feb 2026 12:47:34 +0200 Subject: [PATCH] feat: event team management with departments and roles - Migration 023: event_roles, event_departments, event_member_departments tables - Auto-seed default roles (Head Organizer, Team Lead, Organizer, Volunteer, Sponsor) and departments (Logistics, IT & Tech, Marketing, Finance, Program, Sponsorship, Design, Volunteers) on event creation - API: full CRUD for roles, departments, member-department assignments - Enhanced fetchEventMembers with role + department resolution - Team page: sidebar with department filter + role legend, list/dept view toggle, add/edit/remove members with role + multi-department + notes - i18n: 25 new keys in EN and ET for team management - svelte-check: 0 errors, vitest: 112/112 passed --- messages/en.json | 24 + messages/et.json | 24 + src/lib/api/events.ts | 281 +++++- .../events/[eventSlug]/+layout.server.ts | 10 +- .../events/[eventSlug]/+layout.svelte | 13 +- .../[orgSlug]/events/[eventSlug]/+page.svelte | 13 +- .../events/[eventSlug]/team/+page.svelte | 936 ++++++++++++------ .../migrations/023_event_team_management.sql | 142 +++ 8 files changed, 1135 insertions(+), 308 deletions(-) create mode 100644 supabase/migrations/023_event_team_management.sql diff --git a/messages/en.json b/messages/en.json index 191b81f..2a07f22 100644 --- a/messages/en.json +++ b/messages/en.json @@ -341,6 +341,30 @@ "team_select_member": "Select a member", "team_select_role": "Select role", "team_already_assigned": "Already on team", + "team_departments": "Departments", + "team_roles": "Roles", + "team_all": "All", + "team_no_department": "Unassigned", + "team_add_department": "Add Department", + "team_add_role": "Add Role", + "team_edit_department": "Edit Department", + "team_edit_role": "Edit Role", + "team_dept_name": "Department name", + "team_role_name": "Role name", + "team_dept_created": "Department created", + "team_dept_updated": "Department updated", + "team_dept_deleted": "Department deleted", + "team_role_created": "Role created", + "team_role_updated": "Role updated", + "team_role_deleted": "Role deleted", + "team_dept_delete_confirm": "Delete department {name}? Members will be unassigned from it.", + "team_role_delete_confirm": "Delete role {name}? Members will lose this role assignment.", + "team_view_by_dept": "By department", + "team_view_list": "List view", + "team_member_count": "{count} members", + "team_assign_dept": "Assign departments", + "team_notes": "Notes", + "team_notes_placeholder": "Optional notes about this member...", "overview_subtitle": "Welcome back. Here's what's happening.", "overview_stat_events": "Events", "overview_upcoming_events": "Upcoming Events", diff --git a/messages/et.json b/messages/et.json index 79f59f6..70e5ec9 100644 --- a/messages/et.json +++ b/messages/et.json @@ -341,6 +341,30 @@ "team_select_member": "Vali liige", "team_select_role": "Vali roll", "team_already_assigned": "Juba meeskonnas", + "team_departments": "Osakonnad", + "team_roles": "Rollid", + "team_all": "Kõik", + "team_no_department": "Määramata", + "team_add_department": "Lisa osakond", + "team_add_role": "Lisa roll", + "team_edit_department": "Muuda osakonda", + "team_edit_role": "Muuda rolli", + "team_dept_name": "Osakonna nimi", + "team_role_name": "Rolli nimi", + "team_dept_created": "Osakond loodud", + "team_dept_updated": "Osakond uuendatud", + "team_dept_deleted": "Osakond kustutatud", + "team_role_created": "Roll loodud", + "team_role_updated": "Roll uuendatud", + "team_role_deleted": "Roll kustutatud", + "team_dept_delete_confirm": "Kustuta osakond {name}? Liikmed eemaldatakse sellest.", + "team_role_delete_confirm": "Kustuta roll {name}? Liikmed kaotavad selle rolli.", + "team_view_by_dept": "Osakondade järgi", + "team_view_list": "Nimekirja vaade", + "team_member_count": "{count} liiget", + "team_assign_dept": "Määra osakonnad", + "team_notes": "Märkmed", + "team_notes_placeholder": "Valikulised märkmed selle liikme kohta...", "overview_subtitle": "Tere tagasi. Siin on ülevaade toimuvast.", "overview_stat_events": "Üritused", "overview_upcoming_events": "Tulevased üritused", diff --git a/src/lib/api/events.ts b/src/lib/api/events.ts index ed5711f..a3fc8c5 100644 --- a/src/lib/api/events.ts +++ b/src/lib/api/events.ts @@ -27,9 +27,44 @@ export interface EventMember { event_id: string; user_id: string; role: 'lead' | 'manager' | 'member'; + role_id: string | null; + notes: string | null; assigned_at: string; } +export interface EventRole { + id: string; + event_id: string; + name: string; + color: string; + sort_order: number; + is_default: boolean; + created_at: string; +} + +export interface EventDepartment { + id: string; + event_id: string; + name: string; + color: string; + description: string | null; + sort_order: number; + created_at: string; +} + +export interface EventMemberDepartment { + id: string; + event_member_id: string; + department_id: string; + assigned_at: string; +} + +export interface EventMemberWithDetails extends EventMember { + profile?: { id: string; email: string; full_name: string | null; avatar_url: string | null }; + event_role?: EventRole; + departments: EventDepartment[]; +} + export interface EventWithCounts extends Event { member_count: number; } @@ -211,7 +246,7 @@ export async function deleteEvent( export async function fetchEventMembers( supabase: SupabaseClient, eventId: string -): Promise<(EventMember & { profile?: { id: string; email: string; full_name: string | null; avatar_url: string | null } })[]> { +): Promise { const { data: members, error } = await supabase .from('event_members') .select('*') @@ -234,9 +269,42 @@ export async function fetchEventMembers( const profileMap = Object.fromEntries((profiles ?? []).map(p => [p.id, p])); + // Fetch roles for this event + const { data: roles } = await (supabase as any) + .from('event_roles') + .select('*') + .eq('event_id', eventId); + const roleMap = Object.fromEntries((roles ?? []).map((r: any) => [r.id, r])); + + // Fetch member-department assignments + const memberIds = members.map((m: any) => m.id); + const { data: memberDepts } = await (supabase as any) + .from('event_member_departments') + .select('*') + .in('event_member_id', memberIds); + + // Fetch departments for this event + const { data: departments } = await (supabase as any) + .from('event_departments') + .select('*') + .eq('event_id', eventId); + const deptMap = Object.fromEntries((departments ?? []).map((d: any) => [d.id, d])); + + // Build member-to-departments map + const memberDeptMap: Record = {}; + for (const md of (memberDepts ?? [])) { + const dept = deptMap[(md as any).department_id]; + if (dept) { + if (!memberDeptMap[(md as any).event_member_id]) memberDeptMap[(md as any).event_member_id] = []; + memberDeptMap[(md as any).event_member_id].push(dept as unknown as EventDepartment); + } + } + return members.map((m: any) => ({ ...m, profile: profileMap[m.user_id] ?? undefined, + event_role: m.role_id ? (roleMap[m.role_id] as unknown as EventRole) ?? undefined : undefined, + departments: memberDeptMap[m.id] ?? [], })); } @@ -244,11 +312,17 @@ export async function addEventMember( supabase: SupabaseClient, eventId: string, userId: string, - role: 'lead' | 'manager' | 'member' = 'member' + params: { role?: 'lead' | 'manager' | 'member'; role_id?: string; notes?: string } = {} ): Promise { - const { data, error } = await supabase + const { data, error } = await (supabase as any) .from('event_members') - .upsert({ event_id: eventId, user_id: userId, role }, { onConflict: 'event_id,user_id' }) + .upsert({ + event_id: eventId, + user_id: userId, + role: params.role ?? 'member', + role_id: params.role_id ?? null, + notes: params.notes ?? null, + }, { onConflict: 'event_id,user_id' }) .select() .single(); @@ -275,3 +349,202 @@ export async function removeEventMember( throw error; } } + +// ============================================================ +// Event Roles +// ============================================================ + +export async function fetchEventRoles( + supabase: SupabaseClient, + eventId: string +): Promise { + const { data, error } = await (supabase as any) + .from('event_roles') + .select('*') + .eq('event_id', eventId) + .order('sort_order'); + + if (error) { + log.error('fetchEventRoles failed', { error, data: { eventId } }); + throw error; + } + return (data ?? []) as unknown as EventRole[]; +} + +export async function createEventRole( + supabase: SupabaseClient, + eventId: string, + params: { name: string; color?: string; sort_order?: number } +): Promise { + const { data, error } = await (supabase as any) + .from('event_roles') + .insert({ + event_id: eventId, + name: params.name, + color: params.color ?? '#6366f1', + sort_order: params.sort_order ?? 0, + }) + .select() + .single(); + + if (error) { + log.error('createEventRole failed', { error, data: { eventId, name: params.name } }); + throw error; + } + return data as unknown as EventRole; +} + +export async function updateEventRole( + supabase: SupabaseClient, + roleId: string, + params: Partial> +): Promise { + const { data, error } = await (supabase as any) + .from('event_roles') + .update(params) + .eq('id', roleId) + .select() + .single(); + + if (error) { + log.error('updateEventRole failed', { error, data: { roleId } }); + throw error; + } + return data as unknown as EventRole; +} + +export async function deleteEventRole( + supabase: SupabaseClient, + roleId: string +): Promise { + const { error } = await (supabase as any) + .from('event_roles') + .delete() + .eq('id', roleId); + + if (error) { + log.error('deleteEventRole failed', { error, data: { roleId } }); + throw error; + } +} + +// ============================================================ +// Event Departments +// ============================================================ + +export async function fetchEventDepartments( + supabase: SupabaseClient, + eventId: string +): Promise { + const { data, error } = await (supabase as any) + .from('event_departments') + .select('*') + .eq('event_id', eventId) + .order('sort_order'); + + if (error) { + log.error('fetchEventDepartments failed', { error, data: { eventId } }); + throw error; + } + return (data ?? []) as unknown as EventDepartment[]; +} + +export async function createEventDepartment( + supabase: SupabaseClient, + eventId: string, + params: { name: string; color?: string; description?: string; sort_order?: number } +): Promise { + const { data, error } = await (supabase as any) + .from('event_departments') + .insert({ + event_id: eventId, + name: params.name, + color: params.color ?? '#00A3E0', + description: params.description ?? null, + sort_order: params.sort_order ?? 0, + }) + .select() + .single(); + + if (error) { + log.error('createEventDepartment failed', { error, data: { eventId, name: params.name } }); + throw error; + } + return data as unknown as EventDepartment; +} + +export async function updateEventDepartment( + supabase: SupabaseClient, + deptId: string, + params: Partial> +): Promise { + const { data, error } = await (supabase as any) + .from('event_departments') + .update(params) + .eq('id', deptId) + .select() + .single(); + + if (error) { + log.error('updateEventDepartment failed', { error, data: { deptId } }); + throw error; + } + return data as unknown as EventDepartment; +} + +export async function deleteEventDepartment( + supabase: SupabaseClient, + deptId: string +): Promise { + const { error } = await (supabase as any) + .from('event_departments') + .delete() + .eq('id', deptId); + + if (error) { + log.error('deleteEventDepartment failed', { error, data: { deptId } }); + throw error; + } +} + +// ============================================================ +// Member-Department Assignments +// ============================================================ + +export async function assignMemberDepartment( + supabase: SupabaseClient, + eventMemberId: string, + departmentId: string +): Promise { + const { data, error } = await (supabase as any) + .from('event_member_departments') + .upsert( + { event_member_id: eventMemberId, department_id: departmentId }, + { onConflict: 'event_member_id,department_id' } + ) + .select() + .single(); + + if (error) { + log.error('assignMemberDepartment failed', { error, data: { eventMemberId, departmentId } }); + throw error; + } + return data as unknown as EventMemberDepartment; +} + +export async function unassignMemberDepartment( + supabase: SupabaseClient, + eventMemberId: string, + departmentId: string +): Promise { + const { error } = await (supabase as any) + .from('event_member_departments') + .delete() + .eq('event_member_id', eventMemberId) + .eq('department_id', departmentId); + + if (error) { + log.error('unassignMemberDepartment failed', { error, data: { eventMemberId, departmentId } }); + throw error; + } +} diff --git a/src/routes/[orgSlug]/events/[eventSlug]/+layout.server.ts b/src/routes/[orgSlug]/events/[eventSlug]/+layout.server.ts index 2b45be1..8d63c6e 100644 --- a/src/routes/[orgSlug]/events/[eventSlug]/+layout.server.ts +++ b/src/routes/[orgSlug]/events/[eventSlug]/+layout.server.ts @@ -1,6 +1,6 @@ import { error } from '@sveltejs/kit'; import type { LayoutServerLoad } from './$types'; -import { fetchEventBySlug, fetchEventMembers } from '$lib/api/events'; +import { fetchEventBySlug, fetchEventMembers, fetchEventRoles, fetchEventDepartments } from '$lib/api/events'; import { createLogger } from '$lib/utils/logger'; const log = createLogger('page.event-detail'); @@ -16,9 +16,13 @@ export const load: LayoutServerLoad = async ({ params, locals, parent }) => { const event = await fetchEventBySlug(locals.supabase, orgId, params.eventSlug); if (!event) error(404, 'Event not found'); - const members = await fetchEventMembers(locals.supabase, event.id); + const [members, roles, departments] = await Promise.all([ + fetchEventMembers(locals.supabase, event.id), + fetchEventRoles(locals.supabase, event.id), + fetchEventDepartments(locals.supabase, event.id), + ]); - return { event, eventMembers: members }; + return { event, eventMembers: members, eventRoles: roles, eventDepartments: departments }; } catch (e: any) { if (e?.status === 404) throw e; log.error('Failed to load event', { error: e, data: { orgId, eventSlug: params.eventSlug } }); diff --git a/src/routes/[orgSlug]/events/[eventSlug]/+layout.svelte b/src/routes/[orgSlug]/events/[eventSlug]/+layout.svelte index 702a6bb..1a56d39 100644 --- a/src/routes/[orgSlug]/events/[eventSlug]/+layout.svelte +++ b/src/routes/[orgSlug]/events/[eventSlug]/+layout.svelte @@ -2,7 +2,7 @@ import { page } from "$app/stores"; import { Avatar } from "$lib/components/ui"; import type { Snippet } from "svelte"; - import type { Event, EventMember } from "$lib/api/events"; + import type { Event, EventMemberWithDetails, EventRole, EventDepartment } from "$lib/api/events"; import * as m from "$lib/paraglide/messages"; interface Props { @@ -10,14 +10,9 @@ org: { id: string; name: string; slug: string }; userRole: string; event: Event; - eventMembers: (EventMember & { - profile?: { - id: string; - email: string; - full_name: string | null; - avatar_url: string | null; - }; - })[]; + eventMembers: EventMemberWithDetails[]; + eventRoles: EventRole[]; + eventDepartments: EventDepartment[]; }; children: Snippet; } diff --git a/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte b/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte index 41bd06e..bb94357 100644 --- a/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte +++ b/src/routes/[orgSlug]/events/[eventSlug]/+page.svelte @@ -5,7 +5,7 @@ import type { SupabaseClient } from "@supabase/supabase-js"; import type { Database } from "$lib/supabase/types"; import { toasts } from "$lib/stores/ui"; - import type { Event, EventMember } from "$lib/api/events"; + import type { Event, EventMemberWithDetails, EventRole, EventDepartment } from "$lib/api/events"; import * as m from "$lib/paraglide/messages"; interface Props { @@ -13,14 +13,9 @@ org: { id: string; name: string; slug: string }; userRole: string; event: Event; - eventMembers: (EventMember & { - profile?: { - id: string; - email: string; - full_name: string | null; - avatar_url: string | null; - }; - })[]; + eventMembers: EventMemberWithDetails[]; + eventRoles: EventRole[]; + eventDepartments: EventDepartment[]; }; } diff --git a/src/routes/[orgSlug]/events/[eventSlug]/team/+page.svelte b/src/routes/[orgSlug]/events/[eventSlug]/team/+page.svelte index ce37cfb..175a09b 100644 --- a/src/routes/[orgSlug]/events/[eventSlug]/team/+page.svelte +++ b/src/routes/[orgSlug]/events/[eventSlug]/team/+page.svelte @@ -1,10 +1,15 @@ @@ -196,15 +423,42 @@ {m.events_mod_team()} | {data.event.name} | {data.org.name} -
-
- -
+
+ +
+

{m.team_title()}

-

{m.team_subtitle()}

+

{m.team_member_count({ count: String(teamMembers.length) })}

+ +
+ + +
+
+
{#if isEditor} + +
+
- - {#if teamMembers.length === 0} -
- badge -

- {m.team_empty()} -

- {#if isEditor && availableMembers.length > 0} -
- -
- {/if} -
- {:else} -
-
- {#each teamMembers as member} -
-
- -
-

- {member.profile?.full_name || - member.profile?.email || - "Unknown"} -

-

- {member.profile?.email || ""} -

-
-
-
- {member.role} + + + + +
+ {#if teamMembers.length === 0} +
+ badge +

{m.team_empty()}

+ {#if isEditor && availableMembers.length > 0} +
+ +
+ {/if} +
+ {:else if viewMode === "dept"} + +
+ {#each membersByDept() as group} +
+
+ {#if group.dept} +
+

{group.dept.name}

+ {:else} + help_outline +

{m.team_no_department()}

+ {/if} + {group.members.length} +
+
+
+ {#each group.members as member} + {@render memberRow(member)} + {/each} +
+
+
+ {/each} +
+ {:else} + +
+
+ {#each filteredMembers() as member} + {@render memberRow(member)} + {/each} +
+
+ {/if} +
+{#snippet memberRow(member: EventMemberWithDetails)} +
+
+ +
+

+ {member.profile?.full_name || member.profile?.email || "Unknown"} +

+
+ {#if member.event_role} + {member.event_role.name} + {/if} + {#each member.departments as dept} + + + {dept.name} + + {/each} +
+
+
+ {#if isEditor} +
+ + +
+ {/if} +
+{/snippet} + - (showAddModal = false)} - title={m.team_add_member()} -> + (showAddModal = false)} title={m.team_add_member()}>
+
- - {#each availableMembers as om} - + {/each}
+
- - + + {#each roles as role} + {/each}
+ + {#if departments.length > 0} +
+ {m.team_assign_dept()} +
+ {#each departments as dept} + + {/each} +
+
+ {/if} + + +
+ + +
+
- -
- - (editingMember = null)} - title="Change Role" -> + + (editingMember = null)} title="Edit Member"> {#if editingMember}
- +
-

- {editingMember.profile?.full_name || - editingMember.profile?.email || - "Unknown"} -

-

- {editingMember.role} -

+

{editingMember.profile?.full_name || editingMember.profile?.email || "Unknown"}

+

{editingMember.profile?.email || ""}

+
- - + + {#each roles as role} + {/each}
-
- - + {/each} +
+
+ {/if} + + +
+ + +
+ +
+ +
{/if} + + (showDeptModal = false)} title={editingDept ? m.team_edit_department() : m.team_add_department()}> +
+
+ + +
+
+ Color +
+ {#each presetColors as c} + + {/each} +
+
+ {#if editingDept} + + {/if} +
+ + +
+
+
+ + + (showRoleModal = false)} title={editingRole ? m.team_edit_role() : m.team_add_role()}> +
+
+ + +
+
+ Color +
+ {#each presetColors as c} + + {/each} +
+
+ {#if editingRole} + + {/if} +
+ + +
+
+
+ {#if memberToRemove} @@ -459,29 +846,12 @@ aria-modal="true" aria-label={m.team_remove_btn()} > -
-

- {m.team_remove_btn()} -

-

- {m.team_remove_confirm({ name: getMemberName(memberToRemove) })} -

+
+

{m.team_remove_btn()}

+

{m.team_remove_confirm({ name: getMemberName(memberToRemove) })}

- - +
diff --git a/supabase/migrations/023_event_team_management.sql b/supabase/migrations/023_event_team_management.sql new file mode 100644 index 0000000..2b7afbe --- /dev/null +++ b/supabase/migrations/023_event_team_management.sql @@ -0,0 +1,142 @@ +-- Event Team Management: departments, roles, and member-department assignments +-- Supports real-world event teams where members have roles and belong to departments + +-- ============================================================ +-- 1. Event Roles: customizable per-event position types +-- ============================================================ +CREATE TABLE event_roles ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE, + name TEXT NOT NULL, + color TEXT NOT NULL DEFAULT '#6366f1', + sort_order INT NOT NULL DEFAULT 0, + is_default BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ DEFAULT now(), + UNIQUE(event_id, name) +); + +CREATE INDEX idx_event_roles_event ON event_roles(event_id); + +-- ============================================================ +-- 2. Event Departments: teams/areas within an event +-- ============================================================ +CREATE TABLE event_departments ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE, + name TEXT NOT NULL, + color TEXT NOT NULL DEFAULT '#00A3E0', + description TEXT, + sort_order INT NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT now(), + UNIQUE(event_id, name) +); + +CREATE INDEX idx_event_departments_event ON event_departments(event_id); + +-- ============================================================ +-- 3. Evolve event_members: add role_id FK, keep role text as fallback +-- ============================================================ +ALTER TABLE event_members + ADD COLUMN role_id UUID REFERENCES event_roles(id) ON DELETE SET NULL, + ADD COLUMN notes TEXT; + +-- ============================================================ +-- 4. Member-Department assignments (many-to-many) +-- ============================================================ +CREATE TABLE event_member_departments ( + id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + event_member_id UUID NOT NULL REFERENCES event_members(id) ON DELETE CASCADE, + department_id UUID NOT NULL REFERENCES event_departments(id) ON DELETE CASCADE, + assigned_at TIMESTAMPTZ DEFAULT now(), + UNIQUE(event_member_id, department_id) +); + +CREATE INDEX idx_emd_member ON event_member_departments(event_member_id); +CREATE INDEX idx_emd_department ON event_member_departments(department_id); + +-- ============================================================ +-- 5. RLS policies +-- ============================================================ +ALTER TABLE event_roles ENABLE ROW LEVEL SECURITY; +ALTER TABLE event_departments ENABLE ROW LEVEL SECURITY; +ALTER TABLE event_member_departments ENABLE ROW LEVEL SECURITY; + +-- Event roles: org members can view, editors+ can manage +CREATE POLICY "Org members can view event roles" ON event_roles FOR SELECT + USING (EXISTS ( + SELECT 1 FROM events e + JOIN org_members om ON e.org_id = om.org_id + WHERE e.id = event_roles.event_id AND om.user_id = auth.uid() + )); + +CREATE POLICY "Editors can manage event roles" ON event_roles FOR ALL + USING (EXISTS ( + SELECT 1 FROM events e + JOIN org_members om ON e.org_id = om.org_id + WHERE e.id = event_roles.event_id AND om.user_id = auth.uid() AND om.role IN ('owner', 'admin', 'editor') + )); + +-- Event departments: org members can view, editors+ can manage +CREATE POLICY "Org members can view event departments" ON event_departments FOR SELECT + USING (EXISTS ( + SELECT 1 FROM events e + JOIN org_members om ON e.org_id = om.org_id + WHERE e.id = event_departments.event_id AND om.user_id = auth.uid() + )); + +CREATE POLICY "Editors can manage event departments" ON event_departments FOR ALL + USING (EXISTS ( + SELECT 1 FROM events e + JOIN org_members om ON e.org_id = om.org_id + WHERE e.id = event_departments.event_id AND om.user_id = auth.uid() AND om.role IN ('owner', 'admin', 'editor') + )); + +-- Member-department assignments: org members can view, editors+ can manage +CREATE POLICY "Org members can view member departments" ON event_member_departments FOR SELECT + USING (EXISTS ( + SELECT 1 FROM event_members em + JOIN events e ON em.event_id = e.id + JOIN org_members om ON e.org_id = om.org_id + WHERE em.id = event_member_departments.event_member_id AND om.user_id = auth.uid() + )); + +CREATE POLICY "Editors can manage member departments" ON event_member_departments FOR ALL + USING (EXISTS ( + SELECT 1 FROM event_members em + JOIN events e ON em.event_id = e.id + JOIN org_members om ON e.org_id = om.org_id + WHERE em.id = event_member_departments.event_member_id AND om.user_id = auth.uid() AND om.role IN ('owner', 'admin', 'editor') + )); + +-- ============================================================ +-- 6. Auto-seed default roles when a new event is created +-- ============================================================ +CREATE OR REPLACE FUNCTION public.seed_event_defaults() +RETURNS TRIGGER AS $$ +BEGIN + -- Default roles (generalized from real event data) + INSERT INTO public.event_roles (event_id, name, color, sort_order, is_default) VALUES + (NEW.id, 'Head Organizer', '#EF4444', 0, false), + (NEW.id, 'Team Lead', '#8B5CF6', 1, false), + (NEW.id, 'Organizer', '#F59E0B', 2, true), + (NEW.id, 'Volunteer', '#10B981', 3, false), + (NEW.id, 'Sponsor', '#00A3E0', 4, false); + + -- Default departments (generalized from real event data) + INSERT INTO public.event_departments (event_id, name, color, sort_order) VALUES + (NEW.id, 'Logistics', '#F59E0B', 0), + (NEW.id, 'IT & Tech', '#6366F1', 1), + (NEW.id, 'Marketing', '#EC4899', 2), + (NEW.id, 'Finance', '#10B981', 3), + (NEW.id, 'Program', '#8B5CF6', 4), + (NEW.id, 'Sponsorship', '#00A3E0', 5), + (NEW.id, 'Design', '#F97316', 6), + (NEW.id, 'Volunteers', '#14B8A6', 7); + + RETURN NEW; +END; +$$ LANGUAGE plpgsql SECURITY DEFINER; + +CREATE TRIGGER on_event_created_seed_defaults + AFTER INSERT ON events + FOR EACH ROW EXECUTE FUNCTION public.seed_event_defaults();