diff --git a/components/CreateEventDialog.vue b/components/CreateEventDialog.vue index efd1316..95f0fe6 100644 --- a/components/CreateEventDialog.vue +++ b/components/CreateEventDialog.vue @@ -143,8 +143,33 @@ /> - + + + + + + + + + + + ({ cost_non_members: '', member_pricing_enabled: 'true', visibility: 'public', - status: 'active' + status: 'active', + guests_permitted: 'false', + max_guests_permitted: '0' }); +// Guest settings +const allowGuests = ref(false); +const maxGuestsPerPerson = ref(1); + // Computed const show = computed({ get: () => props.modelValue, @@ -364,6 +395,35 @@ watch(memberPricingEnabled, (newValue) => { eventData.member_pricing_enabled = newValue ? 'true' : 'false'; }); +watch(allowGuests, (newValue) => { + eventData.guests_permitted = newValue ? 'true' : 'false'; + if (!newValue) { + eventData.max_guests_permitted = '0'; + maxGuestsPerPerson.value = 1; + } +}); + +watch(maxGuestsPerPerson, (newValue) => { + if (allowGuests.value) { + eventData.max_guests_permitted = newValue.toString(); + } +}); + +// Fix date picker binding - ensure proper syncing +watch(startDateModel, (newDate) => { + if (newDate instanceof Date) { + eventData.start_datetime = newDate.toISOString(); + console.log('[CreateEventDialog] Start date updated:', eventData.start_datetime); + } +}); + +watch(endDateModel, (newDate) => { + if (newDate instanceof Date) { + eventData.end_datetime = newDate.toISOString(); + console.log('[CreateEventDialog] End date updated:', eventData.end_datetime); + } +}); + watch(isRecurring, (newValue) => { eventData.is_recurring = newValue ? 'true' : 'false'; if (newValue) { @@ -440,6 +500,8 @@ const resetForm = () => { eventData.cost_members = ''; eventData.cost_non_members = ''; eventData.member_pricing_enabled = 'true'; + eventData.guests_permitted = 'false'; + eventData.max_guests_permitted = '0'; eventData.visibility = 'public'; eventData.status = 'active'; eventData.is_recurring = 'false'; @@ -449,10 +511,13 @@ const resetForm = () => { startDateModel.value = null; endDateModel.value = null; + // Reset UI state isPaidEvent.value = false; memberPricingEnabled.value = true; isRecurring.value = false; recurrenceFrequency.value = 'weekly'; + allowGuests.value = false; + maxGuestsPerPerson.value = 1; form.value?.resetValidation(); }; diff --git a/components/EventDetailsDialog.vue b/components/EventDetailsDialog.vue index 85b5cbd..8bacf56 100644 --- a/components/EventDetailsDialog.vue +++ b/components/EventDetailsDialog.vue @@ -192,6 +192,28 @@ + + + + + + mdi-account-group + Bring Guests + + + This event allows up to {{ maxGuestsAllowed }} additional guests per person. + + + + + + - - - mdi-check - {{ isEventFull ? 'Join Waitlist' : 'Confirm Attendance' }} - - - - mdi-close - Decline - - + + mdi-check + {{ isEventFull ? 'Join Waitlist' : 'RSVP' }} + @@ -286,6 +295,7 @@ const { rsvpToEvent } = useEvents(); const rsvpValid = ref(false); const rsvpLoading = ref(false); const rsvpNotes = ref(''); +const selectedGuests = ref(0); // Computed properties const show = computed({ @@ -440,6 +450,28 @@ const paymentInfo = computed(() => ({ recipient: 'MonacoUSA Association' // This should come from config })); +// Guest functionality +const allowsGuests = computed(() => { + return props.event?.guests_permitted === 'true'; +}); + +const maxGuestsAllowed = computed(() => { + if (!allowsGuests.value) return 0; + return parseInt(props.event?.max_guests_permitted || '0'); +}); + +const guestOptions = computed(() => { + const max = maxGuestsAllowed.value; + const options = []; + for (let i = 0; i <= max; i++) { + options.push({ + title: i === 0 ? 'No additional guests' : `${i} guest${i > 1 ? 's' : ''}`, + value: i + }); + } + return options; +}); + // Methods const close = () => { show.value = false; @@ -452,21 +484,27 @@ const submitRSVP = async (status: 'confirmed' | 'declined') => { rsvpLoading.value = true; try { - // Extract database ID - props.event is a raw Event object, not FullCalendar object - // Database ID is stored in 'Id' field (capital I) from NocoDB - const databaseId = (props.event as any).Id || (props.event as any).extendedProps?.database_id || props.event.id; - console.log('[EventDetailsDialog] Using database ID for RSVP:', databaseId); - console.log('[EventDetailsDialog] Event object keys:', Object.keys(props.event)); - console.log('[EventDetailsDialog] Event Id field:', (props.event as any).Id); + // Use event_id field for consistent RSVP relationships + // This ensures RSVPs are linked properly to events using the business identifier + const eventId = props.event.event_id || + (props.event as any).extendedProps?.event_id || + (props.event as any).Id || // Fallback to database ID if event_id not available + props.event.id; - if (!databaseId) { - throw new Error('Unable to determine database ID for event'); + console.log('[EventDetailsDialog] Using event identifier for RSVP:', eventId); + console.log('[EventDetailsDialog] Event object keys:', Object.keys(props.event)); + console.log('[EventDetailsDialog] Event event_id field:', props.event.event_id); + console.log('[EventDetailsDialog] Event database Id field:', (props.event as any).Id); + + if (!eventId) { + throw new Error('Unable to determine event identifier'); } - await rsvpToEvent(databaseId, { + await rsvpToEvent(eventId, { member_id: '', // This will be filled by the composable rsvp_status: status, - rsvp_notes: rsvpNotes.value + rsvp_notes: rsvpNotes.value, + extra_guests: selectedGuests.value.toString() }); emit('rsvp-updated', props.event); diff --git a/composables/useEvents.ts b/composables/useEvents.ts index 47d98b4..d50c06f 100644 --- a/composables/useEvents.ts +++ b/composables/useEvents.ts @@ -106,13 +106,15 @@ export const useEvents = () => { }; /** - * RSVP to an event + * RSVP to an event with support for guests and real-time updates */ - const rsvpToEvent = async (eventId: string, rsvpData: Omit) => { + const rsvpToEvent = async (eventId: string, rsvpData: Omit & { extra_guests?: string }) => { loading.value = true; error.value = null; try { + console.log('[useEvents] RSVP to event:', eventId, 'with data:', rsvpData); + const response = await $fetch<{ success: boolean; data: any; message: string }>(`/api/events/${eventId}/rsvp`, { method: 'POST', body: { @@ -123,28 +125,46 @@ export const useEvents = () => { }); if (response.success) { - // Update local event data - match by database ID (stored in Id field or as fallback) - const eventIndex = events.value.findIndex(e => - (e as any).Id === eventId || e.id === eventId - ); + // Find event by event_id first, then fallback to database ID + let eventIndex = events.value.findIndex(e => e.event_id === eventId); + if (eventIndex === -1) { + eventIndex = events.value.findIndex(e => (e as any).Id === eventId || e.id === eventId); + } - console.log('[useEvents] Looking for event with database ID:', eventId, 'found at index:', eventIndex); + console.log('[useEvents] Event found at index:', eventIndex, 'using event_id:', eventId); if (eventIndex !== -1) { - events.value[eventIndex].user_rsvp = response.data; + const event = events.value[eventIndex]; - // Update attendee count if confirmed + // Update RSVP status + event.user_rsvp = response.data; + + // Calculate attendee count including guests if (rsvpData.rsvp_status === 'confirmed') { - const currentCount = typeof events.value[eventIndex].current_attendees === 'string' - ? parseInt(events.value[eventIndex].current_attendees) || 0 - : events.value[eventIndex].current_attendees || 0; - events.value[eventIndex].current_attendees = (currentCount + 1).toString(); + const currentCount = parseInt(event.current_attendees || '0'); + const guestCount = parseInt(rsvpData.extra_guests || '0'); + const totalAdded = 1 + guestCount; // Member + guests + + event.current_attendees = (currentCount + totalAdded).toString(); + + console.log('[useEvents] Updated attendee count:', { + previous: currentCount, + added: totalAdded, + new: event.current_attendees, + guests: guestCount + }); } + + // Trigger reactivity + events.value[eventIndex] = { ...event }; } - // Clear cache + // Clear cache for fresh data on next load cache.clear(); + // Force refresh events data to ensure accuracy + await fetchEvents({ force: true }); + return response.data; } else { throw new Error(response.message || 'Failed to RSVP'); @@ -158,6 +178,61 @@ export const useEvents = () => { } }; + /** + * Cancel RSVP to an event + */ + const cancelRSVP = async (eventId: string) => { + loading.value = true; + error.value = null; + + try { + // Find the event to get current RSVP info + let event = events.value.find(e => e.event_id === eventId); + if (!event) { + event = events.value.find(e => (e as any).Id === eventId || e.id === eventId); + } + + if (!event?.user_rsvp) { + throw new Error('No RSVP found to cancel'); + } + + const response = await $fetch<{ success: boolean; data: any; message: string }>(`/api/events/${eventId}/rsvp`, { + method: 'DELETE' + }); + + if (response.success) { + const eventIndex = events.value.findIndex(e => e === event); + + if (eventIndex !== -1) { + const currentCount = parseInt(events.value[eventIndex].current_attendees || '0'); + const guestCount = parseInt(events.value[eventIndex].user_rsvp?.extra_guests || '0'); + const totalRemoved = 1 + guestCount; // Member + guests + + // Update attendee count and remove RSVP + events.value[eventIndex].current_attendees = Math.max(0, currentCount - totalRemoved).toString(); + events.value[eventIndex].user_rsvp = undefined; + + // Trigger reactivity + events.value[eventIndex] = { ...events.value[eventIndex] }; + } + + // Clear cache and refresh + cache.clear(); + await fetchEvents({ force: true }); + + return response.data; + } else { + throw new Error(response.message || 'Failed to cancel RSVP'); + } + } catch (err: any) { + error.value = err.message || 'Failed to cancel RSVP'; + console.error('Error canceling RSVP:', err); + throw err; + } finally { + loading.value = false; + } + }; + /** * Update attendance for an event (board/admin only) */ diff --git a/server/api/events/[id]/rsvp.post.ts b/server/api/events/[id]/rsvp.post.ts index c00d549..a3437d2 100644 --- a/server/api/events/[id]/rsvp.post.ts +++ b/server/api/events/[id]/rsvp.post.ts @@ -85,6 +85,38 @@ export default defineEventHandler(async (event) => { } } + // Validate guest count if event allows guests + const extraGuests = parseInt((body as any).extra_guests || '0'); + if (extraGuests > 0) { + if (eventDetails.guests_permitted !== 'true') { + throw createError({ + statusCode: 400, + statusMessage: 'This event does not allow guests' + }); + } + + const maxGuestsAllowed = parseInt(eventDetails.max_guests_permitted || '0'); + if (extraGuests > maxGuestsAllowed) { + throw createError({ + statusCode: 400, + statusMessage: `Maximum ${maxGuestsAllowed} guests allowed per person` + }); + } + } + + // Check event capacity including guests + if (eventDetails.max_attendees && body.rsvp_status === 'confirmed') { + const maxCapacity = parseInt(eventDetails.max_attendees); + const currentAttendees = parseInt(eventDetails.current_attendees || '0'); + const totalRequested = 1 + extraGuests; // Member + guests + + if (currentAttendees + totalRequested > maxCapacity) { + // Auto-set to waitlist if over capacity + body.rsvp_status = 'waitlist'; + console.log(`[RSVP] Event at capacity, placing on waitlist: ${currentAttendees} + ${totalRequested} > ${maxCapacity}`); + } + } + // Create RSVP record const rsvpData = { record_type: 'rsvp', @@ -95,6 +127,7 @@ export default defineEventHandler(async (event) => { payment_reference: paymentReference, attended: 'false', rsvp_notes: body.rsvp_notes || '', + extra_guests: extraGuests.toString(), is_member_pricing: isMemberPricing, created_at: new Date().toISOString(), updated_at: new Date().toISOString() diff --git a/server/utils/nocodb-events.ts b/server/utils/nocodb-events.ts index 3936357..f2e36f1 100644 --- a/server/utils/nocodb-events.ts +++ b/server/utils/nocodb-events.ts @@ -30,25 +30,49 @@ export const getEventTableId = (tableName: 'Events' | 'EventRSVPs'): string => { // 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; + if (tableName === 'Events') { + // Check multiple possible keys for Events table + const eventsTableId = effectiveConfig.tables['events'] || + effectiveConfig.tables['Events'] || + effectiveConfig.tables['events_table']; + if (eventsTableId) { + console.log(`[nocodb-events] Using admin config table ID for ${tableName}:`, eventsTableId); + return eventsTableId; + } + } 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) { + console.log(`[nocodb-events] Using admin config table ID for ${tableName}:`, rsvpTableId); + return rsvpTableId; + } } } } catch (error) { - console.log(`[nocodb-events] Admin config not available, trying fallback for ${tableName}`); + console.log(`[nocodb-events] Admin config not available, trying fallback for ${tableName}:`, error); } // 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; + 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; + } } } @@ -60,7 +84,7 @@ export const getEventTableId = (tableName: 'Events' | 'EventRSVPs'): string => { } // Final fallback to default - const defaultTableId = tableName === 'Events' ? 'mt1mx3vkcw0vbmh' : 'rsvps-table-id'; + const defaultTableId = tableName === 'Events' ? 'mp3wigub1fzdo1b' : 'mt1mx3vkcw0vbmh'; console.log(`[nocodb-events] Using fallback table ID for ${tableName}:`, defaultTableId); return defaultTableId; }; @@ -276,6 +300,7 @@ export function createNocoDBEventsClient() { "title", "description", "event_type", "event_id", "start_datetime", "end_datetime", "location", "is_recurring", "recurrence_pattern", "max_attendees", "is_paid", "cost_members", "cost_non_members", "member_pricing_enabled", + "guests_permitted", "max_guests_permitted", "visibility", "status", "creator", "current_attendees" ]; diff --git a/utils/types.ts b/utils/types.ts index 0a60dc9..2898da8 100644 --- a/utils/types.ts +++ b/utils/types.ts @@ -455,6 +455,8 @@ export interface Event { cost_members?: string; cost_non_members?: string; member_pricing_enabled: string; // 'true' or 'false' as string + guests_permitted?: string; // 'true' or 'false' as string + max_guests_permitted?: string; // Maximum guests per person visibility: 'public' | 'board-only' | 'admin-only'; status: 'active' | 'cancelled' | 'completed' | 'draft'; creator: string; // member_id who created event @@ -476,6 +478,7 @@ export interface EventRSVP { payment_reference: string; // EVT-{member_id}-{date} attended: string; // 'true' or 'false' as string rsvp_notes?: string; + extra_guests?: string; // Number of additional guests as string created_time: string; // Updated to match database schema updated_time: string; // Updated to match database schema @@ -505,6 +508,8 @@ export interface EventCreateRequest { cost_members?: string; cost_non_members?: string; member_pricing_enabled: string; + guests_permitted?: string; + max_guests_permitted?: string; visibility: string; status?: string; } @@ -512,8 +517,9 @@ export interface EventCreateRequest { export interface EventRSVPRequest { event_id: string; member_id: string; - rsvp_status: 'confirmed' | 'declined' | 'pending'; + rsvp_status: 'confirmed' | 'declined' | 'pending' | 'waitlist'; rsvp_notes?: string; + extra_guests?: string; // Number of additional guests as string } export interface EventAttendanceRequest {
+ This event allows up to {{ maxGuestsAllowed }} additional guests per person. +