@hed-hog/lms 0.0.355 → 0.0.358

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 (115) hide show
  1. package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
  2. package/dist/course/course-audio-transcription.service.js +15 -7
  3. package/dist/course/course-audio-transcription.service.js.map +1 -1
  4. package/dist/course/course-operations-integration.service.d.ts +31 -0
  5. package/dist/course/course-operations-integration.service.d.ts.map +1 -1
  6. package/dist/course/course-operations-integration.service.js +286 -22
  7. package/dist/course/course-operations-integration.service.js.map +1 -1
  8. package/dist/course/course-operations.controller.d.ts +10 -0
  9. package/dist/course/course-operations.controller.d.ts.map +1 -0
  10. package/dist/course/course-operations.controller.js +67 -0
  11. package/dist/course/course-operations.controller.js.map +1 -0
  12. package/dist/course/course-structure.controller.d.ts +15 -1
  13. package/dist/course/course-structure.controller.d.ts.map +1 -1
  14. package/dist/course/course-structure.service.d.ts +25 -1
  15. package/dist/course/course-structure.service.d.ts.map +1 -1
  16. package/dist/course/course-structure.service.js +160 -24
  17. package/dist/course/course-structure.service.js.map +1 -1
  18. package/dist/course/course.module.d.ts.map +1 -1
  19. package/dist/course/course.module.js +4 -1
  20. package/dist/course/course.module.js.map +1 -1
  21. package/dist/course/course.service.d.ts +4 -2
  22. package/dist/course/course.service.d.ts.map +1 -1
  23. package/dist/course/course.service.js +61 -2
  24. package/dist/course/course.service.js.map +1 -1
  25. package/dist/course/dto/create-course-structure-lesson.dto.d.ts +3 -0
  26. package/dist/course/dto/create-course-structure-lesson.dto.d.ts.map +1 -1
  27. package/dist/course/dto/create-course-structure-lesson.dto.js +15 -0
  28. package/dist/course/dto/create-course-structure-lesson.dto.js.map +1 -1
  29. package/dist/course/dto/create-course-structure-session.dto.d.ts +1 -0
  30. package/dist/course/dto/create-course-structure-session.dto.d.ts.map +1 -1
  31. package/dist/course/dto/create-course-structure-session.dto.js +5 -0
  32. package/dist/course/dto/create-course-structure-session.dto.js.map +1 -1
  33. package/dist/course/dto/update-course-operations-config.dto.d.ts +6 -0
  34. package/dist/course/dto/update-course-operations-config.dto.d.ts.map +1 -0
  35. package/dist/course/dto/update-course-operations-config.dto.js +33 -0
  36. package/dist/course/dto/update-course-operations-config.dto.js.map +1 -0
  37. package/dist/course/lms-operations-task.subscriber.d.ts +13 -0
  38. package/dist/course/lms-operations-task.subscriber.d.ts.map +1 -0
  39. package/dist/course/lms-operations-task.subscriber.js +57 -0
  40. package/dist/course/lms-operations-task.subscriber.js.map +1 -0
  41. package/dist/enterprise/enterprise.service.js +1 -1
  42. package/dist/enterprise/enterprise.service.js.map +1 -1
  43. package/dist/enterprise/training/training-student.controller.d.ts +0 -95
  44. package/dist/enterprise/training/training-student.controller.d.ts.map +1 -1
  45. package/dist/enterprise/training/training-student.controller.js +1 -34
  46. package/dist/enterprise/training/training-student.controller.js.map +1 -1
  47. package/dist/enterprise/training/training-student.service.d.ts +63 -0
  48. package/dist/enterprise/training/training-student.service.d.ts.map +1 -1
  49. package/dist/enterprise/training/training-student.service.js +320 -4
  50. package/dist/enterprise/training/training-student.service.js.map +1 -1
  51. package/dist/instructor/instructor.service.d.ts.map +1 -1
  52. package/dist/instructor/instructor.service.js +12 -3
  53. package/dist/instructor/instructor.service.js.map +1 -1
  54. package/dist/lms.module.d.ts.map +1 -1
  55. package/dist/lms.module.js +2 -0
  56. package/dist/lms.module.js.map +1 -1
  57. package/dist/platforma/platforma.controller.d.ts +287 -0
  58. package/dist/platforma/platforma.controller.d.ts.map +1 -0
  59. package/dist/platforma/platforma.controller.js +147 -0
  60. package/dist/platforma/platforma.controller.js.map +1 -0
  61. package/hedhog/data/route.yaml +102 -9
  62. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -2
  63. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +10 -13
  64. package/hedhog/frontend/app/_lib/editor/templateSerializer.ts.ejs +26 -4
  65. package/hedhog/frontend/app/_lib/editor/types.ts.ejs +6 -0
  66. package/hedhog/frontend/app/certificates/models/CanvasStage.tsx.ejs +447 -31
  67. package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +59 -47
  68. package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +201 -35
  69. package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +36 -5
  70. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +2 -2
  71. package/hedhog/frontend/app/courses/[id]/_components/course-edit-types.ts.ejs +1 -1
  72. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +17 -6
  73. package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +382 -0
  74. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +45 -8
  75. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +177 -67
  76. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +25 -60
  77. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +1 -0
  78. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +11 -0
  79. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +22 -0
  80. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +79 -64
  81. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +31 -14
  82. package/hedhog/frontend/app/courses/[id]/structure/_components/types.ts.ejs +4 -4
  83. package/hedhog/frontend/app/courses/[id]/structure/_data/adapters/course-structure.adapter.ts.ejs +39 -27
  84. package/hedhog/frontend/app/courses/[id]/structure/_data/types/api-course.types.ts.ejs +24 -2
  85. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +41 -6
  86. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-query.ts.ejs +2 -2
  87. package/hedhog/frontend/app/courses/page.tsx.ejs +80 -103
  88. package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +1 -1
  89. package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +2 -2
  90. package/hedhog/frontend/app/enterprise/page.tsx.ejs +18 -6
  91. package/hedhog/frontend/app/exams/[id]/page.tsx.ejs +5 -4
  92. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +16 -10
  93. package/hedhog/frontend/app/paths/page.tsx.ejs +1 -1
  94. package/hedhog/frontend/app/training/page.tsx.ejs +1 -1
  95. package/hedhog/frontend/messages/en.json +7 -2
  96. package/hedhog/frontend/messages/pt.json +7 -2
  97. package/hedhog/table/course_lesson.yaml +2 -2
  98. package/hedhog/table/course_module.yaml +3 -0
  99. package/package.json +8 -8
  100. package/src/course/course-audio-transcription.service.ts +21 -8
  101. package/src/course/course-operations-integration.service.ts +460 -22
  102. package/src/course/course-operations.controller.ts +45 -0
  103. package/src/course/course-structure.service.ts +209 -4
  104. package/src/course/course.module.ts +4 -1
  105. package/src/course/course.service.ts +67 -1
  106. package/src/course/dto/create-course-structure-lesson.dto.ts +17 -0
  107. package/src/course/dto/create-course-structure-session.dto.ts +13 -1
  108. package/src/course/dto/update-course-operations-config.dto.ts +16 -0
  109. package/src/course/lms-operations-task.subscriber.ts +44 -0
  110. package/src/enterprise/enterprise.service.ts +1 -1
  111. package/src/enterprise/training/training-student.controller.ts +3 -27
  112. package/src/enterprise/training/training-student.service.ts +350 -2
  113. package/src/instructor/instructor.service.ts +12 -3
  114. package/src/lms.module.ts +2 -0
  115. package/src/platforma/platforma.controller.ts +92 -0
