@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
@@ -59,13 +59,14 @@ import { getPhotoUrl } from '@/lib/get-photo-url';
59
59
  import { cn } from '@/lib/utils';
60
60
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
61
61
  import {
62
+ AlertCircle,
62
63
  Ban,
63
64
  CheckCircle2,
64
65
  Clock,
65
66
  Cog,
66
67
  ExternalLink,
67
68
  HardDriveUpload,
68
- Inbox,
69
+ Link2,
69
70
  Loader2,
70
71
  Pencil,
71
72
  Plus,
@@ -85,7 +86,8 @@ type UploadStatus =
85
86
  | 'received'
86
87
  | 'done'
87
88
  | 'error'
88
- | 'cancelled';
89
+ | 'cancelled'
90
+ | 'lesson_not_found';
89
91
 
90
92
  type UploadItemRow = {
91
93
  id: number;
@@ -188,7 +190,7 @@ type BulkUploadRegenerateTokenResponse = {
188
190
  webhook: WebhookIntegrationItem;
189
191
  };
190
192
 
191
- type BulkUploadCleanupStatus = 'done' | 'error' | 'cancelled';
193
+ type BulkUploadCleanupStatus = 'received' | 'done' | 'error' | 'cancelled';
192
194
  type BulkUploadCleanupTimeWindow =
193
195
  | 'last_hour'
194
196
  | 'last_day'
@@ -214,7 +216,8 @@ const CLEANUP_STATUS_OPTIONS: Array<{
214
216
  value: BulkUploadCleanupStatus;
215
217
  label: string;
216
218
  }> = [
217
- { value: 'done', label: 'Concluidos' },
219
+ { value: 'received', label: 'Concluidos (webhook confirmado)' },
220
+ { value: 'done', label: 'Enviados (aguardando confirmação)' },
218
221
  { value: 'error', label: 'Com falha' },
219
222
  { value: 'cancelled', label: 'Cancelados' },
220
223
  ];
@@ -270,21 +273,21 @@ function getStatusMeta(status: UploadStatus) {
270
273
  switch (status) {
271
274
  case 'received':
272
275
  return {
273
- label: 'Arquivo recebido',
276
+ label: 'Concluido',
274
277
  variant: 'default' as const,
275
- icon: Inbox,
278
+ icon: CheckCircle2,
276
279
  iconClass: 'text-green-600',
277
280
  animated: false,
278
281
  progressClass: 'bg-green-500',
279
282
  };
280
283
  case 'done':
281
284
  return {
282
- label: 'Concluido',
283
- variant: 'default' as const,
284
- icon: CheckCircle2,
285
- iconClass: 'text-green-600',
286
- animated: false,
287
- progressClass: 'bg-green-500',
285
+ label: 'Aguardando confirmação',
286
+ variant: 'secondary' as const,
287
+ icon: HardDriveUpload,
288
+ iconClass: 'text-blue-500 animate-pulse',
289
+ animated: true,
290
+ progressClass: 'bg-blue-400',
288
291
  };
289
292
  case 'uploading':
290
293
  return {
@@ -322,6 +325,15 @@ function getStatusMeta(status: UploadStatus) {
322
325
  animated: false,
323
326
  progressClass: 'bg-muted-foreground/40',
324
327
  };
328
+ case 'lesson_not_found':
329
+ return {
330
+ label: 'Aula não encontrada',
331
+ variant: 'outline' as const,
332
+ icon: AlertCircle,
333
+ iconClass: 'text-amber-500',
334
+ animated: false,
335
+ progressClass: 'bg-amber-400',
336
+ };
325
337
  default:
326
338
  return {
327
339
  label: 'Erro',
@@ -342,7 +354,8 @@ export default function LmsBulkUploadSessionsPage() {
342
354
  const [statusFilter, setStatusFilter] = useState('all');
343
355
  const [settingsOpen, setSettingsOpen] = useState(false);
344
356
  const [bucketName, setBucketName] = useState('');
345
- const [lambdaRoleArn, setLambdaRoleArn] = useState('');
357
+ const [awsAccountId, setAwsAccountId] = useState('');
358
+ const [lambdaRoleName, setLambdaRoleName] = useState('');
346
359
  const [storageProfileId, setStorageProfileId] = useState<number | null>(null);
347
360
  const [webhookPlainToken, setWebhookPlainToken] = useState<string | null>(
348
361
  null
@@ -361,10 +374,26 @@ export default function LmsBulkUploadSessionsPage() {
361
374
  const [isCleanupDialogOpen, setIsCleanupDialogOpen] = useState(false);
362
375
  const [cleanupStatuses, setCleanupStatuses] = useState<
363
376
  Set<BulkUploadCleanupStatus>
364
- >(new Set(['done']));
377
+ >(new Set(['received']));
365
378
  const [cleanupTimeWindow, setCleanupTimeWindow] =
366
379
  useState<BulkUploadCleanupTimeWindow>('last_day');
367
380
  const [isCleaningHistory, setIsCleaningHistory] = useState(false);
381
+ const [manualLinkOpen, setManualLinkOpen] = useState(false);
382
+ const [manualLinkItemId, setManualLinkItemId] = useState<number | null>(null);
383
+ const [manualLinkVideoUrl, setManualLinkVideoUrl] = useState<string | null>(null);
384
+ const [manualLinkVideoLoading, setManualLinkVideoLoading] = useState(false);
385
+ const [manualLinkCourseId, setManualLinkCourseId] = useState<number | null>(null);
386
+ const [manualLinkSessionId, setManualLinkSessionId] = useState<number | null>(null);
387
+ const [manualLinkLessonId, setManualLinkLessonId] = useState<number | null>(null);
388
+ const [manualLinkStructure, setManualLinkStructure] = useState<{
389
+ sessions: Array<{
390
+ id: number;
391
+ title: string;
392
+ lessons: Array<{ id: number; title: string }>;
393
+ }>;
394
+ } | null>(null);
395
+ const [manualLinkStructureLoading, setManualLinkStructureLoading] = useState(false);
396
+ const [isLinkingLesson, setIsLinkingLesson] = useState(false);
368
397
  const [pageSize, setPageSize] = usePersistedPageSize({
369
398
  storageKey: 'pagination:global:pageSize',
370
399
  defaultValue: PAGE_SIZE_OPTIONS[1],
@@ -417,6 +446,8 @@ export default function LmsBulkUploadSessionsPage() {
417
446
  },
418
447
  });
419
448
 
449
+ const ACTIVE_STATUSES: UploadStatus[] = ['queued', 'uploading', 'cancelling', 'done'];
450
+
420
451
  const {
421
452
  data,
422
453
  isLoading,
@@ -442,6 +473,23 @@ export default function LmsBulkUploadSessionsPage() {
442
473
  });
443
474
  return response.data;
444
475
  },
476
+ refetchInterval: (query) => {
477
+ const currentRows = query.state.data?.data ?? [];
478
+ const hasActive = currentRows.some((row) => ACTIVE_STATUSES.includes(row.status));
479
+ return hasActive ? 10_000 : false;
480
+ },
481
+ });
482
+
483
+ const { data: coursesResult } = useQuery<{ data: Array<{ id: number; title: string; code: string | null }> }>({
484
+ queryKey: ['lms-bulk-upload-courses-picker', manualLinkOpen],
485
+ enabled: manualLinkOpen,
486
+ queryFn: async () => {
487
+ const response = await request<{ data: Array<{ id: number; title: string; code: string | null }> }>({
488
+ url: '/course?pageSize=500',
489
+ method: 'GET',
490
+ });
491
+ return response.data;
492
+ },
445
493
  });
446
494
 
447
495
  const rows = useMemo(() => data?.data ?? [], [data?.data]);
@@ -449,7 +497,7 @@ export default function LmsBulkUploadSessionsPage() {
449
497
  const stats = useMemo(
450
498
  () => ({
451
499
  sending: rows.filter((row) => row.status === 'uploading').length,
452
- done: rows.filter((row) => row.status === 'done').length,
500
+ done: rows.filter((row) => row.status === 'received').length,
453
501
  error: rows.filter((row) => row.status === 'error').length,
454
502
  }),
455
503
  [rows]
@@ -513,7 +561,16 @@ export default function LmsBulkUploadSessionsPage() {
513
561
  [webhookPreview, webhooksResult?.data]
514
562
  );
515
563
 
516
- const hasBaseConfig = Boolean(storageProfileId && lambdaRoleArn.trim());
564
+ const lambdaRoleArn =
565
+ awsAccountId && lambdaRoleName
566
+ ? `arn:aws:iam::${awsAccountId}:role/${lambdaRoleName}`
567
+ : '';
568
+ const isAccountIdValid = !awsAccountId || /^\d{12}$/.test(awsAccountId);
569
+ const isRoleNameValid =
570
+ !lambdaRoleName || /^[a-zA-Z_0-9+=,.@\-_/]+$/.test(lambdaRoleName);
571
+ const hasBaseConfig = Boolean(
572
+ storageProfileId && awsAccountId && lambdaRoleName && isAccountIdValid && isRoleNameValid
573
+ );
517
574
  const isWebhookActive = Boolean(
518
575
  webhookIntegration?.status === 'active' && webhookIntegration?.public_url
519
576
  );
@@ -540,7 +597,10 @@ export default function LmsBulkUploadSessionsPage() {
540
597
 
541
598
  useEffect(() => {
542
599
  setBucketName(String(settingsResult?.bucketName ?? '').trim());
543
- setLambdaRoleArn(String(settingsResult?.lambdaRoleArn ?? '').trim());
600
+ const arn = String(settingsResult?.lambdaRoleArn ?? '').trim();
601
+ const arnMatch = arn.match(/^arn:[^:]*:iam::(\d{12}):role\/(.+)$/);
602
+ setAwsAccountId(arnMatch?.[1] ?? '');
603
+ setLambdaRoleName(arnMatch?.[2] ?? '');
544
604
  const parsedProfileId = Number(settingsResult?.storageProfileId ?? 0);
545
605
  setStorageProfileId(
546
606
  Number.isFinite(parsedProfileId) && parsedProfileId > 0
@@ -594,15 +654,81 @@ export default function LmsBulkUploadSessionsPage() {
594
654
  refetchProfiles(),
595
655
  refetchWebhooks(),
596
656
  ]);
597
- } catch {
598
- toast.error(
599
- 'Falha ao validar bucket/credenciais temporárias ou salvar a configuração.'
600
- );
657
+ } catch (error: unknown) {
658
+ const message =
659
+ (error as { response?: { data?: { message?: string } } })?.response?.data
660
+ ?.message ?? null;
661
+ toast.error('Falha ao salvar a configuração.', {
662
+ description: message || 'Verifique o ARN da role e as credenciais do perfil de storage.',
663
+ });
601
664
  } finally {
602
665
  setIsSavingSettings(false);
603
666
  }
604
667
  };
605
668
 
669
+ const openManualLink = async (itemId: number) => {
670
+ setManualLinkItemId(itemId);
671
+ setManualLinkCourseId(null);
672
+ setManualLinkSessionId(null);
673
+ setManualLinkLessonId(null);
674
+ setManualLinkStructure(null);
675
+ setManualLinkVideoUrl(null);
676
+ setManualLinkOpen(true);
677
+ setManualLinkVideoLoading(true);
678
+ try {
679
+ const res = await request<{ url: string }>({
680
+ url: `/lms/bulk-upload/item/${itemId}/video-url`,
681
+ method: 'GET',
682
+ });
683
+ setManualLinkVideoUrl(res.data.url);
684
+ } catch {
685
+ // vídeo indisponível, mas o sheet ainda abre
686
+ } finally {
687
+ setManualLinkVideoLoading(false);
688
+ }
689
+ };
690
+
691
+ const loadCourseStructure = async (courseId: number) => {
692
+ setManualLinkStructureLoading(true);
693
+ setManualLinkSessionId(null);
694
+ setManualLinkLessonId(null);
695
+ try {
696
+ const res = await request<{ sessions: Array<{ id: number; title: string; lessons: Array<{ id: number; title: string }> }> }>({
697
+ url: `/course/${courseId}/structure`,
698
+ method: 'GET',
699
+ });
700
+ setManualLinkStructure({ sessions: res.data.sessions ?? [] });
701
+ } catch {
702
+ setManualLinkStructure(null);
703
+ } finally {
704
+ setManualLinkStructureLoading(false);
705
+ }
706
+ };
707
+
708
+ const confirmManualLink = async () => {
709
+ if (!manualLinkItemId || !manualLinkCourseId || !manualLinkSessionId || !manualLinkLessonId) return;
710
+ try {
711
+ setIsLinkingLesson(true);
712
+ await request({
713
+ url: `/lms/bulk-upload/item/${manualLinkItemId}/link-lesson`,
714
+ method: 'POST',
715
+ data: {
716
+ courseId: manualLinkCourseId,
717
+ sessionId: manualLinkSessionId,
718
+ lessonId: manualLinkLessonId,
719
+ },
720
+ });
721
+ toast.success('Aula vinculada! O processamento do vídeo foi iniciado.');
722
+ setManualLinkOpen(false);
723
+ await refetchSessions();
724
+ } catch (error: unknown) {
725
+ const message = (error as { response?: { data?: { message?: string } } })?.response?.data?.message ?? null;
726
+ toast.error('Falha ao vincular aula.', { description: message ?? undefined });
727
+ } finally {
728
+ setIsLinkingLesson(false);
729
+ }
730
+ };
731
+
606
732
  const regenerateWebhookToken = async () => {
607
733
  if (!webhookIntegration) return;
608
734
  try {
@@ -838,28 +964,71 @@ export default function LmsBulkUploadSessionsPage() {
838
964
  </p>
839
965
  <p className="mt-1 text-xs text-amber-800/90 dark:text-amber-300/90">
840
966
  Essa role será usada para criar a Lambda automaticamente
841
- quando necessário. Informe o ARN completo da role em{' '}
842
- <span className="font-mono">
843
- {BULK_UPLOAD_LAMBDA_ROLE_ARN_SLUG}
844
- </span>{' '}
845
- antes de salvar a configuração.
967
+ quando necessário. Informe o número da conta AWS e o nome
968
+ da role IAM de execução da Lambda antes de salvar a
969
+ configuração.
846
970
  </p>
847
971
  </div>
848
972
 
849
973
  <div className="space-y-2">
850
- <Label htmlFor="bulk-upload-lambda-role-arn">
851
- ARN da role da Lambda
852
- </Label>
853
- <Input
854
- id="bulk-upload-lambda-role-arn"
855
- placeholder="arn:aws:iam::123456789012:role/lms-bulk-upload-lambda-role"
856
- value={lambdaRoleArn}
857
- onChange={(event) => setLambdaRoleArn(event.target.value)}
858
- disabled={settingsLoading || isSavingSettings}
859
- />
860
- <p className="text-xs text-muted-foreground">
861
- Setting: {BULK_UPLOAD_LAMBDA_ROLE_ARN_SLUG}
862
- </p>
974
+ <div className="flex gap-2">
975
+ <div className="flex-none space-y-1">
976
+ <Label htmlFor="bulk-upload-aws-account-id">
977
+ Conta AWS
978
+ </Label>
979
+ <Input
980
+ id="bulk-upload-aws-account-id"
981
+ placeholder="123456789012"
982
+ maxLength={12}
983
+ value={awsAccountId}
984
+ onChange={(event) =>
985
+ setAwsAccountId(event.target.value.replace(/\D/g, '').slice(0, 12))
986
+ }
987
+ disabled={settingsLoading || isSavingSettings}
988
+ className={
989
+ awsAccountId && !isAccountIdValid
990
+ ? 'border-destructive focus-visible:ring-destructive'
991
+ : ''
992
+ }
993
+ />
994
+ {awsAccountId && !isAccountIdValid && (
995
+ <p className="text-xs text-destructive">
996
+ Deve ter 12 dígitos.
997
+ </p>
998
+ )}
999
+ </div>
1000
+ <div className="flex-1 space-y-1">
1001
+ <Label htmlFor="bulk-upload-lambda-role-name">
1002
+ Nome da role IAM
1003
+ </Label>
1004
+ <Input
1005
+ id="bulk-upload-lambda-role-name"
1006
+ placeholder="lms-bulk-upload-lambda-role"
1007
+ value={lambdaRoleName}
1008
+ onChange={(event) => setLambdaRoleName(event.target.value)}
1009
+ disabled={settingsLoading || isSavingSettings}
1010
+ className={
1011
+ lambdaRoleName && !isRoleNameValid
1012
+ ? 'border-destructive focus-visible:ring-destructive'
1013
+ : ''
1014
+ }
1015
+ />
1016
+ {lambdaRoleName && !isRoleNameValid && (
1017
+ <p className="text-xs text-destructive">
1018
+ Nome de role inválido.
1019
+ </p>
1020
+ )}
1021
+ </div>
1022
+ </div>
1023
+ {lambdaRoleArn ? (
1024
+ <p className="font-mono text-xs text-muted-foreground">
1025
+ {lambdaRoleArn}
1026
+ </p>
1027
+ ) : (
1028
+ <p className="text-xs text-muted-foreground">
1029
+ Setting: {BULK_UPLOAD_LAMBDA_ROLE_ARN_SLUG}
1030
+ </p>
1031
+ )}
863
1032
  </div>
864
1033
 
865
1034
  <Label>Perfil de integração de storage</Label>
@@ -1240,8 +1409,8 @@ export default function LmsBulkUploadSessionsPage() {
1240
1409
  { value: 'queued', label: 'Na fila' },
1241
1410
  { value: 'uploading', label: 'Enviando' },
1242
1411
  { value: 'cancelling', label: 'Cancelando' },
1243
- { value: 'received', label: 'Arquivo recebido' },
1244
- { value: 'done', label: 'Concluido' },
1412
+ { value: 'done', label: 'Aguardando confirmação' },
1413
+ { value: 'received', label: 'Concluido' },
1245
1414
  { value: 'cancelled', label: 'Cancelado' },
1246
1415
  { value: 'error', label: 'Erro' },
1247
1416
  ],
@@ -1384,6 +1553,16 @@ export default function LmsBulkUploadSessionsPage() {
1384
1553
  </p>
1385
1554
  </div>
1386
1555
  </button>
1556
+ ) : row.status === 'lesson_not_found' ? (
1557
+ <Button
1558
+ variant="outline"
1559
+ size="sm"
1560
+ className="gap-1.5 text-xs"
1561
+ onClick={() => openManualLink(row.id)}
1562
+ >
1563
+ <Link2 className="h-3.5 w-3.5" />
1564
+ Vincular
1565
+ </Button>
1387
1566
  ) : (
1388
1567
  <span className="text-sm text-muted-foreground">-</span>
1389
1568
  )}
@@ -1448,6 +1627,123 @@ export default function LmsBulkUploadSessionsPage() {
1448
1627
  }}
1449
1628
  pageSizeOptions={PAGE_SIZE_OPTIONS}
1450
1629
  />
1630
+
1631
+ <Sheet open={manualLinkOpen} onOpenChange={setManualLinkOpen}>
1632
+ <ResizableSheetContent
1633
+ sheetId="lms-bulk-upload-manual-link-sheet"
1634
+ defaultWidth={560}
1635
+ minWidth={400}
1636
+ className="flex flex-col gap-0 overflow-hidden"
1637
+ >
1638
+ <SheetHeader className="border-b px-6 py-4">
1639
+ <SheetTitle className="flex items-center gap-2">
1640
+ <Link2 className="h-4 w-4" />
1641
+ Vincular aula manualmente
1642
+ </SheetTitle>
1643
+ <SheetDescription>
1644
+ Selecione o curso, módulo e aula para associar este vídeo e iniciar o processamento.
1645
+ </SheetDescription>
1646
+ </SheetHeader>
1647
+
1648
+ <ScrollArea className="flex-1">
1649
+ <div className="space-y-6 p-6">
1650
+ <div className="overflow-hidden rounded-xl border border-border/60 bg-black">
1651
+ {manualLinkVideoLoading ? (
1652
+ <div className="flex h-40 items-center justify-center">
1653
+ <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
1654
+ </div>
1655
+ ) : manualLinkVideoUrl ? (
1656
+ <video
1657
+ src={manualLinkVideoUrl}
1658
+ controls
1659
+ className="w-full"
1660
+ style={{ maxHeight: 280 }}
1661
+ />
1662
+ ) : (
1663
+ <div className="flex h-40 items-center justify-center text-sm text-muted-foreground">
1664
+ Pré-visualização indisponível
1665
+ </div>
1666
+ )}
1667
+ </div>
1668
+
1669
+ <div className="space-y-4">
1670
+ <div className="space-y-2">
1671
+ <Label>Curso</Label>
1672
+ <EntityPicker<{ id: number; title: string; code: string | null }>
1673
+ placeholder="Selecione um curso"
1674
+ options={coursesResult?.data ?? []}
1675
+ value={manualLinkCourseId}
1676
+ valueType="number"
1677
+ getOptionValue={(o) => o.id}
1678
+ getOptionLabel={(o) => o.title}
1679
+ getOptionDescription={(o) => o.code ?? ''}
1680
+ onChange={(value) => {
1681
+ setManualLinkCourseId(value as number | null);
1682
+ if (value) loadCourseStructure(value as number);
1683
+ }}
1684
+ />
1685
+ </div>
1686
+
1687
+ <div className="space-y-2">
1688
+ <Label>Módulo / Sessão</Label>
1689
+ <EntityPicker<{ id: number; title: string }>
1690
+ placeholder={
1691
+ !manualLinkCourseId
1692
+ ? 'Selecione um curso primeiro'
1693
+ : manualLinkStructureLoading
1694
+ ? 'Carregando...'
1695
+ : 'Selecione um módulo'
1696
+ }
1697
+ disabled={!manualLinkCourseId || manualLinkStructureLoading}
1698
+ options={manualLinkStructure?.sessions ?? []}
1699
+ value={manualLinkSessionId}
1700
+ valueType="number"
1701
+ getOptionValue={(o) => o.id}
1702
+ getOptionLabel={(o) => o.title}
1703
+ onChange={(value) => {
1704
+ setManualLinkSessionId(value as number | null);
1705
+ setManualLinkLessonId(null);
1706
+ }}
1707
+ />
1708
+ </div>
1709
+
1710
+ <div className="space-y-2">
1711
+ <Label>Aula</Label>
1712
+ <EntityPicker<{ id: number; title: string }>
1713
+ placeholder={
1714
+ !manualLinkSessionId ? 'Selecione um módulo primeiro' : 'Selecione uma aula'
1715
+ }
1716
+ disabled={!manualLinkSessionId}
1717
+ options={
1718
+ manualLinkStructure?.sessions.find((s) => s.id === manualLinkSessionId)?.lessons ?? []
1719
+ }
1720
+ value={manualLinkLessonId}
1721
+ valueType="number"
1722
+ getOptionValue={(o) => o.id}
1723
+ getOptionLabel={(o) => o.title}
1724
+ onChange={(value) => setManualLinkLessonId(value as number | null)}
1725
+ />
1726
+ </div>
1727
+ </div>
1728
+ </div>
1729
+ </ScrollArea>
1730
+
1731
+ <div className="border-t px-6 py-4">
1732
+ <Button
1733
+ className="w-full gap-2"
1734
+ disabled={!manualLinkCourseId || !manualLinkSessionId || !manualLinkLessonId || isLinkingLesson}
1735
+ onClick={confirmManualLink}
1736
+ >
1737
+ {isLinkingLesson ? (
1738
+ <Loader2 className="h-4 w-4 animate-spin" />
1739
+ ) : (
1740
+ <Link2 className="h-4 w-4" />
1741
+ )}
1742
+ Confirmar vínculo e iniciar processamento
1743
+ </Button>
1744
+ </div>
1745
+ </ResizableSheetContent>
1746
+ </Sheet>
1451
1747
  </Page>
1452
1748
  );
1453
1749
  }
@@ -9,14 +9,16 @@ import {
9
9
  ResizablePanel,
10
10
  ResizablePanelGroup,
11
11
  } from '@/components/ui/resizable';
12
- import { Sheet, SheetHeader, SheetTitle } from '@/components/ui/sheet';
13
12
  import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
13
+ import { Sheet, SheetHeader, SheetTitle } from '@/components/ui/sheet';
14
14
  import { useIsMobile } from '@/hooks/use-mobile';
15
15
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
16
16
  import { useIsMutating, useQueryClient } from '@tanstack/react-query';
17
17
  import { AlertCircle, Menu, RefreshCw } from 'lucide-react';
18
18
  import { useTranslations } from 'next-intl';
19
+ import { useRouter, useSearchParams } from 'next/navigation';
19
20
 
21
+ import { useLmsRealtimeRefresh } from '../../_lib/hooks/use-lms-realtime-refresh';
20
22
  import { ConfirmDialog } from './structure/_components/confirm-dialog';
21
23
  import { CourseTreePanel } from './structure/_components/course-tree-panel';
22
24
  import { CourseTreeSkeleton } from './structure/_components/course-tree-skeleton';
@@ -37,7 +39,6 @@ import {
37
39
  usePasteSessionsMutation,
38
40
  } from './structure/_data/use-course-structure-mutations';
39
41
  import { useCourseStructureQuery } from './structure/_data/use-course-structure-query';
40
- import { useLmsRealtimeRefresh } from '../../_lib/hooks/use-lms-realtime-refresh';
41
42
 
42
43
  interface Props {
43
44
  params: Promise<{ id: string }>;
@@ -60,6 +61,8 @@ export default function CourseStructurePage({ params }: Props) {
60
61
  const isMobile = useIsMobile();
61
62
  const { request } = useApp();
62
63
  const queryClient = useQueryClient();
64
+ const router = useRouter();
65
+ const searchParams = useSearchParams();
63
66
 
64
67
  const { data: courseSummary } = useQuery<ApiCourseSummary>({
65
68
  queryKey: ['lms-course-detail', id],
@@ -85,6 +88,10 @@ export default function CourseStructurePage({ params }: Props) {
85
88
 
86
89
  const setStructureFromApi = useStructureStore((s) => s.setStructureFromApi);
87
90
  const setCourseId = useStructureStore((s) => s.setCourseId);
91
+ const selectItem = useStructureStore((s) => s.selectItem);
92
+
93
+ const restoredFromUrl = useRef(false);
94
+ const suppressNextSyncRef = useRef(false);
88
95
 
89
96
  useEffect(() => {
90
97
  setCourseId(id);
@@ -126,6 +133,31 @@ export default function CourseStructurePage({ params }: Props) {
126
133
  sessions: apiSessions,
127
134
  lessons: apiLessons,
128
135
  });
136
+
137
+ if (!restoredFromUrl.current) {
138
+ restoredFromUrl.current = true;
139
+ const sel = searchParams.get('sel');
140
+ const type = searchParams.get('type') as
141
+ | 'course'
142
+ | 'session'
143
+ | 'lesson'
144
+ | null;
145
+ if (
146
+ sel &&
147
+ (type === 'course' || type === 'session' || type === 'lesson')
148
+ ) {
149
+ const exists =
150
+ type === 'course'
151
+ ? mergedCourse?.id === sel
152
+ : type === 'session'
153
+ ? apiSessions.some((s) => s.id === sel)
154
+ : apiLessons.some((l) => l.id === sel);
155
+ if (exists) {
156
+ suppressNextSyncRef.current = true;
157
+ selectItem(sel, type);
158
+ }
159
+ }
160
+ }
129
161
  }
130
162
  }, [
131
163
  apiCourse,
@@ -159,6 +191,33 @@ export default function CourseStructurePage({ params }: Props) {
159
191
  const copiedType = useStructureStore((s) => s.copiedType);
160
192
  const copiedIds = useStructureStore((s) => s.copiedIds);
161
193
 
194
+ // ── URL sync: selection → URL ──────────────────────────────────────────────
195
+ useEffect(() => {
196
+ if (!restoredFromUrl.current) return;
197
+ if (suppressNextSyncRef.current) {
198
+ suppressNextSyncRef.current = false;
199
+ return;
200
+ }
201
+ const params = new URLSearchParams(searchParams.toString());
202
+ if (activeItemId && activeItemType) {
203
+ params.set('sel', activeItemId);
204
+ params.set('type', activeItemType);
205
+ }
206
+ params.delete('tab');
207
+ router.replace(`?${params.toString()}`, { scroll: false });
208
+ }, [activeItemId, activeItemType]); // eslint-disable-line react-hooks/exhaustive-deps
209
+
210
+ const handleTabChange = useCallback(
211
+ (tab: string) => {
212
+ const params = new URLSearchParams(searchParams.toString());
213
+ params.set('tab', tab);
214
+ router.replace(`?${params.toString()}`, { scroll: false });
215
+ },
216
+ [router, searchParams]
217
+ );
218
+
219
+ const defaultTab = searchParams.get('tab') ?? undefined;
220
+
162
221
  useCourseStructureShortcuts({
163
222
  searchRef,
164
223
  detailPanelRef,
@@ -277,7 +336,10 @@ export default function CourseStructurePage({ params }: Props) {
277
336
  className="flex flex-col h-full min-h-0"
278
337
  >
279
338
  <div className="h-full min-h-0">
280
- <DetailPanel />
339
+ <DetailPanel
340
+ defaultTab={defaultTab}
341
+ onTabChange={handleTabChange}
342
+ />
281
343
  </div>
282
344
  </div>
283
345
  </ResizablePanel>
@@ -305,7 +367,10 @@ export default function CourseStructurePage({ params }: Props) {
305
367
  className="flex-1 min-h-0 border rounded-lg overflow-hidden"
306
368
  >
307
369
  <div className="h-full min-h-0">
308
- <DetailPanel />
370
+ <DetailPanel
371
+ defaultTab={defaultTab}
372
+ onTabChange={handleTabChange}
373
+ />
309
374
  </div>
310
375
  </div>
311
376