// composables/useEvents.ts import type { Event, EventsResponse, EventFilters, EventCreateRequest, EventRSVPRequest } from '~/utils/types'; export const useEvents = () => { const events = ref([]); const loading = ref(false); const error = ref(null); const upcomingEvent = ref(null); const cache = reactive>(new Map()); const CACHE_TIMEOUT = 5 * 60 * 1000; // 5 minutes // Get authenticated user info const { user, userTier } = useAuth(); /** * Fetch events with optional filtering and caching */ const fetchEvents = async (filters?: EventFilters & { force?: boolean }) => { loading.value = true; error.value = null; try { // Create cache key const cacheKey = JSON.stringify(filters || {}); const cached = cache.get(cacheKey); // Check cache if not forcing refresh if (!filters?.force && cached) { const now = Date.now(); if (now - cached.timestamp < CACHE_TIMEOUT) { events.value = cached.data; loading.value = false; return cached.data; } } // Default date range (current month + 2 months ahead) const defaultFilters: EventFilters = { start_date: startOfMonth(new Date()).toISOString(), end_date: endOfMonth(addMonths(new Date(), 2)).toISOString(), user_role: userTier.value, ...filters }; const response = await $fetch('/api/events', { query: { ...defaultFilters, calendar_format: 'false' } }); if (response.success) { events.value = response.data; // Cache the results cache.set(cacheKey, { data: response.data, timestamp: Date.now() }); // Update upcoming event updateUpcomingEvent(response.data); return response.data; } else { throw new Error(response.message || 'Failed to fetch events'); } } catch (err: any) { error.value = err.message || 'Failed to load events'; console.error('Error fetching events:', err); throw err; } finally { loading.value = false; } }; /** * Create a new event (board/admin only) */ const createEvent = async (eventData: EventCreateRequest) => { loading.value = true; error.value = null; try { const response = await $fetch<{ success: boolean; data: Event; message: string }>('/api/events', { method: 'POST', body: eventData }); if (response.success) { // Clear cache and refresh events cache.clear(); await fetchEvents({ force: true }); return response.data; } else { throw new Error(response.message || 'Failed to create event'); } } catch (err: any) { error.value = err.message || 'Failed to create event'; console.error('Error creating event:', err); throw err; } finally { loading.value = false; } }; /** * RSVP to an event with support for guests and real-time updates */ 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: { ...rsvpData, event_id: eventId, member_id: user.value?.id || '' } }); if (response.success) { // 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] Event found at index:', eventIndex, 'using event_id:', eventId); if (eventIndex !== -1) { const event = events.value[eventIndex]; // Update RSVP status event.user_rsvp = response.data; // Calculate attendee count including guests if (rsvpData.rsvp_status === 'confirmed') { 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 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'); } } catch (err: any) { error.value = err.message || 'Failed to RSVP to event'; console.error('Error RSVPing to event:', err); throw err; } finally { loading.value = false; } }; /** * 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) */ const updateAttendance = async (eventId: string, memberId: string, attended: boolean) => { loading.value = true; error.value = null; try { const response = await $fetch<{ success: boolean; data?: any; message: string }>(`/api/events/${eventId}/attendees`, { method: 'PATCH', body: { event_id: eventId, member_id: memberId, attended } }); if (response.success) { // Update local event data const eventIndex = events.value.findIndex(e => e.id === eventId); if (eventIndex !== -1 && events.value[eventIndex].attendee_list) { const attendeeIndex = events.value[eventIndex].attendee_list!.findIndex( a => a.member_id === memberId ); if (attendeeIndex !== -1) { events.value[eventIndex].attendee_list![attendeeIndex].attended = attended ? 'true' : 'false'; } } // Return data if available, otherwise return success status return response.data || { success: true, message: response.message }; } else { throw new Error(response.message || 'Failed to update attendance'); } } catch (err: any) { error.value = err.message || 'Failed to update attendance'; console.error('Error updating attendance:', err); throw err; } finally { loading.value = false; } }; /** * Get events for calendar display */ const getCalendarEvents = async (start: string, end: string) => { try { const response = await $fetch('/api/events', { query: { start_date: start, end_date: end, user_role: userTier.value, calendar_format: 'true' } }); if (response.success) { return response.data; } return []; } catch (err) { console.error('Error fetching calendar events:', err); return []; } }; /** * Get upcoming events for banners/widgets */ const getUpcomingEvents = (limit = 5): Event[] => { const now = new Date(); return events.value .filter(event => new Date(event.start_datetime) >= now) .sort((a, b) => new Date(a.start_datetime).getTime() - new Date(b.start_datetime).getTime()) .slice(0, limit); }; /** * Find event by ID */ const findEventById = (eventId: string): Event | undefined => { return events.value.find(event => event.id === eventId); }; /** * Check if user has RSVP'd to an event */ const hasUserRSVP = (eventId: string): boolean => { const event = findEventById(eventId); return !!event?.user_rsvp; }; /** * Get user's RSVP status for an event */ const getUserRSVPStatus = (eventId: string): string | null => { const event = findEventById(eventId); return event?.user_rsvp?.rsvp_status || null; }; /** * Update the upcoming event reference */ const updateUpcomingEvent = (eventList: Event[]) => { const upcoming = eventList .filter(event => new Date(event.start_datetime) >= new Date()) .sort((a, b) => new Date(a.start_datetime).getTime() - new Date(b.start_datetime).getTime()); upcomingEvent.value = upcoming.length > 0 ? upcoming[0] : null; }; /** * Clear cache manually */ const clearCache = () => { cache.clear(); }; /** * Delete an event (board/admin only) */ const deleteEvent = async (eventId: string) => { loading.value = true; error.value = null; try { const response = await $fetch<{ success: boolean; message: string; deleted: any }>(`/api/events/${eventId}`, { method: 'DELETE' }); if (response.success) { // Remove event from local state const eventIndex = events.value.findIndex(e => e.event_id === eventId || e.id === eventId || (e as any).Id === eventId ); if (eventIndex !== -1) { events.value.splice(eventIndex, 1); } // Clear cache and refresh clearCache(); await fetchEvents({ force: true }); return response; } else { throw new Error(response.message || 'Failed to delete event'); } } catch (err: any) { error.value = err.message || 'Failed to delete event'; console.error('Error deleting event:', err); throw err; } finally { loading.value = false; } }; /** * Refresh events data */ const refreshEvents = async () => { clearCache(); return await fetchEvents({ force: true }); }; // Utility functions for date handling function startOfMonth(date: Date): Date { return new Date(date.getFullYear(), date.getMonth(), 1); } function endOfMonth(date: Date): Date { return new Date(date.getFullYear(), date.getMonth() + 1, 0); } function addMonths(date: Date, months: number): Date { const result = new Date(date); result.setMonth(result.getMonth() + months); return result; } return { // Reactive state events, loading, error, upcomingEvent, // Methods fetchEvents, createEvent, deleteEvent, rsvpToEvent, cancelRSVP, updateAttendance, getCalendarEvents, getUpcomingEvents, findEventById, hasUserRSVP, getUserRSVPStatus, clearCache, refreshEvents }; };