@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.
- package/dist/class-group/class-group.controller.d.ts +3 -3
- package/dist/class-group/class-group.service.d.ts +3 -3
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +12 -20
- package/dist/course/course.service.js.map +1 -1
- package/dist/enterprise/enterprise.controller.d.ts +72 -0
- package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
- package/dist/enterprise/enterprise.controller.js +10 -0
- package/dist/enterprise/enterprise.controller.js.map +1 -1
- package/dist/enterprise/enterprise.service.d.ts +78 -0
- package/dist/enterprise/enterprise.service.d.ts.map +1 -1
- package/dist/enterprise/enterprise.service.js +413 -40
- package/dist/enterprise/enterprise.service.js.map +1 -1
- package/dist/enterprise/training/training-admin.controller.d.ts +6 -3
- package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
- package/dist/enterprise/training/training-admin.controller.js +10 -6
- package/dist/enterprise/training/training-admin.controller.js.map +1 -1
- package/dist/enterprise/training/training-admin.service.d.ts +8 -2
- package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-admin.service.js +108 -52
- package/dist/enterprise/training/training-admin.service.js.map +1 -1
- package/dist/enterprise/training/training-viewer.controller.d.ts +3 -0
- package/dist/enterprise/training/training-viewer.controller.d.ts.map +1 -1
- package/dist/evaluation/evaluation.controller.d.ts +4 -4
- package/dist/evaluation/evaluation.service.d.ts +4 -4
- package/dist/instructor/dto/create-instructor-skill.dto.d.ts +0 -4
- package/dist/instructor/dto/create-instructor-skill.dto.d.ts.map +1 -1
- package/dist/instructor/dto/create-instructor-skill.dto.js +0 -21
- package/dist/instructor/dto/create-instructor-skill.dto.js.map +1 -1
- package/dist/instructor/dto/update-instructor-skill.dto.d.ts +0 -4
- package/dist/instructor/dto/update-instructor-skill.dto.d.ts.map +1 -1
- package/dist/instructor/dto/update-instructor-skill.dto.js +0 -22
- package/dist/instructor/dto/update-instructor-skill.dto.js.map +1 -1
- package/dist/instructor/instructor-skill.controller.d.ts +4 -4
- package/dist/instructor/instructor-skill.service.d.ts +4 -7
- package/dist/instructor/instructor-skill.service.d.ts.map +1 -1
- package/dist/instructor/instructor-skill.service.js +2 -89
- package/dist/instructor/instructor-skill.service.js.map +1 -1
- package/dist/instructor/instructor.controller.d.ts +20 -0
- package/dist/instructor/instructor.controller.d.ts.map +1 -1
- package/dist/instructor/instructor.controller.js +19 -0
- package/dist/instructor/instructor.controller.js.map +1 -1
- package/dist/instructor/instructor.service.d.ts +25 -0
- package/dist/instructor/instructor.service.d.ts.map +1 -1
- package/dist/instructor/instructor.service.js +70 -18
- package/dist/instructor/instructor.service.js.map +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js.map +1 -1
- package/hedhog/data/route.yaml +23 -1
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +42 -24
- package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +591 -0
- package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +7 -2
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +17 -17
- package/hedhog/frontend/app/classes/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +3 -33
- package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +9 -9
- package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +109 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +42 -15
- package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +76 -81
- package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +60 -0
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +3 -3
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-scheduled-classes-tab.tsx.ejs +406 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +1 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +242 -33
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +228 -152
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +71 -31
- package/hedhog/frontend/app/courses/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +37 -41
- package/hedhog/frontend/app/enterprise/_components/enterprise-activity-timeline.tsx.ejs +87 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +4 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +31 -5
- package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +79 -20
- package/hedhog/frontend/app/enterprise/_components/enterprise-company-identity-card.tsx.ejs +11 -2
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +201 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +55 -24
- package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +430 -296
- package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +277 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-overview-analytics.tsx.ejs +205 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-person-edit-sheet.tsx.ejs +97 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +82 -57
- package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +4 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +60 -22
- package/hedhog/frontend/app/enterprise/_components/enterprise-types.ts.ejs +54 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +211 -0
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -7
- package/hedhog/frontend/app/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +1 -1
- package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/exams/page.tsx.ejs +12 -3
- package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +51 -104
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +712 -427
- package/hedhog/frontend/app/instructors/page.tsx.ejs +77 -53
- package/hedhog/frontend/app/paths/page.tsx.ejs +14 -5
- package/hedhog/frontend/app/reports/courses/page.tsx.ejs +5 -5
- package/hedhog/frontend/app/reports/dashboard/page.tsx.ejs +8 -8
- package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/reports/page.tsx.ejs +7 -7
- package/hedhog/frontend/app/reports/students/page.tsx.ejs +6 -6
- package/hedhog/frontend/app/training/page.tsx.ejs +8 -3
- package/hedhog/frontend/messages/en.json +394 -55
- package/hedhog/frontend/messages/pt.json +389 -48
- package/hedhog/frontend/widgets/active-classes-kpi.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/active-courses-kpi.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/approval-rate-kpi.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/class-calendar.tsx.ejs +2 -2
- package/hedhog/frontend/widgets/completion-rate-kpi.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/issued-certificates-kpi.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/total-students-kpi.tsx.ejs +1 -1
- package/hedhog/table/enterprise_student_license_event.yaml +30 -0
- package/hedhog/table/instructor_qualification.yaml +1 -1
- package/hedhog/table/instructor_skill.yaml +0 -11
- package/package.json +8 -8
- package/src/course/course.service.ts +12 -24
- package/src/enterprise/enterprise.controller.ts +5 -0
- package/src/enterprise/enterprise.service.ts +507 -29
- package/src/enterprise/training/training-admin.controller.ts +4 -0
- package/src/enterprise/training/training-admin.service.ts +115 -51
- package/src/instructor/dto/create-instructor-skill.dto.ts +0 -17
- package/src/instructor/dto/update-instructor-skill.dto.ts +0 -18
- package/src/instructor/instructor-skill.service.ts +2 -97
- package/src/instructor/instructor.controller.ts +16 -0
- package/src/instructor/instructor.service.ts +85 -10
- 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 {
|
|
36
|
-
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
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
|
|
179
|
-
if (!watchedPersonId) return;
|
|
243
|
+
const handleEditSelection = async (personId: number) => {
|
|
180
244
|
const response = await request<Person>({
|
|
181
|
-
url: `/person/${
|
|
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 —
|
|
214
|
-
const {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
220
|
-
{ data
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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:
|
|
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)
|
|
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('
|
|
417
|
+
toast.success(t('form.toasts.skillsSaved'));
|
|
291
418
|
} catch {
|
|
292
|
-
toast.error('
|
|
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('
|
|
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('
|
|
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
|
-
|
|
358
|
-
<
|
|
359
|
-
<
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
</
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
<
|
|
394
|
-
{
|
|
395
|
-
|
|
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
|
-
|
|
546
|
+
</>
|
|
400
547
|
) : (
|
|
401
548
|
<SheetHeader className="shrink-0 border-b px-6 py-4">
|
|
402
|
-
<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">
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
<TabsTrigger value="
|
|
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
|
-
|
|
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="
|
|
595
|
+
name="personId"
|
|
512
596
|
render={({ field }) => (
|
|
513
|
-
<
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
674
|
+
{t('form.fields.canTeachCourses')}
|
|
527
675
|
</p>
|
|
528
676
|
<p className="text-xs text-muted-foreground">
|
|
529
|
-
|
|
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={
|
|
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
|
-
|
|
706
|
+
{t('form.fields.qualifications')}{' '}
|
|
591
707
|
<span className="text-destructive">*</span>
|
|
592
708
|
</FieldLabel>
|
|
593
709
|
<div className="space-y-3">
|
|
594
|
-
{
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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={
|
|
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
|
-
|
|
824
|
+
{t('form.skills.none')}
|
|
705
825
|
</p>
|
|
706
826
|
)}
|
|
707
827
|
|
|
708
828
|
<EntityPicker
|
|
709
829
|
key={pickerResetKey}
|
|
710
|
-
placeholder=
|
|
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=
|
|
717
|
-
createTitle=
|
|
718
|
-
createDescription=
|
|
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: '
|
|
861
|
+
label: t('form.skills.createFields.name'),
|
|
742
862
|
required: true,
|
|
743
|
-
placeholder:
|
|
863
|
+
placeholder: t(
|
|
864
|
+
'form.skills.createFields.namePlaceholder'
|
|
865
|
+
),
|
|
744
866
|
},
|
|
745
867
|
{
|
|
746
868
|
name: 'slug',
|
|
747
|
-
label: '
|
|
748
|
-
placeholder:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
816
|
-
|
|
817
|
-
<div className="
|
|
818
|
-
<
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
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
|
-
<
|
|
832
|
-
{
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1143
|
+
{t('form.evaluations.comingSoonTitle')}
|
|
890
1144
|
</p>
|
|
891
1145
|
<p className="mt-1 text-xs text-muted-foreground">
|
|
892
|
-
|
|
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
|
-
|
|
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="
|
|
1167
|
+
name="personId"
|
|
981
1168
|
render={({ field }) => (
|
|
982
|
-
<
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
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">
|
|
1243
|
+
<p className="text-sm font-medium">
|
|
1244
|
+
{t('form.fields.canTeachCourses')}
|
|
1245
|
+
</p>
|
|
995
1246
|
<p className="text-xs text-muted-foreground">
|
|
996
|
-
|
|
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={
|
|
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
|
-
|
|
1276
|
+
{t('form.fields.qualifications')}{' '}
|
|
1277
|
+
<span className="text-destructive">*</span>
|
|
1015
1278
|
</FieldLabel>
|
|
1016
1279
|
<div className="space-y-3">
|
|
1017
|
-
{
|
|
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
|
-
|
|
1038
|
-
field.
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
});
|