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