Logosulfite.app
rafagazani/sulfite 999999

Filtros de impressão (Print Filters)

Parâmetros interativos, lookups dinâmicos (HTTP e banco), paginação, validação declarativa e cascata

Filtros de impressão#

Parâmetros interativos que o usuário preenche antes de imprimir ou exportar o relatório. Definem variáveis injetadas no ScriptEngine e como placeholders em URLs de fontes externas.

Os filtros suportam lookups dinâmicos (opções carregadas via HTTP ou banco de dados), paginação, validação declarativa e dependências em cascata entre parâmetros.

Definindo parâmetros no JSON#

Adicione o campo parameters ao ReportDefinition:

{
  "reportId": "orders_report",
  "reportName": "Pedidos por Período",
  "parameters": [
    {
      "id": "startDate",
      "label": "Data Início",
      "type": "date",
      "defaultValue": "2024-01-01",
      "required": true
    },
    {
      "id": "endDate",
      "label": "Data Fim",
      "type": "date",
      "defaultValue": "2024-12-31",
      "required": true,
      "validation": {
        "rule": "dateAfter",
        "compareWith": "startDate",
        "errorMessage": "Data Fim deve ser posterior à Data Início"
      }
    },
    {
      "id": "status",
      "label": "Status",
      "type": "select",
      "options": ["todos", "aprovado", "pendente"],
      "defaultValue": "todos"
    }
  ],
  "bands": [...]
}

Tipos de parâmetro#

typeDescriçãoEntrada gerada
textTexto livreCampo de texto
number Número (inteiro ou decimal) Campo numérico com validação
dateData (ISO 8601 yyyy-MM-dd)Date picker
datetime Data e hora (yyyy-MM-ddTHH:mm) Date + time picker
currencyValor monetário formatadoCampo com máscara monetária
select Seleção entre opções (estáticas ou dinâmicas) Dropdown ou lookup

Campos format e rounding#

Para os tipos date, datetime, currency e number é possível personalizar o formato de entrada/exibição:

{
  "id": "invoiceDate",
  "label": "Data da Nota",
  "type": "date",
  "format": "MM/dd/yyyy"
}
{
  "id": "totalValue",
  "label": "Valor Total",
  "type": "currency",
  "format": ".,2",
  "rounding": "half_even"
}
typeformatSignificado
date dd/MM/yyyy (padrão) Padrão intl para exibição e entrada
datetime dd/MM/yyyy (padrão) Aplica à parte de data; hora sempre HH:mm
currency ".,2" (padrão) <milhar><decimal><casas> — ex.: ".,2"1.234,56 ; ",.2"1,234.56
number null (ilimitado) Número de casas decimais como string (ex.: "3")

O campo rounding aceita os mesmos modos de RoundingConfig: half_even (padrão), half_up, half_down, up, down, truncate. Aplica-se apenas a currency e number quando format define precisão.

Lookups dinâmicos#

Um parâmetro select pode ter suas opções carregadas dinamicamente via lookup. Existem dois backends:

  • http — busca opções em uma URL (padrão, sem necessidade de declarar source)
  • db — executa uma query SQL em uma conexão do ConnectionRegistry

Lookup HTTP#

{
  "id": "categoryId",
  "label": "Categoria",
  "type": "select",
  "required": true,
  "lookup": {
    "source": "http",
    "url": "https://api.example.com/categories",
    "valueField": "id",
    "labelField": "name",
    "method": "GET",
    "cacheSeconds": 300,
    "pageSize": 20,
    "searchOnOpen": true
  }
}

O campo "source": "http" pode ser omitido — documentos sem source são tratados como HTTP (compatibilidade retroativa).

Lookup de banco de dados#

{
  "id": "productId",
  "label": "Produto",
  "type": "select",
  "required": true,
  "lookup": {
    "source": "db",
    "connectionRef": "main_db",
    "query": "SELECT id, name FROM products WHERE active = true AND ({_search} = '' OR name ILIKE '%' || {_search} || '%') ORDER BY name LIMIT {_page_size} OFFSET {_offset}",
    "valueField": "id",
    "labelField": "name",
    "cacheSeconds": 120,
    "pageSize": 25,
    "searchOnOpen": true
  }
}

A query usa placeholders {paramId} (mesmo padrão das URLs REST). Os seguintes são injetados automaticamente:

PlaceholderValor
{_search}Texto digitado na caixa de busca
{_page}Página atual (1-based)
{_page_size} Valor de pageSize (ou 0 quando sem paginação)
{_offset}(page - 1) * pageSize

