Guides

Guia Completo de Templates

Domine variáveis, arrays, condicionais, loops e mapping em templates HTML e DOCX

Guia Completo de Templates

Templates são o coração do RenderHub. Aprenda a criar templates poderosos e reutilizáveis com variáveis dinâmicas, arrays, condicionais e muito mais.

Por Que Usar Templates?

❌ Sem Templates (Modo CONVERT)

// Gerar 100 faturas → 100x o mesmo código HTML
for (const cliente of clientes) {
  const html = `
    <html>
      <body>
        <h1>Fatura para ${cliente.nome}</h1>
        <p>Total: R$ ${cliente.total}</p>
      </body>
    </html>
  `;

  await renderhub.convert({ input_type: 'html', data: html });
}

Problemas:

  • HTML duplicado em cada requisição
  • Difícil de manter (mudança = alterar código)
  • Payload grande (10 MB limit)
  • Sem versionamento

✅ Com Templates (Modo RENDER)

// Criar template UMA VEZ
const template = await renderhub.createTemplate({
  name: 'fatura',
  content: '<html><body><h1>Fatura para {{customer_name}}</h1></body></html>'
});

// Gerar 100 faturas → enviar apenas dados JSON
for (const cliente of clientes) {
  await renderhub.render({
    template_id: template.id,
    data: { customer_name: cliente.nome, total: cliente.total }
  });
}

Vantagens:

  • Template reutilizável (criado 1x, usado ∞x)
  • Fácil de manter (atualiza template, afeta todas gerações)
  • Payload pequeno (apenas JSON)
  • Versionamento automático

Data Mapping (Mapeamento de Dados)

Data mapping é o processo de transformar dados do seu sistema (banco de dados, API, arquivos) no formato JSON que o template espera.

Conceito de Mapping

Template (estrutura fixa) + Dados (conteúdo dinâmico) = PDF personalizado

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│   Template  │  +  │  Data JSON   │  =  │  PDF Final  │
│  (HTML com  │     │  (valores)   │     │ (renderizado│
│  variáveis) │     │              │     │   completo) │
└─────────────┘     └──────────────┘     └─────────────┘

Exemplo Prático: Mapeando Banco de Dados → Template

1. Template Espera:

{
  customer_name: string,
  customer_email: string,
  invoice_number: string,
  items: [
    { description: string, quantity: number, price: string }
  ],
  total: string
}

2. Dados do Banco:

// Tabela: customers
{
  id: 123,
  nome_cliente: "João Silva",
  email_contato: "joao@email.com",
  // ...
}

// Tabela: pedidos
[
  { produto_nome: "Notebook", qtd: 2, valor_unitario: 3500.00 },
  { produto_nome: "Mouse", qtd: 1, valor_unitario: 50.00 }
]

3. Função de Mapping:

async function mapCustomerToInvoice(customerId) {
  // Buscar dados do banco
  const customer = await db.customers.findById(customerId);
  const orders = await db.orders.findByCustomer(customerId);

  // MAPPING: Transformar estrutura do banco → estrutura do template
  return {
    // Mapear nome do campo
    customer_name: customer.nome_cliente,
    customer_email: customer.email_contato,

    // Gerar dados derivados
    invoice_number: `INV-${new Date().getFullYear()}-${customerId}`,

    // Mapear array complexo
    items: orders.map(order => ({
      description: order.produto_nome,
      quantity: order.qtd,
      price: order.valor_unitario.toFixed(2)
    })),

    // Calcular totais
    total: orders
      .reduce((sum, o) => sum + (o.qtd * o.valor_unitario), 0)
      .toFixed(2)
  };
}

// Usar o mapping
const invoiceData = await mapCustomerToInvoice(123);
await renderhub.render({
  template_id: 'tpl_invoice',
  data: invoiceData  // Dados já mapeados!
});

Estratégias de Mapping

1. Mapping Direto (Field-to-Field)

Quando campos do banco correspondem aos do template:

// Banco de dados
const user = {
  name: "Maria",
  age: 30,
  city: "São Paulo"
};

// Mapping direto (nomes iguais)
const templateData = {
  name: user.name,
  age: user.age,
  city: user.city
};

