@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
|
3
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
4
4
|
import { Badge } from '@/components/ui/badge';
|
|
5
5
|
import { Button } from '@/components/ui/button';
|
|
6
6
|
import type { EntityPickerValue } from '@/components/ui/entity-picker';
|
|
@@ -19,11 +19,14 @@ import {
|
|
|
19
19
|
TableHeader,
|
|
20
20
|
TableRow,
|
|
21
21
|
} from '@/components/ui/table';
|
|
22
|
+
import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
|
|
23
|
+
import { getPhotoUrl } from '@/lib/get-photo-url';
|
|
22
24
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
23
|
-
import { Trash2 } from 'lucide-react';
|
|
25
|
+
import { Pencil, Trash2 } from 'lucide-react';
|
|
24
26
|
import { useTranslations } from 'next-intl';
|
|
25
27
|
import { useCallback, useState } from 'react';
|
|
26
28
|
import { toast } from 'sonner';
|
|
29
|
+
import { UserSheet } from '../../../core/users/user-sheet';
|
|
27
30
|
import { EnterpriseAdminCreateSheet } from './enterprise-admin-create-sheet';
|
|
28
31
|
import {
|
|
29
32
|
USER_ROLE_LABEL,
|
|
@@ -44,8 +47,6 @@ const ADMIN_ROLES: EnterpriseUserRole[] = [
|
|
|
44
47
|
'viewer',
|
|
45
48
|
];
|
|
46
49
|
|
|
47
|
-
const ADMINS_PAGE_SIZE_DEFAULT = 10;
|
|
48
|
-
|
|
49
50
|
export function AdministratorsTab({
|
|
50
51
|
enterpriseId,
|
|
51
52
|
onMutate,
|
|
@@ -57,13 +58,18 @@ export function AdministratorsTab({
|
|
|
57
58
|
const { request } = useApp();
|
|
58
59
|
const [search, setSearch] = useState('');
|
|
59
60
|
const [page, setPage] = useState(1);
|
|
60
|
-
const [pageSize, setPageSize] =
|
|
61
|
+
const [pageSize, setPageSize] = usePersistedPageSize({
|
|
62
|
+
storageKey: 'pagination:global:pageSize',
|
|
63
|
+
defaultValue: 6,
|
|
64
|
+
allowedValues: [6, 12, 24, 48],
|
|
65
|
+
});
|
|
61
66
|
const [pickerValue, setPickerValue] = useState<EntityPickerValue>(null);
|
|
62
67
|
const [selectedUser, setSelectedUser] =
|
|
63
68
|
useState<SystemUserPickerOption | null>(null);
|
|
64
69
|
const [roleForLink, setRoleForLink] =
|
|
65
70
|
useState<EnterpriseUserRole>('enterprise_admin');
|
|
66
71
|
const [createSheetOpen, setCreateSheetOpen] = useState(false);
|
|
72
|
+
const [editingUserId, setEditingUserId] = useState<number | null>(null);
|
|
67
73
|
const [savingRoleUserId, setSavingRoleUserId] = useState<number | null>(null);
|
|
68
74
|
const [removingUserId, setRemovingUserId] = useState<number | null>(null);
|
|
69
75
|
|
|
@@ -257,6 +263,7 @@ export function AdministratorsTab({
|
|
|
257
263
|
<Avatar
|
|
258
264
|
className={`size-7 shrink-0 rounded-full ${getAvatarColor(u.name).bg}`}
|
|
259
265
|
>
|
|
266
|
+
<AvatarImage src={getPhotoUrl(u.photoId)} />
|
|
260
267
|
<AvatarFallback
|
|
261
268
|
className={`text-xs font-semibold ${getAvatarColor(u.name).bg} ${getAvatarColor(u.name).text}`}
|
|
262
269
|
>
|
|
@@ -295,6 +302,14 @@ export function AdministratorsTab({
|
|
|
295
302
|
</Badge>
|
|
296
303
|
</TableCell>
|
|
297
304
|
<TableCell className="text-right">
|
|
305
|
+
<Button
|
|
306
|
+
variant="ghost"
|
|
307
|
+
size="icon"
|
|
308
|
+
className="h-7 w-7"
|
|
309
|
+
onClick={() => setEditingUserId(u.userId)}
|
|
310
|
+
>
|
|
311
|
+
<Pencil className="size-4" />
|
|
312
|
+
</Button>
|
|
298
313
|
<Button
|
|
299
314
|
variant="ghost"
|
|
300
315
|
size="icon"
|
|
@@ -329,6 +344,17 @@ export function AdministratorsTab({
|
|
|
329
344
|
onMutate?.();
|
|
330
345
|
}}
|
|
331
346
|
/>
|
|
347
|
+
<UserSheet
|
|
348
|
+
open={editingUserId !== null}
|
|
349
|
+
onOpenChange={(nextOpen) => {
|
|
350
|
+
if (!nextOpen) setEditingUserId(null);
|
|
351
|
+
}}
|
|
352
|
+
userId={editingUserId}
|
|
353
|
+
onSuccess={() => {
|
|
354
|
+
void refetch();
|
|
355
|
+
onMutate?.();
|
|
356
|
+
}}
|
|
357
|
+
/>
|
|
332
358
|
</>
|
|
333
359
|
);
|
|
334
360
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { CourseAvatar } from '@/app/(app)/(libraries)/lms/_components/course-avatar';
|
|
4
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
4
5
|
import { Badge } from '@/components/ui/badge';
|
|
5
6
|
import { Button } from '@/components/ui/button';
|
|
6
7
|
import {
|
|
@@ -18,13 +19,20 @@ import {
|
|
|
18
19
|
TableHeader,
|
|
19
20
|
TableRow,
|
|
20
21
|
} from '@/components/ui/table';
|
|
22
|
+
import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
|
|
21
23
|
import { formatDate } from '@/lib/format-date';
|
|
22
24
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
+
import {
|
|
26
|
+
CalendarDays,
|
|
27
|
+
Pencil,
|
|
28
|
+
SquareArrowOutUpRight,
|
|
29
|
+
Trash2,
|
|
30
|
+
} from 'lucide-react';
|
|
25
31
|
import { useTranslations } from 'next-intl';
|
|
32
|
+
import { useRouter } from 'next/navigation';
|
|
26
33
|
import { useCallback, useState } from 'react';
|
|
27
34
|
import { toast } from 'sonner';
|
|
35
|
+
import { ClassFormSheet } from '../../_components/class-form-sheet';
|
|
28
36
|
import { EnterpriseClassCreateSheet } from './enterprise-class-create-sheet';
|
|
29
37
|
import {
|
|
30
38
|
CLASS_STATUS_LABEL,
|
|
@@ -37,7 +45,13 @@ import type {
|
|
|
37
45
|
EnterpriseClassStatus,
|
|
38
46
|
} from './enterprise-types';
|
|
39
47
|
|
|
40
|
-
|
|
48
|
+
type EnterpriseClassListResponse = {
|
|
49
|
+
data: EnterpriseClass[];
|
|
50
|
+
total: number;
|
|
51
|
+
page: number;
|
|
52
|
+
pageSize: number;
|
|
53
|
+
lastPage: number;
|
|
54
|
+
};
|
|
41
55
|
|
|
42
56
|
async function loadClassGroupOptions(
|
|
43
57
|
request: ReturnType<typeof useApp>['request'],
|
|
@@ -101,10 +115,15 @@ export function ClassesTab({
|
|
|
101
115
|
'all' | EnterpriseClassStatus
|
|
102
116
|
>('all');
|
|
103
117
|
const [page, setPage] = useState(1);
|
|
104
|
-
const [pageSize, setPageSize] =
|
|
118
|
+
const [pageSize, setPageSize] = usePersistedPageSize({
|
|
119
|
+
storageKey: 'pagination:global:pageSize',
|
|
120
|
+
defaultValue: 6,
|
|
121
|
+
allowedValues: [6, 12, 24, 48],
|
|
122
|
+
});
|
|
105
123
|
const [pickerValue, setPickerValue] = useState<string | number | null>(null);
|
|
106
124
|
const [pickerClass, setPickerClass] = useState<EnterpriseClass | null>(null);
|
|
107
125
|
const [createSheetOpen, setCreateSheetOpen] = useState(false);
|
|
126
|
+
const [editingClassId, setEditingClassId] = useState<string | null>(null);
|
|
108
127
|
|
|
109
128
|
const loadPickerOptions = useCallback(
|
|
110
129
|
(args: { page: number; pageSize: number; search: string }) =>
|
|
@@ -129,11 +148,11 @@ export function ClassesTab({
|
|
|
129
148
|
statusFilter,
|
|
130
149
|
],
|
|
131
150
|
queryFn: async () => {
|
|
132
|
-
const res = await request<
|
|
151
|
+
const res = await request<EnterpriseClassListResponse>({
|
|
133
152
|
url: `/lms/enterprise/${enterpriseId}/classes?${queryParams}`,
|
|
134
153
|
method: 'GET',
|
|
135
154
|
});
|
|
136
|
-
return
|
|
155
|
+
return res.data;
|
|
137
156
|
},
|
|
138
157
|
});
|
|
139
158
|
|
|
@@ -267,9 +286,12 @@ export function ClassesTab({
|
|
|
267
286
|
<TableHeader>
|
|
268
287
|
<TableRow>
|
|
269
288
|
<TableHead>{t('table.class')}</TableHead>
|
|
289
|
+
<TableHead>{t('table.instructor')}</TableHead>
|
|
270
290
|
<TableHead>{t('table.status')}</TableHead>
|
|
271
291
|
<TableHead>{t('table.period')}</TableHead>
|
|
272
|
-
<TableHead className="text-center">
|
|
292
|
+
<TableHead className="text-center">
|
|
293
|
+
{t('table.capacity')}
|
|
294
|
+
</TableHead>
|
|
273
295
|
<TableHead className="w-10" />
|
|
274
296
|
</TableRow>
|
|
275
297
|
</TableHeader>
|
|
@@ -286,15 +308,12 @@ export function ClassesTab({
|
|
|
286
308
|
>
|
|
287
309
|
<TableCell>
|
|
288
310
|
<div className="flex items-center gap-3">
|
|
289
|
-
<
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
{getInitials(cls.courseTitle ?? cls.code ?? '')}
|
|
296
|
-
</AvatarFallback>
|
|
297
|
-
</Avatar>
|
|
311
|
+
<CourseAvatar
|
|
312
|
+
fileId={cls.logoFileId}
|
|
313
|
+
title={cls.courseTitle ?? cls.code ?? 'Curso'}
|
|
314
|
+
className="size-8 rounded-lg"
|
|
315
|
+
iconSize="size-4"
|
|
316
|
+
/>
|
|
298
317
|
<div className="min-w-0">
|
|
299
318
|
<p className="font-mono text-sm">
|
|
300
319
|
{cls.code ?? cls.title ?? '—'}
|
|
@@ -305,6 +324,24 @@ export function ClassesTab({
|
|
|
305
324
|
</div>
|
|
306
325
|
</div>
|
|
307
326
|
</TableCell>
|
|
327
|
+
<TableCell>
|
|
328
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
329
|
+
<Avatar className="h-7 w-7 shrink-0 border">
|
|
330
|
+
{cls.instructorAvatarId ? (
|
|
331
|
+
<AvatarImage
|
|
332
|
+
src={`${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${cls.instructorAvatarId}`}
|
|
333
|
+
alt={cls.instructorName ?? 'Instructor'}
|
|
334
|
+
/>
|
|
335
|
+
) : null}
|
|
336
|
+
<AvatarFallback className="text-[10px]">
|
|
337
|
+
{getInitials(cls.instructorName ?? '') || '—'}
|
|
338
|
+
</AvatarFallback>
|
|
339
|
+
</Avatar>
|
|
340
|
+
<span className="truncate text-sm">
|
|
341
|
+
{cls.instructorName ?? '—'}
|
|
342
|
+
</span>
|
|
343
|
+
</div>
|
|
344
|
+
</TableCell>
|
|
308
345
|
<TableCell>
|
|
309
346
|
<Badge
|
|
310
347
|
variant={
|
|
@@ -336,14 +373,24 @@ export function ClassesTab({
|
|
|
336
373
|
: '—'}
|
|
337
374
|
</TableCell>
|
|
338
375
|
<TableCell className="text-center text-sm">
|
|
339
|
-
{cls.capacity ??
|
|
340
|
-
<span className="text-muted-foreground">—</span>
|
|
341
|
-
)}
|
|
376
|
+
{cls.enrolledCount} / {cls.capacity ?? '—'}
|
|
342
377
|
</TableCell>
|
|
343
378
|
<TableCell
|
|
344
379
|
className="flex items-center gap-1"
|
|
345
380
|
onClick={(e) => e.stopPropagation()}
|
|
346
381
|
>
|
|
382
|
+
<Button
|
|
383
|
+
variant="ghost"
|
|
384
|
+
size="icon"
|
|
385
|
+
className="h-8 w-8"
|
|
386
|
+
onClick={() =>
|
|
387
|
+
setEditingClassId(
|
|
388
|
+
String(cls.courseClassGroupId ?? cls.id)
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
>
|
|
392
|
+
<Pencil className="h-4 w-4" />
|
|
393
|
+
</Button>
|
|
347
394
|
<Button
|
|
348
395
|
variant="ghost"
|
|
349
396
|
size="icon"
|
|
@@ -387,6 +434,18 @@ export function ClassesTab({
|
|
|
387
434
|
onMutate?.();
|
|
388
435
|
}}
|
|
389
436
|
/>
|
|
437
|
+
<ClassFormSheet
|
|
438
|
+
open={editingClassId !== null}
|
|
439
|
+
onOpenChange={(nextOpen) => {
|
|
440
|
+
if (!nextOpen) setEditingClassId(null);
|
|
441
|
+
}}
|
|
442
|
+
classId={editingClassId ?? undefined}
|
|
443
|
+
sheetTitle={t('actions.edit')}
|
|
444
|
+
onSuccess={() => {
|
|
445
|
+
void refetch();
|
|
446
|
+
onMutate?.();
|
|
447
|
+
}}
|
|
448
|
+
/>
|
|
390
449
|
</>
|
|
391
450
|
);
|
|
392
451
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
|
3
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
4
4
|
import { Badge } from '@/components/ui/badge';
|
|
5
5
|
import { Card, CardContent } from '@/components/ui/card';
|
|
6
6
|
import { formatDate } from '@/lib/format-date';
|
|
@@ -22,16 +22,25 @@ export function CompanyIdentityCard({
|
|
|
22
22
|
const { currentLocaleCode, getSettingValue } = useApp();
|
|
23
23
|
const t = useTranslations('lms.EnterpriseDetailPage');
|
|
24
24
|
|
|
25
|
+
function getPersonAvatarUrl(avatarId?: number | null) {
|
|
26
|
+
return typeof avatarId === 'number' && avatarId > 0
|
|
27
|
+
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${avatarId}`
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
return (
|
|
26
32
|
<Card
|
|
27
33
|
className="cursor-pointer border-border/60 transition-all hover:border-primary/40 hover:shadow-md"
|
|
28
34
|
onClick={onEditClick}
|
|
29
35
|
title={t('identityCard.editTooltip')}
|
|
30
36
|
>
|
|
31
|
-
<CardContent className="
|
|
37
|
+
<CardContent className="px-5">
|
|
32
38
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-start">
|
|
33
39
|
{/* Avatar */}
|
|
34
40
|
<Avatar className={`size-16 shrink-0 rounded-2xl ${bg}`}>
|
|
41
|
+
<AvatarImage
|
|
42
|
+
src={getPersonAvatarUrl(account.crmAccount?.avatarId ?? null)}
|
|
43
|
+
/>
|
|
35
44
|
<AvatarFallback
|
|
36
45
|
className={`rounded-2xl text-xl font-bold ${bg} ${text}`}
|
|
37
46
|
>
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
4
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
5
|
+
import { useTranslations } from 'next-intl';
|
|
6
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
7
|
+
import { useForm } from 'react-hook-form';
|
|
8
|
+
import { toast } from 'sonner';
|
|
9
|
+
import {
|
|
10
|
+
CourseFormSheet,
|
|
11
|
+
DEFAULT_COURSE_FORM_VALUES,
|
|
12
|
+
getCourseSheetSchema,
|
|
13
|
+
type CourseCategoryOption,
|
|
14
|
+
type CourseSheetFormValues,
|
|
15
|
+
} from '../../_components/course-form-sheet';
|
|
16
|
+
|
|
17
|
+
type ApiCategory = {
|
|
18
|
+
id: number;
|
|
19
|
+
slug: string;
|
|
20
|
+
name: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type ApiCategoryList = {
|
|
24
|
+
data: ApiCategory[];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type ApiCourse = {
|
|
28
|
+
id: number;
|
|
29
|
+
name: string;
|
|
30
|
+
slug: string;
|
|
31
|
+
title: string;
|
|
32
|
+
description?: string | null;
|
|
33
|
+
level?: string | null;
|
|
34
|
+
status?: string | null;
|
|
35
|
+
categories?: string[];
|
|
36
|
+
primaryColor?: string | null;
|
|
37
|
+
secondaryColor?: string | null;
|
|
38
|
+
logoFileId?: number | null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export interface EnterpriseCourseEditSheetProps {
|
|
42
|
+
open: boolean;
|
|
43
|
+
onOpenChange: (open: boolean) => void;
|
|
44
|
+
courseId: number | null;
|
|
45
|
+
onSaved?: () => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toPtLevel(level?: string | null): CourseSheetFormValues['nivel'] {
|
|
49
|
+
if (level === 'beginner') return 'iniciante';
|
|
50
|
+
if (level === 'intermediate') return 'intermediario';
|
|
51
|
+
if (level === 'advanced') return 'avancado';
|
|
52
|
+
return 'iniciante';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function toPtStatus(status?: string | null): CourseSheetFormValues['status'] {
|
|
56
|
+
if (status === 'published' || status === 'active') return 'ativo';
|
|
57
|
+
if (status === 'draft') return 'rascunho';
|
|
58
|
+
if (status === 'archived') return 'arquivado';
|
|
59
|
+
return 'rascunho';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function toApiLevel(level: CourseSheetFormValues['nivel']) {
|
|
63
|
+
if (level === 'iniciante') return 'beginner';
|
|
64
|
+
if (level === 'intermediario') return 'intermediate';
|
|
65
|
+
return 'advanced';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toApiStatus(status: CourseSheetFormValues['status']) {
|
|
69
|
+
if (status === 'ativo') return 'published';
|
|
70
|
+
if (status === 'rascunho') return 'draft';
|
|
71
|
+
return 'archived';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getContrastColor(hex: string) {
|
|
75
|
+
const cleaned = hex.replace('#', '');
|
|
76
|
+
if (cleaned.length !== 6) return '#FFFFFF';
|
|
77
|
+
|
|
78
|
+
const r = parseInt(cleaned.slice(0, 2), 16);
|
|
79
|
+
const g = parseInt(cleaned.slice(2, 4), 16);
|
|
80
|
+
const b = parseInt(cleaned.slice(4, 6), 16);
|
|
81
|
+
|
|
82
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
83
|
+
return luminance > 0.6 ? '#111827' : '#FFFFFF';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function EnterpriseCourseEditSheet({
|
|
87
|
+
open,
|
|
88
|
+
onOpenChange,
|
|
89
|
+
courseId,
|
|
90
|
+
onSaved,
|
|
91
|
+
}: EnterpriseCourseEditSheetProps) {
|
|
92
|
+
const t = useTranslations('lms.CoursesPage');
|
|
93
|
+
const { request } = useApp();
|
|
94
|
+
const [saving, setSaving] = useState(false);
|
|
95
|
+
|
|
96
|
+
const form = useForm<CourseSheetFormValues>({
|
|
97
|
+
resolver: zodResolver(getCourseSheetSchema(t)),
|
|
98
|
+
defaultValues: DEFAULT_COURSE_FORM_VALUES,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const { data: categoriesData } = useQuery<ApiCategoryList>({
|
|
102
|
+
queryKey: ['enterprise-course-edit-categories'],
|
|
103
|
+
queryFn: async () => {
|
|
104
|
+
const response = await request<ApiCategoryList>({
|
|
105
|
+
url: '/category?page=1&pageSize=500&status=all',
|
|
106
|
+
method: 'GET',
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return response.data;
|
|
110
|
+
},
|
|
111
|
+
placeholderData: (previous) => previous,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (!open || typeof courseId !== 'number') return;
|
|
116
|
+
|
|
117
|
+
request<ApiCourse>({
|
|
118
|
+
url: `/lms/courses/${courseId}`,
|
|
119
|
+
method: 'GET',
|
|
120
|
+
})
|
|
121
|
+
.then((response) => {
|
|
122
|
+
const course = response.data;
|
|
123
|
+
form.reset({
|
|
124
|
+
nomeInterno: course.name ?? '',
|
|
125
|
+
slug: course.slug ?? '',
|
|
126
|
+
tituloComercial: course.title ?? '',
|
|
127
|
+
descricao: course.description ?? '',
|
|
128
|
+
nivel: toPtLevel(course.level),
|
|
129
|
+
status: toPtStatus(course.status),
|
|
130
|
+
categorias: course.categories ?? [],
|
|
131
|
+
primaryColor: course.primaryColor ?? '#1D4ED8',
|
|
132
|
+
secondaryColor: course.secondaryColor ?? '#111827',
|
|
133
|
+
logoFileId: course.logoFileId ?? null,
|
|
134
|
+
});
|
|
135
|
+
})
|
|
136
|
+
.catch(() => {
|
|
137
|
+
toast.error(t('toasts.courseUpdateError'));
|
|
138
|
+
onOpenChange(false);
|
|
139
|
+
});
|
|
140
|
+
}, [courseId, form, onOpenChange, open, request, t]);
|
|
141
|
+
|
|
142
|
+
const categoryOptions = useMemo<CourseCategoryOption[]>(
|
|
143
|
+
() =>
|
|
144
|
+
(categoriesData?.data ?? [])
|
|
145
|
+
.filter((category) => !!category.slug)
|
|
146
|
+
.map((category) => ({
|
|
147
|
+
value: category.slug,
|
|
148
|
+
label: category.name || category.slug,
|
|
149
|
+
}))
|
|
150
|
+
.sort((a, b) => a.label.localeCompare(b.label)),
|
|
151
|
+
[categoriesData]
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
async function onSubmit(values: CourseSheetFormValues) {
|
|
155
|
+
if (typeof courseId !== 'number') return;
|
|
156
|
+
|
|
157
|
+
setSaving(true);
|
|
158
|
+
try {
|
|
159
|
+
await request({
|
|
160
|
+
url: `/lms/courses/${courseId}`,
|
|
161
|
+
method: 'PATCH',
|
|
162
|
+
data: {
|
|
163
|
+
name: values.nomeInterno.trim(),
|
|
164
|
+
slug: values.slug.trim().toLowerCase(),
|
|
165
|
+
title: values.tituloComercial,
|
|
166
|
+
description: values.descricao,
|
|
167
|
+
level: toApiLevel(values.nivel),
|
|
168
|
+
status: toApiStatus(values.status),
|
|
169
|
+
categorySlugs: values.categorias,
|
|
170
|
+
primaryColor: values.primaryColor,
|
|
171
|
+
primaryContrastColor: getContrastColor(values.primaryColor),
|
|
172
|
+
secondaryColor: values.secondaryColor,
|
|
173
|
+
secondaryContrastColor: getContrastColor(values.secondaryColor),
|
|
174
|
+
logoFileId: values.logoFileId ?? null,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
toast.success(t('toasts.courseUpdated'));
|
|
179
|
+
onSaved?.();
|
|
180
|
+
onOpenChange(false);
|
|
181
|
+
} catch {
|
|
182
|
+
toast.error(t('toasts.courseUpdateError'));
|
|
183
|
+
} finally {
|
|
184
|
+
setSaving(false);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<CourseFormSheet
|
|
190
|
+
open={open}
|
|
191
|
+
onOpenChange={onOpenChange}
|
|
192
|
+
editing={true}
|
|
193
|
+
saving={saving}
|
|
194
|
+
form={form}
|
|
195
|
+
onSubmit={onSubmit}
|
|
196
|
+
categories={categoryOptions}
|
|
197
|
+
initialLogoFileId={form.watch('logoFileId') ?? null}
|
|
198
|
+
t={t}
|
|
199
|
+
/>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { CourseAvatar } from '@/app/(app)/(libraries)/lms/_components/course-avatar';
|
|
3
4
|
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
|
4
5
|
import { Badge } from '@/components/ui/badge';
|
|
5
6
|
import { Button } from '@/components/ui/button';
|
|
@@ -18,14 +19,16 @@ import {
|
|
|
18
19
|
TableHeader,
|
|
19
20
|
TableRow,
|
|
20
21
|
} from '@/components/ui/table';
|
|
22
|
+
import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
|
|
21
23
|
import { formatDate } from '@/lib/format-date';
|
|
22
24
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
23
|
-
import { BookOpen, SquareArrowOutUpRight, Trash2 } from 'lucide-react';
|
|
24
|
-
import { useRouter } from 'next/navigation';
|
|
25
|
+
import { BookOpen, Pencil, SquareArrowOutUpRight, Trash2 } from 'lucide-react';
|
|
25
26
|
import { useTranslations } from 'next-intl';
|
|
27
|
+
import { useRouter } from 'next/navigation';
|
|
26
28
|
import { useCallback, useState } from 'react';
|
|
27
29
|
import { toast } from 'sonner';
|
|
28
30
|
import { EnterpriseCourseCreateSheet } from './enterprise-course-create-sheet';
|
|
31
|
+
import { EnterpriseCourseEditSheet } from './enterprise-course-edit-sheet';
|
|
29
32
|
import {
|
|
30
33
|
COURSE_STATUS_LABEL,
|
|
31
34
|
COURSE_STATUS_VARIANT,
|
|
@@ -37,7 +40,13 @@ import type {
|
|
|
37
40
|
EnterpriseCourseStatus,
|
|
38
41
|
} from './enterprise-types';
|
|
39
42
|
|
|
40
|
-
|
|
43
|
+
type EnterpriseCourseListResponse = {
|
|
44
|
+
data: EnterpriseCourse[];
|
|
45
|
+
total: number;
|
|
46
|
+
page: number;
|
|
47
|
+
pageSize: number;
|
|
48
|
+
lastPage: number;
|
|
49
|
+
};
|
|
41
50
|
|
|
42
51
|
async function loadCourseOptions(
|
|
43
52
|
request: ReturnType<typeof useApp>['request'],
|
|
@@ -53,6 +62,7 @@ async function loadCourseOptions(
|
|
|
53
62
|
id: number;
|
|
54
63
|
title: string;
|
|
55
64
|
slug: string | null;
|
|
65
|
+
logoFileId?: number | null;
|
|
56
66
|
status: string;
|
|
57
67
|
level: string | null;
|
|
58
68
|
offering_type: string | null;
|
|
@@ -70,6 +80,7 @@ async function loadCourseOptions(
|
|
|
70
80
|
courseId: c.id,
|
|
71
81
|
title: c.title ?? '',
|
|
72
82
|
slug: c.slug ?? null,
|
|
83
|
+
logoFileId: c.logoFileId ?? null,
|
|
73
84
|
status: c.status ?? 'draft',
|
|
74
85
|
level: c.level ?? null,
|
|
75
86
|
modality: c.offering_type ?? null,
|
|
@@ -97,12 +108,17 @@ export function CoursesTab({
|
|
|
97
108
|
'all' | EnterpriseCourseStatus
|
|
98
109
|
>('all');
|
|
99
110
|
const [page, setPage] = useState(1);
|
|
100
|
-
const [pageSize, setPageSize] =
|
|
111
|
+
const [pageSize, setPageSize] = usePersistedPageSize({
|
|
112
|
+
storageKey: 'pagination:global:pageSize',
|
|
113
|
+
defaultValue: 6,
|
|
114
|
+
allowedValues: [6, 12, 24, 48],
|
|
115
|
+
});
|
|
101
116
|
const [pickerValue, setPickerValue] = useState<string | number | null>(null);
|
|
102
117
|
const [pickerCourse, setPickerCourse] = useState<EnterpriseCourse | null>(
|
|
103
118
|
null
|
|
104
119
|
);
|
|
105
120
|
const [createSheetOpen, setCreateSheetOpen] = useState(false);
|
|
121
|
+
const [editingCourseId, setEditingCourseId] = useState<number | null>(null);
|
|
106
122
|
|
|
107
123
|
const loadPickerOptions = useCallback(
|
|
108
124
|
(args: { page: number; pageSize: number; search: string }) =>
|
|
@@ -127,11 +143,11 @@ export function CoursesTab({
|
|
|
127
143
|
statusFilter,
|
|
128
144
|
],
|
|
129
145
|
queryFn: async () => {
|
|
130
|
-
const res = await request<
|
|
146
|
+
const res = await request<EnterpriseCourseListResponse>({
|
|
131
147
|
url: `/lms/enterprise/${enterpriseId}/courses?${queryParams}`,
|
|
132
148
|
method: 'GET',
|
|
133
149
|
});
|
|
134
|
-
return
|
|
150
|
+
return res.data;
|
|
135
151
|
},
|
|
136
152
|
});
|
|
137
153
|
|
|
@@ -210,15 +226,12 @@ export function CoursesTab({
|
|
|
210
226
|
getPickerOptionDescription={(c) => c.modality ?? undefined}
|
|
211
227
|
renderPickerOption={({ option: c }) => (
|
|
212
228
|
<div className="flex items-center gap-2 min-w-0">
|
|
213
|
-
<
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
{getInitials(c.title)}
|
|
220
|
-
</AvatarFallback>
|
|
221
|
-
</Avatar>
|
|
229
|
+
<CourseAvatar
|
|
230
|
+
fileId={c.logoFileId}
|
|
231
|
+
title={c.title}
|
|
232
|
+
className="size-7 shrink-0 rounded-lg"
|
|
233
|
+
iconSize="size-3.5"
|
|
234
|
+
/>
|
|
222
235
|
<div className="min-w-0">
|
|
223
236
|
<div className="truncate font-medium">{c.title}</div>
|
|
224
237
|
{c.modality && (
|
|
@@ -278,15 +291,12 @@ export function CoursesTab({
|
|
|
278
291
|
>
|
|
279
292
|
<TableCell>
|
|
280
293
|
<div className="flex items-center gap-3">
|
|
281
|
-
<
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
{getInitials(course.title)}
|
|
288
|
-
</AvatarFallback>
|
|
289
|
-
</Avatar>
|
|
294
|
+
<CourseAvatar
|
|
295
|
+
fileId={course.logoFileId}
|
|
296
|
+
title={course.title}
|
|
297
|
+
className="size-8 rounded-lg"
|
|
298
|
+
iconSize="size-4"
|
|
299
|
+
/>
|
|
290
300
|
<p className="font-medium">{course.title}</p>
|
|
291
301
|
</div>
|
|
292
302
|
</TableCell>
|
|
@@ -319,6 +329,16 @@ export function CoursesTab({
|
|
|
319
329
|
className="flex items-center gap-1"
|
|
320
330
|
onClick={(e) => e.stopPropagation()}
|
|
321
331
|
>
|
|
332
|
+
<Button
|
|
333
|
+
variant="ghost"
|
|
334
|
+
size="icon"
|
|
335
|
+
className="h-8 w-8"
|
|
336
|
+
onClick={() =>
|
|
337
|
+
setEditingCourseId(course.courseId ?? course.id)
|
|
338
|
+
}
|
|
339
|
+
>
|
|
340
|
+
<Pencil className="h-4 w-4" />
|
|
341
|
+
</Button>
|
|
322
342
|
<Button
|
|
323
343
|
variant="ghost"
|
|
324
344
|
size="icon"
|
|
@@ -360,6 +380,17 @@ export function CoursesTab({
|
|
|
360
380
|
onMutate?.();
|
|
361
381
|
}}
|
|
362
382
|
/>
|
|
383
|
+
<EnterpriseCourseEditSheet
|
|
384
|
+
open={editingCourseId !== null}
|
|
385
|
+
onOpenChange={(nextOpen) => {
|
|
386
|
+
if (!nextOpen) setEditingCourseId(null);
|
|
387
|
+
}}
|
|
388
|
+
courseId={editingCourseId}
|
|
389
|
+
onSaved={() => {
|
|
390
|
+
void refetch();
|
|
391
|
+
onMutate?.();
|
|
392
|
+
}}
|
|
393
|
+
/>
|
|
363
394
|
</>
|
|
364
395
|
);
|
|
365
396
|
}
|