import { jsPDF } from 'jspdf'; interface ReportData { reprintCost: number; updatesPerYear: number; annualWaste: number; qrMasterCost: number; savings: number; savingsPercent: number; } export function generateSavingsReport(data: ReportData): void { const doc = new jsPDF(); const pageWidth = doc.internal.pageSize.getWidth(); const pageHeight = doc.internal.pageSize.getHeight(); const margin = 20; // Brand Colors const colors = { primary: [79, 70, 229] as [number, number, number], // Indigo 600 primaryLight: [224, 231, 255] as [number, number, number], // Indigo 100 success: [16, 185, 129] as [number, number, number], // Emerald 500 successBg: [236, 253, 245] as [number, number, number], // Emerald 50 text: [30, 41, 59] as [number, number, number], // Slate 800 textLight: [100, 116, 139] as [number, number, number], // Slate 500 border: [226, 232, 240] as [number, number, number], // Slate 200 danger: [239, 68, 68] as [number, number, number], // Red 500 dangerBg: [254, 242, 242] as [number, number, number], // Red 50 }; let currentY = 20; // --- Header --- // Logo / Brand Name doc.setTextColor(...colors.primary); doc.setFontSize(24); doc.setFont('helvetica', 'bold'); doc.text('QR Master', margin, currentY + 8); // Date (Right aligned) doc.setTextColor(...colors.textLight); doc.setFontSize(10); doc.setFont('helvetica', 'normal'); const dateStr = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); doc.text(dateStr, pageWidth - margin, currentY + 8, { align: 'right' }); currentY += 20; // Title Section doc.setLineWidth(0.5); doc.setDrawColor(...colors.primary); doc.line(margin, currentY, margin + 40, currentY); // Small accent line currentY += 10; doc.setTextColor(...colors.text); doc.setFontSize(28); doc.setFont('helvetica', 'bold'); doc.text('Savings Analysis', margin, currentY); currentY += 8; doc.setTextColor(...colors.textLight); doc.setFontSize(12); doc.setFont('helvetica', 'normal'); doc.text('Prepared specifically for your business based on your input parameters.', margin, currentY); currentY += 20; // --- Input Summary --- // Background for inputs doc.setFillColor(248, 250, 252); // Slate 50 doc.setDrawColor(...colors.border); doc.roundedRect(margin, currentY, pageWidth - (margin * 2), 35, 2, 2, 'FD'); const inputInnerY = currentY + 12; // Label 1 doc.setFontSize(10); doc.setTextColor(...colors.textLight); doc.text('REPRINT COST PER BATCH', margin + 10, inputInnerY); // Value 1 doc.setFontSize(16); doc.setTextColor(...colors.text); doc.setFont('helvetica', 'bold'); doc.text(`€${data.reprintCost.toLocaleString()}`, margin + 10, inputInnerY + 10); // Divider doc.setDrawColor(...colors.border); doc.line(pageWidth / 2, inputInnerY - 5, pageWidth / 2, inputInnerY + 18); // Label 2 doc.setFontSize(10); doc.setFont('helvetica', 'normal'); doc.setTextColor(...colors.textLight); doc.text('URL UPDATES/YEAR', (pageWidth / 2) + 10, inputInnerY); // Value 2 doc.setFontSize(16); doc.setTextColor(...colors.text); doc.setFont('helvetica', 'bold'); doc.text(`${data.updatesPerYear}`, (pageWidth / 2) + 10, inputInnerY + 10); currentY += 50; // --- Analysis Results --- doc.setFontSize(14); doc.setTextColor(...colors.text); doc.setFont('helvetica', 'bold'); doc.text('Cost Analysis', margin, currentY); currentY += 10; // 1. Static Codes (Bad) const cardHeight = 26; // Reduced height // perfect vertical centering: (Height 26 / 2) + (Approx cap-height correction ~3) = 16 const textCenterOffset = 16; // Draw "Static" Row doc.setDrawColor(...colors.border); doc.setFillColor(255, 255, 255); doc.roundedRect(margin, currentY, pageWidth - (margin * 2), cardHeight, 2, 2, 'FD'); // Icon/Label doc.setTextColor(...colors.danger); doc.setFontSize(11); doc.setFont('helvetica', 'bold'); doc.text('• Static QR Codes', margin + 8, currentY + textCenterOffset); doc.setTextColor(...colors.textLight); doc.setFontSize(10); doc.setFont('helvetica', 'normal'); // Align sub-label relative to first label doc.text('(Print & Reprint Costs)', margin + 50, currentY + textCenterOffset); doc.setTextColor(...colors.text); doc.setFontSize(12); doc.setFont('helvetica', 'bold'); doc.text(`€${data.annualWaste.toLocaleString()}`, pageWidth - margin - 10, currentY + textCenterOffset, { align: 'right' }); currentY += cardHeight + 4; // Draw "QR Master" Row doc.setDrawColor(...colors.border); doc.setFillColor(255, 255, 255); doc.roundedRect(margin, currentY, pageWidth - (margin * 2), cardHeight, 2, 2, 'FD'); // Icon/Label doc.setTextColor(...colors.success); doc.setFontSize(11); doc.setFont('helvetica', 'bold'); doc.text('• QR Master Pro', margin + 8, currentY + textCenterOffset); doc.setTextColor(...colors.textLight); doc.setFontSize(10); doc.setFont('helvetica', 'normal'); doc.text('(Subscription)', margin + 50, currentY + textCenterOffset); doc.setTextColor(...colors.text); doc.setFontSize(12); doc.setFont('helvetica', 'bold'); doc.text(`€${data.qrMasterCost.toLocaleString()}`, pageWidth - margin - 10, currentY + textCenterOffset, { align: 'right' }); currentY += cardHeight + 15; // --- PAGE CHANGE: Results & Strategy on Page 2 --- doc.addPage(); currentY = 40; // --- TOTAL SAVINGS / INVESTMENT HERO SECTION --- // (No line divider needed at top of new page) const isPositiveSavings = data.savings > 0; const heroColor = isPositiveSavings ? colors.success : colors.primary; // Green or Indigo const heroBg = isPositiveSavings ? colors.successBg : colors.primaryLight; doc.setTextColor(...colors.textLight); doc.setFontSize(11); doc.setFont('helvetica', 'bold'); const heroTitle = isPositiveSavings ? 'PROJECTED ANNUAL SAVINGS' : 'ANNUAL INVESTMENT REQUIRED'; doc.text(heroTitle, margin, currentY); // Big Number and Badge Container const savingsY = currentY + 12; // 1. The Amount doc.setTextColor(...heroColor); doc.setFontSize(42); doc.setFont('helvetica', 'bold'); const amountText = `€${Math.abs(data.savings).toLocaleString()}`; doc.text(amountText, margin, savingsY + 12); // 2. The Percentage Badge (Centered relative to the number) const numberWidth = doc.getStringUnitWidth(amountText) * 42 / doc.internal.scaleFactor; const badgeX = margin + numberWidth + 10; // Slightly tighter gap // Determine badge text and dynamic width doc.setFontSize(9); doc.setFont('helvetica', 'bold'); const badgeText = isPositiveSavings ? `${data.savingsPercent}% Saved` : 'Upgrade'; const textWidth = doc.getStringUnitWidth(badgeText) * 9 / doc.internal.scaleFactor; const badgePadding = 12; // 6px on each side const badgeWidth = textWidth + badgePadding; const badgeHeight = 14; // Reduced height (was 16) // Draw Compact Badge doc.setFillColor(...heroBg); doc.setDrawColor(...heroColor); doc.setLineWidth(0.5); doc.roundedRect(badgeX, savingsY + 1, badgeWidth, badgeHeight, 6, 6, 'FD'); // y+1 to align better with text baseline doc.setTextColor(...heroColor); doc.text(badgeText, badgeX + (badgeWidth / 2), savingsY + 10, { align: 'center' }); // Centered currentY += 40; // Reduced gap // --- Recommended Resources --- doc.setTextColor(...colors.text); doc.setFontSize(12); doc.setFont('helvetica', 'bold'); doc.text('Recommended Reading', margin, currentY); currentY += 8; const resources = [ { title: "The ROI of Dynamic QR Codes", url: "www.qrmaster.net/blog/roi-dynamic-qr" }, { title: "How to Track Physical Marketing", url: "www.qrmaster.net/blog/tracking-guide" }, { title: "Best Practices for QR Campaigns", url: "www.qrmaster.net/blog/best-practices" } ]; resources.forEach(res => { doc.setTextColor(...colors.primary); doc.setFontSize(10); doc.setFont('helvetica', 'normal'); doc.textWithLink(`• ${res.title}`, margin, currentY, { url: `https://${res.url}` }); currentY += 5; }); currentY += 12; // --- Call to Action --- // No page check needed, we are on Page 2 doc.setTextColor(...colors.text); doc.setFontSize(12); doc.setFont('helvetica', 'bold'); doc.text('Next Steps', margin, currentY); currentY += 8; doc.setTextColor(...colors.textLight); doc.setFontSize(10); doc.setFont('helvetica', 'normal'); doc.text('Ready to eliminate reprint costs? Create your account today.', margin, currentY); currentY += 8; // FIXED: Restored spacing to prevent overlap doc.setTextColor(...colors.primary); doc.setFont('helvetica', 'bold'); doc.textWithLink('www.qrmaster.net/signup', margin, currentY, { url: 'https://www.qrmaster.net/signup' }); currentY += 20; // --- Why Dynamic? (Value Add) --- // If we are close to bottom, force page 2 for this section to make it look intentional if (currentY > pageHeight - 60) { doc.addPage(); currentY = 40; } doc.setTextColor(...colors.text); doc.setFontSize(12); doc.setFont('helvetica', 'bold'); doc.text('Why Professionals Choose Dynamic', margin, currentY); currentY += 10; const benefits = [ { title: "Real-time Control", desc: "Update destinations instantly. Fix mistakes locally without reprinting." }, { title: "Smart Analytics", desc: "Track scans, locations, and device types to measure ROI." }, { title: "Brand Identity", desc: "Fully customizable designs that match your corporate identity." } ]; benefits.forEach(benefit => { // Bullet doc.setFillColor(...colors.success); // Green bullets doc.circle(margin + 1, currentY - 1, 1.5, 'F'); // Title doc.setTextColor(...colors.text); doc.setFontSize(10); doc.setFont('helvetica', 'bold'); doc.text(benefit.title, margin + 6, currentY); // Desc const titleWidth = doc.getStringUnitWidth(benefit.title) * 10 / doc.internal.scaleFactor; doc.setTextColor(...colors.textLight); doc.setFont('helvetica', 'normal'); doc.text(`- ${benefit.desc}`, margin + 6 + titleWidth + 2, currentY); currentY += 7; }); // --- Footer --- const footerY = pageHeight - 15; const pageCount = doc.getNumberOfPages(); for (let i = 1; i <= pageCount; i++) { doc.setPage(i); doc.setDrawColor(...colors.border); doc.line(margin, footerY - 10, pageWidth - margin, footerY - 10); doc.setTextColor(...colors.textLight); doc.setFontSize(8); doc.setFont('helvetica', 'normal'); doc.text('© 2026 QR Master. All rights reserved.', margin, footerY); doc.text('www.qrmaster.net', pageWidth - margin, footerY, { align: 'right' }); } // Save doc.save('QRMaster_Savings_Analysis.pdf'); }