@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,264 @@
1
+ 'use client';
2
+
3
+ import { Badge } from '@/components/ui/badge';
4
+ import { Button } from '@/components/ui/button';
5
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
6
+ import {
7
+ addMonths,
8
+ addDays,
9
+ eachDayOfInterval,
10
+ format,
11
+ isAfter,
12
+ isBefore,
13
+ isSameDay,
14
+ isSameMonth,
15
+ isToday,
16
+ parseISO,
17
+ startOfMonth,
18
+ startOfWeek,
19
+ subMonths,
20
+ } from 'date-fns';
21
+ import { ptBR } from 'date-fns/locale/pt-BR';
22
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
23
+ import { useLocale, useTranslations } from 'next-intl';
24
+ import { useMemo, useState } from 'react';
25
+ import { useRouter } from 'next/navigation';
26
+ import type { EnterpriseClass } from './enterprise-types';
27
+
28
+ const STATUS_COLORS: Record<string, string> = {
29
+ open: 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-300',
30
+ ongoing:
31
+ 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900/40 dark:text-emerald-300',
32
+ completed:
33
+ 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300',
34
+ cancelled:
35
+ 'bg-muted text-muted-foreground line-through',
36
+ };
37
+
38
+ const DAY_HEADERS_PT = ['Seg', 'Ter', 'Qua', 'Qui', 'Sex', 'Sáb', 'Dom'];
39
+ const DAY_HEADERS_EN = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
40
+
41
+ function parseDate(value: string | null | undefined): Date | null {
42
+ if (!value) return null;
43
+ try {
44
+ const d = parseISO(String(value).slice(0, 10));
45
+ return isNaN(d.getTime()) ? null : d;
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ function isClassActiveOnDay(
52
+ cls: EnterpriseClass,
53
+ day: Date
54
+ ): boolean {
55
+ const start = parseDate(cls.startDate);
56
+ const end = parseDate(cls.endDate);
57
+ if (!start) return false;
58
+ const dayStart = new Date(day.getFullYear(), day.getMonth(), day.getDate());
59
+ const startDay = new Date(start.getFullYear(), start.getMonth(), start.getDate());
60
+ const endDay = end
61
+ ? new Date(end.getFullYear(), end.getMonth(), end.getDate())
62
+ : startDay;
63
+ return (
64
+ (isSameDay(dayStart, startDay) || isAfter(dayStart, startDay)) &&
65
+ (isSameDay(dayStart, endDay) || isBefore(dayStart, endDay))
66
+ );
67
+ }
68
+
69
+ export function ClassesCalendarTab({
70
+ enterpriseId,
71
+ }: {
72
+ enterpriseId: number;
73
+ }) {
74
+ const { request } = useApp();
75
+ const router = useRouter();
76
+ const locale = useLocale();
77
+ const t = useTranslations('lms.EnterpriseDetailPage');
78
+ const dateLocale = locale.startsWith('pt') ? ptBR : undefined;
79
+ const dayHeaders = locale.startsWith('pt') ? DAY_HEADERS_PT : DAY_HEADERS_EN;
80
+
81
+ const [currentDate, setCurrentDate] = useState(() => new Date());
82
+ const [selectedDay, setSelectedDay] = useState<Date | null>(null);
83
+
84
+ const { data, isLoading } = useQuery<{ data: EnterpriseClass[] }>({
85
+ queryKey: ['enterprise-classes-calendar', enterpriseId],
86
+ queryFn: async () =>
87
+ request<{ data: EnterpriseClass[] }>({
88
+ url: `/lms/enterprise/${enterpriseId}/classes?page=1&pageSize=500`,
89
+ method: 'GET',
90
+ }).then((r) => r.data),
91
+ });
92
+
93
+ const classes = useMemo(() => data?.data ?? [], [data]);
94
+
95
+ const monthStart = startOfMonth(currentDate);
96
+ const gridStart = startOfWeek(monthStart, { weekStartsOn: 1 });
97
+ const days = eachDayOfInterval({ start: gridStart, end: addDays(gridStart, 41) });
98
+
99
+ const getClassesForDay = (day: Date) =>
100
+ classes.filter((cls) => isClassActiveOnDay(cls, day));
101
+
102
+ const selectedDayClasses = selectedDay ? getClassesForDay(selectedDay) : [];
103
+
104
+ const monthLabel = format(
105
+ currentDate,
106
+ locale.startsWith('pt') ? "MMMM 'de' yyyy" : 'MMMM yyyy',
107
+ { locale: dateLocale }
108
+ );
109
+
110
+ return (
111
+ <div className="space-y-4 pb-6">
112
+ {/* Month navigation */}
113
+ <div className="flex items-center justify-between rounded-xl border bg-muted/20 px-3 py-2">
114
+ <button
115
+ type="button"
116
+ onClick={() => { setCurrentDate((d) => subMonths(d, 1)); setSelectedDay(null); }}
117
+ className="flex size-8 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
118
+ aria-label="Mês anterior"
119
+ >
120
+ <ChevronLeft className="size-4" />
121
+ </button>
122
+ <span className="text-sm font-semibold capitalize">{monthLabel}</span>
123
+ <button
124
+ type="button"
125
+ onClick={() => { setCurrentDate((d) => addMonths(d, 1)); setSelectedDay(null); }}
126
+ className="flex size-8 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
127
+ aria-label="Próximo mês"
128
+ >
129
+ <ChevronRight className="size-4" />
130
+ </button>
131
+ </div>
132
+
133
+ {/* Day headers */}
134
+ <div className="grid grid-cols-7 text-center">
135
+ {dayHeaders.map((h) => (
136
+ <div key={h} className="pb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground">
137
+ {h}
138
+ </div>
139
+ ))}
140
+ </div>
141
+
142
+ {/* Calendar grid */}
143
+ {isLoading ? (
144
+ <div className="grid grid-cols-7 gap-px">
145
+ {Array.from({ length: 42 }).map((_, i) => (
146
+ <div key={i} className="h-16 animate-pulse rounded bg-muted/30" />
147
+ ))}
148
+ </div>
149
+ ) : (
150
+ <div className="grid grid-cols-7 gap-px rounded-lg border bg-border/40 overflow-hidden">
151
+ {days.map((day) => {
152
+ const dayClasses = getClassesForDay(day);
153
+ const isCurrentMonth = isSameMonth(day, currentDate);
154
+ const isSelected = selectedDay ? isSameDay(day, selectedDay) : false;
155
+ const todayDay = isToday(day);
156
+
157
+ return (
158
+ <button
159
+ key={day.toISOString()}
160
+ type="button"
161
+ onClick={() => setSelectedDay(isSelected ? null : day)}
162
+ className={[
163
+ 'flex min-h-16 flex-col gap-0.5 bg-background p-1 text-left transition-colors hover:bg-muted/40',
164
+ !isCurrentMonth && 'opacity-40',
165
+ isSelected && 'ring-2 ring-inset ring-primary',
166
+ ].filter(Boolean).join(' ')}
167
+ >
168
+ <span
169
+ className={[
170
+ 'flex size-5 items-center justify-center self-end rounded-full text-[11px] font-medium',
171
+ todayDay
172
+ ? 'bg-primary text-primary-foreground'
173
+ : 'text-foreground',
174
+ ].join(' ')}
175
+ >
176
+ {format(day, 'd')}
177
+ </span>
178
+ <div className="flex min-h-0 flex-col gap-0.5 overflow-hidden">
179
+ {dayClasses.slice(0, 3).map((cls) => (
180
+ <span
181
+ key={cls.id}
182
+ className={[
183
+ 'truncate rounded px-1 text-[9px] font-medium leading-4',
184
+ STATUS_COLORS[cls.status] ?? STATUS_COLORS.open,
185
+ ].join(' ')}
186
+ >
187
+ {cls.courseTitle ?? cls.title ?? `#${cls.id}`}
188
+ </span>
189
+ ))}
190
+ {dayClasses.length > 3 && (
191
+ <span className="px-1 text-[9px] text-muted-foreground">
192
+ +{dayClasses.length - 3}
193
+ </span>
194
+ )}
195
+ </div>
196
+ </button>
197
+ );
198
+ })}
199
+ </div>
200
+ )}
201
+
202
+ {/* Selected day detail */}
203
+ {selectedDay && (
204
+ <div className="space-y-2 rounded-xl border bg-muted/20 p-3">
205
+ <p className="text-xs font-semibold text-muted-foreground">
206
+ {format(selectedDay, locale.startsWith('pt') ? "d 'de' MMMM" : 'MMMM d', { locale: dateLocale })}
207
+ </p>
208
+ {selectedDayClasses.length === 0 ? (
209
+ <p className="text-sm text-muted-foreground">Nenhuma turma neste dia.</p>
210
+ ) : (
211
+ <div className="space-y-1.5">
212
+ {selectedDayClasses.map((cls) => (
213
+ <button
214
+ key={cls.id}
215
+ type="button"
216
+ onClick={() => router.push(`/lms/classes/${cls.id}`)}
217
+ 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"
218
+ >
219
+ <div className="min-w-0 flex-1">
220
+ <p className="truncate text-sm font-medium">
221
+ {cls.courseTitle ?? cls.title ?? `Turma #${cls.id}`}
222
+ </p>
223
+ <p className="truncate text-xs text-muted-foreground">
224
+ {[cls.code, cls.instructorName].filter(Boolean).join(' • ')}
225
+ </p>
226
+ </div>
227
+ <div className="flex shrink-0 flex-col items-end gap-1">
228
+ <Badge
229
+ variant="outline"
230
+ className={['text-[10px] px-1.5 py-0', STATUS_COLORS[cls.status]].join(' ')}
231
+ >
232
+ {cls.status}
233
+ </Badge>
234
+ {cls.capacity != null && (
235
+ <span className="text-[10px] text-muted-foreground">
236
+ {cls.enrolledCount}/{cls.capacity}
237
+ </span>
238
+ )}
239
+ </div>
240
+ </button>
241
+ ))}
242
+ </div>
243
+ )}
244
+ </div>
245
+ )}
246
+
247
+ {/* Legend */}
248
+ <div className="flex flex-wrap gap-3">
249
+ {Object.entries({
250
+ open: 'Aberta',
251
+ ongoing: 'Em andamento',
252
+ completed: 'Concluída',
253
+ cancelled: 'Cancelada',
254
+ }).map(([status, label]) => (
255
+ <div key={status} className="flex items-center gap-1.5">
256
+ <span className={['rounded px-1.5 py-0.5 text-[10px] font-medium', STATUS_COLORS[status]].join(' ')}>
257
+ {label}
258
+ </span>
259
+ </div>
260
+ ))}
261
+ </div>
262
+ </div>
263
+ );
264
+ }
@@ -22,7 +22,14 @@ import {
22
22
  TableRow,
23
23
  } from '@/components/ui/table';
24
24
  import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
25
+ import {
26
+ Tooltip,
27
+ TooltipContent,
28
+ TooltipProvider,
29
+ TooltipTrigger,
30
+ } from '@/components/ui/tooltip';
25
31
  import { useDebounce } from '@/hooks/use-debounce';
32
+ import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
26
33
  import { formatDate } from '@/lib/format-date';
27
34
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
28
35
  import {
@@ -51,7 +58,7 @@ const VIEW_STORAGE_KEY = 'lms-enterprise-view-mode';
51
58
 
52
59
  // ── Constants ─────────────────────────────────────────────────────────────────
53
60
 
54
- const PAGE_SIZE = 12;
61
+ const DEFAULT_PAGE_SIZES = [6, 12, 24, 48, 96] as const;
55
62
 
56
63
  const STATUS_VARIANT: Record<
57
64
  EnterpriseStatus,
@@ -90,6 +97,45 @@ export default function EnterprisePage() {
90
97
  const debouncedSearch = useDebounce(search);
91
98
  const { request, currentLocaleCode, getSettingValue } = useApp();
92
99
 
100
+ const { data: generalSettings } = useQuery<{
101
+ data: Array<{ slug: string; value: string }>;
102
+ }>({
103
+ queryKey: ['setting-group-general'],
104
+ queryFn: async () => {
105
+ const response = await request<{
106
+ data: Array<{ slug: string; value: string }>;
107
+ }>({
108
+ url: '/setting/group/general',
109
+ method: 'GET',
110
+ });
111
+ return response.data;
112
+ },
113
+ staleTime: 5 * 60 * 1000,
114
+ });
115
+
116
+ const pageSizeOptions = useMemo(() => {
117
+ const setting = generalSettings?.data?.find(
118
+ (s) => s.slug === 'pagination-page-sizes'
119
+ );
120
+ if (!setting?.value) return DEFAULT_PAGE_SIZES;
121
+ try {
122
+ const parsed = JSON.parse(setting.value) as string[];
123
+ const sizes = parsed
124
+ .map(Number)
125
+ .filter((n) => !isNaN(n) && n > 0)
126
+ .sort((a, b) => a - b);
127
+ return sizes.length > 0 ? sizes : DEFAULT_PAGE_SIZES;
128
+ } catch {
129
+ return DEFAULT_PAGE_SIZES;
130
+ }
131
+ }, [generalSettings]);
132
+
133
+ const [pageSize, setPageSize] = usePersistedPageSize({
134
+ storageKey: 'pagination:global:pageSize',
135
+ defaultValue: 12,
136
+ allowedValues: pageSizeOptions,
137
+ });
138
+
93
139
  type EnterpriseListResponse = {
94
140
  data: EnterpriseAccount[];
95
141
  total: number;
@@ -112,7 +158,7 @@ export default function EnterprisePage() {
112
158
  queryKey: [
113
159
  'lms-enterprise',
114
160
  page,
115
- PAGE_SIZE,
161
+ pageSize,
116
162
  debouncedSearch,
117
163
  statusFilter,
118
164
  crmFilter,
@@ -120,7 +166,7 @@ export default function EnterprisePage() {
120
166
  queryFn: async () => {
121
167
  const params = new URLSearchParams();
122
168
  params.set('page', String(page));
123
- params.set('pageSize', String(PAGE_SIZE));
169
+ params.set('pageSize', String(pageSize));
124
170
  if (debouncedSearch) params.set('search', debouncedSearch);
125
171
  if (statusFilter !== 'all') params.set('status', statusFilter);
126
172
  if (crmFilter !== 'all') params.set('crmPersonId', crmFilter);
@@ -335,47 +381,56 @@ export default function EnterprisePage() {
335
381
 
336
382
  <KpiCardsGrid items={kpiItems} />
337
383
 
338
- <div className="flex flex-col gap-4 xl:flex-row xl:items-center">
339
- <div className="flex-1">
340
- <SearchBar
341
- searchQuery={search}
342
- onSearchChange={handleSearch}
343
- onSearch={() => {}}
344
- placeholder={t('filters.searchPlaceholder')}
345
- controls={controls}
346
- />
347
- </div>
348
- <div className="flex items-center gap-3">
349
- <span className="text-xs font-medium text-muted-foreground">
350
- {t.has('view.label') ? t('view.label') : 'View'}
351
- </span>
352
- <ToggleGroup
353
- type="single"
354
- value={viewMode}
355
- onValueChange={handleViewModeChange}
356
- variant="outline"
357
- size="sm"
358
- aria-label={
359
- t.has('view.modeAriaLabel')
360
- ? t('view.modeAriaLabel')
361
- : 'View mode'
362
- }
363
- >
364
- <ToggleGroupItem value="table" className="gap-1.5 px-2.5">
365
- <List className="h-4 w-4" />
366
- <span className="hidden sm:inline">
367
- {t.has('view.table') ? t('view.table') : 'Table'}
368
- </span>
369
- </ToggleGroupItem>
370
- <ToggleGroupItem value="cards" className="gap-1.5 px-2.5">
371
- <LayoutGrid className="h-4 w-4" />
372
- <span className="hidden sm:inline">
373
- {t.has('view.cards') ? t('view.cards') : 'Cards'}
374
- </span>
375
- </ToggleGroupItem>
376
- </ToggleGroup>
377
- </div>
378
- </div>
384
+ <SearchBar
385
+ searchQuery={search}
386
+ onSearchChange={handleSearch}
387
+ onSearch={() => {}}
388
+ placeholder={t('filters.searchPlaceholder')}
389
+ controls={controls}
390
+ actions={
391
+ <TooltipProvider>
392
+ <ToggleGroup
393
+ type="single"
394
+ value={viewMode}
395
+ onValueChange={handleViewModeChange}
396
+ variant="outline"
397
+ size="sm"
398
+ aria-label={
399
+ t.has('view.modeAriaLabel') ? t('view.modeAriaLabel') : 'View mode'
400
+ }
401
+ >
402
+ <Tooltip>
403
+ <TooltipTrigger asChild>
404
+ <ToggleGroupItem
405
+ value="table"
406
+ className="cursor-pointer"
407
+ aria-label={t.has('view.table') ? t('view.table') : 'Table'}
408
+ >
409
+ <List className="h-4 w-4" />
410
+ </ToggleGroupItem>
411
+ </TooltipTrigger>
412
+ <TooltipContent>
413
+ {t.has('view.table') ? t('view.table') : 'Table'}
414
+ </TooltipContent>
415
+ </Tooltip>
416
+ <Tooltip>
417
+ <TooltipTrigger asChild>
418
+ <ToggleGroupItem
419
+ value="cards"
420
+ className="cursor-pointer"
421
+ aria-label={t.has('view.cards') ? t('view.cards') : 'Cards'}
422
+ >
423
+ <LayoutGrid className="h-4 w-4" />
424
+ </ToggleGroupItem>
425
+ </TooltipTrigger>
426
+ <TooltipContent>
427
+ {t.has('view.cards') ? t('view.cards') : 'Cards'}
428
+ </TooltipContent>
429
+ </Tooltip>
430
+ </ToggleGroup>
431
+ </TooltipProvider>
432
+ }
433
+ />
379
434
 
380
435
  {accounts.length === 0 && !isLoading ? (
381
436
  <EmptyState
@@ -633,12 +688,14 @@ export default function EnterprisePage() {
633
688
 
634
689
  <PaginationFooter
635
690
  currentPage={page}
636
- pageSize={PAGE_SIZE}
691
+ pageSize={pageSize}
637
692
  totalItems={totalItems}
638
693
  onPageChange={setPage}
639
- onPageSizeChange={function (): void {
640
- throw new Error('Function not implemented.');
694
+ onPageSizeChange={(next) => {
695
+ setPageSize(next);
696
+ setPage(1);
641
697
  }}
698
+ pageSizeOptions={pageSizeOptions}
642
699
  />
643
700
 
644
701
  <EnterpriseSheet
@@ -213,7 +213,7 @@ function toNumberOrFallback(value: unknown, fallback: number) {
213
213
 
214
214
  // ── Constants ─────────────────────────────────────────────────────────────────
215
215
 
216
- const PAGE_SIZES = [6, 12, 24];
216
+ const DEFAULT_PAGE_SIZES = [6, 12, 24, 48, 96] as const;
217
217
 
218
218
  function formatTempo(minutos: number) {
219
219
  if (minutos < 60) return `${minutos}min`;
@@ -263,10 +263,44 @@ export default function ExamesPage() {
263
263
 
264
264
  // Pagination
265
265
  const [currentPage, setCurrentPage] = useState(1);
266
+
267
+ const { data: generalSettings } = useQuery<{
268
+ data: Array<{ slug: string; value: string }>;
269
+ }>({
270
+ queryKey: ['setting-group-general'],
271
+ queryFn: async () => {
272
+ const response = await request<{
273
+ data: Array<{ slug: string; value: string }>;
274
+ }>({
275
+ url: '/setting/group/general',
276
+ method: 'GET',
277
+ });
278
+ return response.data;
279
+ },
280
+ staleTime: 5 * 60 * 1000,
281
+ });
282
+
283
+ const pageSizeOptions = useMemo(() => {
284
+ const setting = generalSettings?.data?.find(
285
+ (s) => s.slug === 'pagination-page-sizes'
286
+ );
287
+ if (!setting?.value) return DEFAULT_PAGE_SIZES;
288
+ try {
289
+ const parsed = JSON.parse(setting.value) as string[];
290
+ const sizes = parsed
291
+ .map(Number)
292
+ .filter((n) => !isNaN(n) && n > 0)
293
+ .sort((a, b) => a - b);
294
+ return sizes.length > 0 ? sizes : DEFAULT_PAGE_SIZES;
295
+ } catch {
296
+ return DEFAULT_PAGE_SIZES;
297
+ }
298
+ }, [generalSettings]);
299
+
266
300
  const [pageSize, setPageSize] = usePersistedPageSize({
267
301
  storageKey: 'pagination:global:pageSize',
268
302
  defaultValue: 12,
269
- allowedValues: PAGE_SIZES,
303
+ allowedValues: pageSizeOptions,
270
304
  });
271
305
 
272
306
  const form = useForm<ExameForm>({
@@ -654,7 +688,7 @@ export default function ExamesPage() {
654
688
  ],
655
689
  },
656
690
  ]}
657
- afterSearchButton={
691
+ actions={
658
692
  <ViewModeToggle
659
693
  viewMode={viewMode}
660
694
  onViewModeChange={setViewMode}
@@ -1037,7 +1071,7 @@ export default function ExamesPage() {
1037
1071
  setPageSize(nextPageSize);
1038
1072
  setCurrentPage(1);
1039
1073
  }}
1040
- pageSizeOptions={PAGE_SIZES}
1074
+ pageSizeOptions={pageSizeOptions}
1041
1075
  />
1042
1076
  </div>
1043
1077
  )}