@burdenoff/microfe-technospam 2026.1102.2 → 2026.1102.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/generated/global-types.d.ts +307 -41
  2. package/dist/generated/global-types.d.ts.map +1 -1
  3. package/dist/generated/wspace-types.d.ts +1730 -8
  4. package/dist/generated/wspace-types.d.ts.map +1 -1
  5. package/dist/stories/fixtures/technospamFixtures.d.ts.map +1 -1
  6. package/dist/technospam/TechnoSpamRoot.d.ts.map +1 -1
  7. package/dist/technospam/TechnoSpamRoot.js +187 -178
  8. package/dist/technospam/TechnoSpamRoot.js.map +1 -1
  9. package/dist/technospam/TechnoSpamRoutes.d.ts.map +1 -1
  10. package/dist/technospam/TechnoSpamRoutes.js +32 -28
  11. package/dist/technospam/TechnoSpamRoutes.js.map +1 -1
  12. package/dist/technospam/api/assessmentApi.d.ts +183 -0
  13. package/dist/technospam/api/assessmentApi.d.ts.map +1 -0
  14. package/dist/technospam/api/assessmentApi.js +278 -0
  15. package/dist/technospam/api/assessmentApi.js.map +1 -0
  16. package/dist/technospam/api/technospamApi.js +141 -141
  17. package/dist/technospam/api/technospamApi.js.map +1 -1
  18. package/dist/technospam/components/CourseAssessments.d.ts +9 -0
  19. package/dist/technospam/components/CourseAssessments.d.ts.map +1 -0
  20. package/dist/technospam/components/CourseAssessments.js +66 -0
  21. package/dist/technospam/components/CourseAssessments.js.map +1 -0
  22. package/dist/technospam/components/MediaCover.js +2 -2
  23. package/dist/technospam/components/MediaCover.js.map +1 -1
  24. package/dist/technospam/components/MobileSectionTabs.d.ts +9 -0
  25. package/dist/technospam/components/MobileSectionTabs.d.ts.map +1 -0
  26. package/dist/technospam/components/MobileSectionTabs.js +43 -0
  27. package/dist/technospam/components/MobileSectionTabs.js.map +1 -0
  28. package/dist/technospam/components/PageLayout.d.ts +7 -1
  29. package/dist/technospam/components/PageLayout.d.ts.map +1 -1
  30. package/dist/technospam/components/PageLayout.js +40 -28
  31. package/dist/technospam/components/PageLayout.js.map +1 -1
  32. package/dist/technospam/components/StatusBadge.d.ts.map +1 -1
  33. package/dist/technospam/components/StatusBadge.js +8 -1
  34. package/dist/technospam/components/StatusBadge.js.map +1 -1
  35. package/dist/technospam/components/admin/AssessmentsAdminTab.d.ts +7 -0
  36. package/dist/technospam/components/admin/AssessmentsAdminTab.d.ts.map +1 -0
  37. package/dist/technospam/components/admin/AssessmentsAdminTab.js +421 -0
  38. package/dist/technospam/components/admin/AssessmentsAdminTab.js.map +1 -0
  39. package/dist/technospam/components/lesson/LessonKindFields.d.ts +3 -1
  40. package/dist/technospam/components/lesson/LessonKindFields.d.ts.map +1 -1
  41. package/dist/technospam/components/lesson/LessonKindFields.js +2 -2
  42. package/dist/technospam/components/lesson/LessonKindFields.js.map +1 -1
  43. package/dist/technospam/components/lesson/SaveCourseOffline.js +6 -6
  44. package/dist/technospam/hooks/useAssessmentIntegrity.d.ts +29 -0
  45. package/dist/technospam/hooks/useAssessmentIntegrity.d.ts.map +1 -0
  46. package/dist/technospam/hooks/useAssessmentIntegrity.js +57 -0
  47. package/dist/technospam/hooks/useAssessmentIntegrity.js.map +1 -0
  48. package/dist/technospam/hooks/usePhoneLayout.d.ts +18 -0
  49. package/dist/technospam/hooks/usePhoneLayout.d.ts.map +1 -0
  50. package/dist/technospam/hooks/usePhoneLayout.js +14 -0
  51. package/dist/technospam/hooks/usePhoneLayout.js.map +1 -0
  52. package/dist/technospam/pages/AdminAdminDashboardPagePage.d.ts.map +1 -1
  53. package/dist/technospam/pages/AdminAdminDashboardPagePage.js +365 -360
  54. package/dist/technospam/pages/AdminAdminDashboardPagePage.js.map +1 -1
  55. package/dist/technospam/pages/ApplicationsProblemsPagePage.d.ts.map +1 -1
  56. package/dist/technospam/pages/ApplicationsProblemsPagePage.js +89 -87
  57. package/dist/technospam/pages/ApplicationsProblemsPagePage.js.map +1 -1
  58. package/dist/technospam/pages/CoursesAssessmentPagePage.d.ts +9 -0
  59. package/dist/technospam/pages/CoursesAssessmentPagePage.d.ts.map +1 -0
  60. package/dist/technospam/pages/CoursesAssessmentPagePage.js +464 -0
  61. package/dist/technospam/pages/CoursesAssessmentPagePage.js.map +1 -0
  62. package/dist/technospam/pages/CoursesCourseDetailsPagePage.d.ts.map +1 -1
  63. package/dist/technospam/pages/CoursesCourseDetailsPagePage.js +213 -211
  64. package/dist/technospam/pages/CoursesCourseDetailsPagePage.js.map +1 -1
  65. package/dist/technospam/pages/CoursesCoursesPagePage.d.ts.map +1 -1
  66. package/dist/technospam/pages/CoursesCoursesPagePage.js +117 -81
  67. package/dist/technospam/pages/CoursesCoursesPagePage.js.map +1 -1
  68. package/dist/technospam/pages/CoursesMyLearningPagePage.js +1 -1
  69. package/dist/technospam/pages/CoursesMyLearningPagePage.js.map +1 -1
  70. package/dist/technospam/pages/DashboardDashboardPagePage.d.ts.map +1 -1
  71. package/dist/technospam/pages/DashboardDashboardPagePage.js +21 -17
  72. package/dist/technospam/pages/DashboardDashboardPagePage.js.map +1 -1
  73. package/dist/technospam/pages/HackathonsHackathonDetailsPagePage.js +3 -3
  74. package/dist/technospam/pages/HackathonsHackathonDetailsPagePage.js.map +1 -1
  75. package/dist/technospam/pages/MentorshipMentorProfilePagePage.js +4 -4
  76. package/dist/technospam/pages/MentorshipMentorProfilePagePage.js.map +1 -1
  77. package/dist/technospam/pages/MentorshipMentorsPagePage.d.ts.map +1 -1
  78. package/dist/technospam/pages/MentorshipMentorsPagePage.js +6 -4
  79. package/dist/technospam/pages/MentorshipMentorsPagePage.js.map +1 -1
  80. package/dist/technospam/pages/MentorshipSessionsPagePage.js +1 -1
  81. package/dist/technospam/pages/MentorshipSessionsPagePage.js.map +1 -1
  82. package/dist/technospam/types/index.d.ts +7 -0
  83. package/dist/technospam/types/index.d.ts.map +1 -1
  84. package/dist/technospam/utils/assessment.d.ts +64 -0
  85. package/dist/technospam/utils/assessment.d.ts.map +1 -0
  86. package/dist/technospam/utils/assessment.js +122 -0
  87. package/dist/technospam/utils/assessment.js.map +1 -0
  88. package/dist/technospam/utils/mobileTabs.d.ts +45 -0
  89. package/dist/technospam/utils/mobileTabs.d.ts.map +1 -0
  90. package/dist/technospam/utils/mobileTabs.js +70 -0
  91. package/dist/technospam/utils/mobileTabs.js.map +1 -0
  92. package/dist/test/matchMedia.d.ts +6 -0
  93. package/dist/test/matchMedia.d.ts.map +1 -0
  94. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"PageLayout.d.ts","sourceRoot":"","sources":["../../../src/technospam/components/PageLayout.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAQvC,UAAU,eAAe;IACvB,KAAK,EAAE,SAAS,CAAC;IACjB,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,WAAW,EACX,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,IAAY,GACb,EAAE,eAAe,+BAwCjB"}
