@anantagnanam/lms 0.0.0-stage → 0.2.0

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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -2
  3. package/astro/client.ts +355 -0
  4. package/astro/components/CertificateCard.astro +83 -0
  5. package/astro/components/CourseCard.astro +61 -0
  6. package/astro/components/CourseCatalog.astro +96 -0
  7. package/astro/components/CourseOverview.astro +308 -0
  8. package/astro/components/LearnerDashboard.astro +127 -0
  9. package/astro/components/LearningShell.astro +37 -0
  10. package/astro/components/LessonAvailabilityNote.astro +26 -0
  11. package/astro/components/LessonContent.astro +53 -0
  12. package/astro/components/LessonPlayer.astro +302 -0
  13. package/astro/components/MediaPlayer.astro +111 -0
  14. package/astro/components/index.ts +7 -0
  15. package/astro/layouts/AcademyLayout.astro +84 -0
  16. package/astro/media-player.ts +99 -0
  17. package/astro/pages/author-preview.astro +71 -0
  18. package/astro/pages/certificate.astro +57 -0
  19. package/astro/pages/cohort.astro +102 -0
  20. package/astro/pages/course.astro +90 -0
  21. package/astro/pages/dashboard.astro +34 -0
  22. package/astro/pages/index.astro +40 -0
  23. package/astro/pages/lesson.astro +103 -0
  24. package/astro/pages/library.astro +173 -0
  25. package/astro/pages/paths.astro +73 -0
  26. package/astro/pages/teach.astro +15 -0
  27. package/astro/quiz-drafts.ts +87 -0
  28. package/astro/quiz-player.ts +362 -0
  29. package/astro/styles.css +1300 -0
  30. package/dist/admin.d.ts +34 -0
  31. package/dist/admin.js +3231 -0
  32. package/dist/admin.js.map +1 -0
  33. package/dist/astro.d.ts +11 -0
  34. package/dist/astro.js +52 -0
  35. package/dist/astro.js.map +1 -0
  36. package/dist/commerce.d.ts +72 -0
  37. package/dist/commerce.js +0 -0
  38. package/dist/config-BX3beTpM.js +15 -0
  39. package/dist/config-BX3beTpM.js.map +1 -0
  40. package/dist/constants-Cnx7k82Z.js +161 -0
  41. package/dist/constants-Cnx7k82Z.js.map +1 -0
  42. package/dist/csv-DKq_4OWU.js +55 -0
  43. package/dist/csv-DKq_4OWU.js.map +1 -0
  44. package/dist/errors-ChzJS9al.js +16 -0
  45. package/dist/errors-ChzJS9al.js.map +1 -0
  46. package/dist/index.d.ts +13 -0
  47. package/dist/index.js +19 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/runtime.d.ts +773 -0
  50. package/dist/runtime.js +2840 -0
  51. package/dist/runtime.js.map +1 -0
  52. package/dist/schemas.d.ts +651 -0
  53. package/dist/schemas.js +347 -0
  54. package/dist/schemas.js.map +1 -0
  55. package/dist/server.d.ts +40 -0
  56. package/dist/server.js +65 -0
  57. package/dist/server.js.map +1 -0
  58. package/dist/types.d.ts +387 -0
  59. package/dist/types.js +0 -0
  60. package/package.json +83 -4
