@hed-hog/lms 0.0.330 → 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/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 +7 -2
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +17 -17
- 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 +42 -15
- 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]/page.tsx.ejs +3 -3
- 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-dnd.tsx.ejs +1 -1
- 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/editor-lesson.tsx.ejs +228 -152
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +71 -31
- package/hedhog/frontend/app/courses/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/enterprise/[id]/page.tsx.ejs +37 -41
- 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/evaluations/_components/evaluation-topic-form-sheet.tsx.ejs +1 -1
- package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/exams/page.tsx.ejs +12 -3
- package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +51 -104
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +712 -427
- package/hedhog/frontend/app/instructors/page.tsx.ejs +77 -53
- package/hedhog/frontend/app/paths/page.tsx.ejs +14 -5
- package/hedhog/frontend/app/reports/courses/page.tsx.ejs +5 -5
- package/hedhog/frontend/app/reports/dashboard/page.tsx.ejs +8 -8
- package/hedhog/frontend/app/reports/evaluations/page.tsx.ejs +6 -1
- package/hedhog/frontend/app/reports/page.tsx.ejs +7 -7
- package/hedhog/frontend/app/reports/students/page.tsx.ejs +6 -6
- package/hedhog/frontend/app/training/page.tsx.ejs +8 -3
- package/hedhog/frontend/messages/en.json +394 -55
- package/hedhog/frontend/messages/pt.json +389 -48
- package/hedhog/frontend/widgets/active-classes-kpi.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/active-courses-kpi.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/approval-rate-kpi.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/class-calendar.tsx.ejs +2 -2
- package/hedhog/frontend/widgets/completion-rate-kpi.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/issued-certificates-kpi.tsx.ejs +1 -1
- package/hedhog/frontend/widgets/total-students-kpi.tsx.ejs +1 -1
- package/hedhog/table/enterprise_student_license_event.yaml +30 -0
- package/hedhog/table/instructor_qualification.yaml +1 -1
- package/hedhog/table/instructor_skill.yaml +0 -11
- package/package.json +8 -8
- 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
|
@@ -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
|
+
);
|
|
@@ -18,36 +18,7 @@ import { Separator } from '@/components/ui/separator';
|
|
|
18
18
|
const SHORTCUT_GROUPS: {
|
|
19
19
|
heading: string;
|
|
20
20
|
items: { keys: string[]; description: string }[];
|
|
21
|
-
}[] = [
|
|
22
|
-
{
|
|
23
|
-
heading: 'Navegação',
|
|
24
|
-
items: [
|
|
25
|
-
{ keys: ['↑', '↓'], description: 'Navegar entre itens' },
|
|
26
|
-
{ keys: ['→'], description: 'Expandir sessão selecionada' },
|
|
27
|
-
{ keys: ['←'], description: 'Recolher sessão / ir para pai' },
|
|
28
|
-
{ keys: ['Enter'], description: 'Focar primeiro campo do editor' },
|
|
29
|
-
],
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
heading: 'Ações',
|
|
33
|
-
items: [
|
|
34
|
-
{ keys: ['Ctrl', 'S'], description: 'Salvar formulário do painel' },
|
|
35
|
-
{ keys: ['Ctrl', 'N'], description: 'Criar nova sessão ou aula' },
|
|
36
|
-
{ keys: ['Ctrl', 'C'], description: 'Copiar item selecionado' },
|
|
37
|
-
{ keys: ['Ctrl', 'V'], description: 'Colar no contexto atual' },
|
|
38
|
-
{ keys: ['Ctrl', 'D'], description: 'Duplicar item' },
|
|
39
|
-
{ keys: ['Delete'], description: 'Excluir item(ns) selecionado(s)' },
|
|
40
|
-
],
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
heading: 'Busca & Interface',
|
|
44
|
-
items: [
|
|
45
|
-
{ keys: ['Ctrl', 'F'], description: 'Focar campo de busca' },
|
|
46
|
-
{ keys: ['Ctrl', '/'], description: 'Abrir ajuda de atalhos' },
|
|
47
|
-
{ keys: ['Esc'], description: 'Limpar busca / seleção / foco' },
|
|
48
|
-
],
|
|
49
|
-
},
|
|
50
|
-
];
|
|
21
|
+
}[] = [];
|
|
51
22
|
|
|
52
23
|
// ── Component ─────────────────────────────────────────────────────────────────
|
|
53
24
|
|
|
@@ -58,6 +29,75 @@ interface ShortcutsHelpProps {
|
|
|
58
29
|
|
|
59
30
|
export function ShortcutsHelp({ open, onOpenChange }: ShortcutsHelpProps) {
|
|
60
31
|
const t = useTranslations('lms.CoursesPage.StructurePage.shortcuts');
|
|
32
|
+
const shortcutGroups = [
|
|
33
|
+
{
|
|
34
|
+
heading: t('groups.navigation.heading'),
|
|
35
|
+
items: [
|
|
36
|
+
{
|
|
37
|
+
keys: ['↑', '↓'],
|
|
38
|
+
description: t('groups.navigation.items.navigate'),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
keys: ['→'],
|
|
42
|
+
description: t('groups.navigation.items.expand'),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
keys: ['←'],
|
|
46
|
+
description: t('groups.navigation.items.collapse'),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
keys: ['Enter'],
|
|
50
|
+
description: t('groups.navigation.items.focusEditor'),
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
heading: t('groups.actions.heading'),
|
|
56
|
+
items: [
|
|
57
|
+
{
|
|
58
|
+
keys: ['Ctrl', 'S'],
|
|
59
|
+
description: t('groups.actions.items.savePanel'),
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
keys: ['Ctrl', 'N'],
|
|
63
|
+
description: t('groups.actions.items.createItem'),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
keys: ['Ctrl', 'C'],
|
|
67
|
+
description: t('groups.actions.items.copyItem'),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
keys: ['Ctrl', 'V'],
|
|
71
|
+
description: t('groups.actions.items.pasteItem'),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
keys: ['Ctrl', 'D'],
|
|
75
|
+
description: t('groups.actions.items.duplicateItem'),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
keys: ['Delete'],
|
|
79
|
+
description: t('groups.actions.items.deleteItems'),
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
heading: t('groups.search.heading'),
|
|
85
|
+
items: [
|
|
86
|
+
{
|
|
87
|
+
keys: ['Ctrl', 'F'],
|
|
88
|
+
description: t('groups.search.items.focusSearch'),
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
keys: ['Ctrl', '/'],
|
|
92
|
+
description: t('groups.search.items.openHelp'),
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
keys: ['Esc'],
|
|
96
|
+
description: t('groups.search.items.clearState'),
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
];
|
|
61
101
|
return (
|
|
62
102
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
63
103
|
<DialogContent className="max-w-sm">
|
|
@@ -69,7 +109,7 @@ export function ShortcutsHelp({ open, onOpenChange }: ShortcutsHelpProps) {
|
|
|
69
109
|
</DialogHeader>
|
|
70
110
|
|
|
71
111
|
<div className="flex flex-col gap-4 mt-1">
|
|
72
|
-
{
|
|
112
|
+
{shortcutGroups.map((group, gi) => (
|
|
73
113
|
<div key={gi}>
|
|
74
114
|
<p className="text-[0.65rem] font-semibold uppercase tracking-wider text-muted-foreground mb-2">
|
|
75
115
|
{group.heading}
|
|
@@ -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] =
|
|
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>>>(
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
UserCheck,
|
|
21
21
|
Users,
|
|
22
22
|
} from 'lucide-react';
|
|
23
|
+
import { useTranslations } from 'next-intl';
|
|
23
24
|
import { notFound, useParams, useRouter } from 'next/navigation';
|
|
24
25
|
import { useState } from 'react';
|
|
25
26
|
import { AdministratorsTab } from '../_components/enterprise-administrators-tab';
|
|
@@ -44,13 +45,6 @@ const STATUS_VARIANT: Record<
|
|
|
44
45
|
suspended: 'destructive',
|
|
45
46
|
};
|
|
46
47
|
|
|
47
|
-
const STATUS_LABEL: Record<EnterpriseStatus, string> = {
|
|
48
|
-
active: 'Active',
|
|
49
|
-
trial: 'Trial',
|
|
50
|
-
inactive: 'Inactive',
|
|
51
|
-
suspended: 'Suspended',
|
|
52
|
-
};
|
|
53
|
-
|
|
54
48
|
// ── CRM Tab ───────────────────────────────────────────────────────────────────
|
|
55
49
|
|
|
56
50
|
function CrmTab({
|
|
@@ -62,6 +56,7 @@ function CrmTab({
|
|
|
62
56
|
crmAccountName: string | null;
|
|
63
57
|
onLinkCrm?: () => void;
|
|
64
58
|
}) {
|
|
59
|
+
const t = useTranslations('lms.EnterpriseDetailPage');
|
|
65
60
|
const router = useRouter();
|
|
66
61
|
if (!crmAccountId || !crmAccountName) {
|
|
67
62
|
return (
|
|
@@ -70,11 +65,10 @@ function CrmTab({
|
|
|
70
65
|
<Link2 className="h-12 w-12" />
|
|
71
66
|
</div>
|
|
72
67
|
<p className="text-sm font-medium text-muted-foreground">
|
|
73
|
-
|
|
68
|
+
{t('crm.noLinked')}
|
|
74
69
|
</p>
|
|
75
70
|
<p className="mt-1 max-w-xs text-xs text-muted-foreground/60">
|
|
76
|
-
|
|
77
|
-
account. Link one to keep both records in sync.
|
|
71
|
+
{t('crm.noLinkedDesc')}
|
|
78
72
|
</p>
|
|
79
73
|
<Button
|
|
80
74
|
variant="outline"
|
|
@@ -83,7 +77,7 @@ function CrmTab({
|
|
|
83
77
|
onClick={onLinkCrm}
|
|
84
78
|
>
|
|
85
79
|
<Link2 className="mr-2 h-4 w-4" />
|
|
86
|
-
|
|
80
|
+
{t('crm.linkButton')}
|
|
87
81
|
</Button>
|
|
88
82
|
</div>
|
|
89
83
|
);
|
|
@@ -100,7 +94,7 @@ function CrmTab({
|
|
|
100
94
|
<div>
|
|
101
95
|
<p className="text-base font-semibold">{crmAccountName}</p>
|
|
102
96
|
<p className="text-xs text-muted-foreground">
|
|
103
|
-
|
|
97
|
+
{t('crm.accountRef', { id: crmAccountId })}
|
|
104
98
|
</p>
|
|
105
99
|
</div>
|
|
106
100
|
</div>
|
|
@@ -111,7 +105,7 @@ function CrmTab({
|
|
|
111
105
|
className="shrink-0"
|
|
112
106
|
>
|
|
113
107
|
<ExternalLink className="mr-2 h-4 w-4" />
|
|
114
|
-
|
|
108
|
+
{t('crm.openButton')}
|
|
115
109
|
</Button>
|
|
116
110
|
</div>
|
|
117
111
|
|
|
@@ -120,22 +114,20 @@ function CrmTab({
|
|
|
120
114
|
<dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
121
115
|
<div>
|
|
122
116
|
<dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
123
|
-
|
|
117
|
+
{t('crm.accountNameLabel')}
|
|
124
118
|
</dt>
|
|
125
119
|
<dd className="mt-1 text-sm">{crmAccountName}</dd>
|
|
126
120
|
</div>
|
|
127
121
|
<div>
|
|
128
122
|
<dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
129
|
-
|
|
123
|
+
{t('crm.crmIdLabel')}
|
|
130
124
|
</dt>
|
|
131
125
|
<dd className="mt-1 font-mono text-sm">#{crmAccountId}</dd>
|
|
132
126
|
</div>
|
|
133
127
|
</dl>
|
|
134
128
|
|
|
135
129
|
<p className="mt-5 text-xs text-muted-foreground/60">
|
|
136
|
-
|
|
137
|
-
<span className="font-medium">Open in CRM</span> to manage the
|
|
138
|
-
commercial account directly.
|
|
130
|
+
{t('crm.readNote')}
|
|
139
131
|
</p>
|
|
140
132
|
</CardContent>
|
|
141
133
|
</Card>
|
|
@@ -145,35 +137,36 @@ function CrmTab({
|
|
|
145
137
|
// ── Overview Tab ──────────────────────────────────────────────────────────────
|
|
146
138
|
|
|
147
139
|
function OverviewTab({ account }: { account: EnterpriseAccount }) {
|
|
140
|
+
const t = useTranslations('lms.EnterpriseDetailPage');
|
|
148
141
|
const { currentLocaleCode, getSettingValue } = useApp();
|
|
149
142
|
|
|
150
143
|
const kpiItems: KpiCardItem[] = [
|
|
151
144
|
{
|
|
152
145
|
key: 'users',
|
|
153
|
-
title: '
|
|
146
|
+
title: t('kpis.users.label'),
|
|
154
147
|
value: account.usersCount,
|
|
155
|
-
description: '
|
|
148
|
+
description: t('kpis.users.description'),
|
|
156
149
|
icon: Users,
|
|
157
150
|
},
|
|
158
151
|
{
|
|
159
152
|
key: 'students',
|
|
160
|
-
title: '
|
|
153
|
+
title: t('kpis.students.label'),
|
|
161
154
|
value: account.studentsCount,
|
|
162
|
-
description: '
|
|
155
|
+
description: t('kpis.students.description'),
|
|
163
156
|
icon: UserCheck,
|
|
164
157
|
},
|
|
165
158
|
{
|
|
166
159
|
key: 'classes',
|
|
167
|
-
title: '
|
|
160
|
+
title: t('kpis.contractedClasses.label'),
|
|
168
161
|
value: account.classesCount,
|
|
169
|
-
description: '
|
|
162
|
+
description: t('kpis.contractedClasses.description'),
|
|
170
163
|
icon: CalendarDays,
|
|
171
164
|
},
|
|
172
165
|
{
|
|
173
166
|
key: 'courses',
|
|
174
|
-
title: '
|
|
167
|
+
title: t('kpis.courses.label'),
|
|
175
168
|
value: account.coursesCount,
|
|
176
|
-
description: '
|
|
169
|
+
description: t('kpis.courses.description'),
|
|
177
170
|
icon: BookOpen,
|
|
178
171
|
},
|
|
179
172
|
];
|
|
@@ -183,19 +176,19 @@ function OverviewTab({ account }: { account: EnterpriseAccount }) {
|
|
|
183
176
|
<KpiCardsGrid items={kpiItems} />
|
|
184
177
|
<div className="flex flex-wrap items-center gap-x-6 gap-y-1 border-t pt-3 text-xs text-muted-foreground">
|
|
185
178
|
<span>
|
|
186
|
-
|
|
179
|
+
{t('overview.slug')}{' '}
|
|
187
180
|
<span className="font-mono text-foreground/70">{account.slug}</span>
|
|
188
181
|
</span>
|
|
189
182
|
<span>
|
|
190
|
-
|
|
183
|
+
{t('overview.created')}{' '}
|
|
191
184
|
{formatDate(account.createdAt, getSettingValue, currentLocaleCode)}
|
|
192
185
|
</span>
|
|
193
186
|
<span>
|
|
194
|
-
|
|
187
|
+
{t('overview.updated')}{' '}
|
|
195
188
|
{formatDate(account.updatedAt, getSettingValue, currentLocaleCode)}
|
|
196
189
|
</span>
|
|
197
190
|
{account.portalEnabled && (
|
|
198
|
-
<span className="text-primary/70">
|
|
191
|
+
<span className="text-primary/70">{t('overview.portalEnabled')}</span>
|
|
199
192
|
)}
|
|
200
193
|
</div>
|
|
201
194
|
</div>
|
|
@@ -207,6 +200,7 @@ function OverviewTab({ account }: { account: EnterpriseAccount }) {
|
|
|
207
200
|
export default function EnterpriseDetailPage() {
|
|
208
201
|
const { id } = useParams<{ id: string }>();
|
|
209
202
|
const { request } = useApp();
|
|
203
|
+
const t = useTranslations('lms.EnterpriseDetailPage');
|
|
210
204
|
const [isSheetOpen, setIsSheetOpen] = useState(false);
|
|
211
205
|
|
|
212
206
|
const {
|
|
@@ -233,9 +227,9 @@ export default function EnterpriseDetailPage() {
|
|
|
233
227
|
<Page>
|
|
234
228
|
<PageHeader
|
|
235
229
|
breadcrumbs={[
|
|
236
|
-
{ label: '
|
|
237
|
-
{ label: '
|
|
238
|
-
{ label: '
|
|
230
|
+
{ label: t('breadcrumbs.home'), href: '/' },
|
|
231
|
+
{ label: t('breadcrumbs.lms'), href: '/lms' },
|
|
232
|
+
{ label: t('breadcrumbs.enterprise'), href: '/lms/enterprise' },
|
|
239
233
|
{
|
|
240
234
|
label: isLoading ? '…' : (account?.name ?? ''),
|
|
241
235
|
},
|
|
@@ -243,7 +237,7 @@ export default function EnterpriseDetailPage() {
|
|
|
243
237
|
extraContent={
|
|
244
238
|
account ? (
|
|
245
239
|
<Badge variant={STATUS_VARIANT[account.status]}>
|
|
246
|
-
{
|
|
240
|
+
{t(`status.${account.status}`)}
|
|
247
241
|
</Badge>
|
|
248
242
|
) : (
|
|
249
243
|
<Skeleton className="h-5 w-16 rounded-full" />
|
|
@@ -251,7 +245,7 @@ export default function EnterpriseDetailPage() {
|
|
|
251
245
|
}
|
|
252
246
|
actions={[
|
|
253
247
|
{
|
|
254
|
-
label: '
|
|
248
|
+
label: t('actions.edit'),
|
|
255
249
|
onClick: () => setIsSheetOpen(true),
|
|
256
250
|
icon: <Pencil className="h-4 w-4" />,
|
|
257
251
|
variant: 'outline',
|
|
@@ -268,12 +262,14 @@ export default function EnterpriseDetailPage() {
|
|
|
268
262
|
) : (
|
|
269
263
|
<Tabs defaultValue="overview">
|
|
270
264
|
<TabsList>
|
|
271
|
-
<TabsTrigger value="overview">
|
|
272
|
-
<TabsTrigger value="crm">
|
|
273
|
-
<TabsTrigger value="classes">
|
|
274
|
-
<TabsTrigger value="courses">
|
|
275
|
-
<TabsTrigger value="students">
|
|
276
|
-
<TabsTrigger value="administrators">
|
|
265
|
+
<TabsTrigger value="overview">{t('tabs.overview')}</TabsTrigger>
|
|
266
|
+
<TabsTrigger value="crm">{t('tabs.crm')}</TabsTrigger>
|
|
267
|
+
<TabsTrigger value="classes">{t('tabs.classes')}</TabsTrigger>
|
|
268
|
+
<TabsTrigger value="courses">{t('tabs.courses')}</TabsTrigger>
|
|
269
|
+
<TabsTrigger value="students">{t('tabs.students')}</TabsTrigger>
|
|
270
|
+
<TabsTrigger value="administrators">
|
|
271
|
+
{t('tabs.administrators')}
|
|
272
|
+
</TabsTrigger>
|
|
277
273
|
</TabsList>
|
|
278
274
|
|
|
279
275
|
<TabsContent value="overview" className="mt-6">
|
|
@@ -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}>
|