@burdenoff/microfe-buildmyiq 2026.706.11 → 2026.709.2
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.
- package/dist/buildmyiq/BuildmyiqRoutes.js +67 -49
- package/dist/buildmyiq/BuildmyiqRoutes.js.map +1 -1
- package/dist/buildmyiq/components/Avatar.js.map +1 -1
- package/dist/buildmyiq/components/BuiDiscussion.js +1 -1
- package/dist/buildmyiq/components/Viz.js.map +1 -1
- package/dist/buildmyiq/index.js +1 -1
- package/dist/buildmyiq/pages/DashboardPage.js +159 -122
- package/dist/buildmyiq/pages/DashboardPage.js.map +1 -1
- package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.js +1 -1
- package/dist/buildmyiq/pages/assessments/AssessmentDetailPage.js.map +1 -1
- package/dist/buildmyiq/pages/assessments/AssessmentsPage.js +1 -1
- package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.js +1 -1
- package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.js.map +1 -1
- package/dist/buildmyiq/pages/authoring/AuthoringPage.js +1 -1
- package/dist/buildmyiq/pages/authoring/CourseBuilderPage.js +1 -1
- package/dist/buildmyiq/pages/authoring/CourseBuilderPage.js.map +1 -1
- package/dist/buildmyiq/pages/catalog/CatalogPage.js +1 -1
- package/dist/buildmyiq/pages/catalog/CourseDetailPage.js +1 -1
- package/dist/buildmyiq/pages/certificates/CertificatesPage.js +1 -1
- package/dist/buildmyiq/pages/certificates/CertificatesPage.js.map +1 -1
- package/dist/buildmyiq/pages/cohorts/CohortsPage.js +1 -1
- package/dist/buildmyiq/pages/insights/InsightsPage.js.map +1 -1
- package/dist/buildmyiq/pages/learning/LessonPlayerPage.js +1 -1
- package/dist/buildmyiq/pages/learning/MyLearningPage.js +1 -1
- package/dist/buildmyiq/pages/learning/MyLearningPage.js.map +1 -1
- package/dist/buildmyiq/pages/paths/PathsPage.js +1 -1
- package/dist/buildmyiq/pages/paths/PathsPage.js.map +1 -1
- package/dist/buildmyiq/pages/skills/SkillsPage.js.map +1 -1
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.js +5 -9
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.js.map +1 -1
- package/dist/buildmyiq/shared/hooks/useTr.js +14 -0
- package/dist/buildmyiq/shared/hooks/useTr.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CourseBuilderPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/authoring/CourseBuilderPage.tsx"],"sourcesContent":["/**\n * CourseBuilderPage — the per-course authoring workspace.\n * @module buildmyiq/pages/authoring\n *\n * Loads one course (`useBuiCourse`) and exposes four tabs:\n * - Details — editable course meta (`useBuiUpdateCourse`).\n * - Curriculum — the module/lesson builder (`useBuiModules`, `useBuiCreateModule`,\n * `useBuiCreateLesson`) plus a right rail with the cover-image dropzone\n * (EvidenceUpload → coverFileId) and a live course-details summary.\n * - Assessments — course assessments (`useBuiAssessments`) linking out to the\n * assessment editor.\n * - Discussion — the shared wspace-conversations thread (`BuiDiscussion`).\n *\n * Publish (`useBuiPublishCourse`) and Archive (`useBuiArchiveCourse`) live in the\n * page header so they are reachable from every tab. Every list carries loading /\n * error / empty states; all data is live `bui*` data — no fabrication.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n Archive,\n ArrowLeft,\n BarChart3,\n BookOpen,\n Calendar,\n ClipboardList,\n Clock,\n Eye,\n FileText,\n Globe,\n Image as ImageIcon,\n Link2,\n ListChecks,\n MessageSquare,\n MousePointerClick,\n PlayCircle,\n Plus,\n Radio,\n Send,\n type LucideIcon,\n} from 'lucide-react';\nimport { Button, GlassCard, Input, Switch, Textarea } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n Tabs,\n type TabItem,\n StatusBadge,\n EmptyState,\n LoadingView,\n ErrorView,\n Section,\n EvidenceUpload,\n BuiDiscussion,\n} from '../../components';\nimport { Modal, Field, SelectField, NO_SKILL } from './_forms';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiCourse,\n useBuiUpdateCourse,\n useBuiPublishCourse,\n useBuiArchiveCourse,\n useBuiModules,\n useBuiCreateModule,\n useBuiCreateLesson,\n useBuiAssessments,\n useBuiSkills,\n type UploadedBuiFile,\n} from '../../hooks';\nimport { COURSE_LEVELS, LESSON_TYPES } from '../../types';\nimport type {\n BuiCourse,\n BuiCourseLevel,\n BuiCreateLessonInput,\n BuiCreateModuleInput,\n BuiLesson,\n BuiLessonType,\n BuiModule,\n BuiUpdateCourseInput,\n} from '../../types';\nimport { cn } from '../../utils/cn';\nimport { formatDate, formatDuration, formatEnumLabel } from '../../utils/formatters';\n\ntype BuilderTab = 'details' | 'curriculum' | 'assessments' | 'discussion';\n\n/** Per-lesson-type icon + theme-safe tint (semantic tokens only). */\nconst LESSON_META: Record<BuiLessonType, { icon: LucideIcon; tint: string }> = {\n ARTICLE: { icon: FileText, tint: 'bg-bg-sunken text-text-secondary' },\n VIDEO: { icon: PlayCircle, tint: 'bg-[var(--color-accent-soft)] text-text-primary' },\n QUIZ: { icon: ListChecks, tint: 'bg-status-warning-bg-subtle text-status-warning-text' },\n ASSIGNMENT: {\n icon: ClipboardList,\n tint: 'bg-status-success-bg-subtle text-status-success-text',\n },\n INTERACTIVE: {\n icon: MousePointerClick,\n tint: 'bg-[color-mix(in_srgb,var(--color-accent-teal)_18%,transparent)] text-[var(--color-accent-teal)]',\n },\n LIVE_SESSION: {\n icon: Radio,\n tint: 'bg-[color-mix(in_srgb,var(--color-accent-primary)_16%,transparent)] text-[var(--color-accent-primary)]',\n },\n};\n\n// ============================================================================\n// Details tab — pure metadata form (publish/archive live in the page header,\n// cover upload lives in the Curriculum right rail).\n// ============================================================================\n\nconst DetailsTab: FC<{ course: BuiCourse }> = ({ course }) => {\n const updateCourse = useBuiUpdateCourse();\n const skillsQuery = useBuiSkills();\n const skills = skillsQuery.data ?? [];\n\n const [title, setTitle] = useState(course.title);\n const [summary, setSummary] = useState(course.summary ?? '');\n const [description, setDescription] = useState(course.description ?? '');\n const [level, setLevel] = useState<BuiCourseLevel>(course.level);\n const [language, setLanguage] = useState(course.language);\n const [estimatedHours, setEstimatedHours] = useState(String(course.estimatedHours ?? 0));\n const [skillId, setSkillId] = useState<string>(course.skillId ?? NO_SKILL);\n const [isPublic, setIsPublic] = useState(course.isPublic);\n const [saveError, setSaveError] = useState<string | null>(null);\n\n const handleSave = async () => {\n setSaveError(null);\n const input: BuiUpdateCourseInput = {\n title: title.trim() || course.title,\n summary: summary.trim() || undefined,\n description: description.trim() || undefined,\n level,\n language: language.trim() || 'en',\n estimatedHours: estimatedHours.trim() ? Number(estimatedHours) : 0,\n skillId: skillId !== NO_SKILL ? skillId : undefined,\n isPublic,\n };\n try {\n await updateCourse.mutateAsync({ id: course.id, input });\n } catch (e) {\n setSaveError(e instanceof Error ? e.message : 'Failed to save changes.');\n }\n };\n\n return (\n <div className=\"mx-auto max-w-3xl\">\n <Section\n title=\"Course details\"\n description=\"Metadata learners see in the catalog.\"\n action={\n <Button onClick={handleSave} disabled={updateCourse.isPending}>\n {updateCourse.isPending ? 'Saving…' : 'Save changes'}\n </Button>\n }\n >\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <div className=\"grid gap-4\">\n <Field label=\"Title\" htmlFor=\"detail-title\" required>\n <Input id=\"detail-title\" value={title} onChange={(e) => setTitle(e.target.value)} />\n </Field>\n <Field label=\"Summary\" htmlFor=\"detail-summary\">\n <Textarea\n id=\"detail-summary\"\n rows={2}\n value={summary}\n onChange={(e) => setSummary(e.target.value)}\n placeholder=\"A short description shown on the catalog card.\"\n />\n </Field>\n <Field label=\"Description\" htmlFor=\"detail-description\">\n <Textarea\n id=\"detail-description\"\n rows={5}\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n placeholder=\"The full course overview.\"\n />\n </Field>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <Field label=\"Level\">\n <SelectField\n value={level}\n onChange={(v) => setLevel(v as BuiCourseLevel)}\n options={COURSE_LEVELS.map((l) => ({ value: l, label: formatEnumLabel(l) }))}\n />\n </Field>\n <Field label=\"Language\" htmlFor=\"detail-language\">\n <Input\n id=\"detail-language\"\n value={language}\n onChange={(e) => setLanguage(e.target.value)}\n placeholder=\"en\"\n />\n </Field>\n <Field label=\"Estimated hours\" htmlFor=\"detail-hours\">\n <Input\n id=\"detail-hours\"\n type=\"number\"\n min={0}\n value={estimatedHours}\n onChange={(e) => setEstimatedHours(e.target.value)}\n />\n </Field>\n <Field label=\"Skill\">\n <SelectField\n value={skillId}\n onChange={setSkillId}\n options={[\n { value: NO_SKILL, label: skillsQuery.isLoading ? 'Loading…' : 'No skill' },\n ...skills.map((s) => ({ value: s.id, label: s.name })),\n ]}\n />\n </Field>\n </div>\n <label className=\"inline-flex cursor-pointer items-center gap-2 text-sm text-text-secondary\">\n <Switch checked={isPublic} onCheckedChange={setIsPublic} />\n Publicly visible in the catalog\n </label>\n {saveError && <p className=\"text-sm text-status-error-text\">{saveError}</p>}\n </div>\n </GlassCard>\n </Section>\n </div>\n );\n};\n\n// ============================================================================\n// Curriculum tab\n// ============================================================================\n\nconst AddModuleModal: FC<{ courseId: string; position: number; onClose: () => void }> = ({\n courseId,\n position,\n onClose,\n}) => {\n const createModule = useBuiCreateModule();\n const [title, setTitle] = useState('');\n const [description, setDescription] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const canSubmit = title.trim().length > 0 && !createModule.isPending;\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n setError(null);\n const input: BuiCreateModuleInput = {\n courseId,\n title: title.trim(),\n description: description.trim() || undefined,\n position,\n };\n try {\n await createModule.mutateAsync(input);\n onClose();\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Failed to add the module.');\n }\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"Add module\"\n footer={\n <>\n <Button variant=\"ghost\" onClick={onClose} disabled={createModule.isPending}>\n Cancel\n </Button>\n <Button onClick={handleSubmit} disabled={!canSubmit}>\n {createModule.isPending ? 'Adding…' : 'Add module'}\n </Button>\n </>\n }\n >\n <div className=\"grid gap-4\">\n <Field label=\"Module title\" htmlFor=\"module-title\" required>\n <Input\n id=\"module-title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. Getting started\"\n />\n </Field>\n <Field label=\"Description\" htmlFor=\"module-description\">\n <Textarea\n id=\"module-description\"\n rows={3}\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n />\n </Field>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n </Modal>\n );\n};\n\nconst AddLessonModal: FC<{\n courseId: string;\n moduleId: string;\n position: number;\n onClose: () => void;\n}> = ({ courseId, moduleId, position, onClose }) => {\n const createLesson = useBuiCreateLesson();\n const [title, setTitle] = useState('');\n const [type, setType] = useState<BuiLessonType>('ARTICLE');\n const [content, setContent] = useState('');\n const [durationMin, setDurationMin] = useState('');\n const [isPreview, setIsPreview] = useState(false);\n const [media, setMedia] = useState<UploadedBuiFile | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const canSubmit = title.trim().length > 0 && !createLesson.isPending;\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n setError(null);\n const input: BuiCreateLessonInput = {\n courseId,\n moduleId,\n title: title.trim(),\n type,\n content: content.trim() || undefined,\n durationMin: durationMin.trim() ? Number(durationMin) : undefined,\n position,\n isPreview,\n mediaFileId: media?.fileId,\n };\n try {\n await createLesson.mutateAsync(input);\n onClose();\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Failed to add the lesson.');\n }\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"Add lesson\"\n className=\"sm:max-w-xl\"\n footer={\n <>\n <Button variant=\"ghost\" onClick={onClose} disabled={createLesson.isPending}>\n Cancel\n </Button>\n <Button onClick={handleSubmit} disabled={!canSubmit}>\n {createLesson.isPending ? 'Adding…' : 'Add lesson'}\n </Button>\n </>\n }\n >\n <div className=\"grid gap-4\">\n <Field label=\"Lesson title\" htmlFor=\"lesson-title\" required>\n <Input\n id=\"lesson-title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. What is data literacy?\"\n />\n </Field>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <Field label=\"Type\">\n <SelectField\n value={type}\n onChange={(v) => setType(v as BuiLessonType)}\n options={LESSON_TYPES.map((t) => ({ value: t, label: formatEnumLabel(t) }))}\n />\n </Field>\n <Field label=\"Duration (minutes)\" htmlFor=\"lesson-duration\">\n <Input\n id=\"lesson-duration\"\n type=\"number\"\n min={0}\n value={durationMin}\n onChange={(e) => setDurationMin(e.target.value)}\n placeholder=\"0\"\n />\n </Field>\n </div>\n <Field\n label=\"Content\"\n htmlFor=\"lesson-content\"\n hint=\"Lesson body — markdown or plain text.\"\n >\n <Textarea\n id=\"lesson-content\"\n rows={5}\n value={content}\n onChange={(e) => setContent(e.target.value)}\n />\n </Field>\n <EvidenceUpload\n id=\"lesson-media\"\n label=\"Media (optional)\"\n value={media}\n onChange={setMedia}\n hint=\"Attach a video, audio file or PDF for this lesson.\"\n />\n <label className=\"inline-flex cursor-pointer items-center gap-2 text-sm text-text-secondary\">\n <Switch checked={isPreview} onCheckedChange={setIsPreview} />\n Free preview lesson\n </label>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n </Modal>\n );\n};\n\nconst LessonRow: FC<{ lesson: BuiLesson }> = ({ lesson }) => {\n const meta = LESSON_META[lesson.type];\n const Icon = meta.icon;\n return (\n <li className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface px-3 py-2.5\">\n <span\n className={cn('flex h-8 w-8 shrink-0 items-center justify-center rounded-lg', meta.tint)}\n aria-hidden=\"true\"\n >\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.durationMin > 0 && (\n <span className=\"inline-flex shrink-0 items-center gap-1 text-xs text-text-secondary\">\n <Clock className=\"h-3.5 w-3.5\" /> {formatDuration(lesson.durationMin)}\n </span>\n )}\n {lesson.isPreview && (\n <span className=\"shrink-0 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary\">\n Preview\n </span>\n )}\n </li>\n );\n};\n\nconst ModuleCard: FC<{ module: BuiModule; index: number; onAddLesson: () => void }> = ({\n module,\n index,\n onAddLesson,\n}) => {\n const lessons = [...(module.lessons ?? [])].sort((a, b) => a.position - b.position);\n const totalMin = lessons.reduce((sum, l) => sum + (l.durationMin || 0), 0);\n return (\n <GlassCard treatment=\"plain\" className=\"overflow-hidden\">\n <div className=\"flex flex-wrap items-center gap-3 border-b border-border-seam bg-bg-sunken/40 px-4 py-3\">\n <span\n className=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-action-primary-bg text-sm font-semibold tabular-nums text-action-primary-text\"\n aria-hidden=\"true\"\n >\n {index + 1}\n </span>\n <div className=\"min-w-0 flex-1\">\n <h3 className=\"truncate font-semibold text-text-primary\">\n <span className=\"text-text-tertiary\">Module {index + 1} · </span>\n {module.title}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {lessons.length} {lessons.length === 1 ? 'lesson' : 'lessons'}\n {totalMin > 0 && ` · ${formatDuration(totalMin)}`}\n </p>\n </div>\n <Button variant=\"outline\" size=\"sm\" onClick={onAddLesson} className=\"shrink-0\">\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add lesson\n </Button>\n </div>\n {module.description && (\n <p className=\"border-b border-border-seam px-4 py-2.5 text-sm text-text-secondary\">\n {module.description}\n </p>\n )}\n <div className=\"p-3\">\n {lessons.length === 0 ? (\n <button\n type=\"button\"\n onClick={onAddLesson}\n className=\"flex w-full items-center justify-center gap-2 rounded-lg border border-dashed border-border-seam px-3 py-3 text-sm text-text-secondary transition-colors hover:border-[var(--color-action-primary-bg)] hover:text-text-primary\"\n >\n <Plus className=\"h-4 w-4\" /> Add the first lesson\n </button>\n ) : (\n <ul className=\"flex flex-col gap-2\">\n {lessons.map((lesson) => (\n <LessonRow key={lesson.id} lesson={lesson} />\n ))}\n </ul>\n )}\n </div>\n </GlassCard>\n );\n};\n\nconst DetailRow: FC<{ icon: ReactNode; label: string; value: string }> = ({\n icon,\n label,\n value,\n}) => (\n <div className=\"flex items-center gap-3 py-2.5\">\n <span className=\"shrink-0 text-text-tertiary\" aria-hidden=\"true\">\n {icon}\n </span>\n <span className=\"text-sm text-text-secondary\">{label}</span>\n <span className=\"ml-auto min-w-0 truncate text-right text-sm font-medium text-text-primary\">\n {value}\n </span>\n </div>\n);\n\nconst CurriculumTab: FC<{ course: BuiCourse }> = ({ course }) => {\n const courseId = course.id;\n const modulesQuery = useBuiModules(courseId);\n const updateCourse = useBuiUpdateCourse();\n const [showAddModule, setShowAddModule] = useState(false);\n const [lessonModuleId, setLessonModuleId] = useState<string | null>(null);\n const [coverFile, setCoverFile] = useState<UploadedBuiFile | null>(null);\n const [coverError, setCoverError] = useState<string | null>(null);\n\n const modules = [...(modulesQuery.data ?? [])].sort((a, b) => a.position - b.position);\n const lessonModule = modules.find((m) => m.id === lessonModuleId) ?? null;\n\n const handleCoverChange = async (file: UploadedBuiFile | null) => {\n setCoverFile(file);\n if (!file) return;\n setCoverError(null);\n try {\n await updateCourse.mutateAsync({ id: courseId, input: { coverFileId: file.fileId } });\n } catch (e) {\n setCoverError(e instanceof Error ? e.message : 'Failed to update the cover.');\n }\n };\n\n return (\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Builder */}\n <div className=\"lg:col-span-2\">\n <Section\n title=\"Curriculum\"\n description=\"Organise the course into modules and lessons.\"\n action={\n <Button onClick={() => setShowAddModule(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add module\n </Button>\n }\n >\n {modulesQuery.isLoading ? (\n <LoadingView label=\"Loading curriculum…\" />\n ) : modulesQuery.isError ? (\n <ErrorView\n message=\"We couldn't load the curriculum.\"\n onRetry={() => modulesQuery.refetch()}\n />\n ) : modules.length === 0 ? (\n <EmptyState\n illustration=\"onboarding\"\n title=\"No modules yet\"\n description=\"Add your first module, then fill it with lessons.\"\n action={\n <Button onClick={() => setShowAddModule(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add module\n </Button>\n }\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n {modules.map((mod, idx) => (\n <ModuleCard\n key={mod.id}\n module={mod}\n index={idx}\n onAddLesson={() => setLessonModuleId(mod.id)}\n />\n ))}\n <button\n type=\"button\"\n onClick={() => setShowAddModule(true)}\n className=\"flex w-full items-center justify-center gap-2 rounded-xl border-2 border-dashed border-border-seam px-4 py-4 text-sm font-medium text-text-secondary transition-colors hover:border-[var(--color-action-primary-bg)] hover:bg-bg-sunken/40 hover:text-text-primary\"\n >\n <Plus className=\"h-4 w-4\" /> Add module\n </button>\n </div>\n )}\n </Section>\n </div>\n\n {/* Right rail: cover + details */}\n <div className=\"flex flex-col gap-6\">\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"text-base font-semibold text-text-primary\">Course cover</h3>\n <p className=\"mt-0.5 text-sm text-text-secondary\">\n Upload an image to represent your course.\n </p>\n <div className=\"mt-3 aspect-video w-full overflow-hidden rounded-xl border border-border-seam bg-bg-sunken\">\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 items-center justify-center gap-2 text-sm text-text-tertiary\">\n <ImageIcon className=\"h-5 w-5\" /> No cover yet\n </div>\n )}\n </div>\n <div className=\"mt-3\">\n <EvidenceUpload\n id=\"course-cover\"\n label={course.cover?.downloadUrl ? 'Replace cover' : 'Upload cover'}\n value={coverFile}\n onChange={handleCoverChange}\n accept=\"image/*\"\n hint=\"PNG or JPG · 16:9 ratio recommended.\"\n />\n {coverError && <p className=\"mt-1 text-xs text-status-error-text\">{coverError}</p>}\n </div>\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"text-base font-semibold text-text-primary\">Course details</h3>\n <dl className=\"mt-2 flex flex-col divide-y divide-border-seam\">\n <DetailRow icon={<Link2 className=\"h-4 w-4\" />} label=\"Slug\" value={course.slug} />\n <DetailRow\n icon={<Globe className=\"h-4 w-4\" />}\n label=\"Language\"\n value={course.language ? course.language.toUpperCase() : '—'}\n />\n <DetailRow\n icon={<BarChart3 className=\"h-4 w-4\" />}\n label=\"Level\"\n value={formatEnumLabel(course.level)}\n />\n <DetailRow\n icon={<Eye className=\"h-4 w-4\" />}\n label=\"Visibility\"\n value={course.isPublic ? 'Public' : 'Private'}\n />\n <DetailRow\n icon={<Calendar className=\"h-4 w-4\" />}\n label=\"Created\"\n value={formatDate(course.createdAt)}\n />\n </dl>\n </GlassCard>\n </div>\n\n {showAddModule && (\n <AddModuleModal\n courseId={courseId}\n position={modules.length}\n onClose={() => setShowAddModule(false)}\n />\n )}\n {lessonModuleId && (\n <AddLessonModal\n courseId={courseId}\n moduleId={lessonModuleId}\n position={lessonModule?.lessons?.length ?? 0}\n onClose={() => setLessonModuleId(null)}\n />\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Assessments tab\n// ============================================================================\n\nconst AssessmentsTab: FC<{ courseId: string }> = ({ courseId }) => {\n const { navigateTo } = useBuildmyiq();\n const assessmentsQuery = useBuiAssessments(courseId);\n const assessments = assessmentsQuery.data ?? [];\n\n return (\n <Section\n title=\"Assessments\"\n description=\"Quizzes, exams and assignments attached to this course.\"\n >\n {assessmentsQuery.isLoading ? (\n <LoadingView label=\"Loading assessments…\" />\n ) : assessmentsQuery.isError ? (\n <ErrorView\n message=\"We couldn't load the assessments.\"\n onRetry={() => assessmentsQuery.refetch()}\n />\n ) : assessments.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No assessments yet\"\n description=\"Create an assessment in the Assessments workspace to test learners on this course.\"\n action={\n <Button variant=\"outline\" onClick={() => navigateTo('assessments')}>\n Open assessments\n </Button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {assessments.map((a) => (\n <GlassCard\n key={a.id}\n treatment=\"plain\"\n className=\"flex cursor-pointer flex-col gap-2 p-4 transition-shadow hover:shadow-lg\"\n onClick={() => navigateTo(`assessments/${a.id}`)}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <h3 className=\"min-w-0 font-semibold text-text-primary\">{a.title}</h3>\n <StatusBadge status={a.status} />\n </div>\n {a.description && (\n <p className=\"line-clamp-2 text-sm text-text-secondary\">{a.description}</p>\n )}\n <div className=\"mt-auto flex flex-wrap items-center gap-3 pt-1 text-xs text-text-secondary\">\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5\">\n {formatEnumLabel(a.type)}\n </span>\n <span>Pass ≥ {a.passingScore ?? 0}%</span>\n {a.timeLimitMin > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" /> {formatDuration(a.timeLimitMin)}\n </span>\n )}\n </div>\n </GlassCard>\n ))}\n </div>\n )}\n </Section>\n );\n};\n\n// ============================================================================\n// Page\n// ============================================================================\n\nexport const CourseBuilderPage: FC = () => {\n const { courseId } = useParams<{ courseId: string }>();\n const { navigateTo } = useBuildmyiq();\n const [tab, setTab] = useState<BuilderTab>('details');\n const courseQuery = useBuiCourse(courseId);\n const course = courseQuery.data ?? null;\n\n const publishCourse = useBuiPublishCourse();\n const archiveCourse = useBuiArchiveCourse();\n const [actionError, setActionError] = useState<string | null>(null);\n const [confirmArchive, setConfirmArchive] = useState(false);\n\n const handlePublish = async () => {\n if (!course) return;\n setActionError(null);\n try {\n await publishCourse.mutateAsync(course.id);\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'Failed to publish the course.');\n }\n };\n\n const handleArchive = async () => {\n if (!course) return;\n setActionError(null);\n try {\n await archiveCourse.mutateAsync(course.id);\n setConfirmArchive(false);\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'Failed to archive the course.');\n }\n };\n\n if (courseQuery.isLoading) {\n return (\n <PageLayout title=\"Course builder\">\n <LoadingView label=\"Loading course…\" />\n </PageLayout>\n );\n }\n\n if (courseQuery.isError) {\n return (\n <PageLayout title=\"Course builder\">\n <ErrorView message=\"We couldn't load this course.\" onRetry={() => courseQuery.refetch()} />\n </PageLayout>\n );\n }\n\n if (!courseId || !course) {\n return (\n <PageLayout title=\"Course builder\">\n <EmptyState\n illustration=\"empty-search\"\n title=\"Course not found\"\n description=\"This course may have been archived or removed.\"\n action={<Button onClick={() => navigateTo('authoring')}>Back to authoring</Button>}\n />\n </PageLayout>\n );\n }\n\n const canPublish = course.status === 'DRAFT' || course.status === 'IN_REVIEW';\n const canArchive = course.status !== 'ARCHIVED';\n\n const tabs: TabItem<BuilderTab>[] = [\n { value: 'details', label: 'Details', icon: <FileText className=\"h-4 w-4\" /> },\n {\n value: 'curriculum',\n label: 'Curriculum',\n icon: <BookOpen className=\"h-4 w-4\" />,\n count: course.modules?.length ?? 0,\n },\n { value: 'assessments', label: 'Assessments', icon: <ListChecks className=\"h-4 w-4\" /> },\n { value: 'discussion', label: 'Discussion', icon: <MessageSquare className=\"h-4 w-4\" /> },\n ];\n\n return (\n <PageLayout\n title={\n <span className=\"flex flex-wrap items-center gap-3\">\n <span className=\"min-w-0\">{course.title}</span>\n <StatusBadge status={course.status} />\n <StatusBadge status={course.level} />\n </span>\n }\n description={course.summary ?? undefined}\n actions={\n <>\n <Button variant=\"ghost\" onClick={() => navigateTo('authoring')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Back\n </Button>\n {canArchive && (\n <Button variant=\"outline\" onClick={() => setConfirmArchive(true)}>\n <Archive className=\"mr-1.5 h-4 w-4\" /> Archive\n </Button>\n )}\n {canPublish && (\n <Button onClick={handlePublish} disabled={publishCourse.isPending}>\n <Send className=\"mr-1.5 h-4 w-4\" />\n {publishCourse.isPending ? 'Publishing…' : 'Publish'}\n </Button>\n )}\n </>\n }\n >\n <div className=\"flex flex-col gap-5\">\n {actionError && (\n <p className=\"rounded-lg border border-status-error-bg-subtle bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\n\n <Tabs items={tabs} value={tab} onChange={setTab} />\n\n {tab === 'details' && <DetailsTab key={course.id} course={course} />}\n {tab === 'curriculum' && <CurriculumTab course={course} />}\n {tab === 'assessments' && <AssessmentsTab courseId={courseId} />}\n {tab === 'discussion' && (\n <BuiDiscussion entityType=\"buildmyiq-course\" entityId={courseId} />\n )}\n </div>\n\n {confirmArchive && (\n <Modal\n open\n onClose={() => setConfirmArchive(false)}\n title=\"Archive this course?\"\n description=\"Archived courses are hidden from the catalog and cannot be enrolled in.\"\n footer={\n <>\n <Button\n variant=\"ghost\"\n onClick={() => setConfirmArchive(false)}\n disabled={archiveCourse.isPending}\n >\n Cancel\n </Button>\n <Button variant=\"destructive\" onClick={handleArchive} disabled={archiveCourse.isPending}>\n {archiveCourse.isPending ? 'Archiving…' : 'Archive course'}\n </Button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n You can still find it in Authoring under the Archived filter.\n </p>\n </Modal>\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CourseBuilderPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/authoring/CourseBuilderPage.tsx"],"sourcesContent":["/**\n * CourseBuilderPage — the per-course authoring workspace.\n * @module buildmyiq/pages/authoring\n *\n * Loads one course (`useBuiCourse`) and exposes four tabs:\n * - Details — editable course meta (`useBuiUpdateCourse`).\n * - Curriculum — the module/lesson builder (`useBuiModules`, `useBuiCreateModule`,\n * `useBuiCreateLesson`) plus a right rail with the cover-image dropzone\n * (EvidenceUpload → coverFileId) and a live course-details summary.\n * - Assessments — course assessments (`useBuiAssessments`) linking out to the\n * assessment editor.\n * - Discussion — the shared wspace-conversations thread (`BuiDiscussion`).\n *\n * Publish (`useBuiPublishCourse`) and Archive (`useBuiArchiveCourse`) live in the\n * page header so they are reachable from every tab. Every list carries loading /\n * error / empty states; all data is live `bui*` data — no fabrication.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n Archive,\n ArrowLeft,\n BarChart3,\n BookOpen,\n Calendar,\n ClipboardList,\n Clock,\n Eye,\n FileText,\n Globe,\n Image as ImageIcon,\n Link2,\n ListChecks,\n MessageSquare,\n MousePointerClick,\n PlayCircle,\n Plus,\n Radio,\n Send,\n type LucideIcon,\n} from 'lucide-react';\nimport { Button, GlassCard, Input, Switch, Textarea } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n Tabs,\n type TabItem,\n StatusBadge,\n EmptyState,\n LoadingView,\n ErrorView,\n Section,\n EvidenceUpload,\n BuiDiscussion,\n} from '../../components';\nimport { Modal, Field, SelectField, NO_SKILL } from './_forms';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiCourse,\n useBuiUpdateCourse,\n useBuiPublishCourse,\n useBuiArchiveCourse,\n useBuiModules,\n useBuiCreateModule,\n useBuiCreateLesson,\n useBuiAssessments,\n useBuiSkills,\n type UploadedBuiFile,\n} from '../../hooks';\nimport { COURSE_LEVELS, LESSON_TYPES } from '../../types';\nimport type {\n BuiCourse,\n BuiCourseLevel,\n BuiCreateLessonInput,\n BuiCreateModuleInput,\n BuiLesson,\n BuiLessonType,\n BuiModule,\n BuiUpdateCourseInput,\n} from '../../types';\nimport { cn } from '../../utils/cn';\nimport { formatDate, formatDuration, formatEnumLabel } from '../../utils/formatters';\n\ntype BuilderTab = 'details' | 'curriculum' | 'assessments' | 'discussion';\n\n/** Per-lesson-type icon + theme-safe tint (semantic tokens only). */\nconst LESSON_META: Record<BuiLessonType, { icon: LucideIcon; tint: string }> = {\n ARTICLE: { icon: FileText, tint: 'bg-bg-sunken text-text-secondary' },\n VIDEO: { icon: PlayCircle, tint: 'bg-[var(--color-accent-soft)] text-text-primary' },\n QUIZ: { icon: ListChecks, tint: 'bg-status-warning-bg-subtle text-status-warning-text' },\n ASSIGNMENT: {\n icon: ClipboardList,\n tint: 'bg-status-success-bg-subtle text-status-success-text',\n },\n INTERACTIVE: {\n icon: MousePointerClick,\n tint: 'bg-[color-mix(in_srgb,var(--color-accent-teal)_18%,transparent)] text-[var(--color-accent-teal)]',\n },\n LIVE_SESSION: {\n icon: Radio,\n tint: 'bg-[color-mix(in_srgb,var(--color-accent-primary)_16%,transparent)] text-[var(--color-accent-primary)]',\n },\n};\n\n// ============================================================================\n// Details tab — pure metadata form (publish/archive live in the page header,\n// cover upload lives in the Curriculum right rail).\n// ============================================================================\n\nconst DetailsTab: FC<{ course: BuiCourse }> = ({ course }) => {\n const updateCourse = useBuiUpdateCourse();\n const skillsQuery = useBuiSkills();\n const skills = skillsQuery.data ?? [];\n\n const [title, setTitle] = useState(course.title);\n const [summary, setSummary] = useState(course.summary ?? '');\n const [description, setDescription] = useState(course.description ?? '');\n const [level, setLevel] = useState<BuiCourseLevel>(course.level);\n const [language, setLanguage] = useState(course.language);\n const [estimatedHours, setEstimatedHours] = useState(String(course.estimatedHours ?? 0));\n const [skillId, setSkillId] = useState<string>(course.skillId ?? NO_SKILL);\n const [isPublic, setIsPublic] = useState(course.isPublic);\n const [saveError, setSaveError] = useState<string | null>(null);\n\n const handleSave = async () => {\n setSaveError(null);\n const input: BuiUpdateCourseInput = {\n title: title.trim() || course.title,\n summary: summary.trim() || undefined,\n description: description.trim() || undefined,\n level,\n language: language.trim() || 'en',\n estimatedHours: estimatedHours.trim() ? Number(estimatedHours) : 0,\n skillId: skillId !== NO_SKILL ? skillId : undefined,\n isPublic,\n };\n try {\n await updateCourse.mutateAsync({ id: course.id, input });\n } catch (e) {\n setSaveError(e instanceof Error ? e.message : 'Failed to save changes.');\n }\n };\n\n return (\n <div className=\"mx-auto max-w-3xl\">\n <Section\n title=\"Course details\"\n description=\"Metadata learners see in the catalog.\"\n action={\n <Button onClick={handleSave} disabled={updateCourse.isPending}>\n {updateCourse.isPending ? 'Saving…' : 'Save changes'}\n </Button>\n }\n >\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <div className=\"grid gap-4\">\n <Field label=\"Title\" htmlFor=\"detail-title\" required>\n <Input id=\"detail-title\" value={title} onChange={(e) => setTitle(e.target.value)} />\n </Field>\n <Field label=\"Summary\" htmlFor=\"detail-summary\">\n <Textarea\n id=\"detail-summary\"\n rows={2}\n value={summary}\n onChange={(e) => setSummary(e.target.value)}\n placeholder=\"A short description shown on the catalog card.\"\n />\n </Field>\n <Field label=\"Description\" htmlFor=\"detail-description\">\n <Textarea\n id=\"detail-description\"\n rows={5}\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n placeholder=\"The full course overview.\"\n />\n </Field>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <Field label=\"Level\">\n <SelectField\n value={level}\n onChange={(v) => setLevel(v as BuiCourseLevel)}\n options={COURSE_LEVELS.map((l) => ({ value: l, label: formatEnumLabel(l) }))}\n />\n </Field>\n <Field label=\"Language\" htmlFor=\"detail-language\">\n <Input\n id=\"detail-language\"\n value={language}\n onChange={(e) => setLanguage(e.target.value)}\n placeholder=\"en\"\n />\n </Field>\n <Field label=\"Estimated hours\" htmlFor=\"detail-hours\">\n <Input\n id=\"detail-hours\"\n type=\"number\"\n min={0}\n value={estimatedHours}\n onChange={(e) => setEstimatedHours(e.target.value)}\n />\n </Field>\n <Field label=\"Skill\">\n <SelectField\n value={skillId}\n onChange={setSkillId}\n options={[\n { value: NO_SKILL, label: skillsQuery.isLoading ? 'Loading…' : 'No skill' },\n ...skills.map((s) => ({ value: s.id, label: s.name })),\n ]}\n />\n </Field>\n </div>\n <label className=\"inline-flex cursor-pointer items-center gap-2 text-sm text-text-secondary\">\n <Switch checked={isPublic} onCheckedChange={setIsPublic} />\n Publicly visible in the catalog\n </label>\n {saveError && <p className=\"text-sm text-status-error-text\">{saveError}</p>}\n </div>\n </GlassCard>\n </Section>\n </div>\n );\n};\n\n// ============================================================================\n// Curriculum tab\n// ============================================================================\n\nconst AddModuleModal: FC<{ courseId: string; position: number; onClose: () => void }> = ({\n courseId,\n position,\n onClose,\n}) => {\n const createModule = useBuiCreateModule();\n const [title, setTitle] = useState('');\n const [description, setDescription] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const canSubmit = title.trim().length > 0 && !createModule.isPending;\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n setError(null);\n const input: BuiCreateModuleInput = {\n courseId,\n title: title.trim(),\n description: description.trim() || undefined,\n position,\n };\n try {\n await createModule.mutateAsync(input);\n onClose();\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Failed to add the module.');\n }\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"Add module\"\n footer={\n <>\n <Button variant=\"ghost\" onClick={onClose} disabled={createModule.isPending}>\n Cancel\n </Button>\n <Button onClick={handleSubmit} disabled={!canSubmit}>\n {createModule.isPending ? 'Adding…' : 'Add module'}\n </Button>\n </>\n }\n >\n <div className=\"grid gap-4\">\n <Field label=\"Module title\" htmlFor=\"module-title\" required>\n <Input\n id=\"module-title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. Getting started\"\n />\n </Field>\n <Field label=\"Description\" htmlFor=\"module-description\">\n <Textarea\n id=\"module-description\"\n rows={3}\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n />\n </Field>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n </Modal>\n );\n};\n\nconst AddLessonModal: FC<{\n courseId: string;\n moduleId: string;\n position: number;\n onClose: () => void;\n}> = ({ courseId, moduleId, position, onClose }) => {\n const createLesson = useBuiCreateLesson();\n const [title, setTitle] = useState('');\n const [type, setType] = useState<BuiLessonType>('ARTICLE');\n const [content, setContent] = useState('');\n const [durationMin, setDurationMin] = useState('');\n const [isPreview, setIsPreview] = useState(false);\n const [media, setMedia] = useState<UploadedBuiFile | null>(null);\n const [error, setError] = useState<string | null>(null);\n\n const canSubmit = title.trim().length > 0 && !createLesson.isPending;\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n setError(null);\n const input: BuiCreateLessonInput = {\n courseId,\n moduleId,\n title: title.trim(),\n type,\n content: content.trim() || undefined,\n durationMin: durationMin.trim() ? Number(durationMin) : undefined,\n position,\n isPreview,\n mediaFileId: media?.fileId,\n };\n try {\n await createLesson.mutateAsync(input);\n onClose();\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Failed to add the lesson.');\n }\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"Add lesson\"\n className=\"sm:max-w-xl\"\n footer={\n <>\n <Button variant=\"ghost\" onClick={onClose} disabled={createLesson.isPending}>\n Cancel\n </Button>\n <Button onClick={handleSubmit} disabled={!canSubmit}>\n {createLesson.isPending ? 'Adding…' : 'Add lesson'}\n </Button>\n </>\n }\n >\n <div className=\"grid gap-4\">\n <Field label=\"Lesson title\" htmlFor=\"lesson-title\" required>\n <Input\n id=\"lesson-title\"\n value={title}\n onChange={(e) => setTitle(e.target.value)}\n placeholder=\"e.g. What is data literacy?\"\n />\n </Field>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <Field label=\"Type\">\n <SelectField\n value={type}\n onChange={(v) => setType(v as BuiLessonType)}\n options={LESSON_TYPES.map((t) => ({ value: t, label: formatEnumLabel(t) }))}\n />\n </Field>\n <Field label=\"Duration (minutes)\" htmlFor=\"lesson-duration\">\n <Input\n id=\"lesson-duration\"\n type=\"number\"\n min={0}\n value={durationMin}\n onChange={(e) => setDurationMin(e.target.value)}\n placeholder=\"0\"\n />\n </Field>\n </div>\n <Field\n label=\"Content\"\n htmlFor=\"lesson-content\"\n hint=\"Lesson body — markdown or plain text.\"\n >\n <Textarea\n id=\"lesson-content\"\n rows={5}\n value={content}\n onChange={(e) => setContent(e.target.value)}\n />\n </Field>\n <EvidenceUpload\n id=\"lesson-media\"\n label=\"Media (optional)\"\n value={media}\n onChange={setMedia}\n hint=\"Attach a video, audio file or PDF for this lesson.\"\n />\n <label className=\"inline-flex cursor-pointer items-center gap-2 text-sm text-text-secondary\">\n <Switch checked={isPreview} onCheckedChange={setIsPreview} />\n Free preview lesson\n </label>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n </Modal>\n );\n};\n\nconst LessonRow: FC<{ lesson: BuiLesson }> = ({ lesson }) => {\n const meta = LESSON_META[lesson.type];\n const Icon = meta.icon;\n return (\n <li className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface px-3 py-2.5\">\n <span\n className={cn('flex h-8 w-8 shrink-0 items-center justify-center rounded-lg', meta.tint)}\n aria-hidden=\"true\"\n >\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.durationMin > 0 && (\n <span className=\"inline-flex shrink-0 items-center gap-1 text-xs text-text-secondary\">\n <Clock className=\"h-3.5 w-3.5\" /> {formatDuration(lesson.durationMin)}\n </span>\n )}\n {lesson.isPreview && (\n <span className=\"shrink-0 rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-xs font-medium text-text-primary\">\n Preview\n </span>\n )}\n </li>\n );\n};\n\nconst ModuleCard: FC<{ module: BuiModule; index: number; onAddLesson: () => void }> = ({\n module,\n index,\n onAddLesson,\n}) => {\n const lessons = [...(module.lessons ?? [])].sort((a, b) => a.position - b.position);\n const totalMin = lessons.reduce((sum, l) => sum + (l.durationMin || 0), 0);\n return (\n <GlassCard treatment=\"plain\" className=\"overflow-hidden\">\n <div className=\"flex flex-wrap items-center gap-3 border-b border-border-seam bg-bg-sunken/40 px-4 py-3\">\n <span\n className=\"flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-action-primary-bg text-sm font-semibold tabular-nums text-action-primary-text\"\n aria-hidden=\"true\"\n >\n {index + 1}\n </span>\n <div className=\"min-w-0 flex-1\">\n <h3 className=\"truncate font-semibold text-text-primary\">\n <span className=\"text-text-tertiary\">Module {index + 1} · </span>\n {module.title}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {lessons.length} {lessons.length === 1 ? 'lesson' : 'lessons'}\n {totalMin > 0 && ` · ${formatDuration(totalMin)}`}\n </p>\n </div>\n <Button variant=\"outline\" size=\"sm\" onClick={onAddLesson} className=\"shrink-0\">\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add lesson\n </Button>\n </div>\n {module.description && (\n <p className=\"border-b border-border-seam px-4 py-2.5 text-sm text-text-secondary\">\n {module.description}\n </p>\n )}\n <div className=\"p-3\">\n {lessons.length === 0 ? (\n <button\n type=\"button\"\n onClick={onAddLesson}\n className=\"flex w-full items-center justify-center gap-2 rounded-lg border border-dashed border-border-seam px-3 py-3 text-sm text-text-secondary transition-colors hover:border-[var(--color-action-primary-bg)] hover:text-text-primary\"\n >\n <Plus className=\"h-4 w-4\" /> Add the first lesson\n </button>\n ) : (\n <ul className=\"flex flex-col gap-2\">\n {lessons.map((lesson) => (\n <LessonRow key={lesson.id} lesson={lesson} />\n ))}\n </ul>\n )}\n </div>\n </GlassCard>\n );\n};\n\nconst DetailRow: FC<{ icon: ReactNode; label: string; value: string }> = ({\n icon,\n label,\n value,\n}) => (\n <div className=\"flex items-center gap-3 py-2.5\">\n <span className=\"shrink-0 text-text-tertiary\" aria-hidden=\"true\">\n {icon}\n </span>\n <span className=\"text-sm text-text-secondary\">{label}</span>\n <span className=\"ml-auto min-w-0 truncate text-right text-sm font-medium text-text-primary\">\n {value}\n </span>\n </div>\n);\n\nconst CurriculumTab: FC<{ course: BuiCourse }> = ({ course }) => {\n const courseId = course.id;\n const modulesQuery = useBuiModules(courseId);\n const updateCourse = useBuiUpdateCourse();\n const [showAddModule, setShowAddModule] = useState(false);\n const [lessonModuleId, setLessonModuleId] = useState<string | null>(null);\n const [coverFile, setCoverFile] = useState<UploadedBuiFile | null>(null);\n const [coverError, setCoverError] = useState<string | null>(null);\n\n const modules = [...(modulesQuery.data ?? [])].sort((a, b) => a.position - b.position);\n const lessonModule = modules.find((m) => m.id === lessonModuleId) ?? null;\n\n const handleCoverChange = async (file: UploadedBuiFile | null) => {\n setCoverFile(file);\n if (!file) return;\n setCoverError(null);\n try {\n await updateCourse.mutateAsync({ id: courseId, input: { coverFileId: file.fileId } });\n } catch (e) {\n setCoverError(e instanceof Error ? e.message : 'Failed to update the cover.');\n }\n };\n\n return (\n <div className=\"grid gap-6 lg:grid-cols-3\">\n {/* Builder */}\n <div className=\"lg:col-span-2\">\n <Section\n title=\"Curriculum\"\n description=\"Organise the course into modules and lessons.\"\n action={\n <Button onClick={() => setShowAddModule(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add module\n </Button>\n }\n >\n {modulesQuery.isLoading ? (\n <LoadingView label=\"Loading curriculum…\" />\n ) : modulesQuery.isError ? (\n <ErrorView\n message=\"We couldn't load the curriculum.\"\n onRetry={() => modulesQuery.refetch()}\n />\n ) : modules.length === 0 ? (\n <EmptyState\n illustration=\"onboarding\"\n title=\"No modules yet\"\n description=\"Add your first module, then fill it with lessons.\"\n action={\n <Button onClick={() => setShowAddModule(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add module\n </Button>\n }\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n {modules.map((mod, idx) => (\n <ModuleCard\n key={mod.id}\n module={mod}\n index={idx}\n onAddLesson={() => setLessonModuleId(mod.id)}\n />\n ))}\n <button\n type=\"button\"\n onClick={() => setShowAddModule(true)}\n className=\"flex w-full items-center justify-center gap-2 rounded-xl border-2 border-dashed border-border-seam px-4 py-4 text-sm font-medium text-text-secondary transition-colors hover:border-[var(--color-action-primary-bg)] hover:bg-bg-sunken/40 hover:text-text-primary\"\n >\n <Plus className=\"h-4 w-4\" /> Add module\n </button>\n </div>\n )}\n </Section>\n </div>\n\n {/* Right rail: cover + details */}\n <div className=\"flex flex-col gap-6\">\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"text-base font-semibold text-text-primary\">Course cover</h3>\n <p className=\"mt-0.5 text-sm text-text-secondary\">\n Upload an image to represent your course.\n </p>\n <div className=\"mt-3 aspect-video w-full overflow-hidden rounded-xl border border-border-seam bg-bg-sunken\">\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 items-center justify-center gap-2 text-sm text-text-tertiary\">\n <ImageIcon className=\"h-5 w-5\" /> No cover yet\n </div>\n )}\n </div>\n <div className=\"mt-3\">\n <EvidenceUpload\n id=\"course-cover\"\n label={course.cover?.downloadUrl ? 'Replace cover' : 'Upload cover'}\n value={coverFile}\n onChange={handleCoverChange}\n accept=\"image/*\"\n hint=\"PNG or JPG · 16:9 ratio recommended.\"\n />\n {coverError && <p className=\"mt-1 text-xs text-status-error-text\">{coverError}</p>}\n </div>\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"text-base font-semibold text-text-primary\">Course details</h3>\n <dl className=\"mt-2 flex flex-col divide-y divide-border-seam\">\n <DetailRow icon={<Link2 className=\"h-4 w-4\" />} label=\"Slug\" value={course.slug} />\n <DetailRow\n icon={<Globe className=\"h-4 w-4\" />}\n label=\"Language\"\n value={course.language ? course.language.toUpperCase() : '—'}\n />\n <DetailRow\n icon={<BarChart3 className=\"h-4 w-4\" />}\n label=\"Level\"\n value={formatEnumLabel(course.level)}\n />\n <DetailRow\n icon={<Eye className=\"h-4 w-4\" />}\n label=\"Visibility\"\n value={course.isPublic ? 'Public' : 'Private'}\n />\n <DetailRow\n icon={<Calendar className=\"h-4 w-4\" />}\n label=\"Created\"\n value={formatDate(course.createdAt)}\n />\n </dl>\n </GlassCard>\n </div>\n\n {showAddModule && (\n <AddModuleModal\n courseId={courseId}\n position={modules.length}\n onClose={() => setShowAddModule(false)}\n />\n )}\n {lessonModuleId && (\n <AddLessonModal\n courseId={courseId}\n moduleId={lessonModuleId}\n position={lessonModule?.lessons?.length ?? 0}\n onClose={() => setLessonModuleId(null)}\n />\n )}\n </div>\n );\n};\n\n// ============================================================================\n// Assessments tab\n// ============================================================================\n\nconst AssessmentsTab: FC<{ courseId: string }> = ({ courseId }) => {\n const { navigateTo } = useBuildmyiq();\n const assessmentsQuery = useBuiAssessments(courseId);\n const assessments = assessmentsQuery.data ?? [];\n\n return (\n <Section\n title=\"Assessments\"\n description=\"Quizzes, exams and assignments attached to this course.\"\n >\n {assessmentsQuery.isLoading ? (\n <LoadingView label=\"Loading assessments…\" />\n ) : assessmentsQuery.isError ? (\n <ErrorView\n message=\"We couldn't load the assessments.\"\n onRetry={() => assessmentsQuery.refetch()}\n />\n ) : assessments.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No assessments yet\"\n description=\"Create an assessment in the Assessments workspace to test learners on this course.\"\n action={\n <Button variant=\"outline\" onClick={() => navigateTo('assessments')}>\n Open assessments\n </Button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {assessments.map((a) => (\n <GlassCard\n key={a.id}\n treatment=\"plain\"\n className=\"flex cursor-pointer flex-col gap-2 p-4 transition-shadow hover:shadow-lg\"\n onClick={() => navigateTo(`assessments/${a.id}`)}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <h3 className=\"min-w-0 font-semibold text-text-primary\">{a.title}</h3>\n <StatusBadge status={a.status} />\n </div>\n {a.description && (\n <p className=\"line-clamp-2 text-sm text-text-secondary\">{a.description}</p>\n )}\n <div className=\"mt-auto flex flex-wrap items-center gap-3 pt-1 text-xs text-text-secondary\">\n <span className=\"rounded-full bg-bg-sunken px-2 py-0.5\">\n {formatEnumLabel(a.type)}\n </span>\n <span>Pass ≥ {a.passingScore ?? 0}%</span>\n {a.timeLimitMin > 0 && (\n <span className=\"inline-flex items-center gap-1\">\n <Clock className=\"h-3.5 w-3.5\" /> {formatDuration(a.timeLimitMin)}\n </span>\n )}\n </div>\n </GlassCard>\n ))}\n </div>\n )}\n </Section>\n );\n};\n\n// ============================================================================\n// Page\n// ============================================================================\n\nexport const CourseBuilderPage: FC = () => {\n const { courseId } = useParams<{ courseId: string }>();\n const { navigateTo } = useBuildmyiq();\n const [tab, setTab] = useState<BuilderTab>('details');\n const courseQuery = useBuiCourse(courseId);\n const course = courseQuery.data ?? null;\n\n const publishCourse = useBuiPublishCourse();\n const archiveCourse = useBuiArchiveCourse();\n const [actionError, setActionError] = useState<string | null>(null);\n const [confirmArchive, setConfirmArchive] = useState(false);\n\n const handlePublish = async () => {\n if (!course) return;\n setActionError(null);\n try {\n await publishCourse.mutateAsync(course.id);\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'Failed to publish the course.');\n }\n };\n\n const handleArchive = async () => {\n if (!course) return;\n setActionError(null);\n try {\n await archiveCourse.mutateAsync(course.id);\n setConfirmArchive(false);\n } catch (e) {\n setActionError(e instanceof Error ? e.message : 'Failed to archive the course.');\n }\n };\n\n if (courseQuery.isLoading) {\n return (\n <PageLayout title=\"Course builder\">\n <LoadingView label=\"Loading course…\" />\n </PageLayout>\n );\n }\n\n if (courseQuery.isError) {\n return (\n <PageLayout title=\"Course builder\">\n <ErrorView message=\"We couldn't load this course.\" onRetry={() => courseQuery.refetch()} />\n </PageLayout>\n );\n }\n\n if (!courseId || !course) {\n return (\n <PageLayout title=\"Course builder\">\n <EmptyState\n illustration=\"empty-search\"\n title=\"Course not found\"\n description=\"This course may have been archived or removed.\"\n action={<Button onClick={() => navigateTo('authoring')}>Back to authoring</Button>}\n />\n </PageLayout>\n );\n }\n\n const canPublish = course.status === 'DRAFT' || course.status === 'IN_REVIEW';\n const canArchive = course.status !== 'ARCHIVED';\n\n const tabs: TabItem<BuilderTab>[] = [\n { value: 'details', label: 'Details', icon: <FileText className=\"h-4 w-4\" /> },\n {\n value: 'curriculum',\n label: 'Curriculum',\n icon: <BookOpen className=\"h-4 w-4\" />,\n count: course.modules?.length ?? 0,\n },\n { value: 'assessments', label: 'Assessments', icon: <ListChecks className=\"h-4 w-4\" /> },\n { value: 'discussion', label: 'Discussion', icon: <MessageSquare className=\"h-4 w-4\" /> },\n ];\n\n return (\n <PageLayout\n title={\n <span className=\"flex flex-wrap items-center gap-3\">\n <span className=\"min-w-0\">{course.title}</span>\n <StatusBadge status={course.status} />\n <StatusBadge status={course.level} />\n </span>\n }\n description={course.summary ?? undefined}\n actions={\n <>\n <Button variant=\"ghost\" onClick={() => navigateTo('authoring')}>\n <ArrowLeft className=\"mr-1.5 h-4 w-4\" /> Back\n </Button>\n {canArchive && (\n <Button variant=\"outline\" onClick={() => setConfirmArchive(true)}>\n <Archive className=\"mr-1.5 h-4 w-4\" /> Archive\n </Button>\n )}\n {canPublish && (\n <Button onClick={handlePublish} disabled={publishCourse.isPending}>\n <Send className=\"mr-1.5 h-4 w-4\" />\n {publishCourse.isPending ? 'Publishing…' : 'Publish'}\n </Button>\n )}\n </>\n }\n >\n <div className=\"flex flex-col gap-5\">\n {actionError && (\n <p className=\"rounded-lg border border-status-error-bg-subtle bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {actionError}\n </p>\n )}\n\n <Tabs items={tabs} value={tab} onChange={setTab} />\n\n {tab === 'details' && <DetailsTab key={course.id} course={course} />}\n {tab === 'curriculum' && <CurriculumTab course={course} />}\n {tab === 'assessments' && <AssessmentsTab courseId={courseId} />}\n {tab === 'discussion' && (\n <BuiDiscussion entityType=\"buildmyiq-course\" entityId={courseId} />\n )}\n </div>\n\n {confirmArchive && (\n <Modal\n open\n onClose={() => setConfirmArchive(false)}\n title=\"Archive this course?\"\n description=\"Archived courses are hidden from the catalog and cannot be enrolled in.\"\n footer={\n <>\n <Button\n variant=\"ghost\"\n onClick={() => setConfirmArchive(false)}\n disabled={archiveCourse.isPending}\n >\n Cancel\n </Button>\n <Button\n variant=\"destructive\"\n onClick={handleArchive}\n disabled={archiveCourse.isPending}\n >\n {archiveCourse.isPending ? 'Archiving…' : 'Archive course'}\n </Button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n You can still find it in Authoring under the Archived filter.\n </p>\n </Modal>\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useBuildmyiq as e } from "../../context/BuildmyiqContext.js";
|
|
2
|
+
import "../../context/index.js";
|
|
2
3
|
import { PageLayout as t } from "../../components/PageLayout.js";
|
|
3
4
|
import { formatEnumLabel as n } from "../../utils/formatters.js";
|
|
4
5
|
import { EmptyState as r } from "../../components/EmptyState.js";
|
|
5
6
|
import { ErrorView as i, LoadingView as a } from "../../components/StateViews.js";
|
|
6
7
|
import { FilterTabs as o } from "../../components/FilterTabs.js";
|
|
7
8
|
import { Pagination as s } from "../../components/Pagination.js";
|
|
8
|
-
import "../../context/index.js";
|
|
9
9
|
import { useBuiCourses as c } from "../../hooks/useBuildmyiqApi.js";
|
|
10
10
|
import { useCursorPager as l } from "../../hooks/useCursorPager.js";
|
|
11
11
|
import "../../hooks/index.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useBuildmyiq as e } from "../../context/BuildmyiqContext.js";
|
|
2
|
+
import "../../context/index.js";
|
|
2
3
|
import { cn as t } from "../../utils/cn.js";
|
|
3
4
|
import { PageLayout as n } from "../../components/PageLayout.js";
|
|
4
5
|
import { formatDuration as r, formatEnumLabel as i, formatHours as a } from "../../utils/formatters.js";
|
|
@@ -6,7 +7,6 @@ import { StatusBadge as o } from "../../components/StatusBadge.js";
|
|
|
6
7
|
import { EmptyState as s } from "../../components/EmptyState.js";
|
|
7
8
|
import { ErrorView as c, LoadingView as l, Section as u } from "../../components/StateViews.js";
|
|
8
9
|
import { MiniBar as d } from "../../components/MiniBar.js";
|
|
9
|
-
import "../../context/index.js";
|
|
10
10
|
import { useBuiAssessments as f, useBuiCourse as ee, useBuiEnroll as p, useBuiEnrollments as m, useBuiModules as h } from "../../hooks/useBuildmyiqApi.js";
|
|
11
11
|
import "../../hooks/index.js";
|
|
12
12
|
import { BuiDiscussion as te } from "../../components/BuiDiscussion.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useBuildmyiq as e } from "../../context/BuildmyiqContext.js";
|
|
2
|
+
import "../../context/index.js";
|
|
2
3
|
import { PageLayout as t } from "../../components/PageLayout.js";
|
|
3
4
|
import { MetricsCard as n } from "../../components/MetricsCard.js";
|
|
4
5
|
import { formatDate as r, formatPercent as ee } from "../../utils/formatters.js";
|
|
@@ -9,7 +10,6 @@ import { DataTable as ie } from "../../components/DataTable.js";
|
|
|
9
10
|
import { FilterTabs as ae } from "../../components/FilterTabs.js";
|
|
10
11
|
import { Pagination as oe } from "../../components/Pagination.js";
|
|
11
12
|
import { Avatar as se } from "../../components/Avatar.js";
|
|
12
|
-
import "../../context/index.js";
|
|
13
13
|
import { useBuiCertificates as ce, useBuiCourses as le, useBuiIssueCertificate as ue, useBuiRevokeCertificate as de } from "../../hooks/useBuildmyiqApi.js";
|
|
14
14
|
import "../../hooks/index.js";
|
|
15
15
|
import { FileLink as fe } from "../../components/FileLink.js";
|
|
@@ -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, Search, ShieldCheck, CalendarClock } 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 MetricsCard,\n StatusBadge,\n DataTable,\n type Column,\n FilterTabs,\n type FilterOption,\n LoadingView,\n ErrorView,\n EmptyState,\n FileLink,\n Avatar,\n Pagination,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiCertificates,\n useBuiCourses,\n useBuiIssueCertificate,\n useBuiRevokeCertificate,\n} from '../../hooks';\nimport { formatDate, formatPercent } from '../../utils/formatters';\nimport type { BuiCertificate } from '../../types';\n\ntype ScopeFilter = 'MINE' | 'ALL';\ntype CourseFilter = string | 'ALL';\n\nconst PAGE_SIZE = 8;\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 [search, setSearch] = useState('');\n const [page, setPage] = useState(0);\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 // Client-side search over the loaded scope (serial / course / learner id).\n const filtered = useMemo(() => {\n const q = search.trim().toLowerCase();\n if (!q) return rows;\n return rows.filter((c) => {\n const course = (courseTitleById.get(c.courseId) ?? '').toLowerCase();\n const learner = c.learnerId === selfId ? 'you me mine' : c.learnerId.toLowerCase();\n return (\n c.serial.toLowerCase().includes(q) || course.includes(q) || learner.includes(q)\n );\n });\n }, [rows, search, courseTitleById, selfId]);\n\n // Honest KPI counts across the loaded scope (before search narrowing).\n const summary = useMemo(() => {\n let issued = 0;\n let expired = 0;\n let revoked = 0;\n for (const c of rows) {\n if (c.status === 'ISSUED') issued += 1;\n else if (c.status === 'EXPIRED') expired += 1;\n else if (c.status === 'REVOKED') revoked += 1;\n }\n return { total: rows.length, issued, expired, revoked };\n }, [rows]);\n\n const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));\n\n // Derive an in-range page (avoids a setState-in-effect clamp loop): if filters/\n // search shrink the result set below the current page, fall back to the last page.\n const safePage = Math.min(page, pageCount - 1);\n\n const pageRows = useMemo(\n () => filtered.slice(safePage * PAGE_SIZE, safePage * PAGE_SIZE + PAGE_SIZE),\n [filtered, safePage]\n );\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 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: 'learner',\n header: 'Learner',\n sortValue: (c) => (c.learnerId === selfId ? '' : c.learnerId),\n render: (c) => {\n const isSelf = c.learnerId === selfId;\n const name = isSelf\n ? currentUser?.name ?? currentUser?.username ?? currentUser?.email ?? null\n : null;\n return (\n <div className=\"flex items-center gap-2.5\">\n <Avatar size=\"sm\" seed={c.learnerId} name={name} />\n {isSelf ? (\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 </div>\n );\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=\"font-medium tabular-nums text-text-primary\">\n {c.finalScore != null ? formatPercent(c.finalScore) : '—'}\n </span>\n ),\n },\n {\n key: 'file',\n header: 'PDF',\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' || search.trim().length > 0;\n const showSummary = !certsQuery.isLoading && !certsQuery.isError && summary.total > 0;\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 {showSummary && (\n <div className=\"mb-5 grid grid-cols-2 gap-4 sm:grid-cols-4\">\n <MetricsCard\n icon={<Award className=\"h-5 w-5\" />}\n value={summary.total}\n label=\"Certificates\"\n featured\n />\n <MetricsCard\n icon={<ShieldCheck className=\"h-5 w-5\" />}\n value={summary.issued}\n label=\"Issued\"\n accent=\"success\"\n />\n <MetricsCard\n icon={<CalendarClock className=\"h-5 w-5\" />}\n value={summary.expired}\n label=\"Expired\"\n />\n <MetricsCard\n icon={<Ban className=\"h-5 w-5\" />}\n value={summary.revoked}\n label=\"Revoked\"\n accent=\"error\"\n />\n </div>\n )}\n\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <FilterTabs\n options={scopeOptions}\n value={scope}\n onChange={(v) => {\n setScope(v);\n setPage(0);\n }}\n />\n <Select\n value={courseFilter}\n onValueChange={(v) => {\n setCourseFilter(v as CourseFilter);\n setPage(0);\n }}\n >\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 className=\"relative w-full sm:ml-auto sm:w-64\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary\" />\n <Input\n value={search}\n onChange={(e) => {\n setSearch(e.target.value);\n setPage(0);\n }}\n placeholder=\"Search certificates…\"\n aria-label=\"Search certificates\"\n className=\"pl-9\"\n />\n </div>\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 ) : filtered.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 ? search.trim().length > 0\n ? 'No credentials match your search. Try a different serial, course, or learner.'\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 <>\n <DataTable\n columns={columns}\n rows={pageRows}\n rowKey={(c) => c.id}\n defaultSort={{ key: 'issuedAt', dir: 'desc' }}\n />\n <Pagination\n page={safePage}\n pageSize={PAGE_SIZE}\n itemCount={pageRows.length}\n hasMore={(safePage + 1) * PAGE_SIZE < filtered.length}\n onPageChange={setPage}\n />\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, Search, ShieldCheck, CalendarClock } 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 MetricsCard,\n StatusBadge,\n DataTable,\n type Column,\n FilterTabs,\n type FilterOption,\n LoadingView,\n ErrorView,\n EmptyState,\n FileLink,\n Avatar,\n Pagination,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiCertificates,\n useBuiCourses,\n useBuiIssueCertificate,\n useBuiRevokeCertificate,\n} from '../../hooks';\nimport { formatDate, formatPercent } from '../../utils/formatters';\nimport type { BuiCertificate } from '../../types';\n\ntype ScopeFilter = 'MINE' | 'ALL';\ntype CourseFilter = string | 'ALL';\n\nconst PAGE_SIZE = 8;\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 [search, setSearch] = useState('');\n const [page, setPage] = useState(0);\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 // Client-side search over the loaded scope (serial / course / learner id).\n const filtered = useMemo(() => {\n const q = search.trim().toLowerCase();\n if (!q) return rows;\n return rows.filter((c) => {\n const course = (courseTitleById.get(c.courseId) ?? '').toLowerCase();\n const learner = c.learnerId === selfId ? 'you me mine' : c.learnerId.toLowerCase();\n return c.serial.toLowerCase().includes(q) || course.includes(q) || learner.includes(q);\n });\n }, [rows, search, courseTitleById, selfId]);\n\n // Honest KPI counts across the loaded scope (before search narrowing).\n const summary = useMemo(() => {\n let issued = 0;\n let expired = 0;\n let revoked = 0;\n for (const c of rows) {\n if (c.status === 'ISSUED') issued += 1;\n else if (c.status === 'EXPIRED') expired += 1;\n else if (c.status === 'REVOKED') revoked += 1;\n }\n return { total: rows.length, issued, expired, revoked };\n }, [rows]);\n\n const pageCount = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));\n\n // Derive an in-range page (avoids a setState-in-effect clamp loop): if filters/\n // search shrink the result set below the current page, fall back to the last page.\n const safePage = Math.min(page, pageCount - 1);\n\n const pageRows = useMemo(\n () => filtered.slice(safePage * PAGE_SIZE, safePage * PAGE_SIZE + PAGE_SIZE),\n [filtered, safePage]\n );\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 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: 'learner',\n header: 'Learner',\n sortValue: (c) => (c.learnerId === selfId ? '' : c.learnerId),\n render: (c) => {\n const isSelf = c.learnerId === selfId;\n const name = isSelf\n ? (currentUser?.name ?? currentUser?.username ?? currentUser?.email ?? null)\n : null;\n return (\n <div className=\"flex items-center gap-2.5\">\n <Avatar size=\"sm\" seed={c.learnerId} name={name} />\n {isSelf ? (\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 </div>\n );\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=\"font-medium tabular-nums text-text-primary\">\n {c.finalScore != null ? formatPercent(c.finalScore) : '—'}\n </span>\n ),\n },\n {\n key: 'file',\n header: 'PDF',\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' || search.trim().length > 0;\n const showSummary = !certsQuery.isLoading && !certsQuery.isError && summary.total > 0;\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 {showSummary && (\n <div className=\"mb-5 grid grid-cols-2 gap-4 sm:grid-cols-4\">\n <MetricsCard\n icon={<Award className=\"h-5 w-5\" />}\n value={summary.total}\n label=\"Certificates\"\n featured\n />\n <MetricsCard\n icon={<ShieldCheck className=\"h-5 w-5\" />}\n value={summary.issued}\n label=\"Issued\"\n accent=\"success\"\n />\n <MetricsCard\n icon={<CalendarClock className=\"h-5 w-5\" />}\n value={summary.expired}\n label=\"Expired\"\n />\n <MetricsCard\n icon={<Ban className=\"h-5 w-5\" />}\n value={summary.revoked}\n label=\"Revoked\"\n accent=\"error\"\n />\n </div>\n )}\n\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <FilterTabs\n options={scopeOptions}\n value={scope}\n onChange={(v) => {\n setScope(v);\n setPage(0);\n }}\n />\n <Select\n value={courseFilter}\n onValueChange={(v) => {\n setCourseFilter(v as CourseFilter);\n setPage(0);\n }}\n >\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 className=\"relative w-full sm:ml-auto sm:w-64\">\n <Search className=\"pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-tertiary\" />\n <Input\n value={search}\n onChange={(e) => {\n setSearch(e.target.value);\n setPage(0);\n }}\n placeholder=\"Search certificates…\"\n aria-label=\"Search certificates\"\n className=\"pl-9\"\n />\n </div>\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 ) : filtered.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 ? search.trim().length > 0\n ? 'No credentials match your search. Try a different serial, course, or learner.'\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 <>\n <DataTable\n columns={columns}\n rows={pageRows}\n rowKey={(c) => c.id}\n defaultSort={{ key: 'issuedAt', dir: 'desc' }}\n />\n <Pagination\n page={safePage}\n pageSize={PAGE_SIZE}\n itemCount={pageRows.length}\n hasMore={(safePage + 1) * PAGE_SIZE < filtered.length}\n onPageChange={setPage}\n />\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,4 +1,5 @@
|
|
|
1
1
|
import { useBuildmyiq as e } from "../../context/BuildmyiqContext.js";
|
|
2
|
+
import "../../context/index.js";
|
|
2
3
|
import { PageLayout as t } from "../../components/PageLayout.js";
|
|
3
4
|
import { MetricsCard as n } from "../../components/MetricsCard.js";
|
|
4
5
|
import { formatDate as r, formatEnumLabel as i, formatNumber as a } from "../../utils/formatters.js";
|
|
@@ -7,7 +8,6 @@ import { EmptyState as te } from "../../components/EmptyState.js";
|
|
|
7
8
|
import { ErrorView as ne, LoadingView as re } from "../../components/StateViews.js";
|
|
8
9
|
import { DataTable as ie } from "../../components/DataTable.js";
|
|
9
10
|
import { FilterTabs as ae } from "../../components/FilterTabs.js";
|
|
10
|
-
import "../../context/index.js";
|
|
11
11
|
import { useBuiCohorts as o, useBuiCourses as s, useBuiCreateCohort as c } from "../../hooks/useBuildmyiqApi.js";
|
|
12
12
|
import "../../hooks/index.js";
|
|
13
13
|
import "../../components/index.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"InsightsPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/insights/InsightsPage.tsx"],"sourcesContent":["/**\n * InsightsPage — educator/admin learning analytics, DERIVED entirely from live\n * `bui*` data (no analytics tables, no fabricated numbers).\n * @module buildmyiq/pages/insights\n *\n * Everything here is computed client-side from real queries:\n * - courses (total / published)\n * - enrollments (status split, completion rate, average progress, trend by month)\n * - submissions (graded / passed / pass rate)\n * - certificates issued\n * - top courses by enrolment\n * The circular meters (completion + pass rate), the tinted KPI cards, the\n * status-distribution bars, the enrolment sparkline and the ranked top-course\n * bars all read as one tokenized system. When the workspace has no data yet the\n * page shows honest zero/empty states.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useMemo } from 'react';\nimport {\n BookOpen,\n Award,\n TrendingUp,\n Users,\n GraduationCap,\n CheckCircle2,\n User,\n Clock,\n XCircle,\n AlarmClock,\n type LucideIcon,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n MetricsCard,\n LoadingView,\n ErrorView,\n Section,\n EmptyState,\n MiniBar,\n ProgressRing,\n Sparkline,\n} from '../../components';\nimport {\n useBuiCourses,\n useBuiEnrollments,\n useBuiSubmissions,\n useBuiCertificates,\n} from '../../hooks';\nimport { formatEnumLabel, formatPercent } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { BuiCourse, BuiEnrollment, BuiSubmission } from '../../types';\n\nconst ENROLLMENT_STATUS_ORDER = ['ENROLLED', 'IN_PROGRESS', 'COMPLETED', 'DROPPED', 'EXPIRED'] as const;\ntype EnrollmentStatusKey = (typeof ENROLLMENT_STATUS_ORDER)[number];\n\n/** Per-status icon + bar/icon tone so the distribution reads as one system. */\nconst STATUS_META: Record<EnrollmentStatusKey, { icon: LucideIcon; bar: string; icon_tint: string }> = {\n ENROLLED: {\n icon: User,\n bar: 'bg-[var(--color-accent-teal)]',\n icon_tint: 'text-[var(--color-accent-teal)]',\n },\n IN_PROGRESS: {\n icon: Clock,\n bar: 'bg-[var(--color-accent-primary)]',\n icon_tint: 'text-[var(--color-accent-primary)]',\n },\n COMPLETED: {\n icon: CheckCircle2,\n bar: 'bg-[var(--color-status-success-text)]',\n icon_tint: 'text-[var(--color-status-success-text)]',\n },\n DROPPED: {\n icon: XCircle,\n bar: 'bg-[var(--color-status-error-text)]',\n icon_tint: 'text-[var(--color-status-error-text)]',\n },\n EXPIRED: {\n icon: AlarmClock,\n bar: 'bg-[var(--color-text-tertiary)]',\n icon_tint: 'text-[var(--color-text-tertiary)]',\n },\n};\n\ntype Tint = 'info' | 'success' | 'warning' | 'accent';\nconst TINT_CLASS: Record<Tint, string> = {\n info: 'bg-[var(--color-status-info-bg)] text-[var(--color-status-info-text)]',\n success: 'bg-[var(--color-status-success-bg)] text-[var(--color-status-success-text)]',\n warning: 'bg-[var(--color-status-warning-bg)] text-[var(--color-status-warning-text)]',\n accent: 'bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]',\n};\n\n/** Soft tinted circle behind a KPI icon (matches the concept's tinted badges). */\nconst TintedIcon: FC<{ tint: Tint; children: ReactNode }> = ({ tint, children }) => (\n <span className={cn('flex h-11 w-11 items-center justify-center rounded-full', TINT_CLASS[tint])}>\n {children}\n </span>\n);\n\ninterface DistRow {\n status: EnrollmentStatusKey;\n label: string;\n count: number;\n pct: number;\n}\n\nconst DistBars: FC<{ rows: DistRow[] }> = ({ rows }) => (\n <div className=\"flex flex-col gap-3\">\n {rows.map((r) => {\n const meta = STATUS_META[r.status];\n const Icon = meta.icon;\n return (\n <div key={r.status} className=\"flex items-center gap-3\">\n <span className=\"flex w-24 shrink-0 items-center gap-2 text-sm text-text-secondary sm:w-28\">\n <Icon className={cn('h-4 w-4 shrink-0', meta.icon_tint)} />\n <span className=\"truncate\">{r.label}</span>\n </span>\n <MiniBar value={r.pct} barClassName={meta.bar} className=\"flex-1\" height={8} />\n <span className=\"w-9 shrink-0 text-right text-sm tabular-nums text-text-primary\">{r.count}</span>\n </div>\n );\n })}\n </div>\n);\n\nexport const InsightsPage: FC = () => {\n const coursesQuery = useBuiCourses({ limit: 100 });\n const enrollmentsQuery = useBuiEnrollments({ limit: 100 });\n const submissionsQuery = useBuiSubmissions({ limit: 100 });\n const certsQuery = useBuiCertificates();\n\n const courses: BuiCourse[] = useMemo(() => coursesQuery.data?.items ?? [], [coursesQuery.data]);\n const enrollments: BuiEnrollment[] = useMemo(\n () => enrollmentsQuery.data?.items ?? [],\n [enrollmentsQuery.data]\n );\n const submissions: BuiSubmission[] = useMemo(\n () => submissionsQuery.data?.items ?? [],\n [submissionsQuery.data]\n );\n const certificates = certsQuery.data ?? [];\n\n const stats = useMemo(() => {\n const published = courses.filter((c) => c.status === 'PUBLISHED').length;\n const completed = enrollments.filter((e) => e.status === 'COMPLETED').length;\n const inProgress = enrollments.filter((e) => e.status === 'IN_PROGRESS').length;\n const completionRate = enrollments.length > 0 ? (completed / enrollments.length) * 100 : 0;\n const avgProgress =\n enrollments.length > 0\n ? enrollments.reduce((s, e) => s + (e.progressPct ?? 0), 0) / enrollments.length\n : 0;\n const graded = submissions.filter((s) => s.status === 'GRADED');\n const passed = graded.filter((s) => s.passed === true).length;\n const passRate = graded.length > 0 ? (passed / graded.length) * 100 : 0;\n const activeLearners = new Set(enrollments.map((e) => e.learnerId)).size;\n return {\n published,\n totalCourses: courses.length,\n completed,\n inProgress,\n completionRate,\n avgProgress,\n gradedCount: graded.length,\n passed,\n passRate,\n activeLearners,\n };\n }, [courses, enrollments, submissions]);\n\n const enrollmentDist: DistRow[] = useMemo(() => {\n const total = enrollments.length;\n return ENROLLMENT_STATUS_ORDER.map((status) => {\n const count = enrollments.filter((e) => e.status === status).length;\n return {\n status,\n label: formatEnumLabel(status),\n count,\n pct: total > 0 ? (count / total) * 100 : 0,\n };\n }).filter((r) => r.count > 0);\n }, [enrollments]);\n\n /** New enrolments bucketed by calendar month (real `enrolledAt` timestamps). */\n const enrolmentTrend = useMemo(() => {\n if (enrollments.length === 0) return [];\n const byMonth = new Map<string, number>();\n for (const e of enrollments) {\n const d = new Date(e.enrolledAt);\n if (Number.isNaN(d.getTime())) continue;\n const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;\n byMonth.set(key, (byMonth.get(key) ?? 0) + 1);\n }\n return [...byMonth.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, v]) => v);\n }, [enrollments]);\n\n const topCourses = useMemo(() => {\n const titleById = new Map(courses.map((c) => [c.id, c.title]));\n const counts = new Map<string, number>();\n for (const e of enrollments) counts.set(e.courseId, (counts.get(e.courseId) ?? 0) + 1);\n const max = Math.max(1, ...counts.values());\n return [...counts.entries()]\n .map(([courseId, count]) => ({\n courseId,\n title: titleById.get(courseId) ?? 'Course',\n count,\n pct: (count / max) * 100,\n }))\n .sort((a, b) => b.count - a.count)\n .slice(0, 6);\n }, [courses, enrollments]);\n\n const isLoading =\n coursesQuery.isLoading || enrollmentsQuery.isLoading || submissionsQuery.isLoading;\n const isError =\n coursesQuery.isError ||\n enrollmentsQuery.isError ||\n submissionsQuery.isError ||\n certsQuery.isError;\n\n const hasAnyData =\n courses.length > 0 || enrollments.length > 0 || submissions.length > 0 || certificates.length > 0;\n\n const retry = () => {\n void coursesQuery.refetch();\n void enrollmentsQuery.refetch();\n void submissionsQuery.refetch();\n void certsQuery.refetch();\n };\n\n return (\n <PageLayout\n title=\"Insights\"\n description=\"Learning analytics across your workspace — completion, engagement and outcomes.\"\n titleGradient\n aurora\n >\n {isLoading ? (\n <LoadingView label=\"Crunching your learning data…\" />\n ) : isError ? (\n <ErrorView\n title=\"Couldn't load insights\"\n message=\"We hit a snag pulling your learning data. Try again in a moment.\"\n onRetry={retry}\n />\n ) : !hasAnyData ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No data to analyse yet\"\n description=\"Publish courses and enrol learners — insights appear here as activity accrues.\"\n />\n ) : (\n <div className=\"flex flex-col gap-8\">\n {/* KPI row — tinted-icon cards, all values derived from live data */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <MetricsCard\n icon={\n <TintedIcon tint=\"info\">\n <BookOpen className=\"h-5 w-5\" />\n </TintedIcon>\n }\n value={stats.published}\n label=\"Published courses\"\n footer={\n <span className=\"text-xs text-text-secondary\">of {stats.totalCourses} total</span>\n }\n />\n <MetricsCard\n icon={\n <TintedIcon tint=\"success\">\n <Users className=\"h-5 w-5\" />\n </TintedIcon>\n }\n value={stats.activeLearners}\n label=\"Active learners\"\n footer={\n <span className=\"text-xs text-text-secondary\">\n {enrollments.length} enrolment{enrollments.length === 1 ? '' : 's'}\n </span>\n }\n />\n <MetricsCard\n icon={\n <TintedIcon tint=\"accent\">\n <TrendingUp className=\"h-5 w-5\" />\n </TintedIcon>\n }\n value={formatPercent(stats.completionRate)}\n label=\"Completion rate\"\n footer={\n <MiniBar value={stats.completionRate} barClassName=\"bg-[var(--color-accent-teal)]\" />\n }\n />\n <MetricsCard\n icon={\n <TintedIcon tint=\"warning\">\n <Award className=\"h-5 w-5\" />\n </TintedIcon>\n }\n value={certificates.length}\n label=\"Certificates issued\"\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n {/* Enrolment status distribution */}\n <Section title=\"Enrolment status\" description={`${enrollments.length} enrolments`}>\n {enrollmentDist.length === 0 ? (\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <p className=\"text-sm text-text-secondary\">No enrolments yet.</p>\n </GlassCard>\n ) : (\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <DistBars rows={enrollmentDist} />\n <div className=\"mt-4 flex flex-wrap items-center justify-between gap-3 border-t border-border-seam pt-3\">\n <span className=\"inline-flex items-center gap-1.5 text-sm text-text-secondary\">\n <GraduationCap className=\"h-4 w-4\" /> Avg progress\n <span className=\"font-medium text-text-primary\">\n {formatPercent(stats.avgProgress)}\n </span>\n </span>\n {enrolmentTrend.length >= 2 && (\n <span className=\"inline-flex items-center gap-2\">\n <Sparkline data={enrolmentTrend} color=\"var(--color-accent-teal)\" />\n <span className=\"text-xs text-text-secondary\">new / month</span>\n </span>\n )}\n </div>\n </GlassCard>\n )}\n </Section>\n\n {/* Learning outcomes — paired circular meters */}\n <Section title=\"Learning outcomes\" description={`${submissions.length} submissions`}>\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-5 p-5\">\n <div className=\"flex items-center justify-around gap-4\">\n <div className=\"flex flex-col items-center gap-2 text-center\">\n <ProgressRing\n value={stats.completionRate}\n size={96}\n stroke={8}\n color=\"var(--color-accent-teal)\"\n label={enrollments.length === 0 ? '—' : undefined}\n />\n <span className=\"text-sm text-text-secondary\">Completion rate</span>\n </div>\n <div className=\"flex flex-col items-center gap-2 text-center\">\n <ProgressRing\n value={stats.passRate}\n size={96}\n stroke={8}\n color=\"var(--color-status-success-text)\"\n label={stats.gradedCount === 0 ? '—' : undefined}\n />\n <span className=\"text-sm text-text-secondary\">Pass rate</span>\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-3 border-t border-border-seam pt-4\">\n <div>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">\n {stats.gradedCount}\n <span className=\"text-sm font-normal text-text-secondary\">\n /{submissions.length}\n </span>\n </p>\n <p className=\"text-xs text-text-secondary\">Graded</p>\n </div>\n <div>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">\n {stats.passed}\n <span className=\"text-sm font-normal text-text-secondary\">\n {' '}\n of {stats.gradedCount}\n </span>\n </p>\n <p className=\"text-xs text-text-secondary\">Passed</p>\n </div>\n </div>\n {stats.gradedCount === 0 && (\n <p className=\"text-xs text-text-secondary\">\n No submissions have been graded yet — pass rate appears once grading begins.\n </p>\n )}\n </GlassCard>\n </Section>\n </div>\n\n {/* Top courses by enrolment */}\n <Section title=\"Top courses by enrolment\">\n {topCourses.length === 0 ? (\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <p className=\"text-sm text-text-secondary\">No enrolments yet.</p>\n </GlassCard>\n ) : (\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <div className=\"flex flex-col gap-4\">\n {topCourses.map((c, i) => (\n <div key={c.courseId} className=\"flex items-center gap-3\">\n <span\n className={cn(\n 'flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold tabular-nums',\n i === 0\n ? 'bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]'\n : 'bg-bg-sunken text-text-secondary'\n )}\n >\n {i + 1}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"truncate text-sm text-text-primary\">{c.title}</span>\n <span className=\"shrink-0 text-sm tabular-nums text-text-secondary\">\n {c.count}\n </span>\n </div>\n <MiniBar\n value={c.pct}\n barClassName=\"bg-[var(--color-accent-teal)]\"\n className=\"mt-1.5\"\n height={8}\n />\n </div>\n </div>\n ))}\n </div>\n </GlassCard>\n )}\n </Section>\n </div>\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"InsightsPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/insights/InsightsPage.tsx"],"sourcesContent":["/**\n * InsightsPage — educator/admin learning analytics, DERIVED entirely from live\n * `bui*` data (no analytics tables, no fabricated numbers).\n * @module buildmyiq/pages/insights\n *\n * Everything here is computed client-side from real queries:\n * - courses (total / published)\n * - enrollments (status split, completion rate, average progress, trend by month)\n * - submissions (graded / passed / pass rate)\n * - certificates issued\n * - top courses by enrolment\n * The circular meters (completion + pass rate), the tinted KPI cards, the\n * status-distribution bars, the enrolment sparkline and the ranked top-course\n * bars all read as one tokenized system. When the workspace has no data yet the\n * page shows honest zero/empty states.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport { useMemo } from 'react';\nimport {\n BookOpen,\n Award,\n TrendingUp,\n Users,\n GraduationCap,\n CheckCircle2,\n User,\n Clock,\n XCircle,\n AlarmClock,\n type LucideIcon,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n MetricsCard,\n LoadingView,\n ErrorView,\n Section,\n EmptyState,\n MiniBar,\n ProgressRing,\n Sparkline,\n} from '../../components';\nimport {\n useBuiCourses,\n useBuiEnrollments,\n useBuiSubmissions,\n useBuiCertificates,\n} from '../../hooks';\nimport { formatEnumLabel, formatPercent } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { BuiCourse, BuiEnrollment, BuiSubmission } from '../../types';\n\nconst ENROLLMENT_STATUS_ORDER = [\n 'ENROLLED',\n 'IN_PROGRESS',\n 'COMPLETED',\n 'DROPPED',\n 'EXPIRED',\n] as const;\ntype EnrollmentStatusKey = (typeof ENROLLMENT_STATUS_ORDER)[number];\n\n/** Per-status icon + bar/icon tone so the distribution reads as one system. */\nconst STATUS_META: Record<\n EnrollmentStatusKey,\n { icon: LucideIcon; bar: string; icon_tint: string }\n> = {\n ENROLLED: {\n icon: User,\n bar: 'bg-[var(--color-accent-teal)]',\n icon_tint: 'text-[var(--color-accent-teal)]',\n },\n IN_PROGRESS: {\n icon: Clock,\n bar: 'bg-[var(--color-accent-primary)]',\n icon_tint: 'text-[var(--color-accent-primary)]',\n },\n COMPLETED: {\n icon: CheckCircle2,\n bar: 'bg-[var(--color-status-success-text)]',\n icon_tint: 'text-[var(--color-status-success-text)]',\n },\n DROPPED: {\n icon: XCircle,\n bar: 'bg-[var(--color-status-error-text)]',\n icon_tint: 'text-[var(--color-status-error-text)]',\n },\n EXPIRED: {\n icon: AlarmClock,\n bar: 'bg-[var(--color-text-tertiary)]',\n icon_tint: 'text-[var(--color-text-tertiary)]',\n },\n};\n\ntype Tint = 'info' | 'success' | 'warning' | 'accent';\nconst TINT_CLASS: Record<Tint, string> = {\n info: 'bg-[var(--color-status-info-bg)] text-[var(--color-status-info-text)]',\n success: 'bg-[var(--color-status-success-bg)] text-[var(--color-status-success-text)]',\n warning: 'bg-[var(--color-status-warning-bg)] text-[var(--color-status-warning-text)]',\n accent: 'bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]',\n};\n\n/** Soft tinted circle behind a KPI icon (matches the concept's tinted badges). */\nconst TintedIcon: FC<{ tint: Tint; children: ReactNode }> = ({ tint, children }) => (\n <span className={cn('flex h-11 w-11 items-center justify-center rounded-full', TINT_CLASS[tint])}>\n {children}\n </span>\n);\n\ninterface DistRow {\n status: EnrollmentStatusKey;\n label: string;\n count: number;\n pct: number;\n}\n\nconst DistBars: FC<{ rows: DistRow[] }> = ({ rows }) => (\n <div className=\"flex flex-col gap-3\">\n {rows.map((r) => {\n const meta = STATUS_META[r.status];\n const Icon = meta.icon;\n return (\n <div key={r.status} className=\"flex items-center gap-3\">\n <span className=\"flex w-24 shrink-0 items-center gap-2 text-sm text-text-secondary sm:w-28\">\n <Icon className={cn('h-4 w-4 shrink-0', meta.icon_tint)} />\n <span className=\"truncate\">{r.label}</span>\n </span>\n <MiniBar value={r.pct} barClassName={meta.bar} className=\"flex-1\" height={8} />\n <span className=\"w-9 shrink-0 text-right text-sm tabular-nums text-text-primary\">\n {r.count}\n </span>\n </div>\n );\n })}\n </div>\n);\n\nexport const InsightsPage: FC = () => {\n const coursesQuery = useBuiCourses({ limit: 100 });\n const enrollmentsQuery = useBuiEnrollments({ limit: 100 });\n const submissionsQuery = useBuiSubmissions({ limit: 100 });\n const certsQuery = useBuiCertificates();\n\n const courses: BuiCourse[] = useMemo(() => coursesQuery.data?.items ?? [], [coursesQuery.data]);\n const enrollments: BuiEnrollment[] = useMemo(\n () => enrollmentsQuery.data?.items ?? [],\n [enrollmentsQuery.data]\n );\n const submissions: BuiSubmission[] = useMemo(\n () => submissionsQuery.data?.items ?? [],\n [submissionsQuery.data]\n );\n const certificates = certsQuery.data ?? [];\n\n const stats = useMemo(() => {\n const published = courses.filter((c) => c.status === 'PUBLISHED').length;\n const completed = enrollments.filter((e) => e.status === 'COMPLETED').length;\n const inProgress = enrollments.filter((e) => e.status === 'IN_PROGRESS').length;\n const completionRate = enrollments.length > 0 ? (completed / enrollments.length) * 100 : 0;\n const avgProgress =\n enrollments.length > 0\n ? enrollments.reduce((s, e) => s + (e.progressPct ?? 0), 0) / enrollments.length\n : 0;\n const graded = submissions.filter((s) => s.status === 'GRADED');\n const passed = graded.filter((s) => s.passed === true).length;\n const passRate = graded.length > 0 ? (passed / graded.length) * 100 : 0;\n const activeLearners = new Set(enrollments.map((e) => e.learnerId)).size;\n return {\n published,\n totalCourses: courses.length,\n completed,\n inProgress,\n completionRate,\n avgProgress,\n gradedCount: graded.length,\n passed,\n passRate,\n activeLearners,\n };\n }, [courses, enrollments, submissions]);\n\n const enrollmentDist: DistRow[] = useMemo(() => {\n const total = enrollments.length;\n return ENROLLMENT_STATUS_ORDER.map((status) => {\n const count = enrollments.filter((e) => e.status === status).length;\n return {\n status,\n label: formatEnumLabel(status),\n count,\n pct: total > 0 ? (count / total) * 100 : 0,\n };\n }).filter((r) => r.count > 0);\n }, [enrollments]);\n\n /** New enrolments bucketed by calendar month (real `enrolledAt` timestamps). */\n const enrolmentTrend = useMemo(() => {\n if (enrollments.length === 0) return [];\n const byMonth = new Map<string, number>();\n for (const e of enrollments) {\n const d = new Date(e.enrolledAt);\n if (Number.isNaN(d.getTime())) continue;\n const key = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`;\n byMonth.set(key, (byMonth.get(key) ?? 0) + 1);\n }\n return [...byMonth.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, v]) => v);\n }, [enrollments]);\n\n const topCourses = useMemo(() => {\n const titleById = new Map(courses.map((c) => [c.id, c.title]));\n const counts = new Map<string, number>();\n for (const e of enrollments) counts.set(e.courseId, (counts.get(e.courseId) ?? 0) + 1);\n const max = Math.max(1, ...counts.values());\n return [...counts.entries()]\n .map(([courseId, count]) => ({\n courseId,\n title: titleById.get(courseId) ?? 'Course',\n count,\n pct: (count / max) * 100,\n }))\n .sort((a, b) => b.count - a.count)\n .slice(0, 6);\n }, [courses, enrollments]);\n\n const isLoading =\n coursesQuery.isLoading || enrollmentsQuery.isLoading || submissionsQuery.isLoading;\n const isError =\n coursesQuery.isError ||\n enrollmentsQuery.isError ||\n submissionsQuery.isError ||\n certsQuery.isError;\n\n const hasAnyData =\n courses.length > 0 ||\n enrollments.length > 0 ||\n submissions.length > 0 ||\n certificates.length > 0;\n\n const retry = () => {\n void coursesQuery.refetch();\n void enrollmentsQuery.refetch();\n void submissionsQuery.refetch();\n void certsQuery.refetch();\n };\n\n return (\n <PageLayout\n title=\"Insights\"\n description=\"Learning analytics across your workspace — completion, engagement and outcomes.\"\n titleGradient\n aurora\n >\n {isLoading ? (\n <LoadingView label=\"Crunching your learning data…\" />\n ) : isError ? (\n <ErrorView\n title=\"Couldn't load insights\"\n message=\"We hit a snag pulling your learning data. Try again in a moment.\"\n onRetry={retry}\n />\n ) : !hasAnyData ? (\n <EmptyState\n illustration=\"empty-data\"\n title=\"No data to analyse yet\"\n description=\"Publish courses and enrol learners — insights appear here as activity accrues.\"\n />\n ) : (\n <div className=\"flex flex-col gap-8\">\n {/* KPI row — tinted-icon cards, all values derived from live data */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4\">\n <MetricsCard\n icon={\n <TintedIcon tint=\"info\">\n <BookOpen className=\"h-5 w-5\" />\n </TintedIcon>\n }\n value={stats.published}\n label=\"Published courses\"\n footer={\n <span className=\"text-xs text-text-secondary\">of {stats.totalCourses} total</span>\n }\n />\n <MetricsCard\n icon={\n <TintedIcon tint=\"success\">\n <Users className=\"h-5 w-5\" />\n </TintedIcon>\n }\n value={stats.activeLearners}\n label=\"Active learners\"\n footer={\n <span className=\"text-xs text-text-secondary\">\n {enrollments.length} enrolment{enrollments.length === 1 ? '' : 's'}\n </span>\n }\n />\n <MetricsCard\n icon={\n <TintedIcon tint=\"accent\">\n <TrendingUp className=\"h-5 w-5\" />\n </TintedIcon>\n }\n value={formatPercent(stats.completionRate)}\n label=\"Completion rate\"\n footer={\n <MiniBar\n value={stats.completionRate}\n barClassName=\"bg-[var(--color-accent-teal)]\"\n />\n }\n />\n <MetricsCard\n icon={\n <TintedIcon tint=\"warning\">\n <Award className=\"h-5 w-5\" />\n </TintedIcon>\n }\n value={certificates.length}\n label=\"Certificates issued\"\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-2\">\n {/* Enrolment status distribution */}\n <Section title=\"Enrolment status\" description={`${enrollments.length} enrolments`}>\n {enrollmentDist.length === 0 ? (\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <p className=\"text-sm text-text-secondary\">No enrolments yet.</p>\n </GlassCard>\n ) : (\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <DistBars rows={enrollmentDist} />\n <div className=\"mt-4 flex flex-wrap items-center justify-between gap-3 border-t border-border-seam pt-3\">\n <span className=\"inline-flex items-center gap-1.5 text-sm text-text-secondary\">\n <GraduationCap className=\"h-4 w-4\" /> Avg progress\n <span className=\"font-medium text-text-primary\">\n {formatPercent(stats.avgProgress)}\n </span>\n </span>\n {enrolmentTrend.length >= 2 && (\n <span className=\"inline-flex items-center gap-2\">\n <Sparkline data={enrolmentTrend} color=\"var(--color-accent-teal)\" />\n <span className=\"text-xs text-text-secondary\">new / month</span>\n </span>\n )}\n </div>\n </GlassCard>\n )}\n </Section>\n\n {/* Learning outcomes — paired circular meters */}\n <Section title=\"Learning outcomes\" description={`${submissions.length} submissions`}>\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-5 p-5\">\n <div className=\"flex items-center justify-around gap-4\">\n <div className=\"flex flex-col items-center gap-2 text-center\">\n <ProgressRing\n value={stats.completionRate}\n size={96}\n stroke={8}\n color=\"var(--color-accent-teal)\"\n label={enrollments.length === 0 ? '—' : undefined}\n />\n <span className=\"text-sm text-text-secondary\">Completion rate</span>\n </div>\n <div className=\"flex flex-col items-center gap-2 text-center\">\n <ProgressRing\n value={stats.passRate}\n size={96}\n stroke={8}\n color=\"var(--color-status-success-text)\"\n label={stats.gradedCount === 0 ? '—' : undefined}\n />\n <span className=\"text-sm text-text-secondary\">Pass rate</span>\n </div>\n </div>\n <div className=\"grid grid-cols-2 gap-3 border-t border-border-seam pt-4\">\n <div>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">\n {stats.gradedCount}\n <span className=\"text-sm font-normal text-text-secondary\">\n /{submissions.length}\n </span>\n </p>\n <p className=\"text-xs text-text-secondary\">Graded</p>\n </div>\n <div>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">\n {stats.passed}\n <span className=\"text-sm font-normal text-text-secondary\">\n {' '}\n of {stats.gradedCount}\n </span>\n </p>\n <p className=\"text-xs text-text-secondary\">Passed</p>\n </div>\n </div>\n {stats.gradedCount === 0 && (\n <p className=\"text-xs text-text-secondary\">\n No submissions have been graded yet — pass rate appears once grading begins.\n </p>\n )}\n </GlassCard>\n </Section>\n </div>\n\n {/* Top courses by enrolment */}\n <Section title=\"Top courses by enrolment\">\n {topCourses.length === 0 ? (\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <p className=\"text-sm text-text-secondary\">No enrolments yet.</p>\n </GlassCard>\n ) : (\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <div className=\"flex flex-col gap-4\">\n {topCourses.map((c, i) => (\n <div key={c.courseId} className=\"flex items-center gap-3\">\n <span\n className={cn(\n 'flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-xs font-semibold tabular-nums',\n i === 0\n ? 'bg-[var(--color-accent-soft)] text-[var(--color-accent-teal)]'\n : 'bg-bg-sunken text-text-secondary'\n )}\n >\n {i + 1}\n </span>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"truncate text-sm text-text-primary\">{c.title}</span>\n <span className=\"shrink-0 text-sm tabular-nums text-text-secondary\">\n {c.count}\n </span>\n </div>\n <MiniBar\n value={c.pct}\n barClassName=\"bg-[var(--color-accent-teal)]\"\n className=\"mt-1.5\"\n height={8}\n />\n </div>\n </div>\n ))}\n </div>\n </GlassCard>\n )}\n </Section>\n </div>\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useBuildmyiq as e } from "../../context/BuildmyiqContext.js";
|
|
2
|
+
import "../../context/index.js";
|
|
2
3
|
import { cn as t } from "../../utils/cn.js";
|
|
3
4
|
import { PageLayout as n } from "../../components/PageLayout.js";
|
|
4
5
|
import { formatDuration as r, formatEnumLabel as i } from "../../utils/formatters.js";
|
|
@@ -7,7 +8,6 @@ import { EmptyState as o } from "../../components/EmptyState.js";
|
|
|
7
8
|
import { ErrorView as s, LoadingView as c } from "../../components/StateViews.js";
|
|
8
9
|
import { MiniBar as l } from "../../components/MiniBar.js";
|
|
9
10
|
import { ProgressRing as u } from "../../components/Viz.js";
|
|
10
|
-
import "../../context/index.js";
|
|
11
11
|
import { useBuiCourse as ee, useBuiEnrollment as te, useBuiRecordLessonProgress as ne, useBuiTransitionEnrollment as re } from "../../hooks/useBuildmyiqApi.js";
|
|
12
12
|
import "../../hooks/index.js";
|
|
13
13
|
import { FileLink as d } from "../../components/FileLink.js";
|