838 lines
31 KiB
TypeScript
838 lines
31 KiB
TypeScript
// 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
|
||
}
|
||
};
|
||
}
|