Skip to content
Open
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
22 changes: 22 additions & 0 deletions server/src/db/models/courses/courseServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
CourseDocument,
CourseObject,
FormattedGeCourses,
TechElectiveDocument,

Check warning on line 11 in server/src/db/models/courses/courseServices.ts

View workflow job for this annotation

GitHub Actions / server-setup

'TechElectiveDocument' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 11 in server/src/db/models/courses/courseServices.ts

View workflow job for this annotation

GitHub Actions / server-setup

'TechElectiveDocument' is defined but never used. Allowed unused vars must match /^_/u
TechElective,

Check warning on line 12 in server/src/db/models/courses/courseServices.ts

View workflow job for this annotation

GitHub Actions / server-setup

'TechElective' is defined but never used. Allowed unused vars must match /^_/u

Check warning on line 12 in server/src/db/models/courses/courseServices.ts

View workflow job for this annotation

GitHub Actions / server-setup

'TechElective' is defined but never used. Allowed unused vars must match /^_/u
Comment on lines +11 to +12
Copy link

Copilot AI May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the unused imports TechElectiveDocument and TechElective from this file to reduce clutter and avoid unnecessary module dependencies.

Suggested change
TechElectiveDocument,
TechElective,

Copilot uses AI. Check for mistakes.
TechElectiveData,
} from "@polylink/shared/types";
import * as geCollection from "./geCollection";
import { MongoQuery } from "../../../types/mongo";
Expand Down Expand Up @@ -517,3 +520,22 @@
throw error;
}
};

export const getTechElectivesCourses = async (
code: string
): Promise<TechElectiveData> => {
if (!code) {
throw new Error("Code is required");
}
try {
const techElectives =
await techElectiveCollection.findTechElectiveCourses(code);

return techElectives;
} catch (error) {
if (environment === "dev") {
console.error("Error fetching tech electives courses: ", error);
}
throw error;
}
};
47 changes: 47 additions & 0 deletions server/src/db/models/flowchart/flowchartHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export const TERM_MAP = {
"-1": "Skip",
1: "Fall",
2: "Winter",
3: "Spring",
5: "Fall",
6: "Winter",
7: "Spring",
9: "Fall",
10: "Winter",
11: "Spring",
13: "Fall",
14: "Winter",
15: "Spring",
17: "Fall",
18: "Winter",
19: "Spring",
21: "Fall",
22: "Winter",
23: "Spring",
25: "Fall",
26: "Winter",
27: "Spring",
29: "Fall",
30: "Winter",
31: "Spring",
};

Comment on lines +1 to +28
Copy link

Copilot AI May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The manual TERM_MAP enumeration is error-prone and may omit term indices (e.g., 4, 8, 12). Consider generating term names algorithmically (e.g., using modulo arithmetic) or explicitly documenting and handling all expected tIndex values.

Suggested change
export const TERM_MAP = {
"-1": "Skip",
1: "Fall",
2: "Winter",
3: "Spring",
5: "Fall",
6: "Winter",
7: "Spring",
9: "Fall",
10: "Winter",
11: "Spring",
13: "Fall",
14: "Winter",
15: "Spring",
17: "Fall",
18: "Winter",
19: "Spring",
21: "Fall",
22: "Winter",
23: "Spring",
25: "Fall",
26: "Winter",
27: "Spring",
29: "Fall",
30: "Winter",
31: "Spring",
};
export const getTermName = (tIndex: number): string => {
if (tIndex === -1) return "Skip";
const terms = ["Fall", "Winter", "Spring"];
const adjustedIndex = (tIndex - 1) % 3; // Adjust to 0-based index for modulo
return terms[adjustedIndex];
};
export const TERM_MAP = new Proxy({}, {
get: (_, prop: string) => {
const tIndex = parseInt(prop, 10);
if (isNaN(tIndex)) {
throw new Error(`Invalid term index: ${prop}`);
}
return getTermName(tIndex);
}
});

