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:
Vadym Samoilenko 2026-03-05 13:06:12 +00:00
parent 13a3ff87cc
commit fdb5a2d961
3 changed files with 73 additions and 72 deletions

View file

@ -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);

View file

@ -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>
)}

View file

@ -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"