@hed-hog/lms 0.0.2
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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/lms.module.d.ts +3 -0
- package/dist/lms.module.d.ts.map +1 -0
- package/dist/lms.module.js +28 -0
- package/dist/lms.module.js.map +1 -0
- package/hedhog/data/menu.yaml +12 -0
- package/hedhog/data/role.yaml +7 -0
- package/hedhog/frontend/app/courses/page.tsx.ejs +1394 -0
- package/hedhog/frontend/app/page.tsx.ejs +1222 -0
- package/hedhog/frontend/messages/en.json +10 -0
- package/hedhog/frontend/messages/pt.json +10 -0
- package/package.json +37 -0
- package/src/index.ts +1 -0
- package/src/lms.module.ts +15 -0
|
@@ -0,0 +1,1222 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Page, PageHeader } from '@/components/entity-list';
|
|
4
|
+
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
|
5
|
+
import { Badge } from '@/components/ui/badge';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import {
|
|
8
|
+
Card,
|
|
9
|
+
CardContent,
|
|
10
|
+
CardDescription,
|
|
11
|
+
CardHeader,
|
|
12
|
+
CardTitle,
|
|
13
|
+
} from '@/components/ui/card';
|
|
14
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
15
|
+
import {
|
|
16
|
+
Table,
|
|
17
|
+
TableBody,
|
|
18
|
+
TableCell,
|
|
19
|
+
TableHead,
|
|
20
|
+
TableHeader,
|
|
21
|
+
TableRow,
|
|
22
|
+
} from '@/components/ui/table';
|
|
23
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
24
|
+
import {
|
|
25
|
+
addDays,
|
|
26
|
+
format,
|
|
27
|
+
getDay,
|
|
28
|
+
parse,
|
|
29
|
+
setHours,
|
|
30
|
+
setMinutes,
|
|
31
|
+
startOfWeek,
|
|
32
|
+
} from 'date-fns';
|
|
33
|
+
import { ptBR } from 'date-fns/locale/pt-BR';
|
|
34
|
+
import { motion } from 'framer-motion';
|
|
35
|
+
import {
|
|
36
|
+
Award,
|
|
37
|
+
BarChart3,
|
|
38
|
+
BookOpen,
|
|
39
|
+
CalendarDays,
|
|
40
|
+
CheckCircle2,
|
|
41
|
+
ChevronLeft,
|
|
42
|
+
ChevronRight,
|
|
43
|
+
FileCheck,
|
|
44
|
+
GraduationCap,
|
|
45
|
+
LayoutDashboard,
|
|
46
|
+
Percent,
|
|
47
|
+
TrendingDown,
|
|
48
|
+
TrendingUp,
|
|
49
|
+
Users,
|
|
50
|
+
} from 'lucide-react';
|
|
51
|
+
import { useTranslations } from 'next-intl';
|
|
52
|
+
import { usePathname } from 'next/navigation';
|
|
53
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
54
|
+
import { Calendar, dateFnsLocalizer, type View } from 'react-big-calendar';
|
|
55
|
+
import 'react-big-calendar/lib/css/react-big-calendar.css';
|
|
56
|
+
import {
|
|
57
|
+
Area,
|
|
58
|
+
AreaChart,
|
|
59
|
+
Bar,
|
|
60
|
+
BarChart,
|
|
61
|
+
CartesianGrid,
|
|
62
|
+
Cell,
|
|
63
|
+
Line,
|
|
64
|
+
LineChart,
|
|
65
|
+
Pie,
|
|
66
|
+
PieChart,
|
|
67
|
+
ResponsiveContainer,
|
|
68
|
+
Tooltip,
|
|
69
|
+
XAxis,
|
|
70
|
+
YAxis,
|
|
71
|
+
} from 'recharts';
|
|
72
|
+
|
|
73
|
+
// ── Navigation ─────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
const NAV_ITEMS = [
|
|
76
|
+
{ label: 'Dashboard', href: '/', icon: LayoutDashboard },
|
|
77
|
+
{ label: 'Cursos', href: '/cursos', icon: BookOpen },
|
|
78
|
+
{ label: 'Turmas', href: '/turmas', icon: Users },
|
|
79
|
+
{ label: 'Exames', href: '/exames', icon: FileCheck },
|
|
80
|
+
{ label: 'Formacoes', href: '/formacoes', icon: GraduationCap },
|
|
81
|
+
{ label: 'Relatorios', href: '/relatorios', icon: BarChart3 },
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// ── Big Calendar localizer ─────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
const locales = { 'pt-BR': ptBR };
|
|
87
|
+
|
|
88
|
+
const localizer = dateFnsLocalizer({
|
|
89
|
+
format,
|
|
90
|
+
parse,
|
|
91
|
+
startOfWeek: () => startOfWeek(new Date(), { weekStartsOn: 1 }),
|
|
92
|
+
getDay,
|
|
93
|
+
locales,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ── Mock Data ──────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
const growthData = [
|
|
99
|
+
{ mes: 'Jul', alunos: 1820 },
|
|
100
|
+
{ mes: 'Ago', alunos: 1950 },
|
|
101
|
+
{ mes: 'Set', alunos: 2100 },
|
|
102
|
+
{ mes: 'Out', alunos: 2280 },
|
|
103
|
+
{ mes: 'Nov', alunos: 2540 },
|
|
104
|
+
{ mes: 'Dez', alunos: 2690 },
|
|
105
|
+
{ mes: 'Jan', alunos: 2847 },
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
const engagementData = [
|
|
109
|
+
{ semana: 'S1', acessos: 820, videoAssistidos: 340, exercicios: 150 },
|
|
110
|
+
{ semana: 'S2', acessos: 950, videoAssistidos: 420, exercicios: 200 },
|
|
111
|
+
{ semana: 'S3', acessos: 870, videoAssistidos: 380, exercicios: 180 },
|
|
112
|
+
{ semana: 'S4', acessos: 1100, videoAssistidos: 510, exercicios: 260 },
|
|
113
|
+
{ semana: 'S5', acessos: 1250, videoAssistidos: 580, exercicios: 310 },
|
|
114
|
+
{ semana: 'S6', acessos: 1180, videoAssistidos: 540, exercicios: 290 },
|
|
115
|
+
{ semana: 'S7', acessos: 1350, videoAssistidos: 620, exercicios: 350 },
|
|
116
|
+
{ semana: 'S8', acessos: 1420, videoAssistidos: 680, exercicios: 380 },
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const topCoursesData = [
|
|
120
|
+
{ curso: 'React Avancado', acessos: 847 },
|
|
121
|
+
{ curso: 'Python IA', acessos: 723 },
|
|
122
|
+
{ curso: 'UX Design', acessos: 651 },
|
|
123
|
+
{ curso: 'Node.js API', acessos: 582 },
|
|
124
|
+
{ curso: 'Data Science', acessos: 498 },
|
|
125
|
+
{ curso: 'Flutter', acessos: 412 },
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
const categoryDistribution = [
|
|
129
|
+
{ nome: 'Tecnologia', valor: 38 },
|
|
130
|
+
{ nome: 'Design', valor: 22 },
|
|
131
|
+
{ nome: 'Gestao', valor: 18 },
|
|
132
|
+
{ nome: 'Marketing', valor: 14 },
|
|
133
|
+
{ nome: 'Outros', valor: 8 },
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const PIE_COLORS = [
|
|
137
|
+
'oklch(0.205 0 0)',
|
|
138
|
+
'oklch(0.45 0 0)',
|
|
139
|
+
'oklch(0.60 0 0)',
|
|
140
|
+
'oklch(0.75 0 0)',
|
|
141
|
+
'oklch(0.87 0 0)',
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const today = new Date();
|
|
145
|
+
|
|
146
|
+
function genCalendarEvents() {
|
|
147
|
+
const turmas = [
|
|
148
|
+
{ nome: 'React Avancado - T01', cor: 'oklch(0.205 0 0)' },
|
|
149
|
+
{ nome: 'Python IA - T02', cor: 'oklch(0.45 0 0)' },
|
|
150
|
+
{ nome: 'UX Design - T01', cor: 'oklch(0.60 0 0)' },
|
|
151
|
+
{ nome: 'Node.js API - T03', cor: 'oklch(0.35 0 0)' },
|
|
152
|
+
{ nome: 'Data Science - T01', cor: 'oklch(0.50 0 0)' },
|
|
153
|
+
];
|
|
154
|
+
const events: {
|
|
155
|
+
title: string;
|
|
156
|
+
start: Date;
|
|
157
|
+
end: Date;
|
|
158
|
+
resource: { cor: string };
|
|
159
|
+
}[] = [];
|
|
160
|
+
for (let d = -14; d <= 30; d++) {
|
|
161
|
+
const dia = addDays(today, d);
|
|
162
|
+
const dayOfWeek = dia.getDay();
|
|
163
|
+
if (dayOfWeek === 0 || dayOfWeek === 6) continue;
|
|
164
|
+
const numAulas = dayOfWeek % 2 === 0 ? 3 : 2;
|
|
165
|
+
for (let a = 0; a < numAulas; a++) {
|
|
166
|
+
const turma = turmas[(d + 14 + a) % turmas.length];
|
|
167
|
+
const horaInicio = 8 + a * 3;
|
|
168
|
+
events.push({
|
|
169
|
+
title: turma.nome,
|
|
170
|
+
start: setMinutes(setHours(dia, horaInicio), 0),
|
|
171
|
+
end: setMinutes(setHours(dia, horaInicio + 2), 0),
|
|
172
|
+
resource: { cor: turma.cor },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return events;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const enrollments = [
|
|
180
|
+
{
|
|
181
|
+
id: 1,
|
|
182
|
+
aluno: 'Ana Costa',
|
|
183
|
+
email: 'ana.costa@email.com',
|
|
184
|
+
curso: 'React Avancado',
|
|
185
|
+
data: '01/03/2026',
|
|
186
|
+
status: 'Confirmada',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 2,
|
|
190
|
+
aluno: 'Bruno Silva',
|
|
191
|
+
email: 'bruno.s@email.com',
|
|
192
|
+
curso: 'Python IA',
|
|
193
|
+
data: '28/02/2026',
|
|
194
|
+
status: 'Confirmada',
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
id: 3,
|
|
198
|
+
aluno: 'Carla Santos',
|
|
199
|
+
email: 'carla.s@email.com',
|
|
200
|
+
curso: 'UX Design',
|
|
201
|
+
data: '28/02/2026',
|
|
202
|
+
status: 'Pendente',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: 4,
|
|
206
|
+
aluno: 'Daniel Rocha',
|
|
207
|
+
email: 'daniel.r@email.com',
|
|
208
|
+
curso: 'Node.js API',
|
|
209
|
+
data: '27/02/2026',
|
|
210
|
+
status: 'Confirmada',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: 5,
|
|
214
|
+
aluno: 'Elena Martins',
|
|
215
|
+
email: 'elena.m@email.com',
|
|
216
|
+
curso: 'Data Science',
|
|
217
|
+
data: '27/02/2026',
|
|
218
|
+
status: 'Confirmada',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: 6,
|
|
222
|
+
aluno: 'Fabio Lima',
|
|
223
|
+
email: 'fabio.l@email.com',
|
|
224
|
+
curso: 'Flutter',
|
|
225
|
+
data: '26/02/2026',
|
|
226
|
+
status: 'Pendente',
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: 7,
|
|
230
|
+
aluno: 'Gisele Alves',
|
|
231
|
+
email: 'gisele.a@email.com',
|
|
232
|
+
curso: 'React Avancado',
|
|
233
|
+
data: '26/02/2026',
|
|
234
|
+
status: 'Confirmada',
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: 8,
|
|
238
|
+
aluno: 'Hugo Pereira',
|
|
239
|
+
email: 'hugo.p@email.com',
|
|
240
|
+
curso: 'Python IA',
|
|
241
|
+
data: '25/02/2026',
|
|
242
|
+
status: 'Confirmada',
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: 9,
|
|
246
|
+
aluno: 'Isabela Nunes',
|
|
247
|
+
email: 'isabela.n@email.com',
|
|
248
|
+
curso: 'UX Design',
|
|
249
|
+
data: '25/02/2026',
|
|
250
|
+
status: 'Cancelada',
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
id: 10,
|
|
254
|
+
aluno: 'Joao Mendes',
|
|
255
|
+
email: 'joao.m@email.com',
|
|
256
|
+
curso: 'Data Science',
|
|
257
|
+
data: '24/02/2026',
|
|
258
|
+
status: 'Confirmada',
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: 11,
|
|
262
|
+
aluno: 'Karen Oliveira',
|
|
263
|
+
email: 'karen.o@email.com',
|
|
264
|
+
curso: 'Node.js API',
|
|
265
|
+
data: '24/02/2026',
|
|
266
|
+
status: 'Confirmada',
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
id: 12,
|
|
270
|
+
aluno: 'Lucas Ferreira',
|
|
271
|
+
email: 'lucas.f@email.com',
|
|
272
|
+
curso: 'Flutter',
|
|
273
|
+
data: '23/02/2026',
|
|
274
|
+
status: 'Pendente',
|
|
275
|
+
},
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
const nextClasses = [
|
|
279
|
+
{
|
|
280
|
+
id: 1,
|
|
281
|
+
turma: 'React Avancado - T01',
|
|
282
|
+
disciplina: 'Hooks Avancados',
|
|
283
|
+
professor: 'Prof. Marcos',
|
|
284
|
+
data: '02/03/2026',
|
|
285
|
+
horario: '08:00 - 10:00',
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
id: 2,
|
|
289
|
+
turma: 'Python IA - T02',
|
|
290
|
+
disciplina: 'Redes Neurais',
|
|
291
|
+
professor: 'Prof. Lucia',
|
|
292
|
+
data: '02/03/2026',
|
|
293
|
+
horario: '10:30 - 12:30',
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
id: 3,
|
|
297
|
+
turma: 'UX Design - T01',
|
|
298
|
+
disciplina: 'Prototipagem',
|
|
299
|
+
professor: 'Prof. Renata',
|
|
300
|
+
data: '02/03/2026',
|
|
301
|
+
horario: '14:00 - 16:00',
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
id: 4,
|
|
305
|
+
turma: 'Node.js API - T03',
|
|
306
|
+
disciplina: 'Auth & JWT',
|
|
307
|
+
professor: 'Prof. Andre',
|
|
308
|
+
data: '03/03/2026',
|
|
309
|
+
horario: '08:00 - 10:00',
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: 5,
|
|
313
|
+
turma: 'Data Science - T01',
|
|
314
|
+
disciplina: 'Visualizacao',
|
|
315
|
+
professor: 'Prof. Paulo',
|
|
316
|
+
data: '03/03/2026',
|
|
317
|
+
horario: '10:30 - 12:30',
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: 6,
|
|
321
|
+
turma: 'React Avancado - T01',
|
|
322
|
+
disciplina: 'Context API',
|
|
323
|
+
professor: 'Prof. Marcos',
|
|
324
|
+
data: '04/03/2026',
|
|
325
|
+
horario: '08:00 - 10:00',
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
id: 7,
|
|
329
|
+
turma: 'Python IA - T02',
|
|
330
|
+
disciplina: 'CNN',
|
|
331
|
+
professor: 'Prof. Lucia',
|
|
332
|
+
data: '04/03/2026',
|
|
333
|
+
horario: '10:30 - 12:30',
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
id: 8,
|
|
337
|
+
turma: 'Flutter - T01',
|
|
338
|
+
disciplina: 'Widgets',
|
|
339
|
+
professor: 'Prof. Thiago',
|
|
340
|
+
data: '05/03/2026',
|
|
341
|
+
horario: '14:00 - 16:00',
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
id: 9,
|
|
345
|
+
turma: 'UX Design - T01',
|
|
346
|
+
disciplina: 'Testes A/B',
|
|
347
|
+
professor: 'Prof. Renata',
|
|
348
|
+
data: '05/03/2026',
|
|
349
|
+
horario: '08:00 - 10:00',
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
id: 10,
|
|
353
|
+
turma: 'Node.js API - T03',
|
|
354
|
+
disciplina: 'WebSockets',
|
|
355
|
+
professor: 'Prof. Andre',
|
|
356
|
+
data: '06/03/2026',
|
|
357
|
+
horario: '08:00 - 10:00',
|
|
358
|
+
},
|
|
359
|
+
];
|
|
360
|
+
|
|
361
|
+
// ── Helpers ────────────────────────────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
const ITEMS_PER_PAGE = 5;
|
|
364
|
+
|
|
365
|
+
function getInitials(name: string) {
|
|
366
|
+
return name
|
|
367
|
+
.split(' ')
|
|
368
|
+
.map((n) => n[0])
|
|
369
|
+
.join('')
|
|
370
|
+
.toUpperCase()
|
|
371
|
+
.slice(0, 2);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function statusColor(status: string) {
|
|
375
|
+
if (status === 'Confirmada') return 'bg-emerald-100 text-emerald-700';
|
|
376
|
+
if (status === 'Pendente') return 'bg-amber-100 text-amber-700';
|
|
377
|
+
return 'bg-red-100 text-red-700';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── Chart Tooltip Style ────────────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
const chartTooltipStyle = {
|
|
383
|
+
backgroundColor: 'hsl(var(--card))',
|
|
384
|
+
border: '1px solid hsl(var(--border))',
|
|
385
|
+
borderRadius: '8px',
|
|
386
|
+
fontSize: '12px',
|
|
387
|
+
boxShadow: '0 4px 6px -1px rgba(0,0,0,0.1)',
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
// ── Container Animations ───────────────────────────────────────────────────────
|
|
391
|
+
|
|
392
|
+
const stagger = {
|
|
393
|
+
hidden: {},
|
|
394
|
+
show: { transition: { staggerChildren: 0.08 } },
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const fadeUp = {
|
|
398
|
+
hidden: { opacity: 0, y: 16 },
|
|
399
|
+
show: { opacity: 1, y: 0, transition: { duration: 0.35, ease: 'easeOut' } },
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// ── Component ──────────────────────────────────────────────────────────────────
|
|
403
|
+
|
|
404
|
+
export default function DashboardPage() {
|
|
405
|
+
const t = useTranslations('lms.DashboardPage');
|
|
406
|
+
|
|
407
|
+
const pathname = usePathname();
|
|
408
|
+
const [loading, setLoading] = useState(true);
|
|
409
|
+
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
410
|
+
|
|
411
|
+
// Calendar
|
|
412
|
+
const [calendarView, setCalendarView] = useState<View>('month');
|
|
413
|
+
const [calendarDate, setCalendarDate] = useState(today);
|
|
414
|
+
const calendarEvents = useMemo(() => genCalendarEvents(), []);
|
|
415
|
+
|
|
416
|
+
// Table pagination
|
|
417
|
+
const [enrollPage, setEnrollPage] = useState(1);
|
|
418
|
+
const [classesPage, setClassesPage] = useState(1);
|
|
419
|
+
|
|
420
|
+
useEffect(() => {
|
|
421
|
+
const timer = setTimeout(() => setLoading(false), 1000);
|
|
422
|
+
return () => clearTimeout(timer);
|
|
423
|
+
}, []);
|
|
424
|
+
|
|
425
|
+
const enrollTotalPages = Math.ceil(enrollments.length / ITEMS_PER_PAGE);
|
|
426
|
+
const pagedEnrollments = enrollments.slice(
|
|
427
|
+
(enrollPage - 1) * ITEMS_PER_PAGE,
|
|
428
|
+
enrollPage * ITEMS_PER_PAGE
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
const classesTotalPages = Math.ceil(nextClasses.length / ITEMS_PER_PAGE);
|
|
432
|
+
const pagedClasses = nextClasses.slice(
|
|
433
|
+
(classesPage - 1) * ITEMS_PER_PAGE,
|
|
434
|
+
classesPage * ITEMS_PER_PAGE
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const eventStyleGetter = useCallback(
|
|
438
|
+
(event: { resource?: { cor?: string } }) => ({
|
|
439
|
+
style: {
|
|
440
|
+
backgroundColor: event.resource?.cor ?? 'oklch(0.205 0 0)',
|
|
441
|
+
border: 'none',
|
|
442
|
+
borderRadius: '4px',
|
|
443
|
+
color: '#fff',
|
|
444
|
+
fontSize: '0.75rem',
|
|
445
|
+
padding: '2px 6px',
|
|
446
|
+
},
|
|
447
|
+
}),
|
|
448
|
+
[]
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
const calendarMessages = useMemo(
|
|
452
|
+
() => ({
|
|
453
|
+
today: 'Hoje',
|
|
454
|
+
previous: 'Anterior',
|
|
455
|
+
next: 'Proximo',
|
|
456
|
+
month: 'Mes',
|
|
457
|
+
week: 'Semana',
|
|
458
|
+
day: 'Dia',
|
|
459
|
+
agenda: 'Agenda',
|
|
460
|
+
date: 'Data',
|
|
461
|
+
time: 'Hora',
|
|
462
|
+
event: 'Evento',
|
|
463
|
+
noEventsInRange: 'Sem aulas neste periodo.',
|
|
464
|
+
showMore: (count: number) => `+${count} mais`,
|
|
465
|
+
}),
|
|
466
|
+
[]
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
const kpis = [
|
|
470
|
+
{
|
|
471
|
+
titulo: 'Total de Alunos',
|
|
472
|
+
valor: '2.847',
|
|
473
|
+
variacao: '+12.5%',
|
|
474
|
+
positivo: true,
|
|
475
|
+
icone: Users,
|
|
476
|
+
descricao: 'vs. mes anterior',
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
titulo: 'Cursos Ativos',
|
|
480
|
+
valor: '48',
|
|
481
|
+
variacao: '+3',
|
|
482
|
+
positivo: true,
|
|
483
|
+
icone: BookOpen,
|
|
484
|
+
descricao: 'novos este mes',
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
titulo: 'Turmas Ativas',
|
|
488
|
+
valor: '24',
|
|
489
|
+
variacao: '+5',
|
|
490
|
+
positivo: true,
|
|
491
|
+
icone: CalendarDays,
|
|
492
|
+
descricao: 'em andamento',
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
titulo: 'Certificados Emitidos',
|
|
496
|
+
valor: '1.203',
|
|
497
|
+
variacao: '+87',
|
|
498
|
+
positivo: true,
|
|
499
|
+
icone: Award,
|
|
500
|
+
descricao: 'este mes',
|
|
501
|
+
},
|
|
502
|
+
{
|
|
503
|
+
titulo: 'Taxa de Conclusao',
|
|
504
|
+
valor: '78.3%',
|
|
505
|
+
variacao: '+2.1%',
|
|
506
|
+
positivo: true,
|
|
507
|
+
icone: CheckCircle2,
|
|
508
|
+
descricao: 'media geral',
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
titulo: 'Taxa de Aprovacao',
|
|
512
|
+
valor: '84.7%',
|
|
513
|
+
variacao: '-0.8%',
|
|
514
|
+
positivo: false,
|
|
515
|
+
icone: Percent,
|
|
516
|
+
descricao: 'nos exames',
|
|
517
|
+
},
|
|
518
|
+
];
|
|
519
|
+
|
|
520
|
+
return (
|
|
521
|
+
<Page>
|
|
522
|
+
<PageHeader
|
|
523
|
+
title={t('title')}
|
|
524
|
+
description={t('description')}
|
|
525
|
+
breadcrumbs={[
|
|
526
|
+
{
|
|
527
|
+
label: t('breadcrumbs.home'),
|
|
528
|
+
href: '/',
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
label: t('breadcrumbs.lms'),
|
|
532
|
+
},
|
|
533
|
+
]}
|
|
534
|
+
/>
|
|
535
|
+
|
|
536
|
+
{/* ── Main Content ──────────────────────────────────────────────────── */}
|
|
537
|
+
|
|
538
|
+
<motion.div initial="hidden" animate="show" variants={stagger}>
|
|
539
|
+
{/* ── KPIs ────────────────────────────────────────────────────────── */}
|
|
540
|
+
<div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
|
541
|
+
{kpis.map((kpi, i) => (
|
|
542
|
+
<motion.div key={kpi.titulo} variants={fadeUp}>
|
|
543
|
+
{loading ? (
|
|
544
|
+
<Card>
|
|
545
|
+
<CardContent className="p-5">
|
|
546
|
+
<Skeleton className="mb-3 h-4 w-24" />
|
|
547
|
+
<Skeleton className="mb-2 h-8 w-20" />
|
|
548
|
+
<Skeleton className="h-3 w-32" />
|
|
549
|
+
</CardContent>
|
|
550
|
+
</Card>
|
|
551
|
+
) : (
|
|
552
|
+
<motion.div
|
|
553
|
+
whileHover={{ y: -2 }}
|
|
554
|
+
transition={{ duration: 0.2 }}
|
|
555
|
+
>
|
|
556
|
+
<Card className="transition-shadow hover:shadow-md">
|
|
557
|
+
<CardContent className="p-5">
|
|
558
|
+
<div className="flex items-center justify-between">
|
|
559
|
+
<p className="text-xs font-medium text-muted-foreground">
|
|
560
|
+
{kpi.titulo}
|
|
561
|
+
</p>
|
|
562
|
+
<div className="flex size-8 items-center justify-center rounded-lg bg-muted">
|
|
563
|
+
<kpi.icone className="size-4 text-muted-foreground" />
|
|
564
|
+
</div>
|
|
565
|
+
</div>
|
|
566
|
+
<p className="mt-2 text-2xl font-bold tracking-tight">
|
|
567
|
+
{kpi.valor}
|
|
568
|
+
</p>
|
|
569
|
+
<div className="mt-1.5 flex items-center gap-1.5">
|
|
570
|
+
{kpi.positivo ? (
|
|
571
|
+
<span className="flex items-center gap-0.5 rounded-full bg-emerald-50 px-1.5 py-0.5 text-[11px] font-semibold text-emerald-700">
|
|
572
|
+
<TrendingUp className="size-3" />
|
|
573
|
+
{kpi.variacao}
|
|
574
|
+
</span>
|
|
575
|
+
) : (
|
|
576
|
+
<span className="flex items-center gap-0.5 rounded-full bg-red-50 px-1.5 py-0.5 text-[11px] font-semibold text-red-600">
|
|
577
|
+
<TrendingDown className="size-3" />
|
|
578
|
+
{kpi.variacao}
|
|
579
|
+
</span>
|
|
580
|
+
)}
|
|
581
|
+
<span className="text-[11px] text-muted-foreground">
|
|
582
|
+
{kpi.descricao}
|
|
583
|
+
</span>
|
|
584
|
+
</div>
|
|
585
|
+
</CardContent>
|
|
586
|
+
</Card>
|
|
587
|
+
</motion.div>
|
|
588
|
+
)}
|
|
589
|
+
</motion.div>
|
|
590
|
+
))}
|
|
591
|
+
</div>
|
|
592
|
+
|
|
593
|
+
{/* ── Charts Row 1 ────────────────────────────────────────────────── */}
|
|
594
|
+
<div className="mb-8 grid grid-cols-1 gap-4 lg:grid-cols-7">
|
|
595
|
+
{/* Line chart - Crescimento de alunos */}
|
|
596
|
+
<motion.div className="lg:col-span-4" variants={fadeUp}>
|
|
597
|
+
{loading ? (
|
|
598
|
+
<Card>
|
|
599
|
+
<CardContent className="p-6">
|
|
600
|
+
<Skeleton className="mb-4 h-5 w-48" />
|
|
601
|
+
<Skeleton className="h-[300px] w-full" />
|
|
602
|
+
</CardContent>
|
|
603
|
+
</Card>
|
|
604
|
+
) : (
|
|
605
|
+
<Card>
|
|
606
|
+
<CardHeader className="pb-2">
|
|
607
|
+
<CardTitle className="text-sm font-semibold">
|
|
608
|
+
Crescimento de Alunos
|
|
609
|
+
</CardTitle>
|
|
610
|
+
<CardDescription>
|
|
611
|
+
Evolucao mensal do total de matriculas
|
|
612
|
+
</CardDescription>
|
|
613
|
+
</CardHeader>
|
|
614
|
+
<CardContent>
|
|
615
|
+
<ResponsiveContainer width="100%" height={300}>
|
|
616
|
+
<LineChart data={growthData}>
|
|
617
|
+
<CartesianGrid
|
|
618
|
+
strokeDasharray="3 3"
|
|
619
|
+
stroke="hsl(var(--border))"
|
|
620
|
+
/>
|
|
621
|
+
<XAxis
|
|
622
|
+
dataKey="mes"
|
|
623
|
+
fontSize={12}
|
|
624
|
+
tickLine={false}
|
|
625
|
+
axisLine={false}
|
|
626
|
+
/>
|
|
627
|
+
<YAxis fontSize={12} tickLine={false} axisLine={false} />
|
|
628
|
+
<Tooltip contentStyle={chartTooltipStyle} />
|
|
629
|
+
<Line
|
|
630
|
+
type="monotone"
|
|
631
|
+
dataKey="alunos"
|
|
632
|
+
stroke="oklch(0.205 0 0)"
|
|
633
|
+
strokeWidth={2.5}
|
|
634
|
+
dot={{
|
|
635
|
+
r: 4,
|
|
636
|
+
fill: 'oklch(0.205 0 0)',
|
|
637
|
+
strokeWidth: 2,
|
|
638
|
+
stroke: '#fff',
|
|
639
|
+
}}
|
|
640
|
+
activeDot={{ r: 6 }}
|
|
641
|
+
name="Alunos"
|
|
642
|
+
/>
|
|
643
|
+
</LineChart>
|
|
644
|
+
</ResponsiveContainer>
|
|
645
|
+
</CardContent>
|
|
646
|
+
</Card>
|
|
647
|
+
)}
|
|
648
|
+
</motion.div>
|
|
649
|
+
|
|
650
|
+
{/* Area chart - Engajamento */}
|
|
651
|
+
<motion.div className="lg:col-span-3" variants={fadeUp}>
|
|
652
|
+
{loading ? (
|
|
653
|
+
<Card>
|
|
654
|
+
<CardContent className="p-6">
|
|
655
|
+
<Skeleton className="mb-4 h-5 w-40" />
|
|
656
|
+
<Skeleton className="h-[300px] w-full" />
|
|
657
|
+
</CardContent>
|
|
658
|
+
</Card>
|
|
659
|
+
) : (
|
|
660
|
+
<Card>
|
|
661
|
+
<CardHeader className="pb-2">
|
|
662
|
+
<CardTitle className="text-sm font-semibold">
|
|
663
|
+
Engajamento
|
|
664
|
+
</CardTitle>
|
|
665
|
+
<CardDescription>
|
|
666
|
+
Acessos, videos e exercicios por semana
|
|
667
|
+
</CardDescription>
|
|
668
|
+
</CardHeader>
|
|
669
|
+
<CardContent>
|
|
670
|
+
<ResponsiveContainer width="100%" height={300}>
|
|
671
|
+
<AreaChart data={engagementData}>
|
|
672
|
+
<defs>
|
|
673
|
+
<linearGradient
|
|
674
|
+
id="colorAcessos"
|
|
675
|
+
x1="0"
|
|
676
|
+
y1="0"
|
|
677
|
+
x2="0"
|
|
678
|
+
y2="1"
|
|
679
|
+
>
|
|
680
|
+
<stop
|
|
681
|
+
offset="5%"
|
|
682
|
+
stopColor="oklch(0.205 0 0)"
|
|
683
|
+
stopOpacity={0.15}
|
|
684
|
+
/>
|
|
685
|
+
<stop
|
|
686
|
+
offset="95%"
|
|
687
|
+
stopColor="oklch(0.205 0 0)"
|
|
688
|
+
stopOpacity={0}
|
|
689
|
+
/>
|
|
690
|
+
</linearGradient>
|
|
691
|
+
<linearGradient
|
|
692
|
+
id="colorVideos"
|
|
693
|
+
x1="0"
|
|
694
|
+
y1="0"
|
|
695
|
+
x2="0"
|
|
696
|
+
y2="1"
|
|
697
|
+
>
|
|
698
|
+
<stop
|
|
699
|
+
offset="5%"
|
|
700
|
+
stopColor="oklch(0.55 0 0)"
|
|
701
|
+
stopOpacity={0.15}
|
|
702
|
+
/>
|
|
703
|
+
<stop
|
|
704
|
+
offset="95%"
|
|
705
|
+
stopColor="oklch(0.55 0 0)"
|
|
706
|
+
stopOpacity={0}
|
|
707
|
+
/>
|
|
708
|
+
</linearGradient>
|
|
709
|
+
</defs>
|
|
710
|
+
<CartesianGrid
|
|
711
|
+
strokeDasharray="3 3"
|
|
712
|
+
stroke="hsl(var(--border))"
|
|
713
|
+
/>
|
|
714
|
+
<XAxis
|
|
715
|
+
dataKey="semana"
|
|
716
|
+
fontSize={12}
|
|
717
|
+
tickLine={false}
|
|
718
|
+
axisLine={false}
|
|
719
|
+
/>
|
|
720
|
+
<YAxis fontSize={12} tickLine={false} axisLine={false} />
|
|
721
|
+
<Tooltip contentStyle={chartTooltipStyle} />
|
|
722
|
+
<Area
|
|
723
|
+
type="monotone"
|
|
724
|
+
dataKey="acessos"
|
|
725
|
+
stroke="oklch(0.205 0 0)"
|
|
726
|
+
strokeWidth={2}
|
|
727
|
+
fill="url(#colorAcessos)"
|
|
728
|
+
name="Acessos"
|
|
729
|
+
/>
|
|
730
|
+
<Area
|
|
731
|
+
type="monotone"
|
|
732
|
+
dataKey="videoAssistidos"
|
|
733
|
+
stroke="oklch(0.55 0 0)"
|
|
734
|
+
strokeWidth={2}
|
|
735
|
+
fill="url(#colorVideos)"
|
|
736
|
+
name="Videos"
|
|
737
|
+
/>
|
|
738
|
+
<Area
|
|
739
|
+
type="monotone"
|
|
740
|
+
dataKey="exercicios"
|
|
741
|
+
stroke="oklch(0.75 0 0)"
|
|
742
|
+
strokeWidth={1.5}
|
|
743
|
+
fill="none"
|
|
744
|
+
name="Exercicios"
|
|
745
|
+
/>
|
|
746
|
+
</AreaChart>
|
|
747
|
+
</ResponsiveContainer>
|
|
748
|
+
</CardContent>
|
|
749
|
+
</Card>
|
|
750
|
+
)}
|
|
751
|
+
</motion.div>
|
|
752
|
+
</div>
|
|
753
|
+
|
|
754
|
+
{/* ── Charts Row 2 ────────────────────────────────────────────────── */}
|
|
755
|
+
<div className="mb-8 grid grid-cols-1 gap-4 lg:grid-cols-7">
|
|
756
|
+
{/* Bar chart - Cursos mais acessados */}
|
|
757
|
+
<motion.div className="lg:col-span-4" variants={fadeUp}>
|
|
758
|
+
{loading ? (
|
|
759
|
+
<Card>
|
|
760
|
+
<CardContent className="p-6">
|
|
761
|
+
<Skeleton className="mb-4 h-5 w-48" />
|
|
762
|
+
<Skeleton className="h-[300px] w-full" />
|
|
763
|
+
</CardContent>
|
|
764
|
+
</Card>
|
|
765
|
+
) : (
|
|
766
|
+
<Card>
|
|
767
|
+
<CardHeader className="pb-2">
|
|
768
|
+
<CardTitle className="text-sm font-semibold">
|
|
769
|
+
Cursos Mais Acessados
|
|
770
|
+
</CardTitle>
|
|
771
|
+
<CardDescription>
|
|
772
|
+
Ranking por numero de acessos este mes
|
|
773
|
+
</CardDescription>
|
|
774
|
+
</CardHeader>
|
|
775
|
+
<CardContent>
|
|
776
|
+
<ResponsiveContainer width="100%" height={300}>
|
|
777
|
+
<BarChart data={topCoursesData} layout="vertical">
|
|
778
|
+
<CartesianGrid
|
|
779
|
+
strokeDasharray="3 3"
|
|
780
|
+
stroke="hsl(var(--border))"
|
|
781
|
+
horizontal={false}
|
|
782
|
+
/>
|
|
783
|
+
<XAxis
|
|
784
|
+
type="number"
|
|
785
|
+
fontSize={12}
|
|
786
|
+
tickLine={false}
|
|
787
|
+
axisLine={false}
|
|
788
|
+
/>
|
|
789
|
+
<YAxis
|
|
790
|
+
type="category"
|
|
791
|
+
dataKey="curso"
|
|
792
|
+
fontSize={12}
|
|
793
|
+
tickLine={false}
|
|
794
|
+
axisLine={false}
|
|
795
|
+
width={110}
|
|
796
|
+
/>
|
|
797
|
+
<Tooltip contentStyle={chartTooltipStyle} />
|
|
798
|
+
<Bar
|
|
799
|
+
dataKey="acessos"
|
|
800
|
+
fill="oklch(0.205 0 0)"
|
|
801
|
+
radius={[0, 4, 4, 0]}
|
|
802
|
+
name="Acessos"
|
|
803
|
+
barSize={24}
|
|
804
|
+
/>
|
|
805
|
+
</BarChart>
|
|
806
|
+
</ResponsiveContainer>
|
|
807
|
+
</CardContent>
|
|
808
|
+
</Card>
|
|
809
|
+
)}
|
|
810
|
+
</motion.div>
|
|
811
|
+
|
|
812
|
+
{/* Pie chart - Distribuicao por categoria */}
|
|
813
|
+
<motion.div className="lg:col-span-3" variants={fadeUp}>
|
|
814
|
+
{loading ? (
|
|
815
|
+
<Card>
|
|
816
|
+
<CardContent className="p-6">
|
|
817
|
+
<Skeleton className="mb-4 h-5 w-48" />
|
|
818
|
+
<Skeleton className="mx-auto h-[220px] w-[220px] rounded-full" />
|
|
819
|
+
</CardContent>
|
|
820
|
+
</Card>
|
|
821
|
+
) : (
|
|
822
|
+
<Card>
|
|
823
|
+
<CardHeader className="pb-2">
|
|
824
|
+
<CardTitle className="text-sm font-semibold">
|
|
825
|
+
Distribuicao por Categoria
|
|
826
|
+
</CardTitle>
|
|
827
|
+
<CardDescription>
|
|
828
|
+
Percentual de cursos ativos por area
|
|
829
|
+
</CardDescription>
|
|
830
|
+
</CardHeader>
|
|
831
|
+
<CardContent className="flex flex-col items-center">
|
|
832
|
+
<ResponsiveContainer width="100%" height={220}>
|
|
833
|
+
<PieChart>
|
|
834
|
+
<Pie
|
|
835
|
+
data={categoryDistribution}
|
|
836
|
+
cx="50%"
|
|
837
|
+
cy="50%"
|
|
838
|
+
innerRadius={55}
|
|
839
|
+
outerRadius={85}
|
|
840
|
+
dataKey="valor"
|
|
841
|
+
nameKey="nome"
|
|
842
|
+
strokeWidth={3}
|
|
843
|
+
stroke="hsl(var(--background))"
|
|
844
|
+
>
|
|
845
|
+
{categoryDistribution.map((_, index) => (
|
|
846
|
+
<Cell
|
|
847
|
+
key={`cell-${index}`}
|
|
848
|
+
fill={PIE_COLORS[index]}
|
|
849
|
+
/>
|
|
850
|
+
))}
|
|
851
|
+
</Pie>
|
|
852
|
+
<Tooltip
|
|
853
|
+
contentStyle={chartTooltipStyle}
|
|
854
|
+
formatter={(value: number) => [
|
|
855
|
+
`${value}%`,
|
|
856
|
+
'Percentual',
|
|
857
|
+
]}
|
|
858
|
+
/>
|
|
859
|
+
</PieChart>
|
|
860
|
+
</ResponsiveContainer>
|
|
861
|
+
<div className="mt-3 flex flex-wrap justify-center gap-x-4 gap-y-2">
|
|
862
|
+
{categoryDistribution.map((item, i) => (
|
|
863
|
+
<div
|
|
864
|
+
key={item.nome}
|
|
865
|
+
className="flex items-center gap-1.5 text-xs"
|
|
866
|
+
>
|
|
867
|
+
<span
|
|
868
|
+
className="inline-block size-2.5 rounded-full"
|
|
869
|
+
style={{ backgroundColor: PIE_COLORS[i] }}
|
|
870
|
+
/>
|
|
871
|
+
<span className="text-muted-foreground">
|
|
872
|
+
{item.nome} ({item.valor}%)
|
|
873
|
+
</span>
|
|
874
|
+
</div>
|
|
875
|
+
))}
|
|
876
|
+
</div>
|
|
877
|
+
</CardContent>
|
|
878
|
+
</Card>
|
|
879
|
+
)}
|
|
880
|
+
</motion.div>
|
|
881
|
+
</div>
|
|
882
|
+
|
|
883
|
+
{/* ── Calendar ────────────────────────────────────────────────────── */}
|
|
884
|
+
<motion.div className="mb-8" variants={fadeUp}>
|
|
885
|
+
{loading ? (
|
|
886
|
+
<Card>
|
|
887
|
+
<CardContent className="p-6">
|
|
888
|
+
<Skeleton className="mb-4 h-5 w-40" />
|
|
889
|
+
<Skeleton className="h-[500px] w-full rounded-lg" />
|
|
890
|
+
</CardContent>
|
|
891
|
+
</Card>
|
|
892
|
+
) : (
|
|
893
|
+
<Card>
|
|
894
|
+
<CardHeader className="pb-2">
|
|
895
|
+
<CardTitle className="text-sm font-semibold">
|
|
896
|
+
Calendario Global de Aulas
|
|
897
|
+
</CardTitle>
|
|
898
|
+
<CardDescription>
|
|
899
|
+
Todas as aulas de todas as turmas
|
|
900
|
+
</CardDescription>
|
|
901
|
+
</CardHeader>
|
|
902
|
+
<CardContent>
|
|
903
|
+
<div className="h-[520px] lg:h-[580px]">
|
|
904
|
+
<Calendar
|
|
905
|
+
localizer={localizer}
|
|
906
|
+
events={calendarEvents}
|
|
907
|
+
startAccessor="start"
|
|
908
|
+
endAccessor="end"
|
|
909
|
+
view={calendarView}
|
|
910
|
+
onView={(v) => setCalendarView(v)}
|
|
911
|
+
date={calendarDate}
|
|
912
|
+
onNavigate={(d) => setCalendarDate(d)}
|
|
913
|
+
views={['month', 'week']}
|
|
914
|
+
messages={calendarMessages}
|
|
915
|
+
eventPropGetter={eventStyleGetter}
|
|
916
|
+
culture="pt-BR"
|
|
917
|
+
popup
|
|
918
|
+
style={{ height: '100%' }}
|
|
919
|
+
/>
|
|
920
|
+
</div>
|
|
921
|
+
</CardContent>
|
|
922
|
+
</Card>
|
|
923
|
+
)}
|
|
924
|
+
</motion.div>
|
|
925
|
+
|
|
926
|
+
{/* ── Tables ──────────────────────────────────────────────────────── */}
|
|
927
|
+
<motion.div className="mb-8" variants={fadeUp}>
|
|
928
|
+
<Tabs defaultValue="matriculas">
|
|
929
|
+
<TabsList className="mb-4">
|
|
930
|
+
<TabsTrigger value="matriculas">Ultimas Matriculas</TabsTrigger>
|
|
931
|
+
<TabsTrigger value="aulas">Proximas Aulas</TabsTrigger>
|
|
932
|
+
</TabsList>
|
|
933
|
+
|
|
934
|
+
{/* ─ Tab: Matriculas ─ */}
|
|
935
|
+
<TabsContent value="matriculas">
|
|
936
|
+
{loading ? (
|
|
937
|
+
<Card>
|
|
938
|
+
<CardContent className="p-6">
|
|
939
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
940
|
+
<div key={i} className="mb-4 flex items-center gap-4">
|
|
941
|
+
<Skeleton className="size-10 rounded-full" />
|
|
942
|
+
<div className="flex-1">
|
|
943
|
+
<Skeleton className="mb-2 h-4 w-40" />
|
|
944
|
+
<Skeleton className="h-3 w-24" />
|
|
945
|
+
</div>
|
|
946
|
+
<Skeleton className="h-6 w-20" />
|
|
947
|
+
</div>
|
|
948
|
+
))}
|
|
949
|
+
</CardContent>
|
|
950
|
+
</Card>
|
|
951
|
+
) : (
|
|
952
|
+
<Card>
|
|
953
|
+
<CardHeader className="pb-2">
|
|
954
|
+
<CardTitle className="text-sm font-semibold">
|
|
955
|
+
Ultimas Matriculas
|
|
956
|
+
</CardTitle>
|
|
957
|
+
<CardDescription>
|
|
958
|
+
{enrollments.length} matriculas recentes
|
|
959
|
+
</CardDescription>
|
|
960
|
+
</CardHeader>
|
|
961
|
+
<CardContent>
|
|
962
|
+
<div className="overflow-x-auto">
|
|
963
|
+
<Table>
|
|
964
|
+
<TableHeader>
|
|
965
|
+
<TableRow>
|
|
966
|
+
<TableHead>Aluno</TableHead>
|
|
967
|
+
<TableHead className="hidden sm:table-cell">
|
|
968
|
+
Email
|
|
969
|
+
</TableHead>
|
|
970
|
+
<TableHead>Curso</TableHead>
|
|
971
|
+
<TableHead className="hidden md:table-cell">
|
|
972
|
+
Data
|
|
973
|
+
</TableHead>
|
|
974
|
+
<TableHead>Status</TableHead>
|
|
975
|
+
</TableRow>
|
|
976
|
+
</TableHeader>
|
|
977
|
+
<TableBody>
|
|
978
|
+
{pagedEnrollments.map((m) => (
|
|
979
|
+
<TableRow key={m.id}>
|
|
980
|
+
<TableCell>
|
|
981
|
+
<div className="flex items-center gap-2.5">
|
|
982
|
+
<Avatar className="size-8">
|
|
983
|
+
<AvatarFallback className="bg-foreground text-[10px] font-medium text-background">
|
|
984
|
+
{getInitials(m.aluno)}
|
|
985
|
+
</AvatarFallback>
|
|
986
|
+
</Avatar>
|
|
987
|
+
<span className="font-medium">{m.aluno}</span>
|
|
988
|
+
</div>
|
|
989
|
+
</TableCell>
|
|
990
|
+
<TableCell className="hidden text-muted-foreground sm:table-cell">
|
|
991
|
+
{m.email}
|
|
992
|
+
</TableCell>
|
|
993
|
+
<TableCell>{m.curso}</TableCell>
|
|
994
|
+
<TableCell className="hidden text-muted-foreground md:table-cell">
|
|
995
|
+
{m.data}
|
|
996
|
+
</TableCell>
|
|
997
|
+
<TableCell>
|
|
998
|
+
<span
|
|
999
|
+
className={`inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-semibold ${statusColor(m.status)}`}
|
|
1000
|
+
>
|
|
1001
|
+
{m.status}
|
|
1002
|
+
</span>
|
|
1003
|
+
</TableCell>
|
|
1004
|
+
</TableRow>
|
|
1005
|
+
))}
|
|
1006
|
+
</TableBody>
|
|
1007
|
+
</Table>
|
|
1008
|
+
</div>
|
|
1009
|
+
|
|
1010
|
+
{/* Pagination */}
|
|
1011
|
+
<div className="mt-4 flex items-center justify-between border-t pt-4">
|
|
1012
|
+
<p className="text-xs text-muted-foreground">
|
|
1013
|
+
Mostrando {(enrollPage - 1) * ITEMS_PER_PAGE + 1} -{' '}
|
|
1014
|
+
{Math.min(
|
|
1015
|
+
enrollPage * ITEMS_PER_PAGE,
|
|
1016
|
+
enrollments.length
|
|
1017
|
+
)}{' '}
|
|
1018
|
+
de {enrollments.length}
|
|
1019
|
+
</p>
|
|
1020
|
+
<div className="flex items-center gap-1">
|
|
1021
|
+
<Button
|
|
1022
|
+
variant="outline"
|
|
1023
|
+
size="icon"
|
|
1024
|
+
className="size-8"
|
|
1025
|
+
disabled={enrollPage === 1}
|
|
1026
|
+
onClick={() => setEnrollPage((p) => p - 1)}
|
|
1027
|
+
aria-label="Pagina anterior"
|
|
1028
|
+
>
|
|
1029
|
+
<ChevronLeft className="size-4" />
|
|
1030
|
+
</Button>
|
|
1031
|
+
{Array.from({ length: enrollTotalPages }).map(
|
|
1032
|
+
(_, i) => (
|
|
1033
|
+
<Button
|
|
1034
|
+
key={i}
|
|
1035
|
+
variant={
|
|
1036
|
+
enrollPage === i + 1 ? 'default' : 'outline'
|
|
1037
|
+
}
|
|
1038
|
+
size="icon"
|
|
1039
|
+
className="size-8 text-xs"
|
|
1040
|
+
onClick={() => setEnrollPage(i + 1)}
|
|
1041
|
+
>
|
|
1042
|
+
{i + 1}
|
|
1043
|
+
</Button>
|
|
1044
|
+
)
|
|
1045
|
+
)}
|
|
1046
|
+
<Button
|
|
1047
|
+
variant="outline"
|
|
1048
|
+
size="icon"
|
|
1049
|
+
className="size-8"
|
|
1050
|
+
disabled={enrollPage === enrollTotalPages}
|
|
1051
|
+
onClick={() => setEnrollPage((p) => p + 1)}
|
|
1052
|
+
aria-label="Proxima pagina"
|
|
1053
|
+
>
|
|
1054
|
+
<ChevronRight className="size-4" />
|
|
1055
|
+
</Button>
|
|
1056
|
+
</div>
|
|
1057
|
+
</div>
|
|
1058
|
+
</CardContent>
|
|
1059
|
+
</Card>
|
|
1060
|
+
)}
|
|
1061
|
+
</TabsContent>
|
|
1062
|
+
|
|
1063
|
+
{/* ─ Tab: Proximas Aulas ─ */}
|
|
1064
|
+
<TabsContent value="aulas">
|
|
1065
|
+
{loading ? (
|
|
1066
|
+
<Card>
|
|
1067
|
+
<CardContent className="p-6">
|
|
1068
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
1069
|
+
<div key={i} className="mb-4 flex items-center gap-4">
|
|
1070
|
+
<Skeleton className="size-8 rounded-lg" />
|
|
1071
|
+
<div className="flex-1">
|
|
1072
|
+
<Skeleton className="mb-2 h-4 w-48" />
|
|
1073
|
+
<Skeleton className="h-3 w-32" />
|
|
1074
|
+
</div>
|
|
1075
|
+
<Skeleton className="h-6 w-28" />
|
|
1076
|
+
</div>
|
|
1077
|
+
))}
|
|
1078
|
+
</CardContent>
|
|
1079
|
+
</Card>
|
|
1080
|
+
) : (
|
|
1081
|
+
<Card>
|
|
1082
|
+
<CardHeader className="pb-2">
|
|
1083
|
+
<CardTitle className="text-sm font-semibold">
|
|
1084
|
+
Proximas Aulas
|
|
1085
|
+
</CardTitle>
|
|
1086
|
+
<CardDescription>
|
|
1087
|
+
Agenda das proximas aulas programadas
|
|
1088
|
+
</CardDescription>
|
|
1089
|
+
</CardHeader>
|
|
1090
|
+
<CardContent>
|
|
1091
|
+
<div className="overflow-x-auto">
|
|
1092
|
+
<Table>
|
|
1093
|
+
<TableHeader>
|
|
1094
|
+
<TableRow>
|
|
1095
|
+
<TableHead>Turma</TableHead>
|
|
1096
|
+
<TableHead>Disciplina</TableHead>
|
|
1097
|
+
<TableHead className="hidden sm:table-cell">
|
|
1098
|
+
Professor
|
|
1099
|
+
</TableHead>
|
|
1100
|
+
<TableHead>Data</TableHead>
|
|
1101
|
+
<TableHead className="hidden md:table-cell">
|
|
1102
|
+
Horario
|
|
1103
|
+
</TableHead>
|
|
1104
|
+
</TableRow>
|
|
1105
|
+
</TableHeader>
|
|
1106
|
+
<TableBody>
|
|
1107
|
+
{pagedClasses.map((aula) => (
|
|
1108
|
+
<TableRow key={aula.id}>
|
|
1109
|
+
<TableCell>
|
|
1110
|
+
<div className="flex items-center gap-2">
|
|
1111
|
+
<div className="flex size-8 items-center justify-center rounded-lg bg-muted">
|
|
1112
|
+
<BookOpen className="size-3.5 text-muted-foreground" />
|
|
1113
|
+
</div>
|
|
1114
|
+
<span className="font-medium">
|
|
1115
|
+
{aula.turma}
|
|
1116
|
+
</span>
|
|
1117
|
+
</div>
|
|
1118
|
+
</TableCell>
|
|
1119
|
+
<TableCell>{aula.disciplina}</TableCell>
|
|
1120
|
+
<TableCell className="hidden text-muted-foreground sm:table-cell">
|
|
1121
|
+
{aula.professor}
|
|
1122
|
+
</TableCell>
|
|
1123
|
+
<TableCell>
|
|
1124
|
+
<Badge
|
|
1125
|
+
variant="outline"
|
|
1126
|
+
className="text-xs font-normal"
|
|
1127
|
+
>
|
|
1128
|
+
{aula.data}
|
|
1129
|
+
</Badge>
|
|
1130
|
+
</TableCell>
|
|
1131
|
+
<TableCell className="hidden text-muted-foreground md:table-cell">
|
|
1132
|
+
{aula.horario}
|
|
1133
|
+
</TableCell>
|
|
1134
|
+
</TableRow>
|
|
1135
|
+
))}
|
|
1136
|
+
</TableBody>
|
|
1137
|
+
</Table>
|
|
1138
|
+
</div>
|
|
1139
|
+
|
|
1140
|
+
{/* Pagination */}
|
|
1141
|
+
<div className="mt-4 flex items-center justify-between border-t pt-4">
|
|
1142
|
+
<p className="text-xs text-muted-foreground">
|
|
1143
|
+
Mostrando {(classesPage - 1) * ITEMS_PER_PAGE + 1} -{' '}
|
|
1144
|
+
{Math.min(
|
|
1145
|
+
classesPage * ITEMS_PER_PAGE,
|
|
1146
|
+
nextClasses.length
|
|
1147
|
+
)}{' '}
|
|
1148
|
+
de {nextClasses.length}
|
|
1149
|
+
</p>
|
|
1150
|
+
<div className="flex items-center gap-1">
|
|
1151
|
+
<Button
|
|
1152
|
+
variant="outline"
|
|
1153
|
+
size="icon"
|
|
1154
|
+
className="size-8"
|
|
1155
|
+
disabled={classesPage === 1}
|
|
1156
|
+
onClick={() => setClassesPage((p) => p - 1)}
|
|
1157
|
+
aria-label="Pagina anterior"
|
|
1158
|
+
>
|
|
1159
|
+
<ChevronLeft className="size-4" />
|
|
1160
|
+
</Button>
|
|
1161
|
+
{Array.from({ length: classesTotalPages }).map(
|
|
1162
|
+
(_, i) => (
|
|
1163
|
+
<Button
|
|
1164
|
+
key={i}
|
|
1165
|
+
variant={
|
|
1166
|
+
classesPage === i + 1 ? 'default' : 'outline'
|
|
1167
|
+
}
|
|
1168
|
+
size="icon"
|
|
1169
|
+
className="size-8 text-xs"
|
|
1170
|
+
onClick={() => setClassesPage(i + 1)}
|
|
1171
|
+
>
|
|
1172
|
+
{i + 1}
|
|
1173
|
+
</Button>
|
|
1174
|
+
)
|
|
1175
|
+
)}
|
|
1176
|
+
<Button
|
|
1177
|
+
variant="outline"
|
|
1178
|
+
size="icon"
|
|
1179
|
+
className="size-8"
|
|
1180
|
+
disabled={classesPage === classesTotalPages}
|
|
1181
|
+
onClick={() => setClassesPage((p) => p + 1)}
|
|
1182
|
+
aria-label="Proxima pagina"
|
|
1183
|
+
>
|
|
1184
|
+
<ChevronRight className="size-4" />
|
|
1185
|
+
</Button>
|
|
1186
|
+
</div>
|
|
1187
|
+
</div>
|
|
1188
|
+
</CardContent>
|
|
1189
|
+
</Card>
|
|
1190
|
+
)}
|
|
1191
|
+
</TabsContent>
|
|
1192
|
+
</Tabs>
|
|
1193
|
+
</motion.div>
|
|
1194
|
+
|
|
1195
|
+
{/* ── Footer status ──────────────────────────────────────────────── */}
|
|
1196
|
+
{!loading && (
|
|
1197
|
+
<motion.div variants={fadeUp}>
|
|
1198
|
+
<Card>
|
|
1199
|
+
<CardContent className="flex flex-wrap items-center justify-between gap-4 p-4">
|
|
1200
|
+
<div className="flex items-center gap-2.5">
|
|
1201
|
+
<Badge
|
|
1202
|
+
variant="secondary"
|
|
1203
|
+
className="gap-1.5 text-xs font-medium"
|
|
1204
|
+
>
|
|
1205
|
+
<span className="inline-block size-2 rounded-full bg-emerald-500" />
|
|
1206
|
+
Sistema Operacional
|
|
1207
|
+
</Badge>
|
|
1208
|
+
</div>
|
|
1209
|
+
<div className="flex flex-wrap gap-x-6 gap-y-1 text-xs text-muted-foreground">
|
|
1210
|
+
<span>24 turmas ativas</span>
|
|
1211
|
+
<span>48 cursos publicados</span>
|
|
1212
|
+
<span>2.847 alunos matriculados</span>
|
|
1213
|
+
<span>1.203 certificados</span>
|
|
1214
|
+
</div>
|
|
1215
|
+
</CardContent>
|
|
1216
|
+
</Card>
|
|
1217
|
+
</motion.div>
|
|
1218
|
+
)}
|
|
1219
|
+
</motion.div>
|
|
1220
|
+
</Page>
|
|
1221
|
+
);
|
|
1222
|
+
}
|