@hed-hog/lms 0.0.365 → 0.0.370

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 (243) hide show
  1. package/dist/certificate/certificate.controller.d.ts +1 -1
  2. package/dist/certificate/certificate.controller.d.ts.map +1 -1
  3. package/dist/certificate/certificate.controller.js +4 -2
  4. package/dist/certificate/certificate.controller.js.map +1 -1
  5. package/dist/certificate/certificate.service.d.ts +50 -0
  6. package/dist/certificate/certificate.service.d.ts.map +1 -1
  7. package/dist/certificate/certificate.service.js +73 -0
  8. package/dist/certificate/certificate.service.js.map +1 -1
  9. package/dist/class-group/class-group.controller.d.ts +1 -0
  10. package/dist/class-group/class-group.controller.d.ts.map +1 -1
  11. package/dist/class-group/class-group.service.d.ts +1 -0
  12. package/dist/class-group/class-group.service.d.ts.map +1 -1
  13. package/dist/course/course-ai-usage.service.d.ts +58 -0
  14. package/dist/course/course-ai-usage.service.d.ts.map +1 -0
  15. package/dist/course/course-ai-usage.service.js +176 -0
  16. package/dist/course/course-ai-usage.service.js.map +1 -0
  17. package/dist/course/course-audio-transcription.service.d.ts +65 -1
  18. package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
  19. package/dist/course/course-audio-transcription.service.js +381 -29
  20. package/dist/course/course-audio-transcription.service.js.map +1 -1
  21. package/dist/course/course-export-scorm12.service.d.ts +3 -0
  22. package/dist/course/course-export-scorm12.service.d.ts.map +1 -1
  23. package/dist/course/course-export-scorm12.service.js +141 -6
  24. package/dist/course/course-export-scorm12.service.js.map +1 -1
  25. package/dist/course/course-export.service.d.ts.map +1 -1
  26. package/dist/course/course-export.service.js +2 -1
  27. package/dist/course/course-export.service.js.map +1 -1
  28. package/dist/course/course-lesson.controller.d.ts +25 -3
  29. package/dist/course/course-lesson.controller.d.ts.map +1 -1
  30. package/dist/course/course-lesson.controller.js +71 -8
  31. package/dist/course/course-lesson.controller.js.map +1 -1
  32. package/dist/course/course-structure.controller.d.ts +30 -7
  33. package/dist/course/course-structure.controller.d.ts.map +1 -1
  34. package/dist/course/course-structure.controller.js +37 -4
  35. package/dist/course/course-structure.controller.js.map +1 -1
  36. package/dist/course/course-structure.service.d.ts +37 -5
  37. package/dist/course/course-structure.service.d.ts.map +1 -1
  38. package/dist/course/course-structure.service.js +165 -20
  39. package/dist/course/course-structure.service.js.map +1 -1
  40. package/dist/course/course-transcription-translation.service.d.ts +31 -0
  41. package/dist/course/course-transcription-translation.service.d.ts.map +1 -0
  42. package/dist/course/course-transcription-translation.service.js +227 -0
  43. package/dist/course/course-transcription-translation.service.js.map +1 -0
  44. package/dist/course/course-video-agent-pipeline.service.d.ts +70 -0
  45. package/dist/course/course-video-agent-pipeline.service.d.ts.map +1 -0
  46. package/dist/course/course-video-agent-pipeline.service.js +398 -0
  47. package/dist/course/course-video-agent-pipeline.service.js.map +1 -0
  48. package/dist/course/course-video-hls.service.d.ts +14 -0
  49. package/dist/course/course-video-hls.service.d.ts.map +1 -1
  50. package/dist/course/course-video-hls.service.js +25 -8
  51. package/dist/course/course-video-hls.service.js.map +1 -1
  52. package/dist/course/course.controller.d.ts +2 -0
  53. package/dist/course/course.controller.d.ts.map +1 -1
  54. package/dist/course/course.module.d.ts.map +1 -1
  55. package/dist/course/course.module.js +9 -0
  56. package/dist/course/course.module.js.map +1 -1
  57. package/dist/course/course.service.d.ts +2 -0
  58. package/dist/course/course.service.d.ts.map +1 -1
  59. package/dist/course/course.service.js +36 -2
  60. package/dist/course/course.service.js.map +1 -1
  61. package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
  62. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
  63. package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
  64. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
  65. package/dist/course/dto/create-course-export.dto.d.ts +1 -0
  66. package/dist/course/dto/create-course-export.dto.d.ts.map +1 -1
  67. package/dist/course/dto/create-course-export.dto.js +6 -0
  68. package/dist/course/dto/create-course-export.dto.js.map +1 -1
  69. package/dist/course/ffmpeg.util.d.ts +10 -0
  70. package/dist/course/ffmpeg.util.d.ts.map +1 -0
  71. package/dist/course/ffmpeg.util.js +79 -0
  72. package/dist/course/ffmpeg.util.js.map +1 -0
  73. package/dist/course/lms-bulk-upload-automation.service.d.ts +3 -1
  74. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
  75. package/dist/course/lms-bulk-upload-automation.service.js +33 -16
  76. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
  77. package/dist/course/lms-bulk-upload.controller.d.ts +3 -0
  78. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  79. package/dist/course/lms-bulk-upload.service.d.ts +3 -0
  80. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  81. package/dist/course/lms-bulk-upload.service.js +48 -29
  82. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  83. package/dist/course/subtitle.util.d.ts +46 -0
  84. package/dist/course/subtitle.util.d.ts.map +1 -0
  85. package/dist/course/subtitle.util.js +206 -0
  86. package/dist/course/subtitle.util.js.map +1 -0
  87. package/dist/enterprise/training/training-admin.controller.d.ts +2 -0
  88. package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
  89. package/dist/enterprise/training/training-admin.service.d.ts +2 -0
  90. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
  91. package/dist/enterprise/training/training-student.service.d.ts +27 -0
  92. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  93. package/dist/enterprise/training/training-student.service.js +197 -10
  94. package/dist/enterprise/training/training-student.service.js.map +1 -1
  95. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +3 -1
  96. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
  97. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +19 -5
  98. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
  99. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -1
  100. package/dist/lesson-xp-map/lesson-xp-map.module.js +2 -1
  101. package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -1
  102. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
  103. package/dist/lms.module.d.ts.map +1 -1
  104. package/dist/lms.module.js +14 -0
  105. package/dist/lms.module.js.map +1 -1
  106. package/dist/platforma/dto/heartbeat.dto.d.ts +9 -0
  107. package/dist/platforma/dto/heartbeat.dto.d.ts.map +1 -0
  108. package/dist/platforma/dto/heartbeat.dto.js +50 -0
  109. package/dist/platforma/dto/heartbeat.dto.js.map +1 -0
  110. package/dist/platforma/handlers/emit-certificate.handler.d.ts +27 -0
  111. package/dist/platforma/handlers/emit-certificate.handler.d.ts.map +1 -0
  112. package/dist/platforma/handlers/emit-certificate.handler.js +117 -0
  113. package/dist/platforma/handlers/emit-certificate.handler.js.map +1 -0
  114. package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts +31 -0
  115. package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts.map +1 -0
  116. package/dist/platforma/handlers/lesson-heartbeat.handler.js +281 -0
  117. package/dist/platforma/handlers/lesson-heartbeat.handler.js.map +1 -0
  118. package/dist/platforma/platforma-heartbeat.service.d.ts +10 -0
  119. package/dist/platforma/platforma-heartbeat.service.d.ts.map +1 -0
  120. package/dist/platforma/platforma-heartbeat.service.js +50 -0
  121. package/dist/platforma/platforma-heartbeat.service.js.map +1 -0
  122. package/dist/platforma/platforma-performance.service.d.ts +121 -0
  123. package/dist/platforma/platforma-performance.service.d.ts.map +1 -0
  124. package/dist/platforma/platforma-performance.service.js +500 -0
  125. package/dist/platforma/platforma-performance.service.js.map +1 -0
  126. package/dist/platforma/platforma-search.service.d.ts +21 -0
  127. package/dist/platforma/platforma-search.service.d.ts.map +1 -0
  128. package/dist/platforma/platforma-search.service.js +64 -0
  129. package/dist/platforma/platforma-search.service.js.map +1 -0
  130. package/dist/platforma/platforma-video.service.d.ts +8 -0
  131. package/dist/platforma/platforma-video.service.d.ts.map +1 -1
  132. package/dist/platforma/platforma-video.service.js +45 -2
  133. package/dist/platforma/platforma-video.service.js.map +1 -1
  134. package/dist/platforma/platforma.controller.d.ts +213 -1
  135. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  136. package/dist/platforma/platforma.controller.js +159 -2
  137. package/dist/platforma/platforma.controller.js.map +1 -1
  138. package/dist/realtime/lms-realtime.controller.d.ts +2 -0
  139. package/dist/realtime/lms-realtime.controller.d.ts.map +1 -1
  140. package/dist/realtime/lms-realtime.controller.js +31 -0
  141. package/dist/realtime/lms-realtime.controller.js.map +1 -1
  142. package/dist/realtime/lms-realtime.service.d.ts +1 -1
  143. package/dist/realtime/lms-realtime.service.d.ts.map +1 -1
  144. package/dist/realtime/lms-realtime.service.js.map +1 -1
  145. package/dist/training/dto/create-training.dto.d.ts +9 -0
  146. package/dist/training/dto/create-training.dto.d.ts.map +1 -1
  147. package/dist/training/dto/create-training.dto.js +45 -1
  148. package/dist/training/dto/create-training.dto.js.map +1 -1
  149. package/dist/training/training.controller.d.ts +144 -0
  150. package/dist/training/training.controller.d.ts.map +1 -1
  151. package/dist/training/training.service.d.ts +149 -0
  152. package/dist/training/training.service.d.ts.map +1 -1
  153. package/dist/training/training.service.js +332 -167
  154. package/dist/training/training.service.js.map +1 -1
  155. package/hedhog/data/image_type.yaml +10 -0
  156. package/hedhog/data/route.yaml +251 -0
  157. package/hedhog/data/setting_group.yaml +97 -0
  158. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +139 -27
  159. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +182 -29
  160. package/hedhog/frontend/app/classes/_components/classes-calendar-view.tsx.ejs +277 -0
  161. package/hedhog/frontend/app/classes/page.tsx.ejs +127 -20
  162. package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +69 -57
  163. package/hedhog/frontend/app/courses/[id]/_components/CourseIssuedCertificatesCard.tsx.ejs +168 -0
  164. package/hedhog/frontend/app/courses/[id]/structure/_components/course-ai-costs-tab.tsx.ejs +191 -0
  165. package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +81 -1
  166. package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +12 -0
  167. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +141 -30
  168. package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +13 -13
  169. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +69 -1
  170. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +11 -23
  171. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +267 -19
  172. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +114 -86
  173. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +239 -31
  174. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +344 -59
  175. package/hedhog/frontend/app/courses/[id]/structure/_components/lesson-video-preview.tsx.ejs +200 -0
  176. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +1 -0
  177. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +3 -0
  178. package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +1 -8
  179. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +19 -7
  180. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -0
  181. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-ai-costs.ts.ejs +40 -0
  182. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +2 -0
  183. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +25 -0
  184. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +148 -0
  185. package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +157 -8
  186. package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +1 -22
  187. package/hedhog/frontend/app/courses/page.tsx.ejs +66 -13
  188. package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +6 -0
  189. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-calendar-tab.tsx.ejs +264 -0
  190. package/hedhog/frontend/app/enterprise/page.tsx.ejs +104 -47
  191. package/hedhog/frontend/app/exams/page.tsx.ejs +38 -4
  192. package/hedhog/frontend/app/instructors/page.tsx.ejs +87 -46
  193. package/hedhog/frontend/app/paths/page.tsx.ejs +650 -168
  194. package/hedhog/frontend/app/training/page.tsx.ejs +38 -4
  195. package/hedhog/frontend/messages/en.json +41 -12
  196. package/hedhog/frontend/messages/pt.json +44 -13
  197. package/hedhog/query/triggers.sql +33 -0
  198. package/hedhog/table/course_ai_usage.yaml +46 -0
  199. package/hedhog/table/course_enrollment.yaml +3 -0
  200. package/hedhog/table/course_lesson.yaml +3 -0
  201. package/hedhog/table/course_lesson_answer.yaml +37 -0
  202. package/hedhog/table/course_lesson_transcription_segment.yaml +8 -0
  203. package/hedhog/table/learning_path.yaml +6 -0
  204. package/hedhog/table/learning_path_module.yaml +22 -0
  205. package/hedhog/table/learning_path_step.yaml +9 -6
  206. package/hedhog/table/lesson_view_event.yaml +66 -0
  207. package/package.json +8 -7
  208. package/src/certificate/certificate.controller.ts +2 -0
  209. package/src/certificate/certificate.service.ts +99 -0
  210. package/src/course/course-ai-usage.service.ts +221 -0
  211. package/src/course/course-audio-transcription.service.ts +471 -43
  212. package/src/course/course-export-scorm12.service.ts +149 -5
  213. package/src/course/course-export.service.ts +1 -0
  214. package/src/course/course-lesson.controller.ts +59 -6
  215. package/src/course/course-structure.controller.ts +19 -1
  216. package/src/course/course-structure.service.ts +184 -10
  217. package/src/course/course-transcription-translation.service.ts +293 -0
  218. package/src/course/course-video-agent-pipeline.service.ts +471 -0
  219. package/src/course/course-video-hls.service.ts +30 -10
  220. package/src/course/course.module.ts +9 -0
  221. package/src/course/course.service.ts +46 -1
  222. package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
  223. package/src/course/dto/create-course-export.dto.ts +6 -0
  224. package/src/course/ffmpeg.util.ts +65 -0
  225. package/src/course/lms-bulk-upload-automation.service.ts +33 -8
  226. package/src/course/lms-bulk-upload.service.ts +20 -1
  227. package/src/course/subtitle.util.ts +220 -0
  228. package/src/enterprise/training/training-student.service.ts +224 -4
  229. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +14 -0
  230. package/src/lesson-xp-map/lesson-xp-map.module.ts +2 -1
  231. package/src/lms.module.ts +14 -0
  232. package/src/platforma/dto/heartbeat.dto.ts +30 -0
  233. package/src/platforma/handlers/emit-certificate.handler.ts +117 -0
  234. package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -0
  235. package/src/platforma/platforma-heartbeat.service.ts +33 -0
  236. package/src/platforma/platforma-performance.service.ts +606 -0
  237. package/src/platforma/platforma-search.service.ts +48 -0
  238. package/src/platforma/platforma-video.service.ts +59 -3
  239. package/src/platforma/platforma.controller.ts +130 -0
  240. package/src/realtime/lms-realtime.controller.ts +27 -1
  241. package/src/realtime/lms-realtime.service.ts +2 -1
  242. package/src/training/dto/create-training.dto.ts +36 -0
  243. package/src/training/training.service.ts +360 -163
