Skip to content

Tutorial: Relatório agrupado com subtotais

Neste tutorial você vai construir um relatório de vendas agrupado por região, com subtotais por grupo e um total geral. O resultado final combina GroupBands, expressões, scripts e agregados.

Exemplo completo: examples/06-grouped-sales

O que você vai aprender

  • Criar groupHeader e groupFooter para agrupar registros
  • Usar um script afterQuery para ordenar dados antes da renderização
  • Calcular campos com expressões (quantity * price)
  • Criar agregados de grupo (SUM por região) e globais

1. Estrutura dos dados

Crie um data.json com registros de vendas. Cada registro tem uma region que será usada para agrupamento:

json
{
  "sales": [
    { "region": "Sudeste", "seller": "Ana Silva", "product": "Notebook Pro", "quantity": 3, "price": 4500.00 },
    { "region": "Sudeste", "seller": "Carlos Lima", "product": "Monitor 27\"", "quantity": 5, "price": 1800.00 },
    { "region": "Sul", "seller": "Marina Costa", "product": "Teclado Mec.", "quantity": 10, "price": 350.00 },
    { "region": "Nordeste", "seller": "João Santos", "product": "Mouse Gamer", "quantity": 8, "price": 250.00 }
  ]
}

2. Definir o data source e registrar script de ordenação

No report.json, declare o data source e um script que ordenará os dados antes da renderização:

json
{
  "dataSources": [
    {
      "id": "sales",
      "type": "list",
      "schema": {
        "region": "string",
        "seller": "string",
        "product": "string",
        "quantity": "integer",
        "price": "number"
      }
    }
  ],
  "scripts": [
    { "id": "ordenar_por_regiao", "hook": "afterQuery" }
  ]
}

Registre o handler em Dart antes de gerar o relatório:

dart
engine.register('ordenar_por_regiao', (ctx) {
  final rows = ctx.datasource('sales')
    ..sort((a, b) => (a['region'] as String).compareTo(b['region'] as String));
  ctx.setDatasource('sales', rows);
});

A ordenação garante que todas as vendas da mesma região fiquem juntas — necessário para o GroupBand funcionar corretamente.

3. Criar o GroupHeader

O groupHeader aparece uma vez no início de cada grupo. Use o campo groupBy para indicar qual campo define o grupo:

json
{
  "type": "groupHeader",
  "id": "group_region_header",
  "dataSourceId": "sales",
  "groupBy": "region",
  "height": 35,
  "backgroundColor": "1565c0",
  "elements": [
    {
      "type": "field",
      "id": "region_name",
      "x": 40,
      "y": 8,
      "width": 300,
      "binding": "region",
      "fontSize": 14,
      "bold": true,
      "color": "ffffff"
    }
  ]
}

4. Criar a banda de detalhe com expressão

No detalhe, exiba os dados de cada venda. O campo total é calculado com uma expressão aritmética que o motor avalia em tempo de render:

json
{
  "type": "detail",
  "id": "detail_sales",
  "dataSourceId": "sales",
  "height": 25,
  "elements": [
    {
      "type": "field",
      "id": "col_seller",
      "x": 40,
      "y": 5,
      "width": 140,
      "binding": "seller"
    },
    {
      "type": "field",
      "id": "col_product",
      "x": 190,
      "y": 5,
      "width": 160,
      "binding": "product"
    },
    {
      "type": "field",
      "id": "col_qty",
      "x": 360,
      "y": 5,
      "width": 50,
      "binding": "quantity",
      "format": "integer",
      "align": "center"
    },
    {
      "type": "field",
      "id": "col_total",
      "x": 420,
      "y": 5,
      "width": 100,
      "expression": "quantity * price",
      "format": "currency:BRL",
      "align": "right"
    }
  ]
}

Expressões

O campo expression aceita operações aritméticas com campos do registro: quantity * price, price * 0.9, revenue - expenses, etc.

5. Criar o GroupFooter com subtotais

