joaopimenta commited on
Commit
ca9d2b2
·
verified ·
1 Parent(s): 0e16edd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +297 -134
app.py CHANGED
@@ -6,224 +6,387 @@ import plotly.graph_objects as go
6
  import requests
7
  import os
8
  from datetime import datetime, timedelta
9
-
 
 
10
  # =========================================================
11
  # 0. CONFIGURAÇÕES
12
  # =========================================================
13
  GITHUB_OWNER = "joao862"
14
  GITHUB_REPO = "ascendum-dashboard"
15
  WORKFLOW_FILE = "atualizar_dados.yml"
16
-
 
 
 
 
17
  # =========================================================
18
- # 1. GITHUB TRIGGER
19
  # =========================================================
20
  def trigger_update_on_github():
 
21
  token = os.getenv("GH_PAT")
22
  if not token:
23
  return "⚠️ Erro: Secret 'GH_PAT' em falta."
24
-
25
  url = f"https://api.github.com/repos/{GITHUB_OWNER}/{GITHUB_REPO}/actions/workflows/{WORKFLOW_FILE}/dispatches"
26
- # SINAL + REMOVIDO ABAIXO (era application/vnd.github.v3+json)
27
- headers = {"Authorization": f"Bearer {token}", "Accept": "application/vnd.github.v3json"}
28
-
 
 
 
29
  try:
30
- requests.post(url, json={"ref": "main"}, headers=headers)
31
- return "✅ GitHub acionado! Aguarde 2 min."
 
 
 
 
 
32
  except Exception as e:
33
  return f"❌ Erro: {str(e)}"
34
-
35
  # =========================================================
36
- # 2. CARREGAMENTO DE DADOS (CORRIGIDO PARA LATIN-1)
37
  # =========================================================
38
- def load_data():
39
- file_path = "dados_vendas.csv"
40
- if not os.path.exists(file_path):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  return pd.DataFrame()
42
-
43
  try:
44
- # CORREÇÃO CRÍTICA PARA O ERRO 0xed:
45
- # encoding='latin-1': Resolve o problema dos acentos portugueses antigos
46
- # skiprows=2: Ignora as linhas de cabeçalho do Motordata
47
- df = pd.read_csv(file_path, sep=';', skiprows=2, on_bad_lines='skip', encoding='latin-1', engine='python')
 
 
 
 
 
48
  except Exception as e:
49
- print(f"Erro a ler CSV: {e}")
50
  return pd.DataFrame()
51
-
52
  # --- LIMPEZA DE COLUNAS ---
53
  df.columns = df.columns.str.strip()
54
-
55
- # Função para limpar o lixo do Excel (="valor")
56
  def clean(val):
 
 
57
  return str(val).replace('="', '').replace('"', '').strip()
58
-
59
- # Identificar colunas chave
60
- col_tipo = next((c for c in df.columns if 'tipo' in c.lower()), 'Tipo')
61
- col_kw = next((c for c in df.columns if 'potencia' in c.lower() and 'kw' in c.lower()), 'Potencia kW')
62
- col_marca = next((c for c in df.columns if 'marca' in c.lower()), 'Marca')
63
  col_data = next((c for c in df.columns if 'datamatricula' in c.lower() and 'emissao' in c.lower()), None)
64
- col_regiao = next((c for c in df.columns if 'dire' in c.lower() or 'distrito' in c.lower() or 'regi' in c.lower()), 'Direção Regional IMT')
65
-
66
- if col_tipo not in df.columns:
 
 
67
  return pd.DataFrame()
68
-
69
- # Aplicar Limpezas
 
 
 
 
70
  df['Tipo_Clean'] = df[col_tipo].apply(clean).str.upper()
71
-
72
- # Filtro: Apenas o que diz explicitamente "AGRICOLA"
73
  df = df[df['Tipo_Clean'] == 'AGRICOLA'].copy()
74
-
 
 
 
 
75
  # Tratar Datas
76
- if col_data:
77
- df['Data_dt'] = pd.to_datetime(df[col_data].apply(clean), errors='coerce')
78
- df = df.dropna(subset=['Data_dt'])
 
 
 
79
  else:
80
- return pd.DataFrame()
81
-
82
- # Tratar Potência, Marca e Região
83
- df["Potencia kW"] = pd.to_numeric(df[col_kw].apply(clean), errors='coerce').fillna(0)
84
- df['Marca'] = df[col_marca].apply(clean).str.upper()
85
- df['Regiao'] = df[col_regiao].apply(clean).str.title()
86
-
87
- # Segmentos
 
 
 
 
 
 
 
88
  bins = [0, 25, 50, 100, 500]
89
  labels = ['< 25 kW', '25 - 50 kW', '50 - 100 kW', '> 100 kW']
90
  df['Cluster Potencia'] = pd.cut(df['Potencia kW'], bins=bins, labels=labels).astype(str)
91
-
 
 
 
 
 
92
  return df
93
-
94
  # =========================================================
95
- # 3. LÓGICA DASHBOARD
96
  # =========================================================
97
  def update_dashboard(period_option, val_min, val_max, selected_regions):
 
 
 
 
 
 
98
  df = load_data()
99
-
 
100
  if df.empty:
101
- return "0", "0", "0", None, None, None, pd.DataFrame(), gr.update(), gr.update(), gr.update()
102
-
 
 
 
 
 
 
 
 
103
  # Limites Globais
104
- new_min = int(df["Potencia kW"].min())
105
- new_max = int(df["Potencia kW"].max())
106
  all_regs = sorted(df["Regiao"].unique().tolist())
107
-
 
108
  if not selected_regions:
109
  selected_regions = all_regs
110
-
111
  # Filtros de Data
112
  today = pd.Timestamp.now().normalize()
113
  if period_option == "YTD (Ano Atual)":
114
- start, end = pd.Timestamp(today.year, 1, 1), today
 
115
  elif period_option == "Último Mês (30 dias)":
116
- start, end = today - timedelta(days=30), today
 
117
  elif period_option == "Últimos 15 Dias":
118
- start, end = today - timedelta(days=15), today
119
- else:
120
- start, end = pd.Timestamp(today.year, 1, 1), today
121
-
122
- start_h, end_h = start - pd.DateOffset(years=1), end - pd.DateOffset(years=1)
123
-
 
 
 
 
124
  # Filtragem
125
- mask = (df["Potencia kW"] >= val_min) & (df["Potencia kW"] <= val_max) & (df["Regiao"].isin(selected_regions))
 
 
 
 
126
  df_curr = df[mask & (df["Data_dt"] >= start) & (df["Data_dt"] <= end)]
127
  df_prev = df[mask & (df["Data_dt"] >= start_h) & (df["Data_dt"] <= end_h)]
128
-
129
  # KPIs
130
  def calc_stats(d_now, d_old, marca=None):
131
- curr = len(d_now[d_now['Marca']==marca]) if marca else len(d_now)
132
- prev = len(d_old[d_old['Marca']==marca]) if marca else len(d_old)
133
-
134
- delta = ((curr - prev) / prev * 100) if prev > 0 else (100 if curr > 0 else 0)
135
- sym = "▲" if delta > 0 else "▼"
136
- if delta == 0: sym = "="
137
-
 
 
 
138
  return f"{curr} ({sym}{abs(delta):.0f}%)"
139
-
140
  def calc_share(d_now, marca):
141
- curr = len(d_now[d_now['Marca']==marca])
 
142
  total = len(d_now)
143
- share = (curr/total*100) if total > 0 else 0
144
  return f"{share:.1f}%"
145
-
146
  txt_total = calc_stats(df_curr, df_prev)
147
  txt_valtra = f"{calc_share(df_curr, 'VALTRA')} | {calc_stats(df_curr, df_prev, 'VALTRA')}"
148
  txt_kioti = f"{calc_share(df_curr, 'KIOTI')} | {calc_stats(df_curr, df_prev, 'KIOTI')}"
149
-
150
  # Gráficos
151
- fig_rank, fig_pie, fig_seg = None, None, None
152
-
 
 
153
  if not df_curr.empty:
154
- # Ranking
155
  top = df_curr['Marca'].value_counts().head(10).reset_index()
156
  top.columns = ['Marca', 'Vendas']
157
- colors = ['#d62728' if m=='VALTRA' else '#ff7f0e' if m=='KIOTI' else '#cccccc' for m in top['Marca']]
158
- fig_rank = go.Figure(go.Bar(x=top['Marca'], y=top['Vendas'], marker_color=colors, text=top['Vendas']))
159
- fig_rank.update_layout(title="Ranking de Vendas", margin=dict(l=10, r=10, t=30, b=10))
160
-
161
- # Pie Chart
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  pie_d = df_curr['Cluster Potencia'].value_counts().reset_index()
163
  pie_d.columns = ['Cluster', 'Count']
