Skip to content

Commit

Permalink
Schedules Builder Improvements (#152)
Browse files Browse the repository at this point in the history
* Add units to workload when there is no FCE data

* Notify about VAR units

* Plus circle for new schedule

* Units message cosmetics

* MVP for manual units

* Function for checking if a set of units is valid

---------

Co-authored-by: “xavilien” <“[email protected]”>
  • Loading branch information
Xavilien and “xavilien” authored May 13, 2024
1 parent 0d62c56 commit 68bcded
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 21 deletions.
5 changes: 5 additions & 0 deletions frontend/src/app/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
35 changes: 27 additions & 8 deletions frontend/src/app/fce.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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();
Expand Down Expand Up @@ -55,7 +55,7 @@ export interface AggregateFCEsOptions {
type: string;
courses: string[];
instructors: string[];
}
};
numSemesters: number;
}

Expand All @@ -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),
);
}

Expand All @@ -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)
Expand All @@ -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.`
);
}

Expand Down Expand Up @@ -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(" "),
};
};

Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface Course {
desc: string;
schedules?: Schedule[];
units: string;
manualUnits?: string;
fces?: FCE[];
}

Expand Down
11 changes: 11 additions & 0 deletions frontend/src/app/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
47 changes: 37 additions & 10 deletions frontend/src/components/ScheduleData.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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)
Expand All @@ -74,12 +80,12 @@ const ScheduleData = ({ scheduled }: ScheduleDataProps) => {
<div className="text-a-600 text-lg">
Total Workload{" "}
<span className="ml-4">
{scheduledResults.reduce((acc, curr) => acc + parseFloat(curr.units), 0)} units,
{message === "" ? "" : "*"}
{aggregatedSelectedData.totalUnits} units
{unitsMessage === "" ? "" : <sup>+</sup>},
</span>
<span className="ml-4">
{roundTo(aggregatedSelectedData.workload, 2)} hrs/week
{message === "" ? "" : "*"}
{fceMessage === "" ? "" : "*"}
</span>
<button className="absolute right-3 z-40 md:right-2">
<FlushedButton
Expand Down Expand Up @@ -126,7 +132,23 @@ const ScheduleData = ({ scheduled }: ScheduleDataProps) => {
</td>
<td>{result.courseID}</td>
<td className="whitespace-nowrap pr-4">{result.name}</td>
<td>{displayUnits(result.units)}</td>
<td>
{
!isValidUnits(result.units) ?
<input
className="bg-white w-20"
value={result.manualUnits !== undefined ? displayUnits(result.manualUnits) : displayUnits(result.units)}
onChange={(e) =>
dispatch(cacheSlice.actions.updateUnits({
courseID: result.courseID,
units: e.target.value,
}))
}
placeholder="Units"
/> :
displayUnits(result.units)
}
</td>
<td>
{result.courseID in aggregatedDataByCourseID
? aggregatedDataByCourseID[result.courseID].workload
Expand All @@ -138,8 +160,13 @@ const ScheduleData = ({ scheduled }: ScheduleDataProps) => {
</tbody>
</table>
</div>)}
<div className="text-gray-500 mt-2 text-sm">
{message === "" ? "" : `*${message}`}
<div className="text-gray-500 mt-3 text-sm">
{unitsMessage === "" ? "" :
<div>
<sup>+</sup>
{unitsMessage}
</div>}
{fceMessage === "" ? "" : `*${fceMessage}`}
</div>
</>
);
Expand Down
8 changes: 5 additions & 3 deletions frontend/src/components/ScheduleSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";
import { ClipboardIcon, ShareIcon } from "@heroicons/react/24/solid";
import { useAppDispatch, useAppSelector } from "../app/hooks";
import { FlushedButton } from "./Buttons";
import { XMarkIcon } from "@heroicons/react/24/outline";
import { XMarkIcon, PlusCircleIcon } from "@heroicons/react/24/outline";
import { userSchedulesSlice } from "../app/userSchedules";
import { showToast } from "./Toast";

Expand Down Expand Up @@ -92,14 +92,16 @@ const ScheduleSelector = () => {

return (
<div>
<div className="mb-2 flex items-baseline gap-3">
<div className="mb-2 flex gap-1">
<div className="text-lg">Schedules</div>
<FlushedButton
onClick={() => {
dispatch(userSchedulesSlice.actions.createEmptySchedule());
}}
>
Create New
<PlusCircleIcon
className="h-5 w-5"
/>
</FlushedButton>
</div>
<div>
Expand Down

0 comments on commit 68bcded

Please sign in to comment.