@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
@@ -1,14 +1,21 @@
1
1
  'use client';
2
2
 
3
3
  import { Input } from '@/components/ui/input';
4
+ import {
5
+ Tooltip,
6
+ TooltipContent,
7
+ TooltipTrigger,
8
+ } from '@/components/ui/tooltip';
4
9
  import { cn } from '@/lib/utils';
5
10
  import {
6
11
  ClipboardList,
12
+ Clock3,
7
13
  Eye,
8
14
  EyeOff,
9
15
  FileText,
10
16
  Film,
11
17
  HelpCircle,
18
+ Loader2,
12
19
  Lock,
13
20
  Paperclip,
14
21
  ScrollText,
@@ -17,10 +24,19 @@ import {
17
24
  } from 'lucide-react';
18
25
  import { useTranslations } from 'next-intl';
19
26
  import { useEffect, useRef } from 'react';
27
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
20
28
  import { useUpdateLessonMutation } from '../_data/use-course-structure-mutations';
29
+ import {
30
+ getQueueJob,
31
+ type QueueJobStatus,
32
+ } from '../_data/services/course-structure.service';
21
33
  import { HighlightedText } from './highlighted-text';
22
34
  import { useStructureStore } from './store';
23
35
  import type { Lesson, LessonStatus, LessonType, Visibility } from './types';
36
+ import {
37
+ getLessonAttachedResourceCount,
38
+ getLessonUploadedVideoCount,
39
+ } from './tree-helpers';
24
40
  import { useTreeDisplaySettings } from './use-tree-display-settings';
25
41
 
26
42
  // ── Type configs ──────────────────────────────────────────────────────────────
@@ -79,6 +95,7 @@ export function TreeRowLesson({
79
95
  const t = useTranslations('lms.CoursesPage.StructurePage.lessonRow');
80
96
  const cfg = LESSON_TYPE_CONFIG[data.type];
81
97
  const Icon = cfg.icon;
98
+ const { request } = useApp();
82
99
 
83
100
  const statusCfg = data.status ? STATUS_CONFIG[data.status] : null;
84
101
  const visibilityCfg = data.visibility
@@ -101,6 +118,35 @@ export function TreeRowLesson({
101
118
 
102
119
  const updateLesson = useUpdateLessonMutation();
103
120
 
121
+ const { data: conversionJobStatus } = useQuery<QueueJobStatus | null>({
122
+ queryKey: [
123
+ 'lesson-conversion-job-status',
124
+ data.id,
125
+ data.videoConversionJobId,
126
+ ],
127
+ enabled: data.type === 'video' && Boolean(data.videoConversionJobId),
128
+ queryFn: async () => {
129
+ if (!data.videoConversionJobId) return null;
130
+ const job = await getQueueJob(request, data.videoConversionJobId);
131
+ return job.status;
132
+ },
133
+ refetchInterval: (query) => {
134
+ const status = query.state.data as QueueJobStatus | null | undefined;
135
+ if (!status) return 3000;
136
+
137
+ if (
138
+ status === 'pending' ||
139
+ status === 'scheduled' ||
140
+ status === 'processing' ||
141
+ status === 'retrying'
142
+ ) {
143
+ return 3000;
144
+ }
145
+
146
+ return false;
147
+ },
148
+ });
149
+
104
150
  const isRenaming = inlineRenamingId === data.id;
105
151
  const inputRef = useRef<HTMLInputElement>(null);
106
152
 
@@ -151,6 +197,45 @@ export function TreeRowLesson({
151
197
  durationH > 0
152
198
  ? `${durationH}h${durationM > 0 ? `${durationM}m` : ''}`
153
199
  : `${durationM}m`;
200
+ const attachedResourceCount = getLessonAttachedResourceCount(data);
201
+ const uploadedVideoCount = getLessonUploadedVideoCount(data);
202
+
203
+ const videoJobVisualState =
204
+ data.type !== 'video'
205
+ ? 'none'
206
+ : conversionJobStatus === 'pending' ||
207
+ conversionJobStatus === 'scheduled' ||
208
+ conversionJobStatus === 'retrying'
209
+ ? 'waiting'
210
+ : conversionJobStatus === 'processing'
211
+ ? 'processing'
212
+ : 'none';
213
+
214
+ const typeIconNode =
215
+ data.type !== 'video' ? (
216
+ <Icon className={cn('size-3.5 shrink-0', cfg.color)} aria-hidden />
217
+ ) : videoJobVisualState === 'waiting' ? (
218
+ <span
219
+ title="Conversão aguardando processamento"
220
+ aria-label="Conversão aguardando processamento"
221
+ className="inline-flex items-center"
222
+ >
223
+ <Clock3 className="size-3.5 shrink-0 text-amber-500" aria-hidden />
224
+ </span>
225
+ ) : videoJobVisualState === 'processing' ? (
226
+ <span
227
+ title="Conversão em processamento"
228
+ aria-label="Conversão em processamento"
229
+ className="inline-flex items-center"
230
+ >
231
+ <Loader2
232
+ className="size-3.5 shrink-0 animate-spin text-blue-500"
233
+ aria-hidden
234
+ />
235
+ </span>
236
+ ) : (
237
+ <Video className={cn('size-3.5 shrink-0', cfg.color)} aria-hidden />
238
+ );
154
239
 
155
240
  return (
156
241
  <div
@@ -172,7 +257,7 @@ export function TreeRowLesson({
172
257
  )}
173
258
  >
174
259
  {/* Type icon */}
175
- <Icon className={cn('size-3.5 shrink-0', cfg.color)} aria-hidden />
260
+ {typeIconNode}
176
261
 
177
262
  {/* Inline rename or title */}
178
263
  {isRenaming ? (
@@ -211,8 +296,8 @@ export function TreeRowLesson({
211
296
  const VisIcon = visibilityCfg.icon;
212
297
  return (
213
298
  <span
214
- title={t('visibility', { value: visibilityCfg.label })}
215
- aria-label={t('visibility', { value: visibilityCfg.label })}
299
+ title={t('visibility', { value: visibilityCfg.label })}
300
+ aria-label={t('visibility', { value: visibilityCfg.label })}
216
301
  className="inline-flex items-center"
217
302
  >
218
303
  <VisIcon
@@ -239,20 +324,41 @@ export function TreeRowLesson({
239
324
  <Film className="size-3 shrink-0 text-violet-500" />
240
325
  </span>
241
326
  )}
327
+ {/* Uploaded videos indicator */}
328
+ {showVideoIndicator && uploadedVideoCount > 0 && (
329
+ <Tooltip>
330
+ <TooltipTrigger asChild>
331
+ <span
332
+ aria-label={t('uploadedVideosAria', {
333
+ count: uploadedVideoCount,
334
+ })}
335
+ className="inline-flex items-center"
336
+ >
337
+ <Video className="size-3 shrink-0 text-violet-500" />
338
+ </span>
339
+ </TooltipTrigger>
340
+ <TooltipContent>
341
+ {t('uploadedVideos', { count: uploadedVideoCount })}
342
+ </TooltipContent>
343
+ </Tooltip>
344
+ )}
242
345
  {/* Resources indicator */}
243
- {showResourcesIndicator && data.resources.length > 0 && (
244
- <span
245
- title={t('resources', { count: data.resources.length })}
246
- aria-label={t('resourcesAria', { count: data.resources.length })}
247
- className="inline-flex items-center gap-0.5"
248
- >
249
- <Paperclip className="size-3 shrink-0 text-sky-500" />
250
- {data.resources.length > 1 && (
251
- <span className="text-[0.55rem] text-sky-500 tabular-nums leading-none">
252
- {data.resources.length}
346
+ {showResourcesIndicator && attachedResourceCount > 0 && (
347
+ <Tooltip>
348
+ <TooltipTrigger asChild>
349
+ <span
350
+ aria-label={t('resourcesAria', {
351
+ count: attachedResourceCount,
352
+ })}
353
+ className="inline-flex items-center"
354
+ >
355
+ <Paperclip className="size-3 shrink-0 text-sky-500" />
253
356
  </span>
254
- )}
255
- </span>
357
+ </TooltipTrigger>
358
+ <TooltipContent>
359
+ {t('resources', { count: attachedResourceCount })}
360
+ </TooltipContent>
361
+ </Tooltip>
256
362
  )}
257
363
  {/* Transcription indicator */}
258
364
  {showTranscriptionIndicator &&
@@ -1,18 +1,33 @@
1
1
  'use client';
2
2
 
3
3
  import { Input } from '@/components/ui/input';
4
+ import {
5
+ Tooltip,
6
+ TooltipContent,
7
+ TooltipTrigger,
8
+ } from '@/components/ui/tooltip';
4
9
  import { cn } from '@/lib/utils';
5
- import { ChevronDown, ChevronRight, Layers } from 'lucide-react';
10
+ import {
11
+ ChevronDown,
12
+ ChevronRight,
13
+ Layers,
14
+ Paperclip,
15
+ Video,
16
+ } from 'lucide-react';
6
17
  import { useTranslations } from 'next-intl';
7
18
  import { useEffect, useRef } from 'react';
8
19
  import { useUpdateSessionMutation } from '../_data/use-course-structure-mutations';
9
20
  import { HighlightedText } from './highlighted-text';
21
+ import { IconActionTooltip } from './icon-action-tooltip';
10
22
  import { useStructureStore } from './store';
11
23
  import type { Session } from './types';
24
+ import { useTreeDisplaySettings } from './use-tree-display-settings';
12
25
 
13
26
  interface TreeRowSessionProps {
14
27
  data: Session;
15
28
  lessonCount: number;
29
+ resourceCount: number;
30
+ videoCount: number;
16
31
  isExpanded: boolean;
17
32
  isSelected: boolean;
18
33
  isActive: boolean;
@@ -25,6 +40,8 @@ interface TreeRowSessionProps {
25
40
  export function TreeRowSession({
26
41
  data,
27
42
  lessonCount,
43
+ resourceCount,
44
+ videoCount,
28
45
  isExpanded,
29
46
  isSelected,
30
47
  isActive,
@@ -37,6 +54,7 @@ export function TreeRowSession({
37
54
  const cancelRename = useStructureStore((s) => s.cancelRename);
38
55
  const startRename = useStructureStore((s) => s.startRename);
39
56
  const renameItem = useStructureStore((s) => s.renameItem);
57
+ const { showCode } = useTreeDisplaySettings();
40
58
 
41
59
  const updateSession = useUpdateSessionMutation();
42
60
 
@@ -103,18 +121,20 @@ export function TreeRowSession({
103
121
  )}
104
122
  >
105
123
  {/* Expand/collapse chevron */}
106
- <button
107
- className="shrink-0 text-muted-foreground hover:text-foreground transition-colors p-0.5 rounded focus-visible:ring-1 focus-visible:ring-ring"
108
- onClick={onToggleExpand}
109
- aria-label={isExpanded ? t('collapse') : t('expand')}
110
- tabIndex={-1}
111
- >
112
- {isExpanded ? (
113
- <ChevronDown className="size-3.5" />
114
- ) : (
115
- <ChevronRight className="size-3.5" />
116
- )}
117
- </button>
124
+ <IconActionTooltip label={isExpanded ? t('collapse') : t('expand')}>
125
+ <button
126
+ className="shrink-0 text-muted-foreground hover:text-foreground transition-colors p-0.5 rounded focus-visible:ring-1 focus-visible:ring-ring"
127
+ onClick={onToggleExpand}
128
+ aria-label={isExpanded ? t('collapse') : t('expand')}
129
+ tabIndex={-1}
130
+ >
131
+ {isExpanded ? (
132
+ <ChevronDown className="size-3.5" />
133
+ ) : (
134
+ <ChevronRight className="size-3.5" />
135
+ )}
136
+ </button>
137
+ </IconActionTooltip>
118
138
 
119
139
  {/* Session icon */}
120
140
  <Layers
@@ -142,6 +162,11 @@ export function TreeRowSession({
142
162
  {/* Meta badges (hidden during rename) */}
143
163
  {!isRenaming && (
144
164
  <div className="flex items-center gap-1 shrink-0">
165
+ {showCode && (
166
+ <span className="inline-flex items-center rounded border border-border/60 bg-muted/60 px-1 text-[0.6rem] font-mono text-muted-foreground leading-4">
167
+ {data.code}
168
+ </span>
169
+ )}
145
170
  {/* Duration - show on hover */}
146
171
  {data.duration > 0 && (
147
172
  <span className="text-[0.6rem] text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity tabular-nums">
@@ -149,9 +174,39 @@ export function TreeRowSession({
149
174
  </span>
150
175
  )}
151
176
  {/* Lesson count - always visible */}
152
- <span className="text-[0.62rem] font-medium tabular-nums text-muted-foreground min-w-[1.5rem] text-right">
177
+ <span className="text-[0.62rem] font-medium tabular-nums text-muted-foreground min-w-6 text-right">
153
178
  {lessonCount}
154
179
  </span>
180
+ {videoCount > 0 && (
181
+ <Tooltip>
182
+ <TooltipTrigger asChild>
183
+ <span
184
+ className="inline-flex items-center"
185
+ aria-label={t('uploadedVideosAria', { count: videoCount })}
186
+ >
187
+ <Video className="size-3 shrink-0 text-violet-500" />
188
+ </span>
189
+ </TooltipTrigger>
190
+ <TooltipContent>
191
+ {t('uploadedVideos', { count: videoCount })}
192
+ </TooltipContent>
193
+ </Tooltip>
194
+ )}
195
+ {resourceCount > 0 && (
196
+ <Tooltip>
197
+ <TooltipTrigger asChild>
198
+ <span
199
+ className="inline-flex items-center"
200
+ aria-label={t('resourcesAria', { count: resourceCount })}
201
+ >
202
+ <Paperclip className="size-3 shrink-0 text-sky-500" />
203
+ </span>
204
+ </TooltipTrigger>
205
+ <TooltipContent>
206
+ {t('resources', { count: resourceCount })}
207
+ </TooltipContent>
208
+ </Tooltip>
209
+ )}
155
210
  {/* Published status dot */}
156
211
  <span
157
212
  className={cn(
@@ -17,6 +17,10 @@ interface TreeRowProps {
17
17
  isMatched: boolean;
18
18
  isEffectivelyExpanded: boolean;
19
19
  lessonCountMap: Map<string, number>;
20
+ sessionIndicatorMap: Map<
21
+ string,
22
+ { resourceCount: number; videoCount: number }
23
+ >;
20
24
  /** Full ordered visible list — required for SHIFT-range selection. */
21
25
  visibleItems: FlatItem[];
22
26
  }
@@ -29,6 +33,7 @@ export function TreeRow({
29
33
  isMatched,
30
34
  isEffectivelyExpanded,
31
35
  lessonCountMap,
36
+ sessionIndicatorMap,
32
37
  visibleItems,
33
38
  }: TreeRowProps) {
34
39
  const selectItem = useStructureStore((s) => s.selectItem);
@@ -76,11 +81,17 @@ export function TreeRow({
76
81
 
77
82
  if (item.type === 'session') {
78
83
  const lessonCount = lessonCountMap.get(item.id) ?? 0;
84
+ const indicatorCounts = sessionIndicatorMap.get(item.id) ?? {
85
+ resourceCount: 0,
86
+ videoCount: 0,
87
+ };
79
88
  return (
80
89
  <TreeContextMenu item={item}>
81
90
  <TreeRowSession
82
91
  data={item.data as Session}
83
92
  lessonCount={lessonCount}
93
+ resourceCount={indicatorCounts.resourceCount}
94
+ videoCount={indicatorCounts.videoCount}
84
95
  isExpanded={isEffectivelyExpanded}
85
96
  isSelected={isSelected}
86
97
  isActive={isActive}
@@ -46,16 +46,43 @@ export interface Resource {
46
46
  size: string;
47
47
  type: string;
48
48
  public: boolean;
49
+ uploadedAt?: string;
49
50
  /** Blob URL (for newly uploaded files) or remote URL for preview/download */
50
51
  url?: string;
51
52
  }
52
53
 
54
+ export interface VideoFrame {
55
+ id: string;
56
+ fileId: number;
57
+ name: string;
58
+ timeSeconds: number;
59
+ url?: string;
60
+ }
61
+
53
62
  export interface LessonInstructor {
54
63
  id: string;
55
64
  role?: 'lead' | 'assistant';
56
65
  name?: string;
57
66
  }
58
67
 
68
+ export interface TranscriptionSegment {
69
+ id: number;
70
+ startSeconds: number;
71
+ endSeconds: number;
72
+ text: string;
73
+ }
74
+
75
+ export interface LessonAudioFile {
76
+ id: number;
77
+ fileId: number | null;
78
+ title: string;
79
+ tipo: string;
80
+ localeId: number | null;
81
+ locale: { id: number; name: string; code: string; region: string } | null;
82
+ file: { id: number; filename: string; path: string } | null;
83
+ createdAt: string;
84
+ }
85
+
59
86
  export interface Lesson {
60
87
  id: string;
61
88
  code: string;
@@ -73,6 +100,8 @@ export interface Lesson {
73
100
  videoUrl?: string;
74
101
  autoDuration?: boolean;
75
102
  transcription?: string;
103
+ transcriptionSegments?: TranscriptionSegment[];
104
+ localeId?: number | null;
76
105
  videoConversionJobId?: number;
77
106
  // Questão
78
107
  linkedExam?: string;
@@ -80,6 +109,7 @@ export interface Lesson {
80
109
  postContent?: string;
81
110
  // Relations
82
111
  resources: Resource[];
112
+ frames?: VideoFrame[];
83
113
  instructors?: LessonInstructor[];
84
114
  }
85
115
 
@@ -118,6 +148,7 @@ export type LessonFormValues = {
118
148
  };
119
149
 
120
150
  export type CourseFormValues = {
151
+ code: string;
121
152
  name: string;
122
153
  title: string;
123
154
  description: string;
@@ -30,6 +30,7 @@ import type {
30
30
  Resource,
31
31
  Session,
32
32
  SessionFormValues,
33
+ VideoFrame,
33
34
  } from '../../_components/types';
34
35
 
35
36
  // Alias to keep internal adapter code concise when mapping resources.
@@ -41,6 +42,7 @@ import type {
41
42
  ApiCreateSessionPayload,
42
43
  ApiGetStructureResponse,
43
44
  ApiLesson,
45
+ ApiLessonFrame,
44
46
  ApiLessonResource,
45
47
  ApiSession,
46
48
  ApiUpdateLessonPayload,
@@ -72,18 +74,30 @@ function normalizeVideoProvider(
72
74
 
73
75
  /** Normalize a raw API resource to the frontend Resource shape. */
74
76
  function normalizeResource(raw: ApiLessonResource): Resource {
77
+ const withUpload = raw as ApiLessonResource & {
78
+ createdAt?: string;
79
+ created_at?: string;
80
+ uploadedAt?: string;
81
+ uploaded_at?: string;
82
+ };
83
+
75
84
  return {
76
85
  id: String(raw.id),
77
86
  fileId: raw.fileId ?? undefined,
78
87
  name: raw.nome,
79
- type: raw.tipo ?? '',
88
+ type: raw.type ?? '',
80
89
  /**
81
90
  * ⚠️ BACKEND NOTE: file size is not returned in the API response.
82
91
  * TODO[BACKEND]: Include `size` in the lesson resource response from
83
92
  * CourseStructureService.getLessonById().
84
93
  */
85
94
  size: '',
86
- public: raw.publico,
95
+ public: raw.is_public ?? true,
96
+ uploadedAt:
97
+ withUpload.uploadedAt ??
98
+ withUpload.uploaded_at ??
99
+ withUpload.createdAt ??
100
+ withUpload.created_at,
87
101
  /**
88
102
  * ⚠️ BACKEND NOTE: a pre-signed or static URL is not returned.
89
103
  * TODO[BACKEND]: Include a resolved URL (e.g. `/file/open/:id`) in the
@@ -93,6 +107,16 @@ function normalizeResource(raw: ApiLessonResource): Resource {
93
107
  };
94
108
  }
95
109
 
110
+ function normalizeFrame(raw: ApiLessonFrame): VideoFrame {
111
+ return {
112
+ id: String(raw.id),
113
+ fileId: raw.fileId,
114
+ name: raw.nome,
115
+ timeSeconds: raw.timeSeconds,
116
+ url: raw.url,
117
+ };
118
+ }
119
+
96
120
  // ─────────────────────────────────────────────────────────────────────────────
97
121
  // Backend → Frontend (normalise)
98
122
  // ─────────────────────────────────────────────────────────────────────────────
@@ -164,6 +188,7 @@ export function normalizeLesson(raw: ApiLesson, order = 0): Lesson {
164
188
  videoUrl: raw.videoUrl,
165
189
  autoDuration: raw.duracaoAutomatica,
166
190
  transcription: raw.transcricao,
191
+ localeId: raw.locale_id ?? null,
167
192
  videoConversionJobId: raw.videoConversionJobId,
168
193
  /**
169
194
  * ⚠️ TYPE MISMATCH: backend sends `exameVinculado` as number;
@@ -173,6 +198,7 @@ export function normalizeLesson(raw: ApiLesson, order = 0): Lesson {
173
198
  raw.exameVinculado != null ? String(raw.exameVinculado) : undefined,
174
199
  postContent: raw.conteudoPost,
175
200
  resources: (raw.recursos ?? []).map(normalizeResource),
201
+ frames: (raw.frames ?? []).map(normalizeFrame),
176
202
  instructors,
177
203
  /**
178
204
  * ⚠️ BACKEND NOTE: status and visibility are not in the DB schema.
@@ -305,8 +331,8 @@ export function toCreateLessonPayload(
305
331
  : Number.isInteger(Number(r.id)) && Number(r.id) > 0
306
332
  ? { fileId: Number(r.id) }
307
333
  : {}),
308
- ...(r.type ? { tipo: r.type } : {}),
309
- publico: r.public,
334
+ ...(r.type ? { type: r.type } : {}),
335
+ is_public: r.public,
310
336
  })),
311
337
  }),
312
338
  };
@@ -363,8 +389,8 @@ export function toUpdateLessonPayload(
363
389
  : Number.isInteger(Number(r.id)) && Number(r.id) > 0
364
390
  ? { fileId: Number(r.id) }
365
391
  : {}),
366
- ...(r.type ? { tipo: r.type } : {}),
367
- publico: r.public,
392
+ ...(r.type ? { type: r.type } : {}),
393
+ is_public: r.public,
368
394
  }));
369
395
  }
370
396
  return payload;
@@ -21,9 +21,12 @@
21
21
  */
22
22
 
23
23
  import type {
24
+ ApiCreateLessonFramePayload,
25
+ ApiCreateLessonFrameResponse,
24
26
  ApiCourseResource,
25
27
  ApiCreateLessonPayload,
26
28
  ApiCreateSessionPayload,
29
+ ApiDeleteLessonFrameResponse,
27
30
  ApiDuplicateSessionResponse,
28
31
  ApiGetStructureResponse,
29
32
  ApiLesson,
@@ -32,6 +35,8 @@ import type {
32
35
  ApiPasteLessonsResponse,
33
36
  ApiReorderLessonsPayload,
34
37
  ApiReorderSessionsPayload,
38
+ ApiUpdateLessonFramePayload,
39
+ ApiUpdateLessonFrameResponse,
35
40
  ApiSession,
36
41
  ApiUpdateCoursePayload,
37
42
  ApiUpdateLessonPayload,
@@ -88,6 +93,7 @@ export interface QueueJobEvent {
88
93
  | 'requeued'
89
94
  | 'unlocked';
90
95
  message?: string | null;
96
+ metadata?: Record<string, unknown> | null;
91
97
  created_at: string;
92
98
  }
93
99
 
@@ -101,6 +107,10 @@ export interface QueueJobResponse {
101
107
  finished_at?: string | null;
102
108
  next_retry_at?: string | null;
103
109
  last_error?: string | null;
110
+ result?: {
111
+ transcriptionJobId?: number | null;
112
+ [key: string]: unknown;
113
+ } | null;
104
114
  queue_job_attempt: QueueJobAttempt[];
105
115
  queue_job_event: QueueJobEvent[];
106
116
  }
@@ -191,8 +201,8 @@ export async function updateCourseResources(
191
201
  recursos: Array<{
192
202
  nome: string;
193
203
  fileId?: number;
194
- tipo?: string;
195
- publico?: boolean;
204
+ type?: string;
205
+ is_public?: boolean;
196
206
  }>;
197
207
  }
198
208
  ): Promise<ApiCourseResource[]> {
@@ -337,6 +347,51 @@ export async function deleteLesson(
337
347
  return extractData<{ success: boolean }>(response);
338
348
  }
339
349
 
350
+ export async function deleteLessonFrame(
351
+ request: RequestFn,
352
+ courseId: string | number,
353
+ sessionId: string | number,
354
+ lessonId: string | number,
355
+ frameId: string | number
356
+ ): Promise<ApiDeleteLessonFrameResponse> {
357
+ const response = await request({
358
+ url: `${LESSON_PATH(courseId, sessionId, lessonId)}/frames/${frameId}`,
359
+ method: 'DELETE',
360
+ });
361
+ return extractData<ApiDeleteLessonFrameResponse>(response);
362
+ }
363
+
364
+ export async function createLessonFrame(
365
+ request: RequestFn,
366
+ courseId: string | number,
367
+ sessionId: string | number,
368
+ lessonId: string | number,
369
+ payload: ApiCreateLessonFramePayload
370
+ ): Promise<ApiCreateLessonFrameResponse> {
371
+ const response = await request({
372
+ url: `${LESSON_PATH(courseId, sessionId, lessonId)}/frames`,
373
+ method: 'POST',
374
+ data: payload,
375
+ });
376
+ return extractData<ApiCreateLessonFrameResponse>(response);
377
+ }
378
+
379
+ export async function updateLessonFrame(
380
+ request: RequestFn,
381
+ courseId: string | number,
382
+ sessionId: string | number,
383
+ lessonId: string | number,
384
+ frameId: string | number,
385
+ payload: ApiUpdateLessonFramePayload
386
+ ): Promise<ApiUpdateLessonFrameResponse> {
387
+ const response = await request({
388
+ url: `${LESSON_PATH(courseId, sessionId, lessonId)}/frames/${frameId}`,
389
+ method: 'PATCH',
390
+ data: payload,
391
+ });
392
+ return extractData<ApiUpdateLessonFrameResponse>(response);
393
+ }
394
+
340
395
  // ─────────────────────────────────────────────────────────────────────────────
341
396
  // Pending backend endpoints (stubs — throw until implemented)
342
397
  // ─────────────────────────────────────────────────────────────────────────────
@@ -502,7 +557,6 @@ export async function uploadFile(
502
557
  url: '/file',
503
558
  method: 'POST',
504
559
  data: formData,
505
- headers: { 'Content-Type': 'multipart/form-data' },
506
560
  onUploadProgress: options?.onUploadProgress,
507
561
  });
508
562
  return extractData<{ id: number; filename: string }>(response);