@hed-hog/lms 0.0.351 → 0.0.354

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 (135) hide show
  1. package/dist/course/course-audio-transcription.service.d.ts +29 -0
  2. package/dist/course/course-audio-transcription.service.d.ts.map +1 -0
  3. package/dist/course/course-audio-transcription.service.js +291 -0
  4. package/dist/course/course-audio-transcription.service.js.map +1 -0
  5. package/dist/course/course-lesson.controller.d.ts +10 -0
  6. package/dist/course/course-lesson.controller.d.ts.map +1 -0
  7. package/dist/course/course-lesson.controller.js +62 -0
  8. package/dist/course/course-lesson.controller.js.map +1 -0
  9. package/dist/course/course-structure.controller.d.ts +41 -15
  10. package/dist/course/course-structure.controller.d.ts.map +1 -1
  11. package/dist/course/course-structure.controller.js +50 -6
  12. package/dist/course/course-structure.controller.js.map +1 -1
  13. package/dist/course/course-structure.service.d.ts +50 -15
  14. package/dist/course/course-structure.service.d.ts.map +1 -1
  15. package/dist/course/course-structure.service.js +238 -73
  16. package/dist/course/course-structure.service.js.map +1 -1
  17. package/dist/course/course-video-conversion.service.d.ts +20 -2
  18. package/dist/course/course-video-conversion.service.d.ts.map +1 -1
  19. package/dist/course/course-video-conversion.service.js +730 -10
  20. package/dist/course/course-video-conversion.service.js.map +1 -1
  21. package/dist/course/course.controller.d.ts +24 -8
  22. package/dist/course/course.controller.d.ts.map +1 -1
  23. package/dist/course/course.module.d.ts.map +1 -1
  24. package/dist/course/course.module.js +5 -3
  25. package/dist/course/course.module.js.map +1 -1
  26. package/dist/course/course.service.d.ts +24 -8
  27. package/dist/course/course.service.d.ts.map +1 -1
  28. package/dist/course/course.service.js +112 -176
  29. package/dist/course/course.service.js.map +1 -1
  30. package/dist/course/dto/create-course-lesson-frame.dto.d.ts +5 -0
  31. package/dist/course/dto/create-course-lesson-frame.dto.d.ts.map +1 -0
  32. package/dist/course/dto/create-course-lesson-frame.dto.js +30 -0
  33. package/dist/course/dto/create-course-lesson-frame.dto.js.map +1 -0
  34. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +4 -2
  35. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  36. package/dist/course/dto/create-course-structure-lesson.dto.js +10 -3
  37. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  38. package/dist/course/dto/create-course.dto.d.ts +1 -1
  39. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  40. package/dist/course/dto/create-course.dto.js +6 -6
  41. package/dist/course/dto/create-course.dto.js.map +1 -1
  42. package/dist/course/dto/update-course-lesson-frame.dto.d.ts +5 -0
  43. package/dist/course/dto/update-course-lesson-frame.dto.d.ts.map +1 -0
  44. package/dist/course/dto/update-course-lesson-frame.dto.js +32 -0
  45. package/dist/course/dto/update-course-lesson-frame.dto.js.map +1 -0
  46. package/dist/course/dto/update-course-resources.dto.d.ts +4 -2
  47. package/dist/course/dto/update-course-resources.dto.d.ts.map +1 -1
  48. package/dist/course/dto/update-course-resources.dto.js +10 -3
  49. package/dist/course/dto/update-course-resources.dto.js.map +1 -1
  50. package/dist/course/dto/update-transcription-segments.dto.d.ts +10 -0
  51. package/dist/course/dto/update-transcription-segments.dto.d.ts.map +1 -0
  52. package/dist/course/dto/update-transcription-segments.dto.js +38 -0
  53. package/dist/course/dto/update-transcription-segments.dto.js.map +1 -0
  54. package/dist/course/lms-setting.controller.d.ts +13 -0
  55. package/dist/course/lms-setting.controller.d.ts.map +1 -0
  56. package/dist/course/lms-setting.controller.js +53 -0
  57. package/dist/course/lms-setting.controller.js.map +1 -0
  58. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
  59. package/dist/enterprise/training/training-admin.service.js +74 -33
  60. package/dist/enterprise/training/training-admin.service.js.map +1 -1
  61. package/dist/index.d.ts +2 -0
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +2 -0
  64. package/dist/index.js.map +1 -1
  65. package/dist/lms.module.d.ts.map +1 -1
  66. package/dist/lms.module.js +6 -0
  67. package/dist/lms.module.js.map +1 -1
  68. package/hedhog/data/route.yaml +63 -0
  69. package/hedhog/data/setting_group.yaml +76 -0
  70. package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +3 -0
  71. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +10 -1
  72. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +7 -2
  73. package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +5 -2
  74. package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +7 -1
  75. package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +3 -0
  76. package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +95 -50
  77. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +11 -36
  78. package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +19 -5
  79. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
  80. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +30 -10
  81. package/hedhog/frontend/app/courses/[id]/structure/_components/CourseInstructorsSummaryCard.tsx.ejs +95 -0
  82. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +29 -11
  83. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +42 -31
  84. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +10 -1
  85. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -9
  86. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +25 -22
  87. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +12 -9
  88. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +315 -229
  89. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +3220 -534
  90. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +20 -15
  91. package/hedhog/frontend/app/courses/[id]/structure/_components/icon-action-tooltip.tsx.ejs +35 -0
  92. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +76 -67
  93. package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +13 -10
  94. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +18 -16
  95. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +6 -0
  96. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +1 -1
  97. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +23 -11
  98. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +48 -9
  99. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +11 -2
  100. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +121 -15
  101. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +69 -14
  102. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +11 -0
  103. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +31 -0
  104. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +32 -6
  105. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +57 -3
  106. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +46 -6
  107. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +14 -0
  108. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lesson-audio-files.ts.ejs +23 -0
  109. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +34 -0
  110. package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +76 -0
  111. package/hedhog/frontend/messages/en.json +39 -3
  112. package/hedhog/frontend/messages/pt.json +39 -3
  113. package/hedhog/table/course.yaml +8 -0
  114. package/hedhog/table/course_lesson_file.yaml +12 -4
  115. package/hedhog/table/course_lesson_transcription_segment.yaml +22 -0
  116. package/hedhog/table/course_lesson_video_frame.yaml +25 -0
  117. package/package.json +9 -9
  118. package/src/course/course-audio-transcription.service.ts +393 -0
  119. package/src/course/course-lesson.controller.ts +28 -0
  120. package/src/course/course-structure.controller.ts +49 -3
  121. package/src/course/course-structure.service.ts +294 -32
  122. package/src/course/course-video-conversion.service.ts +972 -6
  123. package/src/course/course.module.ts +5 -3
  124. package/src/course/course.service.ts +87 -139
  125. package/src/course/dto/create-course-lesson-frame.dto.ts +14 -0
  126. package/src/course/dto/create-course-structure-lesson.dto.ts +19 -3
  127. package/src/course/dto/create-course.dto.ts +5 -5
  128. package/src/course/dto/update-course-lesson-frame.dto.ts +16 -0
  129. package/src/course/dto/update-course-resources.dto.ts +18 -3
  130. package/src/course/dto/update-transcription-segments.dto.ts +20 -0
  131. package/src/course/lms-setting.controller.ts +30 -0
  132. package/src/enterprise/training/training-admin.service.ts +77 -24
  133. package/src/index.ts +2 -0
  134. package/src/lms.module.ts +6 -0
  135. package/hedhog/table/course_instructor.yaml +0 -27
@@ -0,0 +1,95 @@
1
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
2
+ import { Badge } from '@/components/ui/badge';
3
+ import { Users } from 'lucide-react';
4
+
5
+ import { CourseSectionCard } from '../../_components/CourseSectionCard';
6
+
7
+ type CourseInstructorSummary = {
8
+ id: number;
9
+ name: string;
10
+ avatarId: number | null;
11
+ };
12
+
13
+ type CourseInstructorsSummaryCardProps = {
14
+ instructors: CourseInstructorSummary[];
15
+ compact?: boolean;
16
+ };
17
+
18
+ function getInstructorAvatarUrl(avatarId?: number | null) {
19
+ return typeof avatarId === 'number' && avatarId > 0
20
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
21
+ : null;
22
+ }
23
+
24
+ function getInitials(label: string) {
25
+ return label
26
+ .split(' ')
27
+ .filter(Boolean)
28
+ .slice(0, 2)
29
+ .map((part) => part[0]?.toUpperCase() ?? '')
30
+ .join('');
31
+ }
32
+
33
+ export function CourseInstructorsSummaryCard({
34
+ instructors,
35
+ compact = false,
36
+ }: CourseInstructorsSummaryCardProps) {
37
+ return (
38
+ <CourseSectionCard
39
+ title="Instrutores das aulas"
40
+ description="Agrupados a partir das aulas vinculadas ao curso."
41
+ icon={Users}
42
+ compact={compact}
43
+ >
44
+ {instructors.length > 0 ? (
45
+ <div className={compact ? 'space-y-2' : 'space-y-3'}>
46
+ <div className="flex flex-wrap gap-2">
47
+ <Badge variant="secondary" className="rounded-full px-3 py-1">
48
+ {instructors.length} instrutor
49
+ {instructors.length === 1 ? '' : 'es'}
50
+ </Badge>
51
+ </div>
52
+
53
+ <div
54
+ className={
55
+ compact
56
+ ? 'grid gap-2 sm:grid-cols-2 xl:grid-cols-3'
57
+ : 'grid gap-2 sm:grid-cols-2 xl:grid-cols-3'
58
+ }
59
+ >
60
+ {instructors.map((instructor) => (
61
+ <div
62
+ key={instructor.id}
63
+ className="flex items-center gap-3 rounded-xl border border-border/60 bg-background px-3 py-2.5"
64
+ >
65
+ <Avatar className="h-9 w-9 rounded-xl border border-border/60">
66
+ <AvatarImage
67
+ src={
68
+ getInstructorAvatarUrl(instructor.avatarId) ?? undefined
69
+ }
70
+ />
71
+ <AvatarFallback className="rounded-xl bg-muted text-xs font-semibold text-foreground">
72
+ {getInitials(instructor.name || `ID ${instructor.id}`)}
73
+ </AvatarFallback>
74
+ </Avatar>
75
+
76
+ <div className="min-w-0">
77
+ <p className="truncate text-sm font-medium text-foreground">
78
+ {instructor.name || `Instrutor #${instructor.id}`}
79
+ </p>
80
+ <p className="truncate text-xs text-muted-foreground">
81
+ ID {instructor.id}
82
+ </p>
83
+ </div>
84
+ </div>
85
+ ))}
86
+ </div>
87
+ </div>
88
+ ) : (
89
+ <p className="text-sm text-muted-foreground">
90
+ Nenhum instrutor vinculado às aulas deste curso.
91
+ </p>
92
+ )}
93
+ </CourseSectionCard>
94
+ );
95
+ }
@@ -31,7 +31,11 @@ import { TreeDragOverlay } from './drag-overlay';
31
31
  import { SortableTreeRow } from './sortable-tree-row';
