@beanchain/handbook-lms 0.1.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 (69) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +149 -0
  3. package/dist/adapter/index.cjs +4 -0
  4. package/dist/adapter/index.cjs.map +1 -0
  5. package/dist/adapter/index.d.cts +1 -0
  6. package/dist/adapter/index.d.ts +1 -0
  7. package/dist/adapter/index.js +3 -0
  8. package/dist/adapter/index.js.map +1 -0
  9. package/dist/chunk-6Z77HGDR.js +2384 -0
  10. package/dist/chunk-6Z77HGDR.js.map +1 -0
  11. package/dist/chunk-GZIXEHPW.js +268 -0
  12. package/dist/chunk-GZIXEHPW.js.map +1 -0
  13. package/dist/chunk-HOQRIQ33.js +3 -0
  14. package/dist/chunk-HOQRIQ33.js.map +1 -0
  15. package/dist/chunk-N4WYUTCR.js +264 -0
  16. package/dist/chunk-N4WYUTCR.js.map +1 -0
  17. package/dist/chunk-PYWAXMRT.js +6 -0
  18. package/dist/chunk-PYWAXMRT.js.map +1 -0
  19. package/dist/chunk-UVF7B4L2.js +73 -0
  20. package/dist/chunk-UVF7B4L2.js.map +1 -0
  21. package/dist/chunk-VRHNGFUG.js +90 -0
  22. package/dist/chunk-VRHNGFUG.js.map +1 -0
  23. package/dist/config/index.cjs +8 -0
  24. package/dist/config/index.cjs.map +1 -0
  25. package/dist/config/index.d.cts +52 -0
  26. package/dist/config/index.d.ts +52 -0
  27. package/dist/config/index.js +3 -0
  28. package/dist/config/index.js.map +1 -0
  29. package/dist/contracts/index.cjs +307 -0
  30. package/dist/contracts/index.cjs.map +1 -0
  31. package/dist/contracts/index.d.cts +3034 -0
  32. package/dist/contracts/index.d.ts +3034 -0
  33. package/dist/contracts/index.js +3 -0
  34. package/dist/contracts/index.js.map +1 -0
  35. package/dist/core/index.cjs +107 -0
  36. package/dist/core/index.cjs.map +1 -0
  37. package/dist/core/index.d.cts +293 -0
  38. package/dist/core/index.d.ts +293 -0
  39. package/dist/core/index.js +3 -0
  40. package/dist/core/index.js.map +1 -0
  41. package/dist/index-caUTkZqX.d.cts +321 -0
  42. package/dist/index-caUTkZqX.d.ts +321 -0
  43. package/dist/index.cjs +3129 -0
  44. package/dist/index.cjs.map +1 -0
  45. package/dist/index.d.cts +10 -0
  46. package/dist/index.d.ts +10 -0
  47. package/dist/index.js +9 -0
  48. package/dist/index.js.map +1 -0
  49. package/dist/react/index.cjs +77 -0
  50. package/dist/react/index.cjs.map +1 -0
  51. package/dist/react/index.d.cts +18 -0
  52. package/dist/react/index.d.ts +18 -0
  53. package/dist/react/index.js +3 -0
  54. package/dist/react/index.js.map +1 -0
  55. package/dist/react-headless/index.cjs +268 -0
  56. package/dist/react-headless/index.cjs.map +1 -0
  57. package/dist/react-headless/index.d.cts +181 -0
  58. package/dist/react-headless/index.d.ts +181 -0
  59. package/dist/react-headless/index.js +3 -0
  60. package/dist/react-headless/index.js.map +1 -0
  61. package/dist/react-mui/index.cjs +2653 -0
  62. package/dist/react-mui/index.cjs.map +1 -0
  63. package/dist/react-mui/index.d.cts +53 -0
  64. package/dist/react-mui/index.d.ts +53 -0
  65. package/dist/react-mui/index.js +4 -0
  66. package/dist/react-mui/index.js.map +1 -0
  67. package/package.json +85 -0
  68. package/sql/README.md +80 -0
  69. package/sql/blueprint.sql +7 -0
@@ -0,0 +1,2653 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var material = require('@mui/material');
5
+ var DragIndicatorIcon = require('@mui/icons-material/DragIndicator');
6
+ var EditIcon = require('@mui/icons-material/Edit');
7
+ var MDEditor3 = require('@uiw/react-md-editor');
8
+ var jsxRuntime = require('react/jsx-runtime');
9
+ var ExpandMoreIcon = require('@mui/icons-material/ExpandMore');
10
+ var MenuIcon = require('@mui/icons-material/Menu');
11
+ var styles = require('@mui/material/styles');
12
+
13
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
14
+
15
+ var DragIndicatorIcon__default = /*#__PURE__*/_interopDefault(DragIndicatorIcon);
16
+ var EditIcon__default = /*#__PURE__*/_interopDefault(EditIcon);
17
+ var MDEditor3__default = /*#__PURE__*/_interopDefault(MDEditor3);
18
+ var ExpandMoreIcon__default = /*#__PURE__*/_interopDefault(ExpandMoreIcon);
19
+ var MenuIcon__default = /*#__PURE__*/_interopDefault(MenuIcon);
20
+
21
+ // src/react-mui/CurriculumEditor.tsx
22
+ var errorMessage = (error, fallback) => {
23
+ if (error instanceof Error) return error.message || fallback;
24
+ return fallback;
25
+ };
26
+ var sortCurriculum = (items) => [...items].sort((a, b) => a.display_order - b.display_order).map((section) => ({
27
+ ...section,
28
+ modules: [...section.modules ?? []].sort((a, b) => a.display_order - b.display_order).map((module) => ({
29
+ ...module,
30
+ stages: [...module.stages ?? []].sort((a, b) => a.display_order - b.display_order).map((stage) => ({
31
+ ...stage,
32
+ questions: [...stage.questions ?? []].sort((a, b) => a.display_order - b.display_order)
33
+ }))
34
+ }))
35
+ }));
36
+ var useLmsCurriculumEditor = ({
37
+ adapter,
38
+ courseSlug,
39
+ enabled = true
40
+ }) => {
41
+ const [sections, setSections] = react.useState([]);
42
+ const [isLoading, setIsLoading] = react.useState(false);
43
+ const [isSaving, setIsSaving] = react.useState(false);
44
+ const [error, setError] = react.useState(null);
45
+ const reload = react.useCallback(async () => {
46
+ if (!enabled || !courseSlug) return;
47
+ setIsLoading(true);
48
+ setError(null);
49
+ try {
50
+ const next = await adapter.getAdminCurriculum(courseSlug);
51
+ setSections(sortCurriculum(next));
52
+ } catch (err) {
53
+ setError(errorMessage(err, "Unable to load curriculum."));
54
+ } finally {
55
+ setIsLoading(false);
56
+ }
57
+ }, [adapter, courseSlug, enabled]);
58
+ react.useEffect(() => {
59
+ void reload();
60
+ }, [reload]);
61
+ const runSave = react.useCallback(
62
+ async (action, fallback) => {
63
+ setIsSaving(true);
64
+ setError(null);
65
+ try {
66
+ await action();
67
+ await reload();
68
+ return { ok: true, error: null };
69
+ } catch (err) {
70
+ const message = errorMessage(err, fallback);
71
+ setError(message);
72
+ return { ok: false, error: message };
73
+ } finally {
74
+ setIsSaving(false);
75
+ }
76
+ },
77
+ [reload]
78
+ );
79
+ return {
80
+ sections,
81
+ setSections,
82
+ isLoading,
83
+ isSaving,
84
+ error,
85
+ reload,
86
+ createSection: (data) => runSave(() => adapter.createSection({ courseSlug, data }), "Unable to create section."),
87
+ updateSection: (sectionId, data) => runSave(() => adapter.updateSection({ courseSlug, sectionId, data }), "Unable to update section."),
88
+ archiveSection: (sectionId) => runSave(() => adapter.archiveSection({ courseSlug, sectionId }), "Unable to archive section."),
89
+ createModule: (data) => runSave(() => adapter.createModule({ courseSlug, data }), "Unable to create module."),
90
+ updateModule: (moduleId, data) => runSave(() => adapter.updateModule({ courseSlug, moduleId, data }), "Unable to update module."),
91
+ archiveModule: (moduleId) => runSave(() => adapter.archiveModule({ courseSlug, moduleId }), "Unable to archive module."),
92
+ createStage: (moduleId, data) => runSave(() => adapter.createStage({ courseSlug, moduleId, data }), "Unable to create stage."),
93
+ updateStage: (stageId, data) => runSave(() => adapter.updateStage({ courseSlug, stageId, data }), "Unable to update stage."),
94
+ archiveStage: (stageId) => runSave(() => adapter.archiveStage({ courseSlug, stageId }), "Unable to archive stage."),
95
+ createQuestion: (stageId, data) => runSave(() => adapter.createQuestion({ courseSlug, stageId, data }), "Unable to create question."),
96
+ updateQuestion: (questionId, data) => runSave(() => adapter.updateQuestion({ courseSlug, questionId, data }), "Unable to update question."),
97
+ archiveQuestion: (questionId) => runSave(() => adapter.archiveQuestion({ courseSlug, questionId }), "Unable to archive question."),
98
+ reorderCurriculum: (data) => runSave(() => adapter.reorderCurriculum({ courseSlug, data }), "Unable to reorder curriculum.")
99
+ };
100
+ };
101
+ var useLmsReviewWorkspace = ({
102
+ adapter,
103
+ courseSlug,
104
+ enabled = true
105
+ }) => {
106
+ const [learners, setLearners] = react.useState([]);
107
+ const [selectedLearnerId, setSelectedLearnerId] = react.useState("");
108
+ const [summary, setSummary] = react.useState(null);
109
+ const [isLoading, setIsLoading] = react.useState(false);
110
+ const [isSaving, setIsSaving] = react.useState(false);
111
+ const [error, setError] = react.useState(null);
112
+ const loadLearners = react.useCallback(async () => {
113
+ if (!enabled) return;
114
+ try {
115
+ const rows = await adapter.listLearners();
116
+ setLearners(rows);
117
+ if (!selectedLearnerId && rows[0]) {
118
+ setSelectedLearnerId(rows[0].id);
119
+ }
120
+ } catch (err) {
121
+ setError(errorMessage(err, "Unable to load learners."));
122
+ }
123
+ }, [adapter, enabled, selectedLearnerId]);
124
+ const loadSummary = react.useCallback(async () => {
125
+ if (!enabled || !courseSlug || !selectedLearnerId) return;
126
+ setIsLoading(true);
127
+ setError(null);
128
+ try {
129
+ const next = await adapter.getLearningSummary({ courseSlug, learnerUserId: selectedLearnerId });
130
+ setSummary(next);
131
+ } catch (err) {
132
+ setError(errorMessage(err, "Unable to load learner summary."));
133
+ } finally {
134
+ setIsLoading(false);
135
+ }
136
+ }, [adapter, courseSlug, enabled, selectedLearnerId]);
137
+ react.useEffect(() => {
138
+ void loadLearners();
139
+ }, [loadLearners]);
140
+ react.useEffect(() => {
141
+ void loadSummary();
142
+ }, [loadSummary]);
143
+ const runSave = react.useCallback(
144
+ async (action, fallback) => {
145
+ setIsSaving(true);
146
+ setError(null);
147
+ try {
148
+ await action();
149
+ await loadSummary();
150
+ return { ok: true, error: null };
151
+ } catch (err) {
152
+ const message = errorMessage(err, fallback);
153
+ setError(message);
154
+ return { ok: false, error: message };
155
+ } finally {
156
+ setIsSaving(false);
157
+ }
158
+ },
159
+ [loadSummary]
160
+ );
161
+ const records = react.useMemo(() => summary?.records ?? [], [summary?.records]);
162
+ return {
163
+ learners,
164
+ selectedLearnerId,
165
+ setSelectedLearnerId,
166
+ summary,
167
+ records,
168
+ isLoading,
169
+ isSaving,
170
+ error,
171
+ reload: loadSummary,
172
+ reviewModule: (data) => runSave(() => adapter.reviewModule({ courseSlug, data }), "Unable to update review."),
173
+ updateAssignment: (data) => runSave(() => adapter.updateAssignment({ courseSlug, data }), "Unable to update assignment.")
174
+ };
175
+ };
176
+ var useLmsLearningWorkspace = ({
177
+ adapter,
178
+ courseSlug,
179
+ learnerUserId = null,
180
+ enabled = true
181
+ }) => {
182
+ const [summary, setSummary] = react.useState(null);
183
+ const [isLoading, setIsLoading] = react.useState(false);
184
+ const [isSavingQuestion, setIsSavingQuestion] = react.useState(false);
185
+ const [isSubmittingModule, setIsSubmittingModule] = react.useState(false);
186
+ const [isUpdatingAssignment, setIsUpdatingAssignment] = react.useState(false);
187
+ const [error, setError] = react.useState(null);
188
+ const loadSummary = react.useCallback(async () => {
189
+ if (!enabled || !courseSlug) {
190
+ setSummary(null);
191
+ return;
192
+ }
193
+ setIsLoading(true);
194
+ setError(null);
195
+ try {
196
+ const next = await adapter.getLearningSummary({ courseSlug, learnerUserId });
197
+ setSummary(next);
198
+ } catch (err) {
199
+ setError(errorMessage(err, "Unable to load learning summary."));
200
+ } finally {
201
+ setIsLoading(false);
202
+ }
203
+ }, [adapter, courseSlug, enabled, learnerUserId]);
204
+ react.useEffect(() => {
205
+ void loadSummary();
206
+ }, [loadSummary]);
207
+ const saveQuestionResponse = react.useCallback(
208
+ async (data, opts) => {
209
+ setIsSavingQuestion(true);
210
+ setError(null);
211
+ try {
212
+ await adapter.saveQuestionResponse(data);
213
+ if (opts?.reload) await loadSummary();
214
+ return { ok: true, error: null };
215
+ } catch (err) {
216
+ const message = errorMessage(err, "Unable to save response.");
217
+ setError(message);
218
+ return { ok: false, error: message };
219
+ } finally {
220
+ setIsSavingQuestion(false);
221
+ }
222
+ },
223
+ [adapter, loadSummary]
224
+ );
225
+ const submitModule = react.useCallback(
226
+ async (data, opts) => {
227
+ setIsSubmittingModule(true);
228
+ setError(null);
229
+ try {
230
+ await adapter.submitModule(data);
231
+ if (opts?.reload ?? true) await loadSummary();
232
+ return { ok: true, error: null };
233
+ } catch (err) {
234
+ const message = errorMessage(err, "Unable to submit module.");
235
+ setError(message);
236
+ return { ok: false, error: message };
237
+ } finally {
238
+ setIsSubmittingModule(false);
239
+ }
240
+ },
241
+ [adapter, loadSummary]
242
+ );
243
+ const updateAssignment = react.useCallback(
244
+ async (data, opts) => {
245
+ setIsUpdatingAssignment(true);
246
+ setError(null);
247
+ try {
248
+ await adapter.updateAssignment({ courseSlug, data });
249
+ if (opts?.reload ?? true) await loadSummary();
250
+ return { ok: true, error: null };
251
+ } catch (err) {
252
+ const message = errorMessage(err, "Unable to update assignment.");
253
+ setError(message);
254
+ return { ok: false, error: message };
255
+ } finally {
256
+ setIsUpdatingAssignment(false);
257
+ }
258
+ },
259
+ [adapter, courseSlug, loadSummary]
260
+ );
261
+ const records = react.useMemo(() => summary?.records ?? [], [summary?.records]);
262
+ const reviewQueue = react.useMemo(() => summary?.review_queue ?? [], [summary?.review_queue]);
263
+ return {
264
+ summary,
265
+ records,
266
+ reviewQueue,
267
+ isLoading,
268
+ isSavingQuestion,
269
+ isSubmittingModule,
270
+ isUpdatingAssignment,
271
+ isSaving: isSavingQuestion || isSubmittingModule || isUpdatingAssignment,
272
+ error,
273
+ reload: loadSummary,
274
+ saveQuestionResponse,
275
+ submitModule,
276
+ updateAssignment
277
+ };
278
+ };
279
+ var DragDropContext = react.lazy(() => import('@hello-pangea/dnd').then((mod) => ({ default: mod.DragDropContext })));
280
+ var Droppable = react.lazy(() => import('@hello-pangea/dnd').then((mod) => ({ default: mod.Droppable })));
281
+ var Draggable = react.lazy(() => import('@hello-pangea/dnd').then((mod) => ({ default: mod.Draggable })));
282
+ var dndFallback = /* @__PURE__ */ jsxRuntime.jsx(material.Skeleton, { variant: "rounded", height: 56 });
283
+ var slugify = (value) => value.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
284
+ var parseSectionId = (droppableId) => droppableId.startsWith("section-") ? droppableId.slice(8) : null;
285
+ var resolveSlug = ({ title, slug, slugMode }) => slugMode === "derived" ? slugify(title) : slug.trim();
286
+ var validateSlug = (value, config, required) => {
287
+ const slug = value.trim();
288
+ if (!slug) return required ? config.validation.requiredSlugMessage : null;
289
+ return config.validation.slugPattern.test(slug) ? null : config.validation.slugPatternMessage;
290
+ };
291
+ var HandbookLmsCurriculumEditor = ({
292
+ adapter,
293
+ courseSlug,
294
+ config,
295
+ title,
296
+ onBack,
297
+ notifications,
298
+ enabled = true
299
+ }) => {
300
+ const editor = useLmsCurriculumEditor({ adapter, courseSlug, enabled });
301
+ const [error, setError] = react.useState(null);
302
+ const [sectionDialog, setSectionDialog] = react.useState({
303
+ open: false,
304
+ submitAttempted: false,
305
+ formError: null,
306
+ draft: { slug: "", title: "", description: "", slugMode: "derived" }
307
+ });
308
+ const [moduleDialog, setModuleDialog] = react.useState({
309
+ open: false,
310
+ submitAttempted: false,
311
+ formError: null,
312
+ draft: { section_id: "", slug: "", title: "", summary: "", module_mode: "single_stage", slugMode: "derived" }
313
+ });
314
+ const [stageDialog, setStageDialog] = react.useState({
315
+ open: false,
316
+ submitAttempted: false,
317
+ formError: null,
318
+ draft: { module_id: "", title: "", content_md: "" }
319
+ });
320
+ const [questionDialog, setQuestionDialog] = react.useState({
321
+ open: false,
322
+ submitAttempted: false,
323
+ formError: null,
324
+ draft: { stage_id: "", prompt: "", response_type: "long_text" }
325
+ });
326
+ const sections = editor.sections;
327
+ const sectionById = react.useMemo(() => new Map(sections.map((section) => [section.id, section])), [sections]);
328
+ const notify = (severity, message) => {
329
+ notifications?.notify?.({ severity, message });
330
+ if (severity === "success") notifications?.success?.(message);
331
+ if (severity === "error") notifications?.error?.(message);
332
+ if (severity === "info") notifications?.info?.(message);
333
+ if (severity === "warning") notifications?.warning?.(message);
334
+ };
335
+ const sectionCustomSlugError = sectionDialog.draft.slugMode === "custom" ? validateSlug(sectionDialog.draft.slug, config, sectionDialog.submitAttempted) : null;
336
+ const moduleCustomSlugError = moduleDialog.draft.slugMode === "custom" ? validateSlug(moduleDialog.draft.slug, config, moduleDialog.submitAttempted) : null;
337
+ const closeSectionDialog = () => {
338
+ if (editor.isSaving) return;
339
+ setSectionDialog((prev) => ({ ...prev, open: false, submitAttempted: false, formError: null }));
340
+ };
341
+ const closeModuleDialog = () => {
342
+ if (editor.isSaving) return;
343
+ setModuleDialog((prev) => ({ ...prev, open: false, submitAttempted: false, formError: null }));
344
+ };
345
+ const closeStageDialog = () => {
346
+ if (editor.isSaving) return;
347
+ setStageDialog((prev) => ({ ...prev, open: false, submitAttempted: false, formError: null }));
348
+ };
349
+ const closeQuestionDialog = () => {
350
+ if (editor.isSaving) return;
351
+ setQuestionDialog((prev) => ({ ...prev, open: false, submitAttempted: false, formError: null }));
352
+ };
353
+ const saveSection = async () => {
354
+ setSectionDialog((prev) => ({ ...prev, submitAttempted: true, formError: null }));
355
+ const draft = sectionDialog.draft;
356
+ const slug = resolveSlug(draft);
357
+ const customSlugError = draft.slugMode === "custom" ? validateSlug(draft.slug, config, true) : null;
358
+ if (!slug || !draft.title.trim() || customSlugError) {
359
+ setSectionDialog((prev) => ({ ...prev, formError: customSlugError ?? config.validation.requiredTitleMessage }));
360
+ return;
361
+ }
362
+ const payload = {
363
+ slug,
364
+ title: draft.title.trim(),
365
+ description: draft.description.trim() || null
366
+ };
367
+ const result = draft.id ? await editor.updateSection(draft.id, payload) : await editor.createSection(payload);
368
+ if (!result.ok) {
369
+ const message = result.error ?? "Unable to save section.";
370
+ notify("error", message);
371
+ setSectionDialog((prev) => ({ ...prev, formError: message }));
372
+ return;
373
+ }
374
+ notify("success", `${config.naming.section} saved.`);
375
+ closeSectionDialog();
376
+ };
377
+ const saveModule = async () => {
378
+ setModuleDialog((prev) => ({ ...prev, submitAttempted: true, formError: null }));
379
+ const draft = moduleDialog.draft;
380
+ const slug = resolveSlug(draft);
381
+ const customSlugError = draft.slugMode === "custom" ? validateSlug(draft.slug, config, true) : null;
382
+ if (!draft.section_id || !slug || !draft.title.trim() || !draft.summary.trim() || customSlugError) {
383
+ setModuleDialog((prev) => ({
384
+ ...prev,
385
+ formError: customSlugError ?? (!draft.summary.trim() ? config.validation.requiredSummaryMessage : config.validation.requiredTitleMessage)
386
+ }));
387
+ return;
388
+ }
389
+ const section = sectionById.get(draft.section_id);
390
+ const fallbackType = section?.slug === "vocabulary" ? "vocabulary" : section?.slug === "communication-protocol" ? "protocol" : "lesson";
391
+ const payload = {
392
+ section_id: draft.section_id,
393
+ slug,
394
+ title: draft.title.trim(),
395
+ summary: draft.summary.trim(),
396
+ module_mode: draft.module_mode,
397
+ module_type: fallbackType
398
+ };
399
+ const result = draft.id ? await editor.updateModule(draft.id, payload) : await editor.createModule(payload);
400
+ if (!result.ok) {
401
+ const message = result.error ?? "Unable to save module.";
402
+ notify("error", message);
403
+ setModuleDialog((prev) => ({ ...prev, formError: message }));
404
+ return;
405
+ }
406
+ notify("success", `${config.naming.module} saved.`);
407
+ closeModuleDialog();
408
+ };
409
+ const saveStage = async () => {
410
+ setStageDialog((prev) => ({ ...prev, submitAttempted: true, formError: null }));
411
+ const draft = stageDialog.draft;
412
+ if (!draft.module_id || !draft.title.trim()) {
413
+ setStageDialog((prev) => ({ ...prev, formError: config.validation.requiredTitleMessage }));
414
+ return;
415
+ }
416
+ const payload = { title: draft.title.trim(), content_md: draft.content_md };
417
+ const result = draft.id ? await editor.updateStage(draft.id, payload) : await editor.createStage(draft.module_id, payload);
418
+ if (!result.ok) {
419
+ const message = result.error ?? "Unable to save stage.";
420
+ notify("error", message);
421
+ setStageDialog((prev) => ({ ...prev, formError: message }));
422
+ return;
423
+ }
424
+ notify("success", `${config.naming.stage} saved.`);
425
+ closeStageDialog();
426
+ };
427
+ const saveQuestion = async () => {
428
+ setQuestionDialog((prev) => ({ ...prev, submitAttempted: true, formError: null }));
429
+ const draft = questionDialog.draft;
430
+ if (!draft.stage_id || !draft.prompt.trim()) {
431
+ setQuestionDialog((prev) => ({ ...prev, formError: config.validation.requiredPromptMessage }));
432
+ return;
433
+ }
434
+ const payload = {
435
+ stage_id: draft.stage_id,
436
+ prompt: draft.prompt.trim(),
437
+ response_type: draft.response_type
438
+ };
439
+ const result = draft.id ? await editor.updateQuestion(draft.id, payload) : await editor.createQuestion(draft.stage_id, payload);
440
+ if (!result.ok) {
441
+ const message = result.error ?? "Unable to save question.";
442
+ notify("error", message);
443
+ setQuestionDialog((prev) => ({ ...prev, formError: message }));
444
+ return;
445
+ }
446
+ notify("success", `${config.naming.question} saved.`);
447
+ closeQuestionDialog();
448
+ };
449
+ const handleModuleDragEnd = async (result) => {
450
+ if (!config.workflow.enableModuleDragAndDrop || !result.destination) return;
451
+ const sourceId = parseSectionId(result.source.droppableId);
452
+ const destId = parseSectionId(result.destination.droppableId);
453
+ if (!sourceId || !destId) return;
454
+ if (sourceId === destId && result.source.index === result.destination.index) return;
455
+ const nextSections = sections.map((section) => ({ ...section, modules: [...section.modules ?? []] }));
456
+ const source = nextSections.find((section) => section.id === sourceId);
457
+ const destination = nextSections.find((section) => section.id === destId);
458
+ if (!source || !destination) return;
459
+ const [moved] = source.modules.splice(result.source.index, 1);
460
+ if (!moved) return;
461
+ const movedModule = { ...moved, section_id: destId };
462
+ destination.modules.splice(result.destination.index, 0, movedModule);
463
+ for (const section of nextSections) {
464
+ section.modules = section.modules.map((module, index) => ({ ...module, display_order: index + 1 }));
465
+ }
466
+ editor.setSections(nextSections);
467
+ const saveResult = await editor.reorderCurriculum({
468
+ modules: nextSections.flatMap(
469
+ (section) => section.modules.map((module) => ({ id: module.id, section_id: section.id, display_order: module.display_order }))
470
+ )
471
+ });
472
+ if (!saveResult.ok) {
473
+ const message = saveResult.error ?? "Unable to reorder modules.";
474
+ notify("error", message);
475
+ setError(message);
476
+ await editor.reload();
477
+ }
478
+ };
479
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 2, children: [
480
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", useFlexGap: true, children: [
481
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h5", children: title ?? config.labels.editorTitle }),
482
+ onBack ? /* @__PURE__ */ jsxRuntime.jsx(material.Button, { onClick: onBack, children: config.labels.backButton }) : null
483
+ ] }),
484
+ error || editor.error ? /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "error", children: error ?? editor.error }) : null,
485
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Card, { children: [
486
+ /* @__PURE__ */ jsxRuntime.jsx(
487
+ material.CardHeader,
488
+ {
489
+ title: `${config.naming.course} Structure`,
490
+ subheader: `Manage ${config.naming.section.toLowerCase()}s, ${config.naming.module.toLowerCase()}s, ${config.naming.stage.toLowerCase()}s, and ${config.naming.question.toLowerCase()}s.`,
491
+ action: /* @__PURE__ */ jsxRuntime.jsx(
492
+ material.Button,
493
+ {
494
+ size: "small",
495
+ variant: "contained",
496
+ onClick: () => setSectionDialog({
497
+ open: true,
498
+ submitAttempted: false,
499
+ formError: null,
500
+ draft: { slug: "", title: "", description: "", slugMode: "derived" }
501
+ }),
502
+ children: config.labels.addSection
503
+ }
504
+ )
505
+ }
506
+ ),
507
+ /* @__PURE__ */ jsxRuntime.jsx(material.CardContent, { children: editor.isLoading ? /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1, children: [
508
+ /* @__PURE__ */ jsxRuntime.jsx(material.Skeleton, { variant: "rounded", height: 72 }),
509
+ /* @__PURE__ */ jsxRuntime.jsx(material.Skeleton, { variant: "rounded", height: 72 }),
510
+ /* @__PURE__ */ jsxRuntime.jsx(material.Skeleton, { variant: "rounded", height: 72 })
511
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(react.Suspense, { fallback: dndFallback, children: /* @__PURE__ */ jsxRuntime.jsx(DragDropContext, { onDragEnd: handleModuleDragEnd, children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 2, children: [
512
+ sections.map((section) => /* @__PURE__ */ jsxRuntime.jsx(material.Card, { variant: "outlined", children: /* @__PURE__ */ jsxRuntime.jsx(material.CardContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1, children: [
513
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: { xs: "column", sm: "row" }, justifyContent: "space-between", spacing: 1, children: [
514
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", spacing: 1, alignItems: "center", flexWrap: "wrap", children: [
515
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", children: section.title }),
516
+ /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { size: "small", label: `${section.modules?.length ?? 0} ${config.naming.module.toLowerCase()}s` }),
517
+ !section.active ? /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { size: "small", color: "warning", label: "Archived" }) : null
518
+ ] }),
519
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", spacing: 1, flexWrap: "wrap", useFlexGap: true, children: [
520
+ /* @__PURE__ */ jsxRuntime.jsx(
521
+ material.Button,
522
+ {
523
+ size: "small",
524
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(EditIcon__default.default, {}),
525
+ onClick: () => setSectionDialog({
526
+ open: true,
527
+ submitAttempted: false,
528
+ formError: null,
529
+ draft: {
530
+ id: section.id,
531
+ slug: section.slug,
532
+ title: section.title,
533
+ description: section.description ?? "",
534
+ slugMode: section.slug === slugify(section.title) ? "derived" : "custom"
535
+ }
536
+ }),
537
+ children: config.labels.editAction
538
+ }
539
+ ),
540
+ section.active ? /* @__PURE__ */ jsxRuntime.jsx(
541
+ material.Button,
542
+ {
543
+ size: "small",
544
+ color: "error",
545
+ onClick: async () => {
546
+ if (!window.confirm(`Archive ${config.naming.section.toLowerCase()} "${section.title}"?`)) return;
547
+ const result = await editor.archiveSection(section.id);
548
+ if (!result.ok) {
549
+ const message = result.error ?? `Unable to archive ${config.naming.section.toLowerCase()}.`;
550
+ notify("error", message);
551
+ setError(message);
552
+ return;
553
+ }
554
+ notify("success", `${config.naming.section} archived.`);
555
+ },
556
+ children: config.labels.archiveAction
557
+ }
558
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
559
+ material.Button,
560
+ {
561
+ size: "small",
562
+ onClick: async () => {
563
+ const result = await editor.updateSection(section.id, { active: true });
564
+ if (!result.ok) {
565
+ const message = result.error ?? `Unable to restore ${config.naming.section.toLowerCase()}.`;
566
+ notify("error", message);
567
+ setError(message);
568
+ return;
569
+ }
570
+ notify("success", `${config.naming.section} restored.`);
571
+ },
572
+ children: config.labels.restoreAction
573
+ }
574
+ ),
575
+ /* @__PURE__ */ jsxRuntime.jsx(
576
+ material.Button,
577
+ {
578
+ size: "small",
579
+ variant: "contained",
580
+ onClick: () => setModuleDialog({
581
+ open: true,
582
+ submitAttempted: false,
583
+ formError: null,
584
+ draft: {
585
+ section_id: section.id,
586
+ slug: "",
587
+ title: "",
588
+ summary: "",
589
+ module_mode: "single_stage",
590
+ slugMode: "derived"
591
+ }
592
+ }),
593
+ children: config.labels.addModule
594
+ }
595
+ )
596
+ ] })
597
+ ] }),
598
+ /* @__PURE__ */ jsxRuntime.jsx(Droppable, { droppableId: `section-${section.id}`, children: (dropProvided) => /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { ref: dropProvided.innerRef, ...dropProvided.droppableProps, spacing: 1, children: [
599
+ (section.modules ?? []).map((module, moduleIndex) => /* @__PURE__ */ jsxRuntime.jsx(
600
+ Draggable,
601
+ {
602
+ draggableId: `module-${module.id}`,
603
+ index: moduleIndex,
604
+ isDragDisabled: !config.workflow.enableModuleDragAndDrop,
605
+ children: (dragProvided, dragSnapshot) => /* @__PURE__ */ jsxRuntime.jsx(
606
+ material.Card,
607
+ {
608
+ ref: dragProvided.innerRef,
609
+ ...dragProvided.draggableProps,
610
+ style: dragProvided.draggableProps.style,
611
+ variant: "outlined",
612
+ sx: { borderColor: dragSnapshot.isDragging ? "primary.main" : "divider" },
613
+ children: /* @__PURE__ */ jsxRuntime.jsx(material.CardContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1, children: [
614
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: { xs: "column", sm: "row" }, justifyContent: "space-between", spacing: 1, children: [
615
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", spacing: 1, alignItems: "center", flexWrap: "wrap", children: [
616
+ /* @__PURE__ */ jsxRuntime.jsx(
617
+ material.Stack,
618
+ {
619
+ ...dragProvided.dragHandleProps,
620
+ sx: { cursor: config.workflow.enableModuleDragAndDrop ? "grab" : "default", display: "inline-flex" },
621
+ children: /* @__PURE__ */ jsxRuntime.jsx(DragIndicatorIcon__default.default, { fontSize: "small", color: "action" })
622
+ }
623
+ ),
624
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle1", fontWeight: 600, children: module.title }),
625
+ /* @__PURE__ */ jsxRuntime.jsx(
626
+ material.Chip,
627
+ {
628
+ size: "small",
629
+ label: module.module_mode === "multi_stage" ? "Multi-stage" : "Single-stage"
630
+ }
631
+ ),
632
+ !module.active || module.archived_at ? /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { size: "small", color: "warning", label: "Archived" }) : null
633
+ ] }),
634
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", spacing: 1, flexWrap: "wrap", useFlexGap: true, children: [
635
+ /* @__PURE__ */ jsxRuntime.jsx(
636
+ material.Button,
637
+ {
638
+ size: "small",
639
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(EditIcon__default.default, {}),
640
+ onClick: () => setModuleDialog({
641
+ open: true,
642
+ submitAttempted: false,
643
+ formError: null,
644
+ draft: {
645
+ id: module.id,
646
+ section_id: module.section_id,
647
+ slug: module.slug,
648
+ title: module.title,
649
+ summary: module.summary,
650
+ module_mode: module.module_mode,
651
+ slugMode: module.slug === slugify(module.title) ? "derived" : "custom"
652
+ }
653
+ }),
654
+ children: config.labels.editAction
655
+ }
656
+ ),
657
+ module.active && !module.archived_at ? /* @__PURE__ */ jsxRuntime.jsx(
658
+ material.Button,
659
+ {
660
+ size: "small",
661
+ color: "error",
662
+ onClick: async () => {
663
+ const result = await editor.archiveModule(module.id);
664
+ if (!result.ok) {
665
+ const message = result.error ?? `Unable to archive ${config.naming.module.toLowerCase()}.`;
666
+ notify("error", message);
667
+ setError(message);
668
+ return;
669
+ }
670
+ notify("success", `${config.naming.module} archived.`);
671
+ },
672
+ children: config.labels.archiveAction
673
+ }
674
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
675
+ material.Button,
676
+ {
677
+ size: "small",
678
+ onClick: async () => {
679
+ const result = await editor.updateModule(module.id, { active: true });
680
+ if (!result.ok) {
681
+ const message = result.error ?? `Unable to restore ${config.naming.module.toLowerCase()}.`;
682
+ notify("error", message);
683
+ setError(message);
684
+ return;
685
+ }
686
+ notify("success", `${config.naming.module} restored.`);
687
+ },
688
+ children: config.labels.restoreAction
689
+ }
690
+ ),
691
+ /* @__PURE__ */ jsxRuntime.jsx(
692
+ material.Button,
693
+ {
694
+ size: "small",
695
+ variant: "contained",
696
+ onClick: () => setStageDialog({
697
+ open: true,
698
+ submitAttempted: false,
699
+ formError: null,
700
+ draft: { module_id: module.id, title: "", content_md: "" }
701
+ }),
702
+ children: config.labels.addStage
703
+ }
704
+ )
705
+ ] })
706
+ ] }),
707
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: module.summary }),
708
+ (module.stages ?? []).map((stage) => /* @__PURE__ */ jsxRuntime.jsx(material.Card, { variant: "outlined", children: /* @__PURE__ */ jsxRuntime.jsx(material.CardContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1, children: [
709
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: { xs: "column", sm: "row" }, justifyContent: "space-between", spacing: 1, children: [
710
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", spacing: 1, alignItems: "center", flexWrap: "wrap", children: [
711
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", children: stage.title }),
712
+ !stage.active ? /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { size: "small", color: "warning", label: "Archived" }) : null
713
+ ] }),
714
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", spacing: 1, flexWrap: "wrap", useFlexGap: true, children: [
715
+ /* @__PURE__ */ jsxRuntime.jsx(
716
+ material.Button,
717
+ {
718
+ size: "small",
719
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(EditIcon__default.default, {}),
720
+ onClick: () => setStageDialog({
721
+ open: true,
722
+ submitAttempted: false,
723
+ formError: null,
724
+ draft: {
725
+ id: stage.id,
726
+ module_id: stage.module_id,
727
+ title: stage.title,
728
+ content_md: stage.content_md
729
+ }
730
+ }),
731
+ children: config.labels.editAction
732
+ }
733
+ ),
734
+ stage.active ? /* @__PURE__ */ jsxRuntime.jsx(
735
+ material.Button,
736
+ {
737
+ size: "small",
738
+ color: "error",
739
+ onClick: async () => {
740
+ const result = await editor.archiveStage(stage.id);
741
+ if (!result.ok) {
742
+ const message = result.error ?? `Unable to archive ${config.naming.stage.toLowerCase()}.`;
743
+ notify("error", message);
744
+ setError(message);
745
+ return;
746
+ }
747
+ notify("success", `${config.naming.stage} archived.`);
748
+ },
749
+ children: config.labels.archiveAction
750
+ }
751
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
752
+ material.Button,
753
+ {
754
+ size: "small",
755
+ onClick: async () => {
756
+ const result = await editor.updateStage(stage.id, { active: true });
757
+ if (!result.ok) {
758
+ const message = result.error ?? `Unable to restore ${config.naming.stage.toLowerCase()}.`;
759
+ notify("error", message);
760
+ setError(message);
761
+ return;
762
+ }
763
+ notify("success", `${config.naming.stage} restored.`);
764
+ },
765
+ children: config.labels.restoreAction
766
+ }
767
+ ),
768
+ /* @__PURE__ */ jsxRuntime.jsx(
769
+ material.Button,
770
+ {
771
+ size: "small",
772
+ variant: "outlined",
773
+ onClick: () => setQuestionDialog({
774
+ open: true,
775
+ submitAttempted: false,
776
+ formError: null,
777
+ draft: { stage_id: stage.id, prompt: "", response_type: "long_text" }
778
+ }),
779
+ children: config.labels.addQuestion
780
+ }
781
+ )
782
+ ] })
783
+ ] }),
784
+ /* @__PURE__ */ jsxRuntime.jsx(
785
+ MDEditor3__default.default.Markdown,
786
+ {
787
+ source: stage.content_md || "_No stage content yet._",
788
+ style: { backgroundColor: "transparent", color: "inherit", padding: 0, margin: 0 }
789
+ }
790
+ ),
791
+ (stage.questions ?? []).map((question, questionIndex) => /* @__PURE__ */ jsxRuntime.jsxs(
792
+ material.Stack,
793
+ {
794
+ direction: { xs: "column", sm: "row" },
795
+ justifyContent: "space-between",
796
+ spacing: 1,
797
+ sx: { border: "1px solid", borderColor: "divider", borderRadius: 1, p: 1 },
798
+ children: [
799
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { children: [
800
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "body2", fontWeight: 600, children: [
801
+ questionIndex + 1,
802
+ ". ",
803
+ question.prompt
804
+ ] }),
805
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: question.response_type.replace("_", " ") })
806
+ ] }),
807
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", spacing: 1, flexWrap: "wrap", useFlexGap: true, children: [
808
+ /* @__PURE__ */ jsxRuntime.jsx(
809
+ material.Button,
810
+ {
811
+ size: "small",
812
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(EditIcon__default.default, {}),
813
+ onClick: () => setQuestionDialog({
814
+ open: true,
815
+ submitAttempted: false,
816
+ formError: null,
817
+ draft: {
818
+ id: question.id,
819
+ stage_id: question.stage_id,
820
+ prompt: question.prompt,
821
+ response_type: question.response_type
822
+ }
823
+ }),
824
+ children: config.labels.editAction
825
+ }
826
+ ),
827
+ question.active ? /* @__PURE__ */ jsxRuntime.jsx(
828
+ material.Button,
829
+ {
830
+ size: "small",
831
+ color: "error",
832
+ onClick: async () => {
833
+ const result = await editor.archiveQuestion(question.id);
834
+ if (!result.ok) {
835
+ const message = result.error ?? `Unable to archive ${config.naming.question.toLowerCase()}.`;
836
+ notify("error", message);
837
+ setError(message);
838
+ return;
839
+ }
840
+ notify("success", `${config.naming.question} archived.`);
841
+ },
842
+ children: config.labels.archiveAction
843
+ }
844
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
845
+ material.Button,
846
+ {
847
+ size: "small",
848
+ onClick: async () => {
849
+ const result = await editor.updateQuestion(question.id, { active: true });
850
+ if (!result.ok) {
851
+ const message = result.error ?? `Unable to restore ${config.naming.question.toLowerCase()}.`;
852
+ notify("error", message);
853
+ setError(message);
854
+ return;
855
+ }
856
+ notify("success", `${config.naming.question} restored.`);
857
+ },
858
+ children: config.labels.restoreAction
859
+ }
860
+ )
861
+ ] })
862
+ ]
863
+ },
864
+ question.id
865
+ ))
866
+ ] }) }) }, stage.id))
867
+ ] }) })
868
+ }
869
+ )
870
+ },
871
+ module.id
872
+ )),
873
+ dropProvided.placeholder
874
+ ] }) })
875
+ ] }) }) }, section.id)),
876
+ !sections.length ? /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "info", children: config.labels.noModulesMessage }) : null
877
+ ] }) }) }) })
878
+ ] }),
879
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Dialog, { open: sectionDialog.open, onClose: closeSectionDialog, fullWidth: true, maxWidth: "sm", children: [
880
+ /* @__PURE__ */ jsxRuntime.jsx(material.DialogTitle, { children: sectionDialog.draft.id ? `Edit ${config.naming.section}` : `Add ${config.naming.section}` }),
881
+ /* @__PURE__ */ jsxRuntime.jsx(material.DialogContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 2, sx: { mt: 1 }, children: [
882
+ sectionDialog.formError ? /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "error", children: sectionDialog.formError }) : null,
883
+ /* @__PURE__ */ jsxRuntime.jsx(
884
+ material.TextField,
885
+ {
886
+ label: "Title",
887
+ value: sectionDialog.draft.title,
888
+ disabled: editor.isSaving,
889
+ error: sectionDialog.submitAttempted && !sectionDialog.draft.title.trim(),
890
+ helperText: sectionDialog.submitAttempted && !sectionDialog.draft.title.trim() ? config.validation.requiredTitleMessage : void 0,
891
+ onChange: (event) => setSectionDialog((prev) => {
892
+ const nextTitle = event.target.value;
893
+ return {
894
+ ...prev,
895
+ formError: null,
896
+ draft: {
897
+ ...prev.draft,
898
+ title: nextTitle,
899
+ slug: prev.draft.slugMode === "derived" ? slugify(nextTitle) : prev.draft.slug
900
+ }
901
+ };
902
+ })
903
+ }
904
+ ),
905
+ /* @__PURE__ */ jsxRuntime.jsx(
906
+ material.TextField,
907
+ {
908
+ label: "Slug",
909
+ value: sectionDialog.draft.slugMode === "derived" ? slugify(sectionDialog.draft.title) : sectionDialog.draft.slug,
910
+ disabled: editor.isSaving || sectionDialog.draft.slugMode === "derived",
911
+ error: sectionDialog.draft.slugMode === "custom" && Boolean(sectionCustomSlugError),
912
+ helperText: sectionDialog.draft.slugMode === "derived" ? "Derived from title. Switch to custom mode to edit." : sectionCustomSlugError ?? config.validation.slugPatternMessage,
913
+ onChange: (event) => setSectionDialog((prev) => ({
914
+ ...prev,
915
+ formError: null,
916
+ draft: { ...prev.draft, slug: event.target.value }
917
+ }))
918
+ }
919
+ ),
920
+ /* @__PURE__ */ jsxRuntime.jsx(
921
+ material.FormControlLabel,
922
+ {
923
+ control: /* @__PURE__ */ jsxRuntime.jsx(
924
+ material.Switch,
925
+ {
926
+ checked: sectionDialog.draft.slugMode === "custom",
927
+ onChange: (_event, checked) => setSectionDialog((prev) => ({
928
+ ...prev,
929
+ formError: null,
930
+ draft: {
931
+ ...prev.draft,
932
+ slugMode: checked ? "custom" : "derived",
933
+ slug: checked && !prev.draft.slug.trim() ? slugify(prev.draft.title) : prev.draft.slug
934
+ }
935
+ }))
936
+ }
937
+ ),
938
+ label: "Use custom slug"
939
+ }
940
+ ),
941
+ /* @__PURE__ */ jsxRuntime.jsx(
942
+ material.TextField,
943
+ {
944
+ label: "Description",
945
+ multiline: true,
946
+ minRows: 3,
947
+ value: sectionDialog.draft.description,
948
+ disabled: editor.isSaving,
949
+ onChange: (event) => setSectionDialog((prev) => ({
950
+ ...prev,
951
+ formError: null,
952
+ draft: { ...prev.draft, description: event.target.value }
953
+ }))
954
+ }
955
+ )
956
+ ] }) }),
957
+ /* @__PURE__ */ jsxRuntime.jsxs(material.DialogActions, { children: [
958
+ /* @__PURE__ */ jsxRuntime.jsx(material.Button, { onClick: closeSectionDialog, disabled: editor.isSaving, children: config.labels.cancelAction }),
959
+ /* @__PURE__ */ jsxRuntime.jsx(material.Button, { variant: "contained", disabled: editor.isSaving, onClick: () => void saveSection(), children: config.labels.saveAction })
960
+ ] })
961
+ ] }),
962
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Dialog, { open: moduleDialog.open, onClose: closeModuleDialog, fullWidth: true, maxWidth: "sm", children: [
963
+ /* @__PURE__ */ jsxRuntime.jsx(material.DialogTitle, { children: moduleDialog.draft.id ? `Edit ${config.naming.module}` : `Add ${config.naming.module}` }),
964
+ /* @__PURE__ */ jsxRuntime.jsx(material.DialogContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 2, sx: { mt: 1 }, children: [
965
+ moduleDialog.formError ? /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "error", children: moduleDialog.formError }) : null,
966
+ /* @__PURE__ */ jsxRuntime.jsx(
967
+ material.TextField,
968
+ {
969
+ select: true,
970
+ label: config.naming.section,
971
+ value: moduleDialog.draft.section_id,
972
+ disabled: editor.isSaving,
973
+ error: moduleDialog.submitAttempted && !moduleDialog.draft.section_id,
974
+ helperText: moduleDialog.submitAttempted && !moduleDialog.draft.section_id ? `${config.naming.section} is required.` : void 0,
975
+ onChange: (event) => setModuleDialog((prev) => ({
976
+ ...prev,
977
+ formError: null,
978
+ draft: { ...prev.draft, section_id: event.target.value }
979
+ })),
980
+ children: sections.map((section) => /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: section.id, children: section.title }, section.id))
981
+ }
982
+ ),
983
+ /* @__PURE__ */ jsxRuntime.jsx(
984
+ material.TextField,
985
+ {
986
+ label: "Title",
987
+ value: moduleDialog.draft.title,
988
+ disabled: editor.isSaving,
989
+ error: moduleDialog.submitAttempted && !moduleDialog.draft.title.trim(),
990
+ helperText: moduleDialog.submitAttempted && !moduleDialog.draft.title.trim() ? config.validation.requiredTitleMessage : void 0,
991
+ onChange: (event) => setModuleDialog((prev) => {
992
+ const nextTitle = event.target.value;
993
+ return {
994
+ ...prev,
995
+ formError: null,
996
+ draft: {
997
+ ...prev.draft,
998
+ title: nextTitle,
999
+ slug: prev.draft.slugMode === "derived" ? slugify(nextTitle) : prev.draft.slug
1000
+ }
1001
+ };
1002
+ })
1003
+ }
1004
+ ),
1005
+ /* @__PURE__ */ jsxRuntime.jsx(
1006
+ material.TextField,
1007
+ {
1008
+ label: "Slug",
1009
+ value: moduleDialog.draft.slugMode === "derived" ? slugify(moduleDialog.draft.title) : moduleDialog.draft.slug,
1010
+ disabled: editor.isSaving || moduleDialog.draft.slugMode === "derived",
1011
+ error: moduleDialog.draft.slugMode === "custom" && Boolean(moduleCustomSlugError),
1012
+ helperText: moduleDialog.draft.slugMode === "derived" ? "Derived from title. Switch to custom mode to edit." : moduleCustomSlugError ?? config.validation.slugPatternMessage,
1013
+ onChange: (event) => setModuleDialog((prev) => ({
1014
+ ...prev,
1015
+ formError: null,
1016
+ draft: { ...prev.draft, slug: event.target.value }
1017
+ }))
1018
+ }
1019
+ ),
1020
+ /* @__PURE__ */ jsxRuntime.jsx(
1021
+ material.FormControlLabel,
1022
+ {
1023
+ control: /* @__PURE__ */ jsxRuntime.jsx(
1024
+ material.Switch,
1025
+ {
1026
+ checked: moduleDialog.draft.slugMode === "custom",
1027
+ onChange: (_event, checked) => setModuleDialog((prev) => ({
1028
+ ...prev,
1029
+ formError: null,
1030
+ draft: {
1031
+ ...prev.draft,
1032
+ slugMode: checked ? "custom" : "derived",
1033
+ slug: checked && !prev.draft.slug.trim() ? slugify(prev.draft.title) : prev.draft.slug
1034
+ }
1035
+ }))
1036
+ }
1037
+ ),
1038
+ label: "Use custom slug"
1039
+ }
1040
+ ),
1041
+ /* @__PURE__ */ jsxRuntime.jsx(
1042
+ material.TextField,
1043
+ {
1044
+ label: "Summary",
1045
+ multiline: true,
1046
+ minRows: 2,
1047
+ value: moduleDialog.draft.summary,
1048
+ disabled: editor.isSaving,
1049
+ error: moduleDialog.submitAttempted && !moduleDialog.draft.summary.trim(),
1050
+ helperText: moduleDialog.submitAttempted && !moduleDialog.draft.summary.trim() ? config.validation.requiredSummaryMessage : void 0,
1051
+ onChange: (event) => setModuleDialog((prev) => ({
1052
+ ...prev,
1053
+ formError: null,
1054
+ draft: { ...prev.draft, summary: event.target.value }
1055
+ }))
1056
+ }
1057
+ ),
1058
+ /* @__PURE__ */ jsxRuntime.jsxs(
1059
+ material.TextField,
1060
+ {
1061
+ select: true,
1062
+ label: "Module mode",
1063
+ value: moduleDialog.draft.module_mode,
1064
+ disabled: editor.isSaving,
1065
+ onChange: (event) => setModuleDialog((prev) => ({
1066
+ ...prev,
1067
+ formError: null,
1068
+ draft: { ...prev.draft, module_mode: event.target.value }
1069
+ })),
1070
+ children: [
1071
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "single_stage", children: "Single stage" }),
1072
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "multi_stage", children: "Multi stage" })
1073
+ ]
1074
+ }
1075
+ )
1076
+ ] }) }),
1077
+ /* @__PURE__ */ jsxRuntime.jsxs(material.DialogActions, { children: [
1078
+ /* @__PURE__ */ jsxRuntime.jsx(material.Button, { onClick: closeModuleDialog, disabled: editor.isSaving, children: config.labels.cancelAction }),
1079
+ /* @__PURE__ */ jsxRuntime.jsx(material.Button, { variant: "contained", disabled: editor.isSaving, onClick: () => void saveModule(), children: config.labels.saveAction })
1080
+ ] })
1081
+ ] }),
1082
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Dialog, { open: stageDialog.open, onClose: closeStageDialog, fullWidth: true, maxWidth: "md", children: [
1083
+ /* @__PURE__ */ jsxRuntime.jsx(material.DialogTitle, { children: stageDialog.draft.id ? `Edit ${config.naming.stage}` : `Add ${config.naming.stage}` }),
1084
+ /* @__PURE__ */ jsxRuntime.jsx(material.DialogContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 2, sx: { mt: 1 }, children: [
1085
+ stageDialog.formError ? /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "error", children: stageDialog.formError }) : null,
1086
+ /* @__PURE__ */ jsxRuntime.jsx(
1087
+ material.TextField,
1088
+ {
1089
+ label: "Title",
1090
+ value: stageDialog.draft.title,
1091
+ disabled: editor.isSaving,
1092
+ error: stageDialog.submitAttempted && !stageDialog.draft.title.trim(),
1093
+ helperText: stageDialog.submitAttempted && !stageDialog.draft.title.trim() ? config.validation.requiredTitleMessage : void 0,
1094
+ onChange: (event) => setStageDialog((prev) => ({
1095
+ ...prev,
1096
+ formError: null,
1097
+ draft: { ...prev.draft, title: event.target.value }
1098
+ }))
1099
+ }
1100
+ ),
1101
+ /* @__PURE__ */ jsxRuntime.jsx(
1102
+ MDEditor3__default.default,
1103
+ {
1104
+ value: stageDialog.draft.content_md,
1105
+ onChange: (value) => setStageDialog((prev) => ({
1106
+ ...prev,
1107
+ formError: null,
1108
+ draft: { ...prev.draft, content_md: value ?? "" }
1109
+ })),
1110
+ preview: "edit",
1111
+ height: 320
1112
+ }
1113
+ )
1114
+ ] }) }),
1115
+ /* @__PURE__ */ jsxRuntime.jsxs(material.DialogActions, { children: [
1116
+ /* @__PURE__ */ jsxRuntime.jsx(material.Button, { onClick: closeStageDialog, disabled: editor.isSaving, children: config.labels.cancelAction }),
1117
+ /* @__PURE__ */ jsxRuntime.jsx(material.Button, { variant: "contained", disabled: editor.isSaving, onClick: () => void saveStage(), children: config.labels.saveAction })
1118
+ ] })
1119
+ ] }),
1120
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Dialog, { open: questionDialog.open, onClose: closeQuestionDialog, fullWidth: true, maxWidth: "sm", children: [
1121
+ /* @__PURE__ */ jsxRuntime.jsx(material.DialogTitle, { children: questionDialog.draft.id ? `Edit ${config.naming.question}` : `Add ${config.naming.question}` }),
1122
+ /* @__PURE__ */ jsxRuntime.jsx(material.DialogContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 2, sx: { mt: 1 }, children: [
1123
+ questionDialog.formError ? /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "error", children: questionDialog.formError }) : null,
1124
+ /* @__PURE__ */ jsxRuntime.jsx(
1125
+ material.TextField,
1126
+ {
1127
+ label: "Prompt",
1128
+ multiline: true,
1129
+ minRows: 3,
1130
+ value: questionDialog.draft.prompt,
1131
+ disabled: editor.isSaving,
1132
+ error: questionDialog.submitAttempted && !questionDialog.draft.prompt.trim(),
1133
+ helperText: questionDialog.submitAttempted && !questionDialog.draft.prompt.trim() ? config.validation.requiredPromptMessage : void 0,
1134
+ onChange: (event) => setQuestionDialog((prev) => ({
1135
+ ...prev,
1136
+ formError: null,
1137
+ draft: { ...prev.draft, prompt: event.target.value }
1138
+ }))
1139
+ }
1140
+ ),
1141
+ /* @__PURE__ */ jsxRuntime.jsxs(
1142
+ material.TextField,
1143
+ {
1144
+ select: true,
1145
+ label: "Response type",
1146
+ value: questionDialog.draft.response_type,
1147
+ disabled: editor.isSaving,
1148
+ onChange: (event) => setQuestionDialog((prev) => ({
1149
+ ...prev,
1150
+ formError: null,
1151
+ draft: { ...prev.draft, response_type: event.target.value }
1152
+ })),
1153
+ children: [
1154
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "short_text", children: "Short text" }),
1155
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "long_text", children: "Long text" }),
1156
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "checklist", children: "Checklist" })
1157
+ ]
1158
+ }
1159
+ )
1160
+ ] }) }),
1161
+ /* @__PURE__ */ jsxRuntime.jsxs(material.DialogActions, { children: [
1162
+ /* @__PURE__ */ jsxRuntime.jsx(material.Button, { onClick: closeQuestionDialog, disabled: editor.isSaving, children: config.labels.cancelAction }),
1163
+ /* @__PURE__ */ jsxRuntime.jsx(material.Button, { variant: "contained", disabled: editor.isSaving, onClick: () => void saveQuestion(), children: config.labels.saveAction })
1164
+ ] })
1165
+ ] })
1166
+ ] });
1167
+ };
1168
+ var statusLabel = (status) => {
1169
+ if (status === "in_progress") return "In progress";
1170
+ if (status === "submitted") return "Submitted";
1171
+ if (status === "passed") return "Passed";
1172
+ if (status === "revision_required") return "Revision required";
1173
+ return "Pending";
1174
+ };
1175
+ var statusColor = (status) => {
1176
+ if (status === "in_progress") return "warning";
1177
+ if (status === "submitted") return "info";
1178
+ if (status === "passed") return "success";
1179
+ if (status === "revision_required") return "warning";
1180
+ return "default";
1181
+ };
1182
+ var normalizeAnswer = (value) => (value ?? "").trim();
1183
+ var HandbookLmsReviewWorkspace = ({
1184
+ adapter,
1185
+ courseSlug,
1186
+ config,
1187
+ title,
1188
+ onBack,
1189
+ notifications,
1190
+ enabled = true
1191
+ }) => {
1192
+ const review = useLmsReviewWorkspace({ adapter, courseSlug, enabled });
1193
+ const notify = (severity, message) => {
1194
+ notifications?.notify?.({ severity, message });
1195
+ if (severity === "success") notifications?.success?.(message);
1196
+ if (severity === "error") notifications?.error?.(message);
1197
+ if (severity === "info") notifications?.info?.(message);
1198
+ if (severity === "warning") notifications?.warning?.(message);
1199
+ };
1200
+ const [selectedModuleId, setSelectedModuleId] = react.useState("");
1201
+ const [reviewDecision, setReviewDecision] = react.useState(null);
1202
+ const [passFeedback, setPassFeedback] = react.useState("");
1203
+ const [followUpFeedback, setFollowUpFeedback] = react.useState("");
1204
+ const [followUpPrompt, setFollowUpPrompt] = react.useState("");
1205
+ const [selectedFollowUpQuestionIds, setSelectedFollowUpQuestionIds] = react.useState([]);
1206
+ const [statusOverrideTarget, setStatusOverrideTarget] = react.useState("submitted");
1207
+ const [statusOverrideNote, setStatusOverrideNote] = react.useState("");
1208
+ const [followUpHistoryFilter, setFollowUpHistoryFilter] = react.useState("all");
1209
+ const [expandedFollowUpHistoryId, setExpandedFollowUpHistoryId] = react.useState(false);
1210
+ const learnerOptions = react.useMemo(
1211
+ () => [...review.learners].sort((a, b) => (a.email ?? a.id).localeCompare(b.email ?? b.id)),
1212
+ [review.learners]
1213
+ );
1214
+ react.useEffect(() => {
1215
+ if (review.selectedLearnerId) return;
1216
+ const firstQueuedLearner = review.summary?.review_queue[0]?.learner_user_id ?? "";
1217
+ if (firstQueuedLearner) {
1218
+ review.setSelectedLearnerId(firstQueuedLearner);
1219
+ return;
1220
+ }
1221
+ if (learnerOptions[0]) {
1222
+ review.setSelectedLearnerId(learnerOptions[0].id);
1223
+ }
1224
+ }, [learnerOptions, review]);
1225
+ const records = react.useMemo(() => review.summary?.records ?? [], [review.summary?.records]);
1226
+ const sections = react.useMemo(() => {
1227
+ const sectionMap = /* @__PURE__ */ new Map();
1228
+ for (const record of records) {
1229
+ const section = record.module.section;
1230
+ const sectionId = section?.id ?? "uncategorized";
1231
+ const sectionTitle = section?.title ?? "Uncategorized";
1232
+ const displayOrder = section?.display_order ?? 999;
1233
+ const current = sectionMap.get(sectionId) ?? {
1234
+ id: sectionId,
1235
+ title: sectionTitle,
1236
+ display_order: displayOrder,
1237
+ records: []
1238
+ };
1239
+ current.records.push(record);
1240
+ sectionMap.set(sectionId, current);
1241
+ }
1242
+ return Array.from(sectionMap.values()).map((section) => ({
1243
+ ...section,
1244
+ records: section.records.sort((a, b) => a.module.display_order - b.module.display_order)
1245
+ })).sort((a, b) => a.display_order - b.display_order);
1246
+ }, [records]);
1247
+ react.useEffect(() => {
1248
+ if (!records.length) {
1249
+ setSelectedModuleId("");
1250
+ return;
1251
+ }
1252
+ setSelectedModuleId((prev) => prev && records.some((record) => record.module.id === prev) ? prev : records[0].module.id);
1253
+ }, [records]);
1254
+ const selectedRecord = records.find((record) => record.module.id === selectedModuleId) ?? null;
1255
+ const selectedQuestions = react.useMemo(
1256
+ () => (selectedRecord?.module.stages ?? []).flatMap((stage) => stage.questions ?? []),
1257
+ [selectedRecord]
1258
+ );
1259
+ const selectedQuestionById = react.useMemo(
1260
+ () => new Map(selectedQuestions.map((question) => [question.id, question])),
1261
+ [selectedQuestions]
1262
+ );
1263
+ const selectedResponseByQuestionId = react.useMemo(
1264
+ () => new Map((selectedRecord?.responses ?? []).map((response) => [response.question_id, response])),
1265
+ [selectedRecord?.responses]
1266
+ );
1267
+ const submittedQueueItem = review.summary?.review_queue.find(
1268
+ (item) => item.learner_user_id === review.selectedLearnerId && item.module_id === selectedRecord?.module.id
1269
+ ) ?? null;
1270
+ const canReviewSubmittedModule = Boolean(submittedQueueItem) || selectedRecord?.progress.status === "submitted";
1271
+ const followUpHistory = react.useMemo(
1272
+ () => [...selectedRecord?.assignments ?? []].sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()),
1273
+ [selectedRecord?.assignments]
1274
+ );
1275
+ const activeFollowUpHistoryItem = react.useMemo(() => {
1276
+ const pending = followUpHistory.filter((assignment) => assignment.status === "pending");
1277
+ return pending[pending.length - 1] ?? followUpHistory[followUpHistory.length - 1] ?? null;
1278
+ }, [followUpHistory]);
1279
+ const reviewerActivity = react.useMemo(() => {
1280
+ if (!selectedRecord) return [];
1281
+ const activity = [];
1282
+ if (selectedRecord.progress.submitted_at) {
1283
+ activity.push({ at: selectedRecord.progress.submitted_at, label: "Module submitted for review" });
1284
+ }
1285
+ if (selectedRecord.progress.reviewed_at) {
1286
+ activity.push({ at: selectedRecord.progress.reviewed_at, label: `Last review decision: ${statusLabel(selectedRecord.progress.status)}` });
1287
+ }
1288
+ for (const assignment of selectedRecord.assignments ?? []) {
1289
+ activity.push({ at: assignment.created_at, label: "Follow-up assigned" });
1290
+ if (assignment.submitted_at) activity.push({ at: assignment.submitted_at, label: "Follow-up submitted by learner" });
1291
+ if (assignment.completed_at) activity.push({ at: assignment.completed_at, label: "Follow-up completed" });
1292
+ }
1293
+ return activity.sort((a, b) => new Date(b.at).getTime() - new Date(a.at).getTime()).slice(0, 8);
1294
+ }, [selectedRecord]);
1295
+ react.useEffect(() => {
1296
+ setReviewDecision(null);
1297
+ setPassFeedback("");
1298
+ setFollowUpFeedback("");
1299
+ setFollowUpPrompt("");
1300
+ setSelectedFollowUpQuestionIds([]);
1301
+ setStatusOverrideNote("");
1302
+ setFollowUpHistoryFilter("all");
1303
+ setStatusOverrideTarget(selectedRecord?.progress.status ?? "submitted");
1304
+ }, [selectedRecord?.module.id, selectedRecord?.progress.status]);
1305
+ react.useEffect(() => {
1306
+ if (!followUpHistory.length) {
1307
+ setExpandedFollowUpHistoryId(false);
1308
+ return;
1309
+ }
1310
+ setExpandedFollowUpHistoryId((prev) => {
1311
+ if (prev && followUpHistory.some((assignment) => assignment.id === prev)) return prev;
1312
+ return activeFollowUpHistoryItem?.id ?? false;
1313
+ });
1314
+ }, [activeFollowUpHistoryItem?.id, followUpHistory]);
1315
+ const hasReviewDraft = Boolean(passFeedback.trim()) || Boolean(followUpFeedback.trim()) || Boolean(followUpPrompt.trim()) || selectedFollowUpQuestionIds.length > 0;
1316
+ const clearReviewDraft = () => {
1317
+ setReviewDecision(null);
1318
+ setPassFeedback("");
1319
+ setFollowUpFeedback("");
1320
+ setFollowUpPrompt("");
1321
+ setSelectedFollowUpQuestionIds([]);
1322
+ };
1323
+ const confirmDiscardReviewDraft = () => {
1324
+ if (!hasReviewDraft) return true;
1325
+ return window.confirm("You have unsent review inputs. Leave this module and discard them?");
1326
+ };
1327
+ const handlePassDecision = async () => {
1328
+ if (!selectedRecord || !review.selectedLearnerId) return;
1329
+ const result = await review.reviewModule({
1330
+ moduleId: selectedRecord.module.id,
1331
+ learner_user_id: review.selectedLearnerId,
1332
+ status: "passed",
1333
+ review_feedback_md: passFeedback.trim() || void 0,
1334
+ assignment: null
1335
+ });
1336
+ if (result.ok) {
1337
+ clearReviewDraft();
1338
+ notify("success", "Marked as passed.");
1339
+ return;
1340
+ }
1341
+ notify("error", result.error ?? "Unable to submit review decision.");
1342
+ };
1343
+ const handleAssignFollowUpDecision = async () => {
1344
+ if (!selectedRecord || !review.selectedLearnerId) return;
1345
+ const hasFollowUpContent = Boolean(followUpPrompt.trim()) || Boolean(followUpFeedback.trim()) || selectedFollowUpQuestionIds.length > 0;
1346
+ if (!hasFollowUpContent) return;
1347
+ const result = await review.reviewModule({
1348
+ moduleId: selectedRecord.module.id,
1349
+ learner_user_id: review.selectedLearnerId,
1350
+ status: "revision_required",
1351
+ review_feedback_md: followUpFeedback.trim() || void 0,
1352
+ assignment: {
1353
+ prompt: followUpPrompt.trim() || void 0,
1354
+ response_mode: "written",
1355
+ reviewer_note_md: followUpFeedback.trim() || null,
1356
+ target_question_ids: selectedFollowUpQuestionIds
1357
+ }
1358
+ });
1359
+ if (result.ok) {
1360
+ clearReviewDraft();
1361
+ notify("success", "Follow-up assigned.");
1362
+ return;
1363
+ }
1364
+ notify("error", result.error ?? "Unable to assign follow-up.");
1365
+ };
1366
+ const handleApplyStatusOverride = async () => {
1367
+ if (!selectedRecord || !review.selectedLearnerId) return;
1368
+ if (statusOverrideTarget === selectedRecord.progress.status) return;
1369
+ if (statusOverrideTarget === "pending") {
1370
+ const confirmed = window.confirm("Reset this module to Pending? This removes current review state.");
1371
+ if (!confirmed) return;
1372
+ }
1373
+ const result = await review.reviewModule({
1374
+ moduleId: selectedRecord.module.id,
1375
+ learner_user_id: review.selectedLearnerId,
1376
+ status: statusOverrideTarget,
1377
+ review_feedback_md: statusOverrideNote.trim() || void 0,
1378
+ assignment: statusOverrideTarget === "revision_required" ? {
1379
+ prompt: void 0,
1380
+ response_mode: "written",
1381
+ reviewer_note_md: statusOverrideNote.trim() || null,
1382
+ target_question_ids: []
1383
+ } : null
1384
+ });
1385
+ if (result.ok) {
1386
+ setStatusOverrideNote("");
1387
+ notify("success", "Status updated.");
1388
+ return;
1389
+ }
1390
+ notify("error", result.error ?? "Unable to apply status update.");
1391
+ };
1392
+ const renderQuestionResponse = (question, response) => /* @__PURE__ */ jsxRuntime.jsx(material.Card, { variant: "outlined", children: /* @__PURE__ */ jsxRuntime.jsxs(material.CardContent, { children: [
1393
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", fontWeight: 600, sx: { mb: 1 }, children: question.prompt }),
1394
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "caption", color: "text.secondary", children: [
1395
+ "Response type: ",
1396
+ question.response_type.replace("_", " ")
1397
+ ] }),
1398
+ /* @__PURE__ */ jsxRuntime.jsx(material.Stack, { sx: { mt: 1 }, children: response?.response_md ? /* @__PURE__ */ jsxRuntime.jsx(
1399
+ MDEditor3__default.default.Markdown,
1400
+ {
1401
+ source: response.response_md,
1402
+ style: { backgroundColor: "transparent", color: "inherit", padding: 0, margin: 0 }
1403
+ }
1404
+ ) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "No response submitted yet." }) })
1405
+ ] }) }, question.id);
1406
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 2, children: [
1407
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", useFlexGap: true, children: [
1408
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h5", children: title ?? config.labels.reviewTitle }),
1409
+ onBack ? /* @__PURE__ */ jsxRuntime.jsx(material.Button, { onClick: onBack, children: config.labels.backButton }) : null
1410
+ ] }),
1411
+ review.error ? /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "error", children: review.error }) : null,
1412
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Card, { children: [
1413
+ /* @__PURE__ */ jsxRuntime.jsx(material.CardHeader, { title: `Select ${config.naming.learner}` }),
1414
+ /* @__PURE__ */ jsxRuntime.jsx(material.CardContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Grid, { container: true, spacing: 2, children: [
1415
+ /* @__PURE__ */ jsxRuntime.jsx(material.Grid, { size: { xs: 12, md: 6 }, children: /* @__PURE__ */ jsxRuntime.jsx(
1416
+ material.TextField,
1417
+ {
1418
+ select: true,
1419
+ label: config.naming.learner,
1420
+ value: review.selectedLearnerId,
1421
+ onChange: (event) => {
1422
+ if (event.target.value === review.selectedLearnerId) return;
1423
+ if (!confirmDiscardReviewDraft()) return;
1424
+ review.setSelectedLearnerId(event.target.value);
1425
+ },
1426
+ fullWidth: true,
1427
+ children: learnerOptions.map((learner) => /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: learner.id, children: learner.name || learner.email || learner.id }, learner.id))
1428
+ }
1429
+ ) }),
1430
+ /* @__PURE__ */ jsxRuntime.jsx(material.Grid, { size: { xs: 12, md: 6 }, children: /* @__PURE__ */ jsxRuntime.jsxs(material.Alert, { severity: "info", children: [
1431
+ "Review queue items: ",
1432
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: review.summary?.review_queue.length ?? 0 })
1433
+ ] }) })
1434
+ ] }) })
1435
+ ] }),
1436
+ !sections.length ? /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "info", children: "No modules found for this learner." }) : /* @__PURE__ */ jsxRuntime.jsxs(material.Grid, { container: true, spacing: 2, children: [
1437
+ /* @__PURE__ */ jsxRuntime.jsx(material.Grid, { size: { xs: 12, md: 4 }, children: /* @__PURE__ */ jsxRuntime.jsx(material.Card, { children: /* @__PURE__ */ jsxRuntime.jsx(material.CardContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1, children: [
1438
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "h6", children: [
1439
+ config.naming.module,
1440
+ "s"
1441
+ ] }),
1442
+ sections.map((section) => {
1443
+ const completed = section.records.filter((record) => record.progress.status === "passed").length;
1444
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Accordion, { defaultExpanded: true, children: [
1445
+ /* @__PURE__ */ jsxRuntime.jsx(material.AccordionSummary, { expandIcon: /* @__PURE__ */ jsxRuntime.jsx(ExpandMoreIcon__default.default, {}), children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", spacing: 1, alignItems: "center", justifyContent: "space-between", sx: { width: "100%" }, children: [
1446
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", children: section.title }),
1447
+ /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { size: "small", label: `${completed}/${section.records.length}` })
1448
+ ] }) }),
1449
+ /* @__PURE__ */ jsxRuntime.jsx(material.AccordionDetails, { sx: { pt: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx(material.List, { disablePadding: true, children: section.records.map((record) => /* @__PURE__ */ jsxRuntime.jsxs(
1450
+ material.ListItemButton,
1451
+ {
1452
+ selected: record.module.id === selectedModuleId,
1453
+ onClick: () => {
1454
+ if (record.module.id === selectedModuleId) return;
1455
+ if (!confirmDiscardReviewDraft()) return;
1456
+ setSelectedModuleId(record.module.id);
1457
+ },
1458
+ children: [
1459
+ /* @__PURE__ */ jsxRuntime.jsx(material.ListItemText, { primary: record.module.title }),
1460
+ /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { size: "small", color: statusColor(record.progress.status), label: statusLabel(record.progress.status) })
1461
+ ]
1462
+ },
1463
+ record.module.id
1464
+ )) }) })
1465
+ ] }, section.id);
1466
+ })
1467
+ ] }) }) }) }),
1468
+ /* @__PURE__ */ jsxRuntime.jsx(material.Grid, { size: { xs: 12, md: 8 }, children: !selectedRecord ? /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "info", children: "Select a learner module to review answers." }) : /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 2, children: [
1469
+ /* @__PURE__ */ jsxRuntime.jsx(material.Card, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.CardContent, { children: [
1470
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: { xs: "column", sm: "row" }, justifyContent: "space-between", spacing: 1, sx: { mb: 1 }, children: [
1471
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", children: selectedRecord.module.title }),
1472
+ /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { size: "small", color: statusColor(selectedRecord.progress.status), label: statusLabel(selectedRecord.progress.status) })
1473
+ ] }),
1474
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", sx: { mb: 1.5 }, children: selectedRecord.module.summary }),
1475
+ /* @__PURE__ */ jsxRuntime.jsx(material.Stack, { spacing: 2, children: (selectedRecord.module.stages ?? []).map((stage) => /* @__PURE__ */ jsxRuntime.jsx(material.Card, { variant: "outlined", children: /* @__PURE__ */ jsxRuntime.jsx(material.CardContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1.5, children: [
1476
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", fontWeight: 600, children: stage.title }),
1477
+ /* @__PURE__ */ jsxRuntime.jsx(
1478
+ MDEditor3__default.default.Markdown,
1479
+ {
1480
+ source: stage.content_md || "",
1481
+ style: { backgroundColor: "transparent", color: "inherit", padding: 0, margin: 0 }
1482
+ }
1483
+ ),
1484
+ (stage.questions ?? []).length ? /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1, children: [
1485
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", fontWeight: 600, children: "Learner answers" }),
1486
+ (stage.questions ?? []).map(
1487
+ (question) => renderQuestionResponse(question, selectedResponseByQuestionId.get(question.id))
1488
+ )
1489
+ ] }) : null
1490
+ ] }) }) }, stage.id)) }),
1491
+ selectedRecord.progress.review_feedback_md ? /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: selectedRecord.progress.status === "passed" ? "success" : "warning", sx: { mt: 2 }, children: selectedRecord.progress.review_feedback_md }) : null
1492
+ ] }) }),
1493
+ followUpHistory.length ? /* @__PURE__ */ jsxRuntime.jsx(material.Card, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.CardContent, { children: [
1494
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: { xs: "column", sm: "row" }, justifyContent: "space-between", spacing: 1, sx: { mb: 1 }, children: [
1495
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle1", fontWeight: 600, children: "Follow-up history" }),
1496
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", spacing: 0.75, children: [
1497
+ /* @__PURE__ */ jsxRuntime.jsx(
1498
+ material.Chip,
1499
+ {
1500
+ size: "small",
1501
+ label: "All",
1502
+ color: followUpHistoryFilter === "all" ? "primary" : "default",
1503
+ variant: followUpHistoryFilter === "all" ? "filled" : "outlined",
1504
+ onClick: () => setFollowUpHistoryFilter("all")
1505
+ }
1506
+ ),
1507
+ /* @__PURE__ */ jsxRuntime.jsx(
1508
+ material.Chip,
1509
+ {
1510
+ size: "small",
1511
+ label: "Changed only",
1512
+ color: followUpHistoryFilter === "changed" ? "primary" : "default",
1513
+ variant: followUpHistoryFilter === "changed" ? "filled" : "outlined",
1514
+ onClick: () => setFollowUpHistoryFilter("changed")
1515
+ }
1516
+ )
1517
+ ] })
1518
+ ] }),
1519
+ /* @__PURE__ */ jsxRuntime.jsx(material.Stack, { spacing: 1, children: followUpHistory.map((assignment, assignmentIndex) => {
1520
+ const targetedUpdates = (assignment.target_question_ids ?? []).map((questionId) => {
1521
+ const question = selectedQuestionById.get(questionId);
1522
+ const previousAnswer = assignment.target_original_answers_json?.[questionId] ?? "";
1523
+ const updatedAnswer = assignment.target_updated_answers_json?.[questionId] ?? selectedResponseByQuestionId.get(questionId)?.response_md ?? "";
1524
+ const changed = normalizeAnswer(previousAnswer) !== normalizeAnswer(updatedAnswer);
1525
+ return { questionId, question, previousAnswer, updatedAnswer, changed };
1526
+ });
1527
+ const filteredTargetedUpdates = targetedUpdates.filter(
1528
+ (item) => followUpHistoryFilter === "changed" ? item.changed : true
1529
+ );
1530
+ const changedCount = targetedUpdates.filter((item) => item.changed).length;
1531
+ const unchangedCount = Math.max(targetedUpdates.length - changedCount, 0);
1532
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1533
+ material.Accordion,
1534
+ {
1535
+ disableGutters: true,
1536
+ expanded: expandedFollowUpHistoryId === assignment.id,
1537
+ onChange: (_event, expanded) => setExpandedFollowUpHistoryId(expanded ? assignment.id : false),
1538
+ sx: { border: "1px solid", borderColor: "divider", borderRadius: 1, overflow: "hidden", "&:before": { display: "none" } },
1539
+ children: [
1540
+ /* @__PURE__ */ jsxRuntime.jsx(material.AccordionSummary, { expandIcon: /* @__PURE__ */ jsxRuntime.jsx(ExpandMoreIcon__default.default, {}), children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: { xs: "column", sm: "row" }, justifyContent: "space-between", spacing: 1, sx: { width: "100%" }, children: [
1541
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "subtitle2", fontWeight: 700, children: [
1542
+ "Follow-up ",
1543
+ assignmentIndex + 1
1544
+ ] }),
1545
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", spacing: 0.75, flexWrap: "wrap", children: [
1546
+ /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { size: "small", label: assignment.status.replace("_", " ") }),
1547
+ targetedUpdates.length ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1548
+ /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { size: "small", color: "success", label: `Changed ${changedCount}` }),
1549
+ /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { size: "small", variant: "outlined", label: `Unchanged ${unchangedCount}` })
1550
+ ] }) : null
1551
+ ] })
1552
+ ] }) }),
1553
+ /* @__PURE__ */ jsxRuntime.jsx(material.AccordionDetails, { sx: { pt: 0 }, children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1.25, children: [
1554
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 0.5, children: [
1555
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", sx: { bgcolor: "action.hover", px: 1, py: 0.5, borderRadius: 1 }, children: "Assignment and response" }),
1556
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: "Follow-up assignment" }),
1557
+ assignment.prompt ? /* @__PURE__ */ jsxRuntime.jsx(MDEditor3__default.default.Markdown, { source: assignment.prompt, style: { backgroundColor: "transparent", color: "inherit", padding: 0, margin: 0 } }) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "No assignment prompt provided." }),
1558
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: "Learner response to assignment" }),
1559
+ assignment.learner_assignment_response_md || assignment.learner_response_md ? /* @__PURE__ */ jsxRuntime.jsx(
1560
+ MDEditor3__default.default.Markdown,
1561
+ {
1562
+ source: assignment.learner_assignment_response_md ?? assignment.learner_response_md ?? "",
1563
+ style: { backgroundColor: "transparent", color: "inherit", padding: 0, margin: 0 }
1564
+ }
1565
+ ) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "No assignment response submitted." })
1566
+ ] }),
1567
+ /* @__PURE__ */ jsxRuntime.jsx(material.Divider, {}),
1568
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 0.5, children: [
1569
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", sx: { bgcolor: "action.hover", px: 1, py: 0.5, borderRadius: 1 }, children: "Targeted question updates" }),
1570
+ filteredTargetedUpdates.map((item, index) => /* @__PURE__ */ jsxRuntime.jsx(material.Card, { variant: "outlined", children: /* @__PURE__ */ jsxRuntime.jsx(material.CardContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 0.75, children: [
1571
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", spacing: 1, alignItems: "center", flexWrap: "wrap", children: [
1572
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "body2", fontWeight: 600, children: [
1573
+ index + 1,
1574
+ ". ",
1575
+ item.question?.prompt ?? "Targeted question"
1576
+ ] }),
1577
+ /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { size: "small", color: item.changed ? "success" : "default", label: item.changed ? "Changed" : "Unchanged" })
1578
+ ] }),
1579
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Grid, { container: true, spacing: 1.5, children: [
1580
+ /* @__PURE__ */ jsxRuntime.jsx(material.Grid, { size: { xs: 12 }, children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 0.5, children: [
1581
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: "Previous answer" }),
1582
+ item.previousAnswer.trim() ? /* @__PURE__ */ jsxRuntime.jsx(MDEditor3__default.default.Markdown, { source: item.previousAnswer, style: { backgroundColor: "transparent", color: "inherit", padding: 0, margin: 0 } }) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "No previous answer captured." })
1583
+ ] }) }),
1584
+ /* @__PURE__ */ jsxRuntime.jsx(material.Grid, { size: { xs: 12 }, children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 0.5, children: [
1585
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: "Updated answer" }),
1586
+ item.updatedAnswer.trim() ? /* @__PURE__ */ jsxRuntime.jsx(MDEditor3__default.default.Markdown, { source: item.updatedAnswer, style: { backgroundColor: "transparent", color: "inherit", padding: 0, margin: 0 } }) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "No updated answer submitted yet." })
1587
+ ] }) })
1588
+ ] })
1589
+ ] }) }) }, `${assignment.id}-${item.questionId}`)),
1590
+ !filteredTargetedUpdates.length ? /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: followUpHistoryFilter === "changed" ? "No changed targeted answers in this follow-up." : "No targeted questions for this follow-up." }) : null
1591
+ ] }),
1592
+ /* @__PURE__ */ jsxRuntime.jsx(material.Divider, {}),
1593
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 0.5, children: [
1594
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", sx: { bgcolor: "action.hover", px: 1, py: 0.5, borderRadius: 1 }, children: "Additional response" }),
1595
+ assignment.learner_additional_response_md ? /* @__PURE__ */ jsxRuntime.jsx(MDEditor3__default.default.Markdown, { source: assignment.learner_additional_response_md, style: { backgroundColor: "transparent", color: "inherit", padding: 0, margin: 0 } }) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "No additional response submitted." })
1596
+ ] }),
1597
+ assignment.reviewer_note_md ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1598
+ /* @__PURE__ */ jsxRuntime.jsx(material.Divider, {}),
1599
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "caption", color: "text.secondary", children: [
1600
+ "Reviewer note: ",
1601
+ assignment.reviewer_note_md
1602
+ ] })
1603
+ ] }) : null
1604
+ ] }) })
1605
+ ]
1606
+ },
1607
+ assignment.id
1608
+ );
1609
+ }) })
1610
+ ] }) }) : null,
1611
+ canReviewSubmittedModule ? /* @__PURE__ */ jsxRuntime.jsx(material.Card, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.CardContent, { children: [
1612
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle1", fontWeight: 600, sx: { mb: 1 }, children: "Review decision" }),
1613
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1.5, children: [
1614
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: { xs: "column", sm: "row" }, spacing: 1, children: [
1615
+ /* @__PURE__ */ jsxRuntime.jsx(
1616
+ material.Button,
1617
+ {
1618
+ variant: reviewDecision === "pass" ? "contained" : "outlined",
1619
+ color: reviewDecision === "pass" ? "success" : "inherit",
1620
+ onClick: () => setReviewDecision("pass"),
1621
+ disabled: review.isSaving,
1622
+ children: config.labels.passAction
1623
+ }
1624
+ ),
1625
+ /* @__PURE__ */ jsxRuntime.jsx(
1626
+ material.Button,
1627
+ {
1628
+ variant: reviewDecision === "follow_up" ? "contained" : "outlined",
1629
+ color: reviewDecision === "follow_up" ? "warning" : "inherit",
1630
+ onClick: () => setReviewDecision("follow_up"),
1631
+ disabled: review.isSaving || !config.workflow.enableFollowUpAssignments,
1632
+ children: config.labels.requestRevisionAction
1633
+ }
1634
+ )
1635
+ ] }),
1636
+ reviewDecision === "pass" ? /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1, children: [
1637
+ /* @__PURE__ */ jsxRuntime.jsx(
1638
+ material.TextField,
1639
+ {
1640
+ label: "Feedback (optional)",
1641
+ multiline: true,
1642
+ minRows: 2,
1643
+ value: passFeedback,
1644
+ onChange: (event) => setPassFeedback(event.target.value)
1645
+ }
1646
+ ),
1647
+ /* @__PURE__ */ jsxRuntime.jsx(
1648
+ material.Button,
1649
+ {
1650
+ variant: "contained",
1651
+ color: "success",
1652
+ onClick: () => void handlePassDecision(),
1653
+ disabled: review.isSaving,
1654
+ sx: { alignSelf: "flex-start" },
1655
+ children: config.labels.passAction
1656
+ }
1657
+ )
1658
+ ] }) : null,
1659
+ reviewDecision === "follow_up" ? /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1, children: [
1660
+ /* @__PURE__ */ jsxRuntime.jsx(
1661
+ material.TextField,
1662
+ {
1663
+ label: "Feedback",
1664
+ multiline: true,
1665
+ minRows: 2,
1666
+ value: followUpFeedback,
1667
+ onChange: (event) => setFollowUpFeedback(event.target.value)
1668
+ }
1669
+ ),
1670
+ /* @__PURE__ */ jsxRuntime.jsx(
1671
+ material.TextField,
1672
+ {
1673
+ label: `${config.naming.assignment} (optional)`,
1674
+ multiline: true,
1675
+ minRows: 2,
1676
+ value: followUpPrompt,
1677
+ onChange: (event) => setFollowUpPrompt(event.target.value)
1678
+ }
1679
+ ),
1680
+ config.workflow.enableTargetedQuestionSelection ? /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 0.5, children: [
1681
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", children: "Questions to revisit (optional)" }),
1682
+ selectedQuestions.length ? /* @__PURE__ */ jsxRuntime.jsx(material.Stack, { spacing: 0.25, children: selectedQuestions.map((question, index) => {
1683
+ const checked = selectedFollowUpQuestionIds.includes(question.id);
1684
+ return /* @__PURE__ */ jsxRuntime.jsx(
1685
+ material.FormControlLabel,
1686
+ {
1687
+ control: /* @__PURE__ */ jsxRuntime.jsx(
1688
+ material.Checkbox,
1689
+ {
1690
+ size: "small",
1691
+ checked,
1692
+ onChange: (event) => setSelectedFollowUpQuestionIds(
1693
+ (prev) => event.target.checked ? [...prev, question.id] : prev.filter((id) => id !== question.id)
1694
+ )
1695
+ }
1696
+ ),
1697
+ label: /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "body2", children: [
1698
+ index + 1,
1699
+ ". ",
1700
+ question.prompt
1701
+ ] })
1702
+ },
1703
+ question.id
1704
+ );
1705
+ }) }) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "This module currently has no questions to target." })
1706
+ ] }) : null,
1707
+ /* @__PURE__ */ jsxRuntime.jsx(
1708
+ material.Button,
1709
+ {
1710
+ variant: "contained",
1711
+ color: "warning",
1712
+ onClick: () => void handleAssignFollowUpDecision(),
1713
+ disabled: review.isSaving,
1714
+ sx: { alignSelf: "flex-start" },
1715
+ children: config.labels.requestRevisionAction
1716
+ }
1717
+ )
1718
+ ] }) : null,
1719
+ !reviewDecision ? /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "Choose a decision to continue." }) : null
1720
+ ] })
1721
+ ] }) }) : /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "info", children: "This module is not currently in submitted status." }),
1722
+ config.workflow.enableStatusOverride ? /* @__PURE__ */ jsxRuntime.jsx(material.Card, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.CardContent, { children: [
1723
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle1", fontWeight: 600, sx: { mb: 1 }, children: "Reviewer controls" }),
1724
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1.25, children: [
1725
+ /* @__PURE__ */ jsxRuntime.jsxs(
1726
+ material.TextField,
1727
+ {
1728
+ select: true,
1729
+ label: "Set module status",
1730
+ value: statusOverrideTarget,
1731
+ onChange: (event) => setStatusOverrideTarget(event.target.value),
1732
+ children: [
1733
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "pending", children: "Pending (reset)" }),
1734
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "submitted", children: "Submitted (reopen)" }),
1735
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "revision_required", children: "Revision required" }),
1736
+ /* @__PURE__ */ jsxRuntime.jsx(material.MenuItem, { value: "passed", children: "Passed" })
1737
+ ]
1738
+ }
1739
+ ),
1740
+ /* @__PURE__ */ jsxRuntime.jsx(
1741
+ material.TextField,
1742
+ {
1743
+ label: "Reason / note (optional)",
1744
+ multiline: true,
1745
+ minRows: 2,
1746
+ value: statusOverrideNote,
1747
+ onChange: (event) => setStatusOverrideNote(event.target.value)
1748
+ }
1749
+ ),
1750
+ /* @__PURE__ */ jsxRuntime.jsx(
1751
+ material.Button,
1752
+ {
1753
+ variant: "outlined",
1754
+ onClick: () => void handleApplyStatusOverride(),
1755
+ disabled: review.isSaving || !selectedRecord,
1756
+ sx: { alignSelf: "flex-start" },
1757
+ children: config.labels.applyStatusAction
1758
+ }
1759
+ ),
1760
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: "Use this area to reopen submitted work, reset progress, or correct accidental status changes." }),
1761
+ reviewerActivity.length ? /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 0.5, children: [
1762
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", children: "Recent activity" }),
1763
+ reviewerActivity.map((eventItem) => /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "caption", color: "text.secondary", children: [
1764
+ new Date(eventItem.at).toLocaleString(),
1765
+ ": ",
1766
+ eventItem.label
1767
+ ] }, `${eventItem.at}-${eventItem.label}`))
1768
+ ] }) : null
1769
+ ] })
1770
+ ] }) }) : null
1771
+ ] }) })
1772
+ ] })
1773
+ ] });
1774
+ };
1775
+ var unfinishedStatuses = ["in_progress", "revision_required", "pending"];
1776
+ var markdownStyle = { backgroundColor: "transparent", color: "inherit", padding: 0, margin: 0 };
1777
+ var pickInitialLearningModuleId = (orderedRecords, currentModuleId) => {
1778
+ if (!orderedRecords.length) return null;
1779
+ if (currentModuleId && orderedRecords.some((record) => record.module.id === currentModuleId)) {
1780
+ return currentModuleId;
1781
+ }
1782
+ const firstUnfinished = orderedRecords.find((record) => unfinishedStatuses.includes(record.progress.status));
1783
+ return firstUnfinished?.module.id ?? orderedRecords[0].module.id;
1784
+ };
1785
+ var statusLabel2 = (status) => {
1786
+ if (status === "in_progress") return "In progress";
1787
+ if (status === "submitted") return "Submitted";
1788
+ if (status === "passed") return "Passed";
1789
+ if (status === "revision_required") return "Follow up";
1790
+ return "Pending";
1791
+ };
1792
+ var statusColor2 = (status) => {
1793
+ if (status === "in_progress") return "warning";
1794
+ if (status === "submitted") return "info";
1795
+ if (status === "passed") return "success";
1796
+ if (status === "revision_required") return "warning";
1797
+ return "default";
1798
+ };
1799
+ var getStageQuestions = (stage) => stage.questions ?? [];
1800
+ var normalizeResponse = (value) => (value ?? "").trim();
1801
+ var HandbookLmsLearningWorkspace = ({
1802
+ adapter,
1803
+ courseSlug,
1804
+ config,
1805
+ title,
1806
+ onBack,
1807
+ onOpenReview,
1808
+ openReviewLabel,
1809
+ notifications,
1810
+ enabled = true
1811
+ }) => {
1812
+ const theme = styles.useTheme();
1813
+ const isMobile = material.useMediaQuery(theme.breakpoints.down("md"));
1814
+ const learning = useLmsLearningWorkspace({ adapter, courseSlug, enabled });
1815
+ const [selectedModuleId, setSelectedModuleId] = react.useState(null);
1816
+ const [questionDrafts, setQuestionDrafts] = react.useState({});
1817
+ const [savedQuestionDrafts, setSavedQuestionDrafts] = react.useState({});
1818
+ const [assignmentResponseDrafts, setAssignmentResponseDrafts] = react.useState({});
1819
+ const [assignmentAdditionalDrafts, setAssignmentAdditionalDrafts] = react.useState({});
1820
+ const [activeStageStep, setActiveStageStep] = react.useState(0);
1821
+ const [moduleDrawerOpen, setModuleDrawerOpen] = react.useState(false);
1822
+ const [submitSuccessDialogOpen, setSubmitSuccessDialogOpen] = react.useState(false);
1823
+ const [expandedFollowUpId, setExpandedFollowUpId] = react.useState(false);
1824
+ const [followUpSummaryDialog, setFollowUpSummaryDialog] = react.useState(null);
1825
+ const [isCurrentFollowUpBlockVisible, setIsCurrentFollowUpBlockVisible] = react.useState(false);
1826
+ const [notice, setNotice] = react.useState(null);
1827
+ const publishNotice = (severity, message) => {
1828
+ notifications?.notify?.({ severity, message });
1829
+ if (severity === "success") notifications?.success?.(message);
1830
+ if (severity === "error") notifications?.error?.(message);
1831
+ if (severity === "info") notifications?.info?.(message);
1832
+ if (severity === "warning") notifications?.warning?.(message);
1833
+ const hasExternalHandler = Boolean(notifications?.notify || notifications?.[severity]);
1834
+ if (hasExternalHandler) {
1835
+ setNotice(null);
1836
+ return;
1837
+ }
1838
+ setNotice({ severity, message });
1839
+ };
1840
+ const autosaveTimerRef = react.useRef(null);
1841
+ const lastAutosaveNoticeAtRef = react.useRef(0);
1842
+ const activeFollowUpBlockRef = react.useRef(null);
1843
+ const records = react.useMemo(() => learning.records, [learning.records]);
1844
+ const recordsByModule = react.useMemo(() => {
1845
+ const map = /* @__PURE__ */ new Map();
1846
+ for (const record of records) map.set(record.module.id, record);
1847
+ return map;
1848
+ }, [records]);
1849
+ const sections = react.useMemo(() => {
1850
+ const sectionMap = /* @__PURE__ */ new Map();
1851
+ for (const record of records) {
1852
+ const section = record.module.section;
1853
+ const sectionId = section?.id ?? "uncategorized";
1854
+ const sectionTitle = section?.title ?? "Uncategorized";
1855
+ const displayOrder = section?.display_order ?? 999;
1856
+ const current = sectionMap.get(sectionId) ?? {
1857
+ id: sectionId,
1858
+ title: sectionTitle,
1859
+ display_order: displayOrder,
1860
+ records: []
1861
+ };
1862
+ current.records.push(record);
1863
+ sectionMap.set(sectionId, current);
1864
+ }
1865
+ return Array.from(sectionMap.values()).map((section) => ({
1866
+ ...section,
1867
+ records: section.records.sort((a, b) => a.module.display_order - b.module.display_order)
1868
+ })).sort((a, b) => a.display_order - b.display_order);
1869
+ }, [records]);
1870
+ const orderedRecords = react.useMemo(() => sections.flatMap((section) => section.records), [sections]);
1871
+ react.useEffect(() => {
1872
+ setSelectedModuleId((prev) => pickInitialLearningModuleId(orderedRecords, prev));
1873
+ }, [orderedRecords]);
1874
+ const selectedRecord = selectedModuleId ? recordsByModule.get(selectedModuleId) ?? null : null;
1875
+ const stages = react.useMemo(() => selectedRecord?.module.stages ?? [], [selectedRecord?.module.stages]);
1876
+ const moduleQuestionsById = react.useMemo(
1877
+ () => new Map(
1878
+ stages.flatMap(
1879
+ (stage) => stage.questions.map((question) => [question.id, { question, stage }])
1880
+ )
1881
+ ),
1882
+ [stages]
1883
+ );
1884
+ const moduleQuestions = react.useMemo(
1885
+ () => stages.flatMap((stage) => stage.questions ?? []),
1886
+ [stages]
1887
+ );
1888
+ const isMultiStage = selectedRecord?.module.module_mode === "multi_stage" && stages.length > 1;
1889
+ const isFollowUpModule = selectedRecord?.progress.status === "revision_required";
1890
+ const isOriginalWorkReadOnly = selectedRecord?.progress.status === "submitted" || selectedRecord?.progress.status === "passed" || selectedRecord?.progress.status === "revision_required";
1891
+ const followUpBlocks = react.useMemo(
1892
+ () => [...selectedRecord?.assignments ?? []].sort(
1893
+ (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
1894
+ ),
1895
+ [selectedRecord?.assignments]
1896
+ );
1897
+ const activeFollowUpBlock = react.useMemo(() => {
1898
+ const pending = followUpBlocks.filter((assignment) => assignment.status === "pending");
1899
+ return pending[pending.length - 1] ?? null;
1900
+ }, [followUpBlocks]);
1901
+ const orderedModuleIds = react.useMemo(
1902
+ () => orderedRecords.map((record) => record.module.id),
1903
+ [orderedRecords]
1904
+ );
1905
+ const selectedModuleIndex = selectedModuleId ? orderedModuleIds.indexOf(selectedModuleId) : -1;
1906
+ const nextModuleId = selectedModuleIndex >= 0 && selectedModuleIndex < orderedModuleIds.length - 1 ? orderedModuleIds[selectedModuleIndex + 1] : null;
1907
+ const nextModuleTitle = nextModuleId ? recordsByModule.get(nextModuleId)?.module.title ?? null : null;
1908
+ const unansweredQuestionCount = moduleQuestions.filter(
1909
+ (question) => !normalizeResponse(questionDrafts[question.id])
1910
+ ).length;
1911
+ const canSubmitModule = unansweredQuestionCount === 0;
1912
+ react.useEffect(() => {
1913
+ if (!selectedModuleId) return;
1914
+ const selected = recordsByModule.get(selectedModuleId);
1915
+ if (!selected) return;
1916
+ const responseMap = selected.responses.reduce((acc, response) => {
1917
+ acc[response.question_id] = response.response_md;
1918
+ return acc;
1919
+ }, {});
1920
+ setQuestionDrafts(responseMap);
1921
+ setSavedQuestionDrafts(responseMap);
1922
+ const assignmentResponseMap = selected.assignments.reduce((acc, assignment) => {
1923
+ acc[assignment.id] = assignment.learner_assignment_response_md ?? assignment.learner_response_md ?? "";
1924
+ return acc;
1925
+ }, {});
1926
+ const assignmentAdditionalMap = selected.assignments.reduce((acc, assignment) => {
1927
+ acc[assignment.id] = assignment.learner_additional_response_md ?? "";
1928
+ return acc;
1929
+ }, {});
1930
+ setAssignmentResponseDrafts(assignmentResponseMap);
1931
+ setAssignmentAdditionalDrafts(assignmentAdditionalMap);
1932
+ }, [selectedModuleId, recordsByModule]);
1933
+ react.useEffect(() => {
1934
+ setActiveStageStep(0);
1935
+ }, [selectedModuleId]);
1936
+ react.useEffect(() => {
1937
+ if (!isMobile) setModuleDrawerOpen(false);
1938
+ }, [isMobile]);
1939
+ react.useEffect(() => {
1940
+ setIsCurrentFollowUpBlockVisible(false);
1941
+ }, [selectedRecord?.module.id]);
1942
+ react.useEffect(() => {
1943
+ if (!followUpBlocks.length) {
1944
+ setExpandedFollowUpId(false);
1945
+ return;
1946
+ }
1947
+ setExpandedFollowUpId((prev) => {
1948
+ if (prev && followUpBlocks.some((assignment) => assignment.id === prev)) return prev;
1949
+ return activeFollowUpBlock?.id ?? followUpBlocks[followUpBlocks.length - 1]?.id ?? false;
1950
+ });
1951
+ }, [activeFollowUpBlock?.id, followUpBlocks]);
1952
+ react.useEffect(() => {
1953
+ const node = activeFollowUpBlockRef.current;
1954
+ if (!node || !isFollowUpModule || !activeFollowUpBlock) return;
1955
+ const observer = new IntersectionObserver(
1956
+ (entries) => {
1957
+ const entry = entries[0];
1958
+ setIsCurrentFollowUpBlockVisible(Boolean(entry?.isIntersecting));
1959
+ },
1960
+ { threshold: 0.2 }
1961
+ );
1962
+ observer.observe(node);
1963
+ return () => observer.disconnect();
1964
+ }, [isFollowUpModule, activeFollowUpBlock, selectedRecord?.module.id]);
1965
+ const getDirtyQuestionIds = (questionIds) => {
1966
+ const idsToCheck = questionIds ?? stages.flatMap((stage) => stage.questions.map((question) => question.id));
1967
+ return idsToCheck.filter((questionId) => {
1968
+ const current = normalizeResponse(questionDrafts[questionId]);
1969
+ if (!current) return false;
1970
+ const saved = normalizeResponse(savedQuestionDrafts[questionId]);
1971
+ return current !== saved;
1972
+ });
1973
+ };
1974
+ const isStageComplete = (stage) => {
1975
+ const questions = getStageQuestions(stage);
1976
+ if (!questions.length) return false;
1977
+ return questions.every((question) => Boolean(normalizeResponse(questionDrafts[question.id])));
1978
+ };
1979
+ const saveDirtyQuestions = async (opts) => {
1980
+ if (!selectedRecord) return true;
1981
+ const dirtyQuestionIds = getDirtyQuestionIds(opts?.questionIds);
1982
+ if (!dirtyQuestionIds.length) return true;
1983
+ const nextSaved = { ...savedQuestionDrafts };
1984
+ for (const questionId of dirtyQuestionIds) {
1985
+ const response = normalizeResponse(questionDrafts[questionId]);
1986
+ const result = await learning.saveQuestionResponse({
1987
+ courseSlug,
1988
+ moduleId: selectedRecord.module.id,
1989
+ questionId,
1990
+ response_md: response
1991
+ });
1992
+ if (!result.ok) {
1993
+ publishNotice("error", result.error ?? "Unable to save response.");
1994
+ return false;
1995
+ }
1996
+ nextSaved[questionId] = response;
1997
+ }
1998
+ setSavedQuestionDrafts(nextSaved);
1999
+ if (opts?.showSuccessNotice ?? true) {
2000
+ publishNotice("success", dirtyQuestionIds.length === 1 ? "Answer saved." : "Answers saved.");
2001
+ }
2002
+ return true;
2003
+ };
2004
+ react.useEffect(() => {
2005
+ if (!selectedRecord) return;
2006
+ if (autosaveTimerRef.current !== null) {
2007
+ window.clearTimeout(autosaveTimerRef.current);
2008
+ autosaveTimerRef.current = null;
2009
+ }
2010
+ const dirtyQuestionIds = getDirtyQuestionIds();
2011
+ if (!dirtyQuestionIds.length) return;
2012
+ autosaveTimerRef.current = window.setTimeout(() => {
2013
+ void (async () => {
2014
+ const saved = await saveDirtyQuestions({ showSuccessNotice: false });
2015
+ if (saved && notifications && config.workflow.enableAutosaveNotifications) {
2016
+ const cooldownMs = Math.max(0, config.workflow.autosaveNotificationCooldownMs);
2017
+ const now = Date.now();
2018
+ if (now - lastAutosaveNoticeAtRef.current >= cooldownMs) {
2019
+ lastAutosaveNoticeAtRef.current = now;
2020
+ publishNotice("info", config.labels.autosaveNotice);
2021
+ }
2022
+ }
2023
+ autosaveTimerRef.current = null;
2024
+ })();
2025
+ }, 900);
2026
+ return () => {
2027
+ if (autosaveTimerRef.current !== null) {
2028
+ window.clearTimeout(autosaveTimerRef.current);
2029
+ autosaveTimerRef.current = null;
2030
+ }
2031
+ };
2032
+ }, [selectedRecord?.module.id, questionDrafts, savedQuestionDrafts]);
2033
+ const handleSelectModule = async (moduleId) => {
2034
+ if (moduleId === selectedModuleId) return;
2035
+ if (autosaveTimerRef.current !== null) {
2036
+ window.clearTimeout(autosaveTimerRef.current);
2037
+ autosaveTimerRef.current = null;
2038
+ }
2039
+ await saveDirtyQuestions({ showSuccessNotice: false });
2040
+ setSelectedModuleId(moduleId);
2041
+ setModuleDrawerOpen(false);
2042
+ window.requestAnimationFrame(() => {
2043
+ window.scrollTo({ top: 0, behavior: "smooth" });
2044
+ });
2045
+ };
2046
+ const handleQuestionBlur = (questionId) => {
2047
+ void saveDirtyQuestions({ questionIds: [questionId], showSuccessNotice: false });
2048
+ };
2049
+ const handleSubmitModule = async (moduleId) => {
2050
+ if (!canSubmitModule) {
2051
+ publishNotice("info", `Answer all ${config.naming.question.toLowerCase()}s before submitting for review.`);
2052
+ return;
2053
+ }
2054
+ const saved = await saveDirtyQuestions({ showSuccessNotice: false });
2055
+ if (!saved) return;
2056
+ const result = await learning.submitModule({ courseSlug, moduleId }, { reload: true });
2057
+ if (!result.ok) {
2058
+ publishNotice("error", result.error ?? "Unable to submit module.");
2059
+ return;
2060
+ }
2061
+ publishNotice("success", "Module submitted for review.");
2062
+ setSubmitSuccessDialogOpen(true);
2063
+ };
2064
+ const getAssignmentResponseDraft = (assignment) => assignmentResponseDrafts[assignment.id] ?? assignment.learner_assignment_response_md ?? assignment.learner_response_md ?? "";
2065
+ const getAssignmentAdditionalDraft = (assignment) => assignmentAdditionalDrafts[assignment.id] ?? assignment.learner_additional_response_md ?? "";
2066
+ const handleSubmitAssignment = async (assignment) => {
2067
+ const targetQuestionIds = assignment.target_question_ids ?? void 0;
2068
+ const saved = await saveDirtyQuestions({ questionIds: targetQuestionIds, showSuccessNotice: false });
2069
+ if (!saved) return;
2070
+ const assignmentResponse = normalizeResponse(getAssignmentResponseDraft(assignment));
2071
+ const additionalResponse = normalizeResponse(getAssignmentAdditionalDraft(assignment));
2072
+ const updatedTargetedQuestionCount = (assignment.target_question_ids ?? []).filter((questionId) => {
2073
+ const currentAnswer = normalizeResponse(questionDrafts[questionId]);
2074
+ const previousAnswer = normalizeResponse(assignment.target_original_answers_json?.[questionId]);
2075
+ return currentAnswer.length > 0 && currentAnswer !== previousAnswer;
2076
+ }).length;
2077
+ const result = await learning.updateAssignment(
2078
+ {
2079
+ assignmentId: assignment.id,
2080
+ data: {
2081
+ learner_assignment_response_md: assignmentResponse || null,
2082
+ learner_additional_response_md: additionalResponse || null,
2083
+ learner_response_md: assignmentResponse || null,
2084
+ status: "submitted"
2085
+ }
2086
+ },
2087
+ { reload: true }
2088
+ );
2089
+ if (!result.ok) {
2090
+ publishNotice("error", result.error ?? "Unable to submit follow-up.");
2091
+ return;
2092
+ }
2093
+ publishNotice("success", "Follow-up submitted.");
2094
+ setFollowUpSummaryDialog({
2095
+ updatedQuestions: updatedTargetedQuestionCount,
2096
+ hasAssignmentResponse: Boolean(assignmentResponse)
2097
+ });
2098
+ };
2099
+ const renderQuestionEditor = (question, index, opts) => /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1, children: [
2100
+ opts?.showPrompt === false ? null : /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "body2", fontWeight: 600, children: [
2101
+ index + 1,
2102
+ ". ",
2103
+ question.prompt
2104
+ ] }),
2105
+ /* @__PURE__ */ jsxRuntime.jsx(
2106
+ MDEditor3__default.default,
2107
+ {
2108
+ value: questionDrafts[question.id] ?? "",
2109
+ onChange: (value) => setQuestionDrafts((prev) => ({ ...prev, [question.id]: value ?? "" })),
2110
+ preview: "edit",
2111
+ textareaProps: {
2112
+ placeholder: "Write your response in Markdown.",
2113
+ onBlur: () => handleQuestionBlur(question.id)
2114
+ },
2115
+ "data-color-mode": theme.palette.mode
2116
+ }
2117
+ )
2118
+ ] }, question.id);
2119
+ const renderReadOnlyQuestion = (question, index) => /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 0.75, children: [
2120
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "body2", fontWeight: 600, children: [
2121
+ index + 1,
2122
+ ". ",
2123
+ question.prompt
2124
+ ] }),
2125
+ (questionDrafts[question.id] ?? "").trim() ? /* @__PURE__ */ jsxRuntime.jsx(
2126
+ MDEditor3__default.default.Markdown,
2127
+ {
2128
+ source: questionDrafts[question.id] ?? "",
2129
+ style: markdownStyle,
2130
+ "data-color-mode": theme.palette.mode
2131
+ }
2132
+ ) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "No response submitted yet." })
2133
+ ] }, question.id);
2134
+ const renderFollowUpQuestion = (assignment, question, index, editable) => {
2135
+ const previousAnswer = assignment.target_original_answers_json?.[question.id] ?? "";
2136
+ const submittedFollowUpAnswer = assignment.target_updated_answers_json?.[question.id] ?? "";
2137
+ const editableFollowUpAnswer = questionDrafts[question.id] ?? "";
2138
+ const currentAnswer = editable ? editableFollowUpAnswer : submittedFollowUpAnswer;
2139
+ const changed = normalizeResponse(currentAnswer) !== normalizeResponse(previousAnswer);
2140
+ if (editable) {
2141
+ return /* @__PURE__ */ jsxRuntime.jsx(material.Card, { variant: "outlined", sx: { borderColor: changed ? "success.light" : "divider" }, children: /* @__PURE__ */ jsxRuntime.jsx(material.CardContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1, children: [
2142
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", spacing: 1, alignItems: "center", children: [
2143
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "body2", fontWeight: 600, children: [
2144
+ index + 1,
2145
+ ". ",
2146
+ question.prompt
2147
+ ] }),
2148
+ /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { size: "small", color: changed ? "success" : "default", label: changed ? "Changed" : "Unchanged" })
2149
+ ] }),
2150
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Grid, { container: true, spacing: 1.5, children: [
2151
+ /* @__PURE__ */ jsxRuntime.jsx(material.Grid, { size: { xs: 12 }, children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 0.5, children: [
2152
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: "Previous answer" }),
2153
+ previousAnswer.trim() ? /* @__PURE__ */ jsxRuntime.jsx(MDEditor3__default.default.Markdown, { source: previousAnswer, style: markdownStyle, "data-color-mode": theme.palette.mode }) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "No previous answer captured." })
2154
+ ] }) }),
2155
+ /* @__PURE__ */ jsxRuntime.jsx(material.Grid, { size: { xs: 12 }, children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 0.5, children: [
2156
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: "Updated answer" }),
2157
+ renderQuestionEditor(question, index, { showPrompt: false })
2158
+ ] }) })
2159
+ ] })
2160
+ ] }) }) }, question.id);
2161
+ }
2162
+ const hasComparison = previousAnswer.trim() || submittedFollowUpAnswer.trim();
2163
+ if (!hasComparison) {
2164
+ return /* @__PURE__ */ jsxRuntime.jsx(material.Card, { variant: "outlined", children: /* @__PURE__ */ jsxRuntime.jsx(material.CardContent, { children: renderReadOnlyQuestion(question, index) }) }, question.id);
2165
+ }
2166
+ return /* @__PURE__ */ jsxRuntime.jsx(material.Card, { variant: "outlined", sx: { borderColor: changed ? "success.light" : "divider" }, children: /* @__PURE__ */ jsxRuntime.jsx(material.CardContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1, children: [
2167
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", spacing: 1, alignItems: "center", children: [
2168
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "body2", fontWeight: 600, children: [
2169
+ index + 1,
2170
+ ". ",
2171
+ question.prompt
2172
+ ] }),
2173
+ /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { size: "small", color: changed ? "success" : "default", label: changed ? "Changed" : "Unchanged" })
2174
+ ] }),
2175
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Grid, { container: true, spacing: 1.5, children: [
2176
+ /* @__PURE__ */ jsxRuntime.jsx(material.Grid, { size: { xs: 12 }, children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 0.5, children: [
2177
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: "Previous answer" }),
2178
+ previousAnswer.trim() ? /* @__PURE__ */ jsxRuntime.jsx(MDEditor3__default.default.Markdown, { source: previousAnswer, style: markdownStyle, "data-color-mode": theme.palette.mode }) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "No previous answer captured." })
2179
+ ] }) }),
2180
+ /* @__PURE__ */ jsxRuntime.jsx(material.Grid, { size: { xs: 12 }, children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 0.5, children: [
2181
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: "Updated answer" }),
2182
+ submittedFollowUpAnswer.trim() ? /* @__PURE__ */ jsxRuntime.jsx(
2183
+ MDEditor3__default.default.Markdown,
2184
+ {
2185
+ source: submittedFollowUpAnswer,
2186
+ style: markdownStyle,
2187
+ "data-color-mode": theme.palette.mode
2188
+ }
2189
+ ) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "No follow-up answer submitted yet." })
2190
+ ] }) })
2191
+ ] })
2192
+ ] }) }) }, question.id);
2193
+ };
2194
+ const getFollowUpDeltaSummary = (assignment, editable) => {
2195
+ const targetIds = assignment.target_question_ids ?? [];
2196
+ const changed = targetIds.filter((questionId) => {
2197
+ const previous = normalizeResponse(assignment.target_original_answers_json?.[questionId]);
2198
+ const current = normalizeResponse(
2199
+ editable ? questionDrafts[questionId] : assignment.target_updated_answers_json?.[questionId]
2200
+ );
2201
+ return previous !== current;
2202
+ }).length;
2203
+ return {
2204
+ changed,
2205
+ unchanged: Math.max(targetIds.length - changed, 0),
2206
+ total: targetIds.length
2207
+ };
2208
+ };
2209
+ const canSubmitFollowUpBlock = (assignment) => {
2210
+ if (assignment.status !== "pending") return false;
2211
+ const targetedQuestionIds = assignment.target_question_ids ?? [];
2212
+ const hasMissingTargetedAnswer = targetedQuestionIds.some(
2213
+ (questionId) => !normalizeResponse(questionDrafts[questionId])
2214
+ );
2215
+ if (hasMissingTargetedAnswer) return false;
2216
+ if (assignment.response_mode === "written") {
2217
+ return Boolean(normalizeResponse(getAssignmentResponseDraft(assignment)));
2218
+ }
2219
+ return true;
2220
+ };
2221
+ const getFollowUpStageGroups = (assignment) => {
2222
+ const groups = /* @__PURE__ */ new Map();
2223
+ for (const questionId of assignment.target_question_ids ?? []) {
2224
+ const found = moduleQuestionsById.get(questionId);
2225
+ if (!found) continue;
2226
+ const existing = groups.get(found.stage.id);
2227
+ if (existing) {
2228
+ existing.questions.push(found.question);
2229
+ } else {
2230
+ groups.set(found.stage.id, { stage: found.stage, questions: [found.question] });
2231
+ }
2232
+ }
2233
+ return Array.from(groups.values()).map((group) => ({
2234
+ ...group,
2235
+ questions: group.questions.sort((a, b) => a.display_order - b.display_order)
2236
+ })).sort((a, b) => a.stage.display_order - b.stage.display_order);
2237
+ };
2238
+ const renderModuleMenu = () => /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1, children: [
2239
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", justifyContent: "space-between", alignItems: "center", sx: { mb: 1 }, children: [
2240
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", children: `${config.naming.module}s` }),
2241
+ onOpenReview ? /* @__PURE__ */ jsxRuntime.jsx(
2242
+ material.Button,
2243
+ {
2244
+ size: "small",
2245
+ variant: "text",
2246
+ onClick: () => {
2247
+ setModuleDrawerOpen(false);
2248
+ onOpenReview();
2249
+ },
2250
+ children: openReviewLabel ?? "Open review tool"
2251
+ }
2252
+ ) : null
2253
+ ] }),
2254
+ sections.map((section) => {
2255
+ const completed = section.records.filter((record) => record.progress.status === "passed").length;
2256
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Accordion, { defaultExpanded: true, children: [
2257
+ /* @__PURE__ */ jsxRuntime.jsx(material.AccordionSummary, { expandIcon: /* @__PURE__ */ jsxRuntime.jsx(ExpandMoreIcon__default.default, {}), children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", spacing: 1, alignItems: "center", justifyContent: "space-between", sx: { width: "100%" }, children: [
2258
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", children: section.title }),
2259
+ /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { size: "small", label: `${completed}/${section.records.length}` })
2260
+ ] }) }),
2261
+ /* @__PURE__ */ jsxRuntime.jsx(material.AccordionDetails, { sx: { pt: 0 }, children: /* @__PURE__ */ jsxRuntime.jsx(material.List, { disablePadding: true, children: section.records.map((record) => /* @__PURE__ */ jsxRuntime.jsxs(
2262
+ material.ListItemButton,
2263
+ {
2264
+ selected: record.module.id === selectedModuleId,
2265
+ onClick: () => void handleSelectModule(record.module.id),
2266
+ children: [
2267
+ /* @__PURE__ */ jsxRuntime.jsx(material.ListItemText, { primary: record.module.title }),
2268
+ /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { size: "small", color: statusColor2(record.progress.status), label: statusLabel2(record.progress.status) })
2269
+ ]
2270
+ },
2271
+ record.module.id
2272
+ )) }) })
2273
+ ] }, section.id);
2274
+ })
2275
+ ] });
2276
+ if (!enabled) {
2277
+ return /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "info", children: "Learning workspace is disabled." });
2278
+ }
2279
+ if (learning.isLoading) {
2280
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 2, children: [
2281
+ /* @__PURE__ */ jsxRuntime.jsx(material.Skeleton, { variant: "rounded", height: 52 }),
2282
+ /* @__PURE__ */ jsxRuntime.jsx(material.Skeleton, { variant: "rounded", height: 320 })
2283
+ ] });
2284
+ }
2285
+ return /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 2, children: [
2286
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", useFlexGap: true, children: [
2287
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h5", children: title ?? `${config.naming.course} Learning Hub` }),
2288
+ onBack ? /* @__PURE__ */ jsxRuntime.jsx(material.Button, { onClick: onBack, children: config.labels.backButton }) : null
2289
+ ] }),
2290
+ learning.error ? /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "error", children: learning.error }) : null,
2291
+ notice ? /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: notice.severity, children: notice.message }) : null,
2292
+ isMobile ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2293
+ /* @__PURE__ */ jsxRuntime.jsx(material.Card, { variant: "outlined", sx: { position: "sticky", top: 8, zIndex: 1 }, children: /* @__PURE__ */ jsxRuntime.jsx(material.CardContent, { sx: { py: 1.25 }, children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", alignItems: "center", justifyContent: "space-between", spacing: 1.5, children: [
2294
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 0.5, sx: { minWidth: 0 }, children: [
2295
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: "Current module" }),
2296
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", fontWeight: 600, noWrap: true, children: selectedRecord?.module.title ?? "Select a module" })
2297
+ ] }),
2298
+ selectedRecord ? /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { size: "small", color: statusColor2(selectedRecord.progress.status), label: statusLabel2(selectedRecord.progress.status) }) : null,
2299
+ /* @__PURE__ */ jsxRuntime.jsx(
2300
+ material.Button,
2301
+ {
2302
+ size: "small",
2303
+ variant: "outlined",
2304
+ startIcon: /* @__PURE__ */ jsxRuntime.jsx(MenuIcon__default.default, {}),
2305
+ onClick: () => setModuleDrawerOpen(true),
2306
+ sx: { flexShrink: 0 },
2307
+ children: "Modules"
2308
+ }
2309
+ )
2310
+ ] }) }) }),
2311
+ /* @__PURE__ */ jsxRuntime.jsx(
2312
+ material.Drawer,
2313
+ {
2314
+ anchor: "left",
2315
+ open: moduleDrawerOpen,
2316
+ onClose: () => setModuleDrawerOpen(false),
2317
+ PaperProps: { sx: { width: { xs: "min(90vw, 360px)", sm: 360 }, p: 2 } },
2318
+ children: renderModuleMenu()
2319
+ }
2320
+ )
2321
+ ] }) : null,
2322
+ isFollowUpModule && activeFollowUpBlock && !isCurrentFollowUpBlockVisible ? /* @__PURE__ */ jsxRuntime.jsx(
2323
+ material.Button,
2324
+ {
2325
+ size: "small",
2326
+ variant: "outlined",
2327
+ onClick: () => activeFollowUpBlockRef.current?.scrollIntoView({ behavior: "smooth", block: "start" }),
2328
+ sx: { alignSelf: "flex-start" },
2329
+ children: "Jump to current follow-up"
2330
+ }
2331
+ ) : null,
2332
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Grid, { container: true, spacing: 2, children: [
2333
+ !isMobile ? /* @__PURE__ */ jsxRuntime.jsx(material.Grid, { size: { xs: 12, md: 4 }, children: /* @__PURE__ */ jsxRuntime.jsx(material.Card, { children: /* @__PURE__ */ jsxRuntime.jsx(material.CardContent, { children: renderModuleMenu() }) }) }) : null,
2334
+ /* @__PURE__ */ jsxRuntime.jsx(material.Grid, { size: { xs: 12, md: 8 }, children: !selectedRecord ? /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: "info", children: "No learning modules are available yet." }) : /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 2, children: [
2335
+ /* @__PURE__ */ jsxRuntime.jsx(material.Card, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.CardContent, { children: [
2336
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: { xs: "column", sm: "row" }, justifyContent: "space-between", spacing: 1, sx: { mb: 1 }, children: [
2337
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "h6", children: selectedRecord.module.title }),
2338
+ /* @__PURE__ */ jsxRuntime.jsx(
2339
+ material.Chip,
2340
+ {
2341
+ size: "small",
2342
+ color: statusColor2(selectedRecord.progress.status),
2343
+ label: statusLabel2(selectedRecord.progress.status),
2344
+ sx: { display: { xs: "none", sm: "inline-flex" } }
2345
+ }
2346
+ )
2347
+ ] }),
2348
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", sx: { mb: 1.5 }, children: selectedRecord.module.summary }),
2349
+ isMultiStage ? /* @__PURE__ */ jsxRuntime.jsx(material.Stepper, { activeStep: activeStageStep, orientation: "vertical", nonLinear: true, children: stages.map((stage, index) => /* @__PURE__ */ jsxRuntime.jsxs(material.Step, { completed: isStageComplete(stage), children: [
2350
+ /* @__PURE__ */ jsxRuntime.jsx(material.StepButton, { onClick: () => setActiveStageStep(index), sx: { textAlign: "left" }, children: stage.title }),
2351
+ /* @__PURE__ */ jsxRuntime.jsx(material.StepContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 2, sx: { pb: 1 }, children: [
2352
+ /* @__PURE__ */ jsxRuntime.jsx(MDEditor3__default.default.Markdown, { source: stage.content_md || "", style: markdownStyle, "data-color-mode": theme.palette.mode }),
2353
+ getStageQuestions(stage).length ? /* @__PURE__ */ jsxRuntime.jsx(material.Card, { variant: "outlined", children: /* @__PURE__ */ jsxRuntime.jsxs(material.CardContent, { children: [
2354
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", sx: { mb: 1 }, children: "Reflection prompts" }),
2355
+ /* @__PURE__ */ jsxRuntime.jsx(material.Stack, { spacing: 2, children: getStageQuestions(stage).map(
2356
+ (question, questionIndex) => isOriginalWorkReadOnly ? renderReadOnlyQuestion(question, questionIndex) : renderQuestionEditor(question, questionIndex)
2357
+ ) })
2358
+ ] }) }) : null,
2359
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", spacing: 1, children: [
2360
+ /* @__PURE__ */ jsxRuntime.jsx(
2361
+ material.Button,
2362
+ {
2363
+ size: "small",
2364
+ variant: "outlined",
2365
+ disabled: index === 0,
2366
+ onClick: () => setActiveStageStep((prev) => Math.max(prev - 1, 0)),
2367
+ children: "Previous"
2368
+ }
2369
+ ),
2370
+ /* @__PURE__ */ jsxRuntime.jsx(
2371
+ material.Button,
2372
+ {
2373
+ size: "small",
2374
+ variant: "contained",
2375
+ disabled: index >= stages.length - 1,
2376
+ onClick: () => setActiveStageStep((prev) => Math.min(prev + 1, stages.length - 1)),
2377
+ children: "Next"
2378
+ }
2379
+ )
2380
+ ] })
2381
+ ] }) })
2382
+ ] }, stage.id)) }) : /* @__PURE__ */ jsxRuntime.jsx(material.Stack, { spacing: 2, children: stages.map((stage) => /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1, children: [
2383
+ stages.length > 1 ? /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle1", fontWeight: 600, children: stage.title }) : null,
2384
+ /* @__PURE__ */ jsxRuntime.jsx(MDEditor3__default.default.Markdown, { source: stage.content_md || "", style: markdownStyle, "data-color-mode": theme.palette.mode })
2385
+ ] }, stage.id)) }),
2386
+ selectedRecord.progress.review_feedback_md && !isFollowUpModule ? /* @__PURE__ */ jsxRuntime.jsx(material.Alert, { severity: selectedRecord.progress.status === "passed" ? "success" : "warning", sx: { mt: 2 }, children: /* @__PURE__ */ jsxRuntime.jsx(
2387
+ MDEditor3__default.default.Markdown,
2388
+ {
2389
+ source: selectedRecord.progress.review_feedback_md,
2390
+ style: markdownStyle,
2391
+ "data-color-mode": theme.palette.mode
2392
+ }
2393
+ ) }) : null
2394
+ ] }) }),
2395
+ !isMultiStage ? /* @__PURE__ */ jsxRuntime.jsx(material.Card, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.CardContent, { children: [
2396
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle1", fontWeight: 600, sx: { mb: 1 }, children: `${config.naming.question}s` }),
2397
+ /* @__PURE__ */ jsxRuntime.jsx(material.Stack, { spacing: 2, children: stages.flatMap((stage) => stage.questions).map(
2398
+ (question, idx) => isOriginalWorkReadOnly ? renderReadOnlyQuestion(question, idx) : renderQuestionEditor(question, idx)
2399
+ ) })
2400
+ ] }) }) : null,
2401
+ !isOriginalWorkReadOnly ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2402
+ /* @__PURE__ */ jsxRuntime.jsx(
2403
+ material.Button,
2404
+ {
2405
+ variant: "contained",
2406
+ onClick: () => void handleSubmitModule(selectedRecord.module.id),
2407
+ disabled: learning.isSubmittingModule || selectedRecord.progress.status === "submitted" || !canSubmitModule,
2408
+ sx: { alignSelf: "flex-start" },
2409
+ children: config.labels.submitModuleAction
2410
+ }
2411
+ ),
2412
+ !canSubmitModule ? /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "caption", color: "text.secondary", children: [
2413
+ "Answer all questions before submitting. ",
2414
+ unansweredQuestionCount,
2415
+ " remaining."
2416
+ ] }) : null
2417
+ ] }) : null,
2418
+ followUpBlocks.length ? /* @__PURE__ */ jsxRuntime.jsx(material.Card, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.CardContent, { children: [
2419
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle1", fontWeight: 600, sx: { mb: 1 }, children: config.naming.assignment }),
2420
+ /* @__PURE__ */ jsxRuntime.jsx(material.Stack, { spacing: 1.5, children: followUpBlocks.map((assignment, assignmentIndex) => {
2421
+ const isCurrentBlock = assignment.id === activeFollowUpBlock?.id;
2422
+ const isEditable = assignment.status === "pending" && isCurrentBlock;
2423
+ const stageGroups = getFollowUpStageGroups(assignment);
2424
+ const hasTargetedQuestions = stageGroups.length > 0;
2425
+ const deltaSummary = getFollowUpDeltaSummary(assignment, isEditable);
2426
+ const hasAssignmentResponse = Boolean(normalizeResponse(getAssignmentResponseDraft(assignment)));
2427
+ const hasAdditionalResponse = Boolean(normalizeResponse(getAssignmentAdditionalDraft(assignment)));
2428
+ const feedbackText = assignment.reviewer_note_md ?? (assignmentIndex === followUpBlocks.length - 1 ? selectedRecord.progress.review_feedback_md : null);
2429
+ return /* @__PURE__ */ jsxRuntime.jsxs(
2430
+ material.Accordion,
2431
+ {
2432
+ disableGutters: true,
2433
+ expanded: expandedFollowUpId === assignment.id,
2434
+ onChange: (_event, expanded) => setExpandedFollowUpId(expanded ? assignment.id : false),
2435
+ ref: isCurrentBlock ? activeFollowUpBlockRef : null,
2436
+ sx: {
2437
+ border: "1px solid",
2438
+ borderColor: "divider",
2439
+ borderRadius: 1,
2440
+ overflow: "hidden",
2441
+ "&:before": { display: "none" }
2442
+ },
2443
+ children: [
2444
+ /* @__PURE__ */ jsxRuntime.jsx(material.AccordionSummary, { expandIcon: /* @__PURE__ */ jsxRuntime.jsx(ExpandMoreIcon__default.default, {}), children: /* @__PURE__ */ jsxRuntime.jsxs(
2445
+ material.Stack,
2446
+ {
2447
+ direction: { xs: "column", sm: "row" },
2448
+ spacing: 1,
2449
+ justifyContent: "space-between",
2450
+ alignItems: { xs: "flex-start", sm: "center" },
2451
+ sx: { width: "100%" },
2452
+ children: [
2453
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "subtitle2", fontWeight: 700, children: [
2454
+ "Follow-up ",
2455
+ assignmentIndex + 1
2456
+ ] }),
2457
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { direction: "row", spacing: 0.75, flexWrap: "wrap", children: [
2458
+ /* @__PURE__ */ jsxRuntime.jsx(
2459
+ material.Chip,
2460
+ {
2461
+ size: "small",
2462
+ label: assignment.status === "pending" ? "Open" : assignment.status.replace("_", " "),
2463
+ color: assignment.status === "submitted" ? "info" : "default"
2464
+ }
2465
+ ),
2466
+ deltaSummary.total > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2467
+ /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { size: "small", color: "success", label: `Changed ${deltaSummary.changed}` }),
2468
+ /* @__PURE__ */ jsxRuntime.jsx(material.Chip, { size: "small", variant: "outlined", label: `Unchanged ${deltaSummary.unchanged}` })
2469
+ ] }) : null,
2470
+ /* @__PURE__ */ jsxRuntime.jsx(
2471
+ material.Chip,
2472
+ {
2473
+ size: "small",
2474
+ variant: "outlined",
2475
+ label: `Assignment response: ${hasAssignmentResponse ? "Yes" : "No"}`
2476
+ }
2477
+ )
2478
+ ] })
2479
+ ]
2480
+ }
2481
+ ) }),
2482
+ /* @__PURE__ */ jsxRuntime.jsx(material.AccordionDetails, { sx: { pt: 0 }, children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1.5, children: [
2483
+ feedbackText ? /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 0.75, children: [
2484
+ /* @__PURE__ */ jsxRuntime.jsx(
2485
+ material.Typography,
2486
+ {
2487
+ variant: "subtitle2",
2488
+ sx: { bgcolor: "action.hover", px: 1, py: 0.5, borderRadius: 1 },
2489
+ children: "Feedback"
2490
+ }
2491
+ ),
2492
+ /* @__PURE__ */ jsxRuntime.jsx(MDEditor3__default.default.Markdown, { source: feedbackText, style: markdownStyle, "data-color-mode": theme.palette.mode })
2493
+ ] }) : null,
2494
+ /* @__PURE__ */ jsxRuntime.jsx(material.Divider, {}),
2495
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", sx: { bgcolor: "action.hover", px: 1, py: 0.5, borderRadius: 1 }, children: "Assignment and response" }),
2496
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: "Follow-up assignment" }),
2497
+ assignment.prompt ? /* @__PURE__ */ jsxRuntime.jsx(MDEditor3__default.default.Markdown, { source: assignment.prompt, style: markdownStyle, "data-color-mode": theme.palette.mode }) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "No assignment prompt provided." }),
2498
+ assignment.response_mode === "written" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2499
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", fontWeight: 600, children: "Your response to this assignment" }),
2500
+ isEditable ? /* @__PURE__ */ jsxRuntime.jsx(
2501
+ MDEditor3__default.default,
2502
+ {
2503
+ value: getAssignmentResponseDraft(assignment),
2504
+ onChange: (value) => setAssignmentResponseDrafts((prev) => ({ ...prev, [assignment.id]: value ?? "" })),
2505
+ preview: "edit",
2506
+ textareaProps: { placeholder: "Respond directly to the follow-up assignment." },
2507
+ "data-color-mode": theme.palette.mode
2508
+ }
2509
+ ) : assignment.learner_assignment_response_md ?? assignment.learner_response_md ? /* @__PURE__ */ jsxRuntime.jsx(
2510
+ MDEditor3__default.default.Markdown,
2511
+ {
2512
+ source: assignment.learner_assignment_response_md ?? assignment.learner_response_md ?? "",
2513
+ style: markdownStyle,
2514
+ "data-color-mode": theme.palette.mode
2515
+ }
2516
+ ) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "No assignment response submitted." })
2517
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "This follow-up is marked as in-person." }),
2518
+ hasTargetedQuestions ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2519
+ /* @__PURE__ */ jsxRuntime.jsx(material.Divider, {}),
2520
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1, children: [
2521
+ /* @__PURE__ */ jsxRuntime.jsx(
2522
+ material.Typography,
2523
+ {
2524
+ variant: "subtitle2",
2525
+ sx: { bgcolor: "action.hover", px: 1, py: 0.5, borderRadius: 1 },
2526
+ children: "Targeted question updates"
2527
+ }
2528
+ ),
2529
+ stageGroups.map((group) => /* @__PURE__ */ jsxRuntime.jsx(material.Card, { variant: "outlined", children: /* @__PURE__ */ jsxRuntime.jsx(material.CardContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1.25, children: [
2530
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "subtitle2", fontWeight: 700, children: group.stage.title }),
2531
+ /* @__PURE__ */ jsxRuntime.jsx(MDEditor3__default.default.Markdown, { source: group.stage.content_md, style: markdownStyle, "data-color-mode": theme.palette.mode }),
2532
+ /* @__PURE__ */ jsxRuntime.jsx(material.Stack, { spacing: 1.5, children: group.questions.map(
2533
+ (question, questionIndex) => renderFollowUpQuestion(assignment, question, questionIndex, isEditable)
2534
+ ) })
2535
+ ] }) }) }, group.stage.id))
2536
+ ] })
2537
+ ] }) : null,
2538
+ /* @__PURE__ */ jsxRuntime.jsx(material.Divider, {}),
2539
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1, children: [
2540
+ /* @__PURE__ */ jsxRuntime.jsx(
2541
+ material.Typography,
2542
+ {
2543
+ variant: "subtitle2",
2544
+ sx: { bgcolor: "action.hover", px: 1, py: 0.5, borderRadius: 1 },
2545
+ children: "Additional response"
2546
+ }
2547
+ ),
2548
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: "Additional response (optional)" }),
2549
+ isEditable ? /* @__PURE__ */ jsxRuntime.jsx(
2550
+ MDEditor3__default.default,
2551
+ {
2552
+ value: getAssignmentAdditionalDraft(assignment),
2553
+ onChange: (value) => setAssignmentAdditionalDrafts((prev) => ({ ...prev, [assignment.id]: value ?? "" })),
2554
+ preview: "edit",
2555
+ textareaProps: { placeholder: "Add context, reflections, or extra details." },
2556
+ "data-color-mode": theme.palette.mode
2557
+ }
2558
+ ) : assignment.learner_additional_response_md ? /* @__PURE__ */ jsxRuntime.jsx(
2559
+ MDEditor3__default.default.Markdown,
2560
+ {
2561
+ source: assignment.learner_additional_response_md,
2562
+ style: markdownStyle,
2563
+ "data-color-mode": theme.palette.mode
2564
+ }
2565
+ ) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "No additional response submitted." }),
2566
+ !hasAdditionalResponse && !isEditable ? /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: "Additional response was not provided in this round." }) : null
2567
+ ] }),
2568
+ isEditable ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
2569
+ /* @__PURE__ */ jsxRuntime.jsx(material.Divider, {}),
2570
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 0.75, children: [
2571
+ /* @__PURE__ */ jsxRuntime.jsx(
2572
+ material.Button,
2573
+ {
2574
+ variant: "contained",
2575
+ size: "small",
2576
+ onClick: () => void handleSubmitAssignment(assignment),
2577
+ disabled: learning.isUpdatingAssignment || !canSubmitFollowUpBlock(assignment),
2578
+ sx: { alignSelf: "flex-start" },
2579
+ children: "Submit follow-up"
2580
+ }
2581
+ ),
2582
+ !canSubmitFollowUpBlock(assignment) ? /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: "Complete all required follow-up responses before submitting." }) : null
2583
+ ] })
2584
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "caption", color: "text.secondary", children: "This follow-up block is read-only." })
2585
+ ] }) })
2586
+ ]
2587
+ },
2588
+ assignment.id
2589
+ );
2590
+ }) })
2591
+ ] }) }) : null
2592
+ ] }) })
2593
+ ] }),
2594
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Dialog, { open: submitSuccessDialogOpen, onClose: () => setSubmitSuccessDialogOpen(false), fullWidth: true, maxWidth: "xs", children: [
2595
+ /* @__PURE__ */ jsxRuntime.jsx(material.DialogTitle, { children: "Great work!" }),
2596
+ /* @__PURE__ */ jsxRuntime.jsx(material.DialogContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1, children: [
2597
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "Your module was submitted for review." }),
2598
+ nextModuleTitle ? /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "body2", color: "text.secondary", children: [
2599
+ "Next up: ",
2600
+ nextModuleTitle
2601
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: "You have reached the end of the current module list." })
2602
+ ] }) }),
2603
+ /* @__PURE__ */ jsxRuntime.jsxs(material.DialogActions, { children: [
2604
+ /* @__PURE__ */ jsxRuntime.jsx(material.Button, { onClick: () => setSubmitSuccessDialogOpen(false), children: "Close" }),
2605
+ /* @__PURE__ */ jsxRuntime.jsx(
2606
+ material.Button,
2607
+ {
2608
+ variant: "contained",
2609
+ disabled: !nextModuleId,
2610
+ onClick: () => {
2611
+ if (!nextModuleId) {
2612
+ setSubmitSuccessDialogOpen(false);
2613
+ return;
2614
+ }
2615
+ setSubmitSuccessDialogOpen(false);
2616
+ void handleSelectModule(nextModuleId);
2617
+ },
2618
+ children: "Continue to next section"
2619
+ }
2620
+ )
2621
+ ] })
2622
+ ] }),
2623
+ /* @__PURE__ */ jsxRuntime.jsxs(
2624
+ material.Dialog,
2625
+ {
2626
+ open: Boolean(followUpSummaryDialog),
2627
+ onClose: () => setFollowUpSummaryDialog(null),
2628
+ fullWidth: true,
2629
+ maxWidth: "xs",
2630
+ children: [
2631
+ /* @__PURE__ */ jsxRuntime.jsx(material.DialogTitle, { children: "Follow-up submitted" }),
2632
+ /* @__PURE__ */ jsxRuntime.jsx(material.DialogContent, { children: /* @__PURE__ */ jsxRuntime.jsxs(material.Stack, { spacing: 1, children: [
2633
+ /* @__PURE__ */ jsxRuntime.jsxs(material.Typography, { variant: "body2", color: "text.secondary", children: [
2634
+ "You updated ",
2635
+ followUpSummaryDialog?.updatedQuestions ?? 0,
2636
+ " targeted question",
2637
+ (followUpSummaryDialog?.updatedQuestions ?? 0) === 1 ? "" : "s",
2638
+ "."
2639
+ ] }),
2640
+ /* @__PURE__ */ jsxRuntime.jsx(material.Typography, { variant: "body2", color: "text.secondary", children: followUpSummaryDialog?.hasAssignmentResponse ? "Your assignment response was included." : "No assignment response was included." })
2641
+ ] }) }),
2642
+ /* @__PURE__ */ jsxRuntime.jsx(material.DialogActions, { children: /* @__PURE__ */ jsxRuntime.jsx(material.Button, { onClick: () => setFollowUpSummaryDialog(null), children: "Close" }) })
2643
+ ]
2644
+ }
2645
+ )
2646
+ ] });
2647
+ };
2648
+
2649
+ exports.HandbookLmsCurriculumEditor = HandbookLmsCurriculumEditor;
2650
+ exports.HandbookLmsLearningWorkspace = HandbookLmsLearningWorkspace;
2651
+ exports.HandbookLmsReviewWorkspace = HandbookLmsReviewWorkspace;
2652
+ //# sourceMappingURL=index.cjs.map
2653
+ //# sourceMappingURL=index.cjs.map