@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.
@@ -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
+ }