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

On this page