@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.
Files changed (128) hide show
  1. package/dist/class-group/class-group.controller.d.ts +65 -2
  2. package/dist/class-group/class-group.controller.d.ts.map +1 -1
  3. package/dist/class-group/class-group.controller.js +35 -0
  4. package/dist/class-group/class-group.controller.js.map +1 -1
  5. package/dist/class-group/class-group.service.d.ts +67 -2
  6. package/dist/class-group/class-group.service.d.ts.map +1 -1
  7. package/dist/class-group/class-group.service.js +164 -13
  8. package/dist/class-group/class-group.service.js.map +1 -1
  9. package/dist/class-group/dto/create-class-group.dto.d.ts.map +1 -1
  10. package/dist/class-group/dto/create-class-group.dto.js +2 -1
  11. package/dist/class-group/dto/create-class-group.dto.js.map +1 -1
  12. package/dist/class-group/dto/material.dto.d.ts +18 -0
  13. package/dist/class-group/dto/material.dto.d.ts.map +1 -0
  14. package/dist/class-group/dto/material.dto.js +86 -0
  15. package/dist/class-group/dto/material.dto.js.map +1 -0
  16. package/dist/course/course.service.d.ts +2 -0
  17. package/dist/course/course.service.d.ts.map +1 -1
  18. package/dist/course/course.service.js +27 -2
  19. package/dist/course/course.service.js.map +1 -1
  20. package/dist/course/dto/create-course.dto.d.ts +2 -2
  21. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  22. package/dist/course/dto/create-course.dto.js.map +1 -1
  23. package/dist/enterprise/enterprise.controller.d.ts +7 -1
  24. package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
  25. package/dist/enterprise/enterprise.controller.js +72 -2
  26. package/dist/enterprise/enterprise.controller.js.map +1 -1
  27. package/dist/enterprise/enterprise.module.d.ts.map +1 -1
  28. package/dist/enterprise/enterprise.module.js +2 -1
  29. package/dist/enterprise/enterprise.module.js.map +1 -1
  30. package/dist/enterprise/enterprise.service.d.ts +3 -0
  31. package/dist/enterprise/enterprise.service.d.ts.map +1 -1
  32. package/dist/enterprise/enterprise.service.js +84 -1
  33. package/dist/enterprise/enterprise.service.js.map +1 -1
  34. package/dist/enterprise/training/enterprise-training.module.d.ts +3 -0
  35. package/dist/enterprise/training/enterprise-training.module.d.ts.map +1 -0
  36. package/dist/enterprise/training/enterprise-training.module.js +40 -0
  37. package/dist/enterprise/training/enterprise-training.module.js.map +1 -0
  38. package/dist/enterprise/training/training-admin.controller.d.ts +525 -0
  39. package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -0
  40. package/dist/enterprise/training/training-admin.controller.js +385 -0
  41. package/dist/enterprise/training/training-admin.controller.js.map +1 -0
  42. package/dist/enterprise/training/training-admin.service.d.ts +582 -0
  43. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -0
  44. package/dist/enterprise/training/training-admin.service.js +2283 -0
  45. package/dist/enterprise/training/training-admin.service.js.map +1 -0
  46. package/dist/enterprise/training/training-instructor.controller.d.ts +260 -0
  47. package/dist/enterprise/training/training-instructor.controller.d.ts.map +1 -0
  48. package/dist/enterprise/training/training-instructor.controller.js +199 -0
  49. package/dist/enterprise/training/training-instructor.controller.js.map +1 -0
  50. package/dist/enterprise/training/training-instructor.service.d.ts +280 -0
  51. package/dist/enterprise/training/training-instructor.service.d.ts.map +1 -0
  52. package/dist/enterprise/training/training-instructor.service.js +1218 -0
  53. package/dist/enterprise/training/training-instructor.service.js.map +1 -0
  54. package/dist/enterprise/training/training-student.controller.d.ts +168 -0
  55. package/dist/enterprise/training/training-student.controller.d.ts.map +1 -0
  56. package/dist/enterprise/training/training-student.controller.js +104 -0
  57. package/dist/enterprise/training/training-student.controller.js.map +1 -0
  58. package/dist/enterprise/training/training-student.service.d.ts +185 -0
  59. package/dist/enterprise/training/training-student.service.d.ts.map +1 -0
  60. package/dist/enterprise/training/training-student.service.js +674 -0
  61. package/dist/enterprise/training/training-student.service.js.map +1 -0
  62. package/dist/enterprise/training/training-viewer.controller.d.ts +298 -0
  63. package/dist/enterprise/training/training-viewer.controller.d.ts.map +1 -0
  64. package/dist/enterprise/training/training-viewer.controller.js +178 -0
  65. package/dist/enterprise/training/training-viewer.controller.js.map +1 -0
  66. package/dist/evaluation/dto/create-evaluation-topic.dto.d.ts +18 -0
  67. package/dist/evaluation/dto/create-evaluation-topic.dto.d.ts.map +1 -0
  68. package/dist/evaluation/dto/create-evaluation-topic.dto.js +59 -0
  69. package/dist/evaluation/dto/create-evaluation-topic.dto.js.map +1 -0
  70. package/dist/evaluation/dto/update-evaluation-topic.dto.d.ts +6 -0
  71. package/dist/evaluation/dto/update-evaluation-topic.dto.d.ts.map +1 -0
  72. package/dist/evaluation/dto/update-evaluation-topic.dto.js +9 -0
  73. package/dist/evaluation/dto/update-evaluation-topic.dto.js.map +1 -0
  74. package/dist/evaluation/evaluation.controller.d.ts +66 -0
  75. package/dist/evaluation/evaluation.controller.d.ts.map +1 -1
  76. package/dist/evaluation/evaluation.controller.js +73 -0
  77. package/dist/evaluation/evaluation.controller.js.map +1 -1
  78. package/dist/evaluation/evaluation.service.d.ts +71 -0
  79. package/dist/evaluation/evaluation.service.d.ts.map +1 -1
  80. package/dist/evaluation/evaluation.service.js +121 -0
  81. package/dist/evaluation/evaluation.service.js.map +1 -1
  82. package/dist/instructor/instructor.service.js +6 -6
  83. package/dist/instructor/instructor.service.js.map +1 -1
  84. package/dist/lms.module.d.ts.map +1 -1
  85. package/dist/lms.module.js +3 -0
  86. package/dist/lms.module.js.map +1 -1
  87. package/hedhog/data/menu.yaml +19 -2
  88. package/hedhog/data/route.yaml +730 -0
  89. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +74 -8
  90. package/hedhog/frontend/app/_components/course-avatar.tsx.ejs +27 -47
  91. package/hedhog/frontend/app/classes/[id]/_components/event-summary-popover.tsx.ejs +15 -15
  92. package/hedhog/frontend/app/classes/[id]/_components/quick-create-session-popover.tsx.ejs +5 -5
  93. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +2141 -308
  94. package/hedhog/frontend/app/classes/page.tsx.ejs +8 -7
  95. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +21 -8
  96. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +10 -6
  97. package/hedhog/frontend/app/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +201 -0
  98. package/hedhog/frontend/app/evaluations/_components/evaluation-topic-types.ts.ejs +49 -0
  99. package/hedhog/frontend/app/evaluations/page.tsx.ejs +621 -1250
  100. package/hedhog/frontend/app/instructors/page.tsx.ejs +22 -20
  101. package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +1278 -0
  102. package/hedhog/frontend/messages/en.json +98 -7
  103. package/hedhog/frontend/messages/pt.json +98 -7
  104. package/hedhog/table/course_class_group_material.yaml +45 -0
  105. package/package.json +8 -8
  106. package/src/class-group/class-group.controller.ts +30 -0
  107. package/src/class-group/class-group.service.ts +176 -5
  108. package/src/class-group/dto/create-class-group.dto.ts +8 -8
  109. package/src/class-group/dto/material.dto.ts +69 -0
  110. package/src/course/course.service.ts +54 -21
  111. package/src/course/dto/create-course.dto.ts +8 -8
  112. package/src/enterprise/enterprise.controller.ts +62 -1
  113. package/src/enterprise/enterprise.module.ts +2 -1
  114. package/src/enterprise/enterprise.service.ts +84 -1
  115. package/src/enterprise/training/enterprise-training.module.ts +27 -0
  116. package/src/enterprise/training/training-admin.controller.ts +278 -0
  117. package/src/enterprise/training/training-admin.service.ts +2523 -0
  118. package/src/enterprise/training/training-instructor.controller.ts +141 -0
  119. package/src/enterprise/training/training-instructor.service.ts +1303 -0
  120. package/src/enterprise/training/training-student.controller.ts +65 -0
  121. package/src/enterprise/training/training-student.service.ts +762 -0
  122. package/src/enterprise/training/training-viewer.controller.ts +115 -0
  123. package/src/evaluation/dto/create-evaluation-topic.dto.ts +48 -0
  124. package/src/evaluation/dto/update-evaluation-topic.dto.ts +6 -0
  125. package/src/evaluation/evaluation.controller.ts +63 -1
  126. package/src/evaluation/evaluation.service.ts +150 -1
  127. package/src/instructor/instructor.service.ts +4 -4
  128. package/src/lms.module.ts +3 -0
