Add event management system with calendar and CRUD operations
Some checks failed
Build And Push Image / docker (push) Failing after 2m37s
Some checks failed
Build And Push Image / docker (push) Failing after 2m37s
- Add EventCalendar component with FullCalendar integration - Create event CRUD dialogs and upcoming event banner - Implement server-side events API and database utilities - Add events dashboard page and navigation - Improve dues calculation with better overdue day logic - Install FullCalendar and date-fns dependencies
This commit is contained in:
315
composables/useEvents.ts
Normal file
315
composables/useEvents.ts
Normal file
@@ -0,0 +1,315 @@
|
||||
// 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
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user