@hed-hog/lms 0.0.366 → 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/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 +26 -5
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +31 -1
- 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.js +7 -7
- package/dist/course/course-video-agent-pipeline.service.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +4 -0
- package/dist/course/course.module.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/lms-bulk-upload-automation.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload-automation.service.js +26 -13
- 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-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 +4 -0
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/platforma-performance.service.js +121 -121
- 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 +99 -1
- package/dist/platforma/platforma.controller.d.ts.map +1 -1
- package/dist/platforma/platforma.controller.js +111 -2
- package/dist/platforma/platforma.controller.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/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/detail-course.tsx.ejs +69 -1
- 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/_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-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 +26 -4
- package/hedhog/frontend/app/paths/page.tsx.ejs +612 -164
- package/hedhog/frontend/messages/en.json +23 -12
- package/hedhog/frontend/messages/pt.json +23 -12
- package/hedhog/query/triggers.sql +33 -0
- package/hedhog/table/course_ai_usage.yaml +46 -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 -66
- package/package.json +9 -9
- 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 +16 -0
- 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 -471
- package/src/course/course.module.ts +4 -0
- 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 -65
- package/src/course/lms-bulk-upload-automation.service.ts +29 -7
- 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 +4 -0
- package/src/platforma/dto/heartbeat.dto.ts +30 -30
- package/src/platforma/handlers/emit-certificate.handler.ts +117 -117
- package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -343
- package/src/platforma/platforma-heartbeat.service.ts +33 -33
- package/src/platforma/platforma-performance.service.ts +606 -606
- package/src/platforma/platforma-search.service.ts +48 -48
- package/src/platforma/platforma-video.service.ts +59 -3
- package/src/platforma/platforma.controller.ts +88 -0
- 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>;
|
|
@@ -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('');
|
|
@@ -746,6 +998,23 @@ export default function TrainingPage() {
|
|
|
746
998
|
},
|
|
747
999
|
});
|
|
748
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
|
+
|
|
749
1018
|
useEffect(() => {
|
|
750
1019
|
if (courseSheetOpen) {
|
|
751
1020
|
void refetchCategories();
|
|
@@ -989,6 +1258,9 @@ export default function TrainingPage() {
|
|
|
989
1258
|
setEditingFormacao(null);
|
|
990
1259
|
initialLearningPathRef.current = [];
|
|
991
1260
|
setLearningPathItems([]);
|
|
1261
|
+
setModules([{ uid: `module-new-${Date.now()}`, title: '', description: '', items: [] }]);
|
|
1262
|
+
activeModuleUidRef.current = null;
|
|
1263
|
+
setBannerPreviewUrl(null);
|
|
992
1264
|
form.reset({
|
|
993
1265
|
nome: '',
|
|
994
1266
|
descricao: '',
|
|
@@ -999,6 +1271,7 @@ export default function TrainingPage() {
|
|
|
999
1271
|
status: 'rascunho',
|
|
1000
1272
|
primaryColor: '#1D4ED8',
|
|
1001
1273
|
secondaryColor: '#111827',
|
|
1274
|
+
certificateTemplateId: null,
|
|
1002
1275
|
});
|
|
1003
1276
|
examForm.reset();
|
|
1004
1277
|
setSelectedCourseToAdd('');
|
|
@@ -1045,6 +1318,38 @@ export default function TrainingPage() {
|
|
|
1045
1318
|
setLearningPathItems(normalizedItems);
|
|
1046
1319
|
setSelectedCourseToAdd('');
|
|
1047
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
|
+
|
|
1048
1353
|
form.reset({
|
|
1049
1354
|
nome: fullFormacao.nome,
|
|
1050
1355
|
descricao: fullFormacao.descricao,
|
|
@@ -1057,6 +1362,8 @@ export default function TrainingPage() {
|
|
|
1057
1362
|
status: normalizeStatusValue(fullFormacao.status),
|
|
1058
1363
|
primaryColor: fullFormacao.primaryColor ?? '#1D4ED8',
|
|
1059
1364
|
secondaryColor: fullFormacao.secondaryColor ?? '#111827',
|
|
1365
|
+
bannerFileId: fullFormacao.bannerFileId ?? null,
|
|
1366
|
+
certificateTemplateId: fullFormacao.certificateTemplate?.id ?? null,
|
|
1060
1367
|
});
|
|
1061
1368
|
setSheetOpen(true);
|
|
1062
1369
|
} finally {
|
|
@@ -1138,12 +1445,96 @@ export default function TrainingPage() {
|
|
|
1138
1445
|
});
|
|
1139
1446
|
}
|
|
1140
1447
|
|
|
1141
|
-
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;
|
|
1142
1532
|
courseForm.reset(DEFAULT_COURSE_FORM_VALUES);
|
|
1143
1533
|
setCourseSheetOpen(true);
|
|
1144
1534
|
}
|
|
1145
1535
|
|
|
1146
|
-
function openCreateExamSheet() {
|
|
1536
|
+
function openCreateExamSheet(moduleUid?: string) {
|
|
1537
|
+
if (moduleUid) activeModuleUidRef.current = moduleUid;
|
|
1147
1538
|
examForm.reset({
|
|
1148
1539
|
titulo: '',
|
|
1149
1540
|
notaMinima: 7,
|
|
@@ -1199,28 +1590,71 @@ export default function TrainingPage() {
|
|
|
1199
1590
|
});
|
|
1200
1591
|
}
|
|
1201
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
|
+
|
|
1202
1628
|
async function onSubmit(data: FormacaoForm) {
|
|
1203
1629
|
const toApiProgressMode = (value: FormacaoForm['progressionMode']) =>
|
|
1204
1630
|
value === 'livre' ? 'free' : 'sequential';
|
|
1205
1631
|
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
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
|
+
}
|
|
1210
1640
|
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
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
|
+
}));
|
|
1215
1652
|
|
|
1653
|
+
try {
|
|
1216
1654
|
setSaving(true);
|
|
1217
1655
|
|
|
1218
1656
|
if (editingFormacao) {
|
|
1219
1657
|
const dirty = form.formState.dirtyFields;
|
|
1220
|
-
const itemsChanged = !pathsAreEqual(
|
|
1221
|
-
initialLearningPathRef.current,
|
|
1222
|
-
orderedItems
|
|
1223
|
-
);
|
|
1224
1658
|
|
|
1225
1659
|
const payload: {
|
|
1226
1660
|
title?: string;
|
|
@@ -1231,12 +1665,9 @@ export default function TrainingPage() {
|
|
|
1231
1665
|
progressMode?: 'sequential' | 'free';
|
|
1232
1666
|
primaryColor?: string;
|
|
1233
1667
|
secondaryColor?: string;
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
order: number;
|
|
1238
|
-
isRequired: boolean;
|
|
1239
|
-
}>;
|
|
1668
|
+
bannerFileId?: number | null;
|
|
1669
|
+
certificateTemplateId?: number | null;
|
|
1670
|
+
modules?: typeof modulesPayload;
|
|
1240
1671
|
} = {};
|
|
1241
1672
|
|
|
1242
1673
|
if (dirty.nome) payload.title = data.nome;
|
|
@@ -1251,19 +1682,9 @@ export default function TrainingPage() {
|
|
|
1251
1682
|
payload.progressMode = toApiProgressMode(data.progressionMode);
|
|
1252
1683
|
if (dirty.primaryColor) payload.primaryColor = data.primaryColor;
|
|
1253
1684
|
if (dirty.secondaryColor) payload.secondaryColor = data.secondaryColor;
|
|
1254
|
-
if (
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
itemId: item.itemId,
|
|
1258
|
-
order: index,
|
|
1259
|
-
isRequired: item.isRequired !== false,
|
|
1260
|
-
}));
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
if (Object.keys(payload).length === 0) {
|
|
1264
|
-
setSheetOpen(false);
|
|
1265
|
-
return;
|
|
1266
|
-
}
|
|
1685
|
+
if (dirty.bannerFileId) payload.bannerFileId = data.bannerFileId ?? null;
|
|
1686
|
+
if (dirty.certificateTemplateId) payload.certificateTemplateId = data.certificateTemplateId ?? null;
|
|
1687
|
+
payload.modules = modulesPayload;
|
|
1267
1688
|
|
|
1268
1689
|
await request({
|
|
1269
1690
|
url: `/lms/paths/${editingFormacao.id}`,
|
|
@@ -1281,12 +1702,9 @@ export default function TrainingPage() {
|
|
|
1281
1702
|
progressMode: toApiProgressMode(data.progressionMode),
|
|
1282
1703
|
primaryColor: data.primaryColor,
|
|
1283
1704
|
secondaryColor: data.secondaryColor,
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
order: index,
|
|
1288
|
-
isRequired: item.isRequired !== false,
|
|
1289
|
-
})),
|
|
1705
|
+
bannerFileId: data.bannerFileId ?? null,
|
|
1706
|
+
certificateTemplateId: data.certificateTemplateId ?? null,
|
|
1707
|
+
modules: modulesPayload,
|
|
1290
1708
|
};
|
|
1291
1709
|
|
|
1292
1710
|
await request({
|
|
@@ -1300,6 +1718,7 @@ export default function TrainingPage() {
|
|
|
1300
1718
|
await Promise.all([refetchTraining(), refetchStats()]);
|
|
1301
1719
|
setSheetOpen(false);
|
|
1302
1720
|
setLearningPathItems([]);
|
|
1721
|
+
setModules([]);
|
|
1303
1722
|
initialLearningPathRef.current = [];
|
|
1304
1723
|
} finally {
|
|
1305
1724
|
setSaving(false);
|
|
@@ -1339,7 +1758,12 @@ export default function TrainingPage() {
|
|
|
1339
1758
|
|
|
1340
1759
|
await refetchCourses();
|
|
1341
1760
|
if (Number.isFinite(createdCourseId) && createdCourseId > 0) {
|
|
1342
|
-
|
|
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
|
+
}
|
|
1343
1767
|
setSelectedCourseToAdd('');
|
|
1344
1768
|
}
|
|
1345
1769
|
|
|
@@ -1370,7 +1794,12 @@ export default function TrainingPage() {
|
|
|
1370
1794
|
|
|
1371
1795
|
await refetchExams();
|
|
1372
1796
|
if (Number.isFinite(createdExamId) && createdExamId > 0) {
|
|
1373
|
-
|
|
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
|
+
}
|
|
1374
1803
|
setSelectedExamToAdd('');
|
|
1375
1804
|
}
|
|
1376
1805
|
|
|
@@ -1930,6 +2359,60 @@ export default function TrainingPage() {
|
|
|
1930
2359
|
{...form.register('descricao')}
|
|
1931
2360
|
/>
|
|
1932
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
|
+
|
|
1933
2416
|
<Field>
|
|
1934
2417
|
<FieldLabel>
|
|
1935
2418
|
{t('form.fields.progressionMode.label')}{' '}
|
|
@@ -2042,6 +2525,39 @@ export default function TrainingPage() {
|
|
|
2042
2525
|
/>
|
|
2043
2526
|
</Field>
|
|
2044
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
|
+
|
|
2045
2561
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
2046
2562
|
<Field>
|
|
2047
2563
|
<FieldLabel htmlFor="primaryColor">
|
|
@@ -2102,120 +2618,63 @@ export default function TrainingPage() {
|
|
|
2102
2618
|
</Field>
|
|
2103
2619
|
</div>
|
|
2104
2620
|
|
|
2105
|
-
{/*
|
|
2621
|
+
{/* Módulos da trilha */}
|
|
2106
2622
|
<Field>
|
|
2107
|
-
<FieldLabel>
|
|
2623
|
+
<FieldLabel>Módulos da trilha</FieldLabel>
|
|
2108
2624
|
<div className="space-y-2">
|
|
2109
|
-
<
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
disabled={selectableExams.length === 0}
|
|
2163
|
-
>
|
|
2164
|
-
<SelectTrigger className="w-full">
|
|
2165
|
-
<SelectValue
|
|
2166
|
-
placeholder={t('form.fields.exames.placeholder')}
|
|
2167
|
-
/>
|
|
2168
|
-
</SelectTrigger>
|
|
2169
|
-
<SelectContent>
|
|
2170
|
-
{selectableExams.map((exam) => (
|
|
2171
|
-
<SelectItem key={exam.id} value={String(exam.id)}>
|
|
2172
|
-
{exam.titulo} ({exam.limiteTempo}min)
|
|
2173
|
-
</SelectItem>
|
|
2174
|
-
))}
|
|
2175
|
-
</SelectContent>
|
|
2176
|
-
</Select>
|
|
2177
|
-
<Button
|
|
2178
|
-
type="button"
|
|
2179
|
-
variant="outline"
|
|
2180
|
-
className="w-full shrink-0 whitespace-nowrap sm:w-auto"
|
|
2181
|
-
onClick={openCreateExamSheet}
|
|
2182
|
-
>
|
|
2183
|
-
<Plus className="size-4" />
|
|
2184
|
-
</Button>
|
|
2185
|
-
</div>
|
|
2186
|
-
</div>
|
|
2187
|
-
</div>
|
|
2188
|
-
|
|
2189
|
-
{trailItems.length > 0 ? (
|
|
2190
|
-
<>
|
|
2191
|
-
<div className="rounded-md border bg-background">
|
|
2192
|
-
<DndContext
|
|
2193
|
-
sensors={sensors}
|
|
2194
|
-
collisionDetection={closestCenter}
|
|
2195
|
-
onDragEnd={handleTrailDragEnd}
|
|
2196
|
-
>
|
|
2197
|
-
<SortableContext
|
|
2198
|
-
items={trailItems.map((item) => item.uid)}
|
|
2199
|
-
strategy={verticalListSortingStrategy}
|
|
2200
|
-
>
|
|
2201
|
-
{trailItems.map((item) => (
|
|
2202
|
-
<SortableTrailItem
|
|
2203
|
-
key={item.uid}
|
|
2204
|
-
item={item}
|
|
2205
|
-
onRemove={removeTrailItem}
|
|
2206
|
-
/>
|
|
2207
|
-
))}
|
|
2208
|
-
</SortableContext>
|
|
2209
|
-
</DndContext>
|
|
2210
|
-
</div>
|
|
2211
|
-
</>
|
|
2212
|
-
) : (
|
|
2213
|
-
<div className="rounded-md border p-3">
|
|
2214
|
-
<p className="text-sm text-muted-foreground">
|
|
2215
|
-
{t('form.fields.trilha.empty')}
|
|
2216
|
-
</p>
|
|
2217
|
-
</div>
|
|
2218
|
-
)}
|
|
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>
|
|
2219
2678
|
</div>
|
|
2220
2679
|
|
|
2221
2680
|
{(isFetchingCourses || isFetchingExams) && (
|
|
@@ -2223,17 +2682,6 @@ export default function TrainingPage() {
|
|
|
2223
2682
|
{t('form.fields.trilha.loading')}
|
|
2224
2683
|
</p>
|
|
2225
2684
|
)}
|
|
2226
|
-
|
|
2227
|
-
{learningPathItems.length > 0 && (
|
|
2228
|
-
<p className="text-xs text-muted-foreground mt-1">
|
|
2229
|
-
{learningPathItems.length} {t('coursesSummary.items')}{' '}
|
|
2230
|
-
{t('coursesSummary.dot')}{' '}
|
|
2231
|
-
{availableCursos
|
|
2232
|
-
.filter((c) => selectedCursos.includes(c.id))
|
|
2233
|
-
.reduce((a, c) => a + c.cargaHoraria, 0)}
|
|
2234
|
-
{t('coursesSummary.hours')}
|
|
2235
|
-
</p>
|
|
2236
|
-
)}
|
|
2237
2685
|
</Field>
|
|
2238
2686
|
|
|
2239
2687
|
<SheetFooter className="mt-auto shrink-0 gap-2 pt-4 px-0">
|