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:
@@ -2,6 +2,7 @@
|
||||
import { useQuery } from 'convex-svelte';
|
||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||
import jsPDF from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
import { Printer, X, User, Clock, CheckCircle2, XCircle, Calendar, MapPin } from 'lucide-svelte';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
import { formatarDataHoraCompleta, getTipoRegistroLabel } from '$lib/utils/ponto';
|
||||
@@ -109,15 +110,16 @@
|
||||
const registro = registroQuery.data;
|
||||
const doc = new jsPDF();
|
||||
|
||||
// Logo
|
||||
// Adicionar logo no canto superior esquerdo
|
||||
let yPosition = 20;
|
||||
try {
|
||||
const logoImg = new Image();
|
||||
logoImg.src = logoGovPE;
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
logoImg.onload = () => resolve();
|
||||
logoImg.onerror = () => reject();
|
||||
setTimeout(() => reject(), 3000);
|
||||
const logoImg = await new Promise<HTMLImageElement>((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = (err) => reject(err);
|
||||
setTimeout(() => reject(new Error('Timeout loading logo')), 3000);
|
||||
img.src = logoGovPE;
|
||||
});
|
||||
|
||||
const logoWidth = 25;
|
||||
@@ -125,59 +127,75 @@
|
||||
const logoHeight = logoWidth * aspectRatio;
|
||||
|
||||
doc.addImage(logoImg, 'PNG', 15, 10, logoWidth, logoHeight);
|
||||
yPosition = Math.max(20, 10 + logoHeight / 2);
|
||||
yPosition = 10 + logoHeight + 10;
|
||||
} 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.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' });
|
||||
|
||||
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: string[][] = [];
|
||||
|
||||
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]);
|
||||
}
|
||||
if (registro.funcionario.simbolo) {
|
||||
doc.text(
|
||||
`Símbolo: ${registro.funcionario.simbolo.nome} (${registro.funcionario.simbolo.tipo === 'cargo_comissionado' ? 'Cargo Comissionado' : 'Função Gratificada'})`,
|
||||
15,
|
||||
yPosition
|
||||
);
|
||||
yPosition += 6;
|
||||
const simboloTipo = registro.funcionario.simbolo.tipo === 'cargo_comissionado'
|
||||
? 'Cargo Comissionado'
|
||||
: 'Função Gratificada';
|
||||
funcionarioData.push(['Símbolo', `${registro.funcionario.simbolo.nome} (${simboloTipo})`]);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('DADOS DO REGISTRO', 15, yPosition);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
autoTable(doc, {
|
||||
startY: yPosition,
|
||||
head: [['Campo', 'Informação']],
|
||||
body: funcionarioData,
|
||||
theme: 'striped',
|
||||
headStyles: { fillColor: [102, 126, 234] },
|
||||
styles: { fontSize: 10 },
|
||||
margin: { left: 15, right: 15 }
|
||||
});
|
||||
|
||||
yPosition += 8;
|
||||
doc.setFontSize(10);
|
||||
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 = configQuery?.data;
|
||||
const tipoLabel = config
|
||||
? getTipoRegistroLabel(registro.tipo, {
|
||||
@@ -187,25 +205,38 @@
|
||||
nomeSaida: config.nomeSaida,
|
||||
})
|
||||
: getTipoRegistroLabel(registro.tipo);
|
||||
doc.text(`Tipo: ${tipoLabel}`, 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;
|
||||
|
||||
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);
|
||||
yPosition += 6;
|
||||
doc.setFontSize(12);
|
||||
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);
|
||||
yPosition += 6;
|
||||
autoTable(doc, {
|
||||
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(
|
||||
`Sincronizado: ${registro.sincronizadoComServidor ? 'Sim (Servidor)' : 'Não (PC Local)'}`,
|
||||
15,
|
||||
yPosition
|
||||
);
|
||||
yPosition += 10;
|
||||
type JsPDFWithAutoTable2 = jsPDF & {
|
||||
lastAutoTable?: { finalY: number };
|
||||
};
|
||||
const finalY2 = (doc as JsPDFWithAutoTable2).lastAutoTable?.finalY ?? yPosition + 10;
|
||||
yPosition = finalY2 + 10;
|
||||
|
||||
// Imagem capturada (se disponível)
|
||||
if (registro.imagemUrl) {
|
||||
@@ -216,8 +247,10 @@
|
||||
yPosition = 20;
|
||||
}
|
||||
|
||||
doc.setFontSize(12);
|
||||
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');
|
||||
yPosition += 10;
|
||||
|
||||
|
||||
@@ -2849,7 +2849,7 @@
|
||||
{#await client.query( api.ausencias.obterDetalhes, { solicitacaoId: solicitacaoAusenciaAprovar } ) then detalhes}
|
||||
{#if detalhes}
|
||||
<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
|
||||
solicitacao={detalhes}
|
||||
gestorId={currentUser.data._id}
|
||||
|
||||
Reference in New Issue
Block a user