package/dist/admin.js ADDED
@@ -0,0 +1,3231 @@
1
+ import { r as PLUGIN_ID, t as API_BASE } from "./constants-Cnx7k82Z.js";
2
+ import { assignmentSchema, courseDataSchema, lessonSchema, moduleSchema, questionSchema, quizSchema, settingsSchema } from "./schemas.js";
3
+ import { n as enrollmentCsvLines, t as enrollmentCsvHeader } from "./csv-DKq_4OWU.js";
4
+ import { useEffect, useState } from "react";
5
+ import { apiFetch } from "emdash/plugin-utils";
6
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
+
8
+ //#region src/admin.tsx
9
+ const ADMIN_BASE = `/_emdash/admin/plugins/${PLUGIN_ID}`;
10
+ async function api(route, input = {}, method = "GET", signal) {
11
+ let url = `${API_BASE}/${route}`;
12
+ if (method === "GET") {
13
+ const params = new URLSearchParams();
14
+ for (const [key, value] of Object.entries(input)) if (value !== void 0 && value !== "" && value !== null) params.set(key, String(value));
15
+ url += `?${params}`;
16
+ }
17
+ const response = await apiFetch(url, {
18
+ method,
19
+ signal,
20
+ credentials: "same-origin",
21
+ ...method !== "GET" ? {
22
+ headers: { "Content-Type": "application/json" },
23
+ body: JSON.stringify(input)
24
+ } : {}
25
+ });
26
+ const result = await response.json();
27
+ if (!response.ok || !result.success) throw new Error(result.error?.message ?? "The request could not be completed.");
28
+ return result.data;
29
+ }
30
+ function useLoad(route, input = {}, enabled = true) {
31
+ const key = JSON.stringify(input);
32
+ const [generation, setGeneration] = useState(0);
33
+ const [state, setState] = useState({ loading: enabled });
34
+ useEffect(() => {
35
+ if (!enabled) {
36
+ setState({ loading: false });
37
+ return;
38
+ }
39
+ const controller = new AbortController();
40
+ setState((old) => ({
41
+ ...old,
42
+ loading: true,
43
+ error: void 0
44
+ }));
45
+ api(route, JSON.parse(key), "GET", controller.signal).then((data) => {
46
+ if (!controller.signal.aborted) setState({
47
+ loading: false,
48
+ data
49
+ });
50
+ }).catch((error) => {
51
+ if (!controller.signal.aborted) setState({
52
+ loading: false,
53
+ error: error.message
54
+ });
55
+ });
56
+ return () => controller.abort();
57
+ }, [
58
+ route,
59
+ key,
60
+ enabled,
61
+ generation
62
+ ]);
63
+ return {
64
+ ...state,
65
+ reload: () => setGeneration((value) => value + 1)
66
+ };
67
+ }
68
+ function useAction() {
69
+ const [busy, setBusy] = useState(false);
70
+ const [error, setError] = useState("");
71
+ const [message, setMessage] = useState("");
72
+ const run = async (action, success = "Saved.") => {
73
+ if (busy) return;
74
+ setBusy(true);
75
+ setError("");
76
+ setMessage("");
77
+ try {
78
+ await action();
79
+ setMessage(success);
80
+ } catch (cause) {
81
+ setError(cause instanceof Error ? cause.message : "The request failed.");
82
+ } finally {
83
+ setBusy(false);
84
+ }
85
+ };
86
+ return {
87
+ busy,
88
+ error,
89
+ message,
90
+ run
91
+ };
92
+ }
93
+ function useChoices(route) {
94
+ const [cursor, setCursor] = useState();
95
+ const [items, setItems] = useState([]);
96
+ const list = useLoad(route, {
97
+ cursor,
98
+ limit: 100
99
+ });
100
+ useEffect(() => {
101
+ if (list.data) setItems((old) => [...new Map([...old, ...list.data.items].map((item) => [item.id, item])).values()]);
102
+ }, [list.data]);
103
+ return {
104
+ ...list,
105
+ items,
106
+ more: () => setCursor(list.data?.cursor)
107
+ };
108
+ }
109
+ function ChoicesField({ label, value, choices, onChange }) {
110
+ const all = [...choices, ...value.filter((id) => !choices.some((choice) => choice.id === id)).map((id) => ({
111
+ id,
112
+ label: `Saved selection · ${id.slice(0, 8)}`
113
+ }))];
114
+ return /* @__PURE__ */ jsxs("fieldset", {
115
+ className: "lms-choices",
116
+ children: [
117
+ /* @__PURE__ */ jsx("legend", { children: label }),
118
+ all.map((choice) => /* @__PURE__ */ jsxs("label", {
119
+ className: "lms-check",
120
+ children: [/* @__PURE__ */ jsx("input", {
121
+ type: "checkbox",
122
+ checked: value.includes(choice.id),
123
+ onChange: (event) => onChange(event.target.checked ? [...value, choice.id] : value.filter((id) => id !== choice.id))
124
+ }), choice.label]
125
+ }, choice.id)),
126
+ !all.length && /* @__PURE__ */ jsx("p", { children: "No choices loaded yet." })
127
+ ]
128
+ });
129
+ }
130
+ function Notice({ error, message }) {
131
+ return /* @__PURE__ */ jsxs(Fragment, { children: [error && /* @__PURE__ */ jsx("p", {
132
+ role: "alert",
133
+ className: "lms-notice lms-error",
134
+ children: error
135
+ }), message && /* @__PURE__ */ jsx("p", {
136
+ role: "status",
137
+ className: "lms-notice",
138
+ children: message
139
+ })] });
140
+ }
141
+ function Field({ label, children, help }) {
142
+ return /* @__PURE__ */ jsxs("label", {
143
+ className: "lms-field",
144
+ children: [
145
+ /* @__PURE__ */ jsx("span", { children: label }),
146
+ children,
147
+ help && /* @__PURE__ */ jsx("small", { children: help })
148
+ ]
149
+ });
150
+ }
151
+ function Button({ children, busy, danger, ...props }) {
152
+ return /* @__PURE__ */ jsx("button", {
153
+ type: "button",
154
+ ...props,
155
+ className: `lms-button${danger ? " lms-danger" : ""}`,
156
+ disabled: props.disabled || busy,
157
+ children: busy ? "Working…" : children
158
+ });
159
+ }
160
+ function Panel({ title, children }) {
161
+ return /* @__PURE__ */ jsxs("section", {
162
+ className: "lms-panel",
163
+ children: [title && /* @__PURE__ */ jsx("h2", { children: title }), children]
164
+ });
165
+ }
166
+ function Pager({ page, next, reset }) {
167
+ return /* @__PURE__ */ jsxs("div", {
168
+ className: "lms-actions",
169
+ children: [/* @__PURE__ */ jsx(Button, {
170
+ onClick: reset,
171
+ children: "First page"
172
+ }), page?.hasMore && page.cursor && /* @__PURE__ */ jsx(Button, {
173
+ onClick: () => next(page.cursor),
174
+ children: "Next page"
175
+ })]
176
+ });
177
+ }
178
+ function PageFrame({ title, description, children }) {
179
+ const academy = useLoad("academy");
180
+ return /* @__PURE__ */ jsxs("main", {
181
+ className: "ag-lms-admin",
182
+ children: [
183
+ /* @__PURE__ */ jsx("style", { children: css }),
184
+ /* @__PURE__ */ jsxs("header", {
185
+ className: "lms-page-header",
186
+ children: [/* @__PURE__ */ jsxs("div", { children: [
187
+ /* @__PURE__ */ jsx("p", {
188
+ className: "lms-eyebrow",
189
+ children: "ANANTA GNANAM · LEARNING"
190
+ }),
191
+ /* @__PURE__ */ jsx("h1", { children: title }),
192
+ description && /* @__PURE__ */ jsx("p", { children: description })
193
+ ] }), /* @__PURE__ */ jsx("a", {
194
+ href: academy.data?.basePath ?? "/academy",
195
+ className: "lms-button",
196
+ target: "_blank",
197
+ rel: "noopener noreferrer",
198
+ children: "Open academy ↗"
199
+ })]
200
+ }),
201
+ /* @__PURE__ */ jsx("nav", {
202
+ className: "lms-tabs",
203
+ "aria-label": "Learning administration",
204
+ children: [
205
+ ["/", "Overview"],
206
+ ["/courses", "Courses"],
207
+ ["/learners", "Enrollments"],
208
+ ["/assignments", "Assignments"],
209
+ ["/community", "Community"],
210
+ ["/cohorts", "Cohorts"],
211
+ ["/paths", "Learning paths"],
212
+ ["/reports", "Reports"],
213
+ ["/notifications", "Notifications"],
214
+ ["/audit", "Audit history"],
215
+ ["/settings", "Settings"]
216
+ ].map(([path, label]) => /* @__PURE__ */ jsx("a", {
217
+ href: `${ADMIN_BASE}${path === "/" ? "" : path}`,
218
+ children: label
219
+ }, path))
220
+ }),
221
+ children
222
+ ]
223
+ });
224
+ }
225
+ function Metrics({ report }) {
226
+ return /* @__PURE__ */ jsx("div", {
227
+ className: "lms-metrics",
228
+ children: [
229
+ ["Courses", report?.courseCount],
230
+ ["Active enrollments", report?.active],
231
+ ["Course completions", report?.completedEver],
232
+ ["Awaiting approval", report?.pending],
233
+ ["Completion rate", report ? `${report.completionRate}%` : void 0]
234
+ ].map(([label, value]) => /* @__PURE__ */ jsxs("div", {
235
+ className: "lms-panel",
236
+ children: [/* @__PURE__ */ jsx("span", { children: label }), /* @__PURE__ */ jsx("strong", { children: value ?? "—" })]
237
+ }, label))
238
+ });
239
+ }
240
+ function CoursePicker({ value, onChange, allowAll = false }) {
241
+ const [cursor, setCursor] = useState();
242
+ const [choices, setChoices] = useState([]);
243
+ const list = useLoad("admin/courses", {
244
+ limit: 100,
245
+ cursor
246
+ });
247
+ useEffect(() => {
248
+ if (list.data) setChoices((old) => [...new Map([...old, ...list.data.items].map((course) => [course.id, course])).values()]);
249
+ }, [list.data]);
250
+ return /* @__PURE__ */ jsxs("div", { children: [
251
+ /* @__PURE__ */ jsx(Field, {
252
+ label: "Course",
253
+ children: /* @__PURE__ */ jsxs("select", {
254
+ value,
255
+ onChange: (event) => onChange(event.target.value),
256
+ children: [/* @__PURE__ */ jsx("option", {
257
+ value: "",
258
+ children: allowAll ? "All courses" : "Choose a course"
259
+ }), choices.map((course) => /* @__PURE__ */ jsxs("option", {
260
+ value: course.id,
261
+ children: [
262
+ course.title,
263
+ " · ",
264
+ course.status
265
+ ]
266
+ }, course.id))]
267
+ })
268
+ }),
269
+ list.data?.hasMore && /* @__PURE__ */ jsx(Button, {
270
+ onClick: () => setCursor(list.data?.cursor),
271
+ children: "Load more courses"
272
+ }),
273
+ /* @__PURE__ */ jsx(Notice, { error: list.error })
274
+ ] });
275
+ }
276
+ function OverviewPage() {
277
+ const report = useLoad("admin/report");
278
+ return /* @__PURE__ */ jsxs(PageFrame, {
279
+ title: "Learning overview",
280
+ description: "Create learning paths, support your learners, and see their progress.",
281
+ children: [
282
+ /* @__PURE__ */ jsx(Notice, { error: report.error }),
283
+ /* @__PURE__ */ jsx(Metrics, { report: report.data }),
284
+ /* @__PURE__ */ jsxs("div", {
285
+ className: "lms-grid",
286
+ children: [/* @__PURE__ */ jsxs(Panel, {
287
+ title: "Course studio",
288
+ children: [/* @__PURE__ */ jsx("p", { children: "Build modules with reading, video, audio, quizzes, assignments, and live sessions." }), /* @__PURE__ */ jsx("a", {
289
+ className: "lms-button",
290
+ href: `${ADMIN_BASE}/courses`,
291
+ children: "Manage courses"
292
+ })]
293
+ }), /* @__PURE__ */ jsxs(Panel, {
294
+ title: "Support learners",
295
+ children: [/* @__PURE__ */ jsx("p", { children: "Review enrollment requests and give feedback on submitted work." }), /* @__PURE__ */ jsxs("div", {
296
+ className: "lms-actions",
297
+ children: [/* @__PURE__ */ jsx("a", {
298
+ className: "lms-button",
299
+ href: `${ADMIN_BASE}/learners`,
300
+ children: "Enrollments"
301
+ }), /* @__PURE__ */ jsx("a", {
302
+ className: "lms-button",
303
+ href: `${ADMIN_BASE}/assignments`,
304
+ children: "Grade assignments"
305
+ })]
306
+ })]
307
+ })]
308
+ })
309
+ ]
310
+ });
311
+ }
312
+ function CoursesPage() {
313
+ const [cursor, setCursor] = useState();
314
+ const [status, setStatus] = useState("");
315
+ const [edit, setEdit] = useState();
316
+ const list = useLoad("admin/courses", {
317
+ cursor,
318
+ status
319
+ });
320
+ const action = useAction();
321
+ const open = (id) => void action.run(async () => {
322
+ const result = await api("admin/course", { id });
323
+ setEdit({
324
+ course: result.course,
325
+ revision: result.revision
326
+ });
327
+ }, "");
328
+ const importFile = async (file) => {
329
+ if (file.size > 95e4) throw new Error("This course file is too large.");
330
+ const bundle = JSON.parse(await file.text());
331
+ if (bundle.format !== "anantagnanam-lms-course" || bundle.version !== 1 || !bundle.course) throw new Error("Choose an Ananta Gnanam course export.");
332
+ setEdit({
333
+ course: courseDataSchema.parse({
334
+ ...bundle.course,
335
+ status: "draft"
336
+ }),
337
+ revision: null
338
+ });
339
+ };
340
+ if (edit) return /* @__PURE__ */ jsx(CourseEditor, {
341
+ initial: edit.course,
342
+ initialRevision: edit.revision,
343
+ onClose: () => {
344
+ setEdit(void 0);
345
+ list.reload();
346
+ }
347
+ });
348
+ return /* @__PURE__ */ jsxs(PageFrame, {
349
+ title: "Courses",
350
+ description: "Author, publish, and update your courses.",
351
+ children: [
352
+ /* @__PURE__ */ jsx(Notice, {
353
+ error: list.error || action.error,
354
+ message: action.message
355
+ }),
356
+ /* @__PURE__ */ jsxs("div", {
357
+ className: "lms-toolbar",
358
+ children: [
359
+ /* @__PURE__ */ jsx(Button, {
360
+ onClick: () => setEdit({
361
+ course: courseDataSchema.parse({
362
+ title: "New course",
363
+ slug: "new-course"
364
+ }),
365
+ revision: null
366
+ }),
367
+ children: "Create course"
368
+ }),
369
+ /* @__PURE__ */ jsxs("label", {
370
+ className: "lms-button",
371
+ children: ["Import course", /* @__PURE__ */ jsx("input", {
372
+ className: "lms-file",
373
+ type: "file",
374
+ accept: "application/json,.json",
375
+ onChange: (event) => {
376
+ const file = event.target.files?.[0];
377
+ if (file) action.run(() => importFile(file), "");
378
+ event.target.value = "";
379
+ }
380
+ })]
381
+ }),
382
+ /* @__PURE__ */ jsx(Field, {
383
+ label: "Status",
384
+ children: /* @__PURE__ */ jsxs("select", {
385
+ value: status,
386
+ onChange: (event) => {
387
+ setStatus(event.target.value);
388
+ setCursor(void 0);
389
+ },
390
+ children: [/* @__PURE__ */ jsx("option", {
391
+ value: "",
392
+ children: "All statuses"
393
+ }), [
394
+ "draft",
395
+ "published",
396
+ "archived"
397
+ ].map((value) => /* @__PURE__ */ jsx("option", { children: value }, value))]
398
+ })
399
+ })
400
+ ]
401
+ }),
402
+ list.loading && /* @__PURE__ */ jsx("p", {
403
+ role: "status",
404
+ children: "Loading courses…"
405
+ }),
406
+ /* @__PURE__ */ jsx("div", {
407
+ className: "lms-course-grid",
408
+ children: list.data?.items.map((course) => /* @__PURE__ */ jsxs(Panel, { children: [
409
+ /* @__PURE__ */ jsx("span", {
410
+ className: "lms-badge",
411
+ children: course.status
412
+ }),
413
+ /* @__PURE__ */ jsx("h2", { children: course.title }),
414
+ /* @__PURE__ */ jsx("p", { children: course.summary || "Add a summary to introduce this course." }),
415
+ /* @__PURE__ */ jsxs("p", {
416
+ className: "lms-muted",
417
+ children: [
418
+ course.modules.length,
419
+ " modules ·",
420
+ " ",
421
+ course.modules.reduce((sum, module) => sum + module.lessons.length, 0),
422
+ " lessons"
423
+ ]
424
+ }),
425
+ /* @__PURE__ */ jsxs("div", {
426
+ className: "lms-actions",
427
+ children: [/* @__PURE__ */ jsx(Button, {
428
+ busy: action.busy,
429
+ onClick: () => open(course.id),
430
+ children: "Edit course"
431
+ }), /* @__PURE__ */ jsx(Button, {
432
+ onClick: () => void action.run(async () => download(`${course.slug}.json`, JSON.stringify(await api("admin/course/export", { id: course.id }), null, 2), "application/json"), "Course exported."),
433
+ children: "Export"
434
+ })]
435
+ })
436
+ ] }, course.id))
437
+ }),
438
+ list.data && !list.data.items.length && /* @__PURE__ */ jsx(Panel, { children: /* @__PURE__ */ jsx("p", { children: "No courses yet. Create your first course above." }) }),
439
+ /* @__PURE__ */ jsx(Pager, {
440
+ page: list.data,
441
+ next: setCursor,
442
+ reset: () => setCursor(void 0)
443
+ })
444
+ ]
445
+ });
446
+ }
447
+ function CourseHistory({ courseId, disabled, restore }) {
448
+ const [cursor, setCursor] = useState();
449
+ const load = useLoad("admin/course/history", {
450
+ id: courseId,
451
+ cursor
452
+ });
453
+ return /* @__PURE__ */ jsxs("details", {
454
+ className: "lms-panel",
455
+ children: [
456
+ /* @__PURE__ */ jsx("summary", { children: "Saved revision history" }),
457
+ /* @__PURE__ */ jsx(Notice, { error: load.error }),
458
+ /* @__PURE__ */ jsx("p", { children: "Restore a revision into a draft, then review and publish it. Save current edits before restoring." }),
459
+ load.data?.items.map((item) => /* @__PURE__ */ jsxs("div", {
460
+ className: "lms-toolbar",
461
+ children: [/* @__PURE__ */ jsxs("span", { children: [
462
+ new Date(item.createdAt).toLocaleString(),
463
+ " · ",
464
+ item.kind,
465
+ " · ",
466
+ item.title
467
+ ] }), /* @__PURE__ */ jsx(Button, {
468
+ disabled: disabled || load.loading,
469
+ onClick: () => restore(item.id),
470
+ children: "Restore as draft"
471
+ })]
472
+ }, item.id)),
473
+ !load.data?.items.length && /* @__PURE__ */ jsx("p", { children: "No saved history yet. Legacy courses gain history on their next save or migration." }),
474
+ cursor && /* @__PURE__ */ jsx(Button, {
475
+ onClick: () => setCursor(void 0),
476
+ children: "Latest revisions"
477
+ }),
478
+ load.data?.hasMore && /* @__PURE__ */ jsx(Button, {
479
+ disabled: load.loading,
480
+ onClick: () => setCursor(load.data?.cursor),
481
+ children: "Older revisions"
482
+ })
483
+ ]
484
+ });
485
+ }
486
+ function CourseEditor({ initial, initialRevision, onClose }) {
487
+ const [course, setCourse] = useState(initial);
488
+ const academy = useLoad("academy");
489
+ const [revision, setRevision] = useState(initialRevision);
490
+ const [saved, setSaved] = useState(JSON.stringify(initial));
491
+ const [selected, setSelected] = useState(initial.modules[0]?.lessons[0]?.id ?? null);
492
+ const [tab, setTab] = useState("details");
493
+ const action = useAction();
494
+ const dirty = !course.id || JSON.stringify(course) !== saved;
495
+ const prerequisiteChoices = useChoices("catalog");
496
+ const instructorChoices = useChoices("admin/instructors");
497
+ useEffect(() => {
498
+ if (!dirty) return;
499
+ const handler = (event) => {
500
+ event.preventDefault();
501
+ };
502
+ window.addEventListener("beforeunload", handler);
503
+ return () => window.removeEventListener("beforeunload", handler);
504
+ }, [dirty]);
505
+ const patch = (data) => setCourse((old) => ({
506
+ ...old,
507
+ ...data
508
+ }));
509
+ const updateModule = (index, data) => setCourse((old) => ({
510
+ ...old,
511
+ modules: old.modules.map((module, i) => i === index ? {
512
+ ...module,
513
+ ...data
514
+ } : module)
515
+ }));
516
+ const updateLesson = (id, data) => setCourse((old) => ({
517
+ ...old,
518
+ modules: old.modules.map((module) => ({
519
+ ...module,
520
+ lessons: module.lessons.map((lesson) => lesson.id === id ? {
521
+ ...lesson,
522
+ ...data
523
+ } : lesson)
524
+ }))
525
+ }));
526
+ const selectedLesson = course.modules.flatMap((module) => module.lessons).find((lesson) => lesson.id === selected);
527
+ const acceptSaved = (result) => {
528
+ setCourse(result.course);
529
+ setRevision(result.revision);
530
+ setSaved(JSON.stringify(result.course));
531
+ };
532
+ const publishOrArchive = (operation) => void action.run(async () => {
533
+ acceptSaved(await api(`admin/course/${operation}`, {
534
+ id: course.id,
535
+ expectedRevision: revision
536
+ }, "POST"));
537
+ }, operation === "publish" ? "Saved draft published. Existing learners keep their enrolled edition." : "Course archived.");
538
+ const save = () => void action.run(async () => {
539
+ const parsed = courseDataSchema.safeParse({
540
+ ...course,
541
+ status: "draft"
542
+ });
543
+ if (!parsed.success) throw new Error(parsed.error.issues.slice(0, 5).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(" "));
544
+ acceptSaved(await api("admin/course/save", {
545
+ id: course.id,
546
+ expectedRevision: revision,
547
+ data: parsed.data
548
+ }, "POST"));
549
+ }, "Draft saved. Publish it when it is ready for new learners.");
550
+ const reorderLesson = (moduleIndex, index, delta) => {
551
+ const items = [...course.modules[moduleIndex].lessons];
552
+ const to = index + delta;
553
+ if (to < 0 || to >= items.length) return;
554
+ [items[index], items[to]] = [items[to], items[index]];
555
+ updateModule(moduleIndex, { lessons: items });
556
+ };
557
+ return /* @__PURE__ */ jsxs(PageFrame, {
558
+ title: course.title,
559
+ description: dirty ? "You have unsaved changes." : "All changes saved.",
560
+ children: [
561
+ /* @__PURE__ */ jsx(Notice, {
562
+ error: action.error,
563
+ message: action.message
564
+ }),
565
+ /* @__PURE__ */ jsxs("div", {
566
+ className: "lms-toolbar",
567
+ children: [
568
+ /* @__PURE__ */ jsx(Button, {
569
+ onClick: () => {
570
+ if (!dirty || window.confirm("Discard unsaved changes?")) onClose();
571
+ },
572
+ children: "Back to courses"
573
+ }),
574
+ /* @__PURE__ */ jsx(Button, {
575
+ busy: action.busy,
576
+ onClick: () => save(),
577
+ children: "Save draft"
578
+ }),
579
+ course.id && /* @__PURE__ */ jsx(Button, {
580
+ busy: action.busy,
581
+ disabled: dirty,
582
+ onClick: () => void action.run(async () => {
583
+ const exported = await api("admin/course/export", { id: course.id });
584
+ acceptSaved(await api("admin/course/import", {
585
+ ...exported,
586
+ course: {
587
+ ...exported.course,
588
+ title: `Copy of ${course.title}`.slice(0, 200),
589
+ slug: `${course.slug.slice(0, 65)}-copy-${crypto.randomUUID().slice(0, 8)}`
590
+ }
591
+ }, "POST"));
592
+ }, "Course duplicated as a new draft. Review its dates and access settings before publishing."),
593
+ children: "Duplicate as draft"
594
+ }),
595
+ /* @__PURE__ */ jsx(Button, {
596
+ busy: action.busy,
597
+ disabled: dirty,
598
+ onClick: () => publishOrArchive("publish"),
599
+ children: "Publish saved draft"
600
+ }),
601
+ course.status === "published" && /* @__PURE__ */ jsx(Button, {
602
+ busy: action.busy,
603
+ disabled: dirty,
604
+ onClick: () => publishOrArchive("archive"),
605
+ children: "Archive"
606
+ }),
607
+ course.id && course.status === "draft" && /* @__PURE__ */ jsx(Button, {
608
+ danger: true,
609
+ busy: action.busy,
610
+ onClick: () => {
611
+ if (window.confirm(`Delete the draft “${course.title}”?`)) action.run(async () => {
612
+ await api("admin/course/delete", {
613
+ id: course.id,
614
+ expectedRevision: revision
615
+ }, "POST");
616
+ onClose();
617
+ }, "");
618
+ },
619
+ children: "Delete draft"
620
+ })
621
+ ]
622
+ }),
623
+ /* @__PURE__ */ jsxs("p", {
624
+ role: "status",
625
+ children: [
626
+ "Course visibility: ",
627
+ course.status,
628
+ ".",
629
+ dirty ? " Save changes before publishing." : course.publishedEditionId && course.draftRevisionId !== course.publishedDraftRevisionId ? " Saved draft changes are awaiting publication." : "",
630
+ course.publishedEditionId ? " Publishing creates a new edition for new enrollments; existing learners keep their course material and progress." : ""
631
+ ]
632
+ }),
633
+ course.id && /* @__PURE__ */ jsx(CourseHistory, {
634
+ courseId: course.id,
635
+ disabled: dirty || action.busy,
636
+ restore: (revisionId) => void action.run(async () => {
637
+ const result = await api("admin/course/restore", {
638
+ id: course.id,
639
+ expectedRevision: revision,
640
+ revisionId
641
+ }, "POST");
642
+ acceptSaved(result);
643
+ setSelected(result.course.modules[0]?.lessons[0]?.id ?? null);
644
+ }, "Revision restored into a saved draft. Review it before publishing.")
645
+ }, revision),
646
+ /* @__PURE__ */ jsx("div", {
647
+ className: "lms-tabs",
648
+ role: "tablist",
649
+ "aria-label": "Course editor",
650
+ children: [
651
+ "details",
652
+ "curriculum",
653
+ "access"
654
+ ].map((value) => /* @__PURE__ */ jsx(Button, {
655
+ role: "tab",
656
+ "aria-selected": tab === value,
657
+ onClick: () => setTab(value),
658
+ children: value === "access" ? "Enrollment & completion" : value === "details" ? "Course details" : "Curriculum"
659
+ }, value))
660
+ }),
661
+ tab === "details" && /* @__PURE__ */ jsxs(Panel, { children: [
662
+ /* @__PURE__ */ jsxs("div", {
663
+ className: "lms-grid",
664
+ children: [
665
+ /* @__PURE__ */ jsx(Field, {
666
+ label: "Course title",
667
+ children: /* @__PURE__ */ jsx("input", {
668
+ value: course.title,
669
+ maxLength: 200,
670
+ onChange: (event) => patch({ title: event.target.value })
671
+ })
672
+ }),
673
+ /* @__PURE__ */ jsx(Field, {
674
+ label: "Course URL",
675
+ children: /* @__PURE__ */ jsx("input", {
676
+ value: course.slug,
677
+ onChange: (event) => patch({ slug: event.target.value }),
678
+ pattern: "[a-z0-9]+(-[a-z0-9]+)*"
679
+ })
680
+ }),
681
+ /* @__PURE__ */ jsx(Field, {
682
+ label: "Category",
683
+ children: /* @__PURE__ */ jsx("input", {
684
+ value: course.category,
685
+ onChange: (event) => patch({ category: event.target.value })
686
+ })
687
+ }),
688
+ /* @__PURE__ */ jsx(Field, {
689
+ label: "Level",
690
+ children: /* @__PURE__ */ jsx("select", {
691
+ value: course.level,
692
+ onChange: (event) => patch({ level: event.target.value }),
693
+ children: [
694
+ "all",
695
+ "beginner",
696
+ "intermediate",
697
+ "advanced"
698
+ ].map((level) => /* @__PURE__ */ jsx("option", { children: level }, level))
699
+ })
700
+ }),
701
+ /* @__PURE__ */ jsx(Field, {
702
+ label: "Language",
703
+ children: /* @__PURE__ */ jsx("input", {
704
+ value: course.language,
705
+ onChange: (event) => patch({ language: event.target.value }),
706
+ placeholder: "en, ta, hi…"
707
+ })
708
+ }),
709
+ /* @__PURE__ */ jsx(Field, {
710
+ label: "Instructor name",
711
+ children: /* @__PURE__ */ jsx("input", {
712
+ value: course.instructorName,
713
+ onChange: (event) => patch({ instructorName: event.target.value })
714
+ })
715
+ })
716
+ ]
717
+ }),
718
+ /* @__PURE__ */ jsx(Field, {
719
+ label: "Summary",
720
+ children: /* @__PURE__ */ jsx("textarea", {
721
+ rows: 3,
722
+ value: course.summary,
723
+ onChange: (event) => patch({ summary: event.target.value }),
724
+ maxLength: 1e3
725
+ })
726
+ }),
727
+ /* @__PURE__ */ jsx(Field, {
728
+ label: "Course description",
729
+ children: /* @__PURE__ */ jsx("textarea", {
730
+ rows: 7,
731
+ value: course.description,
732
+ onChange: (event) => patch({ description: event.target.value })
733
+ })
734
+ }),
735
+ /* @__PURE__ */ jsxs("div", {
736
+ className: "lms-grid",
737
+ children: [
738
+ /* @__PURE__ */ jsx(Field, {
739
+ label: "Learning outcomes",
740
+ help: "One outcome per line.",
741
+ children: /* @__PURE__ */ jsx("textarea", {
742
+ rows: 5,
743
+ value: course.outcomes.join("\n"),
744
+ onChange: (event) => patch({ outcomes: event.target.value.split("\n").filter(Boolean) })
745
+ })
746
+ }),
747
+ /* @__PURE__ */ jsx(Field, {
748
+ label: "Requirements",
749
+ help: "One requirement per line.",
750
+ children: /* @__PURE__ */ jsx("textarea", {
751
+ rows: 5,
752
+ value: course.requirements.join("\n"),
753
+ onChange: (event) => patch({ requirements: event.target.value.split("\n").filter(Boolean) })
754
+ })
755
+ }),
756
+ /* @__PURE__ */ jsx(Field, {
757
+ label: "Tags",
758
+ help: "Separate tags with commas.",
759
+ children: /* @__PURE__ */ jsx("input", {
760
+ value: course.tags.join(", "),
761
+ onChange: (event) => patch({ tags: event.target.value.split(",").map((tag) => tag.trim()).filter(Boolean) })
762
+ })
763
+ }),
764
+ /* @__PURE__ */ jsx(Field, {
765
+ label: "Course image URL",
766
+ children: /* @__PURE__ */ jsx("input", {
767
+ type: "url",
768
+ value: course.featuredImage?.src ?? "",
769
+ onChange: (event) => patch({ featuredImage: event.target.value ? {
770
+ id: course.featuredImage?.id ?? "",
771
+ src: event.target.value,
772
+ alt: course.featuredImage?.alt ?? ""
773
+ } : null })
774
+ })
775
+ }),
776
+ /* @__PURE__ */ jsx(Field, {
777
+ label: "Image description",
778
+ children: /* @__PURE__ */ jsx("input", {
779
+ value: course.featuredImage?.alt ?? "",
780
+ onChange: (event) => course.featuredImage && patch({ featuredImage: {
781
+ ...course.featuredImage,
782
+ alt: event.target.value
783
+ } })
784
+ })
785
+ }),
786
+ /* @__PURE__ */ jsxs("div", { children: [
787
+ /* @__PURE__ */ jsx(ChoicesField, {
788
+ label: "Additional instructors",
789
+ value: course.instructorIds,
790
+ choices: instructorChoices.items.map((account) => ({
791
+ id: account.id,
792
+ label: account.name || account.email
793
+ })),
794
+ onChange: (instructorIds) => patch({ instructorIds })
795
+ }),
796
+ instructorChoices.data?.hasMore && /* @__PURE__ */ jsx(Button, {
797
+ onClick: instructorChoices.more,
798
+ children: "Load more instructors"
799
+ }),
800
+ /* @__PURE__ */ jsx(Notice, { error: instructorChoices.error })
801
+ ] })
802
+ ]
803
+ })
804
+ ] }),
805
+ tab === "access" && /* @__PURE__ */ jsxs(Panel, { children: [
806
+ /* @__PURE__ */ jsxs("div", {
807
+ className: "lms-grid",
808
+ children: [
809
+ /* @__PURE__ */ jsx(Field, {
810
+ label: "Enrollment",
811
+ children: /* @__PURE__ */ jsxs("select", {
812
+ value: course.enrollmentMode,
813
+ onChange: (event) => patch({ enrollmentMode: event.target.value }),
814
+ children: [
815
+ /* @__PURE__ */ jsx("option", {
816
+ value: "open",
817
+ children: "Free · open enrollment"
818
+ }),
819
+ /* @__PURE__ */ jsx("option", {
820
+ value: "approval",
821
+ children: "Administrator approval"
822
+ }),
823
+ /* @__PURE__ */ jsx("option", {
824
+ value: "invite",
825
+ children: "Invitation only"
826
+ })
827
+ ]
828
+ })
829
+ }),
830
+ /* @__PURE__ */ jsx(Field, {
831
+ label: "Access duration in days",
832
+ help: "0 gives unlimited access.",
833
+ children: /* @__PURE__ */ jsx("input", {
834
+ type: "number",
835
+ min: 0,
836
+ max: 3650,
837
+ value: course.accessDays,
838
+ onChange: (event) => patch({ accessDays: Number(event.target.value) })
839
+ })
840
+ }),
841
+ /* @__PURE__ */ jsx(Field, {
842
+ label: "Enrollment closes",
843
+ children: /* @__PURE__ */ jsx("input", {
844
+ type: "datetime-local",
845
+ value: localDate(course.enrollmentClosesAt),
846
+ onChange: (event) => patch({ enrollmentClosesAt: isoDate(event.target.value) })
847
+ })
848
+ }),
849
+ /* @__PURE__ */ jsxs("div", { children: [
850
+ /* @__PURE__ */ jsx(ChoicesField, {
851
+ label: "Prerequisite courses",
852
+ value: course.prerequisiteCourseIds,
853
+ choices: prerequisiteChoices.items.filter((value) => value.id !== course.id).map((value) => ({
854
+ id: value.id,
855
+ label: value.title
856
+ })),
857
+ onChange: (prerequisiteCourseIds) => patch({ prerequisiteCourseIds })
858
+ }),
859
+ prerequisiteChoices.data?.hasMore && /* @__PURE__ */ jsx(Button, {
860
+ onClick: prerequisiteChoices.more,
861
+ children: "Load more courses"
862
+ }),
863
+ /* @__PURE__ */ jsx(Notice, { error: prerequisiteChoices.error })
864
+ ] })
865
+ ]
866
+ }),
867
+ /* @__PURE__ */ jsxs("label", {
868
+ className: "lms-check",
869
+ children: [/* @__PURE__ */ jsx("input", {
870
+ type: "checkbox",
871
+ checked: course.sequential,
872
+ onChange: (event) => patch({ sequential: event.target.checked })
873
+ }), "Require lessons in order"]
874
+ }),
875
+ /* @__PURE__ */ jsxs("label", {
876
+ className: "lms-check",
877
+ children: [/* @__PURE__ */ jsx("input", {
878
+ type: "checkbox",
879
+ checked: course.certificateEnabled,
880
+ onChange: (event) => patch({ certificateEnabled: event.target.checked })
881
+ }), "Issue a certificate after all required lessons are passed"]
882
+ })
883
+ ] }),
884
+ tab === "curriculum" && /* @__PURE__ */ jsxs("div", {
885
+ className: "lms-studio",
886
+ children: [/* @__PURE__ */ jsxs("aside", {
887
+ className: "lms-panel",
888
+ children: [
889
+ /* @__PURE__ */ jsx("h2", { children: "Modules & lessons" }),
890
+ course.modules.map((module, moduleIndex) => /* @__PURE__ */ jsxs("section", {
891
+ className: "lms-module",
892
+ children: [
893
+ /* @__PURE__ */ jsx(Field, {
894
+ label: `Module ${moduleIndex + 1}`,
895
+ children: /* @__PURE__ */ jsx("input", {
896
+ value: module.title,
897
+ onChange: (event) => updateModule(moduleIndex, { title: event.target.value })
898
+ })
899
+ }),
900
+ /* @__PURE__ */ jsxs("div", {
901
+ className: "lms-grid",
902
+ children: [/* @__PURE__ */ jsx(Field, {
903
+ label: "Release after days",
904
+ children: /* @__PURE__ */ jsx("input", {
905
+ type: "number",
906
+ min: 0,
907
+ value: module.releaseDays,
908
+ onChange: (event) => updateModule(moduleIndex, { releaseDays: Number(event.target.value) })
909
+ })
910
+ }), /* @__PURE__ */ jsx(Field, {
911
+ label: "Release date",
912
+ children: /* @__PURE__ */ jsx("input", {
913
+ type: "datetime-local",
914
+ value: localDate(module.unlockAt),
915
+ onChange: (event) => updateModule(moduleIndex, { unlockAt: isoDate(event.target.value) })
916
+ })
917
+ })]
918
+ }),
919
+ /* @__PURE__ */ jsx("ol", {
920
+ className: "lms-lesson-list",
921
+ children: module.lessons.map((lesson, lessonIndex) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsxs("button", {
922
+ className: "lms-select-lesson",
923
+ "aria-current": selected === lesson.id,
924
+ onClick: () => setSelected(lesson.id),
925
+ children: [lesson.title, /* @__PURE__ */ jsxs("small", { children: [lesson.kind, lesson.required ? " · required" : " · optional"] })]
926
+ }), /* @__PURE__ */ jsxs("div", {
927
+ className: "lms-actions",
928
+ children: [/* @__PURE__ */ jsx(Button, {
929
+ "aria-label": `Move ${lesson.title} up`,
930
+ disabled: lessonIndex === 0,
931
+ onClick: () => reorderLesson(moduleIndex, lessonIndex, -1),
932
+ children: "↑"
933
+ }), /* @__PURE__ */ jsx(Button, {
934
+ "aria-label": `Move ${lesson.title} down`,
935
+ disabled: lessonIndex === module.lessons.length - 1,
936
+ onClick: () => reorderLesson(moduleIndex, lessonIndex, 1),
937
+ children: "↓"
938
+ })]
939
+ })] }, lesson.id))
940
+ }),
941
+ /* @__PURE__ */ jsx(Button, {
942
+ onClick: () => {
943
+ const lesson = lessonSchema.parse({
944
+ id: crypto.randomUUID(),
945
+ title: "New lesson"
946
+ });
947
+ updateModule(moduleIndex, { lessons: [...module.lessons, lesson] });
948
+ setSelected(lesson.id);
949
+ },
950
+ children: "Add lesson"
951
+ }),
952
+ module.lessons.length === 0 && /* @__PURE__ */ jsx(Button, {
953
+ danger: true,
954
+ onClick: () => patch({ modules: course.modules.filter((item) => item.id !== module.id) }),
955
+ children: "Remove empty module"
956
+ }),
957
+ /* @__PURE__ */ jsx("div", {
958
+ className: "lms-actions",
959
+ children: /* @__PURE__ */ jsx(Button, {
960
+ disabled: moduleIndex === 0,
961
+ "aria-label": `Move module ${module.title} up`,
962
+ onClick: () => {
963
+ const next = [...course.modules];
964
+ [next[moduleIndex], next[moduleIndex - 1]] = [next[moduleIndex - 1], next[moduleIndex]];
965
+ patch({ modules: next });
966
+ },
967
+ children: "Move module up"
968
+ })
969
+ })
970
+ ]
971
+ }, module.id)),
972
+ /* @__PURE__ */ jsx(Button, {
973
+ onClick: () => patch({ modules: [...course.modules, moduleSchema.parse({
974
+ id: crypto.randomUUID(),
975
+ title: `Module ${course.modules.length + 1}`
976
+ })] }),
977
+ children: "Add module"
978
+ })
979
+ ]
980
+ }), /* @__PURE__ */ jsxs(Panel, {
981
+ title: selectedLesson?.title ?? "Choose a lesson",
982
+ children: [selectedLesson && /* @__PURE__ */ jsxs("div", {
983
+ className: "lms-actions",
984
+ children: [/* @__PURE__ */ jsx(Button, {
985
+ onClick: () => {
986
+ const copy = {
987
+ ...structuredClone(selectedLesson),
988
+ id: crypto.randomUUID(),
989
+ title: `Copy of ${selectedLesson.title}`.slice(0, 200)
990
+ };
991
+ patch({ modules: course.modules.map((module) => ({
992
+ ...module,
993
+ lessons: module.lessons.flatMap((lesson) => lesson.id === selectedLesson.id ? [lesson, copy] : [lesson])
994
+ })) });
995
+ setSelected(copy.id);
996
+ },
997
+ children: "Duplicate lesson"
998
+ }), /* @__PURE__ */ jsx(Field, {
999
+ label: "Move lesson to module",
1000
+ children: /* @__PURE__ */ jsx("select", {
1001
+ value: course.modules.find((m) => m.lessons.some((l) => l.id === selectedLesson.id))?.id,
1002
+ onChange: (event) => {
1003
+ const target = event.target.value;
1004
+ patch({ modules: course.modules.map((module) => ({
1005
+ ...module,
1006
+ lessons: module.id === target ? [...module.lessons, selectedLesson] : module.lessons.filter((lesson) => lesson.id !== selectedLesson.id)
1007
+ })) });
1008
+ },
1009
+ children: course.modules.map((module) => /* @__PURE__ */ jsx("option", {
1010
+ value: module.id,
1011
+ children: module.title
1012
+ }, module.id))
1013
+ })
1014
+ })]
1015
+ }), selectedLesson ? /* @__PURE__ */ jsx(LessonEditor, {
1016
+ lesson: selectedLesson,
1017
+ previewUrl: course.id && academy.data && JSON.parse(saved).modules.some((module) => module.lessons.some((lesson) => lesson.id === selectedLesson.id)) ? `${academy.data.basePath}/teach/courses/${encodeURIComponent(course.id)}/lessons/${encodeURIComponent(selectedLesson.id)}` : void 0,
1018
+ update: (data) => updateLesson(selectedLesson.id, data),
1019
+ remove: () => {
1020
+ if (window.confirm(`Remove “${selectedLesson.title}” from the draft?`)) {
1021
+ patch({ modules: course.modules.map((module) => ({
1022
+ ...module,
1023
+ lessons: module.lessons.filter((lesson) => lesson.id !== selectedLesson.id)
1024
+ })) });
1025
+ setSelected(null);
1026
+ }
1027
+ }
1028
+ }, selectedLesson.id) : /* @__PURE__ */ jsx("p", { children: "Add a module and a lesson to begin." })]
1029
+ })]
1030
+ })
1031
+ ]
1032
+ });
1033
+ }
1034
+ function ContentPicker({ value, onChange }) {
1035
+ const schema = useLoad("admin/content/collections");
1036
+ const [collection, setCollection] = useState(value?.collection ?? "");
1037
+ const [cursor, setCursor] = useState();
1038
+ const [entries, setEntries] = useState([]);
1039
+ const list = useLoad("admin/content/entries", {
1040
+ collection,
1041
+ cursor,
1042
+ limit: 100
1043
+ }, !!collection);
1044
+ useEffect(() => {
1045
+ if (list.data) setEntries((old) => [...new Map([...old, ...list.data.items].map((entry) => [entry.id, entry])).values()]);
1046
+ }, [list.data]);
1047
+ const fields = schema.data?.find((entry) => entry.slug === collection)?.fields ?? [];
1048
+ return /* @__PURE__ */ jsxs("details", { children: [
1049
+ /* @__PURE__ */ jsx("summary", { children: "Use published EmDash content" }),
1050
+ /* @__PURE__ */ jsx("p", { children: "Choose a published entry and its text field to reuse formatted lesson content." }),
1051
+ /* @__PURE__ */ jsx(Notice, { error: schema.error || list.error }),
1052
+ /* @__PURE__ */ jsxs("div", {
1053
+ className: "lms-grid",
1054
+ children: [/* @__PURE__ */ jsx(Field, {
1055
+ label: "Collection",
1056
+ children: /* @__PURE__ */ jsxs("select", {
1057
+ value: collection,
1058
+ onChange: (event) => {
1059
+ setCollection(event.target.value);
1060
+ setCursor(void 0);
1061
+ setEntries([]);
1062
+ },
1063
+ children: [/* @__PURE__ */ jsx("option", {
1064
+ value: "",
1065
+ children: "Choose a collection"
1066
+ }), schema.data?.map((entry) => /* @__PURE__ */ jsx("option", {
1067
+ value: entry.slug,
1068
+ children: entry.label
1069
+ }, entry.slug))]
1070
+ })
1071
+ }), collection && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Field, {
1072
+ label: "Published entry",
1073
+ children: /* @__PURE__ */ jsxs("select", {
1074
+ value: value?.collection === collection ? value.id : "",
1075
+ onChange: (event) => event.target.value && onChange(event.target.value ? {
1076
+ collection,
1077
+ id: event.target.value,
1078
+ field: (value?.collection === collection ? value.field : void 0) ?? fields.find((field) => field.type === "portableText")?.slug ?? fields[0]?.slug ?? "content"
1079
+ } : null),
1080
+ children: [
1081
+ /* @__PURE__ */ jsx("option", {
1082
+ value: "",
1083
+ children: "Choose an entry"
1084
+ }),
1085
+ value?.collection === collection && !entries.some((entry) => entry.id === value.id) && /* @__PURE__ */ jsx("option", {
1086
+ value: value.id,
1087
+ children: "Current linked entry"
1088
+ }),
1089
+ entries.map((entry) => /* @__PURE__ */ jsx("option", {
1090
+ value: entry.id,
1091
+ children: entry.title
1092
+ }, entry.id))
1093
+ ]
1094
+ })
1095
+ }), /* @__PURE__ */ jsx(Field, {
1096
+ label: "Content field",
1097
+ children: /* @__PURE__ */ jsxs("select", {
1098
+ value: value?.collection === collection ? value.field : "",
1099
+ disabled: !value || value.collection !== collection,
1100
+ onChange: (event) => value && onChange({
1101
+ ...value,
1102
+ field: event.target.value
1103
+ }),
1104
+ children: [/* @__PURE__ */ jsx("option", {
1105
+ value: "",
1106
+ children: "Choose a field"
1107
+ }), fields.map((field) => /* @__PURE__ */ jsx("option", {
1108
+ value: field.slug,
1109
+ children: field.label
1110
+ }, field.slug))]
1111
+ })
1112
+ })] })]
1113
+ }),
1114
+ list.data?.hasMore && /* @__PURE__ */ jsx(Button, {
1115
+ onClick: () => setCursor(list.data?.cursor),
1116
+ children: "Load more entries"
1117
+ })
1118
+ ] });
1119
+ }
1120
+ function LessonEditor({ lesson, previewUrl, update, remove }) {
1121
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1122
+ /* @__PURE__ */ jsxs("div", {
1123
+ className: "lms-grid",
1124
+ children: [
1125
+ /* @__PURE__ */ jsx(Field, {
1126
+ label: "Lesson title",
1127
+ children: /* @__PURE__ */ jsx("input", {
1128
+ value: lesson.title,
1129
+ onChange: (event) => update({ title: event.target.value })
1130
+ })
1131
+ }),
1132
+ /* @__PURE__ */ jsx(Field, {
1133
+ label: "Lesson type",
1134
+ children: /* @__PURE__ */ jsx("select", {
1135
+ value: lesson.kind,
1136
+ onChange: (event) => {
1137
+ const kind = event.target.value;
1138
+ update({
1139
+ kind,
1140
+ preview: [
1141
+ "quiz",
1142
+ "assignment",
1143
+ "live"
1144
+ ].includes(kind) ? false : lesson.preview,
1145
+ quiz: kind === "quiz" ? lesson.quiz ?? quizSchema.parse({ questions: [defaultQuestion()] }) : null,
1146
+ assignment: kind === "assignment" ? lesson.assignment ?? assignmentSchema.parse({ instructions: "Submit your work here." }) : null
1147
+ });
1148
+ },
1149
+ children: [
1150
+ "text",
1151
+ "video",
1152
+ "audio",
1153
+ "quiz",
1154
+ "assignment",
1155
+ "live"
1156
+ ].map((kind) => /* @__PURE__ */ jsx("option", { children: kind }, kind))
1157
+ })
1158
+ }),
1159
+ /* @__PURE__ */ jsx(Field, {
1160
+ label: "Estimated minutes",
1161
+ children: /* @__PURE__ */ jsx("input", {
1162
+ type: "number",
1163
+ min: 0,
1164
+ value: lesson.durationMinutes,
1165
+ onChange: (event) => update({ durationMinutes: Number(event.target.value) })
1166
+ })
1167
+ }),
1168
+ /* @__PURE__ */ jsx(Field, {
1169
+ label: "Minimum study seconds",
1170
+ children: /* @__PURE__ */ jsx("input", {
1171
+ type: "number",
1172
+ min: 0,
1173
+ value: lesson.minStudySeconds,
1174
+ onChange: (event) => update({ minStudySeconds: Number(event.target.value) })
1175
+ })
1176
+ })
1177
+ ]
1178
+ }),
1179
+ /* @__PURE__ */ jsx(Field, {
1180
+ label: "Summary",
1181
+ children: /* @__PURE__ */ jsx("textarea", {
1182
+ rows: 2,
1183
+ value: lesson.summary,
1184
+ onChange: (event) => update({ summary: event.target.value })
1185
+ })
1186
+ }),
1187
+ /* @__PURE__ */ jsxs("section", {
1188
+ className: "lms-notice",
1189
+ children: [
1190
+ /* @__PURE__ */ jsxs("strong", { children: [
1191
+ "Content source:",
1192
+ " ",
1193
+ lesson.contentSource ? "Published CMS content" : lesson.portableText.length ? "Imported rich content" : "Inline text"
1194
+ ] }),
1195
+ previewUrl ? /* @__PURE__ */ jsxs("p", { children: [
1196
+ /* @__PURE__ */ jsx("a", {
1197
+ href: previewUrl,
1198
+ target: "_blank",
1199
+ rel: "noopener noreferrer",
1200
+ children: "Preview saved lesson ↗"
1201
+ }),
1202
+ " ",
1203
+ "· Save changes first to include them in this preview."
1204
+ ] }) : /* @__PURE__ */ jsx("p", { children: "Save this lesson to enable its preview." }),
1205
+ lesson.contentSource ? /* @__PURE__ */ jsxs(Fragment, { children: [
1206
+ /* @__PURE__ */ jsxs("p", { children: [
1207
+ "This lesson displays the published “",
1208
+ lesson.contentSource.field,
1209
+ "” field from",
1210
+ " ",
1211
+ lesson.contentSource.collection,
1212
+ ". Local content is retained while linked."
1213
+ ] }),
1214
+ /* @__PURE__ */ jsxs("p", { children: [
1215
+ /* @__PURE__ */ jsx("a", {
1216
+ href: `/_emdash/admin/content/${encodeURIComponent(lesson.contentSource.collection)}/${encodeURIComponent(lesson.contentSource.id)}`,
1217
+ target: "_blank",
1218
+ rel: "noopener noreferrer",
1219
+ children: "Edit linked entry in EmDash ↗"
1220
+ }),
1221
+ " ",
1222
+ "· Publish entry changes, then publish the saved course draft to create a new learner edition."
1223
+ ] }),
1224
+ /* @__PURE__ */ jsx(Button, {
1225
+ onClick: () => {
1226
+ if (window.confirm("Unlink the CMS entry and use this lesson's retained local content? Save the course to apply.")) update({ contentSource: null });
1227
+ },
1228
+ children: "Use retained local content"
1229
+ })
1230
+ ] }) : lesson.portableText.length > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("p", { children: "Imported rich content is read-only here. Use the saved preview to inspect it, or link a published CMS entry below to edit rich content in EmDash." }), /* @__PURE__ */ jsx(Button, {
1231
+ onClick: () => {
1232
+ if (window.confirm("Remove this lesson's imported rich content and use its retained plain text instead? This removes formatting when you save.")) update({ portableText: [] });
1233
+ },
1234
+ children: "Replace rich content with inline text"
1235
+ })] }) : /* @__PURE__ */ jsx("p", { children: "Edit the inline text below, or select a published CMS entry." })
1236
+ ]
1237
+ }),
1238
+ !lesson.contentSource && lesson.portableText.length === 0 && /* @__PURE__ */ jsx(Field, {
1239
+ label: "Lesson text",
1240
+ children: /* @__PURE__ */ jsx("textarea", {
1241
+ rows: 12,
1242
+ value: lesson.body,
1243
+ onChange: (event) => update({ body: event.target.value })
1244
+ })
1245
+ }),
1246
+ [
1247
+ "video",
1248
+ "audio",
1249
+ "live"
1250
+ ].includes(lesson.kind) && /* @__PURE__ */ jsx(Field, {
1251
+ label: lesson.kind === "live" ? "Meeting link" : "Media URL",
1252
+ help: "Use a direct media file, YouTube link, or Vimeo link.",
1253
+ children: /* @__PURE__ */ jsx("input", {
1254
+ type: "url",
1255
+ value: lesson.mediaUrl ?? "",
1256
+ onChange: (event) => update({ mediaUrl: event.target.value || null })
1257
+ })
1258
+ }),
1259
+ lesson.kind === "live" && /* @__PURE__ */ jsx(Field, {
1260
+ label: "Session starts",
1261
+ children: /* @__PURE__ */ jsx("input", {
1262
+ type: "datetime-local",
1263
+ value: localDate(lesson.liveStartsAt),
1264
+ onChange: (event) => update({ liveStartsAt: isoDate(event.target.value) })
1265
+ })
1266
+ }),
1267
+ ["video", "audio"].includes(lesson.kind) && /* @__PURE__ */ jsxs("section", { children: [
1268
+ /* @__PURE__ */ jsx(Field, {
1269
+ label: "Transcript",
1270
+ help: "Readable text shown alongside native and embedded media.",
1271
+ children: /* @__PURE__ */ jsx("textarea", {
1272
+ rows: 6,
1273
+ maxLength: 5e4,
1274
+ value: lesson.transcript ?? "",
1275
+ onChange: (event) => update({ transcript: event.target.value })
1276
+ })
1277
+ }),
1278
+ /* @__PURE__ */ jsx("h3", { children: "Caption tracks" }),
1279
+ /* @__PURE__ */ jsx("p", { children: "WebVTT tracks apply to native media. Use same-origin files or configure CORS on both media and caption hosts. Embedded players manage their own captions." }),
1280
+ (lesson.captions ?? []).map((track, index) => /* @__PURE__ */ jsxs("div", { children: [
1281
+ /* @__PURE__ */ jsx(Field, {
1282
+ label: `Track ${index + 1} URL`,
1283
+ children: /* @__PURE__ */ jsx("input", {
1284
+ value: track.src,
1285
+ onChange: (event) => update({ captions: lesson.captions.map((item, i) => i === index ? {
1286
+ ...item,
1287
+ src: event.target.value
1288
+ } : item) })
1289
+ })
1290
+ }),
1291
+ /* @__PURE__ */ jsx(Field, {
1292
+ label: "Language code",
1293
+ children: /* @__PURE__ */ jsx("input", {
1294
+ value: track.language,
1295
+ onChange: (event) => update({ captions: lesson.captions.map((item, i) => i === index ? {
1296
+ ...item,
1297
+ language: event.target.value
1298
+ } : item) })
1299
+ })
1300
+ }),
1301
+ /* @__PURE__ */ jsx(Field, {
1302
+ label: "Caption label",
1303
+ children: /* @__PURE__ */ jsx("input", {
1304
+ value: track.label,
1305
+ onChange: (event) => update({ captions: lesson.captions.map((item, i) => i === index ? {
1306
+ ...item,
1307
+ label: event.target.value
1308
+ } : item) })
1309
+ })
1310
+ }),
1311
+ /* @__PURE__ */ jsx(Button, {
1312
+ onClick: () => update({ captions: lesson.captions.filter((_, i) => i !== index) }),
1313
+ children: "Remove track"
1314
+ })
1315
+ ] }, index)),
1316
+ /* @__PURE__ */ jsx(Button, {
1317
+ disabled: (lesson.captions?.length ?? 0) >= 20,
1318
+ onClick: () => update({ captions: [...lesson.captions ?? [], {
1319
+ src: "",
1320
+ language: "en",
1321
+ label: "English"
1322
+ }] }),
1323
+ children: "Add captions"
1324
+ })
1325
+ ] }),
1326
+ /* @__PURE__ */ jsxs("div", {
1327
+ className: "lms-actions",
1328
+ children: [/* @__PURE__ */ jsxs("label", {
1329
+ className: "lms-check",
1330
+ children: [/* @__PURE__ */ jsx("input", {
1331
+ type: "checkbox",
1332
+ checked: lesson.required,
1333
+ onChange: (event) => update({ required: event.target.checked })
1334
+ }), "Required for completion"]
1335
+ }), ![
1336
+ "quiz",
1337
+ "assignment",
1338
+ "live"
1339
+ ].includes(lesson.kind) && /* @__PURE__ */ jsxs("label", {
1340
+ className: "lms-check",
1341
+ children: [/* @__PURE__ */ jsx("input", {
1342
+ type: "checkbox",
1343
+ checked: lesson.preview,
1344
+ onChange: (event) => update({ preview: event.target.checked })
1345
+ }), "Free preview"]
1346
+ })]
1347
+ }),
1348
+ /* @__PURE__ */ jsx(ContentPicker, {
1349
+ value: lesson.contentSource,
1350
+ onChange: (contentSource) => {
1351
+ if (contentSource && window.confirm("Use this published CMS field as the lesson content? Local content will be retained. Save the course to apply.")) update({ contentSource });
1352
+ }
1353
+ }, lesson.id),
1354
+ lesson.kind === "quiz" && lesson.quiz && /* @__PURE__ */ jsx(QuizEditor, {
1355
+ quiz: lesson.quiz,
1356
+ update: (quiz) => update({ quiz })
1357
+ }),
1358
+ lesson.kind === "assignment" && lesson.assignment && /* @__PURE__ */ jsxs(Panel, {
1359
+ title: "Assignment",
1360
+ children: [
1361
+ /* @__PURE__ */ jsx(Field, {
1362
+ label: "Instructions",
1363
+ children: /* @__PURE__ */ jsx("textarea", {
1364
+ rows: 6,
1365
+ value: lesson.assignment.instructions,
1366
+ onChange: (event) => update({ assignment: {
1367
+ ...lesson.assignment,
1368
+ instructions: event.target.value
1369
+ } })
1370
+ })
1371
+ }),
1372
+ /* @__PURE__ */ jsxs("div", {
1373
+ className: "lms-grid",
1374
+ children: [
1375
+ /* @__PURE__ */ jsx(Field, {
1376
+ label: "Maximum points",
1377
+ children: /* @__PURE__ */ jsx("input", {
1378
+ type: "number",
1379
+ min: 1,
1380
+ value: lesson.assignment.maxPoints,
1381
+ onChange: (event) => update({ assignment: {
1382
+ ...lesson.assignment,
1383
+ maxPoints: Number(event.target.value)
1384
+ } })
1385
+ })
1386
+ }),
1387
+ /* @__PURE__ */ jsx(Field, {
1388
+ label: "Passing points",
1389
+ children: /* @__PURE__ */ jsx("input", {
1390
+ type: "number",
1391
+ min: 1,
1392
+ value: lesson.assignment.passingPoints,
1393
+ onChange: (event) => update({ assignment: {
1394
+ ...lesson.assignment,
1395
+ passingPoints: Number(event.target.value)
1396
+ } })
1397
+ })
1398
+ }),
1399
+ /* @__PURE__ */ jsx(Field, {
1400
+ label: "Due date",
1401
+ children: /* @__PURE__ */ jsx("input", {
1402
+ type: "datetime-local",
1403
+ value: localDate(lesson.assignment.dueAt),
1404
+ onChange: (event) => update({ assignment: {
1405
+ ...lesson.assignment,
1406
+ dueAt: isoDate(event.target.value)
1407
+ } })
1408
+ })
1409
+ })
1410
+ ]
1411
+ }),
1412
+ /* @__PURE__ */ jsxs("label", {
1413
+ className: "lms-check",
1414
+ children: [/* @__PURE__ */ jsx("input", {
1415
+ type: "checkbox",
1416
+ checked: lesson.assignment.allowResubmission,
1417
+ onChange: (event) => update({ assignment: {
1418
+ ...lesson.assignment,
1419
+ allowResubmission: event.target.checked
1420
+ } })
1421
+ }), "Allow another submission after a failing grade"]
1422
+ })
1423
+ ]
1424
+ }),
1425
+ /* @__PURE__ */ jsxs(Panel, {
1426
+ title: "Lesson resources",
1427
+ children: [lesson.resources.map((resource, index) => /* @__PURE__ */ jsxs("div", {
1428
+ className: "lms-resource",
1429
+ children: [
1430
+ /* @__PURE__ */ jsx(Field, {
1431
+ label: `Resource ${index + 1}`,
1432
+ children: /* @__PURE__ */ jsx("input", {
1433
+ value: resource.title,
1434
+ onChange: (event) => update({ resources: lesson.resources.map((item) => item.id === resource.id ? {
1435
+ ...item,
1436
+ title: event.target.value
1437
+ } : item) })
1438
+ })
1439
+ }),
1440
+ /* @__PURE__ */ jsx(Field, {
1441
+ label: "URL",
1442
+ children: /* @__PURE__ */ jsx("input", {
1443
+ value: resource.url,
1444
+ onChange: (event) => update({ resources: lesson.resources.map((item) => item.id === resource.id ? {
1445
+ ...item,
1446
+ url: event.target.value
1447
+ } : item) })
1448
+ })
1449
+ }),
1450
+ /* @__PURE__ */ jsx(Button, {
1451
+ danger: true,
1452
+ onClick: () => update({ resources: lesson.resources.filter((item) => item.id !== resource.id) }),
1453
+ children: "Remove resource"
1454
+ })
1455
+ ]
1456
+ }, resource.id)), /* @__PURE__ */ jsx(Button, {
1457
+ onClick: () => update({ resources: [...lesson.resources, {
1458
+ id: crypto.randomUUID(),
1459
+ title: "New resource",
1460
+ url: "/",
1461
+ description: ""
1462
+ }] }),
1463
+ children: "Add resource"
1464
+ })]
1465
+ }),
1466
+ /* @__PURE__ */ jsx(Button, {
1467
+ danger: true,
1468
+ onClick: remove,
1469
+ children: "Remove lesson"
1470
+ })
1471
+ ] });
1472
+ }
1473
+ function defaultQuestion() {
1474
+ return questionSchema.parse({
1475
+ id: crypto.randomUUID(),
1476
+ type: "single",
1477
+ prompt: "New question",
1478
+ options: [{
1479
+ id: "a",
1480
+ text: "First option"
1481
+ }, {
1482
+ id: "b",
1483
+ text: "Second option"
1484
+ }],
1485
+ correctAnswers: ["a"]
1486
+ });
1487
+ }
1488
+ function QuizEditor({ quiz, update }) {
1489
+ const patchQuestion = (id, data) => update({
1490
+ ...quiz,
1491
+ questions: quiz.questions.map((question) => question.id === id ? {
1492
+ ...question,
1493
+ ...data
1494
+ } : question)
1495
+ });
1496
+ return /* @__PURE__ */ jsxs(Panel, {
1497
+ title: "Quiz",
1498
+ children: [
1499
+ /* @__PURE__ */ jsxs("div", {
1500
+ className: "lms-grid",
1501
+ children: [
1502
+ /* @__PURE__ */ jsx(Field, {
1503
+ label: "Passing percentage",
1504
+ children: /* @__PURE__ */ jsx("input", {
1505
+ type: "number",
1506
+ min: 1,
1507
+ max: 100,
1508
+ value: quiz.passingPercent,
1509
+ onChange: (event) => update({
1510
+ ...quiz,
1511
+ passingPercent: Number(event.target.value)
1512
+ })
1513
+ })
1514
+ }),
1515
+ /* @__PURE__ */ jsx(Field, {
1516
+ label: "Maximum attempts",
1517
+ children: /* @__PURE__ */ jsx("input", {
1518
+ type: "number",
1519
+ min: 1,
1520
+ max: 20,
1521
+ value: quiz.maxAttempts,
1522
+ onChange: (event) => update({
1523
+ ...quiz,
1524
+ maxAttempts: Number(event.target.value)
1525
+ })
1526
+ })
1527
+ }),
1528
+ /* @__PURE__ */ jsx(Field, {
1529
+ label: "Time limit in minutes",
1530
+ help: "0 gives an untimed quiz.",
1531
+ children: /* @__PURE__ */ jsx("input", {
1532
+ type: "number",
1533
+ min: 0,
1534
+ max: 180,
1535
+ value: quiz.timeLimitMinutes,
1536
+ onChange: (event) => update({
1537
+ ...quiz,
1538
+ timeLimitMinutes: Number(event.target.value)
1539
+ })
1540
+ })
1541
+ }),
1542
+ /* @__PURE__ */ jsx(Field, {
1543
+ label: "Show explanations",
1544
+ children: /* @__PURE__ */ jsxs("select", {
1545
+ value: quiz.showFeedback,
1546
+ onChange: (event) => update({
1547
+ ...quiz,
1548
+ showFeedback: event.target.value
1549
+ }),
1550
+ children: [
1551
+ /* @__PURE__ */ jsx("option", {
1552
+ value: "after_pass",
1553
+ children: "After passing"
1554
+ }),
1555
+ /* @__PURE__ */ jsx("option", {
1556
+ value: "after_submit",
1557
+ children: "After each submission"
1558
+ }),
1559
+ /* @__PURE__ */ jsx("option", {
1560
+ value: "never",
1561
+ children: "Never"
1562
+ })
1563
+ ]
1564
+ })
1565
+ })
1566
+ ]
1567
+ }),
1568
+ /* @__PURE__ */ jsxs("div", {
1569
+ className: "lms-actions",
1570
+ children: [/* @__PURE__ */ jsxs("label", {
1571
+ className: "lms-check",
1572
+ children: [/* @__PURE__ */ jsx("input", {
1573
+ type: "checkbox",
1574
+ checked: quiz.randomizeQuestions,
1575
+ onChange: (event) => update({
1576
+ ...quiz,
1577
+ randomizeQuestions: event.target.checked
1578
+ })
1579
+ }), "Shuffle questions"]
1580
+ }), /* @__PURE__ */ jsxs("label", {
1581
+ className: "lms-check",
1582
+ children: [/* @__PURE__ */ jsx("input", {
1583
+ type: "checkbox",
1584
+ checked: quiz.randomizeOptions,
1585
+ onChange: (event) => update({
1586
+ ...quiz,
1587
+ randomizeOptions: event.target.checked
1588
+ })
1589
+ }), "Shuffle choices"]
1590
+ })]
1591
+ }),
1592
+ quiz.questions.map((question, index) => /* @__PURE__ */ jsxs("section", {
1593
+ className: "lms-question",
1594
+ children: [
1595
+ /* @__PURE__ */ jsxs("h3", { children: ["Question ", index + 1] }),
1596
+ /* @__PURE__ */ jsxs("div", {
1597
+ className: "lms-grid",
1598
+ children: [/* @__PURE__ */ jsx(Field, {
1599
+ label: "Type",
1600
+ children: /* @__PURE__ */ jsx("select", {
1601
+ value: question.type,
1602
+ onChange: (event) => {
1603
+ const type = event.target.value;
1604
+ const options = type === "true_false" ? [{
1605
+ id: "true",
1606
+ text: "True"
1607
+ }, {
1608
+ id: "false",
1609
+ text: "False"
1610
+ }] : type === "short" ? [] : question.options.length ? question.options : [{
1611
+ id: "a",
1612
+ text: "First option"
1613
+ }, {
1614
+ id: "b",
1615
+ text: "Second option"
1616
+ }];
1617
+ patchQuestion(question.id, {
1618
+ type,
1619
+ options,
1620
+ correctAnswers: type === "short" ? ["Accepted answer"] : [options[0].id]
1621
+ });
1622
+ },
1623
+ children: [
1624
+ ["single", "Single choice"],
1625
+ ["multiple", "Multiple choices"],
1626
+ ["true_false", "True or false"],
1627
+ ["short", "Short answer"]
1628
+ ].map(([value, label]) => /* @__PURE__ */ jsx("option", {
1629
+ value,
1630
+ children: label
1631
+ }, value))
1632
+ })
1633
+ }), /* @__PURE__ */ jsx(Field, {
1634
+ label: "Points",
1635
+ children: /* @__PURE__ */ jsx("input", {
1636
+ type: "number",
1637
+ min: 1,
1638
+ max: 100,
1639
+ value: question.points,
1640
+ onChange: (event) => patchQuestion(question.id, { points: Number(event.target.value) })
1641
+ })
1642
+ })]
1643
+ }),
1644
+ /* @__PURE__ */ jsx(Field, {
1645
+ label: "Question",
1646
+ children: /* @__PURE__ */ jsx("textarea", {
1647
+ rows: 3,
1648
+ value: question.prompt,
1649
+ onChange: (event) => patchQuestion(question.id, { prompt: event.target.value })
1650
+ })
1651
+ }),
1652
+ question.type === "short" ? /* @__PURE__ */ jsx(Field, {
1653
+ label: "Accepted answers",
1654
+ help: "One answer per line. Matching ignores case and extra spaces.",
1655
+ children: /* @__PURE__ */ jsx("textarea", {
1656
+ value: question.correctAnswers.join("\n"),
1657
+ onChange: (event) => patchQuestion(question.id, { correctAnswers: event.target.value.split("\n").filter(Boolean) })
1658
+ })
1659
+ }) : /* @__PURE__ */ jsxs("fieldset", { children: [
1660
+ /* @__PURE__ */ jsx("legend", { children: "Answer choices · select the correct answers" }),
1661
+ question.options.map((option) => /* @__PURE__ */ jsxs("div", {
1662
+ className: "lms-option",
1663
+ children: [
1664
+ /* @__PURE__ */ jsx("input", {
1665
+ "aria-label": `Mark ${option.text} correct`,
1666
+ type: question.type === "multiple" ? "checkbox" : "radio",
1667
+ name: `correct-${question.id}`,
1668
+ checked: question.correctAnswers.includes(option.id),
1669
+ onChange: (event) => patchQuestion(question.id, { correctAnswers: question.type === "multiple" ? event.target.checked ? [...question.correctAnswers, option.id] : question.correctAnswers.filter((id) => id !== option.id) : [option.id] })
1670
+ }),
1671
+ /* @__PURE__ */ jsx("input", {
1672
+ "aria-label": "Answer text",
1673
+ value: option.text,
1674
+ onChange: (event) => patchQuestion(question.id, { options: question.options.map((item) => item.id === option.id ? {
1675
+ ...item,
1676
+ text: event.target.value
1677
+ } : item) })
1678
+ }),
1679
+ question.type !== "true_false" && question.options.length > 2 && /* @__PURE__ */ jsx(Button, {
1680
+ "aria-label": `Remove answer ${option.text}`,
1681
+ onClick: () => patchQuestion(question.id, {
1682
+ options: question.options.filter((item) => item.id !== option.id),
1683
+ correctAnswers: question.correctAnswers.filter((id) => id !== option.id)
1684
+ }),
1685
+ children: "×"
1686
+ })
1687
+ ]
1688
+ }, option.id)),
1689
+ question.type !== "true_false" && question.options.length < 10 && /* @__PURE__ */ jsx(Button, {
1690
+ onClick: () => patchQuestion(question.id, { options: [...question.options, {
1691
+ id: crypto.randomUUID(),
1692
+ text: "New option"
1693
+ }] }),
1694
+ children: "Add answer choice"
1695
+ })
1696
+ ] }),
1697
+ /* @__PURE__ */ jsx(Field, {
1698
+ label: "Explanation",
1699
+ children: /* @__PURE__ */ jsx("textarea", {
1700
+ rows: 2,
1701
+ value: question.explanation,
1702
+ onChange: (event) => patchQuestion(question.id, { explanation: event.target.value })
1703
+ })
1704
+ }),
1705
+ /* @__PURE__ */ jsxs("div", {
1706
+ className: "lms-actions",
1707
+ children: [/* @__PURE__ */ jsx(Button, {
1708
+ disabled: index === 0,
1709
+ "aria-label": `Move question ${index + 1} up`,
1710
+ onClick: () => {
1711
+ const next = [...quiz.questions];
1712
+ [next[index], next[index - 1]] = [next[index - 1], next[index]];
1713
+ update({
1714
+ ...quiz,
1715
+ questions: next
1716
+ });
1717
+ },
1718
+ children: "Move up"
1719
+ }), /* @__PURE__ */ jsx(Button, {
1720
+ danger: true,
1721
+ disabled: quiz.questions.length < 2,
1722
+ onClick: () => update({
1723
+ ...quiz,
1724
+ questions: quiz.questions.filter((item) => item.id !== question.id)
1725
+ }),
1726
+ children: "Remove question"
1727
+ })]
1728
+ })
1729
+ ]
1730
+ }, question.id)),
1731
+ quiz.questions.length < 50 && /* @__PURE__ */ jsx(Button, {
1732
+ onClick: () => update({
1733
+ ...quiz,
1734
+ questions: [...quiz.questions, defaultQuestion()]
1735
+ }),
1736
+ children: "Add question"
1737
+ })
1738
+ ]
1739
+ });
1740
+ }
1741
+ function localDate(value) {
1742
+ if (!value) return "";
1743
+ const date = new Date(value);
1744
+ const offset = date.getTimezoneOffset() * 6e4;
1745
+ return new Date(date.getTime() - offset).toISOString().slice(0, 16);
1746
+ }
1747
+ function isoDate(value) {
1748
+ return value ? new Date(value).toISOString() : null;
1749
+ }
1750
+ function download(name, body, type) {
1751
+ const url = URL.createObjectURL(new Blob([body], { type }));
1752
+ const anchor = document.createElement("a");
1753
+ anchor.href = url;
1754
+ anchor.download = name;
1755
+ anchor.click();
1756
+ setTimeout(() => URL.revokeObjectURL(url), 1e3);
1757
+ }
1758
+ function EnrollmentAccessEditor({ row, close, saved }) {
1759
+ const [status, setStatus] = useState(row.status === "pending" || row.status === "completed" ? "active" : row.status);
1760
+ const [reason, setReason] = useState(row.decisionReason ?? "");
1761
+ const [startsAt, setStartsAt] = useState(row.accessStartsAt);
1762
+ const [expiresAt, setExpiresAt] = useState(row.accessExpiresAt);
1763
+ const action = useAction();
1764
+ return /* @__PURE__ */ jsxs(Panel, {
1765
+ title: `Manage ${row.learnerName}'s access`,
1766
+ children: [
1767
+ /* @__PURE__ */ jsx("p", { children: row.courseTitle }),
1768
+ /* @__PURE__ */ jsx(Notice, { error: action.error }),
1769
+ /* @__PURE__ */ jsxs("form", {
1770
+ onSubmit: (event) => {
1771
+ event.preventDefault();
1772
+ action.run(async () => {
1773
+ await api("admin/enrollment/save", {
1774
+ courseId: row.courseId,
1775
+ userId: row.userId,
1776
+ status,
1777
+ reason,
1778
+ accessStartsAt: startsAt,
1779
+ accessExpiresAt: expiresAt
1780
+ }, "POST");
1781
+ saved();
1782
+ });
1783
+ },
1784
+ children: [
1785
+ /* @__PURE__ */ jsxs("div", {
1786
+ className: "lms-grid",
1787
+ children: [
1788
+ /* @__PURE__ */ jsx(Field, {
1789
+ label: "Enrollment decision",
1790
+ children: /* @__PURE__ */ jsx("select", {
1791
+ value: status,
1792
+ onChange: (event) => setStatus(event.target.value),
1793
+ children: [
1794
+ "active",
1795
+ "rejected",
1796
+ "suspended",
1797
+ "cancelled"
1798
+ ].map((value) => /* @__PURE__ */ jsx("option", {
1799
+ value,
1800
+ children: value === "active" ? "Approve / restore access" : value
1801
+ }, value))
1802
+ })
1803
+ }),
1804
+ /* @__PURE__ */ jsx(Field, {
1805
+ label: "Access starts",
1806
+ children: /* @__PURE__ */ jsx("input", {
1807
+ type: "datetime-local",
1808
+ value: localDate(startsAt),
1809
+ onChange: (event) => setStartsAt(isoDate(event.target.value))
1810
+ })
1811
+ }),
1812
+ /* @__PURE__ */ jsx(Field, {
1813
+ label: "Access expires",
1814
+ children: /* @__PURE__ */ jsx("input", {
1815
+ type: "datetime-local",
1816
+ value: localDate(expiresAt),
1817
+ onChange: (event) => setExpiresAt(isoDate(event.target.value))
1818
+ })
1819
+ }),
1820
+ /* @__PURE__ */ jsx(Field, {
1821
+ label: "Decision note",
1822
+ children: /* @__PURE__ */ jsx("textarea", {
1823
+ maxLength: 1e3,
1824
+ value: reason,
1825
+ onChange: (event) => setReason(event.target.value)
1826
+ })
1827
+ })
1828
+ ]
1829
+ }),
1830
+ /* @__PURE__ */ jsx("p", { children: "These access dates apply to this learner's course enrollment. Leave blank for no additional date restriction." }),
1831
+ /* @__PURE__ */ jsxs("div", {
1832
+ className: "lms-actions",
1833
+ children: [/* @__PURE__ */ jsx(Button, {
1834
+ type: "submit",
1835
+ busy: action.busy,
1836
+ children: "Save access"
1837
+ }), /* @__PURE__ */ jsx(Button, {
1838
+ onClick: close,
1839
+ children: "Close"
1840
+ })]
1841
+ })
1842
+ ]
1843
+ })
1844
+ ]
1845
+ });
1846
+ }
1847
+ function LearnersPage() {
1848
+ const [courseId, setCourseId] = useState("");
1849
+ const [status, setStatus] = useState("");
1850
+ const [cursor, setCursor] = useState();
1851
+ const [email, setEmail] = useState("");
1852
+ const [managing, setManaging] = useState();
1853
+ const list = useLoad("admin/enrollments", {
1854
+ courseId,
1855
+ status,
1856
+ cursor
1857
+ });
1858
+ const action = useAction();
1859
+ const update = (row, next) => void action.run(async () => {
1860
+ await api("admin/enrollment/save", {
1861
+ courseId: row.courseId,
1862
+ userId: row.userId,
1863
+ status: next
1864
+ }, "POST");
1865
+ list.reload();
1866
+ }, "Enrollment updated.");
1867
+ const add = (event) => {
1868
+ event.preventDefault();
1869
+ action.run(async () => {
1870
+ if (!courseId) throw new Error("Choose a course first.");
1871
+ await api("admin/enrollment/save", {
1872
+ courseId,
1873
+ userId: (await api("admin/learner/find", {
1874
+ courseId,
1875
+ email
1876
+ }, "POST")).id,
1877
+ status: "active"
1878
+ }, "POST");
1879
+ setEmail("");
1880
+ list.reload();
1881
+ }, "Learner enrolled.");
1882
+ };
1883
+ return /* @__PURE__ */ jsxs(PageFrame, {
1884
+ title: "Enrollments",
1885
+ description: "Approve requests, enroll learners, and manage access.",
1886
+ children: [
1887
+ /* @__PURE__ */ jsx(Notice, {
1888
+ error: list.error || action.error,
1889
+ message: action.message
1890
+ }),
1891
+ managing && /* @__PURE__ */ jsx(EnrollmentAccessEditor, {
1892
+ row: managing,
1893
+ close: () => setManaging(void 0),
1894
+ saved: () => {
1895
+ setManaging(void 0);
1896
+ list.reload();
1897
+ }
1898
+ }, managing.id),
1899
+ /* @__PURE__ */ jsxs("div", {
1900
+ className: "lms-grid",
1901
+ children: [/* @__PURE__ */ jsx(CoursePicker, {
1902
+ value: courseId,
1903
+ allowAll: true,
1904
+ onChange: (id) => {
1905
+ setCourseId(id);
1906
+ setCursor(void 0);
1907
+ }
1908
+ }), /* @__PURE__ */ jsx(Field, {
1909
+ label: "Status",
1910
+ children: /* @__PURE__ */ jsxs("select", {
1911
+ value: status,
1912
+ onChange: (event) => {
1913
+ setStatus(event.target.value);
1914
+ setCursor(void 0);
1915
+ },
1916
+ children: [/* @__PURE__ */ jsx("option", {
1917
+ value: "",
1918
+ children: "All statuses"
1919
+ }), [
1920
+ "pending",
1921
+ "active",
1922
+ "completed",
1923
+ "rejected",
1924
+ "suspended",
1925
+ "cancelled"
1926
+ ].map((value) => /* @__PURE__ */ jsx("option", { children: value }, value))]
1927
+ })
1928
+ })]
1929
+ }),
1930
+ courseId && /* @__PURE__ */ jsx(Panel, {
1931
+ title: "Enroll an existing account",
1932
+ children: /* @__PURE__ */ jsxs("form", {
1933
+ onSubmit: add,
1934
+ className: "lms-toolbar",
1935
+ children: [/* @__PURE__ */ jsx(Field, {
1936
+ label: "Learner email",
1937
+ children: /* @__PURE__ */ jsx("input", {
1938
+ type: "email",
1939
+ required: true,
1940
+ value: email,
1941
+ onChange: (event) => setEmail(event.target.value)
1942
+ })
1943
+ }), /* @__PURE__ */ jsx(Button, {
1944
+ type: "submit",
1945
+ busy: action.busy,
1946
+ children: "Enroll learner"
1947
+ })]
1948
+ })
1949
+ }),
1950
+ /* @__PURE__ */ jsxs(Panel, { children: [/* @__PURE__ */ jsx("div", {
1951
+ className: "lms-table-wrap",
1952
+ children: /* @__PURE__ */ jsxs("table", {
1953
+ className: "lms-table",
1954
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
1955
+ /* @__PURE__ */ jsx("th", { children: "Learner" }),
1956
+ /* @__PURE__ */ jsx("th", { children: "Course" }),
1957
+ /* @__PURE__ */ jsx("th", { children: "Progress" }),
1958
+ /* @__PURE__ */ jsx("th", { children: "Status" }),
1959
+ /* @__PURE__ */ jsx("th", { children: "Actions" })
1960
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: list.data?.items.map((row) => /* @__PURE__ */ jsxs("tr", { children: [
1961
+ /* @__PURE__ */ jsxs("td", { children: [row.learnerName, /* @__PURE__ */ jsx("small", { children: row.learnerEmail })] }),
1962
+ /* @__PURE__ */ jsx("td", { children: row.courseTitle }),
1963
+ /* @__PURE__ */ jsxs("td", { children: [
1964
+ row.progressSummary?.percent ?? 0,
1965
+ "%",
1966
+ /* @__PURE__ */ jsx("small", { children: row.completedAt ? new Date(row.completedAt).toLocaleDateString() : "In progress" })
1967
+ ] }),
1968
+ /* @__PURE__ */ jsxs("td", { children: [
1969
+ row.status,
1970
+ /* @__PURE__ */ jsxs("small", { children: ["Access: ", row.effectiveAccess] }),
1971
+ /* @__PURE__ */ jsxs("small", { children: ["Source: ", row.accessSource] })
1972
+ ] }),
1973
+ /* @__PURE__ */ jsxs("td", { children: [/* @__PURE__ */ jsxs("div", {
1974
+ className: "lms-actions",
1975
+ children: [
1976
+ /* @__PURE__ */ jsx(Button, {
1977
+ onClick: () => setManaging(row),
1978
+ children: "Manage access"
1979
+ }),
1980
+ row.status === "pending" && /* @__PURE__ */ jsx(Button, {
1981
+ danger: true,
1982
+ busy: action.busy,
1983
+ onClick: () => update(row, "rejected"),
1984
+ children: "Reject"
1985
+ }),
1986
+ [
1987
+ "pending",
1988
+ "rejected",
1989
+ "suspended",
1990
+ "cancelled"
1991
+ ].includes(row.status) && /* @__PURE__ */ jsx(Button, {
1992
+ busy: action.busy,
1993
+ onClick: () => update(row, "active"),
1994
+ children: row.status === "pending" ? "Approve" : "Restore"
1995
+ }),
1996
+ ["active", "completed"].includes(row.status) && /* @__PURE__ */ jsx(Button, {
1997
+ busy: action.busy,
1998
+ onClick: () => update(row, "suspended"),
1999
+ children: "Suspend"
2000
+ }),
2001
+ row.status !== "cancelled" && /* @__PURE__ */ jsx(Button, {
2002
+ danger: true,
2003
+ busy: action.busy,
2004
+ onClick: () => {
2005
+ if (window.confirm(`Cancel ${row.learnerName}'s access to this course?`)) update(row, "cancelled");
2006
+ },
2007
+ children: "Cancel"
2008
+ }),
2009
+ row.certificate && !row.certificate.revokedAt && /* @__PURE__ */ jsx(Button, {
2010
+ danger: true,
2011
+ busy: action.busy,
2012
+ onClick: () => {
2013
+ const reason = window.prompt("Reason for revoking this certificate:");
2014
+ if (reason?.trim()) action.run(async () => {
2015
+ await api("admin/certificate/revoke", {
2016
+ courseId: row.courseId,
2017
+ userId: row.userId,
2018
+ reason
2019
+ }, "POST");
2020
+ list.reload();
2021
+ }, "Certificate revoked.");
2022
+ },
2023
+ children: "Revoke certificate"
2024
+ })
2025
+ ]
2026
+ }), /* @__PURE__ */ jsxs("details", { children: [/* @__PURE__ */ jsx("summary", { children: "Learning record" }), Object.entries(row.progress).map(([id, progress]) => /* @__PURE__ */ jsxs("p", { children: [
2027
+ row.lessonTitles[id] ?? "Lesson",
2028
+ " ·",
2029
+ " ",
2030
+ progress.completedAt ? "Complete" : "Started",
2031
+ " ·",
2032
+ " ",
2033
+ progress.quizAttempts.length,
2034
+ " quiz attempts ·",
2035
+ " ",
2036
+ progress.assignments.length,
2037
+ " submissions"
2038
+ ] }, id))] })] })
2039
+ ] }, row.id)) })]
2040
+ })
2041
+ }), list.data && !list.data.items.length && /* @__PURE__ */ jsx("p", { children: "No enrollments match this filter." })] }),
2042
+ /* @__PURE__ */ jsx(Pager, {
2043
+ page: list.data,
2044
+ next: setCursor,
2045
+ reset: () => setCursor(void 0)
2046
+ })
2047
+ ]
2048
+ });
2049
+ }
2050
+ function AssignmentsPage() {
2051
+ const [courseId, setCourseId] = useState("");
2052
+ const [cursor, setCursor] = useState();
2053
+ const list = useLoad("admin/assignments", {
2054
+ courseId,
2055
+ cursor
2056
+ }, !!courseId);
2057
+ const [selected, setSelected] = useState();
2058
+ const [points, setPoints] = useState(0);
2059
+ const [feedback, setFeedback] = useState("");
2060
+ const action = useAction();
2061
+ const grade = (event) => {
2062
+ event.preventDefault();
2063
+ if (!selected) return;
2064
+ action.run(async () => {
2065
+ await api("admin/assignment/grade", {
2066
+ courseId: selected.courseId,
2067
+ lessonId: selected.lessonId,
2068
+ userId: selected.userId,
2069
+ submissionId: selected.id,
2070
+ points,
2071
+ feedback
2072
+ }, "POST");
2073
+ setSelected(void 0);
2074
+ list.reload();
2075
+ }, "Grade saved and learner notified.");
2076
+ };
2077
+ return /* @__PURE__ */ jsxs(PageFrame, {
2078
+ title: "Assignments",
2079
+ description: "Read submissions and provide clear, useful feedback.",
2080
+ children: [
2081
+ /* @__PURE__ */ jsx(Notice, {
2082
+ error: list.error || action.error,
2083
+ message: action.message
2084
+ }),
2085
+ /* @__PURE__ */ jsx(CoursePicker, {
2086
+ value: courseId,
2087
+ onChange: (id) => {
2088
+ setCourseId(id);
2089
+ setCursor(void 0);
2090
+ setSelected(void 0);
2091
+ }
2092
+ }),
2093
+ !courseId && /* @__PURE__ */ jsx(Panel, { children: /* @__PURE__ */ jsx("p", { children: "Choose a course to view its submissions." }) }),
2094
+ selected && /* @__PURE__ */ jsxs(Panel, {
2095
+ title: `Grade ${selected.learnerName}'s submission`,
2096
+ children: [
2097
+ /* @__PURE__ */ jsx("p", {
2098
+ className: "lms-body",
2099
+ children: selected.body
2100
+ }),
2101
+ selected.attachmentUrl && /* @__PURE__ */ jsx("a", {
2102
+ href: selected.attachmentUrl,
2103
+ target: "_blank",
2104
+ rel: "noopener noreferrer",
2105
+ children: "Open attachment ↗"
2106
+ }),
2107
+ /* @__PURE__ */ jsxs("form", {
2108
+ onSubmit: grade,
2109
+ children: [
2110
+ /* @__PURE__ */ jsx(Field, {
2111
+ label: `Points out of ${selected.maxPoints}`,
2112
+ help: `Passing grade: ${selected.passingPoints}`,
2113
+ children: /* @__PURE__ */ jsx("input", {
2114
+ type: "number",
2115
+ required: true,
2116
+ min: 0,
2117
+ max: selected.maxPoints,
2118
+ value: points,
2119
+ onChange: (event) => setPoints(Number(event.target.value))
2120
+ })
2121
+ }),
2122
+ /* @__PURE__ */ jsx(Field, {
2123
+ label: "Feedback",
2124
+ children: /* @__PURE__ */ jsx("textarea", {
2125
+ rows: 6,
2126
+ value: feedback,
2127
+ onChange: (event) => setFeedback(event.target.value)
2128
+ })
2129
+ }),
2130
+ /* @__PURE__ */ jsxs("div", {
2131
+ className: "lms-actions",
2132
+ children: [/* @__PURE__ */ jsx(Button, {
2133
+ type: "submit",
2134
+ busy: action.busy,
2135
+ children: "Save grade"
2136
+ }), /* @__PURE__ */ jsx(Button, {
2137
+ onClick: () => setSelected(void 0),
2138
+ children: "Close"
2139
+ })]
2140
+ })
2141
+ ]
2142
+ })
2143
+ ]
2144
+ }),
2145
+ list.data?.items.map((item) => /* @__PURE__ */ jsxs(Panel, { children: [
2146
+ /* @__PURE__ */ jsxs("div", {
2147
+ className: "lms-toolbar",
2148
+ children: [
2149
+ /* @__PURE__ */ jsx("h2", { children: item.learnerName }),
2150
+ /* @__PURE__ */ jsx("span", {
2151
+ className: "lms-badge",
2152
+ children: item.status
2153
+ }),
2154
+ /* @__PURE__ */ jsx("small", { children: new Date(item.submittedAt).toLocaleString() })
2155
+ ]
2156
+ }),
2157
+ /* @__PURE__ */ jsx("p", {
2158
+ className: "lms-body",
2159
+ children: item.body
2160
+ }),
2161
+ item.attachmentUrl && /* @__PURE__ */ jsx("p", { children: /* @__PURE__ */ jsx("a", {
2162
+ href: item.attachmentUrl,
2163
+ target: "_blank",
2164
+ rel: "noopener noreferrer",
2165
+ children: "Open attachment ↗"
2166
+ }) }),
2167
+ item.status === "graded" ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("p", { children: [
2168
+ item.points,
2169
+ "/",
2170
+ item.maxPoints,
2171
+ " points"
2172
+ ] }), /* @__PURE__ */ jsx("p", {
2173
+ className: "lms-body",
2174
+ children: item.feedback
2175
+ })] }) : /* @__PURE__ */ jsx(Button, {
2176
+ onClick: () => {
2177
+ setSelected(item);
2178
+ setPoints(0);
2179
+ setFeedback("");
2180
+ },
2181
+ children: "Review & grade"
2182
+ })
2183
+ ] }, item.id)),
2184
+ list.data && !list.data.items.length && /* @__PURE__ */ jsx(Panel, { children: /* @__PURE__ */ jsx("p", { children: "No submissions on this page." }) }),
2185
+ /* @__PURE__ */ jsx(Pager, {
2186
+ page: list.data,
2187
+ next: setCursor,
2188
+ reset: () => setCursor(void 0)
2189
+ })
2190
+ ]
2191
+ });
2192
+ }
2193
+ function CommunityPage() {
2194
+ const [courseId, setCourseId] = useState("");
2195
+ const [kind, setKind] = useState("discussions");
2196
+ const [status, setStatus] = useState("pending");
2197
+ const [cursor, setCursor] = useState();
2198
+ const list = useLoad(`admin/${kind}`, {
2199
+ courseId,
2200
+ status,
2201
+ cursor
2202
+ });
2203
+ const action = useAction();
2204
+ const moderate = (item, next) => void action.run(async () => {
2205
+ await api(`admin/${kind === "reviews" ? "review" : "discussion"}/moderate`, {
2206
+ id: item.id,
2207
+ expectedRevision: item.revision,
2208
+ status: next
2209
+ }, "POST");
2210
+ list.reload();
2211
+ }, "Moderation saved.");
2212
+ return /* @__PURE__ */ jsxs(PageFrame, {
2213
+ title: "Learning community",
2214
+ description: "Moderate lesson discussions and course reviews.",
2215
+ children: [
2216
+ /* @__PURE__ */ jsx(Notice, {
2217
+ error: list.error || action.error,
2218
+ message: action.message
2219
+ }),
2220
+ /* @__PURE__ */ jsxs("div", {
2221
+ className: "lms-grid",
2222
+ children: [
2223
+ /* @__PURE__ */ jsx(CoursePicker, {
2224
+ value: courseId,
2225
+ allowAll: true,
2226
+ onChange: (id) => {
2227
+ setCourseId(id);
2228
+ setCursor(void 0);
2229
+ }
2230
+ }),
2231
+ /* @__PURE__ */ jsx(Field, {
2232
+ label: "Content",
2233
+ children: /* @__PURE__ */ jsxs("select", {
2234
+ value: kind,
2235
+ onChange: (event) => {
2236
+ setKind(event.target.value);
2237
+ setCursor(void 0);
2238
+ },
2239
+ children: [/* @__PURE__ */ jsx("option", {
2240
+ value: "discussions",
2241
+ children: "Discussions"
2242
+ }), /* @__PURE__ */ jsx("option", {
2243
+ value: "reviews",
2244
+ children: "Reviews"
2245
+ })]
2246
+ })
2247
+ }),
2248
+ /* @__PURE__ */ jsx(Field, {
2249
+ label: "Status",
2250
+ children: /* @__PURE__ */ jsx("select", {
2251
+ value: status,
2252
+ onChange: (event) => {
2253
+ setStatus(event.target.value);
2254
+ setCursor(void 0);
2255
+ },
2256
+ children: [
2257
+ "pending",
2258
+ "published",
2259
+ "hidden"
2260
+ ].map((value) => /* @__PURE__ */ jsx("option", { children: value }, value))
2261
+ })
2262
+ })
2263
+ ]
2264
+ }),
2265
+ list.data?.items.map((item) => /* @__PURE__ */ jsxs(Panel, { children: [
2266
+ /* @__PURE__ */ jsxs("h2", { children: [item.authorName, "rating" in item ? ` · ${item.rating}/5` : ""] }),
2267
+ /* @__PURE__ */ jsx("p", {
2268
+ className: "lms-body",
2269
+ children: item.body
2270
+ }),
2271
+ /* @__PURE__ */ jsxs("p", {
2272
+ className: "lms-muted",
2273
+ children: [
2274
+ new Date(item.createdAt).toLocaleString(),
2275
+ " · ",
2276
+ item.status
2277
+ ]
2278
+ }),
2279
+ /* @__PURE__ */ jsxs("div", {
2280
+ className: "lms-actions",
2281
+ children: [item.status !== "published" && /* @__PURE__ */ jsx(Button, {
2282
+ busy: action.busy,
2283
+ onClick: () => moderate(item, "published"),
2284
+ children: "Publish"
2285
+ }), item.status !== "hidden" && /* @__PURE__ */ jsx(Button, {
2286
+ danger: true,
2287
+ busy: action.busy,
2288
+ onClick: () => moderate(item, "hidden"),
2289
+ children: "Hide"
2290
+ })]
2291
+ })
2292
+ ] }, item.id)),
2293
+ list.data && !list.data.items.length && /* @__PURE__ */ jsx(Panel, { children: /* @__PURE__ */ jsx("p", { children: "No community posts match this filter." }) }),
2294
+ /* @__PURE__ */ jsx(Pager, {
2295
+ page: list.data,
2296
+ next: setCursor,
2297
+ reset: () => setCursor(void 0)
2298
+ })
2299
+ ]
2300
+ });
2301
+ }
2302
+ function CohortMembershipQueue() {
2303
+ const cohorts = useChoices("admin/cohorts");
2304
+ const [cohortId, setCohortId] = useState("");
2305
+ const [status, setStatus] = useState("pending");
2306
+ const [cursor, setCursor] = useState();
2307
+ const [reason, setReason] = useState("");
2308
+ const list = useLoad("admin/cohort/memberships", {
2309
+ cohortId,
2310
+ status,
2311
+ cursor
2312
+ });
2313
+ const action = useAction();
2314
+ const decide = (membership, next) => void action.run(async () => {
2315
+ const result = await api("admin/cohort/decision", {
2316
+ cohortId: membership.cohortId,
2317
+ userId: membership.userId,
2318
+ status: next,
2319
+ reason
2320
+ }, "POST");
2321
+ list.reload();
2322
+ const failures = result.results.filter((item) => !item.success);
2323
+ if (failures.length) throw new Error(`Membership saved; ${failures.length} course access updates need attention. ${failures.map((item) => item.error).join(" ")} Review the learner's course enrollment and retry access.`);
2324
+ }, "Cohort membership updated.");
2325
+ return /* @__PURE__ */ jsxs(Panel, {
2326
+ title: "Cohort membership requests",
2327
+ children: [
2328
+ /* @__PURE__ */ jsx(Notice, {
2329
+ error: list.error || action.error,
2330
+ message: action.message
2331
+ }),
2332
+ /* @__PURE__ */ jsxs("div", {
2333
+ className: "lms-grid",
2334
+ children: [
2335
+ /* @__PURE__ */ jsxs(Field, {
2336
+ label: "Cohort",
2337
+ children: [/* @__PURE__ */ jsxs("select", {
2338
+ value: cohortId,
2339
+ onChange: (event) => {
2340
+ setCohortId(event.target.value);
2341
+ setCursor(void 0);
2342
+ },
2343
+ children: [/* @__PURE__ */ jsx("option", {
2344
+ value: "",
2345
+ children: "All cohorts"
2346
+ }), cohorts.items.map((cohort) => /* @__PURE__ */ jsx("option", {
2347
+ value: cohort.id,
2348
+ children: cohort.title
2349
+ }, cohort.id))]
2350
+ }), cohorts.data?.hasMore && /* @__PURE__ */ jsx(Button, {
2351
+ onClick: cohorts.more,
2352
+ children: "Load more cohorts"
2353
+ })]
2354
+ }),
2355
+ /* @__PURE__ */ jsx(Field, {
2356
+ label: "Membership status",
2357
+ children: /* @__PURE__ */ jsxs("select", {
2358
+ value: status,
2359
+ onChange: (event) => {
2360
+ setStatus(event.target.value);
2361
+ setCursor(void 0);
2362
+ },
2363
+ children: [/* @__PURE__ */ jsx("option", {
2364
+ value: "",
2365
+ children: "All statuses"
2366
+ }), [
2367
+ "pending",
2368
+ "active",
2369
+ "rejected",
2370
+ "suspended",
2371
+ "cancelled"
2372
+ ].map((value) => /* @__PURE__ */ jsx("option", { children: value }, value))]
2373
+ })
2374
+ }),
2375
+ /* @__PURE__ */ jsx(Field, {
2376
+ label: "Decision note (optional)",
2377
+ children: /* @__PURE__ */ jsx("input", {
2378
+ value: reason,
2379
+ maxLength: 1e3,
2380
+ onChange: (event) => setReason(event.target.value)
2381
+ })
2382
+ })
2383
+ ]
2384
+ }),
2385
+ /* @__PURE__ */ jsx("div", {
2386
+ className: "lms-table-wrap",
2387
+ children: /* @__PURE__ */ jsxs("table", {
2388
+ className: "lms-table",
2389
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
2390
+ /* @__PURE__ */ jsx("th", { children: "Learner" }),
2391
+ /* @__PURE__ */ jsx("th", { children: "Cohort" }),
2392
+ /* @__PURE__ */ jsx("th", { children: "Membership" }),
2393
+ /* @__PURE__ */ jsx("th", { children: "Actions" })
2394
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: list.data?.items.map((membership) => /* @__PURE__ */ jsxs("tr", { children: [
2395
+ /* @__PURE__ */ jsxs("td", { children: [membership.learnerName, /* @__PURE__ */ jsx("small", { children: membership.learnerEmail })] }),
2396
+ /* @__PURE__ */ jsx("td", { children: cohorts.items.find((cohort) => cohort.id === membership.cohortId)?.title ?? "Cohort" }),
2397
+ /* @__PURE__ */ jsxs("td", { children: [membership.status, membership.reason && /* @__PURE__ */ jsx("small", { children: membership.reason })] }),
2398
+ /* @__PURE__ */ jsx("td", { children: /* @__PURE__ */ jsxs("div", {
2399
+ className: "lms-actions",
2400
+ children: [
2401
+ /* @__PURE__ */ jsx(Button, {
2402
+ busy: action.busy,
2403
+ onClick: () => decide(membership, "active"),
2404
+ children: membership.status === "pending" ? "Approve" : membership.status === "active" ? "Retry course access" : "Restore"
2405
+ }),
2406
+ membership.status === "pending" && /* @__PURE__ */ jsx(Button, {
2407
+ danger: true,
2408
+ busy: action.busy,
2409
+ onClick: () => decide(membership, "rejected"),
2410
+ children: "Reject"
2411
+ }),
2412
+ membership.status === "active" && /* @__PURE__ */ jsx(Button, {
2413
+ busy: action.busy,
2414
+ onClick: () => decide(membership, "suspended"),
2415
+ children: "Suspend membership"
2416
+ }),
2417
+ membership.status !== "cancelled" && /* @__PURE__ */ jsx(Button, {
2418
+ danger: true,
2419
+ busy: action.busy,
2420
+ onClick: () => decide(membership, "cancelled"),
2421
+ children: "Cancel membership"
2422
+ })
2423
+ ]
2424
+ }) })
2425
+ ] }, membership.id)) })]
2426
+ })
2427
+ }),
2428
+ list.data && !list.data.items.length && /* @__PURE__ */ jsx("p", { children: "No membership requests match this filter." }),
2429
+ /* @__PURE__ */ jsx(Pager, {
2430
+ page: list.data,
2431
+ next: setCursor,
2432
+ reset: () => setCursor(void 0)
2433
+ })
2434
+ ]
2435
+ });
2436
+ }
2437
+ function PathsPage() {
2438
+ const [cursor, setCursor] = useState();
2439
+ const list = useLoad("admin/paths", { cursor });
2440
+ const courses = useChoices("admin/courses");
2441
+ const [edit, setEdit] = useState();
2442
+ const action = useAction();
2443
+ return /* @__PURE__ */ jsxs(PageFrame, {
2444
+ title: "Learning paths",
2445
+ description: "Build ordered programs. Learners retain the course list they joined; individual course enrollment still controls access.",
2446
+ children: [
2447
+ /* @__PURE__ */ jsx(Notice, {
2448
+ error: list.error || courses.error || action.error,
2449
+ message: action.message
2450
+ }),
2451
+ !edit && /* @__PURE__ */ jsx(Button, {
2452
+ onClick: () => setEdit({
2453
+ revision: null,
2454
+ title: "New learning path",
2455
+ description: "",
2456
+ status: "draft",
2457
+ courseIds: []
2458
+ }),
2459
+ children: "Create learning path"
2460
+ }),
2461
+ edit && /* @__PURE__ */ jsx(Panel, {
2462
+ title: edit.id ? "Edit path" : "Create path",
2463
+ children: /* @__PURE__ */ jsxs("form", {
2464
+ onSubmit: (event) => {
2465
+ event.preventDefault();
2466
+ action.run(async () => {
2467
+ const { id, revision, ...data } = edit;
2468
+ await api("admin/path/save", {
2469
+ id,
2470
+ expectedRevision: revision,
2471
+ data
2472
+ }, "POST");
2473
+ setEdit(void 0);
2474
+ list.reload();
2475
+ }, "Path saved. Existing learners retain their joined course lists.");
2476
+ },
2477
+ children: [
2478
+ /* @__PURE__ */ jsx(Field, {
2479
+ label: "Title",
2480
+ children: /* @__PURE__ */ jsx("input", {
2481
+ required: true,
2482
+ maxLength: 200,
2483
+ value: edit.title,
2484
+ onChange: (event) => setEdit({
2485
+ ...edit,
2486
+ title: event.target.value
2487
+ })
2488
+ })
2489
+ }),
2490
+ /* @__PURE__ */ jsx(Field, {
2491
+ label: "Description",
2492
+ children: /* @__PURE__ */ jsx("textarea", {
2493
+ maxLength: 5e3,
2494
+ rows: 4,
2495
+ value: edit.description,
2496
+ onChange: (event) => setEdit({
2497
+ ...edit,
2498
+ description: event.target.value
2499
+ })
2500
+ })
2501
+ }),
2502
+ /* @__PURE__ */ jsx(Field, {
2503
+ label: "Visibility",
2504
+ children: /* @__PURE__ */ jsxs("select", {
2505
+ value: edit.status,
2506
+ onChange: (event) => setEdit({
2507
+ ...edit,
2508
+ status: event.target.value
2509
+ }),
2510
+ children: [
2511
+ /* @__PURE__ */ jsx("option", {
2512
+ value: "draft",
2513
+ children: "Draft"
2514
+ }),
2515
+ /* @__PURE__ */ jsx("option", {
2516
+ value: "published",
2517
+ children: "Published"
2518
+ }),
2519
+ /* @__PURE__ */ jsx("option", {
2520
+ value: "archived",
2521
+ children: "Archived"
2522
+ })
2523
+ ]
2524
+ })
2525
+ }),
2526
+ /* @__PURE__ */ jsx(ChoicesField, {
2527
+ label: "Courses",
2528
+ choices: courses.items.map((course) => ({
2529
+ id: course.id,
2530
+ label: `${course.title} (${course.status})`
2531
+ })),
2532
+ value: edit.courseIds,
2533
+ onChange: (courseIds) => setEdit({
2534
+ ...edit,
2535
+ courseIds
2536
+ })
2537
+ }),
2538
+ courses.data?.hasMore && /* @__PURE__ */ jsx(Button, {
2539
+ onClick: courses.more,
2540
+ children: "Load more courses"
2541
+ }),
2542
+ /* @__PURE__ */ jsx("ol", { children: edit.courseIds.map((id, index) => /* @__PURE__ */ jsxs("li", { children: [courses.items.find((course) => course.id === id)?.title ?? id, /* @__PURE__ */ jsx(Button, {
2543
+ disabled: index === 0,
2544
+ "aria-label": "Move course earlier",
2545
+ onClick: () => {
2546
+ const ids = [...edit.courseIds];
2547
+ [ids[index], ids[index - 1]] = [ids[index - 1], ids[index]];
2548
+ setEdit({
2549
+ ...edit,
2550
+ courseIds: ids
2551
+ });
2552
+ },
2553
+ children: "Move up"
2554
+ })] }, id)) }),
2555
+ /* @__PURE__ */ jsx(Button, {
2556
+ type: "submit",
2557
+ busy: action.busy,
2558
+ children: "Save path"
2559
+ }),
2560
+ /* @__PURE__ */ jsx(Button, {
2561
+ disabled: action.busy,
2562
+ onClick: () => setEdit(void 0),
2563
+ children: "Cancel"
2564
+ })
2565
+ ]
2566
+ })
2567
+ }),
2568
+ list.data?.items.map((path) => /* @__PURE__ */ jsxs(Panel, {
2569
+ title: path.title,
2570
+ children: [/* @__PURE__ */ jsxs("p", { children: [
2571
+ path.status,
2572
+ " · ",
2573
+ path.courseIds.length,
2574
+ " courses"
2575
+ ] }), /* @__PURE__ */ jsx(Button, {
2576
+ disabled: Boolean(edit),
2577
+ onClick: () => setEdit({
2578
+ id: path.id,
2579
+ revision: path.revision,
2580
+ title: path.title,
2581
+ description: path.description,
2582
+ courseIds: path.courseIds,
2583
+ status: path.status
2584
+ }),
2585
+ children: "Edit path"
2586
+ })]
2587
+ }, path.id)),
2588
+ /* @__PURE__ */ jsx(Pager, {
2589
+ page: list.data,
2590
+ next: setCursor,
2591
+ reset: () => setCursor(void 0)
2592
+ })
2593
+ ]
2594
+ });
2595
+ }
2596
+ function CohortsPage() {
2597
+ const [cursor, setCursor] = useState();
2598
+ const list = useLoad("admin/cohorts", { cursor });
2599
+ const courses = useChoices("admin/courses");
2600
+ const users = useChoices("admin/users");
2601
+ const academy = useLoad("academy");
2602
+ const [edit, setEdit] = useState();
2603
+ const action = useAction();
2604
+ const save = (event) => {
2605
+ event.preventDefault();
2606
+ if (!edit) return;
2607
+ action.run(async () => {
2608
+ const { id, revision, ...data } = edit;
2609
+ await api("admin/cohort/save", {
2610
+ id,
2611
+ expectedRevision: revision,
2612
+ data
2613
+ }, "POST");
2614
+ setEdit(void 0);
2615
+ list.reload();
2616
+ }, "Cohort saved.");
2617
+ };
2618
+ const enroll = (cohort) => void action.run(async () => {
2619
+ let offset = 0;
2620
+ let hasMore = true;
2621
+ const errors = [];
2622
+ while (hasMore) {
2623
+ const result = await api("admin/cohort/enroll", {
2624
+ id: cohort.id,
2625
+ expectedRevision: cohort.revision,
2626
+ offset
2627
+ }, "POST");
2628
+ offset = result.nextOffset;
2629
+ hasMore = result.hasMore;
2630
+ errors.push(...result.results.filter((row) => !row.success).map((row) => row.error ?? "Enrollment failed."));
2631
+ }
2632
+ if (errors.length) throw new Error(`${errors.length} enrollments need attention: ${errors.slice(0, 3).join(" ")}`);
2633
+ }, "Cohort learners enrolled.");
2634
+ return /* @__PURE__ */ jsxs(PageFrame, {
2635
+ title: "Cohorts",
2636
+ description: "Approve cohort membership and manage shared learning schedules.",
2637
+ children: [
2638
+ /* @__PURE__ */ jsx(Notice, {
2639
+ error: list.error || action.error || courses.error || users.error,
2640
+ message: action.message
2641
+ }),
2642
+ /* @__PURE__ */ jsx(Button, {
2643
+ onClick: () => setEdit({
2644
+ revision: null,
2645
+ title: "New cohort",
2646
+ courseIds: [],
2647
+ memberIds: [],
2648
+ startsAt: null,
2649
+ endsAt: null
2650
+ }),
2651
+ children: "Create cohort"
2652
+ }),
2653
+ edit && /* @__PURE__ */ jsx(Panel, {
2654
+ title: "Cohort details",
2655
+ children: /* @__PURE__ */ jsxs("form", {
2656
+ onSubmit: save,
2657
+ children: [
2658
+ /* @__PURE__ */ jsx(Field, {
2659
+ label: "Cohort name",
2660
+ children: /* @__PURE__ */ jsx("input", {
2661
+ required: true,
2662
+ value: edit.title,
2663
+ onChange: (event) => setEdit({
2664
+ ...edit,
2665
+ title: event.target.value
2666
+ })
2667
+ })
2668
+ }),
2669
+ /* @__PURE__ */ jsxs("div", {
2670
+ className: "lms-grid",
2671
+ children: [
2672
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(ChoicesField, {
2673
+ label: "Courses",
2674
+ value: edit.courseIds,
2675
+ choices: courses.items.map((course) => ({
2676
+ id: course.id,
2677
+ label: course.title
2678
+ })),
2679
+ onChange: (courseIds) => setEdit({
2680
+ ...edit,
2681
+ courseIds
2682
+ })
2683
+ }), courses.data?.hasMore && /* @__PURE__ */ jsx(Button, {
2684
+ onClick: courses.more,
2685
+ children: "Load more courses"
2686
+ })] }),
2687
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx(ChoicesField, {
2688
+ label: "Learners for manual enrollment",
2689
+ value: edit.memberIds,
2690
+ choices: users.items.map((account) => ({
2691
+ id: account.id,
2692
+ label: `${account.name || account.email} · ${account.email}`
2693
+ })),
2694
+ onChange: (memberIds) => setEdit({
2695
+ ...edit,
2696
+ memberIds
2697
+ })
2698
+ }), users.data?.hasMore && /* @__PURE__ */ jsx(Button, {
2699
+ onClick: users.more,
2700
+ children: "Load more learners"
2701
+ })] }),
2702
+ /* @__PURE__ */ jsx(Field, {
2703
+ label: "Access starts",
2704
+ children: /* @__PURE__ */ jsx("input", {
2705
+ type: "datetime-local",
2706
+ value: localDate(edit.startsAt),
2707
+ onChange: (event) => setEdit({
2708
+ ...edit,
2709
+ startsAt: isoDate(event.target.value)
2710
+ })
2711
+ })
2712
+ }),
2713
+ /* @__PURE__ */ jsx(Field, {
2714
+ label: "Access ends",
2715
+ children: /* @__PURE__ */ jsx("input", {
2716
+ type: "datetime-local",
2717
+ value: localDate(edit.endsAt),
2718
+ onChange: (event) => setEdit({
2719
+ ...edit,
2720
+ endsAt: isoDate(event.target.value)
2721
+ })
2722
+ })
2723
+ })
2724
+ ]
2725
+ }),
2726
+ /* @__PURE__ */ jsxs("div", {
2727
+ className: "lms-actions",
2728
+ children: [/* @__PURE__ */ jsx(Button, {
2729
+ type: "submit",
2730
+ busy: action.busy,
2731
+ children: "Save cohort"
2732
+ }), /* @__PURE__ */ jsx(Button, {
2733
+ onClick: () => setEdit(void 0),
2734
+ children: "Close"
2735
+ })]
2736
+ })
2737
+ ]
2738
+ })
2739
+ }),
2740
+ list.data?.items.map((cohort) => /* @__PURE__ */ jsxs(Panel, { children: [
2741
+ /* @__PURE__ */ jsx("h2", { children: cohort.title }),
2742
+ /* @__PURE__ */ jsxs("p", { children: [
2743
+ cohort.memberIds.length,
2744
+ " learners · ",
2745
+ cohort.courseIds.length,
2746
+ " courses"
2747
+ ] }),
2748
+ /* @__PURE__ */ jsxs("div", {
2749
+ className: "lms-actions",
2750
+ children: [
2751
+ /* @__PURE__ */ jsx(Button, {
2752
+ onClick: () => setEdit({ ...cohort }),
2753
+ children: "Edit"
2754
+ }),
2755
+ /* @__PURE__ */ jsx("a", {
2756
+ href: `${academy.data?.basePath ?? "/academy"}/cohorts/${cohort.id}`,
2757
+ target: "_blank",
2758
+ rel: "noopener noreferrer",
2759
+ children: "Open enrollment page ↗"
2760
+ }),
2761
+ /* @__PURE__ */ jsx(Button, {
2762
+ busy: action.busy,
2763
+ onClick: () => enroll(cohort),
2764
+ children: "Enroll members"
2765
+ })
2766
+ ]
2767
+ })
2768
+ ] }, cohort.id)),
2769
+ /* @__PURE__ */ jsx(Pager, {
2770
+ page: list.data,
2771
+ next: setCursor,
2772
+ reset: () => setCursor(void 0)
2773
+ }),
2774
+ /* @__PURE__ */ jsx(CohortMembershipQueue, {})
2775
+ ]
2776
+ });
2777
+ }
2778
+ function ReportsPage() {
2779
+ const [courseId, setCourseId] = useState("");
2780
+ const report = useLoad("admin/report", { courseId });
2781
+ const action = useAction();
2782
+ const exportCsv = () => void action.run(async () => {
2783
+ const rows = [];
2784
+ let cursor;
2785
+ do {
2786
+ const page = await api("admin/enrollments", {
2787
+ courseId,
2788
+ cursor,
2789
+ limit: 100
2790
+ });
2791
+ rows.push(...page.items);
2792
+ if (rows.length > 1e4) throw new Error("Choose a course to export fewer than 10,000 enrollments.");
2793
+ cursor = page.hasMore ? page.cursor : void 0;
2794
+ } while (cursor);
2795
+ download("lms-enrollments.csv", "" + [enrollmentCsvHeader, ...enrollmentCsvLines(rows)].join("\r\n"), "text/csv;charset=utf-8");
2796
+ }, "Report exported.");
2797
+ return /* @__PURE__ */ jsxs(PageFrame, {
2798
+ title: "Learning reports",
2799
+ description: "Enrollment and completion counts cover all records in the selected scope.",
2800
+ children: [
2801
+ /* @__PURE__ */ jsx(Notice, {
2802
+ error: report.error || action.error,
2803
+ message: action.message
2804
+ }),
2805
+ /* @__PURE__ */ jsx(CoursePicker, {
2806
+ value: courseId,
2807
+ allowAll: true,
2808
+ onChange: setCourseId
2809
+ }),
2810
+ /* @__PURE__ */ jsx(Metrics, { report: report.data }),
2811
+ /* @__PURE__ */ jsx(Panel, {
2812
+ title: "Enrollment status",
2813
+ children: /* @__PURE__ */ jsx("div", {
2814
+ className: "lms-table-wrap",
2815
+ children: /* @__PURE__ */ jsxs("table", {
2816
+ className: "lms-table",
2817
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("th", { children: "Status" }), /* @__PURE__ */ jsx("th", { children: "Enrollments" })] }) }), /* @__PURE__ */ jsx("tbody", { children: [
2818
+ "pending",
2819
+ "active",
2820
+ "completed",
2821
+ "rejected",
2822
+ "suspended",
2823
+ "cancelled"
2824
+ ].map((status) => /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("td", { children: status }), /* @__PURE__ */ jsx("td", { children: report.data ? report.data[status] : "—" })] }, status)) })]
2825
+ })
2826
+ })
2827
+ }),
2828
+ /* @__PURE__ */ jsx(Button, {
2829
+ busy: action.busy,
2830
+ onClick: exportCsv,
2831
+ children: "Export enrollments as CSV"
2832
+ }),
2833
+ /* @__PURE__ */ jsxs(Panel, {
2834
+ title: "Access sources",
2835
+ children: [/* @__PURE__ */ jsx("div", {
2836
+ className: "lms-table-wrap",
2837
+ children: /* @__PURE__ */ jsxs("table", {
2838
+ className: "lms-table",
2839
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("th", { children: "Initial grant source" }), /* @__PURE__ */ jsx("th", { children: "Enrollments" })] }) }), /* @__PURE__ */ jsx("tbody", { children: [
2840
+ "free",
2841
+ "manual",
2842
+ "cohort",
2843
+ "purchase"
2844
+ ].map((source) => /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("td", { children: source }), /* @__PURE__ */ jsx("td", { children: report.data?.accessSources[source] ?? "—" })] }, source)) })]
2845
+ })
2846
+ }), /* @__PURE__ */ jsx("p", { children: "An enrollment can have several access grants. The source records its first approved acquisition; each learner's current access is shown in the CSV export." })]
2847
+ }),
2848
+ /* @__PURE__ */ jsxs(Panel, {
2849
+ title: "Access decisions",
2850
+ children: [/* @__PURE__ */ jsx("div", {
2851
+ className: "lms-table-wrap",
2852
+ children: /* @__PURE__ */ jsxs("table", {
2853
+ className: "lms-table",
2854
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("th", { children: "Decision" }), /* @__PURE__ */ jsx("th", { children: "Enrollments" })] }) }), /* @__PURE__ */ jsx("tbody", { children: [
2855
+ "pending",
2856
+ "granted",
2857
+ "suspended",
2858
+ "revoked"
2859
+ ].map((decision) => /* @__PURE__ */ jsxs("tr", { children: [/* @__PURE__ */ jsx("td", { children: decision }), /* @__PURE__ */ jsx("td", { children: report.data?.accessDecisions[decision] ?? "—" })] }, decision)) })]
2860
+ })
2861
+ }), /* @__PURE__ */ jsx("p", { children: "Access decisions are separate from date windows and cohort membership. Use the enrollment export to review current access." })]
2862
+ })
2863
+ ]
2864
+ });
2865
+ }
2866
+ function NotificationsPage() {
2867
+ const [cursor, setCursor] = useState();
2868
+ const [status, setStatus] = useState("failed");
2869
+ const list = useLoad("admin/notifications", {
2870
+ cursor,
2871
+ status
2872
+ });
2873
+ const action = useAction();
2874
+ return /* @__PURE__ */ jsxs(PageFrame, {
2875
+ title: "Learning notifications",
2876
+ description: "Review delivery status and retry failed emails.",
2877
+ children: [
2878
+ /* @__PURE__ */ jsx(Notice, {
2879
+ error: list.error || action.error,
2880
+ message: action.message
2881
+ }),
2882
+ /* @__PURE__ */ jsxs("div", {
2883
+ className: "lms-actions",
2884
+ children: [/* @__PURE__ */ jsx(Field, {
2885
+ label: "Email status",
2886
+ children: /* @__PURE__ */ jsxs("select", {
2887
+ value: status,
2888
+ onChange: (event) => {
2889
+ setStatus(event.target.value);
2890
+ setCursor(void 0);
2891
+ },
2892
+ children: [/* @__PURE__ */ jsx("option", {
2893
+ value: "",
2894
+ children: "All notifications"
2895
+ }), [
2896
+ "failed",
2897
+ "pending",
2898
+ "sending",
2899
+ "sent",
2900
+ "disabled"
2901
+ ].map((value) => /* @__PURE__ */ jsx("option", {
2902
+ value,
2903
+ children: value
2904
+ }, value))]
2905
+ })
2906
+ }), /* @__PURE__ */ jsx(Button, {
2907
+ onClick: list.reload,
2908
+ busy: list.loading,
2909
+ children: "Refresh"
2910
+ })]
2911
+ }),
2912
+ /* @__PURE__ */ jsxs(Panel, { children: [
2913
+ /* @__PURE__ */ jsx("p", { children: "Retries run during scheduled maintenance, with up to three attempts per email. Review delivery before retrying: an interrupted confirmation can mean the email was already sent." }),
2914
+ /* @__PURE__ */ jsx("div", {
2915
+ className: "lms-table-wrap",
2916
+ children: /* @__PURE__ */ jsxs("table", {
2917
+ className: "lms-table",
2918
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
2919
+ /* @__PURE__ */ jsx("th", { children: "Created" }),
2920
+ /* @__PURE__ */ jsx("th", { children: "Learner" }),
2921
+ /* @__PURE__ */ jsx("th", { children: "Notification" }),
2922
+ /* @__PURE__ */ jsx("th", { children: "Email" }),
2923
+ /* @__PURE__ */ jsx("th", { children: "Attempts" }),
2924
+ /* @__PURE__ */ jsx("th", { children: "Action" })
2925
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: list.data?.items.map((item) => /* @__PURE__ */ jsxs("tr", { children: [
2926
+ /* @__PURE__ */ jsx("td", { children: new Date(item.createdAt).toLocaleString() }),
2927
+ /* @__PURE__ */ jsx("td", { children: item.email }),
2928
+ /* @__PURE__ */ jsxs("td", { children: [/* @__PURE__ */ jsx("strong", { children: item.title }), /* @__PURE__ */ jsx("p", { children: item.message })] }),
2929
+ /* @__PURE__ */ jsx("td", { children: item.emailStatus }),
2930
+ /* @__PURE__ */ jsxs("td", { children: [item.emailAttempts, " / 3"] }),
2931
+ /* @__PURE__ */ jsx("td", { children: item.emailStatus === "failed" && item.emailAttempts < 3 && /* @__PURE__ */ jsx(Button, {
2932
+ busy: action.busy,
2933
+ onClick: () => void action.run(async () => {
2934
+ await api("admin/notification/retry", { id: item.id }, "POST");
2935
+ list.reload();
2936
+ }, "Email queued for retry."),
2937
+ children: "Retry email"
2938
+ }) })
2939
+ ] }, item.id)) })]
2940
+ })
2941
+ }),
2942
+ list.loading && /* @__PURE__ */ jsx("p", {
2943
+ role: "status",
2944
+ children: "Loading notifications…"
2945
+ }),
2946
+ !list.loading && list.data?.items.length === 0 && /* @__PURE__ */ jsx("p", { children: "No notifications match this status." })
2947
+ ] }),
2948
+ /* @__PURE__ */ jsx(Pager, {
2949
+ page: list.data,
2950
+ next: setCursor,
2951
+ reset: () => setCursor(void 0)
2952
+ })
2953
+ ]
2954
+ });
2955
+ }
2956
+ function AuditPage() {
2957
+ const [cursor, setCursor] = useState();
2958
+ const list = useLoad("admin/audit", { cursor });
2959
+ return /* @__PURE__ */ jsxs(PageFrame, {
2960
+ title: "Learning audit history",
2961
+ description: "Inspect course changes, enrollment decisions, grades, moderation, and delivery retries.",
2962
+ children: [
2963
+ /* @__PURE__ */ jsx(Notice, { error: list.error }),
2964
+ /* @__PURE__ */ jsx(Button, {
2965
+ onClick: list.reload,
2966
+ busy: list.loading,
2967
+ children: "Refresh"
2968
+ }),
2969
+ /* @__PURE__ */ jsxs(Panel, { children: [
2970
+ /* @__PURE__ */ jsx("div", {
2971
+ className: "lms-table-wrap",
2972
+ children: /* @__PURE__ */ jsxs("table", {
2973
+ className: "lms-table",
2974
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { children: [
2975
+ /* @__PURE__ */ jsx("th", { children: "Time" }),
2976
+ /* @__PURE__ */ jsx("th", { children: "Action" }),
2977
+ /* @__PURE__ */ jsx("th", { children: "Actor" }),
2978
+ /* @__PURE__ */ jsx("th", { children: "Resource" }),
2979
+ /* @__PURE__ */ jsx("th", { children: "Details" })
2980
+ ] }) }), /* @__PURE__ */ jsx("tbody", { children: list.data?.items.map((item) => /* @__PURE__ */ jsxs("tr", { children: [
2981
+ /* @__PURE__ */ jsx("td", { children: new Date(item.createdAt).toLocaleString() }),
2982
+ /* @__PURE__ */ jsx("td", { children: item.action }),
2983
+ /* @__PURE__ */ jsx("td", { children: item.actorId }),
2984
+ /* @__PURE__ */ jsx("td", { children: item.resourceId }),
2985
+ /* @__PURE__ */ jsx("td", { children: JSON.stringify(item.details) })
2986
+ ] }, item.id)) })]
2987
+ })
2988
+ }),
2989
+ list.loading && /* @__PURE__ */ jsx("p", {
2990
+ role: "status",
2991
+ children: "Loading audit history…"
2992
+ }),
2993
+ !list.loading && list.data?.items.length === 0 && /* @__PURE__ */ jsx("p", { children: "No learning administration actions recorded yet." })
2994
+ ] }),
2995
+ /* @__PURE__ */ jsx(Pager, {
2996
+ page: list.data,
2997
+ next: setCursor,
2998
+ reset: () => setCursor(void 0)
2999
+ })
3000
+ ]
3001
+ });
3002
+ }
3003
+ function MigrationPanel() {
3004
+ const load = useLoad("admin/migrations");
3005
+ const action = useAction();
3006
+ return /* @__PURE__ */ jsxs(Panel, { children: [
3007
+ /* @__PURE__ */ jsx("h2", { children: "Course edition migration" }),
3008
+ /* @__PURE__ */ jsx("p", { children: "Upgrade existing courses and pin enrollments to their original curriculum. Progress, assessments, grants and certificates are retained. Each batch handles up to 20 records and can be resumed after interruption." }),
3009
+ /* @__PURE__ */ jsx(Notice, { error: action.error || load.error || load.data?.lastError || void 0 }),
3010
+ load.data && /* @__PURE__ */ jsxs("p", { children: [
3011
+ "Phase: ",
3012
+ load.data.phase,
3013
+ " · Courses processed: ",
3014
+ load.data.coursesProcessed,
3015
+ " · Enrollments processed: ",
3016
+ load.data.enrollmentsProcessed
3017
+ ] }),
3018
+ /* @__PURE__ */ jsx(Button, {
3019
+ busy: action.busy,
3020
+ disabled: load.loading || load.data?.phase === "complete",
3021
+ onClick: () => void action.run(async () => {
3022
+ try {
3023
+ await api("admin/migrations/run", {}, "POST");
3024
+ } finally {
3025
+ load.reload();
3026
+ }
3027
+ }, "Migration checkpoint saved."),
3028
+ children: "Run next batch"
3029
+ }),
3030
+ /* @__PURE__ */ jsx(Button, {
3031
+ onClick: load.reload,
3032
+ children: "Refresh status"
3033
+ }),
3034
+ /* @__PURE__ */ jsx(Notice, { message: action.message })
3035
+ ] });
3036
+ }
3037
+ function HealthPanel() {
3038
+ const load = useLoad("admin/health");
3039
+ return /* @__PURE__ */ jsxs(Panel, {
3040
+ title: "Operational health",
3041
+ children: [
3042
+ /* @__PURE__ */ jsx(Notice, { error: load.error }),
3043
+ load.data && /* @__PURE__ */ jsxs(Fragment, { children: [
3044
+ /* @__PURE__ */ jsxs("p", { children: [
3045
+ "Plugin ",
3046
+ load.data.version,
3047
+ " · Email ",
3048
+ load.data.emailEnabled ? "enabled" : "disabled"
3049
+ ] }),
3050
+ /* @__PURE__ */ jsx("p", {
3051
+ role: load.data.maintenanceStale ? "alert" : void 0,
3052
+ children: load.data.maintenanceStale ? "Maintenance has not completed successfully in the last 45 minutes. Check the host scheduler and logs." : "Scheduled maintenance is reporting successfully."
3053
+ }),
3054
+ /* @__PURE__ */ jsxs("p", { children: ["Last completed: ", load.data.maintenance?.completedAt ?? "No successful run reported"] }),
3055
+ /* @__PURE__ */ jsxs("p", { children: [
3056
+ "Pending emails: ",
3057
+ load.data.pendingEmails,
3058
+ " · Failed emails: ",
3059
+ load.data.failedEmails,
3060
+ " · Oldest pending: ",
3061
+ load.data.oldestPendingAt ?? "None"
3062
+ ] }),
3063
+ /* @__PURE__ */ jsxs("p", { children: ["Migration phase: ", load.data.migration?.phase ?? "Not started"] })
3064
+ ] }),
3065
+ /* @__PURE__ */ jsx(Button, {
3066
+ onClick: load.reload,
3067
+ children: "Refresh health"
3068
+ })
3069
+ ]
3070
+ });
3071
+ }
3072
+ function SettingsPage() {
3073
+ const load = useLoad("admin/settings");
3074
+ const [settings, setSettings] = useState(settingsSchema.parse({}));
3075
+ const action = useAction();
3076
+ useEffect(() => {
3077
+ if (load.data) setSettings(load.data);
3078
+ }, [load.data]);
3079
+ const save = (event) => {
3080
+ event.preventDefault();
3081
+ action.run(async () => {
3082
+ await api("admin/settings/save", settings, "POST");
3083
+ load.reload();
3084
+ }, "LMS settings saved.");
3085
+ };
3086
+ return /* @__PURE__ */ jsxs(PageFrame, {
3087
+ title: "LMS settings",
3088
+ description: "Configure your academy, certificates, moderation, and reminders.",
3089
+ children: [
3090
+ /* @__PURE__ */ jsx(Notice, {
3091
+ error: load.error || action.error,
3092
+ message: action.message
3093
+ }),
3094
+ /* @__PURE__ */ jsx(MigrationPanel, {}),
3095
+ /* @__PURE__ */ jsx(HealthPanel, {}),
3096
+ /* @__PURE__ */ jsx(Panel, { children: /* @__PURE__ */ jsxs("form", {
3097
+ onSubmit: save,
3098
+ children: [
3099
+ /* @__PURE__ */ jsx(Field, {
3100
+ label: "Academy title",
3101
+ children: /* @__PURE__ */ jsx("input", {
3102
+ value: settings.academyTitle,
3103
+ onChange: (event) => setSettings({
3104
+ ...settings,
3105
+ academyTitle: event.target.value
3106
+ })
3107
+ })
3108
+ }),
3109
+ /* @__PURE__ */ jsx(Field, {
3110
+ label: "Academy introduction",
3111
+ children: /* @__PURE__ */ jsx("textarea", {
3112
+ rows: 3,
3113
+ value: settings.academyDescription,
3114
+ onChange: (event) => setSettings({
3115
+ ...settings,
3116
+ academyDescription: event.target.value
3117
+ })
3118
+ })
3119
+ }),
3120
+ /* @__PURE__ */ jsx(Field, {
3121
+ label: "Certificate issuer",
3122
+ children: /* @__PURE__ */ jsx("input", {
3123
+ value: settings.issuerName,
3124
+ onChange: (event) => setSettings({
3125
+ ...settings,
3126
+ issuerName: event.target.value
3127
+ })
3128
+ })
3129
+ }),
3130
+ /* @__PURE__ */ jsx(Field, {
3131
+ label: "Remind inactive learners after days",
3132
+ help: "0 disables reminders. Scheduled maintenance must be enabled in the host.",
3133
+ children: /* @__PURE__ */ jsx("input", {
3134
+ type: "number",
3135
+ min: 0,
3136
+ max: 90,
3137
+ value: settings.reminderDays,
3138
+ onChange: (event) => setSettings({
3139
+ ...settings,
3140
+ reminderDays: Number(event.target.value)
3141
+ })
3142
+ })
3143
+ }),
3144
+ /* @__PURE__ */ jsxs("label", {
3145
+ className: "lms-check",
3146
+ children: [/* @__PURE__ */ jsx("input", {
3147
+ type: "checkbox",
3148
+ checked: settings.moderateDiscussions,
3149
+ onChange: (event) => setSettings({
3150
+ ...settings,
3151
+ moderateDiscussions: event.target.checked
3152
+ })
3153
+ }), "Review discussions before publishing"]
3154
+ }),
3155
+ /* @__PURE__ */ jsxs("label", {
3156
+ className: "lms-check",
3157
+ children: [/* @__PURE__ */ jsx("input", {
3158
+ type: "checkbox",
3159
+ checked: settings.moderateReviews,
3160
+ onChange: (event) => setSettings({
3161
+ ...settings,
3162
+ moderateReviews: event.target.checked
3163
+ })
3164
+ }), "Review course reviews before publishing"]
3165
+ }),
3166
+ /* @__PURE__ */ jsxs("label", {
3167
+ className: "lms-check",
3168
+ children: [/* @__PURE__ */ jsx("input", {
3169
+ type: "checkbox",
3170
+ checked: settings.emailNotifications,
3171
+ onChange: (event) => setSettings({
3172
+ ...settings,
3173
+ emailNotifications: event.target.checked
3174
+ })
3175
+ }), "Send notifications through the site's EmDash email transport"]
3176
+ }),
3177
+ /* @__PURE__ */ jsx(Button, {
3178
+ type: "submit",
3179
+ busy: action.busy,
3180
+ children: "Save settings"
3181
+ })
3182
+ ]
3183
+ }) })
3184
+ ]
3185
+ });
3186
+ }
3187
+ function LearningWidget() {
3188
+ const report = useLoad("admin/report");
3189
+ return /* @__PURE__ */ jsxs("div", {
3190
+ className: "ag-lms-admin",
3191
+ children: [
3192
+ /* @__PURE__ */ jsx("style", { children: css }),
3193
+ /* @__PURE__ */ jsx(Notice, { error: report.error }),
3194
+ /* @__PURE__ */ jsxs("p", { children: [
3195
+ /* @__PURE__ */ jsx("strong", { children: report.data?.active ?? "—" }),
3196
+ " active ·",
3197
+ " ",
3198
+ /* @__PURE__ */ jsx("strong", { children: report.data?.completedEver ?? "—" }),
3199
+ " completed ·",
3200
+ " ",
3201
+ /* @__PURE__ */ jsx("strong", { children: report.data?.pending ?? "—" }),
3202
+ " awaiting approval"
3203
+ ] }),
3204
+ /* @__PURE__ */ jsx("a", {
3205
+ href: ADMIN_BASE,
3206
+ children: "Open learning overview →"
3207
+ })
3208
+ ]
3209
+ });
3210
+ }
3211
+ const pages = {
3212
+ "/": OverviewPage,
3213
+ "/courses": CoursesPage,
3214
+ "/learners": LearnersPage,
3215
+ "/assignments": AssignmentsPage,
3216
+ "/community": CommunityPage,
3217
+ "/cohorts": CohortsPage,
3218
+ "/paths": PathsPage,
3219
+ "/reports": ReportsPage,
3220
+ "/notifications": NotificationsPage,
3221
+ "/audit": AuditPage,
3222
+ "/settings": SettingsPage
3223
+ };
3224
+ const widgets = { learning: LearningWidget };
3225
+ const css = `
3226
+ .ag-lms-admin{color:var(--text-color-kumo-default,#1f2937);font-family:var(--font-emdash,ui-sans-serif,system-ui,sans-serif);padding:clamp(1rem,3vw,2.5rem);max-width:1400px;margin-inline:auto;line-height:1.55;direction:inherit}
3227
+ .ag-lms-admin *{box-sizing:border-box}.ag-lms-admin h1{font-size:clamp(1.6rem,3vw,2.15rem);margin:0}.ag-lms-admin h2{font-size:1.15rem;margin-block:0 .8rem}.ag-lms-admin h3{font-size:1rem}.ag-lms-admin p{margin-block:.5rem 1rem}.lms-page-header{display:flex;align-items:center;justify-content:space-between;gap:1rem;margin-block-end:1.5rem}.lms-page-header>div>p{max-width:65ch;color:var(--text-color-kumo-subtle,#64748b)}.lms-eyebrow{font-size:.7rem;letter-spacing:.15em;font-weight:700}.lms-tabs{display:flex;gap:.3rem;flex-wrap:wrap;margin-block:1rem 1.5rem}.lms-tabs a{padding:.4rem .7rem;border-radius:.4rem;text-decoration:none;color:inherit;background:var(--color-kumo-control,#f1f5f9)}.lms-panel{background:var(--color-kumo-base,#fff);border:1px solid var(--color-kumo-line,#d1d5db);border-radius:.65rem;padding:1.3rem;margin-block-end:1rem;min-width:0}.lms-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.lms-course-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:1rem}.lms-course-grid .lms-panel{display:flex;flex-direction:column;align-items:flex-start}.lms-course-grid .lms-actions{margin-block-start:auto}.lms-metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:1rem}.lms-metrics strong{display:block;font-size:2rem;margin-block-start:.5rem}.lms-metrics span,.lms-muted{font-size:.85rem;color:var(--text-color-kumo-subtle,#64748b)}.lms-actions,.lms-toolbar{display:flex;gap:.5rem;align-items:center;flex-wrap:wrap;margin-block:.6rem 1rem}.lms-toolbar>.lms-field{min-width:160px;margin:0}.lms-button{display:inline-flex;justify-content:center;align-items:center;gap:.4rem;min-height:38px;padding:.5rem .8rem;border:1px solid var(--color-kumo-line,#cbd5e1);border-radius:.4rem;background:var(--color-kumo-control,#f8fafc);color:var(--text-color-kumo-default,#1e293b);font:inherit;font-size:.875rem;font-weight:600;text-decoration:none;cursor:pointer}.lms-button:hover{background:var(--color-kumo-interact,#e2e8f0)}.lms-button:disabled{opacity:.55;cursor:default}.lms-danger{color:var(--text-color-kumo-danger,#b91c1c)}.lms-field{display:grid;gap:.35rem;margin-block-end:1rem;min-width:0;font-size:.875rem;font-weight:600}.lms-field small{font-weight:400;color:var(--text-color-kumo-subtle,#64748b)}.ag-lms-admin input:not([type=checkbox]):not([type=radio]),.ag-lms-admin textarea,.ag-lms-admin select{width:100%;border:1px solid var(--color-kumo-line,#cbd5e1);border-radius:.35rem;background:var(--color-kumo-control,#fff);color:inherit;padding:.6rem;font:inherit;font-size:.875rem;font-weight:400}.ag-lms-admin input[type=checkbox],.ag-lms-admin input[type=radio]{width:1rem;height:1rem;accent-color:var(--color-kumo-brand,#2563eb)}.ag-lms-admin :focus-visible{outline:2px solid var(--color-kumo-focus,#2563eb);outline-offset:3px}.lms-check{display:flex;gap:.6rem;align-items:center;font-size:.9rem;margin-block:.7rem}.lms-notice{padding:.8rem 1rem;border:1px solid var(--color-kumo-line,#cbd5e1);border-radius:.4rem;background:var(--color-kumo-tint,#f1f5f9)}.lms-error{color:var(--text-color-kumo-danger,#b91c1c);border-color:currentColor}.lms-badge{font-size:.7rem;text-transform:uppercase;font-weight:700;border:1px solid var(--color-kumo-line,#cbd5e1);border-radius:1rem;padding:.2rem .6rem;margin-block-end:.6rem}.lms-studio{display:grid;grid-template-columns:330px minmax(0,1fr);gap:1.3rem;align-items:start}.lms-module{border-block-end:1px solid var(--color-kumo-line,#cbd5e1);padding-block:1rem}.lms-lesson-list{list-style:none;padding:0;margin:0}.lms-lesson-list li{display:flex;align-items:center;gap:.3rem}.lms-select-lesson{background:transparent;color:inherit;border:0;border-radius:.35rem;cursor:pointer;text-align:start;padding:.6rem;flex:1;font:inherit;font-size:.9rem}.lms-select-lesson[aria-current=true]{background:var(--color-kumo-control,#eff6ff);font-weight:600}.lms-select-lesson small{display:block;font-weight:400;opacity:.75}.lms-lesson-list .lms-actions{margin:0}.lms-option{display:flex;align-items:center;gap:.5rem;margin-block:.5rem}.lms-question{border-block-start:1px solid var(--color-kumo-line,#cbd5e1);padding-block:1rem}.lms-question fieldset{border:1px solid var(--color-kumo-line,#cbd5e1);border-radius:.4rem;margin-block:1rem;padding:1rem}.lms-resource{border-block-end:1px solid var(--color-kumo-line,#cbd5e1);padding-block:1rem}.lms-file{position:absolute;width:1px!important;height:1px;opacity:0}.lms-table-wrap{overflow:auto}.lms-table{width:100%;border-collapse:collapse;font-size:.875rem}.lms-table th,.lms-table td{padding:.7rem;text-align:start;border-block-end:1px solid var(--color-kumo-line,#e5e7eb);vertical-align:top}.lms-table th{font-weight:600}.lms-table small{display:block;color:var(--text-color-kumo-subtle,#64748b)}.lms-table .lms-actions{margin:0}.lms-body{white-space:pre-wrap;overflow-wrap:anywhere}.ag-lms-admin details{padding:.7rem;border:1px solid var(--color-kumo-line,#d1d5db);border-radius:.4rem;margin-block:1rem}.ag-lms-admin summary{cursor:pointer;font-weight:600}.lms-notice:empty{display:none}@media(max-width:900px){.lms-studio{grid-template-columns:1fr}.lms-page-header{align-items:start;flex-direction:column}}@media(max-width:600px){.lms-grid{grid-template-columns:1fr}.lms-toolbar{align-items:stretch}.lms-toolbar>.lms-field{width:100%}.lms-panel{padding:1rem}}`;
3228
+
3229
+ //#endregion
3230
+ export { AssignmentsPage, AuditPage, CohortsPage, CommunityPage, CoursesPage, LearnersPage, LearningWidget, NotificationsPage, OverviewPage, PathsPage, ReportsPage, SettingsPage, pages, widgets };
3231
+ //# sourceMappingURL=admin.js.map