Para substituir o nome do schema dinamicamente, use {_schema} na query — ele é resolvido a partir do campo extra.schema da DatabaseConnectionEntry antes da parametrização.

Propriedades comuns dos lookups#

CampoTipoDefaultDescrição
source string "http" Backend do lookup: "http" ou "db"
valueField string Campo do resultado usado como valor enviado ao relatório
labelField string Campo do resultado exibido ao usuário
cacheSeconds int 300 Tempo de cache em memória (0 = sem cache)
refreshOnDependencyChange bool false Recarrega quando um parâmetro de dependsOn muda
pageSize int 0 Itens por página (0 = busca tudo de uma vez)
searchOnOpen bool true Carrega os primeiros itens ao abrir o diálogo

Propriedades exclusivas do lookup HTTP#

CampoTipoDefaultDescrição
url string URL com placeholders {paramId}
method string "GET" Método HTTP: "GET" ou "POST"

Propriedades exclusivas do lookup DB#

CampoTipoDefaultDescrição
connectionRef string ID de uma entrada DatabaseConnectionEntry no ConnectionRegistry
query string Query SQL com placeholders {paramId}

Formatos de resposta HTTP aceitos#

O HttpLookupResolver aceita dois formatos:

  • Lista raiz: [{"id": "1", "name": "São Paulo"}, ...]
  • Chave data: {"data": [{"id": "1", "name": "São Paulo"}, ...]}

Para paginação, o resolver lê meta da resposta para calcular hasMore:

{
  "data": [...],
  "meta": { "total": 100, "page": 2, "perPage": 20 }
}

Quando meta está ausente, usa a heurística items.length >= pageSize.

Dependências em cascata#

Use dependsOn para criar filtros encadeados. Quando o valor de um parâmetro-pai muda, os filhos são recarregados automaticamente:

{
  "parameters": [
    {
      "id": "countryId",
      "label": "País",
      "type": "select",
      "lookup": {
        "url": "https://api.example.com/countries",
        "valueField": "id",
        "labelField": "name"
      }
    },
    {
      "id": "stateId",
      "label": "Estado",
      "type": "select",
      "dependsOn": ["countryId"],
      "lookup": {
        "url": "https://api.example.com/countries/{countryId}/states",
        "valueField": "id",
        "labelField": "name",
        "refreshOnDependencyChange": true
      }
    },
    {
      "id": "cityId",
      "label": "Cidade",
      "type": "select",
      "dependsOn": ["stateId"],
      "lookup": {
        "url": "https://api.example.com/states/{stateId}/cities",
        "valueField": "id",
        "labelField": "name",
        "refreshOnDependencyChange": true
      }
    }
  ]
}

Segurança (SSRF)#

O HttpLookupResolver só aceita as seguintes origens (rejeita qualquer outra com ArgumentError):

  • https:// — qualquer host externo via HTTPS
  • http://localhost — desenvolvimento local
  • http://127.0.0.1 — loopback alternativo (útil em testes e servidores locais)

Validação declarativa#

Cada parâmetro pode ter uma regra de validação inline via validation:

{
  "id": "quantity",
  "label": "Quantidade",
  "type": "number",
  "required": true,
  "validation": {
    "rule": "min",
    "value": "1",
    "errorMessage": "Quantidade mínima é 1"
  }
}

Regras disponíveis#

ruleParâmetrosDescrição
requiredCampo não pode ser vazio
minLength value ou compareWith Comprimento mínimo do texto
maxLength value ou compareWith Comprimento máximo do texto
min value ou compareWith Valor numérico mínimo
max value ou compareWith Valor numérico máximo
dateBefore value ou compareWith Data deve ser anterior ao limite
dateAfter value ou compareWith Data deve ser posterior ao limite
lessThan value ou compareWith Valor estritamente menor
greaterThan value ou compareWith Valor estritamente maior
  • value — valor fixo para comparação (ex: "100", "2024-01-01")
  • compareWith — ID de outro parâmetro para comparação dinâmica (ex: "startDate")
  • errorMessage — mensagem exibida quando a validação falha

Injetando parâmetros nas expressões#

Os valores dos parâmetros são injetados diretamente no contexto das expressões do ScriptEngine. Use os IDs dos parâmetros nas expressões de filtro:

{
  "op": "filter",
  "source": "orders",
  "where": "date >= startDate AND date <= endDate"
}
{
  "op": "filter",
  "source": "orders",
  "where": "status == orderStatus"
}

Operadores de comparação suportados para strings e datas (comparação lexicográfica): >=, <=, >, <, ==, !=

Injetando parâmetros em URLs REST#

Use {paramId} como placeholder na URL da fonte de dados:

