Spaces:
Running
Running
Guilherme Silberfarb Costa commited on
Commit ·
d9d8daf
1
Parent(s): 8bd0e6d
validades dos modelos
Browse files
backend/app/services/pesquisa_service.py
CHANGED
|
@@ -16,7 +16,7 @@ import pandas as pd
|
|
| 16 |
from fastapi import HTTPException
|
| 17 |
from joblib import load
|
| 18 |
|
| 19 |
-
from app.core.elaboracao.core import _migrar_pacote_v1_para_v2
|
| 20 |
from app.core.map_layers import add_bairros_layer, add_indice_marker, add_zoom_responsive_circle_markers
|
| 21 |
from app.services import model_repository
|
| 22 |
from app.services.serializers import sanitize_value
|
|
@@ -563,6 +563,7 @@ def _construir_resumo_modelo(caminho_modelo: Path) -> dict[str, Any]:
|
|
| 563 |
"total_trabalhos": None,
|
| 564 |
"endereco_referencia": None,
|
| 565 |
"equacao": None,
|
|
|
|
| 566 |
"r2": None,
|
| 567 |
"tem_app": False,
|
| 568 |
"mapa_disponivel": False,
|
|
@@ -608,6 +609,9 @@ def _construir_resumo_modelo(caminho_modelo: Path) -> dict[str, Any]:
|
|
| 608 |
if resumo["equacao"] is None:
|
| 609 |
resumo["equacao"] = _equacao_do_pacote(pacote)
|
| 610 |
|
|
|
|
|
|
|
|
|
|
| 611 |
if resumo["r2"] is None:
|
| 612 |
resumo["r2"] = _r2_do_pacote(pacote)
|
| 613 |
|
|
|
|
| 16 |
from fastapi import HTTPException
|
| 17 |
from joblib import load
|
| 18 |
|
| 19 |
+
from app.core.elaboracao.core import _migrar_pacote_v1_para_v2, normalizar_observacao_modelo
|
| 20 |
from app.core.map_layers import add_bairros_layer, add_indice_marker, add_zoom_responsive_circle_markers
|
| 21 |
from app.services import model_repository
|
| 22 |
from app.services.serializers import sanitize_value
|
|
|
|
| 563 |
"total_trabalhos": None,
|
| 564 |
"endereco_referencia": None,
|
| 565 |
"equacao": None,
|
| 566 |
+
"observacao_modelo": None,
|
| 567 |
"r2": None,
|
| 568 |
"tem_app": False,
|
| 569 |
"mapa_disponivel": False,
|
|
|
|
| 609 |
if resumo["equacao"] is None:
|
| 610 |
resumo["equacao"] = _equacao_do_pacote(pacote)
|
| 611 |
|
| 612 |
+
if resumo["observacao_modelo"] is None:
|
| 613 |
+
resumo["observacao_modelo"] = normalizar_observacao_modelo(pacote.get("observacao_modelo"))
|
| 614 |
+
|
| 615 |
if resumo["r2"] is None:
|
| 616 |
resumo["r2"] = _r2_do_pacote(pacote)
|
| 617 |
|
frontend/src/components/PesquisaTab.jsx
CHANGED
|
@@ -8,6 +8,7 @@ import MapFrame from './MapFrame'
|
|
| 8 |
import PlotFigure from './PlotFigure'
|
| 9 |
import PesquisaAdminConfigPanel from './PesquisaAdminConfigPanel'
|
| 10 |
import SectionBlock from './SectionBlock'
|
|
|
|
| 11 |
|
| 12 |
const EMPTY_FILTERS = {
|
| 13 |
nomeModelo: '',
|
|
@@ -192,16 +193,33 @@ function CompactHoverList({
|
|
| 192 |
modalContent = null,
|
| 193 |
}) {
|
| 194 |
const rootRef = useRef(null)
|
|
|
|
| 195 |
const panelRef = useRef(null)
|
| 196 |
const [previewOpen, setPreviewOpen] = useState(false)
|
| 197 |
const [modalOpen, setModalOpen] = useState(false)
|
| 198 |
-
const [
|
| 199 |
-
const [previewVertical, setPreviewVertical] = useState('down')
|
| 200 |
-
const [previewMaxHeight, setPreviewMaxHeight] = useState(160)
|
| 201 |
-
const [previewMaxWidth, setPreviewMaxWidth] = useState(560)
|
| 202 |
const inlinePreviewText = String(previewText || '').trim()
|
| 203 |
const inlineModalText = String(modalText || '').trim()
|
| 204 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
function updatePreviewLayout() {
|
| 206 |
const bounds = rootRef.current?.getBoundingClientRect()
|
| 207 |
const panel = panelRef.current
|
|
@@ -209,45 +227,89 @@ function CompactHoverList({
|
|
| 209 |
|
| 210 |
const viewportWidth = window.innerWidth
|
| 211 |
const viewportHeight = window.innerHeight
|
| 212 |
-
const gap =
|
| 213 |
const padding = 16
|
| 214 |
const triggerCenter = bounds.left + (bounds.width / 2)
|
| 215 |
-
const
|
| 216 |
-
const
|
| 217 |
-
const
|
| 218 |
-
|
| 219 |
-
const
|
| 220 |
-
const
|
| 221 |
-
|
| 222 |
-
const availableLeft = Math.max(120, bounds.right - padding)
|
| 223 |
-
const availableRight = Math.max(120, viewportWidth - bounds.left - padding)
|
| 224 |
-
const availableAbove = Math.max(96, bounds.top - gap - padding)
|
| 225 |
-
const availableBelow = Math.max(96, viewportHeight - bounds.bottom - gap - padding)
|
| 226 |
|
| 227 |
const desiredWidth = Math.min(panel.scrollWidth || 560, 560)
|
| 228 |
-
const desiredHeight = Math.min(panel.scrollHeight ||
|
| 229 |
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
} else if (preferredSide === 'right' && desiredWidth > availableRight && availableLeft > availableRight) {
|
| 234 |
-
nextSide = 'left'
|
| 235 |
}
|
| 236 |
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
}
|
| 243 |
|
| 244 |
-
|
| 245 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
}
|
| 252 |
|
| 253 |
useEffect(() => {
|
|
@@ -268,6 +330,10 @@ function CompactHoverList({
|
|
| 268 |
}
|
| 269 |
}, [previewOpen])
|
| 270 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
useEffect(() => {
|
| 272 |
if (!modalOpen) return undefined
|
| 273 |
|
|
@@ -296,15 +362,9 @@ function CompactHoverList({
|
|
| 296 |
<span
|
| 297 |
ref={rootRef}
|
| 298 |
className={`pesquisa-card-popover-wrap${previewOpen ? ' is-open' : ''}`}
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
setPreviewOpen(true)
|
| 303 |
-
}}
|
| 304 |
-
onMouseLeave={() => setPreviewOpen(false)}
|
| 305 |
-
onFocus={() => {
|
| 306 |
-
setPreviewOpen(true)
|
| 307 |
-
}}
|
| 308 |
onBlur={(event) => {
|
| 309 |
if (!rootRef.current?.contains(event.relatedTarget)) {
|
| 310 |
setPreviewOpen(false)
|
|
@@ -324,19 +384,23 @@ function CompactHoverList({
|
|
| 324 |
>
|
| 325 |
{buttonLabel}
|
| 326 |
</button>
|
| 327 |
-
<span
|
| 328 |
-
ref={panelRef}
|
| 329 |
-
className="pesquisa-card-popover-panel"
|
| 330 |
-
role="tooltip"
|
| 331 |
-
aria-label={label}
|
| 332 |
-
style={{
|
| 333 |
-
maxWidth: `${Math.max(120, previewMaxWidth)}px`,
|
| 334 |
-
maxHeight: `${Math.max(96, previewMaxHeight)}px`,
|
| 335 |
-
}}
|
| 336 |
-
>
|
| 337 |
-
<span className="pesquisa-card-popover-preview">{previewContent || inlinePreviewText || inlineModalText}</span>
|
| 338 |
-
</span>
|
| 339 |
</span>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
{modalOpen && typeof document !== 'undefined'
|
| 341 |
? createPortal(
|
| 342 |
<div
|
|
@@ -1274,13 +1338,21 @@ export default function PesquisaTab({ sessionId, onUsarModeloEmAvaliacao = null
|
|
| 1274 |
<div className="pesquisa-card-grid">
|
| 1275 |
{result.modelos.map((modelo) => {
|
| 1276 |
const selecionado = selectedIds.includes(modelo.id)
|
|
|
|
| 1277 |
const finalidadesText = uppercaseListText(modelo.finalidades || [])
|
| 1278 |
const bairrosText = uppercaseListText(modelo.bairros || [])
|
| 1279 |
const variaveisText = buildVariablesDisplay(modelo)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1280 |
return (
|
| 1281 |
-
<article key={modelo.id} className={
|
| 1282 |
<div className="pesquisa-card-top">
|
| 1283 |
<h4 className="pesquisa-card-title">{modelo.nome_modelo || modelo.arquivo}</h4>
|
|
|
|
| 1284 |
<div className="pesquisa-card-actions">
|
| 1285 |
<button
|
| 1286 |
type="button"
|
|
@@ -1303,7 +1375,14 @@ export default function PesquisaTab({ sessionId, onUsarModeloEmAvaliacao = null
|
|
| 1303 |
<div><strong>Dados:</strong> {formatCount(modelo.total_dados)}</div>
|
| 1304 |
<div><strong>Faixa area:</strong> {formatRange(modelo.faixa_area)}</div>
|
| 1305 |
<div><strong>Faixa RH:</strong> {formatRange(modelo.faixa_rh)}</div>
|
| 1306 |
-
<div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1307 |
</div>
|
| 1308 |
</div>
|
| 1309 |
<div className="pesquisa-card-meta-actions">
|
|
@@ -1327,6 +1406,12 @@ export default function PesquisaTab({ sessionId, onUsarModeloEmAvaliacao = null
|
|
| 1327 |
previewContent={buildVariablesContent(variaveisText)}
|
| 1328 |
modalContent={buildVariablesContent(variaveisText)}
|
| 1329 |
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1330 |
</div>
|
| 1331 |
</div>
|
| 1332 |
|
|
|
|
| 8 |
import PlotFigure from './PlotFigure'
|
| 9 |
import PesquisaAdminConfigPanel from './PesquisaAdminConfigPanel'
|
| 10 |
import SectionBlock from './SectionBlock'
|
| 11 |
+
import { getFaixaDataRecencyInfo } from '../modelRecency'
|
| 12 |
|
| 13 |
const EMPTY_FILTERS = {
|
| 14 |
nomeModelo: '',
|
|
|
|
| 193 |
modalContent = null,
|
| 194 |
}) {
|
| 195 |
const rootRef = useRef(null)
|
| 196 |
+
const closePreviewTimeoutRef = useRef(null)
|
| 197 |
const panelRef = useRef(null)
|
| 198 |
const [previewOpen, setPreviewOpen] = useState(false)
|
| 199 |
const [modalOpen, setModalOpen] = useState(false)
|
| 200 |
+
const [previewStyle, setPreviewStyle] = useState({})
|
|
|
|
|
|
|
|
|
|
| 201 |
const inlinePreviewText = String(previewText || '').trim()
|
| 202 |
const inlineModalText = String(modalText || '').trim()
|
| 203 |
|
| 204 |
+
function clearPreviewCloseTimeout() {
|
| 205 |
+
if (closePreviewTimeoutRef.current) {
|
| 206 |
+
window.clearTimeout(closePreviewTimeoutRef.current)
|
| 207 |
+
closePreviewTimeoutRef.current = null
|
| 208 |
+
}
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
function schedulePreviewClose() {
|
| 212 |
+
clearPreviewCloseTimeout()
|
| 213 |
+
closePreviewTimeoutRef.current = window.setTimeout(() => {
|
| 214 |
+
setPreviewOpen(false)
|
| 215 |
+
}, 80)
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
function openPreview() {
|
| 219 |
+
clearPreviewCloseTimeout()
|
| 220 |
+
setPreviewOpen(true)
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
function updatePreviewLayout() {
|
| 224 |
const bounds = rootRef.current?.getBoundingClientRect()
|
| 225 |
const panel = panelRef.current
|
|
|
|
| 227 |
|
| 228 |
const viewportWidth = window.innerWidth
|
| 229 |
const viewportHeight = window.innerHeight
|
| 230 |
+
const gap = 10
|
| 231 |
const padding = 16
|
| 232 |
const triggerCenter = bounds.left + (bounds.width / 2)
|
| 233 |
+
const towardCenterSide = triggerCenter >= (viewportWidth / 2) ? 'left' : 'right'
|
| 234 |
+
const oppositeSide = towardCenterSide === 'left' ? 'right' : 'left'
|
| 235 |
+
const downwardSpace = viewportHeight - bounds.bottom - gap - padding
|
| 236 |
+
const upwardSpace = bounds.top - gap - padding
|
| 237 |
+
const preferredVertical = downwardSpace >= upwardSpace ? 'down' : 'up'
|
| 238 |
+
const oppositeVertical = preferredVertical === 'down' ? 'up' : 'down'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
|
| 240 |
const desiredWidth = Math.min(panel.scrollWidth || 560, 560)
|
| 241 |
+
const desiredHeight = Math.min(panel.scrollHeight || 220, 420)
|
| 242 |
|
| 243 |
+
function clamp(value, min, max) {
|
| 244 |
+
if (max < min) return min
|
| 245 |
+
return Math.min(Math.max(value, min), max)
|
|
|
|
|
|
|
| 246 |
}
|
| 247 |
|
| 248 |
+
function makeVerticalCandidate(side, vertical) {
|
| 249 |
+
const maxWidth = Math.max(160, viewportWidth - (padding * 2))
|
| 250 |
+
const width = Math.min(desiredWidth, maxWidth)
|
| 251 |
+
const maxHeight = Math.max(96, vertical === 'down' ? downwardSpace : upwardSpace)
|
| 252 |
+
const height = Math.min(desiredHeight, maxHeight)
|
| 253 |
+
const leftBase = side === 'left'
|
| 254 |
+
? bounds.right - width
|
| 255 |
+
: bounds.left
|
| 256 |
+
const left = clamp(leftBase, padding, viewportWidth - padding - width)
|
| 257 |
+
const topBase = vertical === 'down'
|
| 258 |
+
? bounds.bottom + gap
|
| 259 |
+
: bounds.top - gap - height
|
| 260 |
+
const top = clamp(topBase, padding, viewportHeight - padding - height)
|
| 261 |
+
return {
|
| 262 |
+
left,
|
| 263 |
+
top,
|
| 264 |
+
maxWidth,
|
| 265 |
+
maxHeight,
|
| 266 |
+
score: (Math.min(maxWidth, desiredWidth) / desiredWidth) + (Math.min(maxHeight, desiredHeight) / desiredHeight),
|
| 267 |
+
}
|
| 268 |
}
|
| 269 |
|
| 270 |
+
function makeSideCandidate(side) {
|
| 271 |
+
const maxWidth = Math.max(180, side === 'right'
|
| 272 |
+
? viewportWidth - bounds.right - gap - padding
|
| 273 |
+
: bounds.left - gap - padding)
|
| 274 |
+
const width = Math.min(desiredWidth, maxWidth)
|
| 275 |
+
const maxHeight = Math.max(120, viewportHeight - (padding * 2))
|
| 276 |
+
const height = Math.min(desiredHeight, maxHeight)
|
| 277 |
+
const leftBase = side === 'right'
|
| 278 |
+
? bounds.right + gap
|
| 279 |
+
: bounds.left - gap - width
|
| 280 |
+
const left = clamp(leftBase, padding, viewportWidth - padding - width)
|
| 281 |
+
const top = clamp(bounds.top, padding, viewportHeight - padding - height)
|
| 282 |
+
return {
|
| 283 |
+
left,
|
| 284 |
+
top,
|
| 285 |
+
maxWidth,
|
| 286 |
+
maxHeight,
|
| 287 |
+
score: (Math.min(maxWidth, desiredWidth) / desiredWidth) + (Math.min(maxHeight, desiredHeight) / desiredHeight),
|
| 288 |
+
}
|
| 289 |
+
}
|
| 290 |
|
| 291 |
+
const candidates = [
|
| 292 |
+
makeVerticalCandidate(towardCenterSide, preferredVertical),
|
| 293 |
+
makeVerticalCandidate(towardCenterSide, oppositeVertical),
|
| 294 |
+
makeSideCandidate(towardCenterSide),
|
| 295 |
+
makeSideCandidate(oppositeSide),
|
| 296 |
+
makeVerticalCandidate(oppositeSide, preferredVertical),
|
| 297 |
+
makeVerticalCandidate(oppositeSide, oppositeVertical),
|
| 298 |
+
]
|
| 299 |
+
|
| 300 |
+
const best = candidates.reduce((current, candidate) => {
|
| 301 |
+
if (!current || candidate.score > current.score) return candidate
|
| 302 |
+
return current
|
| 303 |
+
}, null)
|
| 304 |
+
|
| 305 |
+
if (!best) return
|
| 306 |
+
|
| 307 |
+
setPreviewStyle({
|
| 308 |
+
left: `${best.left}px`,
|
| 309 |
+
top: `${best.top}px`,
|
| 310 |
+
maxWidth: `${Math.max(160, best.maxWidth)}px`,
|
| 311 |
+
maxHeight: `${Math.max(96, best.maxHeight)}px`,
|
| 312 |
+
})
|
| 313 |
}
|
| 314 |
|
| 315 |
useEffect(() => {
|
|
|
|
| 330 |
}
|
| 331 |
}, [previewOpen])
|
| 332 |
|
| 333 |
+
useEffect(() => () => {
|
| 334 |
+
clearPreviewCloseTimeout()
|
| 335 |
+
}, [])
|
| 336 |
+
|
| 337 |
useEffect(() => {
|
| 338 |
if (!modalOpen) return undefined
|
| 339 |
|
|
|
|
| 362 |
<span
|
| 363 |
ref={rootRef}
|
| 364 |
className={`pesquisa-card-popover-wrap${previewOpen ? ' is-open' : ''}`}
|
| 365 |
+
onMouseEnter={() => openPreview()}
|
| 366 |
+
onMouseLeave={() => schedulePreviewClose()}
|
| 367 |
+
onFocus={() => openPreview()}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
onBlur={(event) => {
|
| 369 |
if (!rootRef.current?.contains(event.relatedTarget)) {
|
| 370 |
setPreviewOpen(false)
|
|
|
|
| 384 |
>
|
| 385 |
{buttonLabel}
|
| 386 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
</span>
|
| 388 |
+
{previewOpen && typeof document !== 'undefined'
|
| 389 |
+
? createPortal(
|
| 390 |
+
<span
|
| 391 |
+
ref={panelRef}
|
| 392 |
+
className="pesquisa-card-popover-panel is-floating"
|
| 393 |
+
role="tooltip"
|
| 394 |
+
aria-label={label}
|
| 395 |
+
style={previewStyle}
|
| 396 |
+
onMouseEnter={() => openPreview()}
|
| 397 |
+
onMouseLeave={() => schedulePreviewClose()}
|
| 398 |
+
>
|
| 399 |
+
<span className="pesquisa-card-popover-preview">{previewContent || inlinePreviewText || inlineModalText}</span>
|
| 400 |
+
</span>,
|
| 401 |
+
document.body,
|
| 402 |
+
)
|
| 403 |
+
: null}
|
| 404 |
{modalOpen && typeof document !== 'undefined'
|
| 405 |
? createPortal(
|
| 406 |
<div
|
|
|
|
| 1338 |
<div className="pesquisa-card-grid">
|
| 1339 |
{result.modelos.map((modelo) => {
|
| 1340 |
const selecionado = selectedIds.includes(modelo.id)
|
| 1341 |
+
const faixaDataRecency = getFaixaDataRecencyInfo(modelo.faixa_data)
|
| 1342 |
const finalidadesText = uppercaseListText(modelo.finalidades || [])
|
| 1343 |
const bairrosText = uppercaseListText(modelo.bairros || [])
|
| 1344 |
const variaveisText = buildVariablesDisplay(modelo)
|
| 1345 |
+
const observacaoText = String(modelo.observacao_modelo || '').trim()
|
| 1346 |
+
const cardClassName = [
|
| 1347 |
+
'pesquisa-card',
|
| 1348 |
+
selecionado ? 'is-selected' : '',
|
| 1349 |
+
].filter(Boolean).join(' ')
|
| 1350 |
+
|
| 1351 |
return (
|
| 1352 |
+
<article key={modelo.id} className={cardClassName}>
|
| 1353 |
<div className="pesquisa-card-top">
|
| 1354 |
<h4 className="pesquisa-card-title">{modelo.nome_modelo || modelo.arquivo}</h4>
|
| 1355 |
+
<div className="pesquisa-card-divider" aria-hidden="true" />
|
| 1356 |
<div className="pesquisa-card-actions">
|
| 1357 |
<button
|
| 1358 |
type="button"
|
|
|
|
| 1375 |
<div><strong>Dados:</strong> {formatCount(modelo.total_dados)}</div>
|
| 1376 |
<div><strong>Faixa area:</strong> {formatRange(modelo.faixa_area)}</div>
|
| 1377 |
<div><strong>Faixa RH:</strong> {formatRange(modelo.faixa_rh)}</div>
|
| 1378 |
+
<div>
|
| 1379 |
+
<strong>Faixa data:</strong> {formatRange(modelo.faixa_data)}
|
| 1380 |
+
{faixaDataRecency.label ? (
|
| 1381 |
+
<span className={`pesquisa-card-faixa-data-badge is-aged-${faixaDataRecency.tone}`}>
|
| 1382 |
+
{faixaDataRecency.label}
|
| 1383 |
+
</span>
|
| 1384 |
+
) : null}
|
| 1385 |
+
</div>
|
| 1386 |
</div>
|
| 1387 |
</div>
|
| 1388 |
<div className="pesquisa-card-meta-actions">
|
|
|
|
| 1406 |
previewContent={buildVariablesContent(variaveisText)}
|
| 1407 |
modalContent={buildVariablesContent(variaveisText)}
|
| 1408 |
/>
|
| 1409 |
+
<CompactHoverList
|
| 1410 |
+
label="Observação"
|
| 1411 |
+
buttonLabel="Obs"
|
| 1412 |
+
previewText={observacaoText}
|
| 1413 |
+
modalText={observacaoText}
|
| 1414 |
+
/>
|
| 1415 |
</div>
|
| 1416 |
</div>
|
| 1417 |
|
frontend/src/components/RepositorioTab.jsx
CHANGED
|
@@ -5,6 +5,7 @@ import EquationFormatsPanel from './EquationFormatsPanel'
|
|
| 5 |
import LoadingOverlay from './LoadingOverlay'
|
| 6 |
import MapFrame from './MapFrame'
|
| 7 |
import PlotFigure from './PlotFigure'
|
|
|
|
| 8 |
|
| 9 |
const REPO_INNER_TABS = [
|
| 10 |
{ key: 'mapa', label: 'Mapa' },
|
|
@@ -395,8 +396,6 @@ export default function RepositorioTab({ authUser, sessionId }) {
|
|
| 395 |
<th>Autor</th>
|
| 396 |
<th>Período</th>
|
| 397 |
<th>Dados</th>
|
| 398 |
-
<th>APP</th>
|
| 399 |
-
<th>Status</th>
|
| 400 |
<th className="repo-col-open">Abrir</th>
|
| 401 |
{isAdmin ? <th className="repo-col-delete">Excluir</th> : null}
|
| 402 |
</tr>
|
|
@@ -405,16 +404,24 @@ export default function RepositorioTab({ authUser, sessionId }) {
|
|
| 405 |
{modelos.map((item) => {
|
| 406 |
const key = String(item.id)
|
| 407 |
const emConfirmacao = confirmDeleteId === key
|
|
|
|
| 408 |
return (
|
| 409 |
<tr key={key}>
|
| 410 |
<td>{item.nome_modelo || item.arquivo || key}</td>
|
| 411 |
<td>{item.tipo_imovel || '-'}</td>
|
| 412 |
<td>{item.finalidade || '-'}</td>
|
| 413 |
<td>{item.autor || '-'}</td>
|
| 414 |
-
<td>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
<td>{item.total_dados ?? '-'}</td>
|
| 416 |
-
<td>{item.tem_app ? 'Sim' : 'Não'}</td>
|
| 417 |
-
<td>{item.status || '-'}</td>
|
| 418 |
<td className="repo-col-open">
|
| 419 |
<button
|
| 420 |
type="button"
|
|
@@ -466,7 +473,7 @@ export default function RepositorioTab({ authUser, sessionId }) {
|
|
| 466 |
})}
|
| 467 |
{!modelos.length ? (
|
| 468 |
<tr>
|
| 469 |
-
<td colSpan={isAdmin ?
|
| 470 |
{loading ? 'Carregando modelos...' : 'Nenhum modelo encontrado no repositório.'}
|
| 471 |
</td>
|
| 472 |
</tr>
|
|
|
|
| 5 |
import LoadingOverlay from './LoadingOverlay'
|
| 6 |
import MapFrame from './MapFrame'
|
| 7 |
import PlotFigure from './PlotFigure'
|
| 8 |
+
import { getFaixaDataRecencyInfo } from '../modelRecency'
|
| 9 |
|
| 10 |
const REPO_INNER_TABS = [
|
| 11 |
{ key: 'mapa', label: 'Mapa' },
|
|
|
|
| 396 |
<th>Autor</th>
|
| 397 |
<th>Período</th>
|
| 398 |
<th>Dados</th>
|
|
|
|
|
|
|
| 399 |
<th className="repo-col-open">Abrir</th>
|
| 400 |
{isAdmin ? <th className="repo-col-delete">Excluir</th> : null}
|
| 401 |
</tr>
|
|
|
|
| 404 |
{modelos.map((item) => {
|
| 405 |
const key = String(item.id)
|
| 406 |
const emConfirmacao = confirmDeleteId === key
|
| 407 |
+
const periodoRecency = getFaixaDataRecencyInfo(item.periodo_dados)
|
| 408 |
return (
|
| 409 |
<tr key={key}>
|
| 410 |
<td>{item.nome_modelo || item.arquivo || key}</td>
|
| 411 |
<td>{item.tipo_imovel || '-'}</td>
|
| 412 |
<td>{item.finalidade || '-'}</td>
|
| 413 |
<td>{item.autor || '-'}</td>
|
| 414 |
+
<td>
|
| 415 |
+
<span className="repo-periodo-wrap">
|
| 416 |
+
<span>{item.periodo_dados?.label || '-'}</span>
|
| 417 |
+
{periodoRecency.label ? (
|
| 418 |
+
<span className={`pesquisa-card-faixa-data-badge is-aged-${periodoRecency.tone}`}>
|
| 419 |
+
{periodoRecency.label}
|
| 420 |
+
</span>
|
| 421 |
+
) : null}
|
| 422 |
+
</span>
|
| 423 |
+
</td>
|
| 424 |
<td>{item.total_dados ?? '-'}</td>
|
|
|
|
|
|
|
| 425 |
<td className="repo-col-open">
|
| 426 |
<button
|
| 427 |
type="button"
|
|
|
|
| 473 |
})}
|
| 474 |
{!modelos.length ? (
|
| 475 |
<tr>
|
| 476 |
+
<td colSpan={isAdmin ? 8 : 7}>
|
| 477 |
{loading ? 'Carregando modelos...' : 'Nenhum modelo encontrado no repositório.'}
|
| 478 |
</td>
|
| 479 |
</tr>
|
frontend/src/modelRecency.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
function parseIsoDateOnly(value) {
|
| 2 |
+
const text = String(value ?? '').trim()
|
| 3 |
+
const isoMatch = text.match(/^(\d{4})-(\d{2})-(\d{2})$/)
|
| 4 |
+
if (!isoMatch) return null
|
| 5 |
+
|
| 6 |
+
const year = Number(isoMatch[1])
|
| 7 |
+
const month = Number(isoMatch[2])
|
| 8 |
+
const day = Number(isoMatch[3])
|
| 9 |
+
const date = new Date(Date.UTC(year, month - 1, day))
|
| 10 |
+
|
| 11 |
+
if (
|
| 12 |
+
Number.isNaN(date.getTime())
|
| 13 |
+
|| date.getUTCFullYear() !== year
|
| 14 |
+
|| date.getUTCMonth() !== month - 1
|
| 15 |
+
|| date.getUTCDate() !== day
|
| 16 |
+
) {
|
| 17 |
+
return null
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
return date
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
function subtractMonthsUtc(date, months) {
|
| 24 |
+
const year = date.getUTCFullYear()
|
| 25 |
+
const month = date.getUTCMonth()
|
| 26 |
+
const day = date.getUTCDate()
|
| 27 |
+
const base = new Date(Date.UTC(year, month - months, 1))
|
| 28 |
+
const lastDayOfTargetMonth = new Date(
|
| 29 |
+
Date.UTC(base.getUTCFullYear(), base.getUTCMonth() + 1, 0),
|
| 30 |
+
).getUTCDate()
|
| 31 |
+
|
| 32 |
+
base.setUTCDate(Math.min(day, lastDayOfTargetMonth))
|
| 33 |
+
return base
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
export function getFaixaDataRecencyInfo(periodo) {
|
| 37 |
+
const dataFinal = parseIsoDateOnly(periodo?.max ?? periodo?.data_final)
|
| 38 |
+
if (!dataFinal) return { tone: '', label: '' }
|
| 39 |
+
|
| 40 |
+
const now = new Date()
|
| 41 |
+
const hojeUtc = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()))
|
| 42 |
+
if (dataFinal > hojeUtc) return { tone: '', label: '' }
|
| 43 |
+
|
| 44 |
+
if (dataFinal <= subtractMonthsUtc(hojeUtc, 24)) {
|
| 45 |
+
return { tone: '2y', label: '>= 2 anos' }
|
| 46 |
+
}
|
| 47 |
+
if (dataFinal <= subtractMonthsUtc(hojeUtc, 12)) {
|
| 48 |
+
return { tone: '1y', label: '>= 1 ano' }
|
| 49 |
+
}
|
| 50 |
+
if (dataFinal <= subtractMonthsUtc(hojeUtc, 6)) {
|
| 51 |
+
return { tone: '6m', label: '>= 6 meses' }
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
return { tone: '', label: '' }
|
| 55 |
+
}
|
frontend/src/styles.css
CHANGED
|
@@ -493,6 +493,13 @@ textarea {
|
|
| 493 |
color: #48627a;
|
| 494 |
}
|
| 495 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
.repo-col-open,
|
| 497 |
.repo-col-delete {
|
| 498 |
width: 68px;
|
|
@@ -1929,9 +1936,14 @@ button.pesquisa-coluna-remove:hover {
|
|
| 1929 |
}
|
| 1930 |
|
| 1931 |
.pesquisa-card {
|
| 1932 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1933 |
border-radius: 14px;
|
| 1934 |
-
background: linear-gradient(180deg,
|
| 1935 |
padding: 12px;
|
| 1936 |
display: flex;
|
| 1937 |
flex-direction: column;
|
|
@@ -1951,17 +1963,16 @@ button.pesquisa-coluna-remove:hover {
|
|
| 1951 |
.pesquisa-card:focus-within {
|
| 1952 |
z-index: 3;
|
| 1953 |
transform: translateY(-1px);
|
| 1954 |
-
border-color:
|
| 1955 |
box-shadow:
|
| 1956 |
0 10px 22px rgba(26, 43, 61, 0.08),
|
| 1957 |
inset 0 0 0 1px rgba(255, 255, 255, 0.82);
|
| 1958 |
}
|
| 1959 |
|
| 1960 |
.pesquisa-card.is-selected {
|
| 1961 |
-
border-color: #ffbe77;
|
| 1962 |
box-shadow:
|
| 1963 |
0 10px 24px rgba(255, 163, 63, 0.17),
|
| 1964 |
-
0 0 0
|
| 1965 |
}
|
| 1966 |
|
| 1967 |
.pesquisa-card-top {
|
|
@@ -1981,6 +1992,12 @@ button.pesquisa-coluna-remove:hover {
|
|
| 1981 |
overflow-wrap: anywhere;
|
| 1982 |
}
|
| 1983 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1984 |
.pesquisa-card-actions {
|
| 1985 |
display: grid;
|
| 1986 |
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
@@ -1990,7 +2007,7 @@ button.pesquisa-coluna-remove:hover {
|
|
| 1990 |
|
| 1991 |
.pesquisa-card-meta-actions {
|
| 1992 |
display: grid;
|
| 1993 |
-
grid-template-columns: repeat(
|
| 1994 |
gap: 8px;
|
| 1995 |
min-width: 0;
|
| 1996 |
margin-top: auto;
|
|
@@ -2058,7 +2075,7 @@ button.pesquisa-coluna-remove:hover {
|
|
| 2058 |
gap: 9px;
|
| 2059 |
min-width: 0;
|
| 2060 |
flex: 1 1 auto;
|
| 2061 |
-
border-top: 1px solid
|
| 2062 |
padding-top: 9px;
|
| 2063 |
}
|
| 2064 |
|
|
@@ -2088,6 +2105,40 @@ button.pesquisa-coluna-remove:hover {
|
|
| 2088 |
line-height: 1.34;
|
| 2089 |
}
|
| 2090 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2091 |
.pesquisa-card-popover-row {
|
| 2092 |
display: flex;
|
| 2093 |
align-items: flex-start;
|
|
@@ -2151,12 +2202,11 @@ button.pesquisa-coluna-remove:hover {
|
|
| 2151 |
}
|
| 2152 |
|
| 2153 |
.pesquisa-card-popover-panel {
|
| 2154 |
-
position:
|
| 2155 |
-
top: calc(100% + 8px);
|
| 2156 |
-
right: 0;
|
| 2157 |
display: block;
|
| 2158 |
width: min(560px, calc(100vw - 56px));
|
| 2159 |
max-height: 160px;
|
|
|
|
| 2160 |
padding: 10px 12px;
|
| 2161 |
border: 1px solid #000;
|
| 2162 |
border-radius: 10px;
|
|
@@ -2166,39 +2216,15 @@ button.pesquisa-coluna-remove:hover {
|
|
| 2166 |
opacity: 0;
|
| 2167 |
visibility: hidden;
|
| 2168 |
transform: translateY(6px);
|
| 2169 |
-
pointer-events:
|
| 2170 |
transition: opacity 0.16s ease, transform 0.16s ease, visibility 0.16s ease;
|
|
|
|
| 2171 |
}
|
| 2172 |
|
| 2173 |
-
.pesquisa-card-popover-
|
| 2174 |
-
top: auto;
|
| 2175 |
-
bottom: calc(100% + 8px);
|
| 2176 |
-
transform: translateY(-6px);
|
| 2177 |
-
}
|
| 2178 |
-
|
| 2179 |
-
.pesquisa-card-popover-wrap[data-preview-vertical='down'] .pesquisa-card-popover-panel {
|
| 2180 |
-
top: calc(100% + 8px);
|
| 2181 |
-
bottom: auto;
|
| 2182 |
-
transform: translateY(6px);
|
| 2183 |
-
}
|
| 2184 |
-
|
| 2185 |
-
.pesquisa-card-popover-wrap[data-preview-side='right'] .pesquisa-card-popover-panel {
|
| 2186 |
-
left: 0;
|
| 2187 |
-
right: auto;
|
| 2188 |
-
}
|
| 2189 |
-
|
| 2190 |
-
.pesquisa-card-popover-wrap[data-preview-side='left'] .pesquisa-card-popover-panel {
|
| 2191 |
-
right: 0;
|
| 2192 |
-
left: auto;
|
| 2193 |
-
}
|
| 2194 |
-
|
| 2195 |
-
.pesquisa-card-popover-wrap:hover .pesquisa-card-popover-panel,
|
| 2196 |
-
.pesquisa-card-popover-wrap:focus-within .pesquisa-card-popover-panel,
|
| 2197 |
-
.pesquisa-card-popover-wrap.is-open .pesquisa-card-popover-panel {
|
| 2198 |
opacity: 1;
|
| 2199 |
visibility: visible;
|
| 2200 |
transform: translateY(0);
|
| 2201 |
-
pointer-events: auto;
|
| 2202 |
}
|
| 2203 |
|
| 2204 |
.pesquisa-card-popover-preview {
|
|
|
|
| 493 |
color: #48627a;
|
| 494 |
}
|
| 495 |
|
| 496 |
+
.repo-periodo-wrap {
|
| 497 |
+
display: inline-flex;
|
| 498 |
+
align-items: center;
|
| 499 |
+
flex-wrap: wrap;
|
| 500 |
+
gap: 6px;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
.repo-col-open,
|
| 504 |
.repo-col-delete {
|
| 505 |
width: 68px;
|
|
|
|
| 1936 |
}
|
| 1937 |
|
| 1938 |
.pesquisa-card {
|
| 1939 |
+
--pesquisa-card-bg-start: #ffffff;
|
| 1940 |
+
--pesquisa-card-bg-end: #fcfdff;
|
| 1941 |
+
--pesquisa-card-border: #dbe7f2;
|
| 1942 |
+
--pesquisa-card-border-hover: #c8dced;
|
| 1943 |
+
--pesquisa-card-divider: #e7eef5;
|
| 1944 |
+
border: 1px solid var(--pesquisa-card-border);
|
| 1945 |
border-radius: 14px;
|
| 1946 |
+
background: linear-gradient(180deg, var(--pesquisa-card-bg-start) 0%, var(--pesquisa-card-bg-end) 100%);
|
| 1947 |
padding: 12px;
|
| 1948 |
display: flex;
|
| 1949 |
flex-direction: column;
|
|
|
|
| 1963 |
.pesquisa-card:focus-within {
|
| 1964 |
z-index: 3;
|
| 1965 |
transform: translateY(-1px);
|
| 1966 |
+
border-color: var(--pesquisa-card-border-hover);
|
| 1967 |
box-shadow:
|
| 1968 |
0 10px 22px rgba(26, 43, 61, 0.08),
|
| 1969 |
inset 0 0 0 1px rgba(255, 255, 255, 0.82);
|
| 1970 |
}
|
| 1971 |
|
| 1972 |
.pesquisa-card.is-selected {
|
|
|
|
| 1973 |
box-shadow:
|
| 1974 |
0 10px 24px rgba(255, 163, 63, 0.17),
|
| 1975 |
+
0 0 0 2px rgba(255, 163, 63, 0.18);
|
| 1976 |
}
|
| 1977 |
|
| 1978 |
.pesquisa-card-top {
|
|
|
|
| 1992 |
overflow-wrap: anywhere;
|
| 1993 |
}
|
| 1994 |
|
| 1995 |
+
.pesquisa-card-divider {
|
| 1996 |
+
width: 100%;
|
| 1997 |
+
height: 1px;
|
| 1998 |
+
background: var(--pesquisa-card-divider);
|
| 1999 |
+
}
|
| 2000 |
+
|
| 2001 |
.pesquisa-card-actions {
|
| 2002 |
display: grid;
|
| 2003 |
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
| 2007 |
|
| 2008 |
.pesquisa-card-meta-actions {
|
| 2009 |
display: grid;
|
| 2010 |
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
| 2011 |
gap: 8px;
|
| 2012 |
min-width: 0;
|
| 2013 |
margin-top: auto;
|
|
|
|
| 2075 |
gap: 9px;
|
| 2076 |
min-width: 0;
|
| 2077 |
flex: 1 1 auto;
|
| 2078 |
+
border-top: 1px solid var(--pesquisa-card-divider);
|
| 2079 |
padding-top: 9px;
|
| 2080 |
}
|
| 2081 |
|
|
|
|
| 2105 |
line-height: 1.34;
|
| 2106 |
}
|
| 2107 |
|
| 2108 |
+
.pesquisa-card-faixa-data-badge {
|
| 2109 |
+
display: inline-flex;
|
| 2110 |
+
align-items: center;
|
| 2111 |
+
justify-content: center;
|
| 2112 |
+
margin-left: 8px;
|
| 2113 |
+
padding: 2px 8px;
|
| 2114 |
+
border-radius: 999px;
|
| 2115 |
+
border: 1px solid transparent;
|
| 2116 |
+
font-size: 0.72rem;
|
| 2117 |
+
font-weight: 800;
|
| 2118 |
+
line-height: 1.2;
|
| 2119 |
+
letter-spacing: 0.01em;
|
| 2120 |
+
vertical-align: middle;
|
| 2121 |
+
white-space: nowrap;
|
| 2122 |
+
}
|
| 2123 |
+
|
| 2124 |
+
.pesquisa-card-faixa-data-badge.is-aged-6m {
|
| 2125 |
+
background: #fff0b8;
|
| 2126 |
+
border-color: #d9bd72;
|
| 2127 |
+
color: #705800;
|
| 2128 |
+
}
|
| 2129 |
+
|
| 2130 |
+
.pesquisa-card-faixa-data-badge.is-aged-1y {
|
| 2131 |
+
background: #ffd7a8;
|
| 2132 |
+
border-color: #d5a16a;
|
| 2133 |
+
color: #754100;
|
| 2134 |
+
}
|
| 2135 |
+
|
| 2136 |
+
.pesquisa-card-faixa-data-badge.is-aged-2y {
|
| 2137 |
+
background: #f4c0c0;
|
| 2138 |
+
border-color: #d69292;
|
| 2139 |
+
color: #7a2f2f;
|
| 2140 |
+
}
|
| 2141 |
+
|
| 2142 |
.pesquisa-card-popover-row {
|
| 2143 |
display: flex;
|
| 2144 |
align-items: flex-start;
|
|
|
|
| 2202 |
}
|
| 2203 |
|
| 2204 |
.pesquisa-card-popover-panel {
|
| 2205 |
+
position: fixed;
|
|
|
|
|
|
|
| 2206 |
display: block;
|
| 2207 |
width: min(560px, calc(100vw - 56px));
|
| 2208 |
max-height: 160px;
|
| 2209 |
+
overflow: auto;
|
| 2210 |
padding: 10px 12px;
|
| 2211 |
border: 1px solid #000;
|
| 2212 |
border-radius: 10px;
|
|
|
|
| 2216 |
opacity: 0;
|
| 2217 |
visibility: hidden;
|
| 2218 |
transform: translateY(6px);
|
| 2219 |
+
pointer-events: auto;
|
| 2220 |
transition: opacity 0.16s ease, transform 0.16s ease, visibility 0.16s ease;
|
| 2221 |
+
z-index: 1600;
|
| 2222 |
}
|
| 2223 |
|
| 2224 |
+
.pesquisa-card-popover-panel.is-floating {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2225 |
opacity: 1;
|
| 2226 |
visibility: visible;
|
| 2227 |
transform: translateY(0);
|
|
|
|
| 2228 |
}
|
| 2229 |
|
| 2230 |
.pesquisa-card-popover-preview {
|