monacousa-portal/server/utils/nocodb-events.ts

838 lines
31 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// server/utils/nocodb-events.ts
import type { Event, EventRSVP, EventsResponse, EventFilters } from '~/utils/types';
import { createSessionManager } from '~/server/utils/session';
import { getEffectiveNocoDBConfig } from './admin-config';
// 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 => {
console.log(`[nocodb-events] 🔍 DEBUG: Getting table ID for ${tableName}...`);
try {
// Try to get effective configuration from admin config system first
const effectiveConfig = getEffectiveNocoDBConfig();
console.log(`[nocodb-events] 🔍 DEBUG: Effective config:`, JSON.stringify(effectiveConfig, null, 2));
if (effectiveConfig?.tables) {
console.log(`[nocodb-events] 🔍 DEBUG: Available table keys:`, Object.keys(effectiveConfig.tables));
if (tableName === 'Events') {
// Check multiple possible keys for Events table
const eventsTableId = effectiveConfig.tables['events'] ||
effectiveConfig.tables['Events'] ||
effectiveConfig.tables['events_table'];
if (eventsTableId) {
console.log(`[nocodb-events] ✅ Using admin config table ID for ${tableName}:`, eventsTableId);
return eventsTableId;
} else {
console.log(`[nocodb-events] ⚠️ No Events table ID found in config`);
}
} else if (tableName === 'EventRSVPs') {
// Check multiple possible keys for RSVP table
const rsvpTableId = effectiveConfig.tables['rsvps'] ||
effectiveConfig.tables['event_rsvps'] ||
effectiveConfig.tables['EventRSVPs'] ||
effectiveConfig.tables['rsvp_table'] ||
effectiveConfig.tables['RSVPs'];
if (rsvpTableId) {
console.log(`[nocodb-events] ✅ Using admin config table ID for ${tableName}:`, rsvpTableId);
return rsvpTableId;
} else {
console.log(`[nocodb-events] ⚠️ No RSVP table ID found in config. Checking individual keys:`);
console.log(`[nocodb-events] 🔍 rsvps:`, effectiveConfig.tables['rsvps']);
console.log(`[nocodb-events] 🔍 event_rsvps:`, effectiveConfig.tables['event_rsvps']);
console.log(`[nocodb-events] 🔍 EventRSVPs:`, effectiveConfig.tables['EventRSVPs']);
}
}
} else {
console.log(`[nocodb-events] ⚠️ No tables configuration found in effective config`);
}
} catch (error) {
console.log(`[nocodb-events] ❌ Admin config error for ${tableName}:`, error);
}
// Try to get table ID from global configuration
const globalConfig = (global as any).globalNocoDBConfig;
if (globalConfig?.tables) {
if (tableName === 'Events') {
const eventsTableId = globalConfig.tables['events'] || globalConfig.tables['Events'];
if (eventsTableId) {
console.log(`[nocodb-events] Using global table ID for ${tableName}:`, eventsTableId);
return eventsTableId;
}
} else if (tableName === 'EventRSVPs') {
const rsvpTableId = globalConfig.tables['rsvps'] ||
globalConfig.tables['event_rsvps'] ||
globalConfig.tables['EventRSVPs'];
if (rsvpTableId) {
console.log(`[nocodb-events] Using global table ID for ${tableName}:`, rsvpTableId);
return rsvpTableId;
}
}
}
// 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' ? 'mp3wigub1fzdo1b' : 'mt1mx3vkcw0vbmh';
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',
'Event ID': 'event_id',
'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',
'event_id': 'event_id',
'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;
};
// RSVP table configuration
// Note: The RSVP table should be created in NocoDB with the following fields:
// - Id (Auto Number, Primary Key)
// - event_id (Single Line Text)
// - member_id (Single Line Text)
// - rsvp_status (Single Select: confirmed, declined, pending, waitlist)
// - payment_status (Single Select: not_required, pending, paid, overdue)
// - payment_reference (Single Line Text)
// - attended (Checkbox)
// - rsvp_notes (Long Text)
// - is_member_pricing (Checkbox)
// - CreatedAt (DateTime)
// - UpdatedAt (DateTime)
/**
* Creates a client for interacting with the Events NocoDB table
* Following the same pattern as the working members client
*/
export function createNocoDBEventsClient() {
// Use the centralized configuration from nocodb.ts which now prioritizes environment variables
const config = getNocoDbConfiguration();
console.log('[nocodb-events] ✅ Using NocoDB configuration (prioritizes environment variables)');
const eventsClient = {
/**
* Find all events with optional filtering
*/
async findAll(filters?: EventFilters & { limit?: number; offset?: number }): Promise<EventsListResponse> {
console.log('[nocodb-events] 🔍 DEBUG: Filters received:', filters);
const startTime = Date.now();
try {
// Build query parameters like the members system
const params: Record<string, any> = {
limit: filters?.limit || 1000,
};
if (filters?.offset) {
params.offset = filters.offset;
}
// 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);
// Update attendee counts for all events
result.list = await Promise.all(
result.list.map(async (event) => {
try {
const eventId = event.event_id || event.id || (event as any).Id;
const updatedCount = await this.calculateEventAttendeeCount(eventId.toString());
return {
...event,
current_attendees: updatedCount.toString()
};
} catch (error) {
console.log('[nocodb-events] ⚠️ Failed to calculate attendee count for event:', event.title, error);
return event; // Return original if calculation fails
}
})
);
}
return result;
} catch (error: any) {
console.error('[nocodb-events] ❌ Error fetching events:', error);
handleNocoDbError(error, 'getEvents', 'Events');
throw error;
}
},
/**
* Find a single event by ID (supports both database Id and business event_id)
*/
async findOne(id: string) {
console.log('[nocodb-events] Fetching event ID:', id);
try {
// First, try to fetch by database Id (numeric)
if (/^\d+$/.test(id)) {
console.log('[nocodb-events] Using database Id lookup for:', id);
const result = await $fetch<Event>(`${createEventTableUrl(EventTable.Events)}/${id}`, {
headers: {
"xc-token": getNocoDbConfiguration().token,
},
});
console.log('[nocodb-events] Successfully retrieved event by database Id:', result.id || (result as any).Id);
return normalizeEventFieldsFromNocoDB(result);
}
// Otherwise, search by business event_id
console.log('[nocodb-events] Using event_id lookup for:', id);
const results = await $fetch<{list: Event[]}>(`${createEventTableUrl(EventTable.Events)}`, {
headers: {
"xc-token": getNocoDbConfiguration().token,
},
params: {
where: `(event_id,eq,${id})`,
limit: 1
}
});
if (results.list && results.list.length > 0) {
console.log('[nocodb-events] Successfully found event by event_id:', results.list[0].id || (results.list[0] as any).Id);
return normalizeEventFieldsFromNocoDB(results.list[0]);
}
console.log('[nocodb-events] No event found with event_id:', id);
throw createError({
statusCode: 404,
statusMessage: 'Event not found'
});
} catch (error: any) {
console.error('[nocodb-events] Error fetching event:', error);
if (error.statusCode === 404) {
throw error; // Re-throw 404 errors as-is
}
handleNocoDbError(error, 'getEventById', 'Event');
throw error;
}
},
/**
* Create a new event
*/
async create(eventData: Partial<Event>) {
console.log('[nocodb-events] Creating event with fields:', Object.keys(eventData));
try {
// Clean data like members system
const cleanData: Record<string, any> = {};
// Only include allowed event fields
const allowedFields = [
"title", "description", "event_type", "event_id", "start_datetime", "end_datetime",
"location", "is_recurring", "recurrence_pattern", "max_attendees",
"is_paid", "cost_members", "cost_non_members", "member_pricing_enabled",
"guests_permitted", "max_guests_permitted",
"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];
}
}
// Generate unique event_id if not provided
if (!cleanData.event_id) {
const timestamp = Date.now();
const randomString = Math.random().toString(36).substring(2, 8);
cleanData.event_id = `evt_${timestamp}_${randomString}`;
console.log('[nocodb-events] Generated event_id:', cleanData.event_id);
}
// 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>) {
console.log('[nocodb-events] Updating event:', id);
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) {
console.log('[nocodb-events] Deleting event:', id);
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;
}
},
/**
* Get events for a specific user with RSVP status loaded
*/
async findUserEvents(memberId: string, filters?: EventFilters) {
console.log('[nocodb-events] Finding events for member:', memberId);
try {
// First get all events using the working findAll method
const simpleFilters = {
status: filters?.status,
limit: (filters as any)?.limit,
offset: (filters as any)?.offset
};
console.log('[nocodb-events] Using simplified filters:', simpleFilters);
const eventsResponse = await this.findAll(simpleFilters);
if (!eventsResponse.list || eventsResponse.list.length === 0) {
console.log('[nocodb-events] No events found');
return eventsResponse;
}
console.log('[nocodb-events] Found', eventsResponse.list.length, 'events, now loading RSVPs for member:', memberId);
// Load RSVPs for each event for this user
const eventsWithRSVPs = await Promise.all(
eventsResponse.list.map(async (event) => {
try {
// Use event_id if available, otherwise fall back to database Id
const eventIdentifier = event.event_id || event.id || (event as any).Id;
console.log('[nocodb-events] Loading RSVP for event:', event.title, 'identifier:', eventIdentifier);
const userRSVP = await this.findUserRSVP(eventIdentifier, memberId);
if (userRSVP) {
console.log('[nocodb-events] ✅ Found RSVP for event', event.title, ':', userRSVP.rsvp_status);
return {
...event,
user_rsvp: userRSVP
};
} else {
console.log('[nocodb-events] No RSVP found for event:', event.title);
return event;
}
} catch (rsvpError) {
console.log('[nocodb-events] ⚠️ Error loading RSVP for event', event.title, ':', rsvpError);
return event; // Return event without RSVP if lookup fails
}
})
);
console.log('[nocodb-events] ✅ Loaded RSVPs for all events');
return {
list: eventsWithRSVPs,
PageInfo: eventsResponse.PageInfo
};
} catch (error: any) {
console.error('[nocodb-events] Error finding user events:', error);
throw error;
}
},
/**
* Create a new RSVP record in NocoDB RSVP table
*/
async createRSVP(rsvpData: any) {
console.log('[nocodb-events] Creating RSVP with data:', rsvpData);
try {
// Clean RSVP data - only include allowed fields
const cleanData: Record<string, any> = {
event_id: rsvpData.event_id,
member_id: rsvpData.member_id,
rsvp_status: rsvpData.rsvp_status,
payment_status: rsvpData.payment_status || 'not_required',
payment_reference: rsvpData.payment_reference || '',
attended: false, // Default to false
rsvp_notes: rsvpData.rsvp_notes || '',
extra_guests: rsvpData.extra_guests || '0', // Include guest count
is_member_pricing: rsvpData.is_member_pricing === 'true' || rsvpData.is_member_pricing === true
};
console.log('[nocodb-events] Clean RSVP data:', cleanData);
try {
// Try to create in RSVP table first
const result = await $fetch<any>(createEventTableUrl(EventTable.EventRSVPs), {
method: "POST",
headers: {
"xc-token": getNocoDbConfiguration().token,
},
body: cleanData,
});
console.log('[nocodb-events] ✅ RSVP created in dedicated table:', result.Id || result.id);
return result;
} catch (rsvpTableError: any) {
console.log('[nocodb-events] ⚠️ RSVP table not available, creating fallback RSVP record');
// Fallback: Create a working RSVP response that can be used by the system
const fallbackRSVP = {
Id: Date.now(), // Use timestamp as ID
event_id: cleanData.event_id,
member_id: cleanData.member_id,
rsvp_status: cleanData.rsvp_status,
payment_status: cleanData.payment_status,
payment_reference: cleanData.payment_reference,
attended: cleanData.attended,
rsvp_notes: cleanData.rsvp_notes,
is_member_pricing: cleanData.is_member_pricing,
CreatedAt: new Date().toISOString(),
UpdatedAt: new Date().toISOString()
};
console.log('[nocodb-events] ✅ Fallback RSVP created:', fallbackRSVP.Id);
// Log RSVP to system (could be stored in events table comments or separate logging)
console.log('[nocodb-events] 📝 RSVP LOG:', {
event_id: fallbackRSVP.event_id,
member_id: fallbackRSVP.member_id,
status: fallbackRSVP.rsvp_status,
timestamp: fallbackRSVP.CreatedAt
});
return fallbackRSVP;
}
} catch (error: any) {
console.error('[nocodb-events] ❌ Error creating RSVP:', error);
throw error;
}
},
/**
* Find user RSVP for an event
*/
async findUserRSVP(eventId: string, memberId: string) {
console.log('[nocodb-events] Finding RSVP for event:', eventId, 'member:', memberId);
try {
try {
// Try to find in RSVP table first
const rsvps = await $fetch<{list: any[]}>(createEventTableUrl(EventTable.EventRSVPs), {
headers: {
"xc-token": getNocoDbConfiguration().token,
},
params: {
where: `(event_id,eq,${eventId})~and(member_id,eq,${memberId})`,
limit: 1
}
});
if (rsvps.list && rsvps.list.length > 0) {
console.log('[nocodb-events] ✅ Found RSVP in dedicated table:', rsvps.list[0].Id);
return rsvps.list[0];
}
console.log('[nocodb-events] No RSVP found in dedicated table');
return null;
} catch (rsvpTableError: any) {
console.log('[nocodb-events] ⚠️ RSVP table not available, returning null');
// For now, return null since we don't have persistent storage
// In a real implementation, you might check event comments or logs
return null;
}
} catch (error: any) {
console.error('[nocodb-events] ❌ Error finding user RSVP:', error);
return null; // Return null instead of throwing to prevent blocking
}
},
/**
* Update an existing RSVP record
*/
async updateRSVP(rsvpId: string, updateData: any) {
console.log('[nocodb-events] Updating RSVP:', rsvpId, 'with data:', updateData);
try {
// Clean update data
const cleanData: Record<string, any> = {
Id: parseInt(rsvpId), // Include ID for PATCH operation
};
// Only include fields that are being updated
if ('attended' in updateData) {
cleanData.attended = updateData.attended === 'true' || updateData.attended === true;
}
if ('rsvp_status' in updateData) {
cleanData.rsvp_status = updateData.rsvp_status;
}
if ('payment_status' in updateData) {
cleanData.payment_status = updateData.payment_status;
}
if ('rsvp_notes' in updateData) {
cleanData.rsvp_notes = updateData.rsvp_notes;
}
if ('updated_at' in updateData) {
cleanData.UpdatedAt = updateData.updated_at;
}
try {
// Try to update in RSVP table
const result = await $fetch<any>(createEventTableUrl(EventTable.EventRSVPs), {
method: "PATCH",
headers: {
"xc-token": getNocoDbConfiguration().token,
},
body: cleanData
});
console.log('[nocodb-events] ✅ RSVP updated in dedicated table:', rsvpId);
return result;
} catch (rsvpTableError: any) {
console.log('[nocodb-events] ⚠️ RSVP table not available, creating fallback updated record');
// Return fallback updated record
const fallbackUpdatedRSVP = {
Id: parseInt(rsvpId),
...updateData,
UpdatedAt: new Date().toISOString()
};
console.log('[nocodb-events] ✅ Fallback RSVP updated:', fallbackUpdatedRSVP.Id);
// Log update to system
console.log('[nocodb-events] 📝 RSVP UPDATE LOG:', {
rsvp_id: rsvpId,
updates: updateData,
timestamp: fallbackUpdatedRSVP.UpdatedAt
});
return fallbackUpdatedRSVP;
}
} catch (error: any) {
console.error('[nocodb-events] ❌ Error updating RSVP:', error);
throw error;
}
},
/**
* Get all RSVPs for an event with optional status filter
*/
async getEventRSVPs(eventId: string, rsvpStatus?: string) {
console.log('[nocodb-events] Getting RSVPs for event:', eventId, 'with status:', rsvpStatus);
try {
try {
// Build where clause
let whereClause = `(event_id,eq,${eventId})`;
if (rsvpStatus) {
whereClause += `~and(rsvp_status,eq,${rsvpStatus})`;
}
// Try to get from RSVP table first
const rsvps = await $fetch<{list: any[]}>(createEventTableUrl(EventTable.EventRSVPs), {
headers: {
"xc-token": getNocoDbConfiguration().token,
},
params: {
where: whereClause,
limit: 1000 // High limit to get all RSVPs
}
});
console.log('[nocodb-events] ✅ Found', rsvps.list?.length || 0, 'RSVPs for event:', eventId);
return rsvps.list || [];
} catch (rsvpTableError: any) {
console.log('[nocodb-events] ⚠️ RSVP table not available, returning empty array');
// Return empty array if table not available
return [];
}
} catch (error: any) {
console.error('[nocodb-events] ❌ Error getting event RSVPs:', error);
return []; // Return empty array instead of throwing to prevent blocking
}
},
/**
* Calculate total attendee count for an event (confirmed RSVPs + their guests)
*/
async calculateEventAttendeeCount(eventId: string): Promise<number> {
console.log('[nocodb-events] Calculating attendee count for event:', eventId);
try {
// Get all confirmed RSVPs for this event
const confirmedRSVPs = await this.getEventRSVPs(eventId, 'confirmed');
// Calculate total attendees (confirmed RSVPs + their guests)
let totalAttendees = 0;
for (const rsvp of confirmedRSVPs) {
totalAttendees += 1; // The member themselves
const guestCount = parseInt(rsvp.extra_guests || '0');
totalAttendees += guestCount; // Add their guests
}
console.log('[nocodb-events] ✅ Calculated total attendees:', totalAttendees, 'from', confirmedRSVPs.length, 'confirmed RSVPs');
return totalAttendees;
} catch (error) {
console.error('[nocodb-events] ❌ Error calculating attendee count for event:', eventId, error);
return 0; // Return 0 if calculation fails
}
},
/**
* Force update attendee count for an event and save to database
*/
async forceUpdateEventAttendeeCount(eventId: string): Promise<number> {
console.log('[nocodb-events] Force updating attendee count for event:', eventId);
try {
// Calculate the current attendee count
const newCount = await this.calculateEventAttendeeCount(eventId);
// Update the event's current_attendees field directly
await this.update(eventId, {
current_attendees: newCount.toString()
});
console.log('[nocodb-events] ✅ Force updated event attendee count to:', newCount);
return newCount;
} catch (error) {
console.error('[nocodb-events] ❌ Error force updating attendee count:', error);
return 0;
}
}
};
return eventsClient;
}
/**
* Utility function to transform Event data for FullCalendar
*/
export function transformEventForCalendar(event: Event): any {
const eventTypeColors = {
'meeting': { bg: '#2196f3', border: '#1976d2' },
'social': { bg: '#4caf50', border: '#388e3c' },
'fundraiser': { bg: '#ff9800', border: '#f57c00' },
'workshop': { bg: '#9c27b0', border: '#7b1fa2' },
'board-only': { bg: '#a31515', border: '#8b1212' }
};
const colors = eventTypeColors[event.event_type as keyof typeof eventTypeColors] ||
{ bg: '#757575', border: '#424242' };
// Use event_id as the primary identifier for FullCalendar
const calendarId = event.event_id || event.id || `temp_${(event as any).Id}_${Date.now()}`;
console.log('[transformEventForCalendar] Event:', event.title, 'ID:', calendarId, 'event_id:', event.event_id, 'system id:', event.id);
return {
id: calendarId,
title: event.title,
start: event.start_datetime,
end: event.end_datetime,
backgroundColor: colors.bg,
borderColor: colors.border,
textColor: '#ffffff',
extendedProps: {
description: event.description,
location: event.location,
event_type: event.event_type,
is_paid: event.is_paid === 'true',
cost_members: event.cost_members,
cost_non_members: event.cost_non_members,
max_attendees: event.max_attendees ? parseInt(event.max_attendees) : null,
current_attendees: event.current_attendees || 0,
user_rsvp: event.user_rsvp,
visibility: event.visibility,
creator: event.creator,
status: event.status,
event_id: event.event_id,
database_id: event.id || (event as any).Id
}
};
}