diff --git a/hub/services/static/js/roi-calculator/export-manager.js b/hub/services/static/js/roi-calculator/export-manager.js index 53cd3fe..338d665 100644 --- a/hub/services/static/js/roi-calculator/export-manager.js +++ b/hub/services/static/js/roi-calculator/export-manager.js @@ -8,149 +8,83 @@ class ExportManager { this.uiManager = uiManager; } - exportToPDF() { + 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(); + 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] + }; - // Add header - doc.setFontSize(20); - doc.setTextColor(0, 123, 255); // Bootstrap primary blue - doc.text('CSP ROI Calculator Report', 20, 25); + let currentPage = 1; + let yPos = 30; - // Add generation date - doc.setFontSize(10); - doc.setTextColor(100, 100, 100); - const currentDate = new Date().toLocaleDateString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric' - }); - doc.text(`Generated on: ${currentDate}`, 20, 35); - - // Reset text color - doc.setTextColor(0, 0, 0); - - // Add input parameters section - doc.setFontSize(16); - doc.text('Investment Parameters', 20, 50); - - const inputs = this.calculator.getInputValues(); - let yPos = 60; - - doc.setFontSize(11); - const params = [ - ['Investment Amount:', this.uiManager.formatCurrencyDetailed(inputs.investmentAmount)], - ['Investment Timeframe:', `${inputs.timeframe} years`], - ['Investment Model:', inputs.investmentModel === 'loan' ? 'Loan Model' : 'Direct Investment'], - ...(inputs.investmentModel === 'loan' ? [['Loan Interest Rate:', `${(inputs.loanInterestRate * 100).toFixed(1)}%`]] : []), - ['Revenue per Instance:', this.uiManager.formatCurrencyDetailed(inputs.revenuePerInstance)], - ...(inputs.investmentModel === 'direct' ? [ - ['Servala Revenue Share:', `${(inputs.servalaShare * 100).toFixed(0)}%`], - ['Grace Period:', `${inputs.gracePeriod} months`] - ] : []) - ]; - - params.forEach(([label, value]) => { - doc.text(label, 25, yPos); - doc.text(value, 80, yPos); - yPos += 8; - }); - - // Add scenario results section - yPos += 10; - doc.setFontSize(16); - doc.text('Scenario Results', 20, yPos); - yPos += 10; - - doc.setFontSize(11); - Object.values(this.calculator.results).forEach(result => { - if (yPos > 250) { - doc.addPage(); - yPos = 20; - } - - // Scenario header - doc.setFontSize(14); - doc.setTextColor(0, 123, 255); - doc.text(`${result.scenario} Scenario`, 25, yPos); - yPos += 10; - - doc.setFontSize(11); - doc.setTextColor(0, 0, 0); - - const resultData = [ - ['Final Instances:', result.finalInstances.toLocaleString()], - ['Total Revenue:', this.uiManager.formatCurrencyDetailed(result.totalRevenue)], - ['CSP Revenue:', this.uiManager.formatCurrencyDetailed(result.cspRevenue)], - ['Servala Revenue:', this.uiManager.formatCurrencyDetailed(result.servalaRevenue)], - ['ROI:', this.uiManager.formatPercentage(result.roi)], - ['Break-even:', result.breakEvenMonth ? `${result.breakEvenMonth} months` : 'Not achieved'] - ]; - - resultData.forEach(([label, value]) => { - doc.text(label, 30, yPos); - doc.text(value, 90, yPos); - yPos += 7; - }); - - yPos += 8; - }); - - // Add summary section - if (yPos > 220) { + // Helper function to add new page with header/footer + const addPage = () => { doc.addPage(); - yPos = 20; - } + currentPage++; + yPos = 30; + this.addPageHeader(doc, margin, colors); + }; - yPos += 10; - doc.setFontSize(16); - doc.text('Executive Summary', 20, yPos); - yPos += 10; - - doc.setFontSize(11); - const enabledResults = Object.values(this.calculator.results); - if (enabledResults.length > 0) { - const avgROI = enabledResults.reduce((sum, r) => sum + r.roi, 0) / enabledResults.length; - const avgBreakeven = enabledResults.filter(r => r.breakEvenMonth).reduce((sum, r) => sum + r.breakEvenMonth, 0) / enabledResults.filter(r => r.breakEvenMonth).length; - - doc.text(`This analysis evaluates ${enabledResults.length} growth scenario(s) over a ${inputs.timeframe}-year period.`, 25, yPos); - yPos += 8; - doc.text(`Average projected ROI: ${this.uiManager.formatPercentage(avgROI)}`, 25, yPos); - yPos += 8; - if (!isNaN(avgBreakeven)) { - doc.text(`Average break-even timeline: ${Math.round(avgBreakeven)} months`, 25, yPos); - yPos += 8; + // Helper function to check if we need a new page + const checkPageBreak = (requiredSpace) => { + if (yPos + requiredSpace > pageHeight - 30) { + addPage(); } + }; - yPos += 5; - doc.text('Key assumptions:', 25, yPos); - yPos += 8; - doc.text('• Growth rates based on market analysis and industry benchmarks', 30, yPos); - yPos += 6; - doc.text('• Churn rates reflect typical SaaS industry standards', 30, yPos); - yPos += 6; - doc.text('• Revenue calculations include grace period provisions', 30, yPos); - } + // Title Page + this.createTitlePage(doc, pageWidth, pageHeight, margin, colors); + + // Executive Summary Page + addPage(); + yPos = this.createExecutiveSummary(doc, yPos, margin, colors); - // Add footer - const pageCount = doc.internal.getNumberOfPages(); - for (let i = 1; i <= pageCount; i++) { - doc.setPage(i); - doc.setFontSize(8); - doc.setTextColor(150, 150, 150); - doc.text(`Page ${i} of ${pageCount}`, doc.internal.pageSize.getWidth() - 30, doc.internal.pageSize.getHeight() - 10); - doc.text('Generated by Servala CSP ROI Calculator', 20, doc.internal.pageSize.getHeight() - 10); - } + // 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-csp-roi-report-${new Date().toISOString().split('T')[0]}.pdf`; + const filename = `servala-roi-investment-analysis-${new Date().toISOString().split('T')[0]}.pdf`; doc.save(filename); } catch (error) { @@ -159,6 +93,424 @@ class ExportManager { } } + 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`], + ['Revenue per Instance', `${this.formatCHF(inputs.revenuePerInstance)} / 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