@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
@@ -83,6 +83,7 @@ type UploadStatus =
83
83
  | 'queued'
84
84
  | 'uploading'
85
85
  | 'cancelling'
86
+ | 'processing'
86
87
  | 'received'
87
88
  | 'done'
88
89
  | 'error'
@@ -116,6 +117,9 @@ type UploadItemRow = {
116
117
  matchedSessionTitle: string | null;
117
118
  matchedLessonId: number | null;
118
119
  matchedLessonTitle: string | null;
120
+ jobStatus: string | null;
121
+ jobAttempts: number | null;
122
+ jobMaxAttempts: number | null;
119
123
  };
120
124
 
121
125
  type UploadListResponse = {
@@ -271,6 +275,15 @@ function formatDate(value: string | null) {
271
275
 
272
276
  function getStatusMeta(status: UploadStatus) {
273
277
  switch (status) {
278
+ case 'processing':
279
+ return {
280
+ label: 'Aula vinculada',
281
+ variant: 'secondary' as const,
282
+ icon: Loader2,
283
+ iconClass: 'text-violet-500 animate-spin',
284
+ animated: true,
285
+ progressClass: 'bg-violet-500',
286
+ };
274
287
  case 'received':
275
288
  return {
276
289
  label: 'Concluido',
@@ -394,6 +407,7 @@ export default function LmsBulkUploadSessionsPage() {
394
407
  } | null>(null);
395
408
  const [manualLinkStructureLoading, setManualLinkStructureLoading] = useState(false);
396
409
  const [isLinkingLesson, setIsLinkingLesson] = useState(false);
410
+ const [manualLinkIsReassign, setManualLinkIsReassign] = useState(false);
397
411
  const [pageSize, setPageSize] = usePersistedPageSize({
398
412
  storageKey: 'pagination:global:pageSize',
399
413
  defaultValue: PAGE_SIZE_OPTIONS[1],
@@ -446,7 +460,7 @@ export default function LmsBulkUploadSessionsPage() {
446
460
  },
447
461
  });
448
462
 
449
- const ACTIVE_STATUSES: UploadStatus[] = ['queued', 'uploading', 'cancelling', 'done'];
463
+ const ACTIVE_STATUSES: UploadStatus[] = ['queued', 'uploading', 'cancelling', 'processing', 'done'];
450
464
 
451
465
  const {
452
466
  data,
@@ -485,7 +499,7 @@ export default function LmsBulkUploadSessionsPage() {
485
499
  enabled: manualLinkOpen,
486
500
  queryFn: async () => {
487
501
  const response = await request<{ data: Array<{ id: number; title: string; code: string | null }> }>({
488
- url: '/course?pageSize=500',
502
+ url: '/lms/courses?pageSize=500',
489
503
  method: 'GET',
490
504
  });
491
505
  return response.data;
@@ -498,6 +512,7 @@ export default function LmsBulkUploadSessionsPage() {
498
512
  () => ({
499
513
  sending: rows.filter((row) => row.status === 'uploading').length,
500
514
  done: rows.filter((row) => row.status === 'received').length,
515
+ pending: rows.filter((row) => row.status === 'done').length,
501
516
  error: rows.filter((row) => row.status === 'error').length,
502
517
  }),
503
518
  [rows]
@@ -523,6 +538,15 @@ export default function LmsBulkUploadSessionsPage() {
523
538
  iconContainerClassName: 'bg-green-500/10 text-green-700',
524
539
  layout: 'compact' as const,
525
540
  },
541
+ {
542
+ key: 'pending',
543
+ title: 'Pendentes',
544
+ value: isLoading ? '-' : stats.pending,
545
+ icon: Clock,
546
+ accentClassName: 'from-blue-500/20 via-blue-500/10 to-transparent',
547
+ iconContainerClassName: 'bg-blue-500/10 text-blue-700',
548
+ layout: 'compact' as const,
549
+ },
526
550
  {
527
551
  key: 'error',
528
552
  title: 'Erros',
@@ -666,15 +690,29 @@ export default function LmsBulkUploadSessionsPage() {
666
690
  }
667
691
  };
668
692
 
669
- const openManualLink = async (itemId: number) => {
693
+ const openManualLink = async (
694
+ itemId: number,
695
+ prefill?: {
696
+ courseId: number | null;
697
+ sessionId: number | null;
698
+ lessonId: number | null;
699
+ }
700
+ ) => {
670
701
  setManualLinkItemId(itemId);
671
- setManualLinkCourseId(null);
672
- setManualLinkSessionId(null);
673
- setManualLinkLessonId(null);
702
+ setManualLinkCourseId(prefill?.courseId ?? null);
703
+ setManualLinkSessionId(prefill?.sessionId ?? null);
704
+ setManualLinkLessonId(prefill?.lessonId ?? null);
674
705
  setManualLinkStructure(null);
675
706
  setManualLinkVideoUrl(null);
707
+ setManualLinkIsReassign(Boolean(prefill?.courseId && prefill?.lessonId));
676
708
  setManualLinkOpen(true);
677
709
  setManualLinkVideoLoading(true);
710
+ if (prefill?.courseId) {
711
+ void loadCourseStructure(prefill.courseId, {
712
+ sessionId: prefill.sessionId,
713
+ lessonId: prefill.lessonId,
714
+ });
715
+ }
678
716
  try {
679
717
  const res = await request<{ url: string }>({
680
718
  url: `/lms/bulk-upload/item/${itemId}/video-url`,
@@ -688,16 +726,35 @@ export default function LmsBulkUploadSessionsPage() {
688
726
  }
689
727
  };
690
728
 
691
- const loadCourseStructure = async (courseId: number) => {
729
+ const loadCourseStructure = async (
730
+ courseId: number,
731
+ keep?: { sessionId: number | null; lessonId: number | null }
732
+ ) => {
692
733
  setManualLinkStructureLoading(true);
693
- setManualLinkSessionId(null);
694
- setManualLinkLessonId(null);
734
+ if (!keep) {
735
+ setManualLinkSessionId(null);
736
+ setManualLinkLessonId(null);
737
+ }
695
738
  try {
696
- const res = await request<{ sessions: Array<{ id: number; title: string; lessons: Array<{ id: number; title: string }> }> }>({
697
- url: `/course/${courseId}/structure`,
739
+ const res = await request<{
740
+ sessoes: Array<{ id: string; titulo: string }>;
741
+ aulas: Array<{ id: string; titulo: string; sessaoId: string }>;
742
+ }>({
743
+ url: `/lms/courses/${courseId}/structure`,
698
744
  method: 'GET',
699
745
  });
700
- setManualLinkStructure({ sessions: res.data.sessions ?? [] });
746
+ const sessions = (res.data.sessoes ?? []).map((sessao) => ({
747
+ id: Number(sessao.id),
748
+ title: sessao.titulo,
749
+ lessons: (res.data.aulas ?? [])
750
+ .filter((aula) => aula.sessaoId === sessao.id)
751
+ .map((aula) => ({ id: Number(aula.id), title: aula.titulo })),
752
+ }));
753
+ setManualLinkStructure({ sessions });
754
+ if (keep) {
755
+ setManualLinkSessionId(keep.sessionId);
756
+ setManualLinkLessonId(keep.lessonId);
757
+ }
701
758
  } catch {
702
759
  setManualLinkStructure(null);
703
760
  } finally {
@@ -1384,7 +1441,7 @@ export default function LmsBulkUploadSessionsPage() {
1384
1441
  onSaved={handleProfileSaved}
1385
1442
  />
1386
1443
 
1387
- <KpiCardsGrid items={kpiItems} columns={3} />
1444
+ <KpiCardsGrid items={kpiItems} columns={4} />
1388
1445
 
1389
1446
  <div className="flex items-center justify-between gap-3">
1390
1447
  <SearchBar
@@ -1465,9 +1522,21 @@ export default function LmsBulkUploadSessionsPage() {
1465
1522
  <FileTypeIcon filename={row.fileName} size={20} />
1466
1523
  </span>
1467
1524
  <div className="min-w-0 space-y-0.5">
1468
- <p className="truncate text-sm font-medium">
1469
- {row.fileName}
1470
- </p>
1525
+ <TooltipProvider>
1526
+ <Tooltip>
1527
+ <TooltipTrigger asChild>
1528
+ <p className="truncate text-sm font-medium cursor-default">
1529
+ {row.fileName}
1530
+ </p>
1531
+ </TooltipTrigger>
1532
+ <TooltipContent
1533
+ side="top"
1534
+ className="max-w-md break-all"
1535
+ >
1536
+ {row.fileName}
1537
+ </TooltipContent>
1538
+ </Tooltip>
1539
+ </TooltipProvider>
1471
1540
  <p className="truncate text-xs text-muted-foreground">
1472
1541
  Sessao #{row.sessionId} &middot; {row.appName}
1473
1542
  </p>
@@ -1512,12 +1581,13 @@ export default function LmsBulkUploadSessionsPage() {
1512
1581
 
1513
1582
  <TableCell className="max-w-[22rem]">
1514
1583
  {row.matchedCourseId && row.matchedLessonId ? (
1584
+ <div className="flex items-center gap-1">
1515
1585
  <button
1516
1586
  type="button"
1517
- className="flex w-full items-center gap-2 text-left"
1587
+ className="flex min-w-0 flex-1 items-center gap-2 text-left"
1518
1588
  onClick={() =>
1519
1589
  window.open(
1520
- `/lms/courses/${row.matchedCourseId}`,
1590
+ `/lms/courses/${row.matchedCourseId}?sel=${row.matchedLessonId}&type=lesson&tab=videos`,
1521
1591
  '_blank',
1522
1592
  'noopener,noreferrer'
1523
1593
  )
@@ -1553,7 +1623,33 @@ export default function LmsBulkUploadSessionsPage() {
1553
1623
  </p>
1554
1624
  </div>
1555
1625
  </button>
1556
- ) : row.status === 'lesson_not_found' ? (
1626
+ <TooltipProvider>
1627
+ <Tooltip>
1628
+ <TooltipTrigger asChild>
1629
+ <Button
1630
+ variant="ghost"
1631
+ size="icon"
1632
+ className="h-7 w-7 shrink-0 text-muted-foreground"
1633
+ onClick={(event) => {
1634
+ event.stopPropagation();
1635
+ openManualLink(row.id, {
1636
+ courseId: row.matchedCourseId,
1637
+ sessionId: row.matchedSessionId,
1638
+ lessonId: row.matchedLessonId,
1639
+ });
1640
+ }}
1641
+ aria-label="Alterar aula vinculada"
1642
+ >
1643
+ <Pencil className="h-3.5 w-3.5" />
1644
+ </Button>
1645
+ </TooltipTrigger>
1646
+ <TooltipContent>
1647
+ Alterar aula vinculada
1648
+ </TooltipContent>
1649
+ </Tooltip>
1650
+ </TooltipProvider>
1651
+ </div>
1652
+ ) : (row.status === 'lesson_not_found' || row.status === 'done') ? (
1557
1653
  <Button
1558
1654
  variant="outline"
1559
1655
  size="sm"
@@ -1569,11 +1665,21 @@ export default function LmsBulkUploadSessionsPage() {
1569
1665
  </TableCell>
1570
1666
 
1571
1667
  <TableCell>
1572
- <div className="flex items-center gap-1.5">
1573
- <StatusIcon
1574
- className={cn('h-3.5 w-3.5 shrink-0', meta.iconClass)}
1575
- />
1576
- <Badge variant={meta.variant}>{meta.label}</Badge>
1668
+ <div className="flex flex-col gap-1">
1669
+ <div className="flex items-center gap-1.5">
1670
+ <StatusIcon
1671
+ className={cn('h-3.5 w-3.5 shrink-0', meta.iconClass)}
1672
+ />
1673
+ <Badge variant={meta.variant}>{meta.label}</Badge>
1674
+ </div>
1675
+ {row.status === 'processing' && row.jobStatus ? (
1676
+ <p className="text-[11px] text-muted-foreground">
1677
+ Job: {row.jobStatus}
1678
+ {row.jobAttempts != null && row.jobMaxAttempts != null
1679
+ ? ` (${row.jobAttempts}/${row.jobMaxAttempts})`
1680
+ : ''}
1681
+ </p>
1682
+ ) : null}
1577
1683
  </div>
1578
1684
  </TableCell>
1579
1685
 
@@ -1638,10 +1744,14 @@ export default function LmsBulkUploadSessionsPage() {
1638
1744
  <SheetHeader className="border-b px-6 py-4">
1639
1745
  <SheetTitle className="flex items-center gap-2">
1640
1746
  <Link2 className="h-4 w-4" />
1641
- Vincular aula manualmente
1747
+ {manualLinkIsReassign
1748
+ ? 'Alterar aula vinculada'
1749
+ : 'Vincular aula manualmente'}
1642
1750
  </SheetTitle>
1643
1751
  <SheetDescription>
1644
- Selecione o curso, módulo e aula para associar este vídeo e iniciar o processamento.
1752
+ {manualLinkIsReassign
1753
+ ? 'Selecione o novo curso, módulo e aula para revincular este vídeo e reiniciar o processamento.'
1754
+ : 'Selecione o curso, módulo e aula para associar este vídeo e iniciar o processamento.'}
1645
1755
  </SheetDescription>
1646
1756
  </SheetHeader>
1647
1757
 
@@ -1739,7 +1849,9 @@ export default function LmsBulkUploadSessionsPage() {
1739
1849
  ) : (
1740
1850
  <Link2 className="h-4 w-4" />
1741
1851
  )}
1742
- Confirmar vínculo e iniciar processamento
1852
+ {manualLinkIsReassign
1853
+ ? 'Confirmar novo vínculo e reprocessar'
1854
+ : 'Confirmar vínculo e iniciar processamento'}
1743
1855
  </Button>
1744
1856
  </div>
1745
1857
  </ResizableSheetContent>
@@ -7,6 +7,7 @@ import {
7
7
  PageHeader,
8
8
  PaginationFooter,
9
9
  SearchBar,
10
+ ViewModeToggle,
10
11
  } from '@/components/entity-list';
11
12
  import { Badge } from '@/components/ui/badge';
12
13
  import { Button } from '@/components/ui/button';
@@ -38,8 +39,17 @@ import {
38
39
  SheetTitle,
39
40
  } from '@/components/ui/sheet';
40
41
  import { Skeleton } from '@/components/ui/skeleton';
42
+ import {
43
+ Table,
44
+ TableBody,
45
+ TableCell,
46
+ TableHead,
47
+ TableHeader,
48
+ TableRow,
49
+ } from '@/components/ui/table';
41
50
  import { Textarea } from '@/components/ui/textarea';
42
51
  import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
52
+ import { usePersistedViewMode } from '@/hooks/use-persisted-view-mode';
43
53
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
44
54
  import { zodResolver } from '@hookform/resolvers/zod';
45
55
  import {
@@ -96,7 +106,8 @@ type UpdateCertificateTemplatePayload = {
96
106
  status: TemplateStatus;
97
107
  };
98
108
 
99
- const PAGE_SIZES = [6, 12, 24];
109
+ const DEFAULT_PAGE_SIZES = [6, 12, 24, 48, 96] as const;
110
+ type ViewMode = 'cards' | 'list';
100
111
 
101
112
  const createTemplateSchema = (t: (key: string) => string) =>
102
113
  z.object({
@@ -140,10 +151,49 @@ export default function ModelsPage() {
140
151
  const [searchQuery, setSearchQuery] = useState('');
141
152
  const [statusFilter, setStatusFilter] = useState<StatusFilter>('all');
142
153
  const [currentPage, setCurrentPage] = useState(1);
154
+ const [viewMode, setViewMode] = usePersistedViewMode<ViewMode>({
155
+ storageKey: 'lms:certificate-models:view-mode',
156
+ defaultValue: 'cards',
157
+ allowedValues: ['cards', 'list'],
158
+ });
159
+
160
+ const { data: generalSettings } = useQuery<{
161
+ data: Array<{ slug: string; value: string }>;
162
+ }>({
163
+ queryKey: ['setting-group-general'],
164
+ queryFn: async () => {
165
+ const response = await request<{
166
+ data: Array<{ slug: string; value: string }>;
167
+ }>({
168
+ url: '/setting/group/general',
169
+ method: 'GET',
170
+ });
171
+ return response.data;
172
+ },
173
+ staleTime: 5 * 60 * 1000,
174
+ });
175
+
176
+ const pageSizeOptions = useMemo(() => {
177
+ const setting = generalSettings?.data?.find(
178
+ (s) => s.slug === 'pagination-page-sizes'
179
+ );
180
+ if (!setting?.value) return DEFAULT_PAGE_SIZES;
181
+ try {
182
+ const parsed = JSON.parse(setting.value) as string[];
183
+ const sizes = parsed
184
+ .map(Number)
185
+ .filter((n) => !isNaN(n) && n > 0)
186
+ .sort((a, b) => a - b);
187
+ return sizes.length > 0 ? sizes : DEFAULT_PAGE_SIZES;
188
+ } catch {
189
+ return DEFAULT_PAGE_SIZES;
190
+ }
191
+ }, [generalSettings]);
192
+
143
193
  const [pageSize, setPageSize] = usePersistedPageSize({
144
194
  storageKey: 'pagination:global:pageSize',
145
195
  defaultValue: 12,
146
- allowedValues: PAGE_SIZES,
196
+ allowedValues: pageSizeOptions,
147
197
  });
148
198
  const [isSheetOpen, setIsSheetOpen] = useState(false);
149
199
  const [isEditSheetOpen, setIsEditSheetOpen] = useState(false);
@@ -540,6 +590,14 @@ export default function ModelsPage() {
540
590
  ],
541
591
  },
542
592
  ]}
593
+ actions={
594
+ <ViewModeToggle
595
+ viewMode={viewMode}
596
+ onViewModeChange={setViewMode}
597
+ listLabel={t('viewMode.list')}
598
+ cardsLabel={t('viewMode.cards')}
599
+ />
600
+ }
543
601
  />
544
602
 
545
603
  <div className="flex items-center justify-between gap-3">
@@ -567,31 +625,67 @@ export default function ModelsPage() {
567
625
  </div>
568
626
 
569
627
  {loading ? (
570
- <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
571
- {Array.from({ length: 6 }).map((_, index) => (
572
- <Card
573
- key={index}
574
- className="overflow-hidden border-border/70 py-0"
575
- >
576
- <div className="h-1 w-full bg-linear-to-r from-slate-300/70 via-slate-200 to-transparent" />
577
- <CardContent className="space-y-4 p-5">
578
- <div className="flex items-center gap-2">
579
- <Skeleton className="h-6 w-24 rounded-full" />
580
- <Skeleton className="h-6 w-20 rounded-full" />
581
- </div>
582
- <div>
583
- <Skeleton className="mb-2 h-5 w-3/4" />
584
- <Skeleton className="h-4 w-1/2" />
585
- </div>
586
- <div className="grid grid-cols-2 gap-3">
587
- <Skeleton className="h-14 w-full rounded-xl" />
588
- <Skeleton className="h-14 w-full rounded-xl" />
589
- </div>
590
- <Skeleton className="h-9 w-36 rounded-md" />
591
- </CardContent>
592
- </Card>
593
- ))}
594
- </div>
628
+ viewMode === 'cards' ? (
629
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
630
+ {Array.from({ length: 6 }).map((_, index) => (
631
+ <Card
632
+ key={index}
633
+ className="overflow-hidden border-border/70 py-0"
634
+ >
635
+ <div className="h-1 w-full bg-linear-to-r from-slate-300/70 via-slate-200 to-transparent" />
636
+ <CardContent className="space-y-4 p-5">
637
+ <div className="flex items-center gap-2">
638
+ <Skeleton className="h-6 w-24 rounded-full" />
639
+ <Skeleton className="h-6 w-20 rounded-full" />
640
+ </div>
641
+ <div>
642
+ <Skeleton className="mb-2 h-5 w-3/4" />
643
+ <Skeleton className="h-4 w-1/2" />
644
+ </div>
645
+ <div className="grid grid-cols-2 gap-3">
646
+ <Skeleton className="h-14 w-full rounded-xl" />
647
+ <Skeleton className="h-14 w-full rounded-xl" />
648
+ </div>
649
+ <Skeleton className="h-9 w-36 rounded-md" />
650
+ </CardContent>
651
+ </Card>
652
+ ))}
653
+ </div>
654
+ ) : (
655
+ <div className="overflow-hidden rounded-xl border border-border/70">
656
+ <Table>
657
+ <TableHeader>
658
+ <TableRow>
659
+ <TableHead>{t('table.name')}</TableHead>
660
+ <TableHead>{t('table.status')}</TableHead>
661
+ <TableHead>{t('table.updatedAt')}</TableHead>
662
+ <TableHead className="w-12" />
663
+ </TableRow>
664
+ </TableHeader>
665
+ <TableBody>
666
+ {Array.from({ length: 6 }).map((_, i) => (
667
+ <TableRow key={i}>
668
+ <TableCell>
669
+ <div className="space-y-1.5">
670
+ <Skeleton className="h-4 w-48" />
671
+ <Skeleton className="h-3 w-32" />
672
+ </div>
673
+ </TableCell>
674
+ <TableCell>
675
+ <Skeleton className="h-5 w-20 rounded-full" />
676
+ </TableCell>
677
+ <TableCell>
678
+ <Skeleton className="h-4 w-24" />
679
+ </TableCell>
680
+ <TableCell>
681
+ <Skeleton className="ml-auto size-8 rounded-md" />
682
+ </TableCell>
683
+ </TableRow>
684
+ ))}
685
+ </TableBody>
686
+ </Table>
687
+ </div>
688
+ )
595
689
  ) : totalItems === 0 ? (
596
690
  <EmptyState
597
691
  icon={<Files className="size-12 text-muted-foreground/40" />}
@@ -602,7 +696,7 @@ export default function ModelsPage() {
602
696
  onAction={openCreateSheet}
603
697
  className="py-20"
604
698
  />
605
- ) : (
699
+ ) : viewMode === 'cards' ? (
606
700
  <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
607
701
  {templates.map((template) => (
608
702
  <Card
@@ -684,6 +778,65 @@ export default function ModelsPage() {
684
778
  </Card>
685
779
  ))}
686
780
  </div>
781
+ ) : (
782
+ <div className="overflow-hidden rounded-xl border border-border/70">
783
+ <Table>
784
+ <TableHeader>
785
+ <TableRow>
786
+ <TableHead>{t('table.name')}</TableHead>
787
+ <TableHead>{t('table.status')}</TableHead>
788
+ <TableHead>{t('table.updatedAt')}</TableHead>
789
+ <TableHead className="w-12" />
790
+ </TableRow>
791
+ </TableHeader>
792
+ <TableBody>
793
+ {templates.map((template) => (
794
+ <TableRow
795
+ key={template.id}
796
+ className="cursor-pointer"
797
+ onClick={() => handleCardClick(template)}
798
+ >
799
+ <TableCell>
800
+ <p className="font-semibold text-foreground">
801
+ {template.name}
802
+ </p>
803
+ <p className="font-mono text-xs text-muted-foreground">
804
+ {template.slug}
805
+ </p>
806
+ </TableCell>
807
+ <TableCell>{renderStatusBadge(template.status)}</TableCell>
808
+ <TableCell className="text-sm text-muted-foreground">
809
+ {formatDate(resolveUpdatedAt(template))}
810
+ </TableCell>
811
+ <TableCell onClick={(e) => e.stopPropagation()}>
812
+ <IconActionGroup
813
+ className="ml-auto"
814
+ actions={[
815
+ {
816
+ key: 'editor',
817
+ label: t('cards.actions.editTemplate'),
818
+ icon: <FileEdit className="size-4" />,
819
+ onClick: () =>
820
+ router.push(
821
+ `/lms/certificates/models/editor?templateId=${template.id}`
822
+ ),
823
+ },
824
+ {
825
+ key: 'delete',
826
+ label: t('cards.actions.deleteTemplate'),
827
+ icon: <Trash2 className="size-4" />,
828
+ onClick: () => onDeleteTemplate(template),
829
+ destructive: true,
830
+ disabled: deletingTemplateId === template.id,
831
+ },
832
+ ]}
833
+ />
834
+ </TableCell>
835
+ </TableRow>
836
+ ))}
837
+ </TableBody>
838
+ </Table>
839
+ </div>
687
840
  )}
688
841
 
689
842
  {!loading && totalItems > 0 ? (
@@ -696,7 +849,7 @@ export default function ModelsPage() {
696
849
  setPageSize(value);
697
850
  setCurrentPage(1);
698
851
  }}
699
- pageSizeOptions={PAGE_SIZES}
852
+ pageSizeOptions={pageSizeOptions}
700
853
  />
701
854
  ) : null}
702
855
  </div>