@burdenoff/microfe-buildmyiq 2026.730.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.
@@ -153,7 +153,7 @@ var j = [
153
153
  children: [/* @__PURE__ */ g(O, {
154
154
  id: "assessments-course-filter",
155
155
  "aria-label": "Filter assessments by course",
156
- className: "w-full sm:w-72 data-[placeholder]:!text-text-secondary",
156
+ className: "w-full sm:w-72 data-[placeholder]:text-text-secondary!",
157
157
  children: /* @__PURE__ */ g(k, { placeholder: "Select a course" })
158
158
  }), /* @__PURE__ */ g(E, { children: J.map((e) => /* @__PURE__ */ g(D, {
159
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 <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&apos;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&apos;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&apos;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&apos;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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"}
@@ -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 data-[placeholder]:!text-text-secondary",
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 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
+ {"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"}
@@ -367,7 +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
+ className: "data-[placeholder]:text-text-secondary!",
371
371
  children: /* @__PURE__ */ f(x, { placeholder: "Select a course" })
372
372
  }), /* @__PURE__ */ f(v, { children: V.map((e) => /* @__PURE__ */ f(y, {
373
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\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
+ {"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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"}
@@ -316,7 +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
+ className: "data-[placeholder]:text-text-secondary!",
320
320
  children: /* @__PURE__ */ _(E, { placeholder: "Select a course" })
321
321
  }), /* @__PURE__ */ _(C, { children: U.map((e) => /* @__PURE__ */ _(w, {
322
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\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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"}
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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-buildmyiq",
3
- "version": "2026.730.1",
3
+ "version": "2026.730.2",
4
4
  "description": "Burdenoff BuildMyIQ Microfrontend — education, learning & development",
5
5
  "type": "module",
6
6
  "files": [