164
- fig_pie = px.pie(pie_d, values='Count', names='Cluster', title="Potência", hole=0.4)
165
- fig_pie.update_layout(margin=dict(l=10, r=10, t=30, b=10))
166
-
167
- # Segmento
 
 
 
 
 
 
168
  seg = df_curr[df_curr['Marca'].isin(['VALTRA', 'KIOTI'])]
169
  if not seg.empty:
170
- fig_seg = px.histogram(seg, x="Cluster Potencia", color="Marca", barmode="group",
171
- color_discrete_map={'VALTRA': '#d62728', 'KIOTI': '#ff7f0e'},
172
- title="Valtra vs Kioti")
173
- fig_seg.update_layout(margin=dict(l=10, r=10, t=30, b=10))
 
 
 
 
 
 
 
 
 
174
  else:
175
- fig_seg = go.Figure().add_annotation(text="Sem dados Valtra/Kioti", showarrow=False)
176
-
 
 
 
177
  # Tabela Final
178
  df_show = df_curr[['Data_dt', 'Marca', 'Tipo_Clean', 'Potencia kW', 'Regiao']].copy()
179
  if not df_show.empty:
180
  df_show['Data_dt'] = df_show['Data_dt'].dt.strftime('%d-%m-%Y')
181
-
182
- # CRITICAL FIX: Update sliders with value that's within new range
183
- return txt_total, txt_valtra, txt_kioti, fig_rank, fig_pie, fig_seg, df_show, \
184
- gr.update(minimum=new_min, maximum=new_max, value=new_min), \
185
- gr.update(minimum=new_min, maximum=new_max, value=new_max), \
186
- gr.update(choices=all_regs)
187
-
 
 
 
 
 
188
  # =========================================================
189
- # 4. INTERFACE
190
  # =========================================================
191
- with gr.Blocks(title="Dashboard", theme=gr.themes.Soft()) as demo:
192
- gr.Markdown("# 🚜 Dashboard: Valtra & KIOTI")
193
-
 
 
 
194
  with gr.Row():
195
  with gr.Column(scale=1):
196
- btn_git = gr.Button("🔄 Atualizar (GitHub)", variant="stop")
197
- status = gr.Text(label="Status")
198
-
199
- periodo = gr.Radio(["YTD (Ano Atual)", "Último Mês (30 dias)", "Últimos 15 Dias"], value="YTD (Ano Atual)", label="Período")
200
-
201
- s_min = gr.Slider(0, 500, value=0, step=1, label="Min kW")
202
- s_max = gr.Slider(0, 500, value=500, step=1, label="Max kW")
203
- regioes = gr.CheckboxGroup(choices=[], label="Regiões")
204
-
 
 
 
 
 
 
 
 
 
 
205
  btn_run = gr.Button("🔍 Aplicar Filtros", variant="primary")
206
-
207
  with gr.Column(scale=3):
 
208
  with gr.Row():
209
- k1, k2, k3 = gr.Text(label="Total"), gr.Text(label="Valtra"), gr.Text(label="Kioti")
210
- g1 = gr.Plot()
 
 
 
 
 
211
  with gr.Row():
212
- g2 = gr.Plot()
213
- g3 = gr.Plot()
214
-
215
- with gr.Accordion("Dados", open=False):
216
- tabela = gr.Dataframe()
217
-
 
 
 
 
218
  btn_git.click(trigger_update_on_github, outputs=status)
219
-
220
  io_dash = [periodo, s_min, s_max, regioes]
221
  out_dash = [k1, k2, k3, g1, g2, g3, tabela, s_min, s_max, regioes]
222
-
223
- btn_run.click(update_dashboard, io_dash, out_dash)
224
- periodo.change(update_dashboard, io_dash, out_dash)
225
-
226
- demo.load(update_dashboard, io_dash, out_dash)
227
-
 
228
  if __name__ == "__main__":
229
- demo.launch()
 
 
 
 
 
 
6
  import requests
7
  import os
8
  from datetime import datetime, timedelta
9
+ from functools import lru_cache
10
+ import time
11
+
12
  # =========================================================
13
  # 0. CONFIGURAÇÕES
14
  # =========================================================
15
  GITHUB_OWNER = "joao862"
16
  GITHUB_REPO = "ascendum-dashboard"
17
  WORKFLOW_FILE = "atualizar_dados.yml"
18
+ DATA_FILE = "dados_vendas.csv"
19
+
20
+ # Cache global para evitar recarregar dados constantemente
21
+ _cache = {"data": None, "timestamp": 0, "ttl": 30} # TTL de 30 segundos
22
+
23
  # =========================================================
24
+ # 1. GITHUB TRIGGER (CORRIGIDO)
25
  # =========================================================
26
  def trigger_update_on_github():
27
+ """Aciona workflow do GitHub para atualizar dados"""
28
  token = os.getenv("GH_PAT")
29
  if not token:
30
  return "⚠️ Erro: Secret 'GH_PAT' em falta."
31
+
32
  url = f"https://api.github.com/repos/{GITHUB_OWNER}/{GITHUB_REPO}/actions/workflows/{WORKFLOW_FILE}/dispatches"
33
+ # CORRIGIDO: Adicionado o + que estava em falta
34
+ headers = {
35
+ "Authorization": f"Bearer {token}",
36
+ "Accept": "application/vnd.github.v3+json"
37
+ }
38
+
39
  try:
40
+ response = requests.post(url, json={"ref": "main"}, headers=headers, timeout=10)
41
+ if response.status_code == 204:
42
+ return "✅ GitHub acionado! Aguarde 2-3 min para atualização."
43
+ else:
44
+ return f"⚠️ Resposta inesperada: {response.status_code}"
45
+ except requests.Timeout:
46
+ return "⏱️ Timeout: GitHub pode estar lento. Tente novamente."
47
  except Exception as e:
48
  return f"❌ Erro: {str(e)}"
49
+
50
  # =========================================================
51
+ # 2. CARREGAMENTO DE DADOS (COM CACHE)
52
  # =========================================================
53
+ def load_data(force_reload=False):
54
+ """
55
+ Carrega dados do CSV com cache inteligente.
56
+
57
+ Args:
58
+ force_reload: Forçar recarregar mesmo com cache válido
59
+ """
60
+ global _cache
61
+
62
+ # Verificar cache
63
+ now = time.time()
64
+ if not force_reload and _cache["data"] is not None:
65
+ if now - _cache["timestamp"] < _cache["ttl"]:
66
+ return _cache["data"]
67
+
68
+ # Verificar se ficheiro existe
69
+ if not os.path.exists(DATA_FILE):
70
+ print(f"⚠️ Ficheiro {DATA_FILE} não encontrado.")
71
  return pd.DataFrame()
72
+
73
  try:
74
+ # Ler CSV com encoding correto
75
+ df = pd.read_csv(
76
+ DATA_FILE,
77
+ sep=';',
78
+ skiprows=2,
79
+ on_bad_lines='skip',
80
+ encoding='latin-1',
81
+ engine='python'
82
+ )
83
  except Exception as e:
84
+ print(f"Erro a ler CSV: {e}")
85
  return pd.DataFrame()
86
+
87
  # --- LIMPEZA DE COLUNAS ---
88
  df.columns = df.columns.str.strip()
89
+
90
+ # Função para limpar formatação Excel (="valor")
91
  def clean(val):
92
+ if pd.isna(val):
93
+ return ""
94
  return str(val).replace('="', '').replace('"', '').strip()
95
+
96
+ # Identificar colunas chave (busca flexível)
97
+ col_tipo = next((c for c in df.columns if 'tipo' in c.lower()), None)
98
+ col_kw = next((c for c in df.columns if 'potencia' in c.lower() and 'kw' in c.lower()), None)
99
+ col_marca = next((c for c in df.columns if 'marca' in c.lower()), None)
100
  col_data = next((c for c in df.columns if 'datamatricula' in c.lower() and 'emissao' in c.lower()), None)
101
+ col_regiao = next((c for c in df.columns if 'dire' in c.lower() or 'distrito' in c.lower() or 'regi' in c.lower()), None)
102
+
103
+ # Validações críticas
104
+ if not col_tipo or col_tipo not in df.columns:
105
+ print("⚠️ Coluna 'Tipo' não encontrada.")
106
  return pd.DataFrame()
107
+
108
+ if not col_data:
109
+ print("⚠️ Coluna de data não encontrada.")
110
+ return pd.DataFrame()
111
+
112
+ # Aplicar limpezas
113
  df['Tipo_Clean'] = df[col_tipo].apply(clean).str.upper()
