Thslc99 commited on
Commit
1cecdaa
·
verified ·
1 Parent(s): 2d6ddd0

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +109 -77
app.py CHANGED
@@ -1,13 +1,17 @@
1
  """
2
  LegalOne – PowerBI-like Dashboard (Streamlit)
3
- --------------------------------------------
4
- Visual estilo Power BI: cards, filtros na sidebar, gráficos Plotly, tabela interativa (AgGrid),
5
- export de CSV filtrado e tema escuro. Ingestão por upload de CSV gerado do Legal One.
6
-
7
- Como usar no Hugging Face:
8
- - Crie um Space do tipo **Streamlit**.
9
- - Suba este arquivo como `app.py` e um `requirements.txt` (veja no README da conversa).
10
- - Abra o Space → faça upload do CSV → explore.
 
 
 
 
11
  """
12
 
13
  from __future__ import annotations
@@ -15,32 +19,33 @@ import io
15
  import json
16
  import re
17
  import unicodedata
18
- from typing import Dict, Optional
 
19
 
20
- import pandas as pd
21
  import numpy as np
 
22
  import plotly.express as px
23
  import streamlit as st
24
- from datetime import datetime
25
 
 
26
  try:
27
  from st_aggrid import AgGrid, GridOptionsBuilder, GridUpdateMode
28
  AG_AVAILABLE = True
29
  except Exception:
30
  AG_AVAILABLE = False
31
 
 
32
  st.set_page_config(page_title="LegalOne Dashboard", layout="wide")
 
 
 
33
 
34
- # ----------------- Estilo / Tema -----------------
35
-
36
- # Paleta sóbria tipo Power BI
37
- PRIMARY_BG = "#0f172a" # slate-900
38
- CARD_BG = "#111827" # gray-900
39
- TEXT = "#e5e7eb" # gray-200
40
- ACCENT = "#22c55e" # green-500
41
- SUBTLE = "#94a3b8" # slate-400
42
 
43
- CARD_CSS = f"""
44
  <style>
45
  html, body, [class^="css"], .stApp {{ background-color: {PRIMARY_BG} !important; }}
46
  .block-container {{ padding-top: 1rem; padding-bottom: 1rem; }}
@@ -50,30 +55,30 @@ html, body, [class^="css"], .stApp {{ background-color: {PRIMARY_BG} !important;
50
  .section-title {{ color: {TEXT}; font-weight: 700; font-size: 1.2rem; margin: 12px 0 8px 0; }}
51
  hr {{ border: none; border-top: 1px solid #1f2937; margin: 8px 0 16px 0; }}
52
  </style>
53
- """
54
-
55
- st.markdown(CARD_CSS, unsafe_allow_html=True)
56
-
57
- px.defaults.template = "plotly_dark"
58
- px.defaults.width = None
59
- px.defaults.height = 420
60
-
61
- # ----------------- Utilidades -----------------
62
 
 
63
  def _norm_key(s: Optional[str]) -> str:
64
  if s is None:
65
  return ""
66
  s = str(s).strip().lower()
67
- s = "".join(c for c in unicodedata.normalize("NFD", s) if unicodedata.category(c) != 'Mn')
68
  s = re.sub(r"\s+", " ", s)
69
  return s
70
 
71
  @st.cache_data(show_spinner=False)
72
  def load_csv(upload) -> pd.DataFrame:
 
 
 
 
 
73
  if upload is None:
74
  return pd.DataFrame()
 
75
  df = pd.read_csv(upload)
76
- # garante colunas esperadas
 
