@hed-hog/lms 0.0.331 → 0.0.347

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 (135) hide show
  1. package/dist/class-group/class-group.controller.d.ts +8 -8
  2. package/dist/class-group/class-group.service.d.ts +8 -8
  3. package/dist/course/course.controller.d.ts +6 -1
  4. package/dist/course/course.controller.d.ts.map +1 -1
  5. package/dist/course/course.controller.js +19 -2
  6. package/dist/course/course.controller.js.map +1 -1
  7. package/dist/course/course.service.d.ts +6 -0
  8. package/dist/course/course.service.d.ts.map +1 -1
  9. package/dist/course/course.service.js +63 -28
  10. package/dist/course/course.service.js.map +1 -1
  11. package/dist/course/dto/create-course.dto.d.ts +1 -0
  12. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  13. package/dist/course/dto/create-course.dto.js +5 -0
  14. package/dist/course/dto/create-course.dto.js.map +1 -1
  15. package/dist/enterprise/enterprise.controller.d.ts +84 -12
  16. package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
  17. package/dist/enterprise/enterprise.controller.js +10 -0
  18. package/dist/enterprise/enterprise.controller.js.map +1 -1
  19. package/dist/enterprise/enterprise.service.d.ts +90 -12
  20. package/dist/enterprise/enterprise.service.d.ts.map +1 -1
  21. package/dist/enterprise/enterprise.service.js +413 -40
  22. package/dist/enterprise/enterprise.service.js.map +1 -1
  23. package/dist/enterprise/training/training-admin.controller.d.ts +9 -6
  24. package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
  25. package/dist/enterprise/training/training-admin.controller.js +10 -6
  26. package/dist/enterprise/training/training-admin.controller.js.map +1 -1
  27. package/dist/enterprise/training/training-admin.service.d.ts +11 -5
  28. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
  29. package/dist/enterprise/training/training-admin.service.js +108 -52
  30. package/dist/enterprise/training/training-admin.service.js.map +1 -1
  31. package/dist/enterprise/training/training-viewer.controller.d.ts +3 -0
  32. package/dist/enterprise/training/training-viewer.controller.d.ts.map +1 -1
  33. package/dist/evaluation/evaluation.controller.d.ts +2 -2
  34. package/dist/evaluation/evaluation.service.d.ts +2 -2
  35. package/dist/instructor/dto/create-instructor-skill.dto.d.ts +0 -4
  36. package/dist/instructor/dto/create-instructor-skill.dto.d.ts.map +1 -1
  37. package/dist/instructor/dto/create-instructor-skill.dto.js +0 -21
  38. package/dist/instructor/dto/create-instructor-skill.dto.js.map +1 -1
  39. package/dist/instructor/dto/update-instructor-skill.dto.d.ts +0 -4
  40. package/dist/instructor/dto/update-instructor-skill.dto.d.ts.map +1 -1
  41. package/dist/instructor/dto/update-instructor-skill.dto.js +0 -22
  42. package/dist/instructor/dto/update-instructor-skill.dto.js.map +1 -1
  43. package/dist/instructor/instructor-skill.controller.d.ts +4 -4
  44. package/dist/instructor/instructor-skill.service.d.ts +4 -7
  45. package/dist/instructor/instructor-skill.service.d.ts.map +1 -1
  46. package/dist/instructor/instructor-skill.service.js +2 -89
  47. package/dist/instructor/instructor-skill.service.js.map +1 -1
  48. package/dist/instructor/instructor.controller.d.ts +21 -0
  49. package/dist/instructor/instructor.controller.d.ts.map +1 -1
  50. package/dist/instructor/instructor.controller.js +19 -0
  51. package/dist/instructor/instructor.controller.js.map +1 -1
  52. package/dist/instructor/instructor.service.d.ts +27 -0
  53. package/dist/instructor/instructor.service.d.ts.map +1 -1
  54. package/dist/instructor/instructor.service.js +79 -25
  55. package/dist/instructor/instructor.service.js.map +1 -1
  56. package/dist/lms.module.d.ts.map +1 -1
  57. package/dist/lms.module.js.map +1 -1
  58. package/dist/training/dto/create-training.dto.d.ts +1 -0
  59. package/dist/training/dto/create-training.dto.d.ts.map +1 -1
  60. package/dist/training/dto/create-training.dto.js +5 -0
  61. package/dist/training/dto/create-training.dto.js.map +1 -1
  62. package/dist/training/training.controller.d.ts +4 -0
  63. package/dist/training/training.controller.d.ts.map +1 -1
  64. package/dist/training/training.service.d.ts +8 -0
  65. package/dist/training/training.service.d.ts.map +1 -1
  66. package/dist/training/training.service.js +71 -6
  67. package/dist/training/training.service.js.map +1 -1
  68. package/hedhog/data/route.yaml +23 -1
  69. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +80 -33
  70. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +3 -3
  71. package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +591 -0
  72. package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +6 -1
  73. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +39 -7
  74. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +1 -3
  75. package/hedhog/frontend/app/classes/page.tsx.ejs +34 -7
  76. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +3 -33
  77. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +9 -9
  78. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +109 -0
  79. package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +40 -13
  80. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +76 -81
  81. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +60 -0
  82. package/hedhog/frontend/app/courses/[id]/structure/_components/course-scheduled-classes-tab.tsx.ejs +406 -0
  83. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
  84. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
  85. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
  86. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
  87. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +243 -34
  88. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -0
  89. package/hedhog/frontend/app/courses/page.tsx.ejs +6 -1
  90. package/hedhog/frontend/app/enterprise/_components/enterprise-activity-timeline.tsx.ejs +87 -0
  91. package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +4 -0
  92. package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +31 -5
  93. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +79 -20
  94. package/hedhog/frontend/app/enterprise/_components/enterprise-company-identity-card.tsx.ejs +11 -2
  95. package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +201 -0
  96. package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +55 -24
  97. package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +430 -296
  98. package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +277 -0
  99. package/hedhog/frontend/app/enterprise/_components/enterprise-overview-analytics.tsx.ejs +205 -0
  100. package/hedhog/frontend/app/enterprise/_components/enterprise-person-edit-sheet.tsx.ejs +97 -0
  101. package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +82 -57
  102. package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +4 -0
  103. package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +60 -22
  104. package/hedhog/frontend/app/enterprise/_components/enterprise-types.ts.ejs +54 -0
  105. package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +211 -0
  106. package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -7
  107. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +31 -19
  108. package/hedhog/frontend/app/exams/page.tsx.ejs +6 -1
  109. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +51 -104
  110. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +625 -366
  111. package/hedhog/frontend/app/instructors/page.tsx.ejs +6 -1
  112. package/hedhog/frontend/app/paths/page.tsx.ejs +76 -8
  113. package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +6 -1
  114. package/hedhog/frontend/app/training/page.tsx.ejs +78 -9
  115. package/hedhog/frontend/messages/en.json +101 -10
  116. package/hedhog/frontend/messages/pt.json +115 -11
  117. package/hedhog/table/enterprise_student_license_event.yaml +30 -0
  118. package/hedhog/table/instructor_skill.yaml +0 -11
  119. package/hedhog/table/learning_path.yaml +4 -0
  120. package/package.json +6 -6
  121. package/src/course/course.controller.ts +18 -0
  122. package/src/course/course.service.ts +85 -26
  123. package/src/course/dto/create-course.dto.ts +4 -0
  124. package/src/enterprise/enterprise.controller.ts +5 -0
  125. package/src/enterprise/enterprise.service.ts +507 -29
  126. package/src/enterprise/training/training-admin.controller.ts +4 -0
  127. package/src/enterprise/training/training-admin.service.ts +115 -51
  128. package/src/instructor/dto/create-instructor-skill.dto.ts +0 -17
  129. package/src/instructor/dto/update-instructor-skill.dto.ts +0 -18
  130. package/src/instructor/instructor-skill.service.ts +2 -97
  131. package/src/instructor/instructor.controller.ts +16 -0
  132. package/src/instructor/instructor.service.ts +87 -10
  133. package/src/lms.module.ts +1 -0
  134. package/src/training/dto/create-training.dto.ts +4 -0
  135. package/src/training/training.service.ts +104 -5
