Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Schedules Builder Improvements #152

Merged
merged 6 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading