monacousa-portal/composables/useEvents.ts

323 lines
8.8 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
*/
const rsvpToEvent = async (eventId: string, rsvpData: Omit<EventRSVPRequest, 'event_id'>) => {
loading.value = true;
error.value = null;
try {
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) {
// 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
);
console.log('[useEvents] Looking for event with database ID:', eventId, 'found at index:', eventIndex);
if (eventIndex !== -1) {
events.value[eventIndex].user_rsvp = response.data;
// Update attendee count if confirmed
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();
}
}
// Clear cache
cache.clear();
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;
}
};
/**
* 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(`/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 response.data;
} 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();
};
/**
* 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,
rsvpToEvent,
updateAttendance,
getCalendarEvents,
getUpcomingEvents,
findEventById,
hasUserRSVP,
getUserRSVPStatus,
clearCache,
refreshEvents
};
};