Logosulfite.app
rafagazani/sulfite 999999

sulfite_report_manager

Widget completo de gestão de relatórios — CRUD, busca, permissões e visualizador integrado

sulfite_report_manager#

Pacote Flutter que oferece widgets prontos para listar, buscar, criar, editar, excluir e visualizar relatórios Sulfite em qualquer aplicação, com suporte a múltiplos backends de armazenamento e sistema de permissões granular.

Instalação#

dependencies:
  sulfite_report_manager:
    git:
      url: https://github.com/rafaelgazani/sulfite.git
      path: packages/sulfite_report_manager

Uso rápido#

import 'package:sulfite_report_manager/sulfite_report_manager.dart';
import 'package:sulfite_studio/sulfite_studio.dart';
import 'package:sqlite3/sqlite3.dart';

final db = sqlite3.open('reports.db');

ReportManagerWidget(
  repository: SqliteReportRepository.open(db),
  groupRepository: SqliteReportGroupRepository.open(db),
  canInsert: true,
  canEdit: true,
  canDelete: true,
  viewerBuilder: (context, entry, definition) => SulfiteConsumerScreen(
    report: definition,
    title: entry.name,
    dataSourceResolvers: [
      PostgRestDataSourceResolver(registry: myRegistry),
      RestDataSourceResolver(),
    ],
    onExportPdf: () => _exportPdf(definition),
  ),
)

ReportManagerWidget#

Widget principal que renderiza a tela de gerenciamento completa (AppBar + barra de busca + lista + FAB).

Parâmetros#

ParâmetroTipoPadrãoDescrição
repository ReportRepository Backend de armazenamento (obrigatório)
groupRepository ReportGroupRepository? null Habilita aba de grupos, filtros por grupo e associação relatório↔grupo
tenantId String defaultReportTenantId Tenant usado nas consultas e mutações do manager
canInsert bool false Exibe FAB para criar novo relatório
canEdit bool false Exibe ícone de edição em cada item da lista
canDelete bool false Exibe ícone de exclusão em cada item da lista
canEditGlobal bool false Permite criar/editar/excluir grupos globais
canSettings bool false Exibe ícone de configurações na AppBar
title String 'Relatórios' Título exibido na AppBar
onSettings VoidCallback? null Substituí o dialog padrão ao clicar em configurações
viewerBuilder Widget Function(BuildContext, ReportEntry, ReportDefinition)? null Builder chamado ao tocar em um relatório — recebe a definição já parseada
entryAvailabilityResolver ReportEntryAvailability Function(ReportEntry)? null Desabilita entradas específicas na lista (ex: bloquear relatórios PostgreSQL no Flutter Web)

Mapa de permissões#

canInsert    → FAB "Novo relatório" na lista
canEdit      → Ícone de edição em cada item (editar metadados: nome/descrição)
canDelete    → Ícone de exclusão em cada item
canEditGlobal → Administração de grupos globais
canSettings  → Ícone de configurações na AppBar (onSettings ou dialog padrão)

Grupos de relatórios#

Passe um ReportGroupRepository para ativar:

  • aba Grupos no manager;
  • filtros horizontais por grupo em Relatórios;
  • chips coloridos nos itens da lista;
  • criação, edição e exclusão de grupos;
  • seleção de múltiplos grupos no dialog de relatório.

O cadastro de grupo aceita uma cor opcional livre. O valor é normalizado para RRGGBB e salvo como colorHex; na interface ele aparece como #RRGGBB. Se a cor estiver ausente, o manager calcula uma cor automática a partir do tema atual.

final reportRepo = RestReportRepository(baseUrl: serverUrl);
final groupRepo = RestReportGroupRepository(baseUrl: serverUrl);

ReportManagerWidget(
  repository: reportRepo,
  groupRepository: groupRepo,
  tenantId: 'acme',
  canInsert: permissions.canInsert,
  canEdit: permissions.canEdit,
  canDelete: permissions.canDelete,
  canEditGlobal: permissions.canEditGlobal,
  viewerBuilder: (ctx, entry, definition) {
    return SulfiteConsumerScreen(report: definition, title: entry.name);
  },
)

entryAvailabilityResolver#

Use para desabilitar relatórios condicionalmente sem removê-los da lista — útil para bloquear relatórios que exigem recursos indisponíveis na plataforma atual (ex: PostgreSQL no Flutter Web):

ReportManagerWidget(
  repository: repository,
  entryAvailabilityResolver: (entry) {
    // Bloqueia relatórios com datasource Postgres no Web
    if (kIsWeb && entry.tags.contains('postgres')) {
      return const ReportEntryAvailability.disabled(
        'Não disponível no navegador — requer conexão direta com banco de dados',
      );
    }
    return const ReportEntryAvailability.enabled();
  },
  viewerBuilder: (ctx, entry, def) => SulfiteConsumerScreen(report: def),
)

O item aparece na lista com estilo visual desabilitado e exibe o disabledReason ao usuário. Tocar no item não abre o visualizador.

