Download app.py from pmnvt/dashv3: direct link, hf CLI and curl.
- Browser
- Download file 16.9 kB
-
https://huggingface.co/spaces/pmnvt/dashv3/resolve/main/app.py
- Command line
-
hf download hf://spaces/pmnvt/dashv3/app.py
-
curl -L -o app.py https://huggingface.co/spaces/pmnvt/dashv3/resolve/main/app.py
16.9 kB
| import dash | |
| from dash import dcc, html, Input, Output, dash_table | |
| import dash_bootstrap_components as dbc | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| import pandas as pd | |
| import numpy as np | |
| import os | |
| import re | |
| # --- CONFIGURAÇÕES DE PASTA --- | |
| PASTA_CACHE = "dados_cache" | |
| if not os.path.exists(PASTA_CACHE): os.makedirs(PASTA_CACHE) | |
| # ============================================================================== | |
| # 1. MOTORES DE CARGA E LIMPEZA | |
| # ============================================================================== | |
| def converter_para_float(valor): | |
| if pd.isna(valor) or valor == '': return 0.0 | |
| if isinstance(valor, (int, float)): return float(valor) | |
| try: | |
| v = str(valor).replace('R$', '').strip() | |
| # Formato brasileiro (ex: 1.234.567,89 ou 1.000,00) | |
| if ',' in v and '.' in v and v.rfind(',') > v.rfind('.'): | |
| v = v.replace('.', '').replace(',', '.') | |
| # Formato americano (ex: 1,234,567.89 ou 1,000.00) | |
| elif ',' in v and '.' in v and v.rfind('.') > v.rfind(','): | |
| v = v.replace(',', '') | |
| # Apenas vírgula (ex: 1000,50 -> BR) ou (1,000,000 -> US) | |
| elif ',' in v and '.' not in v: | |
| if v.count(',') > 1: # Múltiplas vírgulas = padrão americano | |
| v = v.replace(',', '') | |
| else: # Uma vírgula = decimal brasileiro | |
| v = v.replace(',', '.') | |
| # Apenas ponto (ex: 1000.50 -> US) ou (1.000.000 -> BR) | |
| elif '.' in v and ',' not in v: | |
| if v.count('.') > 1: # Múltiplos pontos = milhares brasileiros | |
| v = v.replace('.', '') | |
| # Se for só um ponto (ex: 539.72), o Python lê nativamente sem alterar. | |
| return float(v) | |
| except: | |
| return 0.0 | |
| def limpar_colunas(df): | |
| if df.empty: return df | |
| # A mágica aqui: removemos espaços vazios e sublinhados dos cabeçalhos | |
| df.columns = [str(c).strip().lower().replace(' ', '').replace('_', '') for c in df.columns] | |
| mapa = {'exercicio': 'ano', 'ano_licitacao': 'ano', 'exercicio_contrato': 'ano'} | |
| df.rename(columns=mapa, inplace=True) | |
| return df | |
| def carregar_csv_seguro(nome_arquivo, separador=';'): | |
| caminho = os.path.join(PASTA_CACHE, nome_arquivo) | |
| if not os.path.exists(caminho): return pd.DataFrame() | |
| # Tenta ler com o separador inicial (ponto e vírgula) | |
| try: | |
| df = pd.read_csv(caminho, sep=separador, encoding='utf-8', on_bad_lines='skip', low_memory=False) | |
| if len(df.columns) == 1: # Se não dividiu direito, tenta vírgula | |
| df = pd.read_csv(caminho, sep=',', encoding='utf-8', on_bad_lines='skip', low_memory=False) | |
| except: | |
| df = pd.read_csv(caminho, sep=separador, encoding='latin1', on_bad_lines='skip', low_memory=False) | |
| if len(df.columns) == 1: | |
| df = pd.read_csv(caminho, sep=',', encoding='latin1', on_bad_lines='skip', low_memory=False) | |
| return limpar_colunas(df) | |
| def carregar_todos_os_anos(prefixo): | |
| arquivos = [f for f in os.listdir(PASTA_CACHE) if f.startswith(prefixo) and f.endswith('.csv')] | |
| dfs = [] | |
| for f in arquivos: | |
| # TENTA PONTO E VÍRGULA PRIMEIRO (Evita quebrar os centavos) | |
| try: | |
| temp_df = pd.read_csv(os.path.join(PASTA_CACHE, f), sep=';', encoding='utf-8', on_bad_lines='skip') | |
| if len(temp_df.columns) == 1: | |
| temp_df = pd.read_csv(os.path.join(PASTA_CACHE, f), sep=',', encoding='utf-8', on_bad_lines='skip') | |
| except: | |
| temp_df = pd.read_csv(os.path.join(PASTA_CACHE, f), sep=';', encoding='latin1', on_bad_lines='skip') | |
| if len(temp_df.columns) == 1: | |
| temp_df = pd.read_csv(os.path.join(PASTA_CACHE, f), sep=',', encoding='latin1', on_bad_lines='skip') | |
| if not temp_df.empty: | |
| match = re.search(r'(\d{4})', f) | |
| if match: temp_df['ano'] = int(match.group(1)) | |
| dfs.append(limpar_colunas(temp_df)) | |
| if not dfs: return pd.DataFrame() | |
| df_final = pd.concat(dfs, ignore_index=True) | |
| cols_financeiras = ['valorarrecadado', 'valorempenhado', 'valorpago'] | |
| for col in cols_financeiras: | |
| if col in df_final.columns: | |
| df_final[col] = df_final[col].apply(converter_para_float) | |
| return df_final | |
| # --- CARGA GLOBAL --- | |
| receitas_df = carregar_todos_os_anos("receitas_") | |
| despesas_df = carregar_todos_os_anos("despesas_") | |
| restos_df = carregar_todos_os_anos("restos_") | |
| licitacoes_df = carregar_csv_seguro("licitacoes.csv") | |
| contratos_df = carregar_csv_seguro("contratos.csv") | |
| if not contratos_df.empty and 'valortotal' in contratos_df.columns: | |
| contratos_df['valortotal'] = contratos_df['valortotal'].apply(converter_para_float) | |
| col_est = next((c for c in licitacoes_df.columns if 'estimado' in c), None) | |
| col_hom = next((c for c in licitacoes_df.columns if 'homologado' in c), None) | |
| col_sit = next((c for c in licitacoes_df.columns if 'situa' in c or 'status' in c), None) | |
| col_mod = next((c for c in licitacoes_df.columns if 'modalidade' in c), None) | |
| if col_est: licitacoes_df[col_est] = licitacoes_df[col_est].apply(converter_para_float) | |
| if col_hom: licitacoes_df[col_hom] = licitacoes_df[col_hom].apply(converter_para_float) | |
| # ============================================================================== | |
| # 2. OPÇÕES DOS DROPDOWNS E PRÉ-CÁLCULOS | |
| # ============================================================================== | |
| op_rec = [{'label': 'VISÃO GERAL', 'value': 'TOTAL'}] | |
| if not receitas_df.empty and 'contadescricao' in receitas_df.columns: | |
| op_rec += [{'label': str(n), 'value': str(n)} for n in sorted(receitas_df['contadescricao'].dropna().unique())] | |
| op_desp = [{'label': 'VISÃO GERAL', 'value': 'TOTAL'}] | |
| if not despesas_df.empty and 'orgaodescricao' in despesas_df.columns: | |
| op_desp += [{'label': str(n), 'value': str(n)} for n in sorted(despesas_df['orgaodescricao'].dropna().unique())] | |
| op_forn = [{'label': 'VISÃO GERAL (Top 10 Fornecedores)', 'value': 'TOTAL'}] | |
| if not contratos_df.empty and 'fornecedor' in contratos_df.columns: | |
| op_forn += [{'label': str(n), 'value': str(n)} for n in sorted(contratos_df['fornecedor'].dropna().unique())] | |
| op_mod = [{'label': 'VISÃO GERAL (Todas as Modalidades)', 'value': 'TOTAL'}] | |
| if not licitacoes_df.empty and col_mod: | |
| op_mod += [{'label': str(n).upper(), 'value': str(n)} for n in sorted(licitacoes_df[col_mod].dropna().astype(str).unique())] | |
| def format_currency(val): | |
| return f"R$ {val:,.2f}".replace(',','X').replace('.',',').replace('X','.') | |
| def extrair_periodo(df): | |
| if not df.empty and 'ano' in df.columns: | |
| anos = pd.to_numeric(df['ano'], errors='coerce').dropna() | |
| if not anos.empty: return f"({int(anos.min())} a {int(anos.max())})" | |
| return "(Base Total)" | |
| rec_anual = receitas_df.groupby('ano')['valorarrecadado'].sum().reset_index() if not receitas_df.empty else pd.DataFrame() | |
| des_anual = despesas_df.groupby('ano')['valorempenhado'].sum().reset_index() if not despesas_df.empty else pd.DataFrame() | |
| # ============================================================================== | |
| # 3. ESTRUTURA BASE (SIDEBAR) | |
| # ============================================================================== | |
| app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP, dbc.icons.FONT_AWESOME]) | |
| app.title = "Navegantes 360º" | |
| server = app.server | |
| SIDEBAR_STYLE = { | |
| "position": "fixed", "top": 0, "left": 0, "bottom": 0, "width": "18rem", | |
| "padding": "2rem 1rem", "backgroundColor": "#1F2937", "color": "white", "overflowY": "auto" | |
| } | |
| CONTENT_STYLE = {"marginLeft": "18rem", "padding": "2rem 1rem", "backgroundColor": "#F3F4F6", "minHeight": "100vh"} | |
| sidebar = html.Div([ | |
| html.Div([ | |
| html.H3("⚓ Navegantes 360", className="text-white mb-0"), | |
| html.Div(id="periodo-dinamico", style={"fontSize": "0.8rem", "color": "#9CA3AF", "marginTop": "5px"}) | |
| ], className="text-center mb-4"), | |
| html.Div([ | |
| html.Div("SEPAF - Departamento de", style={"fontSize": "0.7rem", "color": "#9CA3AF"}), | |
| html.Div("Planejamento e Gestão", style={"fontSize": "0.85rem", "fontWeight": "bold"}) | |
| ], style={"backgroundColor": "#111827", "padding": "12px", "borderRadius": "8px", "textAlign": "center", "marginBottom": "2rem"}), | |
| dbc.Nav([ | |
| dbc.NavLink([html.I(className="fas fa-chart-line me-3"), "Visão Geral"], href="/", active="exact", className="text-white-50"), | |
| dbc.NavLink([html.I(className="fas fa-wallet me-3"), "Receitas"], href="/receitas", active="exact", className="text-white-50"), | |
| dbc.NavLink([html.I(className="fas fa-file-invoice-dollar me-3"), "Despesas"], href="/despesas", active="exact", className="text-white-50"), | |
| html.Hr(style={"color": "#4B5563"}), | |
| dbc.NavLink([html.I(className="fas fa-gavel me-3"), "Licitações"], href="/licitacoes", active="exact", className="text-white-50"), | |
| dbc.NavLink([html.I(className="fas fa-file-contract me-3"), "Contratos"], href="/contratos", active="exact", className="text-white-50"), | |
| ], vertical=True, pills=True), | |
| ], style=SIDEBAR_STYLE) | |
| app.layout = html.Div([dcc.Location(id="url"), sidebar, html.Div(id="page-content", style=CONTENT_STYLE)]) | |
| # ============================================================================== | |
| # 4. CALLBACKS E ROTEAMENTO | |
| # ============================================================================== | |
| def atualizar_periodo(path): | |
| df_map = {"/": pd.concat([rec_anual, des_anual]) if not rec_anual.empty else pd.DataFrame(), "/receitas": rec_anual, "/despesas": des_anual, "/licitacoes": licitacoes_df, "/contratos": contratos_df} | |
| return f"Período: {extrair_periodo(df_map.get(path, pd.DataFrame()))}" | |
| def render_page(path): | |
| if path == "/receitas": | |
| return html.Div([ | |
| html.H2("💰 Receitas"), | |
| dcc.Dropdown(id='rec-dd', options=op_rec, value='TOTAL', className="mb-4", clearable=False), | |
| html.Div(id='rec-content') | |
| ]) | |
| if path == "/despesas": | |
| return html.Div([ | |
| html.H2("💸 Despesas"), | |
| dcc.Dropdown(id='desp-dd', options=op_desp, value='TOTAL', className="mb-4", clearable=False), | |
| html.Div(id='desp-content') | |
| ]) | |
| if path == "/contratos": | |
| return html.Div([ | |
| html.H2("📄 Gestão de Contratos"), | |
| dcc.Dropdown(id='contrato-dd', options=op_forn, value='TOTAL', className="mb-4", clearable=False), | |
| html.Div(id='contrato-content') | |
| ]) | |
| if path == "/licitacoes": | |
| return html.Div([ | |
| html.H2("⚖️ Processos Licitatórios"), | |
| dcc.Dropdown(id='licitacao-dd', options=op_mod, value='TOTAL', className="mb-4", clearable=False), | |
| html.Div(id='licitacao-content') | |
| ]) | |
| # HOME MANTIDA INTACTA CONFORME PEDIDO | |
| if rec_anual.empty or des_anual.empty: return dbc.Alert("Aguardando dados...", color="warning") | |
| fig = go.Figure() | |
| fig.add_trace(go.Bar(x=rec_anual['ano'], y=rec_anual['valorarrecadado'], name="Receita", marker_color='#10B981')) | |
| fig.add_trace(go.Bar(x=des_anual['ano'], y=des_anual['valorempenhado'], name="Despesa", marker_color='#EF4444')) | |
| if len(rec_anual) > 1: | |
| fig.add_trace(go.Scatter(x=rec_anual['ano'], y=np.poly1d(np.polyfit(rec_anual['ano'], rec_anual['valorarrecadado'], 1))(rec_anual['ano']), name='Tendência Rec.', line=dict(color='#065F46', dash='dot'))) | |
| if len(des_anual) > 1: | |
| fig.add_trace(go.Scatter(x=des_anual['ano'], y=np.poly1d(np.polyfit(des_anual['ano'], des_anual['valorempenhado'], 1))(des_anual['ano']), name='Tendência Desp.', line=dict(color='#991B1B', dash='dot'))) | |
| fig.update_layout(title="Equilíbrio Orçamentário Anual com Tendência", barmode='group', template="plotly_white") | |
| total_con = contratos_df['valortotal'].sum() if not contratos_df.empty and 'valortotal' in contratos_df.columns else 0 | |
| ultima_rec = rec_anual['valorarrecadado'].iloc[-1] if not rec_anual.empty else 0 | |
| ano_rec = int(rec_anual['ano'].iloc[-1]) if not rec_anual.empty else "" | |
| return html.Div([ | |
| html.H2("🏁 Visão Geral Executiva", className="mb-4"), | |
| dbc.Row([ | |
| dbc.Col(dbc.Card(dbc.CardBody([html.H4(format_currency(ultima_rec), className="text-success"), html.P(f"Última Receita Anual ({ano_rec})")]), className="border-0 shadow-sm"), width=4), | |
| dbc.Col(dbc.Card(dbc.CardBody([html.H4(format_currency(total_con), className="text-primary"), html.P(f"Total em Contratos {extrair_periodo(contratos_df)}")]), className="border-0 shadow-sm"), width=4), | |
| dbc.Col(dbc.Card(dbc.CardBody([html.H4(len(licitacoes_df), className="text-info"), html.P(f"Processos Licitatórios {extrair_periodo(licitacoes_df)}")]), className="border-0 shadow-sm"), width=4), | |
| ], className="mb-4"), | |
| dbc.Card(dbc.CardBody(dcc.Graph(figure=fig)), className="border-0 shadow-sm") | |
| ]) | |
| # --- LÓGICA DINÂMICA DOS DROPDOWNS --- | |
| def update_rec(conta): | |
| df = receitas_df if conta == 'TOTAL' else receitas_df[receitas_df['contadescricao'] == conta] | |
| fig = px.line(df.groupby('ano')['valorarrecadado'].sum().reset_index(), x='ano', y='valorarrecadado', title=f"Evolução: {conta}", markers=True, template="plotly_white") | |
| return dbc.Card(dbc.CardBody(dcc.Graph(figure=fig)), className="border-0 shadow-sm") | |
| def update_desp(orgao): | |
| df = despesas_df if orgao == 'TOTAL' else despesas_df[despesas_df['orgaodescricao'] == orgao] | |
| fig = px.bar(df.groupby('ano')['valorempenhado'].sum().reset_index(), x='ano', y='valorempenhado', title=f"Despesa: {orgao}", template="plotly_white", color_discrete_sequence=['#EF4444']) | |
| return dbc.Card(dbc.CardBody(dcc.Graph(figure=fig)), className="border-0 shadow-sm") | |
| def update_contrato(forn): | |
| if contratos_df.empty: return dbc.Alert("Sem dados", color="danger") | |
| if forn == 'TOTAL': | |
| df_top = contratos_df.groupby('fornecedor')['valortotal'].sum().nlargest(10).reset_index() | |
| fig = px.bar(df_top, x='valortotal', y='fornecedor', orientation='h', title="Top 10 Fornecedores", template="plotly_white", color_discrete_sequence=['#3B82F6']) | |
| fig.update_layout(yaxis={'categoryorder':'total ascending'}) | |
| return html.Div([ | |
| dbc.Row([dbc.Col(dbc.Card(dbc.CardBody([html.H4(format_currency(contratos_df['valortotal'].sum()), className="text-primary"), html.P("Volume Financeiro Total")]), className="border-0 shadow-sm mb-4"), width=6)]), | |
| dbc.Card(dbc.CardBody(dcc.Graph(figure=fig)), className="border-0 shadow-sm") | |
| ]) | |
| else: | |
| df_f = contratos_df[contratos_df['fornecedor'] == forn] | |
| cols = [{"name": i.upper(), "id": i} for i in df_f.columns if i not in ['ano', 'fornecedor']] | |
| return html.Div([ | |
| dbc.Row([dbc.Col(dbc.Card(dbc.CardBody([html.H4(format_currency(df_f['valortotal'].sum()), className="text-primary"), html.P(f"Total Contratado: {forn}")]), className="border-0 shadow-sm mb-4"), width=12)]), | |
| dbc.Card(dbc.CardBody(dash_table.DataTable(data=df_f.to_dict('records'), columns=cols, page_size=10, style_table={'overflowX': 'auto'}, style_header={'backgroundColor': '#F3F4F6', 'fontWeight': 'bold'})), className="border-0 shadow-sm") | |
| ]) | |
| def update_licitacao(mod): | |
| if licitacoes_df.empty: return dbc.Alert("Sem dados", color="danger") | |
| df_l = licitacoes_df if mod == 'TOTAL' else licitacoes_df[licitacoes_df[col_mod] == mod] | |
| fig_status = go.Figure().update_layout(title="Status não disponível") | |
| if col_sit: fig_status = px.bar(df_l[col_sit].value_counts().reset_index(), x=col_sit, y='count', title=f"Status: {mod}", template="plotly_white", color_discrete_sequence=['#8B5CF6']) | |
| est = df_l[col_est].sum() if col_est else 0 | |
| hom = df_l[col_hom].sum() if col_hom else 0 | |
| return html.Div([ | |
| dbc.Row([ | |
| dbc.Col(dbc.Card(dbc.CardBody([html.H4(format_currency(est), className="text-warning"), html.P("Estimado Global")]), className="border-0 shadow-sm"), width=6), | |
| dbc.Col(dbc.Card(dbc.CardBody([html.H4(format_currency(hom), className="text-success"), html.P("Homologado Global")]), className="border-0 shadow-sm"), width=6), | |
| ], className="mb-4"), | |
| dbc.Card(dbc.CardBody(dcc.Graph(figure=fig_status)), className="border-0 shadow-sm") | |
| ]) | |
| if __name__ == '__main__': | |
| app.run(debug=False, host='0.0.0.0', port=7860) |