@hed-hog/lms 0.0.364 → 0.0.365

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 (218) hide show
  1. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +1 -0
  2. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
  3. package/dist/bitcode-wallet/bitcode-wallet.service.js +22 -3
  4. package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
  5. package/dist/course/course-export-scorm12-worker.service.d.ts +21 -0
  6. package/dist/course/course-export-scorm12-worker.service.d.ts.map +1 -0
  7. package/dist/course/course-export-scorm12-worker.service.js +109 -0
  8. package/dist/course/course-export-scorm12-worker.service.js.map +1 -0
  9. package/dist/course/course-export-scorm12.service.d.ts +42 -0
  10. package/dist/course/course-export-scorm12.service.d.ts.map +1 -0
  11. package/dist/course/course-export-scorm12.service.js +628 -0
  12. package/dist/course/course-export-scorm12.service.js.map +1 -0
  13. package/dist/course/course-export.service.d.ts +84 -0
  14. package/dist/course/course-export.service.d.ts.map +1 -0
  15. package/dist/course/course-export.service.js +237 -0
  16. package/dist/course/course-export.service.js.map +1 -0
  17. package/dist/course/course-structure.controller.d.ts +17 -9
  18. package/dist/course/course-structure.controller.d.ts.map +1 -1
  19. package/dist/course/course-structure.controller.js +17 -4
  20. package/dist/course/course-structure.controller.js.map +1 -1
  21. package/dist/course/course-structure.service.d.ts +12 -4
  22. package/dist/course/course-structure.service.d.ts.map +1 -1
  23. package/dist/course/course-structure.service.js +98 -23
  24. package/dist/course/course-structure.service.js.map +1 -1
  25. package/dist/course/course-video-hls.service.d.ts +57 -0
  26. package/dist/course/course-video-hls.service.d.ts.map +1 -0
  27. package/dist/course/course-video-hls.service.js +767 -0
  28. package/dist/course/course-video-hls.service.js.map +1 -0
  29. package/dist/course/course.controller.d.ts +45 -13
  30. package/dist/course/course.controller.d.ts.map +1 -1
  31. package/dist/course/course.controller.js +40 -26
  32. package/dist/course/course.controller.js.map +1 -1
  33. package/dist/course/course.mcp-tools.js +1 -1
  34. package/dist/course/course.mcp-tools.js.map +1 -1
  35. package/dist/course/course.module.d.ts.map +1 -1
  36. package/dist/course/course.module.js +11 -0
  37. package/dist/course/course.module.js.map +1 -1
  38. package/dist/course/course.service.d.ts +6 -9
  39. package/dist/course/course.service.d.ts.map +1 -1
  40. package/dist/course/course.service.js +57 -48
  41. package/dist/course/course.service.js.map +1 -1
  42. package/dist/course/dto/cleanup-course-storage.dto.d.ts +1 -1
  43. package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -1
  44. package/dist/course/dto/cleanup-course-storage.dto.js +1 -0
  45. package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -1
  46. package/dist/course/dto/cleanup-upload-history.dto.d.ts +1 -1
  47. package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -1
  48. package/dist/course/dto/cleanup-upload-history.dto.js +1 -1
  49. package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -1
  50. package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
  51. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
  52. package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
  53. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
  54. package/dist/course/dto/create-course-export.dto.d.ts +14 -0
  55. package/dist/course/dto/create-course-export.dto.d.ts.map +1 -0
  56. package/dist/course/dto/create-course-export.dto.js +71 -0
  57. package/dist/course/dto/create-course-export.dto.js.map +1 -0
  58. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +2 -2
  59. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  60. package/dist/course/dto/create-course-structure-lesson.dto.js +3 -2
  61. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  62. package/dist/course/lms-bulk-upload-automation.service.d.ts +16 -1
  63. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
  64. package/dist/course/lms-bulk-upload-automation.service.js +102 -8
  65. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
  66. package/dist/course/lms-bulk-upload-infra.service.d.ts +1 -0
  67. package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -1
  68. package/dist/course/lms-bulk-upload-infra.service.js +32 -8
  69. package/dist/course/lms-bulk-upload-infra.service.js.map +1 -1
  70. package/dist/course/lms-bulk-upload.controller.d.ts +30 -3
  71. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  72. package/dist/course/lms-bulk-upload.controller.js +43 -2
  73. package/dist/course/lms-bulk-upload.controller.js.map +1 -1
  74. package/dist/course/lms-bulk-upload.service.d.ts +11 -0
  75. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  76. package/dist/course/lms-bulk-upload.service.js +59 -6
  77. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  78. package/dist/course/lms-setting.controller.d.ts +2 -1
  79. package/dist/course/lms-setting.controller.d.ts.map +1 -1
  80. package/dist/course/lms-setting.controller.js +4 -2
  81. package/dist/course/lms-setting.controller.js.map +1 -1
  82. package/dist/course/scorm12-schemas.d.ts +4 -0
  83. package/dist/course/scorm12-schemas.d.ts.map +1 -0
  84. package/dist/course/scorm12-schemas.js +9 -0
  85. package/dist/course/scorm12-schemas.js.map +1 -0
  86. package/dist/enterprise/training/training-student.service.d.ts +51 -0
  87. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  88. package/dist/enterprise/training/training-student.service.js +217 -4
  89. package/dist/enterprise/training/training-student.service.js.map +1 -1
  90. package/dist/evaluation/evaluation.service.d.ts +18 -0
  91. package/dist/evaluation/evaluation.service.d.ts.map +1 -1
  92. package/dist/evaluation/evaluation.service.js +125 -0
  93. package/dist/evaluation/evaluation.service.js.map +1 -1
  94. package/dist/exam/dto/create-standalone-question.dto.d.ts +12 -0
  95. package/dist/exam/dto/create-standalone-question.dto.d.ts.map +1 -0
  96. package/dist/exam/dto/create-standalone-question.dto.js +70 -0
  97. package/dist/exam/dto/create-standalone-question.dto.js.map +1 -0
  98. package/dist/exam/exam.module.d.ts.map +1 -1
  99. package/dist/exam/exam.module.js +2 -1
  100. package/dist/exam/exam.module.js.map +1 -1
  101. package/dist/exam/exam.service.d.ts +21 -0
  102. package/dist/exam/exam.service.d.ts.map +1 -1
  103. package/dist/exam/exam.service.js +80 -0
  104. package/dist/exam/exam.service.js.map +1 -1
  105. package/dist/exam/question.controller.d.ts +27 -0
  106. package/dist/exam/question.controller.d.ts.map +1 -0
  107. package/dist/exam/question.controller.js +53 -0
  108. package/dist/exam/question.controller.js.map +1 -0
  109. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +4 -0
  110. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
  111. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +161 -25
  112. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
  113. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
  114. package/dist/lms-commerce-access.subscriber.d.ts +11 -0
  115. package/dist/lms-commerce-access.subscriber.d.ts.map +1 -0
  116. package/dist/lms-commerce-access.subscriber.js +74 -0
  117. package/dist/lms-commerce-access.subscriber.js.map +1 -0
  118. package/dist/lms.module.d.ts.map +1 -1
  119. package/dist/lms.module.js +6 -5
  120. package/dist/lms.module.js.map +1 -1
  121. package/dist/platforma/platforma-video.service.d.ts +39 -0
  122. package/dist/platforma/platforma-video.service.d.ts.map +1 -0
  123. package/dist/platforma/platforma-video.service.js +301 -0
  124. package/dist/platforma/platforma-video.service.js.map +1 -0
  125. package/dist/platforma/platforma.controller.d.ts +95 -1
  126. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  127. package/dist/platforma/platforma.controller.js +160 -2
  128. package/dist/platforma/platforma.controller.js.map +1 -1
  129. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts +5 -0
  130. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts.map +1 -0
  131. package/dist/student-xp/dto/grant-skill-card-xp.dto.js +26 -0
  132. package/dist/student-xp/dto/grant-skill-card-xp.dto.js.map +1 -0
  133. package/dist/student-xp/student-xp.controller.d.ts +15 -0
  134. package/dist/student-xp/student-xp.controller.d.ts.map +1 -1
  135. package/dist/student-xp/student-xp.controller.js +24 -0
  136. package/dist/student-xp/student-xp.controller.js.map +1 -1
  137. package/dist/student-xp/student-xp.service.d.ts +16 -0
  138. package/dist/student-xp/student-xp.service.d.ts.map +1 -1
  139. package/dist/student-xp/student-xp.service.js +51 -1
  140. package/dist/student-xp/student-xp.service.js.map +1 -1
  141. package/hedhog/data/evaluation_topic.yaml +17 -0
  142. package/hedhog/data/menu.yaml +0 -17
  143. package/hedhog/data/queue_definition.yaml +48 -0
  144. package/hedhog/data/route.yaml +94 -124
  145. package/hedhog/data/setting_group.yaml +19 -19
  146. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +337 -41
  147. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +69 -4
  148. package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +420 -0
  149. package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +308 -0
  150. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +17 -15
  151. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -63
  152. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +8 -3
  153. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +31 -8
  154. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +16 -9
  155. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +201 -401
  156. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +378 -690
  157. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -2
  158. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +3 -9
  159. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -1
  160. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +6 -10
  161. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +49 -0
  162. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -3
  163. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +0 -1
  164. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +106 -0
  165. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +28 -1
  166. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +0 -2
  167. package/hedhog/frontend/app/courses/page.tsx.ejs +45 -0
  168. package/hedhog/frontend/messages/en.json +26 -28
  169. package/hedhog/frontend/messages/pt.json +26 -28
  170. package/hedhog/table/course_export.yaml +62 -0
  171. package/package.json +13 -9
  172. package/src/bitcode-wallet/bitcode-wallet.service.ts +43 -4
  173. package/src/course/course-export-scorm12-worker.service.ts +124 -0
  174. package/src/course/course-export-scorm12.service.ts +668 -0
  175. package/src/course/course-export.service.ts +280 -0
  176. package/src/course/course-structure.controller.ts +14 -2
  177. package/src/course/course-structure.service.ts +100 -7
  178. package/src/course/course-video-hls.service.ts +946 -0
  179. package/src/course/course.controller.ts +33 -19
  180. package/src/course/course.mcp-tools.ts +1 -1
  181. package/src/course/course.module.ts +11 -0
  182. package/src/course/course.service.ts +73 -60
  183. package/src/course/dto/cleanup-course-storage.dto.ts +1 -0
  184. package/src/course/dto/cleanup-upload-history.dto.ts +1 -1
  185. package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
  186. package/src/course/dto/create-course-export.dto.ts +56 -0
  187. package/src/course/dto/create-course-structure-lesson.dto.ts +4 -3
  188. package/src/course/lms-bulk-upload-automation.service.ts +153 -6
  189. package/src/course/lms-bulk-upload-infra.service.ts +39 -6
  190. package/src/course/lms-bulk-upload.controller.ts +32 -2
  191. package/src/course/lms-bulk-upload.service.ts +70 -7
  192. package/src/course/lms-setting.controller.ts +4 -2
  193. package/src/course/scorm12-schemas.ts +9 -0
  194. package/src/enterprise/training/training-student.service.ts +221 -2
  195. package/src/evaluation/evaluation.service.ts +123 -0
  196. package/src/exam/dto/create-standalone-question.dto.ts +66 -0
  197. package/src/exam/exam.module.ts +2 -1
  198. package/src/exam/exam.service.ts +86 -0
  199. package/src/exam/question.controller.ts +28 -0
  200. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +205 -31
  201. package/src/lms-commerce-access.subscriber.ts +88 -0
  202. package/src/lms.module.ts +6 -5
  203. package/src/platforma/platforma-video.service.ts +346 -0
  204. package/src/platforma/platforma.controller.ts +95 -1
  205. package/src/platforma/platforma.service.ts +268 -268
  206. package/src/student-xp/dto/grant-skill-card-xp.dto.ts +10 -0
  207. package/src/student-xp/student-xp.controller.ts +18 -2
  208. package/src/student-xp/student-xp.service.ts +84 -2
  209. package/hedhog/data/video_resolution_profile.yaml +0 -7
  210. package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +0 -607
  211. package/hedhog/table/course_video_resolution_profile.yaml +0 -22
  212. package/hedhog/table/video_resolution_profile.yaml +0 -18
  213. package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +0 -16
  214. package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +0 -16
  215. package/src/video-resolution-profile/video-resolution-profile.controller.ts +0 -62
  216. package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +0 -128
  217. package/src/video-resolution-profile/video-resolution-profile.module.ts +0 -13
  218. package/src/video-resolution-profile/video-resolution-profile.service.ts +0 -117
