Guilherme Silberfarb Costa commited on
Commit
fce4fe8
·
1 Parent(s): c485ae0

correcao de erro apos adicao de avaliandos

Browse files
backend/app/api/pesquisa.py CHANGED
@@ -1,6 +1,7 @@
1
  from __future__ import annotations
2
 
3
  import json
 
4
 
5
  from fastapi import APIRouter, HTTPException, Query
6
  from pydantic import BaseModel
@@ -37,27 +38,27 @@ def _parse_avaliandos_geo_json(raw: str | None) -> list[dict]:
37
 
38
 
39
  class AvaliandoGeoPayload(BaseModel):
40
- id: str | None = None
41
- label: str | None = None
42
- lat: float | None = None
43
- lon: float | None = None
44
- logradouro: str | None = None
45
- numero_usado: str | None = None
46
- cdlog: int | None = None
47
- origem: str | None = None
48
 
49
 
50
  class MapaModelosPayload(BaseModel):
51
  modelos_ids: list[str]
52
- avaliando_lat: float | None = None
53
- avaliando_lon: float | None = None
54
  avaliandos: list[AvaliandoGeoPayload] | None = None
55
- modo_exibicao: str | None = "pontos"
56
- criterio_espacial: str | None = None
57
- trabalhos_tecnicos_modelos_modo: str | None = None
58
- trabalhos_tecnicos_proximidade_modo: str | None = None
59
- trabalhos_tecnicos_modo: str | None = None
60
- trabalhos_tecnicos_raio_m: int | None = 1000
61
 
62
 
63
  class LocalizacaoAvaliandoPayload(BaseModel):
 
1
  from __future__ import annotations
2
 
3
  import json
4
+ from typing import Any
5
 
6
  from fastapi import APIRouter, HTTPException, Query
7
  from pydantic import BaseModel
 
38
 
39
 
40
  class AvaliandoGeoPayload(BaseModel):
41
+ id: Any = None
42
+ label: Any = None
43
+ lat: Any = None
44
+ lon: Any = None
45
+ logradouro: Any = None
46
+ numero_usado: Any = None
47
+ cdlog: Any = None
48
+ origem: Any = None
49
 
50
 
51
  class MapaModelosPayload(BaseModel):
52
  modelos_ids: list[str]
53
+ avaliando_lat: Any = None
54
+ avaliando_lon: Any = None
55
  avaliandos: list[AvaliandoGeoPayload] | None = None
56
+ modo_exibicao: Any = "pontos"
57
+ criterio_espacial: Any = None
58
+ trabalhos_tecnicos_modelos_modo: Any = None
59
+ trabalhos_tecnicos_proximidade_modo: Any = None
60
+ trabalhos_tecnicos_modo: Any = None
61
+ trabalhos_tecnicos_raio_m: Any = 1000
62
 
63
 
64
  class LocalizacaoAvaliandoPayload(BaseModel):
