(function () {
const STORAGE_KEY = "mhk_rechner_v1";
const DEFAULTS = {
mode: "projection",
netIncome: 3000,
liabilities: 0,
currentEquity: 0,
externalEquity: 10000,
monthlyBudget: 1000,
durationMonths: 24,
ekQuotePct: 20,
bausparSplitPct: 40,
cashSplitPct: 50,
goldSplitPct: 30,
btcSplitPct: 20,
bausparReturnPct: 2,
cashReturnPct: 2,
goldReturnPct: 4,
btcReturnPct: 12
};
const CalculatorEngine = {
computeFinanzierbar(netto) { return netto * 95; },
computeEkTarget(finanzierbar, ekQuotePct) { return finanzierbar * (ekQuotePct / 100); },
monthlyRate(annualPct) {
const annual = annualPct / 100;
return Math.pow(1 + annual, 1 / 12) - 1;
},
futureValueLumpSum(pv, annualRatePct, months) {
const r = this.monthlyRate(annualRatePct);
return pv * Math.pow(1 + r, months);
},
futureValueMonthly(contrib, annualRatePct, months, endOfMonth) {
const r = this.monthlyRate(annualRatePct);
if (months <= 0) return 0;
if (Math.abs(r) < 1e-12) return contrib * months;
const growthSeries = (Math.pow(1 + r, months) - 1) / r;
return endOfMonth ? contrib * growthSeries : contrib * growthSeries * (1 + r);
},
annuityFactor(annualRatePct, months, endOfMonth) {
if (months <= 0) return 0;
const r = this.monthlyRate(annualRatePct);
if (Math.abs(r) < 1e-12) return months;
const base = (Math.pow(1 + r, months) - 1) / r;
return endOfMonth ? base : base * (1 + r);
},
projectBuckets(input, monthlyContribution) {
const months = input.durationMonths;
const startTotal = input.currentEquity + input.externalEquity;
const bausparSplit = input.bausparSplitPct / 100;
const portfolioSplit = 1 - bausparSplit;
const cashSplit = input.cashSplitPct / 100;
const goldSplit = input.goldSplitPct / 100;
const btcSplit = input.btcSplitPct / 100;
const startBauspar = startTotal * bausparSplit;
const startPortfolio = startTotal * portfolioSplit;
const startCash = startPortfolio * cashSplit;
const startGold = startPortfolio * goldSplit;
const startBtc = startPortfolio * btcSplit;
const monthlyBauspar = monthlyContribution * bausparSplit;
const monthlyPortfolio = monthlyContribution * portfolioSplit;
const monthlyCash = monthlyPortfolio * cashSplit;
const monthlyGold = monthlyPortfolio * goldSplit;
const monthlyBtc = monthlyPortfolio * btcSplit;
const bauspar =
this.futureValueLumpSum(startBauspar, input.bausparReturnPct, months) +
this.futureValueMonthly(monthlyBauspar, input.bausparReturnPct, months, true);
const cash =
this.futureValueLumpSum(startCash, input.cashReturnPct, months) +
this.futureValueMonthly(monthlyCash, input.cashReturnPct, months, true);
const gold =
this.futureValueLumpSum(startGold, input.goldReturnPct, months) +
this.futureValueMonthly(monthlyGold, input.goldReturnPct, months, true);
const btc =
this.futureValueLumpSum(startBtc, input.btcReturnPct, months) +
this.futureValueMonthly(monthlyBtc, input.btcReturnPct, months, true);
return { bauspar: bauspar, cash: cash, gold: gold, btc: btc };
},
requiredMonthlyForTarget(input) {
const months = input.durationMonths;
const startTotal = input.currentEquity + input.externalEquity;
const bausparSplit = input.bausparSplitPct / 100;
const portfolioSplit = 1 - bausparSplit;
const cashSplit = input.cashSplitPct / 100;
const goldSplit = input.goldSplitPct / 100;
const btcSplit = input.btcSplitPct / 100;
const finanzierbar = this.computeFinanzierbar(input.netIncome);
const target = this.computeEkTarget(finanzierbar, input.ekQuotePct);
const fvStartBauspar = startTotal * bausparSplit * Math.pow(1 + this.monthlyRate(input.bausparReturnPct), months);
const fvStartCash = startTotal * portfolioSplit * cashSplit * Math.pow(1 + this.monthlyRate(input.cashReturnPct), months);
const fvStartGold = startTotal * portfolioSplit * goldSplit * Math.pow(1 + this.monthlyRate(input.goldReturnPct), months);
const fvStartBtc = startTotal * portfolioSplit * btcSplit * Math.pow(1 + this.monthlyRate(input.btcReturnPct), months);
const fvStart = fvStartBauspar + fvStartCash + fvStartGold + fvStartBtc;
const monthlyFactor =
bausparSplit * this.annuityFactor(input.bausparReturnPct, months, true) +
portfolioSplit * (
cashSplit * this.annuityFactor(input.cashReturnPct, months, true) +
goldSplit * this.annuityFactor(input.goldReturnPct, months, true) +
btcSplit * this.annuityFactor(input.btcReturnPct, months, true)
);
if (monthlyFactor <= 0) return 0;
return Math.max(0, (target - fvStart) / monthlyFactor);
},
run(input) {
const finanzierbar = this.computeFinanzierbar(input.netIncome);
const ekTarget = this.computeEkTarget(finanzierbar, input.ekQuotePct);
const requiredMonthly = this.requiredMonthlyForTarget(input);
const monthlyContribution = input.mode === "projection" ? input.monthlyBudget : requiredMonthly;
const buckets = this.projectBuckets(input, monthlyContribution);
const projectedTotal = buckets.bauspar + buckets.cash + buckets.gold + buckets.btc;
const gap = ekTarget - projectedTotal;
const progressRatio = ekTarget > 0 ? projectedTotal / ekTarget : 0;
return {
finanzierbar: finanzierbar,
ekTarget: ekTarget,
projectedTotal: projectedTotal,
requiredMonthly: requiredMonthly,
buckets: buckets,
gap: gap,
progressRatio: progressRatio,
onTrack: projectedTotal >= ekTarget
};
}
};
const currency = new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" });
const percent = new Intl.NumberFormat("de-DE", { style: "percent", maximumFractionDigits: 1 });
const ids = [
"netIncome","liabilities","currentEquity","externalEquity","monthlyBudget","durationMonths",
"ekQuotePct","bausparSplitPct","cashSplitPct","goldSplitPct","btcSplitPct",
"bausparReturnPct","cashReturnPct","goldReturnPct","btcReturnPct"
];
function getMode() {
const selected = document.querySelector('input[name="mode"]:checked');
return selected ? selected.value : "projection";
}
function setMode(mode) {
const radio = document.querySelector('input[name="mode"][value="' + mode + '"]');
if (radio) radio.checked = true;
}
function num(v) {
const parsed = Number(v);
return Number.isFinite(parsed) ? parsed : NaN;
}
function readInput() {
return {
mode: getMode(),
netIncome: num(document.getElementById("netIncome").value),
liabilities: num(document.getElementById("liabilities").value),
currentEquity: num(document.getElementById("currentEquity").value),
externalEquity: num(document.getElementById("externalEquity").value),
monthlyBudget: num(document.getElementById("monthlyBudget").value),
durationMonths: num(document.getElementById("durationMonths").value),
ekQuotePct: num(document.getElementById("ekQuotePct").value),
bausparSplitPct: num(document.getElementById("bausparSplitPct").value),
cashSplitPct: num(document.getElementById("cashSplitPct").value),
goldSplitPct: num(document.getElementById("goldSplitPct").value),
btcSplitPct: num(document.getElementById("btcSplitPct").value),
bausparReturnPct: num(document.getElementById("bausparReturnPct").value),
cashReturnPct: num(document.getElementById("cashReturnPct").value),
goldReturnPct: num(document.getElementById("goldReturnPct").value),
btcReturnPct: num(document.getElementById("btcReturnPct").value)
};
}
function validate(input) {
const errors = [];
const nonNegativeFields = [
["netIncome","Nettoeinkommen"],["liabilities","Verbindlichkeiten"],["currentEquity","Aktuelles Eigenkapital"],
["externalEquity","Eigenkapital aus anderen Quellen"],["monthlyBudget","Monatlich verfügbar"],["ekQuotePct","EK-Quote"],
["bausparSplitPct","Bauspar-Split"],["cashSplitPct","Cash-Split"],["goldSplitPct","Gold-Split"],["btcSplitPct","Bitcoin-Split"],
["bausparReturnPct","Bauspar-Rendite"],["cashReturnPct","Cash-Rendite"],["goldReturnPct","Gold-Rendite"],["btcReturnPct","Bitcoin-Rendite"]
];
for (let i = 0; i < nonNegativeFields.length; i++) {
const field = nonNegativeFields[i][0];
const label = nonNegativeFields[i][1];
if (!Number.isFinite(input[field])) errors.push(label + ": bitte einen gültigen Zahlenwert eingeben.");
else if (input[field] < 0) errors.push(label + ": darf nicht negativ sein.");
}
if (!Number.isFinite(input.durationMonths) || input.durationMonths <= 0 || !Number.isInteger(input.durationMonths)) {
errors.push("Laufzeit: bitte eine ganze Zahl größer 0 eingeben.");
}
if (input.bausparSplitPct > 100) errors.push("Bauspar-Split darf maximal 100% sein.");
const portfolioSum = input.cashSplitPct + input.goldSplitPct + input.btcSplitPct;
if (Math.abs(portfolioSum - 100) > 0.01) errors.push("Portfolio-Split (Cash/Gold/Bitcoin) muss 100% ergeben.");
return errors;
}
function renderErrors(errors) {
const box = document.getElementById("errors");
if (!box) return;
if (!errors.length) {
box.style.display = "none";
box.innerHTML = "";
return;
}
box.style.display = "block";
box.innerHTML = "
" + errors.map(function (e) { return "- " + e + "
"; }).join("") + "
";
}
function formatEur(value) { return currency.format(Number.isFinite(value) ? value : 0); }
function render(result) {
document.getElementById("kpiProjected").textContent = formatEur(result.projectedTotal);
document.getElementById("kpiAffordable").textContent = formatEur(result.finanzierbar);
document.getElementById("kpiTarget").textContent = formatEur(result.ekTarget);
document.getElementById("kpiRequiredMonthly").textContent = formatEur(result.requiredMonthly);
const gapLabel = result.gap > 0 ? "Lücke: " + formatEur(result.gap) : "Überschuss: " + formatEur(Math.abs(result.gap));
document.getElementById("gapText").textContent = gapLabel;
const capped = Math.max(0, Math.min(1.5, result.progressRatio));
document.getElementById("progressBar").style.width = Math.min(100, capped * 100) + "%";
document.getElementById("progressText").textContent = "Zielerreichung: " + percent.format(Math.max(0, result.progressRatio));
const statusEl = document.getElementById("statusText");
statusEl.textContent = result.onTrack ? "im Plan" : "unter Plan";
statusEl.className = "status " + (result.onTrack ? "ok" : "bad");
document.getElementById("bucketBauspar").textContent = formatEur(result.buckets.bauspar);
document.getElementById("bucketCash").textContent = formatEur(result.buckets.cash);
document.getElementById("bucketGold").textContent = formatEur(result.buckets.gold);
document.getElementById("bucketBtc").textContent = formatEur(result.buckets.btc);
}
function updateModeUi(mode) {
const monthlyField = document.getElementById("monthlyBudget");
if (monthlyField) monthlyField.disabled = mode === "required_monthly";
}
function setValues(values) {
setMode(values.mode || DEFAULTS.mode);
for (let i = 0; i < ids.length; i++) {
const id = ids[i];
const el = document.getElementById(id);
if (el) el.value = values[id];
}
updateModeUi(getMode());
}
function save(values) {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(values)); } catch (e) {}
}
function load() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw);
return Object.assign({}, DEFAULTS, parsed);
} catch (e) { return null; }
}
function recalc() {
const input = readInput();
const errors = validate(input);
renderErrors(errors);
updateModeUi(input.mode);
save(input);
if (errors.length) return;
const result = CalculatorEngine.run(input);
render(result);
}
function resetDefaults() {
setValues(DEFAULTS);
save(DEFAULTS);
recalc();
}
function init() {
if (!document.getElementById("netIncome")) return;
const saved = load();
setValues(saved || DEFAULTS);
for (let i = 0; i < ids.length; i++) {
const el = document.getElementById(ids[i]);
if (el) el.addEventListener("input", recalc);
}
const modeInputs = document.querySelectorAll('input[name="mode"]');
for (let i = 0; i < modeInputs.length; i++) {
modeInputs[i].addEventListener("change", recalc);
}
const resetBtn = document.getElementById("resetBtn");
if (resetBtn) resetBtn.addEventListener("click", resetDefaults);
recalc();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();