feat(pdf): adicionar exportação de ficha do consultor em PDF
- Novo endpoint GET /api/v1/consultor/{id}/pdf para download
- Serviço PDFService usando WeasyPrint para geração
- Template HTML com layout padrão governo federal
- Botão de exportar PDF no card e modal de dados brutos
This commit is contained in:
@@ -2,6 +2,15 @@ FROM python:3.11-slim
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
libpango-1.0-0 \
|
||||||
|
libpangocairo-1.0-0 \
|
||||||
|
libgdk-pixbuf-2.0-0 \
|
||||||
|
libffi-dev \
|
||||||
|
shared-mime-info \
|
||||||
|
fonts-liberation \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN pip install --upgrade pip
|
RUN pip install --upgrade pip
|
||||||
|
|
||||||
COPY requirements.txt ./
|
COPY requirements.txt ./
|
||||||
|
|||||||
@@ -7,3 +7,5 @@ httpx==0.26.0
|
|||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
rich==13.7.0
|
rich==13.7.0
|
||||||
oracledb==2.5.1
|
oracledb==2.5.1
|
||||||
|
weasyprint>=62.3
|
||||||
|
jinja2==3.1.2
|
||||||
|
|||||||
0
backend/src/application/services/__init__.py
Normal file
0
backend/src/application/services/__init__.py
Normal file
145
backend/src/application/services/pdf_service.py
Normal file
145
backend/src/application/services/pdf_service.py
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict
|
||||||
|
from dataclasses import is_dataclass, asdict
|
||||||
|
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
|
||||||
|
class PDFService:
|
||||||
|
def __init__(self):
|
||||||
|
self.template_dir = Path(__file__).parent.parent.parent / "infrastructure" / "pdf" / "templates"
|
||||||
|
self.env = Environment(
|
||||||
|
loader=FileSystemLoader(str(self.template_dir)),
|
||||||
|
autoescape=True
|
||||||
|
)
|
||||||
|
self.env.filters['format_date'] = self._format_date
|
||||||
|
self.env.filters['format_date_short'] = self._format_date_short
|
||||||
|
self.template = self.env.get_template("ficha_consultor.html")
|
||||||
|
|
||||||
|
def _format_date(self, date_str: str) -> str:
|
||||||
|
if not date_str:
|
||||||
|
return "-"
|
||||||
|
try:
|
||||||
|
if "/" in date_str:
|
||||||
|
return date_str
|
||||||
|
dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
|
||||||
|
return dt.strftime("%d/%m/%Y")
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
return str(date_str)
|
||||||
|
|
||||||
|
def _format_date_short(self, date_str: str) -> str:
|
||||||
|
if not date_str:
|
||||||
|
return "-"
|
||||||
|
try:
|
||||||
|
meses = ['Jan', 'Fev', 'Mar', 'Abr', 'Mai', 'Jun',
|
||||||
|
'Jul', 'Ago', 'Set', 'Out', 'Nov', 'Dez']
|
||||||
|
if "/" in date_str:
|
||||||
|
parts = date_str.split("/")
|
||||||
|
if len(parts) >= 2:
|
||||||
|
mes = int(parts[1]) if len(parts) == 3 else int(parts[0])
|
||||||
|
ano = parts[2] if len(parts) == 3 else parts[1]
|
||||||
|
return f"{meses[mes-1]}/{ano}"
|
||||||
|
return date_str
|
||||||
|
dt = datetime.fromisoformat(date_str.replace("Z", "+00:00"))
|
||||||
|
return f"{meses[dt.month-1]}/{dt.year}"
|
||||||
|
except (ValueError, AttributeError, IndexError):
|
||||||
|
return str(date_str)[:10] if date_str else "-"
|
||||||
|
|
||||||
|
def _consultor_to_dict(self, consultor: Any) -> Dict:
|
||||||
|
if is_dataclass(consultor) and not isinstance(consultor, type):
|
||||||
|
return asdict(consultor)
|
||||||
|
elif hasattr(consultor, 'model_dump'):
|
||||||
|
return consultor.model_dump()
|
||||||
|
elif hasattr(consultor, 'dict'):
|
||||||
|
return consultor.dict()
|
||||||
|
elif isinstance(consultor, dict):
|
||||||
|
return consultor
|
||||||
|
else:
|
||||||
|
return vars(consultor)
|
||||||
|
|
||||||
|
def gerar_ficha_consultor(self, consultor: Any, raw_document: Any = None) -> bytes:
|
||||||
|
from weasyprint import HTML, CSS
|
||||||
|
|
||||||
|
consultor_data = self._consultor_to_dict(consultor)
|
||||||
|
raw_data = self._consultor_to_dict(raw_document) if raw_document else {}
|
||||||
|
|
||||||
|
html_content = self.template.render(
|
||||||
|
consultor=ConsultorWrapper(consultor_data),
|
||||||
|
raw=DictWrapper(raw_data) if raw_data else DictWrapper({}),
|
||||||
|
data_geracao=datetime.now().strftime("%d/%m/%Y às %H:%M"),
|
||||||
|
pontuacao_coord=self._extrair_pontuacao_coord(consultor_data),
|
||||||
|
)
|
||||||
|
|
||||||
|
css_path = self.template_dir / "styles.css"
|
||||||
|
|
||||||
|
pdf_bytes = HTML(
|
||||||
|
string=html_content,
|
||||||
|
base_url=str(self.template_dir)
|
||||||
|
).write_pdf(
|
||||||
|
stylesheets=[CSS(filename=str(css_path))]
|
||||||
|
)
|
||||||
|
|
||||||
|
return pdf_bytes
|
||||||
|
|
||||||
|
def _extrair_pontuacao_coord(self, consultor_data: Dict) -> Dict:
|
||||||
|
result = {}
|
||||||
|
pontuacao = consultor_data.get('pontuacao')
|
||||||
|
if not pontuacao:
|
||||||
|
return result
|
||||||
|
|
||||||
|
bloco_a = pontuacao.get('bloco_a', {})
|
||||||
|
atuacoes = bloco_a.get('atuacoes', [])
|
||||||
|
|
||||||
|
for atuacao in atuacoes:
|
||||||
|
codigo = atuacao.get('codigo', '')
|
||||||
|
result[codigo] = {
|
||||||
|
'base': atuacao.get('base', 0),
|
||||||
|
'tempo': atuacao.get('tempo', 0),
|
||||||
|
'bonus': atuacao.get('bonus', 0),
|
||||||
|
'total': atuacao.get('total', 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class ConsultorWrapper:
|
||||||
|
def __init__(self, data: Dict):
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
def __getattr__(self, name: str):
|
||||||
|
value = self._data.get(name)
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return DictWrapper(value)
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [DictWrapper(item) if isinstance(item, dict) else item for item in value]
|
||||||
|
return value
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return bool(self._data)
|
||||||
|
|
||||||
|
|
||||||
|
class DictWrapper:
|
||||||
|
def __init__(self, data: Dict):
|
||||||
|
self._data = data
|
||||||
|
|
||||||
|
def __getattr__(self, name: str):
|
||||||
|
value = self._data.get(name)
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return DictWrapper(value)
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [DictWrapper(item) if isinstance(item, dict) else item for item in value]
|
||||||
|
return value
|
||||||
|
|
||||||
|
def __bool__(self):
|
||||||
|
return bool(self._data)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self._data)
|
||||||
|
|
||||||
|
def get(self, key: str, default=None):
|
||||||
|
return self._data.get(key, default)
|
||||||
0
backend/src/infrastructure/pdf/__init__.py
Normal file
0
backend/src/infrastructure/pdf/__init__.py
Normal file
941
backend/src/infrastructure/pdf/templates/ficha_consultor.html
Normal file
941
backend/src/infrastructure/pdf/templates/ficha_consultor.html
Normal file
@@ -0,0 +1,941 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Cadastro do Consultor - {{ consultor.nome }}</title>
|
||||||
|
<link rel="stylesheet" href="styles.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{% set dp = raw.dadosPessoais if raw.dadosPessoais else {} %}
|
||||||
|
{% set emails = raw.emails if raw.emails else [] %}
|
||||||
|
{% set telefones = raw.telefones if raw.telefones else [] %}
|
||||||
|
{% set enderecos = raw.enderecos if raw.enderecos else [] %}
|
||||||
|
{% set identificadores = raw.identificadoresRegistrados if raw.identificadoresRegistrados else [] %}
|
||||||
|
{% set identificador_lattes = raw.identificadorLattes if raw.identificadorLattes else None %}
|
||||||
|
{% set titulacoes = raw.titulacoes if raw.titulacoes else [] %}
|
||||||
|
{% set idiomas = raw.idiomas if raw.idiomas else [] %}
|
||||||
|
{% set areas = raw.areasConhecimento if raw.areasConhecimento else [] %}
|
||||||
|
{% set papeis = raw.papeis if raw.papeis else [] %}
|
||||||
|
{% set bolsas = raw.bolsas if raw.bolsas else [] %}
|
||||||
|
{% set dados_bancarios = raw.dadosBancarios if raw.dadosBancarios else [] %}
|
||||||
|
{% set documentos = raw.documento if raw.documento else [] %}
|
||||||
|
{% set atuacoes_raw = raw.atuacoes if raw.atuacoes else [] %}
|
||||||
|
|
||||||
|
{% set total_coordenacoes = consultor.coordenacoes_capes|length if consultor.coordenacoes_capes else 0 %}
|
||||||
|
{% set total_inscricoes = consultor.inscricoes|length if consultor.inscricoes else 0 %}
|
||||||
|
{% set total_avaliacoes = consultor.avaliacoes_comissao|length if consultor.avaliacoes_comissao else 0 %}
|
||||||
|
{% set total_premiacoes = consultor.premiacoes|length if consultor.premiacoes else 0 %}
|
||||||
|
{% set total_bolsas_cnpq = consultor.bolsas_cnpq|length if consultor.bolsas_cnpq else 0 %}
|
||||||
|
{% set total_participacoes = consultor.participacoes|length if consultor.participacoes else 0 %}
|
||||||
|
{% set total_orientacoes = consultor.orientacoes|length if consultor.orientacoes else 0 %}
|
||||||
|
{% set total_bancas = consultor.membros_banca|length if consultor.membros_banca else 0 %}
|
||||||
|
|
||||||
|
<div class="documento">
|
||||||
|
<section class="cover">
|
||||||
|
<div>
|
||||||
|
<table class="cover-header">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong>MINISTÉRIO DA EDUCAÇÃO</strong><br>
|
||||||
|
Coordenação de Aperfeiçoamento de Pessoal de Nível Superior
|
||||||
|
</td>
|
||||||
|
<td class="right small">Relatório Executivo</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="cover-title">Cadastro do Consultor</div>
|
||||||
|
<div class="cover-subtitle">Sistema de Ranking AtuaCAPES · Documento de Alta Gestão</div>
|
||||||
|
|
||||||
|
<div class="cover-name">{{ consultor.nome }}</div>
|
||||||
|
<div class="tag-list">
|
||||||
|
<span class="badge {{ 'success' if consultor.ativo else 'danger' }}">{{ 'Ativo' if consultor.ativo else 'Histórico' }}</span>
|
||||||
|
{% if consultor.veterano %}
|
||||||
|
<span class="badge">Veterano</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if consultor.coordenador_ppg %}
|
||||||
|
<span class="badge">Coordenação PPG</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="cover-meta">
|
||||||
|
<tr>
|
||||||
|
<td><strong>ID Pessoa:</strong> {{ consultor.id_pessoa }}</td>
|
||||||
|
<td><strong>Posição no Ranking:</strong> {{ consultor.rank or '-' }}º</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Pontuação Total:</strong> {{ consultor.pontuacao.pontuacao_total }}</td>
|
||||||
|
<td><strong>Anos de Atuação:</strong> {{ "%.1f"|format(consultor.anos_atuacao) }} anos</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td><strong>Data de Geração:</strong> {{ data_geracao }}</td>
|
||||||
|
<td><strong>Origem:</strong> Elasticsearch / ATUACAPES</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="section-divider"></div>
|
||||||
|
|
||||||
|
<div class="section-title">Resumo Executivo</div>
|
||||||
|
<p class="lead">Visão consolidada dos indicadores do consultor, com foco em desempenho, volume e posicionamento estratégico.</p>
|
||||||
|
|
||||||
|
<table class="kpi-table">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="kpi-label">Pontuação total</div>
|
||||||
|
<div class="kpi-value">{{ consultor.pontuacao.pontuacao_total }}</div>
|
||||||
|
<div class="kpi-sub">Blocos A+C+D consolidados</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="kpi-label">Ranking nacional</div>
|
||||||
|
<div class="kpi-value">{{ consultor.rank or '-' }}º</div>
|
||||||
|
<div class="kpi-sub">Posição atual no ranking</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="kpi-label">Anos de atuação</div>
|
||||||
|
<div class="kpi-value">{{ "%.1f"|format(consultor.anos_atuacao) }}</div>
|
||||||
|
<div class="kpi-sub">Tempo de atuação CAPES</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="kpi-label">Status</div>
|
||||||
|
<div class="kpi-value">{{ 'Ativo' if consultor.ativo else 'Histórico' }}</div>
|
||||||
|
<div class="kpi-sub">{{ 'Consultoria em vigor' if consultor.ativo else 'Sem consultoria ativa' }}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="subsection-title">Indicadores de Volume</div>
|
||||||
|
<table class="kpi-table">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="kpi-label">Coordenações CAPES</div>
|
||||||
|
<div class="kpi-value">{{ total_coordenacoes }}</div>
|
||||||
|
<div class="kpi-sub">Bloco A</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="kpi-label">Consultorias & Vínculos</div>
|
||||||
|
<div class="kpi-value">{{ consultor.consultoria.vinculos|length if consultor.consultoria and consultor.consultoria.vinculos else 0 }}</div>
|
||||||
|
<div class="kpi-sub">Bloco C</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="kpi-label">Premiações & Avaliações</div>
|
||||||
|
<div class="kpi-value">{{ total_premiacoes + total_avaliacoes + total_inscricoes }}</div>
|
||||||
|
<div class="kpi-sub">Bloco D</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="kpi-label">Atuações no ES</div>
|
||||||
|
<div class="kpi-value">{{ atuacoes_raw|length }}</div>
|
||||||
|
<div class="kpi-sub">Base completa</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section force-page-break">
|
||||||
|
<div class="section-title">Identificação e Perfil</div>
|
||||||
|
<table class="meta-table">
|
||||||
|
<tr>
|
||||||
|
<td class="label">Nome</td>
|
||||||
|
<td class="value">{{ dp.nome or consultor.nome }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Nascimento</td>
|
||||||
|
<td class="value">{{ dp.nascimento or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Gênero</td>
|
||||||
|
<td class="value">{{ dp.genero or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Raça/Cor</td>
|
||||||
|
<td class="value">{{ dp.raca or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Nacionalidade</td>
|
||||||
|
<td class="value">{{ dp.nacionalidade or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Município/UF</td>
|
||||||
|
<td class="value">{{ dp.municipio or '-' }}{% if dp.uf %} / {{ dp.uf }}{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Estado civil</td>
|
||||||
|
<td class="value">{{ dp.estadoCivil or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Nome da mãe</td>
|
||||||
|
<td class="value">{{ dp.nomeMae or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Nome do pai</td>
|
||||||
|
<td class="value">{{ dp.nomePai or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Tipo de pessoa</td>
|
||||||
|
<td class="value">{{ dp.tipo or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Ano de óbito</td>
|
||||||
|
<td class="value">{{ dp.anoObito or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section force-page-break">
|
||||||
|
<div class="section-title">Contato e Identificadores</div>
|
||||||
|
|
||||||
|
<div class="subsection-title">E-mails</div>
|
||||||
|
{% if emails %}
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>E-mail</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Principal</th>
|
||||||
|
<th>Última alteração</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for e in emails %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ e.email }}</td>
|
||||||
|
<td>{{ e.tipo or '-' }}</td>
|
||||||
|
<td>{{ e.principalFinalidade or '-' }}</td>
|
||||||
|
<td>{{ e.ultimaAlteracao or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="note">Sem registros de e-mail.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="subsection-title">Telefones</div>
|
||||||
|
{% if telefones %}
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Telefone</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Finalidade</th>
|
||||||
|
<th>Principal</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for t in telefones %}
|
||||||
|
<tr>
|
||||||
|
<td>({{ t.codigo }}) {{ t.numero }}</td>
|
||||||
|
<td>{{ t.tipo or '-' }}</td>
|
||||||
|
<td>{{ t.finalidade or '-' }}</td>
|
||||||
|
<td>{{ t.principalFinalidade or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="note">Sem registros de telefone.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="subsection-title">Endereços</div>
|
||||||
|
{% if enderecos %}
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Endereço</th>
|
||||||
|
<th>Cidade</th>
|
||||||
|
<th>CEP</th>
|
||||||
|
<th>Principal</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for e in enderecos %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ e.tipo or '-' }}</td>
|
||||||
|
<td>{{ e.endereco or '-' }}{% if e.numero %}, {{ e.numero }}{% endif %}{% if e.complemento %} - {{ e.complemento }}{% endif %}</td>
|
||||||
|
<td>{{ e.cidadeExterior or '-' }}</td>
|
||||||
|
<td>{{ e.cep or '-' }}</td>
|
||||||
|
<td>{{ e.principalFinalidade or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="note">Sem registros de endereço.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="subsection-title">Identificadores e Documentos</div>
|
||||||
|
{% if identificadores or documentos or identificador_lattes %}
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Descrição</th>
|
||||||
|
<th>Órgão expedidor</th>
|
||||||
|
<th>Validade</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for i in identificadores %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ i.tipo or '-' }}</td>
|
||||||
|
<td>{{ i.descricao or '-' }}</td>
|
||||||
|
<td>{{ i.orgaoExpedidor or '-' }}</td>
|
||||||
|
<td>{{ i.inicioValidade or '-' }}{% if i.fimValidade %} a {{ i.fimValidade }}{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% if identificador_lattes %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ identificador_lattes.tipo or 'Identificador Lattes' }}</td>
|
||||||
|
<td>{{ identificador_lattes.descricao or '-' }}</td>
|
||||||
|
<td>{{ identificador_lattes.orgaoExpedidor or '-' }}</td>
|
||||||
|
<td>{{ identificador_lattes.inicioValidade or '-' }}{% if identificador_lattes.fimValidade %} a {{ identificador_lattes.fimValidade }}{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
{% for d in documentos %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ d.tipo or '-' }}</td>
|
||||||
|
<td>{{ d.descricao or '-' }}</td>
|
||||||
|
<td>{{ d.orgaoExpedidor or '-' }}</td>
|
||||||
|
<td>{{ d.inicioValidade or '-' }}{% if d.fimValidade %} a {{ d.fimValidade }}{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="note">Sem registros de identificadores/documentos.</p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section force-page-break">
|
||||||
|
<div class="section-title">Formação, Áreas e Competências</div>
|
||||||
|
|
||||||
|
<div class="subsection-title">Titulações</div>
|
||||||
|
{% if titulacoes %}
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Grau</th>
|
||||||
|
<th>IES</th>
|
||||||
|
<th>Área</th>
|
||||||
|
<th>Programa</th>
|
||||||
|
<th>Período</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for t in titulacoes %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ t.grauAcademico.nome if t.grauAcademico else '-' }}</td>
|
||||||
|
<td>{{ t.ies.nome if t.ies else '-' }}{% if t.ies and t.ies.sigla %} ({{ t.ies.sigla }}){% endif %}</td>
|
||||||
|
<td>{{ t.areaConhecimento.nome if t.areaConhecimento else '-' }}</td>
|
||||||
|
<td>{{ t.programa.nome if t.programa else '-' }}</td>
|
||||||
|
<td>{{ t.inicio|format_date_short if t.inicio else '-' }}{% if t.fim %} a {{ t.fim|format_date_short }}{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="note">Sem registros de titulação.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="subsection-title">Áreas de conhecimento</div>
|
||||||
|
{% if areas %}
|
||||||
|
<div class="tag-list">
|
||||||
|
{% for a in areas %}
|
||||||
|
<span class="tag">{{ a.nome or '-' }}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="note">Sem áreas de conhecimento registradas.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="subsection-title">Idiomas</div>
|
||||||
|
{% if idiomas %}
|
||||||
|
<div class="tag-list">
|
||||||
|
{% for i in idiomas %}
|
||||||
|
<span class="tag">{{ i.idioma or i.nome or '-' }}{% if i.nivel %} · {{ i.nivel }}{% endif %}</span>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<p class="note">Sem idiomas registrados.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="subsection-title">Bolsas (base ES)</div>
|
||||||
|
{% if bolsas %}
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Programa</th>
|
||||||
|
<th>Instituição</th>
|
||||||
|
<th>Período</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for b in bolsas %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ b.programa or b.tipo or '-' }}</td>
|
||||||
|
<td>{{ b.ies.nome if b.ies else '-' }}</td>
|
||||||
|
<td>{{ b.inicio|format_date_short if b.inicio else '-' }}{% if b.fim %} a {{ b.fim|format_date_short }}{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="note">Sem bolsas registradas na base ES.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="subsection-title">Bolsas CNPq (pontuação)</div>
|
||||||
|
{% if consultor.bolsas_cnpq %}
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Código</th>
|
||||||
|
<th>Nível</th>
|
||||||
|
<th>Área</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for b in consultor.bolsas_cnpq %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ b.codigo }}</td>
|
||||||
|
<td>{{ b.nivel }}</td>
|
||||||
|
<td>{{ b.area or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="note">Sem bolsas CNPq associadas.</p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section force-page-break">
|
||||||
|
<div class="section-title">Governança, Papéis e Consultoria</div>
|
||||||
|
|
||||||
|
<div class="subsection-title">Papéis cadastrados</div>
|
||||||
|
{% if papeis %}
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Origem</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in papeis %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ p.tipo or '-' }}</td>
|
||||||
|
<td>{{ p.status or '-' }}</td>
|
||||||
|
<td>{{ p.procedencia.origem if p.procedencia else '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="note">Sem papéis registrados na base ES.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="subsection-title">Consultoria CAPES</div>
|
||||||
|
{% if consultor.consultoria %}
|
||||||
|
<table class="meta-table">
|
||||||
|
<tr>
|
||||||
|
<td class="label">Código</td>
|
||||||
|
<td class="value">{{ consultor.consultoria.codigo }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Situação</td>
|
||||||
|
<td class="value">{{ consultor.consultoria.situacao }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Período</td>
|
||||||
|
<td class="value">{{ consultor.consultoria.periodo.inicio|format_date if consultor.consultoria.periodo else '-' }} a {{ consultor.consultoria.periodo.fim|format_date if consultor.consultoria.periodo and consultor.consultoria.periodo.fim else 'Atual' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Áreas</td>
|
||||||
|
<td class="value">{{ consultor.consultoria.areas|join(', ') if consultor.consultoria.areas else '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Anos consecutivos</td>
|
||||||
|
<td class="value">{{ consultor.consultoria.anos_consecutivos }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Retornos</td>
|
||||||
|
<td class="value">{{ consultor.consultoria.retornos }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if consultor.consultoria.vinculos %}
|
||||||
|
<div class="subsection-title">Vínculos institucionais</div>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>IES</th>
|
||||||
|
<th>Período</th>
|
||||||
|
<th>Situação</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for v in consultor.consultoria.vinculos %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ v.ies.nome if v.ies else '-' }}{% if v.ies and v.ies.sigla %} ({{ v.ies.sigla }}){% endif %}</td>
|
||||||
|
<td>{{ 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' }}</td>
|
||||||
|
<td>{{ v.situacao or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<p class="note">Sem registros de consultoria ativa ou histórica.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="subsection-title">Coordenação de PPG</div>
|
||||||
|
<p class="lead">{{ 'Possui atuação de coordenação de PPG.' if consultor.coordenador_ppg else 'Sem indicação de coordenação de PPG nos registros.' }}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section force-page-break">
|
||||||
|
<div class="section-title">Pontuação Consolidada</div>
|
||||||
|
<p class="lead">Resumo dos blocos de pontuação conforme metodologia do Ranking AtuaCAPES.</p>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Bloco</th>
|
||||||
|
<th>Descrição</th>
|
||||||
|
<th>Pontos</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>A</td>
|
||||||
|
<td>Coordenação CAPES</td>
|
||||||
|
<td>{{ consultor.pontuacao.bloco_a.total }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>B</td>
|
||||||
|
<td>Coordenação PPG (reservado)</td>
|
||||||
|
<td>0</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>C</td>
|
||||||
|
<td>Consultoria CAPES</td>
|
||||||
|
<td>{{ consultor.pontuacao.bloco_c.total }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>D</td>
|
||||||
|
<td>Premiações, Avaliações e Participações</td>
|
||||||
|
<td>{{ consultor.pontuacao.bloco_d.total }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<td colspan="2" class="right"><strong>Pontuação total</strong></td>
|
||||||
|
<td><strong>{{ consultor.pontuacao.pontuacao_total }}</strong></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section force-page-break">
|
||||||
|
<div class="section-title">Bloco A — Coordenações CAPES</div>
|
||||||
|
{% if consultor.coordenacoes_capes %}
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Código</th>
|
||||||
|
<th>Área de avaliação</th>
|
||||||
|
<th>Período</th>
|
||||||
|
<th>Anos</th>
|
||||||
|
<th>Presidente</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for coord in consultor.coordenacoes_capes %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ coord.codigo }}</td>
|
||||||
|
<td>{{ coord.area_avaliacao or '-' }}</td>
|
||||||
|
<td>{{ coord.periodo.inicio|format_date_short if coord.periodo else '-' }} a {{ coord.periodo.fim|format_date_short if coord.periodo and coord.periodo.fim else 'Atual' }}</td>
|
||||||
|
<td>{{ "%.1f"|format(coord.periodo.anos_decorridos) if coord.periodo else '-' }}</td>
|
||||||
|
<td>{{ 'Sim' if coord.presidente else 'Não' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if consultor.pontuacao.bloco_a.atuacoes %}
|
||||||
|
<div class="subsection-title">Pontuação detalhada</div>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Código</th>
|
||||||
|
<th>Quantidade</th>
|
||||||
|
<th>Base</th>
|
||||||
|
<th>Tempo</th>
|
||||||
|
<th>Bônus</th>
|
||||||
|
<th>Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for at in consultor.pontuacao.bloco_a.atuacoes %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ at.codigo }}</td>
|
||||||
|
<td>{{ at.quantidade }}</td>
|
||||||
|
<td>{{ at.base }}</td>
|
||||||
|
<td>{{ at.tempo }}</td>
|
||||||
|
<td>{{ at.bonus }}</td>
|
||||||
|
<td><strong>{{ at.total }}</strong></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<p class="note">Nenhuma coordenação CAPES registrada para este consultor.</p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section force-page-break">
|
||||||
|
<div class="section-title">Bloco C — Consultoria CAPES</div>
|
||||||
|
{% if consultor.consultoria %}
|
||||||
|
<table class="meta-table">
|
||||||
|
<tr>
|
||||||
|
<td class="label">Código</td>
|
||||||
|
<td class="value">{{ consultor.consultoria.codigo }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Situação</td>
|
||||||
|
<td class="value">{{ consultor.consultoria.situacao }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Período</td>
|
||||||
|
<td class="value">{{ consultor.consultoria.periodo.inicio|format_date if consultor.consultoria.periodo else '-' }} a {{ consultor.consultoria.periodo.fim|format_date if consultor.consultoria.periodo and consultor.consultoria.periodo.fim else 'Atual' }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Anos consecutivos</td>
|
||||||
|
<td class="value">{{ consultor.consultoria.anos_consecutivos }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Retornos</td>
|
||||||
|
<td class="value">{{ consultor.consultoria.retornos }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{% if consultor.consultoria.vinculos %}
|
||||||
|
<div class="subsection-title">Vínculos institucionais</div>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>IES</th>
|
||||||
|
<th>Período</th>
|
||||||
|
<th>Situação</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for v in consultor.consultoria.vinculos %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ v.ies.nome if v.ies else '-' }}{% if v.ies and v.ies.sigla %} ({{ v.ies.sigla }}){% endif %}</td>
|
||||||
|
<td>{{ 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' }}</td>
|
||||||
|
<td>{{ v.situacao or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if consultor.pontuacao.bloco_c.atuacoes %}
|
||||||
|
<div class="subsection-title">Pontuação detalhada</div>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Código</th>
|
||||||
|
<th>Quantidade</th>
|
||||||
|
<th>Base</th>
|
||||||
|
<th>Tempo</th>
|
||||||
|
<th>Bônus</th>
|
||||||
|
<th>Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for at in consultor.pontuacao.bloco_c.atuacoes %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ at.codigo }}</td>
|
||||||
|
<td>{{ at.quantidade }}</td>
|
||||||
|
<td>{{ at.base }}</td>
|
||||||
|
<td>{{ at.tempo }}</td>
|
||||||
|
<td>{{ at.bonus }}</td>
|
||||||
|
<td><strong>{{ at.total }}</strong></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<p class="note">Nenhum registro de consultoria encontrado para este consultor.</p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section force-page-break">
|
||||||
|
<div class="section-title">Bloco D — Premiações, Avaliações e Participações</div>
|
||||||
|
|
||||||
|
{% if consultor.avaliacoes_comissao %}
|
||||||
|
<div class="subsection-title">Avaliações em comissões ({{ consultor.avaliacoes_comissao|length }})</div>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Código</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Prêmio</th>
|
||||||
|
<th>Ano</th>
|
||||||
|
<th>Comissão</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for a in consultor.avaliacoes_comissao %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ a.codigo }}</td>
|
||||||
|
<td>{{ a.tipo }}</td>
|
||||||
|
<td>{{ a.premio }}</td>
|
||||||
|
<td>{{ a.ano }}</td>
|
||||||
|
<td>{{ a.nome_comissao or '-' }}{% if a.comissao_tipo %} ({{ a.comissao_tipo }}){% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if consultor.premiacoes %}
|
||||||
|
<div class="subsection-title">Premiações recebidas ({{ consultor.premiacoes|length }})</div>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Código</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Prêmio</th>
|
||||||
|
<th>Ano</th>
|
||||||
|
<th>Papel</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in consultor.premiacoes %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ p.codigo }}</td>
|
||||||
|
<td>{{ p.tipo }}</td>
|
||||||
|
<td>{{ p.nome_premio }}</td>
|
||||||
|
<td>{{ p.ano }}</td>
|
||||||
|
<td>{{ p.papel or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if consultor.inscricoes %}
|
||||||
|
<div class="subsection-title">Inscrições em prêmios ({{ consultor.inscricoes|length }})</div>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Código</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Prêmio</th>
|
||||||
|
<th>Ano</th>
|
||||||
|
<th>Situação</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for i in consultor.inscricoes %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ i.codigo }}</td>
|
||||||
|
<td>{{ i.tipo }}</td>
|
||||||
|
<td>{{ i.premio }}</td>
|
||||||
|
<td>{{ i.ano }}</td>
|
||||||
|
<td>{{ i.situacao }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if consultor.orientacoes %}
|
||||||
|
<div class="subsection-title">Orientações acadêmicas ({{ consultor.orientacoes|length }})</div>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Código</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Nível</th>
|
||||||
|
<th>Ano</th>
|
||||||
|
<th>Co-orient.</th>
|
||||||
|
<th>Premiada</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for o in consultor.orientacoes %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ o.codigo }}</td>
|
||||||
|
<td>{{ o.tipo }}</td>
|
||||||
|
<td>{{ o.nivel }}</td>
|
||||||
|
<td>{{ o.ano or '-' }}</td>
|
||||||
|
<td>{{ 'Sim' if o.coorientacao else 'Não' }}</td>
|
||||||
|
<td>{{ 'Sim' if o.premiada else 'Não' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if consultor.membros_banca %}
|
||||||
|
<div class="subsection-title">Bancas examinadoras ({{ consultor.membros_banca|length }})</div>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Código</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Nível</th>
|
||||||
|
<th>Ano</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for b in consultor.membros_banca %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ b.codigo }}</td>
|
||||||
|
<td>{{ b.tipo }}</td>
|
||||||
|
<td>{{ b.nivel }}</td>
|
||||||
|
<td>{{ b.ano or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if consultor.participacoes %}
|
||||||
|
<div class="subsection-title">Participações ({{ consultor.participacoes|length }})</div>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Código</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Descrição</th>
|
||||||
|
<th>Ano</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for p in consultor.participacoes %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ p.codigo }}</td>
|
||||||
|
<td>{{ p.tipo }}</td>
|
||||||
|
<td>{{ p.descricao }}</td>
|
||||||
|
<td>{{ p.ano or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if consultor.pontuacao.bloco_d.atuacoes %}
|
||||||
|
<div class="subsection-title">Pontuação detalhada</div>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Código</th>
|
||||||
|
<th>Quantidade</th>
|
||||||
|
<th>Base</th>
|
||||||
|
<th>Tempo</th>
|
||||||
|
<th>Bônus</th>
|
||||||
|
<th>Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for at in consultor.pontuacao.bloco_d.atuacoes %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ at.codigo }}</td>
|
||||||
|
<td>{{ at.quantidade }}</td>
|
||||||
|
<td>{{ at.base }}</td>
|
||||||
|
<td>{{ at.tempo }}</td>
|
||||||
|
<td>{{ at.bonus }}</td>
|
||||||
|
<td><strong>{{ at.total }}</strong></td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section force-page-break">
|
||||||
|
<div class="section-title">Atuações no Elasticsearch</div>
|
||||||
|
<p class="lead">Base completa de atuações registradas no ATUACAPES. Esta seção consolida todas as entradas encontradas no índice.</p>
|
||||||
|
{% if atuacoes_raw %}
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Tipo</th>
|
||||||
|
<th>Descrição</th>
|
||||||
|
<th>Período</th>
|
||||||
|
<th>Procedência</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for a in atuacoes_raw %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ a.tipo or '-' }}</td>
|
||||||
|
<td>{{ a.descricao or a.nome or '-' }}</td>
|
||||||
|
<td>{{ a.inicio or '-' }}{% if a.fim %} a {{ a.fim }}{% endif %}</td>
|
||||||
|
<td>{{ a.procedencia.origem if a.procedencia else '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="note">Sem atuações registradas no índice.</p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="section force-page-break">
|
||||||
|
<div class="section-title">Apêndice Técnico</div>
|
||||||
|
<div class="subsection-title">Dados bancários</div>
|
||||||
|
{% if dados_bancarios %}
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Banco</th>
|
||||||
|
<th>Agência</th>
|
||||||
|
<th>Conta</th>
|
||||||
|
<th>Tipo</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for b in dados_bancarios %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ b.banco or '-' }}</td>
|
||||||
|
<td>{{ b.agencia or '-' }}</td>
|
||||||
|
<td>{{ b.conta or '-' }}</td>
|
||||||
|
<td>{{ b.tipoConta or '-' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p class="note">Sem dados bancários registrados.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="subsection-title">Metadados do documento</div>
|
||||||
|
<table class="meta-table">
|
||||||
|
<tr>
|
||||||
|
<td class="label">Consultor ID</td>
|
||||||
|
<td class="value">{{ consultor.id_pessoa }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Fonte</td>
|
||||||
|
<td class="value">Elasticsearch ATUACAPES</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="label">Data de geração</td>
|
||||||
|
<td class="value">{{ data_geracao }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Documento gerado automaticamente. Em caso de divergência, prevalecem os registros oficiais do sistema.
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
307
backend/src/infrastructure/pdf/templates/styles.css
Normal file
307
backend/src/infrastructure/pdf/templates/styles.css
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
@page {
|
||||||
|
size: A4;
|
||||||
|
margin: 1.6cm 1.4cm 1.8cm 1.4cm;
|
||||||
|
|
||||||
|
@bottom-left {
|
||||||
|
content: "Sistema de Ranking AtuaCAPES";
|
||||||
|
font-family: "Helvetica Neue", Arial, sans-serif;
|
||||||
|
font-size: 8pt;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
@bottom-right {
|
||||||
|
content: "Página " counter(page) " de " counter(pages);
|
||||||
|
font-family: "Helvetica Neue", Arial, sans-serif;
|
||||||
|
font-size: 8pt;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--ink: #0f172a;
|
||||||
|
--muted: #475569;
|
||||||
|
--accent: #0f3d68;
|
||||||
|
--accent-2: #1d4ed8;
|
||||||
|
--border: #e2e8f0;
|
||||||
|
--bg-soft: #f8fafc;
|
||||||
|
--bg-strong: #eef2f7;
|
||||||
|
--success: #15803d;
|
||||||
|
--danger: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Helvetica Neue", Arial, sans-serif;
|
||||||
|
font-size: 10pt;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: var(--ink);
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.documento {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.force-page-break {
|
||||||
|
page-break-before: always !important;
|
||||||
|
break-before: page !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-break {
|
||||||
|
page-break-inside: avoid !important;
|
||||||
|
break-inside: avoid !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4 {
|
||||||
|
page-break-after: avoid;
|
||||||
|
break-after: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
border: 2px solid var(--accent);
|
||||||
|
padding: 28px 32px;
|
||||||
|
min-height: 23cm;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: var(--bg-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-header {
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding-bottom: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-header td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-title {
|
||||||
|
font-size: 20pt;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1.5px;
|
||||||
|
margin-top: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-subtitle {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11pt;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-name {
|
||||||
|
font-size: 16pt;
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 18px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-meta {
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
padding-top: 10px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 9.5pt;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-meta td {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-meta strong {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 8.5pt;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.success {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.danger {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
padding: 10px 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 13pt;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent);
|
||||||
|
text-transform: uppercase;
|
||||||
|
border-bottom: 2px solid var(--accent);
|
||||||
|
padding-bottom: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subsection-title {
|
||||||
|
font-size: 11pt;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ink);
|
||||||
|
margin: 16px 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lead {
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-table td {
|
||||||
|
background: var(--bg-strong);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 12px;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-label {
|
||||||
|
font-size: 8.5pt;
|
||||||
|
color: var(--muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-value {
|
||||||
|
font-size: 14pt;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kpi-sub {
|
||||||
|
font-size: 9pt;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table thead {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th,
|
||||||
|
.data-table td {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
text-align: left;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table th {
|
||||||
|
font-size: 8.5pt;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-table tbody tr:nth-child(even) {
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
color: var(--muted);
|
||||||
|
font-weight: 600;
|
||||||
|
width: 35%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-table td {
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-table tr:nth-child(even) {
|
||||||
|
background: var(--bg-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-list {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
margin: 0 6px 6px 0;
|
||||||
|
background: var(--bg-strong);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 8.5pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
background: var(--bg-soft);
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
padding: 10px 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 9pt;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border);
|
||||||
|
margin: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 18px;
|
||||||
|
font-size: 8.5pt;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.small {
|
||||||
|
font-size: 8.5pt;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
@@ -141,19 +141,20 @@ class ConsultorRepositoryImpl(ConsultorRepository):
|
|||||||
if not consultorias:
|
if not consultorias:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
visto_ids: Dict[int, Dict[str, Any]] = {}
|
vistos: Dict[tuple, Dict[str, Any]] = {}
|
||||||
for c in consultorias:
|
for c in consultorias:
|
||||||
cid = c.get("id")
|
dc = c.get("dadosConsultoria", {}) or {}
|
||||||
if cid is None:
|
ies_id = (dc.get("ies", {}) or {}).get("id")
|
||||||
continue
|
inicio = c.get("inicio") or dc.get("inicioVinculacao") or dc.get("inicioSituacao")
|
||||||
if cid not in visto_ids:
|
chave = (ies_id, inicio)
|
||||||
visto_ids[cid] = c
|
if chave not in vistos:
|
||||||
|
vistos[chave] = c
|
||||||
else:
|
else:
|
||||||
existente_fim = visto_ids[cid].get("fim")
|
existente_fim = vistos[chave].get("fim")
|
||||||
novo_fim = c.get("fim")
|
novo_fim = c.get("fim")
|
||||||
if existente_fim and not novo_fim:
|
if existente_fim and not novo_fim:
|
||||||
visto_ids[cid] = c
|
vistos[chave] = c
|
||||||
consultorias = list(visto_ids.values()) if visto_ids else consultorias
|
consultorias = list(vistos.values()) if vistos else consultorias
|
||||||
|
|
||||||
periodos: List[Periodo] = []
|
periodos: List[Periodo] = []
|
||||||
vinculos: List[VinculoConsultoria] = []
|
vinculos: List[VinculoConsultoria] = []
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
from io import BytesIO
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
|
|
||||||
from ...application.use_cases.obter_ranking import ObterRankingUseCase
|
from ...application.use_cases.obter_ranking import ObterRankingUseCase
|
||||||
@@ -365,3 +368,45 @@ async def obter_consultor_raw(
|
|||||||
return documento
|
return documento
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/consultor/{id_pessoa}/pdf")
|
||||||
|
async def exportar_ficha_pdf(
|
||||||
|
id_pessoa: int,
|
||||||
|
repository: ConsultorRepositoryImpl = Depends(get_repository),
|
||||||
|
es_client: ElasticsearchClient = Depends(get_es_client),
|
||||||
|
store = Depends(get_ranking_store),
|
||||||
|
):
|
||||||
|
from ...application.services.pdf_service import PDFService
|
||||||
|
|
||||||
|
use_case = ObterConsultorUseCase(repository=repository)
|
||||||
|
rank = None
|
||||||
|
if store.is_ready():
|
||||||
|
found = store.get_by_id(id_pessoa)
|
||||||
|
rank = found.posicao if found else None
|
||||||
|
|
||||||
|
consultor = await use_case.executar(id_pessoa=id_pessoa, rank=rank)
|
||||||
|
|
||||||
|
if not consultor:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Consultor {id_pessoa} não encontrado")
|
||||||
|
|
||||||
|
try:
|
||||||
|
pdf_service = PDFService()
|
||||||
|
raw_documento = await es_client.buscar_documento_completo(id_pessoa)
|
||||||
|
raw_source = raw_documento.get("_source") if raw_documento else {}
|
||||||
|
pdf_bytes = pdf_service.gerar_ficha_consultor(consultor, raw_source)
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
raise HTTPException(status_code=500, detail=f"Erro ao gerar PDF: {str(e)}")
|
||||||
|
|
||||||
|
nome_sanitizado = "".join(c if c.isalnum() or c in " -_" else "_" for c in consultor.nome)
|
||||||
|
nome_arquivo = f"ficha_consultor_{id_pessoa}_{nome_sanitizado[:30]}_{datetime.now().strftime('%Y%m%d')}.pdf"
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
BytesIO(pdf_bytes),
|
||||||
|
media_type="application/pdf",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": f'attachment; filename="{nome_arquivo}"'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useState, useRef, useEffect, useMemo, memo } from 'react';
|
import React, { useState, useRef, useEffect, useMemo, memo } from 'react';
|
||||||
import './ConsultorCard.css';
|
import './ConsultorCard.css';
|
||||||
import RawDataModal from './RawDataModal';
|
import RawDataModal from './RawDataModal';
|
||||||
|
import { rankingService } from '../services/api';
|
||||||
|
|
||||||
const SELOS = {
|
const SELOS = {
|
||||||
PRESID_CAMARA: { label: 'Presidente Camara', cor: 'selo-camara', icone: '👑' },
|
PRESID_CAMARA: { label: 'Presidente Camara', cor: 'selo-camara', icone: '👑' },
|
||||||
|
|||||||
@@ -80,6 +80,94 @@
|
|||||||
color: #c7d2fe;
|
color: #c7d2fe;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-export-pdf {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-self: center;
|
||||||
|
gap: 0.4rem;
|
||||||
|
padding: 0.5rem 0.9rem;
|
||||||
|
width: 80px;
|
||||||
|
min-width: 80px;
|
||||||
|
max-width: 80px;
|
||||||
|
height: 36px;
|
||||||
|
min-height: 36px;
|
||||||
|
max-height: 36px;
|
||||||
|
flex: 0 0 80px;
|
||||||
|
position: relative;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
background: linear-gradient(135deg, rgba(220, 38, 38, 0.2), rgba(185, 28, 28, 0.3));
|
||||||
|
border: 1px solid rgba(220, 38, 38, 0.5);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fca5a5;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 200ms ease, border-color 200ms ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-export-pdf-wrap {
|
||||||
|
width: 80px;
|
||||||
|
height: 36px;
|
||||||
|
flex: 0 0 80px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
align-self: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-export-pdf-wrap .btn-export-pdf {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-export-pdf:hover:not(:disabled) {
|
||||||
|
background: linear-gradient(135deg, rgba(220, 38, 38, 0.3), rgba(185, 28, 28, 0.4));
|
||||||
|
border-color: rgba(220, 38, 38, 0.7);
|
||||||
|
color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-export-pdf:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-export-pdf .pdf-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-export-pdf.loading .pdf-icon {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-export-pdf.loading span {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-export-pdf.loading::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border: 2px solid rgba(252, 165, 165, 0.35);
|
||||||
|
border-top-color: #fecaca;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin-pdf 0.9s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin-pdf {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
.raw-modal-close {
|
.raw-modal-close {
|
||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
@@ -505,6 +593,14 @@
|
|||||||
.raw-modal-header-actions {
|
.raw-modal-header-actions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-export-pdf {
|
||||||
|
width: 80px !important;
|
||||||
|
height: 36px !important;
|
||||||
|
flex: 0 0 auto !important;
|
||||||
|
align-self: center !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dados-pessoais-grid {
|
.dados-pessoais-grid {
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ const RawDataModal = ({ idPessoa, nome, onClose }) => {
|
|||||||
const [viewMode, setViewMode] = useState('formatted');
|
const [viewMode, setViewMode] = useState('formatted');
|
||||||
const [filterType, setFilterType] = useState('all');
|
const [filterType, setFilterType] = useState('all');
|
||||||
const [copyFeedback, setCopyFeedback] = useState(false);
|
const [copyFeedback, setCopyFeedback] = useState(false);
|
||||||
|
const [downloadingPDF, setDownloadingPDF] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -280,6 +281,19 @@ const RawDataModal = ({ idPessoa, nome, onClose }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDownloadPDF = async () => {
|
||||||
|
if (downloadingPDF) return;
|
||||||
|
setDownloadingPDF(true);
|
||||||
|
try {
|
||||||
|
await rankingService.downloadFichaPDF(idPessoa, nome);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Erro ao baixar PDF:', err);
|
||||||
|
alert('Erro ao gerar PDF. Tente novamente.');
|
||||||
|
} finally {
|
||||||
|
setDownloadingPDF(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const source = data?._source || {};
|
const source = data?._source || {};
|
||||||
const dadosPessoais = source.dadosPessoais || {};
|
const dadosPessoais = source.dadosPessoais || {};
|
||||||
const atuacoes = source.atuacoes || [];
|
const atuacoes = source.atuacoes || [];
|
||||||
@@ -303,6 +317,20 @@ const RawDataModal = ({ idPessoa, nome, onClose }) => {
|
|||||||
<span className="raw-modal-subtitle">{nome} (ID: {idPessoa})</span>
|
<span className="raw-modal-subtitle">{nome} (ID: {idPessoa})</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="raw-modal-header-actions">
|
<div className="raw-modal-header-actions">
|
||||||
|
<div className="btn-export-pdf-wrap">
|
||||||
|
<button
|
||||||
|
className={`btn-export-pdf ${downloadingPDF ? 'loading' : ''}`}
|
||||||
|
onClick={handleDownloadPDF}
|
||||||
|
disabled={downloadingPDF}
|
||||||
|
title="Exportar ficha completa em PDF"
|
||||||
|
>
|
||||||
|
<svg className="pdf-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||||
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||||
|
<polyline points="14 2 14 8 20 8"/>
|
||||||
|
</svg>
|
||||||
|
<span>PDF</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="view-toggle">
|
<div className="view-toggle">
|
||||||
<button
|
<button
|
||||||
className={viewMode === 'formatted' ? 'active' : ''}
|
className={viewMode === 'formatted' ? 'active' : ''}
|
||||||
|
|||||||
@@ -133,6 +133,29 @@ export const rankingService = {
|
|||||||
const response = await api.get(`/consultor/${idPessoa}/raw`);
|
const response = await api.get(`/consultor/${idPessoa}/raw`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async downloadFichaPDF(idPessoa, nomeConsultor = '') {
|
||||||
|
const response = await api.get(`/consultor/${idPessoa}/pdf`, {
|
||||||
|
responseType: 'blob',
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob = new Blob([response.data], { type: 'application/pdf' });
|
||||||
|
const url = window.URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
|
||||||
|
const nomeSanitizado = nomeConsultor
|
||||||
|
.replace(/[^a-zA-Z0-9\s\-_]/g, '')
|
||||||
|
.substring(0, 30)
|
||||||
|
.trim();
|
||||||
|
const dataAtual = new Date().toISOString().split('T')[0].replace(/-/g, '');
|
||||||
|
link.download = `ficha_consultor_${idPessoa}_${nomeSanitizado}_${dataAtual}.pdf`;
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
window.URL.revokeObjectURL(url);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
Reference in New Issue
Block a user