From 756321f7dfa6f2a2397d6b9d0227465b0381213a Mon Sep 17 00:00:00 2001 From: Clyde Kallahan Date: Wed, 13 May 2026 18:32:14 -0400 Subject: [PATCH 1/9] fix: export sessionutil funcs --- .../src/pages/class-detail/session-utils.ts | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/class-detail/session-utils.ts b/frontend/src/pages/class-detail/session-utils.ts index 8efad20e6..28c616c2f 100644 --- a/frontend/src/pages/class-detail/session-utils.ts +++ b/frontend/src/pages/class-detail/session-utils.ts @@ -28,11 +28,45 @@ interface RescheduleLink { startTime?: string; } -function parseLocalDate(dateStr: string): Date { +export function parseLocalDate(dateStr: string): Date { const [y, m, d] = dateStr.split('-').map(Number); return new Date(y, m - 1, d); } +export function formatShortDate(dateStr: string): string { + const d = parseLocalDate(dateStr); + return d.toLocaleDateString('en-US', { + weekday: 'short', + month: 'short', + day: 'numeric' + }); +} + +export interface SessionPayload { + date: string; + dateLabel: string; + eventId: number; + classTime: string; + dateObj: Date; + dayName: string; +} + +export function buildSessionPayload(session: SessionDisplay): SessionPayload { + return { + date: session.instance.date, + dateLabel: session.dateObj.toLocaleDateString('en-US', { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric' + }), + eventId: session.instance.event_id ?? session.instance.id, + classTime: session.instance.class_time, + dateObj: session.dateObj, + dayName: session.dayName + }; +} + function parseOverrideDate(rrule: string): string | null { const match = /DTSTART[^:]*:(\d{4})(\d{2})(\d{2})/.exec(rrule); if (!match) return null; From 72fd0d6230cdac26cc99af73cb4d3dc8ead13ffb Mon Sep 17 00:00:00 2001 From: Clyde Kallahan Date: Thu, 14 May 2026 10:55:00 -0400 Subject: [PATCH 2/9] fix: split 3 helpers to lib and session-utils --- .../schedule/RescheduleSessionModal.tsx | 19 +-------- .../src/components/shared/StatusBadge.tsx | 42 +++++++------------ frontend/src/lib/formatters.ts | 16 +++++++ .../src/pages/class-detail/SessionsTab.tsx | 42 +++---------------- frontend/src/pages/programs/ClassEvents.tsx | 23 +++------- 5 files changed, 44 insertions(+), 98 deletions(-) diff --git a/frontend/src/components/schedule/RescheduleSessionModal.tsx b/frontend/src/components/schedule/RescheduleSessionModal.tsx index ca7106f22..f3ccf07ba 100644 --- a/frontend/src/components/schedule/RescheduleSessionModal.tsx +++ b/frontend/src/components/schedule/RescheduleSessionModal.tsx @@ -15,6 +15,7 @@ import { SelectTrigger, SelectValue } from '@/components/ui/select'; +import { toDateInput, toTimeInput, formatDurationStr } from '@/lib/formatters'; interface RescheduleSessionModalProps { open: boolean; @@ -24,22 +25,6 @@ interface RescheduleSessionModalProps { onSuccess: () => void; } -function toDateInput(d: Date): string { - return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; -} - -function toTimeInput(d: Date): string { - return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; -} - -function formatDuration(startTime: string, endTime: string): string { - const [sh, sm] = startTime.split(':').map(Number); - const [eh, em] = endTime.split(':').map(Number); - const totalMin = (eh ?? 0) * 60 + (em ?? 0) - ((sh ?? 0) * 60 + (sm ?? 0)); - if (totalMin <= 0) return '0h0m0s'; - return `${Math.floor(totalMin / 60)}h${totalMin % 60}m0s`; -} - export function RescheduleSessionModal({ open, onOpenChange, @@ -76,7 +61,7 @@ export function RescheduleSessionModal({ const effectiveStart = startTime || toTimeInput(event.start); const effectiveEnd = endTime || toTimeInput(event.end); - const duration = formatDuration(effectiveStart, effectiveEnd); + const duration = formatDurationStr(effectiveStart, effectiveEnd); if (duration === '0h0m0s') { toast.error('End time must be after start time'); return; diff --git a/frontend/src/components/shared/StatusBadge.tsx b/frontend/src/components/shared/StatusBadge.tsx index 872e0fe5f..d3ec48f51 100644 --- a/frontend/src/components/shared/StatusBadge.tsx +++ b/frontend/src/components/shared/StatusBadge.tsx @@ -59,36 +59,26 @@ interface StatusBadgeProps { className?: string; } +const STYLE_REGISTRY: { + variant: StatusBadgeProps['variant']; + styles: Record; +}[] = [ + { variant: 'class', styles: classStatusStyles as Record }, + { variant: 'progClass', styles: progClassStatusStyles as Record }, + { variant: 'program', styles: programStatusStyles as Record }, + { variant: 'enrollment', styles: enrollmentStatusStyles }, + { variant: 'resident', styles: residentStatusStyles } +]; + function getStyleForStatus( status: StatusType, variant: StatusBadgeProps['variant'] ): string { - if (variant === 'class' && status in classStatusStyles) { - return classStatusStyles[status as SelectedClassStatus]; - } - if (variant === 'progClass' && status in progClassStatusStyles) { - return progClassStatusStyles[status as ProgClassStatus]; - } - if (variant === 'program' && status in programStatusStyles) { - return programStatusStyles[status as ProgramEffectiveStatus]; - } - if (variant === 'enrollment' && status in enrollmentStatusStyles) { - return enrollmentStatusStyles[status]; - } - if (variant === 'resident' && status in residentStatusStyles) { - return residentStatusStyles[status]; - } - if (variant === 'auto') { - if (status in classStatusStyles) - return classStatusStyles[status as SelectedClassStatus]; - if (status in progClassStatusStyles) - return progClassStatusStyles[status as ProgClassStatus]; - if (status in programStatusStyles) - return programStatusStyles[status as ProgramEffectiveStatus]; - if (status in enrollmentStatusStyles) - return enrollmentStatusStyles[status]; - if (status in residentStatusStyles) - return residentStatusStyles[status]; + const key = String(status); + for (const entry of STYLE_REGISTRY) { + if ((entry.variant === variant || variant === 'auto') && key in entry.styles) { + return entry.styles[key]; + } } return 'bg-muted text-foreground border-border'; } diff --git a/frontend/src/lib/formatters.ts b/frontend/src/lib/formatters.ts index ff0cddf40..e7cc213fe 100644 --- a/frontend/src/lib/formatters.ts +++ b/frontend/src/lib/formatters.ts @@ -231,6 +231,22 @@ export function formatMonthYear(monthString: string): string { }); } +export function toDateInput(d: Date): string { + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; +} + +export function toTimeInput(d: Date): string { + return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; +} + +export function formatDurationStr(startTime: string, endTime: string): string { + const totalMin = timeToMinutes(endTime) - timeToMinutes(startTime); + if (totalMin <= 0) return '0h0m0s'; + const hours = Math.floor(totalMin / 60); + const minutes = totalMin % 60; + return `${hours}h${minutes}m0s`; +} + const WEEKDAY_NAMES: Record = { 0: 'Monday', 1: 'Tuesday', diff --git a/frontend/src/pages/class-detail/SessionsTab.tsx b/frontend/src/pages/class-detail/SessionsTab.tsx index 6addfed06..cff6b2c47 100644 --- a/frontend/src/pages/class-detail/SessionsTab.tsx +++ b/frontend/src/pages/class-detail/SessionsTab.tsx @@ -105,21 +105,12 @@ interface SessionsTabProps { onClassMutate: () => void; } -export type { SessionDisplay } from './session-utils'; - -function parseLocalDate(dateStr: string): Date { - const [y, m, d] = dateStr.split('-').map(Number); - return new Date(y, m - 1, d); -} +import { + formatShortDate, + buildSessionPayload +} from './session-utils'; -function formatShortDate(dateStr: string): string { - const d = parseLocalDate(dateStr); - return d.toLocaleDateString('en-US', { - weekday: 'short', - month: 'short', - day: 'numeric' - }); -} +export type { SessionDisplay } from './session-utils'; function getTimeCutoff(tf: TimeFilter): Date | null { if (tf === 'all') return null; @@ -263,29 +254,6 @@ export function SessionsTab({ cls, onClassMutate }: SessionsTabProps) { }); }; - const buildSessionPayload = ( - session: SessionDisplay - ): { - date: string; - dateLabel: string; - eventId: number; - classTime: string; - dateObj: Date; - dayName: string; - } => ({ - date: session.instance.date, - dateLabel: session.dateObj.toLocaleDateString('en-US', { - weekday: 'long', - month: 'long', - day: 'numeric', - year: 'numeric' - }), - eventId: session.instance.event_id ?? session.instance.id, - classTime: session.instance.class_time, - dateObj: session.dateObj, - dayName: session.dayName - }); - const openChangeInstructor = (sessions: SessionDisplay[]) => { setChangeInstructorSessions(sessions.map(buildSessionPayload)); setShowChangeInstructor(true); diff --git a/frontend/src/pages/programs/ClassEvents.tsx b/frontend/src/pages/programs/ClassEvents.tsx index e426b74b8..73a777069 100644 --- a/frontend/src/pages/programs/ClassEvents.tsx +++ b/frontend/src/pages/programs/ClassEvents.tsx @@ -20,30 +20,17 @@ import { TableHeader, TableRow } from '@/components/ui/table'; +import { + getPreviousMonth, + getNextMonth, + formatMonthYear +} from '@/components/helperFunctions/formatting'; function toLocalMidnight(dateOnly: string): Date { const [year, month, day] = dateOnly.split('-').map(Number); return new Date(year, month - 1, day); } -function getPreviousMonth(ym: string): string { - const [y, m] = ym.split('-').map(Number); - if (m === 1) return `${y - 1}-12`; - return `${y}-${String(m - 1).padStart(2, '0')}`; -} - -function getNextMonth(ym: string): string { - const [y, m] = ym.split('-').map(Number); - if (m === 12) return `${y + 1}-01`; - return `${y}-${String(m + 1).padStart(2, '0')}`; -} - -function formatMonthYear(ym: string): string { - const [year, month] = ym.split('-').map(Number); - const date = new Date(year, month - 1, 1); - return date.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); -} - function formatClassTime(dateStr: string, timeRange: string): string { const [startTime, endTime] = timeRange.split('-'); const startDateTime = new Date(`${dateStr}T${startTime}:00`); From 5c1a114ea68398c3225966ae5198b37ec09bbcf9 Mon Sep 17 00:00:00 2001 From: Clyde Kallahan Date: Thu, 14 May 2026 11:20:41 -0400 Subject: [PATCH 3/9] fix: type linked_override_event nullable, type VideoViewer fetch --- frontend/src/components/schedule/SessionDetailSheet.tsx | 2 +- frontend/src/pages/class-detail/SessionsTab.tsx | 2 +- frontend/src/pages/knowledge-center/VideoViewer.tsx | 8 +++----- frontend/src/pages/student/ResidentHome.tsx | 1 + frontend/src/types/events.ts | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/schedule/SessionDetailSheet.tsx b/frontend/src/components/schedule/SessionDetailSheet.tsx index 0f8dda5cc..31e2e1fb7 100644 --- a/frontend/src/components/schedule/SessionDetailSheet.tsx +++ b/frontend/src/components/schedule/SessionDetailSheet.tsx @@ -106,7 +106,7 @@ function buildFacilityEvent( end, is_override: !!activeOverride || !!session.instance.override_id, override_id: activeOverride?.id ?? session.instance.override_id ?? 0, - linked_override_event: null as unknown as FacilityProgramClassEvent, + linked_override_event: null, room: '', instructor_name: '', program_id: 0, diff --git a/frontend/src/pages/class-detail/SessionsTab.tsx b/frontend/src/pages/class-detail/SessionsTab.tsx index cff6b2c47..ac8caa7aa 100644 --- a/frontend/src/pages/class-detail/SessionsTab.tsx +++ b/frontend/src/pages/class-detail/SessionsTab.tsx @@ -76,7 +76,7 @@ function buildFacilityEvent( end, is_override: !!activeOverride || !!session.instance.override_id, override_id: activeOverride?.id ?? session.instance.override_id ?? 0, - linked_override_event: null as unknown as FacilityProgramClassEvent, + linked_override_event: null, room: '', instructor_name: '', program_id: 0, diff --git a/frontend/src/pages/knowledge-center/VideoViewer.tsx b/frontend/src/pages/knowledge-center/VideoViewer.tsx index 54d5603f6..bfc42bad7 100644 --- a/frontend/src/pages/knowledge-center/VideoViewer.tsx +++ b/frontend/src/pages/knowledge-center/VideoViewer.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { ArrowLeft } from 'lucide-react'; -import { Video, ServerResponseOne } from '@/types'; +import { Video } from '@/types'; import { useAuth, isAdministrator } from '@/auth/useAuth'; import Breadcrumbs from '@/components/navigation/Breadcrumbs'; import { Badge } from '@/components/ui/badge'; @@ -25,10 +25,8 @@ export default function VideoViewer() { useEffect(() => { const fetchVideoData = async () => { - const resp = (await API.get( - `videos/${videoId}` - )) as ServerResponseOne