@hed-hog/lms 0.0.266 → 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.
@@ -74,3 +74,19 @@
74
74
  - where:
75
75
  slug: admin-lms
76
76
 
77
+ - url: /lms/reports
78
+ menu_id:
79
+ where:
80
+ slug: /lms
81
+ icon: file-chart-column-increasing
82
+ name:
83
+ en: Reports
84
+ pt: Relatórios
85
+ slug: /lms/reports
86
+ relations:
87
+ role:
88
+ - where:
89
+ slug: admin
90
+ - where:
91
+ slug: admin-lms
92
+
@@ -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
+ }
@@ -474,5 +474,155 @@
474
474
  "openingFormacao": "Opening \"{{nome}}\"...",
475
475
  "openingDetails": "Opening training..."
476
476
  }
477
+ },
478
+ "ReportsPage": {
479
+ "title": "Reports",
480
+ "description": "Detailed platform performance analysis",
481
+ "breadcrumbs": {
482
+ "home": "Home",
483
+ "reports": "Reports"
484
+ },
485
+ "actions": {
486
+ "export": "Export"
487
+ },
488
+ "toasts": {
489
+ "exported": "Report exported!"
490
+ },
491
+ "period": {
492
+ "7d": "7 days",
493
+ "30d": "30 days",
494
+ "6m": "6 months",
495
+ "12m": "12 months"
496
+ },
497
+ "kpis": {
498
+ "totalRevenue": {
499
+ "title": "Total Revenue"
500
+ },
501
+ "enrollments": {
502
+ "title": "Enrollments"
503
+ },
504
+ "completionRate": {
505
+ "title": "Completion Rate"
506
+ },
507
+ "churnRate": {
508
+ "title": "Churn Rate"
509
+ },
510
+ "vsPreviousPeriod": "vs. previous period"
511
+ },
512
+ "tabs": {
513
+ "overview": "Overview",
514
+ "courses": "Courses",
515
+ "students": "Students"
516
+ },
517
+ "months": {
518
+ "jan": "Jan",
519
+ "feb": "Feb",
520
+ "mar": "Mar",
521
+ "apr": "Apr",
522
+ "may": "May",
523
+ "jun": "Jun",
524
+ "jul": "Jul",
525
+ "aug": "Aug",
526
+ "sep": "Sep",
527
+ "oct": "Oct",
528
+ "nov": "Nov",
529
+ "dec": "Dec"
530
+ },
531
+ "weekdays": {
532
+ "mon": "Mon",
533
+ "tue": "Tue",
534
+ "wed": "Wed",
535
+ "thu": "Thu",
536
+ "fri": "Fri",
537
+ "sat": "Sat",
538
+ "sun": "Sun"
539
+ },
540
+ "areas": {
541
+ "technology": "Technology",
542
+ "design": "Design",
543
+ "management": "Management",
544
+ "marketing": "Marketing"
545
+ },
546
+ "statusDistribution": {
547
+ "active": "Active",
548
+ "completed": "Completed",
549
+ "inactive": "Inactive"
550
+ },
551
+ "radar": {
552
+ "engagement": "Engagement",
553
+ "completion": "Completion",
554
+ "satisfaction": "Satisfaction",
555
+ "recommendation": "Recommendation",
556
+ "retention": "Retention",
557
+ "performance": "Performance"
558
+ },
559
+ "charts": {
560
+ "monthlyEnrollments": {
561
+ "title": "Monthly Enrollments",
562
+ "description": "Enrollment and cancellation trends",
563
+ "enrollments": "Enrollments",
564
+ "cancellations": "Cancellations"
565
+ },
566
+ "monthlyRevenue": {
567
+ "title": "Monthly Revenue",
568
+ "description": "Revenue in BRL per month",
569
+ "revenue": "Revenue"
570
+ },
571
+ "studentStatus": {
572
+ "title": "Student Status",
573
+ "description": "Distribution by status"
574
+ },
575
+ "qualityIndicators": {
576
+ "title": "Quality Indicators",
577
+ "description": "Key platform metrics",
578
+ "score": "Score"
579
+ },
580
+ "courseRanking": {
581
+ "title": "Course Ranking",
582
+ "description": "By number of enrolled students"
583
+ },
584
+ "performanceByArea": {
585
+ "title": "Performance by Area",
586
+ "description": "Average score and completion rate",
587
+ "completion": "Completion %"
588
+ },
589
+ "weeklyActivity": {
590
+ "title": "Weekly Activity",
591
+ "description": "Accesses, classes watched, and exercises",
592
+ "accesses": "Accesses",
593
+ "classes": "Classes",
594
+ "exercises": "Exercises"
595
+ },
596
+ "performanceEvolution": {
597
+ "title": "Performance Evolution",
598
+ "description": "Average score in the last 12 months",
599
+ "averageScore": "Average Score"
600
+ },
601
+ "metricsByArea": {
602
+ "title": "Knowledge Area Metrics",
603
+ "description": "Detailed comparison across areas"
604
+ }
605
+ },
606
+ "table": {
607
+ "course": "Course",
608
+ "students": "Students",
609
+ "completion": "Completion",
610
+ "score": "Score",
611
+ "area": "Area",
612
+ "averageScore": "Average Score",
613
+ "completionRate": "Completion Rate",
614
+ "trend": "Trend",
615
+ "growing": "Growing"
616
+ },
617
+ "courseRanking": {
618
+ "reactAdvanced": "Advanced React",
619
+ "excelBusiness": "Excel for Business",
620
+ "agileProjectManagement": "Agile Project Management",
621
+ "pythonDataScience": "Python for Data Science",
622
+ "uxDesignFundamentals": "UX Design Fundamentals",
623
+ "typescriptPractice": "TypeScript in Practice",
624
+ "nodeComplete": "Complete Node.js",
625
+ "designSystem": "Design System"
626
+ }
477
627
  }
