Implement EMBA Specialization Solver web app

Full React+TypeScript app with LP-based optimization engine,
drag-and-drop specialization ranking (with touch/arrow support),
course selection UI, results dashboard with decision tree, and
two optimization modes (maximize-count, priority-order).
This commit is contained in:
2026-02-28 20:43:00 -05:00
parent e62afa631b
commit 9e00901179
43 changed files with 10098 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
import { describe, it, expect } from 'vitest';
import { COURSES } from '../courses';
import { ELECTIVE_SETS } from '../electiveSets';
import { SPECIALIZATIONS } from '../specializations';
import { coursesBySet, coursesBySpec } from '../lookups';
describe('Data integrity', () => {
it('has exactly 46 courses', () => {
expect(COURSES.length).toBe(46);
});
it('has exactly 12 elective sets', () => {
expect(ELECTIVE_SETS.length).toBe(12);
});
it('has exactly 14 specializations', () => {
expect(SPECIALIZATIONS.length).toBe(14);
});
it('every course belongs to a valid set and that set references the course', () => {
for (const course of COURSES) {
const set = ELECTIVE_SETS.find((s) => s.id === course.setId);
expect(set, `Course ${course.id} references invalid set ${course.setId}`).toBeDefined();
expect(set!.courseIds).toContain(course.id);
}
});
it('every set course ID references a valid course', () => {
for (const set of ELECTIVE_SETS) {
for (const cid of set.courseIds) {
const course = COURSES.find((c) => c.id === cid);
expect(course, `Set ${set.id} references invalid course ${cid}`).toBeDefined();
}
}
});
it('has exactly 4 required course gates', () => {
const withRequired = SPECIALIZATIONS.filter((s) => s.requiredCourseId);
expect(withRequired.length).toBe(4);
const mapping: Record<string, string> = {};
for (const s of withRequired) {
mapping[s.abbreviation] = s.requiredCourseId!;
}
expect(mapping).toEqual({
SBI: 'spr4-sustainability',
ENT: 'spr4-foundations-entrepreneurship',
EMT: 'sum3-entertainment-media',
BRM: 'fall4-brand-strategy',
});
});
it('has exactly 10 S1 markers and 7 S2 markers for Strategy', () => {
let s1Count = 0;
let s2Count = 0;
for (const course of COURSES) {
for (const q of course.qualifications) {
if (q.specId === 'STR' && q.marker === 'S1') s1Count++;
if (q.specId === 'STR' && q.marker === 'S2') s2Count++;
}
}
expect(s1Count).toBe(10);
expect(s2Count).toBe(7);
});
it('all qualification markers are valid types', () => {
for (const course of COURSES) {
for (const q of course.qualifications) {
expect(['standard', 'S1', 'S2']).toContain(q.marker);
}
}
});
it('Spring Set 1 and Summer Set 1 contain the same three courses', () => {
const spr1Names = coursesBySet['spr1'].map((c) => c.name).sort();
const sum1Names = coursesBySet['sum1'].map((c) => c.name).sort();
expect(spr1Names).toEqual(sum1Names);
});
describe('per-specialization "across sets" counts match reachability table', () => {
// Expected counts: number of distinct sets that have at least one qualifying course
const expectedAcrossSets: Record<string, number> = {
MGT: 11,
STR: 9,
LCM: 9,
FIN: 9,
CRF: 8,
MKT: 7,
BNK: 6,
BRM: 6,
FIM: 6,
MTO: 6,
GLB: 5,
EMT: 4,
ENT: 4,
SBI: 4,
};
for (const [specId, expected] of Object.entries(expectedAcrossSets)) {
it(`${specId} qualifies across ${expected} sets`, () => {
const entries = coursesBySpec[specId] || [];
const courseIds = entries.map((e) => e.courseId);
const setIds = new Set(
courseIds.map((cid) => COURSES.find((c) => c.id === cid)!.setId)
);
expect(setIds.size).toBe(expected);
});
}
});
});

247
app/src/data/courses.ts Normal file
View File

@@ -0,0 +1,247 @@
import type { Course } from './types';
export const COURSES: Course[] = [
// === Spring Elective Set 1 ===
{
id: 'spr1-global-immersion', name: 'Global Immersion Experience II', setId: 'spr1',
qualifications: [{ specId: 'GLB', marker: 'standard' }],
},
{
id: 'spr1-collaboration', name: 'Collaboration, Conflict and Negotiation', setId: 'spr1',
qualifications: [{ specId: 'LCM', marker: 'standard' }, { specId: 'MGT', marker: 'standard' }],
},
{
id: 'spr1-high-stakes', name: 'Conquering High Stakes Communication', setId: 'spr1',
qualifications: [{ specId: 'LCM', marker: 'standard' }],
},
// === Spring Elective Set 2 ===
{
id: 'spr2-consumer-behavior', name: 'Consumer Behavior', setId: 'spr2',
qualifications: [{ specId: 'BRM', marker: 'standard' }, { specId: 'MKT', marker: 'standard' }],
},
{
id: 'spr2-health-medical', name: 'The Business of Health & Medical Care', setId: 'spr2',
qualifications: [{ specId: 'STR', marker: 'S2' }],
},
{
id: 'spr2-human-rights', name: 'Human Rights and Business', setId: 'spr2',
qualifications: [{ specId: 'GLB', marker: 'standard' }, { specId: 'SBI', marker: 'standard' }],
},
{
id: 'spr2-financial-services', name: 'The Financial Services Industry', setId: 'spr2',
qualifications: [
{ specId: 'BNK', marker: 'standard' }, { specId: 'CRF', marker: 'standard' },
{ specId: 'FIN', marker: 'standard' }, { specId: 'FIM', marker: 'standard' },
],
},
// === Spring Elective Set 3 ===
{
id: 'spr3-mergers-acquisitions', name: 'Mergers & Acquisitions', setId: 'spr3',
qualifications: [
{ specId: 'CRF', marker: 'standard' }, { specId: 'FIN', marker: 'standard' },
{ specId: 'LCM', marker: 'standard' }, { specId: 'STR', marker: 'S1' },
],
},
{
id: 'spr3-digital-strategy', name: 'Digital Strategy', setId: 'spr3',
qualifications: [{ specId: 'EMT', marker: 'standard' }, { specId: 'MTO', marker: 'standard' }, { specId: 'STR', marker: 'S2' }],
},
{
id: 'spr3-managing-high-tech', name: 'Managing a High Tech Company: The CEO Perspective', setId: 'spr3',
qualifications: [{ specId: 'EMT', marker: 'standard' }, { specId: 'MGT', marker: 'standard' }, { specId: 'MTO', marker: 'standard' }, { specId: 'STR', marker: 'S2' }],
},
{
id: 'spr3-analytics-ml', name: 'Analytics & Machine Learning for Managers', setId: 'spr3',
qualifications: [{ specId: 'MTO', marker: 'standard' }],
},
// === Spring Elective Set 4 ===
{
id: 'spr4-fintech', name: 'Foundations of Fintech', setId: 'spr4',
qualifications: [{ specId: 'FIN', marker: 'standard' }],
},
{
id: 'spr4-sustainability', name: 'Sustainability for Competitive Advantage', setId: 'spr4',
qualifications: [{ specId: 'LCM', marker: 'standard' }, { specId: 'SBI', marker: 'standard' }, { specId: 'STR', marker: 'S1' }],
},
{
id: 'spr4-pricing', name: 'Pricing', setId: 'spr4',
qualifications: [{ specId: 'BRM', marker: 'standard' }, { specId: 'MKT', marker: 'standard' }, { specId: 'MTO', marker: 'standard' }],
},
{
id: 'spr4-foundations-entrepreneurship', name: 'Foundations of Entrepreneurship', setId: 'spr4',
qualifications: [{ specId: 'ENT', marker: 'standard' }, { specId: 'MGT', marker: 'standard' }, { specId: 'STR', marker: 'S1' }],
},
// === Spring Elective Set 5 ===
{
id: 'spr5-corporate-finance', name: 'Corporate Finance', setId: 'spr5',
qualifications: [{ specId: 'CRF', marker: 'standard' }, { specId: 'FIN', marker: 'standard' }],
},
{
id: 'spr5-consulting-practice', name: 'Consulting Practice: Process and Problem Solving', setId: 'spr5',
qualifications: [{ specId: 'MGT', marker: 'standard' }, { specId: 'STR', marker: 'S1' }],
},
{
id: 'spr5-global-strategy', name: 'Global Strategy', setId: 'spr5',
qualifications: [{ specId: 'GLB', marker: 'standard' }, { specId: 'MGT', marker: 'standard' }, { specId: 'STR', marker: 'S1' }],
},
{
id: 'spr5-customer-insights', name: 'Customer Insights', setId: 'spr5',
qualifications: [{ specId: 'BRM', marker: 'standard' }, { specId: 'MKT', marker: 'standard' }],
},
// === Summer Elective Set 1 ===
{
id: 'sum1-global-immersion', name: 'Global Immersion Experience II', setId: 'sum1',
qualifications: [{ specId: 'GLB', marker: 'standard' }],
},
{
id: 'sum1-collaboration', name: 'Collaboration, Conflict and Negotiation', setId: 'sum1',
qualifications: [{ specId: 'LCM', marker: 'standard' }, { specId: 'MGT', marker: 'standard' }],
},
{
id: 'sum1-high-stakes', name: 'Conquering High Stakes Communication', setId: 'sum1',
qualifications: [{ specId: 'LCM', marker: 'standard' }],
},
// === Summer Elective Set 2 ===
{
id: 'sum2-managing-growing', name: 'Managing Growing Companies', setId: 'sum2',
qualifications: [
{ specId: 'ENT', marker: 'standard' }, { specId: 'LCM', marker: 'standard' },
{ specId: 'MGT', marker: 'standard' }, { specId: 'STR', marker: 'S1' },
],
},
{
id: 'sum2-social-media', name: 'Social Media and Mobile Technology', setId: 'sum2',
qualifications: [{ specId: 'BRM', marker: 'standard' }, { specId: 'EMT', marker: 'standard' }, { specId: 'MKT', marker: 'standard' }],
},
{
id: 'sum2-leading-ai', name: 'Leading in the Age of AI', setId: 'sum2',
qualifications: [{ specId: 'LCM', marker: 'standard' }, { specId: 'MGT', marker: 'standard' }, { specId: 'MTO', marker: 'standard' }],
},
{
id: 'sum2-business-drivers', name: 'Business Drivers', setId: 'sum2',
qualifications: [{ specId: 'STR', marker: 'S1' }],
},
// === Summer Elective Set 3 ===
{
id: 'sum3-valuation', name: 'Valuation', setId: 'sum3',
qualifications: [
{ specId: 'BNK', marker: 'standard' }, { specId: 'CRF', marker: 'standard' },
{ specId: 'FIN', marker: 'standard' }, { specId: 'FIM', marker: 'standard' },
],
},
{
id: 'sum3-entertainment-media', name: 'Entertainment and Media Industries', setId: 'sum3',
qualifications: [{ specId: 'EMT', marker: 'standard' }, { specId: 'MKT', marker: 'standard' }, { specId: 'STR', marker: 'S2' }],
},
{
id: 'sum3-advanced-corporate-strategy', name: 'Advanced Corporate Strategy', setId: 'sum3',
qualifications: [{ specId: 'MGT', marker: 'standard' }, { specId: 'STR', marker: 'S1' }],
},
{
id: 'sum3-power-influence', name: 'Power and Professional Influence', setId: 'sum3',
qualifications: [{ specId: 'LCM', marker: 'standard' }, { specId: 'MGT', marker: 'standard' }],
},
// === Fall Elective Set 1 ===
{
id: 'fall1-operations-strategy', name: 'Operations Strategy', setId: 'fall1',
qualifications: [{ specId: 'MTO', marker: 'standard' }],
},
{
id: 'fall1-private-equity', name: 'Private Equity', setId: 'fall1',
qualifications: [
{ specId: 'BNK', marker: 'standard' }, { specId: 'CRF', marker: 'standard' },
{ specId: 'FIN', marker: 'standard' }, { specId: 'FIM', marker: 'standard' },
{ specId: 'STR', marker: 'S2' },
],
},
{
id: 'fall1-managing-change', name: 'Managing Change', setId: 'fall1',
qualifications: [{ specId: 'LCM', marker: 'standard' }, { specId: 'MGT', marker: 'standard' }, { specId: 'STR', marker: 'S2' }],
},
{
id: 'fall1-social-entrepreneurship', name: 'Social Entrepreneurship', setId: 'fall1',
qualifications: [{ specId: 'ENT', marker: 'standard' }, { specId: 'MGT', marker: 'standard' }, { specId: 'SBI', marker: 'standard' }],
},
// === Fall Elective Set 2 ===
{
id: 'fall2-real-estate', name: 'Real Estate Investment Strategy', setId: 'fall2',
qualifications: [{ specId: 'FIN', marker: 'standard' }, { specId: 'FIM', marker: 'standard' }],
},
{
id: 'fall2-decision-models', name: 'Decision Models and Analytics', setId: 'fall2',
qualifications: [{ specId: 'MGT', marker: 'standard' }, { specId: 'MTO', marker: 'standard' }],
},
{
id: 'fall2-behavioral-finance', name: 'Behavioral Finance and Market Psychology', setId: 'fall2',
qualifications: [
{ specId: 'BNK', marker: 'standard' }, { specId: 'CRF', marker: 'standard' },
{ specId: 'FIN', marker: 'standard' }, { specId: 'FIM', marker: 'standard' },
],
},
{
id: 'fall2-crisis-management', name: 'Crisis Management', setId: 'fall2',
qualifications: [{ specId: 'LCM', marker: 'standard' }, { specId: 'MGT', marker: 'standard' }],
},
// === Fall Elective Set 3 ===
{
id: 'fall3-corporate-governance', name: 'Corporate Governance', setId: 'fall3',
qualifications: [{ specId: 'LCM', marker: 'standard' }, { specId: 'MGT', marker: 'standard' }, { specId: 'SBI', marker: 'standard' }, { specId: 'STR', marker: 'S1' }],
},
{
id: 'fall3-climate-finance', name: 'Climate Finance', setId: 'fall3',
qualifications: [
{ specId: 'BNK', marker: 'standard' }, { specId: 'CRF', marker: 'standard' },
{ specId: 'FIN', marker: 'standard' }, { specId: 'FIM', marker: 'standard' },
{ specId: 'GLB', marker: 'standard' }, { specId: 'SBI', marker: 'standard' },
],
},
{
id: 'fall3-emerging-tech', name: 'Emerging Tech and Business Innovation', setId: 'fall3',
qualifications: [
{ specId: 'BRM', marker: 'standard' }, { specId: 'EMT', marker: 'standard' },
{ specId: 'ENT', marker: 'standard' }, { specId: 'MTO', marker: 'standard' },
{ specId: 'STR', marker: 'S2' },
],
},
{
id: 'fall3-tech-innovation-media', name: 'Technology, Innovation, and Disruption in Media', setId: 'fall3',
qualifications: [
{ specId: 'BRM', marker: 'standard' }, { specId: 'EMT', marker: 'standard' },
{ specId: 'MKT', marker: 'standard' }, { specId: 'MTO', marker: 'standard' },
],
},
// === Fall Elective Set 4 ===
{
id: 'fall4-turnaround', name: 'Turnaround, Restructuring and Distressed Investments', setId: 'fall4',
qualifications: [
{ specId: 'BNK', marker: 'standard' }, { specId: 'CRF', marker: 'standard' },
{ specId: 'FIN', marker: 'standard' }, { specId: 'FIM', marker: 'standard' },
],
},
{
id: 'fall4-financial-services', name: 'The Financial Services Industry', setId: 'fall4',
qualifications: [
{ specId: 'BNK', marker: 'standard' }, { specId: 'CRF', marker: 'standard' },
{ specId: 'FIN', marker: 'standard' }, { specId: 'FIM', marker: 'standard' },
],
},
{
id: 'fall4-game-theory', name: 'Game Theory', setId: 'fall4',
qualifications: [{ specId: 'MGT', marker: 'standard' }, { specId: 'STR', marker: 'S1' }],
},
{
id: 'fall4-brand-strategy', name: 'Brand Strategy', setId: 'fall4',
qualifications: [{ specId: 'BRM', marker: 'standard' }, { specId: 'MKT', marker: 'standard' }],
},
];

View File

@@ -0,0 +1,52 @@
import type { ElectiveSet } from './types';
export const ELECTIVE_SETS: ElectiveSet[] = [
{
id: 'spr1', name: 'Spring Elective Set 1', term: 'Spring',
courseIds: ['spr1-global-immersion', 'spr1-collaboration', 'spr1-high-stakes'],
},
{
id: 'spr2', name: 'Spring Elective Set 2', term: 'Spring',
courseIds: ['spr2-consumer-behavior', 'spr2-health-medical', 'spr2-human-rights', 'spr2-financial-services'],
},
{
id: 'spr3', name: 'Spring Elective Set 3', term: 'Spring',
courseIds: ['spr3-mergers-acquisitions', 'spr3-digital-strategy', 'spr3-managing-high-tech', 'spr3-analytics-ml'],
},
{
id: 'spr4', name: 'Spring Elective Set 4', term: 'Spring',
courseIds: ['spr4-fintech', 'spr4-sustainability', 'spr4-pricing', 'spr4-foundations-entrepreneurship'],
},
{
id: 'spr5', name: 'Spring Elective Set 5', term: 'Spring',
courseIds: ['spr5-corporate-finance', 'spr5-consulting-practice', 'spr5-global-strategy', 'spr5-customer-insights'],
},
{
id: 'sum1', name: 'Summer Elective Set 1', term: 'Summer',
courseIds: ['sum1-global-immersion', 'sum1-collaboration', 'sum1-high-stakes'],
},
{
id: 'sum2', name: 'Summer Elective Set 2', term: 'Summer',
courseIds: ['sum2-managing-growing', 'sum2-social-media', 'sum2-leading-ai', 'sum2-business-drivers'],
},
{
id: 'sum3', name: 'Summer Elective Set 3', term: 'Summer',
courseIds: ['sum3-valuation', 'sum3-entertainment-media', 'sum3-advanced-corporate-strategy', 'sum3-power-influence'],
},
{
id: 'fall1', name: 'Fall Elective Set 1', term: 'Fall',
courseIds: ['fall1-operations-strategy', 'fall1-private-equity', 'fall1-managing-change', 'fall1-social-entrepreneurship'],
},
{
id: 'fall2', name: 'Fall Elective Set 2', term: 'Fall',
courseIds: ['fall2-real-estate', 'fall2-decision-models', 'fall2-behavioral-finance', 'fall2-crisis-management'],
},
{
id: 'fall3', name: 'Fall Elective Set 3', term: 'Fall',
courseIds: ['fall3-corporate-governance', 'fall3-climate-finance', 'fall3-emerging-tech', 'fall3-tech-innovation-media'],
},
{
id: 'fall4', name: 'Fall Elective Set 4', term: 'Fall',
courseIds: ['fall4-turnaround', 'fall4-financial-services', 'fall4-game-theory', 'fall4-brand-strategy'],
},
];

