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:
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useRef, useEffect, useMemo, memo } from 'react';
|
||||
import './ConsultorCard.css';
|
||||
import RawDataModal from './RawDataModal';
|
||||
import { rankingService } from '../services/api';
|
||||
|
||||
const SELOS = {
|
||||
PRESID_CAMARA: { label: 'Presidente Camara', cor: 'selo-camara', icone: '👑' },
|
||||
|
||||
@@ -80,6 +80,94 @@
|
||||
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 {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
@@ -505,6 +593,14 @@
|
||||
.raw-modal-header-actions {
|
||||
width: 100%;
|
||||
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 {
|
||||
|
||||
@@ -240,6 +240,7 @@ const RawDataModal = ({ idPessoa, nome, onClose }) => {
|
||||
const [viewMode, setViewMode] = useState('formatted');
|
||||
const [filterType, setFilterType] = useState('all');
|
||||
const [copyFeedback, setCopyFeedback] = useState(false);
|
||||
const [downloadingPDF, setDownloadingPDF] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
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 dadosPessoais = source.dadosPessoais || {};
|
||||
const atuacoes = source.atuacoes || [];
|
||||
@@ -303,6 +317,20 @@ const RawDataModal = ({ idPessoa, nome, onClose }) => {
|
||||
<span className="raw-modal-subtitle">{nome} (ID: {idPessoa})</span>
|
||||
</div>
|
||||
<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">
|
||||
<button
|
||||
className={viewMode === 'formatted' ? 'active' : ''}
|
||||
|
||||
Reference in New Issue
Block a user