Skip to content

Commit f6a39fb

Browse files
Resolve optimiser free day conflicts (#4083)
* issue 4072 * Issue # 4072: Fixed duplicating moduleCode-lessonType conflicts for different freeDays chosen * Issue #4072: typecheck, build, test checks done * Deleted package-lock.json, recordings to use the uniqueKey, not displayText * Removed redundant lessonDays code, now using lessonGroups. (#4072) * nit: rearrange README directory order --------- Co-authored-by: Leslie Yip <[email protected]>
1 parent 803e3a2 commit f6a39fb

File tree

5 files changed

+79
-47
lines changed

5 files changed

+79
-47
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ Temporary Items
140140
.apdisk
141141

142142
### Node ###
143+
package-lock.json
144+
143145
# Logs
144146
logs
145147
*.log
@@ -312,4 +314,4 @@ $RECYCLE.BIN/
312314
# End of https://www.gitignore.io/api/vim,node,linux,macos,emacs,windows,ansible,database,webstorm,visualstudiocode
313315

314316
# asdf versions
315-
.tool-versions
317+
.tool-versions

website/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@ $ yarn promote-staging # Promote ./dist to production
367367
│   │   ├── hocs - Higher order components
368368
│   │   ├── layout - Global layout components
369369
│   │   ├── modules - Module finder and module info components
370+
│   │   ├── optimiser - Timetable optimiser related components
370371
│   │   ├── planner - Module planner related components
371372
│   │   ├── routes - Routing related components
372373
│   │   ├── settings - Settings page component

website/src/views/optimiser/OptimiserContent.tsx

Lines changed: 60 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import OptimiserHeader from './OptimiserHeader';
1313
import OptimiserForm from './OptimiserForm';
1414
import OptimiserButton from './OptimiserButton';
1515
import OptimiserResults from './OptimiserResults';
16-
import { LessonOption, LessonDaysData, FreeDayConflict } from './types';
16+
import { LessonOption, FreeDayConflict } from './types';
1717

1818
const OptimiserContent: React.FC = () => {
1919
const activeSemester = useSelector(({ app }: State) => app.activeSemester);
@@ -39,6 +39,7 @@ const OptimiserContent: React.FC = () => {
3939
const [error, setError] = useState<Error | null>(null);
4040

4141
// Generate lesson options from current timetable
42+
// find the lesson and the type in lessonOptions, and then find the days of that combination in lessonDaysData
4243
const lessonOptions = useMemo(() => {
4344
const options: LessonOption[] = [];
4445

@@ -69,71 +70,86 @@ const OptimiserContent: React.FC = () => {
6970
return options;
7071
}, [timetable, modules, activeSemester, colors]);
7172

72-
const lessonDaysData = useMemo(() => {
73-
const lessonDays: LessonDaysData[] = [];
74-
73+
// group by classNo
74+
const lessonGroupsData = useMemo(() => {
75+
const moduleGroupMap = new Map<string, Map<string, string[]>>();
76+
// each item has moduleCode-lessonType, combination, so get all the groups with days for that group
7577
lessonOptions.forEach((option) => {
7678
const module = modules[option.moduleCode];
77-
if (!module) return;
78-
79+
// get the timetable so that you can get the groups and the days for that group
7980
const moduleTimetable = getModuleTimetable(module, activeSemester);
80-
const lessonsForType = moduleTimetable.filter(
81-
(lesson) => lesson.lessonType === option.lessonType,
82-
);
83-
84-
const days = new Set<string>();
85-
lessonsForType.forEach((lesson) => {
86-
days.add(lesson.day);
87-
});
88-
if (days.has('Saturday')) {
89-
setHasSaturday(true);
90-
}
91-
lessonDays.push({
92-
uniqueKey: option.uniqueKey,
93-
moduleCode: option.moduleCode,
94-
lessonType: option.lessonType,
95-
displayText: option.displayText,
96-
days,
81+
// now get the groups and the days for that group
82+
// the DS for this is a list of maps, {groupName(string): days(list type)}
83+
const groupDayMap = new Map<string, string[]>();
84+
85+
moduleTimetable.forEach((lesson) => {
86+
if (lesson.lessonType === option.lessonType) {
87+
if (groupDayMap.has(lesson.classNo)) {
88+
// find the the item with that key and push the day to the days array
89+
const days = groupDayMap.get(lesson.classNo) || [];
90+
days.push(lesson.day);
91+
groupDayMap.set(lesson.classNo, days);
92+
} else {
93+
groupDayMap.set(lesson.classNo, [lesson.day]);
94+
}
95+
}
9796
});
97+
moduleGroupMap.set(option.uniqueKey, groupDayMap);
98+
// now you have all the groups for that module-lessonType combination
9899
});
99-
100-
return lessonDays;
100+
return moduleGroupMap;
101101
}, [lessonOptions, modules, activeSemester]);
102102

103+
// Unselected module-lessonType combinations are recorded lessons `module lessonType`
103104
const recordings = useMemo(() => {
104105
const selectedKeys = new Set(selectedLessons.map((lesson) => lesson.uniqueKey));
105106
return lessonOptions
106107
.filter((lesson) => !selectedKeys.has(lesson.uniqueKey))
107-
.map((lesson) => lesson.displayText);
108+
.map((lesson) => lesson.uniqueKey);
108109
}, [selectedLessons, lessonOptions]);
109110

110111
// Validate free days against non-recorded lessons
111112
useEffect(() => {
112-
const recordingsSet = new Set(recordings);
113113
const conflicts: FreeDayConflict[] = [];
114114

115-
// Check each non-recorded lesson (physical lessons that user plans to attend)
116-
lessonDaysData.forEach((lessonData) => {
117-
// Skip if this lesson is recorded (not attending in person)
118-
if (recordingsSet.has(lessonData.displayText)) return;
119-
120-
// Check if ALL days for this lesson are selected as free days
121-
const lessonDaysArray = Array.from(lessonData.days);
122-
const conflictingDays = lessonDaysArray.filter((day) => selectedFreeDays.has(day));
123-
124-
// If all lesson days are selected as free days, it's a conflict
125-
if (conflictingDays.length === lessonDaysArray.length && conflictingDays.length > 0) {
115+
// check if all groups are not possible to attend, then it's a conflict, then day of conflict is the days of one of the groups
116+
// go thorugh each module-lessonType combination, and within that,
117+
// go through each group and within that check if any of the selected free days are in them, if so, that group is invalid
118+
lessonGroupsData.forEach((groupMap, uniqueKey) => {
119+
let validGroups = 0;
120+
const groupDays = new Set<string>();
121+
groupMap.forEach((days, _groupName) => {
122+
if (
123+
recordings.includes(uniqueKey) || // if it is a recorded lesson, dont trigger a conflict
124+
!days.some((day) => selectedFreeDays.has(day))
125+
) {
126+
validGroups += 1;
127+
}
128+
days.forEach((day) => groupDays.add(day));
129+
});
130+
if (selectedFreeDays.size > 0 && validGroups === 0) {
131+
// check if conflict with the same moduleCode and lessonType already exists, if so, remove the old one and add the new one
132+
const existingConflict = conflicts.find(
133+
(conflict) =>
134+
conflict.moduleCode === uniqueKey.split('-')[0] &&
135+
conflict.lessonType === uniqueKey.split('-')[1],
136+
);
137+
if (existingConflict) {
138+
conflicts.splice(conflicts.indexOf(existingConflict), 1);
139+
}
140+
if (groupDays.has('Saturday')) {
141+
setHasSaturday(true);
142+
}
126143
conflicts.push({
127-
moduleCode: lessonData.moduleCode,
128-
lessonType: lessonData.lessonType,
129-
displayText: lessonData.displayText,
130-
conflictingDays,
144+
moduleCode: uniqueKey.split('-')[0],
145+
lessonType: uniqueKey.split('-')[1],
146+
displayText: uniqueKey.split('-').join(' '),
147+
conflictingDays: Array.from(selectedFreeDays).filter((day) => groupDays.has(day)),
131148
});
132149
}
133150
});
134-
135151
setFreeDayConflicts(conflicts);
136-
}, [selectedFreeDays, lessonDaysData, recordings]);
152+
}, [selectedFreeDays, lessonGroupsData, recordings]);
137153

138154
useEffect(() => {
139155
const availableKeys = new Set(lessonOptions.map((option) => option.uniqueKey));

website/src/views/optimiser/OptimiserForm.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
22
import classnames from 'classnames';
33
import { Info, X, AlertTriangle } from 'react-feather';
44
import Tooltip from 'views/components/Tooltip';
5+
import { WorkingDays, Day } from 'types/modules';
56
import { LessonOption, FreeDayConflict } from './types';
67
import styles from './OptimiserForm.scss';
78

@@ -189,8 +190,11 @@ const OptimiserForm: React.FC<OptimiserFormProps> = ({
189190
</div>
190191
{freeDayConflicts.map((conflict, index) => (
191192
<div key={index} className={styles.conflictItem}>
192-
<strong>{conflict.displayText}</strong> happens on:{' '}
193-
{conflict.conflictingDays.join(', ')}
193+
<strong>{conflict.displayText}</strong> cannot be assigned due to your free days:{' '}
194+
{conflict.conflictingDays
195+
.filter((d): d is Day => WorkingDays.includes(d as Day))
196+
.sort((a, b) => WorkingDays.indexOf(a as Day) - WorkingDays.indexOf(b as Day))
197+
.join(', ')}
194198
</div>
195199
))}
196200
<div className={styles.conflictFooter}>

website/src/views/optimiser/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ export interface LessonDaysData {
1616
days: Set<string>;
1717
}
1818

19+
export interface LessonGroupData {
20+
groupName: string;
21+
lessonType: LessonType;
22+
moduleCode: ModuleCode;
23+
displayText: string;
24+
classNo: string;
25+
days: Set<string>;
26+
}
27+
1928
export interface FreeDayConflict {
2029
moduleCode: ModuleCode;
2130
lessonType: LessonType;

0 commit comments

Comments
 (0)