diff --git a/components/CreateEventDialog.vue b/components/CreateEventDialog.vue
index 5b04ba9..b346a7d 100644
--- a/components/CreateEventDialog.vue
+++ b/components/CreateEventDialog.vue
@@ -81,23 +81,58 @@
+
+
+
+
+
+
+
+
@@ -241,6 +276,18 @@
+
+
+
+ {{ errorMessage }}
+
+
+
('');
+const startTime = ref('');
+const endDate = ref('');
+const endTime = ref('');
+
+// Legacy date model refs for backward compatibility
const startDateModel = ref(null);
const endDateModel = ref(null);
@@ -427,30 +480,45 @@ watch(recurrenceFrequency, (newValue) => {
}
});
+// Watch for separate date and time changes to combine them
+watch([startDate, startTime], ([newDate, newTime]) => {
+ if (newDate && newTime) {
+ const combinedDateTime = new Date(`${newDate}T${newTime}`);
+ eventData.start_datetime = combinedDateTime.toISOString();
+ console.log('[CreateEventDialog] Combined start datetime:', eventData.start_datetime);
+ }
+});
+
+watch([endDate, endTime], ([newDate, newTime]) => {
+ if (newDate && newTime) {
+ const combinedDateTime = new Date(`${newDate}T${newTime}`);
+ eventData.end_datetime = combinedDateTime.toISOString();
+ console.log('[CreateEventDialog] Combined end datetime:', eventData.end_datetime);
+ }
+});
+
// Watch for prefilled dates
watch(() => props.prefilledDate, (newDate) => {
if (newDate) {
- const startDate = new Date(newDate);
- startDateModel.value = startDate;
- eventData.start_datetime = startDate.toISOString();
-
+ const prefillDate = new Date(newDate);
+ startDate.value = prefillDate.toISOString().split('T')[0];
+ startTime.value = prefillDate.toTimeString().substring(0, 5);
// Set end date 2 hours later if not provided
if (!props.prefilledEndDate) {
- const endDate = new Date(startDate);
- endDate.setHours(endDate.getHours() + 2);
- endDateModel.value = endDate;
- eventData.end_datetime = endDate.toISOString();
+ const endDateTime = new Date(prefillDate);
+ endDateTime.setHours(endDateTime.getHours() + 2);
+ endDate.value = endDateTime.toISOString().split('T')[0];
+ endTime.value = endDateTime.toTimeString().substring(0, 5);
}
}
}, { immediate: true });
watch(() => props.prefilledEndDate, (newEndDate) => {
if (newEndDate) {
- const endDate = new Date(newEndDate);
- endDateModel.value = endDate;
- eventData.end_datetime = endDate.toISOString();
-
+ const prefillEndDate = new Date(newEndDate);
+ endDate.value = prefillEndDate.toISOString().split('T')[0];
+ endTime.value = prefillEndDate.toTimeString().substring(0, 5);
}
}, { immediate: true });
@@ -472,6 +540,22 @@ const onDatePickerClosed = () => {
// This handler ensures the date picker behaves correctly on mobile and desktop
};
+// Validation functions
+const validateEndTime = () => {
+ if (!startDate.value || !endDate.value || !startTime.value || !endTime.value) {
+ return false; // Return false (no error) if not all fields are filled
+ }
+
+ // Only validate if start and end are on the same date
+ if (startDate.value === endDate.value) {
+ const start = startTime.value;
+ const end = endTime.value;
+ return start >= end; // Return true if there's an error (start >= end)
+ }
+
+ return false; // No error if different dates
+};
+
// Methods
const resetForm = () => {
eventData.title = '';
@@ -512,31 +596,81 @@ const close = () => {
resetForm();
};
+// Error handling
+const errorMessage = ref(null);
+
const handleSubmit = async () => {
if (!form.value) return;
const isValid = await form.value.validate();
if (!isValid.valid) return;
+ // Clear previous errors
+ errorMessage.value = null;
+
+ // Validate that we have proper date/time combination
+ if (!startDate.value || !startTime.value) {
+ errorMessage.value = 'Start date and time are required';
+ return;
+ }
+
+ if (!endDate.value || !endTime.value) {
+ errorMessage.value = 'End date and time are required';
+ return;
+ }
+
loading.value = true;
try {
- // Ensure datetime strings are properly formatted
- const startDate = new Date(eventData.start_datetime);
- const endDate = new Date(eventData.end_datetime);
+ // Combine date and time properly
+ const startDateTime = new Date(`${startDate.value}T${startTime.value}`);
+ const endDateTime = new Date(`${endDate.value}T${endTime.value}`);
+
+ // Validate start is not in the past
+ if (startDateTime < new Date()) {
+ errorMessage.value = 'Event start time cannot be in the past';
+ loading.value = false;
+ return;
+ }
+
+ // Validate end is after start
+ if (endDateTime <= startDateTime) {
+ errorMessage.value = 'Event end time must be after start time';
+ loading.value = false;
+ return;
+ }
const formattedEventData = {
...eventData,
- start_datetime: startDate.toISOString(),
- end_datetime: endDate.toISOString()
+ start_datetime: startDateTime.toISOString(),
+ end_datetime: endDateTime.toISOString()
};
+ console.log('[CreateEventDialog] Creating event with data:', formattedEventData);
+
const newEvent = await createEvent(formattedEventData);
emit('event-created', newEvent);
close();
} catch (error: any) {
console.error('Error creating event:', error);
+
+ // Parse error message for better UX
+ let userErrorMessage = 'Failed to create event';
+
+ if (error?.data?.message) {
+ userErrorMessage = error.data.message;
+ } else if (error?.message) {
+ if (error.message.includes('past')) {
+ userErrorMessage = 'Event date cannot be in the past';
+ } else if (error.message.includes('validation')) {
+ userErrorMessage = 'Please check all required fields';
+ } else {
+ userErrorMessage = error.message;
+ }
+ }
+
+ errorMessage.value = userErrorMessage;
} finally {
loading.value = false;
}
diff --git a/composables/useEvents.ts b/composables/useEvents.ts
index d50c06f..5df6772 100644
--- a/composables/useEvents.ts
+++ b/composables/useEvents.ts
@@ -352,6 +352,47 @@ export const useEvents = () => {
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
*/
@@ -385,7 +426,9 @@ export const useEvents = () => {
// Methods
fetchEvents,
createEvent,
+ deleteEvent,
rsvpToEvent,
+ cancelRSVP,
updateAttendance,
getCalendarEvents,
getUpcomingEvents,
diff --git a/server/api/events/[id].delete.ts b/server/api/events/[id].delete.ts
new file mode 100644
index 0000000..8ccdc41
--- /dev/null
+++ b/server/api/events/[id].delete.ts
@@ -0,0 +1,135 @@
+// server/api/events/[id].delete.ts
+import { createNocoDBEventsClient } from '~/server/utils/nocodb-events';
+import { createSessionManager } from '~/server/utils/session';
+
+export default defineEventHandler(async (event) => {
+ console.log('[api/events/[id].delete] =========================');
+ console.log('[api/events/[id].delete] DELETE /api/events/[id] - Delete event');
+ console.log('[api/events/[id].delete] Request from:', getClientIP(event));
+
+ try {
+ const eventId = getRouterParam(event, 'id');
+
+ if (!eventId) {
+ throw createError({
+ statusCode: 400,
+ statusMessage: 'Event ID is required'
+ });
+ }
+
+ console.log('[api/events/[id].delete] Deleting event:', eventId);
+
+ // Get user session using the working session manager
+ const sessionManager = createSessionManager();
+ const cookieHeader = getHeader(event, 'cookie');
+ const session = sessionManager.getSession(cookieHeader);
+
+ if (!session || !session.user) {
+ throw createError({
+ statusCode: 401,
+ statusMessage: 'Authentication required'
+ });
+ }
+
+ // Check if user has permission to delete events (board or admin only)
+ const userTier = session.user.tier;
+ if (userTier !== 'board' && userTier !== 'admin') {
+ throw createError({
+ statusCode: 403,
+ statusMessage: 'Only board members and admins can delete events'
+ });
+ }
+
+ console.log('[api/events/[id].delete] ✅ User authorized to delete events:', session.user.email, 'tier:', userTier);
+
+ const eventsClient = createNocoDBEventsClient();
+
+ // First, get the event to verify it exists and get the event_id for RSVP cleanup
+ let eventToDelete;
+ try {
+ eventToDelete = await eventsClient.findOne(eventId);
+ console.log('[api/events/[id].delete] Found event to delete:', eventToDelete.title, 'event_id:', eventToDelete.event_id);
+ } catch (error: any) {
+ if (error.statusCode === 404) {
+ throw createError({
+ statusCode: 404,
+ statusMessage: 'Event not found'
+ });
+ }
+ throw error;
+ }
+
+ // Get all RSVPs for this event to clean them up
+ const eventIdentifier = eventToDelete.event_id || eventToDelete.id || (eventToDelete as any).Id;
+ console.log('[api/events/[id].delete] Getting RSVPs for event identifier:', eventIdentifier);
+
+ const eventRSVPs = await eventsClient.getEventRSVPs(eventIdentifier.toString());
+ console.log('[api/events/[id].delete] Found', eventRSVPs.length, 'RSVPs to delete');
+
+ // Delete all RSVPs first
+ let deletedRSVPs = 0;
+ for (const rsvp of eventRSVPs) {
+ try {
+ // Delete RSVP using its database Id
+ const rsvpId = rsvp.Id || rsvp.id;
+ if (rsvpId) {
+ console.log('[api/events/[id].delete] Deleting RSVP:', rsvpId);
+ await $fetch(eventsClient.constructor.prototype.createEventTableUrl('EventRSVPs'), {
+ method: 'DELETE',
+ headers: {
+ 'xc-token': eventsClient.constructor.prototype.getNocoDbConfiguration().token,
+ },
+ body: {
+ Id: parseInt(rsvpId.toString())
+ }
+ });
+ deletedRSVPs++;
+ }
+ } catch (rsvpError: any) {
+ console.log('[api/events/[id].delete] ⚠️ Error deleting RSVP:', rsvp.Id || rsvp.id, rsvpError);
+ // Continue with other RSVPs even if one fails
+ }
+ }
+
+ console.log('[api/events/[id].delete] Deleted', deletedRSVPs, 'RSVPs');
+
+ // Now delete the event itself using its database Id
+ const eventDatabaseId = (eventToDelete as any).Id || eventToDelete.id;
+ if (!eventDatabaseId) {
+ throw createError({
+ statusCode: 500,
+ statusMessage: 'Could not determine event database ID for deletion'
+ });
+ }
+
+ console.log('[api/events/[id].delete] Deleting event with database ID:', eventDatabaseId);
+ await eventsClient.delete(eventDatabaseId.toString());
+
+ console.log('[api/events/[id].delete] ✅ Successfully deleted event and all associated RSVPs');
+
+ return {
+ success: true,
+ message: `Event "${eventToDelete.title}" and ${deletedRSVPs} associated RSVPs deleted successfully`,
+ deleted: {
+ event: {
+ id: eventDatabaseId,
+ event_id: eventToDelete.event_id,
+ title: eventToDelete.title
+ },
+ rsvps_deleted: deletedRSVPs
+ }
+ };
+
+ } catch (error: any) {
+ console.error('[api/events/[id].delete] ❌ Error deleting event:', error);
+
+ if (error.statusCode) {
+ throw error;
+ }
+
+ throw createError({
+ statusCode: 500,
+ statusMessage: 'Failed to delete event'
+ });
+ }
+});
diff --git a/server/utils/nocodb-events.ts b/server/utils/nocodb-events.ts
index ac5b4d2..37c5c61 100644
--- a/server/utils/nocodb-events.ts
+++ b/server/utils/nocodb-events.ts
@@ -274,22 +274,52 @@ export function createNocoDBEventsClient() {
},
/**
- * Find a single event by ID
+ * Find a single event by ID (supports both database Id and business event_id)
*/
async findOne(id: string) {
console.log('[nocodb-events] Fetching event ID:', id);
try {
- const result = await $fetch(`${createEventTableUrl(EventTable.Events)}/${id}`, {
+ // First, try to fetch by database Id (numeric)
+ if (/^\d+$/.test(id)) {
+ console.log('[nocodb-events] Using database Id lookup for:', id);
+ const result = await $fetch(`${createEventTableUrl(EventTable.Events)}/${id}`, {
+ headers: {
+ "xc-token": getNocoDbConfiguration().token,
+ },
+ });
+
+ console.log('[nocodb-events] Successfully retrieved event by database Id:', result.id || (result as any).Id);
+ return normalizeEventFieldsFromNocoDB(result);
+ }
+
+ // Otherwise, search by business event_id
+ console.log('[nocodb-events] Using event_id lookup for:', id);
+ const results = await $fetch<{list: Event[]}>(`${createEventTableUrl(EventTable.Events)}`, {
headers: {
"xc-token": getNocoDbConfiguration().token,
},
+ params: {
+ where: `(event_id,eq,${id})`,
+ limit: 1
+ }
});
- console.log('[nocodb-events] Successfully retrieved event:', result.id || (result as any).Id);
- return normalizeEventFieldsFromNocoDB(result);
+ if (results.list && results.list.length > 0) {
+ console.log('[nocodb-events] Successfully found event by event_id:', results.list[0].id || (results.list[0] as any).Id);
+ return normalizeEventFieldsFromNocoDB(results.list[0]);
+ }
+
+ console.log('[nocodb-events] No event found with event_id:', id);
+ throw createError({
+ statusCode: 404,
+ statusMessage: 'Event not found'
+ });
} catch (error: any) {
console.error('[nocodb-events] Error fetching event:', error);
+ if (error.statusCode === 404) {
+ throw error; // Re-throw 404 errors as-is
+ }
handleNocoDbError(error, 'getEventById', 'Event');
throw error;
}