32
32
  import { useStructureStore } from './store';
33
33
  import { TreeRootContextMenu } from './tree-context-menu';
34
- import { buildLessonCountMap, buildVisibleItems } from './tree-helpers';
34
+ import {
35
+ buildLessonCountMap,
36
+ buildSessionIndicatorMap,
37
+ buildVisibleItems,
38
+ } from './tree-helpers';
35
39
  import type { FlatItem } from './types';
36
40
 
37
41
  const ROW_HEIGHT: Record<FlatItem['type'], number> = {
@@ -74,6 +78,10 @@ export function CourseTreeDnd() {
74
78
  );
75
79
 
76
80
  const lessonCountMap = useMemo(() => buildLessonCountMap(lessons), [lessons]);
81
+ const sessionIndicatorMap = useMemo(
82
+ () => buildSessionIndicatorMap(lessons),
83
+ [lessons]
84
+ );
77
85
 
78
86
  const sortedSessions = useMemo(
79
87
  () => [...sessions].sort((a, b) => a.order - b.order),
@@ -160,20 +168,21 @@ export function CourseTreeDnd() {
160
168
 
161
169
  if (overItem.type === 'lesson') {
162
170
  toSessionId = overItem.data.sessionId;
163
- // Position relative to lessons in the target session (visible order)
171
+ // Position relative to lessons in the target session (visible order),
172
+ // excluding the dragged item so the index matches what the store expects.
164
173
  const destLessons = items.filter(
165
174
  (i) =>
166
175
  i.type === 'lesson' &&
167
176
  i.data.sessionId === toSessionId &&
168
- i.id !== active.id
177
+ i.id !== activeItem.id
169
178
  );
170
- toIndex = destLessons.findIndex((i) => i.id === over.id);
179
+ toIndex = destLessons.findIndex((i) => i.id === overItem.id);
171
180
  if (toIndex === -1) toIndex = destLessons.length;
172
181
  } else if (overItem.type === 'session') {
173
182
  // Dropped directly onto a session header → append to that session
174
183
  toSessionId = overItem.data.id as string;
175
184
  const destLessons = lessons.filter(
176
- (l) => l.sessionId === toSessionId && l.id !== active.id
185
+ (l) => l.sessionId === toSessionId && l.id !== activeItem.id
177
186
  );
178
187
  toIndex = destLessons.length;
179
188
  } else {
@@ -185,11 +194,9 @@ export function CourseTreeDnd() {
185
194
  // Snapshot before store mutation (optimistic)
186
195
  const previousLessons = [...lessons];
187
196
 
188
- // Optimistic store update
189
- moveLesson(String(activeItem.data.id), toSessionId, toIndex);
190
-
191
197
  if (didChangeSession) {
192
- // Cross-session move
198
+ // Cross-session move — toIndex is in destLessons space (correct for API + store)
199
+ moveLesson(String(activeItem.data.id), toSessionId, toIndex);
193
200
  const targetSession = sessions.find((s) => s.id === toSessionId);
194
201
  moveLessonMutation.mutate({
195
202
  lessonId: String(activeItem.data.id),
@@ -202,17 +209,27 @@ export function CourseTreeDnd() {
202
209
  `Aula "${lessonData.title}" movida para "${targetSession?.title ?? toSessionId}"`
203
210
  );
204
211
  } else {
205
- // Reorder within same session compute new ordered IDs
212
+ // Same-session reorder derive both indices from the full sessionLessons
213
+ // array so arrayMove and the store's splice are consistent.
206
214
  const sessionLessons = lessons
207
215
  .filter((l) => l.sessionId === toSessionId)
208
216
  .sort((a, b) => a.order - b.order);
209
217
  const fromLessonIdx = sessionLessons.findIndex(
210
218
  (l) => l.id === String(activeItem.data.id)
211
219
  );
220
+ const toLessonIdx = sessionLessons.findIndex(
221
+ (l) => l.id === overItem.id
222
+ );
223
+ if (fromLessonIdx === -1 || toLessonIdx === -1) return;
224
+
225
+ // Pass the full-array index so the store's splice produces the same
226
+ // result as arrayMove (inserting B after C, not before it).
227
+ moveLesson(String(activeItem.data.id), toSessionId, toLessonIdx);
228
+
212
229
  const orderedIds = arrayMove(
213
230
  sessionLessons,
214
231
  fromLessonIdx,
215
- toIndex
232
+ toLessonIdx
216
233
  ).map((l) => l.id);
217
234
 
218
235
  reorderLessonsMutation.mutate({
@@ -355,6 +372,7 @@ export function CourseTreeDnd() {
355
372
  expandedBySearch.has(item.id)
356
373
  }
357
374
  lessonCountMap={lessonCountMap}
375
+ sessionIndicatorMap={sessionIndicatorMap}
358
376
  visibleItems={items}
359
377
  dragDisabled={dragDisabled}
360
378
  />
@@ -7,6 +7,7 @@ import { useTranslations } from 'next-intl';
7
7
  import { forwardRef, useMemo } from 'react';
8
8
  import { useCreateSessionMutation } from '../_data/use-course-structure-mutations';
9
9
  import { CourseTreeDnd } from './course-tree-dnd';
10
+ import { IconActionTooltip } from './icon-action-tooltip';
10
11
  import { MultiSelectBar } from './multi-select-bar';
11
12
  import { SearchFilter, type SearchFilterHandle } from './search-filter';
12
13
  import { useStructureStore } from './store';
@@ -50,44 +51,54 @@ export const CourseTreePanel = forwardRef<SearchFilterHandle>((_, ref) => {
50
51
  <SearchFilter ref={ref} resultCount={resultCount} />
51
52
  </div>
52
53
 
53
- <Button
54
- variant="ghost"
55
- size="icon"
56
- className={cn(
57
- 'size-8 shrink-0 transition-colors',
58
- isFiltering && 'opacity-40 cursor-not-allowed pointer-events-none'
59
- )}
60
- title={
54
+ <IconActionTooltip
55
+ label={
61
56
  allExpanded
62
57
  ? t('tree.collapseAllShortcut')
63
58
  : t('tree.expandAllShortcut')
64
59
  }
65
- aria-label={allExpanded ? t('tree.collapseAll') : t('tree.expandAll')}
66
- onClick={allExpanded ? collapseAll : expandAll}
67
- disabled={isFiltering}
60
+ asWrapper={isFiltering}
68
61
  >
69
- {allExpanded ? (
70
- <ChevronsDownUp className="size-3.5" />
71
- ) : (
72
- <ChevronsUpDown className="size-3.5" />
73
- )}
74
- </Button>
62
+ <Button
63
+ variant="ghost"
64
+ size="icon"
65
+ className={cn(
66
+ 'size-8 shrink-0 transition-colors',
67
+ isFiltering && 'opacity-40 cursor-not-allowed pointer-events-none'
68
+ )}
69
+ aria-label={
70
+ allExpanded ? t('tree.collapseAll') : t('tree.expandAll')
71
+ }
72
+ onClick={allExpanded ? collapseAll : expandAll}
73
+ disabled={isFiltering}
74
+ >
75
+ {allExpanded ? (
76
+ <ChevronsDownUp className="size-3.5" />
77
+ ) : (
78
+ <ChevronsUpDown className="size-3.5" />
79
+ )}
80
+ </Button>
81
+ </IconActionTooltip>
75
82
 
76
- <Button
77
- variant="ghost"
78
- size="icon"
79
- className="size-8 shrink-0"
80
- title={t('tree.addSession')}
81
- aria-label={t('tree.addSession')}
82
- disabled={createSession.isPending}
83
- onClick={() => createSession.mutate()}
83
+ <IconActionTooltip
84
+ label={t('tree.addSession')}
85
+ asWrapper={createSession.isPending}
84
86
  >
85
- {createSession.isPending ? (
86
- <Loader2 className="size-3.5 animate-spin" />
87
- ) : (
88
- <Plus className="size-3.5" />
89
- )}
90
- </Button>
87
+ <Button
88
+ variant="ghost"
89
+ size="icon"
90
+ className="size-8 shrink-0"
91
+ aria-label={t('tree.addSession')}
92
+ disabled={createSession.isPending}
93
+ onClick={() => createSession.mutate()}
94
+ >
95
+ {createSession.isPending ? (
96
+ <Loader2 className="size-3.5 animate-spin" />
97
+ ) : (
98
+ <Plus className="size-3.5" />
99
+ )}
100
+ </Button>
101
+ </IconActionTooltip>
91
102
  </div>
92
103
 
93
104
  {/* Search results hint */}
@@ -5,7 +5,11 @@ import { SearchX } from 'lucide-react';
5
5
  import { useCallback, useMemo, useRef } from 'react';
6
6
 
7
7
  import { useStructureStore } from './store';
8
- import { buildLessonCountMap, buildVisibleItems } from './tree-helpers';
8
+ import {
9
+ buildLessonCountMap,
10
+ buildSessionIndicatorMap,
11
+ buildVisibleItems,
12
+ } from './tree-helpers';
9
13
  import { TreeRow } from './tree-row';
10
14
  import type { FlatItem } from './types';
11
15
 
@@ -36,6 +40,10 @@ export function CourseTree() {
36
40
  );
37
41
 
38
42
  const lessonCountMap = useMemo(() => buildLessonCountMap(lessons), [lessons]);
43
+ const sessionIndicatorMap = useMemo(
44
+ () => buildSessionIndicatorMap(lessons),
45
+ [lessons]
46
+ );
39
47
 
40
48
  const estimateSize = useCallback(
41
49
  (index: number) => ROW_HEIGHT[items[index]?.type ?? 'lesson'] ?? 32,
@@ -121,6 +129,7 @@ export function CourseTree() {
121
129
  expandedIds.has(item.id) || expandedBySearch.has(item.id)
122
130
  }
123
131
  lessonCountMap={lessonCountMap}
132
+ sessionIndicatorMap={sessionIndicatorMap}
124
133
  visibleItems={items}
125
134
  />
126
135
  </div>
@@ -14,6 +14,9 @@ import {
14
14
  Video,
15
15
  type LucideIcon,
16
16
  } from 'lucide-react';
17
+ import { useEffect, useState } from 'react';
18
+ import { useLmsSettingsQuery } from '../_data/use-lms-settings-query';
19
+ import { useTranscriptionSegmentsQuery } from '../_data/use-transcription-segments';
17
20
  import { useStructureStore } from './store';
18
21
  import type { LessonType } from './types';
19
22
 
@@ -63,6 +66,19 @@ export function DetailLesson({ lessonId }: DetailLessonProps) {
63
66
  const lesson = useStructureStore((s) =>
64
67
  s.lessons.find((l) => l.id === lessonId)
65
68
  );
69
+ const [activeTab, setActiveTab] = useState('dados');
70
+ const lmsSettings = useLmsSettingsQuery();
71
+
72
+ useEffect(() => {
73
+ if (!lmsSettings.transcriptionEnabled && activeTab === 'transcription') {
74
+ setActiveTab('dados');
75
+ }
76
+ }, [lmsSettings.transcriptionEnabled, activeTab]);
77
+
78
+ const { data: segments = [], isLoading: segmentsLoading } =
79
+ useTranscriptionSegmentsQuery(
80
+ activeTab === 'transcription' ? (lesson?.id ?? null) : null
81
+ );
66
82
 
67
83
  if (!lesson) return null;
68
84
 
@@ -97,10 +113,16 @@ export function DetailLesson({ lessonId }: DetailLessonProps) {
97
113
  </div>
98
114
 
99
115
  {/* Tabs */}
100
- <Tabs defaultValue="dados" className="flex flex-col flex-1 min-h-0">
116
+ <Tabs
117
+ value={activeTab}
118
+ onValueChange={setActiveTab}
119
+ className="flex flex-col flex-1 min-h-0"
120
+ >
101
121
  <TabsList className="mx-4 mt-3 w-auto justify-start shrink-0">
102
122
  <TabsTrigger value="dados">Dados</TabsTrigger>
103
- <TabsTrigger value="transcription">Transcrição</TabsTrigger>
123
+ {lmsSettings.transcriptionEnabled && (
124
+ <TabsTrigger value="transcription">Transcrição</TabsTrigger>
125
+ )}
104
126
  <TabsTrigger value="resources">Recursos</TabsTrigger>
105
127
  </TabsList>
106
128
 
@@ -210,15 +232,25 @@ export function DetailLesson({ lessonId }: DetailLessonProps) {
210
232
  </TabsContent>
211
233
 
212
234
  {/* ── Transcrição ───────────────────────────────────────────────────── */}
213
- <TabsContent
235
+ {lmsSettings.transcriptionEnabled && <TabsContent
214
236
  value="transcription"
215
237
  className="flex-1 overflow-y-auto px-4 pb-4 mt-3"
216
238
  >
217
- {lesson.transcription ? (
218
- <div className="rounded-md border bg-muted/20 p-3">
219
- <p className="text-sm leading-relaxed whitespace-pre-wrap font-mono">
220
- {lesson.transcription}
221
- </p>
239
+ {segmentsLoading ? (
240
+ <div className="p-4 text-sm text-muted-foreground">
241
+ Carregando transcrição...
242
+ </div>
243
+ ) : segments.length ? (
244
+ <div className="space-y-1.5 p-3">
245
+ {segments.map((seg) => (
246
+ <div key={seg.id} className="flex gap-3 text-sm">
247
+ <span className="shrink-0 font-mono text-xs text-muted-foreground w-24">
248
+ [{formatSeconds(seg.startSeconds)} –{' '}
249
+ {formatSeconds(seg.endSeconds)}]
250
+ </span>
251
+ <span className="leading-relaxed">{seg.text}</span>
252
+ </div>
253
+ ))}
222
254
  </div>
223
255
  ) : (
224
256
  <EmptyState
@@ -227,7 +259,7 @@ export function DetailLesson({ lessonId }: DetailLessonProps) {
227
259
  Transcrição não disponível para esta aula.
228
260
  </EmptyState>
229
261
  )}
230
- </TabsContent>
262
+ </TabsContent>}
231
263
 
232
264
  {/* ── Recursos ──────────────────────────────────────────────────────── */}
233
265
  <TabsContent
@@ -312,3 +344,13 @@ function EmptyState({
312
344
  </div>
313
345
  );
314
346
  }
347
+
348
+ function formatSeconds(s: number): string {
349
+ const h = Math.floor(s / 3600);
350
+ const m = Math.floor((s % 3600) / 60);
351
+ const sec = Math.floor(s % 60);
352
+
353
+ return h > 0
354
+ ? `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`
355
+ : `${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
356
+ }
@@ -5,6 +5,7 @@ import type { DraggableAttributes } from '@dnd-kit/core';
5
5
  import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
6
6
  import { GripVertical } from 'lucide-react';
7
7
  import { useTranslations } from 'next-intl';
8
+ import { IconActionTooltip } from './icon-action-tooltip';
8
9
 
9
10
  interface DragHandleProps {
10
11
  listeners?: SyntheticListenerMap;
@@ -27,34 +28,36 @@ export function DragHandle({
27
28
  const t = useTranslations('lms.CoursesPage.StructurePage.dragHandle');
28
29
  if (disabled) {
29
30
  return (
31
+ <IconActionTooltip label={t('disabled')}>
32
+ <span
33
+ className={cn(
34
+ 'shrink-0 size-5 flex items-center justify-center opacity-20 cursor-not-allowed',
35
+ className
36
+ )}
37
+ >
38
+ <GripVertical className="size-3.5" />
39
+ </span>
40
+ </IconActionTooltip>
41
+ );
42
+ }
43
+
44
+ return (
45
+ <IconActionTooltip label={t('enabled')}>
30
46
  <span
47
+ {...listeners}
48
+ {...attributes}
31
49
  className={cn(
32
- 'shrink-0 size-5 flex items-center justify-center opacity-20 cursor-not-allowed',
50
+ 'shrink-0 size-5 flex items-center justify-center rounded',
51
+ 'text-muted-foreground/40 hover:text-muted-foreground',
52
+ 'cursor-grab active:cursor-grabbing',
53
+ 'transition-colors touch-none',
33
54
  className
34
55
  )}
35
- title={t('disabled')}
56
+ // Prevent click events from bubbling to the row selection handler
57
+ onClick={(e) => e.stopPropagation()}
36
58
  >
37
59
  <GripVertical className="size-3.5" />
38
60
  </span>
39
- );
40
- }
41
-
42
- return (
43
- <span
44
- {...listeners}
45
- {...attributes}
46
- className={cn(
47
- 'shrink-0 size-5 flex items-center justify-center rounded',
48
- 'text-muted-foreground/40 hover:text-muted-foreground',
49
- 'cursor-grab active:cursor-grabbing',
50
- 'transition-colors touch-none',
51
- className
52
- )}
53
- title={t('enabled')}
54
- // Prevent click events from bubbling to the row selection handler
55
- onClick={(e) => e.stopPropagation()}
56
- >
57
- <GripVertical className="size-3.5" />
58
- </span>
61
+ </IconActionTooltip>
59
62
  );
60
63
  }
@@ -28,6 +28,7 @@ import {
28
28
  import { Separator } from '@/components/ui/separator';
29
29
  import { cn } from '@/lib/utils';
30
30
 
31
+ import { IconActionTooltip } from './icon-action-tooltip';
31
32
  import { useStructureStore } from './store';
32
33
  import type { LessonStatus, Visibility } from './types';
33
34
 
@@ -102,15 +103,17 @@ export function EditorBulk() {
102
103
  {t('types.selected')}
103
104
  </p>
104
105
  </div>
105
- <Button
106
- variant="ghost"
107
- size="icon"
108
- className="size-7 shrink-0"
109
- onClick={clearSelection}
110
- title={t('clearSelection')}
111
- >
112
- <X className="size-3.5" />
113
- </Button>
106
+ <IconActionTooltip label={t('clearSelection')}>
107
+ <Button
108
+ variant="ghost"
109
+ size="icon"
110
+ className="size-7 shrink-0"
111
+ onClick={clearSelection}
112
+ aria-label={t('clearSelection')}
113
+ >
114
+ <X className="size-3.5" />
115
+ </Button>
116
+ </IconActionTooltip>
114
117
  </div>
115
118
 
116
119
  {/* ── Body ─────────────────────────────────────────────────────────── */}