diff --git a/entrypoints/degree-audit/providers/audit-provider.tsx b/entrypoints/degree-audit/providers/audit-provider.tsx
index a5f5782..3245b1e 100644
--- a/entrypoints/degree-audit/providers/audit-provider.tsx
+++ b/entrypoints/degree-audit/providers/audit-provider.tsx
@@ -7,6 +7,7 @@ import {
Course,
CourseId,
CurrentAuditProgress,
+ RequirementRule,
StringSemester,
} from "@/lib/general-types";
import { createContext, useContext, useEffect, useMemo, useState } from "react";
@@ -14,6 +15,13 @@ import LoadingPage from "../components/loading-page";
// Context for sharing audit data betw sidebar and main
type SemesterInfo = Record
;
+type RequirementRuleLike = Omit & {
+ courses?: Array;
+};
+type AuditRequirementLike = Omit & {
+ rule?: RequirementRuleLike[];
+ rules?: RequirementRuleLike[];
+};
interface AuditContextType {
sections: AuditRequirement[];
@@ -35,6 +43,44 @@ interface AuditContextType {
const AuditContext = createContext(null);
+function normalizeCourseDict(
+ courses: Record | Course[] | null | undefined,
+): Record {
+ if (!courses) {
+ return {};
+ }
+
+ if (Array.isArray(courses)) {
+ return courses.reduce(
+ (acc, course) => ({
+ ...acc,
+ [course.id]: course,
+ }),
+ {} as Record,
+ );
+ }
+
+ return courses;
+}
+
+function normalizeRequirements(
+ requirements: AuditRequirementLike[],
+): AuditRequirement[] {
+ return requirements.map((section) => ({
+ ...section,
+ rule: (section.rule ?? section.rules ?? []).map((rule) => ({
+ ...rule,
+ courses: (rule.courses ?? [])
+ .map((courseRef) =>
+ typeof courseRef === "object" && courseRef !== null
+ ? courseRef.id
+ : courseRef,
+ )
+ .filter(Boolean) as CourseId[],
+ })),
+ }));
+}
+
export const AuditContextProvider = ({
children,
}: {
@@ -106,17 +152,10 @@ export const AuditContextProvider = ({
// Load requirements from cache
const cached = await getAuditData(currentAuditId!);
if (cached) {
- setSections(
- cached.requirements.map((section) => ({
- ...section,
- rules: section.rule.map((rule) => ({
- ...rule,
- courses: rule.courses,
- })),
- })),
- );
+ const normalizedCourses = normalizeCourseDict(cached.courses);
+ setSections(normalizeRequirements(cached.requirements));
console.log("[Main] courses", cached.courses);
- setCourseDict(cached.courses);
+ setCourseDict(normalizedCourses);
} else console.warn(`[Main] Audit ${currentAuditId} not in cache.`);
setLoaded(true);
diff --git a/lib/audit-calculations.ts b/lib/audit-calculations.ts
index ca0b524..e4b8673 100644
--- a/lib/audit-calculations.ts
+++ b/lib/audit-calculations.ts
@@ -24,11 +24,15 @@ export function calculateWeightedDegreeCompletion(
results.sections.push(sectionProgress);
});
- results.total.current = results.sections.reduce(
+ // Only include non-GPA sections in completion totals
+ const nonGPASections = results.sections.filter(
+ (section) => !section.title.toLowerCase().includes("gpa"),
+ );
+ results.total.current = nonGPASections.reduce(
(acc, section) => acc + section.progress.current,
0,
);
- results.total.total = results.sections.reduce(
+ results.total.total = nonGPASections.reduce(
(acc, section) => acc + section.progress.total,
0,
);
diff --git a/lib/backend/db-seeder.ts b/lib/backend/db-seeder.ts
index 8ba64aa..277b907 100644
--- a/lib/backend/db-seeder.ts
+++ b/lib/backend/db-seeder.ts
@@ -1,8 +1,9 @@
import coursesData from "@/assets/ut-courses.json";
+import type { CatalogCourse } from "../general-types";
import { db } from "./db";
const STORAGE_KEY = "db_seed_version";
-const CURRENT_VERSION = "20259-v1"; // Incremented manually when json changes
+const CURRENT_VERSION = "20269-v3"; // Incremented manually when json or DB storage format changes
export async function seedDatabase() {
// Check if we've already seeded this version
@@ -14,10 +15,7 @@ export async function seedDatabase() {
console.log(`[DB] Seeding database (Version: ${CURRENT_VERSION})...`);
try {
- const courses = (coursesData as any[]).map((c) => ({
- ...c,
- id: c.id ?? crypto.randomUUID(),
- }));
+ const courses = coursesData as CatalogCourse[];
await db.courses.clear();
await db.courses.bulkPut(courses);
localStorage.setItem(STORAGE_KEY, CURRENT_VERSION);
diff --git a/lib/backend/db.ts b/lib/backend/db.ts
index dcb727e..f2d4761 100644
--- a/lib/backend/db.ts
+++ b/lib/backend/db.ts
@@ -1,12 +1,15 @@
import Dexie from "dexie";
-import { Course } from "../general-types";
+import type { CatalogCourse } from "../general-types";
+
export class UTDatabase extends Dexie {
- courses!: Dexie.Table;
+ courses!: Dexie.Table;
constructor() {
super("UTCoursesDB");
- this.version(2).stores({
- courses: "uniqueId, department, number, fullName, semester.code",
+ // IndexedDB persists the full catalog record; this schema only defines indexes.
+ this.version(3).stores({
+ courses:
+ "uniqueId, [department+number], fullName, courseName, department, number, creditHours, status, isReserved, instructionMode, *flags, *core, url, scrapedAt, semester.code",
});
}
}
diff --git a/lib/general-types.ts b/lib/general-types.ts
index 5fdde45..e9dfe8a 100644
--- a/lib/general-types.ts
+++ b/lib/general-types.ts
@@ -103,6 +103,57 @@ export type Course = {
type: CourseCompletionMethod;
};
+/**
+ * A course instructor entry from the UT course catalog export.
+ */
+export type CatalogInstructor = {
+ fullName: string;
+ firstName: string;
+ lastName: string;
+ middleInitial?: string;
+};
+
+/**
+ * A single meeting time/location entry from the UT course catalog export.
+ */
+export type CatalogCourseScheduleEntry = {
+ days: string;
+ hours: string;
+ location: string;
+};
+
+/**
+ * Semester metadata from the UT course catalog export.
+ */
+export type CatalogSemester = {
+ year: Year;
+ season: SemesterSeason;
+ code: string;
+};
+
+/**
+ * The full catalog-course JSON shape stored in assets/ut-courses.json and persisted to IndexedDB.
+ */
+export type CatalogCourse = {
+ uniqueId: number;
+ fullName: string;
+ courseName: string;
+ department: string;
+ number: string;
+ creditHours: number;
+ status: string;
+ isReserved: boolean;
+ instructionMode: string;
+ instructors: CatalogInstructor[];
+ schedule: CatalogCourseScheduleEntry[];
+ flags: string[];
+ core: string[];
+ url: string;
+ description: string[];
+ semester: CatalogSemester;
+ scrapedAt: number;
+};
+
export interface DegreeAuditCardProps {
title?: string;
majors?: string[];