278 lines
6.7 KiB
TypeScript
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;
|
|
}
|
|
}
|