@hed-hog/lms 0.0.364 → 0.0.365

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +1 -0
  2. package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
  3. package/dist/bitcode-wallet/bitcode-wallet.service.js +22 -3
  4. package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
  5. package/dist/course/course-export-scorm12-worker.service.d.ts +21 -0
  6. package/dist/course/course-export-scorm12-worker.service.d.ts.map +1 -0
  7. package/dist/course/course-export-scorm12-worker.service.js +109 -0
  8. package/dist/course/course-export-scorm12-worker.service.js.map +1 -0
  9. package/dist/course/course-export-scorm12.service.d.ts +42 -0
  10. package/dist/course/course-export-scorm12.service.d.ts.map +1 -0
  11. package/dist/course/course-export-scorm12.service.js +628 -0
  12. package/dist/course/course-export-scorm12.service.js.map +1 -0
  13. package/dist/course/course-export.service.d.ts +84 -0
  14. package/dist/course/course-export.service.d.ts.map +1 -0
  15. package/dist/course/course-export.service.js +237 -0
  16. package/dist/course/course-export.service.js.map +1 -0
  17. package/dist/course/course-structure.controller.d.ts +17 -9
  18. package/dist/course/course-structure.controller.d.ts.map +1 -1
  19. package/dist/course/course-structure.controller.js +17 -4
  20. package/dist/course/course-structure.controller.js.map +1 -1
  21. package/dist/course/course-structure.service.d.ts +12 -4
  22. package/dist/course/course-structure.service.d.ts.map +1 -1
  23. package/dist/course/course-structure.service.js +98 -23
  24. package/dist/course/course-structure.service.js.map +1 -1
  25. package/dist/course/course-video-hls.service.d.ts +57 -0
  26. package/dist/course/course-video-hls.service.d.ts.map +1 -0
  27. package/dist/course/course-video-hls.service.js +767 -0
  28. package/dist/course/course-video-hls.service.js.map +1 -0
  29. package/dist/course/course.controller.d.ts +45 -13
  30. package/dist/course/course.controller.d.ts.map +1 -1
  31. package/dist/course/course.controller.js +40 -26
  32. package/dist/course/course.controller.js.map +1 -1
  33. package/dist/course/course.mcp-tools.js +1 -1
  34. package/dist/course/course.mcp-tools.js.map +1 -1
  35. package/dist/course/course.module.d.ts.map +1 -1
  36. package/dist/course/course.module.js +11 -0
  37. package/dist/course/course.module.js.map +1 -1
  38. package/dist/course/course.service.d.ts +6 -9
  39. package/dist/course/course.service.d.ts.map +1 -1
  40. package/dist/course/course.service.js +57 -48
  41. package/dist/course/course.service.js.map +1 -1
  42. package/dist/course/dto/cleanup-course-storage.dto.d.ts +1 -1
  43. package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -1
  44. package/dist/course/dto/cleanup-course-storage.dto.js +1 -0
  45. package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -1
  46. package/dist/course/dto/cleanup-upload-history.dto.d.ts +1 -1
  47. package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -1
  48. package/dist/course/dto/cleanup-upload-history.dto.js +1 -1
  49. package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -1
  50. package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
  51. package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
  52. package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
  53. package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
  54. package/dist/course/dto/create-course-export.dto.d.ts +14 -0
  55. package/dist/course/dto/create-course-export.dto.d.ts.map +1 -0
  56. package/dist/course/dto/create-course-export.dto.js +71 -0
  57. package/dist/course/dto/create-course-export.dto.js.map +1 -0
  58. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +2 -2
  59. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  60. package/dist/course/dto/create-course-structure-lesson.dto.js +3 -2
  61. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  62. package/dist/course/lms-bulk-upload-automation.service.d.ts +16 -1
  63. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
  64. package/dist/course/lms-bulk-upload-automation.service.js +102 -8
  65. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
  66. package/dist/course/lms-bulk-upload-infra.service.d.ts +1 -0
  67. package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -1
  68. package/dist/course/lms-bulk-upload-infra.service.js +32 -8
  69. package/dist/course/lms-bulk-upload-infra.service.js.map +1 -1
  70. package/dist/course/lms-bulk-upload.controller.d.ts +30 -3
  71. package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
  72. package/dist/course/lms-bulk-upload.controller.js +43 -2
  73. package/dist/course/lms-bulk-upload.controller.js.map +1 -1
  74. package/dist/course/lms-bulk-upload.service.d.ts +11 -0
  75. package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
  76. package/dist/course/lms-bulk-upload.service.js +59 -6
  77. package/dist/course/lms-bulk-upload.service.js.map +1 -1
  78. package/dist/course/lms-setting.controller.d.ts +2 -1
  79. package/dist/course/lms-setting.controller.d.ts.map +1 -1
  80. package/dist/course/lms-setting.controller.js +4 -2
  81. package/dist/course/lms-setting.controller.js.map +1 -1
  82. package/dist/course/scorm12-schemas.d.ts +4 -0
  83. package/dist/course/scorm12-schemas.d.ts.map +1 -0
  84. package/dist/course/scorm12-schemas.js +9 -0
  85. package/dist/course/scorm12-schemas.js.map +1 -0
  86. package/dist/enterprise/training/training-student.service.d.ts +51 -0
  87. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  88. package/dist/enterprise/training/training-student.service.js +217 -4
  89. package/dist/enterprise/training/training-student.service.js.map +1 -1
  90. package/dist/evaluation/evaluation.service.d.ts +18 -0
  91. package/dist/evaluation/evaluation.service.d.ts.map +1 -1
  92. package/dist/evaluation/evaluation.service.js +125 -0
  93. package/dist/evaluation/evaluation.service.js.map +1 -1
  94. package/dist/exam/dto/create-standalone-question.dto.d.ts +12 -0
  95. package/dist/exam/dto/create-standalone-question.dto.d.ts.map +1 -0
  96. package/dist/exam/dto/create-standalone-question.dto.js +70 -0
  97. package/dist/exam/dto/create-standalone-question.dto.js.map +1 -0
  98. package/dist/exam/exam.module.d.ts.map +1 -1
  99. package/dist/exam/exam.module.js +2 -1
  100. package/dist/exam/exam.module.js.map +1 -1
  101. package/dist/exam/exam.service.d.ts +21 -0
  102. package/dist/exam/exam.service.d.ts.map +1 -1
  103. package/dist/exam/exam.service.js +80 -0
  104. package/dist/exam/exam.service.js.map +1 -1
  105. package/dist/exam/question.controller.d.ts +27 -0
  106. package/dist/exam/question.controller.d.ts.map +1 -0
  107. package/dist/exam/question.controller.js +53 -0
  108. package/dist/exam/question.controller.js.map +1 -0
  109. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +4 -0
  110. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
  111. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +161 -25
  112. package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
  113. package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
  114. package/dist/lms-commerce-access.subscriber.d.ts +11 -0
  115. package/dist/lms-commerce-access.subscriber.d.ts.map +1 -0
  116. package/dist/lms-commerce-access.subscriber.js +74 -0
  117. package/dist/lms-commerce-access.subscriber.js.map +1 -0
  118. package/dist/lms.module.d.ts.map +1 -1
  119. package/dist/lms.module.js +6 -5
  120. package/dist/lms.module.js.map +1 -1
  121. package/dist/platforma/platforma-video.service.d.ts +39 -0
  122. package/dist/platforma/platforma-video.service.d.ts.map +1 -0
  123. package/dist/platforma/platforma-video.service.js +301 -0
  124. package/dist/platforma/platforma-video.service.js.map +1 -0
  125. package/dist/platforma/platforma.controller.d.ts +95 -1
  126. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  127. package/dist/platforma/platforma.controller.js +160 -2
  128. package/dist/platforma/platforma.controller.js.map +1 -1
  129. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts +5 -0
  130. package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts.map +1 -0
  131. package/dist/student-xp/dto/grant-skill-card-xp.dto.js +26 -0
  132. package/dist/student-xp/dto/grant-skill-card-xp.dto.js.map +1 -0
  133. package/dist/student-xp/student-xp.controller.d.ts +15 -0
  134. package/dist/student-xp/student-xp.controller.d.ts.map +1 -1
  135. package/dist/student-xp/student-xp.controller.js +24 -0
  136. package/dist/student-xp/student-xp.controller.js.map +1 -1
  137. package/dist/student-xp/student-xp.service.d.ts +16 -0
  138. package/dist/student-xp/student-xp.service.d.ts.map +1 -1
  139. package/dist/student-xp/student-xp.service.js +51 -1
  140. package/dist/student-xp/student-xp.service.js.map +1 -1
  141. package/hedhog/data/evaluation_topic.yaml +17 -0
  142. package/hedhog/data/menu.yaml +0 -17
  143. package/hedhog/data/queue_definition.yaml +48 -0
  144. package/hedhog/data/route.yaml +94 -124
  145. package/hedhog/data/setting_group.yaml +19 -19
  146. package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +337 -41
  147. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +69 -4
  148. package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +420 -0
  149. package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +308 -0
  150. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +17 -15
  151. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -63
  152. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +8 -3
  153. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +31 -8
  154. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +16 -9
  155. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +201 -401
  156. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +378 -690
  157. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -2
  158. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +3 -9
  159. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -1
  160. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +6 -10
  161. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +49 -0
  162. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -3
  163. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +0 -1
  164. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +106 -0
  165. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +28 -1
  166. package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +0 -2
  167. package/hedhog/frontend/app/courses/page.tsx.ejs +45 -0
  168. package/hedhog/frontend/messages/en.json +26 -28
  169. package/hedhog/frontend/messages/pt.json +26 -28
  170. package/hedhog/table/course_export.yaml +62 -0
  171. package/package.json +13 -9
  172. package/src/bitcode-wallet/bitcode-wallet.service.ts +43 -4
  173. package/src/course/course-export-scorm12-worker.service.ts +124 -0
  174. package/src/course/course-export-scorm12.service.ts +668 -0
  175. package/src/course/course-export.service.ts +280 -0
  176. package/src/course/course-structure.controller.ts +14 -2
  177. package/src/course/course-structure.service.ts +100 -7
  178. package/src/course/course-video-hls.service.ts +946 -0
  179. package/src/course/course.controller.ts +33 -19
  180. package/src/course/course.mcp-tools.ts +1 -1
  181. package/src/course/course.module.ts +11 -0
  182. package/src/course/course.service.ts +73 -60
  183. package/src/course/dto/cleanup-course-storage.dto.ts +1 -0
  184. package/src/course/dto/cleanup-upload-history.dto.ts +1 -1
  185. package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
  186. package/src/course/dto/create-course-export.dto.ts +56 -0
  187. package/src/course/dto/create-course-structure-lesson.dto.ts +4 -3
  188. package/src/course/lms-bulk-upload-automation.service.ts +153 -6
  189. package/src/course/lms-bulk-upload-infra.service.ts +39 -6
  190. package/src/course/lms-bulk-upload.controller.ts +32 -2
  191. package/src/course/lms-bulk-upload.service.ts +70 -7
  192. package/src/course/lms-setting.controller.ts +4 -2
  193. package/src/course/scorm12-schemas.ts +9 -0
  194. package/src/enterprise/training/training-student.service.ts +221 -2
  195. package/src/evaluation/evaluation.service.ts +123 -0
  196. package/src/exam/dto/create-standalone-question.dto.ts +66 -0
  197. package/src/exam/exam.module.ts +2 -1
  198. package/src/exam/exam.service.ts +86 -0
  199. package/src/exam/question.controller.ts +28 -0
  200. package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +205 -31
  201. package/src/lms-commerce-access.subscriber.ts +88 -0
  202. package/src/lms.module.ts +6 -5
  203. package/src/platforma/platforma-video.service.ts +346 -0
  204. package/src/platforma/platforma.controller.ts +95 -1
  205. package/src/platforma/platforma.service.ts +268 -268
  206. package/src/student-xp/dto/grant-skill-card-xp.dto.ts +10 -0
  207. package/src/student-xp/student-xp.controller.ts +18 -2
  208. package/src/student-xp/student-xp.service.ts +84 -2
  209. package/hedhog/data/video_resolution_profile.yaml +0 -7
  210. package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +0 -607
  211. package/hedhog/table/course_video_resolution_profile.yaml +0 -22
  212. package/hedhog/table/video_resolution_profile.yaml +0 -18
  213. package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +0 -16
  214. package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +0 -16
  215. package/src/video-resolution-profile/video-resolution-profile.controller.ts +0 -62
  216. package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +0 -128
  217. package/src/video-resolution-profile/video-resolution-profile.module.ts +0 -13
  218. package/src/video-resolution-profile/video-resolution-profile.service.ts +0 -117
