Delete app.py
Browse files
app.py
DELETED
|
@@ -1,428 +0,0 @@
|
|
| 1 |
-
import dash
|
| 2 |
-
from dash import dcc, html, Input, Output, State, dash_table
|
| 3 |
-
import dash_bootstrap_components as dbc
|
| 4 |
-
import plotly.express as px
|
| 5 |
-
import plotly.graph_objects as go
|
| 6 |
-
import pandas as pd
|
| 7 |
-
import requests
|
| 8 |
-
import time
|
| 9 |
-
import datetime
|
| 10 |
-
import numpy as np
|
| 11 |
-
import os
|
| 12 |
-
import urllib3
|
| 13 |
-
import re
|
| 14 |
-
from dash.dash_table.Format import Format, Symbol
|
| 15 |
-
|
| 16 |
-
# --- CONFIGURAÇÃO DE CACHE ---
|
| 17 |
-
PASTA_CACHE = "dados_cache"
|
| 18 |
-
if not os.path.exists(PASTA_CACHE):
|
| 19 |
-
os.makedirs(PASTA_CACHE)
|
| 20 |
-
|
| 21 |
-
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
| 22 |
-
|
| 23 |
-
# ==============================================================================
|
| 24 |
-
# 1. UTILITÁRIOS (CACHE, DATAS E FORMATAÇÃO)
|
| 25 |
-
# ==============================================================================
|
| 26 |
-
|
| 27 |
-
def cache_eh_valido_mensal(caminho_arquivo):
|
| 28 |
-
"""
|
| 29 |
-
Verifica se o arquivo existe, não está vazio e é do MÊS/ANO atuais.
|
| 30 |
-
"""
|
| 31 |
-
if not os.path.exists(caminho_arquivo): return False
|
| 32 |
-
try:
|
| 33 |
-
if os.path.getsize(caminho_arquivo) < 100: return False # Proteção contra arquivo corrompido
|
| 34 |
-
|
| 35 |
-
ts = os.path.getmtime(caminho_arquivo)
|
| 36 |
-
dt_arq = datetime.datetime.fromtimestamp(ts)
|
| 37 |
-
agora = datetime.datetime.now()
|
| 38 |
-
# Válido apenas se for do mesmo ano e mesmo mês
|
| 39 |
-
return (dt_arq.year == agora.year and dt_arq.month == agora.month)
|
| 40 |
-
except:
|
| 41 |
-
return False
|
| 42 |
-
|
| 43 |
-
def fazer_requisicao_robusta(url, params=None, verify=False, timeout=45, max_tentativas=5):
|
| 44 |
-
headers = {
|
| 45 |
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
| 46 |
-
'Accept': 'application/json, text/plain, */*'
|
| 47 |
-
}
|
| 48 |
-
for tentativa in range(1, max_tentativas + 1):
|
| 49 |
-
try:
|
| 50 |
-
response = requests.get(url, headers=headers, params=params, verify=False, timeout=timeout)
|
| 51 |
-
if response.status_code in [200, 201]: return response
|
| 52 |
-
elif response.status_code == 429: time.sleep(10); continue
|
| 53 |
-
except: time.sleep(2); continue
|
| 54 |
-
return None
|
| 55 |
-
|
| 56 |
-
def converter_para_float(valor):
|
| 57 |
-
"""Converte financeiro BR (1.000,00) para float (1000.00)"""
|
| 58 |
-
if pd.isna(valor) or valor == '': return 0.0
|
| 59 |
-
if isinstance(valor, (int, float)): return float(valor)
|
| 60 |
-
try:
|
| 61 |
-
valor_limpo = str(valor).replace('R$', '').replace(' ', '').replace('.', '').replace(',', '.')
|
| 62 |
-
return float(valor_limpo)
|
| 63 |
-
except: return 0.0
|
| 64 |
-
|
| 65 |
-
def corrigir_texto_licitacao(texto):
|
| 66 |
-
"""Corrige encoding quebrado (Mojibake)"""
|
| 67 |
-
if not isinstance(texto, str): return texto
|
| 68 |
-
replaces = {
|
| 69 |
-
'ê': 'ê', 'á': 'á', 'ã': 'ã', 'ç': 'ç', 'é': 'é',
|
| 70 |
-
'ó': 'ó', 'Ãwq': 'í', 'ú': 'ú', 'â': 'â', 'õ': 'õ',
|
| 71 |
-
'Eletrônica': 'Eletrônica', 'Concorrência': 'Concorrência',
|
| 72 |
-
'Pregão': 'Pregão', 'Dispensa de Licitação': 'Dispensa de Licitação'
|
| 73 |
-
}
|
| 74 |
-
try:
|
| 75 |
-
# Tenta decode direto
|
| 76 |
-
return texto.encode('latin1').decode('utf-8')
|
| 77 |
-
except:
|
| 78 |
-
# Fallback manual
|
| 79 |
-
for errado, certo in replaces.items():
|
| 80 |
-
texto = texto.replace(errado, certo)
|
| 81 |
-
return texto
|
| 82 |
-
|
| 83 |
-
# ==============================================================================
|
| 84 |
-
# 2. MOTORES DE CARGA (COM SISTEMA ANTI-TRAVAMENTO)
|
| 85 |
-
# ==============================================================================
|
| 86 |
-
|
| 87 |
-
# --- MOTOR DE RH ---
|
| 88 |
-
def carregar_rh_completo():
|
| 89 |
-
arquivo_cache = f"{PASTA_CACHE}/rh_funcionarios_ativos.csv"
|
| 90 |
-
|
| 91 |
-
# 1. Tenta Cache Válido (Mês Atual)
|
| 92 |
-
if cache_eh_valido_mensal(arquivo_cache):
|
| 93 |
-
try:
|
| 94 |
-
try: df = pd.read_csv(arquivo_cache, sep=None, engine='python')
|
| 95 |
-
except: df = pd.read_csv(arquivo_cache)
|
| 96 |
-
if not df.empty and 'salarioBase' in df.columns:
|
| 97 |
-
df['salarioBase'] = df['salarioBase'].apply(converter_para_float)
|
| 98 |
-
return df
|
| 99 |
-
except: pass
|
| 100 |
-
|
| 101 |
-
# 2. Tenta Atualizar via API (Se falhar, não quebra)
|
| 102 |
-
print(" 🌍 RH: Tentando atualização via API...")
|
| 103 |
-
base_url = "https://navegantes.atende.net/api/transparencia-pessoal-funcionarios"
|
| 104 |
-
todos = []
|
| 105 |
-
pag = 1
|
| 106 |
-
sucesso_api = False
|
| 107 |
-
|
| 108 |
-
# Limite seguro de páginas para não estourar tempo no Cloud
|
| 109 |
-
while pag < 150:
|
| 110 |
-
resp = fazer_requisicao_robusta(base_url, params={"tipoBusca": 1, "pagina": pag})
|
| 111 |
-
if not resp: break
|
| 112 |
-
try:
|
| 113 |
-
d = resp.json()
|
| 114 |
-
recs = d if isinstance(d, list) else d.get('registros', d.get('entidade', []))
|
| 115 |
-
if not recs: break
|
| 116 |
-
todos.extend(recs)
|
| 117 |
-
pag += 1
|
| 118 |
-
sucesso_api = True
|
| 119 |
-
except: break
|
| 120 |
-
|
| 121 |
-
# Se atualizou com sucesso, salva novo cache
|
| 122 |
-
if sucesso_api and todos:
|
| 123 |
-
df = pd.DataFrame(todos)
|
| 124 |
-
if 'salarioBase' in df.columns:
|
| 125 |
-
df['salarioBase'] = df['salarioBase'].apply(converter_para_float)
|
| 126 |
-
df.to_csv(arquivo_cache, index=False, sep=',')
|
| 127 |
-
return df
|
| 128 |
-
|
| 129 |
-
# 3. PLANO B (FALLBACK): API Falhou? Carrega o cache antigo se existir
|
| 130 |
-
print(" ⚠️ RH: API indisponível/bloqueada. Usando cache existente.")
|
| 131 |
-
if os.path.exists(arquivo_cache):
|
| 132 |
-
try:
|
| 133 |
-
try: df = pd.read_csv(arquivo_cache, sep=None, engine='python')
|
| 134 |
-
except: df = pd.read_csv(arquivo_cache)
|
| 135 |
-
if 'salarioBase' in df.columns:
|
| 136 |
-
df['salarioBase'] = df['salarioBase'].apply(converter_para_float)
|
| 137 |
-
return df
|
| 138 |
-
except: pass
|
| 139 |
-
|
| 140 |
-
return pd.DataFrame()
|
| 141 |
-
|
| 142 |
-
# --- MOTOR FINANCEIRO ---
|
| 143 |
-
def obter_dados_financeiros(endpoint, ano, tipo):
|
| 144 |
-
arquivo = f"{PASTA_CACHE}/{tipo}_{ano}.csv"
|
| 145 |
-
|
| 146 |
-
# 1. Cache Válido?
|
| 147 |
-
if cache_eh_valido_mensal(arquivo):
|
| 148 |
-
try: return pd.read_csv(arquivo).to_dict('records')
|
| 149 |
-
except: pass
|
| 150 |
-
|
| 151 |
-
# 2. Tenta API
|
| 152 |
-
print(f" 🌍 {tipo} {ano}: Tentando API...")
|
| 153 |
-
base_url = "https://navegantes.atende.net/api/WCPDadosAbertos/"
|
| 154 |
-
url = f"{base_url}{endpoint}?dataInicial=01/01/{ano}&dataFinal=31/12/{ano}"
|
| 155 |
-
if tipo == 'restos': url = f"{base_url}{endpoint}?dataFinal=31/12/{ano}"
|
| 156 |
-
|
| 157 |
-
resp = fazer_requisicao_robusta(url)
|
| 158 |
-
if resp:
|
| 159 |
-
try:
|
| 160 |
-
recs = resp.json().get('retorno', [])
|
| 161 |
-
if recs:
|
| 162 |
-
pd.DataFrame(recs).to_csv(arquivo, index=False)
|
| 163 |
-
return recs
|
| 164 |
-
except: pass
|
| 165 |
-
|
| 166 |
-
# 3. PLANO B (FALLBACK): API Falhou? Usa cache antigo
|
| 167 |
-
if os.path.exists(arquivo):
|
| 168 |
-
print(f" ⚠️ {tipo} {ano}: Usando cache local (API falhou).")
|
| 169 |
-
try: return pd.read_csv(arquivo).to_dict('records')
|
| 170 |
-
except: pass
|
| 171 |
-
|
| 172 |
-
return []
|
| 173 |
-
|
| 174 |
-
def carregar_historico_financeiro(tipo):
|
| 175 |
-
lista_dfs = []
|
| 176 |
-
ano_atual = datetime.datetime.now().year
|
| 177 |
-
endpoint = {'receitas': 'receitas', 'receitas_orcadas': 'receitasOrcadas', 'despesas': 'despesas', 'despesas_orcadas': 'despesasOrcadas', 'restos': 'despesaRestos'}.get(tipo)
|
| 178 |
-
ano_ini = 2015 if tipo != 'restos' else ano_atual - 4
|
| 179 |
-
|
| 180 |
-
for ano in range(ano_ini, ano_atual + 1):
|
| 181 |
-
d = obter_dados_financeiros(endpoint, ano, tipo)
|
| 182 |
-
if d:
|
| 183 |
-
df = pd.DataFrame(d)
|
| 184 |
-
df['ano'] = ano
|
| 185 |
-
lista_dfs.append(df)
|
| 186 |
-
|
| 187 |
-
if not lista_dfs: return pd.DataFrame()
|
| 188 |
-
|
| 189 |
-
df_final = pd.concat(lista_dfs, ignore_index=True)
|
| 190 |
-
cols = ['valorArrecadado', 'valorOrcado', 'valorEmpenhado', 'valorPago', 'valorProcessadoInscrito', 'valorNaoProcessadoInscrito']
|
| 191 |
-
for c in cols:
|
| 192 |
-
if c in df_final.columns: df_final[c] = df_final[c].apply(converter_para_float)
|
| 193 |
-
else: df_final[c] = 0.0
|
| 194 |
-
return df_final
|
| 195 |
-
|
| 196 |
-
# --- MOTOR LICITAÇÕES ---
|
| 197 |
-
def carregar_licitacoes(arquivo="arquivos_combinados.csv"):
|
| 198 |
-
if not os.path.exists(arquivo):
|
| 199 |
-
print(f" ⚠️ Aviso: '{arquivo}' não encontrado.")
|
| 200 |
-
return pd.DataFrame()
|
| 201 |
-
|
| 202 |
-
try:
|
| 203 |
-
try: df = pd.read_csv(arquivo, sep=';', encoding='latin1')
|
| 204 |
-
except: df = pd.read_csv(arquivo, sep=',', encoding='utf-8')
|
| 205 |
-
|
| 206 |
-
if df.empty: return pd.DataFrame()
|
| 207 |
-
|
| 208 |
-
# Normaliza colunas
|
| 209 |
-
df.columns = [re.sub(r'\s+', '_', re.sub(r'[^\w\s]', '', c.strip().upper())) for c in df.columns]
|
| 210 |
-
|
| 211 |
-
# Mapeia
|
| 212 |
-
mapa = {'ANO': ['ANO','EXERCICIO'], 'SITUACAO': ['SITUACAO','STATUS'], 'MODALIDADE': ['MODALIDADE']}
|
| 213 |
-
for std, aliases in mapa.items():
|
| 214 |
-
for alias in aliases:
|
| 215 |
-
match = next((c for c in df.columns if alias in c), None)
|
| 216 |
-
if match:
|
| 217 |
-
df.rename(columns={match: std}, inplace=True)
|
| 218 |
-
break
|
| 219 |
-
|
| 220 |
-
if 'ANO' in df.columns:
|
| 221 |
-
df['ANO'] = pd.to_numeric(df['ANO'], errors='coerce').fillna(0).astype(int)
|
| 222 |
-
|
| 223 |
-
# Corrige Encoding de Texto
|
| 224 |
-
cols_texto = ['MODALIDADE', 'SITUACAO', 'OBJETO']
|
| 225 |
-
for col in cols_texto:
|
| 226 |
-
if col in df.columns:
|
| 227 |
-
df[col] = df[col].astype(str).apply(corrigir_texto_licitacao)
|
| 228 |
-
|
| 229 |
-
return df
|
| 230 |
-
except Exception as e:
|
| 231 |
-
print(f"Erro Licitações: {e}")
|
| 232 |
-
return pd.DataFrame()
|
| 233 |
-
|
| 234 |
-
# --- CARGA INICIAL ---
|
| 235 |
-
print(f"\n=== NAVEGANTES 360º: STARTUP (Base: {datetime.datetime.now().year}) ===")
|
| 236 |
-
rh_df = carregar_rh_completo()
|
| 237 |
-
receitas_df = carregar_historico_financeiro('receitas')
|
| 238 |
-
rec_orc_df = carregar_historico_financeiro('receitas_orcadas')
|
| 239 |
-
despesas_df = carregar_historico_financeiro('despesas')
|
| 240 |
-
desp_orc_df = carregar_historico_financeiro('despesas_orcadas')
|
| 241 |
-
restos_df = carregar_historico_financeiro('restos')
|
| 242 |
-
licitacoes_df = carregar_licitacoes()
|
| 243 |
-
print("=== SISTEMA PRONTO PARA USO ===")
|
| 244 |
-
|
| 245 |
-
# ==============================================================================
|
| 246 |
-
# 3. DASHBOARD
|
| 247 |
-
# ==============================================================================
|
| 248 |
-
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP, dbc.icons.FONT_AWESOME])
|
| 249 |
-
app.title = "Navegantes 360º"
|
| 250 |
-
server = app.server
|
| 251 |
-
|
| 252 |
-
# Listas
|
| 253 |
-
op_rec = [{'label': 'TOTAL GERAL', 'value': 'TOTAL'}] + ([{'label': n, 'value': n} for n in sorted(receitas_df['contaDescricao'].dropna().unique())] if not receitas_df.empty else [])
|
| 254 |
-
op_desp = [{'label': 'TOTAL GERAL', 'value': 'TOTAL'}] + ([{'label': n, 'value': n} for n in sorted(despesas_df['orgaoDescricao'].dropna().unique())] if not despesas_df.empty else [])
|
| 255 |
-
op_restos = [{'label': 'TOTAL GERAL', 'value': 'TOTAL'}] + ([{'label': n, 'value': n} for n in sorted(restos_df['orgaoDescricao'].dropna().unique())] if not restos_df.empty else [])
|
| 256 |
-
|
| 257 |
-
# Estilos
|
| 258 |
-
SIDEBAR_STYLE = {"position": "fixed", "top": 0, "left": 0, "bottom": 0, "width": "18rem", "padding": "2rem 1rem", "backgroundColor": "#f8f9fa", "overflowY": "auto"}
|
| 259 |
-
CONTENT_STYLE = {"marginLeft": "18rem", "padding": "2rem 1rem"}
|
| 260 |
-
|
| 261 |
-
sidebar = html.Div([
|
| 262 |
-
html.Div([html.Img(src=app.get_asset_url('logo_nvt.jpg'), style={"width": "100%", "maxWidth": "150px"}) if os.path.exists("assets/logo_nvt.jpg") else html.H2("NVT", className="text-center")], className="text-center mb-4"),
|
| 263 |
-
html.Div([html.Div("SEPAF - Departamento de", style={"fontSize": "0.75rem", "color": "#cbd5e1"}), html.Div("Planejamento e Gestão", style={"fontSize": "0.85rem", "fontWeight": "bold", "color": "#ffffff"})], style={"backgroundColor": "#1F2937", "padding": "12px", "borderRadius": "8px", "textAlign": "center", "marginBottom": "1.5rem"}),
|
| 264 |
-
dbc.Nav([
|
| 265 |
-
dbc.NavLink([html.I(className="fas fa-wallet me-3"), "Receitas"], href="/", active="exact"),
|
| 266 |
-
dbc.NavLink([html.I(className="fas fa-file-invoice-dollar me-3"), "Despesas"], href="/despesas", active="exact"),
|
| 267 |
-
dbc.NavLink([html.I(className="fas fa-history me-3"), "Restos a Pagar"], href="/restos", active="exact"),
|
| 268 |
-
dbc.NavLink([html.I(className="fas fa-users me-3"), "Recursos Humanos"], href="/rh", active="exact"),
|
| 269 |
-
html.Div([
|
| 270 |
-
html.B("Licitações", className="ms-3 text-secondary"),
|
| 271 |
-
dbc.NavLink([html.I(className="fas fa-chart-pie me-3"), "Visão Geral"], href="/licitacoes_graficos", active="exact", className="ms-4"),
|
| 272 |
-
dbc.NavLink([html.I(className="fas fa-table me-3"), "Detalhamento"], href="/licitacoes_tabela", active="exact", className="ms-4"),
|
| 273 |
-
], className="my-2"),
|
| 274 |
-
dbc.NavLink([html.I(className="fas fa-table me-3"), "Base de Dados"], href="/tabela", active="exact"),
|
| 275 |
-
], vertical=True, pills=True),
|
| 276 |
-
], style=SIDEBAR_STYLE)
|
| 277 |
-
|
| 278 |
-
content = html.Div(id="page-content", style=CONTENT_STYLE)
|
| 279 |
-
app.layout = html.Div([dcc.Location(id="url", refresh=False), sidebar, content])
|
| 280 |
-
|
| 281 |
-
# --- PÁGINAS ---
|
| 282 |
-
|
| 283 |
-
def layout_licitacoes_graficos():
|
| 284 |
-
if licitacoes_df.empty:
|
| 285 |
-
return dbc.Alert([html.H4("Arquivo não encontrado"), html.P("Faça upload de 'arquivos_combinados.csv' na raiz.")], color="warning")
|
| 286 |
-
|
| 287 |
-
df = licitacoes_df.copy()
|
| 288 |
-
|
| 289 |
-
# 1. KPIs
|
| 290 |
-
ano_atual = datetime.datetime.now().year
|
| 291 |
-
ano_ant = ano_atual - 1
|
| 292 |
-
total_geral = len(df)
|
| 293 |
-
total_atual = len(df[df['ANO'] == ano_atual]) if 'ANO' in df.columns else 0
|
| 294 |
-
total_ant = len(df[df['ANO'] == ano_ant]) if 'ANO' in df.columns else 0
|
| 295 |
-
|
| 296 |
-
# Status
|
| 297 |
-
hom = abt = fra = des = 0
|
| 298 |
-
if 'SITUACAO' in df.columns:
|
| 299 |
-
s = df['SITUACAO'].str.upper().value_counts()
|
| 300 |
-
hom = s.get('HOMOLOGADA', 0)
|
| 301 |
-
abt = s.get('ABERTA', 0) + s.get('EM ANDAMENTO', 0)
|
| 302 |
-
fra = s.get('FRACASSADA', 0)
|
| 303 |
-
des = s.get('DESERTA', 0)
|
| 304 |
-
|
| 305 |
-
# 2. Layout Cards
|
| 306 |
-
cards = html.Div([
|
| 307 |
-
html.H5("Fluxo de Processos", className="text-muted"),
|
| 308 |
-
dbc.Row([
|
| 309 |
-
dbc.Col(dbc.Card(dbc.CardBody([html.H3(total_atual, className="text-primary"), html.P(f"Novos em {ano_atual}")]), className="shadow-sm"), width=4),
|
| 310 |
-
dbc.Col(dbc.Card(dbc.CardBody([html.H3(total_ant, className="text-secondary"), html.P(f"Novos em {ano_ant}")]), className="shadow-sm"), width=4),
|
| 311 |
-
dbc.Col(dbc.Card(dbc.CardBody([html.H3(total_geral, className="text-dark"), html.P("Total Base")]), className="shadow-sm"), width=4),
|
| 312 |
-
], className="mb-4"),
|
| 313 |
-
html.H5("Status Atual", className="text-muted"),
|
| 314 |
-
dbc.Row([
|
| 315 |
-
dbc.Col(dbc.Card(dbc.CardBody([html.H4(hom, className="text-success"), html.P("Homologadas")]), className="shadow-sm border-start border-success border-4"), width=3),
|
| 316 |
-
dbc.Col(dbc.Card(dbc.CardBody([html.H4(abt, className="text-info"), html.P("Abertas")]), className="shadow-sm border-start border-info border-4"), width=3),
|
| 317 |
-
dbc.Col(dbc.Card(dbc.CardBody([html.H4(fra, className="text-warning"), html.P("Fracassadas")]), className="shadow-sm border-start border-warning border-4"), width=3),
|
| 318 |
-
dbc.Col(dbc.Card(dbc.CardBody([html.H4(des, className="text-danger"), html.P("Desertas")]), className="shadow-sm border-start border-danger border-4"), width=3),
|
| 319 |
-
], className="mb-4")
|
| 320 |
-
])
|
| 321 |
-
|
| 322 |
-
# 3. Gráfico (COM FILTRO PARA REMOVER BARRA HOMOLOGADA)
|
| 323 |
-
fig = px.line(title="Sem dados")
|
| 324 |
-
if 'ANO' in df.columns and 'MODALIDADE' in df.columns:
|
| 325 |
-
df_mod = df[df['ANO'] >= 2020].copy()
|
| 326 |
-
# Filtra 'HOMOLOGADA' da coluna Modalidade
|
| 327 |
-
df_mod = df_mod[df_mod['MODALIDADE'].str.upper() != 'HOMOLOGADA']
|
| 328 |
-
|
| 329 |
-
df_grp = df_mod.groupby(['ANO', 'MODALIDADE']).size().reset_index(name='Qtd')
|
| 330 |
-
fig = px.bar(df_grp, x='ANO', y='Qtd', color='MODALIDADE', title="Evolução das Modalidades (2020+)", barmode='group', template='plotly_white')
|
| 331 |
-
fig.update_layout(xaxis_title="Ano", yaxis_title="Processos")
|
| 332 |
-
|
| 333 |
-
return html.Div([html.H2("📊 Painel de Licitações"), html.Hr(), cards, dcc.Graph(figure=fig)])
|
| 334 |
-
|
| 335 |
-
def layout_licitacoes_tabela():
|
| 336 |
-
if licitacoes_df.empty: return dbc.Alert("Sem dados.", color="danger")
|
| 337 |
-
cols = [{"name": c.title().replace('_', ' '), "id": c} for c in licitacoes_df.columns]
|
| 338 |
-
return html.Div([html.H2("📋 Detalhamento"), dbc.Spinner(dash_table.DataTable(data=licitacoes_df.to_dict('records'), columns=cols, page_size=15, sort_action="native", filter_action="native", style_table={'overflowX': 'auto'}))])
|
| 339 |
-
|
| 340 |
-
def layout_rh():
|
| 341 |
-
if rh_df.empty: return dbc.Alert("Dados de RH indisponíveis.", color="danger")
|
| 342 |
-
|
| 343 |
-
kpis = dbc.Row([
|
| 344 |
-
dbc.Col(dbc.Card(dbc.CardBody([html.H4(len(rh_df), className="text-primary"), html.P("Ativos")]), className="shadow-sm border-start border-primary border-4"), width=4),
|
| 345 |
-
dbc.Col(dbc.Card(dbc.CardBody([html.H4(f"R$ {rh_df['salarioBase'].sum():,.2f}", className="text-success"), html.P("Folha Mensal")]), className="shadow-sm border-start border-success border-4"), width=4),
|
| 346 |
-
dbc.Col(dbc.Card(dbc.CardBody([html.H4(f"R$ {rh_df['salarioBase'].mean():,.2f}", className="text-info"), html.P("Média")]), className="shadow-sm border-start border-info border-4"), width=4),
|
| 347 |
-
], className="mb-4")
|
| 348 |
-
|
| 349 |
-
df_cargos = rh_df['cargo'].value_counts().nlargest(10).reset_index()
|
| 350 |
-
df_cargos.columns = ['Cargo', 'Qtd']
|
| 351 |
-
fig_bar = px.bar(df_cargos, x='Qtd', y='Cargo', orientation='h', title="Top 10 Cargos", template='plotly_white')
|
| 352 |
-
fig_bar.update_layout(yaxis={'categoryorder':'total ascending'})
|
| 353 |
-
|
| 354 |
-
col_regime = 'regime' if 'regime' in rh_df.columns else 'situacao'
|
| 355 |
-
df_regime = rh_df[col_regime].value_counts().reset_index()
|
| 356 |
-
df_regime.columns = ['Tipo', 'Qtd']
|
| 357 |
-
fig_tree = px.treemap(df_regime, path=['Tipo'], values='Qtd', color='Tipo', title="Vínculos", template='plotly_white')
|
| 358 |
-
fig_tree.update_traces(textinfo="label+value+percent root")
|
| 359 |
-
|
| 360 |
-
return html.Div([html.H2("Gestão de Pessoas"), html.Hr(), kpis, dbc.Row([dbc.Col(dcc.Graph(figure=fig_bar), width=12)]), dbc.Row([dbc.Col(dcc.Graph(figure=fig_tree), width=12)])])
|
| 361 |
-
|
| 362 |
-
def layout_receitas():
|
| 363 |
-
return html.Div([
|
| 364 |
-
html.H2("Receitas Públicas"), html.Hr(),
|
| 365 |
-
dbc.Row([dbc.Col(dcc.Dropdown(id='rec-dd', options=op_rec, value='TOTAL'), width=8), dbc.Col(dcc.Dropdown(id='rec-stat', options=[{'label':'Soma','value':'sum'},{'label':'Média','value':'mean'}], value='sum'), width=4)]),
|
| 366 |
-
dcc.Graph(id='rec-graph')
|
| 367 |
-
])
|
| 368 |
-
def layout_despesas(): return html.Div([html.H2("Despesas"), html.Hr(), dcc.Dropdown(id='desp-dd', options=op_desp, value='TOTAL'), dcc.Graph(id='desp-graph')])
|
| 369 |
-
def layout_restos(): return html.Div([html.H2("Restos a Pagar"), html.Hr(), dcc.Dropdown(id='restos-dd', options=op_restos, value='TOTAL'), dcc.Graph(id='restos-graph')])
|
| 370 |
-
def layout_tabela(): return html.Div([html.H2("Base de Dados"), dcc.Dropdown(id='tab-dd', options=[{'label':'Receitas','value':'rec'},{'label':'RH','value':'rh'},{'label':'Licitações','value':'lic'}], value='rec'), html.Br(), dbc.Spinner(dash_table.DataTable(id='tabela-main', page_size=15, style_table={'overflowX': 'auto'}))])
|
| 371 |
-
|
| 372 |
-
# --- CALLBACKS ---
|
| 373 |
-
@app.callback(Output("page-content", "children"), Input("url", "pathname"))
|
| 374 |
-
def render_page(path):
|
| 375 |
-
if path == "/despesas": return layout_despesas()
|
| 376 |
-
if path == "/restos": return layout_restos()
|
| 377 |
-
if path == "/rh": return layout_rh()
|
| 378 |
-
if path == "/tabela": return layout_tabela()
|
| 379 |
-
if path == "/licitacoes_graficos": return layout_licitacoes_graficos()
|
| 380 |
-
if path == "/licitacoes_tabela": return layout_licitacoes_tabela()
|
| 381 |
-
return layout_receitas()
|
| 382 |
-
|
| 383 |
-
@app.callback(Output('rec-graph', 'figure'), [Input('rec-dd', 'value'), Input('rec-stat', 'value')])
|
| 384 |
-
def update_rec(conta, stat):
|
| 385 |
-
if receitas_df.empty: return px.line(title="Sem dados")
|
| 386 |
-
df = receitas_df if conta == 'TOTAL' else receitas_df[receitas_df['contaDescricao'] == conta]
|
| 387 |
-
df_orc = rec_orc_df if conta == 'TOTAL' else pd.DataFrame()
|
| 388 |
-
g_real = df.groupby('ano')['valorArrecadado'].agg(stat).reset_index()
|
| 389 |
-
fig = go.Figure()
|
| 390 |
-
fig.add_trace(go.Bar(x=g_real['ano'], y=g_real['valorArrecadado'], name='Realizado', marker_color='#2E86C1'))
|
| 391 |
-
if not df_orc.empty and conta == 'TOTAL':
|
| 392 |
-
g_orc = df_orc.groupby('ano')['valorOrcado'].agg(stat).reset_index()
|
| 393 |
-
fig.add_trace(go.Scatter(x=g_orc['ano'], y=g_orc['valorOrcado'], name='Meta', line=dict(color='red', dash='dot')))
|
| 394 |
-
if len(g_real) > 1:
|
| 395 |
-
x, y = g_real['ano'], g_real['valorArrecadado']
|
| 396 |
-
z = np.polyfit(x, y, 1); p = np.poly1d(z)
|
| 397 |
-
fig.add_trace(go.Scatter(x=x, y=p(x), name='Tendência', line=dict(color='orange', width=2)))
|
| 398 |
-
fig.update_layout(template="plotly_white", title=f"Evolução: {conta}")
|
| 399 |
-
return fig
|
| 400 |
-
|
| 401 |
-
@app.callback(Output('desp-graph', 'figure'), Input('desp-dd', 'value'))
|
| 402 |
-
def update_desp(orgao):
|
| 403 |
-
if despesas_df.empty: return px.line(title="Sem dados")
|
| 404 |
-
df = despesas_df if orgao == 'TOTAL' else despesas_df[despesas_df['orgaoDescricao'] == orgao]
|
| 405 |
-
g_real = df.groupby('ano')['valorEmpenhado'].sum().reset_index()
|
| 406 |
-
fig = go.Figure()
|
| 407 |
-
fig.add_trace(go.Bar(x=g_real['ano'], y=g_real['valorEmpenhado'], name='Empenhado', marker_color='#D4AC0D'))
|
| 408 |
-
if len(g_real) > 1:
|
| 409 |
-
x, y = g_real['ano'], g_real['valorEmpenhado']
|
| 410 |
-
z = np.polyfit(x, y, 1); p = np.poly1d(z)
|
| 411 |
-
fig.add_trace(go.Scatter(x=x, y=p(x), name='Tendência', line=dict(color='black', dash='dash')))
|
| 412 |
-
fig.update_layout(template="plotly_white", title=f"Despesa: {orgao}")
|
| 413 |
-
return fig
|
| 414 |
-
|
| 415 |
-
@app.callback(Output('restos-graph', 'figure'), Input('restos-dd', 'value'))
|
| 416 |
-
def update_rest(orgao):
|
| 417 |
-
df = restos_df if orgao == 'TOTAL' else restos_df[restos_df['orgaoDescricao'] == orgao]
|
| 418 |
-
return px.bar(df.groupby('ano')['valorPago'].sum().reset_index(), x='ano', y='valorPago', title="Restos Pagos", template="plotly_white")
|
| 419 |
-
|
| 420 |
-
@app.callback([Output('tabela-main', 'data'), Output('tabela-main', 'columns')], Input('tab-dd', 'value'))
|
| 421 |
-
def update_tab(tipo):
|
| 422 |
-
df_map = {'rh': rh_df, 'rec': receitas_df.head(200), 'lic': licitacoes_df.head(200)}
|
| 423 |
-
df = df_map.get(tipo, pd.DataFrame())
|
| 424 |
-
cols = [{"name": c, "id": c} for c in df.columns]
|
| 425 |
-
return df.to_dict('records'), cols
|
| 426 |
-
|
| 427 |
-
if __name__ == '__main__':
|
| 428 |
-
app.run(debug=False, host='0.0.0.0', port=7860)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|