@hed-hog/lms 0.0.350 → 0.0.351

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 (160) hide show
  1. package/dist/certificate/certificate.controller.d.ts +2 -2
  2. package/dist/certificate/certificate.controller.d.ts.map +1 -1
  3. package/dist/certificate/certificate.controller.js +8 -6
  4. package/dist/certificate/certificate.controller.js.map +1 -1
  5. package/dist/certificate/certificate.service.d.ts +5 -2
  6. package/dist/certificate/certificate.service.d.ts.map +1 -1
  7. package/dist/certificate/certificate.service.js +70 -6
  8. package/dist/certificate/certificate.service.js.map +1 -1
  9. package/dist/course/course-structure.controller.d.ts +24 -10
  10. package/dist/course/course-structure.controller.d.ts.map +1 -1
  11. package/dist/course/course-structure.controller.js +23 -2
  12. package/dist/course/course-structure.controller.js.map +1 -1
  13. package/dist/course/course-structure.service.d.ts +16 -8
  14. package/dist/course/course-structure.service.d.ts.map +1 -1
  15. package/dist/course/course-structure.service.js +61 -30
  16. package/dist/course/course-structure.service.js.map +1 -1
  17. package/dist/course/course-video-conversion.service.d.ts +37 -0
  18. package/dist/course/course-video-conversion.service.d.ts.map +1 -0
  19. package/dist/course/course-video-conversion.service.js +308 -0
  20. package/dist/course/course-video-conversion.service.js.map +1 -0
  21. package/dist/course/course.controller.d.ts +17 -0
  22. package/dist/course/course.controller.d.ts.map +1 -1
  23. package/dist/course/course.controller.js +23 -0
  24. package/dist/course/course.controller.js.map +1 -1
  25. package/dist/course/course.module.d.ts.map +1 -1
  26. package/dist/course/course.module.js +15 -2
  27. package/dist/course/course.module.js.map +1 -1
  28. package/dist/course/course.service.d.ts +15 -0
  29. package/dist/course/course.service.d.ts.map +1 -1
  30. package/dist/course/course.service.js +103 -49
  31. package/dist/course/course.service.js.map +1 -1
  32. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +5 -1
  33. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  34. package/dist/course/dto/create-course-structure-lesson.dto.js +16 -2
  35. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  36. package/dist/course/dto/create-course.dto.d.ts +1 -0
  37. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  38. package/dist/course/dto/create-course.dto.js +9 -0
  39. package/dist/course/dto/create-course.dto.js.map +1 -1
  40. package/dist/enterprise/enterprise.controller.d.ts +3 -3
  41. package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
  42. package/dist/enterprise/enterprise.controller.js +0 -1
  43. package/dist/enterprise/enterprise.controller.js.map +1 -1
  44. package/dist/enterprise/enterprise.service.d.ts +3 -3
  45. package/dist/evaluation/evaluation.service.d.ts.map +1 -1
  46. package/dist/evaluation/evaluation.service.js +9 -2
  47. package/dist/evaluation/evaluation.service.js.map +1 -1
  48. package/dist/index.d.ts +1 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +1 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/lms.module.d.ts.map +1 -1
  53. package/dist/lms.module.js +3 -0
  54. package/dist/lms.module.js.map +1 -1
  55. package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.d.ts +6 -0
  56. package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.d.ts.map +1 -0
  57. package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.js +33 -0
  58. package/dist/video-resolution-profile/dto/create-video-resolution-profile.dto.js.map +1 -0
  59. package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.d.ts +6 -0
  60. package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.d.ts.map +1 -0
  61. package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.js +33 -0
  62. package/dist/video-resolution-profile/dto/update-video-resolution-profile.dto.js.map +1 -0
  63. package/dist/video-resolution-profile/video-resolution-profile.controller.d.ts +38 -0
  64. package/dist/video-resolution-profile/video-resolution-profile.controller.d.ts.map +1 -0
  65. package/dist/video-resolution-profile/video-resolution-profile.controller.js +89 -0
  66. package/dist/video-resolution-profile/video-resolution-profile.controller.js.map +1 -0
  67. package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.d.ts +26 -0
  68. package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.d.ts.map +1 -0
  69. package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.js +160 -0
  70. package/dist/video-resolution-profile/video-resolution-profile.mcp-tools.js.map +1 -0
  71. package/dist/video-resolution-profile/video-resolution-profile.module.d.ts +3 -0
  72. package/dist/video-resolution-profile/video-resolution-profile.module.d.ts.map +1 -0
  73. package/dist/video-resolution-profile/video-resolution-profile.module.js +26 -0
  74. package/dist/video-resolution-profile/video-resolution-profile.module.js.map +1 -0
  75. package/dist/video-resolution-profile/video-resolution-profile.service.d.ts +45 -0
  76. package/dist/video-resolution-profile/video-resolution-profile.service.d.ts.map +1 -0
  77. package/dist/video-resolution-profile/video-resolution-profile.service.js +117 -0
  78. package/dist/video-resolution-profile/video-resolution-profile.service.js.map +1 -0
  79. package/hedhog/data/menu.yaml +17 -0
  80. package/hedhog/data/route.yaml +133 -0
  81. package/hedhog/data/video_resolution_profile.yaml +7 -0
  82. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +269 -324
  83. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +124 -70
  84. package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +7 -4
  85. package/hedhog/frontend/app/_components/create-lms-person-sheet.tsx.ejs +2 -2
  86. package/hedhog/frontend/app/_components/create-lms-student-person-sheet.tsx.ejs +2 -2
  87. package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +34 -4
  88. package/hedhog/frontend/app/_lib/editor/types.ts.ejs +28 -3
  89. package/hedhog/frontend/app/achievements/page.tsx.ejs +9 -3
  90. package/hedhog/frontend/app/bitcodes/page.tsx.ejs +9 -3
  91. package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +7 -3
  92. package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +29 -8
  93. package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +14 -0
  94. package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +194 -9
  95. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +15 -5
  96. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +9 -5
  97. package/hedhog/frontend/app/classes/page.tsx.ejs +73 -47
  98. package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +19 -9
  99. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +24 -1
  100. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +1 -1
  101. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +1 -1
  102. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +28 -16
  103. package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +11 -6
  104. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +7 -4
  105. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -0
  106. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +24 -87
  107. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +892 -411
  108. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1004 -293
  109. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +11 -11
  110. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +62 -52
  111. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +2 -0
  112. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +19 -6
  113. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +86 -1
  114. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +3 -0
  115. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +1 -0
  116. package/hedhog/frontend/app/courses/page.tsx.ejs +112 -89
  117. package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +1 -1
  118. package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +10 -3
  119. package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +8 -4
  120. package/hedhog/frontend/app/enterprise/_components/enterprise-person-edit-sheet.tsx.ejs +2 -2
  121. package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +10 -4
  122. package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +10 -3
  123. package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +10 -3
  124. package/hedhog/frontend/app/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +10 -3
  125. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +23 -9
  126. package/hedhog/frontend/app/exams/page.tsx.ejs +14 -6
  127. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +9 -3
  128. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +190 -17
  129. package/hedhog/frontend/app/layout.tsx.ejs +5 -1
  130. package/hedhog/frontend/app/paths/page.tsx.ejs +13 -5
  131. package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +10 -10
  132. package/hedhog/frontend/app/training/page.tsx.ejs +13 -5
  133. package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +607 -0
  134. package/hedhog/frontend/messages/en.json +250 -9
  135. package/hedhog/frontend/messages/pt.json +250 -9
  136. package/hedhog/table/course.yaml +4 -0
  137. package/hedhog/table/course_lesson_file.yaml +8 -0
  138. package/hedhog/table/course_video_resolution_profile.yaml +22 -0
  139. package/hedhog/table/video_resolution_profile.yaml +18 -0
  140. package/package.json +7 -6
  141. package/src/certificate/certificate.controller.ts +19 -14
  142. package/src/certificate/certificate.service.ts +106 -11
  143. package/src/course/course-structure.controller.ts +24 -2
  144. package/src/course/course-structure.service.ts +21 -4
  145. package/src/course/course-video-conversion.service.ts +415 -0
  146. package/src/course/course.controller.ts +18 -0
  147. package/src/course/course.module.ts +15 -2
  148. package/src/course/course.service.ts +72 -2
  149. package/src/course/dto/create-course-structure-lesson.dto.ts +13 -2
  150. package/src/course/dto/create-course.dto.ts +8 -0
  151. package/src/enterprise/enterprise.controller.ts +0 -1
  152. package/src/evaluation/evaluation.service.ts +9 -2
  153. package/src/index.ts +1 -0
  154. package/src/lms.module.ts +3 -0
  155. package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +16 -0
  156. package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +16 -0
  157. package/src/video-resolution-profile/video-resolution-profile.controller.ts +62 -0
  158. package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +128 -0
  159. package/src/video-resolution-profile/video-resolution-profile.module.ts +13 -0
  160. package/src/video-resolution-profile/video-resolution-profile.service.ts +117 -0
