/** * Export Management Module * Handles PDF and CSV export functionality */ class ExportManager { constructor(calculator, uiManager) { this.calculator = calculator; this.uiManager = uiManager; } async exportToPDF() { // Check if jsPDF is available if (typeof window.jspdf === 'undefined') { alert('PDF export library is loading. Please try again in a moment.'); return; } // Show model selection dialog const selectedModels = await this.showModelSelectionDialog(); if (!selectedModels) { return; // User cancelled } try { const { jsPDF } = window.jspdf; const doc = new jsPDF('p', 'mm', 'a4'); const pageWidth = doc.internal.pageSize.getWidth(); const pageHeight = doc.internal.pageSize.getHeight(); const margin = 20; // Store selected models for use throughout the export this.selectedModels = selectedModels; // Color scheme const colors = { primary: [0, 123, 255], success: [40, 167, 69], warning: [255, 193, 7], danger: [220, 53, 69], dark: [33, 37, 41], muted: [108, 117, 125] }; let currentPage = 1; let yPos = 30; // Helper function to add new page with header/footer const addPage = () => { doc.addPage(); currentPage++; yPos = 30; this.addPageHeader(doc, margin, colors); }; // Helper function to check if we need a new page const checkPageBreak = (requiredSpace) => { if (yPos + requiredSpace > pageHeight - 30) { addPage(); } }; // Title Page this.createTitlePage(doc, pageWidth, pageHeight, margin, colors); // Executive Summary Page addPage(); yPos = this.createExecutiveSummary(doc, yPos, margin, colors); // Investment Parameters Page checkPageBreak(60); yPos = this.createParametersSection(doc, yPos, margin, colors); // Model Comparison Page checkPageBreak(80); yPos = this.createModelComparison(doc, yPos, margin, colors); // Charts Pages await this.addChartsToPDF(doc, colors, margin, addPage, checkPageBreak); // Detailed Results Table this.createDetailedResults(doc, colors, margin, addPage); // Add footers to all pages this.addFootersToAllPages(doc, currentPage, colors); // Save the PDF const filename = `servala-roi-investment-analysis-${new Date().toISOString().split('T')[0]}.pdf`; doc.save(filename); } catch (error) { console.error('PDF Export Error:', error); alert('An error occurred while generating the PDF. Please try again or export as CSV instead.'); } } createTitlePage(doc, pageWidth, pageHeight, margin, colors) { // Background design element doc.setFillColor(...colors.primary); doc.rect(0, 0, pageWidth, 60, 'F'); // White title text doc.setTextColor(255, 255, 255); doc.setFontSize(28); doc.setFont('helvetica', 'bold'); doc.text('Investment Analysis Report', pageWidth/2, 35, { align: 'center' }); // Subtitle doc.setFontSize(14); doc.setFont('helvetica', 'normal'); doc.text('CSP Partnership ROI Calculator', pageWidth/2, 45, { align: 'center' }); // Company section doc.setTextColor(...colors.dark); doc.setFontSize(20); doc.setFont('helvetica', 'bold'); doc.text('Servala Partnership Opportunity', pageWidth/2, 90, { align: 'center' }); // Investment summary box const inputs = this.calculator.getInputValues(); const boxY = 110; const boxHeight = 40; doc.setFillColor(248, 249, 250); doc.roundedRect(margin, boxY, pageWidth - 2*margin, boxHeight, 3, 3, 'F'); doc.setDrawColor(...colors.muted); doc.roundedRect(margin, boxY, pageWidth - 2*margin, boxHeight, 3, 3, 'S'); doc.setFontSize(16); doc.setTextColor(...colors.primary); doc.text('Investment Overview', pageWidth/2, boxY + 12, { align: 'center' }); doc.setFontSize(12); doc.setTextColor(...colors.dark); doc.text(`Investment Amount: ${this.formatCurrency(inputs.investmentAmount)}`, pageWidth/2, boxY + 22, { align: 'center' }); doc.text(`Analysis Period: ${inputs.timeframe} years`, pageWidth/2, boxY + 32, { align: 'center' }); // Generated date doc.setFontSize(10); doc.setTextColor(...colors.muted); const currentDate = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); doc.text(`Generated on: ${currentDate}`, pageWidth/2, pageHeight - 30, { align: 'center' }); } addPageHeader(doc, margin, colors) { doc.setFillColor(...colors.primary); doc.rect(0, 0, doc.internal.pageSize.getWidth(), 15, 'F'); doc.setTextColor(255, 255, 255); doc.setFontSize(10); doc.setFont('helvetica', 'bold'); doc.text('Servala CSP Investment Analysis', margin, 10); } createExecutiveSummary(doc, yPos, margin, colors) { doc.setFontSize(18); doc.setTextColor(...colors.primary); doc.setFont('helvetica', 'bold'); doc.text('Executive Summary', margin, yPos); yPos += 15; const enabledResults = Object.values(this.calculator.results); const directResults = enabledResults.filter(r => r.investmentModel === 'direct' && this.selectedModels.direct); const loanResults = enabledResults.filter(r => r.investmentModel === 'loan' && this.selectedModels.loan); doc.setFontSize(11); doc.setTextColor(...colors.dark); doc.setFont('helvetica', 'normal'); // Investment models comparison if (directResults.length > 0 && loanResults.length > 0) { const avgDirectROI = directResults.reduce((sum, r) => sum + r.roi, 0) / directResults.length; const avgLoanROI = loanResults.reduce((sum, r) => sum + r.roi, 0) / loanResults.length; const avgDirectNetPos = directResults.reduce((sum, r) => sum + r.netPosition, 0) / directResults.length; const avgLoanNetPos = loanResults.reduce((sum, r) => sum + r.netPosition, 0) / loanResults.length; doc.text('This analysis compares two investment models across multiple growth scenarios:', margin, yPos); yPos += 10; // Direct Investment Summary doc.setFillColor(40, 167, 69, 0.1); // Success color with opacity doc.roundedRect(margin, yPos, (doc.internal.pageSize.getWidth() - 3*margin)/2, 35, 2, 2, 'F'); doc.setTextColor(...colors.success); doc.setFont('helvetica', 'bold'); doc.text('Direct Investment Model', margin + 5, yPos + 8); doc.setTextColor(...colors.dark); doc.setFont('helvetica', 'normal'); doc.setFontSize(10); doc.text(`Average ROI: ${this.uiManager.formatPercentage(avgDirectROI)}`, margin + 5, yPos + 16); doc.text(`Average Net Profit: ${this.formatCurrency(avgDirectNetPos)}`, margin + 5, yPos + 24); doc.text('Performance-based with bonuses', margin + 5, yPos + 32); // Loan Model Summary const loanBoxX = margin + (doc.internal.pageSize.getWidth() - 3*margin)/2 + 10; doc.setFillColor(255, 193, 7, 0.1); // Warning color with opacity doc.roundedRect(loanBoxX, yPos, (doc.internal.pageSize.getWidth() - 3*margin)/2, 35, 2, 2, 'F'); doc.setTextColor(...colors.warning); doc.setFont('helvetica', 'bold'); doc.setFontSize(11); doc.text('Loan Model', loanBoxX + 5, yPos + 8); doc.setTextColor(...colors.dark); doc.setFont('helvetica', 'normal'); doc.setFontSize(10); doc.text(`Average ROI: ${this.uiManager.formatPercentage(avgLoanROI)}`, loanBoxX + 5, yPos + 16); doc.text(`Average Net Profit: ${this.formatCurrency(avgLoanNetPos)}`, loanBoxX + 5, yPos + 24); doc.text('Fixed returns, guaranteed', loanBoxX + 5, yPos + 32); yPos += 45; } return yPos; } createParametersSection(doc, yPos, margin, colors) { doc.setFontSize(16); doc.setTextColor(...colors.primary); doc.setFont('helvetica', 'bold'); doc.text('Investment Parameters', margin, yPos); yPos += 15; const inputs = this.calculator.getInputValues(); // Create parameter table const params = [ ['Investment Amount', this.formatCurrency(inputs.investmentAmount)], ['Investment Timeframe', `${inputs.timeframe} years`], ['Service Revenue per Instance', `${this.formatCurrency(inputs.revenuePerInstance)} / month`], ['Core Service Revenue per Instance', `${this.formatCurrency(inputs.coreServiceRevenue)} / month`], ['Total Revenue per Instance', `${this.formatCurrency(inputs.revenuePerInstance + inputs.coreServiceRevenue)} / month`], ['Loan Interest Rate', `${(inputs.loanInterestRate * 100).toFixed(1)}%`], ['Direct Investment Share', `${(inputs.servalaShare * 100).toFixed(0)}% to Servala`], ['Grace Period', `${inputs.gracePeriod} months`] ]; doc.setFontSize(11); params.forEach(([label, value]) => { doc.setTextColor(...colors.dark); doc.setFont('helvetica', 'normal'); doc.text(label + ':', margin + 5, yPos); doc.setFont('helvetica', 'bold'); doc.text(value, margin + 80, yPos); yPos += 8; }); return yPos + 10; } createModelComparison(doc, yPos, margin, colors) { doc.setFontSize(16); doc.setTextColor(...colors.primary); doc.setFont('helvetica', 'bold'); doc.text('Investment Model Comparison', margin, yPos); yPos += 15; // Create comparison table const scenarios = ['Conservative', 'Moderate', 'Aggressive']; const tableData = []; scenarios.forEach(scenarioName => { const directResult = Object.values(this.calculator.results) .find(r => r.scenario === scenarioName && r.investmentModel === 'direct'); const loanResult = Object.values(this.calculator.results) .find(r => r.scenario === scenarioName && r.investmentModel === 'loan'); if (directResult && loanResult) { tableData.push([ scenarioName, this.formatCurrency(directResult.netPosition), this.uiManager.formatPercentage(directResult.roi), this.formatCurrency(loanResult.netPosition), this.uiManager.formatPercentage(loanResult.roi) ]); } }); // Table headers const headers = ['Scenario', 'Direct Net Profit', 'Direct ROI', 'Loan Net Profit', 'Loan ROI']; const colWidths = [30, 35, 25, 35, 25]; // Draw table this.drawTable(doc, margin, yPos, headers, tableData, colWidths, colors); return yPos + (tableData.length + 2) * 8; } async addChartsToPDF(doc, colors, margin, addPage, checkPageBreak) { // Add charts by capturing them as images const chartIds = ['instanceGrowthChart', 'revenueChart', 'cashFlowChart', 'modelComparisonChart']; const chartTitles = [ 'ROI Progression Over Time', 'Net Financial Position', 'Performance Comparison', 'Investment Model Comparison' ]; for (let i = 0; i < chartIds.length; i++) { checkPageBreak(120); const canvas = document.getElementById(chartIds[i]); if (canvas) { // Add chart title doc.setFontSize(14); doc.setTextColor(...colors.primary); doc.setFont('helvetica', 'bold'); doc.text(chartTitles[i], margin, doc.internal.pageSize.getHeight() - 250); // Capture chart as image const imgData = canvas.toDataURL('image/png', 1.0); doc.addImage(imgData, 'PNG', margin, doc.internal.pageSize.getHeight() - 240, doc.internal.pageSize.getWidth() - 2*margin, 100); if (i < chartIds.length - 1) addPage(); } } } createDetailedResults(doc, colors, margin, addPage) { addPage(); let yPos = 30; doc.setFontSize(16); doc.setTextColor(...colors.primary); doc.setFont('helvetica', 'bold'); doc.text('Detailed Financial Results', margin, yPos); yPos += 15; // Create detailed results table const headers = ['Scenario', 'Model', 'Net Profit', 'ROI', 'Break-even']; const colWidths = [35, 25, 35, 25, 30]; const tableData = []; Object.values(this.calculator.results).forEach(result => { tableData.push([ result.scenario, result.investmentModel === 'direct' ? 'Direct' : 'Loan', this.formatCurrency(result.netPosition), this.uiManager.formatPercentage(result.roi), result.breakEvenMonth ? `${result.breakEvenMonth} months` : 'N/A' ]); }); this.drawTable(doc, margin, yPos, headers, tableData, colWidths, colors); } drawTable(doc, x, y, headers, data, colWidths, colors) { const rowHeight = 8; const startX = x; let currentY = y; // Draw headers doc.setFillColor(...colors.primary); doc.rect(startX, currentY - 6, colWidths.reduce((sum, w) => sum + w, 0), rowHeight, 'F'); doc.setTextColor(255, 255, 255); doc.setFont('helvetica', 'bold'); doc.setFontSize(10); let currentX = startX; headers.forEach((header, i) => { doc.text(header, currentX + 2, currentY - 1); currentX += colWidths[i]; }); currentY += rowHeight; // Draw data rows doc.setTextColor(...colors.dark); doc.setFont('helvetica', 'normal'); data.forEach((row, rowIndex) => { if (rowIndex % 2 === 1) { doc.setFillColor(248, 249, 250); doc.rect(startX, currentY - 6, colWidths.reduce((sum, w) => sum + w, 0), rowHeight, 'F'); } currentX = startX; row.forEach((cell, i) => { doc.text(String(cell), currentX + 2, currentY - 1); currentX += colWidths[i]; }); currentY += rowHeight; }); // Draw table border doc.setDrawColor(...colors.muted); doc.rect(startX, y - 6, colWidths.reduce((sum, w) => sum + w, 0), (data.length + 1) * rowHeight, 'S'); } addFootersToAllPages(doc, totalPages, colors) { for (let i = 1; i <= totalPages; i++) { doc.setPage(i); const pageHeight = doc.internal.pageSize.getHeight(); const pageWidth = doc.internal.pageSize.getWidth(); doc.setFontSize(8); doc.setTextColor(...colors.muted); doc.text(`Page ${i} of ${totalPages}`, pageWidth - 30, pageHeight - 10); doc.text('Generated by Servala CSP ROI Calculator', 20, pageHeight - 10); // Add a line above footer doc.setDrawColor(...colors.muted); doc.line(20, pageHeight - 15, pageWidth - 20, pageHeight - 15); } } async showModelSelectionDialog() { return new Promise((resolve) => { // Create modal dialog const modal = document.createElement('div'); modal.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; z-index: 10000; `; const dialog = document.createElement('div'); dialog.style.cssText = ` background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); max-width: 400px; width: 90%; `; dialog.innerHTML = `
Choose which models to include in your PDF report: