@hed-hog/lms 0.0.325 → 0.0.326

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 (69) hide show
  1. package/dist/course/course.service.d.ts +3 -1
  2. package/dist/course/course.service.d.ts.map +1 -1
  3. package/dist/course/course.service.js +35 -5
  4. package/dist/course/course.service.js.map +1 -1
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/instructor/dto/create-instructor-skill.dto.d.ts +9 -0
  10. package/dist/instructor/dto/create-instructor-skill.dto.d.ts.map +1 -0
  11. package/dist/instructor/dto/create-instructor-skill.dto.js +48 -0
  12. package/dist/instructor/dto/create-instructor-skill.dto.js.map +1 -0
  13. package/dist/instructor/dto/create-instructor.dto.d.ts +2 -0
  14. package/dist/instructor/dto/create-instructor.dto.d.ts.map +1 -1
  15. package/dist/instructor/dto/create-instructor.dto.js +12 -0
  16. package/dist/instructor/dto/create-instructor.dto.js.map +1 -1
  17. package/dist/instructor/dto/update-instructor-skill.dto.d.ts +9 -0
  18. package/dist/instructor/dto/update-instructor-skill.dto.d.ts.map +1 -0
  19. package/dist/instructor/dto/update-instructor-skill.dto.js +50 -0
  20. package/dist/instructor/dto/update-instructor-skill.dto.js.map +1 -0
  21. package/dist/instructor/dto/update-instructor.dto.d.ts +2 -0
  22. package/dist/instructor/dto/update-instructor.dto.d.ts.map +1 -1
  23. package/dist/instructor/dto/update-instructor.dto.js +12 -0
  24. package/dist/instructor/dto/update-instructor.dto.js.map +1 -1
  25. package/dist/instructor/instructor-skill.controller.d.ts +38 -0
  26. package/dist/instructor/instructor-skill.controller.d.ts.map +1 -0
  27. package/dist/instructor/instructor-skill.controller.js +89 -0
  28. package/dist/instructor/instructor-skill.controller.js.map +1 -0
  29. package/dist/instructor/instructor-skill.service.d.ts +48 -0
  30. package/dist/instructor/instructor-skill.service.d.ts.map +1 -0
  31. package/dist/instructor/instructor-skill.service.js +203 -0
  32. package/dist/instructor/instructor-skill.service.js.map +1 -0
  33. package/dist/instructor/instructor.controller.d.ts +26 -0
  34. package/dist/instructor/instructor.controller.d.ts.map +1 -1
  35. package/dist/instructor/instructor.module.d.ts.map +1 -1
  36. package/dist/instructor/instructor.module.js +4 -2
  37. package/dist/instructor/instructor.module.js.map +1 -1
  38. package/dist/instructor/instructor.service.d.ts +35 -0
  39. package/dist/instructor/instructor.service.d.ts.map +1 -1
  40. package/dist/instructor/instructor.service.js +132 -11
  41. package/dist/instructor/instructor.service.js.map +1 -1
  42. package/dist/training/training.service.d.ts +3 -1
  43. package/dist/training/training.service.d.ts.map +1 -1
  44. package/dist/training/training.service.js +34 -4
  45. package/dist/training/training.service.js.map +1 -1
  46. package/hedhog/data/integration_event_catalog.yaml +219 -0
  47. package/hedhog/data/menu.yaml +23 -6
  48. package/hedhog/data/route.yaml +45 -0
  49. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +1 -1
  50. package/hedhog/frontend/app/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +1 -1
  51. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +547 -0
  52. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +845 -239
  53. package/hedhog/frontend/app/instructors/_components/instructor-types.ts.ejs +9 -0
  54. package/hedhog/frontend/app/instructors/page.tsx.ejs +69 -20
  55. package/hedhog/table/instructor.yaml +5 -0
  56. package/hedhog/table/instructor_skill.yaml +26 -0
  57. package/hedhog/table/instructor_skill_assignment.yaml +22 -0
  58. package/package.json +7 -7
  59. package/src/course/course.service.ts +38 -4
  60. package/src/index.ts +2 -0
  61. package/src/instructor/dto/create-instructor-skill.dto.ts +28 -0
  62. package/src/instructor/dto/create-instructor.dto.ts +20 -8
  63. package/src/instructor/dto/update-instructor-skill.dto.ts +30 -0
  64. package/src/instructor/dto/update-instructor.dto.ts +18 -6
  65. package/src/instructor/instructor-skill.controller.ts +60 -0
  66. package/src/instructor/instructor-skill.service.ts +214 -0
  67. package/src/instructor/instructor.module.ts +4 -2
  68. package/src/instructor/instructor.service.ts +148 -0
  69. package/src/training/training.service.ts +38 -4
@@ -0,0 +1,547 @@
1
+ 'use client';
2
+
3
+ import {
4
+ EmptyState,
5
+ Page,
6
+ PageHeader,
7
+ PaginationFooter,
8
+ SearchBar,
9
+ } from '@/components/entity-list';
10
+ import {
11
+ AlertDialog,
12
+ AlertDialogAction,
13
+ AlertDialogCancel,
14
+ AlertDialogContent,
15
+ AlertDialogDescription,
16
+ AlertDialogHeader,
17
+ AlertDialogTitle,
18
+ } from '@/components/ui/alert-dialog';
19
+ import { Badge } from '@/components/ui/badge';
20
+ import { Button } from '@/components/ui/button';
21
+ import {
22
+ DropdownMenu,
23
+ DropdownMenuContent,
24
+ DropdownMenuItem,
25
+ DropdownMenuSeparator,
26
+ DropdownMenuTrigger,
27
+ } from '@/components/ui/dropdown-menu';
28
+ import {
29
+ Form,
30
+ FormControl,
31
+ FormField,
32
+ FormItem,
33
+ FormLabel,
34
+ FormMessage,
35
+ } from '@/components/ui/form';
36
+ import { Input } from '@/components/ui/input';
37
+ import {
38
+ Select,
39
+ SelectContent,
40
+ SelectItem,
41
+ SelectTrigger,
42
+ SelectValue,
43
+ } from '@/components/ui/select';
44
+ import {
45
+ Sheet,
46
+ SheetContent,
47
+ SheetHeader,
48
+ SheetTitle,
49
+ } from '@/components/ui/sheet';
50
+ import { Skeleton } from '@/components/ui/skeleton';
51
+ import {
52
+ Table,
53
+ TableBody,
54
+ TableCell,
55
+ TableHead,
56
+ TableHeader,
57
+ TableRow,
58
+ } from '@/components/ui/table';
59
+ import { cn } from '@/lib/utils';
60
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
61
+ import { zodResolver } from '@hookform/resolvers/zod';
62
+ import { MoreHorizontal, Pencil, Plus, Sparkles, Trash2 } from 'lucide-react';
63
+ import { useEffect, useState } from 'react';
64
+ import { useForm } from 'react-hook-form';
65
+ import { toast } from 'sonner';
66
+ import * as z from 'zod';
67
+
68
+ // ─── Types ────────────────────────────────────────────────────────────────────
69
+
70
+ type InstructorSkill = {
71
+ id: number;
72
+ slug: string;
73
+ namePt: string;
74
+ nameEn?: string | null;
75
+ status: 'active' | 'inactive';
76
+ };
77
+
78
+ type InstructorSkillPaginatedResult = {
79
+ data: InstructorSkill[];
80
+ total: number;
81
+ page: number;
82
+ pageSize: number;
83
+ lastPage?: number;
84
+ };
85
+
86
+ // ─── Zod schema ───────────────────────────────────────────────────────────────
87
+
88
+ const skillSchema = z.object({
89
+ slug: z
90
+ .string()
91
+ .min(1, 'Obrigatório')
92
+ .max(100, 'Máximo 100 caracteres')
93
+ .regex(/^[a-z0-9-]+$/, 'Use apenas letras minúsculas, números e hifens'),
94
+ namePt: z.string().min(1, 'Obrigatório').max(255, 'Máximo 255 caracteres'),
95
+ nameEn: z
96
+ .string()
97
+ .max(255, 'Máximo 255 caracteres')
98
+ .optional()
99
+ .or(z.literal('')),
100
+ status: z.enum(['active', 'inactive']).default('active'),
101
+ });
102
+
103
+ type SkillFormValues = z.infer<typeof skillSchema>;
104
+
105
+ // ─── SkillFormSheet ────────────────────────────────────────────────────────────
106
+
107
+ interface SkillFormSheetProps {
108
+ open: boolean;
109
+ onOpenChange: (open: boolean) => void;
110
+ skillToEdit: InstructorSkill | null;
111
+ onSaved: () => void;
112
+ }
113
+
114
+ function SkillFormSheet({
115
+ open,
116
+ onOpenChange,
117
+ skillToEdit,
118
+ onSaved,
119
+ }: SkillFormSheetProps) {
120
+ const { request } = useApp();
121
+ const [isSaving, setIsSaving] = useState(false);
122
+
123
+ const form = useForm<SkillFormValues>({
124
+ resolver: zodResolver(skillSchema),
125
+ defaultValues: {
126
+ slug: '',
127
+ namePt: '',
128
+ nameEn: '',
129
+ status: 'active',
130
+ },
131
+ });
132
+
133
+ // Populate form when editing
134
+ useEffect(() => {
135
+ if (open) {
136
+ if (skillToEdit) {
137
+ form.reset({
138
+ slug: skillToEdit.slug,
139
+ namePt: skillToEdit.namePt,
140
+ nameEn: skillToEdit.nameEn ?? '',
141
+ status: skillToEdit.status,
142
+ });
143
+ } else {
144
+ form.reset({
145
+ slug: '',
146
+ namePt: '',
147
+ nameEn: '',
148
+ status: 'active',
149
+ });
150
+ }
151
+ }
152
+ }, [open, skillToEdit, form]);
153
+
154
+ const onSubmit = async (values: SkillFormValues) => {
155
+ try {
156
+ setIsSaving(true);
157
+ const payload = {
158
+ slug: values.slug,
159
+ namePt: values.namePt,
160
+ nameEn: values.nameEn || undefined,
161
+ status: values.status,
162
+ };
163
+
164
+ if (skillToEdit) {
165
+ await request({
166
+ url: `/lms/instructor-skills/${skillToEdit.id}`,
167
+ method: 'PATCH',
168
+ data: payload,
169
+ });
170
+ toast.success('Skill atualizada com sucesso.');
171
+ } else {
172
+ await request({
173
+ url: '/lms/instructor-skills',
174
+ method: 'POST',
175
+ data: payload,
176
+ });
177
+ toast.success('Skill criada com sucesso.');
178
+ }
179
+
180
+ onSaved();
181
+ onOpenChange(false);
182
+ } catch {
183
+ toast.error('Erro ao salvar skill. Tente novamente.');
184
+ } finally {
185
+ setIsSaving(false);
186
+ }
187
+ };
188
+
189
+ return (
190
+ <Sheet open={open} onOpenChange={onOpenChange}>
191
+ <SheetContent className="w-full overflow-y-auto sm:max-w-md">
192
+ <SheetHeader>
193
+ <SheetTitle>{skillToEdit ? 'Editar Skill' : 'Nova Skill'}</SheetTitle>
194
+ </SheetHeader>
195
+
196
+ <Form {...form}>
197
+ <form
198
+ onSubmit={form.handleSubmit(onSubmit)}
199
+ className="flex flex-col gap-4 px-4 py-4"
200
+ >
201
+ <FormField
202
+ control={form.control}
203
+ name="slug"
204
+ render={({ field }) => (
205
+ <FormItem>
206
+ <FormLabel>Slug</FormLabel>
207
+ <FormControl>
208
+ <Input placeholder="ex: javascript, react-js" {...field} />
209
+ </FormControl>
210
+ <FormMessage />
211
+ </FormItem>
212
+ )}
213
+ />
214
+
215
+ <FormField
216
+ control={form.control}
217
+ name="namePt"
218
+ render={({ field }) => (
219
+ <FormItem>
220
+ <FormLabel>Nome (PT)</FormLabel>
221
+ <FormControl>
222
+ <Input placeholder="Nome em português" {...field} />
223
+ </FormControl>
224
+ <FormMessage />
225
+ </FormItem>
226
+ )}
227
+ />
228
+
229
+ <FormField
230
+ control={form.control}
231
+ name="nameEn"
232
+ render={({ field }) => (
233
+ <FormItem>
234
+ <FormLabel>Nome (EN)</FormLabel>
235
+ <FormControl>
236
+ <Input
237
+ placeholder="Name in English (optional)"
238
+ {...field}
239
+ />
240
+ </FormControl>
241
+ <FormMessage />
242
+ </FormItem>
243
+ )}
244
+ />
245
+
246
+ <FormField
247
+ control={form.control}
248
+ name="status"
249
+ render={({ field }) => (
250
+ <FormItem>
251
+ <FormLabel>Status</FormLabel>
252
+ <Select onValueChange={field.onChange} value={field.value}>
253
+ <FormControl>
254
+ <SelectTrigger className="w-full">
255
+ <SelectValue placeholder="Selecione o status" />
256
+ </SelectTrigger>
257
+ </FormControl>
258
+ <SelectContent>
259
+ <SelectItem value="active">Ativo</SelectItem>
260
+ <SelectItem value="inactive">Inativo</SelectItem>
261
+ </SelectContent>
262
+ </Select>
263
+ <FormMessage />
264
+ </FormItem>
265
+ )}
266
+ />
267
+
268
+ <div className="flex justify-end gap-2 pt-2">
269
+ <Button
270
+ type="button"
271
+ variant="outline"
272
+ onClick={() => onOpenChange(false)}
273
+ disabled={isSaving}
274
+ >
275
+ Cancelar
276
+ </Button>
277
+ <Button type="submit" disabled={isSaving}>
278
+ {isSaving ? 'Salvando...' : 'Salvar'}
279
+ </Button>
280
+ </div>
281
+ </form>
282
+ </Form>
283
+ </SheetContent>
284
+ </Sheet>
285
+ );
286
+ }
287
+
288
+ // ─── Page ─────────────────────────────────────────────────────────────────────
289
+
290
+ export default function InstructorSkillsPage() {
291
+ const { request } = useApp();
292
+
293
+ const [page, setPage] = useState(1);
294
+ const [pageSize, setPageSize] = useState(15);
295
+ const [searchInput, setSearchInput] = useState('');
296
+ const [debouncedSearch, setDebouncedSearch] = useState('');
297
+ const [sheetOpen, setSheetOpen] = useState(false);
298
+ const [skillToEdit, setSkillToEdit] = useState<InstructorSkill | null>(null);
299
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
300
+ const [skillToDelete, setSkillToDelete] = useState<InstructorSkill | null>(
301
+ null
302
+ );
303
+ const [isDeleting, setIsDeleting] = useState(false);
304
+
305
+ // Debounce search
306
+ useEffect(() => {
307
+ const timeout = setTimeout(() => {
308
+ setDebouncedSearch(searchInput.trim());
309
+ setPage(1);
310
+ }, 300);
311
+ return () => clearTimeout(timeout);
312
+ }, [searchInput]);
313
+
314
+ // List query
315
+ const {
316
+ data: paginate = {
317
+ data: [],
318
+ total: 0,
319
+ page: 1,
320
+ pageSize,
321
+ lastPage: 1,
322
+ },
323
+ isLoading,
324
+ refetch: refetchList,
325
+ } = useQuery<InstructorSkillPaginatedResult>({
326
+ queryKey: ['lms-instructor-skills', page, pageSize, debouncedSearch],
327
+ queryFn: async () => {
328
+ const params = new URLSearchParams({
329
+ page: String(page),
330
+ pageSize: String(pageSize),
331
+ });
332
+ if (debouncedSearch) params.set('search', debouncedSearch);
333
+
334
+ const response = await request<InstructorSkillPaginatedResult>({
335
+ url: `/lms/instructor-skills?${params.toString()}`,
336
+ method: 'GET',
337
+ });
338
+ return response.data;
339
+ },
340
+ placeholderData: (prev) =>
341
+ prev ?? { data: [], total: 0, page: 1, pageSize, lastPage: 1 },
342
+ });
343
+
344
+ const totalPages = Math.max(
345
+ 1,
346
+ (paginate.lastPage ?? Math.ceil((paginate.total || 0) / pageSize)) || 1
347
+ );
348
+
349
+ useEffect(() => {
350
+ if (page > totalPages) setPage(totalPages);
351
+ }, [page, totalPages]);
352
+
353
+ const openCreateSheet = () => {
354
+ setSkillToEdit(null);
355
+ setSheetOpen(true);
356
+ };
357
+
358
+ const openEditSheet = (skill: InstructorSkill) => {
359
+ setSkillToEdit(skill);
360
+ setSheetOpen(true);
361
+ };
362
+
363
+ const handleSaved = async () => {
364
+ await refetchList();
365
+ };
366
+
367
+ const handleDeleteConfirm = async () => {
368
+ if (!skillToDelete) return;
369
+ try {
370
+ setIsDeleting(true);
371
+ await request({
372
+ url: `/lms/instructor-skills/${skillToDelete.id}`,
373
+ method: 'DELETE',
374
+ });
375
+ toast.success('Skill removida com sucesso.');
376
+ setDeleteDialogOpen(false);
377
+ setSkillToDelete(null);
378
+ await refetchList();
379
+ } catch {
380
+ toast.error('Erro ao remover skill. Tente novamente.');
381
+ } finally {
382
+ setIsDeleting(false);
383
+ }
384
+ };
385
+
386
+ return (
387
+ <Page>
388
+ <PageHeader
389
+ breadcrumbs={[
390
+ { label: 'Home', href: '/' },
391
+ { label: 'LMS', href: '/lms' },
392
+ { label: 'Skills de Instrutor' },
393
+ ]}
394
+ title="Skills de Instrutor"
395
+ description="Gerencie o catálogo de skills disponíveis para instrutores."
396
+ actions={[
397
+ {
398
+ label: 'Nova Skill',
399
+ onClick: openCreateSheet,
400
+ icon: <Plus className="h-4 w-4" />,
401
+ },
402
+ ]}
403
+ />
404
+
405
+ <SearchBar
406
+ searchQuery={searchInput}
407
+ onSearchChange={(value) => setSearchInput(value)}
408
+ onSearch={() => setPage(1)}
409
+ placeholder="Buscar por slug ou nome..."
410
+ />
411
+
412
+ {isLoading ? (
413
+ <div className="space-y-3 p-4">
414
+ {Array.from({ length: 5 }).map((_, i) => (
415
+ <Skeleton key={i} className="h-14 w-full" />
416
+ ))}
417
+ </div>
418
+ ) : paginate.data.length === 0 ? (
419
+ <EmptyState
420
+ icon={<Sparkles className="h-12 w-12" />}
421
+ title="Nenhuma skill encontrada"
422
+ description="Crie uma nova skill ou ajuste os filtros de busca."
423
+ actionLabel="Nova Skill"
424
+ actionIcon={<Plus className="mr-2 h-4 w-4" />}
425
+ onAction={openCreateSheet}
426
+ />
427
+ ) : (
428
+ <div className="overflow-x-auto">
429
+ <Table>
430
+ <TableHeader>
431
+ <TableRow>
432
+ <TableHead>Slug</TableHead>
433
+ <TableHead>Nome (PT)</TableHead>
434
+ <TableHead>Nome (EN)</TableHead>
435
+ <TableHead>Status</TableHead>
436
+ <TableHead className="w-10" />
437
+ </TableRow>
438
+ </TableHeader>
439
+ <TableBody>
440
+ {paginate.data.map((skill) => (
441
+ <TableRow
442
+ key={skill.id}
443
+ className="cursor-pointer"
444
+ onDoubleClick={() => openEditSheet(skill)}
445
+ >
446
+ <TableCell>
447
+ <span className="font-mono text-sm">{skill.slug}</span>
448
+ </TableCell>
449
+ <TableCell>
450
+ <span className="font-medium">{skill.namePt}</span>
451
+ </TableCell>
452
+ <TableCell>
453
+ <span className="text-sm text-muted-foreground">
454
+ {skill.nameEn ?? '—'}
455
+ </span>
456
+ </TableCell>
457
+ <TableCell>
458
+ <Badge
459
+ variant="outline"
460
+ className={cn(
461
+ 'border px-2.5 py-1 text-xs font-medium',
462
+ skill.status === 'active'
463
+ ? 'border-green-500/20 bg-green-500/10 text-green-600'
464
+ : 'border-gray-500/20 bg-gray-500/10 text-gray-600'
465
+ )}
466
+ >
467
+ {skill.status === 'active' ? 'Ativo' : 'Inativo'}
468
+ </Badge>
469
+ </TableCell>
470
+ <TableCell>
471
+ <DropdownMenu>
472
+ <DropdownMenuTrigger asChild>
473
+ <Button variant="ghost" size="icon" className="h-8 w-8">
474
+ <MoreHorizontal className="h-4 w-4" />
475
+ </Button>
476
+ </DropdownMenuTrigger>
477
+ <DropdownMenuContent align="end">
478
+ <DropdownMenuItem onClick={() => openEditSheet(skill)}>
479
+ <Pencil className="mr-2 h-4 w-4" />
480
+ Editar
481
+ </DropdownMenuItem>
482
+ <DropdownMenuSeparator />
483
+ <DropdownMenuItem
484
+ className="text-red-600"
485
+ onClick={() => {
486
+ setSkillToDelete(skill);
487
+ setDeleteDialogOpen(true);
488
+ }}
489
+ >
490
+ <Trash2 className="mr-2 h-4 w-4" />
491
+ Excluir
492
+ </DropdownMenuItem>
493
+ </DropdownMenuContent>
494
+ </DropdownMenu>
495
+ </TableCell>
496
+ </TableRow>
497
+ ))}
498
+ </TableBody>
499
+ </Table>
500
+ </div>
501
+ )}
502
+
503
+ <PaginationFooter
504
+ currentPage={page}
505
+ pageSize={pageSize}
506
+ totalItems={paginate.total}
507
+ onPageChange={setPage}
508
+ onPageSizeChange={(nextPageSize) => {
509
+ setPageSize(nextPageSize);
510
+ setPage(1);
511
+ }}
512
+ />
513
+
514
+ <SkillFormSheet
515
+ open={sheetOpen}
516
+ onOpenChange={setSheetOpen}
517
+ skillToEdit={skillToEdit}
518
+ onSaved={handleSaved}
519
+ />
520
+
521
+ <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
522
+ <AlertDialogContent>
523
+ <AlertDialogHeader>
524
+ <AlertDialogTitle>Excluir skill</AlertDialogTitle>
525
+ <AlertDialogDescription>
526
+ Tem certeza que deseja excluir a skill{' '}
527
+ <strong>{skillToDelete?.slug}</strong>? Esta ação não pode ser
528
+ desfeita.
529
+ </AlertDialogDescription>
530
+ </AlertDialogHeader>
531
+ <div className="flex justify-end gap-2">
532
+ <AlertDialogCancel disabled={isDeleting}>
533
+ Cancelar
534
+ </AlertDialogCancel>
535
+ <AlertDialogAction
536
+ onClick={handleDeleteConfirm}
537
+ disabled={isDeleting}
538
+ className="bg-red-600 hover:bg-red-700"
539
+ >
540
+ {isDeleting ? 'Excluindo...' : 'Excluir'}
541
+ </AlertDialogAction>
542
+ </div>
543
+ </AlertDialogContent>
544
+ </AlertDialog>
545
+ </Page>
546
+ );
547
+ }