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:
2025-10-27 23:36:04 -03:00
parent 3a1956f83b
commit 81e6eb4a42
26 changed files with 6405 additions and 935 deletions

View File

@@ -0,0 +1,273 @@
<script lang="ts">
import { useConvexClient } from "convex-svelte";
interface Props {
label: string;
helpUrl?: string;
value?: string; // storageId
disabled?: boolean;
onUpload: (file: File) => Promise<void>;
onRemove: () => Promise<void>;
}
let {
label,
helpUrl,
value = $bindable(),
disabled = false,
onUpload,
onRemove,
}: Props = $props();
const client = useConvexClient();
let fileInput: HTMLInputElement;
let uploading = $state(false);
let error = $state<string | null>(null);
let fileName = $state<string>("");
let fileType = $state<string>("");
let previewUrl = $state<string | null>(null);
let fileUrl = $state<string | null>(null);
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
const ALLOWED_TYPES = [
"application/pdf",
"image/jpeg",
"image/jpg",
"image/png",
];
// Buscar URL do arquivo quando houver um storageId
$effect(() => {
if (value && !fileName) {
// Tem storageId mas não é um upload recente
loadExistingFile(value);
}
});
async function loadExistingFile(storageId: string) {
try {
const url = await client.storage.getUrl(storageId as any);
if (url) {
fileUrl = url;
fileName = "Documento anexado";
// Detectar tipo pelo URL ou assumir PDF
if (url.includes(".pdf") || url.includes("application/pdf")) {
fileType = "application/pdf";
} else {
fileType = "image/jpeg";
previewUrl = url; // Para imagens, a URL serve como preview
}
}
} catch (err) {
console.error("Erro ao carregar arquivo existente:", err);
}
}
async function handleFileSelect(event: Event) {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (!file) return;
error = null;
// Validate file size
if (file.size > MAX_FILE_SIZE) {
error = "Arquivo muito grande. Tamanho máximo: 10MB";
target.value = "";
return;
}
// Validate file type
if (!ALLOWED_TYPES.includes(file.type)) {
error = "Tipo de arquivo não permitido. Use PDF ou imagens (JPG, PNG)";
target.value = "";
return;
}
try {
uploading = true;
fileName = file.name;
fileType = file.type;
// Create preview for images
if (file.type.startsWith("image/")) {
const reader = new FileReader();
reader.onload = (e) => {
previewUrl = e.target?.result as string;
};
reader.readAsDataURL(file);
}
await onUpload(file);
} catch (err: any) {
error = err?.message || "Erro ao fazer upload do arquivo";
previewUrl = null;
} finally {
uploading = false;
target.value = "";
}
}
async function handleRemove() {
if (!confirm("Tem certeza que deseja remover este arquivo?")) {
return;
}
try {
uploading = true;
await onRemove();
fileName = "";
fileType = "";
previewUrl = null;
fileUrl = null;
} catch (err: any) {
error = err?.message || "Erro ao remover arquivo";
} finally {
uploading = false;
}
}
function handleView() {
if (fileUrl) {
window.open(fileUrl, '_blank');
}
}
function openFileDialog() {
fileInput?.click();
}
</script>
<div class="form-control w-full">
<label class="label">
<span class="label-text font-medium flex items-center gap-2">
{label}
{#if helpUrl}
<div class="tooltip tooltip-right" data-tip="Clique para acessar o link">
<a
href={helpUrl}
target="_blank"
rel="noopener noreferrer"
class="text-primary hover:text-primary-focus transition-colors"
aria-label="Acessar link"
>
<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="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
</div>
{/if}
</span>
</label>
<input
type="file"
bind:this={fileInput}
onchange={handleFileSelect}
accept=".pdf,.jpg,.jpeg,.png"
class="hidden"
{disabled}
/>
{#if value || fileName}
<div class="flex items-center gap-2 p-3 border border-base-300 rounded-lg bg-base-100">
<!-- Preview -->
<div class="flex-shrink-0">
{#if previewUrl}
<img src={previewUrl} alt="Preview" class="w-12 h-12 object-cover rounded" />
{:else if fileType === "application/pdf" || fileName.endsWith(".pdf")}
<div class="w-12 h-12 bg-error/10 rounded flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-error" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
</div>
{:else}
<div class="w-12 h-12 bg-success/10 rounded flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
</div>
{/if}
</div>
<!-- File info -->
<div class="flex-1 min-w-0">
<p class="text-sm font-medium truncate">{fileName || "Arquivo anexado"}</p>
<p class="text-xs text-base-content/60">
{#if uploading}
Carregando...
{:else}
Enviado com sucesso
{/if}
</p>
</div>
<!-- Actions -->
<div class="flex gap-2">
{#if fileUrl}
<button
type="button"
onclick={handleView}
class="btn btn-sm btn-ghost text-info"
disabled={uploading || disabled}
title="Visualizar arquivo"
>
<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="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
</svg>
</button>
{/if}
<button
type="button"
onclick={openFileDialog}
class="btn btn-sm btn-ghost"
disabled={uploading || disabled}
title="Substituir arquivo"
>
<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="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
</button>
<button
type="button"
onclick={handleRemove}
class="btn btn-sm btn-ghost text-error"
disabled={uploading || disabled}
title="Remover arquivo"
>
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
{:else}
<button
type="button"
onclick={openFileDialog}
class="btn btn-outline btn-block justify-start gap-2"
disabled={uploading || disabled}
>
{#if uploading}
<span class="loading loading-spinner loading-sm"></span>
Carregando...
{: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="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
Selecionar arquivo (PDF ou imagem, máx. 10MB)
{/if}
</button>
{/if}
{#if error}
<label class="label">
<span class="label-text-alt text-error">{error}</span>
</label>
{/if}
</div>

View File

@@ -0,0 +1,162 @@
<script lang="ts">
import { modelosDeclaracoes } from "$lib/utils/modelosDeclaracoes";
import {
gerarDeclaracaoAcumulacaoCargo,
gerarDeclaracaoDependentesIR,
gerarDeclaracaoIdoneidade,
gerarTermoNepotismo,
gerarTermoOpcaoRemuneracao,
downloadBlob
} from "$lib/utils/declaracoesGenerator";
interface Props {
funcionario?: any;
showPreencherButton?: boolean;
}
let { funcionario, showPreencherButton = false }: Props = $props();
let generating = $state(false);
function baixarModelo(arquivoUrl: string, nomeModelo: string) {
const link = document.createElement('a');
link.href = arquivoUrl;
link.download = nomeModelo + '.pdf';
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
async function gerarPreenchido(modeloId: string) {
if (!funcionario) {
alert('Dados do funcionário não disponíveis');
return;
}
try {
generating = true;
let blob: Blob;
let nomeArquivo: string;
switch (modeloId) {
case 'acumulacao_cargo':
blob = await gerarDeclaracaoAcumulacaoCargo(funcionario);
nomeArquivo = `Declaracao_Acumulacao_Cargo_${funcionario.nome.replace(/ /g, '_')}_${Date.now()}.pdf`;
break;
case 'dependentes_ir':
blob = await gerarDeclaracaoDependentesIR(funcionario);
nomeArquivo = `Declaracao_Dependentes_IR_${funcionario.nome.replace(/ /g, '_')}_${Date.now()}.pdf`;
break;
case 'idoneidade':
blob = await gerarDeclaracaoIdoneidade(funcionario);
nomeArquivo = `Declaracao_Idoneidade_${funcionario.nome.replace(/ /g, '_')}_${Date.now()}.pdf`;
break;
case 'nepotismo':
blob = await gerarTermoNepotismo(funcionario);
nomeArquivo = `Termo_Nepotismo_${funcionario.nome.replace(/ /g, '_')}_${Date.now()}.pdf`;
break;
case 'opcao_remuneracao':
blob = await gerarTermoOpcaoRemuneracao(funcionario);
nomeArquivo = `Termo_Opcao_Remuneracao_${funcionario.nome.replace(/ /g, '_')}_${Date.now()}.pdf`;
break;
default:
alert('Modelo não encontrado');
return;
}
downloadBlob(blob, nomeArquivo);
} catch (error) {
console.error('Erro ao gerar declaração:', error);
alert('Erro ao gerar declaração preenchida');
} finally {
generating = false;
}
}
</script>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title text-xl border-b pb-3">
<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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Modelos de Declarações
</h2>
<div class="alert alert-info shadow-sm mb-4">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-5 w-5" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div class="text-sm">
<p class="font-semibold">Baixe os modelos, preencha, assine e faça upload no sistema</p>
<p class="text-xs opacity-80 mt-1">Estes documentos são necessários para completar o cadastro do funcionário</p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{#each modelosDeclaracoes as modelo}
<div class="card bg-base-200 shadow-sm hover:shadow-md transition-shadow">
<div class="card-body p-4">
<div class="flex items-start gap-3">
<!-- Ícone PDF -->
<div class="flex-shrink-0 w-12 h-12 bg-error/10 rounded-lg flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-error" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
</svg>
</div>
<!-- Conteúdo -->
<div class="flex-1 min-w-0">
<h3 class="font-semibold text-sm mb-1 line-clamp-2">{modelo.nome}</h3>
<p class="text-xs text-base-content/70 mb-3 line-clamp-2">{modelo.descricao}</p>
<!-- Ações -->
<div class="flex flex-col gap-2">
<button
type="button"
class="btn btn-primary btn-xs gap-1"
onclick={() => baixarModelo(modelo.arquivo, modelo.nome)}
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Baixar Modelo
</button>
{#if showPreencherButton && modelo.podePreencherAutomaticamente && funcionario}
<button
type="button"
class="btn btn-outline btn-xs gap-1"
onclick={() => gerarPreenchido(modelo.id)}
disabled={generating}
>
{#if generating}
<span class="loading loading-spinner loading-xs"></span>
Gerando...
{:else}
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
Gerar Preenchido
{/if}
</button>
{/if}
</div>
</div>
</div>
</div>
</div>
{/each}
</div>
<div class="mt-4 text-xs text-base-content/60 text-center">
<p>💡 Dica: Após preencher e assinar os documentos, faça upload na seção "Documentação Anexa"</p>
</div>
</div>
</div>

View 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>