import type { SupabaseClient } from '@supabase/supabase-js'; import type { Database } from '$lib/supabase/types'; import { createLogger } from '$lib/utils/logger'; const log = createLogger('api.events'); export interface Event { id: string; org_id: string; name: string; slug: string; description: string | null; status: 'planning' | 'active' | 'completed' | 'archived'; start_date: string | null; end_date: string | null; venue_name: string | null; venue_address: string | null; cover_image_url: string | null; color: string | null; created_by: string | null; created_at: string; updated_at: string; } export interface EventMember { id: string; event_id: string; user_id: string; role: string; role_id: string | null; notes: string | null; assigned_at: string | null; } export interface EventRole { id: string; event_id: string; name: string; color: string; sort_order: number; is_default: boolean; created_at: string | null; } export interface EventDepartment { id: string; event_id: string; name: string; color: string; description: string | null; planned_budget: number; sort_order: number; created_at: string | null; } export interface EventMemberDepartment { id: string; event_member_id: string; department_id: string; assigned_at: string | null; } export interface EventMemberWithDetails extends EventMember { profile?: { id: string; email: string; full_name: string | null; avatar_url: string | null; phone: string | null; discord_handle: string | null; shirt_size: string | null; hoodie_size: string | null }; event_role?: EventRole; departments: EventDepartment[]; } export interface EventWithCounts extends Event { member_count: number; } function slugify(text: string): string { return text .toLowerCase() .replace(/[^\w\s-]/g, '') .replace(/[\s_]+/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, '') .slice(0, 60) || 'event'; } export async function fetchEvents( supabase: SupabaseClient, orgId: string, status?: string ): Promise { let query = supabase .from('events') .select('*, event_members(count)') .eq('org_id', orgId) .order('start_date', { ascending: true, nullsFirst: false }); if (status && status !== 'all') { query = query.eq('status', status); } const { data, error } = await query; if (error) { log.error('fetchEvents failed', { error, data: { orgId } }); throw error; } const events: EventWithCounts[] = (data ?? []).map((e: any) => ({ ...e, member_count: e.event_members?.[0]?.count ?? 0, event_members: undefined, })); log.debug('fetchEvents ok', { data: { count: events.length } }); return events; } export async function fetchEvent( supabase: SupabaseClient, eventId: string ): Promise { const { data, error } = await supabase .from('events') .select('*') .eq('id', eventId) .single(); if (error) { if (error.code === 'PGRST116') return null; log.error('fetchEvent failed', { error, data: { eventId } }); throw error; } return data as unknown as Event; } export async function fetchEventBySlug( supabase: SupabaseClient, orgId: string, eventSlug: string ): Promise { const { data, error } = await supabase .from('events') .select('*') .eq('org_id', orgId) .eq('slug', eventSlug) .single(); if (error) { if (error.code === 'PGRST116') return null; log.error('fetchEventBySlug failed', { error, data: { orgId, eventSlug } }); throw error; } return data as unknown as Event; } export async function createEvent( supabase: SupabaseClient, orgId: string, userId: string, params: { name: string; description?: string; start_date?: string; end_date?: string; venue_name?: string; venue_address?: string; color?: string; } ): Promise { const baseSlug = slugify(params.name); // Ensure unique slug within org const { data: existing } = await supabase .from('events') .select('slug') .eq('org_id', orgId) .like('slug', `${baseSlug}%`); let slug = baseSlug; if (existing && existing.length > 0) { const existingSlugs = new Set(existing.map((e: any) => e.slug)); if (existingSlugs.has(slug)) { let i = 2; while (existingSlugs.has(`${baseSlug}-${i}`)) i++; slug = `${baseSlug}-${i}`; } } const { data, error } = await supabase .from('events') .insert({ org_id: orgId, name: params.name, slug, description: params.description ?? null, start_date: params.start_date ?? null, end_date: params.end_date ?? null, venue_name: params.venue_name ?? null, venue_address: params.venue_address ?? null, color: params.color ?? null, created_by: userId, }) .select() .single(); if (error) { log.error('createEvent failed', { error, data: { orgId, name: params.name } }); throw error; } log.info('createEvent ok', { data: { id: data.id, name: params.name, slug } }); return data as unknown as Event; } export async function updateEvent( supabase: SupabaseClient, eventId: string, params: Partial> ): Promise { const { data, error } = await supabase .from('events') .update({ ...params, updated_at: new Date().toISOString() }) .eq('id', eventId) .select() .single(); if (error) { log.error('updateEvent failed', { error, data: { eventId } }); throw error; } log.info('updateEvent ok', { data: { id: data.id } }); return data as unknown as Event; } export async function deleteEvent( supabase: SupabaseClient, eventId: string ): Promise { const { error } = await supabase .from('events') .delete() .eq('id', eventId); if (error) { log.error('deleteEvent failed', { error, data: { eventId } }); throw error; } log.info('deleteEvent ok', { data: { eventId } }); } export async function fetchEventMembers( supabase: SupabaseClient, eventId: string ): Promise { const { data: members, error } = await supabase .from('event_members') .select('*') .eq('event_id', eventId) .order('assigned_at'); if (error) { log.error('fetchEventMembers failed', { error, data: { eventId } }); throw error; } if (!members || members.length === 0) return []; // Fetch profiles separately (same pattern as org_members) const userIds = members.map((m: any) => m.user_id); const { data: profiles } = await supabase .from('profiles') .select('id, email, full_name, avatar_url, phone, discord_handle, shirt_size, hoodie_size') .in('id', userIds); const profileMap = Object.fromEntries((profiles ?? []).map(p => [p.id, p])); // Fetch roles for this event const { data: roles } = await supabase .from('event_roles') .select('*') .eq('event_id', eventId); const roleMap = Object.fromEntries((roles ?? []).map(r => [r.id, r])); // Fetch member-department assignments const memberIds = members.map((m: any) => m.id); const { data: memberDepts } = await supabase .from('event_member_departments') .select('*') .in('event_member_id', memberIds); // Fetch departments for this event const { data: departments } = await supabase .from('event_departments') .select('*') .eq('event_id', eventId); const deptMap = Object.fromEntries((departments ?? []).map(d => [d.id, d])); // Build member-to-departments map const memberDeptMap: Record = {}; for (const md of (memberDepts ?? [])) { const dept = deptMap[md.department_id]; if (dept) { if (!memberDeptMap[md.event_member_id]) memberDeptMap[md.event_member_id] = []; memberDeptMap[md.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] ?? [], })); } export async function addEventMember( supabase: SupabaseClient, eventId: string, userId: string, params: { role?: 'lead' | 'manager' | 'member'; role_id?: string; notes?: string } = {} ): Promise { const { data, error } = await supabase .from('event_members') .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(); if (error) { log.error('addEventMember failed', { error, data: { eventId, userId } }); throw error; } return data as unknown as EventMember; } export async function removeEventMember( supabase: SupabaseClient, eventId: string, userId: string ): Promise { const { error } = await supabase .from('event_members') .delete() .eq('event_id', eventId) .eq('user_id', userId); if (error) { log.error('removeEventMember failed', { error, data: { eventId, userId } }); throw error; } } // ============================================================ // Event Roles // ============================================================ export async function fetchEventRoles( supabase: SupabaseClient, eventId: string ): Promise { const { data, error } = await supabase .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 .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 .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 .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 .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 .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 .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 updateDepartmentPlannedBudget( supabase: SupabaseClient, deptId: string, plannedBudget: number ): Promise { const { data, error } = await (supabase as any) .from('event_departments') .update({ planned_budget: plannedBudget }) .eq('id', deptId) .select() .single(); if (error) { log.error('updateDepartmentPlannedBudget failed', { error, data: { deptId, plannedBudget } }); throw error; } return data as unknown as EventDepartment; } export async function deleteEventDepartment( supabase: SupabaseClient, deptId: string ): Promise { const { error } = await supabase .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 .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 .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; } }