diff --git a/backend/src/modulos/documentos/rotas.ts b/backend/src/modulos/documentos/rotas.ts index e0679e9..22e7ac8 100644 --- a/backend/src/modulos/documentos/rotas.ts +++ b/backend/src/modulos/documentos/rotas.ts @@ -97,6 +97,21 @@ router.post('/', autenticar, exigirPerfil('estudante'), upload.single('arquivo') return; } + const TIPOS_VALIDOS = [ + 'certificado_curso', + 'certificado_evento', + 'declaracao_participacao', + 'comprovante_atividade', + 'artigo_publicado', + 'outro', + ]; + if (!TIPOS_VALIDOS.includes(tipo)) { + res.status(400).json({ + erro: `Tipo inválido. Valores aceitos: ${TIPOS_VALIDOS.join(', ')}`, + }); + return; + } + try { const usuario = await buscarUsuario(req.usuario!.sub); if (!usuario?.curso_id) { diff --git a/backend/src/servicos/gerador-certificado.ts b/backend/src/servicos/gerador-certificado.ts index 7761bb9..eec8528 100644 --- a/backend/src/servicos/gerador-certificado.ts +++ b/backend/src/servicos/gerador-certificado.ts @@ -23,10 +23,12 @@ export function gerarHash(documentoId: string): string { } const TIPO_LEGIVEL: Record = { - estagio: 'Estágio', - tcc: 'Trabalho de Conclusão de Curso', - extensao: 'Atividade de Extensão', - monitoria: 'Monitoria', + certificado_curso: 'Certificado de Curso', + certificado_evento: 'Certificado de Evento', + declaracao_participacao: 'Declaração de Participação', + comprovante_atividade: 'Comprovante de Atividade', + artigo_publicado: 'Artigo Publicado', + outro: 'Outro', }; export async function gerarPDF(dados: DadosCertificado, urlVerificacao: string): Promise { diff --git a/backend/testes/helpers/fixtures.ts b/backend/testes/helpers/fixtures.ts index 1a9bb49..4ae8a0c 100644 --- a/backend/testes/helpers/fixtures.ts +++ b/backend/testes/helpers/fixtures.ts @@ -2,15 +2,15 @@ import type { Documento } from '../../src/modulos/documentos/repositorio'; export const DOC_MOCK: Documento = { id: 'doc-1', - titulo: 'Estágio XYZ', - tipo: 'estagio', + titulo: 'Certificado XYZ', + tipo: 'certificado_curso', carga_horaria: 40, estudante_id: 'estudante-id', curso_id: 'curso-1', status: 'pendente', coordenador_id: null, - nome_arquivo: 'estagio.pdf', - caminho_arquivo: 'documentos/estudante-id/estagio.pdf', + nome_arquivo: 'certificado.pdf', + caminho_arquivo: 'documentos/estudante-id/certificado.pdf', tamanho_arquivo: 1024, mime_type: 'application/pdf', criado_em: new Date(), diff --git a/backend/testes/rotas/documentos.test.ts b/backend/testes/rotas/documentos.test.ts index 840a96e..c68e0bc 100644 --- a/backend/testes/rotas/documentos.test.ts +++ b/backend/testes/rotas/documentos.test.ts @@ -55,7 +55,7 @@ describe('GET /:id', () => { const res = await request(app).get('/doc-1'); expect(res.status).toBe(200); expect(res.body.id).toBe('doc-1'); - expect(res.body.titulo).toBe('Estágio XYZ'); + expect(res.body.titulo).toBe('Certificado XYZ'); }); it('retorna 404 quando o documento não existe', async () => { @@ -106,8 +106,8 @@ describe('POST /', () => { it('retorna 400 quando nenhum arquivo é enviado', async () => { const res = await request(app) .post('/') - .field('titulo', 'Estágio') - .field('tipo', 'estagio') + .field('titulo', 'Certificado') + .field('tipo', 'certificado_curso') .field('carga_horaria', '40'); expect(res.status).toBe(400); expect(res.body.erro).toContain('arquivo'); @@ -132,8 +132,8 @@ describe('POST /', () => { filename: 'test.pdf', contentType: 'application/pdf', }) - .field('titulo', 'Estágio') - .field('tipo', 'estagio') + .field('titulo', 'Certificado') + .field('tipo', 'certificado_curso') .field('carga_horaria', 'abc'); expect(res.status).toBe(400); expect(res.body.erro).toContain('carga_horaria'); @@ -147,8 +147,8 @@ describe('POST /', () => { filename: 'test.pdf', contentType: 'application/pdf', }) - .field('titulo', 'Estágio XYZ') - .field('tipo', 'estagio') + .field('titulo', 'Certificado XYZ') + .field('tipo', 'certificado_curso') .field('carga_horaria', '40'); expect(res.status).toBe(422); expect(res.body.erro).toContain('curso'); @@ -156,29 +156,29 @@ describe('POST /', () => { it('retorna 201 com o documento criado após upload bem-sucedido', async () => { mockUsr.buscarPorId.mockResolvedValueOnce({ id: 'estudante-id', curso_id: 'curso-1' } as any); - mockArm.fazerUpload.mockResolvedValueOnce('documentos/estudante-id/estagio.pdf'); + mockArm.fazerUpload.mockResolvedValueOnce('documentos/estudante-id/certificado.pdf'); mockDoc.criar.mockResolvedValueOnce(DOC_MOCK); const res = await request(app) .post('/') .attach('arquivo', Buffer.from('%PDF-1.4'), { - filename: 'estagio.pdf', + filename: 'certificado.pdf', contentType: 'application/pdf', }) - .field('titulo', 'Estágio XYZ') - .field('tipo', 'estagio') + .field('titulo', 'Certificado XYZ') + .field('tipo', 'certificado_curso') .field('carga_horaria', '40'); expect(res.status).toBe(201); expect(res.body.id).toBe('doc-1'); - expect(res.body.titulo).toBe('Estágio XYZ'); + expect(res.body.titulo).toBe('Certificado XYZ'); expect(mockEmitir).toHaveBeenCalledWith( 'documento_submetido', expect.objectContaining({ documentoId: 'doc-1', estudanteId: 'estudante-id', cursoId: 'curso-1', - titulo: 'Estágio XYZ', - tipo: 'estagio', + titulo: 'Certificado XYZ', + tipo: 'certificado_curso', }), ); }); @@ -189,9 +189,9 @@ describe('POST /', () => { await request(app) .post('/') - .attach('arquivo', Buffer.from('%PDF-1.4'), { filename: 'estagio.pdf', contentType: 'application/pdf' }) - .field('titulo', 'Estágio XYZ') - .field('tipo', 'estagio') + .attach('arquivo', Buffer.from('%PDF-1.4'), { filename: 'certificado.pdf', contentType: 'application/pdf' }) + .field('titulo', 'Certificado XYZ') + .field('tipo', 'certificado_curso') .field('carga_horaria', '40'); expect(mockEmitir).not.toHaveBeenCalled(); diff --git a/backend/testes/rotas/publica.test.ts b/backend/testes/rotas/publica.test.ts index 7daf00f..9337b2d 100644 --- a/backend/testes/rotas/publica.test.ts +++ b/backend/testes/rotas/publica.test.ts @@ -57,7 +57,7 @@ describe('GET /verificar/:hash', () => { expect(res.status).toBe(200); expect(res.body.valido).toBe(true); expect(res.body.certificado.id).toBe('cert-1'); - expect(res.body.documento.titulo).toBe('Estágio XYZ'); + expect(res.body.documento.titulo).toBe('Certificado XYZ'); expect(res.body.estudante.nome).toBe('João Silva'); }); diff --git a/backend/testes/unitarios/eventos/barramento.test.ts b/backend/testes/unitarios/eventos/barramento.test.ts index d7b0aab..12cfbed 100644 --- a/backend/testes/unitarios/eventos/barramento.test.ts +++ b/backend/testes/unitarios/eventos/barramento.test.ts @@ -12,15 +12,15 @@ const submetido: EventoDocumentoSubmetido = { documentoId: 'doc-1', estudanteId: 'est-1', cursoId: 'curso-1', - titulo: 'Estágio XYZ', - tipo: 'estagio', + titulo: 'Certificado XYZ', + tipo: 'certificado_curso', }; const aprovado: EventoDocumentoAprovado = { documentoId: 'doc-1', estudanteId: 'est-1', coordenadorId: 'coord-1', - titulo: 'Estágio XYZ', + titulo: 'Certificado XYZ', }; const reprovado: EventoDocumentoReprovado = { diff --git a/backend/testes/unitarios/eventos/handlers.test.ts b/backend/testes/unitarios/eventos/handlers.test.ts index dc2e2d0..8a370e8 100644 --- a/backend/testes/unitarios/eventos/handlers.test.ts +++ b/backend/testes/unitarios/eventos/handlers.test.ts @@ -43,15 +43,15 @@ const ESTUDANTE_MOCK = { const DOCUMENTO_MOCK = { id: 'doc-1', - titulo: 'Estágio XYZ', - tipo: 'estagio', + titulo: 'Certificado XYZ', + tipo: 'certificado_curso', carga_horaria: 40, estudante_id: 'est-1', curso_id: 'curso-1', status: 'aprovado', coordenador_id: 'coord-1', - nome_arquivo: 'estagio.pdf', - caminho_arquivo: 'documentos/est-1/estagio.pdf', + nome_arquivo: 'certificado.pdf', + caminho_arquivo: 'documentos/est-1/certificado.pdf', tamanho_arquivo: 1024, mime_type: 'application/pdf', criado_em: new Date(), @@ -85,8 +85,8 @@ describe('aoDocumentoSubmetido', () => { documentoId: 'doc-1', estudanteId: 'est-1', cursoId: 'curso-1', - titulo: 'Estágio XYZ', - tipo: 'estagio', + titulo: 'Certificado XYZ', + tipo: 'certificado_curso', }; it('notifica cada coordenador encontrado para o curso', async () => { @@ -105,7 +105,7 @@ describe('aoDocumentoSubmetido', () => { destinatarioId: 'coord-1', destinatarioEmail: 'maria@uni.edu', assunto: expect.stringContaining('validação'), - mensagem: expect.stringContaining('Estágio XYZ'), + mensagem: expect.stringContaining('Certificado XYZ'), }), ); }); @@ -134,7 +134,7 @@ describe('aoDocumentoAprovado', () => { documentoId: 'doc-1', estudanteId: 'est-1', coordenadorId: 'coord-1', - titulo: 'Estágio XYZ', + titulo: 'Certificado XYZ', }; it('gera certificado, persiste no banco e notifica o estudante', async () => { @@ -161,7 +161,7 @@ describe('aoDocumentoAprovado', () => { destinatarioId: 'est-1', destinatarioEmail: 'joao@uni.edu', assunto: expect.stringContaining('aprovado'), - mensagem: expect.stringContaining('Estágio XYZ'), + mensagem: expect.stringContaining('Certificado XYZ'), }), ); }); @@ -208,7 +208,7 @@ describe('aoDocumentoReprovado', () => { documentoId: 'doc-1', estudanteId: 'est-1', coordenadorId: 'coord-1', - titulo: 'Estágio XYZ', + titulo: 'Certificado XYZ', observacoes: 'Documentação incompleta', }; @@ -250,7 +250,7 @@ describe('aoDocumentoRevisaoSolicitada', () => { documentoId: 'doc-1', estudanteId: 'est-1', coordenadorId: 'coord-1', - titulo: 'Estágio XYZ', + titulo: 'Certificado XYZ', observacoes: 'Ajustar carga horária', }; diff --git a/backend/testes/unitarios/repositorios/documento.test.ts b/backend/testes/unitarios/repositorios/documento.test.ts index c5aeb9a..e81c527 100644 --- a/backend/testes/unitarios/repositorios/documento.test.ts +++ b/backend/testes/unitarios/repositorios/documento.test.ts @@ -14,15 +14,15 @@ const mockQuery = pool.query as jest.Mock; const DOC_ROW = { id: 'doc-1', - titulo: 'Estágio XYZ', - tipo: 'estagio', + titulo: 'Certificado XYZ', + tipo: 'certificado_curso', carga_horaria: 40, estudante_id: 'est-1', curso_id: 'curso-1', status: 'pendente', coordenador_id: null, - nome_arquivo: 'estagio.pdf', - caminho_arquivo: 'documentos/est-1/estagio.pdf', + nome_arquivo: 'certificado.pdf', + caminho_arquivo: 'documentos/est-1/certificado.pdf', tamanho_arquivo: 1024, mime_type: 'application/pdf', criado_em: new Date(), @@ -39,13 +39,13 @@ describe('criar', () => { mockQuery.mockResolvedValueOnce({ rows: [DOC_ROW] }); const resultado = await criar({ - titulo: 'Estágio XYZ', - tipo: 'estagio', + titulo: 'Certificado XYZ', + tipo: 'certificado_curso', carga_horaria: 40, estudante_id: 'est-1', curso_id: 'curso-1', - nome_arquivo: 'estagio.pdf', - caminho_arquivo: 'documentos/est-1/estagio.pdf', + nome_arquivo: 'certificado.pdf', + caminho_arquivo: 'documentos/est-1/certificado.pdf', tamanho_arquivo: 1024, mime_type: 'application/pdf', }); diff --git a/backend/testes/unitarios/repositorios/validacao.test.ts b/backend/testes/unitarios/repositorios/validacao.test.ts index 7faa470..f859193 100644 --- a/backend/testes/unitarios/repositorios/validacao.test.ts +++ b/backend/testes/unitarios/repositorios/validacao.test.ts @@ -21,7 +21,7 @@ const clienteMock = { const DOC_ROW = { id: 'doc-1', - titulo: 'Estágio XYZ', + titulo: 'Certificado XYZ', status: 'pendente', estudante_id: 'est-1', curso_id: 'curso-1', diff --git a/backend/testes/unitarios/servicos/gerador-certificado.test.ts b/backend/testes/unitarios/servicos/gerador-certificado.test.ts index d84115a..685ff98 100644 --- a/backend/testes/unitarios/servicos/gerador-certificado.test.ts +++ b/backend/testes/unitarios/servicos/gerador-certificado.test.ts @@ -20,8 +20,8 @@ const DADOS_BASE: DadosCertificado = { documentoId: 'doc-1', estudanteNome: 'João Silva', estudanteEmail: 'joao@uni.edu', - titulo: 'Estágio XYZ', - tipo: 'estagio', + titulo: 'Certificado XYZ', + tipo: 'certificado_curso', cargaHoraria: 40, dataAprovacao: new Date('2024-06-01'), }; diff --git a/frontend/src/pages/Cursos.tsx b/frontend/src/pages/Cursos.tsx index b6b9fad..1044455 100644 --- a/frontend/src/pages/Cursos.tsx +++ b/frontend/src/pages/Cursos.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { cursosService, type CursoComContagem, type CriarCursoInput } from '../services/cursos'; import { instituicoesService, type Instituicao } from '../services/instituicoes'; @@ -47,21 +47,40 @@ function Modal({ onClose: () => void; children: React.ReactNode; }) { + useEffect(() => { + document.body.style.overflow = 'hidden'; + return () => { document.body.style.overflow = ''; }; + }, []); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); }; + document.addEventListener('keydown', onKey); + return () => document.removeEventListener('keydown', onKey); + }, [onClose]); + return ( -
-
-
-

{titulo}

- +
+
+
e.stopPropagation()} + > +
+

{titulo}

+ +
+
{children}
-
{children}
); diff --git a/frontend/src/pages/DetalheDocumento.tsx b/frontend/src/pages/DetalheDocumento.tsx index 51245f0..54c95fa 100644 --- a/frontend/src/pages/DetalheDocumento.tsx +++ b/frontend/src/pages/DetalheDocumento.tsx @@ -75,6 +75,13 @@ export function DetalheDocumento() { onError: () => addToast('Erro ao cancelar o documento.', 'error'), }); + const { data: previewData, isLoading: previewLoading } = useQuery({ + queryKey: ['documento-url', id], + queryFn: () => documentosService.buscarUrlDownload(id!), + enabled: !!id && !!doc, + staleTime: 50 * 60 * 1000, + }); + const mutDownload = useMutation({ mutationFn: () => documentosService.buscarUrlDownload(id!), onSuccess: ({ url }) => window.open(url, '_blank'), @@ -139,6 +146,33 @@ export function DetalheDocumento() {
+ {/* Pré-visualização PDF */} + +

Pré-visualização

+ {previewLoading && ( +
+ + Carregando documento... +
+ )} + {previewData && ( +