@hed-hog/lms 0.0.331 → 0.0.338
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/class-group/class-group.controller.d.ts +3 -3
- package/dist/class-group/class-group.service.d.ts +3 -3
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +12 -20
- package/dist/course/course.service.js.map +1 -1
- package/dist/enterprise/enterprise.controller.d.ts +72 -0
- package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
- package/dist/enterprise/enterprise.controller.js +10 -0
- package/dist/enterprise/enterprise.controller.js.map +1 -1
- package/dist/enterprise/enterprise.service.d.ts +78 -0
- package/dist/enterprise/enterprise.service.d.ts.map +1 -1
- package/dist/enterprise/enterprise.service.js +413 -40
- package/dist/enterprise/enterprise.service.js.map +1 -1
- package/dist/enterprise/training/training-admin.controller.d.ts +6 -3
- package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
- package/dist/enterprise/training/training-admin.controller.js +10 -6
- package/dist/enterprise/training/training-admin.controller.js.map +1 -1
- package/dist/enterprise/training/training-admin.service.d.ts +8 -2
- package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
- package/dist/enterprise/training/training-admin.service.js +108 -52
- package/dist/enterprise/training/training-admin.service.js.map +1 -1
- package/dist/enterprise/training/training-viewer.controller.d.ts +3 -0
- package/dist/enterprise/training/training-viewer.controller.d.ts.map +1 -1
- package/dist/evaluation/evaluation.controller.d.ts +4 -4
- package/dist/evaluation/evaluation.service.d.ts +4 -4
- package/dist/instructor/dto/create-instructor-skill.dto.d.ts +0 -4
- package/dist/instructor/dto/create-instructor-skill.dto.d.ts.map +1 -1
- package/dist/instructor/dto/create-instructor-skill.dto.js +0 -21
- package/dist/instructor/dto/create-instructor-skill.dto.js.map +1 -1
- package/dist/instructor/dto/update-instructor-skill.dto.d.ts +0 -4
- package/dist/instructor/dto/update-instructor-skill.dto.d.ts.map +1 -1
- package/dist/instructor/dto/update-instructor-skill.dto.js +0 -22
- package/dist/instructor/dto/update-instructor-skill.dto.js.map +1 -1
- package/dist/instructor/instructor-skill.controller.d.ts +4 -4
- package/dist/instructor/instructor-skill.service.d.ts +4 -7
- package/dist/instructor/instructor-skill.service.d.ts.map +1 -1
- package/dist/instructor/instructor-skill.service.js +2 -89
- package/dist/instructor/instructor-skill.service.js.map +1 -1
- package/dist/instructor/instructor.controller.d.ts +20 -0
- package/dist/instructor/instructor.controller.d.ts.map +1 -1
- package/dist/instructor/instructor.controller.js +19 -0
- package/dist/instructor/instructor.controller.js.map +1 -1
- package/dist/instructor/instructor.service.d.ts +25 -0
- package/dist/instructor/instructor.service.d.ts.map +1 -1
- package/dist/instructor/instructor.service.js +70 -18
- package/dist/instructor/instructor.service.js.map +1 -1
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js.map +1 -1
- package/hedhog/data/route.yaml +23 -1
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +42 -24
- package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +3 -3
- package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +591 -0
- package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/classes/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +3 -33
- package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +9 -9
- package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +109 -0
- package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +40 -13
- package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +76 -81
- package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +60 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-scheduled-classes-tab.tsx.ejs +406 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +242 -33
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -0
- package/hedhog/frontend/app/courses/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/enterprise/_components/enterprise-activity-timeline.tsx.ejs +87 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +4 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +31 -5
- package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +79 -20
- package/hedhog/frontend/app/enterprise/_components/enterprise-company-identity-card.tsx.ejs +11 -2
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +201 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +55 -24
- package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +430 -296
- package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +277 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-overview-analytics.tsx.ejs +205 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-person-edit-sheet.tsx.ejs +97 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +82 -57
- package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +4 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +60 -22
- package/hedhog/frontend/app/enterprise/_components/enterprise-types.ts.ejs +54 -0
- package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +211 -0
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -7
- package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/exams/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +51 -104
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +625 -366
- package/hedhog/frontend/app/instructors/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/paths/page.tsx.ejs +9 -4
- package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/training/page.tsx.ejs +9 -4
- package/hedhog/frontend/messages/en.json +101 -10
- package/hedhog/frontend/messages/pt.json +101 -10
- package/hedhog/table/enterprise_student_license_event.yaml +30 -0
- package/hedhog/table/instructor_skill.yaml +0 -11
- package/package.json +7 -7
- package/src/course/course.service.ts +12 -24
- package/src/enterprise/enterprise.controller.ts +5 -0
- package/src/enterprise/enterprise.service.ts +507 -29
- package/src/enterprise/training/training-admin.controller.ts +4 -0
- package/src/enterprise/training/training-admin.service.ts +115 -51
- package/src/instructor/dto/create-instructor-skill.dto.ts +0 -17
- package/src/instructor/dto/update-instructor-skill.dto.ts +0 -18
- package/src/instructor/instructor-skill.service.ts +2 -97
- package/src/instructor/instructor.controller.ts +16 -0
- package/src/instructor/instructor.service.ts +85 -10
- package/src/lms.module.ts +1 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { EntityPicker } from '@/components/ui/entity-picker';
|
|
4
3
|
import {
|
|
5
4
|
Form,
|
|
6
5
|
FormControl,
|
|
@@ -31,10 +30,12 @@ import { Textarea } from '@/components/ui/textarea';
|
|
|
31
30
|
import { useApp } from '@hed-hog/next-app-provider';
|
|
32
31
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
33
32
|
import { useTranslations } from 'next-intl';
|
|
34
|
-
import { useEffect } from 'react';
|
|
33
|
+
import { useEffect, useState } from 'react';
|
|
35
34
|
import { useForm } from 'react-hook-form';
|
|
36
35
|
import { toast } from 'sonner';
|
|
37
36
|
import { z } from 'zod';
|
|
37
|
+
import { PersonPicker } from '../../../contact/_components/person-picker';
|
|
38
|
+
import { EnterprisePersonEditSheet } from './enterprise-person-edit-sheet';
|
|
38
39
|
import type { EnterpriseAccount } from './enterprise-types';
|
|
39
40
|
|
|
40
41
|
// ── Schema ─────────────────────────────────────────────────────────────────────
|
|
@@ -88,6 +89,9 @@ export function EnterpriseSheet({
|
|
|
88
89
|
const isEditing = Boolean(editingAccount);
|
|
89
90
|
const { request } = useApp();
|
|
90
91
|
const t = useTranslations('lms.EnterprisePage.sheet');
|
|
92
|
+
const [personSheetOpen, setPersonSheetOpen] = useState(false);
|
|
93
|
+
const [personEditId, setPersonEditId] = useState<number | null>(null);
|
|
94
|
+
const [selectedCompanyLabel, setSelectedCompanyLabel] = useState('');
|
|
91
95
|
|
|
92
96
|
const form = useForm<EnterpriseFormValues>({
|
|
93
97
|
resolver: zodResolver(enterpriseSchema),
|
|
@@ -116,6 +120,11 @@ export function EnterpriseSheet({
|
|
|
116
120
|
licenseLimit: editingAccount.licenseLimit ?? null,
|
|
117
121
|
notes: editingAccount.notes ?? '',
|
|
118
122
|
});
|
|
123
|
+
setSelectedCompanyLabel(
|
|
124
|
+
editingAccount.crmAccount?.tradeName ??
|
|
125
|
+
editingAccount.crmAccountName ??
|
|
126
|
+
''
|
|
127
|
+
);
|
|
119
128
|
} else {
|
|
120
129
|
form.reset({
|
|
121
130
|
name: '',
|
|
@@ -126,6 +135,7 @@ export function EnterpriseSheet({
|
|
|
126
135
|
licenseLimit: null,
|
|
127
136
|
notes: '',
|
|
128
137
|
});
|
|
138
|
+
setSelectedCompanyLabel('');
|
|
129
139
|
}
|
|
130
140
|
}, [open, editingAccount, form]);
|
|
131
141
|
|
|
@@ -200,7 +210,10 @@ export function EnterpriseSheet({
|
|
|
200
210
|
<FormItem>
|
|
201
211
|
<FormLabel>{t('fields.name')}</FormLabel>
|
|
202
212
|
<FormControl>
|
|
203
|
-
<Input
|
|
213
|
+
<Input
|
|
214
|
+
placeholder={t('fields.namePlaceholder')}
|
|
215
|
+
{...field}
|
|
216
|
+
/>
|
|
204
217
|
</FormControl>
|
|
205
218
|
<FormMessage />
|
|
206
219
|
</FormItem>
|
|
@@ -215,7 +228,10 @@ export function EnterpriseSheet({
|
|
|
215
228
|
<FormItem>
|
|
216
229
|
<FormLabel>{t('fields.slug')}</FormLabel>
|
|
217
230
|
<FormControl>
|
|
218
|
-
<Input
|
|
231
|
+
<Input
|
|
232
|
+
placeholder={t('fields.slugPlaceholder')}
|
|
233
|
+
{...field}
|
|
234
|
+
/>
|
|
219
235
|
</FormControl>
|
|
220
236
|
<FormDescription>
|
|
221
237
|
{t('fields.slugDescription')}
|
|
@@ -238,10 +254,18 @@ export function EnterpriseSheet({
|
|
|
238
254
|
<SelectValue />
|
|
239
255
|
</SelectTrigger>
|
|
240
256
|
<SelectContent>
|
|
241
|
-
<SelectItem value="active">
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
<SelectItem value="
|
|
257
|
+
<SelectItem value="active">
|
|
258
|
+
{t('status.active')}
|
|
259
|
+
</SelectItem>
|
|
260
|
+
<SelectItem value="trial">
|
|
261
|
+
{t('status.trial')}
|
|
262
|
+
</SelectItem>
|
|
263
|
+
<SelectItem value="inactive">
|
|
264
|
+
{t('status.inactive')}
|
|
265
|
+
</SelectItem>
|
|
266
|
+
<SelectItem value="suspended">
|
|
267
|
+
{t('status.suspended')}
|
|
268
|
+
</SelectItem>
|
|
245
269
|
</SelectContent>
|
|
246
270
|
</Select>
|
|
247
271
|
</FormControl>
|
|
@@ -250,60 +274,33 @@ export function EnterpriseSheet({
|
|
|
250
274
|
)}
|
|
251
275
|
/>
|
|
252
276
|
|
|
253
|
-
|
|
254
|
-
<EntityPicker
|
|
255
|
-
form={form}
|
|
256
|
-
name="crmAccountId"
|
|
257
|
-
onChange={(val) =>
|
|
258
|
-
form.setValue('crmAccountId', val as number | null, {
|
|
259
|
-
shouldValidate: true,
|
|
260
|
-
})
|
|
261
|
-
}
|
|
277
|
+
<PersonPicker
|
|
262
278
|
label={t('fields.company')}
|
|
263
|
-
placeholder={t('fields.companyPlaceholder')}
|
|
264
|
-
searchPlaceholder={t('fields.companySearchPlaceholder')}
|
|
265
|
-
emptyLabel={t('fields.companyEmpty')}
|
|
266
279
|
entityLabel={t('fields.companyEntity')}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
280
|
+
value={form.watch('crmAccountId')}
|
|
281
|
+
initialSelectedLabel={selectedCompanyLabel}
|
|
282
|
+
onChange={(personId, personName) => {
|
|
283
|
+
form.setValue('crmAccountId', personId, {
|
|
284
|
+
shouldValidate: true,
|
|
285
|
+
shouldDirty: true,
|
|
273
286
|
});
|
|
274
|
-
|
|
275
|
-
const res = await request<{
|
|
276
|
-
data: Array<{ id: number; name: string }>;
|
|
277
|
-
total: number;
|
|
278
|
-
lastPage?: number;
|
|
279
|
-
}>({ url: `/person/accounts?${params}`, method: 'GET' });
|
|
280
|
-
return {
|
|
281
|
-
items: res.data.data ?? [],
|
|
282
|
-
hasMore: page < (res.data.lastPage ?? 1),
|
|
283
|
-
};
|
|
287
|
+
setSelectedCompanyLabel(personName ?? '');
|
|
284
288
|
}}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
{
|
|
289
|
-
name: 'name',
|
|
290
|
-
label: t('fields.companyCreateName'),
|
|
291
|
-
placeholder: t('fields.companyCreateNamePlaceholder'),
|
|
292
|
-
required: true,
|
|
293
|
-
},
|
|
294
|
-
]}
|
|
295
|
-
mapSearchToCreateValues={(search) => ({ name: search })}
|
|
296
|
-
onCreate={async (values) => {
|
|
297
|
-
const res = await request<{ id: number; name: string }>({
|
|
298
|
-
url: '/person/accounts',
|
|
299
|
-
method: 'POST',
|
|
300
|
-
data: {
|
|
301
|
-
name: (values.name ?? '').trim(),
|
|
302
|
-
status: 'active',
|
|
303
|
-
},
|
|
304
|
-
});
|
|
305
|
-
return res.data as unknown as { id: number; name: string };
|
|
289
|
+
onEditSelection={(personId) => {
|
|
290
|
+
setPersonEditId(personId);
|
|
291
|
+
setPersonSheetOpen(true);
|
|
306
292
|
}}
|
|
293
|
+
onCreateNew={() => {
|
|
294
|
+
setPersonEditId(null);
|
|
295
|
+
setPersonSheetOpen(true);
|
|
296
|
+
}}
|
|
297
|
+
selectPlaceholder={t('fields.companyPlaceholder')}
|
|
298
|
+
personTypeFilter="company"
|
|
299
|
+
createType="company"
|
|
300
|
+
lockCreateType
|
|
301
|
+
clearable
|
|
302
|
+
showEditButton
|
|
303
|
+
editAriaLabel="Editar empresa"
|
|
307
304
|
/>
|
|
308
305
|
|
|
309
306
|
{/* Portal enabled */}
|
|
@@ -389,6 +386,34 @@ export function EnterpriseSheet({
|
|
|
389
386
|
/>
|
|
390
387
|
</form>
|
|
391
388
|
</Form>
|
|
389
|
+
|
|
390
|
+
<EnterprisePersonEditSheet
|
|
391
|
+
open={personSheetOpen}
|
|
392
|
+
onOpenChange={(nextOpen) => {
|
|
393
|
+
setPersonSheetOpen(nextOpen);
|
|
394
|
+
if (!nextOpen) setPersonEditId(null);
|
|
395
|
+
}}
|
|
396
|
+
personId={personEditId}
|
|
397
|
+
title={
|
|
398
|
+
personEditId
|
|
399
|
+
? 'Editar empresa cliente'
|
|
400
|
+
: 'Cadastrar empresa cliente'
|
|
401
|
+
}
|
|
402
|
+
description={
|
|
403
|
+
personEditId
|
|
404
|
+
? 'Atualize os dados completos da empresa vinculada.'
|
|
405
|
+
: 'Cadastre a empresa cliente completa e vincule-a a esta conta enterprise.'
|
|
406
|
+
}
|
|
407
|
+
allowedTypes={['company']}
|
|
408
|
+
onSaved={(person) => {
|
|
409
|
+
if (!person?.id) return;
|
|
410
|
+
form.setValue('crmAccountId', person.id, {
|
|
411
|
+
shouldValidate: true,
|
|
412
|
+
shouldDirty: true,
|
|
413
|
+
});
|
|
414
|
+
setSelectedCompanyLabel(person.name ?? '');
|
|
415
|
+
}}
|
|
416
|
+
/>
|
|
392
417
|
</SheetContent>
|
|
393
418
|
</Sheet>
|
|
394
419
|
);
|
|
@@ -14,6 +14,7 @@ import { Input } from '@/components/ui/input';
|
|
|
14
14
|
import {
|
|
15
15
|
Sheet,
|
|
16
16
|
SheetContent,
|
|
17
|
+
SheetDescription,
|
|
17
18
|
SheetFooter,
|
|
18
19
|
SheetHeader,
|
|
19
20
|
SheetTitle,
|
|
@@ -103,6 +104,9 @@ export function EnterpriseStudentCreateSheet({
|
|
|
103
104
|
<SheetContent side="right" className="flex flex-col gap-0 sm:max-w-md">
|
|
104
105
|
<SheetHeader className="border-b px-6 py-4">
|
|
105
106
|
<SheetTitle>New student (person)</SheetTitle>
|
|
107
|
+
<SheetDescription>
|
|
108
|
+
Create a new student person record for this enterprise.
|
|
109
|
+
</SheetDescription>
|
|
106
110
|
</SheetHeader>
|
|
107
111
|
|
|
108
112
|
<Form {...form}>
|
|
@@ -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';
|
|
@@ -12,9 +12,10 @@ import {
|
|
|
12
12
|
TableHeader,
|
|
13
13
|
TableRow,
|
|
14
14
|
} from '@/components/ui/table';
|
|
15
|
+
import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
|
|
15
16
|
import { formatDate } from '@/lib/format-date';
|
|
16
17
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
17
|
-
import { Trash2 } from 'lucide-react';
|
|
18
|
+
import { Pencil, Trash2 } from 'lucide-react';
|
|
18
19
|
import { useTranslations } from 'next-intl';
|
|
19
20
|
import { useCallback, useState } from 'react';
|
|
20
21
|
import { toast } from 'sonner';
|
|
@@ -23,16 +24,14 @@ import {
|
|
|
23
24
|
USER_STATUS_VARIANT,
|
|
24
25
|
} from './enterprise-detail-constants';
|
|
25
26
|
import { getAvatarColor, getInitials } from './enterprise-detail-utils';
|
|
27
|
+
import { EnterprisePersonEditSheet } from './enterprise-person-edit-sheet';
|
|
26
28
|
import {
|
|
27
29
|
loadPersonOptions,
|
|
28
30
|
type PersonPickerOption,
|
|
29
31
|
} from './enterprise-person-picker';
|
|
30
32
|
import { RelatedTabLayout } from './enterprise-related-tab';
|
|
31
|
-
import { EnterpriseStudentCreateSheet } from './enterprise-student-create-sheet';
|
|
32
33
|
import type { EnterpriseStudent } from './enterprise-types';
|
|
33
34
|
|
|
34
|
-
const STUDENTS_PAGE_SIZE_DEFAULT = 10;
|
|
35
|
-
|
|
36
35
|
export function StudentsTab({
|
|
37
36
|
enterpriseId,
|
|
38
37
|
onMutate,
|
|
@@ -44,12 +43,17 @@ export function StudentsTab({
|
|
|
44
43
|
const { request, getSettingValue, currentLocaleCode } = useApp();
|
|
45
44
|
const [search, setSearch] = useState('');
|
|
46
45
|
const [page, setPage] = useState(1);
|
|
47
|
-
const [pageSize, setPageSize] =
|
|
46
|
+
const [pageSize, setPageSize] = usePersistedPageSize({
|
|
47
|
+
storageKey: 'pagination:global:pageSize',
|
|
48
|
+
defaultValue: 6,
|
|
49
|
+
allowedValues: [6, 12, 24, 48],
|
|
50
|
+
});
|
|
48
51
|
const [pickerValue, setPickerValue] = useState<EntityPickerValue>(null);
|
|
49
52
|
const [selectedPerson, setSelectedPerson] =
|
|
50
53
|
useState<PersonPickerOption | null>(null);
|
|
51
54
|
const [removingPersonId, setRemovingPersonId] = useState<number | null>(null);
|
|
52
|
-
const [
|
|
55
|
+
const [personSheetOpen, setPersonSheetOpen] = useState(false);
|
|
56
|
+
const [editingPersonId, setEditingPersonId] = useState<number | null>(null);
|
|
53
57
|
|
|
54
58
|
const loadPickerOptions = useCallback(
|
|
55
59
|
(args: { page: number; pageSize: number; search: string }) =>
|
|
@@ -168,7 +172,10 @@ export function StudentsTab({
|
|
|
168
172
|
</div>
|
|
169
173
|
</div>
|
|
170
174
|
)}
|
|
171
|
-
onCreateNew={() =>
|
|
175
|
+
onCreateNew={() => {
|
|
176
|
+
setEditingPersonId(null);
|
|
177
|
+
setPersonSheetOpen(true);
|
|
178
|
+
}}
|
|
172
179
|
onAdd={handleAdd}
|
|
173
180
|
addDisabled={!selectedPerson}
|
|
174
181
|
currentPage={page}
|
|
@@ -203,6 +210,13 @@ export function StudentsTab({
|
|
|
203
210
|
<Avatar
|
|
204
211
|
className={`size-7 shrink-0 rounded-full ${getAvatarColor(s.name ?? '').bg}`}
|
|
205
212
|
>
|
|
213
|
+
<AvatarImage
|
|
214
|
+
src={
|
|
215
|
+
s.avatarId
|
|
216
|
+
? `${process.env.NEXT_PUBLIC_API_BASE_URL}/person/avatar/${s.avatarId}`
|
|
217
|
+
: undefined
|
|
218
|
+
}
|
|
219
|
+
/>
|
|
206
220
|
<AvatarFallback
|
|
207
221
|
className={`text-xs font-semibold ${getAvatarColor(s.name ?? '').bg} ${getAvatarColor(s.name ?? '').text}`}
|
|
208
222
|
>
|
|
@@ -230,6 +244,17 @@ export function StudentsTab({
|
|
|
230
244
|
: '—'}
|
|
231
245
|
</TableCell>
|
|
232
246
|
<TableCell className="text-right">
|
|
247
|
+
<Button
|
|
248
|
+
variant="ghost"
|
|
249
|
+
size="icon"
|
|
250
|
+
className="h-7 w-7"
|
|
251
|
+
onClick={() => {
|
|
252
|
+
setEditingPersonId(s.personId);
|
|
253
|
+
setPersonSheetOpen(true);
|
|
254
|
+
}}
|
|
255
|
+
>
|
|
256
|
+
<Pencil className="size-4" />
|
|
257
|
+
</Button>
|
|
233
258
|
<Button
|
|
234
259
|
variant="ghost"
|
|
235
260
|
size="icon"
|
|
@@ -246,21 +271,34 @@ export function StudentsTab({
|
|
|
246
271
|
</Table>
|
|
247
272
|
)}
|
|
248
273
|
</RelatedTabLayout>
|
|
249
|
-
<
|
|
250
|
-
open={
|
|
251
|
-
onOpenChange={
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
274
|
+
<EnterprisePersonEditSheet
|
|
275
|
+
open={personSheetOpen}
|
|
276
|
+
onOpenChange={(nextOpen) => {
|
|
277
|
+
setPersonSheetOpen(nextOpen);
|
|
278
|
+
if (!nextOpen) setEditingPersonId(null);
|
|
279
|
+
}}
|
|
280
|
+
personId={editingPersonId}
|
|
281
|
+
title={editingPersonId ? 'Editar aluno' : 'Cadastrar novo aluno'}
|
|
282
|
+
description={
|
|
283
|
+
editingPersonId
|
|
284
|
+
? 'Atualize os dados completos da pessoa vinculada como aluna.'
|
|
285
|
+
: 'Cadastre a pessoa completa para vinculacao como aluna nesta conta enterprise.'
|
|
286
|
+
}
|
|
287
|
+
allowedTypes={['individual']}
|
|
288
|
+
onSaved={async (person) => {
|
|
289
|
+
if (editingPersonId === null && person?.id) {
|
|
290
|
+
try {
|
|
291
|
+
await request({
|
|
292
|
+
url: `/lms/enterprise/${enterpriseId}/students`,
|
|
293
|
+
method: 'POST',
|
|
294
|
+
data: { person_id: person.id, status: 'active' },
|
|
295
|
+
});
|
|
296
|
+
} catch {
|
|
297
|
+
toast.error(t('sheet.studentLinkError'));
|
|
298
|
+
}
|
|
261
299
|
}
|
|
262
|
-
|
|
263
|
-
refetch();
|
|
300
|
+
|
|
301
|
+
void refetch();
|
|
264
302
|
onMutate?.();
|
|
265
303
|
}}
|
|
266
304
|
/>
|
|
@@ -15,6 +15,7 @@ export type EnterpriseUserStatus = 'active' | 'inactive' | 'pending';
|
|
|
15
15
|
export interface EnterpriseCrmAccount {
|
|
16
16
|
id: number;
|
|
17
17
|
name: string;
|
|
18
|
+
avatarId: number | null;
|
|
18
19
|
tradeName: string | null;
|
|
19
20
|
industry: string | null;
|
|
20
21
|
website: string | null;
|
|
@@ -47,6 +48,7 @@ export interface EnterpriseUser {
|
|
|
47
48
|
userId: number;
|
|
48
49
|
name: string;
|
|
49
50
|
email: string;
|
|
51
|
+
photoId?: number | null;
|
|
50
52
|
role: EnterpriseUserRole;
|
|
51
53
|
status: EnterpriseUserStatus;
|
|
52
54
|
lastAccessAt: string | null;
|
|
@@ -57,6 +59,7 @@ export interface EnterpriseStudent {
|
|
|
57
59
|
personId: number;
|
|
58
60
|
name: string | null;
|
|
59
61
|
email: string | null;
|
|
62
|
+
avatarId?: number | null;
|
|
60
63
|
status: EnterpriseUserStatus;
|
|
61
64
|
createdAt: string | null;
|
|
62
65
|
}
|
|
@@ -73,6 +76,7 @@ export interface EnterpriseCourse {
|
|
|
73
76
|
courseId?: number;
|
|
74
77
|
title: string;
|
|
75
78
|
slug: string | null;
|
|
79
|
+
logoFileId?: number | null;
|
|
76
80
|
status: EnterpriseCourseStatus;
|
|
77
81
|
level?: string | null;
|
|
78
82
|
modality?: string;
|
|
@@ -87,6 +91,10 @@ export interface EnterpriseClass {
|
|
|
87
91
|
code: string | null;
|
|
88
92
|
title?: string | null;
|
|
89
93
|
courseTitle: string | null;
|
|
94
|
+
logoFileId?: number | null;
|
|
95
|
+
instructorId?: number | null;
|
|
96
|
+
instructorName?: string | null;
|
|
97
|
+
instructorAvatarId?: number | null;
|
|
90
98
|
startDate: string | null;
|
|
91
99
|
endDate: string | null;
|
|
92
100
|
capacity: number | null;
|
|
@@ -94,3 +102,49 @@ export interface EnterpriseClass {
|
|
|
94
102
|
status: EnterpriseClassStatus;
|
|
95
103
|
deliveryMode?: string | null;
|
|
96
104
|
}
|
|
105
|
+
|
|
106
|
+
export interface EnterpriseOverviewKpis {
|
|
107
|
+
students: number;
|
|
108
|
+
classes: number;
|
|
109
|
+
courses: number;
|
|
110
|
+
administrators: number;
|
|
111
|
+
portalEnabled: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface EnterpriseLicenseUsage {
|
|
115
|
+
used: number;
|
|
116
|
+
limit: number | null;
|
|
117
|
+
available: number | null;
|
|
118
|
+
percent: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface EnterpriseScheduledSeats {
|
|
122
|
+
used: number;
|
|
123
|
+
open: number;
|
|
124
|
+
capacity: number;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface EnterpriseLicenseTimelinePoint {
|
|
128
|
+
month: string;
|
|
129
|
+
label: string;
|
|
130
|
+
used: number;
|
|
131
|
+
assigned: number;
|
|
132
|
+
revoked: number;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface EnterpriseActivityItem {
|
|
136
|
+
id: string;
|
|
137
|
+
type: string;
|
|
138
|
+
title: string;
|
|
139
|
+
description: string;
|
|
140
|
+
createdAt: string;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface EnterpriseOverview {
|
|
144
|
+
account: EnterpriseAccount;
|
|
145
|
+
kpis: EnterpriseOverviewKpis;
|
|
146
|
+
licenseUsage: EnterpriseLicenseUsage;
|
|
147
|
+
scheduledSeats: EnterpriseScheduledSeats;
|
|
148
|
+
licenseTimeline: EnterpriseLicenseTimelinePoint[];
|
|
149
|
+
activities: EnterpriseActivityItem[];
|
|
150
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import {
|
|
5
|
+
Form,
|
|
6
|
+
FormControl,
|
|
7
|
+
FormDescription,
|
|
8
|
+
FormField,
|
|
9
|
+
FormItem,
|
|
10
|
+
FormLabel,
|
|
11
|
+
FormMessage,
|
|
12
|
+
} from '@/components/ui/form';
|
|
13
|
+
import { Input } from '@/components/ui/input';
|
|
14
|
+
import {
|
|
15
|
+
Select,
|
|
16
|
+
SelectContent,
|
|
17
|
+
SelectItem,
|
|
18
|
+
SelectTrigger,
|
|
19
|
+
SelectValue,
|
|
20
|
+
} from '@/components/ui/select';
|
|
21
|
+
import {
|
|
22
|
+
Sheet,
|
|
23
|
+
SheetContent,
|
|
24
|
+
SheetDescription,
|
|
25
|
+
SheetFooter,
|
|
26
|
+
SheetHeader,
|
|
27
|
+
SheetTitle,
|
|
28
|
+
} from '@/components/ui/sheet';
|
|
29
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
30
|
+
import { Loader2 } from 'lucide-react';
|
|
31
|
+
import { useEffect } from 'react';
|
|
32
|
+
import { useForm } from 'react-hook-form';
|
|
33
|
+
import { z } from 'zod';
|
|
34
|
+
import type { EnterpriseUser, EnterpriseUserRole } from './enterprise-types';
|
|
35
|
+
|
|
36
|
+
// ── Schema ─────────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
const userCreateSchema = z.object({
|
|
39
|
+
name: z.string().trim().min(1, 'Name is required.'),
|
|
40
|
+
email: z
|
|
41
|
+
.string()
|
|
42
|
+
.trim()
|
|
43
|
+
.min(1, 'Email is required.')
|
|
44
|
+
.email('Enter a valid email address.'),
|
|
45
|
+
role: z.enum(['student', 'hr_manager', 'enterprise_admin', 'viewer']),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
type UserCreateValues = z.infer<typeof userCreateSchema>;
|
|
49
|
+
|
|
50
|
+
const DEFAULT_VALUES: UserCreateValues = {
|
|
51
|
+
name: '',
|
|
52
|
+
email: '',
|
|
53
|
+
role: 'student',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// ── Props ─────────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
export interface EnterpriseUserCreateSheetProps {
|
|
59
|
+
open: boolean;
|
|
60
|
+
onOpenChange: (open: boolean) => void;
|
|
61
|
+
onCreated?: (user: EnterpriseUser) => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Component ─────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
export function EnterpriseUserCreateSheet({
|
|
67
|
+
open,
|
|
68
|
+
onOpenChange,
|
|
69
|
+
onCreated,
|
|
70
|
+
}: EnterpriseUserCreateSheetProps) {
|
|
71
|
+
const form = useForm<UserCreateValues>({
|
|
72
|
+
resolver: zodResolver(userCreateSchema),
|
|
73
|
+
defaultValues: DEFAULT_VALUES,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Reset form whenever the sheet closes
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
if (!open) {
|
|
79
|
+
form.reset(DEFAULT_VALUES);
|
|
80
|
+
}
|
|
81
|
+
}, [open, form]);
|
|
82
|
+
|
|
83
|
+
function handleOpenChange(nextOpen: boolean) {
|
|
84
|
+
if (!nextOpen) form.reset(DEFAULT_VALUES);
|
|
85
|
+
onOpenChange(nextOpen);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function onSubmit(values: UserCreateValues) {
|
|
89
|
+
// Optimistic mock creation — replace with real API call when available
|
|
90
|
+
const newUser: EnterpriseUser = {
|
|
91
|
+
id: Date.now(),
|
|
92
|
+
userId: Date.now() + 1,
|
|
93
|
+
name: values.name,
|
|
94
|
+
email: values.email,
|
|
95
|
+
role: values.role as EnterpriseUserRole,
|
|
96
|
+
status: 'pending',
|
|
97
|
+
lastAccessAt: null,
|
|
98
|
+
};
|
|
99
|
+
onCreated?.(newUser);
|
|
100
|
+
handleOpenChange(false);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const saving = form.formState.isSubmitting;
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<Sheet open={open} onOpenChange={handleOpenChange}>
|
|
107
|
+
<SheetContent side="right" className="flex flex-col gap-0 sm:max-w-md">
|
|
108
|
+
<SheetHeader className="border-b px-6 py-4">
|
|
109
|
+
<SheetTitle>New user</SheetTitle>
|
|
110
|
+
<SheetDescription>
|
|
111
|
+
Create a new enterprise user and send an invitation email.
|
|
112
|
+
</SheetDescription>
|
|
113
|
+
</SheetHeader>
|
|
114
|
+
|
|
115
|
+
<Form {...form}>
|
|
116
|
+
<form
|
|
117
|
+
onSubmit={form.handleSubmit(onSubmit)}
|
|
118
|
+
className="flex flex-1 flex-col overflow-hidden"
|
|
119
|
+
>
|
|
120
|
+
<div className="flex-1 space-y-5 overflow-y-auto px-6 py-5">
|
|
121
|
+
{/* Name */}
|
|
122
|
+
<FormField
|
|
123
|
+
control={form.control}
|
|
124
|
+
name="name"
|
|
125
|
+
render={({ field }) => (
|
|
126
|
+
<FormItem>
|
|
127
|
+
<FormLabel>Full name</FormLabel>
|
|
128
|
+
<FormControl>
|
|
129
|
+
<Input placeholder="e.g. Ana Beatriz Souza" {...field} />
|
|
130
|
+
</FormControl>
|
|
131
|
+
<FormMessage />
|
|
132
|
+
</FormItem>
|
|
133
|
+
)}
|
|
134
|
+
/>
|
|
135
|
+
|
|
136
|
+
{/* Email */}
|
|
137
|
+
<FormField
|
|
138
|
+
control={form.control}
|
|
139
|
+
name="email"
|
|
140
|
+
render={({ field }) => (
|
|
141
|
+
<FormItem>
|
|
142
|
+
<FormLabel>Email</FormLabel>
|
|
143
|
+
<FormControl>
|
|
144
|
+
<Input
|
|
145
|
+
type="email"
|
|
146
|
+
placeholder="user@company.com"
|
|
147
|
+
{...field}
|
|
148
|
+
/>
|
|
149
|
+
</FormControl>
|
|
150
|
+
<FormDescription>
|
|
151
|
+
An invite will be sent to this address.
|
|
152
|
+
</FormDescription>
|
|
153
|
+
<FormMessage />
|
|
154
|
+
</FormItem>
|
|
155
|
+
)}
|
|
156
|
+
/>
|
|
157
|
+
|
|
158
|
+
{/* Role */}
|
|
159
|
+
<FormField
|
|
160
|
+
control={form.control}
|
|
161
|
+
name="role"
|
|
162
|
+
render={({ field }) => (
|
|
163
|
+
<FormItem>
|
|
164
|
+
<FormLabel>Role</FormLabel>
|
|
165
|
+
<Select value={field.value} onValueChange={field.onChange}>
|
|
166
|
+
<FormControl>
|
|
167
|
+
<SelectTrigger className="w-full">
|
|
168
|
+
<SelectValue placeholder="Select a role" />
|
|
169
|
+
</SelectTrigger>
|
|
170
|
+
</FormControl>
|
|
171
|
+
<SelectContent>
|
|
172
|
+
<SelectItem value="enterprise_admin">
|
|
173
|
+
Admin — full account management
|
|
174
|
+
</SelectItem>
|
|
175
|
+
<SelectItem value="hr_manager">
|
|
176
|
+
HR / Manager — manages learners and reports
|
|
177
|
+
</SelectItem>
|
|
178
|
+
<SelectItem value="student">
|
|
179
|
+
Student — access to assigned courses
|
|
180
|
+
</SelectItem>
|
|
181
|
+
<SelectItem value="viewer">
|
|
182
|
+
Viewer — read-only access
|
|
183
|
+
</SelectItem>
|
|
184
|
+
</SelectContent>
|
|
185
|
+
</Select>
|
|
186
|
+
<FormMessage />
|
|
187
|
+
</FormItem>
|
|
188
|
+
)}
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<SheetFooter className="border-t px-6 py-4">
|
|
193
|
+
<Button
|
|
194
|
+
type="button"
|
|
195
|
+
variant="outline"
|
|
196
|
+
onClick={() => handleOpenChange(false)}
|
|
197
|
+
disabled={saving}
|
|
198
|
+
>
|
|
199
|
+
Cancel
|
|
200
|
+
</Button>
|
|
201
|
+
<Button type="submit" disabled={saving}>
|
|
202
|
+
{saving && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
203
|
+
Create user
|
|
204
|
+
</Button>
|
|
205
|
+
</SheetFooter>
|
|
206
|
+
</form>
|
|
207
|
+
</Form>
|
|
208
|
+
</SheetContent>
|
|
209
|
+
</Sheet>
|
|
210
|
+
);
|
|
211
|
+
}
|