import React, { useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { api, downloadBlob } from '../api'
import { buildPesquisaLink, buildPesquisaRoutePayload, getPesquisaFilterDefaults, hasPesquisaRoutePayload } from '../deepLinks'
import DataTable from './DataTable'
import EquationFormatsPanel from './EquationFormatsPanel'
import LoadingOverlay from './LoadingOverlay'
import MapFrame from './MapFrame'
import ModeloObservacaoCard from './ModeloObservacaoCard'
import ModeloTrabalhosTecnicosPanel from './ModeloTrabalhosTecnicosPanel'
import PlotFigure from './PlotFigure'
import PesquisaAdminConfigPanel from './PesquisaAdminConfigPanel'
import SectionBlock from './SectionBlock'
import ShareLinkButton from './ShareLinkButton'
import SinglePillAutocomplete from './SinglePillAutocomplete'
import { getFaixaDataRecencyInfo } from '../modelRecency'
const EMPTY_FILTERS = {
nomeModelo: '',
tipoModelo: '',
negociacaoModelo: '',
dataMin: '',
dataMax: '',
versionamentoModelos: 'incluir_antigos',
avalFinalidade: '',
avalZona: '',
avalBairro: '',
avalArea: '',
avalRh: '',
}
const EMPTY_LOCATION_INPUTS = {
latitude: '',
longitude: '',
logradouro: '',
numero: '',
cdlog: '',
}
const CRITERIO_ESPACIAL_OPTIONS = [
{ value: 'maior_distancia', label: 'Maior distância' },
{ value: 'media_distancia', label: 'Média distância' },
{ value: 'menor_distancia', label: 'Menor distância' },
]
const RESULT_INITIAL = {
modelos: [],
sugestoes: {},
total_filtrado: 0,
total_geral: 0,
}
const PESQUISA_INNER_TABS = [
{ key: 'mapa', label: 'Mapa' },
{ key: 'trabalhos_tecnicos', label: 'Trabalhos Técnicos' },
{ key: 'dados_mercado', label: 'Dados de Mercado' },
{ key: 'metricas', label: 'Métricas' },
{ key: 'transformacoes', label: 'Transformações' },
{ key: 'resumo', label: 'Resumo' },
{ key: 'coeficientes', label: 'Coeficientes' },
{ key: 'obs_calc', label: 'Obs x Calc' },
{ key: 'graficos', label: 'Gráficos' },
]
const MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO = 'selecionados_e_outras_versoes'
const TIPO_SIGLAS = {
RECOND: 'Residencia em condominio',
RCOMD: 'Residencia em condominio',
TCOND: 'Terreno em condominio',
SALA: 'Salas comerciais',
APTO: 'Apartamentos',
APART: 'Apartamentos',
AP: 'Apartamentos',
TERRENO: 'Terrenos',
TER: 'Terrenos',
EDIF: 'Edificio',
RES: 'Residencias isoladas / casas',
CASA: 'Residencias isoladas / casas',
LOJA: 'Loja',
LCOM: 'Loja',
DEP: 'Deposito',
DEPOS: 'Deposito',
CCOM: 'Casa comercial',
}
const TIPOS_MODELO_GENERICOS = [
'Apartamentos',
'Casa comercial',
'Deposito',
'Edificio',
'Loja',
'Salas comerciais',
'Terrenos',
]
function formatRange(range) {
if (!range) return '-'
const min = range.min ?? null
const max = range.max ?? null
if (min === null && max === null) return '-'
if (min !== null && max !== null) return `${formatDateBrIfIso(min)} a ${formatDateBrIfIso(max)}`
if (min !== null) return `a partir de ${formatDateBrIfIso(min)}`
return `ate ${formatDateBrIfIso(max)}`
}
function formatCount(value) {
if (value === null || value === undefined || value === '') return '-'
if (typeof value === 'number') {
return new Intl.NumberFormat('pt-BR').format(value)
}
return String(value)
}
function formatDistanceKm(value) {
const number = Number(value)
if (!Number.isFinite(number)) return '-'
return `${number.toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} km`
}
function formatAvaliandoGeoLabel(index) {
return `A${index + 1}`
}
function parseCoordinateValue(value) {
if (value === null || value === undefined) return null
const text = String(value).trim().replace(',', '.')
if (!text) return null
const parsed = Number(text)
return Number.isFinite(parsed) ? parsed : null
}
function normalizeLogradouroOption(item) {
if (typeof item === 'string') {
const text = String(item || '').trim()
return text ? { value: text, label: text } : null
}
if (!item || typeof item !== 'object') return null
const value = String(item.value ?? item.logradouro ?? '').trim()
if (!value) return null
const label = String(item.label ?? item.display_label ?? value).trim() || value
return {
value,
label,
secondary: String(item.secondary ?? '').trim(),
}
}
function buildAvaliandosGeoPayload(entries = []) {
return (entries || []).map((item, index) => ({
id: String(item?.id || `avaliando-${index + 1}`),
label: formatAvaliandoGeoLabel(index),
lat: parseCoordinateValue(item?.lat),
lon: parseCoordinateValue(item?.lon),
logradouro: item?.logradouro || null,
numero_usado: item?.numero_usado || null,
cdlog: item?.cdlog ?? null,
origem: item?.origem || null,
})).filter((item) => item.lat !== null && item.lon !== null)
}
function getResumoEspacialValor(resumo, criterio) {
if (!resumo || typeof resumo !== 'object') return null
if (criterio === 'menor_distancia') return Number(resumo.menor_distancia_km)
if (criterio === 'media_distancia') return Number(resumo.media_distancia_km)
return Number(resumo.maior_distancia_km)
}
function sortModelosBySpatialCriterion(modelos = [], criterio = 'maior_distancia') {
if (!Array.isArray(modelos)) return []
return [...modelos].sort((a, b) => {
const aValor = getResumoEspacialValor(a?.distancia_resumo, criterio)
const bValor = getResumoEspacialValor(b?.distancia_resumo, criterio)
const aSem = !Number.isFinite(aValor)
const bSem = !Number.isFinite(bValor)
if (aSem !== bSem) return aSem ? 1 : -1
if (!aSem && !bSem && aValor !== bValor) return aValor - bValor
const aNome = String(a?.nome_modelo || a?.arquivo || a?.id || '').toLowerCase()
const bNome = String(b?.nome_modelo || b?.arquivo || b?.id || '').toLowerCase()
return aNome.localeCompare(bNome, 'pt-BR')
})
}
function formatResumoEspacial(resumo) {
if (!resumo || typeof resumo !== 'object') return '-'
return `Maior ${String(resumo.maior_distancia_label || '-')} • Média ${String(resumo.media_distancia_label || '-')} • Menor ${String(resumo.menor_distancia_label || '-')}`
}
function formatResumoEspacialCobertura(resumo) {
if (!resumo || typeof resumo !== 'object') return ''
const total = Number(resumo.total_avaliandos || 0)
const comDistancia = Number(resumo.total_com_distancia || 0)
if (!total) return ''
return `${comDistancia}/${total} com distância calculada`
}
function formatDistanciasPorAvaliando(distancias = []) {
if (!Array.isArray(distancias) || !distancias.length) return '-'
return distancias.map((item, index) => {
const label = String(item?.label || formatAvaliandoGeoLabel(index)).trim() || formatAvaliandoGeoLabel(index)
const distancia = String(item?.distancia_label || '').trim() || formatDistanceKm(item?.distancia_km)
return `${label}: ${distancia}`
}).join(' • ')
}
function formatLocalizacaoOrigemLabel(localizacao) {
return localizacao?.origem === 'eixos'
? 'Eixos de logradouro'
: 'Coordenadas informadas'
}
function createLocalizacaoEntry(resolvida, id) {
return {
...resolvida,
id,
lat: parseCoordinateValue(resolvida?.lat),
lon: parseCoordinateValue(resolvida?.lon),
}
}
function formatDateBrIfIso(value) {
const text = String(value ?? '').trim()
if (!text) return '-'
const isoMatch = text.match(/^(\d{4})-(\d{2})-(\d{2})$/)
if (!isoMatch) return text
return `${isoMatch[3]}/${isoMatch[2]}/${isoMatch[1]}`
}
function normalizeTokenText(value) {
return String(value || '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toUpperCase()
}
function normalizeSearchText(value) {
return String(value || '')
.normalize('NFD')
.replace(/[\u0300-\u036f]/g, '')
.toLowerCase()
.trim()
}
function splitMultiTerms(value) {
const text = String(value || '').trim()
const parts = text.includes('||')
? text.split(/\s*\|\|\s*/)
: text.split(/[;|]/)
const raw = parts
.map((item) => String(item || '').trim())
.filter(Boolean)
const seen = new Set()
const unique = []
raw.forEach((item) => {
const key = normalizeSearchText(item)
if (!key || seen.has(key)) return
seen.add(key)
unique.push(item)
})
return unique
}
function joinMultiTerms(values) {
return (values || []).map((item) => String(item || '').trim()).filter(Boolean).join(' || ')
}
function sanitizeCompactList(values) {
const seen = new Set()
const unique = []
;(values || []).forEach((item) => {
const text = String(item || '').trim()
const key = normalizeSearchText(text)
if (!text || !key || seen.has(key)) return
seen.add(key)
unique.push(text)
})
return unique
}
function uppercaseListText(values) {
const items = sanitizeCompactList(values)
return items.map((item) => item.toLocaleUpperCase('pt-BR')).join(', ')
}
function inferDependentVariable(modelo) {
const equacao = String(modelo?.equacao || '').trim()
if (!equacao || !equacao.includes('=')) return ''
return equacao.split('=', 1)[0].trim()
}
function buildVariablesDisplay(modelo) {
const dependente = inferDependentVariable(modelo)
const independentes = sanitizeCompactList((modelo?.variaveis_resumo || []).map((item) => item?.variavel))
.filter((item) => normalizeSearchText(item) !== normalizeSearchText(dependente))
.filter((item) => normalizeSearchText(item) !== 'const')
const partes = []
if (dependente) {
partes.push(`DEPENDENTE: ${dependente.toLocaleUpperCase('pt-BR')}`)
}
if (independentes.length) {
partes.push(`INDEPENDENTES: ${independentes.map((item) => item.toLocaleUpperCase('pt-BR')).join(', ')}`)
}
return partes.join('\n')
}
function buildVariablesContent(text) {
const lines = String(text || '').split('\n').map((item) => item.trim()).filter(Boolean)
if (!lines.length) return null
return (
<>
{lines.map((line, index) => {
const [head, ...rest] = line.split(':')
const tail = rest.join(':').trim()
return (
{head}:{tail ? ` ${tail}` : ''}
)
})}
>
)
}
function LocalizacaoResumoCard({
title,
localizacao = null,
actions = null,
className = '',
}) {
if (!localizacao) return null
const endereco = String(localizacao?.logradouro || '').trim()
const numeroUsado = String(localizacao?.numero_usado || '').trim()
const enderecoTexto = endereco ? `${endereco}${numeroUsado ? `, ${numeroUsado}` : ''}` : ''
const badges = [
enderecoTexto ? { label: 'Logradouro', value: enderecoTexto } : null,
localizacao?.cdlog ? { label: 'CDLOG', value: String(localizacao.cdlog) } : null,
{ label: 'Lat', value: Number(localizacao.lat).toFixed(6) },
{ label: 'Lon', value: Number(localizacao.lon).toFixed(6) },
{ label: 'Origem', value: formatLocalizacaoOrigemLabel(localizacao) },
].filter(Boolean)
const classes = ['pesquisa-localizacao-registered', className].filter(Boolean).join(' ')
return (
{title}
{badges.map((item) => (
{item.label}: {item.value}
))}
{actions}
)
}
function CompactHoverList({
label,
buttonLabel,
previewText,
modalText,
previewContent = null,
modalContent = null,
}) {
const rootRef = useRef(null)
const closePreviewTimeoutRef = useRef(null)
const panelRef = useRef(null)
const [previewOpen, setPreviewOpen] = useState(false)
const [modalOpen, setModalOpen] = useState(false)
const [previewStyle, setPreviewStyle] = useState({})
const inlinePreviewText = String(previewText || '').trim()
const inlineModalText = String(modalText || '').trim()
function clearPreviewCloseTimeout() {
if (closePreviewTimeoutRef.current) {
window.clearTimeout(closePreviewTimeoutRef.current)
closePreviewTimeoutRef.current = null
}
}
function schedulePreviewClose() {
clearPreviewCloseTimeout()
closePreviewTimeoutRef.current = window.setTimeout(() => {
setPreviewOpen(false)
}, 80)
}
function openPreview() {
clearPreviewCloseTimeout()
setPreviewOpen(true)
}
function updatePreviewLayout() {
const bounds = rootRef.current?.getBoundingClientRect()
const panel = panelRef.current
if (!bounds || !panel || typeof window === 'undefined') return
const viewportWidth = window.innerWidth
const viewportHeight = window.innerHeight
const gap = 10
const padding = 16
const triggerCenter = bounds.left + (bounds.width / 2)
const towardCenterSide = triggerCenter >= (viewportWidth / 2) ? 'left' : 'right'
const oppositeSide = towardCenterSide === 'left' ? 'right' : 'left'
const downwardSpace = viewportHeight - bounds.bottom - gap - padding
const upwardSpace = bounds.top - gap - padding
const preferredVertical = downwardSpace >= upwardSpace ? 'down' : 'up'
const oppositeVertical = preferredVertical === 'down' ? 'up' : 'down'
const desiredWidth = Math.min(panel.scrollWidth || 560, 560)
const desiredHeight = Math.min(panel.scrollHeight || 220, 420)
function clamp(value, min, max) {
if (max < min) return min
return Math.min(Math.max(value, min), max)
}
function makeVerticalCandidate(side, vertical) {
const maxWidth = Math.max(160, viewportWidth - (padding * 2))
const width = Math.min(desiredWidth, maxWidth)
const maxHeight = Math.max(96, vertical === 'down' ? downwardSpace : upwardSpace)
const height = Math.min(desiredHeight, maxHeight)
const leftBase = side === 'left'
? bounds.right - width
: bounds.left
const left = clamp(leftBase, padding, viewportWidth - padding - width)
const topBase = vertical === 'down'
? bounds.bottom + gap
: bounds.top - gap - height
const top = clamp(topBase, padding, viewportHeight - padding - height)
return {
left,
top,
maxWidth,
maxHeight,
score: (Math.min(maxWidth, desiredWidth) / desiredWidth) + (Math.min(maxHeight, desiredHeight) / desiredHeight),
}
}
function makeSideCandidate(side) {
const maxWidth = Math.max(180, side === 'right'
? viewportWidth - bounds.right - gap - padding
: bounds.left - gap - padding)
const width = Math.min(desiredWidth, maxWidth)
const maxHeight = Math.max(120, viewportHeight - (padding * 2))
const height = Math.min(desiredHeight, maxHeight)
const leftBase = side === 'right'
? bounds.right + gap
: bounds.left - gap - width
const left = clamp(leftBase, padding, viewportWidth - padding - width)
const top = clamp(bounds.top, padding, viewportHeight - padding - height)
return {
left,
top,
maxWidth,
maxHeight,
score: (Math.min(maxWidth, desiredWidth) / desiredWidth) + (Math.min(maxHeight, desiredHeight) / desiredHeight),
}
}
const candidates = [
makeVerticalCandidate(towardCenterSide, preferredVertical),
makeVerticalCandidate(towardCenterSide, oppositeVertical),
makeSideCandidate(towardCenterSide),
makeSideCandidate(oppositeSide),
makeVerticalCandidate(oppositeSide, preferredVertical),
makeVerticalCandidate(oppositeSide, oppositeVertical),
]
const best = candidates.reduce((current, candidate) => {
if (!current || candidate.score > current.score) return candidate
return current
}, null)
if (!best) return
setPreviewStyle({
left: `${best.left}px`,
top: `${best.top}px`,
maxWidth: `${Math.max(160, best.maxWidth)}px`,
maxHeight: `${Math.max(96, best.maxHeight)}px`,
})
}
useEffect(() => {
if (!previewOpen) return undefined
const rafId = window.requestAnimationFrame(() => {
updatePreviewLayout()
})
function onWindowResize() {
updatePreviewLayout()
}
window.addEventListener('resize', onWindowResize)
return () => {
window.cancelAnimationFrame(rafId)
window.removeEventListener('resize', onWindowResize)
}
}, [previewOpen])
useEffect(() => () => {
clearPreviewCloseTimeout()
}, [])
useEffect(() => {
if (!modalOpen) return undefined
function onDocumentKeyDown(event) {
if (event.key === 'Escape') {
setModalOpen(false)
}
}
document.addEventListener('keydown', onDocumentKeyDown)
return () => {
document.removeEventListener('keydown', onDocumentKeyDown)
}
}, [modalOpen])
if (!inlineModalText) {
return (
)
}
return (
<>
openPreview()}
onMouseLeave={() => schedulePreviewClose()}
onFocus={() => openPreview()}
onBlur={(event) => {
if (!rootRef.current?.contains(event.relatedTarget)) {
setPreviewOpen(false)
}
}}
>
{previewOpen && typeof document !== 'undefined'
? createPortal(
openPreview()}
onMouseLeave={() => schedulePreviewClose()}
>
{previewContent || inlinePreviewText || inlineModalText}
,
document.body,
)
: null}
{modalOpen && typeof document !== 'undefined'
? createPortal(
{
setModalOpen(false)
}}
>
event.stopPropagation()}
>
{label}
{modalContent || inlineModalText}
,
document.body,
)
: null}
>
)
}
function inferTipoPorNomeModelo(...nomes) {
const tokens = Object.keys(TIPO_SIGLAS).sort((a, b) => b.length - a.length)
for (const nome of nomes) {
const source = normalizeTokenText(nome)
if (!source) continue
for (const token of tokens) {
const re = new RegExp(`(^|[^A-Z])${token}([^A-Z]|$)`)
if (re.test(source)) {
return TIPO_SIGLAS[token]
}
}
}
return ''
}
function formatTipoImovel(modelo) {
const tipoPorNome = inferTipoPorNomeModelo(modelo?.nome_modelo, modelo?.arquivo)
if (tipoPorNome) return tipoPorNome
const text = String(modelo?.tipo_imovel || '').trim()
if (!text) return '-'
const mapped = TIPO_SIGLAS[normalizeTokenText(text)]
return mapped || text
}
function buildApiFilters(filters, avaliandosGeolocalizados = []) {
const payload = {
otica: 'avaliando',
nome: filters.nomeModelo,
tipo_modelo: filters.tipoModelo,
negociacao_modelo: filters.negociacaoModelo,
aval_finalidade: filters.avalFinalidade,
aval_zona: filters.avalZona,
aval_bairro: filters.avalBairro,
data_min: filters.dataMin,
data_max: filters.dataMax,
somente_versoes_atuais: filters.versionamentoModelos !== 'incluir_antigos',
aval_area: filters.avalArea,
aval_rh: filters.avalRh,
}
const avaliandosPayload = buildAvaliandosGeoPayload(avaliandosGeolocalizados)
if (avaliandosPayload.length > 1) {
payload.avaliandos_geo_json = JSON.stringify(avaliandosPayload)
} else if (avaliandosPayload.length === 1) {
payload.aval_lat = avaliandosPayload[0].lat
payload.aval_lon = avaliandosPayload[0].lon
}
return payload
}
function toInputName(field) {
let hash = 0
for (let i = 0; i < field.length; i += 1) {
hash = (hash * 31 + field.charCodeAt(i)) % 1000000007
}
return `mesa_${Math.abs(hash)}`
}
function buildInputAutofillProps(field) {
return {
name: toInputName(field),
autoComplete: 'new-password',
autoCorrect: 'off',
autoCapitalize: 'none',
spellCheck: false,
'data-lpignore': 'true',
'data-1p-ignore': 'true',
}
}
function buildSelectAutofillProps(field) {
return {
name: toInputName(field),
autoComplete: 'new-password',
'data-lpignore': 'true',
'data-1p-ignore': 'true',
}
}
function TextFieldInput({ field, ...props }) {
return (
)
}
function NumberFieldInput({ field, ...props }) {
return (
)
}
function DateFieldInput({ field, ...props }) {
return (
)
}
function ChipAutocompleteInput({
field,
value,
onChange,
placeholder,
suggestions = [],
panelTitle = 'Sugestoes',
loading = false,
emptyMessage = 'Nenhuma sugestao encontrada.',
loadingMessage = 'Carregando sugestoes...',
}) {
const rootRef = useRef(null)
const [query, setQuery] = useState('')
const [open, setOpen] = useState(false)
const [activeIndex, setActiveIndex] = useState(-1)
const selectedValues = useMemo(() => splitMultiTerms(value), [value])
const selectedKeys = useMemo(() => new Set(selectedValues.map((item) => normalizeSearchText(item))), [selectedValues])
const queryNormalized = normalizeSearchText(query)
useEffect(() => {
if (!value) setQuery('')
}, [value])
const filteredSuggestions = useMemo(() => {
const unique = []
const seen = new Set()
;(suggestions || []).forEach((item) => {
const text = String(item || '').trim()
if (!text) return
const key = normalizeSearchText(text)
if (!key || seen.has(key) || selectedKeys.has(key)) return
seen.add(key)
unique.push(text)
})
if (!queryNormalized) return unique.slice(0, 120)
return unique
.filter((item) => normalizeSearchText(item).includes(queryNormalized))
.slice(0, 120)
}, [suggestions, queryNormalized, selectedKeys])
useEffect(() => {
if (!open) return undefined
function onDocumentMouseDown(event) {
if (!rootRef.current) return
if (!rootRef.current.contains(event.target)) {
setOpen(false)
}
}
document.addEventListener('mousedown', onDocumentMouseDown)
return () => document.removeEventListener('mousedown', onDocumentMouseDown)
}, [open])
useEffect(() => {
if (!open || !filteredSuggestions.length) {
setActiveIndex(-1)
return
}
setActiveIndex(-1)
}, [filteredSuggestions, open])
function emitValue(nextValue) {
onChange({
target: {
value: nextValue,
dataset: { field },
name: toInputName(field),
},
})
}
function setSelectedValues(nextSelected) {
emitValue(joinMultiTerms(nextSelected))
}
function addValue(nextValue) {
const text = String(nextValue || '').trim()
if (!text) return
const key = normalizeSearchText(text)
if (!key || selectedKeys.has(key)) {
setQuery('')
return
}
setSelectedValues([...selectedValues, text])
setQuery('')
setOpen(true)
setActiveIndex(-1)
}
function removeValue(nextValue) {
const target = String(nextValue || '').trim()
if (!target) return
const exactIndex = selectedValues.findIndex((item) => String(item || '').trim() === target)
if (exactIndex >= 0) {
const nextSelected = [...selectedValues]
nextSelected.splice(exactIndex, 1)
setSelectedValues(nextSelected)
return
}
const key = normalizeSearchText(target)
const normalizedIndex = selectedValues.findIndex((item) => normalizeSearchText(item) === key)
if (normalizedIndex < 0) return
const nextSelected = [...selectedValues]
nextSelected.splice(normalizedIndex, 1)
setSelectedValues(nextSelected)
}
function onInputChange(event) {
setQuery(event.target.value)
setOpen(true)
setActiveIndex(-1)
}
function onInputKeyDown(event) {
if (event.key === 'Escape') {
setOpen(false)
return
}
if (!filteredSuggestions.length) return
if (event.key === 'ArrowDown') {
event.preventDefault()
setOpen(true)
setActiveIndex((prev) => (prev < 0 ? 0 : (prev + 1) % filteredSuggestions.length))
return
}
if (event.key === 'ArrowUp') {
event.preventDefault()
setOpen(true)
setActiveIndex((prev) => {
if (prev < 0) return filteredSuggestions.length - 1
return (prev - 1 + filteredSuggestions.length) % filteredSuggestions.length
})
return
}
if (event.key === 'Enter' && open && activeIndex >= 0) {
event.preventDefault()
addValue(filteredSuggestions[activeIndex])
return
}
if (event.key === 'Enter') {
const next = String(query || '').trim()
if (next) {
event.preventDefault()
addValue(next)
}
return
}
if (event.key === ',' || event.key === ';' || event.key === '|') {
const next = String(query || '').trim()
if (next) {
event.preventDefault()
addValue(next)
}
return
}
if (event.key === 'Backspace' && !query && selectedValues.length) {
const nextSelected = selectedValues.slice(0, -1)
setSelectedValues(nextSelected)
}
}
return (
{selectedValues.length ? (
{selectedValues.map((item) => (
{item}
))}
) : null}
{
setOpen(true)
setActiveIndex(-1)
}}
onKeyDown={onInputKeyDown}
placeholder={placeholder}
/>
{open ? (
{panelTitle}
{filteredSuggestions.length ? (
{filteredSuggestions.map((item, idx) => (
))}
) : loading ? (
{loadingMessage}
) : (
{emptyMessage}
)}
) : null}
)
}
export default function PesquisaTab({
sessionId,
onUsarModeloEmAvaliacao = null,
onAbrirModeloNoRepositorio = null,
routeRequest = null,
scrollToMapaRequest = null,
onRouteChange = null,
}) {
const [searchLoading, setSearchLoading] = useState(false)
const [contextLoading, setContextLoading] = useState(false)
const [error, setError] = useState('')
const [pesquisaInicializada, setPesquisaInicializada] = useState(false)
const [sugestoesInicializadas, setSugestoesInicializadas] = useState(false)
const [mostrarAdminConfig, setMostrarAdminConfig] = useState(false)
const [logradouroOptions, setLogradouroOptions] = useState([])
const [logradouroOptionsLoading, setLogradouroOptionsLoading] = useState(false)
const [logradouroOptionsLoaded, setLogradouroOptionsLoaded] = useState(false)
const [filters, setFilters] = useState(EMPTY_FILTERS)
const [result, setResult] = useState(RESULT_INITIAL)
const [localizacaoModo, setLocalizacaoModo] = useState('endereco')
const [localizacaoInputs, setLocalizacaoInputs] = useState(EMPTY_LOCATION_INPUTS)
const [avaliandosGeolocalizados, setAvaliandosGeolocalizados] = useState([])
const [localizacaoLoading, setLocalizacaoLoading] = useState(false)
const [localizacaoError, setLocalizacaoError] = useState('')
const [localizacaoStatus, setLocalizacaoStatus] = useState('')
const [criterioEspacial, setCriterioEspacial] = useState('maior_distancia')
const [selectedIds, setSelectedIds] = useState([])
const selectAllRef = useRef(null)
const localizacaoIdCounterRef = useRef(1)
const [mapaLoading, setMapaLoading] = useState(false)
const [mapaError, setMapaError] = useState('')
const [mapaStatus, setMapaStatus] = useState('')
const [mapaHtmls, setMapaHtmls] = useState({ pontos: '', cobertura: '' })
const [mapaPayloads, setMapaPayloads] = useState({ pontos: null, cobertura: null })
const [mapaModoExibicao, setMapaModoExibicao] = useState('pontos')
const [mapaTrabalhosTecnicosModelosModo, setMapaTrabalhosTecnicosModelosModo] = useState('selecionados_e_outras_versoes')
const [mapaTrabalhosTecnicosProximidadeModo, setMapaTrabalhosTecnicosProximidadeModo] = useState('sem_proximidade')
const [mapaTrabalhosTecnicosRaio, setMapaTrabalhosTecnicosRaio] = useState(1000)
const mapaTrabalhosTecnicosConfigRef = useRef('')
const [modeloAbertoMeta, setModeloAbertoMeta] = useState(null)
const [modeloAbertoLoading, setModeloAbertoLoading] = useState(false)
const [modeloAbertoError, setModeloAbertoError] = useState('')
const [modeloAbertoActiveTab, setModeloAbertoActiveTab] = useState('mapa')
const [modeloAbertoDados, setModeloAbertoDados] = useState(null)
const [modeloAbertoEstatisticas, setModeloAbertoEstatisticas] = useState(null)
const [modeloAbertoEscalasHtml, setModeloAbertoEscalasHtml] = useState('')
const [modeloAbertoDadosTransformados, setModeloAbertoDadosTransformados] = useState(null)
const [modeloAbertoResumoHtml, setModeloAbertoResumoHtml] = useState('')
const [modeloAbertoEquacoes, setModeloAbertoEquacoes] = useState(null)
const [modeloAbertoCoeficientes, setModeloAbertoCoeficientes] = useState(null)
const [modeloAbertoObsCalc, setModeloAbertoObsCalc] = useState(null)
const [modeloAbertoMapaHtml, setModeloAbertoMapaHtml] = useState('')
const [modeloAbertoMapaPayload, setModeloAbertoMapaPayload] = useState(null)
const [modeloAbertoMapaChoices, setModeloAbertoMapaChoices] = useState(['Visualização Padrão'])
const [modeloAbertoMapaVar, setModeloAbertoMapaVar] = useState('Visualização Padrão')
const [modeloAbertoTrabalhosTecnicosModelosModo, setModeloAbertoTrabalhosTecnicosModelosModo] = useState(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
const [modeloAbertoTrabalhosTecnicos, setModeloAbertoTrabalhosTecnicos] = useState([])
const [modeloAbertoPlotObsCalc, setModeloAbertoPlotObsCalc] = useState(null)
const [modeloAbertoPlotResiduos, setModeloAbertoPlotResiduos] = useState(null)
const [modeloAbertoPlotHistograma, setModeloAbertoPlotHistograma] = useState(null)
const [modeloAbertoPlotCook, setModeloAbertoPlotCook] = useState(null)
const [modeloAbertoPlotCorr, setModeloAbertoPlotCorr] = useState(null)
const [modeloAbertoLoadedTabs, setModeloAbertoLoadedTabs] = useState({})
const [modeloAbertoLoadingTabs, setModeloAbertoLoadingTabs] = useState({})
const sectionResultadosRef = useRef(null)
const sectionMapaRef = useRef(null)
const scrollMapaHandledRef = useRef('')
const routeRequestHandledRef = useRef('')
const resultRequestSeqRef = useRef(0)
const searchRequestSeqRef = useRef(0)
const contextRequestSeqRef = useRef(0)
const modeloAbertoPendingRequestsRef = useRef({})
const modeloAbertoOpenVersionRef = useRef(0)
const sugestoes = result.sugestoes || {}
const opcoesTipoModelo = useMemo(
() => {
const atual = String(filters.tipoModelo || '').trim()
if (!atual || TIPOS_MODELO_GENERICOS.includes(atual)) return TIPOS_MODELO_GENERICOS
return [...TIPOS_MODELO_GENERICOS, atual]
},
[filters.tipoModelo],
)
const modoModeloAberto = Boolean(modeloAbertoMeta)
const avaliandosGeoPayload = useMemo(
() => buildAvaliandosGeoPayload(avaliandosGeolocalizados),
[avaliandosGeolocalizados],
)
const localizacaoAtiva = avaliandosGeoPayload.length > 0
const localizacaoMultipla = avaliandosGeoPayload.length > 1
const avaliandoUnicoAtivo = avaliandosGeoPayload.length === 1
const modelosOrdenados = useMemo(
() => (localizacaoMultipla ? sortModelosBySpatialCriterion(result.modelos || [], criterioEspacial) : (result.modelos || [])),
[criterioEspacial, localizacaoMultipla, result.modelos],
)
const resultIds = useMemo(() => modelosOrdenados.map((modelo) => modelo.id), [modelosOrdenados])
const todosSelecionados = resultIds.length > 0 && resultIds.every((id) => selectedIds.includes(id))
const algunsSelecionados = resultIds.some((id) => selectedIds.includes(id))
const mapaHtmlAtual = mapaHtmls[mapaModoExibicao] || ''
const mapaPayloadAtual = mapaPayloads[mapaModoExibicao] || null
const mapaFoiGerado = Boolean(mapaHtmls.pontos || mapaHtmls.cobertura || mapaPayloads.pontos || mapaPayloads.cobertura)
const pesquisaShareAvaliando = !localizacaoMultipla ? (avaliandosGeoPayload[0] || null) : null
const pesquisaShareHref = buildPesquisaLink(filters, pesquisaShareAvaliando)
const pesquisaShareDisabled = localizacaoMultipla
const isPesquisaBusy = searchLoading
function buildPesquisaReturnIntent() {
return {
...buildPesquisaRoutePayload(filters, pesquisaShareAvaliando),
pesquisaExecutada: true,
avaliandos: avaliandosGeolocalizados.map((item, index) => ({
id: String(item?.id || `avaliando-${index + 1}`),
lat: parseCoordinateValue(item?.lat),
lon: parseCoordinateValue(item?.lon),
logradouro: String(item?.logradouro || ''),
numero_usado: String(item?.numero_usado || ''),
cdlog: item?.cdlog ?? null,
origem: String(item?.origem || 'coords'),
})).filter((item) => item.lat !== null && item.lon !== null),
}
}
function emitPesquisaRoute(nextFilters = filters, nextAvaliandos = avaliandosGeolocalizados) {
if (typeof onRouteChange !== 'function') return
const avaliandosPayload = buildAvaliandosGeoPayload(nextAvaliandos)
const avaliando = avaliandosPayload.length === 1 ? avaliandosPayload[0] : null
onRouteChange(buildPesquisaRoutePayload(nextFilters, avaliando))
}
function scrollToElementTop(el, behavior = 'smooth', offsetPx = 0) {
if (!el || typeof window === 'undefined') return
const rect = el.getBoundingClientRect()
const targetTop = Math.max(0, window.scrollY + rect.top - offsetPx)
window.scrollTo({ top: targetTop, behavior })
}
function scrollParaTopoDaPagina() {
if (typeof window === 'undefined') return
window.scrollTo({ top: 0, behavior: 'smooth' })
}
function scrollParaResultadosNoTopo() {
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
scrollToElementTop(sectionResultadosRef.current, 'smooth', 0)
})
})
}
function resetMapaPesquisa() {
setMapaHtmls({ pontos: '', cobertura: '' })
setMapaPayloads({ pontos: null, cobertura: null })
setMapaStatus('')
setMapaError('')
setMapaModoExibicao('pontos')
mapaTrabalhosTecnicosConfigRef.current = ''
}
function getMapaTrabalhosTecnicosRequestConfig(overrides = {}) {
const modelosModoBruto = overrides.trabalhosTecnicosModelosModo ?? mapaTrabalhosTecnicosModelosModo
const modelosModo = String(modelosModoBruto || 'selecionados') === 'selecionados_e_anteriores'
? 'selecionados_e_outras_versoes'
: String(modelosModoBruto || 'selecionados')
const proximidadeModoBruto = overrides.trabalhosTecnicosProximidadeModo ?? mapaTrabalhosTecnicosProximidadeModo
const proximidadeModo = localizacaoAtiva ? String(proximidadeModoBruto || 'sem_proximidade') : 'sem_proximidade'
const raioNumero = Number(overrides.trabalhosTecnicosRaio ?? mapaTrabalhosTecnicosRaio)
const raio = Number.isFinite(raioNumero)
? Math.max(0, Math.min(5000, Math.round(raioNumero)))
: 1000
return { modelosModo, proximidadeModo, raio }
}
function buildMapaTrabalhosTecnicosConfigKey(config) {
return JSON.stringify({
totalAvaliandos: avaliandosGeoPayload.length,
modelosModo: config.modelosModo,
proximidadeModo: config.proximidadeModo,
criterioEspacial: localizacaoMultipla ? criterioEspacial : null,
raio: config.proximidadeModo === 'proximos_ao_avaliando' ? config.raio : null,
})
}
async function carregarMapaPesquisa(ids, overrides = {}) {
const idsValidos = (ids || []).map((item) => String(item || '').trim()).filter(Boolean)
const modoExibicaoSolicitado = String(overrides.modoExibicao || mapaModoExibicao || 'pontos')
const trabalhosTecnicosConfig = getMapaTrabalhosTecnicosRequestConfig(overrides)
if (!idsValidos.length) {
setMapaHtmls({ pontos: '', cobertura: '' })
setMapaPayloads({ pontos: null, cobertura: null })
setMapaStatus('Nenhum modelo marcado para exibição no mapa.')
setMapaError('')
mapaTrabalhosTecnicosConfigRef.current = ''
return
}
setMapaLoading(true)
setMapaError('')
try {
const response = await api.pesquisarMapaModelos(
idsValidos,
localizacaoMultipla ? avaliandosGeoPayload : (avaliandosGeoPayload[0] || null),
modoExibicaoSolicitado,
criterioEspacial,
trabalhosTecnicosConfig.modelosModo,
trabalhosTecnicosConfig.proximidadeModo,
trabalhosTecnicosConfig.raio,
)
const mapaHtmlSolicitado = String(
response.mapa_html
|| (modoExibicaoSolicitado === 'cobertura' ? response.mapa_html_cobertura : response.mapa_html_pontos)
|| '',
)
const mapaPayloadSolicitado = response.mapa_payload
|| (modoExibicaoSolicitado === 'cobertura' ? response.mapa_payload_cobertura : response.mapa_payload_pontos)
|| null
setMapaHtmls({
pontos: modoExibicaoSolicitado === 'pontos' ? mapaHtmlSolicitado : '',
cobertura: modoExibicaoSolicitado === 'cobertura' ? mapaHtmlSolicitado : '',
})
setMapaPayloads({
pontos: modoExibicaoSolicitado === 'pontos' ? mapaPayloadSolicitado : null,
cobertura: modoExibicaoSolicitado === 'cobertura' ? mapaPayloadSolicitado : null,
})
setMapaStatus(response.status || '')
mapaTrabalhosTecnicosConfigRef.current = buildMapaTrabalhosTecnicosConfigKey(trabalhosTecnicosConfig)
} catch (err) {
setMapaError(err.message)
} finally {
setMapaLoading(false)
}
}
async function buscarModelos(nextFilters = filters, nextAvaliandos = avaliandosGeolocalizados, options = {}) {
const requestId = resultRequestSeqRef.current + 1
const searchRequestId = searchRequestSeqRef.current + 1
resultRequestSeqRef.current = requestId
searchRequestSeqRef.current = searchRequestId
setSearchLoading(true)
setError('')
try {
const response = await api.pesquisarModelos(buildApiFilters(nextFilters, nextAvaliandos))
if (requestId !== resultRequestSeqRef.current) return
const modelos = response.modelos || []
const idsNovos = new Set(modelos.map((item) => item.id))
setResult({
...RESULT_INITIAL,
...response,
modelos,
sugestoes: response.sugestoes || {},
})
setSelectedIds((current) => current.filter((id) => idsNovos.has(id)))
resetMapaPesquisa()
setPesquisaInicializada(true)
setSugestoesInicializadas(true)
if (options.syncRoute !== false) {
emitPesquisaRoute(nextFilters, nextAvaliandos)
}
} catch (err) {
if (requestId !== resultRequestSeqRef.current) return
setError(err.message)
} finally {
if (searchRequestId === searchRequestSeqRef.current) {
setSearchLoading(false)
}
}
}
async function carregarContextoInicial(options = {}) {
const requestId = resultRequestSeqRef.current + 1
const contextRequestId = contextRequestSeqRef.current + 1
resultRequestSeqRef.current = requestId
contextRequestSeqRef.current = contextRequestId
setContextLoading(true)
setError('')
try {
const response = await api.pesquisarModelos({ somente_contexto: true })
if (requestId !== resultRequestSeqRef.current) return
setResult({
...RESULT_INITIAL,
...response,
modelos: [],
sugestoes: response.sugestoes || {},
})
setSelectedIds([])
resetMapaPesquisa()
setPesquisaInicializada(false)
setSugestoesInicializadas(true)
if (options.syncRoute) {
emitPesquisaRoute(options.filters || getPesquisaFilterDefaults(), options.avaliandos || avaliandosGeolocalizados)
}
} catch (err) {
if (requestId !== resultRequestSeqRef.current) return
setError(err.message)
setSugestoesInicializadas(true)
} finally {
if (contextRequestId === contextRequestSeqRef.current) {
setContextLoading(false)
}
}
}
async function carregarSugestoesLogradouro() {
if (logradouroOptionsLoading || logradouroOptionsLoaded) return
setLogradouroOptionsLoading(true)
try {
const response = await api.pesquisarLogradourosEixos()
const opcoes = Array.isArray(response?.logradouros_eixos)
? response.logradouros_eixos.map(normalizeLogradouroOption).filter(Boolean)
: []
setLogradouroOptions(opcoes)
setLogradouroOptionsLoaded(true)
} catch (_err) {
setLogradouroOptions([])
} finally {
setLogradouroOptionsLoading(false)
}
}
useEffect(() => {
const requestKey = String(routeRequest?.requestKey || '').trim()
if (requestKey) return undefined
void carregarContextoInicial()
return undefined
}, [routeRequest])
useEffect(() => {
const requestKey = String(routeRequest?.requestKey || '').trim()
if (!requestKey) return
if (routeRequestHandledRef.current === requestKey) return
routeRequestHandledRef.current = requestKey
const nextFilters = {
...getPesquisaFilterDefaults(),
...(routeRequest?.filters || {}),
}
const rawAvaliandos = Array.isArray(routeRequest?.avaliandos) ? routeRequest.avaliandos : []
let nextEntries = rawAvaliandos
.map((item, index) => {
const lat = parseCoordinateValue(item?.lat)
const lon = parseCoordinateValue(item?.lon)
if (lat === null || lon === null) return null
return createLocalizacaoEntry({
lat,
lon,
origem: String(item?.origem || 'coords'),
logradouro: String(item?.logradouro || ''),
numero_usado: String(item?.numero_usado || ''),
cdlog: item?.cdlog ?? null,
}, String(item?.id || `avaliando-${localizacaoIdCounterRef.current + index}`))
})
.filter(Boolean)
const lat = parseCoordinateValue(routeRequest?.avaliando?.lat)
const lon = parseCoordinateValue(routeRequest?.avaliando?.lon)
const possuiAvaliando = lat !== null && lon !== null
if (!nextEntries.length && possuiAvaliando) {
nextEntries = [createLocalizacaoEntry({
lat,
lon,
origem: 'coords',
logradouro: '',
numero_usado: '',
cdlog: null,
}, `avaliando-${localizacaoIdCounterRef.current}`)]
localizacaoIdCounterRef.current += 1
} else if (nextEntries.length) {
localizacaoIdCounterRef.current += nextEntries.length
}
setFilters(nextFilters)
setAvaliandosGeolocalizados(nextEntries)
setLocalizacaoModo(possuiAvaliando ? 'coords' : 'endereco')
setLocalizacaoInputs(EMPTY_LOCATION_INPUTS)
setLocalizacaoError('')
setLocalizacaoStatus('')
resetMapaPesquisa()
if (routeRequest?.pesquisaExecutada || hasPesquisaRoutePayload(nextFilters, routeRequest?.avaliando) || nextEntries.length > 1) {
void buscarModelos(nextFilters, nextEntries, { syncRoute: false })
return
}
void carregarContextoInicial({ syncRoute: false, filters: nextFilters, avaliandos: nextEntries })
}, [routeRequest])
useEffect(() => {
if (!selectAllRef.current) return
selectAllRef.current.indeterminate = algunsSelecionados && !todosSelecionados
}, [algunsSelecionados, todosSelecionados])
useEffect(() => {
const requestKey = String(scrollToMapaRequest?.requestKey || '').trim()
if (!requestKey) return
if (scrollMapaHandledRef.current === requestKey) return
scrollMapaHandledRef.current = requestKey
window.requestAnimationFrame(() => {
window.requestAnimationFrame(() => {
scrollToElementTop(sectionMapaRef.current, 'smooth', 0)
})
})
}, [scrollToMapaRequest])
useEffect(() => {
if (!mapaFoiGerado || !localizacaoAtiva || mapaLoading) return undefined
const trabalhosTecnicosConfig = getMapaTrabalhosTecnicosRequestConfig()
const nextKey = buildMapaTrabalhosTecnicosConfigKey(trabalhosTecnicosConfig)
if (nextKey === mapaTrabalhosTecnicosConfigRef.current) return undefined
const timeoutId = window.setTimeout(() => {
void carregarMapaPesquisa(selectedIds)
}, 220)
return () => {
window.clearTimeout(timeoutId)
}
}, [
localizacaoAtiva,
localizacaoMultipla,
mapaFoiGerado,
mapaLoading,
mapaTrabalhosTecnicosModelosModo,
mapaTrabalhosTecnicosProximidadeModo,
mapaTrabalhosTecnicosRaio,
avaliandosGeoPayload.length,
criterioEspacial,
])
function onFieldChange(event) {
const { value, checked, type, dataset, name } = event.target
const field = dataset.field || name
if (!field) return
setFilters((prev) => ({ ...prev, [field]: type === 'checkbox' ? checked : value }))
}
function onLocalizacaoFieldChange(event) {
const { value, dataset, name } = event.target
const field = dataset.field || name
if (!field) return
atualizarCampoLocalizacao(field, value)
}
function atualizarCampoLocalizacao(field, value) {
if (!field) return
setLocalizacaoInputs((prev) => ({ ...prev, [field]: value }))
setLocalizacaoError('')
}
async function onLimparFiltros() {
const nextFilters = getPesquisaFilterDefaults()
setFilters(nextFilters)
await carregarContextoInicial({
syncRoute: true,
filters: nextFilters,
avaliandos: avaliandosGeolocalizados,
})
}
function onToggleSelecionado(modelId) {
setSelectedIds((current) => {
if (current.includes(modelId)) {
return current.filter((id) => id !== modelId)
}
return [...current, modelId]
})
}
function onToggleSelecionarTodos() {
setSelectedIds((current) => {
if (todosSelecionados) {
const idsAtuais = new Set(resultIds)
return current.filter((id) => !idsAtuais.has(id))
}
const next = new Set(current)
resultIds.forEach((id) => next.add(id))
return Array.from(next)
})
}
function onUsarEmAvaliacao(modelo) {
if (!sessionId) {
setError('Sessao indisponivel no momento. Aguarde e tente novamente.')
return
}
if (typeof onUsarModeloEmAvaliacao === 'function') {
onUsarModeloEmAvaliacao(modelo)
}
}
function limparConteudoModeloAberto() {
modeloAbertoPendingRequestsRef.current = {}
setModeloAbertoDados(null)
setModeloAbertoEstatisticas(null)
setModeloAbertoEscalasHtml('')
setModeloAbertoDadosTransformados(null)
setModeloAbertoResumoHtml('')
setModeloAbertoEquacoes(null)
setModeloAbertoCoeficientes(null)
setModeloAbertoObsCalc(null)
setModeloAbertoMapaHtml('')
setModeloAbertoMapaPayload(null)
setModeloAbertoMapaChoices(['Visualização Padrão'])
setModeloAbertoMapaVar('Visualização Padrão')
setModeloAbertoTrabalhosTecnicosModelosModo(MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
setModeloAbertoTrabalhosTecnicos([])
setModeloAbertoPlotObsCalc(null)
setModeloAbertoPlotResiduos(null)
setModeloAbertoPlotHistograma(null)
setModeloAbertoPlotCook(null)
setModeloAbertoPlotCorr(null)
setModeloAbertoLoadedTabs({})
setModeloAbertoLoadingTabs({})
}
function aplicarSecaoModeloAberto(secao, resp) {
const key = String(secao || '').trim()
if (key === 'dados_mercado') {
setModeloAbertoDados(resp?.dados || null)
return
}
if (key === 'metricas') {
setModeloAbertoEstatisticas(resp?.estatisticas || null)
return
}
if (key === 'transformacoes') {
setModeloAbertoEscalasHtml(resp?.escalas_html || '')
setModeloAbertoDadosTransformados(resp?.dados_transformados || null)
return
}
if (key === 'resumo') {
setModeloAbertoResumoHtml(resp?.resumo_html || '')
setModeloAbertoEquacoes(resp?.equacoes || null)
return
}
if (key === 'coeficientes') {
setModeloAbertoCoeficientes(resp?.coeficientes || null)
return
}
if (key === 'obs_calc') {
setModeloAbertoObsCalc(resp?.obs_calc || null)
return
}
if (key === 'graficos') {
setModeloAbertoPlotObsCalc(resp?.grafico_obs_calc || null)
setModeloAbertoPlotResiduos(resp?.grafico_residuos || null)
setModeloAbertoPlotHistograma(resp?.grafico_histograma || null)
setModeloAbertoPlotCook(resp?.grafico_cook || null)
setModeloAbertoPlotCorr(resp?.grafico_correlacao || null)
return
}
if (key === 'trabalhos_tecnicos') {
setModeloAbertoTrabalhosTecnicos(Array.isArray(resp?.trabalhos_tecnicos) ? resp.trabalhos_tecnicos : [])
setModeloAbertoTrabalhosTecnicosModelosModo(resp?.trabalhos_tecnicos_modelos_modo || MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
return
}
if (key === 'mapa') {
const nextChoices = Array.isArray(resp?.mapa_choices) && resp.mapa_choices.length
? resp.mapa_choices
: ['Visualização Padrão']
setModeloAbertoMapaHtml(resp?.mapa_html || '')
setModeloAbertoMapaPayload(resp?.mapa_payload || null)
setModeloAbertoMapaChoices(nextChoices)
setModeloAbertoMapaVar((current) => (nextChoices.includes(current) ? current : 'Visualização Padrão'))
setModeloAbertoTrabalhosTecnicos(Array.isArray(resp?.trabalhos_tecnicos) ? resp.trabalhos_tecnicos : [])
setModeloAbertoTrabalhosTecnicosModelosModo(resp?.trabalhos_tecnicos_modelos_modo || MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO)
}
}
async function garantirSecaoModeloAberto(secao, options = {}) {
const secaoNormalizada = String(secao || '').trim()
if (!sessionId || !secaoNormalizada) return
if (!options.force && modeloAbertoLoadedTabs[secaoNormalizada]) return
if (modeloAbertoPendingRequestsRef.current[secaoNormalizada]) {
await modeloAbertoPendingRequestsRef.current[secaoNormalizada]
return
}
const expectedVersion = options.expectedVersion ?? modeloAbertoOpenVersionRef.current
const trabalhosTecnicosModo = options.trabalhosTecnicosModelosModo || modeloAbertoTrabalhosTecnicosModelosModo
setModeloAbertoLoadingTabs((prev) => ({ ...prev, [secaoNormalizada]: true }))
const request = (async () => {
try {
const resp = await api.visualizacaoSection(sessionId, secaoNormalizada, trabalhosTecnicosModo)
if (modeloAbertoOpenVersionRef.current !== expectedVersion) return
aplicarSecaoModeloAberto(secaoNormalizada, resp)
setModeloAbertoLoadedTabs((prev) => ({
...prev,
[secaoNormalizada]: true,
...(secaoNormalizada === 'mapa' ? { trabalhos_tecnicos: true } : {}),
}))
} catch (err) {
if (modeloAbertoOpenVersionRef.current !== expectedVersion) return
setModeloAbertoError(err.message || 'Falha ao abrir modelo.')
} finally {
if (modeloAbertoOpenVersionRef.current !== expectedVersion) return
setModeloAbertoLoadingTabs((prev) => ({ ...prev, [secaoNormalizada]: false }))
}
})()
modeloAbertoPendingRequestsRef.current[secaoNormalizada] = request
try {
await request
} finally {
if (modeloAbertoPendingRequestsRef.current[secaoNormalizada] === request) {
delete modeloAbertoPendingRequestsRef.current[secaoNormalizada]
}
}
}
async function onAbrirModelo(modelo) {
if (typeof onAbrirModeloNoRepositorio === 'function') {
onAbrirModeloNoRepositorio({
...modelo,
returnIntent: buildPesquisaReturnIntent(),
})
return
}
if (!sessionId) {
setError('Sessao indisponivel no momento. Aguarde e tente novamente.')
return
}
modeloAbertoOpenVersionRef.current += 1
const openVersion = modeloAbertoOpenVersionRef.current
limparConteudoModeloAberto()
setModeloAbertoLoading(true)
setModeloAbertoError('')
setModeloAbertoMeta({
id: modelo.id,
nome: modelo.nome_modelo || modelo.arquivo || modelo.id,
observacao: '',
})
try {
const resp = await api.visualizacaoRepositorioCarregar(sessionId, modelo.id)
if (modeloAbertoOpenVersionRef.current !== openVersion) return
setModeloAbertoActiveTab('mapa')
setModeloAbertoMeta({
id: modelo.id,
nome: modelo.nome_modelo || modelo.arquivo || modelo.id,
observacao: String(resp?.observacao_modelo || '').trim(),
})
await garantirSecaoModeloAberto('mapa', {
force: true,
expectedVersion: openVersion,
trabalhosTecnicosModelosModo: MODELO_ABERTO_TRABALHOS_TECNICOS_PADRAO,
})
window.requestAnimationFrame(() => {
scrollParaTopoDaPagina()
})
} catch (err) {
setModeloAbertoError(err.message || 'Falha ao abrir modelo.')
} finally {
setModeloAbertoLoading(false)
}
}
function onVoltarPesquisa() {
modeloAbertoOpenVersionRef.current += 1
setModeloAbertoMeta(null)
setModeloAbertoError('')
setModeloAbertoActiveTab('mapa')
setModeloAbertoLoading(false)
limparConteudoModeloAberto()
scrollParaResultadosNoTopo()
}
async function atualizarMapaModeloAberto(
nextVar = modeloAbertoMapaVar,
nextTrabalhosTecnicosModo = modeloAbertoTrabalhosTecnicosModelosModo,
) {
if (!sessionId) return
setModeloAbertoLoadingTabs((prev) => ({ ...prev, mapa: true }))
try {
const resp = await api.updateVisualizacaoMap(sessionId, nextVar, nextTrabalhosTecnicosModo)
setModeloAbertoMapaHtml(resp?.mapa_html || '')
setModeloAbertoMapaPayload(resp?.mapa_payload || null)
setModeloAbertoTrabalhosTecnicos(Array.isArray(resp?.trabalhos_tecnicos) ? resp.trabalhos_tecnicos : [])
setModeloAbertoTrabalhosTecnicosModelosModo(resp?.trabalhos_tecnicos_modelos_modo || nextTrabalhosTecnicosModo)
setModeloAbertoLoadedTabs((prev) => ({ ...prev, mapa: true, trabalhos_tecnicos: true }))
} finally {
setModeloAbertoLoadingTabs((prev) => ({ ...prev, mapa: false }))
}
}
async function onModeloAbertoMapChange(nextVar) {
setModeloAbertoMapaVar(nextVar)
try {
await atualizarMapaModeloAberto(nextVar, modeloAbertoTrabalhosTecnicosModelosModo)
} catch (err) {
setModeloAbertoError(err.message || 'Falha ao atualizar mapa do modelo.')
}
}
async function onModeloAbertoTrabalhosTecnicosModeChange(nextMode) {
setModeloAbertoTrabalhosTecnicosModelosModo(nextMode)
try {
await atualizarMapaModeloAberto(modeloAbertoMapaVar, nextMode)
} catch (err) {
setModeloAbertoError(err.message || 'Falha ao atualizar mapa do modelo.')
}
}
async function onDownloadEquacaoModeloAberto(mode) {
if (!sessionId || !mode) return
setModeloAbertoLoading(true)
try {
const blob = await api.exportEquationViz(sessionId, mode)
const sufixo = String(mode) === 'excel_sab' ? 'estilo_sab' : 'excel'
downloadBlob(blob, `equacao_modelo_${sufixo}.xlsx`)
} catch (err) {
setModeloAbertoError(err.message || 'Falha ao exportar equação.')
} finally {
setModeloAbertoLoading(false)
}
}
function onModeloAbertoTabSelect(nextTab) {
setModeloAbertoActiveTab(nextTab)
void garantirSecaoModeloAberto(nextTab)
}
async function onGerarMapaSelecionados() {
if (!selectedIds.length) {
setMapaError('Selecione ao menos um modelo para plotar no mapa.')
return
}
await carregarMapaPesquisa(selectedIds)
}
function onMapaModoExibicaoChange(event) {
const nextModo = String(event?.target?.value || 'pontos')
setMapaModoExibicao(nextModo)
if (!mapaFoiGerado || mapaLoading || !selectedIds.length || mapaHtmls[nextModo] || mapaPayloads[nextModo]) return
void carregarMapaPesquisa(selectedIds, { modoExibicao: nextModo })
}
async function onAdminConfigSalva() {
if (pesquisaInicializada) {
await buscarModelos(filters, avaliandosGeolocalizados)
return
}
await carregarContextoInicial()
}
function onLimparFormularioLocalizacao() {
setLocalizacaoInputs(EMPTY_LOCATION_INPUTS)
setLocalizacaoModo('endereco')
setLocalizacaoError('')
setLocalizacaoStatus('')
}
async function atualizarPesquisaAposGeolocalizacao(nextEntries) {
resetMapaPesquisa()
if (pesquisaInicializada) {
await buscarModelos(filters, nextEntries)
}
}
async function onResolverLocalizacao() {
setLocalizacaoError('')
setLocalizacaoStatus('')
if (localizacaoModo === 'coords') {
const lat = Number(localizacaoInputs.latitude)
const lon = Number(localizacaoInputs.longitude)
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
setLocalizacaoError('Informe latitude e longitude válidas para localizar o avaliando.')
return
}
} else {
const logradouro = String(localizacaoInputs.logradouro || '').trim()
const cdlogTexto = String(localizacaoInputs.cdlog || '').trim()
const cdlog = cdlogTexto ? Number(cdlogTexto) : null
if (!logradouro && !(Number.isFinite(cdlog) && cdlog > 0)) {
setLocalizacaoError('Informe o logradouro ou um CDLOG válido para localizar o avaliando.')
return
}
if (cdlogTexto && !(Number.isFinite(cdlog) && cdlog > 0)) {
setLocalizacaoError('Informe um CDLOG válido para localizar o avaliando.')
return
}
const numero = Number(localizacaoInputs.numero)
if (!Number.isFinite(numero) || numero <= 0) {
setLocalizacaoError('Informe um número válido para localizar o avaliando.')
return
}
}
setLocalizacaoLoading(true)
try {
const response = await api.pesquisarLocalizacaoAvaliando({
latitude: localizacaoModo === 'coords' ? Number(localizacaoInputs.latitude) : null,
longitude: localizacaoModo === 'coords' ? Number(localizacaoInputs.longitude) : null,
logradouro: localizacaoModo === 'endereco' ? String(localizacaoInputs.logradouro || '').trim() : null,
numero: localizacaoModo === 'endereco' ? Number(localizacaoInputs.numero) : null,
cdlog: localizacaoModo === 'endereco' && String(localizacaoInputs.cdlog || '').trim()
? Number(localizacaoInputs.cdlog)
: null,
})
const resolvida = {
...response,
lat: parseCoordinateValue(response?.lat),
lon: parseCoordinateValue(response?.lon),
}
const nextEntry = createLocalizacaoEntry(
resolvida,
`avaliando-${localizacaoIdCounterRef.current}`,
)
localizacaoIdCounterRef.current += 1
const nextEntries = [...avaliandosGeolocalizados, nextEntry]
setAvaliandosGeolocalizados(nextEntries)
setLocalizacaoInputs(EMPTY_LOCATION_INPUTS)
setLocalizacaoStatus(
nextEntries.length > 1
? `${nextEntries.length} avaliandos geolocalizados com sucesso.`
: (response?.status || 'Geolocalização do avaliando registrada.')
)
await atualizarPesquisaAposGeolocalizacao(nextEntries)
} catch (err) {
setLocalizacaoError(err.message || 'Falha ao localizar o avaliando.')
} finally {
setLocalizacaoLoading(false)
}
}
async function onRemoverAvaliandoLocalizacao(id) {
const nextEntries = avaliandosGeolocalizados.filter((item) => item.id !== id)
setAvaliandosGeolocalizados(nextEntries)
setLocalizacaoError('')
setLocalizacaoStatus('')
await atualizarPesquisaAposGeolocalizacao(nextEntries)
}
if (modoModeloAberto) {
return (
{modeloAbertoMeta?.nome || 'Modelo'}
{PESQUISA_INNER_TABS.map((tab) => (
))}
{modeloAbertoActiveTab === 'mapa' ? (
!modeloAbertoLoadedTabs.mapa ? (
Carregando mapa do modelo...
) : (
<>
>
)
) : null}
{modeloAbertoActiveTab === 'trabalhos_tecnicos' ? (
modeloAbertoLoadedTabs.trabalhos_tecnicos
?
:
Carregando trabalhos técnicos do modelo...
) : null}
{modeloAbertoActiveTab === 'dados_mercado'
? (modeloAbertoLoadedTabs.dados_mercado ?
:
Carregando dados de mercado...
)
: null}
{modeloAbertoActiveTab === 'metricas'
? (modeloAbertoLoadedTabs.metricas ?
:
Carregando métricas do modelo...
)
: null}
{modeloAbertoActiveTab === 'transformacoes' ? (
modeloAbertoLoadedTabs.transformacoes ? (
<>
Dados com variáveis transformadas
>
) : (
Carregando transformações do modelo...
)
) : null}
{modeloAbertoActiveTab === 'resumo' ? (
modeloAbertoLoadedTabs.resumo ? (
<>
Equações do Modelo
void onDownloadEquacaoModeloAberto(mode)}
disabled={modeloAbertoLoading}
/>
>
) : (
Carregando resumo do modelo...
)
) : null}
{modeloAbertoActiveTab === 'coeficientes'
? (modeloAbertoLoadedTabs.coeficientes ?
:
Carregando coeficientes do modelo...
)
: null}
{modeloAbertoActiveTab === 'obs_calc'
? (modeloAbertoLoadedTabs.obs_calc ?
:
Carregando observados x calculados...
)
: null}
{modeloAbertoActiveTab === 'graficos' ? (
modeloAbertoLoadedTabs.graficos ? (
<>
>
) : (
Carregando gráficos do modelo...
)
) : null}
{modeloAbertoError ?
{modeloAbertoError}
: null}
)
}
return (
{avaliandosGeolocalizados.length ? (
{avaliandosGeolocalizados.map((item, index) => (
void onRemoverAvaliandoLocalizacao(item.id)}
disabled={localizacaoLoading || isPesquisaBusy}
>
Excluir avaliando
)}
/>
))}
) : null}
{localizacaoModo === 'coords' ? (
) : (
)}
{localizacaoStatus && !localizacaoAtiva ?
{localizacaoStatus}
: null}
{localizacaoError ?
{localizacaoError}
: null}
setMostrarAdminConfig((current) => !current)}
>
⚙
Configure as fontes dos campos
)}
>
{mostrarAdminConfig ? (
void onAdminConfigSalva()} />
) : null}
{error ? {error}
: null}
{formatCount(result.total_filtrado)}{' '}
modelo(s) aceito(s) de {formatCount(result.total_geral)}.
{localizacaoMultipla ? (
) : null}
{resultIds.length ? (
) : null}
{!modelosOrdenados.length ? (
{!pesquisaInicializada
? 'Defina os filtros desejados e clique em Pesquisar.'
: 'Nenhum modelo aceitou os parametros do avaliando informado.'}
) : (
{modelosOrdenados.map((modelo) => {
const selecionado = selectedIds.includes(modelo.id)
const faixaDataRecency = getFaixaDataRecencyInfo(modelo.faixa_data)
const finalidadesText = uppercaseListText(modelo.finalidades || [])
const bairrosText = uppercaseListText(modelo.bairros || [])
const variaveisText = buildVariablesDisplay(modelo)
const observacaoText = String(modelo.observacao_modelo || '').trim()
const resumoEspacial = modelo?.distancia_resumo || null
const coberturaEspacial = formatResumoEspacialCobertura(resumoEspacial)
const cardClassName = [
'pesquisa-card',
selecionado ? 'is-selected' : '',
].filter(Boolean).join(' ')
return (
{modelo.nome_modelo || modelo.arquivo}
{localizacaoMultipla ? (
<>
Resumo espacial: {formatResumoEspacial(resumoEspacial)}
Distâncias por avaliando: {formatDistanciasPorAvaliando(modelo?.distancias_avaliandos || [])}
{coberturaEspacial ?
Cobertura espacial: {coberturaEspacial}
: null}
>
) : (
Distância: {String(modelo.distancia_label || '').trim() || formatDistanceKm(modelo.distancia_km)}
)}
Tipo: {formatTipoImovel(modelo)}
Autor: {modelo.autor || '-'}
Dados: {formatCount(modelo.total_dados)}
Avaliandos: {formatCount(modelo.total_trabalhos)}
Faixa area: {formatRange(modelo.faixa_area)}
Faixa RH: {formatRange(modelo.faixa_rh)}
Faixa data: {formatRange(modelo.faixa_data)}
{faixaDataRecency.label ? (
{faixaDataRecency.label}
) : null}
{modelo.status !== 'ok' ? {modelo.erro_leitura || 'Falha ao ler modelo.'}
: null}
)
})}
)}
{formatCount(selectedIds.length)} modelo(s) selecionado(s) para plotagem.
{localizacaoMultipla ? 'Avaliandos geolocalizados:' : 'Localização do avaliando:'}{' '}
{localizacaoAtiva ? (localizacaoMultipla ? formatCount(avaliandosGeoPayload.length) : 'ativa') : 'não definida'}
{mapaError ? {mapaError}
: null}
{mapaHtmlAtual ? (
{localizacaoAtiva ? (
) : null}
{localizacaoAtiva && mapaTrabalhosTecnicosProximidadeModo === 'proximos_ao_avaliando' ? (
) : null}
) : null}
{(mapaHtmlAtual || mapaPayloadAtual)
?
: Nenhum mapa gerado ainda.
}
)
}