@hed-hog/lms 0.0.365 → 0.0.370
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/certificate/certificate.controller.d.ts +1 -1
- package/dist/certificate/certificate.controller.d.ts.map +1 -1
- package/dist/certificate/certificate.controller.js +4 -2
- package/dist/certificate/certificate.controller.js.map +1 -1
- package/dist/certificate/certificate.service.d.ts +50 -0
- package/dist/certificate/certificate.service.d.ts.map +1 -1
- package/dist/certificate/certificate.service.js +73 -0
- package/dist/certificate/certificate.service.js.map +1 -1
- package/dist/class-group/class-group.controller.d.ts +1 -0
- package/dist/class-group/class-group.controller.d.ts.map +1 -1
- package/dist/class-group/class-group.service.d.ts +1 -0
- package/dist/class-group/class-group.service.d.ts.map +1 -1
- package/dist/course/course-ai-usage.service.d.ts +58 -0
- package/dist/course/course-ai-usage.service.d.ts.map +1 -0
- package/dist/course/course-ai-usage.service.js +176 -0
- package/dist/course/course-ai-usage.service.js.map +1 -0
- package/dist/course/course-audio-transcription.service.d.ts +65 -1
- package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
- package/dist/course/course-audio-transcription.service.js +381 -29
- package/dist/course/course-audio-transcription.service.js.map +1 -1
- package/dist/course/course-export-scorm12.service.d.ts +3 -0
- package/dist/course/course-export-scorm12.service.d.ts.map +1 -1
- package/dist/course/course-export-scorm12.service.js +141 -6
- package/dist/course/course-export-scorm12.service.js.map +1 -1
- package/dist/course/course-export.service.d.ts.map +1 -1
- package/dist/course/course-export.service.js +2 -1
- package/dist/course/course-export.service.js.map +1 -1
- package/dist/course/course-lesson.controller.d.ts +25 -3
- package/dist/course/course-lesson.controller.d.ts.map +1 -1
- package/dist/course/course-lesson.controller.js +71 -8
- package/dist/course/course-lesson.controller.js.map +1 -1
- package/dist/course/course-structure.controller.d.ts +30 -7
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +37 -4
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +37 -5
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +165 -20
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course-transcription-translation.service.d.ts +31 -0
- package/dist/course/course-transcription-translation.service.d.ts.map +1 -0
- package/dist/course/course-transcription-translation.service.js +227 -0
- package/dist/course/course-transcription-translation.service.js.map +1 -0
- package/dist/course/course-video-agent-pipeline.service.d.ts +70 -0
- package/dist/course/course-video-agent-pipeline.service.d.ts.map +1 -0
- package/dist/course/course-video-agent-pipeline.service.js +398 -0
- package/dist/course/course-video-agent-pipeline.service.js.map +1 -0
- package/dist/course/course-video-hls.service.d.ts +14 -0
- package/dist/course/course-video-hls.service.d.ts.map +1 -1
- package/dist/course/course-video-hls.service.js +25 -8
- package/dist/course/course-video-hls.service.js.map +1 -1
- package/dist/course/course.controller.d.ts +2 -0
- 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 +9 -0
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +2 -0
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +36 -2
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course-bulk-job.dto.d.ts +2 -1
- package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-bulk-job.dto.js +6 -1
- package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -1
- package/dist/course/dto/create-course-export.dto.d.ts +1 -0
- package/dist/course/dto/create-course-export.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-export.dto.js +6 -0
- package/dist/course/dto/create-course-export.dto.js.map +1 -1
- package/dist/course/ffmpeg.util.d.ts +10 -0
- package/dist/course/ffmpeg.util.d.ts.map +1 -0
- package/dist/course/ffmpeg.util.js +79 -0
- package/dist/course/ffmpeg.util.js.map +1 -0
- package/dist/course/lms-bulk-upload-automation.service.d.ts +3 -1
- package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload-automation.service.js +33 -16
- package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
- package/dist/course/lms-bulk-upload.controller.d.ts +3 -0
- package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.service.d.ts +3 -0
- package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.service.js +48 -29
- package/dist/course/lms-bulk-upload.service.js.map +1 -1
- package/dist/course/subtitle.util.d.ts +46 -0
- package/dist/course/subtitle.util.d.ts.map +1 -0
- package/dist/course/subtitle.util.js +206 -0
- package/dist/course/subtitle.util.js.map +1 -0
- package/dist/enterprise/training/training-admin.controller.d.ts +2 -0
- package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
- package/dist/enterprise/training/training-admin.service.d.ts +2 -0
- package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.d.ts +27 -0
- package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.js +197 -10
- package/dist/enterprise/training/training-student.service.js.map +1 -1
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +3 -1
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -1
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +19 -5
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
- package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -1
- package/dist/lesson-xp-map/lesson-xp-map.module.js +2 -1
- package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -1
- package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +14 -0
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/dto/heartbeat.dto.d.ts +9 -0
- package/dist/platforma/dto/heartbeat.dto.d.ts.map +1 -0
- package/dist/platforma/dto/heartbeat.dto.js +50 -0
- package/dist/platforma/dto/heartbeat.dto.js.map +1 -0
- package/dist/platforma/handlers/emit-certificate.handler.d.ts +27 -0
- package/dist/platforma/handlers/emit-certificate.handler.d.ts.map +1 -0
- package/dist/platforma/handlers/emit-certificate.handler.js +117 -0
- package/dist/platforma/handlers/emit-certificate.handler.js.map +1 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts +31 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts.map +1 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.js +281 -0
- package/dist/platforma/handlers/lesson-heartbeat.handler.js.map +1 -0
- package/dist/platforma/platforma-heartbeat.service.d.ts +10 -0
- package/dist/platforma/platforma-heartbeat.service.d.ts.map +1 -0
- package/dist/platforma/platforma-heartbeat.service.js +50 -0
- package/dist/platforma/platforma-heartbeat.service.js.map +1 -0
- package/dist/platforma/platforma-performance.service.d.ts +121 -0
- package/dist/platforma/platforma-performance.service.d.ts.map +1 -0
- package/dist/platforma/platforma-performance.service.js +500 -0
- package/dist/platforma/platforma-performance.service.js.map +1 -0
- package/dist/platforma/platforma-search.service.d.ts +21 -0
- package/dist/platforma/platforma-search.service.d.ts.map +1 -0
- package/dist/platforma/platforma-search.service.js +64 -0
- package/dist/platforma/platforma-search.service.js.map +1 -0
- package/dist/platforma/platforma-video.service.d.ts +8 -0
- package/dist/platforma/platforma-video.service.d.ts.map +1 -1
- package/dist/platforma/platforma-video.service.js +45 -2
- package/dist/platforma/platforma-video.service.js.map +1 -1
- package/dist/platforma/platforma.controller.d.ts +213 -1
- package/dist/platforma/platforma.controller.d.ts.map +1 -1
- package/dist/platforma/platforma.controller.js +159 -2
- package/dist/platforma/platforma.controller.js.map +1 -1
- package/dist/realtime/lms-realtime.controller.d.ts +2 -0
- package/dist/realtime/lms-realtime.controller.d.ts.map +1 -1
- package/dist/realtime/lms-realtime.controller.js +31 -0
- package/dist/realtime/lms-realtime.controller.js.map +1 -1
- package/dist/realtime/lms-realtime.service.d.ts +1 -1
- package/dist/realtime/lms-realtime.service.d.ts.map +1 -1
- package/dist/realtime/lms-realtime.service.js.map +1 -1
- package/dist/training/dto/create-training.dto.d.ts +9 -0
- package/dist/training/dto/create-training.dto.d.ts.map +1 -1
- package/dist/training/dto/create-training.dto.js +45 -1
- package/dist/training/dto/create-training.dto.js.map +1 -1
- package/dist/training/training.controller.d.ts +144 -0
- package/dist/training/training.controller.d.ts.map +1 -1
- package/dist/training/training.service.d.ts +149 -0
- package/dist/training/training.service.d.ts.map +1 -1
- package/dist/training/training.service.js +332 -167
- package/dist/training/training.service.js.map +1 -1
- package/hedhog/data/image_type.yaml +10 -0
- package/hedhog/data/route.yaml +251 -0
- package/hedhog/data/setting_group.yaml +97 -0
- package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +139 -27
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +182 -29
- package/hedhog/frontend/app/classes/_components/classes-calendar-view.tsx.ejs +277 -0
- package/hedhog/frontend/app/classes/page.tsx.ejs +127 -20
- package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +69 -57
- package/hedhog/frontend/app/courses/[id]/_components/CourseIssuedCertificatesCard.tsx.ejs +168 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-ai-costs-tab.tsx.ejs +191 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +81 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +12 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +141 -30
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +13 -13
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +69 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +11 -23
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +267 -19
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +114 -86
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +239 -31
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +344 -59
- package/hedhog/frontend/app/courses/[id]/structure/_components/lesson-video-preview.tsx.ejs +200 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +1 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +1 -8
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +19 -7
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-ai-costs.ts.ejs +40 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +2 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +25 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +148 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +157 -8
- package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +1 -22
- package/hedhog/frontend/app/courses/page.tsx.ejs +66 -13
- package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +6 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-classes-calendar-tab.tsx.ejs +264 -0
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +104 -47
- package/hedhog/frontend/app/exams/page.tsx.ejs +38 -4
- package/hedhog/frontend/app/instructors/page.tsx.ejs +87 -46
- package/hedhog/frontend/app/paths/page.tsx.ejs +650 -168
- package/hedhog/frontend/app/training/page.tsx.ejs +38 -4
- package/hedhog/frontend/messages/en.json +41 -12
- package/hedhog/frontend/messages/pt.json +44 -13
- package/hedhog/query/triggers.sql +33 -0
- package/hedhog/table/course_ai_usage.yaml +46 -0
- package/hedhog/table/course_enrollment.yaml +3 -0
- package/hedhog/table/course_lesson.yaml +3 -0
- package/hedhog/table/course_lesson_answer.yaml +37 -0
- package/hedhog/table/course_lesson_transcription_segment.yaml +8 -0
- package/hedhog/table/learning_path.yaml +6 -0
- package/hedhog/table/learning_path_module.yaml +22 -0
- package/hedhog/table/learning_path_step.yaml +9 -6
- package/hedhog/table/lesson_view_event.yaml +66 -0
- package/package.json +8 -7
- package/src/certificate/certificate.controller.ts +2 -0
- package/src/certificate/certificate.service.ts +99 -0
- package/src/course/course-ai-usage.service.ts +221 -0
- package/src/course/course-audio-transcription.service.ts +471 -43
- package/src/course/course-export-scorm12.service.ts +149 -5
- package/src/course/course-export.service.ts +1 -0
- package/src/course/course-lesson.controller.ts +59 -6
- package/src/course/course-structure.controller.ts +19 -1
- package/src/course/course-structure.service.ts +184 -10
- package/src/course/course-transcription-translation.service.ts +293 -0
- package/src/course/course-video-agent-pipeline.service.ts +471 -0
- package/src/course/course-video-hls.service.ts +30 -10
- package/src/course/course.module.ts +9 -0
- package/src/course/course.service.ts +46 -1
- package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
- package/src/course/dto/create-course-export.dto.ts +6 -0
- package/src/course/ffmpeg.util.ts +65 -0
- package/src/course/lms-bulk-upload-automation.service.ts +33 -8
- package/src/course/lms-bulk-upload.service.ts +20 -1
- package/src/course/subtitle.util.ts +220 -0
- package/src/enterprise/training/training-student.service.ts +224 -4
- package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +14 -0
- package/src/lesson-xp-map/lesson-xp-map.module.ts +2 -1
- package/src/lms.module.ts +14 -0
- package/src/platforma/dto/heartbeat.dto.ts +30 -0
- package/src/platforma/handlers/emit-certificate.handler.ts +117 -0
- package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -0
- package/src/platforma/platforma-heartbeat.service.ts +33 -0
- package/src/platforma/platforma-performance.service.ts +606 -0
- package/src/platforma/platforma-search.service.ts +48 -0
- package/src/platforma/platforma-video.service.ts +59 -3
- package/src/platforma/platforma.controller.ts +130 -0
- package/src/realtime/lms-realtime.controller.ts +27 -1
- package/src/realtime/lms-realtime.service.ts +2 -1
- package/src/training/dto/create-training.dto.ts +36 -0
- package/src/training/training.service.ts +360 -163
|
@@ -85,12 +85,14 @@ import {
|
|
|
85
85
|
Clock,
|
|
86
86
|
GraduationCap,
|
|
87
87
|
GripVertical,
|
|
88
|
+
ImageIcon,
|
|
88
89
|
Layers,
|
|
89
90
|
Loader2,
|
|
90
91
|
Pencil,
|
|
91
92
|
Plus,
|
|
92
93
|
Target,
|
|
93
94
|
Trash2,
|
|
95
|
+
Upload,
|
|
94
96
|
Users,
|
|
95
97
|
X,
|
|
96
98
|
} from 'lucide-react';
|
|
@@ -122,6 +124,21 @@ interface Formacao {
|
|
|
122
124
|
criadoEm: string;
|
|
123
125
|
primaryColor?: string;
|
|
124
126
|
secondaryColor?: string;
|
|
127
|
+
bannerFileId?: number | null;
|
|
128
|
+
certificateTemplate?: { id: number; name: string } | null;
|
|
129
|
+
modules?: Array<{
|
|
130
|
+
id: number;
|
|
131
|
+
title: string;
|
|
132
|
+
description: string | null;
|
|
133
|
+
order: number;
|
|
134
|
+
items: LearningPathItem[];
|
|
135
|
+
}>;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface CertificateTemplateOption {
|
|
139
|
+
id: number;
|
|
140
|
+
name: string;
|
|
141
|
+
status: string;
|
|
125
142
|
}
|
|
126
143
|
|
|
127
144
|
type TrainingColorPayload = {
|
|
@@ -159,6 +176,13 @@ interface LearningPathItem {
|
|
|
159
176
|
isRequired?: boolean;
|
|
160
177
|
}
|
|
161
178
|
|
|
179
|
+
interface ModuleState {
|
|
180
|
+
uid: string;
|
|
181
|
+
title: string;
|
|
182
|
+
description: string;
|
|
183
|
+
items: LearningPathItem[];
|
|
184
|
+
}
|
|
185
|
+
|
|
162
186
|
interface TrailRenderableItem {
|
|
163
187
|
uid: string;
|
|
164
188
|
type: 'course' | 'exam';
|
|
@@ -447,6 +471,8 @@ const formacaoSchema = z.object({
|
|
|
447
471
|
.string()
|
|
448
472
|
.regex(/^#([0-9A-Fa-f]{6})$/, 'Cor secundária inválida')
|
|
449
473
|
.default('#111827'),
|
|
474
|
+
bannerFileId: z.number().nullable().optional(),
|
|
475
|
+
certificateTemplateId: z.number().nullable().optional(),
|
|
450
476
|
});
|
|
451
477
|
|
|
452
478
|
type FormacaoForm = z.infer<typeof formacaoSchema>;
|
|
@@ -462,7 +488,7 @@ const STATUS_MAP: Record<
|
|
|
462
488
|
encerrada: { label: 'Encerrada', variant: 'outline' },
|
|
463
489
|
};
|
|
464
490
|
|
|
465
|
-
const
|
|
491
|
+
const DEFAULT_PAGE_SIZES = [6, 12, 24, 48, 96] as const;
|
|
466
492
|
const API_TRAINING_CACHE_KEY = 'lms:training:api-cache';
|
|
467
493
|
|
|
468
494
|
// ── Animations ────────────────────────────────────────────────────────────────
|
|
@@ -535,6 +561,227 @@ function SortableTrailItem(props: {
|
|
|
535
561
|
);
|
|
536
562
|
}
|
|
537
563
|
|
|
564
|
+
function SortableModule(props: {
|
|
565
|
+
module: ModuleState;
|
|
566
|
+
availableCursos: CursoOption[];
|
|
567
|
+
availableExams: ExameOption[];
|
|
568
|
+
sensors: ReturnType<typeof useSensors>;
|
|
569
|
+
onUpdateTitle: (uid: string, title: string) => void;
|
|
570
|
+
onUpdateDescription: (uid: string, description: string) => void;
|
|
571
|
+
onRemoveModule: (uid: string) => void;
|
|
572
|
+
onAddItem: (moduleUid: string, type: 'course' | 'exam', itemId: number) => void;
|
|
573
|
+
onRemoveItem: (moduleUid: string, itemUid: string) => void;
|
|
574
|
+
onDragEndItems: (moduleUid: string, event: DragEndEvent) => void;
|
|
575
|
+
onOpenCreateCourseSheet: (moduleUid: string) => void;
|
|
576
|
+
onOpenCreateExamSheet: (moduleUid: string) => void;
|
|
577
|
+
availableCursosForModule: CursoOption[];
|
|
578
|
+
availableExamsForModule: ExameOption[];
|
|
579
|
+
}) {
|
|
580
|
+
const { module, sensors } = props;
|
|
581
|
+
const { attributes, listeners, setNodeRef, transform, transition } =
|
|
582
|
+
useSortable({ id: module.uid });
|
|
583
|
+
|
|
584
|
+
const style = {
|
|
585
|
+
transform: CSS.Transform.toString(transform),
|
|
586
|
+
transition,
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
const [selectedCourse, setSelectedCourse] = useState('');
|
|
590
|
+
const [selectedExam, setSelectedExam] = useState('');
|
|
591
|
+
|
|
592
|
+
const renderableItems = useMemo<TrailRenderableItem[]>(() => {
|
|
593
|
+
return [...module.items]
|
|
594
|
+
.sort((a, b) => a.order - b.order)
|
|
595
|
+
.map((item, index) => {
|
|
596
|
+
if (item.type === 'course') {
|
|
597
|
+
const course = props.availableCursos.find((c) => c.id === item.itemId);
|
|
598
|
+
if (!course) return null;
|
|
599
|
+
return {
|
|
600
|
+
uid: `course-${course.id}`,
|
|
601
|
+
type: 'course' as const,
|
|
602
|
+
itemId: course.id,
|
|
603
|
+
title: course.nome,
|
|
604
|
+
subtitle: `${course.cargaHoraria}h`,
|
|
605
|
+
order: item.order ?? index,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
const exam = props.availableExams.find((e) => e.id === item.itemId);
|
|
609
|
+
if (!exam) return null;
|
|
610
|
+
return {
|
|
611
|
+
uid: `exam-${exam.id}`,
|
|
612
|
+
type: 'exam' as const,
|
|
613
|
+
itemId: exam.id,
|
|
614
|
+
title: exam.titulo,
|
|
615
|
+
subtitle: `${exam.limiteTempo}min`,
|
|
616
|
+
order: item.order ?? index,
|
|
617
|
+
};
|
|
618
|
+
})
|
|
619
|
+
.filter(Boolean) as TrailRenderableItem[];
|
|
620
|
+
}, [module.items, props.availableCursos, props.availableExams]);
|
|
621
|
+
|
|
622
|
+
function handleCourseSelect(value: string) {
|
|
623
|
+
setSelectedCourse(value);
|
|
624
|
+
const parsed = Number(value);
|
|
625
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
626
|
+
props.onAddItem(module.uid, 'course', parsed);
|
|
627
|
+
setSelectedCourse('');
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function handleExamSelect(value: string) {
|
|
632
|
+
setSelectedExam(value);
|
|
633
|
+
const parsed = Number(value);
|
|
634
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
635
|
+
props.onAddItem(module.uid, 'exam', parsed);
|
|
636
|
+
setSelectedExam('');
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return (
|
|
641
|
+
<div ref={setNodeRef} style={style} className="rounded-md border bg-background">
|
|
642
|
+
{/* Module header */}
|
|
643
|
+
<div className="flex items-start gap-2 border-b p-3">
|
|
644
|
+
<Button
|
|
645
|
+
type="button"
|
|
646
|
+
variant="ghost"
|
|
647
|
+
size="icon"
|
|
648
|
+
className="mt-1 size-7 shrink-0 cursor-grab text-muted-foreground"
|
|
649
|
+
aria-label="Arrastar módulo"
|
|
650
|
+
{...attributes}
|
|
651
|
+
{...listeners}
|
|
652
|
+
>
|
|
653
|
+
<GripVertical className="size-4" />
|
|
654
|
+
</Button>
|
|
655
|
+
<div className="flex flex-1 flex-col gap-1">
|
|
656
|
+
<Input
|
|
657
|
+
placeholder="Título do módulo"
|
|
658
|
+
value={module.title}
|
|
659
|
+
onChange={(e) => props.onUpdateTitle(module.uid, e.target.value)}
|
|
660
|
+
className="h-8 text-sm font-medium"
|
|
661
|
+
/>
|
|
662
|
+
<Input
|
|
663
|
+
placeholder="Descrição (opcional)"
|
|
664
|
+
value={module.description}
|
|
665
|
+
onChange={(e) => props.onUpdateDescription(module.uid, e.target.value)}
|
|
666
|
+
className="h-7 text-xs text-muted-foreground"
|
|
667
|
+
/>
|
|
668
|
+
</div>
|
|
669
|
+
<Button
|
|
670
|
+
type="button"
|
|
671
|
+
variant="ghost"
|
|
672
|
+
size="icon"
|
|
673
|
+
className="mt-1 size-7 shrink-0 text-muted-foreground hover:text-destructive"
|
|
674
|
+
onClick={() => props.onRemoveModule(module.uid)}
|
|
675
|
+
>
|
|
676
|
+
<X className="size-4" />
|
|
677
|
+
</Button>
|
|
678
|
+
</div>
|
|
679
|
+
|
|
680
|
+
{/* Item selectors */}
|
|
681
|
+
<div className="space-y-2 bg-muted/20 p-3">
|
|
682
|
+
<div className="grid grid-cols-1 gap-2 sm:grid-cols-[minmax(0,1fr)_auto]">
|
|
683
|
+
<Select
|
|
684
|
+
value={selectedCourse}
|
|
685
|
+
onValueChange={handleCourseSelect}
|
|
686
|
+
disabled={props.availableCursosForModule.length === 0}
|
|
687
|
+
>
|
|
688
|
+
<SelectTrigger className="w-full">
|
|
689
|
+
<SelectValue placeholder="Adicionar curso..." />
|
|
690
|
+
</SelectTrigger>
|
|
691
|
+
<SelectContent>
|
|
692
|
+
{props.availableCursosForModule.map((course) => (
|
|
693
|
+
<SelectItem key={course.id} value={String(course.id)}>
|
|
694
|
+
<div className="flex items-center gap-2">
|
|
695
|
+
<CourseAvatar
|
|
696
|
+
fileId={course.logoFileId}
|
|
697
|
+
title={course.nome}
|
|
698
|
+
className="size-5 shrink-0 rounded"
|
|
699
|
+
iconSize="size-3"
|
|
700
|
+
/>
|
|
701
|
+
<div className="min-w-0">
|
|
702
|
+
<span className="truncate">{course.nome}</span>
|
|
703
|
+
<span className="ml-1.5 text-xs text-muted-foreground">
|
|
704
|
+
{course.name ?? '—'} · #{course.id}
|
|
705
|
+
</span>
|
|
706
|
+
</div>
|
|
707
|
+
</div>
|
|
708
|
+
</SelectItem>
|
|
709
|
+
))}
|
|
710
|
+
</SelectContent>
|
|
711
|
+
</Select>
|
|
712
|
+
<Button
|
|
713
|
+
type="button"
|
|
714
|
+
variant="outline"
|
|
715
|
+
size="sm"
|
|
716
|
+
className="shrink-0"
|
|
717
|
+
onClick={() => props.onOpenCreateCourseSheet(module.uid)}
|
|
718
|
+
>
|
|
719
|
+
<Plus className="size-4" />
|
|
720
|
+
</Button>
|
|
721
|
+
</div>
|
|
722
|
+
|
|
723
|
+
<div className="grid grid-cols-1 gap-2 sm:grid-cols-[minmax(0,1fr)_auto]">
|
|
724
|
+
<Select
|
|
725
|
+
value={selectedExam}
|
|
726
|
+
onValueChange={handleExamSelect}
|
|
727
|
+
disabled={props.availableExamsForModule.length === 0}
|
|
728
|
+
>
|
|
729
|
+
<SelectTrigger className="w-full">
|
|
730
|
+
<SelectValue placeholder="Adicionar exame..." />
|
|
731
|
+
</SelectTrigger>
|
|
732
|
+
<SelectContent>
|
|
733
|
+
{props.availableExamsForModule.map((exam) => (
|
|
734
|
+
<SelectItem key={exam.id} value={String(exam.id)}>
|
|
735
|
+
{exam.titulo} ({exam.limiteTempo}min)
|
|
736
|
+
</SelectItem>
|
|
737
|
+
))}
|
|
738
|
+
</SelectContent>
|
|
739
|
+
</Select>
|
|
740
|
+
<Button
|
|
741
|
+
type="button"
|
|
742
|
+
variant="outline"
|
|
743
|
+
size="sm"
|
|
744
|
+
className="shrink-0"
|
|
745
|
+
onClick={() => props.onOpenCreateExamSheet(module.uid)}
|
|
746
|
+
>
|
|
747
|
+
<Plus className="size-4" />
|
|
748
|
+
</Button>
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
|
|
752
|
+
{/* Items list */}
|
|
753
|
+
{renderableItems.length > 0 ? (
|
|
754
|
+
<div className="bg-background">
|
|
755
|
+
<DndContext
|
|
756
|
+
sensors={sensors}
|
|
757
|
+
collisionDetection={closestCenter}
|
|
758
|
+
onDragEnd={(event) => props.onDragEndItems(module.uid, event)}
|
|
759
|
+
>
|
|
760
|
+
<SortableContext
|
|
761
|
+
items={renderableItems.map((item) => item.uid)}
|
|
762
|
+
strategy={verticalListSortingStrategy}
|
|
763
|
+
>
|
|
764
|
+
{renderableItems.map((item) => (
|
|
765
|
+
<SortableTrailItem
|
|
766
|
+
key={item.uid}
|
|
767
|
+
item={item}
|
|
768
|
+
onRemove={(uid) => props.onRemoveItem(module.uid, uid)}
|
|
769
|
+
/>
|
|
770
|
+
))}
|
|
771
|
+
</SortableContext>
|
|
772
|
+
</DndContext>
|
|
773
|
+
</div>
|
|
774
|
+
) : (
|
|
775
|
+
<div className="p-3">
|
|
776
|
+
<p className="text-xs text-muted-foreground">
|
|
777
|
+
Adicione cursos e/ou exames a este módulo.
|
|
778
|
+
</p>
|
|
779
|
+
</div>
|
|
780
|
+
)}
|
|
781
|
+
</div>
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
|
|
538
785
|
// ── Page ──────────────────────────────────────────────────────────────────────
|
|
539
786
|
|
|
540
787
|
export default function TrainingPage() {
|
|
@@ -553,6 +800,8 @@ export default function TrainingPage() {
|
|
|
553
800
|
const [learningPathItems, setLearningPathItems] = useState<
|
|
554
801
|
LearningPathItem[]
|
|
555
802
|
>([]);
|
|
803
|
+
const [modules, setModules] = useState<ModuleState[]>([]);
|
|
804
|
+
const activeModuleUidRef = useRef<string | null>(null);
|
|
556
805
|
const [selectedCourseToAdd, setSelectedCourseToAdd] = useState('');
|
|
557
806
|
const [selectedExamToAdd, setSelectedExamToAdd] = useState('');
|
|
558
807
|
const [saving, setSaving] = useState(false);
|
|
@@ -564,6 +813,9 @@ export default function TrainingPage() {
|
|
|
564
813
|
const [cachedListData, setCachedListData] =
|
|
565
814
|
useState<ApiTrainingListResponse | null>(null);
|
|
566
815
|
const initialLearningPathRef = useRef<LearningPathItem[]>([]);
|
|
816
|
+
const [bannerPreviewUrl, setBannerPreviewUrl] = useState<string | null>(null);
|
|
817
|
+
const [bannerUploading, setBannerUploading] = useState(false);
|
|
818
|
+
const bannerInputRef = useRef<HTMLInputElement>(null);
|
|
567
819
|
|
|
568
820
|
// Search/filter inputs
|
|
569
821
|
const [buscaInput, setBuscaInput] = useState('');
|
|
@@ -578,10 +830,44 @@ export default function TrainingPage() {
|
|
|
578
830
|
|
|
579
831
|
// Pagination
|
|
580
832
|
const [currentPage, setCurrentPage] = useState(1);
|
|
833
|
+
|
|
834
|
+
const { data: generalSettings } = useQuery<{
|
|
835
|
+
data: Array<{ slug: string; value: string }>;
|
|
836
|
+
}>({
|
|
837
|
+
queryKey: ['setting-group-general'],
|
|
838
|
+
queryFn: async () => {
|
|
839
|
+
const response = await request<{
|
|
840
|
+
data: Array<{ slug: string; value: string }>;
|
|
841
|
+
}>({
|
|
842
|
+
url: '/setting/group/general',
|
|
843
|
+
method: 'GET',
|
|
844
|
+
});
|
|
845
|
+
return response.data;
|
|
846
|
+
},
|
|
847
|
+
staleTime: 5 * 60 * 1000,
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
const pageSizeOptions = useMemo(() => {
|
|
851
|
+
const setting = generalSettings?.data?.find(
|
|
852
|
+
(s) => s.slug === 'pagination-page-sizes'
|
|
853
|
+
);
|
|
854
|
+
if (!setting?.value) return DEFAULT_PAGE_SIZES;
|
|
855
|
+
try {
|
|
856
|
+
const parsed = JSON.parse(setting.value) as string[];
|
|
857
|
+
const sizes = parsed
|
|
858
|
+
.map(Number)
|
|
859
|
+
.filter((n) => !isNaN(n) && n > 0)
|
|
860
|
+
.sort((a, b) => a - b);
|
|
861
|
+
return sizes.length > 0 ? sizes : DEFAULT_PAGE_SIZES;
|
|
862
|
+
} catch {
|
|
863
|
+
return DEFAULT_PAGE_SIZES;
|
|
864
|
+
}
|
|
865
|
+
}, [generalSettings]);
|
|
866
|
+
|
|
581
867
|
const [pageSize, setPageSize] = usePersistedPageSize({
|
|
582
868
|
storageKey: 'pagination:global:pageSize',
|
|
583
869
|
defaultValue: 12,
|
|
584
|
-
allowedValues:
|
|
870
|
+
allowedValues: pageSizeOptions,
|
|
585
871
|
});
|
|
586
872
|
|
|
587
873
|
const sensors = useSensors(
|
|
@@ -712,6 +998,23 @@ export default function TrainingPage() {
|
|
|
712
998
|
},
|
|
713
999
|
});
|
|
714
1000
|
|
|
1001
|
+
const { data: certificateTemplatesData } = useQuery<{
|
|
1002
|
+
data: CertificateTemplateOption[];
|
|
1003
|
+
}>({
|
|
1004
|
+
queryKey: ['lms-certificate-templates-options'],
|
|
1005
|
+
queryFn: async () => {
|
|
1006
|
+
const response = await request<{ data: CertificateTemplateOption[] }>({
|
|
1007
|
+
url: '/lms/certificates/templates',
|
|
1008
|
+
method: 'GET',
|
|
1009
|
+
params: { page: 1, pageSize: 200, status: 'active' },
|
|
1010
|
+
});
|
|
1011
|
+
return response.data;
|
|
1012
|
+
},
|
|
1013
|
+
initialData: { data: [] },
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
const availableCertificateTemplates = certificateTemplatesData?.data ?? [];
|
|
1017
|
+
|
|
715
1018
|
useEffect(() => {
|
|
716
1019
|
if (courseSheetOpen) {
|
|
717
1020
|
void refetchCategories();
|
|
@@ -955,6 +1258,9 @@ export default function TrainingPage() {
|
|
|
955
1258
|
setEditingFormacao(null);
|
|
956
1259
|
initialLearningPathRef.current = [];
|
|
957
1260
|
setLearningPathItems([]);
|
|
1261
|
+
setModules([{ uid: `module-new-${Date.now()}`, title: '', description: '', items: [] }]);
|
|
1262
|
+
activeModuleUidRef.current = null;
|
|
1263
|
+
setBannerPreviewUrl(null);
|
|
958
1264
|
form.reset({
|
|
959
1265
|
nome: '',
|
|
960
1266
|
descricao: '',
|
|
@@ -965,6 +1271,7 @@ export default function TrainingPage() {
|
|
|
965
1271
|
status: 'rascunho',
|
|
966
1272
|
primaryColor: '#1D4ED8',
|
|
967
1273
|
secondaryColor: '#111827',
|
|
1274
|
+
certificateTemplateId: null,
|
|
968
1275
|
});
|
|
969
1276
|
examForm.reset();
|
|
970
1277
|
setSelectedCourseToAdd('');
|
|
@@ -1011,6 +1318,38 @@ export default function TrainingPage() {
|
|
|
1011
1318
|
setLearningPathItems(normalizedItems);
|
|
1012
1319
|
setSelectedCourseToAdd('');
|
|
1013
1320
|
setSelectedExamToAdd('');
|
|
1321
|
+
|
|
1322
|
+
if (fullFormacao.modules && fullFormacao.modules.length > 0) {
|
|
1323
|
+
setModules(
|
|
1324
|
+
fullFormacao.modules.map((mod) => ({
|
|
1325
|
+
uid: `module-${mod.id}`,
|
|
1326
|
+
title: mod.title,
|
|
1327
|
+
description: mod.description ?? '',
|
|
1328
|
+
items: mod.items.map((item, i) => ({
|
|
1329
|
+
id: item.id,
|
|
1330
|
+
type: item.type,
|
|
1331
|
+
itemId: item.itemId,
|
|
1332
|
+
order: item.order ?? i,
|
|
1333
|
+
isRequired: item.isRequired !== false,
|
|
1334
|
+
})),
|
|
1335
|
+
}))
|
|
1336
|
+
);
|
|
1337
|
+
} else {
|
|
1338
|
+
setModules([{ uid: `module-new-${Date.now()}`, title: '', description: '', items: normalizedItems }]);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
setBannerPreviewUrl(null);
|
|
1342
|
+
if (fullFormacao.bannerFileId) {
|
|
1343
|
+
request<{ url?: string }>({
|
|
1344
|
+
url: `/file/open/${fullFormacao.bannerFileId}`,
|
|
1345
|
+
method: 'PUT',
|
|
1346
|
+
})
|
|
1347
|
+
.then((res) => {
|
|
1348
|
+
if (res?.data?.url) setBannerPreviewUrl(res.data.url);
|
|
1349
|
+
})
|
|
1350
|
+
.catch(() => null);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1014
1353
|
form.reset({
|
|
1015
1354
|
nome: fullFormacao.nome,
|
|
1016
1355
|
descricao: fullFormacao.descricao,
|
|
@@ -1023,6 +1362,8 @@ export default function TrainingPage() {
|
|
|
1023
1362
|
status: normalizeStatusValue(fullFormacao.status),
|
|
1024
1363
|
primaryColor: fullFormacao.primaryColor ?? '#1D4ED8',
|
|
1025
1364
|
secondaryColor: fullFormacao.secondaryColor ?? '#111827',
|
|
1365
|
+
bannerFileId: fullFormacao.bannerFileId ?? null,
|
|
1366
|
+
certificateTemplateId: fullFormacao.certificateTemplate?.id ?? null,
|
|
1026
1367
|
});
|
|
1027
1368
|
setSheetOpen(true);
|
|
1028
1369
|
} finally {
|
|
@@ -1104,12 +1445,96 @@ export default function TrainingPage() {
|
|
|
1104
1445
|
});
|
|
1105
1446
|
}
|
|
1106
1447
|
|
|
1107
|
-
function
|
|
1448
|
+
function addModule() {
|
|
1449
|
+
setModules((prev) => [
|
|
1450
|
+
...prev,
|
|
1451
|
+
{ uid: `module-new-${Date.now()}`, title: '', description: '', items: [] },
|
|
1452
|
+
]);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function removeModule(uid: string) {
|
|
1456
|
+
setModules((prev) => prev.filter((m) => m.uid !== uid));
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function updateModuleTitle(uid: string, title: string) {
|
|
1460
|
+
setModules((prev) => prev.map((m) => (m.uid === uid ? { ...m, title } : m)));
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
function updateModuleDescription(uid: string, description: string) {
|
|
1464
|
+
setModules((prev) => prev.map((m) => (m.uid === uid ? { ...m, description } : m)));
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
function addItemToModule(moduleUid: string, type: 'course' | 'exam', itemId: number) {
|
|
1468
|
+
setModules((prev) =>
|
|
1469
|
+
prev.map((m) => {
|
|
1470
|
+
if (m.uid !== moduleUid) return m;
|
|
1471
|
+
if (m.items.some((i) => i.type === type && i.itemId === itemId)) return m;
|
|
1472
|
+
return {
|
|
1473
|
+
...m,
|
|
1474
|
+
items: normalizeTrailOrder([
|
|
1475
|
+
...m.items,
|
|
1476
|
+
{ type, itemId, order: m.items.length, isRequired: true },
|
|
1477
|
+
]),
|
|
1478
|
+
};
|
|
1479
|
+
})
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
function removeItemFromModule(moduleUid: string, itemUid: string) {
|
|
1484
|
+
const [type, idText] = itemUid.split('-');
|
|
1485
|
+
const itemId = Number(idText);
|
|
1486
|
+
if (!itemId || (type !== 'course' && type !== 'exam')) return;
|
|
1487
|
+
setModules((prev) =>
|
|
1488
|
+
prev.map((m) => {
|
|
1489
|
+
if (m.uid !== moduleUid) return m;
|
|
1490
|
+
return {
|
|
1491
|
+
...m,
|
|
1492
|
+
items: normalizeTrailOrder(
|
|
1493
|
+
m.items.filter((i) => !(i.type === type && i.itemId === itemId))
|
|
1494
|
+
),
|
|
1495
|
+
};
|
|
1496
|
+
})
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function handleModuleDragEnd(event: DragEndEvent) {
|
|
1501
|
+
const { active, over } = event;
|
|
1502
|
+
if (!over || active.id === over.id) return;
|
|
1503
|
+
setModules((prev) => {
|
|
1504
|
+
const oldIndex = prev.findIndex((m) => m.uid === String(active.id));
|
|
1505
|
+
const newIndex = prev.findIndex((m) => m.uid === String(over.id));
|
|
1506
|
+
if (oldIndex < 0 || newIndex < 0) return prev;
|
|
1507
|
+
return arrayMove(prev, oldIndex, newIndex);
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function handleModuleItemDragEnd(moduleUid: string, event: DragEndEvent) {
|
|
1512
|
+
const { active, over } = event;
|
|
1513
|
+
if (!over || active.id === over.id) return;
|
|
1514
|
+
setModules((prev) =>
|
|
1515
|
+
prev.map((m) => {
|
|
1516
|
+
if (m.uid !== moduleUid) return m;
|
|
1517
|
+
const sorted = [...m.items].sort((a, b) => a.order - b.order);
|
|
1518
|
+
const oldIndex = sorted.findIndex(
|
|
1519
|
+
(i) => `${i.type}-${i.itemId}` === String(active.id)
|
|
1520
|
+
);
|
|
1521
|
+
const newIndex = sorted.findIndex(
|
|
1522
|
+
(i) => `${i.type}-${i.itemId}` === String(over.id)
|
|
1523
|
+
);
|
|
1524
|
+
if (oldIndex < 0 || newIndex < 0) return m;
|
|
1525
|
+
return { ...m, items: normalizeTrailOrder(arrayMove(sorted, oldIndex, newIndex)) };
|
|
1526
|
+
})
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
function openCreateCourseSheet(moduleUid?: string) {
|
|
1531
|
+
if (moduleUid) activeModuleUidRef.current = moduleUid;
|
|
1108
1532
|
courseForm.reset(DEFAULT_COURSE_FORM_VALUES);
|
|
1109
1533
|
setCourseSheetOpen(true);
|
|
1110
1534
|
}
|
|
1111
1535
|
|
|
1112
|
-
function openCreateExamSheet() {
|
|
1536
|
+
function openCreateExamSheet(moduleUid?: string) {
|
|
1537
|
+
if (moduleUid) activeModuleUidRef.current = moduleUid;
|
|
1113
1538
|
examForm.reset({
|
|
1114
1539
|
titulo: '',
|
|
1115
1540
|
notaMinima: 7,
|
|
@@ -1165,28 +1590,71 @@ export default function TrainingPage() {
|
|
|
1165
1590
|
});
|
|
1166
1591
|
}
|
|
1167
1592
|
|
|
1593
|
+
async function handleBannerUpload(e: React.ChangeEvent<HTMLInputElement>) {
|
|
1594
|
+
const file = e.target.files?.[0];
|
|
1595
|
+
if (!file) return;
|
|
1596
|
+
|
|
1597
|
+
setBannerUploading(true);
|
|
1598
|
+
try {
|
|
1599
|
+
const formData = new FormData();
|
|
1600
|
+
formData.append('file', file);
|
|
1601
|
+
const uploadRes = await request<{ id?: number }>({
|
|
1602
|
+
url: '/file',
|
|
1603
|
+
method: 'POST',
|
|
1604
|
+
data: formData,
|
|
1605
|
+
headers: { 'Content-Type': 'multipart/form-data' },
|
|
1606
|
+
});
|
|
1607
|
+
const fileId = uploadRes?.data?.id;
|
|
1608
|
+
if (!fileId) return;
|
|
1609
|
+
|
|
1610
|
+
const openRes = await request<{ url?: string }>({
|
|
1611
|
+
url: `/file/open/${fileId}`,
|
|
1612
|
+
method: 'PUT',
|
|
1613
|
+
});
|
|
1614
|
+
if (openRes?.data?.url) setBannerPreviewUrl(openRes.data.url);
|
|
1615
|
+
form.setValue('bannerFileId', fileId, { shouldDirty: true });
|
|
1616
|
+
} finally {
|
|
1617
|
+
setBannerUploading(false);
|
|
1618
|
+
if (bannerInputRef.current) bannerInputRef.current.value = '';
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function handleRemoveBanner() {
|
|
1623
|
+
setBannerPreviewUrl(null);
|
|
1624
|
+
form.setValue('bannerFileId', null, { shouldDirty: true });
|
|
1625
|
+
if (bannerInputRef.current) bannerInputRef.current.value = '';
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1168
1628
|
async function onSubmit(data: FormacaoForm) {
|
|
1169
1629
|
const toApiProgressMode = (value: FormacaoForm['progressionMode']) =>
|
|
1170
1630
|
value === 'livre' ? 'free' : 'sequential';
|
|
1171
1631
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1632
|
+
if (modules.some((m) => !m.title.trim())) {
|
|
1633
|
+
toast.error('Todos os módulos precisam de um título.');
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
if (modules.length === 0) {
|
|
1637
|
+
toast.error(t('toasts.selectAtLeastOneItem'));
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1176
1640
|
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1641
|
+
const modulesPayload = modules.map((mod, i) => ({
|
|
1642
|
+
title: mod.title.trim(),
|
|
1643
|
+
description: mod.description.trim() || undefined,
|
|
1644
|
+
order: i,
|
|
1645
|
+
items: mod.items.map((item, j) => ({
|
|
1646
|
+
type: item.type,
|
|
1647
|
+
itemId: item.itemId,
|
|
1648
|
+
order: j,
|
|
1649
|
+
isRequired: item.isRequired !== false,
|
|
1650
|
+
})),
|
|
1651
|
+
}));
|
|
1181
1652
|
|
|
1653
|
+
try {
|
|
1182
1654
|
setSaving(true);
|
|
1183
1655
|
|
|
1184
1656
|
if (editingFormacao) {
|
|
1185
1657
|
const dirty = form.formState.dirtyFields;
|
|
1186
|
-
const itemsChanged = !pathsAreEqual(
|
|
1187
|
-
initialLearningPathRef.current,
|
|
1188
|
-
orderedItems
|
|
1189
|
-
);
|
|
1190
1658
|
|
|
1191
1659
|
const payload: {
|
|
1192
1660
|
title?: string;
|
|
@@ -1197,12 +1665,9 @@ export default function TrainingPage() {
|
|
|
1197
1665
|
progressMode?: 'sequential' | 'free';
|
|
1198
1666
|
primaryColor?: string;
|
|
1199
1667
|
secondaryColor?: string;
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
order: number;
|
|
1204
|
-
isRequired: boolean;
|
|
1205
|
-
}>;
|
|
1668
|
+
bannerFileId?: number | null;
|
|
1669
|
+
certificateTemplateId?: number | null;
|
|
1670
|
+
modules?: typeof modulesPayload;
|
|
1206
1671
|
} = {};
|
|
1207
1672
|
|
|
1208
1673
|
if (dirty.nome) payload.title = data.nome;
|
|
@@ -1217,19 +1682,9 @@ export default function TrainingPage() {
|
|
|
1217
1682
|
payload.progressMode = toApiProgressMode(data.progressionMode);
|
|
1218
1683
|
if (dirty.primaryColor) payload.primaryColor = data.primaryColor;
|
|
1219
1684
|
if (dirty.secondaryColor) payload.secondaryColor = data.secondaryColor;
|
|
1220
|
-
if (
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
itemId: item.itemId,
|
|
1224
|
-
order: index,
|
|
1225
|
-
isRequired: item.isRequired !== false,
|
|
1226
|
-
}));
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
if (Object.keys(payload).length === 0) {
|
|
1230
|
-
setSheetOpen(false);
|
|
1231
|
-
return;
|
|
1232
|
-
}
|
|
1685
|
+
if (dirty.bannerFileId) payload.bannerFileId = data.bannerFileId ?? null;
|
|
1686
|
+
if (dirty.certificateTemplateId) payload.certificateTemplateId = data.certificateTemplateId ?? null;
|
|
1687
|
+
payload.modules = modulesPayload;
|
|
1233
1688
|
|
|
1234
1689
|
await request({
|
|
1235
1690
|
url: `/lms/paths/${editingFormacao.id}`,
|
|
@@ -1247,12 +1702,9 @@ export default function TrainingPage() {
|
|
|
1247
1702
|
progressMode: toApiProgressMode(data.progressionMode),
|
|
1248
1703
|
primaryColor: data.primaryColor,
|
|
1249
1704
|
secondaryColor: data.secondaryColor,
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
order: index,
|
|
1254
|
-
isRequired: item.isRequired !== false,
|
|
1255
|
-
})),
|
|
1705
|
+
bannerFileId: data.bannerFileId ?? null,
|
|
1706
|
+
certificateTemplateId: data.certificateTemplateId ?? null,
|
|
1707
|
+
modules: modulesPayload,
|
|
1256
1708
|
};
|
|
1257
1709
|
|
|
1258
1710
|
await request({
|
|
@@ -1266,6 +1718,7 @@ export default function TrainingPage() {
|
|
|
1266
1718
|
await Promise.all([refetchTraining(), refetchStats()]);
|
|
1267
1719
|
setSheetOpen(false);
|
|
1268
1720
|
setLearningPathItems([]);
|
|
1721
|
+
setModules([]);
|
|
1269
1722
|
initialLearningPathRef.current = [];
|
|
1270
1723
|
} finally {
|
|
1271
1724
|
setSaving(false);
|
|
@@ -1305,7 +1758,12 @@ export default function TrainingPage() {
|
|
|
1305
1758
|
|
|
1306
1759
|
await refetchCourses();
|
|
1307
1760
|
if (Number.isFinite(createdCourseId) && createdCourseId > 0) {
|
|
1308
|
-
|
|
1761
|
+
const targetUid = activeModuleUidRef.current ?? modules[modules.length - 1]?.uid;
|
|
1762
|
+
if (targetUid) {
|
|
1763
|
+
addItemToModule(targetUid, 'course', createdCourseId);
|
|
1764
|
+
} else {
|
|
1765
|
+
addTrailItem('course', createdCourseId);
|
|
1766
|
+
}
|
|
1309
1767
|
setSelectedCourseToAdd('');
|
|
1310
1768
|
}
|
|
1311
1769
|
|
|
@@ -1336,7 +1794,12 @@ export default function TrainingPage() {
|
|
|
1336
1794
|
|
|
1337
1795
|
await refetchExams();
|
|
1338
1796
|
if (Number.isFinite(createdExamId) && createdExamId > 0) {
|
|
1339
|
-
|
|
1797
|
+
const targetUid = activeModuleUidRef.current ?? modules[modules.length - 1]?.uid;
|
|
1798
|
+
if (targetUid) {
|
|
1799
|
+
addItemToModule(targetUid, 'exam', createdExamId);
|
|
1800
|
+
} else {
|
|
1801
|
+
addTrailItem('exam', createdExamId);
|
|
1802
|
+
}
|
|
1340
1803
|
setSelectedExamToAdd('');
|
|
1341
1804
|
}
|
|
1342
1805
|
|
|
@@ -1500,7 +1963,7 @@ export default function TrainingPage() {
|
|
|
1500
1963
|
],
|
|
1501
1964
|
},
|
|
1502
1965
|
]}
|
|
1503
|
-
|
|
1966
|
+
actions={
|
|
1504
1967
|
<ViewModeToggle
|
|
1505
1968
|
viewMode={viewMode}
|
|
1506
1969
|
onViewModeChange={setViewMode}
|
|
@@ -1848,7 +2311,7 @@ export default function TrainingPage() {
|
|
|
1848
2311
|
setPageSize(nextPageSize);
|
|
1849
2312
|
setCurrentPage(1);
|
|
1850
2313
|
}}
|
|
1851
|
-
pageSizeOptions={
|
|
2314
|
+
pageSizeOptions={pageSizeOptions}
|
|
1852
2315
|
/>
|
|
1853
2316
|
</div>
|
|
1854
2317
|
)}
|
|
@@ -1896,6 +2359,60 @@ export default function TrainingPage() {
|
|
|
1896
2359
|
{...form.register('descricao')}
|
|
1897
2360
|
/>
|
|
1898
2361
|
</Field>
|
|
2362
|
+
{/* Banner da trilha */}
|
|
2363
|
+
<Field>
|
|
2364
|
+
<FieldLabel>Banner da trilha</FieldLabel>
|
|
2365
|
+
<div className="flex items-center gap-3">
|
|
2366
|
+
{bannerPreviewUrl ? (
|
|
2367
|
+
<img
|
|
2368
|
+
src={bannerPreviewUrl}
|
|
2369
|
+
alt="Banner da trilha"
|
|
2370
|
+
className="h-12 w-32 shrink-0 rounded-lg border object-cover"
|
|
2371
|
+
/>
|
|
2372
|
+
) : (
|
|
2373
|
+
<div className="flex h-12 w-32 shrink-0 items-center justify-center rounded-lg border border-dashed bg-muted/40">
|
|
2374
|
+
<ImageIcon className="size-5 text-muted-foreground/50" />
|
|
2375
|
+
</div>
|
|
2376
|
+
)}
|
|
2377
|
+
<div className="flex items-center gap-2">
|
|
2378
|
+
<input
|
|
2379
|
+
ref={bannerInputRef}
|
|
2380
|
+
type="file"
|
|
2381
|
+
accept="image/*"
|
|
2382
|
+
className="hidden"
|
|
2383
|
+
onChange={handleBannerUpload}
|
|
2384
|
+
/>
|
|
2385
|
+
<Button
|
|
2386
|
+
type="button"
|
|
2387
|
+
variant="outline"
|
|
2388
|
+
size="sm"
|
|
2389
|
+
disabled={bannerUploading}
|
|
2390
|
+
onClick={() => bannerInputRef.current?.click()}
|
|
2391
|
+
className="gap-2"
|
|
2392
|
+
>
|
|
2393
|
+
{bannerUploading ? (
|
|
2394
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
2395
|
+
) : (
|
|
2396
|
+
<Upload className="size-3.5" />
|
|
2397
|
+
)}
|
|
2398
|
+
{bannerPreviewUrl ? 'Substituir' : 'Fazer upload'}
|
|
2399
|
+
</Button>
|
|
2400
|
+
{bannerPreviewUrl && (
|
|
2401
|
+
<Button
|
|
2402
|
+
type="button"
|
|
2403
|
+
variant="ghost"
|
|
2404
|
+
size="sm"
|
|
2405
|
+
onClick={handleRemoveBanner}
|
|
2406
|
+
className="gap-2 text-destructive hover:text-destructive"
|
|
2407
|
+
>
|
|
2408
|
+
<X className="size-3.5" />
|
|
2409
|
+
Remover
|
|
2410
|
+
</Button>
|
|
2411
|
+
)}
|
|
2412
|
+
</div>
|
|
2413
|
+
</div>
|
|
2414
|
+
</Field>
|
|
2415
|
+
|
|
1899
2416
|
<Field>
|
|
1900
2417
|
<FieldLabel>
|
|
1901
2418
|
{t('form.fields.progressionMode.label')}{' '}
|
|
@@ -2008,6 +2525,39 @@ export default function TrainingPage() {
|
|
|
2008
2525
|
/>
|
|
2009
2526
|
</Field>
|
|
2010
2527
|
|
|
2528
|
+
<Field>
|
|
2529
|
+
<FieldLabel>Template de certificado</FieldLabel>
|
|
2530
|
+
<Controller
|
|
2531
|
+
name="certificateTemplateId"
|
|
2532
|
+
control={form.control}
|
|
2533
|
+
render={({ field }) => (
|
|
2534
|
+
<Select
|
|
2535
|
+
value={field.value != null ? String(field.value) : '__none__'}
|
|
2536
|
+
onValueChange={(v) =>
|
|
2537
|
+
field.onChange(v === '__none__' ? null : Number(v))
|
|
2538
|
+
}
|
|
2539
|
+
>
|
|
2540
|
+
<SelectTrigger>
|
|
2541
|
+
<SelectValue placeholder="Selecione um template..." />
|
|
2542
|
+
</SelectTrigger>
|
|
2543
|
+
<SelectContent>
|
|
2544
|
+
<SelectItem value="__none__">
|
|
2545
|
+
Sem template vinculado
|
|
2546
|
+
</SelectItem>
|
|
2547
|
+
{availableCertificateTemplates.map((tpl) => (
|
|
2548
|
+
<SelectItem key={tpl.id} value={String(tpl.id)}>
|
|
2549
|
+
{tpl.name}
|
|
2550
|
+
</SelectItem>
|
|
2551
|
+
))}
|
|
2552
|
+
</SelectContent>
|
|
2553
|
+
</Select>
|
|
2554
|
+
)}
|
|
2555
|
+
/>
|
|
2556
|
+
<p className="text-xs text-muted-foreground">
|
|
2557
|
+
O template escolhido será exibido como preview do certificado na área do aluno.
|
|
2558
|
+
</p>
|
|
2559
|
+
</Field>
|
|
2560
|
+
|
|
2011
2561
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
2012
2562
|
<Field>
|
|
2013
2563
|
<FieldLabel htmlFor="primaryColor">
|
|
@@ -2068,120 +2618,63 @@ export default function TrainingPage() {
|
|
|
2068
2618
|
</Field>
|
|
2069
2619
|
</div>
|
|
2070
2620
|
|
|
2071
|
-
{/*
|
|
2621
|
+
{/* Módulos da trilha */}
|
|
2072
2622
|
<Field>
|
|
2073
|
-
<FieldLabel>
|
|
2623
|
+
<FieldLabel>Módulos da trilha</FieldLabel>
|
|
2074
2624
|
<div className="space-y-2">
|
|
2075
|
-
<
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
disabled={selectableExams.length === 0}
|
|
2129
|
-
>
|
|
2130
|
-
<SelectTrigger className="w-full">
|
|
2131
|
-
<SelectValue
|
|
2132
|
-
placeholder={t('form.fields.exames.placeholder')}
|
|
2133
|
-
/>
|
|
2134
|
-
</SelectTrigger>
|
|
2135
|
-
<SelectContent>
|
|
2136
|
-
{selectableExams.map((exam) => (
|
|
2137
|
-
<SelectItem key={exam.id} value={String(exam.id)}>
|
|
2138
|
-
{exam.titulo} ({exam.limiteTempo}min)
|
|
2139
|
-
</SelectItem>
|
|
2140
|
-
))}
|
|
2141
|
-
</SelectContent>
|
|
2142
|
-
</Select>
|
|
2143
|
-
<Button
|
|
2144
|
-
type="button"
|
|
2145
|
-
variant="outline"
|
|
2146
|
-
className="w-full shrink-0 whitespace-nowrap sm:w-auto"
|
|
2147
|
-
onClick={openCreateExamSheet}
|
|
2148
|
-
>
|
|
2149
|
-
<Plus className="size-4" />
|
|
2150
|
-
</Button>
|
|
2151
|
-
</div>
|
|
2152
|
-
</div>
|
|
2153
|
-
</div>
|
|
2154
|
-
|
|
2155
|
-
{trailItems.length > 0 ? (
|
|
2156
|
-
<>
|
|
2157
|
-
<div className="rounded-md border bg-background">
|
|
2158
|
-
<DndContext
|
|
2159
|
-
sensors={sensors}
|
|
2160
|
-
collisionDetection={closestCenter}
|
|
2161
|
-
onDragEnd={handleTrailDragEnd}
|
|
2162
|
-
>
|
|
2163
|
-
<SortableContext
|
|
2164
|
-
items={trailItems.map((item) => item.uid)}
|
|
2165
|
-
strategy={verticalListSortingStrategy}
|
|
2166
|
-
>
|
|
2167
|
-
{trailItems.map((item) => (
|
|
2168
|
-
<SortableTrailItem
|
|
2169
|
-
key={item.uid}
|
|
2170
|
-
item={item}
|
|
2171
|
-
onRemove={removeTrailItem}
|
|
2172
|
-
/>
|
|
2173
|
-
))}
|
|
2174
|
-
</SortableContext>
|
|
2175
|
-
</DndContext>
|
|
2176
|
-
</div>
|
|
2177
|
-
</>
|
|
2178
|
-
) : (
|
|
2179
|
-
<div className="rounded-md border p-3">
|
|
2180
|
-
<p className="text-sm text-muted-foreground">
|
|
2181
|
-
{t('form.fields.trilha.empty')}
|
|
2182
|
-
</p>
|
|
2183
|
-
</div>
|
|
2184
|
-
)}
|
|
2625
|
+
<DndContext
|
|
2626
|
+
sensors={sensors}
|
|
2627
|
+
collisionDetection={closestCenter}
|
|
2628
|
+
onDragEnd={handleModuleDragEnd}
|
|
2629
|
+
>
|
|
2630
|
+
<SortableContext
|
|
2631
|
+
items={modules.map((m) => m.uid)}
|
|
2632
|
+
strategy={verticalListSortingStrategy}
|
|
2633
|
+
>
|
|
2634
|
+
{modules.map((mod) => {
|
|
2635
|
+
const usedCourseIds = mod.items
|
|
2636
|
+
.filter((i) => i.type === 'course')
|
|
2637
|
+
.map((i) => i.itemId);
|
|
2638
|
+
const usedExamIds = mod.items
|
|
2639
|
+
.filter((i) => i.type === 'exam')
|
|
2640
|
+
.map((i) => i.itemId);
|
|
2641
|
+
return (
|
|
2642
|
+
<SortableModule
|
|
2643
|
+
key={mod.uid}
|
|
2644
|
+
module={mod}
|
|
2645
|
+
availableCursos={selectableCursos}
|
|
2646
|
+
availableExams={selectableExams}
|
|
2647
|
+
availableCursosForModule={selectableCursos.filter(
|
|
2648
|
+
(c) => !usedCourseIds.includes(c.id)
|
|
2649
|
+
)}
|
|
2650
|
+
availableExamsForModule={selectableExams.filter(
|
|
2651
|
+
(e) => !usedExamIds.includes(e.id)
|
|
2652
|
+
)}
|
|
2653
|
+
sensors={sensors}
|
|
2654
|
+
onUpdateTitle={updateModuleTitle}
|
|
2655
|
+
onUpdateDescription={updateModuleDescription}
|
|
2656
|
+
onRemoveModule={removeModule}
|
|
2657
|
+
onAddItem={addItemToModule}
|
|
2658
|
+
onRemoveItem={removeItemFromModule}
|
|
2659
|
+
onDragEndItems={handleModuleItemDragEnd}
|
|
2660
|
+
onOpenCreateCourseSheet={openCreateCourseSheet}
|
|
2661
|
+
onOpenCreateExamSheet={openCreateExamSheet}
|
|
2662
|
+
/>
|
|
2663
|
+
);
|
|
2664
|
+
})}
|
|
2665
|
+
</SortableContext>
|
|
2666
|
+
</DndContext>
|
|
2667
|
+
|
|
2668
|
+
<Button
|
|
2669
|
+
type="button"
|
|
2670
|
+
variant="outline"
|
|
2671
|
+
size="sm"
|
|
2672
|
+
className="mt-1 w-full gap-2"
|
|
2673
|
+
onClick={addModule}
|
|
2674
|
+
>
|
|
2675
|
+
<Plus className="size-4" />
|
|
2676
|
+
Adicionar módulo
|
|
2677
|
+
</Button>
|
|
2185
2678
|
</div>
|
|
2186
2679
|
|
|
2187
2680
|
{(isFetchingCourses || isFetchingExams) && (
|
|
@@ -2189,17 +2682,6 @@ export default function TrainingPage() {
|
|
|
2189
2682
|
{t('form.fields.trilha.loading')}
|
|
2190
2683
|
</p>
|
|
2191
2684
|
)}
|
|
2192
|
-
|
|
2193
|
-
{learningPathItems.length > 0 && (
|
|
2194
|
-
<p className="text-xs text-muted-foreground mt-1">
|
|
2195
|
-
{learningPathItems.length} {t('coursesSummary.items')}{' '}
|
|
2196
|
-
{t('coursesSummary.dot')}{' '}
|
|
2197
|
-
{availableCursos
|
|
2198
|
-
.filter((c) => selectedCursos.includes(c.id))
|
|
2199
|
-
.reduce((a, c) => a + c.cargaHoraria, 0)}
|
|
2200
|
-
{t('coursesSummary.hours')}
|
|
2201
|
-
</p>
|
|
2202
|
-
)}
|
|
2203
2685
|
</Field>
|
|
2204
2686
|
|
|
2205
2687
|
<SheetFooter className="mt-auto shrink-0 gap-2 pt-4 px-0">
|