@hed-hog/lms 0.0.306 → 0.0.310

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.
Files changed (120) hide show
  1. package/dist/course/course-structure.controller.d.ts +60 -0
  2. package/dist/course/course-structure.controller.d.ts.map +1 -1
  3. package/dist/course/course-structure.controller.js +79 -0
  4. package/dist/course/course-structure.controller.js.map +1 -1
  5. package/dist/course/course-structure.service.d.ts +61 -1
  6. package/dist/course/course-structure.service.d.ts.map +1 -1
  7. package/dist/course/course-structure.service.js +326 -1
  8. package/dist/course/course-structure.service.js.map +1 -1
  9. package/dist/course/course.controller.d.ts +52 -4
  10. package/dist/course/course.controller.d.ts.map +1 -1
  11. package/dist/course/course.service.d.ts +52 -5
  12. package/dist/course/course.service.d.ts.map +1 -1
  13. package/dist/course/course.service.js +78 -57
  14. package/dist/course/course.service.js.map +1 -1
  15. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  16. package/dist/course/dto/create-course-structure-lesson.dto.js +5 -1
  17. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  18. package/dist/course/dto/create-course.dto.d.ts +1 -1
  19. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  20. package/dist/course/dto/create-course.dto.js +4 -1
  21. package/dist/course/dto/create-course.dto.js.map +1 -1
  22. package/dist/course/dto/move-lesson.dto.d.ts +10 -0
  23. package/dist/course/dto/move-lesson.dto.d.ts.map +1 -0
  24. package/dist/course/dto/move-lesson.dto.js +28 -0
  25. package/dist/course/dto/move-lesson.dto.js.map +1 -0
  26. package/dist/course/dto/paste-lessons.dto.d.ts +4 -0
  27. package/dist/course/dto/paste-lessons.dto.d.ts.map +1 -0
  28. package/dist/course/dto/paste-lessons.dto.js +24 -0
  29. package/dist/course/dto/paste-lessons.dto.js.map +1 -0
  30. package/dist/course/dto/reorder-lessons.dto.d.ts +5 -0
  31. package/dist/course/dto/reorder-lessons.dto.d.ts.map +1 -0
  32. package/dist/course/dto/reorder-lessons.dto.js +24 -0
  33. package/dist/course/dto/reorder-lessons.dto.js.map +1 -0
  34. package/dist/course/dto/reorder-sessions.dto.d.ts +5 -0
  35. package/dist/course/dto/reorder-sessions.dto.d.ts.map +1 -0
  36. package/dist/course/dto/reorder-sessions.dto.js +24 -0
  37. package/dist/course/dto/reorder-sessions.dto.js.map +1 -0
  38. package/dist/training/training.controller.js +1 -1
  39. package/dist/training/training.controller.js.map +1 -1
  40. package/hedhog/data/image_type.yaml +20 -0
  41. package/hedhog/data/menu.yaml +2 -2
  42. package/hedhog/data/route.yaml +60 -6
  43. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +146 -165
  44. package/hedhog/frontend/app/_components/course-avatar.tsx.ejs +70 -0
  45. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +372 -22
  46. package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +10 -1
  47. package/hedhog/frontend/app/certificates/models/TopBar.tsx.ejs +44 -3
  48. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +32 -0
  49. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +437 -77
  50. package/hedhog/frontend/app/classes/page.tsx.ejs +311 -289
  51. package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +10 -7
  52. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +23 -32
  53. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +3 -9
  54. package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +26 -16
  55. package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +19 -5
  56. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +10 -14
  57. package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +131 -107
  58. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +10 -7
  59. package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +38 -19
  60. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +1 -1
  61. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
  62. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +336 -1057
  63. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +45 -0
  64. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -0
  65. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -0
  66. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-skeleton.tsx.ejs +64 -0
  67. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
  68. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
  69. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
  70. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -0
  71. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
  72. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -0
  73. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +52 -0
  74. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -0
  75. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -0
  76. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1827 -0
  77. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -0
  78. package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +41 -0
  79. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +184 -0
  80. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -0
  81. package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +96 -0
  82. package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +74 -0
  83. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -0
  84. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -0
  85. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +948 -0
  86. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -0
  87. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +150 -0
  88. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +182 -0
  89. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +52 -0
  90. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -0
  91. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -0
  92. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -0
  93. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +122 -0
  94. package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -0
  95. package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +97 -0
  96. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +347 -0
  97. package/hedhog/frontend/app/courses/[id]/structure/_data/course-structure-contract.ts.ejs +195 -0
  98. package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +420 -0
  99. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +254 -0
  100. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +987 -0
  101. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +86 -0
  102. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure.ts.ejs +160 -0
  103. package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -3212
  104. package/hedhog/frontend/app/courses/page.tsx.ejs +45 -26
  105. package/hedhog/frontend/app/{training → paths}/page.tsx.ejs +29 -7
  106. package/hedhog/frontend/messages/en.json +91 -11
  107. package/hedhog/frontend/messages/pt.json +91 -11
  108. package/hedhog/table/course.yaml +1 -1
  109. package/hedhog/table/image_type.yaml +14 -0
  110. package/package.json +7 -7
  111. package/src/course/course-structure.controller.ts +63 -0
  112. package/src/course/course-structure.service.ts +390 -3
  113. package/src/course/course.service.ts +59 -27
  114. package/src/course/dto/create-course-structure-lesson.dto.ts +3 -2
  115. package/src/course/dto/create-course.dto.ts +4 -1
  116. package/src/course/dto/move-lesson.dto.ts +17 -0
  117. package/src/course/dto/paste-lessons.dto.ts +9 -0
  118. package/src/course/dto/reorder-lessons.dto.ts +10 -0
  119. package/src/course/dto/reorder-sessions.dto.ts +10 -0
  120. package/src/training/training.controller.ts +1 -1