{
  "id": "orders",
  "type": "list",
  "source": "external",
  "url": "https://api.example.com/orders?from={startDate}&to={endDate}"
}

Os valores são URI-encoded automaticamente antes da requisição.

Usando no Studio#

Parâmetros são editados no painel de propriedades do relatório (Configurações → Parâmetros):

  1. Clique em + para adicionar um parâmetro
  2. Preencha ID, rótulo, tipo, valor padrão e se é obrigatório
  3. Para tipo select, adicione as opções estáticas separadas por vírgula ou configure um lookup
  4. Escolha o backend do lookup (http ou db) e preencha os campos correspondentes
  5. Defina dependsOn para criar filtros encadeados
  6. Configure pageSize para ativar paginação no diálogo de seleção
  7. Adicione uma regra de validação se necessário

Ao clicar em Imprimir ou Salvar PDF, o Studio abre automaticamente o SulfiteFilterScreen se houver parâmetros definidos. O diálogo exibe cada parâmetro com o widget apropriado, validação inline e indicador de carregamento para lookups.

Passando parâmetros programaticamente#

Via SulfiteEngine.generate():

import 'package:sulfite_core/sulfite_core.dart';

final params = <String, String>{
  'startDate': '2024-06-01',
  'endDate': '2024-12-31',
  'orderStatus': 'aprovado',
};

final pdfBytes = await engine.generate(
  report: report,
  data: payload,
  params: params,
);

Usando o SulfiteFilterScreen standalone#

O SulfiteFilterScreen pode ser usado fora do Studio como um diálogo independente:

import 'package:sulfite_studio/sulfite_studio.dart';
import 'package:sulfite_core/sulfite_core.dart';

final params = await SulfiteFilterScreen.show(
  context,
  parameters: report.parameters,
  initialValues: {'status': 'aprovado'},
  lookupResolver: HttpLookupResolver(),
);

if (params != null) {
  // Usar params para gerar o relatório
}

Resolver customizado#

Implemente a interface LookupResolver para controlar como as opções são buscadas:

class MyResolver implements LookupResolver {
  @override
  Future<List<LookupOption>> fetchOptions(
    LookupConfig config,
    ReportParams currentParams,
  ) async {
    // Sua lógica de busca — banco local, GraphQL, etc.
    return [
      LookupOption(value: '1', label: 'Opção A'),
      LookupOption(value: '2', label: 'Opção B'),
    ];
  }
}

Para lookups de banco, use o DbLookupResolver do pacote sulfite_datasources:

import 'package:sulfite_datasources/sulfite_datasources.dart';

final registry = ConnectionRegistry();
registry.register(DatabaseConnectionEntry(
  id: 'main_db',
  host: 'localhost',
  port: 5432,
  database: 'myapp',
  username: 'user',
  password: 'secret',
));

final dbResolver = DbLookupResolver(registry: registry);

final params = await SulfiteFilterScreen.show(
  context,
  parameters: report.parameters,
  lookupResolver: dbResolver,
);

Referência do modelo#

// ReportParameter
ReportParameter(
  id: 'startDate',          // identificador único (sem espaços)
  label: 'Data Início',     // rótulo exibido no diálogo
  type: 'date',             // 'text' | 'number' | 'date' | 'datetime' | 'currency' | 'select'
  defaultValue: '2024-01-01',
  format: 'dd/MM/yyyy',     // opcional — formato de entrada/exibição
  rounding: 'half_even',    // opcional — para 'currency' e 'number'
  options: [],              // para type == 'select' (opções estáticas)
  required: true,           // bloqueia confirmação se vazio
  lookup: LookupConfig.http(  // HTTP lookup
    url: 'https://api.example.com/items',
    valueField: 'id',
    labelField: 'name',
    method: 'GET',
    cacheSeconds: 300,
    pageSize: 20,
    searchOnOpen: true,
    refreshOnDependencyChange: true,
  ),
  // --- OU ---
  lookup: LookupConfig.db(    // DB lookup
    connectionRef: 'main_db',
    query: 'SELECT id, name FROM categories WHERE {_search} = \'\' OR name ILIKE \'%\' || {_search} || \'%\' LIMIT {_page_size} OFFSET {_offset}',
    valueField: 'id',
    labelField: 'name',
    cacheSeconds: 120,
    pageSize: 25,
  ),
  dependsOn: ['parentParam'], // IDs dos parâmetros-pai
  validation: ValidationRule(  // opcional — validação inline
    rule: ValidationRuleType.min,
    value: '1',
    errorMessage: 'Valor mínimo é 1',
  ),
)

ReportParams é um typedef para Map<String, String>.