diff --git a/docs/internal/abono-demo/README.md b/docs/internal/abono-demo/README.md new file mode 100644 index 0000000000..b8c35729f9 --- /dev/null +++ b/docs/internal/abono-demo/README.md @@ -0,0 +1,69 @@ +# Dashboard demo: Abono de Permanência + +Protótipo **nativo** (nível A da [análise de viabilidade](../viabilidade-dashboard-abono-permanencia.md)) para apresentar no seu Metabase local. Os dados são mock (jul–dez/2025) e não falam com a base do cliente. + +## O que o seed cria + +- Banco SQLite `abono_permanencia.sqlite` com duas tabelas: `abono_folha` e `abono_divergencia` +- Coleção **Abono de Permanência (demo)** +- Dashboard em largura full, quatro colunas na mesma linha: + +| Coluna | Cards nativos | +| --- | --- | +| Cabeçalho | Heading **Abono de Permanência** + filtro **Período** (`date/all-options`, padrão `2025-07-01~2025-12-31`) | +| 1. Evolução | 3 Numbers + Combo (linha de valor / barras de beneficiários, **Stack series**) | +| 2. Comparação + Top 3 | Numbers Analisado vs Divergências (valor, pessoas, %) + 3 Bars | +| 3. Scatter | Bolhas financeira × normativa × pessoas. Clique atualiza o órgão | +| 4. Detalhe | Heading `{{orgao}}` (padrão Seduc) + tabelas motivo/cargo + Number da média mensal | + +O check de apply do mockup não entra: o Metabase aplica o período no select. + +## Subir no seu ambiente + +1. Metabase rodando e **setup já feito**, com um usuário **admin**. +2. Na raiz do repositório: + +``` +python3 docs/internal/abono-demo/seed_abono_demo.py \ + --url http://localhost:3000 \ + --email SEU_ADMIN@empresa.com \ + --password 'sua-senha' +``` + +Ou via ambiente: `MB_URL`, `MB_EMAIL`, `MB_PASSWORD`. + +3. Abra a URL que o script imprimir (`/dashboard/`), ou a coleção **Abono de Permanência (demo)**. + +Rodar de novo arquiva o dashboard anterior com o mesmo nome e cria outro. O SQLite é recriado a cada execução. + +Só gerar o arquivo, sem API: + +``` +python3 docs/internal/abono-demo/seed_abono_demo.py --sqlite-only +``` + +## Docker + +O Metabase precisa **enxergar o caminho do SQLite**. Se o app roda num container, monte a pasta e aponte o banco para o path **dentro** do container, por exemplo `/data/abono_permanencia.sqlite`. Depois ajuste Admin > Databases ou rode o seed a partir de um ambiente que use esse mesmo path. + +No WSL / jar / `clojure -M:dev` na mesma máquina, o path absoluto gerado pelo script costuma funcionar. + +## Totais do mock (jul–dez/2025) + +- Valor total: **R$ 300.979.598,94** +- Valor mensal médio: **R$ 50.163.266,49** +- Valor por beneficiário/mês: **R$ 1.967,18** +- Divergências: **12%** da folha (o restante é “analisado”) +- Órgãos: Seduc (maior bolha), SES, SSP, SEFAZ, SEINFRA, DETRAN + +## O que ainda não é nativo neste protótipo + +Deixado de propósito para a próxima etapa: + +- KPIs + Combo no **mesmo** card +- Fundo vermelho no box inteiro de Divergências (só a cor do número) +- Ícones de unidade, olho nos pontos, quadrantes e labels nas bolhas +- Accordion com % no header da coluna 4 +- Uma moldura única por coluna + +Clique na bolha **Seduc** (ou outro órgão) para ver o detalhe da coluna 4 mudar. diff --git a/docs/internal/abono-demo/build_sqlite.py b/docs/internal/abono-demo/build_sqlite.py new file mode 100644 index 0000000000..1058145c7f --- /dev/null +++ b/docs/internal/abono-demo/build_sqlite.py @@ -0,0 +1,218 @@ +"""Build the SQLite mock used by the Abono de Permanência demo dashboard.""" + +from __future__ import annotations + +import sqlite3 +from pathlib import Path + +DB_NAME = "abono_permanencia.sqlite" + +# Totals from the mockup: R$ 300.979.598,94 / média R$ 50.163.266,49 / R$ 1.967,18 por beneficiário. +FOLHA = [ + # competencia, valor_total, qtd_beneficiarios + ("2025-07-01", 50_100_000.00, 25_500), + ("2025-08-01", 51_300_000.00, 25_800), + ("2025-09-01", 52_700_000.00, 26_200), + ("2025-10-01", 49_800_000.00, 25_200), + ("2025-11-01", 49_079_598.94, 25_100), + ("2025-12-01", 48_000_000.00, 25_200), +] + +ORGAOS = [ + ("Seduc", 0.45, 78.4), + ("SES", 0.18, 41.2), + ("SSP", 0.12, 55.0), + ("SEFAZ", 0.10, 22.5), + ("SEINFRA", 0.08, 33.8), + ("DETRAN", 0.07, 18.1), +] + +MOTIVOS = [ + ("Pagamento a maior", 0.40), + ("Tempo de serviço irregular", 0.25), + ("Acumulação indevida", 0.18), + ("Cargo incompatível", 0.12), + ("Outros", 0.05), +] + +CARGOS_POR_ORGAO = { + "Seduc": [ + ("Professor", 0.62), + ("Agente administrativo", 0.22), + ("Diretor escolar", 0.16), + ], + "SES": [ + ("Técnico de enfermagem", 0.48), + ("Médico", 0.32), + ("Agente administrativo", 0.20), + ], + "SSP": [ + ("Policial civil", 0.55), + ("Escrivão", 0.28), + ("Agente administrativo", 0.17), + ], + "SEFAZ": [ + ("Auditor fiscal", 0.50), + ("Analista fazendário", 0.30), + ("Agente administrativo", 0.20), + ], + "SEINFRA": [ + ("Engenheiro", 0.42), + ("Técnico em edificações", 0.35), + ("Agente administrativo", 0.23), + ], + "DETRAN": [ + ("Examinador de trânsito", 0.46), + ("Agente de trânsito", 0.34), + ("Agente administrativo", 0.20), + ], +} + +# Share of payroll flagged as divergence (rest is "analisado"). +DIVERGENCIA_SHARE = 0.12 + + +def db_path(base_dir: Path | None = None) -> Path: + root = base_dir or Path(__file__).resolve().parent + return root / DB_NAME + + +def _create_schema(conn: sqlite3.Connection) -> None: + conn.executescript( + """ + DROP TABLE IF EXISTS abono_folha; + DROP TABLE IF EXISTS abono_divergencia; + + CREATE TABLE abono_folha ( + competencia DATE NOT NULL, + valor_total REAL NOT NULL, + qtd_beneficiarios INTEGER NOT NULL, + valor_analisado REAL NOT NULL, + qtd_analisado INTEGER NOT NULL, + valor_divergencias REAL NOT NULL, + qtd_divergencias INTEGER NOT NULL + ); + + CREATE TABLE abono_divergencia ( + competencia DATE NOT NULL, + orgao TEXT NOT NULL, + motivo TEXT NOT NULL, + cargo TEXT NOT NULL, + valor REAL NOT NULL, + qtd_pessoas INTEGER NOT NULL, + score_normativa REAL NOT NULL + ); + """ + ) + + +def _build_rows() -> tuple[list[tuple], list[tuple]]: + folha_rows: list[tuple] = [] + divergencia_rows: list[tuple] = [] + + for competencia, valor_total, qtd_beneficiarios in FOLHA: + valor_div = round(valor_total * DIVERGENCIA_SHARE, 2) + valor_ok = round(valor_total - valor_div, 2) + qtd_div = int(round(qtd_beneficiarios * DIVERGENCIA_SHARE)) + qtd_ok = qtd_beneficiarios - qtd_div + folha_rows.append( + ( + competencia, + valor_total, + qtd_beneficiarios, + valor_ok, + qtd_ok, + valor_div, + qtd_div, + ) + ) + + remaining_valor = valor_div + remaining_pessoas = qtd_div + org_items = list(enumerate(ORGAOS)) + for org_i, (orgao, org_w, score) in org_items: + last_org = org_i == len(org_items) - 1 + org_valor = remaining_valor if last_org else round(valor_div * org_w, 2) + org_pessoas = remaining_pessoas if last_org else int(round(qtd_div * org_w)) + remaining_valor = round(remaining_valor - org_valor, 2) + remaining_pessoas -= org_pessoas + + mot_remaining_v = org_valor + mot_remaining_p = org_pessoas + mot_items = list(enumerate(MOTIVOS)) + cargos = CARGOS_POR_ORGAO[orgao] + for mot_i, (motivo, mot_w) in mot_items: + last_mot = mot_i == len(mot_items) - 1 + mot_valor = mot_remaining_v if last_mot else round(org_valor * mot_w, 2) + mot_pessoas = mot_remaining_p if last_mot else int(round(org_pessoas * mot_w)) + mot_remaining_v = round(mot_remaining_v - mot_valor, 2) + mot_remaining_p -= mot_pessoas + + cargo_remaining_v = mot_valor + cargo_remaining_p = mot_pessoas + cargo_items = list(enumerate(cargos)) + for cargo_i, (cargo, cargo_w) in cargo_items: + last_cargo = cargo_i == len(cargo_items) - 1 + cargo_valor = ( + cargo_remaining_v if last_cargo else round(mot_valor * cargo_w, 2) + ) + cargo_pessoas = ( + cargo_remaining_p if last_cargo else max(1, int(round(mot_pessoas * cargo_w))) + ) + if cargo_pessoas > cargo_remaining_p: + cargo_pessoas = cargo_remaining_p + cargo_remaining_v = round(cargo_remaining_v - cargo_valor, 2) + cargo_remaining_p -= cargo_pessoas + if cargo_valor <= 0 and cargo_pessoas <= 0: + continue + divergencia_rows.append( + ( + competencia, + orgao, + motivo, + cargo, + cargo_valor, + max(cargo_pessoas, 0), + score, + ) + ) + + return folha_rows, divergencia_rows + + +def build(base_dir: Path | None = None) -> Path: + path = db_path(base_dir) + path.parent.mkdir(parents=True, exist_ok=True) + if path.exists(): + path.unlink() + + folha_rows, divergencia_rows = _build_rows() + conn = sqlite3.connect(path) + try: + _create_schema(conn) + conn.executemany( + """ + INSERT INTO abono_folha ( + competencia, valor_total, qtd_beneficiarios, + valor_analisado, qtd_analisado, valor_divergencias, qtd_divergencias + ) VALUES (?, ?, ?, ?, ?, ?, ?) + """, + folha_rows, + ) + conn.executemany( + """ + INSERT INTO abono_divergencia ( + competencia, orgao, motivo, cargo, valor, qtd_pessoas, score_normativa + ) VALUES (?, ?, ?, ?, ?, ?, ?) + """, + divergencia_rows, + ) + conn.commit() + finally: + conn.close() + return path + + +if __name__ == "__main__": + out = build() + print(f"SQLite gerado em {out}") diff --git a/docs/internal/abono-demo/dash_10_backup_20260821_083836.json b/docs/internal/abono-demo/dash_10_backup_20260821_083836.json new file mode 100644 index 0000000000..5a47e2a23d --- /dev/null +++ b/docs/internal/abono-demo/dash_10_backup_20260821_083836.json @@ -0,0 +1,3191 @@ +{ + "description": "Protótipo nativo (nível A). Dados mock de jul–dez/2025. Clique numa bolha para filtrar o detalhe do órgão.", + "archived": false, + "view_count": 9, + "collection_position": null, + "dashcards": [ + { + "size_x": 24, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [ + "a1b2c3d4" + ], + "action_id": null, + "collection_authority_level": null, + "card": { + "query_average_duration": null, + "download_perms": "none" + }, + "updated_at": "2026-08-17T12:35:56.616049Z", + "col": 0, + "id": 102, + "parameter_mappings": [], + "card_id": null, + "entity_id": "brZlcw2EWDBnuvGBD8s9i", + "visualization_settings": { + "text": "", + "virtual_card": { + "name": null, + "display": "heading", + "visualization_settings": {}, + "archived": false + }, + "dashcard.background": false + }, + "size_y": 2, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 0 + }, + { + "size_x": 6, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "query_average_duration": null, + "download_perms": "none" + }, + "updated_at": "2026-08-17T12:36:40.333137Z", + "col": 12, + "id": 103, + "parameter_mappings": [], + "card_id": null, + "entity_id": "l0HGOZWVi-RhU4MpVdfKc", + "visualization_settings": { + "text": "Análise / detalhamento divergências", + "virtual_card": { + "name": null, + "display": "heading", + "visualization_settings": {}, + "archived": false + }, + "dashcard.background": false + }, + "size_y": 2, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 2 + }, + { + "size_x": 6, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [ + "b2c3d4e5" + ], + "action_id": null, + "collection_authority_level": null, + "card": { + "query_average_duration": null, + "download_perms": "none" + }, + "updated_at": "2026-08-17T12:36:40.333137Z", + "col": 18, + "id": 104, + "parameter_mappings": [ + { + "parameter_id": "b2c3d4e5", + "target": [ + "text-tag", + "orgao" + ] + } + ], + "card_id": null, + "entity_id": "RTJ26Pq7RuG5a5soCclh7", + "visualization_settings": { + "text": "{{orgao}}", + "virtual_card": { + "name": null, + "display": "heading", + "visualization_settings": {}, + "archived": false + }, + "dashcard.background": false + }, + "size_y": 2, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 2 + }, + { + "size_x": 6, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "query_average_duration": null, + "download_perms": "none" + }, + "updated_at": "2026-08-17T12:36:40.333137Z", + "col": 6, + "id": 105, + "parameter_mappings": [], + "card_id": null, + "entity_id": "eXpua-WEULEOPNXe88i96", + "visualization_settings": { + "text": "Top 3 divergências", + "virtual_card": { + "name": null, + "display": "heading", + "visualization_settings": {}, + "archived": false + }, + "dashcard.background": false + }, + "size_y": 1, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 9 + }, + { + "size_x": 6, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "cache_invalidated_at": null, + "description": null, + "archived": false, + "view_count": 5, + "collection_position": null, + "source_card_id": null, + "table_id": 171, + "result_metadata": [ + { + "semantic_type": "type/Currency", + "lib/deduplicated-name": "sum", + "lib/original-name": "sum", + "name": "sum", + "lib/source": "source/aggregations", + "lib/source-column-alias": "sum", + "source": "aggregation", + "field_ref": [ + "aggregation", + 0 + ], + "effective_type": "type/Float", + "lib/desired-column-alias": "sum", + "display_name": "Sum of Valor Total", + "base_type": "type/Float" + } + ], + "embedding_type": null, + "initially_published_at": null, + "can_write": true, + "card_schema": 23, + "database_id": 2, + "enable_embedding": false, + "collection_id": 7, + "query_type": "query", + "download_perms": "full", + "name": "Valor total", + "document_id": null, + "last_used_at": "2026-08-21T12:37:47.363586Z", + "type": "question", + "dimensions": null, + "query_average_duration": null, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2026-08-17T12:33:21.792882Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "lib/type": "mbql/query", + "database": 2, + "stages": [ + { + "source-table": 171, + "lib/type": "mbql.stage/mbql", + "aggregation": [ + [ + "sum", + { + "lib/uuid": "354889ad-77ee-4351-9a23-b8479b789d0e" + }, + [ + "field", + { + "base-type": "type/Float", + "lib/uuid": "fb453e09-dd1c-44c9-9632-09558f28d133", + "effective-type": "type/Float" + }, + 1611 + ] + ] + ] + } + ] + }, + "dimension_mappings": null, + "id": 119, + "legacy_query": null, + "parameter_mappings": [], + "display": "scalar", + "archived_directly": false, + "entity_id": "QYx6xurEeEEk8z-MkV5s4", + "collection_preview": true, + "visualization_settings": { + "column_settings": { + "[\"name\",\"sum\"]": { + "number_style": "currency", + "currency": "BRL", + "currency_style": "symbol", + "decimals": 2 + } + } + }, + "metabase_version": "vLOCAL_DEV (06d1ba2ae111e66253209c01c244d6379acfc6dcb1911fa9ab6012cec9ce52e5)", + "parameters": [], + "dashboard_id": null, + "created_at": "2026-08-17T12:33:21.792882Z", + "public_uuid": null + }, + "updated_at": "2026-08-17T12:33:25.109933Z", + "col": 0, + "id": 106, + "parameter_mappings": [ + { + "parameter_id": "a1b2c3d4", + "card_id": 119, + "target": [ + "dimension", + [ + "field", + 1610, + null + ] + ] + } + ], + "card_id": 119, + "entity_id": "HiTuxoXffv_0Sw42fwvo6", + "visualization_settings": {}, + "size_y": 2, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 2 + }, + { + "size_x": 6, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "cache_invalidated_at": null, + "description": null, + "archived": false, + "view_count": 5, + "collection_position": null, + "source_card_id": null, + "table_id": 171, + "result_metadata": [ + { + "semantic_type": "type/Currency", + "lib/deduplicated-name": "media", + "lib/original-name": "media", + "name": "media", + "lib/source": "source/aggregations", + "lib/source-column-alias": "media", + "source": "aggregation", + "field_ref": [ + "aggregation", + 0 + ], + "effective_type": "type/Float", + "lib/desired-column-alias": "media", + "display_name": "media", + "base_type": "type/Float" + } + ], + "embedding_type": null, + "initially_published_at": null, + "can_write": true, + "card_schema": 23, + "database_id": 2, + "enable_embedding": false, + "collection_id": 7, + "query_type": "query", + "download_perms": "full", + "name": "Valor mensal médio", + "document_id": null, + "last_used_at": "2026-08-21T12:37:47.363586Z", + "type": "question", + "dimensions": null, + "query_average_duration": null, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2026-08-17T12:33:22.271418Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "lib/type": "mbql/query", + "database": 2, + "stages": [ + { + "source-table": 171, + "lib/type": "mbql.stage/mbql", + "aggregation": [ + [ + "avg", + { + "name": "media", + "lib/uuid": "c486c6f8-4493-4e5c-80e5-cf2aae7fd622", + "display-name": "media" + }, + [ + "field", + { + "base-type": "type/Float", + "lib/uuid": "ef158ea2-2a79-4743-905a-bcecb610f4be", + "effective-type": "type/Float" + }, + 1611 + ] + ] + ] + } + ] + }, + "dimension_mappings": null, + "id": 120, + "legacy_query": null, + "parameter_mappings": [], + "display": "scalar", + "archived_directly": false, + "entity_id": "wa70fCexoxI0kEmj-iWhH", + "collection_preview": true, + "visualization_settings": { + "column_settings": { + "[\"name\",\"media\"]": { + "number_style": "currency", + "currency": "BRL", + "currency_style": "symbol", + "decimals": 2 + } + } + }, + "metabase_version": "vLOCAL_DEV (06d1ba2ae111e66253209c01c244d6379acfc6dcb1911fa9ab6012cec9ce52e5)", + "parameters": [], + "dashboard_id": null, + "created_at": "2026-08-17T12:33:22.271418Z", + "public_uuid": null + }, + "updated_at": "2026-08-17T12:33:25.109933Z", + "col": 0, + "id": 107, + "parameter_mappings": [ + { + "parameter_id": "a1b2c3d4", + "card_id": 120, + "target": [ + "dimension", + [ + "field", + 1610, + null + ] + ] + } + ], + "card_id": 120, + "entity_id": "WwUHkSfLoQqahCEDy7CPh", + "visualization_settings": {}, + "size_y": 2, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 4 + }, + { + "size_x": 6, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "cache_invalidated_at": null, + "description": null, + "archived": false, + "view_count": 5, + "collection_position": null, + "source_card_id": null, + "table_id": 171, + "result_metadata": [ + { + "lib/deduplicated-name": "per_capita", + "lib/original-name": "per_capita", + "name": "per_capita", + "lib/source": "source/aggregations", + "lib/source-column-alias": "per_capita", + "source": "aggregation", + "field_ref": [ + "aggregation", + 0 + ], + "effective_type": "type/Float", + "lib/desired-column-alias": "per_capita", + "display_name": "Valor por beneficiário/mês", + "base_type": "type/Float" + } + ], + "embedding_type": null, + "initially_published_at": null, + "can_write": true, + "card_schema": 23, + "database_id": 2, + "enable_embedding": false, + "collection_id": 7, + "query_type": "query", + "download_perms": "full", + "name": "Valor por beneficiário/mês", + "document_id": null, + "last_used_at": "2026-08-21T12:37:47.363586Z", + "type": "question", + "dimensions": null, + "query_average_duration": null, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2026-08-17T12:33:22.458681Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "lib/type": "mbql/query", + "database": 2, + "stages": [ + { + "source-table": 171, + "lib/type": "mbql.stage/mbql", + "aggregation": [ + [ + "/", + { + "name": "per_capita", + "lib/uuid": "7ed3f1e9-2b29-4add-8b13-2cea2e2f4d03", + "display-name": "Valor por beneficiário/mês" + }, + [ + "sum", + { + "lib/uuid": "d69d5c53-c484-4e7a-bc27-aba25d587a56" + }, + [ + "field", + { + "base-type": "type/Float", + "lib/uuid": "49e0793b-e532-4445-91f9-b210211c4661", + "effective-type": "type/Float" + }, + 1611 + ] + ], + [ + "sum", + { + "lib/uuid": "3c86265b-864c-4d2a-8d93-7758dc168168" + }, + [ + "field", + { + "base-type": "type/Integer", + "lib/uuid": "8f61575b-4254-4e39-a89f-e53470426c7f", + "effective-type": "type/Integer" + }, + 1612 + ] + ] + ] + ] + } + ] + }, + "dimension_mappings": null, + "id": 121, + "legacy_query": null, + "parameter_mappings": [], + "display": "scalar", + "archived_directly": false, + "entity_id": "qIfqVhu1PUOB288WCLWju", + "collection_preview": true, + "visualization_settings": { + "column_settings": { + "[\"name\",\"per_capita\"]": { + "number_style": "currency", + "currency": "BRL", + "currency_style": "symbol", + "decimals": 2 + } + } + }, + "metabase_version": "vLOCAL_DEV (06d1ba2ae111e66253209c01c244d6379acfc6dcb1911fa9ab6012cec9ce52e5)", + "parameters": [], + "dashboard_id": null, + "created_at": "2026-08-17T12:33:22.458681Z", + "public_uuid": null + }, + "updated_at": "2026-08-17T12:33:25.109933Z", + "col": 0, + "id": 108, + "parameter_mappings": [ + { + "parameter_id": "a1b2c3d4", + "card_id": 121, + "target": [ + "dimension", + [ + "field", + 1610, + null + ] + ] + } + ], + "card_id": 121, + "entity_id": "5dfVCfTGlngoMnFMgQGHj", + "visualization_settings": {}, + "size_y": 2, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 6 + }, + { + "size_x": 6, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "cache_invalidated_at": null, + "description": null, + "archived": false, + "view_count": 5, + "collection_position": null, + "source_card_id": null, + "table_id": 171, + "result_metadata": [ + { + "database_type": "DATE", + "semantic_type": "type/CreationDate", + "table_id": 171, + "lib/temporal-unit": "month", + "lib/deduplicated-name": "competencia", + "lib/original-name": "competencia", + "lib/breakout?": true, + "unit": "month", + "name": "competencia", + "lib/source": "source/table-defaults", + "lib/source-column-alias": "competencia", + "lib/original-display-name": "Competencia", + "source": "breakout", + "field_ref": [ + "field", + 1610, + { + "temporal-unit": "month" + } + ], + "lib/transformation-added-base-type": true, + "effective_type": "type/Date", + "active": true, + "id": 1610, + "lib/desired-column-alias": "competencia", + "position": 0, + "visibility_type": "normal", + "inherited_temporal_unit": "month", + "display_name": "Competencia: Month", + "fingerprint": { + "global": { + "distinct-count": 6, + "nil%": 0.0 + }, + "type": { + "type/DateTime": { + "earliest": "2025-07-01", + "latest": "2025-12-01" + } + } + }, + "base_type": "type/Date" + }, + { + "semantic_type": "type/Currency", + "lib/deduplicated-name": "Valor total", + "lib/original-name": "Valor total", + "name": "Valor total", + "lib/source": "source/aggregations", + "lib/source-column-alias": "Valor total", + "source": "aggregation", + "field_ref": [ + "aggregation", + 0 + ], + "effective_type": "type/Float", + "lib/desired-column-alias": "Valor total", + "display_name": "Valor total", + "base_type": "type/Float" + }, + { + "lib/deduplicated-name": "Beneficiários", + "lib/original-name": "Beneficiários", + "name": "Beneficiários", + "lib/source": "source/aggregations", + "lib/source-column-alias": "Beneficiários", + "source": "aggregation", + "field_ref": [ + "aggregation", + 1 + ], + "effective_type": "type/Integer", + "lib/desired-column-alias": "Beneficiários", + "display_name": "Beneficiários", + "base_type": "type/Integer" + } + ], + "embedding_type": null, + "initially_published_at": null, + "can_write": true, + "card_schema": 23, + "database_id": 2, + "enable_embedding": false, + "collection_id": 7, + "query_type": "query", + "download_perms": "full", + "name": "Evolução", + "document_id": null, + "last_used_at": "2026-08-21T12:37:47.363586Z", + "type": "question", + "dimensions": null, + "query_average_duration": null, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2026-08-17T12:33:22.643422Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "lib/type": "mbql/query", + "database": 2, + "stages": [ + { + "aggregation": [ + [ + "sum", + { + "name": "Valor total", + "lib/uuid": "4f60efc8-7e28-47b3-8b27-5290ed326cf4", + "display-name": "Valor total" + }, + [ + "field", + { + "base-type": "type/Float", + "lib/uuid": "f50f240a-fcff-4dd2-886e-8a0833f6f4b4", + "effective-type": "type/Float" + }, + 1611 + ] + ], + [ + "sum", + { + "name": "Beneficiários", + "lib/uuid": "b1f22f48-18ef-4661-b30a-587ec83f25bf", + "display-name": "Beneficiários" + }, + [ + "field", + { + "base-type": "type/Integer", + "lib/uuid": "c5c7c656-14e9-4395-9a70-1df271c0f850", + "effective-type": "type/Integer" + }, + 1612 + ] + ] + ], + "lib/type": "mbql.stage/mbql", + "source-table": 171, + "breakout": [ + [ + "field", + { + "base-type": "type/Date", + "temporal-unit": "month", + "lib/uuid": "cbf76b04-3bba-48d7-94a4-414975899fa8", + "effective-type": "type/Date" + }, + 1610 + ] + ] + } + ] + }, + "dimension_mappings": null, + "id": 122, + "legacy_query": null, + "parameter_mappings": [], + "display": "combo", + "archived_directly": false, + "entity_id": "kkxSj1qm8sT4mXHct8txX", + "collection_preview": true, + "visualization_settings": { + "graph.show_values": true, + "graph.x_axis.labels_enabled": false, + "graph.label_value_frequency": "all", + "graph.metrics": [ + "Valor total", + "Beneficiários" + ], + "graph.label_value_formatting": "compact", + "column_settings": { + "[\"name\",\"Valor total\"]": { + "number_style": "currency", + "currency": "BRL", + "currency_style": "symbol", + "decimals": 1 + } + }, + "series_settings": { + "Valor total": { + "display": "line", + "color": "#509EE3", + "line.marker_enabled": true + }, + "Beneficiários": { + "display": "bar", + "color": "#7BBCE7" + } + }, + "graph.dimensions": [ + "competencia" + ], + "graph.split_panels": true + }, + "metabase_version": "vLOCAL_DEV (06d1ba2ae111e66253209c01c244d6379acfc6dcb1911fa9ab6012cec9ce52e5)", + "parameters": [], + "dashboard_id": null, + "created_at": "2026-08-17T12:33:22.643422Z", + "public_uuid": null + }, + "updated_at": "2026-08-17T12:33:25.109933Z", + "col": 0, + "id": 109, + "parameter_mappings": [ + { + "parameter_id": "a1b2c3d4", + "card_id": 122, + "target": [ + "dimension", + [ + "field", + 1610, + null + ] + ] + } + ], + "card_id": 122, + "entity_id": "wY0HVUQoxpsxk1r7qXVB0", + "visualization_settings": {}, + "size_y": 8, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 8 + }, + { + "size_x": 3, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "cache_invalidated_at": null, + "description": null, + "archived": false, + "view_count": 5, + "collection_position": null, + "source_card_id": null, + "table_id": 171, + "result_metadata": [ + { + "semantic_type": "type/Currency", + "lib/deduplicated-name": "sum", + "lib/original-name": "sum", + "name": "sum", + "lib/source": "source/aggregations", + "lib/source-column-alias": "sum", + "source": "aggregation", + "field_ref": [ + "aggregation", + 0 + ], + "effective_type": "type/Float", + "lib/desired-column-alias": "sum", + "display_name": "Sum of Valor Analisado", + "base_type": "type/Float" + } + ], + "embedding_type": null, + "initially_published_at": null, + "can_write": true, + "card_schema": 23, + "database_id": 2, + "enable_embedding": false, + "collection_id": 7, + "query_type": "query", + "download_perms": "full", + "name": "Analisado — valor", + "document_id": null, + "last_used_at": "2026-08-21T12:37:47.363586Z", + "type": "question", + "dimensions": null, + "query_average_duration": null, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2026-08-17T12:33:22.799833Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "lib/type": "mbql/query", + "database": 2, + "stages": [ + { + "source-table": 171, + "lib/type": "mbql.stage/mbql", + "aggregation": [ + [ + "sum", + { + "lib/uuid": "1c7db934-6881-45fc-9ba6-bda4473dfd2b" + }, + [ + "field", + { + "base-type": "type/Float", + "lib/uuid": "207b842f-23db-47e6-8d18-6487dac086b7", + "effective-type": "type/Float" + }, + 1613 + ] + ] + ] + } + ] + }, + "dimension_mappings": null, + "id": 123, + "legacy_query": null, + "parameter_mappings": [], + "display": "scalar", + "archived_directly": false, + "entity_id": "tbT9jaLBqvrlxtRU0uPPO", + "collection_preview": true, + "visualization_settings": { + "column_settings": { + "[\"name\",\"sum\"]": { + "number_style": "currency", + "currency": "BRL", + "currency_style": "symbol", + "decimals": 2 + } + } + }, + "metabase_version": "vLOCAL_DEV (06d1ba2ae111e66253209c01c244d6379acfc6dcb1911fa9ab6012cec9ce52e5)", + "parameters": [], + "dashboard_id": null, + "created_at": "2026-08-17T12:33:22.799833Z", + "public_uuid": null + }, + "updated_at": "2026-08-17T12:33:25.109933Z", + "col": 6, + "id": 110, + "parameter_mappings": [ + { + "parameter_id": "a1b2c3d4", + "card_id": 123, + "target": [ + "dimension", + [ + "field", + 1610, + null + ] + ] + } + ], + "card_id": 123, + "entity_id": "60ML78TqpDBQVuI3Ur9Uw", + "visualization_settings": {}, + "size_y": 3, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 2 + }, + { + "size_x": 3, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "cache_invalidated_at": null, + "description": null, + "archived": false, + "view_count": 5, + "collection_position": null, + "source_card_id": null, + "table_id": 171, + "result_metadata": [ + { + "semantic_type": "type/Currency", + "lib/deduplicated-name": "sum", + "lib/original-name": "sum", + "name": "sum", + "lib/source": "source/aggregations", + "lib/source-column-alias": "sum", + "source": "aggregation", + "field_ref": [ + "aggregation", + 0 + ], + "effective_type": "type/Float", + "lib/desired-column-alias": "sum", + "display_name": "Sum of Valor Divergencias", + "base_type": "type/Float" + } + ], + "embedding_type": null, + "initially_published_at": null, + "can_write": true, + "card_schema": 23, + "database_id": 2, + "enable_embedding": false, + "collection_id": 7, + "query_type": "query", + "download_perms": "full", + "name": "Divergências — valor", + "document_id": null, + "last_used_at": "2026-08-21T12:37:47.363586Z", + "type": "question", + "dimensions": null, + "query_average_duration": null, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2026-08-17T12:33:22.918803Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "lib/type": "mbql/query", + "database": 2, + "stages": [ + { + "source-table": 171, + "lib/type": "mbql.stage/mbql", + "aggregation": [ + [ + "sum", + { + "lib/uuid": "c0cf7fc4-69f4-4f9c-aa14-c160c65cd744" + }, + [ + "field", + { + "base-type": "type/Float", + "lib/uuid": "55f76a66-ab67-4dd2-8b3a-ec01ad8bf06c", + "effective-type": "type/Float" + }, + 1615 + ] + ] + ] + } + ] + }, + "dimension_mappings": null, + "id": 124, + "legacy_query": null, + "parameter_mappings": [], + "display": "scalar", + "archived_directly": false, + "entity_id": "iuEkiUctkJECWQObpq6u9", + "collection_preview": true, + "visualization_settings": { + "scalar.segments": [ + { + "min": 0, + "max": null, + "color": "#E75C58", + "label": "Divergência" + } + ], + "column_settings": { + "[\"name\",\"sum\"]": { + "number_style": "currency", + "currency": "BRL", + "currency_style": "symbol", + "decimals": 2 + } + } + }, + "metabase_version": "vLOCAL_DEV (06d1ba2ae111e66253209c01c244d6379acfc6dcb1911fa9ab6012cec9ce52e5)", + "parameters": [], + "dashboard_id": null, + "created_at": "2026-08-17T12:33:22.918803Z", + "public_uuid": null + }, + "updated_at": "2026-08-17T12:33:25.109933Z", + "col": 9, + "id": 111, + "parameter_mappings": [ + { + "parameter_id": "a1b2c3d4", + "card_id": 124, + "target": [ + "dimension", + [ + "field", + 1610, + null + ] + ] + } + ], + "card_id": 124, + "entity_id": "u-776DPLCiX8bf0PRmBq5", + "visualization_settings": {}, + "size_y": 3, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 2 + }, + { + "size_x": 3, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "cache_invalidated_at": null, + "description": null, + "archived": false, + "view_count": 5, + "collection_position": null, + "source_card_id": null, + "table_id": 171, + "result_metadata": [ + { + "lib/deduplicated-name": "pessoas", + "lib/original-name": "pessoas", + "name": "pessoas", + "lib/source": "source/aggregations", + "lib/source-column-alias": "pessoas", + "source": "aggregation", + "field_ref": [ + "aggregation", + 0 + ], + "effective_type": "type/Integer", + "lib/desired-column-alias": "pessoas", + "display_name": "pessoas", + "base_type": "type/Integer" + } + ], + "embedding_type": null, + "initially_published_at": null, + "can_write": true, + "card_schema": 23, + "database_id": 2, + "enable_embedding": false, + "collection_id": 7, + "query_type": "query", + "download_perms": "full", + "name": "Analisado — pessoas", + "document_id": null, + "last_used_at": "2026-08-21T12:37:47.363586Z", + "type": "question", + "dimensions": null, + "query_average_duration": null, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2026-08-17T12:33:23.04302Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "lib/type": "mbql/query", + "database": 2, + "stages": [ + { + "source-table": 171, + "lib/type": "mbql.stage/mbql", + "aggregation": [ + [ + "sum", + { + "name": "pessoas", + "lib/uuid": "e3bd3407-4437-4ab0-b32c-d82f46668a0a", + "display-name": "pessoas" + }, + [ + "field", + { + "base-type": "type/Integer", + "lib/uuid": "e0092ad9-8cdc-4bea-894f-2bfc942e4045", + "effective-type": "type/Integer" + }, + 1614 + ] + ] + ] + } + ] + }, + "dimension_mappings": null, + "id": 125, + "legacy_query": null, + "parameter_mappings": [], + "display": "scalar", + "archived_directly": false, + "entity_id": "2-GfZDVOhbOfRHVSQD6gG", + "collection_preview": true, + "visualization_settings": {}, + "metabase_version": "vLOCAL_DEV (06d1ba2ae111e66253209c01c244d6379acfc6dcb1911fa9ab6012cec9ce52e5)", + "parameters": [], + "dashboard_id": null, + "created_at": "2026-08-17T12:33:23.04302Z", + "public_uuid": null + }, + "updated_at": "2026-08-17T12:33:25.109933Z", + "col": 6, + "id": 112, + "parameter_mappings": [ + { + "parameter_id": "a1b2c3d4", + "card_id": 125, + "target": [ + "dimension", + [ + "field", + 1610, + null + ] + ] + } + ], + "card_id": 125, + "entity_id": "K7rs3EtLRIhlRSA-X2C99", + "visualization_settings": {}, + "size_y": 2, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 5 + }, + { + "size_x": 3, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "cache_invalidated_at": null, + "description": null, + "archived": false, + "view_count": 5, + "collection_position": null, + "source_card_id": null, + "table_id": 171, + "result_metadata": [ + { + "lib/deduplicated-name": "pessoas", + "lib/original-name": "pessoas", + "name": "pessoas", + "lib/source": "source/aggregations", + "lib/source-column-alias": "pessoas", + "source": "aggregation", + "field_ref": [ + "aggregation", + 0 + ], + "effective_type": "type/Integer", + "lib/desired-column-alias": "pessoas", + "display_name": "pessoas", + "base_type": "type/Integer" + } + ], + "embedding_type": null, + "initially_published_at": null, + "can_write": true, + "card_schema": 23, + "database_id": 2, + "enable_embedding": false, + "collection_id": 7, + "query_type": "query", + "download_perms": "full", + "name": "Divergências — pessoas", + "document_id": null, + "last_used_at": "2026-08-21T12:37:47.363586Z", + "type": "question", + "dimensions": null, + "query_average_duration": null, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2026-08-17T12:33:23.162083Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "lib/type": "mbql/query", + "database": 2, + "stages": [ + { + "source-table": 171, + "lib/type": "mbql.stage/mbql", + "aggregation": [ + [ + "sum", + { + "name": "pessoas", + "lib/uuid": "5c71bcdb-afc1-4b2c-85d6-49fa6bce22a7", + "display-name": "pessoas" + }, + [ + "field", + { + "base-type": "type/Integer", + "lib/uuid": "fe3b4557-e680-4808-a851-c0d9591cc06d", + "effective-type": "type/Integer" + }, + 1616 + ] + ] + ] + } + ] + }, + "dimension_mappings": null, + "id": 126, + "legacy_query": null, + "parameter_mappings": [], + "display": "scalar", + "archived_directly": false, + "entity_id": "dHvoxjnhXJIt6PJh8xLtm", + "collection_preview": true, + "visualization_settings": { + "scalar.segments": [ + { + "min": 0, + "max": null, + "color": "#E75C58", + "label": "Divergência" + } + ] + }, + "metabase_version": "vLOCAL_DEV (06d1ba2ae111e66253209c01c244d6379acfc6dcb1911fa9ab6012cec9ce52e5)", + "parameters": [], + "dashboard_id": null, + "created_at": "2026-08-17T12:33:23.162083Z", + "public_uuid": null + }, + "updated_at": "2026-08-17T12:33:25.109933Z", + "col": 9, + "id": 113, + "parameter_mappings": [ + { + "parameter_id": "a1b2c3d4", + "card_id": 126, + "target": [ + "dimension", + [ + "field", + 1610, + null + ] + ] + } + ], + "card_id": 126, + "entity_id": "hos45pp1AJz8bzWZJUU67", + "visualization_settings": {}, + "size_y": 2, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 5 + }, + { + "size_x": 3, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "cache_invalidated_at": null, + "description": null, + "archived": false, + "view_count": 5, + "collection_position": null, + "source_card_id": null, + "table_id": 171, + "result_metadata": [ + { + "lib/deduplicated-name": "pct", + "lib/original-name": "pct", + "name": "pct", + "lib/source": "source/aggregations", + "lib/source-column-alias": "pct", + "source": "aggregation", + "field_ref": [ + "aggregation", + 0 + ], + "effective_type": "type/Float", + "lib/desired-column-alias": "pct", + "display_name": "% analisado", + "base_type": "type/Float" + } + ], + "embedding_type": null, + "initially_published_at": null, + "can_write": true, + "card_schema": 23, + "database_id": 2, + "enable_embedding": false, + "collection_id": 7, + "query_type": "query", + "download_perms": "full", + "name": "Analisado — % do valor", + "document_id": null, + "last_used_at": "2026-08-21T12:37:47.363586Z", + "type": "question", + "dimensions": null, + "query_average_duration": null, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2026-08-17T12:33:23.29242Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "lib/type": "mbql/query", + "database": 2, + "stages": [ + { + "source-table": 171, + "lib/type": "mbql.stage/mbql", + "aggregation": [ + [ + "/", + { + "name": "pct", + "lib/uuid": "e85038c8-9eac-4878-93ff-e5d568c4ff58", + "display-name": "% analisado" + }, + [ + "sum", + { + "lib/uuid": "fc70a984-aee5-4298-aabf-598aa22a5166" + }, + [ + "field", + { + "base-type": "type/Float", + "lib/uuid": "60fce310-ce23-46c6-86c6-198c35499df7", + "effective-type": "type/Float" + }, + 1613 + ] + ], + [ + "sum", + { + "lib/uuid": "3471170d-e9b5-4491-9d9c-1b3abffba0ee" + }, + [ + "field", + { + "base-type": "type/Float", + "lib/uuid": "eeedae75-3b1e-479b-877d-ed58d81fa73b", + "effective-type": "type/Float" + }, + 1611 + ] + ] + ] + ] + } + ] + }, + "dimension_mappings": null, + "id": 127, + "legacy_query": null, + "parameter_mappings": [], + "display": "scalar", + "archived_directly": false, + "entity_id": "iQ72jYHDj4uP93fdhAeCn", + "collection_preview": true, + "visualization_settings": { + "column_settings": { + "[\"name\",\"pct\"]": { + "number_style": "percent", + "decimals": 1 + } + } + }, + "metabase_version": "vLOCAL_DEV (06d1ba2ae111e66253209c01c244d6379acfc6dcb1911fa9ab6012cec9ce52e5)", + "parameters": [], + "dashboard_id": null, + "created_at": "2026-08-17T12:33:23.29242Z", + "public_uuid": null + }, + "updated_at": "2026-08-17T12:36:40.333137Z", + "col": 6, + "id": 114, + "parameter_mappings": [ + { + "parameter_id": "a1b2c3d4", + "card_id": 127, + "target": [ + "dimension", + [ + "field", + 1610, + null + ] + ] + } + ], + "card_id": 127, + "entity_id": "M99R-NOXXA1RX9HoJSXBF", + "visualization_settings": {}, + "size_y": 2, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 7 + }, + { + "size_x": 3, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "cache_invalidated_at": null, + "description": null, + "archived": false, + "view_count": 5, + "collection_position": null, + "source_card_id": null, + "table_id": 171, + "result_metadata": [ + { + "lib/deduplicated-name": "pct", + "lib/original-name": "pct", + "name": "pct", + "lib/source": "source/aggregations", + "lib/source-column-alias": "pct", + "source": "aggregation", + "field_ref": [ + "aggregation", + 0 + ], + "effective_type": "type/Float", + "lib/desired-column-alias": "pct", + "display_name": "% divergências", + "base_type": "type/Float" + } + ], + "embedding_type": null, + "initially_published_at": null, + "can_write": true, + "card_schema": 23, + "database_id": 2, + "enable_embedding": false, + "collection_id": 7, + "query_type": "query", + "download_perms": "full", + "name": "Divergências — % do valor", + "document_id": null, + "last_used_at": "2026-08-21T12:37:47.363586Z", + "type": "question", + "dimensions": null, + "query_average_duration": null, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2026-08-17T12:33:23.41191Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "lib/type": "mbql/query", + "database": 2, + "stages": [ + { + "source-table": 171, + "lib/type": "mbql.stage/mbql", + "aggregation": [ + [ + "/", + { + "name": "pct", + "lib/uuid": "97a37d35-4605-466d-905c-cf3ed2c66c52", + "display-name": "% divergências" + }, + [ + "sum", + { + "lib/uuid": "e8898168-18fd-4cf2-bb3c-22220df841f7" + }, + [ + "field", + { + "base-type": "type/Float", + "lib/uuid": "d4e4b11b-53e3-4678-b7c7-d42da417901d", + "effective-type": "type/Float" + }, + 1615 + ] + ], + [ + "sum", + { + "lib/uuid": "55d3b288-d36b-4798-b402-dff0d9015b43" + }, + [ + "field", + { + "base-type": "type/Float", + "lib/uuid": "d295443a-bb22-49b7-9f1e-8645754a5b4f", + "effective-type": "type/Float" + }, + 1611 + ] + ] + ] + ] + } + ] + }, + "dimension_mappings": null, + "id": 128, + "legacy_query": null, + "parameter_mappings": [], + "display": "scalar", + "archived_directly": false, + "entity_id": "PmSxAO2nF0-OWjiwswqvS", + "collection_preview": true, + "visualization_settings": { + "scalar.segments": [ + { + "min": 0, + "max": null, + "color": "#E75C58", + "label": "Divergência" + } + ], + "column_settings": { + "[\"name\",\"pct\"]": { + "number_style": "percent", + "decimals": 1 + } + } + }, + "metabase_version": "vLOCAL_DEV (06d1ba2ae111e66253209c01c244d6379acfc6dcb1911fa9ab6012cec9ce52e5)", + "parameters": [], + "dashboard_id": null, + "created_at": "2026-08-17T12:33:23.41191Z", + "public_uuid": null + }, + "updated_at": "2026-08-17T12:36:40.333137Z", + "col": 9, + "id": 115, + "parameter_mappings": [ + { + "parameter_id": "a1b2c3d4", + "card_id": 128, + "target": [ + "dimension", + [ + "field", + 1610, + null + ] + ] + } + ], + "card_id": 128, + "entity_id": "iE-1mUvyNFBtuTMkMNlyD", + "visualization_settings": {}, + "size_y": 2, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 7 + }, + { + "size_x": 2, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "cache_invalidated_at": null, + "description": null, + "archived": false, + "view_count": 5, + "collection_position": null, + "source_card_id": null, + "table_id": 172, + "result_metadata": [ + { + "database_type": "TEXT", + "semantic_type": "type/Category", + "table_id": 172, + "lib/deduplicated-name": "motivo", + "lib/original-name": "motivo", + "lib/breakout?": true, + "name": "motivo", + "lib/source": "source/table-defaults", + "lib/source-column-alias": "motivo", + "lib/original-display-name": "Motivo", + "source": "breakout", + "field_ref": [ + "field", + 1619, + null + ], + "lib/transformation-added-base-type": true, + "effective_type": "type/Text", + "active": true, + "id": 1619, + "lib/desired-column-alias": "motivo", + "position": 2, + "visibility_type": "normal", + "display_name": "Motivo", + "fingerprint": { + "global": { + "distinct-count": 5, + "nil%": 0.0 + }, + "type": { + "type/Text": { + "percent-json": 0.0, + "percent-url": 0.0, + "percent-email": 0.0, + "percent-state": 0.0, + "average-length": 17.2 + } + } + }, + "base_type": "type/Text" + }, + { + "semantic_type": "type/Currency", + "lib/deduplicated-name": "valor", + "lib/original-name": "valor", + "name": "valor", + "lib/source": "source/aggregations", + "lib/source-column-alias": "valor", + "source": "aggregation", + "field_ref": [ + "aggregation", + 0 + ], + "effective_type": "type/Float", + "lib/desired-column-alias": "valor", + "display_name": "valor", + "base_type": "type/Float" + } + ], + "embedding_type": null, + "initially_published_at": null, + "can_write": true, + "card_schema": 23, + "database_id": 2, + "enable_embedding": false, + "collection_id": 7, + "query_type": "query", + "download_perms": "full", + "name": "Top 3 por motivo", + "document_id": null, + "last_used_at": "2026-08-21T12:37:47.363586Z", + "type": "question", + "dimensions": null, + "query_average_duration": null, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2026-08-17T12:33:23.547811Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "lib/type": "mbql/query", + "database": 2, + "stages": [ + { + "order-by": [ + [ + "desc", + { + "lib/uuid": "78b5c760-1de5-4d1a-9eda-cfaf35e233f1" + }, + [ + "aggregation", + { + "lib/uuid": "7b40b3d3-d001-4781-a8f6-e70e7941eabd" + }, + "1cc93080-89b8-4b5e-bc58-05d70e85615f" + ] + ] + ], + "lib/type": "mbql.stage/mbql", + "source-table": 172, + "aggregation": [ + [ + "sum", + { + "name": "valor", + "lib/uuid": "1cc93080-89b8-4b5e-bc58-05d70e85615f", + "display-name": "valor" + }, + [ + "field", + { + "base-type": "type/Float", + "lib/uuid": "d13a7a4a-cad5-4050-b7fb-32a409b34580", + "effective-type": "type/Float" + }, + 1621 + ] + ] + ], + "breakout": [ + [ + "field", + { + "base-type": "type/Text", + "lib/uuid": "f10c2471-906d-4e8e-951e-704481053e96", + "effective-type": "type/Text" + }, + 1619 + ] + ], + "limit": 3 + } + ] + }, + "dimension_mappings": null, + "id": 129, + "legacy_query": null, + "parameter_mappings": [], + "display": "bar", + "archived_directly": false, + "entity_id": "1A_KOysA3SOXKBKP9W9u2", + "collection_preview": true, + "visualization_settings": { + "graph.dimensions": [ + "motivo" + ], + "graph.show_values": true, + "graph.label_value_formatting": "compact", + "graph.x_axis.labels_enabled": false, + "column_settings": { + "[\"name\",\"valor\"]": { + "number_style": "currency", + "currency": "BRL", + "currency_style": "symbol", + "decimals": 2 + } + }, + "graph.metrics": [ + "valor" + ] + }, + "metabase_version": "vLOCAL_DEV (06d1ba2ae111e66253209c01c244d6379acfc6dcb1911fa9ab6012cec9ce52e5)", + "parameters": [], + "dashboard_id": null, + "created_at": "2026-08-17T12:33:23.547811Z", + "public_uuid": null + }, + "updated_at": "2026-08-17T12:36:40.333137Z", + "col": 6, + "id": 116, + "parameter_mappings": [ + { + "parameter_id": "a1b2c3d4", + "card_id": 129, + "target": [ + "dimension", + [ + "field", + 1617, + null + ] + ] + } + ], + "card_id": 129, + "entity_id": "zqEcvYVym7H3XriAp4lrb", + "visualization_settings": {}, + "size_y": 7, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 10 + }, + { + "size_x": 2, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "cache_invalidated_at": null, + "description": null, + "archived": false, + "view_count": 5, + "collection_position": null, + "source_card_id": null, + "table_id": 172, + "result_metadata": [ + { + "database_type": "TEXT", + "semantic_type": "type/Category", + "table_id": 172, + "lib/deduplicated-name": "orgao", + "lib/original-name": "orgao", + "lib/breakout?": true, + "name": "orgao", + "lib/source": "source/table-defaults", + "lib/source-column-alias": "orgao", + "lib/original-display-name": "Orgao", + "source": "breakout", + "field_ref": [ + "field", + 1618, + null + ], + "lib/transformation-added-base-type": true, + "effective_type": "type/Text", + "active": true, + "id": 1618, + "lib/desired-column-alias": "orgao", + "position": 1, + "visibility_type": "normal", + "display_name": "Orgao", + "fingerprint": { + "global": { + "distinct-count": 6, + "nil%": 0.0 + }, + "type": { + "type/Text": { + "percent-json": 0.0, + "percent-url": 0.0, + "percent-email": 0.0, + "percent-state": 0.0, + "average-length": 4.833333333333333 + } + } + }, + "base_type": "type/Text" + }, + { + "semantic_type": "type/Currency", + "lib/deduplicated-name": "valor", + "lib/original-name": "valor", + "name": "valor", + "lib/source": "source/aggregations", + "lib/source-column-alias": "valor", + "source": "aggregation", + "field_ref": [ + "aggregation", + 0 + ], + "effective_type": "type/Float", + "lib/desired-column-alias": "valor", + "display_name": "valor", + "base_type": "type/Float" + } + ], + "embedding_type": null, + "initially_published_at": null, + "can_write": true, + "card_schema": 23, + "database_id": 2, + "enable_embedding": false, + "collection_id": 7, + "query_type": "query", + "download_perms": "full", + "name": "Top 3 por órgão", + "document_id": null, + "last_used_at": "2026-08-21T12:37:47.363586Z", + "type": "question", + "dimensions": null, + "query_average_duration": null, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2026-08-17T12:33:23.685702Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "lib/type": "mbql/query", + "database": 2, + "stages": [ + { + "order-by": [ + [ + "desc", + { + "lib/uuid": "4038f227-ba34-4045-b994-f7850fa64b80" + }, + [ + "aggregation", + { + "lib/uuid": "15d37782-20f7-47b5-93e8-69a40e7f7a1e" + }, + "fefe303e-de68-4034-a596-ba7295c1d4f5" + ] + ] + ], + "lib/type": "mbql.stage/mbql", + "source-table": 172, + "aggregation": [ + [ + "sum", + { + "name": "valor", + "lib/uuid": "fefe303e-de68-4034-a596-ba7295c1d4f5", + "display-name": "valor" + }, + [ + "field", + { + "base-type": "type/Float", + "lib/uuid": "564e5a4d-6dd6-44fa-befc-c958dc374d21", + "effective-type": "type/Float" + }, + 1621 + ] + ] + ], + "breakout": [ + [ + "field", + { + "base-type": "type/Text", + "lib/uuid": "6acf80aa-8014-4eca-8b40-03ba9aa389c0", + "effective-type": "type/Text" + }, + 1618 + ] + ], + "limit": 3 + } + ] + }, + "dimension_mappings": null, + "id": 130, + "legacy_query": null, + "parameter_mappings": [], + "display": "bar", + "archived_directly": false, + "entity_id": "-S9qKF8u1TSLo8p9bRvIA", + "collection_preview": true, + "visualization_settings": { + "graph.dimensions": [ + "orgao" + ], + "graph.show_values": true, + "graph.label_value_formatting": "compact", + "graph.x_axis.labels_enabled": false, + "column_settings": { + "[\"name\",\"valor\"]": { + "number_style": "currency", + "currency": "BRL", + "currency_style": "symbol", + "decimals": 2 + } + }, + "graph.metrics": [ + "valor" + ] + }, + "metabase_version": "vLOCAL_DEV (06d1ba2ae111e66253209c01c244d6379acfc6dcb1911fa9ab6012cec9ce52e5)", + "parameters": [], + "dashboard_id": null, + "created_at": "2026-08-17T12:33:23.685702Z", + "public_uuid": null + }, + "updated_at": "2026-08-17T12:36:40.333137Z", + "col": 8, + "id": 117, + "parameter_mappings": [ + { + "parameter_id": "a1b2c3d4", + "card_id": 130, + "target": [ + "dimension", + [ + "field", + 1617, + null + ] + ] + } + ], + "card_id": 130, + "entity_id": "VbV4bzfCkF81vTFY-MT9u", + "visualization_settings": {}, + "size_y": 7, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 10 + }, + { + "size_x": 2, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "cache_invalidated_at": null, + "description": null, + "archived": false, + "view_count": 5, + "collection_position": null, + "source_card_id": null, + "table_id": 172, + "result_metadata": [ + { + "database_type": "DATE", + "semantic_type": "type/CreationDate", + "table_id": 172, + "lib/temporal-unit": "month", + "lib/deduplicated-name": "competencia", + "lib/original-name": "competencia", + "lib/breakout?": true, + "unit": "month", + "name": "competencia", + "lib/source": "source/table-defaults", + "lib/source-column-alias": "competencia", + "lib/original-display-name": "Competencia", + "source": "breakout", + "field_ref": [ + "field", + 1617, + { + "temporal-unit": "month" + } + ], + "lib/transformation-added-base-type": true, + "effective_type": "type/Date", + "active": true, + "id": 1617, + "lib/desired-column-alias": "competencia", + "position": 0, + "visibility_type": "normal", + "inherited_temporal_unit": "month", + "display_name": "Competencia: Month", + "fingerprint": { + "global": { + "distinct-count": 6, + "nil%": 0.0 + }, + "type": { + "type/DateTime": { + "earliest": "2025-07-01", + "latest": "2025-12-01" + } + } + }, + "base_type": "type/Date" + }, + { + "semantic_type": "type/Currency", + "lib/deduplicated-name": "valor", + "lib/original-name": "valor", + "name": "valor", + "lib/source": "source/aggregations", + "lib/source-column-alias": "valor", + "source": "aggregation", + "field_ref": [ + "aggregation", + 0 + ], + "effective_type": "type/Float", + "lib/desired-column-alias": "valor", + "display_name": "valor", + "base_type": "type/Float" + } + ], + "embedding_type": null, + "initially_published_at": null, + "can_write": true, + "card_schema": 23, + "database_id": 2, + "enable_embedding": false, + "collection_id": 7, + "query_type": "query", + "download_perms": "full", + "name": "Top 3 por mês", + "document_id": null, + "last_used_at": "2026-08-21T12:37:47.363586Z", + "type": "question", + "dimensions": null, + "query_average_duration": null, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2026-08-17T12:33:23.844399Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "lib/type": "mbql/query", + "database": 2, + "stages": [ + { + "order-by": [ + [ + "desc", + { + "lib/uuid": "3883aaa0-170a-42de-b26b-4b6af320a0c4" + }, + [ + "aggregation", + { + "lib/uuid": "a331bc43-924c-4a84-bc9d-d6a07d595bf6" + }, + "4f3b282e-9b5b-4304-be7e-52991c99d18d" + ] + ] + ], + "lib/type": "mbql.stage/mbql", + "source-table": 172, + "aggregation": [ + [ + "sum", + { + "name": "valor", + "lib/uuid": "4f3b282e-9b5b-4304-be7e-52991c99d18d", + "display-name": "valor" + }, + [ + "field", + { + "base-type": "type/Float", + "lib/uuid": "388e9c8d-62fe-434a-80a4-a38ed2756327", + "effective-type": "type/Float" + }, + 1621 + ] + ] + ], + "breakout": [ + [ + "field", + { + "base-type": "type/Date", + "temporal-unit": "month", + "lib/uuid": "d74baf92-e339-4c90-85e8-059934db7922", + "effective-type": "type/Date" + }, + 1617 + ] + ], + "limit": 3 + } + ] + }, + "dimension_mappings": null, + "id": 131, + "legacy_query": null, + "parameter_mappings": [], + "display": "bar", + "archived_directly": false, + "entity_id": "L7pq4n5c7-MGZ5E_YcKd5", + "collection_preview": true, + "visualization_settings": { + "graph.dimensions": [ + "competencia" + ], + "graph.show_values": true, + "graph.label_value_formatting": "compact", + "graph.x_axis.labels_enabled": false, + "column_settings": { + "[\"name\",\"valor\"]": { + "number_style": "currency", + "currency": "BRL", + "currency_style": "symbol", + "decimals": 2 + } + }, + "graph.metrics": [ + "valor" + ] + }, + "metabase_version": "vLOCAL_DEV (06d1ba2ae111e66253209c01c244d6379acfc6dcb1911fa9ab6012cec9ce52e5)", + "parameters": [], + "dashboard_id": null, + "created_at": "2026-08-17T12:33:23.844399Z", + "public_uuid": null + }, + "updated_at": "2026-08-17T12:36:40.333137Z", + "col": 10, + "id": 118, + "parameter_mappings": [ + { + "parameter_id": "a1b2c3d4", + "card_id": 131, + "target": [ + "dimension", + [ + "field", + 1617, + null + ] + ] + } + ], + "card_id": 131, + "entity_id": "CKMmxawVZVw_aRir1ujup", + "visualization_settings": {}, + "size_y": 7, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 10 + }, + { + "size_x": 6, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "cache_invalidated_at": null, + "description": null, + "archived": false, + "view_count": 5, + "collection_position": null, + "source_card_id": null, + "table_id": null, + "result_metadata": [ + { + "name": "orgao", + "database_type": "TEXT", + "base_type": "type/Text", + "display_name": "orgao", + "field_ref": [ + "field", + "orgao", + { + "base-type": "type/Text" + } + ] + }, + { + "name": "divergencia_financeira_bruta", + "database_type": "NUMERIC", + "base_type": "type/Float", + "display_name": "divergencia_financeira_bruta", + "field_ref": [ + "field", + "divergencia_financeira_bruta", + { + "base-type": "type/Float" + } + ] + }, + { + "name": "divergencia_normativa", + "database_type": "NUMERIC", + "base_type": "type/Float", + "display_name": "divergencia_normativa", + "field_ref": [ + "field", + "divergencia_normativa", + { + "base-type": "type/Float" + } + ] + }, + { + "name": "pessoas", + "database_type": "NUMERIC", + "base_type": "type/Float", + "display_name": "pessoas", + "field_ref": [ + "field", + "pessoas", + { + "base-type": "type/Float" + } + ] + } + ], + "embedding_type": null, + "initially_published_at": null, + "can_write": true, + "card_schema": 23, + "database_id": 2, + "enable_embedding": false, + "collection_id": 7, + "query_type": "native", + "download_perms": "full", + "name": "Análise / detalhamento divergências", + "document_id": null, + "last_used_at": "2026-08-21T12:37:47.363586Z", + "type": "question", + "dimensions": null, + "query_average_duration": null, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2026-08-17T12:33:24.036299Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "lib/type": "mbql/query", + "database": 2, + "stages": [ + { + "template-tags": { + "periodo": { + "dimension": [ + "field", + { + "base-type": "type/Date", + "lib/uuid": "1e328897-87db-46fc-a06c-ea5391666b1d", + "effective-type": "type/Date" + }, + 1617 + ], + "id": "c0ffee01", + "name": "periodo", + "display-name": "Período", + "type": "dimension", + "widget-type": "date/all-options" + } + }, + "lib/type": "mbql.stage/native", + "native": "SELECT\n orgao,\n SUM(valor) AS divergencia_financeira_bruta,\n AVG(score_normativa) AS divergencia_normativa,\n SUM(qtd_pessoas) AS pessoas\nFROM abono_divergencia\nWHERE {{periodo}}\nGROUP BY orgao" + } + ] + }, + "dimension_mappings": null, + "id": 132, + "legacy_query": null, + "parameter_mappings": [], + "display": "scatter", + "archived_directly": false, + "entity_id": "9J5gkPgGm5-xamdiAHzQf", + "collection_preview": true, + "visualization_settings": { + "graph.dimensions": [ + "divergencia_financeira_bruta" + ], + "scatter.bubble": "pessoas", + "graph.x_axis.title_text": "Divergência financeira bruta", + "graph.y_axis.title_text": "Divergência normativa", + "click_behavior": { + "type": "crossfilter", + "parameterMapping": { + "b2c3d4e5": { + "id": "b2c3d4e5", + "source": { + "type": "column", + "id": "orgao", + "name": "orgao" + }, + "target": { + "type": "parameter", + "id": "b2c3d4e5" + } + } + } + }, + "column_settings": { + "[\"name\",\"divergencia_financeira_bruta\"]": { + "number_style": "currency", + "currency": "BRL", + "currency_style": "symbol", + "decimals": 2 + }, + "[\"name\",\"divergencia_normativa\"]": { + "decimals": 1 + } + }, + "graph.metrics": [ + "divergencia_normativa" + ] + }, + "metabase_version": "vLOCAL_DEV (06d1ba2ae111e66253209c01c244d6379acfc6dcb1911fa9ab6012cec9ce52e5)", + "parameters": [], + "dashboard_id": null, + "created_at": "2026-08-17T12:33:24.036299Z", + "public_uuid": null + }, + "updated_at": "2026-08-17T12:36:40.333137Z", + "col": 12, + "id": 119, + "parameter_mappings": [ + { + "parameter_id": "a1b2c3d4", + "card_id": 132, + "target": [ + "dimension", + [ + "template-tag", + "periodo" + ] + ] + } + ], + "card_id": 132, + "entity_id": "fPDcFAQrd5v7vb0IBLRzS", + "visualization_settings": { + "graph.dimensions": [ + "divergencia_financeira_bruta" + ], + "scatter.bubble": "pessoas", + "graph.x_axis.title_text": "Divergência financeira bruta", + "graph.y_axis.title_text": "Divergência normativa", + "click_behavior": { + "type": "crossfilter", + "parameterMapping": { + "b2c3d4e5": { + "id": "b2c3d4e5", + "source": { + "type": "column", + "id": "orgao", + "name": "orgao" + }, + "target": { + "type": "parameter", + "id": "b2c3d4e5" + } + } + } + }, + "column_settings": { + "[\"name\",\"divergencia_financeira_bruta\"]": { + "number_style": "currency", + "currency": "BRL", + "currency_style": "symbol", + "decimals": 2 + }, + "[\"name\",\"divergencia_normativa\"]": { + "decimals": 1 + } + }, + "graph.metrics": [ + "divergencia_normativa" + ] + }, + "size_y": 14, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 4 + }, + { + "size_x": 6, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "cache_invalidated_at": null, + "description": null, + "archived": false, + "view_count": 15, + "collection_position": null, + "source_card_id": null, + "table_id": null, + "result_metadata": [ + { + "name": "motivo", + "database_type": "TEXT", + "base_type": "type/Text", + "display_name": "motivo", + "field_ref": [ + "field", + "motivo", + { + "base-type": "type/Text" + } + ] + }, + { + "name": "valor", + "database_type": "NUMERIC", + "base_type": "type/Float", + "display_name": "valor", + "field_ref": [ + "field", + "valor", + { + "base-type": "type/Float" + } + ] + }, + { + "name": "percentual", + "database_type": "NUMERIC", + "base_type": "type/Float", + "display_name": "percentual", + "semantic_type": "type/Share", + "field_ref": [ + "field", + "percentual", + { + "base-type": "type/Float" + } + ] + } + ], + "embedding_type": null, + "initially_published_at": null, + "can_write": true, + "card_schema": 23, + "database_id": 2, + "enable_embedding": false, + "collection_id": 7, + "query_type": "native", + "download_perms": "full", + "name": "Divergência financeira por motivo", + "document_id": null, + "last_used_at": "2026-08-21T12:37:47.363586Z", + "type": "question", + "dimensions": null, + "query_average_duration": null, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2026-08-17T12:33:24.179014Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "lib/type": "mbql/query", + "database": 2, + "stages": [ + { + "template-tags": { + "periodo": { + "dimension": [ + "field", + { + "base-type": "type/Date", + "lib/uuid": "fb83508f-19c9-482b-b4bb-51f267fd1d68", + "effective-type": "type/Date" + }, + 1617 + ], + "id": "c0ffee01", + "name": "periodo", + "display-name": "Período", + "type": "dimension", + "widget-type": "date/all-options" + }, + "orgao": { + "dimension": [ + "field", + { + "base-type": "type/Text", + "lib/uuid": "13da3a6d-80dc-43e5-b9f9-087a14616535", + "effective-type": "type/Text" + }, + 1618 + ], + "id": "c0ffee02", + "name": "orgao", + "display-name": "Órgão", + "type": "dimension", + "widget-type": "string/=" + } + }, + "lib/type": "mbql.stage/native", + "native": "SELECT\n motivo,\n SUM(valor) AS valor,\n ROUND(100.0 * SUM(valor) / SUM(SUM(valor)) OVER (), 1) AS percentual\nFROM abono_divergencia\nWHERE {{periodo}} AND {{orgao}}\nGROUP BY motivo\nORDER BY valor DESC" + } + ] + }, + "dimension_mappings": null, + "id": 133, + "legacy_query": null, + "parameter_mappings": [], + "display": "table", + "archived_directly": false, + "entity_id": "XfLFudnvzqfNC98dKk_Wa", + "collection_preview": true, + "visualization_settings": { + "table.pivot": false, + "column_settings": { + "[\"name\",\"valor\"]": { + "number_style": "currency", + "currency": "BRL", + "currency_style": "symbol", + "decimals": 2 + }, + "[\"name\",\"percentual\"]": { + "suffix": " %", + "decimals": 1 + } + } + }, + "metabase_version": "vLOCAL_DEV (06d1ba2ae111e66253209c01c244d6379acfc6dcb1911fa9ab6012cec9ce52e5)", + "parameters": [], + "dashboard_id": null, + "created_at": "2026-08-17T12:33:24.179014Z", + "public_uuid": null + }, + "updated_at": "2026-08-17T12:36:40.333137Z", + "col": 18, + "id": 120, + "parameter_mappings": [ + { + "parameter_id": "a1b2c3d4", + "card_id": 133, + "target": [ + "dimension", + [ + "template-tag", + "periodo" + ] + ] + }, + { + "parameter_id": "b2c3d4e5", + "card_id": 133, + "target": [ + "dimension", + [ + "template-tag", + "orgao" + ] + ] + } + ], + "card_id": 133, + "entity_id": "hed_Asf0vATq89LmlRAEf", + "visualization_settings": {}, + "size_y": 7, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 4 + }, + { + "size_x": 6, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "cache_invalidated_at": null, + "description": null, + "archived": false, + "view_count": 15, + "collection_position": null, + "source_card_id": null, + "table_id": null, + "result_metadata": [ + { + "name": "cargo", + "database_type": "TEXT", + "base_type": "type/Text", + "display_name": "cargo", + "field_ref": [ + "field", + "cargo", + { + "base-type": "type/Text" + } + ] + }, + { + "name": "pessoas", + "database_type": "NUMERIC", + "base_type": "type/Float", + "display_name": "pessoas", + "field_ref": [ + "field", + "pessoas", + { + "base-type": "type/Float" + } + ] + }, + { + "name": "percentual", + "database_type": "NUMERIC", + "base_type": "type/Float", + "display_name": "percentual", + "semantic_type": "type/Share", + "field_ref": [ + "field", + "percentual", + { + "base-type": "type/Float" + } + ] + } + ], + "embedding_type": null, + "initially_published_at": null, + "can_write": true, + "card_schema": 23, + "database_id": 2, + "enable_embedding": false, + "collection_id": 7, + "query_type": "native", + "download_perms": "full", + "name": "Pessoal afetado", + "document_id": null, + "last_used_at": "2026-08-21T12:37:47.363586Z", + "type": "question", + "dimensions": null, + "query_average_duration": null, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2026-08-17T12:33:24.312324Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "lib/type": "mbql/query", + "database": 2, + "stages": [ + { + "template-tags": { + "periodo": { + "dimension": [ + "field", + { + "base-type": "type/Date", + "lib/uuid": "94a57599-7cee-4821-96f5-142c4e92c0d6", + "effective-type": "type/Date" + }, + 1617 + ], + "id": "c0ffee01", + "name": "periodo", + "display-name": "Período", + "type": "dimension", + "widget-type": "date/all-options" + }, + "orgao": { + "dimension": [ + "field", + { + "base-type": "type/Text", + "lib/uuid": "106503ac-7707-458d-92a8-947378dd48d3", + "effective-type": "type/Text" + }, + 1618 + ], + "id": "c0ffee02", + "name": "orgao", + "display-name": "Órgão", + "type": "dimension", + "widget-type": "string/=" + } + }, + "lib/type": "mbql.stage/native", + "native": "SELECT\n cargo,\n SUM(qtd_pessoas) AS pessoas,\n ROUND(100.0 * SUM(qtd_pessoas) / SUM(SUM(qtd_pessoas)) OVER (), 1) AS percentual\nFROM abono_divergencia\nWHERE {{periodo}} AND {{orgao}}\nGROUP BY cargo\nORDER BY pessoas DESC" + } + ] + }, + "dimension_mappings": null, + "id": 134, + "legacy_query": null, + "parameter_mappings": [], + "display": "table", + "archived_directly": false, + "entity_id": "o8Y0mmKpi3Vx_gkN2mJws", + "collection_preview": true, + "visualization_settings": { + "column_settings": { + "[\"name\",\"percentual\"]": { + "suffix": " %", + "decimals": 1 + } + } + }, + "metabase_version": "vLOCAL_DEV (06d1ba2ae111e66253209c01c244d6379acfc6dcb1911fa9ab6012cec9ce52e5)", + "parameters": [], + "dashboard_id": null, + "created_at": "2026-08-17T12:33:24.312324Z", + "public_uuid": null + }, + "updated_at": "2026-08-17T12:36:40.333137Z", + "col": 18, + "id": 121, + "parameter_mappings": [ + { + "parameter_id": "a1b2c3d4", + "card_id": 134, + "target": [ + "dimension", + [ + "template-tag", + "periodo" + ] + ] + }, + { + "parameter_id": "b2c3d4e5", + "card_id": 134, + "target": [ + "dimension", + [ + "template-tag", + "orgao" + ] + ] + } + ], + "card_id": 134, + "entity_id": "qdJR1Tn78xPwtPpYDtvag", + "visualization_settings": {}, + "size_y": 5, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 11 + }, + { + "size_x": 6, + "dashboard_tab_id": null, + "series": [], + "inline_parameters": [], + "action_id": null, + "collection_authority_level": null, + "card": { + "cache_invalidated_at": null, + "description": null, + "archived": false, + "view_count": 15, + "collection_position": null, + "source_card_id": null, + "table_id": null, + "result_metadata": [ + { + "name": "media_mensal", + "database_type": "NUMERIC", + "base_type": "type/Float", + "display_name": "media_mensal", + "field_ref": [ + "field", + "media_mensal", + { + "base-type": "type/Float" + } + ] + } + ], + "embedding_type": null, + "initially_published_at": null, + "can_write": true, + "card_schema": 23, + "database_id": 2, + "enable_embedding": false, + "collection_id": 7, + "query_type": "native", + "download_perms": "full", + "name": "Divergência média mensal", + "document_id": null, + "last_used_at": "2026-08-21T12:37:47.363586Z", + "type": "question", + "dimensions": null, + "query_average_duration": null, + "creator_id": 1, + "moderation_reviews": [], + "updated_at": "2026-08-17T12:33:24.44477Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "dataset_query": { + "lib/type": "mbql/query", + "database": 2, + "stages": [ + { + "template-tags": { + "periodo": { + "dimension": [ + "field", + { + "base-type": "type/Date", + "lib/uuid": "665da6b5-015b-42cd-8e73-181a5793ce3a", + "effective-type": "type/Date" + }, + 1617 + ], + "id": "c0ffee01", + "name": "periodo", + "display-name": "Período", + "type": "dimension", + "widget-type": "date/all-options" + }, + "orgao": { + "dimension": [ + "field", + { + "base-type": "type/Text", + "lib/uuid": "edad5dd5-7a33-457e-b363-ec0b3ef5fc83", + "effective-type": "type/Text" + }, + 1618 + ], + "id": "c0ffee02", + "name": "orgao", + "display-name": "Órgão", + "type": "dimension", + "widget-type": "string/=" + } + }, + "lib/type": "mbql.stage/native", + "native": "SELECT ROUND(SUM(valor) * 1.0 / COUNT(DISTINCT competencia), 2) AS media_mensal\nFROM abono_divergencia\nWHERE {{periodo}} AND {{orgao}}" + } + ] + }, + "dimension_mappings": null, + "id": 135, + "legacy_query": null, + "parameter_mappings": [], + "display": "scalar", + "archived_directly": false, + "entity_id": "VgghGwXlbEGA1v6GcaKsS", + "collection_preview": true, + "visualization_settings": { + "column_settings": { + "[\"name\",\"media_mensal\"]": { + "number_style": "currency", + "currency": "BRL", + "currency_style": "symbol", + "decimals": 2 + } + } + }, + "metabase_version": "vLOCAL_DEV (06d1ba2ae111e66253209c01c244d6379acfc6dcb1911fa9ab6012cec9ce52e5)", + "parameters": [], + "dashboard_id": null, + "created_at": "2026-08-17T12:33:24.44477Z", + "public_uuid": null + }, + "updated_at": "2026-08-17T12:36:40.333137Z", + "col": 18, + "id": 122, + "parameter_mappings": [ + { + "parameter_id": "a1b2c3d4", + "card_id": 135, + "target": [ + "dimension", + [ + "template-tag", + "periodo" + ] + ] + }, + { + "parameter_id": "b2c3d4e5", + "card_id": 135, + "target": [ + "dimension", + [ + "template-tag", + "orgao" + ] + ] + } + ], + "card_id": 135, + "entity_id": "O4IWtbIHEOAjquMUrE_yt", + "visualization_settings": {}, + "size_y": 2, + "dashboard_id": 10, + "created_at": "2026-08-17T12:33:25.109933Z", + "row": 16 + } + ], + "embedding_type": null, + "initially_published_at": null, + "can_write": true, + "can_set_cache_policy": true, + "tabs": [], + "enable_embedding": false, + "collection_id": 7, + "show_in_getting_started": false, + "name": "Abono de Permanência", + "is_remote_synced": false, + "width": "full", + "caveats": null, + "collection_authority_level": null, + "creator_id": 1, + "can_restore": false, + "moderation_reviews": [], + "updated_at": "2026-08-17T12:33:24.669349Z", + "made_public_by_id": null, + "embedding_params": null, + "cache_ttl": null, + "last_used_param_values": { + "a1b2c3d4": "2025-07-01~2025-12-31", + "b2c3d4e5": [ + "SEFAZ" + ] + }, + "id": 10, + "last_viewed_at": "2026-08-21T12:37:47.087805Z", + "position": null, + "archived_directly": false, + "entity_id": "TYFfGoyCbmJkLJqeZa90y", + "param_fields": { + "a1b2c3d4": [ + { + "semantic_type": "type/CreationDate", + "table_id": 172, + "name": "competencia", + "has_field_values": "none", + "fk_target_field_id": null, + "dimensions": [], + "id": 1617, + "target": null, + "display_name": "Competencia", + "name_field": null, + "base_type": "type/Date" + }, + { + "semantic_type": "type/CreationDate", + "table_id": 171, + "name": "competencia", + "has_field_values": "none", + "fk_target_field_id": null, + "dimensions": [], + "id": 1610, + "target": null, + "display_name": "Competencia", + "name_field": null, + "base_type": "type/Date" + } + ], + "b2c3d4e5": [ + { + "semantic_type": "type/Category", + "table_id": 172, + "name": "orgao", + "has_field_values": "list", + "fk_target_field_id": null, + "dimensions": [], + "id": 1618, + "target": null, + "display_name": "Orgao", + "name_field": null, + "base_type": "type/Text" + } + ], + "c0ffee01": [ + { + "semantic_type": "type/CreationDate", + "table_id": 172, + "name": "competencia", + "has_field_values": "none", + "fk_target_field_id": null, + "dimensions": [], + "id": 1617, + "target": null, + "display_name": "Competencia", + "name_field": null, + "base_type": "type/Date" + } + ], + "c0ffee02": [ + { + "semantic_type": "type/Category", + "table_id": 172, + "name": "orgao", + "has_field_values": "list", + "fk_target_field_id": null, + "dimensions": [], + "id": 1618, + "target": null, + "display_name": "Orgao", + "name_field": null, + "base_type": "type/Text" + } + ] + }, + "last-edit-info": { + "id": 1, + "email": "oxymus@gmail.com", + "first_name": "Felipe", + "last_name": "Zerede", + "timestamp": "2026-08-17T12:36:40.423306Z" + }, + "collection": { + "authority_level": null, + "description": "Dashboard nativo de demonstração — dados mock, sem integração com a base do cliente.", + "archived": false, + "workspace_id": null, + "slug": "abono_de_permanencia__demo_", + "archive_operation_id": null, + "name": "Abono de Permanência (demo)", + "is_remote_synced": false, + "personal_owner_id": null, + "type": null, + "is_sample": false, + "effective_location": "/", + "id": 7, + "archived_directly": null, + "entity_id": "P7Af9XvTLKPPl3Taf5f2g", + "location": "/", + "namespace": null, + "is_personal": false, + "created_at": "2026-08-17T12:33:21.419024Z" + }, + "parameters": [ + { + "id": "a1b2c3d4", + "name": "Período", + "slug": "periodo", + "type": "date/all-options", + "sectionId": "date", + "default": "2025-07-01~2025-12-31" + }, + { + "id": "b2c3d4e5", + "name": "Órgão", + "slug": "orgao", + "type": "string/=", + "sectionId": "string", + "default": "Seduc", + "isMultiSelect": false + } + ], + "auto_apply_filters": true, + "created_at": "2026-08-17T12:33:24.544229Z", + "public_uuid": null, + "points_of_interest": null, + "can_delete": false +} \ No newline at end of file diff --git a/docs/internal/abono-demo/seed_abono_demo.py b/docs/internal/abono-demo/seed_abono_demo.py new file mode 100644 index 0000000000..3421acc403 --- /dev/null +++ b/docs/internal/abono-demo/seed_abono_demo.py @@ -0,0 +1,824 @@ +#!/usr/bin/env python3 +"""Seed a native Metabase dashboard for the Abono de Permanência mock. + +Requires a running Metabase (admin user) and the SQLite mock built next to this file. + + python3 docs/internal/abono-demo/seed_abono_demo.py + +Environment: + MB_URL default http://localhost:3000 + MB_EMAIL admin email + MB_PASSWORD admin password +""" + +from __future__ import annotations + +import argparse +import json +import os +import sys +import time +import urllib.error +import urllib.request +from pathlib import Path +from typing import Any + +HERE = Path(__file__).resolve().parent +if str(HERE) not in sys.path: + sys.path.insert(0, str(HERE)) + +from build_sqlite import build # noqa: E402 + +COLLECTION_NAME = "Abono de Permanência (demo)" +DATABASE_NAME = "Abono de Permanência (mock)" +DASHBOARD_NAME = "Abono de Permanência" + +PARAM_PERIODO = "a1b2c3d4" +PARAM_ORGAO = "b2c3d4e5" + +DEFAULT_PERIODO = "2025-07-01~2025-12-31" +DEFAULT_ORGAO = "Seduc" + +BRL = { + "number_style": "currency", + "currency": "BRL", + "currency_style": "symbol", + "decimals": 2, +} + + +class MetabaseClient: + def __init__(self, base_url: str, email: str, password: str): + self.base_url = base_url.rstrip("/") + self.session_id = self._login(email, password) + + def _login(self, email: str, password: str) -> str: + data = self.request( + "POST", + "/api/session", + {"username": email, "password": password}, + authenticated=False, + ) + session_id = data.get("id") + if not session_id: + raise RuntimeError(f"Login não retornou sessão: {data}") + return session_id + + def request( + self, + method: str, + path: str, + body: Any | None = None, + authenticated: bool = True, + ) -> Any: + url = self.base_url + path + headers = {"Content-Type": "application/json"} + if authenticated: + headers["X-Metabase-Session"] = self.session_id + payload = None if body is None else json.dumps(body).encode("utf-8") + req = urllib.request.Request(url, data=payload, headers=headers, method=method) + try: + with urllib.request.urlopen(req, timeout=120) as resp: + raw = resp.read() + if not raw: + return None + return json.loads(raw.decode("utf-8")) + except urllib.error.HTTPError as exc: + detail = exc.read().decode("utf-8", errors="replace") + raise RuntimeError(f"{method} {path} -> HTTP {exc.code}: {detail}") from exc + except urllib.error.URLError as exc: + raise RuntimeError( + f"Não conectou em {self.base_url} ({exc.reason}). " + "Suba o Metabase e tente de novo." + ) from exc + + +def col_settings(*pairs: tuple[str, dict]) -> dict: + return {json.dumps(["name", name]): settings for name, settings in pairs} + + +def field_ref(field_id: int, extra: dict | None = None) -> list: + return ["field", field_id, extra] + + +def mbql(database_id: int, query: dict) -> dict: + return {"database": database_id, "type": "query", "query": query} + + +def named_sum(field_id: int, name: str) -> list: + return [ + "aggregation-options", + ["sum", field_ref(field_id)], + {"name": name, "display-name": name}, + ] + + +def named_avg(field_id: int, name: str) -> list: + return [ + "aggregation-options", + ["avg", field_ref(field_id)], + {"name": name, "display-name": name}, + ] + + +def map_dimension(parameter_id: str, card_id: int, field_id: int, extra: dict | None = None) -> dict: + return { + "parameter_id": parameter_id, + "card_id": card_id, + "target": ["dimension", field_ref(field_id, extra)], + } + + +def map_template_tag(parameter_id: str, card_id: int, tag: str) -> dict: + return { + "parameter_id": parameter_id, + "card_id": card_id, + "target": ["dimension", ["template-tag", tag]], + } + + +def date_tag(field_id: int) -> dict: + return { + "id": "c0ffee01", + "name": "periodo", + "display-name": "Período", + "type": "dimension", + "dimension": field_ref(field_id), + "widget-type": "date/all-options", + } + + +def orgao_tag(field_id: int) -> dict: + return { + "id": "c0ffee02", + "name": "orgao", + "display-name": "Órgão", + "type": "dimension", + "dimension": field_ref(field_id), + "widget-type": "string/=", + } + + +def virtual_heading(text: str) -> dict: + card = { + "name": None, + "display": "heading", + "visualization_settings": {}, + "archived": False, + } + return { + "text": text, + "virtual_card": card, + "dashcard.background": False, + } + + +def find_named(items: list[dict], name: str) -> dict | None: + wanted = name.lower() + for item in items: + if (item.get("name") or "").lower() == wanted: + return item + return None + + +def ensure_database(mb: MetabaseClient, sqlite_file: Path) -> dict: + dbs = mb.request("GET", "/api/database") + data = dbs.get("data", dbs) if isinstance(dbs, dict) else dbs + existing = find_named(data, DATABASE_NAME) + details = {"db": str(sqlite_file.resolve())} + if existing: + mb.request( + "PUT", + f"/api/database/{existing['id']}", + {"name": DATABASE_NAME, "engine": "sqlite", "details": details, "is_full_sync": True}, + ) + db_id = existing["id"] + else: + created = mb.request( + "POST", + "/api/database", + { + "name": DATABASE_NAME, + "engine": "sqlite", + "details": details, + "is_full_sync": True, + "is_on_demand": False, + }, + ) + db_id = created["id"] + mb.request("POST", f"/api/database/{db_id}/sync_schema") + return wait_for_tables(mb, db_id) + + +def wait_for_tables(mb: MetabaseClient, db_id: int, timeout: int = 90) -> dict: + deadline = time.time() + timeout + while time.time() < deadline: + meta = mb.request("GET", f"/api/database/{db_id}/metadata") + tables = {t["name"]: t for t in meta.get("tables", []) if t.get("active", True)} + if "abono_folha" in tables and "abono_divergencia" in tables: + if all(t.get("fields") for t in (tables["abono_folha"], tables["abono_divergencia"])): + return tables + time.sleep(2) + raise RuntimeError("Timeout esperando o sync das tabelas mock. Veja Admin > Databases.") + + +def fields_by_name(table: dict) -> dict[str, dict]: + return {f["name"]: f for f in table.get("fields", [])} + + +def classify_fields(mb: MetabaseClient, tables: dict) -> None: + folha = fields_by_name(tables["abono_folha"]) + div = fields_by_name(tables["abono_divergencia"]) + updates = [ + (folha["competencia"]["id"], {"semantic_type": "type/CreationDate"}), + (folha["valor_total"]["id"], {"semantic_type": "type/Currency"}), + (folha["valor_analisado"]["id"], {"semantic_type": "type/Currency"}), + (folha["valor_divergencias"]["id"], {"semantic_type": "type/Currency"}), + (div["competencia"]["id"], {"semantic_type": "type/CreationDate"}), + (div["orgao"]["id"], {"semantic_type": "type/Category", "has_field_values": "list"}), + (div["motivo"]["id"], {"semantic_type": "type/Category", "has_field_values": "list"}), + (div["cargo"]["id"], {"semantic_type": "type/Category", "has_field_values": "list"}), + (div["valor"]["id"], {"semantic_type": "type/Currency"}), + ] + for field_id, body in updates: + mb.request("PUT", f"/api/field/{field_id}", body) + mb.request("POST", f"/api/field/{div['orgao']['id']}/rescan_values") + mb.request("POST", f"/api/field/{div['motivo']['id']}/rescan_values") + + +def ensure_collection(mb: MetabaseClient) -> int: + root = mb.request("GET", "/api/collection/root/items?models=collection") + items = root.get("data", []) + existing = find_named(items, COLLECTION_NAME) + if existing: + return existing["id"] + created = mb.request( + "POST", + "/api/collection", + { + "name": COLLECTION_NAME, + "description": "Dashboard nativo de demonstração — dados mock, sem integração com a base do cliente.", + }, + ) + return created["id"] + + +def archive_old_dashboard(mb: MetabaseClient, collection_id: int) -> None: + items = mb.request( + "GET", + f"/api/collection/{collection_id}/items?models=dashboard", + ).get("data", []) + for item in items: + if item.get("name") == DASHBOARD_NAME: + mb.request("PUT", f"/api/dashboard/{item['id']}", {"archived": True}) + + +def create_card(mb: MetabaseClient, collection_id: int, **card: Any) -> dict: + payload = { + "collection_id": collection_id, + "type": "question", + "visualization_settings": {}, + **card, + } + return mb.request("POST", "/api/card", payload) + + +def native_query(database_id: int, sql: str, tags: dict) -> dict: + return { + "database": database_id, + "type": "native", + "native": {"query": sql, "template-tags": tags}, + } + + +def seed(mb: MetabaseClient, sqlite_file: Path) -> str: + tables = ensure_database(mb, sqlite_file) + classify_fields(mb, tables) + collection_id = ensure_collection(mb) + archive_old_dashboard(mb, collection_id) + + folha = tables["abono_folha"] + diverg = tables["abono_divergencia"] + ff = fields_by_name(folha) + df = fields_by_name(diverg) + db_id = folha["db_id"] + + folha_id = folha["id"] + diverg_id = diverg["id"] + f_comp = ff["competencia"]["id"] + f_valor = ff["valor_total"]["id"] + f_bens = ff["qtd_beneficiarios"]["id"] + f_ok_v = ff["valor_analisado"]["id"] + f_ok_p = ff["qtd_analisado"]["id"] + f_div_v = ff["valor_divergencias"]["id"] + f_div_p = ff["qtd_divergencias"]["id"] + d_comp = df["competencia"]["id"] + d_orgao = df["orgao"]["id"] + d_motivo = df["motivo"]["id"] + d_cargo = df["cargo"]["id"] + d_valor = df["valor"]["id"] + d_pessoas = df["qtd_pessoas"]["id"] + d_score = df["score_normativa"]["id"] + + tags_periodo = {"periodo": date_tag(d_comp)} + tags_periodo_folha = {"periodo": date_tag(f_comp)} + tags_both = {"periodo": date_tag(d_comp), "orgao": orgao_tag(d_orgao)} + + red_segment = [{"min": 0, "max": None, "color": "#E75C58", "label": "Divergência"}] + + cards: dict[str, dict] = {} + + def scalar(name: str, field_id: int, table_id: int, extra_settings: dict | None = None) -> dict: + settings = { + "column_settings": col_settings(("sum", BRL)), + **(extra_settings or {}), + } + return create_card( + mb, + collection_id, + name=name, + display="scalar", + dataset_query=mbql( + db_id, + {"source-table": table_id, "aggregation": [["sum", field_ref(field_id)]]}, + ), + visualization_settings=settings, + ) + + cards["valor_total"] = scalar("Valor total", f_valor, folha_id) + cards["valor_medio"] = create_card( + mb, + collection_id, + name="Valor mensal médio", + display="scalar", + dataset_query=mbql( + db_id, + {"source-table": folha_id, "aggregation": [named_avg(f_valor, "media")]}, + ), + visualization_settings={"column_settings": col_settings(("media", BRL))}, + ) + cards["valor_benef"] = create_card( + mb, + collection_id, + name="Valor por beneficiário/mês", + display="scalar", + dataset_query=mbql( + db_id, + { + "source-table": folha_id, + "aggregation": [ + [ + "aggregation-options", + ["/", ["sum", field_ref(f_valor)], ["sum", field_ref(f_bens)]], + {"name": "per_capita", "display-name": "Valor por beneficiário/mês"}, + ] + ], + }, + ), + visualization_settings={"column_settings": col_settings(("per_capita", BRL))}, + ) + + cards["evolucao"] = create_card( + mb, + collection_id, + name="Evolução", + display="combo", + dataset_query=mbql( + db_id, + { + "source-table": folha_id, + "aggregation": [ + named_sum(f_valor, "Valor total"), + named_sum(f_bens, "Beneficiários"), + ], + "breakout": [field_ref(f_comp, {"temporal-unit": "month"})], + }, + ), + visualization_settings={ + "graph.dimensions": ["competencia"], + "graph.metrics": ["Valor total", "Beneficiários"], + "graph.split_panels": True, + "graph.show_values": True, + "graph.label_value_frequency": "all", + "graph.label_value_formatting": "compact", + "graph.x_axis.labels_enabled": False, + "series_settings": { + "Valor total": {"display": "line", "color": "#509EE3", "line.marker_enabled": True}, + "Beneficiários": {"display": "bar", "color": "#7BBCE7"}, + }, + "column_settings": col_settings(("Valor total", {**BRL, "decimals": 1})), + }, + ) + + cards["analisado_valor"] = scalar("Analisado — valor", f_ok_v, folha_id) + cards["diverg_valor"] = scalar( + "Divergências — valor", + f_div_v, + folha_id, + extra_settings={"scalar.segments": red_segment}, + ) + cards["analisado_pessoas"] = create_card( + mb, + collection_id, + name="Analisado — pessoas", + display="scalar", + dataset_query=mbql( + db_id, + {"source-table": folha_id, "aggregation": [named_sum(f_ok_p, "pessoas")]}, + ), + visualization_settings={}, + ) + cards["diverg_pessoas"] = create_card( + mb, + collection_id, + name="Divergências — pessoas", + display="scalar", + dataset_query=mbql( + db_id, + {"source-table": folha_id, "aggregation": [named_sum(f_div_p, "pessoas")]}, + ), + visualization_settings={"scalar.segments": red_segment}, + ) + cards["analisado_pct"] = create_card( + mb, + collection_id, + name="Analisado — % do valor", + display="scalar", + dataset_query=mbql( + db_id, + { + "source-table": folha_id, + "aggregation": [ + [ + "aggregation-options", + ["/", ["sum", field_ref(f_ok_v)], ["sum", field_ref(f_valor)]], + {"name": "pct", "display-name": "% analisado"}, + ] + ], + }, + ), + visualization_settings={ + "column_settings": col_settings(("pct", {"number_style": "percent", "decimals": 1})), + }, + ) + cards["diverg_pct"] = create_card( + mb, + collection_id, + name="Divergências — % do valor", + display="scalar", + dataset_query=mbql( + db_id, + { + "source-table": folha_id, + "aggregation": [ + [ + "aggregation-options", + ["/", ["sum", field_ref(f_div_v)], ["sum", field_ref(f_valor)]], + {"name": "pct", "display-name": "% divergências"}, + ] + ], + }, + ), + visualization_settings={ + "scalar.segments": red_segment, + "column_settings": col_settings(("pct", {"number_style": "percent", "decimals": 1})), + }, + ) + + def top3_bar(name: str, breakout_field: int, extra: dict | None = None) -> dict: + return create_card( + mb, + collection_id, + name=name, + display="bar", + dataset_query=mbql( + db_id, + { + "source-table": diverg_id, + "aggregation": [named_sum(d_valor, "valor")], + "breakout": [field_ref(breakout_field, extra)], + "order-by": [["desc", ["aggregation", 0]]], + "limit": 3, + }, + ), + visualization_settings={ + "graph.dimensions": [ + "motivo" if breakout_field == d_motivo else "orgao" if breakout_field == d_orgao else "competencia" + ], + "graph.metrics": ["valor"], + "graph.show_values": True, + "graph.label_value_formatting": "compact", + "graph.x_axis.labels_enabled": False, + "column_settings": col_settings(("valor", BRL)), + }, + ) + + cards["top_motivo"] = top3_bar("Top 3 por motivo", d_motivo) + cards["top_orgao"] = top3_bar("Top 3 por órgão", d_orgao) + cards["top_mes"] = top3_bar("Top 3 por mês", d_comp, {"temporal-unit": "month"}) + + scatter_sql = """ +SELECT + orgao, + SUM(valor) AS divergencia_financeira_bruta, + AVG(score_normativa) AS divergencia_normativa, + SUM(qtd_pessoas) AS pessoas +FROM abono_divergencia +WHERE {{periodo}} +GROUP BY orgao +""".strip() + cards["scatter"] = create_card( + mb, + collection_id, + name="Análise / detalhamento divergências", + display="scatter", + dataset_query=native_query(db_id, scatter_sql, tags_periodo), + visualization_settings={ + "graph.dimensions": ["divergencia_financeira_bruta"], + "graph.metrics": ["divergencia_normativa"], + "scatter.bubble": "pessoas", + "graph.x_axis.title_text": "Divergência financeira bruta", + "graph.y_axis.title_text": "Divergência normativa", + "click_behavior": { + "type": "crossfilter", + "parameterMapping": { + PARAM_ORGAO: { + "id": PARAM_ORGAO, + "source": {"type": "column", "id": "orgao", "name": "orgao"}, + "target": {"type": "parameter", "id": PARAM_ORGAO}, + } + }, + }, + "column_settings": col_settings( + ("divergencia_financeira_bruta", BRL), + ("divergencia_normativa", {"decimals": 1}), + ), + }, + ) + + detalhe_sql = """ +SELECT + motivo, + SUM(valor) AS valor, + ROUND(100.0 * SUM(valor) / SUM(SUM(valor)) OVER (), 1) AS percentual +FROM abono_divergencia +WHERE {{periodo}} AND {{orgao}} +GROUP BY motivo +ORDER BY valor DESC +""".strip() + cards["detalhe_motivo"] = create_card( + mb, + collection_id, + name="Divergência financeira por motivo", + display="table", + dataset_query=native_query(db_id, detalhe_sql, tags_both), + visualization_settings={ + "table.pivot": False, + "column_settings": col_settings( + ("valor", BRL), + ("percentual", {"suffix": " %", "decimals": 1}), + ), + }, + ) + + cargos_sql = """ +SELECT + cargo, + SUM(qtd_pessoas) AS pessoas, + ROUND(100.0 * SUM(qtd_pessoas) / SUM(SUM(qtd_pessoas)) OVER (), 1) AS percentual +FROM abono_divergencia +WHERE {{periodo}} AND {{orgao}} +GROUP BY cargo +ORDER BY pessoas DESC +""".strip() + cards["detalhe_cargos"] = create_card( + mb, + collection_id, + name="Pessoal afetado", + display="table", + dataset_query=native_query(db_id, cargos_sql, tags_both), + visualization_settings={ + "column_settings": col_settings(("percentual", {"suffix": " %", "decimals": 1})), + }, + ) + + media_sql = """ +SELECT ROUND(SUM(valor) * 1.0 / COUNT(DISTINCT competencia), 2) AS media_mensal +FROM abono_divergencia +WHERE {{periodo}} AND {{orgao}} +""".strip() + cards["media_div"] = create_card( + mb, + collection_id, + name="Divergência média mensal", + display="scalar", + dataset_query=native_query(db_id, media_sql, tags_both), + visualization_settings={"column_settings": col_settings(("media_mensal", BRL))}, + ) + + dashboard = mb.request( + "POST", + "/api/dashboard", + { + "name": DASHBOARD_NAME, + "description": ( + "Protótipo nativo (nível A). Dados mock de jul–dez/2025. " + "Clique numa bolha para filtrar o detalhe do órgão." + ), + "collection_id": collection_id, + "parameters": [ + { + "id": PARAM_PERIODO, + "name": "Período", + "slug": "periodo", + "type": "date/all-options", + "sectionId": "date", + "default": DEFAULT_PERIODO, + }, + { + "id": PARAM_ORGAO, + "name": "Órgão", + "slug": "orgao", + "type": "string/=", + "sectionId": "string", + "default": DEFAULT_ORGAO, + "isMultiSelect": False, + }, + ], + }, + ) + dash_id = dashboard["id"] + mb.request("PUT", f"/api/dashboard/{dash_id}", {"width": "full"}) + + def cid(key: str) -> int: + return cards[key]["id"] + + def folha_maps(card_key: str) -> list[dict]: + return [map_dimension(PARAM_PERIODO, cid(card_key), f_comp)] + + def diverg_maps(card_key: str) -> list[dict]: + return [map_dimension(PARAM_PERIODO, cid(card_key), d_comp)] + + def native_periodo(card_key: str) -> list[dict]: + return [map_template_tag(PARAM_PERIODO, cid(card_key), "periodo")] + + def native_both(card_key: str) -> list[dict]: + return [ + map_template_tag(PARAM_PERIODO, cid(card_key), "periodo"), + map_template_tag(PARAM_ORGAO, cid(card_key), "orgao"), + ] + + heading_main = { + "id": -1, + "card_id": None, + "row": 0, + "col": 0, + "size_x": 24, + "size_y": 2, + "inline_parameters": [PARAM_PERIODO], + "parameter_mappings": [], + "visualization_settings": virtual_heading("Abono de Permanência"), + "series": [], + } + heading_analise = { + "id": -2, + "card_id": None, + "row": 1, + "col": 12, + "size_x": 6, + "size_y": 1, + "inline_parameters": [], + "parameter_mappings": [], + "visualization_settings": virtual_heading("Análise / detalhamento divergências"), + "series": [], + } + heading_orgao = { + "id": -3, + "card_id": None, + "row": 1, + "col": 18, + "size_x": 6, + "size_y": 1, + "inline_parameters": [PARAM_ORGAO], + "parameter_mappings": [ + { + "parameter_id": PARAM_ORGAO, + "target": ["text-tag", "orgao"], + } + ], + "visualization_settings": virtual_heading("{{orgao}}"), + "series": [], + } + heading_top3 = { + "id": -4, + "card_id": None, + "row": 8, + "col": 6, + "size_x": 6, + "size_y": 1, + "inline_parameters": [], + "parameter_mappings": [], + "visualization_settings": virtual_heading("Top 3 divergências"), + "series": [], + } + + question_cards = [ + # Coluna 1 + {"id": -10, "card_id": cid("valor_total"), "row": 2, "col": 0, "size_x": 6, "size_y": 2, "parameter_mappings": folha_maps("valor_total")}, + {"id": -11, "card_id": cid("valor_medio"), "row": 4, "col": 0, "size_x": 6, "size_y": 2, "parameter_mappings": folha_maps("valor_medio")}, + {"id": -12, "card_id": cid("valor_benef"), "row": 6, "col": 0, "size_x": 6, "size_y": 2, "parameter_mappings": folha_maps("valor_benef")}, + {"id": -13, "card_id": cid("evolucao"), "row": 8, "col": 0, "size_x": 6, "size_y": 8, "parameter_mappings": folha_maps("evolucao")}, + # Coluna 2 + {"id": -20, "card_id": cid("analisado_valor"), "row": 2, "col": 6, "size_x": 3, "size_y": 3, "parameter_mappings": folha_maps("analisado_valor")}, + {"id": -21, "card_id": cid("diverg_valor"), "row": 2, "col": 9, "size_x": 3, "size_y": 3, "parameter_mappings": folha_maps("diverg_valor")}, + {"id": -22, "card_id": cid("analisado_pessoas"), "row": 5, "col": 6, "size_x": 3, "size_y": 2, "parameter_mappings": folha_maps("analisado_pessoas")}, + {"id": -23, "card_id": cid("diverg_pessoas"), "row": 5, "col": 9, "size_x": 3, "size_y": 2, "parameter_mappings": folha_maps("diverg_pessoas")}, + {"id": -24, "card_id": cid("analisado_pct"), "row": 7, "col": 6, "size_x": 3, "size_y": 1, "parameter_mappings": folha_maps("analisado_pct")}, + {"id": -25, "card_id": cid("diverg_pct"), "row": 7, "col": 9, "size_x": 3, "size_y": 1, "parameter_mappings": folha_maps("diverg_pct")}, + {"id": -26, "card_id": cid("top_motivo"), "row": 9, "col": 6, "size_x": 2, "size_y": 7, "parameter_mappings": diverg_maps("top_motivo")}, + {"id": -27, "card_id": cid("top_orgao"), "row": 9, "col": 8, "size_x": 2, "size_y": 7, "parameter_mappings": diverg_maps("top_orgao")}, + {"id": -28, "card_id": cid("top_mes"), "row": 9, "col": 10, "size_x": 2, "size_y": 7, "parameter_mappings": diverg_maps("top_mes")}, + # Coluna 3 + {"id": -30, "card_id": cid("scatter"), "row": 2, "col": 12, "size_x": 6, "size_y": 14, "parameter_mappings": native_periodo("scatter"), "visualization_settings": cards["scatter"]["visualization_settings"]}, + # Coluna 4 + {"id": -40, "card_id": cid("detalhe_motivo"), "row": 2, "col": 18, "size_x": 6, "size_y": 7, "parameter_mappings": native_both("detalhe_motivo")}, + {"id": -41, "card_id": cid("detalhe_cargos"), "row": 9, "col": 18, "size_x": 6, "size_y": 5, "parameter_mappings": native_both("detalhe_cargos")}, + {"id": -42, "card_id": cid("media_div"), "row": 14, "col": 18, "size_x": 6, "size_y": 2, "parameter_mappings": native_both("media_div")}, + ] + for dc in question_cards: + dc.setdefault("series", []) + dc.setdefault("inline_parameters", []) + dc.setdefault("visualization_settings", {}) + + mb.request( + "PUT", + f"/api/dashboard/{dash_id}", + { + "name": DASHBOARD_NAME, + "width": "full", + "parameters": dashboard["parameters"] + if dashboard.get("parameters") + else [ + { + "id": PARAM_PERIODO, + "name": "Período", + "slug": "periodo", + "type": "date/all-options", + "sectionId": "date", + "default": DEFAULT_PERIODO, + }, + { + "id": PARAM_ORGAO, + "name": "Órgão", + "slug": "orgao", + "type": "string/=", + "sectionId": "string", + "default": DEFAULT_ORGAO, + "isMultiSelect": False, + }, + ], + "dashcards": [heading_main, heading_analise, heading_orgao, heading_top3, *question_cards], + }, + ) + + return f"{mb.base_url}/dashboard/{dash_id}" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--sqlite-only", + action="store_true", + help="Só gera o arquivo SQLite, sem chamar a API do Metabase.", + ) + parser.add_argument("--url", default=os.environ.get("MB_URL", "http://localhost:3000")) + parser.add_argument("--email", default=os.environ.get("MB_EMAIL", "")) + parser.add_argument("--password", default=os.environ.get("MB_PASSWORD", "")) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + sqlite_file = build() + print(f"SQLite mock: {sqlite_file}") + if args.sqlite_only: + return 0 + if not args.email or not args.password: + print( + "Informe MB_EMAIL e MB_PASSWORD (ou --email / --password).\n" + "O SQLite já foi gerado; rode o script de novo depois do login admin.", + file=sys.stderr, + ) + return 1 + user = MetabaseClient(args.url, args.email, args.password) + me = user.request("GET", "/api/user/current") + if not me.get("is_superuser"): + print("A conta precisa ser admin para adicionar o banco SQLite.", file=sys.stderr) + return 1 + url = seed(user, sqlite_file) + print(f"Dashboard criado: {url}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/internal/viabilidade-dashboard-abono-permanencia.md b/docs/internal/viabilidade-dashboard-abono-permanencia.md new file mode 100644 index 0000000000..c273d8951c --- /dev/null +++ b/docs/internal/viabilidade-dashboard-abono-permanencia.md @@ -0,0 +1,243 @@ +# Viabilidade: dashboard “Abono de Permanência” no Metabase + +Análise do que o Metabase já cobre nativamente e do que precisaria ser implementado para aproximar os modelos solicitados (card composto de evolução e dashboard de quatro colunas). + +**Público:** time de produto/engenharia que vai adaptar o mockup ao Metabase. +**Escopo:** um dashboard por objeto validado (exemplo: Abono de Permanência), com título, filtro de período e quatro colunas na mesma linha. + +--- + +## Resposta direta + +A **estrutura** do dashboard (título, filtro de período, quatro colunas, clique para detalhar) dá para montar com recursos nativos. + +O mockup trata cada coluna como um **widget composto**: vários tipos de gráfico, ícones e layout interno no mesmo card. No Metabase, um card é **uma question** e **um tipo de visualização**. A adaptação realista é: + +- layout nativo + **várias questions por coluna**; +- implementação nova só onde o visual do mockup for obrigatório. + +Não existe plugin público para registrar um chart custom sem alterar o código. Dá para criar visualizações novas no frontend deste repositório, pelo mesmo caminho do Combo e do Scatter (`registerVisualization`). + +--- + +## O que o mockup pede + +### Cabeçalho + +- Título do objeto à esquerda (exemplo: **Abono de Permanência**). +- Seletor de período na mesma barra (mês/ano até mês/ano). +- O check de “aplicar” do mockup **não entra**: o Metabase aplica o filtro no próprio select. + +### Quatro colunas na mesma linha + +1. **Evolução:** KPIs em texto (valor total, média mensal, valor por beneficiário) + gráfico linha (R$ milhões) empilhado sobre barras (pessoas, mil), eixo X mensal compartilhado. +2. **Comparação e Top 3:** dois boxes (Analisado vs Divergências, este com fundo vermelho) + três mini barras (por motivo, órgão, mês). +3. **Análise de divergências:** scatter/bolhas com quadrantes, labels nas bolhas, clique na bolha abre o detalhe. +4. **Detalhe do órgão (Seduc):** lista hierárquica expansível (divergência financeira, pessoal afetado, média mensal) com tabelas internas e drill. + +### Card composto isolado (primeiro print) + +Um único card com lista de KPIs no topo e, abaixo, o bloco **EVOLUÇÃO** (linha + barras sincronizadas). Esse modelo **não** existe como um único tipo de question. + +--- + +## Como o Metabase organiza cards + +| Conceito | Comportamento nativo | +| --- | --- | +| Question / card | Um tipo de visualização (`combo`, `scalar`, `scatter`, `bar`, `pivot`, etc.) | +| Dashboard | Grade de **24 colunas** (`GRID_WIDTH`). Quatro colunas iguais = largura **6** cada | +| Largura | **Fixed width** (padrão) ou **Full width** (melhor para este layout denso) | +| Tipos de card no dashboard | Questions, heading, text (Markdown), link, iframe | +| Filtros | Dashboard inteiro, **heading** da aba, ou card individual | +| Várias séries | Na mesma question (várias métricas) ou sobrepondo questions no mesmo card do dashboard, se compartilharem a dimensão (tempo) | + +Documentação relacionada: + +- [Introduction to dashboards](../dashboards/introduction.md) +- [Dashboard filters](../dashboards/filters.md) +- [Charts with multiple series](../dashboards/multiple-series.md) +- [Combo charts](../questions/visualizations/combo-chart.md) +- [Dashboard interactivity](../dashboards/interactive.md) + +--- + +## Nativo vs implementar + +Legenda: **Nativo** = dá para usar hoje; **Parcial** = cobre o dado, não o visual; **Implementar** = exige código novo. + +### Cabeçalho e layout + +| Pedido | Status | Como fazer / o que falta | +| --- | --- | --- | +| Quatro blocos na mesma linha | **Nativo** | Grade 24 colunas; **Full width** | +| Título do objeto | **Nativo** | Heading card | +| Filtro de período (mês a mês) | **Nativo** | Date picker no **heading** (não no dashboard inteiro, se o filtro for só desta aba) | +| Aplicar com botão check | **Não usar** | O Metabase aplica no select | +| Título + date range no mesmo controle visual do print | **Parcial** | Heading + widget à direita. Não fica “colado” como no mockup | +| Uma moldura única por coluna envolvendo vários gráficos | **Implementar** | Nativo empilha cards soltos, cada um com a própria borda | + +**Implementar?** Não vale um componente só para o chrome do cabeçalho. O Date picker nativo cobre o comportamento. + +### Coluna 1 — Evolução (KPIs + linha + barras) + +| Pedido | Status | Como fazer / o que falta | +| --- | --- | --- | +| Linha (R$) + barras (pessoas) no mesmo eixo X | **Nativo** | Visualização **Combo**. Por série: line, bar ou area | +| Linha em cima, barras embaixo | **Nativo** | **Stack series** (`graph.split_panels`) | +| Escalas diferentes (milhões vs milhares) | **Nativo** | Split y-axis se as séries estiverem sobrepostas; painéis separados já isolam a escala | +| Labels nos pontos e nas barras | **Nativo** | **Show values on data points** | +| Duas ou mais métricas + agrupamento temporal | **Nativo** | 2+ métricas e 1–2 agrupamentos, ou 1 métrica e 2 agrupamentos | +| Três KPIs (total, média, por beneficiário) | **Parcial** | Três cards **Number** (`scalar`) ou **Trend** (`smartscalar`). Sem layout “label à esquerda / valor à direita” numa lista | +| KPIs + gráfico no **mesmo** card | **Implementar** | Uma question = um tipo de viz | +| Ícones `$` / pessoa no eixo | **Implementar** | Só texto de eixo | +| Ícone olho nos pontos | **Implementar** | Sem anotações custom em pontos | + +**Combo no código:** `frontend/src/metabase/visualizations/visualizations/ComboChart/`. +**Stack series:** setting `graph.split_panels` em `frontend/src/metabase/visualizations/lib/settings/graph.ts`. + +Seção nativa próxima: **KPIs with large chart below** em [dashboard sections](../dashboards/introduction.md#dashboard-sections) — KPIs e gráfico são cards separados, não um único widget. + +### Coluna 2 — Analisado vs Divergências + Top 3 + +| Pedido | Status | Como fazer / o que falta | +| --- | --- | --- | +| Métricas Analisado vs Divergências | **Parcial** | Vários cards **Number** / **Trend** | +| Cor condicional do número | **Nativo** | Number: aba **Conditional colors** | +| Fundo vermelho do box inteiro | **Implementar** | Condicional pinta o número, não o card | +| R$ + pessoas + % no mesmo box | **Implementar** | Um Number = um valor | +| Ícones nos KPIs | **Implementar** | | +| Título “Top 3 divergências” | **Nativo** | Heading ou text card | +| Ícones de ação (home, calendário, user, tag) | **Parcial** | Link cards ou click behavior; não há barra de ícones no header do card | +| Três mini barras lado a lado | **Nativo** | Três questions **Bar** (top 3 por motivo / órgão / mês) | + +### Coluna 3 — Scatter / bolhas com quadrantes + +| Pedido | Status | Como fazer / o que falta | +| --- | --- | --- | +| Scatter / bubble (X, Y, tamanho) | **Nativo** | **Scatter** + bubble size | +| Cor por categoria (órgão) | **Nativo** | Breakout | +| Clique na bolha → detalhe na coluna 4 | **Nativo** | **Click behavior** → **Update a dashboard filter** (órgão) ligado aos cards da coluna 4 | +| Uma linha de meta no Y | **Parcial** | Goal line; não desenha a cruz dos quatro quadrantes | +| Quadrantes (linhas tracejadas em X e Y) | **Implementar** | Sem `markLine` em ambos os eixos | +| Labels nas bolhas (“Seduc”) | **Implementar** | Tooltip e legenda; sem rótulo no ponto | + +Esta é a coluna que **mais se aproxima** do mockup só com nativo, se o time aceitar tooltip no lugar do label e sem quadrantes desenhados. + +**Scatter no código:** `frontend/src/metabase/visualizations/visualizations/ScatterPlot/`. +Docs: [Scatterplots and bubble charts](../questions/visualizations/scatterplot-or-bubble-chart.md). + +### Coluna 4 — Detalhe hierárquico do órgão + +| Pedido | Status | Como fazer / o que falta | +| --- | --- | --- | +| Título dinâmico (Secretaria da Educação) | **Nativo** | Heading com variável `{{orgao}}` ligada ao filtro | +| Breakdown com subtotais | **Nativo** | **Pivot table** (query builder): grupos, subtotais, expand/collapse | +| Tabelas (categoria, R$, %) | **Nativo** | Table ou Pivot | +| Lista de cargos + contagens | **Nativo** | Table ou **Row** chart | +| Linha “divergência média mensal” | **Nativo** | Number | +| Drill para outro dashboard / question | **Nativo** | Click behavior ou drill-through | +| Accordion com % e valor no header da seção | **Implementar** | Pivot expande grupos; não replica o visual de seções do mockup | + +**List view** (`list`) serve para explorar **registros** de um model, não para este breakdown analítico. Ver [Model list view](../data-modeling/models.md#model-list-view). + +Pivot: [Pivot tables](../questions/visualizations/pivot-table.md). + +--- + +## Montagem nativa sugerida (espelho funcional) + +``` +[ Heading: Abono de Permanência ] [ Date picker no heading ] +[ 3× Number + 1× Combo (stack) ] [ 4–6× Number + 3× Bar ] +[ 1× Scatter / Bubble ] [ Heading {{orgao}} + Pivot / Tables ] +``` + +Interação: clique na bolha **Seduc** → atualiza filtro de órgão → coluna 4 recalcula. Isso já existe e é o equivalente nativo do “detalhamento ao clicar”. + +Sobrepor questions no mesmo card do dashboard só ajuda quando as séries compartilham o eixo de tempo (evolução). Não resolve KPIs + gráfico, nem boxes + mini barras, nem scatter + accordion. + +--- + +## O que está além do nativo (lista fechada) + +1. Vários tipos de gráfico **no mesmo card** (KPI + combo; dois boxes + três bars). +2. Chrome visual: moldura única da coluna, fundo vermelho do bloco, ícones de unidade/ação, olho nos pontos. +3. Quadrantes + labels em bolhas. +4. Accordion analítico com métricas no header da seção. +5. Filtro de período no pixel do mockup (dois dropdowns + calendário + check na mesma barra do título). + +Os `PLUGIN_*` do produto são para features EE (SSO, embedding SDK, etc.). **Não** há hook para registrar um chart de dashboard sem mudar o código. + +--- + +## Dá para implementar componentes novos? + +Sim. O registro de visualizações é de primeira classe neste repositório. Não é um plugin externo: é código no frontend. + +### Caminho padrão (igual Combo, Scatter, Pivot) + +1. Novo identificador em `cardDisplayTypes` — `frontend/src/metabase-types/api/visualization.ts`. +2. Componente React com `VisualizationDefinition`: `identifier`, `getUiName`, `isSensible`, `checkRenderable`, `settings`, tamanhos min/default. +3. `registerVisualization(...)` em `frontend/src/metabase/visualizations/register.js`. +4. Settings no painel de viz (mesmo padrão de `graph.split_panels`). +5. Se houver **subscriptions / export PNG / static viz**, espelhar em `frontend/src/metabase/static-viz/`. Sem isso, e-mail/Slack saem quebrados ou sem o chart. +6. Testes (Jest + E2E), i18n com `ttag`, e tamanhos em `metabase/visualizations/shared/utils/sizes`. + +O novo tipo entra no query builder, no dashboard, no visualizer e no embedding, como qualquer chart nativo. + +### Três níveis de customização + +| Nível | O que é | Esforço | Quando usar | +| --- | --- | --- | --- | +| **A. Só nativo** | Heading + filtro + grade de 4 colunas + várias questions | Baixo | Entregar o dado rápido; aceitar visual Metabase | +| **B. Novos viz no core** | 1–3 displays novos (scatter com quadrantes; KPI+combo; comparison panel; accordion) | Médio a alto | Reuso em vários dashboards / objetos | +| **C. Iframe / app ao lado** | Card iframe com app React próprio; filtros via `{{variáveis}}` no `src` | Médio, paralelo ao core | Pixel-perfect de um relatório, sem carregar o produto | + +Iframe: [Iframe cards](../dashboards/introduction.md#iframe-cards). O host precisa estar em `allowed-iframe-hosts`. + +### Candidatos a viz novos (se o time for para o nível B) + +| Componente | Coluna | Esforço | Valor | +| --- | --- | --- | --- | +| Scatter com `markLine` nos dois eixos + label no ponto | 3 | Baixo a médio | Alto: o modelo de dados já existe | +| `kpi-combo` (lista de métricas + Combo com split panels) | 1 | Médio a alto | Alto só se for template de vários objetos | +| `comparison-panel` (dois boxes + métricas + cor de fundo) | 2 | Médio | Médio | +| `mini-multi-bar` (três top-N no mesmo card) | 2 | Baixo a médio | Baixo: três Bar nativos já resolvem | +| `breakdown-accordion` | 4 | Médio a alto | Médio: Pivot já entrega o dado | + +--- + +## Recomendação + +1. **Começar no nível A:** heading + date filter, quatro colunas, Combo com **Stack series**, Numbers, Bars, Scatter, Pivot/Tables, click behavior da bolha para o filtro de órgão. +2. **Customizar só a coluna 3** (quadrantes + labels) se o gráfico de divergências for o centro da análise — melhor custo/benefício. +3. **Viz compostos nas colunas 1, 2 e 4** só se este layout for **template** para muitos objetos (Abono, Licença, etc.), não um dashboard único. +4. **Não implementar** o check do filtro nem ícones decorativos nos eixos, a menos que o pixel-perfect seja requisito contratual. Aí o caminho é nível B (viz novos) ou C (iframe). + +--- + +## Protótipo nativo (dados mock) + +Há um seed pronto para apresentar no Metabase local, só com recursos nativos: + +- Pasta: [`docs/internal/abono-demo/`](abono-demo/README.md) +- Script: `python3 docs/internal/abono-demo/seed_abono_demo.py --email ... --password ...` + +## Referências no código + +| Tema | Caminho | +| --- | --- | +| Largura da grade | `frontend/src/metabase/lib/dashboard_grid.js` (`GRID_WIDTH = 24`) | +| Tipos de display | `frontend/src/metabase-types/api/visualization.ts` | +| Registro de viz | `frontend/src/metabase/visualizations/register.js` | +| API de viz | `frontend/src/metabase/visualizations/index.ts` (`registerVisualization`) | +| Combo | `frontend/src/metabase/visualizations/visualizations/ComboChart/` | +| Scatter | `frontend/src/metabase/visualizations/visualizations/ScatterPlot/` | +| Number | `frontend/src/metabase/visualizations/visualizations/Scalar/` | +| Trend | `frontend/src/metabase/visualizations/visualizations/SmartScalar/` | +| Pivot | `frontend/src/metabase/visualizations/visualizations/PivotTable/` | +| Heading / iframe | `.../Heading/`, `.../IFrameViz/` | +| Stack series / split y-axis | `frontend/src/metabase/visualizations/lib/settings/graph.ts` | +| Seções de dashboard | `frontend/src/metabase/dashboard/sections.ts` | +| Static viz (PNG / subscriptions) | `frontend/src/metabase/static-viz/` | diff --git a/docs/internal/viabilidade-dashboard-abono-permanencia.pdf b/docs/internal/viabilidade-dashboard-abono-permanencia.pdf new file mode 100644 index 0000000000..adcbc18b3f Binary files /dev/null and b/docs/internal/viabilidade-dashboard-abono-permanencia.pdf differ