Spaces:
Running
Running
Guilherme Silberfarb Costa commited on
Commit ·
9e7c650
1
Parent(s): 696b48a
multiplas alteracoes
Browse files- README.md +6 -3
- backend/app/api/pesquisa.py +6 -0
- backend/app/api/visualizacao.py +17 -0
- backend/app/core/dados/Bairros_LC12112_16.shp +3 -0
- backend/app/core/map_layers.py +28 -7
- backend/app/models/session.py +2 -0
- backend/app/services/elaboracao_service.py +105 -6
- backend/app/services/pesquisa_service.py +28 -8
- backend/app/services/trabalhos_tecnicos_service.py +76 -31
- backend/app/services/visualizacao_service.py +245 -46
- backend/run_backend.sh +6 -0
- frontend/src/App.jsx +338 -84
- frontend/src/api.js +6 -0
- frontend/src/components/AvaliacaoTab.jsx +22 -2
- frontend/src/components/ElaboracaoTab.jsx +245 -34
- frontend/src/components/ModelosEstatisticosTab.jsx +75 -30
- frontend/src/components/PesquisaTab.jsx +194 -34
- frontend/src/components/PlotFigure.jsx +84 -17
- frontend/src/components/RepositorioTab.jsx +457 -103
- frontend/src/components/ShareLinkButton.jsx +57 -0
- frontend/src/components/TrabalhosTecnicosTab.jsx +86 -15
- frontend/src/deepLinks.js +348 -0
- frontend/src/styles.css +105 -1
README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
---
|
| 2 |
-
title: Mesa React
|
| 3 |
emoji: "🌖"
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
|
@@ -8,7 +8,7 @@ app_port: 7860
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
-
# MESA
|
| 12 |
|
| 13 |
Rearquitetura do app MESA com:
|
| 14 |
|
|
@@ -29,12 +29,15 @@ Rearquitetura do app MESA com:
|
|
| 29 |
|
| 30 |
```bash
|
| 31 |
cd backend
|
| 32 |
-
|
| 33 |
source .venv/bin/activate
|
| 34 |
pip install -r requirements.txt
|
| 35 |
./run_backend.sh
|
| 36 |
```
|
| 37 |
|
|
|
|
|
|
|
|
|
|
| 38 |
API: `http://localhost:8000`
|
| 39 |
Swagger: `http://localhost:8000/docs`
|
| 40 |
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Mesa React Beta
|
| 3 |
emoji: "🌖"
|
| 4 |
colorFrom: blue
|
| 5 |
colorTo: indigo
|
|
|
|
| 8 |
pinned: false
|
| 9 |
---
|
| 10 |
|
| 11 |
+
# MESA React Beta (FastAPI + React)
|
| 12 |
|
| 13 |
Rearquitetura do app MESA com:
|
| 14 |
|
|
|
|
| 29 |
|
| 30 |
```bash
|
| 31 |
cd backend
|
| 32 |
+
python3.12 -m venv .venv
|
| 33 |
source .venv/bin/activate
|
| 34 |
pip install -r requirements.txt
|
| 35 |
./run_backend.sh
|
| 36 |
```
|
| 37 |
|
| 38 |
+
Observacao: o stack geoespacial do backend fecha melhor com Python 3.12 no macOS.
|
| 39 |
+
Se `backend/.venv` existir, `./run_backend.sh` passa a usa-lo automaticamente.
|
| 40 |
+
|
| 41 |
API: `http://localhost:8000`
|
| 42 |
Swagger: `http://localhost:8000/docs`
|
| 43 |
|
backend/app/api/pesquisa.py
CHANGED
|
@@ -9,6 +9,7 @@ from pydantic import BaseModel
|
|
| 9 |
from app.services.pesquisa_service import (
|
| 10 |
PesquisaFiltros,
|
| 11 |
gerar_mapa_modelos,
|
|
|
|
| 12 |
listar_modelos,
|
| 13 |
obter_admin_config_pesquisa,
|
| 14 |
resolver_localizacao_avaliando,
|
|
@@ -83,6 +84,11 @@ def pesquisar_admin_config_salvar(payload: PesquisaAdminConfigPayload) -> dict:
|
|
| 83 |
return salvar_admin_config_pesquisa(payload.campos)
|
| 84 |
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
@router.get("/modelos")
|
| 87 |
def pesquisar_modelos(
|
| 88 |
somente_contexto: bool = Query(False),
|
|
|
|
| 9 |
from app.services.pesquisa_service import (
|
| 10 |
PesquisaFiltros,
|
| 11 |
gerar_mapa_modelos,
|
| 12 |
+
listar_logradouros_eixos,
|
| 13 |
listar_modelos,
|
| 14 |
obter_admin_config_pesquisa,
|
| 15 |
resolver_localizacao_avaliando,
|
|
|
|
| 84 |
return salvar_admin_config_pesquisa(payload.campos)
|
| 85 |
|
| 86 |
|
| 87 |
+
@router.get("/logradouros-eixos")
|
| 88 |
+
def pesquisar_logradouros_eixos(limite: int = Query(2000, ge=1, le=20000)) -> dict:
|
| 89 |
+
return listar_logradouros_eixos(limite=limite)
|
| 90 |
+
|
| 91 |
+
|
| 92 |
@router.get("/modelos")
|
| 93 |
def pesquisar_modelos(
|
| 94 |
somente_contexto: bool = Query(False),
|
backend/app/api/visualizacao.py
CHANGED
|
@@ -28,6 +28,11 @@ class MapaPayload(SessionPayload):
|
|
| 28 |
trabalhos_tecnicos_modelos_modo: str | None = None
|
| 29 |
|
| 30 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
class MapaPopupPayload(SessionPayload):
|
| 32 |
row_id: int
|
| 33 |
|
|
@@ -110,6 +115,18 @@ def exibir(payload: ExibirPayload, request: Request) -> dict[str, Any]:
|
|
| 110 |
)
|
| 111 |
|
| 112 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
@router.post("/evaluation/context")
|
| 114 |
def evaluation_context(payload: SessionPayload) -> dict[str, Any]:
|
| 115 |
session = session_store.get(payload.session_id)
|
|
|
|
| 28 |
trabalhos_tecnicos_modelos_modo: str | None = None
|
| 29 |
|
| 30 |
|
| 31 |
+
class SecaoPayload(SessionPayload):
|
| 32 |
+
secao: str
|
| 33 |
+
trabalhos_tecnicos_modelos_modo: str | None = None
|
| 34 |
+
|
| 35 |
+
|
| 36 |
class MapaPopupPayload(SessionPayload):
|
| 37 |
row_id: int
|
| 38 |
|
|
|
|
| 115 |
)
|
| 116 |
|
| 117 |
|
| 118 |
+
@router.post("/section")
|
| 119 |
+
def section(payload: SecaoPayload, request: Request) -> dict[str, Any]:
|
| 120 |
+
session = session_store.get(payload.session_id)
|
| 121 |
+
return visualizacao_service.carregar_secao_modelo(
|
| 122 |
+
session,
|
| 123 |
+
payload.secao,
|
| 124 |
+
trabalhos_tecnicos_modelos_modo=payload.trabalhos_tecnicos_modelos_modo,
|
| 125 |
+
api_base_url=str(request.base_url).rstrip("/"),
|
| 126 |
+
popup_auth_token=getattr(request.state, "auth_token", None),
|
| 127 |
+
)
|
| 128 |
+
|
| 129 |
+
|
| 130 |
@router.post("/evaluation/context")
|
| 131 |
def evaluation_context(payload: SessionPayload) -> dict[str, Any]:
|
| 132 |
session = session_store.get(payload.session_id)
|
backend/app/core/dados/Bairros_LC12112_16.shp
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:1446d8cc7fdff70154b792fd1cf78b251d064ce38a58c8a81128702d2352967a
|
| 3 |
+
size 5562340
|
backend/app/core/map_layers.py
CHANGED
|
@@ -16,18 +16,34 @@ _SIMPLIFY_TOLERANCE = 0.00005
|
|
| 16 |
|
| 17 |
_BAIRROS_CACHE_LOCK = Lock()
|
| 18 |
_BAIRROS_GEOJSON_CACHE: dict[str, Any] | None = None
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
def _carregar_bairros_geojson() -> dict[str, Any] | None:
|
| 23 |
-
global _BAIRROS_GEOJSON_CACHE,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
with _BAIRROS_CACHE_LOCK:
|
| 25 |
-
if
|
| 26 |
return _BAIRROS_GEOJSON_CACHE
|
| 27 |
-
_BAIRROS_GEOJSON_CARREGADO = True
|
| 28 |
|
| 29 |
-
if not _BAIRROS_SHP_PATH.exists():
|
| 30 |
-
return None
|
| 31 |
|
| 32 |
try:
|
| 33 |
import geopandas as gpd
|
|
@@ -54,7 +70,12 @@ def _carregar_bairros_geojson() -> dict[str, Any] | None:
|
|
| 54 |
geojson = None
|
| 55 |
|
| 56 |
with _BAIRROS_CACHE_LOCK:
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
return geojson
|
| 59 |
|
| 60 |
|
|
|
|
| 16 |
|
| 17 |
_BAIRROS_CACHE_LOCK = Lock()
|
| 18 |
_BAIRROS_GEOJSON_CACHE: dict[str, Any] | None = None
|
| 19 |
+
_BAIRROS_SOURCE_SIGNATURE: tuple[tuple[str, int, int], ...] | None = None
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _assinatura_bairros_source() -> tuple[tuple[str, int, int], ...] | None:
|
| 23 |
+
arquivos = sorted(_BAIRROS_SHP_PATH.parent.glob(f"{_BAIRROS_SHP_PATH.stem}.*"))
|
| 24 |
+
if not arquivos:
|
| 25 |
+
return None
|
| 26 |
+
|
| 27 |
+
assinatura: list[tuple[str, int, int]] = []
|
| 28 |
+
for caminho in arquivos:
|
| 29 |
+
try:
|
| 30 |
+
stat = caminho.stat()
|
| 31 |
+
except OSError:
|
| 32 |
+
continue
|
| 33 |
+
assinatura.append((caminho.name, int(stat.st_mtime_ns), int(stat.st_size)))
|
| 34 |
+
return tuple(assinatura) or None
|
| 35 |
|
| 36 |
|
| 37 |
def _carregar_bairros_geojson() -> dict[str, Any] | None:
|
| 38 |
+
global _BAIRROS_GEOJSON_CACHE, _BAIRROS_SOURCE_SIGNATURE
|
| 39 |
+
assinatura = _assinatura_bairros_source()
|
| 40 |
+
if assinatura is None or not _BAIRROS_SHP_PATH.exists():
|
| 41 |
+
return None
|
| 42 |
+
|
| 43 |
with _BAIRROS_CACHE_LOCK:
|
| 44 |
+
if _BAIRROS_SOURCE_SIGNATURE == assinatura and _BAIRROS_GEOJSON_CACHE is not None:
|
| 45 |
return _BAIRROS_GEOJSON_CACHE
|
|
|
|
| 46 |
|
|
|
|
|
|
|
| 47 |
|
| 48 |
try:
|
| 49 |
import geopandas as gpd
|
|
|
|
| 70 |
geojson = None
|
| 71 |
|
| 72 |
with _BAIRROS_CACHE_LOCK:
|
| 73 |
+
if geojson is not None:
|
| 74 |
+
_BAIRROS_GEOJSON_CACHE = geojson
|
| 75 |
+
_BAIRROS_SOURCE_SIGNATURE = assinatura
|
| 76 |
+
else:
|
| 77 |
+
_BAIRROS_GEOJSON_CACHE = None
|
| 78 |
+
_BAIRROS_SOURCE_SIGNATURE = None
|
| 79 |
return geojson
|
| 80 |
|
| 81 |
|
backend/app/models/session.py
CHANGED
|
@@ -53,6 +53,7 @@ class SessionState:
|
|
| 53 |
pacote_visualizacao: dict[str, Any] | None = None
|
| 54 |
dados_visualizacao: pd.DataFrame | None = None
|
| 55 |
avaliacoes_visualizacao: list[dict[str, Any]] = field(default_factory=list)
|
|
|
|
| 56 |
graficos_dispersao_cache: dict[str, dict[str, Any]] = field(default_factory=dict)
|
| 57 |
|
| 58 |
elaborador: dict[str, Any] | None = None
|
|
@@ -73,3 +74,4 @@ class SessionState:
|
|
| 73 |
self.pacote_visualizacao = None
|
| 74 |
self.dados_visualizacao = None
|
| 75 |
self.avaliacoes_visualizacao = []
|
|
|
|
|
|
| 53 |
pacote_visualizacao: dict[str, Any] | None = None
|
| 54 |
dados_visualizacao: pd.DataFrame | None = None
|
| 55 |
avaliacoes_visualizacao: list[dict[str, Any]] = field(default_factory=list)
|
| 56 |
+
visualizacao_cache: dict[str, Any] = field(default_factory=dict)
|
| 57 |
graficos_dispersao_cache: dict[str, dict[str, Any]] = field(default_factory=dict)
|
| 58 |
|
| 59 |
elaborador: dict[str, Any] | None = None
|
|
|
|
| 74 |
self.pacote_visualizacao = None
|
| 75 |
self.dados_visualizacao = None
|
| 76 |
self.avaliacoes_visualizacao = []
|
| 77 |
+
self.visualizacao_cache = {}
|
backend/app/services/elaboracao_service.py
CHANGED
|
@@ -383,7 +383,7 @@ def _montar_payload_dispersao(
|
|
| 383 |
|
| 384 |
usar_png = _usar_png_dispersao(int(total_pontos))
|
| 385 |
if not usar_png:
|
| 386 |
-
session.graficos_dispersao_cache
|
| 387 |
return {
|
| 388 |
"modo": "interativo",
|
| 389 |
"grafico": payload_interativo,
|
|
@@ -419,6 +419,99 @@ def _montar_payload_dispersao(
|
|
| 419 |
}
|
| 420 |
|
| 421 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
def _clean_int_list(values: list[Any] | None) -> list[int]:
|
| 423 |
if not values:
|
| 424 |
return []
|
|
@@ -1414,6 +1507,10 @@ def fit_model(
|
|
| 1414 |
|
| 1415 |
graficos = charts.criar_painel_diagnostico(resultado)
|
| 1416 |
usar_png_diagnosticos = _usar_png_dispersao(total_pontos_modelo)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1417 |
obs_calc_png = _renderizar_png_grafico(graficos.get("obs_calc")) if usar_png_diagnosticos else None
|
| 1418 |
residuos_png = _renderizar_png_grafico(graficos.get("residuos")) if usar_png_diagnosticos else None
|
| 1419 |
histograma_png = _renderizar_png_grafico(graficos.get("histograma")) if usar_png_diagnosticos else None
|
|
@@ -1425,7 +1522,7 @@ def fit_model(
|
|
| 1425 |
"secao15_cook": graficos.get("cook"),
|
| 1426 |
}
|
| 1427 |
for cache_key, fig_diag in diagnosticos_cache_map.items():
|
| 1428 |
-
if
|
| 1429 |
payload_diag = figure_to_payload(fig_diag)
|
| 1430 |
if payload_diag:
|
| 1431 |
session.graficos_dispersao_cache[cache_key] = payload_diag
|
|
@@ -1499,10 +1596,10 @@ def fit_model(
|
|
| 1499 |
"graficos_diagnostico_modo": "png" if usar_png_diagnosticos else "interativo",
|
| 1500 |
"graficos_diagnostico_total_pontos": total_pontos_modelo,
|
| 1501 |
"graficos_diagnostico_limiar_png": LIMIAR_DISPERSAO_PNG,
|
| 1502 |
-
"grafico_obs_calc":
|
| 1503 |
-
"grafico_residuos":
|
| 1504 |
-
"grafico_histograma":
|
| 1505 |
-
"grafico_cook":
|
| 1506 |
"grafico_obs_calc_png": obs_calc_png,
|
| 1507 |
"grafico_residuos_png": residuos_png,
|
| 1508 |
"grafico_histograma_png": histograma_png,
|
|
@@ -1660,6 +1757,7 @@ def obter_grafico_dispersao_interativo(session: SessionState, alvo: str) -> dict
|
|
| 1660 |
return {
|
| 1661 |
"alvo": key,
|
| 1662 |
"grafico": sanitize_value(grafico),
|
|
|
|
| 1663 |
}
|
| 1664 |
|
| 1665 |
|
|
@@ -1682,6 +1780,7 @@ def obter_grafico_diagnostico_interativo(session: SessionState, grafico: str) ->
|
|
| 1682 |
return {
|
| 1683 |
"grafico": alvo,
|
| 1684 |
"payload": sanitize_value(payload),
|
|
|
|
| 1685 |
}
|
| 1686 |
|
| 1687 |
|
|
|
|
| 383 |
|
| 384 |
usar_png = _usar_png_dispersao(int(total_pontos))
|
| 385 |
if not usar_png:
|
| 386 |
+
session.graficos_dispersao_cache[key] = payload_interativo
|
| 387 |
return {
|
| 388 |
"modo": "interativo",
|
| 389 |
"grafico": payload_interativo,
|
|
|
|
| 419 |
}
|
| 420 |
|
| 421 |
|
| 422 |
+
def _trace_mode_includes_markers(mode: Any) -> bool:
|
| 423 |
+
return "markers" in str(mode or "").strip().lower()
|
| 424 |
+
|
| 425 |
+
|
| 426 |
+
def _extrair_sequencia_payload(valores: Any) -> list[Any]:
|
| 427 |
+
if isinstance(valores, list):
|
| 428 |
+
return valores
|
| 429 |
+
if isinstance(valores, tuple):
|
| 430 |
+
return list(valores)
|
| 431 |
+
if isinstance(valores, dict):
|
| 432 |
+
dtype_text = str(valores.get("dtype") or "").strip()
|
| 433 |
+
bdata_text = str(valores.get("bdata") or "").strip()
|
| 434 |
+
if dtype_text and bdata_text:
|
| 435 |
+
try:
|
| 436 |
+
buffer = base64.b64decode(bdata_text)
|
| 437 |
+
array = np.frombuffer(buffer, dtype=np.dtype(dtype_text))
|
| 438 |
+
return array.tolist()
|
| 439 |
+
except Exception:
|
| 440 |
+
return []
|
| 441 |
+
return []
|
| 442 |
+
try:
|
| 443 |
+
return list(valores)
|
| 444 |
+
except Exception:
|
| 445 |
+
return []
|
| 446 |
+
|
| 447 |
+
|
| 448 |
+
def _coletar_rotulos_indices_trace_payload(trace: dict[str, Any] | None) -> list[str]:
|
| 449 |
+
if not isinstance(trace, dict):
|
| 450 |
+
return []
|
| 451 |
+
if not _trace_mode_includes_markers(trace.get("mode")):
|
| 452 |
+
return []
|
| 453 |
+
|
| 454 |
+
for source_key in ("customdata", "ids", "text"):
|
| 455 |
+
valores = _extrair_sequencia_payload(trace.get(source_key))
|
| 456 |
+
if not valores:
|
| 457 |
+
continue
|
| 458 |
+
rotulos: list[str] = []
|
| 459 |
+
for item in valores:
|
| 460 |
+
if isinstance(item, (list, tuple)):
|
| 461 |
+
texto = next((str(sub).strip() for sub in item if str(sub).strip()), "")
|
| 462 |
+
elif isinstance(item, dict):
|
| 463 |
+
texto = str(
|
| 464 |
+
item.get("indice")
|
| 465 |
+
or item.get("Indice")
|
| 466 |
+
or item.get("Índice")
|
| 467 |
+
or "",
|
| 468 |
+
).strip()
|
| 469 |
+
else:
|
| 470 |
+
texto = str(item or "").strip()
|
| 471 |
+
rotulos.append(texto)
|
| 472 |
+
if any(rotulos):
|
| 473 |
+
return rotulos
|
| 474 |
+
return []
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
def _payload_grafico_com_indices(payload: Any) -> dict[str, Any] | None:
|
| 478 |
+
payload_sanitizado = sanitize_value(payload)
|
| 479 |
+
if not isinstance(payload_sanitizado, dict):
|
| 480 |
+
return None
|
| 481 |
+
|
| 482 |
+
clone = json.loads(json.dumps(payload_sanitizado))
|
| 483 |
+
data = clone.get("data")
|
| 484 |
+
if not isinstance(data, list):
|
| 485 |
+
return None
|
| 486 |
+
|
| 487 |
+
alterado = False
|
| 488 |
+
|
| 489 |
+
for trace in data:
|
| 490 |
+
if not isinstance(trace, dict):
|
| 491 |
+
continue
|
| 492 |
+
rotulos = _coletar_rotulos_indices_trace_payload(trace)
|
| 493 |
+
if not rotulos:
|
| 494 |
+
continue
|
| 495 |
+
mode_parts = [part.strip() for part in str(trace.get("mode") or "markers").split("+") if part.strip()]
|
| 496 |
+
if "text" not in mode_parts:
|
| 497 |
+
mode_parts.append("text")
|
| 498 |
+
if "markers" not in mode_parts:
|
| 499 |
+
mode_parts.append("markers")
|
| 500 |
+
trace["mode"] = "+".join(mode_parts)
|
| 501 |
+
trace["text"] = rotulos
|
| 502 |
+
trace["textposition"] = trace.get("textposition") or "top center"
|
| 503 |
+
base_textfont = trace.get("textfont") if isinstance(trace.get("textfont"), dict) else {}
|
| 504 |
+
trace["textfont"] = {
|
| 505 |
+
"size": 10,
|
| 506 |
+
"color": "#243746",
|
| 507 |
+
**base_textfont,
|
| 508 |
+
}
|
| 509 |
+
trace["cliponaxis"] = False
|
| 510 |
+
alterado = True
|
| 511 |
+
|
| 512 |
+
return clone if alterado else None
|
| 513 |
+
|
| 514 |
+
|
| 515 |
def _clean_int_list(values: list[Any] | None) -> list[int]:
|
| 516 |
if not values:
|
| 517 |
return []
|
|
|
|
| 1507 |
|
| 1508 |
graficos = charts.criar_painel_diagnostico(resultado)
|
| 1509 |
usar_png_diagnosticos = _usar_png_dispersao(total_pontos_modelo)
|
| 1510 |
+
obs_calc_payload = None if usar_png_diagnosticos else figure_to_payload(graficos.get("obs_calc"))
|
| 1511 |
+
residuos_payload = None if usar_png_diagnosticos else figure_to_payload(graficos.get("residuos"))
|
| 1512 |
+
histograma_payload = None if usar_png_diagnosticos else figure_to_payload(graficos.get("histograma"))
|
| 1513 |
+
cook_payload = None if usar_png_diagnosticos else figure_to_payload(graficos.get("cook"))
|
| 1514 |
obs_calc_png = _renderizar_png_grafico(graficos.get("obs_calc")) if usar_png_diagnosticos else None
|
| 1515 |
residuos_png = _renderizar_png_grafico(graficos.get("residuos")) if usar_png_diagnosticos else None
|
| 1516 |
histograma_png = _renderizar_png_grafico(graficos.get("histograma")) if usar_png_diagnosticos else None
|
|
|
|
| 1522 |
"secao15_cook": graficos.get("cook"),
|
| 1523 |
}
|
| 1524 |
for cache_key, fig_diag in diagnosticos_cache_map.items():
|
| 1525 |
+
if fig_diag is not None:
|
| 1526 |
payload_diag = figure_to_payload(fig_diag)
|
| 1527 |
if payload_diag:
|
| 1528 |
session.graficos_dispersao_cache[cache_key] = payload_diag
|
|
|
|
| 1596 |
"graficos_diagnostico_modo": "png" if usar_png_diagnosticos else "interativo",
|
| 1597 |
"graficos_diagnostico_total_pontos": total_pontos_modelo,
|
| 1598 |
"graficos_diagnostico_limiar_png": LIMIAR_DISPERSAO_PNG,
|
| 1599 |
+
"grafico_obs_calc": obs_calc_payload,
|
| 1600 |
+
"grafico_residuos": residuos_payload,
|
| 1601 |
+
"grafico_histograma": histograma_payload,
|
| 1602 |
+
"grafico_cook": cook_payload,
|
| 1603 |
"grafico_obs_calc_png": obs_calc_png,
|
| 1604 |
"grafico_residuos_png": residuos_png,
|
| 1605 |
"grafico_histograma_png": histograma_png,
|
|
|
|
| 1757 |
return {
|
| 1758 |
"alvo": key,
|
| 1759 |
"grafico": sanitize_value(grafico),
|
| 1760 |
+
"grafico_com_indices": _payload_grafico_com_indices(grafico),
|
| 1761 |
}
|
| 1762 |
|
| 1763 |
|
|
|
|
| 1780 |
return {
|
| 1781 |
"grafico": alvo,
|
| 1782 |
"payload": sanitize_value(payload),
|
| 1783 |
+
"payload_com_indices": _payload_grafico_com_indices(payload),
|
| 1784 |
}
|
| 1785 |
|
| 1786 |
|
backend/app/services/pesquisa_service.py
CHANGED
|
@@ -287,7 +287,7 @@ def listar_modelos(filtros: PesquisaFiltros, limite: int | None = None, somente_
|
|
| 287 |
todos = [_carregar_resumo_com_cache(caminho) for caminho in modelos]
|
| 288 |
colunas_filtro = _montar_config_colunas_filtro(todos)
|
| 289 |
admin_fontes = _carregar_fontes_admin(colunas_filtro)
|
| 290 |
-
sugestoes = _extrair_sugestoes(todos, admin_fontes)
|
| 291 |
aval_lat, aval_lon = _normalizar_coordenadas_avaliando(filtros_exec.aval_lat, filtros_exec.aval_lon)
|
| 292 |
avaliandos_geo = _normalizar_avaliandos_geo(filtros_exec.avaliandos_geo)
|
| 293 |
if (aval_lat is None or aval_lon is None) and len(avaliandos_geo) == 1:
|
|
@@ -3057,6 +3057,7 @@ def _extrair_sugestoes(
|
|
| 3057 |
modelos: list[dict[str, Any]],
|
| 3058 |
fontes_admin: dict[str, list[str]] | None = None,
|
| 3059 |
limite: int = 200,
|
|
|
|
| 3060 |
) -> dict[str, list[str]]:
|
| 3061 |
fontes_admin = fontes_admin or {}
|
| 3062 |
nomes: list[str] = []
|
|
@@ -3087,13 +3088,14 @@ def _extrair_sugestoes(
|
|
| 3087 |
tipos_modelo.append(str(_tipo_modelo_modelo(modelo) or ""))
|
| 3088 |
zonas_avaliacao.extend(_zonas_avaliacao_modelo(modelo))
|
| 3089 |
|
| 3090 |
-
|
| 3091 |
-
|
| 3092 |
-
|
| 3093 |
-
|
| 3094 |
-
|
| 3095 |
-
|
| 3096 |
-
|
|
|
|
| 3097 |
|
| 3098 |
return {
|
| 3099 |
"nomes_modelo": _lista_textos_unicos(nomes, limite),
|
|
@@ -3107,6 +3109,24 @@ def _extrair_sugestoes(
|
|
| 3107 |
}
|
| 3108 |
|
| 3109 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3110 |
def _nomes_modelo_sugestao(modelo: dict[str, Any]) -> list[str]:
|
| 3111 |
nomes: list[str] = []
|
| 3112 |
vistos = set()
|
|
|
|
| 287 |
todos = [_carregar_resumo_com_cache(caminho) for caminho in modelos]
|
| 288 |
colunas_filtro = _montar_config_colunas_filtro(todos)
|
| 289 |
admin_fontes = _carregar_fontes_admin(colunas_filtro)
|
| 290 |
+
sugestoes = _extrair_sugestoes(todos, admin_fontes, incluir_logradouros_eixos=False)
|
| 291 |
aval_lat, aval_lon = _normalizar_coordenadas_avaliando(filtros_exec.aval_lat, filtros_exec.aval_lon)
|
| 292 |
avaliandos_geo = _normalizar_avaliandos_geo(filtros_exec.avaliandos_geo)
|
| 293 |
if (aval_lat is None or aval_lon is None) and len(avaliandos_geo) == 1:
|
|
|
|
| 3057 |
modelos: list[dict[str, Any]],
|
| 3058 |
fontes_admin: dict[str, list[str]] | None = None,
|
| 3059 |
limite: int = 200,
|
| 3060 |
+
incluir_logradouros_eixos: bool = True,
|
| 3061 |
) -> dict[str, list[str]]:
|
| 3062 |
fontes_admin = fontes_admin or {}
|
| 3063 |
nomes: list[str] = []
|
|
|
|
| 3088 |
tipos_modelo.append(str(_tipo_modelo_modelo(modelo) or ""))
|
| 3089 |
zonas_avaliacao.extend(_zonas_avaliacao_modelo(modelo))
|
| 3090 |
|
| 3091 |
+
if incluir_logradouros_eixos:
|
| 3092 |
+
try:
|
| 3093 |
+
logradouros_eixos = [
|
| 3094 |
+
str(item.get("logradouro") or "").strip()
|
| 3095 |
+
for item in _carregar_catalogo_vias()
|
| 3096 |
+
]
|
| 3097 |
+
except HTTPException:
|
| 3098 |
+
logradouros_eixos = []
|
| 3099 |
|
| 3100 |
return {
|
| 3101 |
"nomes_modelo": _lista_textos_unicos(nomes, limite),
|
|
|
|
| 3109 |
}
|
| 3110 |
|
| 3111 |
|
| 3112 |
+
def listar_logradouros_eixos(limite: int | None = None) -> dict[str, Any]:
|
| 3113 |
+
try:
|
| 3114 |
+
logradouros = [
|
| 3115 |
+
str(item.get("logradouro") or "").strip()
|
| 3116 |
+
for item in _carregar_catalogo_vias()
|
| 3117 |
+
]
|
| 3118 |
+
except HTTPException:
|
| 3119 |
+
logradouros = []
|
| 3120 |
+
|
| 3121 |
+
itens = _lista_textos_unicos(logradouros, limite)
|
| 3122 |
+
return sanitize_value(
|
| 3123 |
+
{
|
| 3124 |
+
"logradouros_eixos": itens,
|
| 3125 |
+
"total_logradouros": len(itens),
|
| 3126 |
+
}
|
| 3127 |
+
)
|
| 3128 |
+
|
| 3129 |
+
|
| 3130 |
def _nomes_modelo_sugestao(modelo: dict[str, Any]) -> list[str]:
|
| 3131 |
nomes: list[str] = []
|
| 3132 |
vistos = set()
|
backend/app/services/trabalhos_tecnicos_service.py
CHANGED
|
@@ -6,6 +6,7 @@ import math
|
|
| 6 |
import sqlite3
|
| 7 |
from pathlib import Path
|
| 8 |
from statistics import median
|
|
|
|
| 9 |
from typing import Any
|
| 10 |
|
| 11 |
import folium
|
|
@@ -33,6 +34,12 @@ MAPA_TRABALHOS_CLUSTERIZADO = "clusterizado"
|
|
| 33 |
MAPA_TRABALHOS_PONTOS = "pontos"
|
| 34 |
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
def _connect_database(path: Path) -> sqlite3.Connection:
|
| 37 |
conn = sqlite3.connect(str(path))
|
| 38 |
conn.row_factory = sqlite3.Row
|
|
@@ -170,6 +177,70 @@ def _expandir_chaves_modelo(values: list[str], catalogo: dict[str, dict[str, str
|
|
| 170 |
return chaves
|
| 171 |
|
| 172 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
def _enriquecer_modelos(modelo_nomes: list[str], catalogo: dict[str, dict[str, str]]) -> list[dict[str, Any]]:
|
| 174 |
enriched: list[dict[str, Any]] = []
|
| 175 |
for nome in modelo_nomes:
|
|
@@ -426,8 +497,7 @@ def contar_avaliandos_por_modelo(chaves_modelo: list[str]) -> int:
|
|
| 426 |
return 0
|
| 427 |
|
| 428 |
try:
|
| 429 |
-
|
| 430 |
-
catalogo_modelos = _catalogo_modelos_mesa()
|
| 431 |
except Exception:
|
| 432 |
return 0
|
| 433 |
|
|
@@ -435,35 +505,10 @@ def contar_avaliandos_por_modelo(chaves_modelo: list[str]) -> int:
|
|
| 435 |
if not chaves_selecionadas:
|
| 436 |
return 0
|
| 437 |
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
"SELECT trabalho_id, modelo_nome FROM trabalho_modelos ORDER BY trabalho_id, ordem, LOWER(modelo_nome)"
|
| 443 |
-
).fetchall()
|
| 444 |
-
finally:
|
| 445 |
-
try:
|
| 446 |
-
conn.close()
|
| 447 |
-
except Exception:
|
| 448 |
-
pass
|
| 449 |
-
|
| 450 |
-
modelos_por_trabalho: dict[str, list[str]] = {}
|
| 451 |
-
trabalho_ids_override = set(overrides.keys())
|
| 452 |
-
for row in rows:
|
| 453 |
-
trabalho_id = str(row["trabalho_id"])
|
| 454 |
-
if trabalho_id in trabalho_ids_override:
|
| 455 |
-
continue
|
| 456 |
-
modelos_por_trabalho.setdefault(trabalho_id, []).append(str(row["modelo_nome"] or ""))
|
| 457 |
-
|
| 458 |
-
for trabalho_id, override in overrides.items():
|
| 459 |
-
modelos_por_trabalho[trabalho_id] = _dedupe_text_list(override.get("modelos"))
|
| 460 |
-
|
| 461 |
-
total = 0
|
| 462 |
-
for modelos in modelos_por_trabalho.values():
|
| 463 |
-
chaves_trabalho = _expandir_chaves_modelo(modelos, catalogo_modelos)
|
| 464 |
-
if chaves_trabalho and not chaves_trabalho.isdisjoint(chaves_selecionadas):
|
| 465 |
-
total += 1
|
| 466 |
-
return total
|
| 467 |
|
| 468 |
|
| 469 |
def _carregar_trabalho_base(
|
|
|
|
| 6 |
import sqlite3
|
| 7 |
from pathlib import Path
|
| 8 |
from statistics import median
|
| 9 |
+
from threading import Lock
|
| 10 |
from typing import Any
|
| 11 |
|
| 12 |
import folium
|
|
|
|
| 34 |
MAPA_TRABALHOS_PONTOS = "pontos"
|
| 35 |
|
| 36 |
|
| 37 |
+
_MODEL_MATCH_CACHE_LOCK = Lock()
|
| 38 |
+
_MODEL_MATCH_CACHE_SIGNATURE: str | None = None
|
| 39 |
+
_MODEL_MATCH_CACHE_CATALOGO: dict[str, dict[str, str]] = {}
|
| 40 |
+
_MODEL_MATCH_CACHE_TRABALHOS: dict[str, set[str]] = {}
|
| 41 |
+
|
| 42 |
+
|
| 43 |
def _connect_database(path: Path) -> sqlite3.Connection:
|
| 44 |
conn = sqlite3.connect(str(path))
|
| 45 |
conn.row_factory = sqlite3.Row
|
|
|
|
| 177 |
return chaves
|
| 178 |
|
| 179 |
|
| 180 |
+
def _assinatura_cache_modelos_relacionados() -> str:
|
| 181 |
+
resolved_db = trabalhos_tecnicos_repository.resolve_database()
|
| 182 |
+
resolved_modelos = model_repository.resolve_model_repository()
|
| 183 |
+
return "|".join(
|
| 184 |
+
[
|
| 185 |
+
str(resolved_db.provider or ""),
|
| 186 |
+
str(resolved_db.repo_id or ""),
|
| 187 |
+
str(resolved_db.revision or ""),
|
| 188 |
+
str(resolved_db.db_path or ""),
|
| 189 |
+
str(resolved_modelos.signature or ""),
|
| 190 |
+
]
|
| 191 |
+
)
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def _montar_cache_trabalhos_por_modelo() -> tuple[dict[str, dict[str, str]], dict[str, set[str]]]:
|
| 195 |
+
global _MODEL_MATCH_CACHE_SIGNATURE, _MODEL_MATCH_CACHE_CATALOGO, _MODEL_MATCH_CACHE_TRABALHOS
|
| 196 |
+
assinatura = _assinatura_cache_modelos_relacionados()
|
| 197 |
+
with _MODEL_MATCH_CACHE_LOCK:
|
| 198 |
+
if _MODEL_MATCH_CACHE_SIGNATURE == assinatura:
|
| 199 |
+
return _MODEL_MATCH_CACHE_CATALOGO, _MODEL_MATCH_CACHE_TRABALHOS
|
| 200 |
+
resolved = trabalhos_tecnicos_repository.resolve_database()
|
| 201 |
+
catalogo_modelos = _catalogo_modelos_mesa()
|
| 202 |
+
|
| 203 |
+
conn = _connect_database(resolved.db_path)
|
| 204 |
+
try:
|
| 205 |
+
overrides = _fetch_overrides(conn)
|
| 206 |
+
rows = conn.execute(
|
| 207 |
+
"SELECT trabalho_id, modelo_nome FROM trabalho_modelos ORDER BY trabalho_id, ordem, LOWER(modelo_nome)"
|
| 208 |
+
).fetchall()
|
| 209 |
+
finally:
|
| 210 |
+
try:
|
| 211 |
+
conn.close()
|
| 212 |
+
except Exception:
|
| 213 |
+
pass
|
| 214 |
+
|
| 215 |
+
modelos_por_trabalho: dict[str, list[str]] = {}
|
| 216 |
+
trabalho_ids_override = set(overrides.keys())
|
| 217 |
+
for row in rows:
|
| 218 |
+
trabalho_id = str(row["trabalho_id"])
|
| 219 |
+
if trabalho_id in trabalho_ids_override:
|
| 220 |
+
continue
|
| 221 |
+
modelos_por_trabalho.setdefault(trabalho_id, []).append(str(row["modelo_nome"] or ""))
|
| 222 |
+
|
| 223 |
+
for trabalho_id, override in overrides.items():
|
| 224 |
+
modelos_por_trabalho[trabalho_id] = _dedupe_text_list(override.get("modelos"))
|
| 225 |
+
|
| 226 |
+
trabalhos_por_chave: dict[str, set[str]] = {}
|
| 227 |
+
for trabalho_id, modelos in modelos_por_trabalho.items():
|
| 228 |
+
chaves_trabalho: set[str] = set()
|
| 229 |
+
for modelo_nome in modelos:
|
| 230 |
+
texto = str(modelo_nome or "").strip()
|
| 231 |
+
if not texto:
|
| 232 |
+
continue
|
| 233 |
+
_registrar_chaves_modelo(chaves_trabalho, texto)
|
| 234 |
+
|
| 235 |
+
for chave in chaves_trabalho:
|
| 236 |
+
trabalhos_por_chave.setdefault(chave, set()).add(trabalho_id)
|
| 237 |
+
|
| 238 |
+
_MODEL_MATCH_CACHE_SIGNATURE = assinatura
|
| 239 |
+
_MODEL_MATCH_CACHE_CATALOGO = catalogo_modelos
|
| 240 |
+
_MODEL_MATCH_CACHE_TRABALHOS = trabalhos_por_chave
|
| 241 |
+
return _MODEL_MATCH_CACHE_CATALOGO, _MODEL_MATCH_CACHE_TRABALHOS
|
| 242 |
+
|
| 243 |
+
|
| 244 |
def _enriquecer_modelos(modelo_nomes: list[str], catalogo: dict[str, dict[str, str]]) -> list[dict[str, Any]]:
|
| 245 |
enriched: list[dict[str, Any]] = []
|
| 246 |
for nome in modelo_nomes:
|
|
|
|
| 497 |
return 0
|
| 498 |
|
| 499 |
try:
|
| 500 |
+
catalogo_modelos, trabalhos_por_chave = _montar_cache_trabalhos_por_modelo()
|
|
|
|
| 501 |
except Exception:
|
| 502 |
return 0
|
| 503 |
|
|
|
|
| 505 |
if not chaves_selecionadas:
|
| 506 |
return 0
|
| 507 |
|
| 508 |
+
trabalhos_relacionados: set[str] = set()
|
| 509 |
+
for chave in chaves_selecionadas:
|
| 510 |
+
trabalhos_relacionados.update(trabalhos_por_chave.get(chave) or ())
|
| 511 |
+
return len(trabalhos_relacionados)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 512 |
|
| 513 |
|
| 514 |
def _carregar_trabalho_base(
|
backend/app/services/visualizacao_service.py
CHANGED
|
@@ -340,6 +340,7 @@ def carregar_modelo(session: SessionState, caminho_arquivo: str) -> dict[str, An
|
|
| 340 |
session.pacote_visualizacao = pacote
|
| 341 |
session.dados_visualizacao = None
|
| 342 |
session.avaliacoes_visualizacao = []
|
|
|
|
| 343 |
|
| 344 |
nome_modelo = Path(caminho_arquivo).stem
|
| 345 |
badge_html = viz_app._formatar_badge_completo(pacote, nome_modelo=nome_modelo)
|
|
@@ -623,37 +624,91 @@ def _preparar_dados_visualizacao(pacote: dict[str, Any]) -> pd.DataFrame:
|
|
| 623 |
return dados
|
| 624 |
|
| 625 |
|
| 626 |
-
def
|
| 627 |
-
|
| 628 |
-
if
|
| 629 |
-
|
|
|
|
|
|
|
| 630 |
|
| 631 |
-
info = _extrair_modelo_info(pacote)
|
| 632 |
-
equacoes = _equacoes_do_modelo(pacote, info)
|
| 633 |
-
return {
|
| 634 |
-
"campos_avaliacao": campos_avaliacao(session),
|
| 635 |
-
"meta_modelo": sanitize_value(info),
|
| 636 |
-
"equacoes": sanitize_value(equacoes),
|
| 637 |
-
}
|
| 638 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
trabalhos_tecnicos_modelos_modo: Any = None,
|
| 643 |
-
api_base_url: str | None = None,
|
| 644 |
-
popup_auth_token: str | None = None,
|
| 645 |
-
) -> dict[str, Any]:
|
| 646 |
pacote = session.pacote_visualizacao
|
| 647 |
if pacote is None:
|
| 648 |
raise HTTPException(status_code=400, detail="Carregue um modelo primeiro")
|
| 649 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 650 |
dados = _preparar_dados_visualizacao(pacote)
|
|
|
|
| 651 |
dados_publicos = dados.drop(columns=["__mesa_row_id__"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 652 |
|
| 653 |
-
estat = _tabela_estatisticas(pacote).round(2)
|
| 654 |
|
| 655 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 656 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 657 |
X = pacote["transformacoes"]["X"].reset_index()
|
| 658 |
y = pacote["transformacoes"]["y"].reset_index()
|
| 659 |
if "index" in y.columns and "index" in X.columns:
|
|
@@ -661,8 +716,40 @@ def exibir_modelo(
|
|
| 661 |
df_xy = pd.concat([X, y], axis=1)
|
| 662 |
df_xy = df_xy.loc[:, ~df_xy.columns.duplicated()].round(2)
|
| 663 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 664 |
resumo_html = viz_app.formatar_resumo_html(viz_app.reorganizar_modelos_resumos(pacote["modelo"]["diagnosticos"]))
|
|
|
|
| 665 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 666 |
tab_coef = pd.DataFrame(pacote["modelo"]["coeficientes"])
|
| 667 |
if not isinstance(tab_coef.index, pd.RangeIndex):
|
| 668 |
tab_coef.insert(0, "Variável", tab_coef.index.astype(str))
|
|
@@ -672,58 +759,170 @@ def exibir_modelo(
|
|
| 672 |
tab_coef = pd.concat([tab_coef[mask], tab_coef[~mask]], ignore_index=True)
|
| 673 |
tab_coef = tab_coef.round(4)
|
| 674 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 675 |
tab_obs_calc = pacote["modelo"]["obs_calc"].reset_index().round(2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 676 |
|
| 677 |
-
figs = viz_app.gerar_todos_graficos(pacote)
|
| 678 |
|
| 679 |
-
|
| 680 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 681 |
avaliandos_tecnicos, trabalhos_tecnicos, trabalhos_tecnicos_modelos_modo_norm = _carregar_trabalhos_tecnicos_visualizacao(
|
| 682 |
session,
|
| 683 |
trabalhos_tecnicos_modelos_modo,
|
| 684 |
)
|
| 685 |
popup_endpoint = f"{api_base_url.rstrip('/')}/api/visualizacao/map/popup" if api_base_url else "/api/visualizacao/map/popup"
|
| 686 |
mapa_html = viz_app.criar_mapa(
|
| 687 |
-
dados,
|
| 688 |
col_y=info["nome_y"],
|
| 689 |
session_id=session.session_id,
|
| 690 |
popup_endpoint=popup_endpoint,
|
| 691 |
popup_auth_token=popup_auth_token,
|
| 692 |
avaliandos_tecnicos=avaliandos_tecnicos,
|
| 693 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
|
| 695 |
-
colunas_numericas = [
|
| 696 |
-
str(col)
|
| 697 |
-
for col in dados_publicos.select_dtypes(include=[np.number]).columns
|
| 698 |
-
if str(col).lower() not in ["lat", "lon", "latitude", "longitude", "index"]
|
| 699 |
-
]
|
| 700 |
-
choices_mapa = ["Visualização Padrão"] + colunas_numericas
|
| 701 |
|
| 702 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 703 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 704 |
return {
|
| 705 |
-
"dados": dataframe_to_payload(dados_publicos, decimals=2, max_rows=None),
|
| 706 |
-
"estatisticas": dataframe_to_payload(estat, decimals=2),
|
| 707 |
-
"escalas_html": escalas_html,
|
| 708 |
-
"dados_transformados": dataframe_to_payload(df_xy, decimals=2, max_rows=None),
|
| 709 |
-
"resumo_html": resumo_html,
|
| 710 |
-
"coeficientes": dataframe_to_payload(tab_coef, decimals=4),
|
| 711 |
-
"obs_calc": dataframe_to_payload(tab_obs_calc, decimals=2),
|
| 712 |
-
"grafico_obs_calc": figure_to_payload(figs.get("obs_calc")),
|
| 713 |
-
"grafico_residuos": figure_to_payload(figs.get("residuos")),
|
| 714 |
-
"grafico_histograma": figure_to_payload(figs.get("hist")),
|
| 715 |
-
"grafico_cook": figure_to_payload(figs.get("cook")),
|
| 716 |
-
"grafico_correlacao": figure_to_payload(figs.get("corr")),
|
| 717 |
-
"mapa_html": mapa_html,
|
| 718 |
-
"mapa_choices": choices_mapa,
|
| 719 |
"campos_avaliacao": campos_avaliacao(session),
|
| 720 |
"meta_modelo": sanitize_value(info),
|
| 721 |
"equacoes": sanitize_value(equacoes),
|
| 722 |
-
"trabalhos_tecnicos": trabalhos_tecnicos,
|
| 723 |
-
"trabalhos_tecnicos_modelos_modo": trabalhos_tecnicos_modelos_modo_norm,
|
| 724 |
}
|
| 725 |
|
| 726 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 727 |
def atualizar_mapa(
|
| 728 |
session: SessionState,
|
| 729 |
variavel_mapa: str | None,
|
|
|
|
| 340 |
session.pacote_visualizacao = pacote
|
| 341 |
session.dados_visualizacao = None
|
| 342 |
session.avaliacoes_visualizacao = []
|
| 343 |
+
session.visualizacao_cache = {}
|
| 344 |
|
| 345 |
nome_modelo = Path(caminho_arquivo).stem
|
| 346 |
badge_html = viz_app._formatar_badge_completo(pacote, nome_modelo=nome_modelo)
|
|
|
|
| 624 |
return dados
|
| 625 |
|
| 626 |
|
| 627 |
+
def _visualizacao_cache(session: SessionState) -> dict[str, Any]:
|
| 628 |
+
cache = getattr(session, "visualizacao_cache", None)
|
| 629 |
+
if isinstance(cache, dict):
|
| 630 |
+
return cache
|
| 631 |
+
session.visualizacao_cache = {}
|
| 632 |
+
return session.visualizacao_cache
|
| 633 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 634 |
|
| 635 |
+
def _visualizacao_tabs_cache(session: SessionState) -> dict[str, Any]:
|
| 636 |
+
cache = _visualizacao_cache(session)
|
| 637 |
+
tabs = cache.get("tabs")
|
| 638 |
+
if isinstance(tabs, dict):
|
| 639 |
+
return tabs
|
| 640 |
+
cache["tabs"] = {}
|
| 641 |
+
return cache["tabs"]
|
| 642 |
|
| 643 |
+
|
| 644 |
+
def _obter_visualizacao_core(session: SessionState) -> dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 645 |
pacote = session.pacote_visualizacao
|
| 646 |
if pacote is None:
|
| 647 |
raise HTTPException(status_code=400, detail="Carregue um modelo primeiro")
|
| 648 |
|
| 649 |
+
cache = _visualizacao_cache(session)
|
| 650 |
+
core = cache.get("core")
|
| 651 |
+
if isinstance(core, dict):
|
| 652 |
+
dados_cache = core.get("dados")
|
| 653 |
+
if isinstance(dados_cache, pd.DataFrame):
|
| 654 |
+
session.dados_visualizacao = dados_cache
|
| 655 |
+
return core
|
| 656 |
+
|
| 657 |
dados = _preparar_dados_visualizacao(pacote)
|
| 658 |
+
info = _extrair_modelo_info(pacote)
|
| 659 |
dados_publicos = dados.drop(columns=["__mesa_row_id__"])
|
| 660 |
+
colunas_numericas = [
|
| 661 |
+
str(col)
|
| 662 |
+
for col in dados_publicos.select_dtypes(include=[np.number]).columns
|
| 663 |
+
if str(col).lower() not in ["lat", "lon", "latitude", "longitude", "index"]
|
| 664 |
+
]
|
| 665 |
+
core = {
|
| 666 |
+
"dados": dados,
|
| 667 |
+
"info": info,
|
| 668 |
+
"mapa_choices": ["Visualização Padrão"] + colunas_numericas,
|
| 669 |
+
}
|
| 670 |
+
cache["core"] = core
|
| 671 |
+
session.dados_visualizacao = dados
|
| 672 |
+
return core
|
| 673 |
|
|
|
|
| 674 |
|
| 675 |
+
def _payload_modelo_dados_mercado(session: SessionState) -> dict[str, Any]:
|
| 676 |
+
tabs_cache = _visualizacao_tabs_cache(session)
|
| 677 |
+
cached = tabs_cache.get("dados_mercado")
|
| 678 |
+
if isinstance(cached, dict):
|
| 679 |
+
return cached
|
| 680 |
+
|
| 681 |
+
core = _obter_visualizacao_core(session)
|
| 682 |
+
dados_publicos = core["dados"].drop(columns=["__mesa_row_id__"])
|
| 683 |
+
payload = {
|
| 684 |
+
"dados": dataframe_to_payload(dados_publicos, decimals=2, max_rows=None),
|
| 685 |
+
}
|
| 686 |
+
tabs_cache["dados_mercado"] = payload
|
| 687 |
+
return payload
|
| 688 |
+
|
| 689 |
+
|
| 690 |
+
def _payload_modelo_metricas(session: SessionState) -> dict[str, Any]:
|
| 691 |
+
tabs_cache = _visualizacao_tabs_cache(session)
|
| 692 |
+
cached = tabs_cache.get("metricas")
|
| 693 |
+
if isinstance(cached, dict):
|
| 694 |
+
return cached
|
| 695 |
+
|
| 696 |
+
estat = _tabela_estatisticas(session.pacote_visualizacao).round(2)
|
| 697 |
+
payload = {
|
| 698 |
+
"estatisticas": dataframe_to_payload(estat, decimals=2),
|
| 699 |
+
}
|
| 700 |
+
tabs_cache["metricas"] = payload
|
| 701 |
+
return payload
|
| 702 |
|
| 703 |
+
|
| 704 |
+
def _payload_modelo_transformacoes(session: SessionState) -> dict[str, Any]:
|
| 705 |
+
tabs_cache = _visualizacao_tabs_cache(session)
|
| 706 |
+
cached = tabs_cache.get("transformacoes")
|
| 707 |
+
if isinstance(cached, dict):
|
| 708 |
+
return cached
|
| 709 |
+
|
| 710 |
+
pacote = session.pacote_visualizacao
|
| 711 |
+
escalas_html = viz_app.formatar_escalas_html(pacote["transformacoes"]["info"])
|
| 712 |
X = pacote["transformacoes"]["X"].reset_index()
|
| 713 |
y = pacote["transformacoes"]["y"].reset_index()
|
| 714 |
if "index" in y.columns and "index" in X.columns:
|
|
|
|
| 716 |
df_xy = pd.concat([X, y], axis=1)
|
| 717 |
df_xy = df_xy.loc[:, ~df_xy.columns.duplicated()].round(2)
|
| 718 |
|
| 719 |
+
payload = {
|
| 720 |
+
"escalas_html": escalas_html,
|
| 721 |
+
"dados_transformados": dataframe_to_payload(df_xy, decimals=2, max_rows=None),
|
| 722 |
+
}
|
| 723 |
+
tabs_cache["transformacoes"] = payload
|
| 724 |
+
return payload
|
| 725 |
+
|
| 726 |
+
|
| 727 |
+
def _payload_modelo_resumo(session: SessionState) -> dict[str, Any]:
|
| 728 |
+
tabs_cache = _visualizacao_tabs_cache(session)
|
| 729 |
+
cached = tabs_cache.get("resumo")
|
| 730 |
+
if isinstance(cached, dict):
|
| 731 |
+
return cached
|
| 732 |
+
|
| 733 |
+
pacote = session.pacote_visualizacao
|
| 734 |
+
core = _obter_visualizacao_core(session)
|
| 735 |
resumo_html = viz_app.formatar_resumo_html(viz_app.reorganizar_modelos_resumos(pacote["modelo"]["diagnosticos"]))
|
| 736 |
+
equacoes = _equacoes_do_modelo(pacote, core["info"])
|
| 737 |
|
| 738 |
+
payload = {
|
| 739 |
+
"resumo_html": resumo_html,
|
| 740 |
+
"equacoes": sanitize_value(equacoes),
|
| 741 |
+
}
|
| 742 |
+
tabs_cache["resumo"] = payload
|
| 743 |
+
return payload
|
| 744 |
+
|
| 745 |
+
|
| 746 |
+
def _payload_modelo_coeficientes(session: SessionState) -> dict[str, Any]:
|
| 747 |
+
tabs_cache = _visualizacao_tabs_cache(session)
|
| 748 |
+
cached = tabs_cache.get("coeficientes")
|
| 749 |
+
if isinstance(cached, dict):
|
| 750 |
+
return cached
|
| 751 |
+
|
| 752 |
+
pacote = session.pacote_visualizacao
|
| 753 |
tab_coef = pd.DataFrame(pacote["modelo"]["coeficientes"])
|
| 754 |
if not isinstance(tab_coef.index, pd.RangeIndex):
|
| 755 |
tab_coef.insert(0, "Variável", tab_coef.index.astype(str))
|
|
|
|
| 759 |
tab_coef = pd.concat([tab_coef[mask], tab_coef[~mask]], ignore_index=True)
|
| 760 |
tab_coef = tab_coef.round(4)
|
| 761 |
|
| 762 |
+
payload = {
|
| 763 |
+
"coeficientes": dataframe_to_payload(tab_coef, decimals=4),
|
| 764 |
+
}
|
| 765 |
+
tabs_cache["coeficientes"] = payload
|
| 766 |
+
return payload
|
| 767 |
+
|
| 768 |
+
|
| 769 |
+
def _payload_modelo_obs_calc(session: SessionState) -> dict[str, Any]:
|
| 770 |
+
tabs_cache = _visualizacao_tabs_cache(session)
|
| 771 |
+
cached = tabs_cache.get("obs_calc")
|
| 772 |
+
if isinstance(cached, dict):
|
| 773 |
+
return cached
|
| 774 |
+
|
| 775 |
+
pacote = session.pacote_visualizacao
|
| 776 |
tab_obs_calc = pacote["modelo"]["obs_calc"].reset_index().round(2)
|
| 777 |
+
payload = {
|
| 778 |
+
"obs_calc": dataframe_to_payload(tab_obs_calc, decimals=2),
|
| 779 |
+
}
|
| 780 |
+
tabs_cache["obs_calc"] = payload
|
| 781 |
+
return payload
|
| 782 |
|
|
|
|
| 783 |
|
| 784 |
+
def _payload_modelo_graficos(session: SessionState) -> dict[str, Any]:
|
| 785 |
+
tabs_cache = _visualizacao_tabs_cache(session)
|
| 786 |
+
cached = tabs_cache.get("graficos")
|
| 787 |
+
if isinstance(cached, dict):
|
| 788 |
+
return cached
|
| 789 |
+
|
| 790 |
+
figs = viz_app.gerar_todos_graficos(session.pacote_visualizacao)
|
| 791 |
+
payload = {
|
| 792 |
+
"grafico_obs_calc": figure_to_payload(figs.get("obs_calc")),
|
| 793 |
+
"grafico_residuos": figure_to_payload(figs.get("residuos")),
|
| 794 |
+
"grafico_histograma": figure_to_payload(figs.get("hist")),
|
| 795 |
+
"grafico_cook": figure_to_payload(figs.get("cook")),
|
| 796 |
+
"grafico_correlacao": figure_to_payload(figs.get("corr")),
|
| 797 |
+
}
|
| 798 |
+
tabs_cache["graficos"] = payload
|
| 799 |
+
return payload
|
| 800 |
+
|
| 801 |
+
|
| 802 |
+
def _payload_modelo_trabalhos_tecnicos(
|
| 803 |
+
session: SessionState,
|
| 804 |
+
trabalhos_tecnicos_modelos_modo: Any = None,
|
| 805 |
+
) -> dict[str, Any]:
|
| 806 |
+
_, trabalhos_tecnicos, trabalhos_tecnicos_modelos_modo_norm = _carregar_trabalhos_tecnicos_visualizacao(
|
| 807 |
+
session,
|
| 808 |
+
trabalhos_tecnicos_modelos_modo,
|
| 809 |
+
)
|
| 810 |
+
return {
|
| 811 |
+
"trabalhos_tecnicos": trabalhos_tecnicos,
|
| 812 |
+
"trabalhos_tecnicos_modelos_modo": trabalhos_tecnicos_modelos_modo_norm,
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
|
| 816 |
+
def _payload_modelo_mapa(
|
| 817 |
+
session: SessionState,
|
| 818 |
+
trabalhos_tecnicos_modelos_modo: Any = None,
|
| 819 |
+
api_base_url: str | None = None,
|
| 820 |
+
popup_auth_token: str | None = None,
|
| 821 |
+
) -> dict[str, Any]:
|
| 822 |
+
core = _obter_visualizacao_core(session)
|
| 823 |
+
info = core["info"]
|
| 824 |
avaliandos_tecnicos, trabalhos_tecnicos, trabalhos_tecnicos_modelos_modo_norm = _carregar_trabalhos_tecnicos_visualizacao(
|
| 825 |
session,
|
| 826 |
trabalhos_tecnicos_modelos_modo,
|
| 827 |
)
|
| 828 |
popup_endpoint = f"{api_base_url.rstrip('/')}/api/visualizacao/map/popup" if api_base_url else "/api/visualizacao/map/popup"
|
| 829 |
mapa_html = viz_app.criar_mapa(
|
| 830 |
+
core["dados"],
|
| 831 |
col_y=info["nome_y"],
|
| 832 |
session_id=session.session_id,
|
| 833 |
popup_endpoint=popup_endpoint,
|
| 834 |
popup_auth_token=popup_auth_token,
|
| 835 |
avaliandos_tecnicos=avaliandos_tecnicos,
|
| 836 |
)
|
| 837 |
+
return {
|
| 838 |
+
"mapa_html": mapa_html,
|
| 839 |
+
"mapa_choices": core["mapa_choices"],
|
| 840 |
+
"trabalhos_tecnicos": trabalhos_tecnicos,
|
| 841 |
+
"trabalhos_tecnicos_modelos_modo": trabalhos_tecnicos_modelos_modo_norm,
|
| 842 |
+
}
|
| 843 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 844 |
|
| 845 |
+
def carregar_secao_modelo(
|
| 846 |
+
session: SessionState,
|
| 847 |
+
secao: str,
|
| 848 |
+
trabalhos_tecnicos_modelos_modo: Any = None,
|
| 849 |
+
api_base_url: str | None = None,
|
| 850 |
+
popup_auth_token: str | None = None,
|
| 851 |
+
) -> dict[str, Any]:
|
| 852 |
+
secao_norm = str(secao or "").strip().lower().replace("-", "_")
|
| 853 |
+
if secao_norm == "mapa":
|
| 854 |
+
return _payload_modelo_mapa(
|
| 855 |
+
session,
|
| 856 |
+
trabalhos_tecnicos_modelos_modo=trabalhos_tecnicos_modelos_modo,
|
| 857 |
+
api_base_url=api_base_url,
|
| 858 |
+
popup_auth_token=popup_auth_token,
|
| 859 |
+
)
|
| 860 |
+
if secao_norm == "trabalhos_tecnicos":
|
| 861 |
+
return _payload_modelo_trabalhos_tecnicos(session, trabalhos_tecnicos_modelos_modo)
|
| 862 |
+
if secao_norm == "dados_mercado":
|
| 863 |
+
return _payload_modelo_dados_mercado(session)
|
| 864 |
+
if secao_norm == "metricas":
|
| 865 |
+
return _payload_modelo_metricas(session)
|
| 866 |
+
if secao_norm == "transformacoes":
|
| 867 |
+
return _payload_modelo_transformacoes(session)
|
| 868 |
+
if secao_norm == "resumo":
|
| 869 |
+
return _payload_modelo_resumo(session)
|
| 870 |
+
if secao_norm == "coeficientes":
|
| 871 |
+
return _payload_modelo_coeficientes(session)
|
| 872 |
+
if secao_norm == "obs_calc":
|
| 873 |
+
return _payload_modelo_obs_calc(session)
|
| 874 |
+
if secao_norm == "graficos":
|
| 875 |
+
return _payload_modelo_graficos(session)
|
| 876 |
+
raise HTTPException(status_code=400, detail="Secao de visualizacao invalida")
|
| 877 |
|
| 878 |
+
|
| 879 |
+
def exibir_contexto_avaliacao(session: SessionState) -> dict[str, Any]:
|
| 880 |
+
pacote = session.pacote_visualizacao
|
| 881 |
+
if pacote is None:
|
| 882 |
+
raise HTTPException(status_code=400, detail="Carregue um modelo primeiro")
|
| 883 |
+
|
| 884 |
+
info = _extrair_modelo_info(pacote)
|
| 885 |
+
equacoes = _equacoes_do_modelo(pacote, info)
|
| 886 |
return {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 887 |
"campos_avaliacao": campos_avaliacao(session),
|
| 888 |
"meta_modelo": sanitize_value(info),
|
| 889 |
"equacoes": sanitize_value(equacoes),
|
|
|
|
|
|
|
| 890 |
}
|
| 891 |
|
| 892 |
|
| 893 |
+
def exibir_modelo(
|
| 894 |
+
session: SessionState,
|
| 895 |
+
trabalhos_tecnicos_modelos_modo: Any = None,
|
| 896 |
+
api_base_url: str | None = None,
|
| 897 |
+
popup_auth_token: str | None = None,
|
| 898 |
+
) -> dict[str, Any]:
|
| 899 |
+
core = _obter_visualizacao_core(session)
|
| 900 |
+
resposta = {
|
| 901 |
+
"campos_avaliacao": campos_avaliacao(session),
|
| 902 |
+
"meta_modelo": sanitize_value(core["info"]),
|
| 903 |
+
}
|
| 904 |
+
for secao in (
|
| 905 |
+
"dados_mercado",
|
| 906 |
+
"metricas",
|
| 907 |
+
"transformacoes",
|
| 908 |
+
"resumo",
|
| 909 |
+
"coeficientes",
|
| 910 |
+
"obs_calc",
|
| 911 |
+
"graficos",
|
| 912 |
+
):
|
| 913 |
+
resposta.update(carregar_secao_modelo(session, secao))
|
| 914 |
+
resposta.update(
|
| 915 |
+
carregar_secao_modelo(
|
| 916 |
+
session,
|
| 917 |
+
"mapa",
|
| 918 |
+
trabalhos_tecnicos_modelos_modo=trabalhos_tecnicos_modelos_modo,
|
| 919 |
+
api_base_url=api_base_url,
|
| 920 |
+
popup_auth_token=popup_auth_token,
|
| 921 |
+
)
|
| 922 |
+
)
|
| 923 |
+
return resposta
|
| 924 |
+
|
| 925 |
+
|
| 926 |
def atualizar_mapa(
|
| 927 |
session: SessionState,
|
| 928 |
variavel_mapa: str | None,
|
backend/run_backend.sh
CHANGED
|
@@ -4,4 +4,10 @@ set -euo pipefail
|
|
| 4 |
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 5 |
cd "${SCRIPT_DIR}"
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
uvicorn app.main:app --host 0.0.0.0 --port "${PORT:-8000}" --reload --reload-dir "${SCRIPT_DIR}"
|
|
|
|
| 4 |
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 5 |
cd "${SCRIPT_DIR}"
|
| 6 |
|
| 7 |
+
# Prefere o ambiente virtual local do backend quando ele existir.
|
| 8 |
+
if [[ -f "${SCRIPT_DIR}/.venv/bin/activate" ]]; then
|
| 9 |
+
# shellcheck disable=SC1091
|
| 10 |
+
source "${SCRIPT_DIR}/.venv/bin/activate"
|
| 11 |
+
fi
|
| 12 |
+
|
| 13 |
uvicorn app.main:app --host 0.0.0.0 --port "${PORT:-8000}" --reload --reload-dir "${SCRIPT_DIR}"
|
frontend/src/App.jsx
CHANGED
|
@@ -1,5 +1,13 @@
|
|
| 1 |
import React, { useEffect, useRef, useState } from 'react'
|
| 2 |
import { api, getAuthToken, setAuthToken } from './api'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
import AvaliacaoTab from './components/AvaliacaoTab'
|
| 4 |
import ElaboracaoTab from './components/ElaboracaoTab'
|
| 5 |
import InicioTab from './components/InicioTab'
|
|
@@ -21,9 +29,13 @@ export default function App() {
|
|
| 21 |
const [sessionId, setSessionId] = useState('')
|
| 22 |
const [bootError, setBootError] = useState('')
|
| 23 |
const [avaliacaoQuickLoad, setAvaliacaoQuickLoad] = useState(null)
|
| 24 |
-
const [
|
| 25 |
const [trabalhoTecnicoQuickOpen, setTrabalhoTecnicoQuickOpen] = useState(null)
|
| 26 |
-
const [
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
const [authLoading, setAuthLoading] = useState(true)
|
| 29 |
const [authUser, setAuthUser] = useState(null)
|
|
@@ -45,6 +57,11 @@ export default function App() {
|
|
| 45 |
const [scrollHomeBtnLeft, setScrollHomeBtnLeft] = useState(8)
|
| 46 |
const headerRef = useRef(null)
|
| 47 |
const settingsMenuRef = useRef(null)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
const isAdmin = String(authUser?.perfil || '').toLowerCase() === 'admin'
|
| 50 |
const logsEnabled = Boolean(logsStatus?.enabled)
|
|
@@ -56,6 +73,44 @@ export default function App() {
|
|
| 56 |
const logsVisibleEvents = logsEvents.slice(logsStartIndex, logsStartIndex + LOGS_PAGE_SIZE)
|
| 57 |
const logsEndIndex = logsEvents.length ? Math.min(logsStartIndex + LOGS_PAGE_SIZE, logsEvents.length) : 0
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
function resetToLogin(message = '') {
|
| 60 |
setAuthToken('')
|
| 61 |
setAuthUser(null)
|
|
@@ -63,9 +118,13 @@ export default function App() {
|
|
| 63 |
setLoginLoading(false)
|
| 64 |
setSessionId('')
|
| 65 |
setBootError('')
|
| 66 |
-
|
| 67 |
setTrabalhoTecnicoQuickOpen(null)
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
setLogsStatus(null)
|
| 70 |
setLogsOpen(false)
|
| 71 |
setLogsEvents([])
|
|
@@ -76,6 +135,19 @@ export default function App() {
|
|
| 76 |
setAuthError(message)
|
| 77 |
}
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
useEffect(() => {
|
| 80 |
let mounted = true
|
| 81 |
|
|
@@ -128,7 +200,7 @@ export default function App() {
|
|
| 128 |
if (!trabalhoId) return
|
| 129 |
|
| 130 |
setTrabalhoTecnicoQuickOpen({
|
| 131 |
-
requestKey:
|
| 132 |
trabalhoId,
|
| 133 |
origem: String(data?.origem || '').trim() || 'pesquisa_mapa',
|
| 134 |
})
|
|
@@ -136,6 +208,10 @@ export default function App() {
|
|
| 136 |
setLogsOpen(false)
|
| 137 |
setShowStartupIntro(false)
|
| 138 |
setSettingsOpen(false)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
}
|
| 140 |
|
| 141 |
if (typeof window !== 'undefined') {
|
|
@@ -174,6 +250,70 @@ export default function App() {
|
|
| 174 |
}
|
| 175 |
}, [authUser])
|
| 176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
useEffect(() => {
|
| 178 |
if (!authUser || !isAdmin) {
|
| 179 |
setLogsStatus(null)
|
|
@@ -346,7 +486,7 @@ export default function App() {
|
|
| 346 |
const modeloId = String(modelo?.id || '').trim()
|
| 347 |
if (!modeloId) return
|
| 348 |
setAvaliacaoQuickLoad({
|
| 349 |
-
requestKey:
|
| 350 |
modeloId,
|
| 351 |
modeloArquivo: String(modelo?.arquivo || '').trim(),
|
| 352 |
nomeModelo: String(modelo?.nome_modelo || modelo?.arquivo || modeloId),
|
|
@@ -354,30 +494,111 @@ export default function App() {
|
|
| 354 |
setActiveTab('Avaliação')
|
| 355 |
setLogsOpen(false)
|
| 356 |
setShowStartupIntro(false)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
}
|
| 358 |
|
| 359 |
function onAbrirModeloNoRepositorio(modelo) {
|
| 360 |
const modeloId = String(modelo?.modeloId || modelo?.id || '').trim()
|
| 361 |
if (!modeloId) return
|
| 362 |
-
|
| 363 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
modeloId,
|
| 365 |
-
|
| 366 |
-
nomeModelo
|
|
|
|
|
|
|
| 367 |
})
|
| 368 |
setActiveTab('Modelos Estatísticos')
|
| 369 |
setLogsOpen(false)
|
| 370 |
setShowStartupIntro(false)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
}
|
| 372 |
|
| 373 |
function onVoltarAoMapaPesquisa() {
|
| 374 |
-
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
})
|
| 377 |
setActiveTab('Modelos Estatísticos')
|
| 378 |
setLogsOpen(false)
|
| 379 |
setShowStartupIntro(false)
|
| 380 |
setSettingsOpen(false)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
}
|
| 382 |
|
| 383 |
function onScrollToHeader() {
|
|
@@ -390,81 +611,99 @@ export default function App() {
|
|
| 390 |
|
| 391 |
return (
|
| 392 |
<div className="app-shell">
|
| 393 |
-
|
| 394 |
-
<
|
| 395 |
-
<
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
<
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
key={tab.key}
|
| 405 |
-
className={active ? 'tab-pill active' : 'tab-pill'}
|
| 406 |
-
onClick={() => {
|
| 407 |
-
setActiveTab(tab.key)
|
| 408 |
-
setShowStartupIntro(false)
|
| 409 |
-
setLogsOpen(false)
|
| 410 |
-
setSettingsOpen(false)
|
| 411 |
-
}}
|
| 412 |
-
type="button"
|
| 413 |
-
>
|
| 414 |
-
<strong>{tab.label}</strong>
|
| 415 |
-
</button>
|
| 416 |
-
)
|
| 417 |
-
})}
|
| 418 |
-
</nav>
|
| 419 |
-
|
| 420 |
-
<div ref={settingsMenuRef} className={`settings-menu${settingsOpen ? ' is-open' : ''}`}>
|
| 421 |
-
<button
|
| 422 |
-
type="button"
|
| 423 |
-
className="settings-gear-btn"
|
| 424 |
-
aria-haspopup="menu"
|
| 425 |
-
aria-expanded={settingsOpen}
|
| 426 |
-
aria-label="Abrir configurações"
|
| 427 |
-
onClick={() => setSettingsOpen((prev) => !prev)}
|
| 428 |
-
title="Configurações"
|
| 429 |
-
>
|
| 430 |
-
⚙
|
| 431 |
-
</button>
|
| 432 |
-
{settingsOpen ? (
|
| 433 |
-
<div className="settings-menu-panel" role="menu" aria-label="Configurações do usuário">
|
| 434 |
-
<div className="settings-user-summary">
|
| 435 |
-
Usuário: <strong>{authUser.nome || authUser.usuario}</strong> ({authUser.perfil || 'viewer'})
|
| 436 |
-
</div>
|
| 437 |
-
<div className="settings-menu-actions">
|
| 438 |
-
{isAdmin ? (
|
| 439 |
-
<button
|
| 440 |
-
type="button"
|
| 441 |
-
className="settings-menu-btn"
|
| 442 |
-
onClick={() => {
|
| 443 |
-
void onToggleLogs()
|
| 444 |
-
setSettingsOpen(false)
|
| 445 |
-
}}
|
| 446 |
-
disabled={logsStatusLoading || (!logsEnabled && !logsOpen)}
|
| 447 |
-
title={logsOpen ? 'Fechar visualização de logs' : !logsEnabled ? logsDisabledReason : 'Abrir leitura de logs'}
|
| 448 |
-
>
|
| 449 |
-
{logsOpen ? 'Fechar logs' : 'Abrir logs'}
|
| 450 |
-
</button>
|
| 451 |
-
) : null}
|
| 452 |
<button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 453 |
type="button"
|
| 454 |
-
className="settings-menu-btn settings-menu-btn-danger"
|
| 455 |
-
onClick={() => void onLogout()}
|
| 456 |
>
|
| 457 |
-
|
| 458 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 459 |
</div>
|
| 460 |
-
|
| 461 |
-
|
| 462 |
</div>
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
|
| 467 |
-
{authUser && showScrollHomeBtn ? (
|
| 468 |
<button
|
| 469 |
type="button"
|
| 470 |
className="scroll-home-btn"
|
|
@@ -645,13 +884,21 @@ export default function App() {
|
|
| 645 |
sessionId={sessionId}
|
| 646 |
authUser={authUser}
|
| 647 |
onUsarModeloEmAvaliacao={onUsarModeloEmAvaliacao}
|
| 648 |
-
|
| 649 |
-
|
|
|
|
|
|
|
|
|
|
| 650 |
/>
|
| 651 |
</div>
|
| 652 |
|
| 653 |
<div className="tab-pane" hidden={activeTab !== 'Elaboração/Ediç��o'}>
|
| 654 |
-
<ElaboracaoTab
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 655 |
</div>
|
| 656 |
|
| 657 |
<div className="tab-pane" hidden={activeTab !== 'Trabalhos Técnicos'}>
|
|
@@ -659,12 +906,19 @@ export default function App() {
|
|
| 659 |
sessionId={sessionId}
|
| 660 |
onAbrirModeloNoRepositorio={onAbrirModeloNoRepositorio}
|
| 661 |
quickOpenRequest={trabalhoTecnicoQuickOpen}
|
|
|
|
|
|
|
| 662 |
onVoltarAoMapaPesquisa={onVoltarAoMapaPesquisa}
|
|
|
|
| 663 |
/>
|
| 664 |
</div>
|
| 665 |
|
| 666 |
<div className="tab-pane" hidden={activeTab !== 'Avaliação'}>
|
| 667 |
-
<AvaliacaoTab
|
|
|
|
|
|
|
|
|
|
|
|
|
| 668 |
</div>
|
| 669 |
</>
|
| 670 |
)
|
|
|
|
| 1 |
import React, { useEffect, useRef, useState } from 'react'
|
| 2 |
import { api, getAuthToken, setAuthToken } from './api'
|
| 3 |
+
import {
|
| 4 |
+
getAppTabKeyFromSlug,
|
| 5 |
+
getAppTabSlugFromKey,
|
| 6 |
+
hasMesaDeepLink,
|
| 7 |
+
normalizeMesaDeepLink,
|
| 8 |
+
parseMesaDeepLink,
|
| 9 |
+
replaceMesaDeepLink,
|
| 10 |
+
} from './deepLinks'
|
| 11 |
import AvaliacaoTab from './components/AvaliacaoTab'
|
| 12 |
import ElaboracaoTab from './components/ElaboracaoTab'
|
| 13 |
import InicioTab from './components/InicioTab'
|
|
|
|
| 29 |
const [sessionId, setSessionId] = useState('')
|
| 30 |
const [bootError, setBootError] = useState('')
|
| 31 |
const [avaliacaoQuickLoad, setAvaliacaoQuickLoad] = useState(null)
|
| 32 |
+
const [elaboracaoQuickLoad, setElaboracaoQuickLoad] = useState(null)
|
| 33 |
const [trabalhoTecnicoQuickOpen, setTrabalhoTecnicoQuickOpen] = useState(null)
|
| 34 |
+
const [modelosRouteRequest, setModelosRouteRequest] = useState(null)
|
| 35 |
+
const [trabalhosRouteRequest, setTrabalhosRouteRequest] = useState(null)
|
| 36 |
+
const [pendingDeepLinkIntent, setPendingDeepLinkIntent] = useState(null)
|
| 37 |
+
const [modelosModoImersivo, setModelosModoImersivo] = useState(false)
|
| 38 |
+
const [trabalhosModoImersivo, setTrabalhosModoImersivo] = useState(false)
|
| 39 |
|
| 40 |
const [authLoading, setAuthLoading] = useState(true)
|
| 41 |
const [authUser, setAuthUser] = useState(null)
|
|
|
|
| 57 |
const [scrollHomeBtnLeft, setScrollHomeBtnLeft] = useState(8)
|
| 58 |
const headerRef = useRef(null)
|
| 59 |
const settingsMenuRef = useRef(null)
|
| 60 |
+
const lastModelosRouteRef = useRef(null)
|
| 61 |
+
const lastPesquisaRouteRef = useRef(null)
|
| 62 |
+
const lastTrabalhosRouteRef = useRef(null)
|
| 63 |
+
const lastAvaliacaoRouteRef = useRef(null)
|
| 64 |
+
const lastElaboracaoRouteRef = useRef(null)
|
| 65 |
|
| 66 |
const isAdmin = String(authUser?.perfil || '').toLowerCase() === 'admin'
|
| 67 |
const logsEnabled = Boolean(logsStatus?.enabled)
|
|
|
|
| 73 |
const logsVisibleEvents = logsEvents.slice(logsStartIndex, logsStartIndex + LOGS_PAGE_SIZE)
|
| 74 |
const logsEndIndex = logsEvents.length ? Math.min(logsStartIndex + LOGS_PAGE_SIZE, logsEvents.length) : 0
|
| 75 |
|
| 76 |
+
function buildRequestKey(prefix = 'mesa') {
|
| 77 |
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
function rememberRouteIntent(intent) {
|
| 81 |
+
const normalized = normalizeMesaDeepLink(intent)
|
| 82 |
+
if (!normalized.tab) return normalized
|
| 83 |
+
if (normalized.tab === 'modelos') {
|
| 84 |
+
lastModelosRouteRef.current = normalized
|
| 85 |
+
if (normalized.subtab === 'pesquisa') {
|
| 86 |
+
lastPesquisaRouteRef.current = normalized
|
| 87 |
+
}
|
| 88 |
+
}
|
| 89 |
+
if (normalized.tab === 'trabalhos') lastTrabalhosRouteRef.current = normalized
|
| 90 |
+
if (normalized.tab === 'avaliacao') lastAvaliacaoRouteRef.current = normalized
|
| 91 |
+
if (normalized.tab === 'elaboracao') lastElaboracaoRouteRef.current = normalized
|
| 92 |
+
return normalized
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
function syncRouteIntent(intent) {
|
| 96 |
+
const normalized = rememberRouteIntent(intent)
|
| 97 |
+
replaceMesaDeepLink(normalized)
|
| 98 |
+
return normalized
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
function getCurrentMesaRouteIntent(fallbackIntent = null) {
|
| 102 |
+
if (typeof window !== 'undefined') {
|
| 103 |
+
const parsed = parseMesaDeepLink(window.location.search)
|
| 104 |
+
if (hasMesaDeepLink(parsed)) {
|
| 105 |
+
return normalizeMesaDeepLink(parsed)
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
+
if (fallbackIntent) {
|
| 109 |
+
return normalizeMesaDeepLink(fallbackIntent)
|
| 110 |
+
}
|
| 111 |
+
return null
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
function resetToLogin(message = '') {
|
| 115 |
setAuthToken('')
|
| 116 |
setAuthUser(null)
|
|
|
|
| 118 |
setLoginLoading(false)
|
| 119 |
setSessionId('')
|
| 120 |
setBootError('')
|
| 121 |
+
setElaboracaoQuickLoad(null)
|
| 122 |
setTrabalhoTecnicoQuickOpen(null)
|
| 123 |
+
setModelosRouteRequest(null)
|
| 124 |
+
setTrabalhosRouteRequest(null)
|
| 125 |
+
setPendingDeepLinkIntent(null)
|
| 126 |
+
setModelosModoImersivo(false)
|
| 127 |
+
setTrabalhosModoImersivo(false)
|
| 128 |
setLogsStatus(null)
|
| 129 |
setLogsOpen(false)
|
| 130 |
setLogsEvents([])
|
|
|
|
| 135 |
setAuthError(message)
|
| 136 |
}
|
| 137 |
|
| 138 |
+
useEffect(() => {
|
| 139 |
+
if (typeof window === 'undefined') return undefined
|
| 140 |
+
|
| 141 |
+
function syncPendingDeepLink() {
|
| 142 |
+
const nextIntent = parseMesaDeepLink(window.location.search)
|
| 143 |
+
setPendingDeepLinkIntent(hasMesaDeepLink(nextIntent) ? nextIntent : null)
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
syncPendingDeepLink()
|
| 147 |
+
window.addEventListener('popstate', syncPendingDeepLink)
|
| 148 |
+
return () => window.removeEventListener('popstate', syncPendingDeepLink)
|
| 149 |
+
}, [])
|
| 150 |
+
|
| 151 |
useEffect(() => {
|
| 152 |
let mounted = true
|
| 153 |
|
|
|
|
| 200 |
if (!trabalhoId) return
|
| 201 |
|
| 202 |
setTrabalhoTecnicoQuickOpen({
|
| 203 |
+
requestKey: buildRequestKey('trabalho-open'),
|
| 204 |
trabalhoId,
|
| 205 |
origem: String(data?.origem || '').trim() || 'pesquisa_mapa',
|
| 206 |
})
|
|
|
|
| 208 |
setLogsOpen(false)
|
| 209 |
setShowStartupIntro(false)
|
| 210 |
setSettingsOpen(false)
|
| 211 |
+
syncRouteIntent({
|
| 212 |
+
tab: 'trabalhos',
|
| 213 |
+
trabalhoId,
|
| 214 |
+
})
|
| 215 |
}
|
| 216 |
|
| 217 |
if (typeof window !== 'undefined') {
|
|
|
|
| 250 |
}
|
| 251 |
}, [authUser])
|
| 252 |
|
| 253 |
+
useEffect(() => {
|
| 254 |
+
if (!pendingDeepLinkIntent || !authUser) return
|
| 255 |
+
const normalized = normalizeMesaDeepLink(pendingDeepLinkIntent)
|
| 256 |
+
const requiresSession = Boolean(
|
| 257 |
+
normalized.modeloId
|
| 258 |
+
|| normalized.trabalhoId
|
| 259 |
+
|| normalized.tab === 'avaliacao'
|
| 260 |
+
|| normalized.tab === 'elaboracao',
|
| 261 |
+
)
|
| 262 |
+
if (requiresSession && !sessionId) return
|
| 263 |
+
|
| 264 |
+
const nextTab = getAppTabKeyFromSlug(normalized.tab)
|
| 265 |
+
const requestKey = buildRequestKey('deep-link')
|
| 266 |
+
if (nextTab) {
|
| 267 |
+
setActiveTab(nextTab)
|
| 268 |
+
setShowStartupIntro(false)
|
| 269 |
+
setLogsOpen(false)
|
| 270 |
+
setSettingsOpen(false)
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
if (normalized.tab === 'modelos') {
|
| 274 |
+
setModelosRouteRequest({
|
| 275 |
+
requestKey,
|
| 276 |
+
subtab: normalized.subtab,
|
| 277 |
+
filters: normalized.filters,
|
| 278 |
+
avaliando: normalized.avaliando,
|
| 279 |
+
modeloId: normalized.modeloId,
|
| 280 |
+
modelTab: normalized.modelTab,
|
| 281 |
+
})
|
| 282 |
+
rememberRouteIntent(normalized)
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
if (normalized.tab === 'avaliacao' && normalized.modeloId) {
|
| 286 |
+
setAvaliacaoQuickLoad({
|
| 287 |
+
requestKey,
|
| 288 |
+
modeloId: normalized.modeloId,
|
| 289 |
+
modeloArquivo: '',
|
| 290 |
+
nomeModelo: normalized.modeloId,
|
| 291 |
+
})
|
| 292 |
+
rememberRouteIntent(normalized)
|
| 293 |
+
}
|
| 294 |
+
|
| 295 |
+
if (normalized.tab === 'elaboracao' && normalized.modeloId) {
|
| 296 |
+
setElaboracaoQuickLoad({
|
| 297 |
+
requestKey,
|
| 298 |
+
modeloId: normalized.modeloId,
|
| 299 |
+
nomeModelo: normalized.modeloId,
|
| 300 |
+
})
|
| 301 |
+
rememberRouteIntent(normalized)
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
if (normalized.tab === 'trabalhos') {
|
| 305 |
+
setTrabalhosRouteRequest({
|
| 306 |
+
requestKey,
|
| 307 |
+
subtab: normalized.subtab,
|
| 308 |
+
trabalhoId: normalized.trabalhoId,
|
| 309 |
+
})
|
| 310 |
+
rememberRouteIntent(normalized)
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
replaceMesaDeepLink(normalized)
|
| 314 |
+
setPendingDeepLinkIntent(null)
|
| 315 |
+
}, [authUser, pendingDeepLinkIntent, sessionId])
|
| 316 |
+
|
| 317 |
useEffect(() => {
|
| 318 |
if (!authUser || !isAdmin) {
|
| 319 |
setLogsStatus(null)
|
|
|
|
| 486 |
const modeloId = String(modelo?.id || '').trim()
|
| 487 |
if (!modeloId) return
|
| 488 |
setAvaliacaoQuickLoad({
|
| 489 |
+
requestKey: buildRequestKey('avaliacao-open'),
|
| 490 |
modeloId,
|
| 491 |
modeloArquivo: String(modelo?.arquivo || '').trim(),
|
| 492 |
nomeModelo: String(modelo?.nome_modelo || modelo?.arquivo || modeloId),
|
|
|
|
| 494 |
setActiveTab('Avaliação')
|
| 495 |
setLogsOpen(false)
|
| 496 |
setShowStartupIntro(false)
|
| 497 |
+
syncRouteIntent({ tab: 'avaliacao', modeloId })
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
function onEditarModeloEmElaboracao(modelo) {
|
| 501 |
+
const modeloId = String(modelo?.id || '').trim()
|
| 502 |
+
if (!modeloId) return
|
| 503 |
+
setElaboracaoQuickLoad({
|
| 504 |
+
requestKey: buildRequestKey('elaboracao-open'),
|
| 505 |
+
modeloId,
|
| 506 |
+
modeloArquivo: String(modelo?.arquivo || '').trim(),
|
| 507 |
+
nomeModelo: String(modelo?.nome_modelo || modelo?.arquivo || modeloId),
|
| 508 |
+
})
|
| 509 |
+
setActiveTab('Elaboração/Edição')
|
| 510 |
+
setLogsOpen(false)
|
| 511 |
+
setShowStartupIntro(false)
|
| 512 |
+
syncRouteIntent({ tab: 'elaboracao', modeloId })
|
| 513 |
}
|
| 514 |
|
| 515 |
function onAbrirModeloNoRepositorio(modelo) {
|
| 516 |
const modeloId = String(modelo?.modeloId || modelo?.id || '').trim()
|
| 517 |
if (!modeloId) return
|
| 518 |
+
const nomeModelo = String(modelo?.nomeModelo || modelo?.nome_modelo || modeloId).trim()
|
| 519 |
+
const modeloArquivo = String(modelo?.modeloArquivo || modelo?.arquivo || '').trim()
|
| 520 |
+
const explicitReturnIntent = modelo?.returnIntent && typeof modelo.returnIntent === 'object'
|
| 521 |
+
? modelo.returnIntent
|
| 522 |
+
: null
|
| 523 |
+
const returnIntent = explicitReturnIntent || getCurrentMesaRouteIntent(lastModelosRouteRef.current || { tab: 'modelos', subtab: 'repositorio' })
|
| 524 |
+
setModelosRouteRequest({
|
| 525 |
+
requestKey: buildRequestKey('repo-open'),
|
| 526 |
+
subtab: 'repositorio',
|
| 527 |
modeloId,
|
| 528 |
+
modelTab: 'mapa',
|
| 529 |
+
nomeModelo,
|
| 530 |
+
modeloArquivo,
|
| 531 |
+
returnIntent,
|
| 532 |
})
|
| 533 |
setActiveTab('Modelos Estatísticos')
|
| 534 |
setLogsOpen(false)
|
| 535 |
setShowStartupIntro(false)
|
| 536 |
+
syncRouteIntent({
|
| 537 |
+
tab: 'modelos',
|
| 538 |
+
subtab: 'repositorio',
|
| 539 |
+
modeloId,
|
| 540 |
+
modelTab: 'mapa',
|
| 541 |
+
})
|
| 542 |
}
|
| 543 |
|
| 544 |
function onVoltarAoMapaPesquisa() {
|
| 545 |
+
const nextIntent = lastPesquisaRouteRef.current || { tab: 'modelos', subtab: 'pesquisa' }
|
| 546 |
+
setModelosRouteRequest({
|
| 547 |
+
requestKey: buildRequestKey('pesquisa-return'),
|
| 548 |
+
subtab: nextIntent.subtab || 'pesquisa',
|
| 549 |
+
filters: nextIntent.filters || {},
|
| 550 |
+
avaliando: nextIntent.avaliando || null,
|
| 551 |
+
scrollToMapa: true,
|
| 552 |
})
|
| 553 |
setActiveTab('Modelos Estatísticos')
|
| 554 |
setLogsOpen(false)
|
| 555 |
setShowStartupIntro(false)
|
| 556 |
setSettingsOpen(false)
|
| 557 |
+
syncRouteIntent(nextIntent)
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
function onModelosRouteChange(intent) {
|
| 561 |
+
const normalized = normalizeMesaDeepLink(intent)
|
| 562 |
+
if (intent?.forceRouteRequest) {
|
| 563 |
+
const nextTab = getAppTabKeyFromSlug(normalized.tab)
|
| 564 |
+
if (nextTab) {
|
| 565 |
+
setActiveTab(nextTab)
|
| 566 |
+
setShowStartupIntro(false)
|
| 567 |
+
setLogsOpen(false)
|
| 568 |
+
setSettingsOpen(false)
|
| 569 |
+
}
|
| 570 |
+
if (normalized.tab === 'modelos' && !normalized.modeloId) {
|
| 571 |
+
setModelosRouteRequest({
|
| 572 |
+
requestKey: buildRequestKey('modelos-route'),
|
| 573 |
+
subtab: normalized.subtab || 'pesquisa',
|
| 574 |
+
filters: normalized.filters,
|
| 575 |
+
avaliando: normalized.avaliando,
|
| 576 |
+
avaliandos: Array.isArray(intent?.avaliandos) ? intent.avaliandos : null,
|
| 577 |
+
pesquisaExecutada: Boolean(intent?.pesquisaExecutada),
|
| 578 |
+
scrollToMapa: Boolean(intent?.scrollToMapa),
|
| 579 |
+
})
|
| 580 |
+
}
|
| 581 |
+
if (normalized.tab === 'trabalhos') {
|
| 582 |
+
setTrabalhosRouteRequest({
|
| 583 |
+
requestKey: buildRequestKey('trabalhos-route'),
|
| 584 |
+
subtab: normalized.subtab,
|
| 585 |
+
trabalhoId: normalized.trabalhoId,
|
| 586 |
+
})
|
| 587 |
+
}
|
| 588 |
+
}
|
| 589 |
+
syncRouteIntent(normalized)
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
function onAvaliacaoRouteChange(intent) {
|
| 593 |
+
syncRouteIntent(intent)
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
function onElaboracaoRouteChange(intent) {
|
| 597 |
+
syncRouteIntent(intent)
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
function onTrabalhosRouteChange(intent) {
|
| 601 |
+
syncRouteIntent(intent)
|
| 602 |
}
|
| 603 |
|
| 604 |
function onScrollToHeader() {
|
|
|
|
| 611 |
|
| 612 |
return (
|
| 613 |
<div className="app-shell">
|
| 614 |
+
{!(modelosModoImersivo || trabalhosModoImersivo) ? (
|
| 615 |
+
<header ref={headerRef} className={authUser ? 'app-header app-header-logged' : 'app-header app-header-logo-only'}>
|
| 616 |
+
<div className="brand-mark" aria-hidden="true">
|
| 617 |
+
<img src={`${import.meta.env.BASE_URL}logo_mesa.png`} alt="MESA" />
|
| 618 |
+
</div>
|
| 619 |
+
{authUser ? (
|
| 620 |
+
<div className="app-top-actions">
|
| 621 |
+
<nav className="tabs" aria-label="Navegação principal">
|
| 622 |
+
{TABS.map((tab) => {
|
| 623 |
+
const active = tab.key === activeTab
|
| 624 |
+
return (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 625 |
<button
|
| 626 |
+
key={tab.key}
|
| 627 |
+
className={active ? 'tab-pill active' : 'tab-pill'}
|
| 628 |
+
onClick={() => {
|
| 629 |
+
setActiveTab(tab.key)
|
| 630 |
+
setShowStartupIntro(false)
|
| 631 |
+
setLogsOpen(false)
|
| 632 |
+
setSettingsOpen(false)
|
| 633 |
+
const slug = getAppTabSlugFromKey(tab.key)
|
| 634 |
+
if (slug === 'modelos') {
|
| 635 |
+
syncRouteIntent(lastModelosRouteRef.current || { tab: 'modelos' })
|
| 636 |
+
return
|
| 637 |
+
}
|
| 638 |
+
if (slug === 'trabalhos') {
|
| 639 |
+
syncRouteIntent(lastTrabalhosRouteRef.current || { tab: 'trabalhos' })
|
| 640 |
+
return
|
| 641 |
+
}
|
| 642 |
+
if (slug === 'avaliacao') {
|
| 643 |
+
syncRouteIntent(lastAvaliacaoRouteRef.current || { tab: 'avaliacao' })
|
| 644 |
+
return
|
| 645 |
+
}
|
| 646 |
+
if (slug === 'elaboracao') {
|
| 647 |
+
syncRouteIntent(lastElaboracaoRouteRef.current || { tab: 'elaboracao' })
|
| 648 |
+
}
|
| 649 |
+
}}
|
| 650 |
type="button"
|
|
|
|
|
|
|
| 651 |
>
|
| 652 |
+
<strong>{tab.label}</strong>
|
| 653 |
</button>
|
| 654 |
+
)
|
| 655 |
+
})}
|
| 656 |
+
</nav>
|
| 657 |
+
|
| 658 |
+
<div ref={settingsMenuRef} className={`settings-menu${settingsOpen ? ' is-open' : ''}`}>
|
| 659 |
+
<button
|
| 660 |
+
type="button"
|
| 661 |
+
className="settings-gear-btn"
|
| 662 |
+
aria-haspopup="menu"
|
| 663 |
+
aria-expanded={settingsOpen}
|
| 664 |
+
aria-label="Abrir configurações"
|
| 665 |
+
onClick={() => setSettingsOpen((prev) => !prev)}
|
| 666 |
+
title="Configurações"
|
| 667 |
+
>
|
| 668 |
+
⚙
|
| 669 |
+
</button>
|
| 670 |
+
{settingsOpen ? (
|
| 671 |
+
<div className="settings-menu-panel" role="menu" aria-label="Configurações do usuário">
|
| 672 |
+
<div className="settings-user-summary">
|
| 673 |
+
Usuário: <strong>{authUser.nome || authUser.usuario}</strong> ({authUser.perfil || 'viewer'})
|
| 674 |
+
</div>
|
| 675 |
+
<div className="settings-menu-actions">
|
| 676 |
+
{isAdmin ? (
|
| 677 |
+
<button
|
| 678 |
+
type="button"
|
| 679 |
+
className="settings-menu-btn"
|
| 680 |
+
onClick={() => {
|
| 681 |
+
void onToggleLogs()
|
| 682 |
+
setSettingsOpen(false)
|
| 683 |
+
}}
|
| 684 |
+
disabled={logsStatusLoading || (!logsEnabled && !logsOpen)}
|
| 685 |
+
title={logsOpen ? 'Fechar visualização de logs' : !logsEnabled ? logsDisabledReason : 'Abrir leitura de logs'}
|
| 686 |
+
>
|
| 687 |
+
{logsOpen ? 'Fechar logs' : 'Abrir logs'}
|
| 688 |
+
</button>
|
| 689 |
+
) : null}
|
| 690 |
+
<button
|
| 691 |
+
type="button"
|
| 692 |
+
className="settings-menu-btn settings-menu-btn-danger"
|
| 693 |
+
onClick={() => void onLogout()}
|
| 694 |
+
>
|
| 695 |
+
Sair
|
| 696 |
+
</button>
|
| 697 |
+
</div>
|
| 698 |
</div>
|
| 699 |
+
) : null}
|
| 700 |
+
</div>
|
| 701 |
</div>
|
| 702 |
+
) : null}
|
| 703 |
+
</header>
|
| 704 |
+
) : null}
|
| 705 |
|
| 706 |
+
{authUser && showScrollHomeBtn && !(modelosModoImersivo || trabalhosModoImersivo) ? (
|
| 707 |
<button
|
| 708 |
type="button"
|
| 709 |
className="scroll-home-btn"
|
|
|
|
| 884 |
sessionId={sessionId}
|
| 885 |
authUser={authUser}
|
| 886 |
onUsarModeloEmAvaliacao={onUsarModeloEmAvaliacao}
|
| 887 |
+
onEditarModeloEmElaboracao={onEditarModeloEmElaboracao}
|
| 888 |
+
onAbrirModeloNoRepositorio={onAbrirModeloNoRepositorio}
|
| 889 |
+
routeRequest={modelosRouteRequest}
|
| 890 |
+
onRouteChange={onModelosRouteChange}
|
| 891 |
+
onModoImersivoChange={setModelosModoImersivo}
|
| 892 |
/>
|
| 893 |
</div>
|
| 894 |
|
| 895 |
<div className="tab-pane" hidden={activeTab !== 'Elaboração/Ediç��o'}>
|
| 896 |
+
<ElaboracaoTab
|
| 897 |
+
sessionId={sessionId}
|
| 898 |
+
authUser={authUser}
|
| 899 |
+
quickLoadRequest={elaboracaoQuickLoad}
|
| 900 |
+
onRouteChange={onElaboracaoRouteChange}
|
| 901 |
+
/>
|
| 902 |
</div>
|
| 903 |
|
| 904 |
<div className="tab-pane" hidden={activeTab !== 'Trabalhos Técnicos'}>
|
|
|
|
| 906 |
sessionId={sessionId}
|
| 907 |
onAbrirModeloNoRepositorio={onAbrirModeloNoRepositorio}
|
| 908 |
quickOpenRequest={trabalhoTecnicoQuickOpen}
|
| 909 |
+
routeRequest={trabalhosRouteRequest}
|
| 910 |
+
onRouteChange={onTrabalhosRouteChange}
|
| 911 |
onVoltarAoMapaPesquisa={onVoltarAoMapaPesquisa}
|
| 912 |
+
onModoImersivoChange={setTrabalhosModoImersivo}
|
| 913 |
/>
|
| 914 |
</div>
|
| 915 |
|
| 916 |
<div className="tab-pane" hidden={activeTab !== 'Avaliação'}>
|
| 917 |
+
<AvaliacaoTab
|
| 918 |
+
sessionId={sessionId}
|
| 919 |
+
quickLoadRequest={avaliacaoQuickLoad}
|
| 920 |
+
onRouteChange={onAvaliacaoRouteChange}
|
| 921 |
+
/>
|
| 922 |
</div>
|
| 923 |
</>
|
| 924 |
)
|
frontend/src/api.js
CHANGED
|
@@ -174,6 +174,7 @@ export const api = {
|
|
| 174 |
|
| 175 |
pesquisaAdminConfig: () => getJson('/api/pesquisa/admin-config'),
|
| 176 |
pesquisaAdminConfigSalvar: (campos = {}) => postJson('/api/pesquisa/admin-config', { campos }),
|
|
|
|
| 177 |
|
| 178 |
pesquisarModelos(filtros = {}) {
|
| 179 |
const params = new URLSearchParams()
|
|
@@ -355,6 +356,11 @@ export const api = {
|
|
| 355 |
session_id: sessionId,
|
| 356 |
trabalhos_tecnicos_modelos_modo: trabalhosTecnicosModelosModo,
|
| 357 |
}),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
evaluationContextViz: (sessionId) => postJson('/api/visualizacao/evaluation/context', { session_id: sessionId }),
|
| 359 |
updateVisualizacaoMap: (sessionId, variavelMapa, trabalhosTecnicosModelosModo = 'selecionados_e_outras_versoes') => postJson('/api/visualizacao/map/update', {
|
| 360 |
session_id: sessionId,
|
|
|
|
| 174 |
|
| 175 |
pesquisaAdminConfig: () => getJson('/api/pesquisa/admin-config'),
|
| 176 |
pesquisaAdminConfigSalvar: (campos = {}) => postJson('/api/pesquisa/admin-config', { campos }),
|
| 177 |
+
pesquisarLogradourosEixos: (limite = 2000) => getJson(`/api/pesquisa/logradouros-eixos?limite=${encodeURIComponent(String(limite))}`),
|
| 178 |
|
| 179 |
pesquisarModelos(filtros = {}) {
|
| 180 |
const params = new URLSearchParams()
|
|
|
|
| 356 |
session_id: sessionId,
|
| 357 |
trabalhos_tecnicos_modelos_modo: trabalhosTecnicosModelosModo,
|
| 358 |
}),
|
| 359 |
+
visualizacaoSection: (sessionId, secao, trabalhosTecnicosModelosModo = 'selecionados_e_outras_versoes') => postJson('/api/visualizacao/section', {
|
| 360 |
+
session_id: sessionId,
|
| 361 |
+
secao,
|
| 362 |
+
trabalhos_tecnicos_modelos_modo: trabalhosTecnicosModelosModo,
|
| 363 |
+
}),
|
| 364 |
evaluationContextViz: (sessionId) => postJson('/api/visualizacao/evaluation/context', { session_id: sessionId }),
|
| 365 |
updateVisualizacaoMap: (sessionId, variavelMapa, trabalhosTecnicosModelosModo = 'selecionados_e_outras_versoes') => postJson('/api/visualizacao/map/update', {
|
| 366 |
session_id: sessionId,
|
frontend/src/components/AvaliacaoTab.jsx
CHANGED
|
@@ -1,8 +1,10 @@
|
|
| 1 |
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
| 2 |
import { api, downloadBlob } from '../api'
|
| 3 |
import { buildCsvBlob } from '../csv'
|
|
|
|
| 4 |
import LoadingOverlay from './LoadingOverlay'
|
| 5 |
import MapFrame from './MapFrame'
|
|
|
|
| 6 |
import SinglePillAutocomplete from './SinglePillAutocomplete'
|
| 7 |
import TruncatedCellContent from './TruncatedCellContent'
|
| 8 |
|
|
@@ -496,7 +498,7 @@ function obterCoordenadasResolvidas(localizacao) {
|
|
| 496 |
return { lat, lon }
|
| 497 |
}
|
| 498 |
|
| 499 |
-
export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
|
| 500 |
const [loading, setLoading] = useState(false)
|
| 501 |
const [error, setError] = useState('')
|
| 502 |
const [status, setStatus] = useState('')
|
|
@@ -832,6 +834,9 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
|
|
| 832 |
const nomeModelo = uploadResp?.nome_modelo || arquivoUpload.name || ''
|
| 833 |
const contextoResp = await api.evaluationContextViz(sessionId)
|
| 834 |
aplicarRespostaExibicao(contextoResp, nomeModelo)
|
|
|
|
|
|
|
|
|
|
| 835 |
})
|
| 836 |
}
|
| 837 |
|
|
@@ -858,6 +863,12 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
|
|
| 858 |
const contextoResp = await api.evaluationContextViz(sessionId)
|
| 859 |
aplicarRespostaExibicao(contextoResp, nomeModelo)
|
| 860 |
setUploadedFile(null)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 861 |
})
|
| 862 |
}
|
| 863 |
|
|
@@ -1189,6 +1200,9 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
|
|
| 1189 |
? (avaliacoesCards.findIndex((item) => item.id === baseCard?.id) + 1)
|
| 1190 |
: 0
|
| 1191 |
const avaliandoLocalizacaoAtiva = Boolean(obterCoordenadasResolvidas(avaliandoLocalizacaoResolvida))
|
|
|
|
|
|
|
|
|
|
| 1192 |
|
| 1193 |
return (
|
| 1194 |
<div className="tab-content">
|
|
@@ -1220,11 +1234,17 @@ export default function AvaliacaoTab({ sessionId, quickLoadRequest = null }) {
|
|
| 1220 |
<button
|
| 1221 |
type="button"
|
| 1222 |
className="model-source-back-btn"
|
| 1223 |
-
onClick={() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1224 |
disabled={loading}
|
| 1225 |
>
|
| 1226 |
Voltar
|
| 1227 |
</button>
|
|
|
|
| 1228 |
</div>
|
| 1229 |
|
| 1230 |
{modeloLoadSource === 'repo' ? (
|
|
|
|
| 1 |
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
| 2 |
import { api, downloadBlob } from '../api'
|
| 3 |
import { buildCsvBlob } from '../csv'
|
| 4 |
+
import { buildAvaliacaoModeloLink } from '../deepLinks'
|
| 5 |
import LoadingOverlay from './LoadingOverlay'
|
| 6 |
import MapFrame from './MapFrame'
|
| 7 |
+
import ShareLinkButton from './ShareLinkButton'
|
| 8 |
import SinglePillAutocomplete from './SinglePillAutocomplete'
|
| 9 |
import TruncatedCellContent from './TruncatedCellContent'
|
| 10 |
|
|
|
|
| 498 |
return { lat, lon }
|
| 499 |
}
|
| 500 |
|
| 501 |
+
export default function AvaliacaoTab({ sessionId, quickLoadRequest = null, onRouteChange = null }) {
|
| 502 |
const [loading, setLoading] = useState(false)
|
| 503 |
const [error, setError] = useState('')
|
| 504 |
const [status, setStatus] = useState('')
|
|
|
|
| 834 |
const nomeModelo = uploadResp?.nome_modelo || arquivoUpload.name || ''
|
| 835 |
const contextoResp = await api.evaluationContextViz(sessionId)
|
| 836 |
aplicarRespostaExibicao(contextoResp, nomeModelo)
|
| 837 |
+
if (typeof onRouteChange === 'function') {
|
| 838 |
+
onRouteChange({ tab: 'avaliacao' })
|
| 839 |
+
}
|
| 840 |
})
|
| 841 |
}
|
| 842 |
|
|
|
|
| 863 |
const contextoResp = await api.evaluationContextViz(sessionId)
|
| 864 |
aplicarRespostaExibicao(contextoResp, nomeModelo)
|
| 865 |
setUploadedFile(null)
|
| 866 |
+
if (typeof onRouteChange === 'function') {
|
| 867 |
+
onRouteChange({
|
| 868 |
+
tab: 'avaliacao',
|
| 869 |
+
modeloId: modeloIdUsado,
|
| 870 |
+
})
|
| 871 |
+
}
|
| 872 |
})
|
| 873 |
}
|
| 874 |
|
|
|
|
| 1200 |
? (avaliacoesCards.findIndex((item) => item.id === baseCard?.id) + 1)
|
| 1201 |
: 0
|
| 1202 |
const avaliandoLocalizacaoAtiva = Boolean(obterCoordenadasResolvidas(avaliandoLocalizacaoResolvida))
|
| 1203 |
+
const avaliacaoShareHref = repoModeloSelecionado
|
| 1204 |
+
? buildAvaliacaoModeloLink(repoModeloSelecionado)
|
| 1205 |
+
: ''
|
| 1206 |
|
| 1207 |
return (
|
| 1208 |
<div className="tab-content">
|
|
|
|
| 1234 |
<button
|
| 1235 |
type="button"
|
| 1236 |
className="model-source-back-btn"
|
| 1237 |
+
onClick={() => {
|
| 1238 |
+
setModeloLoadSource('')
|
| 1239 |
+
if (typeof onRouteChange === 'function') {
|
| 1240 |
+
onRouteChange({ tab: 'avaliacao' })
|
| 1241 |
+
}
|
| 1242 |
+
}}
|
| 1243 |
disabled={loading}
|
| 1244 |
>
|
| 1245 |
Voltar
|
| 1246 |
</button>
|
| 1247 |
+
{modeloLoadSource === 'repo' && repoModeloSelecionado ? <ShareLinkButton href={avaliacaoShareHref} /> : null}
|
| 1248 |
</div>
|
| 1249 |
|
| 1250 |
{modeloLoadSource === 'repo' ? (
|
frontend/src/components/ElaboracaoTab.jsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
| 2 |
import { api, downloadBlob } from '../api'
|
| 3 |
import { tableToCsvBlob } from '../csv'
|
|
|
|
| 4 |
import Plotly from 'plotly.js-dist-min'
|
| 5 |
import DataTable from './DataTable'
|
| 6 |
import EquationFormatsPanel from './EquationFormatsPanel'
|
|
@@ -8,6 +9,7 @@ import LoadingOverlay from './LoadingOverlay'
|
|
| 8 |
import MapFrame from './MapFrame'
|
| 9 |
import PlotFigure from './PlotFigure'
|
| 10 |
import SectionBlock from './SectionBlock'
|
|
|
|
| 11 |
import SinglePillAutocomplete from './SinglePillAutocomplete'
|
| 12 |
import TruncatedCellContent from './TruncatedCellContent'
|
| 13 |
|
|
@@ -678,6 +680,16 @@ function buildScatterPanels(figure, options = {}) {
|
|
| 678 |
})
|
| 679 |
}
|
| 680 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 681 |
function formatConselhoRegistro(elaborador) {
|
| 682 |
if (!elaborador) return ''
|
| 683 |
const conselho = String(elaborador.conselho || '').trim()
|
|
@@ -847,7 +859,7 @@ function DiagnosticPngCard({ title, pngPayload, alt }) {
|
|
| 847 |
)
|
| 848 |
}
|
| 849 |
|
| 850 |
-
export default function ElaboracaoTab({ sessionId, authUser }) {
|
| 851 |
const [loading, setLoading] = useState(false)
|
| 852 |
const [downloadingAssets, setDownloadingAssets] = useState(false)
|
| 853 |
const [error, setError] = useState('')
|
|
@@ -942,10 +954,13 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 942 |
const [secao13InterativoEixoYResiduo, setSecao13InterativoEixoYResiduo] = useState('residuo_pad')
|
| 943 |
const [secao13InterativoEixoYColuna, setSecao13InterativoEixoYColuna] = useState('')
|
| 944 |
const [secao10InterativoFigura, setSecao10InterativoFigura] = useState(null)
|
|
|
|
| 945 |
const [secao10InterativoSelecionado, setSecao10InterativoSelecionado] = useState('none')
|
| 946 |
const [secao13InterativoFigura, setSecao13InterativoFigura] = useState(null)
|
|
|
|
| 947 |
const [secao13InterativoSelecionado, setSecao13InterativoSelecionado] = useState('none')
|
| 948 |
const [secao15InterativoFigura, setSecao15InterativoFigura] = useState(null)
|
|
|
|
| 949 |
const [secao15InterativoSelecionado, setSecao15InterativoSelecionado] = useState('none')
|
| 950 |
|
| 951 |
const [filtros, setFiltros] = useState(defaultFiltros())
|
|
@@ -989,11 +1004,15 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 989 |
const deleteConfirmTimersRef = useRef({})
|
| 990 |
const uploadInputRef = useRef(null)
|
| 991 |
const elaboracaoRootRef = useRef(null)
|
|
|
|
| 992 |
const [disabledHint, setDisabledHint] = useState(null)
|
| 993 |
const [sectionsMountKey, setSectionsMountKey] = useState(0)
|
| 994 |
const [renderedSectionSteps, setRenderedSectionSteps] = useState(() => ['1'])
|
| 995 |
const [visibleSectionSteps, setVisibleSectionSteps] = useState(() => ['1'])
|
| 996 |
const visibleSectionStepsRef = useRef(new Set(['1']))
|
|
|
|
|
|
|
|
|
|
| 997 |
const [sideNavDynamicStyle, setSideNavDynamicStyle] = useState({})
|
| 998 |
|
| 999 |
const mapaChoices = useMemo(() => [MAPA_VARIAVEL_PADRAO, ...colunasNumericas], [colunasNumericas])
|
|
@@ -1334,6 +1353,18 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 1334 |
}),
|
| 1335 |
[selection?.grafico_dispersao, colunaY],
|
| 1336 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1337 |
const graficosSecao9Interativo = useMemo(
|
| 1338 |
() => buildScatterPanels(secao10InterativoFigura, {
|
| 1339 |
singleLabel: 'Dispersão',
|
|
@@ -1343,6 +1374,19 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 1343 |
}),
|
| 1344 |
[secao10InterativoFigura, colunaY],
|
| 1345 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1346 |
const secao10InterativoOpcoes = useMemo(() => {
|
| 1347 |
const labels = graficosSecao9Interativo.length > 0
|
| 1348 |
? graficosSecao9Interativo.map((item) => String(item.label || '').trim()).filter(Boolean)
|
|
@@ -1383,6 +1427,18 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 1383 |
}),
|
| 1384 |
[fit?.grafico_dispersao_modelo, yLabelSecao13],
|
| 1385 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1386 |
const secao13ModoPng = useMemo(
|
| 1387 |
() => String(fit?.grafico_dispersao_modelo_modo || '') === 'png',
|
| 1388 |
[fit?.grafico_dispersao_modelo_modo],
|
|
@@ -1409,6 +1465,19 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 1409 |
}),
|
| 1410 |
[secao13InterativoFigura, yLabelSecao13Interativo],
|
| 1411 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1412 |
const secao13InterativoOpcoes = useMemo(() => {
|
| 1413 |
const labels = graficosSecao12Interativo.map((item) => String(item.label || '').trim()).filter(Boolean)
|
| 1414 |
return Array.from(new Set(labels))
|
|
@@ -1790,8 +1859,9 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 1790 |
if (!fit || !secao15DiagnosticoPng) {
|
| 1791 |
if (secao15InterativoSelecionado !== 'none') setSecao15InterativoSelecionado('none')
|
| 1792 |
if (secao15InterativoFigura) setSecao15InterativoFigura(null)
|
|
|
|
| 1793 |
}
|
| 1794 |
-
}, [fit, secao15DiagnosticoPng, secao15InterativoSelecionado, secao15InterativoFigura])
|
| 1795 |
|
| 1796 |
useEffect(() => {
|
| 1797 |
if (!sessionId || tipoFonteDados === 'dai') return undefined
|
|
@@ -2125,8 +2195,10 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 2125 |
setSection6EditOpen(true)
|
| 2126 |
setFit(null)
|
| 2127 |
setSecao10InterativoFigura(null)
|
|
|
|
| 2128 |
setSecao10InterativoSelecionado('none')
|
| 2129 |
setSecao13InterativoFigura(null)
|
|
|
|
| 2130 |
setSecao13InterativoSelecionado('none')
|
| 2131 |
setFiltros(defaultFiltros())
|
| 2132 |
setOutliersTexto('')
|
|
@@ -2164,6 +2236,7 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 2164 |
function applySelectionResponse(resp) {
|
| 2165 |
setSelection(resp)
|
| 2166 |
setSecao10InterativoFigura(null)
|
|
|
|
| 2167 |
setSecao10InterativoSelecionado('none')
|
| 2168 |
setSection6EditOpen(false)
|
| 2169 |
setSection11LocksOpen(false)
|
|
@@ -2225,8 +2298,10 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 2225 |
function applyFitResponse(resp, origemMeta = null) {
|
| 2226 |
setFit(resp)
|
| 2227 |
setSecao13InterativoFigura(null)
|
|
|
|
| 2228 |
setSecao13InterativoSelecionado('none')
|
| 2229 |
setSecao15InterativoFigura(null)
|
|
|
|
| 2230 |
setSecao15InterativoSelecionado('none')
|
| 2231 |
setDispersaoEixoX('transformado')
|
| 2232 |
setDispersaoEixoYTipo('y_transformado')
|
|
@@ -2335,8 +2410,10 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 2335 |
setGrauF(0)
|
| 2336 |
setFit(null)
|
| 2337 |
setSecao10InterativoFigura(null)
|
|
|
|
| 2338 |
setSecao10InterativoSelecionado('none')
|
| 2339 |
setSecao13InterativoFigura(null)
|
|
|
|
| 2340 |
setSecao13InterativoSelecionado('none')
|
| 2341 |
setSelectionAppliedSnapshot(buildSelectionSnapshot())
|
| 2342 |
setColunasX([])
|
|
@@ -2413,8 +2490,16 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 2413 |
})
|
| 2414 |
}
|
| 2415 |
|
| 2416 |
-
async function onCarregarModeloRepositorio() {
|
| 2417 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2418 |
setModeloLoadSource('repo')
|
| 2419 |
setImportacaoErro('')
|
| 2420 |
setArquivoCarregadoInfo(null)
|
|
@@ -2432,14 +2517,32 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 2432 |
setRequiresSheet(false)
|
| 2433 |
setSheetOptions([])
|
| 2434 |
setTipoFonteDados('dai')
|
| 2435 |
-
const resp = await api.elaboracaoRepositorioCarregar(sessionId,
|
| 2436 |
aplicarRespostaCarregamento(resp, 'dai', { source: 'repo' })
|
|
|
|
| 2437 |
setUploadedFile(null)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2438 |
}, {
|
| 2439 |
onError: (err) => setImportacaoErro(err?.message || 'Falha ao carregar modelo do repositório.'),
|
| 2440 |
})
|
| 2441 |
}
|
| 2442 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2443 |
function onUploadInputChange(event) {
|
| 2444 |
const input = event.target
|
| 2445 |
const file = input.files?.[0] ?? null
|
|
@@ -2594,8 +2697,10 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 2594 |
setSelection(null)
|
| 2595 |
setFit(null)
|
| 2596 |
setSecao10InterativoFigura(null)
|
|
|
|
| 2597 |
setSecao10InterativoSelecionado('none')
|
| 2598 |
setSecao13InterativoFigura(null)
|
|
|
|
| 2599 |
setSecao13InterativoSelecionado('none')
|
| 2600 |
setTransformacaoY('(x)')
|
| 2601 |
setTransformacoesX({})
|
|
@@ -2761,6 +2866,7 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 2761 |
setSection6EditOpen(false)
|
| 2762 |
setFit(null)
|
| 2763 |
setSecao13InterativoFigura(null)
|
|
|
|
| 2764 |
setSecao13InterativoSelecionado('none')
|
| 2765 |
setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
|
| 2766 |
setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
|
|
@@ -2867,6 +2973,7 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 2867 |
setFit((prev) => ({
|
| 2868 |
...prev,
|
| 2869 |
grafico_dispersao_modelo: resp.grafico,
|
|
|
|
| 2870 |
grafico_dispersao_modelo_modo: resp.modo || (resp.grafico ? 'interativo' : ''),
|
| 2871 |
grafico_dispersao_modelo_png: resp.grafico_png || null,
|
| 2872 |
grafico_dispersao_modelo_total_pontos: resp.total_pontos,
|
|
@@ -2901,6 +3008,7 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 2901 |
figuraInterativa = respInterativo?.grafico || null
|
| 2902 |
}
|
| 2903 |
setSecao13InterativoFigura(figuraInterativa)
|
|
|
|
| 2904 |
const yLabelInterativo = getDispersaoYLabel(eixoYTipo, eixoYResiduo, eixoYColuna, colunaYComRotulo)
|
| 2905 |
const paineis = buildScatterPanels(figuraInterativa, {
|
| 2906 |
singleLabel: 'Dispersão do modelo',
|
|
@@ -3053,6 +3161,7 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 3053 |
setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
|
| 3054 |
setFit(null)
|
| 3055 |
setSecao13InterativoFigura(null)
|
|
|
|
| 3056 |
setSecao13InterativoSelecionado('none')
|
| 3057 |
setTransformacoesAplicadas(null)
|
| 3058 |
setOrigemTransformacoes(null)
|
|
@@ -3100,8 +3209,10 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 3100 |
setSection6EditOpen(true)
|
| 3101 |
setFit(null)
|
| 3102 |
setSecao10InterativoFigura(null)
|
|
|
|
| 3103 |
setSecao10InterativoSelecionado('none')
|
| 3104 |
setSecao13InterativoFigura(null)
|
|
|
|
| 3105 |
setSecao13InterativoSelecionado('none')
|
| 3106 |
setTransformacoesAplicadas(null)
|
| 3107 |
setOrigemTransformacoes(null)
|
|
@@ -3639,6 +3750,84 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 3639 |
}
|
| 3640 |
}
|
| 3641 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3642 |
async function onChangeSecao10InterativoSelecionado(value) {
|
| 3643 |
const nextValue = String(value || 'none').trim() || 'none'
|
| 3644 |
setSecao10InterativoSelecionado(nextValue)
|
|
@@ -3650,6 +3839,7 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 3650 |
await withBusy(async () => {
|
| 3651 |
const resp = await api.getDispersaoInterativo(sessionId, 'secao10')
|
| 3652 |
setSecao10InterativoFigura(resp?.grafico || null)
|
|
|
|
| 3653 |
})
|
| 3654 |
}
|
| 3655 |
|
|
@@ -3668,6 +3858,7 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 3668 |
setSecao15InterativoSelecionado(nextValue)
|
| 3669 |
if (nextValue === 'none') {
|
| 3670 |
setSecao15InterativoFigura(null)
|
|
|
|
| 3671 |
return
|
| 3672 |
}
|
| 3673 |
if (!sessionId) return
|
|
@@ -3675,6 +3866,7 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 3675 |
await withBusy(async () => {
|
| 3676 |
const resp = await api.getDiagnosticoInterativo(sessionId, nextValue)
|
| 3677 |
setSecao15InterativoFigura(resp?.payload || null)
|
|
|
|
| 3678 |
})
|
| 3679 |
}
|
| 3680 |
|
|
@@ -3786,21 +3978,25 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 3786 |
</button>
|
| 3787 |
</div>
|
| 3788 |
) : (
|
| 3789 |
-
|
| 3790 |
-
|
| 3791 |
-
|
| 3792 |
-
|
| 3793 |
-
|
| 3794 |
-
|
| 3795 |
-
|
| 3796 |
-
|
| 3797 |
-
|
| 3798 |
-
|
| 3799 |
-
|
| 3800 |
-
|
| 3801 |
-
|
| 3802 |
-
|
| 3803 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3804 |
|
| 3805 |
{modeloLoadSource === 'repo' ? (
|
| 3806 |
<div className="row upload-repo-row">
|
|
@@ -3816,15 +4012,15 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 3816 |
disabled={loading || repoModelosLoading || repoModeloOptions.length === 0}
|
| 3817 |
onOpenChange={setRepoModeloDropdownOpen}
|
| 3818 |
/>
|
| 3819 |
-
|
| 3820 |
-
|
| 3821 |
-
|
| 3822 |
-
|
| 3823 |
-
|
| 3824 |
-
|
| 3825 |
-
|
| 3826 |
-
|
| 3827 |
-
|
| 3828 |
{repoFonteModelos ? <div className="section1-empty-hint">{repoFonteModelos}</div> : null}
|
| 3829 |
</div>
|
| 3830 |
) : null}
|
|
@@ -4755,8 +4951,11 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 4755 |
<PlotFigure
|
| 4756 |
key={`s9-interativo-plot-${secao10InterativoAtual.id}`}
|
| 4757 |
figure={secao10InterativoAtual.figure}
|
|
|
|
|
|
|
| 4758 |
title={secao10InterativoAtual.title}
|
| 4759 |
subtitle={secao10InterativoAtual.subtitle}
|
|
|
|
| 4760 |
forceHideLegend
|
| 4761 |
className="plot-stretch"
|
| 4762 |
lazy
|
|
@@ -4809,8 +5008,11 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 4809 |
<PlotFigure
|
| 4810 |
key={`s9-plot-${item.id}`}
|
| 4811 |
figure={item.figure}
|
|
|
|
|
|
|
| 4812 |
title={item.title}
|
| 4813 |
subtitle={item.subtitle}
|
|
|
|
| 4814 |
forceHideLegend
|
| 4815 |
className="plot-stretch"
|
| 4816 |
lazy
|
|
@@ -5286,8 +5488,11 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 5286 |
<PlotFigure
|
| 5287 |
key={`s12-interativo-plot-${secao13InterativoAtual.id}`}
|
| 5288 |
figure={secao13InterativoAtual.figure}
|
|
|
|
|
|
|
| 5289 |
title={secao13InterativoAtual.title}
|
| 5290 |
subtitle={secao13InterativoAtual.subtitle}
|
|
|
|
| 5291 |
forceHideLegend
|
| 5292 |
className="plot-stretch"
|
| 5293 |
lazy
|
|
@@ -5340,8 +5545,11 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 5340 |
<PlotFigure
|
| 5341 |
key={`s12-plot-${item.id}`}
|
| 5342 |
figure={item.figure}
|
|
|
|
|
|
|
| 5343 |
title={item.title}
|
| 5344 |
subtitle={item.subtitle}
|
|
|
|
| 5345 |
forceHideLegend
|
| 5346 |
className="plot-stretch"
|
| 5347 |
lazy
|
|
@@ -5510,14 +5718,14 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 5510 |
</div>
|
| 5511 |
) : (
|
| 5512 |
<div className="plot-grid-2-fixed">
|
| 5513 |
-
<PlotFigure figure={fit.grafico_obs_calc} title="Obs x Calc" />
|
| 5514 |
-
<PlotFigure figure={fit.grafico_residuos} title="Resíduos" />
|
| 5515 |
-
<PlotFigure figure={fit.grafico_histograma} title="Histograma" />
|
| 5516 |
-
<PlotFigure figure={fit.grafico_cook} title="Cook" forceHideLegend />
|
| 5517 |
</div>
|
| 5518 |
)}
|
| 5519 |
<div className="plot-full-width">
|
| 5520 |
-
<PlotFigure figure={fit.grafico_correlacao} title="Matriz de correlação" className="plot-correlation-card" />
|
| 5521 |
</div>
|
| 5522 |
{secao15DiagnosticoPng ? (
|
| 5523 |
<>
|
|
@@ -5558,7 +5766,10 @@ export default function ElaboracaoTab({ sessionId, authUser }) {
|
|
| 5558 |
<PlotFigure
|
| 5559 |
key={`s15-interativo-${secao15InterativoSelecionado}`}
|
| 5560 |
figure={secao15InterativoFigura}
|
|
|
|
|
|
|
| 5561 |
title={secao15InterativoLabel}
|
|
|
|
| 5562 |
forceHideLegend={secao15InterativoSelecionado === 'cook'}
|
| 5563 |
className="plot-stretch"
|
| 5564 |
lazy
|
|
|
|
| 1 |
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
| 2 |
import { api, downloadBlob } from '../api'
|
| 3 |
import { tableToCsvBlob } from '../csv'
|
| 4 |
+
import { buildElaboracaoModeloLink } from '../deepLinks'
|
| 5 |
import Plotly from 'plotly.js-dist-min'
|
| 6 |
import DataTable from './DataTable'
|
| 7 |
import EquationFormatsPanel from './EquationFormatsPanel'
|
|
|
|
| 9 |
import MapFrame from './MapFrame'
|
| 10 |
import PlotFigure from './PlotFigure'
|
| 11 |
import SectionBlock from './SectionBlock'
|
| 12 |
+
import ShareLinkButton from './ShareLinkButton'
|
| 13 |
import SinglePillAutocomplete from './SinglePillAutocomplete'
|
| 14 |
import TruncatedCellContent from './TruncatedCellContent'
|
| 15 |
|
|
|
|
| 680 |
})
|
| 681 |
}
|
| 682 |
|
| 683 |
+
function buildScatterPanelFigureMap(panels) {
|
| 684 |
+
const mapa = new Map()
|
| 685 |
+
;(panels || []).forEach((item) => {
|
| 686 |
+
const label = String(item?.label || '').trim()
|
| 687 |
+
if (!label || !item?.figure) return
|
| 688 |
+
mapa.set(label, item.figure)
|
| 689 |
+
})
|
| 690 |
+
return mapa
|
| 691 |
+
}
|
| 692 |
+
|
| 693 |
function formatConselhoRegistro(elaborador) {
|
| 694 |
if (!elaborador) return ''
|
| 695 |
const conselho = String(elaborador.conselho || '').trim()
|
|
|
|
| 859 |
)
|
| 860 |
}
|
| 861 |
|
| 862 |
+
export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest = null, onRouteChange = null }) {
|
| 863 |
const [loading, setLoading] = useState(false)
|
| 864 |
const [downloadingAssets, setDownloadingAssets] = useState(false)
|
| 865 |
const [error, setError] = useState('')
|
|
|
|
| 954 |
const [secao13InterativoEixoYResiduo, setSecao13InterativoEixoYResiduo] = useState('residuo_pad')
|
| 955 |
const [secao13InterativoEixoYColuna, setSecao13InterativoEixoYColuna] = useState('')
|
| 956 |
const [secao10InterativoFigura, setSecao10InterativoFigura] = useState(null)
|
| 957 |
+
const [secao10InterativoFiguraComIndices, setSecao10InterativoFiguraComIndices] = useState(null)
|
| 958 |
const [secao10InterativoSelecionado, setSecao10InterativoSelecionado] = useState('none')
|
| 959 |
const [secao13InterativoFigura, setSecao13InterativoFigura] = useState(null)
|
| 960 |
+
const [secao13InterativoFiguraComIndices, setSecao13InterativoFiguraComIndices] = useState(null)
|
| 961 |
const [secao13InterativoSelecionado, setSecao13InterativoSelecionado] = useState('none')
|
| 962 |
const [secao15InterativoFigura, setSecao15InterativoFigura] = useState(null)
|
| 963 |
+
const [secao15InterativoFiguraComIndices, setSecao15InterativoFiguraComIndices] = useState(null)
|
| 964 |
const [secao15InterativoSelecionado, setSecao15InterativoSelecionado] = useState('none')
|
| 965 |
|
| 966 |
const [filtros, setFiltros] = useState(defaultFiltros())
|
|
|
|
| 1004 |
const deleteConfirmTimersRef = useRef({})
|
| 1005 |
const uploadInputRef = useRef(null)
|
| 1006 |
const elaboracaoRootRef = useRef(null)
|
| 1007 |
+
const quickLoadHandledRef = useRef('')
|
| 1008 |
const [disabledHint, setDisabledHint] = useState(null)
|
| 1009 |
const [sectionsMountKey, setSectionsMountKey] = useState(0)
|
| 1010 |
const [renderedSectionSteps, setRenderedSectionSteps] = useState(() => ['1'])
|
| 1011 |
const [visibleSectionSteps, setVisibleSectionSteps] = useState(() => ['1'])
|
| 1012 |
const visibleSectionStepsRef = useRef(new Set(['1']))
|
| 1013 |
+
const elaboracaoShareHref = repoModeloSelecionado
|
| 1014 |
+
? buildElaboracaoModeloLink(repoModeloSelecionado)
|
| 1015 |
+
: ''
|
| 1016 |
const [sideNavDynamicStyle, setSideNavDynamicStyle] = useState({})
|
| 1017 |
|
| 1018 |
const mapaChoices = useMemo(() => [MAPA_VARIAVEL_PADRAO, ...colunasNumericas], [colunasNumericas])
|
|
|
|
| 1353 |
}),
|
| 1354 |
[selection?.grafico_dispersao, colunaY],
|
| 1355 |
)
|
| 1356 |
+
const graficosSecao9ComIndices = useMemo(
|
| 1357 |
+
() => buildScatterPanels(selection?.grafico_dispersao_com_indices, {
|
| 1358 |
+
singleLabel: 'Dispersão',
|
| 1359 |
+
height: 360,
|
| 1360 |
+
yLabel: colunaY || 'Y',
|
| 1361 |
+
}),
|
| 1362 |
+
[selection?.grafico_dispersao_com_indices, colunaY],
|
| 1363 |
+
)
|
| 1364 |
+
const graficosSecao9ComIndicesMap = useMemo(
|
| 1365 |
+
() => buildScatterPanelFigureMap(graficosSecao9ComIndices),
|
| 1366 |
+
[graficosSecao9ComIndices],
|
| 1367 |
+
)
|
| 1368 |
const graficosSecao9Interativo = useMemo(
|
| 1369 |
() => buildScatterPanels(secao10InterativoFigura, {
|
| 1370 |
singleLabel: 'Dispersão',
|
|
|
|
| 1374 |
}),
|
| 1375 |
[secao10InterativoFigura, colunaY],
|
| 1376 |
)
|
| 1377 |
+
const graficosSecao9InterativoComIndices = useMemo(
|
| 1378 |
+
() => buildScatterPanels(secao10InterativoFiguraComIndices, {
|
| 1379 |
+
singleLabel: 'Dispersão',
|
| 1380 |
+
height: 360,
|
| 1381 |
+
yLabel: colunaY || 'Y',
|
| 1382 |
+
useScatterGlMarkers: true,
|
| 1383 |
+
}),
|
| 1384 |
+
[secao10InterativoFiguraComIndices, colunaY],
|
| 1385 |
+
)
|
| 1386 |
+
const graficosSecao9InterativoComIndicesMap = useMemo(
|
| 1387 |
+
() => buildScatterPanelFigureMap(graficosSecao9InterativoComIndices),
|
| 1388 |
+
[graficosSecao9InterativoComIndices],
|
| 1389 |
+
)
|
| 1390 |
const secao10InterativoOpcoes = useMemo(() => {
|
| 1391 |
const labels = graficosSecao9Interativo.length > 0
|
| 1392 |
? graficosSecao9Interativo.map((item) => String(item.label || '').trim()).filter(Boolean)
|
|
|
|
| 1427 |
}),
|
| 1428 |
[fit?.grafico_dispersao_modelo, yLabelSecao13],
|
| 1429 |
)
|
| 1430 |
+
const graficosSecao12ComIndices = useMemo(
|
| 1431 |
+
() => buildScatterPanels(fit?.grafico_dispersao_modelo_com_indices, {
|
| 1432 |
+
singleLabel: 'Dispersão do modelo',
|
| 1433 |
+
height: 360,
|
| 1434 |
+
yLabel: yLabelSecao13,
|
| 1435 |
+
}),
|
| 1436 |
+
[fit?.grafico_dispersao_modelo_com_indices, yLabelSecao13],
|
| 1437 |
+
)
|
| 1438 |
+
const graficosSecao12ComIndicesMap = useMemo(
|
| 1439 |
+
() => buildScatterPanelFigureMap(graficosSecao12ComIndices),
|
| 1440 |
+
[graficosSecao12ComIndices],
|
| 1441 |
+
)
|
| 1442 |
const secao13ModoPng = useMemo(
|
| 1443 |
() => String(fit?.grafico_dispersao_modelo_modo || '') === 'png',
|
| 1444 |
[fit?.grafico_dispersao_modelo_modo],
|
|
|
|
| 1465 |
}),
|
| 1466 |
[secao13InterativoFigura, yLabelSecao13Interativo],
|
| 1467 |
)
|
| 1468 |
+
const graficosSecao12InterativoComIndices = useMemo(
|
| 1469 |
+
() => buildScatterPanels(secao13InterativoFiguraComIndices, {
|
| 1470 |
+
singleLabel: 'Dispersão do modelo',
|
| 1471 |
+
height: 360,
|
| 1472 |
+
yLabel: yLabelSecao13Interativo,
|
| 1473 |
+
useScatterGlMarkers: true,
|
| 1474 |
+
}),
|
| 1475 |
+
[secao13InterativoFiguraComIndices, yLabelSecao13Interativo],
|
| 1476 |
+
)
|
| 1477 |
+
const graficosSecao12InterativoComIndicesMap = useMemo(
|
| 1478 |
+
() => buildScatterPanelFigureMap(graficosSecao12InterativoComIndices),
|
| 1479 |
+
[graficosSecao12InterativoComIndices],
|
| 1480 |
+
)
|
| 1481 |
const secao13InterativoOpcoes = useMemo(() => {
|
| 1482 |
const labels = graficosSecao12Interativo.map((item) => String(item.label || '').trim()).filter(Boolean)
|
| 1483 |
return Array.from(new Set(labels))
|
|
|
|
| 1859 |
if (!fit || !secao15DiagnosticoPng) {
|
| 1860 |
if (secao15InterativoSelecionado !== 'none') setSecao15InterativoSelecionado('none')
|
| 1861 |
if (secao15InterativoFigura) setSecao15InterativoFigura(null)
|
| 1862 |
+
if (secao15InterativoFiguraComIndices) setSecao15InterativoFiguraComIndices(null)
|
| 1863 |
}
|
| 1864 |
+
}, [fit, secao15DiagnosticoPng, secao15InterativoSelecionado, secao15InterativoFigura, secao15InterativoFiguraComIndices])
|
| 1865 |
|
| 1866 |
useEffect(() => {
|
| 1867 |
if (!sessionId || tipoFonteDados === 'dai') return undefined
|
|
|
|
| 2195 |
setSection6EditOpen(true)
|
| 2196 |
setFit(null)
|
| 2197 |
setSecao10InterativoFigura(null)
|
| 2198 |
+
setSecao10InterativoFiguraComIndices(null)
|
| 2199 |
setSecao10InterativoSelecionado('none')
|
| 2200 |
setSecao13InterativoFigura(null)
|
| 2201 |
+
setSecao13InterativoFiguraComIndices(null)
|
| 2202 |
setSecao13InterativoSelecionado('none')
|
| 2203 |
setFiltros(defaultFiltros())
|
| 2204 |
setOutliersTexto('')
|
|
|
|
| 2236 |
function applySelectionResponse(resp) {
|
| 2237 |
setSelection(resp)
|
| 2238 |
setSecao10InterativoFigura(null)
|
| 2239 |
+
setSecao10InterativoFiguraComIndices(null)
|
| 2240 |
setSecao10InterativoSelecionado('none')
|
| 2241 |
setSection6EditOpen(false)
|
| 2242 |
setSection11LocksOpen(false)
|
|
|
|
| 2298 |
function applyFitResponse(resp, origemMeta = null) {
|
| 2299 |
setFit(resp)
|
| 2300 |
setSecao13InterativoFigura(null)
|
| 2301 |
+
setSecao13InterativoFiguraComIndices(null)
|
| 2302 |
setSecao13InterativoSelecionado('none')
|
| 2303 |
setSecao15InterativoFigura(null)
|
| 2304 |
+
setSecao15InterativoFiguraComIndices(null)
|
| 2305 |
setSecao15InterativoSelecionado('none')
|
| 2306 |
setDispersaoEixoX('transformado')
|
| 2307 |
setDispersaoEixoYTipo('y_transformado')
|
|
|
|
| 2410 |
setGrauF(0)
|
| 2411 |
setFit(null)
|
| 2412 |
setSecao10InterativoFigura(null)
|
| 2413 |
+
setSecao10InterativoFiguraComIndices(null)
|
| 2414 |
setSecao10InterativoSelecionado('none')
|
| 2415 |
setSecao13InterativoFigura(null)
|
| 2416 |
+
setSecao13InterativoFiguraComIndices(null)
|
| 2417 |
setSecao13InterativoSelecionado('none')
|
| 2418 |
setSelectionAppliedSnapshot(buildSelectionSnapshot())
|
| 2419 |
setColunasX([])
|
|
|
|
| 2490 |
})
|
| 2491 |
}
|
| 2492 |
|
| 2493 |
+
async function onCarregarModeloRepositorio(modeloIdOverride = '') {
|
| 2494 |
+
const overrideNormalizado = (
|
| 2495 |
+
modeloIdOverride
|
| 2496 |
+
&& typeof modeloIdOverride === 'object'
|
| 2497 |
+
&& typeof modeloIdOverride.preventDefault === 'function'
|
| 2498 |
+
)
|
| 2499 |
+
? ''
|
| 2500 |
+
: modeloIdOverride
|
| 2501 |
+
const modeloId = String(overrideNormalizado || repoModeloSelecionado || '').trim()
|
| 2502 |
+
if (!sessionId || !modeloId) return
|
| 2503 |
setModeloLoadSource('repo')
|
| 2504 |
setImportacaoErro('')
|
| 2505 |
setArquivoCarregadoInfo(null)
|
|
|
|
| 2517 |
setRequiresSheet(false)
|
| 2518 |
setSheetOptions([])
|
| 2519 |
setTipoFonteDados('dai')
|
| 2520 |
+
const resp = await api.elaboracaoRepositorioCarregar(sessionId, modeloId)
|
| 2521 |
aplicarRespostaCarregamento(resp, 'dai', { source: 'repo' })
|
| 2522 |
+
setRepoModeloSelecionado(modeloId)
|
| 2523 |
setUploadedFile(null)
|
| 2524 |
+
if (typeof onRouteChange === 'function') {
|
| 2525 |
+
onRouteChange({
|
| 2526 |
+
tab: 'elaboracao',
|
| 2527 |
+
modeloId,
|
| 2528 |
+
})
|
| 2529 |
+
}
|
| 2530 |
}, {
|
| 2531 |
onError: (err) => setImportacaoErro(err?.message || 'Falha ao carregar modelo do repositório.'),
|
| 2532 |
})
|
| 2533 |
}
|
| 2534 |
|
| 2535 |
+
useEffect(() => {
|
| 2536 |
+
const requestKey = String(quickLoadRequest?.requestKey || '').trim()
|
| 2537 |
+
const modeloId = String(quickLoadRequest?.modeloId || '').trim()
|
| 2538 |
+
if (!sessionId || !requestKey || !modeloId) return
|
| 2539 |
+
if (quickLoadHandledRef.current === requestKey) return
|
| 2540 |
+
quickLoadHandledRef.current = requestKey
|
| 2541 |
+
setModeloLoadSource('repo')
|
| 2542 |
+
setRepoModeloSelecionado(modeloId)
|
| 2543 |
+
void onCarregarModeloRepositorio(modeloId)
|
| 2544 |
+
}, [quickLoadRequest, sessionId])
|
| 2545 |
+
|
| 2546 |
function onUploadInputChange(event) {
|
| 2547 |
const input = event.target
|
| 2548 |
const file = input.files?.[0] ?? null
|
|
|
|
| 2697 |
setSelection(null)
|
| 2698 |
setFit(null)
|
| 2699 |
setSecao10InterativoFigura(null)
|
| 2700 |
+
setSecao10InterativoFiguraComIndices(null)
|
| 2701 |
setSecao10InterativoSelecionado('none')
|
| 2702 |
setSecao13InterativoFigura(null)
|
| 2703 |
+
setSecao13InterativoFiguraComIndices(null)
|
| 2704 |
setSecao13InterativoSelecionado('none')
|
| 2705 |
setTransformacaoY('(x)')
|
| 2706 |
setTransformacoesX({})
|
|
|
|
| 2866 |
setSection6EditOpen(false)
|
| 2867 |
setFit(null)
|
| 2868 |
setSecao13InterativoFigura(null)
|
| 2869 |
+
setSecao13InterativoFiguraComIndices(null)
|
| 2870 |
setSecao13InterativoSelecionado('none')
|
| 2871 |
setOutlierFiltrosAplicadosSnapshot(buildFiltrosSnapshot(defaultFiltros()))
|
| 2872 |
setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
|
|
|
|
| 2973 |
setFit((prev) => ({
|
| 2974 |
...prev,
|
| 2975 |
grafico_dispersao_modelo: resp.grafico,
|
| 2976 |
+
grafico_dispersao_modelo_com_indices: null,
|
| 2977 |
grafico_dispersao_modelo_modo: resp.modo || (resp.grafico ? 'interativo' : ''),
|
| 2978 |
grafico_dispersao_modelo_png: resp.grafico_png || null,
|
| 2979 |
grafico_dispersao_modelo_total_pontos: resp.total_pontos,
|
|
|
|
| 3008 |
figuraInterativa = respInterativo?.grafico || null
|
| 3009 |
}
|
| 3010 |
setSecao13InterativoFigura(figuraInterativa)
|
| 3011 |
+
setSecao13InterativoFiguraComIndices(null)
|
| 3012 |
const yLabelInterativo = getDispersaoYLabel(eixoYTipo, eixoYResiduo, eixoYColuna, colunaYComRotulo)
|
| 3013 |
const paineis = buildScatterPanels(figuraInterativa, {
|
| 3014 |
singleLabel: 'Dispersão do modelo',
|
|
|
|
| 3161 |
setOutlierTextosAplicadosSnapshot(buildOutlierTextSnapshot('', ''))
|
| 3162 |
setFit(null)
|
| 3163 |
setSecao13InterativoFigura(null)
|
| 3164 |
+
setSecao13InterativoFiguraComIndices(null)
|
| 3165 |
setSecao13InterativoSelecionado('none')
|
| 3166 |
setTransformacoesAplicadas(null)
|
| 3167 |
setOrigemTransformacoes(null)
|
|
|
|
| 3209 |
setSection6EditOpen(true)
|
| 3210 |
setFit(null)
|
| 3211 |
setSecao10InterativoFigura(null)
|
| 3212 |
+
setSecao10InterativoFiguraComIndices(null)
|
| 3213 |
setSecao10InterativoSelecionado('none')
|
| 3214 |
setSecao13InterativoFigura(null)
|
| 3215 |
+
setSecao13InterativoFiguraComIndices(null)
|
| 3216 |
setSecao13InterativoSelecionado('none')
|
| 3217 |
setTransformacoesAplicadas(null)
|
| 3218 |
setOrigemTransformacoes(null)
|
|
|
|
| 3750 |
}
|
| 3751 |
}
|
| 3752 |
|
| 3753 |
+
async function ensureSecao10GraficoComIndices() {
|
| 3754 |
+
const figuraAtual = selection?.grafico_dispersao_com_indices || null
|
| 3755 |
+
if (figuraAtual) {
|
| 3756 |
+
if (secao10InterativoFigura && !secao10InterativoFiguraComIndices) {
|
| 3757 |
+
setSecao10InterativoFiguraComIndices(figuraAtual)
|
| 3758 |
+
}
|
| 3759 |
+
return figuraAtual
|
| 3760 |
+
}
|
| 3761 |
+
if (!sessionId) return null
|
| 3762 |
+
try {
|
| 3763 |
+
const resp = await api.getDispersaoInterativo(sessionId, 'secao10')
|
| 3764 |
+
const figura = resp?.grafico_com_indices || null
|
| 3765 |
+
setSelection((prev) => (prev ? { ...prev, grafico_dispersao_com_indices: figura } : prev))
|
| 3766 |
+
if (secao10InterativoFigura) {
|
| 3767 |
+
setSecao10InterativoFiguraComIndices(figura)
|
| 3768 |
+
}
|
| 3769 |
+
return figura
|
| 3770 |
+
} catch (err) {
|
| 3771 |
+
setError(err?.message || 'Falha ao carregar índices da seção 10.')
|
| 3772 |
+
return null
|
| 3773 |
+
}
|
| 3774 |
+
}
|
| 3775 |
+
|
| 3776 |
+
async function ensureSecao13GraficoComIndices() {
|
| 3777 |
+
const figuraAtual = fit?.grafico_dispersao_modelo_com_indices || null
|
| 3778 |
+
if (figuraAtual) {
|
| 3779 |
+
if (secao13InterativoFigura && !secao13InterativoFiguraComIndices) {
|
| 3780 |
+
setSecao13InterativoFiguraComIndices(figuraAtual)
|
| 3781 |
+
}
|
| 3782 |
+
return figuraAtual
|
| 3783 |
+
}
|
| 3784 |
+
if (!sessionId) return null
|
| 3785 |
+
try {
|
| 3786 |
+
const resp = await api.getDispersaoInterativo(sessionId, 'secao13')
|
| 3787 |
+
const figura = resp?.grafico_com_indices || null
|
| 3788 |
+
setFit((prev) => (prev ? { ...prev, grafico_dispersao_modelo_com_indices: figura } : prev))
|
| 3789 |
+
if (secao13InterativoFigura) {
|
| 3790 |
+
setSecao13InterativoFiguraComIndices(figura)
|
| 3791 |
+
}
|
| 3792 |
+
return figura
|
| 3793 |
+
} catch (err) {
|
| 3794 |
+
setError(err?.message || 'Falha ao carregar índices da seção 13.')
|
| 3795 |
+
return null
|
| 3796 |
+
}
|
| 3797 |
+
}
|
| 3798 |
+
|
| 3799 |
+
async function ensureSecao15GraficoComIndices(grafico) {
|
| 3800 |
+
const alvo = String(grafico || '').trim()
|
| 3801 |
+
if (!alvo || !sessionId) return null
|
| 3802 |
+
const fieldMap = {
|
| 3803 |
+
obs_calc: 'grafico_obs_calc_com_indices',
|
| 3804 |
+
residuos: 'grafico_residuos_com_indices',
|
| 3805 |
+
histograma: 'grafico_histograma_com_indices',
|
| 3806 |
+
cook: 'grafico_cook_com_indices',
|
| 3807 |
+
}
|
| 3808 |
+
const field = fieldMap[alvo]
|
| 3809 |
+
if (!field) return null
|
| 3810 |
+
const figuraAtual = fit?.[field] || null
|
| 3811 |
+
if (figuraAtual) {
|
| 3812 |
+
if (secao15InterativoSelecionado === alvo && !secao15InterativoFiguraComIndices) {
|
| 3813 |
+
setSecao15InterativoFiguraComIndices(figuraAtual)
|
| 3814 |
+
}
|
| 3815 |
+
return figuraAtual
|
| 3816 |
+
}
|
| 3817 |
+
try {
|
| 3818 |
+
const resp = await api.getDiagnosticoInterativo(sessionId, alvo)
|
| 3819 |
+
const figura = resp?.payload_com_indices || null
|
| 3820 |
+
setFit((prev) => (prev ? { ...prev, [field]: figura } : prev))
|
| 3821 |
+
if (secao15InterativoSelecionado === alvo) {
|
| 3822 |
+
setSecao15InterativoFiguraComIndices(figura)
|
| 3823 |
+
}
|
| 3824 |
+
return figura
|
| 3825 |
+
} catch (err) {
|
| 3826 |
+
setError(err?.message || 'Falha ao carregar índices da seção 15.')
|
| 3827 |
+
return null
|
| 3828 |
+
}
|
| 3829 |
+
}
|
| 3830 |
+
|
| 3831 |
async function onChangeSecao10InterativoSelecionado(value) {
|
| 3832 |
const nextValue = String(value || 'none').trim() || 'none'
|
| 3833 |
setSecao10InterativoSelecionado(nextValue)
|
|
|
|
| 3839 |
await withBusy(async () => {
|
| 3840 |
const resp = await api.getDispersaoInterativo(sessionId, 'secao10')
|
| 3841 |
setSecao10InterativoFigura(resp?.grafico || null)
|
| 3842 |
+
setSecao10InterativoFiguraComIndices(null)
|
| 3843 |
})
|
| 3844 |
}
|
| 3845 |
|
|
|
|
| 3858 |
setSecao15InterativoSelecionado(nextValue)
|
| 3859 |
if (nextValue === 'none') {
|
| 3860 |
setSecao15InterativoFigura(null)
|
| 3861 |
+
setSecao15InterativoFiguraComIndices(null)
|
| 3862 |
return
|
| 3863 |
}
|
| 3864 |
if (!sessionId) return
|
|
|
|
| 3866 |
await withBusy(async () => {
|
| 3867 |
const resp = await api.getDiagnosticoInterativo(sessionId, nextValue)
|
| 3868 |
setSecao15InterativoFigura(resp?.payload || null)
|
| 3869 |
+
setSecao15InterativoFiguraComIndices(null)
|
| 3870 |
})
|
| 3871 |
}
|
| 3872 |
|
|
|
|
| 3978 |
</button>
|
| 3979 |
</div>
|
| 3980 |
) : (
|
| 3981 |
+
<div className="model-source-flow">
|
| 3982 |
+
<div className="model-source-flow-head">
|
| 3983 |
+
<button
|
| 3984 |
+
type="button"
|
| 3985 |
+
className="model-source-back-btn"
|
| 3986 |
+
onClick={() => {
|
| 3987 |
+
setModeloLoadSource('')
|
| 3988 |
+
setRepoModeloDropdownOpen(false)
|
| 3989 |
+
setImportacaoErro('')
|
| 3990 |
+
if (typeof onRouteChange === 'function') {
|
| 3991 |
+
onRouteChange({ tab: 'elaboracao' })
|
| 3992 |
+
}
|
| 3993 |
+
}}
|
| 3994 |
+
disabled={loading}
|
| 3995 |
+
>
|
| 3996 |
+
Voltar
|
| 3997 |
+
</button>
|
| 3998 |
+
{modeloLoadSource === 'repo' && repoModeloSelecionado ? <ShareLinkButton href={elaboracaoShareHref} /> : null}
|
| 3999 |
+
</div>
|
| 4000 |
|
| 4001 |
{modeloLoadSource === 'repo' ? (
|
| 4002 |
<div className="row upload-repo-row">
|
|
|
|
| 4012 |
disabled={loading || repoModelosLoading || repoModeloOptions.length === 0}
|
| 4013 |
onOpenChange={setRepoModeloDropdownOpen}
|
| 4014 |
/>
|
| 4015 |
+
</label>
|
| 4016 |
+
<div className="row compact upload-repo-actions">
|
| 4017 |
+
<button type="button" onClick={onCarregarModeloRepositorio} disabled={loading || repoModelosLoading || !repoModeloSelecionado}>
|
| 4018 |
+
Carregar do repositório
|
| 4019 |
+
</button>
|
| 4020 |
+
<button type="button" onClick={() => void carregarModelosRepositorio()} disabled={loading || repoModelosLoading}>
|
| 4021 |
+
Atualizar lista
|
| 4022 |
+
</button>
|
| 4023 |
+
</div>
|
| 4024 |
{repoFonteModelos ? <div className="section1-empty-hint">{repoFonteModelos}</div> : null}
|
| 4025 |
</div>
|
| 4026 |
) : null}
|
|
|
|
| 4951 |
<PlotFigure
|
| 4952 |
key={`s9-interativo-plot-${secao10InterativoAtual.id}`}
|
| 4953 |
figure={secao10InterativoAtual.figure}
|
| 4954 |
+
indexedFigure={graficosSecao9InterativoComIndicesMap.get(String(secao10InterativoAtual.label || '').trim()) || null}
|
| 4955 |
+
onRequestIndexedFigure={ensureSecao10GraficoComIndices}
|
| 4956 |
title={secao10InterativoAtual.title}
|
| 4957 |
subtitle={secao10InterativoAtual.subtitle}
|
| 4958 |
+
showPointIndexToggle
|
| 4959 |
forceHideLegend
|
| 4960 |
className="plot-stretch"
|
| 4961 |
lazy
|
|
|
|
| 5008 |
<PlotFigure
|
| 5009 |
key={`s9-plot-${item.id}`}
|
| 5010 |
figure={item.figure}
|
| 5011 |
+
indexedFigure={graficosSecao9ComIndicesMap.get(String(item.label || '').trim()) || null}
|
| 5012 |
+
onRequestIndexedFigure={ensureSecao10GraficoComIndices}
|
| 5013 |
title={item.title}
|
| 5014 |
subtitle={item.subtitle}
|
| 5015 |
+
showPointIndexToggle
|
| 5016 |
forceHideLegend
|
| 5017 |
className="plot-stretch"
|
| 5018 |
lazy
|
|
|
|
| 5488 |
<PlotFigure
|
| 5489 |
key={`s12-interativo-plot-${secao13InterativoAtual.id}`}
|
| 5490 |
figure={secao13InterativoAtual.figure}
|
| 5491 |
+
indexedFigure={graficosSecao12InterativoComIndicesMap.get(String(secao13InterativoAtual.label || '').trim()) || null}
|
| 5492 |
+
onRequestIndexedFigure={ensureSecao13GraficoComIndices}
|
| 5493 |
title={secao13InterativoAtual.title}
|
| 5494 |
subtitle={secao13InterativoAtual.subtitle}
|
| 5495 |
+
showPointIndexToggle
|
| 5496 |
forceHideLegend
|
| 5497 |
className="plot-stretch"
|
| 5498 |
lazy
|
|
|
|
| 5545 |
<PlotFigure
|
| 5546 |
key={`s12-plot-${item.id}`}
|
| 5547 |
figure={item.figure}
|
| 5548 |
+
indexedFigure={graficosSecao12ComIndicesMap.get(String(item.label || '').trim()) || null}
|
| 5549 |
+
onRequestIndexedFigure={ensureSecao13GraficoComIndices}
|
| 5550 |
title={item.title}
|
| 5551 |
subtitle={item.subtitle}
|
| 5552 |
+
showPointIndexToggle
|
| 5553 |
forceHideLegend
|
| 5554 |
className="plot-stretch"
|
| 5555 |
lazy
|
|
|
|
| 5718 |
</div>
|
| 5719 |
) : (
|
| 5720 |
<div className="plot-grid-2-fixed">
|
| 5721 |
+
<PlotFigure figure={fit.grafico_obs_calc} indexedFigure={fit.grafico_obs_calc_com_indices || null} onRequestIndexedFigure={() => ensureSecao15GraficoComIndices('obs_calc')} title="Obs x Calc" showPointIndexToggle />
|
| 5722 |
+
<PlotFigure figure={fit.grafico_residuos} indexedFigure={fit.grafico_residuos_com_indices || null} onRequestIndexedFigure={() => ensureSecao15GraficoComIndices('residuos')} title="Resíduos" showPointIndexToggle />
|
| 5723 |
+
<PlotFigure figure={fit.grafico_histograma} indexedFigure={fit.grafico_histograma_com_indices || null} onRequestIndexedFigure={() => ensureSecao15GraficoComIndices('histograma')} title="Histograma" showPointIndexToggle />
|
| 5724 |
+
<PlotFigure figure={fit.grafico_cook} indexedFigure={fit.grafico_cook_com_indices || null} onRequestIndexedFigure={() => ensureSecao15GraficoComIndices('cook')} title="Cook" showPointIndexToggle forceHideLegend />
|
| 5725 |
</div>
|
| 5726 |
)}
|
| 5727 |
<div className="plot-full-width">
|
| 5728 |
+
<PlotFigure figure={fit.grafico_correlacao} title="Matriz de correlação" showPointIndexToggle className="plot-correlation-card" />
|
| 5729 |
</div>
|
| 5730 |
{secao15DiagnosticoPng ? (
|
| 5731 |
<>
|
|
|
|
| 5766 |
<PlotFigure
|
| 5767 |
key={`s15-interativo-${secao15InterativoSelecionado}`}
|
| 5768 |
figure={secao15InterativoFigura}
|
| 5769 |
+
indexedFigure={secao15InterativoFiguraComIndices}
|
| 5770 |
+
onRequestIndexedFigure={() => ensureSecao15GraficoComIndices(secao15InterativoSelecionado)}
|
| 5771 |
title={secao15InterativoLabel}
|
| 5772 |
+
showPointIndexToggle
|
| 5773 |
forceHideLegend={secao15InterativoSelecionado === 'cook'}
|
| 5774 |
className="plot-stretch"
|
| 5775 |
lazy
|
frontend/src/components/ModelosEstatisticosTab.jsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
-
import React, { useEffect, useState } from 'react'
|
|
|
|
| 2 |
import PesquisaTab from './PesquisaTab'
|
| 3 |
import RepositorioTab from './RepositorioTab'
|
| 4 |
import VisaoGeralTab from './VisaoGeralTab'
|
|
@@ -13,51 +14,91 @@ export default function ModelosEstatisticosTab({
|
|
| 13 |
sessionId,
|
| 14 |
authUser,
|
| 15 |
onUsarModeloEmAvaliacao,
|
| 16 |
-
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
| 18 |
}) {
|
| 19 |
const [activeSubtab, setActiveSubtab] = useState('Pesquisar Modelos')
|
| 20 |
const [pesquisaMapaScrollRequest, setPesquisaMapaScrollRequest] = useState(null)
|
|
|
|
|
|
|
| 21 |
|
| 22 |
useEffect(() => {
|
| 23 |
-
const
|
| 24 |
-
if (!modeloId) return
|
| 25 |
-
setActiveSubtab('Repositório de Modelos')
|
| 26 |
-
}, [openRepositorioModeloRequest])
|
| 27 |
-
|
| 28 |
-
useEffect(() => {
|
| 29 |
-
const requestKey = String(returnToPesquisaMapaRequest?.requestKey || '').trim()
|
| 30 |
if (!requestKey) return
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
useEffect(() => {
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
}, [activeSubtab, returnToPesquisaMapaRequest])
|
| 40 |
|
| 41 |
return (
|
| 42 |
<div className="tab-content">
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
| 56 |
<div className="tab-pane" hidden={activeSubtab !== 'Pesquisar Modelos'}>
|
| 57 |
<PesquisaTab
|
| 58 |
sessionId={sessionId}
|
| 59 |
onUsarModeloEmAvaliacao={onUsarModeloEmAvaliacao}
|
|
|
|
|
|
|
| 60 |
scrollToMapaRequest={pesquisaMapaScrollRequest}
|
|
|
|
| 61 |
/>
|
| 62 |
</div>
|
| 63 |
|
|
@@ -65,7 +106,11 @@ export default function ModelosEstatisticosTab({
|
|
| 65 |
<RepositorioTab
|
| 66 |
authUser={authUser}
|
| 67 |
sessionId={sessionId}
|
| 68 |
-
openModeloRequest={
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
/>
|
| 70 |
</div>
|
| 71 |
|
|
|
|
| 1 |
+
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
| 2 |
+
import { getModelosSubtabKeyFromSlug, getModelosSubtabSlugFromKey } from '../deepLinks'
|
| 3 |
import PesquisaTab from './PesquisaTab'
|
| 4 |
import RepositorioTab from './RepositorioTab'
|
| 5 |
import VisaoGeralTab from './VisaoGeralTab'
|
|
|
|
| 14 |
sessionId,
|
| 15 |
authUser,
|
| 16 |
onUsarModeloEmAvaliacao,
|
| 17 |
+
onEditarModeloEmElaboracao = null,
|
| 18 |
+
onAbrirModeloNoRepositorio = null,
|
| 19 |
+
routeRequest = null,
|
| 20 |
+
onRouteChange = null,
|
| 21 |
+
onModoImersivoChange = null,
|
| 22 |
}) {
|
| 23 |
const [activeSubtab, setActiveSubtab] = useState('Pesquisar Modelos')
|
| 24 |
const [pesquisaMapaScrollRequest, setPesquisaMapaScrollRequest] = useState(null)
|
| 25 |
+
const [repositorioModeloAberto, setRepositorioModeloAberto] = useState(false)
|
| 26 |
+
const handledRouteRequestRef = useRef('')
|
| 27 |
|
| 28 |
useEffect(() => {
|
| 29 |
+
const requestKey = String(routeRequest?.requestKey || '').trim()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
if (!requestKey) return
|
| 31 |
+
if (handledRouteRequestRef.current === requestKey) return
|
| 32 |
+
handledRouteRequestRef.current = requestKey
|
| 33 |
+
|
| 34 |
+
const nextSubtab = getModelosSubtabKeyFromSlug(routeRequest?.subtab)
|
| 35 |
+
setActiveSubtab(routeRequest?.modeloId ? 'Repositório de Modelos' : nextSubtab)
|
| 36 |
+
|
| 37 |
+
if (routeRequest?.scrollToMapa) {
|
| 38 |
+
setPesquisaMapaScrollRequest({ requestKey })
|
| 39 |
+
}
|
| 40 |
+
}, [routeRequest])
|
| 41 |
+
|
| 42 |
+
const pesquisaRouteRequest = useMemo(() => {
|
| 43 |
+
const requestKey = String(routeRequest?.requestKey || '').trim()
|
| 44 |
+
if (!requestKey) return null
|
| 45 |
+
if (String(routeRequest?.subtab || '').trim() !== 'pesquisa') return null
|
| 46 |
+
return routeRequest
|
| 47 |
+
}, [routeRequest])
|
| 48 |
+
|
| 49 |
+
const repositorioOpenRequest = useMemo(() => {
|
| 50 |
+
const requestKey = String(routeRequest?.requestKey || '').trim()
|
| 51 |
+
const modeloId = String(routeRequest?.modeloId || '').trim()
|
| 52 |
+
if (!requestKey || !modeloId) return null
|
| 53 |
+
return {
|
| 54 |
+
requestKey,
|
| 55 |
+
modeloId,
|
| 56 |
+
modelTab: routeRequest?.modelTab || 'mapa',
|
| 57 |
+
nomeModelo: routeRequest?.nomeModelo || modeloId,
|
| 58 |
+
modeloArquivo: routeRequest?.modeloArquivo || '',
|
| 59 |
+
returnIntent: routeRequest?.returnIntent || null,
|
| 60 |
+
}
|
| 61 |
+
}, [routeRequest])
|
| 62 |
|
| 63 |
useEffect(() => {
|
| 64 |
+
if (typeof onModoImersivoChange !== 'function') return undefined
|
| 65 |
+
onModoImersivoChange(repositorioModeloAberto)
|
| 66 |
+
return () => onModoImersivoChange(false)
|
| 67 |
+
}, [repositorioModeloAberto, onModoImersivoChange])
|
|
|
|
| 68 |
|
| 69 |
return (
|
| 70 |
<div className="tab-content">
|
| 71 |
+
{!repositorioModeloAberto ? (
|
| 72 |
+
<div className="inner-tabs" role="tablist" aria-label="Abas de modelos estatísticos">
|
| 73 |
+
{SUBTABS.map((tab) => (
|
| 74 |
+
<button
|
| 75 |
+
key={tab.key}
|
| 76 |
+
type="button"
|
| 77 |
+
className={activeSubtab === tab.key ? 'inner-tab-pill active' : 'inner-tab-pill'}
|
| 78 |
+
onClick={() => {
|
| 79 |
+
setActiveSubtab(tab.key)
|
| 80 |
+
if (typeof onRouteChange === 'function') {
|
| 81 |
+
onRouteChange({
|
| 82 |
+
tab: 'modelos',
|
| 83 |
+
subtab: getModelosSubtabSlugFromKey(tab.key),
|
| 84 |
+
})
|
| 85 |
+
}
|
| 86 |
+
}}
|
| 87 |
+
>
|
| 88 |
+
{tab.label}
|
| 89 |
+
</button>
|
| 90 |
+
))}
|
| 91 |
+
</div>
|
| 92 |
+
) : null}
|
| 93 |
|
| 94 |
<div className="tab-pane" hidden={activeSubtab !== 'Pesquisar Modelos'}>
|
| 95 |
<PesquisaTab
|
| 96 |
sessionId={sessionId}
|
| 97 |
onUsarModeloEmAvaliacao={onUsarModeloEmAvaliacao}
|
| 98 |
+
onAbrirModeloNoRepositorio={onAbrirModeloNoRepositorio}
|
| 99 |
+
routeRequest={pesquisaRouteRequest}
|
| 100 |
scrollToMapaRequest={pesquisaMapaScrollRequest}
|
| 101 |
+
onRouteChange={onRouteChange}
|
| 102 |
/>
|
| 103 |
</div>
|
| 104 |
|
|
|
|
| 106 |
<RepositorioTab
|
| 107 |
authUser={authUser}
|
| 108 |
sessionId={sessionId}
|
| 109 |
+
openModeloRequest={repositorioOpenRequest}
|
| 110 |
+
onUsarModeloEmAvaliacao={onUsarModeloEmAvaliacao}
|
| 111 |
+
onEditarModeloEmElaboracao={onEditarModeloEmElaboracao}
|
| 112 |
+
onRouteChange={onRouteChange}
|
| 113 |
+
onModeloAbertoChange={setRepositorioModeloAberto}
|
| 114 |
/>
|
| 115 |
</div>
|
| 116 |
|
frontend/src/components/PesquisaTab.jsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
| 2 |
import { createPortal } from 'react-dom'
|
| 3 |
import { api, downloadBlob } from '../api'
|
|
|
|
| 4 |
import DataTable from './DataTable'
|
| 5 |
import EquationFormatsPanel from './EquationFormatsPanel'
|
| 6 |
import LoadingOverlay from './LoadingOverlay'
|
|
@@ -9,6 +10,7 @@ import ModeloTrabalhosTecnicosPanel from './ModeloTrabalhosTecnicosPanel'
|
|
| 9 |
import PlotFigure from './PlotFigure'
|
| 10 |
import PesquisaAdminConfigPanel from './PesquisaAdminConfigPanel'
|
| 11 |
import SectionBlock from './SectionBlock'
|
|
|
|
| 12 |
import SinglePillAutocomplete from './SinglePillAutocomplete'
|
| 13 |
import { getFaixaDataRecencyInfo } from '../modelRecency'
|
| 14 |
|
|
@@ -944,13 +946,20 @@ function ChipAutocompleteInput({
|
|
| 944 |
export default function PesquisaTab({
|
| 945 |
sessionId,
|
| 946 |
onUsarModeloEmAvaliacao = null,
|
|
|
|
|
|
|
| 947 |
scrollToMapaRequest = null,
|
|
|
|
| 948 |
}) {
|
| 949 |
-
const [
|
|
|
|
| 950 |
const [error, setError] = useState('')
|
| 951 |
const [pesquisaInicializada, setPesquisaInicializada] = useState(false)
|
| 952 |
const [sugestoesInicializadas, setSugestoesInicializadas] = useState(false)
|
| 953 |
const [mostrarAdminConfig, setMostrarAdminConfig] = useState(false)
|
|
|
|
|
|
|
|
|
|
| 954 |
|
| 955 |
const [filters, setFilters] = useState(EMPTY_FILTERS)
|
| 956 |
const [result, setResult] = useState(RESULT_INITIAL)
|
|
@@ -1001,6 +1010,10 @@ export default function PesquisaTab({
|
|
| 1001 |
const sectionResultadosRef = useRef(null)
|
| 1002 |
const sectionMapaRef = useRef(null)
|
| 1003 |
const scrollMapaHandledRef = useRef('')
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1004 |
|
| 1005 |
const sugestoes = result.sugestoes || {}
|
| 1006 |
const opcoesTipoModelo = useMemo(
|
|
@@ -1025,6 +1038,33 @@ export default function PesquisaTab({
|
|
| 1025 |
const algunsSelecionados = resultIds.some((id) => selectedIds.includes(id))
|
| 1026 |
const mapaHtmlAtual = mapaHtmls[mapaModoExibicao] || ''
|
| 1027 |
const mapaFoiGerado = Boolean(mapaHtmls.pontos || mapaHtmls.cobertura)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1028 |
|
| 1029 |
function scrollToElementTop(el, behavior = 'smooth', offsetPx = 0) {
|
| 1030 |
if (!el || typeof window === 'undefined') return
|
|
@@ -1033,13 +1073,8 @@ export default function PesquisaTab({
|
|
| 1033 |
window.scrollTo({ top: targetTop, behavior })
|
| 1034 |
}
|
| 1035 |
|
| 1036 |
-
function
|
| 1037 |
-
if (typeof
|
| 1038 |
-
const tabsEl = document.querySelector('.tabs')
|
| 1039 |
-
if (tabsEl) {
|
| 1040 |
-
scrollToElementTop(tabsEl, 'smooth', 0)
|
| 1041 |
-
return
|
| 1042 |
-
}
|
| 1043 |
window.scrollTo({ top: 0, behavior: 'smooth' })
|
| 1044 |
}
|
| 1045 |
|
|
@@ -1126,11 +1161,16 @@ export default function PesquisaTab({
|
|
| 1126 |
}
|
| 1127 |
}
|
| 1128 |
|
| 1129 |
-
async function buscarModelos(nextFilters = filters, nextAvaliandos = avaliandosGeolocalizados) {
|
| 1130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1131 |
setError('')
|
| 1132 |
try {
|
| 1133 |
const response = await api.pesquisarModelos(buildApiFilters(nextFilters, nextAvaliandos))
|
|
|
|
| 1134 |
const modelos = response.modelos || []
|
| 1135 |
const idsNovos = new Set(modelos.map((item) => item.id))
|
| 1136 |
|
|
@@ -1146,18 +1186,29 @@ export default function PesquisaTab({
|
|
| 1146 |
resetMapaPesquisa()
|
| 1147 |
setPesquisaInicializada(true)
|
| 1148 |
setSugestoesInicializadas(true)
|
|
|
|
|
|
|
|
|
|
| 1149 |
} catch (err) {
|
|
|
|
| 1150 |
setError(err.message)
|
| 1151 |
} finally {
|
| 1152 |
-
|
|
|
|
|
|
|
| 1153 |
}
|
| 1154 |
}
|
| 1155 |
|
| 1156 |
-
async function carregarContextoInicial() {
|
| 1157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1158 |
setError('')
|
| 1159 |
try {
|
| 1160 |
const response = await api.pesquisarModelos({ somente_contexto: true })
|
|
|
|
| 1161 |
|
| 1162 |
setResult({
|
| 1163 |
...RESULT_INITIAL,
|
|
@@ -1169,17 +1220,102 @@ export default function PesquisaTab({
|
|
| 1169 |
resetMapaPesquisa()
|
| 1170 |
setPesquisaInicializada(false)
|
| 1171 |
setSugestoesInicializadas(true)
|
|
|
|
|
|
|
|
|
|
| 1172 |
} catch (err) {
|
|
|
|
| 1173 |
setError(err.message)
|
| 1174 |
setSugestoesInicializadas(true)
|
| 1175 |
} finally {
|
| 1176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1177 |
}
|
| 1178 |
}
|
| 1179 |
|
| 1180 |
useEffect(() => {
|
|
|
|
|
|
|
| 1181 |
void carregarContextoInicial()
|
| 1182 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1183 |
|
| 1184 |
useEffect(() => {
|
| 1185 |
if (!selectAllRef.current) return
|
|
@@ -1245,8 +1381,13 @@ export default function PesquisaTab({
|
|
| 1245 |
}
|
| 1246 |
|
| 1247 |
async function onLimparFiltros() {
|
| 1248 |
-
|
| 1249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1250 |
}
|
| 1251 |
|
| 1252 |
function onToggleSelecionado(modelId) {
|
|
@@ -1302,6 +1443,14 @@ export default function PesquisaTab({
|
|
| 1302 |
}
|
| 1303 |
|
| 1304 |
async function onAbrirModelo(modelo) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1305 |
if (!sessionId) {
|
| 1306 |
setError('Sessao indisponivel no momento. Aguarde e tente novamente.')
|
| 1307 |
return
|
|
@@ -1319,7 +1468,7 @@ export default function PesquisaTab({
|
|
| 1319 |
observacao: String(resp?.meta_modelo?.observacao_modelo || '').trim(),
|
| 1320 |
})
|
| 1321 |
window.requestAnimationFrame(() => {
|
| 1322 |
-
|
| 1323 |
})
|
| 1324 |
} catch (err) {
|
| 1325 |
setModeloAbertoError(err.message || 'Falha ao abrir modelo.')
|
|
@@ -1623,7 +1772,7 @@ export default function PesquisaTab({
|
|
| 1623 |
type="button"
|
| 1624 |
className="pesquisa-localizacao-action pesquisa-localizacao-action-reset"
|
| 1625 |
onClick={() => void onRemoverAvaliandoLocalizacao(item.id)}
|
| 1626 |
-
disabled={localizacaoLoading ||
|
| 1627 |
>
|
| 1628 |
Excluir avaliando
|
| 1629 |
</button>
|
|
@@ -1697,11 +1846,14 @@ export default function PesquisaTab({
|
|
| 1697 |
<SinglePillAutocomplete
|
| 1698 |
value={localizacaoInputs.logradouro}
|
| 1699 |
onChange={(nextValue) => atualizarCampoLocalizacao('logradouro', nextValue)}
|
| 1700 |
-
options={
|
| 1701 |
placeholder="Digite ou selecione um logradouro dos eixos"
|
| 1702 |
panelTitle="Logradouros dos eixos"
|
| 1703 |
emptyMessage="Nenhum logradouro encontrado nos eixos."
|
| 1704 |
-
loading={
|
|
|
|
|
|
|
|
|
|
| 1705 |
inputName={toInputName('logradouroEixosPesquisa')}
|
| 1706 |
inputAutoComplete="new-password"
|
| 1707 |
/>
|
|
@@ -1765,7 +1917,7 @@ export default function PesquisaTab({
|
|
| 1765 |
placeholder="Digite e pressione Enter"
|
| 1766 |
suggestions={sugestoes.nomes_modelo || []}
|
| 1767 |
panelTitle="Modelos sugeridos"
|
| 1768 |
-
loading={
|
| 1769 |
/>
|
| 1770 |
</label>
|
| 1771 |
<label className="pesquisa-field">
|
|
@@ -1804,7 +1956,7 @@ export default function PesquisaTab({
|
|
| 1804 |
placeholder="Digite e pressione Enter"
|
| 1805 |
suggestions={sugestoes.finalidades || []}
|
| 1806 |
panelTitle="Finalidades sugeridas"
|
| 1807 |
-
loading={
|
| 1808 |
/>
|
| 1809 |
</label>
|
| 1810 |
</div>
|
|
@@ -1831,7 +1983,7 @@ export default function PesquisaTab({
|
|
| 1831 |
placeholder="Selecione uma ou mais zonas"
|
| 1832 |
suggestions={sugestoes.zonas_avaliacao || []}
|
| 1833 |
panelTitle="Zonas sugeridas"
|
| 1834 |
-
loading={
|
| 1835 |
/>
|
| 1836 |
</label>
|
| 1837 |
<label className="pesquisa-field pesquisa-bairro-bottom-field">
|
|
@@ -1843,7 +1995,7 @@ export default function PesquisaTab({
|
|
| 1843 |
placeholder="Digite e pressione Enter"
|
| 1844 |
suggestions={sugestoes.bairros || []}
|
| 1845 |
panelTitle="Bairros sugeridos"
|
| 1846 |
-
loading={
|
| 1847 |
/>
|
| 1848 |
</label>
|
| 1849 |
</div>
|
|
@@ -1876,10 +2028,10 @@ export default function PesquisaTab({
|
|
| 1876 |
</div>
|
| 1877 |
|
| 1878 |
<div className="row pesquisa-actions pesquisa-actions-primary">
|
| 1879 |
-
<button type="button" onClick={() => void buscarModelos()} disabled={
|
| 1880 |
-
{
|
| 1881 |
</button>
|
| 1882 |
-
<button type="button" onClick={() => void onLimparFiltros()} disabled={
|
| 1883 |
Limpar filtros
|
| 1884 |
</button>
|
| 1885 |
</div>
|
|
@@ -1912,12 +2064,20 @@ export default function PesquisaTab({
|
|
| 1912 |
</select>
|
| 1913 |
</label>
|
| 1914 |
) : null}
|
| 1915 |
-
|
| 1916 |
-
<
|
| 1917 |
-
|
| 1918 |
-
|
| 1919 |
-
|
| 1920 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1921 |
</div>
|
| 1922 |
|
| 1923 |
{!modelosOrdenados.length ? (
|
|
|
|
| 1 |
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
| 2 |
import { createPortal } from 'react-dom'
|
| 3 |
import { api, downloadBlob } from '../api'
|
| 4 |
+
import { buildPesquisaLink, buildPesquisaRoutePayload, getPesquisaFilterDefaults, hasPesquisaRoutePayload } from '../deepLinks'
|
| 5 |
import DataTable from './DataTable'
|
| 6 |
import EquationFormatsPanel from './EquationFormatsPanel'
|
| 7 |
import LoadingOverlay from './LoadingOverlay'
|
|
|
|
| 10 |
import PlotFigure from './PlotFigure'
|
| 11 |
import PesquisaAdminConfigPanel from './PesquisaAdminConfigPanel'
|
| 12 |
import SectionBlock from './SectionBlock'
|
| 13 |
+
import ShareLinkButton from './ShareLinkButton'
|
| 14 |
import SinglePillAutocomplete from './SinglePillAutocomplete'
|
| 15 |
import { getFaixaDataRecencyInfo } from '../modelRecency'
|
| 16 |
|
|
|
|
| 946 |
export default function PesquisaTab({
|
| 947 |
sessionId,
|
| 948 |
onUsarModeloEmAvaliacao = null,
|
| 949 |
+
onAbrirModeloNoRepositorio = null,
|
| 950 |
+
routeRequest = null,
|
| 951 |
scrollToMapaRequest = null,
|
| 952 |
+
onRouteChange = null,
|
| 953 |
}) {
|
| 954 |
+
const [searchLoading, setSearchLoading] = useState(false)
|
| 955 |
+
const [contextLoading, setContextLoading] = useState(false)
|
| 956 |
const [error, setError] = useState('')
|
| 957 |
const [pesquisaInicializada, setPesquisaInicializada] = useState(false)
|
| 958 |
const [sugestoesInicializadas, setSugestoesInicializadas] = useState(false)
|
| 959 |
const [mostrarAdminConfig, setMostrarAdminConfig] = useState(false)
|
| 960 |
+
const [logradouroOptions, setLogradouroOptions] = useState([])
|
| 961 |
+
const [logradouroOptionsLoading, setLogradouroOptionsLoading] = useState(false)
|
| 962 |
+
const [logradouroOptionsLoaded, setLogradouroOptionsLoaded] = useState(false)
|
| 963 |
|
| 964 |
const [filters, setFilters] = useState(EMPTY_FILTERS)
|
| 965 |
const [result, setResult] = useState(RESULT_INITIAL)
|
|
|
|
| 1010 |
const sectionResultadosRef = useRef(null)
|
| 1011 |
const sectionMapaRef = useRef(null)
|
| 1012 |
const scrollMapaHandledRef = useRef('')
|
| 1013 |
+
const routeRequestHandledRef = useRef('')
|
| 1014 |
+
const resultRequestSeqRef = useRef(0)
|
| 1015 |
+
const searchRequestSeqRef = useRef(0)
|
| 1016 |
+
const contextRequestSeqRef = useRef(0)
|
| 1017 |
|
| 1018 |
const sugestoes = result.sugestoes || {}
|
| 1019 |
const opcoesTipoModelo = useMemo(
|
|
|
|
| 1038 |
const algunsSelecionados = resultIds.some((id) => selectedIds.includes(id))
|
| 1039 |
const mapaHtmlAtual = mapaHtmls[mapaModoExibicao] || ''
|
| 1040 |
const mapaFoiGerado = Boolean(mapaHtmls.pontos || mapaHtmls.cobertura)
|
| 1041 |
+
const pesquisaShareAvaliando = !localizacaoMultipla ? (avaliandosGeoPayload[0] || null) : null
|
| 1042 |
+
const pesquisaShareHref = buildPesquisaLink(filters, pesquisaShareAvaliando)
|
| 1043 |
+
const pesquisaShareDisabled = localizacaoMultipla
|
| 1044 |
+
const isPesquisaBusy = searchLoading
|
| 1045 |
+
|
| 1046 |
+
function buildPesquisaReturnIntent() {
|
| 1047 |
+
return {
|
| 1048 |
+
...buildPesquisaRoutePayload(filters, pesquisaShareAvaliando),
|
| 1049 |
+
pesquisaExecutada: true,
|
| 1050 |
+
avaliandos: avaliandosGeolocalizados.map((item, index) => ({
|
| 1051 |
+
id: String(item?.id || `avaliando-${index + 1}`),
|
| 1052 |
+
lat: Number(item?.lat),
|
| 1053 |
+
lon: Number(item?.lon),
|
| 1054 |
+
logradouro: String(item?.logradouro || ''),
|
| 1055 |
+
numero_usado: String(item?.numero_usado || ''),
|
| 1056 |
+
cdlog: item?.cdlog ?? null,
|
| 1057 |
+
origem: String(item?.origem || 'coords'),
|
| 1058 |
+
})).filter((item) => Number.isFinite(item.lat) && Number.isFinite(item.lon)),
|
| 1059 |
+
}
|
| 1060 |
+
}
|
| 1061 |
+
|
| 1062 |
+
function emitPesquisaRoute(nextFilters = filters, nextAvaliandos = avaliandosGeolocalizados) {
|
| 1063 |
+
if (typeof onRouteChange !== 'function') return
|
| 1064 |
+
const avaliandosPayload = buildAvaliandosGeoPayload(nextAvaliandos)
|
| 1065 |
+
const avaliando = avaliandosPayload.length === 1 ? avaliandosPayload[0] : null
|
| 1066 |
+
onRouteChange(buildPesquisaRoutePayload(nextFilters, avaliando))
|
| 1067 |
+
}
|
| 1068 |
|
| 1069 |
function scrollToElementTop(el, behavior = 'smooth', offsetPx = 0) {
|
| 1070 |
if (!el || typeof window === 'undefined') return
|
|
|
|
| 1073 |
window.scrollTo({ top: targetTop, behavior })
|
| 1074 |
}
|
| 1075 |
|
| 1076 |
+
function scrollParaTopoDaPagina() {
|
| 1077 |
+
if (typeof window === 'undefined') return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1078 |
window.scrollTo({ top: 0, behavior: 'smooth' })
|
| 1079 |
}
|
| 1080 |
|
|
|
|
| 1161 |
}
|
| 1162 |
}
|
| 1163 |
|
| 1164 |
+
async function buscarModelos(nextFilters = filters, nextAvaliandos = avaliandosGeolocalizados, options = {}) {
|
| 1165 |
+
const requestId = resultRequestSeqRef.current + 1
|
| 1166 |
+
const searchRequestId = searchRequestSeqRef.current + 1
|
| 1167 |
+
resultRequestSeqRef.current = requestId
|
| 1168 |
+
searchRequestSeqRef.current = searchRequestId
|
| 1169 |
+
setSearchLoading(true)
|
| 1170 |
setError('')
|
| 1171 |
try {
|
| 1172 |
const response = await api.pesquisarModelos(buildApiFilters(nextFilters, nextAvaliandos))
|
| 1173 |
+
if (requestId !== resultRequestSeqRef.current) return
|
| 1174 |
const modelos = response.modelos || []
|
| 1175 |
const idsNovos = new Set(modelos.map((item) => item.id))
|
| 1176 |
|
|
|
|
| 1186 |
resetMapaPesquisa()
|
| 1187 |
setPesquisaInicializada(true)
|
| 1188 |
setSugestoesInicializadas(true)
|
| 1189 |
+
if (options.syncRoute !== false) {
|
| 1190 |
+
emitPesquisaRoute(nextFilters, nextAvaliandos)
|
| 1191 |
+
}
|
| 1192 |
} catch (err) {
|
| 1193 |
+
if (requestId !== resultRequestSeqRef.current) return
|
| 1194 |
setError(err.message)
|
| 1195 |
} finally {
|
| 1196 |
+
if (searchRequestId === searchRequestSeqRef.current) {
|
| 1197 |
+
setSearchLoading(false)
|
| 1198 |
+
}
|
| 1199 |
}
|
| 1200 |
}
|
| 1201 |
|
| 1202 |
+
async function carregarContextoInicial(options = {}) {
|
| 1203 |
+
const requestId = resultRequestSeqRef.current + 1
|
| 1204 |
+
const contextRequestId = contextRequestSeqRef.current + 1
|
| 1205 |
+
resultRequestSeqRef.current = requestId
|
| 1206 |
+
contextRequestSeqRef.current = contextRequestId
|
| 1207 |
+
setContextLoading(true)
|
| 1208 |
setError('')
|
| 1209 |
try {
|
| 1210 |
const response = await api.pesquisarModelos({ somente_contexto: true })
|
| 1211 |
+
if (requestId !== resultRequestSeqRef.current) return
|
| 1212 |
|
| 1213 |
setResult({
|
| 1214 |
...RESULT_INITIAL,
|
|
|
|
| 1220 |
resetMapaPesquisa()
|
| 1221 |
setPesquisaInicializada(false)
|
| 1222 |
setSugestoesInicializadas(true)
|
| 1223 |
+
if (options.syncRoute) {
|
| 1224 |
+
emitPesquisaRoute(options.filters || getPesquisaFilterDefaults(), options.avaliandos || avaliandosGeolocalizados)
|
| 1225 |
+
}
|
| 1226 |
} catch (err) {
|
| 1227 |
+
if (requestId !== resultRequestSeqRef.current) return
|
| 1228 |
setError(err.message)
|
| 1229 |
setSugestoesInicializadas(true)
|
| 1230 |
} finally {
|
| 1231 |
+
if (contextRequestId === contextRequestSeqRef.current) {
|
| 1232 |
+
setContextLoading(false)
|
| 1233 |
+
}
|
| 1234 |
+
}
|
| 1235 |
+
}
|
| 1236 |
+
|
| 1237 |
+
async function carregarSugestoesLogradouro() {
|
| 1238 |
+
if (logradouroOptionsLoading || logradouroOptionsLoaded) return
|
| 1239 |
+
setLogradouroOptionsLoading(true)
|
| 1240 |
+
try {
|
| 1241 |
+
const response = await api.pesquisarLogradourosEixos()
|
| 1242 |
+
const opcoes = Array.isArray(response?.logradouros_eixos)
|
| 1243 |
+
? response.logradouros_eixos.map((item) => String(item || '').trim()).filter(Boolean)
|
| 1244 |
+
: []
|
| 1245 |
+
setLogradouroOptions(opcoes)
|
| 1246 |
+
setLogradouroOptionsLoaded(true)
|
| 1247 |
+
} catch (_err) {
|
| 1248 |
+
setLogradouroOptions([])
|
| 1249 |
+
} finally {
|
| 1250 |
+
setLogradouroOptionsLoading(false)
|
| 1251 |
}
|
| 1252 |
}
|
| 1253 |
|
| 1254 |
useEffect(() => {
|
| 1255 |
+
const requestKey = String(routeRequest?.requestKey || '').trim()
|
| 1256 |
+
if (requestKey) return undefined
|
| 1257 |
void carregarContextoInicial()
|
| 1258 |
+
return undefined
|
| 1259 |
+
}, [routeRequest])
|
| 1260 |
+
|
| 1261 |
+
useEffect(() => {
|
| 1262 |
+
const requestKey = String(routeRequest?.requestKey || '').trim()
|
| 1263 |
+
if (!requestKey) return
|
| 1264 |
+
if (routeRequestHandledRef.current === requestKey) return
|
| 1265 |
+
routeRequestHandledRef.current = requestKey
|
| 1266 |
+
|
| 1267 |
+
const nextFilters = {
|
| 1268 |
+
...getPesquisaFilterDefaults(),
|
| 1269 |
+
...(routeRequest?.filters || {}),
|
| 1270 |
+
}
|
| 1271 |
+
const rawAvaliandos = Array.isArray(routeRequest?.avaliandos) ? routeRequest.avaliandos : []
|
| 1272 |
+
let nextEntries = rawAvaliandos
|
| 1273 |
+
.map((item, index) => {
|
| 1274 |
+
const lat = Number(item?.lat)
|
| 1275 |
+
const lon = Number(item?.lon)
|
| 1276 |
+
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null
|
| 1277 |
+
return createLocalizacaoEntry({
|
| 1278 |
+
lat,
|
| 1279 |
+
lon,
|
| 1280 |
+
origem: String(item?.origem || 'coords'),
|
| 1281 |
+
logradouro: String(item?.logradouro || ''),
|
| 1282 |
+
numero_usado: String(item?.numero_usado || ''),
|
| 1283 |
+
cdlog: item?.cdlog ?? null,
|
| 1284 |
+
}, String(item?.id || `avaliando-${localizacaoIdCounterRef.current + index}`))
|
| 1285 |
+
})
|
| 1286 |
+
.filter(Boolean)
|
| 1287 |
+
const lat = Number(routeRequest?.avaliando?.lat)
|
| 1288 |
+
const lon = Number(routeRequest?.avaliando?.lon)
|
| 1289 |
+
const possuiAvaliando = Number.isFinite(lat) && Number.isFinite(lon)
|
| 1290 |
+
if (!nextEntries.length && possuiAvaliando) {
|
| 1291 |
+
nextEntries = [createLocalizacaoEntry({
|
| 1292 |
+
lat,
|
| 1293 |
+
lon,
|
| 1294 |
+
origem: 'coords',
|
| 1295 |
+
logradouro: '',
|
| 1296 |
+
numero_usado: '',
|
| 1297 |
+
cdlog: null,
|
| 1298 |
+
}, `avaliando-${localizacaoIdCounterRef.current}`)]
|
| 1299 |
+
localizacaoIdCounterRef.current += 1
|
| 1300 |
+
} else if (nextEntries.length) {
|
| 1301 |
+
localizacaoIdCounterRef.current += nextEntries.length
|
| 1302 |
+
}
|
| 1303 |
+
|
| 1304 |
+
setFilters(nextFilters)
|
| 1305 |
+
setAvaliandosGeolocalizados(nextEntries)
|
| 1306 |
+
setLocalizacaoModo(possuiAvaliando ? 'coords' : 'endereco')
|
| 1307 |
+
setLocalizacaoInputs(EMPTY_LOCATION_INPUTS)
|
| 1308 |
+
setLocalizacaoError('')
|
| 1309 |
+
setLocalizacaoStatus('')
|
| 1310 |
+
resetMapaPesquisa()
|
| 1311 |
+
|
| 1312 |
+
if (routeRequest?.pesquisaExecutada || hasPesquisaRoutePayload(nextFilters, routeRequest?.avaliando) || nextEntries.length > 1) {
|
| 1313 |
+
void buscarModelos(nextFilters, nextEntries, { syncRoute: false })
|
| 1314 |
+
return
|
| 1315 |
+
}
|
| 1316 |
+
|
| 1317 |
+
void carregarContextoInicial({ syncRoute: false, filters: nextFilters, avaliandos: nextEntries })
|
| 1318 |
+
}, [routeRequest])
|
| 1319 |
|
| 1320 |
useEffect(() => {
|
| 1321 |
if (!selectAllRef.current) return
|
|
|
|
| 1381 |
}
|
| 1382 |
|
| 1383 |
async function onLimparFiltros() {
|
| 1384 |
+
const nextFilters = getPesquisaFilterDefaults()
|
| 1385 |
+
setFilters(nextFilters)
|
| 1386 |
+
await carregarContextoInicial({
|
| 1387 |
+
syncRoute: true,
|
| 1388 |
+
filters: nextFilters,
|
| 1389 |
+
avaliandos: avaliandosGeolocalizados,
|
| 1390 |
+
})
|
| 1391 |
}
|
| 1392 |
|
| 1393 |
function onToggleSelecionado(modelId) {
|
|
|
|
| 1443 |
}
|
| 1444 |
|
| 1445 |
async function onAbrirModelo(modelo) {
|
| 1446 |
+
if (typeof onAbrirModeloNoRepositorio === 'function') {
|
| 1447 |
+
onAbrirModeloNoRepositorio({
|
| 1448 |
+
...modelo,
|
| 1449 |
+
returnIntent: buildPesquisaReturnIntent(),
|
| 1450 |
+
})
|
| 1451 |
+
return
|
| 1452 |
+
}
|
| 1453 |
+
|
| 1454 |
if (!sessionId) {
|
| 1455 |
setError('Sessao indisponivel no momento. Aguarde e tente novamente.')
|
| 1456 |
return
|
|
|
|
| 1468 |
observacao: String(resp?.meta_modelo?.observacao_modelo || '').trim(),
|
| 1469 |
})
|
| 1470 |
window.requestAnimationFrame(() => {
|
| 1471 |
+
scrollParaTopoDaPagina()
|
| 1472 |
})
|
| 1473 |
} catch (err) {
|
| 1474 |
setModeloAbertoError(err.message || 'Falha ao abrir modelo.')
|
|
|
|
| 1772 |
type="button"
|
| 1773 |
className="pesquisa-localizacao-action pesquisa-localizacao-action-reset"
|
| 1774 |
onClick={() => void onRemoverAvaliandoLocalizacao(item.id)}
|
| 1775 |
+
disabled={localizacaoLoading || isPesquisaBusy}
|
| 1776 |
>
|
| 1777 |
Excluir avaliando
|
| 1778 |
</button>
|
|
|
|
| 1846 |
<SinglePillAutocomplete
|
| 1847 |
value={localizacaoInputs.logradouro}
|
| 1848 |
onChange={(nextValue) => atualizarCampoLocalizacao('logradouro', nextValue)}
|
| 1849 |
+
options={logradouroOptions}
|
| 1850 |
placeholder="Digite ou selecione um logradouro dos eixos"
|
| 1851 |
panelTitle="Logradouros dos eixos"
|
| 1852 |
emptyMessage="Nenhum logradouro encontrado nos eixos."
|
| 1853 |
+
loading={logradouroOptionsLoading}
|
| 1854 |
+
onOpenChange={(open) => {
|
| 1855 |
+
if (open) void carregarSugestoesLogradouro()
|
| 1856 |
+
}}
|
| 1857 |
inputName={toInputName('logradouroEixosPesquisa')}
|
| 1858 |
inputAutoComplete="new-password"
|
| 1859 |
/>
|
|
|
|
| 1917 |
placeholder="Digite e pressione Enter"
|
| 1918 |
suggestions={sugestoes.nomes_modelo || []}
|
| 1919 |
panelTitle="Modelos sugeridos"
|
| 1920 |
+
loading={contextLoading && !sugestoesInicializadas}
|
| 1921 |
/>
|
| 1922 |
</label>
|
| 1923 |
<label className="pesquisa-field">
|
|
|
|
| 1956 |
placeholder="Digite e pressione Enter"
|
| 1957 |
suggestions={sugestoes.finalidades || []}
|
| 1958 |
panelTitle="Finalidades sugeridas"
|
| 1959 |
+
loading={contextLoading && !sugestoesInicializadas}
|
| 1960 |
/>
|
| 1961 |
</label>
|
| 1962 |
</div>
|
|
|
|
| 1983 |
placeholder="Selecione uma ou mais zonas"
|
| 1984 |
suggestions={sugestoes.zonas_avaliacao || []}
|
| 1985 |
panelTitle="Zonas sugeridas"
|
| 1986 |
+
loading={contextLoading && !sugestoesInicializadas}
|
| 1987 |
/>
|
| 1988 |
</label>
|
| 1989 |
<label className="pesquisa-field pesquisa-bairro-bottom-field">
|
|
|
|
| 1995 |
placeholder="Digite e pressione Enter"
|
| 1996 |
suggestions={sugestoes.bairros || []}
|
| 1997 |
panelTitle="Bairros sugeridos"
|
| 1998 |
+
loading={contextLoading && !sugestoesInicializadas}
|
| 1999 |
/>
|
| 2000 |
</label>
|
| 2001 |
</div>
|
|
|
|
| 2028 |
</div>
|
| 2029 |
|
| 2030 |
<div className="row pesquisa-actions pesquisa-actions-primary">
|
| 2031 |
+
<button type="button" onClick={() => void buscarModelos()} disabled={isPesquisaBusy}>
|
| 2032 |
+
{searchLoading ? 'Pesquisando...' : 'Pesquisar'}
|
| 2033 |
</button>
|
| 2034 |
+
<button type="button" onClick={() => void onLimparFiltros()} disabled={isPesquisaBusy}>
|
| 2035 |
Limpar filtros
|
| 2036 |
</button>
|
| 2037 |
</div>
|
|
|
|
| 2064 |
</select>
|
| 2065 |
</label>
|
| 2066 |
) : null}
|
| 2067 |
+
<div className="pesquisa-results-toolbar-actions">
|
| 2068 |
+
<ShareLinkButton
|
| 2069 |
+
href={pesquisaShareHref}
|
| 2070 |
+
label="Copiar link da pesquisa"
|
| 2071 |
+
disabled={pesquisaShareDisabled}
|
| 2072 |
+
title={pesquisaShareDisabled ? 'O compartilhamento da pesquisa suporta apenas um avaliando na versão atual.' : pesquisaShareHref}
|
| 2073 |
+
/>
|
| 2074 |
+
{resultIds.length ? (
|
| 2075 |
+
<label className="pesquisa-select-all">
|
| 2076 |
+
<input ref={selectAllRef} type="checkbox" checked={todosSelecionados} onChange={onToggleSelecionarTodos} />
|
| 2077 |
+
Selecionar todos os exibidos
|
| 2078 |
+
</label>
|
| 2079 |
+
) : null}
|
| 2080 |
+
</div>
|
| 2081 |
</div>
|
| 2082 |
|
| 2083 |
{!modelosOrdenados.length ? (
|
frontend/src/components/PlotFigure.jsx
CHANGED
|
@@ -2,9 +2,21 @@ import React, { useEffect, useRef, useState } from 'react'
|
|
| 2 |
import Plot from 'react-plotly.js'
|
| 3 |
import Plotly from 'plotly.js-dist-min'
|
| 4 |
|
| 5 |
-
function PlotFigure({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
const containerRef = useRef(null)
|
| 7 |
const [shouldRenderPlot, setShouldRenderPlot] = useState(() => !lazy)
|
|
|
|
|
|
|
| 8 |
|
| 9 |
useEffect(() => {
|
| 10 |
if (!lazy) {
|
|
@@ -32,16 +44,21 @@ function PlotFigure({ figure, title, subtitle = '', forceHideLegend = false, cla
|
|
| 32 |
return () => observer.disconnect()
|
| 33 |
}, [lazy, shouldRenderPlot])
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
if (!figure) {
|
| 36 |
return <div className="empty-box">Grafico indisponivel.</div>
|
| 37 |
}
|
| 38 |
|
| 39 |
-
const
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
const baseLayout =
|
| 45 |
const { width: _ignoreWidth, ...layoutNoWidth } = baseLayout
|
| 46 |
const safeAnnotations = Array.isArray(baseLayout.annotations)
|
| 47 |
? baseLayout.annotations.map((annotation) => {
|
|
@@ -49,35 +66,85 @@ function PlotFigure({ figure, title, subtitle = '', forceHideLegend = false, cla
|
|
| 49 |
return { ...clean, showarrow: false }
|
| 50 |
})
|
| 51 |
: baseLayout.annotations
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
const layout = {
|
| 53 |
...layoutNoWidth,
|
| 54 |
autosize: true,
|
| 55 |
-
annotations: safeAnnotations,
|
| 56 |
margin: baseLayout.margin || { t: 40, r: 20, b: 50, l: 50 },
|
| 57 |
}
|
| 58 |
if (forceHideLegend) {
|
| 59 |
layout.showlegend = false
|
| 60 |
}
|
|
|
|
| 61 |
const plotHeight = layout.height ? `${layout.height}px` : '100%'
|
| 62 |
const cardClassName = `plot-card ${className}`.trim()
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
return (
|
| 65 |
<div ref={containerRef} className={cardClassName}>
|
| 66 |
{title || subtitle ? (
|
| 67 |
<div className="plot-card-head">
|
| 68 |
{title ? <h4 className="plot-card-title">{title}</h4> : null}
|
| 69 |
{subtitle ? <div className="plot-card-subtitle">{subtitle}</div> : null}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
</div>
|
| 71 |
) : null}
|
| 72 |
{shouldRenderPlot ? (
|
| 73 |
-
<
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
| 81 |
) : (
|
| 82 |
<div className="plot-lazy-placeholder">Carregando gráfico...</div>
|
| 83 |
)}
|
|
@@ -85,4 +152,4 @@ function PlotFigure({ figure, title, subtitle = '', forceHideLegend = false, cla
|
|
| 85 |
)
|
| 86 |
}
|
| 87 |
|
| 88 |
-
export default
|
|
|
|
| 2 |
import Plot from 'react-plotly.js'
|
| 3 |
import Plotly from 'plotly.js-dist-min'
|
| 4 |
|
| 5 |
+
function PlotFigure({
|
| 6 |
+
figure,
|
| 7 |
+
indexedFigure = null,
|
| 8 |
+
onRequestIndexedFigure = null,
|
| 9 |
+
title,
|
| 10 |
+
subtitle = '',
|
| 11 |
+
forceHideLegend = false,
|
| 12 |
+
className = '',
|
| 13 |
+
lazy = false,
|
| 14 |
+
showPointIndexToggle = false,
|
| 15 |
+
}) {
|
| 16 |
const containerRef = useRef(null)
|
| 17 |
const [shouldRenderPlot, setShouldRenderPlot] = useState(() => !lazy)
|
| 18 |
+
const [showPointIndices, setShowPointIndices] = useState(false)
|
| 19 |
+
const [loadingIndexedFigure, setLoadingIndexedFigure] = useState(false)
|
| 20 |
|
| 21 |
useEffect(() => {
|
| 22 |
if (!lazy) {
|
|
|
|
| 44 |
return () => observer.disconnect()
|
| 45 |
}, [lazy, shouldRenderPlot])
|
| 46 |
|
| 47 |
+
useEffect(() => {
|
| 48 |
+
setShowPointIndices(false)
|
| 49 |
+
setLoadingIndexedFigure(false)
|
| 50 |
+
}, [figure])
|
| 51 |
+
|
| 52 |
if (!figure) {
|
| 53 |
return <div className="empty-box">Grafico indisponivel.</div>
|
| 54 |
}
|
| 55 |
|
| 56 |
+
const hasIndexedFigure = Boolean(indexedFigure)
|
| 57 |
+
const canRequestIndexedFigure = typeof onRequestIndexedFigure === 'function'
|
| 58 |
+
const canToggleIndices = hasIndexedFigure || canRequestIndexedFigure
|
| 59 |
+
const activeFigure = showPointIndices && hasIndexedFigure ? indexedFigure : figure
|
| 60 |
+
const safeFigure = activeFigure || { data: [], layout: {} }
|
| 61 |
+
const baseLayout = safeFigure.layout || {}
|
| 62 |
const { width: _ignoreWidth, ...layoutNoWidth } = baseLayout
|
| 63 |
const safeAnnotations = Array.isArray(baseLayout.annotations)
|
| 64 |
? baseLayout.annotations.map((annotation) => {
|
|
|
|
| 66 |
return { ...clean, showarrow: false }
|
| 67 |
})
|
| 68 |
: baseLayout.annotations
|
| 69 |
+
|
| 70 |
+
const data = (safeFigure.data || []).map((trace) => (
|
| 71 |
+
forceHideLegend ? { ...trace, showlegend: false } : { ...trace }
|
| 72 |
+
))
|
| 73 |
const layout = {
|
| 74 |
...layoutNoWidth,
|
| 75 |
autosize: true,
|
| 76 |
+
annotations: Array.isArray(safeAnnotations) ? safeAnnotations : [],
|
| 77 |
margin: baseLayout.margin || { t: 40, r: 20, b: 50, l: 50 },
|
| 78 |
}
|
| 79 |
if (forceHideLegend) {
|
| 80 |
layout.showlegend = false
|
| 81 |
}
|
| 82 |
+
|
| 83 |
const plotHeight = layout.height ? `${layout.height}px` : '100%'
|
| 84 |
const cardClassName = `plot-card ${className}`.trim()
|
| 85 |
|
| 86 |
+
async function handleToggleChange(event) {
|
| 87 |
+
const checked = Boolean(event.target.checked)
|
| 88 |
+
if (!checked) {
|
| 89 |
+
setShowPointIndices(false)
|
| 90 |
+
return
|
| 91 |
+
}
|
| 92 |
+
if (hasIndexedFigure) {
|
| 93 |
+
setShowPointIndices(true)
|
| 94 |
+
return
|
| 95 |
+
}
|
| 96 |
+
if (!canRequestIndexedFigure || loadingIndexedFigure) return
|
| 97 |
+
setLoadingIndexedFigure(true)
|
| 98 |
+
try {
|
| 99 |
+
const loadedFigure = await onRequestIndexedFigure()
|
| 100 |
+
setShowPointIndices(Boolean(loadedFigure))
|
| 101 |
+
} catch {
|
| 102 |
+
setShowPointIndices(false)
|
| 103 |
+
} finally {
|
| 104 |
+
setLoadingIndexedFigure(false)
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
const toggleTitle = loadingIndexedFigure
|
| 109 |
+
? 'Carregando índices dos pontos...'
|
| 110 |
+
: hasIndexedFigure
|
| 111 |
+
? 'Exibir índices dos pontos'
|
| 112 |
+
: canRequestIndexedFigure
|
| 113 |
+
? 'Carregar índices dos pontos sob demanda'
|
| 114 |
+
: 'Este gráfico não trouxe uma versão com índices.'
|
| 115 |
+
|
| 116 |
return (
|
| 117 |
<div ref={containerRef} className={cardClassName}>
|
| 118 |
{title || subtitle ? (
|
| 119 |
<div className="plot-card-head">
|
| 120 |
{title ? <h4 className="plot-card-title">{title}</h4> : null}
|
| 121 |
{subtitle ? <div className="plot-card-subtitle">{subtitle}</div> : null}
|
| 122 |
+
{showPointIndexToggle ? (
|
| 123 |
+
<label className={`plot-card-toggle${canToggleIndices ? '' : ' is-disabled'}`}>
|
| 124 |
+
<input
|
| 125 |
+
type="checkbox"
|
| 126 |
+
checked={showPointIndices}
|
| 127 |
+
disabled={!canToggleIndices || loadingIndexedFigure}
|
| 128 |
+
onChange={handleToggleChange}
|
| 129 |
+
title={toggleTitle}
|
| 130 |
+
/>
|
| 131 |
+
{loadingIndexedFigure ? 'Carregando índices...' : 'Exibir índices dos pontos'}
|
| 132 |
+
</label>
|
| 133 |
+
) : null}
|
| 134 |
</div>
|
| 135 |
) : null}
|
| 136 |
{shouldRenderPlot ? (
|
| 137 |
+
<div className="plot-card-body">
|
| 138 |
+
<Plot
|
| 139 |
+
data={data}
|
| 140 |
+
layout={layout}
|
| 141 |
+
revision={showPointIndices && hasIndexedFigure ? 1 : 0}
|
| 142 |
+
config={{ responsive: true, displaylogo: false }}
|
| 143 |
+
style={{ width: '100%', height: plotHeight, minHeight: '320px' }}
|
| 144 |
+
useResizeHandler
|
| 145 |
+
plotly={Plotly}
|
| 146 |
+
/>
|
| 147 |
+
</div>
|
| 148 |
) : (
|
| 149 |
<div className="plot-lazy-placeholder">Carregando gráfico...</div>
|
| 150 |
)}
|
|
|
|
| 152 |
)
|
| 153 |
}
|
| 154 |
|
| 155 |
+
export default PlotFigure
|
frontend/src/components/RepositorioTab.jsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import React, { useEffect, useRef, useState } from 'react'
|
| 2 |
import { api, downloadBlob } from '../api'
|
|
|
|
| 3 |
import DataTable from './DataTable'
|
| 4 |
import EquationFormatsPanel from './EquationFormatsPanel'
|
| 5 |
import ListPagination from './ListPagination'
|
|
@@ -7,6 +8,7 @@ import LoadingOverlay from './LoadingOverlay'
|
|
| 7 |
import MapFrame from './MapFrame'
|
| 8 |
import ModeloTrabalhosTecnicosPanel from './ModeloTrabalhosTecnicosPanel'
|
| 9 |
import PlotFigure from './PlotFigure'
|
|
|
|
| 10 |
import TruncatedCellContent from './TruncatedCellContent'
|
| 11 |
import { getFaixaDataRecencyInfo } from '../modelRecency'
|
| 12 |
|
|
@@ -24,6 +26,54 @@ const REPO_INNER_TABS = [
|
|
| 24 |
{ key: 'graficos', label: 'Gráficos' },
|
| 25 |
]
|
| 26 |
const MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO = 'selecionados_e_outras_versoes'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
function formatarFonte(fonte) {
|
| 29 |
if (!fonte || typeof fonte !== 'object') return 'Fonte não informada'
|
|
@@ -37,7 +87,60 @@ function formatarFonte(fonte) {
|
|
| 37 |
return 'Pasta local'
|
| 38 |
}
|
| 39 |
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
const [modelos, setModelos] = useState([])
|
| 42 |
const [fonte, setFonte] = useState(null)
|
| 43 |
const [loading, setLoading] = useState(false)
|
|
@@ -57,6 +160,7 @@ export default function RepositorioTab({ authUser, sessionId, openModeloRequest
|
|
| 57 |
})
|
| 58 |
|
| 59 |
const [modeloAbertoMeta, setModeloAbertoMeta] = useState(null)
|
|
|
|
| 60 |
const [modeloAbertoLoading, setModeloAbertoLoading] = useState(false)
|
| 61 |
const [modeloAbertoError, setModeloAbertoError] = useState('')
|
| 62 |
const [modeloAbertoActiveTab, setModeloAbertoActiveTab] = useState('mapa')
|
|
@@ -78,7 +182,11 @@ export default function RepositorioTab({ authUser, sessionId, openModeloRequest
|
|
| 78 |
const [modeloAbertoPlotHistograma, setModeloAbertoPlotHistograma] = useState(null)
|
| 79 |
const [modeloAbertoPlotCook, setModeloAbertoPlotCook] = useState(null)
|
| 80 |
const [modeloAbertoPlotCorr, setModeloAbertoPlotCorr] = useState(null)
|
|
|
|
|
|
|
| 81 |
const lastOpenRequestKeyRef = useRef('')
|
|
|
|
|
|
|
| 82 |
|
| 83 |
const isAdmin = String(authUser?.perfil || '').toLowerCase() === 'admin'
|
| 84 |
const totalModelos = modelos.length
|
|
@@ -92,6 +200,12 @@ export default function RepositorioTab({ authUser, sessionId, openModeloRequest
|
|
| 92 |
void carregarModelos()
|
| 93 |
}, [])
|
| 94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
useEffect(() => {
|
| 96 |
const totalPages = Math.max(1, Math.ceil(totalModelos / PAGE_SIZE))
|
| 97 |
setListaPage((prev) => Math.min(Math.max(1, prev), totalPages))
|
|
@@ -103,20 +217,30 @@ export default function RepositorioTab({ authUser, sessionId, openModeloRequest
|
|
| 103 |
if (!sessionId || !requestKey || !modeloId) return
|
| 104 |
if (lastOpenRequestKeyRef.current === requestKey) return
|
| 105 |
lastOpenRequestKeyRef.current = requestKey
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
})
|
| 111 |
-
setModeloAbertoError('')
|
| 112 |
-
setModeloAbertoActiveTab('mapa')
|
| 113 |
void onAbrirModelo({
|
| 114 |
id: modeloId,
|
| 115 |
nome_modelo: openModeloRequest?.nomeModelo || modeloId,
|
| 116 |
arquivo: openModeloRequest?.modeloArquivo || '',
|
|
|
|
|
|
|
|
|
|
| 117 |
})
|
| 118 |
}, [openModeloRequest, sessionId])
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
async function carregarModelos() {
|
| 121 |
setLoading(true)
|
| 122 |
setError('')
|
|
@@ -213,55 +337,208 @@ export default function RepositorioTab({ authUser, sessionId, openModeloRequest
|
|
| 213 |
await onExcluirModelo(modeloId)
|
| 214 |
}
|
| 215 |
|
| 216 |
-
function
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
|
|
|
| 227 |
setModeloAbertoMapaVar('Visualização Padrão')
|
| 228 |
-
setModeloAbertoTrabalhosTecnicosModelosModo(
|
| 229 |
-
setModeloAbertoTrabalhosTecnicos(
|
| 230 |
-
setModeloAbertoPlotObsCalc(
|
| 231 |
-
setModeloAbertoPlotResiduos(
|
| 232 |
-
setModeloAbertoPlotHistograma(
|
| 233 |
-
setModeloAbertoPlotCook(
|
| 234 |
-
setModeloAbertoPlotCorr(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 235 |
}
|
| 236 |
|
| 237 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
if (!sessionId) {
|
| 239 |
setError('Sessão indisponível no momento. Aguarde e tente novamente.')
|
| 240 |
return
|
| 241 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
setModeloAbertoLoading(true)
|
| 243 |
setModeloAbertoError('')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
try {
|
| 245 |
-
await api.visualizacaoRepositorioCarregar(sessionId, String(item?.id || ''))
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
|
|
|
|
|
|
| 253 |
})
|
| 254 |
} catch (err) {
|
|
|
|
| 255 |
setModeloAbertoError(err.message || 'Falha ao abrir modelo.')
|
| 256 |
} finally {
|
| 257 |
-
|
|
|
|
|
|
|
| 258 |
}
|
| 259 |
}
|
| 260 |
|
| 261 |
function onVoltarRepositorio() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
setModeloAbertoMeta(null)
|
|
|
|
| 263 |
setModeloAbertoError('')
|
| 264 |
setModeloAbertoActiveTab('mapa')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 265 |
}
|
| 266 |
|
| 267 |
async function atualizarMapaModeloAberto(
|
|
@@ -269,10 +546,16 @@ export default function RepositorioTab({ authUser, sessionId, openModeloRequest
|
|
| 269 |
nextTrabalhosTecnicosModo = modeloAbertoTrabalhosTecnicosModelosModo,
|
| 270 |
) {
|
| 271 |
if (!sessionId) return
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
}
|
| 277 |
|
| 278 |
async function onModeloAbertoMapChange(nextVar) {
|
|
@@ -307,7 +590,28 @@ export default function RepositorioTab({ authUser, sessionId, openModeloRequest
|
|
| 307 |
}
|
| 308 |
}
|
| 309 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
if (modoModeloAberto) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
return (
|
| 312 |
<div className="tab-content">
|
| 313 |
<div className="pesquisa-opened-model-view">
|
|
@@ -316,9 +620,32 @@ export default function RepositorioTab({ authUser, sessionId, openModeloRequest
|
|
| 316 |
<h3>{modeloAbertoMeta?.nome || 'Modelo'}</h3>
|
| 317 |
{modeloAbertoMeta?.observacao ? <p>{modeloAbertoMeta.observacao}</p> : null}
|
| 318 |
</div>
|
| 319 |
-
<
|
| 320 |
-
|
| 321 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 322 |
</div>
|
| 323 |
|
| 324 |
<div className="inner-tabs" role="tablist" aria-label="Abas internas do modelo aberto no repositório">
|
|
@@ -327,7 +654,8 @@ export default function RepositorioTab({ authUser, sessionId, openModeloRequest
|
|
| 327 |
key={tab.key}
|
| 328 |
type="button"
|
| 329 |
className={modeloAbertoActiveTab === tab.key ? 'inner-tab-pill active' : 'inner-tab-pill'}
|
| 330 |
-
onClick={() =>
|
|
|
|
| 331 |
>
|
| 332 |
{tab.label}
|
| 333 |
</button>
|
|
@@ -336,84 +664,110 @@ export default function RepositorioTab({ authUser, sessionId, openModeloRequest
|
|
| 336 |
|
| 337 |
<div className="inner-tab-panel">
|
| 338 |
{modeloAbertoActiveTab === 'mapa' ? (
|
| 339 |
-
|
| 340 |
-
<div className="
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
))}
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
<
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
) : null}
|
| 366 |
|
| 367 |
{modeloAbertoActiveTab === 'trabalhos_tecnicos' ? (
|
| 368 |
-
|
|
|
|
|
|
|
| 369 |
) : null}
|
| 370 |
|
| 371 |
-
{modeloAbertoActiveTab === 'dados_mercado'
|
| 372 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
|
| 374 |
{modeloAbertoActiveTab === 'transformacoes' ? (
|
| 375 |
-
|
| 376 |
-
<
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
) : null}
|
| 381 |
|
| 382 |
{modeloAbertoActiveTab === 'resumo' ? (
|
| 383 |
-
|
| 384 |
-
<
|
| 385 |
-
<
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 394 |
) : null}
|
| 395 |
|
| 396 |
-
{modeloAbertoActiveTab === 'coeficientes'
|
| 397 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
|
| 399 |
{modeloAbertoActiveTab === 'graficos' ? (
|
| 400 |
-
|
| 401 |
-
<
|
| 402 |
-
<
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
<
|
| 409 |
-
|
| 410 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 411 |
) : null}
|
| 412 |
</div>
|
| 413 |
|
| 414 |
{modeloAbertoError ? <div className="error-line inline-error">{modeloAbertoError}</div> : null}
|
| 415 |
</div>
|
| 416 |
-
<LoadingOverlay show={modeloAbertoLoading} label=
|
| 417 |
</div>
|
| 418 |
)
|
| 419 |
}
|
|
|
|
| 1 |
import React, { useEffect, useRef, useState } from 'react'
|
| 2 |
import { api, downloadBlob } from '../api'
|
| 3 |
+
import { buildRepositorioModeloLink, hasMesaDeepLink, normalizeMesaDeepLink, parseMesaDeepLink } from '../deepLinks'
|
| 4 |
import DataTable from './DataTable'
|
| 5 |
import EquationFormatsPanel from './EquationFormatsPanel'
|
| 6 |
import ListPagination from './ListPagination'
|
|
|
|
| 8 |
import MapFrame from './MapFrame'
|
| 9 |
import ModeloTrabalhosTecnicosPanel from './ModeloTrabalhosTecnicosPanel'
|
| 10 |
import PlotFigure from './PlotFigure'
|
| 11 |
+
import ShareLinkButton from './ShareLinkButton'
|
| 12 |
import TruncatedCellContent from './TruncatedCellContent'
|
| 13 |
import { getFaixaDataRecencyInfo } from '../modelRecency'
|
| 14 |
|
|
|
|
| 26 |
{ key: 'graficos', label: 'Gráficos' },
|
| 27 |
]
|
| 28 |
const MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO = 'selecionados_e_outras_versoes'
|
| 29 |
+
const REPO_MODEL_TAB_KEY_BY_SLUG = {
|
| 30 |
+
mapa: 'mapa',
|
| 31 |
+
'trabalhos-tecnicos': 'trabalhos_tecnicos',
|
| 32 |
+
'dados-mercado': 'dados_mercado',
|
| 33 |
+
metricas: 'metricas',
|
| 34 |
+
transformacoes: 'transformacoes',
|
| 35 |
+
resumo: 'resumo',
|
| 36 |
+
coeficientes: 'coeficientes',
|
| 37 |
+
'obs-calc': 'obs_calc',
|
| 38 |
+
graficos: 'graficos',
|
| 39 |
+
}
|
| 40 |
+
const REPO_MODEL_TAB_SLUG_BY_KEY = Object.fromEntries(
|
| 41 |
+
Object.entries(REPO_MODEL_TAB_KEY_BY_SLUG).map(([slug, key]) => [key, slug]),
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
function getRepoModelTabKeyFromSlug(slug) {
|
| 45 |
+
return REPO_MODEL_TAB_KEY_BY_SLUG[String(slug || '').trim()] || 'mapa'
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
function getRepoModelTabSlugFromKey(key) {
|
| 49 |
+
return REPO_MODEL_TAB_SLUG_BY_KEY[String(key || '').trim()] || 'mapa'
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
function scrollParaTopoDaPagina() {
|
| 53 |
+
if (typeof window === 'undefined') return
|
| 54 |
+
window.requestAnimationFrame(() => {
|
| 55 |
+
window.requestAnimationFrame(() => {
|
| 56 |
+
window.scrollTo({ top: 0, behavior: 'smooth' })
|
| 57 |
+
})
|
| 58 |
+
})
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
function buildReturnIntent(value) {
|
| 62 |
+
const normalized = normalizeMesaDeepLink(value)
|
| 63 |
+
if (!value || typeof value !== 'object') return normalized
|
| 64 |
+
return {
|
| 65 |
+
...normalized,
|
| 66 |
+
pesquisaExecutada: Boolean(value.pesquisaExecutada),
|
| 67 |
+
avaliandos: Array.isArray(value.avaliandos) ? value.avaliandos : null,
|
| 68 |
+
scrollToMapa: Boolean(value.scrollToMapa),
|
| 69 |
+
}
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
function resolveRepoModelTabKey(value) {
|
| 73 |
+
const text = String(value || '').trim()
|
| 74 |
+
if (REPO_MODEL_TAB_SLUG_BY_KEY[text]) return text
|
| 75 |
+
return getRepoModelTabKeyFromSlug(text)
|
| 76 |
+
}
|
| 77 |
|
| 78 |
function formatarFonte(fonte) {
|
| 79 |
if (!fonte || typeof fonte !== 'object') return 'Fonte não informada'
|
|
|
|
| 87 |
return 'Pasta local'
|
| 88 |
}
|
| 89 |
|
| 90 |
+
function getReturnButtonLabel(intent) {
|
| 91 |
+
if (!intent || typeof intent !== 'object') {
|
| 92 |
+
return 'Voltar ao repositório'
|
| 93 |
+
}
|
| 94 |
+
const normalized = normalizeMesaDeepLink(intent)
|
| 95 |
+
if (normalized.tab === 'modelos' && normalized.subtab === 'pesquisa') {
|
| 96 |
+
return 'Voltar à pesquisa'
|
| 97 |
+
}
|
| 98 |
+
if (normalized.tab === 'trabalhos') {
|
| 99 |
+
return normalized.trabalhoId ? 'Voltar ao trabalho técnico' : 'Voltar aos trabalhos técnicos'
|
| 100 |
+
}
|
| 101 |
+
if (normalized.tab === 'avaliacao') {
|
| 102 |
+
return 'Voltar à avaliação'
|
| 103 |
+
}
|
| 104 |
+
if (normalized.tab === 'elaboracao') {
|
| 105 |
+
return 'Voltar à elaboração'
|
| 106 |
+
}
|
| 107 |
+
return 'Voltar ao repositório'
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
function getModeloAbertoLoadingLabel(tabKey) {
|
| 111 |
+
switch (resolveRepoModelTabKey(tabKey)) {
|
| 112 |
+
case 'mapa':
|
| 113 |
+
return 'Carregando mapa do modelo...'
|
| 114 |
+
case 'trabalhos_tecnicos':
|
| 115 |
+
return 'Carregando trabalhos técnicos do modelo...'
|
| 116 |
+
case 'dados_mercado':
|
| 117 |
+
return 'Carregando dados de mercado...'
|
| 118 |
+
case 'metricas':
|
| 119 |
+
return 'Carregando métricas do modelo...'
|
| 120 |
+
case 'transformacoes':
|
| 121 |
+
return 'Carregando transformações do modelo...'
|
| 122 |
+
case 'resumo':
|
| 123 |
+
return 'Carregando resumo do modelo...'
|
| 124 |
+
case 'coeficientes':
|
| 125 |
+
return 'Carregando coeficientes do modelo...'
|
| 126 |
+
case 'obs_calc':
|
| 127 |
+
return 'Carregando observados x calculados...'
|
| 128 |
+
case 'graficos':
|
| 129 |
+
return 'Carregando gráficos do modelo...'
|
| 130 |
+
default:
|
| 131 |
+
return 'Carregando modelo...'
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
export default function RepositorioTab({
|
| 136 |
+
authUser,
|
| 137 |
+
sessionId,
|
| 138 |
+
openModeloRequest = null,
|
| 139 |
+
onUsarModeloEmAvaliacao = null,
|
| 140 |
+
onEditarModeloEmElaboracao = null,
|
| 141 |
+
onRouteChange = null,
|
| 142 |
+
onModeloAbertoChange = null,
|
| 143 |
+
}) {
|
| 144 |
const [modelos, setModelos] = useState([])
|
| 145 |
const [fonte, setFonte] = useState(null)
|
| 146 |
const [loading, setLoading] = useState(false)
|
|
|
|
| 160 |
})
|
| 161 |
|
| 162 |
const [modeloAbertoMeta, setModeloAbertoMeta] = useState(null)
|
| 163 |
+
const [modeloAbertoReturnIntent, setModeloAbertoReturnIntent] = useState(null)
|
| 164 |
const [modeloAbertoLoading, setModeloAbertoLoading] = useState(false)
|
| 165 |
const [modeloAbertoError, setModeloAbertoError] = useState('')
|
| 166 |
const [modeloAbertoActiveTab, setModeloAbertoActiveTab] = useState('mapa')
|
|
|
|
| 182 |
const [modeloAbertoPlotHistograma, setModeloAbertoPlotHistograma] = useState(null)
|
| 183 |
const [modeloAbertoPlotCook, setModeloAbertoPlotCook] = useState(null)
|
| 184 |
const [modeloAbertoPlotCorr, setModeloAbertoPlotCorr] = useState(null)
|
| 185 |
+
const [modeloAbertoLoadedTabs, setModeloAbertoLoadedTabs] = useState({})
|
| 186 |
+
const [modeloAbertoLoadingTabs, setModeloAbertoLoadingTabs] = useState({})
|
| 187 |
const lastOpenRequestKeyRef = useRef('')
|
| 188 |
+
const pendingTabRequestsRef = useRef({})
|
| 189 |
+
const modeloOpenVersionRef = useRef(0)
|
| 190 |
|
| 191 |
const isAdmin = String(authUser?.perfil || '').toLowerCase() === 'admin'
|
| 192 |
const totalModelos = modelos.length
|
|
|
|
| 200 |
void carregarModelos()
|
| 201 |
}, [])
|
| 202 |
|
| 203 |
+
useEffect(() => {
|
| 204 |
+
if (typeof onModeloAbertoChange !== 'function') return undefined
|
| 205 |
+
onModeloAbertoChange(modoModeloAberto)
|
| 206 |
+
return () => onModeloAbertoChange(false)
|
| 207 |
+
}, [modoModeloAberto, onModeloAbertoChange])
|
| 208 |
+
|
| 209 |
useEffect(() => {
|
| 210 |
const totalPages = Math.max(1, Math.ceil(totalModelos / PAGE_SIZE))
|
| 211 |
setListaPage((prev) => Math.min(Math.max(1, prev), totalPages))
|
|
|
|
| 217 |
if (!sessionId || !requestKey || !modeloId) return
|
| 218 |
if (lastOpenRequestKeyRef.current === requestKey) return
|
| 219 |
lastOpenRequestKeyRef.current = requestKey
|
| 220 |
+
const nextModelTab = resolveRepoModelTabKey(openModeloRequest?.modelTab)
|
| 221 |
+
const nextReturnIntent = buildReturnIntent(
|
| 222 |
+
openModeloRequest?.returnIntent || { tab: 'modelos', subtab: 'repositorio' },
|
| 223 |
+
)
|
|
|
|
|
|
|
|
|
|
| 224 |
void onAbrirModelo({
|
| 225 |
id: modeloId,
|
| 226 |
nome_modelo: openModeloRequest?.nomeModelo || modeloId,
|
| 227 |
arquivo: openModeloRequest?.modeloArquivo || '',
|
| 228 |
+
}, {
|
| 229 |
+
initialModelTab: nextModelTab,
|
| 230 |
+
returnIntent: nextReturnIntent,
|
| 231 |
})
|
| 232 |
}, [openModeloRequest, sessionId])
|
| 233 |
|
| 234 |
+
function resolveCurrentReturnIntent() {
|
| 235 |
+
if (typeof window !== 'undefined') {
|
| 236 |
+
const parsed = parseMesaDeepLink(window.location.search)
|
| 237 |
+
if (hasMesaDeepLink(parsed)) {
|
| 238 |
+
return normalizeMesaDeepLink(parsed)
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
return normalizeMesaDeepLink({ tab: 'modelos', subtab: 'repositorio' })
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
async function carregarModelos() {
|
| 245 |
setLoading(true)
|
| 246 |
setError('')
|
|
|
|
| 337 |
await onExcluirModelo(modeloId)
|
| 338 |
}
|
| 339 |
|
| 340 |
+
function limparConteudoModeloAberto() {
|
| 341 |
+
pendingTabRequestsRef.current = {}
|
| 342 |
+
setModeloAbertoDados(null)
|
| 343 |
+
setModeloAbertoEstatisticas(null)
|
| 344 |
+
setModeloAbertoEscalasHtml('')
|
| 345 |
+
setModeloAbertoDadosTransformados(null)
|
| 346 |
+
setModeloAbertoResumoHtml('')
|
| 347 |
+
setModeloAbertoEquacoes(null)
|
| 348 |
+
setModeloAbertoCoeficientes(null)
|
| 349 |
+
setModeloAbertoObsCalc(null)
|
| 350 |
+
setModeloAbertoMapaHtml('')
|
| 351 |
+
setModeloAbertoMapaChoices(['Visualização Padrão'])
|
| 352 |
setModeloAbertoMapaVar('Visualização Padrão')
|
| 353 |
+
setModeloAbertoTrabalhosTecnicosModelosModo(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
| 354 |
+
setModeloAbertoTrabalhosTecnicos([])
|
| 355 |
+
setModeloAbertoPlotObsCalc(null)
|
| 356 |
+
setModeloAbertoPlotResiduos(null)
|
| 357 |
+
setModeloAbertoPlotHistograma(null)
|
| 358 |
+
setModeloAbertoPlotCook(null)
|
| 359 |
+
setModeloAbertoPlotCorr(null)
|
| 360 |
+
setModeloAbertoLoadedTabs({})
|
| 361 |
+
setModeloAbertoLoadingTabs({})
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
function aplicarSecaoModeloAberto(secao, resp) {
|
| 365 |
+
const secaoNormalizada = resolveRepoModelTabKey(secao)
|
| 366 |
+
if (secaoNormalizada === 'dados_mercado') {
|
| 367 |
+
setModeloAbertoDados(resp?.dados || null)
|
| 368 |
+
return
|
| 369 |
+
}
|
| 370 |
+
if (secaoNormalizada === 'metricas') {
|
| 371 |
+
setModeloAbertoEstatisticas(resp?.estatisticas || null)
|
| 372 |
+
return
|
| 373 |
+
}
|
| 374 |
+
if (secaoNormalizada === 'transformacoes') {
|
| 375 |
+
setModeloAbertoEscalasHtml(resp?.escalas_html || '')
|
| 376 |
+
setModeloAbertoDadosTransformados(resp?.dados_transformados || null)
|
| 377 |
+
return
|
| 378 |
+
}
|
| 379 |
+
if (secaoNormalizada === 'resumo') {
|
| 380 |
+
setModeloAbertoResumoHtml(resp?.resumo_html || '')
|
| 381 |
+
setModeloAbertoEquacoes(resp?.equacoes || null)
|
| 382 |
+
return
|
| 383 |
+
}
|
| 384 |
+
if (secaoNormalizada === 'coeficientes') {
|
| 385 |
+
setModeloAbertoCoeficientes(resp?.coeficientes || null)
|
| 386 |
+
return
|
| 387 |
+
}
|
| 388 |
+
if (secaoNormalizada === 'obs_calc') {
|
| 389 |
+
setModeloAbertoObsCalc(resp?.obs_calc || null)
|
| 390 |
+
return
|
| 391 |
+
}
|
| 392 |
+
if (secaoNormalizada === 'graficos') {
|
| 393 |
+
setModeloAbertoPlotObsCalc(resp?.grafico_obs_calc || null)
|
| 394 |
+
setModeloAbertoPlotResiduos(resp?.grafico_residuos || null)
|
| 395 |
+
setModeloAbertoPlotHistograma(resp?.grafico_histograma || null)
|
| 396 |
+
setModeloAbertoPlotCook(resp?.grafico_cook || null)
|
| 397 |
+
setModeloAbertoPlotCorr(resp?.grafico_correlacao || null)
|
| 398 |
+
return
|
| 399 |
+
}
|
| 400 |
+
if (secaoNormalizada === 'trabalhos_tecnicos') {
|
| 401 |
+
setModeloAbertoTrabalhosTecnicos(Array.isArray(resp?.trabalhos_tecnicos) ? resp.trabalhos_tecnicos : [])
|
| 402 |
+
setModeloAbertoTrabalhosTecnicosModelosModo(resp?.trabalhos_tecnicos_modelos_modo || MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
| 403 |
+
return
|
| 404 |
+
}
|
| 405 |
+
if (secaoNormalizada === 'mapa') {
|
| 406 |
+
const nextChoices = Array.isArray(resp?.mapa_choices) && resp.mapa_choices.length
|
| 407 |
+
? resp.mapa_choices
|
| 408 |
+
: ['Visualização Padrão']
|
| 409 |
+
setModeloAbertoMapaHtml(resp?.mapa_html || '')
|
| 410 |
+
setModeloAbertoMapaChoices(nextChoices)
|
| 411 |
+
setModeloAbertoMapaVar((current) => (nextChoices.includes(current) ? current : 'Visualização Padrão'))
|
| 412 |
+
setModeloAbertoTrabalhosTecnicos(Array.isArray(resp?.trabalhos_tecnicos) ? resp.trabalhos_tecnicos : [])
|
| 413 |
+
setModeloAbertoTrabalhosTecnicosModelosModo(resp?.trabalhos_tecnicos_modelos_modo || MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
|
| 414 |
+
}
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
async function garantirSecaoModeloAberto(secao, options = {}) {
|
| 418 |
+
const secaoNormalizada = resolveRepoModelTabKey(secao)
|
| 419 |
+
const modeloId = String(options.modeloId || modeloAbertoMeta?.id || '').trim()
|
| 420 |
+
if (!sessionId || !modeloId) return
|
| 421 |
+
if (!options.force && modeloAbertoLoadedTabs[secaoNormalizada]) return
|
| 422 |
+
if (pendingTabRequestsRef.current[secaoNormalizada]) {
|
| 423 |
+
await pendingTabRequestsRef.current[secaoNormalizada]
|
| 424 |
+
return
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
const expectedVersion = options.expectedVersion ?? modeloOpenVersionRef.current
|
| 428 |
+
const trabalhosTecnicosModo = options.trabalhosTecnicosModelosModo || modeloAbertoTrabalhosTecnicosModelosModo
|
| 429 |
+
setModeloAbertoLoadingTabs((prev) => ({ ...prev, [secaoNormalizada]: true }))
|
| 430 |
+
|
| 431 |
+
const request = (async () => {
|
| 432 |
+
try {
|
| 433 |
+
const resp = await api.visualizacaoSection(
|
| 434 |
+
sessionId,
|
| 435 |
+
getRepoModelTabSlugFromKey(secaoNormalizada),
|
| 436 |
+
trabalhosTecnicosModo,
|
| 437 |
+
)
|
| 438 |
+
if (modeloOpenVersionRef.current !== expectedVersion) return
|
| 439 |
+
aplicarSecaoModeloAberto(secaoNormalizada, resp)
|
| 440 |
+
setModeloAbertoLoadedTabs((prev) => ({
|
| 441 |
+
...prev,
|
| 442 |
+
[secaoNormalizada]: true,
|
| 443 |
+
...(secaoNormalizada === 'mapa' ? { trabalhos_tecnicos: true } : {}),
|
| 444 |
+
}))
|
| 445 |
+
} catch (err) {
|
| 446 |
+
if (modeloOpenVersionRef.current !== expectedVersion) return
|
| 447 |
+
setModeloAbertoError(err.message || 'Falha ao carregar dados do modelo.')
|
| 448 |
+
} finally {
|
| 449 |
+
if (modeloOpenVersionRef.current !== expectedVersion) return
|
| 450 |
+
setModeloAbertoLoadingTabs((prev) => ({ ...prev, [secaoNormalizada]: false }))
|
| 451 |
+
}
|
| 452 |
+
})()
|
| 453 |
+
|
| 454 |
+
pendingTabRequestsRef.current[secaoNormalizada] = request
|
| 455 |
+
try {
|
| 456 |
+
await request
|
| 457 |
+
} finally {
|
| 458 |
+
if (pendingTabRequestsRef.current[secaoNormalizada] === request) {
|
| 459 |
+
delete pendingTabRequestsRef.current[secaoNormalizada]
|
| 460 |
+
}
|
| 461 |
+
}
|
| 462 |
}
|
| 463 |
|
| 464 |
+
function emitOpenedModelRoute(meta = modeloAbertoMeta, activeTab = modeloAbertoActiveTab) {
|
| 465 |
+
const modeloId = String(meta?.id || '').trim()
|
| 466 |
+
if (!modeloId || typeof onRouteChange !== 'function') return
|
| 467 |
+
onRouteChange({
|
| 468 |
+
tab: 'modelos',
|
| 469 |
+
subtab: 'repositorio',
|
| 470 |
+
modeloId,
|
| 471 |
+
modelTab: getRepoModelTabSlugFromKey(activeTab),
|
| 472 |
+
})
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
async function onAbrirModelo(item, options = {}) {
|
| 476 |
if (!sessionId) {
|
| 477 |
setError('Sessão indisponível no momento. Aguarde e tente novamente.')
|
| 478 |
return
|
| 479 |
}
|
| 480 |
+
const nextModelTab = resolveRepoModelTabKey(options?.initialModelTab)
|
| 481 |
+
const nextReturnIntent = buildReturnIntent(options?.returnIntent || resolveCurrentReturnIntent())
|
| 482 |
+
const nextMeta = {
|
| 483 |
+
id: String(item?.id || ''),
|
| 484 |
+
nome: item?.nome_modelo || item?.arquivo || String(item?.id || ''),
|
| 485 |
+
arquivo: String(item?.arquivo || '').trim(),
|
| 486 |
+
observacao: '',
|
| 487 |
+
}
|
| 488 |
+
modeloOpenVersionRef.current += 1
|
| 489 |
+
const openVersion = modeloOpenVersionRef.current
|
| 490 |
+
limparConteudoModeloAberto()
|
| 491 |
setModeloAbertoLoading(true)
|
| 492 |
setModeloAbertoError('')
|
| 493 |
+
setModeloAbertoMeta(nextMeta)
|
| 494 |
+
setModeloAbertoActiveTab(nextModelTab)
|
| 495 |
+
setModeloAbertoReturnIntent(nextReturnIntent)
|
| 496 |
+
scrollParaTopoDaPagina()
|
| 497 |
+
emitOpenedModelRoute(nextMeta, nextModelTab)
|
| 498 |
try {
|
| 499 |
+
const resp = await api.visualizacaoRepositorioCarregar(sessionId, String(item?.id || ''))
|
| 500 |
+
if (modeloOpenVersionRef.current !== openVersion) return
|
| 501 |
+
setModeloAbertoMeta((prev) => (
|
| 502 |
+
prev ? { ...prev, observacao: String(resp?.observacao_modelo || '').trim() } : prev
|
| 503 |
+
))
|
| 504 |
+
await garantirSecaoModeloAberto(nextModelTab, {
|
| 505 |
+
force: true,
|
| 506 |
+
expectedVersion: openVersion,
|
| 507 |
+
modeloId: nextMeta.id,
|
| 508 |
+
trabalhosTecnicosModelosModo: MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO,
|
| 509 |
})
|
| 510 |
} catch (err) {
|
| 511 |
+
if (modeloOpenVersionRef.current !== openVersion) return
|
| 512 |
setModeloAbertoError(err.message || 'Falha ao abrir modelo.')
|
| 513 |
} finally {
|
| 514 |
+
if (modeloOpenVersionRef.current === openVersion) {
|
| 515 |
+
setModeloAbertoLoading(false)
|
| 516 |
+
}
|
| 517 |
}
|
| 518 |
}
|
| 519 |
|
| 520 |
function onVoltarRepositorio() {
|
| 521 |
+
const nextIntent = hasMesaDeepLink(modeloAbertoReturnIntent)
|
| 522 |
+
? {
|
| 523 |
+
...modeloAbertoReturnIntent,
|
| 524 |
+
forceRouteRequest: true,
|
| 525 |
+
}
|
| 526 |
+
: {
|
| 527 |
+
tab: 'modelos',
|
| 528 |
+
subtab: 'repositorio',
|
| 529 |
+
forceRouteRequest: true,
|
| 530 |
+
}
|
| 531 |
+
modeloOpenVersionRef.current += 1
|
| 532 |
+
pendingTabRequestsRef.current = {}
|
| 533 |
setModeloAbertoMeta(null)
|
| 534 |
+
setModeloAbertoReturnIntent(null)
|
| 535 |
setModeloAbertoError('')
|
| 536 |
setModeloAbertoActiveTab('mapa')
|
| 537 |
+
setModeloAbertoLoading(false)
|
| 538 |
+
limparConteudoModeloAberto()
|
| 539 |
+
if (typeof onRouteChange === 'function') {
|
| 540 |
+
onRouteChange(nextIntent)
|
| 541 |
+
}
|
| 542 |
}
|
| 543 |
|
| 544 |
async function atualizarMapaModeloAberto(
|
|
|
|
| 546 |
nextTrabalhosTecnicosModo = modeloAbertoTrabalhosTecnicosModelosModo,
|
| 547 |
) {
|
| 548 |
if (!sessionId) return
|
| 549 |
+
setModeloAbertoLoadingTabs((prev) => ({ ...prev, mapa: true }))
|
| 550 |
+
try {
|
| 551 |
+
const resp = await api.updateVisualizacaoMap(sessionId, nextVar, nextTrabalhosTecnicosModo)
|
| 552 |
+
setModeloAbertoMapaHtml(resp?.mapa_html || '')
|
| 553 |
+
setModeloAbertoTrabalhosTecnicos(Array.isArray(resp?.trabalhos_tecnicos) ? resp.trabalhos_tecnicos : [])
|
| 554 |
+
setModeloAbertoTrabalhosTecnicosModelosModo(resp?.trabalhos_tecnicos_modelos_modo || nextTrabalhosTecnicosModo)
|
| 555 |
+
setModeloAbertoLoadedTabs((prev) => ({ ...prev, mapa: true, trabalhos_tecnicos: true }))
|
| 556 |
+
} finally {
|
| 557 |
+
setModeloAbertoLoadingTabs((prev) => ({ ...prev, mapa: false }))
|
| 558 |
+
}
|
| 559 |
}
|
| 560 |
|
| 561 |
async function onModeloAbertoMapChange(nextVar) {
|
|
|
|
| 590 |
}
|
| 591 |
}
|
| 592 |
|
| 593 |
+
function onModeloAbertoTabSelect(nextTab) {
|
| 594 |
+
setModeloAbertoActiveTab(nextTab)
|
| 595 |
+
emitOpenedModelRoute(modeloAbertoMeta, nextTab)
|
| 596 |
+
void garantirSecaoModeloAberto(nextTab)
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
if (modoModeloAberto) {
|
| 600 |
+
const shareHref = buildRepositorioModeloLink(
|
| 601 |
+
modeloAbertoMeta?.id || '',
|
| 602 |
+
getRepoModelTabSlugFromKey(modeloAbertoActiveTab),
|
| 603 |
+
)
|
| 604 |
+
const modeloAcao = {
|
| 605 |
+
id: String(modeloAbertoMeta?.id || '').trim(),
|
| 606 |
+
nome_modelo: String(modeloAbertoMeta?.nome || '').trim(),
|
| 607 |
+
arquivo: String(modeloAbertoMeta?.arquivo || '').trim(),
|
| 608 |
+
}
|
| 609 |
+
const returnButtonLabel = getReturnButtonLabel(modeloAbertoReturnIntent)
|
| 610 |
+
const activeTabLoading = Boolean(modeloAbertoLoadingTabs[modeloAbertoActiveTab])
|
| 611 |
+
const activeTabLoaded = Boolean(modeloAbertoLoadedTabs[modeloAbertoActiveTab])
|
| 612 |
+
const modeloLoadingLabel = modeloAbertoLoading
|
| 613 |
+
? 'Abrindo modelo...'
|
| 614 |
+
: getModeloAbertoLoadingLabel(modeloAbertoActiveTab)
|
| 615 |
return (
|
| 616 |
<div className="tab-content">
|
| 617 |
<div className="pesquisa-opened-model-view">
|
|
|
|
| 620 |
<h3>{modeloAbertoMeta?.nome || 'Modelo'}</h3>
|
| 621 |
{modeloAbertoMeta?.observacao ? <p>{modeloAbertoMeta.observacao}</p> : null}
|
| 622 |
</div>
|
| 623 |
+
<div className="pesquisa-opened-model-actions">
|
| 624 |
+
<ShareLinkButton href={shareHref} />
|
| 625 |
+
{typeof onUsarModeloEmAvaliacao === 'function' ? (
|
| 626 |
+
<button
|
| 627 |
+
type="button"
|
| 628 |
+
className="pesquisa-opened-model-action-btn pesquisa-opened-model-action-btn-secondary"
|
| 629 |
+
onClick={() => onUsarModeloEmAvaliacao(modeloAcao)}
|
| 630 |
+
disabled={modeloAbertoLoading}
|
| 631 |
+
>
|
| 632 |
+
Usar na avaliação
|
| 633 |
+
</button>
|
| 634 |
+
) : null}
|
| 635 |
+
{typeof onEditarModeloEmElaboracao === 'function' ? (
|
| 636 |
+
<button
|
| 637 |
+
type="button"
|
| 638 |
+
className="pesquisa-opened-model-action-btn pesquisa-opened-model-action-btn-secondary"
|
| 639 |
+
onClick={() => onEditarModeloEmElaboracao(modeloAcao)}
|
| 640 |
+
disabled={modeloAbertoLoading}
|
| 641 |
+
>
|
| 642 |
+
Editar na elaboração
|
| 643 |
+
</button>
|
| 644 |
+
) : null}
|
| 645 |
+
<button type="button" className="model-source-back-btn model-source-back-btn-danger" onClick={onVoltarRepositorio} disabled={modeloAbertoLoading}>
|
| 646 |
+
{returnButtonLabel}
|
| 647 |
+
</button>
|
| 648 |
+
</div>
|
| 649 |
</div>
|
| 650 |
|
| 651 |
<div className="inner-tabs" role="tablist" aria-label="Abas internas do modelo aberto no repositório">
|
|
|
|
| 654 |
key={tab.key}
|
| 655 |
type="button"
|
| 656 |
className={modeloAbertoActiveTab === tab.key ? 'inner-tab-pill active' : 'inner-tab-pill'}
|
| 657 |
+
onClick={() => onModeloAbertoTabSelect(tab.key)}
|
| 658 |
+
disabled={modeloAbertoLoading}
|
| 659 |
>
|
| 660 |
{tab.label}
|
| 661 |
</button>
|
|
|
|
| 664 |
|
| 665 |
<div className="inner-tab-panel">
|
| 666 |
{modeloAbertoActiveTab === 'mapa' ? (
|
| 667 |
+
!activeTabLoaded ? (
|
| 668 |
+
<div className="empty-box">Carregando mapa do modelo...</div>
|
| 669 |
+
) : (
|
| 670 |
+
<>
|
| 671 |
+
<div className="row compact visualizacao-mapa-controls pesquisa-mapa-controls-row">
|
| 672 |
+
<label className="pesquisa-field pesquisa-mapa-modo-field">
|
| 673 |
+
Variável no mapa
|
| 674 |
+
<select value={modeloAbertoMapaVar} onChange={(event) => void onModeloAbertoMapChange(event.target.value)}>
|
| 675 |
+
{modeloAbertoMapaChoices.map((choice) => (
|
| 676 |
+
<option key={`repo-modelo-aberto-mapa-${choice}`} value={choice}>{choice}</option>
|
| 677 |
+
))}
|
| 678 |
+
</select>
|
| 679 |
+
</label>
|
| 680 |
+
<label className="pesquisa-field pesquisa-mapa-trabalhos-field">
|
| 681 |
+
Exibição dos trabalhos técnicos
|
| 682 |
+
<select
|
| 683 |
+
value={modeloAbertoTrabalhosTecnicosModelosModo === 'selecionados_e_anteriores'
|
| 684 |
+
? MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO
|
| 685 |
+
: modeloAbertoTrabalhosTecnicosModelosModo}
|
| 686 |
+
onChange={(event) => void onModeloAbertoTrabalhosTecnicosModeChange(event.target.value)}
|
| 687 |
+
autoComplete="off"
|
| 688 |
+
>
|
| 689 |
+
<option value="selecionados">Somente deste modelo</option>
|
| 690 |
+
<option value="selecionados_e_outras_versoes">Incluir demais versões do modelo</option>
|
| 691 |
+
</select>
|
| 692 |
+
</label>
|
| 693 |
+
</div>
|
| 694 |
+
<MapFrame html={modeloAbertoMapaHtml} />
|
| 695 |
+
</>
|
| 696 |
+
)
|
| 697 |
) : null}
|
| 698 |
|
| 699 |
{modeloAbertoActiveTab === 'trabalhos_tecnicos' ? (
|
| 700 |
+
activeTabLoaded
|
| 701 |
+
? <ModeloTrabalhosTecnicosPanel trabalhos={modeloAbertoTrabalhosTecnicos} />
|
| 702 |
+
: <div className="empty-box">Carregando trabalhos técnicos do modelo...</div>
|
| 703 |
) : null}
|
| 704 |
|
| 705 |
+
{modeloAbertoActiveTab === 'dados_mercado'
|
| 706 |
+
? (activeTabLoaded ? <DataTable table={modeloAbertoDados} maxHeight={620} /> : <div className="empty-box">Carregando dados de mercado...</div>)
|
| 707 |
+
: null}
|
| 708 |
+
{modeloAbertoActiveTab === 'metricas'
|
| 709 |
+
? (activeTabLoaded ? <DataTable table={modeloAbertoEstatisticas} maxHeight={620} /> : <div className="empty-box">Carregando métricas do modelo...</div>)
|
| 710 |
+
: null}
|
| 711 |
|
| 712 |
{modeloAbertoActiveTab === 'transformacoes' ? (
|
| 713 |
+
activeTabLoaded ? (
|
| 714 |
+
<>
|
| 715 |
+
<div dangerouslySetInnerHTML={{ __html: modeloAbertoEscalasHtml }} />
|
| 716 |
+
<h4 className="visualizacao-table-title">Dados com variáveis transformadas</h4>
|
| 717 |
+
<DataTable table={modeloAbertoDadosTransformados} />
|
| 718 |
+
</>
|
| 719 |
+
) : (
|
| 720 |
+
<div className="empty-box">Carregando transformações do modelo...</div>
|
| 721 |
+
)
|
| 722 |
) : null}
|
| 723 |
|
| 724 |
{modeloAbertoActiveTab === 'resumo' ? (
|
| 725 |
+
activeTabLoaded ? (
|
| 726 |
+
<>
|
| 727 |
+
<div className="equation-formats-section">
|
| 728 |
+
<h4>Equações do Modelo</h4>
|
| 729 |
+
<EquationFormatsPanel
|
| 730 |
+
equacoes={modeloAbertoEquacoes}
|
| 731 |
+
onDownload={(mode) => void onDownloadEquacaoModeloAberto(mode)}
|
| 732 |
+
disabled={modeloAbertoLoading}
|
| 733 |
+
/>
|
| 734 |
+
</div>
|
| 735 |
+
<div dangerouslySetInnerHTML={{ __html: modeloAbertoResumoHtml }} />
|
| 736 |
+
</>
|
| 737 |
+
) : (
|
| 738 |
+
<div className="empty-box">Carregando resumo do modelo...</div>
|
| 739 |
+
)
|
| 740 |
) : null}
|
| 741 |
|
| 742 |
+
{modeloAbertoActiveTab === 'coeficientes'
|
| 743 |
+
? (activeTabLoaded ? <DataTable table={modeloAbertoCoeficientes} maxHeight={620} /> : <div className="empty-box">Carregando coeficientes do modelo...</div>)
|
| 744 |
+
: null}
|
| 745 |
+
{modeloAbertoActiveTab === 'obs_calc'
|
| 746 |
+
? (activeTabLoaded ? <DataTable table={modeloAbertoObsCalc} maxHeight={620} /> : <div className="empty-box">Carregando observados x calculados...</div>)
|
| 747 |
+
: null}
|
| 748 |
|
| 749 |
{modeloAbertoActiveTab === 'graficos' ? (
|
| 750 |
+
activeTabLoaded ? (
|
| 751 |
+
<>
|
| 752 |
+
<div className="plot-grid-2-fixed">
|
| 753 |
+
<PlotFigure figure={modeloAbertoPlotObsCalc} title="Obs x Calc" />
|
| 754 |
+
<PlotFigure figure={modeloAbertoPlotResiduos} title="Resíduos" />
|
| 755 |
+
<PlotFigure figure={modeloAbertoPlotHistograma} title="Histograma" />
|
| 756 |
+
<PlotFigure figure={modeloAbertoPlotCook} title="Cook" forceHideLegend />
|
| 757 |
+
</div>
|
| 758 |
+
<div className="plot-full-width">
|
| 759 |
+
<PlotFigure figure={modeloAbertoPlotCorr} title="Correlação" className="plot-correlation-card" />
|
| 760 |
+
</div>
|
| 761 |
+
</>
|
| 762 |
+
) : (
|
| 763 |
+
<div className="empty-box">Carregando gráficos do modelo...</div>
|
| 764 |
+
)
|
| 765 |
) : null}
|
| 766 |
</div>
|
| 767 |
|
| 768 |
{modeloAbertoError ? <div className="error-line inline-error">{modeloAbertoError}</div> : null}
|
| 769 |
</div>
|
| 770 |
+
<LoadingOverlay show={modeloAbertoLoading || activeTabLoading} label={modeloLoadingLabel} />
|
| 771 |
</div>
|
| 772 |
)
|
| 773 |
}
|
frontend/src/components/ShareLinkButton.jsx
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useState } from 'react'
|
| 2 |
+
|
| 3 |
+
function fallbackCopyText(text) {
|
| 4 |
+
const textarea = document.createElement('textarea')
|
| 5 |
+
textarea.value = text
|
| 6 |
+
textarea.setAttribute('readonly', 'true')
|
| 7 |
+
textarea.style.position = 'fixed'
|
| 8 |
+
textarea.style.opacity = '0'
|
| 9 |
+
document.body.appendChild(textarea)
|
| 10 |
+
textarea.select()
|
| 11 |
+
document.execCommand('copy')
|
| 12 |
+
document.body.removeChild(textarea)
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export default function ShareLinkButton({
|
| 16 |
+
href = '',
|
| 17 |
+
label = 'Copiar link',
|
| 18 |
+
copiedLabel = 'Link copiado',
|
| 19 |
+
className = 'model-source-back-btn',
|
| 20 |
+
disabled = false,
|
| 21 |
+
title = '',
|
| 22 |
+
}) {
|
| 23 |
+
const [copied, setCopied] = useState(false)
|
| 24 |
+
|
| 25 |
+
useEffect(() => {
|
| 26 |
+
if (!copied) return undefined
|
| 27 |
+
const timeoutId = window.setTimeout(() => setCopied(false), 1800)
|
| 28 |
+
return () => window.clearTimeout(timeoutId)
|
| 29 |
+
}, [copied])
|
| 30 |
+
|
| 31 |
+
async function onCopyClick() {
|
| 32 |
+
if (disabled || !href) return
|
| 33 |
+
try {
|
| 34 |
+
if (navigator.clipboard?.writeText) {
|
| 35 |
+
await navigator.clipboard.writeText(href)
|
| 36 |
+
} else {
|
| 37 |
+
fallbackCopyText(href)
|
| 38 |
+
}
|
| 39 |
+
setCopied(true)
|
| 40 |
+
} catch {
|
| 41 |
+
fallbackCopyText(href)
|
| 42 |
+
setCopied(true)
|
| 43 |
+
}
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
return (
|
| 47 |
+
<button
|
| 48 |
+
type="button"
|
| 49 |
+
className={className}
|
| 50 |
+
onClick={() => void onCopyClick()}
|
| 51 |
+
disabled={disabled || !href}
|
| 52 |
+
title={title || href || 'Copiar link'}
|
| 53 |
+
>
|
| 54 |
+
{copied ? copiedLabel : label}
|
| 55 |
+
</button>
|
| 56 |
+
)
|
| 57 |
+
}
|
frontend/src/components/TrabalhosTecnicosTab.jsx
CHANGED
|
@@ -1,8 +1,10 @@
|
|
| 1 |
import React, { useEffect, useRef, useState } from 'react'
|
| 2 |
import { api } from '../api'
|
|
|
|
| 3 |
import ListPagination from './ListPagination'
|
| 4 |
import LoadingOverlay from './LoadingOverlay'
|
| 5 |
import MapFrame from './MapFrame'
|
|
|
|
| 6 |
import TruncatedCellContent from './TruncatedCellContent'
|
| 7 |
|
| 8 |
const PAGE_SIZE = 50
|
|
@@ -150,7 +152,10 @@ export default function TrabalhosTecnicosTab({
|
|
| 150 |
sessionId,
|
| 151 |
onAbrirModeloNoRepositorio = null,
|
| 152 |
quickOpenRequest = null,
|
|
|
|
|
|
|
| 153 |
onVoltarAoMapaPesquisa = null,
|
|
|
|
| 154 |
}) {
|
| 155 |
const [activeInnerTab, setActiveInnerTab] = useState('repositorio')
|
| 156 |
const [trabalhos, setTrabalhos] = useState([])
|
|
@@ -183,7 +188,9 @@ export default function TrabalhosTecnicosTab({
|
|
| 183 |
const [modeloSugestoesMesa, setModeloSugestoesMesa] = useState([])
|
| 184 |
const [modeloSugestoesLoading, setModeloSugestoesLoading] = useState(false)
|
| 185 |
const [origemAbertura, setOrigemAbertura] = useState('lista')
|
|
|
|
| 186 |
const quickOpenHandledRef = useRef('')
|
|
|
|
| 187 |
const mapaRequestKeyRef = useRef('')
|
| 188 |
const mapaUltimaSelecaoRef = useRef({ ids: [], avaliando: null })
|
| 189 |
const mapaAreaRef = useRef(null)
|
|
@@ -210,6 +217,34 @@ export default function TrabalhosTecnicosTab({
|
|
| 210 |
void abrirTrabalhoPorId(trabalhoId, { origem })
|
| 211 |
}, [quickOpenRequest])
|
| 212 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
async function carregarTrabalhos() {
|
| 214 |
setLoading(true)
|
| 215 |
setError('')
|
|
@@ -376,6 +411,7 @@ export default function TrabalhosTecnicosTab({
|
|
| 376 |
async function abrirTrabalhoPorId(trabalhoId, options = {}) {
|
| 377 |
const chave = String(trabalhoId || '').trim()
|
| 378 |
if (!chave) return
|
|
|
|
| 379 |
setTrabalhoLoading(true)
|
| 380 |
setTrabalhoError('')
|
| 381 |
setEdicaoErro('')
|
|
@@ -385,10 +421,17 @@ export default function TrabalhosTecnicosTab({
|
|
| 385 |
try {
|
| 386 |
const resp = await api.trabalhosTecnicosDetalhe(chave)
|
| 387 |
setTrabalhoAberto(resp?.trabalho || null)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
} catch (err) {
|
| 389 |
setTrabalhoError(err.message || 'Falha ao abrir trabalho técnico.')
|
| 390 |
} finally {
|
| 391 |
setTrabalhoLoading(false)
|
|
|
|
| 392 |
}
|
| 393 |
}
|
| 394 |
|
|
@@ -398,28 +441,34 @@ export default function TrabalhosTecnicosTab({
|
|
| 398 |
await abrirTrabalhoPorId(trabalhoId, { origem: 'lista' })
|
| 399 |
}
|
| 400 |
|
| 401 |
-
function onVoltarLista() {
|
| 402 |
setTrabalhoAberto(null)
|
| 403 |
setTrabalhoError('')
|
| 404 |
setEdicaoErro('')
|
| 405 |
setEditando(false)
|
| 406 |
setEdicao(null)
|
| 407 |
setModeloDraft('')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
}
|
| 409 |
|
| 410 |
function onVoltarDoDetalhe() {
|
| 411 |
if (origemAbertura === 'pesquisa_mapa' && typeof onVoltarAoMapaPesquisa === 'function') {
|
| 412 |
-
onVoltarLista()
|
| 413 |
onVoltarAoMapaPesquisa()
|
| 414 |
return
|
| 415 |
}
|
| 416 |
if (origemAbertura === 'trabalhos_tecnicos_mapa') {
|
| 417 |
scrollRetornoMapaRef.current = true
|
| 418 |
setActiveInnerTab('mapa')
|
| 419 |
-
onVoltarLista()
|
| 420 |
return
|
| 421 |
}
|
| 422 |
-
onVoltarLista()
|
| 423 |
}
|
| 424 |
|
| 425 |
function onAbrirModeloAssociado(modelo) {
|
|
@@ -428,6 +477,11 @@ export default function TrabalhosTecnicosTab({
|
|
| 428 |
modeloId: modelo?.mesa_modelo_id,
|
| 429 |
nomeModelo: modelo?.mesa_modelo_nome || modelo?.nome,
|
| 430 |
modeloArquivo: modelo?.mesa_modelo_arquivo || '',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 431 |
})
|
| 432 |
}
|
| 433 |
|
|
@@ -632,6 +686,7 @@ export default function TrabalhosTecnicosTab({
|
|
| 632 |
const modelosMesa = modelos.filter((item) => item?.disponivel_mesa)
|
| 633 |
const imoveis = Array.isArray(trabalhoAberto?.imoveis) ? trabalhoAberto.imoveis : []
|
| 634 |
const processosSei = listarProcessosSei(trabalhoAberto)
|
|
|
|
| 635 |
|
| 636 |
return (
|
| 637 |
<div className="tab-content">
|
|
@@ -642,6 +697,7 @@ export default function TrabalhosTecnicosTab({
|
|
| 642 |
<p>{trabalhoAberto?.tipo_label || 'Tipo não identificado'}</p>
|
| 643 |
</div>
|
| 644 |
<div className="trabalho-detail-actions">
|
|
|
|
| 645 |
{!editando ? (
|
| 646 |
<button
|
| 647 |
type="button"
|
|
@@ -898,7 +954,7 @@ export default function TrabalhosTecnicosTab({
|
|
| 898 |
)}
|
| 899 |
{modelosMesa.length ? (
|
| 900 |
<div className="section1-empty-hint">
|
| 901 |
-
Clique em um modelo disponível para abri-lo
|
| 902 |
</div>
|
| 903 |
) : null}
|
| 904 |
</>
|
|
@@ -962,7 +1018,7 @@ export default function TrabalhosTecnicosTab({
|
|
| 962 |
</div>
|
| 963 |
<LoadingOverlay
|
| 964 |
show={trabalhoLoading || salvando}
|
| 965 |
-
label={salvando ? 'Salvando trabalho técnico...' : '
|
| 966 |
/>
|
| 967 |
</div>
|
| 968 |
)
|
|
@@ -976,7 +1032,15 @@ export default function TrabalhosTecnicosTab({
|
|
| 976 |
key={tab.key}
|
| 977 |
type="button"
|
| 978 |
className={activeInnerTab === tab.key ? 'inner-tab-pill active' : 'inner-tab-pill'}
|
| 979 |
-
onClick={() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 980 |
>
|
| 981 |
{tab.label}
|
| 982 |
</button>
|
|
@@ -1059,7 +1123,10 @@ export default function TrabalhosTecnicosTab({
|
|
| 1059 |
</tr>
|
| 1060 |
</thead>
|
| 1061 |
<tbody>
|
| 1062 |
-
{trabalhosPagina.map((item) =>
|
|
|
|
|
|
|
|
|
|
| 1063 |
<tr key={String(item?.id || '')}>
|
| 1064 |
<td className="trabalhos-col-nome"><TruncatedCellContent tooltipContent={item?.nome || '-'}>{item?.nome || '-'}</TruncatedCellContent></td>
|
| 1065 |
<td className="trabalhos-col-tipo"><TruncatedCellContent tooltipContent={item?.tipo_label || item?.tipo_codigo || '-'}>{item?.tipo_label || item?.tipo_codigo || '-'}</TruncatedCellContent></td>
|
|
@@ -1107,16 +1174,18 @@ export default function TrabalhosTecnicosTab({
|
|
| 1107 |
<td className="repo-col-open">
|
| 1108 |
<button
|
| 1109 |
type="button"
|
| 1110 |
-
className=
|
| 1111 |
onClick={() => void onAbrirTrabalho(item)}
|
| 1112 |
-
|
| 1113 |
-
|
|
|
|
| 1114 |
>
|
| 1115 |
-
↗
|
| 1116 |
</button>
|
| 1117 |
</td>
|
| 1118 |
</tr>
|
| 1119 |
-
|
|
|
|
| 1120 |
{!trabalhosFiltrados.length ? (
|
| 1121 |
<tr>
|
| 1122 |
<td colSpan={7}>
|
|
@@ -1345,8 +1414,10 @@ export default function TrabalhosTecnicosTab({
|
|
| 1345 |
)}
|
| 1346 |
</div>
|
| 1347 |
<LoadingOverlay
|
| 1348 |
-
show={loading || mapaLoading}
|
| 1349 |
-
label={
|
|
|
|
|
|
|
| 1350 |
/>
|
| 1351 |
</div>
|
| 1352 |
)
|
|
|
|
| 1 |
import React, { useEffect, useRef, useState } from 'react'
|
| 2 |
import { api } from '../api'
|
| 3 |
+
import { buildTrabalhoTecnicoLink, getTrabalhosSubtabKeyFromSlug } from '../deepLinks'
|
| 4 |
import ListPagination from './ListPagination'
|
| 5 |
import LoadingOverlay from './LoadingOverlay'
|
| 6 |
import MapFrame from './MapFrame'
|
| 7 |
+
import ShareLinkButton from './ShareLinkButton'
|
| 8 |
import TruncatedCellContent from './TruncatedCellContent'
|
| 9 |
|
| 10 |
const PAGE_SIZE = 50
|
|
|
|
| 152 |
sessionId,
|
| 153 |
onAbrirModeloNoRepositorio = null,
|
| 154 |
quickOpenRequest = null,
|
| 155 |
+
routeRequest = null,
|
| 156 |
+
onRouteChange = null,
|
| 157 |
onVoltarAoMapaPesquisa = null,
|
| 158 |
+
onModoImersivoChange = null,
|
| 159 |
}) {
|
| 160 |
const [activeInnerTab, setActiveInnerTab] = useState('repositorio')
|
| 161 |
const [trabalhos, setTrabalhos] = useState([])
|
|
|
|
| 188 |
const [modeloSugestoesMesa, setModeloSugestoesMesa] = useState([])
|
| 189 |
const [modeloSugestoesLoading, setModeloSugestoesLoading] = useState(false)
|
| 190 |
const [origemAbertura, setOrigemAbertura] = useState('lista')
|
| 191 |
+
const [abrindoTrabalhoId, setAbrindoTrabalhoId] = useState('')
|
| 192 |
const quickOpenHandledRef = useRef('')
|
| 193 |
+
const routeRequestHandledRef = useRef('')
|
| 194 |
const mapaRequestKeyRef = useRef('')
|
| 195 |
const mapaUltimaSelecaoRef = useRef({ ids: [], avaliando: null })
|
| 196 |
const mapaAreaRef = useRef(null)
|
|
|
|
| 217 |
void abrirTrabalhoPorId(trabalhoId, { origem })
|
| 218 |
}, [quickOpenRequest])
|
| 219 |
|
| 220 |
+
useEffect(() => {
|
| 221 |
+
const requestKey = String(routeRequest?.requestKey || '').trim()
|
| 222 |
+
if (!requestKey) return
|
| 223 |
+
if (routeRequestHandledRef.current === requestKey) return
|
| 224 |
+
routeRequestHandledRef.current = requestKey
|
| 225 |
+
const nextSubtab = getTrabalhosSubtabKeyFromSlug(routeRequest?.subtab)
|
| 226 |
+
setActiveInnerTab(nextSubtab)
|
| 227 |
+
const trabalhoId = String(routeRequest?.trabalhoId || '').trim()
|
| 228 |
+
if (trabalhoId) {
|
| 229 |
+
const trabalhoAbertoId = String(trabalhoAberto?.id || '').trim()
|
| 230 |
+
if (trabalhoAbertoId && trabalhoAbertoId === trabalhoId) {
|
| 231 |
+
setTrabalhoError('')
|
| 232 |
+
return
|
| 233 |
+
}
|
| 234 |
+
void abrirTrabalhoPorId(trabalhoId, { origem: 'lista' })
|
| 235 |
+
return
|
| 236 |
+
}
|
| 237 |
+
if (trabalhoAberto) {
|
| 238 |
+
onVoltarLista({ syncRoute: false, subtab: nextSubtab })
|
| 239 |
+
}
|
| 240 |
+
}, [routeRequest, trabalhoAberto])
|
| 241 |
+
|
| 242 |
+
useEffect(() => {
|
| 243 |
+
if (typeof onModoImersivoChange !== 'function') return undefined
|
| 244 |
+
onModoImersivoChange(Boolean(trabalhoAberto))
|
| 245 |
+
return () => onModoImersivoChange(false)
|
| 246 |
+
}, [trabalhoAberto, onModoImersivoChange])
|
| 247 |
+
|
| 248 |
async function carregarTrabalhos() {
|
| 249 |
setLoading(true)
|
| 250 |
setError('')
|
|
|
|
| 411 |
async function abrirTrabalhoPorId(trabalhoId, options = {}) {
|
| 412 |
const chave = String(trabalhoId || '').trim()
|
| 413 |
if (!chave) return
|
| 414 |
+
setAbrindoTrabalhoId(chave)
|
| 415 |
setTrabalhoLoading(true)
|
| 416 |
setTrabalhoError('')
|
| 417 |
setEdicaoErro('')
|
|
|
|
| 421 |
try {
|
| 422 |
const resp = await api.trabalhosTecnicosDetalhe(chave)
|
| 423 |
setTrabalhoAberto(resp?.trabalho || null)
|
| 424 |
+
if (typeof onRouteChange === 'function') {
|
| 425 |
+
onRouteChange({
|
| 426 |
+
tab: 'trabalhos',
|
| 427 |
+
trabalhoId: chave,
|
| 428 |
+
})
|
| 429 |
+
}
|
| 430 |
} catch (err) {
|
| 431 |
setTrabalhoError(err.message || 'Falha ao abrir trabalho técnico.')
|
| 432 |
} finally {
|
| 433 |
setTrabalhoLoading(false)
|
| 434 |
+
setAbrindoTrabalhoId((prev) => (prev === chave ? '' : prev))
|
| 435 |
}
|
| 436 |
}
|
| 437 |
|
|
|
|
| 441 |
await abrirTrabalhoPorId(trabalhoId, { origem: 'lista' })
|
| 442 |
}
|
| 443 |
|
| 444 |
+
function onVoltarLista(options = {}) {
|
| 445 |
setTrabalhoAberto(null)
|
| 446 |
setTrabalhoError('')
|
| 447 |
setEdicaoErro('')
|
| 448 |
setEditando(false)
|
| 449 |
setEdicao(null)
|
| 450 |
setModeloDraft('')
|
| 451 |
+
if (options.syncRoute !== false && typeof onRouteChange === 'function') {
|
| 452 |
+
onRouteChange({
|
| 453 |
+
tab: 'trabalhos',
|
| 454 |
+
subtab: options.subtab || activeInnerTab,
|
| 455 |
+
})
|
| 456 |
+
}
|
| 457 |
}
|
| 458 |
|
| 459 |
function onVoltarDoDetalhe() {
|
| 460 |
if (origemAbertura === 'pesquisa_mapa' && typeof onVoltarAoMapaPesquisa === 'function') {
|
| 461 |
+
onVoltarLista({ syncRoute: false })
|
| 462 |
onVoltarAoMapaPesquisa()
|
| 463 |
return
|
| 464 |
}
|
| 465 |
if (origemAbertura === 'trabalhos_tecnicos_mapa') {
|
| 466 |
scrollRetornoMapaRef.current = true
|
| 467 |
setActiveInnerTab('mapa')
|
| 468 |
+
onVoltarLista({ subtab: 'mapa' })
|
| 469 |
return
|
| 470 |
}
|
| 471 |
+
onVoltarLista({ subtab: activeInnerTab })
|
| 472 |
}
|
| 473 |
|
| 474 |
function onAbrirModeloAssociado(modelo) {
|
|
|
|
| 477 |
modeloId: modelo?.mesa_modelo_id,
|
| 478 |
nomeModelo: modelo?.mesa_modelo_nome || modelo?.nome,
|
| 479 |
modeloArquivo: modelo?.mesa_modelo_arquivo || '',
|
| 480 |
+
returnIntent: {
|
| 481 |
+
tab: 'trabalhos',
|
| 482 |
+
subtab: activeInnerTab,
|
| 483 |
+
trabalhoId: String(trabalhoAberto?.id || '').trim(),
|
| 484 |
+
},
|
| 485 |
})
|
| 486 |
}
|
| 487 |
|
|
|
|
| 686 |
const modelosMesa = modelos.filter((item) => item?.disponivel_mesa)
|
| 687 |
const imoveis = Array.isArray(trabalhoAberto?.imoveis) ? trabalhoAberto.imoveis : []
|
| 688 |
const processosSei = listarProcessosSei(trabalhoAberto)
|
| 689 |
+
const shareHref = buildTrabalhoTecnicoLink(trabalhoAberto?.id || '')
|
| 690 |
|
| 691 |
return (
|
| 692 |
<div className="tab-content">
|
|
|
|
| 697 |
<p>{trabalhoAberto?.tipo_label || 'Tipo não identificado'}</p>
|
| 698 |
</div>
|
| 699 |
<div className="trabalho-detail-actions">
|
| 700 |
+
<ShareLinkButton href={shareHref} />
|
| 701 |
{!editando ? (
|
| 702 |
<button
|
| 703 |
type="button"
|
|
|
|
| 954 |
)}
|
| 955 |
{modelosMesa.length ? (
|
| 956 |
<div className="section1-empty-hint">
|
| 957 |
+
Clique em um modelo disponível para abri-lo na MESA.
|
| 958 |
</div>
|
| 959 |
) : null}
|
| 960 |
</>
|
|
|
|
| 1018 |
</div>
|
| 1019 |
<LoadingOverlay
|
| 1020 |
show={trabalhoLoading || salvando}
|
| 1021 |
+
label={salvando ? 'Salvando trabalho técnico...' : 'Abrindo trabalho técnico...'}
|
| 1022 |
/>
|
| 1023 |
</div>
|
| 1024 |
)
|
|
|
|
| 1032 |
key={tab.key}
|
| 1033 |
type="button"
|
| 1034 |
className={activeInnerTab === tab.key ? 'inner-tab-pill active' : 'inner-tab-pill'}
|
| 1035 |
+
onClick={() => {
|
| 1036 |
+
setActiveInnerTab(tab.key)
|
| 1037 |
+
if (typeof onRouteChange === 'function') {
|
| 1038 |
+
onRouteChange({
|
| 1039 |
+
tab: 'trabalhos',
|
| 1040 |
+
subtab: tab.key,
|
| 1041 |
+
})
|
| 1042 |
+
}
|
| 1043 |
+
}}
|
| 1044 |
>
|
| 1045 |
{tab.label}
|
| 1046 |
</button>
|
|
|
|
| 1123 |
</tr>
|
| 1124 |
</thead>
|
| 1125 |
<tbody>
|
| 1126 |
+
{trabalhosPagina.map((item) => {
|
| 1127 |
+
const trabalhoId = String(item?.id || '').trim()
|
| 1128 |
+
const abrindoEsteTrabalho = abrindoTrabalhoId === trabalhoId
|
| 1129 |
+
return (
|
| 1130 |
<tr key={String(item?.id || '')}>
|
| 1131 |
<td className="trabalhos-col-nome"><TruncatedCellContent tooltipContent={item?.nome || '-'}>{item?.nome || '-'}</TruncatedCellContent></td>
|
| 1132 |
<td className="trabalhos-col-tipo"><TruncatedCellContent tooltipContent={item?.tipo_label || item?.tipo_codigo || '-'}>{item?.tipo_label || item?.tipo_codigo || '-'}</TruncatedCellContent></td>
|
|
|
|
| 1174 |
<td className="repo-col-open">
|
| 1175 |
<button
|
| 1176 |
type="button"
|
| 1177 |
+
className={abrindoEsteTrabalho ? 'repo-open-btn is-loading' : 'repo-open-btn'}
|
| 1178 |
onClick={() => void onAbrirTrabalho(item)}
|
| 1179 |
+
disabled={trabalhoLoading}
|
| 1180 |
+
title={abrindoEsteTrabalho ? 'Abrindo trabalho técnico...' : 'Abrir trabalho técnico'}
|
| 1181 |
+
aria-label={`${abrindoEsteTrabalho ? 'Abrindo' : 'Abrir'} ${item?.nome || 'trabalho técnico'}`}
|
| 1182 |
>
|
| 1183 |
+
{abrindoEsteTrabalho ? <span className="repo-open-btn-spinner" aria-hidden="true" /> : '↗'}
|
| 1184 |
</button>
|
| 1185 |
</td>
|
| 1186 |
</tr>
|
| 1187 |
+
)
|
| 1188 |
+
})}
|
| 1189 |
{!trabalhosFiltrados.length ? (
|
| 1190 |
<tr>
|
| 1191 |
<td colSpan={7}>
|
|
|
|
| 1414 |
)}
|
| 1415 |
</div>
|
| 1416 |
<LoadingOverlay
|
| 1417 |
+
show={loading || mapaLoading || trabalhoLoading}
|
| 1418 |
+
label={trabalhoLoading
|
| 1419 |
+
? 'Abrindo trabalho técnico...'
|
| 1420 |
+
: (mapaLoading ? 'Gerando mapa dos trabalhos técnicos...' : 'Carregando trabalhos técnicos...')}
|
| 1421 |
/>
|
| 1422 |
</div>
|
| 1423 |
)
|
frontend/src/deepLinks.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const TAB_LABEL_BY_SLUG = {
|
| 2 |
+
modelos: 'Modelos Estatísticos',
|
| 3 |
+
elaboracao: 'Elaboração/Edição',
|
| 4 |
+
avaliacao: 'Avaliação',
|
| 5 |
+
trabalhos: 'Trabalhos Técnicos',
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
const TAB_SLUG_BY_LABEL = Object.fromEntries(
|
| 9 |
+
Object.entries(TAB_LABEL_BY_SLUG).map(([slug, label]) => [label, slug]),
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
const MODELOS_SUBTAB_LABEL_BY_SLUG = {
|
| 13 |
+
pesquisa: 'Pesquisar Modelos',
|
| 14 |
+
repositorio: 'Repositório de Modelos',
|
| 15 |
+
'visao-geral': 'Visão Geral',
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
const MODELOS_SUBTAB_SLUG_BY_LABEL = Object.fromEntries(
|
| 19 |
+
Object.entries(MODELOS_SUBTAB_LABEL_BY_SLUG).map(([slug, label]) => [label, slug]),
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
const TRABALHOS_SUBTAB_LABEL_BY_SLUG = {
|
| 23 |
+
repositorio: 'repositorio',
|
| 24 |
+
mapa: 'mapa',
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
const TRABALHOS_SUBTAB_SLUG_BY_LABEL = Object.fromEntries(
|
| 28 |
+
Object.entries(TRABALHOS_SUBTAB_LABEL_BY_SLUG).map(([slug, label]) => [label, slug]),
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
const MODEL_TAB_SET = new Set([
|
| 32 |
+
'mapa',
|
| 33 |
+
'trabalhos-tecnicos',
|
| 34 |
+
'dados-mercado',
|
| 35 |
+
'metricas',
|
| 36 |
+
'transformacoes',
|
| 37 |
+
'resumo',
|
| 38 |
+
'coeficientes',
|
| 39 |
+
'obs-calc',
|
| 40 |
+
'graficos',
|
| 41 |
+
])
|
| 42 |
+
|
| 43 |
+
const PESQUISA_FILTER_KEYS = [
|
| 44 |
+
'nomeModelo',
|
| 45 |
+
'tipoModelo',
|
| 46 |
+
'negociacaoModelo',
|
| 47 |
+
'dataMin',
|
| 48 |
+
'dataMax',
|
| 49 |
+
'versionamentoModelos',
|
| 50 |
+
'avalFinalidade',
|
| 51 |
+
'avalZona',
|
| 52 |
+
'avalBairro',
|
| 53 |
+
'avalArea',
|
| 54 |
+
'avalRh',
|
| 55 |
+
]
|
| 56 |
+
|
| 57 |
+
const PESQUISA_FILTER_DEFAULTS = {
|
| 58 |
+
nomeModelo: '',
|
| 59 |
+
tipoModelo: '',
|
| 60 |
+
negociacaoModelo: '',
|
| 61 |
+
dataMin: '',
|
| 62 |
+
dataMax: '',
|
| 63 |
+
versionamentoModelos: 'incluir_antigos',
|
| 64 |
+
avalFinalidade: '',
|
| 65 |
+
avalZona: '',
|
| 66 |
+
avalBairro: '',
|
| 67 |
+
avalArea: '',
|
| 68 |
+
avalRh: '',
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
function normalizeSlug(value) {
|
| 72 |
+
return String(value || '').trim().toLowerCase()
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
function trimValue(value) {
|
| 76 |
+
return String(value || '').trim()
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
function parseFiniteNumber(value) {
|
| 80 |
+
const text = trimValue(value).replace(',', '.')
|
| 81 |
+
if (!text) return null
|
| 82 |
+
const parsed = Number(text)
|
| 83 |
+
return Number.isFinite(parsed) ? parsed : null
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
function sanitizePesquisaFilters(rawFilters = {}) {
|
| 87 |
+
const next = { ...PESQUISA_FILTER_DEFAULTS }
|
| 88 |
+
PESQUISA_FILTER_KEYS.forEach((key) => {
|
| 89 |
+
if (!Object.prototype.hasOwnProperty.call(rawFilters, key)) return
|
| 90 |
+
const value = trimValue(rawFilters[key])
|
| 91 |
+
if (key === 'versionamentoModelos') {
|
| 92 |
+
next[key] = value === 'atuais' ? 'atuais' : 'incluir_antigos'
|
| 93 |
+
return
|
| 94 |
+
}
|
| 95 |
+
next[key] = value
|
| 96 |
+
})
|
| 97 |
+
return next
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
function hasPesquisaFilters(filters = {}) {
|
| 101 |
+
return PESQUISA_FILTER_KEYS.some((key) => {
|
| 102 |
+
const value = key === 'versionamentoModelos'
|
| 103 |
+
? trimValue(filters[key] || PESQUISA_FILTER_DEFAULTS[key])
|
| 104 |
+
: trimValue(filters[key])
|
| 105 |
+
if (key === 'versionamentoModelos') return value === 'atuais'
|
| 106 |
+
return Boolean(value)
|
| 107 |
+
})
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
export function getAppTabKeyFromSlug(slug) {
|
| 111 |
+
return TAB_LABEL_BY_SLUG[normalizeSlug(slug)] || ''
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
export function getAppTabSlugFromKey(label) {
|
| 115 |
+
return TAB_SLUG_BY_LABEL[String(label || '').trim()] || ''
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
export function getModelosSubtabKeyFromSlug(slug) {
|
| 119 |
+
return MODELOS_SUBTAB_LABEL_BY_SLUG[normalizeSlug(slug)] || MODELOS_SUBTAB_LABEL_BY_SLUG.pesquisa
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
export function getModelosSubtabSlugFromKey(label) {
|
| 123 |
+
return MODELOS_SUBTAB_SLUG_BY_LABEL[String(label || '').trim()] || 'pesquisa'
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
export function getTrabalhosSubtabKeyFromSlug(slug) {
|
| 127 |
+
return TRABALHOS_SUBTAB_LABEL_BY_SLUG[normalizeSlug(slug)] || TRABALHOS_SUBTAB_LABEL_BY_SLUG.repositorio
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
export function getTrabalhosSubtabSlugFromKey(label) {
|
| 131 |
+
return TRABALHOS_SUBTAB_SLUG_BY_LABEL[String(label || '').trim()] || 'repositorio'
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
export function getModelTabSlug(value) {
|
| 135 |
+
const slug = normalizeSlug(value)
|
| 136 |
+
return MODEL_TAB_SET.has(slug) ? slug : 'mapa'
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
export function hasMesaDeepLink(intent) {
|
| 140 |
+
if (!intent || typeof intent !== 'object') return false
|
| 141 |
+
return Boolean(
|
| 142 |
+
trimValue(intent.tab)
|
| 143 |
+
|| trimValue(intent.modeloId)
|
| 144 |
+
|| trimValue(intent.trabalhoId)
|
| 145 |
+
|| trimValue(intent.subtab)
|
| 146 |
+
|| hasPesquisaFilters(intent.filters)
|
| 147 |
+
|| (intent.avaliando && Number.isFinite(Number(intent.avaliando.lat)) && Number.isFinite(Number(intent.avaliando.lon))),
|
| 148 |
+
)
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
export function normalizeMesaDeepLink(intent = {}) {
|
| 152 |
+
const safeIntent = intent && typeof intent === 'object' ? intent : {}
|
| 153 |
+
const rawTab = normalizeSlug(safeIntent.tab)
|
| 154 |
+
const modeloId = trimValue(safeIntent.modeloId)
|
| 155 |
+
const trabalhoId = trimValue(safeIntent.trabalhoId)
|
| 156 |
+
let tab = TAB_LABEL_BY_SLUG[rawTab] ? rawTab : ''
|
| 157 |
+
|
| 158 |
+
if (!tab) {
|
| 159 |
+
if (modeloId) tab = 'modelos'
|
| 160 |
+
if (!tab && trabalhoId) tab = 'trabalhos'
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
let subtab = normalizeSlug(safeIntent.subtab)
|
| 164 |
+
if (tab === 'modelos') {
|
| 165 |
+
if (modeloId) {
|
| 166 |
+
subtab = 'repositorio'
|
| 167 |
+
} else if (!MODELOS_SUBTAB_LABEL_BY_SLUG[subtab]) {
|
| 168 |
+
subtab = 'pesquisa'
|
| 169 |
+
}
|
| 170 |
+
} else if (tab === 'trabalhos') {
|
| 171 |
+
if (!TRABALHOS_SUBTAB_LABEL_BY_SLUG[subtab]) subtab = 'repositorio'
|
| 172 |
+
} else {
|
| 173 |
+
subtab = ''
|
| 174 |
+
}
|
| 175 |
+
|
| 176 |
+
const filters = sanitizePesquisaFilters(safeIntent.filters)
|
| 177 |
+
const lat = parseFiniteNumber(safeIntent.avaliando?.lat)
|
| 178 |
+
const lon = parseFiniteNumber(safeIntent.avaliando?.lon)
|
| 179 |
+
const avaliando = lat !== null && lon !== null ? { lat, lon } : null
|
| 180 |
+
const modelTab = tab === 'modelos' && subtab === 'repositorio' && modeloId
|
| 181 |
+
? getModelTabSlug(safeIntent.modelTab)
|
| 182 |
+
: ''
|
| 183 |
+
|
| 184 |
+
return {
|
| 185 |
+
tab,
|
| 186 |
+
subtab,
|
| 187 |
+
modelTab,
|
| 188 |
+
modeloId,
|
| 189 |
+
trabalhoId,
|
| 190 |
+
filters,
|
| 191 |
+
avaliando,
|
| 192 |
+
}
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
export function parseMesaDeepLink(search = '') {
|
| 196 |
+
const params = new URLSearchParams(String(search || '').replace(/^\?/, ''))
|
| 197 |
+
const filters = {}
|
| 198 |
+
PESQUISA_FILTER_KEYS.forEach((key) => {
|
| 199 |
+
if (!params.has(key)) return
|
| 200 |
+
filters[key] = params.get(key)
|
| 201 |
+
})
|
| 202 |
+
|
| 203 |
+
return normalizeMesaDeepLink({
|
| 204 |
+
tab: params.get('tab'),
|
| 205 |
+
subtab: params.get('subtab'),
|
| 206 |
+
modelTab: params.get('modelTab'),
|
| 207 |
+
modeloId: params.get('modeloId'),
|
| 208 |
+
trabalhoId: params.get('trabalhoId'),
|
| 209 |
+
filters,
|
| 210 |
+
avaliando: {
|
| 211 |
+
lat: params.get('avalLat'),
|
| 212 |
+
lon: params.get('avalLon'),
|
| 213 |
+
},
|
| 214 |
+
})
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
export function serializeMesaDeepLink(intent = {}) {
|
| 218 |
+
const normalized = normalizeMesaDeepLink(intent)
|
| 219 |
+
const params = new URLSearchParams()
|
| 220 |
+
|
| 221 |
+
if (normalized.tab) params.set('tab', normalized.tab)
|
| 222 |
+
|
| 223 |
+
if (normalized.tab === 'modelos' && normalized.subtab) {
|
| 224 |
+
params.set('subtab', normalized.subtab)
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
if (normalized.tab === 'trabalhos' && normalized.subtab && !normalized.trabalhoId) {
|
| 228 |
+
params.set('subtab', normalized.subtab)
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
if (normalized.modeloId) params.set('modeloId', normalized.modeloId)
|
| 232 |
+
if (normalized.trabalhoId) params.set('trabalhoId', normalized.trabalhoId)
|
| 233 |
+
if (normalized.modelTab) params.set('modelTab', normalized.modelTab)
|
| 234 |
+
|
| 235 |
+
if (normalized.tab === 'modelos' && normalized.subtab === 'pesquisa') {
|
| 236 |
+
PESQUISA_FILTER_KEYS.forEach((key) => {
|
| 237 |
+
const value = key === 'versionamentoModelos'
|
| 238 |
+
? trimValue(normalized.filters[key] || PESQUISA_FILTER_DEFAULTS[key])
|
| 239 |
+
: trimValue(normalized.filters[key])
|
| 240 |
+
if (key === 'versionamentoModelos') {
|
| 241 |
+
if (value === 'atuais') params.set(key, value)
|
| 242 |
+
return
|
| 243 |
+
}
|
| 244 |
+
if (value) params.set(key, value)
|
| 245 |
+
})
|
| 246 |
+
if (normalized.avaliando) {
|
| 247 |
+
params.set('avalLat', String(normalized.avaliando.lat))
|
| 248 |
+
params.set('avalLon', String(normalized.avaliando.lon))
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
const query = params.toString()
|
| 253 |
+
return query ? `?${query}` : ''
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
export function buildMesaUrl(intent = {}) {
|
| 257 |
+
if (typeof window === 'undefined') return serializeMesaDeepLink(intent)
|
| 258 |
+
const url = new URL(window.location.href)
|
| 259 |
+
url.search = serializeMesaDeepLink(intent)
|
| 260 |
+
url.hash = ''
|
| 261 |
+
return url.toString()
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
function syncHuggingFaceParentUrl(queryString = '', hash = '') {
|
| 265 |
+
if (typeof window === 'undefined') return
|
| 266 |
+
if (!window.parent || window.parent === window) return
|
| 267 |
+
try {
|
| 268 |
+
window.parent.postMessage(
|
| 269 |
+
{
|
| 270 |
+
queryString,
|
| 271 |
+
hash,
|
| 272 |
+
},
|
| 273 |
+
'https://huggingface.co',
|
| 274 |
+
)
|
| 275 |
+
} catch {
|
| 276 |
+
// Ignora falhas cross-origin e mantém o sync local no iframe.
|
| 277 |
+
}
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
export function replaceMesaDeepLink(intent = {}) {
|
| 281 |
+
if (typeof window === 'undefined') return
|
| 282 |
+
const queryString = serializeMesaDeepLink(intent)
|
| 283 |
+
window.history.replaceState(null, '', buildMesaUrl(intent))
|
| 284 |
+
syncHuggingFaceParentUrl(queryString, '')
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
export function pushMesaDeepLink(intent = {}) {
|
| 288 |
+
if (typeof window === 'undefined') return
|
| 289 |
+
const queryString = serializeMesaDeepLink(intent)
|
| 290 |
+
window.history.pushState(null, '', buildMesaUrl(intent))
|
| 291 |
+
syncHuggingFaceParentUrl(queryString, '')
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
export function buildRepositorioModeloLink(modeloId, modelTab = 'mapa') {
|
| 295 |
+
return buildMesaUrl({
|
| 296 |
+
tab: 'modelos',
|
| 297 |
+
subtab: 'repositorio',
|
| 298 |
+
modeloId,
|
| 299 |
+
modelTab,
|
| 300 |
+
})
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
export function buildAvaliacaoModeloLink(modeloId) {
|
| 304 |
+
return buildMesaUrl({
|
| 305 |
+
tab: 'avaliacao',
|
| 306 |
+
modeloId,
|
| 307 |
+
})
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
export function buildElaboracaoModeloLink(modeloId) {
|
| 311 |
+
return buildMesaUrl({
|
| 312 |
+
tab: 'elaboracao',
|
| 313 |
+
modeloId,
|
| 314 |
+
})
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
export function buildTrabalhoTecnicoLink(trabalhoId) {
|
| 318 |
+
return buildMesaUrl({
|
| 319 |
+
tab: 'trabalhos',
|
| 320 |
+
trabalhoId,
|
| 321 |
+
})
|
| 322 |
+
}
|
| 323 |
+
|
| 324 |
+
export function buildPesquisaLink(filters = {}, avaliando = null) {
|
| 325 |
+
return buildMesaUrl({
|
| 326 |
+
tab: 'modelos',
|
| 327 |
+
subtab: 'pesquisa',
|
| 328 |
+
filters,
|
| 329 |
+
avaliando,
|
| 330 |
+
})
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
export function buildPesquisaRoutePayload(filters = {}, avaliando = null) {
|
| 334 |
+
return normalizeMesaDeepLink({
|
| 335 |
+
tab: 'modelos',
|
| 336 |
+
subtab: 'pesquisa',
|
| 337 |
+
filters,
|
| 338 |
+
avaliando,
|
| 339 |
+
})
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
export function getPesquisaFilterDefaults() {
|
| 343 |
+
return { ...PESQUISA_FILTER_DEFAULTS }
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
export function hasPesquisaRoutePayload(filters = {}, avaliando = null) {
|
| 347 |
+
return hasPesquisaFilters(filters) || Boolean(avaliando)
|
| 348 |
+
}
|
frontend/src/styles.css
CHANGED
|
@@ -780,6 +780,20 @@ textarea {
|
|
| 780 |
color: #1b7a40;
|
| 781 |
}
|
| 782 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 783 |
.repo-delete-icon-btn {
|
| 784 |
min-width: 28px;
|
| 785 |
min-height: 28px;
|
|
@@ -2984,7 +2998,7 @@ button.pesquisa-coluna-remove:hover {
|
|
| 2984 |
.pesquisa-results-toolbar {
|
| 2985 |
display: flex;
|
| 2986 |
align-items: center;
|
| 2987 |
-
justify-content:
|
| 2988 |
flex-wrap: wrap;
|
| 2989 |
gap: 10px;
|
| 2990 |
margin-bottom: 10px;
|
|
@@ -3007,6 +3021,14 @@ button.pesquisa-coluna-remove:hover {
|
|
| 3007 |
min-height: 34px;
|
| 3008 |
}
|
| 3009 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3010 |
.pesquisa-select-all {
|
| 3011 |
display: inline-flex;
|
| 3012 |
align-items: center;
|
|
@@ -3064,6 +3086,66 @@ button.pesquisa-coluna-remove:hover {
|
|
| 3064 |
justify-content: space-between;
|
| 3065 |
align-items: flex-start;
|
| 3066 |
gap: 12px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3067 |
}
|
| 3068 |
|
| 3069 |
.pesquisa-opened-model-title-wrap h3 {
|
|
@@ -5521,6 +5603,7 @@ button.btn-upload-select {
|
|
| 5521 |
background: #fff;
|
| 5522 |
min-height: 420px;
|
| 5523 |
padding: 8px;
|
|
|
|
| 5524 |
}
|
| 5525 |
|
| 5526 |
.plot-card-head {
|
|
@@ -5529,6 +5612,8 @@ button.btn-upload-select {
|
|
| 5529 |
display: grid;
|
| 5530 |
align-content: start;
|
| 5531 |
gap: 2px;
|
|
|
|
|
|
|
| 5532 |
}
|
| 5533 |
|
| 5534 |
.plot-card-title {
|
|
@@ -5547,6 +5632,10 @@ button.btn-upload-select {
|
|
| 5547 |
line-height: 1.15;
|
| 5548 |
}
|
| 5549 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5550 |
.plot-lazy-placeholder {
|
| 5551 |
min-height: 320px;
|
| 5552 |
display: flex;
|
|
@@ -6790,6 +6879,21 @@ button.btn-download-subtle {
|
|
| 6790 |
width: 100%;
|
| 6791 |
}
|
| 6792 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6793 |
.pesquisa-select-all {
|
| 6794 |
white-space: normal;
|
| 6795 |
}
|
|
|
|
| 780 |
color: #1b7a40;
|
| 781 |
}
|
| 782 |
|
| 783 |
+
.repo-open-btn.is-loading {
|
| 784 |
+
cursor: wait;
|
| 785 |
+
}
|
| 786 |
+
|
| 787 |
+
.repo-open-btn-spinner {
|
| 788 |
+
width: 14px;
|
| 789 |
+
height: 14px;
|
| 790 |
+
display: inline-block;
|
| 791 |
+
border-radius: 50%;
|
| 792 |
+
border: 2px solid currentColor;
|
| 793 |
+
border-right-color: transparent;
|
| 794 |
+
animation: spinLoader 0.7s linear infinite;
|
| 795 |
+
}
|
| 796 |
+
|
| 797 |
.repo-delete-icon-btn {
|
| 798 |
min-width: 28px;
|
| 799 |
min-height: 28px;
|
|
|
|
| 2998 |
.pesquisa-results-toolbar {
|
| 2999 |
display: flex;
|
| 3000 |
align-items: center;
|
| 3001 |
+
justify-content: flex-start;
|
| 3002 |
flex-wrap: wrap;
|
| 3003 |
gap: 10px;
|
| 3004 |
margin-bottom: 10px;
|
|
|
|
| 3021 |
min-height: 34px;
|
| 3022 |
}
|
| 3023 |
|
| 3024 |
+
.pesquisa-results-toolbar-actions {
|
| 3025 |
+
display: flex;
|
| 3026 |
+
align-items: center;
|
| 3027 |
+
gap: 10px;
|
| 3028 |
+
flex-wrap: wrap;
|
| 3029 |
+
margin-left: auto;
|
| 3030 |
+
}
|
| 3031 |
+
|
| 3032 |
.pesquisa-select-all {
|
| 3033 |
display: inline-flex;
|
| 3034 |
align-items: center;
|
|
|
|
| 3086 |
justify-content: space-between;
|
| 3087 |
align-items: flex-start;
|
| 3088 |
gap: 12px;
|
| 3089 |
+
padding-bottom: 12px;
|
| 3090 |
+
margin-bottom: 14px;
|
| 3091 |
+
border-bottom: 1px solid #dbe5ef;
|
| 3092 |
+
}
|
| 3093 |
+
|
| 3094 |
+
.pesquisa-opened-model-title-wrap {
|
| 3095 |
+
flex: 1 1 320px;
|
| 3096 |
+
}
|
| 3097 |
+
|
| 3098 |
+
.pesquisa-opened-model-actions {
|
| 3099 |
+
display: flex;
|
| 3100 |
+
align-items: center;
|
| 3101 |
+
justify-content: flex-end;
|
| 3102 |
+
gap: 10px;
|
| 3103 |
+
flex-wrap: wrap;
|
| 3104 |
+
}
|
| 3105 |
+
|
| 3106 |
+
.pesquisa-opened-model-action-btn {
|
| 3107 |
+
min-height: 36px;
|
| 3108 |
+
padding: 7px 12px;
|
| 3109 |
+
font-size: 0.82rem;
|
| 3110 |
+
}
|
| 3111 |
+
|
| 3112 |
+
.pesquisa-opened-model-action-btn-primary {
|
| 3113 |
+
--btn-bg-start: #3f90d5;
|
| 3114 |
+
--btn-bg-end: #2f79b8;
|
| 3115 |
+
--btn-border: #2a6da8;
|
| 3116 |
+
--btn-shadow-soft: rgba(42, 109, 168, 0.2);
|
| 3117 |
+
--btn-shadow-strong: rgba(42, 109, 168, 0.28);
|
| 3118 |
+
color: #fff;
|
| 3119 |
+
}
|
| 3120 |
+
|
| 3121 |
+
.pesquisa-opened-model-action-btn-secondary {
|
| 3122 |
+
--btn-bg-start: #f1f4f7;
|
| 3123 |
+
--btn-bg-end: #e4e9ef;
|
| 3124 |
+
--btn-border: #c6d0db;
|
| 3125 |
+
--btn-shadow-soft: rgba(66, 84, 103, 0.12);
|
| 3126 |
+
--btn-shadow-strong: rgba(66, 84, 103, 0.2);
|
| 3127 |
+
color: #35506a;
|
| 3128 |
+
}
|
| 3129 |
+
|
| 3130 |
+
.plot-card-toggle {
|
| 3131 |
+
display: inline-flex;
|
| 3132 |
+
align-items: center;
|
| 3133 |
+
gap: 8px;
|
| 3134 |
+
margin-top: 8px;
|
| 3135 |
+
color: #42586e;
|
| 3136 |
+
font-size: 0.8rem;
|
| 3137 |
+
font-weight: 700;
|
| 3138 |
+
position: relative;
|
| 3139 |
+
z-index: 2;
|
| 3140 |
+
pointer-events: auto;
|
| 3141 |
+
}
|
| 3142 |
+
|
| 3143 |
+
.plot-card-toggle.is-disabled {
|
| 3144 |
+
color: #7a8b9c;
|
| 3145 |
+
}
|
| 3146 |
+
|
| 3147 |
+
.plot-card-toggle input {
|
| 3148 |
+
margin: 0;
|
| 3149 |
}
|
| 3150 |
|
| 3151 |
.pesquisa-opened-model-title-wrap h3 {
|
|
|
|
| 5603 |
background: #fff;
|
| 5604 |
min-height: 420px;
|
| 5605 |
padding: 8px;
|
| 5606 |
+
position: relative;
|
| 5607 |
}
|
| 5608 |
|
| 5609 |
.plot-card-head {
|
|
|
|
| 5612 |
display: grid;
|
| 5613 |
align-content: start;
|
| 5614 |
gap: 2px;
|
| 5615 |
+
position: relative;
|
| 5616 |
+
z-index: 2;
|
| 5617 |
}
|
| 5618 |
|
| 5619 |
.plot-card-title {
|
|
|
|
| 5632 |
line-height: 1.15;
|
| 5633 |
}
|
| 5634 |
|
| 5635 |
+
.plot-card-body {
|
| 5636 |
+
position: relative;
|
| 5637 |
+
}
|
| 5638 |
+
|
| 5639 |
.plot-lazy-placeholder {
|
| 5640 |
min-height: 320px;
|
| 5641 |
display: flex;
|
|
|
|
| 6879 |
width: 100%;
|
| 6880 |
}
|
| 6881 |
|
| 6882 |
+
.pesquisa-results-toolbar-actions {
|
| 6883 |
+
width: 100%;
|
| 6884 |
+
justify-content: flex-end;
|
| 6885 |
+
margin-left: 0;
|
| 6886 |
+
}
|
| 6887 |
+
|
| 6888 |
+
.pesquisa-opened-model-head {
|
| 6889 |
+
flex-direction: column;
|
| 6890 |
+
}
|
| 6891 |
+
|
| 6892 |
+
.pesquisa-opened-model-actions {
|
| 6893 |
+
width: 100%;
|
| 6894 |
+
justify-content: flex-start;
|
| 6895 |
+
}
|
| 6896 |
+
|
| 6897 |
.pesquisa-select-all {
|
| 6898 |
white-space: normal;
|
| 6899 |
}
|