Repository: icssc/AntAlmanac (ts)

Issue:
RFC: Finals date
It would be useful to see finals' dates in the finals tab in the calendar (next to the day), at least in the desktop version. I haven't looked at whether that's part of the API/WebSOC, but I think it should be pretty easy if it is. 

Maybe we can count back from the end dates of the quarters. Note: pay attention to how Summer session works.

Displaying the date and day with React Big Calendar *should* be as simple as passing flags in.

Pull request description:
Display Finals Date From Default Term
## Summary

1. Added new optional field to TermData.ts "finalsStartDate"
2. Changed calendarizeFinals to correctly assign finals to their respective dates instead of the arbitrary (2018, 0, 1)
3. Refactored CalendarRoot to use date instead of defaultDate
4. Dynamically update date

Regular classes still populate to the arbitrary 2018 date, which is simpler since those are displayed dateless. All other data relies on the finalsStartDate field specified by defaultTerm.

## Test Plan

1. Make a schedule with classes that have different finals, toggle back and forth.
2. Try with only Saturday finals classes (Math 2b) or a mix of each (any class and Math 2b), etc.

## Issues

Closes #758 

## Future Followup
Make an alternate to hardcoding future dates (#778) including for finals, or at least hardcode the next few years.


Patch:
diff --git a/apps/antalmanac/src/components/Calendar/CalendarRoot.tsx b/apps/antalmanac/src/components/Calendar/CalendarRoot.tsx
index 1d636580..d9525ea4 100644
--- a/apps/antalmanac/src/components/Calendar/CalendarRoot.tsx
+++ b/apps/antalmanac/src/components/Calendar/CalendarRoot.tsx
@@ -8,6 +8,7 @@ import { Calendar, DateLocalizer, momentLocalizer, Views } from 'react-big-calen
 
 import CalendarToolbar from './CalendarToolbar';
 import CourseCalendarEvent, { CalendarEvent } from './CourseCalendarEvent';
+import { getDefaultFinalsStartDate } from '$lib/termData';
 import AppStore from '$stores/AppStore';
 import locationIds from '$lib/location_ids';
 import { useTimeFormatStore } from '$stores/SettingsStore';
@@ -53,8 +54,8 @@ const AntAlmanacEvent = ({ event }: { event: CalendarEvent }) => {
                     {event.showLocationInfo
                         ? event.locations.map((location) => `${location.building} ${location.room}`).join(', ')
                         : event.locations.length > 1
-                        ? `${event.locations.length} Locations`
-                        : `${event.locations[0].building} ${event.locations[0].room}`}
+                          ? `${event.locations.length} Locations`
+                          : `${event.locations[0].building} ${event.locations[0].room}`}
                 </Box>
                 <Box>{event.sectionCode}</Box>
             </Box>
@@ -85,8 +86,8 @@ export default function ScheduleCalendar(props: ScheduleCalendarProps) {
         return showFinalsSchedule
             ? finalsEventsInCalendar
             : hoveredCourseEvents
-            ? [...eventsInCalendar, ...hoveredCourseEvents]
-            : eventsInCalendar;
+              ? [...eventsInCalendar, ...hoveredCourseEvents]
+              : eventsInCalendar;
     };
 
     const handleClosePopover = () => {
@@ -156,10 +157,16 @@ export default function ScheduleCalendar(props: ScheduleCalendarProps) {
 
     const events = getEventsForCalendar();
     const hasWeekendCourse = events.some((event) => event.start.getDay() === 0 || event.start.getDay() === 6);
+
     const calendarStyling = isMobile ? { height: `calc(100% - 55px)` } : { height: `calc(100vh - 104px)` };
+
     const calendarTimeFormat = isMilitaryTime ? 'HH:mm' : 'h:mm A';
     const calendarGutterTimeFormat = isMilitaryTime ? 'HH:mm' : 'h A';
 
+    const defaultFinals = getDefaultFinalsStartDate();
+    const finalsDateFormat = defaultFinals ? 'ddd MM/DD' : 'ddd';
+    const date = showFinalsSchedule && defaultFinals ? defaultFinals : new Date(2018, 0, 1);
+
     // If a final is on a Saturday or Sunday, let the calendar start on Saturday
     moment.updateLocale('es-us', {
         week: {
@@ -246,7 +253,7 @@ export default function ScheduleCalendar(props: ScheduleCalendarProps) {
                             date.getMinutes() > 0 || !localizer
                                 ? ''
                                 : localizer.format(date, calendarGutterTimeFormat, culture),
-                        dayFormat: 'ddd',
+                        dayFormat: `${showFinalsSchedule ? finalsDateFormat : 'ddd'}`,
                         eventTimeRangeFormat: (
                             range: { start: Date; end: Date },
                             culture?: string,
@@ -266,7 +273,7 @@ export default function ScheduleCalendar(props: ScheduleCalendarProps) {
                     }}
                     step={15}
                     timeslots={2}
-                    defaultDate={new Date(2018, 0, 1)}
+                    date={date}
                     min={getStartTime()}
                     max={new Date(2018, 0, 1, 23)}
                     events={events}
diff --git a/apps/antalmanac/src/lib/termData.ts b/apps/antalmanac/src/lib/termData.ts
index dfd65778..008f45ae 100644
--- a/apps/antalmanac/src/lib/termData.ts
+++ b/apps/antalmanac/src/lib/termData.ts
@@ -5,20 +5,29 @@ class Term {
     shortName: `${string} ${string}`;
     longName: string;
     startDate?: [number, number, number];
-    constructor(shortName: `${string} ${string}`, longName: string, startDate?: [number, number, number]) {
+    finalsStartDate?: [number, number, number];
+    constructor(
+        shortName: `${string} ${string}`,
+        longName: string,
+        startDate?: [number, number, number],
+        finalsStartDate?: [number, number, number]
+    ) {
         this.shortName = shortName;
         this.longName = longName;
         this.startDate = startDate;
+        this.finalsStartDate = finalsStartDate;
     }
 }
 
 /**
  * Quarterly Academic Calendar {@link https://www.reg.uci.edu/calendars/quarterly/2023-2024/quarterly23-24.html}
+ * Quick Reference Ten Year Calendar {@link https://www.reg.uci.edu/calendars/academic/tenyr-19-29.html}
  * The `startDate`, if available, should correspond to the __instruction start date__ (not the quarter start date)
+ * The `finalsStartDate`, if available, should correspond to the __final exams__ first date (should be a Saturday)
  * Months are 0-indexed
  */
 const termData = [
-    new Term('2024 Winter', '2024 Winter Quarter', [2024, 0, 8]),
+    new Term('2024 Winter', '2024 Winter Quarter', [2024, 0, 8], [2024, 2, 16]),
     new Term('2023 Fall', '2023 Fall Quarter', [2023, 8, 28]),
     new Term('2023 Summer2', '2023 Summer Session 2', [2023, 7, 7]),
     new Term('2023 Summer10wk', '2023 10-wk Summer', [2023, 5, 26]),
@@ -76,9 +85,23 @@ const termData = [
     new Term('2014 Fall', '2014 Fall Quarter'),
 ];
 
-//returns the default term
+// Returns the default term
 function getDefaultTerm() {
     return termData[defaultTerm];
 }
 
-export { defaultTerm, getDefaultTerm, termData };
+// Returns the default finals start as array
+function getDefaultFinalsStart() {
+    return termData[defaultTerm].finalsStartDate;
+}
+
+/**
+ * Returns the default finals start as Date object
+ * Days offset by 1 to accomodate toggling with Saturday finals
+ */
+function getDefaultFinalsStartDate() {
+    const [year, month, day] = termData[defaultTerm].finalsStartDate || [];
+    return year && month && day ? new Date(year, month, day + 1) : undefined;
+}
+
+export { defaultTerm, getDefaultTerm, termData, getDefaultFinalsStart, getDefaultFinalsStartDate };
diff --git a/apps/antalmanac/src/stores/calendarizeHelpers.ts b/apps/antalmanac/src/stores/calendarizeHelpers.ts
index 68cce826..fe09f3d3 100644
--- a/apps/antalmanac/src/stores/calendarizeHelpers.ts
+++ b/apps/antalmanac/src/stores/calendarizeHelpers.ts
@@ -3,6 +3,7 @@ import { HourMinute } from 'peterportal-api-next-types';
 import { RepeatingCustomEvent } from '@packages/antalmanac-types';
 import { CourseEvent, CustomEvent, Location } from '$components/Calendar/CourseCalendarEvent';
 import { notNull, getReferencesOccurring } from '$lib/utils';
+import { getDefaultFinalsStart } from '$lib/termData';
 
 export const COURSE_WEEK_DAYS = ['Su', 'M', 'Tu', 'W', 'Th', 'F', 'Sa'];
 
@@ -104,6 +105,14 @@ export function calendarizeFinals(currentCourses: ScheduleCourse[] = []): Course
 
             const locationsWithNoDays = bldg ? bldg.map(getLocation) : course.section.meetings[0].bldg.map(getLocation);
 
+            /**
+             * Fallback to January 2018 if no finals start date is available.
+             * defaultFinalsDay is handled later by day since it varies by day.
+             */
+            const [defaultFinalsYear, defaultFinalsMonth, defaultFinalsDay] = [
+                ...(getDefaultFinalsStart() ?? [2018, 0]),
+            ];
+
             return dayIndicesOcurring.map((dayIndex) => ({
                 color: course.section.color,
                 term: course.term,
@@ -119,8 +128,20 @@ export function calendarizeFinals(currentCourses: ScheduleCourse[] = []): Course
                 instructors: course.section.instructors,
                 sectionCode: course.section.sectionCode,
                 sectionType: 'Fin',
-                start: new Date(2018, 0, dayIndex - 1, startHour, startMin),
-                end: new Date(2018, 0, dayIndex - 1, endHour, endMin),
+                start: new Date(
+                    defaultFinalsYear,
+                    defaultFinalsMonth,
+                    defaultFinalsDay ? defaultFinalsDay + dayIndex : dayIndex - 1,
+                    startHour,
+                    startMin
+                ),
+                end: new Date(
+                    defaultFinalsYear,
+                    defaultFinalsMonth,
+                    defaultFinalsDay ? defaultFinalsDay + dayIndex : dayIndex - 1,
+                    endHour,
+                    endMin
+                ),
                 finalExam: {
                     ...finalExam,
                     locations: bldg?.map(getLocation) ?? [],


Test patch:
diff --git a/apps/antalmanac/tests/calendarize-helpers.test.ts b/apps/antalmanac/tests/calendarize-helpers.test.ts
index 622794d4..883eccee 100644
--- a/apps/antalmanac/tests/calendarize-helpers.test.ts
+++ b/apps/antalmanac/tests/calendarize-helpers.test.ts
@@ -166,8 +166,8 @@ describe('calendarize-helpers', () => {
             instructors: [],
             sectionCode: 'placeholderSectionCode',
             sectionType: 'Fin',
-            start: new Date(2018, 0, 0, 1, 2),
-            end: new Date(2018, 0, 0, 3, 4),
+            start: new Date(2024, 2, 17, 1, 2), // Predicated on default term being Winter 2024
+            end: new Date(2024, 2, 17, 3, 4), // ...
             finalExam: {
                 examStatus: 'SCHEDULED_FINAL',
                 dayOfWeek: 'Sun',