@hed-hog/lms 0.0.364 → 0.0.365
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +1 -0
- package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
- package/dist/bitcode-wallet/bitcode-wallet.service.js +22 -3
- package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
- package/dist/course/course-export-scorm12-worker.service.d.ts +21 -0
- package/dist/course/course-export-scorm12-worker.service.d.ts.map +1 -0
- package/dist/course/course-export-scorm12-worker.service.js +109 -0
- package/dist/course/course-export-scorm12-worker.service.js.map +1 -0
- package/dist/course/course-export-scorm12.service.d.ts +42 -0
- package/dist/course/course-export-scorm12.service.d.ts.map +1 -0
- package/dist/course/course-export-scorm12.service.js +628 -0
- package/dist/course/course-export-scorm12.service.js.map +1 -0
- package/dist/course/course-export.service.d.ts +84 -0
- package/dist/course/course-export.service.d.ts.map +1 -0
- package/dist/course/course-export.service.js +237 -0
- package/dist/course/course-export.service.js.map +1 -0
- package/dist/course/course-structure.controller.d.ts +17 -9
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +17 -4
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +12 -4
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +98 -23
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course-video-hls.service.d.ts +57 -0
- package/dist/course/course-video-hls.service.d.ts.map +1 -0
- package/dist/course/course-video-hls.service.js +767 -0
- package/dist/course/course-video-hls.service.js.map +1 -0
- package/dist/course/course.controller.d.ts +45 -13
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.controller.js +40 -26
- package/dist/course/course.controller.js.map +1 -1
- package/dist/course/course.mcp-tools.js +1 -1
- package/dist/course/course.mcp-tools.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +11 -0
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +6 -9
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +57 -48
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/cleanup-course-storage.dto.d.ts +1 -1
- package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -1
- package/dist/course/dto/cleanup-course-storage.dto.js +1 -0
- package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -1
- package/dist/course/dto/cleanup-upload-history.dto.d.ts +1 -1
- package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -1
- package/dist/course/dto/cleanup-upload-history.dto.js +1 -1
- package/dist/course/dto/cleanup-upload-history.dto.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 +14 -0
- package/dist/course/dto/create-course-export.dto.d.ts.map +1 -0
- package/dist/course/dto/create-course-export.dto.js +71 -0
- package/dist/course/dto/create-course-export.dto.js.map +1 -0
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts +2 -2
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js +3 -2
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/lms-bulk-upload-automation.service.d.ts +16 -1
- package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload-automation.service.js +102 -8
- package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
- package/dist/course/lms-bulk-upload-infra.service.d.ts +1 -0
- package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload-infra.service.js +32 -8
- package/dist/course/lms-bulk-upload-infra.service.js.map +1 -1
- package/dist/course/lms-bulk-upload.controller.d.ts +30 -3
- package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.controller.js +43 -2
- package/dist/course/lms-bulk-upload.controller.js.map +1 -1
- package/dist/course/lms-bulk-upload.service.d.ts +11 -0
- package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.service.js +59 -6
- package/dist/course/lms-bulk-upload.service.js.map +1 -1
- package/dist/course/lms-setting.controller.d.ts +2 -1
- package/dist/course/lms-setting.controller.d.ts.map +1 -1
- package/dist/course/lms-setting.controller.js +4 -2
- package/dist/course/lms-setting.controller.js.map +1 -1
- package/dist/course/scorm12-schemas.d.ts +4 -0
- package/dist/course/scorm12-schemas.d.ts.map +1 -0
- package/dist/course/scorm12-schemas.js +9 -0
- package/dist/course/scorm12-schemas.js.map +1 -0
- package/dist/enterprise/training/training-student.service.d.ts +51 -0
- package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-student.service.js +217 -4
- package/dist/enterprise/training/training-student.service.js.map +1 -1
- package/dist/evaluation/evaluation.service.d.ts +18 -0
- package/dist/evaluation/evaluation.service.d.ts.map +1 -1
- package/dist/evaluation/evaluation.service.js +125 -0
- package/dist/evaluation/evaluation.service.js.map +1 -1
- package/dist/exam/dto/create-standalone-question.dto.d.ts +12 -0
- package/dist/exam/dto/create-standalone-question.dto.d.ts.map +1 -0
- package/dist/exam/dto/create-standalone-question.dto.js +70 -0
- package/dist/exam/dto/create-standalone-question.dto.js.map +1 -0
- package/dist/exam/exam.module.d.ts.map +1 -1
- package/dist/exam/exam.module.js +2 -1
- package/dist/exam/exam.module.js.map +1 -1
- package/dist/exam/exam.service.d.ts +21 -0
- package/dist/exam/exam.service.d.ts.map +1 -1
- package/dist/exam/exam.service.js +80 -0
- package/dist/exam/exam.service.js.map +1 -1
- package/dist/exam/question.controller.d.ts +27 -0
- package/dist/exam/question.controller.d.ts.map +1 -0
- package/dist/exam/question.controller.js +53 -0
- package/dist/exam/question.controller.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +4 -0
- 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 +161 -25
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -1
- package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -1
- package/dist/lms-commerce-access.subscriber.d.ts +11 -0
- package/dist/lms-commerce-access.subscriber.d.ts.map +1 -0
- package/dist/lms-commerce-access.subscriber.js +74 -0
- package/dist/lms-commerce-access.subscriber.js.map +1 -0
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +6 -5
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/platforma-video.service.d.ts +39 -0
- package/dist/platforma/platforma-video.service.d.ts.map +1 -0
- package/dist/platforma/platforma-video.service.js +301 -0
- package/dist/platforma/platforma-video.service.js.map +1 -0
- package/dist/platforma/platforma.controller.d.ts +95 -1
- package/dist/platforma/platforma.controller.d.ts.map +1 -1
- package/dist/platforma/platforma.controller.js +160 -2
- package/dist/platforma/platforma.controller.js.map +1 -1
- package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts +5 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.d.ts.map +1 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.js +26 -0
- package/dist/student-xp/dto/grant-skill-card-xp.dto.js.map +1 -0
- package/dist/student-xp/student-xp.controller.d.ts +15 -0
- package/dist/student-xp/student-xp.controller.d.ts.map +1 -1
- package/dist/student-xp/student-xp.controller.js +24 -0
- package/dist/student-xp/student-xp.controller.js.map +1 -1
- package/dist/student-xp/student-xp.service.d.ts +16 -0
- package/dist/student-xp/student-xp.service.d.ts.map +1 -1
- package/dist/student-xp/student-xp.service.js +51 -1
- package/dist/student-xp/student-xp.service.js.map +1 -1
- package/hedhog/data/evaluation_topic.yaml +17 -0
- package/hedhog/data/menu.yaml +0 -17
- package/hedhog/data/queue_definition.yaml +48 -0
- package/hedhog/data/route.yaml +94 -124
- package/hedhog/data/setting_group.yaml +19 -19
- package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +337 -41
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +69 -4
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-export-sheet.tsx.ejs +420 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-exports-tab.tsx.ejs +308 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +17 -15
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -63
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +8 -3
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +31 -8
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +16 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +201 -401
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +378 -690
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +3 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +6 -10
- package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +49 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +4 -3
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +0 -1
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-exports.ts.ejs +106 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +28 -1
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +0 -2
- package/hedhog/frontend/app/courses/page.tsx.ejs +45 -0
- package/hedhog/frontend/messages/en.json +26 -28
- package/hedhog/frontend/messages/pt.json +26 -28
- package/hedhog/table/course_export.yaml +62 -0
- package/package.json +13 -9
- package/src/bitcode-wallet/bitcode-wallet.service.ts +43 -4
- package/src/course/course-export-scorm12-worker.service.ts +124 -0
- package/src/course/course-export-scorm12.service.ts +668 -0
- package/src/course/course-export.service.ts +280 -0
- package/src/course/course-structure.controller.ts +14 -2
- package/src/course/course-structure.service.ts +100 -7
- package/src/course/course-video-hls.service.ts +946 -0
- package/src/course/course.controller.ts +33 -19
- package/src/course/course.mcp-tools.ts +1 -1
- package/src/course/course.module.ts +11 -0
- package/src/course/course.service.ts +73 -60
- package/src/course/dto/cleanup-course-storage.dto.ts +1 -0
- package/src/course/dto/cleanup-upload-history.dto.ts +1 -1
- package/src/course/dto/create-course-bulk-job.dto.ts +7 -3
- package/src/course/dto/create-course-export.dto.ts +56 -0
- package/src/course/dto/create-course-structure-lesson.dto.ts +4 -3
- package/src/course/lms-bulk-upload-automation.service.ts +153 -6
- package/src/course/lms-bulk-upload-infra.service.ts +39 -6
- package/src/course/lms-bulk-upload.controller.ts +32 -2
- package/src/course/lms-bulk-upload.service.ts +70 -7
- package/src/course/lms-setting.controller.ts +4 -2
- package/src/course/scorm12-schemas.ts +9 -0
- package/src/enterprise/training/training-student.service.ts +221 -2
- package/src/evaluation/evaluation.service.ts +123 -0
- package/src/exam/dto/create-standalone-question.dto.ts +66 -0
- package/src/exam/exam.module.ts +2 -1
- package/src/exam/exam.service.ts +86 -0
- package/src/exam/question.controller.ts +28 -0
- package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +205 -31
- package/src/lms-commerce-access.subscriber.ts +88 -0
- package/src/lms.module.ts +6 -5
- package/src/platforma/platforma-video.service.ts +346 -0
- package/src/platforma/platforma.controller.ts +95 -1
- package/src/platforma/platforma.service.ts +268 -268
- package/src/student-xp/dto/grant-skill-card-xp.dto.ts +10 -0
- package/src/student-xp/student-xp.controller.ts +18 -2
- package/src/student-xp/student-xp.service.ts +84 -2
- package/hedhog/data/video_resolution_profile.yaml +0 -7
- package/hedhog/frontend/app/video-resolution-profiles/page.tsx.ejs +0 -607
- package/hedhog/table/course_video_resolution_profile.yaml +0 -22
- package/hedhog/table/video_resolution_profile.yaml +0 -18
- package/src/video-resolution-profile/dto/create-video-resolution-profile.dto.ts +0 -16
- package/src/video-resolution-profile/dto/update-video-resolution-profile.dto.ts +0 -16
- package/src/video-resolution-profile/video-resolution-profile.controller.ts +0 -62
- package/src/video-resolution-profile/video-resolution-profile.mcp-tools.ts +0 -128
- package/src/video-resolution-profile/video-resolution-profile.module.ts +0 -13
- package/src/video-resolution-profile/video-resolution-profile.service.ts +0 -117
|
@@ -59,13 +59,14 @@ import { getPhotoUrl } from '@/lib/get-photo-url';
|
|
|
59
59
|
import { cn } from '@/lib/utils';
|
|
60
60
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
61
61
|
import {
|
|
62
|
+
AlertCircle,
|
|
62
63
|
Ban,
|
|
63
64
|
CheckCircle2,
|
|
64
65
|
Clock,
|
|
65
66
|
Cog,
|
|
66
67
|
ExternalLink,
|
|
67
68
|
HardDriveUpload,
|
|
68
|
-
|
|
69
|
+
Link2,
|
|
69
70
|
Loader2,
|
|
70
71
|
Pencil,
|
|
71
72
|
Plus,
|
|
@@ -85,7 +86,8 @@ type UploadStatus =
|
|
|
85
86
|
| 'received'
|
|
86
87
|
| 'done'
|
|
87
88
|
| 'error'
|
|
88
|
-
| 'cancelled'
|
|
89
|
+
| 'cancelled'
|
|
90
|
+
| 'lesson_not_found';
|
|
89
91
|
|
|
90
92
|
type UploadItemRow = {
|
|
91
93
|
id: number;
|
|
@@ -188,7 +190,7 @@ type BulkUploadRegenerateTokenResponse = {
|
|
|
188
190
|
webhook: WebhookIntegrationItem;
|
|
189
191
|
};
|
|
190
192
|
|
|
191
|
-
type BulkUploadCleanupStatus = 'done' | 'error' | 'cancelled';
|
|
193
|
+
type BulkUploadCleanupStatus = 'received' | 'done' | 'error' | 'cancelled';
|
|
192
194
|
type BulkUploadCleanupTimeWindow =
|
|
193
195
|
| 'last_hour'
|
|
194
196
|
| 'last_day'
|
|
@@ -214,7 +216,8 @@ const CLEANUP_STATUS_OPTIONS: Array<{
|
|
|
214
216
|
value: BulkUploadCleanupStatus;
|
|
215
217
|
label: string;
|
|
216
218
|
}> = [
|
|
217
|
-
{ value: '
|
|
219
|
+
{ value: 'received', label: 'Concluidos (webhook confirmado)' },
|
|
220
|
+
{ value: 'done', label: 'Enviados (aguardando confirmação)' },
|
|
218
221
|
{ value: 'error', label: 'Com falha' },
|
|
219
222
|
{ value: 'cancelled', label: 'Cancelados' },
|
|
220
223
|
];
|
|
@@ -270,21 +273,21 @@ function getStatusMeta(status: UploadStatus) {
|
|
|
270
273
|
switch (status) {
|
|
271
274
|
case 'received':
|
|
272
275
|
return {
|
|
273
|
-
label: '
|
|
276
|
+
label: 'Concluido',
|
|
274
277
|
variant: 'default' as const,
|
|
275
|
-
icon:
|
|
278
|
+
icon: CheckCircle2,
|
|
276
279
|
iconClass: 'text-green-600',
|
|
277
280
|
animated: false,
|
|
278
281
|
progressClass: 'bg-green-500',
|
|
279
282
|
};
|
|
280
283
|
case 'done':
|
|
281
284
|
return {
|
|
282
|
-
label: '
|
|
283
|
-
variant: '
|
|
284
|
-
icon:
|
|
285
|
-
iconClass: 'text-
|
|
286
|
-
animated:
|
|
287
|
-
progressClass: 'bg-
|
|
285
|
+
label: 'Aguardando confirmação',
|
|
286
|
+
variant: 'secondary' as const,
|
|
287
|
+
icon: HardDriveUpload,
|
|
288
|
+
iconClass: 'text-blue-500 animate-pulse',
|
|
289
|
+
animated: true,
|
|
290
|
+
progressClass: 'bg-blue-400',
|
|
288
291
|
};
|
|
289
292
|
case 'uploading':
|
|
290
293
|
return {
|
|
@@ -322,6 +325,15 @@ function getStatusMeta(status: UploadStatus) {
|
|
|
322
325
|
animated: false,
|
|
323
326
|
progressClass: 'bg-muted-foreground/40',
|
|
324
327
|
};
|
|
328
|
+
case 'lesson_not_found':
|
|
329
|
+
return {
|
|
330
|
+
label: 'Aula não encontrada',
|
|
331
|
+
variant: 'outline' as const,
|
|
332
|
+
icon: AlertCircle,
|
|
333
|
+
iconClass: 'text-amber-500',
|
|
334
|
+
animated: false,
|
|
335
|
+
progressClass: 'bg-amber-400',
|
|
336
|
+
};
|
|
325
337
|
default:
|
|
326
338
|
return {
|
|
327
339
|
label: 'Erro',
|
|
@@ -342,7 +354,8 @@ export default function LmsBulkUploadSessionsPage() {
|
|
|
342
354
|
const [statusFilter, setStatusFilter] = useState('all');
|
|
343
355
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
344
356
|
const [bucketName, setBucketName] = useState('');
|
|
345
|
-
const [
|
|
357
|
+
const [awsAccountId, setAwsAccountId] = useState('');
|
|
358
|
+
const [lambdaRoleName, setLambdaRoleName] = useState('');
|
|
346
359
|
const [storageProfileId, setStorageProfileId] = useState<number | null>(null);
|
|
347
360
|
const [webhookPlainToken, setWebhookPlainToken] = useState<string | null>(
|
|
348
361
|
null
|
|
@@ -361,10 +374,26 @@ export default function LmsBulkUploadSessionsPage() {
|
|
|
361
374
|
const [isCleanupDialogOpen, setIsCleanupDialogOpen] = useState(false);
|
|
362
375
|
const [cleanupStatuses, setCleanupStatuses] = useState<
|
|
363
376
|
Set<BulkUploadCleanupStatus>
|
|
364
|
-
>(new Set(['
|
|
377
|
+
>(new Set(['received']));
|
|
365
378
|
const [cleanupTimeWindow, setCleanupTimeWindow] =
|
|
366
379
|
useState<BulkUploadCleanupTimeWindow>('last_day');
|
|
367
380
|
const [isCleaningHistory, setIsCleaningHistory] = useState(false);
|
|
381
|
+
const [manualLinkOpen, setManualLinkOpen] = useState(false);
|
|
382
|
+
const [manualLinkItemId, setManualLinkItemId] = useState<number | null>(null);
|
|
383
|
+
const [manualLinkVideoUrl, setManualLinkVideoUrl] = useState<string | null>(null);
|
|
384
|
+
const [manualLinkVideoLoading, setManualLinkVideoLoading] = useState(false);
|
|
385
|
+
const [manualLinkCourseId, setManualLinkCourseId] = useState<number | null>(null);
|
|
386
|
+
const [manualLinkSessionId, setManualLinkSessionId] = useState<number | null>(null);
|
|
387
|
+
const [manualLinkLessonId, setManualLinkLessonId] = useState<number | null>(null);
|
|
388
|
+
const [manualLinkStructure, setManualLinkStructure] = useState<{
|
|
389
|
+
sessions: Array<{
|
|
390
|
+
id: number;
|
|
391
|
+
title: string;
|
|
392
|
+
lessons: Array<{ id: number; title: string }>;
|
|
393
|
+
}>;
|
|
394
|
+
} | null>(null);
|
|
395
|
+
const [manualLinkStructureLoading, setManualLinkStructureLoading] = useState(false);
|
|
396
|
+
const [isLinkingLesson, setIsLinkingLesson] = useState(false);
|
|
368
397
|
const [pageSize, setPageSize] = usePersistedPageSize({
|
|
369
398
|
storageKey: 'pagination:global:pageSize',
|
|
370
399
|
defaultValue: PAGE_SIZE_OPTIONS[1],
|
|
@@ -417,6 +446,8 @@ export default function LmsBulkUploadSessionsPage() {
|
|
|
417
446
|
},
|
|
418
447
|
});
|
|
419
448
|
|
|
449
|
+
const ACTIVE_STATUSES: UploadStatus[] = ['queued', 'uploading', 'cancelling', 'done'];
|
|
450
|
+
|
|
420
451
|
const {
|
|
421
452
|
data,
|
|
422
453
|
isLoading,
|
|
@@ -442,6 +473,23 @@ export default function LmsBulkUploadSessionsPage() {
|
|
|
442
473
|
});
|
|
443
474
|
return response.data;
|
|
444
475
|
},
|
|
476
|
+
refetchInterval: (query) => {
|
|
477
|
+
const currentRows = query.state.data?.data ?? [];
|
|
478
|
+
const hasActive = currentRows.some((row) => ACTIVE_STATUSES.includes(row.status));
|
|
479
|
+
return hasActive ? 10_000 : false;
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const { data: coursesResult } = useQuery<{ data: Array<{ id: number; title: string; code: string | null }> }>({
|
|
484
|
+
queryKey: ['lms-bulk-upload-courses-picker', manualLinkOpen],
|
|
485
|
+
enabled: manualLinkOpen,
|
|
486
|
+
queryFn: async () => {
|
|
487
|
+
const response = await request<{ data: Array<{ id: number; title: string; code: string | null }> }>({
|
|
488
|
+
url: '/course?pageSize=500',
|
|
489
|
+
method: 'GET',
|
|
490
|
+
});
|
|
491
|
+
return response.data;
|
|
492
|
+
},
|
|
445
493
|
});
|
|
446
494
|
|
|
447
495
|
const rows = useMemo(() => data?.data ?? [], [data?.data]);
|
|
@@ -449,7 +497,7 @@ export default function LmsBulkUploadSessionsPage() {
|
|
|
449
497
|
const stats = useMemo(
|
|
450
498
|
() => ({
|
|
451
499
|
sending: rows.filter((row) => row.status === 'uploading').length,
|
|
452
|
-
done: rows.filter((row) => row.status === '
|
|
500
|
+
done: rows.filter((row) => row.status === 'received').length,
|
|
453
501
|
error: rows.filter((row) => row.status === 'error').length,
|
|
454
502
|
}),
|
|
455
503
|
[rows]
|
|
@@ -513,7 +561,16 @@ export default function LmsBulkUploadSessionsPage() {
|
|
|
513
561
|
[webhookPreview, webhooksResult?.data]
|
|
514
562
|
);
|
|
515
563
|
|
|
516
|
-
const
|
|
564
|
+
const lambdaRoleArn =
|
|
565
|
+
awsAccountId && lambdaRoleName
|
|
566
|
+
? `arn:aws:iam::${awsAccountId}:role/${lambdaRoleName}`
|
|
567
|
+
: '';
|
|
568
|
+
const isAccountIdValid = !awsAccountId || /^\d{12}$/.test(awsAccountId);
|
|
569
|
+
const isRoleNameValid =
|
|
570
|
+
!lambdaRoleName || /^[a-zA-Z_0-9+=,.@\-_/]+$/.test(lambdaRoleName);
|
|
571
|
+
const hasBaseConfig = Boolean(
|
|
572
|
+
storageProfileId && awsAccountId && lambdaRoleName && isAccountIdValid && isRoleNameValid
|
|
573
|
+
);
|
|
517
574
|
const isWebhookActive = Boolean(
|
|
518
575
|
webhookIntegration?.status === 'active' && webhookIntegration?.public_url
|
|
519
576
|
);
|
|
@@ -540,7 +597,10 @@ export default function LmsBulkUploadSessionsPage() {
|
|
|
540
597
|
|
|
541
598
|
useEffect(() => {
|
|
542
599
|
setBucketName(String(settingsResult?.bucketName ?? '').trim());
|
|
543
|
-
|
|
600
|
+
const arn = String(settingsResult?.lambdaRoleArn ?? '').trim();
|
|
601
|
+
const arnMatch = arn.match(/^arn:[^:]*:iam::(\d{12}):role\/(.+)$/);
|
|
602
|
+
setAwsAccountId(arnMatch?.[1] ?? '');
|
|
603
|
+
setLambdaRoleName(arnMatch?.[2] ?? '');
|
|
544
604
|
const parsedProfileId = Number(settingsResult?.storageProfileId ?? 0);
|
|
545
605
|
setStorageProfileId(
|
|
546
606
|
Number.isFinite(parsedProfileId) && parsedProfileId > 0
|
|
@@ -594,15 +654,81 @@ export default function LmsBulkUploadSessionsPage() {
|
|
|
594
654
|
refetchProfiles(),
|
|
595
655
|
refetchWebhooks(),
|
|
596
656
|
]);
|
|
597
|
-
} catch {
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
657
|
+
} catch (error: unknown) {
|
|
658
|
+
const message =
|
|
659
|
+
(error as { response?: { data?: { message?: string } } })?.response?.data
|
|
660
|
+
?.message ?? null;
|
|
661
|
+
toast.error('Falha ao salvar a configuração.', {
|
|
662
|
+
description: message || 'Verifique o ARN da role e as credenciais do perfil de storage.',
|
|
663
|
+
});
|
|
601
664
|
} finally {
|
|
602
665
|
setIsSavingSettings(false);
|
|
603
666
|
}
|
|
604
667
|
};
|
|
605
668
|
|
|
669
|
+
const openManualLink = async (itemId: number) => {
|
|
670
|
+
setManualLinkItemId(itemId);
|
|
671
|
+
setManualLinkCourseId(null);
|
|
672
|
+
setManualLinkSessionId(null);
|
|
673
|
+
setManualLinkLessonId(null);
|
|
674
|
+
setManualLinkStructure(null);
|
|
675
|
+
setManualLinkVideoUrl(null);
|
|
676
|
+
setManualLinkOpen(true);
|
|
677
|
+
setManualLinkVideoLoading(true);
|
|
678
|
+
try {
|
|
679
|
+
const res = await request<{ url: string }>({
|
|
680
|
+
url: `/lms/bulk-upload/item/${itemId}/video-url`,
|
|
681
|
+
method: 'GET',
|
|
682
|
+
});
|
|
683
|
+
setManualLinkVideoUrl(res.data.url);
|
|
684
|
+
} catch {
|
|
685
|
+
// vídeo indisponível, mas o sheet ainda abre
|
|
686
|
+
} finally {
|
|
687
|
+
setManualLinkVideoLoading(false);
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
|
|
691
|
+
const loadCourseStructure = async (courseId: number) => {
|
|
692
|
+
setManualLinkStructureLoading(true);
|
|
693
|
+
setManualLinkSessionId(null);
|
|
694
|
+
setManualLinkLessonId(null);
|
|
695
|
+
try {
|
|
696
|
+
const res = await request<{ sessions: Array<{ id: number; title: string; lessons: Array<{ id: number; title: string }> }> }>({
|
|
697
|
+
url: `/course/${courseId}/structure`,
|
|
698
|
+
method: 'GET',
|
|
699
|
+
});
|
|
700
|
+
setManualLinkStructure({ sessions: res.data.sessions ?? [] });
|
|
701
|
+
} catch {
|
|
702
|
+
setManualLinkStructure(null);
|
|
703
|
+
} finally {
|
|
704
|
+
setManualLinkStructureLoading(false);
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
|
|
708
|
+
const confirmManualLink = async () => {
|
|
709
|
+
if (!manualLinkItemId || !manualLinkCourseId || !manualLinkSessionId || !manualLinkLessonId) return;
|
|
710
|
+
try {
|
|
711
|
+
setIsLinkingLesson(true);
|
|
712
|
+
await request({
|
|
713
|
+
url: `/lms/bulk-upload/item/${manualLinkItemId}/link-lesson`,
|
|
714
|
+
method: 'POST',
|
|
715
|
+
data: {
|
|
716
|
+
courseId: manualLinkCourseId,
|
|
717
|
+
sessionId: manualLinkSessionId,
|
|
718
|
+
lessonId: manualLinkLessonId,
|
|
719
|
+
},
|
|
720
|
+
});
|
|
721
|
+
toast.success('Aula vinculada! O processamento do vídeo foi iniciado.');
|
|
722
|
+
setManualLinkOpen(false);
|
|
723
|
+
await refetchSessions();
|
|
724
|
+
} catch (error: unknown) {
|
|
725
|
+
const message = (error as { response?: { data?: { message?: string } } })?.response?.data?.message ?? null;
|
|
726
|
+
toast.error('Falha ao vincular aula.', { description: message ?? undefined });
|
|
727
|
+
} finally {
|
|
728
|
+
setIsLinkingLesson(false);
|
|
729
|
+
}
|
|
730
|
+
};
|
|
731
|
+
|
|
606
732
|
const regenerateWebhookToken = async () => {
|
|
607
733
|
if (!webhookIntegration) return;
|
|
608
734
|
try {
|
|
@@ -838,28 +964,71 @@ export default function LmsBulkUploadSessionsPage() {
|
|
|
838
964
|
</p>
|
|
839
965
|
<p className="mt-1 text-xs text-amber-800/90 dark:text-amber-300/90">
|
|
840
966
|
Essa role será usada para criar a Lambda automaticamente
|
|
841
|
-
quando necessário. Informe o
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
</span>{' '}
|
|
845
|
-
antes de salvar a configuração.
|
|
967
|
+
quando necessário. Informe o número da conta AWS e o nome
|
|
968
|
+
da role IAM de execução da Lambda antes de salvar a
|
|
969
|
+
configuração.
|
|
846
970
|
</p>
|
|
847
971
|
</div>
|
|
848
972
|
|
|
849
973
|
<div className="space-y-2">
|
|
850
|
-
<
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
974
|
+
<div className="flex gap-2">
|
|
975
|
+
<div className="flex-none space-y-1">
|
|
976
|
+
<Label htmlFor="bulk-upload-aws-account-id">
|
|
977
|
+
Conta AWS
|
|
978
|
+
</Label>
|
|
979
|
+
<Input
|
|
980
|
+
id="bulk-upload-aws-account-id"
|
|
981
|
+
placeholder="123456789012"
|
|
982
|
+
maxLength={12}
|
|
983
|
+
value={awsAccountId}
|
|
984
|
+
onChange={(event) =>
|
|
985
|
+
setAwsAccountId(event.target.value.replace(/\D/g, '').slice(0, 12))
|
|
986
|
+
}
|
|
987
|
+
disabled={settingsLoading || isSavingSettings}
|
|
988
|
+
className={
|
|
989
|
+
awsAccountId && !isAccountIdValid
|
|
990
|
+
? 'border-destructive focus-visible:ring-destructive'
|
|
991
|
+
: ''
|
|
992
|
+
}
|
|
993
|
+
/>
|
|
994
|
+
{awsAccountId && !isAccountIdValid && (
|
|
995
|
+
<p className="text-xs text-destructive">
|
|
996
|
+
Deve ter 12 dígitos.
|
|
997
|
+
</p>
|
|
998
|
+
)}
|
|
999
|
+
</div>
|
|
1000
|
+
<div className="flex-1 space-y-1">
|
|
1001
|
+
<Label htmlFor="bulk-upload-lambda-role-name">
|
|
1002
|
+
Nome da role IAM
|
|
1003
|
+
</Label>
|
|
1004
|
+
<Input
|
|
1005
|
+
id="bulk-upload-lambda-role-name"
|
|
1006
|
+
placeholder="lms-bulk-upload-lambda-role"
|
|
1007
|
+
value={lambdaRoleName}
|
|
1008
|
+
onChange={(event) => setLambdaRoleName(event.target.value)}
|
|
1009
|
+
disabled={settingsLoading || isSavingSettings}
|
|
1010
|
+
className={
|
|
1011
|
+
lambdaRoleName && !isRoleNameValid
|
|
1012
|
+
? 'border-destructive focus-visible:ring-destructive'
|
|
1013
|
+
: ''
|
|
1014
|
+
}
|
|
1015
|
+
/>
|
|
1016
|
+
{lambdaRoleName && !isRoleNameValid && (
|
|
1017
|
+
<p className="text-xs text-destructive">
|
|
1018
|
+
Nome de role inválido.
|
|
1019
|
+
</p>
|
|
1020
|
+
)}
|
|
1021
|
+
</div>
|
|
1022
|
+
</div>
|
|
1023
|
+
{lambdaRoleArn ? (
|
|
1024
|
+
<p className="font-mono text-xs text-muted-foreground">
|
|
1025
|
+
{lambdaRoleArn}
|
|
1026
|
+
</p>
|
|
1027
|
+
) : (
|
|
1028
|
+
<p className="text-xs text-muted-foreground">
|
|
1029
|
+
Setting: {BULK_UPLOAD_LAMBDA_ROLE_ARN_SLUG}
|
|
1030
|
+
</p>
|
|
1031
|
+
)}
|
|
863
1032
|
</div>
|
|
864
1033
|
|
|
865
1034
|
<Label>Perfil de integração de storage</Label>
|
|
@@ -1240,8 +1409,8 @@ export default function LmsBulkUploadSessionsPage() {
|
|
|
1240
1409
|
{ value: 'queued', label: 'Na fila' },
|
|
1241
1410
|
{ value: 'uploading', label: 'Enviando' },
|
|
1242
1411
|
{ value: 'cancelling', label: 'Cancelando' },
|
|
1243
|
-
{ value: '
|
|
1244
|
-
{ value: '
|
|
1412
|
+
{ value: 'done', label: 'Aguardando confirmação' },
|
|
1413
|
+
{ value: 'received', label: 'Concluido' },
|
|
1245
1414
|
{ value: 'cancelled', label: 'Cancelado' },
|
|
1246
1415
|
{ value: 'error', label: 'Erro' },
|
|
1247
1416
|
],
|
|
@@ -1384,6 +1553,16 @@ export default function LmsBulkUploadSessionsPage() {
|
|
|
1384
1553
|
</p>
|
|
1385
1554
|
</div>
|
|
1386
1555
|
</button>
|
|
1556
|
+
) : row.status === 'lesson_not_found' ? (
|
|
1557
|
+
<Button
|
|
1558
|
+
variant="outline"
|
|
1559
|
+
size="sm"
|
|
1560
|
+
className="gap-1.5 text-xs"
|
|
1561
|
+
onClick={() => openManualLink(row.id)}
|
|
1562
|
+
>
|
|
1563
|
+
<Link2 className="h-3.5 w-3.5" />
|
|
1564
|
+
Vincular
|
|
1565
|
+
</Button>
|
|
1387
1566
|
) : (
|
|
1388
1567
|
<span className="text-sm text-muted-foreground">-</span>
|
|
1389
1568
|
)}
|
|
@@ -1448,6 +1627,123 @@ export default function LmsBulkUploadSessionsPage() {
|
|
|
1448
1627
|
}}
|
|
1449
1628
|
pageSizeOptions={PAGE_SIZE_OPTIONS}
|
|
1450
1629
|
/>
|
|
1630
|
+
|
|
1631
|
+
<Sheet open={manualLinkOpen} onOpenChange={setManualLinkOpen}>
|
|
1632
|
+
<ResizableSheetContent
|
|
1633
|
+
sheetId="lms-bulk-upload-manual-link-sheet"
|
|
1634
|
+
defaultWidth={560}
|
|
1635
|
+
minWidth={400}
|
|
1636
|
+
className="flex flex-col gap-0 overflow-hidden"
|
|
1637
|
+
>
|
|
1638
|
+
<SheetHeader className="border-b px-6 py-4">
|
|
1639
|
+
<SheetTitle className="flex items-center gap-2">
|
|
1640
|
+
<Link2 className="h-4 w-4" />
|
|
1641
|
+
Vincular aula manualmente
|
|
1642
|
+
</SheetTitle>
|
|
1643
|
+
<SheetDescription>
|
|
1644
|
+
Selecione o curso, módulo e aula para associar este vídeo e iniciar o processamento.
|
|
1645
|
+
</SheetDescription>
|
|
1646
|
+
</SheetHeader>
|
|
1647
|
+
|
|
1648
|
+
<ScrollArea className="flex-1">
|
|
1649
|
+
<div className="space-y-6 p-6">
|
|
1650
|
+
<div className="overflow-hidden rounded-xl border border-border/60 bg-black">
|
|
1651
|
+
{manualLinkVideoLoading ? (
|
|
1652
|
+
<div className="flex h-40 items-center justify-center">
|
|
1653
|
+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
1654
|
+
</div>
|
|
1655
|
+
) : manualLinkVideoUrl ? (
|
|
1656
|
+
<video
|
|
1657
|
+
src={manualLinkVideoUrl}
|
|
1658
|
+
controls
|
|
1659
|
+
className="w-full"
|
|
1660
|
+
style={{ maxHeight: 280 }}
|
|
1661
|
+
/>
|
|
1662
|
+
) : (
|
|
1663
|
+
<div className="flex h-40 items-center justify-center text-sm text-muted-foreground">
|
|
1664
|
+
Pré-visualização indisponível
|
|
1665
|
+
</div>
|
|
1666
|
+
)}
|
|
1667
|
+
</div>
|
|
1668
|
+
|
|
1669
|
+
<div className="space-y-4">
|
|
1670
|
+
<div className="space-y-2">
|
|
1671
|
+
<Label>Curso</Label>
|
|
1672
|
+
<EntityPicker<{ id: number; title: string; code: string | null }>
|
|
1673
|
+
placeholder="Selecione um curso"
|
|
1674
|
+
options={coursesResult?.data ?? []}
|
|
1675
|
+
value={manualLinkCourseId}
|
|
1676
|
+
valueType="number"
|
|
1677
|
+
getOptionValue={(o) => o.id}
|
|
1678
|
+
getOptionLabel={(o) => o.title}
|
|
1679
|
+
getOptionDescription={(o) => o.code ?? ''}
|
|
1680
|
+
onChange={(value) => {
|
|
1681
|
+
setManualLinkCourseId(value as number | null);
|
|
1682
|
+
if (value) loadCourseStructure(value as number);
|
|
1683
|
+
}}
|
|
1684
|
+
/>
|
|
1685
|
+
</div>
|
|
1686
|
+
|
|
1687
|
+
<div className="space-y-2">
|
|
1688
|
+
<Label>Módulo / Sessão</Label>
|
|
1689
|
+
<EntityPicker<{ id: number; title: string }>
|
|
1690
|
+
placeholder={
|
|
1691
|
+
!manualLinkCourseId
|
|
1692
|
+
? 'Selecione um curso primeiro'
|
|
1693
|
+
: manualLinkStructureLoading
|
|
1694
|
+
? 'Carregando...'
|
|
1695
|
+
: 'Selecione um módulo'
|
|
1696
|
+
}
|
|
1697
|
+
disabled={!manualLinkCourseId || manualLinkStructureLoading}
|
|
1698
|
+
options={manualLinkStructure?.sessions ?? []}
|
|
1699
|
+
value={manualLinkSessionId}
|
|
1700
|
+
valueType="number"
|
|
1701
|
+
getOptionValue={(o) => o.id}
|
|
1702
|
+
getOptionLabel={(o) => o.title}
|
|
1703
|
+
onChange={(value) => {
|
|
1704
|
+
setManualLinkSessionId(value as number | null);
|
|
1705
|
+
setManualLinkLessonId(null);
|
|
1706
|
+
}}
|
|
1707
|
+
/>
|
|
1708
|
+
</div>
|
|
1709
|
+
|
|
1710
|
+
<div className="space-y-2">
|
|
1711
|
+
<Label>Aula</Label>
|
|
1712
|
+
<EntityPicker<{ id: number; title: string }>
|
|
1713
|
+
placeholder={
|
|
1714
|
+
!manualLinkSessionId ? 'Selecione um módulo primeiro' : 'Selecione uma aula'
|
|
1715
|
+
}
|
|
1716
|
+
disabled={!manualLinkSessionId}
|
|
1717
|
+
options={
|
|
1718
|
+
manualLinkStructure?.sessions.find((s) => s.id === manualLinkSessionId)?.lessons ?? []
|
|
1719
|
+
}
|
|
1720
|
+
value={manualLinkLessonId}
|
|
1721
|
+
valueType="number"
|
|
1722
|
+
getOptionValue={(o) => o.id}
|
|
1723
|
+
getOptionLabel={(o) => o.title}
|
|
1724
|
+
onChange={(value) => setManualLinkLessonId(value as number | null)}
|
|
1725
|
+
/>
|
|
1726
|
+
</div>
|
|
1727
|
+
</div>
|
|
1728
|
+
</div>
|
|
1729
|
+
</ScrollArea>
|
|
1730
|
+
|
|
1731
|
+
<div className="border-t px-6 py-4">
|
|
1732
|
+
<Button
|
|
1733
|
+
className="w-full gap-2"
|
|
1734
|
+
disabled={!manualLinkCourseId || !manualLinkSessionId || !manualLinkLessonId || isLinkingLesson}
|
|
1735
|
+
onClick={confirmManualLink}
|
|
1736
|
+
>
|
|
1737
|
+
{isLinkingLesson ? (
|
|
1738
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
1739
|
+
) : (
|
|
1740
|
+
<Link2 className="h-4 w-4" />
|
|
1741
|
+
)}
|
|
1742
|
+
Confirmar vínculo e iniciar processamento
|
|
1743
|
+
</Button>
|
|
1744
|
+
</div>
|
|
1745
|
+
</ResizableSheetContent>
|
|
1746
|
+
</Sheet>
|
|
1451
1747
|
</Page>
|
|
1452
1748
|
);
|
|
1453
1749
|
}
|
|
@@ -9,14 +9,16 @@ import {
|
|
|
9
9
|
ResizablePanel,
|
|
10
10
|
ResizablePanelGroup,
|
|
11
11
|
} from '@/components/ui/resizable';
|
|
12
|
-
import { Sheet, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
|
13
12
|
import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
|
|
13
|
+
import { Sheet, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
|
14
14
|
import { useIsMobile } from '@/hooks/use-mobile';
|
|
15
15
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
16
16
|
import { useIsMutating, useQueryClient } from '@tanstack/react-query';
|
|
17
17
|
import { AlertCircle, Menu, RefreshCw } from 'lucide-react';
|
|
18
18
|
import { useTranslations } from 'next-intl';
|
|
19
|
+
import { useRouter, useSearchParams } from 'next/navigation';
|
|
19
20
|
|
|
21
|
+
import { useLmsRealtimeRefresh } from '../../_lib/hooks/use-lms-realtime-refresh';
|
|
20
22
|
import { ConfirmDialog } from './structure/_components/confirm-dialog';
|
|
21
23
|
import { CourseTreePanel } from './structure/_components/course-tree-panel';
|
|
22
24
|
import { CourseTreeSkeleton } from './structure/_components/course-tree-skeleton';
|
|
@@ -37,7 +39,6 @@ import {
|
|
|
37
39
|
usePasteSessionsMutation,
|
|
38
40
|
} from './structure/_data/use-course-structure-mutations';
|
|
39
41
|
import { useCourseStructureQuery } from './structure/_data/use-course-structure-query';
|
|
40
|
-
import { useLmsRealtimeRefresh } from '../../_lib/hooks/use-lms-realtime-refresh';
|
|
41
42
|
|
|
42
43
|
interface Props {
|
|
43
44
|
params: Promise<{ id: string }>;
|
|
@@ -60,6 +61,8 @@ export default function CourseStructurePage({ params }: Props) {
|
|
|
60
61
|
const isMobile = useIsMobile();
|
|
61
62
|
const { request } = useApp();
|
|
62
63
|
const queryClient = useQueryClient();
|
|
64
|
+
const router = useRouter();
|
|
65
|
+
const searchParams = useSearchParams();
|
|
63
66
|
|
|
64
67
|
const { data: courseSummary } = useQuery<ApiCourseSummary>({
|
|
65
68
|
queryKey: ['lms-course-detail', id],
|
|
@@ -85,6 +88,10 @@ export default function CourseStructurePage({ params }: Props) {
|
|
|
85
88
|
|
|
86
89
|
const setStructureFromApi = useStructureStore((s) => s.setStructureFromApi);
|
|
87
90
|
const setCourseId = useStructureStore((s) => s.setCourseId);
|
|
91
|
+
const selectItem = useStructureStore((s) => s.selectItem);
|
|
92
|
+
|
|
93
|
+
const restoredFromUrl = useRef(false);
|
|
94
|
+
const suppressNextSyncRef = useRef(false);
|
|
88
95
|
|
|
89
96
|
useEffect(() => {
|
|
90
97
|
setCourseId(id);
|
|
@@ -126,6 +133,31 @@ export default function CourseStructurePage({ params }: Props) {
|
|
|
126
133
|
sessions: apiSessions,
|
|
127
134
|
lessons: apiLessons,
|
|
128
135
|
});
|
|
136
|
+
|
|
137
|
+
if (!restoredFromUrl.current) {
|
|
138
|
+
restoredFromUrl.current = true;
|
|
139
|
+
const sel = searchParams.get('sel');
|
|
140
|
+
const type = searchParams.get('type') as
|
|
141
|
+
| 'course'
|
|
142
|
+
| 'session'
|
|
143
|
+
| 'lesson'
|
|
144
|
+
| null;
|
|
145
|
+
if (
|
|
146
|
+
sel &&
|
|
147
|
+
(type === 'course' || type === 'session' || type === 'lesson')
|
|
148
|
+
) {
|
|
149
|
+
const exists =
|
|
150
|
+
type === 'course'
|
|
151
|
+
? mergedCourse?.id === sel
|
|
152
|
+
: type === 'session'
|
|
153
|
+
? apiSessions.some((s) => s.id === sel)
|
|
154
|
+
: apiLessons.some((l) => l.id === sel);
|
|
155
|
+
if (exists) {
|
|
156
|
+
suppressNextSyncRef.current = true;
|
|
157
|
+
selectItem(sel, type);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
129
161
|
}
|
|
130
162
|
}, [
|
|
131
163
|
apiCourse,
|
|
@@ -159,6 +191,33 @@ export default function CourseStructurePage({ params }: Props) {
|
|
|
159
191
|
const copiedType = useStructureStore((s) => s.copiedType);
|
|
160
192
|
const copiedIds = useStructureStore((s) => s.copiedIds);
|
|
161
193
|
|
|
194
|
+
// ── URL sync: selection → URL ──────────────────────────────────────────────
|
|
195
|
+
useEffect(() => {
|
|
196
|
+
if (!restoredFromUrl.current) return;
|
|
197
|
+
if (suppressNextSyncRef.current) {
|
|
198
|
+
suppressNextSyncRef.current = false;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
202
|
+
if (activeItemId && activeItemType) {
|
|
203
|
+
params.set('sel', activeItemId);
|
|
204
|
+
params.set('type', activeItemType);
|
|
205
|
+
}
|
|
206
|
+
params.delete('tab');
|
|
207
|
+
router.replace(`?${params.toString()}`, { scroll: false });
|
|
208
|
+
}, [activeItemId, activeItemType]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
209
|
+
|
|
210
|
+
const handleTabChange = useCallback(
|
|
211
|
+
(tab: string) => {
|
|
212
|
+
const params = new URLSearchParams(searchParams.toString());
|
|
213
|
+
params.set('tab', tab);
|
|
214
|
+
router.replace(`?${params.toString()}`, { scroll: false });
|
|
215
|
+
},
|
|
216
|
+
[router, searchParams]
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const defaultTab = searchParams.get('tab') ?? undefined;
|
|
220
|
+
|
|
162
221
|
useCourseStructureShortcuts({
|
|
163
222
|
searchRef,
|
|
164
223
|
detailPanelRef,
|
|
@@ -277,7 +336,10 @@ export default function CourseStructurePage({ params }: Props) {
|
|
|
277
336
|
className="flex flex-col h-full min-h-0"
|
|
278
337
|
>
|
|
279
338
|
<div className="h-full min-h-0">
|
|
280
|
-
<DetailPanel
|
|
339
|
+
<DetailPanel
|
|
340
|
+
defaultTab={defaultTab}
|
|
341
|
+
onTabChange={handleTabChange}
|
|
342
|
+
/>
|
|
281
343
|
</div>
|
|
282
344
|
</div>
|
|
283
345
|
</ResizablePanel>
|
|
@@ -305,7 +367,10 @@ export default function CourseStructurePage({ params }: Props) {
|
|
|
305
367
|
className="flex-1 min-h-0 border rounded-lg overflow-hidden"
|
|
306
368
|
>
|
|
307
369
|
<div className="h-full min-h-0">
|
|
308
|
-
<DetailPanel
|
|
370
|
+
<DetailPanel
|
|
371
|
+
defaultTab={defaultTab}
|
|
372
|
+
onTabChange={handleTabChange}
|
|
373
|
+
/>
|
|
309
374
|
</div>
|
|
310
375
|
</div>
|
|
311
376
|
|