@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.
Files changed (110) hide show
  1. package/dist/class-group/class-group.controller.d.ts +3 -3
  2. package/dist/class-group/class-group.service.d.ts +3 -3
  3. package/dist/course/course.service.d.ts.map +1 -1
  4. package/dist/course/course.service.js +12 -20
  5. package/dist/course/course.service.js.map +1 -1
  6. package/dist/enterprise/enterprise.controller.d.ts +72 -0
  7. package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
  8. package/dist/enterprise/enterprise.controller.js +10 -0
  9. package/dist/enterprise/enterprise.controller.js.map +1 -1
  10. package/dist/enterprise/enterprise.service.d.ts +78 -0
  11. package/dist/enterprise/enterprise.service.d.ts.map +1 -1
  12. package/dist/enterprise/enterprise.service.js +413 -40
  13. package/dist/enterprise/enterprise.service.js.map +1 -1
  14. package/dist/enterprise/training/training-admin.controller.d.ts +6 -3
  15. package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
  16. package/dist/enterprise/training/training-admin.controller.js +10 -6
  17. package/dist/enterprise/training/training-admin.controller.js.map +1 -1
  18. package/dist/enterprise/training/training-admin.service.d.ts +8 -2
  19. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
  20. package/dist/enterprise/training/training-admin.service.js +108 -52
  21. package/dist/enterprise/training/training-admin.service.js.map +1 -1
  22. package/dist/enterprise/training/training-viewer.controller.d.ts +3 -0
  23. package/dist/enterprise/training/training-viewer.controller.d.ts.map +1 -1
  24. package/dist/evaluation/evaluation.controller.d.ts +4 -4
  25. package/dist/evaluation/evaluation.service.d.ts +4 -4
  26. package/dist/instructor/dto/create-instructor-skill.dto.d.ts +0 -4
  27. package/dist/instructor/dto/create-instructor-skill.dto.d.ts.map +1 -1
  28. package/dist/instructor/dto/create-instructor-skill.dto.js +0 -21
  29. package/dist/instructor/dto/create-instructor-skill.dto.js.map +1 -1
  30. package/dist/instructor/dto/update-instructor-skill.dto.d.ts +0 -4
  31. package/dist/instructor/dto/update-instructor-skill.dto.d.ts.map +1 -1
  32. package/dist/instructor/dto/update-instructor-skill.dto.js +0 -22
  33. package/dist/instructor/dto/update-instructor-skill.dto.js.map +1 -1
  34. package/dist/instructor/instructor-skill.controller.d.ts +4 -4
  35. package/dist/instructor/instructor-skill.service.d.ts +4 -7
  36. package/dist/instructor/instructor-skill.service.d.ts.map +1 -1
  37. package/dist/instructor/instructor-skill.service.js +2 -89
  38. package/dist/instructor/instructor-skill.service.js.map +1 -1
  39. package/dist/instructor/instructor.controller.d.ts +20 -0
  40. package/dist/instructor/instructor.controller.d.ts.map +1 -1
  41. package/dist/instructor/instructor.controller.js +19 -0
  42. package/dist/instructor/instructor.controller.js.map +1 -1
  43. package/dist/instructor/instructor.service.d.ts +25 -0
  44. package/dist/instructor/instructor.service.d.ts.map +1 -1
  45. package/dist/instructor/instructor.service.js +70 -18
  46. package/dist/instructor/instructor.service.js.map +1 -1
  47. package/dist/lms.module.d.ts.map +1 -1
  48. package/dist/lms.module.js.map +1 -1
  49. package/hedhog/data/route.yaml +23 -1
  50. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +42 -24
  51. package/hedhog/frontend/app/_components/course-form-sheet.tsx.ejs +3 -3
  52. package/hedhog/frontend/app/_components/create-lms-instructor-sheet.tsx.ejs +591 -0
  53. package/hedhog/frontend/app/certificates/issued/page.tsx.ejs +6 -1
  54. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +6 -1
  55. package/hedhog/frontend/app/classes/page.tsx.ejs +6 -1
  56. package/hedhog/frontend/app/courses/[id]/_components/CourseClassificationCard.tsx.ejs +3 -33
  57. package/hedhog/frontend/app/courses/[id]/_components/CourseContentCard.tsx.ejs +9 -9
  58. package/hedhog/frontend/app/courses/[id]/_components/CourseMainInfoCard.tsx.ejs +109 -0
  59. package/hedhog/frontend/app/courses/[id]/_components/CourseMultiEntityPicker.tsx.ejs +40 -13
  60. package/hedhog/frontend/app/courses/[id]/_components/CourseRelationsCard.tsx.ejs +76 -81
  61. package/hedhog/frontend/app/courses/[id]/_components/CourseSummaryCard.tsx.ejs +60 -0
  62. package/hedhog/frontend/app/courses/[id]/structure/_components/course-scheduled-classes-tab.tsx.ejs +406 -0
  63. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -0
  64. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -0
  65. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -0
  66. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +174 -0
  67. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +242 -33
  68. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -0
  69. package/hedhog/frontend/app/courses/page.tsx.ejs +6 -1
  70. package/hedhog/frontend/app/enterprise/_components/enterprise-activity-timeline.tsx.ejs +87 -0
  71. package/hedhog/frontend/app/enterprise/_components/enterprise-admin-create-sheet.tsx.ejs +4 -0
  72. package/hedhog/frontend/app/enterprise/_components/enterprise-administrators-tab.tsx.ejs +31 -5
  73. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-tab.tsx.ejs +79 -20
  74. package/hedhog/frontend/app/enterprise/_components/enterprise-company-identity-card.tsx.ejs +11 -2
  75. package/hedhog/frontend/app/enterprise/_components/enterprise-course-edit-sheet.tsx.ejs +201 -0
  76. package/hedhog/frontend/app/enterprise/_components/enterprise-courses-tab.tsx.ejs +55 -24
  77. package/hedhog/frontend/app/enterprise/_components/enterprise-detail-sheet.tsx.ejs +430 -296
  78. package/hedhog/frontend/app/enterprise/_components/enterprise-mocks.ts.ejs +277 -0
  79. package/hedhog/frontend/app/enterprise/_components/enterprise-overview-analytics.tsx.ejs +205 -0
  80. package/hedhog/frontend/app/enterprise/_components/enterprise-person-edit-sheet.tsx.ejs +97 -0
  81. package/hedhog/frontend/app/enterprise/_components/enterprise-sheet.tsx.ejs +82 -57
  82. package/hedhog/frontend/app/enterprise/_components/enterprise-student-create-sheet.tsx.ejs +4 -0
  83. package/hedhog/frontend/app/enterprise/_components/enterprise-students-tab.tsx.ejs +60 -22
  84. package/hedhog/frontend/app/enterprise/_components/enterprise-types.ts.ejs +54 -0
  85. package/hedhog/frontend/app/enterprise/_components/enterprise-user-create-sheet.tsx.ejs +211 -0
  86. package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -7
  87. package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +6 -1
  88. package/hedhog/frontend/app/exams/page.tsx.ejs +6 -1
  89. package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +51 -104
  90. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +625 -366
  91. package/hedhog/frontend/app/instructors/page.tsx.ejs +6 -1
  92. package/hedhog/frontend/app/paths/page.tsx.ejs +9 -4
  93. package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +6 -1
  94. package/hedhog/frontend/app/training/page.tsx.ejs +9 -4
  95. package/hedhog/frontend/messages/en.json +101 -10
  96. package/hedhog/frontend/messages/pt.json +101 -10
  97. package/hedhog/table/enterprise_student_license_event.yaml +30 -0
  98. package/hedhog/table/instructor_skill.yaml +0 -11
  99. package/package.json +7 -7
  100. package/src/course/course.service.ts +12 -24
  101. package/src/enterprise/enterprise.controller.ts +5 -0
  102. package/src/enterprise/enterprise.service.ts +507 -29
  103. package/src/enterprise/training/training-admin.controller.ts +4 -0
  104. package/src/enterprise/training/training-admin.service.ts +115 -51
  105. package/src/instructor/dto/create-instructor-skill.dto.ts +0 -17
  106. package/src/instructor/dto/update-instructor-skill.dto.ts +0 -18
  107. package/src/instructor/instructor-skill.service.ts +2 -97
  108. package/src/instructor/instructor.controller.ts +16 -0
  109. package/src/instructor/instructor.service.ts +85 -10
  110. package/src/lms.module.ts +1 -0
@@ -23,7 +23,6 @@ import { useForm } from 'react-hook-form';
23
23
  import { toast } from 'sonner';
24
24
  import { z } from 'zod';
25
25
 
26
- import { CreateLmsPersonSheet } from '@/app/(app)/(libraries)/lms/_components/create-lms-person-sheet';
27
26
  import { createDefaultTemplate } from '@/app/(app)/(libraries)/lms/_lib/editor/types';
28
27
  import { RichTextEditor } from '@/components/rich-text-editor';
29
28
  import { Badge } from '@/components/ui/badge';
@@ -48,10 +47,19 @@ import {
48
47
  import { Input } from '@/components/ui/input';
49
48
  import { ScrollArea } from '@/components/ui/scroll-area';
50
49
  import { Separator } from '@/components/ui/separator';
50
+ import {
51
+ Sheet,
52
+ SheetContent,
53
+ SheetDescription,
54
+ SheetFooter,
55
+ SheetHeader,
56
+ SheetTitle,
57
+ } from '@/components/ui/sheet';
51
58
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
52
59
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
53
60
  import { useMutation, useQueryClient } from '@tanstack/react-query';
54
61
 
62
+ import { InstructorFormSheet } from '../../../../instructors/_components/instructor-form-sheet';
55
63
  import type {
56
64
  CourseEditFormValues,
57
65
  PickerOption,
@@ -114,6 +122,16 @@ type ApiCourseDetail = {
114
122
  };
115
123
 
116
124
  type ApiCategory = { id: number; slug: string; name: string };
125
+ type ApiCategoryDetail = {
126
+ id: number;
127
+ slug: string;
128
+ name?: string;
129
+ category_id?: number | null;
130
+ color?: string | null;
131
+ icon?: string | null;
132
+ status?: 'active' | 'inactive';
133
+ category_locale?: Array<{ locale_id: number; name: string }>;
134
+ };
117
135
  type ApiCategoryList = {
118
136
  data: ApiCategory[];
119
137
  total: number;
@@ -284,7 +302,27 @@ export function EditorCourse() {
284
302
 
285
303
  // ── UI state ────────────────────────────────────────────────────────────────
286
304
  const [activeTab, setActiveTab] = useState('estrutura');
287
- const [instructorSheetOpen, setInstructorSheetOpen] = useState(false);
305
+ const [instructorEditSheetOpen, setInstructorEditSheetOpen] = useState(false);
306
+ const [editingInstructorId, setEditingInstructorId] = useState<number | null>(
307
+ null
308
+ );
309
+ const [categoryEditSheetOpen, setCategoryEditSheetOpen] = useState(false);
310
+ const [editingCategoryId, setEditingCategoryId] = useState<number | null>(
311
+ null
312
+ );
313
+ const [editingCategoryOriginalSlug, setEditingCategoryOriginalSlug] =
314
+ useState('');
315
+ const [editingCategoryName, setEditingCategoryName] = useState('');
316
+ const [editingCategorySlug, setEditingCategorySlug] = useState('');
317
+ const [editingCategoryColor, setEditingCategoryColor] = useState('#000000');
318
+ const [editingCategoryIcon, setEditingCategoryIcon] = useState('');
319
+ const [editingCategoryParentId, setEditingCategoryParentId] = useState<
320
+ number | null
321
+ >(null);
322
+ const [editingCategoryStatus, setEditingCategoryStatus] = useState<
323
+ 'active' | 'inactive'
324
+ >('active');
325
+ const [savingCategoryEdit, setSavingCategoryEdit] = useState(false);
288
326
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
289
327
  const [deleting, setDeleting] = useState(false);
290
328
  const [logoPreview, setLogoPreview] = useState<string | null>(null);
@@ -691,7 +729,7 @@ export function EditorCourse() {
691
729
 
692
730
  async function handleCreateCategory(values: Record<string, string>) {
693
731
  const name = String(values.name ?? '').trim();
694
- const slug = slugify(values.slug || values.name || '');
732
+ const slug = slugify(values.name || values.slug || '');
695
733
  if (!name || !slug) {
696
734
  toast.error('Informe nome e slug para criar a categoria.');
697
735
  return null;
@@ -764,26 +802,112 @@ export function EditorCourse() {
764
802
  }
765
803
  }
766
804
 
767
- const handleInstructorCreated = async (instructor: {
768
- id: number;
769
- personId: number;
770
- name: string;
771
- avatarId?: number | null;
772
- email?: string | null;
773
- phone?: string | null;
774
- qualificationSlugs: string[];
775
- }) => {
776
- const createdId = String(instructor.id);
777
- const current = form.getValues('instrutores') ?? [];
778
- if (!current.includes(createdId)) {
779
- form.setValue('instrutores', [...current, createdId], {
780
- shouldDirty: true,
781
- shouldTouch: true,
782
- shouldValidate: true,
805
+ function handleCreateInstructor() {
806
+ setEditingInstructorId(null);
807
+ setInstructorEditSheetOpen(true);
808
+ }
809
+
810
+ function handleEditInstructor(instructorId: string) {
811
+ const parsed = Number(instructorId);
812
+ if (!Number.isFinite(parsed) || parsed <= 0) {
813
+ toast.error('Instrutor inválido para edição.');
814
+ return;
815
+ }
816
+ setEditingInstructorId(parsed);
817
+ setInstructorEditSheetOpen(true);
818
+ }
819
+
820
+ async function handleEditCategory(categorySlug: string) {
821
+ const category = (categoryListData?.data ?? []).find(
822
+ (item) => item.slug === categorySlug
823
+ );
824
+
825
+ if (!category?.id) {
826
+ toast.error('Categoria não encontrada para edição.');
827
+ return;
828
+ }
829
+
830
+ try {
831
+ const response = await request<ApiCategoryDetail>({
832
+ url: `/category/${category.id}`,
833
+ method: 'GET',
783
834
  });
835
+ const detail = response.data;
836
+ const localeName = detail.category_locale?.[0]?.name ?? detail.name ?? '';
837
+
838
+ setEditingCategoryId(detail.id);
839
+ setEditingCategoryOriginalSlug(detail.slug || categorySlug);
840
+ setEditingCategorySlug(detail.slug || categorySlug);
841
+ setEditingCategoryName(localeName);
842
+ setEditingCategoryColor(detail.color || '#000000');
843
+ setEditingCategoryIcon(detail.icon || '');
844
+ setEditingCategoryParentId(detail.category_id ?? null);
845
+ setEditingCategoryStatus(detail.status || 'active');
846
+ setCategoryEditSheetOpen(true);
847
+ } catch {
848
+ toast.error('Não foi possível carregar a categoria para edição.');
784
849
  }
785
- await refetchInstructorOptions();
786
- };
850
+ }
851
+
852
+ async function handleSaveCategoryEdit() {
853
+ if (!editingCategoryId) return;
854
+
855
+ const name = editingCategoryName.trim();
856
+ const slug = slugify(editingCategorySlug);
857
+
858
+ if (!name || !slug) {
859
+ toast.error('Informe nome e slug válidos para a categoria.');
860
+ return;
861
+ }
862
+
863
+ const localeCode =
864
+ currentLocaleCode ||
865
+ (locales?.[0] as Locale | undefined)?.code ||
866
+ 'pt-BR';
867
+
868
+ setSavingCategoryEdit(true);
869
+ try {
870
+ await request({
871
+ url: `/category/${editingCategoryId}`,
872
+ method: 'PATCH',
873
+ data: {
874
+ locale: {
875
+ [localeCode]: {
876
+ name,
877
+ },
878
+ },
879
+ slug,
880
+ category_id: editingCategoryParentId,
881
+ color: editingCategoryColor,
882
+ icon: editingCategoryIcon,
883
+ status: editingCategoryStatus,
884
+ },
885
+ });
886
+
887
+ const selectedCategories = form.getValues('categorias') ?? [];
888
+ if (editingCategoryOriginalSlug && editingCategoryOriginalSlug !== slug) {
889
+ form.setValue(
890
+ 'categorias',
891
+ selectedCategories.map((item) =>
892
+ item === editingCategoryOriginalSlug ? slug : item
893
+ ),
894
+ {
895
+ shouldDirty: true,
896
+ shouldTouch: true,
897
+ shouldValidate: true,
898
+ }
899
+ );
900
+ }
901
+
902
+ await refetchCategoryOptions();
903
+ setCategoryEditSheetOpen(false);
904
+ toast.success('Categoria atualizada com sucesso.');
905
+ } catch {
906
+ toast.error('Não foi possível atualizar a categoria.');
907
+ } finally {
908
+ setSavingCategoryEdit(false);
909
+ }
910
+ }
787
911
 
788
912
  async function handleDelete() {
789
913
  setDeleting(true);
@@ -1008,7 +1132,9 @@ export function EditorCourse() {
1008
1132
  categoryOptions={categoryOptions}
1009
1133
  instructorOptions={instructorOptions}
1010
1134
  onCreateCategory={handleCreateCategory}
1011
- onCreateInstructor={() => setInstructorSheetOpen(true)}
1135
+ onCreateInstructor={handleCreateInstructor}
1136
+ onEditCategory={handleEditCategory}
1137
+ onEditInstructor={handleEditInstructor}
1012
1138
  />
1013
1139
  <CourseContentCard form={form} />
1014
1140
  </TabsContent>
@@ -1131,19 +1257,102 @@ export function EditorCourse() {
1131
1257
  </div>
1132
1258
  </form>
1133
1259
 
1134
- {/* ── Instructor sheet ─────────────────────────────────────────────── */}
1135
- <CreateLmsPersonSheet
1136
- open={instructorSheetOpen}
1137
- onOpenChange={setInstructorSheetOpen}
1138
- onCreated={handleInstructorCreated}
1139
- title="Cadastrar instrutor"
1140
- description="Cadastre um novo instrutor e adicione-o ao curso."
1141
- submitLabel="Cadastrar instrutor"
1142
- successMessage="Instrutor cadastrado com sucesso."
1143
- errorMessage="Não foi possível cadastrar o instrutor."
1144
- defaultQualificationSlugs={['course-lessons']}
1260
+ <InstructorFormSheet
1261
+ open={instructorEditSheetOpen}
1262
+ onOpenChange={(open) => {
1263
+ setInstructorEditSheetOpen(open);
1264
+ if (!open) {
1265
+ setEditingInstructorId(null);
1266
+ }
1267
+ }}
1268
+ instructorId={editingInstructorId}
1269
+ onSaved={async (instructor) => {
1270
+ if (editingInstructorId === null && instructor?.id) {
1271
+ const createdId = String(instructor.id);
1272
+ const current = form.getValues('instrutores') ?? [];
1273
+ if (!current.includes(createdId)) {
1274
+ form.setValue('instrutores', [...current, createdId], {
1275
+ shouldDirty: true,
1276
+ shouldTouch: true,
1277
+ shouldValidate: true,
1278
+ });
1279
+ }
1280
+ }
1281
+
1282
+ await refetchInstructorOptions();
1283
+ await refetchCourse();
1284
+ }}
1145
1285
  />
1146
1286
 
1287
+ <Sheet
1288
+ open={categoryEditSheetOpen}
1289
+ onOpenChange={(open) => {
1290
+ setCategoryEditSheetOpen(open);
1291
+ if (!open) {
1292
+ setEditingCategoryId(null);
1293
+ setEditingCategoryOriginalSlug('');
1294
+ setEditingCategoryName('');
1295
+ setEditingCategorySlug('');
1296
+ setEditingCategoryColor('#000000');
1297
+ setEditingCategoryIcon('');
1298
+ setEditingCategoryParentId(null);
1299
+ setEditingCategoryStatus('active');
1300
+ }
1301
+ }}
1302
+ >
1303
+ <SheetContent className="sm:max-w-lg">
1304
+ <SheetHeader>
1305
+ <SheetTitle>Editar categoria</SheetTitle>
1306
+ <SheetDescription>
1307
+ Atualize rapidamente os dados da categoria vinculada ao curso.
1308
+ </SheetDescription>
1309
+ </SheetHeader>
1310
+
1311
+ <div className="space-y-3 px-4 pb-4">
1312
+ <div className="space-y-1.5">
1313
+ <FormLabel>Nome</FormLabel>
1314
+ <Input
1315
+ value={editingCategoryName}
1316
+ onChange={(event) => {
1317
+ const nextName = event.target.value;
1318
+ setEditingCategoryName(nextName);
1319
+ setEditingCategorySlug(slugify(nextName));
1320
+ }}
1321
+ placeholder="Nome da categoria"
1322
+ />
1323
+ </div>
1324
+
1325
+ <div className="space-y-1.5">
1326
+ <FormLabel>Slug</FormLabel>
1327
+ <Input
1328
+ value={editingCategorySlug}
1329
+ readOnly
1330
+ placeholder="slug-da-categoria"
1331
+ />
1332
+ </div>
1333
+ </div>
1334
+
1335
+ <SheetFooter>
1336
+ <Button
1337
+ variant="outline"
1338
+ onClick={() => setCategoryEditSheetOpen(false)}
1339
+ disabled={savingCategoryEdit}
1340
+ >
1341
+ Cancelar
1342
+ </Button>
1343
+ <Button
1344
+ onClick={handleSaveCategoryEdit}
1345
+ disabled={savingCategoryEdit}
1346
+ >
1347
+ {savingCategoryEdit ? (
1348
+ <Loader2 className="size-4 animate-spin" />
1349
+ ) : null}
1350
+ Salvar
1351
+ </Button>
1352
+ </SheetFooter>
1353
+ </SheetContent>
1354
+ </Sheet>
1355
+
1147
1356
  {/* ── Delete dialog ────────────────────────────────────────────────── */}
1148
1357
  <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
1149
1358
  <DialogContent>
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Mock Data — LMS Course Structure
3
+ *
4
+ * ⚠️ TEMPORARY — replace when integrating with the real API.
5
+ *
6
+ * TODO[API]: Remove this file entirely once `useCourseStructure` fetches data
7
+ * from GET /lms/courses/:id/structure. The Zustand store should then
8
+ * be seeded with the server response instead of these constants.
9
+ */
10
+
11
+ import type {
12
+ Course,
13
+ Lesson,
14
+ LessonStatus,
15
+ LessonType,
16
+ Session,
17
+ VideoProvider,
18
+ Visibility,
19
+ } from './types';
20
+
21
+ // ─────────────────────────────────────────────────────────────────────────────
22
+ // Mock Data — LMS Course Structure
23
+ // ─────────────────────────────────────────────────────────────────────────────
24
+
25
+ export const MOCK_COURSE: Course = {
26
+ id: 'course-1',
27
+ code: 'REACT-ADV',
28
+ name: 'React Avancado',
29
+ title: 'React Avancado',
30
+ description:
31
+ 'Domine os conceitos avancados do React: hooks, patterns, performance e gerenciamento de estado moderno.',
32
+ slug: 'react-avancado',
33
+ published: true,
34
+ };
35
+
36
+ // Session config: title + how many lessons each gets
37
+ const SESSION_CONFIG: { title: string; lessonCount: number }[] = [
38
+ { title: 'Boas-vindas ao curso', lessonCount: 5 },
39
+ { title: 'Hooks Avancados useReducer e useContext', lessonCount: 32 },
40
+ { title: 'Patterns de Composicao', lessonCount: 31 },
41
+ { title: 'Gerenciamento de Estado com Zustand', lessonCount: 8 },
42
+ { title: 'Performance e Otimizacao', lessonCount: 30 },
43
+ { title: 'React Server Components', lessonCount: 10 },
44
+ { title: 'Data Fetching Moderno', lessonCount: 9 },
45
+ { title: 'Roteamento Avancado com Next.js', lessonCount: 7 },
46
+ { title: 'Formularios e Validacao com RHF', lessonCount: 11 },
47
+ { title: 'Autenticacao e Seguranca', lessonCount: 9 },
48
+ { title: 'Testing com React Testing Library', lessonCount: 12 },
49
+ { title: 'Acessibilidade a11y', lessonCount: 6 },
50
+ { title: 'Internacionalizacao i18n', lessonCount: 7 },
51
+ { title: 'Animacoes com Framer Motion', lessonCount: 8 },
52
+ { title: 'Arquitetura de Projetos Escalaveis', lessonCount: 10 },
53
+ { title: 'Design Systems e Storybook', lessonCount: 8 },
54
+ { title: 'Deploy CI/CD e DevOps', lessonCount: 7 },
55
+ { title: 'Monorepos e Micro-frontends', lessonCount: 6 },
56
+ { title: 'Debugging Avancado', lessonCount: 6 },
57
+ { title: 'Projeto Final App Completo', lessonCount: 5 },
58
+ ];
59
+
60
+ export const MOCK_SESSIONS: Session[] = SESSION_CONFIG.map((s, i) => ({
61
+ id: `s${i + 1}`,
62
+ code: `S${String(i + 1).padStart(2, '0')}`,
63
+ title: s.title,
64
+ duration: s.lessonCount * 12,
65
+ order: i,
66
+ }));
67
+
68
+ // Lesson title patterns
69
+ const TITLE_PATTERNS = [
70
+ 'Introducao e objetivos',
71
+ 'Conceitos fundamentais',
72
+ 'Configuracao do ambiente',
73
+ 'Pratica guiada passo a passo',
74
+ 'Exercicio: implementacao',
75
+ 'Quiz de revisao',
76
+ 'Caso de uso real',
77
+ 'Implementacao completa',
78
+ 'Debugging e troubleshooting',
79
+ 'Otimizacoes e boas praticas',
80
+ 'Desafio pratico',
81
+ 'Revisao do modulo',
82
+ 'Q e A e duvidas frequentes',
83
+ 'Proximos passos',
84
+ ];
85
+
86
+ const LESSON_STATUSES: LessonStatus[] = [
87
+ 'preparada',
88
+ 'gravada',
89
+ 'editada',
90
+ 'finalizada',
91
+ 'publicada',
92
+ ];
93
+
94
+ const VISIBILITIES: Visibility[] = [
95
+ 'publico',
96
+ 'publico',
97
+ 'privado',
98
+ 'restrito',
99
+ ];
100
+
101
+ const LESSON_TYPES: LessonType[] = [
102
+ 'video',
103
+ 'video',
104
+ 'video',
105
+ 'post',
106
+ 'video',
107
+ 'video',
108
+ 'questao',
109
+ 'exercicio',
110
+ ];
111
+ const PROVIDERS: VideoProvider[] = [
112
+ 'youtube',
113
+ 'vimeo',
114
+ 'bunny',
115
+ 'youtube',
116
+ 'youtube',
117
+ ];
118
+
119
+ let _lid = 0;
120
+
121
+ export const MOCK_LESSONS: Lesson[] = SESSION_CONFIG.flatMap((s, si) =>
122
+ Array.from({ length: s.lessonCount }, (_, li) => {
123
+ _lid += 1;
124
+ const type = LESSON_TYPES[_lid % LESSON_TYPES.length] as LessonType;
125
+ const isVideo = type === 'video';
126
+ const provider = PROVIDERS[_lid % PROVIDERS.length] as VideoProvider;
127
+ const baseTitle = TITLE_PATTERNS[li % TITLE_PATTERNS.length] as string;
128
+ const title =
129
+ li < TITLE_PATTERNS.length
130
+ ? baseTitle
131
+ : baseTitle +
132
+ ' parte ' +
133
+ String(Math.floor(li / TITLE_PATTERNS.length) + 1);
134
+
135
+ const hasResource = li % 4 === 0;
136
+
137
+ const status = LESSON_STATUSES[
138
+ _lid % LESSON_STATUSES.length
139
+ ] as LessonStatus;
140
+ const visibility = VISIBILITIES[_lid % VISIBILITIES.length] as Visibility;
141
+
142
+ const lesson: Lesson = {
143
+ id: `l${_lid}`,
144
+ code: `A${String(_lid).padStart(3, '0')}`,
145
+ title,
146
+ type,
147
+ status,
148
+ visibility,
149
+ duration: 8 + (_lid % 32),
150
+ publicDescription: `Aprenda ${s.title.toLowerCase()} de forma pratica nesta aula.`,
151
+ privateDescription: li % 7 === 0 ? 'Revisar antes de publicar.' : '',
152
+ sessionId: `s${si + 1}`,
153
+ order: li,
154
+ resources: hasResource
155
+ ? [
156
+ {
157
+ id: `r${_lid}`,
158
+ name: `material-${String(_lid).padStart(3, '0')}.pdf`,
159
+ size: `${1 + (_lid % 5)}.${_lid % 9} MB`,
160
+ type: 'application/pdf',
161
+ public: _lid % 2 === 0,
162
+ url: `https://www.w3.org/WAI/WCAG21/Techniques/pdf/PDF1.pdf`,
163
+ },
164
+ ]
165
+ : [],
166
+ };
167
+
168
+ if (isVideo) {
169
+ lesson.videoProvider = provider;
170
+ lesson.videoUrl = `https://example.com/video/${_lid}`;
171
+ lesson.autoDuration = _lid % 3 !== 0;
172
+ if (li === 0) {
173
+ lesson.transcription = `Transcricao completa da primeira aula de "${s.title}". Lorem ipsum dolor sit amet, consectetur adipiscing elit.`;
174
+ }
175
+ }
176
+ if (type === 'questao') {
177
+ lesson.linkedExam = `exam-${_lid}`;
178
+ }
179
+ if (type === 'post') {
180
+ lesson.postContent = `Conteudo detalhado sobre ${s.title.toLowerCase()}...`;
181
+ }
182
+
183
+ return lesson;
184
+ })
185
+ );
@@ -60,6 +60,7 @@ import {
60
60
  import { useTranslations } from 'next-intl';
61
61
  import { useRouter } from 'next/navigation';
62
62
  import { useEffect, useMemo, useRef, useState } from 'react';
63
+ import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
63
64
  import { useForm, useWatch } from 'react-hook-form';
64
65
  import { toast } from 'sonner';
65
66
  import { CourseAvatar } from '../_components/course-avatar';
@@ -307,7 +308,11 @@ export default function CursosPage() {
307
308
 
308
309
  // Pagination
309
310
  const [currentPage, setCurrentPage] = useState(1);
310
- const [pageSize, setPageSize] = useState(12);
311
+ const [pageSize, setPageSize] = usePersistedPageSize({
312
+ storageKey: 'pagination:global:pageSize',
313
+ defaultValue: 12,
314
+ allowedValues: [6, 12, 24],
315
+ });
311
316
 
312
317
  // Double-click tracking
313
318
  const clickTimers = useRef<Map<number, ReturnType<typeof setTimeout>>>(
@@ -0,0 +1,87 @@
1
+ 'use client';
2
+
3
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
4
+ import { formatDate } from '@/lib/format-date';
5
+ import { useApp } from '@hed-hog/next-app-provider';
6
+ import {
7
+ BookOpen,
8
+ CalendarDays,
9
+ CircleDot,
10
+ ShieldCheck,
11
+ UserPlus,
12
+ } from 'lucide-react';
13
+ import type { EnterpriseOverview } from './enterprise-types';
14
+
15
+ const activityIcon = {
16
+ assigned: UserPlus,
17
+ revoked: UserPlus,
18
+ status_changed: UserPlus,
19
+ course: BookOpen,
20
+ class: CalendarDays,
21
+ student: UserPlus,
22
+ admin: ShieldCheck,
23
+ };
24
+
25
+ export function EnterpriseActivityTimeline({
26
+ activities,
27
+ }: {
28
+ activities: EnterpriseOverview['activities'];
29
+ }) {
30
+ const { getSettingValue, currentLocaleCode } = useApp();
31
+
32
+ return (
33
+ <Card className="border-border/60">
34
+ <CardHeader className="pb-2">
35
+ <CardTitle className="flex items-center gap-2 text-sm font-medium">
36
+ <CircleDot className="h-4 w-4 text-muted-foreground" />
37
+ Atividades recentes
38
+ </CardTitle>
39
+ </CardHeader>
40
+ <CardContent>
41
+ {activities.length === 0 ? (
42
+ <div className="flex h-28 items-center justify-center text-xs text-muted-foreground">
43
+ Nenhuma atividade recente.
44
+ </div>
45
+ ) : (
46
+ <div className="space-y-1">
47
+ {activities.map((activity, index) => {
48
+ const Icon =
49
+ activityIcon[activity.type as keyof typeof activityIcon] ??
50
+ CircleDot;
51
+ return (
52
+ <div
53
+ key={activity.id}
54
+ className="grid grid-cols-[auto_1fr] gap-3 py-2"
55
+ >
56
+ <div className="flex flex-col items-center">
57
+ <span className="flex h-7 w-7 items-center justify-center rounded-full border bg-background text-muted-foreground">
58
+ <Icon className="h-3.5 w-3.5" />
59
+ </span>
60
+ {index < activities.length - 1 && (
61
+ <span className="mt-1 h-full min-h-4 w-px bg-border" />
62
+ )}
63
+ </div>
64
+ <div className="min-w-0 pb-2">
65
+ <div className="flex flex-wrap items-center justify-between gap-2">
66
+ <p className="text-sm font-medium">{activity.title}</p>
67
+ <span className="text-[11px] text-muted-foreground">
68
+ {formatDate(
69
+ activity.createdAt,
70
+ getSettingValue,
71
+ currentLocaleCode
72
+ )}
73
+ </span>
74
+ </div>
75
+ <p className="mt-0.5 truncate text-xs text-muted-foreground">
76
+ {activity.description}
77
+ </p>
78
+ </div>
79
+ </div>
80
+ );
81
+ })}
82
+ </div>
83
+ )}
84
+ </CardContent>
85
+ </Card>
86
+ );
87
+ }
@@ -22,6 +22,7 @@ import {
22
22
  import {
23
23
  Sheet,
24
24
  SheetContent,
25
+ SheetDescription,
25
26
  SheetFooter,
26
27
  SheetHeader,
27
28
  SheetTitle,
@@ -167,6 +168,9 @@ export function EnterpriseAdminCreateSheet({
167
168
  <SheetContent side="right" className="flex flex-col gap-0 sm:max-w-md">
168
169
  <SheetHeader className="border-b px-6 py-4">
169
170
  <SheetTitle>New administrator</SheetTitle>
171
+ <SheetDescription>
172
+ Create an administrator account for this enterprise.
173
+ </SheetDescription>
170
174
  </SheetHeader>
171
175
 
172
176
  <Form {...form}>