// Ou usando spread (mesmo nome de campos)
const templateData = { ...user };

2. Mapping com Transformação

Quando precisa transformar/formatar dados:

const product = {
  name: "NOTEBOOK DELL",
  price_cents: 350000,  // Preço em centavos
  created_at: "2024-01-15T10:30:00Z"
};

// Mapping com transformação
const templateData = {
  product_name: product.name.toLowerCase(),  // Minúsculas
  product_price: (product.price_cents / 100).toFixed(2),  // Centavos → Reais
  created_date: new Date(product.created_at).toLocaleDateString('pt-BR')  // Formatar data
};

// Resultado:
// {
//   product_name: "notebook dell",
//   product_price: "3500.00",
//   created_date: "15/01/2024"
// }

3. Mapping com Agregação

Quando precisa combinar/agregar múltiplos dados:

const student = { name: "Pedro", course_id: 5 };
const course = { id: 5, name: "Python Avançado", hours: 40 };
const grades = [9.5, 8.0, 10.0, 9.0];

// Mapping agregado
const certificateData = {
  student_name: student.name,
  course_name: course.name,
  course_hours: course.hours,
  final_grade: (grades.reduce((a, b) => a + b) / grades.length).toFixed(1),
  has_distinction: grades.every(g => g >= 7.0)
};

// Resultado:
// {
//   student_name: "Pedro",
//   course_name: "Python Avançado",
//   course_hours: 40,
//   final_grade: "9.1",
//   has_distinction: true
// }

4. Mapping de Arrays (Join de Tabelas)

Quando precisa combinar dados de múltiplas tabelas:

// Tabela: orders
const order = { id: 1, customer_id: 100, date: "2024-01-15" };

// Tabela: order_items (1:N)
const items = [
  { order_id: 1, product_id: 10, quantity: 2 },
  { order_id: 1, product_id: 20, quantity: 1 }
];

// Tabela: products
const products = [
  { id: 10, name: "Teclado", price: 150.00 },
  { id: 20, name: "Mouse", price: 50.00 }
];

// Mapping com JOIN
const invoiceData = {
  order_id: order.id,
  order_date: order.date,
  items: items.map(item => {
    const product = products.find(p => p.id === item.product_id);
    return {
      description: product.name,
      quantity: item.quantity,
      unit_price: product.price.toFixed(2),
      total: (item.quantity * product.price).toFixed(2)
    };
  }),
  grand_total: items.reduce((sum, item) => {
    const product = products.find(p => p.id === item.product_id);
    return sum + (item.quantity * product.price);
  }, 0).toFixed(2)
};

// Resultado:
// {
//   order_id: 1,
//   order_date: "2024-01-15",
//   items: [
//     { description: "Teclado", quantity: 2, unit_price: "150.00", total: "300.00" },
//     { description: "Mouse", quantity: 1, unit_price: "50.00", total: "50.00" }
//   ],
//   grand_total: "350.00"
// }

Mapping de APIs Externas

Quando consome dados de APIs de terceiros:

// API retorna estrutura diferente
const apiResponse = {
  user: {
    fullName: "Ana Costa",
    contact: {
      emailAddress: "ana@example.com",
      phoneNumber: "+55 11 98765-4321"
    },
    address: {
      streetAddress: "Rua das Flores, 123",
      cityName: "São Paulo",
      stateCode: "SP"
    }
  }
};

// Mapping: API → Template (achatar estrutura)
const contractData = {
  client_name: apiResponse.user.fullName,
  client_email: apiResponse.user.contact.emailAddress,
  client_phone: apiResponse.user.contact.phoneNumber,
  client_address: `${apiResponse.user.address.streetAddress}, ${apiResponse.user.address.cityName} - ${apiResponse.user.address.stateCode}`
};

Mapping com Validação

Sempre valide dados antes de enviar para o template:

function mapAndValidate(rawData) {
  // Validar campos obrigatórios
  if (!rawData.customer_name) {
    throw new Error('customer_name é obrigatório');
  }

  if (!rawData.items || rawData.items.length === 0) {
    throw new Error('items deve ter pelo menos 1 item');
  }

  // Validar tipos
  if (typeof rawData.total !== 'number') {
    throw new Error('total deve ser um número');
  }

  // Mapping com valores padrão
  return {
    customer_name: rawData.customer_name,
    customer_email: rawData.customer_email || 'nao-informado@example.com',
    items: rawData.items.map(item => ({
      description: item.description || 'Sem descrição',
      quantity: item.quantity || 1,
      price: (item.price || 0).toFixed(2)
    })),
    total: rawData.total.toFixed(2),
    discount: rawData.discount ? rawData.discount.toFixed(2) : null
  };
}

Mapping com TypeScript

Use tipos para garantir correção do mapping:

// Tipo do banco de dados
interface CustomerDB {
  id: number;
  nome_cliente: string;
  email_contato: string;
}

// Tipo do template
interface InvoiceTemplate {
  customer_name: string;
  customer_email: string;
  invoice_number: string;
}

// Função de mapping tipada
function mapCustomer(customer: CustomerDB): InvoiceTemplate {
  return {
    customer_name: customer.nome_cliente,
    customer_email: customer.email_contato,
    invoice_number: `INV-${customer.id}`
  };
}

// TypeScript garante que todos os campos estão presentes!

Mapping Reutilizável

Crie funções de mapping reutilizáveis:

// mappers/invoice.js
export function mapOrderToInvoice(order, customer, items) {
  return {
    invoice_number: `${new Date().getFullYear()}-${order.id}`,
    invoice_date: formatDate(order.created_at),
    customer_name: customer.name,
    customer_email: customer.email,
    items: items.map(mapOrderItem),
    subtotal: calculateSubtotal(items),
    tax: calculateTax(items),
    total: calculateTotal(items)
  };
}

function mapOrderItem(item) {
  return {
    description: item.product_name,
    quantity: item.qty,
    unit_price: item.price.toFixed(2),
    total: (item.qty * item.price).toFixed(2)
  };
}

// Usar em qualquer lugar
const invoiceData = mapOrderToInvoice(order, customer, items);
await renderhub.render({ template_id: 'tpl_invoice', data: invoiceData });

Melhores Práticas de Mapping

Faça:

  • Crie funções de mapping dedicadas
  • Valide dados antes de mapear
  • Use valores padrão para campos opcionais
  • Documente o schema esperado pelo template
  • Use TypeScript para type safety
  • Teste mapping com dados reais e edge cases

Não Faça:

  • Fazer mapping inline (dificulta manutenção)
  • Ignorar validação de dados
  • Assumir que dados sempre existem
  • Duplicar lógica de mapping em múltiplos lugares

Sintaxe de Templates (Handlebars)

RenderHub usa Handlebars para templating.

Variáveis Simples

<!-- Template -->
<h1>Olá, {{name}}!</h1>
<p>Email: {{email}}</p>
<p>Idade: {{age}} anos</p>
// Dados
{
  name: "João Silva",
  email: "joao@email.com",
  age: 30
}
<!-- Resultado -->
<h1>Olá, João Silva!</h1>
<p>Email: joao@email.com</p>
<p>Idade: 30 anos</p>

Variáveis Aninhadas (Objetos)

<!-- Template -->
<p>{{user.profile.name}}</p>
<p>{{user.profile.address.city}}</p>
<p>{{company.info.cnpj}}</p>
// Dados
{
  user: {
    profile: {
      name: "Maria Santos",
      address: {
        street: "Rua das Flores, 123",
        city: "São Paulo",
        state: "SP"
      }
    }
  },
  company: {
    info: {
      cnpj: "12.345.678/0001-99"
    }
  }
}

