@burdenoff/microfe-buildmyiq 2026.705.8 → 2026.705.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/dist/buildmyiq/BuildmyiqRoot.d.ts +5 -0
  2. package/dist/buildmyiq/BuildmyiqRoot.d.ts.map +1 -0
  3. package/dist/buildmyiq/BuildmyiqRoot.js +54 -46
  4. package/dist/buildmyiq/BuildmyiqRoot.js.map +1 -1
  5. package/dist/buildmyiq/BuildmyiqRoutes.d.ts +4 -0
  6. package/dist/buildmyiq/BuildmyiqRoutes.d.ts.map +1 -0
  7. package/dist/buildmyiq/BuildmyiqRoutes.js +19 -11
  8. package/dist/buildmyiq/BuildmyiqRoutes.js.map +1 -1
  9. package/dist/buildmyiq/components/BuiDiscussion.d.ts +18 -0
  10. package/dist/buildmyiq/components/BuiDiscussion.d.ts.map +1 -0
  11. package/dist/buildmyiq/components/BuiTags.d.ts +17 -0
  12. package/dist/buildmyiq/components/BuiTags.d.ts.map +1 -0
  13. package/dist/buildmyiq/components/DataTable.d.ts +28 -0
  14. package/dist/buildmyiq/components/DataTable.d.ts.map +1 -0
  15. package/dist/buildmyiq/components/DataTable.js +1 -1
  16. package/dist/buildmyiq/components/DataTable.js.map +1 -1
  17. package/dist/buildmyiq/components/EmptyState.d.ts +18 -0
  18. package/dist/buildmyiq/components/EmptyState.d.ts.map +1 -0
  19. package/dist/buildmyiq/components/EvidenceUpload.d.ts +14 -0
  20. package/dist/buildmyiq/components/EvidenceUpload.d.ts.map +1 -0
  21. package/dist/buildmyiq/components/FileLink.d.ts +15 -0
  22. package/dist/buildmyiq/components/FileLink.d.ts.map +1 -0
  23. package/dist/buildmyiq/components/FilterTabs.d.ts +18 -0
  24. package/dist/buildmyiq/components/FilterTabs.d.ts.map +1 -0
  25. package/dist/buildmyiq/components/MetricsCard.d.ts +23 -0
  26. package/dist/buildmyiq/components/MetricsCard.d.ts.map +1 -0
  27. package/dist/buildmyiq/components/MiniBar.d.ts +16 -0
  28. package/dist/buildmyiq/components/MiniBar.d.ts.map +1 -0
  29. package/dist/buildmyiq/components/MiniBar.js +1 -1
  30. package/dist/buildmyiq/components/MiniBar.js.map +1 -1
  31. package/dist/buildmyiq/components/PageLayout.d.ts +16 -0
  32. package/dist/buildmyiq/components/PageLayout.d.ts.map +1 -0
  33. package/dist/buildmyiq/components/Pagination.d.ts +22 -0
  34. package/dist/buildmyiq/components/Pagination.d.ts.map +1 -0
  35. package/dist/buildmyiq/components/StateViews.d.ts +24 -0
  36. package/dist/buildmyiq/components/StateViews.d.ts.map +1 -0
  37. package/dist/buildmyiq/components/StatusBadge.d.ts +9 -0
  38. package/dist/buildmyiq/components/StatusBadge.d.ts.map +1 -0
  39. package/dist/buildmyiq/components/Tabs.d.ts +17 -0
  40. package/dist/buildmyiq/components/Tabs.d.ts.map +1 -0
  41. package/dist/buildmyiq/components/index.d.ts +19 -0
  42. package/dist/buildmyiq/components/index.d.ts.map +1 -0
  43. package/dist/buildmyiq/context/BuildmyiqContext.d.ts +16 -0
  44. package/dist/buildmyiq/context/BuildmyiqContext.d.ts.map +1 -0
  45. package/dist/buildmyiq/context/index.d.ts +6 -0
  46. package/dist/buildmyiq/context/index.d.ts.map +1 -0
  47. package/dist/buildmyiq/hooks/index.d.ts +10 -0
  48. package/dist/buildmyiq/hooks/index.d.ts.map +1 -0
  49. package/dist/buildmyiq/hooks/useBuiComments.d.ts +36 -0
  50. package/dist/buildmyiq/hooks/useBuiComments.d.ts.map +1 -0
  51. package/dist/buildmyiq/hooks/useBuiFiles.d.ts +47 -0
  52. package/dist/buildmyiq/hooks/useBuiFiles.d.ts.map +1 -0
  53. package/dist/buildmyiq/hooks/useBuiTags.d.ts +44 -0
  54. package/dist/buildmyiq/hooks/useBuiTags.d.ts.map +1 -0
  55. package/dist/buildmyiq/hooks/useBuildmyiqApi.d.ts +65 -0
  56. package/dist/buildmyiq/hooks/useBuildmyiqApi.d.ts.map +1 -0
  57. package/dist/buildmyiq/hooks/useBuildmyiqApi.js +23 -15
  58. package/dist/buildmyiq/hooks/useBuildmyiqApi.js.map +1 -1
  59. package/dist/buildmyiq/hooks/useCursorPager.d.ts +33 -0
  60. package/dist/buildmyiq/hooks/useCursorPager.d.ts.map +1 -0
  61. package/dist/buildmyiq/index.d.ts +9 -0
  62. package/dist/buildmyiq/index.d.ts.map +1 -0
  63. package/dist/buildmyiq/pages/DashboardPage.d.ts +4 -0
  64. package/dist/buildmyiq/pages/DashboardPage.d.ts.map +1 -0
  65. package/dist/buildmyiq/pages/DashboardPage.js +2 -2
  66. package/dist/buildmyiq/pages/DashboardPage.js.map +1 -1
  67. package/dist/buildmyiq/pages/_shared/CourseCard.d.ts +13 -0
  68. package/dist/buildmyiq/pages/_shared/CourseCard.d.ts.map +1 -0
  69. package/dist/buildmyiq/pages/_shared/index.d.ts +6 -0
  70. package/dist/buildmyiq/pages/_shared/index.d.ts.map +1 -0
  71. package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.d.ts +4 -0
  72. package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.d.ts.map +1 -0
  73. package/dist/buildmyiq/pages/assessments/AssessmentsPage.d.ts +4 -0
  74. package/dist/buildmyiq/pages/assessments/AssessmentsPage.d.ts.map +1 -0
  75. package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.d.ts +4 -0
  76. package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.d.ts.map +1 -0
  77. package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.js +264 -0
  78. package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.js.map +1 -0
  79. package/dist/buildmyiq/pages/authoring/AuthoringPage.d.ts +4 -0
  80. package/dist/buildmyiq/pages/authoring/AuthoringPage.d.ts.map +1 -0
  81. package/dist/buildmyiq/pages/authoring/AuthoringPage.js +4 -4
  82. package/dist/buildmyiq/pages/authoring/AuthoringPage.js.map +1 -1
  83. package/dist/buildmyiq/pages/authoring/CourseBuilderPage.d.ts +4 -0
  84. package/dist/buildmyiq/pages/authoring/CourseBuilderPage.d.ts.map +1 -0
  85. package/dist/buildmyiq/pages/authoring/_forms.d.ts +44 -0
  86. package/dist/buildmyiq/pages/authoring/_forms.d.ts.map +1 -0
  87. package/dist/buildmyiq/pages/authoring/_forms.js +15 -15
  88. package/dist/buildmyiq/pages/authoring/_forms.js.map +1 -1
  89. package/dist/buildmyiq/pages/catalog/CatalogPage.d.ts +4 -0
  90. package/dist/buildmyiq/pages/catalog/CatalogPage.d.ts.map +1 -0
  91. package/dist/buildmyiq/pages/catalog/CourseDetailPage.d.ts +4 -0
  92. package/dist/buildmyiq/pages/catalog/CourseDetailPage.d.ts.map +1 -0
  93. package/dist/buildmyiq/pages/catalog/CourseDetailPage.js +9 -1
  94. package/dist/buildmyiq/pages/catalog/CourseDetailPage.js.map +1 -1
  95. package/dist/buildmyiq/pages/certificates/CertificatesPage.d.ts +4 -0
  96. package/dist/buildmyiq/pages/certificates/CertificatesPage.d.ts.map +1 -0
  97. package/dist/buildmyiq/pages/certificates/CertificatesPage.js +1 -1
  98. package/dist/buildmyiq/pages/certificates/CertificatesPage.js.map +1 -1
  99. package/dist/buildmyiq/pages/cohorts/CohortsPage.d.ts +4 -0
  100. package/dist/buildmyiq/pages/cohorts/CohortsPage.d.ts.map +1 -0
  101. package/dist/buildmyiq/pages/cohorts/CohortsPage.js +1 -1
  102. package/dist/buildmyiq/pages/cohorts/CohortsPage.js.map +1 -1
  103. package/dist/buildmyiq/pages/insights/InsightsPage.d.ts +4 -0
  104. package/dist/buildmyiq/pages/insights/InsightsPage.d.ts.map +1 -0
  105. package/dist/buildmyiq/pages/insights/InsightsPage.js +242 -0
  106. package/dist/buildmyiq/pages/insights/InsightsPage.js.map +1 -0
  107. package/dist/buildmyiq/pages/learning/LessonPlayerPage.d.ts +4 -0
  108. package/dist/buildmyiq/pages/learning/LessonPlayerPage.d.ts.map +1 -0
  109. package/dist/buildmyiq/pages/learning/MyLearningPage.d.ts +4 -0
  110. package/dist/buildmyiq/pages/learning/MyLearningPage.d.ts.map +1 -0
  111. package/dist/buildmyiq/pages/learning/MyLearningPage.js +83 -77
  112. package/dist/buildmyiq/pages/learning/MyLearningPage.js.map +1 -1
  113. package/dist/buildmyiq/pages/skills/SkillsPage.d.ts +4 -0
  114. package/dist/buildmyiq/pages/skills/SkillsPage.d.ts.map +1 -0
  115. package/dist/buildmyiq/pages/submissions/SubmissionsPage.d.ts +4 -0
  116. package/dist/buildmyiq/pages/submissions/SubmissionsPage.d.ts.map +1 -0
  117. package/dist/buildmyiq/types/index.d.ts +382 -0
  118. package/dist/buildmyiq/types/index.d.ts.map +1 -0
  119. package/dist/buildmyiq/utils/cn.d.ts +4 -0
  120. package/dist/buildmyiq/utils/cn.d.ts.map +1 -0
  121. package/dist/buildmyiq/utils/formatters.d.ts +33 -0
  122. package/dist/buildmyiq/utils/formatters.d.ts.map +1 -0
  123. package/dist/buildmyiq/utils/i18n.d.ts +6 -0
  124. package/dist/buildmyiq/utils/i18n.d.ts.map +1 -0
  125. package/dist/buildmyiq/utils/index.d.ts +9 -0
  126. package/dist/buildmyiq/utils/index.d.ts.map +1 -0
  127. package/dist/buildmyiq/utils/navigation.d.ts +14 -0
  128. package/dist/buildmyiq/utils/navigation.d.ts.map +1 -0
  129. package/dist/dev/main.d.ts +2 -0
  130. package/dist/dev/main.d.ts.map +1 -0
  131. package/dist/generated/global-operations.d.ts +2 -0
  132. package/dist/generated/global-operations.d.ts.map +1 -0
  133. package/dist/generated/global-types.d.ts +18199 -0
  134. package/dist/generated/global-types.d.ts.map +1 -0
  135. package/dist/generated/wspace-operations.d.ts +2 -0
  136. package/dist/generated/wspace-operations.d.ts.map +1 -0
  137. package/dist/generated/wspace-types.d.ts +43046 -0
  138. package/dist/generated/wspace-types.d.ts.map +1 -0
  139. package/dist/index.d.ts +2 -0
  140. package/dist/index.d.ts.map +1 -0
  141. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"CourseDetailPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/catalog/CourseDetailPage.tsx"],"sourcesContent":["/**\n * CourseDetailPage — a single published course: hero, curriculum, assessments,\n * discussion + tags, and the enrol / continue-learning call to action.\n * @module buildmyiq/pages/catalog\n *\n * All data is live: `buiCourse` (hero + flat lessons), `buiListModules`\n * (curriculum), `buiListAssessments` (course exams), and `buiListEnrollments`\n * (to decide Enrol vs Continue). Enrolling calls `buiEnroll` then routes the\n * learner straight into their new enrollment. Every list has its own\n * loading / error / empty state.\n */\n\nimport type { FC } from 'react';\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport type { LucideIcon } from 'lucide-react';\nimport {\n ArrowLeft,\n BookOpen,\n ClipboardCheck,\n ClipboardList,\n Clock,\n FileText,\n GraduationCap,\n Layers,\n ListChecks,\n MousePointerClick,\n PlayCircle,\n Radio,\n Sparkles,\n Video,\n} from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n Section,\n LoadingView,\n ErrorView,\n EmptyState,\n MiniBar,\n BuiDiscussion,\n BuiTags,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiCourse,\n useBuiModules,\n useBuiAssessments,\n useBuiEnrollments,\n useBuiEnroll,\n} from '../../hooks';\nimport { formatHours, formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport type { BuiLesson, BuiLessonType } from '../../types';\n\nconst COURSE_ENTITY = 'buildmyiq-course';\n\n/** Lesson-type → icon, so learners can scan a module at a glance. */\nconst LESSON_ICON: Record<BuiLessonType, LucideIcon> = {\n ARTICLE: FileText,\n VIDEO: Video,\n QUIZ: ListChecks,\n ASSIGNMENT: ClipboardList,\n INTERACTIVE: MousePointerClick,\n LIVE_SESSION: Radio,\n};\n\nconst LessonRow: FC<{ lesson: BuiLesson }> = ({ lesson }) => {\n const Icon = LESSON_ICON[lesson.type] ?? FileText;\n return (\n <li className=\"flex items-center gap-3 px-4 py-3\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-bg-sunken text-text-secondary\">\n <Icon className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{lesson.title}</p>\n <p className=\"text-xs text-text-tertiary\">{formatEnumLabel(lesson.type)}</p>\n </div>\n {lesson.isPreview && (\n <span className=\"shrink-0 rounded-full bg-status-success-bg-subtle px-2 py-0.5 text-xs font-medium text-status-success-text\">\n Preview\n </span>\n )}\n {lesson.durationMin > 0 && (\n <span className=\"hidden shrink-0 items-center gap-1 text-xs tabular-nums text-text-secondary sm:inline-flex\">\n <Clock className=\"h-3.5 w-3.5\" /> {formatDuration(lesson.durationMin)}\n </span>\n )}\n </li>\n );\n};\n\nexport const CourseDetailPage: FC = () => {\n const { courseId } = useParams<{ courseId: string }>();\n const { currentUser, navigateTo } = useBuildmyiq();\n const learnerId = currentUser?.id;\n\n const courseQuery = useBuiCourse(courseId);\n const modulesQuery = useBuiModules(courseId);\n const assessmentsQuery = useBuiAssessments(courseId);\n const enrollmentsQuery = useBuiEnrollments({\n courseId,\n learnerId,\n enabled: Boolean(courseId && learnerId),\n });\n\n const enroll = useBuiEnroll();\n const [enrolError, setEnrolError] = useState<string | null>(null);\n\n const course = courseQuery.data ?? null;\n const modules = modulesQuery.data ?? [];\n const assessments = assessmentsQuery.data ?? [];\n const enrollments = enrollmentsQuery.data?.items ?? [];\n\n // An existing, non-terminal enrollment means \"Continue\" instead of \"Enrol\".\n const activeEnrollment = useMemo(\n () => enrollments.find((e) => e.status !== 'DROPPED' && e.status !== 'EXPIRED'),\n [enrollments]\n );\n\n // Lessons attached directly to the course (no module) — surfaced as their own group.\n const ungroupedLessons = useMemo(\n () => (course?.lessons ?? []).filter((lesson) => !lesson.moduleId),\n [course]\n );\n\n const totalLessons = course?.lessons?.length ?? 0;\n\n const handleEnrol = async (): Promise<void> => {\n if (!courseId || !learnerId) return;\n setEnrolError(null);\n try {\n const created = await enroll.mutateAsync({ courseId, learnerId });\n navigateTo(`learning/${created.id}`);\n } catch (err) {\n setEnrolError(err instanceof Error ? err.message : 'Could not enrol in this course.');\n }\n };\n\n // Top-level page states.\n if (!courseId) {\n return (\n <PageLayout title=\"Course\">\n <ErrorView title=\"Missing course\" message=\"No course was specified.\" />\n </PageLayout>\n );\n }\n\n if (courseQuery.isLoading) {\n return (\n <PageLayout title=\"Course\">\n <LoadingView label=\"Loading course…\" />\n </PageLayout>\n );\n }\n\n if (courseQuery.isError) {\n return (\n <PageLayout title=\"Course\">\n <ErrorView\n title=\"Couldn't load this course\"\n message={courseQuery.error instanceof Error ? courseQuery.error.message : undefined}\n onRetry={() => courseQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!course) {\n return (\n <PageLayout title=\"Course\">\n <EmptyState\n illustration=\"empty-search\"\n title=\"Course not found\"\n description=\"This course may have been unpublished or removed.\"\n action={<Button onClick={() => navigateTo('catalog')}>Back to catalog</Button>}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout title={course.title}>\n <div className=\"mx-auto flex w-full max-w-5xl flex-col gap-8\">\n <button\n type=\"button\"\n onClick={() => navigateTo('catalog')}\n className=\"inline-flex w-fit items-center gap-1.5 text-sm text-text-secondary transition-colors hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back to catalog\n </button>\n\n {/* Hero */}\n <GlassCard treatment=\"plain\" className=\"overflow-hidden\">\n <div className=\"grid grid-cols-1 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]\">\n <div className=\"relative h-48 w-full overflow-hidden bg-bg-sunken lg:h-full\">\n {course.cover?.downloadUrl ? (\n <img src={course.cover.downloadUrl} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : (\n <div className=\"flex h-full min-h-[12rem] w-full items-center justify-center bg-[var(--color-accent-soft)]\">\n <BookOpen className=\"h-10 w-10 text-text-secondary opacity-60\" />\n </div>\n )}\n </div>\n\n <div className=\"flex flex-col gap-4 p-6\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={course.level} />\n {course.skill?.name && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs text-text-secondary\">\n <Sparkles className=\"h-3.5 w-3.5\" /> {course.skill.name}\n </span>\n )}\n </div>\n\n <div>\n <h1 className=\"text-2xl font-semibold text-text-primary\">{course.title}</h1>\n {course.summary && (\n <p className=\"mt-2 text-sm text-text-secondary\">{course.summary}</p>\n )}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1\">\n <Layers className=\"h-3.5 w-3.5\" /> {modules.length} module\n {modules.length === 1 ? '' : 's'} · {totalLessons} lesson\n {totalLessons === 1 ? '' : 's'}\n </span>\n {course.estimatedHours > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" /> {formatHours(course.estimatedHours)}\n </span>\n )}\n </div>\n\n {course.tags.length > 0 && (\n <div className=\"flex flex-wrap gap-1.5\">\n {course.tags.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {course.description && (\n <p className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-secondary\">\n {course.description}\n </p>\n )}\n\n {/* Primary CTA — Continue if enrolled, else Enrol */}\n <div className=\"mt-auto flex flex-col gap-2 pt-2\">\n {activeEnrollment ? (\n <>\n <Button\n size=\"lg\"\n className=\"w-full sm:w-auto\"\n onClick={() => navigateTo(`learning/${activeEnrollment.id}`)}\n >\n <PlayCircle className=\"mr-2 h-4 w-4\" /> Continue learning\n </Button>\n <div className=\"flex items-center gap-2\">\n <MiniBar value={activeEnrollment.progressPct} className=\"flex-1\" />\n <span className=\"shrink-0 text-xs tabular-nums text-text-secondary\">\n {activeEnrollment.progressPct}% complete\n </span>\n </div>\n </>\n ) : (\n <>\n <Button\n size=\"lg\"\n className=\"w-full sm:w-auto\"\n onClick={handleEnrol}\n disabled={enroll.isPending || !learnerId}\n >\n <GraduationCap className=\"mr-2 h-4 w-4\" />\n {enroll.isPending ? 'Enrolling…' : 'Enrol in this course'}\n </Button>\n {!learnerId && (\n <p className=\"text-xs text-text-tertiary\">\n Sign in to your workspace to enrol.\n </p>\n )}\n {enrolError && <p className=\"text-xs text-status-error-text\">{enrolError}</p>}\n </>\n )}\n </div>\n </div>\n </div>\n </GlassCard>\n\n {/* Curriculum */}\n <Section title=\"Curriculum\" description=\"Modules and lessons in this course.\">\n {modulesQuery.isLoading ? (\n <LoadingView label=\"Loading curriculum…\" />\n ) : modulesQuery.isError ? (\n <ErrorView\n title=\"Couldn't load the curriculum\"\n message={modulesQuery.error instanceof Error ? modulesQuery.error.message : undefined}\n onRetry={() => modulesQuery.refetch()}\n />\n ) : modules.length === 0 && ungroupedLessons.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No lessons yet\"\n description=\"The instructor hasn't added any modules or lessons to this course.\"\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n {modules.map((module, index) => {\n const lessons = module.lessons ?? [];\n return (\n <div\n key={module.id}\n className=\"overflow-hidden rounded-xl border border-border-seam bg-bg-surface\"\n >\n <div className=\"border-b border-border-seam px-4 py-3\">\n <div className=\"flex items-baseline justify-between gap-3\">\n <h3 className=\"font-semibold text-text-primary\">\n <span className=\"mr-2 text-text-tertiary\">\n {String(index + 1).padStart(2, '0')}\n </span>\n {module.title}\n </h3>\n <span className=\"shrink-0 text-xs text-text-tertiary\">\n {lessons.length} lesson{lessons.length === 1 ? '' : 's'}\n </span>\n </div>\n {module.description && (\n <p className=\"mt-1 text-sm text-text-secondary\">{module.description}</p>\n )}\n </div>\n {lessons.length === 0 ? (\n <p className=\"px-4 py-3 text-sm text-text-tertiary\">\n No lessons in this module yet.\n </p>\n ) : (\n <ul className=\"divide-y divide-border-seam\">\n {lessons.map((lesson) => (\n <LessonRow key={lesson.id} lesson={lesson} />\n ))}\n </ul>\n )}\n </div>\n );\n })}\n\n {ungroupedLessons.length > 0 && (\n <div className=\"overflow-hidden rounded-xl border border-border-seam bg-bg-surface\">\n <div className=\"border-b border-border-seam px-4 py-3\">\n <h3 className=\"font-semibold text-text-primary\">Additional lessons</h3>\n </div>\n <ul className=\"divide-y divide-border-seam\">\n {ungroupedLessons.map((lesson) => (\n <LessonRow key={lesson.id} lesson={lesson} />\n ))}\n </ul>\n </div>\n )}\n </div>\n )}\n </Section>\n\n {/* Assessments */}\n <Section title=\"Assessments\" description=\"Quizzes and exams for this course.\">\n {assessmentsQuery.isLoading ? (\n <LoadingView label=\"Loading assessments…\" />\n ) : assessmentsQuery.isError ? (\n <ErrorView\n title=\"Couldn't load assessments\"\n message={\n assessmentsQuery.error instanceof Error ? assessmentsQuery.error.message : undefined\n }\n onRetry={() => assessmentsQuery.refetch()}\n />\n ) : assessments.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<ClipboardCheck className=\"h-9 w-9\" />}\n title=\"No assessments yet\"\n description=\"This course doesn't have any quizzes or exams attached.\"\n />\n ) : (\n <ul className=\"flex flex-col gap-3\">\n {assessments.map((assessment) => (\n <li\n key={assessment.id}\n className=\"flex flex-wrap items-center gap-3 rounded-xl border border-border-seam bg-bg-surface p-4\"\n >\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <ClipboardCheck className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate font-medium text-text-primary\">{assessment.title}</p>\n <div className=\"mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-text-secondary\">\n <span>Pass ≥ {assessment.passingScore}%</span>\n <span>\n {assessment.maxAttempts} attempt\n {assessment.maxAttempts === 1 ? '' : 's'}\n </span>\n {assessment.timeLimitMin > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" />\n {formatDuration(assessment.timeLimitMin)}\n </span>\n )}\n </div>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n <StatusBadge status={assessment.type} />\n <StatusBadge status={assessment.status} />\n </div>\n </li>\n ))}\n </ul>\n )}\n </Section>\n\n {/* Tags (platform wspace-tags) */}\n <BuiTags entityType={COURSE_ENTITY} entityId={courseId} />\n\n {/* Discussion (platform wspace-conversations) */}\n <BuiDiscussion\n entityType={COURSE_ENTITY}\n entityId={courseId}\n description=\"Ask questions and discuss this course with your workspace.\"\n />\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"CourseDetailPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/catalog/CourseDetailPage.tsx"],"sourcesContent":["/**\n * CourseDetailPage — a single published course: hero, curriculum, assessments,\n * discussion + tags, and the enrol / continue-learning call to action.\n * @module buildmyiq/pages/catalog\n *\n * All data is live: `buiCourse` (hero + flat lessons), `buiListModules`\n * (curriculum), `buiListAssessments` (course exams), and `buiListEnrollments`\n * (to decide Enrol vs Continue). Enrolling calls `buiEnroll` then routes the\n * learner straight into their new enrollment. Every list has its own\n * loading / error / empty state.\n */\n\nimport type { FC } from 'react';\nimport { useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport type { LucideIcon } from 'lucide-react';\nimport {\n ArrowLeft,\n BookOpen,\n ClipboardCheck,\n ClipboardList,\n Clock,\n FileText,\n GraduationCap,\n Layers,\n ListChecks,\n MousePointerClick,\n PlayCircle,\n Radio,\n Sparkles,\n Video,\n} from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n Section,\n LoadingView,\n ErrorView,\n EmptyState,\n MiniBar,\n BuiDiscussion,\n BuiTags,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiCourse,\n useBuiModules,\n useBuiAssessments,\n useBuiEnrollments,\n useBuiEnroll,\n} from '../../hooks';\nimport { formatHours, formatDuration, formatEnumLabel } from '../../utils/formatters';\nimport type { BuiLesson, BuiLessonType } from '../../types';\n\nconst COURSE_ENTITY = 'buildmyiq-course';\n\n/** Lesson-type → icon, so learners can scan a module at a glance. */\nconst LESSON_ICON: Record<BuiLessonType, LucideIcon> = {\n ARTICLE: FileText,\n VIDEO: Video,\n QUIZ: ListChecks,\n ASSIGNMENT: ClipboardList,\n INTERACTIVE: MousePointerClick,\n LIVE_SESSION: Radio,\n};\n\nconst LessonRow: FC<{ lesson: BuiLesson }> = ({ lesson }) => {\n const Icon = LESSON_ICON[lesson.type] ?? FileText;\n return (\n <li className=\"flex items-center gap-3 px-4 py-3\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-bg-sunken text-text-secondary\">\n <Icon className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{lesson.title}</p>\n <p className=\"text-xs text-text-tertiary\">{formatEnumLabel(lesson.type)}</p>\n </div>\n {lesson.isPreview && (\n <span className=\"shrink-0 rounded-full bg-status-success-bg-subtle px-2 py-0.5 text-xs font-medium text-status-success-text\">\n Preview\n </span>\n )}\n {lesson.durationMin > 0 && (\n <span className=\"hidden shrink-0 items-center gap-1 text-xs tabular-nums text-text-secondary sm:inline-flex\">\n <Clock className=\"h-3.5 w-3.5\" /> {formatDuration(lesson.durationMin)}\n </span>\n )}\n </li>\n );\n};\n\nexport const CourseDetailPage: FC = () => {\n const { courseId } = useParams<{ courseId: string }>();\n const { currentUser, navigateTo } = useBuildmyiq();\n const learnerId = currentUser?.id;\n\n const courseQuery = useBuiCourse(courseId);\n const modulesQuery = useBuiModules(courseId);\n const assessmentsQuery = useBuiAssessments(courseId);\n const enrollmentsQuery = useBuiEnrollments({\n courseId,\n learnerId,\n enabled: Boolean(courseId && learnerId),\n });\n\n const enroll = useBuiEnroll();\n const [enrolError, setEnrolError] = useState<string | null>(null);\n\n const course = courseQuery.data ?? null;\n const modules = modulesQuery.data ?? [];\n const assessments = assessmentsQuery.data ?? [];\n const enrollments = enrollmentsQuery.data?.items ?? [];\n\n // An existing, non-terminal enrollment means \"Continue\" instead of \"Enrol\".\n const activeEnrollment = useMemo(\n () => enrollments.find((e) => e.status !== 'DROPPED' && e.status !== 'EXPIRED'),\n [enrollments]\n );\n\n // Lessons attached directly to the course (no module) — surfaced as their own group.\n const ungroupedLessons = useMemo(\n () => (course?.lessons ?? []).filter((lesson) => !lesson.moduleId),\n [course]\n );\n\n const totalLessons = course?.lessons?.length ?? 0;\n\n const handleEnrol = async (): Promise<void> => {\n if (!courseId || !learnerId) return;\n setEnrolError(null);\n try {\n const created = await enroll.mutateAsync({ courseId, learnerId });\n navigateTo(`learning/${created.id}`);\n } catch (err) {\n setEnrolError(err instanceof Error ? err.message : 'Could not enrol in this course.');\n }\n };\n\n // Top-level page states.\n if (!courseId) {\n return (\n <PageLayout title=\"Course\">\n <ErrorView title=\"Missing course\" message=\"No course was specified.\" />\n </PageLayout>\n );\n }\n\n if (courseQuery.isLoading) {\n return (\n <PageLayout title=\"Course\">\n <LoadingView label=\"Loading course…\" />\n </PageLayout>\n );\n }\n\n if (courseQuery.isError) {\n return (\n <PageLayout title=\"Course\">\n <ErrorView\n title=\"Couldn't load this course\"\n message={courseQuery.error instanceof Error ? courseQuery.error.message : undefined}\n onRetry={() => courseQuery.refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!course) {\n return (\n <PageLayout title=\"Course\">\n <EmptyState\n illustration=\"empty-search\"\n title=\"Course not found\"\n description=\"This course may have been unpublished or removed.\"\n action={<Button onClick={() => navigateTo('catalog')}>Back to catalog</Button>}\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout title={course.title}>\n <div className=\"mx-auto flex w-full max-w-5xl flex-col gap-8\">\n <button\n type=\"button\"\n onClick={() => navigateTo('catalog')}\n className=\"inline-flex w-fit items-center gap-1.5 text-sm text-text-secondary transition-colors hover:text-text-primary\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back to catalog\n </button>\n\n {/* Hero */}\n <GlassCard treatment=\"plain\" className=\"overflow-hidden\">\n <div className=\"grid grid-cols-1 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]\">\n <div className=\"relative h-48 w-full overflow-hidden bg-bg-sunken lg:h-full\">\n {course.cover?.downloadUrl ? (\n <img src={course.cover.downloadUrl} alt=\"\" className=\"h-full w-full object-cover\" />\n ) : (\n <div className=\"flex h-full min-h-[12rem] w-full items-center justify-center bg-[var(--color-accent-soft)]\">\n <BookOpen className=\"h-10 w-10 text-text-secondary opacity-60\" />\n </div>\n )}\n </div>\n\n <div className=\"flex flex-col gap-4 p-6\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={course.level} />\n {course.skill?.name && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs text-text-secondary\">\n <Sparkles className=\"h-3.5 w-3.5\" /> {course.skill.name}\n </span>\n )}\n </div>\n\n <div>\n <h1 className=\"text-2xl font-semibold text-text-primary\">{course.title}</h1>\n {course.summary && (\n <p className=\"mt-2 text-sm text-text-secondary\">{course.summary}</p>\n )}\n </div>\n\n <div className=\"flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-text-secondary\">\n <span className=\"inline-flex items-center gap-1\">\n <Layers className=\"h-3.5 w-3.5\" /> {modules.length} module\n {modules.length === 1 ? '' : 's'} · {totalLessons} lesson\n {totalLessons === 1 ? '' : 's'}\n </span>\n {course.estimatedHours > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" /> {formatHours(course.estimatedHours)}\n </span>\n )}\n </div>\n\n {course.tags.length > 0 && (\n <div className=\"flex flex-wrap gap-1.5\">\n {course.tags.map((tag) => (\n <span\n key={tag}\n className=\"rounded-full bg-bg-sunken px-2 py-0.5 text-xs text-text-secondary\"\n >\n {tag}\n </span>\n ))}\n </div>\n )}\n\n {course.description && (\n <p className=\"whitespace-pre-wrap text-sm leading-relaxed text-text-secondary\">\n {course.description}\n </p>\n )}\n\n {/* Primary CTA — Continue if enrolled, else Enrol */}\n <div className=\"mt-auto flex flex-col gap-2 pt-2\">\n {activeEnrollment ? (\n <>\n <Button\n size=\"lg\"\n className=\"w-full sm:w-auto\"\n onClick={() => navigateTo(`learning/${activeEnrollment.id}`)}\n >\n <PlayCircle className=\"mr-2 h-4 w-4\" /> Continue learning\n </Button>\n <div className=\"flex items-center gap-2\">\n <MiniBar value={activeEnrollment.progressPct} className=\"flex-1\" />\n <span className=\"shrink-0 text-xs tabular-nums text-text-secondary\">\n {activeEnrollment.progressPct}% complete\n </span>\n </div>\n </>\n ) : (\n <>\n <Button\n size=\"lg\"\n className=\"w-full sm:w-auto\"\n onClick={handleEnrol}\n disabled={enroll.isPending || !learnerId}\n >\n <GraduationCap className=\"mr-2 h-4 w-4\" />\n {enroll.isPending ? 'Enrolling…' : 'Enrol in this course'}\n </Button>\n {!learnerId && (\n <p className=\"text-xs text-text-tertiary\">\n Sign in to your workspace to enrol.\n </p>\n )}\n {enrolError && <p className=\"text-xs text-status-error-text\">{enrolError}</p>}\n </>\n )}\n </div>\n </div>\n </div>\n </GlassCard>\n\n {/* Curriculum */}\n <Section title=\"Curriculum\" description=\"Modules and lessons in this course.\">\n {modulesQuery.isLoading ? (\n <LoadingView label=\"Loading curriculum…\" />\n ) : modulesQuery.isError ? (\n <ErrorView\n title=\"Couldn't load the curriculum\"\n message={modulesQuery.error instanceof Error ? modulesQuery.error.message : undefined}\n onRetry={() => modulesQuery.refetch()}\n />\n ) : modules.length === 0 && ungroupedLessons.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No lessons yet\"\n description=\"The instructor hasn't added any modules or lessons to this course.\"\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n {modules.map((module, index) => {\n const lessons = module.lessons ?? [];\n return (\n <div\n key={module.id}\n className=\"overflow-hidden rounded-xl border border-border-seam bg-bg-surface\"\n >\n <div className=\"border-b border-border-seam px-4 py-3\">\n <div className=\"flex items-baseline justify-between gap-3\">\n <h3 className=\"font-semibold text-text-primary\">\n <span className=\"mr-2 text-text-tertiary\">\n {String(index + 1).padStart(2, '0')}\n </span>\n {module.title}\n </h3>\n <span className=\"shrink-0 text-xs text-text-tertiary\">\n {lessons.length} lesson{lessons.length === 1 ? '' : 's'}\n </span>\n </div>\n {module.description && (\n <p className=\"mt-1 text-sm text-text-secondary\">{module.description}</p>\n )}\n </div>\n {lessons.length === 0 ? (\n <p className=\"px-4 py-3 text-sm text-text-tertiary\">\n No lessons in this module yet.\n </p>\n ) : (\n <ul className=\"divide-y divide-border-seam\">\n {lessons.map((lesson) => (\n <LessonRow key={lesson.id} lesson={lesson} />\n ))}\n </ul>\n )}\n </div>\n );\n })}\n\n {ungroupedLessons.length > 0 && (\n <div className=\"overflow-hidden rounded-xl border border-border-seam bg-bg-surface\">\n <div className=\"border-b border-border-seam px-4 py-3\">\n <h3 className=\"font-semibold text-text-primary\">Additional lessons</h3>\n </div>\n <ul className=\"divide-y divide-border-seam\">\n {ungroupedLessons.map((lesson) => (\n <LessonRow key={lesson.id} lesson={lesson} />\n ))}\n </ul>\n </div>\n )}\n </div>\n )}\n </Section>\n\n {/* Assessments */}\n <Section title=\"Assessments\" description=\"Quizzes and exams for this course.\">\n {assessmentsQuery.isLoading ? (\n <LoadingView label=\"Loading assessments…\" />\n ) : assessmentsQuery.isError ? (\n <ErrorView\n title=\"Couldn't load assessments\"\n message={\n assessmentsQuery.error instanceof Error ? assessmentsQuery.error.message : undefined\n }\n onRetry={() => assessmentsQuery.refetch()}\n />\n ) : assessments.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<ClipboardCheck className=\"h-9 w-9\" />}\n title=\"No assessments yet\"\n description=\"This course doesn't have any quizzes or exams attached.\"\n />\n ) : (\n <ul className=\"flex flex-col gap-3\">\n {assessments.map((assessment) => (\n <li\n key={assessment.id}\n className=\"flex flex-wrap items-center gap-3 rounded-xl border border-border-seam bg-bg-surface p-4\"\n >\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <ClipboardCheck className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate font-medium text-text-primary\">{assessment.title}</p>\n <div className=\"mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-text-secondary\">\n <span>Pass ≥ {assessment.passingScore}%</span>\n <span>\n {assessment.maxAttempts} attempt\n {assessment.maxAttempts === 1 ? '' : 's'}\n </span>\n {assessment.timeLimitMin > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" />\n {formatDuration(assessment.timeLimitMin)}\n </span>\n )}\n </div>\n </div>\n <div className=\"flex shrink-0 items-center gap-2\">\n <StatusBadge status={assessment.type} />\n <StatusBadge status={assessment.status} />\n {assessment.status === 'PUBLISHED' && activeEnrollment && (\n <Button\n size=\"sm\"\n onClick={() => navigateTo(`assessments/${assessment.id}/take`)}\n >\n Take\n </Button>\n )}\n </div>\n </li>\n ))}\n </ul>\n )}\n </Section>\n\n {/* Tags (platform wspace-tags) */}\n <BuiTags entityType={COURSE_ENTITY} entityId={courseId} />\n\n {/* Discussion (platform wspace-conversations) */}\n <BuiDiscussion\n entityType={COURSE_ENTITY}\n entityId={courseId}\n description=\"Ask questions and discuss this course with your workspace.\"\n />\n </div>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { FC } from 'react';