478
628
  }
@@ -474,5 +474,155 @@
474
474
  "openingFormacao": "Abrindo \"{{nome}}\"...",
475
475
  "openingDetails": "Abrindo formacao..."
476
476
  }
477
+ },
478
+ "ReportsPage": {
479
+ "title": "Relatorios",
480
+ "description": "Analise detalhada do desempenho da plataforma",
481
+ "breadcrumbs": {
482
+ "home": "Home",
483
+ "reports": "Relatorios"
484
+ },
485
+ "actions": {
486
+ "export": "Exportar"
487
+ },
488
+ "toasts": {
489
+ "exported": "Relatorio exportado!"
490
+ },
491
+ "period": {
492
+ "7d": "7 dias",
493
+ "30d": "30 dias",
494
+ "6m": "6 meses",
495
+ "12m": "12 meses"
496
+ },
497
+ "kpis": {
498
+ "totalRevenue": {
499
+ "title": "Receita Total"
500
+ },
501
+ "enrollments": {
502
+ "title": "Matriculas"
503
+ },
504
+ "completionRate": {
505
+ "title": "Taxa de Conclusao"
506
+ },
507
+ "churnRate": {
508
+ "title": "Taxa de Cancelamento"
509
+ },
510
+ "vsPreviousPeriod": "vs. periodo anterior"
511
+ },
512
+ "tabs": {
513
+ "overview": "Visao Geral",
514
+ "courses": "Cursos",
515
+ "students": "Alunos"
516
+ },
517
+ "months": {
518
+ "jan": "Jan",
519
+ "feb": "Fev",
520
+ "mar": "Mar",
521
+ "apr": "Abr",
522
+ "may": "Mai",
523
+ "jun": "Jun",
524
+ "jul": "Jul",
525
+ "aug": "Ago",
526
+ "sep": "Set",
527
+ "oct": "Out",
528
+ "nov": "Nov",
529
+ "dec": "Dez"
530
+ },
531
+ "weekdays": {
532
+ "mon": "Seg",
533
+ "tue": "Ter",
534
+ "wed": "Qua",
535
+ "thu": "Qui",
536
+ "fri": "Sex",
537
+ "sat": "Sab",
538
+ "sun": "Dom"
539
+ },
540
+ "areas": {
541
+ "technology": "Tecnologia",
542
+ "design": "Design",
543
+ "management": "Gestao",
544
+ "marketing": "Marketing"
545
+ },
546
+ "statusDistribution": {
547
+ "active": "Ativos",
548
+ "completed": "Concluidos",
549
+ "inactive": "Inativos"
550
+ },
551
+ "radar": {
552
+ "engagement": "Engajamento",
553
+ "completion": "Conclusao",
554
+ "satisfaction": "Satisfacao",
555
+ "recommendation": "Recomendacao",
556
+ "retention": "Retencao",
557
+ "performance": "Desempenho"
558
+ },
559
+ "charts": {
560
+ "monthlyEnrollments": {
561
+ "title": "Matriculas Mensais",
562
+ "description": "Evolucao de matriculas e cancelamentos",
563
+ "enrollments": "Matriculas",
564
+ "cancellations": "Cancelamentos"
565
+ },
566
+ "monthlyRevenue": {
567
+ "title": "Receita Mensal",
568
+ "description": "Faturamento em R$ por mes",
569
+ "revenue": "Receita"
570
+ },
571
+ "studentStatus": {
572
+ "title": "Status dos Alunos",
573
+ "description": "Distribuicao por situacao"
574
+ },
575
+ "qualityIndicators": {
576
+ "title": "Indicadores de Qualidade",
577
+ "description": "Metricas-chave da plataforma",
578
+ "score": "Score"
579
+ },
580
+ "courseRanking": {
581
+ "title": "Ranking de Cursos",
582
+ "description": "Por numero de alunos matriculados"
583
+ },
584
+ "performanceByArea": {
585
+ "title": "Desempenho por Area",
586
+ "description": "Nota media e taxa de conclusao",
587
+ "completion": "Conclusao %"
588
+ },
589
+ "weeklyActivity": {
590
+ "title": "Atividade Semanal",
591
+ "description": "Acessos, aulas assistidas e exercicios",
592
+ "accesses": "Acessos",
593
+ "classes": "Aulas",
594
+ "exercises": "Exercicios"
595
+ },
596
+ "performanceEvolution": {
597
+ "title": "Evolucao de Desempenho",
598
+ "description": "Media de notas nos ultimos 12 meses",
599
+ "averageScore": "Nota Media"
600
+ },
601
+ "metricsByArea": {
602
+ "title": "Metricas por Area de Conhecimento",
603
+ "description": "Comparativo detalhado entre as areas"
604
+ }
605
+ },
606
+ "table": {
607
+ "course": "Curso",
608
+ "students": "Alunos",
609
+ "completion": "Conclusao",
610
+ "score": "Nota",
611
+ "area": "Area",
612
+ "averageScore": "Media de Nota",
613
+ "completionRate": "Taxa de Conclusao",
614
+ "trend": "Tendencia",
615
+ "growing": "Crescendo"
616
+ },
617
+ "courseRanking": {
618
+ "reactAdvanced": "React Avancado",
619
+ "excelBusiness": "Excel para Negocios",
620
+ "agileProjectManagement": "Gestao de Projetos Ageis",
621
+ "pythonDataScience": "Python para Data Science",
622
+ "uxDesignFundamentals": "UX Design Fundamentals",
623
+ "typescriptPractice": "TypeScript na Pratica",
624
+ "nodeComplete": "Node.js Completo",
625
+ "designSystem": "Design System"
626
+ }
477
627
  }
478
628
  }
@@ -0,0 +1,3 @@
1
+ columns:
2
+ - type: pk
3
+ - name: name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/lms",
3
- "version": "0.0.266",
3
+ "version": "0.0.267",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -11,9 +11,9 @@
11
11
  "@nestjs/mapped-types": "*",
12
12
  "@hed-hog/api-locale": "0.0.11",
13
13
  "@hed-hog/api-pagination": "0.0.5",
14
- "@hed-hog/api-prisma": "0.0.4",
15
14
  "@hed-hog/core": "0.0.261",
16
15
  "@hed-hog/api-types": "0.0.1",
16
+ "@hed-hog/api-prisma": "0.0.4",
17
17
  "@hed-hog/api": "0.0.3"
18
18
  },
19
19
  "exports": {