@hed-hog/lms 0.0.366 → 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 (169) 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/course/course-ai-usage.service.d.ts +58 -0
  10. package/dist/course/course-ai-usage.service.d.ts.map +1 -0
  11. package/dist/course/course-ai-usage.service.js +176 -0
  12. package/dist/course/course-ai-usage.service.js.map +1 -0
  13. package/dist/course/course-audio-transcription.service.d.ts +65 -1
  14. package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
  15. package/dist/course/course-audio-transcription.service.js +381 -29
  16. package/dist/course/course-audio-transcription.service.js.map +1 -1
  17. package/dist/course/course-export-scorm12.service.d.ts +3 -0
  18. package/dist/course/course-export-scorm12.service.d.ts.map +1 -1
  19. package/dist/course/course-export-scorm12.service.js +141 -6
  20. package/dist/course/course-export-scorm12.service.js.map +1 -1
  21. package/dist/course/course-export.service.d.ts.map +1 -1
  22. package/dist/course/course-export.service.js +2 -1
  23. package/dist/course/course-export.service.js.map +1 -1
  24. package/dist/course/course-lesson.controller.d.ts +25 -3
  25. package/dist/course/course-lesson.controller.d.ts.map +1 -1
  26. package/dist/course/course-lesson.controller.js +71 -8
  27. package/dist/course/course-lesson.controller.js.map +1 -1
  28. package/dist/course/course-structure.controller.d.ts +26 -5
  29. package/dist/course/course-structure.controller.d.ts.map +1 -1
  30. package/dist/course/course-structure.controller.js +31 -1
  31. package/dist/course/course-structure.controller.js.map +1 -1
  32. package/dist/course/course-structure.service.d.ts +37 -5
  33. package/dist/course/course-structure.service.d.ts.map +1 -1
  34. package/dist/course/course-structure.service.js +165 -20
  35. package/dist/course/course-structure.service.js.map +1 -1
  36. package/dist/course/course-transcription-translation.service.d.ts +31 -0
  37. package/dist/course/course-transcription-translation.service.d.ts.map +1 -0
  38. package/dist/course/course-transcription-translation.service.js +227 -0
  39. package/dist/course/course-transcription-translation.service.js.map +1 -0
  40. package/dist/course/course-video-agent-pipeline.service.js +7 -7
  41. package/dist/course/course-video-agent-pipeline.service.js.map +1 -1
  42. package/dist/course/course.module.d.ts.map +1 -1
  43. package/dist/course/course.module.js +4 -0
  44. package/dist/course/course.module.js.map +1 -1
  45. package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
  46. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
  47. package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
  48. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
  49. package/dist/course/dto/create-course-export.dto.d.ts +1 -0
  50. package/dist/course/dto/create-course-export.dto.d.ts.map +1 -1
  51. package/dist/course/dto/create-course-export.dto.js +6 -0
  52. package/dist/course/dto/create-course-export.dto.js.map +1 -1
  53. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
  54. package/dist/course/lms-bulk-upload-automation.service.js +26 -13
  55. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
  56. package/dist/course/lms-bulk-upload.controller.d.ts +3 -0
  57. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  58. package/dist/course/lms-bulk-upload.service.d.ts +3 -0
  59. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  60. package/dist/course/lms-bulk-upload.service.js +48 -29
  61. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  62. package/dist/course/subtitle.util.d.ts +46 -0
  63. package/dist/course/subtitle.util.d.ts.map +1 -0
  64. package/dist/course/subtitle.util.js +206 -0
  65. package/dist/course/subtitle.util.js.map +1 -0
  66. package/dist/enterprise/training/training-student.service.d.ts +27 -0
  67. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  68. package/dist/enterprise/training/training-student.service.js +197 -10
  69. package/dist/enterprise/training/training-student.service.js.map +1 -1
  70. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +3 -1
  71. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
  72. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +19 -5
  73. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
  74. package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -1
  75. package/dist/lesson-xp-map/lesson-xp-map.module.js +2 -1
  76. package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -1
  77. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
  78. package/dist/lms.module.d.ts.map +1 -1
  79. package/dist/lms.module.js +4 -0
  80. package/dist/lms.module.js.map +1 -1
  81. package/dist/platforma/platforma-performance.service.js +121 -121
  82. package/dist/platforma/platforma-video.service.d.ts +8 -0
  83. package/dist/platforma/platforma-video.service.d.ts.map +1 -1
  84. package/dist/platforma/platforma-video.service.js +45 -2
  85. package/dist/platforma/platforma-video.service.js.map +1 -1
  86. package/dist/platforma/platforma.controller.d.ts +99 -1
  87. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  88. package/dist/platforma/platforma.controller.js +111 -2
  89. package/dist/platforma/platforma.controller.js.map +1 -1
  90. package/dist/training/dto/create-training.dto.d.ts +9 -0
  91. package/dist/training/dto/create-training.dto.d.ts.map +1 -1
  92. package/dist/training/dto/create-training.dto.js +45 -1
  93. package/dist/training/dto/create-training.dto.js.map +1 -1
  94. package/dist/training/training.controller.d.ts +144 -0
  95. package/dist/training/training.controller.d.ts.map +1 -1
  96. package/dist/training/training.service.d.ts +149 -0
  97. package/dist/training/training.service.d.ts.map +1 -1
  98. package/dist/training/training.service.js +332 -167
  99. package/dist/training/training.service.js.map +1 -1
  100. package/hedhog/data/image_type.yaml +10 -0
  101. package/hedhog/data/route.yaml +251 -0
  102. package/hedhog/data/setting_group.yaml +97 -0
  103. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +139 -27
  104. package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +69 -57
  105. package/hedhog/frontend/app/courses/[id]/_components/CourseIssuedCertificatesCard.tsx.ejs +168 -0
  106. package/hedhog/frontend/app/courses/[id]/structure/_components/course-ai-costs-tab.tsx.ejs +191 -0
  107. package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +81 -1
  108. package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +12 -0
  109. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +69 -1
  110. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +267 -19
  111. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +114 -86
  112. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +239 -31
  113. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +344 -59
  114. package/hedhog/frontend/app/courses/[id]/structure/_components/lesson-video-preview.tsx.ejs +200 -0
  115. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +1 -0
  116. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +3 -0
  117. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +19 -7
  118. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -0
  119. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-ai-costs.ts.ejs +40 -0
  120. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +25 -0
  121. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +148 -0
  122. package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +157 -8
  123. package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +1 -22
  124. package/hedhog/frontend/app/courses/page.tsx.ejs +26 -4
  125. package/hedhog/frontend/app/paths/page.tsx.ejs +612 -164
  126. package/hedhog/frontend/messages/en.json +23 -12
  127. package/hedhog/frontend/messages/pt.json +23 -12
  128. package/hedhog/query/triggers.sql +33 -0
  129. package/hedhog/table/course_ai_usage.yaml +46 -0
  130. package/hedhog/table/course_lesson.yaml +3 -0
  131. package/hedhog/table/course_lesson_answer.yaml +37 -0
  132. package/hedhog/table/course_lesson_transcription_segment.yaml +8 -0
  133. package/hedhog/table/learning_path.yaml +6 -0
  134. package/hedhog/table/learning_path_module.yaml +22 -0
  135. package/hedhog/table/learning_path_step.yaml +9 -6
  136. package/hedhog/table/lesson_view_event.yaml +66 -66
  137. package/package.json +9 -9
  138. package/src/certificate/certificate.controller.ts +2 -0
  139. package/src/certificate/certificate.service.ts +99 -0
  140. package/src/course/course-ai-usage.service.ts +221 -0
  141. package/src/course/course-audio-transcription.service.ts +471 -43
  142. package/src/course/course-export-scorm12.service.ts +149 -5
  143. package/src/course/course-export.service.ts +1 -0
  144. package/src/course/course-lesson.controller.ts +59 -6
  145. package/src/course/course-structure.controller.ts +16 -0
  146. package/src/course/course-structure.service.ts +184 -10
  147. package/src/course/course-transcription-translation.service.ts +293 -0
  148. package/src/course/course-video-agent-pipeline.service.ts +471 -471
  149. package/src/course/course.module.ts +4 -0
  150. package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
  151. package/src/course/dto/create-course-export.dto.ts +6 -0
  152. package/src/course/ffmpeg.util.ts +65 -65
  153. package/src/course/lms-bulk-upload-automation.service.ts +29 -7
  154. package/src/course/lms-bulk-upload.service.ts +20 -1
  155. package/src/course/subtitle.util.ts +220 -0
  156. package/src/enterprise/training/training-student.service.ts +224 -4
  157. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +14 -0
  158. package/src/lesson-xp-map/lesson-xp-map.module.ts +2 -1
  159. package/src/lms.module.ts +4 -0
  160. package/src/platforma/dto/heartbeat.dto.ts +30 -30
  161. package/src/platforma/handlers/emit-certificate.handler.ts +117 -117
  162. package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -343
  163. package/src/platforma/platforma-heartbeat.service.ts +33 -33
  164. package/src/platforma/platforma-performance.service.ts +606 -606
  165. package/src/platforma/platforma-search.service.ts +48 -48
  166. package/src/platforma/platforma-video.service.ts +59 -3
  167. package/src/platforma/platforma.controller.ts +88 -0
  168. package/src/training/dto/create-training.dto.ts +36 -0
  169. 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>