Copilot uses AI. Check for mistakes.
export const getCatalogYear = (shortCatalogYear?: string): string => {
const catalogYear = shortCatalogYear?.split(".")[0];
if (!catalogYear) {
return "2022-2026"; // Default catalog year
}

switch (catalogYear) {
case "19-20":
return "2019-2020";
case "20-21":
return "2020-2021";
case "21-22":
return "2021-2022";
case "22-26":
return "2022-2026";
default:
return "2022-2026"; // Default catalog year
}
};
38 changes: 36 additions & 2 deletions server/src/db/models/flowchart/flowchartServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
FetchedFlowchartObject,
FetchFlowchartResponse,
FlowchartData,
FlowchartDocument,
} from "@polylink/shared/types";
import { BulkWriteResult } from "mongodb";
import { environment } from "../../../index";
Expand Down Expand Up @@ -270,15 +271,18 @@ export const isPrimaryFlowchart = async (

export const fetchPrimaryFlowchart = async (
userId: string
): Promise<FlowchartData> => {
): Promise<FlowchartData | null> => {
try {
const primaryFlowchart = await flowchartModel.fetchPrimaryFlowchart(userId);
if (!primaryFlowchart) {
// Fetch any flowchart
const flowchartList = await fetchAllFlowcharts(userId);
const anyFlowchart = flowchartList[0];
if (!anyFlowchart) {
throw new Error("No flowcharts found");
if (environment === "dev") {
console.error("No flowcharts found");
}
return null;
}
const flowchart = await fetchFlowchart(anyFlowchart.flowchartId, userId);
return flowchart.flowchartData;
Expand All @@ -290,3 +294,33 @@ export const fetchPrimaryFlowchart = async (
);
}
};

export const fetchPrimaryFlowchartDoc = async (
userId: string
): Promise<FlowchartDocument | null> => {
try {
const primaryFlowchart = await flowchartModel.fetchPrimaryFlowchart(userId);
if (!primaryFlowchart) {
// Fetch any flowchart
const flowchartList = await fetchAllFlowcharts(userId);
const anyFlowchart = flowchartList[0];
if (!anyFlowchart) {
if (environment === "dev") {
console.error("No flowcharts found");
}
return null;
}
const flowchart = await flowchartModel.fetchFlowchart(
anyFlowchart.flowchartId,
userId
);
return flowchart;
}
return primaryFlowchart;
} catch (error) {
if (environment === "dev") {
console.error("Error fetching primary flowchart:", error);
}
return null;
}
};
6 changes: 5 additions & 1 deletion server/src/db/models/section/sectionServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,5 +463,9 @@ export async function getSectionsByCourseIds(

const result = await getSectionsByFilter(filter, userId, 0, 25);

return result.sections;
return result.sections.sort((a, b) => {
const aRating = a.instructorsWithRatings?.[0]?.overallRating || 0;
const bRating = b.instructorsWithRatings?.[0]?.overallRating || 0;
return bRating - aRating;
});
}
164 changes: 162 additions & 2 deletions server/src/helpers/assistants/scheduleBuilder/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ import {
SelectedSection,
ProfessorRatingDocument,
SectionAddedOrRemoved,
AcademicPlan,
} from "@polylink/shared/types";
import { fetchPrimaryFlowchart } from "../../../db/models/flowchart/flowchartServices";
import {
fetchPrimaryFlowchart,
fetchPrimaryFlowchartDoc,
} from "../../../db/models/flowchart/flowchartServices";
import {
getSectionsByCourseIds,
getSectionsByIds,
Expand All @@ -30,6 +34,18 @@ import {
SUGGESTED_SECTIONS_PER_COURSE,
POTENTIAL_SECTIONS_PER_COURSE,
} from "./const";
import {
getCatalogYear,
TERM_MAP,
} from "../../../db/models/flowchart/flowchartHelpers";
import {
getGeCourses,
getGeSubjects,
} from "../../../db/models/courses/courseServices";
import {
getGeAreas,
getTechElectivesCourses,
} from "../../../db/models/courses/courseServices";

export const getAlternateSections = async ({
userId,
Expand Down Expand Up @@ -88,7 +104,12 @@ export const getUserNextEligibleSections = async ({
potentialSectionsClassNums: number[];
}> => {
const flowchart = await fetchPrimaryFlowchart(userId);

if (!flowchart) {
return {
suggestedSections: [],
potentialSectionsClassNums: [],
};
}
// Sort terms by tIndex
const terms = (flowchart.termData || []).sort(
(a: Term, b: Term) => a.tIndex - b.tIndex
Expand Down Expand Up @@ -596,3 +617,142 @@ export async function buildSectionSummaries(
});
return summaries;
}

Copy link

Copilot AI May 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Add a JSDoc comment or block comment above getFlowchartSummary to describe its purpose, parameters, return type (AcademicPlan | null), and side effects for better maintainability.

Suggested change
/**
* Retrieves a summary of the academic plan (flowchart) for a given user.
*
* @param {string} userId - The unique identifier of the user whose flowchart is being retrieved.
* @returns {Promise<AcademicPlan | null>} A promise that resolves to the academic plan summary
* or null if no flowchart is found for the user.
*
* @remarks
* This function fetches the primary flowchart document for the user and processes its data
* to generate a textual summary of the academic plan. It includes details such as the major,
* concentration, and term-specific information. If the user does not have a flowchart, the
* function returns null.
*/

Copilot uses AI. Check for mistakes.
export async function getFlowchartSummary(
userId: string
): Promise<AcademicPlan | null> {
const flowchart = await fetchPrimaryFlowchartDoc(userId);

if (!flowchart) {
return null;
}

const startYear =
flowchart.flowchartData.startYear === "incoming-transfer"
? 2024
: parseInt(flowchart.flowchartData.startYear);

let summary = `Academic Plan Summary for ${flowchart.flowInfo.majorName} with ${flowchart.flowInfo.concName}concentration\n\n`;

// Process each term
const termSummary = flowchart.flowchartData.termData.map((term) => {
const termNumber = term.tIndex;
const termName = TERM_MAP[termNumber as keyof typeof TERM_MAP];
const baseYearOffset = Math.floor((termNumber - 1) / 4);
const yearOffset =
termName === "Spring" ? baseYearOffset + 1 : baseYearOffset;
const year = startYear + yearOffset;
const termLabel = `${termName} ${year}`;

// Get completed and remaining courses
const completedCourses = term.courses.filter((course) => course.completed);
const remainingCourses = term.courses.filter((course) => !course.completed);

// If all courses are completed, just show a brief summary
if (remainingCourses.length === 0) {
return `\n${termLabel}: Completed`;
}

let termInfo = `\n${termLabel}:\n`;

if (completedCourses.length > 0) {
termInfo += "Completed Courses:\n";
completedCourses.forEach((course) => {
if (course.id) {
termInfo += `- ${course.id}: ${course.displayName} (${course.units} units)\n`;
} else if (course.customId) {
termInfo += `- ${course.customId}: ${course.customDisplayName || course.customId} (${course.customUnits} units)\n`;
}
});
}

if (remainingCourses.length > 0) {
termInfo += "\nRemaining Courses:\n";
remainingCourses.forEach((course) => {
if (course.id) {
termInfo += `- ${course.id}: ${course.displayName} (${course.units} units)\n`;
} else if (course.customId) {
termInfo += `- ${course.customId}: ${course.customDisplayName || course.customId} (${course.customUnits} units)\n`;
}
});
}

return termInfo;
});

summary += termSummary.join("\n");

// Add remaining requirements summary
const remainingCourses = flowchart.flowchartData.termData.flatMap((term) =>
term.courses.filter((course) => !course.completed)
);

const remainingRequired = remainingCourses.filter((course) => course.id);
const remainingElectives = remainingCourses.filter(
(course) => course.customId
);
const completedCoursesIds = flowchart.flowchartData.termData.flatMap((term) =>
term.courses
.filter((course) => course.completed)
.map((course) => course.id)
.filter((id): id is string => !!id)
);
const catalogYear = getCatalogYear(flowchart.flowchartData.name);
const geAreas = await getGeAreas(catalogYear, completedCoursesIds);
const geAreasLeft = geAreas.filter((area) => !area.completed);
const techElectives = await getTechElectivesCourses(flowchart.flowInfo.code);
const techElectivesLeft = techElectives.techElectives
.map((techElective) => ({
...techElective,
courses: techElective.courses.filter(
(course) => !completedCoursesIds.includes(course)
),
}))
.filter((techElective) => techElective.courses.length > 0);

// Get courses for each GE area
const geAreasWithCourses = await Promise.all(
geAreasLeft.map(async (area) => {
const subjects = await getGeSubjects(
area.category,
catalogYear,
completedCoursesIds
);
const courses = await Promise.all(
subjects.map((subject) =>
getGeCourses(subject.subject, area.category, catalogYear)
)
);
return {
category: area.category,
courses: courses
.flatMap((courseGroup) =>
Object.values(
courseGroup as Record<string, Record<string, unknown[]>>
).flatMap((subjectCourses) => Object.values(subjectCourses).flat())
)
.map((course) => (course as { courseId: string }).courseId),
};
})
);

summary += "\n\nRemaining Requirements Summary:\n";
summary += `Total Remaining Courses: ${remainingCourses.length}\n`;
summary += `Required Courses Remaining: ${remainingRequired.length}\n`;
summary += `Elective Courses Remaining: ${remainingElectives.length}\n`;
summary += `GE Areas Remaining: ${geAreasLeft.map((area) => area.category).join(", ")}\n`;
summary += `Tech Electives Remaining: ${techElectivesLeft.map((elective) => elective.name).join(", ")}\n`;

return {
completedCoursesIds,
requiredCoursesLeft: remainingRequired
.filter((course) => course.id)
.map((course) => course.id as string),
techElectives,
GEAreasLeft: geAreasWithCourses.map((area) => ({
category: area.category,
courses: area.courses,
})),
summary,
};
}
Loading
Loading