Arrays e Loops (#each)

<!-- Template -->
<h2>Produtos</h2>
<ul>
  {{#each products}}
    <li>
      {{this.name}} - R$ {{this.price}}
      {{#if this.inStock}}
        <span style="color: green;">✓ Em estoque</span>
      {{else}}
        <span style="color: red;">✗ Indisponível</span>
      {{/if}}
    </li>
  {{/each}}
</ul>
// Dados
{
  products: [
    { name: "Notebook", price: "3500.00", inStock: true },
    { name: "Mouse", price: "50.00", inStock: true },
    { name: "Teclado", price: "150.00", inStock: false }
  ]
}
<!-- Resultado -->
<h2>Produtos</h2>
<ul>
  <li>
    Notebook - R$ 3500.00
    <span style="color: green;">✓ Em estoque</span>
  </li>
  <li>
    Mouse - R$ 50.00
    <span style="color: green;">✓ Em estoque</span>
  </li>
  <li>
    Teclado - R$ 150.00
    <span style="color: red;">✗ Indisponível</span>
  </li>
</ul>

Índice e Posição em Arrays

{{#each items}}
  <p>Item {{@index}} (começando em 0): {{this.name}}</p>
  <p>Posição {{@number}} (começando em 1): {{this.name}}</p>

  {{#if @first}}
    <strong>Este é o primeiro item!</strong>
  {{/if}}

  {{#if @last}}
    <strong>Este é o último item!</strong>
  {{/if}}
{{/each}}
// Dados
{
  items: [
    { name: "Primeiro" },
    { name: "Segundo" },
    { name: "Terceiro" }
  ]
}

Condicionais (#if, #unless)

<!-- IF simples -->
{{#if isPremium}}
  <div class="premium-badge">⭐ Cliente Premium</div>
{{/if}}

<!-- IF / ELSE -->
{{#if isActive}}
  <span style="color: green;">Ativo</span>
{{else}}
  <span style="color: red;">Inativo</span>
{{/if}}

<!-- IF / ELSE IF / ELSE -->
{{#if score >= 90}}
  <p>Aprovado com Distinção</p>
{{else if score >= 70}}
  <p>Aprovado</p>
{{else if score >= 50}}
  <p>Recuperação</p>
{{else}}
  <p>Reprovado</p>
{{/if}}

<!-- UNLESS (negação do IF) -->
{{#unless isBlocked}}
  <button>Fazer Pedido</button>
{{/unless}}
<!-- Equivale a: {{#if (not isBlocked)}} -->

Comparações e Lógica

<!-- Igualdade -->
{{#if (eq status "approved")}}
  <p>Pedido aprovado!</p>
{{/if}}

<!-- Maior que -->
{{#if (gt age 18)}}
  <p>Maior de idade</p>
{{/if}}

<!-- Menor que -->
{{#if (lt stock 10)}}
  <p style="color: red;">Estoque baixo!</p>
{{/if}}

<!-- E lógico (AND) -->
{{#if (and isPremium isActive)}}
  <p>Cliente Premium Ativo</p>
{{/if}}

<!-- OU lógico (OR) -->
{{#if (or hasDiscount isBirthday)}}
  <p>Você tem desconto especial!</p>
{{/if}}

<!-- Negação (NOT) -->
{{#if (not isExpired)}}
  <p>Válido até {{expiryDate}}</p>
{{/if}}

Operadores disponíveis:

  • eq - Igual (==)
  • ne - Diferente (!=)
  • gt - Maior que (>)
  • gte - Maior ou igual (>=)
  • lt - Menor que (<)
  • lte - Menor ou igual (<=)
  • and - E lógico (&&)
  • or - OU lógico (||)
  • not - Negação (!)

Helpers de Formatação

Datas

<!-- Formatar data -->
<p>{{formatDate createdAt "DD/MM/YYYY"}}</p>
<p>{{formatDate updatedAt "DD/MM/YYYY HH:mm"}}</p>
<p>{{formatDate eventDate "dddd, D [de] MMMM [de] YYYY"}}</p>
// Dados
{
  createdAt: "2024-01-15T10:30:00Z",
  updatedAt: "2024-01-20T15:45:00Z",
  eventDate: "2024-02-10T18:00:00Z"
}
<!-- Resultado -->
<p>15/01/2024</p>
<p>20/01/2024 15:45</p>
<p>sábado, 10 de fevereiro de 2024</p>

Formatos de data disponíveis:

  • DD/MM/YYYY → 15/01/2024
  • MM/DD/YYYY → 01/15/2024
  • YYYY-MM-DD → 2024-01-15
  • DD/MM/YYYY HH:mm → 15/01/2024 10:30
  • dddd, DD [de] MMMM → segunda-feira, 15 de janeiro

Moeda e Números

<!-- Formatar moeda -->
<p>{{formatCurrency total "BRL"}}</p>
<p>{{formatCurrency price "USD"}}</p>
<p>{{formatCurrency value "EUR"}}</p>

<!-- Formatar número -->
<p>{{formatNumber quantity}}</p>
<p>{{formatNumber population 0}}</p> <!-- Sem decimais -->
<p>{{formatNumber percentage 2}}</p> <!-- 2 decimais -->
// Dados
{
  total: 1500.50,
  price: 250,
  value: 999.99,
  quantity: 1234567.89,
  population: 7800000000,
  percentage: 0.8765
}
<!-- Resultado -->
<p>R$ 1.500,50</p>
<p>$ 250.00</p>
<p>€ 999,99</p>
<p>1.234.567,89</p>
<p>7.800.000.000</p>
<p>0,88</p>

Texto

<!-- Maiúsculas -->
<p>{{uppercase name}}</p>

<!-- Minúsculas -->
<p>{{lowercase email}}</p>

<!-- Capitalizar primeira letra -->
<p>{{capitalize title}}</p>

<!-- Truncar texto -->
<p>{{truncate description 50}}</p> <!-- Limita a 50 caracteres -->
<p>{{truncate longText 100 "..."}}</p> <!-- Com sufixo customizado -->
// Dados
{
  name: "joão silva",
  email: "JOAO@EMAIL.COM",
  title: "guia completo",
  description: "Este é um texto muito longo que precisa ser truncado para caber no layout do PDF",
  longText: "Lorem ipsum dolor sit amet consectetur adipiscing elit..."
}
<!-- Resultado -->
<p>JOÃO SILVA</p>
<p>joao@email.com</p>
<p>Guia completo</p>
<p>Este é um texto muito longo que precisa ser trun...</p>
<p>Lorem ipsum dolor sit amet consectetur adipiscing...</p>

Matemática

<!-- Soma -->
<p>Total: {{add price shipping}}</p>

<!-- Subtração -->
<p>Desconto: {{subtract total discount}}</p>

<!-- Multiplicação -->
<p>Subtotal: {{multiply price quantity}}</p>

<!-- Divisão -->
<p>Média: {{divide total count}}</p>

<!-- Operações complexas -->
<p>{{add (multiply price quantity) shipping}}</p>
// Dados
{
  price: 100,
  shipping: 15,
  total: 1000,
  discount: 50,
  quantity: 5,
  count: 10
}

Arrays Complexos

Tabelas com Arrays

<style>
  table { width: 100%; border-collapse: collapse; }
  th { background: #f3f4f6; padding: 12px; text-align: left; }
  td { padding: 12px; border-bottom: 1px solid #e5e7eb; }
  tr:hover { background: #f9fafb; }
</style>

<table>
  <thead>
    <tr>
      <th>#</th>
      <th>Produto</th>
      <th>Qtd</th>
      <th>Valor Unit.</th>
      <th>Total</th>
    </tr>
  </thead>
  <tbody>
    {{#each items}}
    <tr>
      <td>{{@number}}</td>
      <td>
        <strong>{{this.product}}</strong><br>
        <small style="color: #6b7280;">{{this.description}}</small>
      </td>
      <td>{{this.quantity}}x</td>
      <td>{{formatCurrency this.unitPrice "BRL"}}</td>
      <td><strong>{{formatCurrency this.total "BRL"}}</strong></td>
    </tr>
    {{/each}}
  </tbody>
  <tfoot>
    <tr>
      <td colspan="4" style="text-align: right;"><strong>TOTAL:</strong></td>
      <td><strong>{{formatCurrency grandTotal "BRL"}}</strong></td>
    </tr>
  </tfoot>
</table>
// Dados
{
  items: [
    {
      product: "Notebook Dell XPS 15",
      description: "Intel i7, 16GB RAM, 512GB SSD",
      quantity: 2,
      unitPrice: 6500.00,
      total: 13000.00
    },
    {
      product: "Mouse Logitech MX Master",
      description: "Wireless, Ergonômico",
      quantity: 2,
      unitPrice: 350.00,
      total: 700.00
    }
  ],
  grandTotal: 13700.00
}

Arrays Aninhados

<!-- Pedidos com itens -->
{{#each orders}}
  <div class="order">
    <h3>Pedido #{{this.number}} - {{formatDate this.date "DD/MM/YYYY"}}</h3>

    <table>
      {{#each this.items}}
      <tr>
        <td>{{this.name}}</td>
        <td>{{this.quantity}}x</td>
        <td>R$ {{this.price}}</td>
      </tr>
      {{/each}}
    </table>

    <p><strong>Total do pedido: R$ {{this.total}}</strong></p>
  </div>
{{/each}}
// Dados
{
  orders: [
    {
      number: "2024-001",
      date: "2024-01-15",
      items: [
        { name: "Produto A", quantity: 2, price: "100.00" },
        { name: "Produto B", quantity: 1, price: "200.00" }
      ],
      total: "400.00"
    },
    {
      number: "2024-002",
      date: "2024-01-20",
      items: [
        { name: "Produto C", quantity: 5, price: "50.00" }
      ],
      total: "250.00"
    }
  ]
}

Filtrar e Contar Arrays

<!-- Total de itens -->
<p>Total de produtos: {{items.length}}</p>

<!-- Array vazio -->
{{#if items.length}}
  <ul>
    {{#each items}}
      <li>{{this.name}}</li>
    {{/each}}
  </ul>
{{else}}
  <p>Nenhum item encontrado.</p>
{{/if}}

<!-- Primeiro e último item -->
<p>Primeiro: {{items.0.name}}</p>
<p>Último: {{items.[items.length - 1].name}}</p>

Mapping para DOCX

Além de HTML, você pode usar variáveis em templates DOCX!

Como Funciona

  1. Crie um arquivo .docx no Word com placeholders
  2. Faça upload como template
  3. Renderize com dados JSON

Variáveis Simples em DOCX

No Word, use {{variavel}} em qualquer lugar:

Contrato de Prestação de Serviços

Contratante: {{client_name}}
CNPJ: {{client_cnpj}}
Endereço: {{client_address}}

Contratado: {{provider_name}}

Valor: {{formatCurrency amount "BRL"}}
Data: {{formatDate signDate "DD/MM/YYYY"}}

Tabelas em DOCX

Crie uma tabela no Word e use variáveis em cada célula:

| Produto          | Quantidade        | Valor         |
|------------------|-------------------|---------------|
| {{#each items}}  |                   |               |
| {{this.name}}    | {{this.quantity}} | {{this.price}}|
| {{/each}}        |                   |               |

No Word, crie uma linha na tabela com as variáveis {{this.name}}, {{this.quantity}}, {{this.price}} dentro do loop {{#each items}}. RenderHub automaticamente replica a linha para cada item do array!

Condicionais em DOCX

{{#if isPremium}}
CLIENTE PREMIUM

Benefícios especiais:
- Desconto de 10%
- Frete grátis
- Suporte prioritário
{{/if}}

Imagens em DOCX

// Dados
{
  company_logo: "https://exemplo.com/logo.png",
  // ou base64:
  signature: "data:image/png;base64,iVBORw0KGg..."
}

No DOCX, insira uma imagem placeholder e substitua pela URL ou base64.

Casos de Uso Reais

1. Fatura Completa

<html>
  <head>
    <style>
      * { margin: 0; padding: 0; box-sizing: border-box; }
      body { font-family: 'Segoe UI', Arial; padding: 40px; }
      .header {
        display: flex;
        justify-content: space-between;
        align-items: flex-start;
        border-bottom: 3px solid #2563eb;
        padding-bottom: 20px;
        margin-bottom: 30px;
      }
      .company img { max-width: 150px; }
      .invoice-info { text-align: right; }
      .invoice-number {
        font-size: 32px;
        font-weight: bold;
        color: #2563eb;
      }
      .section { margin: 30px 0; }
      .section h2 {
        font-size: 18px;
        color: #374151;
        margin-bottom: 10px;
        border-bottom: 1px solid #e5e7eb;
        padding-bottom: 5px;
      }
      table {
        width: 100%;
        border-collapse: collapse;
        margin: 20px 0;
      }
      th {
        background: #f3f4f6;
        padding: 12px;
        text-align: left;
        font-weight: 600;
      }
      td {
        padding: 12px;
        border-bottom: 1px solid #e5e7eb;
      }
      .totals {
        margin-top: 30px;
        text-align: right;
      }
      .totals table {
        margin-left: auto;
        width: 300px;
      }
      .totals td {
        border: none;
        padding: 8px 0;
      }
      .grand-total {
        font-size: 24px;
        font-weight: bold;
        color: #2563eb;
        border-top: 2px solid #2563eb !important;
        padding-top: 15px !important;
      }
      .footer {
        margin-top: 60px;
        padding-top: 20px;
        border-top: 1px solid #e5e7eb;
        font-size: 12px;
        color: #6b7280;
      }
    </style>
  </head>
  <body>
    <div class="header">
      <div class="company">
        {{#if company_logo}}
          <img src="{{company_logo}}" alt="Logo">
        {{/if}}
        <h1>{{company_name}}</h1>
        <p>{{company_address}}</p>
        <p>CNPJ: {{company_cnpj}}</p>
        <p>Tel: {{company_phone}}</p>
      </div>

      <div class="invoice-info">
        <div class="invoice-number">#{{invoice_number}}</div>
        <p><strong>Data de Emissão:</strong> {{formatDate issue_date "DD/MM/YYYY"}}</p>
        <p><strong>Vencimento:</strong> {{formatDate due_date "DD/MM/YYYY"}}</p>
        {{#if (gt (daysDiff due_date today) 0)}}
          <p style="color: green;">✓ Dentro do prazo</p>
        {{else}}
          <p style="color: red;">⚠ Vencida há {{daysDiff today due_date}} dias</p>
        {{/if}}
      </div>
    </div>

    <div class="section">
      <h2>Cliente</h2>
      <p><strong>{{customer_name}}</strong></p>
      <p>{{customer_document}}</p>
      <p>{{customer_address}}</p>
      <p>{{customer_email}} | {{customer_phone}}</p>
    </div>

    <div class="section">
      <h2>Itens</h2>
      <table>
        <thead>
          <tr>
            <th style="width: 50%;">Descrição</th>
            <th>Qtd</th>
            <th>Valor Unit.</th>
            <th>Desconto</th>
            <th>Total</th>
          </tr>
        </thead>
        <tbody>
          {{#each items}}
          <tr>
            <td>
              <strong>{{this.description}}</strong>
              {{#if this.details}}
                <br><small style="color: #6b7280;">{{this.details}}</small>
              {{/if}}
            </td>
            <td>{{this.quantity}}</td>
            <td>{{formatCurrency this.unit_price "BRL"}}</td>
            <td>
              {{#if this.discount}}
                {{this.discount}}%
              {{else}}
                -
              {{/if}}
            </td>
            <td><strong>{{formatCurrency this.total "BRL"}}</strong></td>
          </tr>
          {{/each}}
        </tbody>
      </table>
    </div>

    <div class="totals">
      <table>
        <tr>
          <td>Subtotal:</td>
          <td><strong>{{formatCurrency subtotal "BRL"}}</strong></td>
        </tr>
        {{#if discount_amount}}
        <tr style="color: #10b981;">
          <td>Desconto:</td>
          <td><strong>- {{formatCurrency discount_amount "BRL"}}</strong></td>
        </tr>
        {{/if}}
        {{#if shipping}}
        <tr>
          <td>Frete:</td>
          <td><strong>{{formatCurrency shipping "BRL"}}</strong></td>
        </tr>
        {{/if}}
        <tr>
          <td>Impostos ({{tax_rate}}%):</td>
          <td><strong>{{formatCurrency tax_amount "BRL"}}</strong></td>
        </tr>
        <tr class="grand-total">
          <td>TOTAL:</td>
          <td>{{formatCurrency total "BRL"}}</td>
        </tr>
      </table>
    </div>

    {{#if payment_method}}
    <div class="section">
      <h2>Informações de Pagamento</h2>
      <p><strong>Método:</strong> {{payment_method}}</p>

      {{#if (eq payment_method "Boleto")}}
        <p><strong>Código de Barras:</strong> {{barcode}}</p>
      {{else if (eq payment_method "PIX")}}
        <p><strong>Chave PIX:</strong> {{pix_key}}</p>
        <img src="{{pix_qrcode}}" alt="QR Code" style="max-width: 200px;">
      {{else if (eq payment_method "Transferência")}}
        <p><strong>Banco:</strong> {{bank_name}}</p>
        <p><strong>Agência:</strong> {{bank_agency}} | <strong>Conta:</strong> {{bank_account}}</p>
      {{/if}}
    </div>
    {{/if}}

    {{#if notes}}
    <div class="section">
      <h2>Observações</h2>
      <p>{{notes}}</p>
    </div>
    {{/if}}

    <div class="footer">
      <p>{{company_name}} | {{company_address}} | CNPJ: {{company_cnpj}}</p>
      <p>Este documento foi gerado eletronicamente e é válido sem assinatura.</p>
    </div>
  </body>
</html>

2. Certificado de Conclusão

<html>
  <head>
    <style>
      @page { size: A4 landscape; }
      body {
        margin: 0;
        padding: 0;
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        color: white;
        font-family: 'Georgia', serif;
        display: flex;
        align-items: center;
        justify-content: center;
        min-height: 100vh;
      }
      .certificate {
        border: 15px solid gold;
        padding: 80px;
        max-width: 900px;
        text-align: center;
        background: rgba(255, 255, 255, 0.1);
        backdrop-filter: blur(10px);
      }
      .title {
        font-size: 56px;
        font-weight: bold;
        margin-bottom: 40px;
        text-transform: uppercase;
        letter-spacing: 4px;
      }
      .student-name {
        font-size: 48px;
        font-weight: bold;
        margin: 40px 0;
        text-transform: uppercase;
        color: gold;
      }
      .course-name {
        font-size: 36px;
        margin: 30px 0;
        font-weight: bold;
      }
      .details {
        font-size: 20px;
        margin: 20px 0;
      }
      .signature {
        display: flex;
        justify-content: space-around;
        margin-top: 80px;
      }
      .signature-block {
        text-align: center;
      }
      .signature-line {
        border-top: 2px solid white;
        width: 250px;
        margin: 10px auto;
      }
    </style>
  </head>
  <body>
    <div class="certificate">
      <div class="title">🎓 Certificado de Conclusão</div>

      <p class="details">Certificamos que</p>

      <div class="student-name">{{student_name}}</div>

      <p class="details">concluiu com sucesso o curso</p>

      <div class="course-name">{{course_name}}</div>

      <p class="details">
        Carga horária: {{course_hours}} horas<br>
        Concluído em {{formatDate completion_date "DD [de] MMMM [de] YYYY"}}
      </p>

      {{#if (gte final_grade 9.0)}}
      <p style="font-size: 28px; color: gold; margin-top: 30px;">
        ⭐ COM DISTINÇÃO ⭐<br>
        Nota Final: {{final_grade}}
      </p>
      {{/if}}

      <div class="signature">
        <div class="signature-block">
          {{#if instructor_signature}}
            <img src="{{instructor_signature}}" style="max-width: 150px;">
          {{/if}}
          <div class="signature-line"></div>
          <p>{{instructor_name}}<br>Instrutor</p>
        </div>

        <div class="signature-block">
          {{#if director_signature}}
            <img src="{{director_signature}}" style="max-width: 150px;">
          {{/if}}
          <div class="signature-line"></div>
          <p>{{director_name}}<br>Diretor Acadêmico</p>
        </div>
      </div>

      <p style="font-size: 14px; margin-top: 40px; opacity: 0.8;">
        Certificado #{{certificate_number}} | Verificar em: {{verification_url}}
      </p>
    </div>
  </body>
</html>

Próximos Passos

On this page