@@ -34,7 +34,7 @@ export interface CourseStructureCacheData {
34
34
  course: Course | null;
35
35
  sessions: Session[];
36
36
  lessons: Lesson[];
37
- instructors: { id: number; name: string }[];
37
+ instructors: { id: number; name: string; avatarId?: number | null }[];
38
38
  }
39
39
 
40
40
  // ─────────────────────────────────────────────────────────────────────────────
@@ -45,7 +45,7 @@ export interface CourseStructureQueryResult {
45
45
  course: Course | null;
46
46
  sessions: Session[];
47
47
  lessons: Lesson[];
48
- instructors: { id: number; name: string }[];
48
+ instructors: { id: number; name: string; avatarId?: number | null }[];
49
49
  isLoading: boolean;
50
50
  isFetching: boolean;
51
51
  isError: boolean;
@@ -73,7 +73,7 @@ import {
73
73
  import { useTranslations } from 'next-intl';
74
74
  import { useRouter } from 'next/navigation';
75
75
  import { useEffect, useMemo, useState } from 'react';
76
- import { useForm, useWatch } from 'react-hook-form';
76
+ import { useForm } from 'react-hook-form';
77
77
  import { toast } from 'sonner';
78
78
  import { CourseAvatar } from '../_components/course-avatar';
79
79
  import {
@@ -98,7 +98,7 @@ interface Curso {
98
98
  primaryColor?: string | null;
99
99
  secondaryColor?: string | null;
100
100
  nivel: 'iniciante' | 'intermediario' | 'avancado';
101
- status: 'ativo' | 'rascunho' | 'arquivado';
101
+ status: 'publicado' | 'rascunho' | 'arquivado';
102
102
  tipoCurso: 'on_demand' | 'agendado' | 'hibrido';
103
103
  categorias: string[];
104
104
  destaque: boolean;
@@ -214,11 +214,12 @@ function toPtStatus(status?: string | null): CourseSheetFormValues['status'] {
214
214
  const normalizedStatus = normalizeEnumValue(status);
215
215
 
216
216
  if (
217
+ normalizedStatus === 'publicado' ||
217
218
  normalizedStatus === 'ativo' ||
218
219
  normalizedStatus === 'active' ||
219
220
  normalizedStatus === 'published'
220
221
  ) {
221
- return 'ativo';
222
+ return 'publicado';
222
223
  }
223
224
  if (normalizedStatus === 'rascunho' || normalizedStatus === 'draft') {
224
225
  return 'rascunho';
@@ -264,7 +265,7 @@ function toApiLevel(level: CourseSheetFormValues['nivel']) {
264
265
  }
265
266
 
266
267
  function toApiStatus(status: CourseSheetFormValues['status']) {
267
- if (status === 'ativo') return 'published';
268
+ if (status === 'publicado') return 'published';
268
269
  if (status === 'rascunho') return 'draft';
269
270
  return 'archived';
270
271
  }
@@ -296,7 +297,7 @@ const NIVEL_COLOR: Record<string, string> = {
296
297
  };
297
298
 
298
299
  const STATUS_VARIANT: Record<string, 'default' | 'secondary' | 'outline'> = {
299
- ativo: 'default',
300
+ publicado: 'default',
300
301
  rascunho: 'secondary',
301
302
  arquivado: 'outline',
302
303
  };
@@ -347,7 +348,6 @@ export default function CursosPage() {
347
348
  const [newCategorySlugTouched, setNewCategorySlugTouched] = useState(false);
348
349
 
349
350
  // Data
350
- const [editingCurso, setEditingCurso] = useState<Curso | null>(null);
351
351
  const [cursoToDelete, setCursoToDelete] = useState<Curso | null>(null);
352
352
  const [cachedCourseList, setCachedCourseList] =
353
353
  useState<ApiCourseList | null>(null);
@@ -379,7 +379,6 @@ export default function CursosPage() {
379
379
  resolver: zodResolver(getCourseSheetSchema(t)),
380
380
  defaultValues: DEFAULT_COURSE_FORM_VALUES,
381
381
  });
382
- const watchedFormValues = useWatch({ control: form.control });
383
382
 
384
383
  const {
385
384
  data: courseList,
@@ -407,7 +406,7 @@ export default function CursosPage() {
407
406
  ...(filtroStatusInput !== 'todos'
408
407
  ? {
409
408
  status:
410
- filtroStatusInput === 'ativo'
409
+ filtroStatusInput === 'publicado'
411
410
  ? 'published'
412
411
  : filtroStatusInput === 'rascunho'
413
412
  ? 'draft'
@@ -528,29 +527,6 @@ export default function CursosPage() {
528
527
  () => (effectiveCourseList?.data ?? []).map((item) => mapApiCourse(item)),
529
528
  [effectiveCourseList]
530
529
  );
531
- const previewCurso = useMemo(() => {
532
- if (!sheetOpen || !editingCurso) return null;
533
-
534
- return {
535
- ...editingCurso,
536
- nomeInterno: watchedFormValues.nomeInterno ?? editingCurso.nomeInterno,
537
- tituloComercial:
538
- watchedFormValues.tituloComercial ?? editingCurso.tituloComercial,
539
- descricao: watchedFormValues.descricao ?? editingCurso.descricao,
540
- primaryColor: watchedFormValues.primaryColor ?? editingCurso.primaryColor,
541
- secondaryColor:
542
- watchedFormValues.secondaryColor ?? editingCurso.secondaryColor,
543
- nivel:
544
- (watchedFormValues.nivel as Curso['nivel'] | undefined) ??
545
- editingCurso.nivel,
546
- status:
547
- (watchedFormValues.status as Curso['status'] | undefined) ??
548
- editingCurso.status,
549
- tipoCurso: watchedFormValues.offeringType ?? editingCurso.tipoCurso,
550
- categorias: watchedFormValues.categorias ?? editingCurso.categorias,
551
- } satisfies Curso;
552
- }, [editingCurso, sheetOpen, watchedFormValues]);
553
-
554
530
  const categoryOptions = useMemo<CourseCategoryOption[]>(
555
531
  () =>
556
532
  (categoryListData?.data ?? [])
@@ -572,13 +548,20 @@ export default function CursosPage() {
572
548
  [categoryOptions]
573
549
  );
574
550
 
575
- const paginatedCursos = useMemo(() => {
576
- if (!previewCurso) return cursos;
577
-
578
- return cursos.map((curso) =>
579
- curso.id === previewCurso.id ? previewCurso : curso
580
- );
581
- }, [cursos, previewCurso]);
551
+ const paginatedCursos = cursos;
552
+ const selectedCourses = useMemo(
553
+ () => cursos.filter((curso) => selectedIds.has(curso.id)),
554
+ [cursos, selectedIds]
555
+ );
556
+ const selectedArchivedIds = useMemo(
557
+ () =>
558
+ selectedCourses
559
+ .filter((curso) => curso.status === 'arquivado')
560
+ .map((curso) => curso.id),
561
+ [selectedCourses]
562
+ );
563
+ const hasSelectedNonArchived =
564
+ selectedCourses.length > selectedArchivedIds.length;
582
565
  const totalPages = Math.max(1, effectiveCourseList?.lastPage ?? 1);
583
566
  const safePage = Math.min(currentPage, totalPages);
584
567
  function clearFilters() {
@@ -604,12 +587,18 @@ export default function CursosPage() {
604
587
 
605
588
  function getStatusLabel(curso: Curso) {
606
589
  return t(
607
- `status.${curso.status === 'ativo' ? 'active' : curso.status === 'rascunho' ? 'draft' : 'archived'}`
590
+ `status.${curso.status === 'publicado' ? 'published' : curso.status === 'rascunho' ? 'draft' : 'archived'}`
608
591
  );
609
592
  }
610
593
 
611
594
  function openDeleteDialog(curso: Curso, e: React.MouseEvent) {
612
595
  e.stopPropagation();
596
+
597
+ if (curso.status !== 'arquivado') {
598
+ toast.error(t('toasts.deleteOnlyArchived'));
599
+ return;
600
+ }
601
+
613
602
  setCursoToDelete(curso);
614
603
  setDeleteDialogOpen(true);
615
604
  }
@@ -632,32 +621,13 @@ export default function CursosPage() {
632
621
  // ── CRUD ──────────────────────────────────────────────────────────────────
633
622
 
634
623
  function openCreateSheet() {
635
- setEditingCurso(null);
636
624
  form.reset(DEFAULT_COURSE_FORM_VALUES);
637
625
  setSheetOpen(true);
638
626
  }
639
627
 
640
- function openEditSheet(curso: Curso, e?: React.MouseEvent) {
628
+ function goToCourseDetails(curso: Curso, e?: React.MouseEvent) {
641
629
  e?.stopPropagation();
642
- setEditingCurso(curso);
643
- form.reset({
644
- nomeInterno: curso.nomeInterno,
645
- slug: curso.slug,
646
- code: curso.code || '',
647
- tituloComercial: curso.tituloComercial,
648
- descricao: curso.descricao,
649
- primaryColor: curso.primaryColor || '#1D4ED8',
650
- secondaryColor: curso.secondaryColor || '#111827',
651
- nivel: curso.nivel,
652
- status: curso.status,
653
- offeringType: curso.tipoCurso,
654
- categorias: curso.categorias,
655
- operationsProjectId: curso.operationsProjectId
656
- ? String(curso.operationsProjectId)
657
- : '',
658
- logoFileId: curso.logoFileId ?? null,
659
- });
660
- setSheetOpen(true);
630
+ router.push(`/lms/courses/${curso.id}`);
661
631
  }
662
632
 
663
633
  async function onSubmit(data: CourseSheetFormValues) {
@@ -683,27 +653,15 @@ export default function CursosPage() {
683
653
  logoFileId: data.logoFileId ?? null,
684
654
  };
685
655
 
686
- if (editingCurso) {
687
- await request({
688
- url: `/lms/courses/${editingCurso.id}`,
689
- method: 'PATCH',
690
- data: payload,
691
- });
692
- toast.success(t('toasts.courseUpdated'));
693
- } else {
694
- const response = await request<ApiCourse>({
695
- url: '/lms/courses',
696
- method: 'POST',
697
- data: payload,
698
- });
656
+ const response = await request<ApiCourse>({
657
+ url: '/lms/courses',
658
+ method: 'POST',
659
+ data: payload,
660
+ });
699
661
 
700
- toast.success(t('toasts.courseCreated'));
701
- if (response.data?.id) {
702
- setTimeout(
703
- () => router.push(`/lms/courses/${response.data.id}`),
704
- 400
705
- );
706
- }
662
+ toast.success(t('toasts.courseCreated'));
663
+ if (response.data?.id) {
664
+ setTimeout(() => router.push(`/lms/courses/${response.data.id}`), 400);
707
665
  }
708
666
 
709
667
  await refetchCourses();
@@ -715,6 +673,11 @@ export default function CursosPage() {
715
673
 
716
674
  async function confirmDelete() {
717
675
  if (!cursoToDelete) return;
676
+ if (cursoToDelete.status !== 'arquivado') {
677
+ toast.error(t('toasts.deleteOnlyArchived'));
678
+ return;
679
+ }
680
+
718
681
  await request({
719
682
  url: `/lms/courses/${cursoToDelete.id}`,
720
683
  method: 'DELETE',
@@ -935,7 +898,7 @@ export default function CursosPage() {
935
898
  placeholder: t('table.headers.status'),
936
899
  options: [
937
900
  { value: 'todos', label: t('filters.allStatuses') },
938
- { value: 'ativo', label: t('status.active') },
901
+ { value: 'publicado', label: t('status.published') },
939
902
  { value: 'rascunho', label: t('status.draft') },
940
903
  { value: 'arquivado', label: t('status.archived') },
941
904
  ],
@@ -1043,13 +1006,24 @@ export default function CursosPage() {
1043
1006
  variant="secondary"
1044
1007
  className="gap-1.5 text-destructive hover:text-destructive"
1045
1008
  onClick={() => {
1009
+ if (selectedArchivedIds.length === 0) {
1010
+ toast.error(t('toasts.deleteOnlyArchived'));
1011
+ return;
1012
+ }
1013
+
1014
+ if (hasSelectedNonArchived) {
1015
+ toast.warning(t('toasts.bulkDeleteArchivedOnly'));
1016
+ }
1017
+
1046
1018
  Promise.all(
1047
- Array.from(selectedIds).map((id) =>
1019
+ selectedArchivedIds.map((id) =>
1048
1020
  request({ url: `/lms/courses/${id}`, method: 'DELETE' })
1049
1021
  )
1050
1022
  ).then(async () => {
1051
1023
  toast.success(
1052
- t('toasts.coursesRemoved', { count: selectedIds.size })
1024
+ t('toasts.coursesRemoved', {
1025
+ count: selectedArchivedIds.length,
1026
+ })
1053
1027
  );
1054
1028
  setSelectedIds(new Set());
1055
1029
  await refetchCourses();
@@ -1240,20 +1214,21 @@ export default function CursosPage() {
1240
1214
  <DropdownMenuItem
1241
1215
  onClick={(e) => {
1242
1216
  e.stopPropagation();
1243
- router.push(`/lms/courses/${curso.id}`);
1217
+ goToCourseDetails(curso, e);
1244
1218
  }}
1245
1219
  >
1246
1220
  <Eye className="mr-2 size-4" />{' '}
1247
1221
  {t('table.actions.viewDetails')}
1248
1222
  </DropdownMenuItem>
1249
1223
  <DropdownMenuItem
1250
- onClick={(e) => openEditSheet(curso, e)}
1224
+ onClick={(e) => goToCourseDetails(curso, e)}
1251
1225
  >
1252
1226
  <Pencil className="mr-2 size-4" />{' '}
1253
1227
  {t('table.actions.edit')}
1254
1228
  </DropdownMenuItem>
1255
1229
  <DropdownMenuSeparator />
1256
1230
  <DropdownMenuItem
1231
+ disabled={curso.status !== 'arquivado'}
1257
1232
  className="text-destructive focus:text-destructive"
1258
1233
  onClick={(e) => openDeleteDialog(curso, e)}
1259
1234
  >
@@ -1283,18 +1258,24 @@ export default function CursosPage() {
1283
1258
  <div className="mt-3 flex items-center gap-3 border-t border-border/50 pt-2.5 text-xs text-muted-foreground">
1284
1259
  <span className="flex items-center gap-1">
1285
1260
  <Layers className="size-3.5 shrink-0" />
1286
- <span className="font-medium text-foreground">{curso.quantidadeSessoes}</span>
1287
- {' '}sess.
1261
+ <span className="font-medium text-foreground">
1262
+ {curso.quantidadeSessoes}
1263
+ </span>{' '}
1264
+ sess.
1288
1265
  </span>
1289
1266
  <span className="flex items-center gap-1">
1290
1267
  <BookOpen className="size-3.5 shrink-0" />
1291
- <span className="font-medium text-foreground">{curso.quantidadeAulas}</span>
1292
- {' '}aulas
1268
+ <span className="font-medium text-foreground">
1269
+ {curso.quantidadeAulas}
1270
+ </span>{' '}
1271
+ aulas
1293
1272
  </span>
1294
1273
  <span className="flex items-center gap-1">
1295
1274
  <Paperclip className="size-3.5 shrink-0" />
1296
- <span className="font-medium text-foreground">{curso.quantidadeRecursos}</span>
1297
- {' '}rec.
1275
+ <span className="font-medium text-foreground">
1276
+ {curso.quantidadeRecursos}
1277
+ </span>{' '}
1278
+ rec.
1298
1279
  </span>
1299
1280
  </div>
1300
1281
  </CardContent>
@@ -1433,21 +1414,20 @@ export default function CursosPage() {
1433
1414
  </DropdownMenuTrigger>
1434
1415
  <DropdownMenuContent align="end" className="w-48">
1435
1416
  <DropdownMenuItem
1436
- onClick={() =>
1437
- router.push(`/lms/courses/${curso.id}`)
1438
- }
1417
+ onClick={(e) => goToCourseDetails(curso, e)}
1439
1418
  >
1440
1419
  <Eye className="mr-2 size-4" />{' '}
1441
1420
  {t('table.actions.viewDetails')}
1442
1421
  </DropdownMenuItem>
1443
1422
  <DropdownMenuItem
1444
- onClick={() => openEditSheet(curso)}
1423
+ onClick={(e) => goToCourseDetails(curso, e)}
1445
1424
  >
1446
1425
  <Pencil className="mr-2 size-4" />{' '}
1447
1426
  {t('table.actions.edit')}
1448
1427
  </DropdownMenuItem>
1449
1428
  <DropdownMenuSeparator />
1450
1429
  <DropdownMenuItem
1430
+ disabled={curso.status !== 'arquivado'}
1451
1431
  className="text-destructive focus:text-destructive"
1452
1432
  onClick={(e) => openDeleteDialog(curso, e)}
1453
1433
  >
@@ -1485,21 +1465,18 @@ export default function CursosPage() {
1485
1465
  </div>
1486
1466
  )}
1487
1467
 
1488
- {/* ── Create / Edit Sheet ────────────────────────────────────────────── */}
1468
+ {/* ── Create Sheet ───────────────────────────────────────────────────── */}
1489
1469
  <CourseFormSheet
1490
- key={editingCurso ? `edit-${editingCurso.id}` : 'new'}
1470
+ key="new"
1491
1471
  open={sheetOpen}
1492
- onOpenChange={(open) => {
1493
- setSheetOpen(open);
1494
- if (!open) setEditingCurso(null);
1495
- }}
1496
- editing={!!editingCurso}
1472
+ onOpenChange={setSheetOpen}
1473
+ editing={false}
1497
1474
  saving={saving}
1498
1475
  form={form}
1499
1476
  onSubmit={onSubmit}
1500
1477
  categories={categoryOptions}
1501
1478
  onCreateCategory={openCreateCategorySheet}
1502
- initialLogoFileId={editingCurso?.logoFileId ?? null}
1479
+ initialLogoFileId={null}
1503
1480
  t={t}
1504
1481
  />
1505
1482
 
@@ -23,7 +23,7 @@ function toApiLevel(level: CourseSheetFormValues['nivel']) {
23
23
  }
24
24
 
25
25
  function toApiStatus(status: CourseSheetFormValues['status']) {
26
- if (status === 'ativo') return 'published';
26
+ if (status === 'publicado') return 'published';
27
27
  if (status === 'rascunho') return 'draft';
28
28
  return 'archived';
29
29
  }
@@ -54,7 +54,7 @@ function toPtLevel(level?: string | null): CourseSheetFormValues['nivel'] {
54
54
  }
55
55
 
56
56
  function toPtStatus(status?: string | null): CourseSheetFormValues['status'] {
57
- if (status === 'published' || status === 'active') return 'ativo';
57
+ if (status === 'published' || status === 'active') return 'publicado';
58
58
  if (status === 'draft') return 'rascunho';
59
59
  if (status === 'archived') return 'arquivado';
60
60
  return 'rascunho';
@@ -67,7 +67,7 @@ function toApiLevel(level: CourseSheetFormValues['nivel']) {
67
67
  }
68
68
 
69
69
  function toApiStatus(status: CourseSheetFormValues['status']) {
70
- if (status === 'ativo') return 'published';
70
+ if (status === 'publicado') return 'published';
71
71
  if (status === 'rascunho') return 'draft';
72
72
  return 'archived';
73
73
  }
@@ -588,12 +588,24 @@ export default function EnterprisePage() {
588
588
 
589
589
  {/* CRM */}
590
590
  {account.crmAccountName && (
591
- <p className="truncate text-xs text-muted-foreground">
592
- <span className="font-medium">
593
- {t.has('labels.crm') ? t('labels.crm') : 'CRM'}:
594
- </span>{' '}
595
- {account.crmAccountName}
596
- </p>
591
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
592
+ <Avatar className="h-4 w-4 shrink-0 rounded-full">
593
+ <AvatarImage
594
+ src={getPersonAvatarUrl(account.crmAccount?.avatarId ?? null)}
595
+ alt={account.crmAccountName}
596
+ className="object-cover"
597
+ />
598
+ <AvatarFallback className="bg-slate-100 text-[8px] font-semibold uppercase text-slate-700">
599
+ {getInitials(account.crmAccountName)}
600
+ </AvatarFallback>
601
+ </Avatar>
602
+ <p className="truncate">
603
+ <span className="font-medium">
604
+ {t.has('labels.crm') ? t('labels.crm') : 'CRM'}:
605
+ </span>{' '}
606
+ {account.crmAccountName}
607
+ </p>
608
+ </div>
597
609
  )}
598
610
 
599
611
  {/* Stats grid */}
@@ -1,11 +1,12 @@
1
1
  import { redirect } from 'next/navigation';
2
2
 
3
3
  type ExamPageProps = {
4
- params: {
4
+ params: Promise<{
5
5
  id: string;
6
- };
6
+ }>;
7
7
  };
8
8
 
9
- export default function ExamPage({ params }: ExamPageProps) {
10
- redirect(`/lms/exams/${params.id}/questions`);
9
+ export default async function ExamPage({ params }: ExamPageProps) {
10
+ const { id } = await params;
11
+ redirect(`/lms/exams/${id}/questions`);
11
12
  }
@@ -325,7 +325,7 @@ export function InstructorFormSheet({
325
325
  }
326
326
 
327
327
  const params = new URLSearchParams({
328
- page: String(turmasPage),
328
+ page: String(Math.max(turmasPage, 1)),
329
329
  pageSize: String(TURMAS_PAGE_SIZE),
330
330
  });
331
331
 
@@ -350,12 +350,9 @@ export function InstructorFormSheet({
350
350
  placeholderData: (prev) => prev,
351
351
  });
352
352
  const turmasData = turmasResult?.data ?? [];
353
- const turmasLastPage = turmasResult?.lastPage ?? 1;
353
+ const turmasLastPage = Math.max(turmasResult?.lastPage ?? 1, 1);
354
354
 
355
- const {
356
- data: avaliacoesResult,
357
- isLoading: loadingAvaliacoes,
358
- } = useQuery<{
355
+ const { data: avaliacoesResult, isLoading: loadingAvaliacoes } = useQuery<{
359
356
  data: EvaluationItem[];
360
357
  total: number;
361
358
  lastPage: number;
@@ -399,6 +396,11 @@ export function InstructorFormSheet({
399
396
  useEffect(() => {
400
397
  if (turmasPage > turmasLastPage) {
401
398
  setTurmasPage(turmasLastPage);
399
+ return;
400
+ }
401
+
402
+ if (turmasPage < 1) {
403
+ setTurmasPage(1);
402
404
  }
403
405
  }, [turmasLastPage, turmasPage]);
404
406
 
@@ -1170,7 +1172,7 @@ export function InstructorFormSheet({
1170
1172
  variant="outline"
1171
1173
  size="icon"
1172
1174
  disabled={turmasPage <= 1}
1173
- onClick={() => setTurmasPage((p) => p - 1)}
1175
+ onClick={() => setTurmasPage((p) => Math.max(p - 1, 1))}
1174
1176
  >
1175
1177
  <ChevronLeft className="h-4 w-4" />
1176
1178
  </Button>
@@ -1182,7 +1184,9 @@ export function InstructorFormSheet({
1182
1184
  variant="outline"
1183
1185
  size="icon"
1184
1186
  disabled={turmasPage >= turmasLastPage}
1185
- onClick={() => setTurmasPage((p) => p + 1)}
1187
+ onClick={() =>
1188
+ setTurmasPage((p) => Math.min(p + 1, turmasLastPage))
1189
+ }
1186
1190
  >
1187
1191
  <ChevronRight className="h-4 w-4" />
1188
1192
  </Button>
@@ -1249,7 +1253,8 @@ export function InstructorFormSheet({
1249
1253
  />
1250
1254
  ))}
1251
1255
  <span className="ml-1 text-xs font-medium tabular-nums text-muted-foreground">
1252
- {ev.score.toFixed(1)}{t('form.evaluations.of')}10
1256
+ {ev.score.toFixed(1)}
1257
+ {t('form.evaluations.of')}10
1253
1258
  </span>
1254
1259
  </div>
1255
1260
  </div>
@@ -1272,7 +1277,8 @@ export function InstructorFormSheet({
1272
1277
  {/* Evaluator + date */}
1273
1278
  <div className="flex items-center gap-2 text-xs text-muted-foreground">
1274
1279
  <span>
1275
- {ev.evaluatorName ?? t('form.evaluations.anonymous')}
1280
+ {ev.evaluatorName ??
1281
+ t('form.evaluations.anonymous')}
1276
1282
  </span>
1277
1283
  <span>·</span>
1278
1284
  <span>
@@ -1146,7 +1146,7 @@ export default function TrainingPage() {
1146
1146
  }
1147
1147
 
1148
1148
  function courseStatusToApi(value: CourseSheetFormValues['status']) {
1149
- if (value === 'ativo') return 'published';
1149
+ if (value === 'publicado') return 'published';
1150
1150
  if (value === 'arquivado') return 'archived';
1151
1151
  return 'draft';
1152
1152
  }
@@ -1132,7 +1132,7 @@ export default function TrainingPage() {
1132
1132
  }
1133
1133
 
1134
1134
  function courseStatusToApi(value: CourseSheetFormValues['status']) {
1135
- if (value === 'ativo') return 'published';
1135
+ if (value === 'publicado') return 'published';
1136
1136
  if (value === 'arquivado') return 'archived';
1137
1137
  return 'draft';
1138
1138
  }
@@ -400,7 +400,7 @@
400
400
  "advanced": "Advanced"
401
401
  },
402
402
  "status": {
403
- "active": "Active",
403
+ "published": "Published",
404
404
  "draft": "Draft",
405
405
  "archived": "Archived"
406
406
  },
@@ -574,7 +574,7 @@
574
574
  "sub": "registered"
575
575
  },
576
576
  "activeCourses": {
577
- "label": "Active Courses",
577
+ "label": "Published Courses",
578
578
  "sub": "published"
579
579
  },
580
580
  "totalStudents": {
@@ -610,6 +610,7 @@
610
610
  "advanced": "Advanced"
611
611
  },
612
612
  "status": {
613
+ "published": "Published",
613
614
  "active": "Active",
614
615
  "draft": "Draft",
615
616
  "archived": "Archived"
@@ -771,6 +772,8 @@
771
772
  "courseRemoved": "Course \"{title}\" removed.",
772
773
  "coursesRemoved": "{count} course(s) removed.",
773
774
  "coursesArchived": "{count} course(s) archived.",
775
+ "deleteOnlyArchived": "Only archived courses can be deleted.",
776
+ "bulkDeleteArchivedOnly": "Only selected archived courses will be deleted.",
774
777
  "codeCopied": "Code \"{code}\" copied."
775
778
  },
776
779
  "StructurePage": {
@@ -942,6 +945,8 @@
942
945
  },
943
946
  "lessonRow": {
944
947
  "rename": "Rename lesson",
948
+ "draft": "Draft",
949
+ "published": "Published",
945
950
  "visibility": "Visibility: {value}",
946
951
  "status": "Status: {value}",
947
952
  "videoLinked": "Linked video",
@@ -409,7 +409,7 @@
409
409
  "advanced": "Avançado"
410
410
  },
411
411
  "status": {
412
- "active": "Ativo",
412
+ "published": "Publicado",
413
413
  "draft": "Rascunho",
414
414
  "archived": "Arquivado"
415
415
  },
@@ -583,7 +583,7 @@
583
583
  "sub": "cadastrados"
584
584
  },
585
585
  "activeCourses": {
586
- "label": "Cursos Ativos",
586
+ "label": "Cursos Publicados",
587
587
  "sub": "publicados"
588
588
  },
589
589
  "totalStudents": {
@@ -619,6 +619,7 @@
619
619
  "advanced": "Avançado"
620
620
  },
621
621
  "status": {
622
+ "published": "Publicado",
622
623
  "active": "Ativo",
623
624
  "draft": "Rascunho",
624
625
  "archived": "Arquivado"
@@ -780,6 +781,8 @@
780
781
  "courseRemoved": "Curso \"{title}\" removido.",
781
782
  "coursesRemoved": "{count} curso(s) removido(s).",
782
783
  "coursesArchived": "{count} curso(s) arquivado(s).",
784
+ "deleteOnlyArchived": "Apenas cursos arquivados podem ser excluídos.",
785
+ "bulkDeleteArchivedOnly": "Somente cursos arquivados selecionados serão excluídos.",
783
786
  "codeCopied": "Código \"{code}\" copiado."
784
787
  },
785
788
  "StructurePage": {
@@ -951,6 +954,8 @@
951
954
  },
952
955
  "lessonRow": {
953
956
  "rename": "Renomear aula",
957
+ "draft": "Rascunho",
958
+ "published": "Publicada",
954
959
  "visibility": "Visibilidade: {value}",
955
960
  "status": "Status: {value}",
956
961
  "videoLinked": "Vídeo vinculado",
@@ -26,9 +26,9 @@ columns:
26
26
  type: int
27
27
  isNullable: true
28
28
  - type: order
29
- - name: is_released
29
+ - name: published
30
30
  type: boolean
31
- default: true
31
+ default: false
32
32
  - type: created_at
33
33
  - type: updated_at
34
34