feat: enhance ComprovantePonto component by adding logo support and restructuring document layout with auto-generated tables for employee and registration data, improving PDF output clarity and presentation

This commit is contained in:
2025-11-30 15:40:58 -03:00
parent 3204440a38
commit e43f9fcf14
2 changed files with 90 additions and 57 deletions

View File

@@ -2,6 +2,7 @@
import { useQuery } from 'convex-svelte'; import { useQuery } from 'convex-svelte';
import { api } from '@sgse-app/backend/convex/_generated/api'; import { api } from '@sgse-app/backend/convex/_generated/api';
import jsPDF from 'jspdf'; import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
import { Printer, X, User, Clock, CheckCircle2, XCircle, Calendar, MapPin } from 'lucide-svelte'; import { Printer, X, User, Clock, CheckCircle2, XCircle, Calendar, MapPin } from 'lucide-svelte';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel'; import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { formatarDataHoraCompleta, getTipoRegistroLabel } from '$lib/utils/ponto'; import { formatarDataHoraCompleta, getTipoRegistroLabel } from '$lib/utils/ponto';
@@ -109,15 +110,16 @@
const registro = registroQuery.data; const registro = registroQuery.data;
const doc = new jsPDF(); const doc = new jsPDF();
// Logo // Adicionar logo no canto superior esquerdo
let yPosition = 20; let yPosition = 20;
try { try {
const logoImg = new Image(); const logoImg = await new Promise<HTMLImageElement>((resolve, reject) => {
logoImg.src = logoGovPE; const img = new Image();
await new Promise<void>((resolve, reject) => { img.crossOrigin = 'anonymous';
logoImg.onload = () => resolve(); img.onload = () => resolve(img);
logoImg.onerror = () => reject(); img.onerror = (err) => reject(err);
setTimeout(() => reject(), 3000); setTimeout(() => reject(new Error('Timeout loading logo')), 3000);
img.src = logoGovPE;
}); });
const logoWidth = 25; const logoWidth = 25;
@@ -125,59 +127,75 @@
const logoHeight = logoWidth * aspectRatio; const logoHeight = logoWidth * aspectRatio;
doc.addImage(logoImg, 'PNG', 15, 10, logoWidth, logoHeight); doc.addImage(logoImg, 'PNG', 15, 10, logoWidth, logoHeight);
yPosition = Math.max(20, 10 + logoHeight / 2); yPosition = 10 + logoHeight + 10;
} catch (err) { } catch (err) {
console.warn('Não foi possível carregar a logo:', err); console.warn('Erro ao carregar logo:', err);
yPosition = 20;
} }
// Cabeçalho // Cabeçalho padrão do sistema (centralizado)
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.setTextColor(0, 0, 0);
doc.text('GOVERNO DO ESTADO DE PERNAMBUCO', 105, Math.max(yPosition - 10, 20), { align: 'center' });
doc.setFontSize(12);
doc.text('SECRETARIA DE ESPORTES', 105, Math.max(yPosition - 2, 28), { align: 'center' });
yPosition = Math.max(yPosition, 40);
yPosition += 10;
// Título do comprovante
doc.setFontSize(16); doc.setFontSize(16);
doc.setTextColor(41, 128, 185); doc.setTextColor(102, 126, 234); // Cor primária padrão do sistema
doc.setFont('helvetica', 'bold');
doc.text('COMPROVANTE DE REGISTRO DE PONTO', 105, yPosition, { align: 'center' }); doc.text('COMPROVANTE DE REGISTRO DE PONTO', 105, yPosition, { align: 'center' });
yPosition += 15; yPosition += 15;
// Informações do Funcionário // Informações do Funcionário em tabela
doc.setFontSize(12); const funcionarioData: string[][] = [];
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) {
if (registro.funcionario.matricula) { if (registro.funcionario.matricula) {
doc.text(`Matrícula: ${registro.funcionario.matricula}`, 15, yPosition); funcionarioData.push(['Matrícula', registro.funcionario.matricula]);
yPosition += 6;
} }
doc.text(`Nome: ${registro.funcionario.nome}`, 15, yPosition); funcionarioData.push(['Nome', registro.funcionario.nome || '-']);
yPosition += 6;
if (registro.funcionario.descricaoCargo) { if (registro.funcionario.descricaoCargo) {
doc.text(`Cargo/Função: ${registro.funcionario.descricaoCargo}`, 15, yPosition); funcionarioData.push(['Cargo/Função', registro.funcionario.descricaoCargo]);
yPosition += 6;
} }
if (registro.funcionario.simbolo) { if (registro.funcionario.simbolo) {
doc.text( const simboloTipo = registro.funcionario.simbolo.tipo === 'cargo_comissionado'
`Símbolo: ${registro.funcionario.simbolo.nome} (${registro.funcionario.simbolo.tipo === 'cargo_comissionado' ? 'Cargo Comissionado' : 'Função Gratificada'})`, ? 'Cargo Comissionado'
15, : 'Função Gratificada';
yPosition funcionarioData.push(['Símbolo', `${registro.funcionario.simbolo.nome} (${simboloTipo})`]);
);
yPosition += 6;
} }
} }
yPosition += 5; if (funcionarioData.length > 0) {
doc.setFontSize(12);
doc.setFont('helvetica', 'bold');
doc.setTextColor(0, 0, 0);
doc.text('DADOS DO FUNCIONÁRIO', 15, yPosition);
yPosition += 8;
// Informações do Registro autoTable(doc, {
doc.setFont('helvetica', 'bold'); startY: yPosition,
doc.text('DADOS DO REGISTRO', 15, yPosition); head: [['Campo', 'Informação']],
doc.setFont('helvetica', 'normal'); body: funcionarioData,
theme: 'striped',
headStyles: { fillColor: [102, 126, 234] },
styles: { fontSize: 10 },
margin: { left: 15, right: 15 }
});
yPosition += 8; type JsPDFWithAutoTable = jsPDF & {
doc.setFontSize(10); lastAutoTable?: { finalY: number };
};
const finalY = (doc as JsPDFWithAutoTable).lastAutoTable?.finalY ?? yPosition + 10;
yPosition = finalY + 10;
}
// Informações do Registro em tabela
const config = configQuery?.data; const config = configQuery?.data;
const tipoLabel = config const tipoLabel = config
? getTipoRegistroLabel(registro.tipo, { ? getTipoRegistroLabel(registro.tipo, {
@@ -187,25 +205,38 @@
nomeSaida: config.nomeSaida, nomeSaida: config.nomeSaida,
}) })
: getTipoRegistroLabel(registro.tipo); : getTipoRegistroLabel(registro.tipo);
doc.text(`Tipo: ${tipoLabel}`, 15, yPosition);
yPosition += 6;
const dataHora = formatarDataHoraCompleta(registro.data, registro.hora, registro.minuto, registro.segundo); const dataHora = formatarDataHoraCompleta(registro.data, registro.hora, registro.minuto, registro.segundo);
doc.text(`Data e Hora: ${dataHora}`, 15, yPosition);
yPosition += 6; const registroData: string[][] = [
['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)']
];
doc.text(`Status: ${registro.dentroDoPrazo ? 'Dentro do Prazo' : 'Fora do Prazo'}`, 15, yPosition); doc.setFontSize(12);
yPosition += 6; doc.setFont('helvetica', 'bold');
doc.setTextColor(0, 0, 0);
doc.text('DADOS DO REGISTRO', 15, yPosition);
yPosition += 8;
doc.text(`Tolerância: ${registro.toleranciaMinutos} minutos`, 15, yPosition); autoTable(doc, {
yPosition += 6; startY: yPosition,
head: [['Campo', 'Informação']],
body: registroData,
theme: 'striped',
headStyles: { fillColor: [102, 126, 234] },
styles: { fontSize: 10 },
margin: { left: 15, right: 15 }
});
doc.text( type JsPDFWithAutoTable2 = jsPDF & {
`Sincronizado: ${registro.sincronizadoComServidor ? 'Sim (Servidor)' : 'Não (PC Local)'}`, lastAutoTable?: { finalY: number };
15, };
yPosition const finalY2 = (doc as JsPDFWithAutoTable2).lastAutoTable?.finalY ?? yPosition + 10;
); yPosition = finalY2 + 10;
yPosition += 10;
// Imagem capturada (se disponível) // Imagem capturada (se disponível)
if (registro.imagemUrl) { if (registro.imagemUrl) {
@@ -216,8 +247,10 @@
yPosition = 20; yPosition = 20;
} }
doc.setFontSize(12);
doc.setFont('helvetica', 'bold'); doc.setFont('helvetica', 'bold');
doc.text('FOTO CAPTURADA', 105, yPosition, { align: 'center' }); doc.setTextColor(0, 0, 0);
doc.text('FOTO CAPTURADA', 15, yPosition);
doc.setFont('helvetica', 'normal'); doc.setFont('helvetica', 'normal');
yPosition += 10; yPosition += 10;

View File

@@ -2849,7 +2849,7 @@
{#await client.query( api.ausencias.obterDetalhes, { solicitacaoId: solicitacaoAusenciaAprovar } ) then detalhes} {#await client.query( api.ausencias.obterDetalhes, { solicitacaoId: solicitacaoAusenciaAprovar } ) then detalhes}
{#if detalhes} {#if detalhes}
<dialog class="modal modal-open"> <dialog class="modal modal-open">
<div class="modal-box max-w-4xl"> <div class="modal-box max-h-[90vh] max-w-3xl overflow-y-auto">
<AprovarAusencias <AprovarAusencias
solicitacao={detalhes} solicitacao={detalhes}
gestorId={currentUser.data._id} gestorId={currentUser.data._id}