2025-08-12 04:25:35 +02:00
|
|
|
// 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;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
2025-08-13 15:14:43 +02:00
|
|
|
* RSVP to an event with support for guests and real-time updates
|
2025-08-12 04:25:35 +02:00
|
|
|
*/
|
2025-08-13 15:14:43 +02:00
|
|
|
const rsvpToEvent = async (eventId: string, rsvpData: Omit<EventRSVPRequest, 'event_id'> & { extra_guests?: string }) => {
|
2025-08-12 04:25:35 +02:00
|
|
|
loading.value = true;
|
|
|
|
|
error.value = null;
|
|
|
|
|
|
|
|
|
|
try {
|
2025-08-13 15:14:43 +02:00
|
|
|
console.log('[useEvents] RSVP to event:', eventId, 'with data:', rsvpData);
|
|
|
|
|
|
2025-08-12 17:23:42 +02:00
|
|
|
const response = await $fetch<{ success: boolean; data: any; message: string }>(`/api/events/${eventId}/rsvp`, {
|
2025-08-12 04:25:35 +02:00
|
|
|
method: 'POST',
|
|
|
|
|
body: {
|
|
|
|
|
...rsvpData,
|
|
|
|
|
event_id: eventId,
|
|
|
|
|
member_id: user.value?.id || ''
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (response.success) {
|
2025-08-13 15:14:43 +02:00
|
|
|
// 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);
|
|
|
|
|
}
|
2025-08-13 14:02:29 +02:00
|
|
|
|
2025-08-13 15:14:43 +02:00
|
|
|
console.log('[useEvents] Event found at index:', eventIndex, 'using event_id:', eventId);
|
2025-08-13 14:02:29 +02:00
|
|
|
|
2025-08-12 04:25:35 +02:00
|
|
|
if (eventIndex !== -1) {
|
2025-08-13 15:14:43 +02:00
|
|
|
const event = events.value[eventIndex];
|
|
|
|
|
|
|
|
|
|
// Update RSVP status
|
|
|
|
|
event.user_rsvp = response.data;
|
2025-08-12 04:25:35 +02:00
|
|
|
|
2025-08-13 15:14:43 +02:00
|
|
|
// Calculate attendee count including guests
|
2025-08-12 04:25:35 +02:00
|
|
|
if (rsvpData.rsvp_status === 'confirmed') {
|
2025-08-13 15:14:43 +02:00
|
|
|
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
|
|
|
|
|
});
|
2025-08-12 04:25:35 +02:00
|
|
|
}
|
2025-08-13 15:14:43 +02:00
|
|
|
|
|
|
|
|
// Trigger reactivity
|
|
|
|
|
events.value[eventIndex] = { ...event };
|
2025-08-12 04:25:35 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-13 15:14:43 +02:00
|
|
|
// Clear cache for fresh data on next load
|
2025-08-12 04:25:35 +02:00
|
|
|
cache.clear();
|
|
|
|
|
|
2025-08-13 15:14:43 +02:00
|
|
|
// Force refresh events data to ensure accuracy
|
|
|
|
|
await fetchEvents({ force: true });
|
|
|
|
|
|
2025-08-12 04:25:35 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-13 15:14:43 +02:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-12 04:25:35 +02:00
|
|
|
/**
|
|
|
|
|
* Update attendance for an event (board/admin only)
|
|
|
|
|
*/
|
|
|
|
|
const updateAttendance = async (eventId: string, memberId: string, attended: boolean) => {
|
|
|
|
|
loading.value = true;
|
|
|
|
|
error.value = null;
|
|
|
|
|
|
|
|
|
|
try {
|
2025-08-13 14:30:26 +02:00
|
|
|
const response = await $fetch<{ success: boolean; data?: any; message: string }>(`/api/events/${eventId}/attendees`, {
|
2025-08-12 04:25:35 +02:00
|
|
|
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';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-13 14:30:26 +02:00
|
|
|
// Return data if available, otherwise return success status
|
|
|
|
|
return response.data || { success: true, message: response.message };
|
2025-08-12 04:25:35 +02:00
|
|
|
} 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();
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-13 22:23:06 +02:00
|
|
|
/**
|
|
|
|
|
* 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;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-12 04:25:35 +02:00
|
|
|
/**
|
|
|
|
|
* 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
|
2025-08-13 13:18:07 +02:00
|
|
|
events,
|
|
|
|
|
loading,
|
|
|
|
|
error,
|
|
|
|
|
upcomingEvent,
|
2025-08-12 04:25:35 +02:00
|
|
|
|
|
|
|
|
// Methods
|
|
|
|
|
fetchEvents,
|
|
|
|
|
createEvent,
|
2025-08-13 22:23:06 +02:00
|
|
|
deleteEvent,
|
2025-08-12 04:25:35 +02:00
|
|
|
rsvpToEvent,
|
2025-08-13 22:23:06 +02:00
|
|
|
cancelRSVP,
|
2025-08-12 04:25:35 +02:00
|
|
|
updateAttendance,
|
|
|
|
|
getCalendarEvents,
|
|
|
|
|
getUpcomingEvents,
|
|
|
|
|
findEventById,
|
|
|
|
|
hasUserRSVP,
|
|
|
|
|
getUserRSVPStatus,
|
|
|
|
|
clearCache,
|
|
|
|
|
refreshEvents
|
|
|
|
|
};
|
|
|
|
|
};
|