40
app/src/data/lookups.ts Normal file
View File

@@ -0,0 +1,40 @@
import { COURSES } from './courses';
import { ELECTIVE_SETS } from './electiveSets';
import type { Course, Qualification } from './types';
// Courses indexed by set ID
export const coursesBySet: Record<string, Course[]> = {};
for (const set of ELECTIVE_SETS) {
coursesBySet[set.id] = set.courseIds.map(
(cid) => COURSES.find((c) => c.id === cid)!
);
}
// Qualifications indexed by course ID
export const qualificationsByCourse: Record<string, Qualification[]> = {};
for (const course of COURSES) {
qualificationsByCourse[course.id] = course.qualifications;
}
// Course IDs indexed by specialization ID (with marker info)
export const coursesBySpec: Record<string, { courseId: string; marker: Qualification['marker'] }[]> = {};
for (const course of COURSES) {
for (const q of course.qualifications) {
if (!coursesBySpec[q.specId]) {
coursesBySpec[q.specId] = [];
}
coursesBySpec[q.specId].push({ courseId: course.id, marker: q.marker });
}
}
// Course lookup by ID
export const courseById: Record<string, Course> = {};
for (const course of COURSES) {
courseById[course.id] = course;
}
// Set ID lookup by course ID
export const setIdByCourse: Record<string, string> = {};
for (const course of COURSES) {
setIdByCourse[course.id] = course.setId;
}

