@hed-hog/lms 0.0.355 → 0.0.358

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 (115) hide show
  1. package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
  2. package/dist/course/course-audio-transcription.service.js +15 -7
  3. package/dist/course/course-audio-transcription.service.js.map +1 -1
  4. package/dist/course/course-operations-integration.service.d.ts +31 -0
  5. package/dist/course/course-operations-integration.service.d.ts.map +1 -1
  6. package/dist/course/course-operations-integration.service.js +286 -22
  7. package/dist/course/course-operations-integration.service.js.map +1 -1
  8. package/dist/course/course-operations.controller.d.ts +10 -0
  9. package/dist/course/course-operations.controller.d.ts.map +1 -0
  10. package/dist/course/course-operations.controller.js +67 -0
  11. package/dist/course/course-operations.controller.js.map +1 -0
  12. package/dist/course/course-structure.controller.d.ts +15 -1
  13. package/dist/course/course-structure.controller.d.ts.map +1 -1
  14. package/dist/course/course-structure.service.d.ts +25 -1
  15. package/dist/course/course-structure.service.d.ts.map +1 -1
  16. package/dist/course/course-structure.service.js +160 -24
  17. package/dist/course/course-structure.service.js.map +1 -1
  18. package/dist/course/course.module.d.ts.map +1 -1
  19. package/dist/course/course.module.js +4 -1
  20. package/dist/course/course.module.js.map +1 -1
  21. package/dist/course/course.service.d.ts +4 -2
  22. package/dist/course/course.service.d.ts.map +1 -1
  23. package/dist/course/course.service.js +61 -2
  24. package/dist/course/course.service.js.map +1 -1
  25. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +3 -0
  26. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  27. package/dist/course/dto/create-course-structure-lesson.dto.js +15 -0
  28. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  29. package/dist/course/dto/create-course-structure-session.dto.d.ts +1 -0
  30. package/dist/course/dto/create-course-structure-session.dto.d.ts.map +1 -1
  31. package/dist/course/dto/create-course-structure-session.dto.js +5 -0
  32. package/dist/course/dto/create-course-structure-session.dto.js.map +1 -1
  33. package/dist/course/dto/update-course-operations-config.dto.d.ts +6 -0
  34. package/dist/course/dto/update-course-operations-config.dto.d.ts.map +1 -0
  35. package/dist/course/dto/update-course-operations-config.dto.js +33 -0
  36. package/dist/course/dto/update-course-operations-config.dto.js.map +1 -0
  37. package/dist/course/lms-operations-task.subscriber.d.ts +13 -0
  38. package/dist/course/lms-operations-task.subscriber.d.ts.map +1 -0
  39. package/dist/course/lms-operations-task.subscriber.js +57 -0
  40. package/dist/course/lms-operations-task.subscriber.js.map +1 -0
  41. package/dist/enterprise/enterprise.service.js +1 -1
  42. package/dist/enterprise/enterprise.service.js.map +1 -1
  43. package/dist/enterprise/training/training-student.controller.d.ts +0 -95
  44. package/dist/enterprise/training/training-student.controller.d.ts.map +1 -1
  45. package/dist/enterprise/training/training-student.controller.js +1 -34
  46. package/dist/enterprise/training/training-student.controller.js.map +1 -1
  47. package/dist/enterprise/training/training-student.service.d.ts +63 -0
  48. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  49. package/dist/enterprise/training/training-student.service.js +320 -4
  50. package/dist/enterprise/training/training-student.service.js.map +1 -1
  51. package/dist/instructor/instructor.service.d.ts.map +1 -1
  52. package/dist/instructor/instructor.service.js +12 -3
  53. package/dist/instructor/instructor.service.js.map +1 -1
  54. package/dist/lms.module.d.ts.map +1 -1
  55. package/dist/lms.module.js +2 -0
  56. package/dist/lms.module.js.map +1 -1
  57. package/dist/platforma/platforma.controller.d.ts +287 -0
  58. package/dist/platforma/platforma.controller.d.ts.map +1 -0
  59. package/dist/platforma/platforma.controller.js +147 -0
  60. package/dist/platforma/platforma.controller.js.map +1 -0
  61. package/hedhog/data/route.yaml +102 -9
  62. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -2
  63. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +10 -13
  64. package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +26 -4
  65. package/hedhog/frontend/app/_lib/editor/types.ts.ejs +6 -0
  66. package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +447 -31
  67. package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +59 -47
  68. package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +201 -35
  69. package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +36 -5
  70. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +2 -2
  71. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
  72. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +17 -6
  73. package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +382 -0
  74. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +45 -8
  75. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +177 -67
  76. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +25 -60
  77. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -0
  78. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +11 -0
  79. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +22 -0
  80. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +79 -64
  81. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +31 -14
  82. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +4 -4
  83. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +39 -27
  84. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +24 -2
  85. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +41 -6
  86. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +2 -2
  87. package/hedhog/frontend/app/courses/page.tsx.ejs +80 -103
  88. package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +1 -1
  89. package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +2 -2
  90. package/hedhog/frontend/app/enterprise/page.tsx.ejs +18 -6
  91. package/hedhog/frontend/app/exams/[id]/page.tsx.ejs +5 -4
  92. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +16 -10
  93. package/hedhog/frontend/app/paths/page.tsx.ejs +1 -1
  94. package/hedhog/frontend/app/training/page.tsx.ejs +1 -1
  95. package/hedhog/frontend/messages/en.json +7 -2
  96. package/hedhog/frontend/messages/pt.json +7 -2
  97. package/hedhog/table/course_lesson.yaml +2 -2
  98. package/hedhog/table/course_module.yaml +3 -0
  99. package/package.json +8 -8
  100. package/src/course/course-audio-transcription.service.ts +21 -8
  101. package/src/course/course-operations-integration.service.ts +460 -22
  102. package/src/course/course-operations.controller.ts +45 -0
  103. package/src/course/course-structure.service.ts +209 -4
  104. package/src/course/course.module.ts +4 -1
  105. package/src/course/course.service.ts +67 -1
  106. package/src/course/dto/create-course-structure-lesson.dto.ts +17 -0
  107. package/src/course/dto/create-course-structure-session.dto.ts +13 -1
  108. package/src/course/dto/update-course-operations-config.dto.ts +16 -0
  109. package/src/course/lms-operations-task.subscriber.ts +44 -0
  110. package/src/enterprise/enterprise.service.ts +1 -1
  111. package/src/enterprise/training/training-student.controller.ts +3 -27
  112. package/src/enterprise/training/training-student.service.ts +350 -2
  113. package/src/instructor/instructor.service.ts +12 -3
  114. package/src/lms.module.ts +2 -0
  115. package/src/platforma/platforma.controller.ts +92 -0