@@ -140,9 +140,9 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
140
140
  className="flex flex-col h-full min-h-0"
141
141
  >
142
142
  {/* ── Header ───────────────────────────────────────────────────────── */}
143
- <div className="flex items-center gap-3 px-4 py-3 border-b bg-muted/30 shrink-0">
144
- <div className="flex size-9 items-center justify-center rounded-lg bg-muted shrink-0">
145
- <Layers className="size-4 text-muted-foreground" />
143
+ <div className="flex items-center gap-2 border-b bg-muted/30 px-2 py-2 shrink-0 sm:gap-3 sm:px-4 sm:py-3">
144
+ <div className="flex size-8 items-center justify-center rounded-md bg-muted shrink-0 sm:size-9 sm:rounded-lg">
145
+ <Layers className="size-3.5 text-muted-foreground sm:size-4" />
146
146
  </div>
147
147
  <div className="flex-1 min-w-0">
148
148
  <div className="flex items-center gap-1.5">
@@ -204,7 +204,7 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
204
204
  </CardTitle>
205
205
  </CardHeader>
206
206
  <CardContent className="px-3 pb-2 flex flex-col gap-3">
207
- <div className="grid grid-cols-2 gap-2">
207
+ <div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
208
208
  <FormField
209
209
  control={form.control}
