@hed-hog/lms 0.0.355 → 0.0.358
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
- package/dist/course/course-audio-transcription.service.js +15 -7
- package/dist/course/course-audio-transcription.service.js.map +1 -1
- package/dist/course/course-operations-integration.service.d.ts +31 -0
- package/dist/course/course-operations-integration.service.d.ts.map +1 -1
- package/dist/course/course-operations-integration.service.js +286 -22
- package/dist/course/course-operations-integration.service.js.map +1 -1
- package/dist/course/course-operations.controller.d.ts +10 -0
- package/dist/course/course-operations.controller.d.ts.map +1 -0
- package/dist/course/course-operations.controller.js +67 -0
- package/dist/course/course-operations.controller.js.map +1 -0
- package/dist/course/course-structure.controller.d.ts +15 -1
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.service.d.ts +25 -1
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +160 -24
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +4 -1
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +4 -2
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +61 -2
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts +3 -0
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js +15 -0
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/dto/create-course-structure-session.dto.d.ts +1 -0
- package/dist/course/dto/create-course-structure-session.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-session.dto.js +5 -0
- package/dist/course/dto/create-course-structure-session.dto.js.map +1 -1
- package/dist/course/dto/update-course-operations-config.dto.d.ts +6 -0
- package/dist/course/dto/update-course-operations-config.dto.d.ts.map +1 -0
- package/dist/course/dto/update-course-operations-config.dto.js +33 -0
- package/dist/course/dto/update-course-operations-config.dto.js.map +1 -0
- package/dist/course/lms-operations-task.subscriber.d.ts +13 -0
- package/dist/course/lms-operations-task.subscriber.d.ts.map +1 -0
- package/dist/course/lms-operations-task.subscriber.js +57 -0
- package/dist/course/lms-operations-task.subscriber.js.map +1 -0
- package/dist/enterprise/enterprise.service.js +1 -1
- package/dist/enterprise/enterprise.service.js.map +1 -1
- package/dist/enterprise/training/training-student.controller.d.ts +0 -95
- package/dist/enterprise/training/training-student.controller.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.controller.js +1 -34
- package/dist/enterprise/training/training-student.controller.js.map +1 -1
- package/dist/enterprise/training/training-student.service.d.ts +63 -0
- package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.js +320 -4
- package/dist/enterprise/training/training-student.service.js.map +1 -1
- package/dist/instructor/instructor.service.d.ts.map +1 -1
- package/dist/instructor/instructor.service.js +12 -3
- package/dist/instructor/instructor.service.js.map +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +2 -0
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/platforma.controller.d.ts +287 -0
- package/dist/platforma/platforma.controller.d.ts.map +1 -0
- package/dist/platforma/platforma.controller.js +147 -0
- package/dist/platforma/platforma.controller.js.map +1 -0
- package/hedhog/data/route.yaml +102 -9
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -2
- package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +10 -13
- package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +26 -4
- package/hedhog/frontend/app/_lib/editor/types.ts.ejs +6 -0
- package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +447 -31
- package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +59 -47
- package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +201 -35
- package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +36 -5
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +2 -2
- package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +17 -6
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +382 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +45 -8
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +177 -67
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +25 -60
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +11 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +22 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +79 -64
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +31 -14
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +4 -4
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +39 -27
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +24 -2
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +41 -6
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +2 -2
- package/hedhog/frontend/app/courses/page.tsx.ejs +80 -103
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +1 -1
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +2 -2
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +18 -6
- package/hedhog/frontend/app/exams/[id]/page.tsx.ejs +5 -4
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +16 -10
- package/hedhog/frontend/app/paths/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/training/page.tsx.ejs +1 -1
- package/hedhog/frontend/messages/en.json +7 -2
- package/hedhog/frontend/messages/pt.json +7 -2
- package/hedhog/table/course_lesson.yaml +2 -2
- package/hedhog/table/course_module.yaml +3 -0
- package/package.json +8 -8
- package/src/course/course-audio-transcription.service.ts +21 -8
- package/src/course/course-operations-integration.service.ts +460 -22
- package/src/course/course-operations.controller.ts +45 -0
- package/src/course/course-structure.service.ts +209 -4
- package/src/course/course.module.ts +4 -1
- package/src/course/course.service.ts +67 -1
- package/src/course/dto/create-course-structure-lesson.dto.ts +17 -0
- package/src/course/dto/create-course-structure-session.dto.ts +13 -1
- package/src/course/dto/update-course-operations-config.dto.ts +16 -0
- package/src/course/lms-operations-task.subscriber.ts +44 -0
- package/src/enterprise/enterprise.service.ts +1 -1
- package/src/enterprise/training/training-student.controller.ts +3 -27
- package/src/enterprise/training/training-student.service.ts +350 -2
- package/src/instructor/instructor.service.ts +12 -3
- package/src/lms.module.ts +2 -0
- package/src/platforma/platforma.controller.ts +92 -0
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
Download,
|
|
14
14
|
ExternalLink,
|
|
15
15
|
Eye,
|
|
16
|
-
EyeOff,
|
|
17
16
|
FileText,
|
|
18
17
|
GripVertical,
|
|
19
18
|
HelpCircle,
|
|
@@ -39,6 +38,7 @@ import { useForm, useWatch } from 'react-hook-form';
|
|
|
39
38
|
import { toast } from 'sonner';
|
|
40
39
|
import { z } from 'zod';
|
|
41
40
|
|
|
41
|
+
import { CopyButton } from '@/components/copy-button';
|
|
42
42
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
43
43
|
import { Badge } from '@/components/ui/badge';
|
|
44
44
|
import { Button } from '@/components/ui/button';
|
|
@@ -140,6 +140,8 @@ import type {
|
|
|
140
140
|
VideoProvider,
|
|
141
141
|
} from './types';
|
|
142
142
|
|
|
143
|
+
const EMPTY_VIDEO_FRAMES: VideoFrame[] = [];
|
|
144
|
+
|
|
143
145
|
function formatFileSize(bytes: number): string {
|
|
144
146
|
if (bytes < 1024) return `${bytes} B`;
|
|
145
147
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
@@ -158,6 +160,28 @@ function formatDateTimeLabel(value?: string | null): string | null {
|
|
|
158
160
|
}).format(parsed);
|
|
159
161
|
}
|
|
160
162
|
|
|
163
|
+
function getInitials(name?: string | null): string {
|
|
164
|
+
const normalizedName = String(name ?? '').trim();
|
|
165
|
+
if (!normalizedName) return '--';
|
|
166
|
+
|
|
167
|
+
return normalizedName
|
|
168
|
+
.split(' ')
|
|
169
|
+
.filter(Boolean)
|
|
170
|
+
.slice(0, 2)
|
|
171
|
+
.map((part) => part[0])
|
|
172
|
+
.join('')
|
|
173
|
+
.toUpperCase();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function getInstructorAvatarUrl(avatarId?: number | null): string | undefined {
|
|
177
|
+
if (typeof avatarId !== 'number' || avatarId <= 0) return undefined;
|
|
178
|
+
|
|
179
|
+
const baseUrl = process.env.NEXT_PUBLIC_API_BASE_URL?.replace(/\/$/, '');
|
|
180
|
+
return baseUrl
|
|
181
|
+
? `${baseUrl}/person/avatar/${avatarId}`
|
|
182
|
+
: `/person/avatar/${avatarId}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
161
185
|
function videoProfileResourceType(profileId: number): string {
|
|
162
186
|
return `video_profile:${profileId}`;
|
|
163
187
|
}
|
|
@@ -526,7 +550,7 @@ type FormValues = {
|
|
|
526
550
|
type: LessonType;
|
|
527
551
|
duration: number;
|
|
528
552
|
status: LessonStatus;
|
|
529
|
-
|
|
553
|
+
published: boolean;
|
|
530
554
|
publicDescription: string;
|
|
531
555
|
privateDescription: string;
|
|
532
556
|
videoProvider?: VideoProvider;
|
|
@@ -764,13 +788,14 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
764
788
|
const lesson = useStructureStore((s) =>
|
|
765
789
|
s.lessons.find((l) => l.id === lessonId)
|
|
766
790
|
);
|
|
791
|
+
const sessions = useStructureStore((s) => s.sessions);
|
|
767
792
|
const persistedVideoProvider: VideoProvider | undefined =
|
|
768
793
|
lesson?.videoProvider === 'youtube' || lesson?.videoProvider === 'vimeo'
|
|
769
794
|
? lesson.videoProvider
|
|
770
795
|
: lesson?.videoProvider
|
|
771
796
|
? 'file_storage'
|
|
772
797
|
: undefined;
|
|
773
|
-
const videoFrames = lesson?.frames ??
|
|
798
|
+
const videoFrames = lesson?.frames ?? EMPTY_VIDEO_FRAMES;
|
|
774
799
|
const updateLesson = useUpdateLessonMutation();
|
|
775
800
|
const updateTranscriptionSegments = useUpdateTranscriptionSegmentsMutation(
|
|
776
801
|
lesson?.id ?? null
|
|
@@ -792,7 +817,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
792
817
|
[
|
|
793
818
|
{ value: 'youtube' as VideoProvider, label: 'YouTube' },
|
|
794
819
|
{ value: 'vimeo' as VideoProvider, label: 'Vimeo' },
|
|
795
|
-
{
|
|
820
|
+
{
|
|
821
|
+
value: 'file_storage' as VideoProvider,
|
|
822
|
+
label: t('providers.fileStorage'),
|
|
823
|
+
},
|
|
796
824
|
] as { value: VideoProvider; label: string }[]
|
|
797
825
|
).filter((p) => {
|
|
798
826
|
if (p.value === 'youtube') return lmsSettings.youtubeEnabled;
|
|
@@ -819,7 +847,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
819
847
|
'finalizada',
|
|
820
848
|
'publicada',
|
|
821
849
|
] as const),
|
|
822
|
-
|
|
850
|
+
published: z.boolean(),
|
|
823
851
|
publicDescription: z.string(),
|
|
824
852
|
privateDescription: z.string(),
|
|
825
853
|
videoProvider: z
|
|
@@ -842,7 +870,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
842
870
|
type: lesson?.type ?? 'video',
|
|
843
871
|
duration: lesson?.duration ?? 0,
|
|
844
872
|
status: lesson?.status ?? 'preparada',
|
|
845
|
-
|
|
873
|
+
published: lesson?.published ?? false,
|
|
846
874
|
publicDescription: lesson?.publicDescription ?? '',
|
|
847
875
|
privateDescription: lesson?.privateDescription ?? '',
|
|
848
876
|
videoProvider:
|
|
@@ -1145,15 +1173,21 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1145
1173
|
const nextEntries = Object.entries(current).filter(([id]) =>
|
|
1146
1174
|
frameIds.has(id)
|
|
1147
1175
|
);
|
|
1176
|
+
if (nextEntries.length === Object.keys(current).length) return current;
|
|
1148
1177
|
return Object.fromEntries(nextEntries);
|
|
1149
1178
|
});
|
|
1150
1179
|
|
|
1151
1180
|
setFrameImageErrorIds((current) => {
|
|
1181
|
+
let changed = false;
|
|
1152
1182
|
const next = new Set<string>();
|
|
1153
1183
|
for (const id of current) {
|
|
1154
|
-
if (frameIds.has(id))
|
|
1184
|
+
if (frameIds.has(id)) {
|
|
1185
|
+
next.add(id);
|
|
1186
|
+
} else {
|
|
1187
|
+
changed = true;
|
|
1188
|
+
}
|
|
1155
1189
|
}
|
|
1156
|
-
return next;
|
|
1190
|
+
return changed ? next : current;
|
|
1157
1191
|
});
|
|
1158
1192
|
|
|
1159
1193
|
const frameSignatures = new Set(
|
|
@@ -1219,12 +1253,22 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1219
1253
|
if (cancelled) return;
|
|
1220
1254
|
|
|
1221
1255
|
setFrameAssetMetadataById((current) => {
|
|
1256
|
+
let changed = false;
|
|
1222
1257
|
const next = { ...current };
|
|
1223
1258
|
for (const result of results) {
|
|
1224
1259
|
if (result.status !== 'fulfilled') continue;
|
|
1225
|
-
|
|
1260
|
+
const currentMetadata = current[result.value.frameId];
|
|
1261
|
+
const nextMetadata = result.value.metadata;
|
|
1262
|
+
if (
|
|
1263
|
+
currentMetadata?.url === nextMetadata.url &&
|
|
1264
|
+
currentMetadata?.sizeLabel === nextMetadata.sizeLabel
|
|
1265
|
+
) {
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
next[result.value.frameId] = nextMetadata;
|
|
1269
|
+
changed = true;
|
|
1226
1270
|
}
|
|
1227
|
-
return next;
|
|
1271
|
+
return changed ? next : current;
|
|
1228
1272
|
});
|
|
1229
1273
|
});
|
|
1230
1274
|
|
|
@@ -1365,6 +1409,10 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
1365
1409
|
const cfg = TYPE_CONFIG[lesson.type];
|
|
1366
1410
|
const Icon = cfg.icon;
|
|
1367
1411
|
const lessonTypeLabel = t(cfg.labelKey);
|
|
1412
|
+
const session = sessions.find((s) => s.id === lesson.sessionId);
|
|
1413
|
+
const lessonFullCode = session
|
|
1414
|
+
? `${session.code}_${lesson.code}_${lesson.title}`
|
|
1415
|
+
: null;
|
|
1368
1416
|
const originalVideoResource =
|
|
1369
1417
|
localResources.find((res) => res.type === 'video_original') ?? null;
|
|
1370
1418
|
const isDownloadingOriginalVideo = originalVideoResource
|
|
@@ -2392,7 +2440,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2392
2440
|
setTranscriptionSegments(updater);
|
|
2393
2441
|
}
|
|
2394
2442
|
|
|
2395
|
-
function
|
|
2443
|
+
function persistLesson(values: FormValues, shouldConfirmAutoStatus: boolean) {
|
|
2396
2444
|
if (values.type === 'video') {
|
|
2397
2445
|
const segmentsPayload = transcriptionSegments
|
|
2398
2446
|
.map((segment) => ({
|
|
@@ -2428,6 +2476,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2428
2476
|
sessionId: lesson!.sessionId,
|
|
2429
2477
|
formValues: {
|
|
2430
2478
|
...values,
|
|
2479
|
+
confirmarPublicacaoComStatus: shouldConfirmAutoStatus,
|
|
2431
2480
|
videoUrl:
|
|
2432
2481
|
values.type === 'video' && values.videoProvider === 'file_storage'
|
|
2433
2482
|
? ''
|
|
@@ -2439,15 +2488,39 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2439
2488
|
},
|
|
2440
2489
|
},
|
|
2441
2490
|
{
|
|
2442
|
-
onSuccess: () => {
|
|
2491
|
+
onSuccess: (updatedLesson) => {
|
|
2443
2492
|
persistedInstructorIdsRef.current = [...selectedInstructorIds];
|
|
2444
|
-
form.reset({
|
|
2493
|
+
form.reset({
|
|
2494
|
+
...values,
|
|
2495
|
+
status: updatedLesson.statusProducao ?? values.status,
|
|
2496
|
+
published: updatedLesson.published ?? values.published,
|
|
2497
|
+
transcription: updatedLesson.transcricao ?? values.transcription,
|
|
2498
|
+
});
|
|
2445
2499
|
setResourcesDirty(false);
|
|
2446
2500
|
},
|
|
2447
2501
|
}
|
|
2448
2502
|
);
|
|
2449
2503
|
}
|
|
2450
2504
|
|
|
2505
|
+
function onSubmit(values: FormValues) {
|
|
2506
|
+
const shouldConfirmAutoStatus =
|
|
2507
|
+
values.published && values.status !== 'publicada';
|
|
2508
|
+
|
|
2509
|
+
if (shouldConfirmAutoStatus) {
|
|
2510
|
+
showConfirm({
|
|
2511
|
+
title: 'Confirmar publicação da aula?',
|
|
2512
|
+
description:
|
|
2513
|
+
'Esta aula será publicada e o status de produção será alterado automaticamente para Publicada. Deseja continuar?',
|
|
2514
|
+
confirmText: 'Publicar',
|
|
2515
|
+
destructive: false,
|
|
2516
|
+
onConfirm: () => persistLesson(values, true),
|
|
2517
|
+
});
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
persistLesson(values, false);
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2451
2524
|
// ── Question sheet helpers ────────────────────────────────────────────────
|
|
2452
2525
|
|
|
2453
2526
|
function openCreateQuestion() {
|
|
@@ -2581,15 +2654,29 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2581
2654
|
<div className="flex-1 min-w-0">
|
|
2582
2655
|
<div className="flex items-center gap-1.5">
|
|
2583
2656
|
<span className="text-sm font-semibold truncate">
|
|
2584
|
-
{
|
|
2657
|
+
{lesson.title}
|
|
2585
2658
|
</span>
|
|
2586
2659
|
{isDirty && (
|
|
2587
2660
|
<CircleDot className="size-3 text-amber-500 shrink-0" />
|
|
2588
2661
|
)}
|
|
2589
2662
|
</div>
|
|
2590
|
-
<
|
|
2591
|
-
|
|
2592
|
-
|
|
2663
|
+
<div className="flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-1 text-[0.65rem] text-muted-foreground">
|
|
2664
|
+
<span className="shrink-0">
|
|
2665
|
+
{lesson.code} · {lessonTypeLabel}
|
|
2666
|
+
</span>
|
|
2667
|
+
{lessonFullCode && (
|
|
2668
|
+
<>
|
|
2669
|
+
<span className="text-muted-foreground/60">·</span>
|
|
2670
|
+
<span className="max-w-full truncate rounded border border-border/60 bg-background/70 px-1.5 py-0.5 font-mono text-[0.62rem] text-foreground/80">
|
|
2671
|
+
{lessonFullCode}
|
|
2672
|
+
</span>
|
|
2673
|
+
<CopyButton
|
|
2674
|
+
value={lessonFullCode}
|
|
2675
|
+
className="size-5 shrink-0 text-muted-foreground"
|
|
2676
|
+
/>
|
|
2677
|
+
</>
|
|
2678
|
+
)}
|
|
2679
|
+
</div>
|
|
2593
2680
|
</div>
|
|
2594
2681
|
{watchedStatus && (
|
|
2595
2682
|
<span
|
|
@@ -2629,7 +2716,7 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2629
2716
|
onValueChange={(value) => setActiveTab(value as LessonEditorTab)}
|
|
2630
2717
|
className="flex flex-col flex-1 min-h-0 min-w-0"
|
|
2631
2718
|
>
|
|
2632
|
-
<TabsList className="
|
|
2719
|
+
<TabsList className="mt-0 h-auto w-full justify-start shrink-0 rounded-none border-b bg-muted/50 px-2 py-1 overflow-x-auto overflow-y-hidden whitespace-nowrap sm:px-3">
|
|
2633
2720
|
<TabsTrigger
|
|
2634
2721
|
value="dados"
|
|
2635
2722
|
className="h-6 px-2 text-[11px] shrink-0 sm:h-7 sm:px-2.5 sm:text-xs"
|
|
@@ -2848,42 +2935,21 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2848
2935
|
|
|
2849
2936
|
<FormField
|
|
2850
2937
|
control={form.control}
|
|
2851
|
-
name="
|
|
2938
|
+
name="published"
|
|
2852
2939
|
render={({ field }) => (
|
|
2853
2940
|
<FormItem>
|
|
2854
|
-
<FormLabel className="text-xs">
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
<SelectContent>
|
|
2867
|
-
<SelectItem value="publico">
|
|
2868
|
-
<span className="flex items-center gap-1.5">
|
|
2869
|
-
<Eye className="size-3" />{' '}
|
|
2870
|
-
{t('lessonForm.public')}
|
|
2871
|
-
</span>
|
|
2872
|
-
</SelectItem>
|
|
2873
|
-
<SelectItem value="privado">
|
|
2874
|
-
<span className="flex items-center gap-1.5">
|
|
2875
|
-
<EyeOff className="size-3" />{' '}
|
|
2876
|
-
{t('lessonForm.private')}
|
|
2877
|
-
</span>
|
|
2878
|
-
</SelectItem>
|
|
2879
|
-
<SelectItem value="restrito">
|
|
2880
|
-
<span className="flex items-center gap-1.5">
|
|
2881
|
-
<Lock className="size-3" />{' '}
|
|
2882
|
-
{t('questionEditor.restricted')}
|
|
2883
|
-
</span>
|
|
2884
|
-
</SelectItem>
|
|
2885
|
-
</SelectContent>
|
|
2886
|
-
</Select>
|
|
2941
|
+
<FormLabel className="text-xs">Publicada</FormLabel>
|
|
2942
|
+
<FormControl>
|
|
2943
|
+
<div className="flex h-8 items-center justify-between gap-3 rounded-md border px-3">
|
|
2944
|
+
<p className="cursor-default text-xs text-foreground">
|
|
2945
|
+
Publicada
|
|
2946
|
+
</p>
|
|
2947
|
+
<Switch
|
|
2948
|
+
checked={field.value}
|
|
2949
|
+
onCheckedChange={field.onChange}
|
|
2950
|
+
/>
|
|
2951
|
+
</div>
|
|
2952
|
+
</FormControl>
|
|
2887
2953
|
<FormMessage className="text-xs" />
|
|
2888
2954
|
</FormItem>
|
|
2889
2955
|
)}
|
|
@@ -2901,7 +2967,11 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2901
2967
|
</CardHeader>
|
|
2902
2968
|
<CardContent className="px-3 pb-2 flex flex-col gap-2">
|
|
2903
2969
|
{/* Picker para adicionar */}
|
|
2904
|
-
<EntityPicker<{
|
|
2970
|
+
<EntityPicker<{
|
|
2971
|
+
id: number;
|
|
2972
|
+
name: string;
|
|
2973
|
+
avatarId?: number | null;
|
|
2974
|
+
}>
|
|
2905
2975
|
key={instructorPickerResetKey}
|
|
2906
2976
|
value={null}
|
|
2907
2977
|
onChange={(val) => {
|
|
@@ -2923,6 +2993,44 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2923
2993
|
)}
|
|
2924
2994
|
getOptionValue={(o) => o.id}
|
|
2925
2995
|
getOptionLabel={(o) => o.name}
|
|
2996
|
+
renderOption={({ option }) => {
|
|
2997
|
+
const avatarUrl = getInstructorAvatarUrl(
|
|
2998
|
+
option.avatarId
|
|
2999
|
+
);
|
|
3000
|
+
const displayName = option.name ?? String(option.id);
|
|
3001
|
+
|
|
3002
|
+
return (
|
|
3003
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
3004
|
+
<Avatar className="size-6 shrink-0">
|
|
3005
|
+
<AvatarImage src={avatarUrl} alt={displayName} />
|
|
3006
|
+
<AvatarFallback className="text-[0.6rem] font-medium">
|
|
3007
|
+
{getInitials(displayName)}
|
|
3008
|
+
</AvatarFallback>
|
|
3009
|
+
</Avatar>
|
|
3010
|
+
<span className="truncate text-xs">
|
|
3011
|
+
{displayName}
|
|
3012
|
+
</span>
|
|
3013
|
+
</div>
|
|
3014
|
+
);
|
|
3015
|
+
}}
|
|
3016
|
+
renderSelectedValue={({ option, label }) => {
|
|
3017
|
+
const avatarUrl = getInstructorAvatarUrl(
|
|
3018
|
+
option?.avatarId
|
|
3019
|
+
);
|
|
3020
|
+
const displayName = option?.name ?? label;
|
|
3021
|
+
|
|
3022
|
+
return (
|
|
3023
|
+
<span className="flex min-w-0 items-center gap-2">
|
|
3024
|
+
<Avatar className="size-5 shrink-0">
|
|
3025
|
+
<AvatarImage src={avatarUrl} alt={displayName} />
|
|
3026
|
+
<AvatarFallback className="text-[0.6rem] font-medium">
|
|
3027
|
+
{getInitials(displayName)}
|
|
3028
|
+
</AvatarFallback>
|
|
3029
|
+
</Avatar>
|
|
3030
|
+
<span className="truncate">{displayName}</span>
|
|
3031
|
+
</span>
|
|
3032
|
+
);
|
|
3033
|
+
}}
|
|
2926
3034
|
/>
|
|
2927
3035
|
{/* Lista de instrutores selecionados */}
|
|
2928
3036
|
{selectedInstructorIds.length > 0 && (
|
|
@@ -2937,20 +3045,22 @@ export function EditorLesson({ lessonId }: EditorLessonProps) {
|
|
|
2937
3045
|
key={sid}
|
|
2938
3046
|
className="flex items-center gap-2 rounded-md border bg-muted/20 px-2 py-1"
|
|
2939
3047
|
>
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
.
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
3048
|
+
{(() => {
|
|
3049
|
+
const avatarUrl = getInstructorAvatarUrl(
|
|
3050
|
+
inst?.avatarId
|
|
3051
|
+
);
|
|
3052
|
+
return (
|
|
3053
|
+
<Avatar className="size-6 shrink-0">
|
|
3054
|
+
<AvatarImage
|
|
3055
|
+
src={avatarUrl}
|
|
3056
|
+
alt={displayName}
|
|
3057
|
+
/>
|
|
3058
|
+
<AvatarFallback className="text-[0.6rem] font-medium">
|
|
3059
|
+
{getInitials(displayName)}
|
|
3060
|
+
</AvatarFallback>
|
|
3061
|
+
</Avatar>
|
|
3062
|
+
);
|
|
3063
|
+
})()}
|
|
2954
3064
|
<span className="text-xs flex-1 truncate">
|
|
2955
3065
|
{displayName}
|
|
2956
3066
|
</span>
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
EyeOff,
|
|
9
9
|
Layers,
|
|
10
10
|
Loader2,
|
|
11
|
-
Lock,
|
|
12
11
|
Plus,
|
|
13
12
|
Save,
|
|
14
13
|
Trash2,
|
|
@@ -19,7 +18,6 @@ import { useEffect, useMemo } from 'react';
|
|
|
19
18
|
import { useForm } from 'react-hook-form';
|
|
20
19
|
import { z } from 'zod';
|
|
21
20
|
|
|
22
|
-
import { Badge } from '@/components/ui/badge';
|
|
23
21
|
import { Button } from '@/components/ui/button';
|
|
24
22
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
25
23
|
import {
|
|
@@ -32,16 +30,14 @@ import {
|
|
|
32
30
|
} from '@/components/ui/form';
|
|
33
31
|
import { Input } from '@/components/ui/input';
|
|
34
32
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
35
|
-
import {
|
|
36
|
-
Select,
|
|
37
|
-
SelectContent,
|
|
38
|
-
SelectItem,
|
|
39
|
-
SelectTrigger,
|
|
40
|
-
SelectValue,
|
|
41
|
-
} from '@/components/ui/select';
|
|
42
33
|
import { Separator } from '@/components/ui/separator';
|
|
43
34
|
import { Switch } from '@/components/ui/switch';
|
|
44
35
|
import { Textarea } from '@/components/ui/textarea';
|
|
36
|
+
import {
|
|
37
|
+
Tooltip,
|
|
38
|
+
TooltipContent,
|
|
39
|
+
TooltipTrigger,
|
|
40
|
+
} from '@/components/ui/tooltip';
|
|
45
41
|
|
|
46
42
|
import {
|
|
47
43
|
useCreateLessonMutation,
|
|
@@ -50,7 +46,6 @@ import {
|
|
|
50
46
|
} from '../_data/use-course-structure-mutations';
|
|
51
47
|
import { IconActionTooltip } from './icon-action-tooltip';
|
|
52
48
|
import { useStructureStore } from './store';
|
|
53
|
-
import type { Visibility } from './types';
|
|
54
49
|
|
|
55
50
|
// ── Schema ────────────────────────────────────────────────────────────────────
|
|
56
51
|
|
|
@@ -59,7 +54,6 @@ const schema = z.object({
|
|
|
59
54
|
title: z.string().min(1, 'Título obrigatório'),
|
|
60
55
|
description: z.string(),
|
|
61
56
|
duration: z.coerce.number().min(0),
|
|
62
|
-
visibility: z.enum(['publico', 'privado', 'restrito'] as const),
|
|
63
57
|
published: z.boolean(),
|
|
64
58
|
});
|
|
65
59
|
|
|
@@ -95,8 +89,7 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
|
|
|
95
89
|
title: session?.title ?? '',
|
|
96
90
|
description: session?.description ?? '',
|
|
97
91
|
duration: session?.duration ?? 0,
|
|
98
|
-
|
|
99
|
-
published: session?.published ?? true,
|
|
92
|
+
published: session?.published ?? false,
|
|
100
93
|
};
|
|
101
94
|
|
|
102
95
|
const form = useForm<FormValues>({
|
|
@@ -113,8 +106,7 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
|
|
|
113
106
|
title: session.title,
|
|
114
107
|
description: session.description ?? '',
|
|
115
108
|
duration: session.duration,
|
|
116
|
-
|
|
117
|
-
published: session.published ?? true,
|
|
109
|
+
published: session.published ?? false,
|
|
118
110
|
});
|
|
119
111
|
}, [session?.id]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
120
112
|
|
|
@@ -156,12 +148,23 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
|
|
|
156
148
|
{session.code}
|
|
157
149
|
</p>
|
|
158
150
|
</div>
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
151
|
+
<Tooltip>
|
|
152
|
+
<TooltipTrigger asChild>
|
|
153
|
+
<span
|
|
154
|
+
className="inline-flex shrink-0 items-center"
|
|
155
|
+
aria-label={session.published ? 'Publicada' : 'Rascunho'}
|
|
156
|
+
>
|
|
157
|
+
{session.published ? (
|
|
158
|
+
<Eye className="size-4 text-emerald-600 dark:text-emerald-400" />
|
|
159
|
+
) : (
|
|
160
|
+
<EyeOff className="size-4 text-muted-foreground" />
|
|
161
|
+
)}
|
|
162
|
+
</span>
|
|
163
|
+
</TooltipTrigger>
|
|
164
|
+
<TooltipContent>
|
|
165
|
+
{session.published ? 'Publicada' : 'Rascunho'}
|
|
166
|
+
</TooltipContent>
|
|
167
|
+
</Tooltip>
|
|
165
168
|
<IconActionTooltip
|
|
166
169
|
label="Excluir sessão"
|
|
167
170
|
asWrapper={deleteSession.isPending}
|
|
@@ -285,45 +288,7 @@ export function EditorSession({ sessionId }: EditorSessionProps) {
|
|
|
285
288
|
</CardTitle>
|
|
286
289
|
</CardHeader>
|
|
287
290
|
<CardContent className="px-3 pb-2">
|
|
288
|
-
<div className="grid grid-cols-1 gap-2 items-end
|
|
289
|
-
<FormField
|
|
290
|
-
control={form.control}
|
|
291
|
-
name="visibility"
|
|
292
|
-
render={({ field }) => (
|
|
293
|
-
<FormItem>
|
|
294
|
-
<FormLabel className="text-xs">Visibilidade</FormLabel>
|
|
295
|
-
<Select
|
|
296
|
-
value={field.value}
|
|
297
|
-
onValueChange={field.onChange}
|
|
298
|
-
>
|
|
299
|
-
<FormControl>
|
|
300
|
-
<SelectTrigger className="h-8 text-xs w-full">
|
|
301
|
-
<SelectValue />
|
|
302
|
-
</SelectTrigger>
|
|
303
|
-
</FormControl>
|
|
304
|
-
<SelectContent>
|
|
305
|
-
<SelectItem value="publico">
|
|
306
|
-
<span className="flex items-center gap-1.5">
|
|
307
|
-
<Eye className="size-3" /> Público
|
|
308
|
-
</span>
|
|
309
|
-
</SelectItem>
|
|
310
|
-
<SelectItem value="privado">
|
|
311
|
-
<span className="flex items-center gap-1.5">
|
|
312
|
-
<EyeOff className="size-3" /> Privado
|
|
313
|
-
</span>
|
|
314
|
-
</SelectItem>
|
|
315
|
-
<SelectItem value="restrito">
|
|
316
|
-
<span className="flex items-center gap-1.5">
|
|
317
|
-
<Lock className="size-3" /> Restrito
|
|
318
|
-
</span>
|
|
319
|
-
</SelectItem>
|
|
320
|
-
</SelectContent>
|
|
321
|
-
</Select>
|
|
322
|
-
<FormMessage className="text-xs" />
|
|
323
|
-
</FormItem>
|
|
324
|
-
)}
|
|
325
|
-
/>
|
|
326
|
-
|
|
291
|
+
<div className="grid grid-cols-1 gap-2 items-end">
|
|
327
292
|
<FormField
|
|
328
293
|
control={form.control}
|
|
329
294
|
name="published"
|
|
@@ -72,6 +72,8 @@ interface StructureState {
|
|
|
72
72
|
open: boolean;
|
|
73
73
|
title: string;
|
|
74
74
|
description: string;
|
|
75
|
+
confirmText: string;
|
|
76
|
+
destructive: boolean;
|
|
75
77
|
onConfirm: (() => void) | null;
|
|
76
78
|
};
|
|
77
79
|
|
|
@@ -122,6 +124,7 @@ interface StructureState {
|
|
|
122
124
|
data: Partial<
|
|
123
125
|
LessonFormValues & {
|
|
124
126
|
transcription?: string;
|
|
127
|
+
videoConversionJobId?: number;
|
|
125
128
|
resources?: Lesson['resources'];
|
|
126
129
|
}
|
|
127
130
|
>
|
|
@@ -183,6 +186,8 @@ interface StructureState {
|
|
|
183
186
|
showConfirm: (opts: {
|
|
184
187
|
title: string;
|
|
185
188
|
description: string;
|
|
189
|
+
confirmText?: string;
|
|
190
|
+
destructive?: boolean;
|
|
186
191
|
onConfirm: () => void;
|
|
187
192
|
}) => void;
|
|
188
193
|
closeConfirm: () => void;
|
|
@@ -282,6 +287,8 @@ export const useStructureStore = create<StructureState>((set, get) => ({
|
|
|
282
287
|
open: false,
|
|
283
288
|
title: '',
|
|
284
289
|
description: '',
|
|
290
|
+
confirmText: '',
|
|
291
|
+
destructive: true,
|
|
285
292
|
onConfirm: null as (() => void) | null,
|
|
286
293
|
},
|
|
287
294
|
sessionPickerDialog: {
|
|
@@ -380,6 +387,7 @@ export const useStructureStore = create<StructureState>((set, get) => ({
|
|
|
380
387
|
title: `Nova Sessão ${code}`,
|
|
381
388
|
duration: 30,
|
|
382
389
|
order: sessions.length,
|
|
390
|
+
published: false,
|
|
383
391
|
};
|
|
384
392
|
set((s) => {
|
|
385
393
|
const nextExpandedIds = new Set([...s.expandedIds, newSession.id]);
|
|
@@ -433,6 +441,7 @@ export const useStructureStore = create<StructureState>((set, get) => ({
|
|
|
433
441
|
duration: 10,
|
|
434
442
|
sessionId,
|
|
435
443
|
order: sessionLessons.length,
|
|
444
|
+
published: false,
|
|
436
445
|
resources: [],
|
|
437
446
|
};
|
|
438
447
|
set((s) => {
|
|
@@ -919,6 +928,8 @@ export const useStructureStore = create<StructureState>((set, get) => ({
|
|
|
919
928
|
open: true,
|
|
920
929
|
title: opts.title,
|
|
921
930
|
description: opts.description,
|
|
931
|
+
confirmText: opts.confirmText ?? '',
|
|
932
|
+
destructive: opts.destructive ?? true,
|
|
922
933
|
onConfirm: opts.onConfirm,
|
|
923
934
|
},
|
|
924
935
|
}),
|
|
@@ -328,6 +328,7 @@ function LessonMenu({ data }: { data: Lesson }) {
|
|
|
328
328
|
|
|
329
329
|
const hasLessonClipboard = copiedType === 'lesson' && copiedIds.length > 0;
|
|
330
330
|
const hasOtherSessions = sessions.some((ss) => ss.id !== data.sessionId);
|
|
331
|
+
const session = sessions.find((ss) => ss.id === data.sessionId);
|
|
331
332
|
|
|
332
333
|
// Ids to move when "Mover" is triggered
|
|
333
334
|
const idsToMove = multiLessons ? selectedLessonIds : [data.id];
|
|
@@ -373,6 +374,27 @@ function LessonMenu({ data }: { data: Lesson }) {
|
|
|
373
374
|
Copiar aula
|
|
374
375
|
</ContextMenuItem>
|
|
375
376
|
|
|
377
|
+
<ContextMenuItem
|
|
378
|
+
onSelect={async () => {
|
|
379
|
+
if (!session) {
|
|
380
|
+
toast.error('Sessão da aula não encontrada');
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const lessonCopyCode = `${session.code}_${data.code}_${data.title}`;
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
await navigator.clipboard.writeText(lessonCopyCode);
|
|
388
|
+
toast.success('Código da aula copiado');
|
|
389
|
+
} catch {
|
|
390
|
+
toast.error('Não foi possível copiar o código da aula');
|
|
391
|
+
}
|
|
392
|
+
}}
|
|
393
|
+
>
|
|
394
|
+
<Clipboard className="size-3.5 mr-2 text-muted-foreground" />
|
|
395
|
+
Copiar código da aula
|
|
396
|
+
</ContextMenuItem>
|
|
397
|
+
|
|
376
398
|
{multiLessons && (
|
|
377
399
|
<ContextMenuItem
|
|
378
400
|
onSelect={() => {
|