From 8b887f7750cd2d8bf11590eb1550e39b3f1cded8 Mon Sep 17 00:00:00 2001 From: Bill Ballou Date: Fri, 13 Mar 2026 16:11:56 -0400 Subject: [PATCH] v1.1.0: Add cancelled course, duplicate prevention, and credit bar ticks - Mark "Managing Growing Companies" as cancelled with visual indicator and solver exclusion - Prevent selecting duplicate courses across elective sets (e.g., same course in Spring and Summer) - Add 2.5-credit interval tick marks to specialization progress bars - Bump version to 1.1.0 with date display in UI header --- CHANGELOG.md | 8 ++++ app/src/App.tsx | 9 +++-- app/src/components/CourseSelection.tsx | 42 ++++++++++++++++---- app/src/components/SpecializationRanking.tsx | 19 ++++++++- app/src/data/courses.ts | 1 + app/src/data/lookups.ts | 12 ++++++ app/src/data/types.ts | 1 + app/src/solver/decisionTree.ts | 40 +++++++++++-------- app/src/solver/feasibility.ts | 4 ++ app/src/solver/optimizer.ts | 16 +++++--- app/src/state/appState.ts | 32 +++++++++++++-- app/src/vite-env.d.ts | 1 + app/src/workers/decisionTree.worker.ts | 7 +++- app/vite.config.ts | 3 +- 14 files changed, 156 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7b492c..48b6cfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v1.1.0 — 2026-03-13 + +### Changes + +- **Cancelled course support** — "Managing Growing Companies" (Summer Elective Set 2) is marked as cancelled and rendered with strikethrough, greyed-out styling, and a "(Cancelled)" label; it is excluded from solver computations and decision tree enumeration +- **Duplicate course prevention** — courses that appear in multiple elective sets (e.g., "Global Immersion Experience II" in Spring Set 1 and Summer Set 1, "The Financial Services Industry" in Spring Set 2 and Fall Set 4) are now linked; selecting one automatically disables and excludes its duplicate from selection and solver calculations, shown with an "(Already selected)" label +- **Credit bar tick marks** — specialization progress bars now display light vertical tick marks at 2.5-credit intervals for visual scale reference, layered above bar fills with the 9.0 threshold marker remaining visually distinct + ## v1.0.0 — 2026-02-28 Initial release of the EMBA Specialization Solver. diff --git a/app/src/App.tsx b/app/src/App.tsx index 3a6ec4c..f461c84 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -18,6 +18,8 @@ function App() { treeLoading, openSetIds, selectedCourseIds, + disabledCourseIds, + excludedCourseIds, reorder, setMode, pinCourse, @@ -30,8 +32,8 @@ function App() { // Compute alternative mode result for comparison const altMode = state.mode === 'maximize-count' ? 'priority-order' : 'maximize-count'; const altResult = useMemo( - () => optimize(selectedCourseIds, state.ranking, openSetIds, altMode), - [selectedCourseIds, state.ranking, openSetIds, altMode], + () => optimize(selectedCourseIds, state.ranking, openSetIds, altMode, excludedCourseIds), + [selectedCourseIds, state.ranking, openSetIds, altMode, excludedCourseIds], ); const isMobile = breakpoint === 'mobile'; @@ -105,7 +107,7 @@ function App() {

EMBA Specialization Solver

-
v{__APP_VERSION__}
+
v{__APP_VERSION__} ({__APP_VERSION_DATE__})
@@ -129,6 +131,7 @@ function App() { pinnedCourses={state.pinnedCourses} treeResults={treeResults} treeLoading={treeLoading} + disabledCourseIds={disabledCourseIds} onPin={pinCourse} onUnpin={unpinCourse} onClearAll={clearAll} diff --git a/app/src/components/CourseSelection.tsx b/app/src/components/CourseSelection.tsx index 0a79197..2fb6aca 100644 --- a/app/src/components/CourseSelection.tsx +++ b/app/src/components/CourseSelection.tsx @@ -16,6 +16,7 @@ interface CourseSelectionProps { pinnedCourses: Record; treeResults: SetAnalysis[]; treeLoading: boolean; + disabledCourseIds: Set; onPin: (setId: string, courseId: string) => void; onUnpin: (setId: string) => void; onClearAll: () => void; @@ -27,6 +28,7 @@ function ElectiveSet({ pinnedCourseId, analysis, loading, + disabledCourseIds, onPin, onUnpin, }: { @@ -35,6 +37,7 @@ function ElectiveSet({ pinnedCourseId: string | null | undefined; analysis?: SetAnalysis; loading: boolean; + disabledCourseIds: Set; onPin: (courseId: string) => void; onUnpin: () => void; }) { @@ -101,23 +104,47 @@ function ElectiveSet({ }}>
{courses.map((course) => { + const isCancelled = !!course.cancelled; + const isDisabled = disabledCourseIds.has(course.id); + const isUnavailable = isCancelled || isDisabled; const ceiling = ceilingMap.get(course.id); const reqFor = requiredForSpec[course.id]; const showSkeleton = loading && !analysis; return (