388 lines
13 KiB
TypeScript
388 lines
13 KiB
TypeScript
// server/utils/nocodb-events.ts
|
|
import type { Event, EventRSVP, EventsResponse, EventFilters } from '~/utils/types';
|
|
import { createSessionManager } from '~/server/utils/session';
|
|
|
|
// Import shared NocoDB utilities from the working members system
|
|
import { getNocoDbConfiguration, setGlobalNocoDBConfig, handleNocoDbError } from '~/server/utils/nocodb';
|
|
|
|
// Define interfaces for API responses
|
|
interface EventsListResponse {
|
|
list: Event[];
|
|
PageInfo: {
|
|
pageSize: number;
|
|
totalRows: number;
|
|
isFirstPage: boolean;
|
|
isLastPage: boolean;
|
|
page: number;
|
|
};
|
|
}
|
|
|
|
// Table ID enumeration for Events (similar to Members table pattern)
|
|
export enum EventTable {
|
|
Events = "events-table-id", // Will be configured via admin panel
|
|
EventRSVPs = "rsvps-table-id" // Separate table for RSVPs
|
|
}
|
|
|
|
// Dynamic table ID getter - will use configured table ID from admin panel
|
|
export const getEventTableId = (tableName: 'Events' | 'EventRSVPs'): string => {
|
|
// Try to get table ID from global configuration first
|
|
const globalConfig = (global as any).globalNocoDBConfig;
|
|
if (globalConfig?.tables) {
|
|
const tableKey = tableName === 'Events' ? 'events' : 'event_rsvps';
|
|
const tableId = globalConfig.tables[tableKey] || globalConfig.tables[tableName];
|
|
if (tableId) {
|
|
console.log(`[nocodb-events] Using global table ID for ${tableName}:`, tableId);
|
|
return tableId;
|
|
}
|
|
}
|
|
|
|
// Try runtime config as fallback
|
|
const config = useRuntimeConfig();
|
|
if (tableName === 'Events' && config.nocodb?.eventsTableId) {
|
|
console.log(`[nocodb-events] Using runtime config table ID for ${tableName}:`, config.nocodb.eventsTableId);
|
|
return config.nocodb.eventsTableId;
|
|
}
|
|
|
|
// Final fallback to default
|
|
const defaultTableId = tableName === 'Events' ? 'mt1mx3vkcw0vbmh' : 'rsvps-table-id';
|
|
console.log(`[nocodb-events] Using fallback table ID for ${tableName}:`, defaultTableId);
|
|
return defaultTableId;
|
|
};
|
|
|
|
export const createEventTableUrl = (table: EventTable | string) => {
|
|
let tableId: string;
|
|
|
|
if (table === EventTable.Events || table === 'Events') {
|
|
tableId = getEventTableId('Events');
|
|
} else if (table === EventTable.EventRSVPs || table === 'EventRSVPs') {
|
|
tableId = getEventTableId('EventRSVPs');
|
|
} else {
|
|
tableId = table.toString();
|
|
}
|
|
|
|
const url = `${getNocoDbConfiguration().url}/api/v2/tables/${tableId}/records`;
|
|
console.log('[nocodb-events] Event table URL:', url);
|
|
return url;
|
|
};
|
|
|
|
// Event field normalization (similar to member normalization)
|
|
export const normalizeEventFieldsFromNocoDB = (data: any): Event => {
|
|
console.log('[normalizeEventFieldsFromNocoDB] Input event keys:', Object.keys(data));
|
|
|
|
const normalized: any = { ...data };
|
|
|
|
// Field mapping for event data (adjust based on your actual NocoDB schema)
|
|
const eventFieldMap: Record<string, string> = {
|
|
'Title': 'title',
|
|
'Description': 'description',
|
|
'Event Type': 'event_type',
|
|
'Start Date': 'start_datetime',
|
|
'End Date': 'end_datetime',
|
|
'Location': 'location',
|
|
'Visibility': 'visibility',
|
|
'Status': 'status',
|
|
'Creator': 'creator',
|
|
'Max Attendees': 'max_attendees',
|
|
'Is Paid': 'is_paid',
|
|
'Cost Members': 'cost_members',
|
|
'Cost Non Members': 'cost_non_members',
|
|
// Handle snake_case fields (in case they come in already normalized)
|
|
'title': 'title',
|
|
'description': 'description',
|
|
'event_type': 'event_type',
|
|
'start_datetime': 'start_datetime',
|
|
'end_datetime': 'end_datetime',
|
|
'location': 'location',
|
|
'visibility': 'visibility',
|
|
'status': 'status',
|
|
'creator': 'creator',
|
|
'max_attendees': 'max_attendees',
|
|
'is_paid': 'is_paid',
|
|
'cost_members': 'cost_members',
|
|
'cost_non_members': 'cost_non_members'
|
|
};
|
|
|
|
// Apply field mapping
|
|
for (const [sourceKey, targetKey] of Object.entries(eventFieldMap)) {
|
|
if (sourceKey in data && data[sourceKey] !== undefined && data[sourceKey] !== null) {
|
|
normalized[targetKey] = data[sourceKey];
|
|
console.log(`[normalizeEventFieldsFromNocoDB] Mapped "${sourceKey}" -> "${targetKey}":`, data[sourceKey]);
|
|
}
|
|
}
|
|
|
|
// Ensure required fields exist with fallbacks
|
|
normalized.title = normalized.title || normalized['Title'] || '';
|
|
normalized.status = normalized.status || normalized['Status'] || 'active';
|
|
normalized.visibility = normalized.visibility || normalized['Visibility'] || 'public';
|
|
|
|
console.log('[normalizeEventFieldsFromNocoDB] Final normalized event keys:', Object.keys(normalized));
|
|
return normalized as Event;
|
|
};
|
|
|
|
/**
|
|
* Creates a client for interacting with the Events NocoDB table
|
|
* Following the same pattern as the working members client
|
|
*/
|
|
export function createNocoDBEventsClient() {
|
|
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);
|
|
}
|
|
|
|
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 = [
|
|
"title", "description", "event_type", "start_datetime", "end_datetime",
|
|
"location", "is_recurring", "recurrence_pattern", "max_attendees",
|
|
"is_paid", "cost_members", "cost_non_members", "member_pricing_enabled",
|
|
"visibility", "status", "creator", "current_attendees"
|
|
];
|
|
|
|
for (const field of allowedFields) {
|
|
if (field in eventData && eventData[field as keyof Event] !== undefined) {
|
|
cleanData[field] = eventData[field as keyof Event];
|
|
}
|
|
}
|
|
|
|
// Set defaults
|
|
cleanData.status = cleanData.status || 'active';
|
|
cleanData.visibility = cleanData.visibility || 'public';
|
|
cleanData.current_attendees = cleanData.current_attendees || '0';
|
|
|
|
console.log('[nocodb-events] Clean event data fields:', Object.keys(cleanData));
|
|
|
|
const result = await $fetch<Event>(createEventTableUrl(EventTable.Events), {
|
|
method: "POST",
|
|
headers: {
|
|
"xc-token": getNocoDbConfiguration().token,
|
|
},
|
|
body: cleanData,
|
|
});
|
|
|
|
console.log('[nocodb-events] Created event with ID:', result.id || (result as any).Id);
|
|
return normalizeEventFieldsFromNocoDB(result);
|
|
} catch (error: any) {
|
|
console.error('[nocodb-events] Create event failed:', error);
|
|
handleNocoDbError(error, 'createEvent', 'Event');
|
|
throw error;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Update an existing event
|
|
*/
|
|
async update(id: string, eventData: Partial<Event>) {
|
|
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 (simplified version)
|
|
*/
|
|
async findUserEvents(memberId: string, filters?: EventFilters) {
|
|
console.log('[nocodb-events] Finding events for member:', memberId);
|
|
|
|
try {
|
|
// For now, just get all visible events (we'll add RSVP logic later)
|
|
const events = await this.findAll(filters);
|
|
|
|
// TODO: Add RSVP lookup from separate table
|
|
// For now, return events without RSVP status
|
|
return {
|
|
list: events.list || [],
|
|
PageInfo: events.PageInfo
|
|
};
|
|
} catch (error: any) {
|
|
console.error('[nocodb-events] Error finding user events:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
};
|
|
|
|
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' };
|
|
|
|
return {
|
|
id: event.id,
|
|
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
|
|
}
|
|
};
|
|
}
|