@hed-hog/lms 0.0.351 → 0.0.354
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/course/course-audio-transcription.service.d.ts +29 -0
- package/dist/course/course-audio-transcription.service.d.ts.map +1 -0
- package/dist/course/course-audio-transcription.service.js +291 -0
- package/dist/course/course-audio-transcription.service.js.map +1 -0
- package/dist/course/course-lesson.controller.d.ts +10 -0
- package/dist/course/course-lesson.controller.d.ts.map +1 -0
- package/dist/course/course-lesson.controller.js +62 -0
- package/dist/course/course-lesson.controller.js.map +1 -0
- package/dist/course/course-structure.controller.d.ts +41 -15
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +50 -6
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +50 -15
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +238 -73
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course-video-conversion.service.d.ts +20 -2
- package/dist/course/course-video-conversion.service.d.ts.map +1 -1
- package/dist/course/course-video-conversion.service.js +730 -10
- package/dist/course/course-video-conversion.service.js.map +1 -1
- package/dist/course/course.controller.d.ts +24 -8
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +5 -3
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +24 -8
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +112 -176
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course-lesson-frame.dto.d.ts +5 -0
- package/dist/course/dto/create-course-lesson-frame.dto.d.ts.map +1 -0
- package/dist/course/dto/create-course-lesson-frame.dto.js +30 -0
- package/dist/course/dto/create-course-lesson-frame.dto.js.map +1 -0
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts +4 -2
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js +10 -3
- package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
- package/dist/course/dto/create-course.dto.d.ts +1 -1
- package/dist/course/dto/create-course.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course.dto.js +6 -6
- package/dist/course/dto/create-course.dto.js.map +1 -1
- package/dist/course/dto/update-course-lesson-frame.dto.d.ts +5 -0
- package/dist/course/dto/update-course-lesson-frame.dto.d.ts.map +1 -0
- package/dist/course/dto/update-course-lesson-frame.dto.js +32 -0
- package/dist/course/dto/update-course-lesson-frame.dto.js.map +1 -0
- package/dist/course/dto/update-course-resources.dto.d.ts +4 -2
- package/dist/course/dto/update-course-resources.dto.d.ts.map +1 -1
- package/dist/course/dto/update-course-resources.dto.js +10 -3
- package/dist/course/dto/update-course-resources.dto.js.map +1 -1
- package/dist/course/dto/update-transcription-segments.dto.d.ts +10 -0
- package/dist/course/dto/update-transcription-segments.dto.d.ts.map +1 -0
- package/dist/course/dto/update-transcription-segments.dto.js +38 -0
- package/dist/course/dto/update-transcription-segments.dto.js.map +1 -0
- package/dist/course/lms-setting.controller.d.ts +13 -0
- package/dist/course/lms-setting.controller.d.ts.map +1 -0
- package/dist/course/lms-setting.controller.js +53 -0
- package/dist/course/lms-setting.controller.js.map +1 -0
- package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-admin.service.js +74 -33
- package/dist/enterprise/training/training-admin.service.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +6 -0
- package/dist/lms.module.js.map +1 -1
- package/hedhog/data/route.yaml +63 -0
- package/hedhog/data/setting_group.yaml +76 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +10 -1
- package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +7 -2
- package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +5 -2
- package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +7 -1
- package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +95 -50
- package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +11 -36
- package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +19 -5
- package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +30 -10
- package/hedhog/frontend/app/courses/[id]/structure/_components/CourseInstructorsSummaryCard.tsx.ejs +95 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +29 -11
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +42 -31
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +10 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +51 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +25 -22
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +12 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +315 -229
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +3220 -534
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +20 -15
- package/hedhog/frontend/app/courses/[id]/structure/_components/icon-action-tooltip.tsx.ejs +35 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +76 -67
- package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +13 -10
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +18 -16
- package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +6 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +23 -11
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +48 -9
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +11 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +121 -15
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +69 -14
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +11 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +31 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +32 -6
- package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +57 -3
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +46 -6
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +14 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-lesson-audio-files.ts.ejs +23 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-lms-settings-query.ts.ejs +34 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +76 -0
- package/hedhog/frontend/messages/en.json +39 -3
- package/hedhog/frontend/messages/pt.json +39 -3
- package/hedhog/table/course.yaml +8 -0
- package/hedhog/table/course_lesson_file.yaml +12 -4
- package/hedhog/table/course_lesson_transcription_segment.yaml +22 -0
- package/hedhog/table/course_lesson_video_frame.yaml +25 -0
- package/package.json +9 -9
- package/src/course/course-audio-transcription.service.ts +393 -0
- package/src/course/course-lesson.controller.ts +28 -0
- package/src/course/course-structure.controller.ts +49 -3
- package/src/course/course-structure.service.ts +294 -32
- package/src/course/course-video-conversion.service.ts +972 -6
- package/src/course/course.module.ts +5 -3
- package/src/course/course.service.ts +87 -139
- package/src/course/dto/create-course-lesson-frame.dto.ts +14 -0
- package/src/course/dto/create-course-structure-lesson.dto.ts +19 -3
- package/src/course/dto/create-course.dto.ts +5 -5
- package/src/course/dto/update-course-lesson-frame.dto.ts +16 -0
- package/src/course/dto/update-course-resources.dto.ts +18 -3
- package/src/course/dto/update-transcription-segments.dto.ts +20 -0
- package/src/course/lms-setting.controller.ts +30 -0
- package/src/enterprise/training/training-admin.service.ts +77 -24
- package/src/index.ts +2 -0
- package/src/lms.module.ts +6 -0
- package/hedhog/table/course_instructor.yaml +0 -27
package/hedhog/frontend/app/courses/[id]/structure/_components/CourseInstructorsSummaryCard.tsx.ejs
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
2
|
+
import { Badge } from '@/components/ui/badge';
|
|
3
|
+
import { Users } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
import { CourseSectionCard } from '../../_components/CourseSectionCard';
|
|
6
|
+
|
|
7
|
+
type CourseInstructorSummary = {
|
|
8
|
+
id: number;
|
|
9
|
+
name: string;
|
|
10
|
+
avatarId: number | null;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type CourseInstructorsSummaryCardProps = {
|
|
14
|
+
instructors: CourseInstructorSummary[];
|
|
15
|
+
compact?: boolean;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function getInstructorAvatarUrl(avatarId?: number | null) {
|
|
19
|
+
return typeof avatarId === 'number' && avatarId > 0
|
|
20
|
+
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
|
|
21
|
+
: null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getInitials(label: string) {
|
|
25
|
+
return label
|
|
26
|
+
.split(' ')
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.slice(0, 2)
|
|
29
|
+
.map((part) => part[0]?.toUpperCase() ?? '')
|
|
30
|
+
.join('');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function CourseInstructorsSummaryCard({
|
|
34
|
+
instructors,
|
|
35
|
+
compact = false,
|
|
36
|
+
}: CourseInstructorsSummaryCardProps) {
|
|
37
|
+
return (
|
|
38
|
+
<CourseSectionCard
|
|
39
|
+
title="Instrutores das aulas"
|
|
40
|
+
description="Agrupados a partir das aulas vinculadas ao curso."
|
|
41
|
+
icon={Users}
|
|
42
|
+
compact={compact}
|
|
43
|
+
>
|
|
44
|
+
{instructors.length > 0 ? (
|
|
45
|
+
<div className={compact ? 'space-y-2' : 'space-y-3'}>
|
|
46
|
+
<div className="flex flex-wrap gap-2">
|
|
47
|
+
<Badge variant="secondary" className="rounded-full px-3 py-1">
|
|
48
|
+
{instructors.length} instrutor
|
|
49
|
+
{instructors.length === 1 ? '' : 'es'}
|
|
50
|
+
</Badge>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div
|
|
54
|
+
className={
|
|
55
|
+
compact
|
|
56
|
+
? 'grid gap-2 sm:grid-cols-2 xl:grid-cols-3'
|
|
57
|
+
: 'grid gap-2 sm:grid-cols-2 xl:grid-cols-3'
|
|
58
|
+
}
|
|
59
|
+
>
|
|
60
|
+
{instructors.map((instructor) => (
|
|
61
|
+
<div
|
|
62
|
+
key={instructor.id}
|
|
63
|
+
className="flex items-center gap-3 rounded-xl border border-border/60 bg-background px-3 py-2.5"
|
|
64
|
+
>
|
|
65
|
+
<Avatar className="h-9 w-9 rounded-xl border border-border/60">
|
|
66
|
+
<AvatarImage
|
|
67
|
+
src={
|
|
68
|
+
getInstructorAvatarUrl(instructor.avatarId) ?? undefined
|
|
69
|
+
}
|
|
70
|
+
/>
|
|
71
|
+
<AvatarFallback className="rounded-xl bg-muted text-xs font-semibold text-foreground">
|
|
72
|
+
{getInitials(instructor.name || `ID ${instructor.id}`)}
|
|
73
|
+
</AvatarFallback>
|
|
74
|
+
</Avatar>
|
|
75
|
+
|
|
76
|
+
<div className="min-w-0">
|
|
77
|
+
<p className="truncate text-sm font-medium text-foreground">
|
|
78
|
+
{instructor.name || `Instrutor #${instructor.id}`}
|
|
79
|
+
</p>
|
|
80
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
81
|
+
ID {instructor.id}
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
) : (
|
|
89
|
+
<p className="text-sm text-muted-foreground">
|
|
90
|
+
Nenhum instrutor vinculado às aulas deste curso.
|
|
91
|
+
</p>
|
|
92
|
+
)}
|
|
93
|
+
</CourseSectionCard>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -31,7 +31,11 @@ import { TreeDragOverlay } from './drag-overlay';
|
|
|
31
31
|
import { SortableTreeRow } from './sortable-tree-row';
|
|
32
32
|
import { useStructureStore } from './store';
|
|
33
33
|
import { TreeRootContextMenu } from './tree-context-menu';
|
|
34
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
buildLessonCountMap,
|
|
36
|
+
buildSessionIndicatorMap,
|
|
37
|
+
buildVisibleItems,
|
|
38
|
+
} from './tree-helpers';
|
|
35
39
|
import type { FlatItem } from './types';
|
|
36
40
|
|
|
37
41
|
const ROW_HEIGHT: Record<FlatItem['type'], number> = {
|
|
@@ -74,6 +78,10 @@ export function CourseTreeDnd() {
|
|
|
74
78
|
);
|
|
75
79
|
|
|
76
80
|
const lessonCountMap = useMemo(() => buildLessonCountMap(lessons), [lessons]);
|
|
81
|
+
const sessionIndicatorMap = useMemo(
|
|
82
|
+
() => buildSessionIndicatorMap(lessons),
|
|
83
|
+
[lessons]
|
|
84
|
+
);
|
|
77
85
|
|
|
78
86
|
const sortedSessions = useMemo(
|
|
79
87
|
() => [...sessions].sort((a, b) => a.order - b.order),
|
|
@@ -160,20 +168,21 @@ export function CourseTreeDnd() {
|
|
|
160
168
|
|
|
161
169
|
if (overItem.type === 'lesson') {
|
|
162
170
|
toSessionId = overItem.data.sessionId;
|
|
163
|
-
// Position relative to lessons in the target session (visible order)
|
|
171
|
+
// Position relative to lessons in the target session (visible order),
|
|
172
|
+
// excluding the dragged item so the index matches what the store expects.
|
|
164
173
|
const destLessons = items.filter(
|
|
165
174
|
(i) =>
|
|
166
175
|
i.type === 'lesson' &&
|
|
167
176
|
i.data.sessionId === toSessionId &&
|
|
168
|
-
i.id !==
|
|
177
|
+
i.id !== activeItem.id
|
|
169
178
|
);
|
|
170
|
-
toIndex = destLessons.findIndex((i) => i.id ===
|
|
179
|
+
toIndex = destLessons.findIndex((i) => i.id === overItem.id);
|
|
171
180
|
if (toIndex === -1) toIndex = destLessons.length;
|
|
172
181
|
} else if (overItem.type === 'session') {
|
|
173
182
|
// Dropped directly onto a session header → append to that session
|
|
174
183
|
toSessionId = overItem.data.id as string;
|
|
175
184
|
const destLessons = lessons.filter(
|
|
176
|
-
(l) => l.sessionId === toSessionId && l.id !==
|
|
185
|
+
(l) => l.sessionId === toSessionId && l.id !== activeItem.id
|
|
177
186
|
);
|
|
178
187
|
toIndex = destLessons.length;
|
|
179
188
|
} else {
|
|
@@ -185,11 +194,9 @@ export function CourseTreeDnd() {
|
|
|
185
194
|
// Snapshot before store mutation (optimistic)
|
|
186
195
|
const previousLessons = [...lessons];
|
|
187
196
|
|
|
188
|
-
// Optimistic store update
|
|
189
|
-
moveLesson(String(activeItem.data.id), toSessionId, toIndex);
|
|
190
|
-
|
|
191
197
|
if (didChangeSession) {
|
|
192
|
-
// Cross-session move
|
|
198
|
+
// Cross-session move — toIndex is in destLessons space (correct for API + store)
|
|
199
|
+
moveLesson(String(activeItem.data.id), toSessionId, toIndex);
|
|
193
200
|
const targetSession = sessions.find((s) => s.id === toSessionId);
|
|
194
201
|
moveLessonMutation.mutate({
|
|
195
202
|
lessonId: String(activeItem.data.id),
|
|
@@ -202,17 +209,27 @@ export function CourseTreeDnd() {
|
|
|
202
209
|
`Aula "${lessonData.title}" movida para "${targetSession?.title ?? toSessionId}"`
|
|
203
210
|
);
|
|
204
211
|
} else {
|
|
205
|
-
//
|
|
212
|
+
// Same-session reorder — derive both indices from the full sessionLessons
|
|
213
|
+
// array so arrayMove and the store's splice are consistent.
|
|
206
214
|
const sessionLessons = lessons
|
|
207
215
|
.filter((l) => l.sessionId === toSessionId)
|
|
208
216
|
.sort((a, b) => a.order - b.order);
|
|
209
217
|
const fromLessonIdx = sessionLessons.findIndex(
|
|
210
218
|
(l) => l.id === String(activeItem.data.id)
|
|
211
219
|
);
|
|
220
|
+
const toLessonIdx = sessionLessons.findIndex(
|
|
221
|
+
(l) => l.id === overItem.id
|
|
222
|
+
);
|
|
223
|
+
if (fromLessonIdx === -1 || toLessonIdx === -1) return;
|
|
224
|
+
|
|
225
|
+
// Pass the full-array index so the store's splice produces the same
|
|
226
|
+
// result as arrayMove (inserting B after C, not before it).
|
|
227
|
+
moveLesson(String(activeItem.data.id), toSessionId, toLessonIdx);
|
|
228
|
+
|
|
212
229
|
const orderedIds = arrayMove(
|
|
213
230
|
sessionLessons,
|
|
214
231
|
fromLessonIdx,
|
|
215
|
-
|
|
232
|
+
toLessonIdx
|
|
216
233
|
).map((l) => l.id);
|
|
217
234
|
|
|
218
235
|
reorderLessonsMutation.mutate({
|
|
@@ -355,6 +372,7 @@ export function CourseTreeDnd() {
|
|
|
355
372
|
expandedBySearch.has(item.id)
|
|
356
373
|
}
|
|
357
374
|
lessonCountMap={lessonCountMap}
|
|
375
|
+
sessionIndicatorMap={sessionIndicatorMap}
|
|
358
376
|
visibleItems={items}
|
|
359
377
|
dragDisabled={dragDisabled}
|
|
360
378
|
/>
|
|
@@ -7,6 +7,7 @@ import { useTranslations } from 'next-intl';
|
|
|
7
7
|
import { forwardRef, useMemo } from 'react';
|
|
8
8
|
import { useCreateSessionMutation } from '../_data/use-course-structure-mutations';
|
|
9
9
|
import { CourseTreeDnd } from './course-tree-dnd';
|
|
10
|
+
import { IconActionTooltip } from './icon-action-tooltip';
|
|
10
11
|
import { MultiSelectBar } from './multi-select-bar';
|
|
11
12
|
import { SearchFilter, type SearchFilterHandle } from './search-filter';
|
|
12
13
|
import { useStructureStore } from './store';
|
|
@@ -50,44 +51,54 @@ export const CourseTreePanel = forwardRef<SearchFilterHandle>((_, ref) => {
|
|
|
50
51
|
<SearchFilter ref={ref} resultCount={resultCount} />
|
|
51
52
|
</div>
|
|
52
53
|
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
size="icon"
|
|
56
|
-
className={cn(
|
|
57
|
-
'size-8 shrink-0 transition-colors',
|
|
58
|
-
isFiltering && 'opacity-40 cursor-not-allowed pointer-events-none'
|
|
59
|
-
)}
|
|
60
|
-
title={
|
|
54
|
+
<IconActionTooltip
|
|
55
|
+
label={
|
|
61
56
|
allExpanded
|
|
62
57
|
? t('tree.collapseAllShortcut')
|
|
63
58
|
: t('tree.expandAllShortcut')
|
|
64
59
|
}
|
|
65
|
-
|
|
66
|
-
onClick={allExpanded ? collapseAll : expandAll}
|
|
67
|
-
disabled={isFiltering}
|
|
60
|
+
asWrapper={isFiltering}
|
|
68
61
|
>
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
62
|
+
<Button
|
|
63
|
+
variant="ghost"
|
|
64
|
+
size="icon"
|
|
65
|
+
className={cn(
|
|
66
|
+
'size-8 shrink-0 transition-colors',
|
|
67
|
+
isFiltering && 'opacity-40 cursor-not-allowed pointer-events-none'
|
|
68
|
+
)}
|
|
69
|
+
aria-label={
|
|
70
|
+
allExpanded ? t('tree.collapseAll') : t('tree.expandAll')
|
|
71
|
+
}
|
|
72
|
+
onClick={allExpanded ? collapseAll : expandAll}
|
|
73
|
+
disabled={isFiltering}
|
|
74
|
+
>
|
|
75
|
+
{allExpanded ? (
|
|
76
|
+
<ChevronsDownUp className="size-3.5" />
|
|
77
|
+
) : (
|
|
78
|
+
<ChevronsUpDown className="size-3.5" />
|
|
79
|
+
)}
|
|
80
|
+
</Button>
|
|
81
|
+
</IconActionTooltip>
|
|
75
82
|
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
className="size-8 shrink-0"
|
|
80
|
-
title={t('tree.addSession')}
|
|
81
|
-
aria-label={t('tree.addSession')}
|
|
82
|
-
disabled={createSession.isPending}
|
|
83
|
-
onClick={() => createSession.mutate()}
|
|
83
|
+
<IconActionTooltip
|
|
84
|
+
label={t('tree.addSession')}
|
|
85
|
+
asWrapper={createSession.isPending}
|
|
84
86
|
>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
<Button
|
|
88
|
+
variant="ghost"
|
|
89
|
+
size="icon"
|
|
90
|
+
className="size-8 shrink-0"
|
|
91
|
+
aria-label={t('tree.addSession')}
|
|
92
|
+
disabled={createSession.isPending}
|
|
93
|
+
onClick={() => createSession.mutate()}
|
|
94
|
+
>
|
|
95
|
+
{createSession.isPending ? (
|
|
96
|
+
<Loader2 className="size-3.5 animate-spin" />
|
|
97
|
+
) : (
|
|
98
|
+
<Plus className="size-3.5" />
|
|
99
|
+
)}
|
|
100
|
+
</Button>
|
|
101
|
+
</IconActionTooltip>
|
|
91
102
|
</div>
|
|
92
103
|
|
|
93
104
|
{/* Search results hint */}
|
|
@@ -5,7 +5,11 @@ import { SearchX } from 'lucide-react';
|
|
|
5
5
|
import { useCallback, useMemo, useRef } from 'react';
|
|
6
6
|
|
|
7
7
|
import { useStructureStore } from './store';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
buildLessonCountMap,
|
|
10
|
+
buildSessionIndicatorMap,
|
|
11
|
+
buildVisibleItems,
|
|
12
|
+
} from './tree-helpers';
|
|
9
13
|
import { TreeRow } from './tree-row';
|
|
10
14
|
import type { FlatItem } from './types';
|
|
11
15
|
|
|
@@ -36,6 +40,10 @@ export function CourseTree() {
|
|
|
36
40
|
);
|
|
37
41
|
|
|
38
42
|
const lessonCountMap = useMemo(() => buildLessonCountMap(lessons), [lessons]);
|
|
43
|
+
const sessionIndicatorMap = useMemo(
|
|
44
|
+
() => buildSessionIndicatorMap(lessons),
|
|
45
|
+
[lessons]
|
|
46
|
+
);
|
|
39
47
|
|
|
40
48
|
const estimateSize = useCallback(
|
|
41
49
|
(index: number) => ROW_HEIGHT[items[index]?.type ?? 'lesson'] ?? 32,
|
|
@@ -121,6 +129,7 @@ export function CourseTree() {
|
|
|
121
129
|
expandedIds.has(item.id) || expandedBySearch.has(item.id)
|
|
122
130
|
}
|
|
123
131
|
lessonCountMap={lessonCountMap}
|
|
132
|
+
sessionIndicatorMap={sessionIndicatorMap}
|
|
124
133
|
visibleItems={items}
|
|
125
134
|
/>
|
|
126
135
|
</div>
|
|
@@ -14,6 +14,9 @@ import {
|
|
|
14
14
|
Video,
|
|
15
15
|
type LucideIcon,
|
|
16
16
|
} from 'lucide-react';
|
|
17
|
+
import { useEffect, useState } from 'react';
|
|
18
|
+
import { useLmsSettingsQuery } from '../_data/use-lms-settings-query';
|
|
19
|
+
import { useTranscriptionSegmentsQuery } from '../_data/use-transcription-segments';
|
|
17
20
|
import { useStructureStore } from './store';
|
|
18
21
|
import type { LessonType } from './types';
|
|
19
22
|
|
|
@@ -63,6 +66,19 @@ export function DetailLesson({ lessonId }: DetailLessonProps) {
|
|
|
63
66
|
const lesson = useStructureStore((s) =>
|
|
64
67
|
s.lessons.find((l) => l.id === lessonId)
|
|
65
68
|
);
|
|
69
|
+
const [activeTab, setActiveTab] = useState('dados');
|
|
70
|
+
const lmsSettings = useLmsSettingsQuery();
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!lmsSettings.transcriptionEnabled && activeTab === 'transcription') {
|
|
74
|
+
setActiveTab('dados');
|
|
75
|
+
}
|
|
76
|
+
}, [lmsSettings.transcriptionEnabled, activeTab]);
|
|
77
|
+
|
|
78
|
+
const { data: segments = [], isLoading: segmentsLoading } =
|
|
79
|
+
useTranscriptionSegmentsQuery(
|
|
80
|
+
activeTab === 'transcription' ? (lesson?.id ?? null) : null
|
|
81
|
+
);
|
|
66
82
|
|
|
67
83
|
if (!lesson) return null;
|
|
68
84
|
|
|
@@ -97,10 +113,16 @@ export function DetailLesson({ lessonId }: DetailLessonProps) {
|
|
|
97
113
|
</div>
|
|
98
114
|
|
|
99
115
|
{/* Tabs */}
|
|
100
|
-
<Tabs
|
|
116
|
+
<Tabs
|
|
117
|
+
value={activeTab}
|
|
118
|
+
onValueChange={setActiveTab}
|
|
119
|
+
className="flex flex-col flex-1 min-h-0"
|
|
120
|
+
>
|
|
101
121
|
<TabsList className="mx-4 mt-3 w-auto justify-start shrink-0">
|
|
102
122
|
<TabsTrigger value="dados">Dados</TabsTrigger>
|
|
103
|
-
|
|
123
|
+
{lmsSettings.transcriptionEnabled && (
|
|
124
|
+
<TabsTrigger value="transcription">Transcrição</TabsTrigger>
|
|
125
|
+
)}
|
|
104
126
|
<TabsTrigger value="resources">Recursos</TabsTrigger>
|
|
105
127
|
</TabsList>
|
|
106
128
|
|
|
@@ -210,15 +232,25 @@ export function DetailLesson({ lessonId }: DetailLessonProps) {
|
|
|
210
232
|
</TabsContent>
|
|
211
233
|
|
|
212
234
|
{/* ── Transcrição ───────────────────────────────────────────────────── */}
|
|
213
|
-
<TabsContent
|
|
235
|
+
{lmsSettings.transcriptionEnabled && <TabsContent
|
|
214
236
|
value="transcription"
|
|
215
237
|
className="flex-1 overflow-y-auto px-4 pb-4 mt-3"
|
|
216
238
|
>
|
|
217
|
-
{
|
|
218
|
-
<div className="
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
239
|
+
{segmentsLoading ? (
|
|
240
|
+
<div className="p-4 text-sm text-muted-foreground">
|
|
241
|
+
Carregando transcrição...
|
|
242
|
+
</div>
|
|
243
|
+
) : segments.length ? (
|
|
244
|
+
<div className="space-y-1.5 p-3">
|
|
245
|
+
{segments.map((seg) => (
|
|
246
|
+
<div key={seg.id} className="flex gap-3 text-sm">
|
|
247
|
+
<span className="shrink-0 font-mono text-xs text-muted-foreground w-24">
|
|
248
|
+
[{formatSeconds(seg.startSeconds)} –{' '}
|
|
249
|
+
{formatSeconds(seg.endSeconds)}]
|
|
250
|
+
</span>
|
|
251
|
+
<span className="leading-relaxed">{seg.text}</span>
|
|
252
|
+
</div>
|
|
253
|
+
))}
|
|
222
254
|
</div>
|
|
223
255
|
) : (
|
|
224
256
|
<EmptyState
|
|
@@ -227,7 +259,7 @@ export function DetailLesson({ lessonId }: DetailLessonProps) {
|
|
|
227
259
|
Transcrição não disponível para esta aula.
|
|
228
260
|
</EmptyState>
|
|
229
261
|
)}
|
|
230
|
-
</TabsContent>
|
|
262
|
+
</TabsContent>}
|
|
231
263
|
|
|
232
264
|
{/* ── Recursos ──────────────────────────────────────────────────────── */}
|
|
233
265
|
<TabsContent
|
|
@@ -312,3 +344,13 @@ function EmptyState({
|
|
|
312
344
|
</div>
|
|
313
345
|
);
|
|
314
346
|
}
|
|
347
|
+
|
|
348
|
+
function formatSeconds(s: number): string {
|
|
349
|
+
const h = Math.floor(s / 3600);
|
|
350
|
+
const m = Math.floor((s % 3600) / 60);
|
|
351
|
+
const sec = Math.floor(s % 60);
|
|
352
|
+
|
|
353
|
+
return h > 0
|
|
354
|
+
? `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`
|
|
355
|
+
: `${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}`;
|
|
356
|
+
}
|
|
@@ -5,6 +5,7 @@ import type { DraggableAttributes } from '@dnd-kit/core';
|
|
|
5
5
|
import type { SyntheticListenerMap } from '@dnd-kit/core/dist/hooks/utilities';
|
|
6
6
|
import { GripVertical } from 'lucide-react';
|
|
7
7
|
import { useTranslations } from 'next-intl';
|
|
8
|
+
import { IconActionTooltip } from './icon-action-tooltip';
|
|
8
9
|
|
|
9
10
|
interface DragHandleProps {
|
|
10
11
|
listeners?: SyntheticListenerMap;
|
|
@@ -27,34 +28,36 @@ export function DragHandle({
|
|
|
27
28
|
const t = useTranslations('lms.CoursesPage.StructurePage.dragHandle');
|
|
28
29
|
if (disabled) {
|
|
29
30
|
return (
|
|
31
|
+
<IconActionTooltip label={t('disabled')}>
|
|
32
|
+
<span
|
|
33
|
+
className={cn(
|
|
34
|
+
'shrink-0 size-5 flex items-center justify-center opacity-20 cursor-not-allowed',
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
<GripVertical className="size-3.5" />
|
|
39
|
+
</span>
|
|
40
|
+
</IconActionTooltip>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<IconActionTooltip label={t('enabled')}>
|
|
30
46
|
<span
|
|
47
|
+
{...listeners}
|
|
48
|
+
{...attributes}
|
|
31
49
|
className={cn(
|
|
32
|
-
'shrink-0 size-5 flex items-center justify-center
|
|
50
|
+
'shrink-0 size-5 flex items-center justify-center rounded',
|
|
51
|
+
'text-muted-foreground/40 hover:text-muted-foreground',
|
|
52
|
+
'cursor-grab active:cursor-grabbing',
|
|
53
|
+
'transition-colors touch-none',
|
|
33
54
|
className
|
|
34
55
|
)}
|
|
35
|
-
|
|
56
|
+
// Prevent click events from bubbling to the row selection handler
|
|
57
|
+
onClick={(e) => e.stopPropagation()}
|
|
36
58
|
>
|
|
37
59
|
<GripVertical className="size-3.5" />
|
|
38
60
|
</span>
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return (
|
|
43
|
-
<span
|
|
44
|
-
{...listeners}
|
|
45
|
-
{...attributes}
|
|
46
|
-
className={cn(
|
|
47
|
-
'shrink-0 size-5 flex items-center justify-center rounded',
|
|
48
|
-
'text-muted-foreground/40 hover:text-muted-foreground',
|
|
49
|
-
'cursor-grab active:cursor-grabbing',
|
|
50
|
-
'transition-colors touch-none',
|
|
51
|
-
className
|
|
52
|
-
)}
|
|
53
|
-
title={t('enabled')}
|
|
54
|
-
// Prevent click events from bubbling to the row selection handler
|
|
55
|
-
onClick={(e) => e.stopPropagation()}
|
|
56
|
-
>
|
|
57
|
-
<GripVertical className="size-3.5" />
|
|
58
|
-
</span>
|
|
61
|
+
</IconActionTooltip>
|
|
59
62
|
);
|
|
60
63
|
}
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import { Separator } from '@/components/ui/separator';
|
|
29
29
|
import { cn } from '@/lib/utils';
|
|
30
30
|
|
|
31
|
+
import { IconActionTooltip } from './icon-action-tooltip';
|
|
31
32
|
import { useStructureStore } from './store';
|
|
32
33
|
import type { LessonStatus, Visibility } from './types';
|
|
33
34
|
|
|
@@ -102,15 +103,17 @@ export function EditorBulk() {
|
|
|
102
103
|
{t('types.selected')}
|
|
103
104
|
</p>
|
|
104
105
|
</div>
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
106
|
+
<IconActionTooltip label={t('clearSelection')}>
|
|
107
|
+
<Button
|
|
108
|
+
variant="ghost"
|
|
109
|
+
size="icon"
|
|
110
|
+
className="size-7 shrink-0"
|
|
111
|
+
onClick={clearSelection}
|
|
112
|
+
aria-label={t('clearSelection')}
|
|
113
|
+
>
|
|
114
|
+
<X className="size-3.5" />
|
|
115
|
+
</Button>
|
|
116
|
+
</IconActionTooltip>
|
|
114
117
|
</div>
|
|
115
118
|
|
|
116
119
|
{/* ── Body ─────────────────────────────────────────────────────────── */}
|