@burdenoff/microfe-buildmyiq 2026.720.5 → 2026.725.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssessmentsPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/assessments/AssessmentsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAqB,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAoEnD,eAAO,MAAM,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"AssessmentsPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/assessments/AssessmentsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAqB,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAoEnD,eAAO,MAAM,eAAe,EAAE,EAyZ7B,CAAC"}
|
|
@@ -2,22 +2,22 @@ import { useBuildmyiq as e } from "../../context/BuildmyiqContext.js";
|
|
|
2
2
|
import "../../context/index.js";
|
|
3
3
|
import { PageLayout as t } from "../../components/PageLayout.js";
|
|
4
4
|
import { MetricsCard as n } from "../../components/MetricsCard.js";
|
|
5
|
-
import { formatEnumLabel as
|
|
6
|
-
import { StatusBadge as
|
|
7
|
-
import { EmptyState as
|
|
8
|
-
import { ErrorView as
|
|
5
|
+
import { formatEnumLabel as r, formatRelativeTime as ee } from "../../utils/formatters.js";
|
|
6
|
+
import { StatusBadge as i } from "../../components/StatusBadge.js";
|
|
7
|
+
import { EmptyState as a } from "../../components/EmptyState.js";
|
|
8
|
+
import { ErrorView as te, LoadingView as ne } from "../../components/StateViews.js";
|
|
9
9
|
import { DataTable as o } from "../../components/DataTable.js";
|
|
10
10
|
import { FilterTabs as s } from "../../components/FilterTabs.js";
|
|
11
11
|
import { useBuiAssessments as c, useBuiCourses as l, useBuiCreateAssessment as u } from "../../hooks/useBuildmyiqApi.js";
|
|
12
12
|
import "../../hooks/index.js";
|
|
13
13
|
import "../../components/index.js";
|
|
14
|
-
import { ASSESSMENT_TYPES as
|
|
15
|
-
import { useMemo as
|
|
16
|
-
import { CheckCircle2 as
|
|
17
|
-
import { jsx as
|
|
18
|
-
import { Button as
|
|
14
|
+
import { ASSESSMENT_TYPES as d } from "../../types/index.js";
|
|
15
|
+
import { useMemo as f, useState as p } from "react";
|
|
16
|
+
import { CheckCircle2 as re, ClipboardList as m, FileEdit as ie, ListChecks as ae, Plus as h } from "lucide-react";
|
|
17
|
+
import { jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
18
|
+
import { Button as v, GlassCard as y, Input as b, Label as x, ResponsiveDialog as S, ResponsiveDialogContent as C, ResponsiveDialogDescription as w, ResponsiveDialogFooter as oe, ResponsiveDialogHeader as se, ResponsiveDialogTitle as ce, Select as T, SelectContent as E, SelectItem as D, SelectTrigger as O, SelectValue as k, Switch as A, Textarea as le } from "@burdenoff/fe-libs/ui";
|
|
19
19
|
//#region src/buildmyiq/pages/assessments/AssessmentsPage.tsx
|
|
20
|
-
var
|
|
20
|
+
var j = [
|
|
21
21
|
{
|
|
22
22
|
value: "ALL",
|
|
23
23
|
label: "All"
|
|
@@ -34,7 +34,7 @@ var M = [
|
|
|
34
34
|
value: "ARCHIVED",
|
|
35
35
|
label: "Archived"
|
|
36
36
|
}
|
|
37
|
-
],
|
|
37
|
+
], M = () => ({
|
|
38
38
|
title: "",
|
|
39
39
|
description: "",
|
|
40
40
|
type: "QUIZ",
|
|
@@ -42,8 +42,8 @@ var M = [
|
|
|
42
42
|
maxAttempts: "1",
|
|
43
43
|
timeLimitMin: "0",
|
|
44
44
|
isGraded: !0
|
|
45
|
-
}),
|
|
46
|
-
let { navigateTo:
|
|
45
|
+
}), N = () => {
|
|
46
|
+
let { navigateTo: N } = e(), [P, F] = p(""), [I, L] = p("ALL"), [R, z] = p(!1), [B, V] = p(M), [H, U] = p(null), W = l({ limit: 100 }), G = c(P || void 0), K = c(P || void 0, "ARCHIVED"), q = u(), J = W.data?.items ?? [], Y = f(() => [...G.data ?? [], ...K.data ?? []], [G.data, K.data]), X = f(() => {
|
|
47
47
|
let e = {
|
|
48
48
|
ALL: Y.length,
|
|
49
49
|
DRAFT: 0,
|
|
@@ -52,20 +52,22 @@ var M = [
|
|
|
52
52
|
};
|
|
53
53
|
for (let t of Y) e[t.status] += 1;
|
|
54
54
|
return e;
|
|
55
|
-
}, [Y]), Z =
|
|
55
|
+
}, [Y]), Z = f(() => I === "ALL" ? Y : Y.filter((e) => e.status === I), [Y, I]), ue = f(() => Y.reduce((e, t) => e + (t.questions?.length ?? 0), 0), [Y]), de = j.map((e) => ({
|
|
56
56
|
value: e.value,
|
|
57
57
|
label: e.label,
|
|
58
58
|
count: X[e.value]
|
|
59
|
-
})),
|
|
59
|
+
})), fe = G.isLoading || K.isLoading, pe = G.isError || K.isError, me = G.isSuccess && K.isSuccess, he = () => {
|
|
60
|
+
G.refetch(), K.refetch();
|
|
61
|
+
}, ge = [
|
|
60
62
|
{
|
|
61
63
|
key: "title",
|
|
62
64
|
header: "Assessment",
|
|
63
|
-
render: (e) => /* @__PURE__ */
|
|
65
|
+
render: (e) => /* @__PURE__ */ _("div", {
|
|
64
66
|
className: "min-w-0",
|
|
65
|
-
children: [/* @__PURE__ */
|
|
67
|
+
children: [/* @__PURE__ */ g("p", {
|
|
66
68
|
className: "font-semibold text-text-primary",
|
|
67
69
|
children: e.title
|
|
68
|
-
}), e.description && /* @__PURE__ */
|
|
70
|
+
}), e.description && /* @__PURE__ */ g("p", {
|
|
69
71
|
className: "mt-0.5 max-w-md truncate text-xs text-text-secondary",
|
|
70
72
|
title: e.description,
|
|
71
73
|
children: e.description
|
|
@@ -75,13 +77,13 @@ var M = [
|
|
|
75
77
|
{
|
|
76
78
|
key: "type",
|
|
77
79
|
header: "Type",
|
|
78
|
-
render: (e) => /* @__PURE__ */
|
|
80
|
+
render: (e) => /* @__PURE__ */ g(i, { status: e.type })
|
|
79
81
|
},
|
|
80
82
|
{
|
|
81
83
|
key: "status",
|
|
82
84
|
header: "Status",
|
|
83
85
|
sortValue: (e) => e.status,
|
|
84
|
-
render: (e) => /* @__PURE__ */
|
|
86
|
+
render: (e) => /* @__PURE__ */ g(i, { status: e.status })
|
|
85
87
|
},
|
|
86
88
|
{
|
|
87
89
|
key: "passingScore",
|
|
@@ -89,7 +91,7 @@ var M = [
|
|
|
89
91
|
align: "right",
|
|
90
92
|
hideOnMobile: !0,
|
|
91
93
|
sortValue: (e) => e.passingScore,
|
|
92
|
-
render: (e) => /* @__PURE__ */
|
|
94
|
+
render: (e) => /* @__PURE__ */ _("span", {
|
|
93
95
|
className: "tabular-nums text-text-secondary",
|
|
94
96
|
children: [e.passingScore, "%"]
|
|
95
97
|
})
|
|
@@ -99,7 +101,7 @@ var M = [
|
|
|
99
101
|
header: "Questions",
|
|
100
102
|
align: "right",
|
|
101
103
|
hideOnMobile: !0,
|
|
102
|
-
render: (e) => /* @__PURE__ */
|
|
104
|
+
render: (e) => /* @__PURE__ */ g("span", {
|
|
103
105
|
className: "tabular-nums text-text-secondary",
|
|
104
106
|
children: e.questions?.length ?? "—"
|
|
105
107
|
})
|
|
@@ -109,298 +111,298 @@ var M = [
|
|
|
109
111
|
header: "Updated",
|
|
110
112
|
hideOnMobile: !0,
|
|
111
113
|
sortValue: (e) => e.updatedAt,
|
|
112
|
-
render: (e) => /* @__PURE__ */
|
|
114
|
+
render: (e) => /* @__PURE__ */ g("span", {
|
|
113
115
|
className: "whitespace-nowrap text-text-secondary",
|
|
114
|
-
children:
|
|
116
|
+
children: ee(e.updatedAt)
|
|
115
117
|
})
|
|
116
118
|
}
|
|
117
119
|
], Q = () => {
|
|
118
|
-
|
|
120
|
+
V(M()), U(null), z(!0);
|
|
119
121
|
}, $ = () => {
|
|
120
|
-
|
|
122
|
+
z(!1), V(M()), U(null);
|
|
121
123
|
};
|
|
122
|
-
return /* @__PURE__ */
|
|
124
|
+
return /* @__PURE__ */ _(t, {
|
|
123
125
|
title: "Assessments",
|
|
124
126
|
description: "Quizzes, exams and graded assignments for every course in your workspace.",
|
|
125
127
|
titleGradient: !0,
|
|
126
128
|
aurora: !0,
|
|
127
|
-
actions: /* @__PURE__ */ v
|
|
129
|
+
actions: /* @__PURE__ */ _(v, {
|
|
128
130
|
onClick: Q,
|
|
129
|
-
disabled: !
|
|
130
|
-
children: [/* @__PURE__ */
|
|
131
|
+
disabled: !P,
|
|
132
|
+
children: [/* @__PURE__ */ g(h, { className: "mr-1.5 h-4 w-4" }), " New assessment"]
|
|
131
133
|
}),
|
|
132
134
|
children: [
|
|
133
|
-
/* @__PURE__ */
|
|
135
|
+
/* @__PURE__ */ g(y, {
|
|
134
136
|
treatment: "plain",
|
|
135
137
|
className: "mb-5 p-4",
|
|
136
|
-
children: /* @__PURE__ */
|
|
138
|
+
children: /* @__PURE__ */ _("div", {
|
|
137
139
|
className: "flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end",
|
|
138
|
-
children: [/* @__PURE__ */
|
|
140
|
+
children: [/* @__PURE__ */ _("div", {
|
|
139
141
|
className: "grid gap-1.5",
|
|
140
142
|
children: [
|
|
141
|
-
/* @__PURE__ */
|
|
143
|
+
/* @__PURE__ */ g("span", {
|
|
142
144
|
className: "text-xs font-medium uppercase tracking-wide text-text-secondary",
|
|
143
145
|
children: "Course"
|
|
144
146
|
}),
|
|
145
|
-
/* @__PURE__ */
|
|
146
|
-
value:
|
|
147
|
+
/* @__PURE__ */ _(T, {
|
|
148
|
+
value: P,
|
|
147
149
|
onValueChange: (e) => {
|
|
148
|
-
|
|
150
|
+
F(e), L("ALL");
|
|
149
151
|
},
|
|
150
|
-
children: [/* @__PURE__ */
|
|
152
|
+
children: [/* @__PURE__ */ g(O, {
|
|
151
153
|
className: "w-full sm:w-72",
|
|
152
|
-
children: /* @__PURE__ */
|
|
153
|
-
}), /* @__PURE__ */
|
|
154
|
+
children: /* @__PURE__ */ g(k, { placeholder: "Select a course" })
|
|
155
|
+
}), /* @__PURE__ */ g(E, { children: J.map((e) => /* @__PURE__ */ g(D, {
|
|
154
156
|
value: e.id,
|
|
155
157
|
children: e.title
|
|
156
158
|
}, e.id)) })]
|
|
157
159
|
}),
|
|
158
|
-
|
|
160
|
+
W.isSuccess && J.length === 0 && /* @__PURE__ */ g("p", {
|
|
159
161
|
className: "text-xs text-text-secondary",
|
|
160
162
|
children: "No courses yet — create one in Authoring first."
|
|
161
163
|
})
|
|
162
164
|
]
|
|
163
|
-
}),
|
|
165
|
+
}), P && /* @__PURE__ */ _("div", {
|
|
164
166
|
className: "grid gap-1.5",
|
|
165
|
-
children: [/* @__PURE__ */
|
|
167
|
+
children: [/* @__PURE__ */ g("span", {
|
|
166
168
|
className: "text-xs font-medium uppercase tracking-wide text-text-secondary",
|
|
167
169
|
children: "Status"
|
|
168
|
-
}), /* @__PURE__ */
|
|
169
|
-
options:
|
|
170
|
-
value:
|
|
171
|
-
onChange:
|
|
170
|
+
}), /* @__PURE__ */ g(s, {
|
|
171
|
+
options: de,
|
|
172
|
+
value: I,
|
|
173
|
+
onChange: L
|
|
172
174
|
})]
|
|
173
175
|
})]
|
|
174
176
|
})
|
|
175
177
|
}),
|
|
176
|
-
|
|
178
|
+
P && me && Y.length > 0 && /* @__PURE__ */ _("div", {
|
|
177
179
|
className: "mb-5 grid grid-cols-2 gap-4 lg:grid-cols-4",
|
|
178
180
|
children: [
|
|
179
|
-
/* @__PURE__ */
|
|
180
|
-
icon: /* @__PURE__ */
|
|
181
|
+
/* @__PURE__ */ g(n, {
|
|
182
|
+
icon: /* @__PURE__ */ g(m, { className: "h-5 w-5" }),
|
|
181
183
|
value: X.ALL,
|
|
182
184
|
label: "Assessments"
|
|
183
185
|
}),
|
|
184
|
-
/* @__PURE__ */
|
|
185
|
-
icon: /* @__PURE__ */
|
|
186
|
+
/* @__PURE__ */ g(n, {
|
|
187
|
+
icon: /* @__PURE__ */ g(re, { className: "h-5 w-5" }),
|
|
186
188
|
value: X.PUBLISHED,
|
|
187
189
|
label: "Published",
|
|
188
190
|
accent: "success"
|
|
189
191
|
}),
|
|
190
|
-
/* @__PURE__ */
|
|
191
|
-
icon: /* @__PURE__ */
|
|
192
|
+
/* @__PURE__ */ g(n, {
|
|
193
|
+
icon: /* @__PURE__ */ g(ie, { className: "h-5 w-5" }),
|
|
192
194
|
value: X.DRAFT,
|
|
193
195
|
label: "Drafts"
|
|
194
196
|
}),
|
|
195
|
-
/* @__PURE__ */
|
|
196
|
-
icon: /* @__PURE__ */
|
|
197
|
-
value:
|
|
197
|
+
/* @__PURE__ */ g(n, {
|
|
198
|
+
icon: /* @__PURE__ */ g(ae, { className: "h-5 w-5" }),
|
|
199
|
+
value: ue,
|
|
198
200
|
label: "Questions"
|
|
199
201
|
})
|
|
200
202
|
]
|
|
201
203
|
}),
|
|
202
|
-
|
|
204
|
+
P ? fe ? /* @__PURE__ */ g(ne, { label: "Loading assessments…" }) : pe ? /* @__PURE__ */ g(te, {
|
|
203
205
|
message: "Couldn't load assessments for this course.",
|
|
204
|
-
onRetry:
|
|
205
|
-
}) : Z.length === 0 ? /* @__PURE__ */
|
|
206
|
+
onRetry: he
|
|
207
|
+
}) : Z.length === 0 ? /* @__PURE__ */ g(a, {
|
|
206
208
|
illustration: "empty-data",
|
|
207
|
-
icon: /* @__PURE__ */
|
|
208
|
-
title:
|
|
209
|
-
description:
|
|
210
|
-
action: /* @__PURE__ */ v
|
|
209
|
+
icon: /* @__PURE__ */ g(m, { className: "h-10 w-10" }),
|
|
210
|
+
title: I === "ALL" ? "No assessments yet" : "No assessments match this filter",
|
|
211
|
+
description: I === "ALL" ? "Create the first quiz, exam or assignment for this course." : "Try a different status filter, or create a new assessment.",
|
|
212
|
+
action: /* @__PURE__ */ _(v, {
|
|
211
213
|
onClick: Q,
|
|
212
|
-
children: [/* @__PURE__ */
|
|
214
|
+
children: [/* @__PURE__ */ g(h, { className: "mr-1.5 h-4 w-4" }), " New assessment"]
|
|
213
215
|
})
|
|
214
|
-
}) : /* @__PURE__ */
|
|
215
|
-
columns:
|
|
216
|
+
}) : /* @__PURE__ */ g(o, {
|
|
217
|
+
columns: ge,
|
|
216
218
|
rows: Z,
|
|
217
219
|
rowKey: (e) => e.id,
|
|
218
|
-
onRowClick: (e) =>
|
|
220
|
+
onRowClick: (e) => N(`assessments/${e.id}`),
|
|
219
221
|
defaultSort: {
|
|
220
222
|
key: "updatedAt",
|
|
221
223
|
dir: "desc"
|
|
222
224
|
}
|
|
223
|
-
}) : /* @__PURE__ */
|
|
225
|
+
}) : /* @__PURE__ */ g(a, {
|
|
224
226
|
illustration: "onboarding",
|
|
225
|
-
icon: /* @__PURE__ */
|
|
227
|
+
icon: /* @__PURE__ */ g(m, { className: "h-10 w-10" }),
|
|
226
228
|
title: "Pick a course to see its assessments",
|
|
227
229
|
description: "Assessments are scoped to a course. Choose a course above to view, filter and create its quizzes and exams."
|
|
228
230
|
}),
|
|
229
|
-
/* @__PURE__ */
|
|
230
|
-
open:
|
|
231
|
+
/* @__PURE__ */ g(S, {
|
|
232
|
+
open: R,
|
|
231
233
|
onOpenChange: (e) => {
|
|
232
|
-
e ?
|
|
234
|
+
e ? z(!0) : $();
|
|
233
235
|
},
|
|
234
|
-
children: /* @__PURE__ */
|
|
235
|
-
/* @__PURE__ */
|
|
236
|
-
/* @__PURE__ */
|
|
236
|
+
children: /* @__PURE__ */ _(C, { children: [
|
|
237
|
+
/* @__PURE__ */ _(se, { children: [/* @__PURE__ */ g(ce, { children: "New assessment" }), /* @__PURE__ */ g(w, { children: "Add a quiz, exam or assignment to this course. You can add questions after it's created." })] }),
|
|
238
|
+
/* @__PURE__ */ _("div", {
|
|
237
239
|
className: "grid gap-4 py-2",
|
|
238
240
|
children: [
|
|
239
|
-
/* @__PURE__ */
|
|
241
|
+
/* @__PURE__ */ _("div", {
|
|
240
242
|
className: "grid gap-1.5",
|
|
241
|
-
children: [/* @__PURE__ */
|
|
243
|
+
children: [/* @__PURE__ */ g(x, {
|
|
242
244
|
htmlFor: "assessmentTitle",
|
|
243
245
|
children: "Title *"
|
|
244
|
-
}), /* @__PURE__ */
|
|
246
|
+
}), /* @__PURE__ */ g(b, {
|
|
245
247
|
id: "assessmentTitle",
|
|
246
|
-
value:
|
|
247
|
-
onChange: (e) =>
|
|
248
|
-
...
|
|
248
|
+
value: B.title,
|
|
249
|
+
onChange: (e) => V({
|
|
250
|
+
...B,
|
|
249
251
|
title: e.target.value
|
|
250
252
|
}),
|
|
251
253
|
placeholder: "e.g. Module 1 knowledge check"
|
|
252
254
|
})]
|
|
253
255
|
}),
|
|
254
|
-
/* @__PURE__ */
|
|
256
|
+
/* @__PURE__ */ _("div", {
|
|
255
257
|
className: "grid gap-1.5",
|
|
256
|
-
children: [/* @__PURE__ */
|
|
258
|
+
children: [/* @__PURE__ */ g(x, {
|
|
257
259
|
htmlFor: "assessmentDescription",
|
|
258
260
|
children: "Description"
|
|
259
|
-
}), /* @__PURE__ */
|
|
261
|
+
}), /* @__PURE__ */ g(le, {
|
|
260
262
|
id: "assessmentDescription",
|
|
261
|
-
value:
|
|
262
|
-
onChange: (e) =>
|
|
263
|
-
...
|
|
263
|
+
value: B.description,
|
|
264
|
+
onChange: (e) => V({
|
|
265
|
+
...B,
|
|
264
266
|
description: e.target.value
|
|
265
267
|
}),
|
|
266
268
|
placeholder: "What this assessment covers and how it's graded.",
|
|
267
269
|
rows: 2
|
|
268
270
|
})]
|
|
269
271
|
}),
|
|
270
|
-
/* @__PURE__ */
|
|
272
|
+
/* @__PURE__ */ _("div", {
|
|
271
273
|
className: "grid gap-2 sm:grid-cols-2",
|
|
272
|
-
children: [/* @__PURE__ */
|
|
274
|
+
children: [/* @__PURE__ */ _("div", {
|
|
273
275
|
className: "grid gap-1.5",
|
|
274
|
-
children: [/* @__PURE__ */
|
|
276
|
+
children: [/* @__PURE__ */ g(x, {
|
|
275
277
|
htmlFor: "assessmentType",
|
|
276
278
|
children: "Type"
|
|
277
|
-
}), /* @__PURE__ */
|
|
278
|
-
value:
|
|
279
|
-
onValueChange: (e) =>
|
|
280
|
-
...
|
|
279
|
+
}), /* @__PURE__ */ _(T, {
|
|
280
|
+
value: B.type,
|
|
281
|
+
onValueChange: (e) => V({
|
|
282
|
+
...B,
|
|
281
283
|
type: e
|
|
282
284
|
}),
|
|
283
|
-
children: [/* @__PURE__ */
|
|
285
|
+
children: [/* @__PURE__ */ g(O, {
|
|
284
286
|
id: "assessmentType",
|
|
285
|
-
children: /* @__PURE__ */
|
|
286
|
-
}), /* @__PURE__ */
|
|
287
|
+
children: /* @__PURE__ */ g(k, {})
|
|
288
|
+
}), /* @__PURE__ */ g(E, { children: d.map((e) => /* @__PURE__ */ g(D, {
|
|
287
289
|
value: e,
|
|
288
|
-
children:
|
|
290
|
+
children: r(e)
|
|
289
291
|
}, e)) })]
|
|
290
292
|
})]
|
|
291
|
-
}), /* @__PURE__ */
|
|
293
|
+
}), /* @__PURE__ */ _("div", {
|
|
292
294
|
className: "grid gap-1.5",
|
|
293
|
-
children: [/* @__PURE__ */
|
|
295
|
+
children: [/* @__PURE__ */ g(x, {
|
|
294
296
|
htmlFor: "passingScore",
|
|
295
297
|
children: "Passing score (%)"
|
|
296
|
-
}), /* @__PURE__ */
|
|
298
|
+
}), /* @__PURE__ */ g(b, {
|
|
297
299
|
id: "passingScore",
|
|
298
300
|
type: "number",
|
|
299
301
|
min: "0",
|
|
300
302
|
max: "100",
|
|
301
303
|
step: "1",
|
|
302
|
-
value:
|
|
303
|
-
onChange: (e) =>
|
|
304
|
-
...
|
|
304
|
+
value: B.passingScore,
|
|
305
|
+
onChange: (e) => V({
|
|
306
|
+
...B,
|
|
305
307
|
passingScore: e.target.value
|
|
306
308
|
}),
|
|
307
309
|
placeholder: "70"
|
|
308
310
|
})]
|
|
309
311
|
})]
|
|
310
312
|
}),
|
|
311
|
-
/* @__PURE__ */
|
|
313
|
+
/* @__PURE__ */ _("div", {
|
|
312
314
|
className: "grid gap-2 sm:grid-cols-2",
|
|
313
|
-
children: [/* @__PURE__ */
|
|
315
|
+
children: [/* @__PURE__ */ _("div", {
|
|
314
316
|
className: "grid gap-1.5",
|
|
315
|
-
children: [/* @__PURE__ */
|
|
317
|
+
children: [/* @__PURE__ */ g(x, {
|
|
316
318
|
htmlFor: "maxAttempts",
|
|
317
319
|
children: "Max attempts"
|
|
318
|
-
}), /* @__PURE__ */
|
|
320
|
+
}), /* @__PURE__ */ g(b, {
|
|
319
321
|
id: "maxAttempts",
|
|
320
322
|
type: "number",
|
|
321
323
|
min: "1",
|
|
322
324
|
step: "1",
|
|
323
|
-
value:
|
|
324
|
-
onChange: (e) =>
|
|
325
|
-
...
|
|
325
|
+
value: B.maxAttempts,
|
|
326
|
+
onChange: (e) => V({
|
|
327
|
+
...B,
|
|
326
328
|
maxAttempts: e.target.value
|
|
327
329
|
}),
|
|
328
330
|
placeholder: "1"
|
|
329
331
|
})]
|
|
330
|
-
}), /* @__PURE__ */
|
|
332
|
+
}), /* @__PURE__ */ _("div", {
|
|
331
333
|
className: "grid gap-1.5",
|
|
332
|
-
children: [/* @__PURE__ */
|
|
334
|
+
children: [/* @__PURE__ */ g(x, {
|
|
333
335
|
htmlFor: "timeLimitMin",
|
|
334
336
|
children: "Time limit (minutes)"
|
|
335
|
-
}), /* @__PURE__ */
|
|
337
|
+
}), /* @__PURE__ */ g(b, {
|
|
336
338
|
id: "timeLimitMin",
|
|
337
339
|
type: "number",
|
|
338
340
|
min: "0",
|
|
339
341
|
step: "1",
|
|
340
|
-
value:
|
|
341
|
-
onChange: (e) =>
|
|
342
|
-
...
|
|
342
|
+
value: B.timeLimitMin,
|
|
343
|
+
onChange: (e) => V({
|
|
344
|
+
...B,
|
|
343
345
|
timeLimitMin: e.target.value
|
|
344
346
|
}),
|
|
345
347
|
placeholder: "0 = no limit"
|
|
346
348
|
})]
|
|
347
349
|
})]
|
|
348
350
|
}),
|
|
349
|
-
/* @__PURE__ */
|
|
351
|
+
/* @__PURE__ */ _("div", {
|
|
350
352
|
className: "flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface px-3 py-2.5",
|
|
351
|
-
children: [/* @__PURE__ */
|
|
353
|
+
children: [/* @__PURE__ */ _("div", {
|
|
352
354
|
className: "min-w-0",
|
|
353
|
-
children: [/* @__PURE__ */
|
|
355
|
+
children: [/* @__PURE__ */ g(x, {
|
|
354
356
|
htmlFor: "isGraded",
|
|
355
357
|
className: "cursor-pointer",
|
|
356
358
|
children: "Graded"
|
|
357
|
-
}), /* @__PURE__ */
|
|
359
|
+
}), /* @__PURE__ */ g("p", {
|
|
358
360
|
className: "text-xs text-text-secondary",
|
|
359
361
|
children: "Contributes to the learner's score and pass/fail outcome."
|
|
360
362
|
})]
|
|
361
|
-
}), /* @__PURE__ */
|
|
363
|
+
}), /* @__PURE__ */ g(A, {
|
|
362
364
|
id: "isGraded",
|
|
363
|
-
checked:
|
|
364
|
-
onCheckedChange: (e) =>
|
|
365
|
-
...
|
|
365
|
+
checked: B.isGraded,
|
|
366
|
+
onCheckedChange: (e) => V({
|
|
367
|
+
...B,
|
|
366
368
|
isGraded: e
|
|
367
369
|
})
|
|
368
370
|
})]
|
|
369
371
|
}),
|
|
370
|
-
|
|
372
|
+
H && /* @__PURE__ */ g("p", {
|
|
371
373
|
className: "text-sm text-status-error-text",
|
|
372
|
-
children:
|
|
374
|
+
children: H
|
|
373
375
|
})
|
|
374
376
|
]
|
|
375
377
|
}),
|
|
376
|
-
/* @__PURE__ */
|
|
378
|
+
/* @__PURE__ */ _(oe, { children: [/* @__PURE__ */ g(v, {
|
|
377
379
|
variant: "outline",
|
|
378
380
|
onClick: $,
|
|
379
381
|
children: "Cancel"
|
|
380
|
-
}), /* @__PURE__ */
|
|
382
|
+
}), /* @__PURE__ */ g(v, {
|
|
381
383
|
onClick: async () => {
|
|
382
|
-
if (!
|
|
383
|
-
|
|
384
|
+
if (!P) {
|
|
385
|
+
U("Pick a course first.");
|
|
384
386
|
return;
|
|
385
387
|
}
|
|
386
|
-
if (!
|
|
387
|
-
|
|
388
|
+
if (!B.title.trim()) {
|
|
389
|
+
U("Title is required.");
|
|
388
390
|
return;
|
|
389
391
|
}
|
|
390
392
|
try {
|
|
391
393
|
let e = await q.mutateAsync({
|
|
392
|
-
courseId:
|
|
393
|
-
title:
|
|
394
|
-
description:
|
|
395
|
-
type:
|
|
396
|
-
passingScore:
|
|
397
|
-
maxAttempts:
|
|
398
|
-
timeLimitMin:
|
|
399
|
-
isGraded:
|
|
394
|
+
courseId: P,
|
|
395
|
+
title: B.title.trim(),
|
|
396
|
+
description: B.description.trim() || void 0,
|
|
397
|
+
type: B.type,
|
|
398
|
+
passingScore: B.passingScore ? Number(B.passingScore) : void 0,
|
|
399
|
+
maxAttempts: B.maxAttempts ? Number(B.maxAttempts) : void 0,
|
|
400
|
+
timeLimitMin: B.timeLimitMin ? Number(B.timeLimitMin) : void 0,
|
|
401
|
+
isGraded: B.isGraded
|
|
400
402
|
});
|
|
401
|
-
$(),
|
|
403
|
+
$(), N(`assessments/${e.id}`);
|
|
402
404
|
} catch (e) {
|
|
403
|
-
|
|
405
|
+
U(e instanceof Error ? e.message : "Failed to create assessment.");
|
|
404
406
|
}
|
|
405
407
|
},
|
|
406
408
|
disabled: q.isPending,
|
|
@@ -412,6 +414,6 @@ var M = [
|
|
|
412
414
|
});
|
|
413
415
|
};
|
|
414
416
|
//#endregion
|
|
415
|
-
export {
|
|
417
|
+
export { N as AssessmentsPage };
|
|
416
418
|
|
|
417
419
|
//# sourceMappingURL=AssessmentsPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssessmentsPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/AssessmentsPage.tsx"],"sourcesContent":["/**\n * AssessmentsPage — the assessment authoring surface (primary page of the section).\n * @module buildmyiq/pages/assessments\n *\n * `buiListAssessments` REQUIRES a courseId, so this page is course-scoped: the\n * educator first picks a course, then sees its assessments filtered by status.\n * Everything is real `bui*` data — no fabrication. Create flow wires\n * `useBuiCreateAssessment`.\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { Plus, ClipboardList, CheckCircle2, FileEdit, ListChecks } from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Label,\n Textarea,\n Switch,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n MetricsCard,\n StatusBadge,\n FilterTabs,\n DataTable,\n EmptyState,\n LoadingView,\n ErrorView,\n type Column,\n type FilterOption,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiCourses, useBuiAssessments, useBuiCreateAssessment } from '../../hooks';\nimport { formatEnumLabel, formatRelativeTime } from '../../utils/formatters';\nimport { ASSESSMENT_TYPES } from '../../types';\nimport type { BuiAssessment, BuiAssessmentStatus, BuiAssessmentType } from '../../types';\n\ntype StatusFilter = 'ALL' | BuiAssessmentStatus;\n\nconst STATUS_TABS: { value: StatusFilter; label: string }[] = [\n { value: 'ALL', label: 'All' },\n { value: 'DRAFT', label: 'Draft' },\n { value: 'PUBLISHED', label: 'Published' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\n\ninterface AssessmentFormState {\n title: string;\n description: string;\n type: BuiAssessmentType;\n passingScore: string;\n maxAttempts: string;\n timeLimitMin: string;\n isGraded: boolean;\n}\n\nconst makeEmptyForm = (): AssessmentFormState => ({\n title: '',\n description: '',\n type: 'QUIZ',\n passingScore: '70',\n maxAttempts: '1',\n timeLimitMin: '0',\n isGraded: true,\n});\n\nexport const AssessmentsPage: FC = () => {\n const { navigateTo } = useBuildmyiq();\n\n const [courseId, setCourseId] = useState<string>('');\n const [status, setStatus] = useState<StatusFilter>('ALL');\n const [dialogOpen, setDialogOpen] = useState(false);\n const [form, setForm] = useState<AssessmentFormState>(makeEmptyForm);\n const [formError, setFormError] = useState<string | null>(null);\n\n const coursesQuery = useBuiCourses({ limit: 100 });\n // Fetch the full (unfiltered) list so the tab counts stay accurate; filter client-side.\n const assessmentsQuery = useBuiAssessments(courseId || undefined);\n const createAssessment = useBuiCreateAssessment();\n\n const courses = coursesQuery.data?.items ?? [];\n const assessments = useMemo(() => assessmentsQuery.data ?? [], [assessmentsQuery.data]);\n\n const statusCounts = useMemo(() => {\n const counts: Record<StatusFilter, number> = {\n ALL: assessments.length,\n DRAFT: 0,\n PUBLISHED: 0,\n ARCHIVED: 0,\n };\n for (const a of assessments) counts[a.status] += 1;\n return counts;\n }, [assessments]);\n\n const rows = useMemo(\n () => (status === 'ALL' ? assessments : assessments.filter((a) => a.status === status)),\n [assessments, status]\n );\n\n const totalQuestions = useMemo(\n () => assessments.reduce((sum, a) => sum + (a.questions?.length ?? 0), 0),\n [assessments]\n );\n\n const tabOptions: FilterOption<StatusFilter>[] = STATUS_TABS.map((t) => ({\n value: t.value,\n label: t.label,\n count: statusCounts[t.value],\n }));\n\n const columns: Column<BuiAssessment>[] = [\n {\n key: 'title',\n header: 'Assessment',\n render: (a) => (\n <div className=\"min-w-0\">\n <p className=\"font-semibold text-text-primary\">{a.title}</p>\n {a.description && (\n <p\n className=\"mt-0.5 max-w-md truncate text-xs text-text-secondary\"\n title={a.description}\n >\n {a.description}\n </p>\n )}\n </div>\n ),\n },\n {\n key: 'type',\n header: 'Type',\n render: (a) => <StatusBadge status={a.type} />,\n },\n {\n key: 'status',\n header: 'Status',\n sortValue: (a) => a.status,\n render: (a) => <StatusBadge status={a.status} />,\n },\n {\n key: 'passingScore',\n header: 'Pass',\n align: 'right',\n hideOnMobile: true,\n sortValue: (a) => a.passingScore,\n render: (a) => <span className=\"tabular-nums text-text-secondary\">{a.passingScore}%</span>,\n },\n {\n key: 'questions',\n header: 'Questions',\n align: 'right',\n hideOnMobile: true,\n render: (a) => (\n <span className=\"tabular-nums text-text-secondary\">{a.questions?.length ?? '—'}</span>\n ),\n },\n {\n key: 'updatedAt',\n header: 'Updated',\n hideOnMobile: true,\n sortValue: (a) => a.updatedAt,\n render: (a) => (\n <span className=\"whitespace-nowrap text-text-secondary\">\n {formatRelativeTime(a.updatedAt)}\n </span>\n ),\n },\n ];\n\n const openDialog = () => {\n setForm(makeEmptyForm());\n setFormError(null);\n setDialogOpen(true);\n };\n\n const closeDialog = () => {\n setDialogOpen(false);\n setForm(makeEmptyForm());\n setFormError(null);\n };\n\n const handleCreate = async () => {\n if (!courseId) {\n setFormError('Pick a course first.');\n return;\n }\n if (!form.title.trim()) {\n setFormError('Title is required.');\n return;\n }\n try {\n const created = await createAssessment.mutateAsync({\n courseId,\n title: form.title.trim(),\n description: form.description.trim() || undefined,\n type: form.type,\n passingScore: form.passingScore ? Number(form.passingScore) : undefined,\n maxAttempts: form.maxAttempts ? Number(form.maxAttempts) : undefined,\n timeLimitMin: form.timeLimitMin ? Number(form.timeLimitMin) : undefined,\n isGraded: form.isGraded,\n });\n closeDialog();\n navigateTo(`assessments/${created.id}`);\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to create assessment.');\n }\n };\n\n return (\n <PageLayout\n title=\"Assessments\"\n description=\"Quizzes, exams and graded assignments for every course in your workspace.\"\n titleGradient\n aurora\n actions={\n <Button onClick={openDialog} disabled={!courseId}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New assessment\n </Button>\n }\n >\n <GlassCard treatment=\"plain\" className=\"mb-5 p-4\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end\">\n <div className=\"grid gap-1.5\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Course\n </span>\n <Select\n value={courseId}\n onValueChange={(v) => {\n setCourseId(v);\n setStatus('ALL');\n }}\n >\n <SelectTrigger className=\"w-full sm:w-72\">\n <SelectValue placeholder=\"Select a course\" />\n </SelectTrigger>\n <SelectContent>\n {courses.map((c) => (\n <SelectItem key={c.id} value={c.id}>\n {c.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {coursesQuery.isSuccess && courses.length === 0 && (\n <p className=\"text-xs text-text-secondary\">\n No courses yet — create one in Authoring first.\n </p>\n )}\n </div>\n {courseId && (\n <div className=\"grid gap-1.5\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Status\n </span>\n <FilterTabs options={tabOptions} value={status} onChange={setStatus} />\n </div>\n )}\n </div>\n </GlassCard>\n\n {courseId && assessmentsQuery.isSuccess && assessments.length > 0 && (\n <div className=\"mb-5 grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <MetricsCard\n icon={<ClipboardList className=\"h-5 w-5\" />}\n value={statusCounts.ALL}\n label=\"Assessments\"\n />\n <MetricsCard\n icon={<CheckCircle2 className=\"h-5 w-5\" />}\n value={statusCounts.PUBLISHED}\n label=\"Published\"\n accent=\"success\"\n />\n <MetricsCard\n icon={<FileEdit className=\"h-5 w-5\" />}\n value={statusCounts.DRAFT}\n label=\"Drafts\"\n />\n <MetricsCard\n icon={<ListChecks className=\"h-5 w-5\" />}\n value={totalQuestions}\n label=\"Questions\"\n />\n </div>\n )}\n\n {!courseId ? (\n <EmptyState\n illustration=\"onboarding\"\n icon={<ClipboardList className=\"h-10 w-10\" />}\n title=\"Pick a course to see its assessments\"\n description=\"Assessments are scoped to a course. Choose a course above to view, filter and create its quizzes and exams.\"\n />\n ) : assessmentsQuery.isLoading ? (\n <LoadingView label=\"Loading assessments…\" />\n ) : assessmentsQuery.isError ? (\n <ErrorView\n message=\"Couldn't load assessments for this course.\"\n onRetry={() => assessmentsQuery.refetch()}\n />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<ClipboardList className=\"h-10 w-10\" />}\n title={status === 'ALL' ? 'No assessments yet' : 'No assessments match this filter'}\n description={\n status === 'ALL'\n ? 'Create the first quiz, exam or assignment for this course.'\n : 'Try a different status filter, or create a new assessment.'\n }\n action={\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New assessment\n </Button>\n }\n />\n ) : (\n <DataTable\n columns={columns}\n rows={rows}\n rowKey={(a) => a.id}\n onRowClick={(a) => navigateTo(`assessments/${a.id}`)}\n defaultSort={{ key: 'updatedAt', dir: 'desc' }}\n />\n )}\n\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n if (!open) closeDialog();\n else setDialogOpen(true);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New assessment</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Add a quiz, exam or assignment to this course. You can add questions after it's\n created.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"assessmentTitle\">Title *</Label>\n <Input\n id=\"assessmentTitle\"\n value={form.title}\n onChange={(e) => setForm({ ...form, title: e.target.value })}\n placeholder=\"e.g. Module 1 knowledge check\"\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"assessmentDescription\">Description</Label>\n <Textarea\n id=\"assessmentDescription\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n placeholder=\"What this assessment covers and how it's graded.\"\n rows={2}\n />\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"assessmentType\">Type</Label>\n <Select\n value={form.type}\n onValueChange={(v) => setForm({ ...form, type: v as BuiAssessmentType })}\n >\n <SelectTrigger id=\"assessmentType\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ASSESSMENT_TYPES.map((t) => (\n <SelectItem key={t} value={t}>\n {formatEnumLabel(t)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"passingScore\">Passing score (%)</Label>\n <Input\n id=\"passingScore\"\n type=\"number\"\n min=\"0\"\n max=\"100\"\n step=\"1\"\n value={form.passingScore}\n onChange={(e) => setForm({ ...form, passingScore: e.target.value })}\n placeholder=\"70\"\n />\n </div>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"maxAttempts\">Max attempts</Label>\n <Input\n id=\"maxAttempts\"\n type=\"number\"\n min=\"1\"\n step=\"1\"\n value={form.maxAttempts}\n onChange={(e) => setForm({ ...form, maxAttempts: e.target.value })}\n placeholder=\"1\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"timeLimitMin\">Time limit (minutes)</Label>\n <Input\n id=\"timeLimitMin\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={form.timeLimitMin}\n onChange={(e) => setForm({ ...form, timeLimitMin: e.target.value })}\n placeholder=\"0 = no limit\"\n />\n </div>\n </div>\n\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface px-3 py-2.5\">\n <div className=\"min-w-0\">\n <Label htmlFor=\"isGraded\" className=\"cursor-pointer\">\n Graded\n </Label>\n <p className=\"text-xs text-text-secondary\">\n Contributes to the learner's score and pass/fail outcome.\n </p>\n </div>\n <Switch\n id=\"isGraded\"\n checked={form.isGraded}\n onCheckedChange={(checked) => setForm({ ...form, isGraded: checked })}\n />\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={closeDialog}>\n Cancel\n </Button>\n <Button onClick={handleCreate} disabled={createAssessment.isPending}>\n {createAssessment.isPending ? 'Creating…' : 'Create assessment'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmDA,IAAM,IAAwD;CAC5D;EAAE,OAAO;EAAO,OAAO;CAAM;CAC7B;EAAE,OAAO;EAAS,OAAO;CAAQ;CACjC;EAAE,OAAO;EAAa,OAAO;CAAY;CACzC;EAAE,OAAO;EAAY,OAAO;CAAW;AACzC,GAYM,WAA4C;CAChD,OAAO;CACP,aAAa;CACb,MAAM;CACN,cAAc;CACd,aAAa;CACb,cAAc;CACd,UAAU;AACZ,IAEa,UAA4B;CACvC,IAAM,EAAE,kBAAe,EAAa,GAE9B,CAAC,GAAU,KAAe,EAAiB,EAAE,GAC7C,CAAC,GAAQ,KAAa,EAAuB,KAAK,GAClD,CAAC,GAAY,KAAiB,EAAS,EAAK,GAC5C,CAAC,GAAM,KAAW,EAA8B,CAAa,GAC7D,CAAC,GAAW,KAAgB,EAAwB,IAAI,GAExD,IAAe,EAAc,EAAE,OAAO,IAAI,CAAC,GAE3C,IAAmB,EAAkB,KAAY,KAAA,CAAS,GAC1D,IAAmB,EAAuB,GAE1C,IAAU,EAAa,MAAM,SAAS,CAAC,GACvC,IAAc,QAAc,EAAiB,QAAQ,CAAC,GAAG,CAAC,EAAiB,IAAI,CAAC,GAEhF,IAAe,QAAc;EACjC,IAAM,IAAuC;GAC3C,KAAK,EAAY;GACjB,OAAO;GACP,WAAW;GACX,UAAU;EACZ;EACA,KAAK,IAAM,KAAK,GAAa,EAAO,EAAE,WAAW;EACjD,OAAO;CACT,GAAG,CAAC,CAAW,CAAC,GAEV,IAAO,QACJ,MAAW,QAAQ,IAAc,EAAY,QAAQ,MAAM,EAAE,WAAW,CAAM,GACrF,CAAC,GAAa,CAAM,CACtB,GAEM,KAAiB,QACf,EAAY,QAAQ,GAAK,MAAM,KAAO,EAAE,WAAW,UAAU,IAAI,CAAC,GACxE,CAAC,CAAW,CACd,GAEM,KAA2C,EAAY,KAAK,OAAO;EACvE,OAAO,EAAE;EACT,OAAO,EAAE;EACT,OAAO,EAAa,EAAE;CACxB,EAAE,GAEI,KAAmC;EACvC;GACE,KAAK;GACL,QAAQ;GACR,SAAS,MACP,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eAAmC,EAAE;IAAS,CAAA,GAC1D,EAAE,eACD,kBAAC,KAAD;KACE,WAAU;KACV,OAAO,EAAE;eAER,EAAE;IACF,CAAA,CAEF;;EAET;EACA;GACE,KAAK;GACL,QAAQ;GACR,SAAS,MAAM,kBAAC,GAAD,EAAa,QAAQ,EAAE,KAAO,CAAA;EAC/C;EACA;GACE,KAAK;GACL,QAAQ;GACR,YAAY,MAAM,EAAE;GACpB,SAAS,MAAM,kBAAC,GAAD,EAAa,QAAQ,EAAE,OAAS,CAAA;EACjD;EACA;GACE,KAAK;GACL,QAAQ;GACR,OAAO;GACP,cAAc;GACd,YAAY,MAAM,EAAE;GACpB,SAAS,MAAM,kBAAC,QAAD;IAAM,WAAU;cAAhB,CAAoD,EAAE,cAAa,GAAO;;EAC3F;EACA;GACE,KAAK;GACL,QAAQ;GACR,OAAO;GACP,cAAc;GACd,SAAS,MACP,kBAAC,QAAD;IAAM,WAAU;cAAoC,EAAE,WAAW,UAAU;GAAU,CAAA;EAEzF;EACA;GACE,KAAK;GACL,QAAQ;GACR,cAAc;GACd,YAAY,MAAM,EAAE;GACpB,SAAS,MACP,kBAAC,QAAD;IAAM,WAAU;cACb,GAAmB,EAAE,SAAS;GAC3B,CAAA;EAEV;CACF,GAEM,UAAmB;EAGvB,AAFA,EAAQ,EAAc,CAAC,GACvB,EAAa,IAAI,GACjB,EAAc,EAAI;CACpB,GAEM,UAAoB;EAGxB,AAFA,EAAc,EAAK,GACnB,EAAQ,EAAc,CAAC,GACvB,EAAa,IAAI;CACnB;CA6BA,OACE,kBAAC,GAAD;EACE,OAAM;EACN,aAAY;EACZ,eAAA;EACA,QAAA;EACA,SACE,kBAAC,GAAD;GAAQ,SAAS;GAAY,UAAU,CAAC;aAAxC,CACE,kBAAC,GAAD,EAAM,WAAU,iBAAkB,CAAA,GAAC,iBAC7B;;YARZ;GAWE,kBAAC,GAAD;IAAW,WAAU;IAAQ,WAAU;cACrC,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,QAAD;QAAM,WAAU;kBAAkE;OAE5E,CAAA;OACN,kBAAC,GAAD;QACE,OAAO;QACP,gBAAgB,MAAM;SAEpB,AADA,EAAY,CAAC,GACb,EAAU,KAAK;QACjB;kBALF,CAOE,kBAAC,GAAD;SAAe,WAAU;mBACvB,kBAAC,GAAD,EAAa,aAAY,kBAAmB,CAAA;QAC/B,CAAA,GACf,kBAAC,GAAD,EAAA,UACG,EAAQ,KAAK,MACZ,kBAAC,GAAD;SAAuB,OAAO,EAAE;mBAC7B,EAAE;QACO,GAFK,EAAE,EAEP,CACb,EACY,CAAA,CACT;;OACP,EAAa,aAAa,EAAQ,WAAW,KAC5C,kBAAC,KAAD;QAAG,WAAU;kBAA8B;OAExC,CAAA;MAEF;SACJ,KACC,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,QAAD;OAAM,WAAU;iBAAkE;MAE5E,CAAA,GACN,kBAAC,GAAD;OAAY,SAAS;OAAY,OAAO;OAAQ,UAAU;MAAY,CAAA,CACnE;OAEJ;;GACI,CAAA;GAEV,KAAY,EAAiB,aAAa,EAAY,SAAS,KAC9D,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAe,WAAU,UAAW,CAAA;MAC1C,OAAO,EAAa;MACpB,OAAM;KACP,CAAA;KACD,kBAAC,GAAD;MACE,MAAM,kBAAC,IAAD,EAAc,WAAU,UAAW,CAAA;MACzC,OAAO,EAAa;MACpB,OAAM;MACN,QAAO;KACR,CAAA;KACD,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAU,WAAU,UAAW,CAAA;MACrC,OAAO,EAAa;MACpB,OAAM;KACP,CAAA;KACD,kBAAC,GAAD;MACE,MAAM,kBAAC,GAAD,EAAY,WAAU,UAAW,CAAA;MACvC,OAAO;MACP,OAAM;KACP,CAAA;IACE;;GAGL,IAOE,EAAiB,YACnB,kBAAC,GAAD,EAAa,OAAM,uBAAwB,CAAA,IACzC,EAAiB,UACnB,kBAAC,IAAD;IACE,SAAQ;IACR,eAAe,EAAiB,QAAQ;GACzC,CAAA,IACC,EAAK,WAAW,IAClB,kBAAC,GAAD;IACE,cAAa;IACb,MAAM,kBAAC,GAAD,EAAe,WAAU,YAAa,CAAA;IAC5C,OAAO,MAAW,QAAQ,uBAAuB;IACjD,aACE,MAAW,QACP,+DACA;IAEN,QACE,kBAAC,GAAD;KAAQ,SAAS;eAAjB,CACE,kBAAC,GAAD,EAAM,WAAU,iBAAkB,CAAA,GAAC,iBAC7B;;GAEX,CAAA,IAED,kBAAC,GAAD;IACW;IACH;IACN,SAAS,MAAM,EAAE;IACjB,aAAa,MAAM,EAAW,eAAe,EAAE,IAAI;IACnD,aAAa;KAAE,KAAK;KAAa,KAAK;IAAO;GAC9C,CAAA,IApCD,kBAAC,GAAD;IACE,cAAa;IACb,MAAM,kBAAC,GAAD,EAAe,WAAU,YAAa,CAAA;IAC5C,OAAM;IACN,aAAY;GACb,CAAA;GAkCH,kBAAC,GAAD;IACE,MAAM;IACN,eAAe,MAAS;KACtB,AAAK,IACA,EAAc,EAAI,IADZ,EAAY;IAEzB;cAEA,kBAAC,GAAD,EAAA,UAAA;KACE,kBAAC,IAAD,EAAA,UAAA,CACE,kBAAC,IAAD,EAAA,UAAuB,iBAAqC,CAAA,GAC5D,kBAAC,IAAD,EAAA,UAA6B,2FAGA,CAAA,CACP,EAAA,CAAA;KAExB,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAkB;QAAc,CAAA,GAC/C,kBAAC,GAAD;SACE,IAAG;SACH,OAAO,EAAK;SACZ,WAAW,MAAM,EAAQ;UAAE,GAAG;UAAM,OAAO,EAAE,OAAO;SAAM,CAAC;SAC3D,aAAY;QACb,CAAA,CACE;;OAEL,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SAAO,SAAQ;mBAAwB;QAAkB,CAAA,GACzD,kBAAC,GAAD;SACE,IAAG;SACH,OAAO,EAAK;SACZ,WAAW,MAAM,EAAQ;UAAE,GAAG;UAAM,aAAa,EAAE,OAAO;SAAM,CAAC;SACjE,aAAY;SACZ,MAAM;QACP,CAAA,CACE;;OAEL,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,GAAD;UAAO,SAAQ;oBAAiB;SAAW,CAAA,GAC3C,kBAAC,GAAD;UACE,OAAO,EAAK;UACZ,gBAAgB,MAAM,EAAQ;WAAE,GAAG;WAAM,MAAM;UAAuB,CAAC;oBAFzE,CAIE,kBAAC,GAAD;WAAe,IAAG;qBAChB,kBAAC,GAAD,CAAc,CAAA;UACD,CAAA,GACf,kBAAC,GAAD,EAAA,UACG,GAAiB,KAAK,MACrB,kBAAC,GAAD;WAAoB,OAAO;qBACxB,GAAgB,CAAC;UACR,GAFK,CAEL,CACb,EACY,CAAA,CACT;WACL;YACL,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,GAAD;UAAO,SAAQ;oBAAe;SAAwB,CAAA,GACtD,kBAAC,GAAD;UACE,IAAG;UACH,MAAK;UACL,KAAI;UACJ,KAAI;UACJ,MAAK;UACL,OAAO,EAAK;UACZ,WAAW,MAAM,EAAQ;WAAE,GAAG;WAAM,cAAc,EAAE,OAAO;UAAM,CAAC;UAClE,aAAY;SACb,CAAA,CACE;UACF;;OAEL,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,GAAD;UAAO,SAAQ;oBAAc;SAAmB,CAAA,GAChD,kBAAC,GAAD;UACE,IAAG;UACH,MAAK;UACL,KAAI;UACJ,MAAK;UACL,OAAO,EAAK;UACZ,WAAW,MAAM,EAAQ;WAAE,GAAG;WAAM,aAAa,EAAE,OAAO;UAAM,CAAC;UACjE,aAAY;SACb,CAAA,CACE;YACL,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,GAAD;UAAO,SAAQ;oBAAe;SAA2B,CAAA,GACzD,kBAAC,GAAD;UACE,IAAG;UACH,MAAK;UACL,KAAI;UACJ,MAAK;UACL,OAAO,EAAK;UACZ,WAAW,MAAM,EAAQ;WAAE,GAAG;WAAM,cAAc,EAAE,OAAO;UAAM,CAAC;UAClE,aAAY;SACb,CAAA,CACE;UACF;;OAEL,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,GAAD;UAAO,SAAQ;UAAW,WAAU;oBAAiB;SAE9C,CAAA,GACP,kBAAC,KAAD;UAAG,WAAU;oBAA8B;SAExC,CAAA,CACA;YACL,kBAAC,GAAD;SACE,IAAG;SACH,SAAS,EAAK;SACd,kBAAkB,MAAY,EAAQ;UAAE,GAAG;UAAM,UAAU;SAAQ,CAAC;QACrE,CAAA,CACE;;OAEJ,KAAa,kBAAC,KAAD;QAAG,WAAU;kBAAkC;OAAa,CAAA;MACvE;;KAEL,kBAAC,IAAD,EAAA,UAAA,CACE,kBAAC,GAAD;MAAQ,SAAQ;MAAU,SAAS;gBAAa;KAExC,CAAA,GACR,kBAAC,GAAD;MAAQ,SAAS,YA5QM;OAC/B,IAAI,CAAC,GAAU;QACb,EAAa,sBAAsB;QACnC;OACF;OACA,IAAI,CAAC,EAAK,MAAM,KAAK,GAAG;QACtB,EAAa,oBAAoB;QACjC;OACF;OACA,IAAI;QACF,IAAM,IAAU,MAAM,EAAiB,YAAY;SACjD;SACA,OAAO,EAAK,MAAM,KAAK;SACvB,aAAa,EAAK,YAAY,KAAK,KAAK,KAAA;SACxC,MAAM,EAAK;SACX,cAAc,EAAK,eAAe,OAAO,EAAK,YAAY,IAAI,KAAA;SAC9D,aAAa,EAAK,cAAc,OAAO,EAAK,WAAW,IAAI,KAAA;SAC3D,cAAc,EAAK,eAAe,OAAO,EAAK,YAAY,IAAI,KAAA;SAC9D,UAAU,EAAK;QACjB,CAAC;QAED,AADA,EAAY,GACZ,EAAW,eAAe,EAAQ,IAAI;OACxC,SAAS,GAAK;QACZ,EAAa,aAAe,QAAQ,EAAI,UAAU,8BAA8B;OAClF;MACF;MAmPyC,UAAU,EAAiB;gBACvD,EAAiB,YAAY,cAAc;KACtC,CAAA,CACc,EAAA,CAAA;IACD,EAAA,CAAA;GACT,CAAA;EACR;;AAEhB"}
|
|
1
|
+
{"version":3,"file":"AssessmentsPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/assessments/AssessmentsPage.tsx"],"sourcesContent":["/**\n * AssessmentsPage — the assessment authoring surface (primary page of the section).\n * @module buildmyiq/pages/assessments\n *\n * `buiListAssessments` REQUIRES a courseId, so this page is course-scoped: the\n * educator first picks a course, then sees its assessments filtered by status.\n * Everything is real `bui*` data — no fabrication. Create flow wires\n * `useBuiCreateAssessment`.\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { Plus, ClipboardList, CheckCircle2, FileEdit, ListChecks } from 'lucide-react';\nimport {\n Button,\n GlassCard,\n Input,\n Label,\n Textarea,\n Switch,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n ResponsiveDialog,\n ResponsiveDialogContent,\n ResponsiveDialogHeader,\n ResponsiveDialogTitle,\n ResponsiveDialogDescription,\n ResponsiveDialogFooter,\n} from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n MetricsCard,\n StatusBadge,\n FilterTabs,\n DataTable,\n EmptyState,\n LoadingView,\n ErrorView,\n type Column,\n type FilterOption,\n} from '../../components';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiCourses, useBuiAssessments, useBuiCreateAssessment } from '../../hooks';\nimport { formatEnumLabel, formatRelativeTime } from '../../utils/formatters';\nimport { ASSESSMENT_TYPES } from '../../types';\nimport type { BuiAssessment, BuiAssessmentStatus, BuiAssessmentType } from '../../types';\n\ntype StatusFilter = 'ALL' | BuiAssessmentStatus;\n\nconst STATUS_TABS: { value: StatusFilter; label: string }[] = [\n { value: 'ALL', label: 'All' },\n { value: 'DRAFT', label: 'Draft' },\n { value: 'PUBLISHED', label: 'Published' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\n\ninterface AssessmentFormState {\n title: string;\n description: string;\n type: BuiAssessmentType;\n passingScore: string;\n maxAttempts: string;\n timeLimitMin: string;\n isGraded: boolean;\n}\n\nconst makeEmptyForm = (): AssessmentFormState => ({\n title: '',\n description: '',\n type: 'QUIZ',\n passingScore: '70',\n maxAttempts: '1',\n timeLimitMin: '0',\n isGraded: true,\n});\n\nexport const AssessmentsPage: FC = () => {\n const { navigateTo } = useBuildmyiq();\n\n const [courseId, setCourseId] = useState<string>('');\n const [status, setStatus] = useState<StatusFilter>('ALL');\n const [dialogOpen, setDialogOpen] = useState(false);\n const [form, setForm] = useState<AssessmentFormState>(makeEmptyForm);\n const [formError, setFormError] = useState<string | null>(null);\n\n const coursesQuery = useBuiCourses({ limit: 100 });\n // Fetch the unfiltered list for DRAFT/PUBLISHED/ALL (so those tab counts stay\n // accurate and switching between them is instant) plus a dedicated\n // `status: ARCHIVED` query. The two are merged below because\n // `buiListAssessments` scopes its soft-delete filter to the requested status\n // (BOFF-4780): an unfiltered/undefined-status call always excludes archived\n // (soft-deleted) rows, so they can never surface via client-side filtering\n // alone — they must be fetched explicitly.\n const assessmentsQuery = useBuiAssessments(courseId || undefined);\n const archivedAssessmentsQuery = useBuiAssessments(courseId || undefined, 'ARCHIVED');\n const createAssessment = useBuiCreateAssessment();\n\n const courses = coursesQuery.data?.items ?? [];\n const assessments = useMemo(\n () => [...(assessmentsQuery.data ?? []), ...(archivedAssessmentsQuery.data ?? [])],\n [assessmentsQuery.data, archivedAssessmentsQuery.data]\n );\n\n const statusCounts = useMemo(() => {\n const counts: Record<StatusFilter, number> = {\n ALL: assessments.length,\n DRAFT: 0,\n PUBLISHED: 0,\n ARCHIVED: 0,\n };\n for (const a of assessments) counts[a.status] += 1;\n return counts;\n }, [assessments]);\n\n const rows = useMemo(\n () => (status === 'ALL' ? assessments : assessments.filter((a) => a.status === status)),\n [assessments, status]\n );\n\n const totalQuestions = useMemo(\n () => assessments.reduce((sum, a) => sum + (a.questions?.length ?? 0), 0),\n [assessments]\n );\n\n const tabOptions: FilterOption<StatusFilter>[] = STATUS_TABS.map((t) => ({\n value: t.value,\n label: t.label,\n count: statusCounts[t.value],\n }));\n\n const assessmentsIsLoading = assessmentsQuery.isLoading || archivedAssessmentsQuery.isLoading;\n const assessmentsIsError = assessmentsQuery.isError || archivedAssessmentsQuery.isError;\n const assessmentsIsSuccess = assessmentsQuery.isSuccess && archivedAssessmentsQuery.isSuccess;\n const refetchAssessments = () => {\n void assessmentsQuery.refetch();\n void archivedAssessmentsQuery.refetch();\n };\n\n const columns: Column<BuiAssessment>[] = [\n {\n key: 'title',\n header: 'Assessment',\n render: (a) => (\n <div className=\"min-w-0\">\n <p className=\"font-semibold text-text-primary\">{a.title}</p>\n {a.description && (\n <p\n className=\"mt-0.5 max-w-md truncate text-xs text-text-secondary\"\n title={a.description}\n >\n {a.description}\n </p>\n )}\n </div>\n ),\n },\n {\n key: 'type',\n header: 'Type',\n render: (a) => <StatusBadge status={a.type} />,\n },\n {\n key: 'status',\n header: 'Status',\n sortValue: (a) => a.status,\n render: (a) => <StatusBadge status={a.status} />,\n },\n {\n key: 'passingScore',\n header: 'Pass',\n align: 'right',\n hideOnMobile: true,\n sortValue: (a) => a.passingScore,\n render: (a) => <span className=\"tabular-nums text-text-secondary\">{a.passingScore}%</span>,\n },\n {\n key: 'questions',\n header: 'Questions',\n align: 'right',\n hideOnMobile: true,\n render: (a) => (\n <span className=\"tabular-nums text-text-secondary\">{a.questions?.length ?? '—'}</span>\n ),\n },\n {\n key: 'updatedAt',\n header: 'Updated',\n hideOnMobile: true,\n sortValue: (a) => a.updatedAt,\n render: (a) => (\n <span className=\"whitespace-nowrap text-text-secondary\">\n {formatRelativeTime(a.updatedAt)}\n </span>\n ),\n },\n ];\n\n const openDialog = () => {\n setForm(makeEmptyForm());\n setFormError(null);\n setDialogOpen(true);\n };\n\n const closeDialog = () => {\n setDialogOpen(false);\n setForm(makeEmptyForm());\n setFormError(null);\n };\n\n const handleCreate = async () => {\n if (!courseId) {\n setFormError('Pick a course first.');\n return;\n }\n if (!form.title.trim()) {\n setFormError('Title is required.');\n return;\n }\n try {\n const created = await createAssessment.mutateAsync({\n courseId,\n title: form.title.trim(),\n description: form.description.trim() || undefined,\n type: form.type,\n passingScore: form.passingScore ? Number(form.passingScore) : undefined,\n maxAttempts: form.maxAttempts ? Number(form.maxAttempts) : undefined,\n timeLimitMin: form.timeLimitMin ? Number(form.timeLimitMin) : undefined,\n isGraded: form.isGraded,\n });\n closeDialog();\n navigateTo(`assessments/${created.id}`);\n } catch (err) {\n setFormError(err instanceof Error ? err.message : 'Failed to create assessment.');\n }\n };\n\n return (\n <PageLayout\n title=\"Assessments\"\n description=\"Quizzes, exams and graded assignments for every course in your workspace.\"\n titleGradient\n aurora\n actions={\n <Button onClick={openDialog} disabled={!courseId}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New assessment\n </Button>\n }\n >\n <GlassCard treatment=\"plain\" className=\"mb-5 p-4\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end\">\n <div className=\"grid gap-1.5\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Course\n </span>\n <Select\n value={courseId}\n onValueChange={(v) => {\n setCourseId(v);\n setStatus('ALL');\n }}\n >\n <SelectTrigger className=\"w-full sm:w-72\">\n <SelectValue placeholder=\"Select a course\" />\n </SelectTrigger>\n <SelectContent>\n {courses.map((c) => (\n <SelectItem key={c.id} value={c.id}>\n {c.title}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {coursesQuery.isSuccess && courses.length === 0 && (\n <p className=\"text-xs text-text-secondary\">\n No courses yet — create one in Authoring first.\n </p>\n )}\n </div>\n {courseId && (\n <div className=\"grid gap-1.5\">\n <span className=\"text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Status\n </span>\n <FilterTabs options={tabOptions} value={status} onChange={setStatus} />\n </div>\n )}\n </div>\n </GlassCard>\n\n {courseId && assessmentsIsSuccess && assessments.length > 0 && (\n <div className=\"mb-5 grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <MetricsCard\n icon={<ClipboardList className=\"h-5 w-5\" />}\n value={statusCounts.ALL}\n label=\"Assessments\"\n />\n <MetricsCard\n icon={<CheckCircle2 className=\"h-5 w-5\" />}\n value={statusCounts.PUBLISHED}\n label=\"Published\"\n accent=\"success\"\n />\n <MetricsCard\n icon={<FileEdit className=\"h-5 w-5\" />}\n value={statusCounts.DRAFT}\n label=\"Drafts\"\n />\n <MetricsCard\n icon={<ListChecks className=\"h-5 w-5\" />}\n value={totalQuestions}\n label=\"Questions\"\n />\n </div>\n )}\n\n {!courseId ? (\n <EmptyState\n illustration=\"onboarding\"\n icon={<ClipboardList className=\"h-10 w-10\" />}\n title=\"Pick a course to see its assessments\"\n description=\"Assessments are scoped to a course. Choose a course above to view, filter and create its quizzes and exams.\"\n />\n ) : assessmentsIsLoading ? (\n <LoadingView label=\"Loading assessments…\" />\n ) : assessmentsIsError ? (\n <ErrorView\n message=\"Couldn't load assessments for this course.\"\n onRetry={refetchAssessments}\n />\n ) : rows.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<ClipboardList className=\"h-10 w-10\" />}\n title={status === 'ALL' ? 'No assessments yet' : 'No assessments match this filter'}\n description={\n status === 'ALL'\n ? 'Create the first quiz, exam or assignment for this course.'\n : 'Try a different status filter, or create a new assessment.'\n }\n action={\n <Button onClick={openDialog}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New assessment\n </Button>\n }\n />\n ) : (\n <DataTable\n columns={columns}\n rows={rows}\n rowKey={(a) => a.id}\n onRowClick={(a) => navigateTo(`assessments/${a.id}`)}\n defaultSort={{ key: 'updatedAt', dir: 'desc' }}\n />\n )}\n\n <ResponsiveDialog\n open={dialogOpen}\n onOpenChange={(open) => {\n if (!open) closeDialog();\n else setDialogOpen(true);\n }}\n >\n <ResponsiveDialogContent>\n <ResponsiveDialogHeader>\n <ResponsiveDialogTitle>New assessment</ResponsiveDialogTitle>\n <ResponsiveDialogDescription>\n Add a quiz, exam or assignment to this course. You can add questions after it's\n created.\n </ResponsiveDialogDescription>\n </ResponsiveDialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"assessmentTitle\">Title *</Label>\n <Input\n id=\"assessmentTitle\"\n value={form.title}\n onChange={(e) => setForm({ ...form, title: e.target.value })}\n placeholder=\"e.g. Module 1 knowledge check\"\n />\n </div>\n\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"assessmentDescription\">Description</Label>\n <Textarea\n id=\"assessmentDescription\"\n value={form.description}\n onChange={(e) => setForm({ ...form, description: e.target.value })}\n placeholder=\"What this assessment covers and how it's graded.\"\n rows={2}\n />\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"assessmentType\">Type</Label>\n <Select\n value={form.type}\n onValueChange={(v) => setForm({ ...form, type: v as BuiAssessmentType })}\n >\n <SelectTrigger id=\"assessmentType\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {ASSESSMENT_TYPES.map((t) => (\n <SelectItem key={t} value={t}>\n {formatEnumLabel(t)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"passingScore\">Passing score (%)</Label>\n <Input\n id=\"passingScore\"\n type=\"number\"\n min=\"0\"\n max=\"100\"\n step=\"1\"\n value={form.passingScore}\n onChange={(e) => setForm({ ...form, passingScore: e.target.value })}\n placeholder=\"70\"\n />\n </div>\n </div>\n\n <div className=\"grid gap-2 sm:grid-cols-2\">\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"maxAttempts\">Max attempts</Label>\n <Input\n id=\"maxAttempts\"\n type=\"number\"\n min=\"1\"\n step=\"1\"\n value={form.maxAttempts}\n onChange={(e) => setForm({ ...form, maxAttempts: e.target.value })}\n placeholder=\"1\"\n />\n </div>\n <div className=\"grid gap-1.5\">\n <Label htmlFor=\"timeLimitMin\">Time limit (minutes)</Label>\n <Input\n id=\"timeLimitMin\"\n type=\"number\"\n min=\"0\"\n step=\"1\"\n value={form.timeLimitMin}\n onChange={(e) => setForm({ ...form, timeLimitMin: e.target.value })}\n placeholder=\"0 = no limit\"\n />\n </div>\n </div>\n\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface px-3 py-2.5\">\n <div className=\"min-w-0\">\n <Label htmlFor=\"isGraded\" className=\"cursor-pointer\">\n Graded\n </Label>\n <p className=\"text-xs text-text-secondary\">\n Contributes to the learner's score and pass/fail outcome.\n </p>\n </div>\n <Switch\n id=\"isGraded\"\n checked={form.isGraded}\n onCheckedChange={(checked) => setForm({ ...form, isGraded: checked })}\n />\n </div>\n\n {formError && <p className=\"text-sm text-status-error-text\">{formError}</p>}\n </div>\n\n <ResponsiveDialogFooter>\n <Button variant=\"outline\" onClick={closeDialog}>\n Cancel\n </Button>\n <Button onClick={handleCreate} disabled={createAssessment.isPending}>\n {createAssessment.isPending ? 'Creating…' : 'Create assessment'}\n </Button>\n </ResponsiveDialogFooter>\n </ResponsiveDialogContent>\n </ResponsiveDialog>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|