From e38921a741bf4fd24259a87bd92194953adb7bbf Mon Sep 17 00:00:00 2001 From: D0dii Date: Mon, 1 Sep 2025 18:51:43 +0200 Subject: [PATCH 1/2] feat: skip lectures, force upload courses --- frontend/src/components/algo-dialog.tsx | 189 +++++++++++++++---- frontend/src/lib/utils/schedule-algorithm.ts | 22 ++- 2 files changed, 168 insertions(+), 43 deletions(-) diff --git a/frontend/src/components/algo-dialog.tsx b/frontend/src/components/algo-dialog.tsx index db4c0069..f7646989 100644 --- a/frontend/src/components/algo-dialog.tsx +++ b/frontend/src/components/algo-dialog.tsx @@ -65,6 +65,8 @@ export function AlgorithmDialog({ }) { const [dialogOpen, setDialogOpen] = useState(false); const [baseOnRating, setBaseOnRating] = useState(false); + const [includeLectures, setIncludeLectures] = useState(true); + const [forceFillMissingCourses, setForceFillMissingCourses] = useState(false); const [preferences, setPreferences] = useState< Record >({ @@ -153,46 +155,135 @@ export function AlgorithmDialog({ weekPreferences, availableCourses, baseOnRating, + includeLectures, ); setScheduleResult(result); setIsGenerating(false); }; const handleAddToUserPlan = () => { - if (scheduleResult?.schedule !== undefined) { - // najpierw odznacz wszystkie grupy - const updatedPlan = { - ...plan, - courses: plan.courses.map((course) => ({ - ...course, - groups: course.groups.map((group) => ({ - ...group, - isChecked: false, - })), - })), - synced: false, - }; + if (scheduleResult?.schedule == null) { + return; + } - // następnie zaznacz grupy z wygenerowanego planu - const finalPlan = { - ...updatedPlan, - courses: updatedPlan.courses.map((course) => ({ - ...course, - groups: course.groups.map((group) => { - const isInSchedule = - scheduleResult.schedule?.some( - (scheduleGroup) => scheduleGroup.groupId === group.groupId, - ) ?? false; - return isInSchedule ? { ...group, isChecked: true } : group; - }), - })), + // Build set of selected groupIds from generated schedule + const selectedGroupIds = new Set( + scheduleResult.schedule.map((g) => g.groupId), + ); + + // Optionally force fill remaining courses (choose one group even if outside preferences) + if (forceFillMissingCourses) { + // Helper to convert HH:MM into minutes + const toMinutes = (t: string) => { + const [h, m] = t.split(":").map(Number); + return h * 60 + m; + }; + // Map of all groups by id for quick lookup (use availableCourses as source) + const groupById = new Map(); + for (const c of availableCourses) { + for (const g of c.groups) { + groupById.set(g.groupId, g); + } + } + // Currently selected group objects (from schedule and any newly added) + const selectedGroupObjects: ExtendedGroup[] = scheduleResult.schedule.map( + (g) => g, + ); + const hasConflict = (candidate: ExtendedGroup) => { + return selectedGroupObjects.some((existing) => { + if (existing.day !== candidate.day) { + return false; + } + const s1 = toMinutes(existing.startTime); + const end1 = toMinutes(existing.endTime); + const s2 = toMinutes(candidate.startTime); + const end2 = toMinutes(candidate.endTime); + return !(end1 <= s2 || end2 <= s1); + }); }; - plan.setPlan(finalPlan); + for (const course of availableCourses) { + // Gather selected types for this course + const selectedTypesForCourse = new Set( + course.groups + .filter((g) => selectedGroupIds.has(g.groupId)) + .map((g) => g.courseType), + ); + // All types available for this course (respect includeLectures) + const allTypesForCourse = [ + ...new Set( + course.groups + .filter((g) => (includeLectures ? true : g.courseType !== "W")) + .map((g) => g.courseType), + ), + ]; + // For each missing type, try to pick a group. + for (const type of allTypesForCourse) { + if (selectedTypesForCourse.has(type)) { + continue; + } - setDialogOpen(false); - toast.success("Plan został ustawiony poprawnie."); + const candidates = course.groups + .filter((g) => g.courseType === type) + .filter((g) => (includeLectures ? true : g.courseType !== "W")); + if (candidates.length === 0) { + continue; + } + + // Sort candidates: rating desc, then earlier start time + const sorted = [...candidates].sort((a, b) => { + const ratingA = + typeof a.averageRating === "string" + ? Number.parseFloat(a.averageRating) + : a.averageRating; + const ratingB = + typeof b.averageRating === "string" + ? Number.parseFloat(b.averageRating) + : b.averageRating; + if (ratingB !== ratingA) { + return ratingB - ratingA; + } + return a.startTime.localeCompare(b.startTime); + }); + + let chosen: ExtendedGroup | null = null; + for (const cand of sorted) { + if (!hasConflict(cand)) { + chosen = cand; + break; + } + } + // If all conflict, just take the highest rated (first) to ensure coverage. + if (chosen == null) { + chosen = sorted[0]; + } + selectedGroupIds.add(chosen.groupId); + selectedGroupObjects.push(chosen); + selectedTypesForCourse.add(type); + } + } } + + // Uncheck all, then check selected ones + const finalPlan = { + ...plan, + courses: plan.courses.map((course) => ({ + ...course, + groups: course.groups.map((group) => ({ + ...group, + isChecked: selectedGroupIds.has(group.groupId), + })), + })), + synced: false, + }; + + plan.setPlan(finalPlan); + setDialogOpen(false); + toast.success( + forceFillMissingCourses + ? "Plan ustawiony (uzupełniono brakujące kursy)." + : "Plan został ustawiony poprawnie.", + ); }; return ( @@ -304,12 +395,21 @@ export function AlgorithmDialog({
{scheduleResult === null ? (
-
- -

Dopasuj na podstawie ocen

+
+
+ +

Dopasuj na podstawie ocen

+
+
+ +

Uwzględnij wykłady

+
+
+
+ +

+ Uzupełnij brakujące kursy (dodaj grupy spoza preferencji) +

+
+ +
)}
diff --git a/frontend/src/lib/utils/schedule-algorithm.ts b/frontend/src/lib/utils/schedule-algorithm.ts index f125e077..bad2a64e 100644 --- a/frontend/src/lib/utils/schedule-algorithm.ts +++ b/frontend/src/lib/utils/schedule-algorithm.ts @@ -231,17 +231,27 @@ export const createScheduleBasedOnCoursesAndPreferences = ( userPreferences: WeekPreferences, availableCourses: ExtendedCourse[], basedOnRating = false, + includeLectures = true, ) => { - const coursesWithGroups = availableCourses.filter( + // Optionally filter out lecture groups (courseType === 'W') if user chose to exclude lectures. + const prefilteredCourses = includeLectures + ? availableCourses + : availableCourses.map((course) => ({ + ...course, + groups: course.groups.filter((g) => g.courseType !== "W"), + })); + + // Only consider courses that still have at least one group after filtering. + const coursesWithGroups = prefilteredCourses.filter( (course) => course.groups.length > 0, ); - if (coursesWithGroups.length === 0) { return { success: false, message: "Brak dostępnych kursów z grupami", userPreferences, - availableCourses, + availableCourses, // keep original list for reference + includeLectures, }; } @@ -259,11 +269,14 @@ export const createScheduleBasedOnCoursesAndPreferences = ( message: "Nie udało się wygenerować planu - sprawdź dostępność grup", userPreferences, availableCourses, + includeLectures, }; } + // Coverage score is based on courses actually considered (after optional lecture filtering) + const consideredCoursesCount = coursesWithGroups.length; const score = Math.round( - (bestSchedule.groups.length / availableCourses.length) * 100, + (bestSchedule.groups.length / (consideredCoursesCount || 1)) * 100, ); bestSchedule.score = score; @@ -279,5 +292,6 @@ export const createScheduleBasedOnCoursesAndPreferences = ( message, userPreferences, availableCourses, + includeLectures, }; }; From cc76c8725d145540806a71b3b19b25c00010aa84 Mon Sep 17 00:00:00 2001 From: D0dii Date: Mon, 1 Sep 2025 18:57:55 +0200 Subject: [PATCH 2/2] fix: lint --- frontend/src/components/algo-dialog.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/components/algo-dialog.tsx b/frontend/src/components/algo-dialog.tsx index f7646989..a075c8f9 100644 --- a/frontend/src/components/algo-dialog.tsx +++ b/frontend/src/components/algo-dialog.tsx @@ -254,9 +254,7 @@ export function AlgorithmDialog({ } } // If all conflict, just take the highest rated (first) to ensure coverage. - if (chosen == null) { - chosen = sorted[0]; - } + chosen ??= sorted[0]; selectedGroupIds.add(chosen.groupId); selectedGroupObjects.push(chosen); selectedTypesForCourse.add(type);