Guilherme Silberfarb Costa commited on
Commit
303655d
·
1 Parent(s): 97e9b7f

melhorias esteticas e funcionais

Browse files
backend/app/core/elaboracao/formatadores.py CHANGED
@@ -541,9 +541,15 @@ def formatar_avaliacao_html(avaliacoes_lista, indice_base=0, elem_id_excluir="ex
541
 
542
  n = len(avaliacoes_lista)
543
 
544
- # Corrigir índice base se fora de range
545
- if indice_base < 0 or indice_base >= n:
546
- indice_base = 0
 
 
 
 
 
 
547
 
548
  # Estilos reutilizáveis
549
  _td = 'style="padding: 6px 12px; border-bottom: 1px solid #f0f0f0;"'
@@ -599,13 +605,13 @@ def formatar_avaliacao_html(avaliacoes_lista, indice_base=0, elem_id_excluir="ex
599
  html += f'<td {_td_bold_r} style="text-align: right; padding: 8px 12px; border-bottom: 1px solid #dee2e6; font-weight: 600;">{_formatar_brl(aval["estimado"])}</td>'
600
  html += '</tr>'
601
 
602
- # Estimado / Base (só se houver mais de 1 avaliação)
603
- if n > 1:
604
- estimado_base = avaliacoes_lista[indice_base]["estimado"]
605
  html += '<tr style="background: #fff8f0; font-size: 12px;">'
606
- html += f'<td style="padding: 4px 12px; border-bottom: 1px solid #f0f0f0; font-weight: 500; color: #6c757d; font-style: italic;">Estimado / Base (Aval. {indice_base + 1})</td>'
607
  for i, aval in enumerate(avaliacoes_lista):
608
- if i == indice_base:
609
  celula = '<span style="color: #FF8C00; font-weight: 600;">Base</span>'
610
  else:
611
  if estimado_base != 0:
 
541
 
542
  n = len(avaliacoes_lista)
543
 
544
+ indice_base_normalizado = None
545
+ if indice_base is not None:
546
+ try:
547
+ indice_base_normalizado = int(indice_base)
548
+ except (TypeError, ValueError):
549
+ indice_base_normalizado = 0
550
+
551
+ if indice_base_normalizado < 0 or indice_base_normalizado >= n:
552
+ indice_base_normalizado = 0
553
 
554
  # Estilos reutilizáveis
555
  _td = 'style="padding: 6px 12px; border-bottom: 1px solid #f0f0f0;"'
 
605
  html += f'<td {_td_bold_r} style="text-align: right; padding: 8px 12px; border-bottom: 1px solid #dee2e6; font-weight: 600;">{_formatar_brl(aval["estimado"])}</td>'
606
  html += '</tr>'
607
 
608
+ # Estimado / Base (só se houver mais de 1 avaliação e base selecionada)
609
+ if n > 1 and indice_base_normalizado is not None:
610
+ estimado_base = avaliacoes_lista[indice_base_normalizado]["estimado"]
611
  html += '<tr style="background: #fff8f0; font-size: 12px;">'
612
+ html += f'<td style="padding: 4px 12px; border-bottom: 1px solid #f0f0f0; font-weight: 500; color: #6c757d; font-style: italic;">Estimado / Base (Aval. {indice_base_normalizado + 1})</td>'
613
  for i, aval in enumerate(avaliacoes_lista):
614
+ if i == indice_base_normalizado:
615
  celula = '<span style="color: #FF8C00; font-weight: 600;">Base</span>'
616
  else:
617
  if estimado_base != 0:
backend/app/services/elaboracao_service.py CHANGED
@@ -56,12 +56,39 @@ _AVALIADORES_PATH = Path(__file__).resolve().parent.parent / "core" / "elaboraca
56
  _AVALIADORES_CACHE: list[dict[str, Any]] | None = None
57
  LIMIAR_DISPERSAO_PNG = 1500
58
  LOGGER = logging.getLogger(__name__)
 
59
 
60
 
61
  def _is_rh_col(coluna: str) -> bool:
62
  return str(coluna or "").strip().upper() == "RH"
63
 
64
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  def _parse_data_iso_segura(value: Any) -> str | None:
66
  if value is None:
67
  return None
@@ -1965,11 +1992,11 @@ def calcular_avaliacao_elaboracao(
1965
  raise HTTPException(status_code=400, detail="Erro ao calcular avaliacao")
1966
 
1967
  session.avaliacoes_elaboracao.append(resultado)
1968
- idx_base = int(indice_base) - 1 if indice_base else 0
 
1969
  html = formatar_avaliacao_html(session.avaliacoes_elaboracao, indice_base=idx_base, elem_id_excluir="excluir-aval-elab")
1970
 
1971
- choices = [str(i + 1) for i in range(len(session.avaliacoes_elaboracao))]
1972
- base = indice_base if indice_base else "1"
1973
 
1974
  return {
1975
  "resultado_html": html,
@@ -1990,12 +2017,15 @@ def limpar_avaliacoes_elaboracao(session: SessionState) -> dict[str, Any]:
1990
 
1991
 
1992
  def excluir_avaliacao_elaboracao(session: SessionState, indice_str: str | None, indice_base_str: str | None) -> dict[str, Any]:
 
 
 
1993
  if not indice_str or not session.avaliacoes_elaboracao:
1994
  return {
1995
  "resultado_html": None,
1996
  "avaliacoes": sanitize_value(session.avaliacoes_elaboracao),
1997
  "base_choices": [str(i + 1) for i in range(len(session.avaliacoes_elaboracao))],
1998
- "base_value": indice_base_str,
1999
  }
2000
 
2001
  try:
@@ -2005,7 +2035,7 @@ def excluir_avaliacao_elaboracao(session: SessionState, indice_str: str | None,
2005
  "resultado_html": None,
2006
  "avaliacoes": sanitize_value(session.avaliacoes_elaboracao),
2007
  "base_choices": [str(i + 1) for i in range(len(session.avaliacoes_elaboracao))],
2008
- "base_value": indice_base_str,
2009
  }
2010
 
2011
  if idx < 0 or idx >= len(session.avaliacoes_elaboracao):
@@ -2013,7 +2043,7 @@ def excluir_avaliacao_elaboracao(session: SessionState, indice_str: str | None,
2013
  "resultado_html": None,
2014
  "avaliacoes": sanitize_value(session.avaliacoes_elaboracao),
2015
  "base_choices": [str(i + 1) for i in range(len(session.avaliacoes_elaboracao))],
2016
- "base_value": indice_base_str,
2017
  }
2018
 
2019
  nova_lista = [a for i, a in enumerate(session.avaliacoes_elaboracao) if i != idx]
@@ -2027,27 +2057,22 @@ def excluir_avaliacao_elaboracao(session: SessionState, indice_str: str | None,
2027
  "base_value": None,
2028
  }
2029
 
2030
- base = int(indice_base_str) - 1 if indice_base_str else 0
2031
- if base >= len(nova_lista):
2032
- base = len(nova_lista) - 1
2033
- if base < 0:
2034
- base = 0
2035
-
2036
- html = formatar_avaliacao_html(nova_lista, indice_base=base, elem_id_excluir="excluir-aval-elab")
2037
  choices = [str(i + 1) for i in range(len(nova_lista))]
2038
 
2039
  return {
2040
  "resultado_html": html,
2041
  "avaliacoes": sanitize_value(nova_lista),
2042
  "base_choices": choices,
2043
- "base_value": str(base + 1),
2044
  }
2045
 
2046
 
2047
  def atualizar_base_avaliacao_elaboracao(session: SessionState, indice_base_str: str | None) -> dict[str, Any]:
2048
  if not session.avaliacoes_elaboracao:
2049
  return {"resultado_html": ""}
2050
- indice = int(indice_base_str) - 1 if indice_base_str else 0
2051
  html = formatar_avaliacao_html(session.avaliacoes_elaboracao, indice_base=indice, elem_id_excluir="excluir-aval-elab")
2052
  return {"resultado_html": html}
2053
 
 
56
  _AVALIADORES_CACHE: list[dict[str, Any]] | None = None
57
  LIMIAR_DISPERSAO_PNG = 1500
58
  LOGGER = logging.getLogger(__name__)
59
+ BASE_COMPARACAO_SEM_BASE = "__none__"
60
 
61
 
62
  def _is_rh_col(coluna: str) -> bool:
63
  return str(coluna or "").strip().upper() == "RH"
64
 
65
 
66
+ def _resolver_indice_base(
67
+ indice_base_raw: str | None,
68
+ total_avaliacoes: int,
69
+ default_para_primeira: bool = True,
70
+ ) -> tuple[int | None, str]:
71
+ if total_avaliacoes <= 0:
72
+ return None, BASE_COMPARACAO_SEM_BASE
73
+
74
+ raw = "" if indice_base_raw is None else str(indice_base_raw).strip()
75
+ raw_lower = raw.lower()
76
+ if raw_lower in {"__none__", "sem_base", "none"}:
77
+ return None, BASE_COMPARACAO_SEM_BASE
78
+
79
+ if raw:
80
+ try:
81
+ indice = int(raw) - 1
82
+ except (TypeError, ValueError):
83
+ indice = None
84
+ if indice is not None and 0 <= indice < total_avaliacoes:
85
+ return indice, str(indice + 1)
86
+
87
+ if default_para_primeira:
88
+ return 0, "1"
89
+ return None, BASE_COMPARACAO_SEM_BASE
90
+
91
+
92
  def _parse_data_iso_segura(value: Any) -> str | None:
93
  if value is None:
94
  return None
 
1992
  raise HTTPException(status_code=400, detail="Erro ao calcular avaliacao")
1993
 
1994
  session.avaliacoes_elaboracao.append(resultado)
1995
+ total_avaliacoes = len(session.avaliacoes_elaboracao)
1996
+ idx_base, base = _resolver_indice_base(indice_base, total_avaliacoes, default_para_primeira=True)
1997
  html = formatar_avaliacao_html(session.avaliacoes_elaboracao, indice_base=idx_base, elem_id_excluir="excluir-aval-elab")
1998
 
1999
+ choices = [str(i + 1) for i in range(total_avaliacoes)]
 
2000
 
2001
  return {
2002
  "resultado_html": html,
 
2017
 
2018
 
2019
  def excluir_avaliacao_elaboracao(session: SessionState, indice_str: str | None, indice_base_str: str | None) -> dict[str, Any]:
2020
+ total_atual = len(session.avaliacoes_elaboracao)
2021
+ _, base_value_atual = _resolver_indice_base(indice_base_str, total_atual, default_para_primeira=True)
2022
+
2023
  if not indice_str or not session.avaliacoes_elaboracao:
2024
  return {
2025
  "resultado_html": None,
2026
  "avaliacoes": sanitize_value(session.avaliacoes_elaboracao),
2027
  "base_choices": [str(i + 1) for i in range(len(session.avaliacoes_elaboracao))],
2028
+ "base_value": (base_value_atual if session.avaliacoes_elaboracao else None),
2029
  }
2030
 
2031
  try:
 
2035
  "resultado_html": None,
2036
  "avaliacoes": sanitize_value(session.avaliacoes_elaboracao),
2037
  "base_choices": [str(i + 1) for i in range(len(session.avaliacoes_elaboracao))],
2038
+ "base_value": (base_value_atual if session.avaliacoes_elaboracao else None),
2039
  }
2040
 
2041
  if idx < 0 or idx >= len(session.avaliacoes_elaboracao):
 
2043
  "resultado_html": None,
2044
  "avaliacoes": sanitize_value(session.avaliacoes_elaboracao),
2045
  "base_choices": [str(i + 1) for i in range(len(session.avaliacoes_elaboracao))],
2046
+ "base_value": (base_value_atual if session.avaliacoes_elaboracao else None),
2047
  }
2048
 
2049
  nova_lista = [a for i, a in enumerate(session.avaliacoes_elaboracao) if i != idx]
 
2057
  "base_value": None,
2058
  }
2059
 
2060
+ base_idx, base_value = _resolver_indice_base(indice_base_str, len(nova_lista), default_para_primeira=True)
2061
+ html = formatar_avaliacao_html(nova_lista, indice_base=base_idx, elem_id_excluir="excluir-aval-elab")
 
 
 
 
 
2062
  choices = [str(i + 1) for i in range(len(nova_lista))]
2063
 
2064
  return {
2065
  "resultado_html": html,
2066
  "avaliacoes": sanitize_value(nova_lista),
2067
  "base_choices": choices,
2068
+ "base_value": base_value,
2069
  }
2070
 
2071
 
2072
  def atualizar_base_avaliacao_elaboracao(session: SessionState, indice_base_str: str | None) -> dict[str, Any]:
2073
  if not session.avaliacoes_elaboracao:
2074
  return {"resultado_html": ""}
2075
+ indice, _ = _resolver_indice_base(indice_base_str, len(session.avaliacoes_elaboracao), default_para_primeira=True)
2076
  html = formatar_avaliacao_html(session.avaliacoes_elaboracao, indice_base=indice, elem_id_excluir="excluir-aval-elab")
2077
  return {"resultado_html": html}
2078
 
backend/app/services/pesquisa_service.py CHANGED
@@ -10,6 +10,7 @@ from threading import Lock
10
  from typing import Any
11
 
12
  import folium
 
13
  import numpy as np
14
  import pandas as pd
15
  from fastapi import HTTPException
@@ -484,6 +485,7 @@ def gerar_mapa_modelos(modelos_ids: list[str], limite_pontos_por_modelo: int = 4
484
  camada_indices.add_to(mapa)
485
 
486
  folium.LayerControl(collapsed=False).add_to(mapa)
 
487
  add_zoom_responsive_circle_markers(mapa)
488
  if bounds:
489
  lat_values = [float(coord[0]) for coord in bounds]
 
10
  from typing import Any
11
 
12
  import folium
13
+ from folium import plugins
14
  import numpy as np
15
  import pandas as pd
16
  from fastapi import HTTPException
 
485
  camada_indices.add_to(mapa)
486
 
487
  folium.LayerControl(collapsed=False).add_to(mapa)
488
+ plugins.Fullscreen().add_to(mapa)
489
  add_zoom_responsive_circle_markers(mapa)
490
  if bounds:
491
  lat_values = [float(coord[0]) for coord in bounds]
backend/app/services/visualizacao_service.py CHANGED
@@ -18,12 +18,39 @@ from app.services.serializers import dataframe_to_payload, figure_to_payload, sa
18
 
19
 
20
  CHAVES_ESPERADAS = ["versao", "dados", "transformacoes", "modelo"]
 
21
 
22
 
23
  def _is_rh_col(coluna: str) -> bool:
24
  return str(coluna or "").strip().upper() == "RH"
25
 
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  def listar_modelos_repositorio() -> dict[str, Any]:
28
  return sanitize_value(model_repository.list_repository_models())
29
 
@@ -341,10 +368,10 @@ def calcular_avaliacao(session: SessionState, valores_x: dict[str, Any], indice_
341
 
342
  session.avaliacoes_visualizacao.append(resultado)
343
 
344
- indice = int(indice_base) - 1 if indice_base else 0
 
345
  html = formatar_avaliacao_html(session.avaliacoes_visualizacao, indice_base=indice, elem_id_excluir="excluir-aval-viz")
346
  choices = [str(i + 1) for i in range(len(session.avaliacoes_visualizacao))]
347
- base_value = indice_base if indice_base else "1"
348
 
349
  return {
350
  "resultado_html": html,
@@ -365,12 +392,15 @@ def limpar_avaliacoes(session: SessionState) -> dict[str, Any]:
365
 
366
 
367
  def excluir_avaliacao(session: SessionState, indice_str: str | None, indice_base_str: str | None) -> dict[str, Any]:
 
 
 
368
  if not indice_str or not session.avaliacoes_visualizacao:
369
  return {
370
  "resultado_html": None,
371
  "avaliacoes": sanitize_value(session.avaliacoes_visualizacao),
372
  "base_choices": [str(i + 1) for i in range(len(session.avaliacoes_visualizacao))],
373
- "base_value": indice_base_str,
374
  }
375
 
376
  try:
@@ -380,7 +410,7 @@ def excluir_avaliacao(session: SessionState, indice_str: str | None, indice_base
380
  "resultado_html": None,
381
  "avaliacoes": sanitize_value(session.avaliacoes_visualizacao),
382
  "base_choices": [str(i + 1) for i in range(len(session.avaliacoes_visualizacao))],
383
- "base_value": indice_base_str,
384
  }
385
 
386
  if idx < 0 or idx >= len(session.avaliacoes_visualizacao):
@@ -388,7 +418,7 @@ def excluir_avaliacao(session: SessionState, indice_str: str | None, indice_base
388
  "resultado_html": None,
389
  "avaliacoes": sanitize_value(session.avaliacoes_visualizacao),
390
  "base_choices": [str(i + 1) for i in range(len(session.avaliacoes_visualizacao))],
391
- "base_value": indice_base_str,
392
  }
393
 
394
  nova_lista = [a for i, a in enumerate(session.avaliacoes_visualizacao) if i != idx]
@@ -402,12 +432,7 @@ def excluir_avaliacao(session: SessionState, indice_str: str | None, indice_base
402
  "base_value": None,
403
  }
404
 
405
- base = int(indice_base_str) - 1 if indice_base_str else 0
406
- if base >= len(nova_lista):
407
- base = len(nova_lista) - 1
408
- if base < 0:
409
- base = 0
410
-
411
  html = formatar_avaliacao_html(nova_lista, indice_base=base, elem_id_excluir="excluir-aval-viz")
412
  choices = [str(i + 1) for i in range(len(nova_lista))]
413
 
@@ -415,14 +440,14 @@ def excluir_avaliacao(session: SessionState, indice_str: str | None, indice_base
415
  "resultado_html": html,
416
  "avaliacoes": sanitize_value(nova_lista),
417
  "base_choices": choices,
418
- "base_value": str(base + 1),
419
  }
420
 
421
 
422
  def atualizar_base(session: SessionState, indice_base_str: str | None) -> dict[str, Any]:
423
  if not session.avaliacoes_visualizacao:
424
  return {"resultado_html": ""}
425
- indice = int(indice_base_str) - 1 if indice_base_str else 0
426
  html = formatar_avaliacao_html(session.avaliacoes_visualizacao, indice_base=indice, elem_id_excluir="excluir-aval-viz")
427
  return {"resultado_html": html}
428
 
 
18
 
19
 
20
  CHAVES_ESPERADAS = ["versao", "dados", "transformacoes", "modelo"]
21
+ BASE_COMPARACAO_SEM_BASE = "__none__"
22
 
23
 
24
  def _is_rh_col(coluna: str) -> bool:
25
  return str(coluna or "").strip().upper() == "RH"
26
 
27
 
28
+ def _resolver_indice_base(
29
+ indice_base_raw: str | None,
30
+ total_avaliacoes: int,
31
+ default_para_primeira: bool = True,
32
+ ) -> tuple[int | None, str]:
33
+ if total_avaliacoes <= 0:
34
+ return None, BASE_COMPARACAO_SEM_BASE
35
+
36
+ raw = "" if indice_base_raw is None else str(indice_base_raw).strip()
37
+ raw_lower = raw.lower()
38
+ if raw_lower in {"__none__", "sem_base", "none"}:
39
+ return None, BASE_COMPARACAO_SEM_BASE
40
+
41
+ if raw:
42
+ try:
43
+ indice = int(raw) - 1
44
+ except (TypeError, ValueError):
45
+ indice = None
46
+ if indice is not None and 0 <= indice < total_avaliacoes:
47
+ return indice, str(indice + 1)
48
+
49
+ if default_para_primeira:
50
+ return 0, "1"
51
+ return None, BASE_COMPARACAO_SEM_BASE
52
+
53
+
54
  def listar_modelos_repositorio() -> dict[str, Any]:
55
  return sanitize_value(model_repository.list_repository_models())
56
 
 
368
 
369
  session.avaliacoes_visualizacao.append(resultado)
370
 
371
+ total_avaliacoes = len(session.avaliacoes_visualizacao)
372
+ indice, base_value = _resolver_indice_base(indice_base, total_avaliacoes, default_para_primeira=True)
373
  html = formatar_avaliacao_html(session.avaliacoes_visualizacao, indice_base=indice, elem_id_excluir="excluir-aval-viz")
374
  choices = [str(i + 1) for i in range(len(session.avaliacoes_visualizacao))]
 
375
 
376
  return {
377
  "resultado_html": html,
 
392
 
393
 
394
  def excluir_avaliacao(session: SessionState, indice_str: str | None, indice_base_str: str | None) -> dict[str, Any]:
395
+ total_atual = len(session.avaliacoes_visualizacao)
396
+ _, base_value_atual = _resolver_indice_base(indice_base_str, total_atual, default_para_primeira=True)
397
+
398
  if not indice_str or not session.avaliacoes_visualizacao:
399
  return {
400
  "resultado_html": None,
401
  "avaliacoes": sanitize_value(session.avaliacoes_visualizacao),
402
  "base_choices": [str(i + 1) for i in range(len(session.avaliacoes_visualizacao))],
403
+ "base_value": (base_value_atual if session.avaliacoes_visualizacao else None),
404
  }
405
 
406
  try:
 
410
  "resultado_html": None,
411
  "avaliacoes": sanitize_value(session.avaliacoes_visualizacao),
412
  "base_choices": [str(i + 1) for i in range(len(session.avaliacoes_visualizacao))],
413
+ "base_value": (base_value_atual if session.avaliacoes_visualizacao else None),
414
  }
415
 
416
  if idx < 0 or idx >= len(session.avaliacoes_visualizacao):
 
418
  "resultado_html": None,
419
  "avaliacoes": sanitize_value(session.avaliacoes_visualizacao),
420
  "base_choices": [str(i + 1) for i in range(len(session.avaliacoes_visualizacao))],
421
+ "base_value": (base_value_atual if session.avaliacoes_visualizacao else None),
422
  }
423
 
424
  nova_lista = [a for i, a in enumerate(session.avaliacoes_visualizacao) if i != idx]
 
432
  "base_value": None,
433
  }
434
 
435
+ base, base_value = _resolver_indice_base(indice_base_str, len(nova_lista), default_para_primeira=True)
 
 
 
 
 
436
  html = formatar_avaliacao_html(nova_lista, indice_base=base, elem_id_excluir="excluir-aval-viz")
437
  choices = [str(i + 1) for i in range(len(nova_lista))]
438
 
 
440
  "resultado_html": html,
441
  "avaliacoes": sanitize_value(nova_lista),
442
  "base_choices": choices,
443
+ "base_value": base_value,
444
  }
445
 
446
 
447
  def atualizar_base(session: SessionState, indice_base_str: str | None) -> dict[str, Any]:
448
  if not session.avaliacoes_visualizacao:
449
  return {"resultado_html": ""}
450
+ indice, _ = _resolver_indice_base(indice_base_str, len(session.avaliacoes_visualizacao), default_para_primeira=True)
451
  html = formatar_avaliacao_html(session.avaliacoes_visualizacao, indice_base=indice, elem_id_excluir="excluir-aval-viz")
452
  return {"resultado_html": html}
453
 
frontend/src/components/AvaliacaoTab.jsx CHANGED
@@ -71,6 +71,8 @@ function formatarFonteRepositorio(fonte) {
71
  return 'Fonte: pasta local'
72
  }
73
 
 
 
74
  export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
75
  const [loading, setLoading] = useState(false)
76
  const [error, setError] = useState('')
@@ -92,7 +94,7 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
92
  const [avaliacaoFormVersion, setAvaliacaoFormVersion] = useState(0)
93
 
94
  const [avaliacoesCards, setAvaliacoesCards] = useState([])
95
- const [baseCardId, setBaseCardId] = useState('')
96
  const [confirmarLimpezaAvaliacoes, setConfirmarLimpezaAvaliacoes] = useState(false)
97
 
98
  const uploadInputRef = useRef(null)
@@ -107,10 +109,10 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
107
  [repoModelos],
108
  )
109
 
110
- const baseCard = useMemo(
111
- () => avaliacoesCards.find((item) => item.id === baseCardId) || null,
112
- [avaliacoesCards, baseCardId],
113
- )
114
 
115
  function resolverModeloIdRepositorio(chaveBruta, modelosOverride = null) {
116
  const chave = String(chaveBruta || '').trim()
@@ -274,10 +276,13 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
274
 
275
  useEffect(() => {
276
  if (!avaliacoesCards.length) {
277
- if (baseCardId) setBaseCardId('')
 
 
278
  return
279
  }
280
- if (!baseCardId || !avaliacoesCards.some((item) => item.id === baseCardId)) {
 
281
  setBaseCardId(avaliacoesCards[0].id)
282
  }
283
  }, [avaliacoesCards, baseCardId])
@@ -449,7 +454,7 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
449
 
450
  function onLimparAvaliacoes() {
451
  setAvaliacoesCards([])
452
- setBaseCardId('')
453
  setConfirmarLimpezaAvaliacoes(false)
454
  }
455
 
@@ -681,6 +686,7 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
681
  <div className="row avaliacao-base-row">
682
  <label>Base comparação</label>
683
  <select value={baseCardId} onChange={(event) => setBaseCardId(event.target.value)}>
 
684
  {avaliacoesCards.map((item, idx) => (
685
  <option key={`base-card-${item.id}`} value={item.id}>
686
  {`Aval. ${idx + 1} - ${item.modelo}`}
 
71
  return 'Fonte: pasta local'
72
  }
73
 
74
+ const BASE_COMPARACAO_SEM_BASE = '__none__'
75
+
76
  export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
77
  const [loading, setLoading] = useState(false)
78
  const [error, setError] = useState('')
 
94
  const [avaliacaoFormVersion, setAvaliacaoFormVersion] = useState(0)
95
 
96
  const [avaliacoesCards, setAvaliacoesCards] = useState([])
97
+ const [baseCardId, setBaseCardId] = useState(BASE_COMPARACAO_SEM_BASE)
98
  const [confirmarLimpezaAvaliacoes, setConfirmarLimpezaAvaliacoes] = useState(false)
99
 
100
  const uploadInputRef = useRef(null)
 
109
  [repoModelos],
110
  )
111
 
112
+ const baseCard = useMemo(() => {
113
+ if (baseCardId === BASE_COMPARACAO_SEM_BASE) return null
114
+ return avaliacoesCards.find((item) => item.id === baseCardId) || null
115
+ }, [avaliacoesCards, baseCardId])
116
 
117
  function resolverModeloIdRepositorio(chaveBruta, modelosOverride = null) {
118
  const chave = String(chaveBruta || '').trim()
 
276
 
277
  useEffect(() => {
278
  if (!avaliacoesCards.length) {
279
+ if (baseCardId !== BASE_COMPARACAO_SEM_BASE) {
280
+ setBaseCardId(BASE_COMPARACAO_SEM_BASE)
281
+ }
282
  return
283
  }
284
+ if (baseCardId === BASE_COMPARACAO_SEM_BASE || !baseCardId) return
285
+ if (!avaliacoesCards.some((item) => item.id === baseCardId)) {
286
  setBaseCardId(avaliacoesCards[0].id)
287
  }
288
  }, [avaliacoesCards, baseCardId])
 
454
 
455
  function onLimparAvaliacoes() {
456
  setAvaliacoesCards([])
457
+ setBaseCardId(BASE_COMPARACAO_SEM_BASE)
458
  setConfirmarLimpezaAvaliacoes(false)
459
  }
460
 
 
686
  <div className="row avaliacao-base-row">
687
  <label>Base comparação</label>
688
  <select value={baseCardId} onChange={(event) => setBaseCardId(event.target.value)}>
689
+ <option value={BASE_COMPARACAO_SEM_BASE}>Sem base</option>
690
  {avaliacoesCards.map((item, idx) => (
691
  <option key={`base-card-${item.id}`} value={item.id}>
692
  {`Aval. ${idx + 1} - ${item.modelo}`}
frontend/src/components/ElaboracaoTab.jsx CHANGED
@@ -35,7 +35,8 @@ const MAPA_MODO_SUPERFICIE = 'superficie'
35
  const MAPA_RESIDUOS_VARIAVEL = 'Resíduo Pad.'
36
  const MAPA_RESIDUOS_EXTREMO_LIVRE = 'livre'
37
  const MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT = MAPA_RESIDUOS_EXTREMO_LIVRE
38
- const MAPA_RESIDUOS_EXTREMO_ABS_OPTIONS = [2, 3, 4, 5, 6, 7, 8, 10]
 
39
  const OUTLIER_RECURSIVO_TOOLTIP = 'Aplicar com recursividade executa os mesmos filtros em ciclos sucessivos: nos bastidores, simula a exclusão dos índices encontrados, recalcula o ajuste do modelo e as métricas de outlier e reaplica os filtros, repetindo até não surgir nenhum índice novo. Para você, o resultado prático é que o campo "A excluir" é preenchido automaticamente com o conjunto total de índices encontrados nessa simulação recursiva.'
40
  const ELABORACAO_SECOES_NAV = [
41
  { step: '1', title: 'Importar Dados' },
@@ -766,6 +767,7 @@ export default function ElaboracaoTab({ sessionId }) {
766
  const [repoModeloSelecionado, setRepoModeloSelecionado] = useState('')
767
  const [repoModelosLoading, setRepoModelosLoading] = useState(false)
768
  const [repoFonteModelos, setRepoFonteModelos] = useState('')
 
769
 
770
  const [dados, setDados] = useState(null)
771
  const [mapaHtml, setMapaHtml] = useState('')
@@ -1067,6 +1069,35 @@ export default function ElaboracaoTab({ sessionId }) {
1067
 
1068
  return Array.from(indices)
1069
  }, [fit?.tabela_metricas, filtros, outlierHighlightIndexColumn])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1070
  const resumoResiduoPadStats = useMemo(() => {
1071
  const rows = fit?.tabela_metricas?.rows
1072
  if (!Array.isArray(rows) || rows.length === 0) return null
@@ -1298,6 +1329,12 @@ export default function ElaboracaoTab({ sessionId }) {
1298
  setDisabledHint(null)
1299
  }, [])
1300
 
 
 
 
 
 
 
1301
  const onDisabledHintEnter = useCallback((event, showHint, hintText) => {
1302
  if (!showHint || !hintText || typeof window === 'undefined') {
1303
  setDisabledHint(null)
@@ -3339,7 +3376,7 @@ export default function ElaboracaoTab({ sessionId }) {
3339
 
3340
  return (
3341
  <div ref={elaboracaoRootRef} className="tab-content">
3342
- <div className="elaboracao-layout" style={sideNavDynamicStyle}>
3343
  <aside className="elaboracao-side-nav" aria-label="Navegação de seções da elaboração">
3344
  <ol className="elaboracao-side-nav-list">
3345
  {ELABORACAO_SECOES_NAV.map((secao) => {
@@ -3365,42 +3402,45 @@ export default function ElaboracaoTab({ sessionId }) {
3365
  </aside>
3366
 
3367
  <div key={`sections-${sectionsMountKey}`} className="workflow-sections-stack elaboracao-sections-stack">
3368
- <SectionBlock step="1" title="Importar Dados" subtitle="Upload de CSV, Excel ou .dai com recuperação do fluxo.">
3369
- <div className="section1-groups">
3370
- <div className="subpanel section1-group">
3371
- {!modeloLoadSource ? (
3372
- <div className="model-source-choice-grid">
3373
- <button
3374
- type="button"
3375
- className="model-source-choice-btn model-source-choice-btn-primary"
3376
- onClick={() => {
3377
- setModeloLoadSource('repo')
3378
- setImportacaoErro('')
3379
- }}
3380
- disabled={loading}
3381
- >
3382
- Carregar modelo do repositório
3383
- </button>
3384
- <button
3385
- type="button"
3386
- className="model-source-choice-btn model-source-choice-btn-secondary"
3387
- onClick={() => {
3388
- setModeloLoadSource('upload')
3389
- setImportacaoErro('')
3390
- }}
3391
- disabled={loading}
3392
- >
3393
- Fazer upload de Excel ou modelo
3394
- </button>
3395
- </div>
3396
- ) : (
3397
- <div className="model-source-flow">
 
 
3398
  <div className="model-source-flow-head">
3399
  <button
3400
  type="button"
3401
  className="model-source-back-btn"
3402
  onClick={() => {
3403
  setModeloLoadSource('')
 
3404
  setImportacaoErro('')
3405
  }}
3406
  disabled={loading}
@@ -3421,6 +3461,7 @@ export default function ElaboracaoTab({ sessionId }) {
3421
  emptyMessage={repoModeloOptions.length > 0 ? 'Nenhum modelo encontrado.' : 'Nenhum modelo disponível.'}
3422
  loading={repoModelosLoading}
3423
  disabled={loading || repoModelosLoading || repoModeloOptions.length === 0}
 
3424
  />
3425
  </label>
3426
  <div className="row compact upload-repo-actions">
@@ -5145,64 +5186,61 @@ export default function ElaboracaoTab({ sessionId }) {
5145
 
5146
  <div className="sec16-subsection">
5147
  <h5 className="sec16-subtitle">Mapa de Resíduos Padronizados</h5>
5148
- <details className="dados-mapa-details" open>
5149
- <summary>Mostrar/Ocultar mapa</summary>
5150
- {!mapaResiduosGerado ? (
5151
- <div className="empty-box">
5152
- <div className="row">
5153
- <button type="button" className="btn-gerar-mapa" onClick={onGerarMapaResiduos} disabled={loading}>
5154
- Gerar Mapa de Resíduos Padronizados
5155
- </button>
5156
- </div>
5157
- <div className="section1-empty-hint">O mapa de resíduos padronizados será carregado somente após solicitação explícita.</div>
5158
  </div>
5159
- ) : (
5160
- <>
5161
- <div className="dados-mapa-controls">
5162
- <div className="dados-mapa-control-field">
5163
- <label>Visualização</label>
5164
- <select value={mapaResiduosModo} onChange={(e) => onMapaResiduosModoChange(e.target.value)}>
5165
- <option value={MAPA_MODO_PONTOS}>Pontos</option>
5166
- <option value={MAPA_MODO_CALOR}>Mapa de calor</option>
5167
- <option value={MAPA_MODO_SUPERFICIE}>Superfície contínua</option>
5168
- </select>
5169
- </div>
5170
- <div className="dados-mapa-control-field">
5171
- <label>Extremos da escala (abs.)</label>
5172
- <select
5173
- value={String(mapaResiduosExtremoAbs)}
5174
- onChange={(e) => {
5175
- void onMapaResiduosExtremoAbsChange(e.target.value)
5176
- }}
5177
- >
5178
- <option value={MAPA_RESIDUOS_EXTREMO_LIVRE}>Livre (limites dos dados)</option>
5179
- {MAPA_RESIDUOS_EXTREMO_ABS_OPTIONS.map((valor) => (
5180
- <option key={`mapa-res-ext-${valor}`} value={String(valor)}>
5181
- ±{formatNumberBr(valor, 1)}
5182
- </option>
5183
- ))}
5184
- </select>
5185
- </div>
5186
- </div>
5187
- <div className="residuos-map-scale-hint">
5188
- {mapaResiduosExtremoAbsAtivo === null
5189
- ? 'Escala livre: os extremos seguem os limites observados dos resíduos padronizados.'
5190
- : `Escala fixa: valores ≤ -${formatNumberBr(mapaResiduosExtremoAbsAtivo, 2)} e ≥ ${formatNumberBr(mapaResiduosExtremoAbsAtivo, 2)} usam as cores máximas.`}
5191
  </div>
5192
- <div className="download-actions-bar">
5193
- <button
5194
- type="button"
5195
- className="btn-download-subtle"
5196
- onClick={onDownloadMapaSecao16}
5197
- disabled={loading || downloadingAssets || !mapaResiduosHtml}
 
5198
  >
5199
- Fazer download
5200
- </button>
 
 
 
 
 
5201
  </div>
5202
- <MapFrame html={mapaResiduosHtml} />
5203
- </>
5204
- )}
5205
- </details>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5206
  </div>
5207
 
5208
  <div className="sec16-subsection">
@@ -5295,6 +5333,32 @@ export default function ElaboracaoTab({ sessionId }) {
5295
  >
5296
  Remover
5297
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5298
  </div>
5299
  ))}
5300
  </div>
@@ -5392,7 +5456,7 @@ export default function ElaboracaoTab({ sessionId }) {
5392
  <div className="row avaliacao-base-row">
5393
  <label>Base comparação</label>
5394
  <select value={baseValue || ''} onChange={(e) => onBaseChange(e.target.value)}>
5395
- <option value="">Selecione</option>
5396
  {baseChoices.map((choice) => (
5397
  <option key={`base-${choice}`} value={choice}>{choice}</option>
5398
  ))}
 
35
  const MAPA_RESIDUOS_VARIAVEL = 'Resíduo Pad.'
36
  const MAPA_RESIDUOS_EXTREMO_LIVRE = 'livre'
37
  const MAPA_RESIDUOS_EXTREMO_ABS_DEFAULT = MAPA_RESIDUOS_EXTREMO_LIVRE
38
+ const MAPA_RESIDUOS_EXTREMO_ABS_OPTIONS = [1, 2, 3, 4, 5, 6, 7, 8, 10]
39
+ const BASE_COMPARACAO_SEM_BASE = '__none__'
40
  const OUTLIER_RECURSIVO_TOOLTIP = 'Aplicar com recursividade executa os mesmos filtros em ciclos sucessivos: nos bastidores, simula a exclusão dos índices encontrados, recalcula o ajuste do modelo e as métricas de outlier e reaplica os filtros, repetindo até não surgir nenhum índice novo. Para você, o resultado prático é que o campo "A excluir" é preenchido automaticamente com o conjunto total de índices encontrados nessa simulação recursiva.'
41
  const ELABORACAO_SECOES_NAV = [
42
  { step: '1', title: 'Importar Dados' },
 
767
  const [repoModeloSelecionado, setRepoModeloSelecionado] = useState('')
768
  const [repoModelosLoading, setRepoModelosLoading] = useState(false)
769
  const [repoFonteModelos, setRepoFonteModelos] = useState('')
770
+ const [repoModeloDropdownOpen, setRepoModeloDropdownOpen] = useState(false)
771
 
772
  const [dados, setDados] = useState(null)
773
  const [mapaHtml, setMapaHtml] = useState('')
 
1069
 
1070
  return Array.from(indices)
1071
  }, [fit?.tabela_metricas, filtros, outlierHighlightIndexColumn])
1072
+ const outlierFaixasPorVariavel = useMemo(() => {
1073
+ const rows = fit?.tabela_metricas?.rows
1074
+ const variaveis = (fit?.variaveis_filtro || [])
1075
+ .map((item) => String(item || '').trim())
1076
+ .filter(Boolean)
1077
+ if (!Array.isArray(rows) || rows.length === 0 || variaveis.length === 0) return {}
1078
+
1079
+ const faixas = {}
1080
+ variaveis.forEach((variavel) => {
1081
+ let minimo = Number.POSITIVE_INFINITY
1082
+ let maximo = Number.NEGATIVE_INFINITY
1083
+ let totalValidos = 0
1084
+
1085
+ rows.forEach((row) => {
1086
+ if (!row || typeof row !== 'object') return
1087
+ const valor = toFiniteNumber(row[variavel])
1088
+ if (valor === null) return
1089
+ totalValidos += 1
1090
+ if (valor < minimo) minimo = valor
1091
+ if (valor > maximo) maximo = valor
1092
+ })
1093
+
1094
+ if (totalValidos > 0 && Number.isFinite(minimo) && Number.isFinite(maximo)) {
1095
+ faixas[variavel] = { minimo, maximo }
1096
+ }
1097
+ })
1098
+
1099
+ return faixas
1100
+ }, [fit?.tabela_metricas?.rows, fit?.variaveis_filtro])
1101
  const resumoResiduoPadStats = useMemo(() => {
1102
  const rows = fit?.tabela_metricas?.rows
1103
  if (!Array.isArray(rows) || rows.length === 0) return null
 
1329
  setDisabledHint(null)
1330
  }, [])
1331
 
1332
+ useEffect(() => {
1333
+ if (modeloLoadSource !== 'repo') {
1334
+ setRepoModeloDropdownOpen(false)
1335
+ }
1336
+ }, [modeloLoadSource])
1337
+
1338
  const onDisabledHintEnter = useCallback((event, showHint, hintText) => {
1339
  if (!showHint || !hintText || typeof window === 'undefined') {
1340
  setDisabledHint(null)
 
3376
 
3377
  return (
3378
  <div ref={elaboracaoRootRef} className="tab-content">
3379
+ <div className={`elaboracao-layout${repoModeloDropdownOpen ? ' is-repo-model-open' : ''}`} style={sideNavDynamicStyle}>
3380
  <aside className="elaboracao-side-nav" aria-label="Navegação de seções da elaboração">
3381
  <ol className="elaboracao-side-nav-list">
3382
  {ELABORACAO_SECOES_NAV.map((secao) => {
 
3402
  </aside>
3403
 
3404
  <div key={`sections-${sectionsMountKey}`} className="workflow-sections-stack elaboracao-sections-stack">
3405
+ <SectionBlock step="1" title="Importar Dados" subtitle="Upload de CSV, Excel ou .dai com recuperação do fluxo.">
3406
+ <div className="section1-groups">
3407
+ <div className="subpanel section1-group">
3408
+ {!modeloLoadSource ? (
3409
+ <div className="model-source-choice-grid">
3410
+ <button
3411
+ type="button"
3412
+ className="model-source-choice-btn model-source-choice-btn-primary"
3413
+ onClick={() => {
3414
+ setModeloLoadSource('repo')
3415
+ setRepoModeloDropdownOpen(false)
3416
+ setImportacaoErro('')
3417
+ }}
3418
+ disabled={loading}
3419
+ >
3420
+ Carregar modelo do repositório
3421
+ </button>
3422
+ <button
3423
+ type="button"
3424
+ className="model-source-choice-btn model-source-choice-btn-secondary"
3425
+ onClick={() => {
3426
+ setModeloLoadSource('upload')
3427
+ setRepoModeloDropdownOpen(false)
3428
+ setImportacaoErro('')
3429
+ }}
3430
+ disabled={loading}
3431
+ >
3432
+ Fazer upload de Excel ou modelo
3433
+ </button>
3434
+ </div>
3435
+ ) : (
3436
+ <div className="model-source-flow">
3437
  <div className="model-source-flow-head">
3438
  <button
3439
  type="button"
3440
  className="model-source-back-btn"
3441
  onClick={() => {
3442
  setModeloLoadSource('')
3443
+ setRepoModeloDropdownOpen(false)
3444
  setImportacaoErro('')
3445
  }}
3446
  disabled={loading}
 
3461
  emptyMessage={repoModeloOptions.length > 0 ? 'Nenhum modelo encontrado.' : 'Nenhum modelo disponível.'}
3462
  loading={repoModelosLoading}
3463
  disabled={loading || repoModelosLoading || repoModeloOptions.length === 0}
3464
+ onOpenChange={setRepoModeloDropdownOpen}
3465
  />
3466
  </label>
3467
  <div className="row compact upload-repo-actions">
 
5186
 
5187
  <div className="sec16-subsection">
5188
  <h5 className="sec16-subtitle">Mapa de Resíduos Padronizados</h5>
5189
+ {!mapaResiduosGerado ? (
5190
+ <div className="empty-box">
5191
+ <div className="row">
5192
+ <button type="button" className="btn-gerar-mapa" onClick={onGerarMapaResiduos} disabled={loading}>
5193
+ Gerar Mapa de Resíduos Padronizados
5194
+ </button>
 
 
 
 
5195
  </div>
5196
+ <div className="section1-empty-hint">O mapa de resíduos padronizados será carregado somente após solicitação explícita.</div>
5197
+ </div>
5198
+ ) : (
5199
+ <>
5200
+ <div className="dados-mapa-controls">
5201
+ <div className="dados-mapa-control-field">
5202
+ <label>Visualização</label>
5203
+ <select value={mapaResiduosModo} onChange={(e) => onMapaResiduosModoChange(e.target.value)}>
5204
+ <option value={MAPA_MODO_PONTOS}>Pontos</option>
5205
+ <option value={MAPA_MODO_CALOR}>Mapa de calor</option>
5206
+ <option value={MAPA_MODO_SUPERFICIE}>Superfície contínua</option>
5207
+ </select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5208
  </div>
5209
+ <div className="dados-mapa-control-field">
5210
+ <label>Extremos da escala (abs.)</label>
5211
+ <select
5212
+ value={String(mapaResiduosExtremoAbs)}
5213
+ onChange={(e) => {
5214
+ void onMapaResiduosExtremoAbsChange(e.target.value)
5215
+ }}
5216
  >
5217
+ <option value={MAPA_RESIDUOS_EXTREMO_LIVRE}>Livre (limites dos dados)</option>
5218
+ {MAPA_RESIDUOS_EXTREMO_ABS_OPTIONS.map((valor) => (
5219
+ <option key={`mapa-res-ext-${valor}`} value={String(valor)}>
5220
+ ±{formatNumberBr(valor, 1)}
5221
+ </option>
5222
+ ))}
5223
+ </select>
5224
  </div>
5225
+ </div>
5226
+ <div className="residuos-map-scale-hint">
5227
+ {mapaResiduosExtremoAbsAtivo === null
5228
+ ? 'Escala livre: os extremos seguem os limites observados dos resíduos padronizados.'
5229
+ : `Escala fixa: valores ≤ -${formatNumberBr(mapaResiduosExtremoAbsAtivo, 2)} e ≥ ${formatNumberBr(mapaResiduosExtremoAbsAtivo, 2)} usam as cores máximas.`}
5230
+ </div>
5231
+ <div className="download-actions-bar">
5232
+ <button
5233
+ type="button"
5234
+ className="btn-download-subtle"
5235
+ onClick={onDownloadMapaSecao16}
5236
+ disabled={loading || downloadingAssets || !mapaResiduosHtml}
5237
+ >
5238
+ Fazer download
5239
+ </button>
5240
+ </div>
5241
+ <MapFrame html={mapaResiduosHtml} />
5242
+ </>
5243
+ )}
5244
  </div>
5245
 
5246
  <div className="sec16-subsection">
 
5333
  >
5334
  Remover
5335
  </button>
5336
+ {(() => {
5337
+ const variavel = String(filtro?.variavel || '').trim()
5338
+ const faixa = outlierFaixasPorVariavel[variavel]
5339
+ if (!faixa) {
5340
+ return (
5341
+ <div className="filtro-row-faixa-hint filtro-row-faixa-hint-muted">
5342
+ Faixa atual indisponível para a variável selecionada.
5343
+ </div>
5344
+ )
5345
+ }
5346
+ return (
5347
+ <div className="filtro-row-faixa-hint">
5348
+ Faixa atual:
5349
+ {' '}
5350
+ mín
5351
+ {' '}
5352
+ <strong>{formatNumberBr(faixa.minimo, 4)}</strong>
5353
+ {' '}
5354
+ |
5355
+ {' '}
5356
+ máx
5357
+ {' '}
5358
+ <strong>{formatNumberBr(faixa.maximo, 4)}</strong>
5359
+ </div>
5360
+ )
5361
+ })()}
5362
  </div>
5363
  ))}
5364
  </div>
 
5456
  <div className="row avaliacao-base-row">
5457
  <label>Base comparação</label>
5458
  <select value={baseValue || ''} onChange={(e) => onBaseChange(e.target.value)}>
5459
+ <option value={BASE_COMPARACAO_SEM_BASE}>Sem base</option>
5460
  {baseChoices.map((choice) => (
5461
  <option key={`base-${choice}`} value={choice}>{choice}</option>
5462
  ))}
frontend/src/components/PesquisaTab.jsx CHANGED
@@ -1015,10 +1015,10 @@ export default function PesquisaTab({ sessionId, onUsarModeloEmAvaliacao = null
1015
  {selecionado ? 'Desmarcar' : 'Marcar para mapa'}
1016
  </button>
1017
  <button type="button" className="btn-pesquisa-open" onClick={() => void onAbrirModelo(modelo)}>
1018
- Abrir modelo
1019
  </button>
1020
  <button type="button" className="btn-pesquisa-eval" onClick={() => onUsarEmAvaliacao(modelo)}>
1021
- Usar em avaliação
1022
  </button>
1023
  </div>
1024
  <div className="pesquisa-card-body">
 
1015
  {selecionado ? 'Desmarcar' : 'Marcar para mapa'}
1016
  </button>
1017
  <button type="button" className="btn-pesquisa-open" onClick={() => void onAbrirModelo(modelo)}>
1018
+ Abrir
1019
  </button>
1020
  <button type="button" className="btn-pesquisa-eval" onClick={() => onUsarEmAvaliacao(modelo)}>
1021
+ Avaliação
1022
  </button>
1023
  </div>
1024
  <div className="pesquisa-card-body">
frontend/src/components/SinglePillAutocomplete.jsx CHANGED
@@ -31,6 +31,7 @@ export default function SinglePillAutocomplete({
31
  emptyMessage = 'Nenhuma sugestao encontrada.',
32
  loading = false,
33
  disabled = false,
 
34
  }) {
35
  const rootRef = useRef(null)
36
  const inputRef = useRef(null)
@@ -79,6 +80,15 @@ export default function SinglePillAutocomplete({
79
  return () => document.removeEventListener('mousedown', onDocumentMouseDown)
80
  }, [open])
81
 
 
 
 
 
 
 
 
 
 
82
  useEffect(() => {
83
  if (!open || filteredOptions.length === 0) {
84
  setActiveIndex(-1)
 
31
  emptyMessage = 'Nenhuma sugestao encontrada.',
32
  loading = false,
33
  disabled = false,
34
+ onOpenChange = null,
35
  }) {
36
  const rootRef = useRef(null)
37
  const inputRef = useRef(null)
 
80
  return () => document.removeEventListener('mousedown', onDocumentMouseDown)
81
  }, [open])
82
 
83
+ useEffect(() => {
84
+ if (typeof onOpenChange !== 'function') return
85
+ onOpenChange(Boolean(open && !disabled))
86
+ }, [open, disabled, onOpenChange])
87
+
88
+ useEffect(() => () => {
89
+ if (typeof onOpenChange === 'function') onOpenChange(false)
90
+ }, [onOpenChange])
91
+
92
  useEffect(() => {
93
  if (!open || filteredOptions.length === 0) {
94
  setActiveIndex(-1)
frontend/src/components/VisualizacaoTab.jsx CHANGED
@@ -20,6 +20,7 @@ const INNER_TABS = [
20
  { key: 'avaliacao', label: 'Avaliação' },
21
  { key: 'avaliacao_massa', label: 'Avaliação em Massa' },
22
  ]
 
23
 
24
  export default function VisualizacaoTab({ sessionId }) {
25
  const [loading, setLoading] = useState(false)
@@ -627,7 +628,7 @@ export default function VisualizacaoTab({ sessionId }) {
627
  <div className="row avaliacao-base-row">
628
  <label>Base comparação</label>
629
  <select value={baseValue || ''} onChange={(e) => onBaseChange(e.target.value)}>
630
- <option value="">Selecione</option>
631
  {baseChoices.map((choice) => (
632
  <option key={`base-${choice}`} value={choice}>{choice}</option>
633
  ))}
 
20
  { key: 'avaliacao', label: 'Avaliação' },
21
  { key: 'avaliacao_massa', label: 'Avaliação em Massa' },
22
  ]
23
+ const BASE_COMPARACAO_SEM_BASE = '__none__'
24
 
25
  export default function VisualizacaoTab({ sessionId }) {
26
  const [loading, setLoading] = useState(false)
 
628
  <div className="row avaliacao-base-row">
629
  <label>Base comparação</label>
630
  <select value={baseValue || ''} onChange={(e) => onBaseChange(e.target.value)}>
631
+ <option value={BASE_COMPARACAO_SEM_BASE}>Sem base</option>
632
  {baseChoices.map((choice) => (
633
  <option key={`base-${choice}`} value={choice}>{choice}</option>
634
  ))}
frontend/src/styles.css CHANGED
@@ -852,6 +852,15 @@ textarea {
852
  gap: 14px;
853
  }
854
 
 
 
 
 
 
 
 
 
 
855
  .elaboracao-side-nav {
856
  position: sticky;
857
  top: 96px;
@@ -3794,6 +3803,23 @@ button.btn-upload-select {
3794
  border-color: #ffba66;
3795
  }
3796
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3797
  .outlier-subheader {
3798
  display: flex;
3799
  align-items: center;
@@ -4759,6 +4785,11 @@ button.btn-download-subtle {
4759
  border-top: 1px solid #dde7f1;
4760
  }
4761
 
 
 
 
 
 
4762
  .filtro-row-react {
4763
  grid-template-columns: 1.2fr 110px minmax(110px, 0.8fr) auto;
4764
  }
@@ -4778,6 +4809,11 @@ button.btn-download-subtle {
4778
  gap: 10px;
4779
  }
4780
 
 
 
 
 
 
4781
  .elaboracao-side-nav {
4782
  top: 68px;
4783
  width: 100%;
 
852
  gap: 14px;
853
  }
854
 
855
+ .elaboracao-layout.is-repo-model-open .workflow-section[data-section-step="1"] {
856
+ width: min(calc(100% + 110px), calc(100vw - 20px));
857
+ max-width: min(calc(100% + 110px), calc(100vw - 20px));
858
+ }
859
+
860
+ .elaboracao-layout.is-repo-model-open .workflow-section[data-section-step="1"] .section-body {
861
+ overflow-x: visible;
862
+ }
863
+
864
  .elaboracao-side-nav {
865
  position: sticky;
866
  top: 96px;
 
3803
  border-color: #ffba66;
3804
  }
3805
 
3806
+ .filtro-row-faixa-hint {
3807
+ grid-column: 1 / -1;
3808
+ margin-top: -1px;
3809
+ color: #4f657b;
3810
+ font-size: 0.76rem;
3811
+ line-height: 1.25;
3812
+ }
3813
+
3814
+ .filtro-row-faixa-hint strong {
3815
+ color: #2f4459;
3816
+ font-weight: 700;
3817
+ }
3818
+
3819
+ .filtro-row-faixa-hint-muted {
3820
+ color: #7b8ea2;
3821
+ }
3822
+
3823
  .outlier-subheader {
3824
  display: flex;
3825
  align-items: center;
 
4785
  border-top: 1px solid #dde7f1;
4786
  }
4787
 
4788
+ .elaboracao-layout.is-repo-model-open .workflow-section[data-section-step="1"] {
4789
+ width: min(calc(100% + 64px), calc(100vw - 14px));
4790
+ max-width: min(calc(100% + 64px), calc(100vw - 14px));
4791
+ }
4792
+
4793
  .filtro-row-react {
4794
  grid-template-columns: 1.2fr 110px minmax(110px, 0.8fr) auto;
4795
  }
 
4809
  gap: 10px;
4810
  }
4811
 
4812
+ .elaboracao-layout.is-repo-model-open .workflow-section[data-section-step="1"] {
4813
+ width: 100%;
4814
+ max-width: 100%;
4815
+ }
4816
+
4817
  .elaboracao-side-nav {
4818
  top: 68px;
4819
  width: 100%;