diff --git a/frontend/src/app/cache.ts b/frontend/src/app/cache.ts index 34c8cd1..f862aa9 100644 --- a/frontend/src/app/cache.ts +++ b/frontend/src/app/cache.ts @@ -119,6 +119,11 @@ export const cacheSlice = createSlice({ const fuseIndex : FuseIndex<{ instructor: string }> = Fuse.parseIndex(state.fuseIndex); const fuse = new Fuse(state.allInstructors, {}, fuseIndex); state.selectedInstructors = fuse.search(search).map(({item}) => item); + }, + updateUnits: (state, action: PayloadAction<{units: string, courseID: string}>) => { + const units = action.payload.units + const courseID = action.payload.courseID + state.courseResults[courseID].manualUnits = units } }, extraReducers: (builder) => { diff --git a/frontend/src/app/fce.ts b/frontend/src/app/fce.ts index 3c6b0cc..2f72a2a 100644 --- a/frontend/src/app/fce.ts +++ b/frontend/src/app/fce.ts @@ -1,5 +1,5 @@ -import { FCE } from "./types"; -import { compareSessions, roundTo, sessionToShortString, responseRateZero } from "./utils"; +import { Course, FCE } from "./types"; +import { compareSessions, roundTo, sessionToShortString, responseRateZero, parseUnits, isValidUnits } from "./utils"; export const FCE_RATINGS = [ "Interest in student learning", @@ -14,7 +14,7 @@ export const FCE_RATINGS = [ ]; export const aggregateFCEs = (rawFces: FCE[]) => { - const fces = rawFces.filter((fce) => !responseRateZero(fce)) + const fces = rawFces.filter((fce) => !responseRateZero(fce)); const fcesCounted = fces.length; const semesters = new Set(); @@ -55,7 +55,7 @@ export interface AggregateFCEsOptions { type: string; courses: string[]; instructors: string[]; - } + }; numSemesters: number; } @@ -75,14 +75,14 @@ export const filterFCEs = (fces: FCE[], options: AggregateFCEsOptions) => { // Filter by courses if (options.filters.type === "courses" && options.filters.courses) { result = result.filter(({ courseID }) => - options.filters.courses.includes(courseID) + options.filters.courses.includes(courseID), ); } // Filter by instructors if (options.filters.type === "instructors" && options.filters.instructors) { result = result.filter(({ instructor }) => - options.filters.instructors.includes(instructor) + options.filters.instructors.includes(instructor), ); } @@ -91,9 +91,11 @@ export const filterFCEs = (fces: FCE[], options: AggregateFCEsOptions) => { export const aggregateCourses = ( data: { courseID: string; fces: FCE[] }[], + courses: Course[], options: AggregateFCEsOptions ) => { const messages = []; + const unitsMessage = []; const coursesWithoutFCEs = data .filter(({ fces }) => fces === null) @@ -103,7 +105,7 @@ export const aggregateCourses = ( messages.push( `There are courses without any FCE data (${coursesWithoutFCEs.join( ", " - )}).` + )}). FCE data is estimated using the number of units.` ); } @@ -133,10 +135,27 @@ export const aggregateCourses = ( workload += aggregateFCE.aggregateData.workload; } + for (const courseID of coursesWithoutFCEs) { + const findCourse = courses.filter((course) => course.courseID === courseID); + if (findCourse.length > 0) workload += parseUnits(findCourse[0].units); + } + + const totalUnits = courses.reduce((acc, curr) => acc + parseUnits(curr.units) + parseUnits(curr.manualUnits), 0); + const varUnits = courses.filter((course) => !isValidUnits(course.units)); + if (varUnits.length > 0) { + unitsMessage.push( + `There are courses with variable units (${varUnits + .map((course) => course.courseID) + .join(", ")}). Input the number of units manually above.` + ); + } + return { aggregatedFCEs, workload, - message: messages.join(" "), + totalUnits, + fceMessage: messages.join(" "), + unitsMessage: unitsMessage.join(" "), }; }; diff --git a/frontend/src/app/types.ts b/frontend/src/app/types.ts index 1c70704..0c2f40e 100644 --- a/frontend/src/app/types.ts +++ b/frontend/src/app/types.ts @@ -28,6 +28,7 @@ export interface Course { desc: string; schedules?: Schedule[]; units: string; + manualUnits?: string; fces?: FCE[]; } diff --git a/frontend/src/app/utils.tsx b/frontend/src/app/utils.tsx index 1734410..84bc190 100644 --- a/frontend/src/app/utils.tsx +++ b/frontend/src/app/utils.tsx @@ -219,3 +219,14 @@ export function responseRateZero(fce: FCE): boolean { // Just trying to catch the possible reasonable edge cases return ["0", "0.0", "0.00", "0%", "0.0%", "0.00%"].includes(fce.responseRate); } + +export function isValidUnits(units: string): boolean { + const re = /^\d+(\.\d+)?$/; + return re.test(units); +} + +export function parseUnits(units: string) : number { + if (isValidUnits(units)) { + return parseFloat(units); + } return 0.0; +} diff --git a/frontend/src/components/ScheduleData.tsx b/frontend/src/components/ScheduleData.tsx index 8434f80..ee551c2 100644 --- a/frontend/src/components/ScheduleData.tsx +++ b/frontend/src/components/ScheduleData.tsx @@ -1,12 +1,13 @@ import React from "react"; import { useAppDispatch, useAppSelector } from "../app/hooks"; import { aggregateCourses, AggregatedFCEs } from "../app/fce"; -import { displayUnits, roundTo } from "../app/utils"; +import { displayUnits, isValidUnits, roundTo } from "../app/utils"; import { selectCourseResults, selectFCEResultsForCourses } from "../app/cache"; import { selectSelectedCoursesInActiveSchedule, userSchedulesSlice, } from "../app/userSchedules"; +import { cacheSlice } from "../app/cache"; import { FlushedButton } from "./Buttons"; import { uiSlice } from "../app/ui"; import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/20/solid"; @@ -46,15 +47,20 @@ const ScheduleData = ({ scheduled }: ScheduleDataProps) => { selected.includes(courseID) ); - const aggregatedData = aggregateCourses(scheduledFCEs, options); + const selectedResults = scheduledResults.filter(({ courseID }) => + selected.includes(courseID) + ); + + const aggregatedData = aggregateCourses(scheduledFCEs, scheduledResults, options); const aggregatedDataByCourseID: { [courseID: string]: AggregatedFCEs } = {}; for (const row of aggregatedData.aggregatedFCEs) { if (row.aggregateData !== null) aggregatedDataByCourseID[row.courseID] = row.aggregateData; } - const aggregatedSelectedData = aggregateCourses(selectedFCEs, options); - const message = aggregatedSelectedData.message; + const aggregatedSelectedData = aggregateCourses(selectedFCEs, selectedResults, options); + const fceMessage = aggregatedSelectedData.fceMessage; + const unitsMessage = aggregatedSelectedData.unitsMessage; const selectCourse = (value: boolean, courseID: string) => { if (value) @@ -74,12 +80,12 @@ const ScheduleData = ({ scheduled }: ScheduleDataProps) => {