Compare commits
2 Commits
287af29f6c
...
70b77fbe9f
| Author | SHA1 | Date |
|---|---|---|
|
|
70b77fbe9f | |
|
|
7d55468a21 |
|
|
@ -4,25 +4,36 @@ import { createSessionManager } from '~/server/utils/session';
|
||||||
import type { EventFilters } from '~/utils/types';
|
import type { EventFilters } from '~/utils/types';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
|
console.log('[api/events.get] =========================');
|
||||||
|
console.log('[api/events.get] GET /api/events - List all events');
|
||||||
|
console.log('[api/events.get] Request from:', getClientIP(event));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const query = getQuery(event) as EventFilters & {
|
const query = getQuery(event) as EventFilters & {
|
||||||
limit?: string;
|
limit?: string;
|
||||||
offset?: string;
|
offset?: string;
|
||||||
calendar_format?: string
|
calendar_format?: string;
|
||||||
|
force?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Get user session using the proper SessionManager
|
console.log('[api/events.get] Query parameters:', query);
|
||||||
|
|
||||||
|
// Get user session using the working session manager
|
||||||
const sessionManager = createSessionManager();
|
const sessionManager = createSessionManager();
|
||||||
const cookieHeader = getHeader(event, 'cookie');
|
const cookieHeader = getHeader(event, 'cookie');
|
||||||
const session = sessionManager.getSession(cookieHeader);
|
const session = sessionManager.getSession(cookieHeader);
|
||||||
|
|
||||||
if (!session || !session.user) {
|
if (!session) {
|
||||||
|
console.log('[api/events.get] ❌ No valid session found');
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
statusMessage: 'Authentication required'
|
statusMessage: 'Authentication required'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[api/events.get] ✅ Valid session found for user:', session.user.email);
|
||||||
|
console.log('[api/events.get] User tier:', session.user.tier);
|
||||||
|
|
||||||
const eventsClient = createNocoDBEventsClient();
|
const eventsClient = createNocoDBEventsClient();
|
||||||
|
|
||||||
// Build filters with user role
|
// Build filters with user role
|
||||||
|
|
@ -43,9 +54,13 @@ export default defineEventHandler(async (event) => {
|
||||||
filters.end_date = endDate.toISOString();
|
filters.end_date = endDate.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[api/events.get] Fetching events with filters:', filters);
|
||||||
|
|
||||||
// Get events from database
|
// Get events from database
|
||||||
const response = await eventsClient.findUserEvents(session.user.id, filters);
|
const response = await eventsClient.findUserEvents(session.user.id, filters);
|
||||||
|
|
||||||
|
console.log('[api/events.get] ✅ Successfully fetched', response.list.length, 'events');
|
||||||
|
|
||||||
// Transform for FullCalendar if requested
|
// Transform for FullCalendar if requested
|
||||||
if (query.calendar_format === 'true') {
|
if (query.calendar_format === 'true') {
|
||||||
const calendarEvents = response.list.map(transformEventForCalendar);
|
const calendarEvents = response.list.map(transformEventForCalendar);
|
||||||
|
|
@ -64,10 +79,10 @@ export default defineEventHandler(async (event) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error fetching events:', error);
|
console.error('[api/events.get] ❌ Error fetching events:', error);
|
||||||
|
|
||||||
// Re-throw authentication errors as-is
|
// Re-throw createError instances
|
||||||
if (error.statusCode === 401) {
|
if (error.statusCode) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,40 @@ import { createSessionManager } from '~/server/utils/session';
|
||||||
import type { EventCreateRequest } from '~/utils/types';
|
import type { EventCreateRequest } from '~/utils/types';
|
||||||
|
|
||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
|
console.log('[api/events.post] =========================');
|
||||||
|
console.log('[api/events.post] POST /api/events - Create event');
|
||||||
|
console.log('[api/events.post] Request from:', getClientIP(event));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const body = await readBody(event) as EventCreateRequest;
|
const body = await readBody(event) as EventCreateRequest;
|
||||||
|
|
||||||
// Get user session using the proper SessionManager
|
console.log('[api/events.post] Event data received:', {
|
||||||
|
title: body.title,
|
||||||
|
event_type: body.event_type,
|
||||||
|
start_datetime: body.start_datetime,
|
||||||
|
end_datetime: body.end_datetime,
|
||||||
|
visibility: body.visibility
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get user session using the working session manager
|
||||||
const sessionManager = createSessionManager();
|
const sessionManager = createSessionManager();
|
||||||
const cookieHeader = getHeader(event, 'cookie');
|
const cookieHeader = getHeader(event, 'cookie');
|
||||||
const session = sessionManager.getSession(cookieHeader);
|
const session = sessionManager.getSession(cookieHeader);
|
||||||
|
|
||||||
if (!session || !session.user) {
|
if (!session || !session.user) {
|
||||||
|
console.log('[api/events.post] ❌ No valid session found');
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 401,
|
statusCode: 401,
|
||||||
statusMessage: 'Authentication required'
|
statusMessage: 'Authentication required'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[api/events.post] ✅ Valid session found for user:', session.user.email);
|
||||||
|
console.log('[api/events.post] User tier:', session.user.tier);
|
||||||
|
|
||||||
// Check if user has permission to create events (board or admin only)
|
// Check if user has permission to create events (board or admin only)
|
||||||
if (session.user.tier !== 'board' && session.user.tier !== 'admin') {
|
if (session.user.tier !== 'board' && session.user.tier !== 'admin') {
|
||||||
|
console.log('[api/events.post] ❌ Insufficient permissions. User tier:', session.user.tier);
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
statusCode: 403,
|
||||||
statusMessage: 'Only board members and administrators can create events'
|
statusMessage: 'Only board members and administrators can create events'
|
||||||
|
|
@ -29,6 +46,7 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!body.title || !body.start_datetime || !body.end_datetime) {
|
if (!body.title || !body.start_datetime || !body.end_datetime) {
|
||||||
|
console.log('[api/events.post] ❌ Missing required fields');
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
statusMessage: 'Title, start date, and end date are required'
|
statusMessage: 'Title, start date, and end date are required'
|
||||||
|
|
@ -40,6 +58,7 @@ export default defineEventHandler(async (event) => {
|
||||||
const endDate = new Date(body.end_datetime);
|
const endDate = new Date(body.end_datetime);
|
||||||
|
|
||||||
if (startDate >= endDate) {
|
if (startDate >= endDate) {
|
||||||
|
console.log('[api/events.post] ❌ Invalid date range');
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
statusMessage: 'End date must be after start date'
|
statusMessage: 'End date must be after start date'
|
||||||
|
|
@ -49,6 +68,7 @@ export default defineEventHandler(async (event) => {
|
||||||
// Validate event type
|
// Validate event type
|
||||||
const validEventTypes = ['meeting', 'social', 'fundraiser', 'workshop', 'board-only'];
|
const validEventTypes = ['meeting', 'social', 'fundraiser', 'workshop', 'board-only'];
|
||||||
if (!validEventTypes.includes(body.event_type)) {
|
if (!validEventTypes.includes(body.event_type)) {
|
||||||
|
console.log('[api/events.post] ❌ Invalid event type:', body.event_type);
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
statusMessage: 'Invalid event type'
|
statusMessage: 'Invalid event type'
|
||||||
|
|
@ -58,6 +78,7 @@ export default defineEventHandler(async (event) => {
|
||||||
// Validate visibility
|
// Validate visibility
|
||||||
const validVisibilities = ['public', 'board-only', 'admin-only'];
|
const validVisibilities = ['public', 'board-only', 'admin-only'];
|
||||||
if (!validVisibilities.includes(body.visibility)) {
|
if (!validVisibilities.includes(body.visibility)) {
|
||||||
|
console.log('[api/events.post] ❌ Invalid visibility:', body.visibility);
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
statusMessage: 'Invalid visibility setting'
|
statusMessage: 'Invalid visibility setting'
|
||||||
|
|
@ -66,12 +87,15 @@ export default defineEventHandler(async (event) => {
|
||||||
|
|
||||||
// Admin-only visibility can only be set by admins
|
// Admin-only visibility can only be set by admins
|
||||||
if (body.visibility === 'admin-only' && session.user.tier !== 'admin') {
|
if (body.visibility === 'admin-only' && session.user.tier !== 'admin') {
|
||||||
|
console.log('[api/events.post] ❌ Admin-only event creation attempted by non-admin');
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 403,
|
statusCode: 403,
|
||||||
statusMessage: 'Only administrators can create admin-only events'
|
statusMessage: 'Only administrators can create admin-only events'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[api/events.post] ✅ Validation passed, creating event...');
|
||||||
|
|
||||||
const eventsClient = createNocoDBEventsClient();
|
const eventsClient = createNocoDBEventsClient();
|
||||||
|
|
||||||
// Prepare event data
|
// Prepare event data
|
||||||
|
|
@ -90,14 +114,18 @@ export default defineEventHandler(async (event) => {
|
||||||
cost_non_members: body.cost_non_members || '',
|
cost_non_members: body.cost_non_members || '',
|
||||||
member_pricing_enabled: body.member_pricing_enabled || 'true',
|
member_pricing_enabled: body.member_pricing_enabled || 'true',
|
||||||
visibility: body.visibility as 'public' | 'board-only' | 'admin-only',
|
visibility: body.visibility as 'public' | 'board-only' | 'admin-only',
|
||||||
status: (body.status as 'active' | 'cancelled' | 'completed' | 'draft') || 'active',
|
status: (body.status || 'active') as 'active' | 'cancelled' | 'completed' | 'draft',
|
||||||
creator: session.user.id,
|
creator: session.user.id,
|
||||||
current_attendees: 0
|
current_attendees: '0'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log('[api/events.post] Event data prepared:', Object.keys(eventData));
|
||||||
|
|
||||||
// Create the event
|
// Create the event
|
||||||
const newEvent = await eventsClient.create(eventData);
|
const newEvent = await eventsClient.create(eventData);
|
||||||
|
|
||||||
|
console.log('[api/events.post] ✅ Event created successfully with ID:', newEvent.id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: newEvent,
|
data: newEvent,
|
||||||
|
|
@ -105,7 +133,7 @@ export default defineEventHandler(async (event) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error creating event:', error);
|
console.error('[api/events.post] ❌ Error creating event:', error);
|
||||||
|
|
||||||
// Re-throw createError instances
|
// Re-throw createError instances
|
||||||
if (error.statusCode) {
|
if (error.statusCode) {
|
||||||
|
|
|
||||||
|
|
@ -1,325 +1,387 @@
|
||||||
// server/utils/nocodb-events.ts
|
// server/utils/nocodb-events.ts
|
||||||
import type { Event, EventRSVP, EventsResponse, EventFilters } from '~/utils/types';
|
import type { Event, EventRSVP, EventsResponse, EventFilters } from '~/utils/types';
|
||||||
|
import { createSessionManager } from '~/server/utils/session';
|
||||||
import { getEffectiveNocoDBConfig } from './admin-config';
|
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) {
|
||||||
|
const tableKey = tableName === 'Events' ? 'events' : 'event_rsvps';
|
||||||
|
const tableId = effectiveConfig.tables[tableKey] || effectiveConfig.tables[tableName];
|
||||||
|
if (tableId) {
|
||||||
|
console.log(`[nocodb-events] Using admin config table ID for ${tableName}:`, tableId);
|
||||||
|
return tableId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`[nocodb-events] Admin config not available, trying fallback for ${tableName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get table ID from global configuration
|
||||||
|
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
|
* Creates a client for interacting with the Events NocoDB table
|
||||||
* Provides CRUD operations and specialized queries for events and RSVPs
|
* Following the same pattern as the working members client
|
||||||
*/
|
*/
|
||||||
export function createNocoDBEventsClient() {
|
export function createNocoDBEventsClient() {
|
||||||
// Get effective configuration from admin config system
|
// Validate API token before using it (from incoming version)
|
||||||
const effectiveConfig = getEffectiveNocoDBConfig();
|
const config = getNocoDbConfiguration();
|
||||||
|
const token = config.token;
|
||||||
|
|
||||||
const baseUrl = effectiveConfig.url;
|
|
||||||
const token = effectiveConfig.token;
|
|
||||||
const eventsBaseId = effectiveConfig.baseId;
|
|
||||||
const eventsTableId = effectiveConfig.tables.events || 'events';
|
|
||||||
const rsvpTableId = effectiveConfig.tables.rsvps || 'event_rsvps';
|
|
||||||
|
|
||||||
if (!baseUrl || !token || !eventsBaseId) {
|
|
||||||
throw new Error('Events NocoDB configuration is incomplete. Please check environment variables.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate API token before using it
|
|
||||||
if (token) {
|
if (token) {
|
||||||
const cleanToken = token.trim();
|
const cleanToken = token.trim();
|
||||||
|
|
||||||
// Check for non-ASCII characters that would cause ByteString errors
|
// Check for non-ASCII characters that would cause ByteString errors
|
||||||
if (!/^[\x00-\xFF]*$/.test(cleanToken)) {
|
if (!/^[\x00-\xFF]*$/.test(cleanToken)) {
|
||||||
console.error('[nocodb-events] ❌ CRITICAL ERROR: API token contains invalid Unicode characters!');
|
console.error('[nocodb-events] ❌ CRITICAL ERROR: API token contains invalid Unicode characters!');
|
||||||
console.error('[nocodb-events] This will cause ByteString conversion errors in HTTP headers.');
|
|
||||||
console.error('[nocodb-events] Please update the API token in the admin configuration.');
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
statusMessage: 'Events system: NocoDB API token contains invalid characters. Please reconfigure the database connection in the admin panel with a valid API token.'
|
statusMessage: 'Events system: NocoDB API token contains invalid characters. Please reconfigure the database connection.'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Additional validation for common token issues
|
// Additional validation for common token issues
|
||||||
if (cleanToken.includes('•') || cleanToken.includes('…') || cleanToken.includes('"') || cleanToken.includes('"')) {
|
if (cleanToken.includes('•') || cleanToken.includes('…') || cleanToken.includes('"') || cleanToken.includes('"')) {
|
||||||
console.error('[nocodb-events] ❌ CRITICAL ERROR: API token contains formatting characters!');
|
console.error('[nocodb-events] ❌ CRITICAL ERROR: API token contains formatting characters!');
|
||||||
console.error('[nocodb-events] Found characters like bullets (•), quotes, etc. that break HTTP headers.');
|
|
||||||
console.error('[nocodb-events] Please copy the raw API token from NocoDB without any formatting.');
|
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
statusMessage: 'Events system: NocoDB API token contains formatting characters (bullets, quotes, etc.). Please reconfigure with the raw token from NocoDB.'
|
statusMessage: 'Events system: NocoDB API token contains formatting characters. Please reconfigure with the raw token from NocoDB.'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = {
|
|
||||||
'xc-token': token,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
};
|
|
||||||
|
|
||||||
const eventsClient = {
|
const eventsClient = {
|
||||||
/**
|
/**
|
||||||
* Find all events with optional filtering
|
* Find all events with optional filtering
|
||||||
*/
|
*/
|
||||||
async findAll(filters?: EventFilters & { limit?: number; offset?: number }) {
|
async findAll(filters?: EventFilters & { limit?: number; offset?: number }): Promise<EventsListResponse> {
|
||||||
const queryParams = new URLSearchParams();
|
console.log('[nocodb-events] 🔍 DEBUG: Filters received:', filters);
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
if (filters?.limit) queryParams.set('limit', filters.limit.toString());
|
try {
|
||||||
if (filters?.offset) queryParams.set('offset', filters.offset.toString());
|
// Build query parameters like the members system
|
||||||
|
const params: Record<string, any> = {
|
||||||
|
limit: filters?.limit || 1000,
|
||||||
|
};
|
||||||
|
|
||||||
// TEMPORARILY: Try different query approach to isolate the issue
|
if (filters?.offset) {
|
||||||
// Remove complex filtering until we can identify what works
|
params.offset = filters.offset;
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[nocodb-events] 🔍 DEBUG: Filters received:', JSON.stringify(filters, null, 2));
|
// 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');
|
||||||
|
}
|
||||||
|
|
||||||
// Try only status filter first (simplest case)
|
// TEMPORARILY DISABLE COMPLEX FILTERING TO ISOLATE ISSUE
|
||||||
if (filters?.status) {
|
console.log('[nocodb-events] ⚠️ Temporarily skipping date/role/search filtering to isolate issue');
|
||||||
console.log('[nocodb-events] 🔍 Adding status filter:', filters.status);
|
|
||||||
queryParams.set('where', `(status,eq,${filters.status})`);
|
// TEMPORARILY DISABLE SORTING TO ISOLATE ISSUE
|
||||||
} else {
|
console.log('[nocodb-events] ⚠️ Also temporarily skipping sorting to isolate issue');
|
||||||
// Try no status filter at all to see if that works
|
|
||||||
console.log('[nocodb-events] 🔍 No status filter - fetching all records');
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip date filtering completely for now
|
|
||||||
console.log('[nocodb-events] ⚠️ Temporarily skipping date/role/search filtering to isolate issue');
|
|
||||||
|
|
||||||
// ALSO temporarily skip sorting to see if that's the issue
|
|
||||||
console.log('[nocodb-events] ⚠️ Also temporarily skipping sorting to isolate issue');
|
|
||||||
|
|
||||||
const url = `${baseUrl}/api/v2/tables/${eventsTableId}/records?${queryParams.toString()}`;
|
|
||||||
|
|
||||||
const response = await $fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers
|
|
||||||
});
|
|
||||||
|
|
||||||
return response;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find a single event by ID
|
* Find a single event by ID
|
||||||
*/
|
*/
|
||||||
async findOne(id: string) {
|
async findOne(id: string) {
|
||||||
const url = `${baseUrl}/api/v2/tables/${eventsTableId}/records/${id}`;
|
console.log('[nocodb-events] Fetching event ID:', id);
|
||||||
|
|
||||||
return await $fetch<Event>(url, {
|
try {
|
||||||
method: 'GET',
|
const result = await $fetch<Event>(`${createEventTableUrl(EventTable.Events)}/${id}`, {
|
||||||
headers
|
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
|
* Create a new event
|
||||||
*/
|
*/
|
||||||
async create(eventData: Partial<Event>) {
|
async create(eventData: Partial<Event>) {
|
||||||
const url = `${baseUrl}/api/v2/tables/${eventsTableId}/records`;
|
console.log('[nocodb-events] Creating event with fields:', Object.keys(eventData));
|
||||||
|
|
||||||
// Set default values
|
try {
|
||||||
const data = {
|
// Clean data like members system
|
||||||
...eventData,
|
const cleanData: Record<string, any> = {};
|
||||||
status: eventData.status || 'active',
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
updated_at: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
return await $fetch<Event>(url, {
|
// Only include allowed event fields
|
||||||
method: 'POST',
|
const allowedFields = [
|
||||||
headers,
|
"title", "description", "event_type", "start_datetime", "end_datetime",
|
||||||
body: data
|
"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
|
* Update an existing event
|
||||||
*/
|
*/
|
||||||
async update(id: string, eventData: Partial<Event>) {
|
async update(id: string, eventData: Partial<Event>) {
|
||||||
const url = `${baseUrl}/api/v2/tables/${eventsTableId}/records`;
|
console.log('[nocodb-events] Updating event:', id);
|
||||||
|
|
||||||
const data = {
|
try {
|
||||||
Id: parseInt(id),
|
// Clean data like members system
|
||||||
...eventData,
|
const cleanData: Record<string, any> = {};
|
||||||
updated_at: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
return await $fetch<Event>(url, {
|
const allowedFields = [
|
||||||
method: 'PATCH',
|
"title", "description", "event_type", "start_datetime", "end_datetime",
|
||||||
headers,
|
"location", "is_recurring", "recurrence_pattern", "max_attendees",
|
||||||
body: data
|
"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
|
* Delete an event
|
||||||
*/
|
*/
|
||||||
async delete(id: string) {
|
async delete(id: string) {
|
||||||
const url = `${baseUrl}/api/v2/tables/${eventsTableId}/records`;
|
console.log('[nocodb-events] Deleting event:', id);
|
||||||
|
|
||||||
return await $fetch(url, {
|
try {
|
||||||
method: 'DELETE',
|
const result = await $fetch(createEventTableUrl(EventTable.Events), {
|
||||||
headers,
|
method: "DELETE",
|
||||||
body: { Id: parseInt(id) }
|
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;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an RSVP record for an event
|
* Get events for a specific user (simplified version)
|
||||||
*/
|
|
||||||
async createRSVP(rsvpData: Partial<EventRSVP>) {
|
|
||||||
const url = `${baseUrl}/api/v2/tables/${rsvpTableId}/records`;
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
...rsvpData,
|
|
||||||
created_time: new Date().toISOString(),
|
|
||||||
updated_time: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
return await $fetch<EventRSVP>(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers,
|
|
||||||
body: data
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find RSVPs for a specific event
|
|
||||||
*/
|
|
||||||
async findEventRSVPs(eventId: string) {
|
|
||||||
const queryParams = new URLSearchParams();
|
|
||||||
queryParams.set('where', `(event_id,eq,${eventId})`);
|
|
||||||
queryParams.set('sort', 'created_time');
|
|
||||||
|
|
||||||
const url = `${baseUrl}/api/v2/tables/${rsvpTableId}/records?${queryParams.toString()}`;
|
|
||||||
|
|
||||||
return await $fetch(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a user's RSVP for a specific event
|
|
||||||
*/
|
|
||||||
async findUserRSVP(eventId: string, memberId: string) {
|
|
||||||
const queryParams = new URLSearchParams();
|
|
||||||
queryParams.set('where', `~and((event_id,eq,${eventId}),(member_id,eq,${memberId}))`);
|
|
||||||
queryParams.set('limit', '1');
|
|
||||||
|
|
||||||
const url = `${baseUrl}/api/v2/tables/${rsvpTableId}/records?${queryParams.toString()}`;
|
|
||||||
|
|
||||||
const response = await $fetch<{ list?: EventRSVP[]; PageInfo?: any }>(url, {
|
|
||||||
method: 'GET',
|
|
||||||
headers
|
|
||||||
});
|
|
||||||
|
|
||||||
return response?.list?.[0] || null;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update an RSVP record
|
|
||||||
*/
|
|
||||||
async updateRSVP(id: string, rsvpData: Partial<EventRSVP>) {
|
|
||||||
const url = `${baseUrl}/api/v2/tables/${rsvpTableId}/records`;
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
Id: parseInt(id),
|
|
||||||
...rsvpData,
|
|
||||||
updated_time: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
return await $fetch<EventRSVP>(url, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers,
|
|
||||||
body: data
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update event attendance count (for optimization)
|
|
||||||
*/
|
|
||||||
async updateAttendeeCount(eventId: string, count: number) {
|
|
||||||
const url = `${baseUrl}/api/v2/tables/${eventsTableId}/records`;
|
|
||||||
|
|
||||||
return await $fetch(url, {
|
|
||||||
method: 'PATCH',
|
|
||||||
headers,
|
|
||||||
body: {
|
|
||||||
Id: parseInt(eventId),
|
|
||||||
current_attendees: count.toString(),
|
|
||||||
updated_at: new Date().toISOString()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get events for a specific user with their RSVP status
|
|
||||||
*/
|
*/
|
||||||
async findUserEvents(memberId: string, filters?: EventFilters) {
|
async findUserEvents(memberId: string, filters?: EventFilters) {
|
||||||
// First get all visible events
|
console.log('[nocodb-events] Finding events for member:', memberId);
|
||||||
const events = await this.findAll(filters) as { list?: Event[]; PageInfo?: any };
|
|
||||||
|
|
||||||
if (!events.list || events.list.length === 0) {
|
try {
|
||||||
return { list: [], PageInfo: events.PageInfo };
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user's RSVPs for these events
|
|
||||||
// Fix: Use 'Id' (capital I) as that's the actual field name from NocoDB
|
|
||||||
const eventIds = events.list.map((e: any) => e.Id);
|
|
||||||
|
|
||||||
// Skip RSVP lookup if no valid event IDs
|
|
||||||
if (!eventIds.length || eventIds.some(id => !id)) {
|
|
||||||
console.log('[nocodb-events] ⚠️ No valid event IDs found, skipping RSVP lookup');
|
|
||||||
return { list: events.list, PageInfo: events.PageInfo };
|
|
||||||
}
|
|
||||||
|
|
||||||
const rsvpQueryParams = new URLSearchParams();
|
|
||||||
const eventIdConditions = eventIds.map(id => `(event_id,eq,${id})`).join(',');
|
|
||||||
rsvpQueryParams.set('where', `~and((member_id,eq,${memberId}),~or(${eventIdConditions}))`);
|
|
||||||
|
|
||||||
const rsvpUrl = `${baseUrl}/api/v2/tables/${rsvpTableId}/records?${rsvpQueryParams.toString()}`;
|
|
||||||
|
|
||||||
const rsvps = await $fetch<{ list?: EventRSVP[]; PageInfo?: any }>(rsvpUrl, {
|
|
||||||
method: 'GET',
|
|
||||||
headers
|
|
||||||
});
|
|
||||||
|
|
||||||
// Map RSVPs to events
|
|
||||||
const rsvpMap = new Map();
|
|
||||||
if (rsvps.list) {
|
|
||||||
rsvps.list.forEach((rsvp: EventRSVP) => {
|
|
||||||
rsvpMap.set(rsvp.event_id, rsvp);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add RSVP information to events
|
|
||||||
// Fix: Use 'Id' (capital I) as that's the actual field name from NocoDB
|
|
||||||
const eventsWithRSVP = events.list.map((event: any) => ({
|
|
||||||
...event,
|
|
||||||
user_rsvp: rsvpMap.get(event.Id) || null
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
list: eventsWithRSVP,
|
|
||||||
PageInfo: events.PageInfo
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate payment reference for RSVP
|
|
||||||
*/
|
|
||||||
generatePaymentReference(memberId: string, date?: Date): string {
|
|
||||||
const referenceDate = date || new Date();
|
|
||||||
const dateString = referenceDate.toISOString().split('T')[0]; // YYYY-MM-DD
|
|
||||||
return `EVT-${memberId}-${dateString}`;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if event has reached capacity
|
|
||||||
*/
|
|
||||||
async isEventFull(eventId: string): Promise<boolean> {
|
|
||||||
const event = await this.findOne(eventId);
|
|
||||||
|
|
||||||
if (!event.max_attendees) return false; // Unlimited capacity
|
|
||||||
|
|
||||||
const maxAttendees = parseInt(event.max_attendees);
|
|
||||||
const currentAttendees = parseInt(String(event.current_attendees || 0));
|
|
||||||
|
|
||||||
return currentAttendees >= maxAttendees;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -328,9 +390,8 @@ export function createNocoDBEventsClient() {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility function to transform Event data for FullCalendar
|
* Utility function to transform Event data for FullCalendar
|
||||||
* Updated to use correct field names from NocoDB (Id instead of id)
|
|
||||||
*/
|
*/
|
||||||
export function transformEventForCalendar(event: any): any {
|
export function transformEventForCalendar(event: Event): any {
|
||||||
const eventTypeColors = {
|
const eventTypeColors = {
|
||||||
'meeting': { bg: '#2196f3', border: '#1976d2' },
|
'meeting': { bg: '#2196f3', border: '#1976d2' },
|
||||||
'social': { bg: '#4caf50', border: '#388e3c' },
|
'social': { bg: '#4caf50', border: '#388e3c' },
|
||||||
|
|
@ -343,8 +404,8 @@ export function transformEventForCalendar(event: any): any {
|
||||||
{ bg: '#757575', border: '#424242' };
|
{ bg: '#757575', border: '#424242' };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: event.Id, // Fix: Use 'Id' (capital I) from NocoDB
|
id: event.id,
|
||||||
title: event.title || 'Untitled Event',
|
title: event.title,
|
||||||
start: event.start_datetime,
|
start: event.start_datetime,
|
||||||
end: event.end_datetime,
|
end: event.end_datetime,
|
||||||
backgroundColor: colors.bg,
|
backgroundColor: colors.bg,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue