@hed-hog/lms 0.0.330 → 0.0.338

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 (128) hide show
  1. package/dist/class-group/class-group.controller.d.ts +3 -3
  2. package/dist/class-group/class-group.service.d.ts +3 -3
  3. package/dist/course/course.service.d.ts.map +1 -1
  4. package/dist/course/course.service.js +12 -20
  5. package/dist/course/course.service.js.map +1 -1
  6. package/dist/enterprise/enterprise.controller.d.ts +72 -0
  7. package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
  8. package/dist/enterprise/enterprise.controller.js +10 -0
  9. package/dist/enterprise/enterprise.controller.js.map +1 -1
  10. package/dist/enterprise/enterprise.service.d.ts +78 -0
  11. package/dist/enterprise/enterprise.service.d.ts.map +1 -1
  12. package/dist/enterprise/enterprise.service.js +413 -40
  13. package/dist/enterprise/enterprise.service.js.map +1 -1
  14. package/dist/enterprise/training/training-admin.controller.d.ts +6 -3
  15. package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
  16. package/dist/enterprise/training/training-admin.controller.js +10 -6
  17. package/dist/enterprise/training/training-admin.controller.js.map +1 -1
  18. package/dist/enterprise/training/training-admin.service.d.ts +8 -2
  19. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
  20. package/dist/enterprise/training/training-admin.service.js +108 -52
  21. package/dist/enterprise/training/training-admin.service.js.map +1 -1
  22. package/dist/enterprise/training/training-viewer.controller.d.ts +3 -0
  23. package/dist/enterprise/training/training-viewer.controller.d.ts.map +1 -1
  24. package/dist/evaluation/evaluation.controller.d.ts +4 -4
  25. package/dist/evaluation/evaluation.service.d.ts +4 -4
  26. package/dist/instructor/dto/create-instructor-skill.dto.d.ts +0 -4
  27. package/dist/instructor/dto/create-instructor-skill.dto.d.ts.map +1 -1
  28. package/dist/instructor/dto/create-instructor-skill.dto.js +0 -21
  29. package/dist/instructor/dto/create-instructor-skill.dto.js.map +1 -1
  30. package/dist/instructor/dto/update-instructor-skill.dto.d.ts +0 -4
  31. package/dist/instructor/dto/update-instructor-skill.dto.d.ts.map +1 -1
  32. package/dist/instructor/dto/update-instructor-skill.dto.js +0 -22
  33. package/dist/instructor/dto/update-instructor-skill.dto.js.map +1 -1
  34. package/dist/instructor/instructor-skill.controller.d.ts +4 -4
  35. package/dist/instructor/instructor-skill.service.d.ts +4 -7
  36. package/dist/instructor/instructor-skill.service.d.ts.map +1 -1
  37. package/dist/instructor/instructor-skill.service.js +2 -89
  38. package/dist/instructor/instructor-skill.service.js.map +1 -1
  39. package/dist/instructor/instructor.controller.d.ts +20 -0
  40. package/dist/instructor/instructor.controller.d.ts.map +1 -1
  41. package/dist/instructor/instructor.controller.js +19 -0
  42. package/dist/instructor/instructor.controller.js.map +1 -1
  43. package/dist/instructor/instructor.service.d.ts +25 -0
  44. package/dist/instructor/instructor.service.d.ts.map +1 -1
  45. package/dist/instructor/instructor.service.js +70 -18
  46. package/dist/instructor/instructor.service.js.map +1 -1
  47. package/dist/lms.module.d.ts.map +1 -1
  48. package/dist/lms.module.js.map +1 -1
  49. package/hedhog/data/route.yaml +23 -1
  50. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +42 -24
  51. package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +591 -0
  52. package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +6 -1
  53. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +7 -2
  54. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +17 -17
  55. package/hedhog/frontend/app/classes/page.tsx.ejs +6 -1
  56. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +3 -33
  57. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +9 -9
  58. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +109 -0
  59. package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +42 -15
  60. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +76 -81
  61. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +60 -0
  62. package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +3 -3
  63. package/hedhog/frontend/app/courses/[id]/structure/_components/course-scheduled-classes-tab.tsx.ejs +406 -0
  64. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +1 -1
  65. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
  66. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
  67. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
  68. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
  69. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +242 -33
  70. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +228 -152
  71. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -0
  72. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +71 -31
  73. package/hedhog/frontend/app/courses/page.tsx.ejs +6 -1
  74. package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +37 -41
  75. package/hedhog/frontend/app/enterprise/_components/enterprise-activity-timeline.tsx.ejs +87 -0
  76. package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +4 -0
  77. package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +31 -5
  78. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +79 -20
  79. package/hedhog/frontend/app/enterprise/_components/enterprise-company-identity-card.tsx.ejs +11 -2
  80. package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +201 -0
  81. package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +55 -24
  82. package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +430 -296
  83. package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +277 -0
  84. package/hedhog/frontend/app/enterprise/_components/enterprise-overview-analytics.tsx.ejs +205 -0
  85. package/hedhog/frontend/app/enterprise/_components/enterprise-person-edit-sheet.tsx.ejs +97 -0
  86. package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +82 -57
  87. package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +4 -0
  88. package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +60 -22
  89. package/hedhog/frontend/app/enterprise/_components/enterprise-types.ts.ejs +54 -0
  90. package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +211 -0
  91. package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -7
  92. package/hedhog/frontend/app/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +1 -1
  93. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +6 -1
  94. package/hedhog/frontend/app/exams/page.tsx.ejs +12 -3
  95. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +51 -104
  96. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +712 -427
  97. package/hedhog/frontend/app/instructors/page.tsx.ejs +77 -53
  98. package/hedhog/frontend/app/paths/page.tsx.ejs +14 -5
  99. package/hedhog/frontend/app/reports/courses/page.tsx.ejs +5 -5
  100. package/hedhog/frontend/app/reports/dashboard/page.tsx.ejs +8 -8
  101. package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +6 -1
  102. package/hedhog/frontend/app/reports/page.tsx.ejs +7 -7
  103. package/hedhog/frontend/app/reports/students/page.tsx.ejs +6 -6
  104. package/hedhog/frontend/app/training/page.tsx.ejs +8 -3
  105. package/hedhog/frontend/messages/en.json +394 -55
  106. package/hedhog/frontend/messages/pt.json +389 -48
  107. package/hedhog/frontend/widgets/active-classes-kpi.tsx.ejs +1 -1
  108. package/hedhog/frontend/widgets/active-courses-kpi.tsx.ejs +1 -1
  109. package/hedhog/frontend/widgets/approval-rate-kpi.tsx.ejs +1 -1
  110. package/hedhog/frontend/widgets/class-calendar.tsx.ejs +2 -2
  111. package/hedhog/frontend/widgets/completion-rate-kpi.tsx.ejs +1 -1
  112. package/hedhog/frontend/widgets/issued-certificates-kpi.tsx.ejs +1 -1
  113. package/hedhog/frontend/widgets/total-students-kpi.tsx.ejs +1 -1
  114. package/hedhog/table/enterprise_student_license_event.yaml +30 -0
  115. package/hedhog/table/instructor_qualification.yaml +1 -1
  116. package/hedhog/table/instructor_skill.yaml +0 -11
  117. package/package.json +8 -8
  118. package/src/course/course.service.ts +12 -24
  119. package/src/enterprise/enterprise.controller.ts +5 -0
  120. package/src/enterprise/enterprise.service.ts +507 -29
  121. package/src/enterprise/training/training-admin.controller.ts +4 -0
  122. package/src/enterprise/training/training-admin.service.ts +115 -51
  123. package/src/instructor/dto/create-instructor-skill.dto.ts +0 -17
  124. package/src/instructor/dto/update-instructor-skill.dto.ts +0 -18
  125. package/src/instructor/instructor-skill.service.ts +2 -97
  126. package/src/instructor/instructor.controller.ts +16 -0
  127. package/src/instructor/instructor.service.ts +85 -10
  128. package/src/lms.module.ts +1 -0
