@hed-hog/lms 0.0.319 → 0.0.320
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.
- package/dist/class-group/class-group.controller.d.ts +65 -2
- package/dist/class-group/class-group.controller.d.ts.map +1 -1
- package/dist/class-group/class-group.controller.js +35 -0
- package/dist/class-group/class-group.controller.js.map +1 -1
- package/dist/class-group/class-group.service.d.ts +67 -2
- package/dist/class-group/class-group.service.d.ts.map +1 -1
- package/dist/class-group/class-group.service.js +164 -13
- package/dist/class-group/class-group.service.js.map +1 -1
- package/dist/class-group/dto/create-class-group.dto.d.ts.map +1 -1
- package/dist/class-group/dto/create-class-group.dto.js +2 -1
- package/dist/class-group/dto/create-class-group.dto.js.map +1 -1
- package/dist/class-group/dto/material.dto.d.ts +18 -0
- package/dist/class-group/dto/material.dto.d.ts.map +1 -0
- package/dist/class-group/dto/material.dto.js +86 -0
- package/dist/class-group/dto/material.dto.js.map +1 -0
- package/dist/course/course.service.d.ts +2 -0
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +27 -2
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course.dto.d.ts +2 -2
- package/dist/course/dto/create-course.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course.dto.js.map +1 -1
- package/dist/enterprise/enterprise.controller.d.ts +7 -1
- package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
- package/dist/enterprise/enterprise.controller.js +72 -2
- package/dist/enterprise/enterprise.controller.js.map +1 -1
- package/dist/enterprise/enterprise.module.d.ts.map +1 -1
- package/dist/enterprise/enterprise.module.js +2 -1
- package/dist/enterprise/enterprise.module.js.map +1 -1
- package/dist/enterprise/enterprise.service.d.ts +3 -0
- package/dist/enterprise/enterprise.service.d.ts.map +1 -1
- package/dist/enterprise/enterprise.service.js +84 -1
- package/dist/enterprise/enterprise.service.js.map +1 -1
- package/dist/enterprise/training/enterprise-training.module.d.ts +3 -0
- package/dist/enterprise/training/enterprise-training.module.d.ts.map +1 -0
- package/dist/enterprise/training/enterprise-training.module.js +40 -0
- package/dist/enterprise/training/enterprise-training.module.js.map +1 -0
- package/dist/enterprise/training/training-admin.controller.d.ts +525 -0
- package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -0
- package/dist/enterprise/training/training-admin.controller.js +385 -0
- package/dist/enterprise/training/training-admin.controller.js.map +1 -0
- package/dist/enterprise/training/training-admin.service.d.ts +582 -0
- package/dist/enterprise/training/training-admin.service.d.ts.map +1 -0
- package/dist/enterprise/training/training-admin.service.js +2283 -0
- package/dist/enterprise/training/training-admin.service.js.map +1 -0
- package/dist/enterprise/training/training-instructor.controller.d.ts +260 -0
- package/dist/enterprise/training/training-instructor.controller.d.ts.map +1 -0
- package/dist/enterprise/training/training-instructor.controller.js +199 -0
- package/dist/enterprise/training/training-instructor.controller.js.map +1 -0
- package/dist/enterprise/training/training-instructor.service.d.ts +280 -0
- package/dist/enterprise/training/training-instructor.service.d.ts.map +1 -0
- package/dist/enterprise/training/training-instructor.service.js +1218 -0
- package/dist/enterprise/training/training-instructor.service.js.map +1 -0
- package/dist/enterprise/training/training-student.controller.d.ts +168 -0
- package/dist/enterprise/training/training-student.controller.d.ts.map +1 -0
- package/dist/enterprise/training/training-student.controller.js +104 -0
- package/dist/enterprise/training/training-student.controller.js.map +1 -0
- package/dist/enterprise/training/training-student.service.d.ts +185 -0
- package/dist/enterprise/training/training-student.service.d.ts.map +1 -0
- package/dist/enterprise/training/training-student.service.js +674 -0
- package/dist/enterprise/training/training-student.service.js.map +1 -0
- package/dist/enterprise/training/training-viewer.controller.d.ts +298 -0
- package/dist/enterprise/training/training-viewer.controller.d.ts.map +1 -0
- package/dist/enterprise/training/training-viewer.controller.js +178 -0
- package/dist/enterprise/training/training-viewer.controller.js.map +1 -0
- package/dist/evaluation/dto/create-evaluation-topic.dto.d.ts +18 -0
- package/dist/evaluation/dto/create-evaluation-topic.dto.d.ts.map +1 -0
- package/dist/evaluation/dto/create-evaluation-topic.dto.js +59 -0
- package/dist/evaluation/dto/create-evaluation-topic.dto.js.map +1 -0
- package/dist/evaluation/dto/update-evaluation-topic.dto.d.ts +6 -0
- package/dist/evaluation/dto/update-evaluation-topic.dto.d.ts.map +1 -0
- package/dist/evaluation/dto/update-evaluation-topic.dto.js +9 -0
- package/dist/evaluation/dto/update-evaluation-topic.dto.js.map +1 -0
- package/dist/evaluation/evaluation.controller.d.ts +66 -0
- package/dist/evaluation/evaluation.controller.d.ts.map +1 -1
- package/dist/evaluation/evaluation.controller.js +73 -0
- package/dist/evaluation/evaluation.controller.js.map +1 -1
- package/dist/evaluation/evaluation.service.d.ts +71 -0
- package/dist/evaluation/evaluation.service.d.ts.map +1 -1
- package/dist/evaluation/evaluation.service.js +121 -0
- package/dist/evaluation/evaluation.service.js.map +1 -1
- package/dist/instructor/instructor.service.js +6 -6
- package/dist/instructor/instructor.service.js.map +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +3 -0
- package/dist/lms.module.js.map +1 -1
- package/hedhog/data/menu.yaml +19 -2
- package/hedhog/data/route.yaml +730 -0
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +74 -8
- package/hedhog/frontend/app/_components/course-avatar.tsx.ejs +27 -47
- package/hedhog/frontend/app/classes/[id]/_components/event-summary-popover.tsx.ejs +15 -15
- package/hedhog/frontend/app/classes/[id]/_components/quick-create-session-popover.tsx.ejs +5 -5
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +2141 -308
- package/hedhog/frontend/app/classes/page.tsx.ejs +8 -7
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +21 -8
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +10 -6
- package/hedhog/frontend/app/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +201 -0
- package/hedhog/frontend/app/evaluations/_components/evaluation-topic-types.ts.ejs +49 -0
- package/hedhog/frontend/app/evaluations/page.tsx.ejs +621 -1250
- package/hedhog/frontend/app/instructors/page.tsx.ejs +22 -20
- package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +1278 -0
- package/hedhog/frontend/messages/en.json +98 -7
- package/hedhog/frontend/messages/pt.json +98 -7
- package/hedhog/table/course_class_group_material.yaml +45 -0
- package/package.json +8 -8
- package/src/class-group/class-group.controller.ts +30 -0
- package/src/class-group/class-group.service.ts +176 -5
- package/src/class-group/dto/create-class-group.dto.ts +8 -8
- package/src/class-group/dto/material.dto.ts +69 -0
- package/src/course/course.service.ts +54 -21
- package/src/course/dto/create-course.dto.ts +8 -8
- package/src/enterprise/enterprise.controller.ts +62 -1
- package/src/enterprise/enterprise.module.ts +2 -1
- package/src/enterprise/enterprise.service.ts +84 -1
- package/src/enterprise/training/enterprise-training.module.ts +27 -0
- package/src/enterprise/training/training-admin.controller.ts +278 -0
- package/src/enterprise/training/training-admin.service.ts +2523 -0
- package/src/enterprise/training/training-instructor.controller.ts +141 -0
- package/src/enterprise/training/training-instructor.service.ts +1303 -0
- package/src/enterprise/training/training-student.controller.ts +65 -0
- package/src/enterprise/training/training-student.service.ts +762 -0
- package/src/enterprise/training/training-viewer.controller.ts +115 -0
- package/src/evaluation/dto/create-evaluation-topic.dto.ts +48 -0
- package/src/evaluation/dto/update-evaluation-topic.dto.ts +6 -0
- package/src/evaluation/evaluation.controller.ts +63 -1
- package/src/evaluation/evaluation.service.ts +150 -1
- package/src/instructor/instructor.service.ts +4 -4
- package/src/lms.module.ts +3 -0
|
@@ -1,1250 +1,621 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
import {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
]
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
.
|
|
228
|
-
.
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
},
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
numberOfMonths={2}
|
|
623
|
-
initialFocus
|
|
624
|
-
/>
|
|
625
|
-
{dateRange?.from && (
|
|
626
|
-
<div className="flex items-center justify-between border-t border-border/50 px-3 py-2">
|
|
627
|
-
<span className="text-xs text-muted-foreground">
|
|
628
|
-
{dateRange.to
|
|
629
|
-
? `${format(dateRange.from, 'MMM d')} – ${format(dateRange.to, 'MMM d, yyyy')}`
|
|
630
|
-
: format(dateRange.from, 'MMM d, yyyy')}
|
|
631
|
-
</span>
|
|
632
|
-
<Button
|
|
633
|
-
type="button"
|
|
634
|
-
size="sm"
|
|
635
|
-
className="h-7 px-3 text-xs"
|
|
636
|
-
onClick={() => setDatePopoverOpen(false)}
|
|
637
|
-
>
|
|
638
|
-
{t('filters.apply')}
|
|
639
|
-
</Button>
|
|
640
|
-
</div>
|
|
641
|
-
)}
|
|
642
|
-
</PopoverContent>
|
|
643
|
-
</Popover>
|
|
644
|
-
|
|
645
|
-
<ViewModeToggle
|
|
646
|
-
viewMode={viewMode}
|
|
647
|
-
onViewModeChange={setViewMode}
|
|
648
|
-
listLabel={t('viewMode.list')}
|
|
649
|
-
cardsLabel={t('viewMode.cards')}
|
|
650
|
-
/>
|
|
651
|
-
</form>
|
|
652
|
-
|
|
653
|
-
<div className="flex items-center justify-between gap-3">
|
|
654
|
-
<p className="text-sm text-muted-foreground">
|
|
655
|
-
{totalItems}{' '}
|
|
656
|
-
{totalItems !== 1
|
|
657
|
-
? t('pagination.itemsPlural')
|
|
658
|
-
: t('pagination.items')}
|
|
659
|
-
</p>
|
|
660
|
-
{hasActiveFilters && (
|
|
661
|
-
<Button
|
|
662
|
-
type="button"
|
|
663
|
-
variant="ghost"
|
|
664
|
-
size="sm"
|
|
665
|
-
onClick={clearFilters}
|
|
666
|
-
className="h-8 px-2 text-muted-foreground"
|
|
667
|
-
>
|
|
668
|
-
<X className="mr-1 size-3.5" />
|
|
669
|
-
{t('filters.clear')}
|
|
670
|
-
</Button>
|
|
671
|
-
)}
|
|
672
|
-
</div>
|
|
673
|
-
</div>
|
|
674
|
-
|
|
675
|
-
{/* Content */}
|
|
676
|
-
{isLoading ? (
|
|
677
|
-
viewMode === 'cards' ? (
|
|
678
|
-
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
679
|
-
{Array.from({ length: 6 }).map((_, i) => (
|
|
680
|
-
<Card key={i} className="overflow-hidden">
|
|
681
|
-
<CardContent className="p-4">
|
|
682
|
-
<div className="mb-2 flex items-center justify-between">
|
|
683
|
-
<Skeleton className="h-5 w-16 rounded-full" />
|
|
684
|
-
<Skeleton className="h-4 w-20" />
|
|
685
|
-
</div>
|
|
686
|
-
<div className="mb-3 flex items-start gap-3">
|
|
687
|
-
<Skeleton className="size-10 shrink-0 rounded-lg" />
|
|
688
|
-
<div className="min-w-0 flex-1 space-y-1.5">
|
|
689
|
-
<Skeleton className="h-4 w-3/4" />
|
|
690
|
-
<Skeleton className="h-3.5 w-1/2" />
|
|
691
|
-
</div>
|
|
692
|
-
</div>
|
|
693
|
-
<div className="mb-3 flex gap-1">
|
|
694
|
-
{Array.from({ length: 5 }).map((__, j) => (
|
|
695
|
-
<Skeleton key={j} className="size-3.5 rounded-sm" />
|
|
696
|
-
))}
|
|
697
|
-
</div>
|
|
698
|
-
<div className="flex items-center gap-2 border-t border-border/50 pt-3">
|
|
699
|
-
<Skeleton className="size-6 rounded-full" />
|
|
700
|
-
<Skeleton className="h-3 w-24" />
|
|
701
|
-
</div>
|
|
702
|
-
</CardContent>
|
|
703
|
-
</Card>
|
|
704
|
-
))}
|
|
705
|
-
</div>
|
|
706
|
-
) : (
|
|
707
|
-
<div className="overflow-hidden rounded-xl border border-border/70">
|
|
708
|
-
<Table>
|
|
709
|
-
<TableHeader>
|
|
710
|
-
<TableRow>
|
|
711
|
-
<TableHead>{t('columns.topic')}</TableHead>
|
|
712
|
-
<TableHead>{t('columns.target')}</TableHead>
|
|
713
|
-
<TableHead>{t('columns.score')}</TableHead>
|
|
714
|
-
<TableHead>{t('columns.course')}</TableHead>
|
|
715
|
-
<TableHead>{t('columns.class')}</TableHead>
|
|
716
|
-
<TableHead>{t('columns.student')}</TableHead>
|
|
717
|
-
<TableHead>{t('columns.instructor')}</TableHead>
|
|
718
|
-
<TableHead>{t('columns.date')}</TableHead>
|
|
719
|
-
</TableRow>
|
|
720
|
-
</TableHeader>
|
|
721
|
-
<TableBody>
|
|
722
|
-
{Array.from({ length: 6 }).map((_, i) => (
|
|
723
|
-
<TableRow key={i}>
|
|
724
|
-
{Array.from({ length: 8 }).map((__, j) => (
|
|
725
|
-
<TableCell key={j}>
|
|
726
|
-
<Skeleton className="h-4 w-full" />
|
|
727
|
-
</TableCell>
|
|
728
|
-
))}
|
|
729
|
-
</TableRow>
|
|
730
|
-
))}
|
|
731
|
-
</TableBody>
|
|
732
|
-
</Table>
|
|
733
|
-
</div>
|
|
734
|
-
)
|
|
735
|
-
) : paginatedItems.length === 0 ? (
|
|
736
|
-
<EmptyState
|
|
737
|
-
icon={<Star className="size-8" />}
|
|
738
|
-
title={t('empty.title')}
|
|
739
|
-
description={t('empty.description')}
|
|
740
|
-
/>
|
|
741
|
-
) : viewMode === 'cards' ? (
|
|
742
|
-
<motion.div
|
|
743
|
-
className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3"
|
|
744
|
-
variants={stagger}
|
|
745
|
-
initial="hidden"
|
|
746
|
-
animate="show"
|
|
747
|
-
>
|
|
748
|
-
{paginatedItems.map((item) => (
|
|
749
|
-
<motion.div key={item.id} variants={fadeUp}>
|
|
750
|
-
<Card
|
|
751
|
-
className="cursor-pointer overflow-hidden transition-shadow hover:shadow-md"
|
|
752
|
-
onClick={() => openDetail(item)}
|
|
753
|
-
>
|
|
754
|
-
<CardContent className="p-4">
|
|
755
|
-
{/* Header: badge + date + menu */}
|
|
756
|
-
<div className="mb-2 flex items-center justify-between gap-2">
|
|
757
|
-
<TargetTypeBadge
|
|
758
|
-
targetType={item.targetType}
|
|
759
|
-
label={t(`targetType.${item.targetType}`)}
|
|
760
|
-
/>
|
|
761
|
-
<div className="flex items-center gap-1">
|
|
762
|
-
<span className="text-xs text-muted-foreground">
|
|
763
|
-
{format(parseISO(item.createdAt), 'MMM d, yyyy')}
|
|
764
|
-
</span>
|
|
765
|
-
<DropdownMenu>
|
|
766
|
-
<DropdownMenuTrigger asChild>
|
|
767
|
-
<Button
|
|
768
|
-
variant="ghost"
|
|
769
|
-
size="icon"
|
|
770
|
-
className="size-6 shrink-0"
|
|
771
|
-
onClick={(e) => e.stopPropagation()}
|
|
772
|
-
>
|
|
773
|
-
<MoreHorizontal className="size-3.5" />
|
|
774
|
-
</Button>
|
|
775
|
-
</DropdownMenuTrigger>
|
|
776
|
-
<DropdownMenuContent align="end">
|
|
777
|
-
<DropdownMenuItem
|
|
778
|
-
onClick={() => router.push('/lms/courses')}
|
|
779
|
-
>
|
|
780
|
-
<BookOpen className="mr-2 size-3.5" />
|
|
781
|
-
{t('dropdown.viewCourse')}
|
|
782
|
-
</DropdownMenuItem>
|
|
783
|
-
{item.className && (
|
|
784
|
-
<DropdownMenuItem
|
|
785
|
-
onClick={() => router.push('/lms/classes')}
|
|
786
|
-
>
|
|
787
|
-
<Users className="mr-2 size-3.5" />
|
|
788
|
-
{t('dropdown.viewClass')}
|
|
789
|
-
</DropdownMenuItem>
|
|
790
|
-
)}
|
|
791
|
-
<DropdownMenuItem
|
|
792
|
-
onClick={() => router.push('/lms/training')}
|
|
793
|
-
>
|
|
794
|
-
<GraduationCap className="mr-2 size-3.5" />
|
|
795
|
-
{t('dropdown.viewStudent')}
|
|
796
|
-
</DropdownMenuItem>
|
|
797
|
-
<DropdownMenuItem onClick={() => openDetail(item)}>
|
|
798
|
-
<Star className="mr-2 size-3.5" />
|
|
799
|
-
{t('dropdown.viewDetails')}
|
|
800
|
-
</DropdownMenuItem>
|
|
801
|
-
</DropdownMenuContent>
|
|
802
|
-
</DropdownMenu>
|
|
803
|
-
</div>
|
|
804
|
-
</div>
|
|
805
|
-
|
|
806
|
-
{/* Course logo + topic */}
|
|
807
|
-
<div className="mb-3 flex items-start gap-3">
|
|
808
|
-
<div
|
|
809
|
-
className={cn(
|
|
810
|
-
'flex size-10 shrink-0 items-center justify-center rounded-lg text-sm font-bold text-white',
|
|
811
|
-
getCourseColor(item.courseId)
|
|
812
|
-
)}
|
|
813
|
-
>
|
|
814
|
-
{item.courseName?.[0] ?? '?'}
|
|
815
|
-
</div>
|
|
816
|
-
<div className="min-w-0 flex-1">
|
|
817
|
-
<p className="font-semibold leading-snug line-clamp-1">
|
|
818
|
-
{item.topicName}
|
|
819
|
-
</p>
|
|
820
|
-
<p className="text-sm text-muted-foreground line-clamp-1">
|
|
821
|
-
{item.targetName}
|
|
822
|
-
</p>
|
|
823
|
-
</div>
|
|
824
|
-
</div>
|
|
825
|
-
|
|
826
|
-
{/* Score */}
|
|
827
|
-
<div className="mb-3 flex items-center gap-2">
|
|
828
|
-
<StarRating score={item.score} size={14} />
|
|
829
|
-
<span className="text-sm font-medium tabular-nums">
|
|
830
|
-
{item.score.toFixed(1)}
|
|
831
|
-
</span>
|
|
832
|
-
</div>
|
|
833
|
-
|
|
834
|
-
{/* Footer: student avatar + class/instructor */}
|
|
835
|
-
<div className="flex items-center justify-between gap-2 border-t border-border/50 pt-3">
|
|
836
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
837
|
-
<Avatar className="size-6 shrink-0">
|
|
838
|
-
<AvatarFallback className="text-xs">
|
|
839
|
-
{getInitials(item.evaluatorName)}
|
|
840
|
-
</AvatarFallback>
|
|
841
|
-
</Avatar>
|
|
842
|
-
<span className="truncate text-xs font-medium">
|
|
843
|
-
{item.evaluatorName}
|
|
844
|
-
</span>
|
|
845
|
-
</div>
|
|
846
|
-
{(item.className || item.instructorName) && (
|
|
847
|
-
<div className="flex shrink-0 items-center gap-1 text-xs text-muted-foreground">
|
|
848
|
-
{item.className && (
|
|
849
|
-
<span className="max-w-24 truncate">
|
|
850
|
-
{item.className}
|
|
851
|
-
</span>
|
|
852
|
-
)}
|
|
853
|
-
{item.className && item.instructorName && (
|
|
854
|
-
<span>·</span>
|
|
855
|
-
)}
|
|
856
|
-
{item.instructorName && (
|
|
857
|
-
<span className="max-w-24 truncate">
|
|
858
|
-
{item.instructorName}
|
|
859
|
-
</span>
|
|
860
|
-
)}
|
|
861
|
-
</div>
|
|
862
|
-
)}
|
|
863
|
-
</div>
|
|
864
|
-
</CardContent>
|
|
865
|
-
</Card>
|
|
866
|
-
</motion.div>
|
|
867
|
-
))}
|
|
868
|
-
</motion.div>
|
|
869
|
-
) : (
|
|
870
|
-
<div className="overflow-hidden rounded-xl border border-border/70">
|
|
871
|
-
<Table>
|
|
872
|
-
<TableHeader>
|
|
873
|
-
<TableRow>
|
|
874
|
-
<TableHead>{t('columns.topic')}</TableHead>
|
|
875
|
-
<TableHead>{t('columns.target')}</TableHead>
|
|
876
|
-
<TableHead>{t('columns.score')}</TableHead>
|
|
877
|
-
<TableHead>{t('columns.course')}</TableHead>
|
|
878
|
-
<TableHead>{t('columns.class')}</TableHead>
|
|
879
|
-
<TableHead>{t('columns.student')}</TableHead>
|
|
880
|
-
<TableHead>{t('columns.instructor')}</TableHead>
|
|
881
|
-
<TableHead>{t('columns.date')}</TableHead>
|
|
882
|
-
</TableRow>
|
|
883
|
-
</TableHeader>
|
|
884
|
-
<TableBody>
|
|
885
|
-
{paginatedItems.map((item) => (
|
|
886
|
-
<TableRow
|
|
887
|
-
key={item.id}
|
|
888
|
-
className="cursor-pointer"
|
|
889
|
-
onClick={() => openDetail(item)}
|
|
890
|
-
>
|
|
891
|
-
<TableCell className="font-medium">
|
|
892
|
-
{item.topicName}
|
|
893
|
-
</TableCell>
|
|
894
|
-
<TableCell>
|
|
895
|
-
<div className="flex flex-col gap-1">
|
|
896
|
-
<TargetTypeBadge
|
|
897
|
-
targetType={item.targetType}
|
|
898
|
-
label={t(`targetType.${item.targetType}`)}
|
|
899
|
-
/>
|
|
900
|
-
<span className="text-xs text-muted-foreground">
|
|
901
|
-
{item.targetName}
|
|
902
|
-
</span>
|
|
903
|
-
</div>
|
|
904
|
-
</TableCell>
|
|
905
|
-
<TableCell>
|
|
906
|
-
<div className="flex items-center gap-1.5">
|
|
907
|
-
<StarRating score={item.score} size={13} />
|
|
908
|
-
<span className="text-sm tabular-nums">
|
|
909
|
-
{item.score.toFixed(1)}
|
|
910
|
-
</span>
|
|
911
|
-
</div>
|
|
912
|
-
</TableCell>
|
|
913
|
-
<TableCell className="text-sm">{item.courseName}</TableCell>
|
|
914
|
-
<TableCell className="text-sm text-muted-foreground">
|
|
915
|
-
{item.className ?? '—'}
|
|
916
|
-
</TableCell>
|
|
917
|
-
<TableCell className="text-sm">
|
|
918
|
-
{item.evaluatorName}
|
|
919
|
-
</TableCell>
|
|
920
|
-
<TableCell className="text-sm text-muted-foreground">
|
|
921
|
-
{item.instructorName ?? '—'}
|
|
922
|
-
</TableCell>
|
|
923
|
-
<TableCell className="text-sm text-muted-foreground">
|
|
924
|
-
{format(parseISO(item.createdAt), 'MMM d, yyyy')}
|
|
925
|
-
</TableCell>
|
|
926
|
-
</TableRow>
|
|
927
|
-
))}
|
|
928
|
-
</TableBody>
|
|
929
|
-
</Table>
|
|
930
|
-
</div>
|
|
931
|
-
)}
|
|
932
|
-
|
|
933
|
-
{/* Pagination */}
|
|
934
|
-
{!isLoading && totalItems > 0 && (
|
|
935
|
-
<div className="mt-6">
|
|
936
|
-
<PaginationFooter
|
|
937
|
-
currentPage={currentPage}
|
|
938
|
-
pageSize={pageSize}
|
|
939
|
-
totalItems={totalItems}
|
|
940
|
-
onPageChange={setCurrentPage}
|
|
941
|
-
onPageSizeChange={(nextSize) => {
|
|
942
|
-
setPageSize(nextSize);
|
|
943
|
-
setCurrentPage(1);
|
|
944
|
-
}}
|
|
945
|
-
pageSizeOptions={PAGE_SIZES}
|
|
946
|
-
/>
|
|
947
|
-
</div>
|
|
948
|
-
)}
|
|
949
|
-
</div>
|
|
950
|
-
|
|
951
|
-
{/* Detail Sheet */}
|
|
952
|
-
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
|
953
|
-
<SheetContent
|
|
954
|
-
side="right"
|
|
955
|
-
className="w-full overflow-y-auto p-0 sm:max-w-xl"
|
|
956
|
-
>
|
|
957
|
-
{selectedItem && (
|
|
958
|
-
<>
|
|
959
|
-
{/* Hero band */}
|
|
960
|
-
<div className="border-b border-border/50 px-6 pb-5 pt-6">
|
|
961
|
-
<SheetHeader className="space-y-0">
|
|
962
|
-
<div className="flex items-start gap-4">
|
|
963
|
-
<div
|
|
964
|
-
className={cn(
|
|
965
|
-
'flex size-14 shrink-0 items-center justify-center rounded-xl text-2xl font-bold text-white shadow-sm',
|
|
966
|
-
getCourseColor(selectedItem.courseId)
|
|
967
|
-
)}
|
|
968
|
-
>
|
|
969
|
-
{selectedItem.courseName?.[0] ?? '?'}
|
|
970
|
-
<div className="mb-2">
|
|
971
|
-
<TargetTypeBadge
|
|
972
|
-
targetType={selectedItem.targetType}
|
|
973
|
-
label={t(`targetType.${selectedItem.targetType}`)}
|
|
974
|
-
/>
|
|
975
|
-
</div>
|
|
976
|
-
<SheetTitle className="text-xl leading-snug">
|
|
977
|
-
{selectedItem.topicName}
|
|
978
|
-
</SheetTitle>
|
|
979
|
-
{selectedItem.topicDescription && (
|
|
980
|
-
<p className="mt-1.5 text-sm text-muted-foreground">
|
|
981
|
-
{selectedItem.topicDescription}
|
|
982
|
-
</p>
|
|
983
|
-
)}
|
|
984
|
-
</div>
|
|
985
|
-
</div>
|
|
986
|
-
</SheetHeader>
|
|
987
|
-
</div>
|
|
988
|
-
|
|
989
|
-
{/* Body */}
|
|
990
|
-
<div className="space-y-6 px-6 py-6">
|
|
991
|
-
{/* Score + comparison */}
|
|
992
|
-
{(() => {
|
|
993
|
-
const avg = selectedItem.topicAvgScore;
|
|
994
|
-
const count = selectedItem.topicRatingCount;
|
|
995
|
-
const delta =
|
|
996
|
-
Math.round((selectedItem.score - avg) * 10) / 10;
|
|
997
|
-
return (
|
|
998
|
-
<div className="rounded-xl border border-border/50 bg-muted/20 p-5">
|
|
999
|
-
{/* Arc + stars row */}
|
|
1000
|
-
<div className="flex items-center gap-6">
|
|
1001
|
-
<ScoreArcChart score={selectedItem.score} />
|
|
1002
|
-
<div className="flex-1 space-y-2">
|
|
1003
|
-
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1004
|
-
{t('sheet.scoreLabel')}
|
|
1005
|
-
</p>
|
|
1006
|
-
<StarRating score={selectedItem.score} size={22} />
|
|
1007
|
-
<p className="text-sm text-muted-foreground">
|
|
1008
|
-
<span className="text-lg font-bold text-foreground tabular-nums">
|
|
1009
|
-
{selectedItem.score.toFixed(1)}
|
|
1010
|
-
</span>{' '}
|
|
1011
|
-
/ 5.0
|
|
1012
|
-
</p>
|
|
1013
|
-
</div>
|
|
1014
|
-
</div>
|
|
1015
|
-
|
|
1016
|
-
{/* Comparison bars */}
|
|
1017
|
-
<div className="mt-4 space-y-3 border-t border-border/40 pt-4">
|
|
1018
|
-
<div className="flex items-center justify-between">
|
|
1019
|
-
<p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1020
|
-
{t('sheet.comparison')}
|
|
1021
|
-
</p>
|
|
1022
|
-
{delta !== 0 && (
|
|
1023
|
-
<span
|
|
1024
|
-
className={cn(
|
|
1025
|
-
'text-xs font-semibold tabular-nums',
|
|
1026
|
-
delta > 0
|
|
1027
|
-
? 'text-emerald-600 dark:text-emerald-400'
|
|
1028
|
-
: 'text-red-500 dark:text-red-400'
|
|
1029
|
-
)}
|
|
1030
|
-
>
|
|
1031
|
-
{delta > 0 ? '▲' : '▼'}{' '}
|
|
1032
|
-
{Math.abs(delta).toFixed(1)}
|
|
1033
|
-
</span>
|
|
1034
|
-
)}
|
|
1035
|
-
</div>
|
|
1036
|
-
|
|
1037
|
-
<div className="space-y-1">
|
|
1038
|
-
<div className="flex items-center justify-between text-xs">
|
|
1039
|
-
<span className="text-muted-foreground">
|
|
1040
|
-
{t('sheet.thisEvaluation')}
|
|
1041
|
-
</span>
|
|
1042
|
-
<span className="font-semibold tabular-nums">
|
|
1043
|
-
{selectedItem.score.toFixed(1)}
|
|
1044
|
-
</span>
|
|
1045
|
-
</div>
|
|
1046
|
-
<div className="h-2.5 w-full overflow-hidden rounded-full bg-muted">
|
|
1047
|
-
<div
|
|
1048
|
-
className={cn(
|
|
1049
|
-
'h-full rounded-full transition-all duration-500',
|
|
1050
|
-
getScoreBgColor(selectedItem.score)
|
|
1051
|
-
)}
|
|
1052
|
-
style={{
|
|
1053
|
-
width: `${(selectedItem.score / 5) * 100}%`,
|
|
1054
|
-
}}
|
|
1055
|
-
/>
|
|
1056
|
-
</div>
|
|
1057
|
-
</div>
|
|
1058
|
-
|
|
1059
|
-
<div className="space-y-1">
|
|
1060
|
-
<div className="flex items-center justify-between text-xs">
|
|
1061
|
-
<span className="text-muted-foreground">
|
|
1062
|
-
{t('sheet.targetAverage')} ({count}{' '}
|
|
1063
|
-
{t('sheet.ratings')})
|
|
1064
|
-
</span>
|
|
1065
|
-
<span className="tabular-nums text-muted-foreground">
|
|
1066
|
-
{avg.toFixed(1)}
|
|
1067
|
-
</span>
|
|
1068
|
-
</div>
|
|
1069
|
-
<div className="h-2.5 w-full overflow-hidden rounded-full bg-muted">
|
|
1070
|
-
<div
|
|
1071
|
-
className="h-full rounded-full bg-muted-foreground/40 transition-all duration-500"
|
|
1072
|
-
style={{ width: `${(avg / 5) * 100}%` }}
|
|
1073
|
-
/>
|
|
1074
|
-
</div>
|
|
1075
|
-
</div>
|
|
1076
|
-
</div>
|
|
1077
|
-
</div>
|
|
1078
|
-
);
|
|
1079
|
-
})()}
|
|
1080
|
-
|
|
1081
|
-
{/* Target */}
|
|
1082
|
-
<div>
|
|
1083
|
-
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1084
|
-
{t('sheet.targetLabel')}
|
|
1085
|
-
</p>
|
|
1086
|
-
<button
|
|
1087
|
-
type="button"
|
|
1088
|
-
onClick={() =>
|
|
1089
|
-
window.open(
|
|
1090
|
-
getTargetUrl(selectedItem.targetType),
|
|
1091
|
-
'_blank'
|
|
1092
|
-
)
|
|
1093
|
-
}
|
|
1094
|
-
className="flex w-full items-center gap-3 rounded-lg border border-border/50 bg-card p-3 text-left transition-colors hover:bg-muted/40"
|
|
1095
|
-
>
|
|
1096
|
-
<span className="flex size-8 shrink-0 items-center justify-center rounded-md border border-border/60 bg-background">
|
|
1097
|
-
{getTargetIcon(selectedItem.targetType)}
|
|
1098
|
-
</span>
|
|
1099
|
-
<div className="min-w-0 flex-1">
|
|
1100
|
-
<p className="truncate text-sm font-medium">
|
|
1101
|
-
{selectedItem.targetName}
|
|
1102
|
-
</p>
|
|
1103
|
-
<p className="text-xs text-muted-foreground">
|
|
1104
|
-
{t(`targetType.${selectedItem.targetType}`)}
|
|
1105
|
-
</p>
|
|
1106
|
-
</div>
|
|
1107
|
-
<ExternalLink className="size-3.5 shrink-0 text-muted-foreground" />
|
|
1108
|
-
</button>
|
|
1109
|
-
</div>
|
|
1110
|
-
|
|
1111
|
-
{/* Comment */}
|
|
1112
|
-
{selectedItem.comment ? (
|
|
1113
|
-
<div>
|
|
1114
|
-
<p className="mb-2 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1115
|
-
<MessageSquare className="size-3.5" />
|
|
1116
|
-
Comment
|
|
1117
|
-
</p>
|
|
1118
|
-
<p className="rounded-lg border border-border/50 bg-muted/20 px-4 py-3 text-sm leading-relaxed">
|
|
1119
|
-
{selectedItem.comment}
|
|
1120
|
-
</p>
|
|
1121
|
-
</div>
|
|
1122
|
-
) : (
|
|
1123
|
-
<p className="text-xs italic text-muted-foreground">
|
|
1124
|
-
{t('sheet.noComment')}
|
|
1125
|
-
</p>
|
|
1126
|
-
)}
|
|
1127
|
-
|
|
1128
|
-
{/* People */}
|
|
1129
|
-
<div>
|
|
1130
|
-
<p className="mb-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1131
|
-
{t('sheet.people')}
|
|
1132
|
-
</p>
|
|
1133
|
-
<div className="grid grid-cols-2 gap-3">
|
|
1134
|
-
<button
|
|
1135
|
-
type="button"
|
|
1136
|
-
onClick={() => window.open('/contact/accounts', '_blank')}
|
|
1137
|
-
className="flex items-center gap-3 rounded-lg border border-border/50 bg-card p-3 text-left transition-colors hover:bg-muted/40"
|
|
1138
|
-
>
|
|
1139
|
-
<Avatar className="size-9 shrink-0">
|
|
1140
|
-
<AvatarFallback className="bg-sky-100 text-xs font-semibold text-sky-700 dark:bg-sky-950 dark:text-sky-300">
|
|
1141
|
-
{getInitials(selectedItem.evaluatorName)}
|
|
1142
|
-
</AvatarFallback>
|
|
1143
|
-
</Avatar>
|
|
1144
|
-
<div className="min-w-0">
|
|
1145
|
-
<p className="truncate text-sm font-medium">
|
|
1146
|
-
{selectedItem.evaluatorName}
|
|
1147
|
-
</p>
|
|
1148
|
-
<p className="text-xs text-muted-foreground">
|
|
1149
|
-
{t('sheet.evaluatorLabel')}
|
|
1150
|
-
</p>
|
|
1151
|
-
</div>
|
|
1152
|
-
</button>
|
|
1153
|
-
|
|
1154
|
-
{selectedItem.instructorName && (
|
|
1155
|
-
<button
|
|
1156
|
-
type="button"
|
|
1157
|
-
onClick={() =>
|
|
1158
|
-
window.open('/contact/accounts', '_blank')
|
|
1159
|
-
}
|
|
1160
|
-
className="flex items-center gap-3 rounded-lg border border-border/50 bg-card p-3 text-left transition-colors hover:bg-muted/40"
|
|
1161
|
-
>
|
|
1162
|
-
<Avatar className="size-9 shrink-0">
|
|
1163
|
-
<AvatarFallback className="bg-violet-100 text-xs font-semibold text-violet-700 dark:bg-violet-950 dark:text-violet-300">
|
|
1164
|
-
{getInitials(selectedItem.instructorName)}
|
|
1165
|
-
</AvatarFallback>
|
|
1166
|
-
</Avatar>
|
|
1167
|
-
<div className="min-w-0">
|
|
1168
|
-
<p className="truncate text-sm font-medium">
|
|
1169
|
-
{selectedItem.instructorName}
|
|
1170
|
-
</p>
|
|
1171
|
-
<p className="text-xs text-muted-foreground">
|
|
1172
|
-
{t('sheet.instructorLabel')}
|
|
1173
|
-
</p>
|
|
1174
|
-
</div>
|
|
1175
|
-
</button>
|
|
1176
|
-
)}
|
|
1177
|
-
</div>
|
|
1178
|
-
</div>
|
|
1179
|
-
|
|
1180
|
-
{/* Course & Class */}
|
|
1181
|
-
<div>
|
|
1182
|
-
<p className="mb-3 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
1183
|
-
{t('sheet.courseLabel')}
|
|
1184
|
-
</p>
|
|
1185
|
-
<div className="space-y-2">
|
|
1186
|
-
<button
|
|
1187
|
-
type="button"
|
|
1188
|
-
onClick={() => window.open('/lms/courses', '_blank')}
|
|
1189
|
-
className="flex w-full items-center gap-3 rounded-lg border border-border/50 bg-card p-3 text-left transition-colors hover:bg-muted/40"
|
|
1190
|
-
>
|
|
1191
|
-
<div
|
|
1192
|
-
className={cn(
|
|
1193
|
-
'flex size-8 shrink-0 items-center justify-center rounded-lg text-sm font-bold text-white',
|
|
1194
|
-
getCourseColor(selectedItem.courseId)
|
|
1195
|
-
)}
|
|
1196
|
-
>
|
|
1197
|
-
{selectedItem.courseName?.[0] ?? '?'}
|
|
1198
|
-
</div>
|
|
1199
|
-
<div className="min-w-0 flex-1">
|
|
1200
|
-
<p className="truncate text-sm font-medium">
|
|
1201
|
-
{selectedItem.courseName}
|
|
1202
|
-
</p>
|
|
1203
|
-
<p className="text-xs text-muted-foreground">
|
|
1204
|
-
{t('sheet.courseLabel')}
|
|
1205
|
-
</p>
|
|
1206
|
-
</div>
|
|
1207
|
-
<ExternalLink className="size-3.5 shrink-0 text-muted-foreground" />
|
|
1208
|
-
</button>
|
|
1209
|
-
|
|
1210
|
-
{selectedItem.className && (
|
|
1211
|
-
<button
|
|
1212
|
-
type="button"
|
|
1213
|
-
onClick={() => window.open('/lms/classes', '_blank')}
|
|
1214
|
-
className="flex w-full items-center gap-3 rounded-lg border border-border/50 bg-card p-3 text-left transition-colors hover:bg-muted/40"
|
|
1215
|
-
>
|
|
1216
|
-
<span className="flex size-8 shrink-0 items-center justify-center rounded-md border border-border/60 bg-background">
|
|
1217
|
-
<Users className="size-4 text-muted-foreground" />
|
|
1218
|
-
</span>
|
|
1219
|
-
<div className="min-w-0 flex-1">
|
|
1220
|
-
<p className="truncate text-sm font-medium">
|
|
1221
|
-
{selectedItem.className}
|
|
1222
|
-
</p>
|
|
1223
|
-
<p className="text-xs text-muted-foreground">
|
|
1224
|
-
{t('sheet.classLabel')}
|
|
1225
|
-
</p>
|
|
1226
|
-
</div>
|
|
1227
|
-
<ExternalLink className="size-3.5 shrink-0 text-muted-foreground" />
|
|
1228
|
-
</button>
|
|
1229
|
-
)}
|
|
1230
|
-
</div>
|
|
1231
|
-
</div>
|
|
1232
|
-
|
|
1233
|
-
{/* Date */}
|
|
1234
|
-
<div className="flex items-center justify-between border-t border-border/50 pt-4">
|
|
1235
|
-
<span className="flex items-center gap-1.5 text-xs text-muted-foreground">
|
|
1236
|
-
<CalendarIcon className="size-3.5" />
|
|
1237
|
-
{t('sheet.dateLabel')}
|
|
1238
|
-
</span>
|
|
1239
|
-
<span className="text-sm font-medium">
|
|
1240
|
-
{format(parseISO(selectedItem.createdAt), 'MMMM d, yyyy')}
|
|
1241
|
-
</span>
|
|
1242
|
-
</div>
|
|
1243
|
-
</div>
|
|
1244
|
-
</>
|
|
1245
|
-
)}
|
|
1246
|
-
</SheetContent>
|
|
1247
|
-
</Sheet>
|
|
1248
|
-
</Page>
|
|
1249
|
-
);
|
|
1250
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// ─── THIS FILE IS THE EVALUATION TOPICS MANAGEMENT PAGE ───────────────────────
|
|
4
|
+
// The ratings/report page was moved to: /lms/reports/evaluations/page.tsx
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
EmptyState,
|
|
8
|
+
Page,
|
|
9
|
+
PageHeader,
|
|
10
|
+
PaginationFooter,
|
|
11
|
+
} from '@/components/entity-list';
|
|
12
|
+
import {
|
|
13
|
+
AlertDialog,
|
|
14
|
+
AlertDialogAction,
|
|
15
|
+
AlertDialogCancel,
|
|
16
|
+
AlertDialogContent,
|
|
17
|
+
AlertDialogDescription,
|
|
18
|
+
AlertDialogFooter,
|
|
19
|
+
AlertDialogHeader,
|
|
20
|
+
AlertDialogTitle,
|
|
21
|
+
} from '@/components/ui/alert-dialog';
|
|
22
|
+
import { Badge } from '@/components/ui/badge';
|
|
23
|
+
import { Button } from '@/components/ui/button';
|
|
24
|
+
import { Input } from '@/components/ui/input';
|
|
25
|
+
import { KpiCardsGrid, type KpiCardItem } from '@/components/ui/kpi-cards-grid';
|
|
26
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
27
|
+
import {
|
|
28
|
+
Table,
|
|
29
|
+
TableBody,
|
|
30
|
+
TableCell,
|
|
31
|
+
TableHead,
|
|
32
|
+
TableHeader,
|
|
33
|
+
TableRow,
|
|
34
|
+
} from '@/components/ui/table';
|
|
35
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
36
|
+
import { cn } from '@/lib/utils';
|
|
37
|
+
import {
|
|
38
|
+
DndContext,
|
|
39
|
+
PointerSensor,
|
|
40
|
+
closestCenter,
|
|
41
|
+
useSensor,
|
|
42
|
+
useSensors,
|
|
43
|
+
type DragEndEvent,
|
|
44
|
+
} from '@dnd-kit/core';
|
|
45
|
+
import {
|
|
46
|
+
SortableContext,
|
|
47
|
+
arrayMove,
|
|
48
|
+
useSortable,
|
|
49
|
+
verticalListSortingStrategy,
|
|
50
|
+
} from '@dnd-kit/sortable';
|
|
51
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
52
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
53
|
+
import {
|
|
54
|
+
BookOpen,
|
|
55
|
+
GraduationCap,
|
|
56
|
+
GripVertical,
|
|
57
|
+
ListChecks,
|
|
58
|
+
MessageSquare,
|
|
59
|
+
Pencil,
|
|
60
|
+
Plus,
|
|
61
|
+
Search,
|
|
62
|
+
Star,
|
|
63
|
+
Trash2,
|
|
64
|
+
Users,
|
|
65
|
+
X,
|
|
66
|
+
} from 'lucide-react';
|
|
67
|
+
import { useTranslations } from 'next-intl';
|
|
68
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
69
|
+
import { toast } from 'sonner';
|
|
70
|
+
import { EvaluationTopicFormSheet } from './_components/evaluation-topic-form-sheet';
|
|
71
|
+
import {
|
|
72
|
+
TARGET_TYPE_COLORS,
|
|
73
|
+
TARGET_TYPE_OPTIONS,
|
|
74
|
+
type ApiEvaluationTopicList,
|
|
75
|
+
type EvaluationTargetType,
|
|
76
|
+
type EvaluationTopicItem,
|
|
77
|
+
} from './_components/evaluation-topic-types';
|
|
78
|
+
|
|
79
|
+
// ── SortableTopicRow ──────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
interface SortableTopicRowProps {
|
|
82
|
+
item: EvaluationTopicItem;
|
|
83
|
+
onEdit: (item: EvaluationTopicItem) => void;
|
|
84
|
+
onDelete: (item: EvaluationTopicItem) => void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function SortableTopicRow({ item, onEdit, onDelete }: SortableTopicRowProps) {
|
|
88
|
+
const t = useTranslations('lms.EvaluationTopicsPage');
|
|
89
|
+
const {
|
|
90
|
+
attributes,
|
|
91
|
+
listeners,
|
|
92
|
+
setNodeRef,
|
|
93
|
+
transform,
|
|
94
|
+
transition,
|
|
95
|
+
isDragging,
|
|
96
|
+
} = useSortable({ id: item.id });
|
|
97
|
+
|
|
98
|
+
const style: React.CSSProperties = {
|
|
99
|
+
transform: CSS.Transform.toString(transform),
|
|
100
|
+
transition,
|
|
101
|
+
opacity: isDragging ? 0.4 : undefined,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<TableRow
|
|
106
|
+
ref={setNodeRef}
|
|
107
|
+
style={style}
|
|
108
|
+
className={cn(
|
|
109
|
+
'group cursor-pointer',
|
|
110
|
+
isDragging && 'bg-muted/50 shadow-md'
|
|
111
|
+
)}
|
|
112
|
+
onClick={() => onEdit(item)}
|
|
113
|
+
>
|
|
114
|
+
<TableCell className="w-8 pr-0" onClick={(e) => e.stopPropagation()}>
|
|
115
|
+
<button
|
|
116
|
+
{...attributes}
|
|
117
|
+
{...listeners}
|
|
118
|
+
className="flex cursor-grab touch-none items-center justify-center rounded p-1 text-muted-foreground/40 opacity-0 transition-opacity group-hover:opacity-100 active:cursor-grabbing"
|
|
119
|
+
tabIndex={-1}
|
|
120
|
+
>
|
|
121
|
+
<GripVertical className="size-4" />
|
|
122
|
+
</button>
|
|
123
|
+
</TableCell>
|
|
124
|
+
<TableCell>
|
|
125
|
+
<div className="flex flex-col gap-0.5">
|
|
126
|
+
<span className="font-medium">{item.name}</span>
|
|
127
|
+
{item.description && (
|
|
128
|
+
<span className="max-w-xs truncate text-xs text-muted-foreground">
|
|
129
|
+
{item.description}
|
|
130
|
+
</span>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
</TableCell>
|
|
134
|
+
<TableCell>
|
|
135
|
+
<span
|
|
136
|
+
className={cn(
|
|
137
|
+
'inline-flex items-center rounded-full border px-2 py-0.5 text-xs font-medium',
|
|
138
|
+
TARGET_TYPE_COLORS[item.targetType]
|
|
139
|
+
)}
|
|
140
|
+
>
|
|
141
|
+
{t(`targetType.${item.targetType}`)}
|
|
142
|
+
</span>
|
|
143
|
+
</TableCell>
|
|
144
|
+
<TableCell className="text-sm text-muted-foreground">
|
|
145
|
+
{item.ratingCount}
|
|
146
|
+
</TableCell>
|
|
147
|
+
<TableCell>
|
|
148
|
+
<Badge
|
|
149
|
+
variant={item.isActive ? 'default' : 'secondary'}
|
|
150
|
+
className={cn(
|
|
151
|
+
'text-xs',
|
|
152
|
+
item.isActive
|
|
153
|
+
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300'
|
|
154
|
+
: ''
|
|
155
|
+
)}
|
|
156
|
+
>
|
|
157
|
+
{item.isActive ? t('status.active') : t('status.inactive')}
|
|
158
|
+
</Badge>
|
|
159
|
+
</TableCell>
|
|
160
|
+
<TableCell onClick={(e) => e.stopPropagation()}>
|
|
161
|
+
<div className="flex items-center gap-1">
|
|
162
|
+
<Button
|
|
163
|
+
variant="ghost"
|
|
164
|
+
size="icon"
|
|
165
|
+
className="size-8"
|
|
166
|
+
onClick={() => onEdit(item)}
|
|
167
|
+
>
|
|
168
|
+
<Pencil className="size-3.5" />
|
|
169
|
+
</Button>
|
|
170
|
+
<Button
|
|
171
|
+
variant="ghost"
|
|
172
|
+
size="icon"
|
|
173
|
+
className="size-8 text-destructive hover:text-destructive"
|
|
174
|
+
onClick={() => onDelete(item)}
|
|
175
|
+
disabled={item.ratingCount > 0}
|
|
176
|
+
title={
|
|
177
|
+
item.ratingCount > 0 ? t('delete.disabledTooltip') : undefined
|
|
178
|
+
}
|
|
179
|
+
>
|
|
180
|
+
<Trash2 className="size-3.5" />
|
|
181
|
+
</Button>
|
|
182
|
+
</div>
|
|
183
|
+
</TableCell>
|
|
184
|
+
</TableRow>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
189
|
+
|
|
190
|
+
const PAGE_SIZE = 20;
|
|
191
|
+
|
|
192
|
+
const TARGET_TYPE_ICONS: Record<EvaluationTargetType, React.ReactNode> = {
|
|
193
|
+
course: <BookOpen className="size-4" />,
|
|
194
|
+
course_lesson: <BookOpen className="size-4" />,
|
|
195
|
+
course_class_session: <Users className="size-4" />,
|
|
196
|
+
question: <MessageSquare className="size-4" />,
|
|
197
|
+
exam: <GraduationCap className="size-4" />,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// ── Page ──────────────────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
export default function EvaluationTopicsPage() {
|
|
203
|
+
const t = useTranslations('lms.EvaluationTopicsPage');
|
|
204
|
+
const { request } = useApp();
|
|
205
|
+
|
|
206
|
+
const [searchInput, setSearchInput] = useState('');
|
|
207
|
+
const [debouncedSearch, setDebouncedSearch] = useState('');
|
|
208
|
+
const [activeTab, setActiveTab] = useState<EvaluationTargetType | 'all'>(
|
|
209
|
+
'all'
|
|
210
|
+
);
|
|
211
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
212
|
+
|
|
213
|
+
const [sheetOpen, setSheetOpen] = useState(false);
|
|
214
|
+
const [editingItem, setEditingItem] = useState<EvaluationTopicItem | null>(
|
|
215
|
+
null
|
|
216
|
+
);
|
|
217
|
+
const [deletingItem, setDeletingItem] = useState<EvaluationTopicItem | null>(
|
|
218
|
+
null
|
|
219
|
+
);
|
|
220
|
+
const [deleting, setDeleting] = useState(false);
|
|
221
|
+
const [localTopics, setLocalTopics] = useState<EvaluationTopicItem[]>([]);
|
|
222
|
+
const [reordering, setReordering] = useState(false);
|
|
223
|
+
|
|
224
|
+
// Debounce search
|
|
225
|
+
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
228
|
+
debounceRef.current = setTimeout(
|
|
229
|
+
() => setDebouncedSearch(searchInput),
|
|
230
|
+
400
|
|
231
|
+
);
|
|
232
|
+
return () => {
|
|
233
|
+
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
234
|
+
};
|
|
235
|
+
}, [searchInput]);
|
|
236
|
+
|
|
237
|
+
// Reset page on filter/tab change
|
|
238
|
+
useEffect(() => {
|
|
239
|
+
setCurrentPage(1);
|
|
240
|
+
}, [debouncedSearch, activeTab]);
|
|
241
|
+
|
|
242
|
+
const {
|
|
243
|
+
data: topicList,
|
|
244
|
+
isLoading,
|
|
245
|
+
refetch,
|
|
246
|
+
} = useQuery<ApiEvaluationTopicList>({
|
|
247
|
+
queryKey: [
|
|
248
|
+
'lms-evaluation-topics',
|
|
249
|
+
currentPage,
|
|
250
|
+
PAGE_SIZE,
|
|
251
|
+
debouncedSearch,
|
|
252
|
+
activeTab,
|
|
253
|
+
],
|
|
254
|
+
queryFn: async () => {
|
|
255
|
+
const params = new URLSearchParams();
|
|
256
|
+
params.set('page', String(currentPage));
|
|
257
|
+
params.set('pageSize', String(PAGE_SIZE));
|
|
258
|
+
if (debouncedSearch) params.set('search', debouncedSearch);
|
|
259
|
+
if (activeTab !== 'all') params.set('targetType', activeTab);
|
|
260
|
+
const res = await request<ApiEvaluationTopicList>({
|
|
261
|
+
url: `/lms/evaluations/topics?${params.toString()}`,
|
|
262
|
+
method: 'GET',
|
|
263
|
+
});
|
|
264
|
+
return res.data;
|
|
265
|
+
},
|
|
266
|
+
placeholderData: (previous) =>
|
|
267
|
+
previous ?? {
|
|
268
|
+
data: [],
|
|
269
|
+
total: 0,
|
|
270
|
+
page: 1,
|
|
271
|
+
pageSize: PAGE_SIZE,
|
|
272
|
+
lastPage: 1,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const topics = topicList?.data ?? [];
|
|
277
|
+
const totalItems = topicList?.total ?? 0;
|
|
278
|
+
|
|
279
|
+
// Sync local order whenever server data refreshes
|
|
280
|
+
useEffect(() => {
|
|
281
|
+
setLocalTopics(topics);
|
|
282
|
+
}, [topics]);
|
|
283
|
+
|
|
284
|
+
const sensors = useSensors(
|
|
285
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 8 } })
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
const { data: allTopics } = useQuery<ApiEvaluationTopicList>({
|
|
289
|
+
queryKey: ['lms-evaluation-topics-all-summary'],
|
|
290
|
+
queryFn: async () => {
|
|
291
|
+
const res = await request<ApiEvaluationTopicList>({
|
|
292
|
+
url: '/lms/evaluations/topics?pageSize=1000',
|
|
293
|
+
method: 'GET',
|
|
294
|
+
});
|
|
295
|
+
return res.data;
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const kpiItems = useMemo<KpiCardItem[]>(() => {
|
|
300
|
+
const all = allTopics?.data ?? [];
|
|
301
|
+
const total = allTopics?.total ?? 0;
|
|
302
|
+
const active = all.filter((t) => t.isActive).length;
|
|
303
|
+
const inactive = all.filter((t) => !t.isActive).length;
|
|
304
|
+
const withRatings = all.filter((t) => t.ratingCount > 0).length;
|
|
305
|
+
|
|
306
|
+
return [
|
|
307
|
+
{
|
|
308
|
+
key: 'total',
|
|
309
|
+
title: t('kpi.total'),
|
|
310
|
+
value: total,
|
|
311
|
+
icon: ListChecks,
|
|
312
|
+
description: t('kpi.totalDesc'),
|
|
313
|
+
loading: false,
|
|
314
|
+
iconContainerClassName: 'bg-blue-500/10 text-blue-600',
|
|
315
|
+
accentClassName: 'from-blue-500/25 via-blue-500/10 to-transparent',
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
key: 'active',
|
|
319
|
+
title: t('kpi.active'),
|
|
320
|
+
value: active,
|
|
321
|
+
icon: Star,
|
|
322
|
+
description: t('kpi.activeDesc'),
|
|
323
|
+
loading: false,
|
|
324
|
+
iconContainerClassName: 'bg-emerald-500/10 text-emerald-700',
|
|
325
|
+
accentClassName:
|
|
326
|
+
'from-emerald-500/25 via-emerald-500/10 to-transparent',
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
key: 'inactive',
|
|
330
|
+
title: t('kpi.inactive'),
|
|
331
|
+
value: inactive,
|
|
332
|
+
icon: X,
|
|
333
|
+
description: t('kpi.inactiveDesc'),
|
|
334
|
+
loading: false,
|
|
335
|
+
iconContainerClassName: 'bg-red-500/10 text-red-600',
|
|
336
|
+
accentClassName: 'from-red-500/25 via-red-500/10 to-transparent',
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
key: 'withRatings',
|
|
340
|
+
title: t('kpi.withRatings'),
|
|
341
|
+
value: withRatings,
|
|
342
|
+
icon: Users,
|
|
343
|
+
description: t('kpi.withRatingsDesc'),
|
|
344
|
+
loading: false,
|
|
345
|
+
iconContainerClassName: 'bg-violet-500/10 text-violet-700',
|
|
346
|
+
accentClassName: 'from-violet-500/25 via-purple-500/10 to-transparent',
|
|
347
|
+
},
|
|
348
|
+
];
|
|
349
|
+
}, [t, allTopics]);
|
|
350
|
+
|
|
351
|
+
function openCreate() {
|
|
352
|
+
setEditingItem(null);
|
|
353
|
+
setSheetOpen(true);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function openEdit(item: EvaluationTopicItem) {
|
|
357
|
+
setEditingItem(item);
|
|
358
|
+
setSheetOpen(true);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function handleDelete() {
|
|
362
|
+
if (!deletingItem) return;
|
|
363
|
+
setDeleting(true);
|
|
364
|
+
try {
|
|
365
|
+
await request({
|
|
366
|
+
url: `/lms/evaluations/topics/${deletingItem.id}`,
|
|
367
|
+
method: 'DELETE',
|
|
368
|
+
});
|
|
369
|
+
toast.success(t('delete.success'));
|
|
370
|
+
setDeletingItem(null);
|
|
371
|
+
refetch();
|
|
372
|
+
} catch (err: any) {
|
|
373
|
+
const msg = err?.response?.data?.message ?? t('delete.error');
|
|
374
|
+
toast.error(msg);
|
|
375
|
+
} finally {
|
|
376
|
+
setDeleting(false);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function handleSheetSuccess() {
|
|
381
|
+
refetch();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function handleDragEnd(event: DragEndEvent) {
|
|
385
|
+
const { active, over } = event;
|
|
386
|
+
if (!over || active.id === over.id) return;
|
|
387
|
+
const oldIndex = localTopics.findIndex((t) => t.id === Number(active.id));
|
|
388
|
+
const newIndex = localTopics.findIndex((t) => t.id === Number(over.id));
|
|
389
|
+
if (oldIndex === -1 || newIndex === -1) return;
|
|
390
|
+
const reordered = arrayMove(localTopics, oldIndex, newIndex);
|
|
391
|
+
setLocalTopics(reordered);
|
|
392
|
+
setReordering(true);
|
|
393
|
+
try {
|
|
394
|
+
await request({
|
|
395
|
+
url: '/lms/evaluations/topics/reorder',
|
|
396
|
+
method: 'PATCH',
|
|
397
|
+
data: { ids: reordered.map((item) => item.id) },
|
|
398
|
+
});
|
|
399
|
+
toast.success(t('reorder.success'));
|
|
400
|
+
refetch();
|
|
401
|
+
} catch {
|
|
402
|
+
setLocalTopics(topics);
|
|
403
|
+
toast.error(t('reorder.error'));
|
|
404
|
+
} finally {
|
|
405
|
+
setReordering(false);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const tabItems: { value: EvaluationTargetType | 'all'; label: string }[] = [
|
|
410
|
+
{ value: 'all', label: t('tabs.all') },
|
|
411
|
+
...TARGET_TYPE_OPTIONS.map((opt) => ({
|
|
412
|
+
value: opt.value,
|
|
413
|
+
label: t(`targetType.${opt.labelKey}`),
|
|
414
|
+
})),
|
|
415
|
+
];
|
|
416
|
+
|
|
417
|
+
return (
|
|
418
|
+
<Page>
|
|
419
|
+
<div className="space-y-6">
|
|
420
|
+
<PageHeader
|
|
421
|
+
title={t('title')}
|
|
422
|
+
description={t('description')}
|
|
423
|
+
breadcrumbs={[
|
|
424
|
+
{ label: t('breadcrumbs.home'), href: '/' },
|
|
425
|
+
{ label: t('breadcrumbs.lms'), href: '/lms' },
|
|
426
|
+
{ label: t('breadcrumbs.evaluations') },
|
|
427
|
+
]}
|
|
428
|
+
actions={
|
|
429
|
+
<Button onClick={openCreate}>
|
|
430
|
+
<Plus className="mr-2 size-4" />
|
|
431
|
+
{t('newTopic')}
|
|
432
|
+
</Button>
|
|
433
|
+
}
|
|
434
|
+
/>
|
|
435
|
+
|
|
436
|
+
<KpiCardsGrid items={kpiItems} />
|
|
437
|
+
|
|
438
|
+
{/* Toolbar */}
|
|
439
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
440
|
+
<div className="flex min-w-40 flex-1 items-center gap-2">
|
|
441
|
+
<Input
|
|
442
|
+
className="min-w-0 flex-1"
|
|
443
|
+
placeholder={t('filters.searchPlaceholder')}
|
|
444
|
+
value={searchInput}
|
|
445
|
+
onChange={(e) => setSearchInput(e.target.value)}
|
|
446
|
+
/>
|
|
447
|
+
<Button
|
|
448
|
+
type="button"
|
|
449
|
+
variant="default"
|
|
450
|
+
size="icon"
|
|
451
|
+
className="shrink-0"
|
|
452
|
+
>
|
|
453
|
+
<Search className="size-4" />
|
|
454
|
+
</Button>
|
|
455
|
+
</div>
|
|
456
|
+
|
|
457
|
+
{searchInput && (
|
|
458
|
+
<Button
|
|
459
|
+
type="button"
|
|
460
|
+
variant="ghost"
|
|
461
|
+
size="sm"
|
|
462
|
+
onClick={() => {
|
|
463
|
+
setSearchInput('');
|
|
464
|
+
setDebouncedSearch('');
|
|
465
|
+
}}
|
|
466
|
+
className="h-9 px-2 text-muted-foreground"
|
|
467
|
+
>
|
|
468
|
+
<X className="mr-1 size-3.5" />
|
|
469
|
+
{t('filters.clear')}
|
|
470
|
+
</Button>
|
|
471
|
+
)}
|
|
472
|
+
</div>
|
|
473
|
+
|
|
474
|
+
{/* Tabs by target type */}
|
|
475
|
+
<Tabs
|
|
476
|
+
value={activeTab}
|
|
477
|
+
onValueChange={(v) => setActiveTab(v as EvaluationTargetType | 'all')}
|
|
478
|
+
>
|
|
479
|
+
<TabsList className="flex-wrap">
|
|
480
|
+
{tabItems.map((tab) => (
|
|
481
|
+
<TabsTrigger key={tab.value} value={tab.value}>
|
|
482
|
+
{tab.value !== 'all' && (
|
|
483
|
+
<span className="mr-1.5">
|
|
484
|
+
{TARGET_TYPE_ICONS[tab.value as EvaluationTargetType]}
|
|
485
|
+
</span>
|
|
486
|
+
)}
|
|
487
|
+
{tab.label}
|
|
488
|
+
</TabsTrigger>
|
|
489
|
+
))}
|
|
490
|
+
</TabsList>
|
|
491
|
+
|
|
492
|
+
{tabItems.map((tab) => (
|
|
493
|
+
<TabsContent key={tab.value} value={tab.value} className="mt-4">
|
|
494
|
+
{isLoading ? (
|
|
495
|
+
<div className="overflow-hidden rounded-xl border border-border/70">
|
|
496
|
+
<Table>
|
|
497
|
+
<TableHeader>
|
|
498
|
+
<TableRow>
|
|
499
|
+
<TableHead className="w-8 pr-0" />
|
|
500
|
+
<TableHead>{t('columns.name')}</TableHead>
|
|
501
|
+
<TableHead>{t('columns.targetType')}</TableHead>
|
|
502
|
+
<TableHead>{t('columns.ratings')}</TableHead>
|
|
503
|
+
<TableHead>{t('columns.status')}</TableHead>
|
|
504
|
+
<TableHead className="w-20" />
|
|
505
|
+
</TableRow>
|
|
506
|
+
</TableHeader>
|
|
507
|
+
<TableBody>
|
|
508
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
509
|
+
<TableRow key={i}>
|
|
510
|
+
{Array.from({ length: 6 }).map((__, j) => (
|
|
511
|
+
<TableCell key={j}>
|
|
512
|
+
<Skeleton className="h-4 w-full" />
|
|
513
|
+
</TableCell>
|
|
514
|
+
))}
|
|
515
|
+
</TableRow>
|
|
516
|
+
))}
|
|
517
|
+
</TableBody>
|
|
518
|
+
</Table>
|
|
519
|
+
</div>
|
|
520
|
+
) : topics.length === 0 ? (
|
|
521
|
+
<EmptyState
|
|
522
|
+
icon={<ListChecks className="size-8" />}
|
|
523
|
+
title={t('empty.title')}
|
|
524
|
+
description={t('empty.description')}
|
|
525
|
+
actionLabel={t('newTopic')}
|
|
526
|
+
actionIcon={<Plus className="mr-2 size-4" />}
|
|
527
|
+
onAction={openCreate}
|
|
528
|
+
/>
|
|
529
|
+
) : (
|
|
530
|
+
<DndContext
|
|
531
|
+
sensors={sensors}
|
|
532
|
+
collisionDetection={closestCenter}
|
|
533
|
+
onDragEnd={handleDragEnd}
|
|
534
|
+
>
|
|
535
|
+
<SortableContext
|
|
536
|
+
items={localTopics.map((item) => item.id)}
|
|
537
|
+
strategy={verticalListSortingStrategy}
|
|
538
|
+
>
|
|
539
|
+
<div className="overflow-hidden rounded-xl border border-border/70">
|
|
540
|
+
<Table>
|
|
541
|
+
<TableHeader>
|
|
542
|
+
<TableRow>
|
|
543
|
+
<TableHead className="w-8 pr-0" />
|
|
544
|
+
<TableHead>{t('columns.name')}</TableHead>
|
|
545
|
+
<TableHead>{t('columns.targetType')}</TableHead>
|
|
546
|
+
<TableHead>{t('columns.ratings')}</TableHead>
|
|
547
|
+
<TableHead>{t('columns.status')}</TableHead>
|
|
548
|
+
<TableHead className="w-20" />
|
|
549
|
+
</TableRow>
|
|
550
|
+
</TableHeader>
|
|
551
|
+
<TableBody>
|
|
552
|
+
{localTopics.map((item) => (
|
|
553
|
+
<SortableTopicRow
|
|
554
|
+
key={item.id}
|
|
555
|
+
item={item}
|
|
556
|
+
onEdit={openEdit}
|
|
557
|
+
onDelete={setDeletingItem}
|
|
558
|
+
/>
|
|
559
|
+
))}
|
|
560
|
+
</TableBody>
|
|
561
|
+
</Table>
|
|
562
|
+
</div>
|
|
563
|
+
</SortableContext>
|
|
564
|
+
</DndContext>
|
|
565
|
+
)}
|
|
566
|
+
</TabsContent>
|
|
567
|
+
))}
|
|
568
|
+
</Tabs>
|
|
569
|
+
|
|
570
|
+
{/* Pagination */}
|
|
571
|
+
{!isLoading && totalItems > PAGE_SIZE && (
|
|
572
|
+
<div className="mt-6">
|
|
573
|
+
<PaginationFooter
|
|
574
|
+
currentPage={currentPage}
|
|
575
|
+
pageSize={PAGE_SIZE}
|
|
576
|
+
totalItems={totalItems}
|
|
577
|
+
onPageChange={setCurrentPage}
|
|
578
|
+
onPageSizeChange={() => {}}
|
|
579
|
+
pageSizeOptions={[PAGE_SIZE]}
|
|
580
|
+
/>
|
|
581
|
+
</div>
|
|
582
|
+
)}
|
|
583
|
+
</div>
|
|
584
|
+
|
|
585
|
+
<EvaluationTopicFormSheet
|
|
586
|
+
open={sheetOpen}
|
|
587
|
+
onOpenChange={setSheetOpen}
|
|
588
|
+
editingItem={editingItem}
|
|
589
|
+
onSuccess={handleSheetSuccess}
|
|
590
|
+
/>
|
|
591
|
+
|
|
592
|
+
<AlertDialog
|
|
593
|
+
open={!!deletingItem}
|
|
594
|
+
onOpenChange={(open) => {
|
|
595
|
+
if (!open) setDeletingItem(null);
|
|
596
|
+
}}
|
|
597
|
+
>
|
|
598
|
+
<AlertDialogContent>
|
|
599
|
+
<AlertDialogHeader>
|
|
600
|
+
<AlertDialogTitle>{t('delete.title')}</AlertDialogTitle>
|
|
601
|
+
<AlertDialogDescription>
|
|
602
|
+
{t('delete.description', { name: deletingItem?.name ?? '' })}
|
|
603
|
+
</AlertDialogDescription>
|
|
604
|
+
</AlertDialogHeader>
|
|
605
|
+
<AlertDialogFooter>
|
|
606
|
+
<AlertDialogCancel disabled={deleting}>
|
|
607
|
+
{t('delete.cancel')}
|
|
608
|
+
</AlertDialogCancel>
|
|
609
|
+
<AlertDialogAction
|
|
610
|
+
onClick={handleDelete}
|
|
611
|
+
disabled={deleting}
|
|
612
|
+
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
613
|
+
>
|
|
614
|
+
{deleting ? t('delete.deleting') : t('delete.confirm')}
|
|
615
|
+
</AlertDialogAction>
|
|
616
|
+
</AlertDialogFooter>
|
|
617
|
+
</AlertDialogContent>
|
|
618
|
+
</AlertDialog>
|
|
619
|
+
</Page>
|
|
620
|
+
);
|
|
621
|
+
}
|