@@ -17,10 +17,13 @@ import type { UseFormReturn } from 'react-hook-form';
17
17
  import { CourseSectionCard } from './CourseSectionCard';
18
18
  import type { CourseEditFormValues, TranslationFn } from './course-edit-types';
19
19
 
20
+ type CourseFlag = 'certificado' | 'destaque' | 'listado';
21
+
20
22
  type CourseFlagsCardProps = {
21
23
  form: UseFormReturn<CourseEditFormValues>;
22
24
  t: TranslationFn;
23
25
  compact?: boolean;
26
+ flags?: CourseFlag[];
24
27
  };
25
28
 
26
29
  function FlagRow({
@@ -53,10 +56,13 @@ function FlagRow({
53
56
  );
54
57
  }
55
58
 
59
+ const DEFAULT_FLAGS: CourseFlag[] = ['certificado', 'destaque', 'listado'];
60
+
56
61
  export function CourseFlagsCard({
57
62
  form,
58
63
  t,
59
64
  compact = false,
65
+ flags = DEFAULT_FLAGS,
60
66
  }: CourseFlagsCardProps) {
61
67
  return (
62
68
  <CourseSectionCard
@@ -66,65 +72,71 @@ export function CourseFlagsCard({
66
72
  compact={compact}
67
73
  >
68
74
  <div className="space-y-3">
69
- <FormField
70
- control={form.control}
71
- name="certificado"
72
- render={({ field }) => (
73
- <FormItem>
74
- <FormLabel className="sr-only">
75
- {t('form.flags.certificate.label')}
76
- </FormLabel>
77
- <FormControl>
78
- <FlagRow
79
- title={t('form.flags.certificate.label')}
80
- description={t('form.flags.certificate.description')}
81
- checked={field.value}
82
- onCheckedChange={field.onChange}
83
- />
84
- </FormControl>
85
- </FormItem>
86
- )}
87
- />
75
+ {flags.includes('certificado') && (
76
+ <FormField
77
+ control={form.control}
78
+ name="certificado"
79
+ render={({ field }) => (
80
+ <FormItem>
81
+ <FormLabel className="sr-only">
82
+ {t('form.flags.certificate.label')}
83
+ </FormLabel>
84
+ <FormControl>
85
+ <FlagRow
86
+ title={t('form.flags.certificate.label')}
87
+ description={t('form.flags.certificate.description')}
88
+ checked={field.value}
89
+ onCheckedChange={field.onChange}
90
+ />
91
+ </FormControl>
92
+ </FormItem>
93
+ )}
94
+ />
95
+ )}
88
96
 
89
- <FormField
90
- control={form.control}
91
- name="destaque"
92
- render={({ field }) => (
93
- <FormItem>
94
- <FormLabel className="sr-only">
95
- {t('form.flags.featured.label')}
96
- </FormLabel>
97
- <FormControl>
98
- <FlagRow
99
- title={t('form.flags.featured.label')}
100
- description={t('form.flags.featured.description')}
101
- checked={field.value}
102
- onCheckedChange={field.onChange}
103
- />
104
- </FormControl>
105
- </FormItem>
106
- )}
107
- />
97
+ {flags.includes('destaque') && (
98
+ <FormField
99
+ control={form.control}
100
+ name="destaque"
101
+ render={({ field }) => (
102
+ <FormItem>
103
+ <FormLabel className="sr-only">
104
+ {t('form.flags.featured.label')}
105
+ </FormLabel>
106
+ <FormControl>
107
+ <FlagRow
108
+ title={t('form.flags.featured.label')}
109
+ description={t('form.flags.featured.description')}
110
+ checked={field.value}
111
+ onCheckedChange={field.onChange}
112
+ />
113
+ </FormControl>
114
+ </FormItem>
115
+ )}
116
+ />
117
+ )}
108
118
 
109
- <FormField
110
- control={form.control}
111
- name="listado"
112
- render={({ field }) => (
113
- <FormItem>
114
- <FormLabel className="sr-only">
115
- {t('form.flags.listed.label')}
116
- </FormLabel>
117
- <FormControl>
118
- <FlagRow
119
- title={t('form.flags.listed.label')}
120
- description={t('form.flags.listed.description')}
121
- checked={field.value}
122
- onCheckedChange={field.onChange}
123
- />
124
- </FormControl>
125
- </FormItem>
126
- )}
127
- />
119
+ {flags.includes('listado') && (
120
+ <FormField
121
+ control={form.control}
122
+ name="listado"
123
+ render={({ field }) => (
124
+ <FormItem>
125
+ <FormLabel className="sr-only">
126
+ {t('form.flags.listed.label')}
127
+ </FormLabel>
128
+ <FormControl>
129
+ <FlagRow
130
+ title={t('form.flags.listed.label')}
131
+ description={t('form.flags.listed.description')}
132
+ checked={field.value}
133
+ onCheckedChange={field.onChange}
134
+ />
135
+ </FormControl>
136
+ </FormItem>
137
+ )}
138
+ />
139
+ )}
128
140
  </div>
129
141
  </CourseSectionCard>
130
142
  );
@@ -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
+ }