1
+ {"version":3,"file":"PageLayout.d.ts","sourceRoot":"","sources":["../../../src/technospam/components/PageLayout.tsx"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAWhE,UAAU,eAAe;IACvB,KAAK,EAAE,SAAS,CAAC;IACjB,WAAW,CAAC,EAAE,SAAS,CAAC;IACxB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,IAAI,CAAC,EAAE,OAAO,CAAC;IACf;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,WAAW,EACX,OAAO,EACP,QAAQ,EACR,QAAQ,EACR,IAAY,EACZ,aAAqB,GACtB,EAAE,eAAe,+BAyEjB"}
@@ -1,52 +1,64 @@
1
1
  import { useTechnoSpamNavigate as e } from "../providers/TechnoSpamProvider.js";
2
2
  import { useNativeControls as t } from "../hooks/useNativeControls.js";
3
- import { useTr as n } from "../hooks/useTr.js";
4
- import { cn as r } from "../utils/cn.js";
5
- import { ArrowLeft as i } from "lucide-react";
6
- import { jsx as a, jsxs as o } from "react/jsx-runtime";
7
- import { AuroraBackground as s } from "@burdenoff/fe-libs/ui";
3
+ import { usePhoneLayout as n } from "../hooks/usePhoneLayout.js";
4
+ import { useTr as r } from "../hooks/useTr.js";
5
+ import { cn as i } from "../utils/cn.js";
6
+ import { MobileSectionTabs as a } from "./MobileSectionTabs.js";
7
+ import { useCallback as o, useContext as s } from "react";
8
+ import { ArrowLeft as c } from "lucide-react";
9
+ import { jsx as l, jsxs as u } from "react/jsx-runtime";
10
+ import { getApolloContext as d } from "@apollo/client/react";
11
+ import { AuroraBackground as f, PullToRefresh as p } from "@burdenoff/fe-libs/ui";
8
12
  //#region src/technospam/components/PageLayout.tsx
9
- function c({ title: c, description: l, actions: u, children: d, backPath: f, hero: p = !1 }) {
10
- let m = e(), { impact: h } = t(), g = n();
11
- return /* @__PURE__ */ o("div", {
13
+ function m({ title: m, description: h, actions: g, children: _, backPath: v, hero: y = !1, mobileSection: b = !1 }) {
14
+ let x = e(), { impact: S } = t(), C = r(), { isPhone: w, nativeChrome: T } = n(), E = s(d()).client, D = o(async () => {
15
+ E && await E.refetchQueries({ include: "active" });
16
+ }, [E]);
17
+ return /* @__PURE__ */ u("div", {
12
18
  className: "flex h-full flex-col bg-bg-surface",
13
- children: [/* @__PURE__ */ o("div", {
14
- className: r("relative flex flex-col gap-4 overflow-hidden border-b border-border-seam px-6 py-5 sm:flex-row sm:items-start sm:justify-between", p ? "bg-accent-primary/12" : "bg-bg-surface"),
19
+ children: [/* @__PURE__ */ u("div", {
20
+ className: i("relative flex flex-col gap-3 overflow-hidden border-b border-border-seam px-4 py-4 sm:flex-row sm:items-start sm:justify-between sm:gap-4 sm:px-6 sm:py-5", y ? "bg-accent-primary/12" : "bg-bg-surface"),
15
21
  children: [
16
- p ? /* @__PURE__ */ a(s, { intensity: "subtle" }) : null,
17
- /* @__PURE__ */ o("div", {
22
+ y ? /* @__PURE__ */ l(f, { intensity: "subtle" }) : null,
23
+ /* @__PURE__ */ u("div", {
18
24
  className: "relative flex min-w-0 flex-col gap-1",
19
25
  children: [
20
- f ? /* @__PURE__ */ o("button", {
26
+ v ? /* @__PURE__ */ u("button", {
21
27
  type: "button",
22
28
  onClick: () => {
23
- h("light"), m(f);
29
+ S("light"), x(v);
24
30
  },
25
- className: "flex w-fit items-center gap-1 text-sm text-text-secondary transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)]",
26
- children: [/* @__PURE__ */ a(i, { size: 15 }), g("technospam.common.back", "Back")]
31
+ className: "-ml-1 flex min-h-11 w-fit items-center gap-1 px-1 text-sm sm:min-h-0 text-text-secondary transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)]",
32
+ children: [/* @__PURE__ */ l(c, { size: 15 }), C("technospam.common.back", "Back")]
27
33
  }) : null,
28
- /* @__PURE__ */ a("h1", {
29
- className: "truncate text-xl font-semibold text-text-primary sm:text-2xl",
30
- children: c
34
+ /* @__PURE__ */ l("h1", {
35
+ className: "truncate text-2xl font-semibold text-text-primary",
36
+ children: m
31
37
  }),
32
- l ? /* @__PURE__ */ a("p", {
33
- className: "max-w-2xl text-sm text-text-secondary",
34
- children: l
38
+ h ? /* @__PURE__ */ l("p", {
39
+ className: "sr-only max-w-2xl text-sm text-text-secondary sm:not-sr-only",
40
+ children: h
35
41
  }) : null
36
42
  ]
37
43
  }),
38
- u ? /* @__PURE__ */ a("div", {
44
+ g ? /* @__PURE__ */ l("div", {
39
45
  className: "relative flex flex-wrap items-center gap-2",
40
- children: u
46
+ children: g
41
47
  }) : null
42
48
  ]
43
- }), /* @__PURE__ */ a("div", {
44
- className: "flex-1 overflow-y-auto p-6",
45
- children: d
49
+ }), /* @__PURE__ */ u(p, {
50
+ onRefresh: D,
51
+ enabled: T,
52
+ label: C("technospam.common.refreshing", "Refreshing"),
53
+ className: "relative h-auto min-h-0 flex-1 overflow-y-auto p-4 pb-[calc(env(safe-area-inset-bottom)+1rem)] sm:p-6",
54
+ children: [b && w && !T ? /* @__PURE__ */ l("div", {
55
+ className: "mb-4",
56
+ children: /* @__PURE__ */ l(a, {})
57
+ }) : null, _]
46
58
  })]
47
59
  });
48
60
  }
49
61
  //#endregion
50
- export { c as PageLayout };
62
+ export { m as PageLayout };
51
63
 
52
64
  //# sourceMappingURL=PageLayout.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/technospam/components/PageLayout.tsx"],"sourcesContent":["import type { ReactNode } from 'react';\nimport { ArrowLeft } from 'lucide-react';\nimport { AuroraBackground } from '@burdenoff/fe-libs/ui';\nimport { useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport { useNativeControls } from '../hooks/useNativeControls';\nimport { useTr } from '../hooks/useTr';\nimport { cn } from '../utils/cn';\n\ninterface PageLayoutProps {\n title: ReactNode;\n description?: ReactNode;\n actions?: ReactNode;\n children: ReactNode;\n backPath?: string;\n /** Render the page header with the living-glass aurora backdrop (module hero). */\n hero?: boolean;\n}\n\n/**\n * Consistent page chrome for every TechnoSpam module page: a header band\n * (optionally with the aurora hero backdrop), an optional back affordance, an\n * actions slot, and a scrollable body. Mirrors the platform page pattern so\n * TechnoSpam feels native to the shell.\n */\nexport function PageLayout({\n title,\n description,\n actions,\n children,\n backPath,\n hero = false,\n}: PageLayoutProps) {\n const navigate = useTechnoSpamNavigate();\n const { impact } = useNativeControls();\n const tr = useTr();\n\n return (\n <div className=\"flex h-full flex-col bg-bg-surface\">\n <div\n className={cn(\n 'relative flex flex-col gap-4 overflow-hidden border-b border-border-seam px-6 py-5 sm:flex-row sm:items-start sm:justify-between',\n hero ? 'bg-accent-primary/12' : 'bg-bg-surface'\n )}\n >\n {hero ? <AuroraBackground intensity=\"subtle\" /> : null}\n <div className=\"relative flex min-w-0 flex-col gap-1\">\n {backPath ? (\n <button\n type=\"button\"\n onClick={() => {\n void impact('light');\n navigate(backPath);\n }}\n className=\"flex w-fit items-center gap-1 text-sm text-text-secondary transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <ArrowLeft size={15} />\n {tr('technospam.common.back', 'Back')}\n </button>\n ) : null}\n <h1 className=\"truncate text-xl font-semibold text-text-primary sm:text-2xl\">{title}</h1>\n {description ? (\n <p className=\"max-w-2xl text-sm text-text-secondary\">{description}</p>\n ) : null}\n </div>\n {actions ? (\n <div className=\"relative flex flex-wrap items-center gap-2\">{actions}</div>\n ) : null}\n </div>\n <div className=\"flex-1 overflow-y-auto p-6\">{children}</div>\n </div>\n );\n}\n"],"mappings":";;;;;;;;AAwBA,SAAgB,EAAW,EACzB,UACA,gBACA,YACA,aACA,aACA,UAAO,MACW;CAClB,IAAM,IAAW,GAAuB,EAClC,EAAE,cAAW,GAAmB,EAChC,IAAK,GAAO;AAElB,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GACE,WAAW,EACT,oIACA,IAAO,yBAAyB,gBACjC;aAJH;IAMG,IAAO,kBAAC,GAAD,EAAkB,WAAU,UAAW,CAAA,GAAG;IAClD,kBAAC,OAAD;KAAK,WAAU;eAAf;MACG,IACC,kBAAC,UAAD;OACE,MAAK;OACL,eAAe;AAEb,QADK,EAAO,QAAQ,EACpB,EAAS,EAAS;;OAEpB,WAAU;iBANZ,CAQE,kBAAC,GAAD,EAAW,MAAM,IAAM,CAAA,EACtB,EAAG,0BAA0B,OAAO,CAC9B;WACP;MACJ,kBAAC,MAAD;OAAI,WAAU;iBAAgE;OAAW,CAAA;MACxF,IACC,kBAAC,KAAD;OAAG,WAAU;iBAAyC;OAAgB,CAAA,GACpE;MACA;;IACL,IACC,kBAAC,OAAD;KAAK,WAAU;eAA8C;KAAc,CAAA,GACzE;IACA;MACN,kBAAC,OAAD;GAAK,WAAU;GAA8B;GAAe,CAAA,CACxD"}
1
+ {"version":3,"file":"PageLayout.js","names":[],"sources":["../../../src/technospam/components/PageLayout.tsx"],"sourcesContent":["import { useCallback, useContext, type ReactNode } from 'react';\nimport { ArrowLeft } from 'lucide-react';\nimport { getApolloContext } from '@apollo/client/react';\nimport { AuroraBackground, PullToRefresh } from '@burdenoff/fe-libs/ui';\nimport { useTechnoSpamNavigate } from '../providers/TechnoSpamProvider';\nimport { useNativeControls } from '../hooks/useNativeControls';\nimport { usePhoneLayout } from '../hooks/usePhoneLayout';\nimport { useTr } from '../hooks/useTr';\nimport { cn } from '../utils/cn';\nimport { MobileSectionTabs } from './MobileSectionTabs';\n\ninterface PageLayoutProps {\n title: ReactNode;\n description?: ReactNode;\n actions?: ReactNode;\n children: ReactNode;\n backPath?: string;\n /** Render the page header with the living-glass aurora backdrop (module hero). */\n hero?: boolean;\n /**\n * One of the four learner-journey pages (Home, Courses, Practice, Mentors):\n * on a phone-width mobile browser it shows the in-page section switcher that\n * stands in for the installed app's bottom tab bar.\n */\n mobileSection?: boolean;\n}\n\n/**\n * Consistent page chrome for every TechnoSpam module page: a header band\n * (optionally with the aurora hero backdrop), an optional back affordance, an\n * actions slot, and a scrollable body. Mirrors the platform page pattern so\n * TechnoSpam feels native to the shell.\n */\nexport function PageLayout({\n title,\n description,\n actions,\n children,\n backPath,\n hero = false,\n mobileSection = false,\n}: PageLayoutProps) {\n const navigate = useTechnoSpamNavigate();\n const { impact } = useNativeControls();\n const tr = useTr();\n const { isPhone, nativeChrome } = usePhoneLayout();\n // Read the client from context rather than useApolloClient(), which throws\n // without a provider (isolated page tests and stories render without one).\n const client = useContext(getApolloContext()).client;\n\n // Pull-to-refresh re-runs every query the visible page is subscribed to.\n const refresh = useCallback(async () => {\n if (client) await client.refetchQueries({ include: 'active' });\n }, [client]);\n\n return (\n <div className=\"flex h-full flex-col bg-bg-surface\">\n <div\n className={cn(\n 'relative flex flex-col gap-3 overflow-hidden border-b border-border-seam px-4 py-4 sm:flex-row sm:items-start sm:justify-between sm:gap-4 sm:px-6 sm:py-5',\n hero ? 'bg-accent-primary/12' : 'bg-bg-surface'\n )}\n >\n {hero ? <AuroraBackground intensity=\"subtle\" /> : null}\n <div className=\"relative flex min-w-0 flex-col gap-1\">\n {backPath ? (\n <button\n type=\"button\"\n onClick={() => {\n void impact('light');\n navigate(backPath);\n }}\n className=\"-ml-1 flex min-h-11 w-fit items-center gap-1 px-1 text-sm sm:min-h-0 text-text-secondary transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <ArrowLeft size={15} />\n {tr('technospam.common.back', 'Back')}\n </button>\n ) : null}\n <h1 className=\"truncate text-2xl font-semibold text-text-primary\">{title}</h1>\n {description ? (\n // Visually dropped on phones to keep the header compact, but still\n // announced to screen readers.\n <p className=\"sr-only max-w-2xl text-sm text-text-secondary sm:not-sr-only\">\n {description}\n </p>\n ) : null}\n </div>\n {actions ? (\n <div className=\"relative flex flex-wrap items-center gap-2\">{actions}</div>\n ) : null}\n </div>\n {/* The body owns scrolling. In the installed app it also carries the\n native pull-to-refresh gesture; elsewhere PullToRefresh is a plain\n scroller. The bottom padding clears the home indicator on phones\n (the shell already reserves the tab bar's own height). The scroll\n classes are repeated here because PullToRefresh's own copies live in\n fe-libs' dist, which a consumer's Tailwind scan may not cover; without\n them wide content (tab lists, tables) would scroll the whole page\n sideways instead of this body. */}\n <PullToRefresh\n onRefresh={refresh}\n enabled={nativeChrome}\n label={tr('technospam.common.refreshing', 'Refreshing')}\n className=\"relative h-auto min-h-0 flex-1 overflow-y-auto p-4 pb-[calc(env(safe-area-inset-bottom)+1rem)] sm:p-6\"\n >\n {mobileSection && isPhone && !nativeChrome ? (\n <div className=\"mb-4\">\n <MobileSectionTabs />\n </div>\n ) : null}\n {children}\n </PullToRefresh>\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;AAiCA,SAAgB,EAAW,EACzB,UACA,gBACA,YACA,aACA,aACA,UAAO,IACP,mBAAgB,MACE;CAClB,IAAM,IAAW,GAAuB,EAClC,EAAE,cAAW,GAAmB,EAChC,IAAK,GAAO,EACZ,EAAE,YAAS,oBAAiB,GAAgB,EAG5C,IAAS,EAAW,GAAkB,CAAC,CAAC,QAGxC,IAAU,EAAY,YAAY;AACtC,EAAI,KAAQ,MAAM,EAAO,eAAe,EAAE,SAAS,UAAU,CAAC;IAC7D,CAAC,EAAO,CAAC;AAEZ,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,OAAD;GACE,WAAW,EACT,6JACA,IAAO,yBAAyB,gBACjC;aAJH;IAMG,IAAO,kBAAC,GAAD,EAAkB,WAAU,UAAW,CAAA,GAAG;IAClD,kBAAC,OAAD;KAAK,WAAU;eAAf;MACG,IACC,kBAAC,UAAD;OACE,MAAK;OACL,eAAe;AAEb,QADK,EAAO,QAAQ,EACpB,EAAS,EAAS;;OAEpB,WAAU;iBANZ,CAQE,kBAAC,GAAD,EAAW,MAAM,IAAM,CAAA,EACtB,EAAG,0BAA0B,OAAO,CAC9B;WACP;MACJ,kBAAC,MAAD;OAAI,WAAU;iBAAqD;OAAW,CAAA;MAC7E,IAGC,kBAAC,KAAD;OAAG,WAAU;iBACV;OACC,CAAA,GACF;MACA;;IACL,IACC,kBAAC,OAAD;KAAK,WAAU;eAA8C;KAAc,CAAA,GACzE;IACA;MASN,kBAAC,GAAD;GACE,WAAW;GACX,SAAS;GACT,OAAO,EAAG,gCAAgC,aAAa;GACvD,WAAU;aAJZ,CAMG,KAAiB,KAAW,CAAC,IAC5B,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD,EAAqB,CAAA;IACjB,CAAA,GACJ,MACH,EACa;KACZ"}
@@ -1 +1 @@
1
- {"version":3,"file":"StatusBadge.d.ts","sourceRoot":"","sources":["../../../src/technospam/components/StatusBadge.tsx"],"names":[],"mappings":"AAiEA,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,IAAe,GAChB,EAAE;IACD,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;CAC3B,sCAQA"}
1
+ {"version":3,"file":"StatusBadge.d.ts","sourceRoot":"","sources":["../../../src/technospam/components/StatusBadge.tsx"],"names":[],"mappings":"AAyEA,wBAAgB,WAAW,CAAC,EAC1B,MAAM,EACN,IAAe,GAChB,EAAE;IACD,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;CAC3B,sCAQA"}
@@ -37,7 +37,14 @@ var r = {
37
37
  JUDGING: "info",
38
38
  SUBMITTED: "success",
39
39
  DISQUALIFIED: "danger",
40
- WINNER: "success"
40
+ WINNER: "success",
41
+ EXPIRED: "warning",
42
+ NOT_REQUIRED: "neutral",
43
+ MONITORING: "info",
44
+ PENDING_REVIEW: "info",
45
+ VERIFIED: "success",
46
+ FLAGGED: "warning",
47
+ REJECTED: "danger"
41
48
  }, a = {
42
49
  EASY: "success",
43
50
  MEDIUM: "warning",
@@ -1 +1 @@
1
- {"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/technospam/components/StatusBadge.tsx"],"sourcesContent":["import { Badge } from '@burdenoff/fe-libs/ui';\nimport { humanizeEnum } from '../utils/format';\n\ntype Tone = 'neutral' | 'info' | 'success' | 'warning' | 'danger';\n\nconst TONE_CLASS: Record<Tone, string> = {\n neutral: 'bg-bg-sunken text-text-secondary border-border-seam',\n info: 'bg-accent-primary/6 text-accent-primary border-accent-primary/30',\n success: 'bg-status-success-bg-subtle text-status-success-text border-status-success-border',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text border-status-warning-border',\n danger: 'bg-status-error-bg-subtle text-status-error-text border-status-error-border',\n};\n\n/** Maps any Tec* status enum to a semantic tone. Unknown values fall back to neutral. */\nconst STATUS_TONE: Record<string, Tone> = {\n // course / problem\n DRAFT: 'neutral',\n PUBLISHED: 'success',\n ARCHIVED: 'warning',\n // enrollment\n ENROLLED: 'info',\n IN_PROGRESS: 'info',\n COMPLETED: 'success',\n DROPPED: 'warning',\n // certificate\n ISSUED: 'success',\n REVOKED: 'danger',\n // certificate verification (BOFF-4430)\n VALID: 'success',\n NOT_FOUND: 'warning',\n // submission verdict\n PENDING: 'neutral',\n ACCEPTED: 'success',\n WRONG_ANSWER: 'danger',\n TIME_LIMIT_EXCEEDED: 'warning',\n RUNTIME_ERROR: 'danger',\n COMPILE_ERROR: 'danger',\n // mentor\n ACTIVE: 'success',\n PAUSED: 'warning',\n INACTIVE: 'neutral',\n // session\n REQUESTED: 'info',\n CONFIRMED: 'success',\n CANCELLED: 'danger',\n NO_SHOW: 'danger',\n // hackathon\n REGISTRATION_OPEN: 'success',\n JUDGING: 'info',\n // team\n SUBMITTED: 'success',\n DISQUALIFIED: 'danger',\n WINNER: 'success',\n};\n\nconst DIFFICULTY_TONE: Record<string, Tone> = {\n EASY: 'success',\n MEDIUM: 'warning',\n HARD: 'danger',\n BEGINNER: 'success',\n INTERMEDIATE: 'info',\n ADVANCED: 'warning',\n EXPERT: 'danger',\n};\n\nexport function StatusBadge({\n status,\n kind = 'status',\n}: {\n status?: string | null;\n kind?: 'status' | 'level';\n}) {\n if (!status) return null;\n const tone = (kind === 'level' ? DIFFICULTY_TONE[status] : STATUS_TONE[status]) ?? 'neutral';\n return (\n <Badge variant=\"outline\" className={TONE_CLASS[tone]}>\n {humanizeEnum(status)}\n </Badge>\n );\n}\n"],"mappings":";;;;AAKA,IAAM,IAAmC;CACvC,SAAS;CACT,MAAM;CACN,SAAS;CACT,SAAS;CACT,QAAQ;CACT,EAGK,IAAoC;CAExC,OAAO;CACP,WAAW;CACX,UAAU;CAEV,UAAU;CACV,aAAa;CACb,WAAW;CACX,SAAS;CAET,QAAQ;CACR,SAAS;CAET,OAAO;CACP,WAAW;CAEX,SAAS;CACT,UAAU;CACV,cAAc;CACd,qBAAqB;CACrB,eAAe;CACf,eAAe;CAEf,QAAQ;CACR,QAAQ;CACR,UAAU;CAEV,WAAW;CACX,WAAW;CACX,WAAW;CACX,SAAS;CAET,mBAAmB;CACnB,SAAS;CAET,WAAW;CACX,cAAc;CACd,QAAQ;CACT,EAEK,IAAwC;CAC5C,MAAM;CACN,QAAQ;CACR,MAAM;CACN,UAAU;CACV,cAAc;CACd,UAAU;CACV,QAAQ;CACT;AAED,SAAgB,EAAY,EAC1B,WACA,UAAO,YAIN;AAGD,QAFK,IAGH,kBAAC,GAAD;EAAO,SAAQ;EAAU,WAAW,GAFxB,MAAS,UAAU,EAAgB,KAAU,EAAY,OAAY;YAG9E,EAAa,EAAO;EACf,CAAA,GALU"}
1
+ {"version":3,"file":"StatusBadge.js","names":[],"sources":["../../../src/technospam/components/StatusBadge.tsx"],"sourcesContent":["import { Badge } from '@burdenoff/fe-libs/ui';\nimport { humanizeEnum } from '../utils/format';\n\ntype Tone = 'neutral' | 'info' | 'success' | 'warning' | 'danger';\n\nconst TONE_CLASS: Record<Tone, string> = {\n neutral: 'bg-bg-sunken text-text-secondary border-border-seam',\n info: 'bg-accent-primary/6 text-accent-primary border-accent-primary/30',\n success: 'bg-status-success-bg-subtle text-status-success-text border-status-success-border',\n warning: 'bg-status-warning-bg-subtle text-status-warning-text border-status-warning-border',\n danger: 'bg-status-error-bg-subtle text-status-error-text border-status-error-border',\n};\n\n/** Maps any Tec* status enum to a semantic tone. Unknown values fall back to neutral. */\nconst STATUS_TONE: Record<string, Tone> = {\n // course / problem\n DRAFT: 'neutral',\n PUBLISHED: 'success',\n ARCHIVED: 'warning',\n // enrollment\n ENROLLED: 'info',\n IN_PROGRESS: 'info',\n COMPLETED: 'success',\n DROPPED: 'warning',\n // certificate\n ISSUED: 'success',\n REVOKED: 'danger',\n // certificate verification (BOFF-4430)\n VALID: 'success',\n NOT_FOUND: 'warning',\n // submission verdict\n PENDING: 'neutral',\n ACCEPTED: 'success',\n WRONG_ANSWER: 'danger',\n TIME_LIMIT_EXCEEDED: 'warning',\n RUNTIME_ERROR: 'danger',\n COMPILE_ERROR: 'danger',\n // mentor\n ACTIVE: 'success',\n PAUSED: 'warning',\n INACTIVE: 'neutral',\n // session\n REQUESTED: 'info',\n CONFIRMED: 'success',\n CANCELLED: 'danger',\n NO_SHOW: 'danger',\n // hackathon\n REGISTRATION_OPEN: 'success',\n JUDGING: 'info',\n // team\n SUBMITTED: 'success',\n DISQUALIFIED: 'danger',\n WINNER: 'success',\n // assessment attempt / proctoring (BOFF-4427)\n EXPIRED: 'warning',\n NOT_REQUIRED: 'neutral',\n MONITORING: 'info',\n PENDING_REVIEW: 'info',\n VERIFIED: 'success',\n FLAGGED: 'warning',\n REJECTED: 'danger',\n};\n\nconst DIFFICULTY_TONE: Record<string, Tone> = {\n EASY: 'success',\n MEDIUM: 'warning',\n HARD: 'danger',\n BEGINNER: 'success',\n INTERMEDIATE: 'info',\n ADVANCED: 'warning',\n EXPERT: 'danger',\n};\n\nexport function StatusBadge({\n status,\n kind = 'status',\n}: {\n status?: string | null;\n kind?: 'status' | 'level';\n}) {\n if (!status) return null;\n const tone = (kind === 'level' ? DIFFICULTY_TONE[status] : STATUS_TONE[status]) ?? 'neutral';\n return (\n <Badge variant=\"outline\" className={TONE_CLASS[tone]}>\n {humanizeEnum(status)}\n </Badge>\n );\n}\n"],"mappings":";;;;AAKA,IAAM,IAAmC;CACvC,SAAS;CACT,MAAM;CACN,SAAS;CACT,SAAS;CACT,QAAQ;CACT,EAGK,IAAoC;CAExC,OAAO;CACP,WAAW;CACX,UAAU;CAEV,UAAU;CACV,aAAa;CACb,WAAW;CACX,SAAS;CAET,QAAQ;CACR,SAAS;CAET,OAAO;CACP,WAAW;CAEX,SAAS;CACT,UAAU;CACV,cAAc;CACd,qBAAqB;CACrB,eAAe;CACf,eAAe;CAEf,QAAQ;CACR,QAAQ;CACR,UAAU;CAEV,WAAW;CACX,WAAW;CACX,WAAW;CACX,SAAS;CAET,mBAAmB;CACnB,SAAS;CAET,WAAW;CACX,cAAc;CACd,QAAQ;CAER,SAAS;CACT,cAAc;CACd,YAAY;CACZ,gBAAgB;CAChB,UAAU;CACV,SAAS;CACT,UAAU;CACX,EAEK,IAAwC;CAC5C,MAAM;CACN,QAAQ;CACR,MAAM;CACN,UAAU;CACV,cAAc;CACd,UAAU;CACV,QAAQ;CACT;AAED,SAAgB,EAAY,EAC1B,WACA,UAAO,YAIN;AAGD,QAFK,IAGH,kBAAC,GAAD;EAAO,SAAQ;EAAU,WAAW,GAFxB,MAAS,UAAU,EAAgB,KAAU,EAAY,OAAY;YAG9E,EAAa,EAAO;EACf,CAAA,GALU"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Authoring console tab for certification assessments (BOFF-4427): pick a
3
+ * course, create / edit / publish / archive its assessments, and work the
4
+ * review queue of flagged and review-required attempts.
5
+ */
6
+ export declare function AssessmentsAdminTab(): import("react").JSX.Element;
7
+ //# sourceMappingURL=AssessmentsAdminTab.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AssessmentsAdminTab.d.ts","sourceRoot":"","sources":["../../../../src/technospam/components/admin/AssessmentsAdminTab.tsx"],"names":[],"mappings":"AAyUA;;;;GAIG;AACH,wBAAgB,mBAAmB,gCAsKlC"}
@@ -0,0 +1,421 @@
1
+ import { formatDateTime as e, humanizeEnum as t } from "../../utils/format.js";
2
+ import { StatusBadge as n } from "../StatusBadge.js";
3
+ import { EmptyView as r, ErrorView as i, LoadingRows as a } from "../StateViews.js";
4
+ import { notify as o } from "../../lib/toast.js";
5
+ import { useCoursesQuery as s } from "../../api/technospamApi.js";
6
+ import { draftFromLesson as c, draftToInput as l } from "../lesson/lessonAuthoring.js";
7
+ import { LessonKindFields as u } from "../lesson/LessonKindFields.js";
8
+ import { useArchiveAssessment as d, useAssessmentAttemptEventsQuery as f, useAssessmentAttemptsQuery as p, useAssessmentQuestionKeyQuery as m, useAssessmentsQuery as h, useCreateAssessment as g, usePublishAssessment as _, useReviewAssessmentAttempt as v, useUpdateAssessment as y } from "../../api/assessmentApi.js";
9
+ import { assessmentSettingsToInput as b, draftFromAssessment as x, emptyAssessmentDraft as S } from "../../utils/assessment.js";
10
+ import { useState as C } from "react";
11
+ import { Archive as w, Check as T, ListChecks as E, Pencil as D, Plus as O, Rocket as k, ShieldCheck as A, X as j } from "lucide-react";
12
+ import { Fragment as M, jsx as N, jsxs as P } from "react/jsx-runtime";
13
+ import { Button as F, GlassCard as I, Input as L, Select as R, SelectContent as z, SelectItem as B, SelectTrigger as V, SelectValue as H, Textarea as U } from "@burdenoff/fe-libs/ui";
14
+ //#region src/technospam/components/admin/AssessmentsAdminTab.tsx
15
+ var W = [
16
+ "INTEGRITY_MONITORING",
17
+ "REVIEW_REQUIRED",
18
+ "NONE"
19
+ ], G = [
20
+ "FLAGGED",
21
+ "PENDING_REVIEW",
22
+ "ALL"
23
+ ];
24
+ function K({ label: e, children: t }) {
25
+ return /* @__PURE__ */ P("label", {
26
+ className: "flex flex-col gap-1.5",
27
+ children: [/* @__PURE__ */ N("span", {
28
+ className: "text-xs font-medium text-text-secondary",
29
+ children: e
30
+ }), t]
31
+ });
32
+ }
33
+ function q({ label: e, checked: t, onChange: n }) {
34
+ return /* @__PURE__ */ P("label", {
35
+ className: "flex items-center gap-2 text-sm text-text-primary",
36
+ children: [/* @__PURE__ */ N("input", {
37
+ type: "checkbox",
38
+ checked: t,
39
+ onChange: (e) => n(e.target.checked)
40
+ }), e]
41
+ });
42
+ }
43
+ function J({ courseId: e, assessment: n, onDone: r }) {
44
+ let [i, s] = C(() => n ? x(n) : S()), { data: d, loading: f } = m(n?.id ?? null), [p, h] = C(n ? null : c()), [_, { loading: v }] = g(), [w, { loading: E }] = y(), D = p ?? (d ? c(null, d.tecAssessmentQuestionKey) : null), O = (e, t) => s((n) => ({
45
+ ...n,
46
+ [e]: t
47
+ })), k = async () => {
48
+ let t = b(i);
49
+ if (!t.ok) return o.error(t.error);
50
+ let a = {};
51
+ if (D) {
52
+ let e = l("QUIZ", D);
53
+ if (!e.ok) return o.error(e.error);
54
+ a = { questions: e.input.quizQuestions };
55
+ }
56
+ if (n) {
57
+ (await w({ variables: {
58
+ id: n.id,
59
+ input: {
60
+ ...t.input,
61
+ ...a
62
+ }
63
+ } })).data?.tecUpdateAssessment && (o.success("Assessment saved"), r());
64
+ return;
65
+ }
66
+ (await _({ variables: { input: {
67
+ courseId: e,
68
+ ...t.input,
69
+ ...a
70
+ } } })).data?.tecCreateAssessment && (o.success("Assessment created", "Publish it when the questions are ready."), r());
71
+ };
72
+ return /* @__PURE__ */ P(I, {
73
+ className: "space-y-4 p-4 sm:p-5",
74
+ "data-testid": "assessment-form",
75
+ children: [
76
+ /* @__PURE__ */ P("div", {
77
+ className: "grid grid-cols-1 gap-3 sm:grid-cols-2",
78
+ children: [
79
+ /* @__PURE__ */ N(K, {
80
+ label: "Title",
81
+ children: /* @__PURE__ */ N(L, {
82
+ value: i.title,
83
+ onChange: (e) => O("title", e.target.value)
84
+ })
85
+ }),
86
+ /* @__PURE__ */ N(K, {
87
+ label: "Integrity mode",
88
+ children: /* @__PURE__ */ P(R, {
89
+ value: i.proctoringMode,
90
+ onValueChange: (e) => O("proctoringMode", e),
91
+ children: [/* @__PURE__ */ N(V, {
92
+ "aria-label": "Integrity mode",
93
+ children: /* @__PURE__ */ N(H, {})
94
+ }), /* @__PURE__ */ N(z, { children: W.map((e) => /* @__PURE__ */ N(B, {
95
+ value: e,
96
+ children: t(e)
97
+ }, e)) })]
98
+ })
99
+ }),
100
+ /* @__PURE__ */ N(K, {
101
+ label: "Time limit (minutes)",
102
+ children: /* @__PURE__ */ N(L, {
103
+ inputMode: "numeric",
104
+ value: i.timeLimitMinutes,
105
+ onChange: (e) => O("timeLimitMinutes", e.target.value)
106
+ })
107
+ }),
108
+ /* @__PURE__ */ N(K, {
109
+ label: "Pass mark (%)",
110
+ children: /* @__PURE__ */ N(L, {
111
+ inputMode: "numeric",
112
+ value: i.passingScore,
113
+ onChange: (e) => O("passingScore", e.target.value)
114
+ })
115
+ }),
116
+ /* @__PURE__ */ N(K, {
117
+ label: "Attempts per learner",
118
+ children: /* @__PURE__ */ N(L, {
119
+ inputMode: "numeric",
120
+ value: i.maxAttempts,
121
+ onChange: (e) => O("maxAttempts", e.target.value)
122
+ })
123
+ }),
124
+ /* @__PURE__ */ N(K, {
125
+ label: "Allowed integrity events",
126
+ children: /* @__PURE__ */ N(L, {
127
+ inputMode: "numeric",
128
+ value: i.maxIntegrityViolations,
129
+ onChange: (e) => O("maxIntegrityViolations", e.target.value)
130
+ })
131
+ }),
132
+ /* @__PURE__ */ N("div", {
133
+ className: "sm:col-span-2",
134
+ children: /* @__PURE__ */ N(K, {
135
+ label: "Description",
136
+ children: /* @__PURE__ */ N(U, {
137
+ value: i.description,
138
+ onChange: (e) => O("description", e.target.value)
139
+ })
140
+ })
141
+ }),
142
+ /* @__PURE__ */ P("div", {
143
+ className: "flex flex-wrap gap-x-5 gap-y-2 sm:col-span-2",
144
+ children: [
145
+ /* @__PURE__ */ N(q, {
146
+ label: "Require fullscreen",
147
+ checked: i.requireFullscreen,
148
+ onChange: (e) => O("requireFullscreen", e)
149
+ }),
150
+ /* @__PURE__ */ N(q, {
151
+ label: "Block copy and paste",
152
+ checked: i.blockClipboard,
153
+ onChange: (e) => O("blockClipboard", e)
154
+ }),
155
+ /* @__PURE__ */ N(q, {
156
+ label: "Shuffle questions",
157
+ checked: i.shuffleQuestions,
158
+ onChange: (e) => O("shuffleQuestions", e)
159
+ }),
160
+ /* @__PURE__ */ N(q, {
161
+ label: "Shuffle options",
162
+ checked: i.shuffleOptions,
163
+ onChange: (e) => O("shuffleOptions", e)
164
+ }),
165
+ /* @__PURE__ */ N(q, {
166
+ label: "Issue course certificate",
167
+ checked: i.issuesCertificate,
168
+ onChange: (e) => O("issuesCertificate", e)
169
+ })
170
+ ]
171
+ })
172
+ ]
173
+ }),
174
+ D ? /* @__PURE__ */ N(u, {
175
+ kind: "QUIZ",
176
+ draft: D,
177
+ onChange: h,
178
+ heading: `Questions (learners need ${i.passingScore || "?"}% to pass)`
179
+ }) : f ? /* @__PURE__ */ N(a, { count: 2 }) : null,
180
+ /* @__PURE__ */ P("div", {
181
+ className: "flex justify-end gap-2 border-t border-border-seam pt-3",
182
+ children: [/* @__PURE__ */ P(F, {
183
+ variant: "outline",
184
+ onClick: r,
185
+ children: [/* @__PURE__ */ N(j, { size: 15 }), " Cancel"]
186
+ }), /* @__PURE__ */ P(F, {
187
+ onClick: () => void k(),
188
+ disabled: v || E,
189
+ "data-testid": "assessment-save",
190
+ children: [
191
+ /* @__PURE__ */ N(T, { size: 15 }),
192
+ " ",
193
+ v || E ? "Saving…" : "Save assessment"
194
+ ]
195
+ })]
196
+ })
197
+ ]
198
+ });
199
+ }
200
+ function Y({ attempt: r, title: i }) {
201
+ let [a, s] = C(!1), [c, l] = C(""), { data: u } = f(a ? r.id : null), [d, { loading: p }] = v(), m = r.proctoringStatus === "FLAGGED" || r.proctoringStatus === "PENDING_REVIEW", h = async (e) => {
202
+ let t = (await d({ variables: {
203
+ id: r.id,
204
+ decision: e,
205
+ notes: c.trim() || null
206
+ } })).data?.tecReviewAssessmentAttempt;
207
+ t && o.success(e === "VERIFIED" ? "Attempt verified" : "Attempt rejected", t.certificateId ? "The certificate was issued." : void 0);
208
+ };
209
+ return /* @__PURE__ */ P(I, {
210
+ className: "space-y-3 p-4",
211
+ "data-testid": "assessment-review-row",
212
+ children: [/* @__PURE__ */ P("div", {
213
+ className: "flex flex-wrap items-center justify-between gap-3",
214
+ children: [/* @__PURE__ */ P("div", {
215
+ className: "min-w-0",
216
+ children: [/* @__PURE__ */ N("div", {
217
+ className: "truncate text-sm font-semibold text-text-primary",
218
+ children: i
219
+ }), /* @__PURE__ */ P("div", {
220
+ className: "text-xs text-text-muted",
221
+ children: [
222
+ "Learner ",
223
+ r.learnerId,
224
+ " · attempt ",
225
+ r.attemptNumber,
226
+ " · ",
227
+ r.score ?? 0,
228
+ "% (",
229
+ r.passed ? "passed" : "not passed",
230
+ ") · ",
231
+ r.integrityViolations,
232
+ " integrity events · ",
233
+ e(r.submittedAt ?? r.startedAt)
234
+ ]
235
+ })]
236
+ }), /* @__PURE__ */ P("div", {
237
+ className: "flex items-center gap-2",
238
+ children: [/* @__PURE__ */ N(n, { status: r.proctoringStatus }), /* @__PURE__ */ N(F, {
239
+ size: "sm",
240
+ variant: "outline",
241
+ onClick: () => s((e) => !e),
242
+ children: a ? "Hide" : "Review"
243
+ })]
244
+ })]
245
+ }), a ? /* @__PURE__ */ P("div", {
246
+ className: "space-y-3",
247
+ children: [/* @__PURE__ */ N("ol", {
248
+ className: "max-h-56 space-y-1 overflow-auto rounded-lg bg-bg-sunken p-3 text-xs",
249
+ "data-testid": "assessment-events",
250
+ children: (u?.tecAssessmentAttemptEvents ?? []).map((n) => /* @__PURE__ */ P("li", {
251
+ className: n.violation ? "text-status-warning-text" : "text-text-secondary",
252
+ children: [
253
+ e(n.occurredAt),
254
+ " · ",
255
+ t(n.type),
256
+ n.violation ? " (violation)" : ""
257
+ ]
258
+ }, n.id))
259
+ }), m ? /* @__PURE__ */ P(M, { children: [/* @__PURE__ */ N(U, {
260
+ "aria-label": "Review notes",
261
+ placeholder: "Notes for the learner (optional)",
262
+ value: c,
263
+ onChange: (e) => l(e.target.value)
264
+ }), /* @__PURE__ */ P("div", {
265
+ className: "flex gap-2",
266
+ children: [/* @__PURE__ */ P(F, {
267
+ size: "sm",
268
+ disabled: p,
269
+ onClick: () => void h("VERIFIED"),
270
+ children: [/* @__PURE__ */ N(T, { size: 14 }), " Verify"]
271
+ }), /* @__PURE__ */ P(F, {
272
+ size: "sm",
273
+ variant: "outline",
274
+ disabled: p,
275
+ onClick: () => void h("REJECTED"),
276
+ children: [/* @__PURE__ */ N(j, { size: 14 }), " Reject"]
277
+ })]
278
+ })] }) : r.reviewNotes ? /* @__PURE__ */ P("p", {
279
+ className: "text-sm text-text-secondary",
280
+ children: ["Reviewer note: ", r.reviewNotes]
281
+ }) : null]
282
+ }) : null]
283
+ });
284
+ }
285
+ function X() {
286
+ let { data: e } = s({}), c = e?.tecListCourses?.items ?? [], [l, u] = C(""), f = l || c[0]?.id || "", { data: m, loading: g, error: v } = h({ courseId: f }, !f), [y, b] = C(null), [x] = _(), [S] = d(), [T, j] = C("FLAGGED"), M = p({ proctoringStatus: T === "ALL" ? null : T }), L = m?.tecListAssessments ?? [], U = new Map(L.map((e) => [e.id, e.title]));
287
+ return /* @__PURE__ */ P("div", {
288
+ className: "space-y-6",
289
+ children: [/* @__PURE__ */ P("section", {
290
+ className: "space-y-3",
291
+ children: [
292
+ /* @__PURE__ */ P("div", {
293
+ className: "flex flex-wrap items-end justify-between gap-3",
294
+ children: [/* @__PURE__ */ N(K, {
295
+ label: "Course",
296
+ children: /* @__PURE__ */ P(R, {
297
+ value: f,
298
+ onValueChange: (e) => {
299
+ u(e), b(null);
300
+ },
301
+ children: [/* @__PURE__ */ N(V, {
302
+ "aria-label": "Course",
303
+ className: "min-w-64",
304
+ children: /* @__PURE__ */ N(H, { placeholder: "Choose a course" })
305
+ }), /* @__PURE__ */ N(z, { children: c.map((e) => /* @__PURE__ */ N(B, {
306
+ value: e.id,
307
+ children: e.title
308
+ }, e.id)) })]
309
+ })
310
+ }), f && y === null ? /* @__PURE__ */ P(F, {
311
+ size: "sm",
312
+ onClick: () => b("new"),
313
+ "data-testid": "assessment-new",
314
+ children: [/* @__PURE__ */ N(O, { size: 15 }), " New assessment"]
315
+ }) : null]
316
+ }),
317
+ y !== null && f ? /* @__PURE__ */ N(J, {
318
+ courseId: f,
319
+ assessment: y === "new" ? null : y,
320
+ onDone: () => b(null)
321
+ }, y === "new" ? "new" : y.id) : null,
322
+ f ? v ? /* @__PURE__ */ N(i, {
323
+ title: "Could not load assessments",
324
+ error: v
325
+ }) : g && L.length === 0 ? /* @__PURE__ */ N(a, { count: 2 }) : L.length === 0 ? /* @__PURE__ */ N(r, {
326
+ icon: /* @__PURE__ */ N(A, { size: 26 }),
327
+ title: "No assessments for this course",
328
+ description: "Add a timed, integrity-monitored exam that issues the course certificate."
329
+ }) : /* @__PURE__ */ N("div", {
330
+ className: "space-y-2",
331
+ children: L.map((e) => /* @__PURE__ */ P(I, {
332
+ className: "flex flex-wrap items-center justify-between gap-3 p-4",
333
+ children: [/* @__PURE__ */ P("div", {
334
+ className: "min-w-0",
335
+ children: [/* @__PURE__ */ N("div", {
336
+ className: "truncate text-sm font-semibold text-text-primary",
337
+ children: e.title
338
+ }), /* @__PURE__ */ P("div", {
339
+ className: "text-xs text-text-muted",
340
+ children: [
341
+ e.questionCount,
342
+ " questions · ",
343
+ e.timeLimitMinutes,
344
+ " min · pass",
345
+ " ",
346
+ e.passingScore,
347
+ "% · ",
348
+ t(e.proctoringMode)
349
+ ]
350
+ })]
351
+ }), /* @__PURE__ */ P("div", {
352
+ className: "flex flex-wrap items-center gap-2",
353
+ children: [
354
+ /* @__PURE__ */ N(n, { status: e.status }),
355
+ /* @__PURE__ */ P(F, {
356
+ size: "sm",
357
+ variant: "outline",
358
+ onClick: () => b(e),
359
+ children: [/* @__PURE__ */ N(D, { size: 14 }), " Edit"]
360
+ }),
361
+ e.status === "PUBLISHED" ? /* @__PURE__ */ P(F, {
362
+ size: "sm",
363
+ variant: "outline",
364
+ onClick: () => void S({ variables: { id: e.id } }).then((e) => {
365
+ e.data?.tecArchiveAssessment && o.success("Assessment archived");
366
+ }),
367
+ children: [/* @__PURE__ */ N(w, { size: 14 }), " Archive"]
368
+ }) : /* @__PURE__ */ P(F, {
369
+ size: "sm",
370
+ onClick: () => void x({ variables: { id: e.id } }).then((e) => {
371
+ e.data?.tecPublishAssessment && o.success("Assessment published");
372
+ }),
373
+ children: [/* @__PURE__ */ N(k, { size: 14 }), " Publish"]
374
+ })
375
+ ]
376
+ })]
377
+ }, e.id))
378
+ }) : /* @__PURE__ */ N(r, {
379
+ title: "No courses yet",
380
+ description: "Create a course first, then add its assessments."
381
+ })
382
+ ]
383
+ }), /* @__PURE__ */ P("section", {
384
+ className: "space-y-3",
385
+ children: [/* @__PURE__ */ P("div", {
386
+ className: "flex flex-wrap items-end justify-between gap-3",
387
+ children: [/* @__PURE__ */ P("h3", {
388
+ className: "flex items-center gap-2 text-sm font-semibold text-text-primary",
389
+ children: [/* @__PURE__ */ N(E, { size: 16 }), " Review queue"]
390
+ }), /* @__PURE__ */ P(R, {
391
+ value: T,
392
+ onValueChange: (e) => j(e),
393
+ children: [/* @__PURE__ */ N(V, {
394
+ "aria-label": "Review filter",
395
+ className: "min-w-48",
396
+ children: /* @__PURE__ */ N(H, {})
397
+ }), /* @__PURE__ */ N(z, { children: G.map((e) => /* @__PURE__ */ N(B, {
398
+ value: e,
399
+ children: e === "ALL" ? "All attempts" : t(e)
400
+ }, e)) })]
401
+ })]
402
+ }), M.error ? /* @__PURE__ */ N(i, {
403
+ title: "Could not load attempts",
404
+ error: M.error
405
+ }) : (M.data?.tecListAssessmentAttempts ?? []).length === 0 ? /* @__PURE__ */ N(r, {
406
+ title: "Nothing to review",
407
+ description: "Flagged and review-required attempts appear here."
408
+ }) : /* @__PURE__ */ N("div", {
409
+ className: "space-y-2",
410
+ children: (M.data?.tecListAssessmentAttempts ?? []).map((e) => /* @__PURE__ */ N(Y, {
411
+ attempt: e,
412
+ title: U.get(e.assessmentId) ?? "Assessment attempt"
413
+ }, e.id))
414
+ })]
415
+ })]
416
+ });
417
+ }
418
+ //#endregion
419
+ export { X as AssessmentsAdminTab };
420
+
421
+ //# sourceMappingURL=AssessmentsAdminTab.js.map