Guilherme Silberfarb Costa commited on
Commit
d0f2c73
·
1 Parent(s): 1c93e8e

alteracoes generalizadas

Browse files
backend/app/api/elaboracao.py CHANGED
@@ -80,6 +80,11 @@ class DispersaoPayload(SessionPayload):
80
  tipo: str
81
 
82
 
 
 
 
 
 
83
  class OutlierFiltro(BaseModel):
84
  variavel: str
85
  operador: str
@@ -268,6 +273,16 @@ def model_dispersao(payload: DispersaoPayload) -> dict[str, Any]:
268
  return elaboracao_service.gerar_grafico_dispersao_modelo(session, payload.tipo)
269
 
270
 
 
 
 
 
 
 
 
 
 
 
271
  @router.post("/outliers/apply-filters")
272
  def outliers_apply_filters(payload: OutlierFilterPayload) -> dict[str, Any]:
273
  session = session_store.get(payload.session_id)
@@ -275,6 +290,13 @@ def outliers_apply_filters(payload: OutlierFilterPayload) -> dict[str, Any]:
275
  return elaboracao_service.apply_outlier_filters(session, filtros)
276
 
277
 
 
 
 
 
 
 
 
278
  @router.post("/outliers/restart")
279
  def outliers_restart(payload: OutlierRestartPayload) -> dict[str, Any]:
280
  session = session_store.get(payload.session_id)
 
80
  tipo: str
81
 
82
 
83
+ class TransformPreviewPayload(SessionPayload):
84
+ transformacao_y: str = "(x)"
85
+ transformacoes_x: dict[str, str] = Field(default_factory=dict)
86
+
87
+
88
  class OutlierFiltro(BaseModel):
89
  variavel: str
90
  operador: str
 
273
  return elaboracao_service.gerar_grafico_dispersao_modelo(session, payload.tipo)
274
 
275
 
276
+ @router.post("/transform-preview")
277
+ def transform_preview(payload: TransformPreviewPayload) -> dict[str, Any]:
278
+ session = session_store.get(payload.session_id)
279
+ return elaboracao_service.preview_transformacao_manual(
280
+ session,
281
+ transformacao_y=payload.transformacao_y,
282
+ transformacoes_x=payload.transformacoes_x,
283
+ )
284
+
285
+
286
  @router.post("/outliers/apply-filters")
287
  def outliers_apply_filters(payload: OutlierFilterPayload) -> dict[str, Any]:
288
  session = session_store.get(payload.session_id)
 
290
  return elaboracao_service.apply_outlier_filters(session, filtros)
291
 
292
 
293
+ @router.post("/outliers/apply-filters-recursive")
294
+ def outliers_apply_filters_recursive(payload: OutlierFilterPayload) -> dict[str, Any]:
295
+ session = session_store.get(payload.session_id)
296
+ filtros = [item.model_dump() for item in payload.filtros]
297
+ return elaboracao_service.apply_outlier_filters_recursive(session, filtros)
298
+
299
+
300
  @router.post("/outliers/restart")
301
  def outliers_restart(payload: OutlierRestartPayload) -> dict[str, Any]:
302
  session = session_store.get(payload.session_id)
backend/app/core/elaboracao/app.py CHANGED
@@ -760,7 +760,7 @@ def criar_aba():
760
  )
761
  with gr.Row():
762
  checkboxes_codigo_alocado = gr.CheckboxGroup(
763
- label="Variáveis de Código Alocado/Ajustado",
764
  choices=[], value=[], visible=False, interactive=True,
765
  info="Transformação livre. Avaliação: apenas inteiros no intervalo observado."
766
  )
 
760
  )
761
  with gr.Row():
762
  checkboxes_codigo_alocado = gr.CheckboxGroup(
763
+ label="Variáveis Categóricas Codificadas",
764
  choices=[], value=[], visible=False, interactive=True,
765
  info="Transformação livre. Avaliação: apenas inteiros no intervalo observado."
766
  )
backend/app/core/elaboracao/carregamento.py CHANGED
@@ -364,7 +364,7 @@ def carregar_dados_de_dai(caminho_arquivo):
364
  dropdown_offset = 3 + n_rows + MAX_VARS_X + MAX_VARS_X # 3 + 3 + 20 + 20 = 46
365
  for i, col in enumerate(colunas_x):
366
  if i < MAX_VARS_X:
367
- # Dicotômicas e percentuais: travar. Código alocado: livre.
368
  travar = col in dicotomicas or col in percentuais
