Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions docs/internal/abono-demo/README.md
Original file line number Diff line number Diff line change
@@ -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/<id>`), 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.
218 changes: 218 additions & 0 deletions docs/internal/abono-demo/build_sqlite.py
Original file line number Diff line number Diff line change
@@ -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}")
Loading