316 lines
8.5 KiB
TypeScript
316 lines
8.5 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(`/api/events/${eventId}/rsvp`, {
|
||
|
|
method: 'POST',
|
||
|
|
body: {
|
||
|
|
...rsvpData,
|
||
|
|
event_id: eventId,
|
||
|
|
member_id: user.value?.id || ''
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.success) {
|
||
|
|
// Update local event data
|
||
|
|
const eventIndex = events.value.findIndex(e => e.id === eventId);
|
||
|
|
if (eventIndex !== -1) {
|
||
|
|
events.value[eventIndex].user_rsvp = response.data;
|
||
|
|
|
||
|
|
// Update attendee count if confirmed
|
||
|
|
if (rsvpData.rsvp_status === 'confirmed') {
|
||
|
|
const currentCount = events.value[eventIndex].current_attendees || 0;
|
||
|
|
events.value[eventIndex].current_attendees = currentCount + 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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: readonly(events),
|
||
|
|
loading: readonly(loading),
|
||
|
|
error: readonly(error),
|
||
|
|
upcomingEvent: readonly(upcomingEvent),
|
||
|
|
|
||
|
|
// Methods
|
||
|
|
fetchEvents,
|
||
|
|
createEvent,
|
||
|
|
rsvpToEvent,
|
||
|
|
updateAttendance,
|
||
|
|
getCalendarEvents,
|
||
|
|
getUpcomingEvents,
|
||
|
|
findEventById,
|
||
|
|
hasUserRSVP,
|
||
|
|
getUserRSVPStatus,
|
||
|
|
clearCache,
|
||
|
|
refreshEvents
|
||
|
|
};
|
||
|
|
};
|