Fluxo de visualização#

Ao tocar em um relatório na lista (com viewerBuilder fornecido):

  1. Abre imediatamente uma rota com loading — sem bloquear a lista
  2. Aguarda a animação de transição terminar (sem jank)
  3. Busca o conteúdo completo do relatório (findById) em background
  4. Parseia a ReportDefinition
  5. Chama viewerBuilder(context, entry, definition) → exibe o widget retornado

O viewerBuilder recebe o ReportEntry completo e a ReportDefinition já parseada. O app controla tudo sobre a visualização — resolvers, botões de exportação, botão de editar no designer, etc.

ReportManagerWidget(
  repository: repository,
  canInsert: isAdmin,
  canEdit: isAdmin,
  canDelete: isAdmin,
  canSettings: isAdmin,
  onSettings: () => Navigator.of(context).push(
    MaterialPageRoute(
      builder: (_) => ConnectionManagerWidget(registry: myRegistry),
    ),
  ),
  viewerBuilder: (ctx, entry, definition) {
    return SulfiteConsumerScreen(
      report: definition,
      title: entry.name,
      dataSourceResolvers: [
        PostgRestDataSourceResolver(registry: myRegistry),
        PostgresDataSourceResolver(registry: myRegistry),
        RestDataSourceResolver(),
      ],
      onExportPdf: () => _exportPdf(definition),
      onExportExcel: () => _exportExcel(definition),
    );
  },
)

Abrir o Designer a partir do visualizador#

Use o viewerBuilder para compor o SulfiteConsumerScreen com um botão de edição que abre o Designer:

viewerBuilder: (ctx, entry, definition) {
  return SulfiteConsumerScreen(
    report: definition,
    title: entry.name,
    dataSourceResolvers: [RestDataSourceResolver()],
    // Quando canSettings: true no manager, exponha o botão de editar:
    onEdit: isAdmin
        ? () => Navigator.of(ctx).push(
              MaterialPageRoute(
                builder: (_) => SulfiteDesigner(
                  initialReport: definition,
                  onSave: (updated) async {
                    await repository.update(
                      entry.id,
                      ReportEntryDraft(
                        name: entry.name,
                        definitionJson: jsonEncode(updated.toJson()),
                      ),
                    );
                  },
                ),
              ),
            )
        : null,
  );
}

Backends de armazenamento#

O pacote fornece quatro implementações prontas de ReportRepository:

InMemoryReportRepository#

Para testes, protótipos e demos. Dados não persistem entre sessões.

final repo = InMemoryReportRepository();
await repo.insert(ReportEntryDraft(
  name: 'Meu Relatório',
  definitionJson: jsonEncode(myReport.toJson()),
));

SqliteReportRepository#

Persiste localmente via sqlite3. Ideal para apps mobile/desktop offline.

import 'package:sqlite3/sqlite3.dart';

final db = sqlite3.open('reports.db');
final repo = SqliteReportRepository.open(db);

RestReportRepository#

Conecta a uma API REST. Espera endpoints padrão:

MétodoEndpointDescrição
GET/reportsLista todos
GET/reports?q={query}Busca
GET/reports/{id}Busca por ID (com conteúdo)
POST/reportsCria
PUT/reports/{id}Atualiza
DELETE/reports/{id}Exclui
final repo = RestReportRepository(baseUrl: 'https://api.example.com');

PostgresReportRepository#

Conecta diretamente ao PostgreSQL via postgres package. Recomendado para servidores/desktop apps.

final conn = await Connection.open(...);
final repo = PostgresReportRepository(conn);
await repo.migrate(); // cria a tabela se não existir

Implementar seu próprio repository#

Implemente a interface ReportRepository:

abstract class ReportRepository {
  Future<List<ReportEntry>> listAll();
  Future<List<ReportEntry>> search(String query);
  Future<ReportEntry?> findById(String id);
  Future<ReportEntry> insert(ReportEntryDraft draft);
  Future<ReportEntry> update(String id, ReportEntryDraft draft);
  Future<void> delete(String id);
  Future<void> dispose() async {}
}

Contrato lazy-loading: listAll e search retornam entradas sem conteúdo (packageBytes e definitionJson como null). Somente findById retorna o conteúdo completo. O manager chama findById automaticamente antes de chamar viewerBuilder.

Modelo ReportEntry#

CampoTipoDescrição
idStringIdentificador único
nameStringNome exibido na lista
descriptionString?Descrição opcional
createdAtDateTimeData de criação
updatedAtDateTimeData da última atualização
storageKind ReportStorageKind package (.sulfite) ou json
packageBytes Uint8List? Bytes do .sulfite (lazy — só em findById)
definitionJson String? JSON da definição (lazy — só em findById)

Formato .sulfite (RFC-017)#

O manager suporta nativamente relatórios no formato .sulfite (pacote ZIP com definição + assets embutidos). Ao abrir um relatório com storageKind: package, o SulfitePackageReader extrai a definição automaticamente.

