From 68bbd4ee35553aab4355031d7ebf32d27564621a Mon Sep 17 00:00:00 2001 From: Rodrigo Martins Date: Sat, 12 Apr 2025 20:02:58 -0300 Subject: [PATCH] fix atendimentos.ts Signed-off-by: Rodrigo Martins --- Dockerfile | 13 +- docker-compose.yml | 13 + package.json | 7 +- scripts/fix-uuid.sql | 9 + src/components/clientes/ClienteModal.tsx | 30 +- src/server/index.ts | 79 ++++- src/server/services/PdfService.ts | 381 +++++++++++++++-------- 7 files changed, 374 insertions(+), 158 deletions(-) create mode 100644 docker-compose.yml create mode 100644 scripts/fix-uuid.sql 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..f8f38d8 100644 --- a/src/server/services/PdfService.ts +++ b/src/server/services/PdfService.ts @@ -146,19 +146,19 @@ export class PdfService { private async getConfiguracao() { try { // Buscar configuração ou usar valores padrão + // Removendo campos que não existem no modelo const config = await prisma.configuracaoDocumento.findFirst({ orderBy: { criadoEm: 'desc' }, select: { id: true, - headerImageUrl: true, - footerImageUrl: true, - logoUrl: true, timbradoUrl: true, timbradoMarginTop: true, timbradoMarginBottom: true, timbradoMarginLeft: true, timbradoMarginRight: true, - criadoEm: true + criadoEm: true, + templatePadrao: true, + margensTemplate: true } }); @@ -166,195 +166,304 @@ export class PdfService { // Retornar configuração padrão se não existir return { - cabecalhoHtml: config?.cabecalhoHtml || ` + // Valores padrão para campos que não existem mais no modelo + 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 || false, + margensTemplate: config?.margensTemplate || false }; } catch (error) { console.error('Erro ao buscar configuração:', error); - throw new Error('Falha ao buscar configuração'); + // Returnar configuração padrão em caso de erro para não interromper o fluxo + return { + cabecalhoHtml: `

Documento

`, + rodapeHtml: `
Documento gerado pelo sistema
`, + timbradoUrl: '', + timbradoMarginTop: 113, + timbradoMarginBottom: 35, + timbradoMarginLeft: 35, + timbradoMarginRight: 90, + templatePadrao: false, + margensTemplate: false + }; } } private gerarHTML(content: string, config: any) { + // Para arquivos PDF de timbrado, não vamos tentar incorporá-los como background-image + // pois isso causa corrupção. Vamos apenas marcar as margens + const isPdfTimbrado = config.timbradoUrl?.toLowerCase().endsWith('.pdf'); + + // URL do timbrado apenas para referência em logs - usando FRONTEND_URL conforme indicado const timbradoUrl = config.timbradoUrl - ? `${process.env.BACKEND_URL || 'http://localhost:5000'}${config.timbradoUrl}` + ? `${process.env.FRONTEND_URL || 'https://advx.martinsevieira.adv.br'}${config.timbradoUrl.startsWith('/') ? '' : '/'}${config.timbradoUrl}` : ''; - console.log('Gerando PDF com timbrado:', timbradoUrl); + console.log('Configuração de timbrado:', { + url: timbradoUrl, + isPdfTimbrado + }); + // Usar as margens do template se disponíveis + const margens = config.margensTemplate || { + top: config.timbradoMarginTop || 100, + left: config.timbradoMarginLeft || 50, + right: config.timbradoMarginRight || 50, + bottom: config.timbradoMarginBottom || 50 + }; + + // Aplicar margens + const margemTopo = margens.top; + const margemEsquerda = margens.left; + const margemDireita = margens.right; + const margemInferior = margens.bottom; + + console.log('Aplicando margens:', { + margemTopo, + margemDireita, + margemEsquerda, + margemInferior + }); + + // HTML muito simples sem tentar incorporar o timbrado return ` - -
-
- ${content} -
+
+ ${content}
`; } - async gerarPDF(conteudo: string, escritorio: any) { + async gerarPDF(html: string): Promise { try { - // 1. Buscar configuração do template - const config = await prisma.configuracaoDocumento.findFirst({ - orderBy: { criadoEm: 'desc' } - }); - - // 2. Gerar PDF do conteúdo + console.log('Buscando configurações do documento...'); + const config = await this.getConfiguracao(); + + // Verificar se o timbrado é um PDF + const isTimbradoPdf = config.timbradoUrl?.toLowerCase().endsWith('.pdf'); + + // Se for um PDF, vamos tentar uma abordagem diferente + if (isTimbradoPdf && config.timbradoUrl) { + try { + console.log('Timbrado é PDF, tentando abordagem específica...'); + + // Primeiramente vamos gerar um PDF apenas com o conteúdo + const htmlCompleto = this.gerarHTML(html, config); + + // Gerar PDF do conteúdo + console.log('Gerando PDF do conteúdo...'); + const browser = await puppeteer.launch({ + executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium-browser', + headless: true, + args: ['--no-sandbox'] + }); + + let conteudoPdfBuffer; + try { + const page = await browser.newPage(); + await page.setContent(htmlCompleto, { waitUntil: 'networkidle0' }); + conteudoPdfBuffer = await page.pdf({ format: 'A4' }); + console.log('PDF do conteúdo gerado com sucesso:', conteudoPdfBuffer.length, 'bytes'); + } finally { + await browser.close(); + } + + // Agora vamos buscar o timbrado + const timbradoUrl = config.timbradoUrl.startsWith('/') + ? config.timbradoUrl.substring(1) // Remove a barra inicial se existir + : config.timbradoUrl; + + // Buscar o PDF do timbrado via HTTP do frontend + console.log('Obtendo o PDF do timbrado via HTTP...'); + const response = await fetch(`${process.env.FRONTEND_URL || 'https://advx.martinsevieira.adv.br'}/${timbradoUrl}`); + + if (!response.ok) { + throw new Error(`Failed to fetch timbrado: ${response.status} ${response.statusText}`); + } + + const timbradoBuffer = await response.arrayBuffer(); + console.log('Timbrado obtido com sucesso:', timbradoBuffer.byteLength, 'bytes'); + + // Carregar ambos os PDFs + console.log('Mesclando PDFs...'); + const timbradoPdf = await PDFDocument.load(new Uint8Array(timbradoBuffer)); + const conteudoPdf = await PDFDocument.load(conteudoPdfBuffer); + + // Criar um novo documento + const documentoFinal = await PDFDocument.create(); + + // Determinar quantas páginas precisamos (conteúdo pode ter múltiplas páginas) + const numPaginasConteudo = conteudoPdf.getPageCount(); + console.log(`Mesclando ${numPaginasConteudo} páginas de conteúdo...`); + + // Para cada página de conteúdo, criar uma nova página baseada no timbrado + for (let i = 0; i < numPaginasConteudo; i++) { + // Copiar a página do timbrado (sempre a primeira página) + const [paginaTimbradoCopiada] = await documentoFinal.copyPages(timbradoPdf, [0]); + documentoFinal.addPage(paginaTimbradoCopiada); + + // Obter a página atual do documento final + const paginaAtual = documentoFinal.getPage(i); + + // Copiar o conteúdo da página atual do conteúdo + const paginaConteudo = conteudoPdf.getPage(i); + + // Obter as dimensões da página + const { width, height } = paginaAtual.getSize(); + + // Aplicar margens do template + const margens = config.margensTemplate || { + top: config.timbradoMarginTop || 100, + left: config.timbradoMarginLeft || 50, + right: config.timbradoMarginRight || 50, + bottom: config.timbradoMarginBottom || 50 + }; + + // Incorporar o conteúdo SVG na página do timbrado + const contentSvg = await paginaConteudo.svg(); + + // Criar um PDF temporário com o SVG para poder copiar de volta + const tempPdf = await PDFDocument.create(); + const tempPage = tempPdf.addPage([width, height]); + + // Ajustar o SVG para corresponder às margens definidas + const ajustedSvg = contentSvg + .replace(' - - - - - - -
- ${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]); + try { + const page = await browser.newPage(); + await page.setContent(htmlCompleto, { waitUntil: 'networkidle0' }); - // Segundo: criar nova página - const newPage = pdfDoc.addPage([page.width, page.height]); + pdfBuffer = await page.pdf({ + format: 'A4', + printBackground: true + }); - // Terceiro: desenhar template e conteúdo - newPage.drawPage(page); - newPage.drawPage(contentPage); + console.log('PDF base gerado com sucesso'); + } finally { + await browser.close(); } - - // 6. Gerar PDF final - const pdfBytes = await pdfDoc.save(); - return Buffer.from(pdfBytes); - + + return pdfBuffer; + } catch (error) { console.error('Erro ao gerar PDF:', error); - throw new Error('Falha ao gerar PDF'); + + // Fallback simplificado + try { + console.log('Tentando método alternativo com pdf-lib...'); + + const pdfDoc = await PDFDocument.create(); + const page = pdfDoc.addPage([595, 842]); // A4 + const font = await pdfDoc.embedFont(StandardFonts.Helvetica); + + // Adicionar cabeçalho simples + page.drawText('Documento Gerado pelo Sistema', { + x: 50, + y: 800, + size: 16, + font + }); + + // Adicionar o conteúdo como texto plano + const plainText = html.replace(/<[^>]*>/g, ' ').slice(0, 5000); + const linhas = []; + for (let i = 0; i < plainText.length; i += 80) { + linhas.push(plainText.substring(i, i + 80)); + } + + linhas.forEach((linha, index) => { + page.drawText(linha, { + x: 50, + y: 750 - (index * 15), + size: 10, + font + }); + }); + + const bytes = await pdfDoc.save(); + return Buffer.from(bytes); + } catch (fallbackError) { + console.error('Erro no método alternativo:', fallbackError); + throw new Error('Falha ao gerar PDF'); + } } } }