77
  cols = [
78
  "processo_numero","cliente","contrario","valor_causa","acao","natureza","area","orgao",
79
  "comarca","tribunal","vara","situacao","data_ajuizamento","posicao_cliente",
@@ -82,23 +87,34 @@ def load_csv(upload) -> pd.DataFrame:
82
  for c in cols:
83
  if c not in df.columns:
84
  df[c] = None
 
85
  # tipos
86
  df["valor_causa"] = pd.to_numeric(df["valor_causa"], errors="coerce")
87
  df["data_ajuizamento"] = pd.to_datetime(df["data_ajuizamento"], errors="coerce")
88
- # cat escritório (se faltou), cria como cópia do bruto
89
- df["escritorio_cat"] = df["escritorio_cat"].fillna(df["escritorio_responsavel"]).astype(str)
 
 
 
 
 
 
 
90
  return df[cols]
91
 
92
  @st.cache_data(show_spinner=False)
93
  def apply_mapping(df: pd.DataFrame, mapping_json: str) -> pd.DataFrame:
94
- if not mapping_json:
 
95
  return df
96
  try:
97
  raw = json.loads(mapping_json)
98
  mapping = {_norm_key(k): v for k, v in raw.items()}
99
- df = df.copy()
100
- df["escritorio_cat"] = df["escritorio_responsavel"].apply(lambda x: mapping.get(_norm_key(x), x))
101
- return df
 
 
102
  except Exception:
103
  return df
104
 
@@ -127,13 +143,16 @@ def filter_df(
127
  out = out[out["acao"].isin(acao)]
128
  if periodo and not pd.isna(out["data_ajuizamento"]).all():
129
  start, end = periodo
130
- out = out[(out["data_ajuizamento"] >= pd.to_datetime(start)) & (out["data_ajuizamento"] <= pd.to_datetime(end))]
 
 
 
131
  return out
132
 
133
- # ----------------- Sidebar (Filtros) -----------------
134
-
135
  st.sidebar.title("⚙️ Filtros")
136
- up = st.sidebar.file_uploader("CSV do Legal One", type=["csv"]) # upload manual
 
137
  mapping_str = st.sidebar.text_area(
138
  "Mapeamento (JSON opcional) — escritório bruto → categoria",
139
  value='{"CÍVEL PARTIDO":"Cível – Partido","CÍVEL INDIVIDUAL":"Cível – Individual","CÍVEL RECUPERAÇÃO DE CRÉDITO":"Cível – Recuperação de Crédito"}',
@@ -147,7 +166,7 @@ if base.empty:
147
  st.info("Faça upload do CSV para começar.")
148
  st.stop()
149
 
150
- # Valores únicos para filtros
151
  cats = sorted([c for c in base["escritorio_cat"].dropna().unique()])
152
  tribs = sorted([t for t in base["tribunal"].dropna().unique()])
153
  nats = sorted([n for n in base["natureza"].dropna().unique()])
@@ -165,92 +184,105 @@ max_dt = pd.to_datetime(base["data_ajuizamento"]).max()
165
  if pd.isna(min_dt) or pd.isna(max_dt):
166
  period = None
167
  else:
168
- period = st.sidebar.date_input("Período (Data de ajuizamento)", value=(min_dt.date(), max_dt.date()))
 
 
 
169
 
170
  f = filter_df(base, cliente_q, sel_cat, sel_trib, sel_nat, sel_acao, period)
171
 
172
- # ----------------- KPIs (cards) -----------------
173
-
174
  col1, col2, col3, col4 = st.columns(4)
175
  with col1:
176
- st.markdown('<div class="kpi-card"><div class="kpi-label">Processos</div>'
177
- f'<div class="kpi-value">{int(f["processo_numero"].nunique())}</div></div>', unsafe_allow_html=True)
 
 
 
178
  with col2:
179
- st.markdown('<div class="kpi-card"><div class="kpi-label">Clientes</div>'
180
- f'<div class="kpi-value">{int(f["cliente"].nunique())}</div></div>', unsafe_allow_html=True)
 
 
 
181
  with col3:
182
- st.markdown('<div class="kpi-card"><div class="kpi-label">Categorias de Escritório</div>'
183
- f'<div class="kpi-value">{int(f["escritorio_cat"].nunique())}</div></div>', unsafe_allow_html=True)
 
 
 
184
  with col4:
185
  total_valor = float(f["valor_causa"].fillna(0).sum())
186
- st.markdown('<div class="kpi-card"><div class="kpi-label">Soma Valor da Causa</div>'
187
- f'<div class="kpi-value">R$ {total_valor:,.2f}</div></div>', unsafe_allow_html=True)
 
 
 
188
 
189
  st.markdown("<div class='section-title'>Visão Geral</div>", unsafe_allow_html=True)
190
 
191
- # ----------------- Gráficos principais -----------------
192
-
193
  gcol1, gcol2 = st.columns(2)
194
 
195
  with gcol1:
196
- top_cat = (f.groupby("escritorio_cat")["processo_numero"].nunique()
197
- .sort_values(ascending=False).head(15).reset_index(name="qtd"))
198
- fig1 = px.bar(top_cat, x="escritorio_cat", y="qtd", title="Processos por Categoria de Escritório (Top 15)")
199
- fig1.update_layout(margin=dict(l=10,r=10,b=10,t=50))
 
 
 
200
  st.plotly_chart(fig1, use_container_width=True)
201
 
202
  with gcol2:
203
- by_tri = (f.groupby("tribunal")["processo_numero"].nunique()
204
- .sort_values(ascending=False).reset_index(name="qtd"))
 
 
205
  fig2 = px.bar(by_tri, x="tribunal", y="qtd", title="Processos por Tribunal")
206
- fig2.update_layout(margin=dict(l=10,r=10,b=10,t=50))
207
  st.plotly_chart(fig2, use_container_width=True)
208
 
209
- # Linha do tempo (mês)
210
  if not f["data_ajuizamento"].isna().all():
211
  ts = f.dropna(subset=["data_ajuizamento"]).copy()
212
  ts["mes"] = ts["data_ajuizamento"].dt.to_period("M").dt.to_timestamp()
213
  serie = ts.groupby("mes")["processo_numero"].nunique().reset_index(name="qtd")
214
- fig3 = px.line(serie, x="mes", y="qtd", markers=True, title="Processos por mês (Data de ajuizamento)")
215
- fig3.update_layout(margin=dict(l=10,r=10,b=10,t=50))
 
216
  st.plotly_chart(fig3, use_container_width=True)
217
 
218
- # Histograma Valor da Causa
219
  vc = f["valor_causa"].dropna()
220
  if len(vc) > 0:
221
- fig4 = px.histogram(f, x="valor_causa", nbins=30, title="Distribuição do Valor da Causa")
222
- fig4.update_layout(margin=dict(l=10,r=10,b=10,t=50))
 
223
  st.plotly_chart(fig4, use_container_width=True)
224
 
225
  st.markdown("<div class='section-title'>Tabela</div>", unsafe_allow_html=True)
226
 
227
- # ----------------- Tabela interativa -----------------
228
-
229
  if AG_AVAILABLE:
230
  gob = GridOptionsBuilder.from_dataframe(f)
231
  gob.configure_pagination(paginationAutoPageSize=False, paginationPageSize=20)
232
  gob.configure_side_bar()
233
  gob.configure_default_column(filter=True, sortable=True, resizable=True)
234
  gob.configure_selection("single")
235
- gob.configure_grid_options(domLayout='normal')
236
  grid_options = gob.build()
237
- grid = AgGrid(
238
  f,
239
  gridOptions=grid_options,
240
  update_mode=GridUpdateMode.MODEL_CHANGED,
241
- theme='alpine',
242
  height=420,
243
  fit_columns_on_grid_load=True,
244
  )
245
  else:
246
  st.dataframe(f, use_container_width=True)
247
 
248
- # ----------------- Export -----------------
249
-
250
  st.markdown("---")
251
-
252
  buff = io.StringIO()
253
- # exporta com ; como separador opcional? manter vírgula.
254
  f.to_csv(buff, index=False)
255
  st.download_button(
256
  label="⬇️ Baixar CSV filtrado",
 
1
  """
2
  LegalOne – PowerBI-like Dashboard (Streamlit)
3
+ ---------------------------------------------
4
+ Visual com cards de KPI, filtros na sidebar, gráficos Plotly e tabela interativa.
5
+ - Upload do CSV do Legal One
6
+ - Mapeamento (opcional) de "escritorio_responsavel" -> "escritorio_cat" via JSON
7
+ - Download do CSV filtrado
8
+
9
+ Requisitos (requirements.txt):
10
+ streamlit>=1.37
11
+ pandas>=2.2
12
+ plotly>=5.22
13
+ numpy>=1.26
14
+ streamlit-aggrid>=0.3.4.post3
15
  """
16
 
17
  from __future__ import annotations
 
19
  import json
20
  import re
21
  import unicodedata
22
+ from datetime import datetime
23
+ from typing import Optional
24
 
 
25
  import numpy as np
26
+ import pandas as pd
27
  import plotly.express as px
28
  import streamlit as st
 
29
 
30
+ # AgGrid é opcional
31
  try:
32
  from st_aggrid import AgGrid, GridOptionsBuilder, GridUpdateMode
33
  AG_AVAILABLE = True
34
  except Exception:
35
  AG_AVAILABLE = False
36
 
37
+ # ---------- Configuração de página e tema ----------
38
  st.set_page_config(page_title="LegalOne Dashboard", layout="wide")
39
+ px.defaults.template = "plotly_dark"
40
+ px.defaults.width = None
41
+ px.defaults.height = 420
42
 
43
+ PRIMARY_BG = "#0f172a" # slate-900
44
+ CARD_BG = "#111827" # gray-900
45
+ TEXT = "#e5e7eb" # gray-200
46
+ SUBTLE = "#94a3b8" # slate-400
 
 
 
 
47
 
48
+ st.markdown(f"""
49
  <style>
50
  html, body, [class^="css"], .stApp {{ background-color: {PRIMARY_BG} !important; }}
51
  .block-container {{ padding-top: 1rem; padding-bottom: 1rem; }}
 
55
  .section-title {{ color: {TEXT}; font-weight: 700; font-size: 1.2rem; margin: 12px 0 8px 0; }}
56
  hr {{ border: none; border-top: 1px solid #1f2937; margin: 8px 0 16px 0; }}
57
  </style>
58
+ """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
59
 
60
+ # ---------- Utilidades ----------
61
  def _norm_key(s: Optional[str]) -> str:
62
  if s is None:
63
  return ""
64
  s = str(s).strip().lower()
65
+ s = "".join(c for c in unicodedata.normalize("NFD", s) if unicodedata.category(c) != "Mn")
66
  s = re.sub(r"\s+", " ", s)
67
  return s
68
 
69
  @st.cache_data(show_spinner=False)
70
  def load_csv(upload) -> pd.DataFrame:
71
+ """
72
+ PASSO A: garante a coluna 'escritorio_cat' SEMPRE.
73
+ - se não existir no CSV, cria;
74
+ - se vier nula, preenche com 'escritorio_responsavel'.
75
+ """
76
  if upload is None:
77
  return pd.DataFrame()
78
+
79
  df = pd.read_csv(upload)
80
+
81
+ # colunas esperadas (inclui 'escritorio_cat')
82
  cols = [
83
  "processo_numero","cliente","contrario","valor_causa","acao","natureza","area","orgao",
84
  "comarca","tribunal","vara","situacao","data_ajuizamento","posicao_cliente",
 
87
  for c in cols:
88
  if c not in df.columns:
89
  df[c] = None
90
+
91
  # tipos
92
  df["valor_causa"] = pd.to_numeric(df["valor_causa"], errors="coerce")
93
  df["data_ajuizamento"] = pd.to_datetime(df["data_ajuizamento"], errors="coerce")
94
+
95
+ # garantia da categoria: se vazia, usa o responsável
96
+ df["escritorio_cat"] = (
97
+ df["escritorio_cat"]
98
+ .fillna(df["escritorio_responsavel"])
99
+ .astype(str)
100
+ .replace({"None": None})
101
+ )
102
+
103
  return df[cols]
104
 
105
  @st.cache_data(show_spinner=False)
106
  def apply_mapping(df: pd.DataFrame, mapping_json: str) -> pd.DataFrame:
107
+ """Aplica mapeamento JSON (responsável -> categoria)."""
108
+ if df.empty or not mapping_json:
109
  return df
110
  try:
111
  raw = json.loads(mapping_json)
112
  mapping = {_norm_key(k): v for k, v in raw.items()}
113
+ out = df.copy()
114
+ out["escritorio_cat"] = out["escritorio_responsavel"].apply(
115
+ lambda x: mapping.get(_norm_key(x), x)
116
+ )
117
+ return out
118
  except Exception:
119
  return df
120
 
 
143
  out = out[out["acao"].isin(acao)]
144
  if periodo and not pd.isna(out["data_ajuizamento"]).all():
145
  start, end = periodo
146
+ out = out[
147
+ (out["data_ajuizamento"] >= pd.to_datetime(start)) &
148
+ (out["data_ajuizamento"] <= pd.to_datetime(end))
149
+ ]
150
  return out
151
 
152
+ # ---------- Sidebar (upload + filtros) ----------
 
153
  st.sidebar.title("⚙️ Filtros")
154
+ up = st.sidebar.file_uploader("CSV do Legal One", type=["csv"])
155
+
156
  mapping_str = st.sidebar.text_area(
157
  "Mapeamento (JSON opcional) — escritório bruto → categoria",
158
  value='{"CÍVEL PARTIDO":"Cível – Partido","CÍVEL INDIVIDUAL":"Cível – Individual","CÍVEL RECUPERAÇÃO DE CRÉDITO":"Cível – Recuperação de Crédito"}',
 
166
  st.info("Faça upload do CSV para começar.")
167
  st.stop()
168
 
169
+ # valores únicos para filtros
170
  cats = sorted([c for c in base["escritorio_cat"].dropna().unique()])
171
  tribs = sorted([t for t in base["tribunal"].dropna().unique()])
172
  nats = sorted([n for n in base["natureza"].dropna().unique()])
 
184
  if pd.isna(min_dt) or pd.isna(max_dt):
185
  period = None
186
  else:
187
+ period = st.sidebar.date_input(
188
+ "Período (Data de ajuizamento)",
189
+ value=(min_dt.date(), max_dt.date())
190
+ )
191
 
192
  f = filter_df(base, cliente_q, sel_cat, sel_trib, sel_nat, sel_acao, period)
193
 
194
+ # ---------- KPIs ----------
 
195
  col1, col2, col3, col4 = st.columns(4)
196
  with col1:
197
+ st.markdown(
198
+ f'<div class="kpi-card"><div class="kpi-label">Processos</div>'
199
+ f'<div class="kpi-value">{int(f["processo_numero"].nunique())}</div></div>',
200
+ unsafe_allow_html=True,
201
+ )
202
  with col2:
203
+ st.markdown(
204
+ f'<div class="kpi-card"><div class="kpi-label">Clientes</div>'
205
+ f'<div class="kpi-value">{int(f["cliente"].nunique())}</div></div>',
206
+ unsafe_allow_html=True,
207
+ )
208
  with col3:
209
+ st.markdown(
210
+ f'<div class="kpi-card"><div class="kpi-label">Categorias de Escritório</div>'
211
+ f'<div class="kpi-value">{int(f["escritorio_cat"].nunique())}</div></div>',
212
+ unsafe_allow_html=True,
213
+ )
214
  with col4:
215
  total_valor = float(f["valor_causa"].fillna(0).sum())
216
+ st.markdown(
217
+ f'<div class="kpi-card"><div class="kpi-label">Soma Valor da Causa</div>'
218
+ f'<div class="kpi-value">R$ {total_valor:,.2f}</div></div>',
219
+ unsafe_allow_html=True,
220
+ )
221
 
222
  st.markdown("<div class='section-title'>Visão Geral</div>", unsafe_allow_html=True)
223
 
224
+ # ---------- Gráficos ----------
 
225
  gcol1, gcol2 = st.columns(2)
226
 
227
  with gcol1:
228
+ top_cat = (
229
+ f.groupby("escritorio_cat")["processo_numero"].nunique()
230
+ .sort_values(ascending=False).head(15).reset_index(name="qtd")
231
+ )
232
+ fig1 = px.bar(top_cat, x="escritorio_cat", y="qtd",
233
+ title="Processos por Categoria de Escritório (Top 15)")
234
+ fig1.update_layout(margin=dict(l=10, r=10, b=10, t=50))
235
  st.plotly_chart(fig1, use_container_width=True)
236
 
237
  with gcol2:
238
+ by_tri = (
239
+ f.groupby("tribunal")["processo_numero"].nunique()
240
+ .sort_values(ascending=False).reset_index(name="qtd")
241
+ )
242
  fig2 = px.bar(by_tri, x="tribunal", y="qtd", title="Processos por Tribunal")
243
+ fig2.update_layout(margin=dict(l=10, r=10, b=10, t=50))
244
  st.plotly_chart(fig2, use_container_width=True)
245
 
 
246
  if not f["data_ajuizamento"].isna().all():
247
  ts = f.dropna(subset=["data_ajuizamento"]).copy()
248
  ts["mes"] = ts["data_ajuizamento"].dt.to_period("M").dt.to_timestamp()
249
  serie = ts.groupby("mes")["processo_numero"].nunique().reset_index(name="qtd")
250
+ fig3 = px.line(serie, x="mes", y="qtd", markers=True,
251
+ title="Processos por mês (Data de ajuizamento)")
252
+ fig3.update_layout(margin=dict(l=10, r=10, b=10, t=50))
253
  st.plotly_chart(fig3, use_container_width=True)
254
 
 
255
  vc = f["valor_causa"].dropna()
256
  if len(vc) > 0:
257
+ fig4 = px.histogram(f, x="valor_causa", nbins=30,
258
+ title="Distribuição do Valor da Causa")
259
+ fig4.update_layout(margin=dict(l=10, r=10, b=10, t=50))
260
  st.plotly_chart(fig4, use_container_width=True)
261
 
262
  st.markdown("<div class='section-title'>Tabela</div>", unsafe_allow_html=True)
263
 
264
+ # ---------- Tabela ----------
 
265
  if AG_AVAILABLE:
266
  gob = GridOptionsBuilder.from_dataframe(f)
267
  gob.configure_pagination(paginationAutoPageSize=False, paginationPageSize=20)
268
  gob.configure_side_bar()
269
  gob.configure_default_column(filter=True, sortable=True, resizable=True)
270
  gob.configure_selection("single")
 
271
  grid_options = gob.build()
272
+ AgGrid(
273
  f,
274
  gridOptions=grid_options,
275
  update_mode=GridUpdateMode.MODEL_CHANGED,
276
+ theme="alpine",
277
  height=420,
278
  fit_columns_on_grid_load=True,
279
  )
280
  else:
281
  st.dataframe(f, use_container_width=True)
282
 
283
+ # ---------- Export ----------
 
284
  st.markdown("---")
 
285
  buff = io.StringIO()
 
286
  f.to_csv(buff, index=False)
287
  st.download_button(
288
  label="⬇️ Baixar CSV filtrado",