Monte a proposta comercial
Preencha o cliente, escolha os serviços e envie o PDF por e-mail.
1. Dados do cliente
2. Produtos e serviços
3. Itens selecionados
| Serviço | Qtd. | Valor | Total |
|---|
/** * Montador de Propostas UserVoz * * Instalação: * npm install nodemailer pdfkit * * Execução: * SMTP_PASSWORD="NOVA_SENHA" node montador-uservoz.mjs */ import http from "node:http"; import crypto from "node:crypto"; import nodemailer from "nodemailer"; import PDFDocument from "pdfkit"; const PORT = Number(process.env.PORT || 3000); const SMTP = { host: process.env.SMTP_HOST || "smtp.hostinger.com", port: Number(process.env.SMTP_PORT || 465), user: process.env.SMTP_USER || "financeiro@uservoz.com", password: process.env.SMTP_PASSWORD, fromName: process.env.SMTP_FROM_NAME || "UserVoz Telecomunicações", cc: process.env.SMTP_CC || "comercial@uservoz.net", }; const enviosRecentes = new Map(); const transportador = SMTP.password ? nodemailer.createTransport({ host: SMTP.host, port: SMTP.port, // Porta 465 usa SSL/TLS direto. secure: SMTP.port === 465, // Porta 587 exige STARTTLS. requireTLS: SMTP.port === 587, auth: { user: SMTP.user, pass: SMTP.password, }, connectionTimeout: 15000, greetingTimeout: 10000, socketTimeout: 30000, // Impede leitura de anexos externos. disableFileAccess: true, disableUrlAccess: true, // Não registrar conteúdo SMTP. logger: false, debug: false, }) : null; function emailValido(email) { return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/i.test( String(email || "").trim() ); } function dinheiro(valor) { return new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL", }).format(Number(valor) || 0); } function escaparHTML(valor = "") { return String(valor).replace(/[&<>"']/g, (caractere) => { const mapa = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }; return mapa[caractere]; }); } function gerarPDF(proposta) { return new Promise((resolve, reject) => { const documento = new PDFDocument({ size: "A4", margin: 48, info: { Title: `Proposta Comercial ${proposta.numero}`, Author: "UserVoz Telecomunicações", }, }); const partes = []; documento.on("data", (parte) => partes.push(parte)); documento.on("end", () => resolve(Buffer.concat(partes))); documento.on("error", reject); // Cabeçalho documento.rect(0, 0, 595, 105).fill("#0d2f5d"); documento .fillColor("#ffffff") .font("Helvetica-Bold") .fontSize(25) .text("UserVoz", 48, 33); documento .fillColor("#55c7e8") .font("Helvetica") .fontSize(9) .text("TELECOMUNICAÇÕES", 49, 65); documento .fillColor("#ffffff") .font("Helvetica-Bold") .fontSize(16) .text("PROPOSTA COMERCIAL", 350, 42, { width: 195, align: "right", }); // Identificação da proposta documento .fillColor("#17243a") .font("Helvetica-Bold") .fontSize(15) .text(`Proposta ${proposta.numero}`, 48, 130); documento .font("Helvetica") .fontSize(9) .fillColor("#66758b") .text( `Data de emissão: ${new Date().toLocaleDateString("pt-BR")}` ) .text("Validade da proposta: 10 dias"); // Dados do cliente documento .moveDown(1.5) .font("Helvetica-Bold") .fontSize(12) .fillColor("#0d2f5d") .text("DADOS DO CLIENTE"); documento .moveDown(0.4) .font("Helvetica") .fontSize(10) .fillColor("#17243a") .text(`Empresa: ${proposta.empresa || "Não informado"}`) .text(`CNPJ: ${proposta.cnpj || "Não informado"}`) .text(`Contato: ${proposta.contato || "Não informado"}`) .text(`E-mail: ${proposta.email}`) .text(`Telefone: ${proposta.telefone || "Não informado"}`) .text(`Endereço: ${proposta.endereco || "Não informado"}`); // Produtos documento .moveDown(1.4) .font("Helvetica-Bold") .fontSize(12) .fillColor("#0d2f5d") .text("PRODUTOS E SERVIÇOS"); documento.moveDown(0.5); const posicaoX = 48; const larguras = [265, 55, 90, 90]; const cabecalhos = [ "Serviço", "Qtd.", "Valor unitário", "Total", ]; let posicaoY = documento.y; documento .rect(posicaoX, posicaoY, 500, 25) .fill("#eaf3ff"); cabecalhos.forEach((cabecalho, indice) => { const x = posicaoX + larguras .slice(0, indice) .reduce((total, largura) => total + largura, 0); documento .fillColor("#0d2f5d") .font("Helvetica-Bold") .fontSize(8) .text(cabecalho, x + 5, posicaoY + 8, { width: larguras[indice] - 10, }); }); posicaoY += 25; proposta.itens.forEach((item) => { if (posicaoY > 700) { documento.addPage(); posicaoY = 55; } const totalItem = Number(item.quantidade) * Number(item.valor); const valores = [ item.nome, item.quantidade, dinheiro(item.valor), dinheiro(totalItem), ]; documento .rect(posicaoX, posicaoY, 500, 31) .strokeColor("#dfe6ef") .stroke(); valores.forEach((valor, indice) => { const x = posicaoX + larguras .slice(0, indice) .reduce((total, largura) => total + largura, 0); documento .fillColor("#17243a") .font("Helvetica") .fontSize(8) .text(String(valor), x + 5, posicaoY + 9, { width: larguras[indice] - 10, }); }); posicaoY += 31; }); documento.y = posicaoY + 16; documento .font("Helvetica") .fontSize(10) .fillColor("#66758b") .text(`Condição de pagamento: ${proposta.condicao}`); if (Number(proposta.desconto) > 0) { documento.text( `Desconto aplicado: ${proposta.desconto}%` ); } documento .moveDown(0.5) .font("Helvetica-Bold") .fontSize(17) .fillColor("#0d2f5d") .text(`Investimento total: ${dinheiro(proposta.total)}`, { align: "right", }); if (proposta.observacoes) { documento .moveDown(1.2) .font("Helvetica-Bold") .fontSize(11) .fillColor("#0d2f5d") .text("OBSERVAÇÕES"); documento .moveDown(0.3) .font("Helvetica") .fontSize(9) .fillColor("#17243a") .text(proposta.observacoes); } documento .moveDown(2) .font("Helvetica") .fontSize(8) .fillColor("#66758b") .text( "UserVoz Telecomunicações • WhatsApp (31) 3500-3607 • Telefone (31) 3500-3608", { align: "center" } ); documento.end(); }); } function criarTemplateEmail(proposta) { const nome = proposta.contato || proposta.empresa || "cliente"; return `
Preencha o cliente, escolha os serviços e envie o PDF por e-mail.
| Serviço | Qtd. | Valor | Total |
|---|