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

345 lines
10 KiB
TypeScript

// 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<Event>(url, {
method: 'GET',
headers
});
},
/**
* Create a new event
*/
async create(eventData: Partial<Event>) {
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<Event>(url, {
method: 'POST',
headers,
body: data
});
},
/**
* Update an existing event
*/
async update(id: string, eventData: Partial<Event>) {
const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}/${id}`;
const data = {
...eventData,
updated_at: new Date().toISOString()
};
return await $fetch<Event>(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<EventRSVP>) {
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<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 = '${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<EventRSVP>) {
const url = `${baseUrl}/api/v1/db/data/v1/${eventsBaseId}/${eventsTableId}/${id}`;
const data = {
...rsvpData,
updated_at: 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/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<boolean> {
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
}
};
}