@burdenoff/microfe-technospam 2026.1102.5 → 2026.1103.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generated/wspace-types.d.ts +102 -5
- package/dist/generated/wspace-types.d.ts.map +1 -1
- package/dist/stories/fixtures/technospamFixtures.d.ts.map +1 -1
- package/dist/technospam/api/canonicalLearning.d.ts +26 -1
- package/dist/technospam/api/canonicalLearning.d.ts.map +1 -1
- package/dist/technospam/api/canonicalLearning.js +31 -21
- package/dist/technospam/api/canonicalLearning.js.map +1 -1
- package/dist/technospam/api/technospamApi.d.ts +69 -13
- package/dist/technospam/api/technospamApi.d.ts.map +1 -1
- package/dist/technospam/api/technospamApi.js +462 -375
- package/dist/technospam/api/technospamApi.js.map +1 -1
- package/dist/technospam/components/admin/AssessmentsAdminTab.js +1 -1
- package/dist/technospam/components/admin/AssessmentsAdminTab.js.map +1 -1
- package/dist/technospam/components/admin/EnterpriseIntegrationsTab.js +1 -1
- package/dist/technospam/components/admin/EnterpriseIntegrationsTab.js.map +1 -1
- package/dist/technospam/pages/AdminAdminDashboardPagePage.js +2 -2
- package/dist/technospam/pages/AdminAdminDashboardPagePage.js.map +1 -1
- package/dist/technospam/pages/AnalyticsAnalyticsPagePage.js +1 -1
- package/dist/technospam/pages/AnalyticsAnalyticsPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +1 -1
- package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
- package/dist/technospam/pages/CoursesCoursesPagePage.js +1 -1
- package/dist/technospam/pages/CoursesCoursesPagePage.js.map +1 -1
- package/dist/technospam/pages/DashboardDashboardPagePage.js +1 -1
- package/dist/technospam/pages/DashboardDashboardPagePage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssessmentsAdminTab.js","names":[],"sources":["../../../../src/technospam/components/admin/AssessmentsAdminTab.tsx"],"sourcesContent":["import { useState, type ReactNode } from 'react';\nimport { Archive, Check, ListChecks, Pencil, Plus, Rocket, ShieldCheck, X } from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useCoursesQuery } from '../../api/technospamApi';\nimport {\n useArchiveAssessment,\n useAssessmentAttemptEventsQuery,\n useAssessmentAttemptsQuery,\n useAssessmentQuestionKeyQuery,\n useAssessmentsQuery,\n useCreateAssessment,\n usePublishAssessment,\n useReviewAssessmentAttempt,\n useUpdateAssessment,\n type TecAssessment,\n type TecAssessmentAttempt,\n type TecProctoringMode,\n type TecProctoringStatus,\n} from '../../api/assessmentApi';\nimport { LessonKindFields } from '../lesson/LessonKindFields';\nimport { draftFromLesson, draftToInput, type LessonKindDraft } from '../lesson/lessonAuthoring';\nimport { EmptyView, ErrorView, LoadingRows } from '../StateViews';\nimport { StatusBadge } from '../StatusBadge';\nimport { notify } from '../../lib/toast';\nimport { formatDateTime, humanizeEnum } from '../../utils/format';\nimport {\n assessmentSettingsToInput,\n draftFromAssessment,\n emptyAssessmentDraft,\n type AssessmentSettingsDraft,\n} from '../../utils/assessment';\n\nconst MODES: TecProctoringMode[] = ['INTEGRITY_MONITORING', 'REVIEW_REQUIRED', 'NONE'];\nconst REVIEW_FILTERS: (TecProctoringStatus | 'ALL')[] = ['FLAGGED', 'PENDING_REVIEW', 'ALL'];\n\nfunction Field({ label, children }: { label: string; children: ReactNode }) {\n return (\n <label className=\"flex flex-col gap-1.5\">\n <span className=\"text-xs font-medium text-text-secondary\">{label}</span>\n {children}\n </label>\n );\n}\n\nfunction Toggle({\n label,\n checked,\n onChange,\n}: {\n label: string;\n checked: boolean;\n onChange: (value: boolean) => void;\n}) {\n return (\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input type=\"checkbox\" checked={checked} onChange={(e) => onChange(e.target.checked)} />\n {label}\n </label>\n );\n}\n\n/**\n * Create / edit form for one assessment: settings plus the question set with\n * its answer key (reusing the quiz-lesson editor).\n */\nfunction AssessmentForm({\n courseId,\n assessment,\n onDone,\n}: {\n courseId: string;\n assessment: TecAssessment | null;\n onDone: () => void;\n}) {\n const [settings, setSettings] = useState<AssessmentSettingsDraft>(() =>\n assessment ? draftFromAssessment(assessment) : emptyAssessmentDraft()\n );\n const { data: keyData, loading: keyLoading } = useAssessmentQuestionKeyQuery(\n assessment?.id ?? null\n );\n const [questions, setQuestions] = useState<LessonKindDraft | null>(\n assessment ? null : draftFromLesson()\n );\n const [create, { loading: creating }] = useCreateAssessment();\n const [update, { loading: updating }] = useUpdateAssessment();\n const quiz =\n questions ?? (keyData ? draftFromLesson(null, keyData.tecAssessmentQuestionKey) : null);\n const set = <K extends keyof AssessmentSettingsDraft>(\n key: K,\n value: AssessmentSettingsDraft[K]\n ): void => setSettings((prev) => ({ ...prev, [key]: value }));\n\n const save = async (): Promise<void> => {\n const parsed = assessmentSettingsToInput(settings);\n if (!parsed.ok) return notify.error(parsed.error);\n let questionInput: Record<string, unknown> = {};\n if (quiz) {\n const q = draftToInput('QUIZ', quiz);\n if (!q.ok) return notify.error(q.error);\n questionInput = { questions: q.input.quizQuestions };\n }\n if (assessment) {\n const res = await update({\n variables: { id: assessment.id, input: { ...parsed.input, ...questionInput } },\n });\n if (res.data?.tecUpdateAssessment) {\n notify.success('Assessment saved');\n onDone();\n }\n return;\n }\n const res = await create({\n variables: { input: { courseId, ...parsed.input, ...questionInput } },\n });\n if (res.data?.tecCreateAssessment) {\n notify.success('Assessment created', 'Publish it when the questions are ready.');\n onDone();\n }\n };\n\n return (\n <GlassCard className=\"space-y-4 p-4 sm:p-5\" data-testid=\"assessment-form\">\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n <Field label=\"Title\">\n <Input value={settings.title} onChange={(e) => set('title', e.target.value)} />\n </Field>\n <Field label=\"Integrity mode\">\n <Select\n value={settings.proctoringMode}\n onValueChange={(v) => set('proctoringMode', v as TecProctoringMode)}\n >\n <SelectTrigger aria-label=\"Integrity mode\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {MODES.map((mode) => (\n <SelectItem key={mode} value={mode}>\n {humanizeEnum(mode)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </Field>\n <Field label=\"Time limit (minutes)\">\n <Input\n inputMode=\"numeric\"\n value={settings.timeLimitMinutes}\n onChange={(e) => set('timeLimitMinutes', e.target.value)}\n />\n </Field>\n <Field label=\"Pass mark (%)\">\n <Input\n inputMode=\"numeric\"\n value={settings.passingScore}\n onChange={(e) => set('passingScore', e.target.value)}\n />\n </Field>\n <Field label=\"Attempts per learner\">\n <Input\n inputMode=\"numeric\"\n value={settings.maxAttempts}\n onChange={(e) => set('maxAttempts', e.target.value)}\n />\n </Field>\n <Field label=\"Allowed integrity events\">\n <Input\n inputMode=\"numeric\"\n value={settings.maxIntegrityViolations}\n onChange={(e) => set('maxIntegrityViolations', e.target.value)}\n />\n </Field>\n <div className=\"sm:col-span-2\">\n <Field label=\"Description\">\n <Textarea\n value={settings.description}\n onChange={(e) => set('description', e.target.value)}\n />\n </Field>\n </div>\n <div className=\"flex flex-wrap gap-x-5 gap-y-2 sm:col-span-2\">\n <Toggle\n label=\"Require fullscreen\"\n checked={settings.requireFullscreen}\n onChange={(v) => set('requireFullscreen', v)}\n />\n <Toggle\n label=\"Block copy and paste\"\n checked={settings.blockClipboard}\n onChange={(v) => set('blockClipboard', v)}\n />\n <Toggle\n label=\"Shuffle questions\"\n checked={settings.shuffleQuestions}\n onChange={(v) => set('shuffleQuestions', v)}\n />\n <Toggle\n label=\"Shuffle options\"\n checked={settings.shuffleOptions}\n onChange={(v) => set('shuffleOptions', v)}\n />\n <Toggle\n label=\"Issue course certificate\"\n checked={settings.issuesCertificate}\n onChange={(v) => set('issuesCertificate', v)}\n />\n </div>\n </div>\n {quiz ? (\n <LessonKindFields\n kind=\"QUIZ\"\n draft={quiz}\n onChange={setQuestions}\n heading={`Questions (learners need ${settings.passingScore || '?'}% to pass)`}\n />\n ) : keyLoading ? (\n <LoadingRows count={2} />\n ) : null}\n <div className=\"flex justify-end gap-2 border-t border-border-seam pt-3\">\n <Button variant=\"outline\" onClick={onDone}>\n <X size={15} /> Cancel\n </Button>\n <Button\n onClick={() => void save()}\n disabled={creating || updating}\n data-testid=\"assessment-save\"\n >\n <Check size={15} /> {creating || updating ? 'Saving…' : 'Save assessment'}\n </Button>\n </div>\n </GlassCard>\n );\n}\n\nfunction AttemptReviewRow({ attempt, title }: { attempt: TecAssessmentAttempt; title: string }) {\n const [open, setOpen] = useState(false);\n const [notes, setNotes] = useState('');\n const { data: eventsData } = useAssessmentAttemptEventsQuery(open ? attempt.id : null);\n const [review, { loading }] = useReviewAssessmentAttempt();\n const reviewable =\n attempt.proctoringStatus === 'FLAGGED' || attempt.proctoringStatus === 'PENDING_REVIEW';\n\n const decide = async (decision: 'VERIFIED' | 'REJECTED'): Promise<void> => {\n const res = await review({\n variables: { id: attempt.id, decision, notes: notes.trim() || null },\n });\n const done = res.data?.tecReviewAssessmentAttempt;\n if (done) {\n notify.success(\n decision === 'VERIFIED' ? 'Attempt verified' : 'Attempt rejected',\n done.certificateId ? 'The certificate was issued.' : undefined\n );\n }\n };\n\n return (\n <GlassCard className=\"space-y-3 p-4\" data-testid=\"assessment-review-row\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm font-semibold text-text-primary\">{title}</div>\n <div className=\"text-xs text-text-muted\">\n Learner {attempt.learnerId} · attempt {attempt.attemptNumber} · {attempt.score ?? 0}% (\n {attempt.passed ? 'passed' : 'not passed'}) · {attempt.integrityViolations} integrity\n events · {formatDateTime(attempt.submittedAt ?? attempt.startedAt)}\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <StatusBadge status={attempt.proctoringStatus} />\n <Button size=\"sm\" variant=\"outline\" onClick={() => setOpen((v) => !v)}>\n {open ? 'Hide' : 'Review'}\n </Button>\n </div>\n </div>\n {open ? (\n <div className=\"space-y-3\">\n <ol\n className=\"max-h-56 space-y-1 overflow-auto rounded-lg bg-bg-sunken p-3 text-xs\"\n data-testid=\"assessment-events\"\n >\n {(eventsData?.tecAssessmentAttemptEvents ?? []).map((event) => (\n <li\n key={event.id}\n className={event.violation ? 'text-status-warning-text' : 'text-text-secondary'}\n >\n {formatDateTime(event.occurredAt)} · {humanizeEnum(event.type)}\n {event.violation ? ' (violation)' : ''}\n </li>\n ))}\n </ol>\n {reviewable ? (\n <>\n <Textarea\n aria-label=\"Review notes\"\n placeholder=\"Notes for the learner (optional)\"\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n />\n <div className=\"flex gap-2\">\n <Button size=\"sm\" disabled={loading} onClick={() => void decide('VERIFIED')}>\n <Check size={14} /> Verify\n </Button>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={loading}\n onClick={() => void decide('REJECTED')}\n >\n <X size={14} /> Reject\n </Button>\n </div>\n </>\n ) : attempt.reviewNotes ? (\n <p className=\"text-sm text-text-secondary\">Reviewer note: {attempt.reviewNotes}</p>\n ) : null}\n </div>\n ) : null}\n </GlassCard>\n );\n}\n\n/**\n * Authoring console tab for certification assessments (BOFF-4427): pick a\n * course, create / edit / publish / archive its assessments, and work the\n * review queue of flagged and review-required attempts.\n */\nexport function AssessmentsAdminTab() {\n const { data: courseData } = useCoursesQuery({});\n const courses = courseData?.tecListCourses?.items ?? [];\n const [courseId, setCourseId] = useState<string>('');\n const activeCourseId = courseId || courses[0]?.id || '';\n const { data, loading, error } = useAssessmentsQuery(\n { courseId: activeCourseId },\n !activeCourseId\n );\n const [editing, setEditing] = useState<TecAssessment | 'new' | null>(null);\n const [publish] = usePublishAssessment();\n const [archive] = useArchiveAssessment();\n const [filter, setFilter] = useState<TecProctoringStatus | 'ALL'>('FLAGGED');\n const attemptsQuery = useAssessmentAttemptsQuery({\n proctoringStatus: filter === 'ALL' ? null : filter,\n });\n const assessments = data?.tecListAssessments ?? [];\n const titles = new Map(assessments.map((a) => [a.id, a.title]));\n\n return (\n <div className=\"space-y-6\">\n <section className=\"space-y-3\">\n <div className=\"flex flex-wrap items-end justify-between gap-3\">\n <Field label=\"Course\">\n <Select\n value={activeCourseId}\n onValueChange={(v) => {\n setCourseId(v);\n setEditing(null);\n }}\n >\n <SelectTrigger aria-label=\"Course\" className=\"min-w-64\">\n <SelectValue placeholder=\"Choose a course\" />\n </SelectTrigger>\n <SelectContent>\n {courses.map((course) => (\n <SelectItem key={course.id} value={course.id}>\n {course.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </Field>\n {activeCourseId && editing === null ? (\n <Button size=\"sm\" onClick={() => setEditing('new')} data-testid=\"assessment-new\">\n <Plus size={15} /> New assessment\n </Button>\n ) : null}\n </div>\n\n {editing !== null && activeCourseId ? (\n <AssessmentForm\n key={editing === 'new' ? 'new' : editing.id}\n courseId={activeCourseId}\n assessment={editing === 'new' ? null : editing}\n onDone={() => setEditing(null)}\n />\n ) : null}\n\n {!activeCourseId ? (\n <EmptyView\n title=\"No courses yet\"\n description=\"Create a course first, then add its assessments.\"\n />\n ) : error ? (\n <ErrorView title=\"Could not load assessments\" error={error} />\n ) : loading && assessments.length === 0 ? (\n <LoadingRows count={2} />\n ) : assessments.length === 0 ? (\n <EmptyView\n icon={<ShieldCheck size={26} />}\n title=\"No assessments for this course\"\n description=\"Add a timed, integrity-monitored exam that issues the course certificate.\"\n />\n ) : (\n <div className=\"space-y-2\">\n {assessments.map((assessment) => (\n <GlassCard\n key={assessment.id}\n className=\"flex flex-wrap items-center justify-between gap-3 p-4\"\n >\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm font-semibold text-text-primary\">\n {assessment.title}\n </div>\n <div className=\"text-xs text-text-muted\">\n {assessment.questionCount} questions · {assessment.timeLimitMinutes} min · pass{' '}\n {assessment.passingScore}% · {humanizeEnum(assessment.proctoringMode)}\n </div>\n </div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={assessment.status} />\n <Button size=\"sm\" variant=\"outline\" onClick={() => setEditing(assessment)}>\n <Pencil size={14} /> Edit\n </Button>\n {assessment.status !== 'PUBLISHED' ? (\n <Button\n size=\"sm\"\n onClick={() =>\n void publish({ variables: { id: assessment.id } }).then((res) => {\n if (res.data?.tecPublishAssessment)\n notify.success('Assessment published');\n })\n }\n >\n <Rocket size={14} /> Publish\n </Button>\n ) : (\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() =>\n void archive({ variables: { id: assessment.id } }).then((res) => {\n if (res.data?.tecArchiveAssessment) notify.success('Assessment archived');\n })\n }\n >\n <Archive size={14} /> Archive\n </Button>\n )}\n </div>\n </GlassCard>\n ))}\n </div>\n )}\n </section>\n\n <section className=\"space-y-3\">\n <div className=\"flex flex-wrap items-end justify-between gap-3\">\n <h3 className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <ListChecks size={16} /> Review queue\n </h3>\n <Select value={filter} onValueChange={(v) => setFilter(v as TecProctoringStatus | 'ALL')}>\n <SelectTrigger aria-label=\"Review filter\" className=\"min-w-48\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {REVIEW_FILTERS.map((f) => (\n <SelectItem key={f} value={f}>\n {f === 'ALL' ? 'All attempts' : humanizeEnum(f)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n {attemptsQuery.error ? (\n <ErrorView title=\"Could not load attempts\" error={attemptsQuery.error} />\n ) : (attemptsQuery.data?.tecListAssessmentAttempts ?? []).length === 0 ? (\n <EmptyView\n title=\"Nothing to review\"\n description=\"Flagged and review-required attempts appear here.\"\n />\n ) : (\n <div className=\"space-y-2\">\n {(attemptsQuery.data?.tecListAssessmentAttempts ?? []).map((attempt) => (\n <AttemptReviewRow\n key={attempt.id}\n attempt={attempt}\n title={titles.get(attempt.assessmentId) ?? 'Assessment attempt'}\n />\n ))}\n </div>\n )}\n </section>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;AA0CA,IAAM,IAA6B;CAAC;CAAwB;CAAmB;CAAO,EAChF,IAAkD;CAAC;CAAW;CAAkB;CAAM;AAE5F,SAAS,EAAM,EAAE,UAAO,eAAoD;AAC1E,QACE,kBAAC,SAAD;EAAO,WAAU;YAAjB,CACE,kBAAC,QAAD;GAAM,WAAU;aAA2C;GAAa,CAAA,EACvE,EACK;;;AAIZ,SAAS,EAAO,EACd,UACA,YACA,eAKC;AACD,QACE,kBAAC,SAAD;EAAO,WAAU;YAAjB,CACE,kBAAC,SAAD;GAAO,MAAK;GAAoB;GAAS,WAAW,MAAM,EAAS,EAAE,OAAO,QAAQ;GAAI,CAAA,EACvF,EACK;;;AAQZ,SAAS,EAAe,EACtB,aACA,eACA,aAKC;CACD,IAAM,CAAC,GAAU,KAAe,QAC9B,IAAa,EAAoB,EAAW,GAAG,GAAsB,CACtE,EACK,EAAE,MAAM,GAAS,SAAS,MAAe,EAC7C,GAAY,MAAM,KACnB,EACK,CAAC,GAAW,KAAgB,EAChC,IAAa,OAAO,GAAiB,CACtC,EACK,CAAC,GAAQ,EAAE,SAAS,OAAc,GAAqB,EACvD,CAAC,GAAQ,EAAE,SAAS,OAAc,GAAqB,EACvD,IACJ,MAAc,IAAU,EAAgB,MAAM,EAAQ,yBAAyB,GAAG,OAC9E,KACJ,GACA,MACS,GAAa,OAAU;EAAE,GAAG;GAAO,IAAM;EAAO,EAAE,EAEvD,IAAO,YAA2B;EACtC,IAAM,IAAS,EAA0B,EAAS;AAClD,MAAI,CAAC,EAAO,GAAI,QAAO,EAAO,MAAM,EAAO,MAAM;EACjD,IAAI,IAAyC,EAAE;AAC/C,MAAI,GAAM;GACR,IAAM,IAAI,EAAa,QAAQ,EAAK;AACpC,OAAI,CAAC,EAAE,GAAI,QAAO,EAAO,MAAM,EAAE,MAAM;AACvC,OAAgB,EAAE,WAAW,EAAE,MAAM,eAAe;;AAEtD,MAAI,GAAY;AAId,IAHY,MAAM,EAAO,EACvB,WAAW;IAAE,IAAI,EAAW;IAAI,OAAO;KAAE,GAAG,EAAO;KAAO,GAAG;KAAe;IAAE,EAC/E,CAAC,EACM,MAAM,wBACZ,EAAO,QAAQ,mBAAmB,EAClC,GAAQ;AAEV;;AAKF,GAHY,MAAM,EAAO,EACvB,WAAW,EAAE,OAAO;GAAE;GAAU,GAAG,EAAO;GAAO,GAAG;GAAe,EAAE,EACtE,CAAC,EACM,MAAM,wBACZ,EAAO,QAAQ,sBAAsB,2CAA2C,EAChF,GAAQ;;AAIZ,QACE,kBAAC,GAAD;EAAW,WAAU;EAAuB,eAAY;YAAxD;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MAAO,OAAM;gBACX,kBAAC,GAAD;OAAO,OAAO,EAAS;OAAO,WAAW,MAAM,EAAI,SAAS,EAAE,OAAO,MAAM;OAAI,CAAA;MACzE,CAAA;KACR,kBAAC,GAAD;MAAO,OAAM;gBACX,kBAAC,GAAD;OACE,OAAO,EAAS;OAChB,gBAAgB,MAAM,EAAI,kBAAkB,EAAuB;iBAFrE,CAIE,kBAAC,GAAD;QAAe,cAAW;kBACxB,kBAAC,GAAD,EAAe,CAAA;QACD,CAAA,EAChB,kBAAC,GAAD,EAAA,UACG,EAAM,KAAK,MACV,kBAAC,GAAD;QAAuB,OAAO;kBAC3B,EAAa,EAAK;QACR,EAFI,EAEJ,CACb,EACY,CAAA,CACT;;MACH,CAAA;KACR,kBAAC,GAAD;MAAO,OAAM;gBACX,kBAAC,GAAD;OACE,WAAU;OACV,OAAO,EAAS;OAChB,WAAW,MAAM,EAAI,oBAAoB,EAAE,OAAO,MAAM;OACxD,CAAA;MACI,CAAA;KACR,kBAAC,GAAD;MAAO,OAAM;gBACX,kBAAC,GAAD;OACE,WAAU;OACV,OAAO,EAAS;OAChB,WAAW,MAAM,EAAI,gBAAgB,EAAE,OAAO,MAAM;OACpD,CAAA;MACI,CAAA;KACR,kBAAC,GAAD;MAAO,OAAM;gBACX,kBAAC,GAAD;OACE,WAAU;OACV,OAAO,EAAS;OAChB,WAAW,MAAM,EAAI,eAAe,EAAE,OAAO,MAAM;OACnD,CAAA;MACI,CAAA;KACR,kBAAC,GAAD;MAAO,OAAM;gBACX,kBAAC,GAAD;OACE,WAAU;OACV,OAAO,EAAS;OAChB,WAAW,MAAM,EAAI,0BAA0B,EAAE,OAAO,MAAM;OAC9D,CAAA;MACI,CAAA;KACR,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OAAO,OAAM;iBACX,kBAAC,GAAD;QACE,OAAO,EAAS;QAChB,WAAW,MAAM,EAAI,eAAe,EAAE,OAAO,MAAM;QACnD,CAAA;OACI,CAAA;MACJ,CAAA;KACN,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,GAAD;QACE,OAAM;QACN,SAAS,EAAS;QAClB,WAAW,MAAM,EAAI,qBAAqB,EAAE;QAC5C,CAAA;OACF,kBAAC,GAAD;QACE,OAAM;QACN,SAAS,EAAS;QAClB,WAAW,MAAM,EAAI,kBAAkB,EAAE;QACzC,CAAA;OACF,kBAAC,GAAD;QACE,OAAM;QACN,SAAS,EAAS;QAClB,WAAW,MAAM,EAAI,oBAAoB,EAAE;QAC3C,CAAA;OACF,kBAAC,GAAD;QACE,OAAM;QACN,SAAS,EAAS;QAClB,WAAW,MAAM,EAAI,kBAAkB,EAAE;QACzC,CAAA;OACF,kBAAC,GAAD;QACE,OAAM;QACN,SAAS,EAAS;QAClB,WAAW,MAAM,EAAI,qBAAqB,EAAE;QAC5C,CAAA;OACE;;KACF;;GACL,IACC,kBAAC,GAAD;IACE,MAAK;IACL,OAAO;IACP,UAAU;IACV,SAAS,4BAA4B,EAAS,gBAAgB,IAAI;IAClE,CAAA,GACA,IACF,kBAAC,GAAD,EAAa,OAAO,GAAK,CAAA,GACvB;GACJ,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAQ,SAAQ;KAAU,SAAS;eAAnC,CACE,kBAAC,GAAD,EAAG,MAAM,IAAM,CAAA,EAAA,UACR;QACT,kBAAC,GAAD;KACE,eAAe,KAAK,GAAM;KAC1B,UAAU,KAAY;KACtB,eAAY;eAHd;MAKE,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA;;MAAE,KAAY,IAAW,YAAY;MACjD;OACL;;GACI;;;AAIhB,SAAS,EAAiB,EAAE,YAAS,YAA2D;CAC9F,IAAM,CAAC,GAAM,KAAW,EAAS,GAAM,EACjC,CAAC,GAAO,KAAY,EAAS,GAAG,EAChC,EAAE,MAAM,MAAe,EAAgC,IAAO,EAAQ,KAAK,KAAK,EAChF,CAAC,GAAQ,EAAE,gBAAa,GAA4B,EACpD,IACJ,EAAQ,qBAAqB,aAAa,EAAQ,qBAAqB,kBAEnE,IAAS,OAAO,MAAqD;EAIzE,IAAM,KAHM,MAAM,EAAO,EACvB,WAAW;GAAE,IAAI,EAAQ;GAAI;GAAU,OAAO,EAAM,MAAM,IAAI;GAAM,EACrE,CAAC,EACe,MAAM;AACvB,EAAI,KACF,EAAO,QACL,MAAa,aAAa,qBAAqB,oBAC/C,EAAK,gBAAgB,gCAAgC,KAAA,EACtD;;AAIL,QACE,kBAAC,GAAD;EAAW,WAAU;EAAgB,eAAY;YAAjD,CACE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAoD;KAAY,CAAA,EAC/E,kBAAC,OAAD;KAAK,WAAU;eAAf;MAAyC;MAC9B,EAAQ;MAAU;MAAY,EAAQ;MAAc;MAAI,EAAQ,SAAS;MAAE;MACnF,EAAQ,SAAS,WAAW;MAAa;MAAK,EAAQ;MAAoB;MACjE,EAAe,EAAQ,eAAe,EAAQ,UAAU;MAC9D;OACF;OACN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD,EAAa,QAAQ,EAAQ,kBAAoB,CAAA,EACjD,kBAAC,GAAD;KAAQ,MAAK;KAAK,SAAQ;KAAU,eAAe,GAAS,MAAM,CAAC,EAAE;eAClE,IAAO,SAAS;KACV,CAAA,CACL;MACF;MACL,IACC,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,MAAD;IACE,WAAU;IACV,eAAY;eAEV,GAAY,8BAA8B,EAAE,EAAE,KAAK,MACnD,kBAAC,MAAD;KAEE,WAAW,EAAM,YAAY,6BAA6B;eAF5D;MAIG,EAAe,EAAM,WAAW;MAAC;MAAI,EAAa,EAAM,KAAK;MAC7D,EAAM,YAAY,iBAAiB;MACjC;OALE,EAAM,GAKR,CACL;IACC,CAAA,EACJ,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;IACE,cAAW;IACX,aAAY;IACZ,OAAO;IACP,WAAW,MAAM,EAAS,EAAE,OAAO,MAAM;IACzC,CAAA,EACF,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KAAQ,MAAK;KAAK,UAAU;KAAS,eAAe,KAAK,EAAO,WAAW;eAA3E,CACE,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA,EAAA,UACZ;QACT,kBAAC,GAAD;KACE,MAAK;KACL,SAAQ;KACR,UAAU;KACV,eAAe,KAAK,EAAO,WAAW;eAJxC,CAME,kBAAC,GAAD,EAAG,MAAM,IAAM,CAAA,EAAA,UACR;OACL;MACL,EAAA,CAAA,GACD,EAAQ,cACV,kBAAC,KAAD;IAAG,WAAU;cAAb,CAA2C,mBAAgB,EAAQ,YAAgB;QACjF,KACA;OACJ,KACM;;;AAShB,SAAgB,IAAsB;CACpC,IAAM,EAAE,MAAM,MAAe,EAAgB,EAAE,CAAC,EAC1C,IAAU,GAAY,gBAAgB,SAAS,EAAE,EACjD,CAAC,GAAU,KAAe,EAAiB,GAAG,EAC9C,IAAiB,KAAY,EAAQ,IAAI,MAAM,IAC/C,EAAE,SAAM,YAAS,aAAU,EAC/B,EAAE,UAAU,GAAgB,EAC5B,CAAC,EACF,EACK,CAAC,GAAS,KAAc,EAAuC,KAAK,EACpE,CAAC,KAAW,GAAsB,EAClC,CAAC,KAAW,GAAsB,EAClC,CAAC,GAAQ,KAAa,EAAsC,UAAU,EACtE,IAAgB,EAA2B,EAC/C,kBAAkB,MAAW,QAAQ,OAAO,GAC7C,CAAC,EACI,IAAc,GAAM,sBAAsB,EAAE,EAC5C,IAAS,IAAI,IAAI,EAAY,KAAK,MAAM,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;AAE/D,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,WAAD;GAAS,WAAU;aAAnB;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MAAO,OAAM;gBACX,kBAAC,GAAD;OACE,OAAO;OACP,gBAAgB,MAAM;AAEpB,QADA,EAAY,EAAE,EACd,EAAW,KAAK;;iBAJpB,CAOE,kBAAC,GAAD;QAAe,cAAW;QAAS,WAAU;kBAC3C,kBAAC,GAAD,EAAa,aAAY,mBAAoB,CAAA;QAC/B,CAAA,EAChB,kBAAC,GAAD,EAAA,UACG,EAAQ,KAAK,MACZ,kBAAC,GAAD;QAA4B,OAAO,EAAO;kBACvC,EAAO;QACG,EAFI,EAAO,GAEX,CACb,EACY,CAAA,CACT;;MACH,CAAA,EACP,KAAkB,MAAY,OAC7B,kBAAC,GAAD;MAAQ,MAAK;MAAK,eAAe,EAAW,MAAM;MAAE,eAAY;gBAAhE,CACE,kBAAC,GAAD,EAAM,MAAM,IAAM,CAAA,EAAA,kBACX;UACP,KACA;;IAEL,MAAY,QAAQ,IACnB,kBAAC,GAAD;KAEE,UAAU;KACV,YAAY,MAAY,QAAQ,OAAO;KACvC,cAAc,EAAW,KAAK;KAC9B,EAJK,MAAY,QAAQ,QAAQ,EAAQ,GAIzC,GACA;IAEF,IAKE,IACF,kBAAC,GAAD;KAAW,OAAM;KAAoC;KAAS,CAAA,GAC5D,KAAW,EAAY,WAAW,IACpC,kBAAC,GAAD,EAAa,OAAO,GAAK,CAAA,GACvB,EAAY,WAAW,IACzB,kBAAC,GAAD;KACE,MAAM,kBAAC,GAAD,EAAa,MAAM,IAAM,CAAA;KAC/B,OAAM;KACN,aAAY;KACZ,CAAA,GAEF,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAY,KAAK,MAChB,kBAAC,GAAD;MAEE,WAAU;gBAFZ,CAIE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,OAAD;QAAK,WAAU;kBACZ,EAAW;QACR,CAAA,EACN,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACG,EAAW;SAAc;SAAc,EAAW;SAAiB;SAAY;SAC/E,EAAW;SAAa;SAAK,EAAa,EAAW,eAAe;SACjE;UACF;UACN,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD,EAAa,QAAQ,EAAW,QAAU,CAAA;QAC1C,kBAAC,GAAD;SAAQ,MAAK;SAAK,SAAQ;SAAU,eAAe,EAAW,EAAW;mBAAzE,CACE,kBAAC,GAAD,EAAQ,MAAM,IAAM,CAAA,EAAA,QACb;;QACR,EAAW,WAAW,cAarB,kBAAC,GAAD;SACE,MAAK;SACL,SAAQ;SACR,eACE,KAAK,EAAQ,EAAE,WAAW,EAAE,IAAI,EAAW,IAAI,EAAE,CAAC,CAAC,MAAM,MAAQ;AAC/D,UAAI,EAAI,MAAM,wBAAsB,EAAO,QAAQ,sBAAsB;WACzE;mBANN,CASE,kBAAC,GAAD,EAAS,MAAM,IAAM,CAAA,EAAA,WACd;aAtBT,kBAAC,GAAD;SACE,MAAK;SACL,eACE,KAAK,EAAQ,EAAE,WAAW,EAAE,IAAI,EAAW,IAAI,EAAE,CAAC,CAAC,MAAM,MAAQ;AAC/D,UAAI,EAAI,MAAM,wBACZ,EAAO,QAAQ,uBAAuB;WACxC;mBANN,CASE,kBAAC,GAAD,EAAQ,MAAM,IAAM,CAAA,EAAA,WACb;;QAcP;SACI;QA3CL,EAAW,GA2CN,CACZ;KACE,CAAA,GA/DN,kBAAC,GAAD;KACE,OAAM;KACN,aAAY;KACZ,CAAA;IA8DI;MAEV,kBAAC,WAAD;GAAS,WAAU;aAAnB,CACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,MAAD;KAAI,WAAU;eAAd,CACE,kBAAC,GAAD,EAAY,MAAM,IAAM,CAAA,EAAA,gBACrB;QACL,kBAAC,GAAD;KAAQ,OAAO;KAAQ,gBAAgB,MAAM,EAAU,EAAiC;eAAxF,CACE,kBAAC,GAAD;MAAe,cAAW;MAAgB,WAAU;gBAClD,kBAAC,GAAD,EAAe,CAAA;MACD,CAAA,EAChB,kBAAC,GAAD,EAAA,UACG,EAAe,KAAK,MACnB,kBAAC,GAAD;MAAoB,OAAO;gBACxB,MAAM,QAAQ,iBAAiB,EAAa,EAAE;MACpC,EAFI,EAEJ,CACb,EACY,CAAA,CACT;OACL;OACL,EAAc,QACb,kBAAC,GAAD;IAAW,OAAM;IAA0B,OAAO,EAAc;IAAS,CAAA,IACtE,EAAc,MAAM,6BAA6B,EAAE,EAAE,WAAW,IACnE,kBAAC,GAAD;IACE,OAAM;IACN,aAAY;IACZ,CAAA,GAEF,kBAAC,OAAD;IAAK,WAAU;eACX,EAAc,MAAM,6BAA6B,EAAE,EAAE,KAAK,MAC1D,kBAAC,GAAD;KAEW;KACT,OAAO,EAAO,IAAI,EAAQ,aAAa,IAAI;KAC3C,EAHK,EAAQ,GAGb,CACF;IACE,CAAA,CAEA;KACN"}
|
|
1
|
+
{"version":3,"file":"AssessmentsAdminTab.js","names":[],"sources":["../../../../src/technospam/components/admin/AssessmentsAdminTab.tsx"],"sourcesContent":["import { useState, type ReactNode } from 'react';\nimport { Archive, Check, ListChecks, Pencil, Plus, Rocket, ShieldCheck, X } from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useCoursesQuery } from '../../api/technospamApi';\nimport {\n useArchiveAssessment,\n useAssessmentAttemptEventsQuery,\n useAssessmentAttemptsQuery,\n useAssessmentQuestionKeyQuery,\n useAssessmentsQuery,\n useCreateAssessment,\n usePublishAssessment,\n useReviewAssessmentAttempt,\n useUpdateAssessment,\n type TecAssessment,\n type TecAssessmentAttempt,\n type TecProctoringMode,\n type TecProctoringStatus,\n} from '../../api/assessmentApi';\nimport { LessonKindFields } from '../lesson/LessonKindFields';\nimport { draftFromLesson, draftToInput, type LessonKindDraft } from '../lesson/lessonAuthoring';\nimport { EmptyView, ErrorView, LoadingRows } from '../StateViews';\nimport { StatusBadge } from '../StatusBadge';\nimport { notify } from '../../lib/toast';\nimport { formatDateTime, humanizeEnum } from '../../utils/format';\nimport {\n assessmentSettingsToInput,\n draftFromAssessment,\n emptyAssessmentDraft,\n type AssessmentSettingsDraft,\n} from '../../utils/assessment';\n\nconst MODES: TecProctoringMode[] = ['INTEGRITY_MONITORING', 'REVIEW_REQUIRED', 'NONE'];\nconst REVIEW_FILTERS: (TecProctoringStatus | 'ALL')[] = ['FLAGGED', 'PENDING_REVIEW', 'ALL'];\n\nfunction Field({ label, children }: { label: string; children: ReactNode }) {\n return (\n <label className=\"flex flex-col gap-1.5\">\n <span className=\"text-xs font-medium text-text-secondary\">{label}</span>\n {children}\n </label>\n );\n}\n\nfunction Toggle({\n label,\n checked,\n onChange,\n}: {\n label: string;\n checked: boolean;\n onChange: (value: boolean) => void;\n}) {\n return (\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input type=\"checkbox\" checked={checked} onChange={(e) => onChange(e.target.checked)} />\n {label}\n </label>\n );\n}\n\n/**\n * Create / edit form for one assessment: settings plus the question set with\n * its answer key (reusing the quiz-lesson editor).\n */\nfunction AssessmentForm({\n courseId,\n assessment,\n onDone,\n}: {\n courseId: string;\n assessment: TecAssessment | null;\n onDone: () => void;\n}) {\n const [settings, setSettings] = useState<AssessmentSettingsDraft>(() =>\n assessment ? draftFromAssessment(assessment) : emptyAssessmentDraft()\n );\n const { data: keyData, loading: keyLoading } = useAssessmentQuestionKeyQuery(\n assessment?.id ?? null\n );\n const [questions, setQuestions] = useState<LessonKindDraft | null>(\n assessment ? null : draftFromLesson()\n );\n const [create, { loading: creating }] = useCreateAssessment();\n const [update, { loading: updating }] = useUpdateAssessment();\n const quiz =\n questions ?? (keyData ? draftFromLesson(null, keyData.tecAssessmentQuestionKey) : null);\n const set = <K extends keyof AssessmentSettingsDraft>(\n key: K,\n value: AssessmentSettingsDraft[K]\n ): void => setSettings((prev) => ({ ...prev, [key]: value }));\n\n const save = async (): Promise<void> => {\n const parsed = assessmentSettingsToInput(settings);\n if (!parsed.ok) return notify.error(parsed.error);\n let questionInput: Record<string, unknown> = {};\n if (quiz) {\n const q = draftToInput('QUIZ', quiz);\n if (!q.ok) return notify.error(q.error);\n questionInput = { questions: q.input.quizQuestions };\n }\n if (assessment) {\n const res = await update({\n variables: { id: assessment.id, input: { ...parsed.input, ...questionInput } },\n });\n if (res.data?.tecUpdateAssessment) {\n notify.success('Assessment saved');\n onDone();\n }\n return;\n }\n const res = await create({\n variables: { input: { courseId, ...parsed.input, ...questionInput } },\n });\n if (res.data?.tecCreateAssessment) {\n notify.success('Assessment created', 'Publish it when the questions are ready.');\n onDone();\n }\n };\n\n return (\n <GlassCard className=\"space-y-4 p-4 sm:p-5\" data-testid=\"assessment-form\">\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n <Field label=\"Title\">\n <Input value={settings.title} onChange={(e) => set('title', e.target.value)} />\n </Field>\n <Field label=\"Integrity mode\">\n <Select\n value={settings.proctoringMode}\n onValueChange={(v) => set('proctoringMode', v as TecProctoringMode)}\n >\n <SelectTrigger aria-label=\"Integrity mode\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {MODES.map((mode) => (\n <SelectItem key={mode} value={mode}>\n {humanizeEnum(mode)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </Field>\n <Field label=\"Time limit (minutes)\">\n <Input\n inputMode=\"numeric\"\n value={settings.timeLimitMinutes}\n onChange={(e) => set('timeLimitMinutes', e.target.value)}\n />\n </Field>\n <Field label=\"Pass mark (%)\">\n <Input\n inputMode=\"numeric\"\n value={settings.passingScore}\n onChange={(e) => set('passingScore', e.target.value)}\n />\n </Field>\n <Field label=\"Attempts per learner\">\n <Input\n inputMode=\"numeric\"\n value={settings.maxAttempts}\n onChange={(e) => set('maxAttempts', e.target.value)}\n />\n </Field>\n <Field label=\"Allowed integrity events\">\n <Input\n inputMode=\"numeric\"\n value={settings.maxIntegrityViolations}\n onChange={(e) => set('maxIntegrityViolations', e.target.value)}\n />\n </Field>\n <div className=\"sm:col-span-2\">\n <Field label=\"Description\">\n <Textarea\n value={settings.description}\n onChange={(e) => set('description', e.target.value)}\n />\n </Field>\n </div>\n <div className=\"flex flex-wrap gap-x-5 gap-y-2 sm:col-span-2\">\n <Toggle\n label=\"Require fullscreen\"\n checked={settings.requireFullscreen}\n onChange={(v) => set('requireFullscreen', v)}\n />\n <Toggle\n label=\"Block copy and paste\"\n checked={settings.blockClipboard}\n onChange={(v) => set('blockClipboard', v)}\n />\n <Toggle\n label=\"Shuffle questions\"\n checked={settings.shuffleQuestions}\n onChange={(v) => set('shuffleQuestions', v)}\n />\n <Toggle\n label=\"Shuffle options\"\n checked={settings.shuffleOptions}\n onChange={(v) => set('shuffleOptions', v)}\n />\n <Toggle\n label=\"Issue course certificate\"\n checked={settings.issuesCertificate}\n onChange={(v) => set('issuesCertificate', v)}\n />\n </div>\n </div>\n {quiz ? (\n <LessonKindFields\n kind=\"QUIZ\"\n draft={quiz}\n onChange={setQuestions}\n heading={`Questions (learners need ${settings.passingScore || '?'}% to pass)`}\n />\n ) : keyLoading ? (\n <LoadingRows count={2} />\n ) : null}\n <div className=\"flex justify-end gap-2 border-t border-border-seam pt-3\">\n <Button variant=\"outline\" onClick={onDone}>\n <X size={15} /> Cancel\n </Button>\n <Button\n onClick={() => void save()}\n disabled={creating || updating}\n data-testid=\"assessment-save\"\n >\n <Check size={15} /> {creating || updating ? 'Saving…' : 'Save assessment'}\n </Button>\n </div>\n </GlassCard>\n );\n}\n\nfunction AttemptReviewRow({ attempt, title }: { attempt: TecAssessmentAttempt; title: string }) {\n const [open, setOpen] = useState(false);\n const [notes, setNotes] = useState('');\n const { data: eventsData } = useAssessmentAttemptEventsQuery(open ? attempt.id : null);\n const [review, { loading }] = useReviewAssessmentAttempt();\n const reviewable =\n attempt.proctoringStatus === 'FLAGGED' || attempt.proctoringStatus === 'PENDING_REVIEW';\n\n const decide = async (decision: 'VERIFIED' | 'REJECTED'): Promise<void> => {\n const res = await review({\n variables: { id: attempt.id, decision, notes: notes.trim() || null },\n });\n const done = res.data?.tecReviewAssessmentAttempt;\n if (done) {\n notify.success(\n decision === 'VERIFIED' ? 'Attempt verified' : 'Attempt rejected',\n done.certificateId ? 'The certificate was issued.' : undefined\n );\n }\n };\n\n return (\n <GlassCard className=\"space-y-3 p-4\" data-testid=\"assessment-review-row\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm font-semibold text-text-primary\">{title}</div>\n <div className=\"text-xs text-text-muted\">\n Learner {attempt.learnerId} · attempt {attempt.attemptNumber} · {attempt.score ?? 0}% (\n {attempt.passed ? 'passed' : 'not passed'}) · {attempt.integrityViolations} integrity\n events · {formatDateTime(attempt.submittedAt ?? attempt.startedAt)}\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n <StatusBadge status={attempt.proctoringStatus} />\n <Button size=\"sm\" variant=\"outline\" onClick={() => setOpen((v) => !v)}>\n {open ? 'Hide' : 'Review'}\n </Button>\n </div>\n </div>\n {open ? (\n <div className=\"space-y-3\">\n <ol\n className=\"max-h-56 space-y-1 overflow-auto rounded-lg bg-bg-sunken p-3 text-xs\"\n data-testid=\"assessment-events\"\n >\n {(eventsData?.tecAssessmentAttemptEvents ?? []).map((event) => (\n <li\n key={event.id}\n className={event.violation ? 'text-status-warning-text' : 'text-text-secondary'}\n >\n {formatDateTime(event.occurredAt)} · {humanizeEnum(event.type)}\n {event.violation ? ' (violation)' : ''}\n </li>\n ))}\n </ol>\n {reviewable ? (\n <>\n <Textarea\n aria-label=\"Review notes\"\n placeholder=\"Notes for the learner (optional)\"\n value={notes}\n onChange={(e) => setNotes(e.target.value)}\n />\n <div className=\"flex gap-2\">\n <Button size=\"sm\" disabled={loading} onClick={() => void decide('VERIFIED')}>\n <Check size={14} /> Verify\n </Button>\n <Button\n size=\"sm\"\n variant=\"outline\"\n disabled={loading}\n onClick={() => void decide('REJECTED')}\n >\n <X size={14} /> Reject\n </Button>\n </div>\n </>\n ) : attempt.reviewNotes ? (\n <p className=\"text-sm text-text-secondary\">Reviewer note: {attempt.reviewNotes}</p>\n ) : null}\n </div>\n ) : null}\n </GlassCard>\n );\n}\n\n/**\n * Authoring console tab for certification assessments (BOFF-4427): pick a\n * course, create / edit / publish / archive its assessments, and work the\n * review queue of flagged and review-required attempts.\n */\nexport function AssessmentsAdminTab() {\n const { data: courseData } = useCoursesQuery({});\n const courses = courseData?.courses?.items ?? [];\n const [courseId, setCourseId] = useState<string>('');\n const activeCourseId = courseId || courses[0]?.id || '';\n const { data, loading, error } = useAssessmentsQuery(\n { courseId: activeCourseId },\n !activeCourseId\n );\n const [editing, setEditing] = useState<TecAssessment | 'new' | null>(null);\n const [publish] = usePublishAssessment();\n const [archive] = useArchiveAssessment();\n const [filter, setFilter] = useState<TecProctoringStatus | 'ALL'>('FLAGGED');\n const attemptsQuery = useAssessmentAttemptsQuery({\n proctoringStatus: filter === 'ALL' ? null : filter,\n });\n const assessments = data?.tecListAssessments ?? [];\n const titles = new Map(assessments.map((a) => [a.id, a.title]));\n\n return (\n <div className=\"space-y-6\">\n <section className=\"space-y-3\">\n <div className=\"flex flex-wrap items-end justify-between gap-3\">\n <Field label=\"Course\">\n <Select\n value={activeCourseId}\n onValueChange={(v) => {\n setCourseId(v);\n setEditing(null);\n }}\n >\n <SelectTrigger aria-label=\"Course\" className=\"min-w-64\">\n <SelectValue placeholder=\"Choose a course\" />\n </SelectTrigger>\n <SelectContent>\n {courses.map((course) => (\n <SelectItem key={course.id} value={course.id}>\n {course.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </Field>\n {activeCourseId && editing === null ? (\n <Button size=\"sm\" onClick={() => setEditing('new')} data-testid=\"assessment-new\">\n <Plus size={15} /> New assessment\n </Button>\n ) : null}\n </div>\n\n {editing !== null && activeCourseId ? (\n <AssessmentForm\n key={editing === 'new' ? 'new' : editing.id}\n courseId={activeCourseId}\n assessment={editing === 'new' ? null : editing}\n onDone={() => setEditing(null)}\n />\n ) : null}\n\n {!activeCourseId ? (\n <EmptyView\n title=\"No courses yet\"\n description=\"Create a course first, then add its assessments.\"\n />\n ) : error ? (\n <ErrorView title=\"Could not load assessments\" error={error} />\n ) : loading && assessments.length === 0 ? (\n <LoadingRows count={2} />\n ) : assessments.length === 0 ? (\n <EmptyView\n icon={<ShieldCheck size={26} />}\n title=\"No assessments for this course\"\n description=\"Add a timed, integrity-monitored exam that issues the course certificate.\"\n />\n ) : (\n <div className=\"space-y-2\">\n {assessments.map((assessment) => (\n <GlassCard\n key={assessment.id}\n className=\"flex flex-wrap items-center justify-between gap-3 p-4\"\n >\n <div className=\"min-w-0\">\n <div className=\"truncate text-sm font-semibold text-text-primary\">\n {assessment.title}\n </div>\n <div className=\"text-xs text-text-muted\">\n {assessment.questionCount} questions · {assessment.timeLimitMinutes} min · pass{' '}\n {assessment.passingScore}% · {humanizeEnum(assessment.proctoringMode)}\n </div>\n </div>\n <div className=\"flex flex-wrap items-center gap-2\">\n <StatusBadge status={assessment.status} />\n <Button size=\"sm\" variant=\"outline\" onClick={() => setEditing(assessment)}>\n <Pencil size={14} /> Edit\n </Button>\n {assessment.status !== 'PUBLISHED' ? (\n <Button\n size=\"sm\"\n onClick={() =>\n void publish({ variables: { id: assessment.id } }).then((res) => {\n if (res.data?.tecPublishAssessment)\n notify.success('Assessment published');\n })\n }\n >\n <Rocket size={14} /> Publish\n </Button>\n ) : (\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() =>\n void archive({ variables: { id: assessment.id } }).then((res) => {\n if (res.data?.tecArchiveAssessment) notify.success('Assessment archived');\n })\n }\n >\n <Archive size={14} /> Archive\n </Button>\n )}\n </div>\n </GlassCard>\n ))}\n </div>\n )}\n </section>\n\n <section className=\"space-y-3\">\n <div className=\"flex flex-wrap items-end justify-between gap-3\">\n <h3 className=\"flex items-center gap-2 text-sm font-semibold text-text-primary\">\n <ListChecks size={16} /> Review queue\n </h3>\n <Select value={filter} onValueChange={(v) => setFilter(v as TecProctoringStatus | 'ALL')}>\n <SelectTrigger aria-label=\"Review filter\" className=\"min-w-48\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {REVIEW_FILTERS.map((f) => (\n <SelectItem key={f} value={f}>\n {f === 'ALL' ? 'All attempts' : humanizeEnum(f)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n {attemptsQuery.error ? (\n <ErrorView title=\"Could not load attempts\" error={attemptsQuery.error} />\n ) : (attemptsQuery.data?.tecListAssessmentAttempts ?? []).length === 0 ? (\n <EmptyView\n title=\"Nothing to review\"\n description=\"Flagged and review-required attempts appear here.\"\n />\n ) : (\n <div className=\"space-y-2\">\n {(attemptsQuery.data?.tecListAssessmentAttempts ?? []).map((attempt) => (\n <AttemptReviewRow\n key={attempt.id}\n attempt={attempt}\n title={titles.get(attempt.assessmentId) ?? 'Assessment attempt'}\n />\n ))}\n </div>\n )}\n </section>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -126,7 +126,7 @@ function G(e, t) {
|
|
|
126
126
|
i.href = r, i.download = e, document.body.appendChild(i), i.click(), i.remove(), URL.revokeObjectURL(r);
|
|
127
127
|
}
|
|
128
128
|
function K() {
|
|
129
|
-
let { data: e } = i({}), t = e?.
|
|
129
|
+
let { data: e } = i({}), t = e?.courses?.items ?? [], [a, o] = b(H), [c, l] = b(""), [u, f] = b(""), [p, h] = b(null), { run: g, running: _, error: y } = s();
|
|
130
130
|
return /* @__PURE__ */ j(N, {
|
|
131
131
|
className: "p-5",
|
|
132
132
|
"data-testid": "tec-enterprise-xapi",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EnterpriseIntegrationsTab.js","names":[],"sources":["../../../../src/technospam/components/admin/EnterpriseIntegrationsTab.tsx"],"sourcesContent":["import { useEffect, useState, type ChangeEvent, type FormEvent, type ReactNode } from 'react';\nimport {\n BadgeCheck,\n Download,\n FileJson,\n KeyRound,\n Loader2,\n RefreshCw,\n Search,\n Send,\n Unplug,\n} from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n MAX_VERIFY_BATCH,\n useCoursesQuery,\n useLrsConnectionQuery,\n useVerifyCertificatesQuery,\n useXapiExport,\n useXapiLrsActions,\n useXapiLrsSyncQuery,\n} from '../../api/technospamApi';\nimport {\n type LrsFormErrors,\n type LrsFormValues,\n atsVerificationSnippet,\n dayStartIso,\n lrsConnectionName,\n lrsSyncSummary,\n nextDayStartIso,\n normalizeLrsEndpoint,\n parseSerials,\n validateLrsForm,\n xapiExportFileName,\n} from '../../lib/enterprise';\nimport { notify } from '../../lib/toast';\nimport { formatDate } from '../../utils/format';\nimport { ErrorView } from '../StateViews';\nimport { StatusBadge } from '../StatusBadge';\n\n/** Public workspaces gateway ATS partners call (production). */\nconst PARTNER_ENDPOINT = 'https://graphqlworkspaces.burdenoff.com/workspaces/graphql';\nconst ALL_COURSES = '__all__';\n\nfunction SectionTitle({\n icon: Icon,\n title,\n description,\n}: {\n icon: typeof BadgeCheck;\n title: string;\n description: string;\n}) {\n return (\n <div className=\"mb-4 flex items-start gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-accent-primary/10 text-accent-primary\">\n <Icon size={18} />\n </span>\n <div className=\"min-w-0\">\n <h2 className=\"text-base font-semibold text-text-primary\">{title}</h2>\n <p className=\"mt-0.5 text-sm text-text-secondary\">{description}</p>\n </div>\n </div>\n );\n}\n\nfunction CertificateVerificationCard() {\n const [input, setInput] = useState('');\n const [submitted, setSubmitted] = useState<string[]>([]);\n const { data, loading, error } = useVerifyCertificatesQuery(submitted);\n const parsed = parseSerials(input);\n const tooMany = parsed.length > MAX_VERIFY_BATCH;\n const results = data?.tecVerifyCertificates ?? [];\n\n return (\n <GlassCard className=\"p-5\" data-testid=\"tec-enterprise-verify\">\n <SectionTitle\n icon={BadgeCheck}\n title=\"Certificate verification for ATS partners\"\n description=\"Recruiters and applicant-tracking systems confirm a candidate's TechnoSpam certificate by its serial. The check is public and returns only the course, score, dates and status — never the learner's identity.\"\n />\n <label\n htmlFor=\"tec-verify-serials\"\n className=\"mb-1 block text-xs font-medium text-text-muted\"\n >\n Certificate serials (one per line, up to {MAX_VERIFY_BATCH})\n </label>\n <Textarea\n id=\"tec-verify-serials\"\n value={input}\n onChange={(e) => setInput(e.target.value)}\n placeholder=\"TEC-MH1ABC2D-1A2B3C4D\"\n rows={3}\n className=\"font-mono text-sm\"\n />\n <div className=\"mt-3 flex flex-wrap items-center gap-3\">\n <Button\n size=\"sm\"\n onClick={() => setSubmitted(parsed)}\n disabled={parsed.length === 0 || tooMany || loading}\n >\n {loading ? <Loader2 size={15} className=\"animate-spin\" /> : <Search size={15} />}\n Verify\n </Button>\n <span className=\"text-xs text-text-muted\">\n {tooMany\n ? `Too many serials: ${parsed.length} of at most ${MAX_VERIFY_BATCH}.`\n : `${parsed.length} serial${parsed.length === 1 ? '' : 's'}`}\n </span>\n </div>\n\n {error ? (\n <div className=\"mt-4\">\n <ErrorView title=\"Couldn't verify\" error={error} />\n </div>\n ) : results.length > 0 ? (\n <ul className=\"mt-4 space-y-2\" aria-label=\"Verification results\">\n {results.map((r) => (\n <li\n key={r.serial}\n className=\"flex flex-col gap-1 rounded-lg border border-border-seam bg-bg-sunken p-3 sm:flex-row sm:items-center sm:justify-between\"\n >\n <div className=\"min-w-0\">\n <div className=\"truncate font-mono text-sm text-text-primary\">{r.serial}</div>\n <div className=\"mt-0.5 text-xs text-text-muted\">\n {r.status === 'NOT_FOUND'\n ? 'No TechnoSpam certificate has this serial.'\n : `${r.courseTitle ?? 'Course'} · score ${r.score ?? 0} · issued ${formatDate(r.issuedAt)}${\n r.revokedAt ? ` · revoked ${formatDate(r.revokedAt)}` : ''\n }`}\n </div>\n </div>\n <StatusBadge status={r.status} />\n </li>\n ))}\n </ul>\n ) : null}\n\n <details className=\"mt-5 rounded-lg border border-border-seam p-3\">\n <summary className=\"cursor-pointer text-sm font-medium text-text-primary\">\n Integration details for ATS partners\n </summary>\n <p className=\"mt-2 text-sm text-text-secondary\">\n No API key is needed. Batch checks use <code>tecVerifyCertificates(serials: [...])</code>{' '}\n with up to {MAX_VERIFY_BATCH} serials per request.\n </p>\n <pre className=\"mt-2 overflow-x-auto rounded-md bg-bg-sunken p-3 text-xs text-text-secondary\">\n {atsVerificationSnippet(PARTNER_ENDPOINT)}\n </pre>\n </details>\n </GlassCard>\n );\n}\n\nfunction downloadJson(fileName: string, payload: unknown) {\n const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement('a');\n anchor.href = url;\n anchor.download = fileName;\n document.body.appendChild(anchor);\n anchor.click();\n anchor.remove();\n URL.revokeObjectURL(url);\n}\n\nfunction XapiExportCard() {\n const { data: coursesData } = useCoursesQuery({});\n const courses = coursesData?.tecListCourses?.items ?? [];\n const [courseId, setCourseId] = useState(ALL_COURSES);\n const [from, setFrom] = useState('');\n const [to, setTo] = useState('');\n const [lastCount, setLastCount] = useState<number | null>(null);\n const { run, running, error } = useXapiExport();\n\n const handleExport = async () => {\n const course = courses.find((c) => c.id === courseId);\n const result = await run({\n courseId: course?.id,\n since: dayStartIso(from),\n until: nextDayStartIso(to),\n });\n if (!result) return;\n setLastCount(result.statements.length);\n if (result.statements.length === 0) {\n notify.info('Nothing to export', 'No learner activity in this range.');\n return;\n }\n downloadJson(xapiExportFileName(new Date(), course?.slug), result.statements);\n notify.success(\n 'xAPI export downloaded',\n `${result.statements.length} statement${result.statements.length === 1 ? '' : 's'}${\n result.truncated ? ' (first 5,000 — narrow the date range for the rest)' : ''\n }.`\n );\n };\n\n return (\n <GlassCard className=\"p-5\" data-testid=\"tec-enterprise-xapi\">\n <SectionTitle\n icon={FileJson}\n title=\"xAPI learning-record export (LMS / LRS / HRIS)\"\n description=\"Download learner activity as standard xAPI 1.0.3 statements — enrollments, lesson and course completions, certificates and revocations — ready to POST to any Learning Record Store. Statement ids are stable, so re-importing the same range is safe.\"\n />\n <div className=\"flex flex-wrap items-end gap-3\">\n <div className=\"min-w-48\">\n <label\n className=\"mb-1 block text-xs font-medium text-text-muted\"\n htmlFor=\"tec-xapi-course\"\n >\n Course\n </label>\n <Select value={courseId} onValueChange={setCourseId}>\n <SelectTrigger id=\"tec-xapi-course\" className=\"w-56\">\n <SelectValue placeholder=\"All courses\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL_COURSES}>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 <div>\n <label className=\"mb-1 block text-xs font-medium text-text-muted\" htmlFor=\"tec-xapi-from\">\n From\n </label>\n <Input\n id=\"tec-xapi-from\"\n type=\"date\"\n value={from}\n max={to || undefined}\n onChange={(e) => setFrom(e.target.value)}\n className=\"w-40\"\n />\n </div>\n <div>\n <label className=\"mb-1 block text-xs font-medium text-text-muted\" htmlFor=\"tec-xapi-to\">\n To\n </label>\n <Input\n id=\"tec-xapi-to\"\n type=\"date\"\n value={to}\n min={from || undefined}\n onChange={(e) => setTo(e.target.value)}\n className=\"w-40\"\n />\n </div>\n <Button size=\"sm\" onClick={handleExport} disabled={running}>\n {running ? <Loader2 size={15} className=\"animate-spin\" /> : <Download size={15} />}\n Export statements\n </Button>\n </div>\n {lastCount !== null && !error ? (\n <p className=\"mt-3 text-xs text-text-muted\" role=\"status\">\n Last export: {lastCount} statement{lastCount === 1 ? '' : 's'}.\n </p>\n ) : null}\n {error ? (\n <div className=\"mt-4\">\n <ErrorView title=\"Couldn't export learning records\" error={error} />\n </div>\n ) : null}\n </GlassCard>\n );\n}\n\n/** Refresh while a run is in progress or the first one is pending. */\nconst LRS_POLL_ACTIVE_MS = 5000;\n/** Refresh while connected otherwise, so scheduled runs show up without a reload. */\nconst LRS_POLL_IDLE_MS = 30000;\nconst EMPTY_LRS_FORM: LrsFormValues = { endpoint: '', key: '', secret: '' };\n\nfunction formatDateTime(value: string | null | undefined): string {\n if (!value) return 'Never';\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? 'Never' : date.toLocaleString();\n}\n\nfunction LrsField({\n id,\n label,\n error,\n children,\n}: {\n id: string;\n label: string;\n error?: string;\n children: ReactNode;\n}) {\n return (\n <div className=\"min-w-0\">\n <label htmlFor={id} className=\"mb-1 block text-xs font-medium text-text-muted\">\n {label}\n </label>\n {children}\n {error ? (\n <p id={`${id}-error`} className=\"mt-1 text-xs text-status-error-text\" role=\"alert\">\n {error}\n </p>\n ) : null}\n </div>\n );\n}\n\nfunction LrsConnectForm({\n busy,\n onConnect,\n}: {\n busy: boolean;\n onConnect: (values: LrsFormValues) => Promise<boolean>;\n}) {\n const [values, setValues] = useState<LrsFormValues>(EMPTY_LRS_FORM);\n const [errors, setErrors] = useState<LrsFormErrors>({});\n\n const set = (field: keyof LrsFormValues) => (e: ChangeEvent<HTMLInputElement>) =>\n setValues((v) => ({ ...v, [field]: e.target.value }));\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault();\n const found = validateLrsForm(values);\n setErrors(found);\n if (Object.keys(found).length > 0) return;\n const ok = await onConnect(values);\n if (ok) setValues(EMPTY_LRS_FORM);\n };\n\n return (\n <form onSubmit={handleSubmit} noValidate aria-label=\"Connect a Learning Record Store\">\n <div className=\"grid gap-3 sm:grid-cols-3\">\n <LrsField id=\"tec-lrs-endpoint\" label=\"LRS endpoint\" error={errors.endpoint}>\n <Input\n id=\"tec-lrs-endpoint\"\n type=\"url\"\n inputMode=\"url\"\n autoComplete=\"off\"\n placeholder=\"https://lrs.example.com/data/xAPI\"\n value={values.endpoint}\n onChange={set('endpoint')}\n aria-invalid={Boolean(errors.endpoint)}\n aria-describedby={errors.endpoint ? 'tec-lrs-endpoint-error' : undefined}\n />\n </LrsField>\n <LrsField id=\"tec-lrs-key\" label=\"Key\" error={errors.key}>\n <Input\n id=\"tec-lrs-key\"\n autoComplete=\"off\"\n value={values.key}\n onChange={set('key')}\n aria-invalid={Boolean(errors.key)}\n aria-describedby={errors.key ? 'tec-lrs-key-error' : undefined}\n />\n </LrsField>\n <LrsField id=\"tec-lrs-secret\" label=\"Secret\" error={errors.secret}>\n <Input\n id=\"tec-lrs-secret\"\n type=\"password\"\n autoComplete=\"new-password\"\n value={values.secret}\n onChange={set('secret')}\n aria-invalid={Boolean(errors.secret)}\n aria-describedby={errors.secret ? 'tec-lrs-secret-error' : undefined}\n />\n </LrsField>\n </div>\n <div className=\"mt-3 flex flex-wrap items-center gap-3\">\n <Button size=\"sm\" type=\"submit\" disabled={busy}>\n {busy ? <Loader2 size={15} className=\"animate-spin\" /> : <Send size={15} />}\n Connect LRS\n </Button>\n <span className=\"text-xs text-text-muted\">\n The secret is stored encrypted in your workspace integrations, never in the TechnoSpam\n database.\n </span>\n </div>\n </form>\n );\n}\n\nfunction LrsStat({ label, value }: { label: string; value: ReactNode }) {\n return (\n <div className=\"min-w-0\">\n <dt className=\"text-xs text-text-muted\">{label}</dt>\n <dd className=\"mt-0.5 truncate text-sm text-text-primary\">{value}</dd>\n </div>\n );\n}\n\nfunction LrsSyncCard() {\n const { data, loading, error, startPolling, stopPolling } = useXapiLrsSyncQuery();\n const sync = data?.tecXapiLrsSync ?? null;\n const { data: connectionData } = useLrsConnectionQuery(sync?.connectionId);\n const { connect, disconnect, syncNow, busy } = useXapiLrsActions();\n const connection = connectionData?.getConnection ?? null;\n const endpoint =\n typeof connection?.config?.['baseUrl'] === 'string' ? connection.config['baseUrl'] : null;\n\n // Keep the status current while an LRS is connected: quickly while a run is\n // in progress or the first one is pending, slowly between scheduled runs.\n const pollMs = !sync\n ? null\n : sync.state === 'RUNNING' || sync.state === 'NEVER_RUN'\n ? LRS_POLL_ACTIVE_MS\n : LRS_POLL_IDLE_MS;\n useEffect(() => {\n if (pollMs === null) return undefined;\n startPolling(pollMs);\n return () => stopPolling();\n }, [pollMs, startPolling, stopPolling]);\n\n const handleConnect = async (values: LrsFormValues) => {\n const normalized = normalizeLrsEndpoint(values.endpoint);\n const result = await connect({\n name: lrsConnectionName(normalized),\n endpoint: normalized,\n key: values.key.trim(),\n secret: values.secret,\n });\n return Boolean(result);\n };\n\n return (\n <GlassCard className=\"p-5\" data-testid=\"tec-enterprise-lrs\">\n <SectionTitle\n icon={RefreshCw}\n title=\"Automatic sync to your LRS / HRIS\"\n description=\"Connect a Learning Record Store (or an LMS or HRIS that accepts xAPI) once, and new learner activity is sent to it as xAPI 1.0.3 statements every few minutes. The first sync sends the full history.\"\n />\n {loading && !data ? (\n <div className=\"flex items-center gap-2 text-sm text-text-muted\">\n <Loader2 size={15} className=\"animate-spin\" /> Loading sync status…\n </div>\n ) : error ? (\n <ErrorView title=\"Couldn't load the LRS sync\" error={error} />\n ) : sync ? (\n <div>\n <div className=\"flex flex-wrap items-center gap-3\">\n <StatusBadge status={sync.state} />\n <span className=\"text-sm text-text-secondary\" role=\"status\">\n {lrsSyncSummary(sync.state, sync.consecutiveFailures)}\n </span>\n </div>\n <dl className=\"mt-4 grid gap-3 sm:grid-cols-3\">\n <LrsStat\n label=\"Endpoint\"\n value={<span className=\"font-mono\">{endpoint ?? connection?.name ?? '—'}</span>}\n />\n <LrsStat label=\"Last sync\" value={formatDateTime(sync.lastSyncAt)} />\n <LrsStat label=\"Last success\" value={formatDateTime(sync.lastSuccessAt)} />\n <LrsStat label=\"Statements sent\" value={sync.statementsPushed.toLocaleString()} />\n <LrsStat label=\"Next sync\" value={formatDateTime(sync.nextAttemptAt)} />\n <LrsStat label=\"Interval\" value={`Every ${sync.intervalMinutes} min`} />\n </dl>\n {sync.lastError ? (\n <div\n className=\"mt-4 rounded-lg border border-status-error-border bg-status-error-bg-subtle p-3 text-sm text-text-primary\"\n data-testid=\"tec-lrs-last-error\"\n >\n <div className=\"text-xs font-medium text-status-error-text\">\n Last error · {formatDateTime(sync.lastErrorAt)}\n </div>\n <div className=\"mt-1 break-words\">{sync.lastError}</div>\n </div>\n ) : null}\n <div className=\"mt-4 flex flex-wrap gap-2\">\n <Button size=\"sm\" onClick={() => void syncNow()} disabled={busy !== null}>\n {busy === 'sync' ? (\n <Loader2 size={15} className=\"animate-spin\" />\n ) : (\n <RefreshCw size={15} />\n )}\n Sync now\n </Button>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => void disconnect(sync.connectionId)}\n disabled={busy !== null}\n >\n {busy === 'disconnect' ? (\n <Loader2 size={15} className=\"animate-spin\" />\n ) : (\n <Unplug size={15} />\n )}\n Disconnect\n </Button>\n </div>\n </div>\n ) : (\n <LrsConnectForm busy={busy === 'connect'} onConnect={handleConnect} />\n )}\n </GlassCard>\n );\n}\n\nfunction IdentityCard() {\n return (\n <GlassCard className=\"p-5\">\n <SectionTitle\n icon={KeyRound}\n title=\"Single sign-on and user provisioning\"\n description=\"SAML single sign-on and SCIM 2.0 user provisioning come from the Burdenoff platform identity service that TechnoSpam signs in with, so learners provisioned by your identity provider or HRIS arrive with the same account id used in certificates and xAPI statements.\"\n />\n </GlassCard>\n );\n}\n\n/**\n * Enterprise connectors (BOFF-4430): ATS certificate verification, xAPI\n * export for LMS / LRS / HRIS, the scheduled push to a connected LRS\n * (BOFF-10575), and a pointer to platform SSO/SCIM.\n */\nexport function EnterpriseIntegrationsTab() {\n return (\n <div className=\"space-y-4\">\n <CertificateVerificationCard />\n <XapiExportCard />\n <LrsSyncCard />\n <IdentityCard />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"EnterpriseIntegrationsTab.js","names":[],"sources":["../../../../src/technospam/components/admin/EnterpriseIntegrationsTab.tsx"],"sourcesContent":["import { useEffect, useState, type ChangeEvent, type FormEvent, type ReactNode } from 'react';\nimport {\n BadgeCheck,\n Download,\n FileJson,\n KeyRound,\n Loader2,\n RefreshCw,\n Search,\n Send,\n Unplug,\n} from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\n\nimport {\n MAX_VERIFY_BATCH,\n useCoursesQuery,\n useLrsConnectionQuery,\n useVerifyCertificatesQuery,\n useXapiExport,\n useXapiLrsActions,\n useXapiLrsSyncQuery,\n} from '../../api/technospamApi';\nimport {\n type LrsFormErrors,\n type LrsFormValues,\n atsVerificationSnippet,\n dayStartIso,\n lrsConnectionName,\n lrsSyncSummary,\n nextDayStartIso,\n normalizeLrsEndpoint,\n parseSerials,\n validateLrsForm,\n xapiExportFileName,\n} from '../../lib/enterprise';\nimport { notify } from '../../lib/toast';\nimport { formatDate } from '../../utils/format';\nimport { ErrorView } from '../StateViews';\nimport { StatusBadge } from '../StatusBadge';\n\n/** Public workspaces gateway ATS partners call (production). */\nconst PARTNER_ENDPOINT = 'https://graphqlworkspaces.burdenoff.com/workspaces/graphql';\nconst ALL_COURSES = '__all__';\n\nfunction SectionTitle({\n icon: Icon,\n title,\n description,\n}: {\n icon: typeof BadgeCheck;\n title: string;\n description: string;\n}) {\n return (\n <div className=\"mb-4 flex items-start gap-3\">\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-accent-primary/10 text-accent-primary\">\n <Icon size={18} />\n </span>\n <div className=\"min-w-0\">\n <h2 className=\"text-base font-semibold text-text-primary\">{title}</h2>\n <p className=\"mt-0.5 text-sm text-text-secondary\">{description}</p>\n </div>\n </div>\n );\n}\n\nfunction CertificateVerificationCard() {\n const [input, setInput] = useState('');\n const [submitted, setSubmitted] = useState<string[]>([]);\n const { data, loading, error } = useVerifyCertificatesQuery(submitted);\n const parsed = parseSerials(input);\n const tooMany = parsed.length > MAX_VERIFY_BATCH;\n const results = data?.tecVerifyCertificates ?? [];\n\n return (\n <GlassCard className=\"p-5\" data-testid=\"tec-enterprise-verify\">\n <SectionTitle\n icon={BadgeCheck}\n title=\"Certificate verification for ATS partners\"\n description=\"Recruiters and applicant-tracking systems confirm a candidate's TechnoSpam certificate by its serial. The check is public and returns only the course, score, dates and status — never the learner's identity.\"\n />\n <label\n htmlFor=\"tec-verify-serials\"\n className=\"mb-1 block text-xs font-medium text-text-muted\"\n >\n Certificate serials (one per line, up to {MAX_VERIFY_BATCH})\n </label>\n <Textarea\n id=\"tec-verify-serials\"\n value={input}\n onChange={(e) => setInput(e.target.value)}\n placeholder=\"TEC-MH1ABC2D-1A2B3C4D\"\n rows={3}\n className=\"font-mono text-sm\"\n />\n <div className=\"mt-3 flex flex-wrap items-center gap-3\">\n <Button\n size=\"sm\"\n onClick={() => setSubmitted(parsed)}\n disabled={parsed.length === 0 || tooMany || loading}\n >\n {loading ? <Loader2 size={15} className=\"animate-spin\" /> : <Search size={15} />}\n Verify\n </Button>\n <span className=\"text-xs text-text-muted\">\n {tooMany\n ? `Too many serials: ${parsed.length} of at most ${MAX_VERIFY_BATCH}.`\n : `${parsed.length} serial${parsed.length === 1 ? '' : 's'}`}\n </span>\n </div>\n\n {error ? (\n <div className=\"mt-4\">\n <ErrorView title=\"Couldn't verify\" error={error} />\n </div>\n ) : results.length > 0 ? (\n <ul className=\"mt-4 space-y-2\" aria-label=\"Verification results\">\n {results.map((r) => (\n <li\n key={r.serial}\n className=\"flex flex-col gap-1 rounded-lg border border-border-seam bg-bg-sunken p-3 sm:flex-row sm:items-center sm:justify-between\"\n >\n <div className=\"min-w-0\">\n <div className=\"truncate font-mono text-sm text-text-primary\">{r.serial}</div>\n <div className=\"mt-0.5 text-xs text-text-muted\">\n {r.status === 'NOT_FOUND'\n ? 'No TechnoSpam certificate has this serial.'\n : `${r.courseTitle ?? 'Course'} · score ${r.score ?? 0} · issued ${formatDate(r.issuedAt)}${\n r.revokedAt ? ` · revoked ${formatDate(r.revokedAt)}` : ''\n }`}\n </div>\n </div>\n <StatusBadge status={r.status} />\n </li>\n ))}\n </ul>\n ) : null}\n\n <details className=\"mt-5 rounded-lg border border-border-seam p-3\">\n <summary className=\"cursor-pointer text-sm font-medium text-text-primary\">\n Integration details for ATS partners\n </summary>\n <p className=\"mt-2 text-sm text-text-secondary\">\n No API key is needed. Batch checks use <code>tecVerifyCertificates(serials: [...])</code>{' '}\n with up to {MAX_VERIFY_BATCH} serials per request.\n </p>\n <pre className=\"mt-2 overflow-x-auto rounded-md bg-bg-sunken p-3 text-xs text-text-secondary\">\n {atsVerificationSnippet(PARTNER_ENDPOINT)}\n </pre>\n </details>\n </GlassCard>\n );\n}\n\nfunction downloadJson(fileName: string, payload: unknown) {\n const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement('a');\n anchor.href = url;\n anchor.download = fileName;\n document.body.appendChild(anchor);\n anchor.click();\n anchor.remove();\n URL.revokeObjectURL(url);\n}\n\nfunction XapiExportCard() {\n const { data: coursesData } = useCoursesQuery({});\n const courses = coursesData?.courses?.items ?? [];\n const [courseId, setCourseId] = useState(ALL_COURSES);\n const [from, setFrom] = useState('');\n const [to, setTo] = useState('');\n const [lastCount, setLastCount] = useState<number | null>(null);\n const { run, running, error } = useXapiExport();\n\n const handleExport = async () => {\n const course = courses.find((c) => c.id === courseId);\n const result = await run({\n courseId: course?.id,\n since: dayStartIso(from),\n until: nextDayStartIso(to),\n });\n if (!result) return;\n setLastCount(result.statements.length);\n if (result.statements.length === 0) {\n notify.info('Nothing to export', 'No learner activity in this range.');\n return;\n }\n downloadJson(xapiExportFileName(new Date(), course?.slug), result.statements);\n notify.success(\n 'xAPI export downloaded',\n `${result.statements.length} statement${result.statements.length === 1 ? '' : 's'}${\n result.truncated ? ' (first 5,000 — narrow the date range for the rest)' : ''\n }.`\n );\n };\n\n return (\n <GlassCard className=\"p-5\" data-testid=\"tec-enterprise-xapi\">\n <SectionTitle\n icon={FileJson}\n title=\"xAPI learning-record export (LMS / LRS / HRIS)\"\n description=\"Download learner activity as standard xAPI 1.0.3 statements — enrollments, lesson and course completions, certificates and revocations — ready to POST to any Learning Record Store. Statement ids are stable, so re-importing the same range is safe.\"\n />\n <div className=\"flex flex-wrap items-end gap-3\">\n <div className=\"min-w-48\">\n <label\n className=\"mb-1 block text-xs font-medium text-text-muted\"\n htmlFor=\"tec-xapi-course\"\n >\n Course\n </label>\n <Select value={courseId} onValueChange={setCourseId}>\n <SelectTrigger id=\"tec-xapi-course\" className=\"w-56\">\n <SelectValue placeholder=\"All courses\" />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value={ALL_COURSES}>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 <div>\n <label className=\"mb-1 block text-xs font-medium text-text-muted\" htmlFor=\"tec-xapi-from\">\n From\n </label>\n <Input\n id=\"tec-xapi-from\"\n type=\"date\"\n value={from}\n max={to || undefined}\n onChange={(e) => setFrom(e.target.value)}\n className=\"w-40\"\n />\n </div>\n <div>\n <label className=\"mb-1 block text-xs font-medium text-text-muted\" htmlFor=\"tec-xapi-to\">\n To\n </label>\n <Input\n id=\"tec-xapi-to\"\n type=\"date\"\n value={to}\n min={from || undefined}\n onChange={(e) => setTo(e.target.value)}\n className=\"w-40\"\n />\n </div>\n <Button size=\"sm\" onClick={handleExport} disabled={running}>\n {running ? <Loader2 size={15} className=\"animate-spin\" /> : <Download size={15} />}\n Export statements\n </Button>\n </div>\n {lastCount !== null && !error ? (\n <p className=\"mt-3 text-xs text-text-muted\" role=\"status\">\n Last export: {lastCount} statement{lastCount === 1 ? '' : 's'}.\n </p>\n ) : null}\n {error ? (\n <div className=\"mt-4\">\n <ErrorView title=\"Couldn't export learning records\" error={error} />\n </div>\n ) : null}\n </GlassCard>\n );\n}\n\n/** Refresh while a run is in progress or the first one is pending. */\nconst LRS_POLL_ACTIVE_MS = 5000;\n/** Refresh while connected otherwise, so scheduled runs show up without a reload. */\nconst LRS_POLL_IDLE_MS = 30000;\nconst EMPTY_LRS_FORM: LrsFormValues = { endpoint: '', key: '', secret: '' };\n\nfunction formatDateTime(value: string | null | undefined): string {\n if (!value) return 'Never';\n const date = new Date(value);\n return Number.isNaN(date.getTime()) ? 'Never' : date.toLocaleString();\n}\n\nfunction LrsField({\n id,\n label,\n error,\n children,\n}: {\n id: string;\n label: string;\n error?: string;\n children: ReactNode;\n}) {\n return (\n <div className=\"min-w-0\">\n <label htmlFor={id} className=\"mb-1 block text-xs font-medium text-text-muted\">\n {label}\n </label>\n {children}\n {error ? (\n <p id={`${id}-error`} className=\"mt-1 text-xs text-status-error-text\" role=\"alert\">\n {error}\n </p>\n ) : null}\n </div>\n );\n}\n\nfunction LrsConnectForm({\n busy,\n onConnect,\n}: {\n busy: boolean;\n onConnect: (values: LrsFormValues) => Promise<boolean>;\n}) {\n const [values, setValues] = useState<LrsFormValues>(EMPTY_LRS_FORM);\n const [errors, setErrors] = useState<LrsFormErrors>({});\n\n const set = (field: keyof LrsFormValues) => (e: ChangeEvent<HTMLInputElement>) =>\n setValues((v) => ({ ...v, [field]: e.target.value }));\n\n const handleSubmit = async (e: FormEvent) => {\n e.preventDefault();\n const found = validateLrsForm(values);\n setErrors(found);\n if (Object.keys(found).length > 0) return;\n const ok = await onConnect(values);\n if (ok) setValues(EMPTY_LRS_FORM);\n };\n\n return (\n <form onSubmit={handleSubmit} noValidate aria-label=\"Connect a Learning Record Store\">\n <div className=\"grid gap-3 sm:grid-cols-3\">\n <LrsField id=\"tec-lrs-endpoint\" label=\"LRS endpoint\" error={errors.endpoint}>\n <Input\n id=\"tec-lrs-endpoint\"\n type=\"url\"\n inputMode=\"url\"\n autoComplete=\"off\"\n placeholder=\"https://lrs.example.com/data/xAPI\"\n value={values.endpoint}\n onChange={set('endpoint')}\n aria-invalid={Boolean(errors.endpoint)}\n aria-describedby={errors.endpoint ? 'tec-lrs-endpoint-error' : undefined}\n />\n </LrsField>\n <LrsField id=\"tec-lrs-key\" label=\"Key\" error={errors.key}>\n <Input\n id=\"tec-lrs-key\"\n autoComplete=\"off\"\n value={values.key}\n onChange={set('key')}\n aria-invalid={Boolean(errors.key)}\n aria-describedby={errors.key ? 'tec-lrs-key-error' : undefined}\n />\n </LrsField>\n <LrsField id=\"tec-lrs-secret\" label=\"Secret\" error={errors.secret}>\n <Input\n id=\"tec-lrs-secret\"\n type=\"password\"\n autoComplete=\"new-password\"\n value={values.secret}\n onChange={set('secret')}\n aria-invalid={Boolean(errors.secret)}\n aria-describedby={errors.secret ? 'tec-lrs-secret-error' : undefined}\n />\n </LrsField>\n </div>\n <div className=\"mt-3 flex flex-wrap items-center gap-3\">\n <Button size=\"sm\" type=\"submit\" disabled={busy}>\n {busy ? <Loader2 size={15} className=\"animate-spin\" /> : <Send size={15} />}\n Connect LRS\n </Button>\n <span className=\"text-xs text-text-muted\">\n The secret is stored encrypted in your workspace integrations, never in the TechnoSpam\n database.\n </span>\n </div>\n </form>\n );\n}\n\nfunction LrsStat({ label, value }: { label: string; value: ReactNode }) {\n return (\n <div className=\"min-w-0\">\n <dt className=\"text-xs text-text-muted\">{label}</dt>\n <dd className=\"mt-0.5 truncate text-sm text-text-primary\">{value}</dd>\n </div>\n );\n}\n\nfunction LrsSyncCard() {\n const { data, loading, error, startPolling, stopPolling } = useXapiLrsSyncQuery();\n const sync = data?.tecXapiLrsSync ?? null;\n const { data: connectionData } = useLrsConnectionQuery(sync?.connectionId);\n const { connect, disconnect, syncNow, busy } = useXapiLrsActions();\n const connection = connectionData?.getConnection ?? null;\n const endpoint =\n typeof connection?.config?.['baseUrl'] === 'string' ? connection.config['baseUrl'] : null;\n\n // Keep the status current while an LRS is connected: quickly while a run is\n // in progress or the first one is pending, slowly between scheduled runs.\n const pollMs = !sync\n ? null\n : sync.state === 'RUNNING' || sync.state === 'NEVER_RUN'\n ? LRS_POLL_ACTIVE_MS\n : LRS_POLL_IDLE_MS;\n useEffect(() => {\n if (pollMs === null) return undefined;\n startPolling(pollMs);\n return () => stopPolling();\n }, [pollMs, startPolling, stopPolling]);\n\n const handleConnect = async (values: LrsFormValues) => {\n const normalized = normalizeLrsEndpoint(values.endpoint);\n const result = await connect({\n name: lrsConnectionName(normalized),\n endpoint: normalized,\n key: values.key.trim(),\n secret: values.secret,\n });\n return Boolean(result);\n };\n\n return (\n <GlassCard className=\"p-5\" data-testid=\"tec-enterprise-lrs\">\n <SectionTitle\n icon={RefreshCw}\n title=\"Automatic sync to your LRS / HRIS\"\n description=\"Connect a Learning Record Store (or an LMS or HRIS that accepts xAPI) once, and new learner activity is sent to it as xAPI 1.0.3 statements every few minutes. The first sync sends the full history.\"\n />\n {loading && !data ? (\n <div className=\"flex items-center gap-2 text-sm text-text-muted\">\n <Loader2 size={15} className=\"animate-spin\" /> Loading sync status…\n </div>\n ) : error ? (\n <ErrorView title=\"Couldn't load the LRS sync\" error={error} />\n ) : sync ? (\n <div>\n <div className=\"flex flex-wrap items-center gap-3\">\n <StatusBadge status={sync.state} />\n <span className=\"text-sm text-text-secondary\" role=\"status\">\n {lrsSyncSummary(sync.state, sync.consecutiveFailures)}\n </span>\n </div>\n <dl className=\"mt-4 grid gap-3 sm:grid-cols-3\">\n <LrsStat\n label=\"Endpoint\"\n value={<span className=\"font-mono\">{endpoint ?? connection?.name ?? '—'}</span>}\n />\n <LrsStat label=\"Last sync\" value={formatDateTime(sync.lastSyncAt)} />\n <LrsStat label=\"Last success\" value={formatDateTime(sync.lastSuccessAt)} />\n <LrsStat label=\"Statements sent\" value={sync.statementsPushed.toLocaleString()} />\n <LrsStat label=\"Next sync\" value={formatDateTime(sync.nextAttemptAt)} />\n <LrsStat label=\"Interval\" value={`Every ${sync.intervalMinutes} min`} />\n </dl>\n {sync.lastError ? (\n <div\n className=\"mt-4 rounded-lg border border-status-error-border bg-status-error-bg-subtle p-3 text-sm text-text-primary\"\n data-testid=\"tec-lrs-last-error\"\n >\n <div className=\"text-xs font-medium text-status-error-text\">\n Last error · {formatDateTime(sync.lastErrorAt)}\n </div>\n <div className=\"mt-1 break-words\">{sync.lastError}</div>\n </div>\n ) : null}\n <div className=\"mt-4 flex flex-wrap gap-2\">\n <Button size=\"sm\" onClick={() => void syncNow()} disabled={busy !== null}>\n {busy === 'sync' ? (\n <Loader2 size={15} className=\"animate-spin\" />\n ) : (\n <RefreshCw size={15} />\n )}\n Sync now\n </Button>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => void disconnect(sync.connectionId)}\n disabled={busy !== null}\n >\n {busy === 'disconnect' ? (\n <Loader2 size={15} className=\"animate-spin\" />\n ) : (\n <Unplug size={15} />\n )}\n Disconnect\n </Button>\n </div>\n </div>\n ) : (\n <LrsConnectForm busy={busy === 'connect'} onConnect={handleConnect} />\n )}\n </GlassCard>\n );\n}\n\nfunction IdentityCard() {\n return (\n <GlassCard className=\"p-5\">\n <SectionTitle\n icon={KeyRound}\n title=\"Single sign-on and user provisioning\"\n description=\"SAML single sign-on and SCIM 2.0 user provisioning come from the Burdenoff platform identity service that TechnoSpam signs in with, so learners provisioned by your identity provider or HRIS arrive with the same account id used in certificates and xAPI statements.\"\n />\n </GlassCard>\n );\n}\n\n/**\n * Enterprise connectors (BOFF-4430): ATS certificate verification, xAPI\n * export for LMS / LRS / HRIS, the scheduled push to a connected LRS\n * (BOFF-10575), and a pointer to platform SSO/SCIM.\n */\nexport function EnterpriseIntegrationsTab() {\n return (\n <div className=\"space-y-4\">\n <CertificateVerificationCard />\n <XapiExportCard />\n <LrsSyncCard />\n <IdentityCard />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -221,7 +221,7 @@ function qe({ lesson: e, onSaved: t, onCancel: n }) {
|
|
|
221
221
|
});
|
|
222
222
|
}
|
|
223
223
|
function Je({ courseId: e }) {
|
|
224
|
-
let { data: t, loading: n, refetch: r } = k(e), [i, { loading: a }] = T(), [o] = R(), [c, l] = V(null), [d, f] = V(null), [p, h] = V(""), [g, _] = V("ARTICLE"), [y, b] = V(""), [x, S] = V(""), [C, w] = V(!1), [ee, E] = V(() => z()), D = [...t?.
|
|
224
|
+
let { data: t, loading: n, refetch: r } = k(e), [i, { loading: a }] = T(), [o] = R(), [c, l] = V(null), [d, f] = V(null), [p, h] = V(""), [g, _] = V("ARTICLE"), [y, b] = V(""), [x, S] = V(""), [C, w] = V(!1), [ee, E] = V(() => z()), D = [...t?.lessons?.items ?? []].sort((e, t) => e.position - t.position), O = async (e) => {
|
|
225
225
|
f(e.id);
|
|
226
226
|
try {
|
|
227
227
|
let t = await o({ variables: {
|
|
@@ -658,7 +658,7 @@ function Ze({ hackathon: e }) {
|
|
|
658
658
|
});
|
|
659
659
|
}
|
|
660
660
|
function Qe() {
|
|
661
|
-
let { data: e, loading: t, error: n, refetch: r } = S({}), [o, { loading: c }] = C(), [l] = j(), [g] = b(), [_, y] = V(!1), [x, w] = V(null), [T, ee] = V(null), [E, D] = V(""), [O, k] = V(""), [A, te] = V(""), [ne, re] = V(""), [M, N] = V("BEGINNER"), [P, F] = V(""), [I, L] = V([]), R = e?.
|
|
661
|
+
let { data: e, loading: t, error: n, refetch: r } = S({}), [o, { loading: c }] = C(), [l] = j(), [g] = b(), [_, y] = V(!1), [x, w] = V(null), [T, ee] = V(null), [E, D] = V(""), [O, k] = V(""), [A, te] = V(""), [ne, re] = V(""), [M, N] = V("BEGINNER"), [P, F] = V(""), [I, L] = V([]), R = e?.courses?.items ?? [], ie = () => {
|
|
662
662
|
D(""), k(""), te(""), re(""), N("BEGINNER"), F(""), L([]), y(!1);
|
|
663
663
|
}, z = async () => {
|
|
664
664
|
let e = E.trim();
|