Guilherme Silberfarb Costa
inclusao de menu na lateral esquerda
3c854d0
import React, { useEffect, useRef, useState } from 'react'
import { api, getAuthToken, setAuthToken } from './api'
import AvaliacaoTab from './components/AvaliacaoTab'
import ElaboracaoTab from './components/ElaboracaoTab'
import InicioTab from './components/InicioTab'
import PesquisaTab from './components/PesquisaTab'
import RepositorioTab from './components/RepositorioTab'
const LOGS_PAGE_SIZE = 30
const TABS = [
{ key: 'Pesquisa/Visualização', label: 'Pesquisa/Visualização' },
{ key: 'Elaboração/Edição', label: 'Elaboração/Edição' },
{ key: 'Avaliação', label: 'Avaliação de Imóveis' },
{ key: 'Repositório de Modelos', label: 'Repositório de Modelos' },
]
export default function App() {
const [activeTab, setActiveTab] = useState('')
const [showStartupIntro, setShowStartupIntro] = useState(true)
const [sessionId, setSessionId] = useState('')
const [bootError, setBootError] = useState('')
const [avaliacaoQuickLoad, setAvaliacaoQuickLoad] = useState(null)
const [authLoading, setAuthLoading] = useState(true)
const [authUser, setAuthUser] = useState(null)
const [authError, setAuthError] = useState('')
const [loginLoading, setLoginLoading] = useState(false)
const [usuario, setUsuario] = useState('')
const [matricula, setMatricula] = useState('')
const [logsStatus, setLogsStatus] = useState(null)
const [logsStatusLoading, setLogsStatusLoading] = useState(false)
const [logsOpen, setLogsOpen] = useState(false)
const [logsEvents, setLogsEvents] = useState([])
const [logsLoading, setLogsLoading] = useState(false)
const [logsError, setLogsError] = useState('')
const [logsScope, setLogsScope] = useState('')
const [logsUsuario, setLogsUsuario] = useState('')
const [logsPage, setLogsPage] = useState(1)
const [settingsOpen, setSettingsOpen] = useState(false)
const [showScrollHomeBtn, setShowScrollHomeBtn] = useState(false)
const [scrollHomeBtnLeft, setScrollHomeBtnLeft] = useState(8)
const headerRef = useRef(null)
const settingsMenuRef = useRef(null)
const isAdmin = String(authUser?.perfil || '').toLowerCase() === 'admin'
const logsEnabled = Boolean(logsStatus?.enabled)
const logsDisabledReason = String(logsStatus?.reason || 'Logs indisponíveis')
const logsViewActive = Boolean(authUser && isAdmin && logsOpen)
const logsTotalPages = Math.max(1, Math.ceil(logsEvents.length / LOGS_PAGE_SIZE))
const logsCurrentPage = Math.min(logsPage, logsTotalPages)
const logsStartIndex = logsEvents.length ? (logsCurrentPage - 1) * LOGS_PAGE_SIZE : 0
const logsVisibleEvents = logsEvents.slice(logsStartIndex, logsStartIndex + LOGS_PAGE_SIZE)
const logsEndIndex = logsEvents.length ? Math.min(logsStartIndex + LOGS_PAGE_SIZE, logsEvents.length) : 0
function resetToLogin(message = '') {
setAuthToken('')
setAuthUser(null)
setAuthLoading(false)
setLoginLoading(false)
setSessionId('')
setBootError('')
setLogsStatus(null)
setLogsOpen(false)
setLogsEvents([])
setLogsError('')
setLogsPage(1)
setActiveTab('')
setShowStartupIntro(true)
setAuthError(message)
}
useEffect(() => {
let mounted = true
async function bootstrapAuth() {
const token = getAuthToken()
if (!token) {
if (mounted) setAuthLoading(false)
return
}
try {
const resp = await api.authMe()
if (!mounted) return
setAuthUser(resp?.usuario || null)
} catch {
setAuthToken('')
if (!mounted) return
setAuthUser(null)
} finally {
if (mounted) setAuthLoading(false)
}
}
void bootstrapAuth()
return () => {
mounted = false
}
}, [])
useEffect(() => {
function onAuthRequired(event) {
const message = String(event?.detail?.message || '').trim() || 'Sessão expirada. Faça login novamente.'
resetToLogin(message)
}
if (typeof window !== 'undefined') {
window.addEventListener('mesa:auth-required', onAuthRequired)
return () => window.removeEventListener('mesa:auth-required', onAuthRequired)
}
return undefined
}, [])
useEffect(() => {
let mounted = true
if (!authUser) {
setSessionId('')
setBootError('')
return () => {
mounted = false
}
}
setBootError('')
api.createSession()
.then((resp) => {
if (!mounted) return
setSessionId(resp.session_id)
setBootError('')
})
.catch((err) => {
if (!mounted) return
if (Number(err?.status) === 401) return
setBootError(err.message || 'Falha ao criar sessão')
})
return () => {
mounted = false
}
}, [authUser])
useEffect(() => {
if (!authUser || !isAdmin) {
setLogsStatus(null)
setLogsOpen(false)
setLogsEvents([])
setLogsError('')
setLogsPage(1)
setLogsStatusLoading(false)
setLogsLoading(false)
return
}
void carregarLogsStatus()
}, [authUser, isAdmin])
useEffect(() => {
if (!logsEvents.length && logsPage !== 1) {
setLogsPage(1)
return
}
if (logsPage > logsTotalPages) {
setLogsPage(logsTotalPages)
}
}, [logsEvents.length, logsPage, logsTotalPages])
useEffect(() => {
if (!settingsOpen) return undefined
function onPointerDown(event) {
if (!settingsMenuRef.current) return
if (!settingsMenuRef.current.contains(event.target)) {
setSettingsOpen(false)
}
}
document.addEventListener('mousedown', onPointerDown)
return () => document.removeEventListener('mousedown', onPointerDown)
}, [settingsOpen])
useEffect(() => {
if (!authUser) {
setSettingsOpen(false)
}
}, [authUser])
useEffect(() => {
if (typeof window === 'undefined') return undefined
if (!authUser) {
setShowScrollHomeBtn(false)
setScrollHomeBtnLeft(8)
return undefined
}
function resolveHomeButtonLeft() {
const buttonSize = 42
const navAnchor = document.querySelector('.elaboracao-side-nav-item')
if (navAnchor && typeof navAnchor.getBoundingClientRect === 'function') {
const rect = navAnchor.getBoundingClientRect()
return Math.max(8, rect.left + ((rect.width - buttonSize) / 2))
}
const shell = document.querySelector('.app-shell')
if (shell && typeof shell.getBoundingClientRect === 'function') {
const rect = shell.getBoundingClientRect()
return Math.max(8, rect.left)
}
return 8
}
function updateScrollHomeVisibility() {
const headerEl = headerRef.current
if (!headerEl) {
setShowScrollHomeBtn(false)
return
}
const rect = headerEl.getBoundingClientRect()
const shouldShow = rect.bottom <= 0
const nextLeft = resolveHomeButtonLeft()
setShowScrollHomeBtn((current) => (current === shouldShow ? current : shouldShow))
setScrollHomeBtnLeft((current) => (Math.abs(current - nextLeft) < 0.5 ? current : nextLeft))
}
updateScrollHomeVisibility()
window.addEventListener('scroll', updateScrollHomeVisibility, { passive: true })
window.addEventListener('resize', updateScrollHomeVisibility)
return () => {
window.removeEventListener('scroll', updateScrollHomeVisibility)
window.removeEventListener('resize', updateScrollHomeVisibility)
}
}, [authUser])
async function onSubmitLogin(event) {
event.preventDefault()
setAuthError('')
setBootError('')
setLoginLoading(true)
try {
const resp = await api.authLogin(usuario, matricula)
setAuthToken(resp?.token || '')
setAuthUser(resp?.usuario || null)
setUsuario('')
setMatricula('')
} catch (err) {
setAuthError(err.message || 'Falha no login')
setAuthUser(null)
setAuthToken('')
} finally {
setLoginLoading(false)
setAuthLoading(false)
}
}
async function onLogout() {
setSettingsOpen(false)
try {
await api.authLogout()
} catch {
// Ignore falha de logout remoto para garantir limpeza local.
}
resetToLogin('')
}
async function carregarLogsStatus() {
if (!isAdmin) return
setLogsStatusLoading(true)
try {
const resp = await api.logsStatus()
setLogsStatus(resp || null)
} catch (err) {
setLogsStatus({
enabled: false,
backend: 'disabled',
reason: err.message || 'Falha ao carregar status de logs.',
})
} finally {
setLogsStatusLoading(false)
}
}
async function carregarEventosLogs() {
if (!isAdmin || !logsEnabled) return
setLogsLoading(true)
setLogsError('')
try {
const resp = await api.logsEvents({ scope: logsScope, usuario: logsUsuario, limit: 1000 })
setLogsEvents(Array.isArray(resp?.events) ? resp.events : [])
setLogsPage(1)
} catch (err) {
setLogsError(err.message || 'Falha ao carregar logs.')
setLogsEvents([])
setLogsPage(1)
} finally {
setLogsLoading(false)
}
}
async function onToggleLogs() {
if (!isAdmin) return
if (logsOpen) {
setLogsOpen(false)
return
}
setLogsOpen(true)
if (!logsEnabled) return
await carregarEventosLogs()
}
function onCloseLogsView() {
setLogsOpen(false)
}
function onUsarModeloEmAvaliacao(modelo) {
const modeloId = String(modelo?.id || '').trim()
if (!modeloId) return
setAvaliacaoQuickLoad({
requestKey: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
modeloId,
modeloArquivo: String(modelo?.arquivo || '').trim(),
nomeModelo: String(modelo?.nome_modelo || modelo?.arquivo || modeloId),
})
setActiveTab('Avaliação')
setLogsOpen(false)
setShowStartupIntro(false)
}
function onScrollToHeader() {
if (typeof window === 'undefined') return
const headerEl = headerRef.current
if (!headerEl) return
const top = Math.max(0, window.scrollY + headerEl.getBoundingClientRect().top - 8)
window.scrollTo({ top, behavior: 'smooth' })
}
return (
<div className="app-shell">
<header ref={headerRef} className={authUser ? 'app-header app-header-logged' : 'app-header app-header-logo-only'}>
<div className="brand-mark" aria-hidden="true">
<img src={`${import.meta.env.BASE_URL}logo_mesa.png`} alt="MESA" />
</div>
{authUser ? (
<div className="app-top-actions">
<nav className="tabs" aria-label="Navegação principal">
{TABS.map((tab) => {
const active = tab.key === activeTab
return (
<button
key={tab.key}
className={active ? 'tab-pill active' : 'tab-pill'}
onClick={() => {
setActiveTab(tab.key)
setShowStartupIntro(false)
setLogsOpen(false)
setSettingsOpen(false)
}}
type="button"
>
<strong>{tab.label}</strong>
</button>
)
})}
</nav>
<div ref={settingsMenuRef} className={`settings-menu${settingsOpen ? ' is-open' : ''}`}>
<button
type="button"
className="settings-gear-btn"
aria-haspopup="menu"
aria-expanded={settingsOpen}
aria-label="Abrir configurações"
onClick={() => setSettingsOpen((prev) => !prev)}
title="Configurações"
>
&#9881;
</button>
{settingsOpen ? (
<div className="settings-menu-panel" role="menu" aria-label="Configurações do usuário">
<div className="settings-user-summary">
Usuário: <strong>{authUser.nome || authUser.usuario}</strong> ({authUser.perfil || 'viewer'})
</div>
<div className="settings-menu-actions">
{isAdmin ? (
<button
type="button"
className="settings-menu-btn"
onClick={() => {
void onToggleLogs()
setSettingsOpen(false)
}}
disabled={logsStatusLoading || (!logsEnabled && !logsOpen)}
title={logsOpen ? 'Fechar visualização de logs' : !logsEnabled ? logsDisabledReason : 'Abrir leitura de logs'}
>
{logsOpen ? 'Fechar logs' : 'Abrir logs'}
</button>
) : null}
<button
type="button"
className="settings-menu-btn settings-menu-btn-danger"
onClick={() => void onLogout()}
>
Sair
</button>
</div>
</div>
) : null}
</div>
</div>
) : null}
</header>
{authUser && showScrollHomeBtn ? (
<button
type="button"
className="scroll-home-btn"
style={{ left: `${scrollHomeBtnLeft}px` }}
onClick={onScrollToHeader}
aria-label="Voltar ao cabeçalho"
title="Voltar ao cabeçalho"
>
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M11.49 2.26a.75.75 0 0 1 1.02 0l9 8.25a.75.75 0 0 1-1.02 1.1L20 11.12V20a2 2 0 0 1-2 2h-3.75a.75.75 0 0 1-.75-.75V16a.25.25 0 0 0-.25-.25h-2.5a.25.25 0 0 0-.25.25v5.25a.75.75 0 0 1-.75.75H6a2 2 0 0 1-2-2v-8.88l-.49.49a.75.75 0 0 1-1.02-1.1l9-8.25Z" />
</svg>
</button>
) : null}
{authLoading ? <div className="status-line">Validando autenticação...</div> : null}
{!authLoading && !authUser ? (
<section className="auth-card">
<h3>Entrar na MESA</h3>
<p>Usuário: primeiro nome sem acentos e em minúsculo. Senha: matrícula sem dígito.</p>
<form onSubmit={onSubmitLogin} className="auth-form">
<div className="auth-field">
<label htmlFor="usuario">Usuário</label>
<input
id="usuario"
type="text"
placeholder="ex.: david (minúsculo, sem acento)"
value={usuario}
onChange={(event) => setUsuario(event.target.value)}
autoComplete="off"
disabled={loginLoading}
/>
</div>
<div className="auth-field">
<label htmlFor="matricula">Senha</label>
<input
id="matricula"
type="text"
placeholder="matrícula sem dígito"
value={matricula}
onChange={(event) => setMatricula(event.target.value)}
autoComplete="off"
disabled={loginLoading}
/>
</div>
<button className="auth-submit" type="submit" disabled={loginLoading || !usuario.trim() || !matricula.trim()}>
{loginLoading ? 'Entrando...' : 'Entrar'}
</button>
</form>
{authError ? <div className="error-line">{authError}</div> : null}
</section>
) : null}
{authUser ? (
logsViewActive ? (
<section className="logs-panel logs-panel-dedicated">
<div className="logs-panel-head">
<div className="logs-panel-head-main">
<h3>Logs</h3>
<p className="logs-close-hint">Para voltar ao app, clique em "Fechar logs".</p>
</div>
<div className="logs-panel-head-actions">
<div className="logs-panel-meta">
<span>{logsStatus?.backend === 'hf_dataset' ? 'Origem: Dataset HF' : 'Origem: indisponível'}</span>
{logsStatus?.revision ? <span>Revisão: {String(logsStatus.revision).slice(0, 8)}</span> : null}
</div>
<button type="button" className="logs-close-btn" onClick={onCloseLogsView}>
Fechar logs
</button>
</div>
</div>
{!logsEnabled ? (
<div className="section1-empty-hint">{logsDisabledReason}</div>
) : (
<>
<div className="logs-filters">
<div className="logs-field">
<label htmlFor="logs-scope">Escopo</label>
<select id="logs-scope" value={logsScope} onChange={(event) => setLogsScope(event.target.value)}>
<option value="">Todos</option>
<option value="auth">Auth</option>
<option value="repositorio">Repositório</option>
<option value="elaboracao">Elaboração</option>
<option value="visualizacao">Visualização</option>
</select>
</div>
<div className="logs-field">
<label htmlFor="logs-usuario">Usuário</label>
<input
id="logs-usuario"
type="text"
value={logsUsuario}
onChange={(event) => setLogsUsuario(event.target.value)}
placeholder="filtrar por usuário"
autoComplete="off"
/>
</div>
<button type="button" onClick={() => void carregarEventosLogs()} disabled={logsLoading}>
Atualizar
</button>
</div>
{logsError ? <div className="error-line">{logsError}</div> : null}
<div className="table-container">
<table className="logs-table">
<thead>
<tr>
<th>Timestamp</th>
<th>Usuário</th>
<th>Perfil</th>
<th>Escopo</th>
<th>Ação</th>
<th>Status</th>
<th>Detalhes</th>
</tr>
</thead>
<tbody>
{logsVisibleEvents.map((item) => (
<tr key={item.event_id || `${item.ts}-${item.action}`}>
<td>{item.ts || '-'}</td>
<td>{item.usuario || '-'}</td>
<td>{item.perfil || '-'}</td>
<td>{item.scope || '-'}</td>
<td>{item.action || '-'}</td>
<td>{item.status || '-'}</td>
<td className="logs-table-details">{JSON.stringify(item.details || {})}</td>
</tr>
))}
{!logsEvents.length ? (
<tr>
<td colSpan={7}>{logsLoading ? 'Carregando logs...' : 'Nenhum log encontrado para os filtros.'}</td>
</tr>
) : null}
</tbody>
</table>
</div>
<div className="logs-pagination">
<span>{logsEvents.length ? `Exibindo ${logsStartIndex + 1}-${logsEndIndex} de ${logsEvents.length}` : 'Exibindo 0 de 0'}</span>
<div className="logs-pagination-actions">
<button
type="button"
onClick={() => setLogsPage((prev) => Math.max(1, prev - 1))}
disabled={logsLoading || logsCurrentPage <= 1}
>
Anterior
</button>
<span>Página {logsCurrentPage} de {logsTotalPages}</span>
<button
type="button"
onClick={() => setLogsPage((prev) => Math.min(logsTotalPages, prev + 1))}
disabled={logsLoading || logsCurrentPage >= logsTotalPages}
>
Próxima
</button>
</div>
</div>
</>
)}
</section>
) : (
<>
{bootError ? <div className="error-line">Falha ao criar sessão: {bootError}</div> : null}
{showStartupIntro ? (
<div className="tab-pane">
<InicioTab />
</div>
) : null}
<div className="tab-pane" hidden={activeTab !== 'Pesquisa/Visualização'}>
<PesquisaTab sessionId={sessionId} onUsarModeloEmAvaliacao={onUsarModeloEmAvaliacao} />
</div>
<div className="tab-pane" hidden={activeTab !== 'Elaboração/Edição'}>
<ElaboracaoTab sessionId={sessionId} />
</div>
<div className="tab-pane" hidden={activeTab !== 'Repositório de Modelos'}>
<RepositorioTab authUser={authUser} sessionId={sessionId} />
</div>
<div className="tab-pane" hidden={activeTab !== 'Avaliação'}>
<AvaliacaoTab sessionId={sessionId} quickLoadRequest={avaliacaoQuickLoad} />
</div>
</>
)
) : null}
</div>
)
}