@hed-hog/lms 0.0.265 → 0.0.267

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,910 @@
1
+ 'use client';
2
+
3
+ import { Page, PageHeader } from '@/components/entity-list';
4
+ import { Badge } from '@/components/ui/badge';
5
+ import { Button } from '@/components/ui/button';
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from '@/components/ui/card';
13
+ import {
14
+ Select,
15
+ SelectContent,
16
+ SelectItem,
17
+ SelectTrigger,
18
+ SelectValue,
19
+ } from '@/components/ui/select';
20
+ import { Skeleton } from '@/components/ui/skeleton';
21
+ import {
22
+ Table,
23
+ TableBody,
24
+ TableCell,
25
+ TableHead,
26
+ TableHeader,
27
+ TableRow,
28
+ } from '@/components/ui/table';
29
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
30
+ import { motion } from 'framer-motion';
31
+ import { ArrowUpRight, Download, TrendingDown, TrendingUp } from 'lucide-react';
32
+ import { useTranslations } from 'next-intl';
33
+ import { useEffect, useState } from 'react';
34
+ import {
35
+ Area,
36
+ AreaChart,
37
+ Bar,
38
+ BarChart,
39
+ CartesianGrid,
40
+ Cell,
41
+ Line,
42
+ LineChart,
43
+ Pie,
44
+ PieChart,
45
+ PolarAngleAxis,
46
+ PolarGrid,
47
+ PolarRadiusAxis,
48
+ Radar,
49
+ RadarChart,
50
+ ResponsiveContainer,
51
+ Tooltip,
52
+ XAxis,
53
+ YAxis,
54
+ } from 'recharts';
55
+ import { toast } from 'sonner';
56
+
57
+ // Data for charts
58
+ const monthlyEnrollments = [
59
+ { mes: 'jan', matriculas: 120, cancelamentos: 15, receita: 48000 },
60
+ { mes: 'feb', matriculas: 150, cancelamentos: 12, receita: 60000 },
61
+ { mes: 'mar', matriculas: 180, cancelamentos: 18, receita: 72000 },
62
+ { mes: 'apr', matriculas: 200, cancelamentos: 10, receita: 80000 },
63
+ { mes: 'may', matriculas: 170, cancelamentos: 22, receita: 68000 },
64
+ { mes: 'jun', matriculas: 220, cancelamentos: 8, receita: 88000 },
65
+ { mes: 'jul', matriculas: 190, cancelamentos: 14, receita: 76000 },
66
+ { mes: 'aug', matriculas: 240, cancelamentos: 11, receita: 96000 },
67
+ { mes: 'sep', matriculas: 210, cancelamentos: 16, receita: 84000 },
68
+ { mes: 'oct', matriculas: 260, cancelamentos: 9, receita: 104000 },
69
+ { mes: 'nov', matriculas: 230, cancelamentos: 13, receita: 92000 },
70
+ { mes: 'dec', matriculas: 280, cancelamentos: 7, receita: 112000 },
71
+ ];
72
+
73
+ const categoryPerformance = [
74
+ { area: 'technology', media: 7.8, alunos: 1200, conclusao: 82 },
75
+ { area: 'design', media: 8.2, alunos: 650, conclusao: 88 },
76
+ { area: 'management', media: 7.5, alunos: 480, conclusao: 76 },
77
+ { area: 'marketing', media: 7.9, alunos: 320, conclusao: 79 },
78
+ ];
79
+
80
+ const courseRanking = [
81
+ { nome: 'reactAdvanced', alunos: 245, conclusao: 85, nota: 8.1 },
82
+ { nome: 'excelBusiness', alunos: 534, conclusao: 92, nota: 7.9 },
83
+ { nome: 'agileProjectManagement', alunos: 312, conclusao: 78, nota: 7.5 },
84
+ { nome: 'pythonDataScience', alunos: 178, conclusao: 72, nota: 7.8 },
85
+ { nome: 'uxDesignFundamentals', alunos: 189, conclusao: 88, nota: 8.3 },
86
+ { nome: 'typescriptPractice', alunos: 201, conclusao: 80, nota: 7.6 },
87
+ { nome: 'nodeComplete', alunos: 156, conclusao: 75, nota: 7.4 },
88
+ { nome: 'designSystem', alunos: 87, conclusao: 90, nota: 8.5 },
89
+ ];
90
+
91
+ const radarData = [
92
+ { subject: 'engagement', A: 85, fullMark: 100 },
93
+ { subject: 'completion', A: 78, fullMark: 100 },
94
+ { subject: 'satisfaction', A: 92, fullMark: 100 },
95
+ { subject: 'recommendation', A: 88, fullMark: 100 },
96
+ { subject: 'retention', A: 72, fullMark: 100 },
97
+ { subject: 'performance', A: 81, fullMark: 100 },
98
+ ];
99
+
100
+ const statusDistribution = [
101
+ { nome: 'active', valor: 1865, cor: '#22c55e' }, // green-500
102
+ { nome: 'completed', valor: 742, cor: '#3b82f6' }, // blue-500
103
+ { nome: 'inactive', valor: 240, cor: '#f97316' }, // orange-500
104
+ ];
105
+
106
+ const weeklyActivity = [
107
+ { dia: 'mon', acessos: 420, aulas: 180, exercicios: 95 },
108
+ { dia: 'tue', acessos: 380, aulas: 160, exercicios: 88 },
109
+ { dia: 'wed', acessos: 450, aulas: 200, exercicios: 110 },
110
+ { dia: 'thu', acessos: 410, aulas: 175, exercicios: 92 },
111
+ { dia: 'fri', acessos: 350, aulas: 140, exercicios: 75 },
112
+ { dia: 'sat', acessos: 280, aulas: 110, exercicios: 60 },
113
+ { dia: 'sun', acessos: 220, aulas: 85, exercicios: 45 },
114
+ ];
115
+
116
+ export default function RelatoriosPage() {
117
+ const t = useTranslations('lms.ReportsPage');
118
+ const [loading, setLoading] = useState(true);
119
+ const [periodo, setPeriodo] = useState('12m');
120
+ const [activeTab, setActiveTab] = useState('geral');
121
+
122
+ useEffect(() => {
123
+ const timer = setTimeout(() => setLoading(false), 1000);
124
+ return () => clearTimeout(timer);
125
+ }, []);
126
+
127
+ const tooltipStyle = {
128
+ backgroundColor: 'hsl(var(--card))',
129
+ border: '1px solid hsl(var(--border))',
130
+ borderRadius: '8px',
131
+ fontSize: '12px',
132
+ };
133
+
134
+ return (
135
+ <Page>
136
+ <PageHeader
137
+ title={t('title')}
138
+ description={t('description')}
139
+ breadcrumbs={[
140
+ {
141
+ label: t('breadcrumbs.home'),
142
+ href: '/',
143
+ },
144
+ {
145
+ label: t('breadcrumbs.reports'),
146
+ },
147
+ ]}
148
+ actions={
149
+ <div className="flex items-center gap-3">
150
+ <Select value={periodo} onValueChange={setPeriodo}>
151
+ <SelectTrigger className="w-[140px]">
152
+ <SelectValue />
153
+ </SelectTrigger>
154
+ <SelectContent>
155
+ <SelectItem value="7d">{t('period.7d')}</SelectItem>
156
+ <SelectItem value="30d">{t('period.30d')}</SelectItem>
157
+ <SelectItem value="6m">{t('period.6m')}</SelectItem>
158
+ <SelectItem value="12m">{t('period.12m')}</SelectItem>
159
+ </SelectContent>
160
+ </Select>
161
+ <Button
162
+ variant="outline"
163
+ className="gap-2"
164
+ onClick={() => toast.success(t('toasts.exported'))}
165
+ >
166
+ <Download className="size-4" />
167
+ <span className="hidden sm:inline">{t('actions.export')}</span>
168
+ </Button>
169
+ </div>
170
+ }
171
+ />
172
+
173
+ <motion.div
174
+ initial={{ opacity: 0, y: 20 }}
175
+ animate={{ opacity: 1, y: 0 }}
176
+ transition={{ duration: 0.4 }}
177
+ >
178
+ {/* KPIs */}
179
+ {loading ? (
180
+ <div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
181
+ {Array.from({ length: 4 }).map((_, i) => (
182
+ <Card key={i}>
183
+ <CardContent className="p-6">
184
+ <Skeleton className="mb-2 h-4 w-24" />
185
+ <Skeleton className="mb-1 h-8 w-20" />
186
+ <Skeleton className="h-4 w-16" />
187
+ </CardContent>
188
+ </Card>
189
+ ))}
190
+ </div>
191
+ ) : (
192
+ <div className="mb-8 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
193
+ {[
194
+ {
195
+ titulo: t('kpis.totalRevenue.title'),
196
+ valor: 'R$ 880.000',
197
+ variacao: '+18.2%',
198
+ positivo: true,
199
+ },
200
+ {
201
+ titulo: t('kpis.enrollments.title'),
202
+ valor: '2.850',
203
+ variacao: '+22.5%',
204
+ positivo: true,
205
+ },
206
+ {
207
+ titulo: t('kpis.completionRate.title'),
208
+ valor: '78.3%',
209
+ variacao: '+2.1%',
210
+ positivo: true,
211
+ },
212
+ {
213
+ titulo: t('kpis.churnRate.title'),
214
+ valor: '5.4%',
215
+ variacao: '-1.2%',
216
+ positivo: true,
217
+ },
218
+ ].map((kpi, i) => (
219
+ <motion.div
220
+ key={kpi.titulo}
221
+ initial={{ opacity: 0, y: 20 }}
222
+ animate={{ opacity: 1, y: 0 }}
223
+ transition={{ delay: i * 0.1 }}
224
+ >
225
+ <Card className="transition-shadow hover:shadow-md">
226
+ <CardContent className="p-6">
227
+ <p className="text-sm font-medium text-muted-foreground">
228
+ {kpi.titulo}
229
+ </p>
230
+ <p className="mt-2 text-2xl font-bold">{kpi.valor}</p>
231
+ <div className="mt-1 flex items-center gap-1">
232
+ {kpi.positivo ? (
233
+ <TrendingUp className="size-3 text-emerald-600" />
234
+ ) : (
235
+ <TrendingDown className="size-3 text-red-500" />
236
+ )}
237
+ <span
238
+ className={`text-xs font-medium ${kpi.positivo ? 'text-emerald-600' : 'text-red-500'}`}
239
+ >
240
+ {kpi.variacao}
241
+ </span>
242
+ <span className="text-xs text-muted-foreground">
243
+ {t('kpis.vsPreviousPeriod')}
244
+ </span>
245
+ </div>
246
+ </CardContent>
247
+ </Card>
248
+ </motion.div>
249
+ ))}
250
+ </div>
251
+ )}
252
+
253
+ {/* Tabs */}
254
+ <Tabs value={activeTab} onValueChange={setActiveTab} className="mb-8">
255
+ <TabsList className="mb-6">
256
+ <TabsTrigger value="geral">{t('tabs.overview')}</TabsTrigger>
257
+ <TabsTrigger value="cursos">{t('tabs.courses')}</TabsTrigger>
258
+ <TabsTrigger value="alunos">{t('tabs.students')}</TabsTrigger>
259
+ </TabsList>
260
+
261
+ {/* Tab: Geral */}
262
+ <TabsContent value="geral" className="mt-0">
263
+ {loading ? (
264
+ <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
265
+ {Array.from({ length: 4 }).map((_, i) => (
266
+ <Card key={i}>
267
+ <CardContent className="p-6">
268
+ <Skeleton className="mb-4 h-5 w-40" />
269
+ <Skeleton className="h-[260px] w-full" />
270
+ </CardContent>
271
+ </Card>
272
+ ))}
273
+ </div>
274
+ ) : (
275
+ <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
276
+ {/* Matriculas ao longo do tempo */}
277
+ <motion.div
278
+ initial={{ opacity: 0, y: 20 }}
279
+ animate={{ opacity: 1, y: 0 }}
280
+ transition={{ delay: 0.2 }}
281
+ >
282
+ <Card>
283
+ <CardHeader>
284
+ <CardTitle className="text-base">
285
+ {t('charts.monthlyEnrollments.title')}
286
+ </CardTitle>
287
+ <CardDescription>
288
+ {t('charts.monthlyEnrollments.description')}
289
+ </CardDescription>
290
+ </CardHeader>
291
+ <CardContent>
292
+ <ResponsiveContainer width="100%" height={260}>
293
+ <AreaChart data={monthlyEnrollments}>
294
+ <CartesianGrid
295
+ strokeDasharray="3 3"
296
+ stroke="hsl(var(--border))"
297
+ />
298
+ <XAxis
299
+ dataKey="mes"
300
+ tickFormatter={(value) => t(`months.${value}`)}
301
+ fontSize={12}
302
+ tickLine={false}
303
+ axisLine={false}
304
+ />
305
+ <YAxis
306
+ fontSize={12}
307
+ tickLine={false}
308
+ axisLine={false}
309
+ />
310
+ <Tooltip contentStyle={tooltipStyle} />
311
+ <defs>
312
+ <linearGradient
313
+ id="colorMatriculas"
314
+ x1="0"
315
+ y1="0"
316
+ x2="0"
317
+ y2="1"
318
+ >
319
+ <stop
320
+ offset="5%"
321
+ stopColor="#22c55e"
322
+ stopOpacity={0.3}
323
+ />
324
+ <stop
325
+ offset="95%"
326
+ stopColor="#22c55e"
327
+ stopOpacity={0}
328
+ />
329
+ </linearGradient>
330
+ <linearGradient
331
+ id="colorCancelamentos"
332
+ x1="0"
333
+ y1="0"
334
+ x2="0"
335
+ y2="1"
336
+ >
337
+ <stop
338
+ offset="5%"
339
+ stopColor="#ef4444"
340
+ stopOpacity={0.3}
341
+ />
342
+ <stop
343
+ offset="95%"
344
+ stopColor="#ef4444"
345
+ stopOpacity={0}
346
+ />
347
+ </linearGradient>
348
+ </defs>
349
+ <Area
350
+ type="monotone"
351
+ dataKey="matriculas"
352
+ stroke="#22c55e"
353
+ fill="url(#colorMatriculas)"
354
+ strokeWidth={2.5}
355
+ name={t('charts.monthlyEnrollments.enrollments')}
356
+ />
357
+ <Area
358
+ type="monotone"
359
+ dataKey="cancelamentos"
360
+ stroke="#ef4444"
361
+ fill="url(#colorCancelamentos)"
362
+ strokeWidth={2}
363
+ name={t('charts.monthlyEnrollments.cancellations')}
364
+ />
365
+ </AreaChart>
366
+ </ResponsiveContainer>
367
+ </CardContent>
368
+ </Card>
369
+ </motion.div>
370
+
371
+ {/* Receita */}
372
+ <motion.div
373
+ initial={{ opacity: 0, y: 20 }}
374
+ animate={{ opacity: 1, y: 0 }}
375
+ transition={{ delay: 0.3 }}
376
+ >
377
+ <Card>
378
+ <CardHeader>
379
+ <CardTitle className="text-base">
380
+ {t('charts.monthlyRevenue.title')}
381
+ </CardTitle>
382
+ <CardDescription>
383
+ {t('charts.monthlyRevenue.description')}
384
+ </CardDescription>
385
+ </CardHeader>
386
+ <CardContent>
387
+ <ResponsiveContainer width="100%" height={260}>
388
+ <BarChart data={monthlyEnrollments}>
389
+ <CartesianGrid
390
+ strokeDasharray="3 3"
391
+ stroke="hsl(var(--border))"
392
+ />
393
+ <XAxis
394
+ dataKey="mes"
395
+ tickFormatter={(value) => t(`months.${value}`)}
396
+ fontSize={12}
397
+ tickLine={false}
398
+ axisLine={false}
399
+ />
400
+ <YAxis
401
+ fontSize={12}
402
+ tickLine={false}
403
+ axisLine={false}
404
+ tickFormatter={(v) => `${(v / 1000).toFixed(0)}k`}
405
+ />
406
+ <Tooltip
407
+ contentStyle={tooltipStyle}
408
+ formatter={(value: number) => [
409
+ `R$ ${value.toLocaleString('pt-BR')}`,
410
+ t('charts.monthlyRevenue.revenue'),
411
+ ]}
412
+ />
413
+ <defs>
414
+ <linearGradient
415
+ id="colorReceita"
416
+ x1="0"
417
+ y1="0"
418
+ x2="0"
419
+ y2="1"
420
+ >
421
+ <stop offset="0%" stopColor="#3b82f6" />
422
+ <stop offset="100%" stopColor="#60a5fa" />
423
+ </linearGradient>
424
+ </defs>
425
+ <Bar
426
+ dataKey="receita"
427
+ fill="url(#colorReceita)"
428
+ radius={[6, 6, 0, 0]}
429
+ name={t('charts.monthlyRevenue.revenue')}
430
+ />
431
+ </BarChart>
432
+ </ResponsiveContainer>
433
+ </CardContent>
434
+ </Card>
435
+ </motion.div>
436
+
437
+ {/* Status dos alunos (pie) */}
438
+ <motion.div
439
+ initial={{ opacity: 0, y: 20 }}
440
+ animate={{ opacity: 1, y: 0 }}
441
+ transition={{ delay: 0.4 }}
442
+ >
443
+ <Card>
444
+ <CardHeader>
445
+ <CardTitle className="text-base">
446
+ {t('charts.studentStatus.title')}
447
+ </CardTitle>
448
+ <CardDescription>
449
+ {t('charts.studentStatus.description')}
450
+ </CardDescription>
451
+ </CardHeader>
452
+ <CardContent className="flex flex-col items-center">
453
+ <ResponsiveContainer width="100%" height={220}>
454
+ <PieChart>
455
+ <Pie
456
+ data={statusDistribution}
457
+ cx="50%"
458
+ cy="50%"
459
+ innerRadius={55}
460
+ outerRadius={85}
461
+ dataKey="valor"
462
+ nameKey="nome"
463
+ strokeWidth={2}
464
+ stroke="hsl(var(--background))"
465
+ >
466
+ {statusDistribution.map((entry, index) => (
467
+ <Cell key={`cell-${index}`} fill={entry.cor} />
468
+ ))}
469
+ </Pie>
470
+ <Tooltip contentStyle={tooltipStyle} />
471
+ </PieChart>
472
+ </ResponsiveContainer>
473
+ <div className="mt-2 flex flex-wrap justify-center gap-4">
474
+ {statusDistribution.map((item) => (
475
+ <div
476
+ key={item.nome}
477
+ className="flex items-center gap-1.5 text-xs"
478
+ >
479
+ <span
480
+ className="inline-block size-2.5 rounded-full"
481
+ style={{ backgroundColor: item.cor }}
482
+ />
483
+ <span className="text-muted-foreground">
484
+ {t(`statusDistribution.${item.nome}`)} (
485
+ {item.valor})
486
+ </span>
487
+ </div>
488
+ ))}
489
+ </div>
490
+ </CardContent>
491
+ </Card>
492
+ </motion.div>
493
+
494
+ {/* Radar */}
495
+ <motion.div
496
+ initial={{ opacity: 0, y: 20 }}
497
+ animate={{ opacity: 1, y: 0 }}
498
+ transition={{ delay: 0.5 }}
499
+ >
500
+ <Card>
501
+ <CardHeader>
502
+ <CardTitle className="text-base">
503
+ {t('charts.qualityIndicators.title')}
504
+ </CardTitle>
505
+ <CardDescription>
506
+ {t('charts.qualityIndicators.description')}
507
+ </CardDescription>
508
+ </CardHeader>
509
+ <CardContent>
510
+ <ResponsiveContainer width="100%" height={260}>
511
+ <RadarChart data={radarData}>
512
+ <PolarGrid stroke="hsl(var(--border))" />
513
+ <PolarAngleAxis
514
+ dataKey="subject"
515
+ fontSize={11}
516
+ tickFormatter={(value) => t(`radar.${value}`)}
517
+ />
518
+ <PolarRadiusAxis
519
+ angle={30}
520
+ domain={[0, 100]}
521
+ fontSize={10}
522
+ />
523
+ <Radar
524
+ name={t('charts.qualityIndicators.score')}
525
+ dataKey="A"
526
+ stroke="#a855f7"
527
+ fill="#a855f7"
528
+ fillOpacity={0.25}
529
+ strokeWidth={2.5}
530
+ />
531
+ <Tooltip contentStyle={tooltipStyle} />
532
+ </RadarChart>
533
+ </ResponsiveContainer>
534
+ </CardContent>
535
+ </Card>
536
+ </motion.div>
537
+ </div>
538
+ )}
539
+ </TabsContent>
540
+
541
+ {/* Tab: Cursos */}
542
+ <TabsContent value="cursos" className="mt-0">
543
+ {loading ? (
544
+ <Card>
545
+ <CardContent className="p-4">
546
+ {Array.from({ length: 8 }).map((_, i) => (
547
+ <div
548
+ key={i}
549
+ className="flex items-center gap-4 border-b py-3 last:border-0"
550
+ >
551
+ <Skeleton className="h-4 w-48" />
552
+ <Skeleton className="h-4 w-16" />
553
+ <Skeleton className="h-4 w-16" />
554
+ <Skeleton className="h-4 w-12" />
555
+ </div>
556
+ ))}
557
+ </CardContent>
558
+ </Card>
559
+ ) : (
560
+ <div className="grid grid-cols-1 gap-4 lg:grid-cols-5">
561
+ <motion.div
562
+ className="lg:col-span-3"
563
+ initial={{ opacity: 0, y: 20 }}
564
+ animate={{ opacity: 1, y: 0 }}
565
+ transition={{ delay: 0.2 }}
566
+ >
567
+ <Card>
568
+ <CardHeader>
569
+ <CardTitle className="text-base">
570
+ {t('charts.courseRanking.title')}
571
+ </CardTitle>
572
+ <CardDescription>
573
+ {t('charts.courseRanking.description')}
574
+ </CardDescription>
575
+ </CardHeader>
576
+ <CardContent className="p-0">
577
+ <Table>
578
+ <TableHeader>
579
+ <TableRow>
580
+ <TableHead className="w-[30px]">#</TableHead>
581
+ <TableHead>{t('table.course')}</TableHead>
582
+ <TableHead>{t('table.students')}</TableHead>
583
+ <TableHead className="hidden sm:table-cell">
584
+ {t('table.completion')}
585
+ </TableHead>
586
+ <TableHead>{t('table.score')}</TableHead>
587
+ </TableRow>
588
+ </TableHeader>
589
+ <TableBody>
590
+ {courseRanking
591
+ .sort((a, b) => b.alunos - a.alunos)
592
+ .map((curso, i) => (
593
+ <TableRow key={curso.nome}>
594
+ <TableCell className="font-mono text-xs text-muted-foreground">
595
+ {i + 1}
596
+ </TableCell>
597
+ <TableCell className="font-medium">
598
+ {t(`courseRanking.${curso.nome}`)}
599
+ </TableCell>
600
+ <TableCell>{curso.alunos}</TableCell>
601
+ <TableCell className="hidden sm:table-cell">
602
+ <Badge
603
+ variant={
604
+ curso.conclusao >= 80
605
+ ? 'default'
606
+ : 'secondary'
607
+ }
608
+ >
609
+ {curso.conclusao}%
610
+ </Badge>
611
+ </TableCell>
612
+ <TableCell>
613
+ <span
614
+ className={
615
+ curso.nota >= 8
616
+ ? 'font-medium text-emerald-600'
617
+ : 'text-foreground'
618
+ }
619
+ >
620
+ {curso.nota.toFixed(1)}
621
+ </span>
622
+ </TableCell>
623
+ </TableRow>
624
+ ))}
625
+ </TableBody>
626
+ </Table>
627
+ </CardContent>
628
+ </Card>
629
+ </motion.div>
630
+
631
+ <motion.div
632
+ className="lg:col-span-2"
633
+ initial={{ opacity: 0, y: 20 }}
634
+ animate={{ opacity: 1, y: 0 }}
635
+ transition={{ delay: 0.3 }}
636
+ >
637
+ <Card>
638
+ <CardHeader>
639
+ <CardTitle className="text-base">
640
+ {t('charts.performanceByArea.title')}
641
+ </CardTitle>
642
+ <CardDescription>
643
+ {t('charts.performanceByArea.description')}
644
+ </CardDescription>
645
+ </CardHeader>
646
+ <CardContent>
647
+ <ResponsiveContainer width="100%" height={300}>
648
+ <BarChart data={categoryPerformance} layout="vertical">
649
+ <CartesianGrid
650
+ strokeDasharray="3 3"
651
+ stroke="hsl(var(--border))"
652
+ />
653
+ <XAxis
654
+ type="number"
655
+ fontSize={12}
656
+ tickLine={false}
657
+ axisLine={false}
658
+ domain={[0, 100]}
659
+ />
660
+ <YAxis
661
+ type="category"
662
+ dataKey="area"
663
+ tickFormatter={(value) => t(`areas.${value}`)}
664
+ fontSize={12}
665
+ tickLine={false}
666
+ axisLine={false}
667
+ width={80}
668
+ />
669
+ <Tooltip contentStyle={tooltipStyle} />
670
+ <defs>
671
+ <linearGradient
672
+ id="colorConclusao"
673
+ x1="0"
674
+ y1="0"
675
+ x2="1"
676
+ y2="0"
677
+ >
678
+ <stop offset="0%" stopColor="#22c55e" />
679
+ <stop offset="100%" stopColor="#4ade80" />
680
+ </linearGradient>
681
+ </defs>
682
+ <Bar
683
+ dataKey="conclusao"
684
+ fill="url(#colorConclusao)"
685
+ radius={[0, 6, 6, 0]}
686
+ name={t('charts.performanceByArea.completion')}
687
+ />
688
+ </BarChart>
689
+ </ResponsiveContainer>
690
+ </CardContent>
691
+ </Card>
692
+ </motion.div>
693
+ </div>
694
+ )}
695
+ </TabsContent>
696
+
697
+ {/* Tab: Alunos */}
698
+ <TabsContent value="alunos" className="mt-0">
699
+ {loading ? (
700
+ <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
701
+ {Array.from({ length: 2 }).map((_, i) => (
702
+ <Card key={i}>
703
+ <CardContent className="p-6">
704
+ <Skeleton className="mb-4 h-5 w-40" />
705
+ <Skeleton className="h-[260px] w-full" />
706
+ </CardContent>
707
+ </Card>
708
+ ))}
709
+ </div>
710
+ ) : (
711
+ <div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
712
+ <motion.div
713
+ initial={{ opacity: 0, y: 20 }}
714
+ animate={{ opacity: 1, y: 0 }}
715
+ transition={{ delay: 0.2 }}
716
+ >
717
+ <Card>
718
+ <CardHeader>
719
+ <CardTitle className="text-base">
720
+ {t('charts.weeklyActivity.title')}
721
+ </CardTitle>
722
+ <CardDescription>
723
+ {t('charts.weeklyActivity.description')}
724
+ </CardDescription>
725
+ </CardHeader>
726
+ <CardContent>
727
+ <ResponsiveContainer width="100%" height={280}>
728
+ <BarChart data={weeklyActivity}>
729
+ <CartesianGrid
730
+ strokeDasharray="3 3"
731
+ stroke="hsl(var(--border))"
732
+ />
733
+ <XAxis
734
+ dataKey="dia"
735
+ tickFormatter={(value) => t(`weekdays.${value}`)}
736
+ fontSize={12}
737
+ tickLine={false}
738
+ axisLine={false}
739
+ />
740
+ <YAxis
741
+ fontSize={12}
742
+ tickLine={false}
743
+ axisLine={false}
744
+ />
745
+ <Tooltip contentStyle={tooltipStyle} />
746
+ <Bar
747
+ dataKey="acessos"
748
+ fill="#3b82f6"
749
+ radius={[4, 4, 0, 0]}
750
+ name={t('charts.weeklyActivity.accesses')}
751
+ />
752
+ <Bar
753
+ dataKey="aulas"
754
+ fill="#22c55e"
755
+ radius={[4, 4, 0, 0]}
756
+ name={t('charts.weeklyActivity.classes')}
757
+ />
758
+ <Bar
759
+ dataKey="exercicios"
760
+ fill="#f97316"
761
+ radius={[4, 4, 0, 0]}
762
+ name={t('charts.weeklyActivity.exercises')}
763
+ />
764
+ </BarChart>
765
+ </ResponsiveContainer>
766
+ </CardContent>
767
+ </Card>
768
+ </motion.div>
769
+
770
+ <motion.div
771
+ initial={{ opacity: 0, y: 20 }}
772
+ animate={{ opacity: 1, y: 0 }}
773
+ transition={{ delay: 0.3 }}
774
+ >
775
+ <Card>
776
+ <CardHeader>
777
+ <CardTitle className="text-base">
778
+ {t('charts.performanceEvolution.title')}
779
+ </CardTitle>
780
+ <CardDescription>
781
+ {t('charts.performanceEvolution.description')}
782
+ </CardDescription>
783
+ </CardHeader>
784
+ <CardContent>
785
+ <ResponsiveContainer width="100%" height={280}>
786
+ <LineChart
787
+ data={monthlyEnrollments.map((m, i) => ({
788
+ mes: m.mes,
789
+ nota: 6.8 + Math.sin(i * 0.5) * 0.5 + i * 0.1,
790
+ }))}
791
+ >
792
+ <CartesianGrid
793
+ strokeDasharray="3 3"
794
+ stroke="hsl(var(--border))"
795
+ />
796
+ <XAxis
797
+ dataKey="mes"
798
+ tickFormatter={(value) => t(`months.${value}`)}
799
+ fontSize={12}
800
+ tickLine={false}
801
+ axisLine={false}
802
+ />
803
+ <YAxis
804
+ fontSize={12}
805
+ tickLine={false}
806
+ axisLine={false}
807
+ domain={[6, 10]}
808
+ />
809
+ <Tooltip contentStyle={tooltipStyle} />
810
+ <Line
811
+ type="monotone"
812
+ dataKey="nota"
813
+ stroke="#a855f7"
814
+ strokeWidth={2.5}
815
+ dot={{
816
+ r: 4,
817
+ fill: '#a855f7',
818
+ strokeWidth: 2,
819
+ stroke: '#fff',
820
+ }}
821
+ activeDot={{ r: 6, fill: '#a855f7' }}
822
+ name={t('charts.performanceEvolution.averageScore')}
823
+ />
824
+ </LineChart>
825
+ </ResponsiveContainer>
826
+ </CardContent>
827
+ </Card>
828
+ </motion.div>
829
+
830
+ {/* Performance por Area */}
831
+ <motion.div
832
+ className="lg:col-span-2"
833
+ initial={{ opacity: 0, y: 20 }}
834
+ animate={{ opacity: 1, y: 0 }}
835
+ transition={{ delay: 0.4 }}
836
+ >
837
+ <Card>
838
+ <CardHeader>
839
+ <CardTitle className="text-base">
840
+ {t('charts.metricsByArea.title')}
841
+ </CardTitle>
842
+ <CardDescription>
843
+ {t('charts.metricsByArea.description')}
844
+ </CardDescription>
845
+ </CardHeader>
846
+ <CardContent className="p-0">
847
+ <Table>
848
+ <TableHeader>
849
+ <TableRow>
850
+ <TableHead>{t('table.area')}</TableHead>
851
+ <TableHead>{t('table.students')}</TableHead>
852
+ <TableHead>{t('table.averageScore')}</TableHead>
853
+ <TableHead>{t('table.completionRate')}</TableHead>
854
+ <TableHead className="hidden sm:table-cell">
855
+ {t('table.trend')}
856
+ </TableHead>
857
+ </TableRow>
858
+ </TableHeader>
859
+ <TableBody>
860
+ {categoryPerformance.map((area) => (
861
+ <TableRow key={area.area}>
862
+ <TableCell className="font-medium">
863
+ {t(`areas.${area.area}`)}
864
+ </TableCell>
865
+ <TableCell>{area.alunos}</TableCell>
866
+ <TableCell>
867
+ <span
868
+ className={
869
+ area.media >= 8
870
+ ? 'font-medium text-emerald-600'
871
+ : 'text-foreground'
872
+ }
873
+ >
874
+ {area.media.toFixed(1)}
875
+ </span>
876
+ </TableCell>
877
+ <TableCell>
878
+ <Badge
879
+ variant={
880
+ area.conclusao >= 80
881
+ ? 'default'
882
+ : 'secondary'
883
+ }
884
+ >
885
+ {area.conclusao}%
886
+ </Badge>
887
+ </TableCell>
888
+ <TableCell className="hidden sm:table-cell">
889
+ <div className="flex items-center gap-1 text-emerald-600">
890
+ <ArrowUpRight className="size-3" />
891
+ <span className="text-xs font-medium">
892
+ {t('table.growing')}
893
+ </span>
894
+ </div>
895
+ </TableCell>
896
+ </TableRow>
897
+ ))}
898
+ </TableBody>
899
+ </Table>
900
+ </CardContent>
901
+ </Card>
902
+ </motion.div>
903
+ </div>
904
+ )}
905
+ </TabsContent>
906
+ </Tabs>
907
+ </motion.div>
908
+ </Page>
909
+ );
910
+ }