@@ -0,0 +1,308 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { toast } from 'sonner';
5
+ import {
6
+ CheckCircle2,
7
+ Clock,
8
+ Download,
9
+ FileArchive,
10
+ Loader2,
11
+ Trash2,
12
+ XCircle,
13
+ } from 'lucide-react';
14
+ import {
15
+ AlertDialog,
16
+ AlertDialogAction,
17
+ AlertDialogCancel,
18
+ AlertDialogContent,
19
+ AlertDialogDescription,
20
+ AlertDialogFooter,
21
+ AlertDialogHeader,
22
+ AlertDialogTitle,
23
+ } from '@/components/ui/alert-dialog';
24
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
25
+ import { Badge } from '@/components/ui/badge';
26
+ import { Button } from '@/components/ui/button';
27
+ import {
28
+ Table,
29
+ TableBody,
30
+ TableCell,
31
+ TableHead,
32
+ TableHeader,
33
+ TableRow,
34
+ } from '@/components/ui/table';
35
+ import { getPhotoUrl } from '@/lib/get-photo-url';
36
+ import { useApp } from '@hed-hog/next-app-provider';
37
+ import {
38
+ CourseExportRecord,
39
+ useDeleteCourseExportMutation,
40
+ useCourseExportsQuery,
41
+ } from '../_data/use-course-exports';
42
+
43
+ function formatDate(value: string | null | undefined): string {
44
+ if (!value) return '—';
45
+ const date = new Date(value);
46
+ if (Number.isNaN(date.getTime())) return '—';
47
+ return date.toLocaleString('pt-BR');
48
+ }
49
+
50
+ function formatDuration(seconds: number | null | undefined): string {
51
+ if (seconds == null) return '—';
52
+ if (seconds < 60) return `${seconds}s`;
53
+ const m = Math.floor(seconds / 60);
54
+ const s = seconds % 60;
55
+ if (m < 60) return s > 0 ? `${m}m ${s}s` : `${m}m`;
56
+ const h = Math.floor(m / 60);
57
+ const rm = m % 60;
58
+ return `${h}h ${rm}m`;
59
+ }
60
+
61
+ function StatusBadge({ status }: { status: CourseExportRecord['status'] }) {
62
+ switch (status) {
63
+ case 'completed':
64
+ return (
65
+ <Badge
66
+ variant="outline"
67
+ className="gap-1 text-emerald-600 border-emerald-200 bg-emerald-50"
68
+ >
69
+ <CheckCircle2 className="size-3" />
70
+ Concluído
71
+ </Badge>
72
+ );
73
+ case 'processing':
74
+ return (
75
+ <Badge
76
+ variant="outline"
77
+ className="gap-1 text-blue-600 border-blue-200 bg-blue-50"
78
+ >
79
+ <Loader2 className="size-3 animate-spin" />
80
+ Processando
81
+ </Badge>
82
+ );
83
+ case 'failed':
84
+ return (
85
+ <Badge
86
+ variant="outline"
87
+ className="gap-1 text-red-600 border-red-200 bg-red-50"
88
+ >
89
+ <XCircle className="size-3" />
90
+ Falhou
91
+ </Badge>
92
+ );
93
+ default:
94
+ return (
95
+ <Badge variant="outline" className="gap-1 text-muted-foreground">
96
+ <Clock className="size-3" />
97
+ Aguardando
98
+ </Badge>
99
+ );
100
+ }
101
+ }
102
+
103
+ function FormatBadge({ format }: { format: string }) {
104
+ if (format === 'scorm_1_2') {
105
+ return (
106
+ <Badge variant="secondary" className="gap-1">
107
+ <FileArchive className="size-3" />
108
+ SCORM 1.2
109
+ </Badge>
110
+ );
111
+ }
112
+ return <Badge variant="outline">{format}</Badge>;
113
+ }
114
+
115
+ interface CourseExportsTabProps {
116
+ courseId: string;
117
+ }
118
+
119
+ export function CourseExportsTab({ courseId }: CourseExportsTabProps) {
120
+ const { request } = useApp();
121
+ const { data: exports, isLoading } = useCourseExportsQuery(courseId);
122
+ const [downloadingIds, setDownloadingIds] = useState<Set<number>>(new Set());
123
+ const [confirmDeleteRecord, setConfirmDeleteRecord] =
124
+ useState<CourseExportRecord | null>(null);
125
+
126
+ const deleteMutation = useDeleteCourseExportMutation(courseId);
127
+
128
+ const handleDownload = async (exportRecord: CourseExportRecord) => {
129
+ if (!exportRecord.file_id) return;
130
+
131
+ setDownloadingIds((prev) => new Set(prev).add(exportRecord.id));
132
+ try {
133
+ const response = await request<{ url?: string }>({
134
+ url: `/file/download/${exportRecord.file_id}`,
135
+ method: 'PUT',
136
+ });
137
+ const url = response?.data?.url;
138
+ if (!url) {
139
+ toast.error('Não foi possível gerar o link de download.');
140
+ return;
141
+ }
142
+ const a = document.createElement('a');
143
+ a.href = url;
144
+ a.download = exportRecord.file?.filename ?? `export_${exportRecord.id}.zip`;
145
+ document.body.appendChild(a);
146
+ a.click();
147
+ document.body.removeChild(a);
148
+ } catch {
149
+ toast.error('Erro ao baixar o arquivo. Tente novamente.');
150
+ } finally {
151
+ setDownloadingIds((prev) => {
152
+ const next = new Set(prev);
153
+ next.delete(exportRecord.id);
154
+ return next;
155
+ });
156
+ }
157
+ };
158
+
159
+ const handleConfirmDelete = async () => {
160
+ if (!confirmDeleteRecord) return;
161
+ const id = confirmDeleteRecord.id;
162
+ setConfirmDeleteRecord(null);
163
+ try {
164
+ await deleteMutation.mutateAsync(id);
165
+ toast.success('Exportação excluída com sucesso.');
166
+ } catch {
167
+ toast.error('Erro ao excluir a exportação. Tente novamente.');
168
+ }
169
+ };
170
+
171
+ if (isLoading) {
172
+ return (
173
+ <div className="flex items-center justify-center py-12">
174
+ <Loader2 className="size-5 animate-spin text-muted-foreground" />
175
+ </div>
176
+ );
177
+ }
178
+
179
+ if (!exports || exports.length === 0) {
180
+ return (
181
+ <div className="flex flex-col items-center justify-center gap-2 py-12 text-center">
182
+ <FileArchive className="size-8 text-muted-foreground/40" />
183
+ <p className="text-sm text-muted-foreground">
184
+ Nenhuma exportação realizada ainda.
185
+ </p>
186
+ <p className="text-xs text-muted-foreground/70">
187
+ Use o menu de Jobs para iniciar uma exportação SCORM 1.2.
188
+ </p>
189
+ </div>
190
+ );
191
+ }
192
+
193
+ const canDelete = (exp: CourseExportRecord) =>
194
+ exp.status === 'completed' || exp.status === 'failed';
195
+
196
+ return (
197
+ <>
198
+ <div className="rounded-md border overflow-hidden">
199
+ <Table>
200
+ <TableHeader>
201
+ <TableRow>
202
+ <TableHead>Usuário</TableHead>
203
+ <TableHead>Formato</TableHead>
204
+ <TableHead>Data</TableHead>
205
+ <TableHead>Duração</TableHead>
206
+ <TableHead>Status</TableHead>
207
+ <TableHead className="text-right">Ações</TableHead>
208
+ </TableRow>
209
+ </TableHeader>
210
+ <TableBody>
211
+ {exports.map((exp) => (
212
+ <TableRow key={exp.id}>
213
+ <TableCell>
214
+ <div className="flex items-center gap-2">
215
+ <Avatar className="size-7">
216
+ <AvatarImage src={getPhotoUrl(exp.user.photo_id)} />
217
+ <AvatarFallback className="text-xs">
218
+ {exp.user.name?.charAt(0)?.toUpperCase() ?? '?'}
219
+ </AvatarFallback>
220
+ </Avatar>
221
+ <span className="text-sm truncate max-w-30">
222
+ {exp.user.name}
223
+ </span>
224
+ </div>
225
+ </TableCell>
226
+ <TableCell>
227
+ <FormatBadge format={exp.format} />
228
+ </TableCell>
229
+ <TableCell className="text-xs text-muted-foreground whitespace-nowrap">
230
+ {formatDate(exp.created_at)}
231
+ </TableCell>
232
+ <TableCell className="text-xs text-muted-foreground">
233
+ {formatDuration(exp.duration_seconds)}
234
+ </TableCell>
235
+ <TableCell>
236
+ <StatusBadge status={exp.status} />
237
+ </TableCell>
238
+ <TableCell className="text-right">
239
+ <div className="flex items-center justify-end gap-1">
240
+ {exp.status === 'completed' && exp.file_id ? (
241
+ <Button
242
+ variant="ghost"
243
+ size="sm"
244
+ className="gap-1 h-7 text-xs"
245
+ disabled={downloadingIds.has(exp.id)}
246
+ onClick={() => handleDownload(exp)}
247
+ >
248
+ {downloadingIds.has(exp.id) ? (
249
+ <Loader2 className="size-3 animate-spin" />
250
+ ) : (
251
+ <Download className="size-3" />
252
+ )}
253
+ Baixar
254
+ </Button>
255
+ ) : null}
256
+ {canDelete(exp) ? (
257
+ <Button
258
+ variant="ghost"
259
+ size="sm"
260
+ className="gap-1 h-7 text-xs text-red-600 hover:text-red-700 hover:bg-red-50"
261
+ disabled={deleteMutation.isPending}
262
+ onClick={() => setConfirmDeleteRecord(exp)}
263
+ >
264
+ <Trash2 className="size-3" />
265
+ Excluir
266
+ </Button>
267
+ ) : (
268
+ !exp.file_id && exp.status !== 'completed' && (
269
+ <span className="text-xs text-muted-foreground">—</span>
270
+ )
271
+ )}
272
+ </div>
273
+ </TableCell>
274
+ </TableRow>
275
+ ))}
276
+ </TableBody>
277
+ </Table>
278
+ </div>
279
+
280
+ <AlertDialog
281
+ open={confirmDeleteRecord !== null}
282
+ onOpenChange={(open) => {
283
+ if (!open) setConfirmDeleteRecord(null);
284
+ }}
285
+ >
286
+ <AlertDialogContent>
287
+ <AlertDialogHeader>
288
+ <AlertDialogTitle>Excluir exportação?</AlertDialogTitle>
289
+ <AlertDialogDescription>
290
+ Esta ação irá excluir permanentemente o registro e o arquivo ZIP
291
+ gerado pela exportação. Não será possível recuperar o arquivo após
292
+ a exclusão.
293
+ </AlertDialogDescription>
294
+ </AlertDialogHeader>
295
+ <AlertDialogFooter>
296
+ <AlertDialogCancel>Cancelar</AlertDialogCancel>
297
+ <AlertDialogAction
298
+ className="bg-red-600 text-white hover:bg-red-700"
299
+ onClick={handleConfirmDelete}
300
+ >
301
+ Excluir
302
+ </AlertDialogAction>
303
+ </AlertDialogFooter>
304
+ </AlertDialogContent>
305
+ </AlertDialog>
306
+ </>
307
+ );
308
+ }
@@ -1,9 +1,5 @@
1
1
  'use client';
2
2
 
3
- import { useApp, useQuery } from '@hed-hog/next-app-provider';
4
- import { useQueryClient } from '@tanstack/react-query';
5
- import { Button } from '@/components/ui/button';
6
- import { Badge } from '@/components/ui/badge';
7
3
  import {
8
4
  AlertDialog,
9
5
  AlertDialogAction,
@@ -14,6 +10,8 @@ import {
14
10
  AlertDialogHeader,
15
11
  AlertDialogTitle,
16
12
  } from '@/components/ui/alert-dialog';
13
+ import { Badge } from '@/components/ui/badge';
14
+ import { Button } from '@/components/ui/button';
17
15
  import {
18
16
  Card,
19
17
  CardContent,
@@ -28,26 +26,29 @@ import {
28
26
  } from '@/components/ui/chart';
29
27
  import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
30
28
  import { Skeleton } from '@/components/ui/skeleton';
29
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
30
+ import { useQueryClient } from '@tanstack/react-query';
31
31
  import {
32
- Download,
33
32
  BookOpen,
34
33
  Clapperboard,
35
- Loader2,
34
+ Download,
35
+ FileArchive,
36
36
  FileText,
37
37
  Film,
38
38
  HardDrive,
39
39
  Image,
40
40
  Layers,
41
+ Loader2,
41
42
  Mic,
42
43
  Paperclip,
43
44
  Sparkles,
44
45
  Target,
45
46
  Trash2,
46
- type LucideIcon,
47
47
  Zap,
48
+ type LucideIcon,
48
49
  } from 'lucide-react';
49
50
  import { useTranslations } from 'next-intl';
50
- import React, { useEffect, useMemo, useState } from 'react';
51
+ import { useEffect, useMemo, useState } from 'react';
51
52
  import { Cell, Pie, PieChart } from 'recharts';
52
53
  import { toast } from 'sonner';
53
54
  import { useCourseContentOverviewQuery } from '../_data/use-course-content-overview';
@@ -57,7 +58,6 @@ const TYPE_COLORS: Record<string, string> = {
57
58
  video: '#0ea5e9',
58
59
  questao: '#f97316',
59
60
  post: '#22c55e',
60
- exercicio: '#a855f7',
61
61
  };
62
62
 
63
63
  const TYPE_GRADIENTS: Record<
@@ -79,11 +79,6 @@ const TYPE_GRADIENTS: Record<
79
79
  icon: BookOpen,
80
80
  label: 'Post',
81
81
  },
82
- exercicio: {
83
- from: 'from-violet-500/12 via-fuchsia-500/6',
84
- icon: Target,
85
- label: 'Exercício',
86
- },
87
82
  };