@@ -13,7 +13,6 @@ import {
13
13
  Download,
14
14
  ExternalLink,
15
15
  Eye,
16
- EyeOff,
17
16
  FileText,
18
17
  GripVertical,
19
18
  HelpCircle,
@@ -39,6 +38,7 @@ import { useForm, useWatch } from 'react-hook-form';
39
38
  import { toast } from 'sonner';
40
39
  import { z } from 'zod';
41
40
 
41
+ import { CopyButton } from '@/components/copy-button';
42
42
  import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
43
43
  import { Badge } from '@/components/ui/badge';
44
44
  import { Button } from '@/components/ui/button';
@@ -140,6 +140,8 @@ import type {
140
140
  VideoProvider,
141
141
  } from './types';
142
142
 
143
+ const EMPTY_VIDEO_FRAMES: VideoFrame[] = [];
144
+
143
145
  function formatFileSize(bytes: number): string {
144
146
  if (bytes < 1024) return `${bytes} B`;
145
147
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -158,6 +160,28 @@ function formatDateTimeLabel(value?: string | null): string | null {
158
160
  }).format(parsed);
159
161
  }
160
162
 
163
+ function getInitials(name?: string | null): string {
164
+ const normalizedName = String(name ?? '').trim();
165
+ if (!normalizedName) return '--';
166
+
167
+ return normalizedName
168
+ .split(' ')
169
+ .filter(Boolean)
170
+ .slice(0, 2)
171
+ .map((part) => part[0])
172
+ .join('')
173
+ .toUpperCase();
174
+ }
175
+
176
+ function getInstructorAvatarUrl(avatarId?: number | null): string | undefined {
177
+ if (typeof avatarId !== 'number' || avatarId <= 0) return undefined;
178
+
179
+ const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL?.replace(/\/$/, '');
180
+ return baseUrl
181
+ ? `${baseUrl}/person/avatar/${avatarId}`
182
+ : `/person/avatar/${avatarId}`;
183
+ }
184
+
161
185
  function videoProfileResourceType(profileId: number): string {
162
186
  return `video_profile:${profileId}`;
163
187
  }
@@ -526,7 +550,7 @@ type FormValues = {
526
550
  type: LessonType;
527
551
  duration: number;
528
552
  status: LessonStatus;
529
- visibility: 'publico' | 'privado' | 'restrito';
553
+ published: boolean;
530
554
  publicDescription: string;
531
555
  privateDescription: string;
532
556
  videoProvider?: VideoProvider;
@@ -764,13 +788,14 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
764
788
  const lesson = useStructureStore((s) =>
765
789
  s.lessons.find((l) => l.id === lessonId)
766
790
  );
791
+ const sessions = useStructureStore((s) => s.sessions);
767
792
  const persistedVideoProvider: VideoProvider | undefined =
768
793
  lesson?.videoProvider === 'youtube' || lesson?.videoProvider === 'vimeo'
769
794
  ? lesson.videoProvider
770
795
  : lesson?.videoProvider
771
796
  ? 'file_storage'
772
797
  : undefined;
773
- const videoFrames = lesson?.frames ?? [];
798
+ const videoFrames = lesson?.frames ?? EMPTY_VIDEO_FRAMES;
774
799
  const updateLesson = useUpdateLessonMutation();
775
800
  const updateTranscriptionSegments = useUpdateTranscriptionSegmentsMutation(
776
801
  lesson?.id ?? null
@@ -792,7 +817,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
792
817
  [
793
818
  { value: 'youtube' as VideoProvider, label: 'YouTube' },
794
819
  { value: 'vimeo' as VideoProvider, label: 'Vimeo' },
795
- { value: 'file_storage' as VideoProvider, label: t('providers.fileStorage') },
820
+ {
821
+ value: 'file_storage' as VideoProvider,
822
+ label: t('providers.fileStorage'),
823
+ },
796
824
  ] as { value: VideoProvider; label: string }[]
797
825
  ).filter((p) => {
798
826
  if (p.value === 'youtube') return lmsSettings.youtubeEnabled;
@@ -819,7 +847,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
819
847
  'finalizada',
820
848
  'publicada',
821
849
  ] as const),
822
- visibility: z.enum(['publico', 'privado', 'restrito'] as const),
850
+ published: z.boolean(),
823
851
  publicDescription: z.string(),
824
852
  privateDescription: z.string(),
825
853
  videoProvider: z
@@ -842,7 +870,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
842
870
  type: lesson?.type ?? 'video',
843
871
  duration: lesson?.duration ?? 0,
844
872
  status: lesson?.status ?? 'preparada',
845
- visibility: lesson?.visibility ?? 'publico',
873
+ published: lesson?.published ?? false,
846
874
  publicDescription: lesson?.publicDescription ?? '',
847
875
  privateDescription: lesson?.privateDescription ?? '',
848
876
  videoProvider:
@@ -1145,15 +1173,21 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1145
1173
  const nextEntries = Object.entries(current).filter(([id]) =>
1146
1174
  frameIds.has(id)
1147
1175
  );
1176
+ if (nextEntries.length === Object.keys(current).length) return current;
1148
1177
  return Object.fromEntries(nextEntries);
1149
1178
  });
1150
1179
 
1151
1180
  setFrameImageErrorIds((current) => {
1181
+ let changed = false;
1152
1182
  const next = new Set<string>();
1153
1183
  for (const id of current) {
1154
- if (frameIds.has(id)) next.add(id);
1184
+ if (frameIds.has(id)) {
1185
+ next.add(id);
1186
+ } else {
1187
+ changed = true;
1188
+ }
1155
1189
  }
1156
- return next;
1190
+ return changed ? next : current;
1157
1191
  });
1158
1192
 
1159
1193
  const frameSignatures = new Set(
@@ -1219,12 +1253,22 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1219
1253
  if (cancelled) return;
1220
1254
 
1221
1255
  setFrameAssetMetadataById((current) => {
1256
+ let changed = false;
1222
1257
  const next = { ...current };
1223
1258
  for (const result of results) {
1224
1259
  if (result.status !== 'fulfilled') continue;
1225
- next[result.value.frameId] = result.value.metadata;
1260
+ const currentMetadata = current[result.value.frameId];
1261
+ const nextMetadata = result.value.metadata;
1262
+ if (
1263
+ currentMetadata?.url === nextMetadata.url &&
1264
+ currentMetadata?.sizeLabel === nextMetadata.sizeLabel
1265
+ ) {
1266
+ continue;
1267
+ }
1268
+ next[result.value.frameId] = nextMetadata;
1269
+ changed = true;
1226
1270
  }
1227
- return next;
1271
+ return changed ? next : current;
1228
1272
  });
1229
1273
  });
1230
1274
 
@@ -1365,6 +1409,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
1365
1409
  const cfg = TYPE_CONFIG[lesson.type];
1366
1410
  const Icon = cfg.icon;
1367
1411
  const lessonTypeLabel = t(cfg.labelKey);
1412
+ const session = sessions.find((s) => s.id === lesson.sessionId);
1413
+ const lessonFullCode = session
1414
+ ? `${session.code}_${lesson.code}_${lesson.title}`
1415
+ : null;
1368
1416
  const originalVideoResource =
1369
1417
  localResources.find((res) => res.type === 'video_original') ?? null;
1370
1418
  const isDownloadingOriginalVideo = originalVideoResource
@@ -2392,7 +2440,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
2392
2440
  setTranscriptionSegments(updater);
2393
2441
  }
2394
2442
 
2395
- function onSubmit(values: FormValues) {
2443
+ function persistLesson(values: FormValues, shouldConfirmAutoStatus: boolean) {
2396
2444
  if (values.type === 'video') {
2397
2445
  const segmentsPayload = transcriptionSegments
2398
2446
  .map((segment) => ({
@@ -2428,6 +2476,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
2428
2476
  sessionId: lesson!.sessionId,
2429
2477
  formValues: {
2430
2478
  ...values,
2479
+ confirmarPublicacaoComStatus: shouldConfirmAutoStatus,
2431
2480
  videoUrl:
2432
2481
  values.type === 'video' && values.videoProvider === 'file_storage'
2433
2482
  ? ''
@@ -2439,15 +2488,39 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
2439
2488
  },
2440
2489
  },
2441
2490
  {
2442
- onSuccess: () => {
2491
+ onSuccess: (updatedLesson) => {
2443
2492
  persistedInstructorIdsRef.current = [...selectedInstructorIds];
2444
- form.reset({ ...values, transcription: values.transcription });
2493
+ form.reset({
2494
+ ...values,
2495
+ status: updatedLesson.statusProducao ?? values.status,
2496
+ published: updatedLesson.published ?? values.published,
2497
+ transcription: updatedLesson.transcricao ?? values.transcription,
2498
+ });
2445
2499
  setResourcesDirty(false);
2446
2500
  },
2447
2501
  }
2448
2502
  );
2449
2503
  }
2450
2504
 
2505
+ function onSubmit(values: FormValues) {
2506
+ const shouldConfirmAutoStatus =
2507
+ values.published && values.status !== 'publicada';
2508
+
2509
+ if (shouldConfirmAutoStatus) {
2510
+ showConfirm({
2511
+ title: 'Confirmar publicação da aula?',
2512
+ description:
2513
+ 'Esta aula será publicada e o status de produção será alterado automaticamente para Publicada. Deseja continuar?',
2514
+ confirmText: 'Publicar',
2515
+ destructive: false,
2516
+ onConfirm: () => persistLesson(values, true),
2517
+ });
2518
+ return;
2519
+ }
2520
+
2521
+ persistLesson(values, false);
2522
+ }
2523
+
2451
2524
  // ── Question sheet helpers ────────────────────────────────────────────────
2452
2525
 
2453
2526
  function openCreateQuestion() {
@@ -2581,15 +2654,29 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
2581
2654
  <div className="flex-1 min-w-0">
2582
2655
  <div className="flex items-center gap-1.5">
2583
2656
  <span className="text-sm font-semibold truncate">
2584
- {t('lessonForm.titleCreate')}
2657
+ {lesson.title}
2585
2658
  </span>
2586
2659
  {isDirty && (
2587
2660
  <CircleDot className="size-3 text-amber-500 shrink-0" />
2588
2661
  )}
2589
2662
  </div>
2590
- <p className="text-[0.65rem] text-muted-foreground truncate">
2591
- {lesson.code} · {lessonTypeLabel}
2592
- </p>
2663
+ <div className="flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-1 text-[0.65rem] text-muted-foreground">
2664
+ <span className="shrink-0">
2665
+ {lesson.code} · {lessonTypeLabel}
2666
+ </span>
2667
+ {lessonFullCode && (
2668
+ <>
2669
+ <span className="text-muted-foreground/60">·</span>
2670
+ <span className="max-w-full truncate rounded border border-border/60 bg-background/70 px-1.5 py-0.5 font-mono text-[0.62rem] text-foreground/80">
2671
+ {lessonFullCode}
2672
+ </span>
2673
+ <CopyButton
2674
+ value={lessonFullCode}
2675
+ className="size-5 shrink-0 text-muted-foreground"
2676
+ />
2677
+ </>
2678
+ )}
2679
+ </div>
2593
2680
  </div>
2594
2681
  {watchedStatus && (
2595
2682
  <span
@@ -2629,7 +2716,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
2629
2716
  onValueChange={(value) => setActiveTab(value as LessonEditorTab)}
2630
2717
  className="flex flex-col flex-1 min-h-0 min-w-0"
2631
2718
  >
2632
- <TabsList className="mx-2 mt-1.5 h-auto w-[calc(100%-1rem)] justify-start shrink-0 bg-muted/50 overflow-x-auto overflow-y-hidden whitespace-nowrap sm:mx-3 sm:mt-2 sm:w-auto">
2719
+ <TabsList className="mt-0 h-auto w-full justify-start shrink-0 rounded-none border-b bg-muted/50 px-2 py-1 overflow-x-auto overflow-y-hidden whitespace-nowrap sm:px-3">
2633
2720
  <TabsTrigger
2634
2721
  value="dados"
2635
2722
  className="h-6 px-2 text-[11px] shrink-0 sm:h-7 sm:px-2.5 sm:text-xs"
@@ -2848,42 +2935,21 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
2848
2935
 
2849
2936
  <FormField
2850
2937
  control={form.control}
2851
- name="visibility"
2938
+ name="published"
2852
2939
  render={({ field }) => (
2853
2940
  <FormItem>
2854
- <FormLabel className="text-xs">
2855
- {t('questionEditor.visibility')}
2856
- </FormLabel>
2857
- <Select
2858
- value={field.value}
2859
- onValueChange={field.onChange}
2860
- >
2861
- <FormControl>
2862
- <SelectTrigger className="h-8 text-xs w-full">
2863
- <SelectValue />
2864
- </SelectTrigger>
2865
- </FormControl>
2866
- <SelectContent>
2867
- <SelectItem value="publico">
2868
- <span className="flex items-center gap-1.5">
2869
- <Eye className="size-3" />{' '}
2870
- {t('lessonForm.public')}
2871
- </span>
2872
- </SelectItem>
2873
- <SelectItem value="privado">
2874
- <span className="flex items-center gap-1.5">
2875
- <EyeOff className="size-3" />{' '}
2876
- {t('lessonForm.private')}
2877
- </span>
2878
- </SelectItem>
2879
- <SelectItem value="restrito">
2880
- <span className="flex items-center gap-1.5">
2881
- <Lock className="size-3" />{' '}
2882
- {t('questionEditor.restricted')}
2883
- </span>
2884
- </SelectItem>
2885
- </SelectContent>
2886
- </Select>
2941
+ <FormLabel className="text-xs">Publicada</FormLabel>
2942
+ <FormControl>
2943
+ <div className="flex h-8 items-center justify-between gap-3 rounded-md border px-3">
2944
+ <p className="cursor-default text-xs text-foreground">
2945
+ Publicada
2946
+ </p>
2947
+ <Switch
2948
+ checked={field.value}
2949
+ onCheckedChange={field.onChange}
2950
+ />
2951
+ </div>
2952
+ </FormControl>
2887
2953
  <FormMessage className="text-xs" />
2888
2954
  </FormItem>
2889
2955
  )}
@@ -2901,7 +2967,11 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
2901
2967
  </CardHeader>
2902
2968
  <CardContent className="px-3 pb-2 flex flex-col gap-2">
2903
2969
  {/* Picker para adicionar */}
2904
- <EntityPicker<{ id: number; name: string }>
2970
+ <EntityPicker<{
2971
+ id: number;
2972
+ name: string;
2973
+ avatarId?: number | null;
2974
+ }>
2905
2975
  key={instructorPickerResetKey}
2906
2976
  value={null}
2907
2977
  onChange={(val) => {
@@ -2923,6 +2993,44 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
2923
2993
  )}
2924
2994
  getOptionValue={(o) => o.id}
2925
2995
  getOptionLabel={(o) => o.name}
2996
+ renderOption={({ option }) => {
2997
+ const avatarUrl = getInstructorAvatarUrl(
2998
+ option.avatarId
2999
+ );
3000
+ const displayName = option.name ?? String(option.id);
3001
+
3002
+ return (
3003
+ <div className="flex min-w-0 items-center gap-2">
3004
+ <Avatar className="size-6 shrink-0">
3005
+ <AvatarImage src={avatarUrl} alt={displayName} />
3006
+ <AvatarFallback className="text-[0.6rem] font-medium">
3007
+ {getInitials(displayName)}
3008
+ </AvatarFallback>
3009
+ </Avatar>
3010
+ <span className="truncate text-xs">
3011
+ {displayName}
3012
+ </span>
3013
+ </div>
3014
+ );
3015
+ }}
3016
+ renderSelectedValue={({ option, label }) => {
3017
+ const avatarUrl = getInstructorAvatarUrl(
3018
+ option?.avatarId
3019
+ );
3020
+ const displayName = option?.name ?? label;
3021
+
3022
+ return (
3023
+ <span className="flex min-w-0 items-center gap-2">
3024
+ <Avatar className="size-5 shrink-0">
3025
+ <AvatarImage src={avatarUrl} alt={displayName} />
3026
+ <AvatarFallback className="text-[0.6rem] font-medium">
3027
+ {getInitials(displayName)}
3028
+ </AvatarFallback>
3029
+ </Avatar>
3030
+ <span className="truncate">{displayName}</span>
3031
+ </span>
3032
+ );
3033
+ }}
2926
3034
  />
2927
3035
  {/* Lista de instrutores selecionados */}
2928
3036
  {selectedInstructorIds.length > 0 && (
@@ -2937,20 +3045,22 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
2937
3045
  key={sid}
2938
3046
  className="flex items-center gap-2 rounded-md border bg-muted/20 px-2 py-1"
2939
3047
  >
2940
- <Avatar className="size-6 shrink-0">
2941
- <AvatarImage
2942
- src={undefined}
2943
- alt={displayName}
2944
- />
2945
- <AvatarFallback className="text-[0.6rem] font-medium">
2946
- {displayName
2947
- .split(' ')
2948
- .slice(0, 2)
2949
- .map((n: string) => n[0])
2950
- .join('')
2951
- .toUpperCase()}
2952
- </AvatarFallback>
2953
- </Avatar>
3048
+ {(() => {
3049
+ const avatarUrl = getInstructorAvatarUrl(
3050
+ inst?.avatarId
3051
+ );
3052
+ return (
3053
+ <Avatar className="size-6 shrink-0">
3054
+ <AvatarImage
3055
+ src={avatarUrl}
3056
+ alt={displayName}
3057
+ />
3058
+ <AvatarFallback className="text-[0.6rem] font-medium">
3059
+ {getInitials(displayName)}
3060
+ </AvatarFallback>
3061
+ </Avatar>
3062
+ );
3063
+ })()}
2954
3064
  <span className="text-xs flex-1 truncate">
2955
3065
  {displayName}
2956
3066
  </span>
@@ -8,7 +8,6 @@ import {
8
8
  EyeOff,
9
9
  Layers,
10
10
  Loader2,
11
- Lock,
12
11
  Plus,
13
12
  Save,
14
13
  Trash2,
@@ -19,7 +18,6 @@ import { useEffect, useMemo } from 'react';
19
18
  import { useForm } from 'react-hook-form';
20
19
  import { z } from 'zod';
21
20
 
22
- import { Badge } from '@/components/ui/badge';
23
21
  import { Button } from '@/components/ui/button';
24
22
  import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
25
23
  import {
@@ -32,16 +30,14 @@ import {
32
30
  } from '@/components/ui/form';
33
31
  import { Input } from '@/components/ui/input';
34
32
  import { ScrollArea } from '@/components/ui/scroll-area';
35
- import {
36
- Select,
37
- SelectContent,
38
- SelectItem,
39
- SelectTrigger,
40
- SelectValue,
41
- } from '@/components/ui/select';
42
33
  import { Separator } from '@/components/ui/separator';
43
34
  import { Switch } from '@/components/ui/switch';
44
35
  import { Textarea } from '@/components/ui/textarea';
36
+ import {
37
+ Tooltip,
38
+ TooltipContent,
39
+ TooltipTrigger,
40
+ } from '@/components/ui/tooltip';
45
41
 
46
42
  import {
47
43
  useCreateLessonMutation,
@@ -50,7 +46,6 @@ import {
50
46
  } from '../_data/use-course-structure-mutations';
51
47
  import { IconActionTooltip } from './icon-action-tooltip';
52
48
  import { useStructureStore } from './store';
53
- import type { Visibility } from './types';
54
49
 
55
50
  // ── Schema ────────────────────────────────────────────────────────────────────
56
51
 
@@ -59,7 +54,6 @@ const schema = z.object({
59
54
  title: z.string().min(1, 'Título obrigatório'),
60
55
  description: z.string(),
61
56
  duration: z.coerce.number().min(0),
62
- visibility: z.enum(['publico', 'privado', 'restrito'] as const),
63
57
  published: z.boolean(),
64
58
  });
65
59
 
@@ -95,8 +89,7 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
95
89
  title: session?.title ?? '',
96
90
  description: session?.description ?? '',
97
91
  duration: session?.duration ?? 0,
98
- visibility: (session?.visibility ?? 'publico') as Visibility,
99
- published: session?.published ?? true,
92
+ published: session?.published ?? false,
100
93
  };
101
94
 
102
95
  const form = useForm<FormValues>({
@@ -113,8 +106,7 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
113
106
  title: session.title,
114
107
  description: session.description ?? '',
115
108
  duration: session.duration,
116
- visibility: session.visibility ?? 'publico',
117
- published: session.published ?? true,
109
+ published: session.published ?? false,
118
110
  });
119
111
  }, [session?.id]); // eslint-disable-line react-hooks/exhaustive-deps
120
112
 
@@ -156,12 +148,23 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
156
148
  {session.code}
157
149
  </p>
158
150
  </div>
159
- <Badge
160
- variant={session.published ? 'default' : 'secondary'}
161
- className="shrink-0 text-xs"
162
- >
163
- {session.published ? 'Publicada' : 'Oculta'}
164
- </Badge>
151
+ <Tooltip>
152
+ <TooltipTrigger asChild>
153
+ <span
154
+ className="inline-flex shrink-0 items-center"
155
+ aria-label={session.published ? 'Publicada' : 'Rascunho'}
156
+ >
157
+ {session.published ? (
158
+ <Eye className="size-4 text-emerald-600 dark:text-emerald-400" />
159
+ ) : (
160
+ <EyeOff className="size-4 text-muted-foreground" />
161
+ )}
162
+ </span>
163
+ </TooltipTrigger>
164
+ <TooltipContent>
165
+ {session.published ? 'Publicada' : 'Rascunho'}
166
+ </TooltipContent>
167
+ </Tooltip>
165
168
  <IconActionTooltip
166
169
  label="Excluir sessão"
167
170
  asWrapper={deleteSession.isPending}
@@ -285,45 +288,7 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
285
288
  </CardTitle>
286
289
  </CardHeader>
287
290
  <CardContent className="px-3 pb-2">
288
- <div className="grid grid-cols-1 gap-2 items-end sm:grid-cols-2">
289
- <FormField
290
- control={form.control}
291
- name="visibility"
292
- render={({ field }) => (
293
- <FormItem>
294
- <FormLabel className="text-xs">Visibilidade</FormLabel>
295
- <Select
296
- value={field.value}
297
- onValueChange={field.onChange}
298
- >
299
- <FormControl>
300
- <SelectTrigger className="h-8 text-xs w-full">
301
- <SelectValue />
302
- </SelectTrigger>
303
- </FormControl>
304
- <SelectContent>
305
- <SelectItem value="publico">
306
- <span className="flex items-center gap-1.5">
307
- <Eye className="size-3" /> Público
308
- </span>
309
- </SelectItem>
310
- <SelectItem value="privado">
311
- <span className="flex items-center gap-1.5">
312
- <EyeOff className="size-3" /> Privado
313
- </span>
314
- </SelectItem>
315
- <SelectItem value="restrito">
316
- <span className="flex items-center gap-1.5">
317
- <Lock className="size-3" /> Restrito
318
- </span>
319
- </SelectItem>
320
- </SelectContent>
321
- </Select>
322
- <FormMessage className="text-xs" />
323
- </FormItem>
324
- )}
325
- />
326
-
291
+ <div className="grid grid-cols-1 gap-2 items-end">
327
292
  <FormField
328
293
  control={form.control}
329
294
  name="published"
@@ -63,6 +63,7 @@ export const MOCK_SESSIONS: Session[] = SESSION_CONFIG.map((s, i) => ({
63
63
  title: s.title,
64
64
  duration: s.lessonCount * 12,
65
65
  order: i,
66
+ published: true,
66
67
  }));
67
68
 
68
69
  // Lesson title patterns
@@ -72,6 +72,8 @@ interface StructureState {
72
72
  open: boolean;
73
73
  title: string;
74
74
  description: string;
75
+ confirmText: string;
76
+ destructive: boolean;
75
77
  onConfirm: (() => void) | null;
76
78
  };
77
79
 
@@ -122,6 +124,7 @@ interface StructureState {
122
124
  data: Partial<
123
125
  LessonFormValues & {
124
126
  transcription?: string;
127
+ videoConversionJobId?: number;
125
128
  resources?: Lesson['resources'];
126
129
  }
127
130
  >
@@ -183,6 +186,8 @@ interface StructureState {
183
186
  showConfirm: (opts: {
184
187
  title: string;
185
188
  description: string;
189
+ confirmText?: string;
190
+ destructive?: boolean;
186
191
  onConfirm: () => void;
187
192
  }) => void;
188
193
  closeConfirm: () => void;
@@ -282,6 +287,8 @@ export const useStructureStore = create<StructureState>((set, get) => ({
282
287
  open: false,
283
288
  title: '',
284
289
  description: '',
290
+ confirmText: '',
291
+ destructive: true,
285
292
  onConfirm: null as (() => void) | null,
286
293
  },
287
294
  sessionPickerDialog: {
@@ -380,6 +387,7 @@ export const useStructureStore = create<StructureState>((set, get) => ({
380
387
  title: `Nova Sessão ${code}`,
381
388
  duration: 30,
382
389
  order: sessions.length,
390
+ published: false,
383
391
  };
384
392
  set((s) => {
385
393
  const nextExpandedIds = new Set([...s.expandedIds, newSession.id]);
@@ -433,6 +441,7 @@ export const useStructureStore = create<StructureState>((set, get) => ({
433
441
  duration: 10,
434
442
  sessionId,
435
443
  order: sessionLessons.length,
444
+ published: false,
436
445
  resources: [],
437
446
  };
438
447
  set((s) => {
@@ -919,6 +928,8 @@ export const useStructureStore = create<StructureState>((set, get) => ({
919
928
  open: true,
920
929
  title: opts.title,
921
930
  description: opts.description,
931
+ confirmText: opts.confirmText ?? '',
932
+ destructive: opts.destructive ?? true,
922
933
  onConfirm: opts.onConfirm,
923
934
  },
924
935
  }),
@@ -328,6 +328,7 @@ function LessonMenu({ data }: { data: Lesson }) {
328
328
 
329
329
  const hasLessonClipboard = copiedType === 'lesson' && copiedIds.length > 0;
330
330
  const hasOtherSessions = sessions.some((ss) => ss.id !== data.sessionId);
331
+ const session = sessions.find((ss) => ss.id === data.sessionId);
331
332
 
332
333
  // Ids to move when "Mover" is triggered
333
334
  const idsToMove = multiLessons ? selectedLessonIds : [data.id];
@@ -373,6 +374,27 @@ function LessonMenu({ data }: { data: Lesson }) {
373
374
  Copiar aula
374
375
  </ContextMenuItem>
375
376
 
377
+ <ContextMenuItem
378
+ onSelect={async () => {
379
+ if (!session) {
380
+ toast.error('Sessão da aula não encontrada');
381
+ return;
382
+ }
383
+
384
+ const lessonCopyCode = `${session.code}_${data.code}_${data.title}`;
385
+
386
+ try {
387
+ await navigator.clipboard.writeText(lessonCopyCode);
388
+ toast.success('Código da aula copiado');
389
+ } catch {
390
+ toast.error('Não foi possível copiar o código da aula');
391
+ }
392
+ }}
393
+ >
394
+ <Clipboard className="size-3.5 mr-2 text-muted-foreground" />
395
+ Copiar código da aula
396
+ </ContextMenuItem>
397
+
376
398
  {multiLessons && (
377
399
  <ContextMenuItem
378
400
  onSelect={() => {