@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.
Files changed (135) hide show
  1. package/dist/class-group/class-group.controller.d.ts +8 -8
  2. package/dist/class-group/class-group.service.d.ts +8 -8
  3. package/dist/course/course.controller.d.ts +6 -1
  4. package/dist/course/course.controller.d.ts.map +1 -1
  5. package/dist/course/course.controller.js +19 -2
  6. package/dist/course/course.controller.js.map +1 -1
  7. package/dist/course/course.service.d.ts +6 -0
  8. package/dist/course/course.service.d.ts.map +1 -1
  9. package/dist/course/course.service.js +63 -28
  10. package/dist/course/course.service.js.map +1 -1
  11. package/dist/course/dto/create-course.dto.d.ts +1 -0
  12. package/dist/course/dto/create-course.dto.d.ts.map +1 -1
  13. package/dist/course/dto/create-course.dto.js +5 -0
  14. package/dist/course/dto/create-course.dto.js.map +1 -1
  15. package/dist/enterprise/enterprise.controller.d.ts +84 -12
  16. package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
  17. package/dist/enterprise/enterprise.controller.js +10 -0
  18. package/dist/enterprise/enterprise.controller.js.map +1 -1
  19. package/dist/enterprise/enterprise.service.d.ts +90 -12
  20. package/dist/enterprise/enterprise.service.d.ts.map +1 -1
  21. package/dist/enterprise/enterprise.service.js +413 -40
  22. package/dist/enterprise/enterprise.service.js.map +1 -1
  23. package/dist/enterprise/training/training-admin.controller.d.ts +9 -6
  24. package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
  25. package/dist/enterprise/training/training-admin.controller.js +10 -6
  26. package/dist/enterprise/training/training-admin.controller.js.map +1 -1
  27. package/dist/enterprise/training/training-admin.service.d.ts +11 -5
  28. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
  29. package/dist/enterprise/training/training-admin.service.js +108 -52
  30. package/dist/enterprise/training/training-admin.service.js.map +1 -1
  31. package/dist/enterprise/training/training-viewer.controller.d.ts +3 -0
  32. package/dist/enterprise/training/training-viewer.controller.d.ts.map +1 -1
  33. package/dist/evaluation/evaluation.controller.d.ts +2 -2
  34. package/dist/evaluation/evaluation.service.d.ts +2 -2
  35. package/dist/instructor/dto/create-instructor-skill.dto.d.ts +0 -4
  36. package/dist/instructor/dto/create-instructor-skill.dto.d.ts.map +1 -1
  37. package/dist/instructor/dto/create-instructor-skill.dto.js +0 -21
  38. package/dist/instructor/dto/create-instructor-skill.dto.js.map +1 -1
  39. package/dist/instructor/dto/update-instructor-skill.dto.d.ts +0 -4
  40. package/dist/instructor/dto/update-instructor-skill.dto.d.ts.map +1 -1
  41. package/dist/instructor/dto/update-instructor-skill.dto.js +0 -22
  42. package/dist/instructor/dto/update-instructor-skill.dto.js.map +1 -1
  43. package/dist/instructor/instructor-skill.controller.d.ts +4 -4
  44. package/dist/instructor/instructor-skill.service.d.ts +4 -7
  45. package/dist/instructor/instructor-skill.service.d.ts.map +1 -1
  46. package/dist/instructor/instructor-skill.service.js +2 -89
  47. package/dist/instructor/instructor-skill.service.js.map +1 -1
  48. package/dist/instructor/instructor.controller.d.ts +21 -0
  49. package/dist/instructor/instructor.controller.d.ts.map +1 -1
  50. package/dist/instructor/instructor.controller.js +19 -0
  51. package/dist/instructor/instructor.controller.js.map +1 -1
  52. package/dist/instructor/instructor.service.d.ts +27 -0
  53. package/dist/instructor/instructor.service.d.ts.map +1 -1
  54. package/dist/instructor/instructor.service.js +79 -25
  55. package/dist/instructor/instructor.service.js.map +1 -1
  56. package/dist/lms.module.d.ts.map +1 -1
  57. package/dist/lms.module.js.map +1 -1
  58. package/dist/training/dto/create-training.dto.d.ts +1 -0
  59. package/dist/training/dto/create-training.dto.d.ts.map +1 -1
  60. package/dist/training/dto/create-training.dto.js +5 -0
  61. package/dist/training/dto/create-training.dto.js.map +1 -1
  62. package/dist/training/training.controller.d.ts +4 -0
  63. package/dist/training/training.controller.d.ts.map +1 -1
  64. package/dist/training/training.service.d.ts +8 -0
  65. package/dist/training/training.service.d.ts.map +1 -1
  66. package/dist/training/training.service.js +71 -6
  67. package/dist/training/training.service.js.map +1 -1
  68. package/hedhog/data/route.yaml +23 -1
  69. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +80 -33
  70. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +3 -3
  71. package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +591 -0
  72. package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +6 -1
  73. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +39 -7
  74. package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +1 -3
  75. package/hedhog/frontend/app/classes/page.tsx.ejs +34 -7
  76. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +3 -33
  77. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +9 -9
  78. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +109 -0
  79. package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +40 -13
  80. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +76 -81
  81. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +60 -0
  82. package/hedhog/frontend/app/courses/[id]/structure/_components/course-scheduled-classes-tab.tsx.ejs +406 -0
  83. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
  84. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
  85. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
  86. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
  87. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +243 -34
  88. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -0
  89. package/hedhog/frontend/app/courses/page.tsx.ejs +6 -1
  90. package/hedhog/frontend/app/enterprise/_components/enterprise-activity-timeline.tsx.ejs +87 -0
  91. package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +4 -0
  92. package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +31 -5
  93. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +79 -20
  94. package/hedhog/frontend/app/enterprise/_components/enterprise-company-identity-card.tsx.ejs +11 -2
  95. package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +201 -0
  96. package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +55 -24
  97. package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +430 -296
  98. package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +277 -0
  99. package/hedhog/frontend/app/enterprise/_components/enterprise-overview-analytics.tsx.ejs +205 -0
  100. package/hedhog/frontend/app/enterprise/_components/enterprise-person-edit-sheet.tsx.ejs +97 -0
  101. package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +82 -57
  102. package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +4 -0
  103. package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +60 -22
  104. package/hedhog/frontend/app/enterprise/_components/enterprise-types.ts.ejs +54 -0
  105. package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +211 -0
  106. package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -7
  107. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +31 -19
  108. package/hedhog/frontend/app/exams/page.tsx.ejs +6 -1
  109. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +51 -104
  110. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +625 -366
  111. package/hedhog/frontend/app/instructors/page.tsx.ejs +6 -1
  112. package/hedhog/frontend/app/paths/page.tsx.ejs +76 -8
  113. package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +6 -1
  114. package/hedhog/frontend/app/training/page.tsx.ejs +78 -9
  115. package/hedhog/frontend/messages/en.json +101 -10
  116. package/hedhog/frontend/messages/pt.json +115 -11
  117. package/hedhog/table/enterprise_student_license_event.yaml +30 -0
  118. package/hedhog/table/instructor_skill.yaml +0 -11
  119. package/hedhog/table/learning_path.yaml +4 -0
  120. package/package.json +6 -6
  121. package/src/course/course.controller.ts +18 -0
  122. package/src/course/course.service.ts +85 -26
  123. package/src/course/dto/create-course.dto.ts +4 -0
  124. package/src/enterprise/enterprise.controller.ts +5 -0
  125. package/src/enterprise/enterprise.service.ts +507 -29
  126. package/src/enterprise/training/training-admin.controller.ts +4 -0
  127. package/src/enterprise/training/training-admin.service.ts +115 -51
  128. package/src/instructor/dto/create-instructor-skill.dto.ts +0 -17
  129. package/src/instructor/dto/update-instructor-skill.dto.ts +0 -18
  130. package/src/instructor/instructor-skill.service.ts +2 -97
  131. package/src/instructor/instructor.controller.ts +16 -0
  132. package/src/instructor/instructor.service.ts +87 -10
  133. package/src/lms.module.ts +1 -0
  134. package/src/training/dto/create-training.dto.ts +4 -0
  135. package/src/training/training.service.ts +104 -5
