Fix PDF export: switch to browser print, fix page breaks and bullet alignment
- Replace html2canvas + jsPDF with window.print() in both handleExportPDF and handleDownloadReport — browser print properly respects CSS break-inside: avoid on agent cards and page-break-before on proof pages, eliminating orphaned section headings - Add listStylePosition: 'outside' and explicit lineHeight to <ul>/<li> elements in PDFReport so bullet symbols sit at the text baseline - Add pageBreakInside: 'avoid' alongside existing breakInside: 'avoid' on agent cards for cross-browser compatibility - Replace placeholder shield icon and plain-text Oliver SVG on cover page with BAR-ModComms-logo-v4.png (Barclays eagle) and styled Oliver wordmark Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
13a3ff87cc
commit
fdb5a2d961
3 changed files with 73 additions and 72 deletions
|
|
@ -1205,50 +1205,50 @@ const CampaignDetail: React.FC<{
|
|||
reportRootEl.style.top = '0px';
|
||||
reportRootEl.style.zIndex = '-1';
|
||||
document.body.appendChild(reportRootEl);
|
||||
|
||||
|
||||
const reactRoot = ReactDOM.createRoot(reportRootEl);
|
||||
|
||||
|
||||
try {
|
||||
const proofsWithLatestVersion = proofsToExport.map(p => ({
|
||||
...p,
|
||||
versions: [p.versions[0]], // Only render the latest version
|
||||
}));
|
||||
|
||||
|
||||
reactRoot.render(<PDFReport campaignName={campaignName} proofs={proofsWithLatestVersion} />);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait for render and images
|
||||
|
||||
const { default: jspdf } = await import('jspdf');
|
||||
const { default: html2canvas } = await import('html2canvas');
|
||||
await new Promise(resolve => setTimeout(resolve, 1500)); // Wait for render and images
|
||||
|
||||
const reportContent = reportRootEl.children[0] as HTMLElement;
|
||||
if (!reportContent) throw new Error("PDF report element not found");
|
||||
|
||||
const canvas = await html2canvas(reportContent, { scale: 2, useCORS: true });
|
||||
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
const pdf = new jspdf('p', 'mm', 'a4', true);
|
||||
const pdfWidth = pdf.internal.pageSize.getWidth();
|
||||
const pdfHeight = pdf.internal.pageSize.getHeight();
|
||||
const canvasWidth = canvas.width;
|
||||
const canvasHeight = canvas.height;
|
||||
const ratio = canvasWidth / pdfWidth;
|
||||
const pagedCanvasHeight = canvasHeight / ratio;
|
||||
|
||||
let heightLeft = pagedCanvasHeight;
|
||||
let position = 0;
|
||||
|
||||
pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, pagedCanvasHeight, undefined, 'FAST');
|
||||
heightLeft -= pdfHeight;
|
||||
|
||||
while (heightLeft > 0) {
|
||||
position -= pdfHeight;
|
||||
pdf.addPage();
|
||||
pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, pagedCanvasHeight, undefined, 'FAST');
|
||||
heightLeft -= pdfHeight;
|
||||
}
|
||||
|
||||
pdf.save(`${fileName.replace(/[^a-zA-Z0-9]/g, '_')}.pdf`);
|
||||
|
||||
const safeTitle = fileName.replace(/[^a-zA-Z0-9 _-]/g, '_');
|
||||
const printWindow = window.open('', '_blank');
|
||||
if (!printWindow) throw new Error("Popup blocked. Please allow popups for this site and try again.");
|
||||
|
||||
printWindow.document.write(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>${safeTitle}</title>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
@page { size: A4; margin: 0; }
|
||||
* { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; box-sizing: border-box; }
|
||||
body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
|
||||
ul { list-style-position: outside; padding-left: 20px; margin: 4px 0; }
|
||||
li { line-height: 1.5; margin-bottom: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>${reportContent.outerHTML}</body>
|
||||
</html>`);
|
||||
printWindow.document.close();
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
const timeout = setTimeout(resolve, 3000);
|
||||
printWindow.onload = () => { clearTimeout(timeout); resolve(); };
|
||||
});
|
||||
|
||||
printWindow.print();
|
||||
setTimeout(() => { try { printWindow.close(); } catch (_) {} }, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to generate PDF:", error);
|
||||
|
|
@ -1618,38 +1618,40 @@ const ProofDetailView: React.FC<{
|
|||
|
||||
reactRoot.render(<PDFReport campaignName={campaignName} proofs={[proofForReport]} />);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
const { default: jspdf } = await import('jspdf');
|
||||
const { default: html2canvas } = await import('html2canvas');
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
const reportContent = reportRootEl.children[0] as HTMLElement;
|
||||
if (!reportContent) throw new Error("PDF report element not found");
|
||||
|
||||
const canvas = await html2canvas(reportContent, { scale: 2, useCORS: true });
|
||||
|
||||
const imgData = canvas.toDataURL('image/png');
|
||||
const pdf = new jspdf('p', 'mm', 'a4', true);
|
||||
const pdfWidth = pdf.internal.pageSize.getWidth();
|
||||
const pdfHeight = pdf.internal.pageSize.getHeight();
|
||||
const ratio = canvas.width / pdfWidth;
|
||||
const pagedCanvasHeight = canvas.height / ratio;
|
||||
|
||||
let heightLeft = pagedCanvasHeight;
|
||||
let position = 0;
|
||||
|
||||
pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, pagedCanvasHeight, undefined, 'FAST');
|
||||
heightLeft -= pdfHeight;
|
||||
|
||||
while (heightLeft > 0) {
|
||||
position -= pdfHeight;
|
||||
pdf.addPage();
|
||||
pdf.addImage(imgData, 'PNG', 0, position, pdfWidth, pagedCanvasHeight, undefined, 'FAST');
|
||||
heightLeft -= pdfHeight;
|
||||
}
|
||||
|
||||
const fileName = `${campaignName} - ${proof.proofName} V${selectedVersion.version} Report`;
|
||||
pdf.save(`${fileName.replace(/[^a-zA-Z0-9]/g, '_')}.pdf`);
|
||||
const safeTitle = fileName.replace(/[^a-zA-Z0-9 _-]/g, '_');
|
||||
const printWindow = window.open('', '_blank');
|
||||
if (!printWindow) throw new Error("Popup blocked. Please allow popups for this site and try again.");
|
||||
|
||||
printWindow.document.write(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>${safeTitle}</title>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
@page { size: A4; margin: 0; }
|
||||
* { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; box-sizing: border-box; }
|
||||
body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
|
||||
ul { list-style-position: outside; padding-left: 20px; margin: 4px 0; }
|
||||
li { line-height: 1.5; margin-bottom: 6px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>${reportContent.outerHTML}</body>
|
||||
</html>`);
|
||||
printWindow.document.close();
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
const timeout = setTimeout(resolve, 3000);
|
||||
printWindow.onload = () => { clearTimeout(timeout); resolve(); };
|
||||
});
|
||||
|
||||
printWindow.print();
|
||||
setTimeout(() => { try { printWindow.close(); } catch (_) {} }, 1000);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to generate PDF:", error);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import React from 'react';
|
||||
import type { AgentReview, SubReview, RagStatus, OverallStatus } from '../types';
|
||||
import { BarclaysLogo } from './icons/BarclaysLogo';
|
||||
import { OliverLogo } from './icons/OliverLogo';
|
||||
import type { AgentReview, RagStatus } from '../types';
|
||||
import { LegalIcon } from './icons/LegalIcon';
|
||||
import { BrandIcon } from './icons/BrandIcon';
|
||||
import { ChannelIcon } from './icons/ChannelIcon';
|
||||
|
|
@ -9,6 +7,7 @@ import { ChannelIcon } from './icons/ChannelIcon';
|
|||
interface PDFReportProps {
|
||||
campaignName: string;
|
||||
proofs: any[];
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -75,9 +74,9 @@ const formatFeedbackTextForPDF = (text: string): React.ReactNode => {
|
|||
<p style={{ margin: '0 0 8px 0' }}>{renderBoldMarkdownForPDF(introLines.join(' '))}</p>
|
||||
)}
|
||||
{bulletGroups.length > 0 && (
|
||||
<ul style={{ margin: 0, paddingLeft: '18px', listStyleType: 'disc' }}>
|
||||
<ul style={{ margin: 0, paddingLeft: '20px', listStyleType: 'disc', listStylePosition: 'outside' }}>
|
||||
{bulletGroups.map((group, index) => (
|
||||
<li key={index} style={{ marginBottom: '8px' }}>
|
||||
<li key={index} style={{ marginBottom: '8px', lineHeight: '1.5' }}>
|
||||
{group.map((line, lineIdx) => (
|
||||
<React.Fragment key={lineIdx}>
|
||||
{lineIdx > 0 && <br />}
|
||||
|
|
@ -110,7 +109,7 @@ const RagStatusBadge: React.FC<{ status: RagStatus }> = ({ status }) => {
|
|||
};
|
||||
|
||||
|
||||
export const PDFReport: React.FC<PDFReportProps> = ({ campaignName, proofs }) => {
|
||||
export const PDFReport: React.FC<PDFReportProps> = ({ campaignName, proofs, baseUrl = window.location.origin }) => {
|
||||
const today = new Date().toLocaleDateString('en-GB', {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
|
|
@ -125,8 +124,8 @@ export const PDFReport: React.FC<PDFReportProps> = ({ campaignName, proofs }) =>
|
|||
{/* --- Cover Page --- */}
|
||||
<div style={{ width: '210mm', height: '297mm', display: 'flex', flexDirection: 'column', padding: '20mm', boxSizing: 'border-box', borderBottom: '1px solid #e5e5e5' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', paddingBottom: '10mm', borderBottom: '1px solid #e5e5e5' }}>
|
||||
<BarclaysLogo style={{ height: '50px', width: 'auto', color: '#001f5a' }} />
|
||||
<OliverLogo style={{ height: '25px', width: 'auto' }} />
|
||||
<img src={`${baseUrl}/BAR-ModComms-logo-v4.png`} alt="Mod Comms AI — In partnership with Barclays" style={{ height: '60px', width: 'auto' }} />
|
||||
<div style={{ fontFamily: 'Arial, sans-serif', fontSize: '22px', fontWeight: '900', letterSpacing: '3px', color: '#1a1a1a', textTransform: 'uppercase' }}>Oliver</div>
|
||||
</div>
|
||||
<div style={{ flexGrow: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', textAlign: 'center' }}>
|
||||
<h1 style={{ fontSize: '42px', color: '#001f5a', margin: '0 0 10px 0' }}>AI Compliance & Brand Report</h1>
|
||||
|
|
@ -203,7 +202,7 @@ export const PDFReport: React.FC<PDFReportProps> = ({ campaignName, proofs }) =>
|
|||
{/* Detailed Agent Feedback */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8mm' }}>
|
||||
{agentReviews.map(({ title, review, icon }) => (
|
||||
<div key={title} style={{ border: '1px solid #e5e5e5', borderRadius: '4px', padding: '5mm', backgroundColor: '#fff', breakInside: 'avoid' }}>
|
||||
<div key={title} style={{ border: '1px solid #e5e5e5', borderRadius: '4px', padding: '5mm', backgroundColor: '#fff', breakInside: 'avoid', pageBreakInside: 'avoid' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
|
||||
<h4 style={{ fontSize: '16px', fontWeight: 'bold', color: '#001f5a', margin: 0, display: 'flex', alignItems: 'center', gap: '8px'}}>
|
||||
<span style={{color: '#0070c0'}}>{icon}</span> {title}
|
||||
|
|
@ -214,8 +213,8 @@ export const PDFReport: React.FC<PDFReportProps> = ({ campaignName, proofs }) =>
|
|||
{review.issues && review.issues.length > 0 && (
|
||||
<div>
|
||||
<h5 style={{ fontSize: '13px', fontWeight: 'bold', margin: '0 0 5px 0' }}>Key Actions:</h5>
|
||||
<ul style={{ margin: 0, paddingLeft: '18px', fontSize: '12px', lineHeight: 1.5, listStyleType: 'disc' }}>
|
||||
{review.issues.map((issue, i) => <li key={i} style={{ marginBottom: '4px' }}>{issue}</li>)}
|
||||
<ul style={{ margin: 0, paddingLeft: '20px', fontSize: '12px', lineHeight: 1.5, listStyleType: 'disc', listStylePosition: 'outside' }}>
|
||||
{review.issues.map((issue, i) => <li key={i} style={{ marginBottom: '4px', lineHeight: '1.5' }}>{issue}</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
// Shield with checkmark - Heroicons "shield-check" (outline)
|
||||
// Shield with checkmark - used as Barclays brand icon in UI (Login, LoadingVisual)
|
||||
export const BarclaysLogo: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue