multi-currency support in roi calculator

This commit is contained in:
Tobias Brunner 2025-07-23 14:50:53 +02:00
parent adc3a6b905
commit 5cc6b779c5
Signed by: tobru
SSH key fingerprint: SHA256:kOXg1R6c11XW3/Pt9dbLdQvOJGFAy+B2K6v6PtRWBGQ
7 changed files with 231 additions and 45 deletions

View file

@ -131,7 +131,7 @@ class ExportManager {
doc.setFontSize(12);
doc.setTextColor(...colors.dark);
doc.text(`Investment Amount: ${this.formatCHF(inputs.investmentAmount)}`, pageWidth/2, boxY + 22, { align: 'center' });
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
@ -191,7 +191,7 @@ class ExportManager {
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(`Average Net Profit: ${this.formatCurrency(avgDirectNetPos)}`, margin + 5, yPos + 24);
doc.text('Performance-based with bonuses', margin + 5, yPos + 32);
// Loan Model Summary
@ -208,7 +208,7 @@ class ExportManager {
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(`Average Net Profit: ${this.formatCurrency(avgLoanNetPos)}`, loanBoxX + 5, yPos + 24);
doc.text('Fixed returns, guaranteed', loanBoxX + 5, yPos + 32);
yPos += 45;
@ -228,11 +228,11 @@ class ExportManager {
// Create parameter table
const params = [
['Investment Amount', this.formatCHF(inputs.investmentAmount)],
['Investment Amount', this.formatCurrency(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`],
['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`]
@ -271,9 +271,9 @@ class ExportManager {
if (directResult && loanResult) {
tableData.push([
scenarioName,
this.formatCHF(directResult.netPosition),
this.formatCurrency(directResult.netPosition),
this.uiManager.formatPercentage(directResult.roi),
this.formatCHF(loanResult.netPosition),
this.formatCurrency(loanResult.netPosition),
this.uiManager.formatPercentage(loanResult.roi)
]);
}
@ -339,7 +339,7 @@ class ExportManager {
tableData.push([
result.scenario,
result.investmentModel === 'direct' ? 'Direct' : 'Loan',
this.formatCHF(result.netPosition),
this.formatCurrency(result.netPosition),
this.uiManager.formatPercentage(result.roi),
result.breakEvenMonth ? `${result.breakEvenMonth} months` : 'N/A'
]);
@ -498,17 +498,24 @@ class ExportManager {
});
}
formatCHF(amount) {
formatCurrency(amount) {
try {
// Consistent CHF formatting: CHF in front, no decimals for whole numbers
return new Intl.NumberFormat('de-CH', {
// Get current currency from the page
const currencyElement = document.getElementById('currency');
const currency = currencyElement ? currencyElement.value : 'CHF';
// Determine locale based on currency
const locale = currency === 'EUR' ? 'de-DE' : 'de-CH';
// Consistent currency formatting: currency in front, no decimals for whole numbers
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: 'CHF',
currency: currency,
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(amount);
} catch (error) {
console.error('Error formatting CHF:', error);
console.error('Error formatting currency:', error);
return `CHF ${Math.round(amount).toLocaleString()}`;
}
}
@ -522,15 +529,16 @@ class ExportManager {
// Add input parameters
csvContent += 'INPUT PARAMETERS\n';
const inputs = this.calculator.getInputValues();
csvContent += `Currency,${inputs.currency}\n`;
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`;
csvContent += `Service Revenue per Instance (${inputs.currency}),${inputs.revenuePerInstance}\n`;
csvContent += `Core Service Revenue per Instance (${inputs.currency}),${inputs.coreServiceRevenue}\n`;
csvContent += `Total Revenue per Instance (${inputs.currency}),${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`;
@ -539,7 +547,7 @@ class ExportManager {
// Add scenario summary
csvContent += 'SCENARIO SUMMARY\n';
csvContent += 'Scenario,Investment Model,Final Instances,Total Revenue,CSP Revenue,Servala Revenue,ROI (%),Break-even (months)\n';
csvContent += `Scenario,Investment Model,Final Instances,Total Revenue (${inputs.currency}),CSP Revenue (${inputs.currency}),Servala Revenue (${inputs.currency}),ROI (%),Break-even (months)\n`;
Object.values(this.calculator.results).forEach(result => {
const modelText = result.investmentModel === 'loan' ? 'Loan' : 'Direct';
@ -550,7 +558,7 @@ class ExportManager {
// 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';
csvContent += `Month,Scenario,New Instances,Churned Instances,Total Instances,Service Revenue (${inputs.currency}),Core Revenue (${inputs.currency}),Total Revenue (${inputs.currency}),CSP Revenue (${inputs.currency}),Servala Revenue (${inputs.currency}),Cumulative CSP Revenue (${inputs.currency}),Cumulative Servala Revenue (${inputs.currency})\n`;
// Combine all monthly data
const allData = [];
@ -563,7 +571,7 @@ class ExportManager {
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`;
csvContent += `${data.month},${data.scenario},${data.newInstances},${data.churnedInstances},${data.totalInstances},${(data.serviceRevenue || data.monthlyRevenue || 0).toFixed(2)},${(data.coreRevenue || 0).toFixed(2)},${(data.totalRevenue || data.monthlyRevenue || 0).toFixed(2)},${data.cspRevenue.toFixed(2)},${data.servalaRevenue.toFixed(2)},${data.cumulativeCSPRevenue.toFixed(2)},${data.cumulativeServalaRevenue.toFixed(2)}\n`;
});
// Create and download file