Add comprehensive debug logging and refactor events API session handling

- Add extensive console logging throughout events GET/POST endpoints
- Replace getUserSession with createSessionManager for better session handling
- Temporarily disable complex filtering/sorting logic to isolate issues
- Enhance error handling with proper statusCode checking
This commit is contained in:
Matt 2025-08-12 16:58:31 +02:00
parent e06f639454
commit 7d55468a21
3 changed files with 365 additions and 303 deletions

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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<string, string> = {
'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<EventsListResponse> {
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<string, any> = {
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<EventsListResponse>(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<Event>(url, {
method: 'GET',
headers
});
try {
const result = await $fetch<Event>(`${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<Event>) {
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<Event>(url, {
method: 'POST',
headers,
body: data
});
try {
// Clean data like members system
const cleanData: Record<string, any> = {};
// 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<Event>(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<Event>) {
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<Event>(url, {
method: 'PATCH',
headers,
body: data
});
try {
// Clean data like members system
const cleanData: Record<string, any> = {};
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<Event>(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<EventRSVP>) {
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<EventRSVP>(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<EventRSVP>) {
const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}/${id}`;
const data = {
...rsvpData,
updated_at: new Date().toISOString()
};
return await $fetch<EventRSVP>(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<boolean> {
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;
}
};