// Importar um .sulfite existente
final bytes = await File('relatorio.sulfite').readAsBytes();
await repo.insert(ReportEntryDraft(
  name: 'Meu Relatório',
  packageBytes: bytes,
));

Gerenciar conexões em runtime#

O pacote exporta MutableConnectionRegistry e ConnectionManagerWidget para que o app consumidor possa adicionar, editar e remover conexões de banco de dados ou HTTP em tempo de execução — sem reiniciar o app.

MutableConnectionRegistry#

ChangeNotifier que implementa ConnectionRegistry. Usado como fonte de conexões nomeadas pelos resolvers do sulfite_datasources.

final registry = MutableConnectionRegistry([
  ConnectionEntry.database(
    name: 'producao',
    host: 'db.example.com',
    port: 5432,
    database: 'app_db',
    username: 'app_user',
    password: 'secret',
    ssl: true,
    extra: {'schema': 'public'},
  ),
  ConnectionEntry.http(
    name: 'supabase_prod',
    baseUrl: 'https://xyz.supabase.co/rest/v1',
    defaultHeaders: {
      'apikey': 'your-anon-key',
      'Authorization': 'Bearer your-anon-key',
    },
  ),
]);

// Passado aos resolvers — datasources com connectionRef usarão este registry
final pgResolver = PostgresDataSourceResolver(registry: registry);
final postgrestResolver = PostgRestDataSourceResolver(registry: registry);

// Adicionar ou substituir:
registry.upsert(
  ConnectionEntry.database(name: 'homologacao', host: 'db-hom.example.com', ...),
);

// Remover pelo nome:
registry.remove('homologacao');
MétodoDescrição
upsert(entry) Adiciona ou substitui a conexão com o mesmo name e notifica listeners
remove(name)Remove pelo nome; não-op se não existir
entriesCópia imutável da lista atual
resolve(ref) Busca conexão pelo name (implementa ConnectionRegistry)

Persistência: as conexões ficam em memória. Para persistir entre sessões, salve e restaure a lista no initState do app.

ConnectionManagerWidget#

Tela pronta (Scaffold + AppBar + lista + FAB) para gerenciar as conexões de um MutableConnectionRegistry. Exibe conexões PostgreSQL e HTTP/REST com ações de adicionar, editar, remover e testar.

ConnectionManagerWidget(
  registry: myRegistry,
  onTestConnection: (entry) async {
    // Retorne null se OK, ou uma mensagem de erro
    try {
      await PostgresDataSourceResolver(
        registry: MutableConnectionRegistry([entry]),
      ).resolve(
        DataSource(
          id: '_ping', type: 'list', schema: {},
          source: 'external', connectionRef: entry.name,
          query: 'SELECT 1',
        ),
      );
      return null;
    } catch (e) {
      return e.toString();
    }
  },
)

Parâmetros

ParâmetroTipoDescrição
registry MutableConnectionRegistry Registry mutável que será exibido e editado
onTestConnection Future<String?> Function(ConnectionEntry)? Callback de teste; retorna null em sucesso ou mensagem de erro

Formulário de conexão

PostgreSQL

CampoDescrição
Nome Identificador único — deve coincidir com connectionRef nos datasources
Host / Porta / DatabaseEndereço e banco
Usuário / SenhaAutenticação
Schema PostgreSQLOpcional — substituído em {_schema} nas queries
Usar SSLLiga/desliga SSL

HTTP / REST

CampoDescrição
NomeIdentificador único
Base URLURL base usada pelos datasources HTTP com connectionRef
Headers Pares chave-valor injetados em todas as requisições (ex: apikey , Authorization ). Configuráveis via editor visual.

O nome de uma conexão é imutável após criado — serve como chave de lookup. Para "renomear", remova a antiga e crie uma nova.

Integrando com SulfiteDesigner#

Passe o ConnectionManagerWidget via onSettings do manager ou diretamente no slot reportManager do designer:

final registry = MutableConnectionRegistry();

SulfiteDesigner(
  initialReport: report,
  dataSourceResolvers: [
    PostgRestDataSourceResolver(registry: registry),
    PostgresDataSourceResolver(registry: registry),
    RestDataSourceResolver(),
  ],
  reportManager: ReportManagerWidget(
    repository: SqliteReportRepository.open(db),
    canInsert: true,
    canEdit: true,
    canDelete: true,
    canSettings: true,
    onSettings: () => Navigator.of(context).push(
      MaterialPageRoute(
        builder: (_) => ConnectionManagerWidget(registry: registry),
      ),
    ),
    viewerBuilder: (ctx, entry, def) => SulfiteConsumerScreen(
      report: def,
      title: entry.name,
      dataSourceResolvers: [
        PostgRestDataSourceResolver(registry: registry),
        PostgresDataSourceResolver(registry: registry),
        RestDataSourceResolver(),
      ],
    ),
  ),
)