Skip to content

Commit 6e84419

Browse files
committed
audit flow code cleaning
1 parent 8782f18 commit 6e84419

12 files changed

Lines changed: 250 additions & 307 deletions

domain/course.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,34 @@ export function getCurrentSemester(date = new Date()): StringSemester {
1313
if (month >= 8) return `Fall ${year}`;
1414
return `Summer ${year}`;
1515
}
16+
17+
/** Chronological ordering comparator for two semesters (earliest first). */
18+
export function sortSemesters(
19+
a: StringSemester,
20+
b: StringSemester,
21+
): number {
22+
const seasonRank = (season: string) =>
23+
season === "Spring" ? 1 : season === "Summer" ? 2 : 3;
24+
const [seasonA, yearA] = a.split(" ");
25+
const [seasonB, yearB] = b.split(" ");
26+
27+
const yearDiff = Number(yearA) - Number(yearB);
28+
if (yearDiff !== 0) return yearDiff;
29+
return seasonRank(seasonA) - seasonRank(seasonB);
30+
}
31+
32+
/** The semester immediately following the given one. */
33+
export function nextSemester(semester: StringSemester): StringSemester {
34+
const [season, year] = semester.split(" ") as [SemesterSeason, Year];
35+
switch (season) {
36+
case "Spring":
37+
return `Summer ${year}`;
38+
case "Summer":
39+
return `Fall ${year}`;
40+
case "Fall":
41+
return `Spring ${Number(year) + 1}`;
42+
}
43+
}
1644
export type CourseCompletionMethod =
1745
| "Transfer"
1846
| "Credit By Exam"