@@ -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 placeholder={t('fields.namePlaceholder')} {...field} />
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 placeholder={t('fields.slugPlaceholder')} {...field} />
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">{t('status.active')}</SelectItem>
242
- <SelectItem value="trial">{t('status.trial')}</SelectItem>
243
- <SelectItem value="inactive">{t('status.inactive')}</SelectItem>
244
- <SelectItem value="suspended">{t('status.suspended')}</SelectItem>
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
- {/* Empresa (EntityPicker — handles form.control via Controller internally) */}
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
- clearable
268
- valueType="number"
269
- loadOptions={async ({ page, pageSize, search }) => {
270
- const params = new URLSearchParams({
271
- page: String(page),
272
- pageSize: String(pageSize),
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
- if (search.trim()) params.set('search', search.trim());
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
- getOptionValue={(opt) => (opt as any).id}
286
- getOptionLabel={(opt) => (opt as any).name ?? ''}
287
- createFields={[
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] = useState(STUDENTS_PAGE_SIZE_DEFAULT);
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 [createSheetOpen, setCreateSheetOpen] = useState(false);
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={() => setCreateSheetOpen(true)}
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
- <EnterpriseStudentCreateSheet
250
- open={createSheetOpen}
251
- onOpenChange={setCreateSheetOpen}
252
- onCreated={async (personId) => {
253
- try {
254
- await request({
255
- url: `/lms/enterprise/${enterpriseId}/students`,
256
- method: 'POST',
257
- data: { person_id: personId, status: 'active' },
258
- });
259
- } catch {
260
- toast.error(t('sheet.studentLinkError'));
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
- setCreateSheetOpen(false);
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
+ }