Spaces:
Running
Running
Guilherme Silberfarb Costa commited on
Commit ·
2e13456
1
Parent(s): c2c2480
diversas atualizacoes na aba pesquisas e na estrutura do app
Browse files- backend/app/api/elaboracao.py +22 -0
- backend/app/api/pesquisa.py +3 -1
- backend/app/core/elaboracao/carregamento.py +15 -1
- backend/app/core/elaboracao/core.py +71 -4
- backend/app/core/pesquisa/modelos_dai/MOD_A_SALA_Z1_006C.dai +2 -2
- backend/app/core/pesquisa/modelos_dai/MOD_V_AP_Z1_011D.dai +2 -2
- backend/app/core/pesquisa/modelos_dai/MOD_V_AP_Z1_020B.dai +2 -2
- backend/app/core/pesquisa/modelos_dai/MOD_V_AP_Z1_022.dai +2 -2
- backend/app/core/pesquisa/modelos_dai/MOD_V_EDIF_Z1_Z2_Z3_Z4_002E.dai +2 -2
- backend/app/core/pesquisa/modelos_dai/MOD_V_TER_Z2_008C.dai +2 -2
- backend/app/core/pesquisa/modelos_dai/MOD_V_TER_Z4_003J.dai +2 -2
- backend/app/core/visualizacao/app.py +29 -3
- backend/app/models/session.py +3 -0
- backend/app/services/elaboracao_service.py +187 -0
- backend/app/services/pesquisa_service.py +145 -16
- backend/app/services/visualizacao_service.py +9 -0
- frontend/src/App.jsx +6 -0
- frontend/src/api.js +2 -0
- frontend/src/components/ElaboracaoTab.jsx +176 -14
- frontend/src/components/InicioTab.jsx +19 -0
- frontend/src/components/PesquisaAdminConfigPanel.jsx +77 -64
- frontend/src/components/PesquisaTab.jsx +93 -463
- frontend/src/styles.css +82 -5
backend/app/api/elaboracao.py
CHANGED
|
@@ -124,6 +124,10 @@ class UpdateMapaPayload(SessionPayload):
|
|
| 124 |
variavel_mapa: str | None = None
|
| 125 |
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
@router.post("/upload")
|
| 128 |
async def upload_file(
|
| 129 |
session_id: str = Form(...),
|
|
@@ -333,6 +337,18 @@ def map_update(payload: UpdateMapaPayload) -> dict[str, Any]:
|
|
| 333 |
return elaboracao_service.atualizar_mapa(session, payload.variavel_mapa)
|
| 334 |
|
| 335 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
@router.get("/context")
|
| 337 |
def context(session_id: str) -> dict[str, Any]:
|
| 338 |
session = session_store.get(session_id)
|
|
@@ -344,4 +360,10 @@ def context(session_id: str) -> dict[str, Any]:
|
|
| 344 |
"percentuais": session.percentuais,
|
| 345 |
"outliers_anteriores": session.outliers_anteriores,
|
| 346 |
"iteracao": session.iteracao,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
}
|
|
|
|
| 124 |
variavel_mapa: str | None = None
|
| 125 |
|
| 126 |
|
| 127 |
+
class ColunaDataMercadoPayload(SessionPayload):
|
| 128 |
+
coluna_data: str
|
| 129 |
+
|
| 130 |
+
|
| 131 |
@router.post("/upload")
|
| 132 |
async def upload_file(
|
| 133 |
session_id: str = Form(...),
|
|
|
|
| 337 |
return elaboracao_service.atualizar_mapa(session, payload.variavel_mapa)
|
| 338 |
|
| 339 |
|
| 340 |
+
@router.post("/market-date/preview")
|
| 341 |
+
def market_date_preview(payload: ColunaDataMercadoPayload) -> dict[str, Any]:
|
| 342 |
+
session = session_store.get(payload.session_id)
|
| 343 |
+
return elaboracao_service.previsualizar_coluna_data_mercado(session, payload.coluna_data)
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
@router.post("/market-date/apply")
|
| 347 |
+
def market_date_apply(payload: ColunaDataMercadoPayload) -> dict[str, Any]:
|
| 348 |
+
session = session_store.get(payload.session_id)
|
| 349 |
+
return elaboracao_service.aplicar_coluna_data_mercado(session, payload.coluna_data)
|
| 350 |
+
|
| 351 |
+
|
| 352 |
@router.get("/context")
|
| 353 |
def context(session_id: str) -> dict[str, Any]:
|
| 354 |
session = session_store.get(session_id)
|
|
|
|
| 360 |
"percentuais": session.percentuais,
|
| 361 |
"outliers_anteriores": session.outliers_anteriores,
|
| 362 |
"iteracao": session.iteracao,
|
| 363 |
+
"coluna_data_mercado": session.coluna_data_mercado,
|
| 364 |
+
"periodo_dados_mercado": {
|
| 365 |
+
"coluna_data": session.coluna_data_mercado,
|
| 366 |
+
"data_inicial": session.periodo_dados_mercado_inicio,
|
| 367 |
+
"data_final": session.periodo_dados_mercado_fim,
|
| 368 |
+
},
|
| 369 |
}
|
backend/app/api/pesquisa.py
CHANGED
|
@@ -42,10 +42,11 @@ def pesquisar_admin_config_salvar(payload: PesquisaAdminConfigPayload) -> dict:
|
|
| 42 |
@router.get("/modelos")
|
| 43 |
def pesquisar_modelos(
|
| 44 |
somente_contexto: bool = Query(False),
|
| 45 |
-
otica: str = Query("
|
| 46 |
nome: str | None = Query(None),
|
| 47 |
autor: str | None = Query(None),
|
| 48 |
contem_app: str | None = Query(None),
|
|
|
|
| 49 |
finalidade: str | None = Query(None),
|
| 50 |
finalidade_colunas: str | None = Query(None),
|
| 51 |
bairro: str | None = Query(None),
|
|
@@ -87,6 +88,7 @@ def pesquisar_modelos(
|
|
| 87 |
nome=nome,
|
| 88 |
autor=autor,
|
| 89 |
contem_app=contem_app,
|
|
|
|
| 90 |
finalidade=finalidade,
|
| 91 |
finalidade_colunas=_split_csv(finalidade_colunas),
|
| 92 |
bairro=bairro,
|
|
|
|
| 42 |
@router.get("/modelos")
|
| 43 |
def pesquisar_modelos(
|
| 44 |
somente_contexto: bool = Query(False),
|
| 45 |
+
otica: str = Query("avaliando"),
|
| 46 |
nome: str | None = Query(None),
|
| 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),
|
|
|
|
| 88 |
nome=nome,
|
| 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,
|
backend/app/core/elaboracao/carregamento.py
CHANGED
|
@@ -258,7 +258,21 @@ def carregar_dados_de_dai(caminho_arquivo):
|
|
| 258 |
Consome aplicar_selecao_callback por índice (r[0], r[1], ..., r[23:]).
|
| 259 |
Consome ajustar_modelo_callback por índice (m[0], ..., m[32]).
|
| 260 |
"""
|
| 261 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
|
| 263 |
nome_exibicao = os.path.basename(caminho_arquivo)
|
| 264 |
html_nome = f"<h2 style='margin:0 0 12px 0; font-size:1.4em;'>{nome_exibicao}</h2>"
|
|
|
|
| 258 |
Consome aplicar_selecao_callback por índice (r[0], r[1], ..., r[23:]).
|
| 259 |
Consome ajustar_modelo_callback por índice (m[0], ..., m[32]).
|
| 260 |
"""
|
| 261 |
+
(
|
| 262 |
+
df,
|
| 263 |
+
coluna_y,
|
| 264 |
+
colunas_x,
|
| 265 |
+
transformacao_y,
|
| 266 |
+
transformacoes_x,
|
| 267 |
+
dicotomicas,
|
| 268 |
+
codigo_alocado,
|
| 269 |
+
percentuais,
|
| 270 |
+
msg,
|
| 271 |
+
sucesso,
|
| 272 |
+
elaborador,
|
| 273 |
+
outliers_excluidos,
|
| 274 |
+
_periodo_dados_mercado,
|
| 275 |
+
) = carregar_dai(caminho_arquivo)
|
| 276 |
|
| 277 |
nome_exibicao = os.path.basename(caminho_arquivo)
|
| 278 |
html_nome = f"<h2 style='margin:0 0 12px 0; font-size:1.4em;'>{nome_exibicao}</h2>"
|
backend/app/core/elaboracao/core.py
CHANGED
|
@@ -142,6 +142,11 @@ def _migrar_pacote_v1_para_v2(pacote):
|
|
| 142 |
resumo = pacote.get("modelos_resumos", {})
|
| 143 |
return {
|
| 144 |
"versao": 2,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
"dados": {
|
| 146 |
"df": pacote["Xy_preview_out_coords"],
|
| 147 |
"estatisticas": pacote["estatisticas"],
|
|
@@ -194,12 +199,40 @@ def _migrar_pacote_v1_para_v2(pacote):
|
|
| 194 |
}
|
| 195 |
|
| 196 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
def carregar_dai(caminho):
|
| 198 |
"""
|
| 199 |
Carrega arquivo .dai e extrai DataFrame, variáveis e transformações.
|
| 200 |
|
| 201 |
Retorna:
|
| 202 |
-
tuple: (df, coluna_y, colunas_x, transformacao_y, transformacoes_x, dicotomicas, codigo_alocado, percentuais, mensagem, sucesso)
|
| 203 |
"""
|
| 204 |
try:
|
| 205 |
pacote = load(caminho)
|
|
@@ -211,6 +244,9 @@ def carregar_dai(caminho):
|
|
| 211 |
# Extrai novos campos (se existirem)
|
| 212 |
df_completo = pacote["dados"].get("df_completo", None)
|
| 213 |
outliers_excluidos = pacote["dados"].get("outliers_excluidos", [])
|
|
|
|
|
|
|
|
|
|
| 214 |
|
| 215 |
# Extrai DataFrame — preserva índices originais se df_completo disponível
|
| 216 |
if df_completo is not None:
|
|
@@ -270,10 +306,38 @@ def carregar_dai(caminho):
|
|
| 270 |
|
| 271 |
elaborador = pacote.get("elaborador", None)
|
| 272 |
msg = f"Modelo .dai carregado: {os.path.basename(caminho)} ({len(df)} dados, Y={coluna_y}, {len(colunas_x)} variáveis X)"
|
| 273 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
|
| 275 |
except Exception as e:
|
| 276 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
|
| 278 |
|
| 279 |
def identificar_coluna_y_padrao(df):
|
|
@@ -1717,7 +1781,8 @@ def exportar_base_csv(df):
|
|
| 1717 |
# ============================================================
|
| 1718 |
|
| 1719 |
def exportar_modelo_dai(resultado_modelo, df_original, df_completo=None, estatisticas=None,
|
| 1720 |
-
nome_arquivo="", elaborador=None, outliers_excluidos=None
|
|
|
|
| 1721 |
"""
|
| 1722 |
Exporta o modelo em formato .dai v2 (estrutura nested).
|
| 1723 |
|
|
@@ -1729,6 +1794,7 @@ def exportar_modelo_dai(resultado_modelo, df_original, df_completo=None, estatis
|
|
| 1729 |
nome_arquivo: nome do arquivo de saída
|
| 1730 |
elaborador: dict com dados do avaliador (ou None)
|
| 1731 |
outliers_excluidos: lista de índices excluídos como outliers (ou None)
|
|
|
|
| 1732 |
|
| 1733 |
Retorna:
|
| 1734 |
tuple: (caminho_arquivo, mensagem)
|
|
@@ -1848,6 +1914,7 @@ def exportar_modelo_dai(resultado_modelo, df_original, df_completo=None, estatis
|
|
| 1848 |
pacote = {
|
| 1849 |
"versao": 2,
|
| 1850 |
"elaborador": elaborador,
|
|
|
|
| 1851 |
"dados": {
|
| 1852 |
"df": df_modelo,
|
| 1853 |
"df_completo": df_completo_export,
|
|
|
|
| 142 |
resumo = pacote.get("modelos_resumos", {})
|
| 143 |
return {
|
| 144 |
"versao": 2,
|
| 145 |
+
"periodo_dados_mercado": {
|
| 146 |
+
"coluna_data": None,
|
| 147 |
+
"data_inicial": None,
|
| 148 |
+
"data_final": None,
|
| 149 |
+
},
|
| 150 |
"dados": {
|
| 151 |
"df": pacote["Xy_preview_out_coords"],
|
| 152 |
"estatisticas": pacote["estatisticas"],
|
|
|
|
| 199 |
}
|
| 200 |
|
| 201 |
|
| 202 |
+
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(valor, errors="coerce", dayfirst=True)
|
| 208 |
+
except Exception:
|
| 209 |
+
return None
|
| 210 |
+
if pd.isna(dt):
|
| 211 |
+
return None
|
| 212 |
+
return dt.date().isoformat()
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def _normalizar_periodo_dados_mercado(periodo):
|
| 216 |
+
"""Garante payload padronizado para período de dados de mercado."""
|
| 217 |
+
if not isinstance(periodo, dict):
|
| 218 |
+
periodo = {}
|
| 219 |
+
coluna_data = periodo.get("coluna_data", None)
|
| 220 |
+
coluna_data = str(coluna_data).strip() if coluna_data is not None else None
|
| 221 |
+
if coluna_data == "":
|
| 222 |
+
coluna_data = None
|
| 223 |
+
return {
|
| 224 |
+
"coluna_data": coluna_data,
|
| 225 |
+
"data_inicial": _normalizar_data_iso(periodo.get("data_inicial")),
|
| 226 |
+
"data_final": _normalizar_data_iso(periodo.get("data_final")),
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
|
| 230 |
def carregar_dai(caminho):
|
| 231 |
"""
|
| 232 |
Carrega arquivo .dai e extrai DataFrame, variáveis e transformações.
|
| 233 |
|
| 234 |
Retorna:
|
| 235 |
+
tuple: (df, coluna_y, colunas_x, transformacao_y, transformacoes_x, dicotomicas, codigo_alocado, percentuais, mensagem, sucesso, elaborador, outliers_excluidos, periodo_dados_mercado)
|
| 236 |
"""
|
| 237 |
try:
|
| 238 |
pacote = load(caminho)
|
|
|
|
| 244 |
# Extrai novos campos (se existirem)
|
| 245 |
df_completo = pacote["dados"].get("df_completo", None)
|
| 246 |
outliers_excluidos = pacote["dados"].get("outliers_excluidos", [])
|
| 247 |
+
periodo_dados_mercado = _normalizar_periodo_dados_mercado(
|
| 248 |
+
pacote.get("periodo_dados_mercado", None)
|
| 249 |
+
)
|
| 250 |
|
| 251 |
# Extrai DataFrame — preserva índices originais se df_completo disponível
|
| 252 |
if df_completo is not None:
|
|
|
|
| 306 |
|
| 307 |
elaborador = pacote.get("elaborador", None)
|
| 308 |
msg = f"Modelo .dai carregado: {os.path.basename(caminho)} ({len(df)} dados, Y={coluna_y}, {len(colunas_x)} variáveis X)"
|
| 309 |
+
return (
|
| 310 |
+
df,
|
| 311 |
+
coluna_y,
|
| 312 |
+
colunas_x,
|
| 313 |
+
transformacao_y,
|
| 314 |
+
transformacoes_x,
|
| 315 |
+
dicotomicas,
|
| 316 |
+
codigo_alocado_salvo or [],
|
| 317 |
+
percentuais_salvo or [],
|
| 318 |
+
msg,
|
| 319 |
+
True,
|
| 320 |
+
elaborador,
|
| 321 |
+
outliers_excluidos,
|
| 322 |
+
periodo_dados_mercado,
|
| 323 |
+
)
|
| 324 |
|
| 325 |
except Exception as e:
|
| 326 |
+
return (
|
| 327 |
+
None,
|
| 328 |
+
None,
|
| 329 |
+
None,
|
| 330 |
+
None,
|
| 331 |
+
None,
|
| 332 |
+
[],
|
| 333 |
+
[],
|
| 334 |
+
[],
|
| 335 |
+
f"Erro ao carregar .dai: {str(e)}",
|
| 336 |
+
False,
|
| 337 |
+
None,
|
| 338 |
+
[],
|
| 339 |
+
{"data_inicial": None, "data_final": None},
|
| 340 |
+
)
|
| 341 |
|
| 342 |
|
| 343 |
def identificar_coluna_y_padrao(df):
|
|
|
|
| 1781 |
# ============================================================
|
| 1782 |
|
| 1783 |
def exportar_modelo_dai(resultado_modelo, df_original, df_completo=None, estatisticas=None,
|
| 1784 |
+
nome_arquivo="", elaborador=None, outliers_excluidos=None,
|
| 1785 |
+
periodo_dados_mercado=None):
|
| 1786 |
"""
|
| 1787 |
Exporta o modelo em formato .dai v2 (estrutura nested).
|
| 1788 |
|
|
|
|
| 1794 |
nome_arquivo: nome do arquivo de saída
|
| 1795 |
elaborador: dict com dados do avaliador (ou None)
|
| 1796 |
outliers_excluidos: lista de índices excluídos como outliers (ou None)
|
| 1797 |
+
periodo_dados_mercado: dict com data_inicial e data_final dos dados de mercado
|
| 1798 |
|
| 1799 |
Retorna:
|
| 1800 |
tuple: (caminho_arquivo, mensagem)
|
|
|
|
| 1914 |
pacote = {
|
| 1915 |
"versao": 2,
|
| 1916 |
"elaborador": elaborador,
|
| 1917 |
+
"periodo_dados_mercado": _normalizar_periodo_dados_mercado(periodo_dados_mercado),
|
| 1918 |
"dados": {
|
| 1919 |
"df": df_modelo,
|
| 1920 |
"df_completo": df_completo_export,
|
backend/app/core/pesquisa/modelos_dai/MOD_A_SALA_Z1_006C.dai
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0398acda7bfaa01c782d0b551aa679b21afa85bc3882e5249b5cddceb7b733fd
|
| 3 |
+
size 305003
|
backend/app/core/pesquisa/modelos_dai/MOD_V_AP_Z1_011D.dai
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:61ff06842995a9e3d559e33c51aaaa584f33477df414ca16abc3861cfc14a6c9
|
| 3 |
+
size 5307303
|
backend/app/core/pesquisa/modelos_dai/MOD_V_AP_Z1_020B.dai
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:319994b70fe36527d8a198c87383b0de819d12a83f5098f4ef89bdfb013fb86f
|
| 3 |
+
size 1999486
|
backend/app/core/pesquisa/modelos_dai/MOD_V_AP_Z1_022.dai
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e263780042eb5ec056a49ee8862ed9523bebeac831e5ca6a0f7671ec66097413
|
| 3 |
+
size 1318536
|
backend/app/core/pesquisa/modelos_dai/MOD_V_EDIF_Z1_Z2_Z3_Z4_002E.dai
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:96391e419571bfb37a208a56a969aef8a175e26b11e86f0af99deba40dca18bf
|
| 3 |
+
size 1584486
|
backend/app/core/pesquisa/modelos_dai/MOD_V_TER_Z2_008C.dai
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:5f802758c2a569aba28986de3c0dbf1e4c436556a0ba7ee909d6576de6e3f719
|
| 3 |
+
size 824046
|
backend/app/core/pesquisa/modelos_dai/MOD_V_TER_Z4_003J.dai
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9e4fc5f42b69b3f912853ca1bc14ade1246187ce80288b461ea1064c6d743575
|
| 3 |
+
size 1559857
|
backend/app/core/visualizacao/app.py
CHANGED
|
@@ -1234,6 +1234,18 @@ def _formatar_badge_completo(pacote):
|
|
| 1234 |
if not pacote:
|
| 1235 |
return ""
|
| 1236 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1237 |
# --- Lado direito: lista de variáveis ---
|
| 1238 |
variaveis_html = ""
|
| 1239 |
info = pacote.get("transformacoes", {}).get("info")
|
|
@@ -1266,11 +1278,25 @@ def _formatar_badge_completo(pacote):
|
|
| 1266 |
x_badges.append(badge)
|
| 1267 |
variaveis_html = (
|
| 1268 |
"<div style='font-size:0.9em;line-height:2;'>"
|
| 1269 |
-
"<div><span style='font-weight:600;color:#495057;margin-right:8px;font-size:1.1em;'>Variável Dependente:</span>"
|
| 1270 |
+ y_badge + "</div>"
|
| 1271 |
-
"<div style='margin-top:4px;'>"
|
| 1272 |
-
"<span style='font-weight:600;color:#495057;margin-right:8px;font-size:1.1em;'>Variáveis Independentes:</span>"
|
| 1273 |
+ "".join(x_badges) + "</div>"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1274 |
"</div>"
|
| 1275 |
)
|
| 1276 |
|
|
|
|
| 1234 |
if not pacote:
|
| 1235 |
return ""
|
| 1236 |
|
| 1237 |
+
def _data_br(value):
|
| 1238 |
+
texto = str(value or "").strip()
|
| 1239 |
+
match = re.match(r"^(\d{4})-(\d{2})-(\d{2})$", texto)
|
| 1240 |
+
if match:
|
| 1241 |
+
return f"{match.group(3)}/{match.group(2)}/{match.group(1)}"
|
| 1242 |
+
return texto
|
| 1243 |
+
|
| 1244 |
+
periodo = pacote.get("periodo_dados_mercado") or {}
|
| 1245 |
+
data_inicial = _data_br(periodo.get("data_inicial"))
|
| 1246 |
+
data_final = _data_br(periodo.get("data_final"))
|
| 1247 |
+
periodo_txt = f"{data_inicial} a {data_final}" if data_inicial and data_final else ""
|
| 1248 |
+
|
| 1249 |
# --- Lado direito: lista de variáveis ---
|
| 1250 |
variaveis_html = ""
|
| 1251 |
info = pacote.get("transformacoes", {}).get("info")
|
|
|
|
| 1278 |
x_badges.append(badge)
|
| 1279 |
variaveis_html = (
|
| 1280 |
"<div style='font-size:0.9em;line-height:2;'>"
|
| 1281 |
+
"<div><span style='font-weight:600;color:#495057;margin-right:8px;font-size:1.1em;display:inline-block;min-width:190px;'>Variável Dependente:</span>"
|
| 1282 |
+ y_badge + "</div>"
|
| 1283 |
+
+ "<div style='margin-top:4px;'>"
|
| 1284 |
+
"<span style='font-weight:600;color:#495057;margin-right:8px;font-size:1.1em;display:inline-block;min-width:190px;'>Variáveis Independentes:</span>"
|
| 1285 |
+ "".join(x_badges) + "</div>"
|
| 1286 |
+
+ (
|
| 1287 |
+
"<div style='margin-top:6px;'>"
|
| 1288 |
+
"<span style='font-weight:600;color:#495057;margin-right:8px;font-size:1.1em;display:inline-block;min-width:190px;'>Período dos dados de mercado:</span>"
|
| 1289 |
+
f"<span style='color:#2f4458;'>{periodo_txt}</span>"
|
| 1290 |
+
"</div>"
|
| 1291 |
+
if periodo_txt else ""
|
| 1292 |
+
)
|
| 1293 |
+
+ "</div>"
|
| 1294 |
+
)
|
| 1295 |
+
elif periodo_txt:
|
| 1296 |
+
variaveis_html = (
|
| 1297 |
+
"<div style='font-size:0.95em;line-height:1.7;'>"
|
| 1298 |
+
"<span style='font-weight:600;color:#495057;margin-right:8px;display:inline-block;min-width:190px;'>Período dos dados de mercado:</span>"
|
| 1299 |
+
f"<span style='color:#2f4458;'>{periodo_txt}</span>"
|
| 1300 |
"</div>"
|
| 1301 |
)
|
| 1302 |
|
backend/app/models/session.py
CHANGED
|
@@ -44,6 +44,9 @@ class SessionState:
|
|
| 44 |
geo_col_cdlog: str | None = None
|
| 45 |
geo_col_num: str | None = None
|
| 46 |
mapa_habilitado: bool = False
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
pacote_visualizacao: dict[str, Any] | None = None
|
| 49 |
dados_visualizacao: pd.DataFrame | None = None
|
|
|
|
| 44 |
geo_col_cdlog: str | None = None
|
| 45 |
geo_col_num: str | None = None
|
| 46 |
mapa_habilitado: bool = False
|
| 47 |
+
coluna_data_mercado: str | None = None
|
| 48 |
+
periodo_dados_mercado_inicio: str | None = None
|
| 49 |
+
periodo_dados_mercado_fim: str | None = None
|
| 50 |
|
| 51 |
pacote_visualizacao: dict[str, Any] | None = None
|
| 52 |
dados_visualizacao: pd.DataFrame | None = None
|
backend/app/services/elaboracao_service.py
CHANGED
|
@@ -97,6 +97,127 @@ def _selection_context(session: SessionState) -> dict[str, Any]:
|
|
| 97 |
"codigo_alocado": list(session.codigo_alocado),
|
| 98 |
"percentuais": list(session.percentuais),
|
| 99 |
"outliers_anteriores": list(session.outliers_anteriores),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
}
|
| 101 |
|
| 102 |
|
|
@@ -271,6 +392,9 @@ def _set_dataframe_base(
|
|
| 271 |
session.percentuais = []
|
| 272 |
session.outliers_anteriores = []
|
| 273 |
session.iteracao = 1
|
|
|
|
|
|
|
|
|
|
| 274 |
|
| 275 |
colunas_numericas = [str(c) for c in obter_colunas_numericas(df)]
|
| 276 |
coluna_y_padrao = identificar_coluna_y_padrao(df)
|
|
@@ -282,6 +406,7 @@ def _set_dataframe_base(
|
|
| 282 |
"colunas_numericas": colunas_numericas,
|
| 283 |
"coluna_y_padrao": coluna_y_padrao,
|
| 284 |
"coords": _build_coords_payload(df, tem_coords),
|
|
|
|
| 285 |
}
|
| 286 |
|
| 287 |
|
|
@@ -342,6 +467,7 @@ def load_dai_for_elaboracao(session: SessionState, caminho_arquivo: str) -> dict
|
|
| 342 |
sucesso,
|
| 343 |
elaborador,
|
| 344 |
outliers_excluidos,
|
|
|
|
| 345 |
) = carregar_dai(caminho_arquivo)
|
| 346 |
|
| 347 |
if not sucesso or df is None:
|
|
@@ -349,8 +475,12 @@ def load_dai_for_elaboracao(session: SessionState, caminho_arquivo: str) -> dict
|
|
| 349 |
|
| 350 |
session.elaborador = elaborador
|
| 351 |
session.outliers_anteriores = _clean_int_list(outliers_excluidos)
|
|
|
|
| 352 |
|
| 353 |
base = _set_dataframe_base(session, df, clear_models=True)
|
|
|
|
|
|
|
|
|
|
| 354 |
session.transformacao_y = str(transformacao_y or "(x)")
|
| 355 |
session.transformacoes_x = {str(k): str(v) for k, v in (transformacoes_x or {}).items()}
|
| 356 |
|
|
@@ -389,6 +519,7 @@ def load_dai_for_elaboracao(session: SessionState, caminho_arquivo: str) -> dict
|
|
| 389 |
"outliers_html": html_outliers,
|
| 390 |
"contexto": _selection_context(session),
|
| 391 |
"elaborador": sanitize_value(elaborador),
|
|
|
|
| 392 |
}
|
| 393 |
|
| 394 |
|
|
@@ -1026,6 +1157,57 @@ def exportar_avaliacoes_elaboracao(session: SessionState) -> str:
|
|
| 1026 |
return caminho
|
| 1027 |
|
| 1028 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1029 |
def exportar_modelo(session: SessionState, nome_arquivo: str, elaborador: dict[str, Any] | None = None) -> tuple[str, str]:
|
| 1030 |
if session.resultado_modelo is None:
|
| 1031 |
raise HTTPException(status_code=400, detail="Ajuste um modelo antes de exportar")
|
|
@@ -1043,6 +1225,11 @@ def exportar_modelo(session: SessionState, nome_arquivo: str, elaborador: dict[s
|
|
| 1043 |
nome_arquivo.strip(),
|
| 1044 |
elaborador=elaborador,
|
| 1045 |
outliers_excluidos=session.outliers_anteriores,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1046 |
)
|
| 1047 |
|
| 1048 |
if not caminho:
|
|
|
|
| 97 |
"codigo_alocado": list(session.codigo_alocado),
|
| 98 |
"percentuais": list(session.percentuais),
|
| 99 |
"outliers_anteriores": list(session.outliers_anteriores),
|
| 100 |
+
"iteracao": session.iteracao,
|
| 101 |
+
"coluna_data_mercado": session.coluna_data_mercado,
|
| 102 |
+
"periodo_dados_mercado": {
|
| 103 |
+
"coluna_data": session.coluna_data_mercado,
|
| 104 |
+
"data_inicial": session.periodo_dados_mercado_inicio,
|
| 105 |
+
"data_final": session.periodo_dados_mercado_fim,
|
| 106 |
+
},
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def _normalizar_periodo_dados_mercado(periodo: dict[str, Any] | None) -> dict[str, str | None]:
|
| 111 |
+
if not isinstance(periodo, dict):
|
| 112 |
+
return {"coluna_data": None, "data_inicial": None, "data_final": None}
|
| 113 |
+
coluna_data = str(periodo.get("coluna_data") or "").strip() or None
|
| 114 |
+
inicio = pd.to_datetime(periodo.get("data_inicial"), errors="coerce", dayfirst=True)
|
| 115 |
+
fim = pd.to_datetime(periodo.get("data_final"), errors="coerce", dayfirst=True)
|
| 116 |
+
return {
|
| 117 |
+
"coluna_data": coluna_data,
|
| 118 |
+
"data_inicial": None if pd.isna(inicio) else inicio.date().isoformat(),
|
| 119 |
+
"data_final": None if pd.isna(fim) else fim.date().isoformat(),
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def _payload_data_mercado(session: SessionState) -> dict[str, Any]:
|
| 124 |
+
df = session.df_original
|
| 125 |
+
colunas = [str(c) for c in df.columns] if df is not None else []
|
| 126 |
+
coluna_sugerida = _sugerir_coluna_data_mercado(df) if df is not None else None
|
| 127 |
+
return {
|
| 128 |
+
"colunas_data_mercado": colunas,
|
| 129 |
+
"coluna_data_mercado": session.coluna_data_mercado,
|
| 130 |
+
"coluna_data_mercado_sugerida": coluna_sugerida,
|
| 131 |
+
"periodo_dados_mercado": {
|
| 132 |
+
"coluna_data": session.coluna_data_mercado,
|
| 133 |
+
"data_inicial": session.periodo_dados_mercado_inicio,
|
| 134 |
+
"data_final": session.periodo_dados_mercado_fim,
|
| 135 |
+
},
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
|
| 139 |
+
def _converter_coluna_para_datas(
|
| 140 |
+
serie: pd.Series,
|
| 141 |
+
coluna: str,
|
| 142 |
+
proporcao_minima: float = 0.8,
|
| 143 |
+
proporcao_excel_minima: float = 0.8,
|
| 144 |
+
) -> pd.Series:
|
| 145 |
+
serie_base = serie.copy()
|
| 146 |
+
if pd.api.types.is_object_dtype(serie_base) or pd.api.types.is_string_dtype(serie_base):
|
| 147 |
+
serie_base = serie_base.astype(str).str.strip().replace("", np.nan)
|
| 148 |
+
|
| 149 |
+
mascara_preenchida = serie_base.notna()
|
| 150 |
+
total_preenchido = int(mascara_preenchida.sum())
|
| 151 |
+
if total_preenchido == 0:
|
| 152 |
+
raise HTTPException(status_code=400, detail=f"A coluna '{coluna}' não possui dados preenchidos.")
|
| 153 |
+
|
| 154 |
+
if pd.api.types.is_datetime64_any_dtype(serie_base):
|
| 155 |
+
datas = pd.to_datetime(serie_base, errors="coerce")
|
| 156 |
+
elif pd.api.types.is_numeric_dtype(serie_base):
|
| 157 |
+
serie_num = pd.to_numeric(serie_base, errors="coerce")
|
| 158 |
+
valores_validos = serie_num[mascara_preenchida].dropna()
|
| 159 |
+
if valores_validos.empty:
|
| 160 |
+
raise HTTPException(status_code=400, detail=f"A coluna '{coluna}' não possui datas válidas.")
|
| 161 |
+
|
| 162 |
+
proporcao_excel = float(valores_validos.between(20000, 80000).mean())
|
| 163 |
+
if proporcao_excel < proporcao_excel_minima:
|
| 164 |
+
raise HTTPException(
|
| 165 |
+
status_code=400,
|
| 166 |
+
detail=(
|
| 167 |
+
f"A coluna '{coluna}' não parece conter datas de calendário "
|
| 168 |
+
"(esperado texto de data ou serial de data do Excel)."
|
| 169 |
+
),
|
| 170 |
+
)
|
| 171 |
+
datas = pd.to_datetime(serie_num, unit="D", origin="1899-12-30", errors="coerce")
|
| 172 |
+
else:
|
| 173 |
+
datas = pd.to_datetime(serie_base, errors="coerce", dayfirst=True)
|
| 174 |
+
|
| 175 |
+
datas_validas = datas[mascara_preenchida].dropna()
|
| 176 |
+
proporcao = len(datas_validas) / total_preenchido if total_preenchido else 0.0
|
| 177 |
+
if proporcao < proporcao_minima:
|
| 178 |
+
raise HTTPException(
|
| 179 |
+
status_code=400,
|
| 180 |
+
detail=(
|
| 181 |
+
f"Não foi possível interpretar a coluna '{coluna}' como data "
|
| 182 |
+
f"(conversão válida em {proporcao:.0%} dos registros preenchidos)."
|
| 183 |
+
),
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
return datas
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _sugerir_coluna_data_mercado(df: pd.DataFrame | None) -> str | None:
|
| 190 |
+
if df is None or df.empty:
|
| 191 |
+
return None
|
| 192 |
+
for coluna in df.columns:
|
| 193 |
+
nome = str(coluna)
|
| 194 |
+
try:
|
| 195 |
+
_converter_coluna_para_datas(
|
| 196 |
+
df[coluna],
|
| 197 |
+
nome,
|
| 198 |
+
proporcao_minima=1.0,
|
| 199 |
+
proporcao_excel_minima=1.0,
|
| 200 |
+
)
|
| 201 |
+
return nome
|
| 202 |
+
except HTTPException:
|
| 203 |
+
continue
|
| 204 |
+
return None
|
| 205 |
+
|
| 206 |
+
|
| 207 |
+
def _calcular_periodo_dados_mercado(df: pd.DataFrame, coluna_data: str) -> dict[str, str]:
|
| 208 |
+
if coluna_data not in df.columns:
|
| 209 |
+
raise HTTPException(status_code=400, detail=f"Coluna '{coluna_data}' não encontrada na base.")
|
| 210 |
+
|
| 211 |
+
datas = _converter_coluna_para_datas(df[coluna_data], coluna_data)
|
| 212 |
+
datas_validas = datas.dropna()
|
| 213 |
+
if datas_validas.empty:
|
| 214 |
+
raise HTTPException(status_code=400, detail=f"A coluna '{coluna_data}' não possui datas válidas.")
|
| 215 |
+
|
| 216 |
+
data_inicial = datas_validas.min().date().isoformat()
|
| 217 |
+
data_final = datas_validas.max().date().isoformat()
|
| 218 |
+
return {
|
| 219 |
+
"data_inicial": data_inicial,
|
| 220 |
+
"data_final": data_final,
|
| 221 |
}
|
| 222 |
|
| 223 |
|
|
|
|
| 392 |
session.percentuais = []
|
| 393 |
session.outliers_anteriores = []
|
| 394 |
session.iteracao = 1
|
| 395 |
+
session.coluna_data_mercado = None
|
| 396 |
+
session.periodo_dados_mercado_inicio = None
|
| 397 |
+
session.periodo_dados_mercado_fim = None
|
| 398 |
|
| 399 |
colunas_numericas = [str(c) for c in obter_colunas_numericas(df)]
|
| 400 |
coluna_y_padrao = identificar_coluna_y_padrao(df)
|
|
|
|
| 406 |
"colunas_numericas": colunas_numericas,
|
| 407 |
"coluna_y_padrao": coluna_y_padrao,
|
| 408 |
"coords": _build_coords_payload(df, tem_coords),
|
| 409 |
+
**_payload_data_mercado(session),
|
| 410 |
}
|
| 411 |
|
| 412 |
|
|
|
|
| 467 |
sucesso,
|
| 468 |
elaborador,
|
| 469 |
outliers_excluidos,
|
| 470 |
+
periodo_dados_mercado,
|
| 471 |
) = carregar_dai(caminho_arquivo)
|
| 472 |
|
| 473 |
if not sucesso or df is None:
|
|
|
|
| 475 |
|
| 476 |
session.elaborador = elaborador
|
| 477 |
session.outliers_anteriores = _clean_int_list(outliers_excluidos)
|
| 478 |
+
periodo_normalizado = _normalizar_periodo_dados_mercado(periodo_dados_mercado)
|
| 479 |
|
| 480 |
base = _set_dataframe_base(session, df, clear_models=True)
|
| 481 |
+
session.coluna_data_mercado = periodo_normalizado["coluna_data"]
|
| 482 |
+
session.periodo_dados_mercado_inicio = periodo_normalizado["data_inicial"]
|
| 483 |
+
session.periodo_dados_mercado_fim = periodo_normalizado["data_final"]
|
| 484 |
session.transformacao_y = str(transformacao_y or "(x)")
|
| 485 |
session.transformacoes_x = {str(k): str(v) for k, v in (transformacoes_x or {}).items()}
|
| 486 |
|
|
|
|
| 519 |
"outliers_html": html_outliers,
|
| 520 |
"contexto": _selection_context(session),
|
| 521 |
"elaborador": sanitize_value(elaborador),
|
| 522 |
+
**_payload_data_mercado(session),
|
| 523 |
}
|
| 524 |
|
| 525 |
|
|
|
|
| 1157 |
return caminho
|
| 1158 |
|
| 1159 |
|
| 1160 |
+
def previsualizar_coluna_data_mercado(session: SessionState, coluna_data: str) -> dict[str, Any]:
|
| 1161 |
+
df = session.df_original
|
| 1162 |
+
if df is None:
|
| 1163 |
+
raise HTTPException(status_code=400, detail="Carregue uma base antes de definir a coluna de data.")
|
| 1164 |
+
|
| 1165 |
+
coluna = str(coluna_data or "").strip()
|
| 1166 |
+
if not coluna:
|
| 1167 |
+
raise HTTPException(status_code=400, detail="Selecione a coluna de data dos dados de mercado.")
|
| 1168 |
+
|
| 1169 |
+
periodo = _calcular_periodo_dados_mercado(df, coluna)
|
| 1170 |
+
return {
|
| 1171 |
+
"status": (
|
| 1172 |
+
f"Prévia do período para '{coluna}': "
|
| 1173 |
+
f"{periodo['data_inicial']} a {periodo['data_final']}"
|
| 1174 |
+
),
|
| 1175 |
+
"coluna_data_mercado": coluna,
|
| 1176 |
+
"periodo_dados_mercado": {
|
| 1177 |
+
"coluna_data": coluna,
|
| 1178 |
+
**periodo,
|
| 1179 |
+
},
|
| 1180 |
+
}
|
| 1181 |
+
|
| 1182 |
+
|
| 1183 |
+
def aplicar_coluna_data_mercado(session: SessionState, coluna_data: str) -> dict[str, Any]:
|
| 1184 |
+
df = session.df_original
|
| 1185 |
+
if df is None:
|
| 1186 |
+
raise HTTPException(status_code=400, detail="Carregue uma base antes de definir a coluna de data.")
|
| 1187 |
+
|
| 1188 |
+
coluna = str(coluna_data or "").strip()
|
| 1189 |
+
if not coluna:
|
| 1190 |
+
raise HTTPException(status_code=400, detail="Selecione a coluna de data dos dados de mercado.")
|
| 1191 |
+
|
| 1192 |
+
periodo = _calcular_periodo_dados_mercado(df, coluna)
|
| 1193 |
+
session.coluna_data_mercado = coluna
|
| 1194 |
+
session.periodo_dados_mercado_inicio = periodo["data_inicial"]
|
| 1195 |
+
session.periodo_dados_mercado_fim = periodo["data_final"]
|
| 1196 |
+
|
| 1197 |
+
return {
|
| 1198 |
+
"status": (
|
| 1199 |
+
"Coluna de data dos dados de mercado aplicada: "
|
| 1200 |
+
f"{coluna} ({periodo['data_inicial']} a {periodo['data_final']})."
|
| 1201 |
+
),
|
| 1202 |
+
"coluna_data_mercado": session.coluna_data_mercado,
|
| 1203 |
+
"periodo_dados_mercado": {
|
| 1204 |
+
"coluna_data": session.coluna_data_mercado,
|
| 1205 |
+
**periodo,
|
| 1206 |
+
},
|
| 1207 |
+
"contexto": _selection_context(session),
|
| 1208 |
+
}
|
| 1209 |
+
|
| 1210 |
+
|
| 1211 |
def exportar_modelo(session: SessionState, nome_arquivo: str, elaborador: dict[str, Any] | None = None) -> tuple[str, str]:
|
| 1212 |
if session.resultado_modelo is None:
|
| 1213 |
raise HTTPException(status_code=400, detail="Ajuste um modelo antes de exportar")
|
|
|
|
| 1225 |
nome_arquivo.strip(),
|
| 1226 |
elaborador=elaborador,
|
| 1227 |
outliers_excluidos=session.outliers_anteriores,
|
| 1228 |
+
periodo_dados_mercado={
|
| 1229 |
+
"coluna_data": session.coluna_data_mercado,
|
| 1230 |
+
"data_inicial": session.periodo_dados_mercado_inicio,
|
| 1231 |
+
"data_final": session.periodo_dados_mercado_fim,
|
| 1232 |
+
},
|
| 1233 |
)
|
| 1234 |
|
| 1235 |
if not caminho:
|
backend/app/services/pesquisa_service.py
CHANGED
|
@@ -45,9 +45,9 @@ TIPO_POR_TOKEN = {
|
|
| 45 |
"DEPOS": "Deposito",
|
| 46 |
"RES": "Residencias isoladas / casas",
|
| 47 |
"SALA": "Salas comerciais",
|
| 48 |
-
"APTO": "Apartamentos
|
| 49 |
-
"APART": "Apartamentos
|
| 50 |
-
"AP": "Apartamentos
|
| 51 |
"TERRENO": "Terrenos",
|
| 52 |
"TER": "Terrenos",
|
| 53 |
"EDIF": "Edificio",
|
|
@@ -70,7 +70,7 @@ CAMPO_TEXTO_ALIASES_COLUNA = {
|
|
| 70 |
}
|
| 71 |
|
| 72 |
CAMPO_FAIXA_META_FONTES = {
|
| 73 |
-
"data": [],
|
| 74 |
"area": [],
|
| 75 |
"rh": [],
|
| 76 |
"aval_data": [],
|
|
@@ -82,10 +82,7 @@ CAMPO_FAIXA_META_FONTES = {
|
|
| 82 |
"aval_valor_total": [],
|
| 83 |
}
|
| 84 |
|
| 85 |
-
CAMPO_FAIXA_ALIASES_COLUNA = {
|
| 86 |
-
"data": DATA_ALIASES,
|
| 87 |
-
"aval_data": DATA_ALIASES,
|
| 88 |
-
}
|
| 89 |
|
| 90 |
CAMPO_FAIXA_ALIASES_VARIAVEL = {
|
| 91 |
"area": AREA_GERAL_ALIASES,
|
|
@@ -150,10 +147,11 @@ MAP_COLORS = [
|
|
| 150 |
|
| 151 |
@dataclass(frozen=True)
|
| 152 |
class PesquisaFiltros:
|
| 153 |
-
otica: str = "
|
| 154 |
nome: str | None = None
|
| 155 |
autor: str | None = None
|
| 156 |
contem_app: str | None = None
|
|
|
|
| 157 |
finalidade: str | None = None
|
| 158 |
finalidade_colunas: list[str] | None = None
|
| 159 |
bairro: str | None = None
|
|
@@ -258,6 +256,7 @@ def listar_modelos(filtros: PesquisaFiltros, limite: int | None = None, somente_
|
|
| 258 |
"nome": filtros.nome,
|
| 259 |
"autor": filtros.autor,
|
| 260 |
"contem_app": filtros.contem_app,
|
|
|
|
| 261 |
"finalidade": filtros.finalidade,
|
| 262 |
"finalidade_colunas": filtros.finalidade_colunas or [],
|
| 263 |
"bairro": filtros.bairro,
|
|
@@ -321,6 +320,7 @@ def listar_modelos(filtros: PesquisaFiltros, limite: int | None = None, somente_
|
|
| 321 |
"nome": filtros.nome,
|
| 322 |
"autor": filtros.autor,
|
| 323 |
"contem_app": filtros.contem_app,
|
|
|
|
| 324 |
"finalidade": filtros.finalidade,
|
| 325 |
"finalidade_colunas": filtros.finalidade_colunas or [],
|
| 326 |
"bairro": filtros.bairro,
|
|
@@ -586,14 +586,14 @@ def _construir_resumo_modelo(caminho_modelo: Path) -> dict[str, Any]:
|
|
| 586 |
)
|
| 587 |
resumo["faixa_rh"] = _merge_ranges(resumo["faixa_rh"], _extrair_faixa_por_alias(estat_df, RH_ALIASES))
|
| 588 |
|
| 589 |
-
resumo["faixa_data"] = _merge_ranges(resumo["faixa_data"],
|
| 590 |
|
| 591 |
colunas_catalogo = _coletar_colunas_para_catalogo(estat_df, df_modelo)
|
| 592 |
resumo["compatibilidade_campos"] = _mapear_compatibilidade(colunas_catalogo)
|
| 593 |
resumo["variaveis_resumo"] = _resumo_variaveis(estat_df)
|
| 594 |
resumo["mapa_disponivel"] = _tem_colunas_mapa(df_modelo)
|
| 595 |
resumo["_texto_colunas_index"] = _indexar_texto_colunas(df_modelo)
|
| 596 |
-
resumo["_faixa_colunas_index"] =
|
| 597 |
resumo["_faixa_variaveis_index"] = _indexar_faixas_variaveis(estat_df, resumo["_variaveis_modelo"])
|
| 598 |
|
| 599 |
return resumo
|
|
@@ -619,6 +619,26 @@ def _r2_do_pacote(pacote: dict[str, Any]) -> float | None:
|
|
| 619 |
return _to_float_or_none(gerais.get("r2"))
|
| 620 |
|
| 621 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
def _variaveis_do_modelo(pacote: dict[str, Any]) -> list[str]:
|
| 623 |
variaveis: list[str] = []
|
| 624 |
|
|
@@ -961,6 +981,9 @@ def _aceita_filtros(modelo: dict[str, Any], filtros: PesquisaFiltros, fontes_adm
|
|
| 961 |
if app_flag is not None and _modelo_contem_variavel(modelo, APP_ALIASES) != app_flag:
|
| 962 |
return False
|
| 963 |
|
|
|
|
|
|
|
|
|
|
| 964 |
if filtros.finalidade and not _aceita_texto_com_colunas(modelo, filtros.finalidade, "finalidade", fontes_admin.get("finalidade")):
|
| 965 |
return False
|
| 966 |
|
|
@@ -979,7 +1002,13 @@ def _aceita_filtros(modelo: dict[str, Any], filtros: PesquisaFiltros, fontes_adm
|
|
| 979 |
if not _aceita_range_com_colunas(modelo, "rh", fontes_admin.get("rh"), filtros.rh_min, filtros.rh_max):
|
| 980 |
return False
|
| 981 |
|
| 982 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 983 |
return False
|
| 984 |
|
| 985 |
return True
|
|
@@ -997,7 +1026,7 @@ def _normalizar_contem_app(value: str | None) -> bool | None:
|
|
| 997 |
|
| 998 |
|
| 999 |
def _normalizar_otica(value: str | None) -> str:
|
| 1000 |
-
return "avaliando"
|
| 1001 |
|
| 1002 |
|
| 1003 |
def _anexar_avaliando_info(
|
|
@@ -1032,11 +1061,17 @@ def _anexar_avaliando_info(
|
|
| 1032 |
aceito = _contains_any(candidatos, str(endereco_info))
|
| 1033 |
registrar("endereco", endereco_info, aceito, "sem correspondencia textual no modelo")
|
| 1034 |
|
| 1035 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1036 |
registrar(
|
| 1037 |
"data",
|
| 1038 |
-
|
| 1039 |
-
|
| 1040 |
f"fora da faixa {formatar_faixa(faixa_data_ref)}",
|
| 1041 |
)
|
| 1042 |
faixa_rh_ref = _faixa_resumo_com_colunas(item, "aval_rh", fontes_admin.get("aval_rh"))
|
|
@@ -1107,6 +1142,7 @@ def _extrair_sugestoes(
|
|
| 1107 |
finalidades: list[str] = []
|
| 1108 |
bairros: list[str] = []
|
| 1109 |
enderecos: list[str] = []
|
|
|
|
| 1110 |
|
| 1111 |
fontes_finalidade = _dedupe_strings((fontes_admin.get("finalidade") or []) + (fontes_admin.get("aval_finalidade") or []))
|
| 1112 |
fontes_bairro = _dedupe_strings((fontes_admin.get("bairros") or []) + (fontes_admin.get("aval_bairro") or []))
|
|
@@ -1125,6 +1161,7 @@ def _extrair_sugestoes(
|
|
| 1125 |
else:
|
| 1126 |
bairros.extend([str(item) for item in (modelo.get("bairros") or [])])
|
| 1127 |
enderecos.append(str(modelo.get("endereco_referencia") or ""))
|
|
|
|
| 1128 |
|
| 1129 |
return {
|
| 1130 |
"nomes_modelo": _lista_textos_unicos(nomes, limite),
|
|
@@ -1132,6 +1169,7 @@ def _extrair_sugestoes(
|
|
| 1132 |
"finalidades": _lista_textos_unicos(finalidades, limite),
|
| 1133 |
"bairros": _lista_textos_unicos(bairros, limite),
|
| 1134 |
"enderecos": _lista_textos_unicos(enderecos, limite),
|
|
|
|
| 1135 |
}
|
| 1136 |
|
| 1137 |
|
|
@@ -1238,6 +1276,11 @@ def _fontes_texto_padrao(modelo: dict[str, Any], campo: str) -> list[str]:
|
|
| 1238 |
def _fontes_faixa_disponiveis(modelo: dict[str, Any], campo: str) -> list[str]:
|
| 1239 |
fontes: list[str] = []
|
| 1240 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1241 |
aliases_coluna = CAMPO_FAIXA_ALIASES_COLUNA.get(campo, [])
|
| 1242 |
indice_colunas = modelo.get("_faixa_colunas_index") or {}
|
| 1243 |
if isinstance(indice_colunas, dict):
|
|
@@ -1258,6 +1301,11 @@ def _fontes_faixa_disponiveis(modelo: dict[str, Any], campo: str) -> list[str]:
|
|
| 1258 |
def _fontes_faixa_padrao(modelo: dict[str, Any], campo: str) -> list[str]:
|
| 1259 |
fontes: list[str] = []
|
| 1260 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1261 |
aliases_coluna = CAMPO_FAIXA_ALIASES_COLUNA.get(campo, [])
|
| 1262 |
indice_colunas = modelo.get("_faixa_colunas_index") or {}
|
| 1263 |
if isinstance(indice_colunas, dict):
|
|
@@ -1310,6 +1358,21 @@ def _aceita_range_com_colunas(
|
|
| 1310 |
return any(_range_overlaps(faixa, filtro_min, filtro_max) for faixa in faixas)
|
| 1311 |
|
| 1312 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1313 |
def _aceita_valor_com_colunas(
|
| 1314 |
modelo: dict[str, Any],
|
| 1315 |
campo: str,
|
|
@@ -1596,6 +1659,26 @@ def formatar_faixa(faixa: dict[str, Any] | None) -> str:
|
|
| 1596 |
return f"ate {maximo}"
|
| 1597 |
|
| 1598 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1599 |
def _extrair_termos_bairro(filtros: PesquisaFiltros) -> list[str]:
|
| 1600 |
termos: list[str] = []
|
| 1601 |
if filtros.bairro:
|
|
@@ -1668,6 +1751,52 @@ def _range_overlaps(model_range: dict[str, Any] | None, filtro_min: Any, filtro_
|
|
| 1668 |
return True
|
| 1669 |
|
| 1670 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1671 |
def _to_comparable(value: Any) -> tuple[str, Any] | None:
|
| 1672 |
if value is None:
|
| 1673 |
return None
|
|
|
|
| 45 |
"DEPOS": "Deposito",
|
| 46 |
"RES": "Residencias isoladas / casas",
|
| 47 |
"SALA": "Salas comerciais",
|
| 48 |
+
"APTO": "Apartamentos",
|
| 49 |
+
"APART": "Apartamentos",
|
| 50 |
+
"AP": "Apartamentos",
|
| 51 |
"TERRENO": "Terrenos",
|
| 52 |
"TER": "Terrenos",
|
| 53 |
"EDIF": "Edificio",
|
|
|
|
| 70 |
}
|
| 71 |
|
| 72 |
CAMPO_FAIXA_META_FONTES = {
|
| 73 |
+
"data": ["meta:faixa_data"],
|
| 74 |
"area": [],
|
| 75 |
"rh": [],
|
| 76 |
"aval_data": [],
|
|
|
|
| 82 |
"aval_valor_total": [],
|
| 83 |
}
|
| 84 |
|
| 85 |
+
CAMPO_FAIXA_ALIASES_COLUNA = {}
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
CAMPO_FAIXA_ALIASES_VARIAVEL = {
|
| 88 |
"area": AREA_GERAL_ALIASES,
|
|
|
|
| 147 |
|
| 148 |
@dataclass(frozen=True)
|
| 149 |
class PesquisaFiltros:
|
| 150 |
+
otica: str = "avaliando"
|
| 151 |
nome: str | None = None
|
| 152 |
autor: str | None = None
|
| 153 |
contem_app: str | None = None
|
| 154 |
+
tipo_modelo: str | None = None
|
| 155 |
finalidade: str | None = None
|
| 156 |
finalidade_colunas: list[str] | None = None
|
| 157 |
bairro: str | None = None
|
|
|
|
| 256 |
"nome": filtros.nome,
|
| 257 |
"autor": filtros.autor,
|
| 258 |
"contem_app": filtros.contem_app,
|
| 259 |
+
"tipo_modelo": filtros.tipo_modelo,
|
| 260 |
"finalidade": filtros.finalidade,
|
| 261 |
"finalidade_colunas": filtros.finalidade_colunas or [],
|
| 262 |
"bairro": filtros.bairro,
|
|
|
|
| 320 |
"nome": filtros.nome,
|
| 321 |
"autor": filtros.autor,
|
| 322 |
"contem_app": filtros.contem_app,
|
| 323 |
+
"tipo_modelo": filtros.tipo_modelo,
|
| 324 |
"finalidade": filtros.finalidade,
|
| 325 |
"finalidade_colunas": filtros.finalidade_colunas or [],
|
| 326 |
"bairro": filtros.bairro,
|
|
|
|
| 586 |
)
|
| 587 |
resumo["faixa_rh"] = _merge_ranges(resumo["faixa_rh"], _extrair_faixa_por_alias(estat_df, RH_ALIASES))
|
| 588 |
|
| 589 |
+
resumo["faixa_data"] = _merge_ranges(resumo["faixa_data"], _faixa_data_do_pacote(pacote))
|
| 590 |
|
| 591 |
colunas_catalogo = _coletar_colunas_para_catalogo(estat_df, df_modelo)
|
| 592 |
resumo["compatibilidade_campos"] = _mapear_compatibilidade(colunas_catalogo)
|
| 593 |
resumo["variaveis_resumo"] = _resumo_variaveis(estat_df)
|
| 594 |
resumo["mapa_disponivel"] = _tem_colunas_mapa(df_modelo)
|
| 595 |
resumo["_texto_colunas_index"] = _indexar_texto_colunas(df_modelo)
|
| 596 |
+
resumo["_faixa_colunas_index"] = {}
|
| 597 |
resumo["_faixa_variaveis_index"] = _indexar_faixas_variaveis(estat_df, resumo["_variaveis_modelo"])
|
| 598 |
|
| 599 |
return resumo
|
|
|
|
| 619 |
return _to_float_or_none(gerais.get("r2"))
|
| 620 |
|
| 621 |
|
| 622 |
+
def _faixa_data_do_pacote(pacote: dict[str, Any]) -> dict[str, Any] | None:
|
| 623 |
+
periodo = pacote.get("periodo_dados_mercado") if isinstance(pacote.get("periodo_dados_mercado"), dict) else {}
|
| 624 |
+
data_inicial = _data_iso_or_none(periodo.get("data_inicial"))
|
| 625 |
+
data_final = _data_iso_or_none(periodo.get("data_final"))
|
| 626 |
+
if data_inicial is None and data_final is None:
|
| 627 |
+
return None
|
| 628 |
+
if data_inicial and data_final and data_inicial > data_final:
|
| 629 |
+
data_inicial, data_final = data_final, data_inicial
|
| 630 |
+
return {"min": data_inicial, "max": data_final}
|
| 631 |
+
|
| 632 |
+
|
| 633 |
+
def _data_iso_or_none(value: Any) -> str | None:
|
| 634 |
+
if _is_empty(value):
|
| 635 |
+
return None
|
| 636 |
+
parsed = pd.to_datetime(value, errors="coerce", dayfirst=True)
|
| 637 |
+
if pd.isna(parsed):
|
| 638 |
+
return None
|
| 639 |
+
return parsed.date().isoformat()
|
| 640 |
+
|
| 641 |
+
|
| 642 |
def _variaveis_do_modelo(pacote: dict[str, Any]) -> list[str]:
|
| 643 |
variaveis: list[str] = []
|
| 644 |
|
|
|
|
| 981 |
if app_flag is not None and _modelo_contem_variavel(modelo, APP_ALIASES) != app_flag:
|
| 982 |
return False
|
| 983 |
|
| 984 |
+
if filtros.tipo_modelo and not _contains_any([_tipo_modelo_modelo(modelo)], filtros.tipo_modelo):
|
| 985 |
+
return False
|
| 986 |
+
|
| 987 |
if filtros.finalidade and not _aceita_texto_com_colunas(modelo, filtros.finalidade, "finalidade", fontes_admin.get("finalidade")):
|
| 988 |
return False
|
| 989 |
|
|
|
|
| 1002 |
if not _aceita_range_com_colunas(modelo, "rh", fontes_admin.get("rh"), filtros.rh_min, filtros.rh_max):
|
| 1003 |
return False
|
| 1004 |
|
| 1005 |
+
data_min = filtros.data_min
|
| 1006 |
+
data_max = filtros.data_max
|
| 1007 |
+
if _is_provided(filtros.aval_data) and not _is_provided(data_min) and not _is_provided(data_max):
|
| 1008 |
+
data_min = filtros.aval_data
|
| 1009 |
+
data_max = filtros.aval_data
|
| 1010 |
+
|
| 1011 |
+
if not _aceita_range_contido_com_colunas(modelo, "data", fontes_admin.get("data"), data_min, data_max):
|
| 1012 |
return False
|
| 1013 |
|
| 1014 |
return True
|
|
|
|
| 1026 |
|
| 1027 |
|
| 1028 |
def _normalizar_otica(value: str | None) -> str:
|
| 1029 |
+
return "avaliando"
|
| 1030 |
|
| 1031 |
|
| 1032 |
def _anexar_avaliando_info(
|
|
|
|
| 1061 |
aceito = _contains_any(candidatos, str(endereco_info))
|
| 1062 |
registrar("endereco", endereco_info, aceito, "sem correspondencia textual no modelo")
|
| 1063 |
|
| 1064 |
+
data_min = filtros.data_min
|
| 1065 |
+
data_max = filtros.data_max
|
| 1066 |
+
if _is_provided(filtros.aval_data) and not _is_provided(data_min) and not _is_provided(data_max):
|
| 1067 |
+
data_min = filtros.aval_data
|
| 1068 |
+
data_max = filtros.aval_data
|
| 1069 |
+
|
| 1070 |
+
faixa_data_ref = _faixa_resumo_com_colunas(item, "data", fontes_admin.get("data"))
|
| 1071 |
registrar(
|
| 1072 |
"data",
|
| 1073 |
+
_periodo_texto_informado(data_min, data_max),
|
| 1074 |
+
_aceita_range_contido_com_colunas(item, "data", fontes_admin.get("data"), data_min, data_max),
|
| 1075 |
f"fora da faixa {formatar_faixa(faixa_data_ref)}",
|
| 1076 |
)
|
| 1077 |
faixa_rh_ref = _faixa_resumo_com_colunas(item, "aval_rh", fontes_admin.get("aval_rh"))
|
|
|
|
| 1142 |
finalidades: list[str] = []
|
| 1143 |
bairros: list[str] = []
|
| 1144 |
enderecos: list[str] = []
|
| 1145 |
+
tipos_modelo: list[str] = []
|
| 1146 |
|
| 1147 |
fontes_finalidade = _dedupe_strings((fontes_admin.get("finalidade") or []) + (fontes_admin.get("aval_finalidade") or []))
|
| 1148 |
fontes_bairro = _dedupe_strings((fontes_admin.get("bairros") or []) + (fontes_admin.get("aval_bairro") or []))
|
|
|
|
| 1161 |
else:
|
| 1162 |
bairros.extend([str(item) for item in (modelo.get("bairros") or [])])
|
| 1163 |
enderecos.append(str(modelo.get("endereco_referencia") or ""))
|
| 1164 |
+
tipos_modelo.append(str(_tipo_modelo_modelo(modelo) or ""))
|
| 1165 |
|
| 1166 |
return {
|
| 1167 |
"nomes_modelo": _lista_textos_unicos(nomes, limite),
|
|
|
|
| 1169 |
"finalidades": _lista_textos_unicos(finalidades, limite),
|
| 1170 |
"bairros": _lista_textos_unicos(bairros, limite),
|
| 1171 |
"enderecos": _lista_textos_unicos(enderecos, limite),
|
| 1172 |
+
"tipos_modelo": _lista_textos_unicos(tipos_modelo, limite),
|
| 1173 |
}
|
| 1174 |
|
| 1175 |
|
|
|
|
| 1276 |
def _fontes_faixa_disponiveis(modelo: dict[str, Any], campo: str) -> list[str]:
|
| 1277 |
fontes: list[str] = []
|
| 1278 |
|
| 1279 |
+
for fonte_meta in CAMPO_FAIXA_META_FONTES.get(campo, []):
|
| 1280 |
+
faixa_meta = _faixa_meta(modelo, fonte_meta)
|
| 1281 |
+
if isinstance(faixa_meta, dict) and not (_is_empty(faixa_meta.get("min")) and _is_empty(faixa_meta.get("max"))):
|
| 1282 |
+
fontes.append(fonte_meta)
|
| 1283 |
+
|
| 1284 |
aliases_coluna = CAMPO_FAIXA_ALIASES_COLUNA.get(campo, [])
|
| 1285 |
indice_colunas = modelo.get("_faixa_colunas_index") or {}
|
| 1286 |
if isinstance(indice_colunas, dict):
|
|
|
|
| 1301 |
def _fontes_faixa_padrao(modelo: dict[str, Any], campo: str) -> list[str]:
|
| 1302 |
fontes: list[str] = []
|
| 1303 |
|
| 1304 |
+
for fonte_meta in CAMPO_FAIXA_META_FONTES.get(campo, []):
|
| 1305 |
+
faixa_meta = _faixa_meta(modelo, fonte_meta)
|
| 1306 |
+
if isinstance(faixa_meta, dict) and not (_is_empty(faixa_meta.get("min")) and _is_empty(faixa_meta.get("max"))):
|
| 1307 |
+
fontes.append(fonte_meta)
|
| 1308 |
+
|
| 1309 |
aliases_coluna = CAMPO_FAIXA_ALIASES_COLUNA.get(campo, [])
|
| 1310 |
indice_colunas = modelo.get("_faixa_colunas_index") or {}
|
| 1311 |
if isinstance(indice_colunas, dict):
|
|
|
|
| 1358 |
return any(_range_overlaps(faixa, filtro_min, filtro_max) for faixa in faixas)
|
| 1359 |
|
| 1360 |
|
| 1361 |
+
def _aceita_range_contido_com_colunas(
|
| 1362 |
+
modelo: dict[str, Any],
|
| 1363 |
+
campo: str,
|
| 1364 |
+
fontes_selecionadas: list[str] | None,
|
| 1365 |
+
filtro_min: Any,
|
| 1366 |
+
filtro_max: Any,
|
| 1367 |
+
) -> bool:
|
| 1368 |
+
if filtro_min is None and filtro_max is None:
|
| 1369 |
+
return True
|
| 1370 |
+
faixas = _faixas_para_campo(modelo, campo, fontes_selecionadas)
|
| 1371 |
+
if not faixas:
|
| 1372 |
+
return False
|
| 1373 |
+
return any(_range_contains(faixa, filtro_min, filtro_max) for faixa in faixas)
|
| 1374 |
+
|
| 1375 |
+
|
| 1376 |
def _aceita_valor_com_colunas(
|
| 1377 |
modelo: dict[str, Any],
|
| 1378 |
campo: str,
|
|
|
|
| 1659 |
return f"ate {maximo}"
|
| 1660 |
|
| 1661 |
|
| 1662 |
+
def _periodo_texto_informado(data_min: Any, data_max: Any) -> str | None:
|
| 1663 |
+
inicio = _str_or_none(data_min)
|
| 1664 |
+
fim = _str_or_none(data_max)
|
| 1665 |
+
if not inicio and not fim:
|
| 1666 |
+
return None
|
| 1667 |
+
if inicio and fim:
|
| 1668 |
+
return f"{inicio} a {fim}"
|
| 1669 |
+
if inicio:
|
| 1670 |
+
return f"a partir de {inicio}"
|
| 1671 |
+
return f"ate {fim}"
|
| 1672 |
+
|
| 1673 |
+
|
| 1674 |
+
def _tipo_modelo_modelo(modelo: dict[str, Any]) -> str | None:
|
| 1675 |
+
tipo = _str_or_none(modelo.get("tipo_imovel"))
|
| 1676 |
+
if tipo:
|
| 1677 |
+
return tipo
|
| 1678 |
+
nome_referencia = _str_or_none(modelo.get("nome_modelo")) or _str_or_none(modelo.get("arquivo")) or ""
|
| 1679 |
+
return _inferir_tipo_por_nome(nome_referencia)
|
| 1680 |
+
|
| 1681 |
+
|
| 1682 |
def _extrair_termos_bairro(filtros: PesquisaFiltros) -> list[str]:
|
| 1683 |
termos: list[str] = []
|
| 1684 |
if filtros.bairro:
|
|
|
|
| 1751 |
return True
|
| 1752 |
|
| 1753 |
|
| 1754 |
+
def _range_contains(model_range: dict[str, Any] | None, filtro_min: Any, filtro_max: Any) -> bool:
|
| 1755 |
+
if filtro_min is None and filtro_max is None:
|
| 1756 |
+
return True
|
| 1757 |
+
|
| 1758 |
+
if not model_range:
|
| 1759 |
+
return False
|
| 1760 |
+
|
| 1761 |
+
model_min_cmp = _to_comparable(model_range.get("min"))
|
| 1762 |
+
model_max_cmp = _to_comparable(model_range.get("max"))
|
| 1763 |
+
filtro_min_cmp = _to_comparable(filtro_min) if filtro_min is not None else None
|
| 1764 |
+
filtro_max_cmp = _to_comparable(filtro_max) if filtro_max is not None else None
|
| 1765 |
+
|
| 1766 |
+
if filtro_min_cmp is None and filtro_max_cmp is None:
|
| 1767 |
+
return True
|
| 1768 |
+
|
| 1769 |
+
kinds = {
|
| 1770 |
+
item[0]
|
| 1771 |
+
for item in [model_min_cmp, model_max_cmp, filtro_min_cmp, filtro_max_cmp]
|
| 1772 |
+
if item is not None
|
| 1773 |
+
}
|
| 1774 |
+
|
| 1775 |
+
if len(kinds) != 1:
|
| 1776 |
+
return False
|
| 1777 |
+
|
| 1778 |
+
model_min_val = model_min_cmp[1] if model_min_cmp is not None else None
|
| 1779 |
+
model_max_val = model_max_cmp[1] if model_max_cmp is not None else None
|
| 1780 |
+
filtro_min_val = filtro_min_cmp[1] if filtro_min_cmp is not None else None
|
| 1781 |
+
filtro_max_val = filtro_max_cmp[1] if filtro_max_cmp is not None else None
|
| 1782 |
+
|
| 1783 |
+
if model_min_val is None or model_max_val is None:
|
| 1784 |
+
return False
|
| 1785 |
+
|
| 1786 |
+
if filtro_min_val is not None and filtro_max_val is not None and filtro_min_val > filtro_max_val:
|
| 1787 |
+
filtro_min_val, filtro_max_val = filtro_max_val, filtro_min_val
|
| 1788 |
+
|
| 1789 |
+
if filtro_min_val is not None:
|
| 1790 |
+
if filtro_min_val < model_min_val or filtro_min_val > model_max_val:
|
| 1791 |
+
return False
|
| 1792 |
+
|
| 1793 |
+
if filtro_max_val is not None:
|
| 1794 |
+
if filtro_max_val < model_min_val or filtro_max_val > model_max_val:
|
| 1795 |
+
return False
|
| 1796 |
+
|
| 1797 |
+
return True
|
| 1798 |
+
|
| 1799 |
+
|
| 1800 |
def _to_comparable(value: Any) -> tuple[str, Any] | None:
|
| 1801 |
if value is None:
|
| 1802 |
return None
|
backend/app/services/visualizacao_service.py
CHANGED
|
@@ -71,6 +71,10 @@ def _extrair_modelo_info(pacote: dict[str, Any]) -> dict[str, Any]:
|
|
| 71 |
dicotomicas = [str(v) for v in (pacote["transformacoes"].get("dicotomicas", []) or [])]
|
| 72 |
codigo_alocado = [str(v) for v in (pacote["transformacoes"].get("codigo_alocado", []) or [])]
|
| 73 |
percentuais = [str(v) for v in (pacote["transformacoes"].get("percentuais", []) or [])]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
return {
|
| 76 |
"nome_y": nome_y.strip(),
|
|
@@ -80,6 +84,11 @@ def _extrair_modelo_info(pacote: dict[str, Any]) -> dict[str, Any]:
|
|
| 80 |
"dicotomicas": dicotomicas,
|
| 81 |
"codigo_alocado": codigo_alocado,
|
| 82 |
"percentuais": percentuais,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
}
|
| 84 |
|
| 85 |
|
|
|
|
| 71 |
dicotomicas = [str(v) for v in (pacote["transformacoes"].get("dicotomicas", []) or [])]
|
| 72 |
codigo_alocado = [str(v) for v in (pacote["transformacoes"].get("codigo_alocado", []) or [])]
|
| 73 |
percentuais = [str(v) for v in (pacote["transformacoes"].get("percentuais", []) or [])]
|
| 74 |
+
periodo = pacote.get("periodo_dados_mercado") or {}
|
| 75 |
+
coluna_data = str(periodo.get("coluna_data") or "").strip() or None
|
| 76 |
+
data_inicial = str(periodo.get("data_inicial") or "").strip() or None
|
| 77 |
+
data_final = str(periodo.get("data_final") or "").strip() or None
|
| 78 |
|
| 79 |
return {
|
| 80 |
"nome_y": nome_y.strip(),
|
|
|
|
| 84 |
"dicotomicas": dicotomicas,
|
| 85 |
"codigo_alocado": codigo_alocado,
|
| 86 |
"percentuais": percentuais,
|
| 87 |
+
"periodo_dados_mercado": {
|
| 88 |
+
"coluna_data": coluna_data,
|
| 89 |
+
"data_inicial": data_inicial,
|
| 90 |
+
"data_final": data_final,
|
| 91 |
+
},
|
| 92 |
}
|
| 93 |
|
| 94 |
|
frontend/src/App.jsx
CHANGED
|
@@ -1,10 +1,12 @@
|
|
| 1 |
import React, { useEffect, useState } from 'react'
|
| 2 |
import { api } from './api'
|
| 3 |
import ElaboracaoTab from './components/ElaboracaoTab'
|
|
|
|
| 4 |
import PesquisaTab from './components/PesquisaTab'
|
| 5 |
import VisualizacaoTab from './components/VisualizacaoTab'
|
| 6 |
|
| 7 |
const TABS = [
|
|
|
|
| 8 |
{ key: 'Pesquisa', label: 'Pesquisa', hint: 'Busca inicial de modelos .dai' },
|
| 9 |
{ key: 'Elaboração/Edição', label: 'Elaboração/Edição', hint: 'Fluxo completo de modelagem' },
|
| 10 |
{ key: 'Visualização/Avaliação', label: 'Visualização/Avaliação', hint: 'Leitura e avaliação de modelos .dai' },
|
|
@@ -58,6 +60,10 @@ export default function App() {
|
|
| 58 |
|
| 59 |
{bootError ? <div className="error-line">Falha ao criar sessão: {bootError}</div> : null}
|
| 60 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
{activeTab === 'Pesquisa' ? (
|
| 62 |
<div className="tab-pane">
|
| 63 |
<PesquisaTab />
|
|
|
|
| 1 |
import React, { useEffect, useState } from 'react'
|
| 2 |
import { api } from './api'
|
| 3 |
import ElaboracaoTab from './components/ElaboracaoTab'
|
| 4 |
+
import InicioTab from './components/InicioTab'
|
| 5 |
import PesquisaTab from './components/PesquisaTab'
|
| 6 |
import VisualizacaoTab from './components/VisualizacaoTab'
|
| 7 |
|
| 8 |
const TABS = [
|
| 9 |
+
{ key: 'Início', label: 'Início', hint: 'Visão geral rápida do aplicativo' },
|
| 10 |
{ key: 'Pesquisa', label: 'Pesquisa', hint: 'Busca inicial de modelos .dai' },
|
| 11 |
{ key: 'Elaboração/Edição', label: 'Elaboração/Edição', hint: 'Fluxo completo de modelagem' },
|
| 12 |
{ key: 'Visualização/Avaliação', label: 'Visualização/Avaliação', hint: 'Leitura e avaliação de modelos .dai' },
|
|
|
|
| 60 |
|
| 61 |
{bootError ? <div className="error-line">Falha ao criar sessão: {bootError}</div> : null}
|
| 62 |
|
| 63 |
+
<div className="tab-pane" hidden={activeTab !== 'Início'}>
|
| 64 |
+
<InicioTab />
|
| 65 |
+
</div>
|
| 66 |
+
|
| 67 |
{activeTab === 'Pesquisa' ? (
|
| 68 |
<div className="tab-pane">
|
| 69 |
<PesquisaTab />
|
frontend/src/api.js
CHANGED
|
@@ -153,6 +153,8 @@ export const api = {
|
|
| 153 |
},
|
| 154 |
exportBase: (sessionId, filtered = true) => getBlob(`/api/elaboracao/export-base?session_id=${encodeURIComponent(sessionId)}&filtered=${String(filtered)}`),
|
| 155 |
updateElaboracaoMap: (sessionId, variavelMapa) => postJson('/api/elaboracao/map/update', { session_id: sessionId, variavel_mapa: variavelMapa }),
|
|
|
|
|
|
|
| 156 |
getContext: (sessionId) => getJson(`/api/elaboracao/context?session_id=${encodeURIComponent(sessionId)}`),
|
| 157 |
|
| 158 |
uploadVisualizacaoFile(sessionId, file) {
|
|
|
|
| 153 |
},
|
| 154 |
exportBase: (sessionId, filtered = true) => getBlob(`/api/elaboracao/export-base?session_id=${encodeURIComponent(sessionId)}&filtered=${String(filtered)}`),
|
| 155 |
updateElaboracaoMap: (sessionId, variavelMapa) => postJson('/api/elaboracao/map/update', { session_id: sessionId, variavel_mapa: variavelMapa }),
|
| 156 |
+
previewMarketDateColumn: (sessionId, colunaData) => postJson('/api/elaboracao/market-date/preview', { session_id: sessionId, coluna_data: colunaData }),
|
| 157 |
+
applyMarketDateColumn: (sessionId, colunaData) => postJson('/api/elaboracao/market-date/apply', { session_id: sessionId, coluna_data: colunaData }),
|
| 158 |
getContext: (sessionId) => getJson(`/api/elaboracao/context?session_id=${encodeURIComponent(sessionId)}`),
|
| 159 |
|
| 160 |
uploadVisualizacaoFile(sessionId, file) {
|
frontend/src/components/ElaboracaoTab.jsx
CHANGED
|
@@ -60,6 +60,30 @@ function formatTransformacaoBadge(transformacao) {
|
|
| 60 |
return valor
|
| 61 |
}
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
function buildLoadedModelInfo(resp) {
|
| 64 |
const tipo = String(resp?.tipo || '').toLowerCase()
|
| 65 |
if (tipo !== 'dai') return null
|
|
@@ -77,6 +101,7 @@ function buildLoadedModelInfo(resp) {
|
|
| 77 |
colunas_x: colunasX,
|
| 78 |
transformacao_y: fit.transformacao_y || contexto.transformacao_y || '(x)',
|
| 79 |
transformacoes_x: transformacoesX,
|
|
|
|
| 80 |
}
|
| 81 |
}
|
| 82 |
|
|
@@ -154,6 +179,13 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 154 |
const [dicotomicas, setDicotomicas] = useState([])
|
| 155 |
const [codigoAlocado, setCodigoAlocado] = useState([])
|
| 156 |
const [percentuais, setPercentuais] = useState([])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
const [outliersAnteriores, setOutliersAnteriores] = useState([])
|
| 159 |
const [iteracao, setIteracao] = useState(1)
|
|
@@ -225,6 +257,18 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 225 |
transformacao: formatTransformacaoBadge(transformacoes[coluna]),
|
| 226 |
}))
|
| 227 |
}, [modeloCarregadoInfo])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
const transformacaoAplicadaYBadge = useMemo(
|
| 229 |
() => formatTransformacaoBadge(transformacoesAplicadas?.transformacao_y),
|
| 230 |
[transformacoesAplicadas],
|
|
@@ -364,10 +408,25 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 364 |
|
| 365 |
function applyBaseResponse(resp, options = {}) {
|
| 366 |
const resetXSelection = Boolean(options.resetXSelection)
|
|
|
|
| 367 |
if (resp.status) setStatus(resp.status)
|
| 368 |
if (resp.dados) setDados(resp.dados)
|
| 369 |
if (resp.mapa_html) setMapaHtml(resp.mapa_html)
|
| 370 |
if (resp.colunas_numericas) setColunasNumericas(resp.colunas_numericas)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
if (resp.coluna_y_padrao) setColunaY(resp.coluna_y_padrao)
|
| 372 |
|
| 373 |
if (resp.contexto && !resetXSelection) {
|
|
@@ -389,6 +448,11 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 389 |
setOrigemTransformacoes(null)
|
| 390 |
setOutliersAnteriores([])
|
| 391 |
setIteracao(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
setSelection(null)
|
| 393 |
setFit(null)
|
| 394 |
setCamposAvaliacao([])
|
|
@@ -526,6 +590,12 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 526 |
setTransformacoesX({})
|
| 527 |
setTransformacoesAplicadas(null)
|
| 528 |
setOrigemTransformacoes(null)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 529 |
setCamposAvaliacao([])
|
| 530 |
valoresAvaliacaoRef.current = {}
|
| 531 |
setAvaliacaoFormVersion((prev) => prev + 1)
|
|
@@ -587,6 +657,50 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 587 |
})
|
| 588 |
}
|
| 589 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 590 |
async function onMapCoords() {
|
| 591 |
if (!manualLat || !manualLon || !sessionId) return
|
| 592 |
setLoading(true)
|
|
@@ -1090,6 +1204,10 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1090 |
) : (
|
| 1091 |
<div className="section1-empty-hint">Sem variáveis independentes no modelo carregado.</div>
|
| 1092 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1093 |
</div>
|
| 1094 |
</div>
|
| 1095 |
</div>
|
|
@@ -1377,7 +1495,47 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1377 |
</div>
|
| 1378 |
</SectionBlock>
|
| 1379 |
|
| 1380 |
-
<SectionBlock
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1381 |
<div className="row">
|
| 1382 |
<label>Variável Dependente (Y)</label>
|
| 1383 |
<select value={colunaY} onChange={(e) => setColunaY(e.target.value)}>
|
|
@@ -1389,7 +1547,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1389 |
</div>
|
| 1390 |
</SectionBlock>
|
| 1391 |
|
| 1392 |
-
<SectionBlock step="
|
| 1393 |
<div className="compact-option-group compact-option-group-x">
|
| 1394 |
<h4>Variáveis Independentes (X)</h4>
|
| 1395 |
<div className="checkbox-inline-wrap checkbox-inline-wrap-tools">
|
|
@@ -1472,15 +1630,15 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1472 |
|
| 1473 |
{selection ? (
|
| 1474 |
<>
|
| 1475 |
-
<SectionBlock step="
|
| 1476 |
<DataTable table={selection.estatisticas} />
|
| 1477 |
</SectionBlock>
|
| 1478 |
|
| 1479 |
-
<SectionBlock step="
|
| 1480 |
<div dangerouslySetInnerHTML={{ __html: selection.micronumerosidade_html || '' }} />
|
| 1481 |
</SectionBlock>
|
| 1482 |
|
| 1483 |
-
<SectionBlock step="
|
| 1484 |
<details
|
| 1485 |
className="section-content-toggle"
|
| 1486 |
open={section8Open}
|
|
@@ -1498,7 +1656,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1498 |
</details>
|
| 1499 |
</SectionBlock>
|
| 1500 |
|
| 1501 |
-
<SectionBlock step="
|
| 1502 |
<div className="row">
|
| 1503 |
<label>Grau mínimo dos coeficientes</label>
|
| 1504 |
<select value={grauCoef} onChange={(e) => setGrauCoef(Number(e.target.value))}>
|
|
@@ -1555,7 +1713,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1555 |
)}
|
| 1556 |
</SectionBlock>
|
| 1557 |
|
| 1558 |
-
<SectionBlock step="
|
| 1559 |
<div className="manual-transform-toggle">
|
| 1560 |
<button
|
| 1561 |
type="button"
|
|
@@ -1625,6 +1783,10 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1625 |
</div>
|
| 1626 |
</div>
|
| 1627 |
) : null}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1628 |
</div>
|
| 1629 |
</div>
|
| 1630 |
</div>
|
|
@@ -1635,7 +1797,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1635 |
|
| 1636 |
{fit ? (
|
| 1637 |
<>
|
| 1638 |
-
<SectionBlock step="
|
| 1639 |
<details
|
| 1640 |
className="section-content-toggle"
|
| 1641 |
open={section11Open}
|
|
@@ -1658,7 +1820,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1658 |
</details>
|
| 1659 |
</SectionBlock>
|
| 1660 |
|
| 1661 |
-
<SectionBlock step="
|
| 1662 |
<div dangerouslySetInnerHTML={{ __html: fit.diagnosticos_html || '' }} />
|
| 1663 |
<div className="two-col diagnostic-tables">
|
| 1664 |
<div className="pane">
|
|
@@ -1672,7 +1834,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1672 |
</div>
|
| 1673 |
</SectionBlock>
|
| 1674 |
|
| 1675 |
-
<SectionBlock step="
|
| 1676 |
<div className="plot-grid-2-fixed">
|
| 1677 |
<PlotFigure figure={fit.grafico_obs_calc} title="Obs x Calc" />
|
| 1678 |
<PlotFigure figure={fit.grafico_residuos} title="Resíduos" />
|
|
@@ -1684,11 +1846,11 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1684 |
</div>
|
| 1685 |
</SectionBlock>
|
| 1686 |
|
| 1687 |
-
<SectionBlock step="
|
| 1688 |
<DataTable table={fit.tabela_metricas} maxHeight={320} />
|
| 1689 |
</SectionBlock>
|
| 1690 |
|
| 1691 |
-
<SectionBlock step="
|
| 1692 |
{outliersAnteriores.length > 0 && outliersHtml ? (
|
| 1693 |
<div className="outliers-html-box" dangerouslySetInnerHTML={{ __html: outliersHtml }} />
|
| 1694 |
) : null}
|
|
@@ -1772,7 +1934,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1772 |
<div className="resumo-outliers-box">Outliers anteriores: {joinSelection(outliersAnteriores) || '-'}</div>
|
| 1773 |
</SectionBlock>
|
| 1774 |
|
| 1775 |
-
<SectionBlock step="
|
| 1776 |
<div className="avaliacao-grid" key={`avaliacao-grid-elab-${avaliacaoFormVersion}`}>
|
| 1777 |
{camposAvaliacao.map((campo) => (
|
| 1778 |
<div key={`aval-${campo.coluna}`} className="avaliacao-card">
|
|
@@ -1825,7 +1987,7 @@ export default function ElaboracaoTab({ sessionId }) {
|
|
| 1825 |
/>
|
| 1826 |
</SectionBlock>
|
| 1827 |
|
| 1828 |
-
<SectionBlock step="
|
| 1829 |
<div className="row">
|
| 1830 |
<label>Nome do arquivo (.dai)</label>
|
| 1831 |
<input type="text" value={nomeArquivoExport} onChange={(e) => setNomeArquivoExport(e.target.value)} />
|
|
|
|
| 60 |
return valor
|
| 61 |
}
|
| 62 |
|
| 63 |
+
function formatDateBr(value) {
|
| 64 |
+
const text = String(value || '').trim()
|
| 65 |
+
if (!text) return ''
|
| 66 |
+
const isoMatch = text.match(/^(\d{4})-(\d{2})-(\d{2})$/)
|
| 67 |
+
if (isoMatch) {
|
| 68 |
+
return `${isoMatch[3]}/${isoMatch[2]}/${isoMatch[1]}`
|
| 69 |
+
}
|
| 70 |
+
return text
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
function normalizePeriodoDadosMercado(periodo) {
|
| 74 |
+
if (!periodo || typeof periodo !== 'object') return null
|
| 75 |
+
const dataInicial = String(periodo.data_inicial || '').trim()
|
| 76 |
+
const dataFinal = String(periodo.data_final || '').trim()
|
| 77 |
+
if (!dataInicial || !dataFinal) return null
|
| 78 |
+
return { data_inicial: dataInicial, data_final: dataFinal }
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
function formatPeriodoDadosMercado(periodo) {
|
| 82 |
+
const normalizado = normalizePeriodoDadosMercado(periodo)
|
| 83 |
+
if (!normalizado) return '-'
|
| 84 |
+
return `${formatDateBr(normalizado.data_inicial)} a ${formatDateBr(normalizado.data_final)}`
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
function buildLoadedModelInfo(resp) {
|
| 88 |
const tipo = String(resp?.tipo || '').toLowerCase()
|
| 89 |
if (tipo !== 'dai') return null
|
|
|
|
| 101 |
colunas_x: colunasX,
|
| 102 |
transformacao_y: fit.transformacao_y || contexto.transformacao_y || '(x)',
|
| 103 |
transformacoes_x: transformacoesX,
|
| 104 |
+
periodo_dados_mercado: normalizePeriodoDadosMercado(resp?.periodo_dados_mercado || contexto?.periodo_dados_mercado),
|
| 105 |
}
|
| 106 |
}
|
| 107 |
|
|
|
|
| 179 |
const [dicotomicas, setDicotomicas] = useState([])
|
| 180 |
const [codigoAlocado, setCodigoAlocado] = useState([])
|
| 181 |
const [percentuais, setPercentuais] = useState([])
|
| 182 |
+
const [colunasDataMercado, setColunasDataMercado] = useState([])
|
| 183 |
+
const [colunaDataMercadoSugerida, setColunaDataMercadoSugerida] = useState('')
|
| 184 |
+
const [colunaDataMercado, setColunaDataMercado] = useState('')
|
| 185 |
+
const [periodoDadosMercado, setPeriodoDadosMercado] = useState(null)
|
| 186 |
+
const [periodoDadosMercadoPreview, setPeriodoDadosMercadoPreview] = useState(null)
|
| 187 |
+
const [dataMercadoError, setDataMercadoError] = useState('')
|
| 188 |
+
const [dataMercadoLoading, setDataMercadoLoading] = useState(false)
|
| 189 |
|
| 190 |
const [outliersAnteriores, setOutliersAnteriores] = useState([])
|
| 191 |
const [iteracao, setIteracao] = useState(1)
|
|
|
|
| 257 |
transformacao: formatTransformacaoBadge(transformacoes[coluna]),
|
| 258 |
}))
|
| 259 |
}, [modeloCarregadoInfo])
|
| 260 |
+
const periodoModeloCarregadoTexto = useMemo(
|
| 261 |
+
() => formatPeriodoDadosMercado(modeloCarregadoInfo?.periodo_dados_mercado),
|
| 262 |
+
[modeloCarregadoInfo],
|
| 263 |
+
)
|
| 264 |
+
const periodoDadosMercadoTexto = useMemo(
|
| 265 |
+
() => formatPeriodoDadosMercado(periodoDadosMercado),
|
| 266 |
+
[periodoDadosMercado],
|
| 267 |
+
)
|
| 268 |
+
const periodoDadosMercadoPreviewTexto = useMemo(
|
| 269 |
+
() => formatPeriodoDadosMercado(periodoDadosMercadoPreview),
|
| 270 |
+
[periodoDadosMercadoPreview],
|
| 271 |
+
)
|
| 272 |
const transformacaoAplicadaYBadge = useMemo(
|
| 273 |
() => formatTransformacaoBadge(transformacoesAplicadas?.transformacao_y),
|
| 274 |
[transformacoesAplicadas],
|
|
|
|
| 408 |
|
| 409 |
function applyBaseResponse(resp, options = {}) {
|
| 410 |
const resetXSelection = Boolean(options.resetXSelection)
|
| 411 |
+
setDataMercadoError('')
|
| 412 |
if (resp.status) setStatus(resp.status)
|
| 413 |
if (resp.dados) setDados(resp.dados)
|
| 414 |
if (resp.mapa_html) setMapaHtml(resp.mapa_html)
|
| 415 |
if (resp.colunas_numericas) setColunasNumericas(resp.colunas_numericas)
|
| 416 |
+
if (Array.isArray(resp.colunas_data_mercado)) {
|
| 417 |
+
setColunasDataMercado(resp.colunas_data_mercado.map((item) => String(item)))
|
| 418 |
+
}
|
| 419 |
+
if (Object.prototype.hasOwnProperty.call(resp, 'coluna_data_mercado_sugerida')) {
|
| 420 |
+
setColunaDataMercadoSugerida(String(resp.coluna_data_mercado_sugerida || ''))
|
| 421 |
+
}
|
| 422 |
+
if (Object.prototype.hasOwnProperty.call(resp, 'coluna_data_mercado')) {
|
| 423 |
+
setColunaDataMercado(String(resp.coluna_data_mercado || ''))
|
| 424 |
+
}
|
| 425 |
+
if (Object.prototype.hasOwnProperty.call(resp, 'periodo_dados_mercado')) {
|
| 426 |
+
const periodo = normalizePeriodoDadosMercado(resp.periodo_dados_mercado)
|
| 427 |
+
setPeriodoDadosMercado(periodo)
|
| 428 |
+
setPeriodoDadosMercadoPreview(periodo)
|
| 429 |
+
}
|
| 430 |
if (resp.coluna_y_padrao) setColunaY(resp.coluna_y_padrao)
|
| 431 |
|
| 432 |
if (resp.contexto && !resetXSelection) {
|
|
|
|
| 448 |
setOrigemTransformacoes(null)
|
| 449 |
setOutliersAnteriores([])
|
| 450 |
setIteracao(1)
|
| 451 |
+
setColunaDataMercadoSugerida('')
|
| 452 |
+
setColunaDataMercado('')
|
| 453 |
+
setPeriodoDadosMercado(null)
|
| 454 |
+
setPeriodoDadosMercadoPreview(null)
|
| 455 |
+
setDataMercadoError('')
|
| 456 |
setSelection(null)
|
| 457 |
setFit(null)
|
| 458 |
setCamposAvaliacao([])
|
|
|
|
| 590 |
setTransformacoesX({})
|
| 591 |
setTransformacoesAplicadas(null)
|
| 592 |
setOrigemTransformacoes(null)
|
| 593 |
+
setColunasDataMercado([])
|
| 594 |
+
setColunaDataMercadoSugerida('')
|
| 595 |
+
setColunaDataMercado('')
|
| 596 |
+
setPeriodoDadosMercado(null)
|
| 597 |
+
setPeriodoDadosMercadoPreview(null)
|
| 598 |
+
setDataMercadoError('')
|
| 599 |
setCamposAvaliacao([])
|
| 600 |
valoresAvaliacaoRef.current = {}
|
| 601 |
setAvaliacaoFormVersion((prev) => prev + 1)
|
|
|
|
| 657 |
})
|
| 658 |
}
|
| 659 |
|
| 660 |
+
async function onColunaDataMercadoChange(value) {
|
| 661 |
+
const coluna = String(value || '')
|
| 662 |
+
setColunaDataMercado(coluna)
|
| 663 |
+
setDataMercadoError('')
|
| 664 |
+
|
| 665 |
+
if (!sessionId || !coluna) {
|
| 666 |
+
setPeriodoDadosMercadoPreview(null)
|
| 667 |
+
return
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
setDataMercadoLoading(true)
|
| 671 |
+
try {
|
| 672 |
+
const resp = await api.previewMarketDateColumn(sessionId, coluna)
|
| 673 |
+
const periodo = normalizePeriodoDadosMercado(resp.periodo_dados_mercado)
|
| 674 |
+
setPeriodoDadosMercadoPreview(periodo)
|
| 675 |
+
if (resp.status) setStatus(resp.status)
|
| 676 |
+
} catch (err) {
|
| 677 |
+
setPeriodoDadosMercadoPreview(null)
|
| 678 |
+
setDataMercadoError(err.message || 'Falha ao identificar período da coluna selecionada.')
|
| 679 |
+
} finally {
|
| 680 |
+
setDataMercadoLoading(false)
|
| 681 |
+
}
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
async function onAplicarColunaDataMercado() {
|
| 685 |
+
if (!sessionId || !colunaDataMercado) return
|
| 686 |
+
await withBusy(async () => {
|
| 687 |
+
const resp = await api.applyMarketDateColumn(sessionId, colunaDataMercado)
|
| 688 |
+
const periodo = normalizePeriodoDadosMercado(resp.periodo_dados_mercado)
|
| 689 |
+
setColunaDataMercado(String(resp.coluna_data_mercado || colunaDataMercado))
|
| 690 |
+
setPeriodoDadosMercado(periodo)
|
| 691 |
+
setPeriodoDadosMercadoPreview(periodo)
|
| 692 |
+
setDataMercadoError('')
|
| 693 |
+
if (resp.status) setStatus(resp.status)
|
| 694 |
+
setModeloCarregadoInfo((prev) => {
|
| 695 |
+
if (!prev) return prev
|
| 696 |
+
return {
|
| 697 |
+
...prev,
|
| 698 |
+
periodo_dados_mercado: periodo,
|
| 699 |
+
}
|
| 700 |
+
})
|
| 701 |
+
})
|
| 702 |
+
}
|
| 703 |
+
|
| 704 |
async function onMapCoords() {
|
| 705 |
if (!manualLat || !manualLon || !sessionId) return
|
| 706 |
setLoading(true)
|
|
|
|
| 1204 |
) : (
|
| 1205 |
<div className="section1-empty-hint">Sem variáveis independentes no modelo carregado.</div>
|
| 1206 |
)}
|
| 1207 |
+
<div className="variavel-badge-line">
|
| 1208 |
+
<span className="variavel-badge-label">Período dados:</span>
|
| 1209 |
+
<span className="variavel-badge-value">{periodoModeloCarregadoTexto}</span>
|
| 1210 |
+
</div>
|
| 1211 |
</div>
|
| 1212 |
</div>
|
| 1213 |
</div>
|
|
|
|
| 1495 |
</div>
|
| 1496 |
</SectionBlock>
|
| 1497 |
|
| 1498 |
+
<SectionBlock
|
| 1499 |
+
step="4"
|
| 1500 |
+
title="Definir Data dos Dados de Mercado"
|
| 1501 |
+
subtitle="Selecione a coluna de data dos dados de mercado, visualize o período e aplique."
|
| 1502 |
+
>
|
| 1503 |
+
<div className="market-date-grid">
|
| 1504 |
+
<div className="section1-empty-hint">
|
| 1505 |
+
Sugestão de coluna: {colunaDataMercadoSugerida || '-'}
|
| 1506 |
+
</div>
|
| 1507 |
+
<div className="row market-date-row">
|
| 1508 |
+
<label>Coluna de data dos dados de mercado</label>
|
| 1509 |
+
<select
|
| 1510 |
+
value={colunaDataMercado}
|
| 1511 |
+
onChange={(e) => {
|
| 1512 |
+
void onColunaDataMercadoChange(e.target.value)
|
| 1513 |
+
}}
|
| 1514 |
+
disabled={loading || dataMercadoLoading || colunasDataMercado.length === 0}
|
| 1515 |
+
>
|
| 1516 |
+
<option value="">Selecione</option>
|
| 1517 |
+
{colunasDataMercado.map((col) => (
|
| 1518 |
+
<option key={`data-${col}`} value={col}>{col}</option>
|
| 1519 |
+
))}
|
| 1520 |
+
</select>
|
| 1521 |
+
</div>
|
| 1522 |
+
<div className="row market-date-actions-row">
|
| 1523 |
+
<div className="resumo-outliers-box">
|
| 1524 |
+
Período identificado: {periodoDadosMercadoPreviewTexto}
|
| 1525 |
+
</div>
|
| 1526 |
+
<button
|
| 1527 |
+
type="button"
|
| 1528 |
+
onClick={onAplicarColunaDataMercado}
|
| 1529 |
+
disabled={loading || dataMercadoLoading || !colunaDataMercado || !periodoDadosMercadoPreview}
|
| 1530 |
+
>
|
| 1531 |
+
Aplicar período
|
| 1532 |
+
</button>
|
| 1533 |
+
</div>
|
| 1534 |
+
{dataMercadoError ? <div className="error-line inline-error">{dataMercadoError}</div> : null}
|
| 1535 |
+
</div>
|
| 1536 |
+
</SectionBlock>
|
| 1537 |
+
|
| 1538 |
+
<SectionBlock step="5" title="Selecionar Variável Dependente" subtitle="Defina a variável dependente (Y).">
|
| 1539 |
<div className="row">
|
| 1540 |
<label>Variável Dependente (Y)</label>
|
| 1541 |
<select value={colunaY} onChange={(e) => setColunaY(e.target.value)}>
|
|
|
|
| 1547 |
</div>
|
| 1548 |
</SectionBlock>
|
| 1549 |
|
| 1550 |
+
<SectionBlock step="6" title="Selecionar Variáveis Independentes" subtitle="Escolha regressoras e grupos de tipologia.">
|
| 1551 |
<div className="compact-option-group compact-option-group-x">
|
| 1552 |
<h4>Variáveis Independentes (X)</h4>
|
| 1553 |
<div className="checkbox-inline-wrap checkbox-inline-wrap-tools">
|
|
|
|
| 1630 |
|
| 1631 |
{selection ? (
|
| 1632 |
<>
|
| 1633 |
+
<SectionBlock step="7" title="Estatísticas das Variáveis Selecionadas" subtitle="Resumo estatístico para Y e regressoras.">
|
| 1634 |
<DataTable table={selection.estatisticas} />
|
| 1635 |
</SectionBlock>
|
| 1636 |
|
| 1637 |
+
<SectionBlock step="8" title="Teste de Micronumerosidade" subtitle="Validação de amostra mínima para variáveis selecionadas.">
|
| 1638 |
<div dangerouslySetInnerHTML={{ __html: selection.micronumerosidade_html || '' }} />
|
| 1639 |
</SectionBlock>
|
| 1640 |
|
| 1641 |
+
<SectionBlock step="9" title="Gráficos de Dispersão das Variáveis Independentes" subtitle="Leitura visual entre X e Y no conjunto filtrado.">
|
| 1642 |
<details
|
| 1643 |
className="section-content-toggle"
|
| 1644 |
open={section8Open}
|
|
|
|
| 1656 |
</details>
|
| 1657 |
</SectionBlock>
|
| 1658 |
|
| 1659 |
+
<SectionBlock step="10" title="Transformações Sugeridas" subtitle="Busca automática de combinações por R² e enquadramento.">
|
| 1660 |
<div className="row">
|
| 1661 |
<label>Grau mínimo dos coeficientes</label>
|
| 1662 |
<select value={grauCoef} onChange={(e) => setGrauCoef(Number(e.target.value))}>
|
|
|
|
| 1713 |
)}
|
| 1714 |
</SectionBlock>
|
| 1715 |
|
| 1716 |
+
<SectionBlock step="11" title="Aplicação das Transformações" subtitle="Configuração manual para ajuste do modelo.">
|
| 1717 |
<div className="manual-transform-toggle">
|
| 1718 |
<button
|
| 1719 |
type="button"
|
|
|
|
| 1783 |
</div>
|
| 1784 |
</div>
|
| 1785 |
) : null}
|
| 1786 |
+
<div className="variavel-badge-line">
|
| 1787 |
+
<span className="variavel-badge-label">Período dados:</span>
|
| 1788 |
+
<span className="variavel-badge-value">{periodoDadosMercadoTexto}</span>
|
| 1789 |
+
</div>
|
| 1790 |
</div>
|
| 1791 |
</div>
|
| 1792 |
</div>
|
|
|
|
| 1797 |
|
| 1798 |
{fit ? (
|
| 1799 |
<>
|
| 1800 |
+
<SectionBlock step="12" title="Gráficos de Dispersão (Variáveis Transformadas)" subtitle="Dispersão com variáveis já transformadas.">
|
| 1801 |
<details
|
| 1802 |
className="section-content-toggle"
|
| 1803 |
open={section11Open}
|
|
|
|
| 1820 |
</details>
|
| 1821 |
</SectionBlock>
|
| 1822 |
|
| 1823 |
+
<SectionBlock step="13" title="Diagnóstico de Modelo" subtitle="Resumo diagnóstico e tabelas principais do ajuste.">
|
| 1824 |
<div dangerouslySetInnerHTML={{ __html: fit.diagnosticos_html || '' }} />
|
| 1825 |
<div className="two-col diagnostic-tables">
|
| 1826 |
<div className="pane">
|
|
|
|
| 1834 |
</div>
|
| 1835 |
</SectionBlock>
|
| 1836 |
|
| 1837 |
+
<SectionBlock step="14" title="Gráficos de Diagnóstico do Modelo" subtitle="Obs x calc, resíduos, histograma, Cook e correlação.">
|
| 1838 |
<div className="plot-grid-2-fixed">
|
| 1839 |
<PlotFigure figure={fit.grafico_obs_calc} title="Obs x Calc" />
|
| 1840 |
<PlotFigure figure={fit.grafico_residuos} title="Resíduos" />
|
|
|
|
| 1846 |
</div>
|
| 1847 |
</SectionBlock>
|
| 1848 |
|
| 1849 |
+
<SectionBlock step="15" title="Analisar Outliers" subtitle="Métricas para identificação de observações influentes.">
|
| 1850 |
<DataTable table={fit.tabela_metricas} maxHeight={320} />
|
| 1851 |
</SectionBlock>
|
| 1852 |
|
| 1853 |
+
<SectionBlock step="16" title="Exclusão ou Reinclusão de Outliers" subtitle="Filtre índices, revise e atualize o modelo.">
|
| 1854 |
{outliersAnteriores.length > 0 && outliersHtml ? (
|
| 1855 |
<div className="outliers-html-box" dangerouslySetInnerHTML={{ __html: outliersHtml }} />
|
| 1856 |
) : null}
|
|
|
|
| 1934 |
<div className="resumo-outliers-box">Outliers anteriores: {joinSelection(outliersAnteriores) || '-'}</div>
|
| 1935 |
</SectionBlock>
|
| 1936 |
|
| 1937 |
+
<SectionBlock step="17" title="Avaliação de Imóvel" subtitle="Cálculo individual e comparação entre avaliações.">
|
| 1938 |
<div className="avaliacao-grid" key={`avaliacao-grid-elab-${avaliacaoFormVersion}`}>
|
| 1939 |
{camposAvaliacao.map((campo) => (
|
| 1940 |
<div key={`aval-${campo.coluna}`} className="avaliacao-card">
|
|
|
|
| 1987 |
/>
|
| 1988 |
</SectionBlock>
|
| 1989 |
|
| 1990 |
+
<SectionBlock step="18" title="Exportar Modelo" subtitle="Geração do pacote .dai e download da base tratada.">
|
| 1991 |
<div className="row">
|
| 1992 |
<label>Nome do arquivo (.dai)</label>
|
| 1993 |
<input type="text" value={nomeArquivoExport} onChange={(e) => setNomeArquivoExport(e.target.value)} />
|
frontend/src/components/InicioTab.jsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
|
| 3 |
+
export default function InicioTab() {
|
| 4 |
+
return (
|
| 5 |
+
<div className="tab-content">
|
| 6 |
+
<section className="inicio-card">
|
| 7 |
+
<h3>Resumo rápido</h3>
|
| 8 |
+
<ul className="inicio-lista">
|
| 9 |
+
<li><strong>Pesquisa:</strong> encontra modelos compatíveis com os filtros informados.</li>
|
| 10 |
+
<li><strong>Elaboração/Edição:</strong> cria, ajusta e exporta modelos estatísticos.</li>
|
| 11 |
+
<li><strong>Visualização/Avaliação:</strong> abre modelos `.dai`, mostra diagnósticos e permite avaliação.</li>
|
| 12 |
+
</ul>
|
| 13 |
+
<p className="inicio-creditos">
|
| 14 |
+
Aplicativo criado por Guilherme Silberfarb Costa e David Schuch Bertoglio.
|
| 15 |
+
</p>
|
| 16 |
+
</section>
|
| 17 |
+
</div>
|
| 18 |
+
)
|
| 19 |
+
}
|
frontend/src/components/PesquisaAdminConfigPanel.jsx
CHANGED
|
@@ -1,76 +1,94 @@
|
|
| 1 |
import React, { useEffect, useMemo, useState } from 'react'
|
| 2 |
import { api } from '../api'
|
| 3 |
|
| 4 |
-
const
|
| 5 |
-
{
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
},
|
| 9 |
-
{
|
| 10 |
-
titulo: 'Otica do avaliando',
|
| 11 |
-
campos: [
|
| 12 |
-
'aval_finalidade',
|
| 13 |
-
'aval_bairro',
|
| 14 |
-
'aval_data',
|
| 15 |
-
'aval_area',
|
| 16 |
-
'aval_rh',
|
| 17 |
-
],
|
| 18 |
-
},
|
| 19 |
]
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
bairros: 'Bairro',
|
| 24 |
-
data: 'Data',
|
| 25 |
-
area: 'Area',
|
| 26 |
-
rh: 'RH',
|
| 27 |
-
aval_finalidade: 'Finalidade (avaliando)',
|
| 28 |
-
aval_bairro: 'Bairro (avaliando)',
|
| 29 |
-
aval_data: 'Data (avaliando)',
|
| 30 |
-
aval_area: 'Area (avaliando)',
|
| 31 |
-
aval_rh: 'RH (avaliando)',
|
| 32 |
}
|
| 33 |
|
| 34 |
function normalizeColunasConfig(rawConfig = {}) {
|
| 35 |
const out = {}
|
| 36 |
-
|
| 37 |
-
const
|
| 38 |
-
const vistos = new Set()
|
| 39 |
-
;(Array.isArray(config?.disponiveis) ? config.disponiveis : []).forEach((item) => {
|
| 40 |
-
const id = typeof item === 'string' ? item : item?.id
|
| 41 |
-
const label = typeof item === 'string' ? item : item?.label || item?.id
|
| 42 |
-
const idText = String(id || '').trim()
|
| 43 |
-
if (!idText || vistos.has(idText)) return
|
| 44 |
-
vistos.add(idText)
|
| 45 |
-
disponiveis.push({ id: idText, label: String(label || idText) })
|
| 46 |
-
})
|
| 47 |
const padrao = []
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
})
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
})
|
| 55 |
return out
|
| 56 |
}
|
| 57 |
|
| 58 |
function normalizeSelecionadas(raw = {}, configNormalizada = {}) {
|
| 59 |
const out = {}
|
| 60 |
-
|
| 61 |
-
const disponiveis = new Set((configNormalizada[
|
| 62 |
-
const preferidas =
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
| 64 |
if (validas.length) {
|
| 65 |
-
out[
|
| 66 |
return
|
| 67 |
}
|
| 68 |
-
const padrao = (configNormalizada[
|
| 69 |
-
out[
|
| 70 |
})
|
| 71 |
return out
|
| 72 |
}
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
function serializarCampos(campos = {}) {
|
| 75 |
const normalizado = {}
|
| 76 |
Object.keys(campos || {}).sort().forEach((campo) => {
|
|
@@ -145,7 +163,7 @@ export default function PesquisaAdminConfigPanel({ onSaved }) {
|
|
| 145 |
setError('')
|
| 146 |
setStatus('')
|
| 147 |
try {
|
| 148 |
-
const response = await api.pesquisaAdminConfigSalvar(selecionadas)
|
| 149 |
const configNormalizada = normalizeColunasConfig(response.colunas_filtro || {})
|
| 150 |
const selecionadasNormalizadas = normalizeSelecionadas(response.admin_fontes || {}, configNormalizada)
|
| 151 |
const base = serializarCampos(selecionadasNormalizadas)
|
|
@@ -161,7 +179,7 @@ export default function PesquisaAdminConfigPanel({ onSaved }) {
|
|
| 161 |
}
|
| 162 |
}
|
| 163 |
|
| 164 |
-
function renderCampo(campo) {
|
| 165 |
const configCampo = colunasConfig[campo] || { disponiveis: [], padrao: [] }
|
| 166 |
const selecionadasCampo = selecionadas[campo] || []
|
| 167 |
const selectedSet = new Set(selecionadasCampo)
|
|
@@ -170,7 +188,7 @@ export default function PesquisaAdminConfigPanel({ onSaved }) {
|
|
| 170 |
return (
|
| 171 |
<div key={campo} className="pesquisa-admin-field">
|
| 172 |
<div className="pesquisa-admin-field-head">
|
| 173 |
-
<strong>{
|
| 174 |
<button type="button" className="btn-pesquisa-expand" onClick={() => onRestaurarPadrao(campo)}>
|
| 175 |
Restaurar padrao
|
| 176 |
</button>
|
|
@@ -179,10 +197,10 @@ export default function PesquisaAdminConfigPanel({ onSaved }) {
|
|
| 179 |
<div className="pesquisa-dynamic-filter-row pesquisa-admin-row">
|
| 180 |
<div className="pesquisa-colunas-box">
|
| 181 |
<div className="pesquisa-colunas-chip-list">
|
| 182 |
-
{selecionadasCampo.map((
|
| 183 |
-
<span key={`${campo}-${
|
| 184 |
-
<span>{findLabel(campo,
|
| 185 |
-
<button type="button" className="pesquisa-coluna-remove" onClick={() => onRemove(campo,
|
| 186 |
x
|
| 187 |
</button>
|
| 188 |
</span>
|
|
@@ -225,14 +243,9 @@ export default function PesquisaAdminConfigPanel({ onSaved }) {
|
|
| 225 |
{status ? <div className="status-line">{status}</div> : null}
|
| 226 |
{error ? <div className="error-line inline-error">{error}</div> : null}
|
| 227 |
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
<div className="pesquisa-admin-fields">
|
| 232 |
-
{grupo.campos.map((campo) => renderCampo(campo))}
|
| 233 |
-
</div>
|
| 234 |
-
</section>
|
| 235 |
-
))}
|
| 236 |
</div>
|
| 237 |
)
|
| 238 |
}
|
|
|
|
| 1 |
import React, { useEffect, useMemo, useState } from 'react'
|
| 2 |
import { api } from '../api'
|
| 3 |
|
| 4 |
+
const CAMPOS_UNIFICADOS = [
|
| 5 |
+
{ id: 'finalidade', label: 'Finalidade', targets: ['aval_finalidade'] },
|
| 6 |
+
{ id: 'bairros', label: 'Bairro', targets: ['aval_bairro'] },
|
| 7 |
+
{ id: 'data', label: 'Data', targets: ['data'] },
|
| 8 |
+
{ id: 'area', label: 'Area', targets: ['aval_area'] },
|
| 9 |
+
{ id: 'rh', label: 'RH', targets: ['aval_rh'] },
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
]
|
| 11 |
|
| 12 |
+
function sortByLabel(items = []) {
|
| 13 |
+
return [...items].sort((a, b) => a.label.localeCompare(b.label, 'pt-BR', { sensitivity: 'base' }))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
}
|
| 15 |
|
| 16 |
function normalizeColunasConfig(rawConfig = {}) {
|
| 17 |
const out = {}
|
| 18 |
+
CAMPOS_UNIFICADOS.forEach(({ id, targets }) => {
|
| 19 |
+
const disponiveisMap = new Map()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
const padrao = []
|
| 21 |
+
const padraoVistos = new Set()
|
| 22 |
+
|
| 23 |
+
targets.forEach((target) => {
|
| 24 |
+
const config = rawConfig?.[target] || {}
|
| 25 |
+
;(Array.isArray(config?.disponiveis) ? config.disponiveis : []).forEach((item) => {
|
| 26 |
+
const itemId = typeof item === 'string' ? item : item?.id
|
| 27 |
+
const itemLabel = typeof item === 'string' ? item : item?.label || item?.id
|
| 28 |
+
const idText = String(itemId || '').trim()
|
| 29 |
+
if (!idText) return
|
| 30 |
+
if (!disponiveisMap.has(idText)) {
|
| 31 |
+
disponiveisMap.set(idText, String(itemLabel || idText))
|
| 32 |
+
}
|
| 33 |
+
})
|
| 34 |
+
})
|
| 35 |
+
|
| 36 |
+
const disponiveis = sortByLabel(Array.from(disponiveisMap.entries()).map(([itemId, label]) => ({ id: itemId, label })))
|
| 37 |
+
const idsDisponiveis = new Set(disponiveis.map((item) => item.id))
|
| 38 |
+
|
| 39 |
+
targets.forEach((target) => {
|
| 40 |
+
const config = rawConfig?.[target] || {}
|
| 41 |
+
;(Array.isArray(config?.padrao) ? config.padrao : []).forEach((item) => {
|
| 42 |
+
const idText = String(item || '').trim()
|
| 43 |
+
if (!idText || !idsDisponiveis.has(idText) || padraoVistos.has(idText)) return
|
| 44 |
+
padraoVistos.add(idText)
|
| 45 |
+
padrao.push(idText)
|
| 46 |
+
})
|
| 47 |
})
|
| 48 |
+
|
| 49 |
+
if (!padrao.length) {
|
| 50 |
+
disponiveis.forEach((item) => padrao.push(item.id))
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
out[id] = { disponiveis, padrao }
|
| 54 |
})
|
| 55 |
return out
|
| 56 |
}
|
| 57 |
|
| 58 |
function normalizeSelecionadas(raw = {}, configNormalizada = {}) {
|
| 59 |
const out = {}
|
| 60 |
+
CAMPOS_UNIFICADOS.forEach(({ id, targets }) => {
|
| 61 |
+
const disponiveis = new Set((configNormalizada[id]?.disponiveis || []).map((item) => item.id))
|
| 62 |
+
const preferidas = []
|
| 63 |
+
targets.forEach((target) => {
|
| 64 |
+
;(Array.isArray(raw?.[target]) ? raw[target] : []).forEach((item) => preferidas.push(String(item || '').trim()))
|
| 65 |
+
})
|
| 66 |
+
const validas = preferidas.filter((item, idx) => item && disponiveis.has(item) && preferidas.indexOf(item) === idx)
|
| 67 |
if (validas.length) {
|
| 68 |
+
out[id] = validas
|
| 69 |
return
|
| 70 |
}
|
| 71 |
+
const padrao = (configNormalizada[id]?.padrao || []).filter((item) => disponiveis.has(item))
|
| 72 |
+
out[id] = Array.from(new Set(padrao))
|
| 73 |
})
|
| 74 |
return out
|
| 75 |
}
|
| 76 |
|
| 77 |
+
function buildPayload(selecionadas = {}, colunasConfig = {}) {
|
| 78 |
+
const payload = {}
|
| 79 |
+
CAMPOS_UNIFICADOS.forEach(({ id, targets }) => {
|
| 80 |
+
const idsDisponiveis = new Set((colunasConfig[id]?.disponiveis || []).map((item) => item.id))
|
| 81 |
+
const selecionadasCampo = (selecionadas[id] || [])
|
| 82 |
+
.map((item) => String(item || '').trim())
|
| 83 |
+
.filter((item, idx, arr) => item && idsDisponiveis.has(item) && arr.indexOf(item) === idx)
|
| 84 |
+
|
| 85 |
+
targets.forEach((target) => {
|
| 86 |
+
payload[target] = [...selecionadasCampo]
|
| 87 |
+
})
|
| 88 |
+
})
|
| 89 |
+
return payload
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
function serializarCampos(campos = {}) {
|
| 93 |
const normalizado = {}
|
| 94 |
Object.keys(campos || {}).sort().forEach((campo) => {
|
|
|
|
| 163 |
setError('')
|
| 164 |
setStatus('')
|
| 165 |
try {
|
| 166 |
+
const response = await api.pesquisaAdminConfigSalvar(buildPayload(selecionadas, colunasConfig))
|
| 167 |
const configNormalizada = normalizeColunasConfig(response.colunas_filtro || {})
|
| 168 |
const selecionadasNormalizadas = normalizeSelecionadas(response.admin_fontes || {}, configNormalizada)
|
| 169 |
const base = serializarCampos(selecionadasNormalizadas)
|
|
|
|
| 179 |
}
|
| 180 |
}
|
| 181 |
|
| 182 |
+
function renderCampo(campo, label) {
|
| 183 |
const configCampo = colunasConfig[campo] || { disponiveis: [], padrao: [] }
|
| 184 |
const selecionadasCampo = selecionadas[campo] || []
|
| 185 |
const selectedSet = new Set(selecionadasCampo)
|
|
|
|
| 188 |
return (
|
| 189 |
<div key={campo} className="pesquisa-admin-field">
|
| 190 |
<div className="pesquisa-admin-field-head">
|
| 191 |
+
<strong>{label}</strong>
|
| 192 |
<button type="button" className="btn-pesquisa-expand" onClick={() => onRestaurarPadrao(campo)}>
|
| 193 |
Restaurar padrao
|
| 194 |
</button>
|
|
|
|
| 197 |
<div className="pesquisa-dynamic-filter-row pesquisa-admin-row">
|
| 198 |
<div className="pesquisa-colunas-box">
|
| 199 |
<div className="pesquisa-colunas-chip-list">
|
| 200 |
+
{selecionadasCampo.map((itemId) => (
|
| 201 |
+
<span key={`${campo}-${itemId}`} className="pesquisa-coluna-chip">
|
| 202 |
+
<span>{findLabel(campo, itemId)}</span>
|
| 203 |
+
<button type="button" className="pesquisa-coluna-remove" onClick={() => onRemove(campo, itemId)} aria-label={`Remover fonte ${findLabel(campo, itemId)}`}>
|
| 204 |
x
|
| 205 |
</button>
|
| 206 |
</span>
|
|
|
|
| 243 |
{status ? <div className="status-line">{status}</div> : null}
|
| 244 |
{error ? <div className="error-line inline-error">{error}</div> : null}
|
| 245 |
|
| 246 |
+
<div className="pesquisa-admin-fields">
|
| 247 |
+
{CAMPOS_UNIFICADOS.map((campo) => renderCampo(campo.id, campo.label))}
|
| 248 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
</div>
|
| 250 |
)
|
| 251 |
}
|
frontend/src/components/PesquisaTab.jsx
CHANGED
|
@@ -5,21 +5,12 @@ import PesquisaAdminConfigPanel from './PesquisaAdminConfigPanel'
|
|
| 5 |
import SectionBlock from './SectionBlock'
|
| 6 |
|
| 7 |
const EMPTY_FILTERS = {
|
| 8 |
-
otica: 'modelo',
|
| 9 |
-
nome: '',
|
| 10 |
-
autor: '',
|
| 11 |
contemApp: '',
|
| 12 |
-
|
| 13 |
-
bairros: '',
|
| 14 |
dataMin: '',
|
| 15 |
dataMax: '',
|
| 16 |
-
areaMin: '',
|
| 17 |
-
areaMax: '',
|
| 18 |
-
rhMin: '',
|
| 19 |
-
rhMax: '',
|
| 20 |
avalFinalidade: '',
|
| 21 |
avalBairro: '',
|
| 22 |
-
avalData: '',
|
| 23 |
avalArea: '',
|
| 24 |
avalRh: '',
|
| 25 |
}
|
|
@@ -31,48 +22,14 @@ const RESULT_INITIAL = {
|
|
| 31 |
total_geral: 0,
|
| 32 |
}
|
| 33 |
|
| 34 |
-
const CAMPOS_COLUNAS_FILTRO = [
|
| 35 |
-
'finalidade',
|
| 36 |
-
'bairros',
|
| 37 |
-
'data',
|
| 38 |
-
'area',
|
| 39 |
-
'rh',
|
| 40 |
-
'aval_finalidade',
|
| 41 |
-
'aval_bairro',
|
| 42 |
-
'aval_data',
|
| 43 |
-
'aval_area',
|
| 44 |
-
'aval_area_privativa',
|
| 45 |
-
'aval_area_total',
|
| 46 |
-
'aval_rh',
|
| 47 |
-
'aval_valor_unitario',
|
| 48 |
-
'aval_valor_total',
|
| 49 |
-
]
|
| 50 |
-
|
| 51 |
-
const COLUNAS_FILTRO_INITIAL = {
|
| 52 |
-
finalidade: [],
|
| 53 |
-
bairros: [],
|
| 54 |
-
data: [],
|
| 55 |
-
area: [],
|
| 56 |
-
rh: [],
|
| 57 |
-
aval_finalidade: [],
|
| 58 |
-
aval_bairro: [],
|
| 59 |
-
aval_data: [],
|
| 60 |
-
aval_area: [],
|
| 61 |
-
aval_area_privativa: [],
|
| 62 |
-
aval_area_total: [],
|
| 63 |
-
aval_rh: [],
|
| 64 |
-
aval_valor_unitario: [],
|
| 65 |
-
aval_valor_total: [],
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
const TIPO_SIGLAS = {
|
| 69 |
RECOND: 'Residencia em condominio',
|
| 70 |
RCOMD: 'Residencia em condominio',
|
| 71 |
TCOND: 'Terreno em condominio',
|
| 72 |
SALA: 'Salas comerciais',
|
| 73 |
-
APTO: 'Apartamentos
|
| 74 |
-
APART: 'Apartamentos
|
| 75 |
-
AP: 'Apartamentos
|
| 76 |
TERRENO: 'Terrenos',
|
| 77 |
TER: 'Terrenos',
|
| 78 |
EDIF: 'Edificio',
|
|
@@ -90,9 +47,9 @@ function formatRange(range) {
|
|
| 90 |
const min = range.min ?? null
|
| 91 |
const max = range.max ?? null
|
| 92 |
if (min === null && max === null) return '-'
|
| 93 |
-
if (min !== null && max !== null) return `${min} a ${max}`
|
| 94 |
-
if (min !== null) return `a partir de ${min}`
|
| 95 |
-
return `ate ${max}`
|
| 96 |
}
|
| 97 |
|
| 98 |
function formatCount(value) {
|
|
@@ -103,6 +60,14 @@ function formatCount(value) {
|
|
| 103 |
return String(value)
|
| 104 |
}
|
| 105 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
function normalizeTokenText(value) {
|
| 107 |
return String(value || '')
|
| 108 |
.normalize('NFD')
|
|
@@ -136,75 +101,17 @@ function formatTipoImovel(modelo) {
|
|
| 136 |
return mapped || text
|
| 137 |
}
|
| 138 |
|
| 139 |
-
function normalizeColunasConfig(rawConfig = {}) {
|
| 140 |
-
const out = {}
|
| 141 |
-
CAMPOS_COLUNAS_FILTRO.forEach((campo) => {
|
| 142 |
-
const config = rawConfig?.[campo] || {}
|
| 143 |
-
const disponiveis = []
|
| 144 |
-
const vistos = new Set()
|
| 145 |
-
;(Array.isArray(config.disponiveis) ? config.disponiveis : []).forEach((item) => {
|
| 146 |
-
const id = typeof item === 'string' ? item : item?.id
|
| 147 |
-
const label = typeof item === 'string' ? item : item?.label || item?.id
|
| 148 |
-
const idText = String(id || '').trim()
|
| 149 |
-
if (!idText || vistos.has(idText)) return
|
| 150 |
-
vistos.add(idText)
|
| 151 |
-
disponiveis.push({ id: idText, label: String(label || idText) })
|
| 152 |
-
})
|
| 153 |
-
|
| 154 |
-
const padrao = []
|
| 155 |
-
;(Array.isArray(config.padrao) ? config.padrao : []).forEach((item) => {
|
| 156 |
-
const idText = String(item || '').trim()
|
| 157 |
-
if (!idText || !vistos.has(idText) || padrao.includes(idText)) return
|
| 158 |
-
padrao.push(idText)
|
| 159 |
-
})
|
| 160 |
-
|
| 161 |
-
out[campo] = { disponiveis, padrao }
|
| 162 |
-
})
|
| 163 |
-
return out
|
| 164 |
-
}
|
| 165 |
-
|
| 166 |
-
function reconciliarColunasSelecionadas(atual, configNormalizada, camposEditados = {}) {
|
| 167 |
-
const next = { ...COLUNAS_FILTRO_INITIAL, ...atual }
|
| 168 |
-
CAMPOS_COLUNAS_FILTRO.forEach((campo) => {
|
| 169 |
-
const configCampo = configNormalizada[campo] || { disponiveis: [], padrao: [] }
|
| 170 |
-
const idsDisponiveis = new Set((configCampo.disponiveis || []).map((item) => item.id))
|
| 171 |
-
const selecionadasValidas = (next[campo] || []).filter((id) => idsDisponiveis.has(id))
|
| 172 |
-
if (camposEditados[campo]) {
|
| 173 |
-
next[campo] = selecionadasValidas
|
| 174 |
-
return
|
| 175 |
-
}
|
| 176 |
-
const padraoValido = (configCampo.padrao || []).filter((id) => idsDisponiveis.has(id))
|
| 177 |
-
next[campo] = padraoValido.length ? padraoValido : selecionadasValidas
|
| 178 |
-
})
|
| 179 |
-
return next
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
function buildApiFilters(filters) {
|
| 183 |
-
if (filters.otica === 'avaliando') {
|
| 184 |
-
return {
|
| 185 |
-
otica: filters.otica,
|
| 186 |
-
contem_app: filters.contemApp,
|
| 187 |
-
aval_finalidade: filters.avalFinalidade,
|
| 188 |
-
aval_bairro: filters.avalBairro,
|
| 189 |
-
aval_data: filters.avalData,
|
| 190 |
-
aval_area: filters.avalArea,
|
| 191 |
-
aval_rh: filters.avalRh,
|
| 192 |
-
}
|
| 193 |
-
}
|
| 194 |
-
|
| 195 |
return {
|
| 196 |
-
otica:
|
| 197 |
-
nome: filters.nome,
|
| 198 |
-
autor: filters.autor,
|
| 199 |
contem_app: filters.contemApp,
|
| 200 |
-
|
| 201 |
-
|
|
|
|
| 202 |
data_min: filters.dataMin,
|
| 203 |
data_max: filters.dataMax,
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
rh_min: filters.rhMin,
|
| 207 |
-
rh_max: filters.rhMax,
|
| 208 |
}
|
| 209 |
}
|
| 210 |
|
|
@@ -244,150 +151,15 @@ function NumberFieldInput({ field, ...props }) {
|
|
| 244 |
)
|
| 245 |
}
|
| 246 |
|
| 247 |
-
function
|
| 248 |
-
label,
|
| 249 |
-
campoValor,
|
| 250 |
-
campoColunas,
|
| 251 |
-
configCampo,
|
| 252 |
-
selecionadas,
|
| 253 |
-
onAddColuna,
|
| 254 |
-
onRemoveColuna,
|
| 255 |
-
value,
|
| 256 |
-
onChange,
|
| 257 |
-
list,
|
| 258 |
-
placeholder,
|
| 259 |
-
inputKind = 'text',
|
| 260 |
-
}) {
|
| 261 |
-
const disponiveis = configCampo?.disponiveis || []
|
| 262 |
-
const selectedSet = new Set(selecionadas || [])
|
| 263 |
-
const opcoesAdicionar = disponiveis.filter((item) => !selectedSet.has(item.id))
|
| 264 |
-
const InputComponent = inputKind === 'number' ? NumberFieldInput : TextFieldInput
|
| 265 |
-
|
| 266 |
-
function findLabel(id) {
|
| 267 |
-
const match = disponiveis.find((item) => item.id === id)
|
| 268 |
-
return match?.label || id
|
| 269 |
-
}
|
| 270 |
-
|
| 271 |
return (
|
| 272 |
-
<
|
| 273 |
-
{
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
<span>{findLabel(id)}</span>
|
| 280 |
-
<button type="button" className="pesquisa-coluna-remove" onClick={() => onRemoveColuna(campoColunas, id)} aria-label={`Remover coluna ${findLabel(id)}`}>
|
| 281 |
-
x
|
| 282 |
-
</button>
|
| 283 |
-
</span>
|
| 284 |
-
))}
|
| 285 |
-
{!(selecionadas || []).length ? <span className="pesquisa-colunas-empty">Nenhuma coluna selecionada.</span> : null}
|
| 286 |
-
</div>
|
| 287 |
-
</div>
|
| 288 |
-
|
| 289 |
-
<select
|
| 290 |
-
className="pesquisa-colunas-add"
|
| 291 |
-
defaultValue=""
|
| 292 |
-
onChange={(event) => {
|
| 293 |
-
const selected = String(event.target.value || '').trim()
|
| 294 |
-
if (!selected) return
|
| 295 |
-
onAddColuna(campoColunas, selected)
|
| 296 |
-
event.target.value = ''
|
| 297 |
-
}}
|
| 298 |
-
>
|
| 299 |
-
<option value="">Adicionar coluna...</option>
|
| 300 |
-
{opcoesAdicionar.map((item) => (
|
| 301 |
-
<option key={`${campoColunas}-opt-${item.id}`} value={item.id}>{item.label}</option>
|
| 302 |
-
))}
|
| 303 |
-
</select>
|
| 304 |
-
|
| 305 |
-
<InputComponent
|
| 306 |
-
list={list}
|
| 307 |
-
field={campoValor}
|
| 308 |
-
value={value}
|
| 309 |
-
onChange={onChange}
|
| 310 |
-
placeholder={placeholder}
|
| 311 |
-
/>
|
| 312 |
-
</div>
|
| 313 |
-
</label>
|
| 314 |
-
)
|
| 315 |
-
}
|
| 316 |
-
|
| 317 |
-
function DynamicRangeFilterField({
|
| 318 |
-
label,
|
| 319 |
-
campoColunas,
|
| 320 |
-
configCampo,
|
| 321 |
-
selecionadas,
|
| 322 |
-
onAddColuna,
|
| 323 |
-
onRemoveColuna,
|
| 324 |
-
minLabel,
|
| 325 |
-
minField,
|
| 326 |
-
minValue,
|
| 327 |
-
maxLabel,
|
| 328 |
-
maxField,
|
| 329 |
-
maxValue,
|
| 330 |
-
onChange,
|
| 331 |
-
minPlaceholder,
|
| 332 |
-
maxPlaceholder,
|
| 333 |
-
inputKind = 'number',
|
| 334 |
-
}) {
|
| 335 |
-
const disponiveis = configCampo?.disponiveis || []
|
| 336 |
-
const selectedSet = new Set(selecionadas || [])
|
| 337 |
-
const opcoesAdicionar = disponiveis.filter((item) => !selectedSet.has(item.id))
|
| 338 |
-
const InputComponent = inputKind === 'number' ? NumberFieldInput : TextFieldInput
|
| 339 |
-
|
| 340 |
-
function findLabel(id) {
|
| 341 |
-
const match = disponiveis.find((item) => item.id === id)
|
| 342 |
-
return match?.label || id
|
| 343 |
-
}
|
| 344 |
-
|
| 345 |
-
return (
|
| 346 |
-
<div className="pesquisa-field pesquisa-field-wide">
|
| 347 |
-
{label ? <span>{label}</span> : null}
|
| 348 |
-
<div className="pesquisa-dynamic-filter-row pesquisa-dynamic-filter-row-range">
|
| 349 |
-
<div className="pesquisa-colunas-box">
|
| 350 |
-
<div className="pesquisa-colunas-chip-list">
|
| 351 |
-
{(selecionadas || []).map((id) => (
|
| 352 |
-
<span key={`${campoColunas}-${id}`} className="pesquisa-coluna-chip">
|
| 353 |
-
<span>{findLabel(id)}</span>
|
| 354 |
-
<button type="button" className="pesquisa-coluna-remove" onClick={() => onRemoveColuna(campoColunas, id)} aria-label={`Remover coluna ${findLabel(id)}`}>
|
| 355 |
-
x
|
| 356 |
-
</button>
|
| 357 |
-
</span>
|
| 358 |
-
))}
|
| 359 |
-
{!(selecionadas || []).length ? <span className="pesquisa-colunas-empty">Nenhuma coluna selecionada.</span> : null}
|
| 360 |
-
</div>
|
| 361 |
-
</div>
|
| 362 |
-
|
| 363 |
-
<select
|
| 364 |
-
className="pesquisa-colunas-add"
|
| 365 |
-
defaultValue=""
|
| 366 |
-
onChange={(event) => {
|
| 367 |
-
const selected = String(event.target.value || '').trim()
|
| 368 |
-
if (!selected) return
|
| 369 |
-
onAddColuna(campoColunas, selected)
|
| 370 |
-
event.target.value = ''
|
| 371 |
-
}}
|
| 372 |
-
>
|
| 373 |
-
<option value="">Adicionar coluna...</option>
|
| 374 |
-
{opcoesAdicionar.map((item) => (
|
| 375 |
-
<option key={`${campoColunas}-opt-${item.id}`} value={item.id}>{item.label}</option>
|
| 376 |
-
))}
|
| 377 |
-
</select>
|
| 378 |
-
</div>
|
| 379 |
-
|
| 380 |
-
<div className="pesquisa-range-values-row">
|
| 381 |
-
<label className="pesquisa-field">
|
| 382 |
-
{minLabel}
|
| 383 |
-
<InputComponent field={minField} value={minValue} onChange={onChange} placeholder={minPlaceholder} />
|
| 384 |
-
</label>
|
| 385 |
-
<label className="pesquisa-field">
|
| 386 |
-
{maxLabel}
|
| 387 |
-
<InputComponent field={maxField} value={maxValue} onChange={onChange} placeholder={maxPlaceholder} />
|
| 388 |
-
</label>
|
| 389 |
-
</div>
|
| 390 |
-
</div>
|
| 391 |
)
|
| 392 |
}
|
| 393 |
|
|
@@ -403,9 +175,6 @@ export default function PesquisaTab() {
|
|
| 403 |
const [selectedIds, setSelectedIds] = useState([])
|
| 404 |
const [detailModelId, setDetailModelId] = useState('')
|
| 405 |
const selectAllRef = useRef(null)
|
| 406 |
-
const [colunasConfig, setColunasConfig] = useState({})
|
| 407 |
-
const [colunasFiltro, setColunasFiltro] = useState(COLUNAS_FILTRO_INITIAL)
|
| 408 |
-
const [colunasEditadas, setColunasEditadas] = useState({})
|
| 409 |
|
| 410 |
const [mapaLoading, setMapaLoading] = useState(false)
|
| 411 |
const [mapaError, setMapaError] = useState('')
|
|
@@ -413,21 +182,24 @@ export default function PesquisaTab() {
|
|
| 413 |
const [mapaHtml, setMapaHtml] = useState('')
|
| 414 |
const [mapaLegendas, setMapaLegendas] = useState([])
|
| 415 |
|
| 416 |
-
const usandoOticaAvaliando = filters.otica === 'avaliando'
|
| 417 |
const sugestoes = result.sugestoes || {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
const resultIds = useMemo(() => (result.modelos || []).map((modelo) => modelo.id), [result.modelos])
|
| 419 |
const detalheModelo = useMemo(() => (result.modelos || []).find((modelo) => modelo.id === detailModelId) || null, [result.modelos, detailModelId])
|
| 420 |
const todosSelecionados = resultIds.length > 0 && resultIds.every((id) => selectedIds.includes(id))
|
| 421 |
const algunsSelecionados = resultIds.some((id) => selectedIds.includes(id))
|
| 422 |
|
| 423 |
-
async function buscarModelos(nextFilters = filters
|
| 424 |
setLoading(true)
|
| 425 |
setError('')
|
| 426 |
try {
|
| 427 |
-
const response = await api.pesquisarModelos(buildApiFilters(nextFilters
|
| 428 |
const modelos = response.modelos || []
|
| 429 |
const idsNovos = new Set(modelos.map((item) => item.id))
|
| 430 |
-
const configNormalizada = normalizeColunasConfig(response.colunas_filtro || {})
|
| 431 |
|
| 432 |
setResult({
|
| 433 |
...RESULT_INITIAL,
|
|
@@ -435,8 +207,6 @@ export default function PesquisaTab() {
|
|
| 435 |
modelos,
|
| 436 |
sugestoes: response.sugestoes || {},
|
| 437 |
})
|
| 438 |
-
setColunasConfig(configNormalizada)
|
| 439 |
-
setColunasFiltro((current) => reconciliarColunasSelecionadas(current, configNormalizada, nextColunasEditadas))
|
| 440 |
|
| 441 |
setSelectedIds((current) => current.filter((id) => idsNovos.has(id)))
|
| 442 |
|
|
@@ -457,7 +227,6 @@ export default function PesquisaTab() {
|
|
| 457 |
setError('')
|
| 458 |
try {
|
| 459 |
const response = await api.pesquisarModelos({ somente_contexto: true })
|
| 460 |
-
const configNormalizada = normalizeColunasConfig(response.colunas_filtro || {})
|
| 461 |
|
| 462 |
setResult({
|
| 463 |
...RESULT_INITIAL,
|
|
@@ -465,8 +234,6 @@ export default function PesquisaTab() {
|
|
| 465 |
modelos: [],
|
| 466 |
sugestoes: response.sugestoes || {},
|
| 467 |
})
|
| 468 |
-
setColunasConfig(configNormalizada)
|
| 469 |
-
setColunasFiltro((current) => reconciliarColunasSelecionadas(current, configNormalizada, {}))
|
| 470 |
setSelectedIds([])
|
| 471 |
setMapaHtml('')
|
| 472 |
setMapaStatus('')
|
|
@@ -514,15 +281,9 @@ export default function PesquisaTab() {
|
|
| 514 |
setFilters((prev) => ({ ...prev, [field]: value }))
|
| 515 |
}
|
| 516 |
|
| 517 |
-
function onChangeOtica(otica) {
|
| 518 |
-
setFilters((prev) => ({ ...prev, otica }))
|
| 519 |
-
}
|
| 520 |
-
|
| 521 |
async function onLimparFiltros() {
|
| 522 |
setFilters(EMPTY_FILTERS)
|
| 523 |
-
|
| 524 |
-
setColunasFiltro(COLUNAS_FILTRO_INITIAL)
|
| 525 |
-
await buscarModelos(EMPTY_FILTERS, COLUNAS_FILTRO_INITIAL, {})
|
| 526 |
}
|
| 527 |
|
| 528 |
function onToggleSelecionado(modelId) {
|
|
@@ -554,23 +315,6 @@ export default function PesquisaTab() {
|
|
| 554 |
setDetailModelId('')
|
| 555 |
}
|
| 556 |
|
| 557 |
-
function onAddColunaFiltro(campo, colunaId) {
|
| 558 |
-
setColunasFiltro((current) => {
|
| 559 |
-
const atual = current[campo] || []
|
| 560 |
-
if (atual.includes(colunaId)) return current
|
| 561 |
-
return { ...current, [campo]: [...atual, colunaId] }
|
| 562 |
-
})
|
| 563 |
-
setColunasEditadas((current) => ({ ...current, [campo]: true }))
|
| 564 |
-
}
|
| 565 |
-
|
| 566 |
-
function onRemoveColunaFiltro(campo, colunaId) {
|
| 567 |
-
setColunasFiltro((current) => ({
|
| 568 |
-
...current,
|
| 569 |
-
[campo]: (current[campo] || []).filter((item) => item !== colunaId),
|
| 570 |
-
}))
|
| 571 |
-
setColunasEditadas((current) => ({ ...current, [campo]: true }))
|
| 572 |
-
}
|
| 573 |
-
|
| 574 |
async function onGerarMapaSelecionados() {
|
| 575 |
if (!selectedIds.length) {
|
| 576 |
setMapaError('Selecione ao menos um modelo para plotar no mapa.')
|
|
@@ -604,7 +348,7 @@ export default function PesquisaTab() {
|
|
| 604 |
<SectionBlock
|
| 605 |
step="1"
|
| 606 |
title="Filtros de Pesquisa"
|
| 607 |
-
subtitle="
|
| 608 |
aside={(
|
| 609 |
<button
|
| 610 |
type="button"
|
|
@@ -620,34 +364,7 @@ export default function PesquisaTab() {
|
|
| 620 |
<PesquisaAdminConfigPanel onSaved={() => void onAdminConfigSalva()} />
|
| 621 |
) : null}
|
| 622 |
|
| 623 |
-
<div className="pesquisa-
|
| 624 |
-
<button
|
| 625 |
-
type="button"
|
| 626 |
-
className={`pesquisa-otica-btn${!usandoOticaAvaliando ? ' active' : ''}`}
|
| 627 |
-
role="tab"
|
| 628 |
-
id="pesquisa-otica-modelo"
|
| 629 |
-
aria-selected={!usandoOticaAvaliando}
|
| 630 |
-
aria-controls="pesquisa-panel-modelo"
|
| 631 |
-
tabIndex={!usandoOticaAvaliando ? 0 : -1}
|
| 632 |
-
onClick={() => onChangeOtica('modelo')}
|
| 633 |
-
>
|
| 634 |
-
Otica do modelo
|
| 635 |
-
</button>
|
| 636 |
-
<button
|
| 637 |
-
type="button"
|
| 638 |
-
className={`pesquisa-otica-btn${usandoOticaAvaliando ? ' active' : ''}`}
|
| 639 |
-
role="tab"
|
| 640 |
-
id="pesquisa-otica-avaliando"
|
| 641 |
-
aria-selected={usandoOticaAvaliando}
|
| 642 |
-
aria-controls="pesquisa-panel-avaliando"
|
| 643 |
-
tabIndex={usandoOticaAvaliando ? 0 : -1}
|
| 644 |
-
onClick={() => onChangeOtica('avaliando')}
|
| 645 |
-
>
|
| 646 |
-
Otica do avaliando
|
| 647 |
-
</button>
|
| 648 |
-
</div>
|
| 649 |
-
|
| 650 |
-
<div className="pesquisa-fields-grid pesquisa-fields-grid-single">
|
| 651 |
<label className="pesquisa-field">
|
| 652 |
Contem variavel APP (% APP)
|
| 653 |
<select
|
|
@@ -662,147 +379,69 @@ export default function PesquisaTab() {
|
|
| 662 |
<option value="nao">Nao</option>
|
| 663 |
</select>
|
| 664 |
</label>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 665 |
</div>
|
| 666 |
|
| 667 |
-
|
| 668 |
-
<
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
field="avalFinalidade"
|
| 679 |
-
value={filters.avalFinalidade}
|
| 680 |
-
onChange={onFieldChange}
|
| 681 |
-
placeholder="Ex: Apartamento"
|
| 682 |
-
/>
|
| 683 |
-
</label>
|
| 684 |
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
<TextFieldInput field="avalData" value={filters.avalData} onChange={onFieldChange} placeholder="2025-02-27" />
|
| 700 |
-
</label>
|
| 701 |
<label className="pesquisa-field">
|
| 702 |
-
|
| 703 |
-
<
|
| 704 |
</label>
|
| 705 |
<label className="pesquisa-field">
|
| 706 |
-
|
| 707 |
-
<
|
| 708 |
</label>
|
| 709 |
</div>
|
| 710 |
-
</div>
|
| 711 |
-
) : (
|
| 712 |
-
<div id="pesquisa-panel-modelo" role="tabpanel" aria-labelledby="pesquisa-otica-modelo" className="pesquisa-fields-grid">
|
| 713 |
<label className="pesquisa-field">
|
| 714 |
-
|
| 715 |
-
<
|
| 716 |
-
list="pesquisa-nomes-modelo"
|
| 717 |
-
field="nome"
|
| 718 |
-
value={filters.nome}
|
| 719 |
-
onChange={onFieldChange}
|
| 720 |
-
placeholder="Ex: MOD_A_SALA_Z1"
|
| 721 |
-
/>
|
| 722 |
</label>
|
| 723 |
<label className="pesquisa-field">
|
| 724 |
-
|
| 725 |
-
<
|
| 726 |
-
list="pesquisa-autores"
|
| 727 |
-
field="autor"
|
| 728 |
-
value={filters.autor}
|
| 729 |
-
onChange={onFieldChange}
|
| 730 |
-
placeholder="Nome do avaliador"
|
| 731 |
-
/>
|
| 732 |
</label>
|
| 733 |
-
|
| 734 |
-
<label className="pesquisa-field">
|
| 735 |
-
Finalidade
|
| 736 |
-
<TextFieldInput
|
| 737 |
-
list="pesquisa-finalidades"
|
| 738 |
-
field="finalidade"
|
| 739 |
-
value={filters.finalidade}
|
| 740 |
-
onChange={onFieldChange}
|
| 741 |
-
placeholder="Apartamento, sala, deposito..."
|
| 742 |
-
/>
|
| 743 |
-
</label>
|
| 744 |
-
|
| 745 |
-
<label className="pesquisa-field">
|
| 746 |
-
Bairro
|
| 747 |
-
<TextFieldInput
|
| 748 |
-
list="pesquisa-bairros"
|
| 749 |
-
field="bairros"
|
| 750 |
-
value={filters.bairros}
|
| 751 |
-
onChange={onFieldChange}
|
| 752 |
-
placeholder="Centro, Moinhos de Vento"
|
| 753 |
-
/>
|
| 754 |
-
</label>
|
| 755 |
-
|
| 756 |
-
<div className="pesquisa-inline-trio">
|
| 757 |
-
<div className="pesquisa-field-pair pesquisa-field-pair-inline">
|
| 758 |
-
<span className="pesquisa-field-pair-title">Data</span>
|
| 759 |
-
<label className="pesquisa-field">
|
| 760 |
-
Minima
|
| 761 |
-
<TextFieldInput field="dataMin" value={filters.dataMin} onChange={onFieldChange} placeholder="2022-01-01" />
|
| 762 |
-
</label>
|
| 763 |
-
<label className="pesquisa-field">
|
| 764 |
-
Maxima
|
| 765 |
-
<TextFieldInput field="dataMax" value={filters.dataMax} onChange={onFieldChange} placeholder="2025-12-31" />
|
| 766 |
-
</label>
|
| 767 |
-
</div>
|
| 768 |
-
|
| 769 |
-
<div className="pesquisa-field-pair pesquisa-field-pair-inline">
|
| 770 |
-
<span className="pesquisa-field-pair-title">Area</span>
|
| 771 |
-
<label className="pesquisa-field">
|
| 772 |
-
Minima
|
| 773 |
-
<NumberFieldInput field="areaMin" value={filters.areaMin} onChange={onFieldChange} placeholder="0" />
|
| 774 |
-
</label>
|
| 775 |
-
<label className="pesquisa-field">
|
| 776 |
-
Maxima
|
| 777 |
-
<NumberFieldInput field="areaMax" value={filters.areaMax} onChange={onFieldChange} placeholder="0" />
|
| 778 |
-
</label>
|
| 779 |
-
</div>
|
| 780 |
-
|
| 781 |
-
<div className="pesquisa-field-pair pesquisa-field-pair-inline">
|
| 782 |
-
<span className="pesquisa-field-pair-title">RH</span>
|
| 783 |
-
<label className="pesquisa-field">
|
| 784 |
-
Minimo
|
| 785 |
-
<NumberFieldInput field="rhMin" value={filters.rhMin} onChange={onFieldChange} placeholder="0" />
|
| 786 |
-
</label>
|
| 787 |
-
<label className="pesquisa-field">
|
| 788 |
-
Maximo
|
| 789 |
-
<NumberFieldInput field="rhMax" value={filters.rhMax} onChange={onFieldChange} placeholder="1" />
|
| 790 |
-
</label>
|
| 791 |
-
</div>
|
| 792 |
-
</div>
|
| 793 |
</div>
|
| 794 |
-
|
| 795 |
|
| 796 |
-
<datalist id="pesquisa-nomes-modelo">
|
| 797 |
-
{(sugestoes.nomes_modelo || []).map((item) => (
|
| 798 |
-
<option key={`nome-${item}`} value={item} />
|
| 799 |
-
))}
|
| 800 |
-
</datalist>
|
| 801 |
-
<datalist id="pesquisa-autores">
|
| 802 |
-
{(sugestoes.autores || []).map((item) => (
|
| 803 |
-
<option key={`autor-${item}`} value={item} />
|
| 804 |
-
))}
|
| 805 |
-
</datalist>
|
| 806 |
<datalist id="pesquisa-finalidades">
|
| 807 |
{(sugestoes.finalidades || []).map((item) => (
|
| 808 |
<option key={`finalidade-${item}`} value={item} />
|
|
@@ -829,16 +468,12 @@ export default function PesquisaTab() {
|
|
| 829 |
<SectionBlock
|
| 830 |
step="2"
|
| 831 |
title="Resultados"
|
| 832 |
-
subtitle=
|
| 833 |
-
usandoOticaAvaliando
|
| 834 |
-
? 'Modelos aceitos para os parametros do avaliando informado.'
|
| 835 |
-
: 'Lista de modelos encontrados para os filtros atuais.'
|
| 836 |
-
}
|
| 837 |
>
|
| 838 |
<div className="pesquisa-results-toolbar">
|
| 839 |
<div className="pesquisa-summary-line">
|
| 840 |
<strong>{formatCount(result.total_filtrado)}</strong>{' '}
|
| 841 |
-
|
| 842 |
</div>
|
| 843 |
{resultIds.length ? (
|
| 844 |
<label className="pesquisa-select-all">
|
|
@@ -852,9 +487,7 @@ export default function PesquisaTab() {
|
|
| 852 |
<div className="empty-box">
|
| 853 |
{!pesquisaInicializada
|
| 854 |
? 'Defina os filtros desejados e clique em Pesquisar.'
|
| 855 |
-
:
|
| 856 |
-
? 'Nenhum modelo aceitou os parametros do avaliando informado.'
|
| 857 |
-
: 'Nenhum modelo encontrado com os filtros atuais.'}
|
| 858 |
</div>
|
| 859 |
) : (
|
| 860 |
<div className="pesquisa-card-grid">
|
|
@@ -866,7 +499,6 @@ export default function PesquisaTab() {
|
|
| 866 |
<div className="pesquisa-card-head">
|
| 867 |
<div className="pesquisa-card-head-main">
|
| 868 |
<h4>{modelo.nome_modelo || modelo.arquivo}</h4>
|
| 869 |
-
<p>{modelo.arquivo}</p>
|
| 870 |
<div className="pesquisa-card-head-actions">
|
| 871 |
<label className="pesquisa-select-toggle">
|
| 872 |
<input
|
|
@@ -883,11 +515,9 @@ export default function PesquisaTab() {
|
|
| 883 |
</div>
|
| 884 |
</div>
|
| 885 |
|
| 886 |
-
|
| 887 |
-
<div className="
|
| 888 |
-
|
| 889 |
-
</div>
|
| 890 |
-
) : null}
|
| 891 |
<div className="pesquisa-card-body">
|
| 892 |
<div className="pesquisa-card-dados-list">
|
| 893 |
<div><strong>Finalidades no modelo:</strong> {(modelo.finalidades || []).length ? modelo.finalidades.join(', ') : '-'}</div>
|
|
|
|
| 5 |
import SectionBlock from './SectionBlock'
|
| 6 |
|
| 7 |
const EMPTY_FILTERS = {
|
|
|
|
|
|
|
|
|
|
| 8 |
contemApp: '',
|
| 9 |
+
tipoModelo: '',
|
|
|
|
| 10 |
dataMin: '',
|
| 11 |
dataMax: '',
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
avalFinalidade: '',
|
| 13 |
avalBairro: '',
|
|
|
|
| 14 |
avalArea: '',
|
| 15 |
avalRh: '',
|
| 16 |
}
|
|
|
|
| 22 |
total_geral: 0,
|
| 23 |
}
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
const TIPO_SIGLAS = {
|
| 26 |
RECOND: 'Residencia em condominio',
|
| 27 |
RCOMD: 'Residencia em condominio',
|
| 28 |
TCOND: 'Terreno em condominio',
|
| 29 |
SALA: 'Salas comerciais',
|
| 30 |
+
APTO: 'Apartamentos',
|
| 31 |
+
APART: 'Apartamentos',
|
| 32 |
+
AP: 'Apartamentos',
|
| 33 |
TERRENO: 'Terrenos',
|
| 34 |
TER: 'Terrenos',
|
| 35 |
EDIF: 'Edificio',
|
|
|
|
| 47 |
const min = range.min ?? null
|
| 48 |
const max = range.max ?? null
|
| 49 |
if (min === null && max === null) return '-'
|
| 50 |
+
if (min !== null && max !== null) return `${formatDateBrIfIso(min)} a ${formatDateBrIfIso(max)}`
|
| 51 |
+
if (min !== null) return `a partir de ${formatDateBrIfIso(min)}`
|
| 52 |
+
return `ate ${formatDateBrIfIso(max)}`
|
| 53 |
}
|
| 54 |
|
| 55 |
function formatCount(value) {
|
|
|
|
| 60 |
return String(value)
|
| 61 |
}
|
| 62 |
|
| 63 |
+
function formatDateBrIfIso(value) {
|
| 64 |
+
const text = String(value ?? '').trim()
|
| 65 |
+
if (!text) return '-'
|
| 66 |
+
const isoMatch = text.match(/^(\d{4})-(\d{2})-(\d{2})$/)
|
| 67 |
+
if (!isoMatch) return text
|
| 68 |
+
return `${isoMatch[3]}/${isoMatch[2]}/${isoMatch[1]}`
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
function normalizeTokenText(value) {
|
| 72 |
return String(value || '')
|
| 73 |
.normalize('NFD')
|
|
|
|
| 101 |
return mapped || text
|
| 102 |
}
|
| 103 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
function buildApiFilters(filters) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
return {
|
| 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,
|
| 112 |
data_max: filters.dataMax,
|
| 113 |
+
aval_area: filters.avalArea,
|
| 114 |
+
aval_rh: filters.avalRh,
|
|
|
|
|
|
|
| 115 |
}
|
| 116 |
}
|
| 117 |
|
|
|
|
| 151 |
)
|
| 152 |
}
|
| 153 |
|
| 154 |
+
function DateFieldInput({ field, ...props }) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
return (
|
| 156 |
+
<input
|
| 157 |
+
{...props}
|
| 158 |
+
type="date"
|
| 159 |
+
data-field={field}
|
| 160 |
+
name={toInputName(field)}
|
| 161 |
+
autoComplete="off"
|
| 162 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
)
|
| 164 |
}
|
| 165 |
|
|
|
|
| 175 |
const [selectedIds, setSelectedIds] = useState([])
|
| 176 |
const [detailModelId, setDetailModelId] = useState('')
|
| 177 |
const selectAllRef = useRef(null)
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
const [mapaLoading, setMapaLoading] = useState(false)
|
| 180 |
const [mapaError, setMapaError] = useState('')
|
|
|
|
| 182 |
const [mapaHtml, setMapaHtml] = useState('')
|
| 183 |
const [mapaLegendas, setMapaLegendas] = useState([])
|
| 184 |
|
|
|
|
| 185 |
const sugestoes = result.sugestoes || {}
|
| 186 |
+
const opcoesTipoModelo = useMemo(
|
| 187 |
+
() => [...new Set((sugestoes.tipos_modelo || []).map((item) => String(item || '').trim()).filter(Boolean))]
|
| 188 |
+
.sort((a, b) => a.localeCompare(b, 'pt-BR', { sensitivity: 'base' })),
|
| 189 |
+
[sugestoes.tipos_modelo],
|
| 190 |
+
)
|
| 191 |
const resultIds = useMemo(() => (result.modelos || []).map((modelo) => modelo.id), [result.modelos])
|
| 192 |
const detalheModelo = useMemo(() => (result.modelos || []).find((modelo) => modelo.id === detailModelId) || null, [result.modelos, detailModelId])
|
| 193 |
const todosSelecionados = resultIds.length > 0 && resultIds.every((id) => selectedIds.includes(id))
|
| 194 |
const algunsSelecionados = resultIds.some((id) => selectedIds.includes(id))
|
| 195 |
|
| 196 |
+
async function buscarModelos(nextFilters = filters) {
|
| 197 |
setLoading(true)
|
| 198 |
setError('')
|
| 199 |
try {
|
| 200 |
+
const response = await api.pesquisarModelos(buildApiFilters(nextFilters))
|
| 201 |
const modelos = response.modelos || []
|
| 202 |
const idsNovos = new Set(modelos.map((item) => item.id))
|
|
|
|
| 203 |
|
| 204 |
setResult({
|
| 205 |
...RESULT_INITIAL,
|
|
|
|
| 207 |
modelos,
|
| 208 |
sugestoes: response.sugestoes || {},
|
| 209 |
})
|
|
|
|
|
|
|
| 210 |
|
| 211 |
setSelectedIds((current) => current.filter((id) => idsNovos.has(id)))
|
| 212 |
|
|
|
|
| 227 |
setError('')
|
| 228 |
try {
|
| 229 |
const response = await api.pesquisarModelos({ somente_contexto: true })
|
|
|
|
| 230 |
|
| 231 |
setResult({
|
| 232 |
...RESULT_INITIAL,
|
|
|
|
| 234 |
modelos: [],
|
| 235 |
sugestoes: response.sugestoes || {},
|
| 236 |
})
|
|
|
|
|
|
|
| 237 |
setSelectedIds([])
|
| 238 |
setMapaHtml('')
|
| 239 |
setMapaStatus('')
|
|
|
|
| 281 |
setFilters((prev) => ({ ...prev, [field]: value }))
|
| 282 |
}
|
| 283 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
async function onLimparFiltros() {
|
| 285 |
setFilters(EMPTY_FILTERS)
|
| 286 |
+
await carregarContextoInicial()
|
|
|
|
|
|
|
| 287 |
}
|
| 288 |
|
| 289 |
function onToggleSelecionado(modelId) {
|
|
|
|
| 315 |
setDetailModelId('')
|
| 316 |
}
|
| 317 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
async function onGerarMapaSelecionados() {
|
| 319 |
if (!selectedIds.length) {
|
| 320 |
setMapaError('Selecione ao menos um modelo para plotar no mapa.')
|
|
|
|
| 348 |
<SectionBlock
|
| 349 |
step="1"
|
| 350 |
title="Filtros de Pesquisa"
|
| 351 |
+
subtitle="Informe os dados do avaliando. Todos os filtros sao cumulativos."
|
| 352 |
aside={(
|
| 353 |
<button
|
| 354 |
type="button"
|
|
|
|
| 364 |
<PesquisaAdminConfigPanel onSaved={() => void onAdminConfigSalva()} />
|
| 365 |
) : null}
|
| 366 |
|
| 367 |
+
<div className="pesquisa-fields-grid">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
<label className="pesquisa-field">
|
| 369 |
Contem variavel APP (% APP)
|
| 370 |
<select
|
|
|
|
| 379 |
<option value="nao">Nao</option>
|
| 380 |
</select>
|
| 381 |
</label>
|
| 382 |
+
<label className="pesquisa-field">
|
| 383 |
+
Tipo do modelo
|
| 384 |
+
<select
|
| 385 |
+
data-field="tipoModelo"
|
| 386 |
+
name={toInputName('tipoModelo')}
|
| 387 |
+
value={filters.tipoModelo}
|
| 388 |
+
onChange={onFieldChange}
|
| 389 |
+
autoComplete="off"
|
| 390 |
+
>
|
| 391 |
+
<option value="">Todos</option>
|
| 392 |
+
{opcoesTipoModelo.map((tipo) => (
|
| 393 |
+
<option key={`tipo-modelo-${tipo}`} value={tipo}>{tipo}</option>
|
| 394 |
+
))}
|
| 395 |
+
</select>
|
| 396 |
+
</label>
|
| 397 |
</div>
|
| 398 |
|
| 399 |
+
<div className="pesquisa-fields-grid pesquisa-avaliando-grid">
|
| 400 |
+
<label className="pesquisa-field">
|
| 401 |
+
Finalidade do imovel
|
| 402 |
+
<TextFieldInput
|
| 403 |
+
list="pesquisa-finalidades"
|
| 404 |
+
field="avalFinalidade"
|
| 405 |
+
value={filters.avalFinalidade}
|
| 406 |
+
onChange={onFieldChange}
|
| 407 |
+
placeholder="Ex: Apartamento"
|
| 408 |
+
/>
|
| 409 |
+
</label>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
|
| 411 |
+
<label className="pesquisa-field">
|
| 412 |
+
Bairro do imovel
|
| 413 |
+
<TextFieldInput
|
| 414 |
+
list="pesquisa-bairros"
|
| 415 |
+
field="avalBairro"
|
| 416 |
+
value={filters.avalBairro}
|
| 417 |
+
onChange={onFieldChange}
|
| 418 |
+
placeholder="Ex: Centro"
|
| 419 |
+
/>
|
| 420 |
+
</label>
|
| 421 |
|
| 422 |
+
<div className="pesquisa-avaliando-inline pesquisa-avaliando-inline-periodo">
|
| 423 |
+
<div className="pesquisa-field-pair pesquisa-field-pair-inline">
|
| 424 |
+
<span className="pesquisa-field-pair-title">Periodo de data do imovel</span>
|
|
|
|
|
|
|
| 425 |
<label className="pesquisa-field">
|
| 426 |
+
Data inicial
|
| 427 |
+
<DateFieldInput field="dataMin" value={filters.dataMin} onChange={onFieldChange} />
|
| 428 |
</label>
|
| 429 |
<label className="pesquisa-field">
|
| 430 |
+
Data final
|
| 431 |
+
<DateFieldInput field="dataMax" value={filters.dataMax} onChange={onFieldChange} />
|
| 432 |
</label>
|
| 433 |
</div>
|
|
|
|
|
|
|
|
|
|
| 434 |
<label className="pesquisa-field">
|
| 435 |
+
Area do imovel
|
| 436 |
+
<NumberFieldInput field="avalArea" value={filters.avalArea} onChange={onFieldChange} placeholder="0" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
</label>
|
| 438 |
<label className="pesquisa-field">
|
| 439 |
+
RH do imovel
|
| 440 |
+
<NumberFieldInput field="avalRh" value={filters.avalRh} onChange={onFieldChange} placeholder="0" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
</label>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
</div>
|
| 443 |
+
</div>
|
| 444 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 445 |
<datalist id="pesquisa-finalidades">
|
| 446 |
{(sugestoes.finalidades || []).map((item) => (
|
| 447 |
<option key={`finalidade-${item}`} value={item} />
|
|
|
|
| 468 |
<SectionBlock
|
| 469 |
step="2"
|
| 470 |
title="Resultados"
|
| 471 |
+
subtitle="Modelos aceitos para os parametros do avaliando informado."
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
>
|
| 473 |
<div className="pesquisa-results-toolbar">
|
| 474 |
<div className="pesquisa-summary-line">
|
| 475 |
<strong>{formatCount(result.total_filtrado)}</strong>{' '}
|
| 476 |
+
modelo(s) aceito(s) de <strong>{formatCount(result.total_geral)}</strong>.
|
| 477 |
</div>
|
| 478 |
{resultIds.length ? (
|
| 479 |
<label className="pesquisa-select-all">
|
|
|
|
| 487 |
<div className="empty-box">
|
| 488 |
{!pesquisaInicializada
|
| 489 |
? 'Defina os filtros desejados e clique em Pesquisar.'
|
| 490 |
+
: 'Nenhum modelo aceitou os parametros do avaliando informado.'}
|
|
|
|
|
|
|
| 491 |
</div>
|
| 492 |
) : (
|
| 493 |
<div className="pesquisa-card-grid">
|
|
|
|
| 499 |
<div className="pesquisa-card-head">
|
| 500 |
<div className="pesquisa-card-head-main">
|
| 501 |
<h4>{modelo.nome_modelo || modelo.arquivo}</h4>
|
|
|
|
| 502 |
<div className="pesquisa-card-head-actions">
|
| 503 |
<label className="pesquisa-select-toggle">
|
| 504 |
<input
|
|
|
|
| 515 |
</div>
|
| 516 |
</div>
|
| 517 |
|
| 518 |
+
<div className="pesquisa-card-status-row">
|
| 519 |
+
<div className="status-pill done">Aceito para o avaliando ({modelo.avaliando?.campos_informados || 0} campo(s) validado(s))</div>
|
| 520 |
+
</div>
|
|
|
|
|
|
|
| 521 |
<div className="pesquisa-card-body">
|
| 522 |
<div className="pesquisa-card-dados-list">
|
| 523 |
<div><strong>Finalidades no modelo:</strong> {(modelo.finalidades || []).length ? modelo.finalidades.join(', ') : '-'}</div>
|
frontend/src/styles.css
CHANGED
|
@@ -136,7 +136,7 @@ textarea {
|
|
| 136 |
|
| 137 |
.tabs {
|
| 138 |
display: grid;
|
| 139 |
-
grid-template-columns: repeat(
|
| 140 |
gap: 10px;
|
| 141 |
}
|
| 142 |
|
|
@@ -225,6 +225,36 @@ textarea {
|
|
| 225 |
display: none !important;
|
| 226 |
}
|
| 227 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
.status-strip {
|
| 229 |
display: flex;
|
| 230 |
gap: 8px;
|
|
@@ -614,6 +644,16 @@ button.pesquisa-otica-btn.active:hover {
|
|
| 614 |
gap: 12px 14px;
|
| 615 |
}
|
| 616 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 617 |
.pesquisa-fields-grid .pesquisa-field {
|
| 618 |
min-width: 0;
|
| 619 |
}
|
|
@@ -814,7 +854,8 @@ button.pesquisa-coluna-remove:hover {
|
|
| 814 |
border-radius: 14px;
|
| 815 |
background: linear-gradient(180deg, #ffffff 0%, #fcfdff 100%);
|
| 816 |
padding: 12px;
|
| 817 |
-
display:
|
|
|
|
| 818 |
gap: 10px;
|
| 819 |
min-width: 0;
|
| 820 |
height: 100%;
|
|
@@ -841,9 +882,11 @@ button.pesquisa-coluna-remove:hover {
|
|
| 841 |
}
|
| 842 |
|
| 843 |
.pesquisa-card-top {
|
| 844 |
-
display:
|
|
|
|
| 845 |
gap: 8px;
|
| 846 |
min-width: 0;
|
|
|
|
| 847 |
}
|
| 848 |
|
| 849 |
.pesquisa-card-head {
|
|
@@ -1449,7 +1492,7 @@ button.btn-upload-select {
|
|
| 1449 |
|
| 1450 |
.variavel-badge-line {
|
| 1451 |
display: grid;
|
| 1452 |
-
grid-template-columns:
|
| 1453 |
gap: 10px;
|
| 1454 |
align-items: flex-start;
|
| 1455 |
margin-top: 8px;
|
|
@@ -1465,6 +1508,13 @@ button.btn-upload-select {
|
|
| 1465 |
letter-spacing: 0.02em;
|
| 1466 |
}
|
| 1467 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1468 |
.variavel-chip-wrap {
|
| 1469 |
display: flex;
|
| 1470 |
flex-wrap: wrap;
|
|
@@ -2217,6 +2267,29 @@ button.btn-upload-select {
|
|
| 2217 |
font-weight: 600;
|
| 2218 |
}
|
| 2219 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2220 |
.geo-correcoes {
|
| 2221 |
display: grid;
|
| 2222 |
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
@@ -2621,7 +2694,7 @@ button.btn-upload-select {
|
|
| 2621 |
|
| 2622 |
@media (max-width: 1150px) {
|
| 2623 |
.tabs {
|
| 2624 |
-
grid-template-columns: 1fr;
|
| 2625 |
}
|
| 2626 |
|
| 2627 |
.app-header {
|
|
@@ -2659,6 +2732,10 @@ button.btn-upload-select {
|
|
| 2659 |
}
|
| 2660 |
|
| 2661 |
@media (max-width: 760px) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2662 |
.app-shell {
|
| 2663 |
width: 97vw;
|
| 2664 |
margin-top: 10px;
|
|
|
|
| 136 |
|
| 137 |
.tabs {
|
| 138 |
display: grid;
|
| 139 |
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
| 140 |
gap: 10px;
|
| 141 |
}
|
| 142 |
|
|
|
|
| 225 |
display: none !important;
|
| 226 |
}
|
| 227 |
|
| 228 |
+
.inicio-card {
|
| 229 |
+
border: 1px solid #d6e3ef;
|
| 230 |
+
border-radius: 12px;
|
| 231 |
+
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
| 232 |
+
padding: 16px 18px;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
.inicio-card h3 {
|
| 236 |
+
margin: 0 0 10px;
|
| 237 |
+
font-family: 'Sora', sans-serif;
|
| 238 |
+
color: #2a3f54;
|
| 239 |
+
font-size: 1rem;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
.inicio-lista {
|
| 243 |
+
margin: 0 0 10px;
|
| 244 |
+
padding-left: 18px;
|
| 245 |
+
color: #40596f;
|
| 246 |
+
display: grid;
|
| 247 |
+
gap: 6px;
|
| 248 |
+
font-size: 0.9rem;
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
.inicio-creditos {
|
| 252 |
+
margin: 0;
|
| 253 |
+
color: #334d65;
|
| 254 |
+
font-size: 0.9rem;
|
| 255 |
+
font-weight: 600;
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
.status-strip {
|
| 259 |
display: flex;
|
| 260 |
gap: 8px;
|
|
|
|
| 644 |
gap: 12px 14px;
|
| 645 |
}
|
| 646 |
|
| 647 |
+
.pesquisa-avaliando-inline-periodo {
|
| 648 |
+
grid-template-columns: minmax(280px, 1.45fr) repeat(2, minmax(0, 1fr));
|
| 649 |
+
align-items: end;
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
.pesquisa-avaliando-inline-periodo .pesquisa-field-pair {
|
| 653 |
+
grid-column: auto;
|
| 654 |
+
margin: 0;
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
.pesquisa-fields-grid .pesquisa-field {
|
| 658 |
min-width: 0;
|
| 659 |
}
|
|
|
|
| 854 |
border-radius: 14px;
|
| 855 |
background: linear-gradient(180deg, #ffffff 0%, #fcfdff 100%);
|
| 856 |
padding: 12px;
|
| 857 |
+
display: flex;
|
| 858 |
+
flex-direction: column;
|
| 859 |
gap: 10px;
|
| 860 |
min-width: 0;
|
| 861 |
height: 100%;
|
|
|
|
| 882 |
}
|
| 883 |
|
| 884 |
.pesquisa-card-top {
|
| 885 |
+
display: flex;
|
| 886 |
+
flex-direction: column;
|
| 887 |
gap: 8px;
|
| 888 |
min-width: 0;
|
| 889 |
+
flex: 1 1 auto;
|
| 890 |
}
|
| 891 |
|
| 892 |
.pesquisa-card-head {
|
|
|
|
| 1492 |
|
| 1493 |
.variavel-badge-line {
|
| 1494 |
display: grid;
|
| 1495 |
+
grid-template-columns: 146px minmax(0, 1fr);
|
| 1496 |
gap: 10px;
|
| 1497 |
align-items: flex-start;
|
| 1498 |
margin-top: 8px;
|
|
|
|
| 1508 |
letter-spacing: 0.02em;
|
| 1509 |
}
|
| 1510 |
|
| 1511 |
+
.variavel-badge-value {
|
| 1512 |
+
align-self: center;
|
| 1513 |
+
color: #30475e;
|
| 1514 |
+
font-weight: 700;
|
| 1515 |
+
font-size: 0.86rem;
|
| 1516 |
+
}
|
| 1517 |
+
|
| 1518 |
.variavel-chip-wrap {
|
| 1519 |
display: flex;
|
| 1520 |
flex-wrap: wrap;
|
|
|
|
| 2267 |
font-weight: 600;
|
| 2268 |
}
|
| 2269 |
|
| 2270 |
+
.market-date-grid {
|
| 2271 |
+
display: grid;
|
| 2272 |
+
gap: 10px;
|
| 2273 |
+
}
|
| 2274 |
+
|
| 2275 |
+
.market-date-row {
|
| 2276 |
+
margin-bottom: 0;
|
| 2277 |
+
}
|
| 2278 |
+
|
| 2279 |
+
.market-date-actions-row {
|
| 2280 |
+
align-items: center;
|
| 2281 |
+
justify-content: space-between;
|
| 2282 |
+
gap: 10px;
|
| 2283 |
+
flex-wrap: wrap;
|
| 2284 |
+
}
|
| 2285 |
+
|
| 2286 |
+
.market-date-actions-row .resumo-outliers-box {
|
| 2287 |
+
margin-top: 0;
|
| 2288 |
+
min-height: 38px;
|
| 2289 |
+
display: inline-flex;
|
| 2290 |
+
align-items: center;
|
| 2291 |
+
}
|
| 2292 |
+
|
| 2293 |
.geo-correcoes {
|
| 2294 |
display: grid;
|
| 2295 |
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
|
|
| 2694 |
|
| 2695 |
@media (max-width: 1150px) {
|
| 2696 |
.tabs {
|
| 2697 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 2698 |
}
|
| 2699 |
|
| 2700 |
.app-header {
|
|
|
|
| 2732 |
}
|
| 2733 |
|
| 2734 |
@media (max-width: 760px) {
|
| 2735 |
+
.tabs {
|
| 2736 |
+
grid-template-columns: 1fr;
|
| 2737 |
+
}
|
| 2738 |
+
|
| 2739 |
.app-shell {
|
| 2740 |
width: 97vw;
|
| 2741 |
margin-top: 10px;
|