From b660d123d4710c05664c94f06da3c81c168a3fc0 Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Wed, 19 Nov 2025 05:09:54 -0300 Subject: [PATCH] feat: add PDF receipt generation for point registration - Implemented a new feature to generate a PDF receipt for point registrations, including employee and registration details. - Integrated jsPDF for PDF creation and added functionality to include a logo and captured images in the receipt. - Enhanced the UI with a print button for users to easily access the receipt generation feature. - Improved the confirmation modal layout for better user experience during point registration. --- .../lib/components/ponto/RegistroPonto.svelte | 398 ++++++++++++++++-- 1 file changed, 358 insertions(+), 40 deletions(-) diff --git a/apps/web/src/lib/components/ponto/RegistroPonto.svelte b/apps/web/src/lib/components/ponto/RegistroPonto.svelte index 6c1000d..ad0f920 100644 --- a/apps/web/src/lib/components/ponto/RegistroPonto.svelte +++ b/apps/web/src/lib/components/ponto/RegistroPonto.svelte @@ -13,8 +13,11 @@ getTipoRegistroLabel, getProximoTipoRegistro } from '$lib/utils/ponto'; - import { LogIn, LogOut, Clock, CheckCircle2, XCircle, TrendingUp, TrendingDown } from 'lucide-svelte'; + import { LogIn, LogOut, Clock, CheckCircle2, XCircle, TrendingUp, TrendingDown, Printer } from 'lucide-svelte'; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; + import jsPDF from 'jspdf'; + import logoGovPE from '$lib/assets/logo_governo_PE.png'; + import { formatarDataHoraCompleta } from '$lib/utils/ponto'; const client = useConvexClient(); @@ -309,6 +312,206 @@ erro = null; } + async function imprimirComprovante(registroId: Id<'registrosPonto'>) { + try { + // Buscar dados completos do registro + const registro = await client.query(api.pontos.obterRegistro, { registroId }); + + if (!registro) { + alert('Registro não encontrado'); + return; + } + + 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 = 25; + 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); + } catch (err) { + console.warn('Não foi possível carregar a logo:', err); + } + + // Cabeçalho + doc.setFontSize(16); + doc.setTextColor(41, 128, 185); + doc.text('COMPROVANTE DE REGISTRO DE PONTO', 105, yPosition, { align: 'center' }); + + 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); + + if (registro.funcionario) { + if (registro.funcionario.matricula) { + doc.text(`Matrícula: ${registro.funcionario.matricula}`, 15, yPosition); + yPosition += 6; + } + doc.text(`Nome: ${registro.funcionario.nome}`, 15, yPosition); + yPosition += 6; + if (registro.funcionario.descricaoCargo) { + doc.text(`Cargo/Função: ${registro.funcionario.descricaoCargo}`, 15, yPosition); + yPosition += 6; + } + } + + yPosition += 5; + + // Informações do Registro + doc.setFont('helvetica', 'bold'); + doc.text('DADOS DO REGISTRO', 15, yPosition); + doc.setFont('helvetica', 'normal'); + + yPosition += 8; + doc.setFontSize(10); + + doc.text(`Tipo: ${getTipoRegistroLabel(registro.tipo)}`, 15, yPosition); + yPosition += 6; + + const dataHora = formatarDataHoraCompleta(registro.data, registro.hora, registro.minuto, registro.segundo); + 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 += 10; + + // Imagem capturada (se disponível) + if (registro.imagemUrl) { + // Verificar se precisa de nova página + if (yPosition > 200) { + doc.addPage(); + yPosition = 20; + } + + doc.setFont('helvetica', 'bold'); + doc.text('FOTO CAPTURADA', 105, yPosition, { align: 'center' }); + doc.setFont('helvetica', 'normal'); + yPosition += 10; + + try { + // Carregar imagem usando fetch para evitar problemas de CORS + const response = await fetch(registro.imagemUrl); + if (!response.ok) { + throw new Error('Erro ao carregar imagem'); + } + + const blob = await response.blob(); + const reader = new FileReader(); + + // Converter blob para base64 + const base64 = await new Promise((resolve, reject) => { + reader.onloadend = () => { + if (typeof reader.result === 'string') { + resolve(reader.result); + } else { + reject(new Error('Erro ao converter imagem')); + } + }; + reader.onerror = () => reject(new Error('Erro ao ler imagem')); + reader.readAsDataURL(blob); + }); + + // Criar elemento de imagem para obter dimensões + const img = new Image(); + await new Promise((resolve, reject) => { + img.onload = () => resolve(); + img.onerror = () => reject(new Error('Erro ao processar imagem')); + img.src = base64; + setTimeout(() => reject(new Error('Timeout ao processar imagem')), 10000); + }); + + // Calcular dimensões para caber na página (largura máxima 80mm, manter proporção) + const maxWidth = 80; + const maxHeight = 60; + let imgWidth = img.width; + let imgHeight = img.height; + const aspectRatio = imgWidth / imgHeight; + + if (imgWidth > maxWidth || imgHeight > maxHeight) { + if (aspectRatio > 1) { + // Imagem horizontal + imgWidth = maxWidth; + imgHeight = maxWidth / aspectRatio; + } else { + // Imagem vertical + imgHeight = maxHeight; + imgWidth = maxHeight * aspectRatio; + } + } + + // Centralizar imagem + const xPosition = (doc.internal.pageSize.getWidth() - imgWidth) / 2; + + // Verificar se cabe na página atual + if (yPosition + imgHeight > doc.internal.pageSize.getHeight() - 20) { + doc.addPage(); + yPosition = 20; + } + + // Adicionar imagem ao PDF usando base64 + doc.addImage(base64, 'JPEG', xPosition, yPosition, imgWidth, imgHeight); + yPosition += imgHeight + 10; + } catch (error) { + console.warn('Erro ao adicionar imagem ao PDF:', error); + doc.setFontSize(10); + doc.text('Foto não disponível para impressão', 105, yPosition, { align: 'center' }); + yPosition += 6; + } + } + + // 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' } + ); + } + + // Salvar + const nomeArquivo = `comprovante-ponto-${registro.data}-${registro.hora.toString().padStart(2, '0')}${registro.minuto.toString().padStart(2, '0')}.pdf`; + doc.save(nomeArquivo); + } catch (error) { + console.error('Erro ao gerar comprovante PDF:', error); + alert('Erro ao gerar comprovante PDF. Tente novamente.'); + } + } + const podeRegistrar = $derived.by(() => { return !registrando && !coletandoInfo && config !== undefined; }); @@ -555,7 +758,7 @@ {#each registrosOrdenados as registro (registro._id)}
-
+
{getTipoRegistroLabel(registro.tipo)} @@ -575,6 +778,16 @@
{/if}
+
+ +
@@ -621,53 +834,158 @@ {#if mostrandoModalConfirmacao && imagemCapturada && dataHoraAtual} -