// server/utils/nocodb-events.ts import type { Event, EventRSVP, EventsResponse, EventFilters } from '~/utils/types'; /** * Creates a client for interacting with the Events NocoDB table * Provides CRUD operations and specialized queries for events and RSVPs */ export function createNocoDBEventsClient() { const config = useRuntimeConfig(); const baseUrl = config.nocodb.url; const token = config.nocodb.token; const eventsBaseId = config.nocodb.eventsBaseId; const eventsTableId = config.nocodb.eventsTableId || 'events'; // fallback to table name if (!baseUrl || !token || !eventsBaseId) { throw new Error('Events NocoDB configuration is incomplete. Please check environment variables.'); } const headers = { 'xc-token': token, 'Content-Type': 'application/json' }; const eventsClient = { /** * Find all events with optional filtering */ async findAll(filters?: EventFilters & { limit?: number; offset?: number }) { const queryParams = new URLSearchParams(); if (filters?.limit) queryParams.set('limit', filters.limit.toString()); if (filters?.offset) queryParams.set('offset', filters.offset.toString()); // Build where clause for filtering const whereConditions: string[] = []; if (filters?.start_date && filters?.end_date) { whereConditions.push(`(start_datetime >= '${filters.start_date}' AND start_datetime <= '${filters.end_date}')`); } if (filters?.event_type) { whereConditions.push(`(event_type = '${filters.event_type}')`); } if (filters?.visibility) { whereConditions.push(`(visibility = '${filters.visibility}')`); } else if (filters?.user_role) { // Role-based visibility filtering if (filters.user_role === 'user') { whereConditions.push(`(visibility = 'public')`); } else if (filters.user_role === 'board') { whereConditions.push(`(visibility = 'public' OR visibility = 'board-only')`); } // Admin sees all events (no filter) } if (filters?.status) { whereConditions.push(`(status = '${filters.status}')`); } else { // Default to active events only whereConditions.push(`(status = 'active')`); } if (filters?.search) { whereConditions.push(`(title LIKE '%${filters.search}%' OR description LIKE '%${filters.search}%')`); } if (whereConditions.length > 0) { queryParams.set('where', whereConditions.join(' AND ')); } // Sort by start date queryParams.set('sort', 'start_datetime'); const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}?${queryParams.toString()}`; const response = await $fetch(url, { method: 'GET', headers }); return response; }, /** * Find a single event by ID */ async findOne(id: string) { const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}/${id}`; return await $fetch(url, { method: 'GET', headers }); }, /** * Create a new event */ async create(eventData: Partial) { const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}`; // Set default values const data = { ...eventData, status: eventData.status || 'active', created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; return await $fetch(url, { method: 'POST', headers, body: data }); }, /** * Update an existing event */ async update(id: string, eventData: Partial) { const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}/${id}`; const data = { ...eventData, updated_at: new Date().toISOString() }; return await $fetch(url, { method: 'PATCH', headers, body: data }); }, /** * Delete an event */ async delete(id: string) { const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}/${id}`; return await $fetch(url, { method: 'DELETE', headers }); }, /** * Create an RSVP record for an event */ async createRSVP(rsvpData: Partial) { const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}`; const data = { ...rsvpData, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; return await $fetch(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 = '${eventId}')`); queryParams.set('sort', 'created_at'); const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}?${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', `(event_id = '${eventId}' AND member_id = '${memberId}')`); queryParams.set('limit', '1'); const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}?${queryParams.toString()}`; const response = await $fetch(url, { method: 'GET', headers }); return response?.list?.[0] || null; }, /** * Update an RSVP record */ async updateRSVP(id: string, rsvpData: Partial) { const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}/${id}`; const data = { ...rsvpData, updated_at: new Date().toISOString() }; return await $fetch(url, { method: 'PATCH', headers, body: data }); }, /** * Update event attendance count (for optimization) */ async updateAttendeeCount(eventId: string, count: number) { const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}/${eventId}`; return await $fetch(url, { method: 'PATCH', headers, body: { 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) { // First get all visible events const events = await this.findAll(filters); if (!events.list || events.list.length === 0) { return { list: [], PageInfo: events.PageInfo }; } // Get user's RSVPs for these events const eventIds = events.list.map((e: Event) => e.id); const rsvpQueryParams = new URLSearchParams(); rsvpQueryParams.set('where', `(member_id = '${memberId}' AND event_id IN (${eventIds.map(id => `'${id}'`).join(',')}))`); const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}?${rsvpQueryParams.toString()}`; const rsvps = await $fetch(url, { 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 const eventsWithRSVP = events.list.map((event: Event) => ({ ...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 { const event = await this.findOne(eventId); if (!event.max_attendees) return false; // Unlimited capacity const maxAttendees = parseInt(event.max_attendees); const currentAttendees = event.current_attendees || 0; return currentAttendees >= maxAttendees; } }; 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 } }; }