2025-08-12 04:25:35 +02:00
|
|
|
|
// server/utils/nocodb-events.ts
|
|
|
|
|
|
import type { Event, EventRSVP, EventsResponse, EventFilters } from '~/utils/types';
|
2025-08-12 16:58:31 +02:00
|
|
|
|
import { createSessionManager } from '~/server/utils/session';
|
2025-08-12 12:29:59 +02:00
|
|
|
|
import { getEffectiveNocoDBConfig } from './admin-config';
|
2025-08-12 04:25:35 +02:00
|
|
|
|
|
2025-08-12 16:58:31 +02:00
|
|
|
|
// 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 => {
|
2025-08-13 17:16:22 +02:00
|
|
|
|
console.log(`[nocodb-events] 🔍 DEBUG: Getting table ID for ${tableName}...`);
|
|
|
|
|
|
|
2025-08-12 17:05:33 +02:00
|
|
|
|
try {
|
|
|
|
|
|
// Try to get effective configuration from admin config system first
|
|
|
|
|
|
const effectiveConfig = getEffectiveNocoDBConfig();
|
2025-08-13 17:16:22 +02:00
|
|
|
|
console.log(`[nocodb-events] 🔍 DEBUG: Effective config:`, JSON.stringify(effectiveConfig, null, 2));
|
|
|
|
|
|
|
2025-08-12 17:05:33 +02:00
|
|
|
|
if (effectiveConfig?.tables) {
|
2025-08-13 17:16:22 +02:00
|
|
|
|
console.log(`[nocodb-events] 🔍 DEBUG: Available table keys:`, Object.keys(effectiveConfig.tables));
|
|
|
|
|
|
|
2025-08-13 15:14:43 +02:00
|
|
|
|
if (tableName === 'Events') {
|
|
|
|
|
|
// Check multiple possible keys for Events table
|
|
|
|
|
|
const eventsTableId = effectiveConfig.tables['events'] ||
|
|
|
|
|
|
effectiveConfig.tables['Events'] ||
|
|
|
|
|
|
effectiveConfig.tables['events_table'];
|
|
|
|
|
|
if (eventsTableId) {
|
2025-08-13 17:16:22 +02:00
|
|
|
|
console.log(`[nocodb-events] ✅ Using admin config table ID for ${tableName}:`, eventsTableId);
|
2025-08-13 15:14:43 +02:00
|
|
|
|
return eventsTableId;
|
2025-08-13 17:16:22 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`[nocodb-events] ⚠️ No Events table ID found in config`);
|
2025-08-13 15:14:43 +02:00
|
|
|
|
}
|
|
|
|
|
|
} 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) {
|
2025-08-13 17:16:22 +02:00
|
|
|
|
console.log(`[nocodb-events] ✅ Using admin config table ID for ${tableName}:`, rsvpTableId);
|
2025-08-13 15:14:43 +02:00
|
|
|
|
return rsvpTableId;
|
2025-08-13 17:16:22 +02:00
|
|
|
|
} 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']);
|
2025-08-13 15:14:43 +02:00
|
|
|
|
}
|
2025-08-12 17:05:33 +02:00
|
|
|
|
}
|
2025-08-13 17:16:22 +02:00
|
|
|
|
} else {
|
|
|
|
|
|
console.log(`[nocodb-events] ⚠️ No tables configuration found in effective config`);
|
2025-08-12 17:05:33 +02:00
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
2025-08-13 17:16:22 +02:00
|
|
|
|
console.log(`[nocodb-events] ❌ Admin config error for ${tableName}:`, error);
|
2025-08-12 17:05:33 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Try to get table ID from global configuration
|
2025-08-12 16:58:31 +02:00
|
|
|
|
const globalConfig = (global as any).globalNocoDBConfig;
|
|
|
|
|
|
if (globalConfig?.tables) {
|
2025-08-13 15:14:43 +02:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-08-12 16:58:31 +02:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Try runtime config as fallback
|
2025-08-12 04:25:35 +02:00
|
|
|
|
const config = useRuntimeConfig();
|
2025-08-12 16:58:31 +02:00
|
|
|
|
if (tableName === 'Events' && config.nocodb?.eventsTableId) {
|
|
|
|
|
|
console.log(`[nocodb-events] Using runtime config table ID for ${tableName}:`, config.nocodb.eventsTableId);
|
|
|
|
|
|
return config.nocodb.eventsTableId;
|
|
|
|
|
|
}
|
2025-08-12 04:25:35 +02:00
|
|
|
|
|
2025-08-12 16:58:31 +02:00
|
|
|
|
// Final fallback to default
|
2025-08-13 15:14:43 +02:00
|
|
|
|
const defaultTableId = tableName === 'Events' ? 'mp3wigub1fzdo1b' : 'mt1mx3vkcw0vbmh';
|
2025-08-12 16:58:31 +02:00
|
|
|
|
console.log(`[nocodb-events] Using fallback table ID for ${tableName}:`, defaultTableId);
|
|
|
|
|
|
return defaultTableId;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const createEventTableUrl = (table: EventTable | string) => {
|
|
|
|
|
|
let tableId: string;
|
2025-08-12 04:25:35 +02:00
|
|
|
|
|
2025-08-12 16:58:31 +02:00
|
|
|
|
if (table === EventTable.Events || table === 'Events') {
|
|
|
|
|
|
tableId = getEventTableId('Events');
|
|
|
|
|
|
} else if (table === EventTable.EventRSVPs || table === 'EventRSVPs') {
|
|
|
|
|
|
tableId = getEventTableId('EventRSVPs');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
tableId = table.toString();
|
2025-08-12 04:25:35 +02:00
|
|
|
|
}
|
2025-08-12 16:58:31 +02:00
|
|
|
|
|
|
|
|
|
|
const url = `${getNocoDbConfiguration().url}/api/v2/tables/${tableId}/records`;
|
|
|
|
|
|
console.log('[nocodb-events] Event table URL:', url);
|
|
|
|
|
|
return url;
|
|
|
|
|
|
};
|
2025-08-12 04:25:35 +02:00
|
|
|
|
|
2025-08-12 16:58:31 +02:00
|
|
|
|
// 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',
|
2025-08-12 16:58:31 +02:00
|
|
|
|
'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',
|
2025-08-12 16:58:31 +02:00
|
|
|
|
'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'
|
2025-08-12 04:25:35 +02:00
|
|
|
|
};
|
2025-08-12 16:58:31 +02:00
|
|
|
|
|
|
|
|
|
|
// 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-12 04:25:35 +02:00
|
|
|
|
|
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)
|
|
|
|
|
|
|
2025-08-12 16:58:31 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* 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
|
2025-08-12 17:05:33 +02:00
|
|
|
|
const config = getNocoDbConfiguration();
|
2025-08-13 16:47:53 +02:00
|
|
|
|
console.log('[nocodb-events] ✅ Using NocoDB configuration (prioritizes environment variables)');
|
fix(events): Convert events NocoDB client from v1 to v2 API
- Updated all NocoDB API calls from v1/db/data/v1/ to v2/tables/ endpoints
- Fixed 422 Unprocessable Entity errors on events calendar page
- Ensures consistency with members system which already uses v2 API
- Updated methods: findAll, findOne, create, update, delete, createRSVP, findEventRSVPs, findUserRSVP, updateRSVP, updateAttendeeCount, findUserEvents
- Maintains same functionality while using correct API version
Resolves continuous 422 errors when loading events calendar.
2025-08-12 13:02:13 +02:00
|
|
|
|
|
2025-08-12 04:25:35 +02:00
|
|
|
|
const eventsClient = {
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Find all events with optional filtering
|
|
|
|
|
|
*/
|
2025-08-12 16:58:31 +02:00
|
|
|
|
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;
|
2025-08-12 04:25:35 +02:00
|
|
|
|
}
|
2025-08-12 16:58:31 +02:00
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
2025-08-12 16:58:31 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error('[nocodb-events] ❌ Error fetching events:', error);
|
|
|
|
|
|
handleNocoDbError(error, 'getEvents', 'Events');
|
|
|
|
|
|
throw error;
|
2025-08-12 04:25:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-08-13 22:23:06 +02:00
|
|
|
|
* Find a single event by ID (supports both database Id and business event_id)
|
2025-08-12 04:25:35 +02:00
|
|
|
|
*/
|
|
|
|
|
|
async findOne(id: string) {
|
2025-08-12 16:58:31 +02:00
|
|
|
|
console.log('[nocodb-events] Fetching event ID:', id);
|
2025-08-12 04:25:35 +02:00
|
|
|
|
|
2025-08-12 16:58:31 +02:00
|
|
|
|
try {
|
2025-08-13 22:23:06 +02:00
|
|
|
|
// 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)}`, {
|
2025-08-12 16:58:31 +02:00
|
|
|
|
headers: {
|
|
|
|
|
|
"xc-token": getNocoDbConfiguration().token,
|
|
|
|
|
|
},
|
2025-08-13 22:23:06 +02:00
|
|
|
|
params: {
|
|
|
|
|
|
where: `(event_id,eq,${id})`,
|
|
|
|
|
|
limit: 1
|
|
|
|
|
|
}
|
2025-08-12 16:58:31 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-08-13 22:23:06 +02:00
|
|
|
|
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'
|
|
|
|
|
|
});
|
2025-08-12 16:58:31 +02:00
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
console.error('[nocodb-events] Error fetching event:', error);
|
2025-08-13 22:23:06 +02:00
|
|
|
|
if (error.statusCode === 404) {
|
|
|
|
|
|
throw error; // Re-throw 404 errors as-is
|
|
|
|
|
|
}
|
2025-08-12 16:58:31 +02:00
|
|
|
|
handleNocoDbError(error, 'getEventById', 'Event');
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
2025-08-12 04:25:35 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Create a new event
|
|
|
|
|
|
*/
|
|
|
|
|
|
async create(eventData: Partial<Event>) {
|
2025-08-12 16:58:31 +02:00
|
|
|
|
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",
|
2025-08-12 16:58:31 +02:00
|
|
|
|
"location", "is_recurring", "recurrence_pattern", "max_attendees",
|
|
|
|
|
|
"is_paid", "cost_members", "cost_non_members", "member_pricing_enabled",
|
2025-08-13 15:14:43 +02:00
|
|
|
|
"guests_permitted", "max_guests_permitted",
|
2025-08-12 16:58:31 +02:00
|
|
|
|
"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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-12 16:58:31 +02:00
|
|
|
|
// 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;
|
|
|
|
|
|
}
|
2025-08-12 04:25:35 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Update an existing event
|
|
|
|
|
|
*/
|
|
|
|
|
|
async update(id: string, eventData: Partial<Event>) {
|
2025-08-12 16:58:31 +02:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2025-08-12 04:25:35 +02:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Delete an event
|
|
|
|
|
|
*/
|
|
|
|
|
|
async delete(id: string) {
|
2025-08-12 16:58:31 +02:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
2025-08-12 04:25:35 +02:00
|
|
|
|
});
|
2025-08-12 16:58:31 +02:00
|
|
|
|
|
|
|
|
|
|
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-12 04:25:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-08-13 15:35:53 +02:00
|
|
|
|
* Get events for a specific user with RSVP status loaded
|
2025-08-12 04:25:35 +02:00
|
|
|
|
*/
|
2025-08-12 16:58:31 +02:00
|
|
|
|
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
|
2025-08-12 17:23:42 +02:00
|
|
|
|
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');
|
2025-08-12 16:58:31 +02:00
|
|
|
|
|
|
|
|
|
|
return {
|
2025-08-13 15:35:53 +02:00
|
|
|
|
list: eventsWithRSVPs,
|
|
|
|
|
|
PageInfo: eventsResponse.PageInfo
|
2025-08-12 16:58:31 +02:00
|
|
|
|
};
|
|
|
|
|
|
} 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;
|
|
|
|
|
|
}
|
2025-08-12 04:25:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2025-08-12 04:25:35 +02:00
|
|
|
|
return {
|
2025-08-13 13:51:27 +02:00
|
|
|
|
id: calendarId,
|
2025-08-12 04:25:35 +02:00
|
|
|
|
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
|
2025-08-12 04:25:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|