@hed-hog/lms 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1394 @@
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 { Card, CardContent } from '@/components/ui/card';
7
+ import { Checkbox } from '@/components/ui/checkbox';
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogFooter,
13
+ DialogHeader,
14
+ DialogTitle,
15
+ } from '@/components/ui/dialog';
16
+ import {
17
+ DropdownMenu,
18
+ DropdownMenuContent,
19
+ DropdownMenuItem,
20
+ DropdownMenuSeparator,
21
+ DropdownMenuTrigger,
22
+ } from '@/components/ui/dropdown-menu';
23
+ import {
24
+ Field,
25
+ FieldDescription,
26
+ FieldError,
27
+ FieldLabel,
28
+ } from '@/components/ui/field';
29
+ import { Input } from '@/components/ui/input';
30
+ import {
31
+ Select,
32
+ SelectContent,
33
+ SelectItem,
34
+ SelectTrigger,
35
+ SelectValue,
36
+ } from '@/components/ui/select';
37
+ import { Separator } from '@/components/ui/separator';
38
+ import {
39
+ Sheet,
40
+ SheetContent,
41
+ SheetDescription,
42
+ SheetFooter,
43
+ SheetHeader,
44
+ SheetTitle,
45
+ } from '@/components/ui/sheet';
46
+ import { Skeleton } from '@/components/ui/skeleton';
47
+ import { Switch } from '@/components/ui/switch';
48
+ import {
49
+ Table,
50
+ TableBody,
51
+ TableCell,
52
+ TableHead,
53
+ TableHeader,
54
+ TableRow,
55
+ } from '@/components/ui/table';
56
+ import { Textarea } from '@/components/ui/textarea';
57
+ import { zodResolver } from '@hookform/resolvers/zod';
58
+ import { AnimatePresence, motion } from 'framer-motion';
59
+ import {
60
+ AlertTriangle,
61
+ Archive,
62
+ BarChart3,
63
+ BookOpen,
64
+ CheckSquare,
65
+ ChevronLeft,
66
+ ChevronRight,
67
+ Copy,
68
+ Eye,
69
+ FileCheck,
70
+ Filter,
71
+ GraduationCap,
72
+ Hash,
73
+ LayoutDashboard,
74
+ Loader2,
75
+ MoreHorizontal,
76
+ Pencil,
77
+ Plus,
78
+ Search,
79
+ Trash2,
80
+ Users,
81
+ X,
82
+ } from 'lucide-react';
83
+ import { usePathname, useRouter } from 'next/navigation';
84
+ import { useEffect, useMemo, useState } from 'react';
85
+ import { Controller, useForm } from 'react-hook-form';
86
+ import { toast } from 'sonner';
87
+ import { z } from 'zod';
88
+
89
+ // ── Navigation ───────────────────────────────────────────────────────────────
90
+
91
+ const NAV_ITEMS = [
92
+ { label: 'Dashboard', href: '/', icon: LayoutDashboard },
93
+ { label: 'Cursos', href: '/cursos', icon: BookOpen },
94
+ { label: 'Turmas', href: '/turmas', icon: Users },
95
+ { label: 'Exames', href: '/exames', icon: FileCheck },
96
+ { label: 'Formacoes', href: '/formacoes', icon: GraduationCap },
97
+ { label: 'Relatorios', href: '/relatorios', icon: BarChart3 },
98
+ ];
99
+
100
+ // ── Types ────────────────────────────────────────────────────────────────────
101
+
102
+ interface Curso {
103
+ id: number;
104
+ codigo: string;
105
+ nomeInterno: string;
106
+ tituloComercial: string;
107
+ descricao: string;
108
+ nivel: 'iniciante' | 'intermediario' | 'avancado';
109
+ status: 'ativo' | 'rascunho' | 'arquivado';
110
+ categorias: string[];
111
+ destaque: boolean;
112
+ certificado: boolean;
113
+ listado: boolean;
114
+ alunosInscritos: number;
115
+ criadoEm: string;
116
+ }
117
+
118
+ // ── Schema ───────────────────────────────────────────────────────────────────
119
+
120
+ const cursoSchema = z.object({
121
+ codigo: z
122
+ .string()
123
+ .min(2, 'Codigo deve ter pelo menos 2 caracteres')
124
+ .max(16, 'Codigo deve ter no maximo 16 caracteres')
125
+ .regex(/^[A-Z0-9-]+$/i, 'Apenas letras, numeros e hifens'),
126
+ nomeInterno: z
127
+ .string()
128
+ .min(3, 'Nome interno deve ter pelo menos 3 caracteres'),
129
+ tituloComercial: z
130
+ .string()
131
+ .min(3, 'Titulo comercial deve ter pelo menos 3 caracteres'),
132
+ descricao: z.string().min(10, 'Descricao deve ter pelo menos 10 caracteres'),
133
+ nivel: z.enum(['iniciante', 'intermediario', 'avancado'], {
134
+ errorMap: () => ({ message: 'Selecione um nivel' }),
135
+ }),
136
+ status: z.enum(['ativo', 'rascunho', 'arquivado'], {
137
+ errorMap: () => ({ message: 'Selecione um status' }),
138
+ }),
139
+ categorias: z.array(z.string()).min(1, 'Selecione pelo menos uma categoria'),
140
+ destaque: z.boolean(),
141
+ certificado: z.boolean(),
142
+ listado: z.boolean(),
143
+ });
144
+
145
+ type CursoForm = z.infer<typeof cursoSchema>;
146
+
147
+ // ── Constants ────────────────────────────────────────────────────────────────
148
+
149
+ const CATEGORIAS = [
150
+ 'Tecnologia',
151
+ 'Design',
152
+ 'Gestao',
153
+ 'Marketing',
154
+ 'Financas',
155
+ 'Saude',
156
+ 'Idiomas',
157
+ 'Direito',
158
+ ];
159
+
160
+ const NIVEIS = [
161
+ { value: 'iniciante', label: 'Iniciante' },
162
+ { value: 'intermediario', label: 'Intermediario' },
163
+ { value: 'avancado', label: 'Avancado' },
164
+ ];
165
+
166
+ const STATUS_OPTIONS = [
167
+ { value: 'ativo', label: 'Ativo' },
168
+ { value: 'rascunho', label: 'Rascunho' },
169
+ { value: 'arquivado', label: 'Arquivado' },
170
+ ];
171
+
172
+ const STATUS_MAP: Record<
173
+ string,
174
+ { label: string; variant: 'default' | 'secondary' | 'outline' }
175
+ > = {
176
+ ativo: { label: 'Ativo', variant: 'default' },
177
+ rascunho: { label: 'Rascunho', variant: 'secondary' },
178
+ arquivado: { label: 'Arquivado', variant: 'outline' },
179
+ };
180
+
181
+ const NIVEL_MAP: Record<string, string> = {
182
+ iniciante: 'Iniciante',
183
+ intermediario: 'Intermediario',
184
+ avancado: 'Avancado',
185
+ };
186
+
187
+ // ── Seed Data ────────────────────────────────────────────────────────────────
188
+
189
+ const initialCursos: Curso[] = [
190
+ {
191
+ id: 1,
192
+ codigo: 'REACT-ADV',
193
+ nomeInterno: 'react-avancado',
194
+ tituloComercial: 'React Avancado',
195
+ descricao:
196
+ 'Curso completo de React com hooks, context e patterns avancados para aplicacoes modernas',
197
+ nivel: 'avancado',
198
+ status: 'ativo',
199
+ categorias: ['Tecnologia'],
200
+ destaque: true,
201
+ certificado: true,
202
+ listado: true,
203
+ alunosInscritos: 245,
204
+ criadoEm: '2025-01-15',
205
+ },
206
+ {
207
+ id: 2,
208
+ codigo: 'UX-FUND',
209
+ nomeInterno: 'ux-fundamentals',
210
+ tituloComercial: 'UX Design Fundamentals',
211
+ descricao:
212
+ 'Fundamentos de design de experiencia do usuario com ferramentas modernas e pesquisa',
213
+ nivel: 'iniciante',
214
+ status: 'ativo',
215
+ categorias: ['Design'],
216
+ destaque: false,
217
+ certificado: true,
218
+ listado: true,
219
+ alunosInscritos: 189,
220
+ criadoEm: '2025-02-10',
221
+ },
222
+ {
223
+ id: 3,
224
+ codigo: 'GEST-AGIL',
225
+ nomeInterno: 'gestao-agil',
226
+ tituloComercial: 'Gestao de Projetos Ageis',
227
+ descricao:
228
+ 'Metodologias ageis para gestao de projetos incluindo Scrum, Kanban e SAFe em equipes',
229
+ nivel: 'intermediario',
230
+ status: 'ativo',
231
+ categorias: ['Gestao'],
232
+ destaque: true,
233
+ certificado: true,
234
+ listado: true,
235
+ alunosInscritos: 312,
236
+ criadoEm: '2025-01-20',
237
+ },
238
+ {
239
+ id: 4,
240
+ codigo: 'MKT-DIG',
241
+ nomeInterno: 'marketing-digital',
242
+ tituloComercial: 'Marketing Digital Completo',
243
+ descricao:
244
+ 'Estrategias de marketing digital para negocios modernos incluindo SEO, SEM e redes sociais',
245
+ nivel: 'intermediario',
246
+ status: 'rascunho',
247
+ categorias: ['Marketing'],
248
+ destaque: false,
249
+ certificado: false,
250
+ listado: false,
251
+ alunosInscritos: 0,
252
+ criadoEm: '2025-03-05',
253
+ },
254
+ {
255
+ id: 5,
256
+ codigo: 'PY-DS',
257
+ nomeInterno: 'python-data-science',
258
+ tituloComercial: 'Python para Data Science',
259
+ descricao:
260
+ 'Introducao a Python focado em ciencia de dados, analise estatistica e machine learning basico',
261
+ nivel: 'intermediario',
262
+ status: 'ativo',
263
+ categorias: ['Tecnologia'],
264
+ destaque: false,
265
+ certificado: true,
266
+ listado: true,
267
+ alunosInscritos: 178,
268
+ criadoEm: '2025-02-28',
269
+ },
270
+ {
271
+ id: 6,
272
+ codigo: 'NODE-API',
273
+ nomeInterno: 'node-completo',
274
+ tituloComercial: 'Node.js Completo',
275
+ descricao:
276
+ 'Backend com Node.js, Express e bancos de dados relacionais e NoSQL para APIs robustas',
277
+ nivel: 'avancado',
278
+ status: 'ativo',
279
+ categorias: ['Tecnologia'],
280
+ destaque: true,
281
+ certificado: true,
282
+ listado: true,
283
+ alunosInscritos: 156,
284
+ criadoEm: '2025-01-10',
285
+ },
286
+ {
287
+ id: 7,
288
+ codigo: 'FIGMA-INI',
289
+ nomeInterno: 'figma-iniciantes',
290
+ tituloComercial: 'Figma para Iniciantes',
291
+ descricao:
292
+ 'Aprenda a usar o Figma do zero para criar interfaces profissionais e prototipos interativos',
293
+ nivel: 'iniciante',
294
+ status: 'arquivado',
295
+ categorias: ['Design'],
296
+ destaque: false,
297
+ certificado: true,
298
+ listado: false,
299
+ alunosInscritos: 420,
300
+ criadoEm: '2024-11-05',
301
+ },
302
+ {
303
+ id: 8,
304
+ codigo: 'LIDER-COM',
305
+ nomeInterno: 'lideranca-comunicacao',
306
+ tituloComercial: 'Lideranca e Comunicacao',
307
+ descricao:
308
+ 'Desenvolva habilidades de lideranca e comunicacao assertiva para ambientes corporativos',
309
+ nivel: 'iniciante',
310
+ status: 'ativo',
311
+ categorias: ['Gestao'],
312
+ destaque: false,
313
+ certificado: true,
314
+ listado: true,
315
+ alunosInscritos: 98,
316
+ criadoEm: '2025-03-12',
317
+ },
318
+ {
319
+ id: 9,
320
+ codigo: 'SEO-ADV',
321
+ nomeInterno: 'seo-avancado',
322
+ tituloComercial: 'SEO Avancado',
323
+ descricao:
324
+ 'Tecnicas avancadas de otimizacao para motores de busca, conteudo web e performance de sites',
325
+ nivel: 'avancado',
326
+ status: 'rascunho',
327
+ categorias: ['Marketing', 'Tecnologia'],
328
+ destaque: false,
329
+ certificado: false,
330
+ listado: false,
331
+ alunosInscritos: 0,
332
+ criadoEm: '2025-04-01',
333
+ },
334
+ {
335
+ id: 10,
336
+ codigo: 'TS-PRAT',
337
+ nomeInterno: 'typescript-pratica',
338
+ tituloComercial: 'TypeScript na Pratica',
339
+ descricao:
340
+ 'TypeScript aplicado em projetos reais com boas praticas, design patterns e testes',
341
+ nivel: 'intermediario',
342
+ status: 'ativo',
343
+ categorias: ['Tecnologia'],
344
+ destaque: true,
345
+ certificado: true,
346
+ listado: true,
347
+ alunosInscritos: 201,
348
+ criadoEm: '2025-02-15',
349
+ },
350
+ {
351
+ id: 11,
352
+ codigo: 'DS-SYS',
353
+ nomeInterno: 'design-system',
354
+ tituloComercial: 'Design System Completo',
355
+ descricao:
356
+ 'Como criar e manter um design system escalavel para grandes equipes de produto e engenharia',
357
+ nivel: 'avancado',
358
+ status: 'ativo',
359
+ categorias: ['Design', 'Tecnologia'],
360
+ destaque: false,
361
+ certificado: true,
362
+ listado: true,
363
+ alunosInscritos: 87,
364
+ criadoEm: '2025-03-20',
365
+ },
366
+ {
367
+ id: 12,
368
+ codigo: 'EXCEL-BIZ',
369
+ nomeInterno: 'excel-negocios',
370
+ tituloComercial: 'Excel para Negocios',
371
+ descricao:
372
+ 'Domine Excel com formulas avancadas, dashboards profissionais e analise de dados empresariais',
373
+ nivel: 'iniciante',
374
+ status: 'ativo',
375
+ categorias: ['Gestao', 'Financas'],
376
+ destaque: false,
377
+ certificado: true,
378
+ listado: true,
379
+ alunosInscritos: 534,
380
+ criadoEm: '2024-10-15',
381
+ },
382
+ {
383
+ id: 13,
384
+ codigo: 'FIN-PESSOAL',
385
+ nomeInterno: 'financas-pessoais',
386
+ tituloComercial: 'Financas Pessoais',
387
+ descricao:
388
+ 'Aprenda a gerenciar suas financas, investir e planejar sua aposentadoria de forma inteligente',
389
+ nivel: 'iniciante',
390
+ status: 'ativo',
391
+ categorias: ['Financas'],
392
+ destaque: false,
393
+ certificado: true,
394
+ listado: true,
395
+ alunosInscritos: 342,
396
+ criadoEm: '2025-01-08',
397
+ },
398
+ {
399
+ id: 14,
400
+ codigo: 'FLUTTER-MOB',
401
+ nomeInterno: 'flutter-mobile',
402
+ tituloComercial: 'Flutter para Mobile',
403
+ descricao:
404
+ 'Desenvolvimento de aplicativos moveis multiplataforma com Flutter e Dart do zero ao deploy',
405
+ nivel: 'intermediario',
406
+ status: 'ativo',
407
+ categorias: ['Tecnologia'],
408
+ destaque: true,
409
+ certificado: true,
410
+ listado: true,
411
+ alunosInscritos: 167,
412
+ criadoEm: '2025-04-02',
413
+ },
414
+ {
415
+ id: 15,
416
+ codigo: 'DIR-TRAB',
417
+ nomeInterno: 'direito-trabalhista',
418
+ tituloComercial: 'Direito Trabalhista Essencial',
419
+ descricao:
420
+ 'Conceitos essenciais de direito trabalhista para gestores de RH e empreendedores',
421
+ nivel: 'iniciante',
422
+ status: 'rascunho',
423
+ categorias: ['Direito', 'Gestao'],
424
+ destaque: false,
425
+ certificado: false,
426
+ listado: false,
427
+ alunosInscritos: 0,
428
+ criadoEm: '2025-04-10',
429
+ },
430
+ ];
431
+
432
+ const ITEMS_PER_PAGE = 8;
433
+
434
+ // ── Animations ───────────────────────────────────────────────────────────────
435
+
436
+ const stagger = {
437
+ hidden: {},
438
+ show: { transition: { staggerChildren: 0.06 } },
439
+ };
440
+
441
+ const fadeUp = {
442
+ hidden: { opacity: 0, y: 16 },
443
+ show: { opacity: 1, y: 0, transition: { duration: 0.35, ease: 'easeOut' } },
444
+ };
445
+
446
+ // ── Component ────────────────────────────────────────────────────────────────
447
+
448
+ export default function CursosPage() {
449
+ const pathname = usePathname();
450
+ const router = useRouter();
451
+
452
+ // UI states
453
+ const [loading, setLoading] = useState(true);
454
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
455
+ const [sheetOpen, setSheetOpen] = useState(false);
456
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
457
+ const [saving, setSaving] = useState(false);
458
+
459
+ // Data
460
+ const [cursos, setCursos] = useState<Curso[]>(initialCursos);
461
+ const [editingCurso, setEditingCurso] = useState<Curso | null>(null);
462
+ const [cursoToDelete, setCursoToDelete] = useState<Curso | null>(null);
463
+
464
+ // Selection
465
+ const [selectedIds, setSelectedIds] = useState<Set<number>>(new Set());
466
+
467
+ // Filters
468
+ const [busca, setBusca] = useState('');
469
+ const [filtroStatus, setFiltroStatus] = useState('todos');
470
+ const [filtroNivel, setFiltroNivel] = useState('todos');
471
+ const [filtroCategoria, setFiltroCategoria] = useState('todos');
472
+
473
+ // Pagination
474
+ const [currentPage, setCurrentPage] = useState(1);
475
+
476
+ const form = useForm<CursoForm>({
477
+ resolver: zodResolver(cursoSchema),
478
+ defaultValues: {
479
+ codigo: '',
480
+ nomeInterno: '',
481
+ tituloComercial: '',
482
+ descricao: '',
483
+ nivel: 'iniciante',
484
+ status: 'rascunho',
485
+ categorias: [],
486
+ destaque: false,
487
+ certificado: true,
488
+ listado: false,
489
+ },
490
+ });
491
+
492
+ useEffect(() => {
493
+ const t = setTimeout(() => setLoading(false), 900);
494
+ return () => clearTimeout(t);
495
+ }, []);
496
+
497
+ // ── Filtering ──────────────────────────────────────────────────────────────
498
+
499
+ const filteredCursos = useMemo(() => {
500
+ return cursos.filter((c) => {
501
+ const q = busca.toLowerCase();
502
+ const matchSearch =
503
+ !q ||
504
+ c.codigo.toLowerCase().includes(q) ||
505
+ c.tituloComercial.toLowerCase().includes(q) ||
506
+ c.nomeInterno.toLowerCase().includes(q);
507
+ const matchStatus = filtroStatus === 'todos' || c.status === filtroStatus;
508
+ const matchNivel = filtroNivel === 'todos' || c.nivel === filtroNivel;
509
+ const matchCategoria =
510
+ filtroCategoria === 'todos' || c.categorias.includes(filtroCategoria);
511
+ return matchSearch && matchStatus && matchNivel && matchCategoria;
512
+ });
513
+ }, [cursos, busca, filtroStatus, filtroNivel, filtroCategoria]);
514
+
515
+ const totalPages = Math.max(
516
+ 1,
517
+ Math.ceil(filteredCursos.length / ITEMS_PER_PAGE)
518
+ );
519
+ const safePage = Math.min(currentPage, totalPages);
520
+ const paginatedCursos = filteredCursos.slice(
521
+ (safePage - 1) * ITEMS_PER_PAGE,
522
+ safePage * ITEMS_PER_PAGE
523
+ );
524
+
525
+ // Reset page on filter change
526
+ useEffect(() => {
527
+ setCurrentPage(1);
528
+ }, [busca, filtroStatus, filtroNivel, filtroCategoria]);
529
+
530
+ // ── Selection ──────────────────────────────────────────────────────────────
531
+
532
+ const allPageSelected =
533
+ paginatedCursos.length > 0 &&
534
+ paginatedCursos.every((c) => selectedIds.has(c.id));
535
+
536
+ function toggleSelectAll() {
537
+ setSelectedIds((prev) => {
538
+ const next = new Set(prev);
539
+ if (allPageSelected) {
540
+ paginatedCursos.forEach((c) => next.delete(c.id));
541
+ } else {
542
+ paginatedCursos.forEach((c) => next.add(c.id));
543
+ }
544
+ return next;
545
+ });
546
+ }
547
+
548
+ function toggleSelect(id: number) {
549
+ setSelectedIds((prev) => {
550
+ const next = new Set(prev);
551
+ if (next.has(id)) {
552
+ next.delete(id);
553
+ } else {
554
+ next.add(id);
555
+ }
556
+ return next;
557
+ });
558
+ }
559
+
560
+ // ── CRUD ───────────────────────────────────────────────────────────────────
561
+
562
+ function openCreateSheet() {
563
+ setEditingCurso(null);
564
+ form.reset({
565
+ codigo: '',
566
+ nomeInterno: '',
567
+ tituloComercial: '',
568
+ descricao: '',
569
+ nivel: 'iniciante',
570
+ status: 'rascunho',
571
+ categorias: [],
572
+ destaque: false,
573
+ certificado: true,
574
+ listado: false,
575
+ });
576
+ setSheetOpen(true);
577
+ }
578
+
579
+ function openEditSheet(curso: Curso) {
580
+ setEditingCurso(curso);
581
+ form.reset({
582
+ codigo: curso.codigo,
583
+ nomeInterno: curso.nomeInterno,
584
+ tituloComercial: curso.tituloComercial,
585
+ descricao: curso.descricao,
586
+ nivel: curso.nivel,
587
+ status: curso.status,
588
+ categorias: curso.categorias,
589
+ destaque: curso.destaque,
590
+ certificado: curso.certificado,
591
+ listado: curso.listado,
592
+ });
593
+ setSheetOpen(true);
594
+ }
595
+
596
+ async function onSubmit(data: CursoForm) {
597
+ setSaving(true);
598
+ // Simula um delay de API
599
+ await new Promise((r) => setTimeout(r, 600));
600
+
601
+ if (editingCurso) {
602
+ setCursos((prev) =>
603
+ prev.map((c) => (c.id === editingCurso.id ? { ...c, ...data } : c))
604
+ );
605
+ toast.success('Curso atualizado com sucesso!');
606
+ setSaving(false);
607
+ setSheetOpen(false);
608
+ } else {
609
+ const newId = Date.now();
610
+ const newCurso: Curso = {
611
+ id: newId,
612
+ ...data,
613
+ alunosInscritos: 0,
614
+ criadoEm: new Date().toISOString().split('T')[0],
615
+ };
616
+ setCursos((prev) => [newCurso, ...prev]);
617
+ toast.success('Curso criado com sucesso! Redirecionando...');
618
+ setSaving(false);
619
+ setSheetOpen(false);
620
+ // Redirecionar para pagina dedicada do curso
621
+ setTimeout(() => {
622
+ router.push(`/cursos/${newId}`);
623
+ }, 400);
624
+ }
625
+ }
626
+
627
+ function confirmDelete() {
628
+ if (cursoToDelete) {
629
+ setCursos((prev) => prev.filter((c) => c.id !== cursoToDelete.id));
630
+ setSelectedIds((prev) => {
631
+ const next = new Set(prev);
632
+ next.delete(cursoToDelete.id);
633
+ return next;
634
+ });
635
+ toast.success(`Curso "${cursoToDelete.tituloComercial}" removido.`);
636
+ setCursoToDelete(null);
637
+ setDeleteDialogOpen(false);
638
+ }
639
+ }
640
+
641
+ function bulkDelete() {
642
+ if (selectedIds.size === 0) return;
643
+ setCursos((prev) => prev.filter((c) => !selectedIds.has(c.id)));
644
+ toast.success(`${selectedIds.size} curso(s) removido(s).`);
645
+ setSelectedIds(new Set());
646
+ }
647
+
648
+ function bulkArchive() {
649
+ if (selectedIds.size === 0) return;
650
+ setCursos((prev) =>
651
+ prev.map((c) =>
652
+ selectedIds.has(c.id) ? { ...c, status: 'arquivado' as const } : c
653
+ )
654
+ );
655
+ toast.success(`${selectedIds.size} curso(s) arquivado(s).`);
656
+ setSelectedIds(new Set());
657
+ }
658
+
659
+ // ── Count badges ───────────────────────────────────────────────────────────
660
+
661
+ const countAtivos = cursos.filter((c) => c.status === 'ativo').length;
662
+ const countRascunhos = cursos.filter((c) => c.status === 'rascunho').length;
663
+ const countArquivados = cursos.filter((c) => c.status === 'arquivado').length;
664
+
665
+ // ── Render ─────────────────────────────────────────────────────────────────
666
+
667
+ return (
668
+ <Page>
669
+ <PageHeader
670
+ title="Cursos"
671
+ description="Gerencie seus cursos, categorias e niveis de forma facil e rapida."
672
+ breadcrumbs={[
673
+ {
674
+ label: 'Home',
675
+ href: '/',
676
+ },
677
+ {
678
+ label: 'Cursos',
679
+ },
680
+ ]}
681
+ actions={
682
+ <Button onClick={openCreateSheet} className="gap-2">
683
+ <Plus className="size-4" />
684
+ Criar Curso
685
+ </Button>
686
+ }
687
+ />
688
+
689
+ <motion.div initial="hidden" animate="show" variants={stagger}>
690
+ {/* Filters */}
691
+ <motion.div variants={fadeUp}>
692
+ <Card className="mb-6">
693
+ <CardContent className="p-4">
694
+ <div className="flex flex-col gap-3 lg:flex-row lg:items-center">
695
+ <div className="relative flex-1">
696
+ <Search className="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
697
+ <Input
698
+ placeholder="Buscar por codigo, titulo ou nome interno..."
699
+ value={busca}
700
+ onChange={(e) => setBusca(e.target.value)}
701
+ className="pl-9"
702
+ />
703
+ </div>
704
+ <div className="flex flex-wrap items-center gap-2">
705
+ <div className="flex items-center gap-1.5 text-xs text-muted-foreground">
706
+ <Filter className="size-3.5" />
707
+ Filtros:
708
+ </div>
709
+ <Select value={filtroStatus} onValueChange={setFiltroStatus}>
710
+ <SelectTrigger className="h-9 w-[130px] text-xs">
711
+ <SelectValue placeholder="Status" />
712
+ </SelectTrigger>
713
+ <SelectContent>
714
+ <SelectItem value="todos">Todos Status</SelectItem>
715
+ <SelectItem value="ativo">Ativo</SelectItem>
716
+ <SelectItem value="rascunho">Rascunho</SelectItem>
717
+ <SelectItem value="arquivado">Arquivado</SelectItem>
718
+ </SelectContent>
719
+ </Select>
720
+ <Select value={filtroNivel} onValueChange={setFiltroNivel}>
721
+ <SelectTrigger className="h-9 w-[140px] text-xs">
722
+ <SelectValue placeholder="Nivel" />
723
+ </SelectTrigger>
724
+ <SelectContent>
725
+ <SelectItem value="todos">Todos Niveis</SelectItem>
726
+ <SelectItem value="iniciante">Iniciante</SelectItem>
727
+ <SelectItem value="intermediario">
728
+ Intermediario
729
+ </SelectItem>
730
+ <SelectItem value="avancado">Avancado</SelectItem>
731
+ </SelectContent>
732
+ </Select>
733
+ <Select
734
+ value={filtroCategoria}
735
+ onValueChange={setFiltroCategoria}
736
+ >
737
+ <SelectTrigger className="h-9 w-[140px] text-xs">
738
+ <SelectValue placeholder="Categoria" />
739
+ </SelectTrigger>
740
+ <SelectContent>
741
+ <SelectItem value="todos">Todas Categorias</SelectItem>
742
+ {CATEGORIAS.map((cat) => (
743
+ <SelectItem key={cat} value={cat}>
744
+ {cat}
745
+ </SelectItem>
746
+ ))}
747
+ </SelectContent>
748
+ </Select>
749
+ {(filtroStatus !== 'todos' ||
750
+ filtroNivel !== 'todos' ||
751
+ filtroCategoria !== 'todos' ||
752
+ busca) && (
753
+ <Button
754
+ variant="ghost"
755
+ size="sm"
756
+ className="h-9 text-xs text-muted-foreground"
757
+ onClick={() => {
758
+ setFiltroStatus('todos');
759
+ setFiltroNivel('todos');
760
+ setFiltroCategoria('todos');
761
+ setBusca('');
762
+ }}
763
+ >
764
+ <X className="mr-1 size-3" />
765
+ Limpar
766
+ </Button>
767
+ )}
768
+ </div>
769
+ </div>
770
+ </CardContent>
771
+ </Card>
772
+ </motion.div>
773
+
774
+ {/* Bulk actions bar */}
775
+ <AnimatePresence>
776
+ {selectedIds.size > 0 && (
777
+ <motion.div
778
+ initial={{ opacity: 0, y: -8, height: 0 }}
779
+ animate={{ opacity: 1, y: 0, height: 'auto' }}
780
+ exit={{ opacity: 0, y: -8, height: 0 }}
781
+ transition={{ duration: 0.2 }}
782
+ className="mb-4 overflow-hidden"
783
+ >
784
+ <div className="flex items-center gap-3 rounded-lg border bg-muted/50 px-4 py-2.5">
785
+ <CheckSquare className="size-4 text-muted-foreground" />
786
+ <span className="text-sm font-medium">
787
+ {selectedIds.size} selecionado(s)
788
+ </span>
789
+ <Separator orientation="vertical" className="h-5" />
790
+ <Button
791
+ variant="outline"
792
+ size="sm"
793
+ className="h-7 gap-1.5 text-xs"
794
+ onClick={bulkArchive}
795
+ >
796
+ <Archive className="size-3" />
797
+ Arquivar
798
+ </Button>
799
+ <Button
800
+ variant="outline"
801
+ size="sm"
802
+ className="h-7 gap-1.5 text-xs text-destructive hover:text-destructive"
803
+ onClick={bulkDelete}
804
+ >
805
+ <Trash2 className="size-3" />
806
+ Excluir
807
+ </Button>
808
+ <Button
809
+ variant="ghost"
810
+ size="sm"
811
+ className="ml-auto h-7 text-xs"
812
+ onClick={() => setSelectedIds(new Set())}
813
+ >
814
+ Limpar selecao
815
+ </Button>
816
+ </div>
817
+ </motion.div>
818
+ )}
819
+ </AnimatePresence>
820
+
821
+ {/* Table */}
822
+ <motion.div variants={fadeUp}>
823
+ {loading ? (
824
+ <Card>
825
+ <CardContent className="p-0">
826
+ <div className="p-4">
827
+ {Array.from({ length: 8 }).map((_, i) => (
828
+ <div
829
+ key={i}
830
+ className="flex items-center gap-4 border-b py-4 last:border-0"
831
+ >
832
+ <Skeleton className="size-4 rounded" />
833
+ <Skeleton className="h-4 w-20" />
834
+ <Skeleton className="h-4 w-40" />
835
+ <Skeleton className="h-5 w-20 rounded-full" />
836
+ <Skeleton className="h-5 w-16 rounded-full" />
837
+ <Skeleton className="h-5 w-20 rounded-full" />
838
+ <Skeleton className="ml-auto size-8 rounded-md" />
839
+ </div>
840
+ ))}
841
+ </div>
842
+ </CardContent>
843
+ </Card>
844
+ ) : (
845
+ <Card>
846
+ <CardContent className="p-0">
847
+ <div className="overflow-x-auto">
848
+ <Table>
849
+ <TableHeader>
850
+ <TableRow>
851
+ <TableHead className="w-[40px]">
852
+ <Checkbox
853
+ checked={allPageSelected}
854
+ onCheckedChange={toggleSelectAll}
855
+ aria-label="Selecionar todos"
856
+ />
857
+ </TableHead>
858
+ <TableHead className="w-[100px]">Codigo</TableHead>
859
+ <TableHead>Titulo</TableHead>
860
+ <TableHead className="hidden md:table-cell">
861
+ Nivel
862
+ </TableHead>
863
+ <TableHead>Status</TableHead>
864
+ <TableHead className="hidden lg:table-cell">
865
+ Categorias
866
+ </TableHead>
867
+ <TableHead className="hidden sm:table-cell text-right">
868
+ Alunos
869
+ </TableHead>
870
+ <TableHead className="w-[50px]" />
871
+ </TableRow>
872
+ </TableHeader>
873
+ <TableBody>
874
+ <AnimatePresence mode="popLayout">
875
+ {paginatedCursos.map((curso) => (
876
+ <motion.tr
877
+ key={curso.id}
878
+ layout
879
+ initial={{ opacity: 0 }}
880
+ animate={{ opacity: 1 }}
881
+ exit={{ opacity: 0, x: -20 }}
882
+ transition={{ duration: 0.2 }}
883
+ className="group border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted"
884
+ data-state={
885
+ selectedIds.has(curso.id) ? 'selected' : undefined
886
+ }
887
+ >
888
+ <TableCell>
889
+ <Checkbox
890
+ checked={selectedIds.has(curso.id)}
891
+ onCheckedChange={() => toggleSelect(curso.id)}
892
+ aria-label={`Selecionar ${curso.tituloComercial}`}
893
+ />
894
+ </TableCell>
895
+ <TableCell>
896
+ <code className="rounded bg-muted px-1.5 py-0.5 text-xs font-medium">
897
+ {curso.codigo}
898
+ </code>
899
+ </TableCell>
900
+ <TableCell>
901
+ <div className="flex flex-col">
902
+ <span className="font-medium">
903
+ {curso.tituloComercial}
904
+ </span>
905
+ <span className="text-xs text-muted-foreground">
906
+ {curso.nomeInterno}
907
+ </span>
908
+ </div>
909
+ </TableCell>
910
+ <TableCell className="hidden md:table-cell">
911
+ <Badge
912
+ variant="outline"
913
+ className="text-xs font-normal"
914
+ >
915
+ {NIVEL_MAP[curso.nivel]}
916
+ </Badge>
917
+ </TableCell>
918
+ <TableCell>
919
+ <Badge variant={STATUS_MAP[curso.status].variant}>
920
+ {STATUS_MAP[curso.status].label}
921
+ </Badge>
922
+ </TableCell>
923
+ <TableCell className="hidden lg:table-cell">
924
+ <div className="flex flex-wrap gap-1">
925
+ {curso.categorias.map((cat) => (
926
+ <Badge
927
+ key={cat}
928
+ variant="secondary"
929
+ className="text-[10px] font-normal"
930
+ >
931
+ {cat}
932
+ </Badge>
933
+ ))}
934
+ </div>
935
+ </TableCell>
936
+ <TableCell className="hidden sm:table-cell text-right tabular-nums text-muted-foreground">
937
+ {curso.alunosInscritos.toLocaleString('pt-BR')}
938
+ </TableCell>
939
+ <TableCell>
940
+ <DropdownMenu>
941
+ <DropdownMenuTrigger asChild>
942
+ <Button
943
+ variant="ghost"
944
+ size="icon"
945
+ className="size-8 opacity-0 transition-opacity group-hover:opacity-100 focus:opacity-100"
946
+ >
947
+ <MoreHorizontal className="size-4" />
948
+ <span className="sr-only">Acoes</span>
949
+ </Button>
950
+ </DropdownMenuTrigger>
951
+ <DropdownMenuContent
952
+ align="end"
953
+ className="w-44"
954
+ >
955
+ <DropdownMenuItem
956
+ onClick={() =>
957
+ router.push(`/cursos/${curso.id}`)
958
+ }
959
+ className="gap-2"
960
+ >
961
+ <Eye className="size-3.5" />
962
+ Ver detalhes
963
+ </DropdownMenuItem>
964
+ <DropdownMenuItem
965
+ onClick={() => openEditSheet(curso)}
966
+ className="gap-2"
967
+ >
968
+ <Pencil className="size-3.5" />
969
+ Editar
970
+ </DropdownMenuItem>
971
+ <DropdownMenuItem
972
+ onClick={() => {
973
+ navigator.clipboard.writeText(
974
+ curso.codigo
975
+ );
976
+ toast.info(
977
+ `Codigo "${curso.codigo}" copiado.`
978
+ );
979
+ }}
980
+ className="gap-2"
981
+ >
982
+ <Copy className="size-3.5" />
983
+ Copiar codigo
984
+ </DropdownMenuItem>
985
+ <DropdownMenuSeparator />
986
+ <DropdownMenuItem
987
+ onClick={() => {
988
+ setCursoToDelete(curso);
989
+ setDeleteDialogOpen(true);
990
+ }}
991
+ className="gap-2 text-destructive focus:text-destructive"
992
+ >
993
+ <Trash2 className="size-3.5" />
994
+ Excluir
995
+ </DropdownMenuItem>
996
+ </DropdownMenuContent>
997
+ </DropdownMenu>
998
+ </TableCell>
999
+ </motion.tr>
1000
+ ))}
1001
+ </AnimatePresence>
1002
+ {paginatedCursos.length === 0 && (
1003
+ <TableRow>
1004
+ <TableCell colSpan={8} className="py-16 text-center">
1005
+ <div className="flex flex-col items-center gap-2">
1006
+ <BookOpen className="size-10 text-muted-foreground/40" />
1007
+ <p className="text-sm font-medium text-muted-foreground">
1008
+ Nenhum curso encontrado
1009
+ </p>
1010
+ <p className="text-xs text-muted-foreground/70">
1011
+ Tente ajustar os filtros ou criar um novo curso.
1012
+ </p>
1013
+ </div>
1014
+ </TableCell>
1015
+ </TableRow>
1016
+ )}
1017
+ </TableBody>
1018
+ </Table>
1019
+ </div>
1020
+
1021
+ {/* Pagination */}
1022
+ {filteredCursos.length > 0 && (
1023
+ <div className="flex flex-col items-center justify-between gap-3 border-t px-4 py-3 sm:flex-row">
1024
+ <p className="text-xs text-muted-foreground">
1025
+ Mostrando{' '}
1026
+ <span className="font-medium text-foreground">
1027
+ {(safePage - 1) * ITEMS_PER_PAGE + 1}
1028
+ </span>{' '}
1029
+ a{' '}
1030
+ <span className="font-medium text-foreground">
1031
+ {Math.min(
1032
+ safePage * ITEMS_PER_PAGE,
1033
+ filteredCursos.length
1034
+ )}
1035
+ </span>{' '}
1036
+ de{' '}
1037
+ <span className="font-medium text-foreground">
1038
+ {filteredCursos.length}
1039
+ </span>{' '}
1040
+ resultados
1041
+ </p>
1042
+ <div className="flex items-center gap-1">
1043
+ <Button
1044
+ variant="outline"
1045
+ size="icon"
1046
+ className="size-8"
1047
+ disabled={safePage === 1}
1048
+ onClick={() => setCurrentPage((p) => p - 1)}
1049
+ aria-label="Pagina anterior"
1050
+ >
1051
+ <ChevronLeft className="size-4" />
1052
+ </Button>
1053
+ {Array.from({ length: totalPages }).map((_, i) => (
1054
+ <Button
1055
+ key={i}
1056
+ variant={safePage === i + 1 ? 'default' : 'outline'}
1057
+ size="icon"
1058
+ className="size-8 text-xs"
1059
+ onClick={() => setCurrentPage(i + 1)}
1060
+ >
1061
+ {i + 1}
1062
+ </Button>
1063
+ ))}
1064
+ <Button
1065
+ variant="outline"
1066
+ size="icon"
1067
+ className="size-8"
1068
+ disabled={safePage === totalPages}
1069
+ onClick={() => setCurrentPage((p) => p + 1)}
1070
+ aria-label="Proxima pagina"
1071
+ >
1072
+ <ChevronRight className="size-4" />
1073
+ </Button>
1074
+ </div>
1075
+ </div>
1076
+ )}
1077
+ </CardContent>
1078
+ </Card>
1079
+ )}
1080
+ </motion.div>
1081
+ </motion.div>
1082
+
1083
+ {/* ── Sheet: Criar / Editar ──────────────────────────────────────────── */}
1084
+ <Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
1085
+ <SheetContent className="w-full overflow-y-auto sm:max-w-lg">
1086
+ <SheetHeader>
1087
+ <SheetTitle>
1088
+ {editingCurso ? 'Editar Curso' : 'Criar Novo Curso'}
1089
+ </SheetTitle>
1090
+ <SheetDescription>
1091
+ {editingCurso
1092
+ ? 'Atualize as informacoes do curso abaixo.'
1093
+ : 'Preencha os dados para criar um novo curso. Apos criar, voce sera redirecionado para a pagina do curso.'}
1094
+ </SheetDescription>
1095
+ </SheetHeader>
1096
+
1097
+ <form
1098
+ onSubmit={form.handleSubmit(onSubmit)}
1099
+ className="mt-6 flex flex-col gap-5 px-1"
1100
+ >
1101
+ {/* Codigo */}
1102
+ <Field data-invalid={!!form.formState.errors.codigo}>
1103
+ <FieldLabel htmlFor="codigo">
1104
+ <Hash className="size-3.5 text-muted-foreground" />
1105
+ Codigo do Curso
1106
+ </FieldLabel>
1107
+ <Input
1108
+ id="codigo"
1109
+ placeholder="Ex: REACT-ADV"
1110
+ className="uppercase"
1111
+ {...form.register('codigo')}
1112
+ />
1113
+ {form.formState.errors.codigo && (
1114
+ <FieldError>{form.formState.errors.codigo.message}</FieldError>
1115
+ )}
1116
+ <FieldDescription>
1117
+ Identificador unico do curso (ex: REACT-ADV)
1118
+ </FieldDescription>
1119
+ </Field>
1120
+
1121
+ {/* Nome Interno */}
1122
+ <Field data-invalid={!!form.formState.errors.nomeInterno}>
1123
+ <FieldLabel htmlFor="nomeInterno">Nome Interno</FieldLabel>
1124
+ <Input
1125
+ id="nomeInterno"
1126
+ placeholder="Ex: react-avancado"
1127
+ {...form.register('nomeInterno')}
1128
+ />
1129
+ {form.formState.errors.nomeInterno && (
1130
+ <FieldError>
1131
+ {form.formState.errors.nomeInterno.message}
1132
+ </FieldError>
1133
+ )}
1134
+ <FieldDescription>
1135
+ Slug interno para uso do sistema
1136
+ </FieldDescription>
1137
+ </Field>
1138
+
1139
+ {/* Titulo Comercial */}
1140
+ <Field data-invalid={!!form.formState.errors.tituloComercial}>
1141
+ <FieldLabel htmlFor="tituloComercial">
1142
+ Titulo Comercial
1143
+ </FieldLabel>
1144
+ <Input
1145
+ id="tituloComercial"
1146
+ placeholder="Ex: React Avancado"
1147
+ {...form.register('tituloComercial')}
1148
+ />
1149
+ {form.formState.errors.tituloComercial && (
1150
+ <FieldError>
1151
+ {form.formState.errors.tituloComercial.message}
1152
+ </FieldError>
1153
+ )}
1154
+ </Field>
1155
+
1156
+ {/* Descricao */}
1157
+ <Field data-invalid={!!form.formState.errors.descricao}>
1158
+ <FieldLabel htmlFor="descricao">Descricao</FieldLabel>
1159
+ <Textarea
1160
+ id="descricao"
1161
+ placeholder="Descreva o conteudo e objetivos do curso..."
1162
+ rows={4}
1163
+ {...form.register('descricao')}
1164
+ />
1165
+ {form.formState.errors.descricao && (
1166
+ <FieldError>
1167
+ {form.formState.errors.descricao.message}
1168
+ </FieldError>
1169
+ )}
1170
+ </Field>
1171
+
1172
+ {/* Nivel + Status side by side */}
1173
+ <div className="grid grid-cols-2 gap-4">
1174
+ <Field data-invalid={!!form.formState.errors.nivel}>
1175
+ <FieldLabel>Nivel</FieldLabel>
1176
+ <Controller
1177
+ control={form.control}
1178
+ name="nivel"
1179
+ render={({ field }) => (
1180
+ <Select value={field.value} onValueChange={field.onChange}>
1181
+ <SelectTrigger>
1182
+ <SelectValue placeholder="Selecione" />
1183
+ </SelectTrigger>
1184
+ <SelectContent>
1185
+ {NIVEIS.map((n) => (
1186
+ <SelectItem key={n.value} value={n.value}>
1187
+ {n.label}
1188
+ </SelectItem>
1189
+ ))}
1190
+ </SelectContent>
1191
+ </Select>
1192
+ )}
1193
+ />
1194
+ {form.formState.errors.nivel && (
1195
+ <FieldError>{form.formState.errors.nivel.message}</FieldError>
1196
+ )}
1197
+ </Field>
1198
+
1199
+ <Field data-invalid={!!form.formState.errors.status}>
1200
+ <FieldLabel>Status</FieldLabel>
1201
+ <Controller
1202
+ control={form.control}
1203
+ name="status"
1204
+ render={({ field }) => (
1205
+ <Select value={field.value} onValueChange={field.onChange}>
1206
+ <SelectTrigger>
1207
+ <SelectValue placeholder="Selecione" />
1208
+ </SelectTrigger>
1209
+ <SelectContent>
1210
+ {STATUS_OPTIONS.map((s) => (
1211
+ <SelectItem key={s.value} value={s.value}>
1212
+ {s.label}
1213
+ </SelectItem>
1214
+ ))}
1215
+ </SelectContent>
1216
+ </Select>
1217
+ )}
1218
+ />
1219
+ {form.formState.errors.status && (
1220
+ <FieldError>
1221
+ {form.formState.errors.status.message}
1222
+ </FieldError>
1223
+ )}
1224
+ </Field>
1225
+ </div>
1226
+
1227
+ <Separator />
1228
+
1229
+ {/* Categorias - Multi select via checkboxes */}
1230
+ <Field data-invalid={!!form.formState.errors.categorias}>
1231
+ <FieldLabel>Categorias</FieldLabel>
1232
+ <FieldDescription>
1233
+ Selecione uma ou mais categorias
1234
+ </FieldDescription>
1235
+ <Controller
1236
+ control={form.control}
1237
+ name="categorias"
1238
+ render={({ field }) => (
1239
+ <div className="grid grid-cols-2 gap-2.5">
1240
+ {CATEGORIAS.map((cat) => {
1241
+ const checked = field.value.includes(cat);
1242
+ return (
1243
+ <label
1244
+ key={cat}
1245
+ className="flex cursor-pointer items-center gap-2.5 rounded-md border px-3 py-2 text-sm transition-colors hover:bg-muted/50 has-[:checked]:border-foreground has-[:checked]:bg-muted/50"
1246
+ >
1247
+ <Checkbox
1248
+ checked={checked}
1249
+ onCheckedChange={(isChecked) => {
1250
+ if (isChecked) {
1251
+ field.onChange([...field.value, cat]);
1252
+ } else {
1253
+ field.onChange(
1254
+ field.value.filter((v: string) => v !== cat)
1255
+ );
1256
+ }
1257
+ }}
1258
+ />
1259
+ {cat}
1260
+ </label>
1261
+ );
1262
+ })}
1263
+ </div>
1264
+ )}
1265
+ />
1266
+ {form.formState.errors.categorias && (
1267
+ <FieldError>
1268
+ {form.formState.errors.categorias.message}
1269
+ </FieldError>
1270
+ )}
1271
+ </Field>
1272
+
1273
+ <Separator />
1274
+
1275
+ {/* Flags */}
1276
+ <div className="flex flex-col gap-4">
1277
+ <p className="text-sm font-medium">Flags Principais</p>
1278
+ <Controller
1279
+ control={form.control}
1280
+ name="destaque"
1281
+ render={({ field }) => (
1282
+ <div className="flex items-center justify-between rounded-md border px-4 py-3">
1283
+ <div className="flex flex-col gap-0.5">
1284
+ <span className="text-sm font-medium">Destaque</span>
1285
+ <span className="text-xs text-muted-foreground">
1286
+ Exibir como curso em destaque na vitrine
1287
+ </span>
1288
+ </div>
1289
+ <Switch
1290
+ checked={field.value}
1291
+ onCheckedChange={field.onChange}
1292
+ />
1293
+ </div>
1294
+ )}
1295
+ />
1296
+ <Controller
1297
+ control={form.control}
1298
+ name="certificado"
1299
+ render={({ field }) => (
1300
+ <div className="flex items-center justify-between rounded-md border px-4 py-3">
1301
+ <div className="flex flex-col gap-0.5">
1302
+ <span className="text-sm font-medium">Certificado</span>
1303
+ <span className="text-xs text-muted-foreground">
1304
+ Emitir certificado ao concluir o curso
1305
+ </span>
1306
+ </div>
1307
+ <Switch
1308
+ checked={field.value}
1309
+ onCheckedChange={field.onChange}
1310
+ />
1311
+ </div>
1312
+ )}
1313
+ />
1314
+ <Controller
1315
+ control={form.control}
1316
+ name="listado"
1317
+ render={({ field }) => (
1318
+ <div className="flex items-center justify-between rounded-md border px-4 py-3">
1319
+ <div className="flex flex-col gap-0.5">
1320
+ <span className="text-sm font-medium">Listado</span>
1321
+ <span className="text-xs text-muted-foreground">
1322
+ Visivel no catalogo publico de cursos
1323
+ </span>
1324
+ </div>
1325
+ <Switch
1326
+ checked={field.value}
1327
+ onCheckedChange={field.onChange}
1328
+ />
1329
+ </div>
1330
+ )}
1331
+ />
1332
+ </div>
1333
+
1334
+ <SheetFooter className="mt-2 gap-2 pb-6">
1335
+ <Button
1336
+ type="button"
1337
+ variant="outline"
1338
+ onClick={() => setSheetOpen(false)}
1339
+ >
1340
+ Cancelar
1341
+ </Button>
1342
+ <Button type="submit" disabled={saving} className="gap-2">
1343
+ {saving && <Loader2 className="size-4 animate-spin" />}
1344
+ {editingCurso ? 'Salvar Alteracoes' : 'Criar Curso'}
1345
+ </Button>
1346
+ </SheetFooter>
1347
+ </form>
1348
+ </SheetContent>
1349
+ </Sheet>
1350
+
1351
+ {/* ── Dialog: Confirmar Exclusao ─────────────────────────────────────── */}
1352
+ <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
1353
+ <DialogContent>
1354
+ <DialogHeader>
1355
+ <DialogTitle className="flex items-center gap-2">
1356
+ <AlertTriangle className="size-5 text-destructive" />
1357
+ Confirmar Exclusao
1358
+ </DialogTitle>
1359
+ <DialogDescription>
1360
+ Tem certeza que deseja excluir o curso{' '}
1361
+ <strong className="text-foreground">
1362
+ {cursoToDelete?.tituloComercial}
1363
+ </strong>
1364
+ ?
1365
+ {cursoToDelete && cursoToDelete.alunosInscritos > 0 && (
1366
+ <span className="mt-2 flex items-center gap-1.5 rounded-md bg-amber-50 px-3 py-2 text-xs font-medium text-amber-700">
1367
+ <AlertTriangle className="size-3.5" />
1368
+ Este curso possui {cursoToDelete.alunosInscritos} aluno(s)
1369
+ matriculado(s). A exclusao e irreversivel.
1370
+ </span>
1371
+ )}
1372
+ </DialogDescription>
1373
+ </DialogHeader>
1374
+ <DialogFooter className="gap-2">
1375
+ <Button
1376
+ variant="outline"
1377
+ onClick={() => setDeleteDialogOpen(false)}
1378
+ >
1379
+ Cancelar
1380
+ </Button>
1381
+ <Button
1382
+ variant="destructive"
1383
+ onClick={confirmDelete}
1384
+ className="gap-2"
1385
+ >
1386
+ <Trash2 className="size-4" />
1387
+ Excluir Curso
1388
+ </Button>
1389
+ </DialogFooter>
1390
+ </DialogContent>
1391
+ </Dialog>
1392
+ </Page>
1393
+ );
1394
+ }