@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/adapters/course-structure.adapter.ts.ejs
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter — LMS Course Structure
|
|
3
|
+
*
|
|
4
|
+
* Converts between backend API shapes (Portuguese field names) and the
|
|
5
|
+
* frontend domain types used by the Zustand store and UI components.
|
|
6
|
+
*
|
|
7
|
+
* This is the ONLY place where field-name translation happens.
|
|
8
|
+
*
|
|
9
|
+
* ⚠️ ADAPTER POINTS — read before changing:
|
|
10
|
+
* • If the backend renames a field → update the corresponding function here.
|
|
11
|
+
* • If the backend changes a type → update the cast/conversion here.
|
|
12
|
+
* • The store and UI components must NEVER import from api-course.types.ts
|
|
13
|
+
* directly; they always receive the normalised frontend types.
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ KNOWN MISMATCHES (documented with inline comments):
|
|
16
|
+
* 1. exameVinculado (number) ↔ linkedExam (string) — converted with String()/Number()
|
|
17
|
+
* 2. 'exercicio' tipo — exists in backend and is now part of frontend LessonType.
|
|
18
|
+
* 3. Session.order — not returned by API; inferred from sorted array position.
|
|
19
|
+
* 4. Session.visibility / published — not in DB schema; defaulted to safe values.
|
|
20
|
+
* 5. Lesson.status / visibility — not in DB schema; left undefined.
|
|
21
|
+
* 6. Resource.size / url — not returned by API; left as empty string / undefined.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type {
|
|
25
|
+
Course,
|
|
26
|
+
Lesson,
|
|
27
|
+
LessonFormValues,
|
|
28
|
+
LessonInstructor,
|
|
29
|
+
LessonType,
|
|
30
|
+
Resource,
|
|
31
|
+
Session,
|
|
32
|
+
SessionFormValues,
|
|
33
|
+
} from '../../_components/types';
|
|
34
|
+
|
|
35
|
+
// Alias to keep internal adapter code concise when mapping resources.
|
|
36
|
+
type LessonResourceInput = Resource;
|
|
37
|
+
|
|
38
|
+
import type {
|
|
39
|
+
ApiCourse,
|
|
40
|
+
ApiCreateLessonPayload,
|
|
41
|
+
ApiCreateSessionPayload,
|
|
42
|
+
ApiGetStructureResponse,
|
|
43
|
+
ApiLesson,
|
|
44
|
+
ApiLessonResource,
|
|
45
|
+
ApiSession,
|
|
46
|
+
ApiUpdateLessonPayload,
|
|
47
|
+
ApiUpdateSessionPayload,
|
|
48
|
+
} from '../types/api-course.types';
|
|
49
|
+
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
51
|
+
// Internal helpers
|
|
52
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Map backend lesson type to the frontend LessonType union.
|
|
56
|
+
*
|
|
57
|
+
* ⚠️ ADAPTER POINT:
|
|
58
|
+
* 'exercicio' is now part of the frontend LessonType union.
|
|
59
|
+
*/
|
|
60
|
+
function normalizeLessonType(tipo: ApiLesson['tipo']): LessonType {
|
|
61
|
+
return tipo as LessonType;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Normalize a raw API resource to the frontend Resource shape. */
|
|
65
|
+
function normalizeResource(raw: ApiLessonResource): Resource {
|
|
66
|
+
return {
|
|
67
|
+
id: String(raw.id),
|
|
68
|
+
name: raw.nome,
|
|
69
|
+
type: raw.tipo ?? '',
|
|
70
|
+
/**
|
|
71
|
+
* ⚠️ BACKEND NOTE: file size is not returned in the API response.
|
|
72
|
+
* TODO[BACKEND]: Include `size` in the lesson resource response from
|
|
73
|
+
* CourseStructureService.getLessonById().
|
|
74
|
+
*/
|
|
75
|
+
size: '',
|
|
76
|
+
public: raw.publico,
|
|
77
|
+
/**
|
|
78
|
+
* ⚠️ BACKEND NOTE: a pre-signed or static URL is not returned.
|
|
79
|
+
* TODO[BACKEND]: Include a resolved URL (e.g. `/file/open/:id`) in the
|
|
80
|
+
* ApiLessonResource response.
|
|
81
|
+
*/
|
|
82
|
+
url: raw.url,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
87
|
+
// Backend → Frontend (normalise)
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
/** Normalize the API course metadata to the frontend Course shape. */
|
|
91
|
+
export function normalizeCourse(raw: ApiCourse): Course {
|
|
92
|
+
return {
|
|
93
|
+
id: raw.id,
|
|
94
|
+
name: raw.name ?? raw.slug,
|
|
95
|
+
title: raw.titulo,
|
|
96
|
+
description: raw.descricao,
|
|
97
|
+
slug: raw.slug,
|
|
98
|
+
published: false,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Normalize a single API session to the frontend Session shape.
|
|
104
|
+
*
|
|
105
|
+
* @param raw - Raw session object from the API response.
|
|
106
|
+
* @param index - Array position used to infer `order` (backend omits this field).
|
|
107
|
+
*/
|
|
108
|
+
export function normalizeSession(raw: ApiSession, index: number): Session {
|
|
109
|
+
return {
|
|
110
|
+
id: raw.id,
|
|
111
|
+
code: raw.codigo,
|
|
112
|
+
title: raw.titulo,
|
|
113
|
+
duration: raw.duracao,
|
|
114
|
+
/**
|
|
115
|
+
* ⚠️ BACKEND NOTE: `order` is stored in course_module but not included in
|
|
116
|
+
* the getStructure response. The backend returns modules already sorted
|
|
117
|
+
* by `order ASC`, so array position is a reliable proxy.
|
|
118
|
+
* TODO[BACKEND]: Include `order` in the ApiSession response.
|
|
119
|
+
*/
|
|
120
|
+
order: index + 1,
|
|
121
|
+
/**
|
|
122
|
+
* ⚠️ BACKEND NOTE: visibility and published are not in the backend schema.
|
|
123
|
+
* Defaulted here. If these fields are added to the DB, read them from `raw`.
|
|
124
|
+
*/
|
|
125
|
+
visibility: 'publico',
|
|
126
|
+
published: false,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Normalize a single API lesson to the frontend Lesson shape.
|
|
132
|
+
*
|
|
133
|
+
* @param raw - Raw lesson object from the API response.
|
|
134
|
+
* @param order - Position within its session (caller must compute this).
|
|
135
|
+
*/
|
|
136
|
+
export function normalizeLesson(raw: ApiLesson, order = 0): Lesson {
|
|
137
|
+
const instructors: LessonInstructor[] = (raw.instrutores ?? []).map(
|
|
138
|
+
(inst) => ({ id: String(inst.id), name: inst.name })
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
id: raw.id,
|
|
143
|
+
code: raw.codigo,
|
|
144
|
+
title: raw.titulo,
|
|
145
|
+
publicDescription: raw.descricaoPublica ?? '',
|
|
146
|
+
privateDescription: raw.descricaoPrivada ?? '',
|
|
147
|
+
type: normalizeLessonType(raw.tipo),
|
|
148
|
+
duration: raw.duracao,
|
|
149
|
+
sessionId: raw.sessaoId,
|
|
150
|
+
order,
|
|
151
|
+
videoProvider: raw.videoProvedor,
|
|
152
|
+
videoUrl: raw.videoUrl,
|
|
153
|
+
autoDuration: raw.duracaoAutomatica,
|
|
154
|
+
transcription: raw.transcricao,
|
|
155
|
+
/**
|
|
156
|
+
* ⚠️ TYPE MISMATCH: backend sends `exameVinculado` as number;
|
|
157
|
+
* frontend uses `linkedExam` as string. Converted with String().
|
|
158
|
+
*/
|
|
159
|
+
linkedExam:
|
|
160
|
+
raw.exameVinculado != null ? String(raw.exameVinculado) : undefined,
|
|
161
|
+
postContent: raw.conteudoPost,
|
|
162
|
+
resources: (raw.recursos ?? []).map(normalizeResource),
|
|
163
|
+
instructors,
|
|
164
|
+
/**
|
|
165
|
+
* ⚠️ BACKEND NOTE: status and visibility are not in the DB schema.
|
|
166
|
+
* Left undefined. If added to the schema, read them from `raw`.
|
|
167
|
+
*/
|
|
168
|
+
status: undefined,
|
|
169
|
+
visibility: undefined,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Normalize the full GET /lms/courses/:id/structure response.
|
|
175
|
+
*
|
|
176
|
+
* Returns flat `sessions` and `lessons` arrays ready to be loaded into the
|
|
177
|
+
* Zustand store, plus the available `instructors` pool for the lesson form,
|
|
178
|
+
* and the `course` metadata.
|
|
179
|
+
*/
|
|
180
|
+
export function normalizeStructureResponse(raw: ApiGetStructureResponse): {
|
|
181
|
+
course: Course | null;
|
|
182
|
+
sessions: Session[];
|
|
183
|
+
lessons: Lesson[];
|
|
184
|
+
instructors: { id: number; name: string }[];
|
|
185
|
+
} {
|
|
186
|
+
const sessions = raw.sessoes.map((s, i) => normalizeSession(s, i));
|
|
187
|
+
|
|
188
|
+
// Track per-session lesson index to infer the per-lesson order field.
|
|
189
|
+
const lessonIndexPerSession: Record<string, number> = {};
|
|
190
|
+
|
|
191
|
+
const lessons = raw.aulas.map((a) => {
|
|
192
|
+
const idx = lessonIndexPerSession[a.sessaoId] ?? 0;
|
|
193
|
+
lessonIndexPerSession[a.sessaoId] = idx + 1;
|
|
194
|
+
return normalizeLesson(a, idx + 1);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
course: raw.curso ? normalizeCourse(raw.curso) : null,
|
|
199
|
+
sessions,
|
|
200
|
+
lessons,
|
|
201
|
+
instructors: raw.instructors ?? [],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
206
|
+
// Frontend → Backend (serialise)
|
|
207
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Convert SessionFormValues → ApiCreateSessionPayload.
|
|
211
|
+
* Used for POST /lms/courses/:id/structure/sessions.
|
|
212
|
+
*
|
|
213
|
+
* ⚠️ ADAPTER POINT:
|
|
214
|
+
* `visibility` and `published` from SessionFormValues are intentionally
|
|
215
|
+
* omitted — the backend schema does not have these fields yet.
|
|
216
|
+
* TODO[BACKEND]: Add visibility/published to course_module if required.
|
|
217
|
+
*/
|
|
218
|
+
export function toCreateSessionPayload(
|
|
219
|
+
data: Pick<SessionFormValues, 'title' | 'duration' | 'description' | 'code'>
|
|
220
|
+
): ApiCreateSessionPayload {
|
|
221
|
+
return {
|
|
222
|
+
titulo: data.title,
|
|
223
|
+
duracao: data.duration,
|
|
224
|
+
...(data.description?.trim() && { descricao: data.description }),
|
|
225
|
+
...(data.code?.trim() && { codigo: data.code }),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Convert partial SessionFormValues → ApiUpdateSessionPayload.
|
|
231
|
+
* Used for PATCH /lms/courses/:id/structure/sessions/:sessionId.
|
|
232
|
+
*/
|
|
233
|
+
export function toUpdateSessionPayload(
|
|
234
|
+
data: Partial<
|
|
235
|
+
Pick<SessionFormValues, 'title' | 'duration' | 'description' | 'code'>
|
|
236
|
+
>
|
|
237
|
+
): ApiUpdateSessionPayload {
|
|
238
|
+
const payload: ApiUpdateSessionPayload = {};
|
|
239
|
+
if (data.title !== undefined) payload.titulo = data.title;
|
|
240
|
+
if (data.duration !== undefined) payload.duracao = data.duration;
|
|
241
|
+
if (data.description !== undefined) payload.descricao = data.description;
|
|
242
|
+
if (data.code !== undefined) payload.codigo = data.code;
|
|
243
|
+
return payload;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Convert LessonFormValues → ApiCreateLessonPayload.
|
|
248
|
+
* Used for POST /lms/courses/:id/structure/sessions/:sessionId/lessons.
|
|
249
|
+
*
|
|
250
|
+
* ⚠️ ADAPTER POINT:
|
|
251
|
+
* `status` and `visibility` from LessonFormValues are intentionally omitted
|
|
252
|
+
* (not in the DB schema). `linkedExam` (string) is converted to number.
|
|
253
|
+
*/
|
|
254
|
+
export function toCreateLessonPayload(
|
|
255
|
+
data: LessonFormValues & {
|
|
256
|
+
transcription?: string;
|
|
257
|
+
instructorIds?: number[];
|
|
258
|
+
resources?: LessonResourceInput[];
|
|
259
|
+
}
|
|
260
|
+
): ApiCreateLessonPayload {
|
|
261
|
+
return {
|
|
262
|
+
titulo: data.title,
|
|
263
|
+
// ⚠️ TYPE NOTE: 'exercicio' is not in the frontend LessonType but the
|
|
264
|
+
// backend accepts it. Cast is safe as long as LessonType ⊆ ApiLesson['tipo'].
|
|
265
|
+
tipo: data.type as ApiCreateLessonPayload['tipo'],
|
|
266
|
+
duracao: data.duration,
|
|
267
|
+
...(data.publicDescription && { descricaoPublica: data.publicDescription }),
|
|
268
|
+
...(data.privateDescription && {
|
|
269
|
+
descricaoPrivada: data.privateDescription,
|
|
270
|
+
}),
|
|
271
|
+
...(data.videoProvider && { videoProvedor: data.videoProvider }),
|
|
272
|
+
...(data.videoUrl && { videoUrl: data.videoUrl }),
|
|
273
|
+
...(data.autoDuration !== undefined && {
|
|
274
|
+
duracaoAutomatica: data.autoDuration,
|
|
275
|
+
}),
|
|
276
|
+
...(data.transcription && { transcricao: data.transcription }),
|
|
277
|
+
// ⚠️ TYPE MISMATCH: frontend stores linkedExam as string; backend expects number.
|
|
278
|
+
...(data.linkedExam && { exameVinculado: Number(data.linkedExam) }),
|
|
279
|
+
...(data.postContent && { conteudoPost: data.postContent }),
|
|
280
|
+
...(data.code?.trim() && { codigo: data.code }),
|
|
281
|
+
...(data.instructorIds?.length && { instructorIds: data.instructorIds }),
|
|
282
|
+
...(data.resources?.length && {
|
|
283
|
+
recursos: data.resources.map((r) => ({
|
|
284
|
+
nome: r.name,
|
|
285
|
+
// Only include fileId when the id is a valid server-assigned integer.
|
|
286
|
+
...(Number.isInteger(Number(r.id)) && Number(r.id) > 0
|
|
287
|
+
? { fileId: Number(r.id) }
|
|
288
|
+
: {}),
|
|
289
|
+
...(r.type ? { tipo: r.type } : {}),
|
|
290
|
+
publico: r.public,
|
|
291
|
+
})),
|
|
292
|
+
}),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Convert partial LessonFormValues → ApiUpdateLessonPayload.
|
|
298
|
+
* Used for PATCH /lms/courses/:id/structure/sessions/:sessionId/lessons/:lessonId.
|
|
299
|
+
*/
|
|
300
|
+
export function toUpdateLessonPayload(
|
|
301
|
+
data: Partial<
|
|
302
|
+
LessonFormValues & {
|
|
303
|
+
transcription?: string;
|
|
304
|
+
instructorIds?: number[];
|
|
305
|
+
resources?: LessonResourceInput[];
|
|
306
|
+
}
|
|
307
|
+
>
|
|
308
|
+
): ApiUpdateLessonPayload {
|
|
309
|
+
const payload: ApiUpdateLessonPayload = {};
|
|
310
|
+
if (data.title !== undefined) payload.titulo = data.title;
|
|
311
|
+
if (data.type !== undefined)
|
|
312
|
+
payload.tipo = data.type as ApiCreateLessonPayload['tipo'];
|
|
313
|
+
if (data.duration !== undefined) payload.duracao = data.duration;
|
|
314
|
+
if (data.publicDescription !== undefined)
|
|
315
|
+
payload.descricaoPublica = data.publicDescription;
|
|
316
|
+
if (data.privateDescription !== undefined)
|
|
317
|
+
payload.descricaoPrivada = data.privateDescription;
|
|
318
|
+
if (data.videoProvider !== undefined)
|
|
319
|
+
payload.videoProvedor = data.videoProvider;
|
|
320
|
+
if (data.videoUrl !== undefined) payload.videoUrl = data.videoUrl;
|
|
321
|
+
if (data.autoDuration !== undefined)
|
|
322
|
+
payload.duracaoAutomatica = data.autoDuration;
|
|
323
|
+
if (data.transcription !== undefined)
|
|
324
|
+
payload.transcricao = data.transcription;
|
|
325
|
+
if (data.postContent !== undefined) payload.conteudoPost = data.postContent;
|
|
326
|
+
if (data.code !== undefined) payload.codigo = data.code;
|
|
327
|
+
if (data.instructorIds !== undefined)
|
|
328
|
+
payload.instructorIds = data.instructorIds;
|
|
329
|
+
// ⚠️ TYPE MISMATCH: frontend stores linkedExam as string; backend expects number.
|
|
330
|
+
if (data.linkedExam !== undefined) {
|
|
331
|
+
payload.exameVinculado = data.linkedExam
|
|
332
|
+
? Number(data.linkedExam)
|
|
333
|
+
: undefined;
|
|
334
|
+
}
|
|
335
|
+
// Always send resources when provided so the backend can sync (deleteMany + createMany).
|
|
336
|
+
if (data.resources !== undefined) {
|
|
337
|
+
payload.recursos = data.resources.map((r) => ({
|
|
338
|
+
nome: r.name,
|
|
339
|
+
...(Number.isInteger(Number(r.id)) && Number(r.id) > 0
|
|
340
|
+
? { fileId: Number(r.id) }
|
|
341
|
+
: {}),
|
|
342
|
+
...(r.type ? { tipo: r.type } : {}),
|
|
343
|
+
publico: r.public,
|
|
344
|
+
}));
|
|
345
|
+
}
|
|
346
|
+
return payload;
|
|
347
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Course Structure — API Contract
|
|
3
|
+
*
|
|
4
|
+
* This file is the single source of truth for how the frontend communicates
|
|
5
|
+
* with the backend for the Course Structure feature.
|
|
6
|
+
*
|
|
7
|
+
* It contains:
|
|
8
|
+
* 1. API response shapes (what the server returns)
|
|
9
|
+
* 2. API request shapes (what we send to the server)
|
|
10
|
+
* 3. The public hook interface that call sites depend on
|
|
11
|
+
*
|
|
12
|
+
* ╔═══════════════════════════════════════════════════════════════════╗
|
|
13
|
+
* ║ Planned API surface (to be wired in when integrating backend) ║
|
|
14
|
+
* ╠═══════════════════════════════════════════════════════════════════╣
|
|
15
|
+
* ║ GET /lms/courses/:id/structure → load full tree ║
|
|
16
|
+
* ║ PUT /lms/courses/:id → update course meta ║
|
|
17
|
+
* ║ PUT /lms/courses/:id/order → save drag-drop order║
|
|
18
|
+
* ║ POST /lms/courses/:id/sessions → create session ║
|
|
19
|
+
* ║ PUT /lms/sessions/:id → update session ║
|
|
20
|
+
* ║ DELETE /lms/sessions/:id → delete session ║
|
|
21
|
+
* ║ PUT /lms/sessions/reorder → reorder sessions ║
|
|
22
|
+
* ║ POST /lms/sessions/:sessionId/lessons → create lesson ║
|
|
23
|
+
* ║ PUT /lms/lessons/:id → update lesson ║
|
|
24
|
+
* ║ DELETE /lms/lessons/:id → delete lesson ║
|
|
25
|
+
* ║ PUT /lms/lessons/:id/move → move to session ║
|
|
26
|
+
* ║ POST /lms/lessons/:id/duplicate → duplicate lesson ║
|
|
27
|
+
* ║ POST /lms/sessions/:id/duplicate → duplicate session ║
|
|
28
|
+
* ║ POST /lms/lessons/:id/resources → upload resource ║
|
|
29
|
+
* ║ DELETE /lms/resources/:id → delete resource ║
|
|
30
|
+
* ║ PUT /lms/lessons/:id/transcription → save transcription ║
|
|
31
|
+
* ╚═══════════════════════════════════════════════════════════════════╝
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type {
|
|
35
|
+
Course,
|
|
36
|
+
CourseFormValues,
|
|
37
|
+
Lesson,
|
|
38
|
+
LessonFormValues,
|
|
39
|
+
Session,
|
|
40
|
+
SessionFormValues,
|
|
41
|
+
} from '../_components/types';
|
|
42
|
+
|
|
43
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
// API — Response shapes
|
|
45
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Shape returned by GET /lms/courses/:id/structure
|
|
49
|
+
*
|
|
50
|
+
* The backend can return sessions with nested lessons (single round-trip),
|
|
51
|
+
* which this type captures. The hook will flatten them into the store.
|
|
52
|
+
*/
|
|
53
|
+
export interface CourseStructureResponse {
|
|
54
|
+
course: Course;
|
|
55
|
+
sessions: SessionWithLessons[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface SessionWithLessons extends Session {
|
|
59
|
+
lessons: Lesson[];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
63
|
+
// API — Request shapes
|
|
64
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* PUT /lms/courses/:id/order
|
|
68
|
+
*
|
|
69
|
+
* Sent after drag-and-drop to persist the new visual order.
|
|
70
|
+
* Sessions carry their own lesson order as a nested list.
|
|
71
|
+
*/
|
|
72
|
+
export interface SaveOrderRequest {
|
|
73
|
+
sessions: Array<{
|
|
74
|
+
id: string;
|
|
75
|
+
order: number;
|
|
76
|
+
lessons: Array<{ id: string; order: number }>;
|
|
77
|
+
}>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* PUT /lms/lessons/:id/move
|
|
82
|
+
*
|
|
83
|
+
* Moves a lesson to a different session (or a different position).
|
|
84
|
+
*/
|
|
85
|
+
export interface MoveLessonRequest {
|
|
86
|
+
toSessionId: string;
|
|
87
|
+
toIndex?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* POST /lms/lessons/:id/resources
|
|
92
|
+
*
|
|
93
|
+
* Multipart upload of a single resource file attached to a lesson.
|
|
94
|
+
*/
|
|
95
|
+
export interface UploadResourceRequest {
|
|
96
|
+
file: File;
|
|
97
|
+
name: string;
|
|
98
|
+
/** Whether the resource URL is publicly accessible */
|
|
99
|
+
public: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* PUT /lms/lessons/:id/transcription
|
|
104
|
+
*
|
|
105
|
+
* Persists the auto-generated or manually edited lesson transcription.
|
|
106
|
+
*/
|
|
107
|
+
export interface SaveTranscriptionRequest {
|
|
108
|
+
transcription: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
// Hook interface
|
|
113
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The public interface exposed by `useCourseStructure`.
|
|
117
|
+
*
|
|
118
|
+
* Shaped to mirror the React Query pattern so that swapping the mock
|
|
119
|
+
* implementation for real `useQuery` / `useMutation` calls only touches the
|
|
120
|
+
* hook internals — all call sites stay unchanged.
|
|
121
|
+
*
|
|
122
|
+
* ```
|
|
123
|
+
* // Current implementation (mock):
|
|
124
|
+
* useCourseStructure(courseId) → reads from Zustand store w/ mock data
|
|
125
|
+
*
|
|
126
|
+
* // Future implementation (API):
|
|
127
|
+
* useCourseStructure(courseId) → useQuery + useMutation w/ React Query
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export interface CourseStructureHook {
|
|
131
|
+
// ── Query state (mirrors useQuery) ──────────────────────────────────────
|
|
132
|
+
/** True while the initial structure is being fetched (first load) */
|
|
133
|
+
isLoading: boolean;
|
|
134
|
+
/** True when the fetch failed */
|
|
135
|
+
isError: boolean;
|
|
136
|
+
/** True during background refetches (data is already available) */
|
|
137
|
+
isFetching: boolean;
|
|
138
|
+
|
|
139
|
+
// ── Dirty / saving state ─────────────────────────────────────────────────
|
|
140
|
+
/** True when local state diverges from the last saved server state */
|
|
141
|
+
isDirty: boolean;
|
|
142
|
+
/** True while any mutation (save/create/update/delete) is in-flight */
|
|
143
|
+
isSaving: boolean;
|
|
144
|
+
|
|
145
|
+
// ── Data ─────────────────────────────────────────────────────────────────
|
|
146
|
+
course: Course;
|
|
147
|
+
sessions: Session[];
|
|
148
|
+
lessons: Lesson[];
|
|
149
|
+
|
|
150
|
+
// ── Course ───────────────────────────────────────────────────────────────
|
|
151
|
+
/** PUT /lms/courses/:id */
|
|
152
|
+
updateCourse: (data: CourseFormValues) => void;
|
|
153
|
+
/** PUT /lms/courses/:id/order — persists drag-drop order */
|
|
154
|
+
saveOrder: () => void;
|
|
155
|
+
|
|
156
|
+
// ── Sections (sessions) ──────────────────────────────────────────────────
|
|
157
|
+
/** POST /lms/courses/:id/sessions */
|
|
158
|
+
createSection: () => void;
|
|
159
|
+
/** PUT /lms/sessions/:id */
|
|
160
|
+
updateSection: (id: string, data: SessionFormValues) => void;
|
|
161
|
+
/** DELETE /lms/sessions/:id */
|
|
162
|
+
deleteSection: (id: string) => void;
|
|
163
|
+
/** PUT /lms/sessions/reorder — send new index positions */
|
|
164
|
+
reorderSections: (fromIndex: number, toIndex: number) => void;
|
|
165
|
+
/** POST /lms/sessions/:id/duplicate */
|
|
166
|
+
duplicateSection: (id: string) => void;
|
|
167
|
+
|
|
168
|
+
// ── Lessons ──────────────────────────────────────────────────────────────
|
|
169
|
+
/** POST /lms/sessions/:sessionId/lessons */
|
|
170
|
+
createLesson: (sessionId: string) => void;
|
|
171
|
+
/** PUT /lms/lessons/:id */
|
|
172
|
+
updateLesson: (
|
|
173
|
+
id: string,
|
|
174
|
+
data: Partial<LessonFormValues & { transcription?: string }>
|
|
175
|
+
) => void;
|
|
176
|
+
/** DELETE /lms/lessons/:id */
|
|
177
|
+
deleteLesson: (id: string) => void;
|
|
178
|
+
/** PUT /lms/lessons/:id/move */
|
|
179
|
+
moveLesson: (lessonId: string, toSessionId: string, toIndex?: number) => void;
|
|
180
|
+
/** POST /lms/lessons/:id/duplicate */
|
|
181
|
+
duplicateLesson: (id: string) => void;
|
|
182
|
+
|
|
183
|
+
// ── Bulk operations ──────────────────────────────────────────────────────
|
|
184
|
+
/** Batch DELETE — fires one request per selected item (or a bulk endpoint) */
|
|
185
|
+
deleteSelected: () => void;
|
|
186
|
+
/** Batch duplicate — POST /lms/{sessions|lessons}/:id/duplicate per item */
|
|
187
|
+
duplicateSelected: () => void;
|
|
188
|
+
/** Batch move — PUT /lms/lessons/:id/move per lesson */
|
|
189
|
+
moveLessons: (lessonIds: string[], toSessionId: string) => void;
|
|
190
|
+
|
|
191
|
+
// ── Clipboard (client-only — no API equivalent) ──────────────────────────
|
|
192
|
+
copyItems: (ids: string[], type: 'session' | 'lesson') => void;
|
|
193
|
+
pasteSessions: () => void;
|
|
194
|
+
pasteLessons: (toSessionId: string) => void;
|
|
195
|
+
}
|