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
groupHeaderegroupFooterpara agrupar registros - Usar um script
afterQuerypara ordenar dados antes da renderização - Calcular campos com expressões (
quantity * price) - Criar agregados de grupo (
SUMpor 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:
{
"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:
{
"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:
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:
{
"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:
{
"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:
{
"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:
{
"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
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
groupByusando um transformsort— caso contrário os grupos podem ficar fragmentados - Combine
groupHeader+groupFootercomforcePageBreakBefore: truepara que cada grupo comece em uma nova página - Use
printWhenno groupFooter para ocultar subtotais de grupos com poucos itens - Agregados no
groupFootercalculam apenas os registros do grupo; nosummary, calculam todos