@hed-hog/lms 0.0.312 → 0.0.315

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/class-group/class-group.controller.d.ts +2 -2
  2. package/dist/class-group/class-group.service.d.ts +2 -2
  3. package/dist/enterprise/dto/enterprise-profile.dto.d.ts +13 -0
  4. package/dist/enterprise/dto/enterprise-profile.dto.d.ts.map +1 -0
  5. package/dist/enterprise/dto/enterprise-profile.dto.js +3 -0
  6. package/dist/enterprise/dto/enterprise-profile.dto.js.map +1 -0
  7. package/dist/enterprise/enterprise.controller.d.ts +3 -0
  8. package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
  9. package/dist/enterprise/enterprise.controller.js +14 -0
  10. package/dist/enterprise/enterprise.controller.js.map +1 -1
  11. package/dist/enterprise/enterprise.service.d.ts +3 -0
  12. package/dist/enterprise/enterprise.service.d.ts.map +1 -1
  13. package/dist/enterprise/enterprise.service.js +128 -1
  14. package/dist/enterprise/enterprise.service.js.map +1 -1
  15. package/dist/instructor/instructor.controller.d.ts +23 -0
  16. package/dist/instructor/instructor.controller.d.ts.map +1 -1
  17. package/dist/instructor/instructor.controller.js +41 -0
  18. package/dist/instructor/instructor.controller.js.map +1 -1
  19. package/dist/instructor/instructor.service.d.ts +25 -0
  20. package/dist/instructor/instructor.service.d.ts.map +1 -1
  21. package/dist/instructor/instructor.service.js +126 -8
  22. package/dist/instructor/instructor.service.js.map +1 -1
  23. package/hedhog/data/menu.yaml +23 -7
  24. package/hedhog/data/role.yaml +9 -1
  25. package/hedhog/data/route.yaml +54 -0
  26. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -1
  27. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +44 -44
  28. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -362
  29. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -111
  30. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -134
  31. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -113
  32. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -314
  33. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -62
  34. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +173 -173
  35. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -58
  36. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +51 -51
  37. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -276
  38. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -1216
  39. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1824 -1824
  40. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -443
  41. package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +40 -40
  42. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -185
  43. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -264
  44. package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +95 -95
  45. package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +73 -73
  46. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -136
  47. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -80
  48. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +949 -949
  49. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -525
  50. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +181 -181
  51. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +51 -51
  52. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -271
  53. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -167
  54. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -108
  55. package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -318
  56. package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -10
  57. package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +2 -1
  58. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +438 -0
  59. package/hedhog/frontend/app/instructors/_components/instructor-types.ts.ejs +40 -0
  60. package/hedhog/frontend/app/instructors/page.tsx.ejs +696 -0
  61. package/hedhog/frontend/app/training/page.tsx.ejs +2339 -0
  62. package/hedhog/table/enterprise_user.yaml +1 -1
  63. package/package.json +8 -8
  64. package/src/enterprise/dto/enterprise-profile.dto.ts +17 -0
  65. package/src/enterprise/enterprise.controller.ts +9 -1
  66. package/src/enterprise/enterprise.service.ts +147 -4
  67. package/src/instructor/instructor.controller.ts +36 -9
  68. package/src/instructor/instructor.service.ts +140 -10
