Files
advx/src/server/services/PdfService.ts
T
rodrigo 6c644cb440 fix atendimentos.ts
Signed-off-by: Rodrigo Martins <rodrigo.sousa.577@gmail.com>
2025-04-13 17:40:06 -03:00

497 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import puppeteer from 'puppeteer';
import { prisma } from '../prismaClient';
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
import fs from 'fs/promises';
import path from 'path';
/**
* Serviço para geração e manipulação de arquivos PDF
*/
export class PdfService {
/**
* Cria um documento PDF básico
* @returns Documento PDF
*/
async criarDocumento() {
return await PDFDocument.create();
}
/**
* Carrega um documento PDF existente
* @param pdfBytes - Bytes do PDF
* @returns Documento PDF
*/
async carregarDocumento(pdfBytes: Uint8Array) {
return await PDFDocument.load(pdfBytes);
}
/**
* Adiciona uma página ao documento
* @param doc - Documento PDF
* @param tamanho - Tamanho da página (padrão: A4)
* @returns Página adicionada
*/
adicionarPagina(doc: any, tamanho = [595.28, 841.89]) {
return doc.addPage(tamanho);
}
/**
* Salva o documento como buffer
* @param doc - Documento PDF
* @returns Buffer do PDF
*/
async salvarComoBuffer(doc: any): Promise<Buffer> {
const pdfBytes = await doc.save();
return Buffer.from(pdfBytes);
}
/**
* Carrega uma fonte padrão
* @param doc - Documento PDF
* @param fonte - Nome da fonte padrão
* @returns Fonte carregada
*/
async carregarFonte(doc: any, fonte: StandardFonts) {
return await doc.embedFont(fonte);
}
/**
* Desenha texto em uma página
* @param pagina - Página do PDF
* @param texto - Texto a ser desenhado
* @param x - Posição X
* @param y - Posição Y
* @param fonte - Fonte a ser usada
* @param tamanho - Tamanho da fonte
* @param cor - Cor do texto (padrão: preto)
*/
desenharTexto(
pagina: any,
texto: string,
x: number,
y: number,
fonte: any,
tamanho: number,
cor = rgb(0, 0, 0)
) {
pagina.drawText(texto, {
x,
y,
font: fonte,
size: tamanho,
color: cor,
});
}
/**
* Cria um cabeçalho para o documento
* @param pagina - Página do PDF
* @param titulo - Título do cabeçalho
* @param fonte - Fonte a ser usada
* @param margemSuperior - Margem superior
* @param margem - Margem lateral
* @returns Posição Y após o cabeçalho
*/
criarCabecalho(
pagina: any,
titulo: string,
fonte: any,
margemSuperior: number,
margem: number
): number {
const yPos = pagina.getHeight() - margemSuperior;
this.desenharTexto(
pagina,
titulo,
margem,
yPos,
fonte,
18,
rgb(0, 0, 0.7)
);
// Linha separadora
pagina.drawLine({
start: { x: margem, y: yPos - 10 },
end: { x: pagina.getWidth() - margem, y: yPos - 10 },
thickness: 1,
color: rgb(0, 0, 0.5),
});
return yPos - 30; // Retorna a posição Y após o cabeçalho
}
/**
* Cria um rodapé para o documento
* @param pagina - Página do PDF
* @param texto - Texto do rodapé
* @param fonte - Fonte a ser usada
* @param margem - Margem
*/
criarRodape(pagina: any, texto: string, fonte: any, margem: number) {
const yPos = margem / 2;
this.desenharTexto(
pagina,
texto,
margem,
yPos,
fonte,
8,
rgb(0.5, 0.5, 0.5)
);
}
private async getConfiguracao() {
try {
// Buscar configuração ou usar valores padrão
const config = await prisma.configuracaoDocumento.findFirst({
orderBy: { criadoEm: 'desc' },
select: {
id: true,
timbradoUrl: true,
timbradoMarginTop: true,
timbradoMarginBottom: true,
timbradoMarginLeft: true,
timbradoMarginRight: true,
templatePadrao: true,
margensTemplate: true,
criadoEm: true
}
});
console.log('Configuração encontrada:', config); // Debug
// Retornar configuração padrão se não existir
return {
// Valores padrão
cabecalhoHtml: `
<div style="text-align: center; padding: 20px;">
<h2 style="margin:0;font-size:14pt">{{escritorio.nome}}</h2>
</div>
`,
rodapeHtml: `
<div style="text-align: center; font-size: 10pt;">
{{escritorio.endereco}}<br>
Tel: {{escritorio.telefone}} | Email: {{escritorio.email}}
</div>
`,
// Campos que existem no modelo
timbradoUrl: config?.timbradoUrl || '',
timbradoMarginTop: config?.timbradoMarginTop || 100,
timbradoMarginBottom: config?.timbradoMarginBottom || 50,
timbradoMarginLeft: config?.timbradoMarginLeft || 50,
timbradoMarginRight: config?.timbradoMarginRight || 50,
templatePadrao: config?.templatePadrao || null,
margensTemplate: config?.margensTemplate || null
};
} catch (error) {
console.error('Erro ao buscar configuração:', error);
// Retornar configuração padrão para não interromper o fluxo
return {
cabecalhoHtml: `<div style="text-align: center; padding: 20px;"><h2 style="margin:0;font-size:14pt">Documento</h2></div>`,
rodapeHtml: `<div style="text-align: center; font-size: 10pt;">Documento gerado pelo sistema</div>`,
timbradoUrl: '',
timbradoMarginTop: 100,
timbradoMarginBottom: 50,
timbradoMarginLeft: 50,
timbradoMarginRight: 50,
templatePadrao: null,
margensTemplate: null
};
}
}
/**
* Gera um PDF a partir do conteúdo HTML e dados do escritório
* @param conteudo - Conteúdo HTML para o PDF
* @param escritorio - Dados do escritório para substituir no template
* @returns Buffer contendo o PDF gerado
*/
async gerarPDF(conteudo: string, escritorio: any = {}): Promise<Buffer> {
try {
console.log('Buscando configurações do documento...');
const config = await this.getConfiguracao();
// Verificar se temos um timbrado para usar
const temTimbrado = !!config.timbradoUrl;
console.log(`Iniciando geração de PDF ${temTimbrado ? 'com' : 'sem'} timbrado...`);
// Se temos timbrado, vamos primeiro verificar se ele existe
let timbradoPath = '';
let timbradoExists = false;
if (temTimbrado) {
if (config.timbradoUrl.startsWith('/uploads')) {
timbradoPath = path.join(process.cwd(), 'public', config.timbradoUrl);
} else {
timbradoPath = path.join(process.cwd(), 'public', 'uploads', path.basename(config.timbradoUrl));
}
console.log('Verificando timbrado em:', timbradoPath);
try {
await fs.access(timbradoPath);
timbradoExists = true;
console.log('Timbrado encontrado');
} catch (e) {
console.error('Timbrado não encontrado:', e);
}
}
// Margens definidas no template ou padrão
const margens = config.margensTemplate || {
top: config.timbradoMarginTop || 100,
left: config.timbradoMarginLeft || 50,
right: config.timbradoMarginRight || 50,
bottom: config.timbradoMarginBottom || 50
};
console.log('Usando margens:', margens);
// Abordagem focada em PDF de alta qualidade com formatação precisa
try {
console.log('Gerando PDF com formatação avançada...');
// 1. Primeiro carregar o timbrado se existir
let pdfDoc;
const fontSize = 11;
const lineHeight = fontSize * 1.5; // Espaçamento entre linhas um pouco maior para melhor legibilidade
const pageWidth = 595;
const pageHeight = 842;
if (timbradoExists) {
console.log('Usando timbrado como base do documento...');
// Carregar o arquivo de timbrado
const timbradoBytes = await fs.readFile(timbradoPath);
try {
pdfDoc = await PDFDocument.load(timbradoBytes);
console.log('Timbrado carregado com sucesso');
// Verificar se o timbrado tem páginas
if (pdfDoc.getPageCount() === 0) {
console.log('Timbrado não contém páginas, adicionando página padrão');
pdfDoc.addPage([pageWidth, pageHeight]);
}
} catch (loadError) {
console.error('Erro ao carregar timbrado:', loadError);
// Se falhar ao carregar o timbrado, criar novo documento
pdfDoc = await PDFDocument.create();
pdfDoc.addPage([pageWidth, pageHeight]);
}
} else {
console.log('Criando novo documento sem timbrado...');
pdfDoc = await PDFDocument.create();
pdfDoc.addPage([pageWidth, pageHeight]);
}
// 2. Carregar fontes
console.log('Carregando fontes...');
let font, boldFont, italicFont;
try {
// Verificar se temos a fonte Fira Sans disponível
const firaSansPath = path.join(process.cwd(), 'public', 'fonts', 'FiraSans-Regular.ttf');
const firaSansBoldPath = path.join(process.cwd(), 'public', 'fonts', 'FiraSans-Bold.ttf');
// Verificar se o arquivo existe
let firaSansExists = false;
try {
await fs.access(firaSansPath);
firaSansExists = true;
console.log('Fonte Fira Sans encontrada, utilizando-a para o documento');
} catch (e) {
console.log('Fonte Fira Sans não encontrada, usando Helvetica como fallback');
}
if (firaSansExists) {
// Carregar as fontes Fira Sans
try {
const firaSansBytes = await fs.readFile(firaSansPath);
font = await pdfDoc.embedFont(firaSansBytes);
try {
const firaSansBoldBytes = await fs.readFile(firaSansBoldPath);
boldFont = await pdfDoc.embedFont(firaSansBoldBytes);
} catch (e) {
console.log('Fira Sans Bold não encontrada, usando regular como fallback');
boldFont = font;
}
// Não temos itálico, usar regular
italicFont = font;
} catch (fontError) {
console.error('Erro ao carregar Fira Sans:', fontError);
throw fontError; // Forçar fallback para Helvetica
}
} else {
throw new Error('Fira Sans não encontrada');
}
} catch (error) {
// Fallback para Helvetica
console.log('Usando Helvetica como fallback');
font = await pdfDoc.embedFont(StandardFonts.Helvetica);
boldFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
italicFont = await pdfDoc.embedFont(StandardFonts.HelveticaOblique);
}
// 3. Processar e formatar o HTML com preservação de estrutura
console.log('Processando HTML com preservação de estrutura...');
// Processar variáveis
const processedContent = this.processarTemplate(conteudo, escritorio);
// Função para decodificar entidades HTML melhorada
const decodeHtmlEntities = (text: string): string => {
// Mapa expandido de entidades HTML
const entities: Record<string, string> = {
"&nbsp;": " ",
"&amp;": "&",
"&lt;": "<",
"&gt;": ">",
"&quot;": "\"",
"&apos;": "'",
"&cent;": "¢",
"&pound;": "£",
"&yen;": "¥",
"&euro;": "€",
"&copy;": "©",
"&reg;": "®",
"&trade;": "™",
"&Agrave;": "À",
"&agrave;": "à",
"&Aacute;": "Á",
"&aacute;": "á",
"&Acirc;": "Â",
"&acirc;": "â",
"&Atilde;": "Ã",
"&atilde;": "ã",
"&Auml;": "Ä",
"&auml;": "ä",
"&Aring;": "Å",
"&aring;": "å",
"&AElig;": "Æ",
"&aelig;": "æ",
"&Ccedil;": "Ç",
"&ccedil;": "ç",
"&Egrave;": "È",
"&egrave;": "è",
"&Eacute;": "É",
"&eacute;": "é",
"&Ecirc;": "Ê",
"&ecirc;": "ê",
"&Euml;": "Ë",
"&euml;": "ë",
"&Igrave;": "Ì",
"&igrave;": "ì",
"&Iacute;": "Í",
"&iacute;": "í",
"&Icirc;": "Î",
"&icirc;": "î",
"&Iuml;": "Ï",
"&iuml;": "ï",
"&ETH;": "Ð",
"&eth;": "ð",
"&Ntilde;": "Ñ",
"&ntilde;": "ñ",
"&Ograve;": "Ò",
"&ograve;": "ò",
"&Oacute;": "Ó",
"&oacute;": "ó",
"&Ocirc;": "Ô",
"&ocirc;": "ô",
"&Otilde;": "Õ",
"&otilde;": "õ",
"&Ouml;": "Ö",
"&ouml;": "ö",
"&Oslash;": "Ø",
"&oslash;": "ø",
"&Ugrave;": "Ù",
"&ugrave;": "ù",
"&Uacute;": "Ú",
"&uacute;": "ú",
"&Ucirc;": "Û",
"&ucirc;": "û",
"&Uuml;": "Ü",
"&uuml;": "ü",
"&Yacute;": "Ý",
"&yacute;": "ý",
"&yuml;": "ÿ",
"&szlig;": "ß",
"&ndash;": "–",
"&mdash;": "—",
"&lsquo;": "'",
"&rsquo;": "'",
"&sbquo;": "‚",
"&ldquo;": "\"",
"&rdquo;": "\"",
"&bdquo;": "„",
"&lsaquo;": "‹",
"&rsaquo;": "›",
"&laquo;": "«",
"&raquo;": "»",
"&hellip;": "…",
"&bull;": "•",
"&middot;": "·"
};
return text.replace(/&([a-z0-9]+|#[0-9]{1,6}|#x[0-9a-fA-F]{1,6});/gi, (match) => {
if (entities[match]) {
return entities[match];
} else if (match.startsWith('&#x')) {
// Hexadecimal Unicode
const hex = match.substring(3, match.length - 1);
try {
return String.fromCodePoint(parseInt(hex, 16));
} catch (e) {
return match;
}
} else if (match.startsWith('&#')) {
// Decimal Unicode
const decimal = match.substring(2, match.length - 1);
try {
return String.fromCodePoint(parseInt(decimal, 10));
} catch (e) {
return match;
}
}
return match;
});
};
} catch (error) {
console.error('Erro ao processar HTML:', error);
throw error;
}
} catch (error) {
console.error('Erro geral ao gerar PDF:', error);
throw error;
}
}
/**
* Processa um template HTML substituindo variáveis pelos valores do objeto
* @param template - Template HTML com variáveis no formato {{variavel}}
* @param dados - Objeto com valores para substituir
* @returns Template processado
*/
private processarTemplate(template: string, dados: any): string {
return template.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
const keyParts = key.trim().split('.');
let value = dados;
// Acessar propriedades aninhadas (ex: escritorio.nome)
for (const part of keyParts) {
if (value && typeof value === 'object' && part in value) {
value = value[part];
} else {
value = undefined;
break;
}
}
return value !== undefined ? value : match;
});
}
}