114
+
115
+ # Filtro: Apenas AGRICOLA
116
  df = df[df['Tipo_Clean'] == 'AGRICOLA'].copy()
117
+
118
+ if df.empty:
119
+ print("⚠️ Nenhum registo AGRICOLA encontrado após filtro.")
120
+ return pd.DataFrame()
121
+
122
  # Tratar Datas
123
+ df['Data_dt'] = pd.to_datetime(df[col_data].apply(clean), errors='coerce')
124
+ df = df.dropna(subset=['Data_dt'])
125
+
126
+ # Tratar Potência
127
+ if col_kw:
128
+ df["Potencia kW"] = pd.to_numeric(df[col_kw].apply(clean), errors='coerce').fillna(0)
129
  else:
130
+ df["Potencia kW"] = 0
131
+
132
+ # Tratar Marca
133
+ if col_marca:
134
+ df['Marca'] = df[col_marca].apply(clean).str.upper()
135
+ else:
136
+ df['Marca'] = 'DESCONHECIDA'
137
+
138
+ # Tratar Região
139
+ if col_regiao:
140
+ df['Regiao'] = df[col_regiao].apply(clean).str.title()
141
+ else:
142
+ df['Regiao'] = 'Sem Região'
143
+
144
+ # Segmentos de Potência
145
  bins = [0, 25, 50, 100, 500]
146
  labels = ['< 25 kW', '25 - 50 kW', '50 - 100 kW', '> 100 kW']
147
  df['Cluster Potencia'] = pd.cut(df['Potencia kW'], bins=bins, labels=labels).astype(str)
148
+
149
+ # Atualizar cache
150
+ _cache["data"] = df
151
+ _cache["timestamp"] = now
152
+
153
+ print(f"✅ Dados carregados: {len(df)} registos")
154
  return df
155
+
156
  # =========================================================
157
+ # 3. LÓGICA DASHBOARD (MELHORADA)
158
  # =========================================================
159
  def update_dashboard(period_option, val_min, val_max, selected_regions):
160
+ """Atualiza todos os componentes do dashboard"""
161
+
162
+ # Validação de sliders
163
+ if val_min > val_max:
164
+ val_min, val_max = val_max, val_min # Swap automático
165
+
166
  df = load_data()
167
+
168
+ # Se não há dados, retornar estado vazio
169
  if df.empty:
170
+ empty_fig = go.Figure().add_annotation(text="Sem dados disponíveis", showarrow=False)
171
+ return (
172
+ "0", "0", "0",
173
+ empty_fig, empty_fig, empty_fig,
174
+ pd.DataFrame(),
175
+ gr.update(minimum=0, maximum=500, value=0),
176
+ gr.update(minimum=0, maximum=500, value=500),
177
+ gr.update(choices=[])
178
+ )
179
+
180
  # Limites Globais
181
+ new_min = max(0, int(df["Potencia kW"].min()))
182
+ new_max = min(500, int(df["Potencia kW"].max()))
183
  all_regs = sorted(df["Regiao"].unique().tolist())
184
+
185
+ # Se nenhuma região selecionada, usar todas
186
  if not selected_regions:
187
  selected_regions = all_regs
188
+
189
  # Filtros de Data
190
  today = pd.Timestamp.now().normalize()
191
  if period_option == "YTD (Ano Atual)":
192
+ start = pd.Timestamp(today.year, 1, 1)
193
+ end = today
194
  elif period_option == "Último Mês (30 dias)":
195
+ start = today - timedelta(days=30)
196
+ end = today
197
  elif period_option == "Últimos 15 Dias":
198
+ start = today - timedelta(days=15)
199
+ end = today
200
+ else: # Fallback
201
+ start = pd.Timestamp(today.year, 1, 1)
202
+ end = today
203
+
204
+ # Período homólogo (ano anterior)
205
+ start_h = start - pd.DateOffset(years=1)
206
+ end_h = end - pd.DateOffset(years=1)
207
+
208
  # Filtragem
209
+ mask = (
210
+ (df["Potencia kW"] >= val_min) &
211
+ (df["Potencia kW"] <= val_max) &
212
+ (df["Regiao"].isin(selected_regions))
213
+ )
214
  df_curr = df[mask & (df["Data_dt"] >= start) & (df["Data_dt"] <= end)]
