Spaces:
Running
Running
Guilherme Silberfarb Costa commited on
Commit ·
c4b0839
1
Parent(s): ca6595d
alteracoes generalizadas
Browse files- backend/app/api/pesquisa.py +2 -0
- backend/app/core/elaboracao/core.py +45 -1
- backend/app/core/visualizacao/app.py +28 -3
- backend/app/services/elaboracao_service.py +78 -5
- backend/app/services/pesquisa_service.py +99 -28
- backend/app/services/visualizacao_service.py +2 -2
- frontend/src/components/ElaboracaoTab.jsx +10 -2
- frontend/src/components/PesquisaTab.jsx +309 -30
- frontend/src/styles.css +198 -6
backend/app/api/pesquisa.py
CHANGED
|
@@ -47,6 +47,7 @@ def pesquisar_modelos(
|
|
| 47 |
autor: str | None = Query(None),
|
| 48 |
contem_app: str | None = Query(None),
|
| 49 |
tipo_modelo: str | None = Query(None),
|
|
|
|
| 50 |
finalidade: str | None = Query(None),
|
| 51 |
finalidade_colunas: str | None = Query(None),
|
| 52 |
bairro: str | None = Query(None),
|
|
@@ -89,6 +90,7 @@ def pesquisar_modelos(
|
|
| 89 |
autor=autor,
|
| 90 |
contem_app=contem_app,
|
| 91 |
tipo_modelo=tipo_modelo,
|
|
|
|
| 92 |
finalidade=finalidade,
|
| 93 |
finalidade_colunas=_split_csv(finalidade_colunas),
|
| 94 |
bairro=bairro,
|
|
|
|
| 47 |
autor: str | None = Query(None),
|
| 48 |
contem_app: str | None = Query(None),
|
| 49 |
tipo_modelo: str | None = Query(None),
|
| 50 |
+
negociacao_modelo: str | None = Query(None),
|
| 51 |
finalidade: str | None = Query(None),
|
| 52 |
finalidade_colunas: str | None = Query(None),
|
| 53 |
bairro: str | None = Query(None),
|
|
|
|
| 90 |
autor=autor,
|
| 91 |
contem_app=contem_app,
|
| 92 |
tipo_modelo=tipo_modelo,
|
| 93 |
+
negociacao_modelo=negociacao_modelo,
|
| 94 |
finalidade=finalidade,
|
| 95 |
finalidade_colunas=_split_csv(finalidade_colunas),
|
| 96 |
bairro=bairro,
|
backend/app/core/elaboracao/core.py
CHANGED
|
@@ -6,6 +6,7 @@ Contém: carregamento de dados, estatísticas, transformações, modelo OLS, dia
|
|
| 6 |
|
| 7 |
import os
|
| 8 |
import re
|
|
|
|
| 9 |
import pandas as pd
|
| 10 |
# Desabilita StringDtype para compatibilidade entre versões do pandas
|
| 11 |
pd.set_option('future.infer_string', False)
|
|
@@ -203,8 +204,51 @@ def _normalizar_data_iso(valor):
|
|
| 203 |
"""Converte valores de data para ISO (YYYY-MM-DD), retornando None se inválido."""
|
| 204 |
if valor is None:
|
| 205 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
try:
|
| 207 |
-
dt = pd.to_datetime(
|
| 208 |
except Exception:
|
| 209 |
return None
|
| 210 |
if pd.isna(dt):
|
|
|
|
| 6 |
|
| 7 |
import os
|
| 8 |
import re
|
| 9 |
+
from datetime import datetime
|
| 10 |
import pandas as pd
|
| 11 |
# Desabilita StringDtype para compatibilidade entre versões do pandas
|
| 12 |
pd.set_option('future.infer_string', False)
|
|
|
|
| 204 |
"""Converte valores de data para ISO (YYYY-MM-DD), retornando None se inválido."""
|
| 205 |
if valor is None:
|
| 206 |
return None
|
| 207 |
+
|
| 208 |
+
if isinstance(valor, pd.Timestamp):
|
| 209 |
+
if pd.isna(valor):
|
| 210 |
+
return None
|
| 211 |
+
return valor.date().isoformat()
|
| 212 |
+
|
| 213 |
+
texto = str(valor).strip()
|
| 214 |
+
if not texto:
|
| 215 |
+
return None
|
| 216 |
+
|
| 217 |
+
match_iso = re.match(r"^(\d{4})-(\d{2})-(\d{2})(?:[T\s].*)?$", texto)
|
| 218 |
+
if match_iso:
|
| 219 |
+
try:
|
| 220 |
+
return datetime(
|
| 221 |
+
int(match_iso.group(1)),
|
| 222 |
+
int(match_iso.group(2)),
|
| 223 |
+
int(match_iso.group(3)),
|
| 224 |
+
).date().isoformat()
|
| 225 |
+
except Exception:
|
| 226 |
+
return None
|
| 227 |
+
|
| 228 |
+
match_iso_slash = re.match(r"^(\d{4})/(\d{2})/(\d{2})(?:[T\s].*)?$", texto)
|
| 229 |
+
if match_iso_slash:
|
| 230 |
+
try:
|
| 231 |
+
return datetime(
|
| 232 |
+
int(match_iso_slash.group(1)),
|
| 233 |
+
int(match_iso_slash.group(2)),
|
| 234 |
+
int(match_iso_slash.group(3)),
|
| 235 |
+
).date().isoformat()
|
| 236 |
+
except Exception:
|
| 237 |
+
return None
|
| 238 |
+
|
| 239 |
+
match_br = re.match(r"^(\d{2})/(\d{2})/(\d{4})(?:[T\s].*)?$", texto)
|
| 240 |
+
if match_br:
|
| 241 |
+
try:
|
| 242 |
+
return datetime(
|
| 243 |
+
int(match_br.group(3)),
|
| 244 |
+
int(match_br.group(2)),
|
| 245 |
+
int(match_br.group(1)),
|
| 246 |
+
).date().isoformat()
|
| 247 |
+
except Exception:
|
| 248 |
+
return None
|
| 249 |
+
|
| 250 |
try:
|
| 251 |
+
dt = pd.to_datetime(texto, errors="coerce", dayfirst=True)
|
| 252 |
except Exception:
|
| 253 |
return None
|
| 254 |
if pd.isna(dt):
|
backend/app/core/visualizacao/app.py
CHANGED
|
@@ -10,6 +10,7 @@ from joblib import load
|
|
| 10 |
import os
|
| 11 |
import re
|
| 12 |
import traceback
|
|
|
|
| 13 |
|
| 14 |
|
| 15 |
# Importações para gráficos (trazidas de graficos.py)
|
|
@@ -1239,9 +1240,33 @@ def _formatar_badge_completo(pacote, nome_modelo=""):
|
|
| 1239 |
|
| 1240 |
def _data_br(value):
|
| 1241 |
texto = str(value or "").strip()
|
| 1242 |
-
|
| 1243 |
-
|
| 1244 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1245 |
return texto
|
| 1246 |
|
| 1247 |
model_name = str(nome_modelo or "").strip() or "-"
|
|
|
|
| 10 |
import os
|
| 11 |
import re
|
| 12 |
import traceback
|
| 13 |
+
from datetime import datetime
|
| 14 |
|
| 15 |
|
| 16 |
# Importações para gráficos (trazidas de graficos.py)
|
|
|
|
| 1240 |
|
| 1241 |
def _data_br(value):
|
| 1242 |
texto = str(value or "").strip()
|
| 1243 |
+
if not texto:
|
| 1244 |
+
return ""
|
| 1245 |
+
|
| 1246 |
+
match_iso = re.match(r"^(\d{4})-(\d{2})-(\d{2})(?:[T\s].*)?$", texto)
|
| 1247 |
+
if match_iso:
|
| 1248 |
+
try:
|
| 1249 |
+
dt = datetime(int(match_iso.group(1)), int(match_iso.group(2)), int(match_iso.group(3)))
|
| 1250 |
+
return dt.strftime("%d/%m/%Y")
|
| 1251 |
+
except Exception:
|
| 1252 |
+
return texto
|
| 1253 |
+
|
| 1254 |
+
match_iso_slash = re.match(r"^(\d{4})/(\d{2})/(\d{2})(?:[T\s].*)?$", texto)
|
| 1255 |
+
if match_iso_slash:
|
| 1256 |
+
try:
|
| 1257 |
+
dt = datetime(int(match_iso_slash.group(1)), int(match_iso_slash.group(2)), int(match_iso_slash.group(3)))
|
| 1258 |
+
return dt.strftime("%d/%m/%Y")
|
| 1259 |
+
except Exception:
|
| 1260 |
+
return texto
|
| 1261 |
+
|
| 1262 |
+
match_br = re.match(r"^(\d{2})/(\d{2})/(\d{4})(?:[T\s].*)?$", texto)
|
| 1263 |
+
if match_br:
|
| 1264 |
+
try:
|
| 1265 |
+
dt = datetime(int(match_br.group(3)), int(match_br.group(2)), int(match_br.group(1)))
|
| 1266 |
+
return dt.strftime("%d/%m/%Y")
|
| 1267 |
+
except Exception:
|
| 1268 |
+
return texto
|
| 1269 |
+
|
| 1270 |
return texto
|
| 1271 |
|
| 1272 |
model_name = str(nome_modelo or "").strip() or "-"
|
backend/app/services/elaboracao_service.py
CHANGED
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
| 2 |
|
| 3 |
import json
|
| 4 |
import os
|
|
|
|
|
|
|
| 5 |
from dataclasses import asdict
|
| 6 |
from pathlib import Path
|
| 7 |
from typing import Any
|
|
@@ -48,6 +50,77 @@ _AVALIADORES_PATH = Path(__file__).resolve().parent.parent / "core" / "elaboraca
|
|
| 48 |
_AVALIADORES_CACHE: list[dict[str, Any]] | None = None
|
| 49 |
|
| 50 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
def list_avaliadores() -> list[dict[str, Any]]:
|
| 52 |
global _AVALIADORES_CACHE
|
| 53 |
if _AVALIADORES_CACHE is not None:
|
|
@@ -212,12 +285,12 @@ def _normalizar_periodo_dados_mercado(periodo: dict[str, Any] | None) -> dict[st
|
|
| 212 |
if not isinstance(periodo, dict):
|
| 213 |
return {"coluna_data": None, "data_inicial": None, "data_final": None}
|
| 214 |
coluna_data = str(periodo.get("coluna_data") or "").strip() or None
|
| 215 |
-
inicio =
|
| 216 |
-
fim =
|
| 217 |
return {
|
| 218 |
"coluna_data": coluna_data,
|
| 219 |
-
"data_inicial":
|
| 220 |
-
"data_final":
|
| 221 |
}
|
| 222 |
|
| 223 |
|
|
@@ -271,7 +344,7 @@ def _converter_coluna_para_datas(
|
|
| 271 |
)
|
| 272 |
datas = pd.to_datetime(serie_num, unit="D", origin="1899-12-30", errors="coerce")
|
| 273 |
else:
|
| 274 |
-
datas =
|
| 275 |
|
| 276 |
datas_validas = datas[mascara_preenchida].dropna()
|
| 277 |
proporcao = len(datas_validas) / total_preenchido if total_preenchido else 0.0
|
|
|
|
| 2 |
|
| 3 |
import json
|
| 4 |
import os
|
| 5 |
+
import re
|
| 6 |
+
from datetime import datetime
|
| 7 |
from dataclasses import asdict
|
| 8 |
from pathlib import Path
|
| 9 |
from typing import Any
|
|
|
|
| 50 |
_AVALIADORES_CACHE: list[dict[str, Any]] | None = None
|
| 51 |
|
| 52 |
|
| 53 |
+
def _parse_data_iso_segura(value: Any) -> str | None:
|
| 54 |
+
if value is None:
|
| 55 |
+
return None
|
| 56 |
+
if isinstance(value, pd.Timestamp):
|
| 57 |
+
if pd.isna(value):
|
| 58 |
+
return None
|
| 59 |
+
return value.date().isoformat()
|
| 60 |
+
|
| 61 |
+
text = str(value).strip()
|
| 62 |
+
if not text:
|
| 63 |
+
return None
|
| 64 |
+
|
| 65 |
+
iso_match = re.match(r"^(\d{4})-(\d{2})-(\d{2})(?:[T\s].*)?$", text)
|
| 66 |
+
if iso_match:
|
| 67 |
+
try:
|
| 68 |
+
return datetime(
|
| 69 |
+
int(iso_match.group(1)),
|
| 70 |
+
int(iso_match.group(2)),
|
| 71 |
+
int(iso_match.group(3)),
|
| 72 |
+
).date().isoformat()
|
| 73 |
+
except Exception:
|
| 74 |
+
return None
|
| 75 |
+
|
| 76 |
+
iso_slash_match = re.match(r"^(\d{4})/(\d{2})/(\d{2})(?:[T\s].*)?$", text)
|
| 77 |
+
if iso_slash_match:
|
| 78 |
+
try:
|
| 79 |
+
return datetime(
|
| 80 |
+
int(iso_slash_match.group(1)),
|
| 81 |
+
int(iso_slash_match.group(2)),
|
| 82 |
+
int(iso_slash_match.group(3)),
|
| 83 |
+
).date().isoformat()
|
| 84 |
+
except Exception:
|
| 85 |
+
return None
|
| 86 |
+
|
| 87 |
+
br_match = re.match(r"^(\d{2})/(\d{2})/(\d{4})(?:[T\s].*)?$", text)
|
| 88 |
+
if br_match:
|
| 89 |
+
try:
|
| 90 |
+
return datetime(
|
| 91 |
+
int(br_match.group(3)),
|
| 92 |
+
int(br_match.group(2)),
|
| 93 |
+
int(br_match.group(1)),
|
| 94 |
+
).date().isoformat()
|
| 95 |
+
except Exception:
|
| 96 |
+
return None
|
| 97 |
+
|
| 98 |
+
parsed = pd.to_datetime(text, errors="coerce", dayfirst=True)
|
| 99 |
+
if pd.isna(parsed):
|
| 100 |
+
return None
|
| 101 |
+
return parsed.date().isoformat()
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _parse_serie_datas_texto_segura(serie_texto: pd.Series) -> pd.Series:
|
| 105 |
+
preenchidos = serie_texto.dropna().astype(str).str.strip()
|
| 106 |
+
if preenchidos.empty:
|
| 107 |
+
return pd.to_datetime(serie_texto, errors="coerce")
|
| 108 |
+
|
| 109 |
+
if preenchidos.str.match(r"^\d{4}-\d{2}-\d{2}(?:[T\s].*)?$").all():
|
| 110 |
+
base = serie_texto.astype(str).str.slice(0, 10)
|
| 111 |
+
return pd.to_datetime(base, format="%Y-%m-%d", errors="coerce")
|
| 112 |
+
|
| 113 |
+
if preenchidos.str.match(r"^\d{4}/\d{2}/\d{2}(?:[T\s].*)?$").all():
|
| 114 |
+
base = serie_texto.astype(str).str.slice(0, 10)
|
| 115 |
+
return pd.to_datetime(base, format="%Y/%m/%d", errors="coerce")
|
| 116 |
+
|
| 117 |
+
if preenchidos.str.match(r"^\d{2}/\d{2}/\d{4}(?:[T\s].*)?$").all():
|
| 118 |
+
base = serie_texto.astype(str).str.slice(0, 10)
|
| 119 |
+
return pd.to_datetime(base, format="%d/%m/%Y", errors="coerce")
|
| 120 |
+
|
| 121 |
+
return pd.to_datetime(serie_texto, errors="coerce", dayfirst=True)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
def list_avaliadores() -> list[dict[str, Any]]:
|
| 125 |
global _AVALIADORES_CACHE
|
| 126 |
if _AVALIADORES_CACHE is not None:
|
|
|
|
| 285 |
if not isinstance(periodo, dict):
|
| 286 |
return {"coluna_data": None, "data_inicial": None, "data_final": None}
|
| 287 |
coluna_data = str(periodo.get("coluna_data") or "").strip() or None
|
| 288 |
+
inicio = _parse_data_iso_segura(periodo.get("data_inicial"))
|
| 289 |
+
fim = _parse_data_iso_segura(periodo.get("data_final"))
|
| 290 |
return {
|
| 291 |
"coluna_data": coluna_data,
|
| 292 |
+
"data_inicial": inicio,
|
| 293 |
+
"data_final": fim,
|
| 294 |
}
|
| 295 |
|
| 296 |
|
|
|
|
| 344 |
)
|
| 345 |
datas = pd.to_datetime(serie_num, unit="D", origin="1899-12-30", errors="coerce")
|
| 346 |
else:
|
| 347 |
+
datas = _parse_serie_datas_texto_segura(serie_base)
|
| 348 |
|
| 349 |
datas_validas = datas[mascara_preenchida].dropna()
|
| 350 |
proporcao = len(datas_validas) / total_preenchido if total_preenchido else 0.0
|
backend/app/services/pesquisa_service.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
| 3 |
-
import json
|
| 4 |
import math
|
| 5 |
import re
|
| 6 |
import unicodedata
|
|
@@ -20,8 +19,6 @@ from app.core.elaboracao.core import _migrar_pacote_v1_para_v2
|
|
| 20 |
from app.services import model_repository
|
| 21 |
from app.services.serializers import sanitize_value
|
| 22 |
|
| 23 |
-
ADMIN_CONFIG_PATH = Path(__file__).resolve().parent.parent / "core" / "pesquisa" / "pesquisa_admin_config.json"
|
| 24 |
-
|
| 25 |
AREA_PRIVATIVA_ALIASES = ["APRIV", "APRIVEQ", "ATPRIV", "ACOPRIV", "AREAPRIV", "AREA_PRIVATIVA", "AREA PRIVATIVA"]
|
| 26 |
AREA_TOTAL_ALIASES = ["ATTOTAL", "ATOTAL", "ATOT", "AREA_TOTAL", "AREA TOTAL", "AREA"]
|
| 27 |
AREA_GERAL_ALIASES = AREA_PRIVATIVA_ALIASES + AREA_TOTAL_ALIASES + ["ACONST", "ALOC"]
|
|
@@ -153,6 +150,7 @@ class PesquisaFiltros:
|
|
| 153 |
autor: str | None = None
|
| 154 |
contem_app: str | None = None
|
| 155 |
tipo_modelo: str | None = None
|
|
|
|
| 156 |
finalidade: str | None = None
|
| 157 |
finalidade_colunas: list[str] | None = None
|
| 158 |
bairro: str | None = None
|
|
@@ -193,6 +191,7 @@ _CACHE_LOCK = Lock()
|
|
| 193 |
_CACHE: dict[str, dict[str, Any]] = {}
|
| 194 |
_ADMIN_CONFIG_LOCK = Lock()
|
| 195 |
_CACHE_SOURCE_SIGNATURE: str | None = None
|
|
|
|
| 196 |
|
| 197 |
|
| 198 |
def _resolver_repositorio_modelos() -> model_repository.ModelRepositoryResolution:
|
|
@@ -233,13 +232,13 @@ def salvar_admin_config_pesquisa(campos: dict[str, list[str]] | None) -> dict[st
|
|
| 233 |
todos = [_carregar_resumo_com_cache(caminho) for caminho in modelos]
|
| 234 |
colunas_filtro = _montar_config_colunas_filtro(todos)
|
| 235 |
admin_fontes = _normalizar_fontes_admin(campos or {}, colunas_filtro)
|
| 236 |
-
|
| 237 |
return sanitize_value(
|
| 238 |
{
|
| 239 |
"colunas_filtro": colunas_filtro,
|
| 240 |
"admin_fontes": admin_fontes,
|
| 241 |
"total_modelos": len(todos),
|
| 242 |
-
"status": "Configuracao de busca
|
| 243 |
"fonte_modelos": resolved.as_payload(),
|
| 244 |
}
|
| 245 |
)
|
|
@@ -274,6 +273,7 @@ def listar_modelos(filtros: PesquisaFiltros, limite: int | None = None, somente_
|
|
| 274 |
"autor": filtros.autor,
|
| 275 |
"contem_app": filtros.contem_app,
|
| 276 |
"tipo_modelo": filtros.tipo_modelo,
|
|
|
|
| 277 |
"finalidade": filtros.finalidade,
|
| 278 |
"finalidade_colunas": filtros.finalidade_colunas or [],
|
| 279 |
"bairro": filtros.bairro,
|
|
@@ -339,6 +339,7 @@ def listar_modelos(filtros: PesquisaFiltros, limite: int | None = None, somente_
|
|
| 339 |
"autor": filtros.autor,
|
| 340 |
"contem_app": filtros.contem_app,
|
| 341 |
"tipo_modelo": filtros.tipo_modelo,
|
|
|
|
| 342 |
"finalidade": filtros.finalidade,
|
| 343 |
"finalidade_colunas": filtros.finalidade_colunas or [],
|
| 344 |
"bairro": filtros.bairro,
|
|
@@ -651,7 +652,45 @@ def _faixa_data_do_pacote(pacote: dict[str, Any]) -> dict[str, Any] | None:
|
|
| 651 |
def _data_iso_or_none(value: Any) -> str | None:
|
| 652 |
if _is_empty(value):
|
| 653 |
return None
|
| 654 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 655 |
if pd.isna(parsed):
|
| 656 |
return None
|
| 657 |
return parsed.date().isoformat()
|
|
@@ -857,10 +896,17 @@ def _extrair_faixa_data_real_serie(serie: pd.Series) -> dict[str, Any] | None:
|
|
| 857 |
if proporcao_ano_puro >= 0.8:
|
| 858 |
return None
|
| 859 |
|
| 860 |
-
|
| 861 |
-
serie_data = pd.to_datetime(bruto, errors="coerce",
|
| 862 |
-
|
| 863 |
-
serie_data = pd.to_datetime(bruto, errors="coerce",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 864 |
total = len(bruto)
|
| 865 |
minimo_amostras = min(3, total)
|
| 866 |
if len(serie_data) < minimo_amostras:
|
|
@@ -1000,6 +1046,10 @@ def _aceita_filtros(modelo: dict[str, Any], filtros: PesquisaFiltros, fontes_adm
|
|
| 1000 |
if filtros.tipo_modelo and not _contains_any([_tipo_modelo_modelo(modelo)], filtros.tipo_modelo):
|
| 1001 |
return False
|
| 1002 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1003 |
if filtros.finalidade and not _aceita_texto_com_colunas(modelo, filtros.finalidade, "finalidade", fontes_admin.get("finalidade")):
|
| 1004 |
return False
|
| 1005 |
|
|
@@ -1041,6 +1091,17 @@ def _normalizar_contem_app(value: str | None) -> bool | None:
|
|
| 1041 |
return None
|
| 1042 |
|
| 1043 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1044 |
def _normalizar_otica(value: str | None) -> str:
|
| 1045 |
return "avaliando"
|
| 1046 |
|
|
@@ -1063,13 +1124,23 @@ def _anexar_avaliando_info(
|
|
| 1063 |
|
| 1064 |
finalidade_info = filtros.aval_finalidade
|
| 1065 |
if _is_provided(finalidade_info):
|
| 1066 |
-
|
| 1067 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1068 |
|
| 1069 |
bairro_info = filtros.aval_bairro
|
| 1070 |
if _is_provided(bairro_info):
|
| 1071 |
-
|
| 1072 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1073 |
|
| 1074 |
endereco_info = filtros.aval_endereco
|
| 1075 |
if _is_provided(endereco_info):
|
|
@@ -1222,26 +1293,16 @@ def _montar_config_colunas_filtro(modelos: list[dict[str, Any]]) -> dict[str, An
|
|
| 1222 |
|
| 1223 |
|
| 1224 |
def _carregar_fontes_admin(colunas_filtro: dict[str, Any]) -> dict[str, list[str]]:
|
| 1225 |
-
raw: dict[str, Any] = {}
|
| 1226 |
with _ADMIN_CONFIG_LOCK:
|
| 1227 |
-
|
| 1228 |
-
try:
|
| 1229 |
-
raw_file = json.loads(ADMIN_CONFIG_PATH.read_text(encoding="utf-8"))
|
| 1230 |
-
if isinstance(raw_file, dict):
|
| 1231 |
-
raw = raw_file
|
| 1232 |
-
except Exception:
|
| 1233 |
-
raw = {}
|
| 1234 |
return _normalizar_fontes_admin(raw, colunas_filtro)
|
| 1235 |
|
| 1236 |
|
| 1237 |
-
def
|
| 1238 |
-
ADMIN_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
| 1239 |
payload = {campo: _dedupe_strings(valores) for campo, valores in (fontes_admin or {}).items()}
|
|
|
|
| 1240 |
with _ADMIN_CONFIG_LOCK:
|
| 1241 |
-
|
| 1242 |
-
json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True),
|
| 1243 |
-
encoding="utf-8",
|
| 1244 |
-
)
|
| 1245 |
|
| 1246 |
|
| 1247 |
def _normalizar_fontes_admin(raw: dict[str, Any], colunas_filtro: dict[str, Any]) -> dict[str, list[str]]:
|
|
@@ -1697,6 +1758,16 @@ def _tipo_modelo_modelo(modelo: dict[str, Any]) -> str | None:
|
|
| 1697 |
return _inferir_tipo_por_nome(nome_referencia)
|
| 1698 |
|
| 1699 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1700 |
def _extrair_termos_bairro(filtros: PesquisaFiltros) -> list[str]:
|
| 1701 |
termos: list[str] = []
|
| 1702 |
if filtros.bairro:
|
|
|
|
| 1 |
from __future__ import annotations
|
| 2 |
|
|
|
|
| 3 |
import math
|
| 4 |
import re
|
| 5 |
import unicodedata
|
|
|
|
| 19 |
from app.services import model_repository
|
| 20 |
from app.services.serializers import sanitize_value
|
| 21 |
|
|
|
|
|
|
|
| 22 |
AREA_PRIVATIVA_ALIASES = ["APRIV", "APRIVEQ", "ATPRIV", "ACOPRIV", "AREAPRIV", "AREA_PRIVATIVA", "AREA PRIVATIVA"]
|
| 23 |
AREA_TOTAL_ALIASES = ["ATTOTAL", "ATOTAL", "ATOT", "AREA_TOTAL", "AREA TOTAL", "AREA"]
|
| 24 |
AREA_GERAL_ALIASES = AREA_PRIVATIVA_ALIASES + AREA_TOTAL_ALIASES + ["ACONST", "ALOC"]
|
|
|
|
| 150 |
autor: str | None = None
|
| 151 |
contem_app: str | None = None
|
| 152 |
tipo_modelo: str | None = None
|
| 153 |
+
negociacao_modelo: str | None = None
|
| 154 |
finalidade: str | None = None
|
| 155 |
finalidade_colunas: list[str] | None = None
|
| 156 |
bairro: str | None = None
|
|
|
|
| 191 |
_CACHE: dict[str, dict[str, Any]] = {}
|
| 192 |
_ADMIN_CONFIG_LOCK = Lock()
|
| 193 |
_CACHE_SOURCE_SIGNATURE: str | None = None
|
| 194 |
+
_ADMIN_FONTES_SESSION: dict[str, list[str]] = {}
|
| 195 |
|
| 196 |
|
| 197 |
def _resolver_repositorio_modelos() -> model_repository.ModelRepositoryResolution:
|
|
|
|
| 232 |
todos = [_carregar_resumo_com_cache(caminho) for caminho in modelos]
|
| 233 |
colunas_filtro = _montar_config_colunas_filtro(todos)
|
| 234 |
admin_fontes = _normalizar_fontes_admin(campos or {}, colunas_filtro)
|
| 235 |
+
_salvar_fontes_admin_sessao(admin_fontes)
|
| 236 |
return sanitize_value(
|
| 237 |
{
|
| 238 |
"colunas_filtro": colunas_filtro,
|
| 239 |
"admin_fontes": admin_fontes,
|
| 240 |
"total_modelos": len(todos),
|
| 241 |
+
"status": "Configuracao de busca aplicada para a sessao atual.",
|
| 242 |
"fonte_modelos": resolved.as_payload(),
|
| 243 |
}
|
| 244 |
)
|
|
|
|
| 273 |
"autor": filtros.autor,
|
| 274 |
"contem_app": filtros.contem_app,
|
| 275 |
"tipo_modelo": filtros.tipo_modelo,
|
| 276 |
+
"negociacao_modelo": filtros.negociacao_modelo,
|
| 277 |
"finalidade": filtros.finalidade,
|
| 278 |
"finalidade_colunas": filtros.finalidade_colunas or [],
|
| 279 |
"bairro": filtros.bairro,
|
|
|
|
| 339 |
"autor": filtros.autor,
|
| 340 |
"contem_app": filtros.contem_app,
|
| 341 |
"tipo_modelo": filtros.tipo_modelo,
|
| 342 |
+
"negociacao_modelo": filtros.negociacao_modelo,
|
| 343 |
"finalidade": filtros.finalidade,
|
| 344 |
"finalidade_colunas": filtros.finalidade_colunas or [],
|
| 345 |
"bairro": filtros.bairro,
|
|
|
|
| 652 |
def _data_iso_or_none(value: Any) -> str | None:
|
| 653 |
if _is_empty(value):
|
| 654 |
return None
|
| 655 |
+
|
| 656 |
+
text = str(value).strip()
|
| 657 |
+
if not text:
|
| 658 |
+
return None
|
| 659 |
+
|
| 660 |
+
iso_match = re.match(r"^(\d{4})-(\d{2})-(\d{2})(?:[T\s].*)?$", text)
|
| 661 |
+
if iso_match:
|
| 662 |
+
try:
|
| 663 |
+
return datetime(
|
| 664 |
+
int(iso_match.group(1)),
|
| 665 |
+
int(iso_match.group(2)),
|
| 666 |
+
int(iso_match.group(3)),
|
| 667 |
+
).date().isoformat()
|
| 668 |
+
except Exception:
|
| 669 |
+
return None
|
| 670 |
+
|
| 671 |
+
iso_slash_match = re.match(r"^(\d{4})/(\d{2})/(\d{2})(?:[T\s].*)?$", text)
|
| 672 |
+
if iso_slash_match:
|
| 673 |
+
try:
|
| 674 |
+
return datetime(
|
| 675 |
+
int(iso_slash_match.group(1)),
|
| 676 |
+
int(iso_slash_match.group(2)),
|
| 677 |
+
int(iso_slash_match.group(3)),
|
| 678 |
+
).date().isoformat()
|
| 679 |
+
except Exception:
|
| 680 |
+
return None
|
| 681 |
+
|
| 682 |
+
br_match = re.match(r"^(\d{2})/(\d{2})/(\d{4})(?:[T\s].*)?$", text)
|
| 683 |
+
if br_match:
|
| 684 |
+
try:
|
| 685 |
+
return datetime(
|
| 686 |
+
int(br_match.group(3)),
|
| 687 |
+
int(br_match.group(2)),
|
| 688 |
+
int(br_match.group(1)),
|
| 689 |
+
).date().isoformat()
|
| 690 |
+
except Exception:
|
| 691 |
+
return None
|
| 692 |
+
|
| 693 |
+
parsed = pd.to_datetime(text, errors="coerce", dayfirst=True)
|
| 694 |
if pd.isna(parsed):
|
| 695 |
return None
|
| 696 |
return parsed.date().isoformat()
|
|
|
|
| 896 |
if proporcao_ano_puro >= 0.8:
|
| 897 |
return None
|
| 898 |
|
| 899 |
+
if bruto.str.match(r"^\d{4}-\d{2}-\d{2}(?:[T\s].*)?$").all():
|
| 900 |
+
serie_data = pd.to_datetime(bruto.str.slice(0, 10), errors="coerce", format="%Y-%m-%d").dropna()
|
| 901 |
+
elif bruto.str.match(r"^\d{4}/\d{2}/\d{2}(?:[T\s].*)?$").all():
|
| 902 |
+
serie_data = pd.to_datetime(bruto.str.slice(0, 10), errors="coerce", format="%Y/%m/%d").dropna()
|
| 903 |
+
elif bruto.str.match(r"^\d{2}/\d{2}/\d{4}(?:[T\s].*)?$").all():
|
| 904 |
+
serie_data = pd.to_datetime(bruto.str.slice(0, 10), errors="coerce", format="%d/%m/%Y").dropna()
|
| 905 |
+
else:
|
| 906 |
+
try:
|
| 907 |
+
serie_data = pd.to_datetime(bruto, errors="coerce", dayfirst=True, format="mixed").dropna()
|
| 908 |
+
except TypeError:
|
| 909 |
+
serie_data = pd.to_datetime(bruto, errors="coerce", dayfirst=True).dropna()
|
| 910 |
total = len(bruto)
|
| 911 |
minimo_amostras = min(3, total)
|
| 912 |
if len(serie_data) < minimo_amostras:
|
|
|
|
| 1046 |
if filtros.tipo_modelo and not _contains_any([_tipo_modelo_modelo(modelo)], filtros.tipo_modelo):
|
| 1047 |
return False
|
| 1048 |
|
| 1049 |
+
negociacao_modelo = _normalizar_negociacao_modelo(filtros.negociacao_modelo)
|
| 1050 |
+
if negociacao_modelo and _negociacao_modelo_modelo(modelo) != negociacao_modelo:
|
| 1051 |
+
return False
|
| 1052 |
+
|
| 1053 |
if filtros.finalidade and not _aceita_texto_com_colunas(modelo, filtros.finalidade, "finalidade", fontes_admin.get("finalidade")):
|
| 1054 |
return False
|
| 1055 |
|
|
|
|
| 1091 |
return None
|
| 1092 |
|
| 1093 |
|
| 1094 |
+
def _normalizar_negociacao_modelo(value: str | None) -> str | None:
|
| 1095 |
+
chave = _normalize(value or "")
|
| 1096 |
+
if not chave:
|
| 1097 |
+
return None
|
| 1098 |
+
if chave in {"aluguel", "locacao", "a"}:
|
| 1099 |
+
return "aluguel"
|
| 1100 |
+
if chave in {"venda", "v"}:
|
| 1101 |
+
return "venda"
|
| 1102 |
+
return None
|
| 1103 |
+
|
| 1104 |
+
|
| 1105 |
def _normalizar_otica(value: str | None) -> str:
|
| 1106 |
return "avaliando"
|
| 1107 |
|
|
|
|
| 1124 |
|
| 1125 |
finalidade_info = filtros.aval_finalidade
|
| 1126 |
if _is_provided(finalidade_info):
|
| 1127 |
+
termos_finalidade = _split_terms(str(finalidade_info))
|
| 1128 |
+
aceito = any(
|
| 1129 |
+
_aceita_texto_com_colunas(item, termo, "aval_finalidade", fontes_admin.get("aval_finalidade"))
|
| 1130 |
+
for termo in termos_finalidade
|
| 1131 |
+
) if termos_finalidade else False
|
| 1132 |
+
detalhe = "nenhuma das finalidades informadas foi encontrada no modelo" if len(termos_finalidade) > 1 else "nao encontrada no modelo"
|
| 1133 |
+
registrar("finalidade", finalidade_info, aceito, detalhe)
|
| 1134 |
|
| 1135 |
bairro_info = filtros.aval_bairro
|
| 1136 |
if _is_provided(bairro_info):
|
| 1137 |
+
termos_bairro = _split_terms(str(bairro_info))
|
| 1138 |
+
aceito = any(
|
| 1139 |
+
_aceita_texto_com_colunas(item, termo, "aval_bairro", fontes_admin.get("aval_bairro"))
|
| 1140 |
+
for termo in termos_bairro
|
| 1141 |
+
) if termos_bairro else False
|
| 1142 |
+
detalhe = "nenhum dos bairros informados esta na cobertura do modelo" if len(termos_bairro) > 1 else "bairro fora da cobertura do modelo"
|
| 1143 |
+
registrar("bairro", bairro_info, aceito, detalhe)
|
| 1144 |
|
| 1145 |
endereco_info = filtros.aval_endereco
|
| 1146 |
if _is_provided(endereco_info):
|
|
|
|
| 1293 |
|
| 1294 |
|
| 1295 |
def _carregar_fontes_admin(colunas_filtro: dict[str, Any]) -> dict[str, list[str]]:
|
|
|
|
| 1296 |
with _ADMIN_CONFIG_LOCK:
|
| 1297 |
+
raw = dict(_ADMIN_FONTES_SESSION or {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1298 |
return _normalizar_fontes_admin(raw, colunas_filtro)
|
| 1299 |
|
| 1300 |
|
| 1301 |
+
def _salvar_fontes_admin_sessao(fontes_admin: dict[str, list[str]]) -> None:
|
|
|
|
| 1302 |
payload = {campo: _dedupe_strings(valores) for campo, valores in (fontes_admin or {}).items()}
|
| 1303 |
+
global _ADMIN_FONTES_SESSION
|
| 1304 |
with _ADMIN_CONFIG_LOCK:
|
| 1305 |
+
_ADMIN_FONTES_SESSION = payload
|
|
|
|
|
|
|
|
|
|
| 1306 |
|
| 1307 |
|
| 1308 |
def _normalizar_fontes_admin(raw: dict[str, Any], colunas_filtro: dict[str, Any]) -> dict[str, list[str]]:
|
|
|
|
| 1758 |
return _inferir_tipo_por_nome(nome_referencia)
|
| 1759 |
|
| 1760 |
|
| 1761 |
+
def _negociacao_modelo_modelo(modelo: dict[str, Any]) -> str | None:
|
| 1762 |
+
nome_referencia = _str_or_none(modelo.get("arquivo")) or _str_or_none(modelo.get("nome_modelo")) or ""
|
| 1763 |
+
nome_upper = nome_referencia.upper()
|
| 1764 |
+
if re.search(r"(^|_)A(_|$)", nome_upper):
|
| 1765 |
+
return "aluguel"
|
| 1766 |
+
if re.search(r"(^|_)V(_|$)", nome_upper):
|
| 1767 |
+
return "venda"
|
| 1768 |
+
return None
|
| 1769 |
+
|
| 1770 |
+
|
| 1771 |
def _extrair_termos_bairro(filtros: PesquisaFiltros) -> list[str]:
|
| 1772 |
termos: list[str] = []
|
| 1773 |
if filtros.bairro:
|
backend/app/services/visualizacao_service.py
CHANGED
|
@@ -136,7 +136,7 @@ def exibir_modelo(session: SessionState) -> dict[str, Any]:
|
|
| 136 |
mask = tab_coef["Variável"].astype(str).str.lower().isin(["intercept", "const", "(intercept)"])
|
| 137 |
if mask.any():
|
| 138 |
tab_coef = pd.concat([tab_coef[mask], tab_coef[~mask]], ignore_index=True)
|
| 139 |
-
tab_coef = tab_coef.round(
|
| 140 |
|
| 141 |
tab_obs_calc = pacote["modelo"]["obs_calc"].reset_index().round(2)
|
| 142 |
|
|
@@ -169,7 +169,7 @@ def exibir_modelo(session: SessionState) -> dict[str, Any]:
|
|
| 169 |
"escalas_html": escalas_html,
|
| 170 |
"dados_transformados": dataframe_to_payload(df_xy, decimals=2),
|
| 171 |
"resumo_html": resumo_html,
|
| 172 |
-
"coeficientes": dataframe_to_payload(tab_coef, decimals=
|
| 173 |
"obs_calc": dataframe_to_payload(tab_obs_calc, decimals=2),
|
| 174 |
"grafico_obs_calc": figure_to_payload(figs.get("obs_calc")),
|
| 175 |
"grafico_residuos": figure_to_payload(figs.get("residuos")),
|
|
|
|
| 136 |
mask = tab_coef["Variável"].astype(str).str.lower().isin(["intercept", "const", "(intercept)"])
|
| 137 |
if mask.any():
|
| 138 |
tab_coef = pd.concat([tab_coef[mask], tab_coef[~mask]], ignore_index=True)
|
| 139 |
+
tab_coef = tab_coef.round(4)
|
| 140 |
|
| 141 |
tab_obs_calc = pacote["modelo"]["obs_calc"].reset_index().round(2)
|
| 142 |
|
|
|
|
| 169 |
"escalas_html": escalas_html,
|
| 170 |
"dados_transformados": dataframe_to_payload(df_xy, decimals=2),
|
| 171 |
"resumo_html": resumo_html,
|
| 172 |
+
"coeficientes": dataframe_to_payload(tab_coef, decimals=4),
|
| 173 |
"obs_calc": dataframe_to_payload(tab_obs_calc, decimals=2),
|
| 174 |
"grafico_obs_calc": figure_to_payload(figs.get("obs_calc")),
|
| 175 |
"grafico_residuos": figure_to_payload(figs.get("residuos")),
|
frontend/src/components/ElaboracaoTab.jsx
CHANGED
|
@@ -27,6 +27,7 @@ const GRAU_LABEL_CURTO = {
|
|
| 27 |
2: 'Grau II',
|
| 28 |
3: 'Grau III',
|
| 29 |
}
|
|
|
|
| 30 |
|
| 31 |
function grauBadgeClass(value) {
|
| 32 |
const grau = Number(value)
|
|
@@ -3696,7 +3697,9 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 3696 |
</div>
|
| 3697 |
<div className="outlier-actions-row">
|
| 3698 |
<button onClick={onApplyOutlierFilters} disabled={loading}>Aplicar filtros</button>
|
| 3699 |
-
<
|
|
|
|
|
|
|
| 3700 |
<button type="button" className="btn-filtro-add" onClick={onAddFiltro} disabled={loading}>Adicionar filtro</button>
|
| 3701 |
</div>
|
| 3702 |
</div>
|
|
@@ -3707,7 +3710,12 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 3707 |
<div className="outlier-inputs-grid">
|
| 3708 |
<div className="outlier-input-card">
|
| 3709 |
<label>A excluir</label>
|
| 3710 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3711 |
</div>
|
| 3712 |
<div className="outlier-input-card">
|
| 3713 |
<label>A reincluir</label>
|
|
|
|
| 27 |
2: 'Grau II',
|
| 28 |
3: 'Grau III',
|
| 29 |
}
|
| 30 |
+
const OUTLIER_RECURSIVO_TOOLTIP = 'Aplicar com recursividade executa os mesmos filtros em ciclos sucessivos: nos bastidores, simula a exclusão dos índices encontrados, recalcula o ajuste do modelo e as métricas de outlier e reaplica os filtros, repetindo até não surgir nenhum índice novo. Para você, o resultado prático é que o campo "A excluir" é preenchido automaticamente com o conjunto total de índices encontrados nessa simulação recursiva.'
|
| 31 |
|
| 32 |
function grauBadgeClass(value) {
|
| 33 |
const grau = Number(value)
|
|
|
|
| 3697 |
</div>
|
| 3698 |
<div className="outlier-actions-row">
|
| 3699 |
<button onClick={onApplyOutlierFilters} disabled={loading}>Aplicar filtros</button>
|
| 3700 |
+
<span className="btn-filtro-recursivo-wrap" data-tooltip={OUTLIER_RECURSIVO_TOOLTIP}>
|
| 3701 |
+
<button type="button" className="btn-filtro-recursivo" onClick={onApplyOutlierFiltersRecursive} disabled={loading}>Aplicar com recursividade</button>
|
| 3702 |
+
</span>
|
| 3703 |
<button type="button" className="btn-filtro-add" onClick={onAddFiltro} disabled={loading}>Adicionar filtro</button>
|
| 3704 |
</div>
|
| 3705 |
</div>
|
|
|
|
| 3710 |
<div className="outlier-inputs-grid">
|
| 3711 |
<div className="outlier-input-card">
|
| 3712 |
<label>A excluir</label>
|
| 3713 |
+
<textarea
|
| 3714 |
+
rows={2}
|
| 3715 |
+
value={outliersTexto}
|
| 3716 |
+
onChange={(e) => setOutliersTexto(e.target.value)}
|
| 3717 |
+
placeholder="ex: 5, 12, 30"
|
| 3718 |
+
/>
|
| 3719 |
</div>
|
| 3720 |
<div className="outlier-input-card">
|
| 3721 |
<label>A reincluir</label>
|
frontend/src/components/PesquisaTab.jsx
CHANGED
|
@@ -7,6 +7,7 @@ import SectionBlock from './SectionBlock'
|
|
| 7 |
const EMPTY_FILTERS = {
|
| 8 |
contemApp: '',
|
| 9 |
tipoModelo: '',
|
|
|
|
| 10 |
dataMin: '',
|
| 11 |
dataMax: '',
|
| 12 |
avalFinalidade: '',
|
|
@@ -75,6 +76,37 @@ function normalizeTokenText(value) {
|
|
| 75 |
.toUpperCase()
|
| 76 |
}
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
function inferTipoPorNomeModelo(...nomes) {
|
| 79 |
const tokens = Object.keys(TIPO_SIGLAS).sort((a, b) => b.length - a.length)
|
| 80 |
for (const nome of nomes) {
|
|
@@ -106,6 +138,7 @@ function buildApiFilters(filters) {
|
|
| 106 |
otica: 'avaliando',
|
| 107 |
contem_app: filters.contemApp,
|
| 108 |
tipo_modelo: filters.tipoModelo,
|
|
|
|
| 109 |
aval_finalidade: filters.avalFinalidade,
|
| 110 |
aval_bairro: filters.avalBairro,
|
| 111 |
data_min: filters.dataMin,
|
|
@@ -163,6 +196,245 @@ function DateFieldInput({ field, ...props }) {
|
|
| 163 |
)
|
| 164 |
}
|
| 165 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
export default function PesquisaTab() {
|
| 167 |
const [loading, setLoading] = useState(false)
|
| 168 |
const [error, setError] = useState('')
|
|
@@ -380,7 +652,7 @@ export default function PesquisaTab() {
|
|
| 380 |
</select>
|
| 381 |
</label>
|
| 382 |
<label className="pesquisa-field">
|
| 383 |
-
|
| 384 |
<select
|
| 385 |
data-field="tipoModelo"
|
| 386 |
name={toInputName('tipoModelo')}
|
|
@@ -395,25 +667,30 @@ export default function PesquisaTab() {
|
|
| 395 |
</select>
|
| 396 |
</label>
|
| 397 |
<label className="pesquisa-field">
|
| 398 |
-
Finalidade
|
| 399 |
-
<
|
| 400 |
-
list="pesquisa-finalidades"
|
| 401 |
field="avalFinalidade"
|
| 402 |
value={filters.avalFinalidade}
|
| 403 |
onChange={onFieldChange}
|
| 404 |
-
placeholder="
|
|
|
|
|
|
|
| 405 |
/>
|
| 406 |
</label>
|
| 407 |
|
| 408 |
<label className="pesquisa-field">
|
| 409 |
-
|
| 410 |
-
<
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
value={filters.
|
| 414 |
onChange={onFieldChange}
|
| 415 |
-
|
| 416 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
</label>
|
| 418 |
</div>
|
| 419 |
|
|
@@ -431,28 +708,30 @@ export default function PesquisaTab() {
|
|
| 431 |
</div>
|
| 432 |
|
| 433 |
<div className="pesquisa-avaliando-stack pesquisa-avaliando-bottom-stack">
|
| 434 |
-
<
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
</label>
|
| 442 |
</div>
|
| 443 |
</div>
|
| 444 |
|
| 445 |
-
<datalist id="pesquisa-finalidades">
|
| 446 |
-
{(sugestoes.finalidades || []).map((item) => (
|
| 447 |
-
<option key={`finalidade-${item}`} value={item} />
|
| 448 |
-
))}
|
| 449 |
-
</datalist>
|
| 450 |
-
<datalist id="pesquisa-bairros">
|
| 451 |
-
{(sugestoes.bairros || []).map((item) => (
|
| 452 |
-
<option key={`bairro-${item}`} value={item} />
|
| 453 |
-
))}
|
| 454 |
-
</datalist>
|
| 455 |
-
|
| 456 |
<div className="row pesquisa-actions pesquisa-actions-primary">
|
| 457 |
<button type="button" onClick={() => void buscarModelos()} disabled={loading}>
|
| 458 |
{loading ? 'Pesquisando...' : 'Pesquisar'}
|
|
|
|
| 7 |
const EMPTY_FILTERS = {
|
| 8 |
contemApp: '',
|
| 9 |
tipoModelo: '',
|
| 10 |
+
negociacaoModelo: '',
|
| 11 |
dataMin: '',
|
| 12 |
dataMax: '',
|
| 13 |
avalFinalidade: '',
|
|
|
|
| 76 |
.toUpperCase()
|
| 77 |
}
|
| 78 |
|
| 79 |
+
function normalizeSearchText(value) {
|
| 80 |
+
return String(value || '')
|
| 81 |
+
.normalize('NFD')
|
| 82 |
+
.replace(/[\u0300-\u036f]/g, '')
|
| 83 |
+
.toLowerCase()
|
| 84 |
+
.trim()
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
function splitMultiTerms(value) {
|
| 88 |
+
const text = String(value || '').trim()
|
| 89 |
+
const parts = text.includes('||')
|
| 90 |
+
? text.split(/\s*\|\|\s*/)
|
| 91 |
+
: text.split(/[;|]/)
|
| 92 |
+
const raw = parts
|
| 93 |
+
.map((item) => String(item || '').trim())
|
| 94 |
+
.filter(Boolean)
|
| 95 |
+
const seen = new Set()
|
| 96 |
+
const unique = []
|
| 97 |
+
raw.forEach((item) => {
|
| 98 |
+
const key = normalizeSearchText(item)
|
| 99 |
+
if (!key || seen.has(key)) return
|
| 100 |
+
seen.add(key)
|
| 101 |
+
unique.push(item)
|
| 102 |
+
})
|
| 103 |
+
return unique
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
function joinMultiTerms(values) {
|
| 107 |
+
return (values || []).map((item) => String(item || '').trim()).filter(Boolean).join(' || ')
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
function inferTipoPorNomeModelo(...nomes) {
|
| 111 |
const tokens = Object.keys(TIPO_SIGLAS).sort((a, b) => b.length - a.length)
|
| 112 |
for (const nome of nomes) {
|
|
|
|
| 138 |
otica: 'avaliando',
|
| 139 |
contem_app: filters.contemApp,
|
| 140 |
tipo_modelo: filters.tipoModelo,
|
| 141 |
+
negociacao_modelo: filters.negociacaoModelo,
|
| 142 |
aval_finalidade: filters.avalFinalidade,
|
| 143 |
aval_bairro: filters.avalBairro,
|
| 144 |
data_min: filters.dataMin,
|
|
|
|
| 196 |
)
|
| 197 |
}
|
| 198 |
|
| 199 |
+
function ChipAutocompleteInput({
|
| 200 |
+
field,
|
| 201 |
+
value,
|
| 202 |
+
onChange,
|
| 203 |
+
placeholder,
|
| 204 |
+
suggestions = [],
|
| 205 |
+
panelTitle = 'Sugestoes',
|
| 206 |
+
}) {
|
| 207 |
+
const rootRef = useRef(null)
|
| 208 |
+
const [query, setQuery] = useState('')
|
| 209 |
+
const [open, setOpen] = useState(false)
|
| 210 |
+
const [activeIndex, setActiveIndex] = useState(-1)
|
| 211 |
+
const selectedValues = useMemo(() => splitMultiTerms(value), [value])
|
| 212 |
+
const selectedKeys = useMemo(() => new Set(selectedValues.map((item) => normalizeSearchText(item))), [selectedValues])
|
| 213 |
+
const queryNormalized = normalizeSearchText(query)
|
| 214 |
+
|
| 215 |
+
useEffect(() => {
|
| 216 |
+
if (!value) setQuery('')
|
| 217 |
+
}, [value])
|
| 218 |
+
|
| 219 |
+
const filteredSuggestions = useMemo(() => {
|
| 220 |
+
const unique = []
|
| 221 |
+
const seen = new Set()
|
| 222 |
+
|
| 223 |
+
;(suggestions || []).forEach((item) => {
|
| 224 |
+
const text = String(item || '').trim()
|
| 225 |
+
if (!text) return
|
| 226 |
+
const key = normalizeSearchText(text)
|
| 227 |
+
if (!key || seen.has(key) || selectedKeys.has(key)) return
|
| 228 |
+
seen.add(key)
|
| 229 |
+
unique.push(text)
|
| 230 |
+
})
|
| 231 |
+
|
| 232 |
+
if (!queryNormalized) return unique.slice(0, 120)
|
| 233 |
+
return unique
|
| 234 |
+
.filter((item) => normalizeSearchText(item).includes(queryNormalized))
|
| 235 |
+
.slice(0, 120)
|
| 236 |
+
}, [suggestions, queryNormalized, selectedKeys])
|
| 237 |
+
|
| 238 |
+
useEffect(() => {
|
| 239 |
+
if (!open) return undefined
|
| 240 |
+
function onDocumentMouseDown(event) {
|
| 241 |
+
if (!rootRef.current) return
|
| 242 |
+
if (!rootRef.current.contains(event.target)) {
|
| 243 |
+
setOpen(false)
|
| 244 |
+
}
|
| 245 |
+
}
|
| 246 |
+
document.addEventListener('mousedown', onDocumentMouseDown)
|
| 247 |
+
return () => document.removeEventListener('mousedown', onDocumentMouseDown)
|
| 248 |
+
}, [open])
|
| 249 |
+
|
| 250 |
+
useEffect(() => {
|
| 251 |
+
if (!open || !filteredSuggestions.length) {
|
| 252 |
+
setActiveIndex(-1)
|
| 253 |
+
return
|
| 254 |
+
}
|
| 255 |
+
setActiveIndex(-1)
|
| 256 |
+
}, [filteredSuggestions, open])
|
| 257 |
+
|
| 258 |
+
function emitValue(nextValue) {
|
| 259 |
+
onChange({
|
| 260 |
+
target: {
|
| 261 |
+
value: nextValue,
|
| 262 |
+
dataset: { field },
|
| 263 |
+
name: toInputName(field),
|
| 264 |
+
},
|
| 265 |
+
})
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
function setSelectedValues(nextSelected) {
|
| 269 |
+
emitValue(joinMultiTerms(nextSelected))
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
function addValue(nextValue) {
|
| 273 |
+
const text = String(nextValue || '').trim()
|
| 274 |
+
if (!text) return
|
| 275 |
+
const key = normalizeSearchText(text)
|
| 276 |
+
if (!key || selectedKeys.has(key)) {
|
| 277 |
+
setQuery('')
|
| 278 |
+
return
|
| 279 |
+
}
|
| 280 |
+
setSelectedValues([...selectedValues, text])
|
| 281 |
+
setQuery('')
|
| 282 |
+
setOpen(true)
|
| 283 |
+
setActiveIndex(-1)
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
function removeValue(nextValue) {
|
| 287 |
+
const target = String(nextValue || '').trim()
|
| 288 |
+
if (!target) return
|
| 289 |
+
|
| 290 |
+
const exactIndex = selectedValues.findIndex((item) => String(item || '').trim() === target)
|
| 291 |
+
if (exactIndex >= 0) {
|
| 292 |
+
const nextSelected = [...selectedValues]
|
| 293 |
+
nextSelected.splice(exactIndex, 1)
|
| 294 |
+
setSelectedValues(nextSelected)
|
| 295 |
+
return
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
const key = normalizeSearchText(target)
|
| 299 |
+
const normalizedIndex = selectedValues.findIndex((item) => normalizeSearchText(item) === key)
|
| 300 |
+
if (normalizedIndex < 0) return
|
| 301 |
+
const nextSelected = [...selectedValues]
|
| 302 |
+
nextSelected.splice(normalizedIndex, 1)
|
| 303 |
+
setSelectedValues(nextSelected)
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
function onInputChange(event) {
|
| 307 |
+
setQuery(event.target.value)
|
| 308 |
+
setOpen(true)
|
| 309 |
+
setActiveIndex(-1)
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
function onInputKeyDown(event) {
|
| 313 |
+
if (event.key === 'Escape') {
|
| 314 |
+
setOpen(false)
|
| 315 |
+
return
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
if (!filteredSuggestions.length) return
|
| 319 |
+
|
| 320 |
+
if (event.key === 'ArrowDown') {
|
| 321 |
+
event.preventDefault()
|
| 322 |
+
setOpen(true)
|
| 323 |
+
setActiveIndex((prev) => (prev < 0 ? 0 : (prev + 1) % filteredSuggestions.length))
|
| 324 |
+
return
|
| 325 |
+
}
|
| 326 |
+
|
| 327 |
+
if (event.key === 'ArrowUp') {
|
| 328 |
+
event.preventDefault()
|
| 329 |
+
setOpen(true)
|
| 330 |
+
setActiveIndex((prev) => {
|
| 331 |
+
if (prev < 0) return filteredSuggestions.length - 1
|
| 332 |
+
return (prev - 1 + filteredSuggestions.length) % filteredSuggestions.length
|
| 333 |
+
})
|
| 334 |
+
return
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
if (event.key === 'Enter' && open && activeIndex >= 0) {
|
| 338 |
+
event.preventDefault()
|
| 339 |
+
addValue(filteredSuggestions[activeIndex])
|
| 340 |
+
return
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
if (event.key === 'Enter') {
|
| 344 |
+
const next = String(query || '').trim()
|
| 345 |
+
if (next) {
|
| 346 |
+
event.preventDefault()
|
| 347 |
+
addValue(next)
|
| 348 |
+
}
|
| 349 |
+
return
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
if (event.key === ',' || event.key === ';' || event.key === '|') {
|
| 353 |
+
const next = String(query || '').trim()
|
| 354 |
+
if (next) {
|
| 355 |
+
event.preventDefault()
|
| 356 |
+
addValue(next)
|
| 357 |
+
}
|
| 358 |
+
return
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
if (event.key === 'Backspace' && !query && selectedValues.length) {
|
| 362 |
+
const nextSelected = selectedValues.slice(0, -1)
|
| 363 |
+
setSelectedValues(nextSelected)
|
| 364 |
+
}
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
return (
|
| 368 |
+
<div className={`chip-autocomplete${open ? ' is-open' : ''}`} ref={rootRef}>
|
| 369 |
+
{selectedValues.length ? (
|
| 370 |
+
<div className="chip-autocomplete-selected-wrap">
|
| 371 |
+
{selectedValues.map((item) => (
|
| 372 |
+
<span key={`${field}-selected-${item}`} className="chip-autocomplete-selected">
|
| 373 |
+
<span>{item}</span>
|
| 374 |
+
<button
|
| 375 |
+
type="button"
|
| 376 |
+
className="chip-autocomplete-selected-remove"
|
| 377 |
+
onMouseDown={(event) => {
|
| 378 |
+
event.preventDefault()
|
| 379 |
+
event.stopPropagation()
|
| 380 |
+
removeValue(item)
|
| 381 |
+
}}
|
| 382 |
+
onClick={(event) => {
|
| 383 |
+
event.preventDefault()
|
| 384 |
+
event.stopPropagation()
|
| 385 |
+
}}
|
| 386 |
+
aria-label={`Remover ${item}`}
|
| 387 |
+
>
|
| 388 |
+
×
|
| 389 |
+
</button>
|
| 390 |
+
</span>
|
| 391 |
+
))}
|
| 392 |
+
</div>
|
| 393 |
+
) : null}
|
| 394 |
+
|
| 395 |
+
<TextFieldInput
|
| 396 |
+
field={field}
|
| 397 |
+
value={query}
|
| 398 |
+
onChange={onInputChange}
|
| 399 |
+
onFocus={() => {
|
| 400 |
+
setOpen(true)
|
| 401 |
+
setActiveIndex(-1)
|
| 402 |
+
}}
|
| 403 |
+
onKeyDown={onInputKeyDown}
|
| 404 |
+
placeholder={placeholder}
|
| 405 |
+
/>
|
| 406 |
+
|
| 407 |
+
{open ? (
|
| 408 |
+
<div className="chip-autocomplete-panel" role="listbox">
|
| 409 |
+
<div className="chip-autocomplete-panel-head">{panelTitle}</div>
|
| 410 |
+
{filteredSuggestions.length ? (
|
| 411 |
+
<div className="chip-autocomplete-chip-wrap">
|
| 412 |
+
{filteredSuggestions.map((item, idx) => (
|
| 413 |
+
<button
|
| 414 |
+
type="button"
|
| 415 |
+
key={`${field}-chip-${item}-${idx}`}
|
| 416 |
+
className={`chip-autocomplete-chip${idx === activeIndex ? ' is-active' : ''}`}
|
| 417 |
+
onMouseDown={(event) => {
|
| 418 |
+
event.preventDefault()
|
| 419 |
+
event.stopPropagation()
|
| 420 |
+
addValue(item)
|
| 421 |
+
}}
|
| 422 |
+
>
|
| 423 |
+
{item}
|
| 424 |
+
</button>
|
| 425 |
+
))}
|
| 426 |
+
</div>
|
| 427 |
+
) : (
|
| 428 |
+
<div className="chip-autocomplete-empty">
|
| 429 |
+
Nenhuma sugestao encontrada.
|
| 430 |
+
</div>
|
| 431 |
+
)}
|
| 432 |
+
</div>
|
| 433 |
+
) : null}
|
| 434 |
+
</div>
|
| 435 |
+
)
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
export default function PesquisaTab() {
|
| 439 |
const [loading, setLoading] = useState(false)
|
| 440 |
const [error, setError] = useState('')
|
|
|
|
| 652 |
</select>
|
| 653 |
</label>
|
| 654 |
<label className="pesquisa-field">
|
| 655 |
+
Finalidade Generica
|
| 656 |
<select
|
| 657 |
data-field="tipoModelo"
|
| 658 |
name={toInputName('tipoModelo')}
|
|
|
|
| 667 |
</select>
|
| 668 |
</label>
|
| 669 |
<label className="pesquisa-field">
|
| 670 |
+
Finalidade Cadastral
|
| 671 |
+
<ChipAutocompleteInput
|
|
|
|
| 672 |
field="avalFinalidade"
|
| 673 |
value={filters.avalFinalidade}
|
| 674 |
onChange={onFieldChange}
|
| 675 |
+
placeholder="Digite e pressione Enter"
|
| 676 |
+
suggestions={sugestoes.finalidades || []}
|
| 677 |
+
panelTitle="Finalidades sugeridas"
|
| 678 |
/>
|
| 679 |
</label>
|
| 680 |
|
| 681 |
<label className="pesquisa-field">
|
| 682 |
+
Tipo do Modelo
|
| 683 |
+
<select
|
| 684 |
+
data-field="negociacaoModelo"
|
| 685 |
+
name={toInputName('negociacaoModelo')}
|
| 686 |
+
value={filters.negociacaoModelo}
|
| 687 |
onChange={onFieldChange}
|
| 688 |
+
autoComplete="off"
|
| 689 |
+
>
|
| 690 |
+
<option value="">Indiferente</option>
|
| 691 |
+
<option value="aluguel">Aluguel</option>
|
| 692 |
+
<option value="venda">Venda</option>
|
| 693 |
+
</select>
|
| 694 |
</label>
|
| 695 |
</div>
|
| 696 |
|
|
|
|
| 708 |
</div>
|
| 709 |
|
| 710 |
<div className="pesquisa-avaliando-stack pesquisa-avaliando-bottom-stack">
|
| 711 |
+
<div className="pesquisa-area-rh-grid">
|
| 712 |
+
<label className="pesquisa-field">
|
| 713 |
+
Area do imovel
|
| 714 |
+
<NumberFieldInput field="avalArea" value={filters.avalArea} onChange={onFieldChange} placeholder="0" />
|
| 715 |
+
</label>
|
| 716 |
+
<label className="pesquisa-field">
|
| 717 |
+
RH do imovel
|
| 718 |
+
<NumberFieldInput field="avalRh" value={filters.avalRh} onChange={onFieldChange} placeholder="0" />
|
| 719 |
+
</label>
|
| 720 |
+
</div>
|
| 721 |
+
<label className="pesquisa-field pesquisa-bairro-bottom-field">
|
| 722 |
+
Bairro do imovel
|
| 723 |
+
<ChipAutocompleteInput
|
| 724 |
+
field="avalBairro"
|
| 725 |
+
value={filters.avalBairro}
|
| 726 |
+
onChange={onFieldChange}
|
| 727 |
+
placeholder="Digite e pressione Enter"
|
| 728 |
+
suggestions={sugestoes.bairros || []}
|
| 729 |
+
panelTitle="Bairros sugeridos"
|
| 730 |
+
/>
|
| 731 |
</label>
|
| 732 |
</div>
|
| 733 |
</div>
|
| 734 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
<div className="row pesquisa-actions pesquisa-actions-primary">
|
| 736 |
<button type="button" onClick={() => void buscarModelos()} disabled={loading}>
|
| 737 |
{loading ? 'Pesquisando...' : 'Pesquisar'}
|
frontend/src/styles.css
CHANGED
|
@@ -911,6 +911,119 @@ button.pesquisa-otica-btn.active:hover {
|
|
| 911 |
width: 100%;
|
| 912 |
}
|
| 913 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 914 |
.pesquisa-filtros-groups .pesquisa-field input,
|
| 915 |
.pesquisa-filtros-groups .pesquisa-field select {
|
| 916 |
width: min(100%, 255px);
|
|
@@ -997,6 +1110,13 @@ button.pesquisa-otica-btn.active:hover {
|
|
| 997 |
align-items: start;
|
| 998 |
}
|
| 999 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1000 |
.pesquisa-avaliando-periodo-pair {
|
| 1001 |
margin: 0;
|
| 1002 |
}
|
|
@@ -1005,6 +1125,10 @@ button.pesquisa-otica-btn.active:hover {
|
|
| 1005 |
gap: 12px;
|
| 1006 |
}
|
| 1007 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1008 |
.pesquisa-avaliando-bottom-grid .pesquisa-field-pair {
|
| 1009 |
grid-column: auto;
|
| 1010 |
margin: 0;
|
|
@@ -2864,7 +2988,7 @@ button.btn-upload-select {
|
|
| 2864 |
|
| 2865 |
.outlier-inputs-grid {
|
| 2866 |
display: grid;
|
| 2867 |
-
grid-template-columns:
|
| 2868 |
gap: 12px;
|
| 2869 |
}
|
| 2870 |
|
|
@@ -2884,6 +3008,13 @@ button.btn-upload-select {
|
|
| 2884 |
width: 100%;
|
| 2885 |
}
|
| 2886 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2887 |
.btn-reiniciar-iteracao {
|
| 2888 |
--btn-bg-start: #ff8c00;
|
| 2889 |
--btn-bg-end: #e67e00;
|
|
@@ -2973,11 +3104,68 @@ button.btn-upload-select {
|
|
| 2973 |
}
|
| 2974 |
|
| 2975 |
.outlier-actions-row button.btn-filtro-recursivo {
|
| 2976 |
-
--btn-bg-start: #
|
| 2977 |
-
--btn-bg-end: #
|
| 2978 |
-
--btn-border: #
|
| 2979 |
-
--btn-shadow-soft: rgba(
|
| 2980 |
-
--btn-shadow-strong: rgba(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2981 |
}
|
| 2982 |
|
| 2983 |
.resumo-outliers-box {
|
|
@@ -3758,6 +3946,10 @@ button.btn-download-subtle {
|
|
| 3758 |
grid-template-columns: 1fr;
|
| 3759 |
}
|
| 3760 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3761 |
.pesquisa-dynamic-filter-row,
|
| 3762 |
.pesquisa-range-values-row,
|
| 3763 |
.pesquisa-range-row,
|
|
|
|
| 911 |
width: 100%;
|
| 912 |
}
|
| 913 |
|
| 914 |
+
.chip-autocomplete {
|
| 915 |
+
position: relative;
|
| 916 |
+
}
|
| 917 |
+
|
| 918 |
+
.chip-autocomplete-selected-wrap {
|
| 919 |
+
display: flex;
|
| 920 |
+
flex-wrap: wrap;
|
| 921 |
+
gap: 6px;
|
| 922 |
+
margin: 0 0 7px;
|
| 923 |
+
}
|
| 924 |
+
|
| 925 |
+
.chip-autocomplete-selected {
|
| 926 |
+
display: inline-flex;
|
| 927 |
+
align-items: center;
|
| 928 |
+
flex: 0 0 auto;
|
| 929 |
+
gap: 6px;
|
| 930 |
+
border: 1px solid #b7cee6;
|
| 931 |
+
border-radius: 999px;
|
| 932 |
+
background: #eaf3fc;
|
| 933 |
+
color: #2f4d69;
|
| 934 |
+
font-size: 0.77rem;
|
| 935 |
+
font-weight: 700;
|
| 936 |
+
line-height: 1.1;
|
| 937 |
+
padding: 4px 8px;
|
| 938 |
+
}
|
| 939 |
+
|
| 940 |
+
.chip-autocomplete-selected-remove {
|
| 941 |
+
display: inline-flex;
|
| 942 |
+
align-items: center;
|
| 943 |
+
justify-content: center;
|
| 944 |
+
flex: 0 0 16px;
|
| 945 |
+
width: 16px;
|
| 946 |
+
height: 16px;
|
| 947 |
+
border: none;
|
| 948 |
+
background: transparent;
|
| 949 |
+
color: #365776;
|
| 950 |
+
font-size: 0.9rem;
|
| 951 |
+
font-weight: 700;
|
| 952 |
+
line-height: 1;
|
| 953 |
+
padding: 0;
|
| 954 |
+
margin: 0;
|
| 955 |
+
border-radius: 999px;
|
| 956 |
+
box-shadow: none;
|
| 957 |
+
transform: none;
|
| 958 |
+
cursor: pointer;
|
| 959 |
+
}
|
| 960 |
+
|
| 961 |
+
.chip-autocomplete-selected-remove:hover {
|
| 962 |
+
color: #20374f;
|
| 963 |
+
background: rgba(54, 87, 118, 0.09);
|
| 964 |
+
box-shadow: none;
|
| 965 |
+
transform: none;
|
| 966 |
+
}
|
| 967 |
+
|
| 968 |
+
.chip-autocomplete-panel {
|
| 969 |
+
position: absolute;
|
| 970 |
+
top: calc(100% + 6px);
|
| 971 |
+
left: 0;
|
| 972 |
+
right: 0;
|
| 973 |
+
z-index: 35;
|
| 974 |
+
border: 1px solid #c8d7e6;
|
| 975 |
+
border-radius: 12px;
|
| 976 |
+
background: #ffffff;
|
| 977 |
+
box-shadow: 0 8px 24px rgba(46, 77, 107, 0.14);
|
| 978 |
+
padding: 8px;
|
| 979 |
+
max-height: 190px;
|
| 980 |
+
overflow: auto;
|
| 981 |
+
}
|
| 982 |
+
|
| 983 |
+
.chip-autocomplete-panel-head {
|
| 984 |
+
font-size: 0.72rem;
|
| 985 |
+
font-weight: 700;
|
| 986 |
+
color: #4f667d;
|
| 987 |
+
margin: 1px 1px 7px;
|
| 988 |
+
text-transform: uppercase;
|
| 989 |
+
letter-spacing: 0.03em;
|
| 990 |
+
}
|
| 991 |
+
|
| 992 |
+
.chip-autocomplete-chip-wrap {
|
| 993 |
+
display: flex;
|
| 994 |
+
flex-wrap: wrap;
|
| 995 |
+
gap: 6px;
|
| 996 |
+
}
|
| 997 |
+
|
| 998 |
+
.chip-autocomplete-chip {
|
| 999 |
+
border: 1px solid #c9d9ea;
|
| 1000 |
+
border-radius: 999px;
|
| 1001 |
+
background: #f5f9fd;
|
| 1002 |
+
color: #37516b;
|
| 1003 |
+
font-size: 0.78rem;
|
| 1004 |
+
font-weight: 600;
|
| 1005 |
+
line-height: 1.15;
|
| 1006 |
+
padding: 5px 9px;
|
| 1007 |
+
cursor: pointer;
|
| 1008 |
+
max-width: 100%;
|
| 1009 |
+
white-space: nowrap;
|
| 1010 |
+
overflow: hidden;
|
| 1011 |
+
text-overflow: ellipsis;
|
| 1012 |
+
}
|
| 1013 |
+
|
| 1014 |
+
.chip-autocomplete-chip:hover,
|
| 1015 |
+
.chip-autocomplete-chip.is-active {
|
| 1016 |
+
background: #dcecff;
|
| 1017 |
+
border-color: #aac7e8;
|
| 1018 |
+
color: #233b54;
|
| 1019 |
+
}
|
| 1020 |
+
|
| 1021 |
+
.chip-autocomplete-empty {
|
| 1022 |
+
font-size: 0.8rem;
|
| 1023 |
+
color: #657f99;
|
| 1024 |
+
padding: 2px 2px 5px;
|
| 1025 |
+
}
|
| 1026 |
+
|
| 1027 |
.pesquisa-filtros-groups .pesquisa-field input,
|
| 1028 |
.pesquisa-filtros-groups .pesquisa-field select {
|
| 1029 |
width: min(100%, 255px);
|
|
|
|
| 1110 |
align-items: start;
|
| 1111 |
}
|
| 1112 |
|
| 1113 |
+
.pesquisa-area-rh-grid {
|
| 1114 |
+
display: grid;
|
| 1115 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 1116 |
+
gap: 12px 14px;
|
| 1117 |
+
align-items: start;
|
| 1118 |
+
}
|
| 1119 |
+
|
| 1120 |
.pesquisa-avaliando-periodo-pair {
|
| 1121 |
margin: 0;
|
| 1122 |
}
|
|
|
|
| 1125 |
gap: 12px;
|
| 1126 |
}
|
| 1127 |
|
| 1128 |
+
.pesquisa-bairro-bottom-field {
|
| 1129 |
+
grid-column: 1 / -1;
|
| 1130 |
+
}
|
| 1131 |
+
|
| 1132 |
.pesquisa-avaliando-bottom-grid .pesquisa-field-pair {
|
| 1133 |
grid-column: auto;
|
| 1134 |
margin: 0;
|
|
|
|
| 2988 |
|
| 2989 |
.outlier-inputs-grid {
|
| 2990 |
display: grid;
|
| 2991 |
+
grid-template-columns: minmax(0, 1fr);
|
| 2992 |
gap: 12px;
|
| 2993 |
}
|
| 2994 |
|
|
|
|
| 3008 |
width: 100%;
|
| 3009 |
}
|
| 3010 |
|
| 3011 |
+
.outlier-input-card textarea {
|
| 3012 |
+
width: 100%;
|
| 3013 |
+
resize: vertical;
|
| 3014 |
+
min-height: 58px;
|
| 3015 |
+
max-height: 160px;
|
| 3016 |
+
}
|
| 3017 |
+
|
| 3018 |
.btn-reiniciar-iteracao {
|
| 3019 |
--btn-bg-start: #ff8c00;
|
| 3020 |
--btn-bg-end: #e67e00;
|
|
|
|
| 3104 |
}
|
| 3105 |
|
| 3106 |
.outlier-actions-row button.btn-filtro-recursivo {
|
| 3107 |
+
--btn-bg-start: #59b97f;
|
| 3108 |
+
--btn-bg-end: #3d9c63;
|
| 3109 |
+
--btn-border: #338655;
|
| 3110 |
+
--btn-shadow-soft: rgba(61, 156, 99, 0.2);
|
| 3111 |
+
--btn-shadow-strong: rgba(61, 156, 99, 0.28);
|
| 3112 |
+
}
|
| 3113 |
+
|
| 3114 |
+
.outlier-actions-row .btn-filtro-recursivo-wrap {
|
| 3115 |
+
position: relative;
|
| 3116 |
+
display: inline-flex;
|
| 3117 |
+
}
|
| 3118 |
+
|
| 3119 |
+
.outlier-actions-row .btn-filtro-recursivo-wrap::after {
|
| 3120 |
+
content: attr(data-tooltip);
|
| 3121 |
+
position: absolute;
|
| 3122 |
+
left: calc(100% + 10px);
|
| 3123 |
+
bottom: calc(100% + 2px);
|
| 3124 |
+
width: min(520px, 68vw);
|
| 3125 |
+
max-width: 520px;
|
| 3126 |
+
background: #f4fbf7;
|
| 3127 |
+
border: 1px solid #bfe4cc;
|
| 3128 |
+
border-radius: 10px;
|
| 3129 |
+
color: #2f5f43;
|
| 3130 |
+
font-size: 0.78rem;
|
| 3131 |
+
line-height: 1.35;
|
| 3132 |
+
font-weight: 600;
|
| 3133 |
+
padding: 10px 12px;
|
| 3134 |
+
box-shadow: 0 8px 20px rgba(47, 95, 67, 0.18);
|
| 3135 |
+
opacity: 0;
|
| 3136 |
+
transform: translateY(4px);
|
| 3137 |
+
transition: opacity 0.14s ease, transform 0.14s ease;
|
| 3138 |
+
pointer-events: none;
|
| 3139 |
+
z-index: 30;
|
| 3140 |
+
}
|
| 3141 |
+
|
| 3142 |
+
.outlier-actions-row .btn-filtro-recursivo-wrap::before {
|
| 3143 |
+
content: '';
|
| 3144 |
+
position: absolute;
|
| 3145 |
+
left: calc(100% + 4px);
|
| 3146 |
+
bottom: calc(100% + 12px);
|
| 3147 |
+
width: 10px;
|
| 3148 |
+
height: 10px;
|
| 3149 |
+
background: #f4fbf7;
|
| 3150 |
+
border-left: 1px solid #bfe4cc;
|
| 3151 |
+
border-top: 1px solid #bfe4cc;
|
| 3152 |
+
transform: rotate(225deg);
|
| 3153 |
+
opacity: 0;
|
| 3154 |
+
transition: opacity 0.14s ease, transform 0.14s ease;
|
| 3155 |
+
pointer-events: none;
|
| 3156 |
+
z-index: 29;
|
| 3157 |
+
}
|
| 3158 |
+
|
| 3159 |
+
.outlier-actions-row .btn-filtro-recursivo-wrap:hover::after,
|
| 3160 |
+
.outlier-actions-row .btn-filtro-recursivo-wrap:focus-within::after {
|
| 3161 |
+
opacity: 1;
|
| 3162 |
+
transform: translateY(0);
|
| 3163 |
+
}
|
| 3164 |
+
|
| 3165 |
+
.outlier-actions-row .btn-filtro-recursivo-wrap:hover::before,
|
| 3166 |
+
.outlier-actions-row .btn-filtro-recursivo-wrap:focus-within::before {
|
| 3167 |
+
opacity: 1;
|
| 3168 |
+
transform: rotate(225deg);
|
| 3169 |
}
|
| 3170 |
|
| 3171 |
.resumo-outliers-box {
|
|
|
|
| 3946 |
grid-template-columns: 1fr;
|
| 3947 |
}
|
| 3948 |
|
| 3949 |
+
.pesquisa-area-rh-grid {
|
| 3950 |
+
grid-template-columns: 1fr;
|
| 3951 |
+
}
|
| 3952 |
+
|
| 3953 |
.pesquisa-dynamic-filter-row,
|
| 3954 |
.pesquisa-range-values-row,
|
| 3955 |
.pesquisa-range-row,
|