From cbdeddb5d82436f0319d5ba55b75a6d6830bd659 Mon Sep 17 00:00:00 2001 From: Frederico Castro Date: Fri, 19 Dec 2025 11:59:41 -0300 Subject: [PATCH] feat(ui): expandir modal de dados e melhorar template PDF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Expandir RawDataModal com mais funcionalidades de visualização - Ajustar template HTML da ficha do consultor - Adicionar configuração MCP do projeto - Atualizar gitignore para ignorar arquivos locais --- .gitignore | 6 + .mcp.json | 15 ++ .../src/application/services/pdf_service.py | 63 ++++++ .../pdf/templates/ficha_consultor.html | 30 +-- frontend/src/components/RawDataModal.css | 44 ++++- frontend/src/components/RawDataModal.jsx | 181 +++++++++++++++++- 6 files changed, 314 insertions(+), 25 deletions(-) create mode 100644 .mcp.json 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 ? '−' : '+'}