@hed-hog/lms 0.0.331 → 0.0.347
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/class-group/class-group.controller.d.ts +8 -8
- package/dist/class-group/class-group.service.d.ts +8 -8
- package/dist/course/course.controller.d.ts +6 -1
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.controller.js +19 -2
- package/dist/course/course.controller.js.map +1 -1
- package/dist/course/course.service.d.ts +6 -0
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +63 -28
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/create-course.dto.d.ts +1 -0
- package/dist/course/dto/create-course.dto.d.ts.map +1 -1
- package/dist/course/dto/create-course.dto.js +5 -0
- package/dist/course/dto/create-course.dto.js.map +1 -1
- package/dist/enterprise/enterprise.controller.d.ts +84 -12
- 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 +90 -12
- 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 +9 -6
- 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 +11 -5
- 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 +2 -2
- package/dist/evaluation/evaluation.service.d.ts +2 -2
- 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 +21 -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 +27 -0
- package/dist/instructor/instructor.service.d.ts.map +1 -1
- package/dist/instructor/instructor.service.js +79 -25
- 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/dist/training/dto/create-training.dto.d.ts +1 -0
- package/dist/training/dto/create-training.dto.d.ts.map +1 -1
- package/dist/training/dto/create-training.dto.js +5 -0
- package/dist/training/dto/create-training.dto.js.map +1 -1
- package/dist/training/training.controller.d.ts +4 -0
- package/dist/training/training.controller.d.ts.map +1 -1
- package/dist/training/training.service.d.ts +8 -0
- package/dist/training/training.service.d.ts.map +1 -1
- package/dist/training/training.service.js +71 -6
- package/dist/training/training.service.js.map +1 -1
- package/hedhog/data/route.yaml +23 -1
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +80 -33
- 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 +39 -7
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +1 -3
- package/hedhog/frontend/app/classes/page.tsx.ejs +34 -7
- 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 +243 -34
- 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 +31 -19
- 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 +76 -8
- package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/training/page.tsx.ejs +78 -9
- package/hedhog/frontend/messages/en.json +101 -10
- package/hedhog/frontend/messages/pt.json +115 -11
- package/hedhog/table/enterprise_student_license_event.yaml +30 -0
- package/hedhog/table/instructor_skill.yaml +0 -11
- package/hedhog/table/learning_path.yaml +4 -0
- package/package.json +6 -6
- package/src/course/course.controller.ts +18 -0
- package/src/course/course.service.ts +85 -26
- package/src/course/dto/create-course.dto.ts +4 -0
- 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 +87 -10
- package/src/lms.module.ts +1 -0
- package/src/training/dto/create-training.dto.ts +4 -0
- package/src/training/training.service.ts +104 -5
|
@@ -854,9 +854,7 @@ const getAulaSchema = (t: (key: string) => string) =>
|
|
|
854
854
|
horaFim: z
|
|
855
855
|
.string()
|
|
856
856
|
.min(1, t('sheet.lessonForm.validation.endTimeRequired')),
|
|
857
|
-
local: z
|
|
858
|
-
.string()
|
|
859
|
-
.min(1, t('sheet.lessonForm.validation.locationRequired')),
|
|
857
|
+
local: z.string().optional(),
|
|
860
858
|
tipo: z.string().min(1, t('sheet.lessonForm.validation.typeRequired')),
|
|
861
859
|
instrutorId: z.string().optional(),
|
|
862
860
|
recurrenceFrequency: z
|
|
@@ -95,6 +95,7 @@ import {
|
|
|
95
95
|
import { useTranslations } from 'next-intl';
|
|
96
96
|
import { useRouter } from 'next/navigation';
|
|
97
97
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
98
|
+
import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
|
|
98
99
|
import type { DateRange } from 'react-day-picker';
|
|
99
100
|
import { Controller, useForm, useWatch } from 'react-hook-form';
|
|
100
101
|
import { toast } from 'sonner';
|
|
@@ -226,6 +227,7 @@ type InstructorOption = {
|
|
|
226
227
|
name: string;
|
|
227
228
|
personId?: number;
|
|
228
229
|
avatarId?: number | null;
|
|
230
|
+
userPhotoId?: number | null;
|
|
229
231
|
qualificationSlugs?: string[];
|
|
230
232
|
};
|
|
231
233
|
|
|
@@ -241,6 +243,8 @@ type InstructorApiRow = {
|
|
|
241
243
|
person_id?: number | string;
|
|
242
244
|
avatarId?: number | string | null;
|
|
243
245
|
avatar_id?: number | string | null;
|
|
246
|
+
userPhotoId?: number | string | null;
|
|
247
|
+
user_photo_id?: number | string | null;
|
|
244
248
|
qualificationSlugs?: string[];
|
|
245
249
|
};
|
|
246
250
|
|
|
@@ -267,18 +271,39 @@ function normalizeInstructorOption(
|
|
|
267
271
|
rawAvatarId !== undefined && rawAvatarId !== null
|
|
268
272
|
? Number(rawAvatarId) || null
|
|
269
273
|
: null;
|
|
274
|
+
const rawUserPhotoId = item?.userPhotoId ?? item?.user_photo_id;
|
|
275
|
+
const userPhotoId =
|
|
276
|
+
rawUserPhotoId !== undefined && rawUserPhotoId !== null
|
|
277
|
+
? Number(rawUserPhotoId) || null
|
|
278
|
+
: null;
|
|
270
279
|
|
|
271
280
|
return {
|
|
272
281
|
id,
|
|
273
282
|
name,
|
|
274
283
|
personId: Number(item?.personId ?? item?.person_id ?? 0) || undefined,
|
|
275
284
|
avatarId,
|
|
285
|
+
userPhotoId,
|
|
276
286
|
qualificationSlugs: Array.isArray(item?.qualificationSlugs)
|
|
277
287
|
? item.qualificationSlugs
|
|
278
288
|
: undefined,
|
|
279
289
|
};
|
|
280
290
|
}
|
|
281
291
|
|
|
292
|
+
function getInstructorAvatarUrl(option?: {
|
|
293
|
+
userPhotoId?: number | null;
|
|
294
|
+
avatarId?: number | null;
|
|
295
|
+
}) {
|
|
296
|
+
if (option?.userPhotoId) {
|
|
297
|
+
return `${process.env.NEXT_PUBLIC_API_BASE_URL}/user/avatar/${option.userPhotoId}`;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (option?.avatarId) {
|
|
301
|
+
return `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${option.avatarId}`;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
|
|
282
307
|
function getCourseIdByTitle(
|
|
283
308
|
courses: Array<{ id: number; title: string }>,
|
|
284
309
|
title: string
|
|
@@ -700,7 +725,11 @@ export default function TurmasPage() {
|
|
|
700
725
|
|
|
701
726
|
// Pagination
|
|
702
727
|
const [currentPage, setCurrentPage] = useState(1);
|
|
703
|
-
const [pageSize, setPageSize] =
|
|
728
|
+
const [pageSize, setPageSize] = usePersistedPageSize({
|
|
729
|
+
storageKey: 'pagination:global:pageSize',
|
|
730
|
+
defaultValue: 12,
|
|
731
|
+
allowedValues: PAGE_SIZES,
|
|
732
|
+
});
|
|
704
733
|
|
|
705
734
|
// Double-click tracking
|
|
706
735
|
const clickTimers = useRef<Map<number, ReturnType<typeof setTimeout>>>(
|
|
@@ -749,6 +778,7 @@ export default function TurmasPage() {
|
|
|
749
778
|
params: {
|
|
750
779
|
page: 1,
|
|
751
780
|
pageSize: 500,
|
|
781
|
+
offeringTypes: 'scheduled,blended',
|
|
752
782
|
},
|
|
753
783
|
});
|
|
754
784
|
return response.data;
|
|
@@ -2384,6 +2414,7 @@ export default function TurmasPage() {
|
|
|
2384
2414
|
params: {
|
|
2385
2415
|
page,
|
|
2386
2416
|
pageSize,
|
|
2417
|
+
offeringTypes: 'scheduled,blended',
|
|
2387
2418
|
...(search.trim() ? { search: search.trim() } : {}),
|
|
2388
2419
|
},
|
|
2389
2420
|
});
|
|
@@ -2912,9 +2943,7 @@ export default function TurmasPage() {
|
|
|
2912
2943
|
.slice(0, 2)
|
|
2913
2944
|
.map((p) => p[0]?.toUpperCase() ?? '')
|
|
2914
2945
|
.join('');
|
|
2915
|
-
const avatarUrl = option
|
|
2916
|
-
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${option.avatarId}`
|
|
2917
|
-
: undefined;
|
|
2946
|
+
const avatarUrl = getInstructorAvatarUrl(option);
|
|
2918
2947
|
return (
|
|
2919
2948
|
<div className="flex min-w-0 items-center gap-3 py-0.5">
|
|
2920
2949
|
<Avatar className="h-8 w-8 shrink-0 rounded-lg border border-border/60">
|
|
@@ -2937,9 +2966,7 @@ export default function TurmasPage() {
|
|
|
2937
2966
|
.slice(0, 2)
|
|
2938
2967
|
.map((p) => p[0]?.toUpperCase() ?? '')
|
|
2939
2968
|
.join('');
|
|
2940
|
-
const avatarUrl = option
|
|
2941
|
-
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${option.avatarId}`
|
|
2942
|
-
: undefined;
|
|
2969
|
+
const avatarUrl = getInstructorAvatarUrl(option ?? undefined);
|
|
2943
2970
|
return (
|
|
2944
2971
|
<div className="flex items-center gap-2">
|
|
2945
2972
|
<Avatar className="h-5 w-5 shrink-0 rounded">
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
SelectTrigger,
|
|
14
14
|
SelectValue,
|
|
15
15
|
} from '@/components/ui/select';
|
|
16
|
-
import {
|
|
16
|
+
import { Settings2 } from 'lucide-react';
|
|
17
17
|
import type { UseFormReturn } from 'react-hook-form';
|
|
18
18
|
|
|
19
19
|
import { CourseSectionCard } from './CourseSectionCard';
|
|
@@ -33,23 +33,6 @@ type CourseClassificationCardProps = {
|
|
|
33
33
|
offeringTypes: readonly SelectOption[];
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
function ColorPreview({ color, label }: { color: string; label: string }) {
|
|
37
|
-
return (
|
|
38
|
-
<div className="flex items-center gap-2 rounded-lg border border-border/60 bg-muted/20 px-2.5 py-1.5">
|
|
39
|
-
<span
|
|
40
|
-
className="h-4 w-4 shrink-0 rounded-full border border-black/10"
|
|
41
|
-
style={{ backgroundColor: color }}
|
|
42
|
-
/>
|
|
43
|
-
<div className="min-w-0">
|
|
44
|
-
<p className="text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
|
|
45
|
-
{label}
|
|
46
|
-
</p>
|
|
47
|
-
<p className="truncate text-xs font-medium text-foreground">{color}</p>
|
|
48
|
-
</div>
|
|
49
|
-
</div>
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
36
|
export function CourseClassificationCard({
|
|
54
37
|
form,
|
|
55
38
|
t,
|
|
@@ -57,10 +40,6 @@ export function CourseClassificationCard({
|
|
|
57
40
|
statuses,
|
|
58
41
|
offeringTypes,
|
|
59
42
|
}: CourseClassificationCardProps) {
|
|
60
|
-
const watchedPrimary = form.watch('primaryColor');
|
|
61
|
-
const watchedSecondary = form.watch('secondaryColor');
|
|
62
|
-
const watchedOfferingType = form.watch('tipoOferta');
|
|
63
|
-
|
|
64
43
|
return (
|
|
65
44
|
<CourseSectionCard
|
|
66
45
|
title="Classificação e configuração"
|
|
@@ -156,7 +135,7 @@ export function CourseClassificationCard({
|
|
|
156
135
|
<FormItem>
|
|
157
136
|
<FormLabel>{t('form.fields.primaryColor.label')}</FormLabel>
|
|
158
137
|
<FormControl>
|
|
159
|
-
<div className="flex items-center gap-2
|
|
138
|
+
<div className="flex items-center gap-2">
|
|
160
139
|
<Input
|
|
161
140
|
type="color"
|
|
162
141
|
value={field.value}
|
|
@@ -183,7 +162,7 @@ export function CourseClassificationCard({
|
|
|
183
162
|
<FormItem>
|
|
184
163
|
<FormLabel>{t('form.fields.secondaryColor.label')}</FormLabel>
|
|
185
164
|
<FormControl>
|
|
186
|
-
<div className="flex items-center gap-2
|
|
165
|
+
<div className="flex items-center gap-2">
|
|
187
166
|
<Input
|
|
188
167
|
type="color"
|
|
189
168
|
value={field.value}
|
|
@@ -202,15 +181,6 @@ export function CourseClassificationCard({
|
|
|
202
181
|
</FormItem>
|
|
203
182
|
)}
|
|
204
183
|
/>
|
|
205
|
-
|
|
206
|
-
<div className="space-y-2 rounded-lg border border-border/60 bg-linear-to-br from-muted/30 to-background p-3">
|
|
207
|
-
<div className="flex items-center gap-1.5 text-xs font-medium text-muted-foreground">
|
|
208
|
-
<Palette className="h-3.5 w-3.5" />
|
|
209
|
-
Preview
|
|
210
|
-
</div>
|
|
211
|
-
<ColorPreview color={watchedPrimary} label="Primária" />
|
|
212
|
-
<ColorPreview color={watchedSecondary} label="Secundária" />
|
|
213
|
-
</div>
|
|
214
184
|
</div>
|
|
215
185
|
</CourseSectionCard>
|
|
216
186
|
);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
1
2
|
import {
|
|
2
3
|
FormControl,
|
|
3
4
|
FormField,
|
|
@@ -5,7 +6,6 @@ import {
|
|
|
5
6
|
FormLabel,
|
|
6
7
|
FormMessage,
|
|
7
8
|
} from '@/components/ui/form';
|
|
8
|
-
import { Textarea } from '@/components/ui/textarea';
|
|
9
9
|
import { FileText } from 'lucide-react';
|
|
10
10
|
import type { UseFormReturn } from 'react-hook-form';
|
|
11
11
|
|
|
@@ -31,10 +31,10 @@ export function CourseContentCard({ form }: CourseContentCardProps) {
|
|
|
31
31
|
<FormItem>
|
|
32
32
|
<FormLabel>Objetivos do curso</FormLabel>
|
|
33
33
|
<FormControl>
|
|
34
|
-
<
|
|
35
|
-
{
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
<RichTextEditor
|
|
35
|
+
value={field.value || ''}
|
|
36
|
+
onChange={field.onChange}
|
|
37
|
+
className="w-full max-w-full"
|
|
38
38
|
/>
|
|
39
39
|
</FormControl>
|
|
40
40
|
<FormMessage />
|
|
@@ -49,10 +49,10 @@ export function CourseContentCard({ form }: CourseContentCardProps) {
|
|
|
49
49
|
<FormItem>
|
|
50
50
|
<FormLabel>Público-alvo</FormLabel>
|
|
51
51
|
<FormControl>
|
|
52
|
-
<
|
|
53
|
-
{
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
<RichTextEditor
|
|
53
|
+
value={field.value || ''}
|
|
54
|
+
onChange={field.onChange}
|
|
55
|
+
className="w-full max-w-full"
|
|
56
56
|
/>
|
|
57
57
|
</FormControl>
|
|
58
58
|
<FormMessage />
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
2
|
+
import {
|
|
3
|
+
FormControl,
|
|
4
|
+
FormField,
|
|
5
|
+
FormItem,
|
|
6
|
+
FormLabel,
|
|
7
|
+
FormMessage,
|
|
8
|
+
} from '@/components/ui/form';
|
|
9
|
+
import { Input } from '@/components/ui/input';
|
|
10
|
+
import { Hash, NotebookPen } from 'lucide-react';
|
|
11
|
+
import type { UseFormReturn } from 'react-hook-form';
|
|
12
|
+
|
|
13
|
+
import { CourseSectionCard } from './CourseSectionCard';
|
|
14
|
+
import type { CourseEditFormValues, TranslationFn } from './course-edit-types';
|
|
15
|
+
|
|
16
|
+
type CourseMainInfoCardProps = {
|
|
17
|
+
form: UseFormReturn<CourseEditFormValues>;
|
|
18
|
+
t: TranslationFn;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function CourseMainInfoCard({ form, t }: CourseMainInfoCardProps) {
|
|
22
|
+
return (
|
|
23
|
+
<CourseSectionCard
|
|
24
|
+
title={t('form.sections.basicInfo')}
|
|
25
|
+
description="Os identificadores e a comunicação principal do curso ficam aqui."
|
|
26
|
+
icon={NotebookPen}
|
|
27
|
+
>
|
|
28
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
29
|
+
<FormField
|
|
30
|
+
control={form.control}
|
|
31
|
+
name="slug"
|
|
32
|
+
render={({ field }) => (
|
|
33
|
+
<FormItem>
|
|
34
|
+
<FormLabel>{t('form.fields.slug.label')}</FormLabel>
|
|
35
|
+
<FormControl>
|
|
36
|
+
<div className="relative">
|
|
37
|
+
<Hash className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
38
|
+
<Input
|
|
39
|
+
{...field}
|
|
40
|
+
className="pl-9 font-mono"
|
|
41
|
+
placeholder={t('form.fields.slug.placeholder')}
|
|
42
|
+
onChange={(event) =>
|
|
43
|
+
field.onChange(
|
|
44
|
+
String(event.target.value || '').toLowerCase()
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
</FormControl>
|
|
50
|
+
<FormMessage />
|
|
51
|
+
</FormItem>
|
|
52
|
+
)}
|
|
53
|
+
/>
|
|
54
|
+
|
|
55
|
+
<FormField
|
|
56
|
+
control={form.control}
|
|
57
|
+
name="nomeInterno"
|
|
58
|
+
render={({ field }) => (
|
|
59
|
+
<FormItem>
|
|
60
|
+
<FormLabel>{t('form.fields.internalName.label')}</FormLabel>
|
|
61
|
+
<FormControl>
|
|
62
|
+
<Input
|
|
63
|
+
{...field}
|
|
64
|
+
placeholder={t('form.fields.internalName.placeholder')}
|
|
65
|
+
/>
|
|
66
|
+
</FormControl>
|
|
67
|
+
<FormMessage />
|
|
68
|
+
</FormItem>
|
|
69
|
+
)}
|
|
70
|
+
/>
|
|
71
|
+
|
|
72
|
+
<FormField
|
|
73
|
+
control={form.control}
|
|
74
|
+
name="tituloComercial"
|
|
75
|
+
render={({ field }) => (
|
|
76
|
+
<FormItem className="md:col-span-2">
|
|
77
|
+
<FormLabel>{t('form.fields.title.label')}</FormLabel>
|
|
78
|
+
<FormControl>
|
|
79
|
+
<Input
|
|
80
|
+
{...field}
|
|
81
|
+
placeholder={t('form.fields.title.placeholder')}
|
|
82
|
+
/>
|
|
83
|
+
</FormControl>
|
|
84
|
+
<FormMessage />
|
|
85
|
+
</FormItem>
|
|
86
|
+
)}
|
|
87
|
+
/>
|
|
88
|
+
|
|
89
|
+
<FormField
|
|
90
|
+
control={form.control}
|
|
91
|
+
name="descricaoPublica"
|
|
92
|
+
render={({ field }) => (
|
|
93
|
+
<FormItem className="md:col-span-2">
|
|
94
|
+
<FormLabel>{t('form.fields.description.label')}</FormLabel>
|
|
95
|
+
<FormControl>
|
|
96
|
+
<RichTextEditor
|
|
97
|
+
value={field.value}
|
|
98
|
+
onChange={field.onChange}
|
|
99
|
+
className="w-full max-w-full"
|
|
100
|
+
/>
|
|
101
|
+
</FormControl>
|
|
102
|
+
<FormMessage />
|
|
103
|
+
</FormItem>
|
|
104
|
+
)}
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
</CourseSectionCard>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -3,7 +3,7 @@ import { Badge } from '@/components/ui/badge';
|
|
|
3
3
|
import { Button } from '@/components/ui/button';
|
|
4
4
|
import { EntityPicker } from '@/components/ui/entity-picker';
|
|
5
5
|
import { cn } from '@/lib/utils';
|
|
6
|
-
import { Grip, Plus, X } from 'lucide-react';
|
|
6
|
+
import { Grip, Pencil, Plus, X } from 'lucide-react';
|
|
7
7
|
import { useMemo, useState } from 'react';
|
|
8
8
|
|
|
9
9
|
import type { PickerOption } from './course-edit-types';
|
|
@@ -28,6 +28,7 @@ type CourseMultiEntityPickerProps = {
|
|
|
28
28
|
}>;
|
|
29
29
|
mapSearchToCreateValues?: (search: string) => Record<string, string>;
|
|
30
30
|
onChange: (nextValue: string[]) => void;
|
|
31
|
+
onEditSelected?: (option: PickerOption) => void;
|
|
31
32
|
emptyHint?: string;
|
|
32
33
|
renderOptionMeta?: boolean;
|
|
33
34
|
variant?: 'default' | 'instructor';
|
|
@@ -57,6 +58,7 @@ export function CourseMultiEntityPicker({
|
|
|
57
58
|
createFields,
|
|
58
59
|
mapSearchToCreateValues,
|
|
59
60
|
onChange,
|
|
61
|
+
onEditSelected,
|
|
60
62
|
emptyHint,
|
|
61
63
|
renderOptionMeta = false,
|
|
62
64
|
variant = 'default',
|
|
@@ -192,18 +194,33 @@ export function CourseMultiEntityPicker({
|
|
|
192
194
|
</div>
|
|
193
195
|
</div>
|
|
194
196
|
|
|
195
|
-
<
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
197
|
+
<div className="flex items-center gap-1">
|
|
198
|
+
{onEditSelected ? (
|
|
199
|
+
<Button
|
|
200
|
+
type="button"
|
|
201
|
+
variant="ghost"
|
|
202
|
+
size="icon"
|
|
203
|
+
className="h-8 w-8 rounded-lg text-muted-foreground hover:text-foreground"
|
|
204
|
+
onClick={() => onEditSelected(option)}
|
|
205
|
+
aria-label={`Editar ${option.label}`}
|
|
206
|
+
>
|
|
207
|
+
<Pencil className="h-4 w-4" />
|
|
208
|
+
</Button>
|
|
209
|
+
) : null}
|
|
210
|
+
|
|
211
|
+
<Button
|
|
212
|
+
type="button"
|
|
213
|
+
variant="ghost"
|
|
214
|
+
size="icon"
|
|
215
|
+
className="h-8 w-8 rounded-lg text-muted-foreground hover:text-foreground"
|
|
216
|
+
onClick={() =>
|
|
217
|
+
onChange(value.filter((item) => item !== option.value))
|
|
218
|
+
}
|
|
219
|
+
aria-label={`Remover ${option.label}`}
|
|
220
|
+
>
|
|
221
|
+
<X className="h-4 w-4" />
|
|
222
|
+
</Button>
|
|
223
|
+
</div>
|
|
207
224
|
</div>
|
|
208
225
|
) : (
|
|
209
226
|
<Badge
|
|
@@ -212,6 +229,16 @@ export function CourseMultiEntityPicker({
|
|
|
212
229
|
className="flex items-center gap-1.5 rounded-full border border-border/60 bg-background px-3 py-1.5 text-xs font-medium text-foreground"
|
|
213
230
|
>
|
|
214
231
|
<span className="max-w-45 truncate">{option.label}</span>
|
|
232
|
+
{onEditSelected ? (
|
|
233
|
+
<button
|
|
234
|
+
type="button"
|
|
235
|
+
className="cursor-pointer text-muted-foreground transition-colors hover:text-foreground"
|
|
236
|
+
onClick={() => onEditSelected(option)}
|
|
237
|
+
aria-label={`Editar ${option.label}`}
|
|
238
|
+
>
|
|
239
|
+
<Pencil className="h-3.5 w-3.5" />
|
|
240
|
+
</button>
|
|
241
|
+
) : null}
|
|
215
242
|
<button
|
|
216
243
|
type="button"
|
|
217
244
|
className="cursor-pointer text-muted-foreground transition-colors hover:text-foreground"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RichTextEditor } from '@/components/rich-text-editor';
|
|
1
2
|
import {
|
|
2
3
|
FormControl,
|
|
3
4
|
FormField,
|
|
@@ -5,7 +6,6 @@ import {
|
|
|
5
6
|
FormLabel,
|
|
6
7
|
FormMessage,
|
|
7
8
|
} from '@/components/ui/form';
|
|
8
|
-
import { Textarea } from '@/components/ui/textarea';
|
|
9
9
|
import { Network } from 'lucide-react';
|
|
10
10
|
import type { UseFormReturn } from 'react-hook-form';
|
|
11
11
|
|
|
@@ -26,6 +26,8 @@ type CourseRelationsCardProps = {
|
|
|
26
26
|
values: Record<string, string>
|
|
27
27
|
) => Promise<PickerOption | null>;
|
|
28
28
|
onCreateInstructor: () => void;
|
|
29
|
+
onEditCategory: (categorySlug: string) => void;
|
|
30
|
+
onEditInstructor: (instructorId: string) => void;
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
export function CourseRelationsCard({
|
|
@@ -35,6 +37,8 @@ export function CourseRelationsCard({
|
|
|
35
37
|
instructorOptions,
|
|
36
38
|
onCreateCategory,
|
|
37
39
|
onCreateInstructor,
|
|
40
|
+
onEditCategory,
|
|
41
|
+
onEditInstructor,
|
|
38
42
|
}: CourseRelationsCardProps) {
|
|
39
43
|
return (
|
|
40
44
|
<CourseSectionCard
|
|
@@ -43,83 +47,74 @@ export function CourseRelationsCard({
|
|
|
43
47
|
icon={Network}
|
|
44
48
|
>
|
|
45
49
|
<div className="space-y-3">
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
<
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
'form.fields.categories.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
.normalize('NFD')
|
|
86
|
-
.replace(/[\u0300-\u036f]/g, '')
|
|
87
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
88
|
-
.replace(/^-+|-+$/g, ''),
|
|
89
|
-
})}
|
|
90
|
-
onCreate={onCreateCategory}
|
|
91
|
-
/>
|
|
92
|
-
<FormMessage />
|
|
93
|
-
</FormItem>
|
|
94
|
-
)}
|
|
95
|
-
/>
|
|
50
|
+
<div className="grid items-start gap-3 lg:grid-cols-2">
|
|
51
|
+
<FormField
|
|
52
|
+
control={form.control}
|
|
53
|
+
name="categorias"
|
|
54
|
+
render={({ field }) => (
|
|
55
|
+
<FormItem className="self-start">
|
|
56
|
+
<FormLabel>{t('form.fields.categories.label')}</FormLabel>
|
|
57
|
+
<CourseMultiEntityPicker
|
|
58
|
+
value={field.value}
|
|
59
|
+
onChange={field.onChange}
|
|
60
|
+
onEditSelected={(option) => onEditCategory(option.value)}
|
|
61
|
+
options={categoryOptions}
|
|
62
|
+
placeholder={t('form.fields.categories.selectPlaceholder')}
|
|
63
|
+
searchPlaceholder={t(
|
|
64
|
+
'form.fields.categories.searchPlaceholder'
|
|
65
|
+
)}
|
|
66
|
+
entityLabel="categoria"
|
|
67
|
+
emptyStateDescription={t('form.fields.categories.noResults')}
|
|
68
|
+
emptyHint="Nenhuma categoria vinculada ainda."
|
|
69
|
+
createActionLabel="Criar categoria"
|
|
70
|
+
createTitle="Criar categoria"
|
|
71
|
+
createDescription="Cadastre uma categoria rapidamente sem sair da edição do curso."
|
|
72
|
+
createFields={[
|
|
73
|
+
{
|
|
74
|
+
name: 'name',
|
|
75
|
+
label: 'Nome da categoria',
|
|
76
|
+
placeholder: 'Ex: Desenvolvimento Web',
|
|
77
|
+
required: true,
|
|
78
|
+
},
|
|
79
|
+
]}
|
|
80
|
+
mapSearchToCreateValues={(search) => ({
|
|
81
|
+
name: search,
|
|
82
|
+
})}
|
|
83
|
+
onCreate={onCreateCategory}
|
|
84
|
+
/>
|
|
85
|
+
<FormMessage />
|
|
86
|
+
</FormItem>
|
|
87
|
+
)}
|
|
88
|
+
/>
|
|
96
89
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
90
|
+
<FormField
|
|
91
|
+
control={form.control}
|
|
92
|
+
name="instrutores"
|
|
93
|
+
render={({ field }) => (
|
|
94
|
+
<FormItem className="self-start">
|
|
95
|
+
<FormLabel>{t('form.fields.instructors.label')}</FormLabel>
|
|
96
|
+
<CourseMultiEntityPicker
|
|
97
|
+
value={field.value}
|
|
98
|
+
onChange={field.onChange}
|
|
99
|
+
onEditSelected={(option) => onEditInstructor(option.value)}
|
|
100
|
+
options={instructorOptions}
|
|
101
|
+
placeholder={t('form.fields.instructors.selectPlaceholder')}
|
|
102
|
+
searchPlaceholder={t(
|
|
103
|
+
'form.fields.instructors.searchPlaceholder'
|
|
104
|
+
)}
|
|
105
|
+
entityLabel="instrutor"
|
|
106
|
+
emptyStateDescription={t('form.fields.instructors.noResults')}
|
|
107
|
+
emptyHint="Nenhum instrutor vinculado ainda."
|
|
108
|
+
createActionLabel="Cadastrar instrutor"
|
|
109
|
+
onCreateClick={onCreateInstructor}
|
|
110
|
+
variant="instructor"
|
|
111
|
+
renderOptionMeta
|
|
112
|
+
/>
|
|
113
|
+
<FormMessage />
|
|
114
|
+
</FormItem>
|
|
115
|
+
)}
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
123
118
|
|
|
124
119
|
<FormField
|
|
125
120
|
control={form.control}
|
|
@@ -128,10 +123,10 @@ export function CourseRelationsCard({
|
|
|
128
123
|
<FormItem>
|
|
129
124
|
<FormLabel>{t('form.fields.prerequisites.label')}</FormLabel>
|
|
130
125
|
<FormControl>
|
|
131
|
-
<
|
|
132
|
-
{
|
|
133
|
-
|
|
134
|
-
|
|
126
|
+
<RichTextEditor
|
|
127
|
+
value={field.value || ''}
|
|
128
|
+
onChange={field.onChange}
|
|
129
|
+
className="w-full max-w-full"
|
|
135
130
|
/>
|
|
136
131
|
</FormControl>
|
|
137
132
|
<FormMessage />
|