@hed-hog/lms 0.0.331 → 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/course-form-sheet.tsx.ejs +3 -3
- 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 +6 -1
- 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 +40 -13
- 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]/structure/_components/course-scheduled-classes-tab.tsx.ejs +406 -0
- 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/mock-data.ts.ejs +185 -0
- package/hedhog/frontend/app/courses/page.tsx.ejs +6 -1
- 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/exams/[id]/questions/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/exams/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +51 -104
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +625 -366
- package/hedhog/frontend/app/instructors/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/paths/page.tsx.ejs +9 -4
- package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/training/page.tsx.ejs +9 -4
- package/hedhog/frontend/messages/en.json +101 -10
- package/hedhog/frontend/messages/pt.json +101 -10
- package/hedhog/table/enterprise_student_license_event.yaml +30 -0
- package/hedhog/table/instructor_skill.yaml +0 -11
- package/package.json +7 -7
- 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,
|
|
@@ -31,35 +33,52 @@ import { Switch } from '@/components/ui/switch';
|
|
|
31
33
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
32
34
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
33
35
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
34
|
-
import { useTranslations } from 'next-intl';
|
|
35
36
|
import { useQueryClient } from '@tanstack/react-query';
|
|
36
|
-
import {
|
|
37
|
-
|
|
37
|
+
import {
|
|
38
|
+
ArrowUpRight,
|
|
39
|
+
BookOpen,
|
|
40
|
+
ChevronLeft,
|
|
41
|
+
ChevronRight,
|
|
42
|
+
Loader2,
|
|
43
|
+
Pencil,
|
|
44
|
+
Search,
|
|
45
|
+
Star,
|
|
46
|
+
X,
|
|
47
|
+
} from 'lucide-react';
|
|
48
|
+
import { useTranslations } from 'next-intl';
|
|
49
|
+
import { useRouter } from 'next/navigation';
|
|
38
50
|
import { useEffect, useState } from 'react';
|
|
39
51
|
import { Controller, useForm } from 'react-hook-form';
|
|
40
52
|
import { toast } from 'sonner';
|
|
41
53
|
import { z } from 'zod';
|
|
54
|
+
import { ClassFormSheet } from '../../_components/class-form-sheet';
|
|
55
|
+
import { CourseAvatar } from '../../_components/course-avatar';
|
|
42
56
|
import type { InstructorRow, InstructorSkill } from './instructor-types';
|
|
43
57
|
|
|
44
58
|
type ClassGroupItem = {
|
|
45
59
|
id: number;
|
|
46
60
|
name: string;
|
|
47
61
|
code: string;
|
|
62
|
+
courseLogoUrl: string | null;
|
|
63
|
+
courseLogoFileId?: number | null;
|
|
48
64
|
courseName: string | null;
|
|
49
65
|
startDate: string | null;
|
|
50
66
|
endDate: string | null;
|
|
51
67
|
status: string;
|
|
52
68
|
slots: number;
|
|
69
|
+
totalSlots: number | null;
|
|
53
70
|
};
|
|
54
71
|
|
|
55
72
|
function toKebabCase(str: string) {
|
|
56
|
-
return
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
return (
|
|
74
|
+
str
|
|
75
|
+
.toLowerCase()
|
|
76
|
+
.normalize('NFD')
|
|
77
|
+
// biome-ignore lint: unicode diacritic strip
|
|
78
|
+
.replace(/[̀-ͯ]/g, '')
|
|
79
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
80
|
+
.replace(/^-|-$/g, '')
|
|
81
|
+
);
|
|
63
82
|
}
|
|
64
83
|
|
|
65
84
|
function getSkillDisplayName(s: InstructorSkill): string {
|
|
@@ -91,17 +110,23 @@ export function InstructorFormSheet({
|
|
|
91
110
|
}: InstructorFormSheetProps) {
|
|
92
111
|
const { request, currentLocaleCode } = useApp();
|
|
93
112
|
const t = useTranslations('lms.InstructorsPage');
|
|
113
|
+
const router = useRouter();
|
|
94
114
|
const queryClient = useQueryClient();
|
|
95
115
|
const [saving, setSaving] = useState(false);
|
|
96
116
|
const [selectedPersonName, setSelectedPersonName] = useState('');
|
|
97
|
-
const [trainingAccess, setTrainingAccess] = useState<boolean | null>(null);
|
|
98
|
-
const [togglingAccess, setTogglingAccess] = useState(false);
|
|
99
117
|
const [editPersonOpen, setEditPersonOpen] = useState(false);
|
|
100
118
|
const [personToEdit, setPersonToEdit] = useState<Person | null>(null);
|
|
101
119
|
const [activeTab, setActiveTab] = useState('detalhes');
|
|
102
120
|
const [tabSkillSlugs, setTabSkillSlugs] = useState<string[]>([]);
|
|
103
121
|
const [savingSkills, setSavingSkills] = useState(false);
|
|
104
122
|
const [pickerResetKey, setPickerResetKey] = useState(0);
|
|
123
|
+
const [turmasPage, setTurmasPage] = useState(1);
|
|
124
|
+
const [turmasSearchInput, setTurmasSearchInput] = useState('');
|
|
125
|
+
const [debouncedTurmasSearch, setDebouncedTurmasSearch] = useState('');
|
|
126
|
+
const [turmasStatusFilter, setTurmasStatusFilter] = useState('all');
|
|
127
|
+
const [classSheetOpen, setClassSheetOpen] = useState(false);
|
|
128
|
+
const [editingClassId, setEditingClassId] = useState<number | null>(null);
|
|
129
|
+
const TURMAS_PAGE_SIZE = 6;
|
|
105
130
|
const isEditing = typeof instructorId === 'number' && instructorId > 0;
|
|
106
131
|
const qualificationOptions = [
|
|
107
132
|
{
|
|
@@ -118,6 +143,8 @@ export function InstructorFormSheet({
|
|
|
118
143
|
const statusLabel: Record<string, string> = {
|
|
119
144
|
active: t('form.fields.active'),
|
|
120
145
|
inactive: t('form.fields.inactive'),
|
|
146
|
+
open: t('form.classStatuses.upcoming'),
|
|
147
|
+
ongoing: t('form.classStatuses.inProgress'),
|
|
121
148
|
in_progress: t('form.classStatuses.inProgress'),
|
|
122
149
|
upcoming: t('form.classStatuses.upcoming'),
|
|
123
150
|
completed: t('form.classStatuses.completed'),
|
|
@@ -141,6 +168,7 @@ export function InstructorFormSheet({
|
|
|
141
168
|
control,
|
|
142
169
|
handleSubmit,
|
|
143
170
|
reset,
|
|
171
|
+
setValue,
|
|
144
172
|
watch,
|
|
145
173
|
formState: { errors },
|
|
146
174
|
} = useForm<InstructorFormValues>({
|
|
@@ -153,7 +181,38 @@ export function InstructorFormSheet({
|
|
|
153
181
|
},
|
|
154
182
|
});
|
|
155
183
|
|
|
156
|
-
const
|
|
184
|
+
const applyCanTeachCourses = (
|
|
185
|
+
currentSlugs: string[],
|
|
186
|
+
canTeachCourses: boolean
|
|
187
|
+
) => {
|
|
188
|
+
const uniqueSlugs = [...new Set(currentSlugs)];
|
|
189
|
+
|
|
190
|
+
if (canTeachCourses) {
|
|
191
|
+
return uniqueSlugs.includes('course-lessons')
|
|
192
|
+
? uniqueSlugs
|
|
193
|
+
: [...uniqueSlugs, 'course-lessons'];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const filtered = uniqueSlugs.filter((slug) => slug !== 'course-lessons');
|
|
197
|
+
return filtered.length > 0 ? filtered : ['class-sessions'];
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const applyQualificationToggle = (
|
|
201
|
+
currentSlugs: string[],
|
|
202
|
+
slug: string,
|
|
203
|
+
enabled: boolean
|
|
204
|
+
) => {
|
|
205
|
+
const uniqueSlugs = [...new Set(currentSlugs)];
|
|
206
|
+
const nextSlugs = enabled
|
|
207
|
+
? uniqueSlugs.includes(slug)
|
|
208
|
+
? uniqueSlugs
|
|
209
|
+
: [...uniqueSlugs, slug]
|
|
210
|
+
: uniqueSlugs.filter((value) => value !== slug);
|
|
211
|
+
|
|
212
|
+
return nextSlugs.length > 0
|
|
213
|
+
? nextSlugs
|
|
214
|
+
: [slug === 'course-lessons' ? 'class-sessions' : 'course-lessons'];
|
|
215
|
+
};
|
|
157
216
|
|
|
158
217
|
const { data: contactTypes = [] } = useQuery<ContactTypeOption[]>({
|
|
159
218
|
queryKey: ['contact-person-contact-types', currentLocaleCode],
|
|
@@ -181,16 +240,20 @@ export function InstructorFormSheet({
|
|
|
181
240
|
placeholderData: (previous) => previous ?? [],
|
|
182
241
|
});
|
|
183
242
|
|
|
184
|
-
const
|
|
185
|
-
if (!watchedPersonId) return;
|
|
243
|
+
const handleEditSelection = async (personId: number) => {
|
|
186
244
|
const response = await request<Person>({
|
|
187
|
-
url: `/person/${
|
|
245
|
+
url: `/person/${personId}`,
|
|
188
246
|
method: 'GET',
|
|
189
247
|
});
|
|
190
248
|
setPersonToEdit(response.data);
|
|
191
249
|
setEditPersonOpen(true);
|
|
192
250
|
};
|
|
193
251
|
|
|
252
|
+
const handleCreatePerson = () => {
|
|
253
|
+
setPersonToEdit(null);
|
|
254
|
+
setEditPersonOpen(true);
|
|
255
|
+
};
|
|
256
|
+
|
|
194
257
|
const { data: existingInstructor } = useQuery<InstructorRow | null>({
|
|
195
258
|
queryKey: ['lms-instructor-detail', instructorId],
|
|
196
259
|
queryFn: async () => {
|
|
@@ -216,26 +279,73 @@ export function InstructorFormSheet({
|
|
|
216
279
|
},
|
|
217
280
|
});
|
|
218
281
|
|
|
219
|
-
// Turmas —
|
|
220
|
-
const {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
282
|
+
// Turmas — paginated list filtered by instructorId
|
|
283
|
+
const {
|
|
284
|
+
data: turmasResult,
|
|
285
|
+
isLoading: loadingTurmas,
|
|
286
|
+
refetch: refetchTurmas,
|
|
287
|
+
} = useQuery<{
|
|
288
|
+
data: ClassGroupItem[];
|
|
289
|
+
total: number;
|
|
290
|
+
lastPage: number;
|
|
291
|
+
}>({
|
|
292
|
+
queryKey: [
|
|
293
|
+
'lms-instructor-turmas',
|
|
294
|
+
instructorId,
|
|
295
|
+
turmasPage,
|
|
296
|
+
debouncedTurmasSearch,
|
|
297
|
+
turmasStatusFilter,
|
|
298
|
+
],
|
|
224
299
|
queryFn: async () => {
|
|
225
|
-
|
|
226
|
-
{ data
|
|
227
|
-
|
|
228
|
-
|
|
300
|
+
if (!instructorId) {
|
|
301
|
+
return { data: [], total: 0, lastPage: 1 };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const params = new URLSearchParams({
|
|
305
|
+
page: String(turmasPage),
|
|
306
|
+
pageSize: String(TURMAS_PAGE_SIZE),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (debouncedTurmasSearch) {
|
|
310
|
+
params.set('search', debouncedTurmasSearch);
|
|
311
|
+
}
|
|
312
|
+
if (turmasStatusFilter !== 'all') {
|
|
313
|
+
params.set('status', turmasStatusFilter);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const response = await request<{
|
|
317
|
+
data: ClassGroupItem[];
|
|
318
|
+
total: number;
|
|
319
|
+
lastPage: number;
|
|
320
|
+
}>({
|
|
321
|
+
url: `/lms/instructors/${instructorId}/class-groups?${params.toString()}`,
|
|
229
322
|
method: 'GET',
|
|
230
323
|
});
|
|
231
|
-
|
|
232
|
-
if (Array.isArray(raw)) return raw;
|
|
233
|
-
if (raw && 'data' in raw && Array.isArray(raw.data)) return raw.data;
|
|
234
|
-
return [];
|
|
324
|
+
return response.data;
|
|
235
325
|
},
|
|
236
326
|
enabled: isEditing && open && activeTab === 'turmas',
|
|
237
|
-
placeholderData:
|
|
327
|
+
placeholderData: (prev) => prev,
|
|
238
328
|
});
|
|
329
|
+
const turmasData = turmasResult?.data ?? [];
|
|
330
|
+
const turmasLastPage = turmasResult?.lastPage ?? 1;
|
|
331
|
+
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
const timeout = setTimeout(() => {
|
|
334
|
+
setDebouncedTurmasSearch(turmasSearchInput.trim());
|
|
335
|
+
setTurmasPage(1);
|
|
336
|
+
}, 300);
|
|
337
|
+
return () => clearTimeout(timeout);
|
|
338
|
+
}, [turmasSearchInput]);
|
|
339
|
+
|
|
340
|
+
useEffect(() => {
|
|
341
|
+
setTurmasPage(1);
|
|
342
|
+
}, [turmasStatusFilter]);
|
|
343
|
+
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
if (turmasPage > turmasLastPage) {
|
|
346
|
+
setTurmasPage(turmasLastPage);
|
|
347
|
+
}
|
|
348
|
+
}, [turmasLastPage, turmasPage]);
|
|
239
349
|
|
|
240
350
|
useEffect(() => {
|
|
241
351
|
if (isEditing && existingInstructor) {
|
|
@@ -246,12 +356,12 @@ export function InstructorFormSheet({
|
|
|
246
356
|
? existingInstructor.qualificationSlugs
|
|
247
357
|
: ['course-lessons'],
|
|
248
358
|
status: existingInstructor.status,
|
|
249
|
-
can_teach_courses:
|
|
359
|
+
can_teach_courses:
|
|
360
|
+
existingInstructor.qualificationSlugs.includes('course-lessons'),
|
|
250
361
|
hourlyRate: existingInstructor.hourlyRate ?? null,
|
|
251
362
|
skillSlugs: (existingInstructor.skills ?? []).map((s) => s.slug),
|
|
252
363
|
});
|
|
253
364
|
setSelectedPersonName(existingInstructor.name);
|
|
254
|
-
setTrainingAccess(existingInstructor.hasTrainingAccess ?? false);
|
|
255
365
|
setTabSkillSlugs((existingInstructor.skills ?? []).map((s) => s.slug));
|
|
256
366
|
} else if (!isEditing && open) {
|
|
257
367
|
reset({
|
|
@@ -263,16 +373,27 @@ export function InstructorFormSheet({
|
|
|
263
373
|
skillSlugs: [],
|
|
264
374
|
});
|
|
265
375
|
setSelectedPersonName('');
|
|
266
|
-
setTrainingAccess(null);
|
|
267
376
|
setTabSkillSlugs([]);
|
|
268
377
|
}
|
|
269
378
|
}, [isEditing, existingInstructor, open, reset]);
|
|
270
379
|
|
|
271
|
-
// Reset to first tab when sheet closes
|
|
380
|
+
// Reset to first tab and turmas page when sheet closes
|
|
272
381
|
useEffect(() => {
|
|
273
|
-
if (!open)
|
|
382
|
+
if (!open) {
|
|
383
|
+
setActiveTab('detalhes');
|
|
384
|
+
setTurmasPage(1);
|
|
385
|
+
setTurmasSearchInput('');
|
|
386
|
+
setDebouncedTurmasSearch('');
|
|
387
|
+
setTurmasStatusFilter('all');
|
|
388
|
+
setClassSheetOpen(false);
|
|
389
|
+
setEditingClassId(null);
|
|
390
|
+
}
|
|
274
391
|
}, [open]);
|
|
275
392
|
|
|
393
|
+
useEffect(() => {
|
|
394
|
+
setTurmasPage(1);
|
|
395
|
+
}, [activeTab]);
|
|
396
|
+
|
|
276
397
|
const handleSheetClose = (nextOpen: boolean) => {
|
|
277
398
|
if (!nextOpen) {
|
|
278
399
|
reset();
|
|
@@ -301,6 +422,22 @@ export function InstructorFormSheet({
|
|
|
301
422
|
}
|
|
302
423
|
};
|
|
303
424
|
|
|
425
|
+
const handleEditClassQuick = (classId: number) => {
|
|
426
|
+
setEditingClassId(classId);
|
|
427
|
+
setClassSheetOpen(true);
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const handleOpenClassPage = (classId: number) => {
|
|
431
|
+
router.push(`/lms/classes/${classId}`);
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const handleClassSaved = async () => {
|
|
435
|
+
await Promise.all([
|
|
436
|
+
refetchTurmas(),
|
|
437
|
+
queryClient.invalidateQueries({ queryKey: ['lms-classes'] }),
|
|
438
|
+
]);
|
|
439
|
+
};
|
|
440
|
+
|
|
304
441
|
const onSubmit = async (values: InstructorFormValues) => {
|
|
305
442
|
try {
|
|
306
443
|
setSaving(true);
|
|
@@ -342,9 +479,7 @@ export function InstructorFormSheet({
|
|
|
342
479
|
handleSheetClose(false);
|
|
343
480
|
} catch {
|
|
344
481
|
toast.error(
|
|
345
|
-
isEditing
|
|
346
|
-
? t('form.toasts.updateError')
|
|
347
|
-
: t('form.toasts.createError')
|
|
482
|
+
isEditing ? t('form.toasts.updateError') : t('form.toasts.createError')
|
|
348
483
|
);
|
|
349
484
|
} finally {
|
|
350
485
|
setSaving(false);
|
|
@@ -360,49 +495,55 @@ export function InstructorFormSheet({
|
|
|
360
495
|
>
|
|
361
496
|
{/* ── Instructor header (edit mode) ── */}
|
|
362
497
|
{isEditing && existingInstructor ? (
|
|
363
|
-
|
|
364
|
-
<
|
|
365
|
-
<
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
</AvatarFallback>
|
|
376
|
-
</Avatar>
|
|
377
|
-
<div className="min-w-0 flex-1">
|
|
378
|
-
<h2 className="truncate text-lg font-semibold">
|
|
379
|
-
{existingInstructor.name}
|
|
380
|
-
</h2>
|
|
381
|
-
<div className="mt-1 flex flex-wrap items-center gap-2">
|
|
382
|
-
<Badge
|
|
383
|
-
variant={
|
|
384
|
-
existingInstructor.status === 'active'
|
|
385
|
-
? 'default'
|
|
386
|
-
: 'secondary'
|
|
498
|
+
<>
|
|
499
|
+
<SheetHeader className="sr-only">
|
|
500
|
+
<SheetTitle>{t('form.title')}</SheetTitle>
|
|
501
|
+
<SheetDescription>{t('form.description')}</SheetDescription>
|
|
502
|
+
</SheetHeader>
|
|
503
|
+
<div className="flex shrink-0 items-center gap-4 border-b px-6 py-4">
|
|
504
|
+
<Avatar className="h-14 w-14">
|
|
505
|
+
<AvatarImage
|
|
506
|
+
src={
|
|
507
|
+
existingInstructor.avatarId
|
|
508
|
+
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${existingInstructor.avatarId}`
|
|
509
|
+
: undefined
|
|
387
510
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
</
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
<
|
|
400
|
-
{
|
|
401
|
-
|
|
402
|
-
|
|
511
|
+
alt={existingInstructor.name}
|
|
512
|
+
/>
|
|
513
|
+
<AvatarFallback>
|
|
514
|
+
{existingInstructor.name.slice(0, 2).toUpperCase()}
|
|
515
|
+
</AvatarFallback>
|
|
516
|
+
</Avatar>
|
|
517
|
+
<div className="min-w-0 flex-1">
|
|
518
|
+
<h2 className="truncate text-lg font-semibold">
|
|
519
|
+
{existingInstructor.name}
|
|
520
|
+
</h2>
|
|
521
|
+
<div className="mt-1 flex flex-wrap items-center gap-2">
|
|
522
|
+
<Badge
|
|
523
|
+
variant={
|
|
524
|
+
existingInstructor.status === 'active'
|
|
525
|
+
? 'default'
|
|
526
|
+
: 'secondary'
|
|
527
|
+
}
|
|
528
|
+
>
|
|
529
|
+
{existingInstructor.status === 'active'
|
|
530
|
+
? t('form.fields.active')
|
|
531
|
+
: t('form.fields.inactive')}
|
|
532
|
+
</Badge>
|
|
533
|
+
{existingInstructor.email && (
|
|
534
|
+
<span className="truncate text-xs text-muted-foreground">
|
|
535
|
+
{existingInstructor.email}
|
|
536
|
+
</span>
|
|
537
|
+
)}
|
|
538
|
+
{existingInstructor.phone && (
|
|
539
|
+
<span className="text-xs text-muted-foreground">
|
|
540
|
+
{existingInstructor.phone}
|
|
541
|
+
</span>
|
|
542
|
+
)}
|
|
543
|
+
</div>
|
|
403
544
|
</div>
|
|
404
545
|
</div>
|
|
405
|
-
|
|
546
|
+
</>
|
|
406
547
|
) : (
|
|
407
548
|
<SheetHeader className="shrink-0 border-b px-6 py-4">
|
|
408
549
|
<SheetTitle>{t('form.title')}</SheetTitle>
|
|
@@ -421,8 +562,12 @@ export function InstructorFormSheet({
|
|
|
421
562
|
<TabsTrigger value="detalhes">
|
|
422
563
|
{t('form.tabs.details')}
|
|
423
564
|
</TabsTrigger>
|
|
424
|
-
<TabsTrigger value="skills">
|
|
425
|
-
|
|
565
|
+
<TabsTrigger value="skills">
|
|
566
|
+
{t('form.tabs.skills')}
|
|
567
|
+
</TabsTrigger>
|
|
568
|
+
<TabsTrigger value="turmas">
|
|
569
|
+
{t('form.tabs.classes')}
|
|
570
|
+
</TabsTrigger>
|
|
426
571
|
<TabsTrigger value="avaliacoes">
|
|
427
572
|
{t('form.tabs.evaluations')}
|
|
428
573
|
</TabsTrigger>
|
|
@@ -445,94 +590,83 @@ export function InstructorFormSheet({
|
|
|
445
590
|
{t('form.fields.person')}{' '}
|
|
446
591
|
<span className="text-destructive">*</span>
|
|
447
592
|
</FieldLabel>
|
|
448
|
-
<div className="flex items-center gap-2">
|
|
449
|
-
<div className="min-w-0 flex-1">
|
|
450
|
-
<Controller
|
|
451
|
-
control={control}
|
|
452
|
-
name="personId"
|
|
453
|
-
render={({ field }) => (
|
|
454
|
-
<PersonPicker
|
|
455
|
-
label=""
|
|
456
|
-
entityLabel={t('form.fields.person')}
|
|
457
|
-
value={field.value ?? null}
|
|
458
|
-
initialSelectedLabel={selectedPersonName}
|
|
459
|
-
onChange={(personId, personName) => {
|
|
460
|
-
field.onChange(personId ?? undefined);
|
|
461
|
-
setSelectedPersonName(personName);
|
|
462
|
-
}}
|
|
463
|
-
selectPlaceholder={t(
|
|
464
|
-
'form.fields.searchPerson'
|
|
465
|
-
)}
|
|
466
|
-
personTypeFilter="individual"
|
|
467
|
-
createType="individual"
|
|
468
|
-
lockCreateType
|
|
469
|
-
/>
|
|
470
|
-
)}
|
|
471
|
-
/>
|
|
472
|
-
</div>
|
|
473
|
-
<Button
|
|
474
|
-
type="button"
|
|
475
|
-
variant="outline"
|
|
476
|
-
size="icon"
|
|
477
|
-
className="shrink-0"
|
|
478
|
-
disabled={!watchedPersonId}
|
|
479
|
-
onClick={handleOpenPersonEdit}
|
|
480
|
-
aria-label={t('form.fields.editPerson')}
|
|
481
|
-
title={t('form.fields.editPerson')}
|
|
482
|
-
>
|
|
483
|
-
<Pencil className="h-4 w-4" />
|
|
484
|
-
</Button>
|
|
485
|
-
</div>
|
|
486
|
-
{errors.personId && (
|
|
487
|
-
<FieldError>{errors.personId.message}</FieldError>
|
|
488
|
-
)}
|
|
489
|
-
</Field>
|
|
490
|
-
|
|
491
|
-
{/* Status */}
|
|
492
|
-
<Field>
|
|
493
|
-
<FieldLabel>{t('form.fields.status')}</FieldLabel>
|
|
494
|
-
<Controller
|
|
495
|
-
control={control}
|
|
496
|
-
name="status"
|
|
497
|
-
render={({ field }) => (
|
|
498
|
-
<Select
|
|
499
|
-
value={field.value}
|
|
500
|
-
onValueChange={field.onChange}
|
|
501
|
-
>
|
|
502
|
-
<SelectTrigger className="w-full">
|
|
503
|
-
<SelectValue
|
|
504
|
-
placeholder={t('filters.statusPlaceholder')}
|
|
505
|
-
/>
|
|
506
|
-
</SelectTrigger>
|
|
507
|
-
<SelectContent>
|
|
508
|
-
<SelectItem value="active">
|
|
509
|
-
{t('form.fields.active')}
|
|
510
|
-
</SelectItem>
|
|
511
|
-
<SelectItem value="inactive">
|
|
512
|
-
{t('form.fields.inactive')}
|
|
513
|
-
</SelectItem>
|
|
514
|
-
</SelectContent>
|
|
515
|
-
</Select>
|
|
516
|
-
)}
|
|
517
|
-
/>
|
|
518
|
-
</Field>
|
|
519
|
-
|
|
520
|
-
{/* Valor/hora */}
|
|
521
|
-
<Field>
|
|
522
|
-
<FieldLabel>{t('form.fields.hourlyRate')}</FieldLabel>
|
|
523
593
|
<Controller
|
|
524
594
|
control={control}
|
|
525
|
-
name="
|
|
595
|
+
name="personId"
|
|
526
596
|
render={({ field }) => (
|
|
527
|
-
<
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
597
|
+
<PersonPicker
|
|
598
|
+
label=""
|
|
599
|
+
entityLabel={t('form.fields.person')}
|
|
600
|
+
value={field.value ?? null}
|
|
601
|
+
initialSelectedLabel={selectedPersonName}
|
|
602
|
+
onChange={(personId, personName) => {
|
|
603
|
+
field.onChange(personId ?? undefined);
|
|
604
|
+
setSelectedPersonName(personName);
|
|
605
|
+
}}
|
|
606
|
+
selectPlaceholder={t('form.fields.searchPerson')}
|
|
607
|
+
personTypeFilter="individual"
|
|
608
|
+
createType="individual"
|
|
609
|
+
lockCreateType
|
|
610
|
+
onCreateNew={handleCreatePerson}
|
|
611
|
+
showEditButton
|
|
612
|
+
onEditSelection={handleEditSelection}
|
|
613
|
+
editAriaLabel={t('form.fields.editPerson')}
|
|
531
614
|
/>
|
|
532
615
|
)}
|
|
533
616
|
/>
|
|
617
|
+
{errors.personId && (
|
|
618
|
+
<FieldError>{errors.personId.message}</FieldError>
|
|
619
|
+
)}
|
|
534
620
|
</Field>
|
|
535
621
|
|
|
622
|
+
{/* Status + Valor/hora */}
|
|
623
|
+
<div className="grid grid-cols-2 gap-4">
|
|
624
|
+
<Field>
|
|
625
|
+
<FieldLabel>{t('form.fields.status')}</FieldLabel>
|
|
626
|
+
<Controller
|
|
627
|
+
control={control}
|
|
628
|
+
name="status"
|
|
629
|
+
render={({ field }) => (
|
|
630
|
+
<Select
|
|
631
|
+
value={field.value}
|
|
632
|
+
onValueChange={field.onChange}
|
|
633
|
+
>
|
|
634
|
+
<SelectTrigger className="w-full">
|
|
635
|
+
<SelectValue
|
|
636
|
+
placeholder={t('filters.statusPlaceholder')}
|
|
637
|
+
/>
|
|
638
|
+
</SelectTrigger>
|
|
639
|
+
<SelectContent>
|
|
640
|
+
<SelectItem value="active">
|
|
641
|
+
{t('form.fields.active')}
|
|
642
|
+
</SelectItem>
|
|
643
|
+
<SelectItem value="inactive">
|
|
644
|
+
{t('form.fields.inactive')}
|
|
645
|
+
</SelectItem>
|
|
646
|
+
</SelectContent>
|
|
647
|
+
</Select>
|
|
648
|
+
)}
|
|
649
|
+
/>
|
|
650
|
+
</Field>
|
|
651
|
+
|
|
652
|
+
<Field>
|
|
653
|
+
<FieldLabel>{t('form.fields.hourlyRate')}</FieldLabel>
|
|
654
|
+
<Controller
|
|
655
|
+
control={control}
|
|
656
|
+
name="hourlyRate"
|
|
657
|
+
render={({ field }) => (
|
|
658
|
+
<InputMoney
|
|
659
|
+
placeholder={t(
|
|
660
|
+
'form.fields.hourlyRatePlaceholder'
|
|
661
|
+
)}
|
|
662
|
+
value={field.value ?? ''}
|
|
663
|
+
onValueChange={(value) => field.onChange(value)}
|
|
664
|
+
/>
|
|
665
|
+
)}
|
|
666
|
+
/>
|
|
667
|
+
</Field>
|
|
668
|
+
</div>
|
|
669
|
+
|
|
536
670
|
{/* can_teach_courses */}
|
|
537
671
|
<div className="flex items-center justify-between rounded-lg border p-4">
|
|
538
672
|
<div className="space-y-0.5">
|
|
@@ -549,52 +683,23 @@ export function InstructorFormSheet({
|
|
|
549
683
|
render={({ field }) => (
|
|
550
684
|
<Switch
|
|
551
685
|
checked={field.value}
|
|
552
|
-
onCheckedChange={
|
|
686
|
+
onCheckedChange={(next) => {
|
|
687
|
+
field.onChange(next);
|
|
688
|
+
const nextSlugs = applyCanTeachCourses(
|
|
689
|
+
watch('qualificationSlugs') ?? [],
|
|
690
|
+
next
|
|
691
|
+
);
|
|
692
|
+
setValue('qualificationSlugs', nextSlugs, {
|
|
693
|
+
shouldDirty: true,
|
|
694
|
+
shouldTouch: true,
|
|
695
|
+
shouldValidate: true,
|
|
696
|
+
});
|
|
697
|
+
}}
|
|
553
698
|
/>
|
|
554
699
|
)}
|
|
555
700
|
/>
|
|
556
701
|
</div>
|
|
557
702
|
|
|
558
|
-
{/* Acesso ao Training */}
|
|
559
|
-
<div className="flex items-center justify-between rounded-lg border p-4">
|
|
560
|
-
<div className="space-y-0.5">
|
|
561
|
-
<p className="text-sm font-medium">
|
|
562
|
-
{t('form.fields.trainingAccess')}
|
|
563
|
-
</p>
|
|
564
|
-
<p className="text-xs text-muted-foreground">
|
|
565
|
-
{existingInstructor?.userId
|
|
566
|
-
? t('form.fields.trainingAccessHint')
|
|
567
|
-
: t('form.fields.noUserLinked')}
|
|
568
|
-
</p>
|
|
569
|
-
</div>
|
|
570
|
-
<Switch
|
|
571
|
-
checked={trainingAccess ?? false}
|
|
572
|
-
disabled={
|
|
573
|
-
!existingInstructor?.userId || togglingAccess
|
|
574
|
-
}
|
|
575
|
-
onCheckedChange={async (next) => {
|
|
576
|
-
setTogglingAccess(true);
|
|
577
|
-
try {
|
|
578
|
-
await request({
|
|
579
|
-
url: `/lms/instructors/${instructorId}/training-access`,
|
|
580
|
-
method: 'PATCH',
|
|
581
|
-
data: { enabled: next },
|
|
582
|
-
});
|
|
583
|
-
setTrainingAccess(next);
|
|
584
|
-
toast.success(
|
|
585
|
-
next
|
|
586
|
-
? t('form.toasts.accessEnabled')
|
|
587
|
-
: t('form.toasts.accessDisabled')
|
|
588
|
-
);
|
|
589
|
-
} catch {
|
|
590
|
-
toast.error(t('form.toasts.accessError'));
|
|
591
|
-
} finally {
|
|
592
|
-
setTogglingAccess(false);
|
|
593
|
-
}
|
|
594
|
-
}}
|
|
595
|
-
/>
|
|
596
|
-
</div>
|
|
597
|
-
|
|
598
703
|
{/* Qualificações */}
|
|
599
704
|
<Field>
|
|
600
705
|
<FieldLabel>
|
|
@@ -624,17 +729,19 @@ export function InstructorFormSheet({
|
|
|
624
729
|
<Switch
|
|
625
730
|
checked={checked}
|
|
626
731
|
onCheckedChange={(next) => {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
732
|
+
const nextSlugs =
|
|
733
|
+
applyQualificationToggle(
|
|
734
|
+
field.value,
|
|
630
735
|
option.slug,
|
|
631
|
-
|
|
632
|
-
} else {
|
|
633
|
-
field.onChange(
|
|
634
|
-
field.value.filter(
|
|
635
|
-
(s) => s !== option.slug
|
|
636
|
-
)
|
|
736
|
+
next
|
|
637
737
|
);
|
|
738
|
+
field.onChange(nextSlugs);
|
|
739
|
+
if (option.slug === 'course-lessons') {
|
|
740
|
+
setValue('can_teach_courses', next, {
|
|
741
|
+
shouldDirty: true,
|
|
742
|
+
shouldTouch: true,
|
|
743
|
+
shouldValidate: false,
|
|
744
|
+
});
|
|
638
745
|
}
|
|
639
746
|
}}
|
|
640
747
|
/>
|
|
@@ -753,12 +860,16 @@ export function InstructorFormSheet({
|
|
|
753
860
|
name: 'namePt',
|
|
754
861
|
label: t('form.skills.createFields.name'),
|
|
755
862
|
required: true,
|
|
756
|
-
placeholder: t(
|
|
863
|
+
placeholder: t(
|
|
864
|
+
'form.skills.createFields.namePlaceholder'
|
|
865
|
+
),
|
|
757
866
|
},
|
|
758
867
|
{
|
|
759
868
|
name: 'slug',
|
|
760
869
|
label: t('form.skills.createFields.slug'),
|
|
761
|
-
placeholder: t(
|
|
870
|
+
placeholder: t(
|
|
871
|
+
'form.skills.createFields.slugPlaceholder'
|
|
872
|
+
),
|
|
762
873
|
},
|
|
763
874
|
]}
|
|
764
875
|
mapSearchToCreateValues={(search) => ({
|
|
@@ -788,7 +899,9 @@ export function InstructorFormSheet({
|
|
|
788
899
|
name: namePt,
|
|
789
900
|
} as any;
|
|
790
901
|
} catch {
|
|
791
|
-
toast.error(
|
|
902
|
+
toast.error(
|
|
903
|
+
t('InstructorSkillsPage.messages.createError')
|
|
904
|
+
);
|
|
792
905
|
return null;
|
|
793
906
|
}
|
|
794
907
|
}}
|
|
@@ -825,70 +938,194 @@ export function InstructorFormSheet({
|
|
|
825
938
|
value="turmas"
|
|
826
939
|
className="flex min-h-0 flex-1 flex-col overflow-hidden data-[state=inactive]:hidden"
|
|
827
940
|
>
|
|
828
|
-
<div className="flex
|
|
829
|
-
|
|
830
|
-
<div className="
|
|
831
|
-
<
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
{t('form.classes.
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
{t('form.classes.emptyDescription')}
|
|
841
|
-
</p>
|
|
941
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
942
|
+
<div className="grid shrink-0 grid-cols-1 gap-2 border-b px-6 py-3 sm:grid-cols-[minmax(0,1fr)_200px]">
|
|
943
|
+
<div className="relative">
|
|
944
|
+
<Search className="pointer-events-none absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
945
|
+
<Input
|
|
946
|
+
value={turmasSearchInput}
|
|
947
|
+
onChange={(event) =>
|
|
948
|
+
setTurmasSearchInput(event.target.value)
|
|
949
|
+
}
|
|
950
|
+
placeholder={t('form.classes.searchPlaceholder')}
|
|
951
|
+
className="pl-8"
|
|
952
|
+
/>
|
|
842
953
|
</div>
|
|
843
|
-
|
|
844
|
-
<
|
|
845
|
-
{
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
954
|
+
|
|
955
|
+
<Select
|
|
956
|
+
value={turmasStatusFilter}
|
|
957
|
+
onValueChange={setTurmasStatusFilter}
|
|
958
|
+
>
|
|
959
|
+
<SelectTrigger>
|
|
960
|
+
<SelectValue
|
|
961
|
+
placeholder={t(
|
|
962
|
+
'form.classes.statusFilterPlaceholder'
|
|
963
|
+
)}
|
|
964
|
+
/>
|
|
965
|
+
</SelectTrigger>
|
|
966
|
+
<SelectContent>
|
|
967
|
+
<SelectItem value="all">
|
|
968
|
+
{t('form.classes.allStatuses')}
|
|
969
|
+
</SelectItem>
|
|
970
|
+
<SelectItem value="open">
|
|
971
|
+
{t('form.classStatuses.upcoming')}
|
|
972
|
+
</SelectItem>
|
|
973
|
+
<SelectItem value="ongoing">
|
|
974
|
+
{t('form.classStatuses.inProgress')}
|
|
975
|
+
</SelectItem>
|
|
976
|
+
<SelectItem value="completed">
|
|
977
|
+
{t('form.classStatuses.completed')}
|
|
978
|
+
</SelectItem>
|
|
979
|
+
<SelectItem value="cancelled">
|
|
980
|
+
{t('form.classStatuses.cancelled')}
|
|
981
|
+
</SelectItem>
|
|
982
|
+
</SelectContent>
|
|
983
|
+
</Select>
|
|
984
|
+
</div>
|
|
985
|
+
|
|
986
|
+
<div className="flex-1 overflow-y-auto px-6 py-4">
|
|
987
|
+
{loadingTurmas ? (
|
|
988
|
+
<div className="flex items-center justify-center py-12">
|
|
989
|
+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
990
|
+
</div>
|
|
991
|
+
) : turmasData.length === 0 ? (
|
|
992
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
993
|
+
<BookOpen className="mb-3 h-10 w-10 text-muted-foreground/40" />
|
|
994
|
+
<p className="text-sm font-medium text-muted-foreground">
|
|
995
|
+
{t('form.classes.emptyTitle')}
|
|
996
|
+
</p>
|
|
997
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
998
|
+
{t('form.classes.emptyDescription')}
|
|
999
|
+
</p>
|
|
1000
|
+
</div>
|
|
1001
|
+
) : (
|
|
1002
|
+
<div className="space-y-2">
|
|
1003
|
+
{turmasData.map((turma) => (
|
|
1004
|
+
<div key={turma.id} className="rounded-lg border p-3">
|
|
1005
|
+
<div className="flex items-start gap-3">
|
|
1006
|
+
<div className="shrink-0">
|
|
1007
|
+
<CourseAvatar
|
|
1008
|
+
fileId={turma.courseLogoFileId}
|
|
1009
|
+
title={turma.courseName ?? turma.name}
|
|
1010
|
+
className="h-10 w-10 rounded"
|
|
1011
|
+
iconSize="h-5 w-5"
|
|
1012
|
+
/>
|
|
1013
|
+
</div>
|
|
1014
|
+
<div className="min-w-0 flex-1">
|
|
1015
|
+
<div className="flex items-start justify-between gap-2">
|
|
1016
|
+
<div className="min-w-0">
|
|
1017
|
+
<p className="truncate text-sm font-medium">
|
|
1018
|
+
{turma.name}
|
|
1019
|
+
</p>
|
|
1020
|
+
{turma.courseName && (
|
|
1021
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
1022
|
+
{turma.courseName}
|
|
1023
|
+
</p>
|
|
1024
|
+
)}
|
|
1025
|
+
</div>
|
|
1026
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
1027
|
+
<Button
|
|
1028
|
+
type="button"
|
|
1029
|
+
variant="ghost"
|
|
1030
|
+
size="icon"
|
|
1031
|
+
className="h-7 w-7"
|
|
1032
|
+
onClick={() =>
|
|
1033
|
+
handleEditClassQuick(turma.id)
|
|
1034
|
+
}
|
|
1035
|
+
aria-label={t(
|
|
1036
|
+
'form.classes.actions.editAriaLabel'
|
|
1037
|
+
)}
|
|
1038
|
+
title={t(
|
|
1039
|
+
'form.classes.actions.editAriaLabel'
|
|
1040
|
+
)}
|
|
1041
|
+
>
|
|
1042
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
1043
|
+
</Button>
|
|
1044
|
+
<Button
|
|
1045
|
+
type="button"
|
|
1046
|
+
variant="ghost"
|
|
1047
|
+
size="icon"
|
|
1048
|
+
className="h-7 w-7"
|
|
1049
|
+
onClick={() =>
|
|
1050
|
+
handleOpenClassPage(turma.id)
|
|
1051
|
+
}
|
|
1052
|
+
aria-label={t(
|
|
1053
|
+
'form.classes.actions.openAriaLabel'
|
|
1054
|
+
)}
|
|
1055
|
+
title={t(
|
|
1056
|
+
'form.classes.actions.openAriaLabel'
|
|
1057
|
+
)}
|
|
1058
|
+
>
|
|
1059
|
+
<ArrowUpRight className="h-3.5 w-3.5" />
|
|
1060
|
+
</Button>
|
|
1061
|
+
<Badge
|
|
1062
|
+
variant="outline"
|
|
1063
|
+
className="shrink-0 text-xs"
|
|
1064
|
+
>
|
|
1065
|
+
{statusLabel[turma.status] ??
|
|
1066
|
+
turma.status}
|
|
1067
|
+
</Badge>
|
|
1068
|
+
</div>
|
|
1069
|
+
</div>
|
|
1070
|
+
<div className="mt-1.5 flex flex-wrap gap-x-4 gap-y-0.5 text-xs text-muted-foreground">
|
|
1071
|
+
{turma.startDate && (
|
|
1072
|
+
<span>
|
|
1073
|
+
{t('form.classes.start')}{' '}
|
|
1074
|
+
{new Date(
|
|
1075
|
+
turma.startDate
|
|
1076
|
+
).toLocaleDateString('pt-BR')}
|
|
1077
|
+
</span>
|
|
1078
|
+
)}
|
|
1079
|
+
{turma.endDate && (
|
|
1080
|
+
<span>
|
|
1081
|
+
{t('form.classes.end')}{' '}
|
|
1082
|
+
{new Date(
|
|
1083
|
+
turma.endDate
|
|
1084
|
+
).toLocaleDateString('pt-BR')}
|
|
1085
|
+
</span>
|
|
1086
|
+
)}
|
|
1087
|
+
<span>
|
|
1088
|
+
{turma.totalSlots != null
|
|
1089
|
+
? t('form.classes.studentsOfCapacity', {
|
|
1090
|
+
count: turma.slots,
|
|
1091
|
+
total: turma.totalSlots,
|
|
1092
|
+
})
|
|
1093
|
+
: t('form.classes.students', {
|
|
1094
|
+
count: turma.slots,
|
|
1095
|
+
})}
|
|
1096
|
+
</span>
|
|
1097
|
+
</div>
|
|
1098
|
+
</div>
|
|
857
1099
|
</div>
|
|
858
|
-
<Badge
|
|
859
|
-
variant="outline"
|
|
860
|
-
className="shrink-0 text-xs"
|
|
861
|
-
>
|
|
862
|
-
{statusLabel[turma.status] ?? turma.status}
|
|
863
|
-
</Badge>
|
|
864
|
-
</div>
|
|
865
|
-
<div className="mt-2 flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
|
866
|
-
{turma.startDate && (
|
|
867
|
-
<span>
|
|
868
|
-
{t('form.classes.start')}{' '}
|
|
869
|
-
{new Date(turma.startDate).toLocaleDateString(
|
|
870
|
-
'pt-BR'
|
|
871
|
-
)}
|
|
872
|
-
</span>
|
|
873
|
-
)}
|
|
874
|
-
{turma.endDate && (
|
|
875
|
-
<span>
|
|
876
|
-
{t('form.classes.end')}{' '}
|
|
877
|
-
{new Date(turma.endDate).toLocaleDateString(
|
|
878
|
-
'pt-BR'
|
|
879
|
-
)}
|
|
880
|
-
</span>
|
|
881
|
-
)}
|
|
882
|
-
{turma.slots > 0 && (
|
|
883
|
-
<span>
|
|
884
|
-
{t('form.classes.students', {
|
|
885
|
-
count: turma.slots,
|
|
886
|
-
})}
|
|
887
|
-
</span>
|
|
888
|
-
)}
|
|
889
1100
|
</div>
|
|
890
|
-
|
|
891
|
-
|
|
1101
|
+
))}
|
|
1102
|
+
</div>
|
|
1103
|
+
)}
|
|
1104
|
+
</div>
|
|
1105
|
+
|
|
1106
|
+
{turmasLastPage > 1 && (
|
|
1107
|
+
<div className="flex shrink-0 items-center justify-center gap-2 border-t px-6 py-3">
|
|
1108
|
+
<Button
|
|
1109
|
+
type="button"
|
|
1110
|
+
variant="outline"
|
|
1111
|
+
size="icon"
|
|
1112
|
+
disabled={turmasPage <= 1}
|
|
1113
|
+
onClick={() => setTurmasPage((p) => p - 1)}
|
|
1114
|
+
>
|
|
1115
|
+
<ChevronLeft className="h-4 w-4" />
|
|
1116
|
+
</Button>
|
|
1117
|
+
<span className="text-xs text-muted-foreground">
|
|
1118
|
+
{turmasPage} / {turmasLastPage}
|
|
1119
|
+
</span>
|
|
1120
|
+
<Button
|
|
1121
|
+
type="button"
|
|
1122
|
+
variant="outline"
|
|
1123
|
+
size="icon"
|
|
1124
|
+
disabled={turmasPage >= turmasLastPage}
|
|
1125
|
+
onClick={() => setTurmasPage((p) => p + 1)}
|
|
1126
|
+
>
|
|
1127
|
+
<ChevronRight className="h-4 w-4" />
|
|
1128
|
+
</Button>
|
|
892
1129
|
</div>
|
|
893
1130
|
)}
|
|
894
1131
|
</div>
|
|
@@ -925,92 +1162,81 @@ export function InstructorFormSheet({
|
|
|
925
1162
|
{t('form.fields.person')}{' '}
|
|
926
1163
|
<span className="text-destructive">*</span>
|
|
927
1164
|
</FieldLabel>
|
|
928
|
-
<div className="flex items-center gap-2">
|
|
929
|
-
<div className="min-w-0 flex-1">
|
|
930
|
-
<Controller
|
|
931
|
-
control={control}
|
|
932
|
-
name="personId"
|
|
933
|
-
render={({ field }) => (
|
|
934
|
-
<PersonPicker
|
|
935
|
-
label=""
|
|
936
|
-
entityLabel={t('form.fields.person')}
|
|
937
|
-
value={field.value ?? null}
|
|
938
|
-
initialSelectedLabel={selectedPersonName}
|
|
939
|
-
onChange={(personId, personName) => {
|
|
940
|
-
field.onChange(personId ?? undefined);
|
|
941
|
-
setSelectedPersonName(personName);
|
|
942
|
-
}}
|
|
943
|
-
selectPlaceholder={t('form.fields.searchPerson')}
|
|
944
|
-
personTypeFilter="individual"
|
|
945
|
-
createType="individual"
|
|
946
|
-
lockCreateType
|
|
947
|
-
/>
|
|
948
|
-
)}
|
|
949
|
-
/>
|
|
950
|
-
</div>
|
|
951
|
-
<Button
|
|
952
|
-
type="button"
|
|
953
|
-
variant="outline"
|
|
954
|
-
size="icon"
|
|
955
|
-
className="shrink-0"
|
|
956
|
-
disabled={!watchedPersonId}
|
|
957
|
-
onClick={handleOpenPersonEdit}
|
|
958
|
-
aria-label={t('form.fields.editPerson')}
|
|
959
|
-
title={t('form.fields.editPerson')}
|
|
960
|
-
>
|
|
961
|
-
<Pencil className="h-4 w-4" />
|
|
962
|
-
</Button>
|
|
963
|
-
</div>
|
|
964
|
-
{errors.personId && (
|
|
965
|
-
<FieldError>{errors.personId.message}</FieldError>
|
|
966
|
-
)}
|
|
967
|
-
</Field>
|
|
968
|
-
|
|
969
|
-
{/* Status */}
|
|
970
|
-
<Field>
|
|
971
|
-
<FieldLabel>{t('form.fields.status')}</FieldLabel>
|
|
972
|
-
<Controller
|
|
973
|
-
control={control}
|
|
974
|
-
name="status"
|
|
975
|
-
render={({ field }) => (
|
|
976
|
-
<Select
|
|
977
|
-
value={field.value}
|
|
978
|
-
onValueChange={field.onChange}
|
|
979
|
-
>
|
|
980
|
-
<SelectTrigger className="w-full">
|
|
981
|
-
<SelectValue
|
|
982
|
-
placeholder={t('filters.statusPlaceholder')}
|
|
983
|
-
/>
|
|
984
|
-
</SelectTrigger>
|
|
985
|
-
<SelectContent>
|
|
986
|
-
<SelectItem value="active">
|
|
987
|
-
{t('form.fields.active')}
|
|
988
|
-
</SelectItem>
|
|
989
|
-
<SelectItem value="inactive">
|
|
990
|
-
{t('form.fields.inactive')}
|
|
991
|
-
</SelectItem>
|
|
992
|
-
</SelectContent>
|
|
993
|
-
</Select>
|
|
994
|
-
)}
|
|
995
|
-
/>
|
|
996
|
-
</Field>
|
|
997
|
-
|
|
998
|
-
{/* Valor/hora */}
|
|
999
|
-
<Field>
|
|
1000
|
-
<FieldLabel>{t('form.fields.hourlyRate')}</FieldLabel>
|
|
1001
1165
|
<Controller
|
|
1002
1166
|
control={control}
|
|
1003
|
-
name="
|
|
1167
|
+
name="personId"
|
|
1004
1168
|
render={({ field }) => (
|
|
1005
|
-
<
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1169
|
+
<PersonPicker
|
|
1170
|
+
label=""
|
|
1171
|
+
entityLabel={t('form.fields.person')}
|
|
1172
|
+
value={field.value ?? null}
|
|
1173
|
+
initialSelectedLabel={selectedPersonName}
|
|
1174
|
+
onChange={(personId, personName) => {
|
|
1175
|
+
field.onChange(personId ?? undefined);
|
|
1176
|
+
setSelectedPersonName(personName);
|
|
1177
|
+
}}
|
|
1178
|
+
selectPlaceholder={t('form.fields.searchPerson')}
|
|
1179
|
+
personTypeFilter="individual"
|
|
1180
|
+
createType="individual"
|
|
1181
|
+
lockCreateType
|
|
1182
|
+
onCreateNew={handleCreatePerson}
|
|
1183
|
+
showEditButton
|
|
1184
|
+
onEditSelection={handleEditSelection}
|
|
1185
|
+
editAriaLabel={t('form.fields.editPerson')}
|
|
1009
1186
|
/>
|
|
1010
1187
|
)}
|
|
1011
1188
|
/>
|
|
1189
|
+
{errors.personId && (
|
|
1190
|
+
<FieldError>{errors.personId.message}</FieldError>
|
|
1191
|
+
)}
|
|
1012
1192
|
</Field>
|
|
1013
1193
|
|
|
1194
|
+
{/* Status + Valor/hora */}
|
|
1195
|
+
<div className="grid grid-cols-2 gap-4">
|
|
1196
|
+
<Field>
|
|
1197
|
+
<FieldLabel>{t('form.fields.status')}</FieldLabel>
|
|
1198
|
+
<Controller
|
|
1199
|
+
control={control}
|
|
1200
|
+
name="status"
|
|
1201
|
+
render={({ field }) => (
|
|
1202
|
+
<Select
|
|
1203
|
+
value={field.value}
|
|
1204
|
+
onValueChange={field.onChange}
|
|
1205
|
+
>
|
|
1206
|
+
<SelectTrigger className="w-full">
|
|
1207
|
+
<SelectValue
|
|
1208
|
+
placeholder={t('filters.statusPlaceholder')}
|
|
1209
|
+
/>
|
|
1210
|
+
</SelectTrigger>
|
|
1211
|
+
<SelectContent>
|
|
1212
|
+
<SelectItem value="active">
|
|
1213
|
+
{t('form.fields.active')}
|
|
1214
|
+
</SelectItem>
|
|
1215
|
+
<SelectItem value="inactive">
|
|
1216
|
+
{t('form.fields.inactive')}
|
|
1217
|
+
</SelectItem>
|
|
1218
|
+
</SelectContent>
|
|
1219
|
+
</Select>
|
|
1220
|
+
)}
|
|
1221
|
+
/>
|
|
1222
|
+
</Field>
|
|
1223
|
+
|
|
1224
|
+
<Field>
|
|
1225
|
+
<FieldLabel>{t('form.fields.hourlyRate')}</FieldLabel>
|
|
1226
|
+
<Controller
|
|
1227
|
+
control={control}
|
|
1228
|
+
name="hourlyRate"
|
|
1229
|
+
render={({ field }) => (
|
|
1230
|
+
<InputMoney
|
|
1231
|
+
placeholder={t('form.fields.hourlyRatePlaceholder')}
|
|
1232
|
+
value={field.value ?? ''}
|
|
1233
|
+
onValueChange={(value) => field.onChange(value)}
|
|
1234
|
+
/>
|
|
1235
|
+
)}
|
|
1236
|
+
/>
|
|
1237
|
+
</Field>
|
|
1238
|
+
</div>
|
|
1239
|
+
|
|
1014
1240
|
{/* can_teach_courses */}
|
|
1015
1241
|
<div className="flex items-center justify-between rounded-lg border p-4">
|
|
1016
1242
|
<div className="space-y-0.5">
|
|
@@ -1027,7 +1253,18 @@ export function InstructorFormSheet({
|
|
|
1027
1253
|
render={({ field }) => (
|
|
1028
1254
|
<Switch
|
|
1029
1255
|
checked={field.value}
|
|
1030
|
-
onCheckedChange={
|
|
1256
|
+
onCheckedChange={(next) => {
|
|
1257
|
+
field.onChange(next);
|
|
1258
|
+
const nextSlugs = applyCanTeachCourses(
|
|
1259
|
+
watch('qualificationSlugs') ?? [],
|
|
1260
|
+
next
|
|
1261
|
+
);
|
|
1262
|
+
setValue('qualificationSlugs', nextSlugs, {
|
|
1263
|
+
shouldDirty: true,
|
|
1264
|
+
shouldTouch: true,
|
|
1265
|
+
shouldValidate: true,
|
|
1266
|
+
});
|
|
1267
|
+
}}
|
|
1031
1268
|
/>
|
|
1032
1269
|
)}
|
|
1033
1270
|
/>
|
|
@@ -1060,17 +1297,18 @@ export function InstructorFormSheet({
|
|
|
1060
1297
|
<Switch
|
|
1061
1298
|
checked={checked}
|
|
1062
1299
|
onCheckedChange={(next) => {
|
|
1063
|
-
|
|
1064
|
-
field.
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1300
|
+
const nextSlugs = applyQualificationToggle(
|
|
1301
|
+
field.value,
|
|
1302
|
+
option.slug,
|
|
1303
|
+
next
|
|
1304
|
+
);
|
|
1305
|
+
field.onChange(nextSlugs);
|
|
1306
|
+
if (option.slug === 'course-lessons') {
|
|
1307
|
+
setValue('can_teach_courses', next, {
|
|
1308
|
+
shouldDirty: true,
|
|
1309
|
+
shouldTouch: true,
|
|
1310
|
+
shouldValidate: false,
|
|
1311
|
+
});
|
|
1074
1312
|
}
|
|
1075
1313
|
}}
|
|
1076
1314
|
/>
|
|
@@ -1108,18 +1346,39 @@ export function InstructorFormSheet({
|
|
|
1108
1346
|
</SheetContent>
|
|
1109
1347
|
</Sheet>
|
|
1110
1348
|
|
|
1111
|
-
|
|
1349
|
+
<ClassFormSheet
|
|
1350
|
+
open={classSheetOpen}
|
|
1351
|
+
onOpenChange={(nextOpen) => {
|
|
1352
|
+
setClassSheetOpen(nextOpen);
|
|
1353
|
+
if (!nextOpen) {
|
|
1354
|
+
setEditingClassId(null);
|
|
1355
|
+
}
|
|
1356
|
+
}}
|
|
1357
|
+
classId={editingClassId != null ? String(editingClassId) : undefined}
|
|
1358
|
+
enableInstructorSheet={false}
|
|
1359
|
+
onSuccess={handleClassSaved}
|
|
1360
|
+
/>
|
|
1361
|
+
|
|
1362
|
+
{editPersonOpen && (
|
|
1112
1363
|
<PersonFormSheet
|
|
1113
1364
|
open={editPersonOpen}
|
|
1114
1365
|
person={personToEdit}
|
|
1115
1366
|
contactTypes={contactTypes}
|
|
1116
1367
|
documentTypes={documentTypes}
|
|
1368
|
+
allowedTypes={['individual']}
|
|
1117
1369
|
onOpenChange={(v) => {
|
|
1118
1370
|
setEditPersonOpen(v);
|
|
1119
1371
|
if (!v) setPersonToEdit(null);
|
|
1120
1372
|
}}
|
|
1121
1373
|
onSuccess={(updated) => {
|
|
1122
|
-
if (updated)
|
|
1374
|
+
if (updated) {
|
|
1375
|
+
setSelectedPersonName(updated.name);
|
|
1376
|
+
setValue('personId', updated.id, {
|
|
1377
|
+
shouldDirty: true,
|
|
1378
|
+
shouldTouch: true,
|
|
1379
|
+
shouldValidate: true,
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1123
1382
|
void queryClient.invalidateQueries({
|
|
1124
1383
|
queryKey: ['person-picker-autocomplete'],
|
|
1125
1384
|
});
|