@@ -94,6 +94,8 @@ export type ApiClass = {
94
94
  instructorId?: number | null;
95
95
  courseTitle: string;
96
96
  enrolledCount: number;
97
+ location?: string | null;
98
+ virtualRoomUrl?: string | null;
97
99
  professor?: string | null;
98
100
  professorName?: string | null;
99
101
  instructor?: string | null;
@@ -171,6 +173,7 @@ type TurmaForm = {
171
173
  sessionRecurrenceUntil?: string;
172
174
  sessionTitleMode: 'default-course-code' | 'custom';
173
175
  sessionTitle?: string;
176
+ sessionAccess?: string;
174
177
  status: string;
175
178
  };
176
179
 
@@ -422,6 +425,7 @@ function getTurmaSchema(t: (key: string) => string) {
422
425
  .enum(['default-course-code', 'custom'] as const)
423
426
  .default('default-course-code'),
424
427
  sessionTitle: z.string().optional(),
428
+ sessionAccess: z.string().trim().max(500).optional(),
425
429
  status: z.string().min(1, t('form.validation.statusRequired')),
426
430
  instructorId: z.number().int().positive().optional(),
427
431
  })
@@ -548,6 +552,7 @@ export function ClassFormSheet({
548
552
  sessionRecurrenceUntil: '',
549
553
  sessionTitleMode: 'default-course-code',
550
554
  sessionTitle: '',
555
+ sessionAccess: '',
551
556
  status: 'aberta',
552
557
  },
553
558
  });
