Files
root-org/src/lib/api/events.ts

278 lines
6.7 KiB
TypeScript

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: 'lead' | 'manager' | 'member';
assigned_at: string;
}
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<Database>,
orgId: string,
status?: string
): Promise<EventWithCounts[]> {
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<Database>,
eventId: string
): Promise<Event | null> {
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<Database>,
orgId: string,
eventSlug: string
): Promise<Event | null> {
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<Database>,
orgId: string,
userId: string,
params: {
name: string;
description?: string;
start_date?: string;
end_date?: string;
venue_name?: string;
venue_address?: string;
color?: string;
}
): Promise<Event> {
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<Database>,
eventId: string,
params: Partial<Pick<Event, 'name' | 'description' | 'status' | 'start_date' | 'end_date' | 'venue_name' | 'venue_address' | 'cover_image_url' | 'color'>>
): Promise<Event> {
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<Database>,
eventId: string
): Promise<void> {
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<Database>,
eventId: string
): Promise<(EventMember & { profile?: { id: string; email: string; full_name: string | null; avatar_url: string | null } })[]> {
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')
.in('id', userIds);
const profileMap = Object.fromEntries((profiles ?? []).map(p => [p.id, p]));
return members.map((m: any) => ({
...m,
profile: profileMap[m.user_id] ?? undefined,
}));
}
export async function addEventMember(
supabase: SupabaseClient<Database>,
eventId: string,
userId: string,
role: 'lead' | 'manager' | 'member' = 'member'
): Promise<EventMember> {
const { data, error } = await supabase
.from('event_members')
.upsert({ event_id: eventId, user_id: userId, role }, { 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<Database>,
eventId: string,
userId: string
): Promise<void> {
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;
}
}