From ce59bc4d4da9a3065d691975bae3f648a7b5a87c Mon Sep 17 00:00:00 2001 From: lenabinder1001 Date: Tue, 7 Jul 2026 17:44:43 +0200 Subject: [PATCH] revert changes for new ui --- frontend/src/App.tsx | 4 - .../src/features/layout/components/TopBar.tsx | 11 - .../features/newui/components/CategoryTag.tsx | 29 --- .../components/CourseCategoryDropdown.tsx | 161 -------------- .../newui/components/CoursePicker.tsx | 139 ------------ .../components/RegulationProgressCard.tsx | 116 ---------- .../newui/components/SemesterColumn.tsx | 126 ----------- .../newui/components/StudyPlanPage.tsx | 192 ----------------- .../features/newui/components/Timetable.tsx | 168 --------------- .../newui/hooks/useAutoHideScrollbar.ts | 37 ---- .../features/newui/hooks/useBetaPlanner.ts | 204 ------------------ .../newui/hooks/useStudyPlanOverview.ts | 70 ------ frontend/src/features/newui/index.ts | 1 - frontend/src/features/newui/types.ts | 11 - .../newui/utils/categoryAssignment.ts | 37 ---- .../features/newui/utils/plannerCategory.ts | 20 -- .../src/features/newui/utils/plannerFormat.ts | 28 --- .../newui/utils/regulationProgress.ts | 7 - .../features/newui/utils/studyPlanOverview.ts | 53 ----- .../features/newui/utils/timetableFilter.ts | 20 -- frontend/src/features/routes.ts | 3 - frontend/src/index.css | 15 -- .../tests/newui/categoryAssignment.test.ts | 52 ----- frontend/tests/newui/plannerBeta.test.ts | 53 ----- .../tests/newui/regulationProgress.test.ts | 13 -- .../tests/newui/studyPlanOverview.test.ts | 71 ------ 26 files changed, 1641 deletions(-) delete mode 100644 frontend/src/features/newui/components/CategoryTag.tsx delete mode 100644 frontend/src/features/newui/components/CourseCategoryDropdown.tsx delete mode 100644 frontend/src/features/newui/components/CoursePicker.tsx delete mode 100644 frontend/src/features/newui/components/RegulationProgressCard.tsx delete mode 100644 frontend/src/features/newui/components/SemesterColumn.tsx delete mode 100644 frontend/src/features/newui/components/StudyPlanPage.tsx delete mode 100644 frontend/src/features/newui/components/Timetable.tsx delete mode 100644 frontend/src/features/newui/hooks/useAutoHideScrollbar.ts delete mode 100644 frontend/src/features/newui/hooks/useBetaPlanner.ts delete mode 100644 frontend/src/features/newui/hooks/useStudyPlanOverview.ts delete mode 100644 frontend/src/features/newui/index.ts delete mode 100644 frontend/src/features/newui/types.ts delete mode 100644 frontend/src/features/newui/utils/categoryAssignment.ts delete mode 100644 frontend/src/features/newui/utils/plannerCategory.ts delete mode 100644 frontend/src/features/newui/utils/plannerFormat.ts delete mode 100644 frontend/src/features/newui/utils/regulationProgress.ts delete mode 100644 frontend/src/features/newui/utils/studyPlanOverview.ts delete mode 100644 frontend/src/features/newui/utils/timetableFilter.ts delete mode 100644 frontend/tests/newui/categoryAssignment.test.ts delete mode 100644 frontend/tests/newui/plannerBeta.test.ts delete mode 100644 frontend/tests/newui/regulationProgress.test.ts delete mode 100644 frontend/tests/newui/studyPlanOverview.test.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7eefcd8..a693eeb 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -33,9 +33,6 @@ const RequestLogPage = lazy(() => default: module.RequestLogPage, })), ) -const StudyPlanBeta = lazy(() => - import('./features/newui').then((module) => ({ default: module.StudyPlanPage })), -) const TestLayout = lazy(() => import('./features/test').then((module) => ({ default: module.TestLayout })), ) @@ -109,7 +106,6 @@ function App() { element={} /> - } /> }> } /> }> diff --git a/frontend/src/features/layout/components/TopBar.tsx b/frontend/src/features/layout/components/TopBar.tsx index 195318d..a461b13 100644 --- a/frontend/src/features/layout/components/TopBar.tsx +++ b/frontend/src/features/layout/components/TopBar.tsx @@ -40,15 +40,6 @@ export function TopBar() { ) - const betaButton = ( - - Test UI - - ) - const themeToggleButton = ( - - {isOpen && menuPosition - ? createPortal( -
    - {ALL_CATEGORIES.map((cat) => { - const isAvailable = selectable.includes(cat) - return ( -
  • - -
  • - ) - })} -
, - document.body, - ) - : null} - - ) -} diff --git a/frontend/src/features/newui/components/CoursePicker.tsx b/frontend/src/features/newui/components/CoursePicker.tsx deleted file mode 100644 index 9cf8c5f..0000000 --- a/frontend/src/features/newui/components/CoursePicker.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import type { Course } from '../../courses' -import { cleanCourseTitle } from '../../courses' -import { - getTutorialSlotOptions, - resolveVisibleTutorialSlotId, -} from '../../planner/utils/plannerSlotSelection.ts' -import type { CategoryControl } from '../hooks/useBetaPlanner' -import { formatTutorialSlotLabel } from '../utils/plannerFormat' -import { CourseCategoryDropdown } from './CourseCategoryDropdown' - -interface CoursePickerProps { - favoriteCourses: Course[] - plannedCourseIds: string[] - hiddenSlotIds: string[] - isLoading: boolean - categoryControl: CategoryControl - onAdd: (courseId: string) => void - onRemove: (courseId: string) => void - onSelectTutorial: (courseId: string, slotId: string) => void -} - -function courseMeta(course: Course): string { - return [course.number, course.ects !== null ? `${course.ects} ECTS` : null].filter(Boolean).join(' · ') -} - -export function CoursePicker({ - favoriteCourses, - plannedCourseIds, - hiddenSlotIds, - isLoading, - categoryControl, - onAdd, - onRemove, - onSelectTutorial, -}: CoursePickerProps) { - // Planned courses float to the top; stable sort keeps the alphabetical order - // within each group. - const orderedCourses = [...favoriteCourses].sort((left, right) => { - const leftPlanned = plannedCourseIds.includes(left.id) - const rightPlanned = plannedCourseIds.includes(right.id) - if (leftPlanned === rightPlanned) { - return 0 - } - return leftPlanned ? -1 : 1 - }) - - return ( -
-

- Kurse & Auswahl -

-

- Deine Favoriten — einplanen und bei mehreren Tutorien den Termin wählen. -

- - {isLoading && favoriteCourses.length === 0 ? ( -
Lädt…
- ) : favoriteCourses.length === 0 ? ( -
- Noch keine Favoriten. Markiere Kurse im Katalog als Favorit — sie erscheinen hier. -
- ) : ( -
- {orderedCourses.map((course) => { - const isPlanned = plannedCourseIds.includes(course.id) - const tutorialOptions = getTutorialSlotOptions(course) - const visibleSlotId = resolveVisibleTutorialSlotId(tutorialOptions, hiddenSlotIds) - const selectableCats = categoryControl.selectableOf(course) - const category = categoryControl.categoryOf(course) - return ( -
-
-
-
- {cleanCourseTitle(course.title, course.number)} -
-
{courseMeta(course)}
-
- -
- - {selectableCats.length > 0 ? ( -
- Kategorie - categoryControl.change(course, cat)} - /> -
- ) : null} - - {isPlanned && tutorialOptions.length > 1 ? ( -
- Tutorium - {tutorialOptions.map((option) => { - const isSelected = option.slotId === visibleSlotId - return ( - - ) - })} -
- ) : null} -
- ) - })} -
- )} -
- ) -} diff --git a/frontend/src/features/newui/components/RegulationProgressCard.tsx b/frontend/src/features/newui/components/RegulationProgressCard.tsx deleted file mode 100644 index 40682a4..0000000 --- a/frontend/src/features/newui/components/RegulationProgressCard.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import type { MasterCat } from '../../courses' -import type { RegulationAreaProgress } from '../../dashboard/types' -import { areaProgressPercent } from '../utils/regulationProgress' - -// Reuses the existing category color tokens; areas without a category (e.g. the -// thesis) fall back to a neutral grey, matching the reference design. -const CAT_BAR_CLASS: Record = { - TECH: 'bg-cat-tech', - THEO: 'bg-cat-theo', - PRAK: 'bg-cat-prak', - INFO: 'bg-cat-info', - BASIS: 'bg-cat-basis', -} - -const CAT_COLOR_VAR: Record = { - TECH: 'var(--color-cat-tech)', - THEO: 'var(--color-cat-theo)', - PRAK: 'var(--color-cat-prak)', - INFO: 'var(--color-cat-info)', - BASIS: 'var(--color-cat-basis)', -} - -function barClass(masterCat: MasterCat | null): string { - return (masterCat && CAT_BAR_CLASS[masterCat]) || 'bg-[#c2bcac] dark:bg-neutral-600' -} - -function areaColor(masterCat: MasterCat | null): string { - return masterCat ? CAT_COLOR_VAR[masterCat] : '#c2bcac' -} - -/** Sum of planned ECTS across all raw rule-group codes an area covers. */ -function plannedEctsForArea(area: RegulationAreaProgress, plannedEctsByArea: Map): number { - const codes = area.rawAreaCodes && area.rawAreaCodes.length > 0 ? area.rawAreaCodes : [area.code] - return codes.reduce((sum, code) => sum + (plannedEctsByArea.get(code) ?? 0), 0) -} - -interface RegulationProgressCardProps { - areas: RegulationAreaProgress[] - plannedEctsByArea: Map -} - -export function RegulationProgressCard({ areas, plannedEctsByArea }: RegulationProgressCardProps) { - if (areas.length === 0) { - return null - } - - return ( -
-
-

