mesa-react / frontend /src /components /RepositorioTab.jsx
Guilherme Silberfarb Costa
correcoes de overflows e normalizacoes de tipos
dac5782
import React, { useEffect, useState } from 'react'
import { api, downloadBlob } from '../api'
import DataTable from './DataTable'
import EquationFormatsPanel from './EquationFormatsPanel'
import LoadingOverlay from './LoadingOverlay'
import MapFrame from './MapFrame'
import PlotFigure from './PlotFigure'
const REPO_INNER_TABS = [
{ key: 'mapa', label: 'Mapa' },
{ 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' },
]
function formatarFonte(fonte) {
if (!fonte || typeof fonte !== 'object') return 'Fonte não informada'
const provider = String(fonte.provider || '').toLowerCase()
if (provider === 'hf_dataset') {
const repo = String(fonte.repo_id || '').trim()
const revision = String(fonte.revision || '').trim()
const suffix = fonte.degraded ? ' (modo contingência)' : ''
return `HF Dataset${repo ? ` (${repo})` : ''}${revision ? ` | revisão ${revision.slice(0, 8)}` : ''}${suffix}`
}
return 'Pasta local'
}
export default function RepositorioTab({ authUser, sessionId }) {
const [modelos, setModelos] = useState([])
const [fonte, setFonte] = useState(null)
const [loading, setLoading] = useState(false)
const [uploading, setUploading] = useState(false)
const [deleting, setDeleting] = useState(false)
const [error, setError] = useState('')
const [status, setStatus] = useState('')
const [arquivoUpload, setArquivoUpload] = useState(null)
const [confirmDeleteId, setConfirmDeleteId] = useState('')
const [confirmarSubstituicao, setConfirmarSubstituicao] = useState({ open: false, substituidos: [] })
const [confirmarExclusaoModal, setConfirmarExclusaoModal] = useState({
open: false,
modeloId: '',
nomeModelo: '',
digitado: '',
})
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 [modeloAbertoMapaChoices, setModeloAbertoMapaChoices] = useState(['Visualização Padrão'])
const [modeloAbertoMapaVar, setModeloAbertoMapaVar] = useState('Visualização Padrão')
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 isAdmin = String(authUser?.perfil || '').toLowerCase() === 'admin'
const totalModelos = modelos.length
const modoModeloAberto = Boolean(modeloAbertoMeta)
const nomesSubstituidos = Array.isArray(confirmarSubstituicao.substituidos)
? confirmarSubstituicao.substituidos.filter(Boolean)
: []
const exclusaoDigitadaCorreta = confirmarExclusaoModal.digitado === confirmarExclusaoModal.nomeModelo
useEffect(() => {
void carregarModelos()
}, [])
async function carregarModelos() {
setLoading(true)
setError('')
try {
const resp = await api.repositorioListar()
setModelos(Array.isArray(resp?.modelos) ? resp.modelos : [])
setFonte(resp?.fonte || null)
setStatus('')
} catch (err) {
setError(err.message || 'Falha ao carregar repositório.')
setModelos([])
setFonte(null)
} finally {
setLoading(false)
}
}
function parseSubstituidosErro(err) {
if (!err || typeof err !== 'object') return []
const detail = err.detail
if (!detail || typeof detail !== 'object') return []
const lista = detail.substituidos
if (!Array.isArray(lista)) return []
return lista.map((item) => String(item || '').trim()).filter(Boolean)
}
async function onUploadArquivo(confirmar = false) {
if (!isAdmin || !arquivoUpload) return
setUploading(true)
setError('')
setStatus('')
try {
const resp = await api.repositorioUpload([arquivoUpload], { confirmarSubstituicao: confirmar })
setModelos(Array.isArray(resp?.modelos) ? resp.modelos : [])
setFonte(resp?.fonte || null)
setStatus(resp?.status || 'Modelo incluído no repositório.')
setArquivoUpload(null)
setConfirmarSubstituicao({ open: false, substituidos: [] })
} catch (err) {
const substituidos = parseSubstituidosErro(err)
const erroDuplicado = Number(err?.status) === 409 && substituidos.length > 0
if (!confirmar && erroDuplicado) {
setConfirmarSubstituicao({ open: true, substituidos })
setError('')
return
}
setError(err.message || 'Falha ao incluir modelo no repositório.')
} finally {
setUploading(false)
}
}
async function onExcluirModelo(modeloId) {
if (!isAdmin || !modeloId) return
setDeleting(true)
setError('')
setStatus('')
try {
const resp = await api.repositorioDelete([String(modeloId)])
setModelos(Array.isArray(resp?.modelos) ? resp.modelos : [])
setFonte(resp?.fonte || null)
setStatus(resp?.status || 'Modelo removido do repositório.')
setConfirmDeleteId('')
setConfirmarExclusaoModal({ open: false, modeloId: '', nomeModelo: '', digitado: '' })
} catch (err) {
setError(err.message || 'Falha na exclusão do modelo.')
} finally {
setDeleting(false)
}
}
function onAbrirModalExclusao(item) {
const id = String(item?.id || '').trim()
if (!id) return
const nomeModelo = String(item?.nome_modelo || item?.arquivo || id).trim()
setConfirmDeleteId('')
setConfirmarExclusaoModal({
open: true,
modeloId: id,
nomeModelo,
digitado: '',
})
}
function onCancelarModalExclusao() {
if (deleting) return
setConfirmarExclusaoModal({ open: false, modeloId: '', nomeModelo: '', digitado: '' })
}
async function onConfirmarExclusaoModal() {
const modeloId = String(confirmarExclusaoModal.modeloId || '').trim()
if (!modeloId) return
if (confirmarExclusaoModal.digitado !== confirmarExclusaoModal.nomeModelo) return
await onExcluirModelo(modeloId)
}
function preencherModeloAberto(resp) {
setModeloAbertoDados(resp?.dados || null)
setModeloAbertoEstatisticas(resp?.estatisticas || null)
setModeloAbertoEscalasHtml(resp?.escalas_html || '')
setModeloAbertoDadosTransformados(resp?.dados_transformados || null)
setModeloAbertoResumoHtml(resp?.resumo_html || '')
setModeloAbertoEquacoes(resp?.equacoes || null)
setModeloAbertoCoeficientes(resp?.coeficientes || null)
setModeloAbertoObsCalc(resp?.obs_calc || null)
setModeloAbertoMapaHtml(resp?.mapa_html || '')
setModeloAbertoMapaChoices(resp?.mapa_choices || ['Visualização Padrão'])
setModeloAbertoMapaVar('Visualização Padrão')
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)
}
async function onAbrirModelo(item) {
if (!sessionId) {
setError('Sessão indisponível no momento. Aguarde e tente novamente.')
return
}
setModeloAbertoLoading(true)
setModeloAbertoError('')
try {
await api.visualizacaoRepositorioCarregar(sessionId, String(item?.id || ''))
const resp = await api.exibirVisualizacao(sessionId)
preencherModeloAberto(resp)
setModeloAbertoActiveTab('mapa')
setModeloAbertoMeta({
id: String(item?.id || ''),
nome: item?.nome_modelo || item?.arquivo || String(item?.id || ''),
})
} catch (err) {
setModeloAbertoError(err.message || 'Falha ao abrir modelo.')
} finally {
setModeloAbertoLoading(false)
}
}
function onVoltarRepositorio() {
setModeloAbertoMeta(null)
setModeloAbertoError('')
setModeloAbertoActiveTab('mapa')
}
async function onModeloAbertoMapChange(nextVar) {
setModeloAbertoMapaVar(nextVar)
if (!sessionId) return
try {
const resp = await api.updateVisualizacaoMap(sessionId, nextVar)
setModeloAbertoMapaHtml(resp?.mapa_html || '')
} 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)
}
}
if (modoModeloAberto) {
return (
<div className="tab-content">
<div className="pesquisa-opened-model-view">
<div className="pesquisa-opened-model-head">
<div className="pesquisa-opened-model-title-wrap">
<h3>{modeloAbertoMeta?.nome || 'Modelo'}</h3>
<p>Visualização do modelo do repositório</p>
</div>
<button type="button" className="model-source-back-btn model-source-back-btn-danger" onClick={onVoltarRepositorio} disabled={modeloAbertoLoading}>
Voltar ao repositório
</button>
</div>
<div className="inner-tabs" role="tablist" aria-label="Abas internas do modelo aberto no repositório">
{REPO_INNER_TABS.map((tab) => (
<button
key={tab.key}
type="button"
className={modeloAbertoActiveTab === tab.key ? 'inner-tab-pill active' : 'inner-tab-pill'}
onClick={() => setModeloAbertoActiveTab(tab.key)}
>
{tab.label}
</button>
))}
</div>
<div className="inner-tab-panel">
{modeloAbertoActiveTab === 'mapa' ? (
<>
<div className="row compact visualizacao-mapa-controls">
<label>Variável no mapa</label>
<select value={modeloAbertoMapaVar} onChange={(event) => void onModeloAbertoMapChange(event.target.value)}>
{modeloAbertoMapaChoices.map((choice) => (
<option key={`repo-modelo-aberto-mapa-${choice}`} value={choice}>{choice}</option>
))}
</select>
</div>
<MapFrame html={modeloAbertoMapaHtml} />
</>
) : null}
{modeloAbertoActiveTab === 'dados_mercado' ? <DataTable table={modeloAbertoDados} maxHeight={620} /> : null}
{modeloAbertoActiveTab === 'metricas' ? <DataTable table={modeloAbertoEstatisticas} maxHeight={620} /> : null}
{modeloAbertoActiveTab === 'transformacoes' ? (
<>
<div dangerouslySetInnerHTML={{ __html: modeloAbertoEscalasHtml }} />
<h4 className="visualizacao-table-title">Dados com variáveis transformadas</h4>
<DataTable table={modeloAbertoDadosTransformados} />
</>
) : null}
{modeloAbertoActiveTab === 'resumo' ? (
<>
<div className="equation-formats-section">
<h4>Equações do Modelo</h4>
<EquationFormatsPanel
equacoes={modeloAbertoEquacoes}
onDownload={(mode) => void onDownloadEquacaoModeloAberto(mode)}
disabled={modeloAbertoLoading}
/>
</div>
<div dangerouslySetInnerHTML={{ __html: modeloAbertoResumoHtml }} />
</>
) : null}
{modeloAbertoActiveTab === 'coeficientes' ? <DataTable table={modeloAbertoCoeficientes} maxHeight={620} /> : null}
{modeloAbertoActiveTab === 'obs_calc' ? <DataTable table={modeloAbertoObsCalc} maxHeight={620} /> : null}
{modeloAbertoActiveTab === 'graficos' ? (
<>
<div className="plot-grid-2-fixed">
<PlotFigure figure={modeloAbertoPlotObsCalc} title="Obs x Calc" />
<PlotFigure figure={modeloAbertoPlotResiduos} title="Resíduos" />
<PlotFigure figure={modeloAbertoPlotHistograma} title="Histograma" />
<PlotFigure figure={modeloAbertoPlotCook} title="Cook" forceHideLegend />
</div>
<div className="plot-full-width">
<PlotFigure figure={modeloAbertoPlotCorr} title="Correlação" className="plot-correlation-card" />
</div>
</>
) : null}
</div>
{modeloAbertoError ? <div className="error-line inline-error">{modeloAbertoError}</div> : null}
</div>
<LoadingOverlay show={modeloAbertoLoading} label="Carregando modelo..." />
</div>
)
}
return (
<div className="tab-content">
<div className="repositorio-standalone-panel">
<div className="repo-toolbar">
<div className="repo-summary">
<div><strong>Total:</strong> {totalModelos}</div>
<div><strong>Fonte:</strong> {formatarFonte(fonte)}</div>
<div><strong>Perfil:</strong> {isAdmin ? 'Administrador' : 'Leitura'}</div>
</div>
<div className="repo-actions">
<button
type="button"
className="repo-refresh-btn"
onClick={() => void carregarModelos()}
disabled={loading || uploading || deleting}
>
Atualizar lista
</button>
</div>
</div>
{isAdmin ? (
<div className="repo-admin-controls">
<div className="repo-upload-row repo-upload-row-single">
<input
type="file"
accept=".dai"
onChange={(event) => setArquivoUpload(event.target.files?.[0] || null)}
disabled={uploading || deleting}
/>
<button type="button" onClick={() => void onUploadArquivo()} disabled={uploading || deleting || !arquivoUpload}>
Incluir modelo no repositório
</button>
</div>
{arquivoUpload ? <div className="section1-empty-hint">Arquivo selecionado: {arquivoUpload.name}</div> : null}
</div>
) : (
<div className="section1-empty-hint">Perfil de leitura: inclusão e exclusão disponíveis apenas para administradores.</div>
)}
{status ? <div className="status-line">{status}</div> : null}
{error ? <div className="error-line">{error}</div> : null}
<div className="table-container repo-table-block">
<table className="repo-table">
<thead>
<tr>
<th>Modelo</th>
<th>Tipo</th>
<th>Finalidade</th>
<th>Autor</th>
<th>Período</th>
<th>Dados</th>
<th>APP</th>
<th>Status</th>
<th className="repo-col-open">Abrir</th>
{isAdmin ? <th className="repo-col-delete">Excluir</th> : null}
</tr>
</thead>
<tbody>
{modelos.map((item) => {
const key = String(item.id)
const emConfirmacao = confirmDeleteId === key
return (
<tr key={key}>
<td>{item.nome_modelo || item.arquivo || key}</td>
<td>{item.tipo_imovel || '-'}</td>
<td>{item.finalidade || '-'}</td>
<td>{item.autor || '-'}</td>
<td>{item.periodo_dados?.label || '-'}</td>
<td>{item.total_dados ?? '-'}</td>
<td>{item.tem_app ? 'Sim' : 'Não'}</td>
<td>{item.status || '-'}</td>
<td className="repo-col-open">
<button
type="button"
className="repo-open-btn"
onClick={() => void onAbrirModelo(item)}
title="Abrir modelo"
aria-label={`Abrir ${item.nome_modelo || item.arquivo || key}`}
>
</button>
</td>
{isAdmin ? (
<td className="repo-col-delete">
{!emConfirmacao ? (
<button
type="button"
className="repo-delete-icon-btn"
onClick={() => setConfirmDeleteId(key)}
disabled={deleting}
title="Excluir modelo"
aria-label={`Excluir ${item.nome_modelo || item.arquivo || key}`}
>
🗑
</button>
) : (
<div className="repo-delete-inline-confirm">
<button
type="button"
className="btn-danger repo-delete-confirm-btn"
onClick={() => onAbrirModalExclusao(item)}
disabled={deleting}
>
Excluir
</button>
<button
type="button"
className="repo-delete-cancel-btn"
onClick={() => setConfirmDeleteId('')}
disabled={deleting}
>
Cancelar
</button>
</div>
)}
</td>
) : null}
</tr>
)
})}
{!modelos.length ? (
<tr>
<td colSpan={isAdmin ? 10 : 9}>
{loading ? 'Carregando modelos...' : 'Nenhum modelo encontrado no repositório.'}
</td>
</tr>
) : null}
</tbody>
</table>
</div>
</div>
{confirmarSubstituicao.open ? (
<div className="pesquisa-modal-backdrop">
<div className="pesquisa-modal repo-confirm-modal">
<div className="pesquisa-modal-head">
<div>
<h4>Confirmar substituição de modelo</h4>
<p>Já existe modelo com o mesmo nome no repositório.</p>
</div>
<button
type="button"
className="pesquisa-modal-close"
onClick={() => setConfirmarSubstituicao({ open: false, substituidos: [] })}
disabled={uploading}
>
Fechar
</button>
</div>
<div className="repo-confirm-modal-body">
<div className="repo-confirm-text">
Se você continuar, o modelo existente será substituído.
</div>
<ul className="repo-replace-list">
{(nomesSubstituidos.length ? nomesSubstituidos : [String(arquivoUpload?.name || '')]).map((nome) => (
<li key={`substituir-${nome}`}>{nome}</li>
))}
</ul>
<div className="repo-confirm-actions">
<button
type="button"
className="repo-delete-cancel-btn"
onClick={() => setConfirmarSubstituicao({ open: false, substituidos: [] })}
disabled={uploading}
>
Cancelar
</button>
<button
type="button"
className="btn-danger"
onClick={() => void onUploadArquivo(true)}
disabled={uploading}
>
{uploading ? 'Substituindo...' : 'Confirmar substituição'}
</button>
</div>
</div>
</div>
</div>
) : null}
{confirmarExclusaoModal.open ? (
<div className="pesquisa-modal-backdrop">
<div className="pesquisa-modal repo-confirm-modal">
<div className="pesquisa-modal-head">
<div>
<h4>{confirmarExclusaoModal.nomeModelo || 'Confirmar exclusão'}</h4>
<p>Digite o nome completo do modelo para confirmar a exclusão.</p>
</div>
<button
type="button"
className="pesquisa-modal-close"
onClick={onCancelarModalExclusao}
disabled={deleting}
>
Fechar
</button>
</div>
<form
className="repo-confirm-modal-body"
onSubmit={(event) => {
event.preventDefault()
void onConfirmarExclusaoModal()
}}
>
<label className="repo-delete-typing-field">
Nome do modelo
<input
type="text"
value={confirmarExclusaoModal.digitado}
onChange={(event) => setConfirmarExclusaoModal((prev) => ({ ...prev, digitado: event.target.value }))}
placeholder="Digite exatamente como no título"
autoComplete="off"
disabled={deleting}
/>
</label>
{confirmarExclusaoModal.digitado && !exclusaoDigitadaCorreta ? (
<div className="repo-delete-typing-hint repo-delete-typing-hint-error">
O texto digitado não corresponde ao nome do modelo.
</div>
) : (
<div className="repo-delete-typing-hint">
A exclusão será concluída somente quando o nome for digitado exatamente.
</div>
)}
<div className="repo-confirm-actions">
<button type="button" className="repo-delete-cancel-btn" onClick={onCancelarModalExclusao} disabled={deleting}>
Cancelar
</button>
<button type="submit" className="btn-danger" disabled={deleting || !exclusaoDigitadaCorreta}>
{deleting ? 'Excluindo...' : 'Excluir modelo'}
</button>
</div>
</form>
</div>
</div>
) : null}
</div>
)
}