215
  df_prev = df[mask & (df["Data_dt"] >= start_h) & (df["Data_dt"] <= end_h)]
216
+
217
  # KPIs
218
  def calc_stats(d_now, d_old, marca=None):
219
+ """Calcula estatísticas com variação vs período homólogo"""
220
+ curr = len(d_now[d_now['Marca'] == marca]) if marca else len(d_now)
221
+ prev = len(d_old[d_old['Marca'] == marca]) if marca else len(d_old)
222
+
223
+ if prev > 0:
224
+ delta = ((curr - prev) / prev * 100)
225
+ else:
226
+ delta = 100 if curr > 0 else 0
227
+
228
+ sym = "▲" if delta > 0 else "▼" if delta < 0 else "="
229
  return f"{curr} ({sym}{abs(delta):.0f}%)"
230
+
231
  def calc_share(d_now, marca):
232
+ """Calcula quota de mercado"""
233
+ curr = len(d_now[d_now['Marca'] == marca])
234
  total = len(d_now)
235
+ share = (curr / total * 100) if total > 0 else 0
236
  return f"{share:.1f}%"
237
+
238
  txt_total = calc_stats(df_curr, df_prev)
239
  txt_valtra = f"{calc_share(df_curr, 'VALTRA')} | {calc_stats(df_curr, df_prev, 'VALTRA')}"
240
  txt_kioti = f"{calc_share(df_curr, 'KIOTI')} | {calc_stats(df_curr, df_prev, 'KIOTI')}"
241
+
242
  # Gráficos
243
+ fig_rank = go.Figure().add_annotation(text="Sem dados", showarrow=False)
244
+ fig_pie = go.Figure().add_annotation(text="Sem dados", showarrow=False)
245
+ fig_seg = go.Figure().add_annotation(text="Sem dados", showarrow=False)
246
+
247
  if not df_curr.empty:
248
+ # 1. Ranking Top 10
249
  top = df_curr['Marca'].value_counts().head(10).reset_index()
250
  top.columns = ['Marca', 'Vendas']
251
+ colors = [
252
+ '#d62728' if m == 'VALTRA' else
253
+ '#ff7f0e' if m == 'KIOTI' else
254
+ '#1f77b4'
255
+ for m in top['Marca']
256
+ ]
257
+ fig_rank = go.Figure(go.Bar(
258
+ x=top['Marca'],
259
+ y=top['Vendas'],
260
+ marker_color=colors,
261
+ text=top['Vendas'],
262
+ textposition='outside'
263
+ ))
264
+ fig_rank.update_layout(
265
+ title="📊 Ranking de Vendas (Top 10)",
266
+ xaxis_title="Marca",
267
+ yaxis_title="Nº Vendas",
268
+ margin=dict(l=10, r=10, t=40, b=10),
269
+ showlegend=False
270
+ )
271
+
272
+ # 2. Pie Chart - Distribuição por Potência
273
  pie_d = df_curr['Cluster Potencia'].value_counts().reset_index()
274
  pie_d.columns = ['Cluster', 'Count']
275
+ fig_pie = px.pie(
276
+ pie_d,
277
+ values='Count',
278
+ names='Cluster',
279
+ title="⚡ Distribuição por Potência",
280
+ hole=0.4
281
+ )
282
+ fig_pie.update_layout(margin=dict(l=10, r=10, t=40, b=10))
283
+
284
+ # 3. Comparação Valtra vs Kioti
285
  seg = df_curr[df_curr['Marca'].isin(['VALTRA', 'KIOTI'])]
286
  if not seg.empty:
287
+ fig_seg = px.histogram(
288
+ seg,
289
+ x="Cluster Potencia",
290
+ color="Marca",
291
+ barmode="group",
292
+ color_discrete_map={'VALTRA': '#d62728', 'KIOTI': '#ff7f0e'},
293
+ title="🆚 Valtra vs Kioti por Segmento"
294
+ )
295
+ fig_seg.update_layout(
296
+ xaxis_title="Segmento de Potência",
297
+ yaxis_title="Nº Vendas",
298
+ margin=dict(l=10, r=10, t=40, b=10)
299
+ )
300
  else:
301
+ fig_seg = go.Figure().add_annotation(
302
+ text="Sem dados Valtra/Kioti no período selecionado",
303
+ showarrow=False
304
+ )
305
+
306
  # Tabela Final