@@ -0,0 +1,987 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Course Structure — React Query Mutations
5
+ *
6
+ * Each hook:
7
+ * - reads `courseId` from the Zustand store (set by page.tsx on mount)
8
+ * - calls the corresponding service function
9
+ * - normalises the API response via the adapter
10
+ * - updates both Zustand store AND React Query cache for consistency
11
+ * - shows sonner toast feedback
12
+ *
13
+ * Cache strategy:
14
+ * - Structural changes (create / delete / duplicate / paste / move):
15
+ * invalidateQueries → background refetch refreshes cache & store
16
+ * - In-place changes (update fields):
17
+ * manual setQueryData (no round-trip)
18
+ * - Optimistic mutations (reorder / move):
19
+ * onMutate: cancelQueries + snapshot + setQueryData
20
+ * onError: restore cache snapshot + Zustand rollback
21
+ */
22
+
23
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
24
+ import { toast } from 'sonner';
25
+
26
+ import { useApp } from '@hed-hog/next-app-provider';
27
+
28
+ import { useStructureStore } from '../_components/store';
29
+ import type {
30
+ LessonFormValues,
31
+ Resource,
32
+ SessionFormValues,
33
+ } from '../_components/types';
34
+ import {
35
+ normalizeLesson,
36
+ normalizeSession,
37
+ toCreateLessonPayload,
38
+ toCreateSessionPayload,
39
+ toUpdateLessonPayload,
40
+ toUpdateSessionPayload,
41
+ } from './adapters/course-structure.adapter';
42
+ import {
43
+ createLesson as apiCreateLesson,
44
+ createSession as apiCreateSession,
45
+ deleteLesson as apiDeleteLesson,
46
+ deleteSession as apiDeleteSession,
47
+ duplicateLesson as apiDuplicateLesson,
48
+ duplicateSession as apiDuplicateSession,
49
+ moveLesson as apiMoveLesson,
50
+ pasteLessons as apiPasteLessons,
51
+ reorderLessons as apiReorderLessons,
52
+ reorderSessions as apiReorderSessions,
53
+ updateCourse as apiUpdateCourse,
54
+ updateLesson as apiUpdateLesson,
55
+ updateSession as apiUpdateSession,
56
+ } from './services/course-structure.service';
57
+ import type { CourseStructureCacheData } from './use-course-structure-query';
58
+ import { courseStructureQueryKey } from './use-course-structure-query';
59
+
60
+ // ─────────────────────────────────────────────────────────────────────────────
61
+ // useUpdateCourseMutation
62
+ // ─────────────────────────────────────────────────────────────────────────────
63
+
64
+ interface UpdateCourseVars {
65
+ formValues: import('../_components/types').CourseFormValues;
66
+ }
67
+
68
+ /**
69
+ * Mutation: PATCH /lms/courses/:id
70
+ *
71
+ * Persists the course metadata form (title, slug, code, description, published).
72
+ * On success: updates the store via `updateCourse` and patches the React Query
73
+ * cache entry so the header immediately reflects the new title/slug.
74
+ */
75
+ export function useUpdateCourseMutation() {
76
+ const { request } = useApp();
77
+ const queryClient = useQueryClient();
78
+ const courseId = useStructureStore((s) => s.courseId);
79
+ const updateCourseInStore = useStructureStore((s) => s.updateCourse);
80
+
81
+ return useMutation({
82
+ mutationFn: ({ formValues }: UpdateCourseVars) =>
83
+ apiUpdateCourse(request, courseId, {
84
+ title: formValues.title,
85
+ slug: formValues.slug.toLowerCase(),
86
+ name: formValues.name,
87
+ description: formValues.description,
88
+ status: formValues.published ? 'published' : 'draft',
89
+ }),
90
+ onSuccess: (_, { formValues }) => {
91
+ updateCourseInStore(formValues);
92
+ // Patch cache so structure query reflects new course metadata.
93
+ queryClient.setQueryData<
94
+ import('./use-course-structure-query').CourseStructureCacheData
95
+ >(courseStructureQueryKey(courseId), (old) => {
96
+ if (!old || !old.course) return old;
97
+ return {
98
+ ...old,
99
+ course: {
100
+ ...old.course,
101
+ title: formValues.title,
102
+ slug: formValues.slug.toLowerCase(),
103
+ name: formValues.name,
104
+ description: formValues.description,
105
+ published: formValues.published,
106
+ },
107
+ };
108
+ });
109
+ toast.success('Curso salvo');
110
+ },
111
+ onError: () => {
112
+ void queryClient.invalidateQueries({
113
+ queryKey: courseStructureQueryKey(courseId),
114
+ });
115
+ toast.error('Erro ao salvar curso');
116
+ },
117
+ });
118
+ }
119
+
120
+ // ─────────────────────────────────────────────────────────────────────────────
121
+ // useCreateSessionMutation
122
+ // ─────────────────────────────────────────────────────────────────────────────
123
+
124
+ /**
125
+ * Mutation: POST /lms/courses/:id/structure/sessions
126
+ *
127
+ * Creates a new session with default values and appends it to the store.
128
+ * The newly created session becomes the active/selected item.
129
+ */
130
+ export function useCreateSessionMutation() {
131
+ const { request } = useApp();
132
+ const queryClient = useQueryClient();
133
+ const courseId = useStructureStore((s) => s.courseId);
134
+ const sessions = useStructureStore((s) => s.sessions);
135
+ const addSessionFromApi = useStructureStore((s) => s.addSessionFromApi);
136
+
137
+ return useMutation({
138
+ mutationFn: () => {
139
+ const nextIndex = sessions.length + 1;
140
+ const payload = toCreateSessionPayload({
141
+ title: `Nova Sessão ${String(nextIndex).padStart(2, '0')}`,
142
+ duration: 0,
143
+ code: `S${String(nextIndex).padStart(2, '0')}`,
144
+ description: '',
145
+ });
146
+ return apiCreateSession(request, courseId, payload);
147
+ },
148
+ onSuccess: (apiSession) => {
149
+ const session = normalizeSession(apiSession, sessions.length);
150
+ addSessionFromApi(session);
151
+ // Invalidate so cache stays in sync; useEffect in page.tsx will re-sync
152
+ // store when isMutating reaches 0 (guarded against mid-flight overwrites).
153
+ void queryClient.invalidateQueries({
154
+ queryKey: courseStructureQueryKey(courseId),
155
+ });
156
+ toast.success('Sessão criada');
157
+ },
158
+ onError: () => {
159
+ void queryClient.invalidateQueries({
160
+ queryKey: courseStructureQueryKey(courseId),
161
+ });
162
+ toast.error('Erro ao criar sessão');
163
+ },
164
+ });
165
+ }
166
+
167
+ // ─────────────────────────────────────────────────────────────────────────────
168
+ // useUpdateSessionMutation
169
+ // ─────────────────────────────────────────────────────────────────────────────
170
+
171
+ interface UpdateSessionVars {
172
+ sessionId: string | number;
173
+ formValues: SessionFormValues;
174
+ }
175
+
176
+ /**
177
+ * Mutation: PATCH /lms/courses/:id/structure/sessions/:sessionId
178
+ *
179
+ * Persists the form values and updates the store on success.
180
+ */
181
+ export function useUpdateSessionMutation() {
182
+ const { request } = useApp();
183
+ const queryClient = useQueryClient();
184
+ const courseId = useStructureStore((s) => s.courseId);
185
+ const updateSessionInStore = useStructureStore((s) => s.updateSession);
186
+
187
+ return useMutation({
188
+ mutationFn: ({ sessionId, formValues }: UpdateSessionVars) =>
189
+ apiUpdateSession(
190
+ request,
191
+ courseId,
192
+ sessionId,
193
+ toUpdateSessionPayload(formValues)
194
+ ),
195
+ onSuccess: (_apiSession, { sessionId, formValues }) => {
196
+ updateSessionInStore(String(sessionId), formValues);
197
+ // Manual cache update avoids a full refetch for an in-place title/desc change.
198
+ queryClient.setQueryData<CourseStructureCacheData>(
199
+ courseStructureQueryKey(courseId),
200
+ (old) => {
201
+ if (!old) return old;
202
+ return {
203
+ ...old,
204
+ sessions: old.sessions.map((s) =>
205
+ s.id === String(sessionId)
206
+ ? {
207
+ ...s,
208
+ title: formValues.title ?? s.title,
209
+ code: formValues.code ?? s.code,
210
+ duration: formValues.duration ?? s.duration,
211
+ }
212
+ : s
213
+ ),
214
+ };
215
+ }
216
+ );
217
+ toast.success('Sessão salva');
218
+ },
219
+ onError: () => {
220
+ // Fall back to server truth
221
+ void queryClient.invalidateQueries({
222
+ queryKey: courseStructureQueryKey(courseId),
223
+ });
224
+ toast.error('Erro ao salvar sessão');
225
+ },
226
+ });
227
+ }
228
+
229
+ // ─────────────────────────────────────────────────────────────────────────────
230
+ // useCreateLessonMutation
231
+ // ─────────────────────────────────────────────────────────────────────────────
232
+
233
+ interface CreateLessonVars {
234
+ sessionId: string | number;
235
+ }
236
+
237
+ /**
238
+ * Mutation: POST /lms/courses/:id/structure/sessions/:sessionId/lessons
239
+ *
240
+ * Creates a new lesson with default values in the given session.
241
+ * The newly created lesson becomes the active/selected item.
242
+ */
243
+ export function useCreateLessonMutation() {
244
+ const { request } = useApp();
245
+ const queryClient = useQueryClient();
246
+ const courseId = useStructureStore((s) => s.courseId);
247
+ const lessons = useStructureStore((s) => s.lessons);
248
+ const addLessonFromApi = useStructureStore((s) => s.addLessonFromApi);
249
+
250
+ return useMutation({
251
+ mutationFn: ({ sessionId }: CreateLessonVars) => {
252
+ const sessionLessons = lessons.filter(
253
+ (l) => l.sessionId === String(sessionId)
254
+ );
255
+ const nextIndex = sessionLessons.length + 1;
256
+ const payload = toCreateLessonPayload({
257
+ title: `Nova Aula ${String(nextIndex).padStart(2, '0')}`,
258
+ type: 'video',
259
+ duration: 0,
260
+ code: `A${String(lessons.length + 1).padStart(2, '0')}`,
261
+ publicDescription: '',
262
+ privateDescription: '',
263
+ status: 'preparada',
264
+ visibility: 'publico',
265
+ });
266
+ return apiCreateLesson(request, courseId, sessionId, payload);
267
+ },
268
+ onSuccess: (apiLesson) => {
269
+ const lesson = normalizeLesson(apiLesson, lessons.length);
270
+ addLessonFromApi(lesson);
271
+ void queryClient.invalidateQueries({
272
+ queryKey: courseStructureQueryKey(courseId),
273
+ });
274
+ toast.success('Aula criada');
275
+ },
276
+ onError: () => {
277
+ void queryClient.invalidateQueries({
278
+ queryKey: courseStructureQueryKey(courseId),
279
+ });
280
+ toast.error('Erro ao criar aula');
281
+ },
282
+ });
283
+ }
284
+
285
+ // ─────────────────────────────────────────────────────────────────────────────
286
+ // useUpdateLessonMutation
287
+ // ─────────────────────────────────────────────────────────────────────────────
288
+
289
+ interface UpdateLessonVars {
290
+ lessonId: string | number;
291
+ sessionId: string | number;
292
+ formValues: Partial<
293
+ LessonFormValues & {
294
+ transcription?: string;
295
+ resources?: Resource[];
296
+ instructorIds?: number[];
297
+ }
298
+ >;
299
+ }
300
+
301
+ /**
302
+ * Mutation: PATCH /lms/courses/:id/structure/sessions/:sessionId/lessons/:lessonId
303
+ *
304
+ * Persists the form values and updates the store on success.
305
+ */
306
+ export function useUpdateLessonMutation() {
307
+ const { request } = useApp();
308
+ const queryClient = useQueryClient();
309
+ const courseId = useStructureStore((s) => s.courseId);
310
+ const updateLessonInStore = useStructureStore((s) => s.updateLesson);
311
+
312
+ return useMutation({
313
+ mutationFn: ({ lessonId, sessionId, formValues }: UpdateLessonVars) =>
314
+ apiUpdateLesson(
315
+ request,
316
+ courseId,
317
+ sessionId,
318
+ lessonId,
319
+ toUpdateLessonPayload(formValues)
320
+ ),
321
+ onSuccess: (_apiLesson, { lessonId, formValues }) => {
322
+ updateLessonInStore(String(lessonId), formValues);
323
+ // Manual cache update for in-place field changes.
324
+ queryClient.setQueryData<CourseStructureCacheData>(
325
+ courseStructureQueryKey(courseId),
326
+ (old) => {
327
+ if (!old) return old;
328
+ return {
329
+ ...old,
330
+ lessons: old.lessons.map((l) =>
331
+ l.id === String(lessonId)
332
+ ? {
333
+ ...l,
334
+ title: formValues.title ?? l.title,
335
+ duration: formValues.duration ?? l.duration,
336
+ type: formValues.type ?? l.type,
337
+ }
338
+ : l
339
+ ),
340
+ };
341
+ }
342
+ );
343
+ toast.success('Aula salva');
344
+ },
345
+ onError: () => {
346
+ void queryClient.invalidateQueries({
347
+ queryKey: courseStructureQueryKey(courseId),
348
+ });
349
+ toast.error('Erro ao salvar aula');
350
+ },
351
+ });
352
+ }
353
+
354
+ // ─────────────────────────────────────────────────────────────────────────────
355
+ // useDeleteSessionMutation
356
+ // ─────────────────────────────────────────────────────────────────────────────
357
+
358
+ interface DeleteSessionVars {
359
+ sessionId: string | number;
360
+ }
361
+
362
+ /**
363
+ * Mutation: DELETE /lms/courses/:id/structure/sessions/:sessionId
364
+ *
365
+ * Deletes the session (and its lessons, cascade on backend) then removes it
366
+ * from the Zustand store. Selection is reset to the course root.
367
+ */
368
+ export function useDeleteSessionMutation() {
369
+ const { request } = useApp();
370
+ const queryClient = useQueryClient();
371
+ const courseId = useStructureStore((s) => s.courseId);
372
+ const deleteSessionInStore = useStructureStore((s) => s.deleteSession);
373
+
374
+ return useMutation({
375
+ mutationFn: ({ sessionId }: DeleteSessionVars) =>
376
+ apiDeleteSession(request, courseId, sessionId),
377
+ onSuccess: (_, { sessionId }) => {
378
+ deleteSessionInStore(String(sessionId));
379
+ // Remove session and its lessons from the cache.
380
+ queryClient.setQueryData<CourseStructureCacheData>(
381
+ courseStructureQueryKey(courseId),
382
+ (old) => {
383
+ if (!old) return old;
384
+ const sid = String(sessionId);
385
+ return {
386
+ ...old,
387
+ sessions: old.sessions.filter((s) => s.id !== sid),
388
+ lessons: old.lessons.filter((l) => l.sessionId !== sid),
389
+ };
390
+ }
391
+ );
392
+ toast.success('Sessão excluída');
393
+ },
394
+ onError: () => {
395
+ void queryClient.invalidateQueries({
396
+ queryKey: courseStructureQueryKey(courseId),
397
+ });
398
+ toast.error('Erro ao excluir sessão');
399
+ },
400
+ });
401
+ }
402
+
403
+ // ─────────────────────────────────────────────────────────────────────────────
404
+ // useDeleteLessonMutation
405
+ // ─────────────────────────────────────────────────────────────────────────────
406
+
407
+ interface DeleteLessonVars {
408
+ lessonId: string | number;
409
+ sessionId: string | number;
410
+ }
411
+
412
+ /**
413
+ * Mutation: DELETE /lms/courses/:id/structure/sessions/:sessionId/lessons/:lessonId
414
+ *
415
+ * Deletes a lesson and removes it from the Zustand store.
416
+ */
417
+ export function useDeleteLessonMutation() {
418
+ const { request } = useApp();
419
+ const queryClient = useQueryClient();
420
+ const courseId = useStructureStore((s) => s.courseId);
421
+ const deleteLessonInStore = useStructureStore((s) => s.deleteLesson);
422
+
423
+ return useMutation({
424
+ mutationFn: ({ lessonId, sessionId }: DeleteLessonVars) =>
425
+ apiDeleteLesson(request, courseId, sessionId, lessonId),
426
+ onSuccess: (_, { lessonId }) => {
427
+ deleteLessonInStore(String(lessonId));
428
+ const lid = String(lessonId);
429
+ queryClient.setQueryData<CourseStructureCacheData>(
430
+ courseStructureQueryKey(courseId),
431
+ (old) =>
432
+ old
433
+ ? { ...old, lessons: old.lessons.filter((l) => l.id !== lid) }
434
+ : old
435
+ );
436
+ toast.success('Aula excluída');
437
+ },
438
+ onError: () => {
439
+ void queryClient.invalidateQueries({
440
+ queryKey: courseStructureQueryKey(courseId),
441
+ });
442
+ toast.error('Erro ao excluir aula');
443
+ },
444
+ });
445
+ }
446
+
447
+ // ─────────────────────────────────────────────────────────────────────────────
448
+ // useBulkDeleteMutation
449
+ // ─────────────────────────────────────────────────────────────────────────────
450
+
451
+ interface BulkDeleteVars {
452
+ /** IDs of sessions to delete (backend cascades their lessons). */
453
+ sessionIds: string[];
454
+ /** Individual lessons to delete (only those whose session is NOT also deleted). */
455
+ lessons: Array<{ lessonId: string; sessionId: string }>;
456
+ }
457
+
458
+ /**
459
+ * Bulk delete: iterate API calls since no batch endpoint exists.
460
+ *
461
+ * Strategy:
462
+ * 1. Delete sessions in parallel (backend cascades their lessons).
463
+ * 2. Delete orphan lessons in parallel (those not in a deleted session).
464
+ * 3. Use Promise.allSettled so one failure doesn't abort the rest.
465
+ * 4. Remove successful items from the store individually.
466
+ * 5. Show partial-error toast when some items fail.
467
+ */
468
+ export function useBulkDeleteMutation() {
469
+ const { request } = useApp();
470
+ const queryClient = useQueryClient();
471
+ const courseId = useStructureStore((s) => s.courseId);
472
+ const deleteSessionInStore = useStructureStore((s) => s.deleteSession);
473
+ const deleteLessonInStore = useStructureStore((s) => s.deleteLesson);
474
+
475
+ return useMutation({
476
+ mutationFn: async ({ sessionIds, lessons }: BulkDeleteVars) => {
477
+ const sessionResults = await Promise.allSettled(
478
+ sessionIds.map((id) => apiDeleteSession(request, courseId, id))
479
+ );
480
+ const lessonResults = await Promise.allSettled(
481
+ lessons.map(({ lessonId, sessionId }) =>
482
+ apiDeleteLesson(request, courseId, sessionId, lessonId)
483
+ )
484
+ );
485
+ return {
486
+ sessionIds,
487
+ lessonIds: lessons.map((l) => l.lessonId),
488
+ sessionResults,
489
+ lessonResults,
490
+ };
491
+ },
492
+ onSuccess: ({ sessionIds, lessonIds, sessionResults, lessonResults }) => {
493
+ const successSessionIds = sessionIds.filter(
494
+ (_, i) => sessionResults[i]?.status === 'fulfilled'
495
+ );
496
+ const successLessonIds = lessonIds.filter(
497
+ (_, i) => lessonResults[i]?.status === 'fulfilled'
498
+ );
499
+ const errorCount = [...sessionResults, ...lessonResults].filter(
500
+ (r) => r.status === 'rejected'
501
+ ).length;
502
+
503
+ successSessionIds.forEach((id) => deleteSessionInStore(id));
504
+ successLessonIds.forEach((id) => deleteLessonInStore(id));
505
+
506
+ // Update cache: remove deleted sessions and their cascaded lessons,
507
+ // plus individually deleted orphan lessons.
508
+ const deletedSessionSet = new Set(successSessionIds);
509
+ const deletedLessonSet = new Set(successLessonIds);
510
+ queryClient.setQueryData<CourseStructureCacheData>(
511
+ courseStructureQueryKey(courseId),
512
+ (old) => {
513
+ if (!old) return old;
514
+ return {
515
+ ...old,
516
+ sessions: old.sessions.filter((s) => !deletedSessionSet.has(s.id)),
517
+ lessons: old.lessons.filter(
518
+ (l) =>
519
+ !deletedSessionSet.has(l.sessionId) &&
520
+ !deletedLessonSet.has(l.id)
521
+ ),
522
+ };
523
+ }
524
+ );
525
+
526
+ const successCount = successSessionIds.length + successLessonIds.length;
527
+ if (errorCount === 0) {
528
+ toast.success(
529
+ `${successCount} ${successCount === 1 ? 'item excluído' : 'itens excluídos'}`
530
+ );
531
+ } else {
532
+ toast.error(
533
+ `${errorCount} falha${errorCount > 1 ? 's' : ''} — ${successCount} excluído${successCount !== 1 ? 's' : ''}`
534
+ );
535
+ }
536
+ },
537
+ onError: () => {
538
+ void queryClient.invalidateQueries({
539
+ queryKey: courseStructureQueryKey(courseId),
540
+ });
541
+ toast.error('Erro ao excluir itens');
542
+ },
543
+ });
544
+ }
545
+
546
+ // ─────────────────────────────────────────────────────────────────────────────
547
+ // useReorderSessionsMutation
548
+ // ─────────────────────────────────────────────────────────────────────────────
549
+
550
+ interface ReorderSessionsVars {
551
+ /** New ordered list of session IDs. */
552
+ orderedIds: string[];
553
+ /** Snapshot of sessions before the reorder (for rollback). */
554
+ previousSessions: import('../_components/types').Session[];
555
+ }
556
+
557
+ /**
558
+ * Mutation: PATCH /lms/courses/:id/structure/sessions/reorder
559
+ *
560
+ * Optimistic: UI is already updated by the Zustand store before this runs.
561
+ * On error: rolls back via setStructureFromApi with the previous snapshot.
562
+ */
563
+ export function useReorderSessionsMutation() {
564
+ const { request } = useApp();
565
+ const queryClient = useQueryClient();
566
+ const courseId = useStructureStore((s) => s.courseId);
567
+ const setStructureFromApi = useStructureStore((s) => s.setStructureFromApi);
568
+ const lessons = useStructureStore((s) => s.lessons);
569
+
570
+ const qKey = courseStructureQueryKey(courseId);
571
+
572
+ return useMutation({
573
+ mutationFn: ({ orderedIds }: ReorderSessionsVars) =>
574
+ apiReorderSessions(request, courseId, {
575
+ sessionIds: orderedIds.map(Number),
576
+ }),
577
+ onMutate: async ({ orderedIds }) => {
578
+ // Cancel any in-flight refetches so they don't overwrite optimistic state.
579
+ await queryClient.cancelQueries({ queryKey: qKey });
580
+ const previousCache =
581
+ queryClient.getQueryData<CourseStructureCacheData>(qKey);
582
+ // Reflect the new order in the cache immediately.
583
+ queryClient.setQueryData<CourseStructureCacheData>(qKey, (old) => {
584
+ if (!old) return old;
585
+ const map = new Map(old.sessions.map((s) => [s.id, s]));
586
+ const reordered = orderedIds
587
+ .map((id, i) => {
588
+ const s = map.get(id);
589
+ return s ? { ...s, order: i + 1 } : null;
590
+ })
591
+ .filter(Boolean) as import('../_components/types').Session[];
592
+ return { ...old, sessions: reordered };
593
+ });
594
+ return { previousCache };
595
+ },
596
+ onError: (_err, { previousSessions }, context) => {
597
+ // Rollback Zustand store.
598
+ setStructureFromApi({ sessions: previousSessions, lessons });
599
+ // Rollback cache to snapshot.
600
+ if (context?.previousCache) {
601
+ queryClient.setQueryData(qKey, context.previousCache);
602
+ }
603
+ toast.error('Erro ao salvar ordem das sessões — revertido');
604
+ },
605
+ });
606
+ }
607
+
608
+ // ─────────────────────────────────────────────────────────────────────────────
609
+ // useReorderLessonsMutation
610
+ // ─────────────────────────────────────────────────────────────────────────────
611
+
612
+ interface ReorderLessonsVars {
613
+ sessionId: string;
614
+ /** New ordered list of lesson IDs within the session. */
615
+ orderedIds: string[];
616
+ /** Snapshot of all lessons before the reorder (for rollback). */
617
+ previousLessons: import('../_components/types').Lesson[];
618
+ }
619
+
620
+ /**
621
+ * Mutation: PATCH /lms/courses/:id/structure/sessions/:sessionId/lessons/reorder
622
+ *
623
+ * Optimistic: UI is already updated by the Zustand store before this runs.
624
+ * On error: rolls back via setStructureFromApi with the previous snapshot.
625
+ */
626
+ export function useReorderLessonsMutation() {
627
+ const { request } = useApp();
628
+ const queryClient = useQueryClient();
629
+ const courseId = useStructureStore((s) => s.courseId);
630
+ const sessions = useStructureStore((s) => s.sessions);
631
+ const setStructureFromApi = useStructureStore((s) => s.setStructureFromApi);
632
+
633
+ const qKey = courseStructureQueryKey(courseId);
634
+
635
+ return useMutation({
636
+ mutationFn: ({ sessionId, orderedIds }: ReorderLessonsVars) =>
637
+ apiReorderLessons(request, courseId, sessionId, {
638
+ lessonIds: orderedIds.map(Number),
639
+ }),
640
+ onMutate: async ({ orderedIds, sessionId }) => {
641
+ await queryClient.cancelQueries({ queryKey: qKey });
642
+ const previousCache =
643
+ queryClient.getQueryData<CourseStructureCacheData>(qKey);
644
+ queryClient.setQueryData<CourseStructureCacheData>(qKey, (old) => {
645
+ if (!old) return old;
646
+ const map = new Map(old.lessons.map((l) => [l.id, l]));
647
+ const sessionLessons = orderedIds
648
+ .map((id, i) => {
649
+ const l = map.get(id);
650
+ return l ? { ...l, sessionId, order: i + 1 } : null;
651
+ })
652
+ .filter(Boolean) as import('../_components/types').Lesson[];
653
+ const otherLessons = old.lessons.filter(
654
+ (l) => l.sessionId !== sessionId
655
+ );
656
+ return { ...old, lessons: [...otherLessons, ...sessionLessons] };
657
+ });
658
+ return { previousCache };
659
+ },
660
+ onError: (_err, { previousLessons }, context) => {
661
+ setStructureFromApi({ sessions, lessons: previousLessons });
662
+ if (context?.previousCache) {
663
+ queryClient.setQueryData(qKey, context.previousCache);
664
+ }
665
+ toast.error('Erro ao salvar ordem das aulas — revertido');
666
+ },
667
+ });
668
+ }
669
+
670
+ // ─────────────────────────────────────────────────────────────────────────────
671
+ // useMoveLessonMutation
672
+ // ─────────────────────────────────────────────────────────────────────────────
673
+
674
+ interface MoveLessonVars {
675
+ lessonId: string;
676
+ fromSessionId: string;
677
+ toSessionId: string;
678
+ toIndex: number;
679
+ /** Snapshot of all lessons before the move (for rollback). */
680
+ previousLessons: import('../_components/types').Lesson[];
681
+ }
682
+
683
+ /**
684
+ * Mutation: PATCH /lms/courses/:id/structure/sessions/:sessionId/lessons/:lessonId/move
685
+ *
686
+ * Optimistic: UI is already updated by the Zustand store (moveLesson) before this runs.
687
+ * On error: rolls back via setStructureFromApi with the previous snapshot.
688
+ */
689
+ export function useMoveLessonMutation() {
690
+ const { request } = useApp();
691
+ const queryClient = useQueryClient();
692
+ const courseId = useStructureStore((s) => s.courseId);
693
+ const sessions = useStructureStore((s) => s.sessions);
694
+ const setStructureFromApi = useStructureStore((s) => s.setStructureFromApi);
695
+
696
+ const qKey = courseStructureQueryKey(courseId);
697
+
698
+ return useMutation({
699
+ mutationFn: ({
700
+ lessonId,
701
+ fromSessionId,
702
+ toSessionId,
703
+ toIndex,
704
+ }: MoveLessonVars) =>
705
+ apiMoveLesson(request, courseId, fromSessionId, lessonId, {
706
+ toSessionId: Number(toSessionId),
707
+ toIndex,
708
+ }),
709
+ onMutate: async ({ lessonId, toSessionId, toIndex }) => {
710
+ await queryClient.cancelQueries({ queryKey: qKey });
711
+ const previousCache =
712
+ queryClient.getQueryData<CourseStructureCacheData>(qKey);
713
+ queryClient.setQueryData<CourseStructureCacheData>(qKey, (old) => {
714
+ if (!old) return old;
715
+ const lesson = old.lessons.find((l) => l.id === lessonId);
716
+ if (!lesson) return old;
717
+ const others = old.lessons.filter((l) => l.id !== lessonId);
718
+ const movedLesson = {
719
+ ...lesson,
720
+ sessionId: toSessionId,
721
+ order: toIndex + 1,
722
+ };
723
+ return { ...old, lessons: [...others, movedLesson] };
724
+ });
725
+ return { previousCache };
726
+ },
727
+ onError: (_err, { previousLessons }, context) => {
728
+ setStructureFromApi({ sessions, lessons: previousLessons });
729
+ if (context?.previousCache) {
730
+ queryClient.setQueryData(qKey, context.previousCache);
731
+ }
732
+ toast.error('Erro ao mover aula — revertido');
733
+ },
734
+ });
735
+ }
736
+
737
+ // ─────────────────────────────────────────────────────────────────────────────
738
+ // useDuplicateSessionMutation
739
+ // ─────────────────────────────────────────────────────────────────────────────
740
+
741
+ /**
742
+ * Mutation: POST /lms/courses/:id/structure/sessions/:sessionId/duplicate
743
+ *
744
+ * Creates a full copy of the session and all its lessons.
745
+ * Non-optimistic: waits for API then adds real items to store.
746
+ */
747
+ export function useDuplicateSessionMutation() {
748
+ const { request } = useApp();
749
+ const queryClient = useQueryClient();
750
+ const courseId = useStructureStore((s) => s.courseId);
751
+ const sessions = useStructureStore((s) => s.sessions);
752
+ const addSessionWithLessonsFromApi = useStructureStore(
753
+ (s) => s.addSessionWithLessonsFromApi
754
+ );
755
+
756
+ return useMutation({
757
+ mutationFn: ({ sessionId }: { sessionId: string }) =>
758
+ apiDuplicateSession(request, courseId, sessionId),
759
+ onSuccess: (data) => {
760
+ const session = normalizeSession(data.session, sessions.length);
761
+ const lessons = data.lessons.map((l, i) => normalizeLesson(l, i));
762
+ addSessionWithLessonsFromApi(session, lessons);
763
+ void queryClient.invalidateQueries({
764
+ queryKey: courseStructureQueryKey(courseId),
765
+ });
766
+ toast.success(`Sessão duplicada`);
767
+ },
768
+ onError: () => {
769
+ void queryClient.invalidateQueries({
770
+ queryKey: courseStructureQueryKey(courseId),
771
+ });
772
+ toast.error('Erro ao duplicar sessão');
773
+ },
774
+ });
775
+ }
776
+
777
+ // ─────────────────────────────────────────────────────────────────────────────
778
+ // useDuplicateLessonMutation
779
+ // ─────────────────────────────────────────────────────────────────────────────
780
+
781
+ /**
782
+ * Mutation: POST /lms/courses/:id/structure/sessions/:sessionId/lessons/:lessonId/duplicate
783
+ *
784
+ * Creates a copy of the lesson in the same session.
785
+ * Non-optimistic: waits for API then adds the real item to store.
786
+ */
787
+ export function useDuplicateLessonMutation() {
788
+ const { request } = useApp();
789
+ const queryClient = useQueryClient();
790
+ const courseId = useStructureStore((s) => s.courseId);
791
+ const lessons = useStructureStore((s) => s.lessons);
792
+ const addLessonFromApi = useStructureStore((s) => s.addLessonFromApi);
793
+
794
+ return useMutation({
795
+ mutationFn: ({
796
+ sessionId,
797
+ lessonId,
798
+ }: {
799
+ sessionId: string;
800
+ lessonId: string;
801
+ }) => apiDuplicateLesson(request, courseId, sessionId, lessonId),
802
+ onSuccess: (apiLesson) => {
803
+ const lesson = normalizeLesson(apiLesson, lessons.length);
804
+ addLessonFromApi(lesson);
805
+ void queryClient.invalidateQueries({
806
+ queryKey: courseStructureQueryKey(courseId),
807
+ });
808
+ toast.success('Aula duplicada');
809
+ },
810
+ onError: () => {
811
+ void queryClient.invalidateQueries({
812
+ queryKey: courseStructureQueryKey(courseId),
813
+ });
814
+ toast.error('Erro ao duplicar aula');
815
+ },
816
+ });
817
+ }
818
+
819
+ // ─────────────────────────────────────────────────────────────────────────────
820
+ // usePasteLessonsMutation
821
+ // ─────────────────────────────────────────────────────────────────────────────
822
+
823
+ /**
824
+ * Mutation: POST /lms/courses/:id/structure/sessions/:sessionId/lessons/paste
825
+ *
826
+ * Creates copies of the given lesson IDs into the target session.
827
+ * Non-optimistic: waits for API then adds the new items to store.
828
+ */
829
+ export function usePasteLessonsMutation() {
830
+ const { request } = useApp();
831
+ const queryClient = useQueryClient();
832
+ const courseId = useStructureStore((s) => s.courseId);
833
+ const lessons = useStructureStore((s) => s.lessons);
834
+ const addLessonsFromApi = useStructureStore((s) => s.addLessonsFromApi);
835
+ const clearClipboard = useStructureStore((s) => s.clearClipboard);
836
+
837
+ return useMutation({
838
+ mutationFn: ({
839
+ targetSessionId,
840
+ lessonIds,
841
+ }: {
842
+ targetSessionId: string;
843
+ lessonIds: string[];
844
+ }) =>
845
+ apiPasteLessons(request, courseId, targetSessionId, {
846
+ lessonIds: lessonIds.map(Number),
847
+ }),
848
+ onSuccess: (data, { lessonIds }) => {
849
+ const newLessons = data.lessons.map((l, i) =>
850
+ normalizeLesson(l, lessons.length + i)
851
+ );
852
+ addLessonsFromApi(newLessons);
853
+ clearClipboard();
854
+ void queryClient.invalidateQueries({
855
+ queryKey: courseStructureQueryKey(courseId),
856
+ });
857
+ toast.success(
858
+ lessonIds.length > 1
859
+ ? `${lessonIds.length} aulas coladas`
860
+ : 'Aula colada'
861
+ );
862
+ },
863
+ onError: () => {
864
+ void queryClient.invalidateQueries({
865
+ queryKey: courseStructureQueryKey(courseId),
866
+ });
867
+ toast.error('Erro ao colar aulas');
868
+ },
869
+ });
870
+ }
871
+
872
+ // ─────────────────────────────────────────────────────────────────────────────
873
+ // usePasteSessionsMutation
874
+ // ─────────────────────────────────────────────────────────────────────────────
875
+
876
+ /**
877
+ * Duplicates each copied session (and its lessons) via the duplicate endpoint.
878
+ * "Colar sessão" = duplicate each session in the clipboard into the same course.
879
+ *
880
+ * Non-optimistic: fires duplicate requests serially, then adds all results.
881
+ */
882
+ export function usePasteSessionsMutation() {
883
+ const { request } = useApp();
884
+ const queryClient = useQueryClient();
885
+ const courseId = useStructureStore((s) => s.courseId);
886
+ const sessions = useStructureStore((s) => s.sessions);
887
+ const addSessionWithLessonsFromApi = useStructureStore(
888
+ (s) => s.addSessionWithLessonsFromApi
889
+ );
890
+ const clearClipboard = useStructureStore((s) => s.clearClipboard);
891
+
892
+ return useMutation({
893
+ mutationFn: ({ sessionIds }: { sessionIds: string[] }) =>
894
+ Promise.all(
895
+ sessionIds.map((id) => apiDuplicateSession(request, courseId, id))
896
+ ),
897
+ onSuccess: (results, { sessionIds }) => {
898
+ results.forEach((data, i) => {
899
+ const session = normalizeSession(data.session, sessions.length + i);
900
+ const lessons = data.lessons.map((l, li) => normalizeLesson(l, li));
901
+ addSessionWithLessonsFromApi(session, lessons);
902
+ });
903
+ clearClipboard();
904
+ void queryClient.invalidateQueries({
905
+ queryKey: courseStructureQueryKey(courseId),
906
+ });
907
+ toast.success(
908
+ sessionIds.length > 1
909
+ ? `${sessionIds.length} sessões coladas`
910
+ : 'Sessão colada'
911
+ );
912
+ },
913
+ onError: () => {
914
+ void queryClient.invalidateQueries({
915
+ queryKey: courseStructureQueryKey(courseId),
916
+ });
917
+ toast.error('Erro ao colar sessões');
918
+ },
919
+ });
920
+ }
921
+
922
+ // ─────────────────────────────────────────────────────────────────────────────
923
+ // useMoveLessonsMutation (bulk)
924
+ // ─────────────────────────────────────────────────────────────────────────────
925
+
926
+ /**
927
+ * Moves multiple lessons to a target session, calling the move endpoint for each.
928
+ * Optimistic: caller updates store before calling mutate().
929
+ * On error: rolls back store to snapshot.
930
+ */
931
+ export function useMoveLessonsMutation() {
932
+ const { request } = useApp();
933
+ const queryClient = useQueryClient();
934
+ const courseId = useStructureStore((s) => s.courseId);
935
+ const sessions = useStructureStore((s) => s.sessions);
936
+ const setStructureFromApi = useStructureStore((s) => s.setStructureFromApi);
937
+
938
+ const qKey = courseStructureQueryKey(courseId);
939
+
940
+ return useMutation({
941
+ mutationFn: async ({
942
+ moves,
943
+ }: {
944
+ moves: Array<{
945
+ lessonId: string;
946
+ fromSessionId: string;
947
+ toSessionId: string;
948
+ toIndex: number;
949
+ }>;
950
+ previousLessons: import('../_components/types').Lesson[];
951
+ }) => {
952
+ // Sequential to preserve order integrity in the target session
953
+ for (const m of moves) {
954
+ await apiMoveLesson(request, courseId, m.fromSessionId, m.lessonId, {
955
+ toSessionId: Number(m.toSessionId),
956
+ toIndex: m.toIndex,
957
+ });
958
+ }
959
+ },
960
+ onMutate: async ({ moves }) => {
961
+ await queryClient.cancelQueries({ queryKey: qKey });
962
+ const previousCache =
963
+ queryClient.getQueryData<CourseStructureCacheData>(qKey);
964
+ queryClient.setQueryData<CourseStructureCacheData>(qKey, (old) => {
965
+ if (!old) return old;
966
+ const moveMap = new Map(moves.map((m) => [m.lessonId, m]));
967
+ return {
968
+ ...old,
969
+ lessons: old.lessons.map((l) => {
970
+ const m = moveMap.get(l.id);
971
+ return m
972
+ ? { ...l, sessionId: m.toSessionId, order: m.toIndex + 1 }
973
+ : l;
974
+ }),
975
+ };
976
+ });
977
+ return { previousCache };
978
+ },
979
+ onError: (_err, { previousLessons }, context) => {
980
+ setStructureFromApi({ sessions, lessons: previousLessons });
981
+ if (context?.previousCache) {
982
+ queryClient.setQueryData(qKey, context.previousCache);
983
+ }
984
+ toast.error('Erro ao mover aulas — revertido');
985
+ },
986
+ });
987
+ }