@hed-hog/lms 0.0.328 → 0.0.330
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/instructor/instructor.service.d.ts.map +1 -1
- package/dist/instructor/instructor.service.js +22 -16
- package/dist/instructor/instructor.service.js.map +1 -1
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +18 -8
- package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +7 -5
- package/hedhog/frontend/app/_components/create-lms-person-sheet.tsx.ejs +5 -9
- package/hedhog/frontend/app/_components/create-lms-student-person-sheet.tsx.ejs +5 -9
- package/hedhog/frontend/app/certificates/models/LeftPanel.tsx.ejs +15 -14
- package/hedhog/frontend/app/certificates/models/RightPanel.tsx.ejs +66 -29
- package/hedhog/frontend/app/certificates/models/TemplateEditorPage.tsx.ejs +4 -2
- package/hedhog/frontend/app/certificates/models/TopBar.tsx.ejs +44 -34
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +10 -10
- package/hedhog/frontend/app/classes/page.tsx.ejs +23 -15
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +5 -3
- package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +5 -3
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +9 -7
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-skeleton.tsx.ejs +3 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +4 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +24 -23
- package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +21 -19
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +7 -5
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +18 -16
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +13 -11
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +5 -3
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +14 -9
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +42 -25
- package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +3 -1
- package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +10 -8
- package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +22 -20
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +3 -3
- package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +21 -19
- package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +34 -36
- package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +3 -1
- package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +7 -5
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +106 -54
- package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +79 -59
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +92 -26
- package/hedhog/frontend/app/instructors/page.tsx.ejs +4 -2
- package/hedhog/frontend/messages/en.json +619 -13
- package/hedhog/frontend/messages/pt.json +619 -13
- package/package.json +7 -7
- package/src/instructor/instructor.service.ts +22 -19
- package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +0 -591
- package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +0 -109
- package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +0 -60
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +0 -134
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +0 -113
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +0 -314
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +0 -174
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +0 -185
- package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +0 -277
- package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +0 -207
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/lms",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.330",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -10,14 +10,14 @@
|
|
|
10
10
|
"@nestjs/jwt": "^11",
|
|
11
11
|
"@nestjs/mapped-types": "*",
|
|
12
12
|
"@hed-hog/api": "0.0.8",
|
|
13
|
+
"@hed-hog/category": "0.0.330",
|
|
14
|
+
"@hed-hog/api-types": "0.0.1",
|
|
15
|
+
"@hed-hog/finance": "0.0.330",
|
|
16
|
+
"@hed-hog/core": "0.0.330",
|
|
13
17
|
"@hed-hog/api-locale": "0.0.14",
|
|
14
|
-
"@hed-hog/api-pagination": "0.0.7",
|
|
15
|
-
"@hed-hog/contact": "0.0.328",
|
|
16
18
|
"@hed-hog/api-prisma": "0.0.6",
|
|
17
|
-
"@hed-hog/
|
|
18
|
-
"@hed-hog/
|
|
19
|
-
"@hed-hog/finance": "0.0.328",
|
|
20
|
-
"@hed-hog/core": "0.0.328"
|
|
19
|
+
"@hed-hog/contact": "0.0.330",
|
|
20
|
+
"@hed-hog/api-pagination": "0.0.7"
|
|
21
21
|
},
|
|
22
22
|
"exports": {
|
|
23
23
|
".": {
|
|
@@ -160,7 +160,12 @@ export class InstructorService {
|
|
|
160
160
|
hourlyRateMap.set(Number(r.id), r.hourly_rate !== null ? Number(r.hourly_rate) : null);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
const seenSkillSlugs = new Map<number, Set<string>>();
|
|
163
164
|
for (const a of assignmentRows) {
|
|
165
|
+
const seen = seenSkillSlugs.get(a.instructor_id) ?? new Set<string>();
|
|
166
|
+
if (seen.has(a.instructor_skill.slug)) continue;
|
|
167
|
+
seen.add(a.instructor_skill.slug);
|
|
168
|
+
seenSkillSlugs.set(a.instructor_id, seen);
|
|
164
169
|
const existing = skillsMap.get(a.instructor_id) ?? [];
|
|
165
170
|
existing.push({
|
|
166
171
|
id: a.instructor_skill.id,
|
|
@@ -480,11 +485,18 @@ export class InstructorService {
|
|
|
480
485
|
? Number(rateRows[0].hourly_rate)
|
|
481
486
|
: null;
|
|
482
487
|
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
+
const seenSlugs = new Set<string>();
|
|
489
|
+
const skills = skillAssignments
|
|
490
|
+
.filter((a) => {
|
|
491
|
+
if (seenSlugs.has(a.instructor_skill.slug)) return false;
|
|
492
|
+
seenSlugs.add(a.instructor_skill.slug);
|
|
493
|
+
return true;
|
|
494
|
+
})
|
|
495
|
+
.map((a) => ({
|
|
496
|
+
id: a.instructor_skill.id,
|
|
497
|
+
slug: a.instructor_skill.slug,
|
|
498
|
+
name: a.instructor_skill.instructor_skill_locale?.[0]?.name ?? a.instructor_skill.slug,
|
|
499
|
+
}));
|
|
488
500
|
|
|
489
501
|
return {
|
|
490
502
|
id: instructor.id,
|
|
@@ -890,12 +902,11 @@ export class InstructorService {
|
|
|
890
902
|
) {
|
|
891
903
|
const normalizedSlugs = [...new Set((slugs ?? []).map((s) => s?.trim()).filter(Boolean))];
|
|
892
904
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}
|
|
905
|
+
await (db as any).instructor_skill_assignment.deleteMany({
|
|
906
|
+
where: { instructor_id: instructorId },
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
if (normalizedSlugs.length === 0) return;
|
|
899
910
|
|
|
900
911
|
const skillRows = await (db as any).instructor_skill.findMany({
|
|
901
912
|
where: { slug: { in: normalizedSlugs } },
|
|
@@ -904,20 +915,12 @@ export class InstructorService {
|
|
|
904
915
|
|
|
905
916
|
const skillIds = (skillRows as any[]).map((r) => r.id);
|
|
906
917
|
|
|
907
|
-
await (db as any).instructor_skill_assignment.deleteMany({
|
|
908
|
-
where: {
|
|
909
|
-
instructor_id: instructorId,
|
|
910
|
-
skill_id: { notIn: skillIds },
|
|
911
|
-
},
|
|
912
|
-
});
|
|
913
|
-
|
|
914
918
|
if (skillIds.length > 0) {
|
|
915
919
|
await (db as any).instructor_skill_assignment.createMany({
|
|
916
920
|
data: skillIds.map((skillId: number) => ({
|
|
917
921
|
instructor_id: instructorId,
|
|
918
922
|
skill_id: skillId,
|
|
919
923
|
})),
|
|
920
|
-
skipDuplicates: true,
|
|
921
924
|
});
|
|
922
925
|
}
|
|
923
926
|
}
|
|
@@ -1,591 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
4
|
-
import { Button } from '@/components/ui/button';
|
|
5
|
-
import {
|
|
6
|
-
Field,
|
|
7
|
-
FieldDescription,
|
|
8
|
-
FieldError,
|
|
9
|
-
FieldLabel,
|
|
10
|
-
} from '@/components/ui/field';
|
|
11
|
-
import { Input } from '@/components/ui/input';
|
|
12
|
-
import { Progress } from '@/components/ui/progress';
|
|
13
|
-
import {
|
|
14
|
-
Sheet,
|
|
15
|
-
SheetContent,
|
|
16
|
-
SheetDescription,
|
|
17
|
-
SheetFooter,
|
|
18
|
-
SheetHeader,
|
|
19
|
-
SheetTitle,
|
|
20
|
-
} from '@/components/ui/sheet';
|
|
21
|
-
import { Switch } from '@/components/ui/switch';
|
|
22
|
-
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
23
|
-
import { zodResolver } from '@hookform/resolvers/zod';
|
|
24
|
-
import { Loader2, Trash2, Upload } from 'lucide-react';
|
|
25
|
-
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
26
|
-
import { Controller, useForm } from 'react-hook-form';
|
|
27
|
-
import { toast } from 'sonner';
|
|
28
|
-
import { z } from 'zod';
|
|
29
|
-
|
|
30
|
-
const createInstructorSchema = z.object({
|
|
31
|
-
nome: z.string().trim().min(3, 'Nome e obrigatorio'),
|
|
32
|
-
email: z
|
|
33
|
-
.string()
|
|
34
|
-
.trim()
|
|
35
|
-
.max(255)
|
|
36
|
-
.refine(
|
|
37
|
-
(value) => !value || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
|
|
38
|
-
'E-mail invalido'
|
|
39
|
-
),
|
|
40
|
-
telefone: z
|
|
41
|
-
.string()
|
|
42
|
-
.trim()
|
|
43
|
-
.max(50)
|
|
44
|
-
.refine(
|
|
45
|
-
(value) => !value || /^[0-9+()\s-]{8,20}$/.test(value),
|
|
46
|
-
'Telefone invalido'
|
|
47
|
-
),
|
|
48
|
-
qualificationSlugs: z
|
|
49
|
-
.array(z.string())
|
|
50
|
-
.min(1, 'Selecione pelo menos uma atuacao'),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
type CreateInstructorForm = z.infer<typeof createInstructorSchema>;
|
|
54
|
-
|
|
55
|
-
type CreatedInstructor = {
|
|
56
|
-
id: number;
|
|
57
|
-
personId: number;
|
|
58
|
-
name: string;
|
|
59
|
-
avatarId?: number | null;
|
|
60
|
-
email?: string | null;
|
|
61
|
-
phone?: string | null;
|
|
62
|
-
qualificationSlugs: string[];
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
type InstructorDetails = CreatedInstructor & {
|
|
66
|
-
status?: 'active' | 'inactive';
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
type CreateLmsInstructorSheetProps = {
|
|
70
|
-
open: boolean;
|
|
71
|
-
onOpenChange: (open: boolean) => void;
|
|
72
|
-
onCreated?: (instructor: CreatedInstructor) => void | Promise<void>;
|
|
73
|
-
onSaved?: (instructor: CreatedInstructor) => void | Promise<void>;
|
|
74
|
-
instructorId?: number | null;
|
|
75
|
-
title: string;
|
|
76
|
-
description: string;
|
|
77
|
-
submitLabel: string;
|
|
78
|
-
successMessage: string;
|
|
79
|
-
errorMessage: string;
|
|
80
|
-
defaultQualificationSlugs?: string[];
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
type UploadedFilePayload = {
|
|
84
|
-
id?: number;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
type OpenFilePayload = {
|
|
88
|
-
url?: string;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const QUALIFICATION_OPTIONS = [
|
|
92
|
-
{
|
|
93
|
-
slug: 'course-lessons',
|
|
94
|
-
label: 'Aulas de curso',
|
|
95
|
-
description: 'Pode atuar em aulas gravadas e estrutura de curso.',
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
slug: 'class-sessions',
|
|
99
|
-
label: 'Sessoes de turma',
|
|
100
|
-
description: 'Pode atuar em contextos ao vivo e turmas.',
|
|
101
|
-
},
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
function getInstructorInitials(name?: string | null) {
|
|
105
|
-
return String(name || '')
|
|
106
|
-
.split(' ')
|
|
107
|
-
.filter(Boolean)
|
|
108
|
-
.slice(0, 2)
|
|
109
|
-
.map((part) => part[0]?.toUpperCase() || '')
|
|
110
|
-
.join('');
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function getInstructorAvatarUrl(fileId?: number | null) {
|
|
114
|
-
return typeof fileId === 'number' && fileId > 0
|
|
115
|
-
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${fileId}`
|
|
116
|
-
: '/placeholder.png';
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function CreateLmsInstructorSheet({
|
|
120
|
-
open,
|
|
121
|
-
onOpenChange,
|
|
122
|
-
onCreated,
|
|
123
|
-
onSaved,
|
|
124
|
-
instructorId,
|
|
125
|
-
title,
|
|
126
|
-
description,
|
|
127
|
-
submitLabel,
|
|
128
|
-
successMessage,
|
|
129
|
-
errorMessage,
|
|
130
|
-
defaultQualificationSlugs,
|
|
131
|
-
}: CreateLmsInstructorSheetProps) {
|
|
132
|
-
const { request } = useApp();
|
|
133
|
-
const [saving, setSaving] = useState(false);
|
|
134
|
-
const [avatarId, setAvatarId] = useState<number | null>(null);
|
|
135
|
-
const [persistedAvatarId, setPersistedAvatarId] = useState<number | null>(
|
|
136
|
-
null
|
|
137
|
-
);
|
|
138
|
-
const [avatarPreviewUrl, setAvatarPreviewUrl] =
|
|
139
|
-
useState<string>('/placeholder.png');
|
|
140
|
-
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
|
|
141
|
-
const [avatarUploadProgress, setAvatarUploadProgress] = useState(0);
|
|
142
|
-
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
143
|
-
const isEditing = typeof instructorId === 'number' && instructorId > 0;
|
|
144
|
-
|
|
145
|
-
const initialQualificationSlugs = useMemo(
|
|
146
|
-
() =>
|
|
147
|
-
defaultQualificationSlugs && defaultQualificationSlugs.length > 0
|
|
148
|
-
? [...new Set(defaultQualificationSlugs)]
|
|
149
|
-
: ['course-lessons'],
|
|
150
|
-
[defaultQualificationSlugs]
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
const form = useForm<CreateInstructorForm>({
|
|
154
|
-
resolver: zodResolver(createInstructorSchema),
|
|
155
|
-
defaultValues: {
|
|
156
|
-
nome: '',
|
|
157
|
-
email: '',
|
|
158
|
-
telefone: '',
|
|
159
|
-
qualificationSlugs: initialQualificationSlugs,
|
|
160
|
-
},
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
const {
|
|
164
|
-
data: instructorDetails,
|
|
165
|
-
isLoading: loadingInstructorDetails,
|
|
166
|
-
refetch: refetchInstructorDetails,
|
|
167
|
-
} = useQuery<InstructorDetails>({
|
|
168
|
-
queryKey: ['lms-instructor-sheet', instructorId],
|
|
169
|
-
enabled: open && isEditing,
|
|
170
|
-
queryFn: async () => {
|
|
171
|
-
const response = await request<InstructorDetails>({
|
|
172
|
-
url: `/lms/instructors/${instructorId}`,
|
|
173
|
-
method: 'GET',
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
return response.data;
|
|
177
|
-
},
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
useEffect(() => {
|
|
181
|
-
if (!open || !isEditing) {
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
void refetchInstructorDetails();
|
|
186
|
-
}, [isEditing, open, refetchInstructorDetails]);
|
|
187
|
-
|
|
188
|
-
useEffect(() => {
|
|
189
|
-
if (!open) {
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const instructor = isEditing ? instructorDetails : undefined;
|
|
194
|
-
|
|
195
|
-
if (isEditing && !instructor) {
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const instructorQualificationSlugs: string[] = Array.isArray(
|
|
200
|
-
instructor?.qualificationSlugs
|
|
201
|
-
)
|
|
202
|
-
? instructor.qualificationSlugs
|
|
203
|
-
: [];
|
|
204
|
-
|
|
205
|
-
form.reset({
|
|
206
|
-
nome: instructor?.name || '',
|
|
207
|
-
email: instructor?.email || '',
|
|
208
|
-
telefone: instructor?.phone || '',
|
|
209
|
-
qualificationSlugs:
|
|
210
|
-
instructorQualificationSlugs.length > 0
|
|
211
|
-
? instructorQualificationSlugs
|
|
212
|
-
: initialQualificationSlugs,
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
const nextAvatarId = instructor?.avatarId ?? null;
|
|
216
|
-
setAvatarId(nextAvatarId);
|
|
217
|
-
setPersistedAvatarId(nextAvatarId);
|
|
218
|
-
setAvatarPreviewUrl(getInstructorAvatarUrl(nextAvatarId));
|
|
219
|
-
setIsUploadingAvatar(false);
|
|
220
|
-
setAvatarUploadProgress(0);
|
|
221
|
-
}, [form, initialQualificationSlugs, instructorDetails, isEditing, open]);
|
|
222
|
-
|
|
223
|
-
const deleteFileById = async (fileId?: number | null) => {
|
|
224
|
-
if (!fileId || fileId <= 0) return;
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
await request({
|
|
228
|
-
url: '/file',
|
|
229
|
-
method: 'DELETE',
|
|
230
|
-
data: { ids: [fileId] },
|
|
231
|
-
});
|
|
232
|
-
} catch {
|
|
233
|
-
// Ignore cleanup failures to avoid interrupting the sheet flow.
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
const cleanupUnsavedAvatar = async () => {
|
|
238
|
-
if (avatarId && avatarId !== persistedAvatarId) {
|
|
239
|
-
await deleteFileById(avatarId);
|
|
240
|
-
}
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const handleAvatarUpload = async (file: File) => {
|
|
244
|
-
if (!file.type.startsWith('image/')) {
|
|
245
|
-
toast.error('Selecione um arquivo de imagem valido.');
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (file.size > 5 * 1024 * 1024) {
|
|
250
|
-
toast.error('A imagem deve ter no maximo 5MB.');
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
setIsUploadingAvatar(true);
|
|
255
|
-
setAvatarUploadProgress(0);
|
|
256
|
-
|
|
257
|
-
try {
|
|
258
|
-
const formData = new FormData();
|
|
259
|
-
formData.append('file', file);
|
|
260
|
-
formData.append('destination', 'contact/person/avatar');
|
|
261
|
-
|
|
262
|
-
const previousAvatarId = avatarId;
|
|
263
|
-
|
|
264
|
-
const { data } = await request<UploadedFilePayload>({
|
|
265
|
-
url: '/file',
|
|
266
|
-
method: 'POST',
|
|
267
|
-
data: formData,
|
|
268
|
-
headers: {
|
|
269
|
-
'Content-Type': 'multipart/form-data',
|
|
270
|
-
},
|
|
271
|
-
onUploadProgress: (event) => {
|
|
272
|
-
if (!event.total) return;
|
|
273
|
-
setAvatarUploadProgress(
|
|
274
|
-
Math.round((event.loaded * 100) / event.total)
|
|
275
|
-
);
|
|
276
|
-
},
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
const nextAvatarId = Number(data?.id);
|
|
280
|
-
if (!nextAvatarId) {
|
|
281
|
-
throw new Error('invalid avatar id');
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
const openResponse = await request<OpenFilePayload>({
|
|
285
|
-
url: `/file/open/${nextAvatarId}`,
|
|
286
|
-
method: 'PUT',
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
const openUrl = String(openResponse?.data?.url || '').trim();
|
|
290
|
-
|
|
291
|
-
if (previousAvatarId && previousAvatarId !== persistedAvatarId) {
|
|
292
|
-
await deleteFileById(previousAvatarId);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
setAvatarId(nextAvatarId);
|
|
296
|
-
setAvatarPreviewUrl(
|
|
297
|
-
openUrl.length > 0
|
|
298
|
-
? /^https?:\/\//i.test(openUrl)
|
|
299
|
-
? openUrl
|
|
300
|
-
: `${String(process.env.NEXT_PUBLIC_API_BASE_URL || '')}${openUrl}`
|
|
301
|
-
: getInstructorAvatarUrl(nextAvatarId)
|
|
302
|
-
);
|
|
303
|
-
setAvatarUploadProgress(100);
|
|
304
|
-
toast.success('Foto enviada com sucesso.');
|
|
305
|
-
} catch {
|
|
306
|
-
toast.error('Nao foi possivel enviar a foto.');
|
|
307
|
-
setAvatarUploadProgress(0);
|
|
308
|
-
} finally {
|
|
309
|
-
setIsUploadingAvatar(false);
|
|
310
|
-
if (fileInputRef.current) {
|
|
311
|
-
fileInputRef.current.value = '';
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
const handleRemoveAvatar = async () => {
|
|
317
|
-
if (isUploadingAvatar) return;
|
|
318
|
-
|
|
319
|
-
if (avatarId && avatarId !== persistedAvatarId) {
|
|
320
|
-
await deleteFileById(avatarId);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
setAvatarId(null);
|
|
324
|
-
setAvatarPreviewUrl('/placeholder.png');
|
|
325
|
-
setAvatarUploadProgress(0);
|
|
326
|
-
toast.success('Foto removida com sucesso.');
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
const handleSubmit = form.handleSubmit(async (values) => {
|
|
330
|
-
setSaving(true);
|
|
331
|
-
try {
|
|
332
|
-
const response = await request<CreatedInstructor>({
|
|
333
|
-
url: isEditing
|
|
334
|
-
? `/lms/instructors/${instructorId}`
|
|
335
|
-
: '/lms/instructors',
|
|
336
|
-
method: isEditing ? 'PATCH' : 'POST',
|
|
337
|
-
data: {
|
|
338
|
-
name: values.nome.trim(),
|
|
339
|
-
email: values.email.trim() || undefined,
|
|
340
|
-
phone: values.telefone.trim() || undefined,
|
|
341
|
-
avatarId,
|
|
342
|
-
qualificationSlugs: values.qualificationSlugs,
|
|
343
|
-
status: 'active',
|
|
344
|
-
},
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
const savedInstructor = response.data;
|
|
348
|
-
|
|
349
|
-
if (!savedInstructor?.id || !savedInstructor?.name) {
|
|
350
|
-
throw new Error('invalid instructor payload');
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
const savedAvatarId =
|
|
354
|
-
typeof savedInstructor.avatarId === 'number'
|
|
355
|
-
? savedInstructor.avatarId
|
|
356
|
-
: null;
|
|
357
|
-
|
|
358
|
-
form.reset({
|
|
359
|
-
nome: savedInstructor.name || values.nome.trim(),
|
|
360
|
-
email: savedInstructor.email || values.email.trim(),
|
|
361
|
-
telefone: savedInstructor.phone || values.telefone.trim(),
|
|
362
|
-
qualificationSlugs:
|
|
363
|
-
savedInstructor.qualificationSlugs?.length > 0
|
|
364
|
-
? savedInstructor.qualificationSlugs
|
|
365
|
-
: values.qualificationSlugs,
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
setAvatarId(savedAvatarId);
|
|
369
|
-
setPersistedAvatarId(savedAvatarId);
|
|
370
|
-
setAvatarPreviewUrl(getInstructorAvatarUrl(savedAvatarId));
|
|
371
|
-
|
|
372
|
-
if (!isEditing && onCreated) {
|
|
373
|
-
await onCreated(savedInstructor);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
if (onSaved) {
|
|
377
|
-
await onSaved(savedInstructor);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
onOpenChange(false);
|
|
381
|
-
toast.success(successMessage);
|
|
382
|
-
} catch {
|
|
383
|
-
toast.error(errorMessage);
|
|
384
|
-
} finally {
|
|
385
|
-
setSaving(false);
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
const handleSheetOpenChange = (nextOpen: boolean) => {
|
|
390
|
-
if (nextOpen) {
|
|
391
|
-
onOpenChange(true);
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (saving || isUploadingAvatar) {
|
|
396
|
-
return;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
void cleanupUnsavedAvatar();
|
|
400
|
-
onOpenChange(false);
|
|
401
|
-
};
|
|
402
|
-
|
|
403
|
-
return (
|
|
404
|
-
<Sheet open={open} onOpenChange={handleSheetOpenChange}>
|
|
405
|
-
<SheetContent
|
|
406
|
-
side="right"
|
|
407
|
-
className="flex w-full flex-col sm:max-w-lg overflow-y-auto"
|
|
408
|
-
>
|
|
409
|
-
<SheetHeader>
|
|
410
|
-
<SheetTitle>{title}</SheetTitle>
|
|
411
|
-
<SheetDescription>{description}</SheetDescription>
|
|
412
|
-
</SheetHeader>
|
|
413
|
-
|
|
414
|
-
{isEditing && loadingInstructorDetails ? (
|
|
415
|
-
<div className="flex items-center gap-2 px-4 py-10 text-sm text-muted-foreground">
|
|
416
|
-
<Loader2 className="size-4 animate-spin" />
|
|
417
|
-
Carregando dados do instrutor...
|
|
418
|
-
</div>
|
|
419
|
-
) : (
|
|
420
|
-
<form onSubmit={handleSubmit} className="mt-6 space-y-4 px-4">
|
|
421
|
-
<Field>
|
|
422
|
-
<FieldLabel>Foto</FieldLabel>
|
|
423
|
-
<FieldDescription>
|
|
424
|
-
JPG, PNG ou GIF. Tamanho maximo de 5MB.
|
|
425
|
-
</FieldDescription>
|
|
426
|
-
<div className="flex items-center gap-4 rounded-lg border p-4">
|
|
427
|
-
<Avatar className="h-22 w-22 rounded-full border">
|
|
428
|
-
<AvatarImage
|
|
429
|
-
src={avatarPreviewUrl}
|
|
430
|
-
alt={form.watch('nome') || 'Instrutor'}
|
|
431
|
-
/>
|
|
432
|
-
<AvatarFallback className="rounded-md text-sm font-semibold uppercase">
|
|
433
|
-
{getInstructorInitials(form.watch('nome') || 'Instrutor')}
|
|
434
|
-
</AvatarFallback>
|
|
435
|
-
</Avatar>
|
|
436
|
-
|
|
437
|
-
<div className="flex-1 space-y-2">
|
|
438
|
-
<input
|
|
439
|
-
ref={fileInputRef}
|
|
440
|
-
type="file"
|
|
441
|
-
accept="image/*"
|
|
442
|
-
className="hidden"
|
|
443
|
-
onChange={(event) => {
|
|
444
|
-
const file = event.target.files?.[0];
|
|
445
|
-
if (file) {
|
|
446
|
-
void handleAvatarUpload(file);
|
|
447
|
-
}
|
|
448
|
-
}}
|
|
449
|
-
/>
|
|
450
|
-
|
|
451
|
-
<div className="flex flex-wrap gap-2">
|
|
452
|
-
<Button
|
|
453
|
-
type="button"
|
|
454
|
-
variant="outline"
|
|
455
|
-
onClick={() => fileInputRef.current?.click()}
|
|
456
|
-
disabled={isUploadingAvatar}
|
|
457
|
-
>
|
|
458
|
-
{isUploadingAvatar ? (
|
|
459
|
-
<Loader2 className="mr-2 size-4 animate-spin" />
|
|
460
|
-
) : (
|
|
461
|
-
<Upload className="mr-2 size-4" />
|
|
462
|
-
)}
|
|
463
|
-
{avatarId ? 'Trocar foto' : 'Enviar foto'}
|
|
464
|
-
</Button>
|
|
465
|
-
|
|
466
|
-
{(avatarId || isUploadingAvatar) && (
|
|
467
|
-
<Button
|
|
468
|
-
type="button"
|
|
469
|
-
variant="ghost"
|
|
470
|
-
onClick={() => void handleRemoveAvatar()}
|
|
471
|
-
disabled={!avatarId || isUploadingAvatar}
|
|
472
|
-
>
|
|
473
|
-
<Trash2 className="mr-2 size-4" />
|
|
474
|
-
Remover
|
|
475
|
-
</Button>
|
|
476
|
-
)}
|
|
477
|
-
</div>
|
|
478
|
-
|
|
479
|
-
{isUploadingAvatar && (
|
|
480
|
-
<div className="space-y-1">
|
|
481
|
-
<Progress value={avatarUploadProgress} className="h-2" />
|
|
482
|
-
<p className="text-xs text-muted-foreground">
|
|
483
|
-
Enviando foto... {avatarUploadProgress}%
|
|
484
|
-
</p>
|
|
485
|
-
</div>
|
|
486
|
-
)}
|
|
487
|
-
</div>
|
|
488
|
-
</div>
|
|
489
|
-
</Field>
|
|
490
|
-
|
|
491
|
-
<Field>
|
|
492
|
-
<FieldLabel htmlFor="newInstructorName">Nome</FieldLabel>
|
|
493
|
-
<Input
|
|
494
|
-
id="newInstructorName"
|
|
495
|
-
placeholder="Nome completo"
|
|
496
|
-
{...form.register('nome')}
|
|
497
|
-
/>
|
|
498
|
-
<FieldError>{form.formState.errors.nome?.message}</FieldError>
|
|
499
|
-
</Field>
|
|
500
|
-
|
|
501
|
-
<Field>
|
|
502
|
-
<FieldLabel htmlFor="newInstructorEmail">Email</FieldLabel>
|
|
503
|
-
<Input
|
|
504
|
-
id="newInstructorEmail"
|
|
505
|
-
type="email"
|
|
506
|
-
placeholder="nome@exemplo.com"
|
|
507
|
-
{...form.register('email')}
|
|
508
|
-
/>
|
|
509
|
-
<FieldError>{form.formState.errors.email?.message}</FieldError>
|
|
510
|
-
</Field>
|
|
511
|
-
|
|
512
|
-
<Field>
|
|
513
|
-
<FieldLabel htmlFor="newInstructorPhone">Telefone</FieldLabel>
|
|
514
|
-
<Input
|
|
515
|
-
id="newInstructorPhone"
|
|
516
|
-
placeholder="+55 (11) 90000-0000"
|
|
517
|
-
{...form.register('telefone')}
|
|
518
|
-
/>
|
|
519
|
-
<FieldError>{form.formState.errors.telefone?.message}</FieldError>
|
|
520
|
-
</Field>
|
|
521
|
-
|
|
522
|
-
<Field>
|
|
523
|
-
<FieldLabel>Atuacoes</FieldLabel>
|
|
524
|
-
<FieldDescription>
|
|
525
|
-
Selecione em quais contextos esse instrutor pode atuar.
|
|
526
|
-
</FieldDescription>
|
|
527
|
-
<Controller
|
|
528
|
-
control={form.control}
|
|
529
|
-
name="qualificationSlugs"
|
|
530
|
-
render={({ field }) => (
|
|
531
|
-
<div className="space-y-2 rounded-lg border p-3">
|
|
532
|
-
{QUALIFICATION_OPTIONS.map((option) => {
|
|
533
|
-
const checked = field.value.includes(option.slug);
|
|
534
|
-
|
|
535
|
-
return (
|
|
536
|
-
<label
|
|
537
|
-
key={option.slug}
|
|
538
|
-
className="flex items-start justify-between gap-3 rounded-md border px-3 py-2"
|
|
539
|
-
>
|
|
540
|
-
<div className="space-y-1">
|
|
541
|
-
<p className="text-sm font-medium">
|
|
542
|
-
{option.label}
|
|
543
|
-
</p>
|
|
544
|
-
<p className="text-xs text-muted-foreground">
|
|
545
|
-
{option.description}
|
|
546
|
-
</p>
|
|
547
|
-
</div>
|
|
548
|
-
<Switch
|
|
549
|
-
checked={checked}
|
|
550
|
-
onCheckedChange={(nextChecked) => {
|
|
551
|
-
if (nextChecked) {
|
|
552
|
-
field.onChange([...field.value, option.slug]);
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
field.onChange(
|
|
557
|
-
field.value.filter(
|
|
558
|
-
(value) => value !== option.slug
|
|
559
|
-
)
|
|
560
|
-
);
|
|
561
|
-
}}
|
|
562
|
-
/>
|
|
563
|
-
</label>
|
|
564
|
-
);
|
|
565
|
-
})}
|
|
566
|
-
</div>
|
|
567
|
-
)}
|
|
568
|
-
/>
|
|
569
|
-
<FieldError>
|
|
570
|
-
{form.formState.errors.qualificationSlugs?.message}
|
|
571
|
-
</FieldError>
|
|
572
|
-
</Field>
|
|
573
|
-
|
|
574
|
-
<SheetFooter className="mt-6 px-0">
|
|
575
|
-
<Button
|
|
576
|
-
type="submit"
|
|
577
|
-
disabled={saving || isUploadingAvatar}
|
|
578
|
-
className="gap-2"
|
|
579
|
-
>
|
|
580
|
-
{(saving || isUploadingAvatar) && (
|
|
581
|
-
<Loader2 className="mr-2 size-4 animate-spin" />
|
|
582
|
-
)}
|
|
583
|
-
{submitLabel}
|
|
584
|
-
</Button>
|
|
585
|
-
</SheetFooter>
|
|
586
|
-
</form>
|
|
587
|
-
)}
|
|
588
|
-
</SheetContent>
|
|
589
|
-
</Sheet>
|
|
590
|
-
);
|
|
591
|
-
}
|