diff --git a/frontend/src/assets/TermPlannerHelp/unschedule-dark.gif b/frontend/src/assets/TermPlannerHelp/unschedule-dark.gif index 44352d491..1dd3117ac 100644 Binary files a/frontend/src/assets/TermPlannerHelp/unschedule-dark.gif and b/frontend/src/assets/TermPlannerHelp/unschedule-dark.gif differ diff --git a/frontend/src/assets/TermPlannerHelp/unschedule-dark.jpg b/frontend/src/assets/TermPlannerHelp/unschedule-dark.jpg index 2d653108f..581f7e5a4 100644 Binary files a/frontend/src/assets/TermPlannerHelp/unschedule-dark.jpg and b/frontend/src/assets/TermPlannerHelp/unschedule-dark.jpg differ diff --git a/frontend/src/assets/TermPlannerHelp/unschedule-light.gif b/frontend/src/assets/TermPlannerHelp/unschedule-light.gif index 2f27d6aee..8bb5b25bb 100644 Binary files a/frontend/src/assets/TermPlannerHelp/unschedule-light.gif and b/frontend/src/assets/TermPlannerHelp/unschedule-light.gif differ diff --git a/frontend/src/assets/TermPlannerHelp/unschedule-light.jpg b/frontend/src/assets/TermPlannerHelp/unschedule-light.jpg index 3a286031e..31502fa78 100644 Binary files a/frontend/src/assets/TermPlannerHelp/unschedule-light.jpg and b/frontend/src/assets/TermPlannerHelp/unschedule-light.jpg differ diff --git a/frontend/src/components/PlannerButton/PlannerButton.tsx b/frontend/src/components/PlannerButton/PlannerButton.tsx index 422232eea..6b895f4b7 100644 --- a/frontend/src/components/PlannerButton/PlannerButton.tsx +++ b/frontend/src/components/PlannerButton/PlannerButton.tsx @@ -53,6 +53,7 @@ const PlannerButton = ({ course, hasPlannerUpdated }: PlannerButtonProps) => { isAccurate: course.is_accurate, isMultiterm: course.is_multiterm, supressed: false, + ignoreFromProgression: false, mark: undefined }; dispatch(addToUnplanned({ courseCode: course.code, courseData })); diff --git a/frontend/src/components/QuickAddCartButton/QuickAddCartButton.tsx b/frontend/src/components/QuickAddCartButton/QuickAddCartButton.tsx index 87f361c47..afa07dfd6 100644 --- a/frontend/src/components/QuickAddCartButton/QuickAddCartButton.tsx +++ b/frontend/src/components/QuickAddCartButton/QuickAddCartButton.tsx @@ -42,6 +42,7 @@ const QuickAddCartButton = ({ courseCode, planned }: Props) => { isAccurate: course.is_accurate, isMultiterm: course.is_multiterm, supressed: false, + ignoreFromProgression: false, mark: undefined }; dispatch(addToUnplanned({ courseCode: course.code, courseData })); diff --git a/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx b/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx index fd41b613b..6b1af1eb1 100644 --- a/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx +++ b/frontend/src/pages/ProgressionChecker/Dashboard/Dashboard.tsx @@ -43,7 +43,7 @@ const Dashboard = ({ isLoading, structure, totalUOC, freeElectivesUOC }: Props) let completedUOC = 0; Object.keys(courses).forEach((courseCode) => { - if (courses[courseCode]?.plannedFor) { + if (courses[courseCode]?.plannedFor && !courses[courseCode]?.ignoreFromProgression) { completedUOC += courses[courseCode].UOC * getNumTerms(courses[courseCode].UOC, courses[courseCode].isMultiterm); @@ -70,7 +70,11 @@ const Dashboard = ({ isLoading, structure, totalUOC, freeElectivesUOC }: Props) let currUOC = 0; // only consider disciplinary component courses Object.keys(subgroupStructure.courses).forEach((courseCode) => { - if (courses[courseCode]?.plannedFor && currUOC < subgroupStructure.UOC) { + if ( + courses[courseCode]?.plannedFor && + currUOC < subgroupStructure.UOC && + !courses[courseCode]?.ignoreFromProgression + ) { const courseUOC = courses[courseCode].UOC * getNumTerms(courses[courseCode].UOC, courses[courseCode].isMultiterm); diff --git a/frontend/src/pages/ProgressionChecker/FreeElectivesSection/index.ts b/frontend/src/pages/ProgressionChecker/FreeElectivesSection/index.ts deleted file mode 100644 index 5c3a4d8ba..000000000 --- a/frontend/src/pages/ProgressionChecker/FreeElectivesSection/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import FreeElectiveSection from './FreeElectiveSection'; - -export default FreeElectiveSection; diff --git a/frontend/src/pages/ProgressionChecker/FreeElectivesSection/FreeElectiveSection.tsx b/frontend/src/pages/ProgressionChecker/GenericCoursesSection/GenericCoursesSection.tsx similarity index 65% rename from frontend/src/pages/ProgressionChecker/FreeElectivesSection/FreeElectiveSection.tsx rename to frontend/src/pages/ProgressionChecker/GenericCoursesSection/GenericCoursesSection.tsx index 4444c8518..d7b95d28a 100644 --- a/frontend/src/pages/ProgressionChecker/FreeElectivesSection/FreeElectiveSection.tsx +++ b/frontend/src/pages/ProgressionChecker/GenericCoursesSection/GenericCoursesSection.tsx @@ -11,9 +11,12 @@ const { Title } = Typography; type Props = { courses: ViewSubgroupCourse[]; view: Views; + title: string; + subheading: string; + description: string; }; -const FreeElectivesSection = ({ courses, view }: Props) => { +const GenericCoursesSection = ({ courses, view, title, subheading, description }: Props) => { const uoc = courses.reduce( (sum, course) => sum + (course.UOC ?? 0) * getNumTerms(course.UOC ?? 0, course.isMultiterm), 0 @@ -22,18 +25,15 @@ const FreeElectivesSection = ({ courses, view }: Props) => { return ( - Free Electives + + {title} } > - You have {uoc} UOC worth of additional courses planned + You have {uoc} UOC worth of {subheading} -

- These courses may or may not be counted to your program. Please manually verify your - progression with this information. -

+

{description}

{view === Views.TABLE ? ( ({ @@ -44,14 +44,10 @@ const FreeElectivesSection = ({ courses, view }: Props) => { pagination={false} /> ) : ( - + )} ); }; -export default FreeElectivesSection; +export default GenericCoursesSection; diff --git a/frontend/src/pages/ProgressionChecker/GenericCoursesSection/index.ts b/frontend/src/pages/ProgressionChecker/GenericCoursesSection/index.ts new file mode 100644 index 000000000..ffbe86fcd --- /dev/null +++ b/frontend/src/pages/ProgressionChecker/GenericCoursesSection/index.ts @@ -0,0 +1,3 @@ +import GenericCoursesSection from './GenericCoursesSection'; + +export default GenericCoursesSection; diff --git a/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx b/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx index 2c42c803b..2d2a89cab 100644 --- a/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx +++ b/frontend/src/pages/ProgressionChecker/ProgressionChecker.tsx @@ -24,7 +24,7 @@ import PageTemplate from 'components/PageTemplate'; import { MAX_COURSES_OVERFLOW } from 'config/constants'; import type { RootState } from 'config/store'; import Dashboard from './Dashboard'; -import FreeElectiveSection from './FreeElectivesSection'; +import GenericCoursesSection from './GenericCoursesSection'; import GridView from './GridView'; import S from './styles'; import TableView from './TableView'; @@ -76,6 +76,8 @@ const ProgressionChecker = () => { // these courses are appended as 'Additional Electives' const overflowCourses: ProgressionAdditionalCourses = {}; + const ignoredCourses: ProgressionAdditionalCourses = {}; + const generateViewStructure = () => { // Example groups: Major, Minor, General, Rules Object.keys(structure).forEach((group) => { @@ -106,6 +108,8 @@ const ProgressionChecker = () => { // only consider disciplinary component courses Object.keys(subgroupStructure.courses).forEach((courseCode) => { + if (courses[courseCode]?.ignoreFromProgression) return; + const isDoubleCounted = countedCourses.includes(courseCode) && !/Core/.test(subgroup) && @@ -172,17 +176,24 @@ const ProgressionChecker = () => { .flatMap((spec) => Object.keys(spec.courses)) ); Object.keys(courses).forEach((courseCode) => { - if (!programCourseList.includes(courseCode) && courses[courseCode]?.plannedFor) { - overflowCourses[courseCode] = { - courseCode, - title: courses[courseCode].title, - UOC: courses[courseCode].UOC, - plannedFor: courses[courseCode].plannedFor as string, - isUnplanned: unplanned.includes(courseCode), - isMultiterm: courses[courseCode].isMultiterm, - isDoubleCounted: false, - isOverCounted: false - }; + const course = { + courseCode, + title: courses[courseCode].title, + UOC: courses[courseCode].UOC, + plannedFor: courses[courseCode].plannedFor as string, + isUnplanned: unplanned.includes(courseCode), + isMultiterm: courses[courseCode].isMultiterm, + isDoubleCounted: false, + isOverCounted: false + }; + if (courses[courseCode]?.plannedFor && courses[courseCode]?.ignoreFromProgression) { + ignoredCourses[courseCode] = course; + } else if ( + !programCourseList.includes(courseCode) && + courses[courseCode]?.plannedFor && + !courses[courseCode]?.ignoreFromProgression + ) { + overflowCourses[courseCode] = course; } }); return newViewLayout; @@ -280,7 +291,20 @@ const ProgressionChecker = () => { )} ))} - + + diff --git a/frontend/src/pages/TermPlanner/ContextMenu/ContextMenu.tsx b/frontend/src/pages/TermPlanner/ContextMenu/ContextMenu.tsx index f8374d61a..e9236b9b1 100644 --- a/frontend/src/pages/TermPlanner/ContextMenu/ContextMenu.tsx +++ b/frontend/src/pages/TermPlanner/ContextMenu/ContextMenu.tsx @@ -3,18 +3,25 @@ import { Item, Menu } from 'react-contexify'; import { FaRegCalendarTimes } from 'react-icons/fa'; import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router-dom'; -import { DeleteFilled, EditFilled, InfoCircleFilled } from '@ant-design/icons'; +import { + DeleteFilled, + EditFilled, + InfoCircleFilled, + PieChartFilled, + PieChartOutlined +} from '@ant-design/icons'; import EditMarkModal from 'components/EditMarkModal'; import { addTab } from 'reducers/courseTabsSlice'; -import { removeCourse, unschedule } from 'reducers/plannerSlice'; +import { removeCourse, toggleIgnoreFromProgression, unschedule } from 'reducers/plannerSlice'; import 'react-contexify/ReactContexify.css'; type Props = { code: string; plannedFor: string | null; + ignoreFromProgression: boolean; }; -const ContextMenu = ({ code, plannedFor }: Props) => { +const ContextMenu = ({ code, plannedFor, ignoreFromProgression }: Props) => { const [openModal, setOpenModal] = useState(false); const dispatch = useDispatch(); @@ -34,6 +41,9 @@ const ContextMenu = ({ code, plannedFor }: Props) => { navigate('/course-selector'); dispatch(addTab(code)); }; + const handleToggleProgression = () => { + dispatch(toggleIgnoreFromProgression(code)); + }; const iconStyle = { fontSize: '14px', @@ -54,6 +64,15 @@ const ContextMenu = ({ code, plannedFor }: Props) => { Edit mark + {ignoreFromProgression ? ( + + Acknowledge Progression + + ) : ( + + Ignore Progression + + )} View Info diff --git a/frontend/src/pages/TermPlanner/DraggableCourse/DraggableCourse.tsx b/frontend/src/pages/TermPlanner/DraggableCourse/DraggableCourse.tsx index 3380a35cb..7d64e3635 100644 --- a/frontend/src/pages/TermPlanner/DraggableCourse/DraggableCourse.tsx +++ b/frontend/src/pages/TermPlanner/DraggableCourse/DraggableCourse.tsx @@ -2,7 +2,7 @@ import React, { Suspense } from 'react'; import { useContextMenu } from 'react-contexify'; import { useSelector } from 'react-redux'; import ReactTooltip from 'react-tooltip'; -import { InfoCircleOutlined, WarningOutlined } from '@ant-design/icons'; +import { InfoCircleOutlined, PieChartOutlined, WarningOutlined } from '@ant-design/icons'; import { Badge, Typography } from 'antd'; import { useTheme } from 'styled-components'; import { Term } from 'types/planner'; @@ -35,8 +35,17 @@ const DraggableCourse = ({ code, index, term, showMultiCourseBadge }: Props) => const { Text } = Typography; // prereqs are populated in CourseDescription.jsx via course.raw_requirements - const { title, isUnlocked, plannedFor, isLegacy, isAccurate, handbookNote, supressed, mark } = - courses[code]; + const { + title, + isUnlocked, + plannedFor, + isLegacy, + isAccurate, + handbookNote, + supressed, + ignoreFromProgression, + mark + } = courses[code]; const warningMessage = courses[code].warnings; const isOffered = plannedFor @@ -118,6 +127,9 @@ const DraggableCourse = ({ code, index, term, showMultiCourseBadge }: Props) => style={{ fontSize: '16px', color: theme.warningOutlined.color }} /> ))} + {ignoreFromProgression && ( + + )} {isSmall ? ( {code} @@ -157,7 +169,11 @@ const DraggableCourse = ({ code, index, term, showMultiCourseBadge }: Props) => )} - + {/* display prereq tooltip for all courses. However, if a term is marked as complete and the course has no warning, then disable the tooltip */} {isSmall && ( @@ -177,7 +193,15 @@ const DraggableCourse = ({ code, index, term, showMultiCourseBadge }: Props) => // eslint-disable-next-line react/no-danger
)} + {/* TODO: Fix fullstops. example: "48 UoC required in all courses you have 36 This course will not be included ..." + */} {!isAccurate ? ' The course info may be inaccurate.' : ''} + {ignoreFromProgression ? ' This course will not be included in your progression.' : ''} + + )} + {ignoreFromProgression && !(!isDragDisabled && shouldHaveWarning) && ( + + This course will not be included in your progression. )} diff --git a/frontend/src/pages/TermPlanner/ImportPlannerMenu/ImportPlannerMenu.tsx b/frontend/src/pages/TermPlanner/ImportPlannerMenu/ImportPlannerMenu.tsx index fb9a8ad86..b3711f3d5 100644 --- a/frontend/src/pages/TermPlanner/ImportPlannerMenu/ImportPlannerMenu.tsx +++ b/frontend/src/pages/TermPlanner/ImportPlannerMenu/ImportPlannerMenu.tsx @@ -98,7 +98,12 @@ const ImportPlannerMenu = () => { isAccurate: course.is_accurate, isMultiterm: course.is_multiterm, supressed: false, - mark: undefined + // TODO: should actually hydrate people's mark + // and also their suppressions and ignoreFromProgression + // Maybe we don't need to worry about this as + // exporting / importing becomes redundant with accounts + ignoreFromProgression: false, + mark: undefined // TODO: WTF? }; if (plannedCourses.indexOf(course.code) === -1) { diff --git a/frontend/src/reducers/plannerSlice.ts b/frontend/src/reducers/plannerSlice.ts index 4bc5d67a0..a6603985f 100644 --- a/frontend/src/reducers/plannerSlice.ts +++ b/frontend/src/reducers/plannerSlice.ts @@ -89,6 +89,13 @@ const plannerSlice = createSlice({ state.unplanned.push(courseCode); } }, + toggleIgnoreFromProgression: (state, action: PayloadAction) => { + const code = action.payload; + + if (state.courses[code]) { + state.courses[code].ignoreFromProgression = !state.courses[code].ignoreFromProgression; + } + }, toggleWarnings: (state, action: PayloadAction) => { Object.keys(action.payload).forEach((course) => { if (state.courses[course]) { @@ -467,6 +474,7 @@ export const { addToUnplanned, setUnplannedCourseToTerm, setPlannedCourseToTerm, + toggleIgnoreFromProgression, toggleWarnings, removeCourse, removeCourses, diff --git a/frontend/src/types/planner.ts b/frontend/src/types/planner.ts index 53350d223..f3d01afc9 100644 --- a/frontend/src/types/planner.ts +++ b/frontend/src/types/planner.ts @@ -20,6 +20,7 @@ export type PlannerCourse = { handbookNote: string; isAccurate: boolean; supressed: boolean; + ignoreFromProgression: boolean; isMultiterm: boolean; mark: Mark; legacyOfferings?: CourseLegacyOfferings;