@hed-hog/lms 0.0.353 → 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.
- package/dist/course/course-audio-transcription.service.d.ts +29 -0
- package/dist/course/course-audio-transcription.service.d.ts.map +1 -0
- package/dist/course/course-audio-transcription.service.js +291 -0
- package/dist/course/course-audio-transcription.service.js.map +1 -0
- package/dist/course/course-lesson.controller.d.ts +10 -0
- package/dist/course/course-lesson.controller.d.ts.map +1 -0
- package/dist/course/course-lesson.controller.js +62 -0
- package/dist/course/course-lesson.controller.js.map +1 -0
- package/dist/course/course-structure.controller.d.ts +41 -15
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +50 -6
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +50 -15
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +238 -73
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course-video-conversion.service.d.ts +20 -2
- package/dist/course/course-video-conversion.service.d.ts.map +1 -1
- package/dist/course/course-video-conversion.service.js +730 -10
- package/dist/course/course-video-conversion.service.js.map +1 -1
- package/dist/course/course.controller.d.ts +24 -8
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +5 -3
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +24 -8
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +112 -176
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course-lesson-frame.dto.d.ts +5 -0
- package/dist/course/dto/create-course-lesson-frame.dto.d.ts.map +1 -0
- package/dist/course/dto/create-course-lesson-frame.dto.js +30 -0
- package/dist/course/dto/create-course-lesson-frame.dto.js.map +1 -0
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts +4 -2
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js +10 -3
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/dto/create-course.dto.d.ts +1 -1
- package/dist/course/dto/create-course.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course.dto.js +6 -6
- package/dist/course/dto/create-course.dto.js.map +1 -1
- package/dist/course/dto/update-course-lesson-frame.dto.d.ts +5 -0
- package/dist/course/dto/update-course-lesson-frame.dto.d.ts.map +1 -0
- package/dist/course/dto/update-course-lesson-frame.dto.js +32 -0
- package/dist/course/dto/update-course-lesson-frame.dto.js.map +1 -0
- package/dist/course/dto/update-course-resources.dto.d.ts +4 -2
- package/dist/course/dto/update-course-resources.dto.d.ts.map +1 -1
- package/dist/course/dto/update-course-resources.dto.js +10 -3
- package/dist/course/dto/update-course-resources.dto.js.map +1 -1
- package/dist/course/dto/update-transcription-segments.dto.d.ts +10 -0
- package/dist/course/dto/update-transcription-segments.dto.d.ts.map +1 -0
- package/dist/course/dto/update-transcription-segments.dto.js +38 -0
- package/dist/course/dto/update-transcription-segments.dto.js.map +1 -0
- package/dist/course/lms-setting.controller.d.ts +13 -0
- package/dist/course/lms-setting.controller.d.ts.map +1 -0
- package/dist/course/lms-setting.controller.js +53 -0
- package/dist/course/lms-setting.controller.js.map +1 -0
- package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-admin.service.js +74 -33
- package/dist/enterprise/training/training-admin.service.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +6 -0
- package/dist/lms.module.js.map +1 -1
- package/hedhog/data/route.yaml +63 -0
- package/hedhog/data/setting_group.yaml +76 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +10 -1
- package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +7 -2
- package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +5 -2
- package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +7 -1
- package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +95 -50
- package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +11 -36
- package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +19 -5
- package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +30 -10
- package/hedhog/frontend/app/courses/[id]/structure/_components/CourseInstructorsSummaryCard.tsx.ejs +95 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +29 -11
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +42 -31
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +10 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +25 -22
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +12 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +315 -229
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +3220 -534
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +20 -15
- package/hedhog/frontend/app/courses/[id]/structure/_components/icon-action-tooltip.tsx.ejs +35 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +76 -67
- package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +13 -10
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +18 -16
- package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +6 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +23 -11
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +48 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +11 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +121 -15
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +69 -14
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +11 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +31 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +32 -6
- package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +57 -3
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +46 -6
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +14 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-lesson-audio-files.ts.ejs +23 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +34 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +76 -0
- package/hedhog/frontend/messages/en.json +39 -3
- package/hedhog/frontend/messages/pt.json +39 -3
- package/hedhog/table/course.yaml +8 -0
- package/hedhog/table/course_lesson_file.yaml +12 -4
- package/hedhog/table/course_lesson_transcription_segment.yaml +22 -0
- package/hedhog/table/course_lesson_video_frame.yaml +25 -0
- package/package.json +9 -9
- package/src/course/course-audio-transcription.service.ts +393 -0
- package/src/course/course-lesson.controller.ts +28 -0
- package/src/course/course-structure.controller.ts +49 -3
- package/src/course/course-structure.service.ts +294 -32
- package/src/course/course-video-conversion.service.ts +972 -6
- package/src/course/course.module.ts +5 -3
- package/src/course/course.service.ts +87 -139
- package/src/course/dto/create-course-lesson-frame.dto.ts +14 -0
- package/src/course/dto/create-course-structure-lesson.dto.ts +19 -3
- package/src/course/dto/create-course.dto.ts +5 -5
- package/src/course/dto/update-course-lesson-frame.dto.ts +16 -0
- package/src/course/dto/update-course-resources.dto.ts +18 -3
- package/src/course/dto/update-transcription-segments.dto.ts +20 -0
- package/src/course/lms-setting.controller.ts +30 -0
- package/src/enterprise/training/training-admin.service.ts +77 -24
- package/src/index.ts +2 -0
- package/src/lms.module.ts +6 -0
- package/hedhog/table/course_instructor.yaml +0 -27
|
@@ -10,9 +10,6 @@ import {
|
|
|
10
10
|
CircleDot,
|
|
11
11
|
Clock,
|
|
12
12
|
Download,
|
|
13
|
-
File as FileIcon,
|
|
14
|
-
FileImage,
|
|
15
|
-
FileText,
|
|
16
13
|
Layers,
|
|
17
14
|
Loader2,
|
|
18
15
|
Pencil,
|
|
@@ -32,6 +29,7 @@ import { z } from 'zod';
|
|
|
32
29
|
|
|
33
30
|
import { createDefaultTemplate } from '@/app/(app)/(libraries)/lms/_lib/editor/types';
|
|
34
31
|
import { FfmpegParamsEditor } from '@/components/ffmpeg-params-editor';
|
|
32
|
+
import { FileTypeIcon } from '@/components/file-type-icon';
|
|
35
33
|
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
36
34
|
import { Badge } from '@/components/ui/badge';
|
|
37
35
|
import { Button } from '@/components/ui/button';
|
|
@@ -54,6 +52,7 @@ import {
|
|
|
54
52
|
FormMessage,
|
|
55
53
|
} from '@/components/ui/form';
|
|
56
54
|
import { Input } from '@/components/ui/input';
|
|
55
|
+
import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
|
|
57
56
|
import { Separator } from '@/components/ui/separator';
|
|
58
57
|
import {
|
|
59
58
|
Sheet,
|
|
@@ -62,7 +61,6 @@ import {
|
|
|
62
61
|
SheetHeader,
|
|
63
62
|
SheetTitle,
|
|
64
63
|
} from '@/components/ui/sheet';
|
|
65
|
-
import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
|
|
66
64
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
67
65
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
68
66
|
import {
|
|
@@ -76,7 +74,6 @@ import { cn } from '@/lib/utils';
|
|
|
76
74
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
77
75
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
78
76
|
|
|
79
|
-
import { InstructorFormSheet } from '../../../../instructors/_components/instructor-form-sheet';
|
|
80
77
|
import type {
|
|
81
78
|
CourseEditFormValues,
|
|
82
79
|
PickerOption,
|
|
@@ -86,6 +83,7 @@ import { CourseClassificationCard } from '../../_components/CourseClassification
|
|
|
86
83
|
import { CourseContentCard } from '../../_components/CourseContentCard';
|
|
87
84
|
import { CourseDangerZoneCard } from '../../_components/CourseDangerZoneCard';
|
|
88
85
|
import { CourseFlagsCard } from '../../_components/CourseFlagsCard';
|
|
86
|
+
import { CourseInstructorsSummaryCard } from './CourseInstructorsSummaryCard';
|
|
89
87
|
import { CourseMediaCard } from '../../_components/CourseMediaCard';
|
|
90
88
|
import { CourseRelationsCard } from '../../_components/CourseRelationsCard';
|
|
91
89
|
import {
|
|
@@ -96,6 +94,7 @@ import {
|
|
|
96
94
|
} from '../_data/services/course-structure.service';
|
|
97
95
|
import { useCreateSessionMutation } from '../_data/use-course-structure-mutations';
|
|
98
96
|
import { courseStructureQueryKey } from '../_data/use-course-structure-query';
|
|
97
|
+
import { IconActionTooltip } from './icon-action-tooltip';
|
|
99
98
|
import { useStructureStore } from './store';
|
|
100
99
|
import { Resource } from './types';
|
|
101
100
|
|
|
@@ -108,6 +107,7 @@ type ApiCourseDetail = {
|
|
|
108
107
|
code?: string | null;
|
|
109
108
|
title: string;
|
|
110
109
|
description: string;
|
|
110
|
+
locale_id?: number | null;
|
|
111
111
|
primaryColor?: string | null;
|
|
112
112
|
secondaryColor?: string | null;
|
|
113
113
|
level: 'beginner' | 'intermediate' | 'advanced';
|
|
@@ -125,7 +125,6 @@ type ApiCourseDetail = {
|
|
|
125
125
|
sessionCount: number;
|
|
126
126
|
averageCompletion: number;
|
|
127
127
|
certificatesIssued: number;
|
|
128
|
-
instructorIds?: number[];
|
|
129
128
|
instructors?: Array<{ id: number; name: string; avatarId: number | null }>;
|
|
130
129
|
logoFileId?: number | null;
|
|
131
130
|
logoFilename?: string | null;
|
|
@@ -173,19 +172,6 @@ type ApiCategoryList = {
|
|
|
173
172
|
page: number;
|
|
174
173
|
pageSize: number;
|
|
175
174
|
};
|
|
176
|
-
type ApiInstructor = {
|
|
177
|
-
id: number;
|
|
178
|
-
personId: number;
|
|
179
|
-
name: string;
|
|
180
|
-
avatarId?: number | null;
|
|
181
|
-
qualificationSlugs: string[];
|
|
182
|
-
};
|
|
183
|
-
type ApiInstructorList = {
|
|
184
|
-
data: ApiInstructor[];
|
|
185
|
-
total: number;
|
|
186
|
-
page: number;
|
|
187
|
-
pageSize: number;
|
|
188
|
-
};
|
|
189
175
|
type ApiCertificateTemplate = {
|
|
190
176
|
id: number;
|
|
191
177
|
name: string;
|
|
@@ -213,14 +199,21 @@ type CourseResourceApi = {
|
|
|
213
199
|
id: number;
|
|
214
200
|
nome: string;
|
|
215
201
|
fileId?: number | null;
|
|
216
|
-
|
|
217
|
-
|
|
202
|
+
type?: string | null;
|
|
203
|
+
is_public?: boolean;
|
|
218
204
|
};
|
|
219
205
|
|
|
220
206
|
type CourseResourceItem = Resource & {
|
|
221
207
|
fileId?: number | null;
|
|
222
208
|
};
|
|
223
209
|
|
|
210
|
+
const COURSE_RESOURCE_ALLOWED_TYPES = new Set([
|
|
211
|
+
'lesson_audio',
|
|
212
|
+
'student_download',
|
|
213
|
+
'supplementary_material',
|
|
214
|
+
'video_original',
|
|
215
|
+
]);
|
|
216
|
+
|
|
224
217
|
type Locale = { id?: number; code: string; name: string };
|
|
225
218
|
|
|
226
219
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
@@ -309,24 +302,6 @@ function slugify(value: string) {
|
|
|
309
302
|
.replace(/^-+|-+$/g, '');
|
|
310
303
|
}
|
|
311
304
|
|
|
312
|
-
function getInstructorAvatarUrl(avatarId?: number | null) {
|
|
313
|
-
return typeof avatarId === 'number' && avatarId > 0
|
|
314
|
-
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
|
|
315
|
-
: null;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function getResourceIcon(type: string): typeof FileIcon {
|
|
319
|
-
if (type === 'application/pdf' || type.endsWith('pdf')) return FileText;
|
|
320
|
-
if (type.startsWith('image/')) return FileImage;
|
|
321
|
-
return FileIcon;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function getResourceIconColor(type: string): string {
|
|
325
|
-
if (type === 'application/pdf' || type.endsWith('pdf')) return 'text-red-500';
|
|
326
|
-
if (type.startsWith('image/')) return 'text-blue-500';
|
|
327
|
-
return 'text-muted-foreground';
|
|
328
|
-
}
|
|
329
|
-
|
|
330
305
|
function formatFileSize(bytes: number): string {
|
|
331
306
|
if (bytes < 1024) return `${bytes} B`;
|
|
332
307
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -341,11 +316,19 @@ function mapApiCourseResourceToLocal(
|
|
|
341
316
|
fileId: item.fileId ?? undefined,
|
|
342
317
|
name: item.nome,
|
|
343
318
|
size: '',
|
|
344
|
-
type: item.
|
|
345
|
-
public: item.
|
|
319
|
+
type: item.type ?? 'student_download',
|
|
320
|
+
public: item.is_public ?? true,
|
|
346
321
|
};
|
|
347
322
|
}
|
|
348
323
|
|
|
324
|
+
function normalizeCourseResourceType(type?: string | null) {
|
|
325
|
+
const value = String(type ?? '').trim();
|
|
326
|
+
if (!value) return undefined;
|
|
327
|
+
if (COURSE_RESOURCE_ALLOWED_TYPES.has(value)) return value;
|
|
328
|
+
if (/^video_profile:\d+$/.test(value)) return value;
|
|
329
|
+
return 'student_download';
|
|
330
|
+
}
|
|
331
|
+
|
|
349
332
|
// ── Schema ────────────────────────────────────────────────────────────────────
|
|
350
333
|
|
|
351
334
|
function buildSchema(t: (key: string) => string) {
|
|
@@ -369,6 +352,7 @@ function buildSchema(t: (key: string) => string) {
|
|
|
369
352
|
nivel: z.enum(['iniciante', 'intermediario', 'avancado']),
|
|
370
353
|
status: z.enum(['ativo', 'rascunho', 'arquivado']),
|
|
371
354
|
tipoOferta: z.enum(['agendado', 'sob_demanda', 'hibrido']),
|
|
355
|
+
localeId: z.string().optional().default(''),
|
|
372
356
|
categorias: z.array(z.string()).optional().default([]),
|
|
373
357
|
operationsProjectId: z.string().optional(),
|
|
374
358
|
primaryColor: z
|
|
@@ -397,6 +381,9 @@ export function EditorCourse() {
|
|
|
397
381
|
|
|
398
382
|
const { request, currentLocaleCode, locales } = useApp();
|
|
399
383
|
const t = useTranslations('lms.CursoEditPage');
|
|
384
|
+
const courseLocaleLabel = currentLocaleCode?.startsWith('en')
|
|
385
|
+
? 'Course Language'
|
|
386
|
+
: 'Idioma do Curso';
|
|
400
387
|
const router = useRouter();
|
|
401
388
|
const isMobile = useIsMobile();
|
|
402
389
|
const queryClient = useQueryClient();
|
|
@@ -404,10 +391,6 @@ export function EditorCourse() {
|
|
|
404
391
|
|
|
405
392
|
// ── UI state ────────────────────────────────────────────────────────────────
|
|
406
393
|
const [activeTab, setActiveTab] = useState('estrutura');
|
|
407
|
-
const [instructorEditSheetOpen, setInstructorEditSheetOpen] = useState(false);
|
|
408
|
-
const [editingInstructorId, setEditingInstructorId] = useState<number | null>(
|
|
409
|
-
null
|
|
410
|
-
);
|
|
411
394
|
const [categoryEditSheetOpen, setCategoryEditSheetOpen] = useState(false);
|
|
412
395
|
const [editingCategoryId, setEditingCategoryId] = useState<number | null>(
|
|
413
396
|
null
|
|
@@ -442,6 +425,8 @@ export function EditorCourse() {
|
|
|
442
425
|
const [courseResources, setCourseResources] = useState<CourseResourceItem[]>(
|
|
443
426
|
[]
|
|
444
427
|
);
|
|
428
|
+
const [downloadingCourseResourceKeys, setDownloadingCourseResourceKeys] =
|
|
429
|
+
useState<Set<string>>(() => new Set<string>());
|
|
445
430
|
const [resourcesDragOver, setResourcesDragOver] = useState(false);
|
|
446
431
|
const [isUploadingResources, setIsUploadingResources] = useState(false);
|
|
447
432
|
const [isSavingResources, setIsSavingResources] = useState(false);
|
|
@@ -496,32 +481,6 @@ export function EditorCourse() {
|
|
|
496
481
|
initialData: { data: [], total: 0, page: 1, pageSize: 500 },
|
|
497
482
|
});
|
|
498
483
|
|
|
499
|
-
const { data: instructorListData, refetch: refetchInstructorOptions } =
|
|
500
|
-
useQuery<ApiInstructorList>({
|
|
501
|
-
queryKey: ['lms-course-edit-instructors'],
|
|
502
|
-
queryFn: async () => {
|
|
503
|
-
const response = await request<ApiInstructorList | ApiInstructor[]>({
|
|
504
|
-
url: '/lms/instructors',
|
|
505
|
-
method: 'GET',
|
|
506
|
-
params: {
|
|
507
|
-
page: 1,
|
|
508
|
-
pageSize: 500,
|
|
509
|
-
qualificationSlugs: ['course-lessons'],
|
|
510
|
-
},
|
|
511
|
-
});
|
|
512
|
-
const payload = response.data;
|
|
513
|
-
if (Array.isArray(payload))
|
|
514
|
-
return {
|
|
515
|
-
data: payload,
|
|
516
|
-
total: payload.length,
|
|
517
|
-
page: 1,
|
|
518
|
-
pageSize: payload.length,
|
|
519
|
-
};
|
|
520
|
-
return payload;
|
|
521
|
-
},
|
|
522
|
-
initialData: { data: [], total: 0, page: 1, pageSize: 500 },
|
|
523
|
-
});
|
|
524
|
-
|
|
525
484
|
const {
|
|
526
485
|
data: certificateTemplateData,
|
|
527
486
|
refetch: refetchCertificateTemplates,
|
|
@@ -597,6 +556,7 @@ export function EditorCourse() {
|
|
|
597
556
|
code: data.code?.trim() || undefined,
|
|
598
557
|
title: data.tituloComercial.trim(),
|
|
599
558
|
description: data.descricaoPublica.trim(),
|
|
559
|
+
locale_id: data.localeId ? Number(data.localeId) : null,
|
|
600
560
|
requirements: data.preRequisitos?.trim() || undefined,
|
|
601
561
|
objectives: data.objetivos?.trim() || undefined,
|
|
602
562
|
targetAudience: data.publicoAlvo?.trim() || undefined,
|
|
@@ -612,7 +572,6 @@ export function EditorCourse() {
|
|
|
612
572
|
isFeatured: data.destaque,
|
|
613
573
|
isListed: data.listado,
|
|
614
574
|
certificateModel: data.modeloCertificado.trim() || null,
|
|
615
|
-
instructorIds: (data.instrutores ?? []).map(Number),
|
|
616
575
|
operationsProjectId: data.operationsProjectId
|
|
617
576
|
? Number(data.operationsProjectId)
|
|
618
577
|
: null,
|
|
@@ -632,6 +591,7 @@ export function EditorCourse() {
|
|
|
632
591
|
}
|
|
633
592
|
setPersistedCertificateModel(data.modeloCertificado || '');
|
|
634
593
|
updateCourseInStore({
|
|
594
|
+
code: data.code,
|
|
635
595
|
title: data.tituloComercial,
|
|
636
596
|
slug: data.slug,
|
|
637
597
|
name: data.nomeInterno,
|
|
@@ -641,6 +601,9 @@ export function EditorCourse() {
|
|
|
641
601
|
void queryClient.invalidateQueries({
|
|
642
602
|
queryKey: courseStructureQueryKey(courseId),
|
|
643
603
|
});
|
|
604
|
+
void queryClient.invalidateQueries({
|
|
605
|
+
queryKey: ['lms-course-detail', courseId],
|
|
606
|
+
});
|
|
644
607
|
form.reset(data);
|
|
645
608
|
toast.success(t('toasts.courseUpdated'));
|
|
646
609
|
},
|
|
@@ -665,11 +628,11 @@ export function EditorCourse() {
|
|
|
665
628
|
nivel: 'iniciante',
|
|
666
629
|
status: 'rascunho',
|
|
667
630
|
tipoOferta: 'sob_demanda',
|
|
631
|
+
localeId: '',
|
|
668
632
|
categorias: [],
|
|
669
633
|
operationsProjectId: '',
|
|
670
634
|
primaryColor: '#1D4ED8',
|
|
671
635
|
secondaryColor: '#111827',
|
|
672
|
-
instrutores: [],
|
|
673
636
|
preRequisitos: '',
|
|
674
637
|
modeloCertificado: '',
|
|
675
638
|
certificado: false,
|
|
@@ -757,24 +720,6 @@ export function EditorCourse() {
|
|
|
757
720
|
);
|
|
758
721
|
}, [apiCourse, operationsProjectOptionsData]);
|
|
759
722
|
|
|
760
|
-
const instructorOptions = useMemo(() => {
|
|
761
|
-
const fromCourse = (apiCourse?.instructors ?? []).map((item) => ({
|
|
762
|
-
value: String(item.id),
|
|
763
|
-
label: item.name,
|
|
764
|
-
avatarUrl: getInstructorAvatarUrl(item.avatarId),
|
|
765
|
-
meta: `ID ${item.id}`,
|
|
766
|
-
}));
|
|
767
|
-
const fromDirectory = (instructorListData?.data ?? []).map((item) => ({
|
|
768
|
-
value: String(item.id),
|
|
769
|
-
label: item.name,
|
|
770
|
-
avatarUrl: getInstructorAvatarUrl(item.avatarId),
|
|
771
|
-
meta: item.qualificationSlugs?.join(' • ') || `ID ${item.id}`,
|
|
772
|
-
}));
|
|
773
|
-
return [...fromCourse, ...fromDirectory].filter(
|
|
774
|
-
(item, i, arr) => arr.findIndex((c) => c.value === item.value) === i
|
|
775
|
-
);
|
|
776
|
-
}, [apiCourse?.instructors, instructorListData]);
|
|
777
|
-
|
|
778
723
|
const certificateOptions = useMemo(() => {
|
|
779
724
|
const serverOptions = (certificateTemplateData?.data ?? []).map((item) => ({
|
|
780
725
|
value: item.slug || String(item.id),
|
|
@@ -881,13 +826,13 @@ export function EditorCourse() {
|
|
|
881
826
|
nivel: toPtLevel(apiCourse.level),
|
|
882
827
|
status: toPtStatus(apiCourse.status),
|
|
883
828
|
tipoOferta: toPtOfferingType(apiCourse.offeringType),
|
|
829
|
+
localeId: apiCourse.locale_id ? String(apiCourse.locale_id) : '',
|
|
884
830
|
categorias: apiCourse.categories ?? [],
|
|
885
831
|
operationsProjectId: apiCourse.operationsProjectId
|
|
886
832
|
? String(apiCourse.operationsProjectId)
|
|
887
833
|
: '',
|
|
888
834
|
primaryColor: apiCourse.primaryColor || '#1D4ED8',
|
|
889
835
|
secondaryColor: apiCourse.secondaryColor || '#111827',
|
|
890
|
-
instrutores: (apiCourse.instructorIds ?? []).map(String),
|
|
891
836
|
preRequisitos: apiCourse.requirements ?? '',
|
|
892
837
|
modeloCertificado: nextCertificateModel,
|
|
893
838
|
certificado: apiCourse.hasCertificate ?? false,
|
|
@@ -969,14 +914,18 @@ export function EditorCourse() {
|
|
|
969
914
|
setIsSavingResources(true);
|
|
970
915
|
try {
|
|
971
916
|
const saved = await updateCourseResources(request, courseId, {
|
|
972
|
-
recursos: nextResources.map((item) =>
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
:
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
917
|
+
recursos: nextResources.map((item) => {
|
|
918
|
+
const normalizedType = normalizeCourseResourceType(item.type);
|
|
919
|
+
|
|
920
|
+
return {
|
|
921
|
+
nome: item.name,
|
|
922
|
+
...(typeof item.fileId === 'number' && item.fileId > 0
|
|
923
|
+
? { fileId: item.fileId }
|
|
924
|
+
: {}),
|
|
925
|
+
...(normalizedType ? { type: normalizedType } : {}),
|
|
926
|
+
is_public: item.public,
|
|
927
|
+
};
|
|
928
|
+
}),
|
|
980
929
|
});
|
|
981
930
|
|
|
982
931
|
setCourseResources(saved.map(mapApiCourseResourceToLocal));
|
|
@@ -1006,7 +955,7 @@ export function EditorCourse() {
|
|
|
1006
955
|
fileId: uploaded.id,
|
|
1007
956
|
name: file.name,
|
|
1008
957
|
size: formatFileSize(file.size),
|
|
1009
|
-
type:
|
|
958
|
+
type: 'student_download',
|
|
1010
959
|
public: true,
|
|
1011
960
|
})
|
|
1012
961
|
)
|
|
@@ -1060,9 +1009,16 @@ export function EditorCourse() {
|
|
|
1060
1009
|
return;
|
|
1061
1010
|
}
|
|
1062
1011
|
|
|
1012
|
+
const downloadKey = String(item.fileId ?? item.id);
|
|
1013
|
+
setDownloadingCourseResourceKeys((current) => {
|
|
1014
|
+
const next = new Set(current);
|
|
1015
|
+
next.add(downloadKey);
|
|
1016
|
+
return next;
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1063
1019
|
try {
|
|
1064
1020
|
const response = await request<{ url?: string }>({
|
|
1065
|
-
url: `/file/
|
|
1021
|
+
url: `/file/download/${item.fileId}`,
|
|
1066
1022
|
method: 'PUT',
|
|
1067
1023
|
});
|
|
1068
1024
|
const url = response?.data?.url;
|
|
@@ -1070,9 +1026,39 @@ export function EditorCourse() {
|
|
|
1070
1026
|
toast.error('Nao foi possivel gerar o link de download.');
|
|
1071
1027
|
return;
|
|
1072
1028
|
}
|
|
1073
|
-
|
|
1029
|
+
|
|
1030
|
+
try {
|
|
1031
|
+
const downloadResponse = await fetch(url);
|
|
1032
|
+
if (!downloadResponse.ok) {
|
|
1033
|
+
throw new Error('download request failed');
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
const blob = await downloadResponse.blob();
|
|
1037
|
+
const objectUrl = window.URL.createObjectURL(blob);
|
|
1038
|
+
const anchor = document.createElement('a');
|
|
1039
|
+
anchor.href = objectUrl;
|
|
1040
|
+
anchor.download = item.name || `arquivo-${item.fileId}`;
|
|
1041
|
+
anchor.rel = 'noopener noreferrer';
|
|
1042
|
+
document.body.appendChild(anchor);
|
|
1043
|
+
anchor.click();
|
|
1044
|
+
anchor.remove();
|
|
1045
|
+
window.URL.revokeObjectURL(objectUrl);
|
|
1046
|
+
} catch {
|
|
1047
|
+
const anchor = document.createElement('a');
|
|
1048
|
+
anchor.href = url;
|
|
1049
|
+
anchor.rel = 'noopener noreferrer';
|
|
1050
|
+
document.body.appendChild(anchor);
|
|
1051
|
+
anchor.click();
|
|
1052
|
+
anchor.remove();
|
|
1053
|
+
}
|
|
1074
1054
|
} catch {
|
|
1075
1055
|
toast.error('Nao foi possivel baixar o recurso.');
|
|
1056
|
+
} finally {
|
|
1057
|
+
setDownloadingCourseResourceKeys((current) => {
|
|
1058
|
+
const next = new Set(current);
|
|
1059
|
+
next.delete(downloadKey);
|
|
1060
|
+
return next;
|
|
1061
|
+
});
|
|
1076
1062
|
}
|
|
1077
1063
|
}
|
|
1078
1064
|
|
|
@@ -1112,7 +1098,11 @@ export function EditorCourse() {
|
|
|
1112
1098
|
}
|
|
1113
1099
|
const objectUrl = URL.createObjectURL(file);
|
|
1114
1100
|
setter(objectUrl);
|
|
1115
|
-
type === 'logo'
|
|
1101
|
+
if (type === 'logo') {
|
|
1102
|
+
setUploadingLogo(true);
|
|
1103
|
+
} else {
|
|
1104
|
+
setUploadingBanner(true);
|
|
1105
|
+
}
|
|
1116
1106
|
try {
|
|
1117
1107
|
const formData = new FormData();
|
|
1118
1108
|
formData.append('file', file);
|
|
@@ -1136,7 +1126,11 @@ export function EditorCourse() {
|
|
|
1136
1126
|
} catch {
|
|
1137
1127
|
toast.error(t('toasts.fileUploadError'));
|
|
1138
1128
|
} finally {
|
|
1139
|
-
type === 'logo'
|
|
1129
|
+
if (type === 'logo') {
|
|
1130
|
+
setUploadingLogo(false);
|
|
1131
|
+
} else {
|
|
1132
|
+
setUploadingBanner(false);
|
|
1133
|
+
}
|
|
1140
1134
|
}
|
|
1141
1135
|
}
|
|
1142
1136
|
|
|
@@ -1235,21 +1229,6 @@ export function EditorCourse() {
|
|
|
1235
1229
|
}
|
|
1236
1230
|
}
|
|
1237
1231
|
|
|
1238
|
-
function handleCreateInstructor() {
|
|
1239
|
-
setEditingInstructorId(null);
|
|
1240
|
-
setInstructorEditSheetOpen(true);
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
function handleEditInstructor(instructorId: string) {
|
|
1244
|
-
const parsed = Number(instructorId);
|
|
1245
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
1246
|
-
toast.error('Instrutor inválido para edição.');
|
|
1247
|
-
return;
|
|
1248
|
-
}
|
|
1249
|
-
setEditingInstructorId(parsed);
|
|
1250
|
-
setInstructorEditSheetOpen(true);
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
1232
|
function handleEditVideoProfile(profileId: number) {
|
|
1254
1233
|
const profile = allVideoProfiles.find((p) => p.id === profileId);
|
|
1255
1234
|
if (!profile) return;
|
|
@@ -1390,7 +1369,10 @@ export function EditorCourse() {
|
|
|
1390
1369
|
if (data.status === 'ativo') {
|
|
1391
1370
|
const requiredProfileIds = new Set(linkedProfileIds);
|
|
1392
1371
|
const invalidLesson = lessons.find((lesson) => {
|
|
1393
|
-
if (
|
|
1372
|
+
if (
|
|
1373
|
+
lesson.type !== 'video' ||
|
|
1374
|
+
lesson.videoProvider !== 'file_storage'
|
|
1375
|
+
) {
|
|
1394
1376
|
return false;
|
|
1395
1377
|
}
|
|
1396
1378
|
|
|
@@ -1674,6 +1656,120 @@ export function EditorCourse() {
|
|
|
1674
1656
|
)}
|
|
1675
1657
|
/>
|
|
1676
1658
|
</div>
|
|
1659
|
+
|
|
1660
|
+
<FormField
|
|
1661
|
+
control={form.control}
|
|
1662
|
+
name="localeId"
|
|
1663
|
+
render={({ field }) => (
|
|
1664
|
+
<FormItem>
|
|
1665
|
+
<FormLabel className="text-xs">
|
|
1666
|
+
{courseLocaleLabel}
|
|
1667
|
+
</FormLabel>
|
|
1668
|
+
<FormControl>
|
|
1669
|
+
<EntityPicker
|
|
1670
|
+
value={field.value ? Number(field.value) : null}
|
|
1671
|
+
onChange={(value) => {
|
|
1672
|
+
form.setValue(
|
|
1673
|
+
'localeId',
|
|
1674
|
+
value ? String(value) : '',
|
|
1675
|
+
{
|
|
1676
|
+
shouldDirty: true,
|
|
1677
|
+
shouldTouch: true,
|
|
1678
|
+
shouldValidate: true,
|
|
1679
|
+
}
|
|
1680
|
+
);
|
|
1681
|
+
}}
|
|
1682
|
+
valueType="number"
|
|
1683
|
+
placeholder="Selecionar idioma..."
|
|
1684
|
+
searchable
|
|
1685
|
+
showCreateButton
|
|
1686
|
+
createTitle="Novo Idioma"
|
|
1687
|
+
createFields={[
|
|
1688
|
+
{
|
|
1689
|
+
name: 'name',
|
|
1690
|
+
label: 'Nome',
|
|
1691
|
+
type: 'text',
|
|
1692
|
+
required: true,
|
|
1693
|
+
},
|
|
1694
|
+
{
|
|
1695
|
+
name: 'code',
|
|
1696
|
+
label: 'Código (2 letras)',
|
|
1697
|
+
type: 'text',
|
|
1698
|
+
required: true,
|
|
1699
|
+
},
|
|
1700
|
+
{
|
|
1701
|
+
name: 'region',
|
|
1702
|
+
label: 'Região (2 letras)',
|
|
1703
|
+
type: 'text',
|
|
1704
|
+
required: false,
|
|
1705
|
+
},
|
|
1706
|
+
]}
|
|
1707
|
+
loadOptions={async ({ page, search }) => {
|
|
1708
|
+
try {
|
|
1709
|
+
const res = await request<
|
|
1710
|
+
| {
|
|
1711
|
+
data?: Array<{
|
|
1712
|
+
id: number;
|
|
1713
|
+
name: string;
|
|
1714
|
+
code: string;
|
|
1715
|
+
region?: string;
|
|
1716
|
+
}>;
|
|
1717
|
+
}
|
|
1718
|
+
| Array<{
|
|
1719
|
+
id: number;
|
|
1720
|
+
name: string;
|
|
1721
|
+
code: string;
|
|
1722
|
+
region?: string;
|
|
1723
|
+
}>
|
|
1724
|
+
>({
|
|
1725
|
+
url: `/core/locales?page=${page}&search=${encodeURIComponent(search ?? '')}`,
|
|
1726
|
+
method: 'GET',
|
|
1727
|
+
});
|
|
1728
|
+
|
|
1729
|
+
const payload = Array.isArray(res.data)
|
|
1730
|
+
? res.data
|
|
1731
|
+
: (res.data?.data ?? []);
|
|
1732
|
+
|
|
1733
|
+
return {
|
|
1734
|
+
items: payload,
|
|
1735
|
+
hasMore: false,
|
|
1736
|
+
};
|
|
1737
|
+
} catch {
|
|
1738
|
+
return {
|
|
1739
|
+
items: [],
|
|
1740
|
+
hasMore: false,
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
}}
|
|
1744
|
+
getOptionValue={(option) => option.id}
|
|
1745
|
+
getOptionLabel={(option) =>
|
|
1746
|
+
`${option.name} (${option.code})`
|
|
1747
|
+
}
|
|
1748
|
+
onCreate={async (data) => {
|
|
1749
|
+
const res = await request<{
|
|
1750
|
+
id: number;
|
|
1751
|
+
name: string;
|
|
1752
|
+
code: string;
|
|
1753
|
+
region?: string;
|
|
1754
|
+
}>({
|
|
1755
|
+
url: '/core/locales',
|
|
1756
|
+
method: 'POST',
|
|
1757
|
+
data,
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
return {
|
|
1761
|
+
id: res.data.id,
|
|
1762
|
+
name: res.data.name,
|
|
1763
|
+
code: res.data.code,
|
|
1764
|
+
region: res.data.region,
|
|
1765
|
+
};
|
|
1766
|
+
}}
|
|
1767
|
+
/>
|
|
1768
|
+
</FormControl>
|
|
1769
|
+
<FormMessage className="text-xs" />
|
|
1770
|
+
</FormItem>
|
|
1771
|
+
)}
|
|
1772
|
+
/>
|
|
1677
1773
|
</CardContent>
|
|
1678
1774
|
</Card>
|
|
1679
1775
|
|
|
@@ -1715,19 +1811,22 @@ export function EditorCourse() {
|
|
|
1715
1811
|
levels={NIVEIS}
|
|
1716
1812
|
statuses={STATUS_OPTIONS}
|
|
1717
1813
|
offeringTypes={OFFERING_TYPE_OPTIONS}
|
|
1814
|
+
compact
|
|
1718
1815
|
/>
|
|
1719
1816
|
<CourseRelationsCard
|
|
1720
1817
|
form={form}
|
|
1721
1818
|
t={t}
|
|
1722
1819
|
categoryOptions={categoryOptions}
|
|
1723
1820
|
projectOptions={projectOptions}
|
|
1724
|
-
instructorOptions={instructorOptions}
|
|
1725
1821
|
onCreateCategory={handleCreateCategory}
|
|
1726
|
-
onCreateInstructor={handleCreateInstructor}
|
|
1727
1822
|
onEditCategory={handleEditCategory}
|
|
1728
|
-
|
|
1823
|
+
compact
|
|
1824
|
+
/>
|
|
1825
|
+
<CourseInstructorsSummaryCard
|
|
1826
|
+
instructors={apiCourse?.instructors ?? []}
|
|
1827
|
+
compact
|
|
1729
1828
|
/>
|
|
1730
|
-
<CourseContentCard form={form} />
|
|
1829
|
+
<CourseContentCard form={form} compact />
|
|
1731
1830
|
</TabsContent>
|
|
1732
1831
|
|
|
1733
1832
|
{/* ── Tab: Mídia ──────────────────────────────────────────── */}
|
|
@@ -1780,6 +1879,7 @@ export function EditorCourse() {
|
|
|
1780
1879
|
logoImageType={apiCourse?.logoImageType}
|
|
1781
1880
|
bannerImageType={apiCourse?.bannerImageType}
|
|
1782
1881
|
t={t}
|
|
1882
|
+
compact
|
|
1783
1883
|
/>
|
|
1784
1884
|
</TabsContent>
|
|
1785
1885
|
|
|
@@ -1893,17 +1993,20 @@ export function EditorCourse() {
|
|
|
1893
1993
|
) : (
|
|
1894
1994
|
<div className="flex flex-col gap-1">
|
|
1895
1995
|
{courseResources.map((item) => {
|
|
1896
|
-
const
|
|
1996
|
+
const isDownloadingResource =
|
|
1997
|
+
downloadingCourseResourceKeys.has(
|
|
1998
|
+
String(item.fileId ?? item.id)
|
|
1999
|
+
);
|
|
2000
|
+
|
|
1897
2001
|
return (
|
|
1898
2002
|
<div
|
|
1899
2003
|
key={item.id}
|
|
1900
2004
|
className="flex items-center gap-2 rounded-md border bg-muted/20 px-2.5 py-2"
|
|
1901
2005
|
>
|
|
1902
|
-
<
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
)}
|
|
2006
|
+
<FileTypeIcon
|
|
2007
|
+
filename={item.name}
|
|
2008
|
+
mimeType={item.type}
|
|
2009
|
+
size={14}
|
|
1907
2010
|
/>
|
|
1908
2011
|
<div className="flex-1 min-w-0">
|
|
1909
2012
|
<p className="text-xs truncate font-medium">
|
|
@@ -1914,36 +2017,56 @@ export function EditorCourse() {
|
|
|
1914
2017
|
t('structureEditor.resources.attachedFile')}
|
|
1915
2018
|
</p>
|
|
1916
2019
|
</div>
|
|
1917
|
-
<
|
|
1918
|
-
|
|
1919
|
-
variant="ghost"
|
|
1920
|
-
size="icon"
|
|
1921
|
-
className="size-6 shrink-0 text-muted-foreground hover:text-foreground"
|
|
1922
|
-
onClick={() =>
|
|
1923
|
-
void handleDownloadCourseResource(item)
|
|
1924
|
-
}
|
|
1925
|
-
aria-label={t(
|
|
2020
|
+
<IconActionTooltip
|
|
2021
|
+
label={t(
|
|
1926
2022
|
'structureEditor.resources.downloadAria',
|
|
1927
2023
|
{ name: item.name }
|
|
1928
2024
|
)}
|
|
2025
|
+
asWrapper={isDownloadingResource}
|
|
1929
2026
|
>
|
|
1930
|
-
<
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
2027
|
+
<Button
|
|
2028
|
+
type="button"
|
|
2029
|
+
variant="ghost"
|
|
2030
|
+
size="icon"
|
|
2031
|
+
className="size-6 shrink-0 text-muted-foreground hover:text-foreground"
|
|
2032
|
+
disabled={isDownloadingResource}
|
|
2033
|
+
onClick={() =>
|
|
2034
|
+
void handleDownloadCourseResource(item)
|
|
2035
|
+
}
|
|
2036
|
+
aria-label={t(
|
|
2037
|
+
'structureEditor.resources.downloadAria',
|
|
2038
|
+
{ name: item.name }
|
|
2039
|
+
)}
|
|
2040
|
+
>
|
|
2041
|
+
{isDownloadingResource ? (
|
|
2042
|
+
<Loader2 className="size-3 animate-spin" />
|
|
2043
|
+
) : (
|
|
2044
|
+
<Download className="size-3" />
|
|
2045
|
+
)}
|
|
2046
|
+
</Button>
|
|
2047
|
+
</IconActionTooltip>
|
|
2048
|
+
<IconActionTooltip
|
|
2049
|
+
label={t(
|
|
1941
2050
|
'structureEditor.resources.removeAria',
|
|
1942
2051
|
{ name: item.name }
|
|
1943
2052
|
)}
|
|
1944
2053
|
>
|
|
1945
|
-
<
|
|
1946
|
-
|
|
2054
|
+
<Button
|
|
2055
|
+
type="button"
|
|
2056
|
+
variant="ghost"
|
|
2057
|
+
size="icon"
|
|
2058
|
+
className="size-6 shrink-0 text-muted-foreground hover:text-destructive"
|
|
2059
|
+
onClick={() =>
|
|
2060
|
+
void handleRemoveCourseResource(item)
|
|
2061
|
+
}
|
|
2062
|
+
aria-label={t(
|
|
2063
|
+
'structureEditor.resources.removeAria',
|
|
2064
|
+
{ name: item.name }
|
|
2065
|
+
)}
|
|
2066
|
+
>
|
|
2067
|
+
<X className="size-3" />
|
|
2068
|
+
</Button>
|
|
2069
|
+
</IconActionTooltip>
|
|
1947
2070
|
</div>
|
|
1948
2071
|
);
|
|
1949
2072
|
})}
|
|
@@ -1963,11 +2086,13 @@ export function EditorCourse() {
|
|
|
1963
2086
|
t={t}
|
|
1964
2087
|
options={certificateOptions}
|
|
1965
2088
|
onCreateTemplate={handleCreateCertificateTemplate}
|
|
2089
|
+
compact
|
|
1966
2090
|
/>
|
|
1967
|
-
<CourseFlagsCard form={form} t={t} />
|
|
2091
|
+
<CourseFlagsCard form={form} t={t} compact />
|
|
1968
2092
|
<CourseDangerZoneCard
|
|
1969
2093
|
t={t}
|
|
1970
2094
|
onDelete={() => setDeleteDialogOpen(true)}
|
|
2095
|
+
compact
|
|
1971
2096
|
/>
|
|
1972
2097
|
</TabsContent>
|
|
1973
2098
|
|
|
@@ -2087,36 +2212,48 @@ export function EditorCourse() {
|
|
|
2087
2212
|
)}
|
|
2088
2213
|
</p>
|
|
2089
2214
|
</div>
|
|
2090
|
-
<
|
|
2091
|
-
|
|
2092
|
-
variant="ghost"
|
|
2093
|
-
size="icon"
|
|
2094
|
-
className="size-6 shrink-0 text-muted-foreground hover:text-foreground"
|
|
2095
|
-
onClick={() =>
|
|
2096
|
-
handleEditVideoProfile(profileId)
|
|
2097
|
-
}
|
|
2098
|
-
aria-label={t(
|
|
2215
|
+
<IconActionTooltip
|
|
2216
|
+
label={t(
|
|
2099
2217
|
'structureEditor.videoProfiles.editAria'
|
|
2100
2218
|
)}
|
|
2101
2219
|
>
|
|
2102
|
-
<
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
)
|
|
2113
|
-
|
|
2114
|
-
|
|
2220
|
+
<Button
|
|
2221
|
+
type="button"
|
|
2222
|
+
variant="ghost"
|
|
2223
|
+
size="icon"
|
|
2224
|
+
className="size-6 shrink-0 text-muted-foreground hover:text-foreground"
|
|
2225
|
+
onClick={() =>
|
|
2226
|
+
handleEditVideoProfile(profileId)
|
|
2227
|
+
}
|
|
2228
|
+
aria-label={t(
|
|
2229
|
+
'structureEditor.videoProfiles.editAria'
|
|
2230
|
+
)}
|
|
2231
|
+
>
|
|
2232
|
+
<Pencil className="size-3" />
|
|
2233
|
+
</Button>
|
|
2234
|
+
</IconActionTooltip>
|
|
2235
|
+
<IconActionTooltip
|
|
2236
|
+
label={t(
|
|
2115
2237
|
'structureEditor.videoProfiles.removeAria'
|
|
2116
2238
|
)}
|
|
2117
2239
|
>
|
|
2118
|
-
<
|
|
2119
|
-
|
|
2240
|
+
<Button
|
|
2241
|
+
type="button"
|
|
2242
|
+
variant="ghost"
|
|
2243
|
+
size="icon"
|
|
2244
|
+
className="size-6 shrink-0 text-muted-foreground hover:text-destructive"
|
|
2245
|
+
onClick={() =>
|
|
2246
|
+
setLinkedProfileIds((prev) =>
|
|
2247
|
+
prev.filter((id) => id !== profileId)
|
|
2248
|
+
)
|
|
2249
|
+
}
|
|
2250
|
+
aria-label={t(
|
|
2251
|
+
'structureEditor.videoProfiles.removeAria'
|
|
2252
|
+
)}
|
|
2253
|
+
>
|
|
2254
|
+
<X className="size-3" />
|
|
2255
|
+
</Button>
|
|
2256
|
+
</IconActionTooltip>
|
|
2120
2257
|
</div>
|
|
2121
2258
|
);
|
|
2122
2259
|
})}
|
|
@@ -2245,33 +2382,6 @@ export function EditorCourse() {
|
|
|
2245
2382
|
</div>
|
|
2246
2383
|
</form>
|
|
2247
2384
|
|
|
2248
|
-
<InstructorFormSheet
|
|
2249
|
-
open={instructorEditSheetOpen}
|
|
2250
|
-
onOpenChange={(open) => {
|
|
2251
|
-
setInstructorEditSheetOpen(open);
|
|
2252
|
-
if (!open) {
|
|
2253
|
-
setEditingInstructorId(null);
|
|
2254
|
-
}
|
|
2255
|
-
}}
|
|
2256
|
-
instructorId={editingInstructorId}
|
|
2257
|
-
onSaved={async (instructor) => {
|
|
2258
|
-
if (editingInstructorId === null && instructor?.id) {
|
|
2259
|
-
const createdId = String(instructor.id);
|
|
2260
|
-
const current = form.getValues('instrutores') ?? [];
|
|
2261
|
-
if (!current.includes(createdId)) {
|
|
2262
|
-
form.setValue('instrutores', [...current, createdId], {
|
|
2263
|
-
shouldDirty: true,
|
|
2264
|
-
shouldTouch: true,
|
|
2265
|
-
shouldValidate: true,
|
|
2266
|
-
});
|
|
2267
|
-
}
|
|
2268
|
-
}
|
|
2269
|
-
|
|
2270
|
-
await refetchInstructorOptions();
|
|
2271
|
-
await refetchCourse();
|
|
2272
|
-
}}
|
|
2273
|
-
/>
|
|
2274
|
-
|
|
2275
2385
|
<Sheet
|
|
2276
2386
|
open={videoProfileEditSheetOpen}
|
|
2277
2387
|
onOpenChange={(open) => {
|
|
@@ -2459,27 +2569,3 @@ export function EditorCourse() {
|
|
|
2459
2569
|
</Form>
|
|
2460
2570
|
);
|
|
2461
2571
|
}
|
|
2462
|
-
|
|
2463
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
2464
|
-
|
|
2465
|
-
function StatChip({
|
|
2466
|
-
icon,
|
|
2467
|
-
label,
|
|
2468
|
-
value,
|
|
2469
|
-
}: {
|
|
2470
|
-
icon: React.ReactNode;
|
|
2471
|
-
label: string;
|
|
2472
|
-
value: string | number;
|
|
2473
|
-
}) {
|
|
2474
|
-
return (
|
|
2475
|
-
<div className="flex items-center gap-2 rounded-md border bg-background px-2.5 py-2">
|
|
2476
|
-
<span className="text-muted-foreground shrink-0">{icon}</span>
|
|
2477
|
-
<div className="min-w-0">
|
|
2478
|
-
<p className="text-[0.65rem] text-muted-foreground truncate">{label}</p>
|
|
2479
|
-
<p className="text-sm font-semibold tabular-nums leading-none">
|
|
2480
|
-
{value}
|
|
2481
|
-
</p>
|
|
2482
|
-
</div>
|
|
2483
|
-
</div>
|
|
2484
|
-
);
|
|
2485
|
-
}
|