@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.
- package/dist/course/course-structure.controller.d.ts +60 -0
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +79 -0
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +61 -1
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +326 -1
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course.controller.d.ts +52 -4
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.service.d.ts +52 -5
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +78 -57
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course-structure-lesson.dto.js +5 -1
- 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 +4 -1
- package/dist/course/dto/create-course.dto.js.map +1 -1
- package/dist/course/dto/move-lesson.dto.d.ts +10 -0
- package/dist/course/dto/move-lesson.dto.d.ts.map +1 -0
- package/dist/course/dto/move-lesson.dto.js +28 -0
- package/dist/course/dto/move-lesson.dto.js.map +1 -0
- package/dist/course/dto/paste-lessons.dto.d.ts +4 -0
- package/dist/course/dto/paste-lessons.dto.d.ts.map +1 -0
- package/dist/course/dto/paste-lessons.dto.js +24 -0
- package/dist/course/dto/paste-lessons.dto.js.map +1 -0
- package/dist/course/dto/reorder-lessons.dto.d.ts +5 -0
- package/dist/course/dto/reorder-lessons.dto.d.ts.map +1 -0
- package/dist/course/dto/reorder-lessons.dto.js +24 -0
- package/dist/course/dto/reorder-lessons.dto.js.map +1 -0
- package/dist/course/dto/reorder-sessions.dto.d.ts +5 -0
- package/dist/course/dto/reorder-sessions.dto.d.ts.map +1 -0
- package/dist/course/dto/reorder-sessions.dto.js +24 -0
- package/dist/course/dto/reorder-sessions.dto.js.map +1 -0
- package/dist/training/training.controller.js +1 -1
- package/dist/training/training.controller.js.map +1 -1
- package/hedhog/data/image_type.yaml +20 -0
- package/hedhog/data/menu.yaml +2 -2
- package/hedhog/data/route.yaml +60 -6
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +146 -165
- package/hedhog/frontend/app/_components/course-avatar.tsx.ejs +70 -0
- package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +372 -22
- package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +10 -1
- package/hedhog/frontend/app/certificates/models/TopBar.tsx.ejs +44 -3
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +32 -0
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +437 -77
- package/hedhog/frontend/app/classes/page.tsx.ejs +311 -289
- package/hedhog/frontend/app/courses/[id]/_components/CourseCertificateCard.tsx.ejs +10 -7
- package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +23 -32
- package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +3 -9
- package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +26 -16
- package/hedhog/frontend/app/courses/[id]/_components/CourseFlagsCard.tsx.ejs +19 -5
- package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +10 -14
- package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +131 -107
- package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +10 -7
- package/hedhog/frontend/app/courses/[id]/_components/CourseSectionCard.tsx.ejs +38 -19
- package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +336 -1057
- package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +45 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-skeleton.tsx.ejs +64 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +52 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1827 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +41 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +184 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +96 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +74 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +948 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +150 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +182 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +52 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +122 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +97 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +347 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/course-structure-contract.ts.ejs +195 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/services/course-structure.service.ts.ejs +420 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +254 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +987 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +86 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure.ts.ejs +160 -0
- package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -3212
- package/hedhog/frontend/app/courses/page.tsx.ejs +45 -26
- package/hedhog/frontend/app/{training → paths}/page.tsx.ejs +29 -7
- package/hedhog/frontend/messages/en.json +91 -11
- package/hedhog/frontend/messages/pt.json +91 -11
- package/hedhog/table/course.yaml +1 -1
- package/hedhog/table/image_type.yaml +14 -0
- package/package.json +7 -7
- package/src/course/course-structure.controller.ts +63 -0
- package/src/course/course-structure.service.ts +390 -3
- package/src/course/course.service.ts +59 -27
- package/src/course/dto/create-course-structure-lesson.dto.ts +3 -2
- package/src/course/dto/create-course.dto.ts +4 -1
- package/src/course/dto/move-lesson.dto.ts +17 -0
- package/src/course/dto/paste-lessons.dto.ts +9 -0
- package/src/course/dto/reorder-lessons.dto.ts +10 -0
- package/src/course/dto/reorder-sessions.dto.ts +10 -0
- package/src/training/training.controller.ts +1 -1
package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs
ADDED
|
@@ -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
|
+
}
|