@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,168 @@
1
+ 'use client';
2
+
3
+ import { PaginationFooter } from '@/components/entity-list';
4
+ import { Badge } from '@/components/ui/badge';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Skeleton } from '@/components/ui/skeleton';
7
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
8
+ import { Award, BadgeCheck } from 'lucide-react';
9
+ import { useLocale, useTranslations } from 'next-intl';
10
+ import { useEffect, useMemo, useState } from 'react';
11
+
12
+ import { CourseSectionCard } from './CourseSectionCard';
13
+
14
+ type IssuedCertificate = {
15
+ id: number;
16
+ studentId: number;
17
+ studentName: string;
18
+ courseName: string;
19
+ templateId: number;
20
+ templateName: string;
21
+ templateStatus: string | null;
22
+ certificateType: 'course' | 'exam' | 'course_class_group' | 'learning_path';
23
+ workloadHours: number;
24
+ issuedAt: string;
25
+ completedAt: string;
26
+ finalScore: number | null;
27
+ verificationCode: string;
28
+ verificationUrl: string | null;
29
+ publicAccess: boolean;
30
+ pdfUrl: string | null;
31
+ sourceTitle: string;
32
+ primaryColor: string | null;
33
+ };
34
+
35
+ type ApiIssuedCertificatesResponse = {
36
+ total: number;
37
+ page: number;
38
+ pageSize: number;
39
+ lastPage: number;
40
+ data: IssuedCertificate[];
41
+ };
42
+
43
+ const PAGE_SIZES = [6, 12, 24] as const;
44
+
45
+ type CourseIssuedCertificatesCardProps = {
46
+ courseId: string;
47
+ compact?: boolean;
48
+ };
49
+
50
+ export function CourseIssuedCertificatesCard({
51
+ courseId,
52
+ compact = false,
53
+ }: CourseIssuedCertificatesCardProps) {
54
+ const { request } = useApp();
55
+ const t = useTranslations('lms.CursoEditPage.structureEditor.issuedCertificates');
56
+ const locale = useLocale();
57
+
58
+ const [page, setPage] = useState(1);
59
+ const [pageSize, setPageSize] = useState<number>(6);
60
+
61
+ const { data, isLoading } = useQuery<ApiIssuedCertificatesResponse>({
62
+ queryKey: ['lms-course-issued-certificates', courseId, page, pageSize],
63
+ enabled: Boolean(courseId),
64
+ queryFn: async () => {
65
+ const response = await request<ApiIssuedCertificatesResponse>({
66
+ url: '/lms/certificates/issued',
67
+ method: 'GET',
68
+ params: { courseId, page, pageSize },
69
+ });
70
+ return response.data;
71
+ },
72
+ placeholderData: (old) => old,
73
+ });
74
+
75
+ const certificates = data?.data ?? [];
76
+ const totalItems = data?.total ?? 0;
77
+ const totalPages = Math.max(data?.lastPage ?? 1, 1);
78
+ const loading = isLoading && !data;
79
+
80
+ useEffect(() => {
81
+ if (page > totalPages) setPage(totalPages);
82
+ }, [page, totalPages]);
83
+
84
+ const dateFormatter = useMemo(
85
+ () =>
86
+ new Intl.DateTimeFormat(locale.startsWith('pt') ? 'pt-BR' : 'en-US', {
87
+ dateStyle: 'medium',
88
+ }),
89
+ [locale],
90
+ );
91
+
92
+ function formatDate(value: string) {
93
+ const parsed = new Date(value);
94
+ if (Number.isNaN(parsed.getTime())) return '-';
95
+ return dateFormatter.format(parsed);
96
+ }
97
+
98
+ return (
99
+ <CourseSectionCard
100
+ title={t('title')}
101
+ description={t('description')}
102
+ icon={Award}
103
+ compact={compact}
104
+ >
105
+ {loading ? (
106
+ <div className="flex flex-col gap-2">
107
+ {Array.from({ length: 3 }).map((_, index) => (
108
+ <Skeleton key={index} className="h-14 w-full rounded-md" />
109
+ ))}
110
+ </div>
111
+ ) : totalItems === 0 ? (
112
+ <p className="py-3 text-center text-xs text-muted-foreground">
113
+ {t('empty')}
114
+ </p>
115
+ ) : (
116
+ <div className="flex flex-col gap-2">
117
+ {certificates.map((certificate) => (
118
+ <div
119
+ key={certificate.id}
120
+ className="flex items-center gap-3 rounded-md border bg-background/80 px-2.5 py-2"
121
+ >
122
+ <div className="min-w-0 flex-1">
123
+ <p className="truncate text-xs font-medium">
124
+ {certificate.studentName}
125
+ </p>
126
+ <div className="mt-0.5 flex flex-wrap items-center gap-1.5 text-[0.65rem] text-muted-foreground">
127
+ <span>
128
+ {t('issuedAt')}: {formatDate(certificate.issuedAt)}
129
+ </span>
130
+ <span aria-hidden>·</span>
131
+ <span className="font-mono">
132
+ {certificate.verificationCode}
133
+ </span>
134
+ </div>
135
+ </div>
136
+ {certificate.pdfUrl ? (
137
+ <Button
138
+ type="button"
139
+ variant="ghost"
140
+ size="icon"
141
+ className="size-7 shrink-0 text-muted-foreground hover:text-foreground"
142
+ aria-label={t('openPdf')}
143
+ onClick={() => window.open(certificate.pdfUrl ?? '', '_blank')}
144
+ >
145
+ <BadgeCheck className="size-4" />
146
+ </Button>
147
+ ) : null}
148
+ </div>
149
+ ))}
150
+
151
+ <div className="pt-1">
152
+ <PaginationFooter
153
+ currentPage={page}
154
+ pageSize={pageSize}
155
+ totalItems={totalItems}
156
+ onPageChange={setPage}
157
+ onPageSizeChange={(value) => {
158
+ setPageSize(value);
159
+ setPage(1);
160
+ }}
161
+ pageSizeOptions={PAGE_SIZES}
162
+ />
163
+ </div>
164
+ </div>
165
+ )}
166
+ </CourseSectionCard>
167
+ );
168
+ }
@@ -0,0 +1,191 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from '@/components/ui/card';
10
+ import { Skeleton } from '@/components/ui/skeleton';
11
+ import { DollarSign, Languages, Mic, Sparkles, Zap } from 'lucide-react';
12
+ import { useCourseAiCostsQuery } from '../_data/use-course-ai-costs';
13
+
14
+ interface Props {
15
+ courseId: string;
16
+ }
17
+
18
+ const JOB_TYPE_META: Record<
19
+ string,
20
+ { label: string; icon: typeof Mic; tone: string }
21
+ > = {
22
+ transcription: {
23
+ label: 'Transcrição',
24
+ icon: Mic,
25
+ tone: 'bg-cyan-500/10 text-cyan-600 dark:text-cyan-400',
26
+ },
27
+ translation: {
28
+ label: 'Tradução',
29
+ icon: Languages,
30
+ tone: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
31
+ },
32
+ xp_calculation: {
33
+ label: 'Cálculo de XP',
34
+ icon: Zap,
35
+ tone: 'bg-amber-500/10 text-amber-600 dark:text-amber-400',
36
+ },
37
+ };
38
+
39
+ function formatUsd(value: number): string {
40
+ const fractionDigits = value > 0 && value < 0.01 ? 4 : 2;
41
+ return new Intl.NumberFormat('en-US', {
42
+ style: 'currency',
43
+ currency: 'USD',
44
+ minimumFractionDigits: fractionDigits,
45
+ maximumFractionDigits: fractionDigits,
46
+ }).format(value);
47
+ }
48
+
49
+ export function CourseAiCostsTab({ courseId }: Props) {
50
+ const { data, isLoading, isError } = useCourseAiCostsQuery(courseId);
51
+
52
+ if (isLoading) {
53
+ return (
54
+ <div className="flex flex-col gap-3">
55
+ <Skeleton className="h-28 rounded-xl" />
56
+ <div className="grid gap-3 sm:grid-cols-3">
57
+ {Array.from({ length: 3 }).map((_, i) => (
58
+ <Skeleton key={i} className="h-24 rounded-xl" />
59
+ ))}
60
+ </div>
61
+ <Skeleton className="h-48 rounded-xl" />
62
+ </div>
63
+ );
64
+ }
65
+
66
+ if (isError || !data) {
67
+ return (
68
+ <Card className="border-dashed bg-muted/20">
69
+ <CardHeader>
70
+ <CardTitle className="text-sm font-semibold">
71
+ Não foi possível carregar os custos de IA
72
+ </CardTitle>
73
+ <CardDescription>
74
+ Tente novamente em instantes.
75
+ </CardDescription>
76
+ </CardHeader>
77
+ </Card>
78
+ );
79
+ }
80
+
81
+ if (data.totalCostUsd <= 0 && data.byJobType.length === 0) {
82
+ return (
83
+ <Card className="border-dashed bg-muted/20">
84
+ <CardHeader>
85
+ <CardTitle className="text-sm font-semibold">
86
+ Ainda não há custos de IA registrados
87
+ </CardTitle>
88
+ <CardDescription>
89
+ Os custos aparecem aqui conforme as aulas passam por transcrição,
90
+ tradução e cálculo de XP. Apenas execuções a partir de agora são
91
+ contabilizadas.
92
+ </CardDescription>
93
+ </CardHeader>
94
+ </Card>
95
+ );
96
+ }
97
+
98
+ const byTypeMap = new Map(data.byJobType.map((r) => [r.jobType, r]));
99
+ const orderedTypes = ['transcription', 'translation', 'xp_calculation'];
100
+
101
+ return (
102
+ <div className="flex flex-col gap-3">
103
+ {/* Total */}
104
+ <Card className="overflow-hidden border-border/70 bg-linear-to-br from-background via-background to-muted/30">
105
+ <CardContent className="flex items-center gap-4 px-4 py-4">
106
+ <div className="flex size-12 shrink-0 items-center justify-center rounded-2xl bg-primary/10 text-primary">
107
+ <DollarSign className="size-6" />
108
+ </div>
109
+ <div className="min-w-0">
110
+ <div className="text-[10px] font-semibold uppercase tracking-[0.22em] text-muted-foreground">
111
+ Custo total de IA do curso
112
+ </div>
113
+ <div className="mt-0.5 text-2xl font-semibold text-foreground">
114
+ {formatUsd(data.totalCostUsd)}
115
+ </div>
116
+ <div className="mt-0.5 text-xs text-muted-foreground">
117
+ Soma de transcrição, tradução e cálculo de XP ({data.currency}).
118
+ </div>
119
+ </div>
120
+ </CardContent>
121
+ </Card>
122
+
123
+ {/* Por tipo */}
124
+ <div className="grid gap-3 sm:grid-cols-3">
125
+ {orderedTypes.map((type) => {
126
+ const meta = JOB_TYPE_META[type];
127
+ const row = byTypeMap.get(type);
128
+ const Icon = meta?.icon ?? Sparkles;
129
+ return (
130
+ <Card key={type} className="border-border/70">
131
+ <CardContent className="flex items-center gap-3 px-3 py-3">
132
+ <div
133
+ className={`flex size-9 shrink-0 items-center justify-center rounded-xl ${meta?.tone ?? 'bg-muted text-muted-foreground'}`}
134
+ >
135
+ <Icon className="size-4" />
136
+ </div>
137
+ <div className="min-w-0">
138
+ <div className="text-xs font-medium text-muted-foreground">
139
+ {meta?.label ?? type}
140
+ </div>
141
+ <div className="text-base font-semibold text-foreground">
142
+ {formatUsd(row?.costUsd ?? 0)}
143
+ </div>
144
+ <div className="text-[11px] text-muted-foreground">
145
+ {row?.runs ?? 0} execução(ões)
146
+ </div>
147
+ </div>
148
+ </CardContent>
149
+ </Card>
150
+ );
151
+ })}
152
+ </div>
153
+
154
+ {/* Por aula */}
155
+ <Card className="overflow-hidden border-border/70">
156
+ <CardHeader className="border-b border-border/70 py-3">
157
+ <CardTitle className="text-sm font-semibold">Custo por aula</CardTitle>
158
+ <CardDescription>
159
+ Quanto cada aula consumiu em IA, da mais cara para a mais barata.
160
+ </CardDescription>
161
+ </CardHeader>
162
+ <CardContent className="p-0">
163
+ {data.byLesson.length === 0 ? (
164
+ <div className="px-4 py-6 text-center text-sm text-muted-foreground">
165
+ Nenhum custo por aula registrado.
166
+ </div>
167
+ ) : (
168
+ <div className="divide-y divide-border/60">
169
+ {data.byLesson.map((row, index) => (
170
+ <div
171
+ key={`${row.lessonId ?? 'null'}-${index}`}
172
+ className="flex items-center justify-between gap-3 px-4 py-2.5"
173
+ >
174
+ <div className="min-w-0 flex-1 truncate text-sm text-foreground">
175
+ {row.lessonTitle ?? 'Aula removida'}
176
+ </div>
177
+ <div className="text-[11px] text-muted-foreground">
178
+ {row.runs} exec.
179
+ </div>
180
+ <div className="w-24 text-right text-sm font-semibold text-foreground">
181
+ {formatUsd(row.costUsd)}
182
+ </div>
183
+ </div>
184
+ ))}
185
+ </div>
186
+ )}
187
+ </CardContent>
188
+ </Card>
189
+ </div>
190
+ );
191
+ }
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { useState } from 'react';
3
+ import { useEffect, useState } from 'react';
4
4
  import { toast } from 'sonner';