features/audit/audit-provider.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
calculateWeightedDegreeCompletion,
1818
getCompositeAuditRequirements,
1919
} from "@/lib/audit-calculations";
20+
import { formatMajorLabel } from "@/lib/utils";
2021
import {
2122
getAuditData,
2223
getAuditHistory,
@@ -39,6 +40,7 @@ interface AuditContextValue {
3940
history: AuditHistoryData;
4041
currentAuditId: string;
4142
currentAudit: AuditHistoryEntry;
43+
currentAuditName: string;
4244
setCurrentAuditId: (id: string) => void;
4345
renameAuditTitle: (auditId: string, title: string) => Promise<boolean>;
4446
progresses: CurrentAuditProgress;
@@ -78,11 +80,15 @@ export function AuditContextProvider({
7880
const [auditData, setAuditData] = useState<CachedAuditData | null>(null);
7981
const [history, setHistory] = useState<AuditHistoryData | null>(null);
8082

81-
const currentAudit = useMemo(
83+
const currentAudit = useMemo<AuditHistoryEntry>(
8284
() =>
8385
history?.audits.find((audit) => audit.auditId === currentAuditId) ?? {},
8486
[currentAuditId, history],
8587
);
88+
const currentAuditName =
89+
currentAudit.majors?.map(formatMajorLabel).join("; ") ??
90+
currentAudit.title ??
91+
"Degree Requirements";
8692
const compositeAuditData = useMemo<CompositeAuditData>(
8793
() =>
8894
auditData && currentAuditId
@@ -223,6 +229,7 @@ export function AuditContextProvider({
223229
semesters,
224230
currentAuditId,
225231
currentAudit,
232+
currentAuditName,
226233
setCurrentAuditId: (id) => {
227234
window.history.pushState({}, "", `?auditId=${id}`);
228235
setCurrentAuditIdState(id);

features/audit/components/audit-card.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ const DegreeAuditCard: React.FC<DegreeAuditCardProps> = ({
8080
e.currentTarget.blur();
8181
}
8282
if (e.key === "Escape") {
83-
setDraftTitle(title);
83+
setDraftTitle(title ?? "");
8484
setIsEditing(false);
8585
}
8686
}}

features/audit/components/degree-completion-donut.tsx

Lines changed: 21 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,78 +1,44 @@
11
import { HStack, VStack } from "@/components/ui/stack";
22
import MultiDonutGraph, { Bar, GraphStyleProps } from "./graph";
3-
import type { PlannableProgress } from "@/domain/progress";
4-
import { CATEGORY_COLORS, formatMajorLabel } from "@/lib/utils";
3+
import { CATEGORY_COLORS } from "@/lib/utils";
54
import { useAuditContext } from "../audit-provider";
5+
import { groupAuditSections } from "../section-groups";
66

7-
const isStandaloneSection = (title: string) => {
8-
const t = title.toLowerCase();
9-
return t.includes("core") || t.includes("credit");
10-
};
11-
12-
const isPreUnifiedSection = (title: string) =>
13-
title.toLowerCase().includes("core");
14-
const isPostUnifiedSection = (title: string) =>
15-
title.toLowerCase().includes("credit");
16-
17-
function buildDonutBars(
18-
sections: { title: string; progress: PlannableProgress }[],
19-
unifiedTitle: string,
20-
): Bar[] {
21-
const nonGPA = sections.filter((s) => !s.title.toLowerCase().includes("gpa"));
22-
const preUnified = nonGPA.filter((s) => isPreUnifiedSection(s.title));
23-
const postUnified = nonGPA.filter((s) => isPostUnifiedSection(s.title));
24-
const unified = nonGPA.filter((s) => !isStandaloneSection(s.title));
7+
const DegreeCompletionDonut = (styleProps: GraphStyleProps) => {
8+
const { progresses, sections, currentAudit, currentAuditName } =
9+
useAuditContext();
10+
const { pre, unified, post } = groupAuditSections(sections, progresses);
2511

2612
const bars: Bar[] = [];
2713

28-
preUnified.forEach((section, idx) => {
29-
if (section.progress.total > 0) {
30-
bars.push({
31-
title: section.title,
32-
color: CATEGORY_COLORS[idx % CATEGORY_COLORS.length].rgb,
33-
percentage: section.progress,
34-
});
35-
}
14+
pre.forEach((section, idx) => {
15+
bars.push({
16+
title: section.title,
17+
color: CATEGORY_COLORS[idx % CATEGORY_COLORS.length].rgb,
18+
percentage: section.progress,
19+
});
3620
});
3721

38-
const unifiedTotal = unified.reduce((sum, s) => sum + s.progress.total, 0);
39-
if (unifiedTotal > 0) {
22+
if (unified.length > 0) {
4023
bars.push({
41-
title: unifiedTitle,
24+
title: currentAuditName,
4225
color: CATEGORY_COLORS[5].rgb,
4326
percentage: {
4427
current: unified.reduce((sum, s) => sum + s.progress.current, 0),
4528
planned: unified.reduce((sum, s) => sum + s.progress.planned, 0),
46-
total: unifiedTotal,
29+
total: unified.reduce((sum, s) => sum + s.progress.total, 0),
4730
},
4831
});
4932
}
5033

51-
postUnified.forEach((section, idx) => {
52-
if (section.progress.total > 0) {
53-
bars.push({
54-
title: section.title,
55-
color:
56-
CATEGORY_COLORS[(preUnified.length + idx) % CATEGORY_COLORS.length]
57-
.rgb,
58-
percentage: section.progress,
59-
});
60-
}
34+
post.forEach((section, idx) => {
35+
bars.push({
36+
title: section.title,
37+
color: CATEGORY_COLORS[(pre.length + idx) % CATEGORY_COLORS.length].rgb,
38+
percentage: section.progress,
39+
});
6140
});
6241

63-
return bars;
64-
}
65-
66-
const DegreeCompletionDonut = (styleProps: GraphStyleProps) => {
67-
const { progresses, history, currentAuditId } = useAuditContext();
68-
const currentAudit = history?.audits?.find(
69-
(a, i) => (a.auditId || String(i)) === currentAuditId,
70-
);
71-
const unifiedTitle =
72-
currentAudit?.majors?.map(formatMajorLabel).join("; ") ??
73-
"Degree Requirements";
74-
const bars = buildDonutBars(progresses.sections, unifiedTitle);
75-
7642
const overallPercentage =
7743
(currentAudit?.percentage ??
7844
Math.round((progresses.total.current / progresses.total.total) * 100)) ||

features/audit/components/requirement-breakdown.tsx

Lines changed: 79 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -277,39 +277,48 @@ const ProgressBar = ({
277277
);
278278
};
279279

280-
type RequirementBreakdownProps = {
280+
type CollapsibleProgressCardProps = {
281281
title: string;
282-
hours: Progress;
283-
requirements: RequirementRule[];
282+
current: number;
283+
total: number;
284+
unit: ProgressLabelUnit;
284285
colorIndex?: number;
286+
children: React.ReactNode;
285287
};
286-
const RequirementBreakdown = (props: RequirementBreakdownProps) => {
287-
const { title, hours, requirements, colorIndex = 0 } = props;
288+
289+
// Shared card shell: colored left border, a collapsible header with title,
290+
// progress bar and summary, plus children rendered only while expanded.
291+
const CollapsibleProgressCard = ({
292+
title,
293+
current,
294+
total,
295+
unit,
296+
colorIndex = 0,
297+
children,
298+
}: CollapsibleProgressCardProps) => {
288299
const [isOpen, setIsOpen] = useState(true);
289300
const borderColor = CATEGORY_COLORS[colorIndex % CATEGORY_COLORS.length];
290-
const progressUnit = getSharedProgressUnit(requirements);
291301

292302
return (
293303
<div
294304
className="w-full bg-background rounded-md border border-gray-200 overflow-hidden border-l-4"
295305
style={{ borderLeftColor: borderColor.tailwind }}
296306
>
297-
{/* Main header */}
298307
<button
299308
className="w-full p-4 flex items-center justify-between hover:bg-hover-bg transition-colors bg-background"
300309
onClick={() => setIsOpen(!isOpen)}
301310
>
302311
<VStack gap={2}>
303312
<span className="font-bold text-base text-text">{title}</span>
304313
<ProgressBar
305-
current={hours.current}
306-
total={hours.total}
314+
current={current}
315+
total={total}
307316
colorIndex={colorIndex}
308317
/>
309318
</VStack>
310319
<HStack y="middle" gap={2}>
311320
<span className="text-text font-medium text-sm">
312-
{formatProgressSummary(hours.current, hours.total, progressUnit)}
321+
{formatProgressSummary(current, total, unit)}
313322
</span>
314323
{isOpen ? (
315324
<CaretUpIcon className="w-5 h-5 text-text" weight="bold" />
@@ -318,26 +327,44 @@ const RequirementBreakdown = (props: RequirementBreakdownProps) => {
318327
)}
319328
</HStack>
320329
</button>
321-
322-
{/* Expanded content */}
323-
{isOpen && (
324-
<div className="bg-background">
325-
{/* Requirement rows */}
326-
<div className="px-4 py-4">
327-
{requirements.map((requirement, idx) => (
328-
<RequirementRow
329-
key={`${requirement.text.slice(0, 20)}-${idx}`}
330-
requirement={requirement}
331-
requirementTitle={title}
332-
/>
333-
))}
334-
</div>
335-
</div>
336-
)}
330+
{isOpen && children}
337331
</div>
338332
);
339333
};
340334

335+
type RequirementBreakdownProps = {
336+
title: string;
337+
hours: Progress;
338+
requirements: RequirementRule[];
339+
colorIndex?: number;
340+
};
341+
const RequirementBreakdown = ({
342+
title,
343+
hours,
344+
requirements,
345+
colorIndex = 0,
346+
}: RequirementBreakdownProps) => (
347+
<CollapsibleProgressCard
348+
title={title}
349+
current={hours.current}
350+
total={hours.total}
351+
unit={getSharedProgressUnit(requirements)}
352+
colorIndex={colorIndex}
353+
>
354+
<div className="bg-background">
355+
<div className="px-4 py-4">
356+
{requirements.map((requirement, idx) => (
357+
<RequirementRow
358+
key={`${requirement.text.slice(0, 20)}-${idx}`}
359+
requirement={requirement}
360+
requirementTitle={title}
361+
/>
362+
))}
363+
</div>
364+
</div>
365+
</CollapsibleProgressCard>
366+
);
367+
341368
export default RequirementBreakdown;
342369

343370
type UnifiedDegreeCardSection = {
@@ -355,8 +382,6 @@ export const UnifiedDegreeCard = ({
355382
degreeTitle,
356383
sections,
357384
}: UnifiedDegreeCardProps) => {
358-
const [isOpen, setIsOpen] = useState(true);
359-
360385
const totalCurrent = sections.reduce((sum, s) => sum + s.hours.current, 0);
361386
const totalTotal = sections.reduce((sum, s) => sum + s.hours.total, 0);
362387
const totalProgressUnit = getSharedProgressUnit(
@@ -365,57 +390,32 @@ export const UnifiedDegreeCard = ({
365390
const greenColor = CATEGORY_COLORS[5]; // green
366391

367392
return (
368-
<div
369-
className="w-full bg-background rounded-md border border-gray-200 overflow-hidden border-l-4"
370-
style={{ borderLeftColor: greenColor.tailwind }}
393+
<CollapsibleProgressCard
394+
title={degreeTitle}
395+
current={totalCurrent}
396+
total={totalTotal}
397+
unit={totalProgressUnit}
398+
colorIndex={5}
371399
>
372-
{/* Header */}
373-
<button
374-
className="w-full p-4 flex items-center justify-between hover:bg-hover-bg transition-colors bg-background"
375-
onClick={() => setIsOpen(!isOpen)}
376-
>
377-
<VStack gap={2}>
378-
<span className="font-bold text-base text-text">{degreeTitle}</span>
379-
<ProgressBar
380-
current={totalCurrent}
381-
total={totalTotal}
382-
colorIndex={5}
383-
/>
384-
</VStack>
385-
<HStack y="middle" gap={2}>
386-
<span className="text-text font-medium text-sm">
387-
{formatProgressSummary(totalCurrent, totalTotal, totalProgressUnit)}
388-
</span>
389-
{isOpen ? (
390-
<CaretUpIcon className="w-5 h-5 text-text" weight="bold" />
391-
) : (
392-
<CaretDownIcon className="w-5 h-5 text-text" weight="bold" />
393-
)}
394-
</HStack>
395-
</button>
396-
397-
{/* Expanded: sections with green labels */}
398-
{isOpen && (
399-
<div className="bg-background px-4 pt-4 pb-4">
400-
{sections.map((section, idx) => (
401-
<div key={section.title || idx} className={idx > 0 ? "mt-4" : ""}>
402-
<span
403-
className="text-sm font-semibold mb-4 block"
404-
style={{ color: greenColor.tailwind }}
405-
>
406-
{section.title}
407-
</span>
408-
{section.requirements.map((requirement, rIdx) => (
409-
<RequirementRow
410-
key={`${requirement.text.slice(0, 20)}-${rIdx}`}
411-
requirement={requirement}
412-
requirementTitle={section.title}
413-
/>
414-
))}
415-
</div>
416-
))}
417-
</div>
418-
)}
419-
</div>
400+
<div className="bg-background px-4 pt-4 pb-4">
401+
{sections.map((section, idx) => (
402+
<div key={section.title || idx} className={idx > 0 ? "mt-4" : ""}>
403+
<span
404+
className="text-sm font-semibold mb-4 block"
405+
style={{ color: greenColor.tailwind }}
406+
>
407+
{section.title}
408+
</span>
409+
{section.requirements.map((requirement, rIdx) => (
410+
<RequirementRow
411+
key={`${requirement.text.slice(0, 20)}-${rIdx}`}
412+
requirement={requirement}
413+
requirementTitle={section.title}
414+
/>
415+
))}
416+
</div>
417+
))}
418+
</div>
419+
</CollapsibleProgressCard>
420420
);
421421
};

0 commit comments

Comments
 (0)