Use cases
Relatórios
Gere relatórios analíticos e operacionais com gráficos e tabelas
Este guia mostra como gerar relatórios profissionais com dados e visualizações.
Tipos de Relatórios
- ✅ Relatórios financeiros (demonstrativos, balanços)
- ✅ Relatórios de vendas (desempenho, métricas)
- ✅ Relatórios operacionais (inventário, produção)
- ✅ Relatórios analíticos (dashboards, KPIs)
Estrutura de Dados
interface Report {
// Metadados
title: string;
subtitle?: string;
period: {
start: Date;
end: Date;
};
generatedAt: Date;
generatedBy: string;
// Sumário executivo
summary: {
title: string;
metrics: Array<{
label: string;
value: number | string;
change?: number; // % mudança período anterior
trend?: 'up' | 'down' | 'stable';
}>;
};
// Seções de conteúdo
sections: Array<{
title: string;
type: 'table' | 'chart' | 'text' | 'kpi';
content: any;
}>;
// Rodapé
footer?: {
notes?: string;
disclaimer?: string;
};
}
Template HTML (Relatório de Vendas)
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color: #1f2937;
line-height: 1.5;
padding: 40px;
}
.header {
border-bottom: 4px solid #3b82f6;
padding-bottom: 20px;
margin-bottom: 30px;
}
.report-title {
font-size: 28px;
font-weight: bold;
color: #1f2937;
margin-bottom: 5px;
}
.report-subtitle {
font-size: 16px;
color: #6b7280;
}
.report-meta {
font-size: 12px;
color: #9ca3af;
margin-top: 10px;
}
.kpi-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 40px;
}
.kpi-card {
background: #f9fafb;
border-left: 4px solid #3b82f6;
padding: 20px;
border-radius: 8px;
}
.kpi-label {
font-size: 12px;
text-transform: uppercase;
color: #6b7280;
margin-bottom: 8px;
font-weight: 600;
}
.kpi-value {
font-size: 24px;
font-weight: bold;
color: #1f2937;
margin-bottom: 5px;
}
.kpi-change {
font-size: 12px;
font-weight: 600;
}
.kpi-change.positive {
color: #059669;
}
.kpi-change.negative {
color: #dc2626;
}
.section {
margin-bottom: 40px;
}
.section-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
color: #1f2937;
padding-bottom: 8px;
border-bottom: 2px solid #e5e7eb;
}
.data-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.data-table th {
background: #f3f4f6;
padding: 12px;
text-align: left;
font-size: 11px;
text-transform: uppercase;
color: #6b7280;
font-weight: 600;
border-bottom: 2px solid #e5e7eb;
}
.data-table td {
padding: 10px 12px;
border-bottom: 1px solid #e5e7eb;
font-size: 13px;
}
.data-table tr:hover {
background: #f9fafb;
}
.text-right {
text-align: right;
}
.chart-container {
margin: 20px 0;
text-align: center;
}
.chart-image {
max-width: 100%;
height: auto;
}
.footer {
margin-top: 50px;
padding-top: 20px;
border-top: 1px solid #e5e7eb;
font-size: 11px;
color: #6b7280;
}
.page-break {
page-break-after: always;
}
</style>
</head>
<body>
<div class="header">
<div class="report-title">{{title}}</div>
{{#if subtitle}}
<div class="report-subtitle">{{subtitle}}</div>
{{/if}}
<div class="report-meta">
Período: {{formatDate period.start}} a {{formatDate period.end}} |
Gerado em: {{formatDateTime generatedAt}} por {{generatedBy}}
</div>
</div>
<!-- KPIs -->
{{#if summary.metrics}}
<div class="kpi-grid">
{{#each summary.metrics}}
<div class="kpi-card">
<div class="kpi-label">{{label}}</div>
<div class="kpi-value">{{value}}</div>
{{#if change}}
<div class="kpi-change {{#if (gt change 0)}}positive{{else}}negative{{/if}}">
{{#if (gt change 0)}}↑{{else}}↓{{/if}} {{abs change}}%
</div>
{{/if}}
</div>
{{/each}}
</div>
{{/if}}
<!-- Seções dinâmicas -->
{{#each sections}}
<div class="section">
<h2 class="section-title">{{title}}</h2>
{{#if (eq type "table")}}
<table class="data-table">
<thead>
<tr>
{{#each content.headers}}
<th class="{{align}}">{{this}}</th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each content.rows}}
<tr>
{{#each this}}
<td class="{{@../content.columnAlign.[@@index]}}">{{this}}</td>
{{/each}}
</tr>
{{/each}}
</tbody>
</table>
{{/if}}
{{#if (eq type "chart")}}
<div class="chart-container">
<img src="{{content.imageUrl}}" alt="{{title}}" class="chart-image">
</div>
{{/if}}
{{#if (eq type "text")}}
<div class="text-content">
{{{content.html}}}
</div>
{{/if}}
</div>
{{/each}}
{{#if footer}}
<div class="footer">
{{#if footer.notes}}
<p><strong>Observações:</strong> {{footer.notes}}</p>
{{/if}}
{{#if footer.disclaimer}}
<p style="margin-top: 10px;"><em>{{footer.disclaimer}}</em></p>
{{/if}}
</div>
{{/if}}
</body>
</html>
Implementação com Gráficos
Usando QuickChart para Gráficos
import { RenderHubClient } from '@renderhub/sdk';
export class ReportService {
private renderHub: RenderHubClient;
constructor() {
this.renderHub = new RenderHubClient({
apiKey: process.env.RENDERHUB_API_KEY!,
});
}
async generateSalesReport(
startDate: Date,
endDate: Date
): Promise<Buffer> {
// 1. Buscar dados
const salesData = await this.getSalesData(startDate, endDate);
const previousPeriodData = await this.getPreviousPeriodData(startDate, endDate);
// 2. Calcular métricas
const totalRevenue = salesData.reduce((sum, s) => sum + s.revenue, 0);
const previousRevenue = previousPeriodData.reduce((sum, s) => sum + s.revenue, 0);
const revenueChange = ((totalRevenue - previousRevenue) / previousRevenue) * 100;
const totalOrders = salesData.length;
const previousOrders = previousPeriodData.length;
const ordersChange = ((totalOrders - previousOrders) / previousOrders) * 100;
// 3. Gerar gráfico de linha (receita ao longo do tempo)
const revenueChartUrl = this.generateLineChart(
salesData.map(s => s.date),
salesData.map(s => s.revenue),
'Receita Diária'
);
// 4. Gerar gráfico de pizza (produtos mais vendidos)
const topProducts = this.getTopProducts(salesData);
const productsChartUrl = this.generatePieChart(
topProducts.map(p => p.name),
topProducts.map(p => p.quantity),
'Top 5 Produtos'
);
// 5. Montar relatório
const reportData: Report = {
title: 'Relatório de Vendas',
subtitle: 'Análise de Performance',
period: { start: startDate, end: endDate },
generatedAt: new Date(),
generatedBy: 'Sistema',
summary: {
title: 'Resumo Executivo',
metrics: [
{
label: 'Receita Total',
value: `R$ ${totalRevenue.toFixed(2)}`,
change: revenueChange,
trend: revenueChange > 0 ? 'up' : 'down',
},
{
label: 'Total de Pedidos',
value: totalOrders.toString(),
change: ordersChange,
trend: ordersChange > 0 ? 'up' : 'down',
},
{
label: 'Ticket Médio',
value: `R$ ${(totalRevenue / totalOrders).toFixed(2)}`,
},
{
label: 'Taxa de Conversão',
value: '3.2%',
change: 0.5,
trend: 'up',
},
],
},
sections: [
{
title: 'Evolução da Receita',
type: 'chart',
content: {
imageUrl: revenueChartUrl,
},
},
{
title: 'Produtos Mais Vendidos',
type: 'chart',
content: {
imageUrl: productsChartUrl,
},
},
{
title: 'Detalhamento de Vendas',
type: 'table',
content: {
headers: ['Data', 'Cliente', 'Produto', 'Quantidade', 'Valor'],
rows: salesData.map(s => [
s.date.toLocaleDateString('pt-BR'),
s.customerName,
s.productName,
s.quantity,
`R$ ${s.revenue.toFixed(2)}`,
]),
columnAlign: ['', '', '', 'text-right', 'text-right'],
},
},
],
footer: {
disclaimer:
'Este relatório é confidencial e destinado apenas ao uso interno.',
},
};
// 6. Gerar PDF
const pdf = await this.renderHub.generatePDF('report-v1', reportData);
return pdf;
}
private generateLineChart(
labels: Date[],
data: number[],
title: string
): string {
const chartConfig = {
type: 'line',
data: {
labels: labels.map(d => d.toLocaleDateString('pt-BR')),
datasets: [
{
label: title,
data: data,
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
fill: true,
},
],
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
},
},
},
};
// QuickChart.io - serviço gratuito para gerar gráficos
const chartUrl = `https://quickchart.io/chart?c=${encodeURIComponent(
JSON.stringify(chartConfig)
)}&width=800&height=400`;
return chartUrl;
}
private generatePieChart(
labels: string[],
data: number[],
title: string
): string {
const chartConfig = {
type: 'pie',
data: {
labels: labels,
datasets: [
{
data: data,
backgroundColor: [
'rgb(59, 130, 246)',
'rgb(16, 185, 129)',
'rgb(245, 158, 11)',
'rgb(239, 68, 68)',
'rgb(139, 92, 246)',
],
},
],
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: title,
},
},
},
};
const chartUrl = `https://quickchart.io/chart?c=${encodeURIComponent(
JSON.stringify(chartConfig)
)}&width=600&height=400`;
return chartUrl;
}
private async getSalesData(start: Date, end: Date): Promise<any[]> {
// Implementar busca do banco de dados
throw new Error('Not implemented');
}
private async getPreviousPeriodData(start: Date, end: Date): Promise<any[]> {
// Implementar busca do banco de dados
throw new Error('Not implemented');
}
private getTopProducts(salesData: any[]): any[] {
// Implementar agregação
throw new Error('Not implemented');
}
}
Relatórios Recorrentes
Job Agendado
import cron from 'node-cron';
import { ReportService } from './report.service';
const reportService = new ReportService();
// Relatório semanal toda segunda às 8am
cron.schedule('0 8 * * 1', async () => {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 7);
const pdf = await reportService.generateSalesReport(startDate, endDate);
// Enviar para stakeholders
await sendEmail({
to: ['ceo@company.com', 'cfo@company.com'],
subject: 'Relatório Semanal de Vendas',
text: 'Segue relatório semanal em anexo.',
attachments: [
{
filename: `sales-report-${startDate.toISOString().split('T')[0]}.pdf`,
content: pdf,
},
],
});
});
Próximos Passos
- Veja como gerar Contratos
- Confira exemplos de Faturas
- Aprenda sobre Certificados