feat: integrate jsPDF and jsPDF-autotable for document generation; enhance employee management with print functionality and improved data handling in employee forms
This commit is contained in:
463
apps/web/src/lib/components/PrintModal.svelte
Normal file
463
apps/web/src/lib/components/PrintModal.svelte
Normal file
@@ -0,0 +1,463 @@
|
||||
<script lang="ts">
|
||||
import jsPDF from 'jspdf';
|
||||
import autoTable from 'jspdf-autotable';
|
||||
import { maskCPF, maskCEP, maskPhone } from "$lib/utils/masks";
|
||||
import {
|
||||
SEXO_OPTIONS, ESTADO_CIVIL_OPTIONS, GRAU_INSTRUCAO_OPTIONS,
|
||||
GRUPO_SANGUINEO_OPTIONS, FATOR_RH_OPTIONS, APOSENTADO_OPTIONS
|
||||
} from "$lib/utils/constants";
|
||||
import logoGovPE from "$lib/assets/logo_governo_PE.png";
|
||||
|
||||
interface Props {
|
||||
funcionario: any;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let { funcionario, onClose }: Props = $props();
|
||||
|
||||
let modalRef: HTMLDialogElement;
|
||||
let generating = $state(false);
|
||||
|
||||
// Seções selecionáveis
|
||||
let sections = $state({
|
||||
dadosPessoais: true,
|
||||
filiacao: true,
|
||||
naturalidade: true,
|
||||
documentos: true,
|
||||
formacao: true,
|
||||
saude: true,
|
||||
endereco: true,
|
||||
contato: true,
|
||||
cargo: true,
|
||||
bancario: true,
|
||||
});
|
||||
|
||||
function getLabelFromOptions(value: string | undefined, options: Array<{value: string, label: string}>): string {
|
||||
if (!value) return "-";
|
||||
return options.find(opt => opt.value === value)?.label || value;
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
Object.keys(sections).forEach(key => {
|
||||
sections[key as keyof typeof sections] = true;
|
||||
});
|
||||
}
|
||||
|
||||
function deselectAll() {
|
||||
Object.keys(sections).forEach(key => {
|
||||
sections[key as keyof typeof sections] = false;
|
||||
});
|
||||
}
|
||||
|
||||
async function gerarPDF() {
|
||||
try {
|
||||
generating = true;
|
||||
|
||||
const doc = new jsPDF();
|
||||
|
||||
// Logo no canto superior esquerdo (proporcional)
|
||||
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); // timeout após 3s
|
||||
});
|
||||
|
||||
// Logo proporcional: largura 25mm, altura ajustada automaticamente
|
||||
const logoWidth = 25;
|
||||
const aspectRatio = logoImg.height / logoImg.width;
|
||||
const logoHeight = logoWidth * aspectRatio;
|
||||
|
||||
doc.addImage(logoImg, 'PNG', 15, 10, logoWidth, logoHeight);
|
||||
|
||||
// Ajustar posição inicial do texto para ficar ao lado da logo
|
||||
yPosition = Math.max(20, 10 + logoHeight / 2);
|
||||
} catch (err) {
|
||||
console.warn('Não foi possível carregar a logo:', err);
|
||||
}
|
||||
|
||||
// Cabeçalho (alinhado com a logo)
|
||||
doc.setFontSize(16);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('Secretaria de Esportes', 50, yPosition);
|
||||
doc.setFontSize(12);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text('Governo de Pernambuco', 50, yPosition + 7);
|
||||
|
||||
yPosition = Math.max(45, yPosition + 25);
|
||||
|
||||
// Título da ficha
|
||||
doc.setFontSize(18);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('FICHA CADASTRAL DE FUNCIONÁRIO', 105, yPosition, { align: 'center' });
|
||||
|
||||
yPosition += 8;
|
||||
doc.setFontSize(10);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(`Gerado em: ${new Date().toLocaleString('pt-BR')}`, 105, yPosition, { align: 'center' });
|
||||
|
||||
yPosition += 12;
|
||||
|
||||
// Dados Pessoais
|
||||
if (sections.dadosPessoais) {
|
||||
const dadosPessoais: any[] = [
|
||||
['Nome', funcionario.nome],
|
||||
['Matrícula', funcionario.matricula],
|
||||
['CPF', maskCPF(funcionario.cpf)],
|
||||
['RG', funcionario.rg],
|
||||
['Data Nascimento', funcionario.nascimento],
|
||||
];
|
||||
|
||||
if (funcionario.rgOrgaoExpedidor) dadosPessoais.push(['Órgão Expedidor RG', funcionario.rgOrgaoExpedidor]);
|
||||
if (funcionario.rgDataEmissao) dadosPessoais.push(['Data Emissão RG', funcionario.rgDataEmissao]);
|
||||
if (funcionario.sexo) dadosPessoais.push(['Sexo', getLabelFromOptions(funcionario.sexo, SEXO_OPTIONS)]);
|
||||
if (funcionario.estadoCivil) dadosPessoais.push(['Estado Civil', getLabelFromOptions(funcionario.estadoCivil, ESTADO_CIVIL_OPTIONS)]);
|
||||
if (funcionario.nacionalidade) dadosPessoais.push(['Nacionalidade', funcionario.nacionalidade]);
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPosition,
|
||||
head: [['DADOS PESSOAIS', '']],
|
||||
body: dadosPessoais,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||
styles: { fontSize: 9 },
|
||||
});
|
||||
|
||||
yPosition = (doc as any).lastAutoTable.finalY + 10;
|
||||
}
|
||||
|
||||
// Filiação
|
||||
if (sections.filiacao && (funcionario.nomePai || funcionario.nomeMae)) {
|
||||
const filiacao: any[] = [];
|
||||
if (funcionario.nomePai) filiacao.push(['Nome do Pai', funcionario.nomePai]);
|
||||
if (funcionario.nomeMae) filiacao.push(['Nome da Mãe', funcionario.nomeMae]);
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPosition,
|
||||
head: [['FILIAÇÃO', '']],
|
||||
body: filiacao,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||
styles: { fontSize: 9 },
|
||||
});
|
||||
|
||||
yPosition = (doc as any).lastAutoTable.finalY + 10;
|
||||
}
|
||||
|
||||
// Naturalidade
|
||||
if (sections.naturalidade && (funcionario.naturalidade || funcionario.naturalidadeUF)) {
|
||||
const naturalidade: any[] = [];
|
||||
if (funcionario.naturalidade) naturalidade.push(['Cidade', funcionario.naturalidade]);
|
||||
if (funcionario.naturalidadeUF) naturalidade.push(['UF', funcionario.naturalidadeUF]);
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPosition,
|
||||
head: [['NATURALIDADE', '']],
|
||||
body: naturalidade,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||
styles: { fontSize: 9 },
|
||||
});
|
||||
|
||||
yPosition = (doc as any).lastAutoTable.finalY + 10;
|
||||
}
|
||||
|
||||
// Documentos
|
||||
if (sections.documentos) {
|
||||
const documentosData: any[] = [];
|
||||
|
||||
if (funcionario.carteiraProfissionalNumero) {
|
||||
documentosData.push(['Cart. Profissional', `Nº ${funcionario.carteiraProfissionalNumero}${funcionario.carteiraProfissionalSerie ? ' - Série: ' + funcionario.carteiraProfissionalSerie : ''}`]);
|
||||
}
|
||||
if (funcionario.reservistaNumero) {
|
||||
documentosData.push(['Reservista', `Nº ${funcionario.reservistaNumero}${funcionario.reservistaSerie ? ' - Série: ' + funcionario.reservistaSerie : ''}`]);
|
||||
}
|
||||
if (funcionario.tituloEleitorNumero) {
|
||||
let titulo = `Nº ${funcionario.tituloEleitorNumero}`;
|
||||
if (funcionario.tituloEleitorZona) titulo += ` - Zona: ${funcionario.tituloEleitorZona}`;
|
||||
if (funcionario.tituloEleitorSecao) titulo += ` - Seção: ${funcionario.tituloEleitorSecao}`;
|
||||
documentosData.push(['Título Eleitor', titulo]);
|
||||
}
|
||||
if (funcionario.pisNumero) documentosData.push(['PIS/PASEP', funcionario.pisNumero]);
|
||||
|
||||
if (documentosData.length > 0) {
|
||||
autoTable(doc, {
|
||||
startY: yPosition,
|
||||
head: [['DOCUMENTOS', '']],
|
||||
body: documentosData,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||
styles: { fontSize: 9 },
|
||||
});
|
||||
|
||||
yPosition = (doc as any).lastAutoTable.finalY + 10;
|
||||
}
|
||||
}
|
||||
|
||||
// Formação
|
||||
if (sections.formacao && (funcionario.grauInstrucao || funcionario.formacao)) {
|
||||
const formacaoData: any[] = [];
|
||||
if (funcionario.grauInstrucao) formacaoData.push(['Grau Instrução', getLabelFromOptions(funcionario.grauInstrucao, GRAU_INSTRUCAO_OPTIONS)]);
|
||||
if (funcionario.formacao) formacaoData.push(['Formação', funcionario.formacao]);
|
||||
if (funcionario.formacaoRegistro) formacaoData.push(['Registro Nº', funcionario.formacaoRegistro]);
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPosition,
|
||||
head: [['FORMAÇÃO', '']],
|
||||
body: formacaoData,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||
styles: { fontSize: 9 },
|
||||
});
|
||||
|
||||
yPosition = (doc as any).lastAutoTable.finalY + 10;
|
||||
}
|
||||
|
||||
// Saúde
|
||||
if (sections.saude && (funcionario.grupoSanguineo || funcionario.fatorRH)) {
|
||||
const saudeData: any[] = [];
|
||||
if (funcionario.grupoSanguineo) saudeData.push(['Grupo Sanguíneo', funcionario.grupoSanguineo]);
|
||||
if (funcionario.fatorRH) saudeData.push(['Fator RH', getLabelFromOptions(funcionario.fatorRH, FATOR_RH_OPTIONS)]);
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPosition,
|
||||
head: [['SAÚDE', '']],
|
||||
body: saudeData,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||
styles: { fontSize: 9 },
|
||||
});
|
||||
|
||||
yPosition = (doc as any).lastAutoTable.finalY + 10;
|
||||
}
|
||||
|
||||
// Endereço
|
||||
if (sections.endereco) {
|
||||
const enderecoData: any[] = [
|
||||
['Endereço', funcionario.endereco],
|
||||
['Cidade', funcionario.cidade],
|
||||
['UF', funcionario.uf],
|
||||
['CEP', maskCEP(funcionario.cep)],
|
||||
];
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPosition,
|
||||
head: [['ENDEREÇO', '']],
|
||||
body: enderecoData,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||
styles: { fontSize: 9 },
|
||||
});
|
||||
|
||||
yPosition = (doc as any).lastAutoTable.finalY + 10;
|
||||
}
|
||||
|
||||
// Contato
|
||||
if (sections.contato) {
|
||||
const contatoData: any[] = [
|
||||
['E-mail', funcionario.email],
|
||||
['Telefone', maskPhone(funcionario.telefone)],
|
||||
];
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPosition,
|
||||
head: [['CONTATO', '']],
|
||||
body: contatoData,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||
styles: { fontSize: 9 },
|
||||
});
|
||||
|
||||
yPosition = (doc as any).lastAutoTable.finalY + 10;
|
||||
}
|
||||
|
||||
// Nova página para cargo
|
||||
if (yPosition > 200) {
|
||||
doc.addPage();
|
||||
yPosition = 20;
|
||||
}
|
||||
|
||||
// Cargo e Vínculo
|
||||
if (sections.cargo) {
|
||||
const cargoData: any[] = [
|
||||
['Tipo', funcionario.simboloTipo === 'cargo_comissionado' ? 'Cargo Comissionado' : 'Função Gratificada'],
|
||||
];
|
||||
|
||||
if (funcionario.simbolo) {
|
||||
cargoData.push(['Símbolo', funcionario.simbolo.nome]);
|
||||
}
|
||||
if (funcionario.descricaoCargo) cargoData.push(['Descrição', funcionario.descricaoCargo]);
|
||||
if (funcionario.admissaoData) cargoData.push(['Data Admissão', funcionario.admissaoData]);
|
||||
if (funcionario.nomeacaoPortaria) cargoData.push(['Portaria', funcionario.nomeacaoPortaria]);
|
||||
if (funcionario.nomeacaoData) cargoData.push(['Data Nomeação', funcionario.nomeacaoData]);
|
||||
if (funcionario.nomeacaoDOE) cargoData.push(['DOE', funcionario.nomeacaoDOE]);
|
||||
if (funcionario.pertenceOrgaoPublico) {
|
||||
cargoData.push(['Pertence Órgão Público', 'Sim']);
|
||||
if (funcionario.orgaoOrigem) cargoData.push(['Órgão Origem', funcionario.orgaoOrigem]);
|
||||
}
|
||||
if (funcionario.aposentado && funcionario.aposentado !== 'nao') {
|
||||
cargoData.push(['Aposentado', getLabelFromOptions(funcionario.aposentado, APOSENTADO_OPTIONS)]);
|
||||
}
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPosition,
|
||||
head: [['CARGO E VÍNCULO', '']],
|
||||
body: cargoData,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||
styles: { fontSize: 9 },
|
||||
});
|
||||
|
||||
yPosition = (doc as any).lastAutoTable.finalY + 10;
|
||||
}
|
||||
|
||||
// Dados Bancários
|
||||
if (sections.bancario && funcionario.contaBradescoNumero) {
|
||||
const bancarioData: any[] = [
|
||||
['Conta', `${funcionario.contaBradescoNumero}${funcionario.contaBradescoDV ? '-' + funcionario.contaBradescoDV : ''}`],
|
||||
];
|
||||
if (funcionario.contaBradescoAgencia) bancarioData.push(['Agência', funcionario.contaBradescoAgencia]);
|
||||
|
||||
autoTable(doc, {
|
||||
startY: yPosition,
|
||||
head: [['DADOS BANCÁRIOS - BRADESCO', '']],
|
||||
body: bancarioData,
|
||||
theme: 'grid',
|
||||
headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold' },
|
||||
styles: { fontSize: 9 },
|
||||
});
|
||||
|
||||
yPosition = (doc as any).lastAutoTable.finalY + 10;
|
||||
}
|
||||
|
||||
// Adicionar rodapé em todas as páginas
|
||||
const pageCount = (doc as any).internal.getNumberOfPages();
|
||||
for (let i = 1; i <= pageCount; i++) {
|
||||
doc.setPage(i);
|
||||
doc.setFontSize(9);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setTextColor(128, 128, 128);
|
||||
doc.text('SGSE - Sistema de Gerenciamento da Secretaria de Esportes', 105, 285, { align: 'center' });
|
||||
doc.text(`Página ${i} de ${pageCount}`, 195, 285, { align: 'right' });
|
||||
}
|
||||
|
||||
// Salvar PDF
|
||||
doc.save(`Ficha_${funcionario.nome.replace(/ /g, '_')}_${new Date().getTime()}.pdf`);
|
||||
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Erro ao gerar PDF:', error);
|
||||
alert('Erro ao gerar PDF. Verifique o console para mais detalhes.');
|
||||
} finally {
|
||||
generating = false;
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (modalRef) {
|
||||
modalRef.showModal();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<dialog bind:this={modalRef} class="modal">
|
||||
<div class="modal-box max-w-4xl">
|
||||
<h3 class="font-bold text-2xl mb-4">Imprimir Ficha Cadastral</h3>
|
||||
<p class="text-sm text-base-content/70 mb-6">Selecione as seções que deseja incluir no PDF</p>
|
||||
|
||||
<!-- Botões de seleção -->
|
||||
<div class="flex gap-2 mb-6">
|
||||
<button type="button" class="btn btn-sm btn-outline" onclick={selectAll}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Selecionar Todos
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline" onclick={deselectAll}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
Desmarcar Todos
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Grid de checkboxes -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 mb-6 max-h-96 overflow-y-auto p-2 border rounded-lg bg-base-200">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.dadosPessoais} />
|
||||
<span class="label-text">Dados Pessoais</span>
|
||||
</label>
|
||||
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.filiacao} />
|
||||
<span class="label-text">Filiação</span>
|
||||
</label>
|
||||
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.naturalidade} />
|
||||
<span class="label-text">Naturalidade</span>
|
||||
</label>
|
||||
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.documentos} />
|
||||
<span class="label-text">Documentos</span>
|
||||
</label>
|
||||
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.formacao} />
|
||||
<span class="label-text">Formação</span>
|
||||
</label>
|
||||
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.saude} />
|
||||
<span class="label-text">Saúde</span>
|
||||
</label>
|
||||
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.endereco} />
|
||||
<span class="label-text">Endereço</span>
|
||||
</label>
|
||||
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.contato} />
|
||||
<span class="label-text">Contato</span>
|
||||
</label>
|
||||
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.cargo} />
|
||||
<span class="label-text">Cargo e Vínculo</span>
|
||||
</label>
|
||||
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={sections.bancario} />
|
||||
<span class="label-text">Dados Bancários</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Ações -->
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn btn-ghost" onclick={onClose} disabled={generating}>
|
||||
Cancelar
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary gap-2" onclick={gerarPDF} disabled={generating}>
|
||||
{#if generating}
|
||||
<span class="loading loading-spinner loading-sm"></span>
|
||||
Gerando PDF...
|
||||
{:else}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
|
||||
</svg>
|
||||
Gerar PDF
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button type="button" onclick={onClose}>fechar</button>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
Reference in New Issue
Block a user