mesa-react / frontend /src /components /VisualizacaoTab.jsx
Guilherme Silberfarb Costa
update a lot of things
de0fbb8
raw
history blame
16.6 kB
import React, { useRef, useState } from 'react'
import { api, downloadBlob } from '../api'
import DataTable from './DataTable'
import LoadingOverlay from './LoadingOverlay'
import MapFrame from './MapFrame'
import PlotFigure from './PlotFigure'
import SectionBlock from './SectionBlock'
const INNER_TABS = [
{ key: 'mapa', label: 'Mapa' },
{ key: 'dados', label: 'Dados' },
{ 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' },
]
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 [dados, setDados] = useState(null)
const [estatisticas, setEstatisticas] = useState(null)
const [escalasHtml, setEscalasHtml] = useState('')
const [dadosTransformados, setDadosTransformados] = useState(null)
const [resumoHtml, setResumoHtml] = useState('')
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 [resultadoAvaliacaoHtml, setResultadoAvaliacaoHtml] = useState('')
const [baseChoices, setBaseChoices] = useState([])
const [baseValue, setBaseValue] = useState('')
const [activeInnerTab, setActiveInnerTab] = useState('mapa')
const deleteConfirmTimersRef = useRef({})
const uploadInputRef = useRef(null)
function resetConteudoVisualizacao() {
setDados(null)
setEstatisticas(null)
setEscalasHtml('')
setDadosTransformados(null)
setResumoHtml('')
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)
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 || '')
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)
setResultadoAvaliacaoHtml('')
setBaseChoices([])
setBaseValue('')
}
async function withBusy(fn) {
setLoading(true)
setError('')
try {
await fn()
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
async function onUploadModel(arquivo = null) {
const arquivoUpload = arquivo || uploadedFile
if (!sessionId || !arquivoUpload) return
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)
})
}
function onUploadInputChange(event) {
const input = event.target
const file = input.files?.[0] ?? null
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
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 || '')
})
}
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 || '')
const limpo = {}
camposAvaliacao.forEach((campo) => {
limpo[campo.coluna] = ''
})
valoresAvaliacaoRef.current = limpo
setAvaliacaoFormVersion((prev) => prev + 1)
})
}
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 || '')
})
}
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')
})
}
return (
<div className="tab-content">
<SectionBlock step="1" title="Carregar Modelo .dai" subtitle="Carregue o arquivo e o conteúdo será exibido automaticamente.">
<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 className="upload-dropzone-file">
{uploadedFile ? `Arquivo selecionado: ${uploadedFile.name}` : 'Nenhum arquivo selecionado.'}
</div>
</div>
{status ? <div className="status-line">{status}</div> : null}
{badgeHtml ? <div 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">
<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' ? (
<div className="two-col">
<div className="pane">
<h4>Dados</h4>
<DataTable table={dados} maxHeight={520} />
</div>
<div className="pane">
<h4>Estatísticas</h4>
<DataTable table={estatisticas} maxHeight={520} />
</div>
</div>
) : null}
{activeInnerTab === 'transformacoes' ? (
<>
<div dangerouslySetInnerHTML={{ __html: escalasHtml }} />
<DataTable table={dadosTransformados} />
</>
) : null}
{activeInnerTab === 'resumo' ? (
<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="avaliacao-grid" key={`avaliacao-grid-viz-${avaliacaoFormVersion}`}>
{camposAvaliacao.map((campo) => (
<div key={`campo-${campo.coluna}`} className="avaliacao-card">
<label>{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={onClearAvaliacao} disabled={loading}>Limpar</button>
<button onClick={onExportAvaliacoes} disabled={loading}>Exportar avaliações</button>
</div>
<div className="row avaliacao-base-row">
<label>Base comparação</label>
<select value={baseValue || ''} onChange={(e) => onBaseChange(e.target.value)}>
<option value="">Selecione</option>
{baseChoices.map((choice) => (
<option key={`base-${choice}`} value={choice}>{choice}</option>
))}
</select>
</div>
<div
className="avaliacao-resultado-box"
onClick={onAvaliacaoResultadoClick}
dangerouslySetInnerHTML={{ __html: resultadoAvaliacaoHtml }}
/>
</>
) : 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>
)
}