@hed-hog/lms 0.0.365 → 0.0.366

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 (113) hide show
  1. package/dist/class-group/class-group.controller.d.ts +1 -0
  2. package/dist/class-group/class-group.controller.d.ts.map +1 -1
  3. package/dist/class-group/class-group.service.d.ts +1 -0
  4. package/dist/class-group/class-group.service.d.ts.map +1 -1
  5. package/dist/course/course-structure.controller.d.ts +4 -2
  6. package/dist/course/course-structure.controller.d.ts.map +1 -1
  7. package/dist/course/course-structure.controller.js +6 -3
  8. package/dist/course/course-structure.controller.js.map +1 -1
  9. package/dist/course/course-video-agent-pipeline.service.d.ts +70 -0
  10. package/dist/course/course-video-agent-pipeline.service.d.ts.map +1 -0
  11. package/dist/course/course-video-agent-pipeline.service.js +398 -0
  12. package/dist/course/course-video-agent-pipeline.service.js.map +1 -0
  13. package/dist/course/course-video-hls.service.d.ts +14 -0
  14. package/dist/course/course-video-hls.service.d.ts.map +1 -1
  15. package/dist/course/course-video-hls.service.js +25 -8
  16. package/dist/course/course-video-hls.service.js.map +1 -1
  17. package/dist/course/course.controller.d.ts +2 -0
  18. package/dist/course/course.controller.d.ts.map +1 -1
  19. package/dist/course/course.module.d.ts.map +1 -1
  20. package/dist/course/course.module.js +5 -0
  21. package/dist/course/course.module.js.map +1 -1
  22. package/dist/course/course.service.d.ts +2 -0
  23. package/dist/course/course.service.d.ts.map +1 -1
  24. package/dist/course/course.service.js +36 -2
  25. package/dist/course/course.service.js.map +1 -1
  26. package/dist/course/ffmpeg.util.d.ts +10 -0
  27. package/dist/course/ffmpeg.util.d.ts.map +1 -0
  28. package/dist/course/ffmpeg.util.js +79 -0
  29. package/dist/course/ffmpeg.util.js.map +1 -0
  30. package/dist/course/lms-bulk-upload-automation.service.d.ts +3 -1
  31. package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -1
  32. package/dist/course/lms-bulk-upload-automation.service.js +7 -3
  33. package/dist/course/lms-bulk-upload-automation.service.js.map +1 -1
  34. package/dist/enterprise/training/training-admin.controller.d.ts +2 -0
  35. package/dist/enterprise/training/training-admin.controller.d.ts.map +1 -1
  36. package/dist/enterprise/training/training-admin.service.d.ts +2 -0
  37. package/dist/enterprise/training/training-admin.service.d.ts.map +1 -1
  38. package/dist/lms.module.d.ts.map +1 -1
  39. package/dist/lms.module.js +10 -0
  40. package/dist/lms.module.js.map +1 -1
  41. package/dist/platforma/dto/heartbeat.dto.d.ts +9 -0
  42. package/dist/platforma/dto/heartbeat.dto.d.ts.map +1 -0
  43. package/dist/platforma/dto/heartbeat.dto.js +50 -0
  44. package/dist/platforma/dto/heartbeat.dto.js.map +1 -0
  45. package/dist/platforma/handlers/emit-certificate.handler.d.ts +27 -0
  46. package/dist/platforma/handlers/emit-certificate.handler.d.ts.map +1 -0
  47. package/dist/platforma/handlers/emit-certificate.handler.js +117 -0
  48. package/dist/platforma/handlers/emit-certificate.handler.js.map +1 -0
  49. package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts +31 -0
  50. package/dist/platforma/handlers/lesson-heartbeat.handler.d.ts.map +1 -0
  51. package/dist/platforma/handlers/lesson-heartbeat.handler.js +281 -0
  52. package/dist/platforma/handlers/lesson-heartbeat.handler.js.map +1 -0
  53. package/dist/platforma/platforma-heartbeat.service.d.ts +10 -0
  54. package/dist/platforma/platforma-heartbeat.service.d.ts.map +1 -0
  55. package/dist/platforma/platforma-heartbeat.service.js +50 -0
  56. package/dist/platforma/platforma-heartbeat.service.js.map +1 -0
  57. package/dist/platforma/platforma-performance.service.d.ts +121 -0
  58. package/dist/platforma/platforma-performance.service.d.ts.map +1 -0
  59. package/dist/platforma/platforma-performance.service.js +500 -0
  60. package/dist/platforma/platforma-performance.service.js.map +1 -0
  61. package/dist/platforma/platforma-search.service.d.ts +21 -0
  62. package/dist/platforma/platforma-search.service.d.ts.map +1 -0
  63. package/dist/platforma/platforma-search.service.js +64 -0
  64. package/dist/platforma/platforma-search.service.js.map +1 -0
  65. package/dist/platforma/platforma.controller.d.ts +115 -1
  66. package/dist/platforma/platforma.controller.d.ts.map +1 -1
  67. package/dist/platforma/platforma.controller.js +50 -2
  68. package/dist/platforma/platforma.controller.js.map +1 -1
  69. package/dist/realtime/lms-realtime.controller.d.ts +2 -0
  70. package/dist/realtime/lms-realtime.controller.d.ts.map +1 -1
  71. package/dist/realtime/lms-realtime.controller.js +31 -0
  72. package/dist/realtime/lms-realtime.controller.js.map +1 -1
  73. package/dist/realtime/lms-realtime.service.d.ts +1 -1
  74. package/dist/realtime/lms-realtime.service.d.ts.map +1 -1
  75. package/dist/realtime/lms-realtime.service.js.map +1 -1
  76. package/hedhog/frontend/app/certificates/models/page.tsx.ejs +182 -29
  77. package/hedhog/frontend/app/classes/_components/classes-calendar-view.tsx.ejs +277 -0
  78. package/hedhog/frontend/app/classes/page.tsx.ejs +127 -20
  79. package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +141 -30
  80. package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +13 -13
  81. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +11 -23
  82. package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +1 -8
  83. package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +2 -0
  84. package/hedhog/frontend/app/courses/page.tsx.ejs +40 -9
  85. package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +6 -0
  86. package/hedhog/frontend/app/enterprise/_components/enterprise-classes-calendar-tab.tsx.ejs +264 -0
  87. package/hedhog/frontend/app/enterprise/page.tsx.ejs +104 -47
  88. package/hedhog/frontend/app/exams/page.tsx.ejs +38 -4
  89. package/hedhog/frontend/app/instructors/page.tsx.ejs +87 -46
  90. package/hedhog/frontend/app/paths/page.tsx.ejs +38 -4
  91. package/hedhog/frontend/app/training/page.tsx.ejs +38 -4
  92. package/hedhog/frontend/messages/en.json +18 -0
  93. package/hedhog/frontend/messages/pt.json +21 -1
  94. package/hedhog/table/course_enrollment.yaml +3 -0
  95. package/hedhog/table/lesson_view_event.yaml +66 -0
  96. package/package.json +9 -8
  97. package/src/course/course-structure.controller.ts +3 -1
  98. package/src/course/course-video-agent-pipeline.service.ts +471 -0
  99. package/src/course/course-video-hls.service.ts +30 -10
  100. package/src/course/course.module.ts +5 -0
  101. package/src/course/course.service.ts +46 -1
  102. package/src/course/ffmpeg.util.ts +65 -0
  103. package/src/course/lms-bulk-upload-automation.service.ts +4 -1
  104. package/src/lms.module.ts +10 -0
  105. package/src/platforma/dto/heartbeat.dto.ts +30 -0
  106. package/src/platforma/handlers/emit-certificate.handler.ts +117 -0
  107. package/src/platforma/handlers/lesson-heartbeat.handler.ts +343 -0
  108. package/src/platforma/platforma-heartbeat.service.ts +33 -0
  109. package/src/platforma/platforma-performance.service.ts +606 -0
  110. package/src/platforma/platforma-search.service.ts +48 -0
  111. package/src/platforma/platforma.controller.ts +42 -0
  112. package/src/realtime/lms-realtime.controller.ts +27 -1
  113. package/src/realtime/lms-realtime.service.ts +2 -1
