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;
|
this.uiManager = uiManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
exportToPDF() {
|
async exportToPDF() {
|
||||||
// Check if jsPDF is available
|
// Check if jsPDF is available
|
||||||
if (typeof window.jspdf === 'undefined') {
|
if (typeof window.jspdf === 'undefined') {
|
||||||
alert('PDF export library is loading. Please try again in a moment.');
|
alert('PDF export library is loading. Please try again in a moment.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Show model selection dialog
|
||||||
|
const selectedModels = await this.showModelSelectionDialog();
|
||||||
|
if (!selectedModels) {
|
||||||
|
return; // User cancelled
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { jsPDF } = window.jspdf;
|
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
|
let currentPage = 1;
|
||||||
doc.setFontSize(20);
|
let yPos = 30;
|
||||||
doc.setTextColor(0, 123, 255); // Bootstrap primary blue
|
|
||||||
doc.text('CSP ROI Calculator Report', 20, 25);
|
|
||||||
|
|
||||||
// Add generation date
|
// Helper function to add new page with header/footer
|
||||||
doc.setFontSize(10);
|
const addPage = () => {
|
||||||
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) {
|
|
||||||
doc.addPage();
|
doc.addPage();
|
||||||
yPos = 20;
|
currentPage++;
|
||||||
}
|
yPos = 30;
|
||||||
|
this.addPageHeader(doc, margin, colors);
|
||||||
|
};
|
||||||
|
|
||||||
yPos += 10;
|
// Helper function to check if we need a new page
|
||||||
doc.setFontSize(16);
|
const checkPageBreak = (requiredSpace) => {
|
||||||
doc.text('Executive Summary', 20, yPos);
|
if (yPos + requiredSpace > pageHeight - 30) {
|
||||||
yPos += 10;
|
addPage();
|
||||||
|
|
||||||
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;
|
// Title Page
|
||||||
doc.text('Key assumptions:', 25, yPos);
|
this.createTitlePage(doc, pageWidth, pageHeight, margin, colors);
|
||||||
yPos += 8;
|
|
||||||
doc.text('• Growth rates based on market analysis and industry benchmarks', 30, yPos);
|
// Executive Summary Page
|
||||||
yPos += 6;
|
addPage();
|
||||||
doc.text('• Churn rates reflect typical SaaS industry standards', 30, yPos);
|
yPos = this.createExecutiveSummary(doc, yPos, margin, colors);
|
||||||
yPos += 6;
|
|
||||||
doc.text('• Revenue calculations include grace period provisions', 30, yPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add footer
|
// Investment Parameters Page
|
||||||
const pageCount = doc.internal.getNumberOfPages();
|
checkPageBreak(60);
|
||||||
for (let i = 1; i <= pageCount; i++) {
|
yPos = this.createParametersSection(doc, yPos, margin, colors);
|
||||||
doc.setPage(i);
|
|
||||||
doc.setFontSize(8);
|
// Model Comparison Page
|
||||||
doc.setTextColor(150, 150, 150);
|
checkPageBreak(80);
|
||||||
doc.text(`Page ${i} of ${pageCount}`, doc.internal.pageSize.getWidth() - 30, doc.internal.pageSize.getHeight() - 10);
|
yPos = this.createModelComparison(doc, yPos, margin, colors);
|
||||||
doc.text('Generated by Servala CSP ROI Calculator', 20, doc.internal.pageSize.getHeight() - 10);
|
|
||||||
}
|
// 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
|
// 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);
|
doc.save(filename);
|
||||||
|
|
||||||
} catch (error) {
|
} 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() {
|
exportToCSV() {
|
||||||
try {
|
try {
|
||||||
// Create comprehensive CSV with summary and detailed data
|
// Create comprehensive CSV with summary and detailed data
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue