diff --git a/export/src/types.ts b/export/src/types.ts index 84d6801c4b..83ecf019a2 100644 --- a/export/src/types.ts +++ b/export/src/types.ts @@ -12,9 +12,11 @@ export type ThemeState = Readonly<{ }>; export type ColorScheme = 'LIGHT_COLOR_SCHEME' | 'DARK_COLOR_SCHEME'; export type Semester = number; -export type ClassNo = string; // E.g. "1", "A" -export type LessonType = string; // E.g. "Lecture", "Tutorial" export type ModuleCode = string; // E.g. "CS3216" +export type LessonType = string; // E.g. "Lecture", "Tutorial" +export type ClassNo = string; // E.g. "1", "A" +export type StartTime = string; +export type DayText = string; export type SemTimetableConfig = { [moduleCode: ModuleCode]: ModuleLessonConfig; }; @@ -22,7 +24,12 @@ export interface ModuleLessonConfig { [lessonType: LessonType]: ClassNo; } export type TaModulesConfig = { - [moduleCode: ModuleCode]: [lessonType: LessonType, classNo: ClassNo][]; + [moduleCode: ModuleCode]: [ + lessonType: LessonType, + classNo: ClassNo, + startTime: StartTime, + day: DayText, + ][]; }; // `ExportData` is duplicated from `website/src/types/export.ts`. diff --git a/website/src/actions/timetables.ts b/website/src/actions/timetables.ts index bfa8e1f30a..509ca48015 100644 --- a/website/src/actions/timetables.ts +++ b/website/src/actions/timetables.ts @@ -9,7 +9,15 @@ import type { } from 'types/timetables'; import type { Dispatch, GetState } from 'types/redux'; import type { ColorMapping } from 'types/reducers'; -import type { ClassNo, LessonType, Module, ModuleCode, Semester } from 'types/modules'; +import type { + ClassNo, + DayText, + LessonType, + Module, + ModuleCode, + Semester, + StartTime, +} from 'types/modules'; import { fetchModule } from 'actions/moduleBank'; import { openNotification } from 'actions/app'; @@ -279,10 +287,12 @@ export function addTaLessonInTimetable( moduleCode: ModuleCode, lessonType: LessonType, classNo: ClassNo, + startTime: StartTime, + day: DayText, ) { return { type: ADD_TA_LESSON_IN_TIMETABLE, - payload: { semester, moduleCode, lessonType, classNo }, + payload: { semester, moduleCode, lessonType, classNo, startTime, day }, }; } @@ -292,10 +302,12 @@ export function removeTaLessonInTimetable( moduleCode: ModuleCode, lessonType: LessonType, classNo: ClassNo, + startTime: StartTime, + day: DayText, ) { return { type: REMOVE_TA_LESSON_IN_TIMETABLE, - payload: { semester, moduleCode, lessonType, classNo }, + payload: { semester, moduleCode, lessonType, classNo, startTime, day }, }; } diff --git a/website/src/reducers/index.test.ts b/website/src/reducers/index.test.ts index fa421a4c5e..000b274c23 100644 --- a/website/src/reducers/index.test.ts +++ b/website/src/reducers/index.test.ts @@ -30,7 +30,7 @@ const exportData: ExportData = { }, hidden: ['PC1222'], ta: { - CS1010S: [['Tutorial', '1']], + CS1010S: [['Tutorial', '1', '0900', 'Monday']], }, theme: { id: 'google', @@ -79,7 +79,7 @@ test('reducers should set export data state', () => { hidden: { [1]: ['PC1222'] }, ta: { [1]: { - CS1010S: [['Tutorial', '1']], + CS1010S: [['Tutorial', '1', '0900', 'Monday']], }, }, academicYear: expect.any(String), diff --git a/website/src/reducers/timetables.test.ts b/website/src/reducers/timetables.test.ts index 750da438fb..e5da49edec 100644 --- a/website/src/reducers/timetables.test.ts +++ b/website/src/reducers/timetables.test.ts @@ -129,16 +129,22 @@ describe('hidden module reducer', () => { describe('TA module reducer', () => { const withTaModules: TimetablesState = { ...initialState, - ta: { [1]: { CS1010S: [['Tutorial', '1']] }, [2]: { CS1010S: [['Tutorial', '1']] } }, + ta: { + [1]: { CS1010S: [['Tutorial', '1', '0900', 'Monday']] }, + [2]: { CS1010S: [['Tutorial', '1', '0900', 'Monday']] }, + }, }; test('should update TA modules', () => { expect( - reducer(initialState, addTaLessonInTimetable(1, 'CS3216', 'Tutorial', '1')), - ).toHaveProperty('ta.1', { CS3216: [['Tutorial', '1']] }); + reducer(initialState, addTaLessonInTimetable(1, 'CS3216', 'Tutorial', '1', '0900', 'Monday')), + ).toHaveProperty('ta.1', { CS3216: [['Tutorial', '1', '0900', 'Monday']] }); expect( - reducer(initialState, removeTaLessonInTimetable(1, 'CS1010S', 'Tutorial', '1')), + reducer( + initialState, + removeTaLessonInTimetable(1, 'CS1010S', 'Tutorial', '1', '0900', 'Monday'), + ), ).toMatchObject({ ta: { [1]: {}, @@ -146,11 +152,14 @@ describe('TA module reducer', () => { }); expect( - reducer(withTaModules, removeTaLessonInTimetable(1, 'CS1010S', 'Tutorial', '1')), + reducer( + withTaModules, + removeTaLessonInTimetable(1, 'CS1010S', 'Tutorial', '1', '0900', 'Monday'), + ), ).toMatchObject({ ta: { [1]: {}, - [2]: { CS1010S: [['Tutorial', '1']] }, + [2]: { CS1010S: [['Tutorial', '1', '0900', 'Monday']] }, }, }); }); @@ -160,14 +169,17 @@ describe('TA module reducer', () => { reducer( { ...initialState, - ta: { [1]: { CS1010S: [['Tutorial', '1']] }, [2]: { CS1010S: [['Tutorial', '1']] } }, + ta: { + [1]: { CS1010S: [['Tutorial', '1', '0900', 'Monday']] }, + [2]: { CS1010S: [['Tutorial', '1', '0900', 'Monday']] }, + }, }, removeModule(1, 'CS1010S'), ), ).toMatchObject({ ta: { [1]: {}, - [2]: { CS1010S: [['Tutorial', '1']] }, + [2]: { CS1010S: [['Tutorial', '1', '0900', 'Monday']] }, }, }); }); diff --git a/website/src/reducers/timetables.ts b/website/src/reducers/timetables.ts index f17a2fd10c..b278e325e7 100644 --- a/website/src/reducers/timetables.ts +++ b/website/src/reducers/timetables.ts @@ -49,9 +49,18 @@ export const persistConfig = { // eslint-disable-next-line no-underscore-dangle, @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain _persist: state?._persist!, }), + 3: (state) => ({ + ...state, + ta: {}, + // FIXME: Remove the next line when _persist is optional again. + // Cause: https://github.com/rt2zz/redux-persist/pull/919 + // Issue: https://github.com/rt2zz/redux-persist/pull/1170 + // eslint-disable-next-line no-underscore-dangle, @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain + _persist: state?._persist!, + }), }), /* eslint-enable */ - version: 2, + version: 3, // Our own state reconciler archives old timetables if the acad year is different, // otherwise use the persisted timetable state @@ -190,20 +199,27 @@ function semTaModules(state = DEFAULT_TA_STATE, action: Actions): TaModulesConfi switch (action.type) { case ADD_TA_LESSON_IN_TIMETABLE: { - const { moduleCode, lessonType, classNo } = action.payload; + const { moduleCode, lessonType, classNo, startTime, day } = action.payload; if (!(moduleCode && lessonType && classNo)) return state; + // Prevent duplicate lessons + if (moduleCode in state) { + const isDuplicate = state[moduleCode].some((existingConfig) => + isEqual(existingConfig, [lessonType, classNo, startTime, day]), + ); + if (isDuplicate) return state; + } return { ...state, - [moduleCode]: [...(state[moduleCode] ?? []), [lessonType, classNo]], + [moduleCode]: [...(state[moduleCode] ?? []), [lessonType, classNo, startTime, day]], }; } case REMOVE_TA_LESSON_IN_TIMETABLE: { - const { moduleCode, lessonType, classNo } = action.payload; + const { moduleCode, lessonType, classNo, startTime, day } = action.payload; if (!(moduleCode && lessonType && classNo)) return state; return { ...state, [moduleCode]: state[moduleCode]?.filter( - (lesson) => !isEqual(lesson, [lessonType, classNo]), + (lesson) => !isEqual(lesson, [lessonType, classNo, startTime, day]), ), }; } diff --git a/website/src/types/timetables.ts b/website/src/types/timetables.ts index 34670cddd0..cdac560dbc 100644 --- a/website/src/types/timetables.ts +++ b/website/src/types/timetables.ts @@ -1,4 +1,12 @@ -import { ClassNo, LessonType, ModuleCode, ModuleTitle, RawLesson } from './modules'; +import { + ClassNo, + DayText, + LessonType, + ModuleCode, + ModuleTitle, + RawLesson, + StartTime, +} from './modules'; // ModuleLessonConfig is a mapping of lessonType to ClassNo for a module. export type ModuleLessonConfig = { @@ -11,8 +19,14 @@ export type SemTimetableConfig = { }; // TaModulesConfig is a mapping of moduleCode to the TA's lesson types. +// startTime and day are needed since some modules (e.g. CS2103T) map a single classNo to multiple lessons. export type TaModulesConfig = { - [moduleCode: ModuleCode]: [lessonType: LessonType, classNo: ClassNo][]; + [moduleCode: ModuleCode]: [ + lessonType: LessonType, + classNo: ClassNo, + startTime: StartTime, + day: DayText, + ][]; }; // ModuleLessonConfigWithLessons is a mapping of lessonType to an array of Lessons for a module. @@ -67,9 +81,11 @@ export type TimetableArrangement = { // Represents the lesson which the user is currently hovering over. // Used to highlight lessons which have the same classNo export type HoverLesson = { - readonly classNo: ClassNo; readonly moduleCode: ModuleCode; readonly lessonType: LessonType; + readonly classNo: ClassNo; + readonly startTime: StartTime; + readonly day: DayText; }; export type ColorIndex = number; diff --git a/website/src/utils/ical.test.ts b/website/src/utils/ical.test.ts index 15b5b4d614..ad1761bd56 100644 --- a/website/src/utils/ical.test.ts +++ b/website/src/utils/ical.test.ts @@ -366,7 +366,7 @@ describe(iCalForTimetable, () => { CS3216, }; const actual = iCalForTimetable(1, mockTimetable, moduleData, [], { - CS1010S: [['Tutorial', '1']], + CS1010S: [['Tutorial', '1', '0800', 'Monday']], }); // 5 lesson types for cs1010s, 1 for cs3216 (1 exam for cs1010s will be excluded) expect(actual).toHaveLength(6); diff --git a/website/src/utils/timetables.test.ts b/website/src/utils/timetables.test.ts index b928c2ab74..ac3f58192a 100644 --- a/website/src/utils/timetables.test.ts +++ b/website/src/utils/timetables.test.ts @@ -108,9 +108,9 @@ test('hydrateTaModulesConfigWithLessons should replace ClassNo with lessons', () const modules: ModulesMap = { [moduleCode]: CS1010S }; const taModules: TaModulesConfig = { [moduleCode]: [ - ['Tutorial', '1'], - ['Tutorial', '8'], - ['Recitation', '4'], + ['Tutorial', '1', '0900', 'Monday'], + ['Tutorial', '8', '1600', 'Monday'], + ['Recitation', '4', '1700', 'Thursday'], ], }; @@ -120,8 +120,14 @@ test('hydrateTaModulesConfigWithLessons should replace ClassNo with lessons', () sem, ); expect(configWithLessons[moduleCode].Tutorial[0].classNo).toBe('1'); + expect(configWithLessons[moduleCode].Tutorial[0].startTime).toBe('0900'); + expect(configWithLessons[moduleCode].Tutorial[0].day).toBe('Monday'); expect(configWithLessons[moduleCode].Tutorial[1].classNo).toBe('8'); + expect(configWithLessons[moduleCode].Tutorial[1].startTime).toBe('1600'); + expect(configWithLessons[moduleCode].Tutorial[1].day).toBe('Monday'); expect(configWithLessons[moduleCode].Recitation[0].classNo).toBe('4'); + expect(configWithLessons[moduleCode].Recitation[0].startTime).toBe('1700'); + expect(configWithLessons[moduleCode].Recitation[0].day).toBe('Thursday'); expect(configWithLessons[moduleCode]).not.toHaveProperty('Lecture'); }); diff --git a/website/src/utils/timetables.ts b/website/src/utils/timetables.ts index 784ce64767..1fb5ac1f75 100644 --- a/website/src/utils/timetables.ts +++ b/website/src/utils/timetables.ts @@ -27,12 +27,14 @@ import qs from 'query-string'; import { ClassNo, consumeWeeks, + DayText, LessonType, Module, ModuleCode, NumericWeeks, RawLesson, Semester, + StartTime, } from 'types/modules'; import { @@ -52,7 +54,7 @@ import { import { ModuleCodeMap, ModulesMap } from 'types/reducers'; import { ExamClashes } from 'types/views'; -import { getTimeAsDate } from './timify'; +import { getTimeAsDate, SCHOOLDAYS } from './timify'; import { getModuleTimetable, getExamDate, getExamDuration } from './modules'; import { deltas } from './array'; @@ -137,21 +139,32 @@ export function hydrateTaModulesConfigWithLessons( ): SemTimetableConfigWithLessons { return mapValues( taModules, - (lessons: [lessonType: LessonType, classNo: ClassNo][], moduleCode: ModuleCode) => { + ( + lessons: [lessonType: LessonType, classNo: ClassNo, startTime: StartTime, day: DayText][], + moduleCode: ModuleCode, + ) => { const module = modules[moduleCode]; if (!module) return EMPTY_OBJECT; const moduleLessonConfigWithLessons: ModuleLessonConfigWithLessons = {}; - forEach(lessons, ([lessonType, classNo]) => { + forEach(lessons, ([lessonType, classNo, startTime, day]) => { const moduleConfigWithLessons = hydrateModuleConfigWithLessons( { [lessonType]: classNo }, module, semester, ); + + const moduleConfigForLessonType = moduleConfigWithLessons[lessonType].filter( + (lesson) => lesson.startTime === startTime && lesson.day === day, + ); + if (isEmpty(moduleConfigForLessonType)) { + return; + } + if (!(lessonType in moduleLessonConfigWithLessons)) { moduleLessonConfigWithLessons[lessonType] = []; } - moduleLessonConfigWithLessons[lessonType].push(...moduleConfigWithLessons[lessonType]); + moduleLessonConfigWithLessons[lessonType].push(...moduleConfigForLessonType); }); return moduleLessonConfigWithLessons; }, @@ -599,21 +612,23 @@ export function deserializeHidden(serialized: string): ModuleCode[] { } export function serializeTa(taModules: TaModulesConfig) { - // eg: // eg: // { - // CS2100: [ ['Tutorial', '2'], ['Tutorial', '3'], ['Laboratory', '1'] ], - // CS2107: [ ['Tutorial', '8'] ], + // CS2100: [ ['Laboratory', '30', '1100', 'Monday'], ['Tutorial', '38', '1200', 'Monday'] ], + // CS2103T: [ ['Lecture', 'G12', '0800', 'Thursday'] ], // } - // => &ta=CS2100(TUT:2,TUT:3,LAB:1);CS2107(TUT:8) + // => &ta=CS2100(LAB:30:1100:0,TUT:38:1200:0,LAB:1),CS2103T(LEC:G12:0800:3) return `&ta=${flatMap( taModules, (lessons, moduleCode) => `${moduleCode}(${lessons - .map( - ([lessonType, classNo]) => - `${LESSON_TYPE_ABBREV[lessonType]}${LESSON_TYPE_SEP}${encodeURIComponent(classNo)}`, - ) + .map(([lessonType, classNo, startTime, day]) => { + const dayIndex = SCHOOLDAYS.indexOf(day); + const taLessonConfig = [classNo, startTime, dayIndex] + .map(encodeURIComponent) + .join(LESSON_TYPE_SEP); + return `${LESSON_TYPE_ABBREV[lessonType]}${LESSON_TYPE_SEP}${taLessonConfig}`; + }) .join(LESSON_SEP)})`, ).join(LESSON_SEP)}`; } @@ -631,7 +646,7 @@ export function deserializeTa(serialized: string): TaModulesConfig { return; } - const lessonsMatches = moduleConfig.match(/\((.*)\)/); + const lessonsMatches = moduleConfig.match(/\((.*)/); if (lessonsMatches === null) { return; } @@ -639,14 +654,15 @@ export function deserializeTa(serialized: string): TaModulesConfig { const moduleCode = moduleCodeMatches[1]; const lessons = lessonsMatches[1]; lessons.split(LESSON_SEP).forEach((lesson) => { - const [lessonTypeAbbr, classNo] = lesson.split(LESSON_TYPE_SEP); + const [lessonTypeAbbr, classNo, startTime, dayIndex] = lesson.split(LESSON_TYPE_SEP); if (!(moduleCode in deserialized)) { deserialized[moduleCode] = []; } const lessonType = LESSON_ABBREV_TYPE[lessonTypeAbbr]; + const day = SCHOOLDAYS[parseInt(dayIndex, 10)]; // Ignore unparsable/invalid keys - if (!lessonType) return; - deserialized[moduleCode].push([lessonType, classNo]); + if (!(lessonType && day)) return; + deserialized[moduleCode].push([lessonType, classNo, startTime, day]); }); }); return deserialized; @@ -670,9 +686,11 @@ export function isSameLesson(l1: Lesson, l2: Lesson) { export function getHoverLesson(lesson: Lesson): HoverLesson { return { - classNo: lesson.classNo, moduleCode: lesson.moduleCode, lessonType: lesson.lessonType, + classNo: lesson.classNo, + startTime: lesson.startTime, + day: lesson.day, }; } diff --git a/website/src/views/timetable/ModulesTableFooter.test.tsx b/website/src/views/timetable/ModulesTableFooter.test.tsx index 8690ea0930..47cd4b17a1 100644 --- a/website/src/views/timetable/ModulesTableFooter.test.tsx +++ b/website/src/views/timetable/ModulesTableFooter.test.tsx @@ -20,7 +20,7 @@ describe(countShownMCs, () => { const modules = [BFS1001, CS1010S, CS3216]; const taInTimetable: TaModulesConfig = { [CS1010S.moduleCode]: [], - [CS3216.moduleCode]: [['Tutorial', '1']], + [CS3216.moduleCode]: [['Tutorial', '1', '0900', 'Monday']], }; expect(countShownMCs(modules, [], taInTimetable)).toEqual(4); }); diff --git a/website/src/views/timetable/ShareTimetable.test.tsx b/website/src/views/timetable/ShareTimetable.test.tsx index 30e5d385bf..c464389cc2 100644 --- a/website/src/views/timetable/ShareTimetable.test.tsx +++ b/website/src/views/timetable/ShareTimetable.test.tsx @@ -164,14 +164,14 @@ describe('ShareTimetable', () => { timetable={timetable} hiddenModules={[]} taModules={{ - MA1521: [['Tutorial', '1']], + MA1521: [['Tutorial', '1', '0800', 'Monday']], CS1010S: [ - ['Tutorial', '1'], - ['Laboratory', '1'], + ['Tutorial', '1', '0800', 'Tuesday'], + ['Laboratory', '1', '1000', 'Wednesday'], ], - CS1231S: [ - ['Tutorial', '2'], - ['Tutorial', '3'], + CS2103T: [ + ['Lecture', 'G12', '0800', 'Thursday'], + ['Lecture', 'G12', '1700', 'Friday'], ], }} />, @@ -180,7 +180,7 @@ describe('ShareTimetable', () => { await openAndWait(wrapper); expect(wrapper.find('input').prop('value')).toContain( - 'ta=MA1521(TUT:1),CS1010S(TUT:1,LAB:1),CS1231S(TUT:2,TUT:3)', + 'ta=MA1521(TUT:1:0800:0),CS1010S(TUT:1:0800:1,LAB:1:1000:2),CS2103T(LEC:G12:0800:3,LEC:G12:1700:4)', ); }); diff --git a/website/src/views/timetable/TimetableCell.test.tsx b/website/src/views/timetable/TimetableCell.test.tsx index b958b7044b..403e75bcfa 100644 --- a/website/src/views/timetable/TimetableCell.test.tsx +++ b/website/src/views/timetable/TimetableCell.test.tsx @@ -65,8 +65,10 @@ describe(TimetableCell, () => { const { wrapper } = make({ hoverLesson: { moduleCode: 'CS1010', - classNo: '1', lessonType: 'Lecture', + classNo: '1', + startTime: '1000', + day: 'Wednesday', }, }); @@ -80,8 +82,10 @@ describe(TimetableCell, () => { button = make({ hoverLesson: { moduleCode: 'CS1010', - classNo: '1', lessonType: 'Tutorial', + classNo: '1', + startTime: '0900', + day: 'Monday', }, }) .wrapper.find('button') @@ -94,6 +98,8 @@ describe(TimetableCell, () => { moduleCode: 'CS1010', classNo: '2', lessonType: 'Lecture', + startTime: '0900', + day: 'Monday', }, }) .wrapper.find('button') @@ -106,6 +112,8 @@ describe(TimetableCell, () => { moduleCode: 'CS1101S', classNo: '1', lessonType: 'Lecture', + startTime: '0900', + day: 'Monday', }, }) .wrapper.find('button') diff --git a/website/src/views/timetable/TimetableCell.tsx b/website/src/views/timetable/TimetableCell.tsx index f0348b322d..6ef8a7f377 100644 --- a/website/src/views/timetable/TimetableCell.tsx +++ b/website/src/views/timetable/TimetableCell.tsx @@ -91,7 +91,11 @@ const TimetableCell: React.FC = (props) => { const moduleName = showTitle ? `${lesson.moduleCode} ${lesson.title}` : lesson.moduleCode; const Cell = props.onClick ? 'button' : 'div'; - const isHoveredOver = isEqual(getHoverLesson(lesson), hoverLesson); + const isHoveredOver = lesson.isTaInTimetable + ? isEqual(getHoverLesson(lesson), hoverLesson) + : lesson.moduleCode === hoverLesson?.moduleCode && + lesson.lessonType === hoverLesson.lessonType && + lesson.classNo === hoverLesson.classNo; const conditionalProps = onClick ? { @@ -140,6 +144,7 @@ const TimetableCell: React.FC = (props) => { {lesson.isTaInTimetable && + onClick && isHoveredOver && hoverLesson && (lesson.isActive || !lesson.isOptionInTimetable ? ( diff --git a/website/src/views/timetable/TimetableContent.tsx b/website/src/views/timetable/TimetableContent.tsx index c8e9e2b49a..40f4c83024 100644 --- a/website/src/views/timetable/TimetableContent.tsx +++ b/website/src/views/timetable/TimetableContent.tsx @@ -4,7 +4,15 @@ import { connect } from 'react-redux'; import { sortBy, difference, values, flatten, mapValues, isEmpty } from 'lodash'; import { ColorMapping, HORIZONTAL, ModulesMap, TimetableOrientation } from 'types/reducers'; -import { ClassNo, LessonType, Module, ModuleCode, Semester } from 'types/modules'; +import { + ClassNo, + DayText, + LessonType, + Module, + ModuleCode, + Semester, + StartTime, +} from 'types/modules'; import { ColoredLesson, Lesson, @@ -39,6 +47,7 @@ import { getSemesterModules, hydrateSemTimetableWithLessons, hydrateTaModulesConfigWithLessons, + isSameLesson, lessonsForLessonType, timetableLessonsArray, } from 'utils/timetables'; @@ -95,12 +104,16 @@ type Props = OwnProps & { moduleCode: ModuleCode, lessonType: LessonType, classNo: ClassNo, + startTime: StartTime, + day: DayText, ) => void; removeTaLessonInTimetable: ( semester: Semester, moduleCode: ModuleCode, lessonType: LessonType, classNo: ClassNo, + startTime: StartTime, + day: DayText, ) => void; }; @@ -179,23 +192,44 @@ class TimetableContent extends React.Component { isTaInTimetable = (moduleCode: ModuleCode) => this.props.taInTimetable[moduleCode]?.length > 0; - // Adds current non lecture lessons as TA lessons + // Adds all current lessons as TA lessons setTaLessonInTimetable = (semester: Semester, moduleCode: ModuleCode) => { timetableLessonsArray(this.props.timetableWithLessons) - .filter((lesson) => lesson.moduleCode === moduleCode && lesson.lessonType !== 'Lecture') + .filter((lesson) => lesson.moduleCode === moduleCode) .forEach((lesson) => - this.props.addTaLessonInTimetable(semester, moduleCode, lesson.lessonType, lesson.classNo), + this.props.addTaLessonInTimetable( + semester, + moduleCode, + lesson.lessonType, + lesson.classNo, + lesson.startTime, + lesson.day, + ), ); }; modifyTaCell(lesson: ModifiableLesson) { - const { moduleCode, lessonType, classNo } = lesson; + const { moduleCode, lessonType, classNo, startTime, day } = lesson; if (lesson.isOptionInTimetable) { // Allow multiple lessons of the same type to be added for TA lessons - this.props.addTaLessonInTimetable(this.props.semester, moduleCode, lessonType, classNo); + this.props.addTaLessonInTimetable( + this.props.semester, + moduleCode, + lessonType, + classNo, + startTime, + day, + ); } else if (this.props.taInTimetable[moduleCode].length > 1) { // If a TA lesson is the last of its type, disallow removing it - this.props.removeTaLessonInTimetable(this.props.semester, moduleCode, lessonType, classNo); + this.props.removeTaLessonInTimetable( + this.props.semester, + moduleCode, + lessonType, + classNo, + startTime, + day, + ); } else { this.props.cancelModifyLesson(); } @@ -352,14 +386,18 @@ class TimetableContent extends React.Component { if (activeLesson) { const { moduleCode } = activeLesson; // Remove activeLesson because it will appear again - timetableLessons = timetableLessons.filter( - (lesson) => !areLessonsSameClass(lesson, activeLesson), - ); + // If activeLesson is a TA mod, only the exact lesson that is selected is active + // Otherwise, all lessons with the same class number are active + timetableLessons = this.isTaInTimetable(activeLesson.moduleCode) + ? (timetableLessons = timetableLessons.filter( + (lesson) => !isSameLesson(lesson, activeLesson), + )) + : timetableLessons.filter((lesson) => !areLessonsSameClass(lesson, activeLesson)); const module = modules[moduleCode]; const moduleTimetable = getModuleTimetable(module, semester); const lessonOptions = this.isTaInTimetable(moduleCode) - ? moduleTimetable.filter((lesson) => lesson.lessonType !== 'Lecture') + ? moduleTimetable : lessonsForLessonType(moduleTimetable, activeLesson.lessonType); lessonOptions.forEach((lesson) => { const modifiableLesson: Omit = { @@ -369,16 +407,31 @@ class TimetableContent extends React.Component { title: module.title, }; + // Skip lessons that are already in the timetable + if ( + timetableLessons.some( + (existingLesson) => + lesson.lessonType === existingLesson.lessonType && + lesson.classNo === existingLesson.classNo && + lesson.startTime === existingLesson.startTime && + lesson.day === existingLesson.day, + ) + ) { + return; + } + + const isTa = this.isTaInTimetable(moduleCode); + const isActive = isTa + ? isSameLesson(modifiableLesson, activeLesson) + : areLessonsSameClass(modifiableLesson, activeLesson); + // All lessons added within this block are options to be added in the timetable // Except for the activeLesson modifiableLesson.isOptionInTimetable = true; - if (areLessonsSameClass(modifiableLesson, activeLesson)) { + if (isActive) { modifiableLesson.isActive = true; modifiableLesson.isOptionInTimetable = false; - } else if ( - this.isTaInTimetable(moduleCode) || - lesson.lessonType === activeLesson.lessonType - ) { + } else if (isTa || lesson.lessonType === activeLesson.lessonType) { modifiableLesson.isAvailable = true; } timetableLessons.push(modifiableLesson);