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/2024MM/DD/YYYY→ 01/15/2024YYYY-MM-DD→ 2024-01-15DD/MM/YYYY HH:mm→ 15/01/2024 10:30dddd, 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
- Crie um arquivo
.docxno Word com placeholders - Faça upload como template
- 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
- API de Templates - Criar e gerenciar templates via API
- Tipos de Entrada - HTML, DOCX, Markdown e mais
- Exemplos de Código - Implementações completas