Spaces:
Running
Running
| 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> | |
| ) | |
| } | |