diff --git a/client/src/api/times/times.ts b/client/src/api/times/times.ts index 678b6bd09..1e8247640 100644 --- a/client/src/api/times/times.ts +++ b/client/src/api/times/times.ts @@ -156,7 +156,7 @@ export const COURSES_CLASS_TIMES_QUERY: CoursesClassTimesQueryType = gql` classes(where: { course_id: { _in: $courseIds }, year: { _eq: $year }, term: { _eq: $term } }) { times { day - start_time + time } } } @@ -182,6 +182,61 @@ export const useCoursesClassTimesQuery = (courseIds: string[], year: number, ter return data.classes; }; +type CourseClassTimesDetailedQueryType = TypedDocumentNode< + { + classes: { + times: { + day: string; + time: string; + location: string; + }[]; + section: string; + class_id: string; + activity: string; + }[]; + }, + { courseId: string; year: number; term: string } +>; + +export const COURSE_CLASS_TIMES_DETAILED_QUERY: CourseClassTimesDetailedQueryType = gql` + query GetCoursesClassTimesDetailed($courseId: String!, $year: Int!, $term: String!) { + classes(where: { course_id: { _eq: $courseId }, year: { _eq: $year }, term: { _eq: $term } }) { + times { + day + time + location + } + section + class_id + activity + } + } +`; + +export const useCourseClassTimesDetailedQuery = (courseId: string, year: number, term: string) => { + const skip = courseId.length === 0; + + const { data } = useSuspenseQuery(COURSE_CLASS_TIMES_DETAILED_QUERY, { + variables: { courseId, year, term }, + skip, + }); + + // Data should only be undefined if skipped + if (skip || data === undefined) + return [] as { + times: { + day: string; + time: string; + location: string; + }[]; + class_id: string; + section: string; + activity: string; + }[]; + + return data.classes; +}; + const COURSE_LIST_QUERY: TypedDocumentNode< { courses: { diff --git a/client/src/api/timetable/mutations.ts b/client/src/api/timetable/mutations.ts index 7ab6601bd..14409ba2b 100644 --- a/client/src/api/timetable/mutations.ts +++ b/client/src/api/timetable/mutations.ts @@ -1,6 +1,8 @@ import { QueryClient, useMutation } from '@tanstack/react-query'; import { + addEvent, + AddEventParams, addTimetableCourse, createTimetable, deleteTimetable, @@ -89,3 +91,11 @@ export const useDuplicateTimetable = (queryClient: QueryClient) => variables.onSuccess(data); }, }); + +export const useAddTimetableEvent = (queryClient: QueryClient) => + useMutation({ + mutationFn: ({ event }: { event: AddEventParams }) => addEvent(event), + onSuccess: async (_data, variables, _context) => { + await queryClient.invalidateQueries({ queryKey: ['timetable', variables.event.timetableId] }); + }, + }); diff --git a/client/src/api/timetable/queries.ts b/client/src/api/timetable/queries.ts index 5c8bf4341..b364ee0d4 100644 --- a/client/src/api/timetable/queries.ts +++ b/client/src/api/timetable/queries.ts @@ -31,3 +31,14 @@ export const useTimetableInfoQuery = (timetableId: string) => queryKey: ['timetable', timetableId, 'info'], queryFn: () => getTimetableInfo(timetableId), }).data; + +// export const useTimetableEventQuery = (eventIds: string[]) => { +// const queries = useSuspenseQueries({ +// queries: eventIds.map((id) => ({ +// queryKey: ['timetable', 'event', id], +// queryFn: () => getEventInfo(id), +// })), +// }); + +// return queries.map((query) => query.data); +// }; diff --git a/client/src/api/timetable/routes.ts b/client/src/api/timetable/routes.ts index aa6b47c47..93c7819af 100644 --- a/client/src/api/timetable/routes.ts +++ b/client/src/api/timetable/routes.ts @@ -78,3 +78,57 @@ export const makePrimaryTimetable = async (timetableId: string): Promise = export const duplicateTimetable = async (timetableId: string): Promise => { return (await apiClient.post(`/user/timetables/${timetableId}/duplicate`)).data; }; + +interface TimetableEvent { + id: string; + timetableId: string; + colour: string; + title: string; + location: string | null; + description: string | null; + dayOfWeek: number; + start: number; + end: number; + type: 'CUSTOM' | 'TUTORING'; +} + +export const getEventInfo = async (eventId: string) => { + return (await apiClient.get(`/user/timetables/event/${eventId}`)).data; +}; + +export interface AddEventParams { + timetableId: string; + colour: string; + dayOfWeek: number; // 0 = Monday, 6 = Sunday + start: number; // Mins since midnight + end: number; // Mins since midnight + type: 'CUSTOM' | 'TUTORING'; + title: string; + description?: string; + location?: string; +} +export const addEvent = async ({ + timetableId, + colour, + dayOfWeek, + start, + end, + type, + title, + description, + location, +}: AddEventParams): Promise => { + await apiClient.post(`/user/timetables/event/${timetableId}`, { + event: { + timetableId, + colour, + dayOfWeek, + start, + end, + type, + title, + description, + location, + }, + }); +}; diff --git a/client/src/components/planner/controls/ColorOptions.tsx b/client/src/components/planner/controls/ColorOptions.tsx new file mode 100644 index 000000000..9c3677dda --- /dev/null +++ b/client/src/components/planner/controls/ColorOptions.tsx @@ -0,0 +1,87 @@ +import styled from '@emotion/styled'; +import AddIcon from '@mui/icons-material/Add'; +import CloseIcon from '@mui/icons-material/Close'; +import { IconButton, List, ListItem, useTheme } from '@mui/material'; +import { useMemo } from 'react'; + +import { useGetUserSettingsQuery } from '../../../api/user/queries'; +import { decodeColor } from '../../../utils/colors'; + +const COLORS = ['default-1', 'default-2', 'default-3', 'default-4', 'default-5', 'default-6', 'default-7', 'default-8']; + +const StyledColorIconButton = styled(IconButton, { + shouldForwardProp: (prop) => prop !== 'border' && prop !== 'bgColor', +})<{ border: string; bgColor: string }>(({ border, bgColor }) => ({ + backgroundColor: bgColor, + width: 40, + height: 40, + '&:hover': { + backgroundColor: bgColor, + border: `2px solid ${border}`, + }, +})); + +interface ColorOptionsProps { + maxDefaultColors?: number; + showCustomColorPicker: boolean; + onSelectColor: (color: string) => void; + onCustomColorSelect: () => void; +} + +const ColorOptions = ({ + maxDefaultColors = 4, // Default to 4 color options + showCustomColorPicker, + onSelectColor, + onCustomColorSelect, +}: ColorOptionsProps) => { + const theme = useTheme(); + const { preferredTheme } = useGetUserSettingsQuery(); + const decodedColors = useMemo(() => COLORS.map((color) => decodeColor(color, preferredTheme)), [preferredTheme]); + + const selectedThemeColorDisplay = useMemo(() => { + const colorItems = []; + for (let i = 0; i < COLORS.length; i += maxDefaultColors) { + const isLastChunk = i === COLORS.length - maxDefaultColors; + colorItems.push( + COLORS.slice(i, isLastChunk ? i + maxDefaultColors - 1 : i + maxDefaultColors).map((color, j) => ( + + { + onSelectColor(color); + }} + /> + + )), + ); + } + colorItems[colorItems.length - 1].push( + + {showCustomColorPicker ? : } + , + ); + + return colorItems.map((item, index) => ( + + {item} + + )); + }, [ + maxDefaultColors, + theme.palette.secondary.main, + theme.palette.secondary.dark, + decodedColors, + onCustomColorSelect, + showCustomColorPicker, + onSelectColor, + ]); + + return {selectedThemeColorDisplay}; +}; + +export default ColorOptions; diff --git a/client/src/components/planner/controls/ColorPicker.tsx b/client/src/components/planner/controls/ColorPicker.tsx new file mode 100644 index 000000000..89d85aa30 --- /dev/null +++ b/client/src/components/planner/controls/ColorPicker.tsx @@ -0,0 +1,114 @@ +import { Box, Button, ButtonGroup, ListItem, Popover, TextField } from '@mui/material'; +import { Colorful } from '@uiw/react-color'; +import { useMemo, useState } from 'react'; + +import { useGetUserSettingsQuery } from '../../../api/user/queries'; +import { ColorIndicatorBox, StyledButtonContainer } from '../../../styles/ControlStyles'; +import { decodeColor, oklchToHex } from '../../../utils/colors'; +import ColorOptions from './ColorOptions'; + +interface ColorPickerProps { + color: string; + setColor: (color: string) => void; + colorPickerAnchorEl: HTMLElement | null; + handleOpenColorPicker: (event: React.MouseEvent) => void; + handleCloseColorPicker: () => void; + handleSaveNewColor?: () => void; +} + +const ColorPicker: React.FC = ({ + color, + setColor, + colorPickerAnchorEl, + handleOpenColorPicker, + handleCloseColorPicker, + handleSaveNewColor, +}) => { + // Whether the colour picker popover is shown + const openColorPickerPopover = Boolean(colorPickerAnchorEl); + const colorPickerPopoverId = openColorPickerPopover ? 'simple-popover' : undefined; + + const [showCustomColorPicker, setShowCustomColorPicker] = useState(false); + const { preferredTheme } = useGetUserSettingsQuery(); + + const decodedColor = decodeColor(color, preferredTheme); + const textFieldValue = useMemo(() => oklchToHex(decodedColor), [decodedColor]); + + return ( + + + + + + {handleSaveNewColor && ( + + )} + + + + + { + setColor(selectedColor); + }} + onCustomColorSelect={() => { + setShowCustomColorPicker(!showCustomColorPicker); + }} + /> + + {showCustomColorPicker && ( + + { + setColor(e.hex); + }} + color={color} + disableAlpha + /> + + )} + + { + let newColor = e.target.value; + if (newColor !== '' && !newColor.startsWith('#')) { + newColor = `#${newColor}`; + } + setColor(newColor); + }} + /> + + + + ); +}; + +export default ColorPicker; diff --git a/client/src/components/planner/controls/Controls.tsx b/client/src/components/planner/controls/Controls.tsx index 31d09156b..e5ce40c10 100644 --- a/client/src/components/planner/controls/Controls.tsx +++ b/client/src/components/planner/controls/Controls.tsx @@ -84,7 +84,7 @@ const Controls: React.FC<{ term: Term; setTerm: (term: Term) => void; timetableI }} > - + diff --git a/client/src/components/planner/controls/customEvents/CustomEvents.tsx b/client/src/components/planner/controls/customEvents/CustomEvents.tsx index cb2e68b54..3b5b7b1b9 100644 --- a/client/src/components/planner/controls/customEvents/CustomEvents.tsx +++ b/client/src/components/planner/controls/customEvents/CustomEvents.tsx @@ -1,36 +1,54 @@ import { ArrowDropDown, ArrowDropUp } from '@mui/icons-material'; -import { Box } from '@mui/material'; -import { useState } from 'react'; +import { Box, Popover } from '@mui/material'; +import { useMemo, useState } from 'react'; +import { Term } from '../../../../api/times/times'; import { StyledControlsButton } from '../../../../styles/ControlStyles'; import { DropdownButton } from '../../../../styles/CustomEventStyles'; +import CustomEventsForm from './CustomEventsForm'; -const CustomEvent: React.FC = () => { +interface CustomEventsProps { + term: Term; + timetableId: string; +} + +const CustomEvents = ({ term, timetableId }: CustomEventsProps) => { const [createEventAnchorEl, setCreateEventAnchorEl] = useState(null); - const openCreateEventPopover = Boolean(createEventAnchorEl); - const popoverId = openCreateEventPopover ? 'create-event-popover' : undefined; + const popoverId = useMemo(() => (createEventAnchorEl ? 'create-event-popover' : undefined), [createEventAnchorEl]); - const handleOpen = (event: React.MouseEvent) => { + const handlePopoverOpen = (event: React.MouseEvent) => { setCreateEventAnchorEl(event.currentTarget); }; + const handlePopoverClose = () => { + setCreateEventAnchorEl(null); + }; - // TODO: Implement create event popover return ( - - - CREATE EVENT + + + Create Event - {openCreateEventPopover ? : } + {createEventAnchorEl ? : } + + + ); }; -export default CustomEvent; +export default CustomEvents; diff --git a/client/src/components/planner/controls/customEvents/CustomEventsCustomForm.tsx b/client/src/components/planner/controls/customEvents/CustomEventsCustomForm.tsx new file mode 100644 index 000000000..41b55afe0 --- /dev/null +++ b/client/src/components/planner/controls/customEvents/CustomEventsCustomForm.tsx @@ -0,0 +1,148 @@ +import { Event, LocationOn, Notes } from '@mui/icons-material'; +import { ListItemIcon, TextField } from '@mui/material'; +import { TimePicker } from '@mui/x-date-pickers'; +import { useQueryClient } from '@tanstack/react-query'; +import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; + +import { useAddTimetableEvent } from '../../../../api/timetable/mutations'; +import { StyledListItem } from '../../../../styles/ControlStyles'; +import { StyledListItemText } from '../../../../styles/CustomEventStyles'; +import { areValidEventTimes, createDateWithTime } from '../../../../utils/eventHelpers'; +import DropdownOption from '../../timetable/DropdownOption'; + +const DAYS_SHORT = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']; + +interface CustomEventsCustomFormProps { + timetableId: string; + color: string; + setCustomEventFormSatisfied: (satisfied: boolean) => void; +} + +const CustomEventsCustomForm = forwardRef( + ({ timetableId, color, setCustomEventFormSatisfied }: CustomEventsCustomFormProps, ref) => { + const queryClient = useQueryClient(); + const eventCreateMutation = useAddTimetableEvent(queryClient); + + const [eventName, setEventName] = useState(''); + const [eventDescription, setEventDescription] = useState(''); + const [eventLocation, setEventLocation] = useState(''); + const [startTime, setStartTime] = useState(createDateWithTime(9)); + const [endTime, setEndTime] = useState(createDateWithTime(10)); + const [eventDays, setEventDays] = useState([]); + + useEffect(() => { + setCustomEventFormSatisfied(!!eventName && eventDays.length > 0); + }, [eventName, eventDays, setCustomEventFormSatisfied]); + + const handleCreateEvent = () => { + for (const day of eventDays) { + const isMidnight = endTime.getHours() + endTime.getMinutes() / 60 === 0; + eventCreateMutation.mutate({ + event: { + timetableId, + colour: color, + dayOfWeek: DAYS_SHORT.indexOf(day), + start: startTime.getHours() + startTime.getMinutes() / 60, + end: isMidnight ? 24.0 : endTime.getHours() + endTime.getMinutes() / 60, + type: 'CUSTOM', + title: eventName, + description: eventDescription, + location: eventLocation, + }, + }); + } + }; + useImperativeHandle(ref, () => ({ + handleCreateEvent, + })); + + const handleFormat = (newFormats: string[]) => { + setEventDays(newFormats); + }; + + return ( + <> + + + + + { + setEventName(e.target.value); + }} + variant="outlined" + fullWidth + required + /> + + + + + + { + setEventDescription(e.target.value); + }} + variant="outlined" + multiline + fullWidth + /> + + + + + + { + setEventLocation(e.target.value); + }} + variant="outlined" + fullWidth + /> + + + + { + if (e) setStartTime(e); + }} + /> + + + + { + if (e) setEndTime(e); + }} + /> + + + + ); + }, +); + +CustomEventsCustomForm.displayName = 'CustomEventsCustomForm'; +export default CustomEventsCustomForm; diff --git a/client/src/components/planner/controls/customEvents/CustomEventsForm.tsx b/client/src/components/planner/controls/customEvents/CustomEventsForm.tsx new file mode 100644 index 000000000..2e0b5e249 --- /dev/null +++ b/client/src/components/planner/controls/customEvents/CustomEventsForm.tsx @@ -0,0 +1,102 @@ +import { Add } from '@mui/icons-material'; +import { TabContext } from '@mui/lab'; +import { Box, Tab, Tabs } from '@mui/material'; +import { useMemo, useRef, useState } from 'react'; + +import { Term } from '../../../../api/times/times'; +import { ExecuteButton, StyledList, StyledTabPanel } from '../../../../styles/CustomEventStyles'; +import ColorPicker from '../ColorPicker'; +import CustomEventsCustomForm from './CustomEventsCustomForm'; +import CustomEventsTutoringForm from './CustomEventsTutoringForm'; + +interface CustomEventsFormProps { + term: Term; + handlePopoverClose: () => void; + timetableId: string; +} + +const CustomEventsForm = ({ term, handlePopoverClose, timetableId }: CustomEventsFormProps) => { + const customEventFormRef = useRef<{ handleCreateEvent: () => void }>(null); + const tutoringEventFormRef = useRef<{ handleCreateEvent: () => void }>(null); + + const [eventType, setEventType] = useState('General'); + const [customEventFormSatisfied, setCustomEventFormSatisfied] = useState(false); + const [tutoringEventFormSatisfied, setTutoringEventFormSatisfied] = useState(false); + const buttonDisabled = useMemo( + () => + (eventType === 'General' && !customEventFormSatisfied) || + (eventType === 'Tutoring' && !tutoringEventFormSatisfied), + [eventType, customEventFormSatisfied, tutoringEventFormSatisfied], + ); + const [color, setColor] = useState('default-1'); + const [colorPickerAnchorEl, setColorPickerAnchorEl] = useState(null); + + const handleTabChange = (_: React.SyntheticEvent, newEventType: string) => { + setEventType(newEventType); + }; + + const handleCreateEvent = () => { + if (eventType === 'General') { + customEventFormRef.current?.handleCreateEvent(); + } else if (eventType === 'Tutoring') { + tutoringEventFormRef.current?.handleCreateEvent(); + } + handlePopoverClose(); + }; + + return ( + <> + + + + + + + + + + + + + + + + + { + setColorPickerAnchorEl(e.currentTarget); + }} + handleCloseColorPicker={() => { + setColorPickerAnchorEl(null); + }} + /> + + + + Create + + + ); +}; + +export default CustomEventsForm; diff --git a/client/src/components/planner/controls/customEvents/CustomEventsTutoringForm.tsx b/client/src/components/planner/controls/customEvents/CustomEventsTutoringForm.tsx new file mode 100644 index 000000000..8044ab54c --- /dev/null +++ b/client/src/components/planner/controls/customEvents/CustomEventsTutoringForm.tsx @@ -0,0 +1,164 @@ +import { Class, Event } from '@mui/icons-material'; +import { Autocomplete, ListItemIcon, TextField } from '@mui/material'; +import { useQueryClient } from '@tanstack/react-query'; +import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; + +import { Term, useCourseClassTimesDetailedQuery, useCourseListQuery } from '../../../../api/times/times'; +import { useAddTimetableEvent } from '../../../../api/timetable/mutations'; +import { StyledListItem } from '../../../../styles/ControlStyles'; + +const DAYS_SHORT = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; +const TUTORING_ACTIVITY_TYPES = ['Tutorial', 'Laboratory', 'Tutorial-Laboratory', 'Workshop', 'Seminar', 'Project']; + +interface CustomEventsTutoringFormProps { + setTutoringEventFormSatisfied: (satisfied: boolean) => void; + term: Term; + timetableId: string; + color: string; +} + +const CustomEventsTutoringForm = forwardRef( + ({ term, setTutoringEventFormSatisfied, timetableId, color }: CustomEventsTutoringFormProps, ref) => { + const [selectedCourseId, setSelectedCourseId] = useState(''); + const [selectedClass, setSelectedClass] = useState<{ + day: string; + startTime: string; + endTime: string; + classId: string; + section: string; + activity: string; + location: string; + } | null>(null); + + const courseSelectionRef = useRef(null); + + const courseList = useCourseListQuery(term); + const classList = useCourseClassTimesDetailedQuery(selectedCourseId, term.year, term.term); + const queryClient = useQueryClient(); + const eventCreateMutation = useAddTimetableEvent(queryClient); + + const classListOptions = useMemo( + () => + classList + .filter((cls) => cls.times.length > 0 && TUTORING_ACTIVITY_TYPES.includes(cls.activity)) + .map(({ times: cls, class_id, section, activity }) => ({ + day: cls[0].day, + startTime: cls[0].time.split('-')[0].trim(), + endTime: cls[cls.length - 1].time.split('-')[1].trim(), + classId: class_id, + section, + activity, + location: cls[0].location, + })), + [classList], + ); + + useEffect(() => { + setTutoringEventFormSatisfied(!!selectedCourseId && !!selectedClass); + }, [selectedCourseId, selectedClass, setTutoringEventFormSatisfied]); + + const handleCreateEvent = useCallback(() => { + if (!selectedClass) return; + const startTimeHours = parseInt(selectedClass.startTime.split(':')[0], 10); + const startTimeMinutes = parseInt(selectedClass.startTime.split(':')[1], 10); + const endTimeHours = parseInt(selectedClass.endTime.split(':')[0], 10); + const endTimeMinutes = parseInt(selectedClass.endTime.split(':')[1], 10); + + const startTimeVal = startTimeHours + startTimeMinutes / 60; + const endTimeVal = endTimeHours + endTimeMinutes / 60; + + const isMidnight = endTimeHours + endTimeMinutes / 60 === 0; + eventCreateMutation.mutate({ + event: { + timetableId, + colour: color, + dayOfWeek: DAYS_SHORT.indexOf(selectedClass.day), + start: startTimeVal, + end: isMidnight ? 24.0 : endTimeVal, + type: 'TUTORING', + title: `${courseSelectionRef.current?.value ?? ''} - ${selectedClass.activity}`, + description: selectedClass.section, + location: selectedClass.location, + }, + }); + }, [selectedClass, timetableId, color, eventCreateMutation]); + useImperativeHandle(ref, () => ({ + handleCreateEvent, + })); + + return ( + <> + + + + + } + fullWidth + autoHighlight + noOptionsText="No Results" + onChange={(_, value) => { + setSelectedCourseId(value ? value.course_id : ''); + }} + renderOption={(props, option) => { + return ( +
  • + {option.course_code} +
  • + ); + }} + getOptionLabel={(option) => option.course_code} + isOptionEqualToValue={(option, value) => + option.course_id === value.course_id && option.course_code === value.course_code + } + slotProps={{ + listbox: { + sx: { + maxHeight: '120px', + }, + }, + }} + /> +
    + + + + + } + fullWidth + autoHighlight + noOptionsText="No Results" + onChange={(_, value) => { + setSelectedClass(value); + }} + renderOption={(props, option) => { + return ( +
  • + {option.section} +
  • + ); + }} + getOptionLabel={(option) => option.section} + isOptionEqualToValue={(option, value) => option.classId === value.classId} + slotProps={{ + listbox: { + sx: { + maxHeight: '120px', + }, + }, + }} + /> +
    + + ); + }, +); + +CustomEventsTutoringForm.displayName = 'CustomEventsTutoringForm'; +export default CustomEventsTutoringForm; diff --git a/client/src/utils/colors.ts b/client/src/utils/colors.ts index 5a0ba1d21..6b4422618 100644 --- a/client/src/utils/colors.ts +++ b/client/src/utils/colors.ts @@ -1,3 +1,5 @@ +import { oklch2hex } from 'colorizr'; + import { themes } from '../constants/theme'; export const colors: string[] = [ @@ -35,3 +37,16 @@ export const leastUsedColor = (usedColors: string[]): string => { return colors.reduce((a, b) => (colorCount[a] <= colorCount[b] ? a : b)); }; + +export const oklchToHex = (oklch: string): string => { + if (!oklch.startsWith('oklch(')) { + return oklch; + } + + const [l, c, h] = oklch + .replace('oklch(', '') + .replace(')', '') + .split(' ') + .map((v) => parseFloat(v.trim())); + return oklch2hex([l, c, h]); +}; diff --git a/client/src/utils/eventHelpers.ts b/client/src/utils/eventHelpers.ts new file mode 100644 index 000000000..b2ea5cb2a --- /dev/null +++ b/client/src/utils/eventHelpers.ts @@ -0,0 +1,30 @@ +/** + * @param start The starting time of the event + * @param end The ending time of the event + * @returns Whether the start and end times represent a valid event + */ +export const areValidEventTimes = (start: Date, end: Date) => { + // Return true if the event ends at midnight + if (end.getHours() + end.getMinutes() / 60 === 0) { + return true; + } else { + return start.getHours() + start.getMinutes() / 60 < end.getHours() + end.getMinutes() / 60; + } +}; + +/** + * @param time The start or end time of the event + * @returns Whether the start and end times represent a valid event + */ +export const createDateWithTime = (time: number) => { + return new Date(2022, 0, 0, time, (time - Math.floor(time)) * 60); +}; + +/** + * @param day The day of the week the event starts on + * @returns An array of the days of the week starting with the given day + */ +export const resizeWeekArray = (day: number) => { + const MondayToSunday: string[] = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; + return MondayToSunday.slice(day); +}; diff --git a/server/src/timetable/timetable.service.ts b/server/src/timetable/timetable.service.ts index 6356d8947..a6844bcbf 100644 --- a/server/src/timetable/timetable.service.ts +++ b/server/src/timetable/timetable.service.ts @@ -316,12 +316,18 @@ export class TimetableService { where: { id: timetableId }, }); + const events = await this.prisma.event.findMany({ + select: { id: true }, + where: { timetableId }, + }); + return { id: data.id, name: data.name, year: data.year, term: data.term, primary: data.primary, + events: events.map((event) => event.id), }; } diff --git a/server/src/timetable/types.ts b/server/src/timetable/types.ts index b88c93dbb..4ea3d0c31 100644 --- a/server/src/timetable/types.ts +++ b/server/src/timetable/types.ts @@ -16,6 +16,7 @@ export class UserTimetable { year: number; term: string; primary: boolean; + events: string[]; } export enum Term {