369
  campos_transf[dropdown_offset + i] = gr.update(
370
  value=transformacoes_x.get(col, "(x)"),
 
364
  dropdown_offset = 3 + n_rows + MAX_VARS_X + MAX_VARS_X # 3 + 3 + 20 + 20 = 46
365
  for i, col in enumerate(colunas_x):
366
  if i < MAX_VARS_X:
367
+ # Dicotômicas e percentuais: travar. Variáveis categóricas codificadas: livres.
368
  travar = col in dicotomicas or col in percentuais
369
  campos_transf[dropdown_offset + i] = gr.update(
370
  value=transformacoes_x.get(col, "(x)"),
backend/app/core/elaboracao/core.py CHANGED
@@ -278,7 +278,7 @@ def carregar_dai(caminho):
278
  colunas_x.append(nome_x)
279
  transformacoes_x[nome_x] = transf_x.strip()
280
 
281
- # Extrai dicotômicas, código alocado e percentuais (3 listas separadas)
282
  dicotomicas = pacote["transformacoes"].get("dicotomicas", None)
283
  codigo_alocado_salvo = pacote["transformacoes"].get("codigo_alocado", None)
284
  percentuais_salvo = pacote["transformacoes"].get("percentuais", None)
@@ -672,10 +672,10 @@ def detectar_dicotomicas(df, colunas):
672
 
673
  def detectar_codigo_alocado(df, colunas):
674
  """
675
- Detecta variáveis de código alocado/ajustado.
676
  Critérios: todos os valores são inteiros, ≥3 valores distintos, nenhum zero.
677
  Exclui variáveis já detectadas como dicotômicas (0/1).
678
- Retorna lista de nomes de colunas de código alocado.
679
  """
680
  resultado = []
681
  for col in colunas:
@@ -1505,7 +1505,7 @@ def buscar_melhores_transformacoes(df, coluna_y, colunas_x, transformacoes_fixas
1505
  if indices_usar is not None:
1506
  df_busca = df_busca.loc[indices_usar]
1507
 
1508
- # Detecta dicotômicas e percentuais e fixa em (x) (código alocado fica livre)
1509
  dicotomicas = detectar_dicotomicas(df_busca, colunas_x)
1510
  percentuais = detectar_percentuais(df_busca, colunas_x)
1511
  for col in dicotomicas + percentuais:
@@ -2083,7 +2083,7 @@ def avaliar_imovel(modelo_sm, valores_x, colunas_x, transformacoes_x, transforma
2083
  transformacao_y: str com a transformação aplicada em Y.
2084
  estatisticas_df: DataFrame com colunas Variável, Mínimo, Máximo (entre outras).
2085
  dicotomicas: list de nomes de colunas dicotômicas (0/1).
2086
- codigo_alocado: list de nomes de colunas de código alocado/ajustado.
2087
  percentuais: list de nomes de colunas percentuais (0 a 1).
2088
 
2089
  Returns:
@@ -2125,7 +2125,7 @@ def avaliar_imovel(modelo_sm, valores_x, colunas_x, transformacoes_x, transforma
2125
  min_val = float(limites.loc[col, "Mínimo"])
2126
  max_val = float(limites.loc[col, "Máximo"])
2127
 
2128
- # Dicotômica, código alocado ou percentual — validação já feita no callback
2129
  if col in (dicotomicas or []):
2130
  extrapolacoes[col] = {"status": "dicotomica", "percentual": 0.0, "direcao": "ok"}
2131
  continue
 
278
  colunas_x.append(nome_x)
279
  transformacoes_x[nome_x] = transf_x.strip()
280
 
281
+ # Extrai dicotômicas, variáveis categóricas codificadas e percentuais (3 listas separadas)
282
  dicotomicas = pacote["transformacoes"].get("dicotomicas", None)
283
  codigo_alocado_salvo = pacote["transformacoes"].get("codigo_alocado", None)
284
  percentuais_salvo = pacote["transformacoes"].get("percentuais", None)
 
672
 
673
  def detectar_codigo_alocado(df, colunas):
674
  """
675
+ Detecta variáveis categóricas codificadas.
676
  Critérios: todos os valores são inteiros, ≥3 valores distintos, nenhum zero.
677
  Exclui variáveis já detectadas como dicotômicas (0/1).
678
+ Retorna lista de nomes de colunas categóricas codificadas.
679
  """
680
  resultado = []
681
  for col in colunas:
 
1505
  if indices_usar is not None:
1506
  df_busca = df_busca.loc[indices_usar]
1507
 
1508
+ # Detecta dicotômicas e percentuais e fixa em (x) (variáveis categóricas codificadas ficam livres)
1509
  dicotomicas = detectar_dicotomicas(df_busca, colunas_x)
1510
  percentuais = detectar_percentuais(df_busca, colunas_x)
1511
  for col in dicotomicas + percentuais:
 
2083
  transformacao_y: str com a transformação aplicada em Y.
2084
  estatisticas_df: DataFrame com colunas Variável, Mínimo, Máximo (entre outras).
2085
  dicotomicas: list de nomes de colunas dicotômicas (0/1).
2086
+ codigo_alocado: list de nomes de colunas categóricas codificadas.
2087
  percentuais: list de nomes de colunas percentuais (0 a 1).
2088
 
2089
  Returns:
 
2125
  min_val = float(limites.loc[col, "Mínimo"])
2126
  max_val = float(limites.loc[col, "Máximo"])
2127
 
2128
+ # Dicotômica, categórica codificada ou percentual — validação já feita no callback
2129
  if col in (dicotomicas or []):
2130
  extrapolacoes[col] = {"status": "dicotomica", "percentual": 0.0, "direcao": "ok"}
2131
  continue
backend/app/core/elaboracao/formatadores.py CHANGED
@@ -281,17 +281,17 @@ def formatar_micronumerosidade_html(resultado):
281
  '''
282
 
283
  # =========================
284
- # Seções separadas: Dicotômicas e Códigos Alocados
285
  # =========================
286
  dic_html = _renderizar_secao_micro("Variáveis Dicotômicas", resultado.get("dicotomicas", {}), secao_tipo="dicotomicas")
287
- cod_html = _renderizar_secao_micro("Códigos Ajustados/Alocados", resultado.get("codigo_alocado", {}), secao_tipo="codigo_alocado")
288
 
289
  if dic_html or cod_html:
290
  html += dic_html + cod_html
291
  else:
292
  html += '''
293
  <p style="color: #6c757d; font-style: italic; margin-top: 8px;">
294
- Nenhuma variável dicotômica ou de código alocado selecionada.
295
  </p>
296
  '''
297
 
 
281
  '''
282
 
283
  # =========================
284
+ # Seções separadas: Dicotômicas e Variáveis Categóricas Codificadas
285
  # =========================
286
  dic_html = _renderizar_secao_micro("Variáveis Dicotômicas", resultado.get("dicotomicas", {}), secao_tipo="dicotomicas")
287
+ cod_html = _renderizar_secao_micro("Variáveis Categóricas Codificadas", resultado.get("codigo_alocado", {}), secao_tipo="codigo_alocado")
288
 
289
  if dic_html or cod_html:
290
  html += dic_html + cod_html
291
  else:
292
  html += '''
293
  <p style="color: #6c757d; font-style: italic; margin-top: 8px;">
294
+ Nenhuma variável dicotômica ou categórica codificada selecionada.
295
  </p>
296
  '''
297
 
backend/app/core/elaboracao/modelo.py CHANGED
@@ -465,7 +465,7 @@ def buscar_transformacoes_callback(df, coluna_y, colunas_x, dicotomicas=None, co
465
  msg = f"<p style='color: red;'><b>Erro:</b> A variável dependente <b>{coluna_y}</b> está completamente vazia.</p>"
466
  return (msg, [], "", *btn_hidden)
467
 
468
- # Fixa dicotômicas e percentuais em (x) — código alocado fica livre
469
  transformacoes_fixas = {}
470
  if dicotomicas is None:
471
  dicotomicas = detectar_dicotomicas(df, colunas_x)
@@ -787,7 +787,7 @@ def avaliar_imovel_callback(estado_modelo, tabela_estatisticas, estado_avaliacoe
787
  else:
788
  return _err("Estatísticas não disponíveis.")
789
 
790
- # Validar variáveis dicotômicas, código alocado e percentuais ANTES de avaliar
791
  dicotomicas = estado_modelo.get("dicotomicas", [])
792
  codigo_alocado = estado_modelo.get("codigo_alocado", [])
793
  percentuais = estado_modelo.get("percentuais", [])
@@ -808,7 +808,7 @@ def avaliar_imovel_callback(estado_modelo, tabela_estatisticas, estado_avaliacoe
808
  max_val = float(est_idx.loc[col, "Máximo"])
809
  eh_inteiro = (float(val) == int(float(val)))
810
  if not eh_inteiro or val < min_val or val > max_val:
811
- return _err(f"<p style='color:red;'><b>Erro:</b> A variável <b>{col}</b> é de código alocado/ajustado e aceita apenas "
812
  f"valores inteiros de {int(min_val)} a {int(max_val)}. Valor informado: {val}</p>")
813
  elif col in percentuais:
814
  if val < 0 or val > 1:
@@ -937,7 +937,7 @@ def atualizar_interativo_dicotomicas(colunas_x, dicotomicas, codigo_alocado, per
937
  """Atualiza interactive dos dropdowns de transformação conforme os 3 tipos.
938
 
939
  Dicotômicas e percentuais: transformação travada em (x).
940
- Código alocado: transformação livre.
941
 
942
  Handler para checkboxes_dicotomicas.change(), checkboxes_codigo_alocado.change(),
943
  checkboxes_percentuais.change().
@@ -959,7 +959,7 @@ def atualizar_interativo_dicotomicas(colunas_x, dicotomicas, codigo_alocado, per
959
 
960
 
961
  def popular_dicotomicas_callback(estado_modelo, colunas_x, estado_df):
962
- """Popula os 3 checkboxes: dicotômicas, código alocado e percentuais.
963
 
964
  Para .dai: usa listas salvas no modelo.
965
  Para CSV/Excel: auto-detecta os 3 tipos.
 
465
  msg = f"<p style='color: red;'><b>Erro:</b> A variável dependente <b>{coluna_y}</b> está completamente vazia.</p>"
466
  return (msg, [], "", *btn_hidden)
467
 
468
+ # Fixa dicotômicas e percentuais em (x) — variáveis categóricas codificadas ficam livres
469
  transformacoes_fixas = {}
470
  if dicotomicas is None:
471
  dicotomicas = detectar_dicotomicas(df, colunas_x)
 
787
  else:
788
  return _err("Estatísticas não disponíveis.")
789
 
790
+ # Validar variáveis dicotômicas, categóricas codificadas e percentuais ANTES de avaliar
791
  dicotomicas = estado_modelo.get("dicotomicas", [])
792
  codigo_alocado = estado_modelo.get("codigo_alocado", [])
793
  percentuais = estado_modelo.get("percentuais", [])
 
808
  max_val = float(est_idx.loc[col, "Máximo"])
809
  eh_inteiro = (float(val) == int(float(val)))
810
  if not eh_inteiro or val < min_val or val > max_val:
811
+ return _err(f"<p style='color:red;'><b>Erro:</b> A variável <b>{col}</b> é categórica codificada e aceita apenas "
812
  f"valores inteiros de {int(min_val)} a {int(max_val)}. Valor informado: {val}</p>")
813
  elif col in percentuais:
814
  if val < 0 or val > 1:
 
937
  """Atualiza interactive dos dropdowns de transformação conforme os 3 tipos.
938
 
939
  Dicotômicas e percentuais: transformação travada em (x).
940
+ Variáveis categóricas codificadas: transformação livre.
941
 
942
  Handler para checkboxes_dicotomicas.change(), checkboxes_codigo_alocado.change(),
943
  checkboxes_percentuais.change().
 
959
 
960
 
961
  def popular_dicotomicas_callback(estado_modelo, colunas_x, estado_df):
962
+ """Popula os 3 checkboxes: dicotômicas, variáveis categóricas codificadas e percentuais.
963
 
964
  Para .dai: usa listas salvas no modelo.
965
  Para CSV/Excel: auto-detecta os 3 tipos.
backend/app/core/visualizacao/app.py CHANGED
@@ -1121,7 +1121,7 @@ def calcular_avaliacao_viz(pacote, estado_avaliacoes, indice_base_str, *aval_inp
1121
  return _err("Preencha todos os campos.")
1122
  valores_x[col] = float(aval_inputs[i])
1123
 
1124
- # Validar variáveis dicotômicas, código alocado e percentuais ANTES de avaliar
1125
  dicotomicas = pacote["transformacoes"].get("dicotomicas", [])
1126
  codigo_alocado = pacote["transformacoes"].get("codigo_alocado", [])
1127
  percentuais = pacote["transformacoes"].get("percentuais", [])
@@ -1146,7 +1146,7 @@ def calcular_avaliacao_viz(pacote, estado_avaliacoes, indice_base_str, *aval_inp
1146
  max_val = float(est_idx.loc[col, "Máximo"])
1147
  eh_inteiro = (float(val) == int(float(val)))
1148
  if not eh_inteiro or val < min_val or val > max_val:
1149
- return _err(f"<p style='color:red;'><b>Erro:</b> A variável <b>{col}</b> é de código alocado/ajustado e aceita apenas "
1150
  f"valores inteiros de {int(min_val)} a {int(max_val)}. Valor informado: {val}</p>")
1151
  elif col in (percentuais or []):
1152
  if val < 0 or val > 1:
 
1121
  return _err("Preencha todos os campos.")
1122
  valores_x[col] = float(aval_inputs[i])
1123
 
1124
+ # Validar variáveis dicotômicas, categóricas codificadas e percentuais ANTES de avaliar
1125
  dicotomicas = pacote["transformacoes"].get("dicotomicas", [])
1126
  codigo_alocado = pacote["transformacoes"].get("codigo_alocado", [])
1127
  percentuais = pacote["transformacoes"].get("percentuais", [])
 
1146
  max_val = float(est_idx.loc[col, "Máximo"])
1147
  eh_inteiro = (float(val) == int(float(val)))
1148
  if not eh_inteiro or val < min_val or val > max_val:
1149
+ return _err(f"<p style='color:red;'><b>Erro:</b> A variável <b>{col}</b> é categórica codificada e aceita apenas "
1150
  f"valores inteiros de {int(min_val)} a {int(max_val)}. Valor informado: {val}</p>")
1151
  elif col in (percentuais or []):
1152
  if val < 0 or val > 1:
backend/app/services/elaboracao_service.py CHANGED
@@ -91,6 +91,105 @@ def _parse_indices_text(text: str | None) -> list[int]:
91
  return sorted(set(out))
92
 
93
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
94
  def _selection_context(session: SessionState) -> dict[str, Any]:
95
  return {
96
  "coluna_y": session.coluna_y,
@@ -457,7 +556,7 @@ def load_uploaded_file(session: SessionState, selected_sheet: str | None = None)
457
  "status": msg_abas,
458
  "requires_sheet": True,
459
  "sheets": abas,
460
- "sheet_selected": abas[0],
461
  }
462
 
463
  df, msg, sucesso = carregar_arquivo(caminho, selected_sheet)
@@ -465,10 +564,16 @@ def load_uploaded_file(session: SessionState, selected_sheet: str | None = None)
465
  raise HTTPException(status_code=400, detail=msg)
466
 
467
  base = _set_dataframe_base(session, df, clear_models=True)
 
 
 
 
 
468
  return {
469
  "status": msg,
470
  "requires_sheet": False,
471
  "sheets": [],
 
472
  **base,
473
  "contexto": _selection_context(session),
474
  }
@@ -686,6 +791,22 @@ def search_transformacoes(session: SessionState, grau_min_coef: int = 0, grau_mi
686
  grau_min_f=int(grau_min_f),
687
  )
688
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
689
  session.resultados_busca = sanitize_value(resultados)
690
 
691
  if not resultados:
@@ -709,6 +830,20 @@ def search_transformacoes(session: SessionState, grau_min_coef: int = 0, grau_mi
709
  }
710
 
711
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
712
  def adotar_sugestao(session: SessionState, indice: int) -> dict[str, Any]:
713
  if not session.resultados_busca:
714
  raise HTTPException(status_code=400, detail="Nao ha sugestoes carregadas")
@@ -806,6 +941,12 @@ def fit_model(
806
  else:
807
  df_base = session.df_filtrado
808
 
 
 
 
 
 
 
809
  colunas_novas = {
810
  col: df_base[col].values
811
  for col in df_base.columns
@@ -895,7 +1036,24 @@ def apply_outlier_filters(session: SessionState, filtros: list[dict[str, Any]])
895
  if not filtros:
896
  return {"indices": [], "texto": ""}
897
 
 
 
 
 
 
 
 
 
 
898
  indices_outliers: set[int] = set()
 
 
 
 
 
 
 
 
899
 
900
  for filtro in filtros:
901
  var = str(filtro.get("variavel", "")).strip()
@@ -933,8 +1091,145 @@ def apply_outlier_filters(session: SessionState, filtros: list[dict[str, Any]])
933
  continue
934
 
935
  indices = sorted(indices_outliers)
936
- texto = ", ".join(str(i) for i in indices)
937
- return {"indices": indices, "texto": texto}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
938
 
939
 
940
  def resumir_outliers(outliers_anteriores: list[int], outliers_texto: str | None, reincluir_texto: str | None) -> str:
 
91
  return sorted(set(out))
92
 
93
 
94
+ def _classificar_grau_coef_pvalue(p_valor: Any) -> int:
95
+ try:
96
+ p = float(p_valor)
97
+ except Exception:
98
+ return 0
99
+ if not np.isfinite(p):
100
+ return 0
101
+ if p > 0.30:
102
+ return 0
103
+ if p > 0.20:
104
+ return 1
105
+ if p > 0.10:
106
+ return 2
107
+ return 3
108
+
109
+
110
+ def _classificar_grau_f_pvalue(p_valor: Any) -> int:
111
+ try:
112
+ p = float(p_valor)
113
+ except Exception:
114
+ return 0
115
+ if not np.isfinite(p):
116
+ return 0
117
+ if p > 0.05:
118
+ return 0
119
+ if p > 0.02:
120
+ return 1
121
+ if p > 0.01:
122
+ return 2
123
+ return 3
124
+
125
+
126
+ def _to_finite_float(value: Any) -> float | None:
127
+ try:
128
+ num = float(value)
129
+ except Exception:
130
+ return None
131
+ return num if np.isfinite(num) else None
132
+
133
+
134
+ def _avaliar_combinacao_transformacoes(
135
+ session: SessionState,
136
+ transformacao_y: str | None,
137
+ transformacoes_x: dict[str, str] | None,
138
+ ) -> dict[str, Any]:
139
+ if session.df_filtrado is None or not session.coluna_y or not session.colunas_x:
140
+ return {
141
+ "valido": False,
142
+ "r2": None,
143
+ "r2_ajustado": None,
144
+ "grau_f": 0,
145
+ "graus_coef": {},
146
+ }
147
+
148
+ transformacao_y_final = str(transformacao_y or "(x)")
149
+ incoming = transformacoes_x or {}
150
+ transformacoes_x_final = {col: str(incoming.get(col, "(x)")) for col in session.colunas_x}
151
+
152
+ resultado = ajustar_modelo(
153
+ session.df_filtrado,
154
+ session.coluna_y,
155
+ session.colunas_x,
156
+ transformacao_y_final,
157
+ transformacoes_x_final,
158
+ )
159
+ if resultado is None:
160
+ return {
161
+ "valido": False,
162
+ "r2": None,
163
+ "r2_ajustado": None,
164
+ "grau_f": 0,
165
+ "graus_coef": {col: 0 for col in session.colunas_x},
166
+ }
167
+
168
+ diagnosticos = resultado.get("diagnosticos", {}) or {}
169
+ modelo_sm = resultado.get("modelo_sm")
170
+ pvalues = getattr(modelo_sm, "pvalues", None)
171
+
172
+ graus_coef: dict[str, int] = {}
173
+ for col in session.colunas_x:
174
+ p_col = None
175
+ try:
176
+ if pvalues is not None and hasattr(pvalues, "index") and col in pvalues.index:
177
+ p_col = pvalues[col]
178
+ elif pvalues is not None and isinstance(pvalues, dict):
179
+ p_col = pvalues.get(col)
180
+ except Exception:
181
+ p_col = None
182
+ graus_coef[col] = _classificar_grau_coef_pvalue(p_col)
183
+
184
+ return {
185
+ "valido": True,
186
+ "r2": _to_finite_float(diagnosticos.get("r2")),
187
+ "r2_ajustado": _to_finite_float(diagnosticos.get("r2_ajustado")),
188
+ "grau_f": _classificar_grau_f_pvalue(diagnosticos.get("p_valor_F")),
189
+ "graus_coef": graus_coef,
190
+ }
191
+
192
+
193
  def _selection_context(session: SessionState) -> dict[str, Any]:
194
  return {
195
  "coluna_y": session.coluna_y,
 
556
  "status": msg_abas,
557
  "requires_sheet": True,
558
  "sheets": abas,
559
+ "sheet_selected": "",
560
  }
561
 
562
  df, msg, sucesso = carregar_arquivo(caminho, selected_sheet)
 
564
  raise HTTPException(status_code=400, detail=msg)
565
 
566
  base = _set_dataframe_base(session, df, clear_models=True)
567
+ sheet_selected_result = ""
568
+ if selected_sheet is not None:
569
+ sheet_selected_result = str(selected_sheet)
570
+ elif sucesso_abas and len(abas) == 1:
571
+ sheet_selected_result = str(abas[0])
572
  return {
573
  "status": msg,
574
  "requires_sheet": False,
575
  "sheets": [],
576
+ "sheet_selected": sheet_selected_result,
577
  **base,
578
  "contexto": _selection_context(session),
579
  }
 
791
  grau_min_f=int(grau_min_f),
792
  )
793
 
794
+ # Enriquecimento dos cards da seção 11: garante R² ajustado e graus coerentes
795
+ # para cada combinação retornada.
796
+ for item in resultados:
797
+ metrica = _avaliar_combinacao_transformacoes(
798
+ session,
799
+ item.get("transformacao_y"),
800
+ item.get("transformacoes_x", {}),
801
+ )
802
+ if metrica.get("valido"):
803
+ item["r2"] = metrica.get("r2")
804
+ item["r2_ajustado"] = metrica.get("r2_ajustado")
805
+ item["grau_f"] = metrica.get("grau_f")
806
+ item["graus_coef"] = metrica.get("graus_coef", {})
807
+ else:
808
+ item.setdefault("r2_ajustado", None)
809
+
810
  session.resultados_busca = sanitize_value(resultados)
811
 
812
  if not resultados:
 
830
  }
831
 
832
 
833
+ def preview_transformacao_manual(
834
+ session: SessionState,
835
+ transformacao_y: str | None,
836
+ transformacoes_x: dict[str, str] | None,
837
+ ) -> dict[str, Any]:
838
+ return sanitize_value(
839
+ _avaliar_combinacao_transformacoes(
840
+ session=session,
841
+ transformacao_y=transformacao_y,
842
+ transformacoes_x=transformacoes_x,
843
+ ),
844
+ )
845
+
846
+
847
  def adotar_sugestao(session: SessionState, indice: int) -> dict[str, Any]:
848
  if not session.resultados_busca:
849
  raise HTTPException(status_code=400, detail="Nao ha sugestoes carregadas")
 
941
  else:
942
  df_base = session.df_filtrado
943
 
944
+ # Seção 16: incluir também as variáveis independentes no payload exibido.
945
+ for col in session.colunas_x:
946
+ if col in df_base.columns and col not in tabela_metricas.columns:
947
+ tabela_metricas[col] = df_base[col].values
948
+
949
+ # Estado completo para filtros: mantém todas as colunas originais disponíveis.
950
  colunas_novas = {
951
  col: df_base[col].values
952
  for col in df_base.columns
 
1036
  if not filtros:
1037
  return {"indices": [], "texto": ""}
1038
 
1039
+ indices = _coletar_indices_outliers(metricas, filtros)
1040
+ texto = ", ".join(str(i) for i in indices)
1041
+ return {"indices": indices, "texto": texto}
1042
+
1043
+
1044
+ def _coletar_indices_outliers(metricas: pd.DataFrame, filtros: list[dict[str, Any]]) -> list[int]:
1045
+ if metricas is None or metricas.empty:
1046
+ return []
1047
+
1048
  indices_outliers: set[int] = set()
1049
+ metricas_sort_prioridade = ("Resíduo Pad.", "Resíduo Stud.", "Cook")
1050
+ coluna_sort: str | None = None
1051
+
1052
+ for filtro in filtros:
1053
+ var = str(filtro.get("variavel", "")).strip()
1054
+ if var in metricas_sort_prioridade and var in metricas.columns:
1055
+ coluna_sort = var
1056
+ break
1057
 
1058
  for filtro in filtros:
1059
  var = str(filtro.get("variavel", "")).strip()
 
1091
  continue
1092
 
1093
  indices = sorted(indices_outliers)
1094
+ if coluna_sort:
1095
+ serie_abs = pd.to_numeric(metricas[coluna_sort], errors="coerce").abs()
1096
+
1097
+ def _score_abs(idx: int) -> float:
1098
+ try:
1099
+ valor = float(serie_abs.get(idx))
1100
+ if np.isfinite(valor):
1101
+ return valor
1102
+ except Exception:
1103
+ pass
1104
+ return -1.0
1105
+
1106
+ # Ordena do maior valor absoluto para o menor (empate por índice).
1107
+ indices = sorted(indices, key=lambda idx: (-_score_abs(idx), idx))
1108
+
1109
+ return indices
1110
+
1111
+
1112
+ def _gerar_tabela_metricas_estado_para_filtros(
1113
+ df_filtrado: pd.DataFrame,
1114
+ coluna_y: str,
1115
+ colunas_x: list[str],
1116
+ transformacao_y: str,
1117
+ transformacoes_x: dict[str, str],
1118
+ ) -> pd.DataFrame:
1119
+ resultado = ajustar_modelo(
1120
+ df_filtrado,
1121
+ coluna_y,
1122
+ colunas_x,
1123
+ transformacao_y,
1124
+ transformacoes_x,
1125
+ )
1126
+ if resultado is None:
1127
+ raise HTTPException(status_code=400, detail="Nao foi possivel ajustar o modelo durante a recursividade")
1128
+
1129
+ tabela_metricas = resultado["tabela_obs_calc"].copy()
1130
+ tabela_metricas_estado = tabela_metricas.set_index("Índice")
1131
+
1132
+ indices_usados = resultado.get("indices_usados", [])
1133
+ if indices_usados:
1134
+ df_base = df_filtrado.loc[indices_usados]
1135
+ else:
1136
+ df_base = df_filtrado
1137
+
1138
+ colunas_novas = {
1139
+ col: df_base[col].values
1140
+ for col in df_base.columns
1141
+ if col not in tabela_metricas_estado.columns
1142
+ }
1143
+ if colunas_novas:
1144
+ tabela_metricas_estado = pd.concat(
1145
+ [
1146
+ tabela_metricas_estado,
1147
+ pd.DataFrame(colunas_novas, index=tabela_metricas_estado.index),
1148
+ ],
1149
+ axis=1,
1150
+ )
1151
+
1152
+ return tabela_metricas_estado
1153
+
1154
+
1155
+ def apply_outlier_filters_recursive(
1156
+ session: SessionState,
1157
+ filtros: list[dict[str, Any]],
1158
+ max_iter: int = 25,
1159
+ ) -> dict[str, Any]:
1160
+ if session.df_original is None:
1161
+ raise HTTPException(status_code=400, detail="Carregue dados primeiro")
1162
+ if not session.coluna_y or not session.colunas_x:
1163
+ raise HTTPException(status_code=400, detail="Selecione variaveis primeiro")
1164
+ if not filtros:
1165
+ return {
1166
+ "indices": [],
1167
+ "texto": "",
1168
+ "iteracoes": 0,
1169
+ "total": 0,
1170
+ "max_iter_reached": False,
1171
+ }
1172
+
1173
+ outliers_base = _clean_int_list(session.outliers_anteriores)
1174
+ outliers_atuais = list(outliers_base)
1175
+ novos_acumulados: list[int] = []
1176
+ novos_set: set[int] = set()
1177
+ iteracoes = 0
1178
+ max_iter_reached = False
1179
+
1180
+ # Iteração 1: usa exatamente as métricas atuais (mesmo comportamento do botão "Aplicar filtros").
1181
+ tabela_metricas_inicial = session.tabela_metricas_estado
1182
+ if tabela_metricas_inicial is not None and not tabela_metricas_inicial.empty:
1183
+ indices_iteracao = _coletar_indices_outliers(tabela_metricas_inicial, filtros)
1184
+ novos_iteracao = [idx for idx in indices_iteracao if idx not in outliers_atuais and idx not in novos_set]
1185
+ if novos_iteracao:
1186
+ iteracoes += 1
1187
+ novos_acumulados.extend(novos_iteracao)
1188
+ novos_set.update(novos_iteracao)
1189
+ outliers_atuais = sorted(set(outliers_atuais + novos_iteracao))
1190
+
1191
+ # Próximas iterações: recalcula métricas após excluir os outliers da iteração anterior.
1192
+ for _ in range(max(0, int(max_iter) - 1)):
1193
+ if not novos_acumulados:
1194
+ break
1195
+
1196
+ df_filtrado = session.df_original.copy()
1197
+ if outliers_atuais:
1198
+ df_filtrado = df_filtrado.drop(index=outliers_atuais, errors="ignore")
1199
+ if df_filtrado.empty:
1200
+ break
1201
+
1202
+ try:
1203
+ tabela_metricas_estado = _gerar_tabela_metricas_estado_para_filtros(
1204
+ df_filtrado=df_filtrado,
1205
+ coluna_y=session.coluna_y,
1206
+ colunas_x=list(session.colunas_x),
1207
+ transformacao_y=str(session.transformacao_y or "(x)"),
1208
+ transformacoes_x={str(k): str(v) for k, v in (session.transformacoes_x or {}).items()},
1209
+ )
1210
+ except HTTPException:
1211
+ break
1212
+
1213
+ indices_iteracao = _coletar_indices_outliers(tabela_metricas_estado, filtros)
1214
+ novos_iteracao = [idx for idx in indices_iteracao if idx not in outliers_atuais and idx not in novos_set]
1215
+ if not novos_iteracao:
1216
+ break
1217
+
1218
+ iteracoes += 1
1219
+ novos_acumulados.extend(novos_iteracao)
1220
+ novos_set.update(novos_iteracao)
1221
+ outliers_atuais = sorted(set(outliers_atuais + novos_iteracao))
1222
+ else:
1223
+ max_iter_reached = True
1224
+
1225
+ texto = ", ".join(str(i) for i in novos_acumulados)
1226
+ return {
1227
+ "indices": novos_acumulados,
1228
+ "texto": texto,
1229
+ "iteracoes": iteracoes,
1230
+ "total": len(novos_acumulados),
1231
+ "max_iter_reached": max_iter_reached,
1232
+ }
1233
 
1234
 
1235
  def resumir_outliers(outliers_anteriores: list[int], outliers_texto: str | None, reincluir_texto: str | None) -> str:
frontend/src/api.js CHANGED
@@ -156,8 +156,14 @@ export const api = {
156
  },
157
 
158
  updateModelDispersao: (sessionId, tipo) => postJson('/api/elaboracao/model-dispersao', { session_id: sessionId, tipo }),
 
 
 
 
 
159
 
160
  applyOutlierFilters: (sessionId, filtros) => postJson('/api/elaboracao/outliers/apply-filters', { session_id: sessionId, filtros }),
 
161
  restartOutlierIteration: (sessionId, outliersTexto, reincluirTexto, grauCoef, grauF) => postJson('/api/elaboracao/outliers/restart', {
162
  session_id: sessionId,
163
  outliers_texto: outliersTexto,
 
156
  },
157
 
158
  updateModelDispersao: (sessionId, tipo) => postJson('/api/elaboracao/model-dispersao', { session_id: sessionId, tipo }),
159
+ previewTransformElab: (sessionId, transformacaoY, transformacoesX) => postJson('/api/elaboracao/transform-preview', {
160
+ session_id: sessionId,
161
+ transformacao_y: transformacaoY,
162
+ transformacoes_x: transformacoesX,
163
+ }),
164
 
165
  applyOutlierFilters: (sessionId, filtros) => postJson('/api/elaboracao/outliers/apply-filters', { session_id: sessionId, filtros }),
166
+ applyOutlierFiltersRecursive: (sessionId, filtros) => postJson('/api/elaboracao/outliers/apply-filters-recursive', { session_id: sessionId, filtros }),
167
  restartOutlierIteration: (sessionId, outliersTexto, reincluirTexto, grauCoef, grauF) => postJson('/api/elaboracao/outliers/restart', {
168
  session_id: sessionId,
169
  outliers_texto: outliersTexto,
frontend/src/components/DataTable.jsx CHANGED
@@ -1,6 +1,12 @@
1
  import React from 'react'
2
 
3
- function DataTable({ table, maxHeight = 320 }) {
 
 
 
 
 
 
4
  if (!table || !table.columns || !table.rows) {
5
  return <div className="empty-box">Sem dados.</div>
6
  }
@@ -10,6 +16,9 @@ function DataTable({ table, maxHeight = 320 }) {
10
  ? table.rows.slice(0, MAX_RENDER_ROWS)
11
  : table.rows
12
  const renderTruncated = rowsToRender.length < table.rows.length
 
 
 
13
 
14
  return (
15
  <div className="table-wrapper" style={{ maxHeight }}>
@@ -22,13 +31,19 @@ function DataTable({ table, maxHeight = 320 }) {
22
  </tr>
23
  </thead>
24
  <tbody>
25
- {rowsToRender.map((row, i) => (
26
- <tr key={i}>
27
- {table.columns.map((col) => (
28
- <td key={`${i}-${col}`}>{String(row[col] ?? '')}</td>
29
- ))}
30
- </tr>
31
- ))}
 
 
 
 
 
 
32
  </tbody>
33
  </table>
34
  {renderTruncated ? (
 
1
  import React from 'react'
2
 
3
+ function DataTable({
4
+ table,
5
+ maxHeight = 320,
6
+ highlightedRowIndices = null,
7
+ highlightIndexColumn = 'Índice',
8
+ highlightClassName = 'table-row-highlight',
9
+ }) {
10
  if (!table || !table.columns || !table.rows) {
11
  return <div className="empty-box">Sem dados.</div>
12
  }
 
16
  ? table.rows.slice(0, MAX_RENDER_ROWS)
17
  : table.rows
18
  const renderTruncated = rowsToRender.length < table.rows.length
19
+ const highlightedSet = Array.isArray(highlightedRowIndices) && highlightedRowIndices.length > 0
20
+ ? new Set(highlightedRowIndices.map((item) => String(item)))
21
+ : null
22
 
23
  return (
24
  <div className="table-wrapper" style={{ maxHeight }}>
 
31
  </tr>
32
  </thead>
33
  <tbody>
34
+ {rowsToRender.map((row, i) => {
35
+ const rowIndex = row?.[highlightIndexColumn]
36
+ const rowClassName = highlightedSet && rowIndex != null && highlightedSet.has(String(rowIndex))
37
+ ? highlightClassName
38
+ : ''
39
+ return (
40
+ <tr key={i} className={rowClassName}>
41
+ {table.columns.map((col) => (
42
+ <td key={`${i}-${col}`}>{String(row[col] ?? '')}</td>
43
+ ))}
44
+ </tr>
45
+ )
46
+ })}
47
  </tbody>
48
  </table>
49
  {renderTruncated ? (
frontend/src/components/ElaboracaoTab.jsx CHANGED
@@ -38,8 +38,8 @@ function grauBadgeClass(value) {
38
 
39
  function defaultFiltros() {
40
  return [
41
- { variavel: 'Resíduo Pad.', operador: '<=', valor: -2 },
42
- { variavel: 'Resíduo Pad.', operador: '>=', valor: 2 },
43
  ]
44
  }
45
 
@@ -82,7 +82,7 @@ function buildFiltrosSnapshot(filtros) {
82
  filtros.map((item) => ({
83
  variavel: String(item?.variavel || ''),
84
  operador: String(item?.operador || ''),
85
- valor: Number(item?.valor ?? 0),
86
  })),
87
  )
88
  }
@@ -94,6 +94,34 @@ function buildOutlierTextSnapshot(outliersTexto, reincluirTexto) {
94
  })
95
  }
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  function sleep(ms) {
98
  return new Promise((resolve) => {
99
  window.setTimeout(resolve, ms)
@@ -497,6 +525,33 @@ function buildLoadedModelInfo(resp) {
497
  }
498
  }
499
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  function formatGeoColLabel(coluna, kind = 'default') {
501
  const valor = String(coluna || '')
502
  if (kind === 'cdlog' && valor.toUpperCase() === 'CTM') return 'CTM'
@@ -538,10 +593,12 @@ export default function ElaboracaoTab({ sessionId }) {
538
  const [loading, setLoading] = useState(false)
539
  const [downloadingAssets, setDownloadingAssets] = useState(false)
540
  const [error, setError] = useState('')
541
- const [status, setStatus] = useState('')
 
542
 
543
  const [uploadedFile, setUploadedFile] = useState(null)
544
  const [uploadDragOver, setUploadDragOver] = useState(false)
 
545
  const [requiresSheet, setRequiresSheet] = useState(false)
546
  const [sheetOptions, setSheetOptions] = useState([])
547
  const [selectedSheet, setSelectedSheet] = useState('')
@@ -596,11 +653,11 @@ export default function ElaboracaoTab({ sessionId }) {
596
 
597
  const [transformacaoY, setTransformacaoY] = useState('(x)')
598
  const [transformacoesX, setTransformacoesX] = useState({})
 
 
599
  const [transformacoesAplicadas, setTransformacoesAplicadas] = useState(null)
600
  const [origemTransformacoes, setOrigemTransformacoes] = useState(null)
601
- const [section8Open, setSection8Open] = useState(false)
602
  const [section10ManualOpen, setSection10ManualOpen] = useState(false)
603
- const [section11Open, setSection11Open] = useState(false)
604
  const [section6EditOpen, setSection6EditOpen] = useState(true)
605
 
606
  const [fit, setFit] = useState(null)
@@ -616,6 +673,7 @@ export default function ElaboracaoTab({ sessionId }) {
616
  const valoresAvaliacaoRef = useRef({})
617
  const [avaliacaoFormVersion, setAvaliacaoFormVersion] = useState(0)
618
  const [avaliacaoPendente, setAvaliacaoPendente] = useState(false)
 
619
  const [resultadoAvaliacaoHtml, setResultadoAvaliacaoHtml] = useState('')
620
  const [baseChoices, setBaseChoices] = useState([])
621
  const [baseValue, setBaseValue] = useState('')
@@ -631,9 +689,12 @@ export default function ElaboracaoTab({ sessionId }) {
631
  const [outlierTextosAplicadosSnapshot, setOutlierTextosAplicadosSnapshot] = useState(() => buildOutlierTextSnapshot('', ''))
632
  const marcarTodasXRef = useRef(null)
633
  const classificarXReqRef = useRef(0)
 
634
  const deleteConfirmTimersRef = useRef({})
635
  const uploadInputRef = useRef(null)
 
636
  const [disabledHint, setDisabledHint] = useState(null)
 
637
 
638
  const mapaChoices = useMemo(() => ['Visualização Padrão', ...colunasNumericas], [colunasNumericas])
639
  const colunasXDisponiveis = useMemo(
@@ -677,7 +738,6 @@ export default function ElaboracaoTab({ sessionId }) {
677
  () => formatPeriodoDadosMercado(periodoDadosMercadoPreview),
678
  [periodoDadosMercadoPreview],
679
  )
680
- const pendingWarningText = 'Há alterações em campos que ainda não foram aplicadas.'
681
  const semAlteracaoTooltipText = 'Não houve modificação para ser aplicada.'
682
  const dataMercadoPendente = useMemo(
683
  () => String(colunaDataMercado || '') !== String(colunaDataMercadoAplicada || ''),
@@ -765,6 +825,43 @@ export default function ElaboracaoTab({ sessionId }) {
765
  () => Boolean(fit) && outlierTextosSnapshotAtual !== outlierTextosAplicadosSnapshot,
766
  [fit, outlierTextosSnapshotAtual, outlierTextosAplicadosSnapshot],
767
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
768
  const transformacaoAplicadaYBadge = useMemo(
769
  () => formatTransformacaoBadge(transformacoesAplicadas?.transformacao_y),
770
  [transformacoesAplicadas],
@@ -819,6 +916,10 @@ export default function ElaboracaoTab({ sessionId }) {
819
  () => Math.max(1, Math.min(3, graficosSecao12.length || 1)),
820
  [graficosSecao12.length],
821
  )
 
 
 
 
822
  const showCoordsPanel = Boolean(
823
  coordsInfo && (
824
  !coordsInfo.tem_coords ||
@@ -937,6 +1038,40 @@ export default function ElaboracaoTab({ sessionId }) {
937
  }
938
  }, [sessionId, tipoFonteDados, colunasX, colunasXDisponiveis])
939
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
940
  useEffect(() => {
941
  let ativo = true
942
  if (!sessionId) return () => {
@@ -988,13 +1123,17 @@ export default function ElaboracaoTab({ sessionId }) {
988
  }
989
  }, [sessionId])
990
 
991
- async function withBusy(fn) {
992
  setLoading(true)
993
  setError('')
994
  try {
995
  await fn()
996
  } catch (err) {
997
- setError(err.message)
 
 
 
 
998
  } finally {
999
  setLoading(false)
1000
  }
@@ -1041,11 +1180,13 @@ export default function ElaboracaoTab({ sessionId }) {
1041
 
1042
  async function carregarModelosRepositorio() {
1043
  setRepoModelosLoading(true)
 
1044
  try {
1045
  const resp = await api.elaboracaoRepositorioModelos()
1046
  aplicarRespostaModelosRepositorio(resp)
1047
  } catch (err) {
1048
  setError(err.message || 'Falha ao carregar modelos do repositório.')
 
1049
  setRepoModelos([])
1050
  setRepoModeloSelecionado('')
1051
  setRepoFonteModelos('')
@@ -1058,7 +1199,6 @@ export default function ElaboracaoTab({ sessionId }) {
1058
  const resetXSelection = Boolean(options.resetXSelection)
1059
  const colunaYPadrao = String(resp.coluna_y_padrao || '')
1060
  setDataMercadoError('')
1061
- if (resp.status) setStatus(resp.status)
1062
  if (resp.dados) setDados(resp.dados)
1063
  if (resp.mapa_html) setMapaHtml(resp.mapa_html)
1064
  if (resp.colunas_numericas) setColunasNumericas(resp.colunas_numericas)
@@ -1114,6 +1254,8 @@ export default function ElaboracaoTab({ sessionId }) {
1114
  setSelectionAppliedSnapshot(buildSelectionSnapshot({ coluna_y: '' }))
1115
  setTransformacaoY('(x)')
1116
  setTransformacoesX({})
 
 
1117
  setTransformacoesAplicadas(null)
1118
  setOrigemTransformacoes(null)
1119
  setBuscaTransformAppliedSnapshot(buildGrauSnapshot(grauCoef, grauF))
@@ -1165,8 +1307,9 @@ export default function ElaboracaoTab({ sessionId }) {
1165
  function applySelectionResponse(resp) {
1166
  setSelection(resp)
1167
  setSection6EditOpen(false)
1168
- setSection8Open(false)
1169
  setSection10ManualOpen(false)
 
 
1170
  setTransformacoesAplicadas(null)
1171
  setOrigemTransformacoes(null)
1172
  const transformacaoYAplicada = resp.transformacao_y || transformacaoY
@@ -1203,7 +1346,6 @@ export default function ElaboracaoTab({ sessionId }) {
1203
 
1204
  function applyFitResponse(resp, origemMeta = null) {
1205
  setFit(resp)
1206
- setSection11Open(false)
1207
  const transformacaoYAplicada = resp.transformacao_y || transformacaoY
1208
  const transformacoesXAplicadas = resp.transformacoes_x || transformacoesX
1209
  if (resp.transformacao_y) {
@@ -1238,12 +1380,14 @@ export default function ElaboracaoTab({ sessionId }) {
1238
  valoresAvaliacaoRef.current = init
1239
  setAvaliacaoFormVersion((prev) => prev + 1)
1240
  setAvaliacaoPendente(false)
 
1241
  setResultadoAvaliacaoHtml('')
1242
  setBaseChoices([])
1243
  setBaseValue('')
1244
  }
1245
 
1246
- function aplicarRespostaCarregamento(resp, tipoFonteFallback = 'tabular') {
 
1247
  setManualMapError('')
1248
  setGeoProcessError('')
1249
  setGeoStatusHtml('')
@@ -1252,9 +1396,22 @@ export default function ElaboracaoTab({ sessionId }) {
1252
  setElaborador(resp.elaborador || null)
1253
  setModeloCarregadoInfo(buildLoadedModelInfo(resp))
1254
  setAvaliadorSelecionado(resp.elaborador?.nome_completo || '')
 
 
 
 
 
 
 
 
 
1255
  setRequiresSheet(Boolean(resp.requires_sheet))
1256
  setSheetOptions(resp.sheets || [])
1257
- setSelectedSheet(resp.sheet_selected || '')
 
 
 
 
1258
 
1259
  if (!resp.requires_sheet) {
1260
  const origemResp = String(resp.tipo || '').toLowerCase()
@@ -1264,81 +1421,100 @@ export default function ElaboracaoTab({ sessionId }) {
1264
  ? 'dai'
1265
  : 'tabular'
1266
  setTipoFonteDados(tipoFonte)
 
1267
  const resetXSelection = String(resp.tipo || '').toLowerCase() !== 'dai'
1268
  applyBaseResponse(resp, { resetXSelection })
1269
  return
1270
  }
1271
 
1272
- if (resp.status) {
1273
- setTipoFonteDados('tabular')
1274
- setColunaY('')
1275
- setColunaYDraft('')
1276
- setSelection(null)
1277
- setFit(null)
1278
- setSelectionAppliedSnapshot(buildSelectionSnapshot())
1279
- setColunasX([])
1280
- setDicotomicas([])
1281
- setCodigoAlocado([])
1282
- setPercentuais([])
1283
- setTransformacaoY('(x)')
1284
- setTransformacoesX({})
1285
- setTransformacoesAplicadas(null)
1286
- setOrigemTransformacoes(null)
1287
- setBuscaTransformAppliedSnapshot(buildGrauSnapshot(0, 0))
1288
- setManualTransformAppliedSnapshot(buildTransformacoesSnapshot('(x)', {}))
1289
- setColunasDataMercado([])
1290
- setColunaDataMercadoSugerida('')
1291
- setColunaDataMercado('')
1292
- setColunaDataMercadoAplicada('')
1293
- setPeriodoDadosMercado(null)
1294
- setPeriodoDadosMercadoPreview(null)
1295
- setDataMercadoError('')
1296
- setFiltros(defaultFiltros())
1297
- setOutliersTexto('')
1298
- setReincluirTexto('')
1299
- setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
1300
- setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
1301
- setCamposAvaliacao([])
1302
- valoresAvaliacaoRef.current = {}
1303
- setAvaliacaoFormVersion((prev) => prev + 1)
1304
- setAvaliacaoPendente(false)
1305
- setResultadoAvaliacaoHtml('')
1306
- setStatus(resp.status)
1307
- }
1308
  }
1309
 
1310
  async function onUploadClick(arquivo = null) {
1311
  const arquivoUpload = arquivo || uploadedFile
1312
  if (!arquivoUpload || !sessionId) return
 
 
1313
  await withBusy(async () => {
1314
  setMapaGerado(false)
1315
  setMapaHtml('')
1316
  setGeoAuto200(true)
 
 
 
1317
  const nomeArquivo = String(arquivoUpload?.name || '').toLowerCase()
1318
  const uploadEhDai = nomeArquivo.endsWith('.dai')
1319
  setTipoFonteDados(uploadEhDai ? 'dai' : 'tabular')
1320
  const resp = await api.uploadElaboracaoFile(sessionId, arquivoUpload)
1321
- aplicarRespostaCarregamento(resp, uploadEhDai ? 'dai' : 'tabular')
 
 
 
 
 
1322
  })
1323
  }
1324
 
1325
  async function onCarregarModeloRepositorio() {
1326
  if (!sessionId || !repoModeloSelecionado) return
 
 
 
1327
  await withBusy(async () => {
1328
  setMapaGerado(false)
1329
  setMapaHtml('')
1330
  setGeoAuto200(true)
 
 
 
1331
  setTipoFonteDados('dai')
1332
  const resp = await api.elaboracaoRepositorioCarregar(sessionId, repoModeloSelecionado)
1333
- aplicarRespostaCarregamento(resp, 'dai')
1334
  setUploadedFile(null)
 
 
1335
  })
1336
  }
1337
 
1338
  function onUploadInputChange(event) {
1339
  const input = event.target
1340
  const file = input.files?.[0] ?? null
 
1341
  setUploadedFile(file)
 
1342
  input.value = ''
1343
  if (!file || loading) return
1344
  void onUploadClick(file)
@@ -1362,12 +1538,15 @@ export default function ElaboracaoTab({ sessionId }) {
1362
  setUploadDragOver(false)
1363
  const file = event.dataTransfer?.files?.[0]
1364
  if (!file || loading) return
 
1365
  setUploadedFile(file)
 
1366
  void onUploadClick(file)
1367
  }
1368
 
1369
  async function onConfirmSheet() {
1370
  if (!selectedSheet || !sessionId) return
 
1371
  await withBusy(async () => {
1372
  setMapaGerado(false)
1373
  setMapaHtml('')
@@ -1383,7 +1562,15 @@ export default function ElaboracaoTab({ sessionId }) {
1383
  setModeloCarregadoInfo(null)
1384
  setAvaliadorSelecionado(resp.elaborador?.nome_completo || '')
1385
  setRequiresSheet(false)
 
 
 
 
1386
  applyBaseResponse(resp, { resetXSelection: true })
 
 
 
 
1387
  })
1388
  }
1389
 
@@ -1391,7 +1578,6 @@ export default function ElaboracaoTab({ sessionId }) {
1391
  const coluna = String(value || '')
1392
  setColunaDataMercado(coluna)
1393
  setDataMercadoError('')
1394
- setStatus((prev) => (String(prev || '').startsWith('Prévia do período') ? '' : prev))
1395
 
1396
  if (!sessionId || !coluna) {
1397
  setPeriodoDadosMercadoPreview(null)
@@ -1422,7 +1608,6 @@ export default function ElaboracaoTab({ sessionId }) {
1422
  setPeriodoDadosMercado(periodo)
1423
  setPeriodoDadosMercadoPreview(periodo)
1424
  setDataMercadoError('')
1425
- if (resp.status) setStatus(resp.status)
1426
  setModeloCarregadoInfo((prev) => {
1427
  if (!prev) return prev
1428
  return {
@@ -1482,7 +1667,6 @@ export default function ElaboracaoTab({ sessionId }) {
1482
  setGeoProcessError('')
1483
  try {
1484
  const resp = await api.mapCoords(sessionId, manualLat, manualLon)
1485
- setStatus(resp.status)
1486
  setMapaHtml(resp.mapa_html)
1487
  setDados(resp.dados)
1488
  setCoordsInfo(resp.coords)
@@ -1544,7 +1728,6 @@ export default function ElaboracaoTab({ sessionId }) {
1544
  if (!sessionId) return
1545
  await withBusy(async () => {
1546
  const resp = await api.geocodificarReiniciar(sessionId)
1547
- setStatus(resp.status || '')
1548
  setGeoStatusHtml(resp.status_html || '')
1549
  setGeoFalhasHtml(resp.falhas_html || '')
1550
  setGeoCorrecoes(parseCorrecoes(resp.falhas_para_correcao))
@@ -1565,7 +1748,6 @@ export default function ElaboracaoTab({ sessionId }) {
1565
  if (!sessionId) return
1566
  await withBusy(async () => {
1567
  const resp = await api.geocodificarExcluirCoords(sessionId)
1568
- setStatus(resp.status || '')
1569
  setGeoStatusHtml(resp.status_html || '')
1570
  setGeoFalhasHtml(resp.falhas_html || '')
1571
  setGeoCorrecoes(parseCorrecoes(resp.falhas_para_correcao))
@@ -1693,13 +1875,41 @@ export default function ElaboracaoTab({ sessionId }) {
1693
  async function onApplyOutlierFilters() {
1694
  if (!sessionId) return
1695
  await withBusy(async () => {
1696
- const filtrosValidos = filtros.filter((f) => f.variavel && f.operador)
 
 
 
 
 
 
 
 
 
1697
  const resp = await api.applyOutlierFilters(sessionId, filtrosValidos)
1698
  setOutliersTexto(resp.texto || '')
1699
  setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(filtros))
1700
  })
1701
  }
1702
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1703
  async function onSummaryOutliers() {
1704
  if (!sessionId) return
1705
  await withBusy(async () => {
@@ -1710,7 +1920,7 @@ export default function ElaboracaoTab({ sessionId }) {
1710
 
1711
  function onAddFiltro() {
1712
  const variavelPadrao = fit?.variaveis_filtro?.[0] || 'Resíduo Pad.'
1713
- setFiltros((prev) => [...prev, { variavel: variavelPadrao, operador: '>=', valor: 0 }])
1714
  }
1715
 
1716
  function onRemoveFiltro(index) {
@@ -1720,6 +1930,55 @@ export default function ElaboracaoTab({ sessionId }) {
1720
  })
1721
  }
1722
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1723
  async function onRestartIteration() {
1724
  if (!sessionId) return
1725
  await withBusy(async () => {
@@ -1733,10 +1992,14 @@ export default function ElaboracaoTab({ sessionId }) {
1733
  setFit(null)
1734
  setTransformacoesAplicadas(null)
1735
  setOrigemTransformacoes(null)
 
1736
  setResultadoAvaliacaoHtml('')
1737
  setResumoOutliers(resp.resumo_outliers || resumoOutliers)
 
1738
  if (typeof window !== 'undefined') {
1739
- window.scrollTo({ top: 0, behavior: 'smooth' })
 
 
1740
  }
1741
  })
1742
  }
@@ -1762,6 +2025,7 @@ export default function ElaboracaoTab({ sessionId }) {
1762
  valoresAvaliacaoRef.current = {}
1763
  setAvaliacaoFormVersion((prev) => prev + 1)
1764
  setAvaliacaoPendente(false)
 
1765
  setResultadoAvaliacaoHtml('')
1766
  setOutliersHtml(resp.outliers_html || '')
1767
  setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
@@ -1776,10 +2040,21 @@ export default function ElaboracaoTab({ sessionId }) {
1776
  setResultadoAvaliacaoHtml(resp.resultado_html || '')
1777
  setBaseChoices(resp.base_choices || [])
1778
  setBaseValue(resp.base_value || '')
 
1779
  setAvaliacaoPendente(false)
1780
  })
1781
  }
1782
 
 
 
 
 
 
 
 
 
 
 
1783
  async function onClearAvaliacao() {
1784
  if (!sessionId) return
1785
  await withBusy(async () => {
@@ -1787,13 +2062,7 @@ export default function ElaboracaoTab({ sessionId }) {
1787
  setResultadoAvaliacaoHtml(resp.resultado_html || '')
1788
  setBaseChoices(resp.base_choices || [])
1789
  setBaseValue(resp.base_value || '')
1790
- const limpo = {}
1791
- camposAvaliacao.forEach((campo) => {
1792
- limpo[campo.coluna] = ''
1793
- })
1794
- valoresAvaliacaoRef.current = limpo
1795
- setAvaliacaoFormVersion((prev) => prev + 1)
1796
- setAvaliacaoPendente(false)
1797
  })
1798
  }
1799
 
@@ -1804,6 +2073,7 @@ export default function ElaboracaoTab({ sessionId }) {
1804
  setResultadoAvaliacaoHtml(resp.resultado_html || '')
1805
  setBaseChoices(resp.base_choices || [])
1806
  setBaseValue(resp.base_value || '')
 
1807
  })
1808
  }
1809
 
@@ -2071,143 +2341,231 @@ export default function ElaboracaoTab({ sessionId }) {
2071
  }
2072
 
2073
  return (
2074
- <div className="tab-content">
2075
- {status ? <div className="status-line">{status}</div> : null}
2076
-
2077
  <SectionBlock step="1" title="Importar Dados" subtitle="Upload de CSV, Excel ou .dai com recuperação do fluxo.">
2078
  <div className="section1-groups">
2079
  <div className="subpanel section1-group">
2080
- <h4>Carregar modelo</h4>
2081
- <div className="row upload-repo-row">
2082
- <label>Modelo do repositório</label>
2083
- <select
2084
- value={repoModeloSelecionado}
2085
- onChange={(e) => setRepoModeloSelecionado(e.target.value)}
2086
- disabled={loading || repoModelosLoading || repoModelos.length === 0}
2087
- >
2088
- <option value="">
2089
- {repoModelosLoading ? 'Carregando lista...' : repoModelos.length > 0 ? 'Selecione um modelo' : 'Nenhum modelo disponível'}
2090
- </option>
2091
- {repoModelos.map((item) => (
2092
- <option key={`repo-elab-${item.id}`} value={item.id}>
2093
- {item.nome_modelo || item.arquivo}
2094
- </option>
2095
- ))}
2096
- </select>
2097
- <div className="row compact upload-repo-actions">
2098
- <button type="button" onClick={onCarregarModeloRepositorio} disabled={loading || repoModelosLoading || !repoModeloSelecionado}>
2099
- Carregar do repositório
2100
- </button>
2101
- <button type="button" onClick={() => void carregarModelosRepositorio()} disabled={loading || repoModelosLoading}>
2102
- Atualizar lista
2103
  </button>
2104
- </div>
2105
- {repoFonteModelos ? <div className="section1-empty-hint">{repoFonteModelos}</div> : null}
2106
- </div>
2107
- <div
2108
- className={`upload-dropzone${uploadDragOver ? ' is-dragover' : ''}`}
2109
- onDragOver={onUploadDropZoneDragOver}
2110
- onDragEnter={onUploadDropZoneDragOver}
2111
- onDragLeave={onUploadDropZoneDragLeave}
2112
- onDrop={onUploadDropZoneDrop}
2113
- >
2114
- <input
2115
- ref={uploadInputRef}
2116
- type="file"
2117
- className="upload-hidden-input"
2118
- onChange={onUploadInputChange}
2119
- />
2120
- <div className="row upload-dropzone-main">
2121
  <button
2122
  type="button"
2123
- className="btn-upload-select"
2124
- onClick={() => uploadInputRef.current?.click()}
 
 
 
2125
  disabled={loading}
2126
  >
2127
- Selecionar arquivo
2128
  </button>
2129
  </div>
2130
- <div className="upload-dropzone-hint">Ou arraste e solte aqui para carregar automaticamente.</div>
2131
- <div className="upload-dropzone-file">
2132
- {uploadedFile ? `Arquivo selecionado: ${uploadedFile.name}` : 'Nenhum arquivo selecionado.'}
2133
- </div>
2134
- </div>
 
 
 
 
 
 
 
 
 
 
2135
 
2136
- {requiresSheet ? (
2137
- <div className="row">
2138
- <select value={selectedSheet} onChange={(e) => setSelectedSheet(e.target.value)}>
2139
- {sheetOptions.map((sheet) => (
2140
- <option key={sheet} value={sheet}>{sheet}</option>
 
 
 
 
 
 
 
 
 
 
2141
  ))}
2142
  </select>
2143
- <button onClick={onConfirmSheet} disabled={loading || !selectedSheet}>Confirmar aba</button>
 
 
 
 
 
 
 
 
2144
  </div>
2145
  ) : null}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2146
  </div>
2147
 
2148
- <div className="subpanel section1-group">
2149
- <h4>Informações do modelo</h4>
2150
- {modeloCarregadoInfo ? (
2151
- <div className="modelo-info-card">
2152
- <div className="modelo-info-split">
2153
- <div className="modelo-info-col">
2154
- <div className="modelo-info-stack-block">
2155
- <div className="elaborador-badge-title">NOME DO MODELO:</div>
2156
- <div className="elaborador-badge-name">{modeloCarregadoInfo.nome_modelo || '-'}</div>
 
2157
  </div>
2158
-
2159
- <div className="modelo-info-stack-block">
2160
- <div className="elaborador-badge-title">ELABORADO POR:</div>
2161
- {elaborador?.nome_completo ? (
2162
- <div className="elaborador-badge-name">{elaborador.nome_completo}</div>
2163
- ) : (
2164
- <div className="section1-empty-hint">Elaborador não informado no arquivo.</div>
2165
- )}
2166
- {elaboradorMeta.length > 0 && elaborador?.nome_completo ? (
2167
- <div className="elaborador-badge-meta">{elaboradorMeta.join(' | ')}</div>
2168
- ) : null}
 
 
 
 
 
 
 
 
 
 
 
 
2169
  </div>
2170
  </div>
 
 
 
2171
 
2172
- <div className="modelo-info-col modelo-info-col-vars">
2173
- <div className="elaborador-badge-title">Variáveis selecionadas:</div>
2174
- {modeloCarregadoInfo.coluna_y ? (
2175
- <div className="variavel-badge-line">
2176
- <span className="variavel-badge-label">Dependente:</span>
2177
- <span className="variavel-chip variavel-chip-y variavel-chip-inline">
2178
- {modeloCarregadoInfo.coluna_y}
2179
- <span className="variavel-chip-transform">{` ${transformacaoYModeloBadge}`}</span>
2180
- </span>
2181
  </div>
2182
- ) : (
2183
- <div className="section1-empty-hint">Variável dependente não encontrada no modelo carregado.</div>
2184
- )}
2185
- {variaveisIndependentesModeloBadge.length > 0 ? (
2186
- <div className="variavel-badge-line">
2187
- <span className="variavel-badge-label">Independentes:</span>
2188
- <div className="variavel-chip-wrap">
2189
- {variaveisIndependentesModeloBadge.map((item) => (
2190
- <span key={`ind-${item.coluna}`} className="variavel-chip">
2191
- {item.coluna}
2192
- <span className="variavel-chip-transform">{` ${item.transformacao}`}</span>
2193
- </span>
2194
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2195
  </div>
 
 
 
 
 
 
2196
  </div>
2197
- ) : (
2198
- <div className="section1-empty-hint">Sem variáveis independentes no modelo carregado.</div>
2199
- )}
2200
- <div className="variavel-badge-line">
2201
- <span className="variavel-badge-label">Período dados:</span>
2202
- <span className="variavel-badge-value">{periodoModeloCarregadoTexto}</span>
2203
  </div>
2204
  </div>
2205
  </div>
2206
- </div>
2207
- ) : (
2208
- <div className="section1-empty-hint">Carregue um modelo .dai para visualizar os badges do elaborador.</div>
2209
- )}
2210
- </div>
2211
  </div>
2212
  </SectionBlock>
2213
 
@@ -2270,7 +2628,6 @@ export default function ElaboracaoTab({ sessionId }) {
2270
  <button
2271
  onClick={() => {
2272
  setCoordsMode('skipped')
2273
- setStatus('Seção 2 liberada sem coordenadas completas.')
2274
  }}
2275
  disabled={loading}
2276
  >
@@ -2479,75 +2836,78 @@ export default function ElaboracaoTab({ sessionId }) {
2479
 
2480
  <SectionBlock
2481
  step="3"
2482
- title="Visualizar Dados"
2483
- subtitle="Mapa interativo e tabela prévia da base carregada."
2484
  >
2485
- <div className="dados-visualizacao-groups">
2486
- <div className="subpanel dados-visualizacao-group">
2487
- {!mapaGerado ? (
2488
- <div className="empty-box">
2489
- <div className="row">
2490
- <button type="button" className="btn-gerar-mapa" onClick={onGerarMapa} disabled={loading}>
2491
- Gerar Mapa
2492
- </button>
2493
- </div>
2494
- <div className="section1-empty-hint">O mapa será carregado somente após solicitação explícita.</div>
2495
- </div>
2496
- ) : (
2497
- <details className="dados-mapa-details" open>
2498
- <summary>Mapa</summary>
2499
- <div className="row compact dados-mapa-controls">
2500
- <label>Variável no mapa</label>
2501
- <select value={mapaVariavel} onChange={(e) => onMapVarChange(e.target.value)}>
2502
- {mapaChoices.map((choice) => (
2503
- <option key={choice} value={choice}>{choice}</option>
2504
- ))}
2505
- </select>
2506
- </div>
2507
- <div className="download-actions-bar">
2508
- <button
2509
- type="button"
2510
- className="btn-download-subtle"
2511
- onClick={onDownloadMapaSecao3}
2512
- disabled={loading || downloadingAssets || !mapaHtml}
2513
- >
2514
- Fazer download
2515
- </button>
2516
- </div>
2517
- <MapFrame html={mapaHtml} />
2518
- </details>
2519
- )}
2520
- </div>
2521
-
2522
- <div className="subpanel dados-visualizacao-group">
2523
- <h4>Dados de mercado</h4>
2524
- <div className="dados-outliers-resumo">
2525
- <div className="resumo-outliers-box">
2526
- {outliersAnteriores.length > 0
2527
- ? `Há outliers excluídos: sim (${outliersAnteriores.length})`
2528
- : 'Há outliers excluídos: não'}
2529
  </div>
2530
- {outliersAnteriores.length > 0 ? (
2531
- <div className="resumo-outliers-box">Índices excluídos: {joinSelection(outliersAnteriores)}</div>
2532
- ) : null}
2533
  </div>
2534
- <div className="download-actions-bar">
2535
- <button
2536
- type="button"
2537
- className="btn-download-subtle"
2538
- onClick={() => onDownloadTableCsv(dados, 'secao3_dados_mercado')}
2539
- disabled={loading || downloadingAssets || !dados}
2540
- >
2541
- Fazer download
2542
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2543
  </div>
2544
- <DataTable table={dados} maxHeight={540} />
 
 
 
 
 
 
 
 
 
 
 
 
2545
  </div>
 
2546
  </div>
2547
  </SectionBlock>
2548
 
2549
  <SectionBlock
2550
- step="4"
2551
  title="Definir Data dos Dados de Mercado"
2552
  subtitle="Selecione a coluna de data dos dados de mercado, visualize o período e aplique."
2553
  >
@@ -2589,7 +2949,6 @@ export default function ElaboracaoTab({ sessionId }) {
2589
  Aplicar
2590
  </button>
2591
  </span>
2592
- {dataMercadoPendente ? <span className="pending-apply-note">{pendingWarningText}</span> : null}
2593
  </div>
2594
  <div className="section1-empty-hint">Valor atualmente aplicado: {colunaDataMercadoAplicada || '-'}</div>
2595
  </div>
@@ -2597,7 +2956,7 @@ export default function ElaboracaoTab({ sessionId }) {
2597
  </div>
2598
  </SectionBlock>
2599
 
2600
- <SectionBlock step="5" title="Selecionar Variável Dependente" subtitle="Defina a variável dependente (Y).">
2601
  <div className="row">
2602
  <label>Variável Dependente (Y)</label>
2603
  <select value={colunaYDraft} onChange={(e) => setColunaYDraft(e.target.value)}>
@@ -2622,12 +2981,11 @@ export default function ElaboracaoTab({ sessionId }) {
2622
  Aplicar
2623
  </button>
2624
  </span>
2625
- {dependentePendente ? <span className="pending-apply-note">{pendingWarningText}</span> : null}
2626
  </div>
2627
  <div className="section1-empty-hint section5-applied-hint">Valor atualmente aplicado: {colunaY || '-'}</div>
2628
  </SectionBlock>
2629
 
2630
- <SectionBlock step="6" title="Selecionar Variáveis Independentes" subtitle="Escolha regressoras e grupos de tipologia.">
2631
  {!colunaY ? (
2632
  <div className="section1-empty-hint">
2633
  Aplique a variável dependente na etapa anterior para liberar as opções de variáveis independentes.
@@ -2690,7 +3048,7 @@ export default function ElaboracaoTab({ sessionId }) {
2690
  </div>
2691
 
2692
  <div className="compact-option-group compact-option-group-codigo">
2693
- <h4>Variáveis de Código Alocado/Ajustado</h4>
2694
  <div className="checkbox-inline-wrap">
2695
  {colunasX.map((col) => (
2696
  <label key={`c-${col}`} className="compact-checkbox">
@@ -2722,7 +3080,6 @@ export default function ElaboracaoTab({ sessionId }) {
2722
  >
2723
  <button onClick={onApplySelection} disabled={loading || !podeAplicarSelecao}>Aplicar seleção</button>
2724
  </span>
2725
- {selecaoPendente ? <span className="pending-apply-note">{pendingWarningText}</span> : null}
2726
  </div>
2727
  </>
2728
  ) : null}
@@ -2755,7 +3112,7 @@ export default function ElaboracaoTab({ sessionId }) {
2755
  )}
2756
  </div>
2757
  <div className="section6-summary-group">
2758
- <div className="section6-summary-label">Código alocado/ajustado:</div>
2759
  {codigoAlocado.length > 0 ? (
2760
  <div className="checkbox-inline-wrap">
2761
  {codigoAlocado.map((coluna) => (
@@ -2793,12 +3150,12 @@ export default function ElaboracaoTab({ sessionId }) {
2793
 
2794
  {selection ? (
2795
  <>
2796
- <SectionBlock step="7" title="Estatísticas das Variáveis Selecionadas" subtitle="Resumo estatístico para Y e regressoras.">
2797
  <div className="download-actions-bar">
2798
  <button
2799
  type="button"
2800
  className="btn-download-subtle"
2801
- onClick={() => onDownloadTableCsv(selection.estatisticas, 'secao7_estatisticas')}
2802
  disabled={loading || downloadingAssets || !selection.estatisticas}
2803
  >
2804
  Fazer download
@@ -2807,76 +3164,65 @@ export default function ElaboracaoTab({ sessionId }) {
2807
  <DataTable table={selection.estatisticas} />
2808
  </SectionBlock>
2809
 
2810
- <SectionBlock step="8" title="Teste de Micronumerosidade" subtitle="Validação de amostra mínima para variáveis selecionadas.">
2811
  <div dangerouslySetInnerHTML={{ __html: selection.micronumerosidade_html || '' }} />
2812
  </SectionBlock>
2813
 
2814
- <SectionBlock step="9" title="Gráficos de Dispersão das Variáveis Independentes" subtitle="Leitura visual entre X e Y no conjunto filtrado.">
2815
- <details
2816
- className="section-content-toggle"
2817
- open={section8Open}
2818
- onToggle={(event) => setSection8Open(event.currentTarget.open)}
2819
- >
2820
- <summary>Mostrar/Ocultar gráficos da seção</summary>
2821
- {section8Open ? (
2822
- <>
2823
- <div className="download-actions-bar">
2824
- {graficosSecao9.length > 1 ? <span className="download-actions-label">Fazer download:</span> : null}
2825
- {graficosSecao9.map((item, idx) => {
2826
- const fileBase = `secao9_${sanitizeFileName(item.label, `dispersao_${idx + 1}`)}`
2827
- return (
2828
- <button
2829
- key={`s9-dl-${item.id}`}
2830
- type="button"
2831
- className="btn-download-subtle"
2832
- title={item.legenda}
2833
- onClick={() => onDownloadFigurePng(item.figure, fileBase, { forceHideLegend: true })}
2834
- disabled={loading || downloadingAssets || !item.figure}
2835
- >
2836
- {graficosSecao9.length > 1 ? item.label : 'Fazer download'}
2837
- </button>
2838
- )
2839
- })}
2840
- {graficosSecao9.length > 1 ? (
2841
- <button
2842
- type="button"
2843
- className="btn-download-subtle"
2844
- onClick={() => onDownloadFiguresPngBatch(
2845
- graficosSecao9.map((item, idx) => ({
2846
- figure: item.figure,
2847
- fileNameBase: `secao9_${sanitizeFileName(item.label, `dispersao_${idx + 1}`)}`,
2848
- forceHideLegend: true,
2849
- })),
2850
- )}
2851
- disabled={loading || downloadingAssets || graficosSecao9.length === 0}
2852
- >
2853
- Todos
2854
- </button>
2855
- ) : null}
2856
- </div>
2857
- {graficosSecao9.length > 0 ? (
2858
- <div className="plot-grid-scatter" style={{ '--plot-cols': colunasGraficosSecao9 }}>
2859
- {graficosSecao9.map((item) => (
2860
- <PlotFigure
2861
- key={`s9-plot-${item.id}`}
2862
- figure={item.figure}
2863
- title={item.title}
2864
- subtitle={item.subtitle}
2865
- forceHideLegend
2866
- className="plot-stretch"
2867
- lazy
2868
- />
2869
- ))}
2870
- </div>
2871
- ) : (
2872
- <div className="empty-box">Grafico indisponivel.</div>
2873
  )}
2874
- </>
 
 
 
2875
  ) : null}
2876
- </details>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2877
  </SectionBlock>
2878
 
2879
- <SectionBlock step="10" title="Transformações Sugeridas" subtitle="Busca automática de combinações por R² e enquadramento.">
2880
  <div className="row">
2881
  <label>Grau mínimo dos coeficientes</label>
2882
  <select value={grauCoef} onChange={(e) => setGrauCoef(Number(e.target.value))}>
@@ -2891,7 +3237,6 @@ export default function ElaboracaoTab({ sessionId }) {
2891
  ))}
2892
  </select>
2893
  <button onClick={onSearchTransform} disabled={loading}>Buscar transformações</button>
2894
- {buscaTransformPendente ? <span className="pending-apply-note">{pendingWarningText}</span> : null}
2895
  </div>
2896
  {(selection.busca?.resultados || []).length > 0 ? (
2897
  <div className="transform-suggestions-grid">
@@ -2900,7 +3245,8 @@ export default function ElaboracaoTab({ sessionId }) {
2900
  <div className="transform-suggestion-head">
2901
  <span className="transform-suggestion-rank">#{item.rank || idx + 1}</span>
2902
  <div className="transform-suggestion-metrics">
2903
- <span className="transform-suggestion-r2">R² = {Number(item.r2 ?? 0).toFixed(4)}</span>
 
2904
  <span className={grauBadgeClass(Number(item.grau_f ?? 0))}>
2905
  Teste F: {GRAU_LABEL_CURTO[Number(item.grau_f ?? 0)] || 'Sem enq.'}
2906
  </span>
@@ -2934,32 +3280,55 @@ export default function ElaboracaoTab({ sessionId }) {
2934
  )}
2935
  </SectionBlock>
2936
 
2937
- <SectionBlock step="11" title="Aplicação das Transformações" subtitle="Configuração manual para ajuste do modelo.">
2938
- <div className="manual-transform-toggle">
2939
  <button
2940
  type="button"
2941
  className={section10ManualOpen ? 'btn-manual-toggle active' : 'btn-manual-toggle'}
2942
  onClick={() => setSection10ManualOpen((prev) => !prev)}
2943
  >
2944
- {section10ManualOpen ? 'Ocultar ajustes manuais de transformação' : 'Proceceder com as transformações manualmente'}
2945
  </button>
2946
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2947
 
2948
  {section10ManualOpen ? (
2949
  <>
2950
- <div className="row">
2951
- <label>Transformação de Y</label>
2952
- <select value={transformacaoY} onChange={(e) => setTransformacaoY(e.target.value)}>
2953
- {['(x)', '1/(x)', 'ln(x)', 'exp(x)', '(x)^2', 'raiz(x)', '1/raiz(x)'].map((item) => (
2954
- <option key={`y-${item}`} value={item}>{item}</option>
2955
- ))}
2956
- </select>
2957
- </div>
2958
-
2959
  <div className="transform-grid">
 
 
 
 
 
 
 
 
2960
  {(selection.transform_fields || []).map((field) => (
2961
  <div key={`tf-${field.coluna}`} className="transform-card">
2962
- <span>{field.coluna}</span>
 
 
 
 
 
2963
  <select
2964
  value={transformacoesX[field.coluna] || '(x)'}
2965
  onChange={(e) => setTransformacoesX((prev) => ({ ...prev, [field.coluna]: e.target.value }))}
@@ -2982,7 +3351,6 @@ export default function ElaboracaoTab({ sessionId }) {
2982
  >
2983
  <button className="btn-fit-model" onClick={onFitModel} disabled={loading || !podeAplicarTransformacaoManual}>Aplicar transformações e ajustar modelo</button>
2984
  </span>
2985
- {manualTransformPendente ? <span className="pending-apply-note">{pendingWarningText}</span> : null}
2986
  </div>
2987
  </>
2988
  ) : null}
@@ -3028,80 +3396,69 @@ export default function ElaboracaoTab({ sessionId }) {
3028
 
3029
  {fit ? (
3030
  <>
3031
- <SectionBlock step="12" title="Gráficos de Dispersão (Variáveis Transformadas)" subtitle="Dispersão com variáveis já transformadas.">
3032
- <details
3033
- className="section-content-toggle"
3034
- open={section11Open}
3035
- onToggle={(event) => setSection11Open(event.currentTarget.open)}
3036
- >
3037
- <summary>Mostrar/Ocultar gráficos da seção</summary>
3038
- {section11Open ? (
3039
- <>
3040
- <div className="row">
3041
- <label>Tipo de dispersão</label>
3042
- <select value={tipoDispersao} onChange={(e) => onTipoDispersaoChange(e.target.value)}>
3043
- <option value="Variáveis Independentes Transformadas X Variável Dependente Transformada">X transformado x Y transformado</option>
3044
- <option value="Variáveis Independentes Transformadas X Resíduo Padronizado">X transformado x Resíduo</option>
3045
- <option value="Variáveis Independentes Não Transformadas X Resíduo Padronizado">X não transformado x Resíduo</option>
3046
- </select>
3047
- </div>
3048
- <div className="download-actions-bar">
3049
- {graficosSecao12.length > 1 ? <span className="download-actions-label">Fazer download:</span> : null}
3050
- {graficosSecao12.map((item, idx) => {
3051
- const fileBase = `secao12_${sanitizeFileName(item.label, `dispersao_${idx + 1}`)}`
3052
- return (
3053
- <button
3054
- key={`s12-dl-${item.id}`}
3055
- type="button"
3056
- className="btn-download-subtle"
3057
- title={item.legenda}
3058
- onClick={() => onDownloadFigurePng(item.figure, fileBase, { forceHideLegend: true })}
3059
- disabled={loading || downloadingAssets || !item.figure}
3060
- >
3061
- {graficosSecao12.length > 1 ? item.label : 'Fazer download'}
3062
- </button>
3063
- )
3064
- })}
3065
- {graficosSecao12.length > 1 ? (
3066
- <button
3067
- type="button"
3068
- className="btn-download-subtle"
3069
- onClick={() => onDownloadFiguresPngBatch(
3070
- graficosSecao12.map((item, idx) => ({
3071
- figure: item.figure,
3072
- fileNameBase: `secao12_${sanitizeFileName(item.label, `dispersao_${idx + 1}`)}`,
3073
- forceHideLegend: true,
3074
- })),
3075
- )}
3076
- disabled={loading || downloadingAssets || graficosSecao12.length === 0}
3077
- >
3078
- Todos
3079
- </button>
3080
- ) : null}
3081
- </div>
3082
- {graficosSecao12.length > 0 ? (
3083
- <div className="plot-grid-scatter" style={{ '--plot-cols': colunasGraficosSecao12 }}>
3084
- {graficosSecao12.map((item) => (
3085
- <PlotFigure
3086
- key={`s12-plot-${item.id}`}
3087
- figure={item.figure}
3088
- title={item.title}
3089
- subtitle={item.subtitle}
3090
- forceHideLegend
3091
- className="plot-stretch"
3092
- lazy
3093
- />
3094
- ))}
3095
- </div>
3096
- ) : (
3097
- <div className="empty-box">Grafico indisponivel.</div>
3098
  )}
3099
- </>
 
 
 
3100
  ) : null}
3101
- </details>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3102
  </SectionBlock>
3103
 
3104
- <SectionBlock step="13" title="Diagnóstico de Modelo" subtitle="Resumo diagnóstico e tabelas principais do ajuste.">
3105
  <div dangerouslySetInnerHTML={{ __html: fit.diagnosticos_html || '' }} />
3106
  <div className="equation-formats-section">
3107
  <h4>Equações do Modelo</h4>
@@ -3116,7 +3473,7 @@ export default function ElaboracaoTab({ sessionId }) {
3116
  <button
3117
  type="button"
3118
  className="btn-download-subtle"
3119
- onClick={() => onDownloadTableCsv(fit.tabela_coef, 'secao13_coeficientes')}
3120
  disabled={loading || downloadingAssets || !fit.tabela_coef}
3121
  >
3122
  Coeficientes
@@ -3124,7 +3481,7 @@ export default function ElaboracaoTab({ sessionId }) {
3124
  <button
3125
  type="button"
3126
  className="btn-download-subtle"
3127
- onClick={() => onDownloadTableCsv(fit.tabela_obs_calc, 'secao13_obs_calc')}
3128
  disabled={loading || downloadingAssets || !fit.tabela_obs_calc}
3129
  >
3130
  Obs x Calc
@@ -3133,8 +3490,8 @@ export default function ElaboracaoTab({ sessionId }) {
3133
  type="button"
3134
  className="btn-download-subtle"
3135
  onClick={() => onDownloadTablesCsvBatch([
3136
- { table: fit.tabela_coef, fileNameBase: 'secao13_coeficientes' },
3137
- { table: fit.tabela_obs_calc, fileNameBase: 'secao13_obs_calc' },
3138
  ])}
3139
  disabled={loading || downloadingAssets || (!fit.tabela_coef && !fit.tabela_obs_calc)}
3140
  >
@@ -3153,13 +3510,13 @@ export default function ElaboracaoTab({ sessionId }) {
3153
  </div>
3154
  </SectionBlock>
3155
 
3156
- <SectionBlock step="14" title="Gráficos de Diagnóstico do Modelo" subtitle="Obs x calc, resíduos, histograma, Cook e correlação.">
3157
  <div className="download-actions-bar">
3158
  <span className="download-actions-label">Fazer download:</span>
3159
  <button
3160
  type="button"
3161
  className="btn-download-subtle"
3162
- onClick={() => onDownloadFigurePng(fit.grafico_obs_calc, 'secao14_obs_calc')}
3163
  disabled={loading || downloadingAssets || !fit.grafico_obs_calc}
3164
  >
3165
  Obs x calc
@@ -3167,7 +3524,7 @@ export default function ElaboracaoTab({ sessionId }) {
3167
  <button
3168
  type="button"
3169
  className="btn-download-subtle"
3170
- onClick={() => onDownloadFigurePng(fit.grafico_residuos, 'secao14_residuos')}
3171
  disabled={loading || downloadingAssets || !fit.grafico_residuos}
3172
  >
3173
  Resíduos
@@ -3175,7 +3532,7 @@ export default function ElaboracaoTab({ sessionId }) {
3175
  <button
3176
  type="button"
3177
  className="btn-download-subtle"
3178
- onClick={() => onDownloadFigurePng(fit.grafico_histograma, 'secao14_histograma')}
3179
  disabled={loading || downloadingAssets || !fit.grafico_histograma}
3180
  >
3181
  Histograma
@@ -3183,7 +3540,7 @@ export default function ElaboracaoTab({ sessionId }) {
3183
  <button
3184
  type="button"
3185
  className="btn-download-subtle"
3186
- onClick={() => onDownloadFigurePng(fit.grafico_cook, 'secao14_cook', { forceHideLegend: true })}
3187
  disabled={loading || downloadingAssets || !fit.grafico_cook}
3188
  >
3189
  Cook
@@ -3191,7 +3548,7 @@ export default function ElaboracaoTab({ sessionId }) {
3191
  <button
3192
  type="button"
3193
  className="btn-download-subtle"
3194
- onClick={() => onDownloadFigurePng(fit.grafico_correlacao, 'secao14_correlacao')}
3195
  disabled={loading || downloadingAssets || !fit.grafico_correlacao}
3196
  >
3197
  Correlação
@@ -3200,11 +3557,11 @@ export default function ElaboracaoTab({ sessionId }) {
3200
  type="button"
3201
  className="btn-download-subtle"
3202
  onClick={() => onDownloadFiguresPngBatch([
3203
- { figure: fit.grafico_obs_calc, fileNameBase: 'secao14_obs_calc' },
3204
- { figure: fit.grafico_residuos, fileNameBase: 'secao14_residuos' },
3205
- { figure: fit.grafico_histograma, fileNameBase: 'secao14_histograma' },
3206
- { figure: fit.grafico_cook, fileNameBase: 'secao14_cook', forceHideLegend: true },
3207
- { figure: fit.grafico_correlacao, fileNameBase: 'secao14_correlacao' },
3208
  ])}
3209
  disabled={loading || downloadingAssets || (!fit.grafico_obs_calc && !fit.grafico_residuos && !fit.grafico_histograma && !fit.grafico_cook && !fit.grafico_correlacao)}
3210
  >
@@ -3222,21 +3579,29 @@ export default function ElaboracaoTab({ sessionId }) {
3222
  </div>
3223
  </SectionBlock>
3224
 
3225
- <SectionBlock step="15" title="Analisar Outliers" subtitle="Métricas para identificação de observações influentes.">
3226
  <div className="download-actions-bar">
3227
  <button
3228
  type="button"
3229
  className="btn-download-subtle"
3230
- onClick={() => onDownloadTableCsv(fit.tabela_metricas, 'secao15_tabela_metricas')}
3231
  disabled={loading || downloadingAssets || !fit.tabela_metricas}
3232
  >
3233
  Fazer download
3234
  </button>
3235
  </div>
3236
- <DataTable table={fit.tabela_metricas} maxHeight={320} />
 
 
 
 
 
 
 
 
3237
  </SectionBlock>
3238
 
3239
- <SectionBlock step="16" title="Exclusão ou Reinclusão de Outliers" subtitle="Filtre índices, revise e atualize o modelo.">
3240
  {outliersAnteriores.length > 0 && outliersHtml ? (
3241
  <div className="outliers-html-box" dangerouslySetInnerHTML={{ __html: outliersHtml }} />
3242
  ) : null}
@@ -3272,13 +3637,15 @@ export default function ElaboracaoTab({ sessionId }) {
3272
  ))}
3273
  </select>
3274
  <input
3275
- type="number"
3276
- value={filtro.valor}
 
3277
  onChange={(e) => {
3278
  const next = [...filtros]
3279
- next[idx] = { ...next[idx], valor: Number(e.target.value) }
3280
  setFiltros(next)
3281
  }}
 
3282
  />
3283
  <button
3284
  type="button"
@@ -3293,8 +3660,8 @@ export default function ElaboracaoTab({ sessionId }) {
3293
  </div>
3294
  <div className="outlier-actions-row">
3295
  <button onClick={onApplyOutlierFilters} disabled={loading}>Aplicar filtros</button>
 
3296
  <button type="button" className="btn-filtro-add" onClick={onAddFiltro} disabled={loading}>Adicionar filtro</button>
3297
- {outlierFiltrosPendentes ? <span className="pending-apply-note">{pendingWarningText}</span> : null}
3298
  </div>
3299
  </div>
3300
 
@@ -3315,70 +3682,101 @@ export default function ElaboracaoTab({ sessionId }) {
3315
  <button onClick={onRestartIteration} disabled={loading} className="btn-reiniciar-iteracao">
3316
  Atualizar Modelo (Excluir/Reincluir Outliers)
3317
  </button>
3318
- {outlierTextosPendentes ? <span className="pending-apply-note">{pendingWarningText}</span> : null}
3319
  </div>
3320
  </div>
3321
  <div className="resumo-outliers-box">Iteração: {iteracao} | {resumoOutliers}</div>
3322
  <div className="resumo-outliers-box">Outliers anteriores: {joinSelection(outliersAnteriores) || '-'}</div>
3323
  </SectionBlock>
3324
 
3325
- <SectionBlock step="17" title="Avaliação de Imóvel" subtitle="Cálculo individual e comparação entre avaliações.">
3326
- <div className="avaliacao-grid" key={`avaliacao-grid-elab-${avaliacaoFormVersion}`}>
3327
- {camposAvaliacao.map((campo) => (
3328
- <div key={`aval-${campo.coluna}`} className="avaliacao-card">
3329
- <label>{campo.coluna}</label>
3330
- {campo.tipo === 'dicotomica' ? (
3331
- <select
3332
- defaultValue={String(valoresAvaliacaoRef.current[campo.coluna] ?? '')}
3333
- onChange={(e) => {
3334
- valoresAvaliacaoRef.current[campo.coluna] = e.target.value
3335
- setAvaliacaoPendente(true)
3336
- }}
3337
- >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3338
  <option value="">Selecione</option>
3339
- {(campo.opcoes || [0, 1]).map((opcao) => (
3340
- <option key={`op-${campo.coluna}-${opcao}`} value={String(opcao)}>
3341
- {opcao}
3342
- </option>
3343
  ))}
3344
  </select>
3345
- ) : (
3346
- <input
3347
- type="number"
3348
- defaultValue={valoresAvaliacaoRef.current[campo.coluna] ?? ''}
3349
- placeholder={campo.placeholder || ''}
3350
- onChange={(e) => {
3351
- valoresAvaliacaoRef.current[campo.coluna] = e.target.value
3352
- setAvaliacaoPendente(true)
3353
- }}
3354
- />
3355
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3356
  </div>
3357
- ))}
3358
- </div>
3359
- <div className="row-wrap avaliacao-actions-row">
3360
- <button onClick={onCalculateAvaliacao} disabled={loading}>Calcular</button>
3361
- <button onClick={onClearAvaliacao} disabled={loading}>Limpar</button>
3362
- <button onClick={onExportAvaliacoes} disabled={loading}>Exportar avaliações (Excel)</button>
3363
- {avaliacaoPendente ? <span className="pending-apply-note">{pendingWarningText}</span> : null}
3364
- </div>
3365
- <div className="row avaliacao-base-row">
3366
- <label>Base comparação</label>
3367
- <select value={baseValue || ''} onChange={(e) => onBaseChange(e.target.value)}>
3368
- <option value="">Selecione</option>
3369
- {baseChoices.map((choice) => (
3370
- <option key={`base-${choice}`} value={choice}>{choice}</option>
3371
- ))}
3372
- </select>
3373
  </div>
3374
- <div
3375
- className="avaliacao-resultado-box"
3376
- onClick={onAvaliacaoResultadoClick}
3377
- dangerouslySetInnerHTML={{ __html: resultadoAvaliacaoHtml }}
3378
- />
3379
  </SectionBlock>
3380
 
3381
- <SectionBlock step="18" title="Exportar Modelo" subtitle="Geração do pacote .dai e download da base tratada.">
3382
  <div className="row">
3383
  <label>Nome do arquivo (.dai)</label>
3384
  <input type="text" value={nomeArquivoExport} onChange={(e) => setNomeArquivoExport(e.target.value)} />
@@ -3397,6 +3795,7 @@ export default function ElaboracaoTab({ sessionId }) {
3397
  </SectionBlock>
3398
  </>
3399
  ) : null}
 
3400
 
3401
  {disabledHint ? (
3402
  <div
 
38
 
39
  function defaultFiltros() {
40
  return [
41
+ { variavel: 'Resíduo Pad.', operador: '<=', valor: '-2' },
42
+ { variavel: 'Resíduo Pad.', operador: '>=', valor: '2' },
43
  ]
44
  }
45
 
 
82
  filtros.map((item) => ({
83
  variavel: String(item?.variavel || ''),
84
  operador: String(item?.operador || ''),
85
+ valor: String(item?.valor ?? '').trim(),
86
  })),
87
  )
88
  }
 
94
  })
95
  }
96
 
97
+ function toFiniteNumber(value) {
98
+ if (typeof value === 'number') {
99
+ return Number.isFinite(value) ? value : null
100
+ }
101
+ const raw = String(value ?? '').trim()
102
+ if (!raw) return null
103
+ const normalized = raw.replace(/\s+/g, '').replace(',', '.')
104
+ const parsed = Number(normalized)
105
+ return Number.isFinite(parsed) ? parsed : null
106
+ }
107
+
108
+ function matchesOutlierFilter(rowValue, operador, filtroValor) {
109
+ const left = toFiniteNumber(rowValue)
110
+ const right = toFiniteNumber(filtroValor)
111
+ if (left === null || right === null) return false
112
+ if (operador === '<=') return left <= right
113
+ if (operador === '>=') return left >= right
114
+ if (operador === '<') return left < right
115
+ if (operador === '>') return left > right
116
+ if (operador === '=') return left === right
117
+ return false
118
+ }
119
+
120
+ function formatMetric4(value) {
121
+ const num = Number(value)
122
+ return Number.isFinite(num) ? num.toFixed(4) : '-'
123
+ }
124
+
125
  function sleep(ms) {
126
  return new Promise((resolve) => {
127
  window.setTimeout(resolve, ms)
 
525
  }
526
  }
527
 
528
+ function buildArquivoCarregadoInfo(resp, options = {}) {
529
+ const fileName = String(options.fileName || '').trim()
530
+ const fileNameLower = fileName.toLowerCase()
531
+ const tipo = String(resp?.tipo || '').toLowerCase()
532
+ const isDai = tipo === 'dai' || fileNameLower.endsWith('.dai')
533
+ if (isDai) return null
534
+
535
+ const extMatch = fileName.match(/\.([^.]+)$/)
536
+ const tipoArquivo = extMatch?.[1] ? String(extMatch[1]).toUpperCase() : '-'
537
+ const sheetName = String(options.sheetName || resp?.sheet_selected || '').trim()
538
+ const totalLinhas = Array.isArray(resp?.dados?.rows) ? resp.dados.rows.length : null
539
+ const totalColunas = Array.isArray(resp?.dados?.columns) ? resp.dados.columns.length : null
540
+ const totalAbas = Array.isArray(resp?.sheets) ? resp.sheets.length : 0
541
+
542
+ if (!fileName && totalLinhas === null && totalColunas === null && !sheetName) return null
543
+
544
+ return {
545
+ nome_arquivo: fileName || '-',
546
+ tipo_arquivo: tipoArquivo,
547
+ aba: sheetName,
548
+ total_linhas: Number.isFinite(totalLinhas) ? Number(totalLinhas) : null,
549
+ total_colunas: Number.isFinite(totalColunas) ? Number(totalColunas) : null,
550
+ aguardando_aba: Boolean(resp?.requires_sheet),
551
+ total_abas: totalAbas > 0 ? totalAbas : null,
552
+ }
553
+ }
554
+
555
  function formatGeoColLabel(coluna, kind = 'default') {
556
  const valor = String(coluna || '')
557
  if (kind === 'cdlog' && valor.toUpperCase() === 'CTM') return 'CTM'
 
593
  const [loading, setLoading] = useState(false)
594
  const [downloadingAssets, setDownloadingAssets] = useState(false)
595
  const [error, setError] = useState('')
596
+ const [importacaoErro, setImportacaoErro] = useState('')
597
+ const [arquivoCarregadoInfo, setArquivoCarregadoInfo] = useState(null)
598
 
599
  const [uploadedFile, setUploadedFile] = useState(null)
600
  const [uploadDragOver, setUploadDragOver] = useState(false)
601
+ const [modeloLoadSource, setModeloLoadSource] = useState('')
602
  const [requiresSheet, setRequiresSheet] = useState(false)
603
  const [sheetOptions, setSheetOptions] = useState([])
604
  const [selectedSheet, setSelectedSheet] = useState('')
 
653
 
654
  const [transformacaoY, setTransformacaoY] = useState('(x)')
655
  const [transformacoesX, setTransformacoesX] = useState({})
656
+ const [manualTransformPreview, setManualTransformPreview] = useState(null)
657
+ const [manualTransformPreviewLoading, setManualTransformPreviewLoading] = useState(false)
658
  const [transformacoesAplicadas, setTransformacoesAplicadas] = useState(null)
659
  const [origemTransformacoes, setOrigemTransformacoes] = useState(null)
 
660
  const [section10ManualOpen, setSection10ManualOpen] = useState(false)
 
661
  const [section6EditOpen, setSection6EditOpen] = useState(true)
662
 
663
  const [fit, setFit] = useState(null)
 
673
  const valoresAvaliacaoRef = useRef({})
674
  const [avaliacaoFormVersion, setAvaliacaoFormVersion] = useState(0)
675
  const [avaliacaoPendente, setAvaliacaoPendente] = useState(false)
676
+ const [confirmarLimpezaAvaliacoes, setConfirmarLimpezaAvaliacoes] = useState(false)
677
  const [resultadoAvaliacaoHtml, setResultadoAvaliacaoHtml] = useState('')
678
  const [baseChoices, setBaseChoices] = useState([])
679
  const [baseValue, setBaseValue] = useState('')
 
689
  const [outlierTextosAplicadosSnapshot, setOutlierTextosAplicadosSnapshot] = useState(() => buildOutlierTextSnapshot('', ''))
690
  const marcarTodasXRef = useRef(null)
691
  const classificarXReqRef = useRef(0)
692
+ const manualPreviewReqRef = useRef(0)
693
  const deleteConfirmTimersRef = useRef({})
694
  const uploadInputRef = useRef(null)
695
+ const elaboracaoRootRef = useRef(null)
696
  const [disabledHint, setDisabledHint] = useState(null)
697
+ const [sectionsMountKey, setSectionsMountKey] = useState(0)
698
 
699
  const mapaChoices = useMemo(() => ['Visualização Padrão', ...colunasNumericas], [colunasNumericas])
700
  const colunasXDisponiveis = useMemo(
 
738
  () => formatPeriodoDadosMercado(periodoDadosMercadoPreview),
739
  [periodoDadosMercadoPreview],
740
  )
 
741
  const semAlteracaoTooltipText = 'Não houve modificação para ser aplicada.'
742
  const dataMercadoPendente = useMemo(
743
  () => String(colunaDataMercado || '') !== String(colunaDataMercadoAplicada || ''),
 
825
  () => Boolean(fit) && outlierTextosSnapshotAtual !== outlierTextosAplicadosSnapshot,
826
  [fit, outlierTextosSnapshotAtual, outlierTextosAplicadosSnapshot],
827
  )
828
+ const outlierHighlightIndexColumn = useMemo(() => {
829
+ const cols = fit?.tabela_metricas?.columns || []
830
+ if (cols.includes('Índice')) return 'Índice'
831
+ if (cols.includes('Indice')) return 'Indice'
832
+ return '_index'
833
+ }, [fit?.tabela_metricas?.columns])
834
+ const outlierRowsHighlight = useMemo(() => {
835
+ const rows = fit?.tabela_metricas?.rows
836
+ if (!Array.isArray(rows) || rows.length === 0) return []
837
+
838
+ const filtrosValidos = (filtros || [])
839
+ .map((item) => ({
840
+ variavel: String(item?.variavel || '').trim(),
841
+ operador: String(item?.operador || '').trim(),
842
+ valor: item?.valor,
843
+ }))
844
+ .filter((item) => item.variavel && OPERADORES.includes(item.operador) && toFiniteNumber(item.valor) !== null)
845
+
846
+ if (filtrosValidos.length === 0) return []
847
+
848
+ const indices = new Set()
849
+ rows.forEach((row) => {
850
+ if (!row || typeof row !== 'object') return
851
+ const match = filtrosValidos.some((filtro) => (
852
+ Object.prototype.hasOwnProperty.call(row, filtro.variavel)
853
+ && matchesOutlierFilter(row[filtro.variavel], filtro.operador, filtro.valor)
854
+ ))
855
+ if (!match) return
856
+ const indice = row[outlierHighlightIndexColumn]
857
+ if (indice == null) return
858
+ const indiceTexto = String(indice).trim()
859
+ if (!indiceTexto) return
860
+ indices.add(indiceTexto)
861
+ })
862
+
863
+ return Array.from(indices)
864
+ }, [fit?.tabela_metricas, filtros, outlierHighlightIndexColumn])
865
  const transformacaoAplicadaYBadge = useMemo(
866
  () => formatTransformacaoBadge(transformacoesAplicadas?.transformacao_y),
867
  [transformacoesAplicadas],
 
916
  () => Math.max(1, Math.min(3, graficosSecao12.length || 1)),
917
  [graficosSecao12.length],
918
  )
919
+ const temAvaliacoes = useMemo(
920
+ () => Array.isArray(baseChoices) && baseChoices.length > 0,
921
+ [baseChoices],
922
+ )
923
  const showCoordsPanel = Boolean(
924
  coordsInfo && (
925
  !coordsInfo.tem_coords ||
 
1038
  }
1039
  }, [sessionId, tipoFonteDados, colunasX, colunasXDisponiveis])
1040
 
1041
+ useEffect(() => {
1042
+ if (!sessionId || !selection || !colunaY || colunasX.length === 0) {
1043
+ setManualTransformPreview(null)
1044
+ setManualTransformPreviewLoading(false)
1045
+ return undefined
1046
+ }
1047
+
1048
+ manualPreviewReqRef.current += 1
1049
+ const requestId = manualPreviewReqRef.current
1050
+ let ativo = true
1051
+
1052
+ const timerId = window.setTimeout(() => {
1053
+ setManualTransformPreviewLoading(true)
1054
+ api.previewTransformElab(sessionId, transformacaoY, transformacoesX)
1055
+ .then((resp) => {
1056
+ if (!ativo || requestId !== manualPreviewReqRef.current) return
1057
+ setManualTransformPreview(resp || null)
1058
+ })
1059
+ .catch(() => {
1060
+ if (!ativo || requestId !== manualPreviewReqRef.current) return
1061
+ setManualTransformPreview(null)
1062
+ })
1063
+ .finally(() => {
1064
+ if (!ativo || requestId !== manualPreviewReqRef.current) return
1065
+ setManualTransformPreviewLoading(false)
1066
+ })
1067
+ }, 220)
1068
+
1069
+ return () => {
1070
+ ativo = false
1071
+ window.clearTimeout(timerId)
1072
+ }
1073
+ }, [sessionId, selection, colunaY, colunasX, transformacaoY, transformacoesX])
1074
+
1075
  useEffect(() => {
1076
  let ativo = true
1077
  if (!sessionId) return () => {
 
1123
  }
1124
  }, [sessionId])
1125
 
1126
+ async function withBusy(fn, options = {}) {
1127
  setLoading(true)
1128
  setError('')
1129
  try {
1130
  await fn()
1131
  } catch (err) {
1132
+ const message = err?.message || 'Erro inesperado'
1133
+ setError(message)
1134
+ if (typeof options.onError === 'function') {
1135
+ options.onError(err)
1136
+ }
1137
  } finally {
1138
  setLoading(false)
1139
  }
 
1180
 
1181
  async function carregarModelosRepositorio() {
1182
  setRepoModelosLoading(true)
1183
+ setImportacaoErro('')
1184
  try {
1185
  const resp = await api.elaboracaoRepositorioModelos()
1186
  aplicarRespostaModelosRepositorio(resp)
1187
  } catch (err) {
1188
  setError(err.message || 'Falha ao carregar modelos do repositório.')
1189
+ setImportacaoErro(err.message || 'Falha ao carregar modelos do repositório.')
1190
  setRepoModelos([])
1191
  setRepoModeloSelecionado('')
1192
  setRepoFonteModelos('')
 
1199
  const resetXSelection = Boolean(options.resetXSelection)
1200
  const colunaYPadrao = String(resp.coluna_y_padrao || '')
1201
  setDataMercadoError('')
 
1202
  if (resp.dados) setDados(resp.dados)
1203
  if (resp.mapa_html) setMapaHtml(resp.mapa_html)
1204
  if (resp.colunas_numericas) setColunasNumericas(resp.colunas_numericas)
 
1254
  setSelectionAppliedSnapshot(buildSelectionSnapshot({ coluna_y: '' }))
1255
  setTransformacaoY('(x)')
1256
  setTransformacoesX({})
1257
+ setManualTransformPreview(null)
1258
+ setManualTransformPreviewLoading(false)
1259
  setTransformacoesAplicadas(null)
1260
  setOrigemTransformacoes(null)
1261
  setBuscaTransformAppliedSnapshot(buildGrauSnapshot(grauCoef, grauF))
 
1307
  function applySelectionResponse(resp) {
1308
  setSelection(resp)
1309
  setSection6EditOpen(false)
 
1310
  setSection10ManualOpen(false)
1311
+ setManualTransformPreview(null)
1312
+ setManualTransformPreviewLoading(false)
1313
  setTransformacoesAplicadas(null)
1314
  setOrigemTransformacoes(null)
1315
  const transformacaoYAplicada = resp.transformacao_y || transformacaoY
 
1346
 
1347
  function applyFitResponse(resp, origemMeta = null) {
1348
  setFit(resp)
 
1349
  const transformacaoYAplicada = resp.transformacao_y || transformacaoY
1350
  const transformacoesXAplicadas = resp.transformacoes_x || transformacoesX
1351
  if (resp.transformacao_y) {
 
1380
  valoresAvaliacaoRef.current = init
1381
  setAvaliacaoFormVersion((prev) => prev + 1)
1382
  setAvaliacaoPendente(false)
1383
+ setConfirmarLimpezaAvaliacoes(false)
1384
  setResultadoAvaliacaoHtml('')
1385
  setBaseChoices([])
1386
  setBaseValue('')
1387
  }
1388
 
1389
+ function aplicarRespostaCarregamento(resp, tipoFonteFallback = 'tabular', meta = {}) {
1390
+ setSectionsMountKey((prev) => prev + 1)
1391
  setManualMapError('')
1392
  setGeoProcessError('')
1393
  setGeoStatusHtml('')
 
1396
  setElaborador(resp.elaborador || null)
1397
  setModeloCarregadoInfo(buildLoadedModelInfo(resp))
1398
  setAvaliadorSelecionado(resp.elaborador?.nome_completo || '')
1399
+ const fileName = String(meta.fileName || uploadedFile?.name || arquivoCarregadoInfo?.nome_arquivo || '').trim()
1400
+ const sheetName = String(meta.sheetName || resp?.sheet_selected || '').trim()
1401
+ if (Boolean(resp.requires_sheet)) {
1402
+ setArquivoCarregadoInfo(null)
1403
+ } else {
1404
+ const infoArquivo = buildArquivoCarregadoInfo(resp, { fileName, sheetName })
1405
+ setArquivoCarregadoInfo(infoArquivo)
1406
+ }
1407
+ setImportacaoErro('')
1408
  setRequiresSheet(Boolean(resp.requires_sheet))
1409
  setSheetOptions(resp.sheets || [])
1410
+ if (Boolean(resp.requires_sheet)) {
1411
+ setSelectedSheet('')
1412
+ } else if (Object.prototype.hasOwnProperty.call(resp, 'sheet_selected')) {
1413
+ setSelectedSheet(String(resp.sheet_selected || ''))
1414
+ }
1415
 
1416
  if (!resp.requires_sheet) {
1417
  const origemResp = String(resp.tipo || '').toLowerCase()
 
1421
  ? 'dai'
1422
  : 'tabular'
1423
  setTipoFonteDados(tipoFonte)
1424
+ setModeloLoadSource('')
1425
  const resetXSelection = String(resp.tipo || '').toLowerCase() !== 'dai'
1426
  applyBaseResponse(resp, { resetXSelection })
1427
  return
1428
  }
1429
 
1430
+ setTipoFonteDados('tabular')
1431
+ setColunaY('')
1432
+ setColunaYDraft('')
1433
+ setSelection(null)
1434
+ setFit(null)
1435
+ setSelectionAppliedSnapshot(buildSelectionSnapshot())
1436
+ setColunasX([])
1437
+ setDicotomicas([])
1438
+ setCodigoAlocado([])
1439
+ setPercentuais([])
1440
+ setTransformacaoY('(x)')
1441
+ setTransformacoesX({})
1442
+ setTransformacoesAplicadas(null)
1443
+ setOrigemTransformacoes(null)
1444
+ setBuscaTransformAppliedSnapshot(buildGrauSnapshot(0, 0))
1445
+ setManualTransformAppliedSnapshot(buildTransformacoesSnapshot('(x)', {}))
1446
+ setColunasDataMercado([])
1447
+ setColunaDataMercadoSugerida('')
1448
+ setColunaDataMercado('')
1449
+ setColunaDataMercadoAplicada('')
1450
+ setPeriodoDadosMercado(null)
1451
+ setPeriodoDadosMercadoPreview(null)
1452
+ setDataMercadoError('')
1453
+ setFiltros(defaultFiltros())
1454
+ setOutliersTexto('')
1455
+ setReincluirTexto('')
1456
+ setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
1457
+ setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
1458
+ setCamposAvaliacao([])
1459
+ valoresAvaliacaoRef.current = {}
1460
+ setAvaliacaoFormVersion((prev) => prev + 1)
1461
+ setAvaliacaoPendente(false)
1462
+ setConfirmarLimpezaAvaliacoes(false)
1463
+ setResultadoAvaliacaoHtml('')
 
 
1464
  }
1465
 
1466
  async function onUploadClick(arquivo = null) {
1467
  const arquivoUpload = arquivo || uploadedFile
1468
  if (!arquivoUpload || !sessionId) return
1469
+ setModeloLoadSource('upload')
1470
+ setImportacaoErro('')
1471
  await withBusy(async () => {
1472
  setMapaGerado(false)
1473
  setMapaHtml('')
1474
  setGeoAuto200(true)
1475
+ setSelectedSheet('')
1476
+ setRequiresSheet(false)
1477
+ setSheetOptions([])
1478
  const nomeArquivo = String(arquivoUpload?.name || '').toLowerCase()
1479
  const uploadEhDai = nomeArquivo.endsWith('.dai')
1480
  setTipoFonteDados(uploadEhDai ? 'dai' : 'tabular')
1481
  const resp = await api.uploadElaboracaoFile(sessionId, arquivoUpload)
1482
+ aplicarRespostaCarregamento(resp, uploadEhDai ? 'dai' : 'tabular', {
1483
+ source: 'upload',
1484
+ fileName: String(arquivoUpload?.name || ''),
1485
+ })
1486
+ }, {
1487
+ onError: (err) => setImportacaoErro(err?.message || 'Falha ao carregar arquivo.'),
1488
  })
1489
  }
1490
 
1491
  async function onCarregarModeloRepositorio() {
1492
  if (!sessionId || !repoModeloSelecionado) return
1493
+ setModeloLoadSource('repo')
1494
+ setImportacaoErro('')
1495
+ setArquivoCarregadoInfo(null)
1496
  await withBusy(async () => {
1497
  setMapaGerado(false)
1498
  setMapaHtml('')
1499
  setGeoAuto200(true)
1500
+ setSelectedSheet('')
1501
+ setRequiresSheet(false)
1502
+ setSheetOptions([])
1503
  setTipoFonteDados('dai')
1504
  const resp = await api.elaboracaoRepositorioCarregar(sessionId, repoModeloSelecionado)
1505
+ aplicarRespostaCarregamento(resp, 'dai', { source: 'repo' })
1506
  setUploadedFile(null)
1507
+ }, {
1508
+ onError: (err) => setImportacaoErro(err?.message || 'Falha ao carregar modelo do repositório.'),
1509
  })
1510
  }
1511
 
1512
  function onUploadInputChange(event) {
1513
  const input = event.target
1514
  const file = input.files?.[0] ?? null
1515
+ setModeloLoadSource('upload')
1516
  setUploadedFile(file)
1517
+ setImportacaoErro('')
1518
  input.value = ''
1519
  if (!file || loading) return
1520
  void onUploadClick(file)
 
1538
  setUploadDragOver(false)
1539
  const file = event.dataTransfer?.files?.[0]
1540
  if (!file || loading) return
1541
+ setModeloLoadSource('upload')
1542
  setUploadedFile(file)
1543
+ setImportacaoErro('')
1544
  void onUploadClick(file)
1545
  }
1546
 
1547
  async function onConfirmSheet() {
1548
  if (!selectedSheet || !sessionId) return
1549
+ setImportacaoErro('')
1550
  await withBusy(async () => {
1551
  setMapaGerado(false)
1552
  setMapaHtml('')
 
1562
  setModeloCarregadoInfo(null)
1563
  setAvaliadorSelecionado(resp.elaborador?.nome_completo || '')
1564
  setRequiresSheet(false)
1565
+ setArquivoCarregadoInfo(buildArquivoCarregadoInfo(resp, {
1566
+ fileName: String(uploadedFile?.name || arquivoCarregadoInfo?.nome_arquivo || ''),
1567
+ sheetName: selectedSheet,
1568
+ }))
1569
  applyBaseResponse(resp, { resetXSelection: true })
1570
+ setSectionsMountKey((prev) => prev + 1)
1571
+ setModeloLoadSource('')
1572
+ }, {
1573
+ onError: (err) => setImportacaoErro(err?.message || 'Falha ao confirmar aba do arquivo.'),
1574
  })
1575
  }
1576
 
 
1578
  const coluna = String(value || '')
1579
  setColunaDataMercado(coluna)
1580
  setDataMercadoError('')
 
1581
 
1582
  if (!sessionId || !coluna) {
1583
  setPeriodoDadosMercadoPreview(null)
 
1608
  setPeriodoDadosMercado(periodo)
1609
  setPeriodoDadosMercadoPreview(periodo)
1610
  setDataMercadoError('')
 
1611
  setModeloCarregadoInfo((prev) => {
1612
  if (!prev) return prev
1613
  return {
 
1667
  setGeoProcessError('')
1668
  try {
1669
  const resp = await api.mapCoords(sessionId, manualLat, manualLon)
 
1670
  setMapaHtml(resp.mapa_html)
1671
  setDados(resp.dados)
1672
  setCoordsInfo(resp.coords)
 
1728
  if (!sessionId) return
1729
  await withBusy(async () => {
1730
  const resp = await api.geocodificarReiniciar(sessionId)
 
1731
  setGeoStatusHtml(resp.status_html || '')
1732
  setGeoFalhasHtml(resp.falhas_html || '')
1733
  setGeoCorrecoes(parseCorrecoes(resp.falhas_para_correcao))
 
1748
  if (!sessionId) return
1749
  await withBusy(async () => {
1750
  const resp = await api.geocodificarExcluirCoords(sessionId)
 
1751
  setGeoStatusHtml(resp.status_html || '')
1752
  setGeoFalhasHtml(resp.falhas_html || '')
1753
  setGeoCorrecoes(parseCorrecoes(resp.falhas_para_correcao))
 
1875
  async function onApplyOutlierFilters() {
1876
  if (!sessionId) return
1877
  await withBusy(async () => {
1878
+ const filtrosValidos = (filtros || [])
1879
+ .map((item) => {
1880
+ const valorNumerico = toFiniteNumber(item?.valor)
1881
+ return {
1882
+ variavel: String(item?.variavel || '').trim(),
1883
+ operador: String(item?.operador || '').trim(),
1884
+ valor: valorNumerico,
1885
+ }
1886
+ })
1887
+ .filter((item) => item.variavel && OPERADORES.includes(item.operador) && item.valor !== null)
1888
  const resp = await api.applyOutlierFilters(sessionId, filtrosValidos)
1889
  setOutliersTexto(resp.texto || '')
1890
  setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(filtros))
1891
  })
1892
  }
1893
 
1894
+ async function onApplyOutlierFiltersRecursive() {
1895
+ if (!sessionId) return
1896
+ await withBusy(async () => {
1897
+ const filtrosValidos = (filtros || [])
1898
+ .map((item) => {
1899
+ const valorNumerico = toFiniteNumber(item?.valor)
1900
+ return {
1901
+ variavel: String(item?.variavel || '').trim(),
1902
+ operador: String(item?.operador || '').trim(),
1903
+ valor: valorNumerico,
1904
+ }
1905
+ })
1906
+ .filter((item) => item.variavel && OPERADORES.includes(item.operador) && item.valor !== null)
1907
+ const resp = await api.applyOutlierFiltersRecursive(sessionId, filtrosValidos)
1908
+ setOutliersTexto(resp.texto || '')
1909
+ setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(filtros))
1910
+ })
1911
+ }
1912
+
1913
  async function onSummaryOutliers() {
1914
  if (!sessionId) return
1915
  await withBusy(async () => {
 
1920
 
1921
  function onAddFiltro() {
1922
  const variavelPadrao = fit?.variaveis_filtro?.[0] || 'Resíduo Pad.'
1923
+ setFiltros((prev) => [...prev, { variavel: variavelPadrao, operador: '>=', valor: '0' }])
1924
  }
1925
 
1926
  function onRemoveFiltro(index) {
 
1930
  })
1931
  }
1932
 
1933
+ function aplicarPresetSecoesPosAtualizarModelo() {
1934
+ if (typeof window === 'undefined' || typeof document === 'undefined') return
1935
+ const root = elaboracaoRootRef.current || document
1936
+ const passosAbertos = new Set(['4', '11', '12'])
1937
+ const secoes = Array.from(root.querySelectorAll('.workflow-section[data-section-step]'))
1938
+
1939
+ secoes.forEach((secao) => {
1940
+ const passo = String(secao.getAttribute('data-section-step') || '')
1941
+ const botaoToggle = secao.querySelector('.section-collapse-toggle')
1942
+ if (!botaoToggle) return
1943
+ const estaColapsada = secao.classList.contains('is-collapsed')
1944
+ const deveFicarAberta = passosAbertos.has(passo)
1945
+ if (deveFicarAberta && estaColapsada) botaoToggle.click()
1946
+ if (!deveFicarAberta && !estaColapsada) botaoToggle.click()
1947
+ })
1948
+
1949
+ const realizarScroll = () => {
1950
+ const offsetTopo = 96
1951
+ const secao3 = root.querySelector('.workflow-section[data-section-step="3"]')
1952
+ if (secao3) {
1953
+ const alvo = Math.max(0, window.scrollY + secao3.getBoundingClientRect().bottom - offsetTopo)
1954
+ window.scrollTo({ top: alvo, behavior: 'smooth' })
1955
+ return
1956
+ }
1957
+
1958
+ const cabecalhoSecao4 = root.querySelector('.workflow-section[data-section-step="4"] .section-head')
1959
+ if (cabecalhoSecao4) {
1960
+ const alvo = Math.max(0, window.scrollY + cabecalhoSecao4.getBoundingClientRect().top - offsetTopo)
1961
+ window.scrollTo({ top: alvo, behavior: 'smooth' })
1962
+ return
1963
+ }
1964
+
1965
+ const secao4 = root.querySelector('.workflow-section[data-section-step="4"]')
1966
+ if (secao4) {
1967
+ const alvo = Math.max(0, window.scrollY + secao4.getBoundingClientRect().top - offsetTopo)
1968
+ window.scrollTo({ top: alvo, behavior: 'smooth' })
1969
+ return
1970
+ }
1971
+ window.scrollTo({ top: 0, behavior: 'smooth' })
1972
+ }
1973
+
1974
+ // Aguarda a atualização de layout após expandir/retrair seções.
1975
+ window.requestAnimationFrame(() => {
1976
+ window.requestAnimationFrame(() => {
1977
+ realizarScroll()
1978
+ })
1979
+ })
1980
+ }
1981
+
1982
  async function onRestartIteration() {
1983
  if (!sessionId) return
1984
  await withBusy(async () => {
 
1992
  setFit(null)
1993
  setTransformacoesAplicadas(null)
1994
  setOrigemTransformacoes(null)
1995
+ setSection10ManualOpen(true)
1996
  setResultadoAvaliacaoHtml('')
1997
  setResumoOutliers(resp.resumo_outliers || resumoOutliers)
1998
+ await sleep(1000)
1999
  if (typeof window !== 'undefined') {
2000
+ window.setTimeout(() => {
2001
+ aplicarPresetSecoesPosAtualizarModelo()
2002
+ }, 0)
2003
  }
2004
  })
2005
  }
 
2025
  valoresAvaliacaoRef.current = {}
2026
  setAvaliacaoFormVersion((prev) => prev + 1)
2027
  setAvaliacaoPendente(false)
2028
+ setConfirmarLimpezaAvaliacoes(false)
2029
  setResultadoAvaliacaoHtml('')
2030
  setOutliersHtml(resp.outliers_html || '')
2031
  setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
 
2040
  setResultadoAvaliacaoHtml(resp.resultado_html || '')
2041
  setBaseChoices(resp.base_choices || [])
2042
  setBaseValue(resp.base_value || '')
2043
+ setConfirmarLimpezaAvaliacoes(false)
2044
  setAvaliacaoPendente(false)
2045
  })
2046
  }
2047
 
2048
+ function onResetCamposAvaliacao() {
2049
+ const limpo = {}
2050
+ camposAvaliacao.forEach((campo) => {
2051
+ limpo[campo.coluna] = ''
2052
+ })
2053
+ valoresAvaliacaoRef.current = limpo
2054
+ setAvaliacaoFormVersion((prev) => prev + 1)
2055
+ setAvaliacaoPendente(false)
2056
+ }
2057
+
2058
  async function onClearAvaliacao() {
2059
  if (!sessionId) return
2060
  await withBusy(async () => {
 
2062
  setResultadoAvaliacaoHtml(resp.resultado_html || '')
2063
  setBaseChoices(resp.base_choices || [])
2064
  setBaseValue(resp.base_value || '')
2065
+ setConfirmarLimpezaAvaliacoes(false)
 
 
 
 
 
 
2066
  })
2067
  }
2068
 
 
2073
  setResultadoAvaliacaoHtml(resp.resultado_html || '')
2074
  setBaseChoices(resp.base_choices || [])
2075
  setBaseValue(resp.base_value || '')
2076
+ setConfirmarLimpezaAvaliacoes(false)
2077
  })
2078
  }
2079
 
 
2341
  }
2342
 
2343
  return (
2344
+ <div ref={elaboracaoRootRef} className="tab-content">
2345
+ <div key={`sections-${sectionsMountKey}`} className="workflow-sections-stack">
 
2346
  <SectionBlock step="1" title="Importar Dados" subtitle="Upload de CSV, Excel ou .dai com recuperação do fluxo.">
2347
  <div className="section1-groups">
2348
  <div className="subpanel section1-group">
2349
+ {!modeloLoadSource ? (
2350
+ <div className="model-source-choice-grid">
2351
+ <button
2352
+ type="button"
2353
+ className="model-source-choice-btn model-source-choice-btn-primary"
2354
+ onClick={() => {
2355
+ setModeloLoadSource('repo')
2356
+ setImportacaoErro('')
2357
+ }}
2358
+ disabled={loading}
2359
+ >
2360
+ Carregar modelo do repositório
 
 
 
 
 
 
 
 
 
 
 
2361
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2362
  <button
2363
  type="button"
2364
+ className="model-source-choice-btn model-source-choice-btn-secondary"
2365
+ onClick={() => {
2366
+ setModeloLoadSource('upload')
2367
+ setImportacaoErro('')
2368
+ }}
2369
  disabled={loading}
2370
  >
2371
+ Fazer upload de Excel ou modelo
2372
  </button>
2373
  </div>
2374
+ ) : (
2375
+ <div className="model-source-flow">
2376
+ <div className="model-source-flow-head">
2377
+ <button
2378
+ type="button"
2379
+ className="model-source-back-btn"
2380
+ onClick={() => {
2381
+ setModeloLoadSource('')
2382
+ setImportacaoErro('')
2383
+ }}
2384
+ disabled={loading}
2385
+ >
2386
+ Voltar
2387
+ </button>
2388
+ </div>
2389
 
2390
+ {modeloLoadSource === 'repo' ? (
2391
+ <div className="row upload-repo-row">
2392
+ <label>Modelo do repositório</label>
2393
+ <select
2394
+ value={repoModeloSelecionado}
2395
+ onChange={(e) => setRepoModeloSelecionado(e.target.value)}
2396
+ disabled={loading || repoModelosLoading || repoModelos.length === 0}
2397
+ >
2398
+ <option value="">
2399
+ {repoModelosLoading ? 'Carregando lista...' : repoModelos.length > 0 ? 'Selecione um modelo' : 'Nenhum modelo disponível'}
2400
+ </option>
2401
+ {repoModelos.map((item) => (
2402
+ <option key={`repo-elab-${item.id}`} value={item.id}>
2403
+ {item.nome_modelo || item.arquivo}
2404
+ </option>
2405
  ))}
2406
  </select>
2407
+ <div className="row compact upload-repo-actions">
2408
+ <button type="button" onClick={onCarregarModeloRepositorio} disabled={loading || repoModelosLoading || !repoModeloSelecionado}>
2409
+ Carregar do repositório
2410
+ </button>
2411
+ <button type="button" onClick={() => void carregarModelosRepositorio()} disabled={loading || repoModelosLoading}>
2412
+ Atualizar lista
2413
+ </button>
2414
+ </div>
2415
+ {repoFonteModelos ? <div className="section1-empty-hint">{repoFonteModelos}</div> : null}
2416
  </div>
2417
  ) : null}
2418
+
2419
+ {modeloLoadSource === 'upload' ? (
2420
+ <>
2421
+ <div
2422
+ className={`upload-dropzone${uploadDragOver ? ' is-dragover' : ''}`}
2423
+ onDragOver={onUploadDropZoneDragOver}
2424
+ onDragEnter={onUploadDropZoneDragOver}
2425
+ onDragLeave={onUploadDropZoneDragLeave}
2426
+ onDrop={onUploadDropZoneDrop}
2427
+ >
2428
+ <input
2429
+ ref={uploadInputRef}
2430
+ type="file"
2431
+ className="upload-hidden-input"
2432
+ onChange={onUploadInputChange}
2433
+ />
2434
+ <div className="row upload-dropzone-main">
2435
+ <button
2436
+ type="button"
2437
+ className="btn-upload-select"
2438
+ onClick={() => uploadInputRef.current?.click()}
2439
+ disabled={loading}
2440
+ >
2441
+ Selecionar arquivo
2442
+ </button>
2443
+ </div>
2444
+ <div className="upload-dropzone-hint">Ou arraste e solte aqui para carregar automaticamente.</div>
2445
+ </div>
2446
+
2447
+ {requiresSheet ? (
2448
+ <div className="row">
2449
+ <select value={selectedSheet} onChange={(e) => setSelectedSheet(e.target.value)}>
2450
+ <option value="">Selecione a aba</option>
2451
+ {sheetOptions.map((sheet) => (
2452
+ <option key={sheet} value={sheet}>{sheet}</option>
2453
+ ))}
2454
+ </select>
2455
+ <button onClick={onConfirmSheet} disabled={loading || !selectedSheet}>Confirmar aba</button>
2456
+ </div>
2457
+ ) : null}
2458
+ </>
2459
+ ) : null}
2460
+ </div>
2461
+ )}
2462
+
2463
+ {importacaoErro ? <div className="error-line inline-error import-feedback-line">{importacaoErro}</div> : null}
2464
  </div>
2465
 
2466
+ {(arquivoCarregadoInfo || modeloCarregadoInfo) ? (
2467
+ <div className="subpanel section1-group section1-badges-group">
2468
+ {arquivoCarregadoInfo ? (
2469
+ <>
2470
+ <h4>Informações do arquivo</h4>
2471
+ <div className="upload-file-info-card">
2472
+ <div className="upload-file-info-grid">
2473
+ <div className="upload-file-info-item">
2474
+ <span className="upload-file-info-label">Nome</span>
2475
+ <span className="upload-file-info-value">{arquivoCarregadoInfo.nome_arquivo || '-'}</span>
2476
  </div>
2477
+ <div className="upload-file-info-item">
2478
+ <span className="upload-file-info-label">Tipo</span>
2479
+ <span className="upload-file-info-value">{arquivoCarregadoInfo.tipo_arquivo || '-'}</span>
2480
+ </div>
2481
+ <div className="upload-file-info-item">
2482
+ <span className="upload-file-info-label">Aba selecionada</span>
2483
+ <span className="upload-file-info-value">{arquivoCarregadoInfo.aba || '-'}</span>
2484
+ </div>
2485
+ <div className="upload-file-info-item">
2486
+ <span className="upload-file-info-label">Linhas</span>
2487
+ <span className="upload-file-info-value">
2488
+ {Number.isFinite(Number(arquivoCarregadoInfo.total_linhas))
2489
+ ? Number(arquivoCarregadoInfo.total_linhas).toLocaleString('pt-BR')
2490
+ : '-'}
2491
+ </span>
2492
+ </div>
2493
+ <div className="upload-file-info-item">
2494
+ <span className="upload-file-info-label">Colunas</span>
2495
+ <span className="upload-file-info-value">
2496
+ {Number.isFinite(Number(arquivoCarregadoInfo.total_colunas))
2497
+ ? Number(arquivoCarregadoInfo.total_colunas).toLocaleString('pt-BR')
2498
+ : '-'}
2499
+ </span>
2500
  </div>
2501
  </div>
2502
+ </div>
2503
+ </>
2504
+ ) : null}
2505
 
2506
+ {modeloCarregadoInfo ? (
2507
+ <>
2508
+ <h4>Informações do modelo</h4>
2509
+ <div className="modelo-info-card">
2510
+ <div className="modelo-info-split">
2511
+ <div className="modelo-info-col">
2512
+ <div className="modelo-info-stack-block">
2513
+ <div className="elaborador-badge-title">NOME DO MODELO:</div>
2514
+ <div className="elaborador-badge-name">{modeloCarregadoInfo.nome_modelo || '-'}</div>
2515
  </div>
2516
+
2517
+ <div className="modelo-info-stack-block">
2518
+ <div className="elaborador-badge-title">ELABORADO POR:</div>
2519
+ {elaborador?.nome_completo ? (
2520
+ <div className="elaborador-badge-name">{elaborador.nome_completo}</div>
2521
+ ) : (
2522
+ <div className="section1-empty-hint">Elaborador não informado no arquivo.</div>
2523
+ )}
2524
+ {elaboradorMeta.length > 0 && elaborador?.nome_completo ? (
2525
+ <div className="elaborador-badge-meta">{elaboradorMeta.join(' | ')}</div>
2526
+ ) : null}
2527
+ </div>
2528
+ </div>
2529
+
2530
+ <div className="modelo-info-col modelo-info-col-vars">
2531
+ <div className="elaborador-badge-title">Variáveis selecionadas:</div>
2532
+ {modeloCarregadoInfo.coluna_y ? (
2533
+ <div className="variavel-badge-line">
2534
+ <span className="variavel-badge-label">Dependente:</span>
2535
+ <span className="variavel-chip variavel-chip-y variavel-chip-inline">
2536
+ {modeloCarregadoInfo.coluna_y}
2537
+ <span className="variavel-chip-transform">{` ${transformacaoYModeloBadge}`}</span>
2538
+ </span>
2539
+ </div>
2540
+ ) : (
2541
+ <div className="section1-empty-hint">Variável dependente não encontrada no modelo carregado.</div>
2542
+ )}
2543
+ {variaveisIndependentesModeloBadge.length > 0 ? (
2544
+ <div className="variavel-badge-line">
2545
+ <span className="variavel-badge-label">Independentes:</span>
2546
+ <div className="variavel-chip-wrap">
2547
+ {variaveisIndependentesModeloBadge.map((item) => (
2548
+ <span key={`ind-${item.coluna}`} className="variavel-chip">
2549
+ {item.coluna}
2550
+ <span className="variavel-chip-transform">{` ${item.transformacao}`}</span>
2551
+ </span>
2552
+ ))}
2553
+ </div>
2554
  </div>
2555
+ ) : (
2556
+ <div className="section1-empty-hint">Sem variáveis independentes no modelo carregado.</div>
2557
+ )}
2558
+ <div className="variavel-badge-line">
2559
+ <span className="variavel-badge-label">Período dados:</span>
2560
+ <span className="variavel-badge-value">{periodoModeloCarregadoTexto}</span>
2561
  </div>
 
 
 
 
 
 
2562
  </div>
2563
  </div>
2564
  </div>
2565
+ </>
2566
+ ) : null}
2567
+ </div>
2568
+ ) : null}
 
2569
  </div>
2570
  </SectionBlock>
2571
 
 
2628
  <button
2629
  onClick={() => {
2630
  setCoordsMode('skipped')
 
2631
  }}
2632
  disabled={loading}
2633
  >
 
2836
 
2837
  <SectionBlock
2838
  step="3"
2839
+ title="Visualizar Mapa"
2840
+ subtitle="Mapa interativo da base carregada."
2841
  >
2842
+ <div className="dados-visualizacao-group">
2843
+ {!mapaGerado ? (
2844
+ <div className="empty-box">
2845
+ <div className="row">
2846
+ <button type="button" className="btn-gerar-mapa" onClick={onGerarMapa} disabled={loading}>
2847
+ Gerar Mapa
2848
+ </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2849
  </div>
2850
+ <div className="section1-empty-hint">O mapa será carregado somente após solicitação explícita.</div>
 
 
2851
  </div>
2852
+ ) : (
2853
+ <details className="dados-mapa-details" open>
2854
+ <summary>Mapa</summary>
2855
+ <div className="row compact dados-mapa-controls">
2856
+ <label>Variável no mapa</label>
2857
+ <select value={mapaVariavel} onChange={(e) => onMapVarChange(e.target.value)}>
2858
+ {mapaChoices.map((choice) => (
2859
+ <option key={choice} value={choice}>{choice}</option>
2860
+ ))}
2861
+ </select>
2862
+ </div>
2863
+ <div className="download-actions-bar">
2864
+ <button
2865
+ type="button"
2866
+ className="btn-download-subtle"
2867
+ onClick={onDownloadMapaSecao3}
2868
+ disabled={loading || downloadingAssets || !mapaHtml}
2869
+ >
2870
+ Fazer download
2871
+ </button>
2872
+ </div>
2873
+ <MapFrame html={mapaHtml} />
2874
+ </details>
2875
+ )}
2876
+ </div>
2877
+ </SectionBlock>
2878
+
2879
+ <SectionBlock
2880
+ step="4"
2881
+ title="Visualizar Dados de Mercado"
2882
+ subtitle="Tabela prévia da base carregada."
2883
+ >
2884
+ <div className="dados-visualizacao-group">
2885
+ <div className="dados-outliers-resumo">
2886
+ <div className="resumo-outliers-box">
2887
+ {outliersAnteriores.length > 0
2888
+ ? `Há outliers excluídos: sim (${outliersAnteriores.length})`
2889
+ : 'Há outliers excluídos: não'}
2890
  </div>
2891
+ {outliersAnteriores.length > 0 ? (
2892
+ <div className="resumo-outliers-box">Índices excluídos: {joinSelection(outliersAnteriores)}</div>
2893
+ ) : null}
2894
+ </div>
2895
+ <div className="download-actions-bar">
2896
+ <button
2897
+ type="button"
2898
+ className="btn-download-subtle"
2899
+ onClick={() => onDownloadTableCsv(dados, 'secao4_dados_mercado')}
2900
+ disabled={loading || downloadingAssets || !dados}
2901
+ >
2902
+ Fazer download
2903
+ </button>
2904
  </div>
2905
+ <DataTable table={dados} maxHeight={540} />
2906
  </div>
2907
  </SectionBlock>
2908
 
2909
  <SectionBlock
2910
+ step="5"
2911
  title="Definir Data dos Dados de Mercado"
2912
  subtitle="Selecione a coluna de data dos dados de mercado, visualize o período e aplique."
2913
  >
 
2949
  Aplicar
2950
  </button>
2951
  </span>
 
2952
  </div>
2953
  <div className="section1-empty-hint">Valor atualmente aplicado: {colunaDataMercadoAplicada || '-'}</div>
2954
  </div>
 
2956
  </div>
2957
  </SectionBlock>
2958
 
2959
+ <SectionBlock step="6" title="Selecionar Variável Dependente" subtitle="Defina a variável dependente (Y).">
2960
  <div className="row">
2961
  <label>Variável Dependente (Y)</label>
2962
  <select value={colunaYDraft} onChange={(e) => setColunaYDraft(e.target.value)}>
 
2981
  Aplicar
2982
  </button>
2983
  </span>
 
2984
  </div>
2985
  <div className="section1-empty-hint section5-applied-hint">Valor atualmente aplicado: {colunaY || '-'}</div>
2986
  </SectionBlock>
2987
 
2988
+ <SectionBlock step="7" title="Selecionar Variáveis Independentes" subtitle="Escolha regressoras e grupos de tipologia.">
2989
  {!colunaY ? (
2990
  <div className="section1-empty-hint">
2991
  Aplique a variável dependente na etapa anterior para liberar as opções de variáveis independentes.
 
3048
  </div>
3049
 
3050
  <div className="compact-option-group compact-option-group-codigo">
3051
+ <h4>Variáveis Categóricas Codificadas</h4>
3052
  <div className="checkbox-inline-wrap">
3053
  {colunasX.map((col) => (
3054
  <label key={`c-${col}`} className="compact-checkbox">
 
3080
  >
3081
  <button onClick={onApplySelection} disabled={loading || !podeAplicarSelecao}>Aplicar seleção</button>
3082
  </span>
 
3083
  </div>
3084
  </>
3085
  ) : null}
 
3112
  )}
3113
  </div>
3114
  <div className="section6-summary-group">
3115
+ <div className="section6-summary-label">Variáveis categóricas codificadas:</div>
3116
  {codigoAlocado.length > 0 ? (
3117
  <div className="checkbox-inline-wrap">
3118
  {codigoAlocado.map((coluna) => (
 
3150
 
3151
  {selection ? (
3152
  <>
3153
+ <SectionBlock step="8" title="Estatísticas das Variáveis Selecionadas" subtitle="Resumo estatístico para Y e regressoras.">
3154
  <div className="download-actions-bar">
3155
  <button
3156
  type="button"
3157
  className="btn-download-subtle"
3158
+ onClick={() => onDownloadTableCsv(selection.estatisticas, 'secao8_estatisticas')}
3159
  disabled={loading || downloadingAssets || !selection.estatisticas}
3160
  >
3161
  Fazer download
 
3164
  <DataTable table={selection.estatisticas} />
3165
  </SectionBlock>
3166
 
3167
+ <SectionBlock step="9" title="Teste de Micronumerosidade" subtitle="Validação de amostra mínima para variáveis selecionadas.">
3168
  <div dangerouslySetInnerHTML={{ __html: selection.micronumerosidade_html || '' }} />
3169
  </SectionBlock>
3170
 
3171
+ <SectionBlock step="10" title="Gráficos de Dispersão das Variáveis Independentes" subtitle="Leitura visual entre X e Y no conjunto filtrado.">
3172
+ <div className="download-actions-bar">
3173
+ {graficosSecao9.length > 1 ? <span className="download-actions-label">Fazer download:</span> : null}
3174
+ {graficosSecao9.map((item, idx) => {
3175
+ const fileBase = `secao10_${sanitizeFileName(item.label, `dispersao_${idx + 1}`)}`
3176
+ return (
3177
+ <button
3178
+ key={`s9-dl-${item.id}`}
3179
+ type="button"
3180
+ className="btn-download-subtle"
3181
+ title={item.legenda}
3182
+ onClick={() => onDownloadFigurePng(item.figure, fileBase, { forceHideLegend: true })}
3183
+ disabled={loading || downloadingAssets || !item.figure}
3184
+ >
3185
+ {graficosSecao9.length > 1 ? item.label : 'Fazer download'}
3186
+ </button>
3187
+ )
3188
+ })}
3189
+ {graficosSecao9.length > 1 ? (
3190
+ <button
3191
+ type="button"
3192
+ className="btn-download-subtle"
3193
+ onClick={() => onDownloadFiguresPngBatch(
3194
+ graficosSecao9.map((item, idx) => ({
3195
+ figure: item.figure,
3196
+ fileNameBase: `secao10_${sanitizeFileName(item.label, `dispersao_${idx + 1}`)}`,
3197
+ forceHideLegend: true,
3198
+ })),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3199
  )}
3200
+ disabled={loading || downloadingAssets || graficosSecao9.length === 0}
3201
+ >
3202
+ Todos
3203
+ </button>
3204
  ) : null}
3205
+ </div>
3206
+ {graficosSecao9.length > 0 ? (
3207
+ <div className="plot-grid-scatter" style={{ '--plot-cols': colunasGraficosSecao9 }}>
3208
+ {graficosSecao9.map((item) => (
3209
+ <PlotFigure
3210
+ key={`s9-plot-${item.id}`}
3211
+ figure={item.figure}
3212
+ title={item.title}
3213
+ subtitle={item.subtitle}
3214
+ forceHideLegend
3215
+ className="plot-stretch"
3216
+ lazy
3217
+ />
3218
+ ))}
3219
+ </div>
3220
+ ) : (
3221
+ <div className="empty-box">Grafico indisponivel.</div>
3222
+ )}
3223
  </SectionBlock>
3224
 
3225
+ <SectionBlock step="11" title="Transformações Sugeridas" subtitle="Busca automática de combinações por R² e enquadramento.">
3226
  <div className="row">
3227
  <label>Grau mínimo dos coeficientes</label>
3228
  <select value={grauCoef} onChange={(e) => setGrauCoef(Number(e.target.value))}>
 
3237
  ))}
3238
  </select>
3239
  <button onClick={onSearchTransform} disabled={loading}>Buscar transformações</button>
 
3240
  </div>
3241
  {(selection.busca?.resultados || []).length > 0 ? (
3242
  <div className="transform-suggestions-grid">
 
3245
  <div className="transform-suggestion-head">
3246
  <span className="transform-suggestion-rank">#{item.rank || idx + 1}</span>
3247
  <div className="transform-suggestion-metrics">
3248
+ <span className="transform-suggestion-r2">R² = {formatMetric4(item.r2)}</span>
3249
+ <span className="transform-suggestion-r2adj">R² ajustado = {formatMetric4(item.r2_ajustado)}</span>
3250
  <span className={grauBadgeClass(Number(item.grau_f ?? 0))}>
3251
  Teste F: {GRAU_LABEL_CURTO[Number(item.grau_f ?? 0)] || 'Sem enq.'}
3252
  </span>
 
3280
  )}
3281
  </SectionBlock>
3282
 
3283
+ <SectionBlock step="12" title="Aplicação das Transformações" subtitle="Configuração manual para ajuste do modelo.">
3284
+ <div className="manual-transform-toggle section12-toggle-wrap">
3285
  <button
3286
  type="button"
3287
  className={section10ManualOpen ? 'btn-manual-toggle active' : 'btn-manual-toggle'}
3288
  onClick={() => setSection10ManualOpen((prev) => !prev)}
3289
  >
3290
+ {section10ManualOpen ? 'Ocultar ajustes manuais de transformação' : 'Proceder com as transformações manualmente'}
3291
  </button>
3292
  </div>
3293
+ {section10ManualOpen ? (
3294
+ <div className="transform-preview-summary">
3295
+ <div className="transform-preview-summary-title">Prévia com as transformações selecionadas</div>
3296
+ {manualTransformPreview?.valido ? (
3297
+ <div className="transform-preview-summary-metrics">
3298
+ <span className="transform-suggestion-r2">R² = {formatMetric4(manualTransformPreview.r2)}</span>
3299
+ <span className="transform-suggestion-r2adj">R² ajustado = {formatMetric4(manualTransformPreview.r2_ajustado)}</span>
3300
+ <span className={grauBadgeClass(Number(manualTransformPreview.grau_f ?? 0))}>
3301
+ Teste F: {GRAU_LABEL_CURTO[Number(manualTransformPreview.grau_f ?? 0)] || 'Sem enq.'}
3302
+ </span>
3303
+ </div>
3304
+ ) : (
3305
+ <div className="section1-empty-hint">Combinação sem ajuste válido para os dados atuais.</div>
3306
+ )}
3307
+ {manualTransformPreviewLoading ? (
3308
+ <div className="section1-empty-hint">Atualizando métricas...</div>
3309
+ ) : null}
3310
+ </div>
3311
+ ) : null}
3312
 
3313
  {section10ManualOpen ? (
3314
  <>
 
 
 
 
 
 
 
 
 
3315
  <div className="transform-grid">
3316
+ <div className="transform-card transform-card-y">
3317
+ <span>{colunaY ? `${colunaY} (Y)` : 'Variável dependente (Y)'}</span>
3318
+ <select value={transformacaoY} onChange={(e) => setTransformacaoY(e.target.value)}>
3319
+ {['(x)', '1/(x)', 'ln(x)', 'exp(x)', '(x)^2', 'raiz(x)', '1/raiz(x)'].map((item) => (
3320
+ <option key={`y-${item}`} value={item}>{item}</option>
3321
+ ))}
3322
+ </select>
3323
+ </div>
3324
  {(selection.transform_fields || []).map((field) => (
3325
  <div key={`tf-${field.coluna}`} className="transform-card">
3326
+ <div className="transform-card-head">
3327
+ <span>{field.coluna}</span>
3328
+ <span className={grauBadgeClass(Number(manualTransformPreview?.graus_coef?.[field.coluna] ?? 0))}>
3329
+ {GRAU_LABEL_CURTO[Number(manualTransformPreview?.graus_coef?.[field.coluna] ?? 0)] || 'Sem enq.'}
3330
+ </span>
3331
+ </div>
3332
  <select
3333
  value={transformacoesX[field.coluna] || '(x)'}
3334
  onChange={(e) => setTransformacoesX((prev) => ({ ...prev, [field.coluna]: e.target.value }))}
 
3351
  >
3352
  <button className="btn-fit-model" onClick={onFitModel} disabled={loading || !podeAplicarTransformacaoManual}>Aplicar transformações e ajustar modelo</button>
3353
  </span>
 
3354
  </div>
3355
  </>
3356
  ) : null}
 
3396
 
3397
  {fit ? (
3398
  <>
3399
+ <SectionBlock step="13" title="Gráficos de Dispersão (Variáveis Transformadas)" subtitle="Dispersão com variáveis já transformadas.">
3400
+ <div className="row">
3401
+ <label>Tipo de dispersão</label>
3402
+ <select value={tipoDispersao} onChange={(e) => onTipoDispersaoChange(e.target.value)}>
3403
+ <option value="Variáveis Independentes Transformadas X Variável Dependente Transformada">X transformado x Y transformado</option>
3404
+ <option value="Variáveis Independentes Transformadas X Resíduo Padronizado">X transformado x Resíduo</option>
3405
+ <option value="Variáveis Independentes Não Transformadas X Resíduo Padronizado">X não transformado x Resíduo</option>
3406
+ </select>
3407
+ </div>
3408
+ <div className="download-actions-bar">
3409
+ {graficosSecao12.length > 1 ? <span className="download-actions-label">Fazer download:</span> : null}
3410
+ {graficosSecao12.map((item, idx) => {
3411
+ const fileBase = `secao13_${sanitizeFileName(item.label, `dispersao_${idx + 1}`)}`
3412
+ return (
3413
+ <button
3414
+ key={`s12-dl-${item.id}`}
3415
+ type="button"
3416
+ className="btn-download-subtle"
3417
+ title={item.legenda}
3418
+ onClick={() => onDownloadFigurePng(item.figure, fileBase, { forceHideLegend: true })}
3419
+ disabled={loading || downloadingAssets || !item.figure}
3420
+ >
3421
+ {graficosSecao12.length > 1 ? item.label : 'Fazer download'}
3422
+ </button>
3423
+ )
3424
+ })}
3425
+ {graficosSecao12.length > 1 ? (
3426
+ <button
3427
+ type="button"
3428
+ className="btn-download-subtle"
3429
+ onClick={() => onDownloadFiguresPngBatch(
3430
+ graficosSecao12.map((item, idx) => ({
3431
+ figure: item.figure,
3432
+ fileNameBase: `secao13_${sanitizeFileName(item.label, `dispersao_${idx + 1}`)}`,
3433
+ forceHideLegend: true,
3434
+ })),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3435
  )}
3436
+ disabled={loading || downloadingAssets || graficosSecao12.length === 0}
3437
+ >
3438
+ Todos
3439
+ </button>
3440
  ) : null}
3441
+ </div>
3442
+ {graficosSecao12.length > 0 ? (
3443
+ <div className="plot-grid-scatter" style={{ '--plot-cols': colunasGraficosSecao12 }}>
3444
+ {graficosSecao12.map((item) => (
3445
+ <PlotFigure
3446
+ key={`s12-plot-${item.id}`}
3447
+ figure={item.figure}
3448
+ title={item.title}
3449
+ subtitle={item.subtitle}
3450
+ forceHideLegend
3451
+ className="plot-stretch"
3452
+ lazy
3453
+ />
3454
+ ))}
3455
+ </div>
3456
+ ) : (
3457
+ <div className="empty-box">Grafico indisponivel.</div>
3458
+ )}
3459
  </SectionBlock>
3460
 
3461
+ <SectionBlock step="14" title="Diagnóstico de Modelo" subtitle="Resumo diagnóstico e tabelas principais do ajuste.">
3462
  <div dangerouslySetInnerHTML={{ __html: fit.diagnosticos_html || '' }} />
3463
  <div className="equation-formats-section">
3464
  <h4>Equações do Modelo</h4>
 
3473
  <button
3474
  type="button"
3475
  className="btn-download-subtle"
3476
+ onClick={() => onDownloadTableCsv(fit.tabela_coef, 'secao14_coeficientes')}
3477
  disabled={loading || downloadingAssets || !fit.tabela_coef}
3478
  >
3479
  Coeficientes
 
3481
  <button
3482
  type="button"
3483
  className="btn-download-subtle"
3484
+ onClick={() => onDownloadTableCsv(fit.tabela_obs_calc, 'secao14_obs_calc')}
3485
  disabled={loading || downloadingAssets || !fit.tabela_obs_calc}
3486
  >
3487
  Obs x Calc
 
3490
  type="button"
3491
  className="btn-download-subtle"
3492
  onClick={() => onDownloadTablesCsvBatch([
3493
+ { table: fit.tabela_coef, fileNameBase: 'secao14_coeficientes' },
3494
+ { table: fit.tabela_obs_calc, fileNameBase: 'secao14_obs_calc' },
3495
  ])}
3496
  disabled={loading || downloadingAssets || (!fit.tabela_coef && !fit.tabela_obs_calc)}
3497
  >
 
3510
  </div>
3511
  </SectionBlock>
3512
 
3513
+ <SectionBlock step="15" title="Gráficos de Diagnóstico do Modelo" subtitle="Obs x calc, resíduos, histograma, Cook e correlação.">
3514
  <div className="download-actions-bar">
3515
  <span className="download-actions-label">Fazer download:</span>
3516
  <button
3517
  type="button"
3518
  className="btn-download-subtle"
3519
+ onClick={() => onDownloadFigurePng(fit.grafico_obs_calc, 'secao15_obs_calc')}
3520
  disabled={loading || downloadingAssets || !fit.grafico_obs_calc}
3521
  >
3522
  Obs x calc
 
3524
  <button
3525
  type="button"
3526
  className="btn-download-subtle"
3527
+ onClick={() => onDownloadFigurePng(fit.grafico_residuos, 'secao15_residuos')}
3528
  disabled={loading || downloadingAssets || !fit.grafico_residuos}
3529
  >
3530
  Resíduos
 
3532
  <button
3533
  type="button"
3534
  className="btn-download-subtle"
3535
+ onClick={() => onDownloadFigurePng(fit.grafico_histograma, 'secao15_histograma')}
3536
  disabled={loading || downloadingAssets || !fit.grafico_histograma}
3537
  >
3538
  Histograma
 
3540
  <button
3541
  type="button"
3542
  className="btn-download-subtle"
3543
+ onClick={() => onDownloadFigurePng(fit.grafico_cook, 'secao15_cook', { forceHideLegend: true })}
3544
  disabled={loading || downloadingAssets || !fit.grafico_cook}
3545
  >
3546
  Cook
 
3548
  <button
3549
  type="button"
3550
  className="btn-download-subtle"
3551
+ onClick={() => onDownloadFigurePng(fit.grafico_correlacao, 'secao15_correlacao')}
3552
  disabled={loading || downloadingAssets || !fit.grafico_correlacao}
3553
  >
3554
  Correlação
 
3557
  type="button"
3558
  className="btn-download-subtle"
3559
  onClick={() => onDownloadFiguresPngBatch([
3560
+ { figure: fit.grafico_obs_calc, fileNameBase: 'secao15_obs_calc' },
3561
+ { figure: fit.grafico_residuos, fileNameBase: 'secao15_residuos' },
3562
+ { figure: fit.grafico_histograma, fileNameBase: 'secao15_histograma' },
3563
+ { figure: fit.grafico_cook, fileNameBase: 'secao15_cook', forceHideLegend: true },
3564
+ { figure: fit.grafico_correlacao, fileNameBase: 'secao15_correlacao' },
3565
  ])}
3566
  disabled={loading || downloadingAssets || (!fit.grafico_obs_calc && !fit.grafico_residuos && !fit.grafico_histograma && !fit.grafico_cook && !fit.grafico_correlacao)}
3567
  >
 
3579
  </div>
3580
  </SectionBlock>
3581
 
3582
+ <SectionBlock step="16" title="Analisar Outliers" subtitle="Métricas para identificação de observações influentes.">
3583
  <div className="download-actions-bar">
3584
  <button
3585
  type="button"
3586
  className="btn-download-subtle"
3587
+ onClick={() => onDownloadTableCsv(fit.tabela_metricas, 'secao16_tabela_metricas')}
3588
  disabled={loading || downloadingAssets || !fit.tabela_metricas}
3589
  >
3590
  Fazer download
3591
  </button>
3592
  </div>
3593
+ <div className="outlier-highlight-note">
3594
+ Linhas amarelas indicam observações que atendem aos filtros definidos na seção 17.
3595
+ </div>
3596
+ <DataTable
3597
+ table={fit.tabela_metricas}
3598
+ maxHeight={320}
3599
+ highlightedRowIndices={outlierRowsHighlight}
3600
+ highlightIndexColumn={outlierHighlightIndexColumn}
3601
+ />
3602
  </SectionBlock>
3603
 
3604
+ <SectionBlock step="17" title="Exclusão ou Reinclusão de Outliers" subtitle="Filtre índices, revise e atualize o modelo.">
3605
  {outliersAnteriores.length > 0 && outliersHtml ? (
3606
  <div className="outliers-html-box" dangerouslySetInnerHTML={{ __html: outliersHtml }} />
3607
  ) : null}
 
3637
  ))}
3638
  </select>
3639
  <input
3640
+ type="text"
3641
+ inputMode="decimal"
3642
+ value={String(filtro.valor ?? '')}
3643
  onChange={(e) => {
3644
  const next = [...filtros]
3645
+ next[idx] = { ...next[idx], valor: e.target.value }
3646
  setFiltros(next)
3647
  }}
3648
+ placeholder="ex: 1,99"
3649
  />
3650
  <button
3651
  type="button"
 
3660
  </div>
3661
  <div className="outlier-actions-row">
3662
  <button onClick={onApplyOutlierFilters} disabled={loading}>Aplicar filtros</button>
3663
+ <button type="button" className="btn-filtro-recursivo" onClick={onApplyOutlierFiltersRecursive} disabled={loading}>Aplicar com recursividade</button>
3664
  <button type="button" className="btn-filtro-add" onClick={onAddFiltro} disabled={loading}>Adicionar filtro</button>
 
3665
  </div>
3666
  </div>
3667
 
 
3682
  <button onClick={onRestartIteration} disabled={loading} className="btn-reiniciar-iteracao">
3683
  Atualizar Modelo (Excluir/Reincluir Outliers)
3684
  </button>
 
3685
  </div>
3686
  </div>
3687
  <div className="resumo-outliers-box">Iteração: {iteracao} | {resumoOutliers}</div>
3688
  <div className="resumo-outliers-box">Outliers anteriores: {joinSelection(outliersAnteriores) || '-'}</div>
3689
  </SectionBlock>
3690
 
3691
+ <SectionBlock step="18" title="Avaliação de Imóvel" subtitle="Cálculo individual e comparação entre avaliações.">
3692
+ <div className="avaliacao-groups">
3693
+ <div className="subpanel avaliacao-group">
3694
+ <h4>Parâmetros</h4>
3695
+ <div className="avaliacao-grid" key={`avaliacao-grid-elab-${avaliacaoFormVersion}`}>
3696
+ {camposAvaliacao.map((campo) => (
3697
+ <div key={`aval-${campo.coluna}`} className="avaliacao-card">
3698
+ <label>{campo.coluna}</label>
3699
+ {campo.tipo === 'dicotomica' ? (
3700
+ <select
3701
+ defaultValue={String(valoresAvaliacaoRef.current[campo.coluna] ?? '')}
3702
+ onChange={(e) => {
3703
+ valoresAvaliacaoRef.current[campo.coluna] = e.target.value
3704
+ setAvaliacaoPendente(true)
3705
+ }}
3706
+ >
3707
+ <option value="">Selecione</option>
3708
+ {(campo.opcoes || [0, 1]).map((opcao) => (
3709
+ <option key={`op-${campo.coluna}-${opcao}`} value={String(opcao)}>
3710
+ {opcao}
3711
+ </option>
3712
+ ))}
3713
+ </select>
3714
+ ) : (
3715
+ <input
3716
+ type="number"
3717
+ defaultValue={valoresAvaliacaoRef.current[campo.coluna] ?? ''}
3718
+ placeholder={campo.placeholder || ''}
3719
+ onChange={(e) => {
3720
+ valoresAvaliacaoRef.current[campo.coluna] = e.target.value
3721
+ setAvaliacaoPendente(true)
3722
+ }}
3723
+ />
3724
+ )}
3725
+ </div>
3726
+ ))}
3727
+ </div>
3728
+ <div className="row-wrap avaliacao-actions-row">
3729
+ <button onClick={onCalculateAvaliacao} disabled={loading}>Calcular</button>
3730
+ <button onClick={onResetCamposAvaliacao} disabled={loading}>Resetar campos</button>
3731
+ </div>
3732
+ </div>
3733
+
3734
+ {temAvaliacoes ? (
3735
+ <div className="subpanel avaliacao-group">
3736
+ <h4>Avaliações</h4>
3737
+ <div className="row avaliacao-base-row">
3738
+ <label>Base comparação</label>
3739
+ <select value={baseValue || ''} onChange={(e) => onBaseChange(e.target.value)}>
3740
  <option value="">Selecione</option>
3741
+ {baseChoices.map((choice) => (
3742
+ <option key={`base-${choice}`} value={choice}>{choice}</option>
 
 
3743
  ))}
3744
  </select>
3745
+ <button type="button" className="btn-avaliacao-export" onClick={onExportAvaliacoes} disabled={loading}>
3746
+ Exportar avaliações (Excel)
3747
+ </button>
3748
+ {!confirmarLimpezaAvaliacoes ? (
3749
+ <button
3750
+ type="button"
3751
+ className="btn-avaliacao-clear"
3752
+ onClick={() => setConfirmarLimpezaAvaliacoes(true)}
3753
+ disabled={loading}
3754
+ >
3755
+ Limpar avaliações
3756
+ </button>
3757
+ ) : (
3758
+ <div className="avaliacao-clear-confirm avaliacao-clear-confirm-inline">
3759
+ <span>Confirmar limpeza?</span>
3760
+ <button type="button" className="btn-avaliacao-clear" onClick={onClearAvaliacao} disabled={loading}>
3761
+ Confirmar
3762
+ </button>
3763
+ <button type="button" onClick={() => setConfirmarLimpezaAvaliacoes(false)} disabled={loading}>
3764
+ Cancelar
3765
+ </button>
3766
+ </div>
3767
+ )}
3768
+ </div>
3769
+ <div
3770
+ className="avaliacao-resultado-box"
3771
+ onClick={onAvaliacaoResultadoClick}
3772
+ dangerouslySetInnerHTML={{ __html: resultadoAvaliacaoHtml }}
3773
+ />
3774
  </div>
3775
+ ) : null}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3776
  </div>
 
 
 
 
 
3777
  </SectionBlock>
3778
 
3779
+ <SectionBlock step="19" title="Exportar Modelo" subtitle="Geração do pacote .dai e download da base tratada.">
3780
  <div className="row">
3781
  <label>Nome do arquivo (.dai)</label>
3782
  <input type="text" value={nomeArquivoExport} onChange={(e) => setNomeArquivoExport(e.target.value)} />
 
3795
  </SectionBlock>
3796
  </>
3797
  ) : null}
3798
+ </div>
3799
 
3800
  {disabledHint ? (
3801
  <div
frontend/src/components/SectionBlock.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import React from 'react'
2
 
3
  export default function SectionBlock({
4
  step,
@@ -6,18 +6,39 @@ export default function SectionBlock({
6
  subtitle,
7
  aside,
8
  children,
 
 
9
  }) {
 
 
10
  return (
11
- <section className="workflow-section" style={{ '--section-order': Number(step) || 1 }}>
 
 
 
 
12
  <header className="section-head">
13
  <span className="section-index">{step}</span>
14
  <div className="section-title-wrap">
15
  <h3>{title}</h3>
16
  {subtitle ? <p>{subtitle}</p> : null}
17
  </div>
18
- {aside ? <div className="section-head-aside">{aside}</div> : null}
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </header>
20
- <div className="section-body">{children}</div>
21
  </section>
22
  )
23
  }
 
1
+ import React, { useState } from 'react'
2
 
3
  export default function SectionBlock({
4
  step,
 
6
  subtitle,
7
  aside,
8
  children,
9
+ collapsible = true,
10
+ defaultOpen = true,
11
  }) {
12
+ const [open, setOpen] = useState(defaultOpen)
13
+
14
  return (
15
+ <section
16
+ className={`workflow-section${collapsible && !open ? ' is-collapsed' : ''}`}
17
+ style={{ '--section-order': Number(step) || 1 }}
18
+ data-section-step={String(step)}
19
+ >
20
  <header className="section-head">
21
  <span className="section-index">{step}</span>
22
  <div className="section-title-wrap">
23
  <h3>{title}</h3>
24
  {subtitle ? <p>{subtitle}</p> : null}
25
  </div>
26
+ <div className="section-head-actions">
27
+ {aside ? <div className="section-head-aside">{aside}</div> : null}
28
+ {collapsible ? (
29
+ <button
30
+ type="button"
31
+ className={`section-collapse-toggle${open ? ' is-open' : ''}`}
32
+ onClick={() => setOpen((current) => !current)}
33
+ aria-expanded={open}
34
+ title={open ? 'Recolher seção' : 'Expandir seção'}
35
+ >
36
+ <span className="section-collapse-toggle-icon" aria-hidden="true">▾</span>
37
+ </button>
38
+ ) : null}
39
+ </div>
40
  </header>
41
+ {!collapsible || open ? <div className="section-body">{children}</div> : null}
42
  </section>
43
  )
44
  }
frontend/src/components/VisualizacaoTab.jsx CHANGED
@@ -28,6 +28,7 @@ export default function VisualizacaoTab({ sessionId }) {
28
 
29
  const [uploadedFile, setUploadedFile] = useState(null)
30
  const [uploadDragOver, setUploadDragOver] = useState(false)
 
31
  const [repoModelos, setRepoModelos] = useState([])
32
  const [repoModeloSelecionado, setRepoModeloSelecionado] = useState('')
33
  const [repoModelosLoading, setRepoModelosLoading] = useState(false)
@@ -55,6 +56,7 @@ export default function VisualizacaoTab({ sessionId }) {
55
  const [camposAvaliacao, setCamposAvaliacao] = useState([])
56
  const valoresAvaliacaoRef = useRef({})
57
  const [avaliacaoFormVersion, setAvaliacaoFormVersion] = useState(0)
 
58
  const [resultadoAvaliacaoHtml, setResultadoAvaliacaoHtml] = useState('')
59
  const [baseChoices, setBaseChoices] = useState([])
60
  const [baseValue, setBaseValue] = useState('')
@@ -62,6 +64,7 @@ export default function VisualizacaoTab({ sessionId }) {
62
  const [activeInnerTab, setActiveInnerTab] = useState('mapa')
63
  const deleteConfirmTimersRef = useRef({})
64
  const uploadInputRef = useRef(null)
 
65
 
66
  function resetConteudoVisualizacao() {
67
  setDados(null)
@@ -86,6 +89,7 @@ export default function VisualizacaoTab({ sessionId }) {
86
  setCamposAvaliacao([])
87
  valoresAvaliacaoRef.current = {}
88
  setAvaliacaoFormVersion((prev) => prev + 1)
 
89
  setResultadoAvaliacaoHtml('')
90
  setBaseChoices([])
91
  setBaseValue('')
@@ -120,6 +124,7 @@ export default function VisualizacaoTab({ sessionId }) {
120
  })
121
  valoresAvaliacaoRef.current = values
122
  setAvaliacaoFormVersion((prev) => prev + 1)
 
123
  setResultadoAvaliacaoHtml('')
124
  setBaseChoices([])
125
  setBaseValue('')
@@ -207,6 +212,7 @@ export default function VisualizacaoTab({ sessionId }) {
207
  async function onUploadModel(arquivo = null) {
208
  const arquivoUpload = arquivo || uploadedFile
209
  if (!sessionId || !arquivoUpload) return
 
210
  await withBusy(async () => {
211
  resetConteudoVisualizacao()
212
  const uploadResp = await api.uploadVisualizacaoFile(sessionId, arquivoUpload)
@@ -220,6 +226,7 @@ export default function VisualizacaoTab({ sessionId }) {
220
 
221
  async function onCarregarModeloRepositorio() {
222
  if (!sessionId || !repoModeloSelecionado) return
 
223
  await withBusy(async () => {
224
  resetConteudoVisualizacao()
225
  const uploadResp = await api.visualizacaoRepositorioCarregar(sessionId, repoModeloSelecionado)
@@ -235,6 +242,7 @@ export default function VisualizacaoTab({ sessionId }) {
235
  function onUploadInputChange(event) {
236
  const input = event.target
237
  const file = input.files?.[0] ?? null
 
238
  setUploadedFile(file)
239
  input.value = ''
240
  if (!file || loading) return
@@ -259,6 +267,7 @@ export default function VisualizacaoTab({ sessionId }) {
259
  setUploadDragOver(false)
260
  const file = event.dataTransfer?.files?.[0]
261
  if (!file || loading) return
 
262
  setUploadedFile(file)
263
  void onUploadModel(file)
264
  }
@@ -279,9 +288,19 @@ export default function VisualizacaoTab({ sessionId }) {
279
  setResultadoAvaliacaoHtml(resp.resultado_html || '')
280
  setBaseChoices(resp.base_choices || [])
281
  setBaseValue(resp.base_value || '')
 
282
  })
283
  }
284
 
 
 
 
 
 
 
 
 
 
285
  async function onClearAvaliacao() {
286
  if (!sessionId) return
287
  await withBusy(async () => {
@@ -289,12 +308,7 @@ export default function VisualizacaoTab({ sessionId }) {
289
  setResultadoAvaliacaoHtml(resp.resultado_html || '')
290
  setBaseChoices(resp.base_choices || [])
291
  setBaseValue(resp.base_value || '')
292
- const limpo = {}
293
- camposAvaliacao.forEach((campo) => {
294
- limpo[campo.coluna] = ''
295
- })
296
- valoresAvaliacaoRef.current = limpo
297
- setAvaliacaoFormVersion((prev) => prev + 1)
298
  })
299
  }
300
 
@@ -305,6 +319,7 @@ export default function VisualizacaoTab({ sessionId }) {
305
  setResultadoAvaliacaoHtml(resp.resultado_html || '')
306
  setBaseChoices(resp.base_choices || [])
307
  setBaseValue(resp.base_value || '')
 
308
  })
309
  }
310
 
@@ -375,61 +390,97 @@ export default function VisualizacaoTab({ sessionId }) {
375
  return (
376
  <div className="tab-content">
377
  <SectionBlock step="1" title="Carregar Modelo .dai" subtitle="Carregue o arquivo e o conteúdo será exibido automaticamente.">
378
- <div className="row upload-repo-row">
379
- <label>Modelo do repositório</label>
380
- <select
381
- value={repoModeloSelecionado}
382
- onChange={(e) => setRepoModeloSelecionado(e.target.value)}
383
- disabled={loading || repoModelosLoading || repoModelos.length === 0}
384
- >
385
- <option value="">
386
- {repoModelosLoading ? 'Carregando lista...' : repoModelos.length > 0 ? 'Selecione um modelo' : 'Nenhum modelo disponível'}
387
- </option>
388
- {repoModelos.map((item) => (
389
- <option key={`repo-viz-${item.id}`} value={item.id}>
390
- {item.nome_modelo || item.arquivo}
391
- </option>
392
- ))}
393
- </select>
394
- <div className="row compact upload-repo-actions">
395
- <button type="button" onClick={onCarregarModeloRepositorio} disabled={loading || repoModelosLoading || !repoModeloSelecionado}>
396
- Carregar do repositório
397
- </button>
398
- <button type="button" onClick={() => void carregarModelosRepositorio()} disabled={loading || repoModelosLoading}>
399
- Atualizar lista
400
  </button>
401
- </div>
402
- {repoFonteModelos ? <div className="section1-empty-hint">{repoFonteModelos}</div> : null}
403
- </div>
404
- <div
405
- className={`upload-dropzone${uploadDragOver ? ' is-dragover' : ''}`}
406
- onDragOver={onUploadDropZoneDragOver}
407
- onDragEnter={onUploadDropZoneDragOver}
408
- onDragLeave={onUploadDropZoneDragLeave}
409
- onDrop={onUploadDropZoneDrop}
410
- >
411
- <input
412
- ref={uploadInputRef}
413
- type="file"
414
- className="upload-hidden-input"
415
- accept=".dai"
416
- onChange={onUploadInputChange}
417
- />
418
- <div className="row upload-dropzone-main">
419
  <button
420
  type="button"
421
- className="btn-upload-select"
422
- onClick={() => uploadInputRef.current?.click()}
423
  disabled={loading}
424
  >
425
- Selecionar arquivo
426
  </button>
427
  </div>
428
- <div className="upload-dropzone-hint">Ou arraste e solte aqui para carregar automaticamente.</div>
429
- <div className="upload-dropzone-file">
430
- {uploadedFile ? `Arquivo selecionado: ${uploadedFile.name}` : 'Nenhum arquivo selecionado.'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  </div>
432
- </div>
433
  {status ? <div className="status-line">{status}</div> : null}
434
  {badgeHtml ? <div className="upload-badge-block" dangerouslySetInnerHTML={{ __html: badgeHtml }} /> : null}
435
  </SectionBlock>
@@ -524,59 +575,89 @@ export default function VisualizacaoTab({ sessionId }) {
524
  {equacoes?.excel_sab || 'Equação indisponível.'}
525
  </div>
526
  </div>
527
- <div className="avaliacao-grid" key={`avaliacao-grid-viz-${avaliacaoFormVersion}`}>
528
- {camposAvaliacao.map((campo) => (
529
- <div key={`campo-${campo.coluna}`} className="avaliacao-card">
530
- <label>{campo.coluna}</label>
531
- {campo.tipo === 'dicotomica' ? (
532
- <select
533
- defaultValue={String(valoresAvaliacaoRef.current[campo.coluna] ?? '')}
534
- onChange={(e) => {
535
- valoresAvaliacaoRef.current[campo.coluna] = e.target.value
536
- }}
537
- >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  <option value="">Selecione</option>
539
- {(campo.opcoes || [0, 1]).map((opcao) => (
540
- <option key={`op-viz-${campo.coluna}-${opcao}`} value={String(opcao)}>
541
- {opcao}
542
- </option>
543
  ))}
544
  </select>
545
- ) : (
546
- <input
547
- type="number"
548
- defaultValue={valoresAvaliacaoRef.current[campo.coluna] ?? ''}
549
- placeholder={campo.placeholder || ''}
550
- onChange={(e) => {
551
- valoresAvaliacaoRef.current[campo.coluna] = e.target.value
552
- }}
553
- />
554
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
555
  </div>
556
- ))}
557
- </div>
558
-
559
- <div className="row-wrap avaliacao-actions-row">
560
- <button onClick={onCalcularAvaliacao} disabled={loading}>Calcular</button>
561
- <button onClick={onClearAvaliacao} disabled={loading}>Limpar</button>
562
- <button onClick={onExportAvaliacoes} disabled={loading}>Exportar avaliações</button>
563
  </div>
564
-
565
- <div className="row avaliacao-base-row">
566
- <label>Base comparação</label>
567
- <select value={baseValue || ''} onChange={(e) => onBaseChange(e.target.value)}>
568
- <option value="">Selecione</option>
569
- {baseChoices.map((choice) => (
570
- <option key={`base-${choice}`} value={choice}>{choice}</option>
571
- ))}
572
- </select>
573
- </div>
574
-
575
- <div
576
- className="avaliacao-resultado-box"
577
- onClick={onAvaliacaoResultadoClick}
578
- dangerouslySetInnerHTML={{ __html: resultadoAvaliacaoHtml }}
579
- />
580
  </>
581
  ) : null}
582
 
 
28
 
29
  const [uploadedFile, setUploadedFile] = useState(null)
30
  const [uploadDragOver, setUploadDragOver] = useState(false)
31
+ const [modeloLoadSource, setModeloLoadSource] = useState('')
32
  const [repoModelos, setRepoModelos] = useState([])
33
  const [repoModeloSelecionado, setRepoModeloSelecionado] = useState('')
34
  const [repoModelosLoading, setRepoModelosLoading] = useState(false)
 
56
  const [camposAvaliacao, setCamposAvaliacao] = useState([])
57
  const valoresAvaliacaoRef = useRef({})
58
  const [avaliacaoFormVersion, setAvaliacaoFormVersion] = useState(0)
59
+ const [confirmarLimpezaAvaliacoes, setConfirmarLimpezaAvaliacoes] = useState(false)
60
  const [resultadoAvaliacaoHtml, setResultadoAvaliacaoHtml] = useState('')
61
  const [baseChoices, setBaseChoices] = useState([])
62
  const [baseValue, setBaseValue] = useState('')
 
64
  const [activeInnerTab, setActiveInnerTab] = useState('mapa')
65
  const deleteConfirmTimersRef = useRef({})
66
  const uploadInputRef = useRef(null)
67
+ const temAvaliacoes = Array.isArray(baseChoices) && baseChoices.length > 0
68
 
69
  function resetConteudoVisualizacao() {
70
  setDados(null)
 
89
  setCamposAvaliacao([])
90
  valoresAvaliacaoRef.current = {}
91
  setAvaliacaoFormVersion((prev) => prev + 1)
92
+ setConfirmarLimpezaAvaliacoes(false)
93
  setResultadoAvaliacaoHtml('')
94
  setBaseChoices([])
95
  setBaseValue('')
 
124
  })
125
  valoresAvaliacaoRef.current = values
126
  setAvaliacaoFormVersion((prev) => prev + 1)
127
+ setConfirmarLimpezaAvaliacoes(false)
128
  setResultadoAvaliacaoHtml('')
129
  setBaseChoices([])
130
  setBaseValue('')
 
212
  async function onUploadModel(arquivo = null) {
213
  const arquivoUpload = arquivo || uploadedFile
214
  if (!sessionId || !arquivoUpload) return
215
+ setModeloLoadSource('upload')
216
  await withBusy(async () => {
217
  resetConteudoVisualizacao()
218
  const uploadResp = await api.uploadVisualizacaoFile(sessionId, arquivoUpload)
 
226
 
227
  async function onCarregarModeloRepositorio() {
228
  if (!sessionId || !repoModeloSelecionado) return
229
+ setModeloLoadSource('repo')
230
  await withBusy(async () => {
231
  resetConteudoVisualizacao()
232
  const uploadResp = await api.visualizacaoRepositorioCarregar(sessionId, repoModeloSelecionado)
 
242
  function onUploadInputChange(event) {
243
  const input = event.target
244
  const file = input.files?.[0] ?? null
245
+ setModeloLoadSource('upload')
246
  setUploadedFile(file)
247
  input.value = ''
248
  if (!file || loading) return
 
267
  setUploadDragOver(false)
268
  const file = event.dataTransfer?.files?.[0]
269
  if (!file || loading) return
270
+ setModeloLoadSource('upload')
271
  setUploadedFile(file)
272
  void onUploadModel(file)
273
  }
 
288
  setResultadoAvaliacaoHtml(resp.resultado_html || '')
289
  setBaseChoices(resp.base_choices || [])
290
  setBaseValue(resp.base_value || '')
291
+ setConfirmarLimpezaAvaliacoes(false)
292
  })
293
  }
294
 
295
+ function onResetCamposAvaliacao() {
296
+ const limpo = {}
297
+ camposAvaliacao.forEach((campo) => {
298
+ limpo[campo.coluna] = ''
299
+ })
300
+ valoresAvaliacaoRef.current = limpo
301
+ setAvaliacaoFormVersion((prev) => prev + 1)
302
+ }
303
+
304
  async function onClearAvaliacao() {
305
  if (!sessionId) return
306
  await withBusy(async () => {
 
308
  setResultadoAvaliacaoHtml(resp.resultado_html || '')
309
  setBaseChoices(resp.base_choices || [])
310
  setBaseValue(resp.base_value || '')
311
+ setConfirmarLimpezaAvaliacoes(false)
 
 
 
 
 
312
  })
313
  }
314
 
 
319
  setResultadoAvaliacaoHtml(resp.resultado_html || '')
320
  setBaseChoices(resp.base_choices || [])
321
  setBaseValue(resp.base_value || '')
322
+ setConfirmarLimpezaAvaliacoes(false)
323
  })
324
  }
325
 
 
390
  return (
391
  <div className="tab-content">
392
  <SectionBlock step="1" title="Carregar Modelo .dai" subtitle="Carregue o arquivo e o conteúdo será exibido automaticamente.">
393
+ {!modeloLoadSource ? (
394
+ <div className="model-source-choice-grid">
395
+ <button
396
+ type="button"
397
+ className="model-source-choice-btn model-source-choice-btn-primary"
398
+ onClick={() => setModeloLoadSource('repo')}
399
+ disabled={loading}
400
+ >
401
+ Carregar modelo do repositório
 
 
 
 
 
 
 
 
 
 
 
 
 
402
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
403
  <button
404
  type="button"
405
+ className="model-source-choice-btn model-source-choice-btn-secondary"
406
+ onClick={() => setModeloLoadSource('upload')}
407
  disabled={loading}
408
  >
409
+ Fazer upload de modelo
410
  </button>
411
  </div>
412
+ ) : (
413
+ <div className="model-source-flow">
414
+ <div className="model-source-flow-head">
415
+ <button
416
+ type="button"
417
+ className="model-source-back-btn"
418
+ onClick={() => setModeloLoadSource('')}
419
+ disabled={loading}
420
+ >
421
+ Voltar
422
+ </button>
423
+ </div>
424
+
425
+ {modeloLoadSource === 'repo' ? (
426
+ <div className="row upload-repo-row">
427
+ <label>Modelo do repositório</label>
428
+ <select
429
+ value={repoModeloSelecionado}
430
+ onChange={(e) => setRepoModeloSelecionado(e.target.value)}
431
+ disabled={loading || repoModelosLoading || repoModelos.length === 0}
432
+ >
433
+ <option value="">
434
+ {repoModelosLoading ? 'Carregando lista...' : repoModelos.length > 0 ? 'Selecione um modelo' : 'Nenhum modelo disponível'}
435
+ </option>
436
+ {repoModelos.map((item) => (
437
+ <option key={`repo-viz-${item.id}`} value={item.id}>
438
+ {item.nome_modelo || item.arquivo}
439
+ </option>
440
+ ))}
441
+ </select>
442
+ <div className="row compact upload-repo-actions">
443
+ <button type="button" onClick={onCarregarModeloRepositorio} disabled={loading || repoModelosLoading || !repoModeloSelecionado}>
444
+ Carregar do repositório
445
+ </button>
446
+ <button type="button" onClick={() => void carregarModelosRepositorio()} disabled={loading || repoModelosLoading}>
447
+ Atualizar lista
448
+ </button>
449
+ </div>
450
+ {repoFonteModelos ? <div className="section1-empty-hint">{repoFonteModelos}</div> : null}
451
+ </div>
452
+ ) : null}
453
+
454
+ {modeloLoadSource === 'upload' ? (
455
+ <div
456
+ className={`upload-dropzone${uploadDragOver ? ' is-dragover' : ''}`}
457
+ onDragOver={onUploadDropZoneDragOver}
458
+ onDragEnter={onUploadDropZoneDragOver}
459
+ onDragLeave={onUploadDropZoneDragLeave}
460
+ onDrop={onUploadDropZoneDrop}
461
+ >
462
+ <input
463
+ ref={uploadInputRef}
464
+ type="file"
465
+ className="upload-hidden-input"
466
+ accept=".dai"
467
+ onChange={onUploadInputChange}
468
+ />
469
+ <div className="row upload-dropzone-main">
470
+ <button
471
+ type="button"
472
+ className="btn-upload-select"
473
+ onClick={() => uploadInputRef.current?.click()}
474
+ disabled={loading}
475
+ >
476
+ Selecionar arquivo
477
+ </button>
478
+ </div>
479
+ <div className="upload-dropzone-hint">Ou arraste e solte aqui para carregar automaticamente.</div>
480
+ </div>
481
+ ) : null}
482
  </div>
483
+ )}
484
  {status ? <div className="status-line">{status}</div> : null}
485
  {badgeHtml ? <div className="upload-badge-block" dangerouslySetInnerHTML={{ __html: badgeHtml }} /> : null}
486
  </SectionBlock>
 
575
  {equacoes?.excel_sab || 'Equação indisponível.'}
576
  </div>
577
  </div>
578
+ <div className="avaliacao-groups">
579
+ <div className="subpanel avaliacao-group">
580
+ <h4>Parâmetros</h4>
581
+ <div className="avaliacao-grid" key={`avaliacao-grid-viz-${avaliacaoFormVersion}`}>
582
+ {camposAvaliacao.map((campo) => (
583
+ <div key={`campo-${campo.coluna}`} className="avaliacao-card">
584
+ <label>{campo.coluna}</label>
585
+ {campo.tipo === 'dicotomica' ? (
586
+ <select
587
+ defaultValue={String(valoresAvaliacaoRef.current[campo.coluna] ?? '')}
588
+ onChange={(e) => {
589
+ valoresAvaliacaoRef.current[campo.coluna] = e.target.value
590
+ }}
591
+ >
592
+ <option value="">Selecione</option>
593
+ {(campo.opcoes || [0, 1]).map((opcao) => (
594
+ <option key={`op-viz-${campo.coluna}-${opcao}`} value={String(opcao)}>
595
+ {opcao}
596
+ </option>
597
+ ))}
598
+ </select>
599
+ ) : (
600
+ <input
601
+ type="number"
602
+ defaultValue={valoresAvaliacaoRef.current[campo.coluna] ?? ''}
603
+ placeholder={campo.placeholder || ''}
604
+ onChange={(e) => {
605
+ valoresAvaliacaoRef.current[campo.coluna] = e.target.value
606
+ }}
607
+ />
608
+ )}
609
+ </div>
610
+ ))}
611
+ </div>
612
+ <div className="row-wrap avaliacao-actions-row">
613
+ <button onClick={onCalcularAvaliacao} disabled={loading}>Calcular</button>
614
+ <button onClick={onResetCamposAvaliacao} disabled={loading}>Resetar campos</button>
615
+ </div>
616
+ </div>
617
+
618
+ {temAvaliacoes ? (
619
+ <div className="subpanel avaliacao-group">
620
+ <h4>Avaliações</h4>
621
+ <div className="row avaliacao-base-row">
622
+ <label>Base comparação</label>
623
+ <select value={baseValue || ''} onChange={(e) => onBaseChange(e.target.value)}>
624
  <option value="">Selecione</option>
625
+ {baseChoices.map((choice) => (
626
+ <option key={`base-${choice}`} value={choice}>{choice}</option>
 
 
627
  ))}
628
  </select>
629
+ <button type="button" className="btn-avaliacao-export" onClick={onExportAvaliacoes} disabled={loading}>
630
+ Exportar avaliações
631
+ </button>
632
+ {!confirmarLimpezaAvaliacoes ? (
633
+ <button
634
+ type="button"
635
+ className="btn-avaliacao-clear"
636
+ onClick={() => setConfirmarLimpezaAvaliacoes(true)}
637
+ disabled={loading}
638
+ >
639
+ Limpar avaliações
640
+ </button>
641
+ ) : (
642
+ <div className="avaliacao-clear-confirm avaliacao-clear-confirm-inline">
643
+ <span>Confirmar limpeza?</span>
644
+ <button type="button" className="btn-avaliacao-clear" onClick={onClearAvaliacao} disabled={loading}>
645
+ Confirmar
646
+ </button>
647
+ <button type="button" onClick={() => setConfirmarLimpezaAvaliacoes(false)} disabled={loading}>
648
+ Cancelar
649
+ </button>
650
+ </div>
651
+ )}
652
+ </div>
653
+ <div
654
+ className="avaliacao-resultado-box"
655
+ onClick={onAvaliacaoResultadoClick}
656
+ dangerouslySetInnerHTML={{ __html: resultadoAvaliacaoHtml }}
657
+ />
658
  </div>
659
+ ) : null}
 
 
 
 
 
 
660
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
661
  </>
662
  ) : null}
663
 
frontend/src/styles.css CHANGED
@@ -558,14 +558,14 @@ textarea {
558
  }
559
 
560
  .workflow-section {
561
- border: 1px solid #c9d6e3;
562
  border-radius: var(--radius-lg);
563
  background: var(--bg-2);
564
  min-width: 0;
565
  max-width: 100%;
566
  box-shadow:
567
- 0 6px 18px rgba(20, 28, 36, 0.08),
568
- inset 0 0 0 1px #edf3f9;
569
  animation: sectionIn 0.35s ease both;
570
  animation-delay: calc(var(--section-order, 1) * 25ms);
571
  }
@@ -613,7 +613,51 @@ textarea {
613
  }
614
 
615
  .section-head-aside {
 
 
 
 
616
  margin-left: auto;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
617
  }
618
 
619
  .pesquisa-admin-toggle {
@@ -667,6 +711,13 @@ textarea {
667
  min-width: 0;
668
  }
669
 
 
 
 
 
 
 
 
670
  .dados-visualizacao-group {
671
  margin: 0;
672
  min-width: 0;
@@ -1528,15 +1579,81 @@ button.btn-pesquisa-expand:hover {
1528
  min-width: 0;
1529
  }
1530
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1531
  .avaliacao-actions-row {
1532
  gap: 12px;
1533
- margin-bottom: 16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1534
  }
1535
 
1536
  .avaliacao-base-row {
1537
  gap: 12px;
1538
  margin-top: 2px;
1539
  margin-bottom: 16px;
 
 
 
 
 
 
 
 
 
 
1540
  }
1541
 
1542
  .avaliacao-resultado-box {
@@ -1635,6 +1752,69 @@ button:disabled {
1635
  background: #fff;
1636
  }
1637
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1638
  .upload-dropzone {
1639
  border: 1px dashed #c5d5e5;
1640
  border-radius: 12px;
@@ -1686,6 +1866,49 @@ button.btn-upload-select {
1686
  word-break: break-word;
1687
  }
1688
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1689
  .upload-badge-block {
1690
  margin-top: 14px;
1691
  }
@@ -1982,6 +2205,14 @@ button.btn-upload-select {
1982
  background: #fff8ef;
1983
  }
1984
 
 
 
 
 
 
 
 
 
1985
  .table-hint {
1986
  border-top: 1px solid #edf2f6;
1987
  padding: 7px 9px;
@@ -2136,6 +2367,18 @@ button.btn-upload-select {
2136
  color: #33495f;
2137
  }
2138
 
 
 
 
 
 
 
 
 
 
 
 
 
2139
  .transform-suggestions-grid {
2140
  display: grid;
2141
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
@@ -2185,6 +2428,17 @@ button.btn-upload-select {
2185
  font-weight: 700;
2186
  }
2187
 
 
 
 
 
 
 
 
 
 
 
 
2188
  .transform-suggestion-line {
2189
  color: #3f5368;
2190
  font-size: 0.85rem;
@@ -2204,16 +2458,50 @@ button.btn-upload-select {
2204
  margin-bottom: 12px;
2205
  }
2206
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2207
  .btn-manual-toggle {
2208
  min-width: 320px;
 
 
 
 
 
2209
  }
2210
 
2211
  .btn-manual-toggle.active {
2212
- --btn-bg-start: #6b7f93;
2213
- --btn-bg-end: #586b7d;
2214
- --btn-border: #4a5c6e;
2215
- --btn-shadow-soft: rgba(86, 105, 122, 0.18);
2216
- --btn-shadow-strong: rgba(86, 105, 122, 0.24);
2217
  }
2218
 
2219
  .section6-selected-summary {
@@ -2526,6 +2814,16 @@ button.btn-upload-select {
2526
  margin-bottom: 10px;
2527
  }
2528
 
 
 
 
 
 
 
 
 
 
 
2529
  .outliers-html-box {
2530
  margin-bottom: 10px;
2531
  }
@@ -2674,6 +2972,14 @@ button.btn-upload-select {
2674
  --btn-shadow-strong: rgba(47, 128, 207, 0.27);
2675
  }
2676
 
 
 
 
 
 
 
 
 
2677
  .resumo-outliers-box {
2678
  color: #4d647b;
2679
  font-weight: 700;
@@ -2686,44 +2992,6 @@ button.btn-upload-select {
2686
  margin-top: 8px;
2687
  }
2688
 
2689
- .section-content-toggle {
2690
- border: 1px solid #dce7f1;
2691
- border-radius: 12px;
2692
- background: #fbfdff;
2693
- padding: 10px;
2694
- }
2695
-
2696
- .section-content-toggle > summary {
2697
- list-style: none;
2698
- cursor: pointer;
2699
- user-select: none;
2700
- display: inline-flex;
2701
- align-items: center;
2702
- gap: 7px;
2703
- font-family: 'Sora', sans-serif;
2704
- color: #30485f;
2705
- font-size: 0.86rem;
2706
- }
2707
-
2708
- .section-content-toggle > summary::-webkit-details-marker {
2709
- display: none;
2710
- }
2711
-
2712
- .section-content-toggle > summary::before {
2713
- content: '▸';
2714
- color: #5f7489;
2715
- font-size: 0.85rem;
2716
- transition: transform 0.15s ease;
2717
- }
2718
-
2719
- .section-content-toggle[open] > summary::before {
2720
- transform: rotate(90deg);
2721
- }
2722
-
2723
- .section-content-toggle[open] > summary {
2724
- margin-bottom: 10px;
2725
- }
2726
-
2727
  .transformacoes-aplicadas-wrap {
2728
  margin-top: 12px;
2729
  }
@@ -3250,6 +3518,50 @@ button.btn-download-subtle {
3250
  word-break: break-word;
3251
  }
3252
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3253
  ::-webkit-scrollbar {
3254
  width: 8px;
3255
  height: 8px;
@@ -3374,6 +3686,15 @@ button.btn-download-subtle {
3374
  grid-template-columns: 1fr;
3375
  }
3376
 
 
 
 
 
 
 
 
 
 
3377
  .pesquisa-filtros-groups,
3378
  .pesquisa-filtros-grid,
3379
  .pesquisa-fields-grid,
@@ -3467,4 +3788,8 @@ button.btn-download-subtle {
3467
  .micro-msg-grid-codigo {
3468
  grid-template-columns: 1fr;
3469
  }
 
 
 
 
3470
  }
 
558
  }
559
 
560
  .workflow-section {
561
+ border: 2px solid #aebfd0;
562
  border-radius: var(--radius-lg);
563
  background: var(--bg-2);
564
  min-width: 0;
565
  max-width: 100%;
566
  box-shadow:
567
+ 0 8px 22px rgba(20, 28, 36, 0.1),
568
+ inset 0 0 0 1px #dfe9f3;
569
  animation: sectionIn 0.35s ease both;
570
  animation-delay: calc(var(--section-order, 1) * 25ms);
571
  }
 
613
  }
614
 
615
  .section-head-aside {
616
+ margin-left: 0;
617
+ }
618
+
619
+ .section-head-actions {
620
  margin-left: auto;
621
+ display: inline-flex;
622
+ align-items: center;
623
+ gap: 10px;
624
+ }
625
+
626
+ .section-collapse-toggle {
627
+ min-height: 34px;
628
+ min-width: 34px;
629
+ padding: 0;
630
+ border-radius: 10px;
631
+ border: 1px solid #cf6f00;
632
+ background: linear-gradient(180deg, #ff9a26 0%, #e67900 100%);
633
+ color: #ffffff;
634
+ box-shadow:
635
+ 0 2px 8px rgba(230, 121, 0, 0.24),
636
+ inset 0 0 0 1px rgba(255, 255, 255, 0.22);
637
+ }
638
+
639
+ .section-collapse-toggle:hover {
640
+ transform: translateY(-1px);
641
+ box-shadow:
642
+ 0 4px 12px rgba(230, 121, 0, 0.29),
643
+ inset 0 0 0 1px rgba(255, 255, 255, 0.24);
644
+ border-color: #b15e00;
645
+ }
646
+
647
+ .section-collapse-toggle-icon {
648
+ display: inline-block;
649
+ line-height: 1;
650
+ font-size: 0.94rem;
651
+ font-weight: 900;
652
+ transition: transform 0.16s ease;
653
+ }
654
+
655
+ .section-collapse-toggle.is-open .section-collapse-toggle-icon {
656
+ transform: rotate(0deg);
657
+ }
658
+
659
+ .workflow-section.is-collapsed .section-collapse-toggle-icon {
660
+ transform: rotate(-90deg);
661
  }
662
 
663
  .pesquisa-admin-toggle {
 
711
  min-width: 0;
712
  }
713
 
714
+ .workflow-sections-stack {
715
+ display: flex;
716
+ flex-direction: column;
717
+ gap: 34.5px;
718
+ min-width: 0;
719
+ }
720
+
721
  .dados-visualizacao-group {
722
  margin: 0;
723
  min-width: 0;
 
1579
  min-width: 0;
1580
  }
1581
 
1582
+ .avaliacao-groups {
1583
+ display: grid;
1584
+ gap: 14px;
1585
+ }
1586
+
1587
+ .avaliacao-group {
1588
+ margin: 0;
1589
+ border-left: 1px solid var(--border-soft);
1590
+ background: #fff;
1591
+ }
1592
+
1593
+ .avaliacao-group h4 {
1594
+ margin-bottom: 10px;
1595
+ }
1596
+
1597
  .avaliacao-actions-row {
1598
  gap: 12px;
1599
+ margin-bottom: 8px;
1600
+ }
1601
+
1602
+ .btn-avaliacao-export {
1603
+ --btn-bg-start: #4a90c8;
1604
+ --btn-bg-end: #3978ab;
1605
+ --btn-border: #346f9f;
1606
+ --btn-shadow-soft: rgba(53, 113, 157, 0.2);
1607
+ --btn-shadow-strong: rgba(53, 113, 157, 0.25);
1608
+ }
1609
+
1610
+ .btn-avaliacao-clear {
1611
+ --btn-bg-start: #cf3d4f;
1612
+ --btn-bg-end: #b22f40;
1613
+ --btn-border: #a22b3a;
1614
+ --btn-shadow-soft: rgba(162, 43, 58, 0.2);
1615
+ --btn-shadow-strong: rgba(162, 43, 58, 0.25);
1616
+ background: linear-gradient(180deg, #cf3d4f 0%, #b22f40 100%);
1617
+ border-color: #a22b3a;
1618
+ box-shadow: 0 3px 8px rgba(162, 43, 58, 0.2);
1619
+ color: #fff;
1620
+ }
1621
+
1622
+ .btn-avaliacao-clear:hover {
1623
+ box-shadow: 0 6px 14px rgba(162, 43, 58, 0.25);
1624
+ }
1625
+
1626
+ .avaliacao-clear-confirm {
1627
+ display: inline-flex;
1628
+ flex-wrap: wrap;
1629
+ align-items: center;
1630
+ gap: 8px;
1631
+ border: 1px solid #f0cf9f;
1632
+ border-radius: 10px;
1633
+ background: #fffaf2;
1634
+ padding: 8px 10px;
1635
+ }
1636
+
1637
+ .avaliacao-clear-confirm span {
1638
+ color: #7a4d00;
1639
+ font-size: 0.84rem;
1640
+ font-weight: 600;
1641
  }
1642
 
1643
  .avaliacao-base-row {
1644
  gap: 12px;
1645
  margin-top: 2px;
1646
  margin-bottom: 16px;
1647
+ align-items: center;
1648
+ }
1649
+
1650
+ .avaliacao-base-row select {
1651
+ min-width: 220px;
1652
+ }
1653
+
1654
+ .avaliacao-clear-confirm-inline {
1655
+ margin-left: 2px;
1656
+ padding: 6px 8px;
1657
  }
1658
 
1659
  .avaliacao-resultado-box {
 
1752
  background: #fff;
1753
  }
1754
 
1755
+ .section1-badges-group {
1756
+ display: grid;
1757
+ gap: 12px;
1758
+ }
1759
+
1760
+ .model-source-choice-grid {
1761
+ display: flex;
1762
+ flex-wrap: wrap;
1763
+ align-items: center;
1764
+ gap: 10px;
1765
+ }
1766
+
1767
+ button.model-source-choice-btn {
1768
+ min-height: 44px;
1769
+ min-width: 260px;
1770
+ max-width: 360px;
1771
+ width: auto;
1772
+ flex: 0 0 auto;
1773
+ text-align: left;
1774
+ justify-content: flex-start;
1775
+ padding: 10px 13px;
1776
+ }
1777
+
1778
+ button.model-source-choice-btn-primary {
1779
+ --btn-bg-start: #3f90d5;
1780
+ --btn-bg-end: #2f79b8;
1781
+ --btn-border: #2a6da8;
1782
+ --btn-shadow-soft: rgba(42, 109, 168, 0.2);
1783
+ --btn-shadow-strong: rgba(42, 109, 168, 0.28);
1784
+ color: #ffffff;
1785
+ }
1786
+
1787
+ button.model-source-choice-btn-secondary {
1788
+ --btn-bg-start: #f1f4f7;
1789
+ --btn-bg-end: #e4e9ef;
1790
+ --btn-border: #c6d0db;
1791
+ --btn-shadow-soft: rgba(66, 84, 103, 0.12);
1792
+ --btn-shadow-strong: rgba(66, 84, 103, 0.2);
1793
+ color: #35506a;
1794
+ }
1795
+
1796
+ .model-source-flow {
1797
+ display: grid;
1798
+ gap: 12px;
1799
+ }
1800
+
1801
+ .model-source-flow-head {
1802
+ display: flex;
1803
+ justify-content: flex-start;
1804
+ }
1805
+
1806
+ button.model-source-back-btn {
1807
+ min-height: 36px;
1808
+ padding: 7px 12px;
1809
+ font-size: 0.82rem;
1810
+ --btn-bg-start: #f8fbff;
1811
+ --btn-bg-end: #edf3fa;
1812
+ --btn-border: #c8d8e8;
1813
+ --btn-shadow-soft: rgba(53, 83, 114, 0.1);
1814
+ --btn-shadow-strong: rgba(53, 83, 114, 0.16);
1815
+ color: #3f5973;
1816
+ }
1817
+
1818
  .upload-dropzone {
1819
  border: 1px dashed #c5d5e5;
1820
  border-radius: 12px;
 
1866
  word-break: break-word;
1867
  }
1868
 
1869
+ .upload-file-info-card {
1870
+ margin-top: 0;
1871
+ border: 1px solid #d1deea;
1872
+ border-radius: 12px;
1873
+ background: linear-gradient(180deg, #fbfdff 0%, #f6faff 100%);
1874
+ padding: 10px 12px;
1875
+ }
1876
+
1877
+ .upload-file-info-grid {
1878
+ display: grid;
1879
+ grid-template-columns: repeat(2, minmax(0, 1fr));
1880
+ gap: 8px 10px;
1881
+ }
1882
+
1883
+ .upload-file-info-item {
1884
+ display: grid;
1885
+ gap: 2px;
1886
+ min-width: 0;
1887
+ }
1888
+
1889
+ .upload-file-info-item-wide {
1890
+ grid-column: 1 / -1;
1891
+ }
1892
+
1893
+ .upload-file-info-label {
1894
+ font-size: 0.72rem;
1895
+ font-weight: 800;
1896
+ letter-spacing: 0.03em;
1897
+ text-transform: uppercase;
1898
+ color: #5f7891;
1899
+ }
1900
+
1901
+ .upload-file-info-value {
1902
+ color: #2f4358;
1903
+ font-size: 0.86rem;
1904
+ font-weight: 700;
1905
+ word-break: break-word;
1906
+ }
1907
+
1908
+ .import-feedback-line {
1909
+ margin-top: 10px;
1910
+ }
1911
+
1912
  .upload-badge-block {
1913
  margin-top: 14px;
1914
  }
 
2205
  background: #fff8ef;
2206
  }
2207
 
2208
+ .table-wrapper tr.table-row-highlight td {
2209
+ background: #fff3a8;
2210
+ }
2211
+
2212
+ .table-wrapper tr.table-row-highlight:hover td {
2213
+ background: #ffe882;
2214
+ }
2215
+
2216
  .table-hint {
2217
  border-top: 1px solid #edf2f6;
2218
  padding: 7px 9px;
 
2367
  color: #33495f;
2368
  }
2369
 
2370
+ .transform-card-head {
2371
+ display: flex;
2372
+ align-items: center;
2373
+ justify-content: space-between;
2374
+ gap: 8px;
2375
+ }
2376
+
2377
+ .transform-card.transform-card-y {
2378
+ border-color: #bfd7f0;
2379
+ background: linear-gradient(180deg, #f0f7ff 0%, #e6f1fc 100%);
2380
+ }
2381
+
2382
  .transform-suggestions-grid {
2383
  display: grid;
2384
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
 
2428
  font-weight: 700;
2429
  }
2430
 
2431
+ .transform-suggestion-r2adj {
2432
+ border-radius: 8px;
2433
+ border: 1px solid #d7e3ef;
2434
+ background: #f4f8fc;
2435
+ color: #355a78;
2436
+ font-family: 'JetBrains Mono', monospace;
2437
+ font-size: 0.76rem;
2438
+ padding: 3px 7px;
2439
+ font-weight: 700;
2440
+ }
2441
+
2442
  .transform-suggestion-line {
2443
  color: #3f5368;
2444
  font-size: 0.85rem;
 
2458
  margin-bottom: 12px;
2459
  }
2460
 
2461
+ .section12-toggle-wrap {
2462
+ margin-bottom: 18px;
2463
+ }
2464
+
2465
+ .transform-preview-summary {
2466
+ border: 1px solid #d8e5f2;
2467
+ border-radius: 12px;
2468
+ background: linear-gradient(180deg, #fbfdff 0%, #f6f9fd 100%);
2469
+ padding: 10px 12px;
2470
+ margin: 0 0 12px;
2471
+ display: grid;
2472
+ gap: 7px;
2473
+ }
2474
+
2475
+ .transform-preview-summary-title {
2476
+ font-size: 0.79rem;
2477
+ font-weight: 800;
2478
+ text-transform: uppercase;
2479
+ letter-spacing: 0.04em;
2480
+ color: #4a627a;
2481
+ }
2482
+
2483
+ .transform-preview-summary-metrics {
2484
+ display: flex;
2485
+ flex-wrap: wrap;
2486
+ gap: 7px;
2487
+ align-items: center;
2488
+ }
2489
+
2490
  .btn-manual-toggle {
2491
  min-width: 320px;
2492
+ --btn-bg-start: #2f80cf;
2493
+ --btn-bg-end: #2368af;
2494
+ --btn-border: #1f5f9f;
2495
+ --btn-shadow-soft: rgba(47, 128, 207, 0.2);
2496
+ --btn-shadow-strong: rgba(47, 128, 207, 0.27);
2497
  }
2498
 
2499
  .btn-manual-toggle.active {
2500
+ --btn-bg-start: #2b74bc;
2501
+ --btn-bg-end: #215f9d;
2502
+ --btn-border: #1d548a;
2503
+ --btn-shadow-soft: rgba(43, 116, 188, 0.2);
2504
+ --btn-shadow-strong: rgba(43, 116, 188, 0.27);
2505
  }
2506
 
2507
  .section6-selected-summary {
 
2814
  margin-bottom: 10px;
2815
  }
2816
 
2817
+ .outlier-highlight-note {
2818
+ margin: 0 0 10px;
2819
+ padding: 8px 10px;
2820
+ border-radius: 10px;
2821
+ border: 1px solid #f1df91;
2822
+ background: #fff9df;
2823
+ color: #5f5222;
2824
+ font-size: 0.82rem;
2825
+ }
2826
+
2827
  .outliers-html-box {
2828
  margin-bottom: 10px;
2829
  }
 
2972
  --btn-shadow-strong: rgba(47, 128, 207, 0.27);
2973
  }
2974
 
2975
+ .outlier-actions-row button.btn-filtro-recursivo {
2976
+ --btn-bg-start: #8f6bd9;
2977
+ --btn-bg-end: #7552c3;
2978
+ --btn-border: #6848b4;
2979
+ --btn-shadow-soft: rgba(117, 82, 195, 0.2);
2980
+ --btn-shadow-strong: rgba(117, 82, 195, 0.28);
2981
+ }
2982
+
2983
  .resumo-outliers-box {
2984
  color: #4d647b;
2985
  font-weight: 700;
 
2992
  margin-top: 8px;
2993
  }
2994
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2995
  .transformacoes-aplicadas-wrap {
2996
  margin-top: 12px;
2997
  }
 
3518
  word-break: break-word;
3519
  }
3520
 
3521
+ /* Micronumerosidade (dicotômicas): versão mais enxuta, mantendo status e testes */
3522
+ .micro-grid:not(.micro-grid-codigo) {
3523
+ display: grid;
3524
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
3525
+ gap: 10px;
3526
+ margin-top: 8px;
3527
+ }
3528
+
3529
+ .micro-grid:not(.micro-grid-codigo) .micro-card {
3530
+ padding: 8px 9px;
3531
+ border-radius: 9px;
3532
+ min-height: 0;
3533
+ }
3534
+
3535
+ .micro-grid:not(.micro-grid-codigo) .micro-card-head {
3536
+ margin-bottom: 7px;
3537
+ }
3538
+
3539
+ .micro-grid:not(.micro-grid-codigo) .micro-title {
3540
+ font-size: 0.86rem;
3541
+ line-height: 1.2;
3542
+ }
3543
+
3544
+ .micro-grid:not(.micro-grid-codigo) .micro-status {
3545
+ font-size: 0.95rem;
3546
+ }
3547
+
3548
+ .micro-grid:not(.micro-grid-codigo) .micro-msg-grid {
3549
+ display: flex;
3550
+ flex-wrap: wrap;
3551
+ gap: 5px 7px;
3552
+ }
3553
+
3554
+ .micro-grid:not(.micro-grid-codigo) .micro-msg {
3555
+ font-size: 0.75rem;
3556
+ line-height: 1.2;
3557
+ padding: 2px 7px;
3558
+ border: 1px solid #dfe8f1;
3559
+ border-radius: 999px;
3560
+ background: #f8fbff;
3561
+ max-width: 100%;
3562
+ white-space: normal;
3563
+ }
3564
+
3565
  ::-webkit-scrollbar {
3566
  width: 8px;
3567
  height: 8px;
 
3686
  grid-template-columns: 1fr;
3687
  }
3688
 
3689
+ .model-source-choice-grid {
3690
+ justify-content: flex-start;
3691
+ }
3692
+
3693
+ button.model-source-choice-btn {
3694
+ min-width: 220px;
3695
+ max-width: none;
3696
+ }
3697
+
3698
  .pesquisa-filtros-groups,
3699
  .pesquisa-filtros-grid,
3700
  .pesquisa-fields-grid,
 
3788
  .micro-msg-grid-codigo {
3789
  grid-template-columns: 1fr;
3790
  }
3791
+
3792
+ .upload-file-info-grid {
3793
+ grid-template-columns: 1fr;
3794
+ }
3795
  }