"use client" /** * Course offering detail — identity header, module tabs, and per-module bodies. * Route: `//learning-activities/courses/:offeringId` */ import * as React from "react" import { Navigate, useNavigate, useSearchParams } from "react-router-dom" import { FavoriteNameCell, HubTable, PillCell, RowActionsCell, TableNewRowDot, type BulkAction } from "@/components/data-views" import type { ColumnDef } from "@/components/data-table/types" import { PageHeader } from "@/components/page-header" import { PrimaryPageTemplate } from "@/components/templates/primary-page-template" import { AvatarInitials } from "@/components/ui/avatar" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { ExportDrawer } from "@/components/export-drawer" import { KeyMetrics } from "@/components/key-metrics" import { Kbd } from "@/components/ui/kbd" import { StatusBadge, type StatusBadgeTone } from "@/components/ui/status-badge" import { Tabs, TabsContent, TabsList, TabsListScrollRegion, TabsTrigger } from "@/components/ui/tabs" import { Tip } from "@/components/ui/tip" import { useProduct } from "@/contexts/product-context" import { useDocumentTitle } from "@/hooks/use-document-title" import { initialsFromDisplayName } from "@/lib/initials-from-name" import { cn } from "@/lib/utils" import { productPersistKey } from "@/stores/app-store" import { COURSE_DETAIL_MODULE_LABELS, type CourseDetailModule, type CourseFormsPanel, laCourseDetailHref, laHubFromCourseDetailHref, offeringDetailTitle, parseCourseDetailNav, } from "@/lib/learning-activities-course-detail-nav" import { currentLearningActivitiesBasePath } from "@/lib/learning-activities-nav" import { courseFormRows, courseFormReviewRows, courseGradebookRows, coursePracticumRows, courseReportRows, type CourseFormRow, type CourseFormReviewRow, type CourseGradebookRow, type CoursePracticumRow, type CourseReportRow, } from "@/lib/mock/learning-activities-course-detail" import { courseOfferingModuleSnapshots, courseOfferingOverviewInsight, courseOfferingOverviewMetrics, COURSE_OVERVIEW_METRIC_TARGETS, courseOfferingStaticQuickActions, courseOfferingWhatsNewItems, type CourseOverviewAction, type CourseWhatsNewItem, } from "@/lib/mock/learning-activities-course-overview" import { getCourseOfferingById, LEARNING_ACTIVITY_OFFERINGS, learningActivityTypeLabel, offeringDisplayTitle, type CourseOffering, type LearningActivityType, } from "@/lib/mock/learning-activities-offerings" const DETAIL_MODULES: CourseDetailModule[] = [ "overview", "setup", "forms-evaluations", "patient-log", "timesheet", "time-off", "gradebook", "reports", ] /** * Tab strip omits "setup" — Configure is low-frequency and config-shaped, * not a peer of the daily-use operational modules, so it lives in the page * header's overflow menu instead (`exxat-page-header-actions.mdc`). Content * routing still uses the full `DETAIL_MODULES` list so `?module=setup` * deep links keep working. */ const VISIBLE_DETAIL_MODULES = DETAIL_MODULES.filter(module => module !== "setup") const ACTIVITY_MODULE_IDS = new Set([ "forms-evaluations", "patient-log", "timesheet", "time-off", ]) function isActivityModule(module: CourseDetailModule): module is LearningActivityType { return ACTIVITY_MODULE_IDS.has(module as LearningActivityType) } /** Tone for a "count / total" completion metric — derives from the ratio, never hardcoded. */ /** Basis filter shared by every Forms/Evaluations table — one filter, not a page-level toggle. */ const FORMS_BASIS_FILTER_OPTIONS = [ { value: "true", label: "Practicum based" }, { value: "false", label: "Non-practicum based" }, ] function completionTone(count: number, total: number): StatusBadgeTone { if (total <= 0) return "neutral" if (count >= total) return "success" if (count === 0) return "danger" return "warning" } const STATUS_TONE_ICON: Record = { success: "fa-circle-check", warning: "fa-triangle-exclamation", danger: "fa-circle-xmark", info: "fa-clock", neutral: "fa-circle-minus", } function CompletionStatusBadge({ count, total, label, }: { count: number total: number label: string }) { const tone = completionTone(count, total) return ( ) } /** * Academic year + term are fixed scheduling facts about *when* this offering * runs — plain text, not a scannable tag (record-detail.md §4 "Identity" * layer). Cohort + professional year are the offering's *classification* * (which students it serves) and stay scannable `PillCell` chips. Neither is * a status — nothing here drives a decision or changes state, so no `Badge` * is fabricated (`record-detail.md` §4 "Status" layer only applies to * decision-driving state, which this dataset doesn't have at the header). * All four render inline in one row below the title — splitting them across * a subtitle line and a separate chip row reads as two disconnected facts * about the same offering instead of one identity line. */ function OfferingMetaRow({ offering }: { offering: CourseOffering }) { return (
{offering.academicYear} · {offering.term}
) } function ModuleDisabledBanner({ offering, module, }: { offering: CourseOffering module: LearningActivityType }) { const action = offering.activityActions.find(a => a.type === module) if (!action || action.enabled) return null return (
{learningActivityTypeLabel(module)} is not enabled for this offering {action.disabledReason ? ` — ${action.disabledReason}` : "."}
) } function OfferingConfigureCards({ offering }: { offering: CourseOffering }) { const enabled = offering.activityActions.filter(a => a.enabled) const disabled = offering.activityActions.filter(a => !a.enabled) return (
Offering metadata
Course number {offering.courseNumber}
Course name {offering.courseName}
Academic year {offering.academicYear}
Term {offering.term}
Cohort {offering.cohort}
Professional year {offering.professionalYear}
Learning activities {enabled.map(action => (
{learningActivityTypeLabel(action.type)}
))} {disabled.map(action => (
{learningActivityTypeLabel(action.type)}
{action.disabledReason ? (

{action.disabledReason}

) : null}
))}
) } function CourseOverviewPanel({ offering, offeringId, onNavigate, }: { offering: CourseOffering offeringId: string onNavigate: (patch: { module: CourseDetailModule; formsPanel?: CourseFormsPanel }) => void }) { const metrics = React.useMemo(() => { return courseOfferingOverviewMetrics(offeringId, offering).map(metric => { const target = COURSE_OVERVIEW_METRIC_TARGETS[metric.id] if (!target) return metric return { ...metric, onClick: () => onNavigate({ module: target.module, ...(target.formsPanel ? { formsPanel: target.formsPanel } : {}), }), } }) }, [offering, offeringId, onNavigate]) const insight = React.useMemo( () => courseOfferingOverviewInsight(offeringId, offering), [offering, offeringId], ) const actions = React.useMemo(() => courseOfferingStaticQuickActions(offering), [offering]) const whatsNew = React.useMemo(() => courseOfferingWhatsNewItems(offeringId), [offeringId]) const moduleSnapshots = React.useMemo( () => courseOfferingModuleSnapshots(offeringId, offering), [offering, offeringId], ) const runAction = (action: CourseOverviewAction | CourseWhatsNewItem) => { onNavigate({ module: action.module, ...(action.formsPanel ? { formsPanel: action.formsPanel } : {}), }) } return (
{/* Flat KeyMetrics owns `px-4 lg:px-6` — same as ListPageTemplate hubs. */}

Quick actions

    {actions.map(action => (