@burdenoff/microfe-buildmyiq 2026.726.1 → 2026.730.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/pages/assessments/AssessmentsPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/assessments/AssessmentsPage.js +5 -2
- package/dist/buildmyiq/pages/assessments/AssessmentsPage.js.map +1 -1
- package/dist/buildmyiq/pages/authoring/AuthoringPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/authoring/AuthoringPage.js +4 -0
- package/dist/buildmyiq/pages/authoring/AuthoringPage.js.map +1 -1
- package/dist/buildmyiq/pages/authoring/CourseBuilderPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/authoring/CourseBuilderPage.js +6 -0
- package/dist/buildmyiq/pages/authoring/CourseBuilderPage.js.map +1 -1
- package/dist/buildmyiq/pages/authoring/_forms.js +1 -1
- package/dist/buildmyiq/pages/authoring/_forms.js.map +1 -1
- package/dist/buildmyiq/pages/certificates/CertificatesPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/certificates/CertificatesPage.js +1 -0
- package/dist/buildmyiq/pages/certificates/CertificatesPage.js.map +1 -1
- package/dist/buildmyiq/pages/cohorts/CohortsPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/cohorts/CohortsPage.js +1 -0
- package/dist/buildmyiq/pages/cohorts/CohortsPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssessmentsPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/assessments/AssessmentsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAqB,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAoEnD,eAAO,MAAM,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"AssessmentsPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/assessments/AssessmentsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAqB,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAoEnD,eAAO,MAAM,eAAe,EAAE,EAga7B,CAAC"}
|
|
@@ -140,7 +140,8 @@ var j = [
|
|
|
140
140
|
children: [/* @__PURE__ */ _("div", {
|
|
141
141
|
className: "grid gap-1.5",
|
|
142
142
|
children: [
|
|
143
|
-
/* @__PURE__ */ g(
|
|
143
|
+
/* @__PURE__ */ g(x, {
|
|
144
|
+
htmlFor: "assessments-course-filter",
|
|
144
145
|
className: "text-xs font-medium uppercase tracking-wide text-text-secondary",
|
|
145
146
|
children: "Course"
|
|
146
147
|
}),
|
|
@@ -150,7 +151,9 @@ var j = [
|
|
|
150
151
|
F(e), L("ALL");
|
|
151
152
|
},
|
|
152
153
|
children: [/* @__PURE__ */ g(O, {
|
|
153
|
-
|
|
154
|
+
id: "assessments-course-filter",
|
|
155
|
+
"aria-label": "Filter assessments by course",
|
|
156
|
+
className: "w-full sm:w-72 data-[placeholder]:text-text-secondary!",
|
|
154
157
|
children: /* @__PURE__ */ g(k, { placeholder: "Select a course" })
|
|
155
158
|
}), /* @__PURE__ */ g(E, { children: J.map((e) => /* @__PURE__ */ g(D, {
|
|
156
159
|
value: e.id,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssessmentsPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/AssessmentsPage.tsx"],"sourcesContent":["/**\n * AssessmentsPage — the assessment authoring surface (primary page of the section).\n * @module buildmyiq/pages/assessments\n *\n * `buiListAssessments` REQUIRES a courseId, so this page is course-scoped: the\n * educator first picks a course, then sees its assessments filtered by status.\n * Everything is real `bui*` data — no fabrication. Create flow wires\n * `useBuiCreateAssessment`.\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { Plus, ClipboardList, CheckCircle2, FileEdit, ListChecks } from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Label,\n Textarea,\n Switch,\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 FilterTabs,\n DataTable,\n EmptyState,\n LoadingView,\n ErrorView,\n type Column,\n type FilterOption,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiCourses, useBuiAssessments, useBuiCreateAssessment } from '../../hooks';\nimport { formatEnumLabel, formatRelativeTime } from '../../utils/formatters';\nimport { ASSESSMENT_TYPES } from '../../types';\nimport type { BuiAssessment, BuiAssessmentStatus, BuiAssessmentType } from '../../types';\n\ntype StatusFilter = 'ALL' | BuiAssessmentStatus;\n\nconst STATUS_TABS: { value: StatusFilter; label: string }[] = [\n { value: 'ALL', label: 'All' },\n { value: 'DRAFT', label: 'Draft' },\n { value: 'PUBLISHED', label: 'Published' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\n\ninterface AssessmentFormState {\n title: string;\n description: string;\n type: BuiAssessmentType;\n passingScore: string;\n maxAttempts: string;\n timeLimitMin: string;\n isGraded: boolean;\n}\n\nconst makeEmptyForm = (): AssessmentFormState => ({\n title: '',\n description: '',\n type: 'QUIZ',\n passingScore: '70',\n maxAttempts: '1',\n timeLimitMin: '0',\n isGraded: true,\n});\n\nexport const AssessmentsPage: FC = () => {\n const { navigateTo } = useBuildmyiq();\n\n const [courseId, setCourseId] = useState<string>('');\n const [status, setStatus] = useState<StatusFilter>('ALL');\n const [dialogOpen, setDialogOpen] = useState(false);\n const [form, setForm] = useState<AssessmentFormState>(makeEmptyForm);\n const [formError, setFormError] = useState<string | null>(null);\n\n const coursesQuery = useBuiCourses({ limit: 100 });\n // Fetch the unfiltered list for DRAFT/PUBLISHED/ALL (so those tab counts stay\n // accurate and switching between them is instant) plus a dedicated\n // `status: ARCHIVED` query. The two are merged below because\n // `buiListAssessments` scopes its soft-delete filter to the requested status\n // (BOFF-4780): an unfiltered/undefined-status call always excludes archived\n // (soft-deleted) rows, so they can never surface via client-side filtering\n // alone — they must be fetched explicitly.\n const assessmentsQuery = useBuiAssessments(courseId || undefined);\n const archivedAssessmentsQuery = useBuiAssessments(courseId || undefined, 'ARCHIVED');\n const createAssessment = useBuiCreateAssessment();\n\n const courses = coursesQuery.data?.items ?? [];\n const assessments = useMemo(\n () => [...(assessmentsQuery.data ?? []), ...(archivedAssessmentsQuery.data ?? [])],\n [assessmentsQuery.data, archivedAssessmentsQuery.data]\n );\n\n const statusCounts = useMemo(() => {\n const counts: Record<StatusFilter, number> = {\n ALL: assessments.length,\n DRAFT: 0,\n PUBLISHED: 0,\n ARCHIVED: 0,\n };\n for (const a of assessments) counts[a.status] += 1;\n return counts;\n }, [assessments]);\n\n const rows = useMemo(\n () => (status === 'ALL' ? assessments : assessments.filter((a) => a.status === status)),\n [assessments, status]\n );\n\n const totalQuestions = useMemo(\n () => assessments.reduce((sum, a) => sum + (a.questions?.length ?? 0), 0),\n [assessments]\n );\n\n const tabOptions: FilterOption<StatusFilter>[] = STATUS_TABS.map((t) => ({\n value: t.value,\n label: t.label,\n count: statusCounts[t.value],\n }));\n\n const assessmentsIsLoading = assessmentsQuery.isLoading || archivedAssessmentsQuery.isLoading;\n const assessmentsIsError = assessmentsQuery.isError || archivedAssessmentsQuery.isError;\n const assessmentsIsSuccess = assessmentsQuery.isSuccess && archivedAssessmentsQuery.isSuccess;\n const refetchAssessments = () => {\n void assessmentsQuery.refetch();\n void archivedAssessmentsQuery.refetch();\n };\n\n const columns: Column<BuiAssessment>[] = [\n {\n key: 'title',\n header: 'Assessment',\n render: (a) => (\n <div className=\"min-w-0\">\n <p className=\"font-semibold text-text-primary\">{a.title}</p>\n {a.description && (\n <p\n className=\"mt-0.5 max-w-md truncate text-xs text-text-secondary\"\n title={a.description}\n >\n {a.description}\n </p>\n )}\n </div>\n ),\n },\n {\n key: 'type',\n header: 'Type',\n render: (a) => <StatusBadge status={a.type} />,\n },\n {\n key: 'status',\n header: 'Status',\n sortValue: (a) => a.status,\n render: (a) => <StatusBadge status={a.status} />,\n },\n {\n key: 'passingScore',\n header: 'Pass',\n align: 'right',\n hideOnMobile: true,\n sortValue: (a) => a.passingScore,\n render: (a) => <span className=\"tabular-nums text-text-secondary\">{a.passingScore}%</span>,\n },\n {\n key: 'questions',\n header: 'Questions',\n align: 'right',\n hideOnMobile: true,\n render: (a) => (\n <span className=\"tabular-nums text-text-secondary\">{a.questions?.length ?? '—'}</span>\n ),\n },\n {\n key: 'updatedAt',\n header: 'Updated',\n hideOnMobile: true,\n sortValue: (a) => a.updatedAt,\n render: (a) => (\n <span className=\"whitespace-nowrap text-text-secondary\">\n {formatRelativeTime(a.updatedAt)}\n </span>\n ),\n },\n ];\n\n const openDialog = () => {\n setForm(makeEmptyForm());\n setFormError(null);\n setDialogOpen(true);\n };\n\n const closeDialog = () => {\n setDialogOpen(false);\n setForm(makeEmptyForm());\n setFormError(null);\n };\n\n const handleCreate = async () => {\n if (!courseId) {\n setFormError('Pick a course first.');\n return;\n }\n if (!form.title.trim()) {\n setFormError('Title is required.');\n return;\n }\n try {\n const created = await createAssessment.mutateAsync({\n courseId,\n title: form.title.trim(),\n description: form.description.trim() || undefined,\n type: form.type,\n passingScore: form.passingScore ? Number(form.passingScore) : undefined,\n maxAttempts: form.maxAttempts ? Number(form.maxAttempts) : undefined,\n timeLimitMin: form.timeLimitMin ? Number(form.timeLimitMin) : undefined,\n isGraded: form.isGraded,\n });\n closeDialog();\n navigateTo(`assessments/${created.id}`);\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to create assessment.');\n }\n };\n\n return (\n <PageLayout\n title=\"Assessments\"\n description=\"Quizzes, exams and graded assignments for every course in your workspace.\"\n titleGradient\n aurora\n actions={\n <Button onClick={openDialog} disabled={!courseId}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New assessment\n </Button>\n }\n >\n <GlassCard treatment=\"plain\" className=\"mb-5 p-4\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end\">\n <div className=\"grid gap-1.5\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Course\n </span>\n <Select\n value={courseId}\n onValueChange={(v) => {\n setCourseId(v);\n setStatus('ALL');\n }}\n >\n <SelectTrigger className=\"w-full sm:w-72\">\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 {coursesQuery.isSuccess && courses.length === 0 && (\n <p className=\"text-xs text-text-secondary\">\n No courses yet — create one in Authoring first.\n </p>\n )}\n </div>\n {courseId && (\n <div className=\"grid gap-1.5\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Status\n </span>\n <FilterTabs options={tabOptions} value={status} onChange={setStatus} />\n </div>\n )}\n </div>\n </GlassCard>\n\n {courseId && assessmentsIsSuccess && assessments.length > 0 && (\n <div className=\"mb-5 grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <MetricsCard\n icon={<ClipboardList className=\"h-5 w-5\" />}\n value={statusCounts.ALL}\n label=\"Assessments\"\n />\n <MetricsCard\n icon={<CheckCircle2 className=\"h-5 w-5\" />}\n value={statusCounts.PUBLISHED}\n label=\"Published\"\n accent=\"success\"\n />\n <MetricsCard\n icon={<FileEdit className=\"h-5 w-5\" />}\n value={statusCounts.DRAFT}\n label=\"Drafts\"\n />\n <MetricsCard\n icon={<ListChecks className=\"h-5 w-5\" />}\n value={totalQuestions}\n label=\"Questions\"\n />\n </div>\n )}\n\n {!courseId ? (\n <EmptyState\n illustration=\"onboarding\"\n icon={<ClipboardList className=\"h-10 w-10\" />}\n title=\"Pick a course to see its assessments\"\n description=\"Assessments are scoped to a course. Choose a course above to view, filter and create its quizzes and exams.\"\n />\n ) : assessmentsIsLoading ? (\n <LoadingView label=\"Loading assessments…\" />\n ) : assessmentsIsError ? (\n <ErrorView\n message=\"Couldn't load assessments for this course.\"\n onRetry={refetchAssessments}\n />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<ClipboardList className=\"h-10 w-10\" />}\n title={status === 'ALL' ? 'No assessments yet' : 'No assessments match this filter'}\n description={\n status === 'ALL'\n ? 'Create the first quiz, exam or assignment for this course.'\n : 'Try a different status filter, or create a new assessment.'\n }\n action={\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New assessment\n </Button>\n }\n />\n ) : (\n <DataTable\n columns={columns}\n rows={rows}\n rowKey={(a) => a.id}\n onRowClick={(a) => navigateTo(`assessments/${a.id}`)}\n defaultSort={{ key: 'updatedAt', dir: 'desc' }}\n />\n )}\n\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n if (!open) closeDialog();\n else setDialogOpen(true);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New assessment</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Add a quiz, exam or assignment to this course. You can add questions after it's\n created.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"assessmentTitle\">Title *</Label>\n <Input\n id=\"assessmentTitle\"\n value={form.title}\n onChange={(e) => setForm({ ...form, title: e.target.value })}\n placeholder=\"e.g. Module 1 knowledge check\"\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"assessmentDescription\">Description</Label>\n <Textarea\n id=\"assessmentDescription\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n placeholder=\"What this assessment covers and how it's graded.\"\n rows={2}\n />\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"assessmentType\">Type</Label>\n <Select\n value={form.type}\n onValueChange={(v) => setForm({ ...form, type: v as BuiAssessmentType })}\n >\n <SelectTrigger id=\"assessmentType\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ASSESSMENT_TYPES.map((t) => (\n <SelectItem key={t} value={t}>\n {formatEnumLabel(t)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"passingScore\">Passing score (%)</Label>\n <Input\n id=\"passingScore\"\n type=\"number\"\n min=\"0\"\n max=\"100\"\n step=\"1\"\n value={form.passingScore}\n onChange={(e) => setForm({ ...form, passingScore: e.target.value })}\n placeholder=\"70\"\n />\n </div>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"maxAttempts\">Max attempts</Label>\n <Input\n id=\"maxAttempts\"\n type=\"number\"\n min=\"1\"\n step=\"1\"\n value={form.maxAttempts}\n onChange={(e) => setForm({ ...form, maxAttempts: e.target.value })}\n placeholder=\"1\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"timeLimitMin\">Time limit (minutes)</Label>\n <Input\n id=\"timeLimitMin\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={form.timeLimitMin}\n onChange={(e) => setForm({ ...form, timeLimitMin: e.target.value })}\n placeholder=\"0 = no limit\"\n />\n </div>\n </div>\n\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface px-3 py-2.5\">\n <div className=\"min-w-0\">\n <Label htmlFor=\"isGraded\" className=\"cursor-pointer\">\n Graded\n </Label>\n <p className=\"text-xs text-text-secondary\">\n Contributes to the learner's score and pass/fail outcome.\n </p>\n </div>\n <Switch\n id=\"isGraded\"\n checked={form.isGraded}\n onCheckedChange={(checked) => setForm({ ...form, isGraded: checked })}\n />\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={closeDialog}>\n Cancel\n </Button>\n <Button onClick={handleCreate} disabled={createAssessment.isPending}>\n {createAssessment.isPending ? 'Creating…' : 'Create assessment'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AssessmentsPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/AssessmentsPage.tsx"],"sourcesContent":["/**\n * AssessmentsPage — the assessment authoring surface (primary page of the section).\n * @module buildmyiq/pages/assessments\n *\n * `buiListAssessments` REQUIRES a courseId, so this page is course-scoped: the\n * educator first picks a course, then sees its assessments filtered by status.\n * Everything is real `bui*` data — no fabrication. Create flow wires\n * `useBuiCreateAssessment`.\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { Plus, ClipboardList, CheckCircle2, FileEdit, ListChecks } from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Label,\n Textarea,\n Switch,\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 FilterTabs,\n DataTable,\n EmptyState,\n LoadingView,\n ErrorView,\n type Column,\n type FilterOption,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiCourses, useBuiAssessments, useBuiCreateAssessment } from '../../hooks';\nimport { formatEnumLabel, formatRelativeTime } from '../../utils/formatters';\nimport { ASSESSMENT_TYPES } from '../../types';\nimport type { BuiAssessment, BuiAssessmentStatus, BuiAssessmentType } from '../../types';\n\ntype StatusFilter = 'ALL' | BuiAssessmentStatus;\n\nconst STATUS_TABS: { value: StatusFilter; label: string }[] = [\n { value: 'ALL', label: 'All' },\n { value: 'DRAFT', label: 'Draft' },\n { value: 'PUBLISHED', label: 'Published' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\n\ninterface AssessmentFormState {\n title: string;\n description: string;\n type: BuiAssessmentType;\n passingScore: string;\n maxAttempts: string;\n timeLimitMin: string;\n isGraded: boolean;\n}\n\nconst makeEmptyForm = (): AssessmentFormState => ({\n title: '',\n description: '',\n type: 'QUIZ',\n passingScore: '70',\n maxAttempts: '1',\n timeLimitMin: '0',\n isGraded: true,\n});\n\nexport const AssessmentsPage: FC = () => {\n const { navigateTo } = useBuildmyiq();\n\n const [courseId, setCourseId] = useState<string>('');\n const [status, setStatus] = useState<StatusFilter>('ALL');\n const [dialogOpen, setDialogOpen] = useState(false);\n const [form, setForm] = useState<AssessmentFormState>(makeEmptyForm);\n const [formError, setFormError] = useState<string | null>(null);\n\n const coursesQuery = useBuiCourses({ limit: 100 });\n // Fetch the unfiltered list for DRAFT/PUBLISHED/ALL (so those tab counts stay\n // accurate and switching between them is instant) plus a dedicated\n // `status: ARCHIVED` query. The two are merged below because\n // `buiListAssessments` scopes its soft-delete filter to the requested status\n // (BOFF-4780): an unfiltered/undefined-status call always excludes archived\n // (soft-deleted) rows, so they can never surface via client-side filtering\n // alone — they must be fetched explicitly.\n const assessmentsQuery = useBuiAssessments(courseId || undefined);\n const archivedAssessmentsQuery = useBuiAssessments(courseId || undefined, 'ARCHIVED');\n const createAssessment = useBuiCreateAssessment();\n\n const courses = coursesQuery.data?.items ?? [];\n const assessments = useMemo(\n () => [...(assessmentsQuery.data ?? []), ...(archivedAssessmentsQuery.data ?? [])],\n [assessmentsQuery.data, archivedAssessmentsQuery.data]\n );\n\n const statusCounts = useMemo(() => {\n const counts: Record<StatusFilter, number> = {\n ALL: assessments.length,\n DRAFT: 0,\n PUBLISHED: 0,\n ARCHIVED: 0,\n };\n for (const a of assessments) counts[a.status] += 1;\n return counts;\n }, [assessments]);\n\n const rows = useMemo(\n () => (status === 'ALL' ? assessments : assessments.filter((a) => a.status === status)),\n [assessments, status]\n );\n\n const totalQuestions = useMemo(\n () => assessments.reduce((sum, a) => sum + (a.questions?.length ?? 0), 0),\n [assessments]\n );\n\n const tabOptions: FilterOption<StatusFilter>[] = STATUS_TABS.map((t) => ({\n value: t.value,\n label: t.label,\n count: statusCounts[t.value],\n }));\n\n const assessmentsIsLoading = assessmentsQuery.isLoading || archivedAssessmentsQuery.isLoading;\n const assessmentsIsError = assessmentsQuery.isError || archivedAssessmentsQuery.isError;\n const assessmentsIsSuccess = assessmentsQuery.isSuccess && archivedAssessmentsQuery.isSuccess;\n const refetchAssessments = () => {\n void assessmentsQuery.refetch();\n void archivedAssessmentsQuery.refetch();\n };\n\n const columns: Column<BuiAssessment>[] = [\n {\n key: 'title',\n header: 'Assessment',\n render: (a) => (\n <div className=\"min-w-0\">\n <p className=\"font-semibold text-text-primary\">{a.title}</p>\n {a.description && (\n <p\n className=\"mt-0.5 max-w-md truncate text-xs text-text-secondary\"\n title={a.description}\n >\n {a.description}\n </p>\n )}\n </div>\n ),\n },\n {\n key: 'type',\n header: 'Type',\n render: (a) => <StatusBadge status={a.type} />,\n },\n {\n key: 'status',\n header: 'Status',\n sortValue: (a) => a.status,\n render: (a) => <StatusBadge status={a.status} />,\n },\n {\n key: 'passingScore',\n header: 'Pass',\n align: 'right',\n hideOnMobile: true,\n sortValue: (a) => a.passingScore,\n render: (a) => <span className=\"tabular-nums text-text-secondary\">{a.passingScore}%</span>,\n },\n {\n key: 'questions',\n header: 'Questions',\n align: 'right',\n hideOnMobile: true,\n render: (a) => (\n <span className=\"tabular-nums text-text-secondary\">{a.questions?.length ?? '—'}</span>\n ),\n },\n {\n key: 'updatedAt',\n header: 'Updated',\n hideOnMobile: true,\n sortValue: (a) => a.updatedAt,\n render: (a) => (\n <span className=\"whitespace-nowrap text-text-secondary\">\n {formatRelativeTime(a.updatedAt)}\n </span>\n ),\n },\n ];\n\n const openDialog = () => {\n setForm(makeEmptyForm());\n setFormError(null);\n setDialogOpen(true);\n };\n\n const closeDialog = () => {\n setDialogOpen(false);\n setForm(makeEmptyForm());\n setFormError(null);\n };\n\n const handleCreate = async () => {\n if (!courseId) {\n setFormError('Pick a course first.');\n return;\n }\n if (!form.title.trim()) {\n setFormError('Title is required.');\n return;\n }\n try {\n const created = await createAssessment.mutateAsync({\n courseId,\n title: form.title.trim(),\n description: form.description.trim() || undefined,\n type: form.type,\n passingScore: form.passingScore ? Number(form.passingScore) : undefined,\n maxAttempts: form.maxAttempts ? Number(form.maxAttempts) : undefined,\n timeLimitMin: form.timeLimitMin ? Number(form.timeLimitMin) : undefined,\n isGraded: form.isGraded,\n });\n closeDialog();\n navigateTo(`assessments/${created.id}`);\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to create assessment.');\n }\n };\n\n return (\n <PageLayout\n title=\"Assessments\"\n description=\"Quizzes, exams and graded assignments for every course in your workspace.\"\n titleGradient\n aurora\n actions={\n <Button onClick={openDialog} disabled={!courseId}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New assessment\n </Button>\n }\n >\n <GlassCard treatment=\"plain\" className=\"mb-5 p-4\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end\">\n <div className=\"grid gap-1.5\">\n <Label\n htmlFor=\"assessments-course-filter\"\n className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\"\n >\n Course\n </Label>\n <Select\n value={courseId}\n onValueChange={(v) => {\n setCourseId(v);\n setStatus('ALL');\n }}\n >\n <SelectTrigger\n id=\"assessments-course-filter\"\n aria-label=\"Filter assessments by course\"\n className=\"w-full sm:w-72 data-[placeholder]:text-text-secondary!\"\n >\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 {coursesQuery.isSuccess && courses.length === 0 && (\n <p className=\"text-xs text-text-secondary\">\n No courses yet — create one in Authoring first.\n </p>\n )}\n </div>\n {courseId && (\n <div className=\"grid gap-1.5\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Status\n </span>\n <FilterTabs options={tabOptions} value={status} onChange={setStatus} />\n </div>\n )}\n </div>\n </GlassCard>\n\n {courseId && assessmentsIsSuccess && assessments.length > 0 && (\n <div className=\"mb-5 grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <MetricsCard\n icon={<ClipboardList className=\"h-5 w-5\" />}\n value={statusCounts.ALL}\n label=\"Assessments\"\n />\n <MetricsCard\n icon={<CheckCircle2 className=\"h-5 w-5\" />}\n value={statusCounts.PUBLISHED}\n label=\"Published\"\n accent=\"success\"\n />\n <MetricsCard\n icon={<FileEdit className=\"h-5 w-5\" />}\n value={statusCounts.DRAFT}\n label=\"Drafts\"\n />\n <MetricsCard\n icon={<ListChecks className=\"h-5 w-5\" />}\n value={totalQuestions}\n label=\"Questions\"\n />\n </div>\n )}\n\n {!courseId ? (\n <EmptyState\n illustration=\"onboarding\"\n icon={<ClipboardList className=\"h-10 w-10\" />}\n title=\"Pick a course to see its assessments\"\n description=\"Assessments are scoped to a course. Choose a course above to view, filter and create its quizzes and exams.\"\n />\n ) : assessmentsIsLoading ? (\n <LoadingView label=\"Loading assessments…\" />\n ) : assessmentsIsError ? (\n <ErrorView\n message=\"Couldn't load assessments for this course.\"\n onRetry={refetchAssessments}\n />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<ClipboardList className=\"h-10 w-10\" />}\n title={status === 'ALL' ? 'No assessments yet' : 'No assessments match this filter'}\n description={\n status === 'ALL'\n ? 'Create the first quiz, exam or assignment for this course.'\n : 'Try a different status filter, or create a new assessment.'\n }\n action={\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New assessment\n </Button>\n }\n />\n ) : (\n <DataTable\n columns={columns}\n rows={rows}\n rowKey={(a) => a.id}\n onRowClick={(a) => navigateTo(`assessments/${a.id}`)}\n defaultSort={{ key: 'updatedAt', dir: 'desc' }}\n />\n )}\n\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n if (!open) closeDialog();\n else setDialogOpen(true);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New assessment</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Add a quiz, exam or assignment to this course. You can add questions after it's\n created.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"assessmentTitle\">Title *</Label>\n <Input\n id=\"assessmentTitle\"\n value={form.title}\n onChange={(e) => setForm({ ...form, title: e.target.value })}\n placeholder=\"e.g. Module 1 knowledge check\"\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"assessmentDescription\">Description</Label>\n <Textarea\n id=\"assessmentDescription\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n placeholder=\"What this assessment covers and how it's graded.\"\n rows={2}\n />\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"assessmentType\">Type</Label>\n <Select\n value={form.type}\n onValueChange={(v) => setForm({ ...form, type: v as BuiAssessmentType })}\n >\n <SelectTrigger id=\"assessmentType\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ASSESSMENT_TYPES.map((t) => (\n <SelectItem key={t} value={t}>\n {formatEnumLabel(t)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"passingScore\">Passing score (%)</Label>\n <Input\n id=\"passingScore\"\n type=\"number\"\n min=\"0\"\n max=\"100\"\n step=\"1\"\n value={form.passingScore}\n onChange={(e) => setForm({ ...form, passingScore: e.target.value })}\n placeholder=\"70\"\n />\n </div>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"maxAttempts\">Max attempts</Label>\n <Input\n id=\"maxAttempts\"\n type=\"number\"\n min=\"1\"\n step=\"1\"\n value={form.maxAttempts}\n onChange={(e) => setForm({ ...form, maxAttempts: e.target.value })}\n placeholder=\"1\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"timeLimitMin\">Time limit (minutes)</Label>\n <Input\n id=\"timeLimitMin\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={form.timeLimitMin}\n onChange={(e) => setForm({ ...form, timeLimitMin: e.target.value })}\n placeholder=\"0 = no limit\"\n />\n </div>\n </div>\n\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface px-3 py-2.5\">\n <div className=\"min-w-0\">\n <Label htmlFor=\"isGraded\" className=\"cursor-pointer\">\n Graded\n </Label>\n <p className=\"text-xs text-text-secondary\">\n Contributes to the learner's score and pass/fail outcome.\n </p>\n </div>\n <Switch\n id=\"isGraded\"\n checked={form.isGraded}\n onCheckedChange={(checked) => setForm({ ...form, isGraded: checked })}\n />\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={closeDialog}>\n Cancel\n </Button>\n <Button onClick={handleCreate} disabled={createAssessment.isPending}>\n {createAssessment.isPending ? 'Creating…' : 'Create assessment'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthoringPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/authoring/AuthoringPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"AuthoringPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/authoring/AuthoringPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAsLhC,eAAO,MAAM,aAAa,EAAE,EAkI3B,CAAC"}
|
|
@@ -123,7 +123,9 @@ var j = [
|
|
|
123
123
|
children: [
|
|
124
124
|
/* @__PURE__ */ w(h, {
|
|
125
125
|
label: "Level",
|
|
126
|
+
htmlFor: "new-course-level",
|
|
126
127
|
children: /* @__PURE__ */ w(v, {
|
|
128
|
+
id: "new-course-level",
|
|
127
129
|
value: k,
|
|
128
130
|
onChange: (e) => j(e),
|
|
129
131
|
options: m.map((e) => ({
|
|
@@ -156,7 +158,9 @@ var j = [
|
|
|
156
158
|
}),
|
|
157
159
|
/* @__PURE__ */ w(h, {
|
|
158
160
|
label: "Skill",
|
|
161
|
+
htmlFor: "new-course-skill",
|
|
159
162
|
children: /* @__PURE__ */ w(v, {
|
|
163
|
+
id: "new-course-skill",
|
|
160
164
|
value: I,
|
|
161
165
|
onChange: L,
|
|
162
166
|
options: [{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthoringPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/authoring/AuthoringPage.tsx"],"sourcesContent":["/**\n * AuthoringPage — the instructor course-management surface.\n * @module buildmyiq/pages/authoring\n *\n * Lists EVERY workspace course (including DRAFT / IN_REVIEW) via `useBuiCourses`,\n * filterable by workflow status and optionally narrowed to the caller's own\n * courses. A \"New course\" modal creates a draft (`useBuiCreateCourse`) and jumps\n * straight into the CourseBuilder. All data is live `bui*` data — no fabrication.\n */\n\nimport type { FC } from 'react';\nimport { useMemo, useState } from 'react';\nimport { Plus } from 'lucide-react';\nimport { Button, GlassCard, Input, Switch, Textarea } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n FilterTabs,\n Pagination,\n EmptyState,\n LoadingView,\n ErrorView,\n} from '../../components';\nimport { CourseCard } from '../_shared';\nimport { Modal, Field, SelectField, NO_SKILL, slugify } from './_forms';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiCourses, useBuiCreateCourse, useBuiSkills, useCursorPager } from '../../hooks';\nimport { COURSE_LEVELS } from '../../types';\nimport type { BuiCourse, BuiCourseLevel, BuiCourseStatus, BuiCreateCourseInput } from '../../types';\nimport { formatEnumLabel } from '../../utils/formatters';\nimport { BuiPermissionButton } from '../../components/BuiPermissionButton';\n\ntype StatusFilter = 'ALL' | BuiCourseStatus;\n\nconst STATUS_TABS: { value: StatusFilter; label: string }[] = [\n { value: 'ALL', label: 'All' },\n { value: 'DRAFT', label: 'Draft' },\n { value: 'IN_REVIEW', label: 'In review' },\n { value: 'PUBLISHED', label: 'Published' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\n\nconst PAGE_SIZE = 24;\n\n/**\n * Create-course modal. Mounted only while open so its form state resets each\n * time it is opened.\n */\nconst NewCourseModal: FC<{ onClose: () => void; onCreated: (course: BuiCourse) => void }> = ({\n onClose,\n onCreated,\n}) => {\n const { currentUser } = useBuildmyiq();\n const skillsQuery = useBuiSkills();\n const createCourse = useBuiCreateCourse();\n\n const [title, setTitle] = useState('');\n const [slug, setSlug] = useState('');\n const [slugEdited, setSlugEdited] = useState(false);\n const [summary, setSummary] = useState('');\n const [level, setLevel] = useState<BuiCourseLevel>('BEGINNER');\n const [language, setLanguage] = useState('en');\n const [estimatedHours, setEstimatedHours] = useState('');\n const [skillId, setSkillId] = useState<string>(NO_SKILL);\n const [error, setError] = useState<string | null>(null);\n\n const skills = skillsQuery.data ?? [];\n\n const onTitleChange = (value: string) => {\n setTitle(value);\n if (!slugEdited) setSlug(slugify(value));\n };\n\n const canSubmit = title.trim().length > 0 && slug.trim().length > 0 && !createCourse.isPending;\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n setError(null);\n const input: BuiCreateCourseInput = {\n title: title.trim(),\n slug: slug.trim(),\n summary: summary.trim() || undefined,\n level,\n language: language.trim() || 'en',\n estimatedHours: estimatedHours.trim() ? Number(estimatedHours) : undefined,\n skillId: skillId !== NO_SKILL ? skillId : undefined,\n instructorId: currentUser?.id,\n };\n try {\n const course = await createCourse.mutateAsync(input);\n onCreated(course);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Failed to create course.');\n }\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"New course\"\n description=\"Create a draft course — you can add modules, lessons and assessments next.\"\n className=\"sm:max-w-xl\"\n footer={\n <>\n <Button variant=\"ghost\" onClick={onClose} disabled={createCourse.isPending}>\n Cancel\n </Button>\n <Button onClick={handleSubmit} disabled={!canSubmit}>\n {createCourse.isPending ? 'Creating…' : 'Create course'}\n </Button>\n </>\n }\n >\n <div className=\"grid gap-4\">\n <Field label=\"Title\" htmlFor=\"new-course-title\" required>\n <Input\n id=\"new-course-title\"\n value={title}\n onChange={(e) => onTitleChange(e.target.value)}\n placeholder=\"e.g. Foundations of Data Literacy\"\n />\n </Field>\n <Field\n label=\"Slug\"\n htmlFor=\"new-course-slug\"\n required\n hint=\"Used in the course URL. Auto-generated from the title — edit if you like.\"\n >\n <Input\n id=\"new-course-slug\"\n value={slug}\n onChange={(e) => {\n setSlug(e.target.value);\n setSlugEdited(true);\n }}\n placeholder=\"foundations-of-data-literacy\"\n />\n </Field>\n <Field label=\"Summary\" htmlFor=\"new-course-summary\">\n <Textarea\n id=\"new-course-summary\"\n rows={3}\n value={summary}\n onChange={(e) => setSummary(e.target.value)}\n placeholder=\"A short description shown on the catalog card.\"\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=\"new-course-language\">\n <Input\n id=\"new-course-language\"\n value={language}\n onChange={(e) => setLanguage(e.target.value)}\n placeholder=\"en\"\n />\n </Field>\n <Field label=\"Estimated hours\" htmlFor=\"new-course-hours\">\n <Input\n id=\"new-course-hours\"\n type=\"number\"\n min={0}\n value={estimatedHours}\n onChange={(e) => setEstimatedHours(e.target.value)}\n placeholder=\"0\"\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 {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n </Modal>\n );\n};\n\nexport const AuthoringPage: FC = () => {\n const { currentUser, navigateTo } = useBuildmyiq();\n const myId = currentUser?.id;\n const [status, setStatus] = useState<StatusFilter>('ALL');\n const [mineOnly, setMineOnly] = useState(false);\n const [showCreate, setShowCreate] = useState(false);\n const pager = useCursorPager();\n\n const coursesQuery = useBuiCourses({\n status: status === 'ALL' ? undefined : status,\n cursor: pager.cursor,\n limit: PAGE_SIZE,\n });\n\n const items: BuiCourse[] = useMemo(() => coursesQuery.data?.items ?? [], [coursesQuery.data]);\n const visible = useMemo(\n () => (mineOnly && myId ? items.filter((c) => c.instructorId === myId) : items),\n [items, mineOnly, myId]\n );\n\n const handleStatus = (next: StatusFilter) => {\n setStatus(next);\n pager.reset();\n };\n\n const emptyTitle = mineOnly\n ? 'No courses you author yet'\n : status === 'ALL'\n ? 'No courses yet'\n : `No ${formatEnumLabel(status).toLowerCase()} courses`;\n\n return (\n <PageLayout\n title=\"Authoring\"\n description=\"Create, curate and publish your workspace courses.\"\n titleGradient\n aurora\n actions={\n <BuiPermissionButton resource=\"buildmyiq-course\" action=\"create\" deniedBehavior=\"hide\">\n <Button onClick={() => setShowCreate(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New course\n </Button>\n </BuiPermissionButton>\n }\n >\n <div className=\"flex flex-col gap-5\">\n <GlassCard\n treatment=\"plain\"\n className=\"flex flex-col gap-3 p-3 sm:flex-row sm:items-center sm:justify-between\"\n >\n <div className=\"-mx-1 overflow-x-auto px-1\">\n <FilterTabs\n options={STATUS_TABS}\n value={status}\n onChange={handleStatus}\n className=\"border-0 bg-transparent p-0\"\n />\n </div>\n <label className=\"inline-flex shrink-0 cursor-pointer items-center gap-2 text-sm text-text-secondary\">\n <Switch checked={mineOnly} onCheckedChange={setMineOnly} disabled={!currentUser?.id} />\n Mine only\n </label>\n </GlassCard>\n\n {coursesQuery.isLoading ? (\n <LoadingView label=\"Loading courses…\" />\n ) : coursesQuery.isError ? (\n <ErrorView\n message=\"We couldn't load your courses.\"\n onRetry={() => coursesQuery.refetch()}\n />\n ) : (\n <>\n {visible.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={emptyTitle}\n description={\n mineOnly\n ? 'None of the courses on this page are authored by you — clear \"Mine only\" or page through to find them.'\n : 'Create your first course to start building curriculum, assessments and cohorts.'\n }\n action={\n <BuiPermissionButton\n resource=\"buildmyiq-course\"\n action=\"create\"\n deniedBehavior=\"hide\"\n >\n <Button onClick={() => setShowCreate(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New course\n </Button>\n </BuiPermissionButton>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4\">\n {visible.map((course) => (\n <CourseCard\n key={course.id}\n course={course}\n showStatus\n onClick={() => navigateTo(`authoring/${course.id}`)}\n />\n ))}\n </div>\n )}\n {/* Pager stays visible even when mineOnly trims the page empty, so the\n user can advance to later pages to find their courses. */}\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={visible.length}\n hasMore={coursesQuery.data?.pageInfo.hasMore ?? false}\n onPageChange={(p) => pager.onPageChange(p, coursesQuery.data?.pageInfo.cursor)}\n />\n </>\n )}\n </div>\n\n {showCreate && (\n <NewCourseModal\n onClose={() => setShowCreate(false)}\n onCreated={(course) => {\n setShowCreate(false);\n navigateTo(`authoring/${course.id}`);\n }}\n />\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AuthoringPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/authoring/AuthoringPage.tsx"],"sourcesContent":["/**\n * AuthoringPage — the instructor course-management surface.\n * @module buildmyiq/pages/authoring\n *\n * Lists EVERY workspace course (including DRAFT / IN_REVIEW) via `useBuiCourses`,\n * filterable by workflow status and optionally narrowed to the caller's own\n * courses. A \"New course\" modal creates a draft (`useBuiCreateCourse`) and jumps\n * straight into the CourseBuilder. All data is live `bui*` data — no fabrication.\n */\n\nimport type { FC } from 'react';\nimport { useMemo, useState } from 'react';\nimport { Plus } from 'lucide-react';\nimport { Button, GlassCard, Input, Switch, Textarea } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n FilterTabs,\n Pagination,\n EmptyState,\n LoadingView,\n ErrorView,\n} from '../../components';\nimport { CourseCard } from '../_shared';\nimport { Modal, Field, SelectField, NO_SKILL, slugify } from './_forms';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiCourses, useBuiCreateCourse, useBuiSkills, useCursorPager } from '../../hooks';\nimport { COURSE_LEVELS } from '../../types';\nimport type { BuiCourse, BuiCourseLevel, BuiCourseStatus, BuiCreateCourseInput } from '../../types';\nimport { formatEnumLabel } from '../../utils/formatters';\nimport { BuiPermissionButton } from '../../components/BuiPermissionButton';\n\ntype StatusFilter = 'ALL' | BuiCourseStatus;\n\nconst STATUS_TABS: { value: StatusFilter; label: string }[] = [\n { value: 'ALL', label: 'All' },\n { value: 'DRAFT', label: 'Draft' },\n { value: 'IN_REVIEW', label: 'In review' },\n { value: 'PUBLISHED', label: 'Published' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\n\nconst PAGE_SIZE = 24;\n\n/**\n * Create-course modal. Mounted only while open so its form state resets each\n * time it is opened.\n */\nconst NewCourseModal: FC<{ onClose: () => void; onCreated: (course: BuiCourse) => void }> = ({\n onClose,\n onCreated,\n}) => {\n const { currentUser } = useBuildmyiq();\n const skillsQuery = useBuiSkills();\n const createCourse = useBuiCreateCourse();\n\n const [title, setTitle] = useState('');\n const [slug, setSlug] = useState('');\n const [slugEdited, setSlugEdited] = useState(false);\n const [summary, setSummary] = useState('');\n const [level, setLevel] = useState<BuiCourseLevel>('BEGINNER');\n const [language, setLanguage] = useState('en');\n const [estimatedHours, setEstimatedHours] = useState('');\n const [skillId, setSkillId] = useState<string>(NO_SKILL);\n const [error, setError] = useState<string | null>(null);\n\n const skills = skillsQuery.data ?? [];\n\n const onTitleChange = (value: string) => {\n setTitle(value);\n if (!slugEdited) setSlug(slugify(value));\n };\n\n const canSubmit = title.trim().length > 0 && slug.trim().length > 0 && !createCourse.isPending;\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n setError(null);\n const input: BuiCreateCourseInput = {\n title: title.trim(),\n slug: slug.trim(),\n summary: summary.trim() || undefined,\n level,\n language: language.trim() || 'en',\n estimatedHours: estimatedHours.trim() ? Number(estimatedHours) : undefined,\n skillId: skillId !== NO_SKILL ? skillId : undefined,\n instructorId: currentUser?.id,\n };\n try {\n const course = await createCourse.mutateAsync(input);\n onCreated(course);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Failed to create course.');\n }\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"New course\"\n description=\"Create a draft course — you can add modules, lessons and assessments next.\"\n className=\"sm:max-w-xl\"\n footer={\n <>\n <Button variant=\"ghost\" onClick={onClose} disabled={createCourse.isPending}>\n Cancel\n </Button>\n <Button onClick={handleSubmit} disabled={!canSubmit}>\n {createCourse.isPending ? 'Creating…' : 'Create course'}\n </Button>\n </>\n }\n >\n <div className=\"grid gap-4\">\n <Field label=\"Title\" htmlFor=\"new-course-title\" required>\n <Input\n id=\"new-course-title\"\n value={title}\n onChange={(e) => onTitleChange(e.target.value)}\n placeholder=\"e.g. Foundations of Data Literacy\"\n />\n </Field>\n <Field\n label=\"Slug\"\n htmlFor=\"new-course-slug\"\n required\n hint=\"Used in the course URL. Auto-generated from the title — edit if you like.\"\n >\n <Input\n id=\"new-course-slug\"\n value={slug}\n onChange={(e) => {\n setSlug(e.target.value);\n setSlugEdited(true);\n }}\n placeholder=\"foundations-of-data-literacy\"\n />\n </Field>\n <Field label=\"Summary\" htmlFor=\"new-course-summary\">\n <Textarea\n id=\"new-course-summary\"\n rows={3}\n value={summary}\n onChange={(e) => setSummary(e.target.value)}\n placeholder=\"A short description shown on the catalog card.\"\n />\n </Field>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <Field label=\"Level\" htmlFor=\"new-course-level\">\n <SelectField\n id=\"new-course-level\"\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=\"new-course-language\">\n <Input\n id=\"new-course-language\"\n value={language}\n onChange={(e) => setLanguage(e.target.value)}\n placeholder=\"en\"\n />\n </Field>\n <Field label=\"Estimated hours\" htmlFor=\"new-course-hours\">\n <Input\n id=\"new-course-hours\"\n type=\"number\"\n min={0}\n value={estimatedHours}\n onChange={(e) => setEstimatedHours(e.target.value)}\n placeholder=\"0\"\n />\n </Field>\n <Field label=\"Skill\" htmlFor=\"new-course-skill\">\n <SelectField\n id=\"new-course-skill\"\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 {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n </Modal>\n );\n};\n\nexport const AuthoringPage: FC = () => {\n const { currentUser, navigateTo } = useBuildmyiq();\n const myId = currentUser?.id;\n const [status, setStatus] = useState<StatusFilter>('ALL');\n const [mineOnly, setMineOnly] = useState(false);\n const [showCreate, setShowCreate] = useState(false);\n const pager = useCursorPager();\n\n const coursesQuery = useBuiCourses({\n status: status === 'ALL' ? undefined : status,\n cursor: pager.cursor,\n limit: PAGE_SIZE,\n });\n\n const items: BuiCourse[] = useMemo(() => coursesQuery.data?.items ?? [], [coursesQuery.data]);\n const visible = useMemo(\n () => (mineOnly && myId ? items.filter((c) => c.instructorId === myId) : items),\n [items, mineOnly, myId]\n );\n\n const handleStatus = (next: StatusFilter) => {\n setStatus(next);\n pager.reset();\n };\n\n const emptyTitle = mineOnly\n ? 'No courses you author yet'\n : status === 'ALL'\n ? 'No courses yet'\n : `No ${formatEnumLabel(status).toLowerCase()} courses`;\n\n return (\n <PageLayout\n title=\"Authoring\"\n description=\"Create, curate and publish your workspace courses.\"\n titleGradient\n aurora\n actions={\n <BuiPermissionButton resource=\"buildmyiq-course\" action=\"create\" deniedBehavior=\"hide\">\n <Button onClick={() => setShowCreate(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New course\n </Button>\n </BuiPermissionButton>\n }\n >\n <div className=\"flex flex-col gap-5\">\n <GlassCard\n treatment=\"plain\"\n className=\"flex flex-col gap-3 p-3 sm:flex-row sm:items-center sm:justify-between\"\n >\n <div className=\"-mx-1 overflow-x-auto px-1\">\n <FilterTabs\n options={STATUS_TABS}\n value={status}\n onChange={handleStatus}\n className=\"border-0 bg-transparent p-0\"\n />\n </div>\n <label className=\"inline-flex shrink-0 cursor-pointer items-center gap-2 text-sm text-text-secondary\">\n <Switch checked={mineOnly} onCheckedChange={setMineOnly} disabled={!currentUser?.id} />\n Mine only\n </label>\n </GlassCard>\n\n {coursesQuery.isLoading ? (\n <LoadingView label=\"Loading courses…\" />\n ) : coursesQuery.isError ? (\n <ErrorView\n message=\"We couldn't load your courses.\"\n onRetry={() => coursesQuery.refetch()}\n />\n ) : (\n <>\n {visible.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={emptyTitle}\n description={\n mineOnly\n ? 'None of the courses on this page are authored by you — clear \"Mine only\" or page through to find them.'\n : 'Create your first course to start building curriculum, assessments and cohorts.'\n }\n action={\n <BuiPermissionButton\n resource=\"buildmyiq-course\"\n action=\"create\"\n deniedBehavior=\"hide\"\n >\n <Button onClick={() => setShowCreate(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New course\n </Button>\n </BuiPermissionButton>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4\">\n {visible.map((course) => (\n <CourseCard\n key={course.id}\n course={course}\n showStatus\n onClick={() => navigateTo(`authoring/${course.id}`)}\n />\n ))}\n </div>\n )}\n {/* Pager stays visible even when mineOnly trims the page empty, so the\n user can advance to later pages to find their courses. */}\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={visible.length}\n hasMore={coursesQuery.data?.pageInfo.hasMore ?? false}\n onPageChange={(p) => pager.onPageChange(p, coursesQuery.data?.pageInfo.cursor)}\n />\n </>\n )}\n </div>\n\n {showCreate && (\n <NewCourseModal\n onClose={() => setShowCreate(false)}\n onCreated={(course) => {\n setShowCreate(false);\n navigateTo(`authoring/${course.id}`);\n }}\n />\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CourseBuilderPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/authoring/CourseBuilderPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAa,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"CourseBuilderPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/authoring/CourseBuilderPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAa,MAAM,OAAO,CAAC;AAquB3C,eAAO,MAAM,iBAAiB,EAAE,EA+J/B,CAAC"}
|
|
@@ -126,7 +126,9 @@ var se = {
|
|
|
126
126
|
children: [
|
|
127
127
|
/* @__PURE__ */ G(E, {
|
|
128
128
|
label: "Level",
|
|
129
|
+
htmlFor: "detail-level",
|
|
129
130
|
children: /* @__PURE__ */ G(k, {
|
|
131
|
+
id: "detail-level",
|
|
130
132
|
value: f,
|
|
131
133
|
onChange: (e) => p(e),
|
|
132
134
|
options: w.map((e) => ({
|
|
@@ -158,7 +160,9 @@ var se = {
|
|
|
158
160
|
}),
|
|
159
161
|
/* @__PURE__ */ G(E, {
|
|
160
162
|
label: "Skill",
|
|
163
|
+
htmlFor: "detail-skill",
|
|
161
164
|
children: /* @__PURE__ */ G(k, {
|
|
165
|
+
id: "detail-skill",
|
|
162
166
|
value: v,
|
|
163
167
|
onChange: x,
|
|
164
168
|
options: [{
|
|
@@ -303,7 +307,9 @@ var se = {
|
|
|
303
307
|
className: "grid gap-4 sm:grid-cols-2",
|
|
304
308
|
children: [/* @__PURE__ */ G(E, {
|
|
305
309
|
label: "Type",
|
|
310
|
+
htmlFor: "lesson-type",
|
|
306
311
|
children: /* @__PURE__ */ G(k, {
|
|
312
|
+
id: "lesson-type",
|
|
307
313
|
value: c,
|
|
308
314
|
onChange: (e) => l(e),
|
|
309
315
|
options: T.map((e) => ({
|
|
@@ -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';\nimport { BuiPermissionButton } from '../../components/BuiPermissionButton';\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 <BuiPermissionButton resource=\"buildmyiq-course\" action=\"update\" deniedBehavior=\"tooltip\">\n <Button onClick={handleSave} disabled={updateCourse.isPending}>\n {updateCourse.isPending ? 'Saving…' : 'Save changes'}\n </Button>\n </BuiPermissionButton>\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 <BuiPermissionButton resource=\"buildmyiq-lesson\" action=\"create\" deniedBehavior=\"hide\">\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 </BuiPermissionButton>\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 <BuiPermissionButton resource=\"buildmyiq-lesson\" action=\"create\" deniedBehavior=\"hide\">\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 </BuiPermissionButton>\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 <BuiPermissionButton resource=\"buildmyiq-module\" action=\"create\" deniedBehavior=\"hide\">\n <Button onClick={() => setShowAddModule(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add module\n </Button>\n </BuiPermissionButton>\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 <BuiPermissionButton\n resource=\"buildmyiq-module\"\n action=\"create\"\n deniedBehavior=\"hide\"\n >\n <Button onClick={() => setShowAddModule(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add module\n </Button>\n </BuiPermissionButton>\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 <BuiPermissionButton\n resource=\"buildmyiq-module\"\n action=\"create\"\n deniedBehavior=\"hide\"\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 </BuiPermissionButton>\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 <BuiPermissionButton resource=\"buildmyiq-course\" action=\"delete\" deniedBehavior=\"hide\">\n <Button variant=\"outline\" onClick={() => setConfirmArchive(true)}>\n <Archive className=\"mr-1.5 h-4 w-4\" /> Archive\n </Button>\n </BuiPermissionButton>\n )}\n {canPublish && (\n <BuiPermissionButton resource=\"buildmyiq-course\" action=\"update\" deniedBehavior=\"hide\">\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 </BuiPermissionButton>\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
|
+
{"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';\nimport { BuiPermissionButton } from '../../components/BuiPermissionButton';\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 <BuiPermissionButton resource=\"buildmyiq-course\" action=\"update\" deniedBehavior=\"tooltip\">\n <Button onClick={handleSave} disabled={updateCourse.isPending}>\n {updateCourse.isPending ? 'Saving…' : 'Save changes'}\n </Button>\n </BuiPermissionButton>\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\" htmlFor=\"detail-level\">\n <SelectField\n id=\"detail-level\"\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\" htmlFor=\"detail-skill\">\n <SelectField\n id=\"detail-skill\"\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\" htmlFor=\"lesson-type\">\n <SelectField\n id=\"lesson-type\"\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 <BuiPermissionButton resource=\"buildmyiq-lesson\" action=\"create\" deniedBehavior=\"hide\">\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 </BuiPermissionButton>\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 <BuiPermissionButton resource=\"buildmyiq-lesson\" action=\"create\" deniedBehavior=\"hide\">\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 </BuiPermissionButton>\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 <BuiPermissionButton resource=\"buildmyiq-module\" action=\"create\" deniedBehavior=\"hide\">\n <Button onClick={() => setShowAddModule(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add module\n </Button>\n </BuiPermissionButton>\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 <BuiPermissionButton\n resource=\"buildmyiq-module\"\n action=\"create\"\n deniedBehavior=\"hide\"\n >\n <Button onClick={() => setShowAddModule(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Add module\n </Button>\n </BuiPermissionButton>\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 <BuiPermissionButton\n resource=\"buildmyiq-module\"\n action=\"create\"\n deniedBehavior=\"hide\"\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 </BuiPermissionButton>\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 <BuiPermissionButton resource=\"buildmyiq-course\" action=\"delete\" deniedBehavior=\"hide\">\n <Button variant=\"outline\" onClick={() => setConfirmArchive(true)}>\n <Archive className=\"mr-1.5 h-4 w-4\" /> Archive\n </Button>\n </BuiPermissionButton>\n )}\n {canPublish && (\n <BuiPermissionButton resource=\"buildmyiq-course\" action=\"update\" deniedBehavior=\"hide\">\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 </BuiPermissionButton>\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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"}
|
|
@@ -41,7 +41,7 @@ var h = "__none__", g = (e) => e.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-"
|
|
|
41
41
|
disabled: s,
|
|
42
42
|
children: [/* @__PURE__ */ t(p, {
|
|
43
43
|
id: e,
|
|
44
|
-
className: "w-full",
|
|
44
|
+
className: "w-full data-[placeholder]:text-text-secondary!",
|
|
45
45
|
children: /* @__PURE__ */ t(m, { placeholder: o })
|
|
46
46
|
}), /* @__PURE__ */ t(d, { children: a.map((e) => /* @__PURE__ */ t(f, {
|
|
47
47
|
value: e.value,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"_forms.js","names":[],"sources":["../../../../src/buildmyiq/pages/authoring/_forms.tsx"],"sourcesContent":["/**\n * Authoring form primitives — co-located helpers shared by AuthoringPage and\n * CourseBuilderPage only.\n * @module buildmyiq/pages/authoring\n *\n * Thin, token-only wrappers over the fe-libs ResponsiveDialog + Select primitives so the\n * authoring create/edit modals stay consistent and free of duplication. These\n * are intentionally NOT exported from the components barrel — they are private\n * to the authoring section.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport {\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n Label,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\nimport { cn } from '../../utils/cn';\n\n/** Sentinel value for an \"unset\" optional Select (radix forbids an empty string value). */\nexport const NO_SKILL = '__none__';\n\n/** Turn a free-text title into a URL-safe slug. */\nexport const slugify = (text: string): string =>\n text\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 80);\n\ninterface ModalProps {\n open: boolean;\n onClose: () => void;\n title: string;\n description?: string;\n footer?: ReactNode;\n className?: string;\n children: ReactNode;\n}\n\n/** Controlled modal built on the fe-libs ResponsiveDialog. Closes on ESC / overlay / X. */\nexport const Modal: FC<ModalProps> = ({\n open,\n onClose,\n title,\n description,\n footer,\n className,\n children,\n}) => (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n if (!next) onClose();\n }}\n >\n <ResponsiveDialogContent className={cn('max-h-[90vh] overflow-y-auto', className)}>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{title}</ResponsiveDialogTitle>\n {description && <ResponsiveDialogDescription>{description}</ResponsiveDialogDescription>}\n </ResponsiveDialogHeader>\n {children}\n {footer && <ResponsiveDialogFooter>{footer}</ResponsiveDialogFooter>}\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n);\n\ninterface FieldProps {\n label: string;\n htmlFor?: string;\n required?: boolean;\n hint?: string;\n error?: string;\n className?: string;\n children: ReactNode;\n}\n\n/** Label + control + hint/error wrapper for form rows. */\nexport const Field: FC<FieldProps> = ({\n label,\n htmlFor,\n required,\n hint,\n error,\n className,\n children,\n}) => (\n <div className={cn('grid gap-1.5', className)}>\n <Label htmlFor={htmlFor}>\n {label}\n {required && <span className=\"text-status-error-text\"> *</span>}\n </Label>\n {children}\n {hint && !error && <p className=\"text-xs text-text-tertiary\">{hint}</p>}\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n </div>\n);\n\nexport interface SelectOption {\n value: string;\n label: string;\n}\n\ninterface SelectFieldProps {\n id?: string;\n value: string;\n onChange: (value: string) => void;\n options: SelectOption[];\n placeholder?: string;\n disabled?: boolean;\n}\n\n/** A single-select control over the fe-libs Select primitive. */\nexport const SelectField: FC<SelectFieldProps> = ({\n id,\n value,\n onChange,\n options,\n placeholder,\n disabled,\n}) => (\n <Select value={value} onValueChange={onChange} disabled={disabled}>\n <SelectTrigger id={id} className=\"w-full
|
|
1
|
+
{"version":3,"file":"_forms.js","names":[],"sources":["../../../../src/buildmyiq/pages/authoring/_forms.tsx"],"sourcesContent":["/**\n * Authoring form primitives — co-located helpers shared by AuthoringPage and\n * CourseBuilderPage only.\n * @module buildmyiq/pages/authoring\n *\n * Thin, token-only wrappers over the fe-libs ResponsiveDialog + Select primitives so the\n * authoring create/edit modals stay consistent and free of duplication. These\n * are intentionally NOT exported from the components barrel — they are private\n * to the authoring section.\n */\n\nimport type { FC, ReactNode } from 'react';\nimport {\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n Label,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@burdenoff/fe-libs/ui';\nimport { cn } from '../../utils/cn';\n\n/** Sentinel value for an \"unset\" optional Select (radix forbids an empty string value). */\nexport const NO_SKILL = '__none__';\n\n/** Turn a free-text title into a URL-safe slug. */\nexport const slugify = (text: string): string =>\n text\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 80);\n\ninterface ModalProps {\n open: boolean;\n onClose: () => void;\n title: string;\n description?: string;\n footer?: ReactNode;\n className?: string;\n children: ReactNode;\n}\n\n/** Controlled modal built on the fe-libs ResponsiveDialog. Closes on ESC / overlay / X. */\nexport const Modal: FC<ModalProps> = ({\n open,\n onClose,\n title,\n description,\n footer,\n className,\n children,\n}) => (\n <ResponsiveDialog\n open={open}\n onOpenChange={(next) => {\n if (!next) onClose();\n }}\n >\n <ResponsiveDialogContent className={cn('max-h-[90vh] overflow-y-auto', className)}>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>{title}</ResponsiveDialogTitle>\n {description && <ResponsiveDialogDescription>{description}</ResponsiveDialogDescription>}\n </ResponsiveDialogHeader>\n {children}\n {footer && <ResponsiveDialogFooter>{footer}</ResponsiveDialogFooter>}\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n);\n\ninterface FieldProps {\n label: string;\n htmlFor?: string;\n required?: boolean;\n hint?: string;\n error?: string;\n className?: string;\n children: ReactNode;\n}\n\n/** Label + control + hint/error wrapper for form rows. */\nexport const Field: FC<FieldProps> = ({\n label,\n htmlFor,\n required,\n hint,\n error,\n className,\n children,\n}) => (\n <div className={cn('grid gap-1.5', className)}>\n <Label htmlFor={htmlFor}>\n {label}\n {required && <span className=\"text-status-error-text\"> *</span>}\n </Label>\n {children}\n {hint && !error && <p className=\"text-xs text-text-tertiary\">{hint}</p>}\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n </div>\n);\n\nexport interface SelectOption {\n value: string;\n label: string;\n}\n\ninterface SelectFieldProps {\n id?: string;\n value: string;\n onChange: (value: string) => void;\n options: SelectOption[];\n placeholder?: string;\n disabled?: boolean;\n}\n\n/** A single-select control over the fe-libs Select primitive. */\nexport const SelectField: FC<SelectFieldProps> = ({\n id,\n value,\n onChange,\n options,\n placeholder,\n disabled,\n}) => (\n <Select value={value} onValueChange={onChange} disabled={disabled}>\n <SelectTrigger id={id} className=\"w-full data-[placeholder]:text-text-secondary!\">\n <SelectValue placeholder={placeholder} />\n </SelectTrigger>\n <SelectContent>\n {options.map((opt) => (\n <SelectItem key={opt.value} value={opt.value}>\n {opt.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n);\n"],"mappings":";;;;AA6BA,IAAa,IAAW,YAGX,KAAW,MACtB,EACG,YAAY,EACZ,KAAK,EACL,QAAQ,eAAe,GAAG,EAC1B,QAAQ,YAAY,EAAE,EACtB,MAAM,GAAG,EAAE,GAaH,KAAyB,EACpC,SACA,YACA,UACA,gBACA,WACA,cACA,kBAEA,kBAAC,GAAD;CACQ;CACN,eAAe,MAAS;EACtB,AAAK,KAAM,EAAQ;CACrB;WAEA,kBAAC,GAAD;EAAyB,WAAW,EAAG,gCAAgC,CAAS;YAAhF;GACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAwB,EAA6B,CAAA,GACpD,KAAe,kBAAC,GAAD,EAAA,UAA8B,EAAyC,CAAA,CACjE,EAAA,CAAA;GACvB;GACA,KAAU,kBAAC,GAAD,EAAA,UAAyB,EAA+B,CAAA;EAC5C;;AACT,CAAA,GAcP,KAAyB,EACpC,UACA,YACA,aACA,SACA,UACA,cACA,kBAEA,kBAAC,OAAD;CAAK,WAAW,EAAG,gBAAgB,CAAS;WAA5C;EACE,kBAAC,GAAD;GAAgB;aAAhB,CACG,GACA,KAAY,kBAAC,QAAD;IAAM,WAAU;cAAyB;GAAQ,CAAA,CACzD;;EACN;EACA,KAAQ,CAAC,KAAS,kBAAC,KAAD;GAAG,WAAU;aAA8B;EAAQ,CAAA;EACrE,KAAS,kBAAC,KAAD;GAAG,WAAU;aAAkC;EAAS,CAAA;CAC/D;IAkBM,KAAqC,EAChD,OACA,UACA,aACA,YACA,gBACA,kBAEA,kBAAC,GAAD;CAAe;CAAO,eAAe;CAAoB;WAAzD,CACE,kBAAC,GAAD;EAAmB;EAAI,WAAU;YAC/B,kBAAC,GAAD,EAA0B,eAAc,CAAA;CAC3B,CAAA,GACf,kBAAC,GAAD,EAAA,UACG,EAAQ,KAAK,MACZ,kBAAC,GAAD;EAA4B,OAAO,EAAI;YACpC,EAAI;CACK,GAFK,EAAI,KAET,CACb,EACY,CAAA,CACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CertificatesPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/certificates/CertificatesPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAqB,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAoEnD,eAAO,MAAM,gBAAgB,EAAE,
|
|
1
|
+
{"version":3,"file":"CertificatesPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/certificates/CertificatesPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAqB,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAoEnD,eAAO,MAAM,gBAAgB,EAAE,EA+f9B,CAAC"}
|
|
@@ -367,6 +367,7 @@ var S = 8, Ce = (e) => {
|
|
|
367
367
|
})),
|
|
368
368
|
children: [/* @__PURE__ */ f(b, {
|
|
369
369
|
id: "cert-course",
|
|
370
|
+
className: "data-[placeholder]:text-text-secondary!",
|
|
370
371
|
children: /* @__PURE__ */ f(x, { placeholder: "Select a course" })
|
|
371
372
|
}), /* @__PURE__ */ f(v, { children: V.map((e) => /* @__PURE__ */ f(y, {
|
|
372
373
|
value: e.id,
|
|
@@ -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';\nimport { BuiPermissionButton } from '../../components/BuiPermissionButton';\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 <BuiPermissionButton\n resource=\"buildmyiq-certificate\"\n action=\"update\"\n deniedBehavior=\"hide\"\n >\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 </BuiPermissionButton>\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 <BuiPermissionButton resource=\"buildmyiq-certificate\" action=\"create\" deniedBehavior=\"hide\">\n <Button onClick={openDialog} disabled={!hasCourses}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Issue certificate\n </Button>\n </BuiPermissionButton>\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 <BuiPermissionButton\n resource=\"buildmyiq-certificate\"\n action=\"create\"\n deniedBehavior=\"hide\"\n >\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Issue certificate\n </Button>\n </BuiPermissionButton>\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';\nimport { BuiPermissionButton } from '../../components/BuiPermissionButton';\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 <BuiPermissionButton\n resource=\"buildmyiq-certificate\"\n action=\"update\"\n deniedBehavior=\"hide\"\n >\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 </BuiPermissionButton>\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 <BuiPermissionButton resource=\"buildmyiq-certificate\" action=\"create\" deniedBehavior=\"hide\">\n <Button onClick={openDialog} disabled={!hasCourses}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Issue certificate\n </Button>\n </BuiPermissionButton>\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 <BuiPermissionButton\n resource=\"buildmyiq-certificate\"\n action=\"create\"\n deniedBehavior=\"hide\"\n >\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Issue certificate\n </Button>\n </BuiPermissionButton>\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\n id=\"cert-course\"\n className=\"data-[placeholder]:text-text-secondary!\"\n >\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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CohortsPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/cohorts/CohortsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAqB,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AA0EnD,eAAO,MAAM,WAAW,EAAE,
|
|
1
|
+
{"version":3,"file":"CohortsPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/cohorts/CohortsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAqB,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AA0EnD,eAAO,MAAM,WAAW,EAAE,EAubzB,CAAC"}
|
|
@@ -316,6 +316,7 @@ var D = (e) => {
|
|
|
316
316
|
})),
|
|
317
317
|
children: [/* @__PURE__ */ _(T, {
|
|
318
318
|
id: "cohort-course",
|
|
319
|
+
className: "data-[placeholder]:text-text-secondary!",
|
|
319
320
|
children: /* @__PURE__ */ _(E, { placeholder: "Select a course" })
|
|
320
321
|
}), /* @__PURE__ */ _(C, { children: U.map((e) => /* @__PURE__ */ _(w, {
|
|
321
322
|
value: e.id,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CohortsPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/cohorts/CohortsPage.tsx"],"sourcesContent":["/**\n * CohortsPage — scheduled learning cohorts for the workspace.\n * @module buildmyiq/pages/cohorts\n *\n * The primary surface of the Cohorts section. Lists real `BuiCohort` rows for\n * the workspace (optionally scoped to a course), lets facilitators filter by\n * lifecycle status, and create a new cohort. Every value is derived from live\n * `bui*` data — enrolled counts are NOT queryable per-cohort, so we only ever\n * surface the declared `capacity` (never a fabricated enrolment number).\n *\n * Reads: `buiListCohorts` (useBuiCohorts), `buiListCourses` (useBuiCourses).\n * Writes: `buiCreateCohort` (useBuiCreateCohort).\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { Plus, Users, CalendarRange, PlayCircle, UserPlus, 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} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiCohorts, useBuiCourses, useBuiCreateCohort } from '../../hooks';\nimport { formatDate, formatEnumLabel, formatNumber } from '../../utils/formatters';\nimport { COHORT_STATUSES, type BuiCohort, type BuiCohortStatus } from '../../types';\nimport { BuiPermissionButton } from '../../components/BuiPermissionButton';\n\ntype StatusFilter = BuiCohortStatus | 'ALL';\ntype CourseFilter = string | 'ALL';\n\n/** Convert a native `<input type=\"date\">` value (YYYY-MM-DD) to a DateTime, or undefined. */\nconst toDateTime = (value: string): string | undefined => {\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n const d = new Date(trimmed);\n return Number.isNaN(d.getTime()) ? undefined : d.toISOString();\n};\n\n/** Status-tinted icon-tile classes (semantic tokens only) for the cohort cell. */\nconst COHORT_TILE: Record<BuiCohortStatus, string> = {\n SCHEDULED: 'bg-bg-sunken text-text-secondary',\n ENROLLING: 'bg-status-warning-bg-subtle text-status-warning-text',\n ACTIVE: 'bg-status-success-bg-subtle text-status-success-text',\n COMPLETED: 'bg-[var(--color-accent-soft)] text-[var(--color-accent-primary)]',\n CANCELLED: 'bg-status-error-bg-subtle text-status-error-text',\n};\n\n/** Whole-week span between two ISO dates, or null when either is missing/invalid. */\nconst weeksBetween = (start?: string | null, end?: string | null): number | null => {\n if (!start || !end) return null;\n const s = new Date(start).getTime();\n const e = new Date(end).getTime();\n if (Number.isNaN(s) || Number.isNaN(e) || e < s) return null;\n return Math.max(1, Math.round((e - s) / (7 * 24 * 60 * 60 * 1000)));\n};\n\ninterface CreateFormState {\n courseId: string;\n name: string;\n capacity: string;\n facilitatorId: string;\n startDate: string;\n endDate: string;\n}\n\nexport const CohortsPage: FC = () => {\n const { currentUser } = useBuildmyiq();\n\n const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL');\n const [courseFilter, setCourseFilter] = useState<CourseFilter>('ALL');\n const [dialogOpen, setDialogOpen] = useState(false);\n const [formError, setFormError] = useState<string | null>(null);\n\n const emptyForm = useMemo<CreateFormState>(\n () => ({\n courseId: '',\n name: '',\n capacity: '',\n facilitatorId: currentUser?.id ?? '',\n startDate: '',\n endDate: '',\n }),\n [currentUser?.id]\n );\n const [form, setForm] = useState<CreateFormState>(emptyForm);\n\n // Course lookup: powers the course filter, the create-form course picker, and\n // the courseId → title mapping in the table.\n const coursesQuery = useBuiCourses({ limit: 100 });\n const courses = useMemo(() => coursesQuery.data?.items ?? [], [coursesQuery.data]);\n const courseTitleById = useMemo(() => {\n const map = new Map<string, string>();\n for (const c of courses) map.set(c.id, c.title);\n return map;\n }, [courses]);\n\n // Fetch cohorts scoped only by course — status is filtered client-side so the\n // status tabs can carry accurate per-status counts from a single fetch.\n const cohortsQuery = useBuiCohorts(courseFilter === 'ALL' ? undefined : courseFilter);\n const createCohort = useBuiCreateCohort();\n\n const allCohorts = useMemo(() => cohortsQuery.data ?? [], [cohortsQuery.data]);\n\n const statusCounts = useMemo(() => {\n const counts: Record<string, number> = {};\n for (const c of allCohorts) counts[c.status] = (counts[c.status] ?? 0) + 1;\n return counts;\n }, [allCohorts]);\n\n // KPI strip — honest derived counts across the current course scope.\n const summary = useMemo(\n () => ({\n total: allCohorts.length,\n active: statusCounts.ACTIVE ?? 0,\n enrolling: statusCounts.ENROLLING ?? 0,\n scheduled: statusCounts.SCHEDULED ?? 0,\n seats: allCohorts.reduce((sum, c) => sum + (c.capacity ?? 0), 0),\n }),\n [allCohorts, statusCounts]\n );\n\n const rows = useMemo(\n () =>\n statusFilter === 'ALL' ? allCohorts : allCohorts.filter((c) => c.status === statusFilter),\n [allCohorts, statusFilter]\n );\n\n const statusOptions: FilterOption<StatusFilter>[] = useMemo(\n () => [\n { value: 'ALL', label: 'All', count: allCohorts.length },\n ...COHORT_STATUSES.map((s) => ({\n value: s,\n label: formatEnumLabel(s),\n count: statusCounts[s] ?? 0,\n })),\n ],\n [allCohorts.length, statusCounts]\n );\n\n const columns: Column<BuiCohort>[] = useMemo(\n () => [\n {\n key: 'name',\n header: 'Cohort',\n sortValue: (c) => c.name.toLowerCase(),\n render: (c) => (\n <div className=\"flex items-center gap-3\">\n <div\n className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${COHORT_TILE[c.status]}`}\n >\n <Users className=\"h-4 w-4\" />\n </div>\n <div className=\"min-w-0\">\n <span className=\"block truncate font-semibold text-text-primary\">{c.name}</span>\n {/* Course is a dedicated column ≥ md; surface it inline on mobile. */}\n <span className=\"block truncate text-xs text-text-tertiary md:hidden\">\n {courseTitleById.get(c.courseId) ?? '—'}\n </span>\n </div>\n </div>\n ),\n },\n {\n key: 'course',\n header: 'Course',\n hideOnMobile: true,\n sortValue: (c) => (courseTitleById.get(c.courseId) ?? '').toLowerCase(),\n render: (c) => (\n <span className=\"text-text-secondary\">{courseTitleById.get(c.courseId) ?? '—'}</span>\n ),\n },\n {\n key: 'capacity',\n header: 'Capacity',\n align: 'right',\n sortValue: (c) => c.capacity,\n render: (c) => (\n <span className=\"tabular-nums text-text-secondary\">\n {formatNumber(c.capacity ?? 0)}\n <span className=\"ml-1 text-text-tertiary\">seats</span>\n </span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n render: (c) => <StatusBadge status={c.status} />,\n },\n {\n key: 'startDate',\n header: 'Starts',\n align: 'right',\n hideOnMobile: true,\n sortValue: (c) => c.startDate ?? '',\n render: (c) => <span className=\"text-text-tertiary\">{formatDate(c.startDate)}</span>,\n },\n {\n key: 'endDate',\n header: 'Ends',\n align: 'right',\n hideOnMobile: true,\n sortValue: (c) => c.endDate ?? '',\n render: (c) => {\n const weeks = weeksBetween(c.startDate, c.endDate);\n return (\n <div className=\"text-right\">\n <span className=\"text-text-tertiary\">{formatDate(c.endDate)}</span>\n {weeks !== null && (\n <span className=\"block text-xs text-text-tertiary/70\">\n {weeks} {weeks === 1 ? 'week' : 'weeks'}\n </span>\n )}\n </div>\n );\n },\n },\n ],\n [courseTitleById]\n );\n\n const openDialog = () => {\n setForm(emptyForm);\n setFormError(null);\n setDialogOpen(true);\n };\n\n const closeDialog = () => {\n setDialogOpen(false);\n setForm(emptyForm);\n setFormError(null);\n };\n\n const handleCreate = async () => {\n if (!form.courseId) {\n setFormError('Please choose a course for this cohort.');\n return;\n }\n if (!form.name.trim()) {\n setFormError('Cohort name is required.');\n return;\n }\n const capacityNum = form.capacity.trim() ? Number(form.capacity) : undefined;\n if (capacityNum !== undefined && (!Number.isFinite(capacityNum) || capacityNum < 0)) {\n setFormError('Capacity must be a non-negative number.');\n return;\n }\n const startIso = toDateTime(form.startDate);\n const endIso = toDateTime(form.endDate);\n if (startIso && endIso && new Date(endIso) < new Date(startIso)) {\n setFormError('End date cannot be before the start date.');\n return;\n }\n\n setFormError(null);\n try {\n await createCohort.mutateAsync({\n courseId: form.courseId,\n name: form.name.trim(),\n capacity: capacityNum,\n facilitatorId: form.facilitatorId.trim() || undefined,\n startDate: startIso,\n endDate: endIso,\n });\n closeDialog();\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to create cohort.');\n }\n };\n\n const hasCourses = courses.length > 0;\n const showSummary = !cohortsQuery.isLoading && !cohortsQuery.isError && summary.total > 0;\n\n return (\n <PageLayout\n title=\"Cohorts\"\n description=\"Scheduled, facilitator-led groups that move through a course together.\"\n titleGradient\n aurora\n actions={\n <BuiPermissionButton resource=\"buildmyiq-cohort\" action=\"create\" deniedBehavior=\"hide\">\n <Button onClick={openDialog} disabled={!hasCourses}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New cohort\n </Button>\n </BuiPermissionButton>\n }\n >\n {showSummary && (\n <div className=\"mb-5 grid grid-cols-2 gap-4 sm:grid-cols-4\">\n <MetricsCard\n icon={<Users className=\"h-5 w-5\" />}\n value={summary.total}\n label=\"Cohorts\"\n featured\n footer={\n <p className=\"text-xs text-text-tertiary\">\n {formatNumber(summary.seats)} seats scheduled\n </p>\n }\n />\n <MetricsCard\n icon={<PlayCircle className=\"h-5 w-5\" />}\n value={summary.active}\n label=\"Active\"\n accent=\"success\"\n />\n <MetricsCard\n icon={<UserPlus className=\"h-5 w-5\" />}\n value={summary.enrolling}\n label=\"Enrolling\"\n accent=\"warning\"\n />\n <MetricsCard\n icon={<CalendarClock className=\"h-5 w-5\" />}\n value={summary.scheduled}\n label=\"Scheduled\"\n />\n </div>\n )}\n\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <FilterTabs options={statusOptions} value={statusFilter} onChange={setStatusFilter} />\n <Select value={courseFilter} onValueChange={(v) => setCourseFilter(v as CourseFilter)}>\n <SelectTrigger className=\"w-56\" aria-label=\"Filter by course\">\n <SelectValue placeholder=\"All courses\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All courses</SelectItem>\n {courses.map((c) => (\n <SelectItem key={c.id} value={c.id}>\n {c.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {cohortsQuery.isLoading ? (\n <LoadingView label=\"Loading cohorts…\" />\n ) : cohortsQuery.isError ? (\n <ErrorView message=\"Couldn't load cohorts.\" onRetry={() => cohortsQuery.refetch()} />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration={\n statusFilter === 'ALL' && courseFilter === 'ALL' ? 'onboarding' : 'empty-search'\n }\n icon={<CalendarRange className=\"h-10 w-10\" />}\n title={\n statusFilter === 'ALL' && courseFilter === 'ALL'\n ? 'No cohorts yet'\n : 'No cohorts match these filters'\n }\n description={\n statusFilter === 'ALL' && courseFilter === 'ALL'\n ? hasCourses\n ? 'Schedule a cohort to run a course with a group of learners on a shared timeline.'\n : 'Publish a course first, then schedule a cohort to run it with a group of learners.'\n : 'Try a different status or course to see more cohorts.'\n }\n action={\n statusFilter === 'ALL' && courseFilter === 'ALL' && hasCourses ? (\n <BuiPermissionButton\n resource=\"buildmyiq-cohort\"\n action=\"create\"\n deniedBehavior=\"hide\"\n >\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New cohort\n </Button>\n </BuiPermissionButton>\n ) : undefined\n }\n />\n ) : (\n <>\n <DataTable\n columns={columns}\n rows={rows}\n rowKey={(c) => c.id}\n defaultSort={{ key: 'startDate', dir: 'desc' }}\n />\n <p className=\"mt-4 text-center text-sm text-text-tertiary tabular-nums\">\n Showing {rows.length} of {allCohorts.length}{' '}\n {allCohorts.length === 1 ? 'cohort' : 'cohorts'}\n </p>\n </>\n )}\n\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n if (!open) closeDialog();\n else setDialogOpen(true);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New cohort</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Schedule a facilitator-led group to run a course together.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <form\n onSubmit={(e) => {\n e.preventDefault();\n void handleCreate();\n }}\n >\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-course\">Course *</Label>\n <Select\n value={form.courseId}\n onValueChange={(v) => setForm((prev) => ({ ...prev, courseId: v }))}\n >\n <SelectTrigger id=\"cohort-course\">\n <SelectValue placeholder=\"Select a course\" />\n </SelectTrigger>\n <SelectContent>\n {courses.map((c) => (\n <SelectItem key={c.id} value={c.id}>\n {c.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-name\">Name *</Label>\n <Input\n id=\"cohort-name\"\n value={form.name}\n onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}\n placeholder=\"Spring 2026 Cohort\"\n />\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-capacity\">Capacity</Label>\n <Input\n id=\"cohort-capacity\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={form.capacity}\n onChange={(e) => setForm((prev) => ({ ...prev, capacity: e.target.value }))}\n placeholder=\"25\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-facilitator\">Facilitator (user ID)</Label>\n <Input\n id=\"cohort-facilitator\"\n value={form.facilitatorId}\n onChange={(e) =>\n setForm((prev) => ({ ...prev, facilitatorId: e.target.value }))\n }\n placeholder=\"Defaults to you\"\n />\n </div>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-start\">Start date</Label>\n <Input\n id=\"cohort-start\"\n type=\"date\"\n value={form.startDate}\n onChange={(e) => setForm((prev) => ({ ...prev, startDate: e.target.value }))}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-end\">End date</Label>\n <Input\n id=\"cohort-end\"\n type=\"date\"\n value={form.endDate}\n onChange={(e) => setForm((prev) => ({ ...prev, endDate: e.target.value }))}\n />\n </div>\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={closeDialog}>\n Cancel\n </Button>\n <Button type=\"submit\" disabled={createCohort.isPending}>\n {createCohort.isPending ? 'Creating…' : 'Create cohort'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CohortsPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/cohorts/CohortsPage.tsx"],"sourcesContent":["/**\n * CohortsPage — scheduled learning cohorts for the workspace.\n * @module buildmyiq/pages/cohorts\n *\n * The primary surface of the Cohorts section. Lists real `BuiCohort` rows for\n * the workspace (optionally scoped to a course), lets facilitators filter by\n * lifecycle status, and create a new cohort. Every value is derived from live\n * `bui*` data — enrolled counts are NOT queryable per-cohort, so we only ever\n * surface the declared `capacity` (never a fabricated enrolment number).\n *\n * Reads: `buiListCohorts` (useBuiCohorts), `buiListCourses` (useBuiCourses).\n * Writes: `buiCreateCohort` (useBuiCreateCohort).\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { Plus, Users, CalendarRange, PlayCircle, UserPlus, 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} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiCohorts, useBuiCourses, useBuiCreateCohort } from '../../hooks';\nimport { formatDate, formatEnumLabel, formatNumber } from '../../utils/formatters';\nimport { COHORT_STATUSES, type BuiCohort, type BuiCohortStatus } from '../../types';\nimport { BuiPermissionButton } from '../../components/BuiPermissionButton';\n\ntype StatusFilter = BuiCohortStatus | 'ALL';\ntype CourseFilter = string | 'ALL';\n\n/** Convert a native `<input type=\"date\">` value (YYYY-MM-DD) to a DateTime, or undefined. */\nconst toDateTime = (value: string): string | undefined => {\n const trimmed = value.trim();\n if (!trimmed) return undefined;\n const d = new Date(trimmed);\n return Number.isNaN(d.getTime()) ? undefined : d.toISOString();\n};\n\n/** Status-tinted icon-tile classes (semantic tokens only) for the cohort cell. */\nconst COHORT_TILE: Record<BuiCohortStatus, string> = {\n SCHEDULED: 'bg-bg-sunken text-text-secondary',\n ENROLLING: 'bg-status-warning-bg-subtle text-status-warning-text',\n ACTIVE: 'bg-status-success-bg-subtle text-status-success-text',\n COMPLETED: 'bg-[var(--color-accent-soft)] text-[var(--color-accent-primary)]',\n CANCELLED: 'bg-status-error-bg-subtle text-status-error-text',\n};\n\n/** Whole-week span between two ISO dates, or null when either is missing/invalid. */\nconst weeksBetween = (start?: string | null, end?: string | null): number | null => {\n if (!start || !end) return null;\n const s = new Date(start).getTime();\n const e = new Date(end).getTime();\n if (Number.isNaN(s) || Number.isNaN(e) || e < s) return null;\n return Math.max(1, Math.round((e - s) / (7 * 24 * 60 * 60 * 1000)));\n};\n\ninterface CreateFormState {\n courseId: string;\n name: string;\n capacity: string;\n facilitatorId: string;\n startDate: string;\n endDate: string;\n}\n\nexport const CohortsPage: FC = () => {\n const { currentUser } = useBuildmyiq();\n\n const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL');\n const [courseFilter, setCourseFilter] = useState<CourseFilter>('ALL');\n const [dialogOpen, setDialogOpen] = useState(false);\n const [formError, setFormError] = useState<string | null>(null);\n\n const emptyForm = useMemo<CreateFormState>(\n () => ({\n courseId: '',\n name: '',\n capacity: '',\n facilitatorId: currentUser?.id ?? '',\n startDate: '',\n endDate: '',\n }),\n [currentUser?.id]\n );\n const [form, setForm] = useState<CreateFormState>(emptyForm);\n\n // Course lookup: powers the course filter, the create-form course picker, and\n // the courseId → title mapping in the table.\n const coursesQuery = useBuiCourses({ limit: 100 });\n const courses = useMemo(() => coursesQuery.data?.items ?? [], [coursesQuery.data]);\n const courseTitleById = useMemo(() => {\n const map = new Map<string, string>();\n for (const c of courses) map.set(c.id, c.title);\n return map;\n }, [courses]);\n\n // Fetch cohorts scoped only by course — status is filtered client-side so the\n // status tabs can carry accurate per-status counts from a single fetch.\n const cohortsQuery = useBuiCohorts(courseFilter === 'ALL' ? undefined : courseFilter);\n const createCohort = useBuiCreateCohort();\n\n const allCohorts = useMemo(() => cohortsQuery.data ?? [], [cohortsQuery.data]);\n\n const statusCounts = useMemo(() => {\n const counts: Record<string, number> = {};\n for (const c of allCohorts) counts[c.status] = (counts[c.status] ?? 0) + 1;\n return counts;\n }, [allCohorts]);\n\n // KPI strip — honest derived counts across the current course scope.\n const summary = useMemo(\n () => ({\n total: allCohorts.length,\n active: statusCounts.ACTIVE ?? 0,\n enrolling: statusCounts.ENROLLING ?? 0,\n scheduled: statusCounts.SCHEDULED ?? 0,\n seats: allCohorts.reduce((sum, c) => sum + (c.capacity ?? 0), 0),\n }),\n [allCohorts, statusCounts]\n );\n\n const rows = useMemo(\n () =>\n statusFilter === 'ALL' ? allCohorts : allCohorts.filter((c) => c.status === statusFilter),\n [allCohorts, statusFilter]\n );\n\n const statusOptions: FilterOption<StatusFilter>[] = useMemo(\n () => [\n { value: 'ALL', label: 'All', count: allCohorts.length },\n ...COHORT_STATUSES.map((s) => ({\n value: s,\n label: formatEnumLabel(s),\n count: statusCounts[s] ?? 0,\n })),\n ],\n [allCohorts.length, statusCounts]\n );\n\n const columns: Column<BuiCohort>[] = useMemo(\n () => [\n {\n key: 'name',\n header: 'Cohort',\n sortValue: (c) => c.name.toLowerCase(),\n render: (c) => (\n <div className=\"flex items-center gap-3\">\n <div\n className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${COHORT_TILE[c.status]}`}\n >\n <Users className=\"h-4 w-4\" />\n </div>\n <div className=\"min-w-0\">\n <span className=\"block truncate font-semibold text-text-primary\">{c.name}</span>\n {/* Course is a dedicated column ≥ md; surface it inline on mobile. */}\n <span className=\"block truncate text-xs text-text-tertiary md:hidden\">\n {courseTitleById.get(c.courseId) ?? '—'}\n </span>\n </div>\n </div>\n ),\n },\n {\n key: 'course',\n header: 'Course',\n hideOnMobile: true,\n sortValue: (c) => (courseTitleById.get(c.courseId) ?? '').toLowerCase(),\n render: (c) => (\n <span className=\"text-text-secondary\">{courseTitleById.get(c.courseId) ?? '—'}</span>\n ),\n },\n {\n key: 'capacity',\n header: 'Capacity',\n align: 'right',\n sortValue: (c) => c.capacity,\n render: (c) => (\n <span className=\"tabular-nums text-text-secondary\">\n {formatNumber(c.capacity ?? 0)}\n <span className=\"ml-1 text-text-tertiary\">seats</span>\n </span>\n ),\n },\n {\n key: 'status',\n header: 'Status',\n render: (c) => <StatusBadge status={c.status} />,\n },\n {\n key: 'startDate',\n header: 'Starts',\n align: 'right',\n hideOnMobile: true,\n sortValue: (c) => c.startDate ?? '',\n render: (c) => <span className=\"text-text-tertiary\">{formatDate(c.startDate)}</span>,\n },\n {\n key: 'endDate',\n header: 'Ends',\n align: 'right',\n hideOnMobile: true,\n sortValue: (c) => c.endDate ?? '',\n render: (c) => {\n const weeks = weeksBetween(c.startDate, c.endDate);\n return (\n <div className=\"text-right\">\n <span className=\"text-text-tertiary\">{formatDate(c.endDate)}</span>\n {weeks !== null && (\n <span className=\"block text-xs text-text-tertiary/70\">\n {weeks} {weeks === 1 ? 'week' : 'weeks'}\n </span>\n )}\n </div>\n );\n },\n },\n ],\n [courseTitleById]\n );\n\n const openDialog = () => {\n setForm(emptyForm);\n setFormError(null);\n setDialogOpen(true);\n };\n\n const closeDialog = () => {\n setDialogOpen(false);\n setForm(emptyForm);\n setFormError(null);\n };\n\n const handleCreate = async () => {\n if (!form.courseId) {\n setFormError('Please choose a course for this cohort.');\n return;\n }\n if (!form.name.trim()) {\n setFormError('Cohort name is required.');\n return;\n }\n const capacityNum = form.capacity.trim() ? Number(form.capacity) : undefined;\n if (capacityNum !== undefined && (!Number.isFinite(capacityNum) || capacityNum < 0)) {\n setFormError('Capacity must be a non-negative number.');\n return;\n }\n const startIso = toDateTime(form.startDate);\n const endIso = toDateTime(form.endDate);\n if (startIso && endIso && new Date(endIso) < new Date(startIso)) {\n setFormError('End date cannot be before the start date.');\n return;\n }\n\n setFormError(null);\n try {\n await createCohort.mutateAsync({\n courseId: form.courseId,\n name: form.name.trim(),\n capacity: capacityNum,\n facilitatorId: form.facilitatorId.trim() || undefined,\n startDate: startIso,\n endDate: endIso,\n });\n closeDialog();\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to create cohort.');\n }\n };\n\n const hasCourses = courses.length > 0;\n const showSummary = !cohortsQuery.isLoading && !cohortsQuery.isError && summary.total > 0;\n\n return (\n <PageLayout\n title=\"Cohorts\"\n description=\"Scheduled, facilitator-led groups that move through a course together.\"\n titleGradient\n aurora\n actions={\n <BuiPermissionButton resource=\"buildmyiq-cohort\" action=\"create\" deniedBehavior=\"hide\">\n <Button onClick={openDialog} disabled={!hasCourses}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New cohort\n </Button>\n </BuiPermissionButton>\n }\n >\n {showSummary && (\n <div className=\"mb-5 grid grid-cols-2 gap-4 sm:grid-cols-4\">\n <MetricsCard\n icon={<Users className=\"h-5 w-5\" />}\n value={summary.total}\n label=\"Cohorts\"\n featured\n footer={\n <p className=\"text-xs text-text-tertiary\">\n {formatNumber(summary.seats)} seats scheduled\n </p>\n }\n />\n <MetricsCard\n icon={<PlayCircle className=\"h-5 w-5\" />}\n value={summary.active}\n label=\"Active\"\n accent=\"success\"\n />\n <MetricsCard\n icon={<UserPlus className=\"h-5 w-5\" />}\n value={summary.enrolling}\n label=\"Enrolling\"\n accent=\"warning\"\n />\n <MetricsCard\n icon={<CalendarClock className=\"h-5 w-5\" />}\n value={summary.scheduled}\n label=\"Scheduled\"\n />\n </div>\n )}\n\n <div className=\"mb-5 flex flex-wrap items-center gap-3\">\n <FilterTabs options={statusOptions} value={statusFilter} onChange={setStatusFilter} />\n <Select value={courseFilter} onValueChange={(v) => setCourseFilter(v as CourseFilter)}>\n <SelectTrigger className=\"w-56\" aria-label=\"Filter by course\">\n <SelectValue placeholder=\"All courses\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"ALL\">All courses</SelectItem>\n {courses.map((c) => (\n <SelectItem key={c.id} value={c.id}>\n {c.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n {cohortsQuery.isLoading ? (\n <LoadingView label=\"Loading cohorts…\" />\n ) : cohortsQuery.isError ? (\n <ErrorView message=\"Couldn't load cohorts.\" onRetry={() => cohortsQuery.refetch()} />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration={\n statusFilter === 'ALL' && courseFilter === 'ALL' ? 'onboarding' : 'empty-search'\n }\n icon={<CalendarRange className=\"h-10 w-10\" />}\n title={\n statusFilter === 'ALL' && courseFilter === 'ALL'\n ? 'No cohorts yet'\n : 'No cohorts match these filters'\n }\n description={\n statusFilter === 'ALL' && courseFilter === 'ALL'\n ? hasCourses\n ? 'Schedule a cohort to run a course with a group of learners on a shared timeline.'\n : 'Publish a course first, then schedule a cohort to run it with a group of learners.'\n : 'Try a different status or course to see more cohorts.'\n }\n action={\n statusFilter === 'ALL' && courseFilter === 'ALL' && hasCourses ? (\n <BuiPermissionButton\n resource=\"buildmyiq-cohort\"\n action=\"create\"\n deniedBehavior=\"hide\"\n >\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New cohort\n </Button>\n </BuiPermissionButton>\n ) : undefined\n }\n />\n ) : (\n <>\n <DataTable\n columns={columns}\n rows={rows}\n rowKey={(c) => c.id}\n defaultSort={{ key: 'startDate', dir: 'desc' }}\n />\n <p className=\"mt-4 text-center text-sm text-text-tertiary tabular-nums\">\n Showing {rows.length} of {allCohorts.length}{' '}\n {allCohorts.length === 1 ? 'cohort' : 'cohorts'}\n </p>\n </>\n )}\n\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n if (!open) closeDialog();\n else setDialogOpen(true);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New cohort</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Schedule a facilitator-led group to run a course together.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <form\n onSubmit={(e) => {\n e.preventDefault();\n void handleCreate();\n }}\n >\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-course\">Course *</Label>\n <Select\n value={form.courseId}\n onValueChange={(v) => setForm((prev) => ({ ...prev, courseId: v }))}\n >\n <SelectTrigger\n id=\"cohort-course\"\n className=\"data-[placeholder]:text-text-secondary!\"\n >\n <SelectValue placeholder=\"Select a course\" />\n </SelectTrigger>\n <SelectContent>\n {courses.map((c) => (\n <SelectItem key={c.id} value={c.id}>\n {c.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-name\">Name *</Label>\n <Input\n id=\"cohort-name\"\n value={form.name}\n onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}\n placeholder=\"Spring 2026 Cohort\"\n />\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-capacity\">Capacity</Label>\n <Input\n id=\"cohort-capacity\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={form.capacity}\n onChange={(e) => setForm((prev) => ({ ...prev, capacity: e.target.value }))}\n placeholder=\"25\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-facilitator\">Facilitator (user ID)</Label>\n <Input\n id=\"cohort-facilitator\"\n value={form.facilitatorId}\n onChange={(e) =>\n setForm((prev) => ({ ...prev, facilitatorId: e.target.value }))\n }\n placeholder=\"Defaults to you\"\n />\n </div>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-start\">Start date</Label>\n <Input\n id=\"cohort-start\"\n type=\"date\"\n value={form.startDate}\n onChange={(e) => setForm((prev) => ({ ...prev, startDate: e.target.value }))}\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"cohort-end\">End date</Label>\n <Input\n id=\"cohort-end\"\n type=\"date\"\n value={form.endDate}\n onChange={(e) => setForm((prev) => ({ ...prev, endDate: e.target.value }))}\n />\n </div>\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={closeDialog}>\n Cancel\n </Button>\n <Button type=\"submit\" disabled={createCohort.isPending}>\n {createCohort.isPending ? 'Creating…' : 'Create cohort'}\n </Button>\n </ResponsiveDialogFooter>\n </form>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|