diff --git a/server/api/events/index.get.ts b/server/api/events/index.get.ts index 6c59fa3..4f9e739 100644 --- a/server/api/events/index.get.ts +++ b/server/api/events/index.get.ts @@ -1,24 +1,39 @@ // server/api/events/index.get.ts import { createNocoDBEventsClient, transformEventForCalendar } from '~/server/utils/nocodb-events'; +import { createSessionManager } from '~/server/utils/session'; import type { EventFilters } from '~/utils/types'; export default defineEventHandler(async (event) => { + console.log('[api/events.get] ========================='); + console.log('[api/events.get] GET /api/events - List all events'); + console.log('[api/events.get] Request from:', getClientIP(event)); + try { const query = getQuery(event) as EventFilters & { limit?: string; offset?: string; - calendar_format?: string + calendar_format?: string; + force?: string; }; - // Get user session for role-based filtering - const session = await getUserSession(event); - if (!session || !session.user) { + console.log('[api/events.get] Query parameters:', query); + + // Get user session using the working session manager + const sessionManager = createSessionManager(); + const cookieHeader = getHeader(event, 'cookie'); + const session = sessionManager.getSession(cookieHeader); + + if (!session) { + console.log('[api/events.get] ❌ No valid session found'); throw createError({ statusCode: 401, statusMessage: 'Authentication required' }); } + console.log('[api/events.get] ✅ Valid session found for user:', session.user.email); + console.log('[api/events.get] User tier:', session.user.tier); + const eventsClient = createNocoDBEventsClient(); // Build filters with user role @@ -39,9 +54,13 @@ export default defineEventHandler(async (event) => { filters.end_date = endDate.toISOString(); } + console.log('[api/events.get] Fetching events with filters:', filters); + // Get events from database const response = await eventsClient.findUserEvents(session.user.id, filters); + console.log('[api/events.get] ✅ Successfully fetched', response.list.length, 'events'); + // Transform for FullCalendar if requested if (query.calendar_format === 'true') { const calendarEvents = response.list.map(transformEventForCalendar); @@ -59,32 +78,17 @@ export default defineEventHandler(async (event) => { pagination: response.PageInfo }; - } catch (error) { - console.error('Error fetching events:', error); + } catch (error: any) { + console.error('[api/events.get] ❌ Error fetching events:', error); + + // Re-throw createError instances + if (error.statusCode) { + throw error; + } + throw createError({ statusCode: 500, statusMessage: 'Failed to fetch events' }); } }); - -// Helper function to get user session (you may need to adjust this based on your auth implementation) -async function getUserSession(event: any) { - // This should be replaced with your actual session retrieval logic - // For now, assuming you have a session utility similar to your auth system - try { - const sessionCookie = getCookie(event, 'session') || getHeader(event, 'authorization'); - if (!sessionCookie) return null; - - // Decode session - adjust based on your session implementation - // This is a placeholder that should be replaced with your actual session logic - return { - user: { - id: 'user-id', // This should come from your session - tier: 'user' // This should come from your session - } - }; - } catch { - return null; - } -} diff --git a/server/api/events/index.post.ts b/server/api/events/index.post.ts index 8f1acb6..ffbd29b 100644 --- a/server/api/events/index.post.ts +++ b/server/api/events/index.post.ts @@ -1,22 +1,43 @@ // server/api/events/index.post.ts import { createNocoDBEventsClient } from '~/server/utils/nocodb-events'; +import { createSessionManager } from '~/server/utils/session'; import type { EventCreateRequest } from '~/utils/types'; export default defineEventHandler(async (event) => { + console.log('[api/events.post] ========================='); + console.log('[api/events.post] POST /api/events - Create event'); + console.log('[api/events.post] Request from:', getClientIP(event)); + try { const body = await readBody(event) as EventCreateRequest; - // Get user session for authentication and authorization - const session = await getUserSession(event); - if (!session || !session.user) { + console.log('[api/events.post] Event data received:', { + title: body.title, + event_type: body.event_type, + start_datetime: body.start_datetime, + end_datetime: body.end_datetime, + visibility: body.visibility + }); + + // Get user session using the working session manager + const sessionManager = createSessionManager(); + const cookieHeader = getHeader(event, 'cookie'); + const session = sessionManager.getSession(cookieHeader); + + if (!session) { + console.log('[api/events.post] ❌ No valid session found'); throw createError({ statusCode: 401, statusMessage: 'Authentication required' }); } + console.log('[api/events.post] ✅ Valid session found for user:', session.user.email); + console.log('[api/events.post] User tier:', session.user.tier); + // Check if user has permission to create events (board or admin only) if (session.user.tier !== 'board' && session.user.tier !== 'admin') { + console.log('[api/events.post] ❌ Insufficient permissions. User tier:', session.user.tier); throw createError({ statusCode: 403, statusMessage: 'Only board members and administrators can create events' @@ -25,6 +46,7 @@ export default defineEventHandler(async (event) => { // Validate required fields if (!body.title || !body.start_datetime || !body.end_datetime) { + console.log('[api/events.post] ❌ Missing required fields'); throw createError({ statusCode: 400, statusMessage: 'Title, start date, and end date are required' @@ -36,6 +58,7 @@ export default defineEventHandler(async (event) => { const endDate = new Date(body.end_datetime); if (startDate >= endDate) { + console.log('[api/events.post] ❌ Invalid date range'); throw createError({ statusCode: 400, statusMessage: 'End date must be after start date' @@ -45,6 +68,7 @@ export default defineEventHandler(async (event) => { // Validate event type const validEventTypes = ['meeting', 'social', 'fundraiser', 'workshop', 'board-only']; if (!validEventTypes.includes(body.event_type)) { + console.log('[api/events.post] ❌ Invalid event type:', body.event_type); throw createError({ statusCode: 400, statusMessage: 'Invalid event type' @@ -54,6 +78,7 @@ export default defineEventHandler(async (event) => { // Validate visibility const validVisibilities = ['public', 'board-only', 'admin-only']; if (!validVisibilities.includes(body.visibility)) { + console.log('[api/events.post] ❌ Invalid visibility:', body.visibility); throw createError({ statusCode: 400, statusMessage: 'Invalid visibility setting' @@ -62,19 +87,22 @@ export default defineEventHandler(async (event) => { // Admin-only visibility can only be set by admins if (body.visibility === 'admin-only' && session.user.tier !== 'admin') { + console.log('[api/events.post] ❌ Admin-only event creation attempted by non-admin'); throw createError({ statusCode: 403, statusMessage: 'Only administrators can create admin-only events' }); } + console.log('[api/events.post] ✅ Validation passed, creating event...'); + const eventsClient = createNocoDBEventsClient(); // Prepare event data const eventData = { title: body.title.trim(), description: body.description?.trim() || '', - event_type: body.event_type, + event_type: body.event_type as 'meeting' | 'social' | 'fundraiser' | 'workshop' | 'board-only', start_datetime: body.start_datetime, end_datetime: body.end_datetime, location: body.location?.trim() || '', @@ -85,23 +113,27 @@ export default defineEventHandler(async (event) => { cost_members: body.cost_members || '', cost_non_members: body.cost_non_members || '', member_pricing_enabled: body.member_pricing_enabled || 'true', - visibility: body.visibility, - status: body.status || 'active', + visibility: body.visibility as 'public' | 'board-only' | 'admin-only', + status: (body.status || 'active') as 'active' | 'cancelled' | 'completed' | 'draft', creator: session.user.id, - current_attendees: '0' + current_attendees: 0 }; + console.log('[api/events.post] Event data prepared:', Object.keys(eventData)); + // Create the event const newEvent = await eventsClient.create(eventData); + console.log('[api/events.post] ✅ Event created successfully with ID:', newEvent.id); + return { success: true, data: newEvent, message: 'Event created successfully' }; - } catch (error) { - console.error('Error creating event:', error); + } catch (error: any) { + console.error('[api/events.post] ❌ Error creating event:', error); // Re-throw createError instances if (error.statusCode) { @@ -114,20 +146,3 @@ export default defineEventHandler(async (event) => { }); } }); - -// Helper function to get user session (same as in index.get.ts) -async function getUserSession(event: any) { - try { - const sessionCookie = getCookie(event, 'session') || getHeader(event, 'authorization'); - if (!sessionCookie) return null; - - return { - user: { - id: 'user-id', // Replace with actual session logic - tier: 'board' // Replace with actual session logic - } - }; - } catch { - return null; - } -} diff --git a/server/utils/nocodb-events.ts b/server/utils/nocodb-events.ts index f135575..8b53439 100644 --- a/server/utils/nocodb-events.ts +++ b/server/utils/nocodb-events.ts @@ -1,302 +1,345 @@ // server/utils/nocodb-events.ts import type { Event, EventRSVP, EventsResponse, EventFilters } from '~/utils/types'; +import { createSessionManager } from '~/server/utils/session'; + +// Import shared NocoDB utilities from the working members system +import { getNocoDbConfiguration, setGlobalNocoDBConfig, handleNocoDbError } from '~/server/utils/nocodb'; + +// Define interfaces for API responses +interface EventsListResponse { + list: Event[]; + PageInfo: { + pageSize: number; + totalRows: number; + isFirstPage: boolean; + isLastPage: boolean; + page: number; + }; +} + +// Table ID enumeration for Events (similar to Members table pattern) +export enum EventTable { + Events = "events-table-id", // Will be configured via admin panel + EventRSVPs = "rsvps-table-id" // Separate table for RSVPs +} + +// Dynamic table ID getter - will use configured table ID from admin panel +export const getEventTableId = (tableName: 'Events' | 'EventRSVPs'): string => { + // Try to get table ID from global configuration first + const globalConfig = (global as any).globalNocoDBConfig; + if (globalConfig?.tables) { + const tableKey = tableName === 'Events' ? 'events' : 'event_rsvps'; + const tableId = globalConfig.tables[tableKey] || globalConfig.tables[tableName]; + if (tableId) { + console.log(`[nocodb-events] Using global table ID for ${tableName}:`, tableId); + return tableId; + } + } + + // Try runtime config as fallback + const config = useRuntimeConfig(); + if (tableName === 'Events' && config.nocodb?.eventsTableId) { + console.log(`[nocodb-events] Using runtime config table ID for ${tableName}:`, config.nocodb.eventsTableId); + return config.nocodb.eventsTableId; + } + + // Final fallback to default + const defaultTableId = tableName === 'Events' ? 'mt1mx3vkcw0vbmh' : 'rsvps-table-id'; + console.log(`[nocodb-events] Using fallback table ID for ${tableName}:`, defaultTableId); + return defaultTableId; +}; + +export const createEventTableUrl = (table: EventTable | string) => { + let tableId: string; + + if (table === EventTable.Events || table === 'Events') { + tableId = getEventTableId('Events'); + } else if (table === EventTable.EventRSVPs || table === 'EventRSVPs') { + tableId = getEventTableId('EventRSVPs'); + } else { + tableId = table.toString(); + } + + const url = `${getNocoDbConfiguration().url}/api/v2/tables/${tableId}/records`; + console.log('[nocodb-events] Event table URL:', url); + return url; +}; + +// Event field normalization (similar to member normalization) +export const normalizeEventFieldsFromNocoDB = (data: any): Event => { + console.log('[normalizeEventFieldsFromNocoDB] Input event keys:', Object.keys(data)); + + const normalized: any = { ...data }; + + // Field mapping for event data (adjust based on your actual NocoDB schema) + const eventFieldMap: Record = { + 'Title': 'title', + 'Description': 'description', + 'Event Type': 'event_type', + 'Start Date': 'start_datetime', + 'End Date': 'end_datetime', + 'Location': 'location', + 'Visibility': 'visibility', + 'Status': 'status', + 'Creator': 'creator', + 'Max Attendees': 'max_attendees', + 'Is Paid': 'is_paid', + 'Cost Members': 'cost_members', + 'Cost Non Members': 'cost_non_members', + // Handle snake_case fields (in case they come in already normalized) + 'title': 'title', + 'description': 'description', + 'event_type': 'event_type', + 'start_datetime': 'start_datetime', + 'end_datetime': 'end_datetime', + 'location': 'location', + 'visibility': 'visibility', + 'status': 'status', + 'creator': 'creator', + 'max_attendees': 'max_attendees', + 'is_paid': 'is_paid', + 'cost_members': 'cost_members', + 'cost_non_members': 'cost_non_members' + }; + + // Apply field mapping + for (const [sourceKey, targetKey] of Object.entries(eventFieldMap)) { + if (sourceKey in data && data[sourceKey] !== undefined && data[sourceKey] !== null) { + normalized[targetKey] = data[sourceKey]; + console.log(`[normalizeEventFieldsFromNocoDB] Mapped "${sourceKey}" -> "${targetKey}":`, data[sourceKey]); + } + } + + // Ensure required fields exist with fallbacks + normalized.title = normalized.title || normalized['Title'] || ''; + normalized.status = normalized.status || normalized['Status'] || 'active'; + normalized.visibility = normalized.visibility || normalized['Visibility'] || 'public'; + + console.log('[normalizeEventFieldsFromNocoDB] Final normalized event keys:', Object.keys(normalized)); + return normalized as Event; +}; /** * Creates a client for interacting with the Events NocoDB table - * Provides CRUD operations and specialized queries for events and RSVPs + * Following the same pattern as the working members client */ export function createNocoDBEventsClient() { - const config = useRuntimeConfig(); - - const baseUrl = config.nocodb.url; - const token = config.nocodb.token; - const eventsBaseId = config.nocodb.eventsBaseId; - const eventsTableId = config.nocodb.eventsTableId || 'events'; // fallback to table name - - if (!baseUrl || !token || !eventsBaseId) { - throw new Error('Events NocoDB configuration is incomplete. Please check environment variables.'); - } - - const headers = { - 'xc-token': token, - 'Content-Type': 'application/json' - }; - const eventsClient = { /** * Find all events with optional filtering */ - async findAll(filters?: EventFilters & { limit?: number; offset?: number }) { - const queryParams = new URLSearchParams(); + async findAll(filters?: EventFilters & { limit?: number; offset?: number }): Promise { + console.log('[nocodb-events] 🔍 DEBUG: Filters received:', filters); + const startTime = Date.now(); - if (filters?.limit) queryParams.set('limit', filters.limit.toString()); - if (filters?.offset) queryParams.set('offset', filters.offset.toString()); - - // Build where clause for filtering - const whereConditions: string[] = []; - - if (filters?.start_date && filters?.end_date) { - whereConditions.push(`(start_datetime >= '${filters.start_date}' AND start_datetime <= '${filters.end_date}')`); - } - - if (filters?.event_type) { - whereConditions.push(`(event_type = '${filters.event_type}')`); - } - - if (filters?.visibility) { - whereConditions.push(`(visibility = '${filters.visibility}')`); - } else if (filters?.user_role) { - // Role-based visibility filtering - if (filters.user_role === 'user') { - whereConditions.push(`(visibility = 'public')`); - } else if (filters.user_role === 'board') { - whereConditions.push(`(visibility = 'public' OR visibility = 'board-only')`); + try { + // Build query parameters like the members system + const params: Record = { + limit: filters?.limit || 1000, + }; + + if (filters?.offset) { + params.offset = filters.offset; } - // Admin sees all events (no filter) + + // Simple status filtering (like members system) + if (filters?.status) { + console.log('[nocodb-events] 🔍 Adding status filter:', filters.status); + params.where = `(status,eq,${filters.status})`; + } else { + console.log('[nocodb-events] 🔍 No status filter - fetching all records'); + } + + // TEMPORARILY DISABLE COMPLEX FILTERING TO ISOLATE ISSUE + console.log('[nocodb-events] ⚠️ Temporarily skipping date/role/search filtering to isolate issue'); + + // TEMPORARILY DISABLE SORTING TO ISOLATE ISSUE + console.log('[nocodb-events] ⚠️ Also temporarily skipping sorting to isolate issue'); + + const url = createEventTableUrl(EventTable.Events); + + const result = await $fetch(url, { + headers: { + "xc-token": getNocoDbConfiguration().token, + }, + params + }); + + console.log('[nocodb-events] ✅ Successfully fetched events, count:', result.list?.length || 0); + console.log('[nocodb-events] Request duration:', Date.now() - startTime, 'ms'); + + // Apply field normalization like members system + if (result.list) { + result.list = result.list.map(normalizeEventFieldsFromNocoDB); + } + + return result; + } catch (error: any) { + console.error('[nocodb-events] ❌ Error fetching events:', error); + handleNocoDbError(error, 'getEvents', 'Events'); + throw error; } - - if (filters?.status) { - whereConditions.push(`(status = '${filters.status}')`); - } else { - // Default to active events only - whereConditions.push(`(status = 'active')`); - } - - if (filters?.search) { - whereConditions.push(`(title LIKE '%${filters.search}%' OR description LIKE '%${filters.search}%')`); - } - - if (whereConditions.length > 0) { - queryParams.set('where', whereConditions.join(' AND ')); - } - - // Sort by start date - queryParams.set('sort', 'start_datetime'); - - const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}?${queryParams.toString()}`; - - const response = await $fetch(url, { - method: 'GET', - headers - }); - - return response; }, /** * Find a single event by ID */ async findOne(id: string) { - const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}/${id}`; + console.log('[nocodb-events] Fetching event ID:', id); - return await $fetch(url, { - method: 'GET', - headers - }); + try { + const result = await $fetch(`${createEventTableUrl(EventTable.Events)}/${id}`, { + headers: { + "xc-token": getNocoDbConfiguration().token, + }, + }); + + console.log('[nocodb-events] Successfully retrieved event:', result.id || (result as any).Id); + return normalizeEventFieldsFromNocoDB(result); + } catch (error: any) { + console.error('[nocodb-events] Error fetching event:', error); + handleNocoDbError(error, 'getEventById', 'Event'); + throw error; + } }, /** * Create a new event */ async create(eventData: Partial) { - const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}`; + console.log('[nocodb-events] Creating event with fields:', Object.keys(eventData)); - // Set default values - const data = { - ...eventData, - status: eventData.status || 'active', - created_at: new Date().toISOString(), - updated_at: new Date().toISOString() - }; - - return await $fetch(url, { - method: 'POST', - headers, - body: data - }); + try { + // Clean data like members system + const cleanData: Record = {}; + + // Only include allowed event fields + const allowedFields = [ + "title", "description", "event_type", "start_datetime", "end_datetime", + "location", "is_recurring", "recurrence_pattern", "max_attendees", + "is_paid", "cost_members", "cost_non_members", "member_pricing_enabled", + "visibility", "status", "creator", "current_attendees" + ]; + + for (const field of allowedFields) { + if (field in eventData && eventData[field as keyof Event] !== undefined) { + cleanData[field] = eventData[field as keyof Event]; + } + } + + // Set defaults + cleanData.status = cleanData.status || 'active'; + cleanData.visibility = cleanData.visibility || 'public'; + cleanData.current_attendees = cleanData.current_attendees || '0'; + + console.log('[nocodb-events] Clean event data fields:', Object.keys(cleanData)); + + const result = await $fetch(createEventTableUrl(EventTable.Events), { + method: "POST", + headers: { + "xc-token": getNocoDbConfiguration().token, + }, + body: cleanData, + }); + + console.log('[nocodb-events] Created event with ID:', result.id || (result as any).Id); + return normalizeEventFieldsFromNocoDB(result); + } catch (error: any) { + console.error('[nocodb-events] Create event failed:', error); + handleNocoDbError(error, 'createEvent', 'Event'); + throw error; + } }, /** * Update an existing event */ async update(id: string, eventData: Partial) { - const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}/${id}`; + console.log('[nocodb-events] Updating event:', id); - const data = { - ...eventData, - updated_at: new Date().toISOString() - }; - - return await $fetch(url, { - method: 'PATCH', - headers, - body: data - }); + try { + // Clean data like members system + const cleanData: Record = {}; + + const allowedFields = [ + "title", "description", "event_type", "start_datetime", "end_datetime", + "location", "is_recurring", "recurrence_pattern", "max_attendees", + "is_paid", "cost_members", "cost_non_members", "member_pricing_enabled", + "visibility", "status", "creator", "current_attendees" + ]; + + for (const field of allowedFields) { + if (field in eventData && eventData[field as keyof Event] !== undefined) { + cleanData[field] = eventData[field as keyof Event]; + } + } + + // PATCH requires ID in the body (like members system) + cleanData.Id = parseInt(id); + + const result = await $fetch(createEventTableUrl(EventTable.Events), { + method: "PATCH", + headers: { + "xc-token": getNocoDbConfiguration().token, + }, + body: cleanData + }); + + console.log('[nocodb-events] Update successful for ID:', id); + return normalizeEventFieldsFromNocoDB(result); + } catch (error: any) { + console.error('[nocodb-events] Update event failed:', error); + handleNocoDbError(error, 'updateEvent', 'Event'); + throw error; + } }, /** * Delete an event */ async delete(id: string) { - const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}/${id}`; + console.log('[nocodb-events] Deleting event:', id); - return await $fetch(url, { - method: 'DELETE', - headers - }); + try { + const result = await $fetch(createEventTableUrl(EventTable.Events), { + method: "DELETE", + headers: { + "xc-token": getNocoDbConfiguration().token, + }, + body: { + "Id": parseInt(id) + } + }); + + console.log('[nocodb-events] Delete successful for ID:', id); + return result; + } catch (error: any) { + console.error('[nocodb-events] Delete event failed:', error); + handleNocoDbError(error, 'deleteEvent', 'Event'); + throw error; + } }, /** - * Create an RSVP record for an event - */ - async createRSVP(rsvpData: Partial) { - const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}`; - - const data = { - ...rsvpData, - created_at: new Date().toISOString(), - updated_at: new Date().toISOString() - }; - - return await $fetch(url, { - method: 'POST', - headers, - body: data - }); - }, - - /** - * Find RSVPs for a specific event - */ - async findEventRSVPs(eventId: string) { - const queryParams = new URLSearchParams(); - queryParams.set('where', `(event_id = '${eventId}')`); - queryParams.set('sort', 'created_at'); - - const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}?${queryParams.toString()}`; - - return await $fetch(url, { - method: 'GET', - headers - }); - }, - - /** - * Find a user's RSVP for a specific event - */ - async findUserRSVP(eventId: string, memberId: string) { - const queryParams = new URLSearchParams(); - queryParams.set('where', `(event_id = '${eventId}' AND member_id = '${memberId}')`); - queryParams.set('limit', '1'); - - const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}?${queryParams.toString()}`; - - const response = await $fetch(url, { - method: 'GET', - headers - }); - - return response?.list?.[0] || null; - }, - - /** - * Update an RSVP record - */ - async updateRSVP(id: string, rsvpData: Partial) { - const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}/${id}`; - - const data = { - ...rsvpData, - updated_at: new Date().toISOString() - }; - - return await $fetch(url, { - method: 'PATCH', - headers, - body: data - }); - }, - - /** - * Update event attendance count (for optimization) - */ - async updateAttendeeCount(eventId: string, count: number) { - const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}/${eventId}`; - - return await $fetch(url, { - method: 'PATCH', - headers, - body: { - current_attendees: count.toString(), - updated_at: new Date().toISOString() - } - }); - }, - - /** - * Get events for a specific user with their RSVP status + * Get events for a specific user (simplified version) */ async findUserEvents(memberId: string, filters?: EventFilters) { - // First get all visible events - const events = await this.findAll(filters); + console.log('[nocodb-events] Finding events for member:', memberId); - if (!events.list || events.list.length === 0) { - return { list: [], PageInfo: events.PageInfo }; + try { + // For now, just get all visible events (we'll add RSVP logic later) + const events = await this.findAll(filters); + + // TODO: Add RSVP lookup from separate table + // For now, return events without RSVP status + return { + list: events.list || [], + PageInfo: events.PageInfo + }; + } catch (error: any) { + console.error('[nocodb-events] Error finding user events:', error); + throw error; } - - // Get user's RSVPs for these events - const eventIds = events.list.map((e: Event) => e.id); - const rsvpQueryParams = new URLSearchParams(); - rsvpQueryParams.set('where', `(member_id = '${memberId}' AND event_id IN (${eventIds.map(id => `'${id}'`).join(',')}))`); - - const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}?${rsvpQueryParams.toString()}`; - - const rsvps = await $fetch(url, { - method: 'GET', - headers - }); - - // Map RSVPs to events - const rsvpMap = new Map(); - if (rsvps.list) { - rsvps.list.forEach((rsvp: EventRSVP) => { - rsvpMap.set(rsvp.event_id, rsvp); - }); - } - - // Add RSVP information to events - const eventsWithRSVP = events.list.map((event: Event) => ({ - ...event, - user_rsvp: rsvpMap.get(event.id) || null - })); - - return { - list: eventsWithRSVP, - PageInfo: events.PageInfo - }; - }, - - /** - * Generate payment reference for RSVP - */ - generatePaymentReference(memberId: string, date?: Date): string { - const referenceDate = date || new Date(); - const dateString = referenceDate.toISOString().split('T')[0]; // YYYY-MM-DD - return `EVT-${memberId}-${dateString}`; - }, - - /** - * Check if event has reached capacity - */ - async isEventFull(eventId: string): Promise { - const event = await this.findOne(eventId); - - if (!event.max_attendees) return false; // Unlimited capacity - - const maxAttendees = parseInt(event.max_attendees); - const currentAttendees = event.current_attendees || 0; - - return currentAttendees >= maxAttendees; } };