@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
@@ -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,
@@ -31,35 +33,52 @@ import { Switch } from '@/components/ui/switch';
31
33
  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
- import { useTranslations } from 'next-intl';
35
36
  import { useQueryClient } from '@tanstack/react-query';
36
- import { BookOpen, Loader2, Pencil, Star, X } from 'lucide-react';
37
- 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';
38
50
  import { useEffect, useState } from 'react';
39
51
  import { Controller, useForm } from 'react-hook-form';
40
52
  import { toast } from 'sonner';
41
53
  import { z } from 'zod';
54
+ import { ClassFormSheet } from '../../_components/class-form-sheet';
55
+ import { CourseAvatar } from '../../_components/course-avatar';
42
56
  import type { InstructorRow, InstructorSkill } from './instructor-types';
43
57
 
44
58
  type ClassGroupItem = {
45
59
  id: number;
46
60
  name: string;
47
61
  code: string;
62
+ courseLogoUrl: string | null;
63
+ courseLogoFileId?: number | null;
48
64
  courseName: string | null;
49
65
  startDate: string | null;
50
66
  endDate: string | null;
51
67
  status: string;
52
68
  slots: number;
69
+ totalSlots: number | null;
53
70
  };
54
71
 
55
72
  function toKebabCase(str: string) {
56
- return str
57
- .toLowerCase()
58
- .normalize('NFD')
59
- // biome-ignore lint: unicode diacritic strip
60
- .replace(/[̀-ͯ]/g, '')
61
- .replace(/[^a-z0-9]+/g, '-')
62
- .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
+ );
63
82
  }
64
83
 