2
+
3
+ export declare const CertificatesPage: FC;
4
+ //# sourceMappingURL=CertificatesPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CertificatesPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/certificates/CertificatesPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAqB,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AA8DnD,eAAO,MAAM,gBAAgB,EAAE,EA2X9B,CAAC"}
@@ -57,7 +57,7 @@ var le = (e) => {
57
57
  render: (e) => /* @__PURE__ */ p("div", {
58
58
  className: "flex items-center gap-3",
59
59
  children: [/* @__PURE__ */ f("div", {
60
- className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-accent",
60
+ className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-primary)]",
61
61
  children: /* @__PURE__ */ f(u, { className: "h-4 w-4" })
62
62
  }), /* @__PURE__ */ f("span", {
63
63
  className: "min-w-0 truncate font-mono text-xs font-semibold text-text-primary",
@@ -1 +1 @@
1
- {"version":3,"file":"CertificatesPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/certificates/CertificatesPage.tsx"],"sourcesContent":["/**\n * CertificatesPage — the credentialing surface for the workspace.\n * @module buildmyiq/pages/certificates\n *\n * The primary page of the Certificates section. Lists real `BuiCertificate`\n * rows (issued / expired / revoked) for the workspace, scoped by a \"Mine / All\"\n * learner toggle and an optional course filter. Facilitators can issue a new\n * certificate and revoke an issued one. Every value is derived from live `bui*`\n * data — no fabricated learners, scores, or serials.\n *\n * Reads: `buiListCertificates` (useBuiCertificates), `buiListCourses` (useBuiCourses).\n * Writes: `buiIssueCertificate` (useBuiIssueCertificate),\n * `buiRevokeCertificate` (useBuiRevokeCertificate).\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { Plus, Award, Ban, Download } from 'lucide-react';\nimport {\n Button,\n Label,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n DataTable,\n type Column,\n FilterTabs,\n type FilterOption,\n LoadingView,\n ErrorView,\n EmptyState,\n FileLink,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiCertificates,\n useBuiCourses,\n useBuiIssueCertificate,\n useBuiRevokeCertificate,\n} from '../../hooks';\nimport { formatDate } from '../../utils/formatters';\nimport type { BuiCertificate } from '../../types';\n\ntype ScopeFilter = 'MINE' | 'ALL';\ntype CourseFilter = string | 'ALL';\n\n/** Convert a native `<input type=\"date\">` value (YYYY-MM-DD) to a DateTime, or undefined. */\nconst toDateTime = (value: string): string | undefined => {\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n const d = new Date(trimmed);\n return Number.isNaN(d.getTime()) ? undefined : d.toISOString();\n};\n\n/** A collision-resistant, human-legible default serial for a new credential. */\nconst generateSerial = (): string => `BMIQ-${Date.now().toString(36).toUpperCase()}`;\n\ninterface IssueFormState {\n courseId: string;\n learnerId: string;\n serial: string;\n finalScore: string;\n expiresAt: string;\n}\n\nexport const CertificatesPage: FC = () => {\n const { currentUser } = useBuildmyiq();\n const selfId = currentUser?.id;\n\n const [scope, setScope] = useState<ScopeFilter>(selfId ? 'MINE' : 'ALL');\n const [courseFilter, setCourseFilter] = useState<CourseFilter>('ALL');\n const [dialogOpen, setDialogOpen] = useState(false);\n const [formError, setFormError] = useState<string | null>(null);\n const [actionError, setActionError] = useState<string | null>(null);\n const [revokingId, setRevokingId] = useState<string | null>(null);\n\n const makeEmptyForm = (): IssueFormState => ({\n courseId: '',\n learnerId: selfId ?? '',\n serial: generateSerial(),\n finalScore: '',\n expiresAt: '',\n });\n const [form, setForm] = useState<IssueFormState>(makeEmptyForm);\n\n // Course lookup: powers the course filter, the issue-form course picker, and\n // the courseId → title mapping in the table.\n const coursesQuery = useBuiCourses({ limit: 100 });\n const courses = useMemo(() => coursesQuery.data?.items ?? [], [coursesQuery.data]);\n const courseTitleById = useMemo(() => {\n const map = new Map<string, string>();\n for (const c of courses) map.set(c.id, c.title);\n return map;\n }, [courses]);\n\n const learnerId = scope === 'MINE' ? selfId : undefined;\n const courseId = courseFilter === 'ALL' ? undefined : courseFilter;\n\n const certsQuery = useBuiCertificates(courseId, learnerId);\n const issueCert = useBuiIssueCertificate();\n const revokeCert = useBuiRevokeCertificate();\n\n const rows = useMemo(() => certsQuery.data ?? [], [certsQuery.data]);\n\n const scopeOptions: FilterOption<ScopeFilter>[] = [\n { value: 'MINE', label: 'Mine' },\n { value: 'ALL', label: 'All' },\n ];\n\n const handleRevoke = async (cert: BuiCertificate): Promise<void> => {\n if (\n typeof window !== 'undefined' &&\n !window.confirm(`Revoke certificate ${cert.serial}? This cannot be undone.`)\n ) {\n return;\n }\n setActionError(null);\n setRevokingId(cert.id);\n try {\n await revokeCert.mutateAsync(cert.id);\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Failed to revoke certificate.');\n } finally {\n setRevokingId(null);\n }\n };\n\n const columns: Column<BuiCertificate>[] = [\n {\n key: 'serial',\n header: 'Serial',\n sortValue: (c) => c.serial.toLowerCase(),\n render: (c) => (\n <div className=\"flex items-center gap-3\">\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-accent\">\n <Award className=\"h-4 w-4\" />\n </div>\n <span className=\"min-w-0 truncate font-mono text-xs font-semibold text-text-primary\">\n {c.serial}\n </span>\n </div>\n ),\n },\n {\n key: 'course',\n header: 'Course',\n sortValue: (c) => (courseTitleById.get(c.courseId) ?? '').toLowerCase(),\n render: (c) => (\n <span className=\"text-text-secondary\">{courseTitleById.get(c.courseId) ?? '—'}</span>\n ),\n },\n {\n key: 'learner',\n header: 'Learner',\n hideOnMobile: true,\n sortValue: (c) => (c.learnerId === selfId ? '' : c.learnerId),\n render: (c) =>\n c.learnerId === selfId ? (\n <span className=\"font-medium text-text-primary\">You</span>\n ) : (\n <span className=\"font-mono text-xs text-text-tertiary\" title={c.learnerId}>\n {c.learnerId.slice(0, 8)}…\n </span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n render: (c) => <StatusBadge status={c.status} />,\n },\n {\n key: 'issuedAt',\n header: 'Issued',\n hideOnMobile: true,\n sortValue: (c) => c.issuedAt,\n render: (c) => <span className=\"text-text-secondary\">{formatDate(c.issuedAt)}</span>,\n },\n {\n key: 'expiresAt',\n header: 'Expires',\n hideOnMobile: true,\n sortValue: (c) => c.expiresAt ?? '',\n render: (c) => <span className=\"text-text-tertiary\">{formatDate(c.expiresAt)}</span>,\n },\n {\n key: 'finalScore',\n header: 'Score',\n align: 'right',\n hideOnMobile: true,\n sortValue: (c) => c.finalScore ?? -1,\n render: (c) => (\n <span className=\"tabular-nums text-text-secondary\">{c.finalScore ?? '—'}</span>\n ),\n },\n {\n key: 'file',\n header: 'File',\n hideOnMobile: true,\n render: (c) =>\n c.file ? <FileLink fileId={c.file.id} /> : <span className=\"text-text-tertiary\">—</span>,\n },\n {\n key: 'actions',\n header: '',\n align: 'right',\n render: (c) =>\n c.status === 'ISSUED' ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"text-status-error-text hover:text-status-error-text\"\n disabled={revokingId === c.id}\n onClick={() => void handleRevoke(c)}\n >\n <Ban className=\"mr-1.5 h-4 w-4\" />\n {revokingId === c.id ? 'Revoking…' : 'Revoke'}\n </Button>\n ) : null,\n },\n ];\n\n const openDialog = (): void => {\n setForm(makeEmptyForm());\n setFormError(null);\n setDialogOpen(true);\n };\n\n const closeDialog = (): void => {\n setDialogOpen(false);\n setForm(makeEmptyForm());\n setFormError(null);\n };\n\n const handleIssue = async (): Promise<void> => {\n if (!form.courseId) {\n setFormError('Please choose a course for this certificate.');\n return;\n }\n if (!form.learnerId.trim()) {\n setFormError('A learner (user ID) is required.');\n return;\n }\n if (!form.serial.trim()) {\n setFormError('A certificate serial is required.');\n return;\n }\n const scoreNum = form.finalScore.trim() ? Number(form.finalScore) : undefined;\n if (scoreNum !== undefined && (!Number.isFinite(scoreNum) || scoreNum < 0)) {\n setFormError('Final score must be a non-negative number.');\n return;\n }\n const expiresIso = toDateTime(form.expiresAt);\n\n setFormError(null);\n try {\n await issueCert.mutateAsync({\n courseId: form.courseId,\n learnerId: form.learnerId.trim(),\n serial: form.serial.trim(),\n finalScore: scoreNum,\n expiresAt: expiresIso,\n });\n closeDialog();\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to issue certificate.');\n }\n };\n\n const hasCourses = courses.length > 0;\n const isFiltered = scope === 'MINE' || courseFilter !== 'ALL';\n\n return (\n <PageLayout\n title=\"Certificates\"\n description=\"Verifiable credentials issued to learners on course completion.\"\n titleGradient\n aurora\n actions={\n <Button onClick={openDialog} disabled={!hasCourses}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Issue certificate\n </Button>\n }\n >\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <FilterTabs options={scopeOptions} value={scope} onChange={setScope} />\n <Select value={courseFilter} onValueChange={(v) => setCourseFilter(v as CourseFilter)}>\n <SelectTrigger className=\"w-56\" aria-label=\"Filter by course\">\n <SelectValue placeholder=\"All courses\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All courses</SelectItem>\n {courses.map((c) => (\n <SelectItem key={c.id} value={c.id}>\n {c.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {actionError && (\n <p className=\"mb-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\n\n {certsQuery.isLoading ? (\n <LoadingView label=\"Loading certificates…\" />\n ) : certsQuery.isError ? (\n <ErrorView message=\"Couldn't load certificates.\" onRetry={() => certsQuery.refetch()} />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration={isFiltered ? 'empty-search' : 'onboarding'}\n icon={<Award className=\"h-10 w-10\" />}\n title={isFiltered ? 'No certificates match these filters' : 'No certificates issued yet'}\n description={\n isFiltered\n ? scope === 'MINE'\n ? \"You haven't earned a certificate for this selection yet.\"\n : 'No certificates have been issued for this course yet.'\n : hasCourses\n ? 'Issue a certificate to credential a learner who has completed a course.'\n : 'Publish a course first, then issue certificates to learners who complete it.'\n }\n action={\n !isFiltered && hasCourses ? (\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Issue certificate\n </Button>\n ) : undefined\n }\n />\n ) : (\n <DataTable\n columns={columns}\n rows={rows}\n rowKey={(c) => c.id}\n defaultSort={{ key: 'issuedAt', dir: 'desc' }}\n />\n )}\n\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n if (!open) closeDialog();\n else setDialogOpen(true);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Issue certificate</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Credential a learner for completing a course. The serial is auto-generated but\n editable.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <form\n onSubmit={(e) => {\n e.preventDefault();\n void handleIssue();\n }}\n >\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cert-course\">Course *</Label>\n <Select\n value={form.courseId}\n onValueChange={(v) => setForm((prev) => ({ ...prev, courseId: v }))}\n >\n <SelectTrigger id=\"cert-course\">\n <SelectValue placeholder=\"Select a course\" />\n </SelectTrigger>\n <SelectContent>\n {courses.map((c) => (\n <SelectItem key={c.id} value={c.id}>\n {c.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cert-learner\">Learner (user ID) *</Label>\n <Input\n id=\"cert-learner\"\n value={form.learnerId}\n onChange={(e) => setForm((prev) => ({ ...prev, learnerId: e.target.value }))}\n placeholder=\"Defaults to you\"\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cert-serial\">Serial *</Label>\n <Input\n id=\"cert-serial\"\n value={form.serial}\n onChange={(e) => setForm((prev) => ({ ...prev, serial: e.target.value }))}\n placeholder=\"BMIQ-XXXXXX\"\n />\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cert-score\">Final score</Label>\n <Input\n id=\"cert-score\"\n type=\"number\"\n min=\"0\"\n step=\"0.1\"\n value={form.finalScore}\n onChange={(e) => setForm((prev) => ({ ...prev, finalScore: e.target.value }))}\n placeholder=\"e.g. 92\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cert-expires\">Expires</Label>\n <Input\n id=\"cert-expires\"\n type=\"date\"\n value={form.expiresAt}\n onChange={(e) => setForm((prev) => ({ ...prev, expiresAt: e.target.value }))}\n />\n </div>\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={closeDialog}>\n Cancel\n </Button>\n <Button type=\"submit\" disabled={issueCert.isPending}>\n <Download className=\"mr-1.5 h-4 w-4\" />\n {issueCert.isPending ? 'Issuing…' : 'Issue certificate'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"CertificatesPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/certificates/CertificatesPage.tsx"],"sourcesContent":["/**\n * CertificatesPage — the credentialing surface for the workspace.\n * @module buildmyiq/pages/certificates\n *\n * The primary page of the Certificates section. Lists real `BuiCertificate`\n * rows (issued / expired / revoked) for the workspace, scoped by a \"Mine / All\"\n * learner toggle and an optional course filter. Facilitators can issue a new\n * certificate and revoke an issued one. Every value is derived from live `bui*`\n * data — no fabricated learners, scores, or serials.\n *\n * Reads: `buiListCertificates` (useBuiCertificates), `buiListCourses` (useBuiCourses).\n * Writes: `buiIssueCertificate` (useBuiIssueCertificate),\n * `buiRevokeCertificate` (useBuiRevokeCertificate).\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { Plus, Award, Ban, Download } from 'lucide-react';\nimport {\n Button,\n Label,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n DataTable,\n type Column,\n FilterTabs,\n type FilterOption,\n LoadingView,\n ErrorView,\n EmptyState,\n FileLink,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiCertificates,\n useBuiCourses,\n useBuiIssueCertificate,\n useBuiRevokeCertificate,\n} from '../../hooks';\nimport { formatDate } from '../../utils/formatters';\nimport type { BuiCertificate } from '../../types';\n\ntype ScopeFilter = 'MINE' | 'ALL';\ntype CourseFilter = string | 'ALL';\n\n/** Convert a native `<input type=\"date\">` value (YYYY-MM-DD) to a DateTime, or undefined. */\nconst toDateTime = (value: string): string | undefined => {\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n const d = new Date(trimmed);\n return Number.isNaN(d.getTime()) ? undefined : d.toISOString();\n};\n\n/** A collision-resistant, human-legible default serial for a new credential. */\nconst generateSerial = (): string => `BMIQ-${Date.now().toString(36).toUpperCase()}`;\n\ninterface IssueFormState {\n courseId: string;\n learnerId: string;\n serial: string;\n finalScore: string;\n expiresAt: string;\n}\n\nexport const CertificatesPage: FC = () => {\n const { currentUser } = useBuildmyiq();\n const selfId = currentUser?.id;\n\n const [scope, setScope] = useState<ScopeFilter>(selfId ? 'MINE' : 'ALL');\n const [courseFilter, setCourseFilter] = useState<CourseFilter>('ALL');\n const [dialogOpen, setDialogOpen] = useState(false);\n const [formError, setFormError] = useState<string | null>(null);\n const [actionError, setActionError] = useState<string | null>(null);\n const [revokingId, setRevokingId] = useState<string | null>(null);\n\n const makeEmptyForm = (): IssueFormState => ({\n courseId: '',\n learnerId: selfId ?? '',\n serial: generateSerial(),\n finalScore: '',\n expiresAt: '',\n });\n const [form, setForm] = useState<IssueFormState>(makeEmptyForm);\n\n // Course lookup: powers the course filter, the issue-form course picker, and\n // the courseId → title mapping in the table.\n const coursesQuery = useBuiCourses({ limit: 100 });\n const courses = useMemo(() => coursesQuery.data?.items ?? [], [coursesQuery.data]);\n const courseTitleById = useMemo(() => {\n const map = new Map<string, string>();\n for (const c of courses) map.set(c.id, c.title);\n return map;\n }, [courses]);\n\n const learnerId = scope === 'MINE' ? selfId : undefined;\n const courseId = courseFilter === 'ALL' ? undefined : courseFilter;\n\n const certsQuery = useBuiCertificates(courseId, learnerId);\n const issueCert = useBuiIssueCertificate();\n const revokeCert = useBuiRevokeCertificate();\n\n const rows = useMemo(() => certsQuery.data ?? [], [certsQuery.data]);\n\n const scopeOptions: FilterOption<ScopeFilter>[] = [\n { value: 'MINE', label: 'Mine' },\n { value: 'ALL', label: 'All' },\n ];\n\n const handleRevoke = async (cert: BuiCertificate): Promise<void> => {\n if (\n typeof window !== 'undefined' &&\n !window.confirm(`Revoke certificate ${cert.serial}? This cannot be undone.`)\n ) {\n return;\n }\n setActionError(null);\n setRevokingId(cert.id);\n try {\n await revokeCert.mutateAsync(cert.id);\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Failed to revoke certificate.');\n } finally {\n setRevokingId(null);\n }\n };\n\n const columns: Column<BuiCertificate>[] = [\n {\n key: 'serial',\n header: 'Serial',\n sortValue: (c) => c.serial.toLowerCase(),\n render: (c) => (\n <div className=\"flex items-center gap-3\">\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-primary)]\">\n <Award className=\"h-4 w-4\" />\n </div>\n <span className=\"min-w-0 truncate font-mono text-xs font-semibold text-text-primary\">\n {c.serial}\n </span>\n </div>\n ),\n },\n {\n key: 'course',\n header: 'Course',\n sortValue: (c) => (courseTitleById.get(c.courseId) ?? '').toLowerCase(),\n render: (c) => (\n <span className=\"text-text-secondary\">{courseTitleById.get(c.courseId) ?? '—'}</span>\n ),\n },\n {\n key: 'learner',\n header: 'Learner',\n hideOnMobile: true,\n sortValue: (c) => (c.learnerId === selfId ? '' : c.learnerId),\n render: (c) =>\n c.learnerId === selfId ? (\n <span className=\"font-medium text-text-primary\">You</span>\n ) : (\n <span className=\"font-mono text-xs text-text-tertiary\" title={c.learnerId}>\n {c.learnerId.slice(0, 8)}…\n </span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n render: (c) => <StatusBadge status={c.status} />,\n },\n {\n key: 'issuedAt',\n header: 'Issued',\n hideOnMobile: true,\n sortValue: (c) => c.issuedAt,\n render: (c) => <span className=\"text-text-secondary\">{formatDate(c.issuedAt)}</span>,\n },\n {\n key: 'expiresAt',\n header: 'Expires',\n hideOnMobile: true,\n sortValue: (c) => c.expiresAt ?? '',\n render: (c) => <span className=\"text-text-tertiary\">{formatDate(c.expiresAt)}</span>,\n },\n {\n key: 'finalScore',\n header: 'Score',\n align: 'right',\n hideOnMobile: true,\n sortValue: (c) => c.finalScore ?? -1,\n render: (c) => (\n <span className=\"tabular-nums text-text-secondary\">{c.finalScore ?? '—'}</span>\n ),\n },\n {\n key: 'file',\n header: 'File',\n hideOnMobile: true,\n render: (c) =>\n c.file ? <FileLink fileId={c.file.id} /> : <span className=\"text-text-tertiary\">—</span>,\n },\n {\n key: 'actions',\n header: '',\n align: 'right',\n render: (c) =>\n c.status === 'ISSUED' ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"text-status-error-text hover:text-status-error-text\"\n disabled={revokingId === c.id}\n onClick={() => void handleRevoke(c)}\n >\n <Ban className=\"mr-1.5 h-4 w-4\" />\n {revokingId === c.id ? 'Revoking…' : 'Revoke'}\n </Button>\n ) : null,\n },\n ];\n\n const openDialog = (): void => {\n setForm(makeEmptyForm());\n setFormError(null);\n setDialogOpen(true);\n };\n\n const closeDialog = (): void => {\n setDialogOpen(false);\n setForm(makeEmptyForm());\n setFormError(null);\n };\n\n const handleIssue = async (): Promise<void> => {\n if (!form.courseId) {\n setFormError('Please choose a course for this certificate.');\n return;\n }\n if (!form.learnerId.trim()) {\n setFormError('A learner (user ID) is required.');\n return;\n }\n if (!form.serial.trim()) {\n setFormError('A certificate serial is required.');\n return;\n }\n const scoreNum = form.finalScore.trim() ? Number(form.finalScore) : undefined;\n if (scoreNum !== undefined && (!Number.isFinite(scoreNum) || scoreNum < 0)) {\n setFormError('Final score must be a non-negative number.');\n return;\n }\n const expiresIso = toDateTime(form.expiresAt);\n\n setFormError(null);\n try {\n await issueCert.mutateAsync({\n courseId: form.courseId,\n learnerId: form.learnerId.trim(),\n serial: form.serial.trim(),\n finalScore: scoreNum,\n expiresAt: expiresIso,\n });\n closeDialog();\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to issue certificate.');\n }\n };\n\n const hasCourses = courses.length > 0;\n const isFiltered = scope === 'MINE' || courseFilter !== 'ALL';\n\n return (\n <PageLayout\n title=\"Certificates\"\n description=\"Verifiable credentials issued to learners on course completion.\"\n titleGradient\n aurora\n actions={\n <Button onClick={openDialog} disabled={!hasCourses}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Issue certificate\n </Button>\n }\n >\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <FilterTabs options={scopeOptions} value={scope} onChange={setScope} />\n <Select value={courseFilter} onValueChange={(v) => setCourseFilter(v as CourseFilter)}>\n <SelectTrigger className=\"w-56\" aria-label=\"Filter by course\">\n <SelectValue placeholder=\"All courses\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All courses</SelectItem>\n {courses.map((c) => (\n <SelectItem key={c.id} value={c.id}>\n {c.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {actionError && (\n <p className=\"mb-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\n\n {certsQuery.isLoading ? (\n <LoadingView label=\"Loading certificates…\" />\n ) : certsQuery.isError ? (\n <ErrorView message=\"Couldn't load certificates.\" onRetry={() => certsQuery.refetch()} />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration={isFiltered ? 'empty-search' : 'onboarding'}\n icon={<Award className=\"h-10 w-10\" />}\n title={isFiltered ? 'No certificates match these filters' : 'No certificates issued yet'}\n description={\n isFiltered\n ? scope === 'MINE'\n ? \"You haven't earned a certificate for this selection yet.\"\n : 'No certificates have been issued for this course yet.'\n : hasCourses\n ? 'Issue a certificate to credential a learner who has completed a course.'\n : 'Publish a course first, then issue certificates to learners who complete it.'\n }\n action={\n !isFiltered && hasCourses ? (\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Issue certificate\n </Button>\n ) : undefined\n }\n />\n ) : (\n <DataTable\n columns={columns}\n rows={rows}\n rowKey={(c) => c.id}\n defaultSort={{ key: 'issuedAt', dir: 'desc' }}\n />\n )}\n\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n if (!open) closeDialog();\n else setDialogOpen(true);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>Issue certificate</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Credential a learner for completing a course. The serial is auto-generated but\n editable.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <form\n onSubmit={(e) => {\n e.preventDefault();\n void handleIssue();\n }}\n >\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cert-course\">Course *</Label>\n <Select\n value={form.courseId}\n onValueChange={(v) => setForm((prev) => ({ ...prev, courseId: v }))}\n >\n <SelectTrigger id=\"cert-course\">\n <SelectValue placeholder=\"Select a course\" />\n </SelectTrigger>\n <SelectContent>\n {courses.map((c) => (\n <SelectItem key={c.id} value={c.id}>\n {c.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cert-learner\">Learner (user ID) *</Label>\n <Input\n id=\"cert-learner\"\n value={form.learnerId}\n onChange={(e) => setForm((prev) => ({ ...prev, learnerId: e.target.value }))}\n placeholder=\"Defaults to you\"\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cert-serial\">Serial *</Label>\n <Input\n id=\"cert-serial\"\n value={form.serial}\n onChange={(e) => setForm((prev) => ({ ...prev, serial: e.target.value }))}\n placeholder=\"BMIQ-XXXXXX\"\n />\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cert-score\">Final score</Label>\n <Input\n id=\"cert-score\"\n type=\"number\"\n min=\"0\"\n step=\"0.1\"\n value={form.finalScore}\n onChange={(e) => setForm((prev) => ({ ...prev, finalScore: e.target.value }))}\n placeholder=\"e.g. 92\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cert-expires\">Expires</Label>\n <Input\n id=\"cert-expires\"\n type=\"date\"\n value={form.expiresAt}\n onChange={(e) => setForm((prev) => ({ ...prev, expiresAt: e.target.value }))}\n />\n </div>\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={closeDialog}>\n Cancel\n </Button>\n <Button type=\"submit\" disabled={issueCert.isPending}>\n <Download className=\"mr-1.5 h-4 w-4\" />\n {issueCert.isPending ? 'Issuing…' : 'Issue certificate'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { FC } from 'react';
2
+
3
+ export declare const CohortsPage: FC;
4
+ //# sourceMappingURL=CohortsPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CohortsPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/cohorts/CohortsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAqB,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAsDnD,eAAO,MAAM,WAAW,EAAE,EAiWzB,CAAC"}
@@ -53,7 +53,7 @@ var A = (e) => {
53
53
  render: (e) => /* @__PURE__ */ _("div", {
54
54
  className: "flex items-center gap-3",
55
55
  children: [/* @__PURE__ */ g("div", {
56
- className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-accent",
56
+ className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-primary)]",
57
57
  children: /* @__PURE__ */ g(h, { className: "h-4 w-4" })
58
58
  }), /* @__PURE__ */ g("span", {
59
59
  className: "min-w-0 truncate font-semibold text-text-primary",
@@ -1 +1 @@
1
- {"version":3,"file":"CohortsPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/cohorts/CohortsPage.tsx"],"sourcesContent":["/**\n * CohortsPage — scheduled learning cohorts for the workspace.\n * @module buildmyiq/pages/cohorts\n *\n * The primary surface of the Cohorts section. Lists real `BuiCohort` rows for\n * the workspace (optionally scoped to a course), lets facilitators filter by\n * lifecycle status, and create a new cohort. Every value is derived from live\n * `bui*` data — enrolled counts are NOT queryable per-cohort, so we only ever\n * surface the declared `capacity` (never a fabricated enrolment number).\n *\n * Reads: `buiListCohorts` (useBuiCohorts), `buiListCourses` (useBuiCourses).\n * Writes: `buiCreateCohort` (useBuiCreateCohort).\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { Plus, Users, CalendarRange } from 'lucide-react';\nimport {\n Button,\n Label,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n DataTable,\n type Column,\n FilterTabs,\n type FilterOption,\n LoadingView,\n ErrorView,\n EmptyState,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiCohorts, useBuiCourses, useBuiCreateCohort } from '../../hooks';\nimport { formatDate, formatEnumLabel, formatNumber } from '../../utils/formatters';\nimport { COHORT_STATUSES, type BuiCohort, type BuiCohortStatus } from '../../types';\n\ntype StatusFilter = BuiCohortStatus | 'ALL';\ntype CourseFilter = string | 'ALL';\n\n/** Convert a native `<input type=\"date\">` value (YYYY-MM-DD) to a DateTime, or undefined. */\nconst toDateTime = (value: string): string | undefined => {\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n const d = new Date(trimmed);\n return Number.isNaN(d.getTime()) ? undefined : d.toISOString();\n};\n\ninterface CreateFormState {\n courseId: string;\n name: string;\n capacity: string;\n facilitatorId: string;\n startDate: string;\n endDate: string;\n}\n\nexport const CohortsPage: FC = () => {\n const { currentUser } = useBuildmyiq();\n\n const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL');\n const [courseFilter, setCourseFilter] = useState<CourseFilter>('ALL');\n const [dialogOpen, setDialogOpen] = useState(false);\n const [formError, setFormError] = useState<string | null>(null);\n\n const emptyForm = useMemo<CreateFormState>(\n () => ({\n courseId: '',\n name: '',\n capacity: '',\n facilitatorId: currentUser?.id ?? '',\n startDate: '',\n endDate: '',\n }),\n [currentUser?.id]\n );\n const [form, setForm] = useState<CreateFormState>(emptyForm);\n\n // Course lookup: powers the course filter, the create-form course picker, and\n // the courseId → title mapping in the table.\n const coursesQuery = useBuiCourses({ limit: 100 });\n const courses = useMemo(() => coursesQuery.data?.items ?? [], [coursesQuery.data]);\n const courseTitleById = useMemo(() => {\n const map = new Map<string, string>();\n for (const c of courses) map.set(c.id, c.title);\n return map;\n }, [courses]);\n\n // Fetch cohorts scoped only by course — status is filtered client-side so the\n // status tabs can carry accurate per-status counts from a single fetch.\n const cohortsQuery = useBuiCohorts(courseFilter === 'ALL' ? undefined : courseFilter);\n const createCohort = useBuiCreateCohort();\n\n const allCohorts = useMemo(() => cohortsQuery.data ?? [], [cohortsQuery.data]);\n\n const statusCounts = useMemo(() => {\n const counts: Record<string, number> = {};\n for (const c of allCohorts) counts[c.status] = (counts[c.status] ?? 0) + 1;\n return counts;\n }, [allCohorts]);\n\n const rows = useMemo(\n () =>\n statusFilter === 'ALL' ? allCohorts : allCohorts.filter((c) => c.status === statusFilter),\n [allCohorts, statusFilter]\n );\n\n const statusOptions: FilterOption<StatusFilter>[] = useMemo(\n () => [\n { value: 'ALL', label: 'All', count: allCohorts.length },\n ...COHORT_STATUSES.map((s) => ({\n value: s,\n label: formatEnumLabel(s),\n count: statusCounts[s] ?? 0,\n })),\n ],\n [allCohorts.length, statusCounts]\n );\n\n const columns: Column<BuiCohort>[] = useMemo(\n () => [\n {\n key: 'name',\n header: 'Cohort',\n sortValue: (c) => c.name.toLowerCase(),\n render: (c) => (\n <div className=\"flex items-center gap-3\">\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-accent\">\n <Users className=\"h-4 w-4\" />\n </div>\n <span className=\"min-w-0 truncate font-semibold text-text-primary\">{c.name}</span>\n </div>\n ),\n },\n {\n key: 'course',\n header: 'Course',\n hideOnMobile: true,\n sortValue: (c) => (courseTitleById.get(c.courseId) ?? '').toLowerCase(),\n render: (c) => (\n <span className=\"text-text-secondary\">{courseTitleById.get(c.courseId) ?? '—'}</span>\n ),\n },\n {\n key: 'capacity',\n header: 'Capacity',\n align: 'right',\n sortValue: (c) => c.capacity,\n render: (c) => (\n <span className=\"tabular-nums text-text-secondary\">{formatNumber(c.capacity ?? 0)}</span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n render: (c) => <StatusBadge status={c.status} />,\n },\n {\n key: 'startDate',\n header: 'Starts',\n align: 'right',\n hideOnMobile: true,\n sortValue: (c) => c.startDate ?? '',\n render: (c) => <span className=\"text-text-tertiary\">{formatDate(c.startDate)}</span>,\n },\n {\n key: 'endDate',\n header: 'Ends',\n align: 'right',\n hideOnMobile: true,\n sortValue: (c) => c.endDate ?? '',\n render: (c) => <span className=\"text-text-tertiary\">{formatDate(c.endDate)}</span>,\n },\n ],\n [courseTitleById]\n );\n\n const openDialog = () => {\n setForm(emptyForm);\n setFormError(null);\n setDialogOpen(true);\n };\n\n const closeDialog = () => {\n setDialogOpen(false);\n setForm(emptyForm);\n setFormError(null);\n };\n\n const handleCreate = async () => {\n if (!form.courseId) {\n setFormError('Please choose a course for this cohort.');\n return;\n }\n if (!form.name.trim()) {\n setFormError('Cohort name is required.');\n return;\n }\n const capacityNum = form.capacity.trim() ? Number(form.capacity) : undefined;\n if (capacityNum !== undefined && (!Number.isFinite(capacityNum) || capacityNum < 0)) {\n setFormError('Capacity must be a non-negative number.');\n return;\n }\n const startIso = toDateTime(form.startDate);\n const endIso = toDateTime(form.endDate);\n if (startIso && endIso && new Date(endIso) < new Date(startIso)) {\n setFormError('End date cannot be before the start date.');\n return;\n }\n\n setFormError(null);\n try {\n await createCohort.mutateAsync({\n courseId: form.courseId,\n name: form.name.trim(),\n capacity: capacityNum,\n facilitatorId: form.facilitatorId.trim() || undefined,\n startDate: startIso,\n endDate: endIso,\n });\n closeDialog();\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to create cohort.');\n }\n };\n\n const hasCourses = courses.length > 0;\n\n return (\n <PageLayout\n title=\"Cohorts\"\n description=\"Scheduled, facilitator-led groups that move through a course together.\"\n titleGradient\n aurora\n actions={\n <Button onClick={openDialog} disabled={!hasCourses}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New cohort\n </Button>\n }\n >\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <FilterTabs options={statusOptions} value={statusFilter} onChange={setStatusFilter} />\n <Select value={courseFilter} onValueChange={(v) => setCourseFilter(v as CourseFilter)}>\n <SelectTrigger className=\"w-56\" aria-label=\"Filter by course\">\n <SelectValue placeholder=\"All courses\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All courses</SelectItem>\n {courses.map((c) => (\n <SelectItem key={c.id} value={c.id}>\n {c.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {cohortsQuery.isLoading ? (\n <LoadingView label=\"Loading cohorts…\" />\n ) : cohortsQuery.isError ? (\n <ErrorView message=\"Couldn't load cohorts.\" onRetry={() => cohortsQuery.refetch()} />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration={\n statusFilter === 'ALL' && courseFilter === 'ALL' ? 'onboarding' : 'empty-search'\n }\n icon={<CalendarRange className=\"h-10 w-10\" />}\n title={\n statusFilter === 'ALL' && courseFilter === 'ALL'\n ? 'No cohorts yet'\n : 'No cohorts match these filters'\n }\n description={\n statusFilter === 'ALL' && courseFilter === 'ALL'\n ? hasCourses\n ? 'Schedule a cohort to run a course with a group of learners on a shared timeline.'\n : 'Publish a course first, then schedule a cohort to run it with a group of learners.'\n : 'Try a different status or course to see more cohorts.'\n }\n action={\n statusFilter === 'ALL' && courseFilter === 'ALL' && hasCourses ? (\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New cohort\n </Button>\n ) : undefined\n }\n />\n ) : (\n <DataTable\n columns={columns}\n rows={rows}\n rowKey={(c) => c.id}\n defaultSort={{ key: 'startDate', dir: 'desc' }}\n />\n )}\n\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n if (!open) closeDialog();\n else setDialogOpen(true);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New cohort</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Schedule a facilitator-led group to run a course together.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <form\n onSubmit={(e) => {\n e.preventDefault();\n void handleCreate();\n }}\n >\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-course\">Course *</Label>\n <Select\n value={form.courseId}\n onValueChange={(v) => setForm((prev) => ({ ...prev, courseId: v }))}\n >\n <SelectTrigger id=\"cohort-course\">\n <SelectValue placeholder=\"Select a course\" />\n </SelectTrigger>\n <SelectContent>\n {courses.map((c) => (\n <SelectItem key={c.id} value={c.id}>\n {c.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-name\">Name *</Label>\n <Input\n id=\"cohort-name\"\n value={form.name}\n onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}\n placeholder=\"Spring 2026 Cohort\"\n />\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-capacity\">Capacity</Label>\n <Input\n id=\"cohort-capacity\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={form.capacity}\n onChange={(e) => setForm((prev) => ({ ...prev, capacity: e.target.value }))}\n placeholder=\"25\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-facilitator\">Facilitator (user ID)</Label>\n <Input\n id=\"cohort-facilitator\"\n value={form.facilitatorId}\n onChange={(e) =>\n setForm((prev) => ({ ...prev, facilitatorId: e.target.value }))\n }\n placeholder=\"Defaults to you\"\n />\n </div>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-start\">Start date</Label>\n <Input\n id=\"cohort-start\"\n type=\"date\"\n value={form.startDate}\n onChange={(e) => setForm((prev) => ({ ...prev, startDate: e.target.value }))}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-end\">End date</Label>\n <Input\n id=\"cohort-end\"\n type=\"date\"\n value={form.endDate}\n onChange={(e) => setForm((prev) => ({ ...prev, endDate: e.target.value }))}\n />\n </div>\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={closeDialog}>\n Cancel\n </Button>\n <Button type=\"submit\" disabled={createCohort.isPending}>\n {createCohort.isPending ? 'Creating…' : 'Create cohort'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"CohortsPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/cohorts/CohortsPage.tsx"],"sourcesContent":["/**\n * CohortsPage — scheduled learning cohorts for the workspace.\n * @module buildmyiq/pages/cohorts\n *\n * The primary surface of the Cohorts section. Lists real `BuiCohort` rows for\n * the workspace (optionally scoped to a course), lets facilitators filter by\n * lifecycle status, and create a new cohort. Every value is derived from live\n * `bui*` data — enrolled counts are NOT queryable per-cohort, so we only ever\n * surface the declared `capacity` (never a fabricated enrolment number).\n *\n * Reads: `buiListCohorts` (useBuiCohorts), `buiListCourses` (useBuiCourses).\n * Writes: `buiCreateCohort` (useBuiCreateCohort).\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { Plus, Users, CalendarRange } from 'lucide-react';\nimport {\n Button,\n Label,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n DataTable,\n type Column,\n FilterTabs,\n type FilterOption,\n LoadingView,\n ErrorView,\n EmptyState,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiCohorts, useBuiCourses, useBuiCreateCohort } from '../../hooks';\nimport { formatDate, formatEnumLabel, formatNumber } from '../../utils/formatters';\nimport { COHORT_STATUSES, type BuiCohort, type BuiCohortStatus } from '../../types';\n\ntype StatusFilter = BuiCohortStatus | 'ALL';\ntype CourseFilter = string | 'ALL';\n\n/** Convert a native `<input type=\"date\">` value (YYYY-MM-DD) to a DateTime, or undefined. */\nconst toDateTime = (value: string): string | undefined => {\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n const d = new Date(trimmed);\n return Number.isNaN(d.getTime()) ? undefined : d.toISOString();\n};\n\ninterface CreateFormState {\n courseId: string;\n name: string;\n capacity: string;\n facilitatorId: string;\n startDate: string;\n endDate: string;\n}\n\nexport const CohortsPage: FC = () => {\n const { currentUser } = useBuildmyiq();\n\n const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL');\n const [courseFilter, setCourseFilter] = useState<CourseFilter>('ALL');\n const [dialogOpen, setDialogOpen] = useState(false);\n const [formError, setFormError] = useState<string | null>(null);\n\n const emptyForm = useMemo<CreateFormState>(\n () => ({\n courseId: '',\n name: '',\n capacity: '',\n facilitatorId: currentUser?.id ?? '',\n startDate: '',\n endDate: '',\n }),\n [currentUser?.id]\n );\n const [form, setForm] = useState<CreateFormState>(emptyForm);\n\n // Course lookup: powers the course filter, the create-form course picker, and\n // the courseId → title mapping in the table.\n const coursesQuery = useBuiCourses({ limit: 100 });\n const courses = useMemo(() => coursesQuery.data?.items ?? [], [coursesQuery.data]);\n const courseTitleById = useMemo(() => {\n const map = new Map<string, string>();\n for (const c of courses) map.set(c.id, c.title);\n return map;\n }, [courses]);\n\n // Fetch cohorts scoped only by course — status is filtered client-side so the\n // status tabs can carry accurate per-status counts from a single fetch.\n const cohortsQuery = useBuiCohorts(courseFilter === 'ALL' ? undefined : courseFilter);\n const createCohort = useBuiCreateCohort();\n\n const allCohorts = useMemo(() => cohortsQuery.data ?? [], [cohortsQuery.data]);\n\n const statusCounts = useMemo(() => {\n const counts: Record<string, number> = {};\n for (const c of allCohorts) counts[c.status] = (counts[c.status] ?? 0) + 1;\n return counts;\n }, [allCohorts]);\n\n const rows = useMemo(\n () =>\n statusFilter === 'ALL' ? allCohorts : allCohorts.filter((c) => c.status === statusFilter),\n [allCohorts, statusFilter]\n );\n\n const statusOptions: FilterOption<StatusFilter>[] = useMemo(\n () => [\n { value: 'ALL', label: 'All', count: allCohorts.length },\n ...COHORT_STATUSES.map((s) => ({\n value: s,\n label: formatEnumLabel(s),\n count: statusCounts[s] ?? 0,\n })),\n ],\n [allCohorts.length, statusCounts]\n );\n\n const columns: Column<BuiCohort>[] = useMemo(\n () => [\n {\n key: 'name',\n header: 'Cohort',\n sortValue: (c) => c.name.toLowerCase(),\n render: (c) => (\n <div className=\"flex items-center gap-3\">\n <div className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-[var(--color-accent-primary)]\">\n <Users className=\"h-4 w-4\" />\n </div>\n <span className=\"min-w-0 truncate font-semibold text-text-primary\">{c.name}</span>\n </div>\n ),\n },\n {\n key: 'course',\n header: 'Course',\n hideOnMobile: true,\n sortValue: (c) => (courseTitleById.get(c.courseId) ?? '').toLowerCase(),\n render: (c) => (\n <span className=\"text-text-secondary\">{courseTitleById.get(c.courseId) ?? '—'}</span>\n ),\n },\n {\n key: 'capacity',\n header: 'Capacity',\n align: 'right',\n sortValue: (c) => c.capacity,\n render: (c) => (\n <span className=\"tabular-nums text-text-secondary\">{formatNumber(c.capacity ?? 0)}</span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n render: (c) => <StatusBadge status={c.status} />,\n },\n {\n key: 'startDate',\n header: 'Starts',\n align: 'right',\n hideOnMobile: true,\n sortValue: (c) => c.startDate ?? '',\n render: (c) => <span className=\"text-text-tertiary\">{formatDate(c.startDate)}</span>,\n },\n {\n key: 'endDate',\n header: 'Ends',\n align: 'right',\n hideOnMobile: true,\n sortValue: (c) => c.endDate ?? '',\n render: (c) => <span className=\"text-text-tertiary\">{formatDate(c.endDate)}</span>,\n },\n ],\n [courseTitleById]\n );\n\n const openDialog = () => {\n setForm(emptyForm);\n setFormError(null);\n setDialogOpen(true);\n };\n\n const closeDialog = () => {\n setDialogOpen(false);\n setForm(emptyForm);\n setFormError(null);\n };\n\n const handleCreate = async () => {\n if (!form.courseId) {\n setFormError('Please choose a course for this cohort.');\n return;\n }\n if (!form.name.trim()) {\n setFormError('Cohort name is required.');\n return;\n }\n const capacityNum = form.capacity.trim() ? Number(form.capacity) : undefined;\n if (capacityNum !== undefined && (!Number.isFinite(capacityNum) || capacityNum < 0)) {\n setFormError('Capacity must be a non-negative number.');\n return;\n }\n const startIso = toDateTime(form.startDate);\n const endIso = toDateTime(form.endDate);\n if (startIso && endIso && new Date(endIso) < new Date(startIso)) {\n setFormError('End date cannot be before the start date.');\n return;\n }\n\n setFormError(null);\n try {\n await createCohort.mutateAsync({\n courseId: form.courseId,\n name: form.name.trim(),\n capacity: capacityNum,\n facilitatorId: form.facilitatorId.trim() || undefined,\n startDate: startIso,\n endDate: endIso,\n });\n closeDialog();\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to create cohort.');\n }\n };\n\n const hasCourses = courses.length > 0;\n\n return (\n <PageLayout\n title=\"Cohorts\"\n description=\"Scheduled, facilitator-led groups that move through a course together.\"\n titleGradient\n aurora\n actions={\n <Button onClick={openDialog} disabled={!hasCourses}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New cohort\n </Button>\n }\n >\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <FilterTabs options={statusOptions} value={statusFilter} onChange={setStatusFilter} />\n <Select value={courseFilter} onValueChange={(v) => setCourseFilter(v as CourseFilter)}>\n <SelectTrigger className=\"w-56\" aria-label=\"Filter by course\">\n <SelectValue placeholder=\"All courses\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All courses</SelectItem>\n {courses.map((c) => (\n <SelectItem key={c.id} value={c.id}>\n {c.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {cohortsQuery.isLoading ? (\n <LoadingView label=\"Loading cohorts…\" />\n ) : cohortsQuery.isError ? (\n <ErrorView message=\"Couldn't load cohorts.\" onRetry={() => cohortsQuery.refetch()} />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration={\n statusFilter === 'ALL' && courseFilter === 'ALL' ? 'onboarding' : 'empty-search'\n }\n icon={<CalendarRange className=\"h-10 w-10\" />}\n title={\n statusFilter === 'ALL' && courseFilter === 'ALL'\n ? 'No cohorts yet'\n : 'No cohorts match these filters'\n }\n description={\n statusFilter === 'ALL' && courseFilter === 'ALL'\n ? hasCourses\n ? 'Schedule a cohort to run a course with a group of learners on a shared timeline.'\n : 'Publish a course first, then schedule a cohort to run it with a group of learners.'\n : 'Try a different status or course to see more cohorts.'\n }\n action={\n statusFilter === 'ALL' && courseFilter === 'ALL' && hasCourses ? (\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New cohort\n </Button>\n ) : undefined\n }\n />\n ) : (\n <DataTable\n columns={columns}\n rows={rows}\n rowKey={(c) => c.id}\n defaultSort={{ key: 'startDate', dir: 'desc' }}\n />\n )}\n\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n if (!open) closeDialog();\n else setDialogOpen(true);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New cohort</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Schedule a facilitator-led group to run a course together.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <form\n onSubmit={(e) => {\n e.preventDefault();\n void handleCreate();\n }}\n >\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-course\">Course *</Label>\n <Select\n value={form.courseId}\n onValueChange={(v) => setForm((prev) => ({ ...prev, courseId: v }))}\n >\n <SelectTrigger id=\"cohort-course\">\n <SelectValue placeholder=\"Select a course\" />\n </SelectTrigger>\n <SelectContent>\n {courses.map((c) => (\n <SelectItem key={c.id} value={c.id}>\n {c.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-name\">Name *</Label>\n <Input\n id=\"cohort-name\"\n value={form.name}\n onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}\n placeholder=\"Spring 2026 Cohort\"\n />\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-capacity\">Capacity</Label>\n <Input\n id=\"cohort-capacity\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={form.capacity}\n onChange={(e) => setForm((prev) => ({ ...prev, capacity: e.target.value }))}\n placeholder=\"25\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-facilitator\">Facilitator (user ID)</Label>\n <Input\n id=\"cohort-facilitator\"\n value={form.facilitatorId}\n onChange={(e) =>\n setForm((prev) => ({ ...prev, facilitatorId: e.target.value }))\n }\n placeholder=\"Defaults to you\"\n />\n </div>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-start\">Start date</Label>\n <Input\n id=\"cohort-start\"\n type=\"date\"\n value={form.startDate}\n onChange={(e) => setForm((prev) => ({ ...prev, startDate: e.target.value }))}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-end\">End date</Label>\n <Input\n id=\"cohort-end\"\n type=\"date\"\n value={form.endDate}\n onChange={(e) => setForm((prev) => ({ ...prev, endDate: e.target.value }))}\n />\n </div>\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={closeDialog}>\n Cancel\n </Button>\n <Button type=\"submit\" disabled={createCohort.isPending}>\n {createCohort.isPending ? 'Creating…' : 'Create cohort'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { FC } from 'react';
2
+
3
+ export declare const InsightsPage: FC;
4
+ //# sourceMappingURL=InsightsPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"InsightsPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/insights/InsightsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAoChC,eAAO,MAAM,YAAY,EAAE,EAoN1B,CAAC"}