@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
@@ -854,9 +854,7 @@ const getAulaSchema = (t: (key: string) => string) =>
854
854
  horaFim: z
855
855
  .string()
856
856
  .min(1, t('sheet.lessonForm.validation.endTimeRequired')),
857
- local: z
858
- .string()
859
- .min(1, t('sheet.lessonForm.validation.locationRequired')),
857
+ local: z.string().optional(),
860
858
  tipo: z.string().min(1, t('sheet.lessonForm.validation.typeRequired')),
861
859
  instrutorId: z.string().optional(),
862
860
  recurrenceFrequency: z
@@ -95,6 +95,7 @@ import {
95
95
  import { useTranslations } from 'next-intl';
96
96
  import { useRouter } from 'next/navigation';
97
97
  import { useEffect, useMemo, useRef, useState } from 'react';
98
+ import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
98
99
  import type { DateRange } from 'react-day-picker';
99
100
  import { Controller, useForm, useWatch } from 'react-hook-form';
100
101
  import { toast } from 'sonner';
@@ -226,6 +227,7 @@ type InstructorOption = {
226
227
  name: string;
227
228
  personId?: number;
228
229
  avatarId?: number | null;
230
+ userPhotoId?: number | null;
229
231
  qualificationSlugs?: string[];
230
232
  };
231
233
 
@@ -241,6 +243,8 @@ type InstructorApiRow = {
241
243
  person_id?: number | string;
242
244
  avatarId?: number | string | null;
243
245
  avatar_id?: number | string | null;
246
+ userPhotoId?: number | string | null;
247
+ user_photo_id?: number | string | null;
244
248
  qualificationSlugs?: string[];
245
249
  };
246
250
 
@@ -267,18 +271,39 @@ function normalizeInstructorOption(
267
271
  rawAvatarId !== undefined && rawAvatarId !== null
268
272
  ? Number(rawAvatarId) || null
269
273
  : null;
274
+ const rawUserPhotoId = item?.userPhotoId ?? item?.user_photo_id;
275
+ const userPhotoId =
276
+ rawUserPhotoId !== undefined && rawUserPhotoId !== null
277
+ ? Number(rawUserPhotoId) || null
278
+ : null;
270
279
 
271
280
  return {
272
281
  id,
273
282
  name,
274
283
  personId: Number(item?.personId ?? item?.person_id ?? 0) || undefined,
275
284
  avatarId,
285
+ userPhotoId,
276
286
  qualificationSlugs: Array.isArray(item?.qualificationSlugs)
277
287
  ? item.qualificationSlugs
278
288
  : undefined,
279
289
  };
280
290
  }
281
291
 
292
+ function getInstructorAvatarUrl(option?: {
293
+ userPhotoId?: number | null;
294
+ avatarId?: number | null;
295
+ }) {
296
+ if (option?.userPhotoId) {
297
+ return `${process.env.NEXT_PUBLIC_API_BASE_URL}/user/avatar/${option.userPhotoId}`;
298
+ }
299
+
300
+ if (option?.avatarId) {
301
+ return `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${option.avatarId}`;
302
+ }
303
+
304
+ return undefined;
305
+ }
306
+
282
307
  function getCourseIdByTitle(
283
308
  courses: Array<{ id: number; title: string }>,
284
309
  title: string
@@ -700,7 +725,11 @@ export default function TurmasPage() {
700
725
 
701
726
  // Pagination
702
727
  const [currentPage, setCurrentPage] = useState(1);
703
- const [pageSize, setPageSize] = useState(12);
728
+ const [pageSize, setPageSize] = usePersistedPageSize({
729
+ storageKey: 'pagination:global:pageSize',
730
+ defaultValue: 12,
731
+ allowedValues: PAGE_SIZES,
732
+ });
704
733
 
705
734
  // Double-click tracking
706
735
  const clickTimers = useRef<Map<number, ReturnType<typeof setTimeout>>>(
@@ -749,6 +778,7 @@ export default function TurmasPage() {
749
778
  params: {
750
779
  page: 1,
751
780
  pageSize: 500,
781
+ offeringTypes: 'scheduled,blended',
752
782
  },
753
783
  });
754
784
  return response.data;
@@ -2384,6 +2414,7 @@ export default function TurmasPage() {
2384
2414
  params: {
2385
2415
  page,
2386
2416
  pageSize,
2417
+ offeringTypes: 'scheduled,blended',
2387
2418
  ...(search.trim() ? { search: search.trim() } : {}),
2388
2419
  },
2389
2420
  });
@@ -2912,9 +2943,7 @@ export default function TurmasPage() {
2912
2943
  .slice(0, 2)
2913
2944
  .map((p) => p[0]?.toUpperCase() ?? '')
2914
2945
  .join('');
2915
- const avatarUrl = option.avatarId
2916
- ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${option.avatarId}`
2917
- : undefined;
2946
+ const avatarUrl = getInstructorAvatarUrl(option);
2918
2947
  return (
2919
2948
  <div className="flex min-w-0 items-center gap-3 py-0.5">
2920
2949
  <Avatar className="h-8 w-8 shrink-0 rounded-lg border border-border/60">
@@ -2937,9 +2966,7 @@ export default function TurmasPage() {
2937
2966
  .slice(0, 2)
2938
2967
  .map((p) => p[0]?.toUpperCase() ?? '')
2939
2968
  .join('');
2940
- const avatarUrl = option?.avatarId
2941
- ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${option.avatarId}`
2942
- : undefined;
2969
+ const avatarUrl = getInstructorAvatarUrl(option ?? undefined);
2943
2970
  return (
2944
2971
  <div className="flex items-center gap-2">
2945
2972
  <Avatar className="h-5 w-5 shrink-0 rounded">
@@ -13,7 +13,7 @@ import {
13
13
  SelectTrigger,
14
14
  SelectValue,
15
15
  } from '@/components/ui/select';
16
- import { Palette, Settings2 } from 'lucide-react';
16
+ import { Settings2 } from 'lucide-react';
17
17
  import type { UseFormReturn } from 'react-hook-form';
18
18
 
19
19
  import { CourseSectionCard } from './CourseSectionCard';
@@ -33,23 +33,6 @@ type CourseClassificationCardProps = {
33
33
  offeringTypes: readonly SelectOption[];
34
34
  };
35
35
 
36
- function ColorPreview({ color, label }: { color: string; label: string }) {
37
- return (
38
- <div className="flex items-center gap-2 rounded-lg border border-border/60 bg-muted/20 px-2.5 py-1.5">
39
- <span
40
- className="h-4 w-4 shrink-0 rounded-full border border-black/10"
41
- style={{ backgroundColor: color }}
42
- />
43
- <div className="min-w-0">
44
- <p className="text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
45
- {label}
46
- </p>
47
- <p className="truncate text-xs font-medium text-foreground">{color}</p>
48
- </div>
49
- </div>
50
- );
51
- }
52
-
53
36
  export function CourseClassificationCard({
54
37
  form,
55
38
  t,
@@ -57,10 +40,6 @@ export function CourseClassificationCard({
57
40
  statuses,
58
41
  offeringTypes,
59
42
  }: CourseClassificationCardProps) {
60
- const watchedPrimary = form.watch('primaryColor');
61
- const watchedSecondary = form.watch('secondaryColor');
62
- const watchedOfferingType = form.watch('tipoOferta');
63
-
64
43
  return (
65
44
  <CourseSectionCard
66
45
  title="Classificação e configuração"
@@ -156,7 +135,7 @@ export function CourseClassificationCard({
156
135
  <FormItem>
157
136
  <FormLabel>{t('form.fields.primaryColor.label')}</FormLabel>
158
137
  <FormControl>
159
- <div className="flex items-center gap-2 rounded-lg border border-border/60 bg-muted/20 px-2 py-1.5">
138
+ <div className="flex items-center gap-2">
160
139
  <Input
161
140
  type="color"
162
141
  value={field.value}
@@ -183,7 +162,7 @@ export function CourseClassificationCard({
183
162
  <FormItem>
184
163
  <FormLabel>{t('form.fields.secondaryColor.label')}</FormLabel>
185
164
  <FormControl>
186
- <div className="flex items-center gap-2 rounded-lg border border-border/60 bg-muted/20 px-2 py-1.5">
165
+ <div className="flex items-center gap-2">
187
166
  <Input
188
167
  type="color"
189
168
  value={field.value}
@@ -202,15 +181,6 @@ export function CourseClassificationCard({
202
181
  </FormItem>
203
182
  )}
204
183
  />
205
-
206
- <div className="space-y-2 rounded-lg border border-border/60 bg-linear-to-br from-muted/30 to-background p-3">
207
- <div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
208
- <Palette className="h-3.5 w-3.5" />
209
- Preview
210
- </div>
211
- <ColorPreview color={watchedPrimary} label="Primária" />
212
- <ColorPreview color={watchedSecondary} label="Secundária" />
213
- </div>
214
184
  </div>
215
185
  </CourseSectionCard>
216
186
  );
@@ -1,3 +1,4 @@
1
+ import { RichTextEditor } from '@/components/rich-text-editor';
1
2
  import {
2
3
  FormControl,
3
4
  FormField,
@@ -5,7 +6,6 @@ import {
5
6
  FormLabel,
6
7
  FormMessage,
7
8
  } from '@/components/ui/form';
8
- import { Textarea } from '@/components/ui/textarea';
9
9
  import { FileText } from 'lucide-react';
10
10
  import type { UseFormReturn } from 'react-hook-form';
11
11
 
@@ -31,10 +31,10 @@ export function CourseContentCard({ form }: CourseContentCardProps) {
31
31
  <FormItem>
32
32
  <FormLabel>Objetivos do curso</FormLabel>
33
33
  <FormControl>
34
- <Textarea
35
- {...field}
36
- rows={3}
37
- placeholder="Explique o que o aluno será capaz de fazer ao concluir o curso."
34
+ <RichTextEditor
35
+ value={field.value || ''}
36
+ onChange={field.onChange}
37
+ className="w-full max-w-full"
38
38
  />
39
39
  </FormControl>
40
40
  <FormMessage />
@@ -49,10 +49,10 @@ export function CourseContentCard({ form }: CourseContentCardProps) {
49
49
  <FormItem>
50
50
  <FormLabel>Público-alvo</FormLabel>
51
51
  <FormControl>
52
- <Textarea
53
- {...field}
54
- rows={3}
55
- placeholder="Descreva para quem este curso foi pensado."
52
+ <RichTextEditor
53
+ value={field.value || ''}
54
+ onChange={field.onChange}
55
+ className="w-full max-w-full"
56
56
  />
57
57
  </FormControl>
58
58
  <FormMessage />
@@ -0,0 +1,109 @@
1
+ import { RichTextEditor } from '@/components/rich-text-editor';
2
+ import {
3
+ FormControl,
4
+ FormField,
5
+ FormItem,
6
+ FormLabel,
7
+ FormMessage,
8
+ } from '@/components/ui/form';
9
+ import { Input } from '@/components/ui/input';
10
+ import { Hash, NotebookPen } from 'lucide-react';
11
+ import type { UseFormReturn } from 'react-hook-form';
12
+
13
+ import { CourseSectionCard } from './CourseSectionCard';
14
+ import type { CourseEditFormValues, TranslationFn } from './course-edit-types';
15
+
16
+ type CourseMainInfoCardProps = {
17
+ form: UseFormReturn<CourseEditFormValues>;
18
+ t: TranslationFn;
19
+ };
20
+
21
+ export function CourseMainInfoCard({ form, t }: CourseMainInfoCardProps) {
22
+ return (
23
+ <CourseSectionCard
24
+ title={t('form.sections.basicInfo')}
25
+ description="Os identificadores e a comunicação principal do curso ficam aqui."
26
+ icon={NotebookPen}
27
+ >
28
+ <div className="grid gap-3 md:grid-cols-2">
29
+ <FormField
30
+ control={form.control}
31
+ name="slug"
32
+ render={({ field }) => (
33
+ <FormItem>
34
+ <FormLabel>{t('form.fields.slug.label')}</FormLabel>
35
+ <FormControl>
36
+ <div className="relative">
37
+ <Hash className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
38
+ <Input
39
+ {...field}
40
+ className="pl-9 font-mono"
41
+ placeholder={t('form.fields.slug.placeholder')}
42
+ onChange={(event) =>
43
+ field.onChange(
44
+ String(event.target.value || '').toLowerCase()
45
+ )
46
+ }
47
+ />
48
+ </div>
49
+ </FormControl>
50
+ <FormMessage />
51
+ </FormItem>
52
+ )}
53
+ />
54
+
55
+ <FormField
56
+ control={form.control}
57
+ name="nomeInterno"
58
+ render={({ field }) => (
59
+ <FormItem>
60
+ <FormLabel>{t('form.fields.internalName.label')}</FormLabel>
61
+ <FormControl>
62
+ <Input
63
+ {...field}
64
+ placeholder={t('form.fields.internalName.placeholder')}
65
+ />
66
+ </FormControl>
67
+ <FormMessage />
68
+ </FormItem>
69
+ )}
70
+ />
71
+
72
+ <FormField
73
+ control={form.control}
74
+ name="tituloComercial"
75
+ render={({ field }) => (
76
+ <FormItem className="md:col-span-2">
77
+ <FormLabel>{t('form.fields.title.label')}</FormLabel>
78
+ <FormControl>
79
+ <Input
80
+ {...field}
81
+ placeholder={t('form.fields.title.placeholder')}
82
+ />
83
+ </FormControl>
84
+ <FormMessage />
85
+ </FormItem>
86
+ )}
87
+ />
88
+
89
+ <FormField
90
+ control={form.control}
91
+ name="descricaoPublica"
92
+ render={({ field }) => (
93
+ <FormItem className="md:col-span-2">
94
+ <FormLabel>{t('form.fields.description.label')}</FormLabel>
95
+ <FormControl>
96
+ <RichTextEditor
97
+ value={field.value}
98
+ onChange={field.onChange}
99
+ className="w-full max-w-full"
100
+ />
101
+ </FormControl>
102
+ <FormMessage />
103
+ </FormItem>
104
+ )}
105
+ />
106
+ </div>
107
+ </CourseSectionCard>
108
+ );
109
+ }
@@ -3,7 +3,7 @@ import { Badge } from '@/components/ui/badge';
3
3
  import { Button } from '@/components/ui/button';
4
4
  import { EntityPicker } from '@/components/ui/entity-picker';
5
5
  import { cn } from '@/lib/utils';
6
- import { Grip, Plus, X } from 'lucide-react';
6
+ import { Grip, Pencil, Plus, X } from 'lucide-react';
7
7
  import { useMemo, useState } from 'react';
8
8
 
9
9
  import type { PickerOption } from './course-edit-types';
@@ -28,6 +28,7 @@ type CourseMultiEntityPickerProps = {
28
28
  }>;
29
29
  mapSearchToCreateValues?: (search: string) => Record<string, string>;
30
30
  onChange: (nextValue: string[]) => void;
31
+ onEditSelected?: (option: PickerOption) => void;
31
32
  emptyHint?: string;
32
33
  renderOptionMeta?: boolean;
33
34
  variant?: 'default' | 'instructor';
@@ -57,6 +58,7 @@ export function CourseMultiEntityPicker({
57
58
  createFields,
58
59
  mapSearchToCreateValues,
59
60
  onChange,
61
+ onEditSelected,
60
62
  emptyHint,
61
63
  renderOptionMeta = false,
62
64
  variant = 'default',
@@ -192,18 +194,33 @@ export function CourseMultiEntityPicker({
192
194
  </div>
193
195
  </div>
194
196
 
195
- <Button
196
- type="button"
197
- variant="ghost"
198
- size="icon"
199
- className="h-8 w-8 rounded-lg text-muted-foreground hover:text-foreground"
200
- onClick={() =>
201
- onChange(value.filter((item) => item !== option.value))
202
- }
203
- aria-label={`Remover ${option.label}`}
204
- >
205
- <X className="h-4 w-4" />
206
- </Button>
197
+ <div className="flex items-center gap-1">
198
+ {onEditSelected ? (
199
+ <Button
200
+ type="button"
201
+ variant="ghost"
202
+ size="icon"
203
+ className="h-8 w-8 rounded-lg text-muted-foreground hover:text-foreground"
204
+ onClick={() => onEditSelected(option)}
205
+ aria-label={`Editar ${option.label}`}
206
+ >
207
+ <Pencil className="h-4 w-4" />
208
+ </Button>
209
+ ) : null}
210
+
211
+ <Button
212
+ type="button"
213
+ variant="ghost"
214
+ size="icon"
215
+ className="h-8 w-8 rounded-lg text-muted-foreground hover:text-foreground"
216
+ onClick={() =>
217
+ onChange(value.filter((item) => item !== option.value))
218
+ }
219
+ aria-label={`Remover ${option.label}`}
220
+ >
221
+ <X className="h-4 w-4" />
222
+ </Button>
223
+ </div>
207
224
  </div>
208
225
  ) : (
209
226
  <Badge
@@ -212,6 +229,16 @@ export function CourseMultiEntityPicker({
212
229
  className="flex items-center gap-1.5 rounded-full border border-border/60 bg-background px-3 py-1.5 text-xs font-medium text-foreground"
213
230
  >
214
231
  <span className="max-w-45 truncate">{option.label}</span>
232
+ {onEditSelected ? (
233
+ <button
234
+ type="button"
235
+ className="cursor-pointer text-muted-foreground transition-colors hover:text-foreground"
236
+ onClick={() => onEditSelected(option)}
237
+ aria-label={`Editar ${option.label}`}
238
+ >
239
+ <Pencil className="h-3.5 w-3.5" />
240
+ </button>
241
+ ) : null}
215
242
  <button
216
243
  type="button"
217
244
  className="cursor-pointer text-muted-foreground transition-colors hover:text-foreground"
@@ -1,3 +1,4 @@
1
+ import { RichTextEditor } from '@/components/rich-text-editor';
1
2
  import {
2
3
  FormControl,
3
4
  FormField,
@@ -5,7 +6,6 @@ import {
5
6
  FormLabel,
6
7
  FormMessage,
7
8
  } from '@/components/ui/form';
8
- import { Textarea } from '@/components/ui/textarea';
9
9
  import { Network } from 'lucide-react';
10
10
  import type { UseFormReturn } from 'react-hook-form';
11
11
 
@@ -26,6 +26,8 @@ type CourseRelationsCardProps = {
26
26
  values: Record<string, string>
27
27
  ) => Promise<PickerOption | null>;
28
28
  onCreateInstructor: () => void;
29
+ onEditCategory: (categorySlug: string) => void;
30
+ onEditInstructor: (instructorId: string) => void;
29
31
  };
30
32
 
31
33
  export function CourseRelationsCard({
@@ -35,6 +37,8 @@ export function CourseRelationsCard({
35
37
  instructorOptions,
36
38
  onCreateCategory,
37
39
  onCreateInstructor,
40
+ onEditCategory,
41
+ onEditInstructor,
38
42
  }: CourseRelationsCardProps) {
39
43
  return (
40
44
  <CourseSectionCard
@@ -43,83 +47,74 @@ export function CourseRelationsCard({
43
47
  icon={Network}
44
48
  >
45
49
  <div className="space-y-3">
46
- <FormField
47
- control={form.control}
48
- name="categorias"
49
- render={({ field }) => (
50
- <FormItem>
51
- <FormLabel>{t('form.fields.categories.label')}</FormLabel>
52
- <CourseMultiEntityPicker
53
- value={field.value}
54
- onChange={field.onChange}
55
- options={categoryOptions}
56
- placeholder={t('form.fields.categories.selectPlaceholder')}
57
- searchPlaceholder={t(
58
- 'form.fields.categories.searchPlaceholder'
59
- )}
60
- entityLabel="categoria"
61
- emptyStateDescription={t('form.fields.categories.noResults')}
62
- emptyHint="Nenhuma categoria vinculada ainda."
63
- createActionLabel="Criar categoria"
64
- createTitle="Criar categoria"
65
- createDescription="Cadastre uma categoria rapidamente sem sair da edição do curso."
66
- createFields={[
67
- {
68
- name: 'name',
69
- label: 'Nome da categoria',
70
- placeholder: 'Ex: Desenvolvimento Web',
71
- required: true,
72
- },
73
- {
74
- name: 'slug',
75
- label: 'Slug',
76
- placeholder: 'desenvolvimento-web',
77
- required: true,
78
- },
79
- ]}
80
- mapSearchToCreateValues={(search) => ({
81
- name: search,
82
- slug: search
83
- .trim()
84
- .toLowerCase()
85
- .normalize('NFD')
86
- .replace(/[\u0300-\u036f]/g, '')
87
- .replace(/[^a-z0-9]+/g, '-')
88
- .replace(/^-+|-+$/g, ''),
89
- })}
90
- onCreate={onCreateCategory}
91
- />
92
- <FormMessage />
93
- </FormItem>
94
- )}
95
- />
50
+ <div className="grid items-start gap-3 lg:grid-cols-2">
51
+ <FormField
52
+ control={form.control}
53
+ name="categorias"
54
+ render={({ field }) => (
55
+ <FormItem className="self-start">
56
+ <FormLabel>{t('form.fields.categories.label')}</FormLabel>
57
+ <CourseMultiEntityPicker
58
+ value={field.value}
59
+ onChange={field.onChange}
60
+ onEditSelected={(option) => onEditCategory(option.value)}
61
+ options={categoryOptions}
62
+ placeholder={t('form.fields.categories.selectPlaceholder')}
63
+ searchPlaceholder={t(
64
+ 'form.fields.categories.searchPlaceholder'
65
+ )}
66
+ entityLabel="categoria"
67
+ emptyStateDescription={t('form.fields.categories.noResults')}
68
+ emptyHint="Nenhuma categoria vinculada ainda."
69
+ createActionLabel="Criar categoria"
70
+ createTitle="Criar categoria"
71
+ createDescription="Cadastre uma categoria rapidamente sem sair da edição do curso."
72
+ createFields={[
73
+ {
74
+ name: 'name',
75
+ label: 'Nome da categoria',
76
+ placeholder: 'Ex: Desenvolvimento Web',
77
+ required: true,
78
+ },
79
+ ]}
80
+ mapSearchToCreateValues={(search) => ({
81
+ name: search,
82
+ })}
83
+ onCreate={onCreateCategory}
84
+ />
85
+ <FormMessage />
86
+ </FormItem>
87
+ )}
88
+ />
96
89
 
97
- <FormField
98
- control={form.control}
99
- name="instrutores"
100
- render={({ field }) => (
101
- <FormItem>
102
- <FormLabel>{t('form.fields.instructors.label')}</FormLabel>
103
- <CourseMultiEntityPicker
104
- value={field.value}
105
- onChange={field.onChange}
106
- options={instructorOptions}
107
- placeholder={t('form.fields.instructors.selectPlaceholder')}
108
- searchPlaceholder={t(
109
- 'form.fields.instructors.searchPlaceholder'
110
- )}
111
- entityLabel="instrutor"
112
- emptyStateDescription={t('form.fields.instructors.noResults')}
113
- emptyHint="Nenhum instrutor vinculado ainda."
114
- createActionLabel="Cadastrar instrutor"
115
- onCreateClick={onCreateInstructor}
116
- variant="instructor"
117
- renderOptionMeta
118
- />
119
- <FormMessage />
120
- </FormItem>
121
- )}
122
- />
90
+ <FormField
91
+ control={form.control}
92
+ name="instrutores"
93
+ render={({ field }) => (
94
+ <FormItem className="self-start">
95
+ <FormLabel>{t('form.fields.instructors.label')}</FormLabel>
96
+ <CourseMultiEntityPicker
97
+ value={field.value}
98
+ onChange={field.onChange}
99
+ onEditSelected={(option) => onEditInstructor(option.value)}
100
+ options={instructorOptions}
101
+ placeholder={t('form.fields.instructors.selectPlaceholder')}
102
+ searchPlaceholder={t(
103
+ 'form.fields.instructors.searchPlaceholder'
104
+ )}
105
+ entityLabel="instrutor"
106
+ emptyStateDescription={t('form.fields.instructors.noResults')}
107
+ emptyHint="Nenhum instrutor vinculado ainda."
108
+ createActionLabel="Cadastrar instrutor"
109
+ onCreateClick={onCreateInstructor}
110
+ variant="instructor"
111
+ renderOptionMeta
112
+ />
113
+ <FormMessage />
114
+ </FormItem>
115
+ )}
116
+ />
117
+ </div>
123
118
 
124
119
  <FormField
125
120
  control={form.control}
@@ -128,10 +123,10 @@ export function CourseRelationsCard({
128
123
  <FormItem>
129
124
  <FormLabel>{t('form.fields.prerequisites.label')}</FormLabel>
130
125
  <FormControl>
131
- <Textarea
132
- {...field}
133
- rows={3}
134
- placeholder={t('form.fields.prerequisites.placeholder')}
126
+ <RichTextEditor
127
+ value={field.value || ''}
128
+ onChange={field.onChange}
129
+ className="w-full max-w-full"
135
130
  />
136
131
  </FormControl>
137
132
  <FormMessage />