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

571 lines
14 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: 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<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<EventMemberWithDetails[]> {
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<string, EventDepartment[]> = {};
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<Database>,
eventId: string,
userId: string,
params: { role?: 'lead' | 'manager' | 'member'; role_id?: string; notes?: string } = {}
): Promise<EventMember> {
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<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;
}
}
// ============================================================
// Event Roles
// ============================================================
export async function fetchEventRoles(
supabase: SupabaseClient<Database>,
eventId: string
): Promise<EventRole[]> {
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<Database>,
eventId: string,
params: { name: string; color?: string; sort_order?: number }
): Promise<EventRole> {
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<Database>,
roleId: string,
params: Partial<Pick<EventRole, 'name' | 'color' | 'sort_order' | 'is_default'>>
): Promise<EventRole> {
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<Database>,
roleId: string
): Promise<void> {
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<Database>,
eventId: string
): Promise<EventDepartment[]> {
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<Database>,
eventId: string,
params: { name: string; color?: string; description?: string; sort_order?: number }
): Promise<EventDepartment> {
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<Database>,
deptId: string,
params: Partial<Pick<EventDepartment, 'name' | 'color' | 'description' | 'sort_order'>>
): Promise<EventDepartment> {
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<Database>,
deptId: string,
plannedBudget: number
): Promise<EventDepartment> {
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<Database>,
deptId: string
): Promise<void> {
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<Database>,
eventMemberId: string,
departmentId: string
): Promise<EventMemberDepartment> {
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<Database>,
eventMemberId: string,
departmentId: string
): Promise<void> {
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;
}
}