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

793 lines
29 KiB
TypeScript
Raw Normal View History

// server/utils/nocodb-events.ts
import type { Event, EventRSVP, EventsResponse, EventFilters } from '~/utils/types';
import { createSessionManager } from '~/server/utils/session';
2025-08-12 12:29:59 +02:00
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 => {
try {
// Try to get effective configuration from admin config system first
const effectiveConfig = getEffectiveNocoDBConfig();
if (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 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;
}
}
}
} catch (error) {
console.log(`[nocodb-events] Admin config not available, trying fallback 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',
2025-08-13 13:51:27 +02:00
'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',
2025-08-13 13:51:27 +02:00
'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;
};
2025-08-13 14:30:26 +02:00
// 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() {
2025-08-13 16:47:53 +02:00
// Use the centralized configuration from nocodb.ts which now prioritizes environment variables
const config = getNocoDbConfiguration();
2025-08-13 16:47:53 +02:00
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);
2025-08-13 15:57:34 +02:00
// 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
*/
async findOne(id: string) {
console.log('[nocodb-events] Fetching event ID:', id);
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>) {
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 = [
2025-08-13 13:51:27 +02:00
"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];
}
}
2025-08-13 13:51:27 +02:00
// 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;
}
},
/**
2025-08-13 15:35:53 +02:00
* 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 {
2025-08-13 15:35:53 +02:00
// First get all events using the working findAll method
const simpleFilters = {
status: filters?.status,
limit: (filters as any)?.limit,
offset: (filters as any)?.offset
};
2025-08-13 15:35:53 +02:00
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 {
2025-08-13 15:35:53 +02:00
list: eventsWithRSVPs,
PageInfo: eventsResponse.PageInfo
};
} catch (error: any) {
console.error('[nocodb-events] Error finding user events:', error);
throw error;
}
2025-08-13 14:30:26 +02:00
},
/**
* 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 || '',
2025-08-13 15:57:34 +02:00
extra_guests: rsvpData.extra_guests || '0', // Include guest count
2025-08-13 14:30:26 +02:00
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;
}
2025-08-13 15:57:34 +02:00
},
/**
* 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
}
2025-08-13 16:47:53 +02:00
},
/**
* 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' };
2025-08-13 13:51:27 +02:00
// 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 {
2025-08-13 13:51:27 +02:00
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,
2025-08-13 13:51:27 +02:00
status: event.status,
event_id: event.event_id,
database_id: event.id || (event as any).Id
}
};
}