65
84
  function getSkillDisplayName(s: InstructorSkill): string {
@@ -91,17 +110,23 @@ export function InstructorFormSheet({
91
110
  }: InstructorFormSheetProps) {
92
111
  const { request, currentLocaleCode } = useApp();
93
112
  const t = useTranslations('lms.InstructorsPage');
113
+ const router = useRouter();
94
114
  const queryClient = useQueryClient();
95
115
  const [saving, setSaving] = useState(false);
96
116
  const [selectedPersonName, setSelectedPersonName] = useState('');
97
- const [trainingAccess, setTrainingAccess] = useState<boolean | null>(null);
98
- const [togglingAccess, setTogglingAccess] = useState(false);
99
117
  const [editPersonOpen, setEditPersonOpen] = useState(false);
100
118
  const [personToEdit, setPersonToEdit] = useState<Person | null>(null);
101
119
  const [activeTab, setActiveTab] = useState('detalhes');
102
120
  const [tabSkillSlugs, setTabSkillSlugs] = useState<string[]>([]);
103
121
  const [savingSkills, setSavingSkills] = useState(false);
104
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;
105
130
  const isEditing = typeof instructorId === 'number' && instructorId > 0;
106
131
  const qualificationOptions = [
107
132
  {
@@ -118,6 +143,8 @@ export function InstructorFormSheet({
118
143
  const statusLabel: Record<string, string> = {
119
144
  active: t('form.fields.active'),
120
145
  inactive: t('form.fields.inactive'),
146
+ open: t('form.classStatuses.upcoming'),
147
+ ongoing: t('form.classStatuses.inProgress'),
121
148
  in_progress: t('form.classStatuses.inProgress'),
122
149
  upcoming: t('form.classStatuses.upcoming'),
123
150
  completed: t('form.classStatuses.completed'),
@@ -141,6 +168,7 @@ export function InstructorFormSheet({
141
168
  control,
142
169
  handleSubmit,
143
170
  reset,
171
+ setValue,
144
172
  watch,
145
173
  formState: { errors },
146
174
  } = useForm<InstructorFormValues>({
@@ -153,7 +181,38 @@ export function InstructorFormSheet({
153
181
  },
154
182
  });
155
183
 
156
- 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
+ };
157
216
 
158
217
  const { data: contactTypes = [] } = useQuery<ContactTypeOption[]>({
159
218
  queryKey: ['contact-person-contact-types', currentLocaleCode],
@@ -181,16 +240,20 @@ export function InstructorFormSheet({
181
240
  placeholderData: (previous) => previous ?? [],
182
241
  });
183
242
 
184
- const handleOpenPersonEdit = async () => {
185
- if (!watchedPersonId) return;
243
+ const handleEditSelection = async (personId: number) => {
186
244
  const response = await request<Person>({
187
- url: `/person/${watchedPersonId}`,
245
+ url: `/person/${personId}`,
188
246
  method: 'GET',
189
247
  });
190
248
  setPersonToEdit(response.data);
191
249
  setEditPersonOpen(true);
192
250
  };
193
251
 
252
+ const handleCreatePerson = () => {
253
+ setPersonToEdit(null);
254
+ setEditPersonOpen(true);
255
+ };
256
+
194
257
  const { data: existingInstructor } = useQuery<InstructorRow | null>({
195
258
  queryKey: ['lms-instructor-detail', instructorId],
196
259
  queryFn: async () => {
@@ -216,26 +279,73 @@ export function InstructorFormSheet({
216
279
  },
217
280
  });
218
281
 
219
- // Turmas — enterprise/training/admin endpoint supports instructorId filter
220
- const { data: turmasData = [], isLoading: loadingTurmas } = useQuery<
221
- ClassGroupItem[]
222
- >({
223
- 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
+ ],
224
299
  queryFn: async () => {
225
- const response = await request<
226
- { data?: ClassGroupItem[] } | ClassGroupItem[]
227
- >({
228
- 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()}`,
229
322
  method: 'GET',
230
323
  });
231
- const raw = response.data;
232
- if (Array.isArray(raw)) return raw;
233
- if (raw && 'data' in raw && Array.isArray(raw.data)) return raw.data;
234
- return [];
324
+ return response.data;
235
325
  },
236
326
  enabled: isEditing && open && activeTab === 'turmas',
237
- placeholderData: [],
327
+ placeholderData: (prev) => prev,
238
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]);
239
349
 
240
350
  useEffect(() => {
241
351
  if (isEditing && existingInstructor) {
@@ -246,12 +356,12 @@ export function InstructorFormSheet({
246
356
  ? existingInstructor.qualificationSlugs
247
357
  : ['course-lessons'],
248
358
  status: existingInstructor.status,
249
- can_teach_courses: true,
359
+ can_teach_courses:
360
+ existingInstructor.qualificationSlugs.includes('course-lessons'),
250
361
  hourlyRate: existingInstructor.hourlyRate ?? null,
251
362
  skillSlugs: (existingInstructor.skills ?? []).map((s) => s.slug),
252
363
  });
253
364
  setSelectedPersonName(existingInstructor.name);
254
- setTrainingAccess(existingInstructor.hasTrainingAccess ?? false);
255
365
  setTabSkillSlugs((existingInstructor.skills ?? []).map((s) => s.slug));
256
366
  } else if (!isEditing && open) {
257
367
  reset({
@@ -263,16 +373,27 @@ export function InstructorFormSheet({
263
373
  skillSlugs: [],
264
374
  });
265
375
  setSelectedPersonName('');
266
- setTrainingAccess(null);
267
376
  setTabSkillSlugs([]);
268
377
  }
269
378
  }, [isEditing, existingInstructor, open, reset]);
270
379
 
271
- // Reset to first tab when sheet closes
380
+ // Reset to first tab and turmas page when sheet closes
272
381
  useEffect(() => {
273
- 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
+ }
274
391
  }, [open]);
275
392
 
393
+ useEffect(() => {
394
+ setTurmasPage(1);
395
+ }, [activeTab]);
396
+
276
397
  const handleSheetClose = (nextOpen: boolean) => {
277
398
  if (!nextOpen) {
278
399
  reset();
@@ -301,6 +422,22 @@ export function InstructorFormSheet({
301
422
  }
302
423
  };
303
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
+
304
441
  const onSubmit = async (values: InstructorFormValues) => {
305
442
  try {
306
443
  setSaving(true);
@@ -342,9 +479,7 @@ export function InstructorFormSheet({
342
479
  handleSheetClose(false);
343
480
  } catch {
344
481
  toast.error(
345
- isEditing
346
- ? t('form.toasts.updateError')
347
- : t('form.toasts.createError')
482
+ isEditing ? t('form.toasts.updateError') : t('form.toasts.createError')
348
483
  );
349
484
  } finally {
350
485
  setSaving(false);
@@ -360,49 +495,55 @@ export function InstructorFormSheet({
360
495
  >
361
496
  {/* ── Instructor header (edit mode) ── */}
362
497
  {isEditing && existingInstructor ? (
363
- <div className="flex shrink-0 items-center gap-4 border-b px-6 py-4">
364
- <Avatar className="h-14 w-14">
365
- <AvatarImage
366
- src={
367
- existingInstructor.avatarId
368
- ? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${existingInstructor.avatarId}`
369
- : undefined
370
- }
371
- alt={existingInstructor.name}
372
- />
373
- <AvatarFallback>
374
- {existingInstructor.name.slice(0, 2).toUpperCase()}
375
- </AvatarFallback>
376
- </Avatar>
377
- <div className="min-w-0 flex-1">
378
- <h2 className="truncate text-lg font-semibold">
379
- {existingInstructor.name}
380
- </h2>
381
- <div className="mt-1 flex flex-wrap items-center gap-2">
382
- <Badge
383
- variant={
384
- existingInstructor.status === 'active'
385
- ? 'default'
386
- : '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
387
510
  }
388
- >
389
- {existingInstructor.status === 'active'
390
- ? t('form.fields.active')
391
- : t('form.fields.inactive')}
392
- </Badge>
393
- {existingInstructor.email && (
394
- <span className="truncate text-xs text-muted-foreground">
395
- {existingInstructor.email}
396
- </span>
397
- )}
398
- {existingInstructor.phone && (
399
- <span className="text-xs text-muted-foreground">
400
- {existingInstructor.phone}
401
- </span>
402
- )}
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>
403
544
  </div>
404
545
  </div>
405
- </div>
546
+ </>
406
547
  ) : (
407
548
  <SheetHeader className="shrink-0 border-b px-6 py-4">
408
549
  <SheetTitle>{t('form.title')}</SheetTitle>
@@ -421,8 +562,12 @@ export function InstructorFormSheet({
421
562
  <TabsTrigger value="detalhes">
422
563
  {t('form.tabs.details')}
423
564
  </TabsTrigger>
424
- <TabsTrigger value="skills">{t('form.tabs.skills')}</TabsTrigger>
425
- <TabsTrigger value="turmas">{t('form.tabs.classes')}</TabsTrigger>
565
+ <TabsTrigger value="skills">
566
+ {t('form.tabs.skills')}
567
+ </TabsTrigger>
568
+ <TabsTrigger value="turmas">
569
+ {t('form.tabs.classes')}
570
+ </TabsTrigger>
426
571
  <TabsTrigger value="avaliacoes">
427
572
  {t('form.tabs.evaluations')}
428
573
  </TabsTrigger>
@@ -445,94 +590,83 @@ export function InstructorFormSheet({
445
590
  {t('form.fields.person')}{' '}
446
591
  <span className="text-destructive">*</span>
447
592
  </FieldLabel>
448
- <div className="flex items-center gap-2">
449
- <div className="min-w-0 flex-1">
450
- <Controller
451
- control={control}
452
- name="personId"
453
- render={({ field }) => (
454
- <PersonPicker
455
- label=""
456
- entityLabel={t('form.fields.person')}
457
- value={field.value ?? null}
458
- initialSelectedLabel={selectedPersonName}
459
- onChange={(personId, personName) => {
460
- field.onChange(personId ?? undefined);
461
- setSelectedPersonName(personName);
462
- }}
463
- selectPlaceholder={t(
464
- 'form.fields.searchPerson'
465
- )}
466
- personTypeFilter="individual"
467
- createType="individual"
468
- lockCreateType
469
- />
470
- )}
471
- />
472
- </div>
473
- <Button
474
- type="button"
475
- variant="outline"
476
- size="icon"
477
- className="shrink-0"
478
- disabled={!watchedPersonId}
479
- onClick={handleOpenPersonEdit}
480
- aria-label={t('form.fields.editPerson')}
481
- title={t('form.fields.editPerson')}
482
- >
483
- <Pencil className="h-4 w-4" />
484
- </Button>
485
- </div>
486
- {errors.personId && (
487
- <FieldError>{errors.personId.message}</FieldError>
488
- )}
489
- </Field>
490
-
491
- {/* Status */}
492
- <Field>
493
- <FieldLabel>{t('form.fields.status')}</FieldLabel>
494
- <Controller
495
- control={control}
496
- name="status"
497
- render={({ field }) => (
498
- <Select
499
- value={field.value}
500
- onValueChange={field.onChange}
501
- >
502
- <SelectTrigger className="w-full">
503
- <SelectValue
504
- placeholder={t('filters.statusPlaceholder')}
505
- />
506
- </SelectTrigger>
507
- <SelectContent>
508
- <SelectItem value="active">
509
- {t('form.fields.active')}
510
- </SelectItem>
511
- <SelectItem value="inactive">
512
- {t('form.fields.inactive')}
513
- </SelectItem>
514
- </SelectContent>
515
- </Select>
516
- )}
517
- />
518
- </Field>
519
-
520
- {/* Valor/hora */}
521
- <Field>
522
- <FieldLabel>{t('form.fields.hourlyRate')}</FieldLabel>
523
593
  <Controller
524
594
  control={control}
525
- name="hourlyRate"
595
+ name="personId"
526
596
  render={({ field }) => (
527
- <InputMoney
528
- placeholder={t('form.fields.hourlyRatePlaceholder')}
529
- value={field.value ?? ''}
530
- 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')}
531
614
  />
532
615
  )}
533
616
  />
617
+ {errors.personId && (
618
+ <FieldError>{errors.personId.message}</FieldError>
619
+ )}
534
620
  </Field>
535
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
+
536
670
  {/* can_teach_courses */}
537
671
  <div className="flex items-center justify-between rounded-lg border p-4">
538
672
  <div className="space-y-0.5">
@@ -549,52 +683,23 @@ export function InstructorFormSheet({
549
683
  render={({ field }) => (
550
684
  <Switch
551
685
  checked={field.value}
552
- 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
+ }}
553
698
  />
554
699
  )}
555
700
  />
556
701
  </div>
557
702
 
558
- {/* Acesso ao Training */}
559
- <div className="flex items-center justify-between rounded-lg border p-4">
560
- <div className="space-y-0.5">
561
- <p className="text-sm font-medium">
562
- {t('form.fields.trainingAccess')}
563
- </p>
564
- <p className="text-xs text-muted-foreground">
565
- {existingInstructor?.userId
566
- ? t('form.fields.trainingAccessHint')
567
- : t('form.fields.noUserLinked')}
568
- </p>
569
- </div>
570
- <Switch
571
- checked={trainingAccess ?? false}
572
- disabled={
573
- !existingInstructor?.userId || togglingAccess
574
- }
575
- onCheckedChange={async (next) => {
576
- setTogglingAccess(true);
577
- try {
578
- await request({
579
- url: `/lms/instructors/${instructorId}/training-access`,
580
- method: 'PATCH',
581
- data: { enabled: next },
582
- });
583
- setTrainingAccess(next);
584
- toast.success(
585
- next
586
- ? t('form.toasts.accessEnabled')
587
- : t('form.toasts.accessDisabled')
588
- );
589
- } catch {
590
- toast.error(t('form.toasts.accessError'));
591
- } finally {
592
- setTogglingAccess(false);
593
- }
594
- }}
595
- />
596
- </div>
597
-
598
703
  {/* Qualificações */}
599
704
  <Field>
600
705
  <FieldLabel>
@@ -624,17 +729,19 @@ export function InstructorFormSheet({
624
729
  <Switch
625
730
  checked={checked}
626
731
  onCheckedChange={(next) => {
627
- if (next) {
628
- field.onChange([
629
- ...field.value,
732
+ const nextSlugs =
733
+ applyQualificationToggle(
734
+ field.value,
630
735
  option.slug,
631
- ]);
632
- } else {
633
- field.onChange(
634
- field.value.filter(
635
- (s) => s !== option.slug
636
- )
736
+ next
637
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
+ });
638
745
  }
639
746
  }}
640
747
  />
@@ -753,12 +860,16 @@ export function InstructorFormSheet({
753
860
  name: 'namePt',
754
861
  label: t('form.skills.createFields.name'),
755
862
  required: true,
756
- placeholder: t('form.skills.createFields.namePlaceholder'),
863
+ placeholder: t(
864
+ 'form.skills.createFields.namePlaceholder'
865
+ ),
757
866
  },
758
867
  {
759
868
  name: 'slug',
760
869
  label: t('form.skills.createFields.slug'),
761
- placeholder: t('form.skills.createFields.slugPlaceholder'),
870
+ placeholder: t(
871
+ 'form.skills.createFields.slugPlaceholder'
872
+ ),
762
873
  },
763
874
  ]}
764
875
  mapSearchToCreateValues={(search) => ({
@@ -788,7 +899,9 @@ export function InstructorFormSheet({
788
899
  name: namePt,
789
900
  } as any;
790
901
  } catch {
791
- toast.error(t('InstructorSkillsPage.messages.createError'));
902
+ toast.error(
903
+ t('InstructorSkillsPage.messages.createError')
904
+ );
792
905
  return null;
793
906
  }
794
907
  }}
@@ -825,70 +938,194 @@ export function InstructorFormSheet({
825
938
  value="turmas"
826
939
  className="flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
827
940
  >
828
- <div className="flex-1 overflow-y-auto px-6 py-4">
829
- {loadingTurmas ? (
830
- <div className="flex items-center justify-center py-12">
831
- <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
832
- </div>
833
- ) : turmasData.length === 0 ? (
834
- <div className="flex flex-col items-center justify-center py-12 text-center">
835
- <BookOpen className="mb-3 h-10 w-10 text-muted-foreground/40" />
836
- <p className="text-sm font-medium text-muted-foreground">
837
- {t('form.classes.emptyTitle')}
838
- </p>
839
- <p className="mt-1 text-xs text-muted-foreground">
840
- {t('form.classes.emptyDescription')}
841
- </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
+ />
842
953
  </div>
843
- ) : (
844
- <div className="space-y-2">
845
- {turmasData.map((turma) => (
846
- <div key={turma.id} className="rounded-lg border p-3">
847
- <div className="flex items-start justify-between gap-2">
848
- <div className="min-w-0 flex-1">
849
- <p className="truncate text-sm font-medium">
850
- {turma.name}
851
- </p>
852
- {turma.courseName && (
853
- <p className="truncate text-xs text-muted-foreground">
854
- {turma.courseName}
855
- </p>
856
- )}
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>
857
1099
  </div>
858
- <Badge
859
- variant="outline"
860
- className="shrink-0 text-xs"
861
- >
862
- {statusLabel[turma.status] ?? turma.status}
863
- </Badge>
864
- </div>
865
- <div className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
866
- {turma.startDate && (
867
- <span>
868
- {t('form.classes.start')}{' '}
869
- {new Date(turma.startDate).toLocaleDateString(
870
- 'pt-BR'
871
- )}
872
- </span>
873
- )}
874
- {turma.endDate && (
875
- <span>
876
- {t('form.classes.end')}{' '}
877
- {new Date(turma.endDate).toLocaleDateString(
878
- 'pt-BR'
879
- )}
880
- </span>
881
- )}
882
- {turma.slots > 0 && (
883
- <span>
884
- {t('form.classes.students', {
885
- count: turma.slots,
886
- })}
887
- </span>
888
- )}
889
1100
  </div>
890
- </div>
891
- ))}
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>
892
1129
  </div>
893
1130
  )}
894
1131
  </div>
@@ -925,92 +1162,81 @@ export function InstructorFormSheet({
925
1162
  {t('form.fields.person')}{' '}
926
1163
  <span className="text-destructive">*</span>
927
1164
  </FieldLabel>
928
- <div className="flex items-center gap-2">
929
- <div className="min-w-0 flex-1">
930
- <Controller
931
- control={control}
932
- name="personId"
933
- render={({ field }) => (
934
- <PersonPicker
935
- label=""
936
- entityLabel={t('form.fields.person')}
937
- value={field.value ?? null}
938
- initialSelectedLabel={selectedPersonName}
939
- onChange={(personId, personName) => {
940
- field.onChange(personId ?? undefined);
941
- setSelectedPersonName(personName);
942
- }}
943
- selectPlaceholder={t('form.fields.searchPerson')}
944
- personTypeFilter="individual"
945
- createType="individual"
946
- lockCreateType
947
- />
948
- )}
949
- />
950
- </div>
951
- <Button
952
- type="button"
953
- variant="outline"
954
- size="icon"
955
- className="shrink-0"
956
- disabled={!watchedPersonId}
957
- onClick={handleOpenPersonEdit}
958
- aria-label={t('form.fields.editPerson')}
959
- title={t('form.fields.editPerson')}
960
- >
961
- <Pencil className="h-4 w-4" />
962
- </Button>
963
- </div>
964
- {errors.personId && (
965
- <FieldError>{errors.personId.message}</FieldError>
966
- )}
967
- </Field>
968
-
969
- {/* Status */}
970
- <Field>
971
- <FieldLabel>{t('form.fields.status')}</FieldLabel>
972
- <Controller
973
- control={control}
974
- name="status"
975
- render={({ field }) => (
976
- <Select
977
- value={field.value}
978
- onValueChange={field.onChange}
979
- >
980
- <SelectTrigger className="w-full">
981
- <SelectValue
982
- placeholder={t('filters.statusPlaceholder')}
983
- />
984
- </SelectTrigger>
985
- <SelectContent>
986
- <SelectItem value="active">
987
- {t('form.fields.active')}
988
- </SelectItem>
989
- <SelectItem value="inactive">
990
- {t('form.fields.inactive')}
991
- </SelectItem>
992
- </SelectContent>
993
- </Select>
994
- )}
995
- />
996
- </Field>
997
-
998
- {/* Valor/hora */}
999
- <Field>
1000
- <FieldLabel>{t('form.fields.hourlyRate')}</FieldLabel>
1001
1165
  <Controller
1002
1166
  control={control}
1003
- name="hourlyRate"
1167
+ name="personId"
1004
1168
  render={({ field }) => (
1005
- <InputMoney
1006
- placeholder={t('form.fields.hourlyRatePlaceholder')}
1007
- value={field.value ?? ''}
1008
- 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')}
1009
1186
  />
1010
1187
  )}
1011
1188
  />
1189
+ {errors.personId && (
1190
+ <FieldError>{errors.personId.message}</FieldError>
1191
+ )}
1012
1192
  </Field>
1013
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
+
1014
1240
  {/* can_teach_courses */}
1015
1241
  <div className="flex items-center justify-between rounded-lg border p-4">
1016
1242
  <div className="space-y-0.5">
@@ -1027,7 +1253,18 @@ export function InstructorFormSheet({
1027
1253
  render={({ field }) => (
1028
1254
  <Switch
1029
1255
  checked={field.value}
1030
- 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
+ }}
1031
1268
  />
1032
1269
  )}
1033
1270
  />
@@ -1060,17 +1297,18 @@ export function InstructorFormSheet({
1060
1297
  <Switch
1061
1298
  checked={checked}
1062
1299
  onCheckedChange={(next) => {
1063
- if (next) {
1064
- field.onChange([
1065
- ...field.value,
1066
- option.slug,
1067
- ]);
1068
- } else {
1069
- field.onChange(
1070
- field.value.filter(
1071
- (s) => s !== option.slug
1072
- )
1073
- );
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
+ });
1074
1312
  }
1075
1313
  }}
1076
1314
  />
@@ -1108,18 +1346,39 @@ export function InstructorFormSheet({
1108
1346
  </SheetContent>
1109
1347
  </Sheet>
1110
1348
 
1111
- {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 && (
1112
1363
  <PersonFormSheet
1113
1364
  open={editPersonOpen}
1114
1365
  person={personToEdit}
1115
1366
  contactTypes={contactTypes}
1116
1367
  documentTypes={documentTypes}
1368
+ allowedTypes={['individual']}
1117
1369
  onOpenChange={(v) => {
1118
1370
  setEditPersonOpen(v);
1119
1371
  if (!v) setPersonToEdit(null);
1120
1372
  }}
1121
1373
  onSuccess={(updated) => {
1122
- 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
+ }
1123
1382
  void queryClient.invalidateQueries({
1124
1383
  queryKey: ['person-picker-autocomplete'],
1125
1384
  });