feat(export): adicionar exportação Excel do ranking com barra de progresso

- Novo endpoint GET /api/v1/ranking/exportar/excel
- Exporta apenas consultores com pontuação > 0
- Usa xlsxwriter para geração rápida (~40s para 300k registros)
- Layout profissional com formatação, filtros e cores condicionais
- Barra de progresso real no frontend com dois estados:
  - Animação indeterminada durante geração no servidor
  - Progresso real durante download do arquivo
- Botão de exportação integrado ao layout do sistema
- Suporte a cancelamento da exportação
This commit is contained in:
Frederico Castro
2025-12-28 01:45:52 -03:00
parent 015c8f5741
commit 840934a187
9 changed files with 822 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ import ConsultorCard from './components/ConsultorCard';
import CompararModal from './components/CompararModal';
import FiltroSelos from './components/FiltroSelos';
import SugerirConsultores from './components/SugerirConsultores';
import ExportProgress from './components/ExportProgress';
import { rankingService } from './services/api';
import './App.css';
@@ -25,6 +26,10 @@ function App() {
const [modalAberto, setModalAberto] = useState(false);
const [filtroSelos, setFiltroSelos] = useState([]);
const [sugerirAberto, setSugerirAberto] = useState(false);
const [exportando, setExportando] = useState(false);
const [exportProgress, setExportProgress] = useState({ loaded: 0, total: 0, percent: 0 });
const [exportStatus, setExportStatus] = useState('preparing');
const abortControllerRef = useRef(null);
const toggleSelecionado = (consultor) => {
setSelecionados((prev) => {
@@ -60,6 +65,45 @@ function App() {
}
};
const handleExportarExcel = async () => {
if (exportando) return;
try {
setExportando(true);
setExportStatus('preparing');
setExportProgress({ loaded: 0, total: 0, percent: 0 });
abortControllerRef.current = new AbortController();
await rankingService.downloadRankingExcel(
filtroSelos,
(progress) => {
setExportStatus('downloading');
setExportProgress(progress);
},
abortControllerRef.current
);
setExportStatus('complete');
setTimeout(() => setExportando(false), 1000);
} catch (err) {
if (err.name === 'CanceledError' || err.code === 'ERR_CANCELED') {
console.log('Exportação cancelada pelo usuário');
} else {
console.error('Erro ao exportar Excel:', err);
setExportStatus('error');
setTimeout(() => setExportando(false), 2000);
}
}
};
const handleCancelExport = () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
}
setExportando(false);
};
useEffect(() => {
loadRanking();
}, [page, pageSize, filtroSelos]);
@@ -220,6 +264,15 @@ function App() {
Sugerir por Tema
</button>
<button
className="btn-exportar"
onClick={handleExportarExcel}
disabled={exportando || loading}
title="Exportar ranking para Excel (apenas consultores com pontuação)"
>
{exportando ? 'Exportando...' : '📊 Exportar Excel'}
</button>
<form className="search-box" onSubmit={handleSubmitBuscar}>
<input
type="text"
@@ -290,6 +343,14 @@ function App() {
/>
)}
{exportando && (
<ExportProgress
progress={exportProgress}
status={exportStatus}
onCancel={handleCancelExport}
/>
)}
<footer>
<p>Dados: ATUACAPES (Elasticsearch) + Oracle</p>
<p>Clique em qualquer consultor para ver detalhes</p>