O groupFooter aparece ao final de cada grupo. Use aggregate para calcular o subtotal:

json
{
  "type": "groupFooter",
  "id": "group_region_footer",
  "dataSourceId": "sales",
  "groupBy": "region",
  "height": 30,
  "backgroundColor": "e3f2fd",
  "elements": [
    {
      "type": "text",
      "id": "subtotal_label",
      "x": 300,
      "y": 7,
      "content": "Subtotal:",
      "fontSize": 10,
      "bold": true,
      "color": "1565c0"
    },
    {
      "type": "aggregate",
      "id": "subtotal_value",
      "x": 420,
      "y": 7,
      "width": 100,
      "verb": "SUM",
      "dataSourceId": "sales",
      "targetKey": "price",
      "format": "currency:BRL",
      "bold": true,
      "color": "1565c0",
      "align": "right"
    }
  ]
}

6. Adicionar o Summary com total geral

O summary aparece uma vez após todos os grupos:

json
{
  "type": "summary",
  "id": "summary_totals",
  "height": 50,
  "elements": [
    {
      "type": "text",
      "id": "grand_total_label",
      "x": 300,
      "y": 15,
      "content": "TOTAL GERAL:",
      "fontSize": 13,
      "bold": true
    },
    {
      "type": "aggregate",
      "id": "grand_total",
      "x": 420,
      "y": 15,
      "width": 100,
      "verb": "SUM",
      "dataSourceId": "sales",
      "targetKey": "price",
      "format": "currency:BRL",
      "fontSize": 13,
      "bold": true,
      "align": "right"
    }
  ]
}

7. Gerar em Dart

dart
import 'dart:convert';
import 'dart:io';
import 'package:sulfite_core/sulfite_core.dart';

void main() async {
  final engine = SulfiteEngineImpl();
  final reportJson = File('examples/06-grouped-sales/report.json').readAsStringSync();
  final dataJson = jsonDecode(File('examples/06-grouped-sales/data.json').readAsStringSync());

  final report = await engine.parseReport(reportJson);
  final context = await engine.processData(report, dataJson);

  // Gerar em todos os formatos
  final pdf = await engine.renderToPdf(context);
  final html = await engine.renderToHtml(context);
  final csv = await engine.renderToCsv(context);
  final xlsx = await engine.renderToExcel(context);

  File('vendas_agrupadas.pdf').writeAsBytesSync(pdf);
  print('Gerado em 4 formatos!');
}

8. Resultado

O PDF terá a seguinte estrutura visual:

┌─────────────────────────────────────────────┐
│  RELATÓRIO DE VENDAS POR REGIÃO             │  ← header
├─────────────────────────────────────────────┤
│  ▌ SUDESTE                                  │  ← groupHeader (azul)
│   Ana Silva     Notebook Pro   3   R$13.500 │  ← detail
│   Carlos Lima   Monitor 27"   5   R$ 9.000  │  ← detail
│                        Subtotal: R$ 22.500  │  ← groupFooter
├─────────────────────────────────────────────┤
│  ▌ SUL                                      │  ← groupHeader
│   Marina Costa  Teclado Mec.  10  R$ 3.500  │  ← detail
│                        Subtotal: R$  3.500  │  ← groupFooter
├─────────────────────────────────────────────┤
│              TOTAL GERAL: R$ 28.000         │  ← summary
└─────────────────────────────────────────────┘

Dicas

  • Sempre ordene os dados pelo campo groupBy usando um transform sort — caso contrário os grupos podem ficar fragmentados
  • Combine groupHeader + groupFooter com forcePageBreakBefore: true para que cada grupo comece em uma nova página
  • Use printWhen no groupFooter para ocultar subtotais de grupos com poucos itens
  • Agregados no groupFooter calculam apenas os registros do grupo; no summary, calculam todos

Próximos passos

Sulfite do 🇧🇷 para o mundo © 2026 Rafael S. Pinheiro