From cda20f31fc095876dd3cacd346f982a9a713112b Mon Sep 17 00:00:00 2001 From: Tim Yu Date: Sun, 11 May 2025 21:02:40 -0500 Subject: [PATCH 1/3] Implemented recurring events --- backend/src/controllers/EventController.ts | 134 ++++++++++++++++++- frontend/src/app/dashboard/calendar/page.tsx | 30 ++++- 2 files changed, 156 insertions(+), 8 deletions(-) diff --git a/backend/src/controllers/EventController.ts b/backend/src/controllers/EventController.ts index 6634795d..4019c696 100644 --- a/backend/src/controllers/EventController.ts +++ b/backend/src/controllers/EventController.ts @@ -69,10 +69,14 @@ export default class EventController { */ public static async postEvent(req: Request, res: Response) { const values = EventController.getUserEventValues(req); - const result = await prisma.user_events.create({ - data: { ...values }, + const eventSeries = EventController.createEventSeriesFromEvent({ ...values }); + // const eventSeries = [{ ...values }]; + const result = await prisma.user_events.createManyAndReturn({ + data: eventSeries, + }); + res.json({ + eventSeries: result, }); - res.json(result); } /** @@ -180,4 +184,128 @@ export default class EventController { deadline_id, }; } + + /** + * Creates an event series from a single event in request if the event is recurring. + * + * Note that we create recurring events in the following way: + * - Create the first event, regardless of whether it is in the recurrence period. + * - Repeatedly check same time previous (day/week/month) to see if it falls + * in recurrence period + * - If time is not in recurrence period but after start of recurrence period, + * do not create an event. + * - If time is in recurrence period, create an event + * - If time if before start of recurrence period, stop searching + * - Repeatedly check same time next (day/week/month) to see if it falls + * in recurrence period + * - Similar logic, but we are moving towards the end of recurrence period + * and stopping search there. + * - Note that if any newly created events due to recurrence (but not the + * event given as the parameter) would fall partially within the recurrence + * period, we do not create this new event. For recurrences to be created, + * they must fall completely within the recurrence period. + * - Note that this also prevents mishandling of recurrence starting after + * its end. + * + * For example: If we want to create an weekly recurring event on May 5, 2025, + * at 00:00-01:00, with the recurrence period between May 12 at 00:30 and + * May 27 at 00:00, then the output would be three events on: + * - May 5, 2025, + * - May 19, 2025, + * - May 26, 2025. + * + * @param event The initial event that we want to create recurrences for. + * This event will always be returned. + * + * @returns An array with `event` and its recurring instances. + */ + private static createEventSeriesFromEvent(event: any) { + const returned = [event]; + + // If event is not recurring or invalid, return single event array + if ( + !event.is_recurring || + !event.recurrence_pattern || + !event.recurrence_start_date || + !event.recurrence_end_date + ) { + return returned; + } + + // Define some helper functions + + const getNextRecurrence = ( + current: Date | null, pattern: string, forward: boolean + ): Date | null => { + if (!current) return null; + + const nextDate = new Date(current.getTime()); + if (pattern === 'daily') { + nextDate.setDate(current.getDate() + (forward ? 1 : -1)); + } else if (pattern === 'weekly') { + nextDate.setDate(current.getDate() + (forward ? 7 : -7)); + } else if (pattern === 'monthly') { + nextDate.setMonth(current.getMonth() + (forward ? 1 : -1)); + } else { + return null; + } + + return nextDate; + }; + + const eventInRecurrencePeriod = ( + eventToCheck: any, recurrenceStart: Date, recurrenceEnd: Date + ): boolean => { + return ( + new Date(eventToCheck.start_time) >= recurrenceStart && ( + !eventToCheck.end_time || + new Date(eventToCheck.end_time) < recurrenceEnd + ) + ); + }; + + const addEventIfInRecurrencePeriod = ( + originalEvent: any, startTime: Date, eventDuration: number, returnedArray: any[] + ) => { + const newEvent = { + ...originalEvent, + start_time: startTime, + end_time: eventDuration > 0 ? new Date(startTime.getTime() + eventDuration) : undefined, + }; + if (eventInRecurrencePeriod( + newEvent, + new Date(event.recurrence_start_date), + new Date(event.recurrence_end_date) + )) { + returnedArray.push(newEvent); + } + }; + + // Calculate event duration + const eventDuration = event.end_time ? + new Date(event.end_time).getTime() - new Date(event.start_time).getTime() : + -1; + + // 1. Add days after event: + for ( + let d = getNextRecurrence(new Date(event.start_time), event.recurrence_pattern, true); + d && d < new Date(event.recurrence_end_date); + d = getNextRecurrence(d, event.recurrence_pattern, true) + ) { + addEventIfInRecurrencePeriod(event, d, eventDuration, returned); + } + + // 2. Add days before event: + for ( + let d = getNextRecurrence(new Date(event.start_time), event.recurrence_pattern, false); + d && d > new Date(event.recurrence_start_date); + d = getNextRecurrence(d, event.recurrence_pattern, false) + ) { + addEventIfInRecurrencePeriod(event, d, eventDuration, returned); + } + + return returned; + } + + } diff --git a/frontend/src/app/dashboard/calendar/page.tsx b/frontend/src/app/dashboard/calendar/page.tsx index b2f4880e..b25e715b 100644 --- a/frontend/src/app/dashboard/calendar/page.tsx +++ b/frontend/src/app/dashboard/calendar/page.tsx @@ -346,12 +346,32 @@ export default function Home() { axios .post(backendBaseUrl + `/api/calendar/events`, eventWithUser) .then((response) => { - console.log('Successfully saved event:', response.data); + // controller may save multiple events now. + console.log('Successfully saved events:', response); + console.log('event series', response.data.eventSeries); + const eventsToAdd: (typeof eventWithUser)[] = []; + + // const { id, start_time, end_time, ...restEvent } = eventWithUser; + + for (const newEvent of response.data.eventSeries) { + // Notice that the keys might not be in the correct order + eventsToAdd.push({ + ...eventWithUser, + id: newEvent.id, + start_time: new Date(newEvent.start_time).toISOString(), + end_time: newEvent.endTime, + start: new Date(newEvent.start_time), + end: newEvent.end_time ? new Date(newEvent.end_time) : undefined, + }); + } + + setAllEvents([...allEvents, ...eventsToAdd]); + // We want to be consistent and use our backend id - const correctId = response.data.id; - const { id, ...restEvent } = eventWithUser; - console.log(`Replace ${id} with ${correctId}`); - setAllEvents([...allEvents, { id: correctId, ...restEvent }]); + // const correctId = response.data.id; + // const { id, ...restEvent } = eventWithUser; + // console.log(`Replace ${id} with ${correctId}`); + // setAllEvents([...allEvents, { id: correctId, ...restEvent }]); }) .catch((error) => { console.error('Error saving event:', error); From c7596902e2d2c3e316dda8cf60772c6030e2c232 Mon Sep 17 00:00:00 2001 From: Tim Yu Date: Sun, 11 May 2025 21:12:17 -0500 Subject: [PATCH 2/3] Frontend dragging event now handles ID changes --- frontend/src/app/dashboard/calendar/page.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/dashboard/calendar/page.tsx b/frontend/src/app/dashboard/calendar/page.tsx index b25e715b..aef66f6b 100644 --- a/frontend/src/app/dashboard/calendar/page.tsx +++ b/frontend/src/app/dashboard/calendar/page.tsx @@ -239,7 +239,7 @@ export default function Home() { recurrence_start_date: newEvent.recurrence_start_date, color: colorTypes.eventByUser, }; - setAllEvents([...allEvents, event]); + // setAllEvents([...allEvents, event]); //added to test getting event name to the backend axios @@ -247,6 +247,13 @@ export default function Home() { .then((response) => { console.log('Successfully saved event into backend: ', user!.uid); console.log(response); + + // Noticed we also need to handle the ID change after adding a dragged + // event. + const correctId = response.data.eventSeries[0].id; + const { id, ...restEvent } = event; + console.log(`Replace ${id} with ${correctId}`); + setAllEvents([...allEvents, { id: correctId, ...restEvent }]); }) .catch((error) => { console.log(error); From 79b2b0aa58b8c8c4d73ac069d33bef46256035f2 Mon Sep 17 00:00:00 2001 From: Tim Yu Date: Sun, 11 May 2025 21:12:55 -0500 Subject: [PATCH 3/3] Modified documentation for post event controller --- backend/src/controllers/EventController.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/controllers/EventController.ts b/backend/src/controllers/EventController.ts index 4019c696..f47acd1d 100644 --- a/backend/src/controllers/EventController.ts +++ b/backend/src/controllers/EventController.ts @@ -63,7 +63,9 @@ export default class EventController { } /** - * Adds a new event to database using values in the request. + * Adds a new event and its recurrences to database using values in the request. + * Response will be an object containing an array. + * * @param req.body - Fields have the same names of the columns in the * `user_events` table. `id` will be ignored since an UUID will be generated. */