@@ -0,0 +1,438 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button';
4
+ import { EntityPicker } from '@/components/ui/entity-picker';
5
+ import { Field, FieldError, FieldLabel } from '@/components/ui/field';
6
+ import {
7
+ Select,
8
+ SelectContent,
9
+ SelectItem,
10
+ SelectTrigger,
11
+ SelectValue,
12
+ } from '@/components/ui/select';
13
+ import {
14
+ Sheet,
15
+ SheetContent,
16
+ SheetDescription,
17
+ SheetFooter,
18
+ SheetHeader,
19
+ SheetTitle,
20
+ } from '@/components/ui/sheet';
21
+ import { Switch } from '@/components/ui/switch';
22
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
23
+ import { zodResolver } from '@hookform/resolvers/zod';
24
+ import { Loader2, Plus } from 'lucide-react';
25
+ import { useEffect, useState } from 'react';
26
+ import { Controller, useForm } from 'react-hook-form';
27
+ import { toast } from 'sonner';
28
+ import { z } from 'zod';
29
+ import { CreateLmsPersonSheet } from '../../_components/create-lms-person-sheet';
30
+ import type { InstructorRow, PersonOption } from './instructor-types';
31
+
32
+ const QUALIFICATION_OPTIONS = [
33
+ {
34
+ slug: 'course-lessons',
35
+ label: 'Aulas de curso',
36
+ description: 'Pode atuar em aulas gravadas e estrutura de curso.',
37
+ },
38
+ {
39
+ slug: 'class-sessions',
40
+ label: 'Sessões de turma',
41
+ description: 'Pode atuar em contextos ao vivo e turmas.',
42
+ },
43
+ ];
44
+
45
+ const instructorFormSchema = z.object({
46
+ personId: z
47
+ .number({ invalid_type_error: 'Selecione uma pessoa' })
48
+ .int()
49
+ .positive('Selecione uma pessoa'),
50
+ qualificationSlugs: z
51
+ .array(z.string())
52
+ .min(1, 'Selecione pelo menos uma qualificação'),
53
+ status: z.enum(['active', 'inactive']),
54
+ can_teach_courses: z.boolean(),
55
+ });
56
+
57
+ type InstructorFormValues = z.infer<typeof instructorFormSchema>;
58
+
59
+ type InstructorFormSheetProps = {
60
+ open: boolean;
61
+ onOpenChange: (open: boolean) => void;
62
+ instructorId?: number | null;
63
+ onSaved?: (instructor: InstructorRow) => void | Promise<void>;
64
+ };
65
+
66
+ export function InstructorFormSheet({
67
+ open,
68
+ onOpenChange,
69
+ instructorId,
70
+ onSaved,
71
+ }: InstructorFormSheetProps) {
72
+ const { request } = useApp();
73
+ const [saving, setSaving] = useState(false);
74
+ const [createPersonOpen, setCreatePersonOpen] = useState(false);
75
+ const [trainingAccess, setTrainingAccess] = useState<boolean | null>(null);
76
+ const [togglingAccess, setTogglingAccess] = useState(false);
77
+ const isEditing = typeof instructorId === 'number' && instructorId > 0;
78
+
79
+ const {
80
+ control,
81
+ handleSubmit,
82
+ reset,
83
+ setValue,
84
+ formState: { errors },
85
+ } = useForm<InstructorFormValues>({
86
+ resolver: zodResolver(instructorFormSchema),
87
+ defaultValues: {
88
+ personId: undefined,
89
+ qualificationSlugs: ['course-lessons'],
90
+ status: 'active',
91
+ can_teach_courses: true,
92
+ },
93
+ });
94
+
95
+ const { data: existingInstructor } = useQuery<InstructorRow | null>({
96
+ queryKey: ['lms-instructor-detail', instructorId],
97
+ queryFn: async () => {
98
+ if (!instructorId) return null;
99
+ const response = await request<InstructorRow>({
100
+ url: `/lms/instructors/${instructorId}`,
101
+ method: 'GET',
102
+ });
103
+ return response.data;
104
+ },
105
+ enabled: isEditing && open,
106
+ placeholderData: null,
107
+ });
108
+
109
+ useEffect(() => {
110
+ if (isEditing && existingInstructor) {
111
+ reset({
112
+ personId: existingInstructor.personId,
113
+ qualificationSlugs:
114
+ existingInstructor.qualificationSlugs.length > 0
115
+ ? existingInstructor.qualificationSlugs
116
+ : ['course-lessons'],
117
+ status: existingInstructor.status,
118
+ can_teach_courses: true,
119
+ });
120
+ setTrainingAccess(existingInstructor.hasTrainingAccess ?? false);
121
+ } else if (!isEditing && open) {
122
+ reset({
123
+ personId: undefined,
124
+ qualificationSlugs: ['course-lessons'],
125
+ status: 'active',
126
+ can_teach_courses: true,
127
+ });
128
+ setTrainingAccess(null);
129
+ }
130
+ }, [isEditing, existingInstructor, open, reset]);
131
+
132
+ const handleSheetClose = (nextOpen: boolean) => {
133
+ if (!nextOpen) {
134
+ reset();
135
+ }
136
+ onOpenChange(nextOpen);
137
+ };
138
+
139
+ const onSubmit = async (values: InstructorFormValues) => {
140
+ try {
141
+ setSaving(true);
142
+
143
+ let result: InstructorRow;
144
+
145
+ if (isEditing) {
146
+ const response = await request<InstructorRow>({
147
+ url: `/lms/instructors/${instructorId}`,
148
+ method: 'PATCH',
149
+ data: {
150
+ qualificationSlugs: values.qualificationSlugs,
151
+ status: values.status,
152
+ },
153
+ });
154
+ result = response.data;
155
+ toast.success('Instrutor atualizado com sucesso.');
156
+ } else {
157
+ const response = await request<InstructorRow>({
158
+ url: '/lms/instructors',
159
+ method: 'POST',
160
+ data: {
161
+ personId: values.personId,
162
+ name: '',
163
+ qualificationSlugs: values.qualificationSlugs,
164
+ status: values.status,
165
+ },
166
+ });
167
+ result = response.data;
168
+ toast.success('Instrutor criado com sucesso.');
169
+ }
170
+
171
+ await onSaved?.(result);
172
+ handleSheetClose(false);
173
+ } catch {
174
+ toast.error(
175
+ isEditing
176
+ ? 'Erro ao atualizar instrutor. Tente novamente.'
177
+ : 'Erro ao criar instrutor. Tente novamente.'
178
+ );
179
+ } finally {
180
+ setSaving(false);
181
+ }
182
+ };
183
+
184
+ return (
185
+ <>
186
+ <Sheet open={open} onOpenChange={handleSheetClose}>
187
+ <SheetContent
188
+ side="right"
189
+ className="flex w-full max-w-md flex-col overflow-y-auto sm:max-w-lg"
190
+ >
191
+ <SheetHeader>
192
+ <SheetTitle>
193
+ {isEditing ? 'Editar Instrutor' : 'Novo Instrutor'}
194
+ </SheetTitle>
195
+ <SheetDescription>
196
+ {isEditing
197
+ ? 'Atualize os dados do perfil do instrutor.'
198
+ : 'Vincule uma pessoa existente ou crie uma nova para cadastrá-la como instrutora.'}
199
+ </SheetDescription>
200
+ </SheetHeader>
201
+
202
+ <form
203
+ onSubmit={handleSubmit(onSubmit)}
204
+ className="mt-6 flex flex-1 flex-col gap-5 px-4"
205
+ >
206
+ {/* Person picker */}
207
+ {!isEditing && (
208
+ <Field>
209
+ <FieldLabel>
210
+ Pessoa <span className="text-destructive">*</span>
211
+ </FieldLabel>
212
+ <div className="flex items-center gap-2">
213
+ <div className="min-w-0 flex-1">
214
+ <Controller
215
+ control={control}
216
+ name="personId"
217
+ render={({ field }) => (
218
+ <EntityPicker
219
+ value={field.value ?? null}
220
+ onChange={(val) =>
221
+ field.onChange(
222
+ val !== null ? Number(val) : undefined
223
+ )
224
+ }
225
+ valueType="number"
226
+ loadOptions={async ({ page, pageSize, search }) => {
227
+ const response = await request<{
228
+ data: PersonOption[];
229
+ lastPage?: number;
230
+ }>({
231
+ url: '/person',
232
+ method: 'GET',
233
+ params: {
234
+ page,
235
+ pageSize,
236
+ type: 'individual',
237
+ ...(search.trim()
238
+ ? { search: search.trim() }
239
+ : {}),
240
+ },
241
+ });
242
+ return {
243
+ items: (response.data?.data ?? []).map((p) => ({
244
+ id: p.id,
245
+ name: p.name,
246
+ })),
247
+ hasMore: page < (response.data?.lastPage ?? 1),
248
+ };
249
+ }}
250
+ getOptionValue={(opt) => opt.id as number}
251
+ getOptionLabel={(opt) => opt.name ?? ''}
252
+ placeholder="Buscar pessoa por nome..."
253
+ searchable
254
+ clearable
255
+ showCreateButton={false}
256
+ />
257
+ )}
258
+ />
259
+ </div>
260
+ <Button
261
+ type="button"
262
+ variant="outline"
263
+ size="icon"
264
+ className="shrink-0"
265
+ onClick={() => setCreatePersonOpen(true)}
266
+ aria-label="Criar nova pessoa"
267
+ title="Criar nova pessoa"
268
+ >
269
+ <Plus className="h-4 w-4" />
270
+ </Button>
271
+ </div>
272
+ {errors.personId && (
273
+ <FieldError>{errors.personId.message}</FieldError>
274
+ )}
275
+ </Field>
276
+ )}
277
+
278
+ {/* Status */}
279
+ <Field>
280
+ <FieldLabel>Status</FieldLabel>
281
+ <Controller
282
+ control={control}
283
+ name="status"
284
+ render={({ field }) => (
285
+ <Select value={field.value} onValueChange={field.onChange}>
286
+ <SelectTrigger>
287
+ <SelectValue placeholder="Selecione o status" />
288
+ </SelectTrigger>
289
+ <SelectContent>
290
+ <SelectItem value="active">Ativo</SelectItem>
291
+ <SelectItem value="inactive">Inativo</SelectItem>
292
+ </SelectContent>
293
+ </Select>
294
+ )}
295
+ />
296
+ </Field>
297
+
298
+ {/* can_teach_courses */}
299
+ <div className="flex items-center justify-between rounded-lg border p-4">
300
+ <div className="space-y-0.5">
301
+ <p className="text-sm font-medium">Pode ensinar cursos</p>
302
+ <p className="text-xs text-muted-foreground">
303
+ Permite que o instrutor seja vinculado a aulas de curso.
304
+ </p>
305
+ </div>
306
+ <Controller
307
+ control={control}
308
+ name="can_teach_courses"
309
+ render={({ field }) => (
310
+ <Switch
311
+ checked={field.value}
312
+ onCheckedChange={field.onChange}
313
+ />
314
+ )}
315
+ />
316
+ </div>
317
+
318
+ {/* Acesso ao Training */}
319
+ {isEditing && (
320
+ <div className="flex items-center justify-between rounded-lg border p-4">
321
+ <div className="space-y-0.5">
322
+ <p className="text-sm font-medium">
323
+ Acesso ao Hcode Training
324
+ </p>
325
+ <p className="text-xs text-muted-foreground">
326
+ {existingInstructor?.userId
327
+ ? 'Permite que este instrutor acesse a plataforma com o perfil de instrutor.'
328
+ : 'Nenhum usuário vinculado a este cadastro. Vincule um usuário para habilitar.'}
329
+ </p>
330
+ </div>
331
+ <Switch
332
+ checked={trainingAccess ?? false}
333
+ disabled={!existingInstructor?.userId || togglingAccess}
334
+ onCheckedChange={async (next) => {
335
+ setTogglingAccess(true);
336
+ try {
337
+ await request({
338
+ url: `/lms/instructors/${instructorId}/training-access`,
339
+ method: 'PATCH',
340
+ data: { enabled: next },
341
+ });
342
+ setTrainingAccess(next);
343
+ toast.success(
344
+ next
345
+ ? 'Acesso ao Training habilitado.'
346
+ : 'Acesso ao Training desabilitado.'
347
+ );
348
+ } catch {
349
+ toast.error('Erro ao alterar acesso ao Training.');
350
+ } finally {
351
+ setTogglingAccess(false);
352
+ }
353
+ }}
354
+ />
355
+ </div>
356
+ )}
357
+
358
+ {/* Qualificações */}
359
+ <Field>
360
+ <FieldLabel>
361
+ Qualificações <span className="text-destructive">*</span>
362
+ </FieldLabel>
363
+ <div className="space-y-3">
364
+ {QUALIFICATION_OPTIONS.map((option) => (
365
+ <Controller
366
+ key={option.slug}
367
+ control={control}
368
+ name="qualificationSlugs"
369
+ render={({ field }) => {
370
+ const checked = field.value.includes(option.slug);
371
+ return (
372
+ <div className="flex items-start justify-between rounded-lg border p-3">
373
+ <div className="space-y-0.5">
374
+ <p className="text-sm font-medium">
375
+ {option.label}
376
+ </p>
377
+ <p className="text-xs text-muted-foreground">
378
+ {option.description}
379
+ </p>
380
+ </div>
381
+ <Switch
382
+ checked={checked}
383
+ onCheckedChange={(next) => {
384
+ if (next) {
385
+ field.onChange([...field.value, option.slug]);
386
+ } else {
387
+ field.onChange(
388
+ field.value.filter((s) => s !== option.slug)
389
+ );
390
+ }
391
+ }}
392
+ />
393
+ </div>
394
+ );
395
+ }}
396
+ />
397
+ ))}
398
+ </div>
399
+ {errors.qualificationSlugs && (
400
+ <FieldError>{errors.qualificationSlugs.message}</FieldError>
401
+ )}
402
+ </Field>
403
+
404
+ <SheetFooter className="mt-auto pb-4">
405
+ <Button
406
+ type="button"
407
+ variant="outline"
408
+ onClick={() => handleSheetClose(false)}
409
+ disabled={saving}
410
+ >
411
+ Cancelar
412
+ </Button>
413
+ <Button type="submit" disabled={saving}>
414
+ {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
415
+ {isEditing ? 'Salvar alterações' : 'Criar instrutor'}
416
+ </Button>
417
+ </SheetFooter>
418
+ </form>
419
+ </SheetContent>
420
+ </Sheet>
421
+
422
+ <CreateLmsPersonSheet
423
+ open={createPersonOpen}
424
+ onOpenChange={setCreatePersonOpen}
425
+ onCreated={(instructor) => {
426
+ setValue('personId', instructor.personId);
427
+ setCreatePersonOpen(false);
428
+ }}
429
+ title="Nova Pessoa"
430
+ description="Preencha os dados para criar uma nova pessoa e vinculá-la como instrutora."
431
+ submitLabel="Criar pessoa"
432
+ successMessage="Pessoa criada com sucesso."
433
+ errorMessage="Erro ao criar pessoa."
434
+ defaultQualificationSlugs={['course-lessons']}
435
+ />
436
+ </>
437
+ );
438
+ }
@@ -0,0 +1,40 @@
1
+ export type InstructorRow = {
2
+ id: number;
3
+ personId: number;
4
+ name: string;
5
+ avatarId: number | null;
6
+ email?: string | null;
7
+ phone?: string | null;
8
+ status: 'active' | 'inactive';
9
+ qualificationSlugs: string[];
10
+ userId?: number | null;
11
+ hasTrainingAccess?: boolean;
12
+ };
13
+
14
+ export type InstructorStats = {
15
+ total: number;
16
+ active: number;
17
+ inactive: number;
18
+ };
19
+
20
+ export type InstructorPaginatedResult = {
21
+ data: InstructorRow[];
22
+ total: number;
23
+ page: number;
24
+ pageSize: number;
25
+ lastPage?: number;
26
+ };
27
+
28
+ export type PersonOption = {
29
+ id: number;
30
+ name: string;
31
+ avatarId?: number | null;
32
+ email?: string | null;
33
+ };
34
+
35
+ export type InstructorFormValues = {
36
+ personId: number | null;
37
+ qualificationSlugs: string[];
38
+ status: 'active' | 'inactive';
39
+ can_teach_courses: boolean;
40
+ };