diff --git a/frontend/src/App.css b/frontend/src/App.css index 41864a7..d934a0f 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -177,6 +177,7 @@ display: flex; align-items: center; gap: 0.5rem; + margin-left: auto; } .pagination button { diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8e2795c..86c40ef 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -3,6 +3,7 @@ import Header from './components/Header'; import ConsultorCard from './components/ConsultorCard'; import CompararModal from './components/CompararModal'; import FiltroSelos from './components/FiltroSelos'; +import FiltroAtivo from './components/FiltroAtivo'; import SugerirConsultores from './components/SugerirConsultores'; import ExportProgress from './components/ExportProgress'; import { rankingService } from './services/api'; @@ -25,11 +26,13 @@ function App() { const [selecionados, setSelecionados] = useState([]); const [modalAberto, setModalAberto] = useState(false); const [filtroSelos, setFiltroSelos] = useState([]); + const [filtroAtivo, setFiltroAtivo] = useState(null); 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 loadingRef = useRef(false); const toggleSelecionado = (consultor) => { setSelecionados((prev) => { @@ -76,6 +79,7 @@ function App() { await rankingService.downloadRankingExcel( filtroSelos, + filtroAtivo, (progress) => { setExportStatus('downloading'); setExportProgress(progress); @@ -105,8 +109,13 @@ function App() { }; useEffect(() => { + const requestKey = `${page}-${pageSize}-${filtroSelos.join(',')}-${filtroAtivo}`; + if (loadingRef.current === requestKey) { + return; + } + loadingRef.current = requestKey; loadRanking(); - }, [page, pageSize, filtroSelos]); + }, [page, pageSize, filtroSelos, filtroAtivo]); const loadRanking = async (retryCount = 0) => { const MAX_RETRIES = 10; @@ -116,7 +125,7 @@ function App() { setLoading(true); setError(null); setProcessMessage(''); - const response = await rankingService.getRanking(page, pageSize, filtroSelos); + const response = await rankingService.getRanking(page, pageSize, filtroSelos, filtroAtivo); setConsultores(response.consultores); setTotal(response.total); setTotalPages(response.total_pages || 0); @@ -169,7 +178,7 @@ function App() { throw new Error('Timeout: processamento demorou mais que 45 minutos'); } - const response = await rankingService.getRanking(page, pageSize, filtroSelos); + const response = await rankingService.getRanking(page, pageSize, filtroSelos, filtroAtivo); setConsultores(response.consultores); setTotal(response.total); setTotalPages(response.total_pages || 0); @@ -260,6 +269,11 @@ function App() { onChange={(selos) => { setFiltroSelos(selos); setPage(1); }} /> + { setFiltroAtivo(valor); setPage(1); }} + /> + @@ -273,6 +287,14 @@ function App() { {exportando ? 'Exportando...' : '📊 Exportar Excel'} +
+ + + {page} / {totalPages || '?'} + + +
+
- -
- - - {page} / {totalPages || '?'} - - -
diff --git a/frontend/src/components/FiltroAtivo.css b/frontend/src/components/FiltroAtivo.css new file mode 100644 index 0000000..7b8fca1 --- /dev/null +++ b/frontend/src/components/FiltroAtivo.css @@ -0,0 +1,153 @@ +.filtro-ativo { + position: relative; +} + +.filtro-ativo-trigger { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.9rem; + background: rgba(255, 255, 255, 0.06); + border: 1px solid var(--stroke); + border-radius: 8px; + color: var(--text); + font-size: 0.9rem; + cursor: pointer; + transition: all 200ms ease; + white-space: nowrap; +} + +.filtro-ativo-trigger:hover { + border-color: var(--accent-2); + background: rgba(255, 255, 255, 0.08); +} + +.filtro-ativo-trigger.ativo { + border-color: var(--accent); + background: rgba(79, 70, 229, 0.15); +} + +.filtro-ativo .filtro-icone { + font-size: 1rem; +} + +.filtro-ativo .filtro-label { + font-weight: 500; +} + +.filtro-ativo .filtro-seta { + font-size: 0.7rem; + color: var(--muted); + transition: transform 200ms ease; +} + +.filtro-ativo .filtro-seta.aberto { + transform: rotate(180deg); +} + +.filtro-ativo .filtro-limpar { + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + font-size: 0.7rem; + background: rgba(255, 59, 48, 0.2); + color: #ff6b6b; + border-radius: 50%; + margin-left: 0.25rem; + transition: all 150ms ease; +} + +.filtro-ativo .filtro-limpar:hover { + background: rgba(255, 59, 48, 0.4); +} + +.filtro-ativo-dropdown { + position: absolute; + top: calc(100% + 8px); + left: 0; + min-width: 180px; + background: linear-gradient(165deg, rgba(15, 23, 42, 0.98), rgba(30, 41, 59, 0.98)); + border: 1px solid var(--stroke); + border-radius: 12px; + box-shadow: 0 15px 40px rgba(0, 0, 0, 0.4), 0 0 20px rgba(79, 70, 229, 0.1); + z-index: 100; + backdrop-filter: blur(12px); + animation: dropdownSlide 200ms ease; + overflow: hidden; +} + +@keyframes dropdownSlide { + from { + opacity: 0; + transform: translateY(-8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.filtro-ativo-header { + display: flex; + align-items: center; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--stroke); + font-size: 0.85rem; + color: var(--muted); +} + +.filtro-ativo-opcoes { + padding: 0.5rem; +} + +.filtro-opcao-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.6rem 0.75rem; + background: transparent; + border: 1px solid transparent; + border-radius: 6px; + cursor: pointer; + transition: all 150ms ease; + font-size: 0.9rem; + width: 100%; +} + +.filtro-opcao-item:hover { + background: rgba(255, 255, 255, 0.08); + border-color: var(--stroke); +} + +.filtro-opcao-item.selecionado { + background: rgba(79, 70, 229, 0.2); + border-color: var(--accent); +} + +.filtro-opcao-item .opcao-icone { + font-size: 1rem; +} + +.filtro-opcao-item .opcao-label { + color: var(--text); + flex: 1; +} + +.filtro-opcao-item .opcao-check { + color: var(--accent); + font-weight: bold; +} + +@media (max-width: 480px) { + .filtro-ativo-dropdown { + position: fixed; + top: auto; + bottom: 0; + left: 0; + right: 0; + min-width: 100%; + border-radius: 16px 16px 0 0; + } +} diff --git a/frontend/src/components/FiltroAtivo.jsx b/frontend/src/components/FiltroAtivo.jsx new file mode 100644 index 0000000..fb594d3 --- /dev/null +++ b/frontend/src/components/FiltroAtivo.jsx @@ -0,0 +1,78 @@ +import { useState, useRef, useEffect } from 'react'; +import './FiltroAtivo.css'; + +const OPCOES = [ + { value: null, label: 'Todos', icone: '👥' }, + { value: true, label: 'Ativos', icone: '✅' }, + { value: false, label: 'Inativos', icone: '📋' }, +]; + +function FiltroAtivo({ valor, onChange }) { + const [aberto, setAberto] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const handleClickOutside = (e) => { + if (ref.current && !ref.current.contains(e.target)) { + setAberto(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const selecionarOpcao = (novoValor) => { + onChange(novoValor); + setAberto(false); + }; + + const limparFiltro = (e) => { + e.stopPropagation(); + onChange(null); + }; + + const opcaoAtual = OPCOES.find((o) => o.value === valor) || OPCOES[0]; + const filtroAtivo = valor !== null; + + return ( +
+ + + {aberto && ( +
+
+ Filtrar por status +
+ +
+ {OPCOES.map((opcao) => ( + + ))} +
+
+ )} +
+ ); +} + +export default FiltroAtivo; diff --git a/frontend/src/components/RawDataModal.jsx b/frontend/src/components/RawDataModal.jsx index 808d8aa..d6fbedd 100644 --- a/frontend/src/components/RawDataModal.jsx +++ b/frontend/src/components/RawDataModal.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import ReactDOM from 'react-dom'; import { rankingService } from '../services/api'; import './RawDataModal.css'; @@ -473,8 +473,13 @@ const RawDataModal = ({ idPessoa, nome, onClose }) => { const [filterType, setFilterType] = useState('all'); const [copyFeedback, setCopyFeedback] = useState(false); const [downloadingPDF, setDownloadingPDF] = useState(false); + const fetchedRef = useRef(null); useEffect(() => { + if (fetchedRef.current === idPessoa) { + return; + } + fetchedRef.current = idPessoa; const fetchData = async () => { try { setLoading(true); diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 69d04cf..948b6c3 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -9,7 +9,7 @@ const api = axios.create({ }); export const rankingService = { - async getRanking(page = 1, size = 100, selos = []) { + async getRanking(page = 1, size = 100, selos = [], ativo = null) { const params = { page, size }; if (selos && selos.length > 0) { const normalizados = selos @@ -19,6 +19,9 @@ export const rankingService = { params.selos = normalizados.join(','); } } + if (ativo !== null) { + params.ativo = ativo; + } const response = await api.get('/ranking/paginado', { params }); const data = response.data; @@ -187,7 +190,7 @@ export const rankingService = { return response.data; }, - async getExportInfo(selos = []) { + async getExportInfo(selos = [], ativo = null) { const params = {}; if (selos && selos.length > 0) { const normalizados = selos @@ -197,11 +200,14 @@ export const rankingService = { params.selos = normalizados.join(','); } } + if (ativo !== null) { + params.ativo = ativo; + } const response = await api.get('/ranking/exportar/info', { params }); return response.data; }, - async downloadRankingExcel(selos = [], onProgress = null, abortController = null) { + async downloadRankingExcel(selos = [], ativo = null, onProgress = null, abortController = null) { const params = {}; if (selos && selos.length > 0) { const normalizados = selos @@ -211,6 +217,9 @@ export const rankingService = { params.selos = normalizados.join(','); } } + if (ativo !== null) { + params.ativo = ativo; + } const config = { params,