backend/app/core/map_layers.py CHANGED
@@ -174,6 +174,17 @@ def add_trabalhos_tecnicos_markers(
174
  endereco_texto = ", ".join(endereco_parts) or label or "Endereco nao informado"
175
  modelos_texto = ", ".join(modelos) or "Modelo nao informado"
176
  modelos_label = "Modelo" if len(modelos) == 1 else "Modelos"
 
 
 
 
 
 
 
 
 
 
 
177
 
178
  if trabalho_id:
179
  payload_json = json.dumps(
@@ -199,6 +210,16 @@ def add_trabalhos_tecnicos_markers(
199
  f"<div style='margin-bottom:6px;'>{trabalho_nome_html}</div>"
200
  + (f"<div><span style='color:#666;'>Tipo:</span> {escape(tipo_label)}</div>" if tipo_label else "")
201
  + f"<div><span style='color:#666;'>Endereco:</span> {escape(endereco_texto)}</div>"
 
 
 
 
 
 
 
 
 
 
202
  + f"<div><span style='color:#666;'>{escape(modelos_label)}:</span> {escape(modelos_texto)}</div>"
203
  + "</div>"
204
  )
 
174
  endereco_texto = ", ".join(endereco_parts) or label or "Endereco nao informado"
175
  modelos_texto = ", ".join(modelos) or "Modelo nao informado"
176
  modelos_label = "Modelo" if len(modelos) == 1 else "Modelos"
177
+ avaliandos_proximos = item.get("avaliandos_proximos") if isinstance(item.get("avaliandos_proximos"), list) else []
178
+ avaliandos_proximos_texto = ", ".join(
179
+ (
180
+ f"{str(entry.get('label') or '').strip()} ({str(entry.get('distancia_label') or '').strip()})"
181
+ if str(entry.get("distancia_label") or "").strip()
182
+ else str(entry.get("label") or "").strip()
183
+ )
184
+ for entry in avaliandos_proximos
185
+ if str(entry.get("label") or "").strip()
186
+ )
187
+ distancia_min_label = str(item.get("distancia_label_min") or "").strip()
188
 
189
  if trabalho_id:
190
  payload_json = json.dumps(
 
210
  f"<div style='margin-bottom:6px;'>{trabalho_nome_html}</div>"
211
  + (f"<div><span style='color:#666;'>Tipo:</span> {escape(tipo_label)}</div>" if tipo_label else "")
212
  + f"<div><span style='color:#666;'>Endereco:</span> {escape(endereco_texto)}</div>"
213
+ + (
214
+ f"<div><span style='color:#666;'>Menor distancia:</span> {escape(distancia_min_label)}</div>"
215
+ if distancia_min_label
216
+ else ""
217
+ )
218
+ + (
219
+ f"<div><span style='color:#666;'>Proximo de:</span> {escape(avaliandos_proximos_texto)}</div>"
220
+ if avaliandos_proximos_texto
221
+ else ""
222
+ )
223
  + f"<div><span style='color:#666;'>{escape(modelos_label)}:</span> {escape(modelos_texto)}</div>"
224
  + "</div>"
225
  )
backend/app/services/pesquisa_service.py CHANGED
@@ -707,6 +707,76 @@ def _chave_unica_trabalho_tecnico(item: dict[str, Any]) -> tuple[str, str, str,
707
  )
708
 
709
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
710
  def gerar_mapa_modelos(
711
  modelos_ids: list[str],
712
  limite_pontos_por_modelo: int = 0,
@@ -833,16 +903,13 @@ def gerar_mapa_modelos(
833
 
834
  avaliandos_tecnicos_proximos: list[dict[str, Any]] | None = None
835
  if (
836
- avaliando_unico is not None
837
- and aval_lat is not None
838
- and aval_lon is not None
839
  and trabalhos_tecnicos_proximidade_modo_norm == TRABALHOS_TECNICOS_PROXIMIDADE_ATIVADA
840
  ):
841
- avaliandos_tecnicos_proximos = trabalhos_tecnicos_service.listar_avaliandos_proximos(
842
- aval_lat,
843
- aval_lon,
844
  trabalhos_tecnicos_raio_m_norm,
845
- chaves_modelo_excluir=chaves_modelos_selecionados,
846
  )
847
  for item in avaliandos_tecnicos_proximos or []:
848
  prox_lat, prox_lon = _normalizar_coordenadas_avaliando(item.get("coord_lat"), item.get("coord_lon"))
@@ -859,24 +926,14 @@ def gerar_mapa_modelos(
859
  total_pontos = 0
860
  for modelo in modelos_plotados:
861
  total_pontos += int(modelo["total_pontos"])
862
- mapas_html = {
863
- "pontos": _renderizar_mapa_modelos(
864
- modelos_plotados,
865
- bounds,
866
- avaliandos_geo,
867
- "pontos",
868
- avaliandos_tecnicos_proximos=avaliandos_tecnicos_proximos,
869
- trabalhos_tecnicos_raio_m=trabalhos_tecnicos_raio_m_norm,
870
- ),
871
- "cobertura": _renderizar_mapa_modelos(
872
- modelos_plotados,
873
- bounds,
874
- avaliandos_geo,
875
- "cobertura",
876
- avaliandos_tecnicos_proximos=avaliandos_tecnicos_proximos,
877
- trabalhos_tecnicos_raio_m=trabalhos_tecnicos_raio_m_norm,
878
- ),
879
- }
880
 
881
  total_trabalhos_modelos = len(
882
  {
@@ -890,10 +947,9 @@ def gerar_mapa_modelos(
890
  detalhe_trabalhos = (
891
  f" Trabalhos técnicos {descricao_modelos_status}: {total_trabalhos_modelos}."
892
  + (
893
- f" Próximos ao avaliando (até {trabalhos_tecnicos_raio_m_norm} m): {total_trabalhos_proximos}."
894
  if (
895
- aval_lat is not None
896
- and aval_lon is not None
897
  and trabalhos_tecnicos_proximidade_modo_norm == TRABALHOS_TECNICOS_PROXIMIDADE_ATIVADA
898
  )
899
  else ""
@@ -902,9 +958,9 @@ def gerar_mapa_modelos(
902
 
903
  return sanitize_value(
904
  {
905
- "mapa_html": mapas_html[modo_exibicao_norm],
906
- "mapa_html_pontos": mapas_html["pontos"],
907
- "mapa_html_cobertura": mapas_html["cobertura"],
908
  "total_modelos_plotados": len(modelos_plotados),
909
  "total_pontos": total_pontos,
910
  "modelos_plotados": [
@@ -1068,19 +1124,29 @@ def _renderizar_mapa_modelos(
1068
  camada_modelo.add_to(mapa)
1069
 
1070
  if camada_trabalhos_proximos is not None:
1071
- if aval_lat is not None and aval_lon is not None and int(trabalhos_tecnicos_raio_m or 0) > 0:
1072
- folium.Circle(
1073
- location=[aval_lat, aval_lon],
1074
- radius=float(trabalhos_tecnicos_raio_m),
1075
- color="#c62828",
1076
- weight=2,
1077
- opacity=0.75,
1078
- fill=True,
1079
- fill_color="#c62828",
1080
- fill_opacity=0.06,
1081
- dash_array="6,6",
1082
- tooltip=f"Raio de busca: ate {int(trabalhos_tecnicos_raio_m)} m",
1083
- ).add_to(camada_trabalhos_proximos)
 
 
 
 
 
 
 
 
 
 
1084
  add_trabalhos_tecnicos_markers(camada_trabalhos_proximos, avaliandos_tecnicos_proximos or [])
1085
  camada_trabalhos_proximos.add_to(mapa)
1086
 
 
707
  )
708
 
709
 
710
+ def _listar_avaliandos_tecnicos_proximos_por_avaliandos(
711
+ avaliandos_geo: list[dict[str, Any]] | None,
712
+ raio_m: int,
713
+ chaves_modelos_selecionados: list[str] | None = None,
714
+ ) -> list[dict[str, Any]]:
715
+ agregados: dict[tuple[str, str, str, str, str, str], dict[str, Any]] = {}
716
+
717
+ for idx, avaliando in enumerate(avaliandos_geo or []):
718
+ lat, lon = _normalizar_coordenadas_avaliando(avaliando.get("lat"), avaliando.get("lon"))
719
+ if lat is None or lon is None:
720
+ continue
721
+
722
+ label = str(avaliando.get("label") or f"A{idx + 1}").strip() or f"A{idx + 1}"
723
+ avaliando_id = str(avaliando.get("id") or label or f"avaliando-{idx + 1}").strip() or f"avaliando-{idx + 1}"
724
+ proximos = trabalhos_tecnicos_service.listar_avaliandos_proximos(
725
+ lat,
726
+ lon,
727
+ raio_m,
728
+ chaves_modelo_excluir=chaves_modelos_selecionados,
729
+ )
730
+
731
+ for item in proximos or []:
732
+ chave = _chave_unica_trabalho_tecnico(item)
733
+ distancia_m = _to_float_or_none(item.get("distancia_m"))
734
+ distancia_label = str(item.get("distancia_label") or "").strip() or None
735
+ proximidade_avaliando = {
736
+ "id": avaliando_id,
737
+ "label": label,
738
+ "distancia_m": distancia_m,
739
+ "distancia_label": distancia_label,
740
+ }
741
+ atual = agregados.get(chave)
742
+ if atual is None:
743
+ novo_item = dict(item)
744
+ novo_item["avaliandos_proximos"] = [proximidade_avaliando]
745
+ novo_item["distancia_m_min"] = distancia_m
746
+ novo_item["distancia_label_min"] = distancia_label
747
+ agregados[chave] = novo_item
748
+ continue
749
+
750
+ proximidades_existentes = atual.setdefault("avaliandos_proximos", [])
751
+ if not any(str(entry.get("id") or "").strip() == avaliando_id for entry in proximidades_existentes):
752
+ proximidades_existentes.append(proximidade_avaliando)
753
+
754
+ distancia_atual = _to_float_or_none(atual.get("distancia_m_min"))
755
+ if distancia_m is not None and (distancia_atual is None or distancia_m < distancia_atual):
756
+ atual["distancia_m_min"] = distancia_m
757
+ atual["distancia_label_min"] = distancia_label
758
+
759
+ consolidado = list(agregados.values())
760
+ for item in consolidado:
761
+ proximidades = item.get("avaliandos_proximos") if isinstance(item.get("avaliandos_proximos"), list) else []
762
+ proximidades.sort(
763
+ key=lambda entry: (
764
+ str(entry.get("label") or "").casefold(),
765
+ float(entry.get("distancia_m") or 0.0),
766
+ )
767
+ )
768
+ item["avaliandos_proximos"] = proximidades
769
+
770
+ consolidado.sort(
771
+ key=lambda item: (
772
+ float(item.get("distancia_m_min") or 0.0),
773
+ str(item.get("trabalho_nome") or "").casefold(),
774
+ str(item.get("label") or item.get("endereco") or "").casefold(),
775
+ )
776
+ )
777
+ return sanitize_value(consolidado)
778
+
779
+
780
  def gerar_mapa_modelos(
781
  modelos_ids: list[str],
782
  limite_pontos_por_modelo: int = 0,
 
903
 
904
  avaliandos_tecnicos_proximos: list[dict[str, Any]] | None = None
905
  if (
906
+ avaliandos_geo
 
 
907
  and trabalhos_tecnicos_proximidade_modo_norm == TRABALHOS_TECNICOS_PROXIMIDADE_ATIVADA
908
  ):
909
+ avaliandos_tecnicos_proximos = _listar_avaliandos_tecnicos_proximos_por_avaliandos(
910
+ avaliandos_geo,
 
911
  trabalhos_tecnicos_raio_m_norm,
912
+ chaves_modelos_selecionados,
913
  )
914
  for item in avaliandos_tecnicos_proximos or []:
915
  prox_lat, prox_lon = _normalizar_coordenadas_avaliando(item.get("coord_lat"), item.get("coord_lon"))
 
926
  total_pontos = 0
927
  for modelo in modelos_plotados:
928
  total_pontos += int(modelo["total_pontos"])
929
+ mapa_html = _renderizar_mapa_modelos(
930
+ modelos_plotados,
931
+ bounds,
932
+ avaliandos_geo,
933
+ modo_exibicao_norm,
934
+ avaliandos_tecnicos_proximos=avaliandos_tecnicos_proximos,
935
+ trabalhos_tecnicos_raio_m=trabalhos_tecnicos_raio_m_norm,
936
+ )
 
 
 
 
 
 
 
 
 
 
937
 
938
  total_trabalhos_modelos = len(
939
  {
 
947
  detalhe_trabalhos = (
948
  f" Trabalhos técnicos {descricao_modelos_status}: {total_trabalhos_modelos}."
949
  + (
950
+ f" {'Próximos aos avaliandos' if len(avaliandos_geo) > 1 else 'Próximos ao avaliando'} (até {trabalhos_tecnicos_raio_m_norm} m): {total_trabalhos_proximos}."
951
  if (
952
+ avaliandos_geo
 
953
  and trabalhos_tecnicos_proximidade_modo_norm == TRABALHOS_TECNICOS_PROXIMIDADE_ATIVADA
954
  )
955
  else ""
 
958
 
959
  return sanitize_value(
960
  {
961
+ "mapa_html": mapa_html,
962
+ "mapa_html_pontos": mapa_html if modo_exibicao_norm == "pontos" else "",
963
+ "mapa_html_cobertura": mapa_html if modo_exibicao_norm == "cobertura" else "",
964
  "total_modelos_plotados": len(modelos_plotados),
965
  "total_pontos": total_pontos,
966
  "modelos_plotados": [
 
1124
  camada_modelo.add_to(mapa)
1125
 
1126
  if camada_trabalhos_proximos is not None:
1127
+ if int(trabalhos_tecnicos_raio_m or 0) > 0:
1128
+ for idx, avaliando in enumerate(avaliandos_geo):
1129
+ lat_item, lon_item = _normalizar_coordenadas_avaliando(avaliando.get("lat"), avaliando.get("lon"))
1130
+ if lat_item is None or lon_item is None:
1131
+ continue
1132
+ label = str(avaliando.get("label") or f"A{idx + 1}").strip() or f"A{idx + 1}"
1133
+ tooltip_raio = (
1134
+ f"Raio de busca: ate {int(trabalhos_tecnicos_raio_m)} m"
1135
+ if len(avaliandos_geo) == 1
1136
+ else f"Raio de busca • {label}: ate {int(trabalhos_tecnicos_raio_m)} m"
1137
+ )
1138
+ folium.Circle(
1139
+ location=[lat_item, lon_item],
1140
+ radius=float(trabalhos_tecnicos_raio_m),
1141
+ color="#c62828",
1142
+ weight=2,
1143
+ opacity=0.75,
1144
+ fill=True,
1145
+ fill_color="#c62828",
1146
+ fill_opacity=0.06,
1147
+ dash_array="6,6",
1148
+ tooltip=tooltip_raio,
1149
+ ).add_to(camada_trabalhos_proximos)
1150
  add_trabalhos_tecnicos_markers(camada_trabalhos_proximos, avaliandos_tecnicos_proximos or [])
1151
  camada_trabalhos_proximos.add_to(mapa)
1152
 
frontend/src/api.js CHANGED
@@ -95,7 +95,19 @@ async function handleResponse(response, path = '') {
95
  }
96
  const detailMessage = typeof detail === 'string'
97
  ? detail
98
- : String(detail?.message || response.statusText || 'Erro inesperado')
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
  if (response.status === 401 && path !== '/api/auth/login') {
101
  setAuthToken('')
 
95
  }
96
  const detailMessage = typeof detail === 'string'
97
  ? detail
98
+ : Array.isArray(detail)
99
+ ? detail
100
+ .map((item) => {
101
+ const loc = Array.isArray(item?.loc)
102
+ ? item.loc.filter((part) => part !== 'body').join(' > ')
103
+ : ''
104
+ const msg = String(item?.msg || '').trim()
105
+ if (loc && msg) return `${loc}: ${msg}`
106
+ return msg || loc || ''
107
+ })
108
+ .filter(Boolean)
109
+ .join(' | ')
110
+ : String(detail?.message || detail?.detail || response.statusText || 'Erro inesperado')
111
 
112
  if (response.status === 401 && path !== '/api/auth/login') {
113
  setAuthToken('')
frontend/src/components/PesquisaTab.jsx CHANGED
@@ -287,51 +287,36 @@ function buildVariablesContent(text) {
287
 
288
  function LocalizacaoResumoCard({
289
  title,
290
- subtitle = '',
291
  localizacao = null,
292
  actions = null,
293
  className = '',
294
  }) {
295
  if (!localizacao) return null
 
 
 
 
 
 
 
 
 
 
296
  const classes = ['pesquisa-localizacao-registered', className].filter(Boolean).join(' ')
297
  return (
298
  <div className={classes}>
299
  <div className="pesquisa-localizacao-registered-head">
300
  <div className="pesquisa-localizacao-registered-copy">
301
  <strong>{title}</strong>
302
- {subtitle ? <span>{subtitle}</span> : null}
303
- </div>
304
- {actions}
305
- </div>
306
-
307
- <div className="pesquisa-localizacao-summary">
308
- {localizacao?.logradouro ? (
309
- <div className="pesquisa-localizacao-summary-row">
310
- <span className="pesquisa-localizacao-summary-label">Endereço</span>
311
- <span className="pesquisa-localizacao-summary-value">
312
- {localizacao.logradouro}
313
- {localizacao?.numero_usado ? `, ${localizacao.numero_usado}` : ''}
314
- </span>
315
- </div>
316
- ) : null}
317
- {localizacao?.cdlog ? (
318
- <div className="pesquisa-localizacao-summary-row">
319
- <span className="pesquisa-localizacao-summary-label">CDLOG</span>
320
- <span className="pesquisa-localizacao-summary-value">{localizacao.cdlog}</span>
321
  </div>
322
- ) : null}
323
- <div className="pesquisa-localizacao-summary-row">
324
- <span className="pesquisa-localizacao-summary-label">Latitude</span>
325
- <span className="pesquisa-localizacao-summary-value">{Number(localizacao.lat).toFixed(6)}</span>
326
- </div>
327
- <div className="pesquisa-localizacao-summary-row">
328
- <span className="pesquisa-localizacao-summary-label">Longitude</span>
329
- <span className="pesquisa-localizacao-summary-value">{Number(localizacao.lon).toFixed(6)}</span>
330
- </div>
331
- <div className="pesquisa-localizacao-summary-row">
332
- <span className="pesquisa-localizacao-summary-label">Origem</span>
333
- <span className="pesquisa-localizacao-summary-value">{formatLocalizacaoOrigemLabel(localizacao)}</span>
334
  </div>
 
335
  </div>
336
  </div>
337
  )
@@ -972,7 +957,6 @@ export default function PesquisaTab({
972
  const [localizacaoModo, setLocalizacaoModo] = useState('endereco')
973
  const [localizacaoInputs, setLocalizacaoInputs] = useState(EMPTY_LOCATION_INPUTS)
974
  const [avaliandosGeolocalizados, setAvaliandosGeolocalizados] = useState([])
975
- const [localizacaoEditorAberto, setLocalizacaoEditorAberto] = useState(false)
976
  const [localizacaoLoading, setLocalizacaoLoading] = useState(false)
977
  const [localizacaoError, setLocalizacaoError] = useState('')
978
  const [localizacaoStatus, setLocalizacaoStatus] = useState('')
@@ -1032,7 +1016,6 @@ export default function PesquisaTab({
1032
  const localizacaoAtiva = avaliandosGeoPayload.length > 0
1033
  const localizacaoMultipla = avaliandosGeoPayload.length > 1
1034
  const avaliandoUnicoAtivo = avaliandosGeoPayload.length === 1
1035
- const avaliandoPrincipal = avaliandosGeolocalizados.length === 1 ? avaliandosGeolocalizados[0] : null
1036
  const modelosOrdenados = useMemo(
1037
  () => (localizacaoMultipla ? sortModelosBySpatialCriterion(result.modelos || [], criterioEspacial) : (result.modelos || [])),
1038
  [criterioEspacial, localizacaoMultipla, result.modelos],
@@ -1040,10 +1023,6 @@ export default function PesquisaTab({
1040
  const resultIds = useMemo(() => modelosOrdenados.map((modelo) => modelo.id), [modelosOrdenados])
1041
  const todosSelecionados = resultIds.length > 0 && resultIds.every((id) => selectedIds.includes(id))
1042
  const algunsSelecionados = resultIds.some((id) => selectedIds.includes(id))
1043
- const localizacaoRegistradaMensagem = localizacaoMultipla
1044
- ? `${formatCount(avaliandosGeoPayload.length)} avaliandos foram geolocalizados e serão usados na distância espacial dos modelos.`
1045
- : (String(localizacaoStatus || '').trim()
1046
- || 'A geolocalização do avaliando foi registrada e será usada na distância espacial dos modelos.')
1047
  const mapaHtmlAtual = mapaHtmls[mapaModoExibicao] || ''
1048
  const mapaFoiGerado = Boolean(mapaHtmls.pontos || mapaHtmls.cobertura)
1049
 
@@ -1086,7 +1065,7 @@ export default function PesquisaTab({
1086
  ? 'selecionados_e_outras_versoes'
1087
  : String(modelosModoBruto || 'selecionados')
1088
  const proximidadeModoBruto = overrides.trabalhosTecnicosProximidadeModo ?? mapaTrabalhosTecnicosProximidadeModo
1089
- const proximidadeModo = avaliandoUnicoAtivo ? String(proximidadeModoBruto || 'sem_proximidade') : 'sem_proximidade'
1090
  const raioNumero = Number(overrides.trabalhosTecnicosRaio ?? mapaTrabalhosTecnicosRaio)
1091
  const raio = Number.isFinite(raioNumero)
1092
  ? Math.max(0, Math.min(5000, Math.round(raioNumero)))
@@ -1122,16 +1101,21 @@ export default function PesquisaTab({
1122
  try {
1123
  const response = await api.pesquisarMapaModelos(
1124
  idsValidos,
1125
- localizacaoMultipla ? avaliandosGeoPayload : avaliandoPrincipal,
1126
  modoExibicaoSolicitado,
1127
  criterioEspacial,
1128
  trabalhosTecnicosConfig.modelosModo,
1129
  trabalhosTecnicosConfig.proximidadeModo,
1130
  trabalhosTecnicosConfig.raio,
1131
  )
 
 
 
 
 
1132
  setMapaHtmls({
1133
- pontos: response.mapa_html_pontos || '',
1134
- cobertura: response.mapa_html_cobertura || '',
1135
  })
1136
  setMapaStatus(response.status || '')
1137
  mapaTrabalhosTecnicosConfigRef.current = buildMapaTrabalhosTecnicosConfigKey(trabalhosTecnicosConfig)
@@ -1403,6 +1387,13 @@ export default function PesquisaTab({
1403
  await carregarMapaPesquisa(selectedIds)
1404
  }
1405
 
 
 
 
 
 
 
 
1406
  async function onAdminConfigSalva() {
1407
  if (pesquisaInicializada) {
1408
  await buscarModelos(filters, avaliandosGeolocalizados)
@@ -1411,21 +1402,6 @@ export default function PesquisaTab({
1411
  await carregarContextoInicial()
1412
  }
1413
 
1414
- function onAdicionarOutroAvaliando() {
1415
- setLocalizacaoInputs(EMPTY_LOCATION_INPUTS)
1416
- setLocalizacaoModo('endereco')
1417
- setLocalizacaoError('')
1418
- setLocalizacaoStatus('')
1419
- setLocalizacaoEditorAberto(true)
1420
- }
1421
-
1422
- function onCancelarInclusaoAvaliando() {
1423
- setLocalizacaoInputs(EMPTY_LOCATION_INPUTS)
1424
- setLocalizacaoModo('endereco')
1425
- setLocalizacaoError('')
1426
- setLocalizacaoEditorAberto(false)
1427
- }
1428
-
1429
  function onLimparFormularioLocalizacao() {
1430
  setLocalizacaoInputs(EMPTY_LOCATION_INPUTS)
1431
  setLocalizacaoModo('endereco')
@@ -1486,7 +1462,6 @@ export default function PesquisaTab({
1486
  const nextEntries = [...avaliandosGeolocalizados, nextEntry]
1487
  setAvaliandosGeolocalizados(nextEntries)
1488
  setLocalizacaoInputs(EMPTY_LOCATION_INPUTS)
1489
- setLocalizacaoEditorAberto(false)
1490
  setLocalizacaoStatus(
1491
  nextEntries.length > 1
1492
  ? `${nextEntries.length} avaliandos geolocalizados com sucesso.`
@@ -1499,24 +1474,11 @@ export default function PesquisaTab({
1499
  setLocalizacaoLoading(false)
1500
  }
1501
  }
1502
-
1503
- async function onLimparLocalizacao() {
1504
- setLocalizacaoInputs(EMPTY_LOCATION_INPUTS)
1505
- setAvaliandosGeolocalizados([])
1506
- setLocalizacaoEditorAberto(false)
1507
- setLocalizacaoError('')
1508
- setLocalizacaoStatus('')
1509
- await atualizarPesquisaAposGeolocalizacao([])
1510
- }
1511
-
1512
  async function onRemoverAvaliandoLocalizacao(id) {
1513
  const nextEntries = avaliandosGeolocalizados.filter((item) => item.id !== id)
1514
  setAvaliandosGeolocalizados(nextEntries)
1515
  setLocalizacaoError('')
1516
  setLocalizacaoStatus('')
1517
- if (!nextEntries.length) {
1518
- setLocalizacaoEditorAberto(false)
1519
- }
1520
  await atualizarPesquisaAposGeolocalizacao(nextEntries)
1521
  }
1522
 
@@ -1648,85 +1610,30 @@ export default function PesquisaTab({
1648
  subtitle="Registre um endereço ou coordenadas do avaliando. O preenchimento desta seção não é obrigatório. Se você não informar localização, a pesquisa continua normal, a distância espacial dos modelos não será calculada e o avaliando não aparecerá no mapa gerado."
1649
  >
1650
  <div className="pesquisa-localizacao-section">
1651
- {avaliandoPrincipal ? (
1652
- <LocalizacaoResumoCard
1653
- title="Geolocalização registrada"
1654
- subtitle={localizacaoRegistradaMensagem}
1655
- localizacao={avaliandoPrincipal}
1656
- actions={(
1657
- <div className="pesquisa-localizacao-registered-actions">
1658
- <button
1659
- type="button"
1660
- className="pesquisa-localizacao-action pesquisa-localizacao-action-ok"
1661
- onClick={localizacaoEditorAberto ? onCancelarInclusaoAvaliando : onAdicionarOutroAvaliando}
1662
- disabled={localizacaoLoading || loading}
1663
- >
1664
- {localizacaoEditorAberto ? 'Cancelar inclusão' : 'Adicionar outro avaliando'}
1665
- </button>
1666
- <button
1667
- type="button"
1668
- className="pesquisa-localizacao-action pesquisa-localizacao-action-reset pesquisa-localizacao-restart-btn"
1669
- onClick={() => void onLimparLocalizacao()}
1670
- disabled={localizacaoLoading || loading}
1671
- >
1672
- Reiniciar geolocalização
1673
- </button>
1674
- </div>
1675
- )}
1676
- />
1677
- ) : null}
1678
-
1679
- {localizacaoMultipla ? (
1680
- <div className="pesquisa-localizacao-multiple-block">
1681
- <div className="pesquisa-localizacao-registered-head pesquisa-localizacao-multi-head">
1682
- <div className="pesquisa-localizacao-registered-copy">
1683
- <strong>Geolocalizações registradas</strong>
1684
- <span>{localizacaoRegistradaMensagem}</span>
1685
- </div>
1686
- <div className="pesquisa-localizacao-registered-actions">
1687
- <button
1688
- type="button"
1689
- className="pesquisa-localizacao-action pesquisa-localizacao-action-ok"
1690
- onClick={localizacaoEditorAberto ? onCancelarInclusaoAvaliando : onAdicionarOutroAvaliando}
1691
- disabled={localizacaoLoading || loading}
1692
- >
1693
- {localizacaoEditorAberto ? 'Cancelar inclusão' : 'Adicionar outro avaliando'}
1694
- </button>
1695
- <button
1696
- type="button"
1697
- className="pesquisa-localizacao-action pesquisa-localizacao-action-reset"
1698
- onClick={() => void onLimparLocalizacao()}
1699
- disabled={localizacaoLoading || loading}
1700
- >
1701
- Reiniciar geolocalizações
1702
- </button>
1703
- </div>
1704
- </div>
1705
-
1706
- <div className="pesquisa-localizacao-multi-grid">
1707
- {avaliandosGeolocalizados.map((item, index) => (
1708
- <LocalizacaoResumoCard
1709
- key={item.id}
1710
- title={`Avaliando ${formatAvaliandoGeoLabel(index)}`}
1711
- localizacao={item}
1712
- className="pesquisa-localizacao-multi-card"
1713
- actions={(
1714
- <button
1715
- type="button"
1716
- className="pesquisa-localizacao-action pesquisa-localizacao-action-reset"
1717
- onClick={() => void onRemoverAvaliandoLocalizacao(item.id)}
1718
- disabled={localizacaoLoading || loading}
1719
- >
1720
- Remover
1721
- </button>
1722
- )}
1723
- />
1724
- ))}
1725
- </div>
1726
  </div>
1727
  ) : null}
1728
 
1729
- {!localizacaoAtiva || localizacaoEditorAberto ? (localizacaoModo === 'coords' ? (
1730
  <div className="pesquisa-localizacao-grid pesquisa-localizacao-grid-coords">
1731
  <label className="pesquisa-field">
1732
  Forma de localização
@@ -1755,7 +1662,7 @@ export default function PesquisaTab({
1755
  onClick={() => void onResolverLocalizacao()}
1756
  disabled={localizacaoLoading}
1757
  >
1758
- {localizacaoLoading ? 'Buscando...' : 'Buscar'}
1759
  </button>
1760
  <button
1761
  type="button"
@@ -1810,7 +1717,7 @@ export default function PesquisaTab({
1810
  onClick={() => void onResolverLocalizacao()}
1811
  disabled={localizacaoLoading}
1812
  >
1813
- {localizacaoLoading ? 'Buscando...' : 'Buscar'}
1814
  </button>
1815
  <button
1816
  type="button"
@@ -1822,7 +1729,7 @@ export default function PesquisaTab({
1822
  </button>
1823
  </div>
1824
  </div>
1825
- )) : null}
1826
 
1827
  {localizacaoStatus && !localizacaoAtiva ? <div className="status-line">{localizacaoStatus}</div> : null}
1828
  {localizacaoError ? <div className="error-line inline-error">{localizacaoError}</div> : null}
@@ -2154,7 +2061,7 @@ export default function PesquisaTab({
2154
  <select
2155
  {...buildSelectAutofillProps('mapaModoExibicao')}
2156
  value={mapaModoExibicao}
2157
- onChange={(event) => setMapaModoExibicao(event.target.value)}
2158
  >
2159
  <option value="pontos">Pontos representando dados de mercado</option>
2160
  <option value="cobertura">Cobertura dos modelos</option>
@@ -2176,7 +2083,7 @@ export default function PesquisaTab({
2176
  </select>
2177
  </label>
2178
 
2179
- {avaliandoUnicoAtivo ? (
2180
  <label className="pesquisa-field pesquisa-mapa-proximidade-field">
2181
  Trabalhos técnicos adicionais por raio
2182
  <select
@@ -2186,14 +2093,16 @@ export default function PesquisaTab({
2186
  disabled={mapaLoading || !selectedIds.length}
2187
  >
2188
  <option value="sem_proximidade">Não mostrar</option>
2189
- <option value="proximos_ao_avaliando">Mostrar próximos do avaliando</option>
 
 
2190
  </select>
2191
  </label>
2192
  ) : null}
2193
 
2194
- {avaliandoUnicoAtivo && mapaTrabalhosTecnicosProximidadeModo === 'proximos_ao_avaliando' ? (
2195
  <label className="pesquisa-field pesquisa-mapa-raio-field">
2196
- Raio do avaliando (m)
2197
  <div className="pesquisa-mapa-raio-control">
2198
  <input
2199
  type="range"
 
287
 
288
  function LocalizacaoResumoCard({
289
  title,
 
290
  localizacao = null,
291
  actions = null,
292
  className = '',
293
  }) {
294
  if (!localizacao) return null
295
+ const endereco = String(localizacao?.logradouro || '').trim()
296
+ const numeroUsado = String(localizacao?.numero_usado || '').trim()
297
+ const enderecoTexto = endereco ? `${endereco}${numeroUsado ? `, ${numeroUsado}` : ''}` : ''
298
+ const badges = [
299
+ enderecoTexto ? { label: 'Logradouro', value: enderecoTexto } : null,
300
+ localizacao?.cdlog ? { label: 'CDLOG', value: String(localizacao.cdlog) } : null,
301
+ { label: 'Lat', value: Number(localizacao.lat).toFixed(6) },
302
+ { label: 'Lon', value: Number(localizacao.lon).toFixed(6) },
303
+ { label: 'Origem', value: formatLocalizacaoOrigemLabel(localizacao) },
304
+ ].filter(Boolean)
305
  const classes = ['pesquisa-localizacao-registered', className].filter(Boolean).join(' ')
306
  return (
307
  <div className={classes}>
308
  <div className="pesquisa-localizacao-registered-head">
309
  <div className="pesquisa-localizacao-registered-copy">
310
  <strong>{title}</strong>
311
+ <div className="pesquisa-localizacao-registered-inline-meta">
312
+ {badges.map((item) => (
313
+ <span key={`${title}-${item.label}`} className="pesquisa-localizacao-registered-inline-item">
314
+ <strong>{item.label}:</strong> {item.value}
315
+ </span>
316
+ ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
318
  </div>
319
+ {actions}
320
  </div>
321
  </div>
322
  )
 
957
  const [localizacaoModo, setLocalizacaoModo] = useState('endereco')
958
  const [localizacaoInputs, setLocalizacaoInputs] = useState(EMPTY_LOCATION_INPUTS)
959
  const [avaliandosGeolocalizados, setAvaliandosGeolocalizados] = useState([])
 
960
  const [localizacaoLoading, setLocalizacaoLoading] = useState(false)
961
  const [localizacaoError, setLocalizacaoError] = useState('')
962
  const [localizacaoStatus, setLocalizacaoStatus] = useState('')
 
1016
  const localizacaoAtiva = avaliandosGeoPayload.length > 0
1017
  const localizacaoMultipla = avaliandosGeoPayload.length > 1
1018
  const avaliandoUnicoAtivo = avaliandosGeoPayload.length === 1
 
1019
  const modelosOrdenados = useMemo(
1020
  () => (localizacaoMultipla ? sortModelosBySpatialCriterion(result.modelos || [], criterioEspacial) : (result.modelos || [])),
1021
  [criterioEspacial, localizacaoMultipla, result.modelos],
 
1023
  const resultIds = useMemo(() => modelosOrdenados.map((modelo) => modelo.id), [modelosOrdenados])
1024
  const todosSelecionados = resultIds.length > 0 && resultIds.every((id) => selectedIds.includes(id))
1025
  const algunsSelecionados = resultIds.some((id) => selectedIds.includes(id))
 
 
 
 
1026
  const mapaHtmlAtual = mapaHtmls[mapaModoExibicao] || ''
1027
  const mapaFoiGerado = Boolean(mapaHtmls.pontos || mapaHtmls.cobertura)
1028
 
 
1065
  ? 'selecionados_e_outras_versoes'
1066
  : String(modelosModoBruto || 'selecionados')
1067
  const proximidadeModoBruto = overrides.trabalhosTecnicosProximidadeModo ?? mapaTrabalhosTecnicosProximidadeModo
1068
+ const proximidadeModo = localizacaoAtiva ? String(proximidadeModoBruto || 'sem_proximidade') : 'sem_proximidade'
1069
  const raioNumero = Number(overrides.trabalhosTecnicosRaio ?? mapaTrabalhosTecnicosRaio)
1070
  const raio = Number.isFinite(raioNumero)
1071
  ? Math.max(0, Math.min(5000, Math.round(raioNumero)))
 
1101
  try {
1102
  const response = await api.pesquisarMapaModelos(
1103
  idsValidos,
1104
+ localizacaoMultipla ? avaliandosGeoPayload : (avaliandosGeoPayload[0] || null),
1105
  modoExibicaoSolicitado,
1106
  criterioEspacial,
1107
  trabalhosTecnicosConfig.modelosModo,
1108
  trabalhosTecnicosConfig.proximidadeModo,
1109
  trabalhosTecnicosConfig.raio,
1110
  )
1111
+ const mapaHtmlSolicitado = String(
1112
+ response.mapa_html
1113
+ || (modoExibicaoSolicitado === 'cobertura' ? response.mapa_html_cobertura : response.mapa_html_pontos)
1114
+ || '',
1115
+ )
1116
  setMapaHtmls({
1117
+ pontos: modoExibicaoSolicitado === 'pontos' ? mapaHtmlSolicitado : '',
1118
+ cobertura: modoExibicaoSolicitado === 'cobertura' ? mapaHtmlSolicitado : '',
1119
  })
1120
  setMapaStatus(response.status || '')
1121
  mapaTrabalhosTecnicosConfigRef.current = buildMapaTrabalhosTecnicosConfigKey(trabalhosTecnicosConfig)
 
1387
  await carregarMapaPesquisa(selectedIds)
1388
  }
1389
 
1390
+ function onMapaModoExibicaoChange(event) {
1391
+ const nextModo = String(event?.target?.value || 'pontos')
1392
+ setMapaModoExibicao(nextModo)
1393
+ if (!mapaFoiGerado || mapaLoading || !selectedIds.length || mapaHtmls[nextModo]) return
1394
+ void carregarMapaPesquisa(selectedIds, { modoExibicao: nextModo })
1395
+ }
1396
+
1397
  async function onAdminConfigSalva() {
1398
  if (pesquisaInicializada) {
1399
  await buscarModelos(filters, avaliandosGeolocalizados)
 
1402
  await carregarContextoInicial()
1403
  }
1404
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1405
  function onLimparFormularioLocalizacao() {
1406
  setLocalizacaoInputs(EMPTY_LOCATION_INPUTS)
1407
  setLocalizacaoModo('endereco')
 
1462
  const nextEntries = [...avaliandosGeolocalizados, nextEntry]
1463
  setAvaliandosGeolocalizados(nextEntries)
1464
  setLocalizacaoInputs(EMPTY_LOCATION_INPUTS)
 
1465
  setLocalizacaoStatus(
1466
  nextEntries.length > 1
1467
  ? `${nextEntries.length} avaliandos geolocalizados com sucesso.`
 
1474
  setLocalizacaoLoading(false)
1475
  }
1476
  }
 
 
 
 
 
 
 
 
 
 
1477
  async function onRemoverAvaliandoLocalizacao(id) {
1478
  const nextEntries = avaliandosGeolocalizados.filter((item) => item.id !== id)
1479
  setAvaliandosGeolocalizados(nextEntries)
1480
  setLocalizacaoError('')
1481
  setLocalizacaoStatus('')
 
 
 
1482
  await atualizarPesquisaAposGeolocalizacao(nextEntries)
1483
  }
1484
 
 
1610
  subtitle="Registre um endereço ou coordenadas do avaliando. O preenchimento desta seção não é obrigatório. Se você não informar localização, a pesquisa continua normal, a distância espacial dos modelos não será calculada e o avaliando não aparecerá no mapa gerado."
1611
  >
1612
  <div className="pesquisa-localizacao-section">
1613
+ {avaliandosGeolocalizados.length ? (
1614
+ <div className="pesquisa-localizacao-multi-grid">
1615
+ {avaliandosGeolocalizados.map((item, index) => (
1616
+ <LocalizacaoResumoCard
1617
+ key={item.id}
1618
+ title={`Avaliando ${index + 1}: Geolocalização registrada`}
1619
+ localizacao={item}
1620
+ className="pesquisa-localizacao-multi-card"
1621
+ actions={(
1622
+ <button
1623
+ type="button"
1624
+ className="pesquisa-localizacao-action pesquisa-localizacao-action-reset"
1625
+ onClick={() => void onRemoverAvaliandoLocalizacao(item.id)}
1626
+ disabled={localizacaoLoading || loading}
1627
+ >
1628
+ Excluir avaliando
1629
+ </button>
1630
+ )}
1631
+ />
1632
+ ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1633
  </div>
1634
  ) : null}
1635
 
1636
+ {localizacaoModo === 'coords' ? (
1637
  <div className="pesquisa-localizacao-grid pesquisa-localizacao-grid-coords">
1638
  <label className="pesquisa-field">
1639
  Forma de localização
 
1662
  onClick={() => void onResolverLocalizacao()}
1663
  disabled={localizacaoLoading}
1664
  >
1665
+ {localizacaoLoading ? 'Adicionando...' : 'Adicionar'}
1666
  </button>
1667
  <button
1668
  type="button"
 
1717
  onClick={() => void onResolverLocalizacao()}
1718
  disabled={localizacaoLoading}
1719
  >
1720
+ {localizacaoLoading ? 'Adicionando...' : 'Adicionar'}
1721
  </button>
1722
  <button
1723
  type="button"
 
1729
  </button>
1730
  </div>
1731
  </div>
1732
+ )}
1733
 
1734
  {localizacaoStatus && !localizacaoAtiva ? <div className="status-line">{localizacaoStatus}</div> : null}
1735
  {localizacaoError ? <div className="error-line inline-error">{localizacaoError}</div> : null}
 
2061
  <select
2062
  {...buildSelectAutofillProps('mapaModoExibicao')}
2063
  value={mapaModoExibicao}
2064
+ onChange={onMapaModoExibicaoChange}
2065
  >
2066
  <option value="pontos">Pontos representando dados de mercado</option>
2067
  <option value="cobertura">Cobertura dos modelos</option>
 
2083
  </select>
2084
  </label>
2085
 
2086
+ {localizacaoAtiva ? (
2087
  <label className="pesquisa-field pesquisa-mapa-proximidade-field">
2088
  Trabalhos técnicos adicionais por raio
2089
  <select
 
2093
  disabled={mapaLoading || !selectedIds.length}
2094
  >
2095
  <option value="sem_proximidade">Não mostrar</option>
2096
+ <option value="proximos_ao_avaliando">
2097
+ {localizacaoMultipla ? 'Mostrar próximos de cada avaliando' : 'Mostrar próximos do avaliando'}
2098
+ </option>
2099
  </select>
2100
  </label>
2101
  ) : null}
2102
 
2103
+ {localizacaoAtiva && mapaTrabalhosTecnicosProximidadeModo === 'proximos_ao_avaliando' ? (
2104
  <label className="pesquisa-field pesquisa-mapa-raio-field">
2105
+ {localizacaoMultipla ? 'Raio de cada avaliando (m)' : 'Raio do avaliando (m)'}
2106
  <div className="pesquisa-mapa-raio-control">
2107
  <input
2108
  type="range"
frontend/src/styles.css CHANGED
@@ -2613,12 +2613,6 @@ button.pesquisa-otica-btn.active:hover {
2613
  color: #215a37;
2614
  }
2615
 
2616
- .pesquisa-localizacao-registered-copy span {
2617
- font-size: 0.82rem;
2618
- color: #496a56;
2619
- line-height: 1.45;
2620
- }
2621
-
2622
  .pesquisa-localizacao-registered-actions {
2623
  display: inline-flex;
2624
  align-items: center;
@@ -2627,11 +2621,6 @@ button.pesquisa-otica-btn.active:hover {
2627
  flex-wrap: wrap;
2628
  }
2629
 
2630
- .pesquisa-localizacao-multiple-block {
2631
- display: grid;
2632
- gap: 14px;
2633
- }
2634
-
2635
  .pesquisa-localizacao-multi-grid {
2636
  display: grid;
2637
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
@@ -2691,6 +2680,29 @@ button.pesquisa-otica-btn.active:hover {
2691
  min-width: 220px;
2692
  }
2693
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2694
  .pesquisa-localizacao-summary {
2695
  grid-column: 1 / -1;
2696
  display: grid;
 
2613
  color: #215a37;
2614
  }
2615
 
 
 
 
 
 
 
2616
  .pesquisa-localizacao-registered-actions {
2617
  display: inline-flex;
2618
  align-items: center;
 
2621
  flex-wrap: wrap;
2622
  }
2623
 
 
 
 
 
 
2624
  .pesquisa-localizacao-multi-grid {
2625
  display: grid;
2626
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
 
2680
  min-width: 220px;
2681
  }
2682
 
2683
+ .pesquisa-localizacao-registered-inline-meta {
2684
+ display: flex;
2685
+ flex-wrap: wrap;
2686
+ gap: 4px 12px;
2687
+ padding-top: 2px;
2688
+ color: #496a56;
2689
+ font-size: 0.8rem;
2690
+ line-height: 1.4;
2691
+ }
2692
+
2693
+ .pesquisa-localizacao-registered-inline-item {
2694
+ display: inline-flex;
2695
+ align-items: baseline;
2696
+ gap: 4px;
2697
+ min-width: 0;
2698
+ overflow-wrap: anywhere;
2699
+ }
2700
+
2701
+ .pesquisa-localizacao-registered-inline-item strong {
2702
+ color: #2d5c45;
2703
+ font-size: 0.79rem;
2704
+ }
2705
+
2706
  .pesquisa-localizacao-summary {
2707
  grid-column: 1 / -1;
2708
  display: grid;