redesign ROI PDF
This commit is contained in:
parent
c1ed95eff5
commit
a07788cb74
1 changed files with 478 additions and 126 deletions
|
|
@ -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;
|
||||
|
||||
// Add header
|
||||
doc.setFontSize(20);
|
||||
doc.setTextColor(0, 123, 255); // Bootstrap primary blue
|
||||
doc.text('CSP ROI Calculator Report', 20, 25);
|
||||
// Store selected models for use throughout the export
|
||||
this.selectedModels = selectedModels;
|
||||
|
||||
// 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);
|
||||
// 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]
|
||||
};
|
||||
|
||||
// Reset text color
|
||||
doc.setTextColor(0, 0, 0);
|
||||
let currentPage = 1;
|
||||
let yPos = 30;
|
||||
|
||||
// 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) {
|
||||
// Helper function to add new page with header/footer
|
||||
const addPage = () => {
|
||||
doc.addPage();
|
||||
yPos = 20;
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
// Scenario header
|
||||
doc.setFontSize(14);
|
||||
doc.setTextColor(0, 123, 255);
|
||||
doc.text(`${result.scenario} Scenario`, 25, yPos);
|
||||
yPos += 10;
|
||||
// Title Page
|
||||
this.createTitlePage(doc, pageWidth, pageHeight, margin, colors);
|
||||
|
||||
doc.setFontSize(11);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
// Executive Summary Page
|
||||
addPage();
|
||||
yPos = this.createExecutiveSummary(doc, yPos, margin, colors);
|
||||
|
||||
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']
|
||||
];
|
||||
// Investment Parameters Page
|
||||
checkPageBreak(60);
|
||||
yPos = this.createParametersSection(doc, yPos, margin, colors);
|
||||
|
||||
resultData.forEach(([label, value]) => {
|
||||
doc.text(label, 30, yPos);
|
||||
doc.text(value, 90, yPos);
|
||||
yPos += 7;
|
||||
});
|
||||
// Model Comparison Page
|
||||
checkPageBreak(80);
|
||||
yPos = this.createModelComparison(doc, yPos, margin, colors);
|
||||
|
||||
yPos += 8;
|
||||
});
|
||||
// Charts Pages
|
||||
await this.addChartsToPDF(doc, colors, margin, addPage, checkPageBreak);
|
||||
|
||||
// Add summary section
|
||||
if (yPos > 220) {
|
||||
doc.addPage();
|
||||
yPos = 20;
|
||||
}
|
||||
// Detailed Results Table
|
||||
this.createDetailedResults(doc, colors, margin, addPage);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
// 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 = `
|
||||
<h5 style="margin-bottom: 1rem; color: #007bff;">Select Investment Models for PDF</h5>
|
||||
<p style="margin-bottom: 1.5rem; color: #6c757d;">Choose which models to include in your PDF report:</p>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label style="display: flex; align-items: center; margin-bottom: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" id="pdf-direct-model" checked style="margin-right: 0.5rem;">
|
||||
<span style="color: #28a745; font-weight: bold;">Direct Investment Model</span>
|
||||
</label>
|
||||
<small style="color: #6c757d; margin-left: 1.5rem;">Performance-based revenue sharing with scaling bonuses</small>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 2rem;">
|
||||
<label style="display: flex; align-items: center; margin-bottom: 0.5rem; cursor: pointer;">
|
||||
<input type="checkbox" id="pdf-loan-model" checked style="margin-right: 0.5rem;">
|
||||
<span style="color: #ffc107; font-weight: bold;">Loan Model</span>
|
||||
</label>
|
||||
<small style="color: #6c757d; margin-left: 1.5rem;">Fixed interest lending with guaranteed returns</small>
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; justify-content: flex-end;">
|
||||
<button id="pdf-cancel-btn" style="padding: 0.5rem 1rem; border: 1px solid #ccc; background: white; border-radius: 4px; cursor: pointer;">Cancel</button>
|
||||
<button id="pdf-export-btn" style="padding: 0.5rem 1rem; border: none; background: #007bff; color: white; border-radius: 4px; cursor: pointer;">Export PDF</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue