diff --git a/Dockerfile b/Dockerfile index 2cae6d4..2f108ab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,18 @@ FROM node:20-slim +# Configurar Puppeteer para usar o Chromium do sistema +ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true +ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser + # Instalar dependências do sistema -RUN apt-get update && apt-get install -y \ - chromium \ +RUN apt-get update && apt-get install -y chromium \ libgconf-2-4 \ postgresql-client \ - && rm -rf /var/lib/apt/lists/* + && rm -rf /var/lib/apt/lists/* \ + && ln -sf /usr/bin/chromium /usr/bin/chromium-browser + +# Verificar a instalação do Chromium +RUN chromium --version # Criar diretório da aplicação WORKDIR /usr/src/app diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..e97489f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: '3.8' +services: + backend: + build: . + ports: + - "5000:5000" + environment: + - NODE_ENV=production + - CHROME_BIN=/usr/bin/google-chrome-stable + # ... outras variáveis de ambiente existentes ... + volumes: + - .:/usr/src/app + - /usr/src/app/node_modules diff --git a/package.json b/package.json index 37fcf7c..d1cd598 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "multer": "^1.4.5-lts.1", "opentimestamps-javascript": "^1.0.1", "pdf-lib": "^1.17.1", + "pdfkit": "^0.13.0", "puppeteer": "^24.5.0", "qrcode": "^1.5.3", "react": "^18.2.0", @@ -111,11 +112,5 @@ "--disable-gpu", "--window-size=1920x1080" ] - }, - "puppeteerOptions": { - "browserFetcher": { - "platform": "win32", - "downloadPath": ".cache/puppeteer" - } } } diff --git a/scripts/fix-uuid.sql b/scripts/fix-uuid.sql new file mode 100644 index 0000000..0ec4191 --- /dev/null +++ b/scripts/fix-uuid.sql @@ -0,0 +1,9 @@ +-- Identificar registros com UUID inválido +SELECT id, uuid_column +FROM atendimento +WHERE uuid_column !~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'; + +-- Atualizar registros com UUID inválido +UPDATE atendimento +SET uuid_column = gen_random_uuid() +WHERE uuid_column !~ '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'; diff --git a/src/components/clientes/ClienteModal.tsx b/src/components/clientes/ClienteModal.tsx index c17000a..e4c0c7e 100644 --- a/src/components/clientes/ClienteModal.tsx +++ b/src/components/clientes/ClienteModal.tsx @@ -200,7 +200,7 @@ export const ClienteModal: React.FC = ({ // Monitorar o valor do CEP para autopreenchimento const cepValue = watch("cep"); - // Função para buscar dados do CEP - versão com gerenciamento de estados do componente + // Função para buscar dados do CEP - versão corrigida const fetchAddressByCep = useCallback(async (cep: string) => { if (!cep || cep.length !== 8) return; @@ -213,7 +213,6 @@ export const ClienteModal: React.FC = ({ if (response.data && !response.data.erro) { // Usar estado local para garantir re-renderização imediata - // Forçar preenchimento dos campos e atualização da interface const endereco = response.data.logradouro || ""; const bairro = response.data.bairro || ""; const cidade = response.data.localidade || ""; @@ -225,8 +224,7 @@ export const ClienteModal: React.FC = ({ setValue("cidade", cidade, { shouldValidate: true }); setValue("estado", estado, { shouldValidate: true }); - // Garantir re-renderização do componente - // Esta é a parte crucial: forçar o componente a re-renderizar com novo estado + // Atualizar o estado para re-renderização do componente setEnderecoState({ endereco, bairro, @@ -235,13 +233,23 @@ export const ClienteModal: React.FC = ({ preenchido: true, }); - // Focar no próximo campo - setTimeout(() => { - const numeroInput = document.querySelector( - 'input[name="numero"]' - ) as HTMLInputElement; - if (numeroInput) numeroInput.focus(); - }, 100); + // Focar no próximo campo apenas se o usuário não estiver digitando ativamente + // Esta alteração evita interromper o fluxo natural de preenchimento + const activeElement = document.activeElement; + const isUserTyping = + activeElement && + (activeElement.tagName === 'INPUT' || activeElement.tagName === 'SELECT') && + activeElement !== document.querySelector('input[name="cep"]'); + + // Só focar no campo número se o usuário não estiver digitando em outro campo + if (!isUserTyping) { + setTimeout(() => { + const numeroInput = document.querySelector( + 'input[name="numero"]' + ) as HTMLInputElement; + if (numeroInput) numeroInput.focus(); + }, 100); + } } else { console.error("CEP não encontrado"); alert("CEP não encontrado. Por favor, verifique o CEP informado."); diff --git a/src/server/index.ts b/src/server/index.ts index a017f0a..8ae74ed 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -55,6 +55,13 @@ const orgaoJulgadorRepository = new OrgaoJulgadorRepository(); const custaProcessualRepository = new CustaProcessualRepository(); const honorarioRepository = new HonorarioRepository(); +// Atualizar a função isValidUUID para ser mais robusta +const isValidUUID = (uuid: string | null | undefined): boolean => { + if (!uuid) return false; + const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + return uuidRegex.test(uuid); +}; + // Middleware para extrair usuário do token // Remover este middleware pois já temos o auth.ts // const extractUser = (req: any, res: any, next: any) => { ... } @@ -1000,9 +1007,9 @@ app.post("/api/atendimentos", authMiddleware, async (req, res) => { conteudo, cliente: { connect: { id: clienteId } }, usuario: { connect: { id: req.user!.id } }, - ...(processoId && { processo: { connect: { id: processoId } } }), + ...(processoId && { processo: { connect: { id: processoId } } }), // Adicionar vírgula aqui status: 'ATIVO', - dataAtualizacao: new Date(), + dataAtualizacao: new Date() }, include: { cliente: true, @@ -1337,6 +1344,74 @@ app.delete("/api/agendamentos/:id", authMiddleware, async (req, res) => { } }); +// Atualizar a rota de agendamentos +app.get("/api/atendimentos/agendamentos", async (req, res) => { + try { + const { data } = req.query; + + if (!data || typeof data !== 'string') { + return res.status(400).json({ error: 'Data inválida' }); + } + + const dataConsulta = new Date(data); + + // Validar se a data é válida + if (isNaN(dataConsulta.getTime())) { + return res.status(400).json({ error: 'Formato de data inválido' }); + } + + // Definir início e fim do dia + const inicioDia = new Date(dataConsulta.setHours(0, 0, 0, 0)); + const fimDia = new Date(dataConsulta.setHours(23, 59, 59, 999)); + + // Buscar na tabela correta com filtros apropriados + const agendamentos = await prisma.agendamentoAtendimento.findMany({ + where: { + data: { + gte: inicioDia, + lte: fimDia + } + }, + include: { + cliente: { + select: { + id: true, + nome: true, + cpfCnpj: true, + telefone: true + } + }, + usuario: { + select: { + id: true, + nome: true + } + } + }, + orderBy: { + data: 'asc' + } + }); + + // Filtrar apenas agendamentos com UUIDs válidos + const validAgendamentos = agendamentos.filter(agendamento => + isValidUUID(agendamento.id) && + isValidUUID(agendamento.clienteId) && + isValidUUID(agendamento.usuarioId) + ); + + console.log(`Encontrados ${validAgendamentos.length} agendamentos válidos para ${data}`); + + res.json(validAgendamentos); + } catch (error) { + console.error('Erro ao buscar agendamentos:', error); + res.status(500).json({ + error: 'Erro ao buscar agendamentos', + details: error instanceof Error ? error.message : 'Erro desconhecido' + }); + } +}); + // Erro handling middleware app.use( ( diff --git a/src/server/services/PdfService.ts b/src/server/services/PdfService.ts index c5342cb..635822d 100644 --- a/src/server/services/PdfService.ts +++ b/src/server/services/PdfService.ts @@ -150,14 +150,13 @@ export class PdfService { orderBy: { criadoEm: 'desc' }, select: { id: true, - headerImageUrl: true, - footerImageUrl: true, - logoUrl: true, timbradoUrl: true, timbradoMarginTop: true, timbradoMarginBottom: true, timbradoMarginLeft: true, timbradoMarginRight: true, + templatePadrao: true, + margensTemplate: true, criadoEm: true } }); @@ -166,195 +165,734 @@ export class PdfService { // Retornar configuração padrão se não existir return { - cabecalhoHtml: config?.cabecalhoHtml || ` + // Valores padrão + cabecalhoHtml: `

{{escritorio.nome}}

`, - rodapeHtml: config?.rodapeHtml || ` + rodapeHtml: `
{{escritorio.endereco}}
Tel: {{escritorio.telefone}} | Email: {{escritorio.email}}
`, - logoUrl: config?.logoUrl || '', + // Campos que existem no modelo timbradoUrl: config?.timbradoUrl || '', timbradoMarginTop: config?.timbradoMarginTop || 100, timbradoMarginBottom: config?.timbradoMarginBottom || 50, timbradoMarginLeft: config?.timbradoMarginLeft || 50, - timbradoMarginRight: config?.timbradoMarginRight || 50 + timbradoMarginRight: config?.timbradoMarginRight || 50, + templatePadrao: config?.templatePadrao || null, + margensTemplate: config?.margensTemplate || null }; } catch (error) { console.error('Erro ao buscar configuração:', error); - throw new Error('Falha ao buscar configuração'); + // Retornar configuração padrão para não interromper o fluxo + return { + cabecalhoHtml: `

Documento

`, + rodapeHtml: `
Documento gerado pelo sistema
`, + timbradoUrl: '', + timbradoMarginTop: 100, + timbradoMarginBottom: 50, + timbradoMarginLeft: 50, + timbradoMarginRight: 50, + templatePadrao: null, + margensTemplate: null + }; } } - private gerarHTML(content: string, config: any) { - const timbradoUrl = config.timbradoUrl - ? `${process.env.BACKEND_URL || 'http://localhost:5000'}${config.timbradoUrl}` - : ''; - - console.log('Gerando PDF com timbrado:', timbradoUrl); - - return ` - - - - - - - - -
-
- ${content} -
-
- - - `; - } - - async gerarPDF(conteudo: string, escritorio: any) { - try { - // 1. Buscar configuração do template - const config = await prisma.configuracaoDocumento.findFirst({ - orderBy: { criadoEm: 'desc' } - }); - - // 2. Gerar PDF do conteúdo - const browser = await puppeteer.launch({ - headless: 'new', - args: ['--no-sandbox'] - }); - - const page = await browser.newPage(); - - // Usar template padrão ou fallback para o template básico - const template = config?.templatePadrao || ` - - - - - - - -
- ${conteudo} -
- - - `; - - // Processar o template substituindo variáveis - const templateProcessado = template.replace(/\{\{(.+?)\}\}/g, (match, key) => { - return escritorio[key.trim()] || match; - }); - - await page.setContent(templateProcessado); - - const margens = config?.margensTemplate || { - top: '35mm', - bottom: '29mm', - left: '25mm', - right: '25mm' - }; - - const contentPdfBuffer = await page.pdf({ - format: 'A4', - margin: margens, - displayHeaderFooter: false, - printBackground: true - }); - - await browser.close(); - - // 2. Carregar os PDFs - const templatePath = path.join(process.cwd(), 'public', 'uploads', 'Timbrado_2025.pdf'); - const templateBytes = await fs.readFile(templatePath); - - const templateDoc = await PDFDocument.load(templateBytes); - const contentDoc = await PDFDocument.load(contentPdfBuffer); - - // 3. Criar documento final - const pdfDoc = await PDFDocument.create(); - - // 4. Copiar template e conteúdo - const [templatePage] = await templateDoc.copyPages(templateDoc, [0]); - const contentPages = await contentDoc.getPages(); - - // 5. Para cada página do conteúdo - for (let i = 0; i < contentPages.length; i++) { - // Primeiro: adicionar template - const page = await pdfDoc.embedPage(templatePage); - const contentPage = await pdfDoc.embedPage(contentPages[i]); + } 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; + }); + }; - // Segundo: criar nova página - const newPage = pdfDoc.addPage([page.width, page.height]); + // Extrair elementos estruturados do HTML para melhor formatação + const extractStructuredContent = (html: string) => { + // Limpar o HTML inicial + let cleanHtml = html + // Remover scripts e styles + .replace(/)<[^<]*)*<\/script>/gi, '') + .replace(/)<[^<]*)*<\/style>/gi, '') + // Normalizar quebras de linha + .replace(/\r\n/g, '\n') + .replace(/\r/g, '\n'); + + // Substituir os   por espaços regulares mas preservar os consecutivos + cleanHtml = cleanHtml.replace(/( ){2,}/g, match => ' '.repeat(match.length / 6)); + cleanHtml = cleanHtml.replace(/ /g, ' '); + + // Preservar informações de estilo e formatação + const elements = []; + + // Extrair e processar elementos de cabeçalho + const headingRegex = /]*>([\s\S]*?)<\/h\1>/gi; + let headingMatch; + while ((headingMatch = headingRegex.exec(cleanHtml)) !== null) { + elements.push({ + type: 'heading', + level: parseInt(headingMatch[1]), + content: decodeHtmlEntities(headingMatch[2].replace(/<[^>]*>/g, '').trim()) + }); + } + + // Extrair e processar parágrafos + const paragraphRegex = /]*>([\s\S]*?)<\/p>/gi; + let paragraphMatch; + while ((paragraphMatch = paragraphRegex.exec(cleanHtml)) !== null) { + elements.push({ + type: 'paragraph', + content: decodeHtmlEntities(paragraphMatch[1] + .replace(//gi, '\n') + .replace(/<[^>]*>/g, '') + .trim()) + }); + } + + // Extrair texto de divs que não foram capturados + const divRegex = /]*>([\s\S]*?)<\/div>/gi; + let divMatch; + while ((divMatch = divRegex.exec(cleanHtml)) !== null) { + const content = decodeHtmlEntities(divMatch[1] + .replace(//gi, '\n') + .replace(/<[^>]*>/g, '') + .trim()); + + if (content) { + elements.push({ + type: 'div', + content + }); + } + } + + // Se o texto não tiver estrutura clara, extrair todo o texto restante + if (elements.length === 0) { + const plainText = decodeHtmlEntities(cleanHtml + .replace(//gi, '\n') + .replace(/<\/p>\s*]*>/gi, '\n\n') + .replace(/<\/div>\s*]*>/gi, '\n') + .replace(/<[^>]*>/g, '') + .trim()); + + // Dividir por linhas vazias para criar parágrafos + const paragraphs = plainText.split(/\n\s*\n/); + + for (const paragraph of paragraphs) { + if (paragraph.trim()) { + elements.push({ + type: 'paragraph', + content: paragraph.trim() + }); + } + } + } + + // Ordenar elementos para manter a ordem original do documento + return elements.sort((a, b) => { + const aIndex = html.indexOf(a.content); + const bIndex = html.indexOf(b.content); + return aIndex - bIndex; + }); + }; - // Terceiro: desenhar template e conteúdo - newPage.drawPage(page); - newPage.drawPage(contentPage); + // Extrair conteúdo estruturado + const structuredContent = extractStructuredContent(processedContent); + + console.log(`Renderizando ${structuredContent.length} elementos...`); + + // Obter primeira página do documento + let currentPage = pdfDoc.getPage(0); + let y = pageHeight - margens.top; // Posição inicial a partir do topo com margem + let pageIndex = 0; + + // Armazenar a primeira página como template para reproduzir o timbrado em novas páginas + let firstPageTemplate = null; + if (timbradoExists) { + firstPageTemplate = pdfDoc.getPage(0); + console.log('Template de timbrado capturado para replicação em novas páginas'); + } + + // Função para adicionar nova página quando necessário + const addNewPageIfNeeded = (minSpace: number): boolean => { + if (y - minSpace < margens.bottom) { + try { + // Adicionar nova página + let newPage; + + if (timbradoExists && firstPageTemplate) { + // Tentar criar uma cópia da primeira página para manter o timbrado + try { + // Copiamos a página para um novo documento e depois adicionamos de volta + const [copiedPage] = pdfDoc.copyPages(pdfDoc, [0]); + newPage = pdfDoc.addPage(copiedPage.getSize()); + + // Desenhar o conteúdo da primeira página (timbrado) como fundo + const { width, height } = newPage.getSize(); + newPage.drawPage(copiedPage, { + x: 0, + y: 0, + width, + height, + opacity: 1 + }); + + console.log('Nova página criada com timbrado'); + } catch (copyError) { + console.error('Erro ao copiar timbrado para nova página:', copyError); + // Fallback: adicionar página em branco + newPage = pdfDoc.addPage([pageWidth, pageHeight]); + } + } else { + // Sem timbrado, adicionar página em branco + newPage = pdfDoc.addPage([pageWidth, pageHeight]); + } + + currentPage = newPage; + pageIndex++; + y = pageHeight - margens.top; + return true; + } catch (error) { + console.error('Erro ao adicionar nova página:', error); + // Tentar novamente com página simples + try { + const newPage = pdfDoc.addPage([pageWidth, pageHeight]); + currentPage = newPage; + pageIndex++; + y = pageHeight - margens.top; + return true; + } catch (fallbackError) { + console.error('Falha crítica ao adicionar página:', fallbackError); + return false; + } + } + } + return false; + }; + + // Função para quebrar texto em linhas com boa distribuição + const wrapTextToLines = (text: string, maxWidth: number): string[] => { + const lines = []; + const words = text.split(' '); + let currentLine = ''; + + // Estimativa de largura (usando proporção aproximada para cada fonte) + const getApproxWidth = (text: string): number => { + const averageCharWidth = fontSize * 0.55; // Estimativa para Helvetica/Fira Sans + return text.length * averageCharWidth; + }; + + for (const word of words) { + const testLine = currentLine ? `${currentLine} ${word}` : word; + + if (getApproxWidth(testLine) > maxWidth) { + if (currentLine) { + lines.push(currentLine); + currentLine = word; + } else { + // Palavra muito longa, não quebrar + lines.push(word); + currentLine = ''; + } + } else { + currentLine = testLine; + } + } + + if (currentLine) { + lines.push(currentLine); + } + + return lines; + }; + + // Renderizar todos os elementos estruturados + for (const element of structuredContent) { + const availableWidth = pageWidth - margens.left - margens.right; + + switch (element.type) { + case 'heading': { + // Tamanho de fonte para cabeçalhos (h1, h2, h3...) + const headingSize = fontSize * (1.8 - (element.level - 1) * 0.2); + const headingLines = wrapTextToLines(element.content, availableWidth); + + // Espaço antes do cabeçalho + y -= lineHeight * 0.5; + + // Espaço necessário para o cabeçalho completo + const headingSpace = headingLines.length * lineHeight * 1.2 + lineHeight; + addNewPageIfNeeded(headingSpace); + + // Verificar se é para centralizar o cabeçalho (h1 ou h2) + const centerHeading = element.level <= 2; + + // Desenhar cada linha do cabeçalho + for (const line of headingLines) { + let xPos = margens.left; + + if (centerHeading) { + // Centralizar o texto + const estimatedWidth = line.length * (headingSize * 0.55); + xPos = Math.max(margens.left, (pageWidth - estimatedWidth) / 2); + } + + currentPage.drawText(line, { + x: xPos, + y, + font: boldFont, + size: headingSize + }); + + y -= lineHeight * 1.2; + } + + // Espaço adicional após cabeçalho + y -= lineHeight * 0.3; + break; + } + + case 'paragraph': + case 'div': { + // Detectar alinhamento (apenas para parágrafos curtos) + const isCentered = processedContent.includes('text-align: center') && + element.content.length < 100; + + // Dividir em linhas para respeitar quebras originais + const textLines = element.content.split('\n'); + let firstParagraphLine = true; + + for (const textLine of textLines) { + if (!textLine.trim()) { + // Linha vazia - adicionar espaço + y -= lineHeight * 0.5; + continue; + } + + // Quebrar a linha para caber na largura + const wrappedLines = wrapTextToLines(textLine, availableWidth); + + for (const wrappedLine of wrappedLines) { + // Verificar espaço para nova página + if (addNewPageIfNeeded(lineHeight)) { + // Nova página foi adicionada + } + + // Calcular posição X para alinhamento + let xPos = margens.left; + + if (isCentered) { + // Centralizar texto se tiver marcação de alinhamento + const estimatedWidth = wrappedLine.trim().length * (fontSize * 0.55); + xPos = Math.max(margens.left, (pageWidth - estimatedWidth) / 2); + } + + // Desenhar a linha de texto + currentPage.drawText(wrappedLine.trim(), { + x: xPos, + y, + font, + size: fontSize + }); + + y -= lineHeight; + } + + if (firstParagraphLine) { + firstParagraphLine = false; + } else { + // Espaço extra entre parágrafos distintos + y -= lineHeight * 0.2; + } + } + + // Espaço após parágrafo + y -= lineHeight * 0.3; + break; + } + } + } + + // Adicionar rodapé (apenas se não houver timbrado ou na última página) + if (!timbradoExists && (escritorio.nome || escritorio.endereco || escritorio.telefone || escritorio.email)) { + const footerY = margens.bottom - 15; + + let footerText = ''; + if (escritorio.nome) footerText += escritorio.nome; + if (escritorio.endereco) { + if (footerText) footerText += ' | '; + footerText += escritorio.endereco; + } + if (escritorio.telefone) { + if (footerText) footerText += ' | '; + footerText += `Tel: ${escritorio.telefone}`; + } + if (escritorio.email) { + if (footerText) footerText += ' | '; + footerText += escritorio.email; + } + + try { + currentPage.drawText(footerText, { + x: margens.left, + y: footerY, + font, + size: 8, + color: rgb(0.5, 0.5, 0.5) // Cinza claro para ser discreto + }); + } catch (footerError) { + console.error('Erro ao adicionar rodapé:', footerError); + } + } + + // Salvar documento + console.log('Finalizando e salvando PDF...'); + const pdfBytes = await pdfDoc.save(); + console.log(`PDF gerado com sucesso: ${pdfBytes.length} bytes`); + + return Buffer.from(pdfBytes); + } catch (error) { + console.error('Erro ao gerar PDF:', error); + + // Fallback absolutamente mínimo e à prova de falhas + try { + console.log('Usando fallback ultra-minimalista...'); + + // Criar documento do zero da forma mais simples possível + const pdfDoc = await PDFDocument.create(); + const page = pdfDoc.addPage(); + const font = await pdfDoc.embedFont(StandardFonts.Helvetica); + + // Desenhar texto mínimo + page.drawText('Documento', { + x: 50, + y: 800, + size: 14, + font + }); + + page.drawText('Gerado em: ' + new Date().toLocaleString('pt-BR'), { + x: 50, + y: 780, + size: 10, + font + }); + + // Extrair e adicionar apenas parte do texto + const plainText = conteudo.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); + + // Garantir que não exceda limite seguro + const safeText = plainText.substring(0, Math.min(plainText.length, 1000)); + + // Adicionar linhas de texto com posições absolutos + const maxLines = 30; + const lineLength = 60; + + for (let i = 0; i < safeText.length && i/lineLength < maxLines; i += lineLength) { + const line = safeText.substring(i, i + lineLength); + const lineY = 750 - Math.floor(i/lineLength) * 12; + + if (lineY < 50) break; // Evitar desenhar fora da página + + page.drawText(line, { + x: 50, + y: lineY, + size: 9, + font + }); + } + + // Salvar PDF + const pdfBytes = await pdfDoc.save(); + return Buffer.from(pdfBytes); + } catch (fallbackError) { + console.error('Erro no fallback:', fallbackError); + + // Criar um PDF manualmente como último recurso + return Buffer.from('%PDF-1.4\n1 0 obj\n<>\nendobj\n2 0 obj\n<>\nendobj\n3 0 obj\n<>>>\nendobj\nxref\n0 4\n0000000000 65535 f\n0000000015 00000 n\n0000000060 00000 n\n0000000111 00000 n\ntrailer\n<>\nstartxref\n190\n%%EOF'); + } } - - // 6. Gerar PDF final - const pdfBytes = await pdfDoc.save(); - return Buffer.from(pdfBytes); - } catch (error) { - console.error('Erro ao gerar PDF:', error); - throw new Error('Falha ao gerar PDF'); + 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; + }); + } }