@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.
package/hedhog/data/menu.yaml
CHANGED
|
@@ -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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/lms",
|
|
3
|
-
"version": "0.0.
|
|
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": {
|