From db2daacdad2c498b9f8ad9edf47fca43708410cf Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Sun, 23 Nov 2025 09:06:15 -0300 Subject: [PATCH] refactor: improve table rendering and layout in registro-pontos page - Enhanced the rendering of tables for employee time records, consolidating data into structured formats for better readability. - Updated the logic for displaying various sections, including employee information, GPS validation, and geofencing, to utilize tables for clarity. - Improved styling and layout consistency across the page, ensuring a more user-friendly experience when reviewing time records and validation statuses. --- .../registro-pontos/+page.svelte | 938 +++++++++++------- 1 file changed, 571 insertions(+), 367 deletions(-) diff --git a/apps/web/src/routes/(dashboard)/recursos-humanos/registro-pontos/+page.svelte b/apps/web/src/routes/(dashboard)/recursos-humanos/registro-pontos/+page.svelte index 962e565..9b2091d 100644 --- a/apps/web/src/routes/(dashboard)/recursos-humanos/registro-pontos/+page.svelte +++ b/apps/web/src/routes/(dashboard)/recursos-humanos/registro-pontos/+page.svelte @@ -1515,9 +1515,9 @@ content: `+${saldoPar.trabalhadoHoras}h ${saldoPar.trabalhadoMinutosResto}min / ${sinalDiferenca}${saldoPar.diferencaHoras}h ${saldoPar.diferencaMinutosResto}min`, rowSpan: saldoPar.tamanhoPar }); - } else { - linha.push('-'); - } + } else { + linha.push('-'); + } } else { linha.push('-'); } @@ -1547,7 +1547,7 @@ content: `+0h 0min / ${sinalDiferenca}${saldoEsperado.diferencaHoras}h ${saldoEsperado.diferencaMinutosResto}min`, rowSpan: saldoEsperado.tamanhoPar }); - } else { + } else { linha.push({ content: `+${saldoEsperado.trabalhadoHoras}h ${saldoEsperado.trabalhadoMinutosResto}min / ${sinalDiferenca}${saldoEsperado.diferencaHoras}h ${saldoEsperado.diferencaMinutosResto}min`, rowSpan: saldoEsperado.tamanhoPar @@ -1608,12 +1608,12 @@ content: `+0h 0min / ${sinalDiferenca}${saldoEsperadoCompleto.diferencaHoras}h ${saldoEsperadoCompleto.diferencaMinutosResto}min`, rowSpan: saldoEsperadoCompleto.tamanhoPar }); - } else { + } else { linha.push('-'); - } - } else { - linha.push('-'); - } + } + } else { + linha.push('-'); + } } } else { // Saída não marcada: verificar se faz parte de um par completamente não marcado @@ -1888,7 +1888,7 @@ if (data.column.index === 1 && valor) { // Aplicar cor no Saldo Atual (vermelho para negativo, azul para positivo) if (campo === 'Saldo Atual') { - if (saldoMinutos < 0) { + if (saldoMinutos < 0) { data.cell.styles.textColor = [200, 0, 0]; // Vermelho para negativo } else if (saldoMinutos > 0) { data.cell.styles.textColor = [0, 100, 200]; // Azul para positivo @@ -1950,7 +1950,7 @@ const finalY = (doc as { lastAutoTable?: { finalY: number } }).lastAutoTable?.finalY; if (finalY) { yPosition = finalY + 10; - } else { + } else { yPosition += bancoHorasData.length * 7 + 10; } } else { @@ -2151,56 +2151,79 @@ setTimeout(() => reject(), 3000); }); - const logoWidth = 25; + const logoWidth = 30; const aspectRatio = logoImg.height / logoImg.width; const logoHeight = logoWidth * aspectRatio; doc.addImage(logoImg, 'PNG', 15, 10, logoWidth, logoHeight); - yPosition = Math.max(20, 10 + logoHeight / 2); + yPosition = Math.max(25, 10 + logoHeight / 2); } catch (err) { console.warn('Não foi possível carregar a logo:', err); } - // Cabeçalho - doc.setFontSize(16); + // Cabeçalho com estilo melhorado + doc.setFontSize(18); doc.setTextColor(41, 128, 185); + doc.setFont('helvetica', 'bold'); doc.text('DETALHES DO REGISTRO DE PONTO', 105, yPosition, { align: 'center' }); + + // Linha decorativa abaixo do título + doc.setDrawColor(41, 128, 185); + doc.setLineWidth(0.5); + doc.line(15, yPosition + 3, 195, yPosition + 3); yPosition += 15; - // Informações do Funcionário - doc.setFontSize(12); - doc.setTextColor(0, 0, 0); - doc.setFont('helvetica', 'bold'); - doc.text('DADOS DO FUNCIONÁRIO', 15, yPosition); - doc.setFont('helvetica', 'normal'); - - yPosition += 8; - doc.setFontSize(10); - + // Informações do Funcionário em tabela + const funcionarioData: any[][] = []; if (registro.funcionario) { if (registro.funcionario.matricula) { - doc.text(`Matrícula: ${registro.funcionario.matricula}`, 15, yPosition); - yPosition += 6; + funcionarioData.push(['Matrícula', registro.funcionario.matricula]); } - doc.text(`Nome: ${registro.funcionario.nome}`, 15, yPosition); - yPosition += 6; + funcionarioData.push(['Nome', registro.funcionario.nome]); if (registro.funcionario.descricaoCargo) { - doc.text(`Cargo/Função: ${registro.funcionario.descricaoCargo}`, 15, yPosition); - yPosition += 6; + funcionarioData.push(['Cargo/Função', registro.funcionario.descricaoCargo]); } } - yPosition += 5; - - // Informações do Registro + if (funcionarioData.length > 0) { + doc.setFontSize(12); + doc.setTextColor(41, 128, 185); doc.setFont('helvetica', 'bold'); - doc.text('DADOS DO REGISTRO', 15, yPosition); - doc.setFont('helvetica', 'normal'); - + doc.text('DADOS DO FUNCIONÁRIO', 15, yPosition); yPosition += 8; - doc.setFontSize(10); + autoTable(doc, { + startY: yPosition, + head: [['Campo', 'Valor']], + body: funcionarioData, + theme: 'striped', + headStyles: { + fillColor: [41, 128, 185], + textColor: [255, 255, 255], + fontStyle: 'bold', + fontSize: 10 + }, + bodyStyles: { + fontSize: 10, + textColor: [0, 0, 0] + }, + columnStyles: { + 0: { cellWidth: 50, fontStyle: 'bold' }, + 1: { cellWidth: 140 } + }, + margin: { left: 15, right: 15 }, + styles: { cellPadding: 4 } + }); + + type JsPDFWithAutoTable = jsPDF & { + lastAutoTable?: { finalY: number }; + }; + const finalY = (doc as JsPDFWithAutoTable).lastAutoTable?.finalY ?? yPosition + 10; + yPosition = finalY + 10; + } + + // Informações do Registro em tabela const config = await client.query(api.configuracaoPonto.obterConfiguracao, {}); const tipoLabel = config ? getTipoRegistroLabel(registro.tipo, { @@ -2210,34 +2233,75 @@ nomeSaida: config.nomeSaida, }) : getTipoRegistroLabel(registro.tipo); - doc.text(`Tipo: ${tipoLabel}`, 15, yPosition); - yPosition += 6; const dataHora = `${registro.data} ${registro.hora.toString().padStart(2, '0')}:${registro.minuto.toString().padStart(2, '0')}:${registro.segundo.toString().padStart(2, '0')}`; - doc.text(`Data e Hora: ${dataHora}`, 15, yPosition); - yPosition += 6; - - doc.text(`Status: ${registro.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'}`, 15, yPosition); - yPosition += 6; - - doc.text(`Tolerância: ${registro.toleranciaMinutos} minutos`, 15, yPosition); - yPosition += 6; - - doc.text( - `Sincronizado: ${registro.sincronizadoComServidor ? 'Sim (Servidor)' : 'Não (PC Local)'}`, - 15, - yPosition - ); - yPosition += 6; + + const registroData: any[][] = [ + ['Tipo', tipoLabel], + ['Data e Hora', dataHora], + ['Status', registro.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'], + ['Tolerância', `${registro.toleranciaMinutos} minutos`], + ['Sincronizado', registro.sincronizadoComServidor ? 'Sim (Servidor)' : 'Não (PC Local)'] + ]; if (registro.justificativa) { - doc.text(`Justificativa: ${registro.justificativa}`, 15, yPosition); - yPosition += 6; + registroData.push(['Justificativa', registro.justificativa]); } - yPosition += 5; + // Verificar se precisa de nova página + if (yPosition > 200) { + doc.addPage(); + yPosition = 20; + } - // Localização + doc.setFontSize(12); + doc.setTextColor(41, 128, 185); + doc.setFont('helvetica', 'bold'); + doc.text('DADOS DO REGISTRO', 15, yPosition); + yPosition += 8; + + autoTable(doc, { + startY: yPosition, + head: [['Campo', 'Valor']], + body: registroData, + theme: 'striped', + headStyles: { + fillColor: [41, 128, 185], + textColor: [255, 255, 255], + fontStyle: 'bold', + fontSize: 10 + }, + bodyStyles: { + fontSize: 10, + textColor: [0, 0, 0] + }, + columnStyles: { + 0: { cellWidth: 50, fontStyle: 'bold' }, + 1: { cellWidth: 140 } + }, + margin: { left: 15, right: 15 }, + styles: { cellPadding: 4 }, + didParseCell: (data: any) => { + if (data.section === 'body' && data.column.index === 1) { + // Aplicar cor verde para "Dentro do Prazo" e vermelho para "Fora do Prazo" + if (data.cell.text[0] === 'Dentro do Prazo') { + data.cell.styles.textColor = [0, 128, 0]; + data.cell.styles.fontStyle = 'bold'; + } else if (data.cell.text[0] === 'Fora do Prazo') { + data.cell.styles.textColor = [200, 0, 0]; + data.cell.styles.fontStyle = 'bold'; + } + } + } + }); + + type JsPDFWithAutoTable2 = jsPDF & { + lastAutoTable?: { finalY: number }; + }; + const finalYRegistro = (doc as JsPDFWithAutoTable2).lastAutoTable?.finalY ?? yPosition + 10; + yPosition = finalYRegistro + 10; + + // Localização em tabela if (registro.latitude && registro.longitude) { // Verificar se precisa de nova página if (yPosition > 200) { @@ -2245,50 +2309,69 @@ yPosition = 20; } - doc.setFont('helvetica', 'bold'); - doc.text('LOCALIZAÇÃO', 15, yPosition); - doc.setFont('helvetica', 'normal'); - - yPosition += 8; - doc.setFontSize(10); - - doc.text(`Latitude: ${registro.latitude.toFixed(6)}`, 15, yPosition); - yPosition += 6; - - doc.text(`Longitude: ${registro.longitude.toFixed(6)}`, 15, yPosition); - yPosition += 6; + const localizacaoData: any[][] = [ + ['Latitude', registro.latitude.toFixed(6)], + ['Longitude', registro.longitude.toFixed(6)] + ]; if (registro.precisao) { - doc.text(`Precisão: ${registro.precisao.toFixed(2)} metros`, 15, yPosition); - yPosition += 6; + localizacaoData.push(['Precisão', `${registro.precisao.toFixed(2)} metros`]); } if (registro.endereco) { - doc.text(`Endereço: ${registro.endereco}`, 15, yPosition); - yPosition += 6; + localizacaoData.push(['Endereço', registro.endereco]); } if (registro.cidade) { - doc.text(`Cidade: ${registro.cidade}`, 15, yPosition); - yPosition += 6; + localizacaoData.push(['Cidade', registro.cidade]); } if (registro.estado) { - doc.text(`Estado: ${registro.estado}`, 15, yPosition); - yPosition += 6; + localizacaoData.push(['Estado', registro.estado]); } if (registro.pais) { - doc.text(`País: ${registro.pais}`, 15, yPosition); - yPosition += 6; + localizacaoData.push(['País', registro.pais]); } if (registro.timezone) { - doc.text(`Fuso Horário: ${registro.timezone}`, 15, yPosition); - yPosition += 6; + localizacaoData.push(['Fuso Horário', registro.timezone]); } - yPosition += 5; + doc.setFontSize(12); + doc.setTextColor(41, 128, 185); + doc.setFont('helvetica', 'bold'); + doc.text('LOCALIZAÇÃO', 15, yPosition); + yPosition += 8; + + autoTable(doc, { + startY: yPosition, + head: [['Campo', 'Valor']], + body: localizacaoData, + theme: 'striped', + headStyles: { + fillColor: [41, 128, 185], + textColor: [255, 255, 255], + fontStyle: 'bold', + fontSize: 10 + }, + bodyStyles: { + fontSize: 10, + textColor: [0, 0, 0] + }, + columnStyles: { + 0: { cellWidth: 50, fontStyle: 'bold' }, + 1: { cellWidth: 140 } + }, + margin: { left: 15, right: 15 }, + styles: { cellPadding: 4 } + }); + + type JsPDFWithAutoTable3 = jsPDF & { + lastAutoTable?: { finalY: number }; + }; + const finalYLocalizacao = (doc as JsPDFWithAutoTable3).lastAutoTable?.finalY ?? yPosition + 10; + yPosition = finalYLocalizacao + 10; } // Validação de GPS e Anti-Spoofing @@ -2299,185 +2382,318 @@ yPosition = 20; } - doc.setFont('helvetica', 'bold'); doc.setFontSize(12); + doc.setTextColor(41, 128, 185); + doc.setFont('helvetica', 'bold'); doc.text('VALIDAÇÃO DE LOCALIZAÇÃO GPS', 15, yPosition); - doc.setFont('helvetica', 'normal'); - doc.setFontSize(10); yPosition += 10; - // Informações detalhadas do GPS - doc.setFont('helvetica', 'bold'); - doc.text('Dados do GPS:', 15, yPosition); - doc.setFont('helvetica', 'normal'); - yPosition += 6; - + // Dados do GPS em tabela + const gpsData: any[][] = []; if (registro.precisao !== null && registro.precisao !== undefined) { - doc.text(` Precisão: ${registro.precisao.toFixed(2)} metros`, 20, yPosition); - yPosition += 6; + gpsData.push(['Precisão', `${registro.precisao.toFixed(2)} metros`]); } - if (registro.altitude !== null && registro.altitude !== undefined) { - doc.text(` Altitude: ${registro.altitude.toFixed(2)} metros`, 20, yPosition); - yPosition += 6; + if (gpsData.length > 0) { + autoTable(doc, { + startY: yPosition, + head: [['Campo', 'Valor']], + body: gpsData, + theme: 'striped', + headStyles: { + fillColor: [41, 128, 185], + textColor: [255, 255, 255], + fontStyle: 'bold', + fontSize: 10 + }, + bodyStyles: { + fontSize: 10, + textColor: [0, 0, 0] + }, + columnStyles: { + 0: { cellWidth: 50, fontStyle: 'bold' }, + 1: { cellWidth: 140 } + }, + margin: { left: 15, right: 15 }, + styles: { cellPadding: 4 } + }); + + type JsPDFWithAutoTable4 = jsPDF & { + lastAutoTable?: { finalY: number }; + }; + const finalYGps = (doc as JsPDFWithAutoTable4).lastAutoTable?.finalY ?? yPosition + 10; + yPosition = finalYGps + 5; } - if (registro.altitudeAccuracy !== null && registro.altitudeAccuracy !== undefined) { - doc.text(` Precisão da Altitude: ${registro.altitudeAccuracy.toFixed(2)} metros`, 20, yPosition); - yPosition += 6; - } - - if (registro.heading !== null && registro.heading !== undefined) { - doc.text(` Direção (Heading): ${registro.heading.toFixed(2)}°`, 20, yPosition); - yPosition += 6; - } - - if (registro.speed !== null && registro.speed !== undefined) { - doc.text(` Velocidade: ${(registro.speed * 3.6).toFixed(2)} km/h`, 20, yPosition); - yPosition += 6; - } - - yPosition += 3; - - // Confiabilidade e Scores - doc.setFont('helvetica', 'bold'); - doc.text('Confiabilidade:', 15, yPosition); - doc.setFont('helvetica', 'normal'); - yPosition += 6; - + // Confiabilidade em tabela + const confiabilidadeData: any[][] = []; if (registro.confiabilidadeGPS !== null && registro.confiabilidadeGPS !== undefined) { const confiabilidadePercent = (registro.confiabilidadeGPS * 100).toFixed(1); - const confiabilidadeCor = registro.confiabilidadeGPS >= 0.7 ? [0, 128, 0] : registro.confiabilidadeGPS >= 0.4 ? [255, 165, 0] : [255, 0, 0]; - doc.setTextColor(confiabilidadeCor[0], confiabilidadeCor[1], confiabilidadeCor[2]); - doc.text(` Confiabilidade GPS (Frontend): ${confiabilidadePercent}%`, 20, yPosition); - doc.setTextColor(0, 0, 0); - yPosition += 6; + confiabilidadeData.push(['Confiabilidade GPS (Frontend)', `${confiabilidadePercent}%`]); } if (registro.scoreConfiancaBackend !== null && registro.scoreConfiancaBackend !== undefined) { const scorePercent = (registro.scoreConfiancaBackend * 100).toFixed(1); - const scoreCor = registro.scoreConfiancaBackend >= 0.7 ? [0, 128, 0] : registro.scoreConfiancaBackend >= 0.4 ? [255, 165, 0] : [255, 0, 0]; - doc.setTextColor(scoreCor[0], scoreCor[1], scoreCor[2]); - doc.text(` Score de Confiança (Backend): ${scorePercent}%`, 20, yPosition); + confiabilidadeData.push(['Score de Confiança (Backend)', `${scorePercent}%`]); + } + + if (confiabilidadeData.length > 0) { + doc.setFontSize(11); doc.setTextColor(0, 0, 0); - yPosition += 6; - } + doc.setFont('helvetica', 'bold'); + doc.text('Confiabilidade:', 15, yPosition); + yPosition += 8; - yPosition += 3; - - // Status de Validação - if (registro.suspeitaSpoofing !== null && registro.suspeitaSpoofing !== undefined) { - doc.setFont('helvetica', 'bold'); - doc.text('Status de Validação:', 15, yPosition); - doc.setFont('helvetica', 'normal'); - yPosition += 6; - - if (registro.suspeitaSpoofing) { - doc.setTextColor(255, 0, 0); - doc.setFont('helvetica', 'bold'); - doc.text(' ⚠️ MARCAÇÃO SUSPEITA DETECTADA', 20, yPosition); - doc.setFont('helvetica', 'normal'); - doc.setTextColor(0, 0, 0); - yPosition += 6; - } else { - doc.setTextColor(0, 128, 0); - doc.setFont('helvetica', 'bold'); - doc.text(' ✓ Localização validada com sucesso', 20, yPosition); - doc.setFont('helvetica', 'normal'); - doc.setTextColor(0, 0, 0); - yPosition += 6; - } - - if (registro.motivoSuspeita) { - doc.setTextColor(255, 0, 0); - const motivoLines = doc.splitTextToSize(` Motivo: ${registro.motivoSuspeita}`, 170); - doc.text(motivoLines, 20, yPosition); - yPosition += motivoLines.length * 5; - doc.setTextColor(0, 0, 0); - } - - yPosition += 3; - } - - // Avisos de Validação - if (registro.avisosValidacao && registro.avisosValidacao.length > 0) { - doc.setFont('helvetica', 'bold'); - doc.text('Avisos de Validação:', 15, yPosition); - doc.setFont('helvetica', 'normal'); - yPosition += 6; - - registro.avisosValidacao.forEach((aviso: string) => { - const avisoLines = doc.splitTextToSize(` • ${aviso}`, 170); - doc.text(avisoLines, 20, yPosition); - yPosition += avisoLines.length * 5; + autoTable(doc, { + startY: yPosition, + head: [['Campo', 'Valor']], + body: confiabilidadeData, + theme: 'striped', + headStyles: { + fillColor: [60, 60, 60], + textColor: [255, 255, 255], + fontStyle: 'bold', + fontSize: 10 + }, + bodyStyles: { + fontSize: 10 + }, + columnStyles: { + 0: { cellWidth: 80, fontStyle: 'bold' }, + 1: { cellWidth: 110 } + }, + margin: { left: 15, right: 15 }, + styles: { cellPadding: 4 }, + didParseCell: (data: any) => { + if (data.section === 'body' && data.column.index === 1) { + // Aplicar cores baseadas nos valores + const valorTexto = data.cell.text[0]; + const valorNum = parseFloat(valorTexto.replace('%', '')); + if (!isNaN(valorNum)) { + if (valorNum >= 70) { + data.cell.styles.textColor = [0, 128, 0]; + data.cell.styles.fontStyle = 'bold'; + } else if (valorNum >= 40) { + data.cell.styles.textColor = [255, 165, 0]; + data.cell.styles.fontStyle = 'bold'; + } else { + data.cell.styles.textColor = [255, 0, 0]; + data.cell.styles.fontStyle = 'bold'; + } + } + } + } }); - yPosition += 3; + type JsPDFWithAutoTable5 = jsPDF & { + lastAutoTable?: { finalY: number }; + }; + const finalYConf = (doc as JsPDFWithAutoTable5).lastAutoTable?.finalY ?? yPosition + 10; + yPosition = finalYConf + 5; } - // Análise de Propriedades GPS - doc.setFont('helvetica', 'bold'); - doc.text('Análise de Propriedades GPS:', 15, yPosition); - doc.setFont('helvetica', 'normal'); - yPosition += 6; + // Status de Validação em destaque + if (registro.suspeitaSpoofing !== null && registro.suspeitaSpoofing !== undefined) { + const statusData: any[][] = []; + if (registro.suspeitaSpoofing) { + statusData.push(['Status', '⚠️ MARCAÇÃO SUSPEITA DETECTADA']); + if (registro.motivoSuspeita) { + statusData.push(['Motivo', registro.motivoSuspeita]); + } + } else { + statusData.push(['Status', '✓ Localização validada com sucesso']); + } + + if (statusData.length > 0) { + doc.setFontSize(11); + doc.setTextColor(0, 0, 0); + doc.setFont('helvetica', 'bold'); + doc.text('Status de Validação:', 15, yPosition); + yPosition += 8; + + autoTable(doc, { + startY: yPosition, + head: [['Campo', 'Valor']], + body: statusData, + theme: 'striped', + headStyles: { + fillColor: registro.suspeitaSpoofing ? [200, 0, 0] : [0, 128, 0], + textColor: [255, 255, 255], + fontStyle: 'bold', + fontSize: 10 + }, + bodyStyles: { + fontSize: 10 + }, + columnStyles: { + 0: { cellWidth: 50, fontStyle: 'bold' }, + 1: { cellWidth: 140 } + }, + margin: { left: 15, right: 15 }, + styles: { cellPadding: 4 }, + didParseCell: (data: any) => { + if (data.section === 'body' && data.column.index === 1) { + if (registro.suspeitaSpoofing) { + data.cell.styles.textColor = [200, 0, 0]; + data.cell.styles.fontStyle = 'bold'; + } else { + data.cell.styles.textColor = [0, 128, 0]; + data.cell.styles.fontStyle = 'bold'; + } + } + } + }); + + type JsPDFWithAutoTable6 = jsPDF & { + lastAutoTable?: { finalY: number }; + }; + const finalYStatus = (doc as JsPDFWithAutoTable6).lastAutoTable?.finalY ?? yPosition + 10; + yPosition = finalYStatus + 5; + } + } + + // Avisos de Validação em tabela + if (registro.avisosValidacao && registro.avisosValidacao.length > 0) { + const avisosData = registro.avisosValidacao.map((aviso: string) => ['', aviso]); + + doc.setFontSize(11); + doc.setTextColor(0, 0, 0); + doc.setFont('helvetica', 'bold'); + doc.text('Avisos de Validação:', 15, yPosition); + yPosition += 8; + + autoTable(doc, { + startY: yPosition, + head: [['', 'Aviso']], + body: avisosData, + theme: 'striped', + headStyles: { + fillColor: [255, 165, 0], + textColor: [255, 255, 255], + fontStyle: 'bold', + fontSize: 10 + }, + bodyStyles: { + fontSize: 10, + textColor: [0, 0, 0] + }, + columnStyles: { + 0: { cellWidth: 10 }, + 1: { cellWidth: 180 } + }, + margin: { left: 15, right: 15 }, + styles: { cellPadding: 4 } + }); + + type JsPDFWithAutoTable7 = jsPDF & { + lastAutoTable?: { finalY: number }; + }; + const finalYAvisos = (doc as JsPDFWithAutoTable7).lastAutoTable?.finalY ?? yPosition + 10; + yPosition = finalYAvisos + 5; + } + + // Análise de Propriedades GPS em tabela + const propriedadesData: any[][] = []; let propriedadesGPS = 0; let propriedadesTotais = 5; if (registro.altitude !== null && registro.altitude !== undefined && registro.altitude !== 0) { - doc.text(' ✓ Altitude disponível', 20, yPosition); + propriedadesData.push(['Altitude', '✓ Disponível']); propriedadesGPS++; } else { - doc.text(' ✗ Altitude não disponível', 20, yPosition); + propriedadesData.push(['Altitude', '✗ Não disponível']); } - yPosition += 5; if (registro.altitudeAccuracy !== null && registro.altitudeAccuracy !== undefined && registro.altitudeAccuracy > 0) { - doc.text(' ✓ Precisão de altitude disponível', 20, yPosition); + propriedadesData.push(['Precisão de Altitude', '✓ Disponível']); propriedadesGPS++; } else { - doc.text(' ✗ Precisão de altitude não disponível', 20, yPosition); + propriedadesData.push(['Precisão de Altitude', '✗ Não disponível']); } - yPosition += 5; if (registro.heading !== null && registro.heading !== undefined && !isNaN(registro.heading)) { - doc.text(' ✓ Direção (heading) disponível', 20, yPosition); + propriedadesData.push(['Direção (Heading)', '✓ Disponível']); propriedadesGPS++; } else { - doc.text(' ✗ Direção (heading) não disponível', 20, yPosition); + propriedadesData.push(['Direção (Heading)', '✗ Não disponível']); } - yPosition += 5; if (registro.speed !== null && registro.speed !== undefined && !isNaN(registro.speed)) { - doc.text(' ✓ Velocidade disponível', 20, yPosition); + propriedadesData.push(['Velocidade', '✓ Disponível']); propriedadesGPS++; } else { - doc.text(' ✗ Velocidade não disponível', 20, yPosition); + propriedadesData.push(['Velocidade', '✗ Não disponível']); } - yPosition += 5; if (registro.precisao !== null && registro.precisao !== undefined && registro.precisao < 20) { - doc.text(' ✓ Alta precisão GPS (< 20m)', 20, yPosition); + propriedadesData.push(['Precisão GPS', '✓ Alta precisão (< 20m)']); propriedadesGPS++; } else if (registro.precisao !== null && registro.precisao !== undefined && registro.precisao >= 20 && registro.precisao < 100) { - doc.text(' ⚠ Precisão média GPS (20-100m)', 20, yPosition); + propriedadesData.push(['Precisão GPS', '⚠ Precisão média (20-100m)']); propriedadesGPS += 0.5; } else { - doc.text(' ✗ Baixa precisão GPS (> 100m)', 20, yPosition); + propriedadesData.push(['Precisão GPS', '✗ Baixa precisão (> 100m)']); } - yPosition += 5; // Indicador de qualidade GPS const qualidadeGPS = (propriedadesGPS / propriedadesTotais) * 100; const qualidadeTexto = qualidadeGPS >= 80 ? 'Alta qualidade (GPS real)' : qualidadeGPS >= 50 ? 'Qualidade média' : 'Baixa qualidade (possível spoofing)'; const qualidadeCor = qualidadeGPS >= 80 ? [0, 128, 0] : qualidadeGPS >= 50 ? [255, 165, 0] : [255, 0, 0]; + propriedadesData.push(['Qualidade GPS', `${qualidadeTexto} (${qualidadeGPS.toFixed(0)}%)`]); - doc.setFont('helvetica', 'bold'); - doc.setTextColor(qualidadeCor[0], qualidadeCor[1], qualidadeCor[2]); - doc.text(`Qualidade GPS: ${qualidadeTexto} (${qualidadeGPS.toFixed(0)}% das propriedades)`, 20, yPosition); - doc.setFont('helvetica', 'normal'); + doc.setFontSize(11); doc.setTextColor(0, 0, 0); + doc.setFont('helvetica', 'bold'); + doc.text('Análise de Propriedades GPS:', 15, yPosition); yPosition += 8; + + autoTable(doc, { + startY: yPosition, + head: [['Propriedade', 'Status']], + body: propriedadesData, + theme: 'striped', + headStyles: { + fillColor: [60, 60, 60], + textColor: [255, 255, 255], + fontStyle: 'bold', + fontSize: 10 + }, + bodyStyles: { + fontSize: 10 + }, + columnStyles: { + 0: { cellWidth: 80, fontStyle: 'bold' }, + 1: { cellWidth: 110 } + }, + margin: { left: 15, right: 15 }, + styles: { cellPadding: 4 }, + didParseCell: (data: any) => { + if (data.section === 'body' && data.column.index === 1) { + const texto = data.cell.text[0]; + if (texto.includes('✓')) { + data.cell.styles.textColor = [0, 128, 0]; + data.cell.styles.fontStyle = 'bold'; + } else if (texto.includes('✗')) { + data.cell.styles.textColor = [200, 0, 0]; + } else if (texto.includes('⚠')) { + data.cell.styles.textColor = [255, 165, 0]; + data.cell.styles.fontStyle = 'bold'; + } + // Última linha (Qualidade GPS) + if (data.row.index === propriedadesData.length - 1) { + data.cell.styles.textColor = qualidadeCor; + data.cell.styles.fontStyle = 'bold'; + } + } + } + }); + + type JsPDFWithAutoTable8 = jsPDF & { + lastAutoTable?: { finalY: number }; + }; + const finalYPropriedades = (doc as JsPDFWithAutoTable8).lastAutoTable?.finalY ?? yPosition + 10; + yPosition = finalYPropriedades + 10; } // Validação de Geofencing (Localização Permitida) @@ -2488,11 +2704,10 @@ yPosition = 20; } - doc.setFont('helvetica', 'bold'); doc.setFontSize(12); + doc.setTextColor(41, 128, 185); + doc.setFont('helvetica', 'bold'); doc.text('VALIDAÇÃO DE LOCALIZAÇÃO PERMITIDA', 15, yPosition); - doc.setFont('helvetica', 'normal'); - doc.setFontSize(10); yPosition += 10; if (registro.enderecoMarcacaoEsperado || registro.dentroRaioPermitido !== undefined) { @@ -2519,89 +2734,43 @@ } } - doc.setFont('helvetica', 'bold'); - doc.text('Endereço Esperado:', 15, yPosition); - doc.setFont('helvetica', 'normal'); - yPosition += 6; - - doc.text(` Nome: ${enderecoEsperadoNome}`, 20, yPosition); - yPosition += 6; - - const enderecoLines = doc.splitTextToSize(` Endereço: ${enderecoEsperadoEndereco}`, 170); - doc.text(enderecoLines, 20, yPosition); - yPosition += enderecoLines.length * 5 + 3; + const geofencingData: any[][] = [ + ['Endereço Esperado', enderecoEsperadoNome], + ['Localização', enderecoEsperadoEndereco] + ]; if (enderecoEsperadoLatitude !== null && enderecoEsperadoLongitude !== null) { - doc.text(` Coordenadas: ${enderecoEsperadoLatitude.toFixed(6)}, ${enderecoEsperadoLongitude.toFixed(6)}`, 20, yPosition); - yPosition += 6; + geofencingData.push(['Coordenadas Esperadas', `${enderecoEsperadoLatitude.toFixed(6)}, ${enderecoEsperadoLongitude.toFixed(6)}`]); } - yPosition += 3; - - doc.setFont('helvetica', 'bold'); - doc.text('Localização do Registro:', 15, yPosition); - doc.setFont('helvetica', 'normal'); - yPosition += 6; - - doc.text(` Coordenadas: ${registro.latitude.toFixed(6)}, ${registro.longitude.toFixed(6)}`, 20, yPosition); - yPosition += 6; + geofencingData.push(['Coordenadas do Registro', `${registro.latitude.toFixed(6)}, ${registro.longitude.toFixed(6)}`]); if (registro.distanciaEnderecoEsperado !== null && registro.distanciaEnderecoEsperado !== undefined) { const distanciaKm = (registro.distanciaEnderecoEsperado / 1000).toFixed(2); const distanciaMetros = registro.distanciaEnderecoEsperado.toFixed(0); - - if (registro.distanciaEnderecoEsperado >= 1000) { - doc.text(` Distância: ${distanciaKm} km (${distanciaMetros} metros)`, 20, yPosition); - } else { - doc.text(` Distância: ${distanciaMetros} metros`, 20, yPosition); - } - yPosition += 6; + const distanciaTexto = registro.distanciaEnderecoEsperado >= 1000 + ? `${distanciaKm} km (${distanciaMetros} metros)` + : `${distanciaMetros} metros`; + geofencingData.push(['Distância', distanciaTexto]); } - yPosition += 3; - - doc.setFont('helvetica', 'bold'); - doc.text('Raio Permitido:', 15, yPosition); - doc.setFont('helvetica', 'normal'); - yPosition += 6; - if (registro.raioToleranciaUsado !== null && registro.raioToleranciaUsado !== undefined) { const raioKm = (registro.raioToleranciaUsado / 1000).toFixed(2); const raioMetros = registro.raioToleranciaUsado.toFixed(0); - - if (registro.raioToleranciaUsado >= 1000) { - doc.text(` ${raioKm} km (${raioMetros} metros)`, 20, yPosition); + const raioTexto = registro.raioToleranciaUsado >= 1000 + ? `${raioKm} km (${raioMetros} metros)` + : `${raioMetros} metros`; + geofencingData.push(['Raio Permitido', raioTexto]); } else { - doc.text(` ${raioMetros} metros`, 20, yPosition); - } - yPosition += 6; - } else { - doc.text(' Não configurado', 20, yPosition); - yPosition += 6; + geofencingData.push(['Raio Permitido', 'Não configurado']); } - yPosition += 3; - // Status da validação - doc.setFont('helvetica', 'bold'); - doc.text('Status:', 15, yPosition); - doc.setFont('helvetica', 'normal'); - yPosition += 6; - + let statusTexto = 'Não validado'; if (registro.dentroRaioPermitido === true) { - doc.setTextColor(0, 128, 0); - doc.setFont('helvetica', 'bold'); - doc.text(' ✓ DENTRO DO RAIO PERMITIDO', 20, yPosition); - doc.setFont('helvetica', 'normal'); - doc.setTextColor(0, 0, 0); + statusTexto = '✓ DENTRO DO RAIO PERMITIDO'; } else if (registro.dentroRaioPermitido === false) { - doc.setTextColor(255, 0, 0); - doc.setFont('helvetica', 'bold'); - doc.text(' ⚠️ FORA DO RAIO PERMITIDO', 20, yPosition); - doc.setFont('helvetica', 'normal'); - doc.setTextColor(0, 0, 0); - yPosition += 6; - + statusTexto = '⚠️ FORA DO RAIO PERMITIDO'; if ( registro.distanciaEnderecoEsperado !== null && registro.distanciaEnderecoEsperado !== undefined && @@ -2611,34 +2780,75 @@ const distanciaExcedente = registro.distanciaEnderecoEsperado - registro.raioToleranciaUsado; const distanciaExcedenteKm = (distanciaExcedente / 1000).toFixed(2); const distanciaExcedenteMetros = distanciaExcedente.toFixed(0); + const excedenteTexto = distanciaExcedente >= 1000 + ? `${distanciaExcedenteKm} km além do permitido` + : `${distanciaExcedenteMetros} metros além do permitido`; + geofencingData.push(['Distância Excedente', excedenteTexto]); + } + } + geofencingData.push(['Status', statusTexto]); - if (distanciaExcedente >= 1000) { - doc.text(` ${distanciaExcedenteKm} km além do permitido`, 20, yPosition); - } else { - doc.text(` ${distanciaExcedenteMetros} metros além do permitido`, 20, yPosition); + autoTable(doc, { + startY: yPosition, + head: [['Campo', 'Valor']], + body: geofencingData, + theme: 'striped', + headStyles: { + fillColor: [41, 128, 185], + textColor: [255, 255, 255], + fontStyle: 'bold', + fontSize: 10 + }, + bodyStyles: { + fontSize: 10 + }, + columnStyles: { + 0: { cellWidth: 60, fontStyle: 'bold' }, + 1: { cellWidth: 130 } + }, + margin: { left: 15, right: 15 }, + styles: { cellPadding: 4 }, + didParseCell: (data: any) => { + if (data.section === 'body' && data.column.index === 1) { + const texto = data.cell.text[0]; + if (texto.includes('✓ DENTRO')) { + data.cell.styles.textColor = [0, 128, 0]; + data.cell.styles.fontStyle = 'bold'; + } else if (texto.includes('⚠️ FORA')) { + data.cell.styles.textColor = [200, 0, 0]; + data.cell.styles.fontStyle = 'bold'; + } } } + }); - yPosition += 6; + type JsPDFWithAutoTable9 = jsPDF & { + lastAutoTable?: { finalY: number }; + }; + const finalYGeofencing = (doc as JsPDFWithAutoTable9).lastAutoTable?.finalY ?? yPosition + 10; + yPosition = finalYGeofencing + 5; - doc.setFont('helvetica', 'normal'); - doc.setTextColor(0, 0, 0); + // Observação se fora do raio + if (registro.dentroRaioPermitido === false) { doc.setFontSize(9); + doc.setTextColor(100, 100, 100); const observacaoLines = doc.splitTextToSize( 'O registro foi realizado fora da área permitida de marcação de ponto. Verifique se o funcionário possui autorização para trabalho remoto ou deslocamento.', 170 ); doc.text(observacaoLines, 20, yPosition); - yPosition += observacaoLines.length * 4; + yPosition += observacaoLines.length * 4 + 5; doc.setFontSize(10); - } else { - doc.text(' Não validado', 20, yPosition); + doc.setTextColor(0, 0, 0); } - yPosition += 8; + yPosition += 5; } else { + doc.setFontSize(10); + doc.setTextColor(100, 100, 100); doc.text('Validação de localização permitida não configurada para este registro.', 15, yPosition); yPosition += 8; + doc.setTextColor(0, 0, 0); } } @@ -2649,139 +2859,112 @@ yPosition = 20; } + doc.setFontSize(12); + doc.setTextColor(41, 128, 185); doc.setFont('helvetica', 'bold'); doc.text('DADOS TÉCNICOS', 15, yPosition); - doc.setFont('helvetica', 'normal'); + yPosition += 10; - yPosition += 8; - doc.setFontSize(10); + // Consolidar todos os dados técnicos em uma única tabela + const dadosTecnicosData: any[][] = []; // Informações de Rede if (registro.ipAddress || registro.ipPublico || registro.ipLocal) { - doc.setFont('helvetica', 'bold'); - doc.text('Rede:', 15, yPosition); - doc.setFont('helvetica', 'normal'); - yPosition += 6; - if (registro.ipAddress) { - doc.text(` IP: ${registro.ipAddress}`, 20, yPosition); - yPosition += 6; + dadosTecnicosData.push(['IP', registro.ipAddress]); } - if (registro.ipPublico) { - doc.text(` IP Público: ${registro.ipPublico}`, 20, yPosition); - yPosition += 6; + dadosTecnicosData.push(['IP Público', registro.ipPublico]); } - if (registro.ipLocal) { - doc.text(` IP Local: ${registro.ipLocal}`, 20, yPosition); - yPosition += 6; + dadosTecnicosData.push(['IP Local', registro.ipLocal]); } - - yPosition += 3; } // Informações do Navegador if (registro.browser || registro.userAgent) { - doc.setFont('helvetica', 'bold'); - doc.text('Navegador:', 15, yPosition); - doc.setFont('helvetica', 'normal'); - yPosition += 6; - if (registro.browser) { - doc.text(` Navegador: ${registro.browser}${registro.browserVersion ? ` ${registro.browserVersion}` : ''}`, 20, yPosition); - yPosition += 6; + dadosTecnicosData.push(['Navegador', `${registro.browser}${registro.browserVersion ? ` ${registro.browserVersion}` : ''}`]); } - if (registro.engine) { - doc.text(` Engine: ${registro.engine}`, 20, yPosition); - yPosition += 6; + dadosTecnicosData.push(['Engine', registro.engine]); } - if (registro.userAgent) { - // Quebrar user agent em múltiplas linhas se necessário - const userAgentLines = doc.splitTextToSize(` User Agent: ${registro.userAgent}`, 170); - doc.text(userAgentLines, 20, yPosition); - yPosition += userAgentLines.length * 6; + dadosTecnicosData.push(['User Agent', registro.userAgent]); } - - yPosition += 3; } // Informações do Sistema if (registro.sistemaOperacional || registro.arquitetura) { - doc.setFont('helvetica', 'bold'); - doc.text('Sistema:', 15, yPosition); - doc.setFont('helvetica', 'normal'); - yPosition += 6; - if (registro.sistemaOperacional) { - doc.text(` SO: ${registro.sistemaOperacional}${registro.osVersion ? ` ${registro.osVersion}` : ''}`, 20, yPosition); - yPosition += 6; + dadosTecnicosData.push(['Sistema Operacional', `${registro.sistemaOperacional}${registro.osVersion ? ` ${registro.osVersion}` : ''}`]); } - if (registro.arquitetura) { - doc.text(` Arquitetura: ${registro.arquitetura}`, 20, yPosition); - yPosition += 6; + dadosTecnicosData.push(['Arquitetura', registro.arquitetura]); } - if (registro.plataforma) { - doc.text(` Plataforma: ${registro.plataforma}`, 20, yPosition); - yPosition += 6; + dadosTecnicosData.push(['Plataforma', registro.plataforma]); } - - yPosition += 3; } // Informações do Dispositivo if (registro.deviceType || registro.screenResolution) { - doc.setFont('helvetica', 'bold'); - doc.text('Dispositivo:', 15, yPosition); - doc.setFont('helvetica', 'normal'); - yPosition += 6; - if (registro.deviceType) { - doc.text(` Tipo: ${registro.deviceType}`, 20, yPosition); - yPosition += 6; + dadosTecnicosData.push(['Tipo de Dispositivo', registro.deviceType]); } - if (registro.deviceModel) { - doc.text(` Modelo: ${registro.deviceModel}`, 20, yPosition); - yPosition += 6; + dadosTecnicosData.push(['Modelo', registro.deviceModel]); } - if (registro.screenResolution) { - doc.text(` Resolução: ${registro.screenResolution}`, 20, yPosition); - yPosition += 6; + dadosTecnicosData.push(['Resolução', registro.screenResolution]); } - if (registro.coresTela) { - doc.text(` Cores: ${registro.coresTela}`, 20, yPosition); - yPosition += 6; + dadosTecnicosData.push(['Cores da Tela', registro.coresTela]); } - if (registro.isMobile || registro.isTablet || registro.isDesktop) { const tipoDispositivo = registro.isMobile ? 'Mobile' : registro.isTablet ? 'Tablet' : 'Desktop'; - doc.text(` Categoria: ${tipoDispositivo}`, 20, yPosition); - yPosition += 6; + dadosTecnicosData.push(['Categoria', tipoDispositivo]); } - if (registro.idioma) { - doc.text(` Idioma: ${registro.idioma}`, 20, yPosition); - yPosition += 6; + dadosTecnicosData.push(['Idioma', registro.idioma]); } - if (registro.connectionType) { - doc.text(` Conexão: ${registro.connectionType}`, 20, yPosition); - yPosition += 6; + dadosTecnicosData.push(['Tipo de Conexão', registro.connectionType]); } - if (registro.memoryInfo) { - doc.text(` Memória: ${registro.memoryInfo}`, 20, yPosition); - yPosition += 6; + dadosTecnicosData.push(['Memória', registro.memoryInfo]); } + } - yPosition += 3; + if (dadosTecnicosData.length > 0) { + autoTable(doc, { + startY: yPosition, + head: [['Campo', 'Valor']], + body: dadosTecnicosData, + theme: 'striped', + headStyles: { + fillColor: [60, 60, 60], + textColor: [255, 255, 255], + fontStyle: 'bold', + fontSize: 10 + }, + bodyStyles: { + fontSize: 9, + textColor: [0, 0, 0] + }, + columnStyles: { + 0: { cellWidth: 60, fontStyle: 'bold' }, + 1: { cellWidth: 130 } + }, + margin: { left: 15, right: 15 }, + styles: { cellPadding: 3 } + }); + + type JsPDFWithAutoTable10 = jsPDF & { + lastAutoTable?: { finalY: number }; + }; + const finalYTecnicos = (doc as JsPDFWithAutoTable10).lastAutoTable?.finalY ?? yPosition + 10; + yPosition = finalYTecnicos + 10; } // Imagem capturada (se disponível) @@ -2869,17 +3052,38 @@ } } - // Rodapé + // Rodapé melhorado const pageCount = doc.getNumberOfPages(); for (let i = 1; i <= pageCount; i++) { doc.setPage(i); + + // Linha decorativa no rodapé + doc.setDrawColor(200, 200, 200); + doc.setLineWidth(0.3); + doc.line(15, doc.internal.pageSize.getHeight() - 20, 195, doc.internal.pageSize.getHeight() - 20); + + // Texto do rodapé doc.setFontSize(8); - doc.setTextColor(128, 128, 128); + doc.setTextColor(100, 100, 100); + doc.setFont('helvetica', 'normal'); + const dataGeracao = new Date().toLocaleDateString('pt-BR', { + day: '2-digit', + month: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); 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' } + `SGSE - Sistema de Gerenciamento de Secretaria de Esportes`, + 15, + doc.internal.pageSize.getHeight() - 12, + { align: 'left' } + ); + doc.text( + `Gerado em: ${dataGeracao} | Página ${i} de ${pageCount}`, + 195, + doc.internal.pageSize.getHeight() - 12, + { align: 'right' } ); }