MLMPBR commited on
Commit
8a176ca
·
verified ·
1 Parent(s): e93bd58

Delete app.py

Browse files
Files changed (1) hide show
  1. app.py +0 -428
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)