@@ -0,0 +1,277 @@
1
+ 'use client';
2
+
3
+ import { Badge } from '@/components/ui/badge';
4
+ import {
5
+ addDays,
6
+ addMonths,
7
+ eachDayOfInterval,
8
+ format,
9
+ isAfter,
10
+ isBefore,
11
+ isSameDay,
12
+ isSameMonth,
13
+ isToday,
14
+ parseISO,
15
+ startOfMonth,
16
+ startOfWeek,
17
+ subMonths,
18
+ } from 'date-fns';
19
+ import { ptBR } from 'date-fns/locale/pt-BR';
20
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
21
+ import { useLocale } from 'next-intl';
22
+ import { useState } from 'react';
23
+ import { useRouter } from 'next/navigation';
24
+
25
+ interface Turma {
26
+ id: number;
27
+ codigo: string;
28
+ curso: string;
29
+ dataInicio: string;
30
+ dataFim: string;
31
+ status: 'aberta' | 'em_andamento' | 'concluida' | 'cancelada';
32
+ professor: string;
33
+ vagas: number;
34
+ matriculados: number;
35
+ }
36
+
37
+ const STATUS_COLORS: Record<string, string> = {
38
+ aberta:
39
+ 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300',
40
+ em_andamento:
41
+ 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300',
42
+ concluida:
43
+ 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300',
44
+ cancelada: 'bg-muted text-muted-foreground line-through',
45
+ };
46
+
47
+ const STATUS_LABELS: Record<string, string> = {
48
+ aberta: 'Aberta',
49
+ em_andamento: 'Em andamento',
50
+ concluida: 'Concluída',
51
+ cancelada: 'Cancelada',
52
+ };
53
+
54
+ const DAY_HEADERS_PT = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom'];
55
+ const DAY_HEADERS_EN = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
56
+
57
+ function parseDate(value: string | null | undefined): Date | null {
58
+ if (!value) return null;
59
+ try {
60
+ const d = parseISO(String(value).slice(0, 10));
61
+ return isNaN(d.getTime()) ? null : d;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function isTurmaActiveOnDay(turma: Turma, day: Date): boolean {
68
+ const start = parseDate(turma.dataInicio);
69
+ const end = parseDate(turma.dataFim);
70
+ if (!start) return false;
71
+ const dayStart = new Date(day.getFullYear(), day.getMonth(), day.getDate());
72
+ const startDay = new Date(
73
+ start.getFullYear(),
74
+ start.getMonth(),
75
+ start.getDate()
76
+ );
77
+ const endDay = end
78
+ ? new Date(end.getFullYear(), end.getMonth(), end.getDate())
79
+ : startDay;
80
+ return (
81
+ (isSameDay(dayStart, startDay) || isAfter(dayStart, startDay)) &&
82
+ (isSameDay(dayStart, endDay) || isBefore(dayStart, endDay))
83
+ );
84
+ }
85
+
86
+ export function ClassesCalendarView({ turmas }: { turmas: Turma[] }) {
87
+ const router = useRouter();
88
+ const locale = useLocale();
89
+ const dateLocale = locale.startsWith('pt') ? ptBR : undefined;
90
+ const dayHeaders = locale.startsWith('pt') ? DAY_HEADERS_PT : DAY_HEADERS_EN;
91
+
92
+ const [currentDate, setCurrentDate] = useState(() => new Date());
93
+ const [selectedDay, setSelectedDay] = useState<Date | null>(null);
94
+
95
+ const monthStart = startOfMonth(currentDate);
96
+ const gridStart = startOfWeek(monthStart, { weekStartsOn: 1 });
97
+ const days = eachDayOfInterval({
98
+ start: gridStart,
99
+ end: addDays(gridStart, 41),
100
+ });
101
+
102
+ const getTurmasForDay = (day: Date) =>
103
+ turmas.filter((t) => isTurmaActiveOnDay(t, day));
104
+
105
+ const selectedDayTurmas = selectedDay ? getTurmasForDay(selectedDay) : [];
106
+
107
+ const monthLabel = format(
108
+ currentDate,
109
+ locale.startsWith('pt') ? "MMMM 'de' yyyy" : 'MMMM yyyy',
110
+ { locale: dateLocale }
111
+ );
112
+
113
+ return (
114
+ <div className="space-y-4 pb-6">
115
+ {/* Month navigation */}
116
+ <div className="flex items-center justify-between rounded-xl border bg-muted/20 px-3 py-2">
117
+ <button
118
+ type="button"
119
+ onClick={() => {
120
+ setCurrentDate((d) => subMonths(d, 1));
121
+ setSelectedDay(null);
122
+ }}
123
+ className="flex size-8 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
124
+ aria-label="Mês anterior"
125
+ >
126
+ <ChevronLeft className="size-4" />
127
+ </button>
128
+ <span className="text-sm font-semibold capitalize">{monthLabel}</span>
129
+ <button
130
+ type="button"
131
+ onClick={() => {
132
+ setCurrentDate((d) => addMonths(d, 1));
133
+ setSelectedDay(null);
134
+ }}
135
+ className="flex size-8 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
136
+ aria-label="Próximo mês"
137
+ >
138
+ <ChevronRight className="size-4" />
139
+ </button>
140
+ </div>
141
+
142
+ {/* Day headers */}
143
+ <div className="grid grid-cols-7 text-center">
144
+ {dayHeaders.map((h) => (
145
+ <div
146
+ key={h}
147
+ className="pb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground"
148
+ >
149
+ {h}
150
+ </div>
151
+ ))}
152
+ </div>
153
+
154
+ {/* Calendar grid */}
155
+ <div className="grid grid-cols-7 gap-px overflow-hidden rounded-lg border bg-border/40">
156
+ {days.map((day) => {
157
+ const dayTurmas = getTurmasForDay(day);
158
+ const isCurrentMonth = isSameMonth(day, currentDate);
159
+ const isSelected = selectedDay ? isSameDay(day, selectedDay) : false;
160
+ const todayDay = isToday(day);
161
+
162
+ return (
163
+ <button
164
+ key={day.toISOString()}
165
+ type="button"
166
+ onClick={() => setSelectedDay(isSelected ? null : day)}
167
+ className={[
168
+ 'flex min-h-16 flex-col gap-0.5 bg-background p-1 text-left transition-colors hover:bg-muted/40',
169
+ !isCurrentMonth && 'opacity-40',
170
+ isSelected && 'ring-2 ring-inset ring-primary',
171
+ ]
172
+ .filter(Boolean)
173
+ .join(' ')}
174
+ >
175
+ <span
176
+ className={[
177
+ 'flex size-5 items-center justify-center self-end rounded-full text-[11px] font-medium',
178
+ todayDay
179
+ ? 'bg-primary text-primary-foreground'
180
+ : 'text-foreground',
181
+ ].join(' ')}
182
+ >
183
+ {format(day, 'd')}
184
+ </span>
185
+ <div className="flex min-h-0 flex-col gap-0.5 overflow-hidden">
186
+ {dayTurmas.slice(0, 3).map((turma) => (
187
+ <span
188
+ key={turma.id}
189
+ className={[
190
+ 'truncate rounded px-1 text-[9px] font-medium leading-4',
191
+ STATUS_COLORS[turma.status] ?? STATUS_COLORS.aberta,
192
+ ].join(' ')}
193
+ >
194
+ {turma.curso}
195
+ </span>
196
+ ))}
197
+ {dayTurmas.length > 3 && (
198
+ <span className="px-1 text-[9px] text-muted-foreground">
199
+ +{dayTurmas.length - 3}
200
+ </span>
201
+ )}
202
+ </div>
203
+ </button>
204
+ );
205
+ })}
206
+ </div>
207
+
208
+ {/* Selected day detail */}
209
+ {selectedDay && (
210
+ <div className="space-y-2 rounded-xl border bg-muted/20 p-3">
211
+ <p className="text-xs font-semibold text-muted-foreground">
212
+ {format(
213
+ selectedDay,
214
+ locale.startsWith('pt') ? "d 'de' MMMM" : 'MMMM d',
215
+ { locale: dateLocale }
216
+ )}
217
+ </p>
218
+ {selectedDayTurmas.length === 0 ? (
219
+ <p className="text-sm text-muted-foreground">
220
+ Nenhuma turma neste dia.
221
+ </p>
222
+ ) : (
223
+ <div className="space-y-1.5">
224
+ {selectedDayTurmas.map((turma) => (
225
+ <button
226
+ key={turma.id}
227
+ type="button"
228
+ onClick={() => router.push(`/lms/classes/${turma.id}`)}
229
+ className="flex w-full items-center gap-3 rounded-lg border bg-background px-3 py-2 text-left transition-colors hover:bg-muted/50"
230
+ >
231
+ <div className="min-w-0 flex-1">
232
+ <p className="truncate text-sm font-medium">{turma.curso}</p>
233
+ <p className="truncate text-xs text-muted-foreground">
234
+ {[turma.codigo, turma.professor !== '-' ? turma.professor : null]
235
+ .filter(Boolean)
236
+ .join(' • ')}
237
+ </p>
238
+ </div>
239
+ <div className="flex shrink-0 flex-col items-end gap-1">
240
+ <Badge
241
+ variant="outline"
242
+ className={[
243
+ 'px-1.5 py-0 text-[10px]',
244
+ STATUS_COLORS[turma.status],
245
+ ].join(' ')}
246
+ >
247
+ {STATUS_LABELS[turma.status] ?? turma.status}
248
+ </Badge>
249
+ <span className="text-[10px] text-muted-foreground">
250
+ {turma.matriculados}/{turma.vagas}
251
+ </span>
252
+ </div>
253
+ </button>
254
+ ))}
255
+ </div>
256
+ )}
257
+ </div>
258
+ )}
259
+
260
+ {/* Legend */}
261
+ <div className="flex flex-wrap gap-3">
262
+ {Object.entries(STATUS_LABELS).map(([status, label]) => (
263
+ <div key={status} className="flex items-center gap-1.5">
264
+ <span
265
+ className={[
266
+ 'rounded px-1.5 py-0.5 text-[10px] font-medium',
267
+ STATUS_COLORS[status],
268
+ ].join(' ')}
269
+ >
270
+ {label}
271
+ </span>
272
+ </div>
273
+ ))}
274
+ </div>
275
+ </div>
276
+ );
277
+ }
@@ -7,8 +7,17 @@ import {
7
7
  PageHeader,
8
8
  PaginationFooter,
9
9
  SearchBar,
10
- ViewModeToggle,
11
10
  } from '@/components/entity-list';
11
+ import {
12
+ Tooltip,
13
+ TooltipContent,
14
+ TooltipProvider,
15
+ TooltipTrigger,
16
+ } from '@/components/ui/tooltip';
17
+ import {
18
+ ToggleGroup,
19
+ ToggleGroupItem,
20
+ } from '@/components/ui/toggle-group';
12
21
  import { Badge } from '@/components/ui/badge';
13
22
  import { Button } from '@/components/ui/button';
14
23
  import { Card, CardContent } from '@/components/ui/card';
@@ -39,10 +48,13 @@ import { motion } from 'framer-motion';
39
48
  import {
40
49
  AlertTriangle,
41
50
  BarChart3,
51
+ CalendarDays,
42
52
  CalendarIcon,
43
53
  Clock,
44
54
  Eye,
55
+ LayoutGrid,
45
56
  Laptop,
57
+ List,
46
58
  Loader2,
47
59
  MapPin,
48
60
  Monitor,
@@ -59,6 +71,7 @@ import { useRouter } from 'next/navigation';
59
71
  import { useEffect, useMemo, useRef, useState } from 'react';
60
72
  import { toast } from 'sonner';
61
73
  import { ClassFormSheet } from '../_components/class-form-sheet';
74
+ import { ClassesCalendarView } from './_components/classes-calendar-view';
62
75
 
63
76
  // ── Types ─────────────────────────────────────────────────────────────────────
64
77
 
@@ -229,7 +242,7 @@ function mapApiClass(item: ApiClass): Turma {
229
242
  };
230
243
  }
231
244
 
232
- type ViewMode = 'cards' | 'list';
245
+ type ViewMode = 'cards' | 'list' | 'calendar';
233
246
 
234
247
  // ── Constants ─────────────────────────────────────────────────────────────────
235
248
 
@@ -269,7 +282,7 @@ const TIPO_ICON: Record<string, LucideIcon> = {
269
282
  hibrida: Laptop,
270
283
  };
271
284
 
272
- const PAGE_SIZES = [6, 12, 24];
285
+ const DEFAULT_PAGE_SIZES = [6, 12, 24, 48, 96] as const;
273
286
 
274
287
  // ── Animations ────────────────────────────────────────────────────────────────
275
288
 
@@ -306,15 +319,49 @@ export default function TurmasPage() {
306
319
  const [viewMode, setViewMode] = usePersistedViewMode<ViewMode>({
307
320
  storageKey: 'lms:classes:view-mode',
308
321
  defaultValue: 'cards',
309
- allowedValues: ['cards', 'list'],
322
+ allowedValues: ['cards', 'list', 'calendar'],
310
323
  });
311
324
 
312
325
  // Pagination
313
326
  const [currentPage, setCurrentPage] = useState(1);
327
+
328
+ const { data: generalSettings } = useQuery<{
329
+ data: Array<{ slug: string; value: string }>;
330
+ }>({
331
+ queryKey: ['setting-group-general'],
332
+ queryFn: async () => {
333
+ const response = await request<{
334
+ data: Array<{ slug: string; value: string }>;
335
+ }>({
336
+ url: '/setting/group/general',
337
+ method: 'GET',
338
+ });
339
+ return response.data;
340
+ },
341
+ staleTime: 5 * 60 * 1000,
342
+ });
343
+
344
+ const pageSizeOptions = useMemo(() => {
345
+ const setting = generalSettings?.data?.find(
346
+ (s) => s.slug === 'pagination-page-sizes'
347
+ );
348
+ if (!setting?.value) return DEFAULT_PAGE_SIZES;
349
+ try {
350
+ const parsed = JSON.parse(setting.value) as string[];
351
+ const sizes = parsed
352
+ .map(Number)
353
+ .filter((n) => !isNaN(n) && n > 0)
354
+ .sort((a, b) => a - b);
355
+ return sizes.length > 0 ? sizes : DEFAULT_PAGE_SIZES;
356
+ } catch {
357
+ return DEFAULT_PAGE_SIZES;
358
+ }
359
+ }, [generalSettings]);
360
+
314
361
  const [pageSize, setPageSize] = usePersistedPageSize({
315
362
  storageKey: 'pagination:global:pageSize',
316
363
  defaultValue: 12,
317
- allowedValues: PAGE_SIZES,
364
+ allowedValues: pageSizeOptions,
318
365
  });
319
366
 
320
367
  const { data: coursesResponse } = useQuery<ApiCourseList>({
@@ -333,6 +380,9 @@ export default function TurmasPage() {
333
380
  },
334
381
  });
335
382
 
383
+ const effectivePage = viewMode === 'calendar' ? 1 : currentPage;
384
+ const effectivePageSize = viewMode === 'calendar' ? 500 : pageSize;
385
+
336
386
  const {
337
387
  data: classesResponse,
338
388
  refetch: refetchClasses,
@@ -341,8 +391,8 @@ export default function TurmasPage() {
341
391
  } = useQuery<ApiClassList>({
342
392
  queryKey: [
343
393
  'lms-classes-list',
344
- currentPage,
345
- pageSize,
394
+ effectivePage,
395
+ effectivePageSize,
346
396
  buscaDebounced,
347
397
  filtroStatusInput,
348
398
  filtroTipoInput,
@@ -359,8 +409,8 @@ export default function TurmasPage() {
359
409
  url: '/lms/classes',
360
410
  method: 'GET',
361
411
  params: {
362
- page: currentPage,
363
- pageSize,
412
+ page: effectivePage,
413
+ pageSize: effectivePageSize,
364
414
  ...(buscaDebounced ? { search: buscaDebounced } : {}),
365
415
  ...(filtroStatusInput !== 'todos'
366
416
  ? { status: toApiStatus(filtroStatusInput) }
@@ -742,13 +792,62 @@ export default function TurmasPage() {
742
792
  className: 'sm:w-56',
743
793
  },
744
794
  ]}
745
- afterSearchButton={
746
- <ViewModeToggle
747
- viewMode={viewMode}
748
- onViewModeChange={setViewMode}
749
- listLabel={t('viewMode.list')}
750
- cardsLabel={t('viewMode.cards')}
751
- />
795
+ actions={
796
+ <TooltipProvider>
797
+ <ToggleGroup
798
+ type="single"
799
+ value={viewMode}
800
+ onValueChange={(value) => {
801
+ if (
802
+ value === 'cards' ||
803
+ value === 'list' ||
804
+ value === 'calendar'
805
+ ) {
806
+ setViewMode(value);
807
+ }
808
+ }}
809
+ variant="outline"
810
+ size="sm"
811
+ className="shrink-0"
812
+ >
813
+ <Tooltip>
814
+ <TooltipTrigger asChild>
815
+ <ToggleGroupItem
816
+ value="cards"
817
+ aria-label={t('viewMode.cards')}
818
+ className="cursor-pointer"
819
+ >
820
+ <LayoutGrid className="h-4 w-4" />
821
+ </ToggleGroupItem>
822
+ </TooltipTrigger>
823
+ <TooltipContent>{t('viewMode.cards')}</TooltipContent>
824
+ </Tooltip>
825
+ <Tooltip>
826
+ <TooltipTrigger asChild>
827
+ <ToggleGroupItem
828
+ value="list"
829
+ aria-label={t('viewMode.list')}
830
+ className="cursor-pointer"
831
+ >
832
+ <List className="h-4 w-4" />
833
+ </ToggleGroupItem>
834
+ </TooltipTrigger>
835
+ <TooltipContent>{t('viewMode.list')}</TooltipContent>
836
+ </Tooltip>
837
+ <Tooltip>
838
+ <TooltipTrigger asChild>
839
+ <ToggleGroupItem
840
+ value="calendar"
841
+ aria-label={t('viewMode.calendar')}
842
+ className="cursor-pointer"
843
+ >
844
+ <CalendarDays className="h-4 w-4" />
845
+ </ToggleGroupItem>
846
+ </TooltipTrigger>
847
+ <TooltipContent>{t('viewMode.calendar')}</TooltipContent>
848
+ </Tooltip>
849
+ </ToggleGroup>
850
+ </TooltipProvider>
752
851
  }
753
852
  />
754
853
  <div className="flex items-center justify-between gap-3">
@@ -774,7 +873,13 @@ export default function TurmasPage() {
774
873
 
775
874
  {/* Classes list */}
776
875
  {loading ? (
777
- viewMode === 'cards' ? (
876
+ viewMode === 'calendar' ? (
877
+ <div className="grid grid-cols-7 gap-px">
878
+ {Array.from({ length: 42 }).map((_, i) => (
879
+ <div key={i} className="h-16 animate-pulse rounded bg-muted/30" />
880
+ ))}
881
+ </div>
882
+ ) : viewMode === 'cards' ? (
778
883
  <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
779
884
  {Array.from({ length: 6 }).map((_, i) => (
780
885
  <Card key={i} className="overflow-hidden border-border/70">
@@ -1018,7 +1123,7 @@ export default function TurmasPage() {
1018
1123
  );
1019
1124
  })}
1020
1125
  </motion.div>
1021
- ) : (
1126
+ ) : viewMode === 'list' ? (
1022
1127
  <div className="overflow-hidden rounded-xl border border-border/70">
1023
1128
  <Table>
1024
1129
  <TableHeader>
@@ -1146,10 +1251,12 @@ export default function TurmasPage() {
1146
1251
  </TableBody>
1147
1252
  </Table>
1148
1253
  </div>
1254
+ ) : (
1255
+ <ClassesCalendarView turmas={visibleTurmas} />
1149
1256
  )}
1150
1257
 
1151
1258
  {/* Pagination footer */}
1152
- {!loading && visibleTurmas.length > 0 && (
1259
+ {!loading && visibleTurmas.length > 0 && viewMode !== 'calendar' && (
1153
1260
  <div className="mt-6">
1154
1261
  <PaginationFooter
1155
1262
  currentPage={currentPage}
@@ -1160,7 +1267,7 @@ export default function TurmasPage() {
1160
1267
  setPageSize(value);
1161
1268
  setCurrentPage(1);
1162
1269
  }}
1163
- pageSizeOptions={PAGE_SIZES}
1270
+ pageSizeOptions={pageSizeOptions}
1164
1271
  />
1165
1272
  </div>
1166
1273
  )}
@@ -17,10 +17,13 @@ import type { UseFormReturn } from 'react-hook-form';
17
17
  import { CourseSectionCard } from './CourseSectionCard';
18
18
  import type { CourseEditFormValues, TranslationFn } from './course-edit-types';
19
19
 
20
+ type CourseFlag = 'certificado' | 'destaque' | 'listado';
21
+
20
22
  type CourseFlagsCardProps = {
21
23
  form: UseFormReturn<CourseEditFormValues>;
22
24
  t: TranslationFn;
23
25
  compact?: boolean;
26
+ flags?: CourseFlag[];
24
27
  };
25
28
 
26
29
  function FlagRow({
@@ -53,10 +56,13 @@ function FlagRow({
53
56
  );
54
57
  }
55
58
 
59
+ const DEFAULT_FLAGS: CourseFlag[] = ['certificado', 'destaque', 'listado'];
60
+
56
61
  export function CourseFlagsCard({
57
62
  form,
58
63
  t,
59
64
  compact = false,
65
+ flags = DEFAULT_FLAGS,
60
66
  }: CourseFlagsCardProps) {
61
67
  return (
62
68
  <CourseSectionCard
@@ -66,65 +72,71 @@ export function CourseFlagsCard({
66
72
  compact={compact}
67
73
  >
68
74
  <div className="space-y-3">
69
- <FormField
70
- control={form.control}
71
- name="certificado"
72
- render={({ field }) => (
73
- <FormItem>
74
- <FormLabel className="sr-only">
75
- {t('form.flags.certificate.label')}
76
- </FormLabel>
77
- <FormControl>
78
- <FlagRow
79
- title={t('form.flags.certificate.label')}
80
- description={t('form.flags.certificate.description')}
81
- checked={field.value}
82
- onCheckedChange={field.onChange}
83
- />
84
- </FormControl>
85
- </FormItem>
86
- )}
87
- />
75
+ {flags.includes('certificado') && (
76
+ <FormField
77
+ control={form.control}
78
+ name="certificado"
79
+ render={({ field }) => (
80
+ <FormItem>
81
+ <FormLabel className="sr-only">
82
+ {t('form.flags.certificate.label')}
83
+ </FormLabel>
84
+ <FormControl>
85
+ <FlagRow
86
+ title={t('form.flags.certificate.label')}
87
+ description={t('form.flags.certificate.description')}
88
+ checked={field.value}
89
+ onCheckedChange={field.onChange}
90
+ />
91
+ </FormControl>
92
+ </FormItem>
93
+ )}
94
+ />
95
+ )}
88
96
 
89
- <FormField
90
- control={form.control}
91
- name="destaque"
92
- render={({ field }) => (
93
- <FormItem>
94
- <FormLabel className="sr-only">
95
- {t('form.flags.featured.label')}
96
- </FormLabel>
97
- <FormControl>
98
- <FlagRow
99
- title={t('form.flags.featured.label')}
100
- description={t('form.flags.featured.description')}
101
- checked={field.value}
102
- onCheckedChange={field.onChange}
103
- />
104
- </FormControl>
105
- </FormItem>
106
- )}
107
- />
97
+ {flags.includes('destaque') && (
98
+ <FormField
99
+ control={form.control}
100
+ name="destaque"
101
+ render={({ field }) => (
102
+ <FormItem>
103
+ <FormLabel className="sr-only">
104
+ {t('form.flags.featured.label')}
105
+ </FormLabel>
106
+ <FormControl>
107
+ <FlagRow
108
+ title={t('form.flags.featured.label')}
109
+ description={t('form.flags.featured.description')}
110
+ checked={field.value}
111
+ onCheckedChange={field.onChange}
112
+ />
113
+ </FormControl>
114
+ </FormItem>
115
+ )}
116
+ />
117
+ )}
108
118
 
109
- <FormField
110
- control={form.control}
111
- name="listado"
112
- render={({ field }) => (
113
- <FormItem>
114
- <FormLabel className="sr-only">
115
- {t('form.flags.listed.label')}
116
- </FormLabel>
117
- <FormControl>
118
- <FlagRow
119
- title={t('form.flags.listed.label')}
120
- description={t('form.flags.listed.description')}
121
- checked={field.value}
122
- onCheckedChange={field.onChange}
123
- />
124
- </FormControl>
125
- </FormItem>
126
- )}
127
- />
119
+ {flags.includes('listado') && (
120
+ <FormField
121
+ control={form.control}
122
+ name="listado"
123
+ render={({ field }) => (
124
+ <FormItem>
125
+ <FormLabel className="sr-only">
126
+ {t('form.flags.listed.label')}
127
+ </FormLabel>
128
+ <FormControl>
129
+ <FlagRow
130
+ title={t('form.flags.listed.label')}
131
+ description={t('form.flags.listed.description')}
132
+ checked={field.value}
133
+ onCheckedChange={field.onChange}
134
+ />
135
+ </FormControl>
136
+ </FormItem>
137
+ )}
138
+ />
139
+ )}
128
140
  </div>
129
141
  </CourseSectionCard>
130
142
  );