88
83
 
89
84
  const AREA_COLORS = ['#0f766e', '#0ea5e9', '#14b8a6', '#38bdf8', '#0891b2'];
@@ -162,6 +157,13 @@ const STORAGE_CATEGORY_META: Record<
162
157
  iconClassName: 'text-muted-foreground',
163
158
  iconBackgroundClassName: 'bg-muted',
164
159
  },
160
+ course_export: {
161
+ label: 'Arquivos de exportação',
162
+ accentClassName: 'from-emerald-500/20 via-green-500/10 to-transparent',
163
+ icon: FileArchive,
164
+ iconClassName: 'text-emerald-600 dark:text-emerald-400',
165
+ iconBackgroundClassName: 'bg-emerald-500/10',
166
+ },
165
167
  };
166
168
 
167
169
  interface Props {
@@ -299,7 +301,7 @@ export function CourseOverviewTab({ courseId, locale }: Props) {
299
301
 
300
302
  const lessonTypeData = useMemo(() => {
301
303
  if (!data) return [];
302
- const types = ['video', 'questao', 'post', 'exercicio'] as const;
304
+ const types = ['video', 'questao', 'post'] as const;
303
305
  return types
304
306
  .map((type) => ({
305
307
  type,
@@ -11,7 +11,6 @@ import {
11
11
  ExternalLink,
12
12
  FileText,
13
13
  HelpCircle,
14
- Layers,
15
14
  Loader2,
16
15
  Mic,
17
16
  Paperclip,
@@ -25,8 +24,8 @@ import {
25
24
  useStartTranscriptionMutation,
26
25
  useTranscriptionSegmentsQuery,
27
26
  } from '../_data/use-transcription-segments';
28
- import { useStructureStore } from './store';
29
27
  import { LessonXpTab } from './detail-lesson-xp-tab';
28
+ import { useStructureStore } from './store';
30
29
  import type { LessonType } from './types';
31
30
 
32
31
  const LESSON_TYPE_CONFIG: Record<
@@ -51,12 +50,6 @@ const LESSON_TYPE_CONFIG: Record<
51
50
  bg: 'bg-amber-500/10',
52
51
  label: 'Quiz',
53
52
  },
54
- exercicio: {
55
- icon: Layers,
56
- color: 'text-rose-500',
57
- bg: 'bg-rose-500/10',
58
- label: 'Exercício',
59
- },
60
53
  };
61
54
 
62
55
  const VIDEO_PROVIDER_LABELS: Record<string, string> = {
@@ -86,7 +79,9 @@ export function DetailLesson({ lessonId }: DetailLessonProps) {
86
79
 
87
80
  const { data: segments = [], isLoading: segmentsLoading } =
88
81
  useTranscriptionSegmentsQuery(
89
- activeTab === 'transcription' || activeTab === 'xp' ? (lesson?.id ?? null) : null
82
+ activeTab === 'transcription' || activeTab === 'xp'
83
+ ? (lesson?.id ?? null)
84
+ : null
90
85
  );
91
86
 
92
87
  const queryClient = useQueryClient();
@@ -97,7 +92,9 @@ export function DetailLesson({ lessonId }: DetailLessonProps) {
97
92
  startTranscription({
98
93
  onSuccess: () => {
99
94
  toast.success('Transcrição iniciada!');
100
- queryClient.invalidateQueries({ queryKey: ['lesson-transcription-segments', lesson?.id] });
95
+ queryClient.invalidateQueries({
96
+ queryKey: ['lesson-transcription-segments', lesson?.id],
97
+ });
101
98
  },
102
99
  onError: (err) => {
103
100
  toast.error(err.message || 'Erro ao iniciar a transcrição.');
@@ -243,64 +240,55 @@ export function DetailLesson({ lessonId }: DetailLessonProps) {
243
240
  </Section>
244
241
  </>
245
242
  )}
246
-
247
- {!lesson.publicDescription &&
248
- !lesson.privateDescription &&
249
- lesson.type === 'exercicio' && (
250
- <div className="flex flex-col items-center gap-2 py-8 text-center">
251
- <Layers className="size-8 text-muted-foreground/40" />
252
- <p className="text-sm text-muted-foreground">
253
- Sem informações adicionais nesta aula.
254
- </p>
255
- </div>
256
- )}
257
243
  </div>
258
244
  </TabsContent>
259
245
 
260
246
  {/* ── Transcrição ───────────────────────────────────────────────────── */}
261
- {lmsSettings.transcriptionEnabled && <TabsContent
262
- value="transcription"
263
- className="flex-1 overflow-y-auto px-4 pb-4 mt-3"
264
- >
265
- <div className="flex justify-end mb-3">
266
- <Button
267
- size="sm"
268
- variant="outline"
269
- onClick={handleStartTranscription}
270
- disabled={isStarting}
271
- >
272
- {isStarting ? (
273
- <Loader2 className="size-4 mr-2 animate-spin" />
274
- ) : (
275
- <Mic className="size-4 mr-2" />
276
- )}
277
- Iniciar transcrição
278
- </Button>
279
- </div>
280
- {segmentsLoading ? (
281
- <div className="p-4 text-sm text-muted-foreground">
282
- Carregando transcrição...
247
+ {lmsSettings.transcriptionEnabled && (
248
+ <TabsContent
249
+ value="transcription"
250
+ className="flex-1 overflow-y-auto px-4 pb-4 mt-3"
251
+ >
252
+ <div className="flex justify-end mb-3">
253
+ <Button
254
+ size="sm"
255
+ variant="outline"
256
+ onClick={handleStartTranscription}
257
+ disabled={isStarting}
258
+ >
259
+ {isStarting ? (
260
+ <Loader2 className="size-4 mr-2 animate-spin" />
261
+ ) : (
262
+ <Mic className="size-4 mr-2" />
263
+ )}
264
+ Iniciar transcrição
265
+ </Button>
283
266
  </div>
284
- ) : segments.length ? (
285
- <div className="space-y-1.5 p-3">
286
- {segments.map((seg) => (
287
- <div key={seg.id} className="flex gap-3 text-sm">
288
- <span className="shrink-0 font-mono text-xs text-muted-foreground w-24">
289
- [{formatSeconds(seg.startSeconds)} –{' '}
290
- {formatSeconds(seg.endSeconds)}]
291
- </span>
292
- <span className="leading-relaxed">{seg.text}</span>
293
- </div>
294
- ))}
295
- </div>
296
- ) : (
297
- <EmptyState
298
- icon={<FileText className="size-8 text-muted-foreground/40" />}
299
- >
300
- Transcrição não disponível para esta aula.
301
- </EmptyState>
302
- )}
303
- </TabsContent>}
267
+ {segmentsLoading ? (
268
+ <div className="p-4 text-sm text-muted-foreground">
269
+ Carregando transcrição...
270
+ </div>
271
+ ) : segments.length ? (
272
+ <div className="space-y-1.5 p-3">
273
+ {segments.map((seg) => (
274
+ <div key={seg.id} className="flex gap-3 text-sm">
275
+ <span className="shrink-0 font-mono text-xs text-muted-foreground w-24">
276
+ [{formatSeconds(seg.startSeconds)} –{' '}
277
+ {formatSeconds(seg.endSeconds)}]
278
+ </span>
279
+ <span className="leading-relaxed">{seg.text}</span>
280
+ </div>
281
+ ))}
282
+ </div>
283
+ ) : (
284
+ <EmptyState
285
+ icon={<FileText className="size-8 text-muted-foreground/40" />}
286
+ >
287
+ Transcrição não disponível para esta aula.
288
+ </EmptyState>
289
+ )}
290
+ </TabsContent>
291
+ )}
304
292
 
305
293
  {/* ── XP da Aula ────────────────────────────────────────────────────── */}
306
294
  <TabsContent
@@ -10,7 +10,12 @@ import { EditorLesson } from './editor-lesson';
10
10
  import { EditorSession } from './editor-session';
11
11
  import { useStructureStore } from './store';
12
12
 
13
- export const DetailPanel = memo(function DetailPanel() {
13
+ interface DetailPanelProps {
14
+ defaultTab?: string;
15
+ onTabChange?: (tab: string) => void;
16
+ }
17
+
18
+ export const DetailPanel = memo(function DetailPanel({ defaultTab, onTabChange }: DetailPanelProps) {
14
19
  const t = useTranslations('lms.CoursesPage.StructurePage');
15
20
  const { activeItemId, activeItemType, selectedIds } = useStructureStore();
16
21
 
@@ -47,7 +52,7 @@ export const DetailPanel = memo(function DetailPanel() {
47
52
  }
48
53
 
49
54
  if (activeItemType === 'course') {
50
- return <EditorCourse />;
55
+ return <EditorCourse defaultTab={defaultTab} onTabChange={onTabChange} />;
51
56
  }
52
57
 
53
58
  if (activeItemType === 'session') {
@@ -55,7 +60,7 @@ export const DetailPanel = memo(function DetailPanel() {
55
60
  }
56
61
 
57
62
  if (activeItemType === 'lesson') {
58
- return <EditorLesson lessonId={activeItemId} />;
63
+ return <EditorLesson lessonId={activeItemId} defaultTab={defaultTab} onTabChange={onTabChange} />;
59
64
  }
60
65
 
61
66
  return null;
@@ -18,10 +18,24 @@ const LESSON_TYPE_CONFIG: Record<
18
18
  LessonType,
19
19
  { icon: LucideIcon; color: string; label: string; bg: string }
20
20
  > = {
21
- video: { icon: Video, color: 'text-blue-500', label: 'Vídeo', bg: 'bg-blue-500/10' },
22
- post: { icon: FileText, color: 'text-emerald-500', label: 'Post', bg: 'bg-emerald-500/10' },
23
- questao: { icon: HelpCircle, color: 'text-amber-500', label: 'Quiz', bg: 'bg-amber-500/10' },
24
- exercicio: { icon: Layers, color: 'text-rose-500', label: 'Exercício', bg: 'bg-rose-500/10' },
21
+ video: {
22
+ icon: Video,
23
+ color: 'text-blue-500',
24
+ label: 'Vídeo',
25
+ bg: 'bg-blue-500/10',
26
+ },
27
+ post: {
28
+ icon: FileText,
29
+ color: 'text-emerald-500',
30
+ label: 'Post',
31
+ bg: 'bg-emerald-500/10',
32
+ },
33
+ questao: {
34
+ icon: HelpCircle,
35
+ color: 'text-amber-500',
36
+ label: 'Quiz',
37
+ bg: 'bg-amber-500/10',
38
+ },
25
39
  };
26
40
 
27
41
  interface DetailSessionProps {
@@ -29,8 +43,14 @@ interface DetailSessionProps {
29
43
  }
30
44
 
31
45
  export function DetailSession({ sessionId }: DetailSessionProps) {
32
- const session = useStructureStore((s) => s.sessions.find((ss) => ss.id === sessionId));
33
- const lessons = useStructureStore((s) => s.lessons.filter((l) => l.sessionId === sessionId).sort((a, b) => a.order - b.order));
46
+ const session = useStructureStore((s) =>
47
+ s.sessions.find((ss) => ss.id === sessionId)
48
+ );
49
+ const lessons = useStructureStore((s) =>
50
+ s.lessons
51
+ .filter((l) => l.sessionId === sessionId)
52
+ .sort((a, b) => a.order - b.order)
53
+ );
34
54
  const selectItem = useStructureStore((s) => s.selectItem);
35
55
 
36
56
  if (!session) return null;
@@ -157,7 +177,10 @@ function LessonListItem({
157
177
  {lesson.duration}min
158
178
  </span>
159
179
  </div>
160
- <Badge variant="outline" className="text-[0.55rem] px-1 py-0 h-4 shrink-0">
180
+ <Badge
181
+ variant="outline"
182
+ className="text-[0.55rem] px-1 py-0 h-4 shrink-0"
183
+ >
161
184
  {lesson.code}
162
185
  </Badge>
163
186
  </button>
@@ -171,4 +194,4 @@ function StatCard({ label, value }: { label: string; value: string | number }) {
171
194
  <span className="text-[0.65rem] text-muted-foreground">{label}</span>
172
195
  </div>
173
196
  );
174
- }
197
+ }