diff --git a/.gitignore b/.gitignore
index 1b7f05c..248b4cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,3 +31,9 @@ node_modules/
*.csv
backend/logs/
.cache/
+
+# LibreOffice profile (local)
+.lo_profile/
+
+# XDG config (local)
+.xdg/
diff --git a/.mcp.json b/.mcp.json
new file mode 100644
index 0000000..2b6029b
--- /dev/null
+++ b/.mcp.json
@@ -0,0 +1,15 @@
+{
+ "mcpServers": {
+ "atuacapes": {
+ "command": "/home/fred/MCPs-Central/mcp-atuacapes/venv/bin/python",
+ "args": ["-m", "atuacapes_mcp.server"],
+ "cwd": "/home/fred/MCPs-Central/mcp-atuacapes",
+ "env": {
+ "ES_HOST": "http://elastic-atuacapes.hom.capes.gov.br:9200",
+ "ES_USER": "admin-atuacapes",
+ "ES_PASS": "O}!S0bj%FhJ:",
+ "ES_INDEX": "atuacapes"
+ }
+ }
+ }
+}
diff --git a/backend/src/application/services/pdf_service.py b/backend/src/application/services/pdf_service.py
index f730b65..176670b 100644
--- a/backend/src/application/services/pdf_service.py
+++ b/backend/src/application/services/pdf_service.py
@@ -15,6 +15,7 @@ class PDFService:
)
self.env.filters['format_date'] = self._format_date
self.env.filters['format_date_short'] = self._format_date_short
+ self.env.filters['sort_by_date'] = self._sort_by_date
self.template = self.env.get_template("ficha_consultor.html")
def _format_date(self, date_str: str) -> str:
@@ -46,6 +47,68 @@ class PDFService:
except (ValueError, AttributeError, IndexError):
return str(date_str)[:10] if date_str else "-"
+ def _sort_by_date(self, items, *fields):
+ if not items or not fields:
+ return items
+
+ def extract_field(value, field):
+ for part in field.split("."):
+ if value is None:
+ return None
+ if isinstance(value, DictWrapper):
+ value = value.get(part)
+ elif isinstance(value, dict):
+ value = value.get(part)
+ else:
+ value = getattr(value, part, None)
+ return value
+
+ def parse_date(value):
+ if value is None:
+ return None
+ if isinstance(value, datetime):
+ return value
+ if isinstance(value, (int, float)):
+ year = int(value)
+ return datetime(year, 1, 1)
+ if not isinstance(value, str):
+ return None
+ text = value.strip()
+ if not text:
+ return None
+ if text.isdigit() and len(text) == 4:
+ return datetime(int(text), 1, 1)
+ if "/" in text:
+ parts = text.split("/")
+ try:
+ if len(parts) == 3:
+ day = int(parts[0])
+ month = int(parts[1])
+ year = int(parts[2])
+ return datetime(year, month, day)
+ if len(parts) == 2:
+ month = int(parts[0])
+ year = int(parts[1])
+ return datetime(year, month, 1)
+ except ValueError:
+ return None
+ try:
+ return datetime.fromisoformat(text.replace("Z", "+00:00"))
+ except ValueError:
+ return None
+
+ def sort_key(item):
+ value = None
+ for field in fields:
+ value = parse_date(extract_field(item, field))
+ if value is not None:
+ break
+ if value is None:
+ return (0, datetime.min)
+ return (1, value)
+
+ return sorted(items, key=sort_key, reverse=True)
+
def _consultor_to_dict(self, consultor: Any) -> Dict:
if is_dataclass(consultor) and not isinstance(consultor, type):
return asdict(consultor)
diff --git a/backend/src/infrastructure/pdf/templates/ficha_consultor.html b/backend/src/infrastructure/pdf/templates/ficha_consultor.html
index 05ab016..445415e 100644
--- a/backend/src/infrastructure/pdf/templates/ficha_consultor.html
+++ b/backend/src/infrastructure/pdf/templates/ficha_consultor.html
@@ -195,7 +195,7 @@
- {% for e in emails %}
+ {% for e in emails|sort_by_date('ultimaAlteracao') %}
| {{ e.email }} |
{{ e.tipo or '-' }} |
@@ -275,7 +275,7 @@
- {% for i in identificadores %}
+ {% for i in identificadores|sort_by_date('fimValidade', 'inicioValidade') %}
| {{ i.tipo or '-' }} |
{{ i.descricao or '-' }} |
@@ -291,7 +291,7 @@
{{ identificador_lattes.inicioValidade or '-' }}{% if identificador_lattes.fimValidade %} a {{ identificador_lattes.fimValidade }}{% endif %} |
{% endif %}
- {% for d in documentos %}
+ {% for d in documentos|sort_by_date('fimValidade', 'inicioValidade') %}
| {{ d.tipo or '-' }} |
{{ d.descricao or '-' }} |
@@ -322,7 +322,7 @@
- {% for t in titulacoes %}
+ {% for t in titulacoes|sort_by_date('fim', 'inicio') %}
| {{ t.grauAcademico.nome if t.grauAcademico else '-' }} |
{{ t.ies.nome if t.ies else '-' }}{% if t.ies and t.ies.sigla %} ({{ t.ies.sigla }}){% endif %} |
@@ -370,7 +370,7 @@
- {% for b in bolsas %}
+ {% for b in bolsas|sort_by_date('fim', 'inicio') %}
| {{ b.programa or b.tipo or '-' }} |
{{ b.ies.nome if b.ies else '-' }} |
@@ -475,7 +475,7 @@
- {% for v in consultor.consultoria.vinculos %}
+ {% for v in consultor.consultoria.vinculos|sort_by_date('periodo.fim', 'periodo.inicio') %}
| {{ v.ies.nome if v.ies else '-' }}{% if v.ies and v.ies.sigla %} ({{ v.ies.sigla }}){% endif %} |
{{ v.periodo.inicio|format_date_short if v.periodo else '-' }} a {{ v.periodo.fim|format_date_short if v.periodo and v.periodo.fim else 'Atual' }} |
@@ -549,7 +549,7 @@
- {% for coord in consultor.coordenacoes_capes %}
+ {% for coord in consultor.coordenacoes_capes|sort_by_date('periodo.fim', 'periodo.inicio') %}
| {{ coord.codigo }} |
{{ coord.area_avaliacao or '-' }} |
@@ -630,7 +630,7 @@
- {% for v in consultor.consultoria.vinculos %}
+ {% for v in consultor.consultoria.vinculos|sort_by_date('periodo.fim', 'periodo.inicio') %}
| {{ v.ies.nome if v.ies else '-' }}{% if v.ies and v.ies.sigla %} ({{ v.ies.sigla }}){% endif %} |
{{ v.periodo.inicio|format_date_short if v.periodo else '-' }} a {{ v.periodo.fim|format_date_short if v.periodo and v.periodo.fim else 'Atual' }} |
@@ -689,7 +689,7 @@
- {% for a in consultor.avaliacoes_comissao %}
+ {% for a in consultor.avaliacoes_comissao|sort_by_date('ano') %}
| {{ a.codigo }} |
{{ a.tipo }} |
@@ -715,7 +715,7 @@
- {% for p in consultor.premiacoes %}
+ {% for p in consultor.premiacoes|sort_by_date('ano') %}
| {{ p.codigo }} |
{{ p.tipo }} |
@@ -741,7 +741,7 @@
- {% for i in consultor.inscricoes %}
+ {% for i in consultor.inscricoes|sort_by_date('ano') %}
| {{ i.codigo }} |
{{ i.tipo }} |
@@ -768,7 +768,7 @@
- {% for o in consultor.orientacoes %}
+ {% for o in consultor.orientacoes|sort_by_date('ano') %}
| {{ o.codigo }} |
{{ o.tipo }} |
@@ -794,7 +794,7 @@
- {% for b in consultor.membros_banca %}
+ {% for b in consultor.membros_banca|sort_by_date('ano') %}
| {{ b.codigo }} |
{{ b.tipo }} |
@@ -818,7 +818,7 @@
- {% for p in consultor.participacoes %}
+ {% for p in consultor.participacoes|sort_by_date('ano') %}
| {{ p.codigo }} |
{{ p.tipo }} |
@@ -873,7 +873,7 @@
- {% for a in atuacoes_raw %}
+ {% for a in atuacoes_raw|sort_by_date('fim', 'inicio') %}
| {{ a.tipo or '-' }} |
{{ a.descricao or a.nome or '-' }} |
diff --git a/frontend/src/components/RawDataModal.css b/frontend/src/components/RawDataModal.css
index db01081..fd73a9d 100644
--- a/frontend/src/components/RawDataModal.css
+++ b/frontend/src/components/RawDataModal.css
@@ -481,15 +481,38 @@
min-width: 35px;
}
-.atuacao-tipo {
+.atuacao-main {
+ display: flex;
+ flex-direction: column;
+ gap: 0.15rem;
flex: 1;
+ min-width: 0;
+}
+
+.atuacao-tipo {
font-size: 0.9rem;
color: #e2e8f0;
font-weight: 500;
}
+.atuacao-summary {
+ font-size: 0.75rem;
+ color: #94a3b8;
+ line-height: 1.3;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.atuacao-meta {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ gap: 0.2rem;
+}
+
.atuacao-periodo {
- font-size: 0.8rem;
+ font-size: 0.75rem;
color: #94a3b8;
display: flex;
align-items: center;
@@ -501,6 +524,16 @@
font-weight: 600;
}
+.atuacao-meta-line {
+ font-size: 0.72rem;
+ color: #7dd3fc;
+ text-align: right;
+ max-width: 260px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
.atuacao-toggle {
color: #64748b;
font-size: 1.1rem;
@@ -611,7 +644,7 @@
flex-wrap: wrap;
}
- .atuacao-tipo {
+ .atuacao-main {
order: 1;
flex: 1 1 100%;
}
@@ -620,9 +653,10 @@
order: 2;
}
- .atuacao-periodo {
+ .atuacao-meta {
order: 3;
- flex: 1;
+ align-items: flex-start;
+ width: 100%;
}
.atuacao-toggle {
diff --git a/frontend/src/components/RawDataModal.jsx b/frontend/src/components/RawDataModal.jsx
index 0452635..764b404 100644
--- a/frontend/src/components/RawDataModal.jsx
+++ b/frontend/src/components/RawDataModal.jsx
@@ -61,6 +61,33 @@ const LABEL_MAP = {
colegio: 'Colégio',
ies: 'IES',
programa: 'Programa',
+ nomeEmpregador: 'Empregador',
+ emprego: 'Vínculo',
+ atividade: 'Atividade',
+ dtAdmissao: 'Admissão',
+ dtDesligamento: 'Desligamento',
+ cnpjEmpregador: 'CNPJ Empregador',
+ profissao: 'Profissão',
+ matricula: 'Matrícula',
+ vinculo: 'Vínculo',
+ historico: 'Histórico',
+ inicioRelacionamento: 'Início Relacionamento',
+ fimRelacionamento: 'Fim Relacionamento',
+ categoria: 'Categoria',
+ tipoVinculo: 'Tipo de Vínculo',
+ vinculoTrabalho: 'Vínculo de Trabalho',
+ regimeTrabalho: 'Regime de Trabalho',
+ cargaHoraria: 'Carga Horária',
+ linhaPesquisa: 'Linha de Pesquisa',
+ areaConcentracao: 'Área de Concentração',
+ areaPesquisa: 'Área de Pesquisa',
+ consultorResponsavel: 'Consultor Responsável',
+ unidadeOrganizacional: 'Unidade Organizacional',
+ localEvento: 'Local do Evento',
+ nrProcesso: 'Número do Processo',
+ idProcesso: 'ID do Processo',
+ anoInicio: 'Ano de Início',
+ tipoTrabalho: 'Tipo de Trabalho',
inicioVinculacao: 'Início Vinculação',
fimVinculacao: 'Fim Vinculação',
inicioSituacao: 'Início Situação',
@@ -78,6 +105,11 @@ const LABEL_MAP = {
nivel: 'Nível',
modalidade: 'Modalidade',
camaraTematica: 'Câmara Temática',
+ totalOrientacaoFinalizadaMestrado: 'Orientações Finalizadas (Mestrado)',
+ totalOrientacaoFinalizadaDoutorado: 'Orientações Finalizadas (Doutorado)',
+ totalOrientacaoAndamentoMestrado: 'Orientações em Andamento (Mestrado)',
+ totalOrientacaoAndamentoDoutorado: 'Orientações em Andamento (Doutorado)',
+ totalAcompanhamentoPosDoutorado: 'Acompanhamentos Pós-Doutorado',
};
const formatLabel = (key) => LABEL_MAP[key] || key.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase());
@@ -154,6 +186,122 @@ const AtuacaoCard = ({ atuacao, index }) => {
const [expanded, setExpanded] = useState(false);
const tipo = atuacao.tipo || 'Tipo não informado';
+ const getNestedValue = (obj, path) => {
+ if (!obj || !path) return undefined;
+ return path.split('.').reduce((acc, key) => (acc ? acc[key] : undefined), obj);
+ };
+
+ const joinParts = (parts) => parts.filter(Boolean).join(' · ');
+
+ const buildSummary = () => {
+ const tipoLower = tipo.toLowerCase();
+ const dadosDocencia = atuacao.dadosDocencia || {};
+ const dadosEmprego = atuacao.dadosEmprego || {};
+ const dadosEvento = atuacao.dadosEvento || atuacao.dadosParticipacaoEvento || {};
+ const dadosProjeto = atuacao.dadosProjeto || {};
+ const dadosProcesso = atuacao.dadosProcesso || {};
+ const dadosOrientacaoDiscente = atuacao.dadosOrientacaoDiscente || {};
+
+ if (tipoLower.includes('docência') || tipoLower.includes('docencia')) {
+ const ies = dadosDocencia.ies?.sigla
+ ? `${dadosDocencia.ies.sigla} — ${dadosDocencia.ies.nome}`
+ : dadosDocencia.ies?.nome;
+ const primary = joinParts([
+ dadosDocencia.programa?.nome || dadosDocencia.areaConhecimento?.nome || atuacao.descricao,
+ ies,
+ dadosDocencia.categoria,
+ ]);
+ const secondary = joinParts([
+ dadosDocencia.tipoVinculo,
+ dadosDocencia.regimeTrabalho,
+ dadosDocencia.cargaHoraria ? `${dadosDocencia.cargaHoraria}h` : null,
+ ]);
+ return { primary, secondary };
+ }
+
+ if (tipoLower.includes('emprego')) {
+ const primary = joinParts([
+ dadosEmprego.nomeEmpregador || atuacao.descricao,
+ dadosEmprego.emprego || dadosEmprego.vinculo,
+ dadosEmprego.atividade,
+ ]);
+ const secondary = joinParts([
+ dadosEmprego.dtAdmissao,
+ dadosEmprego.dtDesligamento ? `até ${dadosEmprego.dtDesligamento}` : null,
+ dadosEmprego.profissao,
+ ]);
+ return { primary, secondary };
+ }
+
+ if (tipoLower.includes('evento')) {
+ const primary = joinParts([
+ dadosEvento.nome || atuacao.descricao,
+ dadosEvento.unidadeOrganizacional,
+ dadosEvento.localEvento,
+ ]);
+ const secondary = joinParts([
+ dadosEvento.atividade,
+ dadosEvento.tipo,
+ dadosEvento.codigo ? `Código ${dadosEvento.codigo}` : null,
+ ]);
+ return { primary, secondary };
+ }
+
+ if (tipoLower.includes('projeto')) {
+ const primary = joinParts([
+ dadosProjeto.nome || atuacao.descricao,
+ dadosProjeto.programa?.nome,
+ dadosProjeto.ies?.sigla || dadosProjeto.ies?.nome,
+ ]);
+ const secondary = joinParts([
+ dadosProjeto.situacao,
+ dadosProjeto.linhaPesquisa,
+ dadosProjeto.anoInicio ? `Início ${dadosProjeto.anoInicio}` : null,
+ ]);
+ return { primary, secondary };
+ }
+
+ if (tipoLower.includes('orientação de discentes') || tipoLower.includes('orientacao de discentes')) {
+ const mestradoParts = [];
+ if (dadosOrientacaoDiscente.totalOrientacaoFinalizadaMestrado) {
+ mestradoParts.push(`Finalizadas ${dadosOrientacaoDiscente.totalOrientacaoFinalizadaMestrado}`);
+ }
+ if (dadosOrientacaoDiscente.totalOrientacaoAndamentoMestrado) {
+ mestradoParts.push(`Andamento ${dadosOrientacaoDiscente.totalOrientacaoAndamentoMestrado}`);
+ }
+ const doutoradoParts = [];
+ if (dadosOrientacaoDiscente.totalOrientacaoFinalizadaDoutorado) {
+ doutoradoParts.push(`Finalizadas ${dadosOrientacaoDiscente.totalOrientacaoFinalizadaDoutorado}`);
+ }
+ if (dadosOrientacaoDiscente.totalOrientacaoAndamentoDoutorado) {
+ doutoradoParts.push(`Andamento ${dadosOrientacaoDiscente.totalOrientacaoAndamentoDoutorado}`);
+ }
+ const primary = joinParts([
+ mestradoParts.length ? `Mestrado ${mestradoParts.join(' / ')}` : null,
+ doutoradoParts.length ? `Doutorado ${doutoradoParts.join(' / ')}` : null,
+ ]);
+ const secondary = dadosOrientacaoDiscente.totalAcompanhamentoPosDoutorado
+ ? `Pós-doc ${dadosOrientacaoDiscente.totalAcompanhamentoPosDoutorado}`
+ : null;
+ return { primary, secondary };
+ }
+
+ if (tipoLower.includes('processo')) {
+ const iesLabel = dadosProcesso.ies?.sigla || dadosProcesso.ies?.nome;
+ const primary = joinParts([dadosProcesso.nrProcesso || atuacao.descricao, iesLabel]);
+ const secondary = dadosProcesso.ies?.statusJuridico
+ ? `IES ${dadosProcesso.ies.statusJuridico.trim()}`
+ : null;
+ return { primary, secondary };
+ }
+
+ const fallbackPrimary = joinParts([
+ atuacao.descricao,
+ getNestedValue(atuacao, 'procedencia.origem'),
+ ]);
+ return { primary: fallbackPrimary, secondary: null };
+ };
+
const getAtuacaoColor = (tipo) => {
if (tipo.includes('Coordenação')) return 'atuacao-coordenacao';
if (tipo.includes('Consultor')) return 'atuacao-consultoria';
@@ -177,8 +325,17 @@ const AtuacaoCard = ({ atuacao, index }) => {
'dadosParticipacaoInscricaoPremio',
'dadosBolsistaCNPq',
'dadosOrientacao',
+ 'dadosOrientacaoDiscente',
'dadosParticipacao',
+ 'dadosParticipacaoEvento',
'dadosGestaoPrograma',
+ 'dadosDocencia',
+ 'dadosEmprego',
+ 'dadosEvento',
+ 'dadosProjeto',
+ 'dadosProcesso',
+ 'dadosAnaliseProcesso',
+ 'dadosPapelPessoa',
];
const dateKeys = ['inicio', 'fim', 'inicioVinculacao', 'fimVinculacao', 'inicioSituacao', 'inativacaoSituacao'];
@@ -196,6 +353,13 @@ const AtuacaoCard = ({ atuacao, index }) => {
}
});
+ const baseKeys = ['descricao', 'quantidade', 'id', 'procedencia'];
+ baseKeys.forEach((key) => {
+ if (atuacao[key] !== null && atuacao[key] !== undefined && allData[key] === undefined) {
+ allData[key] = atuacao[key];
+ }
+ });
+
if (Object.keys(allData).length === 0) {
if (atuacao.inicio) allData.inicio = atuacao.inicio;
if (atuacao.fim) allData.fim = atuacao.fim;
@@ -207,16 +371,23 @@ const AtuacaoCard = ({ atuacao, index }) => {
const dados = getAllDados();
const hasData = Object.keys(dados).length > 0;
+ const summary = buildSummary();
return (
setExpanded(!expanded)}>
#{index + 1}
-
{tipo}
-
- {atuacao.inicio &&
{formatDate(atuacao.inicio)}}
- {atuacao.inicio && atuacao.fim &&
- }
- {atuacao.fim ?
{formatDate(atuacao.fim)} : atuacao.inicio &&
Atual}
+
+ {tipo}
+ {summary.primary && {summary.primary}}
+
+
+
+ {atuacao.inicio && {formatDate(atuacao.inicio)}}
+ {atuacao.inicio && atuacao.fim && - }
+ {atuacao.fim ? {formatDate(atuacao.fim)} : atuacao.inicio && Atual}
+
+ {summary.secondary &&
{summary.secondary}
}
{expanded ? '−' : '+'}