210
210
  name="code"
@@ -280,7 +280,7 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
280
280
  </CardTitle>
281
281
  </CardHeader>
282
282
  <CardContent className="px-3 pb-2">
283
- <div className="grid grid-cols-2 gap-2 items-end">
283
+ <div className="grid grid-cols-1 gap-2 items-end sm:grid-cols-2">
284
284
  <FormField
285
285
  control={form.control}
286
286
  name="visibility"
@@ -342,7 +342,7 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
342
342
 
343
343
  {/* ── Aulas da sessão ───────────────────────────────────────────── */}
344
344
  {lessons.length === 0 && (
345
- <div className="flex flex-col items-center gap-2 py-6 px-2 rounded-lg border border-dashed text-center">
345
+ <div className="flex flex-col items-center gap-2 px-2 py-4 rounded-lg border border-dashed text-center sm:py-6">
346
346
  <Video className="size-5 text-muted-foreground/40" />
347
347
  <p className="text-xs text-muted-foreground leading-relaxed">
348
348
  Esta sessão ainda não tem aulas.
@@ -370,12 +370,12 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
370
370
  {/* ── Footer ───────────────────────────────────────────────────────── */}
371
371
  <div className="shrink-0 border-t bg-background">
372
372
  <Separator />
373
- <div className="flex items-center gap-2 px-3 py-2">
373
+ <div className="flex items-center gap-1.5 px-2 py-1.5 sm:gap-2 sm:px-3 sm:py-2">
374
374
  <Button
375
375
  type="button"
376
376
  variant="ghost"
377
377
  size="sm"
378
- className="h-7 text-xs"
378
+ className="h-6 text-[11px] sm:h-7 sm:text-xs"
379
379
  disabled={!isDirty || updateSession.isPending}
380
380
  onClick={() => form.reset()}
381
381
  >
@@ -387,7 +387,7 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
387
387
  type="button"
388
388
  variant="outline"
389
389
  size="sm"
390
- className="h-7 text-xs"
390
+ className="h-6 text-[11px] sm:h-7 sm:text-xs"
391
391
  disabled={createLesson.isPending}
392
392
  onClick={() => createLesson.mutate({ sessionId })}
393
393
  >
@@ -401,7 +401,7 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
401
401
  <Button
402
402
  type="submit"
403
403
  size="sm"
404
- className="h-7 text-xs"
404
+ className="h-6 text-[11px] sm:h-7 sm:text-xs"
405
405
  disabled={!isDirty || updateSession.isPending}
406
406
  >
407
407
  {updateSession.isPending ? (
@@ -430,7 +430,7 @@ function StatChip({
430
430
  value: string | number;
431
431
  }) {
432
432
  return (
433
- <div className="flex items-center gap-2 rounded-md border bg-muted/20 px-2.5 py-2">
433
+ <div className="flex items-center gap-1.5 rounded-md border bg-muted/20 px-2 py-1.5 sm:gap-2 sm:px-2.5 sm:py-2">
434
434
  <span className="text-muted-foreground shrink-0">{icon}</span>
435
435
  <div className="min-w-0">
436
436
  <p className="text-[0.65rem] text-muted-foreground truncate">{label}</p>
@@ -6,11 +6,11 @@ import { useTranslations } from 'next-intl';
6
6
  import { Badge } from '@/components/ui/badge';
7
7
  import { Button } from '@/components/ui/button';
8
8
  import {
9
- Dialog,
10
- DialogContent,
11
- DialogHeader,
12
- DialogTitle,
13
- } from '@/components/ui/dialog';
9
+ Sheet,
10
+ SheetHeader,
11
+ SheetTitle,
12
+ } from '@/components/ui/sheet';
13
+ import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
14
14
  import { Separator } from '@/components/ui/separator';
15
15
 
16
16
  // ── Data ──────────────────────────────────────────────────────────────────────
@@ -99,56 +99,65 @@ export function ShortcutsHelp({ open, onOpenChange }: ShortcutsHelpProps) {
99
99
  },
100
100
  ];
101
101
  return (
102
- <Dialog open={open} onOpenChange={onOpenChange}>
103
- <DialogContent className="max-w-sm">
104
- <DialogHeader>
105
- <DialogTitle className="flex items-center gap-2">
102
+ <Sheet open={open} onOpenChange={onOpenChange}>
103
+ <ResizableSheetContent
104
+ sheetId="lms-course-structure-shortcuts-help-sheet"
105
+ defaultWidth={520}
106
+ minWidth={400}
107
+ maxWidth={860}
108
+ side="right"
109
+ className="w-full sm:max-w-md p-0 gap-0"
110
+ >
111
+ <SheetHeader className="border-b px-4 py-3">
112
+ <SheetTitle className="flex items-center gap-2">
106
113
  <Keyboard className="size-4" />
107
114
  {t('title')}
108
- </DialogTitle>
109
- </DialogHeader>
115
+ </SheetTitle>
116
+ </SheetHeader>
110
117
 
111
- <div className="flex flex-col gap-4 mt-1">
112
- {shortcutGroups.map((group, gi) => (
113
- <div key={gi}>
114
- <p className="text-[0.65rem] font-semibold uppercase tracking-wider text-muted-foreground mb-2">
115
- {group.heading}
116
- </p>
117
- <div className="flex flex-col gap-1.5">
118
- {group.items.map(({ keys, description }, ki) => (
119
- <div
120
- key={ki}
121
- className="flex items-center justify-between gap-3"
122
- >
123
- <span className="text-sm text-muted-foreground">
124
- {description}
125
- </span>
126
- <div className="flex items-center gap-1 shrink-0">
127
- {keys.map((k, i) => (
128
- <Badge
129
- key={i}
130
- variant="outline"
131
- className="h-5 px-1.5 text-[0.65rem] font-mono font-medium"
132
- >
133
- {k}
134
- </Badge>
135
- ))}
118
+ <div className="flex-1 min-h-0 overflow-y-auto px-4 py-3">
119
+ <div className="flex flex-col gap-4 mt-1">
120
+ {shortcutGroups.map((group, gi) => (
121
+ <div key={gi}>
122
+ <p className="text-[0.65rem] font-semibold uppercase tracking-wider text-muted-foreground mb-2">
123
+ {group.heading}
124
+ </p>
125
+ <div className="flex flex-col gap-1.5">
126
+ {group.items.map(({ keys, description }, ki) => (
127
+ <div
128
+ key={ki}
129
+ className="flex items-center justify-between gap-3"
130
+ >
131
+ <span className="text-sm text-muted-foreground">
132
+ {description}
133
+ </span>
134
+ <div className="flex items-center gap-1 shrink-0">
135
+ {keys.map((k, i) => (
136
+ <Badge
137
+ key={i}
138
+ variant="outline"
139
+ className="h-5 px-1.5 text-[0.65rem] font-mono font-medium"
140
+ >
141
+ {k}
142
+ </Badge>
143
+ ))}
144
+ </div>
136
145
  </div>
137
- </div>
138
- ))}
146
+ ))}
147
+ </div>
148
+ {gi < shortcutGroups.length - 1 && (
149
+ <Separator className="mt-3" />
150
+ )}
139
151
  </div>
140
- {gi < SHORTCUT_GROUPS.length - 1 && (
141
- <Separator className="mt-3" />
142
- )}
143
- </div>
144
- ))}
145
- </div>
152
+ ))}
153
+ </div>
146
154
 
147
- <p className="text-[0.65rem] text-muted-foreground mt-1">
148
- {t('footer')}
149
- </p>
150
- </DialogContent>
151
- </Dialog>
155
+ <p className="text-[0.65rem] text-muted-foreground mt-4">
156
+ {t('footer')}
157
+ </p>
158
+ </div>
159
+ </ResizableSheetContent>
160
+ </Sheet>
152
161
  );
153
162
  }
154
163
 
@@ -161,15 +170,16 @@ export function ShortcutsHelpTrigger({ onOpen }: { onOpen: () => void }) {
161
170
  <Button
162
171
  variant="ghost"
163
172
  size="sm"
164
- className="h-8 gap-1.5 text-xs text-muted-foreground hover:text-foreground"
173
+ className="h-8 gap-1.5 px-2 text-xs text-muted-foreground hover:text-foreground sm:px-3"
165
174
  onClick={onOpen}
166
175
  title={t('triggerTitle')}
176
+ aria-label={t('triggerTitle')}
167
177
  >
168
178
  <Keyboard className="size-3.5" />
169
- {t('triggerLabel')}
179
+ <span className="sr-only sm:not-sr-only">{t('triggerLabel')}</span>
170
180
  <Badge
171
181
  variant="outline"
172
- className="h-4 px-1 text-[0.6rem] font-mono ml-0.5"
182
+ className="ml-0.5 hidden h-4 px-1 text-[0.6rem] font-mono sm:inline-flex"
173
183
  >
174
184
  Ctrl+/
175
185
  </Badge>
@@ -41,6 +41,7 @@ export interface Session {
41
41
 
42
42
  export interface Resource {
43
43
  id: string;
44
+ fileId?: number;
44
45
  name: string;
45
46
  size: string;
46
47
  type: string;
@@ -72,6 +73,7 @@ export interface Lesson {
72
73
  videoUrl?: string;
73
74
  autoDuration?: boolean;
74
75
  transcription?: string;
76
+ videoConversionJobId?: number;
75
77
  // Questão
76
78
  linkedExam?: string;
77
79
  // Post
@@ -74,6 +74,7 @@ function normalizeVideoProvider(
74
74
  function normalizeResource(raw: ApiLessonResource): Resource {
75
75
  return {
76
76
  id: String(raw.id),
77
+ fileId: raw.fileId ?? undefined,
77
78
  name: raw.nome,
78
79
  type: raw.tipo ?? '',
79
80
  /**
@@ -163,6 +164,7 @@ export function normalizeLesson(raw: ApiLesson, order = 0): Lesson {
163
164
  videoUrl: raw.videoUrl,
164
165
  autoDuration: raw.duracaoAutomatica,
165
166
  transcription: raw.transcricao,
167
+ videoConversionJobId: raw.videoConversionJobId,
166
168
  /**
167
169
  * ⚠️ TYPE MISMATCH: backend sends `exameVinculado` as number;
168
170
  * frontend uses `linkedExam` as string. Converted with String().
@@ -265,6 +267,7 @@ export function toUpdateSessionPayload(
265
267
  export function toCreateLessonPayload(
266
268
  data: LessonFormValues & {
267
269
  transcription?: string;
270
+ videoConversionJobId?: number;
268
271
  instructorIds?: number[];
269
272
  resources?: LessonResourceInput[];
270
273
  }
@@ -285,6 +288,9 @@ export function toCreateLessonPayload(
285
288
  duracaoAutomatica: data.autoDuration,
286
289
  }),
287
290
  ...(data.transcription && { transcricao: data.transcription }),
291
+ ...(data.videoConversionJobId !== undefined && {
292
+ videoConversionJobId: data.videoConversionJobId,
293
+ }),
288
294
  // ⚠️ TYPE MISMATCH: frontend stores linkedExam as string; backend expects number.
289
295
  ...(data.linkedExam && { exameVinculado: Number(data.linkedExam) }),
290
296
  ...(data.postContent && { conteudoPost: data.postContent }),
@@ -294,9 +300,11 @@ export function toCreateLessonPayload(
294
300
  recursos: data.resources.map((r) => ({
295
301
  nome: r.name,
296
302
  // Only include fileId when the id is a valid server-assigned integer.
297
- ...(Number.isInteger(Number(r.id)) && Number(r.id) > 0
298
- ? { fileId: Number(r.id) }
299
- : {}),
303
+ ...(Number.isInteger(r.fileId ?? Number.NaN) && (r.fileId ?? 0) > 0
304
+ ? { fileId: r.fileId }
305
+ : Number.isInteger(Number(r.id)) && Number(r.id) > 0
306
+ ? { fileId: Number(r.id) }
307
+ : {}),
300
308
  ...(r.type ? { tipo: r.type } : {}),
301
309
  publico: r.public,
302
310
  })),
@@ -312,6 +320,7 @@ export function toUpdateLessonPayload(
312
320
  data: Partial<
313
321
  LessonFormValues & {
314
322
  transcription?: string;
323
+ videoConversionJobId?: number;
315
324
  instructorIds?: number[];
316
325
  resources?: LessonResourceInput[];
317
326
  }
@@ -333,6 +342,8 @@ export function toUpdateLessonPayload(
333
342
  payload.duracaoAutomatica = data.autoDuration;
334
343
  if (data.transcription !== undefined)
335
344
  payload.transcricao = data.transcription;
345
+ if (data.videoConversionJobId !== undefined)
346
+ payload.videoConversionJobId = data.videoConversionJobId;
336
347
  if (data.postContent !== undefined) payload.conteudoPost = data.postContent;
337
348
  if (data.code !== undefined) payload.codigo = data.code;
338
349
  if (data.instructorIds !== undefined)
@@ -347,9 +358,11 @@ export function toUpdateLessonPayload(
347
358
  if (data.resources !== undefined) {
348
359
  payload.recursos = data.resources.map((r) => ({
349
360
  nome: r.name,
350
- ...(Number.isInteger(Number(r.id)) && Number(r.id) > 0
351
- ? { fileId: Number(r.id) }
352
- : {}),
361
+ ...(Number.isInteger(r.fileId ?? Number.NaN) && (r.fileId ?? 0) > 0
362
+ ? { fileId: r.fileId }
363
+ : Number.isInteger(Number(r.id)) && Number(r.id) > 0
364
+ ? { fileId: Number(r.id) }
365
+ : {}),
353
366
  ...(r.type ? { tipo: r.type } : {}),
354
367
  publico: r.public,
355
368
  }));
@@ -48,8 +48,63 @@ type RequestFn = (input: {
48
48
  method: string;
49
49
  data?: unknown;
50
50
  headers?: Record<string, string>;
51
+ onUploadProgress?: (event: { loaded: number; total?: number }) => void;
51
52
  }) => Promise<{ data: unknown }>;
52
53
 
54
+ export type QueueJobStatus =
55
+ | 'pending'
56
+ | 'scheduled'
57
+ | 'processing'
58
+ | 'completed'
59
+ | 'failed'
60
+ | 'retrying'
61
+ | 'canceled'
62
+ | 'dead_letter';
63
+
64
+ export interface QueueJobAttempt {
65
+ id: number;
66
+ attempt_number: number;
67
+ status: 'processing' | 'completed' | 'failed' | 'canceled';
68
+ started_at: string;
69
+ finished_at?: string | null;
70
+ duration_ms?: number | null;
71
+ worker_id?: string | null;
72
+ error_message?: string | null;
73
+ created_at: string;
74
+ }
75
+
76
+ export interface QueueJobEvent {
77
+ id: number;
78
+ event_type:
79
+ | 'created'
80
+ | 'scheduled'
81
+ | 'locked'
82
+ | 'started'
83
+ | 'completed'
84
+ | 'failed'
85
+ | 'retry_scheduled'
86
+ | 'canceled'
87
+ | 'moved_to_dead_letter'
88
+ | 'requeued'
89
+ | 'unlocked';
90
+ message?: string | null;
91
+ created_at: string;
92
+ }
93
+
94
+ export interface QueueJobResponse {
95
+ id: number;
96
+ status: QueueJobStatus;
97
+ attempts: number;
98
+ max_attempts: number;
99
+ created_at: string;
100
+ started_at?: string | null;
101
+ finished_at?: string | null;
102
+ next_retry_at?: string | null;
103
+ last_error?: string | null;
104
+ queue_job_attempt: QueueJobAttempt[];
105
+ queue_job_event: QueueJobEvent[];
106
+ }
107
+
53
108
  // ─────────────────────────────────────────────────────────────────────────────
54
109
  // URL helpers
55
110
  // ─────────────────────────────────────────────────────────────────────────────
@@ -434,7 +489,10 @@ export async function pasteLessons(
434
489
  export async function uploadFile(
435
490
  request: RequestFn,
436
491
  file: File,
437
- destination = 'lms/lessons'
492
+ destination = 'lms/lessons',
493
+ options?: {
494
+ onUploadProgress?: (event: { loaded: number; total?: number }) => void;
495
+ }
438
496
  ): Promise<{ id: number; filename: string }> {
439
497
  const formData = new FormData();
440
498
  formData.append('file', file);
@@ -445,10 +503,37 @@ export async function uploadFile(
445
503
  method: 'POST',
446
504
  data: formData,
447
505
  headers: { 'Content-Type': 'multipart/form-data' },
506
+ onUploadProgress: options?.onUploadProgress,
448
507
  });
449
508
  return extractData<{ id: number; filename: string }>(response);
450
509
  }
451
510
 
511
+ export async function enqueueLessonVideoConversion(
512
+ request: RequestFn,
513
+ courseId: string | number,
514
+ sessionId: string | number,
515
+ lessonId: string | number,
516
+ originalFileId: number
517
+ ): Promise<{ queueJobId: number; status: 'queued' }> {
518
+ const response = await request({
519
+ url: `${LESSONS_PATH(courseId, sessionId)}/${lessonId}/video-conversions`,
520
+ method: 'POST',
521
+ data: { originalFileId },
522
+ });
523
+ return extractData<{ queueJobId: number; status: 'queued' }>(response);
524
+ }
525
+
526
+ export async function getQueueJob(
527
+ request: RequestFn,
528
+ jobId: number
529
+ ): Promise<QueueJobResponse> {
530
+ const response = await request({
531
+ url: `/queue/jobs/${jobId}`,
532
+ method: 'GET',
533
+ });
534
+ return extractData<QueueJobResponse>(response);
535
+ }
536
+
452
537
  /**
453
538
  * DELETE /file
454
539
  *
@@ -20,6 +20,7 @@
20
20
  export interface ApiLessonResource {
21
21
  id: number;
22
22
  nome: string;
23
+ fileId?: number | null;
23
24
  /** MIME category or custom type label; may be null. */
24
25
  tipo: string | null;
25
26
  publico: boolean;
@@ -76,6 +77,7 @@ export interface ApiLesson {
76
77
  videoUrl?: string;
77
78
  duracaoAutomatica?: boolean;
78
79
  transcricao?: string;
80
+ videoConversionJobId?: number;
79
81
  /**
80
82
  * ID of the linked exam (when tipo === 'questao').
81
83
  * ⚠️ TYPE MISMATCH: backend sends number; frontend stores as string.
@@ -188,6 +190,7 @@ export interface ApiCreateLessonPayload {
188
190
  videoUrl?: string;
189
191
  duracaoAutomatica?: boolean;
190
192
  transcricao?: string;
193
+ videoConversionJobId?: number;
191
194
  /** ID of the linked exam. ⚠️ TYPE MISMATCH: backend expects number; frontend stores string. */
192
195
  exameVinculado?: number;
193
196
  conteudoPost?: string;
@@ -297,6 +297,7 @@ interface UpdateLessonVars {
297
297
  formValues: Partial<
298
298
  LessonFormValues & {
299
299
  transcription?: string;
300
+ videoConversionJobId?: number;
300
301
  resources?: Resource[];
301
302
  instructorIds?: number[];
302
303
  }