View File

@@ -0,0 +1,18 @@
import type { Specialization } from './types';
export const SPECIALIZATIONS: Specialization[] = [
{ id: 'BNK', name: 'Banking', abbreviation: 'BNK' },
{ id: 'BRM', name: 'Brand Management', abbreviation: 'BRM', requiredCourseId: 'fall4-brand-strategy' },
{ id: 'CRF', name: 'Corporate Finance', abbreviation: 'CRF' },
{ id: 'EMT', name: 'Entertainment, Media, and Technology', abbreviation: 'EMT', requiredCourseId: 'sum3-entertainment-media' },
{ id: 'ENT', name: 'Entrepreneurship and Innovation', abbreviation: 'ENT', requiredCourseId: 'spr4-foundations-entrepreneurship' },
{ id: 'FIN', name: 'Finance', abbreviation: 'FIN' },
{ id: 'FIM', name: 'Financial Instruments and Markets', abbreviation: 'FIM' },
{ id: 'GLB', name: 'Global Business', abbreviation: 'GLB' },
{ id: 'LCM', name: 'Leadership and Change Management', abbreviation: 'LCM' },
{ id: 'MGT', name: 'Management', abbreviation: 'MGT' },
{ id: 'MKT', name: 'Marketing', abbreviation: 'MKT' },
{ id: 'MTO', name: 'Management of Technology and Operations', abbreviation: 'MTO' },
{ id: 'SBI', name: 'Sustainable Business and Innovation', abbreviation: 'SBI', requiredCourseId: 'spr4-sustainability' },
{ id: 'STR', name: 'Strategy', abbreviation: 'STR' },
];

40
app/src/data/types.ts Normal file
View File

@@ -0,0 +1,40 @@
export type Term = 'Spring' | 'Summer' | 'Fall';
export type MarkerType = 'standard' | 'S1' | 'S2';
export interface ElectiveSet {
id: string;
name: string;
term: Term;
courseIds: string[];
}
export interface Qualification {
specId: string;
marker: MarkerType;
}
export interface Course {
id: string;
name: string;
setId: string;
qualifications: Qualification[];
}
export interface Specialization {
id: string;
name: string;
abbreviation: string;
requiredCourseId?: string;
}
export type SpecStatus = 'achieved' | 'achievable' | 'missing_required' | 'unreachable';
export interface AllocationResult {
achieved: string[];
allocations: Record<string, Record<string, number>>; // courseId -> specId -> credits
statuses: Record<string, SpecStatus>;
upperBounds: Record<string, number>;
}
export type OptimizationMode = 'maximize-count' | 'priority-order';