@hed-hog/lms 0.0.268 → 0.0.270
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 +8 -1
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +1387 -0
- package/hedhog/frontend/app/classes/page.tsx.ejs +4 -4
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +1237 -0
- package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +2642 -0
- package/hedhog/frontend/app/courses/page.tsx.ejs +825 -727
- package/hedhog/frontend/app/exams/[id]/attempt/page.tsx.ejs +976 -0
- package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +931 -0
- package/hedhog/frontend/app/exams/page.tsx.ejs +9 -7
- package/hedhog/frontend/app/training/page.tsx.ejs +3 -3
- package/hedhog/frontend/messages/en.json +703 -14
- package/hedhog/frontend/messages/pt.json +863 -174
- package/hedhog/query/triggers.sql +0 -0
- package/hedhog/table/certificate.yaml +89 -0
- package/hedhog/table/certificate_template.yaml +24 -0
- package/hedhog/table/course.yaml +67 -1
- package/hedhog/table/course_category.yaml +22 -0
- package/hedhog/table/course_class_attendance.yaml +34 -0
- package/hedhog/table/course_class_group.yaml +58 -0
- package/hedhog/table/course_class_session.yaml +38 -0
- package/hedhog/table/course_class_session_instructor.yaml +27 -0
- package/hedhog/table/course_enrollment.yaml +45 -0
- package/hedhog/table/course_image.yaml +33 -0
- package/hedhog/table/course_lesson.yaml +35 -0
- package/hedhog/table/course_lesson_file.yaml +23 -0
- package/hedhog/table/course_lesson_instructor.yaml +27 -0
- package/hedhog/table/course_lesson_progress.yaml +40 -0
- package/hedhog/table/course_lesson_question.yaml +24 -0
- package/hedhog/table/course_module.yaml +25 -0
- package/hedhog/table/course_prerequisite.yaml +48 -0
- package/hedhog/table/evaluation_rating.yaml +30 -0
- package/hedhog/table/evaluation_topic.yaml +68 -0
- package/hedhog/table/exam.yaml +91 -0
- package/hedhog/table/exam_answer.yaml +40 -0
- package/hedhog/table/exam_attempt.yaml +51 -0
- package/hedhog/table/exam_image.yaml +33 -0
- package/hedhog/table/exam_option.yaml +25 -0
- package/hedhog/table/exam_question.yaml +24 -0
- package/hedhog/table/image_type.yaml +28 -0
- package/hedhog/table/instructor.yaml +23 -0
- package/hedhog/table/learning_path.yaml +49 -0
- package/hedhog/table/learning_path_enrollment.yaml +33 -0
- package/hedhog/table/learning_path_image.yaml +33 -0
- package/hedhog/table/learning_path_step.yaml +43 -0
- package/hedhog/table/question.yaml +15 -0
- package/package.json +9 -6
- package/src/index.ts +1 -1
- package/src/lms.module.ts +15 -15
- package/hedhog/table/classes.yaml +0 -3
- package/hedhog/table/exams.yaml +0 -3
- package/hedhog/table/reports.yaml +0 -3
- package/hedhog/table/training.yaml +0 -3
|
@@ -0,0 +1,1387 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Page, PageHeader } from '@/components/entity-list';
|
|
4
|
+
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
5
|
+
import { Badge } from '@/components/ui/badge';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
8
|
+
import { Checkbox } from '@/components/ui/checkbox';
|
|
9
|
+
import {
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogDescription,
|
|
13
|
+
DialogFooter,
|
|
14
|
+
DialogHeader,
|
|
15
|
+
DialogTitle,
|
|
16
|
+
} from '@/components/ui/dialog';
|
|
17
|
+
import {
|
|
18
|
+
DropdownMenu,
|
|
19
|
+
DropdownMenuContent,
|
|
20
|
+
DropdownMenuItem,
|
|
21
|
+
DropdownMenuSeparator,
|
|
22
|
+
DropdownMenuTrigger,
|
|
23
|
+
} from '@/components/ui/dropdown-menu';
|
|
24
|
+
import { Field, FieldError, FieldLabel } from '@/components/ui/field';
|
|
25
|
+
import { Input } from '@/components/ui/input';
|
|
26
|
+
import {
|
|
27
|
+
Select,
|
|
28
|
+
SelectContent,
|
|
29
|
+
SelectItem,
|
|
30
|
+
SelectTrigger,
|
|
31
|
+
SelectValue,
|
|
32
|
+
} from '@/components/ui/select';
|
|
33
|
+
import {
|
|
34
|
+
Sheet,
|
|
35
|
+
SheetContent,
|
|
36
|
+
SheetDescription,
|
|
37
|
+
SheetFooter,
|
|
38
|
+
SheetHeader,
|
|
39
|
+
SheetTitle,
|
|
40
|
+
} from '@/components/ui/sheet';
|
|
41
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
42
|
+
import { Switch } from '@/components/ui/switch';
|
|
43
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
44
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
45
|
+
import { addDays, format, getDay, setHours, setMinutes } from 'date-fns';
|
|
46
|
+
import { enUS, ptBR } from 'date-fns/locale';
|
|
47
|
+
import { motion } from 'framer-motion';
|
|
48
|
+
import {
|
|
49
|
+
AlertTriangle,
|
|
50
|
+
BarChart3,
|
|
51
|
+
Calendar as CalendarIcon,
|
|
52
|
+
Check,
|
|
53
|
+
CheckCircle2,
|
|
54
|
+
Clock,
|
|
55
|
+
Eye,
|
|
56
|
+
Loader2,
|
|
57
|
+
Mail,
|
|
58
|
+
MapPin,
|
|
59
|
+
Monitor,
|
|
60
|
+
MoreHorizontal,
|
|
61
|
+
Plus,
|
|
62
|
+
Save,
|
|
63
|
+
Search,
|
|
64
|
+
UserMinus,
|
|
65
|
+
UserPlus,
|
|
66
|
+
Users,
|
|
67
|
+
Video,
|
|
68
|
+
} from 'lucide-react';
|
|
69
|
+
import { useLocale, useTranslations } from 'next-intl';
|
|
70
|
+
import Link from 'next/link';
|
|
71
|
+
import { useParams, useRouter } from 'next/navigation';
|
|
72
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
73
|
+
import { Calendar, dateFnsLocalizer, View } from 'react-big-calendar';
|
|
74
|
+
import 'react-big-calendar/lib/css/react-big-calendar.css';
|
|
75
|
+
import { Controller, useForm } from 'react-hook-form';
|
|
76
|
+
import { toast } from 'sonner';
|
|
77
|
+
import { z } from 'zod';
|
|
78
|
+
|
|
79
|
+
// ── Types ─────────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
interface Aluno {
|
|
82
|
+
id: number;
|
|
83
|
+
nome: string;
|
|
84
|
+
email: string;
|
|
85
|
+
telefone: string;
|
|
86
|
+
avatar?: string;
|
|
87
|
+
matriculadoEm: string;
|
|
88
|
+
progresso: number;
|
|
89
|
+
presenca: number;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
interface Aula {
|
|
93
|
+
id: number;
|
|
94
|
+
titulo: string;
|
|
95
|
+
data: Date;
|
|
96
|
+
horaInicio: string;
|
|
97
|
+
horaFim: string;
|
|
98
|
+
local: string;
|
|
99
|
+
tipo: 'presencial' | 'online';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface PresencaItem {
|
|
103
|
+
alunoId: number;
|
|
104
|
+
presente: boolean;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Calendar Localizer ────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
const locales = { 'pt-BR': ptBR, 'en-US': enUS };
|
|
110
|
+
const localizer = dateFnsLocalizer({
|
|
111
|
+
format,
|
|
112
|
+
parse: (str: string) => new Date(str),
|
|
113
|
+
startOfWeek: () => 0,
|
|
114
|
+
getDay,
|
|
115
|
+
locales,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// ── Schemas ───────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
const getAulaSchema = (t: (key: string) => string) =>
|
|
121
|
+
z.object({
|
|
122
|
+
titulo: z.string().min(3, t('sheet.lessonForm.validation.titleMin')),
|
|
123
|
+
data: z.string().min(1, t('sheet.lessonForm.validation.dateRequired')),
|
|
124
|
+
horaInicio: z
|
|
125
|
+
.string()
|
|
126
|
+
.min(1, t('sheet.lessonForm.validation.startTimeRequired')),
|
|
127
|
+
horaFim: z
|
|
128
|
+
.string()
|
|
129
|
+
.min(1, t('sheet.lessonForm.validation.endTimeRequired')),
|
|
130
|
+
local: z.string().min(1, t('sheet.lessonForm.validation.locationRequired')),
|
|
131
|
+
tipo: z.string().min(1, t('sheet.lessonForm.validation.typeRequired')),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
type AulaForm = z.infer<ReturnType<typeof getAulaSchema>>;
|
|
135
|
+
|
|
136
|
+
// ── Mock Data ─────────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
const TURMA_MOCK = {
|
|
139
|
+
id: 1,
|
|
140
|
+
codigo: 'T2024-001',
|
|
141
|
+
curso: 'React Avancado',
|
|
142
|
+
cursoId: 1,
|
|
143
|
+
tipo: 'online' as const,
|
|
144
|
+
dataInicio: '2024-02-01',
|
|
145
|
+
dataFim: '2024-06-30',
|
|
146
|
+
horario: '19:00 - 22:00',
|
|
147
|
+
status: 'em_andamento' as const,
|
|
148
|
+
vagas: 30,
|
|
149
|
+
matriculados: 28,
|
|
150
|
+
professor: 'Prof. Marcos Silva',
|
|
151
|
+
local: 'https://meet.google.com/abc-defg-hij',
|
|
152
|
+
descricao:
|
|
153
|
+
'Turma focada em conceitos avancados de React, incluindo hooks customizados, performance e arquitetura.',
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
function generateAlunos(): Aluno[] {
|
|
157
|
+
const nomes = [
|
|
158
|
+
'Ana Silva',
|
|
159
|
+
'Bruno Costa',
|
|
160
|
+
'Carla Oliveira',
|
|
161
|
+
'Daniel Santos',
|
|
162
|
+
'Elena Ferreira',
|
|
163
|
+
'Felipe Souza',
|
|
164
|
+
'Gabriela Lima',
|
|
165
|
+
'Henrique Almeida',
|
|
166
|
+
'Isabela Rocha',
|
|
167
|
+
'João Pedro',
|
|
168
|
+
'Katia Martins',
|
|
169
|
+
'Lucas Ribeiro',
|
|
170
|
+
'Maria Clara',
|
|
171
|
+
'Nicolas Pereira',
|
|
172
|
+
'Olivia Gomes',
|
|
173
|
+
'Paulo Henrique',
|
|
174
|
+
'Raquel Dias',
|
|
175
|
+
'Samuel Nunes',
|
|
176
|
+
'Tatiana Vieira',
|
|
177
|
+
'Vinicius Castro',
|
|
178
|
+
'William Araújo',
|
|
179
|
+
'Yasmin Barbosa',
|
|
180
|
+
'Zeca Mendes',
|
|
181
|
+
'Amanda Torres',
|
|
182
|
+
'Bruno Lopes',
|
|
183
|
+
'Camila Ramos',
|
|
184
|
+
'Diego Farias',
|
|
185
|
+
'Eduarda Moreira',
|
|
186
|
+
];
|
|
187
|
+
return nomes.map((nome, i) => ({
|
|
188
|
+
id: i + 1,
|
|
189
|
+
nome,
|
|
190
|
+
email: `${nome.toLowerCase().replace(' ', '.')}@email.com`,
|
|
191
|
+
telefone: `(11) 9${Math.floor(Math.random() * 9000 + 1000)}-${Math.floor(Math.random() * 9000 + 1000)}`,
|
|
192
|
+
matriculadoEm: `2024-0${Math.floor(Math.random() * 2 + 1)}-${String(Math.floor(Math.random() * 28 + 1)).padStart(2, '0')}`,
|
|
193
|
+
progresso: Math.floor(Math.random() * 60 + 40),
|
|
194
|
+
presenca: Math.floor(Math.random() * 30 + 70),
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function generateAulas(): Aula[] {
|
|
199
|
+
const today = new Date();
|
|
200
|
+
const aulas: Aula[] = [];
|
|
201
|
+
const titulos = [
|
|
202
|
+
'Introducao a Hooks',
|
|
203
|
+
'useEffect Avancado',
|
|
204
|
+
'Context API',
|
|
205
|
+
'Redux vs Zustand',
|
|
206
|
+
'Performance Optimization',
|
|
207
|
+
'React Query',
|
|
208
|
+
'Testing com Jest',
|
|
209
|
+
'Storybook',
|
|
210
|
+
'Next.js Fundamentos',
|
|
211
|
+
'SSR vs SSG',
|
|
212
|
+
'API Routes',
|
|
213
|
+
'Deploy e CI/CD',
|
|
214
|
+
];
|
|
215
|
+
for (let i = -10; i < 20; i++) {
|
|
216
|
+
const dia = addDays(today, i);
|
|
217
|
+
if (dia.getDay() === 0 || dia.getDay() === 6) continue;
|
|
218
|
+
aulas.push({
|
|
219
|
+
id: aulas.length + 1,
|
|
220
|
+
titulo: titulos[aulas.length % titulos.length],
|
|
221
|
+
data: dia,
|
|
222
|
+
horaInicio: '19:00',
|
|
223
|
+
horaFim: '22:00',
|
|
224
|
+
local: i % 3 === 0 ? 'Sala 201' : 'https://meet.google.com/abc-defg-hij',
|
|
225
|
+
tipo: i % 3 === 0 ? 'presencial' : 'online',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
return aulas;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const ALUNOS_DISPONIVEIS = [
|
|
232
|
+
{ id: 101, nome: 'Fernando Moura', email: 'fernando.moura@email.com' },
|
|
233
|
+
{ id: 102, nome: 'Juliana Cardoso', email: 'juliana.cardoso@email.com' },
|
|
234
|
+
{ id: 103, nome: 'Roberto Freitas', email: 'roberto.freitas@email.com' },
|
|
235
|
+
{ id: 104, nome: 'Simone Andrade', email: 'simone.andrade@email.com' },
|
|
236
|
+
{ id: 105, nome: 'Thiago Monteiro', email: 'thiago.monteiro@email.com' },
|
|
237
|
+
];
|
|
238
|
+
|
|
239
|
+
// ── Main Component ────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
export default function TurmaDetalhePage() {
|
|
242
|
+
const t = useTranslations('lms.ClassesPage.DetailPage');
|
|
243
|
+
const tClasses = useTranslations('lms.ClassesPage');
|
|
244
|
+
const locale = useLocale();
|
|
245
|
+
const params = useParams();
|
|
246
|
+
const router = useRouter();
|
|
247
|
+
const id = params.id as string;
|
|
248
|
+
const dateLocale = locale === 'pt' ? ptBR : enUS;
|
|
249
|
+
const calendarCulture = locale === 'pt' ? 'pt-BR' : 'en-US';
|
|
250
|
+
|
|
251
|
+
const calendarMessages = {
|
|
252
|
+
today: t('calendar.today'),
|
|
253
|
+
previous: t('calendar.previous'),
|
|
254
|
+
next: t('calendar.next'),
|
|
255
|
+
month: t('calendar.month'),
|
|
256
|
+
week: t('calendar.week'),
|
|
257
|
+
day: t('calendar.day'),
|
|
258
|
+
agenda: t('calendar.agenda'),
|
|
259
|
+
date: t('calendar.date'),
|
|
260
|
+
time: t('calendar.time'),
|
|
261
|
+
event: t('calendar.event'),
|
|
262
|
+
noEventsInRange: t('calendar.noEventsInRange'),
|
|
263
|
+
showMore: (count: number) => t('calendar.showMore', { count }),
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Data
|
|
267
|
+
const [loading, setLoading] = useState(true);
|
|
268
|
+
const [turma] = useState(TURMA_MOCK);
|
|
269
|
+
const [alunos, setAlunos] = useState<Aluno[]>([]);
|
|
270
|
+
const [aulas, setAulas] = useState<Aula[]>([]);
|
|
271
|
+
|
|
272
|
+
// Tabs
|
|
273
|
+
const [activeTab, setActiveTab] = useState('alunos');
|
|
274
|
+
|
|
275
|
+
// Alunos
|
|
276
|
+
const [alunoSearch, setAlunoSearch] = useState('');
|
|
277
|
+
const [selectedAlunos, setSelectedAlunos] = useState<number[]>([]);
|
|
278
|
+
const [addAlunoDialogOpen, setAddAlunoDialogOpen] = useState(false);
|
|
279
|
+
const [removeAlunoDialogOpen, setRemoveAlunoDialogOpen] = useState(false);
|
|
280
|
+
const [alunoToRemove, setAlunoToRemove] = useState<Aluno | null>(null);
|
|
281
|
+
const [alunosToAdd, setAlunosToAdd] = useState<number[]>([]);
|
|
282
|
+
|
|
283
|
+
// Calendario
|
|
284
|
+
const [calendarView, setCalendarView] = useState<View>('month');
|
|
285
|
+
const [calendarDate, setCalendarDate] = useState(new Date());
|
|
286
|
+
const [aulaSheetOpen, setAulaSheetOpen] = useState(false);
|
|
287
|
+
const [editingAula, setEditingAula] = useState<Aula | null>(null);
|
|
288
|
+
const [selectedAulaForPresenca, setSelectedAulaForPresenca] =
|
|
289
|
+
useState<Aula | null>(null);
|
|
290
|
+
|
|
291
|
+
// Presenca
|
|
292
|
+
const [presencaSheetOpen, setPresencaSheetOpen] = useState(false);
|
|
293
|
+
const [presencaList, setPresencaList] = useState<PresencaItem[]>([]);
|
|
294
|
+
const [savingPresenca, setSavingPresenca] = useState(false);
|
|
295
|
+
|
|
296
|
+
// Form
|
|
297
|
+
const aulaSchema = getAulaSchema(t);
|
|
298
|
+
const aulaForm = useForm<AulaForm>({
|
|
299
|
+
resolver: zodResolver(aulaSchema),
|
|
300
|
+
defaultValues: {
|
|
301
|
+
titulo: '',
|
|
302
|
+
data: '',
|
|
303
|
+
horaInicio: '19:00',
|
|
304
|
+
horaFim: '22:00',
|
|
305
|
+
local: '',
|
|
306
|
+
tipo: 'online',
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Load data
|
|
311
|
+
useEffect(() => {
|
|
312
|
+
const timer = setTimeout(() => {
|
|
313
|
+
setAlunos(generateAlunos());
|
|
314
|
+
setAulas(generateAulas());
|
|
315
|
+
setLoading(false);
|
|
316
|
+
}, 800);
|
|
317
|
+
return () => clearTimeout(timer);
|
|
318
|
+
}, []);
|
|
319
|
+
|
|
320
|
+
// Filter alunos
|
|
321
|
+
const filteredAlunos = useMemo(() => {
|
|
322
|
+
if (!alunoSearch.trim()) return alunos;
|
|
323
|
+
const q = alunoSearch.toLowerCase();
|
|
324
|
+
return alunos.filter(
|
|
325
|
+
(a) =>
|
|
326
|
+
a.nome.toLowerCase().includes(q) || a.email.toLowerCase().includes(q)
|
|
327
|
+
);
|
|
328
|
+
}, [alunos, alunoSearch]);
|
|
329
|
+
|
|
330
|
+
// Calendar events
|
|
331
|
+
const calendarEvents = useMemo(() => {
|
|
332
|
+
return aulas.map((aula) => {
|
|
333
|
+
const [hi, mi] = aula.horaInicio.split(':').map(Number);
|
|
334
|
+
const [hf, mf] = aula.horaFim.split(':').map(Number);
|
|
335
|
+
return {
|
|
336
|
+
id: aula.id,
|
|
337
|
+
title: aula.titulo,
|
|
338
|
+
start: setMinutes(setHours(aula.data, hi), mi),
|
|
339
|
+
end: setMinutes(setHours(aula.data, hf), mf),
|
|
340
|
+
resource: aula,
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
}, [aulas]);
|
|
344
|
+
|
|
345
|
+
// Event style
|
|
346
|
+
const eventStyleGetter = useCallback(
|
|
347
|
+
(event: { resource?: Aula }) => ({
|
|
348
|
+
style: {
|
|
349
|
+
backgroundColor:
|
|
350
|
+
event.resource?.tipo === 'presencial' ? '#3b82f6' : '#22c55e',
|
|
351
|
+
border: 'none',
|
|
352
|
+
borderRadius: '6px',
|
|
353
|
+
color: '#fff',
|
|
354
|
+
fontSize: '0.75rem',
|
|
355
|
+
fontWeight: 500,
|
|
356
|
+
padding: '3px 8px',
|
|
357
|
+
boxShadow: '0 1px 3px rgba(0,0,0,0.12)',
|
|
358
|
+
},
|
|
359
|
+
}),
|
|
360
|
+
[]
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// Handlers
|
|
364
|
+
const toggleSelectAluno = (id: number, e?: React.MouseEvent) => {
|
|
365
|
+
if (e?.shiftKey && selectedAlunos.length > 0) {
|
|
366
|
+
const lastSelected = selectedAlunos[selectedAlunos.length - 1];
|
|
367
|
+
const lastIndex = filteredAlunos.findIndex((a) => a.id === lastSelected);
|
|
368
|
+
const currentIndex = filteredAlunos.findIndex((a) => a.id === id);
|
|
369
|
+
const [start, end] = [
|
|
370
|
+
Math.min(lastIndex, currentIndex),
|
|
371
|
+
Math.max(lastIndex, currentIndex),
|
|
372
|
+
];
|
|
373
|
+
const range = filteredAlunos.slice(start, end + 1).map((a) => a.id);
|
|
374
|
+
setSelectedAlunos((prev) => [...new Set([...prev, ...range])]);
|
|
375
|
+
} else if (e?.ctrlKey || e?.metaKey) {
|
|
376
|
+
setSelectedAlunos((prev) =>
|
|
377
|
+
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
|
378
|
+
);
|
|
379
|
+
} else {
|
|
380
|
+
setSelectedAlunos((prev) =>
|
|
381
|
+
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const handleAddAlunos = () => {
|
|
387
|
+
if (alunosToAdd.length === 0) return;
|
|
388
|
+
const novosAlunos = ALUNOS_DISPONIVEIS.filter((a) =>
|
|
389
|
+
alunosToAdd.includes(a.id)
|
|
390
|
+
).map((a) => ({
|
|
391
|
+
...a,
|
|
392
|
+
telefone: '(11) 99999-9999',
|
|
393
|
+
matriculadoEm: format(new Date(), 'yyyy-MM-dd'),
|
|
394
|
+
progresso: 0,
|
|
395
|
+
presenca: 100,
|
|
396
|
+
}));
|
|
397
|
+
setAlunos((prev) => [...prev, ...novosAlunos]);
|
|
398
|
+
setAddAlunoDialogOpen(false);
|
|
399
|
+
setAlunosToAdd([]);
|
|
400
|
+
toast.success(t('toasts.studentsAdded', { count: novosAlunos.length }));
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const handleRemoveAluno = () => {
|
|
404
|
+
if (!alunoToRemove) return;
|
|
405
|
+
setAlunos((prev) => prev.filter((a) => a.id !== alunoToRemove.id));
|
|
406
|
+
setRemoveAlunoDialogOpen(false);
|
|
407
|
+
setAlunoToRemove(null);
|
|
408
|
+
toast.success(t('toasts.studentRemoved'));
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const handleRemoveSelectedAlunos = () => {
|
|
412
|
+
setAlunos((prev) => prev.filter((a) => !selectedAlunos.includes(a.id)));
|
|
413
|
+
setSelectedAlunos([]);
|
|
414
|
+
toast.success(
|
|
415
|
+
t('toasts.studentsRemoved', { count: selectedAlunos.length })
|
|
416
|
+
);
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const openAulaSheet = (aula?: Aula) => {
|
|
420
|
+
if (aula) {
|
|
421
|
+
setEditingAula(aula);
|
|
422
|
+
aulaForm.reset({
|
|
423
|
+
titulo: aula.titulo,
|
|
424
|
+
data: format(aula.data, 'yyyy-MM-dd'),
|
|
425
|
+
horaInicio: aula.horaInicio,
|
|
426
|
+
horaFim: aula.horaFim,
|
|
427
|
+
local: aula.local,
|
|
428
|
+
tipo: aula.tipo,
|
|
429
|
+
});
|
|
430
|
+
} else {
|
|
431
|
+
setEditingAula(null);
|
|
432
|
+
aulaForm.reset({
|
|
433
|
+
titulo: '',
|
|
434
|
+
data: '',
|
|
435
|
+
horaInicio: '19:00',
|
|
436
|
+
horaFim: '22:00',
|
|
437
|
+
local: '',
|
|
438
|
+
tipo: 'online',
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
setAulaSheetOpen(true);
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const handleSaveAula = aulaForm.handleSubmit((data) => {
|
|
445
|
+
if (editingAula) {
|
|
446
|
+
setAulas((prev) =>
|
|
447
|
+
prev.map((a) =>
|
|
448
|
+
a.id === editingAula.id
|
|
449
|
+
? {
|
|
450
|
+
...a,
|
|
451
|
+
titulo: data.titulo,
|
|
452
|
+
data: new Date(data.data),
|
|
453
|
+
horaInicio: data.horaInicio,
|
|
454
|
+
horaFim: data.horaFim,
|
|
455
|
+
local: data.local,
|
|
456
|
+
tipo: data.tipo as 'presencial' | 'online',
|
|
457
|
+
}
|
|
458
|
+
: a
|
|
459
|
+
)
|
|
460
|
+
);
|
|
461
|
+
toast.success(t('toasts.lessonUpdated'));
|
|
462
|
+
} else {
|
|
463
|
+
const newAula: Aula = {
|
|
464
|
+
id: Math.max(...aulas.map((a) => a.id), 0) + 1,
|
|
465
|
+
titulo: data.titulo,
|
|
466
|
+
data: new Date(data.data),
|
|
467
|
+
horaInicio: data.horaInicio,
|
|
468
|
+
horaFim: data.horaFim,
|
|
469
|
+
local: data.local,
|
|
470
|
+
tipo: data.tipo as 'presencial' | 'online',
|
|
471
|
+
};
|
|
472
|
+
setAulas((prev) => [...prev, newAula]);
|
|
473
|
+
toast.success(t('toasts.lessonCreated'));
|
|
474
|
+
}
|
|
475
|
+
setAulaSheetOpen(false);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const handleSelectEvent = useCallback((event: { resource?: Aula }) => {
|
|
479
|
+
if (event.resource) {
|
|
480
|
+
openAulaSheet(event.resource);
|
|
481
|
+
}
|
|
482
|
+
}, []);
|
|
483
|
+
|
|
484
|
+
const openPresenca = (aula: Aula) => {
|
|
485
|
+
setSelectedAulaForPresenca(aula);
|
|
486
|
+
setPresencaList(
|
|
487
|
+
alunos.map((a) => ({ alunoId: a.id, presente: Math.random() > 0.15 }))
|
|
488
|
+
);
|
|
489
|
+
setPresencaSheetOpen(true);
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
const togglePresenca = (alunoId: number) => {
|
|
493
|
+
setPresencaList((prev) =>
|
|
494
|
+
prev.map((p) =>
|
|
495
|
+
p.alunoId === alunoId ? { ...p, presente: !p.presente } : p
|
|
496
|
+
)
|
|
497
|
+
);
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const handleSavePresenca = async () => {
|
|
501
|
+
setSavingPresenca(true);
|
|
502
|
+
await new Promise((r) => setTimeout(r, 800));
|
|
503
|
+
setSavingPresenca(false);
|
|
504
|
+
setPresencaSheetOpen(false);
|
|
505
|
+
toast.success(t('toasts.attendanceSaved'));
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// KPIs
|
|
509
|
+
const kpis = [
|
|
510
|
+
{
|
|
511
|
+
label: t('kpis.enrolledStudents.label'),
|
|
512
|
+
valor: alunos.length,
|
|
513
|
+
sub: t('kpis.enrolledStudents.sub', { vagas: turma.vagas }),
|
|
514
|
+
icon: Users,
|
|
515
|
+
iconBg: 'bg-orange-100',
|
|
516
|
+
iconColor: 'text-orange-600',
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
label: t('kpis.occupancyRate.label'),
|
|
520
|
+
valor: `${Math.round((alunos.length / turma.vagas) * 100)}%`,
|
|
521
|
+
sub:
|
|
522
|
+
turma.vagas - alunos.length > 0
|
|
523
|
+
? t('kpis.occupancyRate.subFree', {
|
|
524
|
+
count: turma.vagas - alunos.length,
|
|
525
|
+
})
|
|
526
|
+
: t('kpis.occupancyRate.subFull'),
|
|
527
|
+
icon: BarChart3,
|
|
528
|
+
iconBg: 'bg-muted',
|
|
529
|
+
iconColor: 'text-foreground',
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
label: t('kpis.avgAttendance.label'),
|
|
533
|
+
valor: `${Math.round(alunos.reduce((a, b) => a + b.presenca, 0) / Math.max(alunos.length, 1))}%`,
|
|
534
|
+
sub: t('kpis.avgAttendance.sub'),
|
|
535
|
+
icon: CheckCircle2,
|
|
536
|
+
iconBg: 'bg-muted',
|
|
537
|
+
iconColor: 'text-foreground',
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
label: t('kpis.completedLessons.label'),
|
|
541
|
+
valor: aulas.filter((a) => a.data < new Date()).length,
|
|
542
|
+
sub: t('kpis.completedLessons.sub', { total: aulas.length }),
|
|
543
|
+
icon: CalendarIcon,
|
|
544
|
+
iconBg: 'bg-muted',
|
|
545
|
+
iconColor: 'text-foreground',
|
|
546
|
+
},
|
|
547
|
+
];
|
|
548
|
+
|
|
549
|
+
const STATUS_MAP: Record<string, { label: string; color: string }> = {
|
|
550
|
+
aberta: {
|
|
551
|
+
label: tClasses('status.aberta'),
|
|
552
|
+
color: 'bg-blue-100 text-blue-700 border-blue-200',
|
|
553
|
+
},
|
|
554
|
+
em_andamento: {
|
|
555
|
+
label: tClasses('status.em_andamento'),
|
|
556
|
+
color: 'bg-emerald-100 text-emerald-700 border-emerald-200',
|
|
557
|
+
},
|
|
558
|
+
concluida: {
|
|
559
|
+
label: tClasses('status.concluida'),
|
|
560
|
+
color: 'bg-gray-100 text-gray-700 border-gray-200',
|
|
561
|
+
},
|
|
562
|
+
cancelada: {
|
|
563
|
+
label: tClasses('status.cancelada'),
|
|
564
|
+
color: 'bg-red-100 text-red-700 border-red-200',
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const fadeUp = {
|
|
569
|
+
hidden: { opacity: 0, y: 20 },
|
|
570
|
+
visible: { opacity: 1, y: 0 },
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
return (
|
|
574
|
+
<Page>
|
|
575
|
+
<PageHeader
|
|
576
|
+
title={turma.curso}
|
|
577
|
+
breadcrumbs={[
|
|
578
|
+
{
|
|
579
|
+
label: t('breadcrumbs.home'),
|
|
580
|
+
href: '/',
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
label: t('breadcrumbs.classes'),
|
|
584
|
+
href: '/lms/classes',
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
label: t('breadcrumbs.management'),
|
|
588
|
+
},
|
|
589
|
+
]}
|
|
590
|
+
actions={
|
|
591
|
+
<div className="flex items-center gap-2">
|
|
592
|
+
<div className="flex gap-2">
|
|
593
|
+
<Button variant="outline" asChild>
|
|
594
|
+
<Link href={`/lms/courses/${turma.cursoId}`}>
|
|
595
|
+
{t('actions.viewCourse')}
|
|
596
|
+
</Link>
|
|
597
|
+
</Button>
|
|
598
|
+
</div>
|
|
599
|
+
<Button onClick={() => openAulaSheet()} className="gap-2">
|
|
600
|
+
<Plus className="size-4" /> {t('actions.newLesson')}
|
|
601
|
+
</Button>
|
|
602
|
+
</div>
|
|
603
|
+
}
|
|
604
|
+
/>
|
|
605
|
+
|
|
606
|
+
<div>
|
|
607
|
+
<motion.div
|
|
608
|
+
initial="hidden"
|
|
609
|
+
animate="visible"
|
|
610
|
+
variants={{ visible: { transition: { staggerChildren: 0.05 } } }}
|
|
611
|
+
>
|
|
612
|
+
{/* Header */}
|
|
613
|
+
<motion.div
|
|
614
|
+
variants={fadeUp}
|
|
615
|
+
className="mb-3 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"
|
|
616
|
+
>
|
|
617
|
+
<div>
|
|
618
|
+
<div className="flex flex-wrap items-center gap-2 mb-1">
|
|
619
|
+
<Badge className={`${STATUS_MAP[turma.status].color} border`}>
|
|
620
|
+
{STATUS_MAP[turma.status].label}
|
|
621
|
+
</Badge>
|
|
622
|
+
</div>
|
|
623
|
+
<p className="text-muted-foreground">
|
|
624
|
+
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">
|
|
625
|
+
{turma.codigo}
|
|
626
|
+
</code>
|
|
627
|
+
<span className="mx-2">|</span>
|
|
628
|
+
{turma.professor}
|
|
629
|
+
</p>
|
|
630
|
+
</div>
|
|
631
|
+
</motion.div>
|
|
632
|
+
|
|
633
|
+
{/* KPIs */}
|
|
634
|
+
<motion.div
|
|
635
|
+
variants={fadeUp}
|
|
636
|
+
className="mb-6 grid grid-cols-2 gap-4 lg:grid-cols-4"
|
|
637
|
+
>
|
|
638
|
+
{loading
|
|
639
|
+
? Array.from({ length: 4 }).map((_, i) => (
|
|
640
|
+
<Card key={i}>
|
|
641
|
+
<CardContent className="p-4">
|
|
642
|
+
<Skeleton className="mb-2 h-8 w-16" />
|
|
643
|
+
<Skeleton className="h-4 w-28" />
|
|
644
|
+
</CardContent>
|
|
645
|
+
</Card>
|
|
646
|
+
))
|
|
647
|
+
: kpis.map((kpi, i) => (
|
|
648
|
+
<motion.div
|
|
649
|
+
key={kpi.label}
|
|
650
|
+
initial={{ opacity: 0, y: 12 }}
|
|
651
|
+
animate={{ opacity: 1, y: 0 }}
|
|
652
|
+
transition={{ delay: i * 0.07 }}
|
|
653
|
+
>
|
|
654
|
+
<Card className="overflow-hidden">
|
|
655
|
+
<CardContent className="flex items-start justify-between p-5">
|
|
656
|
+
<div>
|
|
657
|
+
<p className="text-sm text-muted-foreground">
|
|
658
|
+
{kpi.label}
|
|
659
|
+
</p>
|
|
660
|
+
<p className="mt-1 text-3xl font-bold tracking-tight">
|
|
661
|
+
{kpi.valor}
|
|
662
|
+
</p>
|
|
663
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
664
|
+
{kpi.sub}
|
|
665
|
+
</p>
|
|
666
|
+
</div>
|
|
667
|
+
<div
|
|
668
|
+
className={`flex size-10 shrink-0 items-center justify-center rounded-lg ${kpi.iconBg}`}
|
|
669
|
+
>
|
|
670
|
+
<kpi.icon className={`size-5 ${kpi.iconColor}`} />
|
|
671
|
+
</div>
|
|
672
|
+
</CardContent>
|
|
673
|
+
</Card>
|
|
674
|
+
</motion.div>
|
|
675
|
+
))}
|
|
676
|
+
</motion.div>
|
|
677
|
+
|
|
678
|
+
{/* Info Card */}
|
|
679
|
+
<motion.div variants={fadeUp} className="mb-6">
|
|
680
|
+
<Card>
|
|
681
|
+
<CardContent className="p-5">
|
|
682
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
683
|
+
<div className="flex items-center gap-3">
|
|
684
|
+
<div className="flex size-10 items-center justify-center rounded-lg bg-amber-100">
|
|
685
|
+
<CalendarIcon className="size-5 text-amber-600" />
|
|
686
|
+
</div>
|
|
687
|
+
<div>
|
|
688
|
+
<p className="text-xs text-muted-foreground">
|
|
689
|
+
{t('info.period')}
|
|
690
|
+
</p>
|
|
691
|
+
<p className="text-sm font-medium">
|
|
692
|
+
{format(new Date(turma.dataInicio), 'dd/MM/yyyy')} -{' '}
|
|
693
|
+
{format(new Date(turma.dataFim), 'dd/MM/yyyy')}
|
|
694
|
+
</p>
|
|
695
|
+
</div>
|
|
696
|
+
</div>
|
|
697
|
+
<div className="flex items-center gap-3">
|
|
698
|
+
<div className="flex size-10 items-center justify-center rounded-lg bg-purple-100">
|
|
699
|
+
<Clock className="size-5 text-purple-600" />
|
|
700
|
+
</div>
|
|
701
|
+
<div>
|
|
702
|
+
<p className="text-xs text-muted-foreground">
|
|
703
|
+
{t('info.schedule')}
|
|
704
|
+
</p>
|
|
705
|
+
<p className="text-sm font-medium">{turma.horario}</p>
|
|
706
|
+
</div>
|
|
707
|
+
</div>
|
|
708
|
+
<div className="flex items-center gap-3">
|
|
709
|
+
<div className="flex size-10 items-center justify-center rounded-lg bg-emerald-100">
|
|
710
|
+
{turma.tipo === 'online' ? (
|
|
711
|
+
<Video className="size-5 text-emerald-600" />
|
|
712
|
+
) : (
|
|
713
|
+
<MapPin className="size-5 text-emerald-600" />
|
|
714
|
+
)}
|
|
715
|
+
</div>
|
|
716
|
+
<div>
|
|
717
|
+
<p className="text-xs text-muted-foreground">
|
|
718
|
+
{turma.tipo === 'online'
|
|
719
|
+
? t('info.onlineLabel')
|
|
720
|
+
: t('info.locationLabel')}
|
|
721
|
+
</p>
|
|
722
|
+
{turma.tipo === 'online' ? (
|
|
723
|
+
<a
|
|
724
|
+
href={turma.local}
|
|
725
|
+
target="_blank"
|
|
726
|
+
rel="noopener noreferrer"
|
|
727
|
+
className="text-sm font-medium text-blue-600 hover:underline"
|
|
728
|
+
>
|
|
729
|
+
{t('info.accessRoom')}
|
|
730
|
+
</a>
|
|
731
|
+
) : (
|
|
732
|
+
<p className="text-sm font-medium">{turma.local}</p>
|
|
733
|
+
)}
|
|
734
|
+
</div>
|
|
735
|
+
</div>
|
|
736
|
+
<div className="flex items-center gap-3">
|
|
737
|
+
<div className="flex size-10 items-center justify-center rounded-lg bg-blue-100">
|
|
738
|
+
<Monitor className="size-5 text-blue-600" />
|
|
739
|
+
</div>
|
|
740
|
+
<div>
|
|
741
|
+
<p className="text-xs text-muted-foreground">
|
|
742
|
+
{t('info.modality')}
|
|
743
|
+
</p>
|
|
744
|
+
<p className="text-sm font-medium capitalize">
|
|
745
|
+
{tClasses(`type.${turma.tipo}`)}
|
|
746
|
+
</p>
|
|
747
|
+
</div>
|
|
748
|
+
</div>
|
|
749
|
+
</div>
|
|
750
|
+
</CardContent>
|
|
751
|
+
</Card>
|
|
752
|
+
</motion.div>
|
|
753
|
+
|
|
754
|
+
{/* Tabs */}
|
|
755
|
+
<motion.div variants={fadeUp}>
|
|
756
|
+
<Tabs
|
|
757
|
+
value={activeTab}
|
|
758
|
+
onValueChange={setActiveTab}
|
|
759
|
+
className="w-full"
|
|
760
|
+
>
|
|
761
|
+
<TabsList className="mb-4 w-full justify-start overflow-x-auto">
|
|
762
|
+
<TabsTrigger value="alunos" className="gap-2">
|
|
763
|
+
<Users className="size-4" />
|
|
764
|
+
{t('tabs.students')}
|
|
765
|
+
</TabsTrigger>
|
|
766
|
+
<TabsTrigger value="calendario" className="gap-2">
|
|
767
|
+
<CalendarIcon className="size-4" />
|
|
768
|
+
{t('tabs.calendar')}
|
|
769
|
+
</TabsTrigger>
|
|
770
|
+
<TabsTrigger value="presenca" className="gap-2">
|
|
771
|
+
<CheckCircle2 className="size-4" />
|
|
772
|
+
{t('tabs.attendance')}
|
|
773
|
+
</TabsTrigger>
|
|
774
|
+
</TabsList>
|
|
775
|
+
|
|
776
|
+
{/* ── Tab Alunos ────────────────────────────────────────────────── */}
|
|
777
|
+
<TabsContent value="alunos" className="mt-0">
|
|
778
|
+
{/* Actions bar */}
|
|
779
|
+
<div className="mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
780
|
+
<div className="relative flex-1 max-w-md">
|
|
781
|
+
<Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
782
|
+
<Input
|
|
783
|
+
placeholder={t('students.searchPlaceholder')}
|
|
784
|
+
value={alunoSearch}
|
|
785
|
+
onChange={(e) => setAlunoSearch(e.target.value)}
|
|
786
|
+
className="pl-9"
|
|
787
|
+
/>
|
|
788
|
+
</div>
|
|
789
|
+
<div className="flex gap-2">
|
|
790
|
+
{selectedAlunos.length > 0 && (
|
|
791
|
+
<Button
|
|
792
|
+
variant="destructive"
|
|
793
|
+
size="sm"
|
|
794
|
+
className="gap-2"
|
|
795
|
+
onClick={handleRemoveSelectedAlunos}
|
|
796
|
+
>
|
|
797
|
+
<UserMinus className="size-4" />
|
|
798
|
+
{t('students.actions.removeSelected', {
|
|
799
|
+
count: selectedAlunos.length,
|
|
800
|
+
})}
|
|
801
|
+
</Button>
|
|
802
|
+
)}
|
|
803
|
+
<Button
|
|
804
|
+
size="sm"
|
|
805
|
+
className="gap-2"
|
|
806
|
+
onClick={() => setAddAlunoDialogOpen(true)}
|
|
807
|
+
>
|
|
808
|
+
<UserPlus className="size-4" />
|
|
809
|
+
{t('students.actions.addStudent')}
|
|
810
|
+
</Button>
|
|
811
|
+
</div>
|
|
812
|
+
</div>
|
|
813
|
+
|
|
814
|
+
{/* Selection info */}
|
|
815
|
+
{selectedAlunos.length > 0 && (
|
|
816
|
+
<div className="mb-4 flex items-center gap-3 rounded-lg border bg-muted/50 px-4 py-2 text-sm">
|
|
817
|
+
<Checkbox
|
|
818
|
+
checked={selectedAlunos.length === filteredAlunos.length}
|
|
819
|
+
onCheckedChange={(checked) =>
|
|
820
|
+
setSelectedAlunos(
|
|
821
|
+
checked ? filteredAlunos.map((a) => a.id) : []
|
|
822
|
+
)
|
|
823
|
+
}
|
|
824
|
+
/>
|
|
825
|
+
<span>
|
|
826
|
+
{t('students.selectedCount', {
|
|
827
|
+
count: selectedAlunos.length,
|
|
828
|
+
})}
|
|
829
|
+
</span>
|
|
830
|
+
<Button
|
|
831
|
+
variant="ghost"
|
|
832
|
+
size="sm"
|
|
833
|
+
onClick={() => setSelectedAlunos([])}
|
|
834
|
+
>
|
|
835
|
+
{t('students.actions.clearSelection')}
|
|
836
|
+
</Button>
|
|
837
|
+
</div>
|
|
838
|
+
)}
|
|
839
|
+
|
|
840
|
+
{/* Alunos grid */}
|
|
841
|
+
{loading ? (
|
|
842
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
843
|
+
{Array.from({ length: 6 }).map((_, i) => (
|
|
844
|
+
<Card key={i}>
|
|
845
|
+
<CardContent className="p-4">
|
|
846
|
+
<Skeleton className="mb-2 h-12 w-12 rounded-full" />
|
|
847
|
+
<Skeleton className="h-4 w-32" />
|
|
848
|
+
<Skeleton className="mt-1 h-3 w-24" />
|
|
849
|
+
</CardContent>
|
|
850
|
+
</Card>
|
|
851
|
+
))}
|
|
852
|
+
</div>
|
|
853
|
+
) : filteredAlunos.length === 0 ? (
|
|
854
|
+
<Card>
|
|
855
|
+
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
|
856
|
+
<Users className="mb-4 size-12 text-muted-foreground/50" />
|
|
857
|
+
<p className="text-muted-foreground">
|
|
858
|
+
{alunoSearch
|
|
859
|
+
? t('students.empty.notFound')
|
|
860
|
+
: t('students.empty.notEnrolled')}
|
|
861
|
+
</p>
|
|
862
|
+
</CardContent>
|
|
863
|
+
</Card>
|
|
864
|
+
) : (
|
|
865
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
866
|
+
{filteredAlunos.map((aluno) => {
|
|
867
|
+
const isSelected = selectedAlunos.includes(aluno.id);
|
|
868
|
+
return (
|
|
869
|
+
<Card
|
|
870
|
+
key={aluno.id}
|
|
871
|
+
className={`group cursor-pointer transition-all hover:shadow-md ${isSelected ? 'ring-2 ring-primary' : ''}`}
|
|
872
|
+
onClick={(e) => toggleSelectAluno(aluno.id, e)}
|
|
873
|
+
>
|
|
874
|
+
<CardContent className="p-4">
|
|
875
|
+
<div className="flex items-start gap-3">
|
|
876
|
+
<div className="relative">
|
|
877
|
+
<Avatar className="size-12">
|
|
878
|
+
<AvatarImage src={aluno.avatar} />
|
|
879
|
+
<AvatarFallback className="bg-gradient-to-br from-blue-100 to-blue-200 text-blue-700 font-medium">
|
|
880
|
+
{aluno.nome
|
|
881
|
+
.split(' ')
|
|
882
|
+
.map((n) => n[0])
|
|
883
|
+
.join('')
|
|
884
|
+
.slice(0, 2)}
|
|
885
|
+
</AvatarFallback>
|
|
886
|
+
</Avatar>
|
|
887
|
+
{isSelected && (
|
|
888
|
+
<div className="absolute -right-1 -top-1 flex size-5 items-center justify-center rounded-full bg-primary text-primary-foreground">
|
|
889
|
+
<Check className="size-3" />
|
|
890
|
+
</div>
|
|
891
|
+
)}
|
|
892
|
+
</div>
|
|
893
|
+
<div className="flex-1 min-w-0">
|
|
894
|
+
<div className="flex items-start justify-between gap-2">
|
|
895
|
+
<div>
|
|
896
|
+
<h4 className="font-semibold truncate">
|
|
897
|
+
{aluno.nome}
|
|
898
|
+
</h4>
|
|
899
|
+
<p className="text-xs text-muted-foreground truncate">
|
|
900
|
+
{aluno.email}
|
|
901
|
+
</p>
|
|
902
|
+
</div>
|
|
903
|
+
<DropdownMenu>
|
|
904
|
+
<DropdownMenuTrigger asChild>
|
|
905
|
+
<Button
|
|
906
|
+
variant="ghost"
|
|
907
|
+
size="icon"
|
|
908
|
+
className="size-8"
|
|
909
|
+
onClick={(e) => e.stopPropagation()}
|
|
910
|
+
>
|
|
911
|
+
<MoreHorizontal className="size-4" />
|
|
912
|
+
</Button>
|
|
913
|
+
</DropdownMenuTrigger>
|
|
914
|
+
<DropdownMenuContent align="end">
|
|
915
|
+
<DropdownMenuItem>
|
|
916
|
+
<Eye className="mr-2 size-4" />
|
|
917
|
+
{t('students.menu.viewProfile')}
|
|
918
|
+
</DropdownMenuItem>
|
|
919
|
+
<DropdownMenuItem>
|
|
920
|
+
<Mail className="mr-2 size-4" />
|
|
921
|
+
{t('students.menu.sendEmail')}
|
|
922
|
+
</DropdownMenuItem>
|
|
923
|
+
<DropdownMenuSeparator />
|
|
924
|
+
<DropdownMenuItem
|
|
925
|
+
className="text-destructive"
|
|
926
|
+
onClick={(e) => {
|
|
927
|
+
e.stopPropagation();
|
|
928
|
+
setAlunoToRemove(aluno);
|
|
929
|
+
setRemoveAlunoDialogOpen(true);
|
|
930
|
+
}}
|
|
931
|
+
>
|
|
932
|
+
<UserMinus className="mr-2 size-4" />
|
|
933
|
+
{t('students.menu.removeFromClass')}
|
|
934
|
+
</DropdownMenuItem>
|
|
935
|
+
</DropdownMenuContent>
|
|
936
|
+
</DropdownMenu>
|
|
937
|
+
</div>
|
|
938
|
+
</div>
|
|
939
|
+
</div>
|
|
940
|
+
<div className="mt-4 grid grid-cols-2 gap-2">
|
|
941
|
+
<div className="rounded-lg bg-muted/50 p-2 text-center">
|
|
942
|
+
<p className="text-lg font-bold text-blue-600">
|
|
943
|
+
{aluno.progresso}%
|
|
944
|
+
</p>
|
|
945
|
+
<p className="text-[10px] text-muted-foreground">
|
|
946
|
+
{t('students.progress')}
|
|
947
|
+
</p>
|
|
948
|
+
</div>
|
|
949
|
+
<div className="rounded-lg bg-muted/50 p-2 text-center">
|
|
950
|
+
<p
|
|
951
|
+
className={`text-lg font-bold ${aluno.presenca >= 75 ? 'text-emerald-600' : aluno.presenca >= 50 ? 'text-amber-600' : 'text-red-600'}`}
|
|
952
|
+
>
|
|
953
|
+
{aluno.presenca}%
|
|
954
|
+
</p>
|
|
955
|
+
<p className="text-[10px] text-muted-foreground">
|
|
956
|
+
{t('students.attendance')}
|
|
957
|
+
</p>
|
|
958
|
+
</div>
|
|
959
|
+
</div>
|
|
960
|
+
</CardContent>
|
|
961
|
+
</Card>
|
|
962
|
+
);
|
|
963
|
+
})}
|
|
964
|
+
</div>
|
|
965
|
+
)}
|
|
966
|
+
</TabsContent>
|
|
967
|
+
|
|
968
|
+
{/* ── Tab Calendario ────────────────────────────────────────────── */}
|
|
969
|
+
<TabsContent value="calendario" className="mt-0">
|
|
970
|
+
<div className="mb-4 flex items-center justify-between">
|
|
971
|
+
<p className="text-sm text-muted-foreground">
|
|
972
|
+
{t('calendar.helper')}
|
|
973
|
+
</p>
|
|
974
|
+
<Button
|
|
975
|
+
size="sm"
|
|
976
|
+
className="gap-2"
|
|
977
|
+
onClick={() => openAulaSheet()}
|
|
978
|
+
>
|
|
979
|
+
<Plus className="size-4" />
|
|
980
|
+
{t('actions.newLesson')}
|
|
981
|
+
</Button>
|
|
982
|
+
</div>
|
|
983
|
+
<Card>
|
|
984
|
+
<CardContent className="p-4">
|
|
985
|
+
<div className="h-[600px]">
|
|
986
|
+
<Calendar
|
|
987
|
+
localizer={localizer}
|
|
988
|
+
events={calendarEvents}
|
|
989
|
+
startAccessor="start"
|
|
990
|
+
endAccessor="end"
|
|
991
|
+
view={calendarView}
|
|
992
|
+
onView={(v) => setCalendarView(v)}
|
|
993
|
+
date={calendarDate}
|
|
994
|
+
onNavigate={(d) => setCalendarDate(d)}
|
|
995
|
+
views={['month', 'week']}
|
|
996
|
+
messages={calendarMessages}
|
|
997
|
+
eventPropGetter={eventStyleGetter}
|
|
998
|
+
onSelectEvent={handleSelectEvent}
|
|
999
|
+
culture={calendarCulture}
|
|
1000
|
+
popup
|
|
1001
|
+
selectable
|
|
1002
|
+
style={{ height: '100%' }}
|
|
1003
|
+
/>
|
|
1004
|
+
</div>
|
|
1005
|
+
</CardContent>
|
|
1006
|
+
</Card>
|
|
1007
|
+
</TabsContent>
|
|
1008
|
+
|
|
1009
|
+
{/* ── Tab Presenca ──────────────────────────────────────────────── */}
|
|
1010
|
+
<TabsContent value="presenca" className="mt-0">
|
|
1011
|
+
<p className="mb-4 text-sm text-muted-foreground">
|
|
1012
|
+
{t('attendance.helper')}
|
|
1013
|
+
</p>
|
|
1014
|
+
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
1015
|
+
{loading
|
|
1016
|
+
? Array.from({ length: 6 }).map((_, i) => (
|
|
1017
|
+
<Card key={i}>
|
|
1018
|
+
<CardContent className="p-4">
|
|
1019
|
+
<Skeleton className="h-20" />
|
|
1020
|
+
</CardContent>
|
|
1021
|
+
</Card>
|
|
1022
|
+
))
|
|
1023
|
+
: aulas
|
|
1024
|
+
.filter((a) => a.data <= new Date())
|
|
1025
|
+
.slice(-12)
|
|
1026
|
+
.reverse()
|
|
1027
|
+
.map((aula) => (
|
|
1028
|
+
<Card
|
|
1029
|
+
key={aula.id}
|
|
1030
|
+
className="cursor-pointer transition-all hover:shadow-md hover:-translate-y-0.5"
|
|
1031
|
+
onClick={() => openPresenca(aula)}
|
|
1032
|
+
>
|
|
1033
|
+
<CardContent className="p-4">
|
|
1034
|
+
<div className="flex items-start justify-between gap-2">
|
|
1035
|
+
<div>
|
|
1036
|
+
<h4 className="font-semibold">
|
|
1037
|
+
{aula.titulo}
|
|
1038
|
+
</h4>
|
|
1039
|
+
<p className="text-xs text-muted-foreground">
|
|
1040
|
+
{format(aula.data, 'EEEE, dd/MM', {
|
|
1041
|
+
locale: dateLocale,
|
|
1042
|
+
})}
|
|
1043
|
+
</p>
|
|
1044
|
+
</div>
|
|
1045
|
+
<Badge
|
|
1046
|
+
variant={
|
|
1047
|
+
aula.tipo === 'online'
|
|
1048
|
+
? 'secondary'
|
|
1049
|
+
: 'outline'
|
|
1050
|
+
}
|
|
1051
|
+
className="text-[10px]"
|
|
1052
|
+
>
|
|
1053
|
+
{aula.tipo === 'online' ? (
|
|
1054
|
+
<Video className="mr-1 size-3" />
|
|
1055
|
+
) : (
|
|
1056
|
+
<MapPin className="mr-1 size-3" />
|
|
1057
|
+
)}
|
|
1058
|
+
{tClasses(`type.${aula.tipo}`)}
|
|
1059
|
+
</Badge>
|
|
1060
|
+
</div>
|
|
1061
|
+
<div className="mt-3 flex items-center justify-between text-sm">
|
|
1062
|
+
<span className="text-muted-foreground">
|
|
1063
|
+
{aula.horaInicio} - {aula.horaFim}
|
|
1064
|
+
</span>
|
|
1065
|
+
<Button
|
|
1066
|
+
variant="ghost"
|
|
1067
|
+
size="sm"
|
|
1068
|
+
className="h-7 gap-1 text-xs"
|
|
1069
|
+
>
|
|
1070
|
+
<CheckCircle2 className="size-3" />
|
|
1071
|
+
{t('attendance.register')}
|
|
1072
|
+
</Button>
|
|
1073
|
+
</div>
|
|
1074
|
+
</CardContent>
|
|
1075
|
+
</Card>
|
|
1076
|
+
))}
|
|
1077
|
+
</div>
|
|
1078
|
+
</TabsContent>
|
|
1079
|
+
</Tabs>
|
|
1080
|
+
</motion.div>
|
|
1081
|
+
</motion.div>
|
|
1082
|
+
</div>
|
|
1083
|
+
|
|
1084
|
+
{/* ── Dialog Adicionar Alunos ──────────────────────────────────────────── */}
|
|
1085
|
+
<Dialog open={addAlunoDialogOpen} onOpenChange={setAddAlunoDialogOpen}>
|
|
1086
|
+
<DialogContent className=" max-w-3xl">
|
|
1087
|
+
<DialogHeader>
|
|
1088
|
+
<DialogTitle>{t('dialogs.addStudents.title')}</DialogTitle>
|
|
1089
|
+
<DialogDescription>
|
|
1090
|
+
{t('dialogs.addStudents.description')}
|
|
1091
|
+
</DialogDescription>
|
|
1092
|
+
</DialogHeader>
|
|
1093
|
+
<div className="space-y-2 max-h-64 overflow-y-auto py-2">
|
|
1094
|
+
{ALUNOS_DISPONIVEIS.map((aluno) => (
|
|
1095
|
+
<div
|
|
1096
|
+
key={aluno.id}
|
|
1097
|
+
className="flex items-center gap-3 rounded-lg border p-3 hover:bg-muted/50 cursor-pointer"
|
|
1098
|
+
onClick={() =>
|
|
1099
|
+
setAlunosToAdd((prev) =>
|
|
1100
|
+
prev.includes(aluno.id)
|
|
1101
|
+
? prev.filter((x) => x !== aluno.id)
|
|
1102
|
+
: [...prev, aluno.id]
|
|
1103
|
+
)
|
|
1104
|
+
}
|
|
1105
|
+
>
|
|
1106
|
+
<Checkbox checked={alunosToAdd.includes(aluno.id)} />
|
|
1107
|
+
<Avatar className="size-9">
|
|
1108
|
+
<AvatarFallback className="text-xs">
|
|
1109
|
+
{aluno.nome
|
|
1110
|
+
.split(' ')
|
|
1111
|
+
.map((n) => n[0])
|
|
1112
|
+
.join('')
|
|
1113
|
+
.slice(0, 2)}
|
|
1114
|
+
</AvatarFallback>
|
|
1115
|
+
</Avatar>
|
|
1116
|
+
<div className="flex-1 min-w-0">
|
|
1117
|
+
<p className="text-sm font-medium truncate">{aluno.nome}</p>
|
|
1118
|
+
<p className="text-xs text-muted-foreground truncate">
|
|
1119
|
+
{aluno.email}
|
|
1120
|
+
</p>
|
|
1121
|
+
</div>
|
|
1122
|
+
</div>
|
|
1123
|
+
))}
|
|
1124
|
+
</div>
|
|
1125
|
+
<DialogFooter>
|
|
1126
|
+
<Button
|
|
1127
|
+
variant="outline"
|
|
1128
|
+
onClick={() => {
|
|
1129
|
+
setAddAlunoDialogOpen(false);
|
|
1130
|
+
setAlunosToAdd([]);
|
|
1131
|
+
}}
|
|
1132
|
+
>
|
|
1133
|
+
{t('common.cancel')}
|
|
1134
|
+
</Button>
|
|
1135
|
+
<Button
|
|
1136
|
+
onClick={handleAddAlunos}
|
|
1137
|
+
disabled={alunosToAdd.length === 0}
|
|
1138
|
+
>
|
|
1139
|
+
{t('dialogs.addStudents.confirm')}{' '}
|
|
1140
|
+
{alunosToAdd.length > 0 && `(${alunosToAdd.length})`}
|
|
1141
|
+
</Button>
|
|
1142
|
+
</DialogFooter>
|
|
1143
|
+
</DialogContent>
|
|
1144
|
+
</Dialog>
|
|
1145
|
+
|
|
1146
|
+
{/* ── Dialog Remover Aluno ─────────────────────────────────────────────── */}
|
|
1147
|
+
<Dialog
|
|
1148
|
+
open={removeAlunoDialogOpen}
|
|
1149
|
+
onOpenChange={setRemoveAlunoDialogOpen}
|
|
1150
|
+
>
|
|
1151
|
+
<DialogContent className="max-w-3xl">
|
|
1152
|
+
<DialogHeader>
|
|
1153
|
+
<DialogTitle className="flex items-center gap-2">
|
|
1154
|
+
<AlertTriangle className="size-5 text-destructive" />
|
|
1155
|
+
{t('dialogs.removeStudent.title')}
|
|
1156
|
+
</DialogTitle>
|
|
1157
|
+
<DialogDescription>
|
|
1158
|
+
{t('dialogs.removeStudent.descriptionPrefix')}{' '}
|
|
1159
|
+
<strong>{alunoToRemove?.nome}</strong>{' '}
|
|
1160
|
+
{t('dialogs.removeStudent.descriptionSuffix')}
|
|
1161
|
+
</DialogDescription>
|
|
1162
|
+
</DialogHeader>
|
|
1163
|
+
<DialogFooter>
|
|
1164
|
+
<Button
|
|
1165
|
+
variant="outline"
|
|
1166
|
+
onClick={() => {
|
|
1167
|
+
setRemoveAlunoDialogOpen(false);
|
|
1168
|
+
setAlunoToRemove(null);
|
|
1169
|
+
}}
|
|
1170
|
+
>
|
|
1171
|
+
{t('common.cancel')}
|
|
1172
|
+
</Button>
|
|
1173
|
+
<Button variant="destructive" onClick={handleRemoveAluno}>
|
|
1174
|
+
{t('common.remove')}
|
|
1175
|
+
</Button>
|
|
1176
|
+
</DialogFooter>
|
|
1177
|
+
</DialogContent>
|
|
1178
|
+
</Dialog>
|
|
1179
|
+
|
|
1180
|
+
{/* ── Sheet Aula ───────────────────────────────────────────────────────── */}
|
|
1181
|
+
<Sheet open={aulaSheetOpen} onOpenChange={setAulaSheetOpen}>
|
|
1182
|
+
<SheetContent className="w-full sm:max-w-lg overflow-y-auto">
|
|
1183
|
+
<SheetHeader>
|
|
1184
|
+
<SheetTitle>
|
|
1185
|
+
{editingAula
|
|
1186
|
+
? t('sheet.lessonForm.titleEdit')
|
|
1187
|
+
: t('sheet.lessonForm.titleCreate')}
|
|
1188
|
+
</SheetTitle>
|
|
1189
|
+
<SheetDescription>
|
|
1190
|
+
{editingAula
|
|
1191
|
+
? t('sheet.lessonForm.descriptionEdit')
|
|
1192
|
+
: t('sheet.lessonForm.descriptionCreate')}
|
|
1193
|
+
</SheetDescription>
|
|
1194
|
+
</SheetHeader>
|
|
1195
|
+
<form onSubmit={handleSaveAula} className="mt-6 space-y-5 px-4">
|
|
1196
|
+
<Field>
|
|
1197
|
+
<FieldLabel>{t('sheet.lessonForm.fields.title')}</FieldLabel>
|
|
1198
|
+
<Input
|
|
1199
|
+
{...aulaForm.register('titulo')}
|
|
1200
|
+
placeholder={t('sheet.lessonForm.fields.titlePlaceholder')}
|
|
1201
|
+
/>
|
|
1202
|
+
{aulaForm.formState.errors.titulo && (
|
|
1203
|
+
<FieldError>
|
|
1204
|
+
{aulaForm.formState.errors.titulo.message}
|
|
1205
|
+
</FieldError>
|
|
1206
|
+
)}
|
|
1207
|
+
</Field>
|
|
1208
|
+
|
|
1209
|
+
<div className="grid grid-cols-2 gap-4">
|
|
1210
|
+
<Field>
|
|
1211
|
+
<FieldLabel>{t('sheet.lessonForm.fields.date')}</FieldLabel>
|
|
1212
|
+
<Input type="date" {...aulaForm.register('data')} />
|
|
1213
|
+
{aulaForm.formState.errors.data && (
|
|
1214
|
+
<FieldError>
|
|
1215
|
+
{aulaForm.formState.errors.data.message}
|
|
1216
|
+
</FieldError>
|
|
1217
|
+
)}
|
|
1218
|
+
</Field>
|
|
1219
|
+
<Field>
|
|
1220
|
+
<FieldLabel>{t('sheet.lessonForm.fields.type')}</FieldLabel>
|
|
1221
|
+
<Controller
|
|
1222
|
+
name="tipo"
|
|
1223
|
+
control={aulaForm.control}
|
|
1224
|
+
render={({ field }) => (
|
|
1225
|
+
<Select value={field.value} onValueChange={field.onChange}>
|
|
1226
|
+
<SelectTrigger>
|
|
1227
|
+
<SelectValue
|
|
1228
|
+
placeholder={t('sheet.lessonForm.fields.select')}
|
|
1229
|
+
/>
|
|
1230
|
+
</SelectTrigger>
|
|
1231
|
+
<SelectContent>
|
|
1232
|
+
<SelectItem value="online">
|
|
1233
|
+
{tClasses('type.online')}
|
|
1234
|
+
</SelectItem>
|
|
1235
|
+
<SelectItem value="presencial">
|
|
1236
|
+
{tClasses('type.presencial')}
|
|
1237
|
+
</SelectItem>
|
|
1238
|
+
</SelectContent>
|
|
1239
|
+
</Select>
|
|
1240
|
+
)}
|
|
1241
|
+
/>
|
|
1242
|
+
</Field>
|
|
1243
|
+
</div>
|
|
1244
|
+
|
|
1245
|
+
<div className="grid grid-cols-2 gap-4">
|
|
1246
|
+
<Field>
|
|
1247
|
+
<FieldLabel>
|
|
1248
|
+
{t('sheet.lessonForm.fields.startTime')}
|
|
1249
|
+
</FieldLabel>
|
|
1250
|
+
<Input type="time" {...aulaForm.register('horaInicio')} />
|
|
1251
|
+
</Field>
|
|
1252
|
+
<Field>
|
|
1253
|
+
<FieldLabel>{t('sheet.lessonForm.fields.endTime')}</FieldLabel>
|
|
1254
|
+
<Input type="time" {...aulaForm.register('horaFim')} />
|
|
1255
|
+
</Field>
|
|
1256
|
+
</div>
|
|
1257
|
+
|
|
1258
|
+
<Field>
|
|
1259
|
+
<FieldLabel>{t('sheet.lessonForm.fields.location')}</FieldLabel>
|
|
1260
|
+
<Input
|
|
1261
|
+
{...aulaForm.register('local')}
|
|
1262
|
+
placeholder={t('sheet.lessonForm.fields.locationPlaceholder')}
|
|
1263
|
+
/>
|
|
1264
|
+
{aulaForm.formState.errors.local && (
|
|
1265
|
+
<FieldError>
|
|
1266
|
+
{aulaForm.formState.errors.local.message}
|
|
1267
|
+
</FieldError>
|
|
1268
|
+
)}
|
|
1269
|
+
</Field>
|
|
1270
|
+
|
|
1271
|
+
<SheetFooter className="mt-6 px-0">
|
|
1272
|
+
<Button type="submit">
|
|
1273
|
+
{editingAula
|
|
1274
|
+
? t('sheet.lessonForm.actions.save')
|
|
1275
|
+
: t('sheet.lessonForm.actions.create')}
|
|
1276
|
+
</Button>
|
|
1277
|
+
</SheetFooter>
|
|
1278
|
+
</form>
|
|
1279
|
+
</SheetContent>
|
|
1280
|
+
</Sheet>
|
|
1281
|
+
|
|
1282
|
+
{/* ── Sheet Presenca ───────────────────────────────────────────────────── */}
|
|
1283
|
+
<Sheet open={presencaSheetOpen} onOpenChange={setPresencaSheetOpen}>
|
|
1284
|
+
<SheetContent className="w-full sm:max-w-xl overflow-y-auto">
|
|
1285
|
+
<SheetHeader>
|
|
1286
|
+
<SheetTitle>{t('sheet.attendance.title')}</SheetTitle>
|
|
1287
|
+
<SheetDescription>
|
|
1288
|
+
{selectedAulaForPresenca && (
|
|
1289
|
+
<>
|
|
1290
|
+
<strong>{selectedAulaForPresenca.titulo}</strong> -{' '}
|
|
1291
|
+
{format(selectedAulaForPresenca.data, 'dd/MM/yyyy')}
|
|
1292
|
+
</>
|
|
1293
|
+
)}
|
|
1294
|
+
</SheetDescription>
|
|
1295
|
+
</SheetHeader>
|
|
1296
|
+
|
|
1297
|
+
<div className="mt-6 space-y-2 px-4">
|
|
1298
|
+
<div className="flex items-center justify-between text-sm text-muted-foreground mb-3">
|
|
1299
|
+
<span>
|
|
1300
|
+
{t('sheet.attendance.summary', {
|
|
1301
|
+
present: presencaList.filter((p) => p.presente).length,
|
|
1302
|
+
total: presencaList.length,
|
|
1303
|
+
})}
|
|
1304
|
+
</span>
|
|
1305
|
+
<div className="flex gap-2">
|
|
1306
|
+
<Button
|
|
1307
|
+
variant="outline"
|
|
1308
|
+
size="sm"
|
|
1309
|
+
onClick={() =>
|
|
1310
|
+
setPresencaList((prev) =>
|
|
1311
|
+
prev.map((p) => ({ ...p, presente: true }))
|
|
1312
|
+
)
|
|
1313
|
+
}
|
|
1314
|
+
>
|
|
1315
|
+
{t('sheet.attendance.allPresent')}
|
|
1316
|
+
</Button>
|
|
1317
|
+
<Button
|
|
1318
|
+
variant="outline"
|
|
1319
|
+
size="sm"
|
|
1320
|
+
onClick={() =>
|
|
1321
|
+
setPresencaList((prev) =>
|
|
1322
|
+
prev.map((p) => ({ ...p, presente: false }))
|
|
1323
|
+
)
|
|
1324
|
+
}
|
|
1325
|
+
>
|
|
1326
|
+
{t('sheet.attendance.allAbsent')}
|
|
1327
|
+
</Button>
|
|
1328
|
+
</div>
|
|
1329
|
+
</div>
|
|
1330
|
+
|
|
1331
|
+
{alunos.map((aluno) => {
|
|
1332
|
+
const presenca = presencaList.find((p) => p.alunoId === aluno.id);
|
|
1333
|
+
if (!presenca) return null;
|
|
1334
|
+
return (
|
|
1335
|
+
<div
|
|
1336
|
+
key={aluno.id}
|
|
1337
|
+
className={`flex items-center justify-between rounded-lg border p-3 transition-colors ${presenca.presente ? 'bg-emerald-50/50 border-emerald-200' : 'bg-red-50/50 border-red-200'}`}
|
|
1338
|
+
>
|
|
1339
|
+
<div className="flex items-center gap-3">
|
|
1340
|
+
<Avatar className="size-9">
|
|
1341
|
+
<AvatarFallback className="text-xs">
|
|
1342
|
+
{aluno.nome
|
|
1343
|
+
.split(' ')
|
|
1344
|
+
.map((n) => n[0])
|
|
1345
|
+
.join('')
|
|
1346
|
+
.slice(0, 2)}
|
|
1347
|
+
</AvatarFallback>
|
|
1348
|
+
</Avatar>
|
|
1349
|
+
<span className="font-medium">{aluno.nome}</span>
|
|
1350
|
+
</div>
|
|
1351
|
+
<div className="flex items-center gap-3">
|
|
1352
|
+
<span
|
|
1353
|
+
className={`text-sm font-medium ${presenca.presente ? 'text-emerald-600' : 'text-red-600'}`}
|
|
1354
|
+
>
|
|
1355
|
+
{presenca.presente
|
|
1356
|
+
? t('sheet.attendance.present')
|
|
1357
|
+
: t('sheet.attendance.absent')}
|
|
1358
|
+
</span>
|
|
1359
|
+
<Switch
|
|
1360
|
+
checked={presenca.presente}
|
|
1361
|
+
onCheckedChange={() => togglePresenca(aluno.id)}
|
|
1362
|
+
/>
|
|
1363
|
+
</div>
|
|
1364
|
+
</div>
|
|
1365
|
+
);
|
|
1366
|
+
})}
|
|
1367
|
+
</div>
|
|
1368
|
+
|
|
1369
|
+
<SheetFooter className="mt-6">
|
|
1370
|
+
<Button
|
|
1371
|
+
onClick={handleSavePresenca}
|
|
1372
|
+
disabled={savingPresenca}
|
|
1373
|
+
className="gap-2"
|
|
1374
|
+
>
|
|
1375
|
+
{savingPresenca ? (
|
|
1376
|
+
<Loader2 className="size-4 animate-spin" />
|
|
1377
|
+
) : (
|
|
1378
|
+
<Save className="size-4" />
|
|
1379
|
+
)}
|
|
1380
|
+
{t('sheet.attendance.save')}
|
|
1381
|
+
</Button>
|
|
1382
|
+
</SheetFooter>
|
|
1383
|
+
</SheetContent>
|
|
1384
|
+
</Sheet>
|
|
1385
|
+
</Page>
|
|
1386
|
+
);
|
|
1387
|
+
}
|