feat: Phase 1 - Events entity (migration, API, list page, detail layout with module sidebar, overview page)
This commit is contained in:
277
src/lib/api/events.ts
Normal file
277
src/lib/api/events.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user