mesa-react / frontend /src /components /VisualizacaoTab.jsx
Guilherme Silberfarb Costa
inclusao de diferenciacao de unitarios e totais
f0f8ff1
import React, { useEffect, useMemo, useRef, 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'
import SectionBlock from './SectionBlock'
import SinglePillAutocomplete from './SinglePillAutocomplete'
const 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' },
{ key: 'avaliacao', label: 'Avaliação' },
{ key: 'avaliacao_massa', label: 'Avaliação em Massa' },
]
const BASE_COMPARACAO_SEM_BASE = '__none__'
export default function VisualizacaoTab({ sessionId }) {
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [status, setStatus] = useState('')
const [badgeHtml, setBadgeHtml] = useState('')
const [uploadedFile, setUploadedFile] = useState(null)
const [uploadDragOver, setUploadDragOver] = useState(false)
const [modeloLoadSource, setModeloLoadSource] = useState('')
const [repoModelos, setRepoModelos] = useState([])
const [repoModeloSelecionado, setRepoModeloSelecionado] = useState('')
const [repoModelosLoading, setRepoModelosLoading] = useState(false)
const [repoFonteModelos, setRepoFonteModelos] = useState('')
const [dados, setDados] = useState(null)
const [estatisticas, setEstatisticas] = useState(null)
const [escalasHtml, setEscalasHtml] = useState('')
const [dadosTransformados, setDadosTransformados] = useState(null)
const [resumoHtml, setResumoHtml] = useState('')
const [equacoes, setEquacoes] = useState(null)
const [coeficientes, setCoeficientes] = useState(null)
const [obsCalc, setObsCalc] = useState(null)
const [plotObsCalc, setPlotObsCalc] = useState(null)
const [plotResiduos, setPlotResiduos] = useState(null)
const [plotHistograma, setPlotHistograma] = useState(null)
const [plotCook, setPlotCook] = useState(null)
const [plotCorr, setPlotCorr] = useState(null)
const [mapaHtml, setMapaHtml] = useState('')
const [mapaChoices, setMapaChoices] = useState(['Visualização Padrão'])
const [mapaVar, setMapaVar] = useState('Visualização Padrão')
const [camposAvaliacao, setCamposAvaliacao] = useState([])
const valoresAvaliacaoRef = useRef({})
const [avaliacaoFormVersion, setAvaliacaoFormVersion] = useState(0)
const [confirmarLimpezaAvaliacoes, setConfirmarLimpezaAvaliacoes] = useState(false)
const [resultadoAvaliacaoHtml, setResultadoAvaliacaoHtml] = useState('')
const [baseChoices, setBaseChoices] = useState([])
const [baseValue, setBaseValue] = useState('')
const [activeInnerTab, setActiveInnerTab] = useState('mapa')
const deleteConfirmTimersRef = useRef({})
const uploadInputRef = useRef(null)
const temAvaliacoes = Array.isArray(baseChoices) && baseChoices.length > 0
const repoModeloOptions = useMemo(
() => (repoModelos || []).map((item) => ({
value: String(item?.id || ''),
label: String(item?.nome_modelo || item?.arquivo || item?.id || ''),
secondary: String(item?.arquivo || ''),
})).filter((item) => item.value && item.label),
[repoModelos],
)
function resetConteudoVisualizacao() {
setDados(null)
setEstatisticas(null)
setEscalasHtml('')
setDadosTransformados(null)
setResumoHtml('')
setEquacoes(null)
setCoeficientes(null)
setObsCalc(null)
setPlotObsCalc(null)
setPlotResiduos(null)
setPlotHistograma(null)
setPlotCook(null)
setPlotCorr(null)
setMapaHtml('')
setMapaChoices(['Visualização Padrão'])
setMapaVar('Visualização Padrão')
setCamposAvaliacao([])
valoresAvaliacaoRef.current = {}
setAvaliacaoFormVersion((prev) => prev + 1)
setConfirmarLimpezaAvaliacoes(false)
setResultadoAvaliacaoHtml('')
setBaseChoices([])
setBaseValue('')
setActiveInnerTab('mapa')
}
function applyExibicaoResponse(resp) {
setDados(resp.dados || null)
setEstatisticas(resp.estatisticas || null)
setEscalasHtml(resp.escalas_html || '')
setDadosTransformados(resp.dados_transformados || null)
setResumoHtml(resp.resumo_html || '')
setEquacoes(resp.equacoes || null)
setCoeficientes(resp.coeficientes || null)
setObsCalc(resp.obs_calc || null)
setPlotObsCalc(resp.grafico_obs_calc || null)
setPlotResiduos(resp.grafico_residuos || null)
setPlotHistograma(resp.grafico_histograma || null)
setPlotCook(resp.grafico_cook || null)
setPlotCorr(resp.grafico_correlacao || null)
setMapaHtml(resp.mapa_html || '')
setMapaChoices(resp.mapa_choices || ['Visualização Padrão'])
setMapaVar('Visualização Padrão')
setCamposAvaliacao(resp.campos_avaliacao || [])
const values = {}
;(resp.campos_avaliacao || []).forEach((campo) => {
values[campo.coluna] = ''
})
valoresAvaliacaoRef.current = values
setAvaliacaoFormVersion((prev) => prev + 1)
setConfirmarLimpezaAvaliacoes(false)
setResultadoAvaliacaoHtml('')
setBaseChoices([])
setBaseValue('')
}
async function withBusy(fn) {
setLoading(true)
setError('')
try {
await fn()
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
function formatarFonteRepositorio(fonte) {
if (!fonte || typeof fonte !== 'object') return ''
const provider = String(fonte.provider || '').toLowerCase()
if (provider === 'hf_dataset') {
const repo = String(fonte.repo_id || '').trim()
const revision = String(fonte.revision || '').trim()
const degradado = Boolean(fonte.degraded)
const sufixo = degradado ? ' (modo contingência)' : ''
return `Fonte: HF Dataset${repo ? ` (${repo})` : ''}${revision ? ` | revisão ${revision.slice(0, 8)}` : ''}${sufixo}`
}
return 'Fonte: pasta local'
}
function aplicarRespostaModelosRepositorio(resp) {
const modelos = Array.isArray(resp?.modelos) ? resp.modelos : []
setRepoModelos(modelos)
setRepoFonteModelos(formatarFonteRepositorio(resp?.fonte || null))
setRepoModeloSelecionado((prev) => {
const atual = String(prev || '')
if (atual && modelos.some((item) => String(item.id) === atual)) return atual
return ''
})
}
async function carregarModelosRepositorio() {
setRepoModelosLoading(true)
try {
const resp = await api.visualizacaoRepositorioModelos()
aplicarRespostaModelosRepositorio(resp)
} catch (err) {
setError(err.message || 'Falha ao carregar modelos do repositório.')
setRepoModelos([])
setRepoModeloSelecionado('')
setRepoFonteModelos('')
} finally {
setRepoModelosLoading(false)
}
}
useEffect(() => {
let ativo = true
if (!sessionId) return () => {
ativo = false
}
setRepoModelosLoading(true)
api.visualizacaoRepositorioModelos()
.then((resp) => {
if (!ativo) return
aplicarRespostaModelosRepositorio(resp)
})
.catch(() => {
if (!ativo) return
setRepoModelos([])
setRepoModeloSelecionado('')
setRepoFonteModelos('')
})
.finally(() => {
if (!ativo) return
setRepoModelosLoading(false)
})
return () => {
ativo = false
}
}, [sessionId])
async function onUploadModel(arquivo = null) {
const arquivoUpload = arquivo || uploadedFile
if (!sessionId || !arquivoUpload) return
setModeloLoadSource('upload')
await withBusy(async () => {
resetConteudoVisualizacao()
const uploadResp = await api.uploadVisualizacaoFile(sessionId, arquivoUpload)
setStatus(uploadResp.status || '')
setBadgeHtml(uploadResp.badge_html || '')
const exibirResp = await api.exibirVisualizacao(sessionId)
applyExibicaoResponse(exibirResp)
})
}
async function onCarregarModeloRepositorio() {
if (!sessionId || !repoModeloSelecionado) return
setModeloLoadSource('repo')
await withBusy(async () => {
resetConteudoVisualizacao()
const uploadResp = await api.visualizacaoRepositorioCarregar(sessionId, repoModeloSelecionado)
setStatus(uploadResp.status || '')
setBadgeHtml(uploadResp.badge_html || '')
const exibirResp = await api.exibirVisualizacao(sessionId)
applyExibicaoResponse(exibirResp)
setUploadedFile(null)
})
}
function onUploadInputChange(event) {
const input = event.target
const file = input.files?.[0] ?? null
setModeloLoadSource('upload')
setUploadedFile(file)
input.value = ''
if (!file || loading) return
void onUploadModel(file)
}
function onUploadDropZoneDragOver(event) {
event.preventDefault()
event.dataTransfer.dropEffect = 'copy'
setUploadDragOver(true)
}
function onUploadDropZoneDragLeave(event) {
event.preventDefault()
if (!event.currentTarget.contains(event.relatedTarget)) {
setUploadDragOver(false)
}
}
function onUploadDropZoneDrop(event) {
event.preventDefault()
setUploadDragOver(false)
const file = event.dataTransfer?.files?.[0]
if (!file || loading) return
setModeloLoadSource('upload')
setUploadedFile(file)
void onUploadModel(file)
}
async function onMapChange(value) {
setMapaVar(value)
if (!sessionId) return
await withBusy(async () => {
const resp = await api.updateVisualizacaoMap(sessionId, value)
setMapaHtml(resp.mapa_html || '')
})
}
async function onCalcularAvaliacao() {
if (!sessionId) return
await withBusy(async () => {
const resp = await api.evaluationCalculateViz(sessionId, valoresAvaliacaoRef.current, baseValue || null)
setResultadoAvaliacaoHtml(resp.resultado_html || '')
setBaseChoices(resp.base_choices || [])
setBaseValue(resp.base_value || '')
setConfirmarLimpezaAvaliacoes(false)
})
}
function onResetCamposAvaliacao() {
const limpo = {}
camposAvaliacao.forEach((campo) => {
limpo[campo.coluna] = ''
})
valoresAvaliacaoRef.current = limpo
setAvaliacaoFormVersion((prev) => prev + 1)
}
async function onClearAvaliacao() {
if (!sessionId) return
await withBusy(async () => {
const resp = await api.evaluationClearViz(sessionId)
setResultadoAvaliacaoHtml(resp.resultado_html || '')
setBaseChoices(resp.base_choices || [])
setBaseValue(resp.base_value || '')
setConfirmarLimpezaAvaliacoes(false)
})
}
async function onDeleteAvaliacao(indice) {
if (!sessionId) return
await withBusy(async () => {
const resp = await api.evaluationDeleteViz(sessionId, indice ? String(indice) : null, baseValue || null)
setResultadoAvaliacaoHtml(resp.resultado_html || '')
setBaseChoices(resp.base_choices || [])
setBaseValue(resp.base_value || '')
setConfirmarLimpezaAvaliacoes(false)
})
}
function onAvaliacaoResultadoClick(event) {
const ativarExclusao = event.target.closest('[data-avaliacao-delete-arm]')
if (ativarExclusao) {
const indice = ativarExclusao.getAttribute('data-avaliacao-delete-index')
if (!indice) return
const cell = ativarExclusao.closest('td')
const botaoConfirmar = cell?.querySelector(`[data-avaliacao-delete-confirm="${indice}"]`)
if (!botaoConfirmar) return
ativarExclusao.style.display = 'none'
botaoConfirmar.style.display = 'inline-block'
const timerKey = String(indice)
if (deleteConfirmTimersRef.current[timerKey]) {
clearTimeout(deleteConfirmTimersRef.current[timerKey])
}
deleteConfirmTimersRef.current[timerKey] = window.setTimeout(() => {
botaoConfirmar.style.display = 'none'
ativarExclusao.style.display = 'inline'
delete deleteConfirmTimersRef.current[timerKey]
}, 10000)
return
}
const confirmarExclusao = event.target.closest('[data-avaliacao-delete-confirm]')
if (!confirmarExclusao) return
const indice = confirmarExclusao.getAttribute('data-avaliacao-delete-confirm')
if (!indice) return
const timerKey = String(indice)
if (deleteConfirmTimersRef.current[timerKey]) {
clearTimeout(deleteConfirmTimersRef.current[timerKey])
delete deleteConfirmTimersRef.current[timerKey]
}
onDeleteAvaliacao(indice)
}
async function onBaseChange(value) {
setBaseValue(value)
if (!sessionId) return
await withBusy(async () => {
const resp = await api.evaluationBaseViz(sessionId, value)
setResultadoAvaliacaoHtml(resp.resultado_html || '')
})
}
async function onExportAvaliacoes() {
if (!sessionId) return
await withBusy(async () => {
const blob = await api.exportEvaluationViz(sessionId)
downloadBlob(blob, 'avaliacoes_visualizacao.xlsx')
})
}
async function onDownloadEquacao(mode) {
if (!sessionId || !mode) return
await withBusy(async () => {
const blob = await api.exportEquationViz(sessionId, mode)
const sufixo = String(mode) === 'excel_sab' ? 'estilo_sab' : 'excel'
downloadBlob(blob, `equacao_modelo_${sufixo}.xlsx`)
})
}
return (
<div className="tab-content">
<SectionBlock step="1" title="Carregar Modelo .dai" subtitle="Carregue o arquivo e o conteúdo será exibido automaticamente.">
{!modeloLoadSource ? (
<div className="model-source-choice-grid">
<button
type="button"
className="model-source-choice-btn model-source-choice-btn-primary"
onClick={() => setModeloLoadSource('repo')}
disabled={loading}
>
Carregar modelo do repositório
</button>
<button
type="button"
className="model-source-choice-btn model-source-choice-btn-secondary"
onClick={() => setModeloLoadSource('upload')}
disabled={loading}
>
Fazer upload de modelo
</button>
</div>
) : (
<div className="model-source-flow">
<div className="model-source-flow-head">
<button
type="button"
className="model-source-back-btn"
onClick={() => setModeloLoadSource('')}
disabled={loading}
>
Voltar
</button>
</div>
{modeloLoadSource === 'repo' ? (
<div className="row upload-repo-row">
<label className="upload-repo-field">
Modelo do repositório
<SinglePillAutocomplete
value={repoModeloSelecionado}
onChange={setRepoModeloSelecionado}
options={repoModeloOptions}
placeholder={repoModelosLoading ? 'Carregando lista...' : repoModeloOptions.length > 0 ? 'Digite para buscar modelo' : 'Nenhum modelo disponível'}
emptyMessage={repoModeloOptions.length > 0 ? 'Nenhum modelo encontrado.' : 'Nenhum modelo disponível.'}
loading={repoModelosLoading}
disabled={loading || repoModelosLoading || repoModeloOptions.length === 0}
/>
</label>
<div className="row compact upload-repo-actions">
<button type="button" onClick={onCarregarModeloRepositorio} disabled={loading || repoModelosLoading || !repoModeloSelecionado}>
Carregar do repositório
</button>
<button type="button" onClick={() => void carregarModelosRepositorio()} disabled={loading || repoModelosLoading}>
Atualizar lista
</button>
</div>
{repoFonteModelos ? <div className="section1-empty-hint">{repoFonteModelos}</div> : null}
</div>
) : null}
{modeloLoadSource === 'upload' ? (
<div
className={`upload-dropzone${uploadDragOver ? ' is-dragover' : ''}`}
onDragOver={onUploadDropZoneDragOver}
onDragEnter={onUploadDropZoneDragOver}
onDragLeave={onUploadDropZoneDragLeave}
onDrop={onUploadDropZoneDrop}
>
<input
ref={uploadInputRef}
type="file"
className="upload-hidden-input"
accept=".dai"
onChange={onUploadInputChange}
/>
<div className="row upload-dropzone-main">
<button
type="button"
className="btn-upload-select"
onClick={() => uploadInputRef.current?.click()}
disabled={loading}
>
Selecionar arquivo
</button>
</div>
<div className="upload-dropzone-hint">Ou arraste e solte aqui para carregar automaticamente.</div>
</div>
) : null}
</div>
)}
{status ? <div className="status-line">{status}</div> : null}
{badgeHtml ? <div className="upload-badge-block" dangerouslySetInnerHTML={{ __html: badgeHtml }} /> : null}
</SectionBlock>
{dados ? (
<SectionBlock step="2" title="Conteúdo do Modelo" subtitle="Carregue o modelo no topo e navegue pelas abas internas abaixo.">
<div className="inner-tabs" role="tablist" aria-label="Abas internas de visualização">
{INNER_TABS.map((tab) => (
<button
key={tab.key}
type="button"
className={activeInnerTab === tab.key ? 'inner-tab-pill active' : 'inner-tab-pill'}
onClick={() => setActiveInnerTab(tab.key)}
>
{tab.label}
</button>
))}
</div>
<div className="inner-tab-panel">
{activeInnerTab === 'mapa' ? (
<>
<div className="row compact visualizacao-mapa-controls">
<label>Variável no mapa</label>
<select value={mapaVar} onChange={(e) => onMapChange(e.target.value)}>
{mapaChoices.map((choice) => (
<option key={choice} value={choice}>{choice}</option>
))}
</select>
</div>
<MapFrame html={mapaHtml} />
</>
) : null}
{activeInnerTab === 'dados_mercado' ? (
<DataTable table={dados} maxHeight={620} />
) : null}
{activeInnerTab === 'metricas' ? (
<DataTable table={estatisticas} maxHeight={620} />
) : null}
{activeInnerTab === 'transformacoes' ? (
<>
<div dangerouslySetInnerHTML={{ __html: escalasHtml }} />
<h4 className="visualizacao-table-title">Dados com variáveis transformadas</h4>
<DataTable table={dadosTransformados} />
</>
) : null}
{activeInnerTab === 'resumo' ? (
<>
<div className="equation-formats-section">
<h4>Equações do Modelo</h4>
<EquationFormatsPanel
equacoes={equacoes}
onDownload={(mode) => void onDownloadEquacao(mode)}
disabled={loading}
/>
</div>
<div dangerouslySetInnerHTML={{ __html: resumoHtml }} />
</>
) : null}
{activeInnerTab === 'coeficientes' ? (
<DataTable table={coeficientes} maxHeight={620} />
) : null}
{activeInnerTab === 'obs_calc' ? (
<DataTable table={obsCalc} maxHeight={620} />
) : null}
{activeInnerTab === 'graficos' ? (
<>
<div className="plot-grid-2-fixed">
<PlotFigure figure={plotObsCalc} title="Obs x Calc" />
<PlotFigure figure={plotResiduos} title="Resíduos" />
<PlotFigure figure={plotHistograma} title="Histograma" />
<PlotFigure figure={plotCook} title="Cook" forceHideLegend />
</div>
<div className="plot-full-width">
<PlotFigure figure={plotCorr} title="Correlação" className="plot-correlation-card" />
</div>
</>
) : null}
{activeInnerTab === 'avaliacao' ? (
<>
<div className="equation-formats-section avaliacao-equacao-section">
<h5>Equação (estilo SAB)</h5>
<div className="equation-box equation-box-plain">
{equacoes?.excel_sab || 'Equação indisponível.'}
</div>
</div>
<div className="avaliacao-groups">
<div className="subpanel avaliacao-group">
<h4>Parâmetros</h4>
<div className="avaliacao-grid" key={`avaliacao-grid-viz-${avaliacaoFormVersion}`}>
{camposAvaliacao.map((campo) => (
<div key={`campo-${campo.coluna}`} className="avaliacao-card">
<label>{String(campo?.rotulo || campo?.coluna || '')}</label>
{campo.tipo === 'dicotomica' ? (
<select
defaultValue={String(valoresAvaliacaoRef.current[campo.coluna] ?? '')}
onChange={(e) => {
valoresAvaliacaoRef.current[campo.coluna] = e.target.value
}}
>
<option value="">Selecione</option>
{(campo.opcoes || [0, 1]).map((opcao) => (
<option key={`op-viz-${campo.coluna}-${opcao}`} value={String(opcao)}>
{opcao}
</option>
))}
</select>
) : (
<input
type="number"
defaultValue={valoresAvaliacaoRef.current[campo.coluna] ?? ''}
placeholder={campo.placeholder || ''}
onChange={(e) => {
valoresAvaliacaoRef.current[campo.coluna] = e.target.value
}}
/>
)}
</div>
))}
</div>
<div className="row-wrap avaliacao-actions-row">
<button onClick={onCalcularAvaliacao} disabled={loading}>Calcular</button>
<button onClick={onResetCamposAvaliacao} disabled={loading}>Resetar campos</button>
</div>
</div>
{temAvaliacoes ? (
<div className="subpanel avaliacao-group">
<h4>Avaliações</h4>
<div className="row avaliacao-base-row">
<label>Base comparação</label>
<select value={baseValue || ''} onChange={(e) => onBaseChange(e.target.value)}>
<option value={BASE_COMPARACAO_SEM_BASE}>Sem base</option>
{baseChoices.map((choice) => (
<option key={`base-${choice}`} value={choice}>{choice}</option>
))}
</select>
<button type="button" className="btn-avaliacao-export" onClick={onExportAvaliacoes} disabled={loading}>
Exportar avaliações
</button>
{!confirmarLimpezaAvaliacoes ? (
<button
type="button"
className="btn-avaliacao-clear"
onClick={() => setConfirmarLimpezaAvaliacoes(true)}
disabled={loading}
>
Limpar avaliações
</button>
) : (
<div className="avaliacao-clear-confirm avaliacao-clear-confirm-inline">
<span>Confirmar limpeza?</span>
<button type="button" className="btn-avaliacao-clear" onClick={onClearAvaliacao} disabled={loading}>
Confirmar
</button>
<button type="button" onClick={() => setConfirmarLimpezaAvaliacoes(false)} disabled={loading}>
Cancelar
</button>
</div>
)}
</div>
<div
className="avaliacao-resultado-box"
onClick={onAvaliacaoResultadoClick}
dangerouslySetInnerHTML={{ __html: resultadoAvaliacaoHtml }}
/>
</div>
) : null}
</div>
</>
) : null}
{activeInnerTab === 'avaliacao_massa' ? (
<div className="empty-box">Módulo em desenvolvimento.</div>
) : null}
</div>
</SectionBlock>
) : null}
<LoadingOverlay show={loading} label="Processando dados..." />
{error ? <div className="error-line">{error}</div> : null}
</div>
)
}