monacousa-portal/composables/useEvents.ts

442 lines
13 KiB
TypeScript

// composables/useEvents.ts
import type { Event, EventsResponse, EventFilters, EventCreateRequest, EventRSVPRequest } from '~/utils/types';
export const useEvents = () => {
const events = ref<Event[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);
const upcomingEvent = ref<Event | null>(null);
const cache = reactive<Map<string, { data: Event[]; timestamp: number }>>(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<EventsResponse>('/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<EventRSVPRequest, 'event_id'> & { 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<EventsResponse>('/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
};
};