From 7defdaa59d9edcea895ff7b279359c1f3e1419bb Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Sat, 29 Nov 2025 16:56:30 -0300 Subject: [PATCH] feat: add Excel and PDF report generation functionality for employee leave and certificate data, integrating XLSX and jsPDF libraries for enhanced reporting capabilities --- apps/web/package.json | 1 + .../FuncionarioMatriculaAutocomplete.svelte | 138 ++++ .../FuncionarioNomeAutocomplete.svelte | 134 ++++ .../atestados-licencas/+page.svelte | 603 +++++++++++++++++- bun.lock | 19 + 5 files changed, 893 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/lib/components/FuncionarioMatriculaAutocomplete.svelte create mode 100644 apps/web/src/lib/components/FuncionarioNomeAutocomplete.svelte diff --git a/apps/web/package.json b/apps/web/package.json index 7314345..c498b30 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -56,6 +56,7 @@ "lucide-svelte": "^0.552.0", "papaparse": "^5.4.1", "svelte-sonner": "^1.0.5", + "xlsx": "^0.18.5", "zod": "^4.1.12" } } \ No newline at end of file diff --git a/apps/web/src/lib/components/FuncionarioMatriculaAutocomplete.svelte b/apps/web/src/lib/components/FuncionarioMatriculaAutocomplete.svelte new file mode 100644 index 0000000..90b97e3 --- /dev/null +++ b/apps/web/src/lib/components/FuncionarioMatriculaAutocomplete.svelte @@ -0,0 +1,138 @@ + + +
+ + +
+ + + +
+ + {#if mostrarDropdown && funcionariosFiltrados.length > 0} +
+ {#each funcionariosFiltrados as funcionario} + + {/each} +
+ {/if} + + {#if mostrarDropdown && busca && funcionariosFiltrados.length === 0} +
+ Nenhum funcionário encontrado +
+ {/if} +
+ diff --git a/apps/web/src/lib/components/FuncionarioNomeAutocomplete.svelte b/apps/web/src/lib/components/FuncionarioNomeAutocomplete.svelte new file mode 100644 index 0000000..e4d0f6c --- /dev/null +++ b/apps/web/src/lib/components/FuncionarioNomeAutocomplete.svelte @@ -0,0 +1,134 @@ + + +
+ + +
+ + + +
+ + {#if mostrarDropdown && funcionariosFiltrados.length > 0} +
+ {#each funcionariosFiltrados as funcionario} + + {/each} +
+ {/if} + + {#if mostrarDropdown && busca && funcionariosFiltrados.length === 0} +
+ Nenhum funcionário encontrado +
+ {/if} +
+ diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/atestados-licencas/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/atestados-licencas/+page.svelte index 25b58ae..cc57459 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/atestados-licencas/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/atestados-licencas/+page.svelte @@ -5,15 +5,23 @@ import { api } from '@sgse-app/backend/convex/_generated/api'; import { toast } from 'svelte-sonner'; import FuncionarioSelect from '$lib/components/FuncionarioSelect.svelte'; + import FuncionarioNomeAutocomplete from '$lib/components/FuncionarioNomeAutocomplete.svelte'; + import FuncionarioMatriculaAutocomplete from '$lib/components/FuncionarioMatriculaAutocomplete.svelte'; import FileUpload from '$lib/components/FileUpload.svelte'; import ErrorModal from '$lib/components/ErrorModal.svelte'; import CalendarioAfastamentos from '$lib/components/CalendarioAfastamentos.svelte'; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; + import jsPDF from 'jspdf'; + import autoTable from 'jspdf-autotable'; + import * as XLSX from 'xlsx'; + import logoGovPE from '$lib/assets/logo_governo_PE.png'; + import { format } from 'date-fns'; + import { ptBR } from 'date-fns/locale'; const client = useConvexClient(); // Estado da aba ativa - let abaAtiva = $state<'dashboard' | 'atestado' | 'declaracao' | 'maternidade' | 'paternidade'>( + let abaAtiva = $state<'dashboard' | 'atestado' | 'declaracao' | 'maternidade' | 'paternidade' | 'relatorios'>( 'dashboard' ); @@ -489,6 +497,389 @@ filtroDataInicio = ''; filtroDataFim = ''; } + + // Estados para relatórios + let relatorioPeriodoInicio = $state(''); + let relatorioPeriodoFim = $state(''); + let relatorioFuncionarioNome = $state(''); + let relatorioFuncionarioMatricula = $state(''); + let relatorioCampos = $state({ + atestados: false, + licencaPaternidade: false, + licencaMaternidade: false, + declaracao: false, + todos: false + }); + let gerandoRelatorio = $state(false); + + // Função para obter dados filtrados para relatório + async function obterDadosRelatorio() { + const dados = dadosQuery?.data; + if (!dados) return { atestados: [], licencas: [] }; + + let atestados = dados.atestados; + let licencas = dados.licencas; + + // Filtro por período + if (relatorioPeriodoInicio && relatorioPeriodoFim) { + const inicio = new Date(relatorioPeriodoInicio); + const fim = new Date(relatorioPeriodoFim); + atestados = atestados.filter((a) => { + const aInicio = new Date(a.dataInicio); + const aFim = new Date(a.dataFim); + return ( + (aInicio >= inicio && aInicio <= fim) || + (aFim >= inicio && aFim <= fim) || + (aInicio <= inicio && aFim >= fim) + ); + }); + licencas = licencas.filter((l) => { + const lInicio = new Date(l.dataInicio); + const lFim = new Date(l.dataFim); + return ( + (lInicio >= inicio && lInicio <= fim) || + (lFim >= inicio && lFim <= fim) || + (lInicio <= inicio && lFim >= fim) + ); + }); + } + + // Filtro por funcionário (nome ou matrícula) + if (relatorioFuncionarioNome || relatorioFuncionarioMatricula) { + const termoNome = relatorioFuncionarioNome.toLowerCase(); + const termoMatricula = relatorioFuncionarioMatricula.toLowerCase(); + atestados = atestados.filter((a) => { + const nomeMatch = !termoNome || a.funcionario?.nome?.toLowerCase().includes(termoNome); + const matriculaMatch = !termoMatricula || a.funcionario?.matricula?.toLowerCase().includes(termoMatricula); + return nomeMatch && matriculaMatch; + }); + licencas = licencas.filter((l) => { + const nomeMatch = !termoNome || l.funcionario?.nome?.toLowerCase().includes(termoNome); + const matriculaMatch = !termoMatricula || l.funcionario?.matricula?.toLowerCase().includes(termoMatricula); + return nomeMatch && matriculaMatch; + }); + } + + // Filtro por campos selecionados + if (!relatorioCampos.todos) { + if (!relatorioCampos.atestados) { + atestados = atestados.filter((a) => a.tipo !== 'atestado_medico'); + } + if (!relatorioCampos.declaracao) { + atestados = atestados.filter((a) => a.tipo !== 'declaracao_comparecimento'); + } + if (!relatorioCampos.licencaMaternidade) { + licencas = licencas.filter((l) => l.tipo !== 'maternidade'); + } + if (!relatorioCampos.licencaPaternidade) { + licencas = licencas.filter((l) => l.tipo !== 'paternidade'); + } + } + + return { atestados, licencas }; + } + + // Função para gerar PDF + async function gerarPDF() { + if (!relatorioPeriodoInicio || !relatorioPeriodoFim) { + toast.error('Selecione o período para gerar o relatório'); + return; + } + + if (!relatorioCampos.todos && !relatorioCampos.atestados && !relatorioCampos.declaracao && !relatorioCampos.licencaMaternidade && !relatorioCampos.licencaPaternidade) { + toast.error('Selecione pelo menos um tipo de campo para incluir no relatório'); + return; + } + + gerandoRelatorio = true; + try { + const { atestados, licencas } = await obterDadosRelatorio(); + const doc = new jsPDF(); + + // Logo + let yPosition = 20; + try { + const logoImg = new Image(); + logoImg.src = logoGovPE; + await new Promise((resolve, reject) => { + logoImg.onload = () => resolve(); + logoImg.onerror = () => reject(); + setTimeout(() => reject(), 3000); + }); + + const logoWidth = 30; + const aspectRatio = logoImg.height / logoImg.width; + const logoHeight = logoWidth * aspectRatio; + + doc.addImage(logoImg, 'PNG', 15, 10, logoWidth, logoHeight); + yPosition = Math.max(25, 10 + logoHeight / 2); + } catch (err) { + console.warn('Não foi possível carregar a logo:', err); + } + + // Título + doc.setFontSize(18); + doc.setTextColor(41, 128, 185); + doc.text('RELATÓRIO DE ATESTADOS E LICENÇAS', 105, yPosition, { align: 'center' }); + + yPosition += 10; + + // Período + doc.setFontSize(11); + doc.setTextColor(0, 0, 0); + doc.text( + `Período: ${format(new Date(relatorioPeriodoInicio), 'dd/MM/yyyy', { locale: ptBR })} até ${format(new Date(relatorioPeriodoFim), 'dd/MM/yyyy', { locale: ptBR })}`, + 105, + yPosition, + { align: 'center' } + ); + + yPosition += 8; + + // Filtros aplicados + doc.setFontSize(9); + doc.setTextColor(100, 100, 100); + let filtrosTexto = 'Filtros: '; + if (relatorioFuncionarioNome) filtrosTexto += `Nome: ${relatorioFuncionarioNome}; `; + if (relatorioFuncionarioMatricula) filtrosTexto += `Matrícula: ${relatorioFuncionarioMatricula}; `; + if (relatorioCampos.todos) filtrosTexto += 'Todos os tipos'; + else { + const tipos = []; + if (relatorioCampos.atestados) tipos.push('Atestados'); + if (relatorioCampos.declaracao) tipos.push('Declarações'); + if (relatorioCampos.licencaMaternidade) tipos.push('Licença Maternidade'); + if (relatorioCampos.licencaPaternidade) tipos.push('Licença Paternidade'); + filtrosTexto += tipos.join(', '); + } + doc.text(filtrosTexto, 105, yPosition, { align: 'center', maxWidth: 180 }); + + yPosition += 10; + + // Data de geração + doc.setFontSize(9); + doc.text(`Gerado em: ${format(new Date(), 'dd/MM/yyyy HH:mm', { locale: ptBR })}`, 15, yPosition); + + yPosition += 12; + + // Preparar dados para tabela + const dadosTabela: string[][] = []; + + // Adicionar atestados + if (relatorioCampos.todos || relatorioCampos.atestados || relatorioCampos.declaracao) { + atestados.forEach((a) => { + const tipo = a.tipo === 'atestado_medico' ? 'Atestado Médico' : 'Declaração'; + dadosTabela.push([ + a.funcionario?.nome || '-', + a.funcionario?.matricula || '-', + tipo, + formatarData(a.dataInicio), + formatarData(a.dataFim), + a.dias.toString(), + a.cid || '-', + a.status === 'ativo' ? 'Ativo' : 'Finalizado' + ]); + }); + } + + // Adicionar licenças + if (relatorioCampos.todos || relatorioCampos.licencaMaternidade || relatorioCampos.licencaPaternidade) { + licencas.forEach((l) => { + const tipo = l.tipo === 'maternidade' ? 'Licença Maternidade' : 'Licença Paternidade'; + dadosTabela.push([ + l.funcionario?.nome || '-', + l.funcionario?.matricula || '-', + tipo + (l.ehProrrogacao ? ' (Prorrogação)' : ''), + formatarData(l.dataInicio), + formatarData(l.dataFim), + l.dias.toString(), + '-', + l.status === 'ativo' ? 'Ativo' : 'Finalizado' + ]); + }); + } + + // Ordenar por data de início + dadosTabela.sort((a, b) => { + const dataA = new Date(a[3].split('/').reverse().join('-')); + const dataB = new Date(b[3].split('/').reverse().join('-')); + return dataA.getTime() - dataB.getTime(); + }); + + // Tabela + if (dadosTabela.length > 0) { + autoTable(doc, { + startY: yPosition, + head: [['Funcionário', 'Matrícula', 'Tipo', 'Data Início', 'Data Fim', 'Dias', 'CID', 'Status']], + body: dadosTabela, + theme: 'striped', + headStyles: { + fillColor: [41, 128, 185], + textColor: [255, 255, 255], + fontStyle: 'bold' + }, + styles: { fontSize: 8 }, + columnStyles: { + 0: { cellWidth: 50 }, + 1: { cellWidth: 25 }, + 2: { cellWidth: 40 }, + 3: { cellWidth: 25 }, + 4: { cellWidth: 25 }, + 5: { cellWidth: 15 }, + 6: { cellWidth: 20 }, + 7: { cellWidth: 20 } + } + }); + + // Rodapé + const pageCount = doc.getNumberOfPages(); + for (let i = 1; i <= pageCount; i++) { + doc.setPage(i); + doc.setFontSize(8); + doc.setTextColor(128, 128, 128); + doc.text( + `SGSE - Sistema de Gerenciamento de Secretaria | Página ${i} de ${pageCount}`, + doc.internal.pageSize.getWidth() / 2, + doc.internal.pageSize.getHeight() - 10, + { align: 'center' } + ); + } + } else { + doc.setFontSize(12); + doc.setTextColor(150, 150, 150); + doc.text('Nenhum registro encontrado para os filtros selecionados', 105, yPosition + 20, { + align: 'center' + }); + } + + // Salvar + const nomeArquivo = `relatorio-atestados-licencas-${format(new Date(), 'yyyy-MM-dd-HHmm')}.pdf`; + doc.save(nomeArquivo); + toast.success('Relatório PDF gerado com sucesso!'); + } catch (error) { + console.error('Erro ao gerar PDF:', error); + toast.error('Erro ao gerar relatório PDF. Tente novamente.'); + } finally { + gerandoRelatorio = false; + } + } + + // Função para gerar Excel + async function gerarExcel() { + if (!relatorioPeriodoInicio || !relatorioPeriodoFim) { + toast.error('Selecione o período para gerar o relatório'); + return; + } + + if (!relatorioCampos.todos && !relatorioCampos.atestados && !relatorioCampos.declaracao && !relatorioCampos.licencaMaternidade && !relatorioCampos.licencaPaternidade) { + toast.error('Selecione pelo menos um tipo de campo para incluir no relatório'); + return; + } + + gerandoRelatorio = true; + try { + const { atestados, licencas } = await obterDadosRelatorio(); + + // Preparar dados + const dados: Array> = []; + + // Adicionar atestados + if (relatorioCampos.todos || relatorioCampos.atestados || relatorioCampos.declaracao) { + atestados.forEach((a) => { + const tipo = a.tipo === 'atestado_medico' ? 'Atestado Médico' : 'Declaração'; + dados.push({ + 'Funcionário': a.funcionario?.nome || '-', + 'Matrícula': a.funcionario?.matricula || '-', + 'Tipo': tipo, + 'Data Início': formatarData(a.dataInicio), + 'Data Fim': formatarData(a.dataFim), + 'Dias': a.dias, + 'CID': a.cid || '-', + 'Status': a.status === 'ativo' ? 'Ativo' : 'Finalizado' + }); + }); + } + + // Adicionar licenças + if (relatorioCampos.todos || relatorioCampos.licencaMaternidade || relatorioCampos.licencaPaternidade) { + licencas.forEach((l) => { + const tipo = l.tipo === 'maternidade' ? 'Licença Maternidade' : 'Licença Paternidade'; + dados.push({ + 'Funcionário': l.funcionario?.nome || '-', + 'Matrícula': l.funcionario?.matricula || '-', + 'Tipo': tipo + (l.ehProrrogacao ? ' (Prorrogação)' : ''), + 'Data Início': formatarData(l.dataInicio), + 'Data Fim': formatarData(l.dataFim), + 'Dias': l.dias, + 'CID': '-', + 'Status': l.status === 'ativo' ? 'Ativo' : 'Finalizado' + }); + }); + } + + // Ordenar por data de início + dados.sort((a, b) => { + const dataAStr = a['Data Início'] as string; + const dataBStr = b['Data Início'] as string; + const dataA = new Date(dataAStr.split('/').reverse().join('-')); + const dataB = new Date(dataBStr.split('/').reverse().join('-')); + return dataA.getTime() - dataB.getTime(); + }); + + // Criar workbook + const wb = XLSX.utils.book_new(); + + // Criar worksheet + const ws = XLSX.utils.json_to_sheet(dados); + + // Ajustar largura das colunas + const colWidths = [ + { wch: 30 }, // Funcionário + { wch: 15 }, // Matrícula + { wch: 25 }, // Tipo + { wch: 12 }, // Data Início + { wch: 12 }, // Data Fim + { wch: 8 }, // Dias + { wch: 12 }, // CID + { wch: 12 } // Status + ]; + ws['!cols'] = colWidths; + + // Adicionar worksheet ao workbook + XLSX.utils.book_append_sheet(wb, ws, 'Relatório'); + + // Gerar arquivo + const nomeArquivo = `relatorio-atestados-licencas-${format(new Date(), 'yyyy-MM-dd-HHmm')}.xlsx`; + XLSX.writeFile(wb, nomeArquivo); + + toast.success('Relatório Excel gerado com sucesso!'); + } catch (error) { + console.error('Erro ao gerar Excel:', error); + toast.error('Erro ao gerar relatório Excel. Tente novamente.'); + } finally { + gerandoRelatorio = false; + } + } + + // Função para selecionar/deselecionar todos os campos + function toggleTodosCampos() { + if (relatorioCampos.todos) { + relatorioCampos = { + atestados: false, + licencaPaternidade: false, + licencaMaternidade: false, + declaracao: false, + todos: false + }; + } else { + relatorioCampos = { + atestados: true, + licencaPaternidade: true, + licencaMaternidade: true, + declaracao: true, + todos: true + }; + } + } @@ -611,6 +1002,26 @@ > Licença Paternidade + @@ -1657,10 +2068,198 @@ {/if} + + + {:else if abaAtiva === 'relatorios'} + +
+
+

+ + + + Imprimir Relatórios +

+ +
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+ + +
+ + +
+
+ + +
+ +
+ + + + + + + + + +
+
+ + +
+ + +
{/if} - +