Spaces:
Running
Running
Guilherme Silberfarb Costa commited on
Commit ·
82ec900
1
Parent(s): c578055
prototipo aba pesquisa
Browse files- .gitignore +3 -0
- backend/app/api/pesquisa.py +108 -0
- backend/app/core/pesquisa/modelos_dai/.gitkeep +0 -0
- backend/app/core/pesquisa/modelos_dai/README.md +19 -0
- backend/app/main.py +2 -1
- backend/app/services/pesquisa_service.py +1599 -0
- frontend/src/App.jsx +5 -13
- frontend/src/api.js +15 -0
- frontend/src/components/PesquisaTab.jsx +1113 -0
- frontend/src/styles.css +679 -0
.gitignore
CHANGED
|
@@ -11,3 +11,6 @@ frontend/dist/
|
|
| 11 |
|
| 12 |
# System
|
| 13 |
.DS_Store
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
# System
|
| 13 |
.DS_Store
|
| 14 |
+
|
| 15 |
+
# Modelos locais da pesquisa (binários, não versionar)
|
| 16 |
+
backend/app/core/pesquisa/modelos_dai/*.dai
|
backend/app/api/pesquisa.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from fastapi import APIRouter, Query
|
| 4 |
+
from pydantic import BaseModel
|
| 5 |
+
|
| 6 |
+
from app.services.pesquisa_service import PesquisaFiltros, gerar_mapa_modelos, listar_modelos
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
router = APIRouter(prefix="/api/pesquisa", tags=["pesquisa"])
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _split_csv(value: str | None) -> list[str]:
|
| 13 |
+
if not value:
|
| 14 |
+
return []
|
| 15 |
+
return [item.strip() for item in value.split(",") if item.strip()]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class MapaModelosPayload(BaseModel):
|
| 19 |
+
modelos_ids: list[str]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@router.get("/modelos")
|
| 23 |
+
def pesquisar_modelos(
|
| 24 |
+
somente_contexto: bool = Query(False),
|
| 25 |
+
otica: str = Query("modelo"),
|
| 26 |
+
nome: str | None = Query(None),
|
| 27 |
+
autor: str | None = Query(None),
|
| 28 |
+
finalidade: str | None = Query(None),
|
| 29 |
+
finalidade_colunas: str | None = Query(None),
|
| 30 |
+
bairro: str | None = Query(None),
|
| 31 |
+
bairros: str | None = Query(None),
|
| 32 |
+
bairros_colunas: str | None = Query(None),
|
| 33 |
+
endereco: str | None = Query(None),
|
| 34 |
+
data_min: str | None = Query(None),
|
| 35 |
+
data_colunas: str | None = Query(None),
|
| 36 |
+
data_max: str | None = Query(None),
|
| 37 |
+
area_min: float | None = Query(None),
|
| 38 |
+
area_colunas: str | None = Query(None),
|
| 39 |
+
area_max: float | None = Query(None),
|
| 40 |
+
rh_min: float | None = Query(None),
|
| 41 |
+
rh_colunas: str | None = Query(None),
|
| 42 |
+
rh_max: float | None = Query(None),
|
| 43 |
+
aval_finalidade: str | None = Query(None),
|
| 44 |
+
aval_finalidade_colunas: str | None = Query(None),
|
| 45 |
+
aval_bairro: str | None = Query(None),
|
| 46 |
+
aval_bairro_colunas: str | None = Query(None),
|
| 47 |
+
aval_endereco: str | None = Query(None),
|
| 48 |
+
aval_data: str | None = Query(None),
|
| 49 |
+
aval_data_colunas: str | None = Query(None),
|
| 50 |
+
aval_area: float | None = Query(None),
|
| 51 |
+
aval_area_colunas: str | None = Query(None),
|
| 52 |
+
aval_area_privativa: float | None = Query(None),
|
| 53 |
+
aval_area_privativa_colunas: str | None = Query(None),
|
| 54 |
+
aval_area_total: float | None = Query(None),
|
| 55 |
+
aval_area_total_colunas: str | None = Query(None),
|
| 56 |
+
aval_rh: float | None = Query(None),
|
| 57 |
+
aval_rh_colunas: str | None = Query(None),
|
| 58 |
+
aval_valor_unitario: float | None = Query(None),
|
| 59 |
+
aval_valor_unitario_colunas: str | None = Query(None),
|
| 60 |
+
aval_valor_total: float | None = Query(None),
|
| 61 |
+
aval_valor_total_colunas: str | None = Query(None),
|
| 62 |
+
limite: int = Query(300, ge=1, le=2000),
|
| 63 |
+
) -> dict:
|
| 64 |
+
filtros = PesquisaFiltros(
|
| 65 |
+
otica=otica,
|
| 66 |
+
nome=nome,
|
| 67 |
+
autor=autor,
|
| 68 |
+
finalidade=finalidade,
|
| 69 |
+
finalidade_colunas=_split_csv(finalidade_colunas),
|
| 70 |
+
bairro=bairro,
|
| 71 |
+
bairros=_split_csv(bairros),
|
| 72 |
+
bairros_colunas=_split_csv(bairros_colunas),
|
| 73 |
+
endereco=endereco,
|
| 74 |
+
data_min=data_min,
|
| 75 |
+
data_colunas=_split_csv(data_colunas),
|
| 76 |
+
data_max=data_max,
|
| 77 |
+
area_min=area_min,
|
| 78 |
+
area_colunas=_split_csv(area_colunas),
|
| 79 |
+
area_max=area_max,
|
| 80 |
+
rh_min=rh_min,
|
| 81 |
+
rh_colunas=_split_csv(rh_colunas),
|
| 82 |
+
rh_max=rh_max,
|
| 83 |
+
aval_finalidade=aval_finalidade,
|
| 84 |
+
aval_finalidade_colunas=_split_csv(aval_finalidade_colunas),
|
| 85 |
+
aval_bairro=aval_bairro,
|
| 86 |
+
aval_bairro_colunas=_split_csv(aval_bairro_colunas),
|
| 87 |
+
aval_endereco=aval_endereco,
|
| 88 |
+
aval_data=aval_data,
|
| 89 |
+
aval_data_colunas=_split_csv(aval_data_colunas),
|
| 90 |
+
aval_area=aval_area,
|
| 91 |
+
aval_area_colunas=_split_csv(aval_area_colunas),
|
| 92 |
+
aval_area_privativa=aval_area_privativa,
|
| 93 |
+
aval_area_privativa_colunas=_split_csv(aval_area_privativa_colunas),
|
| 94 |
+
aval_area_total=aval_area_total,
|
| 95 |
+
aval_area_total_colunas=_split_csv(aval_area_total_colunas),
|
| 96 |
+
aval_rh=aval_rh,
|
| 97 |
+
aval_rh_colunas=_split_csv(aval_rh_colunas),
|
| 98 |
+
aval_valor_unitario=aval_valor_unitario,
|
| 99 |
+
aval_valor_unitario_colunas=_split_csv(aval_valor_unitario_colunas),
|
| 100 |
+
aval_valor_total=aval_valor_total,
|
| 101 |
+
aval_valor_total_colunas=_split_csv(aval_valor_total_colunas),
|
| 102 |
+
)
|
| 103 |
+
return listar_modelos(filtros=filtros, limite=limite, somente_contexto=somente_contexto)
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
@router.post("/mapa-modelos")
|
| 107 |
+
def pesquisar_mapa_modelos(payload: MapaModelosPayload) -> dict:
|
| 108 |
+
return gerar_mapa_modelos(payload.modelos_ids)
|
backend/app/core/pesquisa/modelos_dai/.gitkeep
ADDED
|
File without changes
|
backend/app/core/pesquisa/modelos_dai/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Pasta de Modelos da Aba Pesquisa
|
| 2 |
+
|
| 3 |
+
Coloque nesta pasta os arquivos `.dai` que devem aparecer na aba **Pesquisa**.
|
| 4 |
+
|
| 5 |
+
## Estrutura
|
| 6 |
+
|
| 7 |
+
- `NOME_MODELO.dai`
|
| 8 |
+
|
| 9 |
+
Exemplo:
|
| 10 |
+
|
| 11 |
+
- `MOD_A_SALA_Z1_006C.dai`
|
| 12 |
+
|
| 13 |
+
## Como os dados sao lidos
|
| 14 |
+
|
| 15 |
+
- Nome do modelo: nome do arquivo `.dai`
|
| 16 |
+
- Autor, equacao, R2 e dados estatisticos: lidos do proprio pacote `.dai`
|
| 17 |
+
- Finalidade e bairros: extraidos das colunas do `.dai` (com prioridade para aliases de `NME IMO-FINAL` e `NME BAI`)
|
| 18 |
+
|
| 19 |
+
Nao ha suporte a arquivo auxiliar `.meta.json` nesta pasta.
|
backend/app/main.py
CHANGED
|
@@ -6,7 +6,7 @@ from fastapi import FastAPI
|
|
| 6 |
from fastapi.middleware.cors import CORSMiddleware
|
| 7 |
from fastapi.staticfiles import StaticFiles
|
| 8 |
|
| 9 |
-
from app.api import elaboracao, health, session, visualizacao
|
| 10 |
|
| 11 |
|
| 12 |
app = FastAPI(
|
|
@@ -27,6 +27,7 @@ app.include_router(health.router)
|
|
| 27 |
app.include_router(session.router)
|
| 28 |
app.include_router(elaboracao.router)
|
| 29 |
app.include_router(visualizacao.router)
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
def _mount_frontend_if_exists() -> None:
|
|
|
|
| 6 |
from fastapi.middleware.cors import CORSMiddleware
|
| 7 |
from fastapi.staticfiles import StaticFiles
|
| 8 |
|
| 9 |
+
from app.api import elaboracao, health, pesquisa, session, visualizacao
|
| 10 |
|
| 11 |
|
| 12 |
app = FastAPI(
|
|
|
|
| 27 |
app.include_router(session.router)
|
| 28 |
app.include_router(elaboracao.router)
|
| 29 |
app.include_router(visualizacao.router)
|
| 30 |
+
app.include_router(pesquisa.router)
|
| 31 |
|
| 32 |
|
| 33 |
def _mount_frontend_if_exists() -> None:
|
backend/app/services/pesquisa_service.py
ADDED
|
@@ -0,0 +1,1599 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
import re
|
| 5 |
+
import unicodedata
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from datetime import date, datetime
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from threading import Lock
|
| 10 |
+
from typing import Any
|
| 11 |
+
|
| 12 |
+
import folium
|
| 13 |
+
import pandas as pd
|
| 14 |
+
from fastapi import HTTPException
|
| 15 |
+
from joblib import load
|
| 16 |
+
|
| 17 |
+
from app.core.elaboracao.core import _migrar_pacote_v1_para_v2
|
| 18 |
+
from app.services.serializers import sanitize_value
|
| 19 |
+
|
| 20 |
+
MODELOS_DIR = Path(__file__).resolve().parent.parent / "core" / "pesquisa" / "modelos_dai"
|
| 21 |
+
|
| 22 |
+
AREA_PRIVATIVA_ALIASES = ["APRIV", "ACOPRIV", "AREAPRIV", "AREA_PRIVATIVA", "AREA PRIVATIVA"]
|
| 23 |
+
AREA_TOTAL_ALIASES = ["ATOTAL", "ATOT", "AREA_TOTAL", "AREA TOTAL", "AREA"]
|
| 24 |
+
VALOR_UNITARIO_ALIASES = ["VU", "VUNIT", "VULOC", "VUAPRIV", "VALOR_UNITARIO", "VALOR UNITARIO"]
|
| 25 |
+
VALOR_TOTAL_ALIASES = ["VLOC", "VTOT", "VALOR_TOTAL", "VALOR TOTAL"]
|
| 26 |
+
RH_ALIASES = ["RH", "FATOR_RH", "FATOR RH", "RENDA_HABITACIONAL"]
|
| 27 |
+
DATA_ALIASES = ["DATA", "DT", "ANO", "DATA_AVALIACAO", "DATA AVALIACAO", "COMPETENCIA"]
|
| 28 |
+
BAIRRO_ALIASES = ["BAIRRO", "BAIRROS", "NOME_BAIRRO", "BAIRRO_NOME", "NME BAI", "NME_BAI"]
|
| 29 |
+
FINALIDADE_ALIASES = ["NME IMO-FINAL", "NME_IMO_FINAL", "NME IMO FINAL", "FINALIDADE", "TIPO_IMOVEL", "TIPO IMOVEL"]
|
| 30 |
+
LAT_ALIASES = ["LAT", "LATITUDE", "SIAT_LATITUDE"]
|
| 31 |
+
LON_ALIASES = ["LON", "LONG", "LONGITUDE", "SIAT_LONGITUDE"]
|
| 32 |
+
|
| 33 |
+
TIPO_POR_TOKEN = {
|
| 34 |
+
"RECOND": "Residencia em condominio",
|
| 35 |
+
"RCOMD": "Residencia em condominio",
|
| 36 |
+
"TCOND": "Terreno em condominio",
|
| 37 |
+
"LCOM": "Loja",
|
| 38 |
+
"LOJA": "Loja",
|
| 39 |
+
"CCOM": "Casa comercial",
|
| 40 |
+
"DEP": "Deposito",
|
| 41 |
+
"DEPOS": "Deposito",
|
| 42 |
+
"RES": "Residencias isoladas / casas",
|
| 43 |
+
"SALA": "Salas comerciais",
|
| 44 |
+
"APTO": "Apartamentos residenciais",
|
| 45 |
+
"APART": "Apartamentos residenciais",
|
| 46 |
+
"AP": "Apartamentos residenciais",
|
| 47 |
+
"TERRENO": "Terrenos",
|
| 48 |
+
"TER": "Terrenos",
|
| 49 |
+
"EDIF": "Edificio",
|
| 50 |
+
"CASA": "Residencias isoladas / casas",
|
| 51 |
+
"GALPAO": "Galpao",
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
CAMPO_TEXTO_META_FONTES = {
|
| 55 |
+
"finalidade": [],
|
| 56 |
+
"bairros": [],
|
| 57 |
+
"aval_finalidade": [],
|
| 58 |
+
"aval_bairro": [],
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
CAMPO_TEXTO_ALIASES_COLUNA = {
|
| 62 |
+
"finalidade": FINALIDADE_ALIASES,
|
| 63 |
+
"bairros": BAIRRO_ALIASES,
|
| 64 |
+
"aval_finalidade": FINALIDADE_ALIASES,
|
| 65 |
+
"aval_bairro": BAIRRO_ALIASES,
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
CAMPO_FAIXA_META_FONTES = {
|
| 69 |
+
"data": [],
|
| 70 |
+
"area": [],
|
| 71 |
+
"rh": [],
|
| 72 |
+
"aval_data": [],
|
| 73 |
+
"aval_area": [],
|
| 74 |
+
"aval_area_privativa": [],
|
| 75 |
+
"aval_area_total": [],
|
| 76 |
+
"aval_rh": [],
|
| 77 |
+
"aval_valor_unitario": [],
|
| 78 |
+
"aval_valor_total": [],
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
CAMPO_FAIXA_ALIASES_COLUNA = {
|
| 82 |
+
"data": DATA_ALIASES,
|
| 83 |
+
"area": AREA_PRIVATIVA_ALIASES + AREA_TOTAL_ALIASES,
|
| 84 |
+
"rh": RH_ALIASES,
|
| 85 |
+
"aval_data": DATA_ALIASES,
|
| 86 |
+
"aval_area": AREA_PRIVATIVA_ALIASES + AREA_TOTAL_ALIASES,
|
| 87 |
+
"aval_area_privativa": AREA_PRIVATIVA_ALIASES,
|
| 88 |
+
"aval_area_total": AREA_TOTAL_ALIASES,
|
| 89 |
+
"aval_rh": RH_ALIASES,
|
| 90 |
+
"aval_valor_unitario": VALOR_UNITARIO_ALIASES,
|
| 91 |
+
"aval_valor_total": VALOR_TOTAL_ALIASES,
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
FONTE_META_LABELS = {
|
| 95 |
+
"meta:nome_modelo": "Metadado: Nome do modelo",
|
| 96 |
+
"meta:arquivo": "Metadado: Arquivo",
|
| 97 |
+
"meta:autor": "Metadado: Autor/Elaborador",
|
| 98 |
+
"meta:finalidade": "Metadado: Finalidade",
|
| 99 |
+
"meta:tipo_imovel": "Metadado: Tipo",
|
| 100 |
+
"meta:finalidades": "Metadado: Finalidades",
|
| 101 |
+
"meta:bairros": "Metadado: Bairros",
|
| 102 |
+
"meta:endereco_referencia": "Metadado: Endereco de referencia",
|
| 103 |
+
"meta:faixa_data": "Metadado: Faixa de data",
|
| 104 |
+
"meta:faixa_area": "Metadado: Faixa de area",
|
| 105 |
+
"meta:faixa_rh": "Metadado: Faixa de RH",
|
| 106 |
+
"meta:faixa_area_privativa": "Metadado: Faixa de area privativa",
|
| 107 |
+
"meta:faixa_area_total": "Metadado: Faixa de area total",
|
| 108 |
+
"meta:faixa_valor_unitario": "Metadado: Faixa de valor unitario",
|
| 109 |
+
"meta:faixa_valor_total": "Metadado: Faixa de valor total",
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
MAX_COLUNAS_INDEXADAS = 120
|
| 113 |
+
MAX_VALORES_INDEXADOS_POR_COLUNA = 140
|
| 114 |
+
MAX_LINHAS_INDEXACAO = 5000
|
| 115 |
+
|
| 116 |
+
COMPATIBILIDADE_MAP = {
|
| 117 |
+
"area_privativa": AREA_PRIVATIVA_ALIASES,
|
| 118 |
+
"area_total": AREA_TOTAL_ALIASES,
|
| 119 |
+
"valor_unitario": VALOR_UNITARIO_ALIASES,
|
| 120 |
+
"valor_total": VALOR_TOTAL_ALIASES,
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
RANGE_CAMPOS = {
|
| 124 |
+
"area_privativa": AREA_PRIVATIVA_ALIASES,
|
| 125 |
+
"area_total": AREA_TOTAL_ALIASES,
|
| 126 |
+
"valor_unitario": VALOR_UNITARIO_ALIASES,
|
| 127 |
+
"valor_total": VALOR_TOTAL_ALIASES,
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
MAP_COLORS = [
|
| 131 |
+
"#1f77b4",
|
| 132 |
+
"#d62728",
|
| 133 |
+
"#2ca02c",
|
| 134 |
+
"#ff7f0e",
|
| 135 |
+
"#9467bd",
|
| 136 |
+
"#17becf",
|
| 137 |
+
"#8c564b",
|
| 138 |
+
"#e377c2",
|
| 139 |
+
"#bcbd22",
|
| 140 |
+
"#7f7f7f",
|
| 141 |
+
]
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
@dataclass(frozen=True)
|
| 145 |
+
class PesquisaFiltros:
|
| 146 |
+
otica: str = "modelo"
|
| 147 |
+
nome: str | None = None
|
| 148 |
+
autor: str | None = None
|
| 149 |
+
finalidade: str | None = None
|
| 150 |
+
finalidade_colunas: list[str] | None = None
|
| 151 |
+
bairro: str | None = None
|
| 152 |
+
bairros: list[str] | None = None
|
| 153 |
+
bairros_colunas: list[str] | None = None
|
| 154 |
+
endereco: str | None = None
|
| 155 |
+
data_min: str | None = None
|
| 156 |
+
data_colunas: list[str] | None = None
|
| 157 |
+
data_max: str | None = None
|
| 158 |
+
area_min: float | None = None
|
| 159 |
+
area_colunas: list[str] | None = None
|
| 160 |
+
area_max: float | None = None
|
| 161 |
+
rh_min: float | None = None
|
| 162 |
+
rh_colunas: list[str] | None = None
|
| 163 |
+
rh_max: float | None = None
|
| 164 |
+
aval_finalidade: str | None = None
|
| 165 |
+
aval_finalidade_colunas: list[str] | None = None
|
| 166 |
+
aval_bairro: str | None = None
|
| 167 |
+
aval_bairro_colunas: list[str] | None = None
|
| 168 |
+
aval_endereco: str | None = None
|
| 169 |
+
aval_data: str | None = None
|
| 170 |
+
aval_data_colunas: list[str] | None = None
|
| 171 |
+
aval_area: float | None = None
|
| 172 |
+
aval_area_colunas: list[str] | None = None
|
| 173 |
+
aval_area_privativa: float | None = None
|
| 174 |
+
aval_area_privativa_colunas: list[str] | None = None
|
| 175 |
+
aval_area_total: float | None = None
|
| 176 |
+
aval_area_total_colunas: list[str] | None = None
|
| 177 |
+
aval_rh: float | None = None
|
| 178 |
+
aval_rh_colunas: list[str] | None = None
|
| 179 |
+
aval_valor_unitario: float | None = None
|
| 180 |
+
aval_valor_unitario_colunas: list[str] | None = None
|
| 181 |
+
aval_valor_total: float | None = None
|
| 182 |
+
aval_valor_total_colunas: list[str] | None = None
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
_CACHE_LOCK = Lock()
|
| 186 |
+
_CACHE: dict[str, dict[str, Any]] = {}
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def ensure_modelos_dir() -> Path:
|
| 190 |
+
MODELOS_DIR.mkdir(parents=True, exist_ok=True)
|
| 191 |
+
return MODELOS_DIR
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def listar_modelos(filtros: PesquisaFiltros, limite: int | None = None, somente_contexto: bool = False) -> dict[str, Any]:
|
| 195 |
+
pasta = ensure_modelos_dir()
|
| 196 |
+
modelos = sorted(pasta.glob("*.dai"), key=lambda item: item.name.lower())
|
| 197 |
+
|
| 198 |
+
otica = _normalizar_otica(filtros.otica)
|
| 199 |
+
filtros_exec = PesquisaFiltros(**{**filtros.__dict__, "otica": otica})
|
| 200 |
+
|
| 201 |
+
todos = [_carregar_resumo_com_cache(caminho) for caminho in modelos]
|
| 202 |
+
sugestoes = _extrair_sugestoes(todos)
|
| 203 |
+
colunas_filtro = _montar_config_colunas_filtro(todos)
|
| 204 |
+
|
| 205 |
+
if somente_contexto:
|
| 206 |
+
return sanitize_value(
|
| 207 |
+
{
|
| 208 |
+
"modelos": [],
|
| 209 |
+
"sugestoes": sugestoes,
|
| 210 |
+
"colunas_filtro": colunas_filtro,
|
| 211 |
+
"total_filtrado": 0,
|
| 212 |
+
"total_geral": len(todos),
|
| 213 |
+
"modelos_dir": str(pasta),
|
| 214 |
+
"filtros_aplicados": {
|
| 215 |
+
"nome": filtros.nome,
|
| 216 |
+
"autor": filtros.autor,
|
| 217 |
+
"finalidade": filtros.finalidade,
|
| 218 |
+
"finalidade_colunas": filtros.finalidade_colunas or [],
|
| 219 |
+
"bairro": filtros.bairro,
|
| 220 |
+
"bairros": filtros.bairros or [],
|
| 221 |
+
"bairros_colunas": filtros.bairros_colunas or [],
|
| 222 |
+
"endereco": filtros.endereco,
|
| 223 |
+
"data_min": filtros.data_min,
|
| 224 |
+
"data_colunas": filtros.data_colunas or [],
|
| 225 |
+
"data_max": filtros.data_max,
|
| 226 |
+
"area_min": filtros.area_min,
|
| 227 |
+
"area_colunas": filtros.area_colunas or [],
|
| 228 |
+
"area_max": filtros.area_max,
|
| 229 |
+
"rh_min": filtros.rh_min,
|
| 230 |
+
"rh_colunas": filtros.rh_colunas or [],
|
| 231 |
+
"rh_max": filtros.rh_max,
|
| 232 |
+
"otica": otica,
|
| 233 |
+
"aval_finalidade": filtros.aval_finalidade,
|
| 234 |
+
"aval_finalidade_colunas": filtros.aval_finalidade_colunas or [],
|
| 235 |
+
"aval_bairro": filtros.aval_bairro,
|
| 236 |
+
"aval_bairro_colunas": filtros.aval_bairro_colunas or [],
|
| 237 |
+
"aval_endereco": filtros.aval_endereco,
|
| 238 |
+
"aval_data": filtros.aval_data,
|
| 239 |
+
"aval_data_colunas": filtros.aval_data_colunas or [],
|
| 240 |
+
"aval_area": filtros.aval_area,
|
| 241 |
+
"aval_area_colunas": filtros.aval_area_colunas or [],
|
| 242 |
+
"aval_area_privativa": filtros.aval_area_privativa,
|
| 243 |
+
"aval_area_privativa_colunas": filtros.aval_area_privativa_colunas or [],
|
| 244 |
+
"aval_area_total": filtros.aval_area_total,
|
| 245 |
+
"aval_area_total_colunas": filtros.aval_area_total_colunas or [],
|
| 246 |
+
"aval_rh": filtros.aval_rh,
|
| 247 |
+
"aval_rh_colunas": filtros.aval_rh_colunas or [],
|
| 248 |
+
"aval_valor_unitario": filtros.aval_valor_unitario,
|
| 249 |
+
"aval_valor_unitario_colunas": filtros.aval_valor_unitario_colunas or [],
|
| 250 |
+
"aval_valor_total": filtros.aval_valor_total,
|
| 251 |
+
"aval_valor_total_colunas": filtros.aval_valor_total_colunas or [],
|
| 252 |
+
},
|
| 253 |
+
}
|
| 254 |
+
)
|
| 255 |
+
|
| 256 |
+
filtrados = [item for item in todos if _aceita_filtros(item, filtros_exec)]
|
| 257 |
+
|
| 258 |
+
if otica == "avaliando":
|
| 259 |
+
filtrados = [_anexar_avaliando_info(item, filtros_exec) for item in filtrados]
|
| 260 |
+
filtrados = [item for item in filtrados if item.get("avaliando", {}).get("aceito")]
|
| 261 |
+
|
| 262 |
+
if limite and limite > 0:
|
| 263 |
+
filtrados = filtrados[:limite]
|
| 264 |
+
|
| 265 |
+
modelos_publicos = [_modelo_publico(item) for item in filtrados]
|
| 266 |
+
|
| 267 |
+
return sanitize_value(
|
| 268 |
+
{
|
| 269 |
+
"modelos": modelos_publicos,
|
| 270 |
+
"sugestoes": sugestoes,
|
| 271 |
+
"colunas_filtro": colunas_filtro,
|
| 272 |
+
"total_filtrado": len(filtrados),
|
| 273 |
+
"total_geral": len(todos),
|
| 274 |
+
"modelos_dir": str(pasta),
|
| 275 |
+
"filtros_aplicados": {
|
| 276 |
+
"nome": filtros.nome,
|
| 277 |
+
"autor": filtros.autor,
|
| 278 |
+
"finalidade": filtros.finalidade,
|
| 279 |
+
"finalidade_colunas": filtros.finalidade_colunas or [],
|
| 280 |
+
"bairro": filtros.bairro,
|
| 281 |
+
"bairros": filtros.bairros or [],
|
| 282 |
+
"bairros_colunas": filtros.bairros_colunas or [],
|
| 283 |
+
"endereco": filtros.endereco,
|
| 284 |
+
"data_min": filtros.data_min,
|
| 285 |
+
"data_colunas": filtros.data_colunas or [],
|
| 286 |
+
"data_max": filtros.data_max,
|
| 287 |
+
"area_min": filtros.area_min,
|
| 288 |
+
"area_colunas": filtros.area_colunas or [],
|
| 289 |
+
"area_max": filtros.area_max,
|
| 290 |
+
"rh_min": filtros.rh_min,
|
| 291 |
+
"rh_colunas": filtros.rh_colunas or [],
|
| 292 |
+
"rh_max": filtros.rh_max,
|
| 293 |
+
"otica": otica,
|
| 294 |
+
"aval_finalidade": filtros.aval_finalidade,
|
| 295 |
+
"aval_finalidade_colunas": filtros.aval_finalidade_colunas or [],
|
| 296 |
+
"aval_bairro": filtros.aval_bairro,
|
| 297 |
+
"aval_bairro_colunas": filtros.aval_bairro_colunas or [],
|
| 298 |
+
"aval_endereco": filtros.aval_endereco,
|
| 299 |
+
"aval_data": filtros.aval_data,
|
| 300 |
+
"aval_data_colunas": filtros.aval_data_colunas or [],
|
| 301 |
+
"aval_area": filtros.aval_area,
|
| 302 |
+
"aval_area_colunas": filtros.aval_area_colunas or [],
|
| 303 |
+
"aval_area_privativa": filtros.aval_area_privativa,
|
| 304 |
+
"aval_area_privativa_colunas": filtros.aval_area_privativa_colunas or [],
|
| 305 |
+
"aval_area_total": filtros.aval_area_total,
|
| 306 |
+
"aval_area_total_colunas": filtros.aval_area_total_colunas or [],
|
| 307 |
+
"aval_rh": filtros.aval_rh,
|
| 308 |
+
"aval_rh_colunas": filtros.aval_rh_colunas or [],
|
| 309 |
+
"aval_valor_unitario": filtros.aval_valor_unitario,
|
| 310 |
+
"aval_valor_unitario_colunas": filtros.aval_valor_unitario_colunas or [],
|
| 311 |
+
"aval_valor_total": filtros.aval_valor_total,
|
| 312 |
+
"aval_valor_total_colunas": filtros.aval_valor_total_colunas or [],
|
| 313 |
+
},
|
| 314 |
+
}
|
| 315 |
+
)
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def gerar_mapa_modelos(modelos_ids: list[str], limite_pontos_por_modelo: int = 4500) -> dict[str, Any]:
|
| 319 |
+
ids = [str(item).strip() for item in (modelos_ids or []) if str(item).strip()]
|
| 320 |
+
if not ids:
|
| 321 |
+
raise HTTPException(status_code=400, detail="Selecione ao menos um modelo para gerar o mapa")
|
| 322 |
+
|
| 323 |
+
pasta = ensure_modelos_dir()
|
| 324 |
+
caminhos_por_id = {caminho.stem: caminho for caminho in pasta.glob("*.dai")}
|
| 325 |
+
|
| 326 |
+
selecionados: list[tuple[str, Path]] = []
|
| 327 |
+
vistos = set()
|
| 328 |
+
for modelo_id in ids:
|
| 329 |
+
if modelo_id in vistos:
|
| 330 |
+
continue
|
| 331 |
+
caminho = caminhos_por_id.get(modelo_id)
|
| 332 |
+
if caminho is None:
|
| 333 |
+
continue
|
| 334 |
+
vistos.add(modelo_id)
|
| 335 |
+
selecionados.append((modelo_id, caminho))
|
| 336 |
+
|
| 337 |
+
if not selecionados:
|
| 338 |
+
raise HTTPException(status_code=404, detail="Nenhum modelo selecionado foi encontrado na pasta de pesquisa")
|
| 339 |
+
|
| 340 |
+
modelos_plotados: list[dict[str, Any]] = []
|
| 341 |
+
bounds: list[list[float]] = []
|
| 342 |
+
|
| 343 |
+
for idx, (modelo_id, caminho) in enumerate(selecionados):
|
| 344 |
+
resumo = _carregar_resumo_com_cache(caminho)
|
| 345 |
+
df = _carregar_dataframe_modelo(caminho)
|
| 346 |
+
if df is None or df.empty:
|
| 347 |
+
continue
|
| 348 |
+
|
| 349 |
+
pontos = _coletar_pontos_modelo(df, limite_pontos_por_modelo)
|
| 350 |
+
if not pontos:
|
| 351 |
+
continue
|
| 352 |
+
|
| 353 |
+
cor = MAP_COLORS[idx % len(MAP_COLORS)]
|
| 354 |
+
nome = str(resumo.get("nome_modelo") or modelo_id)
|
| 355 |
+
|
| 356 |
+
modelos_plotados.append(
|
| 357 |
+
{
|
| 358 |
+
"id": modelo_id,
|
| 359 |
+
"nome": nome,
|
| 360 |
+
"cor": cor,
|
| 361 |
+
"total_pontos": len(pontos),
|
| 362 |
+
"pontos": pontos,
|
| 363 |
+
}
|
| 364 |
+
)
|
| 365 |
+
bounds.extend([[ponto["lat"], ponto["lon"]] for ponto in pontos])
|
| 366 |
+
|
| 367 |
+
if not modelos_plotados:
|
| 368 |
+
raise HTTPException(
|
| 369 |
+
status_code=400,
|
| 370 |
+
detail="Nao foi possivel gerar o mapa: os modelos selecionados nao possuem coordenadas validas",
|
| 371 |
+
)
|
| 372 |
+
|
| 373 |
+
centro_lat = sum(coord[0] for coord in bounds) / len(bounds)
|
| 374 |
+
centro_lon = sum(coord[1] for coord in bounds) / len(bounds)
|
| 375 |
+
|
| 376 |
+
mapa = folium.Map(
|
| 377 |
+
location=[centro_lat, centro_lon],
|
| 378 |
+
zoom_start=12,
|
| 379 |
+
control_scale=True,
|
| 380 |
+
tiles="CartoDB positron",
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
total_pontos = 0
|
| 384 |
+
for modelo in modelos_plotados:
|
| 385 |
+
total_pontos += int(modelo["total_pontos"])
|
| 386 |
+
camada = folium.FeatureGroup(name=f'{modelo["nome"]} ({modelo["total_pontos"]})', show=True)
|
| 387 |
+
for ponto in modelo["pontos"]:
|
| 388 |
+
folium.CircleMarker(
|
| 389 |
+
location=[ponto["lat"], ponto["lon"]],
|
| 390 |
+
radius=3,
|
| 391 |
+
color=modelo["cor"],
|
| 392 |
+
fill=True,
|
| 393 |
+
fill_color=modelo["cor"],
|
| 394 |
+
fill_opacity=0.72,
|
| 395 |
+
opacity=0.9,
|
| 396 |
+
weight=1,
|
| 397 |
+
tooltip=modelo["nome"],
|
| 398 |
+
).add_to(camada)
|
| 399 |
+
camada.add_to(mapa)
|
| 400 |
+
|
| 401 |
+
folium.LayerControl(collapsed=False).add_to(mapa)
|
| 402 |
+
if bounds:
|
| 403 |
+
mapa.fit_bounds(bounds, padding=(20, 20))
|
| 404 |
+
|
| 405 |
+
return sanitize_value(
|
| 406 |
+
{
|
| 407 |
+
"mapa_html": mapa.get_root().render(),
|
| 408 |
+
"total_modelos_plotados": len(modelos_plotados),
|
| 409 |
+
"total_pontos": total_pontos,
|
| 410 |
+
"modelos_plotados": [
|
| 411 |
+
{
|
| 412 |
+
"id": modelo["id"],
|
| 413 |
+
"nome": modelo["nome"],
|
| 414 |
+
"cor": modelo["cor"],
|
| 415 |
+
"total_pontos": modelo["total_pontos"],
|
| 416 |
+
}
|
| 417 |
+
for modelo in modelos_plotados
|
| 418 |
+
],
|
| 419 |
+
"status": f"Mapa gerado com {len(modelos_plotados)} modelo(s) e {total_pontos} ponto(s)",
|
| 420 |
+
}
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
def _carregar_resumo_com_cache(caminho_modelo: Path) -> dict[str, Any]:
|
| 425 |
+
assinatura = _assinatura_arquivos(caminho_modelo)
|
| 426 |
+
cache_key = str(caminho_modelo)
|
| 427 |
+
|
| 428 |
+
with _CACHE_LOCK:
|
| 429 |
+
cached = _CACHE.get(cache_key)
|
| 430 |
+
if cached and cached.get("assinatura") == assinatura:
|
| 431 |
+
return dict(cached["resumo"])
|
| 432 |
+
|
| 433 |
+
resumo = _construir_resumo_modelo(caminho_modelo)
|
| 434 |
+
|
| 435 |
+
with _CACHE_LOCK:
|
| 436 |
+
_CACHE[cache_key] = {
|
| 437 |
+
"assinatura": assinatura,
|
| 438 |
+
"resumo": resumo,
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
return dict(resumo)
|
| 442 |
+
|
| 443 |
+
|
| 444 |
+
def _assinatura_arquivos(caminho_modelo: Path) -> tuple[int, int]:
|
| 445 |
+
stat_modelo = caminho_modelo.stat()
|
| 446 |
+
return (stat_modelo.st_mtime_ns, stat_modelo.st_size)
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
def _construir_resumo_modelo(caminho_modelo: Path) -> dict[str, Any]:
|
| 450 |
+
resumo = {
|
| 451 |
+
"id": caminho_modelo.stem,
|
| 452 |
+
"arquivo": caminho_modelo.name,
|
| 453 |
+
"nome_modelo": caminho_modelo.stem,
|
| 454 |
+
"autor": None,
|
| 455 |
+
"finalidade": _inferir_finalidade_por_nome(caminho_modelo.stem),
|
| 456 |
+
"finalidades": [],
|
| 457 |
+
"tipo_imovel": _inferir_tipo_por_nome(caminho_modelo.stem),
|
| 458 |
+
"bairros": [],
|
| 459 |
+
"faixa_area": None,
|
| 460 |
+
"faixa_rh": None,
|
| 461 |
+
"faixa_data": None,
|
| 462 |
+
"total_dados": None,
|
| 463 |
+
"total_trabalhos": None,
|
| 464 |
+
"endereco_referencia": None,
|
| 465 |
+
"equacao": None,
|
| 466 |
+
"r2": None,
|
| 467 |
+
"mapa_disponivel": False,
|
| 468 |
+
"compatibilidade_campos": {chave: [] for chave in COMPATIBILIDADE_MAP},
|
| 469 |
+
"faixas_por_campo": {chave: None for chave in RANGE_CAMPOS},
|
| 470 |
+
"variaveis_resumo": [],
|
| 471 |
+
"status": "ok",
|
| 472 |
+
"erro_leitura": None,
|
| 473 |
+
"fonte": {
|
| 474 |
+
"modelo": "dai",
|
| 475 |
+
},
|
| 476 |
+
"_texto_colunas_index": {},
|
| 477 |
+
"_faixa_colunas_index": {},
|
| 478 |
+
}
|
| 479 |
+
|
| 480 |
+
try:
|
| 481 |
+
pacote = load(caminho_modelo)
|
| 482 |
+
except Exception as exc:
|
| 483 |
+
resumo["status"] = "erro"
|
| 484 |
+
resumo["erro_leitura"] = f"Falha ao ler .dai: {exc}"
|
| 485 |
+
return resumo
|
| 486 |
+
|
| 487 |
+
if not isinstance(pacote, dict):
|
| 488 |
+
resumo["status"] = "erro"
|
| 489 |
+
resumo["erro_leitura"] = "Arquivo .dai invalido (conteudo nao e dict)."
|
| 490 |
+
return resumo
|
| 491 |
+
|
| 492 |
+
if "versao" not in pacote:
|
| 493 |
+
try:
|
| 494 |
+
pacote = _migrar_pacote_v1_para_v2(pacote)
|
| 495 |
+
except Exception:
|
| 496 |
+
pass
|
| 497 |
+
|
| 498 |
+
dados_pacote = pacote.get("dados") if isinstance(pacote.get("dados"), dict) else {}
|
| 499 |
+
estat_df = _to_dataframe(dados_pacote.get("estatisticas"))
|
| 500 |
+
df_modelo = _to_dataframe(dados_pacote.get("df_completo"))
|
| 501 |
+
if df_modelo is None:
|
| 502 |
+
df_modelo = _to_dataframe(dados_pacote.get("df"))
|
| 503 |
+
|
| 504 |
+
if resumo["autor"] is None:
|
| 505 |
+
resumo["autor"] = _autor_do_pacote(pacote)
|
| 506 |
+
|
| 507 |
+
if resumo["equacao"] is None:
|
| 508 |
+
resumo["equacao"] = _equacao_do_pacote(pacote)
|
| 509 |
+
|
| 510 |
+
if resumo["r2"] is None:
|
| 511 |
+
resumo["r2"] = _r2_do_pacote(pacote)
|
| 512 |
+
|
| 513 |
+
if resumo["total_dados"] is None and df_modelo is not None:
|
| 514 |
+
resumo["total_dados"] = int(len(df_modelo))
|
| 515 |
+
|
| 516 |
+
if not resumo["finalidades"] and df_modelo is not None:
|
| 517 |
+
resumo["finalidades"] = _extrair_finalidades(df_modelo)
|
| 518 |
+
|
| 519 |
+
if resumo["finalidade"] is None:
|
| 520 |
+
resumo["finalidade"] = resumo["finalidades"][0] if resumo["finalidades"] else None
|
| 521 |
+
|
| 522 |
+
if not resumo["bairros"] and df_modelo is not None:
|
| 523 |
+
resumo["bairros"] = _extrair_bairros(df_modelo)
|
| 524 |
+
|
| 525 |
+
faixas_por_campo = {
|
| 526 |
+
chave: _extrair_faixa_por_alias(estat_df, aliases)
|
| 527 |
+
for chave, aliases in RANGE_CAMPOS.items()
|
| 528 |
+
}
|
| 529 |
+
resumo["faixas_por_campo"] = faixas_por_campo
|
| 530 |
+
|
| 531 |
+
resumo["faixa_area"] = _merge_ranges(
|
| 532 |
+
resumo["faixa_area"],
|
| 533 |
+
_extrair_faixa_por_alias(estat_df, AREA_PRIVATIVA_ALIASES + AREA_TOTAL_ALIASES),
|
| 534 |
+
)
|
| 535 |
+
resumo["faixa_rh"] = _merge_ranges(resumo["faixa_rh"], _extrair_faixa_por_alias(estat_df, RH_ALIASES))
|
| 536 |
+
|
| 537 |
+
faixa_data_estat = _extrair_faixa_por_alias(estat_df, DATA_ALIASES)
|
| 538 |
+
if faixa_data_estat is None and df_modelo is not None:
|
| 539 |
+
faixa_data_estat = _extrair_faixa_data_dataframe(df_modelo)
|
| 540 |
+
resumo["faixa_data"] = _merge_ranges(resumo["faixa_data"], faixa_data_estat)
|
| 541 |
+
|
| 542 |
+
colunas_catalogo = _coletar_colunas_para_catalogo(estat_df, df_modelo)
|
| 543 |
+
resumo["compatibilidade_campos"] = _mapear_compatibilidade(colunas_catalogo)
|
| 544 |
+
resumo["variaveis_resumo"] = _resumo_variaveis(estat_df)
|
| 545 |
+
resumo["mapa_disponivel"] = _tem_colunas_mapa(df_modelo)
|
| 546 |
+
resumo["_texto_colunas_index"] = _indexar_texto_colunas(df_modelo)
|
| 547 |
+
resumo["_faixa_colunas_index"] = _indexar_faixas_colunas(df_modelo)
|
| 548 |
+
|
| 549 |
+
return resumo
|
| 550 |
+
|
| 551 |
+
|
| 552 |
+
def _autor_do_pacote(pacote: dict[str, Any]) -> str | None:
|
| 553 |
+
elaborador = pacote.get("elaborador")
|
| 554 |
+
if isinstance(elaborador, dict):
|
| 555 |
+
return _str_or_none(elaborador.get("nome_completo")) or _str_or_none(elaborador.get("nome"))
|
| 556 |
+
return None
|
| 557 |
+
|
| 558 |
+
|
| 559 |
+
def _equacao_do_pacote(pacote: dict[str, Any]) -> str | None:
|
| 560 |
+
modelo = pacote.get("modelo") if isinstance(pacote.get("modelo"), dict) else {}
|
| 561 |
+
diagnosticos = modelo.get("diagnosticos") if isinstance(modelo.get("diagnosticos"), dict) else {}
|
| 562 |
+
return _str_or_none(diagnosticos.get("equacao"))
|
| 563 |
+
|
| 564 |
+
|
| 565 |
+
def _r2_do_pacote(pacote: dict[str, Any]) -> float | None:
|
| 566 |
+
modelo = pacote.get("modelo") if isinstance(pacote.get("modelo"), dict) else {}
|
| 567 |
+
diagnosticos = modelo.get("diagnosticos") if isinstance(modelo.get("diagnosticos"), dict) else {}
|
| 568 |
+
gerais = diagnosticos.get("gerais") if isinstance(diagnosticos.get("gerais"), dict) else {}
|
| 569 |
+
return _to_float_or_none(gerais.get("r2"))
|
| 570 |
+
|
| 571 |
+
|
| 572 |
+
def _to_dataframe(value: Any) -> pd.DataFrame | None:
|
| 573 |
+
if value is None:
|
| 574 |
+
return None
|
| 575 |
+
if isinstance(value, pd.DataFrame):
|
| 576 |
+
return value.copy()
|
| 577 |
+
if isinstance(value, list):
|
| 578 |
+
try:
|
| 579 |
+
return pd.DataFrame(value)
|
| 580 |
+
except Exception:
|
| 581 |
+
return None
|
| 582 |
+
if isinstance(value, dict):
|
| 583 |
+
try:
|
| 584 |
+
return pd.DataFrame(value)
|
| 585 |
+
except Exception:
|
| 586 |
+
return None
|
| 587 |
+
return None
|
| 588 |
+
|
| 589 |
+
|
| 590 |
+
def _carregar_dataframe_modelo(caminho_modelo: Path) -> pd.DataFrame | None:
|
| 591 |
+
try:
|
| 592 |
+
pacote = load(caminho_modelo)
|
| 593 |
+
except Exception:
|
| 594 |
+
return None
|
| 595 |
+
|
| 596 |
+
if not isinstance(pacote, dict):
|
| 597 |
+
return None
|
| 598 |
+
|
| 599 |
+
if "versao" not in pacote:
|
| 600 |
+
try:
|
| 601 |
+
pacote = _migrar_pacote_v1_para_v2(pacote)
|
| 602 |
+
except Exception:
|
| 603 |
+
return None
|
| 604 |
+
|
| 605 |
+
dados = pacote.get("dados") if isinstance(pacote.get("dados"), dict) else {}
|
| 606 |
+
df_modelo = _to_dataframe(dados.get("df_completo"))
|
| 607 |
+
if df_modelo is None:
|
| 608 |
+
df_modelo = _to_dataframe(dados.get("df"))
|
| 609 |
+
return df_modelo
|
| 610 |
+
|
| 611 |
+
|
| 612 |
+
def _coletar_pontos_modelo(df_modelo: pd.DataFrame, limite_pontos: int) -> list[dict[str, float]]:
|
| 613 |
+
if df_modelo is None or df_modelo.empty:
|
| 614 |
+
return []
|
| 615 |
+
|
| 616 |
+
col_lat = _identificar_coluna_por_alias(df_modelo.columns, LAT_ALIASES)
|
| 617 |
+
col_lon = _identificar_coluna_por_alias(df_modelo.columns, LON_ALIASES)
|
| 618 |
+
if col_lat is None or col_lon is None:
|
| 619 |
+
return []
|
| 620 |
+
|
| 621 |
+
base = df_modelo[[col_lat, col_lon]].copy()
|
| 622 |
+
base[col_lat] = pd.to_numeric(base[col_lat], errors="coerce")
|
| 623 |
+
base[col_lon] = pd.to_numeric(base[col_lon], errors="coerce")
|
| 624 |
+
base = base.dropna(subset=[col_lat, col_lon])
|
| 625 |
+
if base.empty:
|
| 626 |
+
return []
|
| 627 |
+
|
| 628 |
+
base = base[(base[col_lat].between(-90, 90)) & (base[col_lon].between(-180, 180))]
|
| 629 |
+
if base.empty:
|
| 630 |
+
return []
|
| 631 |
+
|
| 632 |
+
if limite_pontos > 0 and len(base) > limite_pontos:
|
| 633 |
+
passo = max(1, math.ceil(len(base) / limite_pontos))
|
| 634 |
+
base = base.iloc[::passo].head(limite_pontos)
|
| 635 |
+
|
| 636 |
+
pontos: list[dict[str, float]] = []
|
| 637 |
+
for _, row in base.iterrows():
|
| 638 |
+
pontos.append({"lat": float(row[col_lat]), "lon": float(row[col_lon])})
|
| 639 |
+
return pontos
|
| 640 |
+
|
| 641 |
+
|
| 642 |
+
def _identificar_coluna_por_alias(colunas: Any, aliases: list[str]) -> str | None:
|
| 643 |
+
for coluna in colunas:
|
| 644 |
+
nome = str(coluna)
|
| 645 |
+
if _has_alias(nome, aliases):
|
| 646 |
+
return nome
|
| 647 |
+
return None
|
| 648 |
+
|
| 649 |
+
|
| 650 |
+
def _extrair_bairros(df: pd.DataFrame) -> list[str]:
|
| 651 |
+
candidatos = [col for col in df.columns if _has_alias(str(col), BAIRRO_ALIASES)]
|
| 652 |
+
bairros: set[str] = set()
|
| 653 |
+
|
| 654 |
+
for col in candidatos:
|
| 655 |
+
serie = df[col]
|
| 656 |
+
for valor in serie.dropna().head(5000):
|
| 657 |
+
texto = str(valor).strip()
|
| 658 |
+
if texto:
|
| 659 |
+
bairros.add(texto)
|
| 660 |
+
if len(bairros) >= 30:
|
| 661 |
+
break
|
| 662 |
+
|
| 663 |
+
return sorted(bairros)[:30]
|
| 664 |
+
|
| 665 |
+
|
| 666 |
+
def _extrair_finalidades(df: pd.DataFrame) -> list[str]:
|
| 667 |
+
candidatos = [col for col in df.columns if _has_alias(str(col), FINALIDADE_ALIASES)]
|
| 668 |
+
finalidades: list[str] = []
|
| 669 |
+
vistos = set()
|
| 670 |
+
|
| 671 |
+
for col in candidatos:
|
| 672 |
+
serie = df[col]
|
| 673 |
+
for valor in serie.dropna().head(5000):
|
| 674 |
+
texto = str(valor).strip()
|
| 675 |
+
chave = _normalize(texto)
|
| 676 |
+
if not texto or not chave or chave in vistos:
|
| 677 |
+
continue
|
| 678 |
+
vistos.add(chave)
|
| 679 |
+
finalidades.append(texto)
|
| 680 |
+
if len(finalidades) >= 20:
|
| 681 |
+
break
|
| 682 |
+
if len(finalidades) >= 20:
|
| 683 |
+
break
|
| 684 |
+
|
| 685 |
+
return finalidades
|
| 686 |
+
|
| 687 |
+
|
| 688 |
+
def _extrair_faixa_data_dataframe(df: pd.DataFrame) -> dict[str, Any] | None:
|
| 689 |
+
candidatos = [col for col in df.columns if _has_alias(str(col), DATA_ALIASES)]
|
| 690 |
+
for col in candidatos:
|
| 691 |
+
serie = pd.to_datetime(df[col], errors="coerce", dayfirst=True)
|
| 692 |
+
serie = serie.dropna()
|
| 693 |
+
if serie.empty:
|
| 694 |
+
continue
|
| 695 |
+
return {
|
| 696 |
+
"min": serie.min().date().isoformat(),
|
| 697 |
+
"max": serie.max().date().isoformat(),
|
| 698 |
+
}
|
| 699 |
+
return None
|
| 700 |
+
|
| 701 |
+
|
| 702 |
+
def _extrair_faixa_por_alias(estat_df: pd.DataFrame | None, aliases: list[str]) -> dict[str, Any] | None:
|
| 703 |
+
if estat_df is None or estat_df.empty:
|
| 704 |
+
return None
|
| 705 |
+
|
| 706 |
+
estat_indexado = estat_df.copy()
|
| 707 |
+
if "Variável" in estat_indexado.columns:
|
| 708 |
+
estat_indexado = estat_indexado.set_index("Variável")
|
| 709 |
+
|
| 710 |
+
if estat_indexado.empty:
|
| 711 |
+
return None
|
| 712 |
+
|
| 713 |
+
min_col = _buscar_coluna(estat_indexado.columns, ["minimo", "mínimo", "min"])
|
| 714 |
+
max_col = _buscar_coluna(estat_indexado.columns, ["maximo", "máximo", "max"])
|
| 715 |
+
if min_col is None or max_col is None:
|
| 716 |
+
return None
|
| 717 |
+
|
| 718 |
+
mins: list[Any] = []
|
| 719 |
+
maxs: list[Any] = []
|
| 720 |
+
|
| 721 |
+
for var, linha in estat_indexado.iterrows():
|
| 722 |
+
nome = str(var)
|
| 723 |
+
if not _has_alias(nome, aliases):
|
| 724 |
+
continue
|
| 725 |
+
min_val = linha.get(min_col)
|
| 726 |
+
max_val = linha.get(max_col)
|
| 727 |
+
if _is_empty(min_val) or _is_empty(max_val):
|
| 728 |
+
continue
|
| 729 |
+
mins.append(min_val)
|
| 730 |
+
maxs.append(max_val)
|
| 731 |
+
|
| 732 |
+
if not mins and not maxs:
|
| 733 |
+
return None
|
| 734 |
+
|
| 735 |
+
return {
|
| 736 |
+
"min": _min_value(mins),
|
| 737 |
+
"max": _max_value(maxs),
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
|
| 741 |
+
def _resumo_variaveis(estat_df: pd.DataFrame | None, limite: int = 12) -> list[dict[str, Any]]:
|
| 742 |
+
if estat_df is None or estat_df.empty:
|
| 743 |
+
return []
|
| 744 |
+
|
| 745 |
+
trabalho = estat_df.copy()
|
| 746 |
+
if "Variável" in trabalho.columns:
|
| 747 |
+
trabalho = trabalho.set_index("Variável")
|
| 748 |
+
if trabalho.empty:
|
| 749 |
+
return []
|
| 750 |
+
|
| 751 |
+
min_col = _buscar_coluna(trabalho.columns, ["minimo", "mínimo", "min"])
|
| 752 |
+
max_col = _buscar_coluna(trabalho.columns, ["maximo", "máximo", "max"])
|
| 753 |
+
if min_col is None or max_col is None:
|
| 754 |
+
return []
|
| 755 |
+
|
| 756 |
+
linhas: list[dict[str, Any]] = []
|
| 757 |
+
for var, linha in trabalho.iterrows():
|
| 758 |
+
min_val = linha.get(min_col)
|
| 759 |
+
max_val = linha.get(max_col)
|
| 760 |
+
if _is_empty(min_val) and _is_empty(max_val):
|
| 761 |
+
continue
|
| 762 |
+
linhas.append(
|
| 763 |
+
{
|
| 764 |
+
"variavel": str(var),
|
| 765 |
+
"min": sanitize_value(min_val),
|
| 766 |
+
"max": sanitize_value(max_val),
|
| 767 |
+
}
|
| 768 |
+
)
|
| 769 |
+
if len(linhas) >= limite:
|
| 770 |
+
break
|
| 771 |
+
|
| 772 |
+
return linhas
|
| 773 |
+
|
| 774 |
+
|
| 775 |
+
def _coletar_colunas_para_catalogo(estat_df: pd.DataFrame | None, df: pd.DataFrame | None) -> list[str]:
|
| 776 |
+
nomes: list[str] = []
|
| 777 |
+
if estat_df is not None and not estat_df.empty:
|
| 778 |
+
if "Variável" in estat_df.columns:
|
| 779 |
+
nomes.extend([str(v) for v in estat_df["Variável"].dropna().tolist()])
|
| 780 |
+
else:
|
| 781 |
+
nomes.extend([str(v) for v in estat_df.index.tolist()])
|
| 782 |
+
if df is not None and not df.empty:
|
| 783 |
+
nomes.extend([str(v) for v in df.columns.tolist()])
|
| 784 |
+
|
| 785 |
+
unicos = []
|
| 786 |
+
vistos = set()
|
| 787 |
+
for nome in nomes:
|
| 788 |
+
chave = _normalize(nome)
|
| 789 |
+
if not chave or chave in vistos:
|
| 790 |
+
continue
|
| 791 |
+
vistos.add(chave)
|
| 792 |
+
unicos.append(nome)
|
| 793 |
+
return unicos
|
| 794 |
+
|
| 795 |
+
|
| 796 |
+
def _mapear_compatibilidade(colunas: list[str]) -> dict[str, list[str]]:
|
| 797 |
+
out: dict[str, list[str]] = {}
|
| 798 |
+
for chave, aliases in COMPATIBILIDADE_MAP.items():
|
| 799 |
+
encontrados = [col for col in colunas if _has_alias(col, aliases)]
|
| 800 |
+
out[chave] = encontrados
|
| 801 |
+
return out
|
| 802 |
+
|
| 803 |
+
|
| 804 |
+
def _tem_colunas_mapa(df: pd.DataFrame | None) -> bool:
|
| 805 |
+
if df is None or df.empty:
|
| 806 |
+
return False
|
| 807 |
+
nomes = [str(col) for col in df.columns]
|
| 808 |
+
tem_lat = any(_has_alias(col, LAT_ALIASES) for col in nomes)
|
| 809 |
+
tem_lon = any(_has_alias(col, LON_ALIASES) for col in nomes)
|
| 810 |
+
return tem_lat and tem_lon
|
| 811 |
+
|
| 812 |
+
|
| 813 |
+
def _aceita_filtros(modelo: dict[str, Any], filtros: PesquisaFiltros) -> bool:
|
| 814 |
+
if filtros.nome and not _contains_any([modelo.get("nome_modelo"), modelo.get("arquivo")], filtros.nome):
|
| 815 |
+
return False
|
| 816 |
+
|
| 817 |
+
if filtros.autor and not _contains_any([modelo.get("autor")], filtros.autor):
|
| 818 |
+
return False
|
| 819 |
+
|
| 820 |
+
if filtros.finalidade and not _aceita_texto_com_colunas(modelo, filtros.finalidade, "finalidade", filtros.finalidade_colunas):
|
| 821 |
+
return False
|
| 822 |
+
|
| 823 |
+
if filtros.endereco and not _contains_any([modelo.get("endereco_referencia"), ", ".join(modelo.get("bairros") or [])], filtros.endereco):
|
| 824 |
+
return False
|
| 825 |
+
|
| 826 |
+
termos_bairro = _extrair_termos_bairro(filtros)
|
| 827 |
+
if termos_bairro:
|
| 828 |
+
for termo in termos_bairro:
|
| 829 |
+
if not _aceita_texto_com_colunas(modelo, termo, "bairros", filtros.bairros_colunas):
|
| 830 |
+
return False
|
| 831 |
+
|
| 832 |
+
if not _aceita_range_com_colunas(modelo, "area", filtros.area_colunas, filtros.area_min, filtros.area_max):
|
| 833 |
+
return False
|
| 834 |
+
|
| 835 |
+
if not _aceita_range_com_colunas(modelo, "rh", filtros.rh_colunas, filtros.rh_min, filtros.rh_max):
|
| 836 |
+
return False
|
| 837 |
+
|
| 838 |
+
if not _aceita_range_com_colunas(modelo, "data", filtros.data_colunas, filtros.data_min, filtros.data_max):
|
| 839 |
+
return False
|
| 840 |
+
|
| 841 |
+
return True
|
| 842 |
+
|
| 843 |
+
|
| 844 |
+
def _normalizar_otica(value: str | None) -> str:
|
| 845 |
+
return "avaliando" if _normalize(value or "") == "avaliando" else "modelo"
|
| 846 |
+
|
| 847 |
+
|
| 848 |
+
def _anexar_avaliando_info(modelo: dict[str, Any], filtros: PesquisaFiltros) -> dict[str, Any]:
|
| 849 |
+
item = dict(modelo)
|
| 850 |
+
checks: list[dict[str, Any]] = []
|
| 851 |
+
rejeicoes: list[str] = []
|
| 852 |
+
|
| 853 |
+
def registrar(campo: str, informado: Any, aceito: bool, detalhe: str) -> None:
|
| 854 |
+
check = {"campo": campo, "informado": sanitize_value(informado), "aceito": bool(aceito), "detalhe": detalhe}
|
| 855 |
+
checks.append(check)
|
| 856 |
+
if _is_provided(informado) and not aceito:
|
| 857 |
+
rejeicoes.append(f"{campo}: {detalhe}")
|
| 858 |
+
|
| 859 |
+
finalidade_info = filtros.aval_finalidade
|
| 860 |
+
if _is_provided(finalidade_info):
|
| 861 |
+
aceito = _aceita_texto_com_colunas(item, str(finalidade_info), "aval_finalidade", filtros.aval_finalidade_colunas)
|
| 862 |
+
registrar("finalidade", finalidade_info, aceito, "nao encontrada no modelo")
|
| 863 |
+
|
| 864 |
+
bairro_info = filtros.aval_bairro
|
| 865 |
+
if _is_provided(bairro_info):
|
| 866 |
+
aceito = _aceita_texto_com_colunas(item, str(bairro_info), "aval_bairro", filtros.aval_bairro_colunas)
|
| 867 |
+
registrar("bairro", bairro_info, aceito, "bairro fora da cobertura do modelo")
|
| 868 |
+
|
| 869 |
+
endereco_info = filtros.aval_endereco
|
| 870 |
+
if _is_provided(endereco_info):
|
| 871 |
+
candidatos = [item.get("endereco_referencia"), ", ".join(item.get("bairros") or [])]
|
| 872 |
+
aceito = _contains_any(candidatos, str(endereco_info))
|
| 873 |
+
registrar("endereco", endereco_info, aceito, "sem correspondencia textual no modelo")
|
| 874 |
+
|
| 875 |
+
faixa_data_ref = _faixa_resumo_com_colunas(item, "aval_data", filtros.aval_data_colunas)
|
| 876 |
+
registrar(
|
| 877 |
+
"data",
|
| 878 |
+
filtros.aval_data,
|
| 879 |
+
_aceita_valor_com_colunas(item, "aval_data", filtros.aval_data_colunas, filtros.aval_data),
|
| 880 |
+
f"fora da faixa {formatar_faixa(faixa_data_ref)}",
|
| 881 |
+
)
|
| 882 |
+
faixa_rh_ref = _faixa_resumo_com_colunas(item, "aval_rh", filtros.aval_rh_colunas)
|
| 883 |
+
registrar(
|
| 884 |
+
"rh",
|
| 885 |
+
filtros.aval_rh,
|
| 886 |
+
_aceita_valor_com_colunas(item, "aval_rh", filtros.aval_rh_colunas, filtros.aval_rh),
|
| 887 |
+
f"fora da faixa {formatar_faixa(faixa_rh_ref)}",
|
| 888 |
+
)
|
| 889 |
+
faixa_area_ref = _faixa_resumo_com_colunas(item, "aval_area", filtros.aval_area_colunas)
|
| 890 |
+
registrar(
|
| 891 |
+
"area",
|
| 892 |
+
filtros.aval_area,
|
| 893 |
+
_aceita_valor_com_colunas(item, "aval_area", filtros.aval_area_colunas, filtros.aval_area),
|
| 894 |
+
f"fora da faixa {formatar_faixa(faixa_area_ref)}",
|
| 895 |
+
)
|
| 896 |
+
|
| 897 |
+
faixa_area_priv = _faixa_resumo_com_colunas(item, "aval_area_privativa", filtros.aval_area_privativa_colunas)
|
| 898 |
+
faixa_area_total = _faixa_resumo_com_colunas(item, "aval_area_total", filtros.aval_area_total_colunas)
|
| 899 |
+
faixa_valor_unit = _faixa_resumo_com_colunas(item, "aval_valor_unitario", filtros.aval_valor_unitario_colunas)
|
| 900 |
+
faixa_valor_tot = _faixa_resumo_com_colunas(item, "aval_valor_total", filtros.aval_valor_total_colunas)
|
| 901 |
+
|
| 902 |
+
registrar(
|
| 903 |
+
"area_privativa",
|
| 904 |
+
filtros.aval_area_privativa,
|
| 905 |
+
_aceita_valor_com_colunas(item, "aval_area_privativa", filtros.aval_area_privativa_colunas, filtros.aval_area_privativa),
|
| 906 |
+
f"fora da faixa {formatar_faixa(faixa_area_priv)}",
|
| 907 |
+
)
|
| 908 |
+
registrar(
|
| 909 |
+
"area_total",
|
| 910 |
+
filtros.aval_area_total,
|
| 911 |
+
_aceita_valor_com_colunas(item, "aval_area_total", filtros.aval_area_total_colunas, filtros.aval_area_total),
|
| 912 |
+
f"fora da faixa {formatar_faixa(faixa_area_total)}",
|
| 913 |
+
)
|
| 914 |
+
registrar(
|
| 915 |
+
"valor_unitario",
|
| 916 |
+
filtros.aval_valor_unitario,
|
| 917 |
+
_aceita_valor_com_colunas(item, "aval_valor_unitario", filtros.aval_valor_unitario_colunas, filtros.aval_valor_unitario),
|
| 918 |
+
f"fora da faixa {formatar_faixa(faixa_valor_unit)}",
|
| 919 |
+
)
|
| 920 |
+
registrar(
|
| 921 |
+
"valor_total",
|
| 922 |
+
filtros.aval_valor_total,
|
| 923 |
+
_aceita_valor_com_colunas(item, "aval_valor_total", filtros.aval_valor_total_colunas, filtros.aval_valor_total),
|
| 924 |
+
f"fora da faixa {formatar_faixa(faixa_valor_tot)}",
|
| 925 |
+
)
|
| 926 |
+
|
| 927 |
+
checks_informados = [check for check in checks if _is_provided(check.get("informado"))]
|
| 928 |
+
aceito = all(check.get("aceito") for check in checks_informados) if checks_informados else True
|
| 929 |
+
|
| 930 |
+
item["avaliando"] = {
|
| 931 |
+
"aceito": bool(aceito),
|
| 932 |
+
"checks": checks,
|
| 933 |
+
"motivos_rejeicao": rejeicoes,
|
| 934 |
+
"campos_informados": len(checks_informados),
|
| 935 |
+
}
|
| 936 |
+
return item
|
| 937 |
+
|
| 938 |
+
|
| 939 |
+
def _extrair_sugestoes(modelos: list[dict[str, Any]], limite: int = 200) -> dict[str, list[str]]:
|
| 940 |
+
nomes: list[str] = []
|
| 941 |
+
autores: list[str] = []
|
| 942 |
+
finalidades: list[str] = []
|
| 943 |
+
bairros: list[str] = []
|
| 944 |
+
enderecos: list[str] = []
|
| 945 |
+
|
| 946 |
+
for modelo in modelos:
|
| 947 |
+
nomes.append(str(modelo.get("nome_modelo") or ""))
|
| 948 |
+
nomes.append(str(modelo.get("arquivo") or ""))
|
| 949 |
+
autores.append(str(modelo.get("autor") or ""))
|
| 950 |
+
finalidades.append(str(modelo.get("finalidade") or ""))
|
| 951 |
+
finalidades.extend([str(item) for item in (modelo.get("finalidades") or [])])
|
| 952 |
+
bairros.extend([str(item) for item in (modelo.get("bairros") or [])])
|
| 953 |
+
enderecos.append(str(modelo.get("endereco_referencia") or ""))
|
| 954 |
+
|
| 955 |
+
return {
|
| 956 |
+
"nomes_modelo": _lista_textos_unicos(nomes, limite),
|
| 957 |
+
"autores": _lista_textos_unicos(autores, limite),
|
| 958 |
+
"finalidades": _lista_textos_unicos(finalidades, limite),
|
| 959 |
+
"bairros": _lista_textos_unicos(bairros, limite),
|
| 960 |
+
"enderecos": _lista_textos_unicos(enderecos, limite),
|
| 961 |
+
}
|
| 962 |
+
|
| 963 |
+
|
| 964 |
+
def _modelo_publico(modelo: dict[str, Any]) -> dict[str, Any]:
|
| 965 |
+
return {chave: valor for chave, valor in modelo.items() if not str(chave).startswith("_")}
|
| 966 |
+
|
| 967 |
+
|
| 968 |
+
def _montar_config_colunas_filtro(modelos: list[dict[str, Any]]) -> dict[str, Any]:
|
| 969 |
+
todas_colunas_texto = set()
|
| 970 |
+
todas_colunas_faixa = set()
|
| 971 |
+
for modelo in modelos:
|
| 972 |
+
indice_texto = modelo.get("_texto_colunas_index") or {}
|
| 973 |
+
if isinstance(indice_texto, dict):
|
| 974 |
+
todas_colunas_texto.update([str(col) for col in indice_texto.keys()])
|
| 975 |
+
indice_faixa = modelo.get("_faixa_colunas_index") or {}
|
| 976 |
+
if isinstance(indice_faixa, dict):
|
| 977 |
+
todas_colunas_faixa.update([str(col) for col in indice_faixa.keys()])
|
| 978 |
+
|
| 979 |
+
colunas_texto_ordenadas = sorted(todas_colunas_texto, key=lambda item: item.lower())
|
| 980 |
+
colunas_faixa_ordenadas = sorted(todas_colunas_faixa, key=lambda item: item.lower())
|
| 981 |
+
config: dict[str, Any] = {}
|
| 982 |
+
|
| 983 |
+
campos = list(CAMPO_TEXTO_META_FONTES.keys()) + [campo for campo in CAMPO_FAIXA_META_FONTES.keys() if campo not in CAMPO_TEXTO_META_FONTES]
|
| 984 |
+
|
| 985 |
+
for campo in campos:
|
| 986 |
+
if campo in CAMPO_TEXTO_META_FONTES:
|
| 987 |
+
meta_fontes = list(CAMPO_TEXTO_META_FONTES.get(campo, []))
|
| 988 |
+
aliases = CAMPO_TEXTO_ALIASES_COLUNA.get(campo, [])
|
| 989 |
+
colunas_ordenadas = colunas_texto_ordenadas
|
| 990 |
+
else:
|
| 991 |
+
meta_fontes = list(CAMPO_FAIXA_META_FONTES.get(campo, []))
|
| 992 |
+
aliases = CAMPO_FAIXA_ALIASES_COLUNA.get(campo, [])
|
| 993 |
+
colunas_ordenadas = colunas_faixa_ordenadas
|
| 994 |
+
|
| 995 |
+
disponiveis = [{"id": fonte, "label": FONTE_META_LABELS.get(fonte, fonte)} for fonte in meta_fontes]
|
| 996 |
+
disponiveis.extend([{"id": f"col:{col}", "label": col} for col in colunas_ordenadas])
|
| 997 |
+
|
| 998 |
+
padrao = list(meta_fontes)
|
| 999 |
+
for col in colunas_ordenadas:
|
| 1000 |
+
if _has_alias(col, aliases):
|
| 1001 |
+
padrao.append(f"col:{col}")
|
| 1002 |
+
|
| 1003 |
+
config[campo] = {
|
| 1004 |
+
"disponiveis": disponiveis,
|
| 1005 |
+
"padrao": _dedupe_strings(padrao),
|
| 1006 |
+
}
|
| 1007 |
+
|
| 1008 |
+
return config
|
| 1009 |
+
|
| 1010 |
+
|
| 1011 |
+
def _dedupe_strings(values: list[str]) -> list[str]:
|
| 1012 |
+
out: list[str] = []
|
| 1013 |
+
seen = set()
|
| 1014 |
+
for value in values:
|
| 1015 |
+
text = str(value).strip()
|
| 1016 |
+
if not text or text in seen:
|
| 1017 |
+
continue
|
| 1018 |
+
seen.add(text)
|
| 1019 |
+
out.append(text)
|
| 1020 |
+
return out
|
| 1021 |
+
|
| 1022 |
+
|
| 1023 |
+
def _aceita_texto_com_colunas(modelo: dict[str, Any], consulta: str, campo: str, fontes_selecionadas: list[str] | None) -> bool:
|
| 1024 |
+
fontes = _resolver_fontes_campo(modelo, campo, fontes_selecionadas)
|
| 1025 |
+
candidatos = _valores_para_fontes(modelo, fontes)
|
| 1026 |
+
return _contains_any(candidatos, consulta)
|
| 1027 |
+
|
| 1028 |
+
|
| 1029 |
+
def _aceita_range_com_colunas(
|
| 1030 |
+
modelo: dict[str, Any],
|
| 1031 |
+
campo: str,
|
| 1032 |
+
fontes_selecionadas: list[str] | None,
|
| 1033 |
+
filtro_min: Any,
|
| 1034 |
+
filtro_max: Any,
|
| 1035 |
+
) -> bool:
|
| 1036 |
+
if filtro_min is None and filtro_max is None:
|
| 1037 |
+
return True
|
| 1038 |
+
faixas = _faixas_para_campo(modelo, campo, fontes_selecionadas)
|
| 1039 |
+
if not faixas:
|
| 1040 |
+
return False
|
| 1041 |
+
return any(_range_overlaps(faixa, filtro_min, filtro_max) for faixa in faixas)
|
| 1042 |
+
|
| 1043 |
+
|
| 1044 |
+
def _aceita_valor_com_colunas(
|
| 1045 |
+
modelo: dict[str, Any],
|
| 1046 |
+
campo: str,
|
| 1047 |
+
fontes_selecionadas: list[str] | None,
|
| 1048 |
+
valor: Any,
|
| 1049 |
+
) -> bool:
|
| 1050 |
+
if not _is_provided(valor):
|
| 1051 |
+
return True
|
| 1052 |
+
return _aceita_range_com_colunas(modelo, campo, fontes_selecionadas, valor, valor)
|
| 1053 |
+
|
| 1054 |
+
|
| 1055 |
+
def _faixa_resumo_com_colunas(modelo: dict[str, Any], campo: str, fontes_selecionadas: list[str] | None) -> dict[str, Any] | None:
|
| 1056 |
+
faixas = _faixas_para_campo(modelo, campo, fontes_selecionadas)
|
| 1057 |
+
return _combinar_faixas(faixas)
|
| 1058 |
+
|
| 1059 |
+
|
| 1060 |
+
def _faixas_para_campo(modelo: dict[str, Any], campo: str, fontes_selecionadas: list[str] | None) -> list[dict[str, Any]]:
|
| 1061 |
+
fontes = _resolver_fontes_faixa(modelo, campo, fontes_selecionadas)
|
| 1062 |
+
return _faixas_para_fontes(modelo, fontes)
|
| 1063 |
+
|
| 1064 |
+
|
| 1065 |
+
def _resolver_fontes_campo(modelo: dict[str, Any], campo: str, fontes_selecionadas: list[str] | None) -> list[str]:
|
| 1066 |
+
base = _dedupe_strings([str(item) for item in (fontes_selecionadas or [])])
|
| 1067 |
+
if base:
|
| 1068 |
+
return base
|
| 1069 |
+
|
| 1070 |
+
fontes = list(CAMPO_TEXTO_META_FONTES.get(campo, []))
|
| 1071 |
+
aliases = CAMPO_TEXTO_ALIASES_COLUNA.get(campo, [])
|
| 1072 |
+
indice = modelo.get("_texto_colunas_index") or {}
|
| 1073 |
+
if isinstance(indice, dict):
|
| 1074 |
+
for col in indice.keys():
|
| 1075 |
+
col_text = str(col)
|
| 1076 |
+
if _has_alias(col_text, aliases):
|
| 1077 |
+
fontes.append(f"col:{col_text}")
|
| 1078 |
+
return _dedupe_strings(fontes)
|
| 1079 |
+
|
| 1080 |
+
|
| 1081 |
+
def _resolver_fontes_faixa(modelo: dict[str, Any], campo: str, fontes_selecionadas: list[str] | None) -> list[str]:
|
| 1082 |
+
base = _dedupe_strings([str(item) for item in (fontes_selecionadas or [])])
|
| 1083 |
+
if base:
|
| 1084 |
+
return base
|
| 1085 |
+
|
| 1086 |
+
fontes = list(CAMPO_FAIXA_META_FONTES.get(campo, []))
|
| 1087 |
+
aliases = CAMPO_FAIXA_ALIASES_COLUNA.get(campo, [])
|
| 1088 |
+
indice = modelo.get("_faixa_colunas_index") or {}
|
| 1089 |
+
if isinstance(indice, dict):
|
| 1090 |
+
for col in indice.keys():
|
| 1091 |
+
col_text = str(col)
|
| 1092 |
+
if _has_alias(col_text, aliases):
|
| 1093 |
+
fontes.append(f"col:{col_text}")
|
| 1094 |
+
return _dedupe_strings(fontes)
|
| 1095 |
+
|
| 1096 |
+
|
| 1097 |
+
def _valores_para_fontes(modelo: dict[str, Any], fontes: list[str]) -> list[str]:
|
| 1098 |
+
candidatos: list[str] = []
|
| 1099 |
+
indice_colunas = modelo.get("_texto_colunas_index") or {}
|
| 1100 |
+
if not isinstance(indice_colunas, dict):
|
| 1101 |
+
indice_colunas = {}
|
| 1102 |
+
|
| 1103 |
+
for fonte in fontes:
|
| 1104 |
+
if fonte.startswith("meta:"):
|
| 1105 |
+
candidatos.extend(_valores_meta(modelo, fonte))
|
| 1106 |
+
continue
|
| 1107 |
+
if fonte.startswith("col:"):
|
| 1108 |
+
col = fonte[4:]
|
| 1109 |
+
valores = indice_colunas.get(col) or []
|
| 1110 |
+
candidatos.extend([str(item) for item in valores if _str_or_none(item)])
|
| 1111 |
+
|
| 1112 |
+
return candidatos
|
| 1113 |
+
|
| 1114 |
+
|
| 1115 |
+
def _valores_meta(modelo: dict[str, Any], fonte: str) -> list[str]:
|
| 1116 |
+
if fonte == "meta:nome_modelo":
|
| 1117 |
+
return [str(modelo.get("nome_modelo") or "")]
|
| 1118 |
+
if fonte == "meta:arquivo":
|
| 1119 |
+
return [str(modelo.get("arquivo") or "")]
|
| 1120 |
+
if fonte == "meta:autor":
|
| 1121 |
+
return [str(modelo.get("autor") or "")]
|
| 1122 |
+
if fonte == "meta:finalidade":
|
| 1123 |
+
return [str(modelo.get("finalidade") or "")]
|
| 1124 |
+
if fonte == "meta:tipo_imovel":
|
| 1125 |
+
return [str(modelo.get("tipo_imovel") or "")]
|
| 1126 |
+
if fonte == "meta:finalidades":
|
| 1127 |
+
return [str(item) for item in (modelo.get("finalidades") or [])]
|
| 1128 |
+
if fonte == "meta:bairros":
|
| 1129 |
+
return [str(item) for item in (modelo.get("bairros") or [])]
|
| 1130 |
+
if fonte == "meta:endereco_referencia":
|
| 1131 |
+
return [str(modelo.get("endereco_referencia") or "")]
|
| 1132 |
+
return []
|
| 1133 |
+
|
| 1134 |
+
|
| 1135 |
+
def _faixas_para_fontes(modelo: dict[str, Any], fontes: list[str]) -> list[dict[str, Any]]:
|
| 1136 |
+
candidatos: list[dict[str, Any]] = []
|
| 1137 |
+
indice_colunas = modelo.get("_faixa_colunas_index") or {}
|
| 1138 |
+
if not isinstance(indice_colunas, dict):
|
| 1139 |
+
indice_colunas = {}
|
| 1140 |
+
|
| 1141 |
+
for fonte in fontes:
|
| 1142 |
+
faixa: dict[str, Any] | None = None
|
| 1143 |
+
if fonte.startswith("meta:"):
|
| 1144 |
+
faixa = _faixa_meta(modelo, fonte)
|
| 1145 |
+
elif fonte.startswith("col:"):
|
| 1146 |
+
faixa = indice_colunas.get(fonte[4:])
|
| 1147 |
+
if not isinstance(faixa, dict):
|
| 1148 |
+
continue
|
| 1149 |
+
if _is_empty(faixa.get("min")) and _is_empty(faixa.get("max")):
|
| 1150 |
+
continue
|
| 1151 |
+
candidatos.append(faixa)
|
| 1152 |
+
|
| 1153 |
+
return candidatos
|
| 1154 |
+
|
| 1155 |
+
|
| 1156 |
+
def _faixa_meta(modelo: dict[str, Any], fonte: str) -> dict[str, Any] | None:
|
| 1157 |
+
if fonte == "meta:faixa_data":
|
| 1158 |
+
return modelo.get("faixa_data")
|
| 1159 |
+
if fonte == "meta:faixa_area":
|
| 1160 |
+
return modelo.get("faixa_area")
|
| 1161 |
+
if fonte == "meta:faixa_rh":
|
| 1162 |
+
return modelo.get("faixa_rh")
|
| 1163 |
+
|
| 1164 |
+
faixas_por_campo = modelo.get("faixas_por_campo") or {}
|
| 1165 |
+
if not isinstance(faixas_por_campo, dict):
|
| 1166 |
+
faixas_por_campo = {}
|
| 1167 |
+
|
| 1168 |
+
if fonte == "meta:faixa_area_privativa":
|
| 1169 |
+
return faixas_por_campo.get("area_privativa")
|
| 1170 |
+
if fonte == "meta:faixa_area_total":
|
| 1171 |
+
return faixas_por_campo.get("area_total")
|
| 1172 |
+
if fonte == "meta:faixa_valor_unitario":
|
| 1173 |
+
return faixas_por_campo.get("valor_unitario")
|
| 1174 |
+
if fonte == "meta:faixa_valor_total":
|
| 1175 |
+
return faixas_por_campo.get("valor_total")
|
| 1176 |
+
return None
|
| 1177 |
+
|
| 1178 |
+
|
| 1179 |
+
def _combinar_faixas(faixas: list[dict[str, Any]]) -> dict[str, Any] | None:
|
| 1180 |
+
kind: str | None = None
|
| 1181 |
+
min_vals: list[Any] = []
|
| 1182 |
+
max_vals: list[Any] = []
|
| 1183 |
+
|
| 1184 |
+
for faixa in faixas:
|
| 1185 |
+
cmp_min = _to_comparable(faixa.get("min"))
|
| 1186 |
+
cmp_max = _to_comparable(faixa.get("max"))
|
| 1187 |
+
faixa_kind = cmp_min[0] if cmp_min is not None else (cmp_max[0] if cmp_max is not None else None)
|
| 1188 |
+
if faixa_kind is None:
|
| 1189 |
+
continue
|
| 1190 |
+
if kind is None:
|
| 1191 |
+
kind = faixa_kind
|
| 1192 |
+
if faixa_kind != kind:
|
| 1193 |
+
continue
|
| 1194 |
+
if cmp_min is not None:
|
| 1195 |
+
min_vals.append(cmp_min[1])
|
| 1196 |
+
if cmp_max is not None:
|
| 1197 |
+
max_vals.append(cmp_max[1])
|
| 1198 |
+
|
| 1199 |
+
if not min_vals and not max_vals:
|
| 1200 |
+
return None
|
| 1201 |
+
|
| 1202 |
+
minimo = min(min_vals) if min_vals else None
|
| 1203 |
+
maximo = max(max_vals) if max_vals else None
|
| 1204 |
+
return {
|
| 1205 |
+
"min": _formatar_limite_faixa(minimo),
|
| 1206 |
+
"max": _formatar_limite_faixa(maximo),
|
| 1207 |
+
}
|
| 1208 |
+
|
| 1209 |
+
|
| 1210 |
+
def _formatar_limite_faixa(valor: Any) -> Any:
|
| 1211 |
+
if valor is None:
|
| 1212 |
+
return None
|
| 1213 |
+
if isinstance(valor, datetime):
|
| 1214 |
+
return valor.date().isoformat()
|
| 1215 |
+
return sanitize_value(valor)
|
| 1216 |
+
|
| 1217 |
+
|
| 1218 |
+
def _indexar_texto_colunas(df_modelo: pd.DataFrame | None) -> dict[str, list[str]]:
|
| 1219 |
+
if df_modelo is None or df_modelo.empty:
|
| 1220 |
+
return {}
|
| 1221 |
+
|
| 1222 |
+
indice: dict[str, list[str]] = {}
|
| 1223 |
+
colunas = [str(col) for col in df_modelo.columns[:MAX_COLUNAS_INDEXADAS]]
|
| 1224 |
+
base = df_modelo[colunas].head(MAX_LINHAS_INDEXACAO)
|
| 1225 |
+
|
| 1226 |
+
for coluna in colunas:
|
| 1227 |
+
serie = base[coluna]
|
| 1228 |
+
valores: list[str] = []
|
| 1229 |
+
vistos = set()
|
| 1230 |
+
for valor in serie.dropna().tolist():
|
| 1231 |
+
texto = _str_or_none(valor)
|
| 1232 |
+
if texto is None and isinstance(valor, (int, float)):
|
| 1233 |
+
texto = str(valor)
|
| 1234 |
+
if texto is None:
|
| 1235 |
+
continue
|
| 1236 |
+
chave = _normalize(texto)
|
| 1237 |
+
if not chave or chave in vistos:
|
| 1238 |
+
continue
|
| 1239 |
+
vistos.add(chave)
|
| 1240 |
+
valores.append(texto)
|
| 1241 |
+
if len(valores) >= MAX_VALORES_INDEXADOS_POR_COLUNA:
|
| 1242 |
+
break
|
| 1243 |
+
if valores:
|
| 1244 |
+
indice[coluna] = valores
|
| 1245 |
+
|
| 1246 |
+
return indice
|
| 1247 |
+
|
| 1248 |
+
|
| 1249 |
+
def _indexar_faixas_colunas(df_modelo: pd.DataFrame | None) -> dict[str, dict[str, Any]]:
|
| 1250 |
+
if df_modelo is None or df_modelo.empty:
|
| 1251 |
+
return {}
|
| 1252 |
+
|
| 1253 |
+
indice: dict[str, dict[str, Any]] = {}
|
| 1254 |
+
colunas = [str(col) for col in df_modelo.columns[:MAX_COLUNAS_INDEXADAS]]
|
| 1255 |
+
base = df_modelo[colunas].head(MAX_LINHAS_INDEXACAO)
|
| 1256 |
+
|
| 1257 |
+
for coluna in colunas:
|
| 1258 |
+
faixa = _extrair_faixa_serie(base[coluna])
|
| 1259 |
+
if faixa is not None:
|
| 1260 |
+
indice[coluna] = faixa
|
| 1261 |
+
|
| 1262 |
+
return indice
|
| 1263 |
+
|
| 1264 |
+
|
| 1265 |
+
def _extrair_faixa_serie(serie: pd.Series) -> dict[str, Any] | None:
|
| 1266 |
+
serie_limpa = serie.dropna()
|
| 1267 |
+
if serie_limpa.empty:
|
| 1268 |
+
return None
|
| 1269 |
+
|
| 1270 |
+
total = len(serie_limpa)
|
| 1271 |
+
minimo_amostras = min(3, total)
|
| 1272 |
+
|
| 1273 |
+
serie_num = pd.to_numeric(serie_limpa, errors="coerce").dropna()
|
| 1274 |
+
if len(serie_num) >= minimo_amostras and (len(serie_num) / total) >= 0.6:
|
| 1275 |
+
return {
|
| 1276 |
+
"min": sanitize_value(serie_num.min()),
|
| 1277 |
+
"max": sanitize_value(serie_num.max()),
|
| 1278 |
+
}
|
| 1279 |
+
|
| 1280 |
+
serie_data = pd.to_datetime(serie_limpa, errors="coerce", dayfirst=True).dropna()
|
| 1281 |
+
if len(serie_data) >= minimo_amostras and (len(serie_data) / total) >= 0.6:
|
| 1282 |
+
return {
|
| 1283 |
+
"min": serie_data.min().date().isoformat(),
|
| 1284 |
+
"max": serie_data.max().date().isoformat(),
|
| 1285 |
+
}
|
| 1286 |
+
|
| 1287 |
+
return None
|
| 1288 |
+
|
| 1289 |
+
|
| 1290 |
+
def _lista_textos_unicos(valores: list[str], limite: int) -> list[str]:
|
| 1291 |
+
unicos: list[str] = []
|
| 1292 |
+
vistos = set()
|
| 1293 |
+
for valor in valores:
|
| 1294 |
+
texto = _str_or_none(valor)
|
| 1295 |
+
if not texto:
|
| 1296 |
+
continue
|
| 1297 |
+
chave = _normalize(texto)
|
| 1298 |
+
if not chave or chave in vistos:
|
| 1299 |
+
continue
|
| 1300 |
+
vistos.add(chave)
|
| 1301 |
+
unicos.append(texto)
|
| 1302 |
+
if len(unicos) >= limite:
|
| 1303 |
+
break
|
| 1304 |
+
return sorted(unicos, key=lambda item: item.lower())
|
| 1305 |
+
|
| 1306 |
+
|
| 1307 |
+
def _aceita_valor_na_faixa(faixa: dict[str, Any] | None, valor: Any) -> bool:
|
| 1308 |
+
if not _is_provided(valor):
|
| 1309 |
+
return True
|
| 1310 |
+
return _range_overlaps(faixa, valor, valor)
|
| 1311 |
+
|
| 1312 |
+
|
| 1313 |
+
def formatar_faixa(faixa: dict[str, Any] | None) -> str:
|
| 1314 |
+
if not faixa:
|
| 1315 |
+
return "nao disponivel"
|
| 1316 |
+
minimo = faixa.get("min")
|
| 1317 |
+
maximo = faixa.get("max")
|
| 1318 |
+
if _is_empty(minimo) and _is_empty(maximo):
|
| 1319 |
+
return "nao disponivel"
|
| 1320 |
+
if not _is_empty(minimo) and not _is_empty(maximo):
|
| 1321 |
+
return f"{minimo} a {maximo}"
|
| 1322 |
+
if not _is_empty(minimo):
|
| 1323 |
+
return f"a partir de {minimo}"
|
| 1324 |
+
return f"ate {maximo}"
|
| 1325 |
+
|
| 1326 |
+
|
| 1327 |
+
def _extrair_termos_bairro(filtros: PesquisaFiltros) -> list[str]:
|
| 1328 |
+
termos: list[str] = []
|
| 1329 |
+
if filtros.bairro:
|
| 1330 |
+
termos.extend(_split_terms(filtros.bairro))
|
| 1331 |
+
if filtros.bairros:
|
| 1332 |
+
for entrada in filtros.bairros:
|
| 1333 |
+
termos.extend(_split_terms(entrada))
|
| 1334 |
+
|
| 1335 |
+
limpos = []
|
| 1336 |
+
vistos = set()
|
| 1337 |
+
for termo in termos:
|
| 1338 |
+
chave = _normalize(termo)
|
| 1339 |
+
if not chave or chave in vistos:
|
| 1340 |
+
continue
|
| 1341 |
+
vistos.add(chave)
|
| 1342 |
+
limpos.append(termo)
|
| 1343 |
+
return limpos
|
| 1344 |
+
|
| 1345 |
+
|
| 1346 |
+
def _split_terms(texto: str) -> list[str]:
|
| 1347 |
+
if not texto:
|
| 1348 |
+
return []
|
| 1349 |
+
partes = re.split(r"[,;|]", texto)
|
| 1350 |
+
return [parte.strip() for parte in partes if parte.strip()]
|
| 1351 |
+
|
| 1352 |
+
|
| 1353 |
+
def _range_overlaps(model_range: dict[str, Any] | None, filtro_min: Any, filtro_max: Any) -> bool:
|
| 1354 |
+
if filtro_min is None and filtro_max is None:
|
| 1355 |
+
return True
|
| 1356 |
+
|
| 1357 |
+
if not model_range:
|
| 1358 |
+
return False
|
| 1359 |
+
|
| 1360 |
+
model_min_cmp = _to_comparable(model_range.get("min"))
|
| 1361 |
+
model_max_cmp = _to_comparable(model_range.get("max"))
|
| 1362 |
+
filtro_min_cmp = _to_comparable(filtro_min) if filtro_min is not None else None
|
| 1363 |
+
filtro_max_cmp = _to_comparable(filtro_max) if filtro_max is not None else None
|
| 1364 |
+
|
| 1365 |
+
if filtro_min_cmp is None and filtro_max_cmp is None:
|
| 1366 |
+
return True
|
| 1367 |
+
|
| 1368 |
+
kinds = {
|
| 1369 |
+
item[0]
|
| 1370 |
+
for item in [model_min_cmp, model_max_cmp, filtro_min_cmp, filtro_max_cmp]
|
| 1371 |
+
if item is not None
|
| 1372 |
+
}
|
| 1373 |
+
|
| 1374 |
+
if len(kinds) != 1:
|
| 1375 |
+
return False
|
| 1376 |
+
|
| 1377 |
+
model_min_val = model_min_cmp[1] if model_min_cmp is not None else None
|
| 1378 |
+
model_max_val = model_max_cmp[1] if model_max_cmp is not None else None
|
| 1379 |
+
filtro_min_val = filtro_min_cmp[1] if filtro_min_cmp is not None else None
|
| 1380 |
+
filtro_max_val = filtro_max_cmp[1] if filtro_max_cmp is not None else None
|
| 1381 |
+
|
| 1382 |
+
if model_min_val is None and model_max_val is None:
|
| 1383 |
+
return False
|
| 1384 |
+
|
| 1385 |
+
if model_min_val is None:
|
| 1386 |
+
model_min_val = model_max_val
|
| 1387 |
+
if model_max_val is None:
|
| 1388 |
+
model_max_val = model_min_val
|
| 1389 |
+
|
| 1390 |
+
if filtro_min_val is not None and model_max_val < filtro_min_val:
|
| 1391 |
+
return False
|
| 1392 |
+
|
| 1393 |
+
if filtro_max_val is not None and model_min_val > filtro_max_val:
|
| 1394 |
+
return False
|
| 1395 |
+
|
| 1396 |
+
return True
|
| 1397 |
+
|
| 1398 |
+
|
| 1399 |
+
def _to_comparable(value: Any) -> tuple[str, Any] | None:
|
| 1400 |
+
if value is None:
|
| 1401 |
+
return None
|
| 1402 |
+
|
| 1403 |
+
if isinstance(value, (int, float)):
|
| 1404 |
+
if isinstance(value, float) and (math.isnan(value) or math.isinf(value)):
|
| 1405 |
+
return None
|
| 1406 |
+
return ("num", float(value))
|
| 1407 |
+
|
| 1408 |
+
if isinstance(value, pd.Timestamp):
|
| 1409 |
+
return ("dt", value.to_pydatetime())
|
| 1410 |
+
|
| 1411 |
+
if isinstance(value, datetime):
|
| 1412 |
+
return ("dt", value)
|
| 1413 |
+
|
| 1414 |
+
if isinstance(value, date):
|
| 1415 |
+
return ("dt", datetime(value.year, value.month, value.day))
|
| 1416 |
+
|
| 1417 |
+
texto = str(value).strip()
|
| 1418 |
+
if not texto:
|
| 1419 |
+
return None
|
| 1420 |
+
|
| 1421 |
+
numero = _to_float_or_none(texto)
|
| 1422 |
+
if numero is not None:
|
| 1423 |
+
return ("num", float(numero))
|
| 1424 |
+
|
| 1425 |
+
data_val = _parse_datetime(texto)
|
| 1426 |
+
if data_val is not None:
|
| 1427 |
+
return ("dt", data_val)
|
| 1428 |
+
|
| 1429 |
+
return None
|
| 1430 |
+
|
| 1431 |
+
|
| 1432 |
+
def _parse_datetime(texto: str) -> datetime | None:
|
| 1433 |
+
for fmt in ("%Y-%m-%d", "%d/%m/%Y", "%Y/%m/%d", "%Y%m%d", "%Y"):
|
| 1434 |
+
try:
|
| 1435 |
+
return datetime.strptime(texto, fmt)
|
| 1436 |
+
except Exception:
|
| 1437 |
+
continue
|
| 1438 |
+
return None
|
| 1439 |
+
|
| 1440 |
+
|
| 1441 |
+
def _contains_any(candidatos: list[Any], consulta: str) -> bool:
|
| 1442 |
+
alvo = _normalize(consulta)
|
| 1443 |
+
if not alvo:
|
| 1444 |
+
return True
|
| 1445 |
+
for item in candidatos:
|
| 1446 |
+
if item is None:
|
| 1447 |
+
continue
|
| 1448 |
+
if alvo in _normalize(str(item)):
|
| 1449 |
+
return True
|
| 1450 |
+
return False
|
| 1451 |
+
|
| 1452 |
+
|
| 1453 |
+
def _merge_ranges(preferencial: dict[str, Any] | None, fallback: dict[str, Any] | None) -> dict[str, Any] | None:
|
| 1454 |
+
if not preferencial and not fallback:
|
| 1455 |
+
return None
|
| 1456 |
+
if not preferencial:
|
| 1457 |
+
return fallback
|
| 1458 |
+
if not fallback:
|
| 1459 |
+
return preferencial
|
| 1460 |
+
|
| 1461 |
+
min_final = preferencial.get("min") if not _is_empty(preferencial.get("min")) else fallback.get("min")
|
| 1462 |
+
max_final = preferencial.get("max") if not _is_empty(preferencial.get("max")) else fallback.get("max")
|
| 1463 |
+
|
| 1464 |
+
if _is_empty(min_final) and _is_empty(max_final):
|
| 1465 |
+
return None
|
| 1466 |
+
|
| 1467 |
+
return {"min": sanitize_value(min_final), "max": sanitize_value(max_final)}
|
| 1468 |
+
|
| 1469 |
+
|
| 1470 |
+
def _buscar_coluna(colunas: Any, aliases: list[str]) -> Any:
|
| 1471 |
+
for coluna in colunas:
|
| 1472 |
+
if _has_alias(str(coluna), aliases):
|
| 1473 |
+
return coluna
|
| 1474 |
+
return None
|
| 1475 |
+
|
| 1476 |
+
|
| 1477 |
+
def _has_alias(nome: str, aliases: list[str]) -> bool:
|
| 1478 |
+
nome_norm = _normalize(nome)
|
| 1479 |
+
if not nome_norm:
|
| 1480 |
+
return False
|
| 1481 |
+
|
| 1482 |
+
for alias in aliases:
|
| 1483 |
+
alias_norm = _normalize(alias)
|
| 1484 |
+
if alias_norm == nome_norm or alias_norm in nome_norm:
|
| 1485 |
+
return True
|
| 1486 |
+
return False
|
| 1487 |
+
|
| 1488 |
+
|
| 1489 |
+
def _normalize(value: str) -> str:
|
| 1490 |
+
ascii_text = unicodedata.normalize("NFKD", str(value)).encode("ascii", "ignore").decode("ascii")
|
| 1491 |
+
ascii_text = ascii_text.lower().strip()
|
| 1492 |
+
return re.sub(r"[^a-z0-9]+", "", ascii_text)
|
| 1493 |
+
|
| 1494 |
+
|
| 1495 |
+
def _min_value(values: list[Any]) -> Any:
|
| 1496 |
+
comparaveis = [_to_comparable(v) for v in values]
|
| 1497 |
+
comparaveis = [c for c in comparaveis if c is not None]
|
| 1498 |
+
if not comparaveis:
|
| 1499 |
+
return sanitize_value(values[0]) if values else None
|
| 1500 |
+
kind = comparaveis[0][0]
|
| 1501 |
+
valores = [c[1] for c in comparaveis if c[0] == kind]
|
| 1502 |
+
menor = min(valores)
|
| 1503 |
+
if isinstance(menor, datetime):
|
| 1504 |
+
return menor.date().isoformat()
|
| 1505 |
+
return sanitize_value(menor)
|
| 1506 |
+
|
| 1507 |
+
|
| 1508 |
+
def _max_value(values: list[Any]) -> Any:
|
| 1509 |
+
comparaveis = [_to_comparable(v) for v in values]
|
| 1510 |
+
comparaveis = [c for c in comparaveis if c is not None]
|
| 1511 |
+
if not comparaveis:
|
| 1512 |
+
return sanitize_value(values[0]) if values else None
|
| 1513 |
+
kind = comparaveis[0][0]
|
| 1514 |
+
valores = [c[1] for c in comparaveis if c[0] == kind]
|
| 1515 |
+
maior = max(valores)
|
| 1516 |
+
if isinstance(maior, datetime):
|
| 1517 |
+
return maior.date().isoformat()
|
| 1518 |
+
return sanitize_value(maior)
|
| 1519 |
+
|
| 1520 |
+
|
| 1521 |
+
def _inferir_finalidade_por_nome(nome_arquivo: str) -> str | None:
|
| 1522 |
+
nome_upper = nome_arquivo.upper()
|
| 1523 |
+
if re.search(r"(^|_)A(_|$)", nome_upper) or "ALUG" in nome_upper:
|
| 1524 |
+
return "Aluguel"
|
| 1525 |
+
if re.search(r"(^|_)V(_|$)", nome_upper) or "VENDA" in nome_upper:
|
| 1526 |
+
return "Venda"
|
| 1527 |
+
return None
|
| 1528 |
+
|
| 1529 |
+
|
| 1530 |
+
def _inferir_tipo_por_nome(nome_arquivo: str) -> str | None:
|
| 1531 |
+
nome_upper = nome_arquivo.upper()
|
| 1532 |
+
tokens_ordenados = sorted(TIPO_POR_TOKEN.items(), key=lambda item: len(item[0]), reverse=True)
|
| 1533 |
+
for token, tipo in tokens_ordenados:
|
| 1534 |
+
if _contains_tipo_token(nome_upper, token):
|
| 1535 |
+
return tipo
|
| 1536 |
+
return None
|
| 1537 |
+
|
| 1538 |
+
|
| 1539 |
+
def _contains_tipo_token(nome_upper: str, token: str) -> bool:
|
| 1540 |
+
padrao = rf"(^|[^A-Z]){re.escape(token)}([^A-Z]|$)"
|
| 1541 |
+
return re.search(padrao, nome_upper) is not None
|
| 1542 |
+
|
| 1543 |
+
|
| 1544 |
+
def _to_float_or_none(value: Any) -> float | None:
|
| 1545 |
+
if value is None:
|
| 1546 |
+
return None
|
| 1547 |
+
|
| 1548 |
+
if isinstance(value, bool):
|
| 1549 |
+
return None
|
| 1550 |
+
|
| 1551 |
+
if isinstance(value, (int, float)):
|
| 1552 |
+
if isinstance(value, float) and (math.isnan(value) or math.isinf(value)):
|
| 1553 |
+
return None
|
| 1554 |
+
return float(value)
|
| 1555 |
+
|
| 1556 |
+
texto = str(value).strip()
|
| 1557 |
+
if not texto:
|
| 1558 |
+
return None
|
| 1559 |
+
|
| 1560 |
+
if "," in texto and "." in texto:
|
| 1561 |
+
if texto.rfind(",") > texto.rfind("."):
|
| 1562 |
+
texto = texto.replace(".", "").replace(",", ".")
|
| 1563 |
+
else:
|
| 1564 |
+
texto = texto.replace(",", "")
|
| 1565 |
+
else:
|
| 1566 |
+
texto = texto.replace(",", ".")
|
| 1567 |
+
|
| 1568 |
+
try:
|
| 1569 |
+
return float(texto)
|
| 1570 |
+
except Exception:
|
| 1571 |
+
return None
|
| 1572 |
+
|
| 1573 |
+
|
| 1574 |
+
def _str_or_none(value: Any) -> str | None:
|
| 1575 |
+
if value is None:
|
| 1576 |
+
return None
|
| 1577 |
+
texto = str(value).strip()
|
| 1578 |
+
return texto or None
|
| 1579 |
+
|
| 1580 |
+
|
| 1581 |
+
def _is_empty(value: Any) -> bool:
|
| 1582 |
+
if value is None:
|
| 1583 |
+
return True
|
| 1584 |
+
if isinstance(value, str) and not value.strip():
|
| 1585 |
+
return True
|
| 1586 |
+
try:
|
| 1587 |
+
if pd.isna(value):
|
| 1588 |
+
return True
|
| 1589 |
+
except Exception:
|
| 1590 |
+
pass
|
| 1591 |
+
return False
|
| 1592 |
+
|
| 1593 |
+
|
| 1594 |
+
def _is_provided(value: Any) -> bool:
|
| 1595 |
+
if value is None:
|
| 1596 |
+
return False
|
| 1597 |
+
if isinstance(value, str):
|
| 1598 |
+
return bool(value.strip())
|
| 1599 |
+
return True
|
frontend/src/App.jsx
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
import React, { useEffect, useState } from 'react'
|
| 2 |
import { api } from './api'
|
| 3 |
import ElaboracaoTab from './components/ElaboracaoTab'
|
|
|
|
| 4 |
import VisualizacaoTab from './components/VisualizacaoTab'
|
| 5 |
|
| 6 |
const TABS = [
|
| 7 |
-
{ key: 'Pesquisa', label: 'Pesquisa', hint: '
|
| 8 |
{ key: 'Elaboração/Edição', label: 'Elaboração/Edição', hint: 'Fluxo completo de modelagem' },
|
| 9 |
{ key: 'Visualização/Avaliação', label: 'Visualização/Avaliação', hint: 'Leitura e avaliação de modelos .dai' },
|
| 10 |
]
|
|
@@ -58,18 +59,9 @@ export default function App() {
|
|
| 58 |
{bootError ? <div className="error-line">Falha ao criar sessão: {bootError}</div> : null}
|
| 59 |
|
| 60 |
{activeTab === 'Pesquisa' ? (
|
| 61 |
-
<
|
| 62 |
-
<
|
| 63 |
-
|
| 64 |
-
<div className="section-title-wrap">
|
| 65 |
-
<h3>Pesquisa</h3>
|
| 66 |
-
<p>Este módulo segue em desenvolvimento no app original e foi mantido com o mesmo status.</p>
|
| 67 |
-
</div>
|
| 68 |
-
</header>
|
| 69 |
-
<div className="section-body">
|
| 70 |
-
<div className="empty-box">Aba disponível para expansão futura.</div>
|
| 71 |
-
</div>
|
| 72 |
-
</section>
|
| 73 |
) : null}
|
| 74 |
|
| 75 |
<div className="tab-pane" hidden={activeTab !== 'Elaboração/Edição'}>
|
|
|
|
| 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' },
|
| 11 |
]
|
|
|
|
| 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 />
|
| 64 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
) : null}
|
| 66 |
|
| 67 |
<div className="tab-pane" hidden={activeTab !== 'Elaboração/Edição'}>
|
frontend/src/api.js
CHANGED
|
@@ -59,6 +59,21 @@ export function downloadBlob(blob, fileName) {
|
|
| 59 |
export const api = {
|
| 60 |
createSession: () => postJson('/api/sessions', {}),
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
uploadElaboracaoFile(sessionId, file) {
|
| 63 |
const form = new FormData()
|
| 64 |
form.append('session_id', sessionId)
|
|
|
|
| 59 |
export const api = {
|
| 60 |
createSession: () => postJson('/api/sessions', {}),
|
| 61 |
|
| 62 |
+
pesquisarModelos(filtros = {}) {
|
| 63 |
+
const params = new URLSearchParams()
|
| 64 |
+
Object.entries(filtros).forEach(([key, value]) => {
|
| 65 |
+
if (value === null || value === undefined) return
|
| 66 |
+
const text = String(value).trim()
|
| 67 |
+
if (!text) return
|
| 68 |
+
params.append(key, text)
|
| 69 |
+
})
|
| 70 |
+
const query = params.toString()
|
| 71 |
+
return getJson(query ? `/api/pesquisa/modelos?${query}` : '/api/pesquisa/modelos')
|
| 72 |
+
},
|
| 73 |
+
pesquisarMapaModelos(modelosIds = []) {
|
| 74 |
+
return postJson('/api/pesquisa/mapa-modelos', { modelos_ids: modelosIds })
|
| 75 |
+
},
|
| 76 |
+
|
| 77 |
uploadElaboracaoFile(sessionId, file) {
|
| 78 |
const form = new FormData()
|
| 79 |
form.append('session_id', sessionId)
|
frontend/src/components/PesquisaTab.jsx
ADDED
|
@@ -0,0 +1,1113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
| 2 |
+
import { api } from '../api'
|
| 3 |
+
import MapFrame from './MapFrame'
|
| 4 |
+
import SectionBlock from './SectionBlock'
|
| 5 |
+
|
| 6 |
+
const EMPTY_FILTERS = {
|
| 7 |
+
otica: 'modelo',
|
| 8 |
+
nome: '',
|
| 9 |
+
autor: '',
|
| 10 |
+
finalidade: '',
|
| 11 |
+
bairros: '',
|
| 12 |
+
dataMin: '',
|
| 13 |
+
dataMax: '',
|
| 14 |
+
areaMin: '',
|
| 15 |
+
areaMax: '',
|
| 16 |
+
rhMin: '',
|
| 17 |
+
rhMax: '',
|
| 18 |
+
avalFinalidade: '',
|
| 19 |
+
avalBairro: '',
|
| 20 |
+
avalData: '',
|
| 21 |
+
avalArea: '',
|
| 22 |
+
avalAreaPrivativa: '',
|
| 23 |
+
avalAreaTotal: '',
|
| 24 |
+
avalRh: '',
|
| 25 |
+
avalValorUnitario: '',
|
| 26 |
+
avalValorTotal: '',
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
const RESULT_INITIAL = {
|
| 30 |
+
modelos: [],
|
| 31 |
+
sugestoes: {},
|
| 32 |
+
total_filtrado: 0,
|
| 33 |
+
total_geral: 0,
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
const CAMPOS_COLUNAS_FILTRO = [
|
| 37 |
+
'finalidade',
|
| 38 |
+
'bairros',
|
| 39 |
+
'data',
|
| 40 |
+
'area',
|
| 41 |
+
'rh',
|
| 42 |
+
'aval_finalidade',
|
| 43 |
+
'aval_bairro',
|
| 44 |
+
'aval_data',
|
| 45 |
+
'aval_area',
|
| 46 |
+
'aval_area_privativa',
|
| 47 |
+
'aval_area_total',
|
| 48 |
+
'aval_rh',
|
| 49 |
+
'aval_valor_unitario',
|
| 50 |
+
'aval_valor_total',
|
| 51 |
+
]
|
| 52 |
+
|
| 53 |
+
const COLUNAS_FILTRO_INITIAL = {
|
| 54 |
+
finalidade: [],
|
| 55 |
+
bairros: [],
|
| 56 |
+
data: [],
|
| 57 |
+
area: [],
|
| 58 |
+
rh: [],
|
| 59 |
+
aval_finalidade: [],
|
| 60 |
+
aval_bairro: [],
|
| 61 |
+
aval_data: [],
|
| 62 |
+
aval_area: [],
|
| 63 |
+
aval_area_privativa: [],
|
| 64 |
+
aval_area_total: [],
|
| 65 |
+
aval_rh: [],
|
| 66 |
+
aval_valor_unitario: [],
|
| 67 |
+
aval_valor_total: [],
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
const TIPO_SIGLAS = {
|
| 71 |
+
RECOND: 'Residencia em condominio',
|
| 72 |
+
RCOMD: 'Residencia em condominio',
|
| 73 |
+
TCOND: 'Terreno em condominio',
|
| 74 |
+
SALA: 'Salas comerciais',
|
| 75 |
+
APTO: 'Apartamentos residenciais',
|
| 76 |
+
APART: 'Apartamentos residenciais',
|
| 77 |
+
AP: 'Apartamentos residenciais',
|
| 78 |
+
TERRENO: 'Terrenos',
|
| 79 |
+
TER: 'Terrenos',
|
| 80 |
+
EDIF: 'Edificio',
|
| 81 |
+
RES: 'Residencias isoladas / casas',
|
| 82 |
+
CASA: 'Residencias isoladas / casas',
|
| 83 |
+
LOJA: 'Loja',
|
| 84 |
+
LCOM: 'Loja',
|
| 85 |
+
DEP: 'Deposito',
|
| 86 |
+
DEPOS: 'Deposito',
|
| 87 |
+
CCOM: 'Casa comercial',
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
function formatRange(range) {
|
| 91 |
+
if (!range) return '-'
|
| 92 |
+
const min = range.min ?? null
|
| 93 |
+
const max = range.max ?? null
|
| 94 |
+
if (min === null && max === null) return '-'
|
| 95 |
+
if (min !== null && max !== null) return `${min} a ${max}`
|
| 96 |
+
if (min !== null) return `a partir de ${min}`
|
| 97 |
+
return `ate ${max}`
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
function formatCount(value) {
|
| 101 |
+
if (value === null || value === undefined || value === '') return '-'
|
| 102 |
+
if (typeof value === 'number') {
|
| 103 |
+
return new Intl.NumberFormat('pt-BR').format(value)
|
| 104 |
+
}
|
| 105 |
+
return String(value)
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
function normalizeTokenText(value) {
|
| 109 |
+
return String(value || '')
|
| 110 |
+
.normalize('NFD')
|
| 111 |
+
.replace(/[\u0300-\u036f]/g, '')
|
| 112 |
+
.toUpperCase()
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
function inferTipoPorNomeModelo(...nomes) {
|
| 116 |
+
const tokens = Object.keys(TIPO_SIGLAS).sort((a, b) => b.length - a.length)
|
| 117 |
+
for (const nome of nomes) {
|
| 118 |
+
const source = normalizeTokenText(nome)
|
| 119 |
+
if (!source) continue
|
| 120 |
+
for (const token of tokens) {
|
| 121 |
+
const re = new RegExp(`(^|[^A-Z])${token}([^A-Z]|$)`)
|
| 122 |
+
if (re.test(source)) {
|
| 123 |
+
return TIPO_SIGLAS[token]
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
}
|
| 127 |
+
return ''
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
function formatTipoImovel(modelo) {
|
| 131 |
+
const tipoPorNome = inferTipoPorNomeModelo(modelo?.nome_modelo, modelo?.arquivo)
|
| 132 |
+
if (tipoPorNome) return tipoPorNome
|
| 133 |
+
|
| 134 |
+
const text = String(modelo?.tipo_imovel || '').trim()
|
| 135 |
+
if (!text) return '-'
|
| 136 |
+
|
| 137 |
+
const mapped = TIPO_SIGLAS[normalizeTokenText(text)]
|
| 138 |
+
return mapped || text
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
function normalizeColunasConfig(rawConfig = {}) {
|
| 142 |
+
const out = {}
|
| 143 |
+
CAMPOS_COLUNAS_FILTRO.forEach((campo) => {
|
| 144 |
+
const config = rawConfig?.[campo] || {}
|
| 145 |
+
const disponiveis = []
|
| 146 |
+
const vistos = new Set()
|
| 147 |
+
;(Array.isArray(config.disponiveis) ? config.disponiveis : []).forEach((item) => {
|
| 148 |
+
const id = typeof item === 'string' ? item : item?.id
|
| 149 |
+
const label = typeof item === 'string' ? item : item?.label || item?.id
|
| 150 |
+
const idText = String(id || '').trim()
|
| 151 |
+
if (!idText || vistos.has(idText)) return
|
| 152 |
+
vistos.add(idText)
|
| 153 |
+
disponiveis.push({ id: idText, label: String(label || idText) })
|
| 154 |
+
})
|
| 155 |
+
|
| 156 |
+
const padrao = []
|
| 157 |
+
;(Array.isArray(config.padrao) ? config.padrao : []).forEach((item) => {
|
| 158 |
+
const idText = String(item || '').trim()
|
| 159 |
+
if (!idText || !vistos.has(idText) || padrao.includes(idText)) return
|
| 160 |
+
padrao.push(idText)
|
| 161 |
+
})
|
| 162 |
+
|
| 163 |
+
out[campo] = { disponiveis, padrao }
|
| 164 |
+
})
|
| 165 |
+
return out
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
function reconciliarColunasSelecionadas(atual, configNormalizada, camposEditados = {}) {
|
| 169 |
+
const next = { ...COLUNAS_FILTRO_INITIAL, ...atual }
|
| 170 |
+
CAMPOS_COLUNAS_FILTRO.forEach((campo) => {
|
| 171 |
+
const configCampo = configNormalizada[campo] || { disponiveis: [], padrao: [] }
|
| 172 |
+
const idsDisponiveis = new Set((configCampo.disponiveis || []).map((item) => item.id))
|
| 173 |
+
const selecionadasValidas = (next[campo] || []).filter((id) => idsDisponiveis.has(id))
|
| 174 |
+
if (camposEditados[campo]) {
|
| 175 |
+
next[campo] = selecionadasValidas
|
| 176 |
+
return
|
| 177 |
+
}
|
| 178 |
+
const padraoValido = (configCampo.padrao || []).filter((id) => idsDisponiveis.has(id))
|
| 179 |
+
next[campo] = padraoValido.length ? padraoValido : selecionadasValidas
|
| 180 |
+
})
|
| 181 |
+
return next
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
function buildApiFilters(filters, colunasFiltro = COLUNAS_FILTRO_INITIAL) {
|
| 185 |
+
if (filters.otica === 'avaliando') {
|
| 186 |
+
return {
|
| 187 |
+
otica: filters.otica,
|
| 188 |
+
aval_finalidade: filters.avalFinalidade,
|
| 189 |
+
aval_finalidade_colunas: (colunasFiltro.aval_finalidade || []).join(','),
|
| 190 |
+
aval_bairro: filters.avalBairro,
|
| 191 |
+
aval_bairro_colunas: (colunasFiltro.aval_bairro || []).join(','),
|
| 192 |
+
aval_data: filters.avalData,
|
| 193 |
+
aval_data_colunas: (colunasFiltro.aval_data || []).join(','),
|
| 194 |
+
aval_area: filters.avalArea,
|
| 195 |
+
aval_area_colunas: (colunasFiltro.aval_area || []).join(','),
|
| 196 |
+
aval_area_privativa: filters.avalAreaPrivativa,
|
| 197 |
+
aval_area_privativa_colunas: (colunasFiltro.aval_area_privativa || []).join(','),
|
| 198 |
+
aval_area_total: filters.avalAreaTotal,
|
| 199 |
+
aval_area_total_colunas: (colunasFiltro.aval_area_total || []).join(','),
|
| 200 |
+
aval_rh: filters.avalRh,
|
| 201 |
+
aval_rh_colunas: (colunasFiltro.aval_rh || []).join(','),
|
| 202 |
+
aval_valor_unitario: filters.avalValorUnitario,
|
| 203 |
+
aval_valor_unitario_colunas: (colunasFiltro.aval_valor_unitario || []).join(','),
|
| 204 |
+
aval_valor_total: filters.avalValorTotal,
|
| 205 |
+
aval_valor_total_colunas: (colunasFiltro.aval_valor_total || []).join(','),
|
| 206 |
+
}
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
return {
|
| 210 |
+
otica: filters.otica,
|
| 211 |
+
nome: filters.nome,
|
| 212 |
+
autor: filters.autor,
|
| 213 |
+
finalidade: filters.finalidade,
|
| 214 |
+
finalidade_colunas: (colunasFiltro.finalidade || []).join(','),
|
| 215 |
+
bairros: filters.bairros,
|
| 216 |
+
bairros_colunas: (colunasFiltro.bairros || []).join(','),
|
| 217 |
+
data_min: filters.dataMin,
|
| 218 |
+
data_colunas: (colunasFiltro.data || []).join(','),
|
| 219 |
+
data_max: filters.dataMax,
|
| 220 |
+
area_min: filters.areaMin,
|
| 221 |
+
area_colunas: (colunasFiltro.area || []).join(','),
|
| 222 |
+
area_max: filters.areaMax,
|
| 223 |
+
rh_min: filters.rhMin,
|
| 224 |
+
rh_colunas: (colunasFiltro.rh || []).join(','),
|
| 225 |
+
rh_max: filters.rhMax,
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
function toInputName(field) {
|
| 230 |
+
let hash = 0
|
| 231 |
+
for (let i = 0; i < field.length; i += 1) {
|
| 232 |
+
hash = (hash * 31 + field.charCodeAt(i)) % 1000000007
|
| 233 |
+
}
|
| 234 |
+
return `mesa_${Math.abs(hash)}`
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
function TextFieldInput({ field, ...props }) {
|
| 238 |
+
return (
|
| 239 |
+
<input
|
| 240 |
+
{...props}
|
| 241 |
+
data-field={field}
|
| 242 |
+
name={toInputName(field)}
|
| 243 |
+
autoComplete="off"
|
| 244 |
+
autoCorrect="off"
|
| 245 |
+
autoCapitalize="none"
|
| 246 |
+
spellCheck={false}
|
| 247 |
+
/>
|
| 248 |
+
)
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
function NumberFieldInput({ field, ...props }) {
|
| 252 |
+
return (
|
| 253 |
+
<input
|
| 254 |
+
{...props}
|
| 255 |
+
type="number"
|
| 256 |
+
step="any"
|
| 257 |
+
inputMode="decimal"
|
| 258 |
+
data-field={field}
|
| 259 |
+
name={toInputName(field)}
|
| 260 |
+
autoComplete="off"
|
| 261 |
+
/>
|
| 262 |
+
)
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
function DynamicFilterField({
|
| 266 |
+
label,
|
| 267 |
+
campoValor,
|
| 268 |
+
campoColunas,
|
| 269 |
+
configCampo,
|
| 270 |
+
selecionadas,
|
| 271 |
+
onAddColuna,
|
| 272 |
+
onRemoveColuna,
|
| 273 |
+
value,
|
| 274 |
+
onChange,
|
| 275 |
+
list,
|
| 276 |
+
placeholder,
|
| 277 |
+
inputKind = 'text',
|
| 278 |
+
}) {
|
| 279 |
+
const disponiveis = configCampo?.disponiveis || []
|
| 280 |
+
const selectedSet = new Set(selecionadas || [])
|
| 281 |
+
const opcoesAdicionar = disponiveis.filter((item) => !selectedSet.has(item.id))
|
| 282 |
+
const InputComponent = inputKind === 'number' ? NumberFieldInput : TextFieldInput
|
| 283 |
+
|
| 284 |
+
function findLabel(id) {
|
| 285 |
+
const match = disponiveis.find((item) => item.id === id)
|
| 286 |
+
return match?.label || id
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
return (
|
| 290 |
+
<label className="pesquisa-field pesquisa-field-wide">
|
| 291 |
+
{label ? <span>{label}</span> : null}
|
| 292 |
+
<div className="pesquisa-dynamic-filter-row">
|
| 293 |
+
<div className="pesquisa-colunas-box">
|
| 294 |
+
<div className="pesquisa-colunas-chip-list">
|
| 295 |
+
{(selecionadas || []).map((id) => (
|
| 296 |
+
<span key={`${campoColunas}-${id}`} className="pesquisa-coluna-chip">
|
| 297 |
+
<span>{findLabel(id)}</span>
|
| 298 |
+
<button type="button" className="pesquisa-coluna-remove" onClick={() => onRemoveColuna(campoColunas, id)} aria-label={`Remover coluna ${findLabel(id)}`}>
|
| 299 |
+
x
|
| 300 |
+
</button>
|
| 301 |
+
</span>
|
| 302 |
+
))}
|
| 303 |
+
{!(selecionadas || []).length ? <span className="pesquisa-colunas-empty">Nenhuma coluna selecionada.</span> : null}
|
| 304 |
+
</div>
|
| 305 |
+
</div>
|
| 306 |
+
|
| 307 |
+
<select
|
| 308 |
+
className="pesquisa-colunas-add"
|
| 309 |
+
defaultValue=""
|
| 310 |
+
onChange={(event) => {
|
| 311 |
+
const selected = String(event.target.value || '').trim()
|
| 312 |
+
if (!selected) return
|
| 313 |
+
onAddColuna(campoColunas, selected)
|
| 314 |
+
event.target.value = ''
|
| 315 |
+
}}
|
| 316 |
+
>
|
| 317 |
+
<option value="">Adicionar coluna...</option>
|
| 318 |
+
{opcoesAdicionar.map((item) => (
|
| 319 |
+
<option key={`${campoColunas}-opt-${item.id}`} value={item.id}>{item.label}</option>
|
| 320 |
+
))}
|
| 321 |
+
</select>
|
| 322 |
+
|
| 323 |
+
<InputComponent
|
| 324 |
+
list={list}
|
| 325 |
+
field={campoValor}
|
| 326 |
+
value={value}
|
| 327 |
+
onChange={onChange}
|
| 328 |
+
placeholder={placeholder}
|
| 329 |
+
/>
|
| 330 |
+
</div>
|
| 331 |
+
</label>
|
| 332 |
+
)
|
| 333 |
+
}
|
| 334 |
+
|
| 335 |
+
function DynamicRangeFilterField({
|
| 336 |
+
label,
|
| 337 |
+
campoColunas,
|
| 338 |
+
configCampo,
|
| 339 |
+
selecionadas,
|
| 340 |
+
onAddColuna,
|
| 341 |
+
onRemoveColuna,
|
| 342 |
+
minLabel,
|
| 343 |
+
minField,
|
| 344 |
+
minValue,
|
| 345 |
+
maxLabel,
|
| 346 |
+
maxField,
|
| 347 |
+
maxValue,
|
| 348 |
+
onChange,
|
| 349 |
+
minPlaceholder,
|
| 350 |
+
maxPlaceholder,
|
| 351 |
+
inputKind = 'number',
|
| 352 |
+
}) {
|
| 353 |
+
const disponiveis = configCampo?.disponiveis || []
|
| 354 |
+
const selectedSet = new Set(selecionadas || [])
|
| 355 |
+
const opcoesAdicionar = disponiveis.filter((item) => !selectedSet.has(item.id))
|
| 356 |
+
const InputComponent = inputKind === 'number' ? NumberFieldInput : TextFieldInput
|
| 357 |
+
|
| 358 |
+
function findLabel(id) {
|
| 359 |
+
const match = disponiveis.find((item) => item.id === id)
|
| 360 |
+
return match?.label || id
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
return (
|
| 364 |
+
<div className="pesquisa-field pesquisa-field-wide">
|
| 365 |
+
{label ? <span>{label}</span> : null}
|
| 366 |
+
<div className="pesquisa-dynamic-filter-row pesquisa-dynamic-filter-row-range">
|
| 367 |
+
<div className="pesquisa-colunas-box">
|
| 368 |
+
<div className="pesquisa-colunas-chip-list">
|
| 369 |
+
{(selecionadas || []).map((id) => (
|
| 370 |
+
<span key={`${campoColunas}-${id}`} className="pesquisa-coluna-chip">
|
| 371 |
+
<span>{findLabel(id)}</span>
|
| 372 |
+
<button type="button" className="pesquisa-coluna-remove" onClick={() => onRemoveColuna(campoColunas, id)} aria-label={`Remover coluna ${findLabel(id)}`}>
|
| 373 |
+
x
|
| 374 |
+
</button>
|
| 375 |
+
</span>
|
| 376 |
+
))}
|
| 377 |
+
{!(selecionadas || []).length ? <span className="pesquisa-colunas-empty">Nenhuma coluna selecionada.</span> : null}
|
| 378 |
+
</div>
|
| 379 |
+
</div>
|
| 380 |
+
|
| 381 |
+
<select
|
| 382 |
+
className="pesquisa-colunas-add"
|
| 383 |
+
defaultValue=""
|
| 384 |
+
onChange={(event) => {
|
| 385 |
+
const selected = String(event.target.value || '').trim()
|
| 386 |
+
if (!selected) return
|
| 387 |
+
onAddColuna(campoColunas, selected)
|
| 388 |
+
event.target.value = ''
|
| 389 |
+
}}
|
| 390 |
+
>
|
| 391 |
+
<option value="">Adicionar coluna...</option>
|
| 392 |
+
{opcoesAdicionar.map((item) => (
|
| 393 |
+
<option key={`${campoColunas}-opt-${item.id}`} value={item.id}>{item.label}</option>
|
| 394 |
+
))}
|
| 395 |
+
</select>
|
| 396 |
+
</div>
|
| 397 |
+
|
| 398 |
+
<div className="pesquisa-range-values-row">
|
| 399 |
+
<label className="pesquisa-field">
|
| 400 |
+
{minLabel}
|
| 401 |
+
<InputComponent field={minField} value={minValue} onChange={onChange} placeholder={minPlaceholder} />
|
| 402 |
+
</label>
|
| 403 |
+
<label className="pesquisa-field">
|
| 404 |
+
{maxLabel}
|
| 405 |
+
<InputComponent field={maxField} value={maxValue} onChange={onChange} placeholder={maxPlaceholder} />
|
| 406 |
+
</label>
|
| 407 |
+
</div>
|
| 408 |
+
</div>
|
| 409 |
+
)
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
function FiltroGroup({ title, children }) {
|
| 413 |
+
return (
|
| 414 |
+
<section className="pesquisa-filtro-grupo">
|
| 415 |
+
<h5>{title}</h5>
|
| 416 |
+
<div className="pesquisa-filtros-grid">
|
| 417 |
+
{children}
|
| 418 |
+
</div>
|
| 419 |
+
</section>
|
| 420 |
+
)
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
export default function PesquisaTab() {
|
| 424 |
+
const [loading, setLoading] = useState(false)
|
| 425 |
+
const [error, setError] = useState('')
|
| 426 |
+
const [pesquisaInicializada, setPesquisaInicializada] = useState(false)
|
| 427 |
+
|
| 428 |
+
const [filters, setFilters] = useState(EMPTY_FILTERS)
|
| 429 |
+
const [result, setResult] = useState(RESULT_INITIAL)
|
| 430 |
+
|
| 431 |
+
const [selectedIds, setSelectedIds] = useState([])
|
| 432 |
+
const [detailModelId, setDetailModelId] = useState('')
|
| 433 |
+
const selectAllRef = useRef(null)
|
| 434 |
+
const [colunasConfig, setColunasConfig] = useState({})
|
| 435 |
+
const [colunasFiltro, setColunasFiltro] = useState(COLUNAS_FILTRO_INITIAL)
|
| 436 |
+
const [colunasEditadas, setColunasEditadas] = useState({})
|
| 437 |
+
|
| 438 |
+
const [mapaLoading, setMapaLoading] = useState(false)
|
| 439 |
+
const [mapaError, setMapaError] = useState('')
|
| 440 |
+
const [mapaStatus, setMapaStatus] = useState('')
|
| 441 |
+
const [mapaHtml, setMapaHtml] = useState('')
|
| 442 |
+
const [mapaLegendas, setMapaLegendas] = useState([])
|
| 443 |
+
|
| 444 |
+
const usandoOticaAvaliando = filters.otica === 'avaliando'
|
| 445 |
+
const sugestoes = result.sugestoes || {}
|
| 446 |
+
const resultIds = useMemo(() => (result.modelos || []).map((modelo) => modelo.id), [result.modelos])
|
| 447 |
+
const detalheModelo = useMemo(() => (result.modelos || []).find((modelo) => modelo.id === detailModelId) || null, [result.modelos, detailModelId])
|
| 448 |
+
const todosSelecionados = resultIds.length > 0 && resultIds.every((id) => selectedIds.includes(id))
|
| 449 |
+
const algunsSelecionados = resultIds.some((id) => selectedIds.includes(id))
|
| 450 |
+
|
| 451 |
+
async function buscarModelos(nextFilters = filters, nextColunasFiltro = colunasFiltro, nextColunasEditadas = colunasEditadas) {
|
| 452 |
+
setLoading(true)
|
| 453 |
+
setError('')
|
| 454 |
+
try {
|
| 455 |
+
const response = await api.pesquisarModelos(buildApiFilters(nextFilters, nextColunasFiltro))
|
| 456 |
+
const modelos = response.modelos || []
|
| 457 |
+
const idsNovos = new Set(modelos.map((item) => item.id))
|
| 458 |
+
const configNormalizada = normalizeColunasConfig(response.colunas_filtro || {})
|
| 459 |
+
|
| 460 |
+
setResult({
|
| 461 |
+
...RESULT_INITIAL,
|
| 462 |
+
...response,
|
| 463 |
+
modelos,
|
| 464 |
+
sugestoes: response.sugestoes || {},
|
| 465 |
+
})
|
| 466 |
+
setColunasConfig(configNormalizada)
|
| 467 |
+
setColunasFiltro((current) => reconciliarColunasSelecionadas(current, configNormalizada, nextColunasEditadas))
|
| 468 |
+
|
| 469 |
+
setSelectedIds((current) => current.filter((id) => idsNovos.has(id)))
|
| 470 |
+
|
| 471 |
+
setMapaHtml('')
|
| 472 |
+
setMapaStatus('')
|
| 473 |
+
setMapaLegendas([])
|
| 474 |
+
setMapaError('')
|
| 475 |
+
setPesquisaInicializada(true)
|
| 476 |
+
} catch (err) {
|
| 477 |
+
setError(err.message)
|
| 478 |
+
} finally {
|
| 479 |
+
setLoading(false)
|
| 480 |
+
}
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
async function carregarContextoInicial() {
|
| 484 |
+
setLoading(true)
|
| 485 |
+
setError('')
|
| 486 |
+
try {
|
| 487 |
+
const response = await api.pesquisarModelos({ somente_contexto: true })
|
| 488 |
+
const configNormalizada = normalizeColunasConfig(response.colunas_filtro || {})
|
| 489 |
+
|
| 490 |
+
setResult({
|
| 491 |
+
...RESULT_INITIAL,
|
| 492 |
+
...response,
|
| 493 |
+
modelos: [],
|
| 494 |
+
sugestoes: response.sugestoes || {},
|
| 495 |
+
})
|
| 496 |
+
setColunasConfig(configNormalizada)
|
| 497 |
+
setColunasFiltro((current) => reconciliarColunasSelecionadas(current, configNormalizada, {}))
|
| 498 |
+
setSelectedIds([])
|
| 499 |
+
setMapaHtml('')
|
| 500 |
+
setMapaStatus('')
|
| 501 |
+
setMapaLegendas([])
|
| 502 |
+
setMapaError('')
|
| 503 |
+
setPesquisaInicializada(false)
|
| 504 |
+
} catch (err) {
|
| 505 |
+
setError(err.message)
|
| 506 |
+
} finally {
|
| 507 |
+
setLoading(false)
|
| 508 |
+
}
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
useEffect(() => {
|
| 512 |
+
void carregarContextoInicial()
|
| 513 |
+
}, [])
|
| 514 |
+
|
| 515 |
+
useEffect(() => {
|
| 516 |
+
if (!selectAllRef.current) return
|
| 517 |
+
selectAllRef.current.indeterminate = algunsSelecionados && !todosSelecionados
|
| 518 |
+
}, [algunsSelecionados, todosSelecionados])
|
| 519 |
+
|
| 520 |
+
useEffect(() => {
|
| 521 |
+
if (!detailModelId) return
|
| 522 |
+
if (!detalheModelo) {
|
| 523 |
+
setDetailModelId('')
|
| 524 |
+
}
|
| 525 |
+
}, [detailModelId, detalheModelo])
|
| 526 |
+
|
| 527 |
+
useEffect(() => {
|
| 528 |
+
if (!detailModelId) return undefined
|
| 529 |
+
function onEsc(event) {
|
| 530 |
+
if (event.key === 'Escape') {
|
| 531 |
+
setDetailModelId('')
|
| 532 |
+
}
|
| 533 |
+
}
|
| 534 |
+
window.addEventListener('keydown', onEsc)
|
| 535 |
+
return () => window.removeEventListener('keydown', onEsc)
|
| 536 |
+
}, [detailModelId])
|
| 537 |
+
|
| 538 |
+
function onFieldChange(event) {
|
| 539 |
+
const { value, dataset, name } = event.target
|
| 540 |
+
const field = dataset.field || name
|
| 541 |
+
if (!field) return
|
| 542 |
+
setFilters((prev) => ({ ...prev, [field]: value }))
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
function onChangeOtica(otica) {
|
| 546 |
+
setFilters((prev) => ({ ...prev, otica }))
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
async function onLimparFiltros() {
|
| 550 |
+
setFilters(EMPTY_FILTERS)
|
| 551 |
+
setColunasEditadas({})
|
| 552 |
+
setColunasFiltro(COLUNAS_FILTRO_INITIAL)
|
| 553 |
+
await buscarModelos(EMPTY_FILTERS, COLUNAS_FILTRO_INITIAL, {})
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
function onToggleSelecionado(modelId) {
|
| 557 |
+
setSelectedIds((current) => {
|
| 558 |
+
if (current.includes(modelId)) {
|
| 559 |
+
return current.filter((id) => id !== modelId)
|
| 560 |
+
}
|
| 561 |
+
return [...current, modelId]
|
| 562 |
+
})
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
function onToggleSelecionarTodos() {
|
| 566 |
+
setSelectedIds((current) => {
|
| 567 |
+
if (todosSelecionados) {
|
| 568 |
+
const idsAtuais = new Set(resultIds)
|
| 569 |
+
return current.filter((id) => !idsAtuais.has(id))
|
| 570 |
+
}
|
| 571 |
+
const next = new Set(current)
|
| 572 |
+
resultIds.forEach((id) => next.add(id))
|
| 573 |
+
return Array.from(next)
|
| 574 |
+
})
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
function onAbrirDetalhes(modeloId) {
|
| 578 |
+
setDetailModelId(modeloId)
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
function onFecharDetalhes() {
|
| 582 |
+
setDetailModelId('')
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
function onAddColunaFiltro(campo, colunaId) {
|
| 586 |
+
setColunasFiltro((current) => {
|
| 587 |
+
const atual = current[campo] || []
|
| 588 |
+
if (atual.includes(colunaId)) return current
|
| 589 |
+
return { ...current, [campo]: [...atual, colunaId] }
|
| 590 |
+
})
|
| 591 |
+
setColunasEditadas((current) => ({ ...current, [campo]: true }))
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
function onRemoveColunaFiltro(campo, colunaId) {
|
| 595 |
+
setColunasFiltro((current) => ({
|
| 596 |
+
...current,
|
| 597 |
+
[campo]: (current[campo] || []).filter((item) => item !== colunaId),
|
| 598 |
+
}))
|
| 599 |
+
setColunasEditadas((current) => ({ ...current, [campo]: true }))
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
async function onGerarMapaSelecionados() {
|
| 603 |
+
if (!selectedIds.length) {
|
| 604 |
+
setMapaError('Selecione ao menos um modelo para plotar no mapa.')
|
| 605 |
+
return
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
setMapaLoading(true)
|
| 609 |
+
setMapaError('')
|
| 610 |
+
try {
|
| 611 |
+
const response = await api.pesquisarMapaModelos(selectedIds)
|
| 612 |
+
setMapaHtml(response.mapa_html || '')
|
| 613 |
+
setMapaStatus(response.status || '')
|
| 614 |
+
setMapaLegendas(response.modelos_plotados || [])
|
| 615 |
+
} catch (err) {
|
| 616 |
+
setMapaError(err.message)
|
| 617 |
+
} finally {
|
| 618 |
+
setMapaLoading(false)
|
| 619 |
+
}
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
return (
|
| 623 |
+
<div className="tab-content">
|
| 624 |
+
<SectionBlock step="1" title="Filtros de Pesquisa" subtitle="Use a otica do modelo ou do avaliando. Todos os filtros sao cumulativos.">
|
| 625 |
+
<div className="pesquisa-otica-switch" role="tablist" aria-label="Otica de pesquisa">
|
| 626 |
+
<button
|
| 627 |
+
type="button"
|
| 628 |
+
className={`pesquisa-otica-btn${!usandoOticaAvaliando ? ' active' : ''}`}
|
| 629 |
+
role="tab"
|
| 630 |
+
id="pesquisa-otica-modelo"
|
| 631 |
+
aria-selected={!usandoOticaAvaliando}
|
| 632 |
+
aria-controls="pesquisa-panel-modelo"
|
| 633 |
+
tabIndex={!usandoOticaAvaliando ? 0 : -1}
|
| 634 |
+
onClick={() => onChangeOtica('modelo')}
|
| 635 |
+
>
|
| 636 |
+
Otica do modelo
|
| 637 |
+
</button>
|
| 638 |
+
<button
|
| 639 |
+
type="button"
|
| 640 |
+
className={`pesquisa-otica-btn${usandoOticaAvaliando ? ' active' : ''}`}
|
| 641 |
+
role="tab"
|
| 642 |
+
id="pesquisa-otica-avaliando"
|
| 643 |
+
aria-selected={usandoOticaAvaliando}
|
| 644 |
+
aria-controls="pesquisa-panel-avaliando"
|
| 645 |
+
tabIndex={usandoOticaAvaliando ? 0 : -1}
|
| 646 |
+
onClick={() => onChangeOtica('avaliando')}
|
| 647 |
+
>
|
| 648 |
+
Otica do avaliando
|
| 649 |
+
</button>
|
| 650 |
+
</div>
|
| 651 |
+
|
| 652 |
+
{usandoOticaAvaliando ? (
|
| 653 |
+
<div id="pesquisa-panel-avaliando" role="tabpanel" aria-labelledby="pesquisa-otica-avaliando" className="pesquisa-filtros-groups">
|
| 654 |
+
<FiltroGroup title="Finalidade">
|
| 655 |
+
<DynamicFilterField
|
| 656 |
+
label="Finalidade do imovel"
|
| 657 |
+
campoValor="avalFinalidade"
|
| 658 |
+
campoColunas="aval_finalidade"
|
| 659 |
+
configCampo={colunasConfig.aval_finalidade}
|
| 660 |
+
selecionadas={colunasFiltro.aval_finalidade}
|
| 661 |
+
onAddColuna={onAddColunaFiltro}
|
| 662 |
+
onRemoveColuna={onRemoveColunaFiltro}
|
| 663 |
+
value={filters.avalFinalidade}
|
| 664 |
+
onChange={onFieldChange}
|
| 665 |
+
list="pesquisa-finalidades"
|
| 666 |
+
placeholder="Ex: Apartamento"
|
| 667 |
+
/>
|
| 668 |
+
</FiltroGroup>
|
| 669 |
+
|
| 670 |
+
<FiltroGroup title="Bairro">
|
| 671 |
+
<DynamicFilterField
|
| 672 |
+
label="Bairro do imovel"
|
| 673 |
+
campoValor="avalBairro"
|
| 674 |
+
campoColunas="aval_bairro"
|
| 675 |
+
configCampo={colunasConfig.aval_bairro}
|
| 676 |
+
selecionadas={colunasFiltro.aval_bairro}
|
| 677 |
+
onAddColuna={onAddColunaFiltro}
|
| 678 |
+
onRemoveColuna={onRemoveColunaFiltro}
|
| 679 |
+
value={filters.avalBairro}
|
| 680 |
+
onChange={onFieldChange}
|
| 681 |
+
list="pesquisa-bairros"
|
| 682 |
+
placeholder="Ex: Centro"
|
| 683 |
+
/>
|
| 684 |
+
</FiltroGroup>
|
| 685 |
+
|
| 686 |
+
<FiltroGroup title="Datas">
|
| 687 |
+
<DynamicFilterField
|
| 688 |
+
label="Data de referencia"
|
| 689 |
+
campoValor="avalData"
|
| 690 |
+
campoColunas="aval_data"
|
| 691 |
+
configCampo={colunasConfig.aval_data}
|
| 692 |
+
selecionadas={colunasFiltro.aval_data}
|
| 693 |
+
onAddColuna={onAddColunaFiltro}
|
| 694 |
+
onRemoveColuna={onRemoveColunaFiltro}
|
| 695 |
+
value={filters.avalData}
|
| 696 |
+
onChange={onFieldChange}
|
| 697 |
+
placeholder="2025-02-27"
|
| 698 |
+
/>
|
| 699 |
+
</FiltroGroup>
|
| 700 |
+
|
| 701 |
+
<FiltroGroup title="Areas">
|
| 702 |
+
<DynamicFilterField
|
| 703 |
+
label="Area (generica)"
|
| 704 |
+
campoValor="avalArea"
|
| 705 |
+
campoColunas="aval_area"
|
| 706 |
+
configCampo={colunasConfig.aval_area}
|
| 707 |
+
selecionadas={colunasFiltro.aval_area}
|
| 708 |
+
onAddColuna={onAddColunaFiltro}
|
| 709 |
+
onRemoveColuna={onRemoveColunaFiltro}
|
| 710 |
+
value={filters.avalArea}
|
| 711 |
+
onChange={onFieldChange}
|
| 712 |
+
placeholder="0"
|
| 713 |
+
inputKind="number"
|
| 714 |
+
/>
|
| 715 |
+
<DynamicFilterField
|
| 716 |
+
label="Area privativa"
|
| 717 |
+
campoValor="avalAreaPrivativa"
|
| 718 |
+
campoColunas="aval_area_privativa"
|
| 719 |
+
configCampo={colunasConfig.aval_area_privativa}
|
| 720 |
+
selecionadas={colunasFiltro.aval_area_privativa}
|
| 721 |
+
onAddColuna={onAddColunaFiltro}
|
| 722 |
+
onRemoveColuna={onRemoveColunaFiltro}
|
| 723 |
+
value={filters.avalAreaPrivativa}
|
| 724 |
+
onChange={onFieldChange}
|
| 725 |
+
placeholder="0"
|
| 726 |
+
inputKind="number"
|
| 727 |
+
/>
|
| 728 |
+
<DynamicFilterField
|
| 729 |
+
label="Area total"
|
| 730 |
+
campoValor="avalAreaTotal"
|
| 731 |
+
campoColunas="aval_area_total"
|
| 732 |
+
configCampo={colunasConfig.aval_area_total}
|
| 733 |
+
selecionadas={colunasFiltro.aval_area_total}
|
| 734 |
+
onAddColuna={onAddColunaFiltro}
|
| 735 |
+
onRemoveColuna={onRemoveColunaFiltro}
|
| 736 |
+
value={filters.avalAreaTotal}
|
| 737 |
+
onChange={onFieldChange}
|
| 738 |
+
placeholder="0"
|
| 739 |
+
inputKind="number"
|
| 740 |
+
/>
|
| 741 |
+
</FiltroGroup>
|
| 742 |
+
|
| 743 |
+
<FiltroGroup title="RH">
|
| 744 |
+
<DynamicFilterField
|
| 745 |
+
label="RH do imovel"
|
| 746 |
+
campoValor="avalRh"
|
| 747 |
+
campoColunas="aval_rh"
|
| 748 |
+
configCampo={colunasConfig.aval_rh}
|
| 749 |
+
selecionadas={colunasFiltro.aval_rh}
|
| 750 |
+
onAddColuna={onAddColunaFiltro}
|
| 751 |
+
onRemoveColuna={onRemoveColunaFiltro}
|
| 752 |
+
value={filters.avalRh}
|
| 753 |
+
onChange={onFieldChange}
|
| 754 |
+
placeholder="0"
|
| 755 |
+
inputKind="number"
|
| 756 |
+
/>
|
| 757 |
+
</FiltroGroup>
|
| 758 |
+
|
| 759 |
+
<FiltroGroup title="Valores">
|
| 760 |
+
<DynamicFilterField
|
| 761 |
+
label="Valor unitario"
|
| 762 |
+
campoValor="avalValorUnitario"
|
| 763 |
+
campoColunas="aval_valor_unitario"
|
| 764 |
+
configCampo={colunasConfig.aval_valor_unitario}
|
| 765 |
+
selecionadas={colunasFiltro.aval_valor_unitario}
|
| 766 |
+
onAddColuna={onAddColunaFiltro}
|
| 767 |
+
onRemoveColuna={onRemoveColunaFiltro}
|
| 768 |
+
value={filters.avalValorUnitario}
|
| 769 |
+
onChange={onFieldChange}
|
| 770 |
+
placeholder="0"
|
| 771 |
+
inputKind="number"
|
| 772 |
+
/>
|
| 773 |
+
<DynamicFilterField
|
| 774 |
+
label="Valor total"
|
| 775 |
+
campoValor="avalValorTotal"
|
| 776 |
+
campoColunas="aval_valor_total"
|
| 777 |
+
configCampo={colunasConfig.aval_valor_total}
|
| 778 |
+
selecionadas={colunasFiltro.aval_valor_total}
|
| 779 |
+
onAddColuna={onAddColunaFiltro}
|
| 780 |
+
onRemoveColuna={onRemoveColunaFiltro}
|
| 781 |
+
value={filters.avalValorTotal}
|
| 782 |
+
onChange={onFieldChange}
|
| 783 |
+
placeholder="0"
|
| 784 |
+
inputKind="number"
|
| 785 |
+
/>
|
| 786 |
+
</FiltroGroup>
|
| 787 |
+
</div>
|
| 788 |
+
) : (
|
| 789 |
+
<div id="pesquisa-panel-modelo" role="tabpanel" aria-labelledby="pesquisa-otica-modelo" className="pesquisa-filtros-groups pesquisa-filtros-groups-stack">
|
| 790 |
+
<FiltroGroup title="Modelo">
|
| 791 |
+
<label className="pesquisa-field">
|
| 792 |
+
Nome do modelo
|
| 793 |
+
<TextFieldInput
|
| 794 |
+
list="pesquisa-nomes-modelo"
|
| 795 |
+
field="nome"
|
| 796 |
+
value={filters.nome}
|
| 797 |
+
onChange={onFieldChange}
|
| 798 |
+
placeholder="Ex: MOD_A_SALA_Z1"
|
| 799 |
+
/>
|
| 800 |
+
</label>
|
| 801 |
+
<label className="pesquisa-field">
|
| 802 |
+
Autor
|
| 803 |
+
<TextFieldInput
|
| 804 |
+
list="pesquisa-autores"
|
| 805 |
+
field="autor"
|
| 806 |
+
value={filters.autor}
|
| 807 |
+
onChange={onFieldChange}
|
| 808 |
+
placeholder="Nome do avaliador"
|
| 809 |
+
/>
|
| 810 |
+
</label>
|
| 811 |
+
</FiltroGroup>
|
| 812 |
+
|
| 813 |
+
<FiltroGroup title="Finalidade">
|
| 814 |
+
<DynamicFilterField
|
| 815 |
+
label=""
|
| 816 |
+
campoValor="finalidade"
|
| 817 |
+
campoColunas="finalidade"
|
| 818 |
+
configCampo={colunasConfig.finalidade}
|
| 819 |
+
selecionadas={colunasFiltro.finalidade}
|
| 820 |
+
onAddColuna={onAddColunaFiltro}
|
| 821 |
+
onRemoveColuna={onRemoveColunaFiltro}
|
| 822 |
+
value={filters.finalidade}
|
| 823 |
+
onChange={onFieldChange}
|
| 824 |
+
list="pesquisa-finalidades"
|
| 825 |
+
placeholder="Apartamento, sala, deposito..."
|
| 826 |
+
/>
|
| 827 |
+
</FiltroGroup>
|
| 828 |
+
|
| 829 |
+
<FiltroGroup title="Bairro">
|
| 830 |
+
<DynamicFilterField
|
| 831 |
+
label=""
|
| 832 |
+
campoValor="bairros"
|
| 833 |
+
campoColunas="bairros"
|
| 834 |
+
configCampo={colunasConfig.bairros}
|
| 835 |
+
selecionadas={colunasFiltro.bairros}
|
| 836 |
+
onAddColuna={onAddColunaFiltro}
|
| 837 |
+
onRemoveColuna={onRemoveColunaFiltro}
|
| 838 |
+
value={filters.bairros}
|
| 839 |
+
onChange={onFieldChange}
|
| 840 |
+
list="pesquisa-bairros"
|
| 841 |
+
placeholder="Centro, Moinhos de Vento"
|
| 842 |
+
/>
|
| 843 |
+
</FiltroGroup>
|
| 844 |
+
|
| 845 |
+
<FiltroGroup title="Datas">
|
| 846 |
+
<DynamicRangeFilterField
|
| 847 |
+
label=""
|
| 848 |
+
campoColunas="data"
|
| 849 |
+
configCampo={colunasConfig.data}
|
| 850 |
+
selecionadas={colunasFiltro.data}
|
| 851 |
+
onAddColuna={onAddColunaFiltro}
|
| 852 |
+
onRemoveColuna={onRemoveColunaFiltro}
|
| 853 |
+
minLabel="Data minima"
|
| 854 |
+
minField="dataMin"
|
| 855 |
+
minValue={filters.dataMin}
|
| 856 |
+
maxLabel="Data maxima"
|
| 857 |
+
maxField="dataMax"
|
| 858 |
+
maxValue={filters.dataMax}
|
| 859 |
+
onChange={onFieldChange}
|
| 860 |
+
minPlaceholder="2022 ou 2022-01-01"
|
| 861 |
+
maxPlaceholder="2025 ou 2025-12-31"
|
| 862 |
+
inputKind="text"
|
| 863 |
+
/>
|
| 864 |
+
</FiltroGroup>
|
| 865 |
+
|
| 866 |
+
<FiltroGroup title="Areas">
|
| 867 |
+
<DynamicRangeFilterField
|
| 868 |
+
label=""
|
| 869 |
+
campoColunas="area"
|
| 870 |
+
configCampo={colunasConfig.area}
|
| 871 |
+
selecionadas={colunasFiltro.area}
|
| 872 |
+
onAddColuna={onAddColunaFiltro}
|
| 873 |
+
onRemoveColuna={onRemoveColunaFiltro}
|
| 874 |
+
minLabel="Area minima"
|
| 875 |
+
minField="areaMin"
|
| 876 |
+
minValue={filters.areaMin}
|
| 877 |
+
maxLabel="Area maxima"
|
| 878 |
+
maxField="areaMax"
|
| 879 |
+
maxValue={filters.areaMax}
|
| 880 |
+
onChange={onFieldChange}
|
| 881 |
+
minPlaceholder="0"
|
| 882 |
+
maxPlaceholder="0"
|
| 883 |
+
inputKind="number"
|
| 884 |
+
/>
|
| 885 |
+
</FiltroGroup>
|
| 886 |
+
|
| 887 |
+
<FiltroGroup title="RH">
|
| 888 |
+
<DynamicRangeFilterField
|
| 889 |
+
label=""
|
| 890 |
+
campoColunas="rh"
|
| 891 |
+
configCampo={colunasConfig.rh}
|
| 892 |
+
selecionadas={colunasFiltro.rh}
|
| 893 |
+
onAddColuna={onAddColunaFiltro}
|
| 894 |
+
onRemoveColuna={onRemoveColunaFiltro}
|
| 895 |
+
minLabel="RH minimo"
|
| 896 |
+
minField="rhMin"
|
| 897 |
+
minValue={filters.rhMin}
|
| 898 |
+
maxLabel="RH maximo"
|
| 899 |
+
maxField="rhMax"
|
| 900 |
+
maxValue={filters.rhMax}
|
| 901 |
+
onChange={onFieldChange}
|
| 902 |
+
minPlaceholder="0"
|
| 903 |
+
maxPlaceholder="1"
|
| 904 |
+
inputKind="number"
|
| 905 |
+
/>
|
| 906 |
+
</FiltroGroup>
|
| 907 |
+
</div>
|
| 908 |
+
)}
|
| 909 |
+
|
| 910 |
+
<datalist id="pesquisa-nomes-modelo">
|
| 911 |
+
{(sugestoes.nomes_modelo || []).map((item) => (
|
| 912 |
+
<option key={`nome-${item}`} value={item} />
|
| 913 |
+
))}
|
| 914 |
+
</datalist>
|
| 915 |
+
<datalist id="pesquisa-autores">
|
| 916 |
+
{(sugestoes.autores || []).map((item) => (
|
| 917 |
+
<option key={`autor-${item}`} value={item} />
|
| 918 |
+
))}
|
| 919 |
+
</datalist>
|
| 920 |
+
<datalist id="pesquisa-finalidades">
|
| 921 |
+
{(sugestoes.finalidades || []).map((item) => (
|
| 922 |
+
<option key={`finalidade-${item}`} value={item} />
|
| 923 |
+
))}
|
| 924 |
+
</datalist>
|
| 925 |
+
<datalist id="pesquisa-bairros">
|
| 926 |
+
{(sugestoes.bairros || []).map((item) => (
|
| 927 |
+
<option key={`bairro-${item}`} value={item} />
|
| 928 |
+
))}
|
| 929 |
+
</datalist>
|
| 930 |
+
|
| 931 |
+
<div className="row pesquisa-actions pesquisa-actions-primary">
|
| 932 |
+
<button type="button" onClick={() => void buscarModelos()} disabled={loading}>
|
| 933 |
+
{loading ? 'Pesquisando...' : 'Pesquisar'}
|
| 934 |
+
</button>
|
| 935 |
+
<button type="button" onClick={() => void onLimparFiltros()} disabled={loading}>
|
| 936 |
+
Limpar filtros
|
| 937 |
+
</button>
|
| 938 |
+
</div>
|
| 939 |
+
|
| 940 |
+
{error ? <div className="error-line inline-error">{error}</div> : null}
|
| 941 |
+
</SectionBlock>
|
| 942 |
+
|
| 943 |
+
<SectionBlock
|
| 944 |
+
step="2"
|
| 945 |
+
title="Resultados"
|
| 946 |
+
subtitle={
|
| 947 |
+
usandoOticaAvaliando
|
| 948 |
+
? 'Modelos aceitos para os parametros do avaliando informado.'
|
| 949 |
+
: 'Lista de modelos encontrados para os filtros atuais.'
|
| 950 |
+
}
|
| 951 |
+
>
|
| 952 |
+
<div className="pesquisa-results-toolbar">
|
| 953 |
+
<div className="pesquisa-summary-line">
|
| 954 |
+
<strong>{formatCount(result.total_filtrado)}</strong>{' '}
|
| 955 |
+
{usandoOticaAvaliando ? 'modelo(s) aceito(s)' : 'modelo(s) exibido(s)'} de <strong>{formatCount(result.total_geral)}</strong>.
|
| 956 |
+
</div>
|
| 957 |
+
{resultIds.length ? (
|
| 958 |
+
<label className="pesquisa-select-all">
|
| 959 |
+
<input ref={selectAllRef} type="checkbox" checked={todosSelecionados} onChange={onToggleSelecionarTodos} />
|
| 960 |
+
Selecionar todos os exibidos
|
| 961 |
+
</label>
|
| 962 |
+
) : null}
|
| 963 |
+
</div>
|
| 964 |
+
|
| 965 |
+
{!result.modelos?.length ? (
|
| 966 |
+
<div className="empty-box">
|
| 967 |
+
{!pesquisaInicializada
|
| 968 |
+
? 'Defina os filtros desejados e clique em Pesquisar.'
|
| 969 |
+
: usandoOticaAvaliando
|
| 970 |
+
? 'Nenhum modelo aceitou os parametros do avaliando informado.'
|
| 971 |
+
: 'Nenhum modelo encontrado com os filtros atuais.'}
|
| 972 |
+
</div>
|
| 973 |
+
) : (
|
| 974 |
+
<div className="pesquisa-card-grid">
|
| 975 |
+
{result.modelos.map((modelo) => {
|
| 976 |
+
const selecionado = selectedIds.includes(modelo.id)
|
| 977 |
+
return (
|
| 978 |
+
<article key={modelo.id} className={`pesquisa-card${selecionado ? ' is-selected' : ''}`}>
|
| 979 |
+
<div className="pesquisa-card-top">
|
| 980 |
+
<div className="pesquisa-card-head">
|
| 981 |
+
<div className="pesquisa-card-head-main">
|
| 982 |
+
<h4>{modelo.nome_modelo || modelo.arquivo}</h4>
|
| 983 |
+
<p>{modelo.arquivo}</p>
|
| 984 |
+
<div className="pesquisa-card-head-actions">
|
| 985 |
+
<label className="pesquisa-select-toggle">
|
| 986 |
+
<input
|
| 987 |
+
type="checkbox"
|
| 988 |
+
checked={selecionado}
|
| 989 |
+
onChange={() => onToggleSelecionado(modelo.id)}
|
| 990 |
+
/>
|
| 991 |
+
Selecionar
|
| 992 |
+
</label>
|
| 993 |
+
<button type="button" className="btn-pesquisa-expand" onClick={() => onAbrirDetalhes(modelo.id)}>
|
| 994 |
+
Ver mais
|
| 995 |
+
</button>
|
| 996 |
+
</div>
|
| 997 |
+
</div>
|
| 998 |
+
</div>
|
| 999 |
+
|
| 1000 |
+
{usandoOticaAvaliando ? (
|
| 1001 |
+
<div className="pesquisa-card-status-row">
|
| 1002 |
+
<div className="status-pill done">Aceito para o avaliando ({modelo.avaliando?.campos_informados || 0} campo(s) validado(s))</div>
|
| 1003 |
+
</div>
|
| 1004 |
+
) : null}
|
| 1005 |
+
<div className="pesquisa-card-body">
|
| 1006 |
+
<div className="pesquisa-card-dados-list">
|
| 1007 |
+
<div><strong>Finalidades no modelo:</strong> {(modelo.finalidades || []).length ? modelo.finalidades.join(', ') : '-'}</div>
|
| 1008 |
+
<div><strong>Tipo:</strong> {formatTipoImovel(modelo)}</div>
|
| 1009 |
+
<div><strong>Autor:</strong> {modelo.autor || '-'}</div>
|
| 1010 |
+
<div><strong>Dados:</strong> {formatCount(modelo.total_dados)}</div>
|
| 1011 |
+
<div><strong>Faixa area:</strong> {formatRange(modelo.faixa_area)}</div>
|
| 1012 |
+
<div><strong>Faixa RH:</strong> {formatRange(modelo.faixa_rh)}</div>
|
| 1013 |
+
<div><strong>Faixa data:</strong> {formatRange(modelo.faixa_data)}</div>
|
| 1014 |
+
<div><strong>Bairros:</strong> {(modelo.bairros || []).length ? (modelo.bairros || []).join(', ') : '-'}</div>
|
| 1015 |
+
</div>
|
| 1016 |
+
</div>
|
| 1017 |
+
</div>
|
| 1018 |
+
|
| 1019 |
+
{modelo.status !== 'ok' ? <div className="inline-error pesquisa-card-error">{modelo.erro_leitura || 'Falha ao ler modelo.'}</div> : null}
|
| 1020 |
+
</article>
|
| 1021 |
+
)
|
| 1022 |
+
})}
|
| 1023 |
+
</div>
|
| 1024 |
+
)}
|
| 1025 |
+
</SectionBlock>
|
| 1026 |
+
|
| 1027 |
+
<SectionBlock step="3" title="Mapa" subtitle="Plote os modelos selecionados com cores distintas e legenda.">
|
| 1028 |
+
<div className="pesquisa-summary-line">
|
| 1029 |
+
<strong>{formatCount(selectedIds.length)}</strong> modelo(s) selecionado(s) para plotagem.
|
| 1030 |
+
</div>
|
| 1031 |
+
|
| 1032 |
+
<div className="row pesquisa-actions">
|
| 1033 |
+
<button type="button" onClick={() => void onGerarMapaSelecionados()} disabled={mapaLoading || !selectedIds.length}>
|
| 1034 |
+
{mapaLoading ? 'Gerando mapa...' : 'Plotar mapa dos selecionados'}
|
| 1035 |
+
</button>
|
| 1036 |
+
</div>
|
| 1037 |
+
|
| 1038 |
+
{mapaStatus ? <div className="status-line">{mapaStatus}</div> : null}
|
| 1039 |
+
{mapaError ? <div className="error-line inline-error">{mapaError}</div> : null}
|
| 1040 |
+
|
| 1041 |
+
{mapaLegendas.length ? (
|
| 1042 |
+
<div className="pesquisa-legenda-grid">
|
| 1043 |
+
{mapaLegendas.map((item) => (
|
| 1044 |
+
<div key={item.id} className="pesquisa-legenda-item">
|
| 1045 |
+
<span className="pesquisa-legenda-color" style={{ backgroundColor: item.cor }} />
|
| 1046 |
+
<span>{item.nome} ({item.total_pontos})</span>
|
| 1047 |
+
</div>
|
| 1048 |
+
))}
|
| 1049 |
+
</div>
|
| 1050 |
+
) : null}
|
| 1051 |
+
|
| 1052 |
+
{mapaHtml ? <MapFrame html={mapaHtml} /> : <div className="empty-box">Nenhum mapa gerado ainda.</div>}
|
| 1053 |
+
</SectionBlock>
|
| 1054 |
+
|
| 1055 |
+
{detalheModelo ? (
|
| 1056 |
+
<div className="pesquisa-modal-backdrop" role="presentation" onClick={onFecharDetalhes}>
|
| 1057 |
+
<div className="pesquisa-modal" role="dialog" aria-modal="true" aria-labelledby={`pesquisa-modal-title-${detalheModelo.id}`} onClick={(event) => event.stopPropagation()}>
|
| 1058 |
+
<div className="pesquisa-modal-head">
|
| 1059 |
+
<div>
|
| 1060 |
+
<h4 id={`pesquisa-modal-title-${detalheModelo.id}`}>{detalheModelo.nome_modelo || detalheModelo.arquivo}</h4>
|
| 1061 |
+
<p>{detalheModelo.arquivo}</p>
|
| 1062 |
+
</div>
|
| 1063 |
+
<button type="button" className="pesquisa-modal-close" onClick={onFecharDetalhes}>
|
| 1064 |
+
Fechar
|
| 1065 |
+
</button>
|
| 1066 |
+
</div>
|
| 1067 |
+
|
| 1068 |
+
<div className="pesquisa-modal-body">
|
| 1069 |
+
<div className="pesquisa-card-kpis">
|
| 1070 |
+
<span><strong>Faixa area:</strong> {formatRange(detalheModelo.faixa_area)}</span>
|
| 1071 |
+
<span><strong>Faixa RH:</strong> {formatRange(detalheModelo.faixa_rh)}</span>
|
| 1072 |
+
<span><strong>Faixa data:</strong> {formatRange(detalheModelo.faixa_data)}</span>
|
| 1073 |
+
</div>
|
| 1074 |
+
|
| 1075 |
+
<div className="pesquisa-compare-block">
|
| 1076 |
+
<h5>Equacao</h5>
|
| 1077 |
+
<div className="equation-box">{detalheModelo.equacao || 'Nao disponivel no modelo.'}</div>
|
| 1078 |
+
</div>
|
| 1079 |
+
|
| 1080 |
+
<div className="pesquisa-compare-block">
|
| 1081 |
+
<h5>Faixas de variaveis (min/max)</h5>
|
| 1082 |
+
{detalheModelo.variaveis_resumo?.length ? (
|
| 1083 |
+
<div className="table-wrapper pesquisa-variaveis-table">
|
| 1084 |
+
<table>
|
| 1085 |
+
<thead>
|
| 1086 |
+
<tr>
|
| 1087 |
+
<th>Variavel</th>
|
| 1088 |
+
<th>Min</th>
|
| 1089 |
+
<th>Max</th>
|
| 1090 |
+
</tr>
|
| 1091 |
+
</thead>
|
| 1092 |
+
<tbody>
|
| 1093 |
+
{detalheModelo.variaveis_resumo.map((item) => (
|
| 1094 |
+
<tr key={`${detalheModelo.id}-${item.variavel}`}>
|
| 1095 |
+
<td>{item.variavel}</td>
|
| 1096 |
+
<td>{item.min ?? '-'}</td>
|
| 1097 |
+
<td>{item.max ?? '-'}</td>
|
| 1098 |
+
</tr>
|
| 1099 |
+
))}
|
| 1100 |
+
</tbody>
|
| 1101 |
+
</table>
|
| 1102 |
+
</div>
|
| 1103 |
+
) : (
|
| 1104 |
+
<div className="empty-box">Sem estatisticas suficientes para listar variaveis.</div>
|
| 1105 |
+
)}
|
| 1106 |
+
</div>
|
| 1107 |
+
</div>
|
| 1108 |
+
</div>
|
| 1109 |
+
</div>
|
| 1110 |
+
) : null}
|
| 1111 |
+
</div>
|
| 1112 |
+
)
|
| 1113 |
+
}
|
frontend/src/styles.css
CHANGED
|
@@ -392,6 +392,631 @@ textarea {
|
|
| 392 |
border-style: solid;
|
| 393 |
}
|
| 394 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
.row {
|
| 396 |
display: flex;
|
| 397 |
align-items: center;
|
|
@@ -1882,6 +2507,10 @@ button.btn-upload-select {
|
|
| 1882 |
grid-template-columns: 1.2fr 110px minmax(110px, 0.8fr) auto;
|
| 1883 |
}
|
| 1884 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1885 |
.micro-msg-grid-codigo {
|
| 1886 |
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 1887 |
}
|
|
@@ -1933,6 +2562,56 @@ button.btn-upload-select {
|
|
| 1933 |
grid-template-columns: 1fr;
|
| 1934 |
}
|
| 1935 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1936 |
.variavel-badge-line {
|
| 1937 |
grid-template-columns: 1fr;
|
| 1938 |
gap: 5px;
|
|
|
|
| 392 |
border-style: solid;
|
| 393 |
}
|
| 394 |
|
| 395 |
+
.pesquisa-filtros-groups {
|
| 396 |
+
display: grid;
|
| 397 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 398 |
+
gap: 22px;
|
| 399 |
+
margin-bottom: 12px;
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
.pesquisa-filtros-groups.pesquisa-filtros-groups-stack {
|
| 403 |
+
grid-template-columns: 1fr;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.pesquisa-filtro-grupo {
|
| 407 |
+
border: 1px solid #c4d6e8;
|
| 408 |
+
border-radius: 12px;
|
| 409 |
+
background: linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
|
| 410 |
+
padding: 10px 11px 11px;
|
| 411 |
+
box-shadow:
|
| 412 |
+
0 1px 0 rgba(255, 255, 255, 0.9),
|
| 413 |
+
inset 0 0 0 1px rgba(255, 255, 255, 0.74);
|
| 414 |
+
position: relative;
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
.pesquisa-filtro-grupo::before {
|
| 418 |
+
content: '';
|
| 419 |
+
position: absolute;
|
| 420 |
+
left: 0;
|
| 421 |
+
top: 0;
|
| 422 |
+
bottom: 0;
|
| 423 |
+
width: 4px;
|
| 424 |
+
border-radius: 12px 0 0 12px;
|
| 425 |
+
background: linear-gradient(180deg, #ff9a26 0%, #e67900 100%);
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
.pesquisa-filtro-grupo h5 {
|
| 429 |
+
margin: 0 0 9px;
|
| 430 |
+
padding-bottom: 6px;
|
| 431 |
+
border-bottom: 1px solid #d4e2ef;
|
| 432 |
+
color: #2f4b67;
|
| 433 |
+
font-size: 0.82rem;
|
| 434 |
+
font-family: 'Sora', sans-serif;
|
| 435 |
+
letter-spacing: 0.02em;
|
| 436 |
+
text-transform: uppercase;
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
.pesquisa-filtros-grid {
|
| 440 |
+
display: grid;
|
| 441 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 442 |
+
gap: 14px;
|
| 443 |
+
margin-bottom: 0;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
.pesquisa-otica-switch {
|
| 447 |
+
display: flex;
|
| 448 |
+
align-items: flex-end;
|
| 449 |
+
gap: 4px;
|
| 450 |
+
margin-bottom: 18px;
|
| 451 |
+
border-bottom: 2px solid #c9d9e8;
|
| 452 |
+
padding-bottom: 0;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
.pesquisa-otica-btn {
|
| 456 |
+
border: 1px solid #cfdbe7;
|
| 457 |
+
border-bottom-color: transparent;
|
| 458 |
+
border-radius: 9px 9px 0 0;
|
| 459 |
+
background: linear-gradient(180deg, #f3f7fb 0%, #eaf1f8 100%);
|
| 460 |
+
color: #6b7f93;
|
| 461 |
+
font-weight: 700;
|
| 462 |
+
padding: 9px 13px 8px;
|
| 463 |
+
box-shadow: none;
|
| 464 |
+
margin-bottom: -2px;
|
| 465 |
+
}
|
| 466 |
+
|
| 467 |
+
.pesquisa-otica-btn.active {
|
| 468 |
+
border-color: #cf6f00;
|
| 469 |
+
border-bottom-color: #cf6f00;
|
| 470 |
+
background: linear-gradient(180deg, #ff9a26 0%, #e67900 100%);
|
| 471 |
+
color: #ffffff;
|
| 472 |
+
box-shadow:
|
| 473 |
+
0 1px 0 rgba(255, 255, 255, 0.25) inset,
|
| 474 |
+
0 6px 14px rgba(230, 121, 0, 0.28);
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
button.pesquisa-otica-btn:hover {
|
| 478 |
+
transform: none;
|
| 479 |
+
box-shadow: none;
|
| 480 |
+
color: #445f78;
|
| 481 |
+
background: linear-gradient(180deg, #f7fbff 0%, #edf4fa 100%);
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
button.pesquisa-otica-btn.active:hover {
|
| 485 |
+
color: #ffffff;
|
| 486 |
+
background: linear-gradient(180deg, #ff9a26 0%, #e67900 100%);
|
| 487 |
+
box-shadow:
|
| 488 |
+
0 1px 0 rgba(255, 255, 255, 0.25) inset,
|
| 489 |
+
0 6px 14px rgba(230, 121, 0, 0.28);
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
.pesquisa-field {
|
| 493 |
+
display: grid;
|
| 494 |
+
gap: 7px;
|
| 495 |
+
font-size: 0.84rem;
|
| 496 |
+
align-content: start;
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.pesquisa-field-wide {
|
| 500 |
+
grid-column: 1 / -1;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
.pesquisa-field input {
|
| 504 |
+
width: 100%;
|
| 505 |
+
}
|
| 506 |
+
|
| 507 |
+
.pesquisa-field input::placeholder {
|
| 508 |
+
color: #b7c4d2;
|
| 509 |
+
opacity: 1;
|
| 510 |
+
}
|
| 511 |
+
|
| 512 |
+
.pesquisa-dynamic-filter-row {
|
| 513 |
+
display: grid;
|
| 514 |
+
grid-template-columns: minmax(0, 1.8fr) minmax(190px, 1fr) minmax(220px, 1.2fr);
|
| 515 |
+
gap: 10px;
|
| 516 |
+
align-items: start;
|
| 517 |
+
}
|
| 518 |
+
|
| 519 |
+
.pesquisa-dynamic-filter-row.pesquisa-dynamic-filter-row-range {
|
| 520 |
+
grid-template-columns: minmax(0, 1.8fr) minmax(190px, 1fr);
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
.pesquisa-colunas-box {
|
| 524 |
+
min-height: 38px;
|
| 525 |
+
border: 1px solid #cfdbe7;
|
| 526 |
+
border-radius: 10px;
|
| 527 |
+
background: #fff;
|
| 528 |
+
padding: 6px 8px;
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
.pesquisa-colunas-chip-list {
|
| 532 |
+
display: flex;
|
| 533 |
+
flex-wrap: wrap;
|
| 534 |
+
gap: 6px;
|
| 535 |
+
align-items: flex-start;
|
| 536 |
+
max-height: 110px;
|
| 537 |
+
overflow: auto;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
.pesquisa-coluna-chip {
|
| 541 |
+
display: inline-flex;
|
| 542 |
+
align-items: center;
|
| 543 |
+
gap: 5px;
|
| 544 |
+
border: 1px solid #d8e4ef;
|
| 545 |
+
border-radius: 999px;
|
| 546 |
+
background: #f6faff;
|
| 547 |
+
color: #486179;
|
| 548 |
+
font-size: 0.74rem;
|
| 549 |
+
line-height: 1.2;
|
| 550 |
+
padding: 3px 8px;
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
button.pesquisa-coluna-remove {
|
| 554 |
+
border: none;
|
| 555 |
+
background: transparent;
|
| 556 |
+
color: #5f768c;
|
| 557 |
+
font-weight: 800;
|
| 558 |
+
padding: 0;
|
| 559 |
+
min-height: auto;
|
| 560 |
+
box-shadow: none;
|
| 561 |
+
line-height: 1;
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
button.pesquisa-coluna-remove:hover {
|
| 565 |
+
transform: none;
|
| 566 |
+
box-shadow: none;
|
| 567 |
+
color: #304b63;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
.pesquisa-colunas-empty {
|
| 571 |
+
color: #7c90a4;
|
| 572 |
+
font-size: 0.75rem;
|
| 573 |
+
font-style: italic;
|
| 574 |
+
padding: 2px 0;
|
| 575 |
+
}
|
| 576 |
+
|
| 577 |
+
.pesquisa-colunas-add {
|
| 578 |
+
width: 100%;
|
| 579 |
+
min-height: 38px;
|
| 580 |
+
padding: 6px 8px;
|
| 581 |
+
font-size: 0.77rem;
|
| 582 |
+
background: #fbfdff;
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
.pesquisa-range-row {
|
| 586 |
+
grid-column: 1 / -1;
|
| 587 |
+
display: grid;
|
| 588 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 589 |
+
gap: 16px;
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
.pesquisa-range-values-row {
|
| 593 |
+
display: grid;
|
| 594 |
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 595 |
+
gap: 16px;
|
| 596 |
+
margin-top: 10px;
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
.pesquisa-range-row.pesquisa-range-row-three {
|
| 600 |
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
.pesquisa-actions {
|
| 604 |
+
margin-top: 2px;
|
| 605 |
+
margin-bottom: 10px;
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
.pesquisa-actions-primary {
|
| 609 |
+
margin-top: 22px;
|
| 610 |
+
margin-bottom: 14px;
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
.btn-pesquisa-expand {
|
| 614 |
+
--btn-bg-start: #f7fbff;
|
| 615 |
+
--btn-bg-end: #ecf3fa;
|
| 616 |
+
--btn-border: #c7d7e6;
|
| 617 |
+
--btn-shadow-soft: rgba(73, 102, 128, 0.08);
|
| 618 |
+
--btn-shadow-strong: rgba(73, 102, 128, 0.13);
|
| 619 |
+
color: #415c74;
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
.pesquisa-status {
|
| 623 |
+
margin-top: 2px;
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
.pesquisa-summary-line {
|
| 627 |
+
margin-bottom: 10px;
|
| 628 |
+
color: #4f657a;
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
.pesquisa-results-toolbar {
|
| 632 |
+
display: flex;
|
| 633 |
+
align-items: center;
|
| 634 |
+
justify-content: space-between;
|
| 635 |
+
flex-wrap: wrap;
|
| 636 |
+
gap: 10px;
|
| 637 |
+
margin-bottom: 10px;
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
.pesquisa-results-toolbar .pesquisa-summary-line {
|
| 641 |
+
margin: 0;
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
.pesquisa-select-all {
|
| 645 |
+
display: inline-flex;
|
| 646 |
+
align-items: center;
|
| 647 |
+
gap: 8px;
|
| 648 |
+
border: 1px solid #d9e4ef;
|
| 649 |
+
border-radius: 999px;
|
| 650 |
+
background: #f7fbff;
|
| 651 |
+
color: #3f566b;
|
| 652 |
+
font-size: 0.82rem;
|
| 653 |
+
font-weight: 700;
|
| 654 |
+
padding: 6px 10px;
|
| 655 |
+
white-space: nowrap;
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
.pesquisa-select-all input {
|
| 659 |
+
margin: 0;
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
.pesquisa-card-grid {
|
| 663 |
+
display: grid;
|
| 664 |
+
grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
|
| 665 |
+
gap: 12px;
|
| 666 |
+
min-width: 0;
|
| 667 |
+
align-items: stretch;
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
.pesquisa-card {
|
| 671 |
+
border: 1px solid #dbe7f2;
|
| 672 |
+
border-radius: 14px;
|
| 673 |
+
background: linear-gradient(180deg, #ffffff 0%, #fcfdff 100%);
|
| 674 |
+
padding: 12px;
|
| 675 |
+
display: grid;
|
| 676 |
+
gap: 10px;
|
| 677 |
+
min-width: 0;
|
| 678 |
+
height: 100%;
|
| 679 |
+
overflow: hidden;
|
| 680 |
+
box-shadow:
|
| 681 |
+
0 6px 18px rgba(26, 43, 61, 0.06),
|
| 682 |
+
inset 0 0 0 1px rgba(255, 255, 255, 0.75);
|
| 683 |
+
transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
|
| 684 |
+
}
|
| 685 |
+
|
| 686 |
+
.pesquisa-card:hover {
|
| 687 |
+
transform: translateY(-1px);
|
| 688 |
+
border-color: #c8dced;
|
| 689 |
+
box-shadow:
|
| 690 |
+
0 10px 22px rgba(26, 43, 61, 0.08),
|
| 691 |
+
inset 0 0 0 1px rgba(255, 255, 255, 0.82);
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
.pesquisa-card.is-selected {
|
| 695 |
+
border-color: #ffbe77;
|
| 696 |
+
box-shadow:
|
| 697 |
+
0 10px 24px rgba(255, 163, 63, 0.17),
|
| 698 |
+
0 0 0 1px rgba(255, 163, 63, 0.28);
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
.pesquisa-card-top {
|
| 702 |
+
display: grid;
|
| 703 |
+
gap: 8px;
|
| 704 |
+
min-width: 0;
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
.pesquisa-card-head {
|
| 708 |
+
display: flex;
|
| 709 |
+
align-items: flex-start;
|
| 710 |
+
justify-content: space-between;
|
| 711 |
+
gap: 10px;
|
| 712 |
+
min-width: 0;
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
.pesquisa-card-head > div {
|
| 716 |
+
min-width: 0;
|
| 717 |
+
flex: 1 1 auto;
|
| 718 |
+
}
|
| 719 |
+
|
| 720 |
+
.pesquisa-card-head-main {
|
| 721 |
+
display: grid;
|
| 722 |
+
gap: 6px;
|
| 723 |
+
min-width: 0;
|
| 724 |
+
}
|
| 725 |
+
|
| 726 |
+
.pesquisa-card-head h4 {
|
| 727 |
+
margin: 0;
|
| 728 |
+
font-family: 'Sora', sans-serif;
|
| 729 |
+
color: #2e4358;
|
| 730 |
+
font-size: 0.93rem;
|
| 731 |
+
line-height: 1.32;
|
| 732 |
+
overflow-wrap: anywhere;
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
.pesquisa-card-head p {
|
| 736 |
+
margin: 3px 0 0;
|
| 737 |
+
color: #5f758b;
|
| 738 |
+
font-size: 0.8rem;
|
| 739 |
+
line-height: 1.35;
|
| 740 |
+
overflow-wrap: anywhere;
|
| 741 |
+
}
|
| 742 |
+
|
| 743 |
+
.btn-pesquisa-expand {
|
| 744 |
+
flex: 0 0 auto;
|
| 745 |
+
min-width: 84px;
|
| 746 |
+
padding: 6px 10px;
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
button.btn-pesquisa-expand:hover {
|
| 750 |
+
transform: none;
|
| 751 |
+
box-shadow: 0 4px 10px var(--btn-shadow-strong);
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
+
.pesquisa-card-head-actions {
|
| 755 |
+
display: flex;
|
| 756 |
+
flex-wrap: wrap;
|
| 757 |
+
align-items: center;
|
| 758 |
+
justify-content: flex-start;
|
| 759 |
+
gap: 8px;
|
| 760 |
+
min-width: 0;
|
| 761 |
+
margin-top: 2px;
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
.pesquisa-select-toggle {
|
| 765 |
+
display: inline-flex;
|
| 766 |
+
align-items: center;
|
| 767 |
+
gap: 7px;
|
| 768 |
+
border: 1px solid #d8e4ef;
|
| 769 |
+
border-radius: 9px;
|
| 770 |
+
background: #f5f9fd;
|
| 771 |
+
padding: 5px 8px;
|
| 772 |
+
width: fit-content;
|
| 773 |
+
max-width: 100%;
|
| 774 |
+
font-size: 0.8rem;
|
| 775 |
+
font-weight: 700;
|
| 776 |
+
color: #48627b;
|
| 777 |
+
}
|
| 778 |
+
|
| 779 |
+
.pesquisa-select-toggle input {
|
| 780 |
+
margin: 0;
|
| 781 |
+
}
|
| 782 |
+
|
| 783 |
+
.pesquisa-card-status-row .status-pill {
|
| 784 |
+
max-width: 100%;
|
| 785 |
+
white-space: normal;
|
| 786 |
+
text-transform: none;
|
| 787 |
+
letter-spacing: 0.01em;
|
| 788 |
+
font-size: 0.75rem;
|
| 789 |
+
padding: 5px 10px;
|
| 790 |
+
}
|
| 791 |
+
|
| 792 |
+
.pesquisa-card-body {
|
| 793 |
+
display: grid;
|
| 794 |
+
gap: 9px;
|
| 795 |
+
min-width: 0;
|
| 796 |
+
border-top: 1px solid #e7eef5;
|
| 797 |
+
padding-top: 9px;
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
.pesquisa-card-kpis {
|
| 801 |
+
display: grid;
|
| 802 |
+
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
|
| 803 |
+
gap: 7px 10px;
|
| 804 |
+
font-size: 0.83rem;
|
| 805 |
+
color: #41586f;
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
.pesquisa-card-kpis span {
|
| 809 |
+
min-width: 0;
|
| 810 |
+
overflow-wrap: anywhere;
|
| 811 |
+
}
|
| 812 |
+
|
| 813 |
+
.pesquisa-card-dados-list {
|
| 814 |
+
display: grid;
|
| 815 |
+
gap: 6px;
|
| 816 |
+
font-size: 0.82rem;
|
| 817 |
+
color: #41586f;
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
.pesquisa-card-dados-list div {
|
| 821 |
+
min-width: 0;
|
| 822 |
+
overflow-wrap: anywhere;
|
| 823 |
+
line-height: 1.34;
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
.pesquisa-card-bairros {
|
| 827 |
+
padding-top: 2px;
|
| 828 |
+
font-size: 0.82rem;
|
| 829 |
+
color: #4d647b;
|
| 830 |
+
min-width: 0;
|
| 831 |
+
overflow-wrap: anywhere;
|
| 832 |
+
}
|
| 833 |
+
|
| 834 |
+
.pesquisa-card-faixas {
|
| 835 |
+
display: grid;
|
| 836 |
+
gap: 4px;
|
| 837 |
+
padding-top: 2px;
|
| 838 |
+
font-size: 0.81rem;
|
| 839 |
+
color: #496178;
|
| 840 |
+
}
|
| 841 |
+
|
| 842 |
+
.pesquisa-card-faixas span {
|
| 843 |
+
min-width: 0;
|
| 844 |
+
overflow-wrap: anywhere;
|
| 845 |
+
}
|
| 846 |
+
|
| 847 |
+
.pesquisa-modal-backdrop {
|
| 848 |
+
position: fixed;
|
| 849 |
+
inset: 0;
|
| 850 |
+
z-index: 1400;
|
| 851 |
+
display: flex;
|
| 852 |
+
align-items: center;
|
| 853 |
+
justify-content: center;
|
| 854 |
+
padding: 20px;
|
| 855 |
+
background: rgba(19, 30, 43, 0.44);
|
| 856 |
+
backdrop-filter: blur(2px);
|
| 857 |
+
}
|
| 858 |
+
|
| 859 |
+
.pesquisa-modal {
|
| 860 |
+
width: min(980px, 100%);
|
| 861 |
+
max-height: 90vh;
|
| 862 |
+
overflow: auto;
|
| 863 |
+
border: 1px solid #d5e2ef;
|
| 864 |
+
border-radius: 16px;
|
| 865 |
+
background: #fff;
|
| 866 |
+
padding: 16px;
|
| 867 |
+
box-shadow: 0 20px 44px rgba(18, 31, 46, 0.28);
|
| 868 |
+
}
|
| 869 |
+
|
| 870 |
+
.pesquisa-modal-head {
|
| 871 |
+
display: flex;
|
| 872 |
+
align-items: flex-start;
|
| 873 |
+
justify-content: space-between;
|
| 874 |
+
gap: 10px;
|
| 875 |
+
}
|
| 876 |
+
|
| 877 |
+
.pesquisa-modal-head h4 {
|
| 878 |
+
margin: 0;
|
| 879 |
+
color: #2d4358;
|
| 880 |
+
font-family: 'Sora', sans-serif;
|
| 881 |
+
font-size: 1rem;
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
.pesquisa-modal-head p {
|
| 885 |
+
margin: 3px 0 0;
|
| 886 |
+
color: #5c7288;
|
| 887 |
+
font-size: 0.82rem;
|
| 888 |
+
}
|
| 889 |
+
|
| 890 |
+
.pesquisa-modal-close {
|
| 891 |
+
--btn-bg-start: #748292;
|
| 892 |
+
--btn-bg-end: #5f6d7d;
|
| 893 |
+
--btn-border: #4f5e6f;
|
| 894 |
+
--btn-shadow-soft: rgba(95, 109, 125, 0.18);
|
| 895 |
+
--btn-shadow-strong: rgba(95, 109, 125, 0.24);
|
| 896 |
+
min-width: 82px;
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
.pesquisa-modal-body {
|
| 900 |
+
display: grid;
|
| 901 |
+
gap: 12px;
|
| 902 |
+
margin-top: 12px;
|
| 903 |
+
}
|
| 904 |
+
|
| 905 |
+
.pesquisa-card-error {
|
| 906 |
+
margin-top: 2px;
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
.btn-pesquisa-compare {
|
| 910 |
+
min-width: 96px;
|
| 911 |
+
}
|
| 912 |
+
|
| 913 |
+
.pesquisa-compare-grid {
|
| 914 |
+
display: grid;
|
| 915 |
+
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
| 916 |
+
gap: 12px;
|
| 917 |
+
}
|
| 918 |
+
|
| 919 |
+
.pesquisa-compare-card {
|
| 920 |
+
border: 1px solid #d5e2ee;
|
| 921 |
+
border-radius: 12px;
|
| 922 |
+
background: #fff;
|
| 923 |
+
padding: 12px;
|
| 924 |
+
display: grid;
|
| 925 |
+
gap: 11px;
|
| 926 |
+
}
|
| 927 |
+
|
| 928 |
+
.pesquisa-compare-card h4 {
|
| 929 |
+
margin: 0;
|
| 930 |
+
font-family: 'Sora', sans-serif;
|
| 931 |
+
color: #2f4458;
|
| 932 |
+
font-size: 0.95rem;
|
| 933 |
+
}
|
| 934 |
+
|
| 935 |
+
.pesquisa-compare-meta {
|
| 936 |
+
display: grid;
|
| 937 |
+
gap: 5px;
|
| 938 |
+
font-size: 0.82rem;
|
| 939 |
+
color: #4d647b;
|
| 940 |
+
}
|
| 941 |
+
|
| 942 |
+
.pesquisa-compare-block h5 {
|
| 943 |
+
margin: 0 0 7px;
|
| 944 |
+
font-size: 0.85rem;
|
| 945 |
+
color: #344b60;
|
| 946 |
+
font-family: 'Sora', sans-serif;
|
| 947 |
+
}
|
| 948 |
+
|
| 949 |
+
.pesquisa-compat-grid {
|
| 950 |
+
display: grid;
|
| 951 |
+
gap: 7px;
|
| 952 |
+
}
|
| 953 |
+
|
| 954 |
+
.pesquisa-compat-row {
|
| 955 |
+
display: grid;
|
| 956 |
+
grid-template-columns: minmax(120px, 0.8fr) minmax(0, 1fr);
|
| 957 |
+
gap: 7px;
|
| 958 |
+
border: 1px solid #e0e9f2;
|
| 959 |
+
border-radius: 9px;
|
| 960 |
+
padding: 6px 7px;
|
| 961 |
+
background: #fbfdff;
|
| 962 |
+
font-size: 0.81rem;
|
| 963 |
+
}
|
| 964 |
+
|
| 965 |
+
.pesquisa-compat-row span {
|
| 966 |
+
color: #587087;
|
| 967 |
+
}
|
| 968 |
+
|
| 969 |
+
.pesquisa-compat-row strong {
|
| 970 |
+
color: #2f465c;
|
| 971 |
+
word-break: break-word;
|
| 972 |
+
}
|
| 973 |
+
|
| 974 |
+
.pesquisa-variaveis-table {
|
| 975 |
+
max-height: 250px;
|
| 976 |
+
}
|
| 977 |
+
|
| 978 |
+
.table-wrapper.pesquisa-variaveis-table table {
|
| 979 |
+
min-width: 360px;
|
| 980 |
+
}
|
| 981 |
+
|
| 982 |
+
.table-wrapper.pesquisa-variaveis-table th,
|
| 983 |
+
.table-wrapper.pesquisa-variaveis-table td {
|
| 984 |
+
white-space: nowrap;
|
| 985 |
+
}
|
| 986 |
+
|
| 987 |
+
.table-wrapper.pesquisa-variaveis-table th:first-child,
|
| 988 |
+
.table-wrapper.pesquisa-variaveis-table td:first-child {
|
| 989 |
+
white-space: normal;
|
| 990 |
+
overflow-wrap: anywhere;
|
| 991 |
+
}
|
| 992 |
+
|
| 993 |
+
.pesquisa-legenda-grid {
|
| 994 |
+
display: flex;
|
| 995 |
+
flex-wrap: wrap;
|
| 996 |
+
gap: 8px;
|
| 997 |
+
margin: 10px 0 12px;
|
| 998 |
+
}
|
| 999 |
+
|
| 1000 |
+
.pesquisa-legenda-item {
|
| 1001 |
+
display: inline-flex;
|
| 1002 |
+
align-items: center;
|
| 1003 |
+
gap: 7px;
|
| 1004 |
+
border: 1px solid #dce6f1;
|
| 1005 |
+
border-radius: 999px;
|
| 1006 |
+
background: #fbfdff;
|
| 1007 |
+
padding: 5px 10px;
|
| 1008 |
+
color: #3f566b;
|
| 1009 |
+
font-size: 0.8rem;
|
| 1010 |
+
font-weight: 700;
|
| 1011 |
+
}
|
| 1012 |
+
|
| 1013 |
+
.pesquisa-legenda-color {
|
| 1014 |
+
width: 11px;
|
| 1015 |
+
height: 11px;
|
| 1016 |
+
border-radius: 50%;
|
| 1017 |
+
border: 1px solid rgba(24, 43, 62, 0.22);
|
| 1018 |
+
}
|
| 1019 |
+
|
| 1020 |
.row {
|
| 1021 |
display: flex;
|
| 1022 |
align-items: center;
|
|
|
|
| 2507 |
grid-template-columns: 1.2fr 110px minmax(110px, 0.8fr) auto;
|
| 2508 |
}
|
| 2509 |
|
| 2510 |
+
.pesquisa-filtros-groups {
|
| 2511 |
+
grid-template-columns: 1fr;
|
| 2512 |
+
}
|
| 2513 |
+
|
| 2514 |
.micro-msg-grid-codigo {
|
| 2515 |
grid-template-columns: repeat(2, minmax(0, 1fr));
|
| 2516 |
}
|
|
|
|
| 2562 |
grid-template-columns: 1fr;
|
| 2563 |
}
|
| 2564 |
|
| 2565 |
+
.pesquisa-filtros-groups,
|
| 2566 |
+
.pesquisa-filtros-grid,
|
| 2567 |
+
.pesquisa-card-grid,
|
| 2568 |
+
.pesquisa-compare-grid,
|
| 2569 |
+
.pesquisa-compat-row {
|
| 2570 |
+
grid-template-columns: 1fr;
|
| 2571 |
+
}
|
| 2572 |
+
|
| 2573 |
+
.pesquisa-dynamic-filter-row,
|
| 2574 |
+
.pesquisa-range-values-row,
|
| 2575 |
+
.pesquisa-range-row,
|
| 2576 |
+
.pesquisa-range-row.pesquisa-range-row-three {
|
| 2577 |
+
grid-template-columns: 1fr;
|
| 2578 |
+
}
|
| 2579 |
+
|
| 2580 |
+
.pesquisa-otica-switch {
|
| 2581 |
+
overflow-x: auto;
|
| 2582 |
+
scrollbar-width: thin;
|
| 2583 |
+
}
|
| 2584 |
+
|
| 2585 |
+
.pesquisa-card-head {
|
| 2586 |
+
flex-direction: column;
|
| 2587 |
+
}
|
| 2588 |
+
|
| 2589 |
+
.pesquisa-card-head-actions {
|
| 2590 |
+
width: 100%;
|
| 2591 |
+
justify-content: flex-start;
|
| 2592 |
+
}
|
| 2593 |
+
|
| 2594 |
+
.pesquisa-results-toolbar {
|
| 2595 |
+
align-items: flex-start;
|
| 2596 |
+
}
|
| 2597 |
+
|
| 2598 |
+
.pesquisa-select-all {
|
| 2599 |
+
white-space: normal;
|
| 2600 |
+
}
|
| 2601 |
+
|
| 2602 |
+
.pesquisa-modal-backdrop {
|
| 2603 |
+
padding: 10px;
|
| 2604 |
+
}
|
| 2605 |
+
|
| 2606 |
+
.pesquisa-modal {
|
| 2607 |
+
max-height: 95vh;
|
| 2608 |
+
padding: 12px;
|
| 2609 |
+
}
|
| 2610 |
+
|
| 2611 |
+
.pesquisa-modal-head {
|
| 2612 |
+
flex-direction: column;
|
| 2613 |
+
}
|
| 2614 |
+
|
| 2615 |
.variavel-badge-line {
|
| 2616 |
grid-template-columns: 1fr;
|
| 2617 |
gap: 5px;
|