Spaces:
Running
Running
Guilherme Silberfarb Costa commited on
Commit ·
b9bb1d5
1
Parent(s): e56a352
Improve elaboracao interactions and pesquisa map behavior
Browse files- backend/app/api/elaboracao.py +7 -2
- backend/app/core/map_layers.py +3 -0
- backend/app/core/visualizacao/app.py +1 -1
- backend/app/services/elaboracao_service.py +33 -10
- backend/app/services/pesquisa_service.py +78 -11
- backend/app/services/trabalhos_tecnicos_importer.py +99 -8
- backend/app/services/trabalhos_tecnicos_service.py +97 -5
- frontend/src/api.js +2 -1
- frontend/src/components/ElaboracaoTab.jsx +4 -2
- frontend/src/components/LeafletMapFrame.jsx +67 -4
- frontend/src/components/PesquisaTab.jsx +15 -3
backend/app/api/elaboracao.py
CHANGED
|
@@ -129,7 +129,8 @@ class AvaliacaoPayload(SessionPayload):
|
|
| 129 |
|
| 130 |
|
| 131 |
class AvaliacaoKnnDetalhesPayload(SessionPayload):
|
| 132 |
-
valores_x: dict[str, Any]
|
|
|
|
| 133 |
|
| 134 |
|
| 135 |
class AvaliacaoDeletePayload(SessionPayload):
|
|
@@ -401,7 +402,11 @@ def evaluation_calculate(payload: AvaliacaoPayload, request: Request) -> dict[st
|
|
| 401 |
def evaluation_knn_details(payload: AvaliacaoKnnDetalhesPayload, request: Request) -> dict[str, Any]:
|
| 402 |
session = session_store.get(payload.session_id)
|
| 403 |
user = auth_service.require_user(request)
|
| 404 |
-
resposta = elaboracao_service.detalhes_knn_avaliacao_elaboracao(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 405 |
log_event(
|
| 406 |
"elaboracao",
|
| 407 |
"avaliacao_knn_detalhes",
|
|
|
|
| 129 |
|
| 130 |
|
| 131 |
class AvaliacaoKnnDetalhesPayload(SessionPayload):
|
| 132 |
+
valores_x: dict[str, Any] | None = None
|
| 133 |
+
indice_avaliacao: int | None = None
|
| 134 |
|
| 135 |
|
| 136 |
class AvaliacaoDeletePayload(SessionPayload):
|
|
|
|
| 402 |
def evaluation_knn_details(payload: AvaliacaoKnnDetalhesPayload, request: Request) -> dict[str, Any]:
|
| 403 |
session = session_store.get(payload.session_id)
|
| 404 |
user = auth_service.require_user(request)
|
| 405 |
+
resposta = elaboracao_service.detalhes_knn_avaliacao_elaboracao(
|
| 406 |
+
session,
|
| 407 |
+
payload.valores_x,
|
| 408 |
+
payload.indice_avaliacao,
|
| 409 |
+
)
|
| 410 |
log_event(
|
| 411 |
"elaboracao",
|
| 412 |
"avaliacao_knn_detalhes",
|
backend/app/core/map_layers.py
CHANGED
|
@@ -231,6 +231,7 @@ def build_trabalhos_tecnicos_marker_payloads(
|
|
| 231 |
endereco = str(item.get("endereco") or "").strip()
|
| 232 |
numero = str(item.get("numero") or "").strip()
|
| 233 |
modelos = [str(valor).strip() for valor in (item.get("modelos_relacionados") or []) if str(valor).strip()]
|
|
|
|
| 234 |
|
| 235 |
endereco_parts = []
|
| 236 |
if endereco:
|
|
@@ -306,6 +307,7 @@ def build_trabalhos_tecnicos_marker_payloads(
|
|
| 306 |
"lon": lon,
|
| 307 |
"tooltip_html": detalhes_html,
|
| 308 |
"popup_html": detalhes_html,
|
|
|
|
| 309 |
"marker_html": marker_html,
|
| 310 |
"marker_style": "estrela",
|
| 311 |
"ignore_bounds": bool(ignore_bounds),
|
|
@@ -327,6 +329,7 @@ def build_trabalhos_tecnicos_marker_payloads(
|
|
| 327 |
"lon": lon,
|
| 328 |
"tooltip_html": detalhes_html,
|
| 329 |
"popup_html": detalhes_html,
|
|
|
|
| 330 |
"marker_html": marker_html,
|
| 331 |
"marker_style": "ponto",
|
| 332 |
"ignore_bounds": bool(ignore_bounds),
|
|
|
|
| 231 |
endereco = str(item.get("endereco") or "").strip()
|
| 232 |
numero = str(item.get("numero") or "").strip()
|
| 233 |
modelos = [str(valor).strip() for valor in (item.get("modelos_relacionados") or []) if str(valor).strip()]
|
| 234 |
+
modelos_origem_ids = [str(valor).strip() for valor in (item.get("modelos_origem_ids") or []) if str(valor).strip()]
|
| 235 |
|
| 236 |
endereco_parts = []
|
| 237 |
if endereco:
|
|
|
|
| 307 |
"lon": lon,
|
| 308 |
"tooltip_html": detalhes_html,
|
| 309 |
"popup_html": detalhes_html,
|
| 310 |
+
"source_overlay_ids": modelos_origem_ids,
|
| 311 |
"marker_html": marker_html,
|
| 312 |
"marker_style": "estrela",
|
| 313 |
"ignore_bounds": bool(ignore_bounds),
|
|
|
|
| 329 |
"lon": lon,
|
| 330 |
"tooltip_html": detalhes_html,
|
| 331 |
"popup_html": detalhes_html,
|
| 332 |
+
"source_overlay_ids": modelos_origem_ids,
|
| 333 |
"marker_html": marker_html,
|
| 334 |
"marker_style": "ponto",
|
| 335 |
"ignore_bounds": bool(ignore_bounds),
|
backend/app/core/visualizacao/app.py
CHANGED
|
@@ -929,7 +929,7 @@ def criar_mapa(
|
|
| 929 |
camada_indices.add_to(mapa)
|
| 930 |
|
| 931 |
if avaliandos_tecnicos:
|
| 932 |
-
camada_trabalhos_tecnicos = folium.FeatureGroup(name="
|
| 933 |
add_trabalhos_tecnicos_markers(camada_trabalhos_tecnicos, avaliandos_tecnicos)
|
| 934 |
camada_trabalhos_tecnicos.add_to(mapa)
|
| 935 |
|
|
|
|
| 929 |
camada_indices.add_to(mapa)
|
| 930 |
|
| 931 |
if avaliandos_tecnicos:
|
| 932 |
+
camada_trabalhos_tecnicos = folium.FeatureGroup(name="Trabalhos tecnicos", show=True)
|
| 933 |
add_trabalhos_tecnicos_markers(camada_trabalhos_tecnicos, avaliandos_tecnicos)
|
| 934 |
camada_trabalhos_tecnicos.add_to(mapa)
|
| 935 |
|
backend/app/services/elaboracao_service.py
CHANGED
|
@@ -2248,7 +2248,11 @@ def calcular_avaliacao_elaboracao(
|
|
| 2248 |
}
|
| 2249 |
|
| 2250 |
|
| 2251 |
-
def detalhes_knn_avaliacao_elaboracao(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2252 |
if session.resultado_modelo is None:
|
| 2253 |
raise HTTPException(status_code=400, detail="Ajuste um modelo primeiro")
|
| 2254 |
if session.tabela_estatisticas is None:
|
|
@@ -2258,12 +2262,31 @@ def detalhes_knn_avaliacao_elaboracao(session: SessionState, valores_x: dict[str
|
|
| 2258 |
if not colunas_x:
|
| 2259 |
raise HTTPException(status_code=400, detail="Modelo sem variaveis")
|
| 2260 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2261 |
entradas: dict[str, float] = {}
|
| 2262 |
for col in colunas_x:
|
| 2263 |
-
if col not in
|
| 2264 |
raise HTTPException(status_code=400, detail=f"Valor ausente para {col}")
|
| 2265 |
try:
|
| 2266 |
-
entradas[col] = float(
|
| 2267 |
except Exception as exc:
|
| 2268 |
raise HTTPException(status_code=400, detail=f"Valor invalido para {col}") from exc
|
| 2269 |
|
|
@@ -2288,7 +2311,7 @@ def detalhes_knn_avaliacao_elaboracao(session: SessionState, valores_x: dict[str
|
|
| 2288 |
if col in session.percentuais and (valor < -PERCENTUAL_RUIDO_TOL or valor > 1 + PERCENTUAL_RUIDO_TOL):
|
| 2289 |
raise HTTPException(status_code=400, detail=f"{col} aceita valores entre 0 e 1")
|
| 2290 |
|
| 2291 |
-
valor_area = _resolver_valor_area_avaliacao(
|
| 2292 |
if session.tipo_y and valor_area is None:
|
| 2293 |
raise HTTPException(status_code=400, detail="Informe um valor de area maior que 1 para a coluna configurada.")
|
| 2294 |
|
|
@@ -2299,12 +2322,12 @@ def detalhes_knn_avaliacao_elaboracao(session: SessionState, valores_x: dict[str
|
|
| 2299 |
coluna_y_knn = str(session.coluna_y or session.resultado_modelo.get("coluna_y") or "").strip()
|
| 2300 |
resultado_knn = enriquecer_resultado_knn_com_area(
|
| 2301 |
estimar_valor_knn_avaliacao(
|
| 2302 |
-
|
| 2303 |
-
|
| 2304 |
-
|
| 2305 |
-
|
| 2306 |
-
|
| 2307 |
-
|
| 2308 |
),
|
| 2309 |
tipo_y=session.tipo_y,
|
| 2310 |
coluna_area=session.coluna_area,
|
|
|
|
| 2248 |
}
|
| 2249 |
|
| 2250 |
|
| 2251 |
+
def detalhes_knn_avaliacao_elaboracao(
|
| 2252 |
+
session: SessionState,
|
| 2253 |
+
valores_x: dict[str, Any] | None = None,
|
| 2254 |
+
indice_avaliacao: int | None = None,
|
| 2255 |
+
) -> dict[str, Any]:
|
| 2256 |
if session.resultado_modelo is None:
|
| 2257 |
raise HTTPException(status_code=400, detail="Ajuste um modelo primeiro")
|
| 2258 |
if session.tabela_estatisticas is None:
|
|
|
|
| 2262 |
if not colunas_x:
|
| 2263 |
raise HTTPException(status_code=400, detail="Modelo sem variaveis")
|
| 2264 |
|
| 2265 |
+
valores_resolvidos: dict[str, Any] = {}
|
| 2266 |
+
idx_avaliacao = (int(indice_avaliacao) - 1) if indice_avaliacao is not None else None
|
| 2267 |
+
if idx_avaliacao is not None and 0 <= idx_avaliacao < len(session.avaliacoes_elaboracao):
|
| 2268 |
+
avaliacao_sessao = session.avaliacoes_elaboracao[idx_avaliacao]
|
| 2269 |
+
if isinstance(avaliacao_sessao, dict):
|
| 2270 |
+
valores_resolvidos.update(avaliacao_sessao.get("valores_x") or {})
|
| 2271 |
+
coluna_area_sessao = str(avaliacao_sessao.get("coluna_area") or "").strip()
|
| 2272 |
+
valor_area_sessao = avaliacao_sessao.get("valor_area")
|
| 2273 |
+
if (
|
| 2274 |
+
coluna_area_sessao
|
| 2275 |
+
and coluna_area_sessao not in valores_resolvidos
|
| 2276 |
+
and valor_area_sessao is not None
|
| 2277 |
+
):
|
| 2278 |
+
valores_resolvidos[coluna_area_sessao] = valor_area_sessao
|
| 2279 |
+
if isinstance(valores_x, dict):
|
| 2280 |
+
valores_resolvidos.update(valores_x)
|
| 2281 |
+
if not valores_resolvidos:
|
| 2282 |
+
raise HTTPException(status_code=400, detail="Avaliacao selecionada indisponivel para detalhamento do KNN.")
|
| 2283 |
+
|
| 2284 |
entradas: dict[str, float] = {}
|
| 2285 |
for col in colunas_x:
|
| 2286 |
+
if col not in valores_resolvidos:
|
| 2287 |
raise HTTPException(status_code=400, detail=f"Valor ausente para {col}")
|
| 2288 |
try:
|
| 2289 |
+
entradas[col] = float(valores_resolvidos[col])
|
| 2290 |
except Exception as exc:
|
| 2291 |
raise HTTPException(status_code=400, detail=f"Valor invalido para {col}") from exc
|
| 2292 |
|
|
|
|
| 2311 |
if col in session.percentuais and (valor < -PERCENTUAL_RUIDO_TOL or valor > 1 + PERCENTUAL_RUIDO_TOL):
|
| 2312 |
raise HTTPException(status_code=400, detail=f"{col} aceita valores entre 0 e 1")
|
| 2313 |
|
| 2314 |
+
valor_area = _resolver_valor_area_avaliacao(valores_resolvidos, session.coluna_area)
|
| 2315 |
if session.tipo_y and valor_area is None:
|
| 2316 |
raise HTTPException(status_code=400, detail="Informe um valor de area maior que 1 para a coluna configurada.")
|
| 2317 |
|
|
|
|
| 2322 |
coluna_y_knn = str(session.coluna_y or session.resultado_modelo.get("coluna_y") or "").strip()
|
| 2323 |
resultado_knn = enriquecer_resultado_knn_com_area(
|
| 2324 |
estimar_valor_knn_avaliacao(
|
| 2325 |
+
df_base=df_knn,
|
| 2326 |
+
coluna_y=coluna_y_knn,
|
| 2327 |
+
colunas_x=colunas_x,
|
| 2328 |
+
valores_x=entradas,
|
| 2329 |
+
alpha_geo=0.35,
|
| 2330 |
+
retornar_detalhes=True,
|
| 2331 |
),
|
| 2332 |
tipo_y=session.tipo_y,
|
| 2333 |
coluna_area=session.coluna_area,
|
backend/app/services/pesquisa_service.py
CHANGED
|
@@ -876,6 +876,44 @@ def _listar_avaliandos_tecnicos_proximos_por_avaliandos(
|
|
| 876 |
return sanitize_value(consolidado)
|
| 877 |
|
| 878 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 879 |
def gerar_mapa_modelos(
|
| 880 |
modelos_ids: list[str],
|
| 881 |
limite_pontos_por_modelo: int = 0,
|
|
@@ -1193,12 +1231,16 @@ def _marker_payloads_avaliandos(avaliandos_geo: list[dict[str, Any]]) -> list[di
|
|
| 1193 |
marker_html = (
|
| 1194 |
"<div style='display:flex;align-items:center;justify-content:center;"
|
| 1195 |
"width:30px;height:30px;filter:drop-shadow(0 2px 6px rgba(0,0,0,0.22));'>"
|
| 1196 |
-
"<
|
|
|
|
|
|
|
|
|
|
| 1197 |
"<path d='M12 3.2L3.9 10v10.1h5.4v-5.3h5.4v5.3h5.4V10L12 3.2Z' "
|
| 1198 |
-
"fill='#c62828' stroke='rgba(255,255,255,0.96)' stroke-width='1.
|
| 1199 |
"<rect x='10.1' y='15.2' width='3.8' height='4.9' rx='0.5' fill='rgba(255,255,255,0.96)'/>"
|
| 1200 |
"</svg>"
|
| 1201 |
"</div>"
|
|
|
|
| 1202 |
)
|
| 1203 |
payloads.append(
|
| 1204 |
{
|
|
@@ -1299,13 +1341,13 @@ def _build_mapa_modelos_payload(
|
|
| 1299 |
avaliando_unico = avaliandos_geo[0] if len(avaliandos_geo) == 1 else None
|
| 1300 |
aval_lat = avaliando_unico.get("lat") if avaliando_unico is not None else None
|
| 1301 |
aval_lon = avaliando_unico.get("lon") if avaliando_unico is not None else None
|
|
|
|
| 1302 |
|
| 1303 |
for modelo in modelos_plotados:
|
| 1304 |
layer: dict[str, Any] = {
|
| 1305 |
"id": str(modelo.get("id") or ""),
|
| 1306 |
"label": str(modelo.get("nome") or "Modelo"),
|
| 1307 |
"show": True,
|
| 1308 |
-
"markers": build_trabalhos_tecnicos_marker_payloads(modelo.get("avaliandos_tecnicos") or []),
|
| 1309 |
}
|
| 1310 |
if modo_exibicao == "pontos":
|
| 1311 |
tooltip_html = _tooltip_mapa_modelo_html(modelo)
|
|
@@ -1326,12 +1368,23 @@ def _build_mapa_modelos_payload(
|
|
| 1326 |
layer["shapes"] = _shapes_modelo_payload(modelo, aval_lat, aval_lon)
|
| 1327 |
overlay_layers.append(layer)
|
| 1328 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1329 |
if avaliandos_tecnicos_proximos is not None:
|
| 1330 |
label_raio = f" (até {int(trabalhos_tecnicos_raio_m or 0)} m)" if trabalhos_tecnicos_raio_m is not None else ""
|
| 1331 |
proximos_layer: dict[str, Any] = {
|
| 1332 |
"id": "trabalhos-tecnicos-proximos",
|
| 1333 |
"label": f"Trabalhos técnicos próximos{label_raio}",
|
| 1334 |
"show": True,
|
|
|
|
| 1335 |
"markers": build_trabalhos_tecnicos_marker_payloads(
|
| 1336 |
avaliandos_tecnicos_proximos or [],
|
| 1337 |
origem="pesquisa_mapa",
|
|
@@ -1409,6 +1462,12 @@ def _renderizar_mapa_modelos(
|
|
| 1409 |
add_bairros_layer(mapa, show=True)
|
| 1410 |
nome_camada_avaliando = "Avaliando" if len(avaliandos_geo) == 1 else "Avaliandos"
|
| 1411 |
camada_avaliando = folium.FeatureGroup(name=nome_camada_avaliando, show=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1412 |
camada_trabalhos_proximos = None
|
| 1413 |
if avaliandos_tecnicos_proximos is not None:
|
| 1414 |
label_raio = f" (ate {int(trabalhos_tecnicos_raio_m or 0)} m)" if trabalhos_tecnicos_raio_m is not None else ""
|
|
@@ -1451,10 +1510,12 @@ def _renderizar_mapa_modelos(
|
|
| 1451 |
|
| 1452 |
if renderizar_cobertura:
|
| 1453 |
_adicionar_geometria_modelo_no_mapa(camada_modelo, camada_modelo, modelo, aval_lat, aval_lon)
|
| 1454 |
-
|
| 1455 |
-
add_trabalhos_tecnicos_markers(camada_modelo, modelo.get("avaliandos_tecnicos") or [])
|
| 1456 |
camada_modelo.add_to(mapa)
|
| 1457 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1458 |
if camada_trabalhos_proximos is not None:
|
| 1459 |
if int(trabalhos_tecnicos_raio_m or 0) > 0:
|
| 1460 |
for idx, avaliando in enumerate(avaliandos_geo):
|
|
@@ -1495,14 +1556,20 @@ def _renderizar_mapa_modelos(
|
|
| 1495 |
marker_icon = folium.DivIcon(
|
| 1496 |
html=(
|
| 1497 |
"<div style='display:flex;align-items:center;justify-content:center;"
|
| 1498 |
-
"width:
|
| 1499 |
-
"
|
| 1500 |
-
"
|
| 1501 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1502 |
"</div>"
|
| 1503 |
),
|
| 1504 |
-
icon_size=(
|
| 1505 |
-
icon_anchor=(
|
| 1506 |
class_name="mesa-avaliando-marker",
|
| 1507 |
)
|
| 1508 |
folium.Marker(
|
|
|
|
| 876 |
return sanitize_value(consolidado)
|
| 877 |
|
| 878 |
|
| 879 |
+
def _consolidar_trabalhos_tecnicos_modelos(modelos_plotados: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
| 880 |
+
agregados: dict[tuple[str, str, str, str, str, str], dict[str, Any]] = {}
|
| 881 |
+
|
| 882 |
+
for modelo in modelos_plotados or []:
|
| 883 |
+
modelo_id = str(modelo.get("id") or "").strip()
|
| 884 |
+
for item in (modelo.get("avaliandos_tecnicos") or []):
|
| 885 |
+
if not isinstance(item, dict):
|
| 886 |
+
continue
|
| 887 |
+
chave = _chave_unica_trabalho_tecnico(item)
|
| 888 |
+
atual = agregados.get(chave)
|
| 889 |
+
if atual is None:
|
| 890 |
+
novo_item = dict(item)
|
| 891 |
+
novo_item["modelos_relacionados"] = [str(valor).strip() for valor in (item.get("modelos_relacionados") or []) if str(valor).strip()]
|
| 892 |
+
novo_item["modelos_origem_ids"] = [modelo_id] if modelo_id else []
|
| 893 |
+
agregados[chave] = novo_item
|
| 894 |
+
continue
|
| 895 |
+
|
| 896 |
+
_append_aliases_unicos(
|
| 897 |
+
atual.setdefault("modelos_relacionados", []),
|
| 898 |
+
*[str(valor).strip() for valor in (item.get("modelos_relacionados") or []) if str(valor).strip()],
|
| 899 |
+
)
|
| 900 |
+
if modelo_id:
|
| 901 |
+
_append_aliases_unicos(atual.setdefault("modelos_origem_ids", []), modelo_id)
|
| 902 |
+
for campo in ("trabalho_nome", "tipo_label", "label", "endereco", "numero"):
|
| 903 |
+
if not str(atual.get(campo) or "").strip() and str(item.get(campo) or "").strip():
|
| 904 |
+
atual[campo] = item.get(campo)
|
| 905 |
+
|
| 906 |
+
consolidado = list(agregados.values())
|
| 907 |
+
consolidado.sort(
|
| 908 |
+
key=lambda item: (
|
| 909 |
+
str(item.get("trabalho_nome") or "").casefold(),
|
| 910 |
+
str(item.get("label") or item.get("endereco") or "").casefold(),
|
| 911 |
+
str(item.get("numero") or "").casefold(),
|
| 912 |
+
)
|
| 913 |
+
)
|
| 914 |
+
return sanitize_value(consolidado)
|
| 915 |
+
|
| 916 |
+
|
| 917 |
def gerar_mapa_modelos(
|
| 918 |
modelos_ids: list[str],
|
| 919 |
limite_pontos_por_modelo: int = 0,
|
|
|
|
| 1231 |
marker_html = (
|
| 1232 |
"<div style='display:flex;align-items:center;justify-content:center;"
|
| 1233 |
"width:30px;height:30px;filter:drop-shadow(0 2px 6px rgba(0,0,0,0.22));'>"
|
| 1234 |
+
"<div style='display:flex;align-items:center;justify-content:center;"
|
| 1235 |
+
"width:24px;height:24px;border-radius:999px;background:rgba(255,255,255,0.97);"
|
| 1236 |
+
"box-shadow:0 0 0 1.4px rgba(255,255,255,0.98),0 0 0 2.5px rgba(0,0,0,0.74);'>"
|
| 1237 |
+
"<svg viewBox='0 0 24 24' width='20' height='20' aria-hidden='true'>"
|
| 1238 |
"<path d='M12 3.2L3.9 10v10.1h5.4v-5.3h5.4v5.3h5.4V10L12 3.2Z' "
|
| 1239 |
+
"fill='#c62828' stroke='rgba(255,255,255,0.96)' stroke-width='1.2' stroke-linejoin='round'/>"
|
| 1240 |
"<rect x='10.1' y='15.2' width='3.8' height='4.9' rx='0.5' fill='rgba(255,255,255,0.96)'/>"
|
| 1241 |
"</svg>"
|
| 1242 |
"</div>"
|
| 1243 |
+
"</div>"
|
| 1244 |
)
|
| 1245 |
payloads.append(
|
| 1246 |
{
|
|
|
|
| 1341 |
avaliando_unico = avaliandos_geo[0] if len(avaliandos_geo) == 1 else None
|
| 1342 |
aval_lat = avaliando_unico.get("lat") if avaliando_unico is not None else None
|
| 1343 |
aval_lon = avaliando_unico.get("lon") if avaliando_unico is not None else None
|
| 1344 |
+
trabalhos_tecnicos_modelos = _consolidar_trabalhos_tecnicos_modelos(modelos_plotados)
|
| 1345 |
|
| 1346 |
for modelo in modelos_plotados:
|
| 1347 |
layer: dict[str, Any] = {
|
| 1348 |
"id": str(modelo.get("id") or ""),
|
| 1349 |
"label": str(modelo.get("nome") or "Modelo"),
|
| 1350 |
"show": True,
|
|
|
|
| 1351 |
}
|
| 1352 |
if modo_exibicao == "pontos":
|
| 1353 |
tooltip_html = _tooltip_mapa_modelo_html(modelo)
|
|
|
|
| 1368 |
layer["shapes"] = _shapes_modelo_payload(modelo, aval_lat, aval_lon)
|
| 1369 |
overlay_layers.append(layer)
|
| 1370 |
|
| 1371 |
+
if trabalhos_tecnicos_modelos:
|
| 1372 |
+
overlay_layers.append(
|
| 1373 |
+
{
|
| 1374 |
+
"id": "trabalhos-tecnicos-modelos",
|
| 1375 |
+
"label": "Trabalhos tecnicos dos modelos",
|
| 1376 |
+
"show": True,
|
| 1377 |
+
"markers": build_trabalhos_tecnicos_marker_payloads(trabalhos_tecnicos_modelos),
|
| 1378 |
+
}
|
| 1379 |
+
)
|
| 1380 |
+
|
| 1381 |
if avaliandos_tecnicos_proximos is not None:
|
| 1382 |
label_raio = f" (até {int(trabalhos_tecnicos_raio_m or 0)} m)" if trabalhos_tecnicos_raio_m is not None else ""
|
| 1383 |
proximos_layer: dict[str, Any] = {
|
| 1384 |
"id": "trabalhos-tecnicos-proximos",
|
| 1385 |
"label": f"Trabalhos técnicos próximos{label_raio}",
|
| 1386 |
"show": True,
|
| 1387 |
+
"required_overlay_ids": ["trabalhos-tecnicos-modelos"],
|
| 1388 |
"markers": build_trabalhos_tecnicos_marker_payloads(
|
| 1389 |
avaliandos_tecnicos_proximos or [],
|
| 1390 |
origem="pesquisa_mapa",
|
|
|
|
| 1462 |
add_bairros_layer(mapa, show=True)
|
| 1463 |
nome_camada_avaliando = "Avaliando" if len(avaliandos_geo) == 1 else "Avaliandos"
|
| 1464 |
camada_avaliando = folium.FeatureGroup(name=nome_camada_avaliando, show=True)
|
| 1465 |
+
trabalhos_tecnicos_modelos = _consolidar_trabalhos_tecnicos_modelos(modelos_plotados)
|
| 1466 |
+
camada_trabalhos_modelos = (
|
| 1467 |
+
folium.FeatureGroup(name="Trabalhos tecnicos dos modelos", show=True)
|
| 1468 |
+
if trabalhos_tecnicos_modelos
|
| 1469 |
+
else None
|
| 1470 |
+
)
|
| 1471 |
camada_trabalhos_proximos = None
|
| 1472 |
if avaliandos_tecnicos_proximos is not None:
|
| 1473 |
label_raio = f" (ate {int(trabalhos_tecnicos_raio_m or 0)} m)" if trabalhos_tecnicos_raio_m is not None else ""
|
|
|
|
| 1510 |
|
| 1511 |
if renderizar_cobertura:
|
| 1512 |
_adicionar_geometria_modelo_no_mapa(camada_modelo, camada_modelo, modelo, aval_lat, aval_lon)
|
|
|
|
|
|
|
| 1513 |
camada_modelo.add_to(mapa)
|
| 1514 |
|
| 1515 |
+
if camada_trabalhos_modelos is not None:
|
| 1516 |
+
add_trabalhos_tecnicos_markers(camada_trabalhos_modelos, trabalhos_tecnicos_modelos)
|
| 1517 |
+
camada_trabalhos_modelos.add_to(mapa)
|
| 1518 |
+
|
| 1519 |
if camada_trabalhos_proximos is not None:
|
| 1520 |
if int(trabalhos_tecnicos_raio_m or 0) > 0:
|
| 1521 |
for idx, avaliando in enumerate(avaliandos_geo):
|
|
|
|
| 1556 |
marker_icon = folium.DivIcon(
|
| 1557 |
html=(
|
| 1558 |
"<div style='display:flex;align-items:center;justify-content:center;"
|
| 1559 |
+
"width:30px;height:30px;filter:drop-shadow(0 2px 6px rgba(0,0,0,0.22));'>"
|
| 1560 |
+
"<div style='display:flex;align-items:center;justify-content:center;"
|
| 1561 |
+
"width:24px;height:24px;border-radius:999px;background:rgba(255,255,255,0.97);"
|
| 1562 |
+
"box-shadow:0 0 0 1.4px rgba(255,255,255,0.98),0 0 0 2.5px rgba(0,0,0,0.74);'>"
|
| 1563 |
+
"<svg viewBox='0 0 24 24' width='20' height='20' aria-hidden='true'>"
|
| 1564 |
+
"<path d='M12 3.2L3.9 10v10.1h5.4v-5.3h5.4v5.3h5.4V10L12 3.2Z' "
|
| 1565 |
+
"fill='#c62828' stroke='rgba(255,255,255,0.96)' stroke-width='1.2' stroke-linejoin='round'/>"
|
| 1566 |
+
"<rect x='10.1' y='15.2' width='3.8' height='4.9' rx='0.5' fill='rgba(255,255,255,0.96)'/>"
|
| 1567 |
+
"</svg>"
|
| 1568 |
+
"</div>"
|
| 1569 |
"</div>"
|
| 1570 |
),
|
| 1571 |
+
icon_size=(30, 30),
|
| 1572 |
+
icon_anchor=(15, 15),
|
| 1573 |
class_name="mesa-avaliando-marker",
|
| 1574 |
)
|
| 1575 |
folium.Marker(
|
backend/app/services/trabalhos_tecnicos_importer.py
CHANGED
|
@@ -13,7 +13,11 @@ from typing import Any
|
|
| 13 |
|
| 14 |
DEFAULT_SOURCE_XLSX_FILE = "dados_geocodificados_limpos_v2.xlsx"
|
| 15 |
DEFAULT_LOCAL_DB_FILE = "trabalhos_tecnicos.sqlite3"
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
TIPO_LABELS = {
|
| 18 |
"LA": "Laudo de Avaliacao",
|
| 19 |
"PT": "Parecer Tecnico",
|
|
@@ -48,9 +52,14 @@ XLSX_NS = {
|
|
| 48 |
class TrabalhoRawGroup:
|
| 49 |
raw_name: str
|
| 50 |
ano: int | None = None
|
|
|
|
|
|
|
| 51 |
endereco_base: str = ""
|
| 52 |
numero_base: str = ""
|
| 53 |
first_row_number: int = 0
|
|
|
|
|
|
|
|
|
|
| 54 |
modelos: dict[str, int] = field(default_factory=dict)
|
| 55 |
imoveis: dict[tuple[str, str, str, str], dict[str, Any]] = field(default_factory=dict)
|
| 56 |
registros: list[dict[str, Any]] = field(default_factory=list)
|
|
@@ -69,6 +78,14 @@ def _clean_text(value: Any) -> str:
|
|
| 69 |
return text
|
| 70 |
|
| 71 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
def _to_int(value: Any) -> int | None:
|
| 73 |
text = _clean_text(value)
|
| 74 |
if not text:
|
|
@@ -215,24 +232,41 @@ def read_xlsx_rows(path: str | Path) -> tuple[str, list[dict[str, Any]]]:
|
|
| 215 |
|
| 216 |
|
| 217 |
def _build_groups(records: list[dict[str, Any]]) -> tuple[list[TrabalhoRawGroup], int]:
|
| 218 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
if missing_columns:
|
| 220 |
raise ValueError(f"Planilha sem colunas obrigatorias: {', '.join(missing_columns)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
|
| 222 |
groups: dict[str, TrabalhoRawGroup] = {}
|
| 223 |
invalid_rows = 0
|
| 224 |
|
| 225 |
for offset, record in enumerate(records, start=2):
|
| 226 |
-
|
|
|
|
|
|
|
| 227 |
if not raw_name:
|
| 228 |
invalid_rows += 1
|
| 229 |
continue
|
| 230 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
group = groups.get(raw_name)
|
| 232 |
if group is None:
|
| 233 |
group = TrabalhoRawGroup(
|
| 234 |
raw_name=raw_name,
|
| 235 |
ano=_to_int(record.get("ANO")),
|
|
|
|
|
|
|
| 236 |
endereco_base=_clean_text(record.get("ENDERECO")),
|
| 237 |
numero_base=_clean_text(record.get("NUM")),
|
| 238 |
first_row_number=offset,
|
|
@@ -241,6 +275,10 @@ def _build_groups(records: list[dict[str, Any]]) -> tuple[list[TrabalhoRawGroup]
|
|
| 241 |
else:
|
| 242 |
if group.ano is None:
|
| 243 |
group.ano = _to_int(record.get("ANO"))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 244 |
if not group.endereco_base:
|
| 245 |
group.endereco_base = _clean_text(record.get("ENDERECO"))
|
| 246 |
if not group.numero_base:
|
|
@@ -252,6 +290,12 @@ def _build_groups(records: list[dict[str, Any]]) -> tuple[list[TrabalhoRawGroup]
|
|
| 252 |
coord_x = _to_float(record.get("x"))
|
| 253 |
coord_y = _to_float(record.get("y"))
|
| 254 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 255 |
if modelo_nome:
|
| 256 |
group.modelos.setdefault(modelo_nome, len(group.modelos) + 1)
|
| 257 |
|
|
@@ -280,6 +324,11 @@ def _build_groups(records: list[dict[str, Any]]) -> tuple[list[TrabalhoRawGroup]
|
|
| 280 |
"source_row": offset,
|
| 281 |
"ano": _to_int(record.get("ANO")),
|
| 282 |
"nome_original": raw_name,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
"modelo_nome": modelo_nome,
|
| 284 |
"endereco": endereco,
|
| 285 |
"numero": numero or "S/N",
|
|
@@ -292,9 +341,9 @@ def _build_groups(records: list[dict[str, Any]]) -> tuple[list[TrabalhoRawGroup]
|
|
| 292 |
|
| 293 |
used_names: dict[str, str] = {}
|
| 294 |
for group in ordered_groups:
|
| 295 |
-
base_name = build_clean_trabalho_name(group.raw_name, group.endereco_base, group.numero_base)
|
| 296 |
if not base_name:
|
| 297 |
-
base_name = _slugify(group.raw_name) or f"TRABALHO_{group.first_row_number}"
|
| 298 |
candidate = base_name
|
| 299 |
suffix = 2
|
| 300 |
while candidate in used_names and used_names[candidate] != group.raw_name:
|
|
@@ -306,6 +355,15 @@ def _build_groups(records: list[dict[str, Any]]) -> tuple[list[TrabalhoRawGroup]
|
|
| 306 |
return ordered_groups, invalid_rows
|
| 307 |
|
| 308 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
def build_database_from_xlsx(xlsx_path: str | Path, db_path: str | Path) -> dict[str, Any]:
|
| 310 |
source_path = Path(xlsx_path).expanduser().resolve()
|
| 311 |
target_path = Path(db_path).expanduser().resolve()
|
|
@@ -333,6 +391,8 @@ def build_database_from_xlsx(xlsx_path: str | Path, db_path: str | Path) -> dict
|
|
| 333 |
trabalho_id TEXT PRIMARY KEY,
|
| 334 |
nome TEXT NOT NULL,
|
| 335 |
nome_original TEXT NOT NULL,
|
|
|
|
|
|
|
| 336 |
tipo_codigo TEXT NOT NULL,
|
| 337 |
tipo_label TEXT NOT NULL,
|
| 338 |
ano INTEGER,
|
|
@@ -340,6 +400,9 @@ def build_database_from_xlsx(xlsx_path: str | Path, db_path: str | Path) -> dict
|
|
| 340 |
numero_principal TEXT,
|
| 341 |
endereco_resumo TEXT,
|
| 342 |
modelo_resumo TEXT,
|
|
|
|
|
|
|
|
|
|
| 343 |
total_registros INTEGER NOT NULL,
|
| 344 |
total_imoveis INTEGER NOT NULL,
|
| 345 |
total_modelos INTEGER NOT NULL,
|
|
@@ -375,6 +438,11 @@ def build_database_from_xlsx(xlsx_path: str | Path, db_path: str | Path) -> dict
|
|
| 375 |
source_row INTEGER NOT NULL,
|
| 376 |
ano INTEGER,
|
| 377 |
nome_original TEXT NOT NULL,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
modelo_nome TEXT,
|
| 379 |
endereco TEXT,
|
| 380 |
numero TEXT,
|
|
@@ -398,7 +466,7 @@ def build_database_from_xlsx(xlsx_path: str | Path, db_path: str | Path) -> dict
|
|
| 398 |
"invalid_row_count": str(invalid_rows),
|
| 399 |
"total_trabalhos": str(len(groups)),
|
| 400 |
"generated_at_utc": imported_at,
|
| 401 |
-
"generator_version": "sqlite-
|
| 402 |
}
|
| 403 |
conn.executemany(
|
| 404 |
"INSERT INTO meta (key, value) VALUES (?, ?)",
|
|
@@ -417,6 +485,9 @@ def build_database_from_xlsx(xlsx_path: str | Path, db_path: str | Path) -> dict
|
|
| 417 |
modelo_resumo = modelos_ordenados[0][0]
|
| 418 |
elif len(modelos_ordenados) > 1:
|
| 419 |
modelo_resumo = f"{len(modelos_ordenados)} modelos vinculados"
|
|
|
|
|
|
|
|
|
|
| 420 |
|
| 421 |
conn.execute(
|
| 422 |
"""
|
|
@@ -424,6 +495,8 @@ def build_database_from_xlsx(xlsx_path: str | Path, db_path: str | Path) -> dict
|
|
| 424 |
trabalho_id,
|
| 425 |
nome,
|
| 426 |
nome_original,
|
|
|
|
|
|
|
| 427 |
tipo_codigo,
|
| 428 |
tipo_label,
|
| 429 |
ano,
|
|
@@ -431,16 +504,21 @@ def build_database_from_xlsx(xlsx_path: str | Path, db_path: str | Path) -> dict
|
|
| 431 |
numero_principal,
|
| 432 |
endereco_resumo,
|
| 433 |
modelo_resumo,
|
|
|
|
|
|
|
|
|
|
| 434 |
total_registros,
|
| 435 |
total_imoveis,
|
| 436 |
total_modelos,
|
| 437 |
tem_coordenadas
|
| 438 |
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 439 |
""",
|
| 440 |
(
|
| 441 |
group.clean_name,
|
| 442 |
group.clean_name,
|
| 443 |
group.raw_name,
|
|
|
|
|
|
|
| 444 |
tipo_codigo,
|
| 445 |
_tipo_label(tipo_codigo),
|
| 446 |
group.ano,
|
|
@@ -448,6 +526,9 @@ def build_database_from_xlsx(xlsx_path: str | Path, db_path: str | Path) -> dict
|
|
| 448 |
group.numero_base or "S/N",
|
| 449 |
endereco_resumo,
|
| 450 |
modelo_resumo,
|
|
|
|
|
|
|
|
|
|
| 451 |
len(group.registros),
|
| 452 |
len(imoveis_ordenados),
|
| 453 |
len(modelos_ordenados),
|
|
@@ -491,18 +572,28 @@ def build_database_from_xlsx(xlsx_path: str | Path, db_path: str | Path) -> dict
|
|
| 491 |
source_row,
|
| 492 |
ano,
|
| 493 |
nome_original,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
modelo_nome,
|
| 495 |
endereco,
|
| 496 |
numero,
|
| 497 |
coord_x,
|
| 498 |
coord_y
|
| 499 |
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 500 |
""",
|
| 501 |
(
|
| 502 |
group.clean_name,
|
| 503 |
registro["source_row"],
|
| 504 |
registro["ano"],
|
| 505 |
registro["nome_original"],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
registro["modelo_nome"],
|
| 507 |
registro["endereco"],
|
| 508 |
registro["numero"],
|
|
|
|
| 13 |
|
| 14 |
DEFAULT_SOURCE_XLSX_FILE = "dados_geocodificados_limpos_v2.xlsx"
|
| 15 |
DEFAULT_LOCAL_DB_FILE = "trabalhos_tecnicos.sqlite3"
|
| 16 |
+
COLUNAS_BASE_OBRIGATORIAS = ["ANO", "MODELO", "ENDERECO", "NUM", "x", "y"]
|
| 17 |
+
COLUNAS_IDENTIFICADORAS = ["NOME_PASTA", "AVALIANDO", "TRABALHO"]
|
| 18 |
+
COLUNAS_TECNICO = ["TÉCNICO", "TECNICO"]
|
| 19 |
+
COLUNA_PROCESSO = "PROCESSO"
|
| 20 |
+
COLUNA_FINALIDADE_PROCESSO = "FINALIDADE PROCESSO"
|
| 21 |
TIPO_LABELS = {
|
| 22 |
"LA": "Laudo de Avaliacao",
|
| 23 |
"PT": "Parecer Tecnico",
|
|
|
|
| 52 |
class TrabalhoRawGroup:
|
| 53 |
raw_name: str
|
| 54 |
ano: int | None = None
|
| 55 |
+
codigo_trabalho: str = ""
|
| 56 |
+
nome_pasta: str = ""
|
| 57 |
endereco_base: str = ""
|
| 58 |
numero_base: str = ""
|
| 59 |
first_row_number: int = 0
|
| 60 |
+
tecnicos: dict[str, int] = field(default_factory=dict)
|
| 61 |
+
processos: dict[str, int] = field(default_factory=dict)
|
| 62 |
+
finalidades_processo: dict[str, int] = field(default_factory=dict)
|
| 63 |
modelos: dict[str, int] = field(default_factory=dict)
|
| 64 |
imoveis: dict[tuple[str, str, str, str], dict[str, Any]] = field(default_factory=dict)
|
| 65 |
registros: list[dict[str, Any]] = field(default_factory=list)
|
|
|
|
| 78 |
return text
|
| 79 |
|
| 80 |
|
| 81 |
+
def _first_non_empty(record: dict[str, Any], *keys: str) -> str:
|
| 82 |
+
for key in keys:
|
| 83 |
+
text = _clean_text(record.get(key))
|
| 84 |
+
if text:
|
| 85 |
+
return text
|
| 86 |
+
return ""
|
| 87 |
+
|
| 88 |
+
|
| 89 |
def _to_int(value: Any) -> int | None:
|
| 90 |
text = _clean_text(value)
|
| 91 |
if not text:
|
|
|
|
| 232 |
|
| 233 |
|
| 234 |
def _build_groups(records: list[dict[str, Any]]) -> tuple[list[TrabalhoRawGroup], int]:
|
| 235 |
+
if not records:
|
| 236 |
+
raise ValueError("Planilha sem linhas de dados")
|
| 237 |
+
|
| 238 |
+
available_columns = set(records[0].keys())
|
| 239 |
+
missing_columns = [column for column in COLUNAS_BASE_OBRIGATORIAS if column not in available_columns]
|
| 240 |
if missing_columns:
|
| 241 |
raise ValueError(f"Planilha sem colunas obrigatorias: {', '.join(missing_columns)}")
|
| 242 |
+
if not any(column in available_columns for column in COLUNAS_IDENTIFICADORAS):
|
| 243 |
+
raise ValueError(
|
| 244 |
+
"Planilha sem coluna identificadora obrigatoria: informe ao menos uma entre "
|
| 245 |
+
+ ", ".join(COLUNAS_IDENTIFICADORAS)
|
| 246 |
+
)
|
| 247 |
|
| 248 |
groups: dict[str, TrabalhoRawGroup] = {}
|
| 249 |
invalid_rows = 0
|
| 250 |
|
| 251 |
for offset, record in enumerate(records, start=2):
|
| 252 |
+
nome_pasta = _clean_text(record.get("NOME_PASTA"))
|
| 253 |
+
codigo_trabalho = _clean_text(record.get("TRABALHO"))
|
| 254 |
+
raw_name = _first_non_empty(record, "NOME_PASTA", "AVALIANDO", "TRABALHO")
|
| 255 |
if not raw_name:
|
| 256 |
invalid_rows += 1
|
| 257 |
continue
|
| 258 |
|
| 259 |
+
tecnico = _first_non_empty(record, *COLUNAS_TECNICO)
|
| 260 |
+
processo = _clean_text(record.get(COLUNA_PROCESSO))
|
| 261 |
+
finalidade_processo = _clean_text(record.get(COLUNA_FINALIDADE_PROCESSO))
|
| 262 |
+
|
| 263 |
group = groups.get(raw_name)
|
| 264 |
if group is None:
|
| 265 |
group = TrabalhoRawGroup(
|
| 266 |
raw_name=raw_name,
|
| 267 |
ano=_to_int(record.get("ANO")),
|
| 268 |
+
codigo_trabalho=codigo_trabalho,
|
| 269 |
+
nome_pasta=nome_pasta,
|
| 270 |
endereco_base=_clean_text(record.get("ENDERECO")),
|
| 271 |
numero_base=_clean_text(record.get("NUM")),
|
| 272 |
first_row_number=offset,
|
|
|
|
| 275 |
else:
|
| 276 |
if group.ano is None:
|
| 277 |
group.ano = _to_int(record.get("ANO"))
|
| 278 |
+
if not group.codigo_trabalho:
|
| 279 |
+
group.codigo_trabalho = codigo_trabalho
|
| 280 |
+
if not group.nome_pasta:
|
| 281 |
+
group.nome_pasta = nome_pasta
|
| 282 |
if not group.endereco_base:
|
| 283 |
group.endereco_base = _clean_text(record.get("ENDERECO"))
|
| 284 |
if not group.numero_base:
|
|
|
|
| 290 |
coord_x = _to_float(record.get("x"))
|
| 291 |
coord_y = _to_float(record.get("y"))
|
| 292 |
|
| 293 |
+
if tecnico:
|
| 294 |
+
group.tecnicos.setdefault(tecnico, len(group.tecnicos) + 1)
|
| 295 |
+
if processo:
|
| 296 |
+
group.processos.setdefault(processo, len(group.processos) + 1)
|
| 297 |
+
if finalidade_processo:
|
| 298 |
+
group.finalidades_processo.setdefault(finalidade_processo, len(group.finalidades_processo) + 1)
|
| 299 |
if modelo_nome:
|
| 300 |
group.modelos.setdefault(modelo_nome, len(group.modelos) + 1)
|
| 301 |
|
|
|
|
| 324 |
"source_row": offset,
|
| 325 |
"ano": _to_int(record.get("ANO")),
|
| 326 |
"nome_original": raw_name,
|
| 327 |
+
"codigo_trabalho": codigo_trabalho,
|
| 328 |
+
"nome_pasta": nome_pasta,
|
| 329 |
+
"tecnico": tecnico,
|
| 330 |
+
"processo": processo,
|
| 331 |
+
"finalidade_processo": finalidade_processo,
|
| 332 |
"modelo_nome": modelo_nome,
|
| 333 |
"endereco": endereco,
|
| 334 |
"numero": numero or "S/N",
|
|
|
|
| 341 |
|
| 342 |
used_names: dict[str, str] = {}
|
| 343 |
for group in ordered_groups:
|
| 344 |
+
base_name = build_clean_trabalho_name(group.nome_pasta or group.raw_name, group.endereco_base, group.numero_base)
|
| 345 |
if not base_name:
|
| 346 |
+
base_name = _slugify(group.nome_pasta or group.raw_name) or f"TRABALHO_{group.first_row_number}"
|
| 347 |
candidate = base_name
|
| 348 |
suffix = 2
|
| 349 |
while candidate in used_names and used_names[candidate] != group.raw_name:
|
|
|
|
| 355 |
return ordered_groups, invalid_rows
|
| 356 |
|
| 357 |
|
| 358 |
+
def _summarize_ordered_values(values: dict[str, int]) -> str:
|
| 359 |
+
ordered = [value for value, _ in sorted(values.items(), key=lambda item: (item[1], item[0].lower()))]
|
| 360 |
+
if not ordered:
|
| 361 |
+
return ""
|
| 362 |
+
if len(ordered) == 1:
|
| 363 |
+
return ordered[0]
|
| 364 |
+
return f"{ordered[0]} (+{len(ordered) - 1})"
|
| 365 |
+
|
| 366 |
+
|
| 367 |
def build_database_from_xlsx(xlsx_path: str | Path, db_path: str | Path) -> dict[str, Any]:
|
| 368 |
source_path = Path(xlsx_path).expanduser().resolve()
|
| 369 |
target_path = Path(db_path).expanduser().resolve()
|
|
|
|
| 391 |
trabalho_id TEXT PRIMARY KEY,
|
| 392 |
nome TEXT NOT NULL,
|
| 393 |
nome_original TEXT NOT NULL,
|
| 394 |
+
codigo_trabalho TEXT,
|
| 395 |
+
nome_pasta TEXT,
|
| 396 |
tipo_codigo TEXT NOT NULL,
|
| 397 |
tipo_label TEXT NOT NULL,
|
| 398 |
ano INTEGER,
|
|
|
|
| 400 |
numero_principal TEXT,
|
| 401 |
endereco_resumo TEXT,
|
| 402 |
modelo_resumo TEXT,
|
| 403 |
+
tecnico_resumo TEXT,
|
| 404 |
+
processo_resumo TEXT,
|
| 405 |
+
finalidade_processo_resumo TEXT,
|
| 406 |
total_registros INTEGER NOT NULL,
|
| 407 |
total_imoveis INTEGER NOT NULL,
|
| 408 |
total_modelos INTEGER NOT NULL,
|
|
|
|
| 438 |
source_row INTEGER NOT NULL,
|
| 439 |
ano INTEGER,
|
| 440 |
nome_original TEXT NOT NULL,
|
| 441 |
+
codigo_trabalho TEXT,
|
| 442 |
+
nome_pasta TEXT,
|
| 443 |
+
tecnico TEXT,
|
| 444 |
+
processo TEXT,
|
| 445 |
+
finalidade_processo TEXT,
|
| 446 |
modelo_nome TEXT,
|
| 447 |
endereco TEXT,
|
| 448 |
numero TEXT,
|
|
|
|
| 466 |
"invalid_row_count": str(invalid_rows),
|
| 467 |
"total_trabalhos": str(len(groups)),
|
| 468 |
"generated_at_utc": imported_at,
|
| 469 |
+
"generator_version": "sqlite-v2",
|
| 470 |
}
|
| 471 |
conn.executemany(
|
| 472 |
"INSERT INTO meta (key, value) VALUES (?, ?)",
|
|
|
|
| 485 |
modelo_resumo = modelos_ordenados[0][0]
|
| 486 |
elif len(modelos_ordenados) > 1:
|
| 487 |
modelo_resumo = f"{len(modelos_ordenados)} modelos vinculados"
|
| 488 |
+
tecnico_resumo = _summarize_ordered_values(group.tecnicos)
|
| 489 |
+
processo_resumo = _summarize_ordered_values(group.processos)
|
| 490 |
+
finalidade_processo_resumo = _summarize_ordered_values(group.finalidades_processo)
|
| 491 |
|
| 492 |
conn.execute(
|
| 493 |
"""
|
|
|
|
| 495 |
trabalho_id,
|
| 496 |
nome,
|
| 497 |
nome_original,
|
| 498 |
+
codigo_trabalho,
|
| 499 |
+
nome_pasta,
|
| 500 |
tipo_codigo,
|
| 501 |
tipo_label,
|
| 502 |
ano,
|
|
|
|
| 504 |
numero_principal,
|
| 505 |
endereco_resumo,
|
| 506 |
modelo_resumo,
|
| 507 |
+
tecnico_resumo,
|
| 508 |
+
processo_resumo,
|
| 509 |
+
finalidade_processo_resumo,
|
| 510 |
total_registros,
|
| 511 |
total_imoveis,
|
| 512 |
total_modelos,
|
| 513 |
tem_coordenadas
|
| 514 |
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 515 |
""",
|
| 516 |
(
|
| 517 |
group.clean_name,
|
| 518 |
group.clean_name,
|
| 519 |
group.raw_name,
|
| 520 |
+
group.codigo_trabalho or None,
|
| 521 |
+
group.nome_pasta or None,
|
| 522 |
tipo_codigo,
|
| 523 |
_tipo_label(tipo_codigo),
|
| 524 |
group.ano,
|
|
|
|
| 526 |
group.numero_base or "S/N",
|
| 527 |
endereco_resumo,
|
| 528 |
modelo_resumo,
|
| 529 |
+
tecnico_resumo or None,
|
| 530 |
+
processo_resumo or None,
|
| 531 |
+
finalidade_processo_resumo or None,
|
| 532 |
len(group.registros),
|
| 533 |
len(imoveis_ordenados),
|
| 534 |
len(modelos_ordenados),
|
|
|
|
| 572 |
source_row,
|
| 573 |
ano,
|
| 574 |
nome_original,
|
| 575 |
+
codigo_trabalho,
|
| 576 |
+
nome_pasta,
|
| 577 |
+
tecnico,
|
| 578 |
+
processo,
|
| 579 |
+
finalidade_processo,
|
| 580 |
modelo_nome,
|
| 581 |
endereco,
|
| 582 |
numero,
|
| 583 |
coord_x,
|
| 584 |
coord_y
|
| 585 |
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 586 |
""",
|
| 587 |
(
|
| 588 |
group.clean_name,
|
| 589 |
registro["source_row"],
|
| 590 |
registro["ano"],
|
| 591 |
registro["nome_original"],
|
| 592 |
+
registro["codigo_trabalho"] or None,
|
| 593 |
+
registro["nome_pasta"] or None,
|
| 594 |
+
registro["tecnico"] or None,
|
| 595 |
+
registro["processo"] or None,
|
| 596 |
+
registro["finalidade_processo"] or None,
|
| 597 |
registro["modelo_nome"],
|
| 598 |
registro["endereco"],
|
| 599 |
registro["numero"],
|
backend/app/services/trabalhos_tecnicos_service.py
CHANGED
|
@@ -349,6 +349,80 @@ def _dedupe_text_list(values: Any) -> list[str]:
|
|
| 349 |
return resultado
|
| 350 |
|
| 351 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
def _tipo_label_from_codigo(codigo: Any, fallback: Any = None) -> str:
|
| 353 |
codigo_texto = str(codigo or "").strip().upper()
|
| 354 |
if codigo_texto in TIPO_LABELS:
|
|
@@ -432,6 +506,7 @@ def _parse_override_payload(payload_json: str | None) -> dict[str, Any]:
|
|
| 432 |
"nome": str(raw.get("nome") or "").strip(),
|
| 433 |
"tipo_codigo": str(raw.get("tipo_codigo") or "").strip().upper(),
|
| 434 |
"ano": _to_int_or_none(raw.get("ano")),
|
|
|
|
| 435 |
"processos_sei": _dedupe_text_list(raw.get("processos_sei")),
|
| 436 |
"modelos": modelos,
|
| 437 |
"imoveis": imoveis,
|
|
@@ -456,7 +531,9 @@ def _apply_override_trabalho(
|
|
| 456 |
) -> dict[str, Any]:
|
| 457 |
if not override:
|
| 458 |
trabalho = dict(base)
|
| 459 |
-
|
|
|
|
|
|
|
| 460 |
return trabalho
|
| 461 |
|
| 462 |
trabalho = dict(base)
|
|
@@ -479,7 +556,12 @@ def _apply_override_trabalho(
|
|
| 479 |
trabalho["tipo_codigo"] = override.get("tipo_codigo") or trabalho.get("tipo_codigo") or ""
|
| 480 |
trabalho["tipo_label"] = _tipo_label_from_codigo(trabalho.get("tipo_codigo"), trabalho.get("tipo_label"))
|
| 481 |
trabalho["ano"] = override.get("ano")
|
| 482 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
trabalho["imoveis"] = imoveis
|
| 484 |
trabalho["modelos"] = _enriquecer_modelos(modelos_raw, catalogo_modelos)
|
| 485 |
trabalho["total_imoveis"] = len(imoveis)
|
|
@@ -563,6 +645,7 @@ def _carregar_trabalho_base(
|
|
| 563 |
""",
|
| 564 |
(trabalho_id,),
|
| 565 |
).fetchall()
|
|
|
|
| 566 |
|
| 567 |
modelos = _enriquecer_modelos([str(row["modelo_nome"]) for row in modelo_rows], catalogo_modelos)
|
| 568 |
modelos_por_imovel: dict[int, list[str]] = {}
|
|
@@ -597,7 +680,8 @@ def _carregar_trabalho_base(
|
|
| 597 |
"total_imoveis": int(trabalho_row["total_imoveis"] or 0),
|
| 598 |
"total_modelos": int(trabalho_row["total_modelos"] or 0),
|
| 599 |
"tem_coordenadas": bool(trabalho_row["tem_coordenadas"]),
|
| 600 |
-
"processos_sei":
|
|
|
|
| 601 |
"modelos": modelos,
|
| 602 |
"imoveis": imoveis,
|
| 603 |
}
|
|
@@ -626,7 +710,10 @@ def _normalizar_edicao_payload(
|
|
| 626 |
nome = str(payload.get("nome") or base.get("nome") or "").strip()
|
| 627 |
tipo_codigo = str(payload.get("tipo_codigo") or base.get("tipo_codigo") or "").strip().upper()
|
| 628 |
ano = _to_int_or_none(payload.get("ano"))
|
| 629 |
-
|
|
|
|
|
|
|
|
|
|
| 630 |
|
| 631 |
modelos_informados = _dedupe_text_list(payload.get("modelos"))
|
| 632 |
if not modelos_informados:
|
|
@@ -675,6 +762,7 @@ def _listar_avaliandos_tecnicos(chaves_modelo: list[str] | None = None) -> list[
|
|
| 675 |
conn = _connect_database(resolved.db_path)
|
| 676 |
try:
|
| 677 |
overrides = _fetch_overrides(conn)
|
|
|
|
| 678 |
rows = conn.execute(
|
| 679 |
"""
|
| 680 |
SELECT
|
|
@@ -737,6 +825,7 @@ def _listar_avaliandos_tecnicos(chaves_modelo: list[str] | None = None) -> list[
|
|
| 737 |
"coord_lon": float(coord_x),
|
| 738 |
"coord_lat": float(coord_y),
|
| 739 |
"modelos_relacionados": set(),
|
|
|
|
| 740 |
},
|
| 741 |
)
|
| 742 |
if modelo_nome:
|
|
@@ -1205,6 +1294,7 @@ def listar_trabalhos() -> dict[str, Any]:
|
|
| 1205 |
try:
|
| 1206 |
meta = _fetch_meta(conn)
|
| 1207 |
overrides = _fetch_overrides(conn)
|
|
|
|
| 1208 |
trabalhos_rows = conn.execute(
|
| 1209 |
"""
|
| 1210 |
SELECT
|
|
@@ -1238,6 +1328,7 @@ def listar_trabalhos() -> dict[str, Any]:
|
|
| 1238 |
total_com_modelo_mesa = 0
|
| 1239 |
for row in trabalhos_rows:
|
| 1240 |
trabalho_id = str(row["trabalho_id"])
|
|
|
|
| 1241 |
base = {
|
| 1242 |
"id": trabalho_id,
|
| 1243 |
"nome": str(row["nome"]),
|
|
@@ -1251,7 +1342,8 @@ def listar_trabalhos() -> dict[str, Any]:
|
|
| 1251 |
"total_imoveis": int(row["total_imoveis"] or 0),
|
| 1252 |
"total_modelos": int(row["total_modelos"] or 0),
|
| 1253 |
"tem_coordenadas": bool(row["tem_coordenadas"]),
|
| 1254 |
-
"processos_sei":
|
|
|
|
| 1255 |
"modelos": _enriquecer_modelos(modelos_por_trabalho.get(trabalho_id, []), catalogo_modelos),
|
| 1256 |
"imoveis": [],
|
| 1257 |
}
|
|
|
|
| 349 |
return resultado
|
| 350 |
|
| 351 |
|
| 352 |
+
def _table_has_column(conn: sqlite3.Connection, table_name: str, column_name: str) -> bool:
|
| 353 |
+
try:
|
| 354 |
+
rows = conn.execute(f"PRAGMA table_info({table_name})").fetchall()
|
| 355 |
+
except Exception:
|
| 356 |
+
return False
|
| 357 |
+
column_name_norm = str(column_name or "").strip().casefold()
|
| 358 |
+
return any(str(row["name"] or "").strip().casefold() == column_name_norm for row in rows)
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def _resumo_textos(valores: list[str], limite_inline: int = 2) -> str:
|
| 362 |
+
unicos = _dedupe_text_list(valores)
|
| 363 |
+
if not unicos:
|
| 364 |
+
return ""
|
| 365 |
+
if len(unicos) <= limite_inline:
|
| 366 |
+
return " | ".join(unicos)
|
| 367 |
+
return " | ".join(unicos[:limite_inline]) + f" | +{len(unicos) - limite_inline}"
|
| 368 |
+
|
| 369 |
+
|
| 370 |
+
def _carregar_processos_importados_por_trabalho(
|
| 371 |
+
conn: sqlite3.Connection,
|
| 372 |
+
trabalho_id: str | None = None,
|
| 373 |
+
) -> dict[str, list[str]]:
|
| 374 |
+
processos_por_trabalho: dict[str, list[str]] = {}
|
| 375 |
+
|
| 376 |
+
if _table_has_column(conn, "trabalho_registros", "processo"):
|
| 377 |
+
params: list[Any] = []
|
| 378 |
+
where_clause = "WHERE TRIM(COALESCE(processo, '')) <> ''"
|
| 379 |
+
if trabalho_id:
|
| 380 |
+
where_clause += " AND trabalho_id = ?"
|
| 381 |
+
params.append(trabalho_id)
|
| 382 |
+
rows = conn.execute(
|
| 383 |
+
f"""
|
| 384 |
+
SELECT trabalho_id, processo
|
| 385 |
+
FROM trabalho_registros
|
| 386 |
+
{where_clause}
|
| 387 |
+
ORDER BY trabalho_id, source_row, LOWER(TRIM(processo))
|
| 388 |
+
""",
|
| 389 |
+
params,
|
| 390 |
+
).fetchall()
|
| 391 |
+
for row in rows:
|
| 392 |
+
chave = str(row["trabalho_id"] or "").strip()
|
| 393 |
+
processo = str(row["processo"] or "").strip()
|
| 394 |
+
if not chave or not processo:
|
| 395 |
+
continue
|
| 396 |
+
processos_por_trabalho.setdefault(chave, []).append(processo)
|
| 397 |
+
|
| 398 |
+
if processos_por_trabalho:
|
| 399 |
+
return {chave: _dedupe_text_list(valores) for chave, valores in processos_por_trabalho.items()}
|
| 400 |
+
|
| 401 |
+
if _table_has_column(conn, "trabalhos", "processo_resumo"):
|
| 402 |
+
params = []
|
| 403 |
+
where_clause = "WHERE TRIM(COALESCE(processo_resumo, '')) <> ''"
|
| 404 |
+
if trabalho_id:
|
| 405 |
+
where_clause += " AND trabalho_id = ?"
|
| 406 |
+
params.append(trabalho_id)
|
| 407 |
+
rows = conn.execute(
|
| 408 |
+
f"""
|
| 409 |
+
SELECT trabalho_id, processo_resumo
|
| 410 |
+
FROM trabalhos
|
| 411 |
+
{where_clause}
|
| 412 |
+
ORDER BY LOWER(trabalho_id)
|
| 413 |
+
""",
|
| 414 |
+
params,
|
| 415 |
+
).fetchall()
|
| 416 |
+
for row in rows:
|
| 417 |
+
chave = str(row["trabalho_id"] or "").strip()
|
| 418 |
+
processo = str(row["processo_resumo"] or "").strip()
|
| 419 |
+
if not chave or not processo:
|
| 420 |
+
continue
|
| 421 |
+
processos_por_trabalho[chave] = [processo]
|
| 422 |
+
|
| 423 |
+
return {chave: _dedupe_text_list(valores) for chave, valores in processos_por_trabalho.items()}
|
| 424 |
+
|
| 425 |
+
|
| 426 |
def _tipo_label_from_codigo(codigo: Any, fallback: Any = None) -> str:
|
| 427 |
codigo_texto = str(codigo or "").strip().upper()
|
| 428 |
if codigo_texto in TIPO_LABELS:
|
|
|
|
| 506 |
"nome": str(raw.get("nome") or "").strip(),
|
| 507 |
"tipo_codigo": str(raw.get("tipo_codigo") or "").strip().upper(),
|
| 508 |
"ano": _to_int_or_none(raw.get("ano")),
|
| 509 |
+
"processos_sei_informados": "processos_sei" in raw,
|
| 510 |
"processos_sei": _dedupe_text_list(raw.get("processos_sei")),
|
| 511 |
"modelos": modelos,
|
| 512 |
"imoveis": imoveis,
|
|
|
|
| 531 |
) -> dict[str, Any]:
|
| 532 |
if not override:
|
| 533 |
trabalho = dict(base)
|
| 534 |
+
processos_base = _dedupe_text_list(base.get("processos_sei"))
|
| 535 |
+
trabalho["processos_sei"] = processos_base
|
| 536 |
+
trabalho["processos_sei_resumo"] = _resumo_textos(processos_base, limite_inline=1)
|
| 537 |
return trabalho
|
| 538 |
|
| 539 |
trabalho = dict(base)
|
|
|
|
| 556 |
trabalho["tipo_codigo"] = override.get("tipo_codigo") or trabalho.get("tipo_codigo") or ""
|
| 557 |
trabalho["tipo_label"] = _tipo_label_from_codigo(trabalho.get("tipo_codigo"), trabalho.get("tipo_label"))
|
| 558 |
trabalho["ano"] = override.get("ano")
|
| 559 |
+
processos_base = _dedupe_text_list(base.get("processos_sei"))
|
| 560 |
+
if override.get("processos_sei_informados"):
|
| 561 |
+
trabalho["processos_sei"] = _dedupe_text_list(override.get("processos_sei"))
|
| 562 |
+
else:
|
| 563 |
+
trabalho["processos_sei"] = processos_base
|
| 564 |
+
trabalho["processos_sei_resumo"] = _resumo_textos(_dedupe_text_list(trabalho.get("processos_sei")), limite_inline=1)
|
| 565 |
trabalho["imoveis"] = imoveis
|
| 566 |
trabalho["modelos"] = _enriquecer_modelos(modelos_raw, catalogo_modelos)
|
| 567 |
trabalho["total_imoveis"] = len(imoveis)
|
|
|
|
| 645 |
""",
|
| 646 |
(trabalho_id,),
|
| 647 |
).fetchall()
|
| 648 |
+
processos_sei = _carregar_processos_importados_por_trabalho(conn, trabalho_id).get(trabalho_id, [])
|
| 649 |
|
| 650 |
modelos = _enriquecer_modelos([str(row["modelo_nome"]) for row in modelo_rows], catalogo_modelos)
|
| 651 |
modelos_por_imovel: dict[int, list[str]] = {}
|
|
|
|
| 680 |
"total_imoveis": int(trabalho_row["total_imoveis"] or 0),
|
| 681 |
"total_modelos": int(trabalho_row["total_modelos"] or 0),
|
| 682 |
"tem_coordenadas": bool(trabalho_row["tem_coordenadas"]),
|
| 683 |
+
"processos_sei": processos_sei,
|
| 684 |
+
"processos_sei_resumo": _resumo_textos(processos_sei, limite_inline=1),
|
| 685 |
"modelos": modelos,
|
| 686 |
"imoveis": imoveis,
|
| 687 |
}
|
|
|
|
| 710 |
nome = str(payload.get("nome") or base.get("nome") or "").strip()
|
| 711 |
tipo_codigo = str(payload.get("tipo_codigo") or base.get("tipo_codigo") or "").strip().upper()
|
| 712 |
ano = _to_int_or_none(payload.get("ano"))
|
| 713 |
+
if "processos_sei" in payload:
|
| 714 |
+
processos_sei = _dedupe_text_list(payload.get("processos_sei"))
|
| 715 |
+
else:
|
| 716 |
+
processos_sei = _dedupe_text_list(base.get("processos_sei"))
|
| 717 |
|
| 718 |
modelos_informados = _dedupe_text_list(payload.get("modelos"))
|
| 719 |
if not modelos_informados:
|
|
|
|
| 762 |
conn = _connect_database(resolved.db_path)
|
| 763 |
try:
|
| 764 |
overrides = _fetch_overrides(conn)
|
| 765 |
+
processos_por_trabalho = _carregar_processos_importados_por_trabalho(conn)
|
| 766 |
rows = conn.execute(
|
| 767 |
"""
|
| 768 |
SELECT
|
|
|
|
| 825 |
"coord_lon": float(coord_x),
|
| 826 |
"coord_lat": float(coord_y),
|
| 827 |
"modelos_relacionados": set(),
|
| 828 |
+
"processos_sei": list(processos_por_trabalho.get(trabalho_id, [])),
|
| 829 |
},
|
| 830 |
)
|
| 831 |
if modelo_nome:
|
|
|
|
| 1294 |
try:
|
| 1295 |
meta = _fetch_meta(conn)
|
| 1296 |
overrides = _fetch_overrides(conn)
|
| 1297 |
+
processos_por_trabalho = _carregar_processos_importados_por_trabalho(conn)
|
| 1298 |
trabalhos_rows = conn.execute(
|
| 1299 |
"""
|
| 1300 |
SELECT
|
|
|
|
| 1328 |
total_com_modelo_mesa = 0
|
| 1329 |
for row in trabalhos_rows:
|
| 1330 |
trabalho_id = str(row["trabalho_id"])
|
| 1331 |
+
processos_sei = list(processos_por_trabalho.get(trabalho_id, []))
|
| 1332 |
base = {
|
| 1333 |
"id": trabalho_id,
|
| 1334 |
"nome": str(row["nome"]),
|
|
|
|
| 1342 |
"total_imoveis": int(row["total_imoveis"] or 0),
|
| 1343 |
"total_modelos": int(row["total_modelos"] or 0),
|
| 1344 |
"tem_coordenadas": bool(row["tem_coordenadas"]),
|
| 1345 |
+
"processos_sei": processos_sei,
|
| 1346 |
+
"processos_sei_resumo": _resumo_textos(processos_sei, limite_inline=1),
|
| 1347 |
"modelos": _enriquecer_modelos(modelos_por_trabalho.get(trabalho_id, []), catalogo_modelos),
|
| 1348 |
"imoveis": [],
|
| 1349 |
}
|
frontend/src/api.js
CHANGED
|
@@ -301,9 +301,10 @@ export const api = {
|
|
| 301 |
valores_x: valoresX,
|
| 302 |
indice_base: indiceBase,
|
| 303 |
}),
|
| 304 |
-
evaluationKnnDetailsElab: (sessionId, valoresX) => postJson('/api/elaboracao/evaluation/knn-details', {
|
| 305 |
session_id: sessionId,
|
| 306 |
valores_x: valoresX,
|
|
|
|
| 307 |
}),
|
| 308 |
evaluationClearElab: (sessionId) => postJson('/api/elaboracao/evaluation/clear', { session_id: sessionId }),
|
| 309 |
evaluationDeleteElab: (sessionId, indice, indiceBase) => postJson('/api/elaboracao/evaluation/delete', {
|
|
|
|
| 301 |
valores_x: valoresX,
|
| 302 |
indice_base: indiceBase,
|
| 303 |
}),
|
| 304 |
+
evaluationKnnDetailsElab: (sessionId, valoresX = null, indiceAvaliacao = null) => postJson('/api/elaboracao/evaluation/knn-details', {
|
| 305 |
session_id: sessionId,
|
| 306 |
valores_x: valoresX,
|
| 307 |
+
indice_avaliacao: indiceAvaliacao,
|
| 308 |
}),
|
| 309 |
evaluationClearElab: (sessionId) => postJson('/api/elaboracao/evaluation/clear', { session_id: sessionId }),
|
| 310 |
evaluationDeleteElab: (sessionId, indice, indiceBase) => postJson('/api/elaboracao/evaluation/delete', {
|
frontend/src/components/ElaboracaoTab.jsx
CHANGED
|
@@ -3299,7 +3299,9 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 3299 |
const idx = Number(indiceRaw) - 1
|
| 3300 |
if (!Number.isInteger(idx) || idx < 0) return
|
| 3301 |
const avaliacao = Array.isArray(avaliacoesResultado) ? avaliacoesResultado[idx] : null
|
| 3302 |
-
|
|
|
|
|
|
|
| 3303 |
|
| 3304 |
setKnnDetalheAberto(true)
|
| 3305 |
setKnnDetalheLoading(true)
|
|
@@ -3312,7 +3314,7 @@ export default function ElaboracaoTab({ sessionId, authUser, quickLoadRequest =
|
|
| 3312 |
setKnnDetalheInfo(null)
|
| 3313 |
|
| 3314 |
try {
|
| 3315 |
-
const resp = await api.evaluationKnnDetailsElab(sessionId,
|
| 3316 |
setKnnDetalheMapaHtml(String(resp?.mapa_html || ''))
|
| 3317 |
setKnnDetalheMapaPayload(resp?.mapa_payload || null)
|
| 3318 |
setKnnDetalheAvaliando(Array.isArray(resp?.avaliando) ? resp.avaliando : [])
|
|
|
|
| 3299 |
const idx = Number(indiceRaw) - 1
|
| 3300 |
if (!Number.isInteger(idx) || idx < 0) return
|
| 3301 |
const avaliacao = Array.isArray(avaliacoesResultado) ? avaliacoesResultado[idx] : null
|
| 3302 |
+
const valoresKnn = avaliacao && typeof avaliacao === 'object'
|
| 3303 |
+
? construirPayloadKnnAvaliacao(avaliacao)
|
| 3304 |
+
: null
|
| 3305 |
|
| 3306 |
setKnnDetalheAberto(true)
|
| 3307 |
setKnnDetalheLoading(true)
|
|
|
|
| 3314 |
setKnnDetalheInfo(null)
|
| 3315 |
|
| 3316 |
try {
|
| 3317 |
+
const resp = await api.evaluationKnnDetailsElab(sessionId, valoresKnn, idx + 1)
|
| 3318 |
setKnnDetalheMapaHtml(String(resp?.mapa_html || ''))
|
| 3319 |
setKnnDetalheMapaPayload(resp?.mapa_payload || null)
|
| 3320 |
setKnnDetalheAvaliando(Array.isArray(resp?.avaliando) ? resp.avaliando : [])
|
frontend/src/components/LeafletMapFrame.jsx
CHANGED
|
@@ -343,8 +343,8 @@ function buildLegacyOverlayLayers(payload) {
|
|
| 343 |
|
| 344 |
if (Array.isArray(payload?.trabalhos_tecnicos_points) && payload.trabalhos_tecnicos_points.length) {
|
| 345 |
overlays.push({
|
| 346 |
-
id: '
|
| 347 |
-
label: '
|
| 348 |
show: true,
|
| 349 |
markers: payload.trabalhos_tecnicos_points,
|
| 350 |
})
|
|
@@ -391,6 +391,8 @@ export default function LeafletMapFrame({ payload, sessionId }) {
|
|
| 391 |
const canvasRenderer = L.canvas({ padding: 0.5, pane: 'mesa-market-pane' })
|
| 392 |
const baseLayers = {}
|
| 393 |
const overlayLayers = {}
|
|
|
|
|
|
|
| 394 |
|
| 395 |
;(payload.tile_layers || []).forEach((layerDef, index) => {
|
| 396 |
const tileLayer = L.tileLayer(String(layerDef?.url || ''), {
|
|
@@ -576,6 +578,46 @@ export default function LeafletMapFrame({ payload, sessionId }) {
|
|
| 576 |
})
|
| 577 |
}
|
| 578 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 579 |
function addShapeOverlays(layerGroup, shapes) {
|
| 580 |
if (!Array.isArray(shapes) || !shapes.length) return
|
| 581 |
shapes.forEach((shape) => {
|
|
@@ -680,9 +722,13 @@ export default function LeafletMapFrame({ payload, sessionId }) {
|
|
| 680 |
}
|
| 681 |
|
| 682 |
overlaySpecs.forEach((spec, index) => {
|
|
|
|
| 683 |
const label = String(spec?.label || spec?.id || `Camada ${index + 1}`)
|
| 684 |
const layerGroup = L.layerGroup()
|
| 685 |
overlayLayers[label] = layerGroup
|
|
|
|
|
|
|
|
|
|
| 686 |
if (spec?.show !== false) {
|
| 687 |
layerGroup.addTo(map)
|
| 688 |
}
|
|
@@ -690,11 +736,24 @@ export default function LeafletMapFrame({ payload, sessionId }) {
|
|
| 690 |
addGeoJsonOverlay(layerGroup, spec)
|
| 691 |
}
|
| 692 |
addHeatmapOverlay(layerGroup, spec?.heatmap)
|
| 693 |
-
addShapeOverlays(layerGroup, spec?.shapes)
|
| 694 |
addPointMarkers(layerGroup, spec?.points)
|
| 695 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 696 |
})
|
| 697 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 698 |
if (payload.controls?.layer_control) {
|
| 699 |
L.control.layers(baseLayers, overlayLayers, { collapsed: true }).addTo(map)
|
| 700 |
}
|
|
@@ -989,6 +1048,10 @@ export default function LeafletMapFrame({ payload, sessionId }) {
|
|
| 989 |
}
|
| 990 |
|
| 991 |
return () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 992 |
disposed = true
|
| 993 |
restoreMapInteractions?.()
|
| 994 |
map.remove()
|
|
|
|
| 343 |
|
| 344 |
if (Array.isArray(payload?.trabalhos_tecnicos_points) && payload.trabalhos_tecnicos_points.length) {
|
| 345 |
overlays.push({
|
| 346 |
+
id: 'trabalhos-tecnicos',
|
| 347 |
+
label: 'Trabalhos técnicos',
|
| 348 |
show: true,
|
| 349 |
markers: payload.trabalhos_tecnicos_points,
|
| 350 |
})
|
|
|
|
| 391 |
const canvasRenderer = L.canvas({ padding: 0.5, pane: 'mesa-market-pane' })
|
| 392 |
const baseLayers = {}
|
| 393 |
const overlayLayers = {}
|
| 394 |
+
const overlayGroupsById = new Map()
|
| 395 |
+
const dependencyAwareOverlays = []
|
| 396 |
|
| 397 |
;(payload.tile_layers || []).forEach((layerDef, index) => {
|
| 398 |
const tileLayer = L.tileLayer(String(layerDef?.url || ''), {
|
|
|
|
| 578 |
})
|
| 579 |
}
|
| 580 |
|
| 581 |
+
function filterDependencyAwareMarkers(items) {
|
| 582 |
+
if (!Array.isArray(items) || !items.length) return []
|
| 583 |
+
return items.filter((item) => {
|
| 584 |
+
const sourceIds = Array.isArray(item?.source_overlay_ids)
|
| 585 |
+
? item.source_overlay_ids.map((entry) => String(entry || '').trim()).filter(Boolean)
|
| 586 |
+
: []
|
| 587 |
+
if (!sourceIds.length) return true
|
| 588 |
+
return sourceIds.some((sourceId) => {
|
| 589 |
+
const sourceLayer = overlayGroupsById.get(sourceId)
|
| 590 |
+
return Boolean(sourceLayer && map.hasLayer(sourceLayer))
|
| 591 |
+
})
|
| 592 |
+
})
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
function areRequiredOverlaysVisible(spec) {
|
| 596 |
+
const requiredIds = Array.isArray(spec?.required_overlay_ids)
|
| 597 |
+
? spec.required_overlay_ids.map((entry) => String(entry || '').trim()).filter(Boolean)
|
| 598 |
+
: []
|
| 599 |
+
if (!requiredIds.length) return true
|
| 600 |
+
return requiredIds.every((requiredId) => {
|
| 601 |
+
const requiredLayer = overlayGroupsById.get(requiredId)
|
| 602 |
+
return Boolean(requiredLayer && map.hasLayer(requiredLayer))
|
| 603 |
+
})
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
function renderDependencyAwareOverlay(spec, layerGroup) {
|
| 607 |
+
if (!layerGroup) return
|
| 608 |
+
layerGroup.clearLayers()
|
| 609 |
+
if (!map.hasLayer(layerGroup)) return
|
| 610 |
+
if (!areRequiredOverlaysVisible(spec)) return
|
| 611 |
+
addShapeOverlays(layerGroup, spec?.shapes)
|
| 612 |
+
addMarkerOverlays(layerGroup, filterDependencyAwareMarkers(spec?.markers))
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
function refreshDependencyAwareOverlays() {
|
| 616 |
+
dependencyAwareOverlays.forEach(({ spec, layerGroup }) => {
|
| 617 |
+
renderDependencyAwareOverlay(spec, layerGroup)
|
| 618 |
+
})
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
function addShapeOverlays(layerGroup, shapes) {
|
| 622 |
if (!Array.isArray(shapes) || !shapes.length) return
|
| 623 |
shapes.forEach((shape) => {
|
|
|
|
| 722 |
}
|
| 723 |
|
| 724 |
overlaySpecs.forEach((spec, index) => {
|
| 725 |
+
const specId = String(spec?.id || '').trim()
|
| 726 |
const label = String(spec?.label || spec?.id || `Camada ${index + 1}`)
|
| 727 |
const layerGroup = L.layerGroup()
|
| 728 |
overlayLayers[label] = layerGroup
|
| 729 |
+
if (specId) {
|
| 730 |
+
overlayGroupsById.set(specId, layerGroup)
|
| 731 |
+
}
|
| 732 |
if (spec?.show !== false) {
|
| 733 |
layerGroup.addTo(map)
|
| 734 |
}
|
|
|
|
| 736 |
addGeoJsonOverlay(layerGroup, spec)
|
| 737 |
}
|
| 738 |
addHeatmapOverlay(layerGroup, spec?.heatmap)
|
|
|
|
| 739 |
addPointMarkers(layerGroup, spec?.points)
|
| 740 |
+
const hasMarkerDependencies = Array.isArray(spec?.markers)
|
| 741 |
+
&& spec.markers.some((item) => Array.isArray(item?.source_overlay_ids) && item.source_overlay_ids.length)
|
| 742 |
+
const hasOverlayDependencies = Array.isArray(spec?.required_overlay_ids) && spec.required_overlay_ids.length > 0
|
| 743 |
+
if (hasMarkerDependencies || hasOverlayDependencies) {
|
| 744 |
+
dependencyAwareOverlays.push({ spec, layerGroup })
|
| 745 |
+
renderDependencyAwareOverlay(spec, layerGroup)
|
| 746 |
+
} else {
|
| 747 |
+
addShapeOverlays(layerGroup, spec?.shapes)
|
| 748 |
+
addMarkerOverlays(layerGroup, spec?.markers)
|
| 749 |
+
}
|
| 750 |
})
|
| 751 |
|
| 752 |
+
if (dependencyAwareOverlays.length) {
|
| 753 |
+
map.on('overlayadd', refreshDependencyAwareOverlays)
|
| 754 |
+
map.on('overlayremove', refreshDependencyAwareOverlays)
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
if (payload.controls?.layer_control) {
|
| 758 |
L.control.layers(baseLayers, overlayLayers, { collapsed: true }).addTo(map)
|
| 759 |
}
|
|
|
|
| 1048 |
}
|
| 1049 |
|
| 1050 |
return () => {
|
| 1051 |
+
if (dependencyAwareOverlays.length) {
|
| 1052 |
+
map.off('overlayadd', refreshDependencyAwareOverlays)
|
| 1053 |
+
map.off('overlayremove', refreshDependencyAwareOverlays)
|
| 1054 |
+
}
|
| 1055 |
disposed = true
|
| 1056 |
restoreMapInteractions?.()
|
| 1057 |
map.remove()
|
frontend/src/components/PesquisaTab.jsx
CHANGED
|
@@ -82,6 +82,15 @@ const TIPO_SIGLAS = {
|
|
| 82 |
DEPOS: 'Deposito',
|
| 83 |
CCOM: 'Casa comercial',
|
| 84 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
|
| 86 |
function formatRange(range) {
|
| 87 |
if (!range) return '-'
|
|
@@ -1024,9 +1033,12 @@ export default function PesquisaTab({
|
|
| 1024 |
|
| 1025 |
const sugestoes = result.sugestoes || {}
|
| 1026 |
const opcoesTipoModelo = useMemo(
|
| 1027 |
-
() =>
|
| 1028 |
-
|
| 1029 |
-
|
|
|
|
|
|
|
|
|
|
| 1030 |
)
|
| 1031 |
const modoModeloAberto = Boolean(modeloAbertoMeta)
|
| 1032 |
const avaliandosGeoPayload = useMemo(
|
|
|
|
| 82 |
DEPOS: 'Deposito',
|
| 83 |
CCOM: 'Casa comercial',
|
| 84 |
}
|
| 85 |
+
const TIPOS_MODELO_GENERICOS = [
|
| 86 |
+
'Apartamentos',
|
| 87 |
+
'Casa comercial',
|
| 88 |
+
'Deposito',
|
| 89 |
+
'Edificio',
|
| 90 |
+
'Loja',
|
| 91 |
+
'Salas comerciais',
|
| 92 |
+
'Terrenos',
|
| 93 |
+
]
|
| 94 |
|
| 95 |
function formatRange(range) {
|
| 96 |
if (!range) return '-'
|
|
|
|
| 1033 |
|
| 1034 |
const sugestoes = result.sugestoes || {}
|
| 1035 |
const opcoesTipoModelo = useMemo(
|
| 1036 |
+
() => {
|
| 1037 |
+
const atual = String(filters.tipoModelo || '').trim()
|
| 1038 |
+
if (!atual || TIPOS_MODELO_GENERICOS.includes(atual)) return TIPOS_MODELO_GENERICOS
|
| 1039 |
+
return [...TIPOS_MODELO_GENERICOS, atual]
|
| 1040 |
+
},
|
| 1041 |
+
[filters.tipoModelo],
|
| 1042 |
)
|
| 1043 |
const modoModeloAberto = Boolean(modeloAbertoMeta)
|
| 1044 |
const avaliandosGeoPayload = useMemo(
|