@@ -1,7 +0,0 @@
1
- - name: 1080p Full HD
2
- ffmpeg_params: '-vf "yadif=mode=0,scale=1920:1080:force_original_aspect_ratio=decrease,pad=1920:1080:(ow-iw)/2:(oh-ih)/2" -c:v libx264 -preset slower -profile:v high -level:v 4.0 -crf 23 -x264-params "aq-mode=2:aq-strength=1.0" -fps_mode cfr -movflags +faststart -c:a aac -ac 2'
3
- status: active
4
-
5
- - name: 720p HD
6
- ffmpeg_params: '-vf "yadif=mode=0,scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2" -c:v libx264 -preset slower -profile:v high -level:v 4.0 -crf 23 -x264-params "aq-mode=2:aq-strength=1.0" -fps_mode cfr -movflags +faststart -c:a aac -ac 2'
7
- status: active
@@ -1,607 +0,0 @@
1
- 'use client';
2
-
3
- import {
4
- EmptyState,
5
- Page,
6
- PageHeader,
7
- PaginationFooter,
8
- SearchBar,
9
- } from '@/components/entity-list';
10
- import { FfmpegParamsEditor } from '@/components/ffmpeg-params-editor';
11
- import {
12
- AlertDialog,
13
- AlertDialogAction,
14
- AlertDialogCancel,
15
- AlertDialogContent,
16
- AlertDialogDescription,
17
- AlertDialogHeader,
18
- AlertDialogTitle,
19
- } from '@/components/ui/alert-dialog';
20
- import { Badge } from '@/components/ui/badge';
21
- import { Button } from '@/components/ui/button';
22
- import {
23
- Form,
24
- FormControl,
25
- FormField,
26
- FormItem,
27
- FormLabel,
28
- FormMessage,
29
- } from '@/components/ui/form';
30
- import { Input } from '@/components/ui/input';
31
- import {
32
- Select,
33
- SelectContent,
34
- SelectItem,
35
- SelectTrigger,
36
- SelectValue,
37
- } from '@/components/ui/select';
38
- import {
39
- Sheet,
40
- SheetHeader,
41
- SheetTitle,
42
- } from '@/components/ui/sheet';
43
- import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
44
- import { Skeleton } from '@/components/ui/skeleton';
45
- import {
46
- Table,
47
- TableBody,
48
- TableCell,
49
- TableHead,
50
- TableHeader,
51
- TableRow,
52
- } from '@/components/ui/table';
53
- import {
54
- Tooltip,
55
- TooltipContent,
56
- TooltipTrigger,
57
- } from '@/components/ui/tooltip';
58
- import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
59
- import { cn } from '@/lib/utils';
60
- import { useApp, useQuery } from '@hed-hog/next-app-provider';
61
- import { zodResolver } from '@hookform/resolvers/zod';
62
- import {
63
- Film,
64
- PauseCircle,
65
- Pencil,
66
- PlayCircle,
67
- Plus,
68
- Trash2,
69
- } from 'lucide-react';
70
- import { useTranslations } from 'next-intl';
71
- import { useEffect, useState } from 'react';
72
- import { useForm } from 'react-hook-form';
73
- import { toast } from 'sonner';
74
- import * as z from 'zod';
75
-
76
- // ─── Types ────────────────────────────────────────────────────────────────────
77
-
78
- type VideoResolutionProfile = {
79
- id: number;
80
- name: string;
81
- ffmpeg_params: string;
82
- status: 'active' | 'inactive';
83
- };
84
-
85
- type ProfilePaginatedResult = {
86
- data: VideoResolutionProfile[];
87
- total: number;
88
- page: number;
89
- pageSize: number;
90
- lastPage?: number;
91
- };
92
-
93
- // ─── Zod schema ───────────────────────────────────────────────────────────────
94
-
95
- const createSchema = (t: ReturnType<typeof useTranslations>) =>
96
- z.object({
97
- name: z
98
- .string()
99
- .min(1, t('form.validation.required'))
100
- .max(100, t('form.validation.max100')),
101
- ffmpeg_params: z.string().min(1, t('form.validation.required')),
102
- status: z.enum(['active', 'inactive']),
103
- });
104
-
105
- type ProfileFormValues = z.infer<ReturnType<typeof createSchema>>;
106
-
107
- // ─── ProfileFormSheet ──────────────────────────────────────────────────────────
108
-
109
- interface ProfileFormSheetProps {
110
- open: boolean;
111
- onOpenChange: (open: boolean) => void;
112
- profileToEdit: VideoResolutionProfile | null;
113
- onSaved: () => void;
114
- }
115
-
116
- function ProfileFormSheet({
117
- open,
118
- onOpenChange,
119
- profileToEdit,
120
- onSaved,
121
- }: ProfileFormSheetProps) {
122
- const { request } = useApp();
123
- const t = useTranslations('lms.VideoResolutionProfilesPage');
124
- const [isSaving, setIsSaving] = useState(false);
125
-
126
- const form = useForm<ProfileFormValues>({
127
- resolver: zodResolver(createSchema(t)),
128
- defaultValues: { name: '', ffmpeg_params: '', status: 'active' },
129
- });
130
-
131
- useEffect(() => {
132
- if (open) {
133
- if (profileToEdit) {
134
- form.reset({
135
- name: profileToEdit.name,
136
- ffmpeg_params: profileToEdit.ffmpeg_params,
137
- status: profileToEdit.status,
138
- });
139
- } else {
140
- form.reset({ name: '', ffmpeg_params: '', status: 'active' });
141
- }
142
- }
143
- }, [open, profileToEdit, form]);
144
-
145
- const onSubmit = async (values: ProfileFormValues) => {
146
- try {
147
- setIsSaving(true);
148
- if (profileToEdit) {
149
- await request({
150
- url: `/lms/video-resolution-profiles/${profileToEdit.id}`,
151
- method: 'PATCH',
152
- data: values,
153
- });
154
- toast.success(t('messages.updateSuccess'));
155
- } else {
156
- await request({
157
- url: '/lms/video-resolution-profiles',
158
- method: 'POST',
159
- data: values,
160
- });
161
- toast.success(t('messages.createSuccess'));
162
- }
163
- onSaved();
164
- onOpenChange(false);
165
- } catch {
166
- toast.error(t('messages.saveError'));
167
- } finally {
168
- setIsSaving(false);
169
- }
170
- };
171
-
172
- return (
173
- <Sheet open={open} onOpenChange={onOpenChange}>
174
- <ResizableSheetContent
175
- sheetId="lms-video-resolution-profiles-form-sheet"
176
- defaultWidth={560}
177
- minWidth={420}
178
- maxWidth={920}
179
- className="w-full overflow-y-auto sm:max-w-lg"
180
- >
181
- <SheetHeader>
182
- <SheetTitle>
183
- {profileToEdit ? t('sheet.editTitle') : t('sheet.createTitle')}
184
- </SheetTitle>
185
- </SheetHeader>
186
-
187
- <Form {...form}>
188
- <form
189
- onSubmit={form.handleSubmit(onSubmit)}
190
- className="flex flex-col gap-4 px-4 py-4"
191
- >
192
- <div className="grid grid-cols-2 gap-4">
193
- <FormField
194
- control={form.control}
195
- name="name"
196
- render={({ field }) => (
197
- <FormItem>
198
- <FormLabel>{t('form.name')}</FormLabel>
199
- <FormControl>
200
- <Input
201
- placeholder={t('form.namePlaceholder')}
202
- {...field}
203
- />
204
- </FormControl>
205
- <FormMessage />
206
- </FormItem>
207
- )}
208
- />
209
-
210
- <FormField
211
- control={form.control}
212
- name="status"
213
- render={({ field }) => (
214
- <FormItem>
215
- <FormLabel>{t('form.status')}</FormLabel>
216
- <Select onValueChange={field.onChange} value={field.value}>
217
- <FormControl>
218
- <SelectTrigger className="w-full">
219
- <SelectValue
220
- placeholder={t('form.statusPlaceholder')}
221
- />
222
- </SelectTrigger>
223
- </FormControl>
224
- <SelectContent>
225
- <SelectItem value="active">
226
- {t('status.active')}
227
- </SelectItem>
228
- <SelectItem value="inactive">
229
- {t('status.inactive')}
230
- </SelectItem>
231
- </SelectContent>
232
- </Select>
233
- <FormMessage />
234
- </FormItem>
235
- )}
236
- />
237
- </div>
238
-
239
- <FormField
240
- control={form.control}
241
- name="ffmpeg_params"
242
- render={({ field }) => (
243
- <FormItem>
244
- <FormLabel>{t('form.ffmpegParams')}</FormLabel>
245
- <FormControl>
246
- <FfmpegParamsEditor
247
- value={field.value}
248
- onChange={field.onChange}
249
- />
250
- </FormControl>
251
- <FormMessage />
252
- </FormItem>
253
- )}
254
- />
255
-
256
- <div className="flex justify-end gap-2 pt-2">
257
- <Button
258
- type="button"
259
- variant="outline"
260
- onClick={() => onOpenChange(false)}
261
- disabled={isSaving}
262
- >
263
- {t('actions.cancel')}
264
- </Button>
265
- <Button type="submit" disabled={isSaving}>
266
- {isSaving ? t('actions.saving') : t('actions.save')}
267
- </Button>
268
- </div>
269
- </form>
270
- </Form>
271
- </ResizableSheetContent>
272
- </Sheet>
273
- );
274
- }
275
-
276
- // ─── Page ─────────────────────────────────────────────────────────────────────
277
-
278
- export default function VideoResolutionProfilesPage() {
279
- const { request } = useApp();
280
- const t = useTranslations('lms.VideoResolutionProfilesPage');
281
-
282
- const [page, setPage] = useState(1);
283
- const [pageSize, setPageSize] = usePersistedPageSize({
284
- storageKey: 'pagination:global:pageSize',
285
- defaultValue: 6,
286
- allowedValues: [6, 12, 24, 48],
287
- });
288
- const [searchInput, setSearchInput] = useState('');
289
- const [debouncedSearch, setDebouncedSearch] = useState('');
290
- const [sheetOpen, setSheetOpen] = useState(false);
291
- const [profileToEdit, setProfileToEdit] =
292
- useState<VideoResolutionProfile | null>(null);
293
- const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
294
- const [profileToDelete, setProfileToDelete] =
295
- useState<VideoResolutionProfile | null>(null);
296
- const [isDeleting, setIsDeleting] = useState(false);
297
- const [togglingProfileId, setTogglingProfileId] = useState<number | null>(
298
- null
299
- );
300
-
301
- useEffect(() => {
302
- const timeout = setTimeout(() => {
303
- setDebouncedSearch(searchInput.trim());
304
- setPage(1);
305
- }, 300);
306
- return () => clearTimeout(timeout);
307
- }, [searchInput]);
308
-
309
- const {
310
- data: paginate = { data: [], total: 0, page: 1, pageSize, lastPage: 1 },
311
- isLoading,
312
- refetch: refetchList,
313
- } = useQuery<ProfilePaginatedResult>({
314
- queryKey: [
315
- 'lms-video-resolution-profiles',
316
- page,
317
- pageSize,
318
- debouncedSearch,
319
- ],
320
- queryFn: async () => {
321
- const params = new URLSearchParams({
322
- page: String(page),
323
- pageSize: String(pageSize),
324
- });
325
- if (debouncedSearch) params.set('search', debouncedSearch);
326
- const response = await request<ProfilePaginatedResult>({
327
- url: `/lms/video-resolution-profiles?${params.toString()}`,
328
- method: 'GET',
329
- });
330
- return response.data;
331
- },
332
- placeholderData: (prev) =>
333
- prev ?? { data: [], total: 0, page: 1, pageSize, lastPage: 1 },
334
- });
335
-
336
- const totalPages = Math.max(
337
- 1,
338
- (paginate.lastPage ?? Math.ceil((paginate.total || 0) / pageSize)) || 1
339
- );
340
-
341
- useEffect(() => {
342
- if (page > totalPages) setPage(totalPages);
343
- }, [page, totalPages]);
344
-
345
- const openCreateSheet = () => {
346
- setProfileToEdit(null);
347
- setSheetOpen(true);
348
- };
349
-
350
- const openEditSheet = (profile: VideoResolutionProfile) => {
351
- setProfileToEdit(profile);
352
- setSheetOpen(true);
353
- };
354
-
355
- const handleSaved = async () => {
356
- await refetchList();
357
- };
358
-
359
- const handleDeleteConfirm = async () => {
360
- if (!profileToDelete) return;
361
- try {
362
- setIsDeleting(true);
363
- await request({
364
- url: `/lms/video-resolution-profiles/${profileToDelete.id}`,
365
- method: 'DELETE',
366
- });
367
- toast.success(t('messages.deleteSuccess'));
368
- setDeleteDialogOpen(false);
369
- setProfileToDelete(null);
370
- await refetchList();
371
- } catch {
372
- toast.error(t('messages.deleteError'));
373
- } finally {
374
- setIsDeleting(false);
375
- }
376
- };
377
-
378
- const handleQuickStatusToggle = async (profile: VideoResolutionProfile) => {
379
- const nextStatus = profile.status === 'active' ? 'inactive' : 'active';
380
-
381
- try {
382
- setTogglingProfileId(profile.id);
383
- await request({
384
- url: `/lms/video-resolution-profiles/${profile.id}`,
385
- method: 'PATCH',
386
- data: { status: nextStatus },
387
- });
388
- toast.success(t('messages.updateSuccess'));
389
- await refetchList();
390
- } catch {
391
- toast.error(t('messages.saveError'));
392
- } finally {
393
- setTogglingProfileId(null);
394
- }
395
- };
396
-
397
- return (
398
- <Page>
399
- <PageHeader
400
- breadcrumbs={[
401
- { label: t('breadcrumbs.home'), href: '/' },
402
- { label: t('breadcrumbs.lms'), href: '/lms' },
403
- { label: t('breadcrumbs.current') },
404
- ]}
405
- title={t('title')}
406
- description={t('description')}
407
- actions={[
408
- {
409
- label: t('actions.create'),
410
- onClick: openCreateSheet,
411
- icon: <Plus className="h-4 w-4" />,
412
- },
413
- ]}
414
- />
415
-
416
- <SearchBar
417
- searchQuery={searchInput}
418
- onSearchChange={(value) => setSearchInput(value)}
419
- onSearch={() => setPage(1)}
420
- placeholder={t('filters.searchPlaceholder')}
421
- />
422
-
423
- {isLoading ? (
424
- <div className="space-y-3 p-4">
425
- {Array.from({ length: 5 }).map((_, i) => (
426
- <Skeleton key={i} className="h-14 w-full" />
427
- ))}
428
- </div>
429
- ) : paginate.data.length === 0 ? (
430
- <EmptyState
431
- icon={<Film className="h-12 w-12" />}
432
- title={t('empty.title')}
433
- description={t('empty.description')}
434
- actionLabel={t('actions.create')}
435
- actionIcon={<Plus className="mr-2 h-4 w-4" />}
436
- onAction={openCreateSheet}
437
- />
438
- ) : (
439
- <div className="overflow-x-auto">
440
- <Table>
441
- <TableHeader>
442
- <TableRow>
443
- <TableHead>{t('table.name')}</TableHead>
444
- <TableHead>{t('table.ffmpegParams')}</TableHead>
445
- <TableHead>{t('table.status')}</TableHead>
446
- <TableHead className="w-10" />
447
- </TableRow>
448
- </TableHeader>
449
- <TableBody>
450
- {paginate.data.map((profile) => (
451
- <TableRow
452
- key={profile.id}
453
- className="cursor-pointer"
454
- onDoubleClick={() => openEditSheet(profile)}
455
- >
456
- <TableCell>
457
- <span className="font-medium text-sm">{profile.name}</span>
458
- </TableCell>
459
- <TableCell className="max-w-xs">
460
- <span className="font-mono text-xs text-muted-foreground truncate block max-w-xs">
461
- {profile.ffmpeg_params}
462
- </span>
463
- </TableCell>
464
- <TableCell>
465
- <Badge
466
- variant="outline"
467
- className={cn(
468
- 'border px-2.5 py-1 text-xs font-medium',
469
- profile.status === 'active'
470
- ? 'border-green-500/20 bg-green-500/10 text-green-600'
471
- : 'border-gray-500/20 bg-gray-500/10 text-gray-600'
472
- )}
473
- >
474
- {profile.status === 'active'
475
- ? t('status.active')
476
- : t('status.inactive')}
477
- </Badge>
478
- </TableCell>
479
- <TableCell className="text-right">
480
- <div className="flex items-center justify-end gap-1">
481
- <Tooltip>
482
- <TooltipTrigger asChild>
483
- <Button
484
- variant="ghost"
485
- size="icon"
486
- className="h-8 w-8 cursor-pointer"
487
- onClick={(event) => {
488
- event.stopPropagation();
489
- openEditSheet(profile);
490
- }}
491
- aria-label={t('actions.edit')}
492
- >
493
- <Pencil className="h-4 w-4" />
494
- </Button>
495
- </TooltipTrigger>
496
- <TooltipContent>{t('actions.edit')}</TooltipContent>
497
- </Tooltip>
498
-
499
- <Tooltip>
500
- <TooltipTrigger asChild>
501
- <Button
502
- variant="ghost"
503
- size="icon"
504
- className={cn(
505
- 'h-8 w-8 cursor-pointer',
506
- profile.status === 'active'
507
- ? 'text-amber-600 hover:text-amber-600'
508
- : 'text-emerald-600 hover:text-emerald-600'
509
- )}
510
- onClick={(event) => {
511
- event.stopPropagation();
512
- void handleQuickStatusToggle(profile);
513
- }}
514
- aria-label={
515
- profile.status === 'active'
516
- ? t('status.inactive')
517
- : t('status.active')
518
- }
519
- disabled={togglingProfileId === profile.id}
520
- >
521
- {profile.status === 'active' ? (
522
- <PauseCircle className="h-4 w-4" />
523
- ) : (
524
- <PlayCircle className="h-4 w-4" />
525
- )}
526
- </Button>
527
- </TooltipTrigger>
528
- <TooltipContent>
529
- {profile.status === 'active'
530
- ? t('status.inactive')
531
- : t('status.active')}
532
- </TooltipContent>
533
- </Tooltip>
534
-
535
- <Tooltip>
536
- <TooltipTrigger asChild>
537
- <Button
538
- variant="ghost"
539
- size="icon"
540
- className="h-8 w-8 cursor-pointer text-destructive hover:text-destructive"
541
- onClick={(event) => {
542
- event.stopPropagation();
543
- setProfileToDelete(profile);
544
- setDeleteDialogOpen(true);
545
- }}
546
- aria-label={t('actions.delete')}
547
- >
548
- <Trash2 className="h-4 w-4" />
549
- </Button>
550
- </TooltipTrigger>
551
- <TooltipContent>{t('actions.delete')}</TooltipContent>
552
- </Tooltip>
553
- </div>
554
- </TableCell>
555
- </TableRow>
556
- ))}
557
- </TableBody>
558
- </Table>
559
- </div>
560
- )}
561
-
562
- <PaginationFooter
563
- currentPage={page}
564
- pageSize={pageSize}
565
- totalItems={paginate.total}
566
- onPageChange={setPage}
567
- onPageSizeChange={(nextPageSize) => {
568
- setPageSize(nextPageSize);
569
- setPage(1);
570
- }}
571
- pageSizeOptions={[6, 12, 24]}
572
- />
573
-
574
- <ProfileFormSheet
575
- open={sheetOpen}
576
- onOpenChange={setSheetOpen}
577
- profileToEdit={profileToEdit}
578
- onSaved={handleSaved}
579
- />
580
-
581
- <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
582
- <AlertDialogContent>
583
- <AlertDialogHeader>
584
- <AlertDialogTitle>{t('deleteDialog.title')}</AlertDialogTitle>
585
- <AlertDialogDescription>
586
- {t('deleteDialog.description', {
587
- name: profileToDelete?.name ?? '',
588
- })}
589
- </AlertDialogDescription>
590
- </AlertDialogHeader>
591
- <div className="flex justify-end gap-2">
592
- <AlertDialogCancel disabled={isDeleting}>
593
- {t('actions.cancel')}
594
- </AlertDialogCancel>
595
- <AlertDialogAction
596
- onClick={handleDeleteConfirm}
597
- disabled={isDeleting}
598
- className="bg-red-600 hover:bg-red-700"
599
- >
600
- {isDeleting ? t('actions.deleting') : t('actions.delete')}
601
- </AlertDialogAction>
602
- </div>
603
- </AlertDialogContent>
604
- </AlertDialog>
605
- </Page>
606
- );
607
- }
@@ -1,22 +0,0 @@
1
- columns:
2
- - type: pk
3
- - name: course_id
4
- type: fk
5
- references:
6
- table: course
7
- column: id
8
- onDelete: CASCADE
9
- - name: video_resolution_profile_id
10
- type: fk
11
- references:
12
- table: video_resolution_profile
13
- column: id
14
- onDelete: CASCADE
15
- - type: created_at
16
- - type: updated_at
17
-
18
- indices:
19
- - columns: [course_id, video_resolution_profile_id]
20
- isUnique: true
21
- - columns: [course_id]
22
- - columns: [video_resolution_profile_id]
@@ -1,18 +0,0 @@
1
- columns:
2
- - type: pk
3
- - name: name
4
- type: varchar
5
- length: 100
6
- - name: ffmpeg_params
7
- type: text
8
- - name: status
9
- type: enum
10
- values: [active, inactive]
11
- default: active
12
- - type: created_at
13
- - type: updated_at
14
-
15
- indices:
16
- - columns: [name]
17
- isUnique: true
18
- - columns: [status]
@@ -1,16 +0,0 @@
1
- import { IsEnum, IsNotEmpty, IsOptional, IsString, MaxLength } from 'class-validator';
2
-
3
- export class CreateVideoResolutionProfileDto {
4
- @IsString()
5
- @IsNotEmpty()
6
- @MaxLength(100)
7
- name: string;
8
-
9
- @IsString()
10
- @IsNotEmpty()
11
- ffmpeg_params: string;
12
-
13
- @IsOptional()
14
- @IsEnum(['active', 'inactive'])
15
- status?: 'active' | 'inactive';
16
- }
@@ -1,16 +0,0 @@
1
- import { IsEnum, IsOptional, IsString, MaxLength } from 'class-validator';
2
-
3
- export class UpdateVideoResolutionProfileDto {
4
- @IsOptional()
5
- @IsString()
6
- @MaxLength(100)
7
- name?: string;
8
-
9
- @IsOptional()
10
- @IsString()
11
- ffmpeg_params?: string;
12
-
13
- @IsOptional()
14
- @IsEnum(['active', 'inactive'])
15
- status?: 'active' | 'inactive';
16
- }