@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/services/course-structure.service.ts.ejs
ADDED
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service — LMS Course Structure
|
|
3
|
+
*
|
|
4
|
+
* Contains all API call functions for the course structure feature.
|
|
5
|
+
* Each function receives the `request` helper from `useApp()` as its first
|
|
6
|
+
* argument, following the pattern used in operations/_lib/api.ts.
|
|
7
|
+
*
|
|
8
|
+
* Usage inside a React component or custom hook:
|
|
9
|
+
* const { request } = useApp();
|
|
10
|
+
* const structure = await getCourseStructure(request, courseId);
|
|
11
|
+
*
|
|
12
|
+
* ⚠️ API BASE PATH:
|
|
13
|
+
* All structure endpoints live under /lms/courses/:courseId/structure.
|
|
14
|
+
* If the base path changes, update STRUCTURE_BASE() at the top of this file.
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ MISSING BACKEND ENDPOINTS:
|
|
17
|
+
* moveLesson, reorderSessions, and reorderLessons are stubs that throw until
|
|
18
|
+
* the corresponding backend endpoints are implemented. The UI should catch
|
|
19
|
+
* these errors and fall back to optimistic local state (Zustand store).
|
|
20
|
+
* See TODO[BACKEND] comments for the required endpoint signatures.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
ApiCreateLessonPayload,
|
|
25
|
+
ApiCreateSessionPayload,
|
|
26
|
+
ApiDuplicateSessionResponse,
|
|
27
|
+
ApiGetStructureResponse,
|
|
28
|
+
ApiLesson,
|
|
29
|
+
ApiMoveLessonPayload,
|
|
30
|
+
ApiPasteLessonsPayload,
|
|
31
|
+
ApiPasteLessonsResponse,
|
|
32
|
+
ApiReorderLessonsPayload,
|
|
33
|
+
ApiReorderSessionsPayload,
|
|
34
|
+
ApiSession,
|
|
35
|
+
ApiUpdateCoursePayload,
|
|
36
|
+
ApiUpdateLessonPayload,
|
|
37
|
+
ApiUpdateSessionPayload,
|
|
38
|
+
} from '../types/api-course.types';
|
|
39
|
+
|
|
40
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
// Request function type (mirrors useApp().request from @hed-hog/next-app-provider)
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/** Matches the signature of the `request` helper returned by useApp(). */
|
|
45
|
+
type RequestFn = (input: {
|
|
46
|
+
url: string;
|
|
47
|
+
method: string;
|
|
48
|
+
data?: unknown;
|
|
49
|
+
headers?: Record<string, string>;
|
|
50
|
+
}) => Promise<{ data: unknown }>;
|
|
51
|
+
|
|
52
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
// URL helpers
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* ⚠️ BACKEND ADAPTER POINT:
|
|
58
|
+
* Update this function if the structure endpoint base path changes.
|
|
59
|
+
*/
|
|
60
|
+
const STRUCTURE_BASE = (courseId: string | number) =>
|
|
61
|
+
`/lms/courses/${courseId}/structure`;
|
|
62
|
+
|
|
63
|
+
const SESSIONS_PATH = (courseId: string | number) =>
|
|
64
|
+
`${STRUCTURE_BASE(courseId)}/sessions`;
|
|
65
|
+
|
|
66
|
+
const SESSION_PATH = (courseId: string | number, sessionId: string | number) =>
|
|
67
|
+
`${SESSIONS_PATH(courseId)}/${sessionId}`;
|
|
68
|
+
|
|
69
|
+
const LESSONS_PATH = (courseId: string | number, sessionId: string | number) =>
|
|
70
|
+
`${SESSION_PATH(courseId, sessionId)}/lessons`;
|
|
71
|
+
|
|
72
|
+
const LESSON_PATH = (
|
|
73
|
+
courseId: string | number,
|
|
74
|
+
sessionId: string | number,
|
|
75
|
+
lessonId: string | number
|
|
76
|
+
) => `${LESSONS_PATH(courseId, sessionId)}/${lessonId}`;
|
|
77
|
+
|
|
78
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
79
|
+
// Internal utility
|
|
80
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
function extractData<T>(response: { data: unknown }): T {
|
|
83
|
+
return response.data as T;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
87
|
+
// Implemented endpoints
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* GET /lms/courses/:courseId/structure
|
|
92
|
+
*
|
|
93
|
+
* Loads the full course structure: sessions, lessons, and available instructors.
|
|
94
|
+
*
|
|
95
|
+
* ⚠️ BACKEND NOTE:
|
|
96
|
+
* The response does NOT include course metadata (title, slug, etc.).
|
|
97
|
+
* Fetch GET /lms/courses/:courseId separately for page header data.
|
|
98
|
+
*/
|
|
99
|
+
export async function getCourseStructure(
|
|
100
|
+
request: RequestFn,
|
|
101
|
+
courseId: string | number
|
|
102
|
+
): Promise<ApiGetStructureResponse> {
|
|
103
|
+
const response = await request({
|
|
104
|
+
url: STRUCTURE_BASE(courseId),
|
|
105
|
+
method: 'GET',
|
|
106
|
+
});
|
|
107
|
+
return extractData<ApiGetStructureResponse>(response);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* PATCH /lms/courses/:courseId
|
|
112
|
+
*
|
|
113
|
+
* Partial update of course metadata (title, slug, description, status).
|
|
114
|
+
* Uses the standard course endpoint, not the structure sub-path.
|
|
115
|
+
*/
|
|
116
|
+
export async function updateCourse(
|
|
117
|
+
request: RequestFn,
|
|
118
|
+
courseId: string | number,
|
|
119
|
+
payload: ApiUpdateCoursePayload
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
await request({
|
|
122
|
+
url: `/lms/courses/${courseId}`,
|
|
123
|
+
method: 'PATCH',
|
|
124
|
+
data: payload,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* POST /lms/courses/:courseId/structure/sessions
|
|
130
|
+
*
|
|
131
|
+
* Creates a new session (course_module) appended to the end of the course.
|
|
132
|
+
* Returns the created session object.
|
|
133
|
+
*/
|
|
134
|
+
export async function createSession(
|
|
135
|
+
request: RequestFn,
|
|
136
|
+
courseId: string | number,
|
|
137
|
+
payload: ApiCreateSessionPayload
|
|
138
|
+
): Promise<ApiSession> {
|
|
139
|
+
const response = await request({
|
|
140
|
+
url: SESSIONS_PATH(courseId),
|
|
141
|
+
method: 'POST',
|
|
142
|
+
data: payload,
|
|
143
|
+
});
|
|
144
|
+
return extractData<ApiSession>(response);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* PATCH /lms/courses/:courseId/structure/sessions/:sessionId
|
|
149
|
+
*
|
|
150
|
+
* Partial update of a session. Only provided fields are persisted.
|
|
151
|
+
* Returns the updated session object.
|
|
152
|
+
*/
|
|
153
|
+
export async function updateSession(
|
|
154
|
+
request: RequestFn,
|
|
155
|
+
courseId: string | number,
|
|
156
|
+
sessionId: string | number,
|
|
157
|
+
payload: ApiUpdateSessionPayload
|
|
158
|
+
): Promise<ApiSession> {
|
|
159
|
+
const response = await request({
|
|
160
|
+
url: SESSION_PATH(courseId, sessionId),
|
|
161
|
+
method: 'PATCH',
|
|
162
|
+
data: payload,
|
|
163
|
+
});
|
|
164
|
+
return extractData<ApiSession>(response);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* DELETE /lms/courses/:courseId/structure/sessions/:sessionId
|
|
169
|
+
*
|
|
170
|
+
* Deletes a session and cascades to all its lessons (course_lesson records).
|
|
171
|
+
*/
|
|
172
|
+
export async function deleteSession(
|
|
173
|
+
request: RequestFn,
|
|
174
|
+
courseId: string | number,
|
|
175
|
+
sessionId: string | number
|
|
176
|
+
): Promise<{ success: boolean }> {
|
|
177
|
+
const response = await request({
|
|
178
|
+
url: SESSION_PATH(courseId, sessionId),
|
|
179
|
+
method: 'DELETE',
|
|
180
|
+
});
|
|
181
|
+
return extractData<{ success: boolean }>(response);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* POST /lms/courses/:courseId/structure/sessions/:sessionId/lessons
|
|
186
|
+
*
|
|
187
|
+
* Creates a new lesson in the specified session.
|
|
188
|
+
* Returns the full lesson object (backend runs getLessonById after creation).
|
|
189
|
+
*/
|
|
190
|
+
export async function createLesson(
|
|
191
|
+
request: RequestFn,
|
|
192
|
+
courseId: string | number,
|
|
193
|
+
sessionId: string | number,
|
|
194
|
+
payload: ApiCreateLessonPayload
|
|
195
|
+
): Promise<ApiLesson> {
|
|
196
|
+
const response = await request({
|
|
197
|
+
url: LESSONS_PATH(courseId, sessionId),
|
|
198
|
+
method: 'POST',
|
|
199
|
+
data: payload,
|
|
200
|
+
});
|
|
201
|
+
return extractData<ApiLesson>(response);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* PATCH /lms/courses/:courseId/structure/sessions/:sessionId/lessons/:lessonId
|
|
206
|
+
*
|
|
207
|
+
* Partial update of a lesson. Only provided fields are persisted.
|
|
208
|
+
* Returns the full updated lesson object.
|
|
209
|
+
*/
|
|
210
|
+
export async function updateLesson(
|
|
211
|
+
request: RequestFn,
|
|
212
|
+
courseId: string | number,
|
|
213
|
+
sessionId: string | number,
|
|
214
|
+
lessonId: string | number,
|
|
215
|
+
payload: ApiUpdateLessonPayload
|
|
216
|
+
): Promise<ApiLesson> {
|
|
217
|
+
const response = await request({
|
|
218
|
+
url: LESSON_PATH(courseId, sessionId, lessonId),
|
|
219
|
+
method: 'PATCH',
|
|
220
|
+
data: payload,
|
|
221
|
+
});
|
|
222
|
+
return extractData<ApiLesson>(response);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* DELETE /lms/courses/:courseId/structure/sessions/:sessionId/lessons/:lessonId
|
|
227
|
+
*
|
|
228
|
+
* Deletes a lesson and its relations (files, instructors, questions).
|
|
229
|
+
*/
|
|
230
|
+
export async function deleteLesson(
|
|
231
|
+
request: RequestFn,
|
|
232
|
+
courseId: string | number,
|
|
233
|
+
sessionId: string | number,
|
|
234
|
+
lessonId: string | number
|
|
235
|
+
): Promise<{ success: boolean }> {
|
|
236
|
+
const response = await request({
|
|
237
|
+
url: LESSON_PATH(courseId, sessionId, lessonId),
|
|
238
|
+
method: 'DELETE',
|
|
239
|
+
});
|
|
240
|
+
return extractData<{ success: boolean }>(response);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
244
|
+
// Pending backend endpoints (stubs — throw until implemented)
|
|
245
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* [PENDING BACKEND] Move a lesson to a different session or position.
|
|
249
|
+
*
|
|
250
|
+
* TODO[BACKEND]: Add to CourseStructureController:
|
|
251
|
+
* @Patch('sessions/:sessionId/lessons/:lessonId/move')
|
|
252
|
+
* Body: { toSessionId: number; toIndex?: number }
|
|
253
|
+
*
|
|
254
|
+
* TODO[FRONTEND]: Once the endpoint exists, replace the throw below with:
|
|
255
|
+
* const response = await request({
|
|
256
|
+
* url: `${LESSON_PATH(courseId, sessionId, lessonId)}/move`,
|
|
257
|
+
* method: 'PATCH',
|
|
258
|
+
* data: payload,
|
|
259
|
+
* });
|
|
260
|
+
* return extractData<void>(response);
|
|
261
|
+
*
|
|
262
|
+
* The UI should catch this error and keep the optimistic Zustand state.
|
|
263
|
+
/**
|
|
264
|
+
* PATCH /lms/courses/:courseId/structure/sessions/:sessionId/lessons/:lessonId/move
|
|
265
|
+
*
|
|
266
|
+
* Moves a lesson to a different session or reorders within the same session.
|
|
267
|
+
*/
|
|
268
|
+
export async function moveLesson(
|
|
269
|
+
request: RequestFn,
|
|
270
|
+
courseId: string | number,
|
|
271
|
+
sessionId: string | number,
|
|
272
|
+
lessonId: string | number,
|
|
273
|
+
payload: ApiMoveLessonPayload
|
|
274
|
+
): Promise<{ success: boolean }> {
|
|
275
|
+
const response = await request({
|
|
276
|
+
url: `${LESSON_PATH(courseId, sessionId, lessonId)}/move`,
|
|
277
|
+
method: 'PATCH',
|
|
278
|
+
data: payload,
|
|
279
|
+
});
|
|
280
|
+
return extractData<{ success: boolean }>(response);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* PATCH /lms/courses/:courseId/structure/sessions/reorder
|
|
285
|
+
*
|
|
286
|
+
* Persists the new session order after drag-and-drop.
|
|
287
|
+
* Body: { sessionIds: number[] } — full ordered list of session IDs.
|
|
288
|
+
*/
|
|
289
|
+
export async function reorderSessions(
|
|
290
|
+
request: RequestFn,
|
|
291
|
+
courseId: string | number,
|
|
292
|
+
payload: ApiReorderSessionsPayload
|
|
293
|
+
): Promise<{ success: boolean }> {
|
|
294
|
+
const response = await request({
|
|
295
|
+
url: `${SESSIONS_PATH(courseId)}/reorder`,
|
|
296
|
+
method: 'PATCH',
|
|
297
|
+
data: payload,
|
|
298
|
+
});
|
|
299
|
+
return extractData<{ success: boolean }>(response);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* PATCH /lms/courses/:courseId/structure/sessions/:sessionId/lessons/reorder
|
|
304
|
+
*
|
|
305
|
+
* Persists the new lesson order within a session after drag-and-drop.
|
|
306
|
+
* Body: { lessonIds: number[] } — full ordered list of lesson IDs.
|
|
307
|
+
*/
|
|
308
|
+
export async function reorderLessons(
|
|
309
|
+
request: RequestFn,
|
|
310
|
+
courseId: string | number,
|
|
311
|
+
sessionId: string | number,
|
|
312
|
+
payload: ApiReorderLessonsPayload
|
|
313
|
+
): Promise<{ success: boolean }> {
|
|
314
|
+
const response = await request({
|
|
315
|
+
url: `${LESSONS_PATH(courseId, sessionId)}/reorder`,
|
|
316
|
+
method: 'PATCH',
|
|
317
|
+
data: payload,
|
|
318
|
+
});
|
|
319
|
+
return extractData<{ success: boolean }>(response);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* POST /lms/courses/:courseId/structure/sessions/:sessionId/duplicate
|
|
324
|
+
*
|
|
325
|
+
* Creates a full copy of the session and all its lessons.
|
|
326
|
+
* Returns the new session and the list of cloned lessons.
|
|
327
|
+
*/
|
|
328
|
+
export async function duplicateSession(
|
|
329
|
+
request: RequestFn,
|
|
330
|
+
courseId: string | number,
|
|
331
|
+
sessionId: string | number
|
|
332
|
+
): Promise<ApiDuplicateSessionResponse> {
|
|
333
|
+
const response = await request({
|
|
334
|
+
url: `${SESSION_PATH(courseId, sessionId)}/duplicate`,
|
|
335
|
+
method: 'POST',
|
|
336
|
+
});
|
|
337
|
+
return extractData<ApiDuplicateSessionResponse>(response);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* POST /lms/courses/:courseId/structure/sessions/:sessionId/lessons/:lessonId/duplicate
|
|
342
|
+
*
|
|
343
|
+
* Creates a copy of the lesson in the same session.
|
|
344
|
+
* Returns the new lesson object.
|
|
345
|
+
*/
|
|
346
|
+
export async function duplicateLesson(
|
|
347
|
+
request: RequestFn,
|
|
348
|
+
courseId: string | number,
|
|
349
|
+
sessionId: string | number,
|
|
350
|
+
lessonId: string | number
|
|
351
|
+
): Promise<ApiLesson> {
|
|
352
|
+
const response = await request({
|
|
353
|
+
url: `${LESSON_PATH(courseId, sessionId, lessonId)}/duplicate`,
|
|
354
|
+
method: 'POST',
|
|
355
|
+
});
|
|
356
|
+
return extractData<ApiLesson>(response);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* POST /lms/courses/:courseId/structure/sessions/:sessionId/lessons/paste
|
|
361
|
+
*
|
|
362
|
+
* Creates copies of the given lesson IDs into the target session.
|
|
363
|
+
* Source lessons must belong to the same course.
|
|
364
|
+
* Returns the list of newly created lessons.
|
|
365
|
+
*/
|
|
366
|
+
export async function pasteLessons(
|
|
367
|
+
request: RequestFn,
|
|
368
|
+
courseId: string | number,
|
|
369
|
+
targetSessionId: string | number,
|
|
370
|
+
payload: ApiPasteLessonsPayload
|
|
371
|
+
): Promise<ApiPasteLessonsResponse> {
|
|
372
|
+
const response = await request({
|
|
373
|
+
url: `${LESSONS_PATH(courseId, targetSessionId)}/paste`,
|
|
374
|
+
method: 'POST',
|
|
375
|
+
data: payload,
|
|
376
|
+
});
|
|
377
|
+
return extractData<ApiPasteLessonsResponse>(response);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
381
|
+
// File upload / delete (generic — used by lesson resources)
|
|
382
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* POST /file
|
|
386
|
+
*
|
|
387
|
+
* Uploads a single file as multipart/form-data.
|
|
388
|
+
* Returns the server-assigned file record ID and the stored filename.
|
|
389
|
+
*
|
|
390
|
+
* Used by the lesson resource upload flow in EditorLesson.
|
|
391
|
+
*/
|
|
392
|
+
export async function uploadFile(
|
|
393
|
+
request: RequestFn,
|
|
394
|
+
file: File
|
|
395
|
+
): Promise<{ id: number; filename: string }> {
|
|
396
|
+
const formData = new FormData();
|
|
397
|
+
formData.append('file', file);
|
|
398
|
+
formData.append('destination', 'lms/lessons');
|
|
399
|
+
|
|
400
|
+
const response = await request({
|
|
401
|
+
url: '/file',
|
|
402
|
+
method: 'POST',
|
|
403
|
+
data: formData,
|
|
404
|
+
headers: { 'Content-Type': 'multipart/form-data' },
|
|
405
|
+
});
|
|
406
|
+
return extractData<{ id: number; filename: string }>(response);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* DELETE /file/:fileId
|
|
411
|
+
*
|
|
412
|
+
* Permanently deletes a stored file by its ID.
|
|
413
|
+
* Called when the user removes a resource from a lesson.
|
|
414
|
+
*/
|
|
415
|
+
export async function deleteFile(
|
|
416
|
+
request: RequestFn,
|
|
417
|
+
fileId: number
|
|
418
|
+
): Promise<void> {
|
|
419
|
+
await request({ url: `/file/${fileId}`, method: 'DELETE' });
|
|
420
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Types — LMS Course Structure
|
|
3
|
+
*
|
|
4
|
+
* Raw shapes returned by (and sent to) the backend API.
|
|
5
|
+
* Field names follow the Portuguese identifiers used by the NestJS service.
|
|
6
|
+
*
|
|
7
|
+
* ⚠️ BACKEND ADAPTER POINT:
|
|
8
|
+
* If the backend renames any field, update ONLY this file and the adapter.
|
|
9
|
+
* The rest of the frontend is fully decoupled from these raw shapes.
|
|
10
|
+
*
|
|
11
|
+
* Reference: libraries/lms/src/course/course-structure.service.ts
|
|
12
|
+
* Reference: libraries/lms/src/course/dto/
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
16
|
+
// Response types (backend → frontend)
|
|
17
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/** Resource (arquivo de suporte) attached to a lesson. */
|
|
20
|
+
export interface ApiLessonResource {
|
|
21
|
+
id: number;
|
|
22
|
+
nome: string;
|
|
23
|
+
/** MIME category or custom type label; may be null. */
|
|
24
|
+
tipo: string | null;
|
|
25
|
+
publico: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* TODO[BACKEND]: The API does not currently return a URL for the resource.
|
|
28
|
+
* Add a resolved URL field (e.g. `/file/open/:id`) to the lesson response
|
|
29
|
+
* in CourseStructureService.getLessonById() to enable in-browser preview.
|
|
30
|
+
*/
|
|
31
|
+
url?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Instructor linked to a lesson or available in the course pool. */
|
|
35
|
+
export interface ApiLessonInstructor {
|
|
36
|
+
id: number;
|
|
37
|
+
name: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Single lesson (aula) as returned by getStructure or createLesson/updateLesson.
|
|
42
|
+
*
|
|
43
|
+
* ⚠️ BACKEND NOTE — type mapping:
|
|
44
|
+
* The backend maps DB lesson types to UI types in mapDbTypeToUiType():
|
|
45
|
+
* DB 'video' → UI 'video'
|
|
46
|
+
* DB 'text' → UI 'post'
|
|
47
|
+
* DB 'quiz' → UI 'questao' or 'exercicio' (based on content.sourceType)
|
|
48
|
+
*
|
|
49
|
+
* ⚠️ BACKEND NOTE — missing fields:
|
|
50
|
+
* `order` and `visibility` are NOT included in the API response.
|
|
51
|
+
* `status` is not part of the DB schema.
|
|
52
|
+
* These are inferred or defaulted in the adapter.
|
|
53
|
+
*/
|
|
54
|
+
export interface ApiLesson {
|
|
55
|
+
id: string;
|
|
56
|
+
codigo: string;
|
|
57
|
+
titulo: string;
|
|
58
|
+
descricaoPublica: string;
|
|
59
|
+
descricaoPrivada: string;
|
|
60
|
+
/** UI-level lesson type resolved by the backend service. */
|
|
61
|
+
tipo: 'video' | 'questao' | 'post' | 'exercicio';
|
|
62
|
+
/** Duration in minutes. */
|
|
63
|
+
duracao: number;
|
|
64
|
+
/** ID of the parent session (course_module). */
|
|
65
|
+
sessaoId: string;
|
|
66
|
+
videoProvedor?: 'youtube' | 'vimeo' | 'bunny' | 'custom';
|
|
67
|
+
videoUrl?: string;
|
|
68
|
+
duracaoAutomatica?: boolean;
|
|
69
|
+
transcricao?: string;
|
|
70
|
+
/**
|
|
71
|
+
* ID of the linked exam (when tipo === 'questao').
|
|
72
|
+
* ⚠️ TYPE MISMATCH: backend sends number; frontend stores as string.
|
|
73
|
+
* Conversion is handled in the adapter.
|
|
74
|
+
*/
|
|
75
|
+
exameVinculado?: number;
|
|
76
|
+
conteudoPost?: string;
|
|
77
|
+
recursos: ApiLessonResource[];
|
|
78
|
+
instrutores: ApiLessonInstructor[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Single session (sessão / course_module) as returned by getStructure
|
|
83
|
+
* or createSession/updateSession.
|
|
84
|
+
*
|
|
85
|
+
* ⚠️ BACKEND NOTE — missing fields:
|
|
86
|
+
* `order` is stored in the DB but NOT included in the response.
|
|
87
|
+
* The adapter infers it from the array position (already sorted ASC by the backend).
|
|
88
|
+
* TODO[BACKEND]: Include `order` in session responses.
|
|
89
|
+
*/
|
|
90
|
+
export interface ApiSession {
|
|
91
|
+
id: string;
|
|
92
|
+
codigo: string;
|
|
93
|
+
titulo: string;
|
|
94
|
+
/** Duration in minutes. */
|
|
95
|
+
duracao: number;
|
|
96
|
+
collapsed: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Course metadata as returned inside the GET /structure response.
|
|
101
|
+
*/
|
|
102
|
+
export interface ApiCourse {
|
|
103
|
+
id: string;
|
|
104
|
+
name: string;
|
|
105
|
+
slug: string;
|
|
106
|
+
titulo: string;
|
|
107
|
+
descricao: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Response shape of GET /lms/courses/:id/structure
|
|
112
|
+
*
|
|
113
|
+
* Reference: CourseStructureService.getStructure()
|
|
114
|
+
*/
|
|
115
|
+
export interface ApiGetStructureResponse {
|
|
116
|
+
sessoes: ApiSession[];
|
|
117
|
+
aulas: ApiLesson[];
|
|
118
|
+
/** Pool of instructors qualified for course-lessons. */
|
|
119
|
+
instructors: ApiLessonInstructor[];
|
|
120
|
+
/** Course metadata included since getStructure was updated to return it. */
|
|
121
|
+
curso: ApiCourse | null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
125
|
+
// Request types (frontend → backend)
|
|
126
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* POST /lms/courses/:id/structure/sessions
|
|
130
|
+
* Reference: CreateCourseStructureSessionDto
|
|
131
|
+
*/
|
|
132
|
+
export interface ApiCreateSessionPayload {
|
|
133
|
+
titulo: string;
|
|
134
|
+
/** Duration in minutes. */
|
|
135
|
+
duracao: number;
|
|
136
|
+
descricao?: string;
|
|
137
|
+
codigo?: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* PATCH /lms/courses/:id
|
|
142
|
+
* Reference: UpdateCourseDto (extends PartialType(CreateCourseDto))
|
|
143
|
+
*/
|
|
144
|
+
export interface ApiUpdateCoursePayload {
|
|
145
|
+
title?: string;
|
|
146
|
+
slug?: string;
|
|
147
|
+
name?: string;
|
|
148
|
+
description?: string;
|
|
149
|
+
/** 'published' = visible to students; 'draft' = hidden. */
|
|
150
|
+
status?: 'draft' | 'published' | 'archived';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* PATCH /lms/courses/:id/structure/sessions/:sessionId
|
|
155
|
+
* Reference: UpdateCourseStructureSessionDto (all fields optional via PartialType)
|
|
156
|
+
*/
|
|
157
|
+
export interface ApiUpdateSessionPayload {
|
|
158
|
+
titulo?: string;
|
|
159
|
+
/** Duration in minutes. */
|
|
160
|
+
duracao?: number;
|
|
161
|
+
descricao?: string;
|
|
162
|
+
codigo?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* POST /lms/courses/:id/structure/sessions/:sessionId/lessons
|
|
167
|
+
* Reference: CreateCourseStructureLessonDto
|
|
168
|
+
*/
|
|
169
|
+
export interface ApiCreateLessonPayload {
|
|
170
|
+
titulo: string;
|
|
171
|
+
tipo: 'video' | 'questao' | 'post' | 'exercicio';
|
|
172
|
+
/** Duration in minutes. */
|
|
173
|
+
duracao: number;
|
|
174
|
+
descricaoPublica?: string;
|
|
175
|
+
descricaoPrivada?: string;
|
|
176
|
+
videoProvedor?: 'youtube' | 'vimeo' | 'bunny' | 'custom';
|
|
177
|
+
videoUrl?: string;
|
|
178
|
+
duracaoAutomatica?: boolean;
|
|
179
|
+
transcricao?: string;
|
|
180
|
+
/** ID of the linked exam. ⚠️ TYPE MISMATCH: backend expects number; frontend stores string. */
|
|
181
|
+
exameVinculado?: number;
|
|
182
|
+
conteudoPost?: string;
|
|
183
|
+
instructorIds?: number[];
|
|
184
|
+
recursos?: Array<{
|
|
185
|
+
nome: string;
|
|
186
|
+
fileId?: number;
|
|
187
|
+
tipo?: string;
|
|
188
|
+
publico?: boolean;
|
|
189
|
+
}>;
|
|
190
|
+
codigo?: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* PATCH /lms/courses/:id/structure/sessions/:sessionId/lessons/:lessonId
|
|
195
|
+
* Reference: UpdateCourseStructureLessonDto (all fields optional via PartialType)
|
|
196
|
+
*/
|
|
197
|
+
export type ApiUpdateLessonPayload = Partial<ApiCreateLessonPayload>;
|
|
198
|
+
|
|
199
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
200
|
+
// Pending backend endpoint request types
|
|
201
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
202
|
+
//
|
|
203
|
+
// The following types define the expected payloads for endpoints that do NOT
|
|
204
|
+
// yet exist in the backend. They are kept here so that when the endpoints are
|
|
205
|
+
// implemented, the service layer only needs to remove the "throw" stubs.
|
|
206
|
+
//
|
|
207
|
+
// TODO[BACKEND]: Implement in CourseStructureController:
|
|
208
|
+
// 1. PATCH /lms/courses/:id/structure/sessions/reorder
|
|
209
|
+
// Body: ApiReorderSessionsPayload
|
|
210
|
+
// 2. PATCH /lms/courses/:id/structure/sessions/:sessionId/lessons/reorder
|
|
211
|
+
// Body: ApiReorderLessonsPayload
|
|
212
|
+
// 3. PATCH /lms/courses/:id/structure/sessions/:sessionId/lessons/:lessonId/move
|
|
213
|
+
// Body: ApiMoveLessonPayload
|
|
214
|
+
|
|
215
|
+
/** [PENDING BACKEND] Reorder sessions by providing new sorted ID list. */
|
|
216
|
+
export interface ApiReorderSessionsPayload {
|
|
217
|
+
/** Ordered array of session IDs reflecting the new visual order. */
|
|
218
|
+
sessionIds: number[];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** [PENDING BACKEND] Reorder lessons within a session. */
|
|
222
|
+
export interface ApiReorderLessonsPayload {
|
|
223
|
+
/** Ordered array of lesson IDs in their new positions. */
|
|
224
|
+
lessonIds: number[];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** [PENDING BACKEND] Move a lesson to a different session or position. */
|
|
228
|
+
export interface ApiMoveLessonPayload {
|
|
229
|
+
toSessionId: number;
|
|
230
|
+
toIndex?: number;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* POST /lms/courses/:id/structure/sessions/:sessionId/duplicate
|
|
235
|
+
* Response: new session + all cloned lessons.
|
|
236
|
+
*/
|
|
237
|
+
export interface ApiDuplicateSessionResponse {
|
|
238
|
+
session: ApiSession;
|
|
239
|
+
lessons: ApiLesson[];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* POST /lms/courses/:id/structure/sessions/:sessionId/lessons/paste
|
|
244
|
+
* Body: IDs of source lessons to clone into the target session.
|
|
245
|
+
*/
|
|
246
|
+
export interface ApiPasteLessonsPayload {
|
|
247
|
+
/** IDs of source lessons (must belong to the same course). */
|
|
248
|
+
lessonIds: number[];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Response from pasteLessons endpoint. */
|
|
252
|
+
export interface ApiPasteLessonsResponse {
|
|
253
|
+
lessons: ApiLesson[];
|
|
254
|
+
}
|