@@ -10,7 +10,9 @@ import type {
10
10
  import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
11
11
  import { Badge } from '@/components/ui/badge';
12
12
  import { Button } from '@/components/ui/button';
13
+ import { EntityPicker } from '@/components/ui/entity-picker';
13
14
  import { Field, FieldError, FieldLabel } from '@/components/ui/field';
15
+ import { Input } from '@/components/ui/input';
14
16
  import { InputMoney } from '@/components/ui/input-money';
15
17
  import {
16
18
  Select,
@@ -32,62 +34,51 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
32
34
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
33
35
  import { zodResolver } from '@hookform/resolvers/zod';
34
36
  import { useQueryClient } from '@tanstack/react-query';
35
- import { BookOpen, Loader2, Pencil, Star, X } from 'lucide-react';
36
- import { EntityPicker } from '@/components/ui/entity-picker';
37
+ import {
38
+ ArrowUpRight,
39
+ BookOpen,
40
+ ChevronLeft,
41
+ ChevronRight,
42
+ Loader2,
43
+ Pencil,
44
+ Search,
45
+ Star,
46
+ X,
47
+ } from 'lucide-react';
48
+ import { useTranslations } from 'next-intl';
49
+ import { useRouter } from 'next/navigation';
37
50
  import { useEffect, useState } from 'react';
38
51
  import { Controller, useForm } from 'react-hook-form';
39
52
  import { toast } from 'sonner';
40
53
  import { z } from 'zod';
54
+ import { ClassFormSheet } from '../../_components/class-form-sheet';
55
+ import { CourseAvatar } from '../../_components/course-avatar';
41
56
  import type { InstructorRow, InstructorSkill } from './instructor-types';
42
57
 
43
- const QUALIFICATION_OPTIONS = [
44
- {
45
- slug: 'course-lessons',
46
- label: 'Aulas de curso',
47
- description: 'Pode atuar em aulas gravadas e estrutura de curso.',
48
- },
49
- {
50
- slug: 'class-sessions',
51
- label: 'Sessões de turma',
52
- description: 'Pode atuar em contextos ao vivo e turmas.',
53
- },
54
- ];
55
-
56
- const instructorFormSchema = z.object({
57
- personId: z
58
- .number({ invalid_type_error: 'Selecione uma pessoa' })
59
- .int()
60
- .positive('Selecione uma pessoa'),
61
- qualificationSlugs: z
62
- .array(z.string())
63
- .min(1, 'Selecione pelo menos uma qualificação'),
64
- status: z.enum(['active', 'inactive']),
65
- can_teach_courses: z.boolean(),
66
- hourlyRate: z.coerce.number().min(0).optional().nullable(),
67
- skillSlugs: z.array(z.string()).optional().default([]),
68
- });
69
-
70
- type InstructorFormValues = z.infer<typeof instructorFormSchema>;
71
-
72
58
  type ClassGroupItem = {
73
59
  id: number;
74
60
  name: string;
75
61
  code: string;
62
+ courseLogoUrl: string | null;
63
+ courseLogoFileId?: number | null;
76
64
  courseName: string | null;
77
65
  startDate: string | null;
78
66
  endDate: string | null;
79
67
  status: string;
80
68
  slots: number;
69
+ totalSlots: number | null;
81
70
  };
82
71
 
83
72
  function toKebabCase(str: string) {
84
- return str
85
- .toLowerCase()
86
- .normalize('NFD')
87
- // biome-ignore lint: unicode diacritic strip
88
- .replace(/[̀-ͯ]/g, '')
89
- .replace(/[^a-z0-9]+/g, '-')
90
- .replace(/^-|-$/g, '');
73
+ return (
74
+ str
75
+ .toLowerCase()
76
+ .normalize('NFD')
77
+ // biome-ignore lint: unicode diacritic strip
78
+ .replace(/[̀-ͯ]/g, '')
79
+ .replace(/[^a-z0-9]+/g, '-')
80
+ .replace(/^-|-$/g, '')
81
+ );
91
82
  }
92
83
 
93
84
  function getSkillDisplayName(s: InstructorSkill): string {
@@ -95,13 +86,13 @@ function getSkillDisplayName(s: InstructorSkill): string {
95
86
  return locales?.[0]?.name ?? s.name ?? s.slug;
96
87
  }
97
88
 
98
- const STATUS_LABEL: Record<string, string> = {
99
- active: 'Ativo',
100
- inactive: 'Inativo',
101
- in_progress: 'Em andamento',
102
- upcoming: 'Futuro',
103
- completed: 'Concluído',
104
- cancelled: 'Cancelado',
89
+ type InstructorFormValues = {
90
+ personId?: number;
91
+ qualificationSlugs: string[];
92
+ status: 'active' | 'inactive';
93
+ can_teach_courses: boolean;
94
+ hourlyRate?: number | null;
95
+ skillSlugs?: string[];
105
96
  };
106
97
 
107
98
  type InstructorFormSheetProps = {
@@ -118,23 +109,66 @@ export function InstructorFormSheet({
118
109
  onSaved,
119
110
  }: InstructorFormSheetProps) {
120
111
  const { request, currentLocaleCode } = useApp();
112
+ const t = useTranslations('lms.InstructorsPage');
113
+ const router = useRouter();
121
114
  const queryClient = useQueryClient();
122
115
  const [saving, setSaving] = useState(false);
123
116
  const [selectedPersonName, setSelectedPersonName] = useState('');
124
- const [trainingAccess, setTrainingAccess] = useState<boolean | null>(null);
125
- const [togglingAccess, setTogglingAccess] = useState(false);
126
117
  const [editPersonOpen, setEditPersonOpen] = useState(false);
127
118
  const [personToEdit, setPersonToEdit] = useState<Person | null>(null);
128
119
  const [activeTab, setActiveTab] = useState('detalhes');
129
120
  const [tabSkillSlugs, setTabSkillSlugs] = useState<string[]>([]);
130
121
  const [savingSkills, setSavingSkills] = useState(false);
131
122
  const [pickerResetKey, setPickerResetKey] = useState(0);
123
+ const [turmasPage, setTurmasPage] = useState(1);
124
+ const [turmasSearchInput, setTurmasSearchInput] = useState('');
125
+ const [debouncedTurmasSearch, setDebouncedTurmasSearch] = useState('');
126
+ const [turmasStatusFilter, setTurmasStatusFilter] = useState('all');
127
+ const [classSheetOpen, setClassSheetOpen] = useState(false);
128
+ const [editingClassId, setEditingClassId] = useState<number | null>(null);
129
+ const TURMAS_PAGE_SIZE = 6;
132
130
  const isEditing = typeof instructorId === 'number' && instructorId > 0;
131
+ const qualificationOptions = [
132
+ {
133
+ slug: 'course-lessons',
134
+ label: t('qualificationLabels.courseLessons'),
135
+ description: t('form.qualificationDescriptions.courseLessons'),
136
+ },
137
+ {
138
+ slug: 'class-sessions',
139
+ label: t('qualificationLabels.classSessions'),
140
+ description: t('form.qualificationDescriptions.classSessions'),
141
+ },
142
+ ];
143
+ const statusLabel: Record<string, string> = {
144
+ active: t('form.fields.active'),
145
+ inactive: t('form.fields.inactive'),
146
+ open: t('form.classStatuses.upcoming'),
147
+ ongoing: t('form.classStatuses.inProgress'),
148
+ in_progress: t('form.classStatuses.inProgress'),
149
+ upcoming: t('form.classStatuses.upcoming'),
150
+ completed: t('form.classStatuses.completed'),
151
+ cancelled: t('form.classStatuses.cancelled'),
152
+ };
153
+ const instructorFormSchema = z.object({
154
+ personId: z
155
+ .number({ invalid_type_error: t('form.fields.personRequired') })
156
+ .int()
157
+ .positive(t('form.fields.personRequired')),
158
+ qualificationSlugs: z
159
+ .array(z.string())
160
+ .min(1, t('form.fields.qualificationsRequired')),
161
+ status: z.enum(['active', 'inactive']),
162
+ can_teach_courses: z.boolean(),
163
+ hourlyRate: z.coerce.number().min(0).optional().nullable(),
164
+ skillSlugs: z.array(z.string()).optional().default([]),
165
+ });
133
166
 
134
167
  const {
135
168
  control,
136
169
  handleSubmit,
137
170
  reset,
171
+ setValue,
138
172
  watch,
139
173
  formState: { errors },
140
174
  } = useForm<InstructorFormValues>({
@@ -147,7 +181,38 @@ export function InstructorFormSheet({
147
181
  },
148
182
  });
149
183
 
150
- const watchedPersonId = watch('personId');
184
+ const applyCanTeachCourses = (
185
+ currentSlugs: string[],
186
+ canTeachCourses: boolean
187
+ ) => {
188
+ const uniqueSlugs = [...new Set(currentSlugs)];
189
+
190
+ if (canTeachCourses) {
191
+ return uniqueSlugs.includes('course-lessons')
192
+ ? uniqueSlugs
193
+ : [...uniqueSlugs, 'course-lessons'];
194
+ }
195
+
196
+ const filtered = uniqueSlugs.filter((slug) => slug !== 'course-lessons');
197
+ return filtered.length > 0 ? filtered : ['class-sessions'];
198
+ };
199
+
200
+ const applyQualificationToggle = (
201
+ currentSlugs: string[],
202
+ slug: string,
203
+ enabled: boolean
204
+ ) => {
205
+ const uniqueSlugs = [...new Set(currentSlugs)];
206
+ const nextSlugs = enabled
207
+ ? uniqueSlugs.includes(slug)
208
+ ? uniqueSlugs
209
+ : [...uniqueSlugs, slug]
210
+ : uniqueSlugs.filter((value) => value !== slug);
211
+
212
+ return nextSlugs.length > 0
213
+ ? nextSlugs
214
+ : [slug === 'course-lessons' ? 'class-sessions' : 'course-lessons'];
215
+ };
151
216
 
152
217
  const { data: contactTypes = [] } = useQuery<ContactTypeOption[]>({
153
218
  queryKey: ['contact-person-contact-types', currentLocaleCode],
@@ -175,16 +240,20 @@ export function InstructorFormSheet({
175
240
  placeholderData: (previous) => previous ?? [],
176
241
  });
177
242
 
178
- const handleOpenPersonEdit = async () => {
179
- if (!watchedPersonId) return;
243
+ const handleEditSelection = async (personId: number) => {
180
244
  const response = await request<Person>({
181
- url: `/person/${watchedPersonId}`,
245
+ url: `/person/${personId}`,
182
246
  method: 'GET',
183
247
  });
184
248
  setPersonToEdit(response.data);
185
249
  setEditPersonOpen(true);
186
250
  };
187
251
 
252
+ const handleCreatePerson = () => {
253
+ setPersonToEdit(null);
254
+ setEditPersonOpen(true);
255
+ };
256
+
188
257
  const { data: existingInstructor } = useQuery<InstructorRow | null>({
189
258
  queryKey: ['lms-instructor-detail', instructorId],
190
259
  queryFn: async () => {
@@ -210,26 +279,73 @@ export function InstructorFormSheet({
210
279
  },
211
280
  });
212
281
 
213
- // Turmas — enterprise/training/admin endpoint supports instructorId filter
214
- const { data: turmasData = [], isLoading: loadingTurmas } = useQuery<
215
- ClassGroupItem[]
216
- >({
217
- queryKey: ['lms-instructor-turmas', instructorId],
282
+ // Turmas — paginated list filtered by instructorId
283
+ const {
284
+ data: turmasResult,
285
+ isLoading: loadingTurmas,
286
+ refetch: refetchTurmas,
287
+ } = useQuery<{
288
+ data: ClassGroupItem[];
289
+ total: number;
290
+ lastPage: number;
291
+ }>({
292
+ queryKey: [
293
+ 'lms-instructor-turmas',
294
+ instructorId,
295
+ turmasPage,
296
+ debouncedTurmasSearch,
297
+ turmasStatusFilter,
298
+ ],
218
299
  queryFn: async () => {
219
- const response = await request<
220
- { data?: ClassGroupItem[] } | ClassGroupItem[]
221
- >({
222
- url: `/lms/enterprise/training/admin/class-groups?instructorId=${instructorId}`,
300
+ if (!instructorId) {
301
+ return { data: [], total: 0, lastPage: 1 };
302
+ }
303
+
304
+ const params = new URLSearchParams({
305
+ page: String(turmasPage),
306
+ pageSize: String(TURMAS_PAGE_SIZE),
307
+ });
308
+
309
+ if (debouncedTurmasSearch) {
310
+ params.set('search', debouncedTurmasSearch);
311
+ }
312
+ if (turmasStatusFilter !== 'all') {
313
+ params.set('status', turmasStatusFilter);
314
+ }
315
+
316
+ const response = await request<{
317
+ data: ClassGroupItem[];
318
+ total: number;
319
+ lastPage: number;
320
+ }>({
321
+ url: `/lms/instructors/${instructorId}/class-groups?${params.toString()}`,
223
322
  method: 'GET',
224
323
  });
225
- const raw = response.data;
226
- if (Array.isArray(raw)) return raw;
227
- if (raw && 'data' in raw && Array.isArray(raw.data)) return raw.data;
228
- return [];
324
+ return response.data;
229
325
  },
230
326
  enabled: isEditing && open && activeTab === 'turmas',
231
- placeholderData: [],
327
+ placeholderData: (prev) => prev,
232
328
  });
329
+ const turmasData = turmasResult?.data ?? [];
330
+ const turmasLastPage = turmasResult?.lastPage ?? 1;
331
+
332
+ useEffect(() => {
333
+ const timeout = setTimeout(() => {
334
+ setDebouncedTurmasSearch(turmasSearchInput.trim());
335
+ setTurmasPage(1);
336
+ }, 300);
337
+ return () => clearTimeout(timeout);
338
+ }, [turmasSearchInput]);
339
+
340
+ useEffect(() => {
341
+ setTurmasPage(1);
342
+ }, [turmasStatusFilter]);
343
+
344
+ useEffect(() => {
345
+ if (turmasPage > turmasLastPage) {
346
+ setTurmasPage(turmasLastPage);
347
+ }
348
+ }, [turmasLastPage, turmasPage]);
233
349
 
234
350
  useEffect(() => {
235
351
  if (isEditing && existingInstructor) {
@@ -240,12 +356,12 @@ export function InstructorFormSheet({
240
356
  ? existingInstructor.qualificationSlugs
241
357
  : ['course-lessons'],
242
358
  status: existingInstructor.status,
243
- can_teach_courses: true,
359
+ can_teach_courses:
360
+ existingInstructor.qualificationSlugs.includes('course-lessons'),
244
361
  hourlyRate: existingInstructor.hourlyRate ?? null,
245
362
  skillSlugs: (existingInstructor.skills ?? []).map((s) => s.slug),
246
363
  });
247
364
  setSelectedPersonName(existingInstructor.name);
248
- setTrainingAccess(existingInstructor.hasTrainingAccess ?? false);
249
365
  setTabSkillSlugs((existingInstructor.skills ?? []).map((s) => s.slug));
250
366
  } else if (!isEditing && open) {
251
367
  reset({
@@ -257,16 +373,27 @@ export function InstructorFormSheet({
257
373
  skillSlugs: [],
258
374
  });
259
375
  setSelectedPersonName('');
260
- setTrainingAccess(null);
261
376
  setTabSkillSlugs([]);
262
377
  }
263
378
  }, [isEditing, existingInstructor, open, reset]);
264
379
 
265
- // Reset to first tab when sheet closes
380
+ // Reset to first tab and turmas page when sheet closes
266
381
  useEffect(() => {
267
- if (!open) setActiveTab('detalhes');
382
+ if (!open) {
383
+ setActiveTab('detalhes');
384
+ setTurmasPage(1);
385
+ setTurmasSearchInput('');
386
+ setDebouncedTurmasSearch('');
387
+ setTurmasStatusFilter('all');
388
+ setClassSheetOpen(false);
389
+ setEditingClassId(null);
390
+ }
268
391
  }, [open]);
269
392
 
393
+ useEffect(() => {
394
+ setTurmasPage(1);
395
+ }, [activeTab]);
396
+
270
397
  const handleSheetClose = (nextOpen: boolean) => {
271
398
  if (!nextOpen) {
272
399
  reset();
@@ -287,14 +414,30 @@ export function InstructorFormSheet({
287
414
  queryKey: ['lms-instructor-detail', instructorId],
288
415
  });
289
416
  await queryClient.invalidateQueries({ queryKey: ['lms-instructors'] });
290
- toast.success('Skills atualizadas com sucesso.');
417
+ toast.success(t('form.toasts.skillsSaved'));
291
418
  } catch {
292
- toast.error('Erro ao salvar skills.');
419
+ toast.error(t('form.toasts.skillsError'));
293
420
  } finally {
294
421
  setSavingSkills(false);
295
422
  }
296
423
  };
297
424
 
425
+ const handleEditClassQuick = (classId: number) => {
426
+ setEditingClassId(classId);
427
+ setClassSheetOpen(true);
428
+ };
429
+
430
+ const handleOpenClassPage = (classId: number) => {
431
+ router.push(`/lms/classes/${classId}`);
432
+ };
433
+
434
+ const handleClassSaved = async () => {
435
+ await Promise.all([
436
+ refetchTurmas(),
437
+ queryClient.invalidateQueries({ queryKey: ['lms-classes'] }),
438
+ ]);
439
+ };
440
+
298
441
  const onSubmit = async (values: InstructorFormValues) => {
299
442
  try {
300
443
  setSaving(true);
@@ -314,7 +457,7 @@ export function InstructorFormSheet({
314
457
  },
315
458
  });
316
459
  result = response.data;
317
- toast.success('Instrutor atualizado com sucesso.');
460
+ toast.success(t('form.toasts.instructorUpdated'));
318
461
  } else {
319
462
  const response = await request<InstructorRow>({
320
463
  url: '/lms/instructors',
@@ -329,16 +472,14 @@ export function InstructorFormSheet({
329
472
  },
330
473
  });
331
474
  result = response.data;
332
- toast.success('Instrutor criado com sucesso.');
475
+ toast.success(t('form.toasts.instructorCreated'));
333
476
  }
334
477
 
335
478
  await onSaved?.(result);
336
479
  handleSheetClose(false);
337
480
  } catch {
338
481
  toast.error(
339
- isEditing
340
- ? 'Erro ao atualizar instrutor. Tente novamente.'
341
- : 'Erro ao criar instrutor. Tente novamente.'
482
+ isEditing ? t('form.toasts.updateError') : t('form.toasts.createError')
342
483
  );
343
484
  } finally {
344
485
  setSaving(false);
@@ -354,56 +495,59 @@ export function InstructorFormSheet({
354
495
  >
355
496
  {/* ── Instructor header (edit mode) ── */}
356
497
  {isEditing && existingInstructor ? (
357
- <div className="flex shrink-0 items-center gap-4 border-b px-6 py-4">
358
- <Avatar className="h-14 w-14">
359
- <AvatarImage
360
- src={
361
- existingInstructor.avatarId
362
- ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${existingInstructor.avatarId}`
363
- : undefined
364
- }
365
- alt={existingInstructor.name}
366
- />
367
- <AvatarFallback>
368
- {existingInstructor.name.slice(0, 2).toUpperCase()}
369
- </AvatarFallback>
370
- </Avatar>
371
- <div className="min-w-0 flex-1">
372
- <h2 className="truncate text-lg font-semibold">
373
- {existingInstructor.name}
374
- </h2>
375
- <div className="mt-1 flex flex-wrap items-center gap-2">
376
- <Badge
377
- variant={
378
- existingInstructor.status === 'active'
379
- ? 'default'
380
- : 'secondary'
498
+ <>
499
+ <SheetHeader className="sr-only">
500
+ <SheetTitle>{t('form.title')}</SheetTitle>
501
+ <SheetDescription>{t('form.description')}</SheetDescription>
502
+ </SheetHeader>
503
+ <div className="flex shrink-0 items-center gap-4 border-b px-6 py-4">
504
+ <Avatar className="h-14 w-14">
505
+ <AvatarImage
506
+ src={
507
+ existingInstructor.avatarId
508
+ ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${existingInstructor.avatarId}`
509
+ : undefined
381
510
  }
382
- >
383
- {existingInstructor.status === 'active'
384
- ? 'Ativo'
385
- : 'Inativo'}
386
- </Badge>
387
- {existingInstructor.email && (
388
- <span className="truncate text-xs text-muted-foreground">
389
- {existingInstructor.email}
390
- </span>
391
- )}
392
- {existingInstructor.phone && (
393
- <span className="text-xs text-muted-foreground">
394
- {existingInstructor.phone}
395
- </span>
396
- )}
511
+ alt={existingInstructor.name}
512
+ />
513
+ <AvatarFallback>
514
+ {existingInstructor.name.slice(0, 2).toUpperCase()}
515
+ </AvatarFallback>
516
+ </Avatar>
517
+ <div className="min-w-0 flex-1">
518
+ <h2 className="truncate text-lg font-semibold">
519
+ {existingInstructor.name}
520
+ </h2>
521
+ <div className="mt-1 flex flex-wrap items-center gap-2">
522
+ <Badge
523
+ variant={
524
+ existingInstructor.status === 'active'
525
+ ? 'default'
526
+ : 'secondary'
527
+ }
528
+ >
529
+ {existingInstructor.status === 'active'
530
+ ? t('form.fields.active')
531
+ : t('form.fields.inactive')}
532
+ </Badge>
533
+ {existingInstructor.email && (
534
+ <span className="truncate text-xs text-muted-foreground">
535
+ {existingInstructor.email}
536
+ </span>
537
+ )}
538
+ {existingInstructor.phone && (
539
+ <span className="text-xs text-muted-foreground">
540
+ {existingInstructor.phone}
541
+ </span>
542
+ )}
543
+ </div>
397
544
  </div>
398
545
  </div>
399
- </div>
546
+ </>
400
547
  ) : (
401
548
  <SheetHeader className="shrink-0 border-b px-6 py-4">
402
- <SheetTitle>Novo Instrutor</SheetTitle>
403
- <SheetDescription>
404
- Vincule uma pessoa existente ou crie uma nova para cadastrá-la
405
- como instrutora.
406
- </SheetDescription>
549
+ <SheetTitle>{t('form.title')}</SheetTitle>
550
+ <SheetDescription>{t('form.description')}</SheetDescription>
407
551
  </SheetHeader>
408
552
  )}
409
553
 
@@ -415,10 +559,18 @@ export function InstructorFormSheet({
415
559
  className="flex min-h-0 flex-1 flex-col"
416
560
  >
417
561
  <TabsList className="mx-6 mt-3 shrink-0 justify-start">
418
- <TabsTrigger value="detalhes">Detalhes</TabsTrigger>
419
- <TabsTrigger value="skills">Skills</TabsTrigger>
420
- <TabsTrigger value="turmas">Turmas</TabsTrigger>
421
- <TabsTrigger value="avaliacoes">Avaliações</TabsTrigger>
562
+ <TabsTrigger value="detalhes">
563
+ {t('form.tabs.details')}
564
+ </TabsTrigger>
565
+ <TabsTrigger value="skills">
566
+ {t('form.tabs.skills')}
567
+ </TabsTrigger>
568
+ <TabsTrigger value="turmas">
569
+ {t('form.tabs.classes')}
570
+ </TabsTrigger>
571
+ <TabsTrigger value="avaliacoes">
572
+ {t('form.tabs.evaluations')}
573
+ </TabsTrigger>
422
574
  </TabsList>
423
575
 
424
576
  {/* ── Detalhes ── */}
@@ -435,99 +587,94 @@ export function InstructorFormSheet({
435
587
  {/* Person picker */}
436
588
  <Field>
437
589
  <FieldLabel>
438
- Pessoa <span className="text-destructive">*</span>
590
+ {t('form.fields.person')}{' '}
591
+ <span className="text-destructive">*</span>
439
592
  </FieldLabel>
440
- <div className="flex items-center gap-2">
441
- <div className="min-w-0 flex-1">
442
- <Controller
443
- control={control}
444
- name="personId"
445
- render={({ field }) => (
446
- <PersonPicker
447
- label=""
448
- entityLabel="pessoa"
449
- value={field.value ?? null}
450
- initialSelectedLabel={selectedPersonName}
451
- onChange={(personId, personName) => {
452
- field.onChange(personId ?? undefined);
453
- setSelectedPersonName(personName);
454
- }}
455
- selectPlaceholder="Buscar ou selecionar pessoa..."
456
- personTypeFilter="individual"
457
- createType="individual"
458
- lockCreateType
459
- />
460
- )}
461
- />
462
- </div>
463
- <Button
464
- type="button"
465
- variant="outline"
466
- size="icon"
467
- className="shrink-0"
468
- disabled={!watchedPersonId}
469
- onClick={handleOpenPersonEdit}
470
- aria-label="Editar cadastro da pessoa"
471
- title="Editar cadastro da pessoa"
472
- >
473
- <Pencil className="h-4 w-4" />
474
- </Button>
475
- </div>
476
- {errors.personId && (
477
- <FieldError>{errors.personId.message}</FieldError>
478
- )}
479
- </Field>
480
-
481
- {/* Status */}
482
- <Field>
483
- <FieldLabel>Status</FieldLabel>
484
- <Controller
485
- control={control}
486
- name="status"
487
- render={({ field }) => (
488
- <Select
489
- value={field.value}
490
- onValueChange={field.onChange}
491
- >
492
- <SelectTrigger className="w-full">
493
- <SelectValue placeholder="Selecione o status" />
494
- </SelectTrigger>
495
- <SelectContent>
496
- <SelectItem value="active">Ativo</SelectItem>
497
- <SelectItem value="inactive">
498
- Inativo
499
- </SelectItem>
500
- </SelectContent>
501
- </Select>
502
- )}
503
- />
504
- </Field>
505
-
506
- {/* Valor/hora */}
507
- <Field>
508
- <FieldLabel>Valor/hora (R$)</FieldLabel>
509
593
  <Controller
510
594
  control={control}
511
- name="hourlyRate"
595
+ name="personId"
512
596
  render={({ field }) => (
513
- <InputMoney
514
- placeholder="Ex: 150,00"
515
- value={field.value ?? ''}
516
- onValueChange={(value) => field.onChange(value)}
597
+ <PersonPicker
598
+ label=""
599
+ entityLabel={t('form.fields.person')}
600
+ value={field.value ?? null}
601
+ initialSelectedLabel={selectedPersonName}
602
+ onChange={(personId, personName) => {
603
+ field.onChange(personId ?? undefined);
604
+ setSelectedPersonName(personName);
605
+ }}
606
+ selectPlaceholder={t('form.fields.searchPerson')}
607
+ personTypeFilter="individual"
608
+ createType="individual"
609
+ lockCreateType
610
+ onCreateNew={handleCreatePerson}
611
+ showEditButton
612
+ onEditSelection={handleEditSelection}
613
+ editAriaLabel={t('form.fields.editPerson')}
517
614
  />
518
615
  )}
519
616
  />
617
+ {errors.personId && (
618
+ <FieldError>{errors.personId.message}</FieldError>
619
+ )}
520
620
  </Field>
521
621
 
622
+ {/* Status + Valor/hora */}
623
+ <div className="grid grid-cols-2 gap-4">
624
+ <Field>
625
+ <FieldLabel>{t('form.fields.status')}</FieldLabel>
626
+ <Controller
627
+ control={control}
628
+ name="status"
629
+ render={({ field }) => (
630
+ <Select
631
+ value={field.value}
632
+ onValueChange={field.onChange}
633
+ >
634
+ <SelectTrigger className="w-full">
635
+ <SelectValue
636
+ placeholder={t('filters.statusPlaceholder')}
637
+ />
638
+ </SelectTrigger>
639
+ <SelectContent>
640
+ <SelectItem value="active">
641
+ {t('form.fields.active')}
642
+ </SelectItem>
643
+ <SelectItem value="inactive">
644
+ {t('form.fields.inactive')}
645
+ </SelectItem>
646
+ </SelectContent>
647
+ </Select>
648
+ )}
649
+ />
650
+ </Field>
651
+
652
+ <Field>
653
+ <FieldLabel>{t('form.fields.hourlyRate')}</FieldLabel>
654
+ <Controller
655
+ control={control}
656
+ name="hourlyRate"
657
+ render={({ field }) => (
658
+ <InputMoney
659
+ placeholder={t(
660
+ 'form.fields.hourlyRatePlaceholder'
661
+ )}
662
+ value={field.value ?? ''}
663
+ onValueChange={(value) => field.onChange(value)}
664
+ />
665
+ )}
666
+ />
667
+ </Field>
668
+ </div>
669
+
522
670
  {/* can_teach_courses */}
523
671
  <div className="flex items-center justify-between rounded-lg border p-4">
524
672
  <div className="space-y-0.5">
525
673
  <p className="text-sm font-medium">
526
- Pode ensinar cursos
674
+ {t('form.fields.canTeachCourses')}
527
675
  </p>
528
676
  <p className="text-xs text-muted-foreground">
529
- Permite que o instrutor seja vinculado a aulas de
530
- curso.
677
+ {t('form.fields.canTeachCoursesHint')}
531
678
  </p>
532
679
  </div>
533
680
  <Controller
@@ -536,62 +683,31 @@ export function InstructorFormSheet({
536
683
  render={({ field }) => (
537
684
  <Switch
538
685
  checked={field.value}
539
- onCheckedChange={field.onChange}
686
+ onCheckedChange={(next) => {
687
+ field.onChange(next);
688
+ const nextSlugs = applyCanTeachCourses(
689
+ watch('qualificationSlugs') ?? [],
690
+ next
691
+ );
692
+ setValue('qualificationSlugs', nextSlugs, {
693
+ shouldDirty: true,
694
+ shouldTouch: true,
695
+ shouldValidate: true,
696
+ });
697
+ }}
540
698
  />
541
699
  )}
542
700
  />
543
701
  </div>
544
702
 
545
- {/* Acesso ao Training */}
546
- <div className="flex items-center justify-between rounded-lg border p-4">
547
- <div className="space-y-0.5">
548
- <p className="text-sm font-medium">
549
- Acesso ao Hcode Training
550
- </p>
551
- <p className="text-xs text-muted-foreground">
552
- {existingInstructor?.userId
553
- ? 'Permite que este instrutor acesse a plataforma com o perfil de instrutor.'
554
- : 'Nenhum usuário vinculado a este cadastro. Vincule um usuário para habilitar.'}
555
- </p>
556
- </div>
557
- <Switch
558
- checked={trainingAccess ?? false}
559
- disabled={
560
- !existingInstructor?.userId || togglingAccess
561
- }
562
- onCheckedChange={async (next) => {
563
- setTogglingAccess(true);
564
- try {
565
- await request({
566
- url: `/lms/instructors/${instructorId}/training-access`,
567
- method: 'PATCH',
568
- data: { enabled: next },
569
- });
570
- setTrainingAccess(next);
571
- toast.success(
572
- next
573
- ? 'Acesso ao Training habilitado.'
574
- : 'Acesso ao Training desabilitado.'
575
- );
576
- } catch {
577
- toast.error(
578
- 'Erro ao alterar acesso ao Training.'
579
- );
580
- } finally {
581
- setTogglingAccess(false);
582
- }
583
- }}
584
- />
585
- </div>
586
-
587
703
  {/* Qualificações */}
588
704
  <Field>
589
705
  <FieldLabel>
590
- Qualificações{' '}
706
+ {t('form.fields.qualifications')}{' '}
591
707
  <span className="text-destructive">*</span>
592
708
  </FieldLabel>
593
709
  <div className="space-y-3">
594
- {QUALIFICATION_OPTIONS.map((option) => (
710
+ {qualificationOptions.map((option) => (
595
711
  <Controller
596
712
  key={option.slug}
597
713
  control={control}
@@ -613,17 +729,19 @@ export function InstructorFormSheet({
613
729
  <Switch
614
730
  checked={checked}
615
731
  onCheckedChange={(next) => {
616
- if (next) {
617
- field.onChange([
618
- ...field.value,
732
+ const nextSlugs =
733
+ applyQualificationToggle(
734
+ field.value,
619
735
  option.slug,
620
- ]);
621
- } else {
622
- field.onChange(
623
- field.value.filter(
624
- (s) => s !== option.slug
625
- )
736
+ next
626
737
  );
738
+ field.onChange(nextSlugs);
739
+ if (option.slug === 'course-lessons') {
740
+ setValue('can_teach_courses', next, {
741
+ shouldDirty: true,
742
+ shouldTouch: true,
743
+ shouldValidate: false,
744
+ });
627
745
  }
628
746
  }}
629
747
  />
@@ -649,13 +767,13 @@ export function InstructorFormSheet({
649
767
  onClick={() => handleSheetClose(false)}
650
768
  disabled={saving}
651
769
  >
652
- Cancelar
770
+ {t('form.buttons.cancel')}
653
771
  </Button>
654
772
  <Button type="submit" disabled={saving}>
655
773
  {saving && (
656
774
  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
657
775
  )}
658
- Salvar alterações
776
+ {t('form.buttons.save')}
659
777
  </Button>
660
778
  </SheetFooter>
661
779
  </form>
@@ -669,7 +787,7 @@ export function InstructorFormSheet({
669
787
  <div className="flex min-h-0 flex-1 flex-col">
670
788
  <div className="flex-1 overflow-y-auto px-6 py-4">
671
789
  <p className="mb-4 text-sm text-muted-foreground">
672
- Gerencie as skills associadas a este instrutor.
790
+ {t('form.skills.description')}
673
791
  </p>
674
792
 
675
793
  {tabSkillSlugs.length > 0 ? (
@@ -691,7 +809,9 @@ export function InstructorFormSheet({
691
809
  prev.filter((s) => s !== slug)
692
810
  )
693
811
  }
694
- aria-label={`Remover skill ${skill?.name ?? slug}`}
812
+ aria-label={t('form.skills.removeSkill', {
813
+ skill: skill?.name ?? slug,
814
+ })}
695
815
  >
696
816
  <X className="h-3 w-3" />
697
817
  </button>
@@ -701,21 +821,21 @@ export function InstructorFormSheet({
701
821
  </div>
702
822
  ) : (
703
823
  <p className="mb-6 italic text-sm text-muted-foreground">
704
- Nenhuma skill atribuída.
824
+ {t('form.skills.none')}
705
825
  </p>
706
826
  )}
707
827
 
708
828
  <EntityPicker
709
829
  key={pickerResetKey}
710
- placeholder="Adicionar skill..."
830
+ placeholder={t('form.skills.addSkill')}
711
831
  value={null}
712
832
  clearable={false}
713
833
  valueType="string"
714
834
  showCreateButton
715
835
  entityLabel="skill"
716
- noResultsLabel="Nenhuma skill disponível"
717
- createTitle="Nova skill"
718
- createDescription="Cadastre uma nova skill para vincular ao instrutor."
836
+ noResultsLabel={t('form.skills.noSkills')}
837
+ createTitle={t('form.skills.newSkill')}
838
+ createDescription={t('form.skills.newSkillDescription')}
719
839
  options={skillsData
720
840
  .filter((s) => !tabSkillSlugs.includes(s.slug))
721
841
  .map((s) => ({
@@ -738,14 +858,18 @@ export function InstructorFormSheet({
738
858
  createFields={[
739
859
  {
740
860
  name: 'namePt',
741
- label: 'Nome',
861
+ label: t('form.skills.createFields.name'),
742
862
  required: true,
743
- placeholder: 'Ex: JavaScript',
863
+ placeholder: t(
864
+ 'form.skills.createFields.namePlaceholder'
865
+ ),
744
866
  },
745
867
  {
746
868
  name: 'slug',
747
- label: 'Slug (opcional)',
748
- placeholder: 'Ex: javascript',
869
+ label: t('form.skills.createFields.slug'),
870
+ placeholder: t(
871
+ 'form.skills.createFields.slugPlaceholder'
872
+ ),
749
873
  },
750
874
  ]}
751
875
  mapSearchToCreateValues={(search) => ({
@@ -775,7 +899,9 @@ export function InstructorFormSheet({
775
899
  name: namePt,
776
900
  } as any;
777
901
  } catch {
778
- toast.error('Erro ao criar skill.');
902
+ toast.error(
903
+ t('InstructorSkillsPage.messages.createError')
904
+ );
779
905
  return null;
780
906
  }
781
907
  }}
@@ -790,7 +916,7 @@ export function InstructorFormSheet({
790
916
  onClick={() => handleSheetClose(false)}
791
917
  disabled={savingSkills}
792
918
  >
793
- Cancelar
919
+ {t('form.buttons.cancel')}
794
920
  </Button>
795
921
  <Button
796
922
  type="button"
@@ -800,7 +926,7 @@ export function InstructorFormSheet({
800
926
  {savingSkills && (
801
927
  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
802
928
  )}
803
- Salvar skills
929
+ {t('form.buttons.saveSkills')}
804
930
  </Button>
805
931
  </div>
806
932
  </div>
@@ -812,66 +938,194 @@ export function InstructorFormSheet({
812
938
  value="turmas"
813
939
  className="flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
814
940
  >
815
- <div className="flex-1 overflow-y-auto px-6 py-4">
816
- {loadingTurmas ? (
817
- <div className="flex items-center justify-center py-12">
818
- <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
819
- </div>
820
- ) : turmasData.length === 0 ? (
821
- <div className="flex flex-col items-center justify-center py-12 text-center">
822
- <BookOpen className="mb-3 h-10 w-10 text-muted-foreground/40" />
823
- <p className="text-sm font-medium text-muted-foreground">
824
- Nenhuma turma encontrada
825
- </p>
826
- <p className="mt-1 text-xs text-muted-foreground">
827
- Este instrutor ainda não está vinculado a nenhuma turma.
828
- </p>
941
+ <div className="flex min-h-0 flex-1 flex-col">
942
+ <div className="grid shrink-0 grid-cols-1 gap-2 border-b px-6 py-3 sm:grid-cols-[minmax(0,1fr)_200px]">
943
+ <div className="relative">
944
+ <Search className="pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
945
+ <Input
946
+ value={turmasSearchInput}
947
+ onChange={(event) =>
948
+ setTurmasSearchInput(event.target.value)
949
+ }
950
+ placeholder={t('form.classes.searchPlaceholder')}
951
+ className="pl-8"
952
+ />
829
953
  </div>
830
- ) : (
831
- <div className="space-y-2">
832
- {turmasData.map((turma) => (
833
- <div key={turma.id} className="rounded-lg border p-3">
834
- <div className="flex items-start justify-between gap-2">
835
- <div className="min-w-0 flex-1">
836
- <p className="truncate text-sm font-medium">
837
- {turma.name}
838
- </p>
839
- {turma.courseName && (
840
- <p className="truncate text-xs text-muted-foreground">
841
- {turma.courseName}
842
- </p>
843
- )}
954
+
955
+ <Select
956
+ value={turmasStatusFilter}
957
+ onValueChange={setTurmasStatusFilter}
958
+ >
959
+ <SelectTrigger>
960
+ <SelectValue
961
+ placeholder={t(
962
+ 'form.classes.statusFilterPlaceholder'
963
+ )}
964
+ />
965
+ </SelectTrigger>
966
+ <SelectContent>
967
+ <SelectItem value="all">
968
+ {t('form.classes.allStatuses')}
969
+ </SelectItem>
970
+ <SelectItem value="open">
971
+ {t('form.classStatuses.upcoming')}
972
+ </SelectItem>
973
+ <SelectItem value="ongoing">
974
+ {t('form.classStatuses.inProgress')}
975
+ </SelectItem>
976
+ <SelectItem value="completed">
977
+ {t('form.classStatuses.completed')}
978
+ </SelectItem>
979
+ <SelectItem value="cancelled">
980
+ {t('form.classStatuses.cancelled')}
981
+ </SelectItem>
982
+ </SelectContent>
983
+ </Select>
984
+ </div>
985
+
986
+ <div className="flex-1 overflow-y-auto px-6 py-4">
987
+ {loadingTurmas ? (
988
+ <div className="flex items-center justify-center py-12">
989
+ <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
990
+ </div>
991
+ ) : turmasData.length === 0 ? (
992
+ <div className="flex flex-col items-center justify-center py-12 text-center">
993
+ <BookOpen className="mb-3 h-10 w-10 text-muted-foreground/40" />
994
+ <p className="text-sm font-medium text-muted-foreground">
995
+ {t('form.classes.emptyTitle')}
996
+ </p>
997
+ <p className="mt-1 text-xs text-muted-foreground">
998
+ {t('form.classes.emptyDescription')}
999
+ </p>
1000
+ </div>
1001
+ ) : (
1002
+ <div className="space-y-2">
1003
+ {turmasData.map((turma) => (
1004
+ <div key={turma.id} className="rounded-lg border p-3">
1005
+ <div className="flex items-start gap-3">
1006
+ <div className="shrink-0">
1007
+ <CourseAvatar
1008
+ fileId={turma.courseLogoFileId}
1009
+ title={turma.courseName ?? turma.name}
1010
+ className="h-10 w-10 rounded"
1011
+ iconSize="h-5 w-5"
1012
+ />
1013
+ </div>
1014
+ <div className="min-w-0 flex-1">
1015
+ <div className="flex items-start justify-between gap-2">
1016
+ <div className="min-w-0">
1017
+ <p className="truncate text-sm font-medium">
1018
+ {turma.name}
1019
+ </p>
1020
+ {turma.courseName && (
1021
+ <p className="truncate text-xs text-muted-foreground">
1022
+ {turma.courseName}
1023
+ </p>
1024
+ )}
1025
+ </div>
1026
+ <div className="flex shrink-0 items-center gap-1">
1027
+ <Button
1028
+ type="button"
1029
+ variant="ghost"
1030
+ size="icon"
1031
+ className="h-7 w-7"
1032
+ onClick={() =>
1033
+ handleEditClassQuick(turma.id)
1034
+ }
1035
+ aria-label={t(
1036
+ 'form.classes.actions.editAriaLabel'
1037
+ )}
1038
+ title={t(
1039
+ 'form.classes.actions.editAriaLabel'
1040
+ )}
1041
+ >
1042
+ <Pencil className="h-3.5 w-3.5" />
1043
+ </Button>
1044
+ <Button
1045
+ type="button"
1046
+ variant="ghost"
1047
+ size="icon"
1048
+ className="h-7 w-7"
1049
+ onClick={() =>
1050
+ handleOpenClassPage(turma.id)
1051
+ }
1052
+ aria-label={t(
1053
+ 'form.classes.actions.openAriaLabel'
1054
+ )}
1055
+ title={t(
1056
+ 'form.classes.actions.openAriaLabel'
1057
+ )}
1058
+ >
1059
+ <ArrowUpRight className="h-3.5 w-3.5" />
1060
+ </Button>
1061
+ <Badge
1062
+ variant="outline"
1063
+ className="shrink-0 text-xs"
1064
+ >
1065
+ {statusLabel[turma.status] ??
1066
+ turma.status}
1067
+ </Badge>
1068
+ </div>
1069
+ </div>
1070
+ <div className="mt-1.5 flex flex-wrap gap-x-4 gap-y-0.5 text-xs text-muted-foreground">
1071
+ {turma.startDate && (
1072
+ <span>
1073
+ {t('form.classes.start')}{' '}
1074
+ {new Date(
1075
+ turma.startDate
1076
+ ).toLocaleDateString('pt-BR')}
1077
+ </span>
1078
+ )}
1079
+ {turma.endDate && (
1080
+ <span>
1081
+ {t('form.classes.end')}{' '}
1082
+ {new Date(
1083
+ turma.endDate
1084
+ ).toLocaleDateString('pt-BR')}
1085
+ </span>
1086
+ )}
1087
+ <span>
1088
+ {turma.totalSlots != null
1089
+ ? t('form.classes.studentsOfCapacity', {
1090
+ count: turma.slots,
1091
+ total: turma.totalSlots,
1092
+ })
1093
+ : t('form.classes.students', {
1094
+ count: turma.slots,
1095
+ })}
1096
+ </span>
1097
+ </div>
1098
+ </div>
844
1099
  </div>
845
- <Badge
846
- variant="outline"
847
- className="shrink-0 text-xs"
848
- >
849
- {STATUS_LABEL[turma.status] ?? turma.status}
850
- </Badge>
851
- </div>
852
- <div className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
853
- {turma.startDate && (
854
- <span>
855
- Início:{' '}
856
- {new Date(turma.startDate).toLocaleDateString(
857
- 'pt-BR'
858
- )}
859
- </span>
860
- )}
861
- {turma.endDate && (
862
- <span>
863
- Fim:{' '}
864
- {new Date(turma.endDate).toLocaleDateString(
865
- 'pt-BR'
866
- )}
867
- </span>
868
- )}
869
- {turma.slots > 0 && (
870
- <span>{turma.slots} aluno(s)</span>
871
- )}
872
1100
  </div>
873
- </div>
874
- ))}
1101
+ ))}
1102
+ </div>
1103
+ )}
1104
+ </div>
1105
+
1106
+ {turmasLastPage > 1 && (
1107
+ <div className="flex shrink-0 items-center justify-center gap-2 border-t px-6 py-3">
1108
+ <Button
1109
+ type="button"
1110
+ variant="outline"
1111
+ size="icon"
1112
+ disabled={turmasPage <= 1}
1113
+ onClick={() => setTurmasPage((p) => p - 1)}
1114
+ >
1115
+ <ChevronLeft className="h-4 w-4" />
1116
+ </Button>
1117
+ <span className="text-xs text-muted-foreground">
1118
+ {turmasPage} / {turmasLastPage}
1119
+ </span>
1120
+ <Button
1121
+ type="button"
1122
+ variant="outline"
1123
+ size="icon"
1124
+ disabled={turmasPage >= turmasLastPage}
1125
+ onClick={() => setTurmasPage((p) => p + 1)}
1126
+ >
1127
+ <ChevronRight className="h-4 w-4" />
1128
+ </Button>
875
1129
  </div>
876
1130
  )}
877
1131
  </div>
@@ -886,11 +1140,10 @@ export function InstructorFormSheet({
886
1140
  <div className="flex flex-1 flex-col items-center justify-center px-6 py-12 text-center">
887
1141
  <Star className="mb-3 h-10 w-10 text-muted-foreground/40" />
888
1142
  <p className="text-sm font-medium text-muted-foreground">
889
- Avaliações — funcionalidade em breve
1143
+ {t('form.evaluations.comingSoonTitle')}
890
1144
  </p>
891
1145
  <p className="mt-1 text-xs text-muted-foreground">
892
- Em breve você poderá visualizar as avaliações recebidas
893
- pelos alunos.
1146
+ {t('form.evaluations.comingSoonDescription')}
894
1147
  </p>
895
1148
  </div>
896
1149
  </TabsContent>
@@ -906,94 +1159,92 @@ export function InstructorFormSheet({
906
1159
  {/* Person picker */}
907
1160
  <Field>
908
1161
  <FieldLabel>
909
- Pessoa <span className="text-destructive">*</span>
1162
+ {t('form.fields.person')}{' '}
1163
+ <span className="text-destructive">*</span>
910
1164
  </FieldLabel>
911
- <div className="flex items-center gap-2">
912
- <div className="min-w-0 flex-1">
913
- <Controller
914
- control={control}
915
- name="personId"
916
- render={({ field }) => (
917
- <PersonPicker
918
- label=""
919
- entityLabel="pessoa"
920
- value={field.value ?? null}
921
- initialSelectedLabel={selectedPersonName}
922
- onChange={(personId, personName) => {
923
- field.onChange(personId ?? undefined);
924
- setSelectedPersonName(personName);
925
- }}
926
- selectPlaceholder="Buscar ou selecionar pessoa..."
927
- personTypeFilter="individual"
928
- createType="individual"
929
- lockCreateType
930
- />
931
- )}
932
- />
933
- </div>
934
- <Button
935
- type="button"
936
- variant="outline"
937
- size="icon"
938
- className="shrink-0"
939
- disabled={!watchedPersonId}
940
- onClick={handleOpenPersonEdit}
941
- aria-label="Editar cadastro da pessoa"
942
- title="Editar cadastro da pessoa"
943
- >
944
- <Pencil className="h-4 w-4" />
945
- </Button>
946
- </div>
947
- {errors.personId && (
948
- <FieldError>{errors.personId.message}</FieldError>
949
- )}
950
- </Field>
951
-
952
- {/* Status */}
953
- <Field>
954
- <FieldLabel>Status</FieldLabel>
955
- <Controller
956
- control={control}
957
- name="status"
958
- render={({ field }) => (
959
- <Select
960
- value={field.value}
961
- onValueChange={field.onChange}
962
- >
963
- <SelectTrigger className="w-full">
964
- <SelectValue placeholder="Selecione o status" />
965
- </SelectTrigger>
966
- <SelectContent>
967
- <SelectItem value="active">Ativo</SelectItem>
968
- <SelectItem value="inactive">Inativo</SelectItem>
969
- </SelectContent>
970
- </Select>
971
- )}
972
- />
973
- </Field>
974
-
975
- {/* Valor/hora */}
976
- <Field>
977
- <FieldLabel>Valor/hora (R$)</FieldLabel>
978
1165
  <Controller
979
1166
  control={control}
980
- name="hourlyRate"
1167
+ name="personId"
981
1168
  render={({ field }) => (
982
- <InputMoney
983
- placeholder="Ex: 150,00"
984
- value={field.value ?? ''}
985
- onValueChange={(value) => field.onChange(value)}
1169
+ <PersonPicker
1170
+ label=""
1171
+ entityLabel={t('form.fields.person')}
1172
+ value={field.value ?? null}
1173
+ initialSelectedLabel={selectedPersonName}
1174
+ onChange={(personId, personName) => {
1175
+ field.onChange(personId ?? undefined);
1176
+ setSelectedPersonName(personName);
1177
+ }}
1178
+ selectPlaceholder={t('form.fields.searchPerson')}
1179
+ personTypeFilter="individual"
1180
+ createType="individual"
1181
+ lockCreateType
1182
+ onCreateNew={handleCreatePerson}
1183
+ showEditButton
1184
+ onEditSelection={handleEditSelection}
1185
+ editAriaLabel={t('form.fields.editPerson')}
986
1186
  />
987
1187
  )}
988
1188
  />
1189
+ {errors.personId && (
1190
+ <FieldError>{errors.personId.message}</FieldError>
1191
+ )}
989
1192
  </Field>
990
1193
 
1194
+ {/* Status + Valor/hora */}
1195
+ <div className="grid grid-cols-2 gap-4">
1196
+ <Field>
1197
+ <FieldLabel>{t('form.fields.status')}</FieldLabel>
1198
+ <Controller
1199
+ control={control}
1200
+ name="status"
1201
+ render={({ field }) => (
1202
+ <Select
1203
+ value={field.value}
1204
+ onValueChange={field.onChange}
1205
+ >
1206
+ <SelectTrigger className="w-full">
1207
+ <SelectValue
1208
+ placeholder={t('filters.statusPlaceholder')}
1209
+ />
1210
+ </SelectTrigger>
1211
+ <SelectContent>
1212
+ <SelectItem value="active">
1213
+ {t('form.fields.active')}
1214
+ </SelectItem>
1215
+ <SelectItem value="inactive">
1216
+ {t('form.fields.inactive')}
1217
+ </SelectItem>
1218
+ </SelectContent>
1219
+ </Select>
1220
+ )}
1221
+ />
1222
+ </Field>
1223
+
1224
+ <Field>
1225
+ <FieldLabel>{t('form.fields.hourlyRate')}</FieldLabel>
1226
+ <Controller
1227
+ control={control}
1228
+ name="hourlyRate"
1229
+ render={({ field }) => (
1230
+ <InputMoney
1231
+ placeholder={t('form.fields.hourlyRatePlaceholder')}
1232
+ value={field.value ?? ''}
1233
+ onValueChange={(value) => field.onChange(value)}
1234
+ />
1235
+ )}
1236
+ />
1237
+ </Field>
1238
+ </div>
1239
+
991
1240
  {/* can_teach_courses */}
992
1241
  <div className="flex items-center justify-between rounded-lg border p-4">
993
1242
  <div className="space-y-0.5">
994
- <p className="text-sm font-medium">Pode ensinar cursos</p>
1243
+ <p className="text-sm font-medium">
1244
+ {t('form.fields.canTeachCourses')}
1245
+ </p>
995
1246
  <p className="text-xs text-muted-foreground">
996
- Permite que o instrutor seja vinculado a aulas de curso.
1247
+ {t('form.fields.canTeachCoursesHint')}
997
1248
  </p>
998
1249
  </div>
999
1250
  <Controller
@@ -1002,7 +1253,18 @@ export function InstructorFormSheet({
1002
1253
  render={({ field }) => (
1003
1254
  <Switch
1004
1255
  checked={field.value}
1005
- onCheckedChange={field.onChange}
1256
+ onCheckedChange={(next) => {
1257
+ field.onChange(next);
1258
+ const nextSlugs = applyCanTeachCourses(
1259
+ watch('qualificationSlugs') ?? [],
1260
+ next
1261
+ );
1262
+ setValue('qualificationSlugs', nextSlugs, {
1263
+ shouldDirty: true,
1264
+ shouldTouch: true,
1265
+ shouldValidate: true,
1266
+ });
1267
+ }}
1006
1268
  />
1007
1269
  )}
1008
1270
  />
@@ -1011,10 +1273,11 @@ export function InstructorFormSheet({
1011
1273
  {/* Qualificações */}
1012
1274
  <Field>
1013
1275
  <FieldLabel>
1014
- Qualificações <span className="text-destructive">*</span>
1276
+ {t('form.fields.qualifications')}{' '}
1277
+ <span className="text-destructive">*</span>
1015
1278
  </FieldLabel>
1016
1279
  <div className="space-y-3">
1017
- {QUALIFICATION_OPTIONS.map((option) => (
1280
+ {qualificationOptions.map((option) => (
1018
1281
  <Controller
1019
1282
  key={option.slug}
1020
1283
  control={control}
@@ -1034,17 +1297,18 @@ export function InstructorFormSheet({
1034
1297
  <Switch
1035
1298
  checked={checked}
1036
1299
  onCheckedChange={(next) => {
1037
- if (next) {
1038
- field.onChange([
1039
- ...field.value,
1040
- option.slug,
1041
- ]);
1042
- } else {
1043
- field.onChange(
1044
- field.value.filter(
1045
- (s) => s !== option.slug
1046
- )
1047
- );
1300
+ const nextSlugs = applyQualificationToggle(
1301
+ field.value,
1302
+ option.slug,
1303
+ next
1304
+ );
1305
+ field.onChange(nextSlugs);
1306
+ if (option.slug === 'course-lessons') {
1307
+ setValue('can_teach_courses', next, {
1308
+ shouldDirty: true,
1309
+ shouldTouch: true,
1310
+ shouldValidate: false,
1311
+ });
1048
1312
  }
1049
1313
  }}
1050
1314
  />
@@ -1070,11 +1334,11 @@ export function InstructorFormSheet({
1070
1334
  onClick={() => handleSheetClose(false)}
1071
1335
  disabled={saving}
1072
1336
  >
1073
- Cancelar
1337
+ {t('form.buttons.cancel')}
1074
1338
  </Button>
1075
1339
  <Button type="submit" disabled={saving}>
1076
1340
  {saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
1077
- Criar instrutor
1341
+ {t('form.buttons.create')}
1078
1342
  </Button>
1079
1343
  </SheetFooter>
1080
1344
  </form>
@@ -1082,18 +1346,39 @@ export function InstructorFormSheet({
1082
1346
  </SheetContent>
1083
1347
  </Sheet>
1084
1348
 
1085
- {editPersonOpen && personToEdit && (
1349
+ <ClassFormSheet
1350
+ open={classSheetOpen}
1351
+ onOpenChange={(nextOpen) => {
1352
+ setClassSheetOpen(nextOpen);
1353
+ if (!nextOpen) {
1354
+ setEditingClassId(null);
1355
+ }
1356
+ }}
1357
+ classId={editingClassId != null ? String(editingClassId) : undefined}
1358
+ enableInstructorSheet={false}
1359
+ onSuccess={handleClassSaved}
1360
+ />
1361
+
1362
+ {editPersonOpen && (
1086
1363
  <PersonFormSheet
1087
1364
  open={editPersonOpen}
1088
1365
  person={personToEdit}
1089
1366
  contactTypes={contactTypes}
1090
1367
  documentTypes={documentTypes}
1368
+ allowedTypes={['individual']}
1091
1369
  onOpenChange={(v) => {
1092
1370
  setEditPersonOpen(v);
1093
1371
  if (!v) setPersonToEdit(null);
1094
1372
  }}
1095
1373
  onSuccess={(updated) => {
1096
- if (updated) setSelectedPersonName(updated.name);
1374
+ if (updated) {
1375
+ setSelectedPersonName(updated.name);
1376
+ setValue('personId', updated.id, {
1377
+ shouldDirty: true,
1378
+ shouldTouch: true,
1379
+ shouldValidate: true,
1380
+ });
1381
+ }
1097
1382
  void queryClient.invalidateQueries({
1098
1383
  queryKey: ['person-picker-autocomplete'],
1099
1384
  });