/** * 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.formatCHF(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.formatCHF(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.formatCHF(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.formatCHF(inputs.investmentAmount)], ['Investment Timeframe', `${inputs.timeframe} years`], ['Service Revenue per Instance', `${this.formatCHF(inputs.revenuePerInstance)} / month`], ['Core Service Revenue per Instance', `${this.formatCHF(inputs.coreServiceRevenue)} / month`], ['Total Revenue per Instance', `${this.formatCHF(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.formatCHF(directResult.netPosition), this.uiManager.formatPercentage(directResult.roi), this.formatCHF(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.formatCHF(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 = `
Select Investment Models for PDF

Choose which models to include in your PDF report:

Performance-based revenue sharing with scaling bonuses
Fixed interest lending with guaranteed returns
`; modal.appendChild(dialog); document.body.appendChild(modal); // Handle button clicks document.getElementById('pdf-cancel-btn').addEventListener('click', () => { document.body.removeChild(modal); resolve(null); }); document.getElementById('pdf-export-btn').addEventListener('click', () => { const directSelected = document.getElementById('pdf-direct-model').checked; const loanSelected = document.getElementById('pdf-loan-model').checked; if (!directSelected && !loanSelected) { alert('Please select at least one investment model.'); return; } document.body.removeChild(modal); resolve({ direct: directSelected, loan: loanSelected }); }); // Close on background click modal.addEventListener('click', (e) => { if (e.target === modal) { document.body.removeChild(modal); resolve(null); } }); }); } formatCHF(amount) { try { // Consistent CHF formatting: CHF in front, no decimals for whole numbers return new Intl.NumberFormat('de-CH', { style: 'currency', currency: 'CHF', minimumFractionDigits: 0, maximumFractionDigits: 0 }).format(amount); } catch (error) { console.error('Error formatting CHF:', error); return `CHF ${Math.round(amount).toLocaleString()}`; } } exportToCSV() { try { // Create comprehensive CSV with summary and detailed data let csvContent = 'CSP ROI Calculator Export\n'; csvContent += `Generated on: ${new Date().toLocaleDateString()}\n\n`; // Add input parameters csvContent += 'INPUT PARAMETERS\n'; const inputs = this.calculator.getInputValues(); csvContent += `Investment Amount,${inputs.investmentAmount}\n`; csvContent += `Timeframe (years),${inputs.timeframe}\n`; csvContent += `Investment Model,${inputs.investmentModel === 'loan' ? 'Loan Model' : 'Direct Investment'}\n`; if (inputs.investmentModel === 'loan') { csvContent += `Loan Interest Rate (%),${(inputs.loanInterestRate * 100).toFixed(1)}\n`; } csvContent += `Service Revenue per Instance,${inputs.revenuePerInstance}\n`; csvContent += `Core Service Revenue per Instance,${inputs.coreServiceRevenue}\n`; csvContent += `Total Revenue per Instance,${inputs.revenuePerInstance + inputs.coreServiceRevenue}\n`; if (inputs.investmentModel === 'direct') { csvContent += `Servala Share (%),${(inputs.servalaShare * 100).toFixed(0)}\n`; csvContent += `Grace Period (months),${inputs.gracePeriod}\n`; } csvContent += '\n'; // Add scenario summary csvContent += 'SCENARIO SUMMARY\n'; csvContent += 'Scenario,Investment Model,Final Instances,Total Revenue,CSP Revenue,Servala Revenue,ROI (%),Break-even (months)\n'; Object.values(this.calculator.results).forEach(result => { const modelText = result.investmentModel === 'loan' ? 'Loan' : 'Direct'; csvContent += `${result.scenario},${modelText},${result.finalInstances},${result.totalRevenue.toFixed(2)},${result.cspRevenue.toFixed(2)},${result.servalaRevenue.toFixed(2)},${result.roi.toFixed(2)},${result.breakEvenMonth || 'N/A'}\n`; }); csvContent += '\n'; // Add detailed monthly data csvContent += 'MONTHLY BREAKDOWN\n'; csvContent += 'Month,Scenario,New Instances,Churned Instances,Total Instances,Monthly Revenue,CSP Revenue,Servala Revenue,Cumulative CSP Revenue,Cumulative Servala Revenue\n'; // Combine all monthly data const allData = []; Object.keys(this.calculator.monthlyData).forEach(scenario => { this.calculator.monthlyData[scenario].forEach(monthData => { allData.push(monthData); }); }); allData.sort((a, b) => a.month - b.month || a.scenario.localeCompare(b.scenario)); allData.forEach(data => { csvContent += `${data.month},${data.scenario},${data.newInstances},${data.churnedInstances},${data.totalInstances},${data.monthlyRevenue.toFixed(2)},${data.cspRevenue.toFixed(2)},${data.servalaRevenue.toFixed(2)},${data.cumulativeCSPRevenue.toFixed(2)},${data.cumulativeServalaRevenue.toFixed(2)}\n`; }); // Create and download file const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); const filename = `servala-csp-roi-data-${new Date().toISOString().split('T')[0]}.csv`; link.setAttribute('download', filename); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); // Clean up URL.revokeObjectURL(url); } catch (error) { console.error('CSV Export Error:', error); alert('An error occurred while generating the CSV file. Please try again.'); } } }