@@ -22,7 +22,14 @@ import {
22
22
  TableRow,
23
23
  } from '@/components/ui/table';
24
24
  import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
25
+ import {
26
+ Tooltip,
27
+ TooltipContent,
28
+ TooltipProvider,
29
+ TooltipTrigger,
30
+ } from '@/components/ui/tooltip';
25
31
  import { useDebounce } from '@/hooks/use-debounce';
32
+ import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
26
33
  import { formatDate } from '@/lib/format-date';
27
34
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
28
35
  import {
@@ -51,7 +58,7 @@ const VIEW_STORAGE_KEY = 'lms-enterprise-view-mode';
51
58
 
52
59
  // ── Constants ─────────────────────────────────────────────────────────────────
53
60
 
54
- const PAGE_SIZE = 12;
61
+ const DEFAULT_PAGE_SIZES = [6, 12, 24, 48, 96] as const;
55
62
 
56
63
  const STATUS_VARIANT: Record<
57
64
  EnterpriseStatus,
@@ -90,6 +97,45 @@ export default function EnterprisePage() {
90
97
  const debouncedSearch = useDebounce(search);
91
98
  const { request, currentLocaleCode, getSettingValue } = useApp();
92
99
 
100
+ const { data: generalSettings } = useQuery<{
101
+ data: Array<{ slug: string; value: string }>;
102
+ }>({
103
+ queryKey: ['setting-group-general'],
104
+ queryFn: async () => {
105
+ const response = await request<{
106
+ data: Array<{ slug: string; value: string }>;
107
+ }>({
108
+ url: '/setting/group/general',
109
+ method: 'GET',
110
+ });
111
+ return response.data;
112
+ },
113
+ staleTime: 5 * 60 * 1000,
114
+ });
115
+
116
+ const pageSizeOptions = useMemo(() => {
117
+ const setting = generalSettings?.data?.find(
118
+ (s) => s.slug === 'pagination-page-sizes'
119
+ );
120
+ if (!setting?.value) return DEFAULT_PAGE_SIZES;
121
+ try {
122
+ const parsed = JSON.parse(setting.value) as string[];
123
+ const sizes = parsed
124
+ .map(Number)
125
+ .filter((n) => !isNaN(n) && n > 0)
126
+ .sort((a, b) => a - b);
127
+ return sizes.length > 0 ? sizes : DEFAULT_PAGE_SIZES;
128
+ } catch {
129
+ return DEFAULT_PAGE_SIZES;
130
+ }
131
+ }, [generalSettings]);
132
+
133
+ const [pageSize, setPageSize] = usePersistedPageSize({
134
+ storageKey: 'pagination:global:pageSize',
135
+ defaultValue: 12,
136
+ allowedValues: pageSizeOptions,
137
+ });
138
+
93
139
  type EnterpriseListResponse = {
94
140
  data: EnterpriseAccount[];
95
141
  total: number;
@@ -112,7 +158,7 @@ export default function EnterprisePage() {
112
158
  queryKey: [
113
159
  'lms-enterprise',
114
160
  page,
115
- PAGE_SIZE,
161
+ pageSize,
116
162
  debouncedSearch,
117
163
  statusFilter,
118
164
  crmFilter,
@@ -120,7 +166,7 @@ export default function EnterprisePage() {
120
166
  queryFn: async () => {
121
167
  const params = new URLSearchParams();
122
168
  params.set('page', String(page));
123
- params.set('pageSize', String(PAGE_SIZE));
169
+ params.set('pageSize', String(pageSize));
124
170
  if (debouncedSearch) params.set('search', debouncedSearch);
125
171
  if (statusFilter !== 'all') params.set('status', statusFilter);
126
172
  if (crmFilter !== 'all') params.set('crmPersonId', crmFilter);
@@ -335,47 +381,56 @@ export default function EnterprisePage() {
335
381
 
336
382
  <KpiCardsGrid items={kpiItems} />
337
383
 
338
- <div className="flex flex-col gap-4 xl:flex-row xl:items-center">
339
- <div className="flex-1">
340
- <SearchBar
341
- searchQuery={search}
342
- onSearchChange={handleSearch}
343
- onSearch={() => {}}
344
- placeholder={t('filters.searchPlaceholder')}
345
- controls={controls}
346
- />
347
- </div>
348
- <div className="flex items-center gap-3">
349
- <span className="text-xs font-medium text-muted-foreground">
350
- {t.has('view.label') ? t('view.label') : 'View'}
351
- </span>
352
- <ToggleGroup
353
- type="single"
354
- value={viewMode}
355
- onValueChange={handleViewModeChange}
356
- variant="outline"
357
- size="sm"
358
- aria-label={
359
- t.has('view.modeAriaLabel')
360
- ? t('view.modeAriaLabel')
361
- : 'View mode'
362
- }
363
- >
364
- <ToggleGroupItem value="table" className="gap-1.5 px-2.5">
365
- <List className="h-4 w-4" />
366
- <span className="hidden sm:inline">
367
- {t.has('view.table') ? t('view.table') : 'Table'}
368
- </span>
369
- </ToggleGroupItem>
370
- <ToggleGroupItem value="cards" className="gap-1.5 px-2.5">
371
- <LayoutGrid className="h-4 w-4" />
372
- <span className="hidden sm:inline">
373
- {t.has('view.cards') ? t('view.cards') : 'Cards'}
374
- </span>
375
- </ToggleGroupItem>
376
- </ToggleGroup>
377
- </div>
378
- </div>
384
+ <SearchBar
385
+ searchQuery={search}
386
+ onSearchChange={handleSearch}
387
+ onSearch={() => {}}
388
+ placeholder={t('filters.searchPlaceholder')}
389
+ controls={controls}
390
+ actions={
391
+ <TooltipProvider>
392
+ <ToggleGroup
393
+ type="single"
394
+ value={viewMode}
395
+ onValueChange={handleViewModeChange}
396
+ variant="outline"
397
+ size="sm"
398
+ aria-label={
399
+ t.has('view.modeAriaLabel') ? t('view.modeAriaLabel') : 'View mode'
400
+ }
401
+ >
402
+ <Tooltip>
403
+ <TooltipTrigger asChild>
404
+ <ToggleGroupItem
405
+ value="table"
406
+ className="cursor-pointer"
407
+ aria-label={t.has('view.table') ? t('view.table') : 'Table'}
408
+ >
409
+ <List className="h-4 w-4" />
410
+ </ToggleGroupItem>
411
+ </TooltipTrigger>
412
+ <TooltipContent>
413
+ {t.has('view.table') ? t('view.table') : 'Table'}
414
+ </TooltipContent>
415
+ </Tooltip>
416
+ <Tooltip>
417
+ <TooltipTrigger asChild>
418
+ <ToggleGroupItem
419
+ value="cards"
420
+ className="cursor-pointer"
421
+ aria-label={t.has('view.cards') ? t('view.cards') : 'Cards'}
422
+ >
423
+ <LayoutGrid className="h-4 w-4" />
424
+ </ToggleGroupItem>
425
+ </TooltipTrigger>
426
+ <TooltipContent>
427
+ {t.has('view.cards') ? t('view.cards') : 'Cards'}
428
+ </TooltipContent>
429
+ </Tooltip>
430
+ </ToggleGroup>
431
+ </TooltipProvider>
432
+ }
433
+ />
379
434
 
380
435
  {accounts.length === 0 && !isLoading ? (
381
436
  <EmptyState
@@ -633,12 +688,14 @@ export default function EnterprisePage() {
633
688
 
634
689
  <PaginationFooter
635
690
  currentPage={page}
636
- pageSize={PAGE_SIZE}
691
+ pageSize={pageSize}
637
692
  totalItems={totalItems}
638
693
  onPageChange={setPage}
639
- onPageSizeChange={function (): void {
640
- throw new Error('Function not implemented.');
694
+ onPageSizeChange={(next) => {
695
+ setPageSize(next);
696
+ setPage(1);
641
697
  }}
698
+ pageSizeOptions={pageSizeOptions}
642
699
  />
643
700
 
644
701
  <EnterpriseSheet
@@ -213,7 +213,7 @@ function toNumberOrFallback(value: unknown, fallback: number) {
213
213
 
214
214
  // ── Constants ─────────────────────────────────────────────────────────────────
215
215
 
216
- const PAGE_SIZES = [6, 12, 24];
216
+ const DEFAULT_PAGE_SIZES = [6, 12, 24, 48, 96] as const;
217
217
 
218
218
  function formatTempo(minutos: number) {
219
219
  if (minutos < 60) return `${minutos}min`;
@@ -263,10 +263,44 @@ export default function ExamesPage() {
263
263
 
264
264
  // Pagination
265
265
  const [currentPage, setCurrentPage] = useState(1);
266
+
267
+ const { data: generalSettings } = useQuery<{
268
+ data: Array<{ slug: string; value: string }>;
269
+ }>({
270
+ queryKey: ['setting-group-general'],
271
+ queryFn: async () => {
272
+ const response = await request<{
273
+ data: Array<{ slug: string; value: string }>;
274
+ }>({
275
+ url: '/setting/group/general',
276
+ method: 'GET',
277
+ });
278
+ return response.data;
279
+ },
280
+ staleTime: 5 * 60 * 1000,
281
+ });
282
+
283
+ const pageSizeOptions = useMemo(() => {
284
+ const setting = generalSettings?.data?.find(
285
+ (s) => s.slug === 'pagination-page-sizes'
286
+ );
287
+ if (!setting?.value) return DEFAULT_PAGE_SIZES;
288
+ try {
289
+ const parsed = JSON.parse(setting.value) as string[];
290
+ const sizes = parsed
291
+ .map(Number)
292
+ .filter((n) => !isNaN(n) && n > 0)
293
+ .sort((a, b) => a - b);
294
+ return sizes.length > 0 ? sizes : DEFAULT_PAGE_SIZES;
295
+ } catch {
296
+ return DEFAULT_PAGE_SIZES;
297
+ }
298
+ }, [generalSettings]);
299
+
266
300
  const [pageSize, setPageSize] = usePersistedPageSize({
267
301
  storageKey: 'pagination:global:pageSize',
268
302
  defaultValue: 12,
269
- allowedValues: PAGE_SIZES,
303
+ allowedValues: pageSizeOptions,
270
304
  });
271
305
 
272
306
  const form = useForm<ExameForm>({
@@ -654,7 +688,7 @@ export default function ExamesPage() {
654
688
  ],
655
689
  },
656
690
  ]}
657
- afterSearchButton={
691
+ actions={
658
692
  <ViewModeToggle
659
693
  viewMode={viewMode}
660
694
  onViewModeChange={setViewMode}
@@ -1037,7 +1071,7 @@ export default function ExamesPage() {
1037
1071
  setPageSize(nextPageSize);
1038
1072
  setCurrentPage(1);
1039
1073
  }}
1040
- pageSizeOptions={PAGE_SIZES}
1074
+ pageSizeOptions={pageSizeOptions}
1041
1075
  />
1042
1076
  </div>
1043
1077
  )}
@@ -33,6 +33,12 @@ import {
33
33
  TableRow,
34
34
  } from '@/components/ui/table';
35
35
  import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
36
+ import {
37
+ Tooltip,
38
+ TooltipContent,
39
+ TooltipProvider,
40
+ TooltipTrigger,
41
+ } from '@/components/ui/tooltip';
36
42
  import { usePersistedPageSize } from '@/hooks/use-persisted-page-size';
37
43
  import { cn } from '@/lib/utils';
38
44
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
@@ -48,7 +54,7 @@ import {
48
54
  Users,
49
55
  } from 'lucide-react';
50
56
  import { useTranslations } from 'next-intl';
51
- import { useEffect, useState } from 'react';
57
+ import { useEffect, useMemo, useState } from 'react';
52
58
  import { toast } from 'sonner';
53
59
  import { InstructorFormSheet } from './_components/instructor-form-sheet';
54
60
  import type {
@@ -95,10 +101,43 @@ export default function InstructorsPage() {
95
101
  const t = useTranslations('lms.InstructorsPage');
96
102
 
97
103
  const [page, setPage] = useState(1);
104
+ const { data: generalSettings } = useQuery<{
105
+ data: Array<{ slug: string; value: string }>;
106
+ }>({
107
+ queryKey: ['setting-group-general'],
108
+ queryFn: async () => {
109
+ const response = await request<{
110
+ data: Array<{ slug: string; value: string }>;
111
+ }>({
112
+ url: '/setting/group/general',
113
+ method: 'GET',
114
+ });
115
+ return response.data;
116
+ },
117
+ staleTime: 5 * 60 * 1000,
118
+ });
119
+
120
+ const pageSizeOptions = useMemo(() => {
121
+ const setting = generalSettings?.data?.find(
122
+ (s) => s.slug === 'pagination-page-sizes'
123
+ );
124
+ if (!setting?.value) return [6, 12, 24, 48, 96];
125
+ try {
126
+ const parsed = JSON.parse(setting.value) as string[];
127
+ const sizes = parsed
128
+ .map(Number)
129
+ .filter((n) => !isNaN(n) && n > 0)
130
+ .sort((a, b) => a - b);
131
+ return sizes.length > 0 ? sizes : [6, 12, 24, 48, 96];
132
+ } catch {
133
+ return [6, 12, 24, 48, 96];
134
+ }
135
+ }, [generalSettings]);
136
+
98
137
  const [pageSize, setPageSize] = usePersistedPageSize({
99
138
  storageKey: 'pagination:global:pageSize',
100
139
  defaultValue: 12,
101
- allowedValues: [6, 12, 24, 48],
140
+ allowedValues: pageSizeOptions,
102
141
  });
103
142
  const [searchInput, setSearchInput] = useState('');
104
143
  const [debouncedSearch, setDebouncedSearch] = useState('');
@@ -329,50 +368,52 @@ export default function InstructorsPage() {
329
368
 
330
369
  <KpiCardsGrid items={statsCards} />
331
370
 
332
- <div className="flex flex-col gap-4 xl:flex-row xl:items-center">
333
- <div className="flex-1">
334
- <SearchBar
335
- searchQuery={searchInput}
336
- onSearchChange={(value) => {
337
- setSearchInput(value);
338
- }}
339
- onSearch={() => setPage(1)}
340
- placeholder={t('search.placeholder')}
341
- controls={searchControls}
342
- />{' '}
343
- </div>
344
-
345
- <div className="flex items-center justify-between gap-3 sm:justify-start xl:justify-end">
346
- <span className="text-xs font-medium text-muted-foreground">
347
- {t('viewMode.label')}
348
- </span>
349
- <ToggleGroup
350
- type="single"
351
- value={viewMode}
352
- onValueChange={handleViewModeChange}
353
- variant="outline"
354
- size="sm"
355
- aria-label={t('viewMode.ariaLabel')}
356
- >
357
- <ToggleGroupItem
358
- value="table"
359
- className="gap-1.5 px-2.5"
360
- aria-label={t('viewMode.tableAriaLabel')}
361
- >
362
- <List className="h-4 w-4" />
363
- <span className="hidden sm:inline">{t('viewMode.table')}</span>
364
- </ToggleGroupItem>
365
- <ToggleGroupItem
366
- value="cards"
367
- className="gap-1.5 px-2.5"
368
- aria-label={t('viewMode.cardsAriaLabel')}
371
+ <SearchBar
372
+ searchQuery={searchInput}
373
+ onSearchChange={(value) => {
374
+ setSearchInput(value);
375
+ }}
376
+ onSearch={() => setPage(1)}
377
+ placeholder={t('search.placeholder')}
378
+ controls={searchControls}
379
+ actions={
380
+ <TooltipProvider>
381
+ <ToggleGroup
382
+ type="single"
383
+ value={viewMode}
384
+ onValueChange={handleViewModeChange}
385
+ variant="outline"
386
+ size="sm"
387
+ aria-label={t('viewMode.ariaLabel')}
369
388
  >
370
- <LayoutGrid className="h-4 w-4" />
371
- <span className="hidden sm:inline">{t('viewMode.cards')}</span>
372
- </ToggleGroupItem>
373
- </ToggleGroup>
374
- </div>
375
- </div>
389
+ <Tooltip>
390
+ <TooltipTrigger asChild>
391
+ <ToggleGroupItem
392
+ value="table"
393
+ className="cursor-pointer"
394
+ aria-label={t('viewMode.tableAriaLabel')}
395
+ >
396
+ <List className="h-4 w-4" />
397
+ </ToggleGroupItem>
398
+ </TooltipTrigger>
399
+ <TooltipContent>{t('viewMode.table')}</TooltipContent>
400
+ </Tooltip>
401
+ <Tooltip>
402
+ <TooltipTrigger asChild>
403
+ <ToggleGroupItem
404
+ value="cards"
405
+ className="cursor-pointer"
406
+ aria-label={t('viewMode.cardsAriaLabel')}
407
+ >
408
+ <LayoutGrid className="h-4 w-4" />
409
+ </ToggleGroupItem>
410
+ </TooltipTrigger>
411
+ <TooltipContent>{t('viewMode.cards')}</TooltipContent>
412
+ </Tooltip>
413
+ </ToggleGroup>
414
+ </TooltipProvider>
415
+ }
416
+ />
376
417
 
377
418
  {isLoading ? (
378
419
  viewMode === 'cards' ? (
@@ -726,7 +767,7 @@ export default function InstructorsPage() {
726
767
  setPageSize(nextPageSize);
727
768
  setPage(1);
728
769
  }}
729
- pageSizeOptions={[6, 12, 24]}
770
+ pageSizeOptions={pageSizeOptions}
730
771
  />
731
772
 
732
773
  <InstructorFormSheet
@@ -462,7 +462,7 @@ const STATUS_MAP: Record<
462
462
  encerrada: { label: 'Encerrada', variant: 'outline' },
463
463
  };
464
464
 
465
- const PAGE_SIZES = [6, 12, 24];
465
+ const DEFAULT_PAGE_SIZES = [6, 12, 24, 48, 96] as const;
466
466
  const API_TRAINING_CACHE_KEY = 'lms:training:api-cache';
467
467
 
468
468
  // ── Animations ────────────────────────────────────────────────────────────────
@@ -578,10 +578,44 @@ export default function TrainingPage() {
578
578
 
579
579
  // Pagination
580
580
  const [currentPage, setCurrentPage] = useState(1);
581
+
582
+ const { data: generalSettings } = useQuery<{
583
+ data: Array<{ slug: string; value: string }>;
584
+ }>({
585
+ queryKey: ['setting-group-general'],
586
+ queryFn: async () => {
587
+ const response = await request<{
588
+ data: Array<{ slug: string; value: string }>;
589
+ }>({
590
+ url: '/setting/group/general',
591
+ method: 'GET',
592
+ });
593
+ return response.data;
594
+ },
595
+ staleTime: 5 * 60 * 1000,
596
+ });
597
+
598
+ const pageSizeOptions = useMemo(() => {
599
+ const setting = generalSettings?.data?.find(
600
+ (s) => s.slug === 'pagination-page-sizes'
601
+ );
602
+ if (!setting?.value) return DEFAULT_PAGE_SIZES;
603
+ try {
604
+ const parsed = JSON.parse(setting.value) as string[];
605
+ const sizes = parsed
606
+ .map(Number)
607
+ .filter((n) => !isNaN(n) && n > 0)
608
+ .sort((a, b) => a - b);
609
+ return sizes.length > 0 ? sizes : DEFAULT_PAGE_SIZES;
610
+ } catch {
611
+ return DEFAULT_PAGE_SIZES;
612
+ }
613
+ }, [generalSettings]);
614
+
581
615
  const [pageSize, setPageSize] = usePersistedPageSize({
582
616
  storageKey: 'pagination:global:pageSize',
583
617
  defaultValue: 12,
584
- allowedValues: PAGE_SIZES,
618
+ allowedValues: pageSizeOptions,
585
619
  });
586
620
 
587
621
  const sensors = useSensors(
@@ -1500,7 +1534,7 @@ export default function TrainingPage() {
1500
1534
  ],
1501
1535
  },
1502
1536
  ]}
1503
- afterSearchButton={
1537
+ actions={
1504
1538
  <ViewModeToggle
1505
1539
  viewMode={viewMode}
1506
1540
  onViewModeChange={setViewMode}
@@ -1848,7 +1882,7 @@ export default function TrainingPage() {
1848
1882
  setPageSize(nextPageSize);
1849
1883
  setCurrentPage(1);
1850
1884
  }}
1851
- pageSizeOptions={PAGE_SIZES}
1885
+ pageSizeOptions={pageSizeOptions}
1852
1886
  />
1853
1887
  </div>
1854
1888
  )}
@@ -448,7 +448,7 @@ const STATUS_MAP: Record<
448
448
  encerrada: { label: 'Encerrada', variant: 'outline' },
449
449
  };
450
450
 
451
- const PAGE_SIZES = [6, 12, 24];
451
+ const DEFAULT_PAGE_SIZES = [6, 12, 24, 48, 96] as const;
452
452
  const API_TRAINING_CACHE_KEY = 'lms:training:api-cache';
453
453
 
454
454
  // ── Animations ────────────────────────────────────────────────────────────────
@@ -564,10 +564,44 @@ export default function TrainingPage() {
564
564
 
565
565
  // Pagination
566
566
  const [currentPage, setCurrentPage] = useState(1);
567
+
568
+ const { data: generalSettings } = useQuery<{
569
+ data: Array<{ slug: string; value: string }>;
570
+ }>({
571
+ queryKey: ['setting-group-general'],
572
+ queryFn: async () => {
573
+ const response = await request<{
574
+ data: Array<{ slug: string; value: string }>;
575
+ }>({
576
+ url: '/setting/group/general',
577
+ method: 'GET',
578
+ });
579
+ return response.data;
580
+ },
581
+ staleTime: 5 * 60 * 1000,
582
+ });
583
+
584
+ const pageSizeOptions = useMemo(() => {
585
+ const setting = generalSettings?.data?.find(
586
+ (s) => s.slug === 'pagination-page-sizes'
587
+ );
588
+ if (!setting?.value) return DEFAULT_PAGE_SIZES;
589
+ try {
590
+ const parsed = JSON.parse(setting.value) as string[];
591
+ const sizes = parsed
592
+ .map(Number)
593
+ .filter((n) => !isNaN(n) && n > 0)
594
+ .sort((a, b) => a - b);
595
+ return sizes.length > 0 ? sizes : DEFAULT_PAGE_SIZES;
596
+ } catch {
597
+ return DEFAULT_PAGE_SIZES;
598
+ }
599
+ }, [generalSettings]);
600
+
567
601
  const [pageSize, setPageSize] = usePersistedPageSize({
568
602
  storageKey: 'pagination:global:pageSize',
569
603
  defaultValue: 12,
570
- allowedValues: PAGE_SIZES,
604
+ allowedValues: pageSizeOptions,
571
605
  });
572
606
 
573
607
  const sensors = useSensors(
@@ -1490,7 +1524,7 @@ export default function TrainingPage() {
1490
1524
  ],
1491
1525
  },
1492
1526
  ]}
1493
- afterSearchButton={
1527
+ actions={
1494
1528
  <ViewModeToggle
1495
1529
  viewMode={viewMode}
1496
1530
  onViewModeChange={setViewMode}
@@ -1838,7 +1872,7 @@ export default function TrainingPage() {
1838
1872
  setPageSize(nextPageSize);
1839
1873
  setCurrentPage(1);
1840
1874
  }}
1841
- pageSizeOptions={PAGE_SIZES}
1875
+ pageSizeOptions={pageSizeOptions}
1842
1876
  />
1843
1877
  </div>
1844
1878
  )}
@@ -555,6 +555,13 @@
555
555
  "withTranscription": "With transcription",
556
556
  "withXp": "With XP"
557
557
  },
558
+ "videoPipeline": {
559
+ "title": "Video pipeline",
560
+ "description": "Video lessons by stage: total → has video → processed",
561
+ "total": "Video lessons",
562
+ "withVideo": "With video",
563
+ "withProcessedVideo": "Video processed"
564
+ },
558
565
  "areas": {
559
566
  "title": "Macro areas",
560
567
  "description": "Areas mapped across lessons, ordered by XP weight"
@@ -3049,6 +3056,17 @@
3049
3056
  "cancel": "Cancel",
3050
3057
  "delete": "Remove"
3051
3058
  }
3059
+ },
3060
+ "viewMode": {
3061
+ "list": "Show as list",
3062
+ "cards": "Show as cards"
3063
+ },
3064
+ "table": {
3065
+ "name": "Name",
3066
+ "slug": "Slug",
3067
+ "status": "Status",
3068
+ "updatedAt": "Updated at",
3069
+ "actions": "Actions"
3052
3070
  }
3053
3071
  },
3054
3072
  "ClassesPage": {
@@ -564,6 +564,13 @@
564
564
  "withTranscription": "Com transcrição",
565
565
  "withXp": "Com XP"
566
566
  },
567
+ "videoPipeline": {
568
+ "title": "Pipeline de vídeo",
569
+ "description": "Aulas de vídeo por estágio: total → com vídeo → processado",
570
+ "total": "Aulas de vídeo",
571
+ "withVideo": "Com vídeo",
572
+ "withProcessedVideo": "Vídeo processado"
573
+ },
567
574
  "areas": {
568
575
  "title": "Áreas macro",
569
576
  "description": "Áreas mapeadas nas aulas, ordenadas por peso de XP"
@@ -3071,6 +3078,17 @@
3071
3078
  "cancel": "Cancelar",
3072
3079
  "delete": "Remover"
3073
3080
  }
3081
+ },
3082
+ "viewMode": {
3083
+ "list": "Visualizar em lista",
3084
+ "cards": "Visualizar em cards"
3085
+ },
3086
+ "table": {
3087
+ "name": "Nome",
3088
+ "slug": "Slug",
3089
+ "status": "Status",
3090
+ "updatedAt": "Atualizado em",
3091
+ "actions": "Ações"
3074
3092
  }
3075
3093
  },
3076
3094
  "ClassesPage": {
@@ -3152,7 +3170,8 @@
3152
3170
  },
3153
3171
  "viewMode": {
3154
3172
  "list": "Visualizar em lista",
3155
- "cards": "Visualizar em cards"
3173
+ "cards": "Visualizar em cards",
3174
+ "calendar": "Visualizar em calendário"
3156
3175
  },
3157
3176
  "pagination": {
3158
3177
  "class": "turma",
@@ -4569,6 +4588,7 @@
4569
4588
  "users": "Usuários",
4570
4589
  "courses": "Cursos",
4571
4590
  "classes": "Turmas",
4591
+ "classesCalendar": "Calendário de Turmas",
4572
4592
  "billing": "Faturamento",
4573
4593
  "students": "Alunos",
4574
4594
  "administrators": "Administradores",
@@ -34,6 +34,9 @@ columns:
34
34
  - name: final_score
35
35
  type: int
36
36
  isNullable: true
37
+ - name: certificate_issued_at
38
+ type: datetime
39
+ isNullable: true
37
40
  - type: created_at
38
41
  - type: updated_at
39
42