planModel.js Core logic

Plan display helpers and PDF export. Provides functions for computing semester credit totals, retrieving ordered course lists, and finding edge connections. Also contains the async PDF export that builds a print-ready HTML document.

Parent
Called by
PlannerContext.jsx, Header.jsx (exportReport)
Note
exportReport is async but still "pure" in spirit — it takes all data as parameters

Exported functions

getSemSH(semId, placements, courseMap, shOverrides?, bonusSH?) → number
Sums credit hours for all courses in a semester. Respects shOverrides for variable-credit courses. Adds bonusSH[semId] for AP/transfer credit. Used in semester row headers.
Returns: number — total SH for the semester
getOrderedCourses(semId, placements, semOrders, courseMap) → string[]
Returns course IDs for a semester in the user's preferred drag order. Starts with courses in semOrders[semId] (filtered to only those still in this semester), then appends any remaining courses not in the order array. Handles the case where a user places a new course without any explicit order for it.
Returns: string[] — course IDs in display order
getConnections(courseId, edges) → Edge[]
Returns all edges where the given course is either the from or to end. Used by RelationLines when a course is selected — it needs all related courses to draw lines to/from them. Each edge has { from, to, type }.
Returns: Edge[] — all edges touching this course
exportReport(placements, courseMap, currentSemId, SEMESTERS, SEM_INDEX, gradInfo, workPl, internPl) → void (async)
Builds a complete print-ready HTML document and opens it in a new browser window, then triggers the browser print dialog. No server, no third-party library — entirely browser-native.

Document contents:
  • Page 1: NUPath coverage grid (12 attributes, WF excluded), major requirements tree, minor 1 and 2 trees, substitution list, total SH done/planned/required
  • Page 2+: per-semester blocks — co-op/internship rows (company logo + role) or course rows (subject pill, NUPath badges, SH). Past semesters marked "completed", current "in progress".
Async because it awaits major/minor JSON fetches. All HTML generation is synchronous once data is loaded. Uses -webkit-print-color-adjust: exact to preserve colors in print.

PDF export mechanism

sequenceDiagram participant H as Header.jsx participant PM as planModel.js participant ML as majorLoader / minorLoader participant B as Browser H->>PM: exportReport(placements, gradInfo, workPl, internPl) PM->>ML: loadMajor() + loadMinor1() + loadMinor2() in parallel ML-->>PM: requirement trees PM->>PM: build full HTML string in memory note over PM: page 1: NUPath grid + req trees
page 2+: semester schedule blocks
page-break CSS between pages PM->>B: new Blob([html], {type:"text/html"}) PM->>B: URL.createObjectURL(blob) → blobUrl PM->>B: window.open(blobUrl, "_blank") → w alt pop-up blocked (w === null) PM->>B: alert("Allow pop-ups…") PM->>B: URL.revokeObjectURL(blobUrl) else pop-up allowed PM->>B: setTimeout(() => w.print(), 400ms) note over B: 400ms lets new tab finish rendering B->>B: print dialog opens B-->>PM: w.onafterprint fires PM->>B: w.close() PM->>B: URL.revokeObjectURL(blobUrl) end
The 400 ms delay is not a debounce — it is a rendering buffer. The blob URL is always revoked after use to release memory.

Company logo (CompanyLogo.jsx)

Co-op and internship blocks display the company's logo. The logo is fetched from Google's S2 favicon service — no API key required, always cross-origin:

https://www.google.com/s2/favicons?domain=microsoft.com&sz=128

The CompanyLogo component starts with visibility: hidden (not display: none) so the onLoad event still fires. It becomes visible on successful load and stays hidden on error — so a missing or unknown domain simply shows nothing, no broken image icon. The parent must supply key={domain} so React remounts the component (and re-fetches) when the domain changes.

The same URL is used in the PDF export (via pdfFaviconUrl() in planModel.js) with an inline onerror="this.style.display='none'" fallback.