feat(equipe): implementar montagem de equipe interdisciplinar com PDF
- Adicionar seleção múltipla de consultores entre diferentes buscas - Criar endpoint POST /api/v1/equipe/pdf para gerar documento da equipe - Implementar template HTML profissional para PDF da equipe - Exibir estatísticas: total, ativos, coordenadores, premiados, IES - Persistir seleção ao trocar termos de busca (equipe interdisciplinar) - Mostrar temas combinados na barra flutuante e no PDF - Corrigir renderização de emojis no PDF (substituir por texto) - Melhorar contraste da cor do ranking no frontend (roxo → ciano) - Corrigir validação Pydantic para ignorar campos extras do .env
This commit is contained in:
@@ -32,6 +32,10 @@
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(96, 165, 250, 0.05));
|
||||
}
|
||||
|
||||
.sugerir-header-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sugerir-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
@@ -39,6 +43,13 @@
|
||||
color: var(--accent-2);
|
||||
}
|
||||
|
||||
.sugerir-subtitle {
|
||||
margin: 0.25rem 0 0 0;
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.sugerir-close {
|
||||
background: none;
|
||||
border: none;
|
||||
@@ -173,13 +184,25 @@
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sugerir-resultados h3 {
|
||||
.sugerir-resultados-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid var(--stroke);
|
||||
}
|
||||
|
||||
.sugerir-resultados h3 {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--silver);
|
||||
border-bottom: 1px solid var(--stroke);
|
||||
}
|
||||
|
||||
.sugerir-hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.sugerir-lista {
|
||||
@@ -192,16 +215,56 @@
|
||||
padding: 1rem;
|
||||
margin: 0.5rem;
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid var(--stroke);
|
||||
border: 2px solid var(--stroke);
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, border-color 0.2s, transform 0.2s;
|
||||
transition: background 0.2s, border-color 0.2s, transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.sugerir-item:hover {
|
||||
background: rgba(99, 102, 241, 0.1);
|
||||
border-color: rgba(99, 102, 241, 0.3);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.sugerir-item.selecionado {
|
||||
background: rgba(16, 185, 129, 0.1);
|
||||
border-color: rgba(16, 185, 129, 0.5);
|
||||
box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.sugerir-item.selecionado:hover {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
}
|
||||
|
||||
.sugerir-checkbox-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.sugerir-checkbox-item input[type="checkbox"] {
|
||||
width: 1.2rem;
|
||||
height: 1.2rem;
|
||||
accent-color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-ver-ranking {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid var(--stroke);
|
||||
color: var(--silver);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.2s;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.btn-ver-ranking:hover {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.sugerir-item-header {
|
||||
@@ -262,6 +325,72 @@
|
||||
border: 1px solid rgba(107, 114, 128, 0.4);
|
||||
}
|
||||
|
||||
.sugerir-item-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.stat-ranking {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.8rem;
|
||||
color: #22d3ee;
|
||||
font-weight: 600;
|
||||
background: rgba(34, 211, 238, 0.15);
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(34, 211, 238, 0.3);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.stat-pontuacao {
|
||||
font-size: 0.75rem;
|
||||
color: #fcd34d;
|
||||
font-weight: 600;
|
||||
background: rgba(234, 179, 8, 0.15);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(234, 179, 8, 0.3);
|
||||
}
|
||||
|
||||
.sugerir-motivos {
|
||||
background: rgba(16, 185, 129, 0.05);
|
||||
border: 1px solid rgba(16, 185, 129, 0.15);
|
||||
border-radius: 8px;
|
||||
padding: 0.6rem 0.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.motivos-label {
|
||||
font-size: 0.7rem;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
display: block;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.motivos-lista {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.tag.motivo {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: #6ee7b7;
|
||||
border: 1px solid rgba(16, 185, 129, 0.3);
|
||||
font-size: 0.7rem;
|
||||
padding: 0.2rem 0.5rem;
|
||||
}
|
||||
|
||||
.sugerir-item-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -271,8 +400,11 @@
|
||||
|
||||
.sugerir-ies {
|
||||
font-size: 0.8rem;
|
||||
color: var(--muted);
|
||||
color: var(--silver);
|
||||
font-weight: 500;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.sugerir-areas {
|
||||
@@ -316,6 +448,126 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.equipe-flutuante {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(135deg, rgba(16, 185, 129, 0.95), rgba(5, 150, 105, 0.95));
|
||||
backdrop-filter: blur(8px);
|
||||
padding: 1rem 1.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.equipe-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.equipe-count {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.equipe-nomes {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.equipe-nome {
|
||||
font-size: 0.8rem;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.equipe-acoes {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.btn-limpar {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
color: white;
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-limpar:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.btn-gerar-pdf {
|
||||
background: white;
|
||||
border: none;
|
||||
color: #059669;
|
||||
padding: 0.6rem 1.25rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btn-gerar-pdf:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.btn-gerar-pdf:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.equipe-header-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.equipe-interdisciplinar {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
color: white;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.equipe-temas {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.equipe-tema {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.sugerir-modal {
|
||||
max-height: 95vh;
|
||||
@@ -334,4 +586,18 @@
|
||||
width: 100%;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.equipe-flutuante {
|
||||
flex-direction: column;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.equipe-acoes {
|
||||
width: 100%;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.btn-gerar-pdf {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
|
||||
const [loadingAreas, setLoadingAreas] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [buscaRealizada, setBuscaRealizada] = useState(false);
|
||||
const [selecionados, setSelecionados] = useState([]);
|
||||
const [gerandoPdf, setGerandoPdf] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const carregarAreas = async () => {
|
||||
@@ -51,6 +53,17 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSelecionado = (consultor, e) => {
|
||||
e.stopPropagation();
|
||||
setSelecionados((prev) => {
|
||||
const existe = prev.find((c) => c.id_pessoa === consultor.id_pessoa);
|
||||
if (existe) {
|
||||
return prev.filter((c) => c.id_pessoa !== consultor.id_pessoa);
|
||||
}
|
||||
return [...prev, { ...consultor, _busca_tema: tema, _busca_area: areaAvaliacao }];
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickConsultor = (consultor) => {
|
||||
if (onSelectConsultor) {
|
||||
onSelectConsultor(consultor.id_pessoa);
|
||||
@@ -58,17 +71,81 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleGerarPdf = async () => {
|
||||
if (selecionados.length === 0) return;
|
||||
|
||||
try {
|
||||
setGerandoPdf(true);
|
||||
setError(null);
|
||||
|
||||
const temasUnicos = [...new Set(selecionados.map((c) => c._busca_tema).filter(Boolean))];
|
||||
const areasUnicas = [...new Set(selecionados.map((c) => c._busca_area).filter(Boolean))];
|
||||
const temasCombinados = temasUnicos.length > 0 ? temasUnicos.join(' + ') : 'Equipe Selecionada';
|
||||
const areasCombinadas = areasUnicas.length > 0 ? areasUnicas.join(', ') : null;
|
||||
|
||||
const response = await fetch('/api/v1/equipe/pdf', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tema: temasCombinados,
|
||||
area_avaliacao: areasCombinadas,
|
||||
consultores: selecionados.map((c) => ({
|
||||
id_pessoa: c.id_pessoa,
|
||||
nome: c.nome,
|
||||
ies: c.ies,
|
||||
areas_avaliacao: c.areas_avaliacao,
|
||||
areas_conhecimento: c.areas_conhecimento,
|
||||
linhas_pesquisa: c.linhas_pesquisa,
|
||||
situacao: c.situacao,
|
||||
foi_coordenador: c.foi_coordenador,
|
||||
foi_premiado: c.foi_premiado,
|
||||
posicao_ranking: c.posicao_ranking,
|
||||
pontuacao_ranking: c.pontuacao_ranking,
|
||||
motivos_match: c.motivos_match,
|
||||
})),
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Erro ao gerar PDF');
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const nomeArquivo = temasUnicos.length > 1
|
||||
? `equipe_interdisciplinar_${new Date().toISOString().slice(0, 10)}.pdf`
|
||||
: `equipe_${temasCombinados.replace(/\s+/g, '_')}_${new Date().toISOString().slice(0, 10)}.pdf`;
|
||||
a.download = nomeArquivo;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
} catch (err) {
|
||||
console.error('Erro ao gerar PDF:', err);
|
||||
setError('Erro ao gerar PDF da equipe. Tente novamente.');
|
||||
} finally {
|
||||
setGerandoPdf(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const isSelecionado = (id) => selecionados.some((c) => c.id_pessoa === id);
|
||||
|
||||
return (
|
||||
<div className="sugerir-overlay" onClick={onClose} onKeyDown={handleKeyDown}>
|
||||
<div className="sugerir-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="sugerir-header">
|
||||
<h2>Sugerir Consultores por Tema</h2>
|
||||
<div className="sugerir-header-content">
|
||||
<h2>Montar Equipe de Consultores</h2>
|
||||
<p className="sugerir-subtitle">Selecione consultores para gerar documento da equipe</p>
|
||||
</div>
|
||||
<button className="sugerir-close" onClick={onClose}>×</button>
|
||||
</div>
|
||||
|
||||
@@ -140,21 +217,33 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
|
||||
|
||||
{buscaRealizada && (
|
||||
<div className="sugerir-resultados">
|
||||
<h3>
|
||||
{consultores.length > 0
|
||||
? `${consultores.length} consultores encontrados`
|
||||
: 'Nenhum consultor encontrado'}
|
||||
</h3>
|
||||
<div className="sugerir-resultados-header">
|
||||
<h3>
|
||||
{consultores.length > 0
|
||||
? `${consultores.length} consultores para "${tema}"`
|
||||
: 'Nenhum consultor encontrado'}
|
||||
</h3>
|
||||
{consultores.length > 0 && (
|
||||
<span className="sugerir-hint">Selecione os consultores para a equipe</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{consultores.length > 0 && (
|
||||
<div className="sugerir-lista">
|
||||
{consultores.map((c, index) => (
|
||||
<div
|
||||
key={c.id_pessoa}
|
||||
className="sugerir-item"
|
||||
onClick={() => handleClickConsultor(c)}
|
||||
className={`sugerir-item ${isSelecionado(c.id_pessoa) ? 'selecionado' : ''}`}
|
||||
onClick={(e) => toggleSelecionado(c, e)}
|
||||
>
|
||||
<div className="sugerir-item-header">
|
||||
<label className="sugerir-checkbox-item" onClick={(e) => e.stopPropagation()}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelecionado(c.id_pessoa)}
|
||||
onChange={(e) => toggleSelecionado(c, e)}
|
||||
/>
|
||||
</label>
|
||||
<span className="sugerir-rank">#{index + 1}</span>
|
||||
<span className="sugerir-nome">{c.nome}</span>
|
||||
<div className="sugerir-badges">
|
||||
@@ -166,9 +255,45 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
|
||||
<span className="badge inativo">Inativo</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="btn-ver-ranking"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleClickConsultor(c);
|
||||
}}
|
||||
title="Ver no ranking"
|
||||
>
|
||||
→
|
||||
</button>
|
||||
</div>
|
||||
<div className="sugerir-item-details">
|
||||
|
||||
<div className="sugerir-item-stats">
|
||||
{c.posicao_ranking && (
|
||||
<span className="stat-ranking" title="Posicao no ranking geral">
|
||||
<span className="stat-icon">📊</span>
|
||||
#{c.posicao_ranking.toLocaleString()} no ranking
|
||||
</span>
|
||||
)}
|
||||
{c.pontuacao_ranking > 0 && (
|
||||
<span className="stat-pontuacao" title="Pontuacao no ranking">
|
||||
{c.pontuacao_ranking.toFixed(0)} pts
|
||||
</span>
|
||||
)}
|
||||
{c.ies && <span className="sugerir-ies">{c.ies}</span>}
|
||||
</div>
|
||||
|
||||
{c.motivos_match && c.motivos_match.length > 0 && (
|
||||
<div className="sugerir-motivos">
|
||||
<span className="motivos-label">Por que este consultor:</span>
|
||||
<div className="motivos-lista">
|
||||
{c.motivos_match.map((motivo, idx) => (
|
||||
<span key={idx} className="tag motivo">{motivo}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="sugerir-item-details">
|
||||
{c.areas_avaliacao.length > 0 && (
|
||||
<div className="sugerir-areas">
|
||||
{c.areas_avaliacao.slice(0, 3).map((area) => (
|
||||
@@ -195,6 +320,50 @@ const SugerirConsultores = ({ onClose, onSelectConsultor }) => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selecionados.length > 0 && (
|
||||
<div className="equipe-flutuante">
|
||||
<div className="equipe-info">
|
||||
<div className="equipe-header-row">
|
||||
<span className="equipe-count">{selecionados.length} selecionado{selecionados.length > 1 ? 's' : ''}</span>
|
||||
{(() => {
|
||||
const temas = [...new Set(selecionados.map((c) => c._busca_tema).filter(Boolean))];
|
||||
return temas.length > 1 && (
|
||||
<span className="equipe-interdisciplinar">Interdisciplinar</span>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="equipe-temas">
|
||||
{(() => {
|
||||
const temas = [...new Set(selecionados.map((c) => c._busca_tema).filter(Boolean))];
|
||||
return temas.map((t) => (
|
||||
<span key={t} className="equipe-tema">{t}</span>
|
||||
));
|
||||
})()}
|
||||
</div>
|
||||
<div className="equipe-nomes">
|
||||
{selecionados.slice(0, 5).map((c) => (
|
||||
<span key={c.id_pessoa} className="equipe-nome">{c.nome.split(' ')[0]}</span>
|
||||
))}
|
||||
{selecionados.length > 5 && (
|
||||
<span className="equipe-nome">+{selecionados.length - 5}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="equipe-acoes">
|
||||
<button className="btn-limpar" onClick={() => setSelecionados([])}>
|
||||
Limpar
|
||||
</button>
|
||||
<button
|
||||
className="btn-gerar-pdf"
|
||||
onClick={handleGerarPdf}
|
||||
disabled={gerandoPdf}
|
||||
>
|
||||
{gerandoPdf ? 'Gerando...' : 'Gerar PDF da Equipe'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user