diff --git a/apps/web/package.json b/apps/web/package.json index c498b30..44bda5f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -49,6 +49,7 @@ "date-fns": "^4.1.0", "emoji-picker-element": "^1.27.0", "eslint": "catalog:", + "exceljs": "^4.4.0", "is-network-error": "^1.3.0", "jspdf": "^3.0.3", "jspdf-autotable": "^5.0.2", @@ -57,6 +58,7 @@ "papaparse": "^5.4.1", "svelte-sonner": "^1.0.5", "xlsx": "^0.18.5", + "xlsx-js-style": "^1.2.0", "zod": "^4.1.12" } } \ No newline at end of file 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 ddb0bd2..2d46f71 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 @@ -13,7 +13,7 @@ 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 ExcelJS from 'exceljs'; import logoGovPE from '$lib/assets/logo_governo_PE.png'; import { format } from 'date-fns'; import { ptBR } from 'date-fns/locale'; @@ -729,19 +729,22 @@ headStyles: { fillColor: [41, 128, 185], textColor: [255, 255, 255], - fontStyle: 'bold' + fontStyle: 'bold', + fontSize: 7 }, - styles: { fontSize: 8 }, + styles: { fontSize: 7 }, 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 } - } + 0: { cellWidth: 45, fontSize: 7 }, // Funcionário + 1: { cellWidth: 20, fontSize: 7 }, // Matrícula + 2: { cellWidth: 35, fontSize: 7 }, // Tipo + 3: { cellWidth: 22, fontSize: 7 }, // Data Início + 4: { cellWidth: 22, fontSize: 7 }, // Data Fim + 5: { cellWidth: 12, fontSize: 7 }, // Dias + 6: { cellWidth: 18, fontSize: 7 }, // CID + 7: { cellWidth: 18, fontSize: 7 } // Status + }, + margin: { top: yPosition, left: 10, right: 10 }, + tableWidth: 'wrap' }); // Rodapé @@ -839,31 +842,192 @@ return dataA.getTime() - dataB.getTime(); }); - // Criar workbook - const wb = XLSX.utils.book_new(); + // Criar workbook com ExcelJS + const workbook = new ExcelJS.Workbook(); + const worksheet = workbook.addWorksheet('Relatório'); - // Criar worksheet - const ws = XLSX.utils.json_to_sheet(dados); + // Obter cabeçalhos + const headers = Object.keys(dados[0] || {}); + + // Carregar logo + let logoBuffer: ArrayBuffer | null = null; + try { + // Tentar carregar a logo via fetch primeiro + const response = await fetch(logoGovPE); + if (response.ok) { + logoBuffer = await response.arrayBuffer(); + } else { + // Fallback: usar canvas + const logoImg = new Image(); + logoImg.crossOrigin = 'anonymous'; + logoImg.src = logoGovPE; + await new Promise((resolve, reject) => { + logoImg.onload = () => resolve(); + logoImg.onerror = () => reject(); + setTimeout(() => reject(), 3000); + }); + + const canvas = document.createElement('canvas'); + canvas.width = logoImg.width; + canvas.height = logoImg.height; + const ctx = canvas.getContext('2d'); + if (ctx) { + ctx.drawImage(logoImg, 0, 0); + const blob = await new Promise((resolve, reject) => { + canvas.toBlob((blob) => { + if (blob) resolve(blob); + else reject(new Error('Falha ao converter imagem')); + }, 'image/png'); + }); + logoBuffer = await blob.arrayBuffer(); + } + } + } catch (err) { + console.warn('Não foi possível carregar a logo:', err); + } + + // Linha 1: Cabeçalho com logo e título + // Mesclar A1:B1 para logo + worksheet.mergeCells('A1:B1'); + const logoCell = worksheet.getCell('A1'); + logoCell.alignment = { vertical: 'middle', horizontal: 'left' }; + logoCell.border = { + right: { style: 'thin', color: { argb: 'FFE0E0E0' } } + }; + + // Adicionar logo se disponível + if (logoBuffer) { + const logoId = workbook.addImage({ + buffer: new Uint8Array(logoBuffer), + extension: 'png' + }); + // Ajustar tamanho da logo para caber na linha com altura 60 + worksheet.addImage(logoId, { + tl: { col: 0, row: 0 }, + ext: { width: 140, height: 55 } + }); + } + + // Mesclar C1 até última coluna para título + const lastCol = String.fromCharCode(65 + headers.length - 1); // A=65 + worksheet.mergeCells(`C1:${lastCol}1`); + const titleCell = worksheet.getCell('C1'); + titleCell.value = 'RELATÓRIO DE ATESTADOS E LICENÇAS'; + titleCell.font = { bold: true, size: 18, color: { argb: 'FF2980B9' } }; + titleCell.alignment = { vertical: 'middle', horizontal: 'center' }; + + // Ajustar altura da linha 1 para acomodar a logo + worksheet.getRow(1).height = 60; + + // Linha 2: Cabeçalhos da tabela + headers.forEach((header, index) => { + const cell = worksheet.getCell(2, index + 1); + cell.value = header; + cell.font = { bold: true, size: 11, color: { argb: 'FFFFFFFF' } }; + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FF2980B9' } + }; + cell.alignment = { vertical: 'middle', horizontal: 'center', wrapText: true }; + cell.border = { + top: { style: 'thin', color: { argb: 'FF000000' } }, + bottom: { style: 'thin', color: { argb: 'FF000000' } }, + left: { style: 'thin', color: { argb: 'FF000000' } }, + right: { style: 'thin', color: { argb: 'FF000000' } } + }; + }); + + // Ajustar altura da linha 2 + worksheet.getRow(2).height = 25; + + // Linhas 3+: Dados + dados.forEach((rowData, rowIndex) => { + const row = rowIndex + 3; // Começa na linha 3 + headers.forEach((header, colIndex) => { + const cell = worksheet.getCell(row, colIndex + 1); + cell.value = rowData[header]; + + // Cor de fundo alternada (zebra striping) + const isEvenRow = rowIndex % 2 === 1; + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: isEvenRow ? 'FFF8F9FA' : 'FFFFFFFF' } + }; + + // Alinhamento + let alignment: 'left' | 'center' | 'right' = 'left'; + if (header === 'Dias' || header === 'Data Início' || header === 'Data Fim' || header === 'Status') { + alignment = 'center'; + } + cell.alignment = { vertical: 'middle', horizontal: alignment, wrapText: true }; + + // Fonte + cell.font = { size: 10, color: { argb: 'FF000000' } }; + + // Bordas + cell.border = { + top: { style: 'thin', color: { argb: 'FFE0E0E0' } }, + bottom: { style: 'thin', color: { argb: 'FFE0E0E0' } }, + left: { style: 'thin', color: { argb: 'FFE0E0E0' } }, + right: { style: 'thin', color: { argb: 'FFE0E0E0' } } + }; + + // Formatação especial para Status + if (header === 'Status') { + const statusValue = rowData[header]; + if (statusValue === 'Ativo') { + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFD4EDDA' } + }; + cell.font = { size: 10, color: { argb: 'FF155724' } }; + } else if (statusValue === 'Finalizado') { + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: 'FFF8D7DA' } + }; + cell.font = { size: 10, color: { argb: 'FF721C24' } }; + } + } + }); + }); // 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 + worksheet.columns = [ + { width: 30 }, // Funcionário + { width: 15 }, // Matrícula + { width: 25 }, // Tipo + { width: 12 }, // Data Início + { width: 12 }, // Data Fim + { width: 8 }, // Dias + { width: 12 }, // CID + { width: 12 } // Status ]; - ws['!cols'] = colWidths; - // Adicionar worksheet ao workbook - XLSX.utils.book_append_sheet(wb, ws, 'Relatório'); + // Congelar linha 2 (cabeçalho da tabela) + worksheet.views = [ + { + state: 'frozen', + ySplit: 2, + topLeftCell: 'A3', + activeCell: 'A3' + } + ]; // Gerar arquivo const nomeArquivo = `relatorio-atestados-licencas-${format(new Date(), 'yyyy-MM-dd-HHmm')}.xlsx`; - XLSX.writeFile(wb, nomeArquivo); + const buffer = await workbook.xlsx.writeBuffer(); + const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = nomeArquivo; + link.click(); + window.URL.revokeObjectURL(url); toast.success('Relatório Excel gerado com sucesso!'); } catch (error) { @@ -874,26 +1038,22 @@ } } - // Função para selecionar/deselecionar todos os campos - function toggleTodosCampos() { + // Efeito para sincronizar o checkbox "Todos" com os demais campos + $effect(() => { if (relatorioCampos.todos) { - relatorioCampos = { - atestados: false, - licencaPaternidade: false, - licencaMaternidade: false, - declaracao: false, - todos: false - }; + // Se "Todos" está marcado, marcar todos os outros + relatorioCampos.atestados = true; + relatorioCampos.licencaPaternidade = true; + relatorioCampos.licencaMaternidade = true; + relatorioCampos.declaracao = true; } else { - relatorioCampos = { - atestados: true, - licencaPaternidade: true, - licencaMaternidade: true, - declaracao: true, - todos: true - }; + // Se "Todos" está desmarcado, desmarcar todos os outros + relatorioCampos.atestados = false; + relatorioCampos.licencaPaternidade = false; + relatorioCampos.licencaMaternidade = false; + relatorioCampos.declaracao = false; } - } + }); @@ -959,12 +1119,12 @@
@@ -1725,46 +1927,69 @@
-

Registrar Atestado Médico

+
+
+ + + +
+
+

Registrar Atestado Médico

+

Preencha os dados do atestado médico do funcionário

+
+
- +
+ +
-
- + Data Início * -
+
-
- Data Fim * -
+
-
- CID * -
+ @@ -1785,21 +2010,38 @@
-
- Observações -
+
-
- +
+
+ @@ -1817,33 +2073,56 @@
-

Registrar Declaração de Comparecimento

+
+
+ + + +
+
+

Registrar Declaração de Comparecimento

+

Preencha os dados da declaração de comparecimento

+
+
- +
+ +
-
- + Data Início * -
+
-
- Data Fim * -
+
@@ -1863,24 +2142,55 @@
-
- Observações -
+
-
- - + @@ -1891,61 +2201,84 @@
-

Registrar Licença Maternidade

+
+
+ + + +
+
+

Registrar Licença Maternidade

+

Preencha os dados da licença maternidade (120 dias)

+
+
- +
+ +
-
- + Data Início * -
+
-
- Data Fim * -
+ -
- Calculado automaticamente (120 dias) -
+
-
{#if licencaMaternidade.ehProrrogacao}
-
- + Licença Original * -
+
-
- +
+
+ @@ -2007,38 +2371,61 @@
-

Registrar Licença Paternidade

+
+
+ + + +
+
+

Registrar Licença Paternidade

+

Preencha os dados da licença paternidade (20 dias)

+
+
- +
+ +
-
- + Data Início * -
+
-
- Data Fim * -
+ -
- Calculado automaticamente (20 dias) -
+
@@ -2056,21 +2443,38 @@
-
- Observações -
+
-
- +
+
+
-
+
{:else if abaAtiva === 'relatorios'}
-

- - - - Imprimir Relatórios -

+
+
+ + + +
+
+

Imprimir Relatórios

+

Gere relatórios em PDF ou Excel com filtros personalizados

+
+
@@ -2137,7 +2560,7 @@
-
-
+
+ @@ -2169,7 +2593,6 @@ Todos @@ -2218,7 +2641,8 @@
-
+
+