Spaces:
Running
Running
Guilherme Silberfarb Costa commited on
Commit ·
303655d
1
Parent(s): 97e9b7f
melhorias esteticas e funcionais
Browse files- backend/app/core/elaboracao/formatadores.py +14 -8
- backend/app/services/elaboracao_service.py +40 -15
- backend/app/services/pesquisa_service.py +2 -0
- backend/app/services/visualizacao_service.py +38 -13
- frontend/src/components/AvaliacaoTab.jsx +14 -8
- frontend/src/components/ElaboracaoTab.jsx +151 -87
- frontend/src/components/PesquisaTab.jsx +2 -2
- frontend/src/components/SinglePillAutocomplete.jsx +10 -0
- frontend/src/components/VisualizacaoTab.jsx +2 -1
- frontend/src/styles.css +36 -0
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 |
-
|
| 545 |
-
if indice_base
|
| 546 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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[
|
| 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. {
|
| 607 |
for i, aval in enumerate(avaliacoes_lista):
|
| 608 |
-
if i ==
|
| 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 |
-
|
|
|
|
| 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(
|
| 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":
|
| 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":
|
| 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":
|
| 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 |
-
|
| 2031 |
-
|
| 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":
|
| 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 =
|
| 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 |
-
|
|
|
|
| 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":
|
| 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":
|
| 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":
|
| 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 =
|
| 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":
|
| 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 =
|
| 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 |
-
|
| 112 |
-
|
| 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
|
|
|
|
|
|
|
| 278 |
return
|
| 279 |
}
|
| 280 |
-
if (
|
|
|
|
| 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=
|
| 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 |
-
|
| 3369 |
-
|
| 3370 |
-
|
| 3371 |
-
|
| 3372 |
-
|
| 3373 |
-
|
| 3374 |
-
|
| 3375 |
-
|
| 3376 |
-
|
| 3377 |
-
|
| 3378 |
-
|
| 3379 |
-
|
| 3380 |
-
|
| 3381 |
-
|
| 3382 |
-
|
| 3383 |
-
|
| 3384 |
-
|
| 3385 |
-
|
| 3386 |
-
|
| 3387 |
-
|
| 3388 |
-
|
| 3389 |
-
|
| 3390 |
-
|
| 3391 |
-
|
| 3392 |
-
|
| 3393 |
-
|
| 3394 |
-
|
| 3395 |
-
|
| 3396 |
-
|
| 3397 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 5149 |
-
<
|
| 5150 |
-
|
| 5151 |
-
|
| 5152 |
-
|
| 5153 |
-
|
| 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 |
-
|
| 5162 |
-
|
| 5163 |
-
|
| 5164 |
-
|
| 5165 |
-
|
| 5166 |
-
|
| 5167 |
-
|
| 5168 |
-
</
|
| 5169 |
-
|
| 5170 |
-
<
|
| 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="
|
| 5193 |
-
<
|
| 5194 |
-
|
| 5195 |
-
|
| 5196 |
-
|
| 5197 |
-
|
|
|
|
| 5198 |
>
|
| 5199 |
-
|
| 5200 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5201 |
</div>
|
| 5202 |
-
|
| 5203 |
-
<
|
| 5204 |
-
|
| 5205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=
|
| 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
|
| 1019 |
</button>
|
| 1020 |
<button type="button" className="btn-pesquisa-eval" onClick={() => onUsarEmAvaliacao(modelo)}>
|
| 1021 |
-
|
| 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=
|
| 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%;
|