5
5
  import {
6
6
  AlignLeft,
@@ -8,6 +8,7 @@ import {
8
8
  Download,
9
9
  FileArchive,
10
10
  Info,
11
+ Languages,
11
12
  Loader2,
12
13
  Monitor,
13
14
  Moon,
@@ -19,6 +20,7 @@ import { cn } from '@/lib/utils';
19
20
  import { Alert, AlertDescription } from '@/components/ui/alert';
20
21
  import { Badge } from '@/components/ui/badge';
21
22
  import { Button } from '@/components/ui/button';
23
+ import { Checkbox } from '@/components/ui/checkbox';
22
24
  import { Label } from '@/components/ui/label';
23
25
  import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
24
26
  import {
@@ -31,6 +33,7 @@ import {
31
33
  import {
32
34
  ScormVisualSettings,
33
35
  useCreateCourseExportMutation,
36
+ useCourseTranscriptionLocalesQuery,
34
37
  } from '../_data/use-course-exports';
35
38
 
36
39
  // ── Color presets ────────────────────────────────────────────
@@ -119,7 +122,23 @@ export function CourseExportSheet({
119
122
  estimatedSizeMb,
120
123
  }: CourseExportSheetProps) {
121
124
  const [visual, setVisual] = useState<Required<ScormVisualSettings>>(DEFAULT_VISUAL);
125
+ const [selectedLocaleIds, setSelectedLocaleIds] = useState<number[]>([]);
122
126
  const createExport = useCreateCourseExportMutation(courseId);
127
+ const { data: availableLocales = [] } = useCourseTranscriptionLocalesQuery(
128
+ open ? courseId : null,
129
+ );
130
+
131
+ useEffect(() => {
132
+ if (availableLocales.length > 0) {
133
+ setSelectedLocaleIds(availableLocales.map((l) => l.id));
134
+ }
135
+ }, [availableLocales]);
136
+
137
+ const toggleLocale = (id: number) => {
138
+ setSelectedLocaleIds((prev) =>
139
+ prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id],
140
+ );
141
+ };
123
142
 
124
143
  const setV = <K extends keyof ScormVisualSettings>(
125
144
  key: K,
@@ -131,6 +150,7 @@ export function CourseExportSheet({
131
150
  await createExport.mutateAsync({
132
151
  format: 'scorm_1_2',
133
152
  visualSettings: visual,
153
+ subtitleLocaleIds: selectedLocaleIds.length > 0 ? selectedLocaleIds : undefined,
134
154
  });
135
155
  onOpenChange(false);
136
156
  toast.success(
@@ -198,6 +218,66 @@ export function CourseExportSheet({
198
218
  </Alert>
199
219
  )}
200
220
 
221
+ {/* ── Subtitles ── */}
222
+ <div className="rounded-lg border overflow-hidden">
223
+ <div className="flex items-center gap-2 px-3 py-2.5 border-b bg-muted/20">
224
+ <Languages className="size-4 text-muted-foreground" />
225
+ <span className="text-sm font-medium">Legendas</span>
226
+ {availableLocales.length > 0 && (
227
+ <span className="ml-auto text-xs text-muted-foreground">
228
+ {selectedLocaleIds.length} de {availableLocales.length} selecionado(s)
229
+ </span>
230
+ )}
231
+ </div>
232
+ <div className="px-3 py-3">
233
+ {availableLocales.length === 0 ? (
234
+ <p className="text-xs text-muted-foreground">
235
+ Nenhuma legenda disponível. Gere transcrições nas aulas para incluí-las na exportação.
236
+ </p>
237
+ ) : (
238
+ <div className="flex flex-col gap-2">
239
+ {availableLocales.map((loc) => (
240
+ <label
241
+ key={loc.id}
242
+ className="flex items-center gap-2.5 cursor-pointer group"
243
+ >
244
+ <Checkbox
245
+ checked={selectedLocaleIds.includes(loc.id)}
246
+ onCheckedChange={() => toggleLocale(loc.id)}
247
+ id={`locale-${loc.id}`}
248
+ />
249
+ <span className="text-sm group-hover:text-foreground transition-colors">
250
+ {loc.name}
251
+ {loc.region && (
252
+ <span className="text-xs text-muted-foreground ml-1">
253
+ ({loc.code}-{loc.region})
254
+ </span>
255
+ )}
256
+ </span>
257
+ </label>
258
+ ))}
259
+ <div className="flex gap-2 mt-1 pt-1 border-t">
260
+ <button
261
+ type="button"
262
+ onClick={() => setSelectedLocaleIds(availableLocales.map((l) => l.id))}
263
+ className="text-xs text-primary hover:underline"
264
+ >
265
+ Selecionar todos
266
+ </button>
267
+ <span className="text-xs text-muted-foreground">·</span>
268
+ <button
269
+ type="button"
270
+ onClick={() => setSelectedLocaleIds([])}
271
+ className="text-xs text-primary hover:underline"
272
+ >
273
+ Desmarcar todos
274
+ </button>
275
+ </div>
276
+ </div>
277
+ )}
278
+ </div>
279
+ </div>
280
+
201
281
  {/* ── Appearance ── */}
202
282
  <div className="rounded-lg border overflow-hidden">
203
283
  <div className="flex items-center gap-2 px-3 py-2.5 border-b bg-muted/20">
@@ -58,6 +58,14 @@ function formatDuration(seconds: number | null | undefined): string {
58
58
  return `${h}h ${rm}m`;
59
59
  }
60
60
 
61
+ function formatFileSize(bytes: number | null | undefined): string {
62
+ if (bytes == null) return '—';
63
+ if (bytes < 1024) return `${bytes} B`;
64
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
65
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
66
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
67
+ }
68
+
61
69
  function StatusBadge({ status }: { status: CourseExportRecord['status'] }) {
62
70
  switch (status) {
63
71
  case 'completed':
@@ -203,6 +211,7 @@ export function CourseExportsTab({ courseId }: CourseExportsTabProps) {
203
211
  <TableHead>Formato</TableHead>
204
212
  <TableHead>Data</TableHead>
205
213
  <TableHead>Duração</TableHead>
214
+ <TableHead>Tamanho</TableHead>
206
215
  <TableHead>Status</TableHead>
207
216
  <TableHead className="text-right">Ações</TableHead>
208
217
  </TableRow>
@@ -232,6 +241,9 @@ export function CourseExportsTab({ courseId }: CourseExportsTabProps) {
232
241
  <TableCell className="text-xs text-muted-foreground">
233
242
  {formatDuration(exp.duration_seconds)}
234
243
  </TableCell>
244
+ <TableCell className="text-xs text-muted-foreground">
245
+ {formatFileSize(exp.file?.size)}
246
+ </TableCell>
235
247
  <TableCell>
236
248
  <StatusBadge status={exp.status} />
237
249
  </TableCell>