- Prüfungsordnungs-Fortschritt -

-
- - - abgeschlossen - - - - geplant - -
-
-

- Voller Balken = abgeschlossen · schraffiert = was das aktuelle Semester hinzufügt. -

- -
- {areas.map((area) => { - const earnedPct = areaProgressPercent(area.earnedEcts, area.requiredEcts) - const planned = plannedEctsForArea(area, plannedEctsByArea) - const plannedPct = - area.requiredEcts > 0 - ? Math.min(100 - earnedPct, Math.round((planned / area.requiredEcts) * 100)) - : 0 - const color = areaColor(area.masterCat) - return ( -
-
- - - {area.code} - - - {area.name} - - - {area.earnedEcts} - {planned > 0 ? +{planned} : null} / {area.requiredEcts} - -
-
- - {plannedPct > 0 ? ( - - ) : null} -
-
- ) - })} -
-
- ) -} diff --git a/frontend/src/features/newui/components/SemesterColumn.tsx b/frontend/src/features/newui/components/SemesterColumn.tsx deleted file mode 100644 index 8b7b471..0000000 --- a/frontend/src/features/newui/components/SemesterColumn.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import type { Course } from '../../courses' -import { cleanCourseTitle } from '../../courses' -import { formatSemesterLabelDisplay } from '../../planner/utils/semesterLabels' -import type { CategoryControl } from '../hooks/useBetaPlanner' -import type { SemesterGroup } from '../types' -import { formatGrade } from '../utils/studyPlanOverview' -import { CategoryTag } from './CategoryTag' -import { CourseCategoryDropdown } from './CourseCategoryDropdown' - -interface SemesterColumnProps { - semester: SemesterGroup - /** Courses planned for the open (current) semester — shown only there. */ - plannedCourses?: Course[] - categoryControl?: CategoryControl - onRemovePlanned?: (courseId: string) => void -} - -export function SemesterColumn({ - semester, - plannedCourses = [], - categoryControl, - onRemovePlanned, -}: SemesterColumnProps) { - const { label, courses, totalEcts, averageGrade, isOpen } = semester - const plannedEcts = plannedCourses.reduce((sum, course) => sum + (course.ects ?? 0), 0) - - return ( -
-
- - {formatSemesterLabelDisplay(label)} - - - {isOpen ? 'Offen' : 'Fertig'} - -
- -
- {isOpen - ? `${plannedEcts} ECTS geplant · noch keine Note` - : `${totalEcts} ECTS · Ø ${formatGrade(averageGrade)}`} -
- - {isOpen ? ( - plannedCourses.length === 0 ? ( -
- Noch keine Kurse. -
- Unten einplanen → -
- ) : ( -
    - {plannedCourses.map((course) => { - const selectableCats = categoryControl?.selectableOf(course) ?? [] - const category = categoryControl?.categoryOf(course) ?? null - return ( -
  • - {categoryControl && selectableCats.length > 0 ? ( - categoryControl.change(course, cat)} - /> - ) : ( - - )} - - {cleanCourseTitle(course.title, course.number)} - - -
  • - ) - })} -
- ) - ) : ( -
    - {courses.map((course) => ( -
  • - - - {course.title} - - - {formatGrade(course.grade)} - -
  • - ))} -
- )} -
- ) -} diff --git a/frontend/src/features/newui/components/StudyPlanPage.tsx b/frontend/src/features/newui/components/StudyPlanPage.tsx deleted file mode 100644 index c58b871..0000000 --- a/frontend/src/features/newui/components/StudyPlanPage.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import { Link } from 'react-router-dom' -import { useAuth } from '../../auth' -import { ROUTES } from '../../routes' -import { useTheme } from '../../theme' -import { useAutoHideScrollbar } from '../hooks/useAutoHideScrollbar' -import { useBetaPlanner } from '../hooks/useBetaPlanner' -import { useStudyPlanOverview } from '../hooks/useStudyPlanOverview' -import { formatGrade } from '../utils/studyPlanOverview' -import { CoursePicker } from './CoursePicker' -import { RegulationProgressCard } from './RegulationProgressCard' -import { SemesterColumn } from './SemesterColumn' -import { Timetable } from './Timetable' - -// Exact palette from the reference mockup. -const CARD = - 'rounded-[20px] border border-[#ece7db] bg-white shadow-[0_2px_10px_rgba(60,50,20,0.04)] dark:border-neutral-800 dark:bg-neutral-900' -const KPI_LABEL = 'text-[10.5px] font-bold uppercase tracking-[0.1em] text-[#a39d90]' -const GOLD = 'text-[#b8790c] dark:text-[#d79a2e]' - -function BetaHeader() { - return ( -
-
- - StudyPlanner - - - Beta - -
- - ← Klassische Ansicht - -
- ) -} - -export function StudyPlanPage() { - const { isAuthenticated } = useAuth() - const { isDark } = useTheme() - const { summary, semesters, regulationAreas, isLoading } = useStudyPlanOverview() - const { - favoriteCourses, - plannedCourses, - plannedCourseIds, - hiddenSlotIds, - isLoading: isLoadingPlanner, - plannedEctsByArea, - categoryControl, - addCourse, - removeCourse, - selectTutorialSlot, - } = useBetaPlanner() - const plannedEcts = plannedCourses.reduce((sum, course) => sum + (course.ects ?? 0), 0) - const plannedCategoryById = new Map( - plannedCourses.map((course) => [course.id, categoryControl.categoryOf(course)]), - ) - const semesterScrollRef = useAutoHideScrollbar() - - const requiredEcts = summary?.requiredEcts ?? 120 - const totalEcts = summary?.totalEcts ?? 0 - const completedRatio = requiredEcts > 0 ? Math.min(1, totalEcts / requiredEcts) : 0 - const remainingEcts = Math.max(0, requiredEcts - totalEcts) - - const pageBackground = isDark - ? { - backgroundColor: '#141317', - backgroundImage: - 'radial-gradient(circle at 12% 0%, #7c5cff12, transparent 45%), radial-gradient(circle at 90% 10%, #e8a01010, transparent 40%)', - } - : { - backgroundColor: '#f7f4ee', - backgroundImage: - 'radial-gradient(circle at 12% 0%, #7c5cff14, transparent 45%), radial-gradient(circle at 90% 10%, #e8a01018, transparent 40%)', - } - - return ( -
- - -
-
-

Studienplan

-

- M.Sc. Informatik · dieselben Daten aus Transcript und manuell angelegten Kursen -

-
- - {!isAuthenticated ? ( -
- Bitte melde dich an, um deinen Studienplan zu sehen. -
- ) : ( - <> - {/* Three summary panels */} -
-
-
ECTS im Studium
-
- {totalEcts} - abgeschlossen / {requiredEcts} -
-
-
-
-

- Noch {remainingEcts} ECTS bis zum Abschluss. -

-
- -
-
Fortschritt
-
- {summary ? ( - <> - {summary.progressPercentage} - % - - ) : ( - '–' - )} -
-

des Studiengangs

-
- -
-
Ø-Note
-
- {summary ? formatGrade(summary.averageGrade) : '–'} -
-

gewichtet nach Noten

-
-
- - {/* Semesterverlauf */} -
-
-

Semesterverlauf

- - noch {remainingEcts} ECTS bis zum Abschluss · Kategorie pro Kurs änderbar - -
- - {isLoading && semesters.length === 0 ? ( -
Lädt…
- ) : ( -
- {semesters.map((semester) => ( - - ))} -
- )} -
- -
- - -
- - - - )} -
-
- ) -} diff --git a/frontend/src/features/newui/components/Timetable.tsx b/frontend/src/features/newui/components/Timetable.tsx deleted file mode 100644 index 202817b..0000000 --- a/frontend/src/features/newui/components/Timetable.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import { useMemo } from 'react' -import type { Course, MasterCat } from '../../courses' -import { useTheme } from '../../theme' -import { - buildBlockLeft, - buildBlockWidth, - buildDayLayout, - END_HOUR, - START_HOUR, -} from '../../planner/utils/plannerDayLayout.ts' -import { DAY_ORDER, buildPlannerBlocks } from '../../planner/utils/plannerFeedback.ts' -import { isTutorialLikeSlotType } from '../../planner/utils/plannerSlotSelection.ts' -import { useAutoHideScrollbar } from '../hooks/useAutoHideScrollbar' -import { isLectureOrTutorialBlock } from '../utils/timetableFilter' - -const PX_PER_HOUR = 42 -const GRID_HEIGHT = (END_HOUR - START_HOUR) * PX_PER_HOUR -const CAT_COLOR_VAR: Record = { - TECH: 'var(--color-cat-tech)', - THEO: 'var(--color-cat-theo)', - PRAK: 'var(--color-cat-prak)', - INFO: 'var(--color-cat-info)', - BASIS: 'var(--color-cat-basis)', -} - -function categoryColor(category: MasterCat | null | undefined): string { - return category ? CAT_COLOR_VAR[category] : '#7c5cff' -} - -const DAY_LABELS_DE: Record<(typeof DAY_ORDER)[number], string> = { - Monday: 'MO', - Tuesday: 'DI', - Wednesday: 'MI', - Thursday: 'DO', - Friday: 'FR', -} -const HOUR_MARKS = Array.from({ length: (END_HOUR - START_HOUR) / 2 + 1 }, (_, i) => START_HOUR + i * 2) - -interface TimetableProps { - plannedCourses: Course[] - hiddenSlotIds: string[] - plannedEcts: number - /** Assigned Informatik category per planned course id — drives the block color. */ - categoryByCourseId: Map -} - -export function Timetable({ plannedCourses, hiddenSlotIds, plannedEcts, categoryByCourseId }: TimetableProps) { - const blocks = useMemo( - () => - buildPlannerBlocks(plannedCourses) - .filter(isLectureOrTutorialBlock) - .filter((block) => !hiddenSlotIds.includes(block.slotId)), - [plannedCourses, hiddenSlotIds], - ) - const layoutByDay = useMemo( - () => - Object.fromEntries( - DAY_ORDER.map((day) => [day, buildDayLayout(blocks.filter((block) => block.day === day))]), - ) as Record<(typeof DAY_ORDER)[number], ReturnType>, - [blocks], - ) - const isEmpty = blocks.length === 0 - const scrollRef = useAutoHideScrollbar() - const { isDark } = useTheme() - // Day-column stripe background — theme-aware (inline style can't use `dark:`). - const stripeBase = isDark ? '#202024' : '#faf8f3' - const stripeLine = isDark ? '#2b2b31' : '#f2ede2' - - return ( -
-
-

- Stundenplan -

- - {plannedCourses.length} Kurse · {plannedEcts} ECTS - -
-
- - - Vorlesung - - - - Tutorium - -
- -
-
-
-
- {DAY_ORDER.map((day) => ( -
- {DAY_LABELS_DE[day]} -
- ))} -
- -
-
- {HOUR_MARKS.map((hour) => ( - - {String(hour).padStart(2, '0')} - - ))} -
- - {DAY_ORDER.map((day) => ( -
- {layoutByDay[day].visibleBlocks.map((block) => { - const isTutorial = isTutorialLikeSlotType(block.slotType) - const top = ((block.startMinutes - START_HOUR * 60) / 60) * PX_PER_HOUR - const height = ((block.endMinutes - block.startMinutes) / 60) * PX_PER_HOUR - const color = categoryColor(categoryByCourseId.get(block.courseId)) - return ( -
-
- {block.courseTitle} -
-
- {isTutorial ? 'Tut' : 'VL'} · {block.label} -
-
- ) - })} -
- ))} -
-
-
- - {isEmpty ? ( -

- Noch leer — links Kurse einplanen, sie erscheinen hier. -

- ) : null} -
- ) -} diff --git a/frontend/src/features/newui/hooks/useAutoHideScrollbar.ts b/frontend/src/features/newui/hooks/useAutoHideScrollbar.ts deleted file mode 100644 index b75354f..0000000 --- a/frontend/src/features/newui/hooks/useAutoHideScrollbar.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { useEffect, useRef } from 'react' -import type { RefObject } from 'react' - -const IDLE_HIDE_DELAY_MS = 800 - -/** - * Returns a ref for a scroll container whose scrollbar stays hidden until the - * user scrolls (an `is-scrolling` class is toggled; the CSS in `index.css` shows - * the thumb while it is present, then hides it a short moment after scrolling). - */ -export function useAutoHideScrollbar(): RefObject { - const ref = useRef(null) - - useEffect(() => { - const element = ref.current - if (!element) { - return - } - let hideTimeout: number | undefined - function handleScroll(): void { - const node = ref.current - if (!node) { - return - } - node.classList.add('is-scrolling') - window.clearTimeout(hideTimeout) - hideTimeout = window.setTimeout(() => node.classList.remove('is-scrolling'), IDLE_HIDE_DELAY_MS) - } - element.addEventListener('scroll', handleScroll, { passive: true }) - return () => { - element.removeEventListener('scroll', handleScroll) - window.clearTimeout(hideTimeout) - } - }, []) - - return ref -} diff --git a/frontend/src/features/newui/hooks/useBetaPlanner.ts b/frontend/src/features/newui/hooks/useBetaPlanner.ts deleted file mode 100644 index 649961e..0000000 --- a/frontend/src/features/newui/hooks/useBetaPlanner.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { useMemo } from 'react' -import { useRegulationVersion } from '../../../shared/hooks/useRegulationVersion' -import { useAuth } from '../../auth' -import type { Course, MasterCat } from '../../courses' -import { ALL_CATALOG_PERIODS, findCatalogPeriodForSemesterLabel, useCatalogCourses, useCatalogPeriods } from '../../courses' -import { useFavorites } from '../../favorites' -import { useSemesterPlanner } from '../../planner/hooks/useSemesterPlanner' -import { - getPlannerCourseAreaOptions, - getSuggestedPlannerAssignment, - resolveChosenInfoAlternativeAreaCode, -} from '../../planner/utils/plannerAssignments.ts' -import { - defaultHiddenTutorialSlotIds, - getTutorialSlotOptions, - hiddenSlotIdsForTutorialSelection, -} from '../../planner/utils/plannerSlotSelection.ts' -import { useTranscript } from '../../transcript' -import { areaCodeForCategory, categoriesFromOptions } from '../utils/plannerCategory' - -export interface CategoryControl { - categoryOf: (course: Course) => MasterCat | null - selectableOf: (course: Course) => MasterCat[] - change: (course: Course, category: MasterCat) => void -} - -export interface BetaPlannerResult { - activeSemesterLabel: string - favoriteCourses: Course[] - plannedCourses: Course[] - plannedCourseIds: string[] - hiddenSlotIds: string[] - isLoading: boolean - /** Planned ECTS per regulation-area code (assigned or suggested). */ - plannedEctsByArea: Map - categoryControl: CategoryControl - addCourse: (courseId: string) => void - removeCourse: (courseId: string) => void - toggleFavorite: (courseId: string) => void - selectTutorialSlot: (courseId: string, slotId: string) => void -} - -/** - * Beta-page planner: reuses the existing semester-plan state, favorites, catalog - * and planner area-assignment logic; only the presentation is new. - */ -export function useBetaPlanner(): BetaPlannerResult { - const { user } = useAuth() - const { favoriteIds, toggleFavorite } = useFavorites() - const { completedCourses } = useTranscript() - const { regulationVersion } = useRegulationVersion(user?.profile.regulationVersionCode) - const { - activeSemesterLabel, - plannedCourseIds, - hiddenSlotIds, - planAssignments, - isLoadingSemesterPlan, - setPlannedCourseIds, - setHiddenSlotIds, - setAssignment, - } = useSemesterPlanner() - - const { periods } = useCatalogPeriods() - const activePeriodId = useMemo( - () => findCatalogPeriodForSemesterLabel(periods, activeSemesterLabel)?.periodId, - [periods, activeSemesterLabel], - ) - const { courses, isLoading } = useCatalogCourses('', 500, activePeriodId) - const { courses: allCatalogCourses } = useCatalogCourses('', 1000, ALL_CATALOG_PERIODS) - - const courseById = useMemo( - () => new Map([...allCatalogCourses, ...courses].map((course) => [course.id, course])), - [allCatalogCourses, courses], - ) - - const favoriteCourses = useMemo(() => { - const byId = new Map() - for (const course of [...courses, ...allCatalogCourses]) { - if (favoriteIds.includes(course.id) && !byId.has(course.id)) { - byId.set(course.id, course) - } - } - return [...byId.values()].sort((left, right) => left.title.localeCompare(right.title)) - }, [courses, allCatalogCourses, favoriteIds]) - - const plannedCourses = useMemo( - () => - plannedCourseIds - .map((courseId) => courseById.get(courseId)) - .filter((course): course is Course => course !== undefined), - [plannedCourseIds, courseById], - ) - - const studyProgramCode = user?.profile.studyProgramCode ?? null - const ruleGroups = useMemo(() => regulationVersion?.ruleGroups ?? [], [regulationVersion?.ruleGroups]) - const chosenInfoAlternativeCode = useMemo( - () => - resolveChosenInfoAlternativeAreaCode({ - plannedCourses, - completedCourses, - studyProgramCode, - regulationRuleGroups: ruleGroups, - }), - [plannedCourses, completedCourses, studyProgramCode, ruleGroups], - ) - - const optionsFor = (course: Course) => - getPlannerCourseAreaOptions(course, studyProgramCode, ruleGroups, chosenInfoAlternativeCode) - - const resolvedAreaCode = (course: Course): string | null => - planAssignments[course.id] ?? - getSuggestedPlannerAssignment(course, { - studyProgramCode, - regulationRuleGroups: ruleGroups, - planAssignments, - plannedCourses, - completedCourses, - chosenInfoAlternativeCode, - }) - - const categoryControl: CategoryControl = { - categoryOf: (course) => { - const code = resolvedAreaCode(course) - if (!code) { - return null - } - return optionsFor(course).find((option) => option.code === code)?.masterCat ?? null - }, - selectableOf: (course) => categoriesFromOptions(optionsFor(course)), - change: (course, category) => { - const code = areaCodeForCategory(optionsFor(course), category) - if (code) { - setAssignment(course.id, code) - } - }, - } - - const plannedEctsByArea = useMemo(() => { - const map = new Map() - for (const course of plannedCourses) { - const code = - planAssignments[course.id] ?? - getSuggestedPlannerAssignment(course, { - studyProgramCode, - regulationRuleGroups: ruleGroups, - planAssignments, - plannedCourses, - completedCourses, - chosenInfoAlternativeCode, - }) - if (!code) { - continue - } - map.set(code, (map.get(code) ?? 0) + (course.ects ?? 0)) - } - return map - }, [plannedCourses, planAssignments, studyProgramCode, ruleGroups, completedCourses, chosenInfoAlternativeCode]) - - function addCourse(courseId: string): void { - if (!plannedCourseIds.includes(courseId)) { - setPlannedCourseIds([...plannedCourseIds, courseId]) - } - const course = courseById.get(courseId) ?? null - const defaultHidden = course ? defaultHiddenTutorialSlotIds(getTutorialSlotOptions(course)) : [] - setHiddenSlotIds([ - ...hiddenSlotIds.filter((slotId) => !slotId.startsWith(`${courseId}:`)), - ...defaultHidden, - ]) - } - - function removeCourse(courseId: string): void { - setPlannedCourseIds(plannedCourseIds.filter((id) => id !== courseId)) - setHiddenSlotIds(hiddenSlotIds.filter((slotId) => !slotId.startsWith(`${courseId}:`))) - setAssignment(courseId, null) - } - - function selectTutorialSlot(courseId: string, selectedSlotId: string): void { - const course = courseById.get(courseId) - if (!course) { - return - } - const tutorialSlotIds = getTutorialSlotOptions(course).map((option) => option.slotId) - const nextHidden = hiddenSlotIdsForTutorialSelection(tutorialSlotIds, selectedSlotId) - setHiddenSlotIds([ - ...hiddenSlotIds.filter((slotId) => !slotId.startsWith(`${courseId}:`)), - ...nextHidden, - ]) - } - - return { - activeSemesterLabel, - favoriteCourses, - plannedCourses, - plannedCourseIds, - hiddenSlotIds, - isLoading: isLoadingSemesterPlan || isLoading, - plannedEctsByArea, - categoryControl, - addCourse, - removeCourse, - toggleFavorite, - selectTutorialSlot, - } -} diff --git a/frontend/src/features/newui/hooks/useStudyPlanOverview.ts b/frontend/src/features/newui/hooks/useStudyPlanOverview.ts deleted file mode 100644 index 6f3dfce..0000000 --- a/frontend/src/features/newui/hooks/useStudyPlanOverview.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { useCallback, useMemo } from 'react' -import type { CompletedCourse, MasterCat } from '../../courses' -import type { ProgressSummary, RegulationAreaProgress } from '../../dashboard/types' -import { useProgressSnapshot } from '../../dashboard/hooks/useProgressSnapshot' -import { getCurrentSemesterLabel } from '../../planner/utils/semesterLabels' -import { useTranscript } from '../../transcript' -import type { SemesterGroup } from '../types' -import { buildCategoryAreaMap, selectableCategoriesFromMap } from '../utils/categoryAssignment' -import { buildSemesterGroups } from '../utils/studyPlanOverview' - -interface UseStudyPlanOverviewResult { - summary: ProgressSummary | null - semesters: SemesterGroup[] - regulationAreas: RegulationAreaProgress[] - /** Categories the study program supports — the ones a course can be moved to. */ - selectableCategories: MasterCat[] - isLoading: boolean - changeCourseCategory: (course: CompletedCourse, masterCat: MasterCat) => void -} - -/** - * Read model for the beta study-plan page. It reuses the existing transcript and - * progress data unchanged — only the shaping (semester grouping) is added here. - */ -export function useStudyPlanOverview(): UseStudyPlanOverviewResult { - const { completedCourses, isLoadingCompletedCourses, setCategory, updateCourse } = useTranscript() - const { progressSnapshot, isLoadingProgress } = useProgressSnapshot() - - const semesters = useMemo( - () => buildSemesterGroups(completedCourses, getCurrentSemesterLabel()), - [completedCourses], - ) - - const regulationAreas = useMemo( - () => progressSnapshot?.regulationProgress ?? [], - [progressSnapshot], - ) - - // A category is assignable by setting the study-area code that maps to it — - // the backend derives the category from that area. Catalog-matched courses - // accept any regulation area, so the choices come from the program's areas. - const categoryAreaMap = useMemo(() => buildCategoryAreaMap(regulationAreas), [regulationAreas]) - const selectableCategories = useMemo( - () => selectableCategoriesFromMap(categoryAreaMap), - [categoryAreaMap], - ) - - const changeCourseCategory = useCallback( - (course: CompletedCourse, masterCat: MasterCat): void => { - const areaCode = categoryAreaMap.get(masterCat) - if (areaCode) { - // Set masterCat too so the UI reflects the change immediately; the server - // re-derives the same category from the study-area code on save. - void updateCourse(course.id, { studyAreaCode: areaCode, masterCat }) - } else { - void setCategory(course.id, masterCat) - } - }, - [categoryAreaMap, setCategory, updateCourse], - ) - - return { - summary: progressSnapshot?.summary ?? null, - semesters, - regulationAreas, - selectableCategories, - isLoading: isLoadingCompletedCourses || isLoadingProgress, - changeCourseCategory, - } -} diff --git a/frontend/src/features/newui/index.ts b/frontend/src/features/newui/index.ts deleted file mode 100644 index 2ac1c5e..0000000 --- a/frontend/src/features/newui/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { StudyPlanPage } from './components/StudyPlanPage' diff --git a/frontend/src/features/newui/types.ts b/frontend/src/features/newui/types.ts deleted file mode 100644 index e1bd87a..0000000 --- a/frontend/src/features/newui/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { CompletedCourse } from '../courses' - -/** One semester column in the "Semesterverlauf" of the beta study-plan UI. */ -export interface SemesterGroup { - label: string - courses: CompletedCourse[] - totalEcts: number - averageGrade: number | null - /** Current or future semester — rendered as the open ("offen") column. */ - isOpen: boolean -} diff --git a/frontend/src/features/newui/utils/categoryAssignment.ts b/frontend/src/features/newui/utils/categoryAssignment.ts deleted file mode 100644 index 0e3d617..0000000 --- a/frontend/src/features/newui/utils/categoryAssignment.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { MasterCat } from '../../courses' -import type { RegulationAreaProgress } from '../../dashboard/types' -import { studyAreaCodeToMasterCat } from '../../../shared/utils/regulation.ts' - -const CATEGORY_ORDER: readonly MasterCat[] = ['TECH', 'THEO', 'PRAK', 'INFO', 'BASIS'] - -/** The rule-group code the backend accepts when assigning this area. */ -function areaCodeOf(area: RegulationAreaProgress): string { - return area.rawAreaCodes?.[0] ?? area.code -} - -/** The master category an area counts toward (derived from its code, then the - * backend-provided category as a fallback). */ -function areaCategory(area: RegulationAreaProgress): MasterCat | null { - return studyAreaCodeToMasterCat(areaCodeOf(area)) ?? area.masterCat -} - -/** - * Maps each master category to a study-area code of the student's regulation. - * This is what makes a category assignable: the backend derives the category - * from the study area, so picking a category means assigning its area code. - */ -export function buildCategoryAreaMap(areas: RegulationAreaProgress[]): Map { - const map = new Map() - for (const area of areas) { - const cat = areaCategory(area) - if (cat && !map.has(cat)) { - map.set(cat, areaCodeOf(area)) - } - } - return map -} - -/** Categories the regulation supports, in canonical display order. */ -export function selectableCategoriesFromMap(categoryAreaMap: Map): MasterCat[] { - return CATEGORY_ORDER.filter((cat) => categoryAreaMap.has(cat)) -} diff --git a/frontend/src/features/newui/utils/plannerCategory.ts b/frontend/src/features/newui/utils/plannerCategory.ts deleted file mode 100644 index 1cec993..0000000 --- a/frontend/src/features/newui/utils/plannerCategory.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { MasterCat } from '../../courses' -import type { RegulationAreaOption } from '../../../shared/utils/regulation' - -const CATEGORY_ORDER: readonly MasterCat[] = ['TECH', 'THEO', 'PRAK', 'INFO', 'BASIS'] - -/** Distinct categories a planned course can be assigned to, in display order. */ -export function categoriesFromOptions(options: RegulationAreaOption[]): MasterCat[] { - const cats = new Set() - for (const option of options) { - if (option.masterCat) { - cats.add(option.masterCat) - } - } - return CATEGORY_ORDER.filter((cat) => cats.has(cat)) -} - -/** The area code that assigns a planned course to the given category, if any. */ -export function areaCodeForCategory(options: RegulationAreaOption[], category: MasterCat): string | null { - return options.find((option) => option.masterCat === category)?.code ?? null -} diff --git a/frontend/src/features/newui/utils/plannerFormat.ts b/frontend/src/features/newui/utils/plannerFormat.ts deleted file mode 100644 index 8f5ec10..0000000 --- a/frontend/src/features/newui/utils/plannerFormat.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { normalizeWeekday, parseTimeRange } from '../../planner/utils/plannerFeedback.ts' - -const DAY_SHORT: Record = { - Monday: 'Mo', - Tuesday: 'Di', - Wednesday: 'Mi', - Thursday: 'Do', - Friday: 'Fr', -} - -function formatHour(minutes: number): string { - const hour = Math.floor(minutes / 60) - const minute = minutes % 60 - return minute === 0 ? String(hour) : `${hour}:${String(minute).padStart(2, '0')}` -} - -/** - * Compacts a tutorial-slot label ("Mo · 14:00-16:00 · A101") to just weekday and - * hour range ("Mo 14-16"). - */ -export function formatTutorialSlotLabel(label: string): string { - const [dayPart = '', timePart = ''] = label.split(' · ') - const day = normalizeWeekday(dayPart) - const dayShort = day ? DAY_SHORT[day] : dayPart.trim() - const range = parseTimeRange(timePart) - const time = range ? `${formatHour(range.startMinutes)}-${formatHour(range.endMinutes)}` : timePart.trim() - return [dayShort, time].filter(Boolean).join(' ') -} diff --git a/frontend/src/features/newui/utils/regulationProgress.ts b/frontend/src/features/newui/utils/regulationProgress.ts deleted file mode 100644 index 1b45bac..0000000 --- a/frontend/src/features/newui/utils/regulationProgress.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** Completed share of an area, clamped to 0–100 (earned may exceed required). */ -export function areaProgressPercent(earnedEcts: number, requiredEcts: number): number { - if (requiredEcts <= 0) { - return 0 - } - return Math.min(100, Math.round((earnedEcts / requiredEcts) * 100)) -} diff --git a/frontend/src/features/newui/utils/studyPlanOverview.ts b/frontend/src/features/newui/utils/studyPlanOverview.ts deleted file mode 100644 index 67eb167..0000000 --- a/frontend/src/features/newui/utils/studyPlanOverview.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { CompletedCourse } from '../../courses' -import { compareSemesterLabels } from '../../planner/utils/semesterLabels.ts' -import type { SemesterGroup } from '../types.ts' - -/** Mean of the counted, graded courses; null when nothing is graded yet. */ -export function averageOfGrades(courses: CompletedCourse[]): number | null { - const grades = courses - .filter((course) => course.grade !== null && course.isGradeCounted !== false) - .map((course) => course.grade as number) - if (grades.length === 0) { - return null - } - return grades.reduce((sum, grade) => sum + grade, 0) / grades.length -} - -/** - * Groups completed courses into semester columns, newest first. The current - * semester always gets a column (even without courses) so it can render as the - * open "offen" column; current and future semesters are marked open. - */ -export function buildSemesterGroups( - completedCourses: CompletedCourse[], - currentSemesterLabel: string, -): SemesterGroup[] { - const groups = new Map() - for (const course of completedCourses) { - const existing = groups.get(course.semester) - if (existing) { - existing.push(course) - } else { - groups.set(course.semester, [course]) - } - } - - if (!groups.has(currentSemesterLabel)) { - groups.set(currentSemesterLabel, []) - } - - return [...groups.entries()] - .map(([label, courses]) => ({ - label, - courses, - totalEcts: courses.reduce((sum, course) => sum + course.ects, 0), - averageGrade: averageOfGrades(courses), - isOpen: compareSemesterLabels(label, currentSemesterLabel) >= 0, - })) - .sort((left, right) => compareSemesterLabels(right.label, left.label)) -} - -/** German one-decimal grade, e.g. 1.9 -> "1,9"; null -> "–". */ -export function formatGrade(grade: number | null): string { - return grade === null ? '–' : grade.toFixed(1).replace('.', ',') -} diff --git a/frontend/src/features/newui/utils/timetableFilter.ts b/frontend/src/features/newui/utils/timetableFilter.ts deleted file mode 100644 index f091d4b..0000000 --- a/frontend/src/features/newui/utils/timetableFilter.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { PlannerBlock } from '../../planner/utils/plannerFeedback.ts' -import { isTutorialLikeSlotType } from '../../planner/utils/plannerSlotSelection.ts' - -// Session types that are neither lectures nor tutorials and must stay out of the -// weekly grid. -const OTHER_SESSION_PATTERN = /seminar|praktikum|kolloquium/i - -/** - * The beta timetable shows only lectures and tutorials — never exams/resits - * (non-weekly slot kinds) or other session types. - */ -export function isLectureOrTutorialBlock(block: Pick): boolean { - if (block.slotKind !== 'weekly') { - return false - } - if (isTutorialLikeSlotType(block.slotType)) { - return true - } - return !OTHER_SESSION_PATTERN.test(block.slotType) -} diff --git a/frontend/src/features/routes.ts b/frontend/src/features/routes.ts index 1c380d1..518c30a 100644 --- a/frontend/src/features/routes.ts +++ b/frontend/src/features/routes.ts @@ -8,9 +8,6 @@ export const ROUTES = { account: '/account', log: '/log', semesterDetail: '/semester/:label', - // Standalone beta UI surface (parallel look, same data); rendered outside the - // shared Layout so it can carry its own header and color scheme. - beta: '/beta', } as const // The planner hub used to live at '/'; '/' now opens the catalog instead. diff --git a/frontend/src/index.css b/frontend/src/index.css index 91ae986..40a4922 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -112,21 +112,6 @@ body { } } -/* Auto-hiding scrollbar: transparent at rest, visible only while the container is - actively scrolled (`is-scrolling`, toggled by useAutoHideScrollbar with a short - trailing delay). Uses the standard `scrollbar-color` property (which updates - reactively, unlike `::-webkit-scrollbar-*` pseudo-elements that get stuck). - Setting `scrollbar-color` also makes WebKit/Chrome ignore the always-on global - scrollbar pseudo-elements for these containers. */ -.beta-scroll { - scrollbar-width: thin; - scrollbar-color: transparent transparent; -} - -.beta-scroll.is-scrolling { - scrollbar-color: var(--color-scrollbar) transparent; -} - .dark { --color-bg: rgb(26 28 32); --color-surface: #27292F; diff --git a/frontend/tests/newui/categoryAssignment.test.ts b/frontend/tests/newui/categoryAssignment.test.ts deleted file mode 100644 index ceddff6..0000000 --- a/frontend/tests/newui/categoryAssignment.test.ts +++ /dev/null @@ -1,52 +0,0 @@ -import assert from 'node:assert/strict' -import test from 'node:test' -import type { RegulationAreaProgress } from '../../src/features/dashboard/types.ts' -import { - buildCategoryAreaMap, - selectableCategoriesFromMap, -} from '../../src/features/newui/utils/categoryAssignment.ts' - -function area(overrides: Partial & Pick): RegulationAreaProgress { - return { - name: overrides.code, - requiredEcts: 18, - earnedEcts: 0, - masterCat: null, - ...overrides, - } -} - -const REGULATION: RegulationAreaProgress[] = [ - area({ code: 'TECH', masterCat: 'TECH' }), - area({ code: 'THEO', masterCat: 'THEO' }), - area({ code: 'PRAK', masterCat: 'PRAK' }), - area({ code: 'INFO', masterCat: 'INFO' }), - area({ code: 'FOKUS', masterCat: 'BASIS' }), - area({ code: 'THESIS', masterCat: null, requiredEcts: 30 }), -] - -test('maps every regulation category to a valid study-area code', () => { - const map = buildCategoryAreaMap(REGULATION) - assert.equal(map.get('TECH'), 'TECH') - assert.equal(map.get('INFO'), 'INFO') - // FOKUS has no code-derived category, so its backend category (BASIS) is used. - assert.equal(map.get('BASIS'), 'FOKUS') - // The thesis has no category and must not become selectable. - assert.equal(map.has('THESIS' as never), false) -}) - -test('prefers the raw rule-group code over a merged display code', () => { - const map = buildCategoryAreaMap([ - area({ code: 'INF-MERGED', masterCat: 'INFO', rawAreaCodes: ['INFO'] }), - ]) - assert.equal(map.get('INFO'), 'INFO') -}) - -test('lists selectable categories in canonical order', () => { - const map = buildCategoryAreaMap(REGULATION) - assert.deepEqual(selectableCategoriesFromMap(map), ['TECH', 'THEO', 'PRAK', 'INFO', 'BASIS']) -}) - -test('returns no categories for an empty regulation', () => { - assert.deepEqual(selectableCategoriesFromMap(buildCategoryAreaMap([])), []) -}) diff --git a/frontend/tests/newui/plannerBeta.test.ts b/frontend/tests/newui/plannerBeta.test.ts deleted file mode 100644 index 3391031..0000000 --- a/frontend/tests/newui/plannerBeta.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import assert from 'node:assert/strict' -import test from 'node:test' -import type { RegulationAreaOption } from '../../src/shared/utils/regulation.ts' -import { - areaCodeForCategory, - categoriesFromOptions, -} from '../../src/features/newui/utils/plannerCategory.ts' -import { formatTutorialSlotLabel } from '../../src/features/newui/utils/plannerFormat.ts' -import { isLectureOrTutorialBlock } from '../../src/features/newui/utils/timetableFilter.ts' - -function option(overrides: Partial & Pick): RegulationAreaOption { - return { - label: overrides.code, - shortLabel: overrides.code, - isFlexible: false, - ...overrides, - } -} - -test('lists selectable categories from area options in canonical order', () => { - const options = [ - option({ code: 'INFO-PRAK', masterCat: 'PRAK' }), - option({ code: 'INFO-TECH', masterCat: 'TECH' }), - option({ code: 'UEBK', masterCat: 'BASIS' }), - option({ code: 'NONE', masterCat: null }), - ] - assert.deepEqual(categoriesFromOptions(options), ['TECH', 'PRAK', 'BASIS']) -}) - -test('resolves the area code for a target category', () => { - const options = [ - option({ code: 'INFO-TECH', masterCat: 'TECH' }), - option({ code: 'INFO-PRAK', masterCat: 'PRAK' }), - ] - assert.equal(areaCodeForCategory(options, 'PRAK'), 'INFO-PRAK') - assert.equal(areaCodeForCategory(options, 'THEO'), null) -}) - -test('keeps only lectures and tutorials in the timetable', () => { - assert.equal(isLectureOrTutorialBlock({ slotKind: 'weekly', slotType: '' }), true) - assert.equal(isLectureOrTutorialBlock({ slotKind: 'weekly', slotType: 'Vorlesung' }), true) - assert.equal(isLectureOrTutorialBlock({ slotKind: 'weekly', slotType: 'Übung' }), true) - assert.equal(isLectureOrTutorialBlock({ slotKind: 'exam', slotType: 'Klausur' }), false) - assert.equal(isLectureOrTutorialBlock({ slotKind: 'resit', slotType: 'Nachklausur' }), false) - assert.equal(isLectureOrTutorialBlock({ slotKind: 'weekly', slotType: 'Seminar' }), false) - assert.equal(isLectureOrTutorialBlock({ slotKind: 'weekly', slotType: 'Praktikum' }), false) -}) - -test('compacts tutorial labels to weekday and hour range', () => { - assert.equal(formatTutorialSlotLabel('Mo · 14:00-16:00 · A101'), 'Mo 14-16') - assert.equal(formatTutorialSlotLabel('Montag · 08:00 - 10:00 · B2'), 'Mo 8-10') - assert.equal(formatTutorialSlotLabel('Do · 14:30-16:00'), 'Do 14:30-16') -}) diff --git a/frontend/tests/newui/regulationProgress.test.ts b/frontend/tests/newui/regulationProgress.test.ts deleted file mode 100644 index 8c986de..0000000 --- a/frontend/tests/newui/regulationProgress.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import assert from 'node:assert/strict' -import test from 'node:test' -import { areaProgressPercent } from '../../src/features/newui/utils/regulationProgress.ts' - -test('clamps the earned share to 0-100 and rounds', () => { - assert.equal(areaProgressPercent(9, 18), 50) - assert.equal(areaProgressPercent(24, 18), 100) - assert.equal(areaProgressPercent(0, 30), 0) -}) - -test('returns 0 when nothing is required', () => { - assert.equal(areaProgressPercent(6, 0), 0) -}) diff --git a/frontend/tests/newui/studyPlanOverview.test.ts b/frontend/tests/newui/studyPlanOverview.test.ts deleted file mode 100644 index 6907ce3..0000000 --- a/frontend/tests/newui/studyPlanOverview.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import assert from 'node:assert/strict' -import test from 'node:test' -import type { CompletedCourse } from '../../src/features/courses/types.ts' -import { - averageOfGrades, - buildSemesterGroups, - formatGrade, -} from '../../src/features/newui/utils/studyPlanOverview.ts' - -function course(overrides: Partial & Pick): CompletedCourse { - return { - title: 'Course', - ects: 6, - masterCat: 'INFO', - grade: 2.0, - ...overrides, - } -} - -test('groups completed courses by semester, newest first', () => { - const groups = buildSemesterGroups( - [ - course({ id: 'a', semester: 'SS 2025', ects: 6 }), - course({ id: 'b', semester: 'WS 2024/25', ects: 3 }), - course({ id: 'c', semester: 'SS 2025', ects: 6 }), - ], - 'SS 2026', - ) - - assert.deepEqual( - groups.map((group) => group.label), - ['SS 2026', 'SS 2025', 'WS 2024/25'], - ) - const ss2025 = groups.find((group) => group.label === 'SS 2025') - assert.equal(ss2025?.courses.length, 2) - assert.equal(ss2025?.totalEcts, 12) -}) - -test('always adds an open column for the current semester', () => { - const groups = buildSemesterGroups([], 'SS 2026') - assert.equal(groups.length, 1) - assert.equal(groups[0]?.label, 'SS 2026') - assert.equal(groups[0]?.isOpen, true) - assert.equal(groups[0]?.courses.length, 0) -}) - -test('marks current and future semesters as open, past as closed', () => { - const groups = buildSemesterGroups( - [course({ id: 'past', semester: 'SS 2025' }), course({ id: 'future', semester: 'SS 2027' })], - 'SS 2026', - ) - assert.equal(groups.find((group) => group.label === 'SS 2025')?.isOpen, false) - assert.equal(groups.find((group) => group.label === 'SS 2027')?.isOpen, true) -}) - -test('averageOfGrades ignores ungraded and uncounted courses', () => { - const avg = averageOfGrades([ - course({ id: 'a', semester: 'SS 2025', grade: 1.0 }), - course({ id: 'b', semester: 'SS 2025', grade: 3.0 }), - course({ id: 'c', semester: 'SS 2025', grade: null }), - course({ id: 'd', semester: 'SS 2025', grade: 5.0, isGradeCounted: false }), - ]) - assert.equal(avg, 2.0) - assert.equal(averageOfGrades([course({ id: 'x', semester: 'SS 2025', grade: null })]), null) -}) - -test('formatGrade renders one German decimal or a dash', () => { - assert.equal(formatGrade(1.9), '1,9') - assert.equal(formatGrade(2), '2,0') - assert.equal(formatGrade(null), '–') -})