@hed-hog/lms 0.0.325 → 0.0.326
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/course/course.service.d.ts +3 -1
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +35 -5
- package/dist/course/course.service.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/instructor/dto/create-instructor-skill.dto.d.ts +9 -0
- package/dist/instructor/dto/create-instructor-skill.dto.d.ts.map +1 -0
- package/dist/instructor/dto/create-instructor-skill.dto.js +48 -0
- package/dist/instructor/dto/create-instructor-skill.dto.js.map +1 -0
- package/dist/instructor/dto/create-instructor.dto.d.ts +2 -0
- package/dist/instructor/dto/create-instructor.dto.d.ts.map +1 -1
- package/dist/instructor/dto/create-instructor.dto.js +12 -0
- package/dist/instructor/dto/create-instructor.dto.js.map +1 -1
- package/dist/instructor/dto/update-instructor-skill.dto.d.ts +9 -0
- package/dist/instructor/dto/update-instructor-skill.dto.d.ts.map +1 -0
- package/dist/instructor/dto/update-instructor-skill.dto.js +50 -0
- package/dist/instructor/dto/update-instructor-skill.dto.js.map +1 -0
- package/dist/instructor/dto/update-instructor.dto.d.ts +2 -0
- package/dist/instructor/dto/update-instructor.dto.d.ts.map +1 -1
- package/dist/instructor/dto/update-instructor.dto.js +12 -0
- package/dist/instructor/dto/update-instructor.dto.js.map +1 -1
- package/dist/instructor/instructor-skill.controller.d.ts +38 -0
- package/dist/instructor/instructor-skill.controller.d.ts.map +1 -0
- package/dist/instructor/instructor-skill.controller.js +89 -0
- package/dist/instructor/instructor-skill.controller.js.map +1 -0
- package/dist/instructor/instructor-skill.service.d.ts +48 -0
- package/dist/instructor/instructor-skill.service.d.ts.map +1 -0
- package/dist/instructor/instructor-skill.service.js +203 -0
- package/dist/instructor/instructor-skill.service.js.map +1 -0
- package/dist/instructor/instructor.controller.d.ts +26 -0
- package/dist/instructor/instructor.controller.d.ts.map +1 -1
- package/dist/instructor/instructor.module.d.ts.map +1 -1
- package/dist/instructor/instructor.module.js +4 -2
- package/dist/instructor/instructor.module.js.map +1 -1
- package/dist/instructor/instructor.service.d.ts +35 -0
- package/dist/instructor/instructor.service.d.ts.map +1 -1
- package/dist/instructor/instructor.service.js +132 -11
- package/dist/instructor/instructor.service.js.map +1 -1
- package/dist/training/training.service.d.ts +3 -1
- package/dist/training/training.service.d.ts.map +1 -1
- package/dist/training/training.service.js +34 -4
- package/dist/training/training.service.js.map +1 -1
- package/hedhog/data/integration_event_catalog.yaml +219 -0
- package/hedhog/data/menu.yaml +23 -6
- package/hedhog/data/route.yaml +45 -0
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +1 -1
- package/hedhog/frontend/app/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +1 -1
- package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +547 -0
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +845 -239
- package/hedhog/frontend/app/instructors/_components/instructor-types.ts.ejs +9 -0
- package/hedhog/frontend/app/instructors/page.tsx.ejs +69 -20
- package/hedhog/table/instructor.yaml +5 -0
- package/hedhog/table/instructor_skill.yaml +26 -0
- package/hedhog/table/instructor_skill_assignment.yaml +22 -0
- package/package.json +7 -7
- package/src/course/course.service.ts +38 -4
- package/src/index.ts +2 -0
- package/src/instructor/dto/create-instructor-skill.dto.ts +28 -0
- package/src/instructor/dto/create-instructor.dto.ts +20 -8
- package/src/instructor/dto/update-instructor-skill.dto.ts +30 -0
- package/src/instructor/dto/update-instructor.dto.ts +18 -6
- package/src/instructor/instructor-skill.controller.ts +60 -0
- package/src/instructor/instructor-skill.service.ts +214 -0
- package/src/instructor/instructor.module.ts +4 -2
- package/src/instructor/instructor.service.ts +148 -0
- package/src/training/training.service.ts +38 -4
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { PersonPicker } from '@/app/(app)/(libraries)/contact/_components/person-picker';
|
|
4
|
+
import { PersonFormSheet } from '@/app/(app)/(libraries)/contact/person/_components/person-form-sheet';
|
|
5
|
+
import type {
|
|
6
|
+
ContactTypeOption,
|
|
7
|
+
DocumentTypeOption,
|
|
8
|
+
Person,
|
|
9
|
+
} from '@/app/(app)/(libraries)/contact/person/_components/person-types';
|
|
10
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
11
|
+
import { Badge } from '@/components/ui/badge';
|
|
3
12
|
import { Button } from '@/components/ui/button';
|
|
4
|
-
import { EntityPicker } from '@/components/ui/entity-picker';
|
|
5
13
|
import { Field, FieldError, FieldLabel } from '@/components/ui/field';
|
|
14
|
+
import { InputMoney } from '@/components/ui/input-money';
|
|
6
15
|
import {
|
|
7
16
|
Select,
|
|
8
17
|
SelectContent,
|
|
@@ -19,15 +28,16 @@ import {
|
|
|
19
28
|
SheetTitle,
|
|
20
29
|
} from '@/components/ui/sheet';
|
|
21
30
|
import { Switch } from '@/components/ui/switch';
|
|
31
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
22
32
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
23
33
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
24
|
-
import {
|
|
34
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
35
|
+
import { BookOpen, Loader2, Pencil, Star, X } from 'lucide-react';
|
|
25
36
|
import { useEffect, useState } from 'react';
|
|
26
37
|
import { Controller, useForm } from 'react-hook-form';
|
|
27
38
|
import { toast } from 'sonner';
|
|
28
39
|
import { z } from 'zod';
|
|
29
|
-
import {
|
|
30
|
-
import type { InstructorRow, PersonOption } from './instructor-types';
|
|
40
|
+
import type { InstructorRow, InstructorSkill } from './instructor-types';
|
|
31
41
|
|
|
32
42
|
const QUALIFICATION_OPTIONS = [
|
|
33
43
|
{
|
|
@@ -52,10 +62,32 @@ const instructorFormSchema = z.object({
|
|
|
52
62
|
.min(1, 'Selecione pelo menos uma qualificação'),
|
|
53
63
|
status: z.enum(['active', 'inactive']),
|
|
54
64
|
can_teach_courses: z.boolean(),
|
|
65
|
+
hourlyRate: z.coerce.number().min(0).optional().nullable(),
|
|
66
|
+
skillSlugs: z.array(z.string()).optional().default([]),
|
|
55
67
|
});
|
|
56
68
|
|
|
57
69
|
type InstructorFormValues = z.infer<typeof instructorFormSchema>;
|
|
58
70
|
|
|
71
|
+
type ClassGroupItem = {
|
|
72
|
+
id: number;
|
|
73
|
+
name: string;
|
|
74
|
+
code: string;
|
|
75
|
+
courseName: string | null;
|
|
76
|
+
startDate: string | null;
|
|
77
|
+
endDate: string | null;
|
|
78
|
+
status: string;
|
|
79
|
+
slots: number;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const STATUS_LABEL: Record<string, string> = {
|
|
83
|
+
active: 'Ativo',
|
|
84
|
+
inactive: 'Inativo',
|
|
85
|
+
in_progress: 'Em andamento',
|
|
86
|
+
upcoming: 'Futuro',
|
|
87
|
+
completed: 'Concluído',
|
|
88
|
+
cancelled: 'Cancelado',
|
|
89
|
+
};
|
|
90
|
+
|
|
59
91
|
type InstructorFormSheetProps = {
|
|
60
92
|
open: boolean;
|
|
61
93
|
onOpenChange: (open: boolean) => void;
|
|
@@ -69,18 +101,25 @@ export function InstructorFormSheet({
|
|
|
69
101
|
instructorId,
|
|
70
102
|
onSaved,
|
|
71
103
|
}: InstructorFormSheetProps) {
|
|
72
|
-
const { request } = useApp();
|
|
104
|
+
const { request, currentLocaleCode } = useApp();
|
|
105
|
+
const queryClient = useQueryClient();
|
|
73
106
|
const [saving, setSaving] = useState(false);
|
|
74
|
-
const [
|
|
107
|
+
const [selectedPersonName, setSelectedPersonName] = useState('');
|
|
75
108
|
const [trainingAccess, setTrainingAccess] = useState<boolean | null>(null);
|
|
76
109
|
const [togglingAccess, setTogglingAccess] = useState(false);
|
|
110
|
+
const [editPersonOpen, setEditPersonOpen] = useState(false);
|
|
111
|
+
const [personToEdit, setPersonToEdit] = useState<Person | null>(null);
|
|
112
|
+
const [activeTab, setActiveTab] = useState('detalhes');
|
|
113
|
+
const [tabSkillSlugs, setTabSkillSlugs] = useState<string[]>([]);
|
|
114
|
+
const [savingSkills, setSavingSkills] = useState(false);
|
|
115
|
+
const [addSkillValue, setAddSkillValue] = useState('');
|
|
77
116
|
const isEditing = typeof instructorId === 'number' && instructorId > 0;
|
|
78
117
|
|
|
79
118
|
const {
|
|
80
119
|
control,
|
|
81
120
|
handleSubmit,
|
|
82
121
|
reset,
|
|
83
|
-
|
|
122
|
+
watch,
|
|
84
123
|
formState: { errors },
|
|
85
124
|
} = useForm<InstructorFormValues>({
|
|
86
125
|
resolver: zodResolver(instructorFormSchema),
|
|
@@ -92,6 +131,44 @@ export function InstructorFormSheet({
|
|
|
92
131
|
},
|
|
93
132
|
});
|
|
94
133
|
|
|
134
|
+
const watchedPersonId = watch('personId');
|
|
135
|
+
|
|
136
|
+
const { data: contactTypes = [] } = useQuery<ContactTypeOption[]>({
|
|
137
|
+
queryKey: ['contact-person-contact-types', currentLocaleCode],
|
|
138
|
+
queryFn: async () => {
|
|
139
|
+
const response = await request<{ data: ContactTypeOption[] }>({
|
|
140
|
+
url: '/person-contact-type?pageSize=100',
|
|
141
|
+
method: 'GET',
|
|
142
|
+
});
|
|
143
|
+
return response.data.data || [];
|
|
144
|
+
},
|
|
145
|
+
enabled: open,
|
|
146
|
+
placeholderData: (previous) => previous ?? [],
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
const { data: documentTypes = [] } = useQuery<DocumentTypeOption[]>({
|
|
150
|
+
queryKey: ['contact-person-document-types', currentLocaleCode],
|
|
151
|
+
queryFn: async () => {
|
|
152
|
+
const response = await request<{ data: DocumentTypeOption[] }>({
|
|
153
|
+
url: '/person-document-type?pageSize=100',
|
|
154
|
+
method: 'GET',
|
|
155
|
+
});
|
|
156
|
+
return response.data.data || [];
|
|
157
|
+
},
|
|
158
|
+
enabled: open,
|
|
159
|
+
placeholderData: (previous) => previous ?? [],
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const handleOpenPersonEdit = async () => {
|
|
163
|
+
if (!watchedPersonId) return;
|
|
164
|
+
const response = await request<Person>({
|
|
165
|
+
url: `/person/${watchedPersonId}`,
|
|
166
|
+
method: 'GET',
|
|
167
|
+
});
|
|
168
|
+
setPersonToEdit(response.data);
|
|
169
|
+
setEditPersonOpen(true);
|
|
170
|
+
};
|
|
171
|
+
|
|
95
172
|
const { data: existingInstructor } = useQuery<InstructorRow | null>({
|
|
96
173
|
queryKey: ['lms-instructor-detail', instructorId],
|
|
97
174
|
queryFn: async () => {
|
|
@@ -106,6 +183,38 @@ export function InstructorFormSheet({
|
|
|
106
183
|
placeholderData: null,
|
|
107
184
|
});
|
|
108
185
|
|
|
186
|
+
const { data: skillsData = [] } = useQuery<InstructorSkill[]>({
|
|
187
|
+
queryKey: ['lms-instructor-skills-all'],
|
|
188
|
+
queryFn: async () => {
|
|
189
|
+
const response = await request<InstructorSkill[]>({
|
|
190
|
+
url: '/lms/instructor-skills/all',
|
|
191
|
+
method: 'GET',
|
|
192
|
+
});
|
|
193
|
+
return response.data;
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Turmas — enterprise/training/admin endpoint supports instructorId filter
|
|
198
|
+
const { data: turmasData = [], isLoading: loadingTurmas } = useQuery<
|
|
199
|
+
ClassGroupItem[]
|
|
200
|
+
>({
|
|
201
|
+
queryKey: ['lms-instructor-turmas', instructorId],
|
|
202
|
+
queryFn: async () => {
|
|
203
|
+
const response = await request<
|
|
204
|
+
{ data?: ClassGroupItem[] } | ClassGroupItem[]
|
|
205
|
+
>({
|
|
206
|
+
url: `/lms/enterprise/training/admin/class-groups?instructorId=${instructorId}`,
|
|
207
|
+
method: 'GET',
|
|
208
|
+
});
|
|
209
|
+
const raw = response.data;
|
|
210
|
+
if (Array.isArray(raw)) return raw;
|
|
211
|
+
if (raw && 'data' in raw && Array.isArray(raw.data)) return raw.data;
|
|
212
|
+
return [];
|
|
213
|
+
},
|
|
214
|
+
enabled: isEditing && open && activeTab === 'turmas',
|
|
215
|
+
placeholderData: [],
|
|
216
|
+
});
|
|
217
|
+
|
|
109
218
|
useEffect(() => {
|
|
110
219
|
if (isEditing && existingInstructor) {
|
|
111
220
|
reset({
|
|
@@ -116,19 +225,32 @@ export function InstructorFormSheet({
|
|
|
116
225
|
: ['course-lessons'],
|
|
117
226
|
status: existingInstructor.status,
|
|
118
227
|
can_teach_courses: true,
|
|
228
|
+
hourlyRate: existingInstructor.hourlyRate ?? null,
|
|
229
|
+
skillSlugs: (existingInstructor.skills ?? []).map((s) => s.slug),
|
|
119
230
|
});
|
|
231
|
+
setSelectedPersonName(existingInstructor.name);
|
|
120
232
|
setTrainingAccess(existingInstructor.hasTrainingAccess ?? false);
|
|
233
|
+
setTabSkillSlugs((existingInstructor.skills ?? []).map((s) => s.slug));
|
|
121
234
|
} else if (!isEditing && open) {
|
|
122
235
|
reset({
|
|
123
236
|
personId: undefined,
|
|
124
237
|
qualificationSlugs: ['course-lessons'],
|
|
125
238
|
status: 'active',
|
|
126
239
|
can_teach_courses: true,
|
|
240
|
+
hourlyRate: null,
|
|
241
|
+
skillSlugs: [],
|
|
127
242
|
});
|
|
243
|
+
setSelectedPersonName('');
|
|
128
244
|
setTrainingAccess(null);
|
|
245
|
+
setTabSkillSlugs([]);
|
|
129
246
|
}
|
|
130
247
|
}, [isEditing, existingInstructor, open, reset]);
|
|
131
248
|
|
|
249
|
+
// Reset to first tab when sheet closes
|
|
250
|
+
useEffect(() => {
|
|
251
|
+
if (!open) setActiveTab('detalhes');
|
|
252
|
+
}, [open]);
|
|
253
|
+
|
|
132
254
|
const handleSheetClose = (nextOpen: boolean) => {
|
|
133
255
|
if (!nextOpen) {
|
|
134
256
|
reset();
|
|
@@ -136,6 +258,27 @@ export function InstructorFormSheet({
|
|
|
136
258
|
onOpenChange(nextOpen);
|
|
137
259
|
};
|
|
138
260
|
|
|
261
|
+
const handleSaveSkills = async () => {
|
|
262
|
+
if (!instructorId) return;
|
|
263
|
+
setSavingSkills(true);
|
|
264
|
+
try {
|
|
265
|
+
await request({
|
|
266
|
+
url: `/lms/instructors/${instructorId}`,
|
|
267
|
+
method: 'PATCH',
|
|
268
|
+
data: { skillSlugs: tabSkillSlugs },
|
|
269
|
+
});
|
|
270
|
+
await queryClient.invalidateQueries({
|
|
271
|
+
queryKey: ['lms-instructor-detail', instructorId],
|
|
272
|
+
});
|
|
273
|
+
await queryClient.invalidateQueries({ queryKey: ['lms-instructors'] });
|
|
274
|
+
toast.success('Skills atualizadas com sucesso.');
|
|
275
|
+
} catch {
|
|
276
|
+
toast.error('Erro ao salvar skills.');
|
|
277
|
+
} finally {
|
|
278
|
+
setSavingSkills(false);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
139
282
|
const onSubmit = async (values: InstructorFormValues) => {
|
|
140
283
|
try {
|
|
141
284
|
setSaving(true);
|
|
@@ -147,8 +290,11 @@ export function InstructorFormSheet({
|
|
|
147
290
|
url: `/lms/instructors/${instructorId}`,
|
|
148
291
|
method: 'PATCH',
|
|
149
292
|
data: {
|
|
293
|
+
personId: values.personId,
|
|
150
294
|
qualificationSlugs: values.qualificationSlugs,
|
|
151
295
|
status: values.status,
|
|
296
|
+
hourlyRate: values.hourlyRate ?? null,
|
|
297
|
+
skillSlugs: values.skillSlugs ?? [],
|
|
152
298
|
},
|
|
153
299
|
});
|
|
154
300
|
result = response.data;
|
|
@@ -162,6 +308,8 @@ export function InstructorFormSheet({
|
|
|
162
308
|
name: '',
|
|
163
309
|
qualificationSlugs: values.qualificationSlugs,
|
|
164
310
|
status: values.status,
|
|
311
|
+
hourlyRate: values.hourlyRate ?? null,
|
|
312
|
+
skillSlugs: values.skillSlugs ?? [],
|
|
165
313
|
},
|
|
166
314
|
});
|
|
167
315
|
result = response.data;
|
|
@@ -186,253 +334,711 @@ export function InstructorFormSheet({
|
|
|
186
334
|
<Sheet open={open} onOpenChange={handleSheetClose}>
|
|
187
335
|
<SheetContent
|
|
188
336
|
side="right"
|
|
189
|
-
className="flex w-full max-w-
|
|
337
|
+
className="flex w-full max-w-lg flex-col overflow-hidden p-0 sm:max-w-xl"
|
|
190
338
|
>
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
339
|
+
{/* ── Instructor header (edit mode) ── */}
|
|
340
|
+
{isEditing && existingInstructor ? (
|
|
341
|
+
<div className="flex shrink-0 items-center gap-4 border-b px-6 py-4">
|
|
342
|
+
<Avatar className="h-14 w-14">
|
|
343
|
+
<AvatarImage
|
|
344
|
+
src={
|
|
345
|
+
existingInstructor.avatarId
|
|
346
|
+
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${existingInstructor.avatarId}`
|
|
347
|
+
: undefined
|
|
348
|
+
}
|
|
349
|
+
alt={existingInstructor.name}
|
|
350
|
+
/>
|
|
351
|
+
<AvatarFallback>
|
|
352
|
+
{existingInstructor.name.slice(0, 2).toUpperCase()}
|
|
353
|
+
</AvatarFallback>
|
|
354
|
+
</Avatar>
|
|
355
|
+
<div className="min-w-0 flex-1">
|
|
356
|
+
<h2 className="truncate text-lg font-semibold">
|
|
357
|
+
{existingInstructor.name}
|
|
358
|
+
</h2>
|
|
359
|
+
<div className="mt-1 flex flex-wrap items-center gap-2">
|
|
360
|
+
<Badge
|
|
361
|
+
variant={
|
|
362
|
+
existingInstructor.status === 'active'
|
|
363
|
+
? 'default'
|
|
364
|
+
: 'secondary'
|
|
365
|
+
}
|
|
366
|
+
>
|
|
367
|
+
{existingInstructor.status === 'active'
|
|
368
|
+
? 'Ativo'
|
|
369
|
+
: 'Inativo'}
|
|
370
|
+
</Badge>
|
|
371
|
+
{existingInstructor.email && (
|
|
372
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
373
|
+
{existingInstructor.email}
|
|
374
|
+
</span>
|
|
375
|
+
)}
|
|
376
|
+
{existingInstructor.phone && (
|
|
377
|
+
<span className="text-xs text-muted-foreground">
|
|
378
|
+
{existingInstructor.phone}
|
|
379
|
+
</span>
|
|
380
|
+
)}
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
) : (
|
|
385
|
+
<SheetHeader className="shrink-0 border-b px-6 py-4">
|
|
386
|
+
<SheetTitle>Novo Instrutor</SheetTitle>
|
|
387
|
+
<SheetDescription>
|
|
388
|
+
Vincule uma pessoa existente ou crie uma nova para cadastrá-la
|
|
389
|
+
como instrutora.
|
|
390
|
+
</SheetDescription>
|
|
391
|
+
</SheetHeader>
|
|
392
|
+
)}
|
|
393
|
+
|
|
394
|
+
{/* ── Tabs (edit) / plain form (create) ── */}
|
|
395
|
+
{isEditing ? (
|
|
396
|
+
<Tabs
|
|
397
|
+
value={activeTab}
|
|
398
|
+
onValueChange={setActiveTab}
|
|
399
|
+
className="flex min-h-0 flex-1 flex-col"
|
|
400
|
+
>
|
|
401
|
+
<TabsList className="mx-6 mt-3 shrink-0 justify-start">
|
|
402
|
+
<TabsTrigger value="detalhes">Detalhes</TabsTrigger>
|
|
403
|
+
<TabsTrigger value="skills">Skills</TabsTrigger>
|
|
404
|
+
<TabsTrigger value="turmas">Turmas</TabsTrigger>
|
|
405
|
+
<TabsTrigger value="avaliacoes">Avaliações</TabsTrigger>
|
|
406
|
+
</TabsList>
|
|
407
|
+
|
|
408
|
+
{/* ── Detalhes ── */}
|
|
409
|
+
<TabsContent
|
|
410
|
+
value="detalhes"
|
|
411
|
+
className="flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
|
|
412
|
+
>
|
|
413
|
+
<form
|
|
414
|
+
onSubmit={handleSubmit(onSubmit)}
|
|
415
|
+
className="flex min-h-0 flex-1 flex-col"
|
|
416
|
+
>
|
|
417
|
+
<div className="flex-1 overflow-y-auto px-6 py-4">
|
|
418
|
+
<div className="flex flex-col gap-5">
|
|
419
|
+
{/* Person picker */}
|
|
420
|
+
<Field>
|
|
421
|
+
<FieldLabel>
|
|
422
|
+
Pessoa <span className="text-destructive">*</span>
|
|
423
|
+
</FieldLabel>
|
|
424
|
+
<div className="flex items-center gap-2">
|
|
425
|
+
<div className="min-w-0 flex-1">
|
|
426
|
+
<Controller
|
|
427
|
+
control={control}
|
|
428
|
+
name="personId"
|
|
429
|
+
render={({ field }) => (
|
|
430
|
+
<PersonPicker
|
|
431
|
+
label=""
|
|
432
|
+
entityLabel="pessoa"
|
|
433
|
+
value={field.value ?? null}
|
|
434
|
+
initialSelectedLabel={selectedPersonName}
|
|
435
|
+
onChange={(personId, personName) => {
|
|
436
|
+
field.onChange(personId ?? undefined);
|
|
437
|
+
setSelectedPersonName(personName);
|
|
438
|
+
}}
|
|
439
|
+
selectPlaceholder="Buscar ou selecionar pessoa..."
|
|
440
|
+
personTypeFilter="individual"
|
|
441
|
+
createType="individual"
|
|
442
|
+
lockCreateType
|
|
443
|
+
/>
|
|
444
|
+
)}
|
|
445
|
+
/>
|
|
446
|
+
</div>
|
|
447
|
+
<Button
|
|
448
|
+
type="button"
|
|
449
|
+
variant="outline"
|
|
450
|
+
size="icon"
|
|
451
|
+
className="shrink-0"
|
|
452
|
+
disabled={!watchedPersonId}
|
|
453
|
+
onClick={handleOpenPersonEdit}
|
|
454
|
+
aria-label="Editar cadastro da pessoa"
|
|
455
|
+
title="Editar cadastro da pessoa"
|
|
456
|
+
>
|
|
457
|
+
<Pencil className="h-4 w-4" />
|
|
458
|
+
</Button>
|
|
459
|
+
</div>
|
|
460
|
+
{errors.personId && (
|
|
461
|
+
<FieldError>{errors.personId.message}</FieldError>
|
|
462
|
+
)}
|
|
463
|
+
</Field>
|
|
464
|
+
|
|
465
|
+
{/* Status */}
|
|
466
|
+
<Field>
|
|
467
|
+
<FieldLabel>Status</FieldLabel>
|
|
468
|
+
<Controller
|
|
469
|
+
control={control}
|
|
470
|
+
name="status"
|
|
471
|
+
render={({ field }) => (
|
|
472
|
+
<Select
|
|
473
|
+
value={field.value}
|
|
474
|
+
onValueChange={field.onChange}
|
|
475
|
+
>
|
|
476
|
+
<SelectTrigger className="w-full">
|
|
477
|
+
<SelectValue placeholder="Selecione o status" />
|
|
478
|
+
</SelectTrigger>
|
|
479
|
+
<SelectContent>
|
|
480
|
+
<SelectItem value="active">Ativo</SelectItem>
|
|
481
|
+
<SelectItem value="inactive">
|
|
482
|
+
Inativo
|
|
483
|
+
</SelectItem>
|
|
484
|
+
</SelectContent>
|
|
485
|
+
</Select>
|
|
486
|
+
)}
|
|
487
|
+
/>
|
|
488
|
+
</Field>
|
|
489
|
+
|
|
490
|
+
{/* Valor/hora */}
|
|
491
|
+
<Field>
|
|
492
|
+
<FieldLabel>Valor/hora (R$)</FieldLabel>
|
|
493
|
+
<Controller
|
|
494
|
+
control={control}
|
|
495
|
+
name="hourlyRate"
|
|
496
|
+
render={({ field }) => (
|
|
497
|
+
<InputMoney
|
|
498
|
+
placeholder="Ex: 150,00"
|
|
499
|
+
value={field.value ?? ''}
|
|
500
|
+
onValueChange={(value) => field.onChange(value)}
|
|
501
|
+
/>
|
|
502
|
+
)}
|
|
503
|
+
/>
|
|
504
|
+
</Field>
|
|
505
|
+
|
|
506
|
+
{/* can_teach_courses */}
|
|
507
|
+
<div className="flex items-center justify-between rounded-lg border p-4">
|
|
508
|
+
<div className="space-y-0.5">
|
|
509
|
+
<p className="text-sm font-medium">
|
|
510
|
+
Pode ensinar cursos
|
|
511
|
+
</p>
|
|
512
|
+
<p className="text-xs text-muted-foreground">
|
|
513
|
+
Permite que o instrutor seja vinculado a aulas de
|
|
514
|
+
curso.
|
|
515
|
+
</p>
|
|
516
|
+
</div>
|
|
517
|
+
<Controller
|
|
518
|
+
control={control}
|
|
519
|
+
name="can_teach_courses"
|
|
520
|
+
render={({ field }) => (
|
|
521
|
+
<Switch
|
|
522
|
+
checked={field.value}
|
|
523
|
+
onCheckedChange={field.onChange}
|
|
524
|
+
/>
|
|
525
|
+
)}
|
|
526
|
+
/>
|
|
527
|
+
</div>
|
|
528
|
+
|
|
529
|
+
{/* Acesso ao Training */}
|
|
530
|
+
<div className="flex items-center justify-between rounded-lg border p-4">
|
|
531
|
+
<div className="space-y-0.5">
|
|
532
|
+
<p className="text-sm font-medium">
|
|
533
|
+
Acesso ao Hcode Training
|
|
534
|
+
</p>
|
|
535
|
+
<p className="text-xs text-muted-foreground">
|
|
536
|
+
{existingInstructor?.userId
|
|
537
|
+
? 'Permite que este instrutor acesse a plataforma com o perfil de instrutor.'
|
|
538
|
+
: 'Nenhum usuário vinculado a este cadastro. Vincule um usuário para habilitar.'}
|
|
539
|
+
</p>
|
|
540
|
+
</div>
|
|
541
|
+
<Switch
|
|
542
|
+
checked={trainingAccess ?? false}
|
|
543
|
+
disabled={
|
|
544
|
+
!existingInstructor?.userId || togglingAccess
|
|
224
545
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
})),
|
|
247
|
-
hasMore: page < (response.data?.lastPage ?? 1),
|
|
248
|
-
};
|
|
546
|
+
onCheckedChange={async (next) => {
|
|
547
|
+
setTogglingAccess(true);
|
|
548
|
+
try {
|
|
549
|
+
await request({
|
|
550
|
+
url: `/lms/instructors/${instructorId}/training-access`,
|
|
551
|
+
method: 'PATCH',
|
|
552
|
+
data: { enabled: next },
|
|
553
|
+
});
|
|
554
|
+
setTrainingAccess(next);
|
|
555
|
+
toast.success(
|
|
556
|
+
next
|
|
557
|
+
? 'Acesso ao Training habilitado.'
|
|
558
|
+
: 'Acesso ao Training desabilitado.'
|
|
559
|
+
);
|
|
560
|
+
} catch {
|
|
561
|
+
toast.error(
|
|
562
|
+
'Erro ao alterar acesso ao Training.'
|
|
563
|
+
);
|
|
564
|
+
} finally {
|
|
565
|
+
setTogglingAccess(false);
|
|
566
|
+
}
|
|
249
567
|
}}
|
|
250
|
-
getOptionValue={(opt) => opt.id as number}
|
|
251
|
-
getOptionLabel={(opt) => opt.name ?? ''}
|
|
252
|
-
placeholder="Buscar pessoa por nome..."
|
|
253
|
-
searchable
|
|
254
|
-
clearable
|
|
255
|
-
showCreateButton={false}
|
|
256
568
|
/>
|
|
569
|
+
</div>
|
|
570
|
+
|
|
571
|
+
{/* Qualificações */}
|
|
572
|
+
<Field>
|
|
573
|
+
<FieldLabel>
|
|
574
|
+
Qualificações{' '}
|
|
575
|
+
<span className="text-destructive">*</span>
|
|
576
|
+
</FieldLabel>
|
|
577
|
+
<div className="space-y-3">
|
|
578
|
+
{QUALIFICATION_OPTIONS.map((option) => (
|
|
579
|
+
<Controller
|
|
580
|
+
key={option.slug}
|
|
581
|
+
control={control}
|
|
582
|
+
name="qualificationSlugs"
|
|
583
|
+
render={({ field }) => {
|
|
584
|
+
const checked = field.value.includes(
|
|
585
|
+
option.slug
|
|
586
|
+
);
|
|
587
|
+
return (
|
|
588
|
+
<div className="flex items-start justify-between rounded-lg border p-3">
|
|
589
|
+
<div className="space-y-0.5">
|
|
590
|
+
<p className="text-sm font-medium">
|
|
591
|
+
{option.label}
|
|
592
|
+
</p>
|
|
593
|
+
<p className="text-xs text-muted-foreground">
|
|
594
|
+
{option.description}
|
|
595
|
+
</p>
|
|
596
|
+
</div>
|
|
597
|
+
<Switch
|
|
598
|
+
checked={checked}
|
|
599
|
+
onCheckedChange={(next) => {
|
|
600
|
+
if (next) {
|
|
601
|
+
field.onChange([
|
|
602
|
+
...field.value,
|
|
603
|
+
option.slug,
|
|
604
|
+
]);
|
|
605
|
+
} else {
|
|
606
|
+
field.onChange(
|
|
607
|
+
field.value.filter(
|
|
608
|
+
(s) => s !== option.slug
|
|
609
|
+
)
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
}}
|
|
613
|
+
/>
|
|
614
|
+
</div>
|
|
615
|
+
);
|
|
616
|
+
}}
|
|
617
|
+
/>
|
|
618
|
+
))}
|
|
619
|
+
</div>
|
|
620
|
+
{errors.qualificationSlugs && (
|
|
621
|
+
<FieldError>
|
|
622
|
+
{errors.qualificationSlugs.message}
|
|
623
|
+
</FieldError>
|
|
624
|
+
)}
|
|
625
|
+
</Field>
|
|
626
|
+
</div>
|
|
627
|
+
</div>
|
|
628
|
+
|
|
629
|
+
<SheetFooter className="shrink-0 border-t px-6 py-4">
|
|
630
|
+
<Button
|
|
631
|
+
type="button"
|
|
632
|
+
variant="outline"
|
|
633
|
+
onClick={() => handleSheetClose(false)}
|
|
634
|
+
disabled={saving}
|
|
635
|
+
>
|
|
636
|
+
Cancelar
|
|
637
|
+
</Button>
|
|
638
|
+
<Button type="submit" disabled={saving}>
|
|
639
|
+
{saving && (
|
|
640
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
257
641
|
)}
|
|
258
|
-
|
|
642
|
+
Salvar alterações
|
|
643
|
+
</Button>
|
|
644
|
+
</SheetFooter>
|
|
645
|
+
</form>
|
|
646
|
+
</TabsContent>
|
|
647
|
+
|
|
648
|
+
{/* ── Skills ── */}
|
|
649
|
+
<TabsContent
|
|
650
|
+
value="skills"
|
|
651
|
+
className="flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
|
|
652
|
+
>
|
|
653
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
654
|
+
<div className="flex-1 overflow-y-auto px-6 py-4">
|
|
655
|
+
<p className="mb-4 text-sm text-muted-foreground">
|
|
656
|
+
Gerencie as skills associadas a este instrutor.
|
|
657
|
+
</p>
|
|
658
|
+
|
|
659
|
+
{tabSkillSlugs.length > 0 ? (
|
|
660
|
+
<div className="mb-6 flex flex-wrap gap-2">
|
|
661
|
+
{tabSkillSlugs.map((slug) => {
|
|
662
|
+
const skill = skillsData.find((s) => s.slug === slug);
|
|
663
|
+
return (
|
|
664
|
+
<Badge
|
|
665
|
+
key={slug}
|
|
666
|
+
variant="secondary"
|
|
667
|
+
className="flex items-center gap-1 pr-1"
|
|
668
|
+
>
|
|
669
|
+
<span>{skill?.name ?? slug}</span>
|
|
670
|
+
<button
|
|
671
|
+
type="button"
|
|
672
|
+
className="ml-1 rounded-full p-0.5 hover:bg-muted-foreground/20"
|
|
673
|
+
onClick={() =>
|
|
674
|
+
setTabSkillSlugs((prev) =>
|
|
675
|
+
prev.filter((s) => s !== slug)
|
|
676
|
+
)
|
|
677
|
+
}
|
|
678
|
+
aria-label={`Remover skill ${skill?.name ?? slug}`}
|
|
679
|
+
>
|
|
680
|
+
<X className="h-3 w-3" />
|
|
681
|
+
</button>
|
|
682
|
+
</Badge>
|
|
683
|
+
);
|
|
684
|
+
})}
|
|
685
|
+
</div>
|
|
686
|
+
) : (
|
|
687
|
+
<p className="mb-6 italic text-sm text-muted-foreground">
|
|
688
|
+
Nenhuma skill atribuída.
|
|
689
|
+
</p>
|
|
690
|
+
)}
|
|
691
|
+
|
|
692
|
+
{skillsData.filter((s) => !tabSkillSlugs.includes(s.slug))
|
|
693
|
+
.length > 0 && (
|
|
694
|
+
<Select
|
|
695
|
+
value={addSkillValue}
|
|
696
|
+
onValueChange={(val) => {
|
|
697
|
+
if (val) {
|
|
698
|
+
setTabSkillSlugs((prev) => [...prev, val]);
|
|
699
|
+
setAddSkillValue('');
|
|
700
|
+
}
|
|
701
|
+
}}
|
|
702
|
+
>
|
|
703
|
+
<SelectTrigger className="w-full">
|
|
704
|
+
<SelectValue placeholder="Adicionar skill..." />
|
|
705
|
+
</SelectTrigger>
|
|
706
|
+
<SelectContent>
|
|
707
|
+
{skillsData
|
|
708
|
+
.filter((s) => !tabSkillSlugs.includes(s.slug))
|
|
709
|
+
.map((skill) => (
|
|
710
|
+
<SelectItem key={skill.slug} value={skill.slug}>
|
|
711
|
+
{skill.name}
|
|
712
|
+
</SelectItem>
|
|
713
|
+
))}
|
|
714
|
+
</SelectContent>
|
|
715
|
+
</Select>
|
|
716
|
+
)}
|
|
717
|
+
</div>
|
|
718
|
+
|
|
719
|
+
<div className="shrink-0 border-t px-6 py-4">
|
|
720
|
+
<div className="flex justify-end gap-2">
|
|
721
|
+
<Button
|
|
722
|
+
type="button"
|
|
723
|
+
variant="outline"
|
|
724
|
+
onClick={() => handleSheetClose(false)}
|
|
725
|
+
disabled={savingSkills}
|
|
726
|
+
>
|
|
727
|
+
Cancelar
|
|
728
|
+
</Button>
|
|
729
|
+
<Button
|
|
730
|
+
type="button"
|
|
731
|
+
onClick={handleSaveSkills}
|
|
732
|
+
disabled={savingSkills}
|
|
733
|
+
>
|
|
734
|
+
{savingSkills && (
|
|
735
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
736
|
+
)}
|
|
737
|
+
Salvar skills
|
|
738
|
+
</Button>
|
|
739
|
+
</div>
|
|
259
740
|
</div>
|
|
260
|
-
<Button
|
|
261
|
-
type="button"
|
|
262
|
-
variant="outline"
|
|
263
|
-
size="icon"
|
|
264
|
-
className="shrink-0"
|
|
265
|
-
onClick={() => setCreatePersonOpen(true)}
|
|
266
|
-
aria-label="Criar nova pessoa"
|
|
267
|
-
title="Criar nova pessoa"
|
|
268
|
-
>
|
|
269
|
-
<Plus className="h-4 w-4" />
|
|
270
|
-
</Button>
|
|
271
741
|
</div>
|
|
272
|
-
|
|
273
|
-
<FieldError>{errors.personId.message}</FieldError>
|
|
274
|
-
)}
|
|
275
|
-
</Field>
|
|
276
|
-
)}
|
|
277
|
-
|
|
278
|
-
{/* Status */}
|
|
279
|
-
<Field>
|
|
280
|
-
<FieldLabel>Status</FieldLabel>
|
|
281
|
-
<Controller
|
|
282
|
-
control={control}
|
|
283
|
-
name="status"
|
|
284
|
-
render={({ field }) => (
|
|
285
|
-
<Select value={field.value} onValueChange={field.onChange}>
|
|
286
|
-
<SelectTrigger>
|
|
287
|
-
<SelectValue placeholder="Selecione o status" />
|
|
288
|
-
</SelectTrigger>
|
|
289
|
-
<SelectContent>
|
|
290
|
-
<SelectItem value="active">Ativo</SelectItem>
|
|
291
|
-
<SelectItem value="inactive">Inativo</SelectItem>
|
|
292
|
-
</SelectContent>
|
|
293
|
-
</Select>
|
|
294
|
-
)}
|
|
295
|
-
/>
|
|
296
|
-
</Field>
|
|
297
|
-
|
|
298
|
-
{/* can_teach_courses */}
|
|
299
|
-
<div className="flex items-center justify-between rounded-lg border p-4">
|
|
300
|
-
<div className="space-y-0.5">
|
|
301
|
-
<p className="text-sm font-medium">Pode ensinar cursos</p>
|
|
302
|
-
<p className="text-xs text-muted-foreground">
|
|
303
|
-
Permite que o instrutor seja vinculado a aulas de curso.
|
|
304
|
-
</p>
|
|
305
|
-
</div>
|
|
306
|
-
<Controller
|
|
307
|
-
control={control}
|
|
308
|
-
name="can_teach_courses"
|
|
309
|
-
render={({ field }) => (
|
|
310
|
-
<Switch
|
|
311
|
-
checked={field.value}
|
|
312
|
-
onCheckedChange={field.onChange}
|
|
313
|
-
/>
|
|
314
|
-
)}
|
|
315
|
-
/>
|
|
316
|
-
</div>
|
|
742
|
+
</TabsContent>
|
|
317
743
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
744
|
+
{/* ── Turmas ── */}
|
|
745
|
+
<TabsContent
|
|
746
|
+
value="turmas"
|
|
747
|
+
className="flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
|
|
748
|
+
>
|
|
749
|
+
<div className="flex-1 overflow-y-auto px-6 py-4">
|
|
750
|
+
{loadingTurmas ? (
|
|
751
|
+
<div className="flex items-center justify-center py-12">
|
|
752
|
+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
753
|
+
</div>
|
|
754
|
+
) : turmasData.length === 0 ? (
|
|
755
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
756
|
+
<BookOpen className="mb-3 h-10 w-10 text-muted-foreground/40" />
|
|
757
|
+
<p className="text-sm font-medium text-muted-foreground">
|
|
758
|
+
Nenhuma turma encontrada
|
|
759
|
+
</p>
|
|
760
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
761
|
+
Este instrutor ainda não está vinculado a nenhuma turma.
|
|
762
|
+
</p>
|
|
763
|
+
</div>
|
|
764
|
+
) : (
|
|
765
|
+
<div className="space-y-2">
|
|
766
|
+
{turmasData.map((turma) => (
|
|
767
|
+
<div key={turma.id} className="rounded-lg border p-3">
|
|
768
|
+
<div className="flex items-start justify-between gap-2">
|
|
769
|
+
<div className="min-w-0 flex-1">
|
|
770
|
+
<p className="truncate text-sm font-medium">
|
|
771
|
+
{turma.name}
|
|
772
|
+
</p>
|
|
773
|
+
{turma.courseName && (
|
|
774
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
775
|
+
{turma.courseName}
|
|
776
|
+
</p>
|
|
777
|
+
)}
|
|
778
|
+
</div>
|
|
779
|
+
<Badge
|
|
780
|
+
variant="outline"
|
|
781
|
+
className="shrink-0 text-xs"
|
|
782
|
+
>
|
|
783
|
+
{STATUS_LABEL[turma.status] ?? turma.status}
|
|
784
|
+
</Badge>
|
|
785
|
+
</div>
|
|
786
|
+
<div className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
|
787
|
+
{turma.startDate && (
|
|
788
|
+
<span>
|
|
789
|
+
Início:{' '}
|
|
790
|
+
{new Date(turma.startDate).toLocaleDateString(
|
|
791
|
+
'pt-BR'
|
|
792
|
+
)}
|
|
793
|
+
</span>
|
|
794
|
+
)}
|
|
795
|
+
{turma.endDate && (
|
|
796
|
+
<span>
|
|
797
|
+
Fim:{' '}
|
|
798
|
+
{new Date(turma.endDate).toLocaleDateString(
|
|
799
|
+
'pt-BR'
|
|
800
|
+
)}
|
|
801
|
+
</span>
|
|
802
|
+
)}
|
|
803
|
+
{turma.slots > 0 && (
|
|
804
|
+
<span>{turma.slots} aluno(s)</span>
|
|
805
|
+
)}
|
|
806
|
+
</div>
|
|
807
|
+
</div>
|
|
808
|
+
))}
|
|
809
|
+
</div>
|
|
810
|
+
)}
|
|
811
|
+
</div>
|
|
812
|
+
</TabsContent>
|
|
813
|
+
|
|
814
|
+
{/* ── Avaliações ── */}
|
|
815
|
+
<TabsContent
|
|
816
|
+
value="avaliacoes"
|
|
817
|
+
className="flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
|
|
818
|
+
>
|
|
819
|
+
{/* TODO: implementar endpoint de avaliações de instrutor */}
|
|
820
|
+
<div className="flex flex-1 flex-col items-center justify-center px-6 py-12 text-center">
|
|
821
|
+
<Star className="mb-3 h-10 w-10 text-muted-foreground/40" />
|
|
822
|
+
<p className="text-sm font-medium text-muted-foreground">
|
|
823
|
+
Avaliações — funcionalidade em breve
|
|
324
824
|
</p>
|
|
325
|
-
<p className="text-xs text-muted-foreground">
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
: 'Nenhum usuário vinculado a este cadastro. Vincule um usuário para habilitar.'}
|
|
825
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
826
|
+
Em breve você poderá visualizar as avaliações recebidas
|
|
827
|
+
pelos alunos.
|
|
329
828
|
</p>
|
|
330
829
|
</div>
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
830
|
+
</TabsContent>
|
|
831
|
+
</Tabs>
|
|
832
|
+
) : (
|
|
833
|
+
/* ── Create mode: plain form ── */
|
|
834
|
+
<form
|
|
835
|
+
onSubmit={handleSubmit(onSubmit)}
|
|
836
|
+
className="flex min-h-0 flex-1 flex-col"
|
|
837
|
+
>
|
|
838
|
+
<div className="flex-1 overflow-y-auto px-6 py-4">
|
|
839
|
+
<div className="flex flex-col gap-5">
|
|
840
|
+
{/* Person picker */}
|
|
841
|
+
<Field>
|
|
842
|
+
<FieldLabel>
|
|
843
|
+
Pessoa <span className="text-destructive">*</span>
|
|
844
|
+
</FieldLabel>
|
|
845
|
+
<div className="flex items-center gap-2">
|
|
846
|
+
<div className="min-w-0 flex-1">
|
|
847
|
+
<Controller
|
|
848
|
+
control={control}
|
|
849
|
+
name="personId"
|
|
850
|
+
render={({ field }) => (
|
|
851
|
+
<PersonPicker
|
|
852
|
+
label=""
|
|
853
|
+
entityLabel="pessoa"
|
|
854
|
+
value={field.value ?? null}
|
|
855
|
+
initialSelectedLabel={selectedPersonName}
|
|
856
|
+
onChange={(personId, personName) => {
|
|
857
|
+
field.onChange(personId ?? undefined);
|
|
858
|
+
setSelectedPersonName(personName);
|
|
859
|
+
}}
|
|
860
|
+
selectPlaceholder="Buscar ou selecionar pessoa..."
|
|
861
|
+
personTypeFilter="individual"
|
|
862
|
+
createType="individual"
|
|
863
|
+
lockCreateType
|
|
864
|
+
/>
|
|
865
|
+
)}
|
|
866
|
+
/>
|
|
867
|
+
</div>
|
|
868
|
+
<Button
|
|
869
|
+
type="button"
|
|
870
|
+
variant="outline"
|
|
871
|
+
size="icon"
|
|
872
|
+
className="shrink-0"
|
|
873
|
+
disabled={!watchedPersonId}
|
|
874
|
+
onClick={handleOpenPersonEdit}
|
|
875
|
+
aria-label="Editar cadastro da pessoa"
|
|
876
|
+
title="Editar cadastro da pessoa"
|
|
877
|
+
>
|
|
878
|
+
<Pencil className="h-4 w-4" />
|
|
879
|
+
</Button>
|
|
880
|
+
</div>
|
|
881
|
+
{errors.personId && (
|
|
882
|
+
<FieldError>{errors.personId.message}</FieldError>
|
|
883
|
+
)}
|
|
884
|
+
</Field>
|
|
885
|
+
|
|
886
|
+
{/* Status */}
|
|
887
|
+
<Field>
|
|
888
|
+
<FieldLabel>Status</FieldLabel>
|
|
889
|
+
<Controller
|
|
890
|
+
control={control}
|
|
891
|
+
name="status"
|
|
892
|
+
render={({ field }) => (
|
|
893
|
+
<Select
|
|
894
|
+
value={field.value}
|
|
895
|
+
onValueChange={field.onChange}
|
|
896
|
+
>
|
|
897
|
+
<SelectTrigger className="w-full">
|
|
898
|
+
<SelectValue placeholder="Selecione o status" />
|
|
899
|
+
</SelectTrigger>
|
|
900
|
+
<SelectContent>
|
|
901
|
+
<SelectItem value="active">Ativo</SelectItem>
|
|
902
|
+
<SelectItem value="inactive">Inativo</SelectItem>
|
|
903
|
+
</SelectContent>
|
|
904
|
+
</Select>
|
|
905
|
+
)}
|
|
906
|
+
/>
|
|
907
|
+
</Field>
|
|
908
|
+
|
|
909
|
+
{/* Valor/hora */}
|
|
910
|
+
<Field>
|
|
911
|
+
<FieldLabel>Valor/hora (R$)</FieldLabel>
|
|
912
|
+
<Controller
|
|
913
|
+
control={control}
|
|
914
|
+
name="hourlyRate"
|
|
915
|
+
render={({ field }) => (
|
|
916
|
+
<InputMoney
|
|
917
|
+
placeholder="Ex: 150,00"
|
|
918
|
+
value={field.value ?? ''}
|
|
919
|
+
onValueChange={(value) => field.onChange(value)}
|
|
920
|
+
/>
|
|
921
|
+
)}
|
|
922
|
+
/>
|
|
923
|
+
</Field>
|
|
924
|
+
|
|
925
|
+
{/* can_teach_courses */}
|
|
926
|
+
<div className="flex items-center justify-between rounded-lg border p-4">
|
|
927
|
+
<div className="space-y-0.5">
|
|
928
|
+
<p className="text-sm font-medium">Pode ensinar cursos</p>
|
|
929
|
+
<p className="text-xs text-muted-foreground">
|
|
930
|
+
Permite que o instrutor seja vinculado a aulas de curso.
|
|
931
|
+
</p>
|
|
932
|
+
</div>
|
|
933
|
+
<Controller
|
|
934
|
+
control={control}
|
|
935
|
+
name="can_teach_courses"
|
|
936
|
+
render={({ field }) => (
|
|
937
|
+
<Switch
|
|
938
|
+
checked={field.value}
|
|
939
|
+
onCheckedChange={field.onChange}
|
|
940
|
+
/>
|
|
941
|
+
)}
|
|
942
|
+
/>
|
|
943
|
+
</div>
|
|
944
|
+
|
|
945
|
+
{/* Qualificações */}
|
|
946
|
+
<Field>
|
|
947
|
+
<FieldLabel>
|
|
948
|
+
Qualificações <span className="text-destructive">*</span>
|
|
949
|
+
</FieldLabel>
|
|
950
|
+
<div className="space-y-3">
|
|
951
|
+
{QUALIFICATION_OPTIONS.map((option) => (
|
|
952
|
+
<Controller
|
|
953
|
+
key={option.slug}
|
|
954
|
+
control={control}
|
|
955
|
+
name="qualificationSlugs"
|
|
956
|
+
render={({ field }) => {
|
|
957
|
+
const checked = field.value.includes(option.slug);
|
|
958
|
+
return (
|
|
959
|
+
<div className="flex items-start justify-between rounded-lg border p-3">
|
|
960
|
+
<div className="space-y-0.5">
|
|
961
|
+
<p className="text-sm font-medium">
|
|
962
|
+
{option.label}
|
|
963
|
+
</p>
|
|
964
|
+
<p className="text-xs text-muted-foreground">
|
|
965
|
+
{option.description}
|
|
966
|
+
</p>
|
|
967
|
+
</div>
|
|
968
|
+
<Switch
|
|
969
|
+
checked={checked}
|
|
970
|
+
onCheckedChange={(next) => {
|
|
971
|
+
if (next) {
|
|
972
|
+
field.onChange([
|
|
973
|
+
...field.value,
|
|
974
|
+
option.slug,
|
|
975
|
+
]);
|
|
976
|
+
} else {
|
|
977
|
+
field.onChange(
|
|
978
|
+
field.value.filter(
|
|
979
|
+
(s) => s !== option.slug
|
|
980
|
+
)
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
}}
|
|
984
|
+
/>
|
|
985
|
+
</div>
|
|
986
|
+
);
|
|
987
|
+
}}
|
|
988
|
+
/>
|
|
989
|
+
))}
|
|
990
|
+
</div>
|
|
991
|
+
{errors.qualificationSlugs && (
|
|
992
|
+
<FieldError>
|
|
993
|
+
{errors.qualificationSlugs.message}
|
|
994
|
+
</FieldError>
|
|
995
|
+
)}
|
|
996
|
+
</Field>
|
|
997
|
+
</div>
|
|
398
998
|
</div>
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
disabled={saving}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
</Button>
|
|
417
|
-
</SheetFooter>
|
|
418
|
-
</form>
|
|
999
|
+
|
|
1000
|
+
<SheetFooter className="shrink-0 border-t px-6 py-4">
|
|
1001
|
+
<Button
|
|
1002
|
+
type="button"
|
|
1003
|
+
variant="outline"
|
|
1004
|
+
onClick={() => handleSheetClose(false)}
|
|
1005
|
+
disabled={saving}
|
|
1006
|
+
>
|
|
1007
|
+
Cancelar
|
|
1008
|
+
</Button>
|
|
1009
|
+
<Button type="submit" disabled={saving}>
|
|
1010
|
+
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
1011
|
+
Criar instrutor
|
|
1012
|
+
</Button>
|
|
1013
|
+
</SheetFooter>
|
|
1014
|
+
</form>
|
|
1015
|
+
)}
|
|
419
1016
|
</SheetContent>
|
|
420
1017
|
</Sheet>
|
|
421
1018
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
1019
|
+
{editPersonOpen && personToEdit && (
|
|
1020
|
+
<PersonFormSheet
|
|
1021
|
+
open={editPersonOpen}
|
|
1022
|
+
person={personToEdit}
|
|
1023
|
+
contactTypes={contactTypes}
|
|
1024
|
+
documentTypes={documentTypes}
|
|
1025
|
+
onOpenChange={(v) => {
|
|
1026
|
+
setEditPersonOpen(v);
|
|
1027
|
+
if (!v) setPersonToEdit(null);
|
|
1028
|
+
}}
|
|
1029
|
+
onSuccess={(updated) => {
|
|
1030
|
+
if (updated) setSelectedPersonName(updated.name);
|
|
1031
|
+
void queryClient.invalidateQueries({
|
|
1032
|
+
queryKey: ['person-picker-autocomplete'],
|
|
1033
|
+
});
|
|
1034
|
+
void queryClient.invalidateQueries({
|
|
1035
|
+
queryKey: ['lms-instructors'],
|
|
1036
|
+
});
|
|
1037
|
+
setEditPersonOpen(false);
|
|
1038
|
+
setPersonToEdit(null);
|
|
1039
|
+
}}
|
|
1040
|
+
/>
|
|
1041
|
+
)}
|
|
436
1042
|
</>
|
|
437
1043
|
);
|
|
438
1044
|
}
|