Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 134 additions & 4 deletions backend/src/controllers/EventController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,22 @@ 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.
*/
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);
}

/**
Expand Down Expand Up @@ -180,4 +186,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;
}


}
32 changes: 18 additions & 14 deletions backend/src/utils/Scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default class Scheduler {

// when free block not found, find next day
if (!freeBlock) {
// move current time to start of next time block
// move current time to start of next day
const workTimeStart = UserPreferenceUtils.minuteNumberToHourAndMinute(userPreferences.startTime);
currentSearchedTime.setDate(currentSearchedTime.getDate() + 1);
currentSearchedTime.setHours(workTimeStart[0]);
Expand Down Expand Up @@ -216,7 +216,6 @@ export default class Scheduler {
* @param userPreferences The user preferences.
* @param startSearchTime The start time for searching the next free time block.
* @returns an object containing the time and date for the next free time block;
* Returns
*/
static nextFreeTimeBlock(
events: UserEvent[],
Expand All @@ -227,12 +226,14 @@ export default class Scheduler {
const dayEnd = userPreferences.endTime;
const minIncrement = 5;

// Start at startSearchTime and continue search until end of day.
// Increment time by minIncrement minutes if time is not free.
for (
let checkTime = new Date(startSearchTime);
UserPreferenceUtils.dateToMinuteNumber(checkTime) <= dayEnd;
checkTime = new Date(checkTime.getTime() + minIncrement * 60000)
) {
if (this.timeIsFree(events, userPreferences, startSearchTime)) {
if (this.timeIsFree(events, userPreferences, checkTime)) {
const availableMinutes = this.minutesToNextEvent(
events,
userPreferences,
Expand Down Expand Up @@ -266,7 +267,8 @@ export default class Scheduler {
if (!isWithinWorkHours) return false;

// checks whether any events overlap at the current time
const hasConflict = this.eventsAtTime(events, time).length > 0;
const conflictingEvents = this.eventsAtTime(events, time);
const hasConflict = conflictingEvents.length > 0;

// Current time is free when there's no event at this time
return !hasConflict;
Expand All @@ -282,7 +284,7 @@ export default class Scheduler {
return events.filter(
(event) =>
event &&
(event.start_time != undefined && event.end_time != undefined) &&
(event.start_time && event.end_time) &&
(event.start_time <= time && event.end_time > time)
);
}
Expand All @@ -299,28 +301,30 @@ export default class Scheduler {
// Filter events that start after given time and before end of work day
const currentMinutes = UserPreferenceUtils.dateToMinuteNumber(time);
const dayEnd = userPreferences.endTime;
const futureEvents = events
const minutesToDayEnd = dayEnd - currentMinutes;
const dayEndDate = new Date(time.getTime() + minutesToDayEnd * 60000);

const futureEventsOnThisDay = events
.filter(
(event) =>
event &&
event.start_time >= time &&
event.start_time < new Date(time.getDate() + 1) &&
UserPreferenceUtils.dateToMinuteNumber(event.start_time) <
userPreferences.endTime
event.start_time < dayEndDate
)
.sort(
(a, b) =>
UserPreferenceUtils.dateToMinuteNumber(a.start_time) -
UserPreferenceUtils.dateToMinuteNumber(b.start_time)
);

if (futureEvents.length > 0) {
return (
UserPreferenceUtils.dateToMinuteNumber(futureEvents[0].start_time) -
currentMinutes
if (futureEventsOnThisDay.length > 0) {
const returned = Math.max(
UserPreferenceUtils.dateToMinuteNumber(futureEventsOnThisDay[0].start_time) - currentMinutes - 1,
1
);
return returned;
}

return dayEnd - currentMinutes;
return minutesToDayEnd;
}
}
80 changes: 71 additions & 9 deletions frontend/public/help.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ Welcome to StressLess! Here's how you get started...
3. Click the sign in button above the sign up page to sign in
4. Enter your email and password to sign in

# What if I'm Already Signed In?
1. If you're already signed in, click get started
2. The button will bring you down the page where it says you are already signed in
3. Click on the green go to dashboard button underneath the you already signed in message to access your profile page


# How Do I Change Visibility Settings?

1. You can view the app in either dark or light mode
Expand Down Expand Up @@ -69,6 +75,16 @@ You can either edit your preferences in two ways


<!-- Section: dashboard/calendar -->
# Custom Scheduling
## How Do I Create My Custom Schedule?
1. Make sure all of your user preferences are filed out in the profile page (See *How Do I Edit My Preferences in the Survey* on the user help guide on the profile page)
2. Add deadlines (See *How Do I Add My Own Custom Deadline* on the user help guide on the calendar page)
3. Locate the purple generate schedule button in the bottom right corner
4. Press the purple generate schedule button
## What if I Don't Like the Schedule the Custom Schedule Created?
1. Locate the red remove generated events buttons in the bottom right corner underneath the purple generate schedule button
2. Press remove generated events to remove the events the schedule generated

# Calendar

## How Do I Change Calendar Views?
Expand All @@ -89,19 +105,65 @@ You can either edit your preferences in two ways
1. At the top of the calendar in the left hand corner next to the arrows click on the today button
2. When you click on the today button, the current day will glow yellow showing you what the current day is

## What is The Difference Between an Event and Deadline?
### Event
An **event** is an activity in the schedule that must happen at a specific time.

Some examples of events include
- Reoccurring class times
- Reoccurring work shifts
- Club meetings
- Sports practices
- A social event
- Scheduled meetings

### Deadline

A **deadline** has a due date of when it has to get done, but it has no set time the user has to work on it

Some example of deadlines include

- An assignment for class
- An essay to write
- A test to study for
- A presentation to work on
- A project

## How Do I Add a Preexisting Event?

1. You can add an event when you are on the calendar page.
2. You can drag any of the preexisting events from the right side into the desired day

## How Do I Add My Own Custom Event?
## How Do I Add My Own Custom Event or Deadline?

1. Don’t use the pre-existing events on the right
2. Instead, click on a day you want to add the event
3. Type in the label of your custom event
4. Click create to add your event
2. Instead, click on a day you want to add the event or deadline
3. When you click on the day, the add event or deadline window should open
4. At the top of the file, the user can select whether they want it to be an event or deadline (see the user FAQ *What is the Difference Between an Event and Deadline* to determine if it is classified as an event or deadline)
5. For both events and deadlines, enter in the title in the title box and the description in the description box

### Events
1. Enter in the location in the location box
2. Enter in the start date and time and end date in time below the location box

**For Reoccurring Events**

1. Enter the start date and time and end date and time again.
2. Reoccurring events have the options of daily, weekly, and monthly
3. Select daily if it reoccurs daily
4. Select weekly if it reoccurs weekly
5. Select monthly if it reoccurs monthly
6. Click on the create button to create the event
7. If you don’t want to create the event anymore, press cancel

### Deadlines
1. Enter in how long you estimate your deadline will take in the projected duration box (it is better to overestimate than underestimate how long it will take)
2. Under the projected duration box, enter what date and what time your deadline is due
3. Click on the create button to create the deadline
4. If you don’t want to create the deadline anymore, press cancel


## How Do I Change the Day of an Event?
## How Do I Change the Day of an Event or Deadline?

1. You can be in either weekly or monthly mode
2. Change the day by dragging the event to the new desired day
Expand Down Expand Up @@ -129,11 +191,11 @@ You can either edit your preferences in two ways
5. Increase the number of days for the event by dragging the arrow to the right
6. Decrease the number of days for the event by dragging the arrow to the left

## How Do I Delete an Event?
## How Do I Delete an Event or Deadline?

1. You can delete an event by clicking on the event on the calendar.
2. If you click on the event, you can press the read delete button to delete the event
3. If you don’t want to delete the event, you can press the cancel button
1. You can delete an event or deadline by clicking on the event on the calendar.
2. If you click on the event or deadline, you can press the read delete button to delete the event or deadline
3. If you don’t want to delete the event or deadline, you can press the cancel button

## How Do I Sign Out?
1. Click on the sidebar icon on the upper lefthand corner on either the user profile or calendar page
Expand Down
Loading