@@ -645,6 +650,8 @@ export function ClassFormSheet({
645
650
  sessionTitle:
646
651
  data.sessionTitle ??
647
652
  getDefaultSessionTitle(data.courseTitle, data.code),
653
+ sessionAccess:
654
+ data.virtualRoomUrl?.trim() || data.location?.trim() || '',
648
655
  status: toPtStatus(data.status),
649
656
  });
650
657
  })
@@ -680,6 +687,7 @@ export function ClassFormSheet({
680
687
  sessionRecurrenceUntil: '',
681
688
  sessionTitleMode: 'default-course-code',
682
689
  sessionTitle: getDefaultSessionTitle(undefined, nextCode),
690
+ sessionAccess: '',
683
691
  status: 'aberta',
684
692
  });
685
693
  }
@@ -699,6 +707,20 @@ export function ClassFormSheet({
699
707
  [selectedCourseTitle, watchedFormValues.codigo]
700
708
  );
701
709
 
710
+ const sessionAccessLabel = t.has('form.fields.sessionAccess.label')
711
+ ? t('form.fields.sessionAccess.label')
712
+ : 'Local / link das aulas';
713
+ const sessionAccessPlaceholder = t.has(
714
+ 'form.fields.sessionAccess.placeholder'
715
+ )
716
+ ? t('form.fields.sessionAccess.placeholder')
717
+ : 'Sala 201 ou https://meet.google.com/...';
718
+ const sessionAccessDescription = t.has(
719
+ 'form.fields.sessionAccess.description'
720
+ )
721
+ ? t('form.fields.sessionAccess.description')
722
+ : 'Opcional. Quando preenchido, será aplicado a todas as aulas geradas para esta turma.';
723
+
702
724
  const filteredEndTimeOptions = useMemo(() => {
703
725
  const startTime = watchedFormValues.horarioInicio ?? '';
704
726
  if (!startTime) return TIME_OPTIONS;
@@ -732,6 +754,7 @@ export function ClassFormSheet({
732
754
  ).map(([value, label]) => ({
733
755
  value: value as SessionRecurrenceDay,
734
756
  label,
757
+ shortLabel: label.trim().slice(0, 3),
735
758
  })),
736
759
  [t]
737
760
  );
@@ -1015,8 +1038,15 @@ export function ClassFormSheet({
1015
1038
  data.sessionTitleMode === 'custom'
1016
1039
  ? data.sessionTitle?.trim() || defaultSessionTitle
1017
1040
  : defaultSessionTitle;
1041
+ const sessionAccess = data.sessionAccess?.trim() || undefined;
1042
+ const isOnlineClass = data.tipo === 'online';
1043
+ const isHybridClass = data.tipo === 'hibrida';
1018
1044
  const sessionTemplate = {
1019
1045
  title: sessionTitle,
1046
+ ...(sessionAccess && !isOnlineClass ? { location: sessionAccess } : {}),
1047
+ ...(sessionAccess && (isOnlineClass || isHybridClass)
1048
+ ? { meetingUrl: sessionAccess }
1049
+ : {}),
1020
1050
  recurrence: buildSessionRecurrencePayload(data),
1021
1051
  };
1022
1052
 
@@ -1032,6 +1062,14 @@ export function ClassFormSheet({
1032
1062
  startTime: data.horarioInicio,
1033
1063
  endTime: data.horarioFim,
1034
1064
  capacity: data.vagas,
1065
+ location:
1066
+ sessionAccess && (!isOnlineClass || isHybridClass)
1067
+ ? sessionAccess
1068
+ : null,
1069
+ virtualRoomUrl:
1070
+ sessionAccess && (isOnlineClass || isHybridClass)
1071
+ ? sessionAccess
1072
+ : null,
1035
1073
  sessionTemplate,
1036
1074
  };
1037
1075
 
@@ -1344,6 +1382,30 @@ export function ClassFormSheet({
1344
1382
  </FieldError>
1345
1383
  </Field>
1346
1384
  </div>
1385
+
1386
+ <Field>
1387
+ <FieldLabel htmlFor="sessionAccess">
1388
+ {sessionAccessLabel}
1389
+ </FieldLabel>
1390
+ <Input
1391
+ id="sessionAccess"
1392
+ value={watchedFormValues.sessionAccess ?? ''}
1393
+ placeholder={sessionAccessPlaceholder}
1394
+ onChange={(event) =>
1395
+ form.setValue('sessionAccess', event.target.value, {
1396
+ shouldDirty: true,
1397
+ shouldTouch: true,
1398
+ shouldValidate: true,
1399
+ })
1400
+ }
1401
+ />
1402
+ <FieldDescription>
1403
+ {sessionAccessDescription}
1404
+ </FieldDescription>
1405
+ <FieldError>
1406
+ {form.formState.errors.sessionAccess?.message}
1407
+ </FieldError>
1408
+ </Field>
1347
1409
  </div>
1348
1410
  </div>
1349
1411
 
@@ -1408,7 +1470,7 @@ export function ClassFormSheet({
1408
1470
  <FieldLabel>
1409
1471
  {t('form.recurrence.customDialog.repeatOn')}
1410
1472
  </FieldLabel>
1411
- <div className="flex flex-wrap gap-2">
1473
+ <div className="grid grid-cols-4 gap-2 sm:grid-cols-7">
1412
1474
  {recurrenceDayOptions.map((day) => {
1413
1475
  const active = (
1414
1476
  watchedFormValues.sessionRecurrenceDaysOfWeek ?? []
@@ -1421,14 +1483,16 @@ export function ClassFormSheet({
1421
1483
  key={day.value}
1422
1484
  type="button"
1423
1485
  variant={active ? 'default' : 'outline'}
1424
- size="icon"
1425
- className="rounded-full"
1486
+ size="sm"
1487
+ className="h-9"
1426
1488
  disabled={isWeekdays}
1489
+ title={day.label}
1490
+ aria-label={day.label}
1427
1491
  onClick={() =>
1428
1492
  toggleCustomRecurrenceDay(day.value)
1429
1493
  }
1430
1494
  >
1431
- {day.label}
1495
+ {day.shortLabel}
1432
1496
  </Button>
1433
1497
  );
1434
1498
  })}
@@ -1876,7 +1940,7 @@ export function ClassFormSheet({
1876
1940
  <FieldLabel>
1877
1941
  {t('form.recurrence.customDialog.repeatOn')}
1878
1942
  </FieldLabel>
1879
- <div className="flex flex-wrap gap-2">
1943
+ <div className="grid grid-cols-4 gap-2 sm:grid-cols-7">
1880
1944
  {recurrenceDayOptions.map((day) => {
1881
1945
  const active = (
1882
1946
  watchedFormValues.sessionRecurrenceDaysOfWeek ?? []
@@ -1886,11 +1950,13 @@ export function ClassFormSheet({
1886
1950
  key={day.value}
1887
1951
  type="button"
1888
1952
  variant={active ? 'default' : 'outline'}
1889
- size="icon"
1890
- className="rounded-full"
1953
+ size="sm"
1954
+ className="h-9"
1955
+ title={day.label}
1956
+ aria-label={day.label}
1891
1957
  onClick={() => toggleCustomRecurrenceDay(day.value)}
1892
1958
  >
1893
- {day.label}
1959
+ {day.shortLabel}
1894
1960
  </Button>
1895
1961
  );
1896
1962
  })}
@@ -1,9 +1,8 @@
1
- 'use client';
2
-
3
- import { cn } from '@/lib/utils';
4
- import { useApp } from '@hed-hog/next-app-provider';
5
- import { BookOpen } from 'lucide-react';
6
- import { useEffect, useState } from 'react';
1
+ 'use client';
2
+
3
+ import { cn } from '@/lib/utils';
4
+ import { BookOpen } from 'lucide-react';
5
+ import { useState } from 'react';
7
6
 
8
7
  type CourseAvatarProps = {
9
8
  fileId?: number | null;
@@ -15,47 +14,28 @@ type CourseAvatarProps = {
15
14
  export function CourseAvatar({
16
15
  fileId,
17
16
  title,
18
- className,
19
- iconSize = 'size-6',
20
- }: CourseAvatarProps) {
21
- const { request } = useApp();
22
- const [url, setUrl] = useState<string | null>(null);
23
-
24
- useEffect(() => {
25
- if (!fileId) {
26
- setUrl(null);
27
- return;
28
- }
29
-
30
- let cancelled = false;
31
-
32
- request<{ url?: string }>({
33
- url: `/file/open/${fileId}`,
34
- method: 'PUT',
35
- })
36
- .then((res) => {
37
- if (!cancelled && res?.data?.url) {
38
- setUrl(res.data.url);
39
- }
40
- })
41
- .catch(() => {
42
- if (!cancelled) setUrl(null);
43
- });
44
-
45
- return () => {
46
- cancelled = true;
47
- };
48
- }, [fileId, request]);
49
-
50
- if (url) {
51
- return (
52
- <img
53
- src={url}
54
- alt={title}
55
- className={cn('shrink-0 object-cover', className)}
56
- />
57
- );
58
- }
17
+ className,
18
+ iconSize = 'size-6',
19
+ }: CourseAvatarProps) {
20
+ const [failedFileId, setFailedFileId] = useState<number | null>(null);
21
+
22
+ const apiBaseUrl = String(process.env.NEXT_PUBLIC_API_BASE_URL || '').replace(
23
+ /\/$/,
24
+ ''
25
+ );
26
+ const url = fileId && apiBaseUrl ? `${apiBaseUrl}/file/open/${fileId}` : null;
27
+ const imageFailed = !!fileId && failedFileId === fileId;
28
+
29
+ if (url && !imageFailed) {
30
+ return (
31
+ <img
32
+ src={url}
33
+ alt={title}
34
+ className={cn('shrink-0 object-cover', className)}
35
+ onError={() => setFailedFileId(fileId ?? null)}
36
+ />
37
+ );
38
+ }
59
39
 
60
40
  return (
61
41
  <div
@@ -183,11 +183,11 @@ export function EventSummaryPopover({
183
183
  <>
184
184
  <div className="fixed inset-0 z-40" onClick={handleClose} />
185
185
 
186
- <div
187
- className="fixed z-50 w-105 max-h-[calc(100vh-24px)] overflow-y-auto rounded-lg border bg-popover p-4 shadow-lg outline-none"
188
- style={{ top: pos.top, left: pos.left }}
189
- onClick={(mouseEvent) => mouseEvent.stopPropagation()}
190
- >
186
+ <div
187
+ className="fixed z-50 w-105 max-h-[calc(100vh-24px)] overflow-y-auto rounded-lg border border-border bg-card p-4 text-card-foreground shadow-lg outline-none"
188
+ style={{ top: pos.top, left: pos.left }}
189
+ onClick={(mouseEvent) => mouseEvent.stopPropagation()}
190
+ >
191
191
  <div className="mb-3 flex items-center justify-between gap-2">
192
192
  <Badge variant={event.tipo === 'online' ? 'secondary' : 'outline'}>
193
193
  {tClasses(`type.${event.tipo}`)}
@@ -234,10 +234,10 @@ export function EventSummaryPopover({
234
234
  ) : null}
235
235
  </div>
236
236
 
237
- <div className="rounded-md border bg-muted/30 p-3 text-sm">
238
- {loadingAttendance ? (
239
- <div className="flex items-center gap-2 text-muted-foreground">
240
- <Loader2 className="size-4 animate-spin" />
237
+ <div className="rounded-md border border-border bg-muted/30 p-3 text-sm">
238
+ {loadingAttendance ? (
239
+ <div className="flex items-center gap-2 text-muted-foreground">
240
+ <Loader2 className="size-4 animate-spin" />
241
241
  {t('attendance.loading')}
242
242
  </div>
243
243
  ) : hasAttendance ? (
@@ -247,12 +247,12 @@ export function EventSummaryPopover({
247
247
  total: attendanceSummary?.total ?? 0,
248
248
  })}
249
249
  </p>
250
- ) : (
251
- <div className="space-y-2">
252
- <div className="flex items-start gap-2 text-amber-700">
253
- <AlertTriangle className="mt-0.5 size-4" />
254
- <p className="text-sm">{t('attendance.missing')}</p>
255
- </div>
250
+ ) : (
251
+ <div className="space-y-2">
252
+ <div className="flex items-start gap-2 text-amber-700 dark:text-amber-300">
253
+ <AlertTriangle className="mt-0.5 size-4" />
254
+ <p className="text-sm">{t('attendance.missing')}</p>
255
+ </div>
256
256
  <Button
257
257
  type="button"
258
258
  variant="outline"
@@ -555,11 +555,11 @@ export function QuickCreateSessionPopover({
555
555
  <>
556
556
  <div className="fixed inset-0 z-40" onClick={handleClose} />
557
557
 
558
- <div
559
- className="fixed z-50 w-90 max-h-[calc(100vh-24px)] overflow-y-auto rounded-lg border bg-popover p-4 shadow-lg outline-none"
560
- style={{ top: pos.top, left: pos.left }}
561
- onClick={(event) => event.stopPropagation()}
562
- >
558
+ <div
559
+ className="fixed z-50 w-90 max-h-[calc(100vh-24px)] overflow-y-auto rounded-lg border border-border bg-card p-4 text-card-foreground shadow-lg outline-none"
560
+ style={{ top: pos.top, left: pos.left }}
561
+ onClick={(event) => event.stopPropagation()}
562
+ >
563
563
  <div className="mb-3 flex items-center justify-between">
564
564
  <div className="flex items-center gap-2">
565
565
  <CalendarPlus className="size-4 text-muted-foreground" />