307
  df_show = df_curr[['Data_dt', 'Marca', 'Tipo_Clean', 'Potencia kW', 'Regiao']].copy()
308
  if not df_show.empty:
309
  df_show['Data_dt'] = df_show['Data_dt'].dt.strftime('%d-%m-%Y')
310
+ df_show = df_show.sort_values('Data_dt', ascending=False) # Mais recentes primeiro
311
+
312
+ # Retornar com sliders ajustados
313
+ return (
314
+ txt_total, txt_valtra, txt_kioti,
315
+ fig_rank, fig_pie, fig_seg,
316
+ df_show,
317
+ gr.update(minimum=new_min, maximum=new_max, value=max(new_min, val_min)),
318
+ gr.update(minimum=new_min, maximum=new_max, value=min(new_max, val_max)),
319
+ gr.update(choices=all_regs, value=selected_regions) # Preservar seleção
320
+ )
321
+
322
  # =========================================================
323
+ # 4. INTERFACE (MELHORADA)
324
  # =========================================================
325
+ with gr.Blocks(title="Dashboard Valtra & Kioti", theme=gr.themes.Soft()) as demo:
326
+ gr.Markdown("""
327
+ # 🚜 Dashboard de Vendas: Valtra & KIOTI
328
+ **Análise de matrículas de tratores agrícolas em Portugal**
329
+ """)
330
+
331
  with gr.Row():
332
  with gr.Column(scale=1):
333
+ gr.Markdown("### ⚙️ Controlos")
334
+
335
+ btn_git = gr.Button("🔄 Atualizar Dados (GitHub)", variant="secondary", size="sm")
336
+ status = gr.Textbox(label="Status", interactive=False, lines=2)
337
+
338
+ gr.Markdown("---")
339
+
340
+ periodo = gr.Radio(
341
+ ["YTD (Ano Atual)", "Último Mês (30 dias)", "Últimos 15 Dias"],
342
+ value="YTD (Ano Atual)",
343
+ label="📅 Período"
344
+ )
345
+
346
+ gr.Markdown("**Filtro de Potência (kW)**")
347
+ s_min = gr.Slider(0, 500, value=0, step=1, label="Mínimo")
348
+ s_max = gr.Slider(0, 500, value=500, step=1, label="Máximo")
349
+
350
+ regioes = gr.CheckboxGroup(choices=[], label="🗺️ Regiões", value=[])
351
+
352
  btn_run = gr.Button("🔍 Aplicar Filtros", variant="primary")
353
+
354
  with gr.Column(scale=3):
355
+ gr.Markdown("### 📈 Indicadores Chave")
356
  with gr.Row():
357
+ k1 = gr.Textbox(label="📊 Total Mercado", interactive=False)
358
+ k2 = gr.Textbox(label="🔴 Valtra (Quota | Vendas)", interactive=False)
359
+ k3 = gr.Textbox(label="🟠 Kioti (Quota | Vendas)", interactive=False)
360
+
361
+ gr.Markdown("### 📊 Análises")
362
+ g1 = gr.Plot(label="Ranking")
363
+
364
  with gr.Row():
365
+ g2 = gr.Plot(label="Distribuição")
366
+ g3 = gr.Plot(label="Comparação")
367
+
368
+ with gr.Accordion("📋 Dados Detalhados", open=False):
369
+ tabela = gr.Dataframe(
370
+ headers=["Data", "Marca", "Tipo", "Potência kW", "Região"],
371
+ interactive=False
372
+ )
373
+
374
+ # --- EVENTOS ---
375
  btn_git.click(trigger_update_on_github, outputs=status)
376
+
377
  io_dash = [periodo, s_min, s_max, regioes]
378
  out_dash = [k1, k2, k3, g1, g2, g3, tabela, s_min, s_max, regioes]
379
+
380
+ btn_run.click(update_dashboard, inputs=io_dash, outputs=out_dash)
381
+ periodo.change(update_dashboard, inputs=io_dash, outputs=out_dash)
382
+
383
+ # Carregamento inicial
384
+ demo.load(update_dashboard, inputs=io_dash, outputs=out_dash)
385
+
386
  if __name__ == "__main__":
387
+ demo.launch(
388
+ server_name="0.0.0.0",
389
+ server_port=7860,
390
+ share=False,
391
+ show_error=True
392
+ )