@@ -56,6 +56,7 @@ import {
56
56
  } from 'lucide-react';
57
57
  import { useTranslations } from 'next-intl';
58
58
  import { useEffect, useState } from 'react';
59
+ import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
59
60
  import { toast } from 'sonner';
60
61
  import { InstructorFormSheet } from './_components/instructor-form-sheet';
61
62
  import type {
@@ -102,7 +103,11 @@ export default function InstructorsPage() {
102
103
  const t = useTranslations('lms.InstructorsPage');
103
104
 
104
105
  const [page, setPage] = useState(1);
105
- const [pageSize, setPageSize] = useState(12);
106
+ const [pageSize, setPageSize] = usePersistedPageSize({
107
+ storageKey: 'pagination:global:pageSize',
108
+ defaultValue: 12,
109
+ allowedValues: [6, 12, 24, 48],
110
+ });
106
111
  const [searchInput, setSearchInput] = useState('');
107
112
  const [debouncedSearch, setDebouncedSearch] = useState('');
108
113
  const [statusFilter, setStatusFilter] = useState('all');
@@ -64,6 +64,7 @@ import {
64
64
  TableRow,
65
65
  } from '@/components/ui/table';
66
66
  import { Textarea } from '@/components/ui/textarea';
67
+ import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
67
68
  import { usePersistedViewMode } from '@/hooks/use-persisted-view-mode';
68
69
  import {
69
70
  DndContext,
@@ -113,6 +114,7 @@ interface Formacao {
113
114
  id: number;
114
115
  nome: string;
115
116
  descricao: string;
117
+ progressionMode: 'sequencial' | 'livre';
116
118
  area: string;
117
119
  nivel: string;
118
120
  prerequisitos: string;
@@ -134,6 +136,9 @@ type TrainingColorPayload = {
134
136
  secondaryColor?: string | null;
135
137
  primary_color?: string | null;
136
138
  secondary_color?: string | null;
139
+ progressionMode?: string | null;
140
+ progressMode?: string | null;
141
+ progress_mode?: string | null;
137
142
  };
138
143
 
139
144
  interface CursoOption {
@@ -339,6 +344,14 @@ function normalizeLevelValue(value: string) {
339
344
  return 'Iniciante';
340
345
  }
341
346
 
347
+ function normalizeProgressModeValue(value?: string) {
348
+ const normalized = normalizeText(value ?? '');
349
+
350
+ if (['livre', 'free'].includes(normalized)) return 'livre';
351
+
352
+ return 'sequencial';
353
+ }
354
+
342
355
  function normalizeStatusValue(value: string) {
343
356
  const normalized = normalizeText(value);
344
357
 
@@ -390,6 +403,12 @@ function normalizeTrainingColorPayload<T extends TrainingColorPayload>(
390
403
 
391
404
  return {
392
405
  ...payload,
406
+ progressionMode: normalizeProgressModeValue(
407
+ payload.progressionMode ??
408
+ payload.progressMode ??
409
+ payload.progress_mode ??
410
+ undefined
411
+ ),
393
412
  primaryColor: primaryColor ?? undefined,
394
413
  secondaryColor: secondaryColor ?? undefined,
395
414
  };
@@ -421,7 +440,8 @@ function buildCourseCodeFromTitle(title: string) {
421
440
 
422
441
  const formacaoSchema = z.object({
423
442
  nome: z.string().min(3, 'Minimo 3 caracteres'),
424
- descricao: z.string().min(10, 'Minimo 10 caracteres'),
443
+ descricao: z.string().optional(),
444
+ progressionMode: z.enum(['sequencial', 'livre']),
425
445
  area: z.enum(['Tecnologia', 'Design', 'Gestao', 'Marketing', 'Financas']),
426
446
  nivel: z.enum(['Iniciante', 'Intermediario', 'Avancado']),
427
447
  prerequisitos: z.string().optional(),
@@ -565,7 +585,11 @@ export default function TrainingPage() {
565
585
 
566
586
  // Pagination
567
587
  const [currentPage, setCurrentPage] = useState(1);
568
- const [pageSize, setPageSize] = useState(12);
588
+ const [pageSize, setPageSize] = usePersistedPageSize({
589
+ storageKey: 'pagination:global:pageSize',
590
+ defaultValue: 12,
591
+ allowedValues: PAGE_SIZES,
592
+ });
569
593
 
570
594
  // Double-click tracking
571
595
  const clickTimers = useRef<Map<number, ReturnType<typeof setTimeout>>>(
@@ -723,6 +747,7 @@ export default function TrainingPage() {
723
747
  defaultValues: {
724
748
  nome: '',
725
749
  descricao: '',
750
+ progressionMode: 'sequencial' as const,
726
751
  area: 'Tecnologia',
727
752
  nivel: 'Iniciante',
728
753
  prerequisitos: '',
@@ -962,6 +987,7 @@ export default function TrainingPage() {
962
987
  form.reset({
963
988
  nome: '',
964
989
  descricao: '',
990
+ progressionMode: 'sequencial',
965
991
  area: 'Tecnologia',
966
992
  nivel: 'Iniciante',
967
993
  prerequisitos: '',
@@ -1017,6 +1043,9 @@ export default function TrainingPage() {
1017
1043
  form.reset({
1018
1044
  nome: fullFormacao.nome,
1019
1045
  descricao: fullFormacao.descricao,
1046
+ progressionMode: normalizeProgressModeValue(
1047
+ fullFormacao.progressionMode
1048
+ ),
1020
1049
  area: normalizeAreaValue(fullFormacao.area),
1021
1050
  nivel: normalizeLevelValue(fullFormacao.nivel),
1022
1051
  prerequisitos: fullFormacao.prerequisitos,
@@ -1166,6 +1195,9 @@ export default function TrainingPage() {
1166
1195
  }
1167
1196
 
1168
1197
  async function onSubmit(data: FormacaoForm) {
1198
+ const toApiProgressMode = (value: FormacaoForm['progressionMode']) =>
1199
+ value === 'livre' ? 'free' : 'sequential';
1200
+
1169
1201
  try {
1170
1202
  const orderedItems = normalizeTrailOrder(
1171
1203
  [...learningPathItems].sort((a, b) => a.order - b.order)
@@ -1191,6 +1223,7 @@ export default function TrainingPage() {
1191
1223
  shortDescription?: string;
1192
1224
  level?: 'beginner' | 'intermediate' | 'advanced';
1193
1225
  status?: 'draft' | 'active' | 'archived';
1226
+ progressMode?: 'sequential' | 'free';
1194
1227
  primaryColor?: string;
1195
1228
  secondaryColor?: string;
1196
1229
  items?: Array<{
@@ -1210,6 +1243,7 @@ export default function TrainingPage() {
1210
1243
  if (dirty.status) {
1211
1244
  payload.status = ptStatusToApi(data.status as Formacao['status']);
1212
1245
  }
1246
+ payload.progressMode = toApiProgressMode(data.progressionMode);
1213
1247
  if (dirty.primaryColor) payload.primaryColor = data.primaryColor;
1214
1248
  if (dirty.secondaryColor) payload.secondaryColor = data.secondaryColor;
1215
1249
  if (itemsChanged) {
@@ -1239,6 +1273,7 @@ export default function TrainingPage() {
1239
1273
  shortDescription: data.prerequisitos?.trim() || undefined,
1240
1274
  level: ptLevelToApi(data.nivel),
1241
1275
  status: ptStatusToApi(data.status as Formacao['status']),
1276
+ progressMode: toApiProgressMode(data.progressionMode),
1242
1277
  primaryColor: data.primaryColor,
1243
1278
  secondaryColor: data.secondaryColor,
1244
1279
  items: orderedItems.map((item, index) => ({
@@ -1893,18 +1928,51 @@ export default function TrainingPage() {
1893
1928
  <FieldError>{form.formState.errors.nome?.message}</FieldError>
1894
1929
  </Field>
1895
1930
  <Field>
1896
- <FieldLabel htmlFor="descricao">
1897
- {t('form.fields.description.label')}{' '}
1898
- <span className="text-destructive">*</span>
1931
+ <FieldLabel htmlFor="descricao">
1932
+ {t('form.fields.description.label')}
1899
1933
  </FieldLabel>
1900
1934
  <Textarea
1901
- id="descricao"
1935
+ id="descricao"
1902
1936
  rows={3}
1903
1937
  placeholder={t('form.fields.description.placeholder')}
1904
- {...form.register('descricao')}
1938
+ {...form.register('descricao')}
1939
+ />
1940
+ </Field>
1941
+ <Field>
1942
+ <FieldLabel>
1943
+ {t('form.fields.progressionMode.label')}{' '}
1944
+ <span className="text-destructive">*</span>
1945
+ </FieldLabel>
1946
+ <Controller
1947
+ name="progressionMode"
1948
+ control={form.control}
1949
+ render={({ field }) => (
1950
+ <Select onValueChange={field.onChange} value={field.value}>
1951
+ <SelectTrigger>
1952
+ <SelectValue
1953
+ placeholder={t(
1954
+ 'form.fields.progressionMode.placeholder'
1955
+ )}
1956
+ />
1957
+ </SelectTrigger>
1958
+ <SelectContent>
1959
+ <SelectItem value="sequencial">
1960
+ {t('form.fields.progressionMode.options.sequential')}
1961
+ </SelectItem>
1962
+ <SelectItem value="livre">
1963
+ {t('form.fields.progressionMode.options.free')}
1964
+ </SelectItem>
1965
+ </SelectContent>
1966
+ </Select>
1967
+ )}
1905
1968
  />
1969
+ <p className="text-xs text-muted-foreground">
1970
+ {watchedFormValues.progressionMode === 'livre'
1971
+ ? t('form.fields.progressionMode.help.free')
1972
+ : t('form.fields.progressionMode.help.sequential')}
1973
+ </p>
1906
1974
  <FieldError>
1907
- {form.formState.errors.descricao?.message}
1975
+ {form.formState.errors.progressionMode?.message}
1908
1976
  </FieldError>
1909
1977
  </Field>
1910
1978
  <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
@@ -61,6 +61,7 @@ import {
61
61
  import { useTranslations } from 'next-intl';
62
62
  import { useRouter } from 'next/navigation';
63
63
  import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
64
+ import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
64
65
  import type { DateRange } from 'react-day-picker';
65
66
 
66
67
  // ── Types ─────────────────────────────────────────────────────────────────────
@@ -360,7 +361,11 @@ export default function EvaluationsReportPage() {
360
361
 
361
362
  // Pagination
362
363
  const [currentPage, setCurrentPage] = useState(1);
363
- const [pageSize, setPageSize] = useState(12);
364
+ const [pageSize, setPageSize] = usePersistedPageSize({
365
+ storageKey: 'pagination:global:pageSize',
366
+ defaultValue: 12,
367
+ allowedValues: PAGE_SIZES,
368
+ });
364
369
 
365
370
  // Reset page when filters change
366
371
  useEffect(() => {
@@ -63,6 +63,7 @@ import {
63
63
  TableRow,
64
64
  } from '@/components/ui/table';
65
65
  import { Textarea } from '@/components/ui/textarea';
66
+ import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
66
67
  import { usePersistedViewMode } from '@/hooks/use-persisted-view-mode';
67
68
  import {
68
69
  DndContext,
@@ -112,6 +113,7 @@ interface Formacao {
112
113
  id: number;
113
114
  nome: string;
114
115
  descricao: string;
116
+ progressionMode: 'sequencial' | 'livre';
115
117
  area: string;
116
118
  nivel: string;
117
119
  prerequisitos: string;
@@ -343,6 +345,14 @@ function normalizeStatusValue(value: string) {
343
345
  return 'rascunho';
344
346
  }
345
347
 
348
+ function normalizeProgressModeValue(value?: string) {
349
+ const normalized = normalizeText(value ?? '');
350
+
351
+ if (['livre', 'free'].includes(normalized)) return 'livre';
352
+
353
+ return 'sequencial';
354
+ }
355
+
346
356
  function slugifyText(value: string) {
347
357
  return normalizeText(value)
348
358
  .replace(/[^a-z0-9\s-]/g, '')
@@ -416,7 +426,8 @@ function buildCourseCodeFromTitle(title: string) {
416
426
 
417
427
  const formacaoSchema = z.object({
418
428
  nome: z.string().min(3, 'Minimo 3 caracteres'),
419
- descricao: z.string().min(10, 'Minimo 10 caracteres'),
429
+ descricao: z.string().optional(),
430
+ progressionMode: z.enum(['sequencial', 'livre']),
420
431
  area: z.enum(['Tecnologia', 'Design', 'Gestao', 'Marketing', 'Financas']),
421
432
  nivel: z.enum(['Iniciante', 'Intermediario', 'Avancado']),
422
433
  prerequisitos: z.string().optional(),
@@ -560,7 +571,11 @@ export default function TrainingPage() {
560
571
 
561
572
  // Pagination
562
573
  const [currentPage, setCurrentPage] = useState(1);
563
- const [pageSize, setPageSize] = useState(12);
574
+ const [pageSize, setPageSize] = usePersistedPageSize({
575
+ storageKey: 'pagination:global:pageSize',
576
+ defaultValue: 12,
577
+ allowedValues: PAGE_SIZES,
578
+ });
564
579
 
565
580
  // Double-click tracking
566
581
  const clickTimers = useRef<Map<number, ReturnType<typeof setTimeout>>>(
@@ -716,6 +731,7 @@ export default function TrainingPage() {
716
731
  defaultValues: {
717
732
  nome: '',
718
733
  descricao: '',
734
+ progressionMode: 'sequencial',
719
735
  area: 'Tecnologia',
720
736
  nivel: 'Iniciante',
721
737
  prerequisitos: '',
@@ -867,6 +883,8 @@ export default function TrainingPage() {
867
883
  ...formacao,
868
884
  nome: watchedFormValues.nome ?? formacao.nome,
869
885
  descricao: watchedFormValues.descricao ?? formacao.descricao,
886
+ progressionMode:
887
+ watchedFormValues.progressionMode ?? formacao.progressionMode,
870
888
  nivel: watchedFormValues.nivel ?? formacao.nivel,
871
889
  status: watchedFormValues.status ?? formacao.status,
872
890
  prerequisitos:
@@ -955,6 +973,7 @@ export default function TrainingPage() {
955
973
  form.reset({
956
974
  nome: '',
957
975
  descricao: '',
976
+ progressionMode: 'sequencial',
958
977
  area: 'Tecnologia',
959
978
  nivel: 'Iniciante',
960
979
  prerequisitos: '',
@@ -1010,6 +1029,9 @@ export default function TrainingPage() {
1010
1029
  form.reset({
1011
1030
  nome: fullFormacao.nome,
1012
1031
  descricao: fullFormacao.descricao,
1032
+ progressionMode: normalizeProgressModeValue(
1033
+ fullFormacao.progressionMode
1034
+ ),
1013
1035
  area: normalizeAreaValue(fullFormacao.area),
1014
1036
  nivel: normalizeLevelValue(fullFormacao.nivel),
1015
1037
  prerequisitos: fullFormacao.prerequisitos,
@@ -1159,6 +1181,9 @@ export default function TrainingPage() {
1159
1181
  }
1160
1182
 
1161
1183
  async function onSubmit(data: FormacaoForm) {
1184
+ const toApiProgressMode = (value: FormacaoForm['progressionMode']) =>
1185
+ value === 'livre' ? 'free' : 'sequential';
1186
+
1162
1187
  try {
1163
1188
  const orderedItems = normalizeTrailOrder(
1164
1189
  [...learningPathItems].sort((a, b) => a.order - b.order)
@@ -1184,6 +1209,7 @@ export default function TrainingPage() {
1184
1209
  shortDescription?: string;
1185
1210
  level?: 'beginner' | 'intermediate' | 'advanced';
1186
1211
  status?: 'draft' | 'active' | 'archived';
1212
+ progressMode?: 'sequential' | 'free';
1187
1213
  primaryColor?: string;
1188
1214
  secondaryColor?: string;
1189
1215
  items?: Array<{
@@ -1195,7 +1221,9 @@ export default function TrainingPage() {
1195
1221
  } = {};
1196
1222
 
1197
1223
  if (dirty.nome) payload.title = data.nome;
1198
- if (dirty.descricao) payload.description = data.descricao;
1224
+ if (dirty.descricao) {
1225
+ payload.description = data.descricao?.trim() || undefined;
1226
+ }
1199
1227
  if (dirty.prerequisitos) {
1200
1228
  payload.shortDescription = data.prerequisitos?.trim() || undefined;
1201
1229
  }
@@ -1203,6 +1231,9 @@ export default function TrainingPage() {
1203
1231
  if (dirty.status) {
1204
1232
  payload.status = ptStatusToApi(data.status as Formacao['status']);
1205
1233
  }
1234
+ if (dirty.progressionMode) {
1235
+ payload.progressMode = toApiProgressMode(data.progressionMode);
1236
+ }
1206
1237
  if (dirty.primaryColor) payload.primaryColor = data.primaryColor;
1207
1238
  if (dirty.secondaryColor) payload.secondaryColor = data.secondaryColor;
1208
1239
  if (itemsChanged) {
@@ -1228,10 +1259,11 @@ export default function TrainingPage() {
1228
1259
  } else {
1229
1260
  const payload = {
1230
1261
  title: data.nome,
1231
- description: data.descricao,
1262
+ description: data.descricao?.trim() || undefined,
1232
1263
  shortDescription: data.prerequisitos?.trim() || undefined,
1233
1264
  level: ptLevelToApi(data.nivel),
1234
1265
  status: ptStatusToApi(data.status as Formacao['status']),
1266
+ progressMode: toApiProgressMode(data.progressionMode),
1235
1267
  primaryColor: data.primaryColor,
1236
1268
  secondaryColor: data.secondaryColor,
1237
1269
  items: orderedItems.map((item, index) => ({
@@ -1886,20 +1918,57 @@ export default function TrainingPage() {
1886
1918
  <FieldError>{form.formState.errors.nome?.message}</FieldError>
1887
1919
  </Field>
1888
1920
  <Field>
1889
- <FieldLabel htmlFor="descricao">
1890
- {t('form.fields.description.label')}{' '}
1891
- <span className="text-destructive">*</span>
1921
+ <FieldLabel htmlFor="descricao">
1922
+ {t('form.fields.description.label')}
1892
1923
  </FieldLabel>
1893
1924
  <Textarea
1894
- id="descricao"
1925
+ id="descricao"
1895
1926
  rows={3}
1927
+ required={false}
1896
1928
  placeholder={t('form.fields.description.placeholder')}
1897
- {...form.register('descricao')}
1929
+ {...form.register('descricao')}
1898
1930
  />
1899
1931
  <FieldError>
1900
1932
  {form.formState.errors.descricao?.message}
1901
1933
  </FieldError>
1902
1934
  </Field>
1935
+ <Field>
1936
+ <FieldLabel>
1937
+ {t('form.fields.progressionMode.label')}{' '}
1938
+ <span className="text-destructive">*</span>
1939
+ </FieldLabel>
1940
+ <Controller
1941
+ name="progressionMode"
1942
+ control={form.control}
1943
+ render={({ field }) => (
1944
+ <Select onValueChange={field.onChange} value={field.value}>
1945
+ <SelectTrigger>
1946
+ <SelectValue
1947
+ placeholder={t(
1948
+ 'form.fields.progressionMode.placeholder'
1949
+ )}
1950
+ />
1951
+ </SelectTrigger>
1952
+ <SelectContent>
1953
+ <SelectItem value="sequencial">
1954
+ {t('form.fields.progressionMode.options.sequential')}
1955
+ </SelectItem>
1956
+ <SelectItem value="livre">
1957
+ {t('form.fields.progressionMode.options.free')}
1958
+ </SelectItem>
1959
+ </SelectContent>
1960
+ </Select>
1961
+ )}
1962
+ />
1963
+ <p className="text-xs text-muted-foreground">
1964
+ {watchedFormValues.progressionMode === 'livre'
1965
+ ? t('form.fields.progressionMode.help.free')
1966
+ : t('form.fields.progressionMode.help.sequential')}
1967
+ </p>
1968
+ <FieldError>
1969
+ {form.formState.errors.progressionMode?.message}
1970
+ </FieldError>
1971
+ </Field>
1903
1972
  <div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
1904
1973
  <Field>
1905
1974
  <FieldLabel>
@@ -2907,6 +2907,11 @@
2907
2907
  "subFree": "{count} spots available",
2908
2908
  "subFull": "Full"
2909
2909
  },
2910
+ "nextClass": "Next Class",
2911
+ "noClass": "No class",
2912
+ "createNext": "Create next class",
2913
+ "calendar": "Calendar",
2914
+ "completed": "{count} completed",
2910
2915
  "avgAttendance": {
2911
2916
  "label": "Average Attendance",
2912
2917
  "sub": "overall attendance"
@@ -3538,10 +3543,92 @@
3538
3543
  "studentRemoveError": "Failed to remove student."
3539
3544
  },
3540
3545
  "form": {
3546
+ "title": {
3547
+ "create": "Create New Course",
3548
+ "edit": "Edit Course"
3549
+ },
3550
+ "description": {
3551
+ "create": "Fill in the details to create a new course. After creating, you will be redirected to the course page.",
3552
+ "edit": "Update the course information below."
3553
+ },
3554
+ "fields": {
3555
+ "internalName": {
3556
+ "label": "Internal Name",
3557
+ "placeholder": "Ex: Advanced React",
3558
+ "description": "Internal course name for administrative use"
3559
+ },
3560
+ "slug": {
3561
+ "label": "Slug",
3562
+ "placeholder": "Ex: advanced-react",
3563
+ "description": "Unique URL identifier (lowercase letters, numbers, and hyphens)"
3564
+ },
3565
+ "commercialTitle": {
3566
+ "label": "Commercial Title",
3567
+ "placeholder": "Ex: Advanced React"
3568
+ },
3569
+ "description": {
3570
+ "label": "Description",
3571
+ "placeholder": "Describe the course content and objectives..."
3572
+ },
3573
+ "level": {
3574
+ "label": "Level",
3575
+ "placeholder": "Select"
3576
+ },
3577
+ "status": {
3578
+ "label": "Status",
3579
+ "placeholder": "Select"
3580
+ },
3581
+ "categories": {
3582
+ "label": "Categories",
3583
+ "description": "Select one or more categories",
3584
+ "createAction": "Create category",
3585
+ "selectPlaceholder": "Select a category",
3586
+ "removeAction": "Remove category",
3587
+ "searchPlaceholder": "Type to search category...",
3588
+ "noResults": "No categories found",
3589
+ "selectedCount": "selected category(ies)"
3590
+ },
3591
+ "primaryColor": {
3592
+ "label": "Primary Color",
3593
+ "placeholder": "#1D4ED8"
3594
+ },
3595
+ "secondaryColor": {
3596
+ "label": "Secondary Color",
3597
+ "placeholder": "#111827"
3598
+ },
3599
+ "logo": {
3600
+ "label": "Course Logo",
3601
+ "alt": "Course logo",
3602
+ "upload": "Upload logo",
3603
+ "replace": "Replace logo",
3604
+ "remove": "Remove",
3605
+ "description": "Optional image to identify the course"
3606
+ }
3607
+ },
3608
+ "validation": {
3609
+ "internalNameMinLength": "Internal name must have at least 3 characters",
3610
+ "slugPattern": "Slug must contain only lowercase letters, numbers and hyphens"
3611
+ },
3612
+ "actions": {
3613
+ "cancel": "Cancel",
3614
+ "save": "Save Changes",
3615
+ "create": "Create Course"
3616
+ },
3541
3617
  "toasts": {
3542
3618
  "courseCreated": "Course created successfully! Redirecting...",
3543
3619
  "courseCreateError": "Failed to create the course."
3544
3620
  }
3621
+ },
3622
+ "levels": {
3623
+ "beginner": "Beginner",
3624
+ "intermediate": "Intermediate",
3625
+ "advanced": "Advanced"
3626
+ },
3627
+ "status": {
3628
+ "active": "Active",
3629
+ "inactive": "Inactive",
3630
+ "draft": "Draft",
3631
+ "archived": "Archived"
3545
3632
  }
3546
3633
  },
3547
3634
  "InstructorsPage": {
@@ -3652,9 +3739,17 @@
3652
3739
  "classes": {
3653
3740
  "emptyTitle": "No classes found",
3654
3741
  "emptyDescription": "This instructor is not linked to any classes yet.",
3742
+ "searchPlaceholder": "Search class by name, code or course...",
3743
+ "statusFilterPlaceholder": "Filter by status",
3744
+ "allStatuses": "All statuses",
3655
3745
  "start": "Start:",
3656
3746
  "end": "End:",
3657
- "students": "{count} student(s)"
3747
+ "students": "{count} student(s)",
3748
+ "studentsOfCapacity": "{count} / {total} student(s)",
3749
+ "actions": {
3750
+ "editAriaLabel": "Edit class",
3751
+ "openAriaLabel": "Open class page"
3752
+ }
3658
3753
  },
3659
3754
  "evaluations": {
3660
3755
  "comingSoonTitle": "Evaluations — feature coming soon",
@@ -3704,23 +3799,17 @@
3704
3799
  "current": "Instructor Skills"
3705
3800
  },
3706
3801
  "filters": {
3707
- "searchPlaceholder": "Search by slug or name..."
3802
+ "searchPlaceholder": "Search by slug..."
3708
3803
  },
3709
3804
  "table": {
3710
3805
  "slug": "Slug",
3711
- "namePt": "Name (PT)",
3712
- "nameEn": "Name (EN)",
3713
3806
  "status": "Status"
3714
3807
  },
3715
3808
  "form": {
3716
3809
  "slug": "Slug",
3717
3810
  "slugPlaceholder": "e.g. javascript, react-js",
3718
- "namePt": "Name (PT)",
3719
- "namePtPlaceholder": "Portuguese name",
3720
- "nameEn": "Name (EN)",
3721
- "nameEnPlaceholder": "Name in English (optional)",
3722
3811
  "status": "Status",
3723
- "statusPlaceholder": "Select status",
3812
+ "statusPlaceholder": "Select a status",
3724
3813
  "validation": {
3725
3814
  "required": "Required",
3726
3815
  "max100": "Maximum 100 characters",
@@ -3933,7 +4022,8 @@
3933
4022
  },
3934
4023
  "actions": {
3935
4024
  "create": "Create new class",
3936
- "add": "Add"
4025
+ "add": "Add",
4026
+ "edit": "Edit"
3937
4027
  },
3938
4028
  "empty": {
3939
4029
  "title": "No classes found.",
@@ -3941,6 +4031,7 @@
3941
4031
  },
3942
4032
  "table": {
3943
4033
  "class": "Class",
4034
+ "instructor": "Instructor",
3944
4035
  "status": "Status",
3945
4036
  "period": "Period",
3946
4037
  "capacity": "Capacity"