@hed-hog/lms 0.0.314 → 0.0.316
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/enterprise/dto/enterprise-profile.dto.d.ts +13 -0
- package/dist/enterprise/dto/enterprise-profile.dto.d.ts.map +1 -0
- package/dist/enterprise/dto/enterprise-profile.dto.js +3 -0
- package/dist/enterprise/dto/enterprise-profile.dto.js.map +1 -0
- package/dist/enterprise/enterprise.controller.d.ts +3 -0
- package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
- package/dist/enterprise/enterprise.controller.js +14 -0
- package/dist/enterprise/enterprise.controller.js.map +1 -1
- package/dist/enterprise/enterprise.service.d.ts +3 -0
- package/dist/enterprise/enterprise.service.d.ts.map +1 -1
- package/dist/enterprise/enterprise.service.js +128 -1
- package/dist/enterprise/enterprise.service.js.map +1 -1
- package/dist/instructor/instructor.controller.d.ts +23 -0
- package/dist/instructor/instructor.controller.d.ts.map +1 -1
- package/dist/instructor/instructor.controller.js +41 -0
- package/dist/instructor/instructor.controller.js.map +1 -1
- package/dist/instructor/instructor.service.d.ts +25 -0
- package/dist/instructor/instructor.service.d.ts.map +1 -1
- package/dist/instructor/instructor.service.js +126 -8
- package/dist/instructor/instructor.service.js.map +1 -1
- package/hedhog/data/menu.yaml +23 -7
- package/hedhog/data/role.yaml +17 -1
- package/hedhog/data/route.yaml +48 -0
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +44 -44
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -362
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -111
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -134
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -113
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -314
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -62
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +173 -173
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -58
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +51 -51
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -276
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -1216
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1824 -1824
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -443
- package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +40 -40
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -185
- package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -264
- package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +95 -95
- package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +73 -73
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -136
- package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -80
- package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +949 -949
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -525
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +181 -181
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +51 -51
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -271
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -167
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -108
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -318
- package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -10
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +2 -1
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +438 -0
- package/hedhog/frontend/app/instructors/_components/instructor-types.ts.ejs +40 -0
- package/hedhog/frontend/app/instructors/page.tsx.ejs +696 -0
- package/hedhog/frontend/app/training/page.tsx.ejs +2339 -0
- package/hedhog/query/add_route_role.sql +15 -0
- package/hedhog/table/enterprise_user.yaml +1 -1
- package/package.json +6 -6
- package/src/enterprise/dto/enterprise-profile.dto.ts +17 -0
- package/src/enterprise/enterprise.controller.ts +9 -1
- package/src/enterprise/enterprise.service.ts +147 -4
- package/src/instructor/instructor.controller.ts +36 -9
- package/src/instructor/instructor.service.ts +140 -10
|
@@ -1,264 +1,264 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Button } from '@/components/ui/button';
|
|
4
|
-
import { Separator } from '@/components/ui/separator';
|
|
5
|
-
import { cn } from '@/lib/utils';
|
|
6
|
-
import { Copy, FolderOpen, Loader2, Trash2, X } from 'lucide-react';
|
|
7
|
-
import { toast } from 'sonner';
|
|
8
|
-
import {
|
|
9
|
-
useBulkDeleteMutation,
|
|
10
|
-
useDuplicateLessonMutation,
|
|
11
|
-
useDuplicateSessionMutation,
|
|
12
|
-
useMoveLessonsMutation,
|
|
13
|
-
} from '../_data/use-course-structure-mutations';
|
|
14
|
-
import { useStructureStore } from './store';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Compact action bar shown when 2+ items are selected.
|
|
18
|
-
* Fixed below the search toolbar inside CourseTreePanel.
|
|
19
|
-
*/
|
|
20
|
-
export function MultiSelectBar() {
|
|
21
|
-
const selectedIds = useStructureStore((s) => s.selectedIds);
|
|
22
|
-
const lessons = useStructureStore((s) => s.lessons);
|
|
23
|
-
const sessions = useStructureStore((s) => s.sessions);
|
|
24
|
-
const clearSelection = useStructureStore((s) => s.clearSelection);
|
|
25
|
-
const copyItems = useStructureStore((s) => s.copyItems);
|
|
26
|
-
const showConfirm = useStructureStore((s) => s.showConfirm);
|
|
27
|
-
const showSessionPicker = useStructureStore((s) => s.showSessionPicker);
|
|
28
|
-
|
|
29
|
-
const bulkDelete = useBulkDeleteMutation();
|
|
30
|
-
const duplicateLessonMutation = useDuplicateLessonMutation();
|
|
31
|
-
const duplicateSessionMutation = useDuplicateSessionMutation();
|
|
32
|
-
const moveLessonsMutation = useMoveLessonsMutation();
|
|
33
|
-
|
|
34
|
-
const isDuplicating =
|
|
35
|
-
duplicateLessonMutation.isPending || duplicateSessionMutation.isPending;
|
|
36
|
-
const isMoving = moveLessonsMutation.isPending;
|
|
37
|
-
|
|
38
|
-
const count = selectedIds.size;
|
|
39
|
-
if (count <= 1) return null;
|
|
40
|
-
|
|
41
|
-
const selectedLessonIds = lessons
|
|
42
|
-
.filter((l) => selectedIds.has(`lesson:${l.id}`))
|
|
43
|
-
.map((l) => l.id);
|
|
44
|
-
const selectedSessionIds = sessions
|
|
45
|
-
.filter((ss) => selectedIds.has(`session:${ss.id}`))
|
|
46
|
-
.map((ss) => ss.id);
|
|
47
|
-
|
|
48
|
-
const allAreLessons = selectedLessonIds.length === count;
|
|
49
|
-
const allAreSessions = selectedSessionIds.length === count;
|
|
50
|
-
|
|
51
|
-
const label = allAreLessons
|
|
52
|
-
? `${count} aulas`
|
|
53
|
-
: allAreSessions
|
|
54
|
-
? `${count} sessões`
|
|
55
|
-
: `${count} itens`;
|
|
56
|
-
|
|
57
|
-
const canMove = allAreLessons && sessions.length > 1;
|
|
58
|
-
|
|
59
|
-
function handleCopy() {
|
|
60
|
-
if (allAreLessons) {
|
|
61
|
-
copyItems(selectedLessonIds, 'lesson');
|
|
62
|
-
toast.success(`${count} aulas copiadas`);
|
|
63
|
-
} else if (allAreSessions) {
|
|
64
|
-
copyItems(selectedSessionIds, 'session');
|
|
65
|
-
toast.success(`${count} sessões copiadas`);
|
|
66
|
-
} else {
|
|
67
|
-
toast('Selecione apenas aulas ou apenas sessões para copiar.');
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function handleDuplicate() {
|
|
72
|
-
if (allAreLessons) {
|
|
73
|
-
// Duplicate each lesson sequentially
|
|
74
|
-
const duplicateNext = (index: number) => {
|
|
75
|
-
if (index >= selectedLessonIds.length) return;
|
|
76
|
-
const lessonId = selectedLessonIds[index]!;
|
|
77
|
-
const lesson = lessons.find((l) => l.id === lessonId);
|
|
78
|
-
if (!lesson) return;
|
|
79
|
-
duplicateLessonMutation.mutate(
|
|
80
|
-
{ sessionId: lesson.sessionId, lessonId },
|
|
81
|
-
{ onSettled: () => duplicateNext(index + 1) }
|
|
82
|
-
);
|
|
83
|
-
};
|
|
84
|
-
duplicateNext(0);
|
|
85
|
-
toast.success(`${count} aulas sendo duplicadas...`);
|
|
86
|
-
} else if (allAreSessions) {
|
|
87
|
-
const duplicateNext = (index: number) => {
|
|
88
|
-
if (index >= selectedSessionIds.length) return;
|
|
89
|
-
duplicateSessionMutation.mutate(
|
|
90
|
-
{ sessionId: selectedSessionIds[index]! },
|
|
91
|
-
{ onSettled: () => duplicateNext(index + 1) }
|
|
92
|
-
);
|
|
93
|
-
};
|
|
94
|
-
duplicateNext(0);
|
|
95
|
-
toast.success(`${count} sessões sendo duplicadas...`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function handleMove() {
|
|
100
|
-
if (!allAreLessons) return;
|
|
101
|
-
const parentSessionIds = new Set(
|
|
102
|
-
lessons
|
|
103
|
-
.filter((l) => selectedLessonIds.includes(l.id))
|
|
104
|
-
.map((l) => l.sessionId)
|
|
105
|
-
);
|
|
106
|
-
const excludeSessionId =
|
|
107
|
-
parentSessionIds.size === 1 ? ([...parentSessionIds][0] ?? null) : null;
|
|
108
|
-
|
|
109
|
-
showSessionPicker({
|
|
110
|
-
title: `Mover ${count} aulas para`,
|
|
111
|
-
excludeSessionId,
|
|
112
|
-
onPick: (targetId) => {
|
|
113
|
-
const previousLessons = [...lessons];
|
|
114
|
-
const movingLessons = selectedLessonIds
|
|
115
|
-
.map((id) => lessons.find((l) => l.id === id))
|
|
116
|
-
.filter(Boolean) as import('./types').Lesson[];
|
|
117
|
-
const moves = movingLessons.map((l, i) => ({
|
|
118
|
-
lessonId: l.id,
|
|
119
|
-
fromSessionId: l.sessionId,
|
|
120
|
-
toSessionId: targetId,
|
|
121
|
-
toIndex: i,
|
|
122
|
-
}));
|
|
123
|
-
// Optimistic
|
|
124
|
-
useStructureStore.getState().moveLessons(selectedLessonIds, targetId);
|
|
125
|
-
moveLessonsMutation.mutate({ moves, previousLessons });
|
|
126
|
-
toast.success(`${count} aulas movidas`);
|
|
127
|
-
},
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function handleDelete() {
|
|
132
|
-
// Compute the split: sessions + orphan lessons (lessons whose session is NOT deleted)
|
|
133
|
-
const sessionIdSet = new Set(selectedSessionIds);
|
|
134
|
-
const orphanLessons = lessons
|
|
135
|
-
.filter(
|
|
136
|
-
(l) =>
|
|
137
|
-
selectedIds.has(`lesson:${l.id}`) && !sessionIdSet.has(l.sessionId)
|
|
138
|
-
)
|
|
139
|
-
.map((l) => ({ lessonId: l.id, sessionId: l.sessionId }));
|
|
140
|
-
|
|
141
|
-
showConfirm({
|
|
142
|
-
title: `Excluir ${label}?`,
|
|
143
|
-
description: allAreSessions
|
|
144
|
-
? 'As sessões e todas as suas aulas serão excluídas permanentemente.'
|
|
145
|
-
: 'Esta ação não pode ser desfeita.',
|
|
146
|
-
onConfirm: () =>
|
|
147
|
-
bulkDelete.mutate({
|
|
148
|
-
sessionIds: selectedSessionIds,
|
|
149
|
-
lessons: orphanLessons,
|
|
150
|
-
}),
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return (
|
|
155
|
-
<div
|
|
156
|
-
role="toolbar"
|
|
157
|
-
aria-label={`${count} itens selecionados — ações`}
|
|
158
|
-
className={cn(
|
|
159
|
-
'flex items-center gap-0.5 px-2 py-1 shrink-0 border-b',
|
|
160
|
-
'bg-primary/5 dark:bg-primary/10'
|
|
161
|
-
)}
|
|
162
|
-
>
|
|
163
|
-
{/* Count chip */}
|
|
164
|
-
<span
|
|
165
|
-
className={cn(
|
|
166
|
-
'text-[0.65rem] font-semibold px-1.5 py-0.5 rounded-md mr-1 shrink-0',
|
|
167
|
-
'bg-primary/15 text-primary leading-none'
|
|
168
|
-
)}
|
|
169
|
-
>
|
|
170
|
-
{label}
|
|
171
|
-
</span>
|
|
172
|
-
|
|
173
|
-
<div className="flex-1" />
|
|
174
|
-
|
|
175
|
-
{/* Copy */}
|
|
176
|
-
<Button
|
|
177
|
-
variant="ghost"
|
|
178
|
-
size="icon"
|
|
179
|
-
className="size-6 text-muted-foreground hover:text-foreground"
|
|
180
|
-
title="Copiar selecionados (Ctrl+C)"
|
|
181
|
-
aria-label="Copiar selecionados"
|
|
182
|
-
onClick={handleCopy}
|
|
183
|
-
>
|
|
184
|
-
<Copy className="size-3" />
|
|
185
|
-
</Button>
|
|
186
|
-
|
|
187
|
-
{/* Duplicate */}
|
|
188
|
-
<Button
|
|
189
|
-
variant="ghost"
|
|
190
|
-
size="icon"
|
|
191
|
-
className="size-6 text-muted-foreground hover:text-foreground"
|
|
192
|
-
title="Duplicar selecionados (Ctrl+D)"
|
|
193
|
-
aria-label="Duplicar selecionados"
|
|
194
|
-
disabled={isDuplicating}
|
|
195
|
-
onClick={handleDuplicate}
|
|
196
|
-
>
|
|
197
|
-
{isDuplicating ? (
|
|
198
|
-
<Loader2 className="size-3 animate-spin" />
|
|
199
|
-
) : (
|
|
200
|
-
<svg
|
|
201
|
-
viewBox="0 0 24 24"
|
|
202
|
-
className="size-3 fill-none stroke-current stroke-2"
|
|
203
|
-
strokeLinecap="round"
|
|
204
|
-
strokeLinejoin="round"
|
|
205
|
-
aria-hidden
|
|
206
|
-
>
|
|
207
|
-
<rect x="9" y="9" width="13" height="13" rx="2" />
|
|
208
|
-
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
209
|
-
</svg>
|
|
210
|
-
)}
|
|
211
|
-
</Button>
|
|
212
|
-
|
|
213
|
-
{/* Move (lessons only) */}
|
|
214
|
-
{canMove && (
|
|
215
|
-
<Button
|
|
216
|
-
variant="ghost"
|
|
217
|
-
size="icon"
|
|
218
|
-
className="size-6 text-muted-foreground hover:text-foreground"
|
|
219
|
-
title="Mover para outra sessão"
|
|
220
|
-
aria-label="Mover para outra sessão"
|
|
221
|
-
disabled={isMoving}
|
|
222
|
-
onClick={handleMove}
|
|
223
|
-
>
|
|
224
|
-
{isMoving ? (
|
|
225
|
-
<Loader2 className="size-3 animate-spin" />
|
|
226
|
-
) : (
|
|
227
|
-
<FolderOpen className="size-3" />
|
|
228
|
-
)}
|
|
229
|
-
</Button>
|
|
230
|
-
)}
|
|
231
|
-
|
|
232
|
-
{/* Delete */}
|
|
233
|
-
<Button
|
|
234
|
-
variant="ghost"
|
|
235
|
-
size="icon"
|
|
236
|
-
className="size-6 text-destructive/60 hover:text-destructive"
|
|
237
|
-
title="Excluir selecionados (Delete)"
|
|
238
|
-
aria-label="Excluir selecionados"
|
|
239
|
-
disabled={bulkDelete.isPending}
|
|
240
|
-
onClick={handleDelete}
|
|
241
|
-
>
|
|
242
|
-
{bulkDelete.isPending ? (
|
|
243
|
-
<Loader2 className="size-3 animate-spin" />
|
|
244
|
-
) : (
|
|
245
|
-
<Trash2 className="size-3" />
|
|
246
|
-
)}
|
|
247
|
-
</Button>
|
|
248
|
-
|
|
249
|
-
<Separator orientation="vertical" className="mx-0.5 h-3.5" />
|
|
250
|
-
|
|
251
|
-
{/* Deselect */}
|
|
252
|
-
<Button
|
|
253
|
-
variant="ghost"
|
|
254
|
-
size="icon"
|
|
255
|
-
className="size-6 text-muted-foreground hover:text-foreground"
|
|
256
|
-
title="Limpar seleção (Escape)"
|
|
257
|
-
aria-label="Limpar seleção"
|
|
258
|
-
onClick={clearSelection}
|
|
259
|
-
>
|
|
260
|
-
<X className="size-3" />
|
|
261
|
-
</Button>
|
|
262
|
-
</div>
|
|
263
|
-
);
|
|
264
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { Separator } from '@/components/ui/separator';
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
import { Copy, FolderOpen, Loader2, Trash2, X } from 'lucide-react';
|
|
7
|
+
import { toast } from 'sonner';
|
|
8
|
+
import {
|
|
9
|
+
useBulkDeleteMutation,
|
|
10
|
+
useDuplicateLessonMutation,
|
|
11
|
+
useDuplicateSessionMutation,
|
|
12
|
+
useMoveLessonsMutation,
|
|
13
|
+
} from '../_data/use-course-structure-mutations';
|
|
14
|
+
import { useStructureStore } from './store';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Compact action bar shown when 2+ items are selected.
|
|
18
|
+
* Fixed below the search toolbar inside CourseTreePanel.
|
|
19
|
+
*/
|
|
20
|
+
export function MultiSelectBar() {
|
|
21
|
+
const selectedIds = useStructureStore((s) => s.selectedIds);
|
|
22
|
+
const lessons = useStructureStore((s) => s.lessons);
|
|
23
|
+
const sessions = useStructureStore((s) => s.sessions);
|
|
24
|
+
const clearSelection = useStructureStore((s) => s.clearSelection);
|
|
25
|
+
const copyItems = useStructureStore((s) => s.copyItems);
|
|
26
|
+
const showConfirm = useStructureStore((s) => s.showConfirm);
|
|
27
|
+
const showSessionPicker = useStructureStore((s) => s.showSessionPicker);
|
|
28
|
+
|
|
29
|
+
const bulkDelete = useBulkDeleteMutation();
|
|
30
|
+
const duplicateLessonMutation = useDuplicateLessonMutation();
|
|
31
|
+
const duplicateSessionMutation = useDuplicateSessionMutation();
|
|
32
|
+
const moveLessonsMutation = useMoveLessonsMutation();
|
|
33
|
+
|
|
34
|
+
const isDuplicating =
|
|
35
|
+
duplicateLessonMutation.isPending || duplicateSessionMutation.isPending;
|
|
36
|
+
const isMoving = moveLessonsMutation.isPending;
|
|
37
|
+
|
|
38
|
+
const count = selectedIds.size;
|
|
39
|
+
if (count <= 1) return null;
|
|
40
|
+
|
|
41
|
+
const selectedLessonIds = lessons
|
|
42
|
+
.filter((l) => selectedIds.has(`lesson:${l.id}`))
|
|
43
|
+
.map((l) => l.id);
|
|
44
|
+
const selectedSessionIds = sessions
|
|
45
|
+
.filter((ss) => selectedIds.has(`session:${ss.id}`))
|
|
46
|
+
.map((ss) => ss.id);
|
|
47
|
+
|
|
48
|
+
const allAreLessons = selectedLessonIds.length === count;
|
|
49
|
+
const allAreSessions = selectedSessionIds.length === count;
|
|
50
|
+
|
|
51
|
+
const label = allAreLessons
|
|
52
|
+
? `${count} aulas`
|
|
53
|
+
: allAreSessions
|
|
54
|
+
? `${count} sessões`
|
|
55
|
+
: `${count} itens`;
|
|
56
|
+
|
|
57
|
+
const canMove = allAreLessons && sessions.length > 1;
|
|
58
|
+
|
|
59
|
+
function handleCopy() {
|
|
60
|
+
if (allAreLessons) {
|
|
61
|
+
copyItems(selectedLessonIds, 'lesson');
|
|
62
|
+
toast.success(`${count} aulas copiadas`);
|
|
63
|
+
} else if (allAreSessions) {
|
|
64
|
+
copyItems(selectedSessionIds, 'session');
|
|
65
|
+
toast.success(`${count} sessões copiadas`);
|
|
66
|
+
} else {
|
|
67
|
+
toast('Selecione apenas aulas ou apenas sessões para copiar.');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function handleDuplicate() {
|
|
72
|
+
if (allAreLessons) {
|
|
73
|
+
// Duplicate each lesson sequentially
|
|
74
|
+
const duplicateNext = (index: number) => {
|
|
75
|
+
if (index >= selectedLessonIds.length) return;
|
|
76
|
+
const lessonId = selectedLessonIds[index]!;
|
|
77
|
+
const lesson = lessons.find((l) => l.id === lessonId);
|
|
78
|
+
if (!lesson) return;
|
|
79
|
+
duplicateLessonMutation.mutate(
|
|
80
|
+
{ sessionId: lesson.sessionId, lessonId },
|
|
81
|
+
{ onSettled: () => duplicateNext(index + 1) }
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
duplicateNext(0);
|
|
85
|
+
toast.success(`${count} aulas sendo duplicadas...`);
|
|
86
|
+
} else if (allAreSessions) {
|
|
87
|
+
const duplicateNext = (index: number) => {
|
|
88
|
+
if (index >= selectedSessionIds.length) return;
|
|
89
|
+
duplicateSessionMutation.mutate(
|
|
90
|
+
{ sessionId: selectedSessionIds[index]! },
|
|
91
|
+
{ onSettled: () => duplicateNext(index + 1) }
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
duplicateNext(0);
|
|
95
|
+
toast.success(`${count} sessões sendo duplicadas...`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function handleMove() {
|
|
100
|
+
if (!allAreLessons) return;
|
|
101
|
+
const parentSessionIds = new Set(
|
|
102
|
+
lessons
|
|
103
|
+
.filter((l) => selectedLessonIds.includes(l.id))
|
|
104
|
+
.map((l) => l.sessionId)
|
|
105
|
+
);
|
|
106
|
+
const excludeSessionId =
|
|
107
|
+
parentSessionIds.size === 1 ? ([...parentSessionIds][0] ?? null) : null;
|
|
108
|
+
|
|
109
|
+
showSessionPicker({
|
|
110
|
+
title: `Mover ${count} aulas para`,
|
|
111
|
+
excludeSessionId,
|
|
112
|
+
onPick: (targetId) => {
|
|
113
|
+
const previousLessons = [...lessons];
|
|
114
|
+
const movingLessons = selectedLessonIds
|
|
115
|
+
.map((id) => lessons.find((l) => l.id === id))
|
|
116
|
+
.filter(Boolean) as import('./types').Lesson[];
|
|
117
|
+
const moves = movingLessons.map((l, i) => ({
|
|
118
|
+
lessonId: l.id,
|
|
119
|
+
fromSessionId: l.sessionId,
|
|
120
|
+
toSessionId: targetId,
|
|
121
|
+
toIndex: i,
|
|
122
|
+
}));
|
|
123
|
+
// Optimistic
|
|
124
|
+
useStructureStore.getState().moveLessons(selectedLessonIds, targetId);
|
|
125
|
+
moveLessonsMutation.mutate({ moves, previousLessons });
|
|
126
|
+
toast.success(`${count} aulas movidas`);
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function handleDelete() {
|
|
132
|
+
// Compute the split: sessions + orphan lessons (lessons whose session is NOT deleted)
|
|
133
|
+
const sessionIdSet = new Set(selectedSessionIds);
|
|
134
|
+
const orphanLessons = lessons
|
|
135
|
+
.filter(
|
|
136
|
+
(l) =>
|
|
137
|
+
selectedIds.has(`lesson:${l.id}`) && !sessionIdSet.has(l.sessionId)
|
|
138
|
+
)
|
|
139
|
+
.map((l) => ({ lessonId: l.id, sessionId: l.sessionId }));
|
|
140
|
+
|
|
141
|
+
showConfirm({
|
|
142
|
+
title: `Excluir ${label}?`,
|
|
143
|
+
description: allAreSessions
|
|
144
|
+
? 'As sessões e todas as suas aulas serão excluídas permanentemente.'
|
|
145
|
+
: 'Esta ação não pode ser desfeita.',
|
|
146
|
+
onConfirm: () =>
|
|
147
|
+
bulkDelete.mutate({
|
|
148
|
+
sessionIds: selectedSessionIds,
|
|
149
|
+
lessons: orphanLessons,
|
|
150
|
+
}),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<div
|
|
156
|
+
role="toolbar"
|
|
157
|
+
aria-label={`${count} itens selecionados — ações`}
|
|
158
|
+
className={cn(
|
|
159
|
+
'flex items-center gap-0.5 px-2 py-1 shrink-0 border-b',
|
|
160
|
+
'bg-primary/5 dark:bg-primary/10'
|
|
161
|
+
)}
|
|
162
|
+
>
|
|
163
|
+
{/* Count chip */}
|
|
164
|
+
<span
|
|
165
|
+
className={cn(
|
|
166
|
+
'text-[0.65rem] font-semibold px-1.5 py-0.5 rounded-md mr-1 shrink-0',
|
|
167
|
+
'bg-primary/15 text-primary leading-none'
|
|
168
|
+
)}
|
|
169
|
+
>
|
|
170
|
+
{label}
|
|
171
|
+
</span>
|
|
172
|
+
|
|
173
|
+
<div className="flex-1" />
|
|
174
|
+
|
|
175
|
+
{/* Copy */}
|
|
176
|
+
<Button
|
|
177
|
+
variant="ghost"
|
|
178
|
+
size="icon"
|
|
179
|
+
className="size-6 text-muted-foreground hover:text-foreground"
|
|
180
|
+
title="Copiar selecionados (Ctrl+C)"
|
|
181
|
+
aria-label="Copiar selecionados"
|
|
182
|
+
onClick={handleCopy}
|
|
183
|
+
>
|
|
184
|
+
<Copy className="size-3" />
|
|
185
|
+
</Button>
|
|
186
|
+
|
|
187
|
+
{/* Duplicate */}
|
|
188
|
+
<Button
|
|
189
|
+
variant="ghost"
|
|
190
|
+
size="icon"
|
|
191
|
+
className="size-6 text-muted-foreground hover:text-foreground"
|
|
192
|
+
title="Duplicar selecionados (Ctrl+D)"
|
|
193
|
+
aria-label="Duplicar selecionados"
|
|
194
|
+
disabled={isDuplicating}
|
|
195
|
+
onClick={handleDuplicate}
|
|
196
|
+
>
|
|
197
|
+
{isDuplicating ? (
|
|
198
|
+
<Loader2 className="size-3 animate-spin" />
|
|
199
|
+
) : (
|
|
200
|
+
<svg
|
|
201
|
+
viewBox="0 0 24 24"
|
|
202
|
+
className="size-3 fill-none stroke-current stroke-2"
|
|
203
|
+
strokeLinecap="round"
|
|
204
|
+
strokeLinejoin="round"
|
|
205
|
+
aria-hidden
|
|
206
|
+
>
|
|
207
|
+
<rect x="9" y="9" width="13" height="13" rx="2" />
|
|
208
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
209
|
+
</svg>
|
|
210
|
+
)}
|
|
211
|
+
</Button>
|
|
212
|
+
|
|
213
|
+
{/* Move (lessons only) */}
|
|
214
|
+
{canMove && (
|
|
215
|
+
<Button
|
|
216
|
+
variant="ghost"
|
|
217
|
+
size="icon"
|
|
218
|
+
className="size-6 text-muted-foreground hover:text-foreground"
|
|
219
|
+
title="Mover para outra sessão"
|
|
220
|
+
aria-label="Mover para outra sessão"
|
|
221
|
+
disabled={isMoving}
|
|
222
|
+
onClick={handleMove}
|
|
223
|
+
>
|
|
224
|
+
{isMoving ? (
|
|
225
|
+
<Loader2 className="size-3 animate-spin" />
|
|
226
|
+
) : (
|
|
227
|
+
<FolderOpen className="size-3" />
|
|
228
|
+
)}
|
|
229
|
+
</Button>
|
|
230
|
+
)}
|
|
231
|
+
|
|
232
|
+
{/* Delete */}
|
|
233
|
+
<Button
|
|
234
|
+
variant="ghost"
|
|
235
|
+
size="icon"
|
|
236
|
+
className="size-6 text-destructive/60 hover:text-destructive"
|
|
237
|
+
title="Excluir selecionados (Delete)"
|
|
238
|
+
aria-label="Excluir selecionados"
|
|
239
|
+
disabled={bulkDelete.isPending}
|
|
240
|
+
onClick={handleDelete}
|
|
241
|
+
>
|
|
242
|
+
{bulkDelete.isPending ? (
|
|
243
|
+
<Loader2 className="size-3 animate-spin" />
|
|
244
|
+
) : (
|
|
245
|
+
<Trash2 className="size-3" />
|
|
246
|
+
)}
|
|
247
|
+
</Button>
|
|
248
|
+
|
|
249
|
+
<Separator orientation="vertical" className="mx-0.5 h-3.5" />
|
|
250
|
+
|
|
251
|
+
{/* Deselect */}
|
|
252
|
+
<Button
|
|
253
|
+
variant="ghost"
|
|
254
|
+
size="icon"
|
|
255
|
+
className="size-6 text-muted-foreground hover:text-foreground"
|
|
256
|
+
title="Limpar seleção (Escape)"
|
|
257
|
+
aria-label="Limpar seleção"
|
|
258
|
+
onClick={clearSelection}
|
|
259
|
+
>
|
|
260
|
+
<X className="size-3" />
|
|
261
|
+
</Button>
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
}
|