1/** 2 * avr-main.js â Altersvorsorgedepot Rechner: Core Calculator 3 * 4 * Depends on: 5 * - Chart.js (CDN) 6 * - avr-pdf.js â window.AVR_PDF.generatePDF 7 * - avr-modal.js â window.AVR_MODAL.openEmailModal / closeEmailModal / submitEmailForm 8 * 9 * Load order: avr-pdf.js â avr-modal.js â avr-main.js 10 * 11 * Sections: 12 * 1. State & constants 13 * 2. DOM helpers 14 * 3. View switching 15 * 4. Financial calculations 16 * 5. calc() â orchestrator 17 * 6. Charts 18 * 7. Kinder UI 19 * 8. init() 20 */ 21(function () { 22 'use strict'; 23 24 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 25 * 1. State & constants 26 * âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 27 // âââ CalcConstants â mirrors Dart CalcConstants exactly âââââââââââââââââ 28 var DEPOT_START = 2027; 29 var RETIRE_AGE = 67; 30 var PAYOUT_YEARS = 18; 31 var GZ_STUFE1_RATE = 0.50; 32 var GZ_STUFE1_CAP = 360.0; 33 var GZ_STUFE2_RATE = 0.25; 34 var GZ_MAX_BEITRAG = 1800.0; 35 var GZ_MAX_VERTRAG = 6840.0; 36 var KZ_MAX = 300.0; 37 var BONUS_BETRAG = 200.0; 38 var BONUS_MAX_AGE = 25; 39 var MINDESTBEITRAG = 120.0; 40 var TAX_GF = 12348; 41 var TAX_Z2 = 17799; 42 var TAX_Z3 = 69878; 43 var TAX_Z4 = 277825; 44 var TAX_Z2A = 914.51; 45 var TAX_Z2B = 1400; 46 var TAX_Z3A = 173.10; 47 var TAX_Z3B = 2397; 48 var TAX_Z3C = 1034.87; 49 var TAX_Z4O = 11135.63; 50 var TAX_Z5O = 19470.38; 51 var TFS = 0.30; 52 var ABGST_BASE = 0.26375; 53 var KAPEST_RATE = 0.25; 54 var SOLI_RATE = 0.055; 55 var VP_DRAG = 0.003; 56 var VP_FAKTOR = 6.5 / 12; 57 var ERTRAGSANTEIL_67 = 0.17; 58 var BANK_RATE = 0.02; 59 var AVR_KOSTEN = 0.000; 60 var ETF_KOSTEN = 0.000; 61 var INFLATION_RATE = 0.000; 62 var KIRCHENSTEUER = 0; 63 // ââ GRV pension estimation (mirrors Dart EntgeltpunkteEstimator) ââ 64 var RENTENWERT = 39.32; // â¬/Entgeltpunkt/Monat (West, Jul 2024, DRV) 65 var DURCHSCHNITTSENTGELT = 45358.0; // Durchschnittsentgelt 2024 (BBF) 66 var BBG = 90600.0; // Beitragsbemessungsgrenze 2024, West 67 var ARBEITSBEGINN = 25; // Assumed career start age (conservative: Uni-Abschluss) 68 var KINDER_MAX_ALTER = 25; 69 var MAX_KINDER = 6; 70 var BIRTH_MIN = DEPOT_START - 25; 71 var BIRTH_MAX = DEPOT_START + 23; 72 73 var children = []; // Array of { by: number|null, ta: 18|25 } 74 var gc = null; // Chart.js instance â view-1 75 var gc2 = null; // Chart.js instance â view-2 76 var lastChartData = { lbl: [], dO: [], dO2: [], dT: [] }; 77 78 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 79 * 2. DOM helpers 80 * âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 81 function root() { return document.querySelector('.avr'); } 82 function $id(id) { return root().querySelector('#' + id); } 83 function $all(selector) { return root().querySelectorAll(selector); } 84 85 function fmt(n) { 86 return new Intl.NumberFormat('de-DE', { maximumFractionDigits: 0 }).format(n) + '\u00a0\u20ac'; 87 } 88 89 function setAll(cls, text) { 90 $all(cls).forEach(function (el) { el.textContent = text; }); 91 } 92 93 function setEuro(cls, value) { setAll(cls, fmt(value)); } 94 95 function paintSlider(el) { 96 var pct = ((parseFloat(el.value) - parseFloat(el.min)) / 97 (parseFloat(el.max) - parseFloat(el.min))) * 100; 98 el.style.background = 99 'linear-gradient(to right, #F47323 0%, #F47323 ' + pct + '%, #e8ddd7 ' + pct + '%, #e8ddd7 100%)'; 100 } 101 102 function syncSliderDisplay(inp, cls, decimals) { 103 var txt = parseFloat(inp.value).toLocaleString('de-DE', { 104 minimumFractionDigits: decimals, 105 maximumFractionDigits: decimals 106 });
107 $all(cls).forEach(function (el) { el.textContent = txt; }); 108 } 109 110 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 111 * 3. View switching 112 * âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 113 function showView(n) { 114 root().classList.toggle('is-view2', n === 2); 115 if (n === 2 && lastChartData.lbl.length) { 116 setTimeout(function () { 117 renderChart2(lastChartData.lbl, lastChartData.dO, lastChartData.dO2, lastChartData.dT); 118 }, 20); 119 } 120 // scroll to top of calculator on every view switch 121 var r = root(); 122 if (r) { 123 setTimeout(function () { 124 r.scrollIntoView({ behavior: 'smooth', block: 'start' }); 125 }, 30); 126 } 127 // Move focus to the leading element of the new view 128 setTimeout(function () { 129 var target = n === 2 ? $id('back-btn') : $id('cta-btn'); 130 if (target) target.focus(); 131 }, 50); 132 133 if (n === 2){ 134 window.dataLayer = window.dataLayer || []; 135 window.dataLayer.push({ 136 'event': 'calculator_submit', 137 'button_text': 'Dein Rentenreport', //Populate dynamically 138 'lead_type': 'avd_report_download' 139 }); 140 } 141 142 143 144 } 145 146 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 147 * 4. Financial calculations 148 * âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 149 function yearlySubsidy(annualOwn, kids, taxRate, year) { 150 // Grundzulage requires minimum own contribution of 120 â¬/year 151 var gzEligible = annualOwn >= MINDESTBEITRAG; 152 var capped = Math.min(annualOwn, GZ_MAX_BEITRAG); 153 var gz = gzEligible 154 ? Math.min(capped, GZ_STUFE1_CAP) * GZ_STUFE1_RATE + Math.max(Math.min(capped - GZ_STUFE1_CAP, GZ_MAX_BEITRAG - GZ_STUFE1_CAP), 0) * GZ_STUFE2_RATE 155 : 0; 156 var kz = annualOwn > 0 157 ? kids.reduce(function (sum, c) { 158 if (!c.by) return sum; 159 var childAge = year - c.by; 160 if (childAge < 0 || childAge >= c.ta) return sum; 161 return sum + Math.min(annualOwn, KZ_MAX); 162 }, 0) 163 : 0; 164 var zulagen = gz + kz; 165 var basisBetrag = Math.min(annualOwn, GZ_MAX_BEITRAG) + gz + kz; 166 return { 167 gz: gz, 168 kz: kz, 169 zul: zulagen, 170 tax: Math.max(basisBetrag * taxRate - zulagen, 0) 171 }; 172 } 173 174 function calcEinkommensteuer(zve) { 175 if (zve <= TAX_GF) return 0; 176 if (zve <= TAX_Z2) { var y = (zve - TAX_GF) / 10000; return (TAX_Z2A * y + TAX_Z2B) * y; } 177 if (zve <= TAX_Z3) { var z = (zve - TAX_Z2) / 10000; return (TAX_Z3A * z + TAX_Z3B) * z + TAX_Z3C; } 178 if (zve <= TAX_Z4) return 0.42 * zve - TAX_Z4O; 179 return 0.45 * zve - TAX_Z5O; 180 } 181 182 function calcIncrementalRetaxRate(avPayout, baseIncome) { 183 if (avPayout <= 0) return 0; 184 return (calcEinkommensteuer(baseIncome + avPayout) - calcEinkommensteuer(baseIncome)) / avPayout; 185 } 186 187 function buildCapital(age, annualOwn, kids, taxRate, annualReturn, bonus200) { 188 var years = RETIRE_AGE - age; 189 var netReturn = annualReturn - AVR_KOSTEN; 190 var jbGef = Math.min(annualOwn, GZ_MAX_BEITRAG); 191 var jbUngef = Math.max(0, annualOwn - GZ_MAX_BEITRAG); 192 var capGef = 0, capUngef = 0, totalOwn = 0, totalZul = 0, totalTax = 0; 193 var balByYear = [], ownByYear = [], zulByYear = []; 194 195 for (var i = 0; i < years; i++) { 196 var s = yearlySubsidy(jbGef, kids, taxRate, DEPOT_START + i); 197 var bonus = (i === 0 ? bonus200 : 0); 198 var inflowGef = jbGef + s.zul + bonus; 199 capGef = (capGef + inflowGef) * (1 + netReturn); 200 if (jbUngef > 0) capUngef = (capUngef + jbUngef) * (1 + netReturn); 201 totalOwn += annualOwn; 202 totalZul += s.zul + bonus; 203 totalTax += s.tax; 204 balByYear.push(capGef + capUngef); 205 ownByYear.push(totalOwn); 206 zulByYear.push(totalZul); 207 } 208 return { cap: capGef + capUngef, capGef: capGef, capUngef: capUngef, 209 tO: totalOwn, tZ: totalZul, tT: totalTax, 210 bal: balByYear, own: ownByYear, zul: zulByYear }; 211 } 212 213 function calcPension(grossCap, annualReturn, taxRetirement) { 214 var n = PAYOUT_YEARS * 12; 215 var rM = Math.pow(1 + annualReturn, 1 / 12) - 1; 216 var brutto = (rM < 1e-9) ? grossCap / n : grossCap * rM / (1 - Math.pow(1 + rM, -n)); 217 return { b: brutto, n: brutto * (1 - taxRetirement) }; 218 } 219 220 /** 221 * ETF / MSCI World â EXACT MATCH to Excel "ETF - Sparplan" tab. 222 * 223 * Frey method (Vorabpauschale annual, liquidation tax at end): 224 * cap(new) = start à (1+r) + contrib à (1+r/2) â VAPtax 225 * VAP = (start à Basiszins + contrib à Basiszins/2) à (1âTFS)² 226 * VAPtax = VAP à ABGST 227 * 228 * At liquidation: 229 * ertragForTax = (cap â totalContrib) + cumVAPtax â gross gain incl. VAP credits
230 * taxableGain = ertragForTax à (1âTFS) â cumVAP 231 * liqTax = taxableGain à ABGST 232 * netto = cap â liqTax 233 * 234 * Returns full breakdown object (all values for display + .cap = netto). 235 */ 236 function buildETFCapital(years, annualContrib, annualReturn, abgstOverride) { 237 var ABGST = abgstOverride || ABGST_BASE; 238 var netReturn = annualReturn - ETF_KOSTEN; 239 var cap = 0, totalContrib = 0, vpGesamt = 0; 240 for (var i = 0; i < years; i++) { 241 var depotSoY = cap; 242 cap = (cap + annualContrib) * (1 + netReturn); 243 var vpBase = depotSoY + annualContrib * VP_FAKTOR; 244 var vpJahr = vpBase * VP_DRAG; 245 cap -= vpJahr; 246 vpGesamt += vpJahr; 247 totalContrib += annualContrib; 248 } 249 var grossFV = 0; 250 for (var i = 0; i < years; i++) { grossFV = (grossFV + annualContrib) * (1 + annualReturn - ETF_KOSTEN); } 251 var gewinn = cap - totalContrib; 252 var ertragForTax = gewinn + vpGesamt; // gross gain: net gain + cumulative VP credits (§19 Abs. 1 InvStG) 253 var taxableGain = Math.max(0, ertragForTax * (1 - TFS) - vpGesamt); 254 var liqTax = taxableGain * ABGST; 255 var netto = cap - liqTax; 256 return { 257 cap: netto, brutto: cap, grossFV: grossFV, totalContrib: totalContrib, 258 ertrag: gewinn, ertragForTax: ertragForTax, ertragGross: grossFV - totalContrib, 259 cumVAP: vpGesamt, cumVAPTax: vpGesamt, taxableGain: taxableGain, 260 liqTax: liqTax, totalTax: vpGesamt + liqTax 261 }; 262 } 263 264 function buildBankCapital(years, annualContrib, bankRate) { 265 var ABGST = 0.26375; 266 267 var cap = 0, totalContrib = 0, cumTax = 0; 268 269 for (var i = 0; i < years; i++) { 270 var zinsertrag = cap * bankRate + annualContrib * bankRate * 0.5; 271 var tax = zinsertrag * ABGST; 272 cap = cap * (1 + bankRate) + annualContrib * (1 + bankRate / 2) - tax; 273 totalContrib += annualContrib; 274 cumTax += tax; 275 } 276 277 // Gross Future Value (no taxes) â for display 278 var grossFV = 0; 279 for (var i = 0; i < years; i++) { grossFV = grossFV * (1 + bankRate) + annualContrib; } 280 281 return { 282 cap: cap, // Endkapital netto â .cmp-bank-cap 283 brutto: cap + cumTax, // Endkapital brutto (vor Steuern) â .cmp-bank-gross 284 grossFV: grossFV, // Endkapital brutto (kein Steuer) â .cmp-bank-gross-fv 285 totalContrib: totalContrib, // Einzahlungen 286 ertragGross: (cap + cumTax) - totalContrib, // Zinsertrag brutto â .cmp-bank-ertrag-gross (matches XLS) 287 ertrag: cap - totalContrib, // Ertrag netto â .cmp-bank-ertrag 288 cumTax: cumTax // Abgeltungssteuer gesamt â .cmp-bank-abgst 289 }; 290 } 291 292 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 293 * 5. calc() â main orchestrator 294 * âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 295 function calc() { 296 var age = parseInt($id('age').value, 10); 297 var monthly = parseFloat($id('contrib').value); 298 var retRate = parseFloat($id('ret').value) / 100; 299 var taxNow = parseFloat($id('taxrate').value); 300 var taxRetir = parseFloat($id('retax').value) / 100; 301 var bonus200 = age < BONUS_MAX_AGE ? BONUS_BETRAG : 0; 302 var years = RETIRE_AGE - age; 303 if (years <= 0) return; 304 305 // ââ Fixed defaults ââ 306 var bruttoMidpoints = {'0.21': 13000, '0.30': 27000, '0.37': 47000, '0.42': 65000}; 307 var bruttoJahr = bruttoMidpoints[String(taxNow)] || 45000; 308 var kirchensteuer = KIRCHENSTEUER; 309 var kapest = KAPEST_RATE; 310 var abgst = ABGST_BASE; 311 var inflationRate = INFLATION_RATE; 312 313 var acc = buildCapital(age, monthly * 12, children, taxNow, retRate, bonus200); 314 var growth= acc.cap - acc.tO - acc.tZ; 315 316 // ââ GRV pension estimate (mirrors Dart EntgeltpunkteEstimator) ââ 317 // Total Entgeltpunkte = career years à (brutto / Durchschnittsentgelt), capped at BBG. 318 // Career years = RETIRE_AGE â ARBEITSBEGINN regardless of current age 319 // (person has worked since 25 or will have by 67). 320 var grvYears = RETIRE_AGE - ARBEITSBEGINN; 321 var epPerYear = Math.min(bruttoJahr, BBG) / DURCHSCHNITTSENTGELT; 322 var renteMonat = grvYears * epPerYear * RENTENWERT; 323 var baseIncome = renteMonat * 12; 324 325 // ââ AV-Depot annuities (gross, before tax) ââ 326 var avPayoutRate = retRate - AVR_KOSTEN; 327 var monatGef = calcPension(acc.capGef, avPayoutRate, 0).b; 328 var monatUngef = calcPension(acc.capUngef, avPayoutRate, 0).b; 329 var avJahresgef = monatGef * 12; 330 331 // ââ Gefördert bucket: volle nachgelagerte Besteuerung (§22 Nr. 5 EStG) ââ 332 var retaxRate = calcIncrementalRetaxRate(avJahresgef, baseIncome); 333 var kirchenFaktor = 1 + kirchensteuer; 334 var effectiveRetax = retaxRate * kirchenFaktor; 335 var nettoGef = monatGef * (1 - effectiveRetax); 336 337 // ââ Ungefördert bucket: Ertragsanteilbesteuerung (§22 Nr. 1 Satz 3a EStG) ââ 338 // Only ERTRAGSANTEIL_67 (17%) of each annuity payment counts as taxable income. 339 // The incremental rate is computed on top of baseIncome + gefördert already in scope. 340 var ungefErtragsanteilJahr = monatUngef * 12 * ERTRAGSANTEIL_67; 341 var retaxRateUngef = acc.capUngef > 0 342 ? calcIncrementalRetaxRate(ungefErtragsanteilJahr, baseIncome + avJahresgef) 343 : 0; 344 var effectiveRetaxUngef = retaxRateUngef * kirchenFaktor * ERTRAGSANTEIL_67; 345 var nettoUngef = monatUngef * (1 - effectiveRetaxUngef); 346 347 var realFactor = Math.pow(1 + inflationRate, years); 348 var p = { b: monatGef + monatUngef, n: nettoGef + nettoUngef }; 349 // Blended effective rate across both buckets (used for display only) 350 var retaxMonthly = p.b - p.n; 351 var effectiveRetaxBlended = p.b > 0 ? retaxMonthly / p.b : effectiveRetax; 352 var retaxPctActual = (effectiveRetaxBlended * 100).toLocaleStr
352ing('de-DE', {minimumFractionDigits: 1}); 353 354 // Net capital at retirement (gross â tax across both buckets) 355 var retaxCapital = acc.capGef * effectiveRetax + acc.capUngef * effectiveRetaxUngef; 356 var netCapital = acc.cap - retaxCapital; 357 358 // ââ Capital & pension outputs ââ 359 setAll('.r-total', fmt(netCapital)); 360 setAll('.r-total-age', 'monatlich netto nach\u00a0' + retaxPctActual + '\u00a0% \u00a732a Rentensteuer'); 361 setAll('.r-self-pct', (acc.cap > 0 ? Math.round(acc.tO / acc.cap * 100) : 0) + '%'); 362 setEuro('.r-gross', acc.cap); 363 setEuro('.r-gross-hero', acc.cap); 364 setEuro('.r-eigen', acc.tO); 365 setEuro('.r-growth', growth); 366 setEuro('.r-zulagen', acc.tZ); 367 setEuro('.r-zulagen2', acc.tZ); 368 setEuro('.r-taxsave', acc.tT); 369 setEuro('.r-monthly-brutto', p.b); 370 setEuro('.r-monthly-netto', p.n); 371 setEuro('.r-monthly-hero', p.n); 372 setEuro('.r-monthly-hero-brutto',p.b); 373 setEuro('.r-lump-brutto', acc.cap * 0.3); 374 setEuro('.r-lump-netto', acc.cap * (1 - effectiveRetaxBlended) * 0.3); 375 setAll('.r-monthly-note', 376 'Ausz.\u00a0\u00fcber 18\u00a0J. (67\u201385) \u00b7 ' + retaxPctActual + '\u00a0% \u00a732a (inkrementell)'); 377 378 // ââ Narrative ââ 379 setAll('.r-narr-contrib', fmt(monthly)); 380 setAll('.r-narr-ret', (retRate * 100).toLocaleString('de-DE', { minimumFractionDigits: 1 })); 381 setAll('.r-narr-gross', fmt(acc.cap)); 382 setAll('.r-narr-netto', fmt(p.n)); 383 384 // ââ Zulagen sub-label ââ 385 var yr1 = yearlySubsidy(monthly * 12, children, taxNow, DEPOT_START); 386 var subLabel = (yr1.gz > 0 ? fmt(yr1.gz).trim() + '\u00a0\u20ac\u00a0Grundzulage/J.' : 'keine Grundzulage') 387 + (yr1.kz > 0 ? ' + ' + fmt(yr1.kz).trim() + '\u00a0\u20ac\u00a0Kinder/J.' : '') 388 + (bonus200 ? ' + ' + BONUS_BETRAG.toFixed(0) + '\u00a0\u20ac\u00a0Einmalbonus' : ''); 389 setAll('.r-zulagen-sub', subLabel); 390 391 // ââ Berechnungsdetails ââ 392 var txPct = Math.round(taxNow * 100); 393 var bracket = root().querySelector('.v1-tax-btn.active .v1-tax-btn-range'); 394 setAll('.cs-annual-own', fmt(monthly * 12)); 395 setAll('.cs-age-years', age + ' Jahre / ' + years + ' Jahre Laufzeit'); 396 var eligibleKinder = children.filter(function(c) { 397 if (!c.by) return false; 398 var childAge = DEPOT_START - c.by; 399 return childAge >= 0 && childAge < c.ta; 400 }); 401 // cs-kinder: BERECHNUNG column â per-child breakdown 402 var kinderDetail = root().querySelector('#cs-kinder-detail'); 403 if (kinderDetail) { 404 if (children.length === 0) { 405 kinderDetail.innerHTML = '<span class="cs-kinder">\u2013</span>'; 406 } else { 407 var retireYearK = DEPOT_START + years; 408 var rows = children.map(function (c, ci) { 409 var label = 'Kind\u00a0' + (ci + 1); 410 if (!c.by) { 411 return '<span class="cs-kinder-row cs-kinder-missing">' + label + '\u00a0\u2014 Geburtsjahr fehlt</span>'; 412 } 413 var wS = Math.max(DEPOT_START, c.by); 414 var wE = Math.min(retireYearK, c.by + c.ta); 415 var eY = Math.max(0, wE - wS); 416 var line = label + '\u00a0\u2014 geb.\u00a0' + c.by + '\u00a0\u00b7\u00a0Kindergeld bis\u00a0' + c.ta; 417 if (eY > 0) { 418 var total = eY * 300; 419 line += '\u00a0\u00b7\u00a0300\u00a0\u20ac/Jahr\u00a0\u00b7\u00a0' 420 + eY + '\u00a0Jahre\u00a0(' + wS + '\u2013' + wE + ')\u00a0=\u00a0' 421 + new Intl.NumberFormat('de-DE').format(total) + '\u00a0\u20ac'; 422 } else { 423 line += '\u00a0\u00b7\u00a0kein Anspruch ab\u00a0' + DEPOT_START; 424 } 425 return '<span class="cs-kinder-row">' + line + '</span>'; 426 }); 427 kinderDetail.innerHTML = rows.join(''); 428 } 429 } 430 setAll('.cs-kinder', children.length === 0 ? '\u2013' : children.length + '\u00a0Kind' + (children.length > 1 ? 'er' : '')); 431 setAll('.cs-rendite', (retRate * 100).toLocaleString('de-DE', { minimumFractionDigits: 1 }) + '\u00a0%'); 432 setAll('.cs-einkommen', bracket ? bracket.textContent.trim() : '\u2013'); 433 setAll('.cs-steuersatz', txPct + '\u00a0%'); 434 setAll('.cs-retax', (effectiveRetaxBlended * 100).toFixed(1) + '\u00a0%'); 435 setAll('.cs-grundzulage', fmt(yr1.gz).trim() + '/Jahr'); 436 setAll('.cs-kindzulage', yr1.kz > 0 ? fmt(yr1.kz) + '/Jahr' : '0\u00a0\u20ac'); 437 setAll('.cs-bonus', bonus200 ? '200\u00a0\u20ac' : '0\u00a0\u20ac'); 438 setAll('.cs-zul-pa', fmt(yr1.zul));
439 setAll('.cs-tax-pa', fmt(acc.tT)); 440 setAll('.cs-foerderquote', monthly * 12 > 0 441 ? (yr1.zul / (monthly * 12) * 100).toFixed(1) + '\u00a0%' 442 : '0\u00a0%'); 443 444 // ââ Berechnungsdetails: 3-column BERECHNUNG + ERGEBNIS attrs ââââââââââââââ 445 446 // ââ Section 01 attrs ââ 447 setAll('.cs-annual-calc', new Intl.NumberFormat('de-DE',{maximumFractionDigits:0}).format(monthly) + '\u00a0\u20ac \u00d7 12'); 448 setAll('.cs-age-calc', 'Bis Renteneintritt mit\u00a0' + RETIRE_AGE); 449 // cs-age-val: just the current age (ERGEBNIS for "Alter zu Beginn" row) 450 setAll('.cs-age-val', age + '\u00a0Jahre'); 451 // cs-anspar-val: laufzeit in years (ERGEBNIS for "Ansparphase" row) 452 setAll('.cs-anspar-val', years + '\u00a0Jahre\u00a0lang'); 453 // cs-kinder (used as BERECHNUNG col): already set above as detail string 454 // cs-kinder-val: ERGEBNIS â Kindergeldanspruch end-year 455 var kinderValStr; 456 if (children.length === 0) { 457 kinderValStr = '\u2013'; 458 } else { 459 var lastKgYear = 0; 460 children.forEach(function (c) { 461 if (c.by) { var end = c.by + c.ta; if (end > lastKgYear) lastKgYear = end; } 462 }); 463 kinderValStr = eligibleKinder.length === 0 464 ? 'Kein Kindergeldanspruch in 2027' 465 : eligibleKinder.length === 1 466 ? 'Kindergeldanspruch bis\u00a0' + lastKgYear 467 : eligibleKinder.length + ' Kinder \u2013 Kindergeldanspruch bis\u00a0' + lastKgYear; 468 } 469 setAll('.cs-kinder-val', kinderValStr); 470 471 // ââ Section 02 attrs ââ 472 // Grundzulage calc formula (dynamic based on capped contribution) 473 var capped02 = Math.min(monthly * 12, 1800); 474 var tier1 = Math.min(capped02, 360); 475 var tier2 = Math.max(0, Math.min(capped02 - 360, 1440)); 476 var gz02Str = yr1.gz > 0 477 ? (tier2 > 0 478 ? '50\u00a0%\u00a0\u00d7\u00a0' + new Intl.NumberFormat('de-DE').format(tier1) 479 + '\u00a0\u20ac\u00a0+\u00a025\u00a0%\u00a0\u00d7\u00a0' + new Intl.NumberFormat('de-DE').format(tier2) + '\u00a0\u20ac' 480 : '50\u00a0%\u00a0\u00d7\u00a0' + new Intl.NumberFormat('de-DE').format(tier1) + '\u00a0\u20ac') 481 : '\u2013'; 482 setAll('.cs-grundzulage-calc', gz02Str); 483 // Bonus calc label 484 setAll('.cs-bonus-calc', bonus200 ? 'Einmalig\u00a0200\u00a0\u20ac zu Beginn' : '\u2013'); 485 486 // ââ Section 03 attrs ââ 487 // Eigene Einzahlungen total: annual à years 488 setAll('.cs-eigen-years-calc', 489 new Intl.NumberFormat('de-DE',{maximumFractionDigits:0}).format(monthly * 12) + '\u00a0\u20ac\u00a0\u00d7\u00a0' + years + '\u00a0Jahre'); 490 // Staatliche Zulagen detailed breakdown 491 var retireYear03 = DEPOT_START + years; 492 var zulParts03 = []; 493 // Grundzulage: yr1.gz à years (gz is same every year since contribution fixed) 494 if (yr1.gz > 0) { 495 var gzTotal = Math.round(yr1.gz * years); 496 zulParts03.push( 497 'Grundzulage ('+years+'\u00a0J.\u00a0\u00d7\u00a0' 498 + new Intl.NumberFormat('de-DE').format(Math.round(yr1.gz)) 499 + '\u00a0\u20ac/J.\u00a0=\u00a0' 500 + new Intl.NumberFormat('de-DE').format(gzTotal) + '\u00a0\u20ac)' 501 ); 502 } 503 // Per-child Kinderzulage totals 504 children.forEach(function (c, ci) { 505 if (!c.by) return; 506 var wS = Math.max(DEPOT_START, c.by); 507 var wE = Math.min(retireYear03, c.by + c.ta); 508 var eY = Math.max(0, wE - wS); 509 if (eY > 0) { 510 zulParts03.push( 511 'Kinderzulage Kind\u00a0' + (ci + 1) 512 + '\u00a0(' + eY + '\u00a0J.\u00a0\u00d7\u00a0' 513 + new Intl.NumberFormat('de-DE', { maximumFractionDigits: 0 }).format(Math.min(monthly * 12, KZ_MAX)) 514 + '\u00a0\u20ac/J.\u00a0=\u00a0' 515 + new Intl.NumberFormat('de-DE').format(eY * Math.min(monthly * 12, KZ_MAX)) + '\u00a0\u20ac)' 516 ); 517 } 518 }); 519 if (bonus200) zulParts03.push('Berufseinsteiger-Bonus (einmalig\u00a0200\u00a0\u20ac)'); 520 setAll('.cs-zulagen-detail-calc', 521 zulParts03.length > 0 ? zulParts03.join('\u00a0+\u00a0') : '\u2013'); 522 // Förderquote formula 523 setAll('.cs-foerderquote-calc', monthly * 12 > 0 524 ? fmt(acc.tZ).trim() + '\u00a0/\u00a0' + fmt(acc.tO).trim() 525 : '\u2013'); 526 // Steuerersparnis label 527 setAll('.cs-steuersparnis-calc', years > 0 528 ? 'Gesch\u00e4tzte Erstattung \u00fcber\u00a0' + years + '\u00a0Jahre\u00a0(' + txPct + '\u00a0% Steuersatz)' 529 : '\u2013'); 530 531 // ââ Section 04 attrs ââ 532 // retaxCapital and netCapital already computed above (before capital outputs) 533 setAll('.cs-payout-method', 'inkl. Zinseszins auf Zulagen'); 534 setAll('.cs-self-pct-formula', 535 fmt(acc.tO).trim() + '\u00a0\u00f7\u00a0' + fmt(acc.cap).trim()); 536 setAll('.cs-self-pct-pct', 537 'ca.\u00a0' + (acc.cap > 0 ? Math.round(acc.tO / acc.cap * 100) : 0) + '\u00a0%'); 538 setAll('.cs-retax-formula', 539 'Prognostiziertes Brutto-Verm\u00f6gen\u00a0(' + fmt(acc.cap).trim() 540 + ')\u00a0\u00d7\u00a0Rentensteuersatz\u00a0(' + retaxPctActual + '\u00a0%)'); 541 setEuro('.cs-retax-amount', retaxCapital); 542 setAll('.cs-netto-formula', 543 'Prognostiziertes Brutto-Verm\u00f6gen\u00a0(' + fmt(acc.cap).trim() 544 + ')\u00a0\u2013\u00a0Rentenbesteuerung\u00a0(' + fmt(retaxCapital).trim() + ')'); 545 546 // ââ Section 05 attrs that don't need etf (moved here) ââ 547 setAll('.cs-msci-eigen-calc', 548 new Intl.NumberFormat('de-DE',{maximumFractionDigits:0}).format(monthly * 12) 549 + '\u00a0\u20ac\u00a0\u00d7\u00a0' + years + '\u00a0Jahre'); 550 setAll('.cs-msci-ertrag-label', 551 'Wertzuwachs inkl. Zinseszinseffekt \u00fcber\u00a0' + years + '\u00a0Jahren'
552 + ' mit\u00a0' + (retRate * 100).toLocaleString('de-DE', {minimumFractionDigits: 1}) 553 + '\u00a0% j\u00e4hrlicher Nettorendite'); 554 // cs-msci-vap-neg and cs-msci-liq-tax-neg are set AFTER etf is computed below 555 556 // ââ Legacy compat (kept for PDF/other uses) ââ 557 setAll('.cs-eigen-calc', fmt(monthly) + '\u00a0\u00d7 12 \u00d7 ' + years + '\u00a0J.'); 558 setAll('.cs-retax-applied-calc', 'Brutto \u00d7 (1 \u2212 ' + retaxPctActual + '\u00a0%)'); 559 setAll('.cs-retax-monthly-calc', 'Brutto \u00d7 (1 \u2212 ' + retaxPctActual + '\u00a0%)'); 560 setAll('.cs-zul-calc', yr1.zul > 0 561 ? fmt(yr1.gz).trim() + '\u00a0GZ' 562 + (yr1.kz > 0 ? '\u00a0+\u00a0' + fmt(yr1.kz).trim() + '\u00a0KZ' : '') 563 : '\u2013'); 564 565 // ââ Update Section 02 dynamic per-child rows ââ 566 updateKinderSection02(); 567 568 // ââ Comparison ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 569 // ETF: Vorabpauschale annually + Abgeltungssteuer at liquidation (Frey method) 570 // Sparkonto: Abgeltungssteuer on annual interest (Frey method) 571 // Both: no additional income tax during payout (taxes already paid) 572 var msciReturn = retRate; // gross â buildETFCapital deducts ETF_KOSTEN internally 573 var msciPayRate = retRate - ETF_KOSTEN; // net rate for payout annuity (costs continue) 574 var bankPayRate = BANK_RATE; // Sparkonto: no cost deduction 575 var etf = buildETFCapital(years, monthly * 12, msciReturn, abgst); 576 var bank = buildBankCapital(years, monthly * 12, BANK_RATE); 577 var msciPension = calcPension(etf.cap, msciPayRate, 0); // netto PMT from netto capital 578 var msciPensionB= calcPension(etf.brutto, msciPayRate, 0); // brutto PMT from brutto capital 579 var bankPension = calcPension(bank.cap, bankPayRate, 0); // netto PMT 580 var bankPensionB= calcPension(bank.brutto,bankPayRate, 0); // brutto PMT 581 582 setAll('.cs-monthly-contrib', Math.round(monthly)); 583 setAll('.cs-laufzeit', years); 584 setAll('.cs-rendite-pct', (retRate * 100).toLocaleString('de-DE', { minimumFractionDigits: 1 }) + '\u00a0%'); 585 586 // ââ AVR comparison row ââ 587 setEuro('.cmp-avr-cap', acc.cap); 588 setEuro('.cmp-avr-mon', p.b); 589 590 // ââ MSCI World / ETF ââ 591 setEuro('.cmp-msci-cap', etf.cap); // Endkapital netto (Berechnungsdetails) 592 setEuro('.cmp-msci-gross', etf.brutto); // Endkapital brutto (Vergleich, matches XLS) 593 setEuro('.cmp-msci-mon', msciPensionB.b); // Monatliche Rente brutto 594 setEuro('.cmp-msci-gross-fv', etf.grossFV); // Future Value brutto (kein Steuer) 595 setEuro('.cmp-msci-ertrag-gross', etf.ertragGross); // Ertrag brutto (Zinseszins) 596 setEuro('.cmp-msci-brutto', etf.brutto); // Endkapital nach VAP 597 setEuro('.cmp-msci-ertrag', etf.ertrag); // Kapitalertrag = depot - eigenBeiträge 598 setEuro('.cmp-msci-vap-sum', etf.cumVAP); // VAP kumuliert 599 setEuro('.cmp-msci-vap-tax', etf.cumVAPTax); // Steuer auf VAP 600 setEuro('.cmp-msci-taxable', etf.taxableGain); // Steuerpfl. Auflösungsgewinn 601 setEuro('.cmp-msci-liq-tax', etf.liqTax); // Abgeltungssteuer Auflösung 602 setEuro('.cmp-msci-total-tax', etf.totalTax); // Steuern gesamt 603 604 // ââ Sparkonto ââ 605 setEuro('.cmp-bank-cap', bank.cap); // Endkapital netto (Berechnungsdetails) 606 setEuro('.cmp-bank-gross', bank.brutto); // Endkapital brutto (Vergleich, matches XLS) 607 setEuro('.cmp-bank-mon', bankPensionB.b); // Monatliche Rente brutto 608 setEuro('.cmp-bank-gross-fv', bank.grossFV); // Endkapital brutto (kein Steuer) 609 setEuro('.cmp-bank-ertrag-gross', bank.ertragGross); // Ertrag brutto 610 setEuro('.cmp-bank-ertrag', bank.ertrag); // Ertrag netto 611 setEuro('.cmp-bank-abgst', -bank.cumTax); // Abgeltungssteuer gesamt (negativ) 612 613 // ââ Section 06: Sparkonto Berechnung labels (dynamic) ââ 614 var fmtN = function(n) { return new Intl.NumberFormat('de-DE',{maximumFractionDigits:0}).format(n); }; 615 setAll('.cs-bank-eigen-calc', 616 fmtN(monthly * 12) + '\u00a0\u20ac\u00a0\u00d7\u00a0' + years + '\u00a0Jahre'); 617 setAll('.cs-bank-ertrag-label', 618 'Wertzuwachs inkl. Zinseszinseffekt \u00fcber\u00a0' + years + '\u00a0Jahren' 619 + ' mit\u00a02,0\u00a0% j\u00e4hrlicher Rendite'); 620 setAll('.cs-bank-abgst-label', 621 'Zinsen werden bei Gutschrift mit Abgeltungssteuer i.H.v.\u00a025\u00a0% sowie' 622 + ' Solidarit\u00e4tszuschlag (insgesamt 26,375\u00a0%) versteuert.' 623 + ' Die tats\u00e4chlich anfallende Steuer kann je nach pers\u00f6nlicher' 624 + ' steuerlicher Situation abweichen.'); 625 setAll('.cs-bank-netto-label', 626 'Endverm\u00f6gen (Einzahlungen\u00a0+\u00a0Zinsertrag)' 627 + '\u00a0\u2013\u00a0j\u00e4hrlich anfallende Abgeltungssteuer bei Zinszahlungen'); 628 629 // ââ AVR advantage ââ 630 setEuro('.cmp-avr-vs-msci', acc.cap - etf.cap); 631 632 // ââ Section 05: etf-dependent Berechnung attrs (must be after etf is computed) ââ 633 setAll('.cs-msci-vap-neg', 634 etf.cumVAP > 0 ? '\u2212\u00a0' + fmt(etf.cumVAP) : '0\u00a0\u20ac'); 635 setAll('.cs-msci-liq-tax-neg', 636 etf.liqTax > 0 ? '\u2212\u00a0' + fmt(etf.liqTax) : '0\u00a0\u20ac'); 637 setAll('.cs-msci-vap-label', 638 'Vorabpauschale kumuliert unter Ber\u00fccksichtigung der Teilfreistellung\u00a0' 639 + fmt(acc.tZ).trim() + '\u00a0/\u00a0' + fmt(acc.tO).trim()); 640 setAll('.cs-msci-liq-label', 641 'Kursgewinne von regul\u00e4ren ETFs werden\u00a0\u2013 nach Abzug der 30\u00a0% Teilfreistellung\u00a0\u2013 bei Ver\u00e4u\u00dferung mit Abgeltungssteuer i.H.v.\u00a025\u00a0% sowie Solidarit\u00e4tszuschlag (insgesamt 26,375\u00a0%) versteuert. Die tats\u00e4chlich anfallende Steuer kann je nach pers\u00f6nlicher steuerlicher Situation abweichen. Hierbei wird die bereits gezahlte Vorabpauschale ber\u00fccksichtigt und f\u00e4llt nicht doppelt an.'); 642 setAll('.cs-msci-netto-label', 643 'Endverm\u00f6gen (Einzahlungen\u00a0+\u00a0Ertrag) \u2013 Vorabpauschale \u2013 Abgeltungssteuer bei Verkauf'); 644 645 // ââ Chart aria descriptions ââ 646 updateChartAria(age, monthly, acc, growth); 647 648 // ââ Charts ââ 649 var lbl = [], dO = [], dO2 = [], dT = []; 650 for (var i = 0; i < years; i++) { 651 lbl.push(age + i + 1); 652 dO.push(acc.own[i]);
653 dO2.push(acc.own[i] + acc.zul[i]); 654 dT.push(acc.bal[i]); 655 } 656 lastChartData = { lbl: lbl, dO: dO, dO2: dO2, dT: dT }; 657 renderChart(lbl, dO, dO2, dT); 658 if (root().classList.contains('is-view2')) { 659 renderChart2(lbl, dO, dO2, dT); 660 } 661 662 updateKinderDetail(); 663 664 window.AVR_STATE = { 665 sparrate: monthly, 666 alter: age, 667 einkommen: bruttoJahr, 668 rendite: parseFloat((retRate * 100).toFixed(1)), 669 kinder_geboren: children 670 .map(function(c) { return c.by; }) 671 .filter(function(by) { return by !== null; }) 672 .join(',') 673 }; 674 } 675 676 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 677 * 6. Charts 678 * âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 679 function makeDataset(label, data, color, fill) { 680 return { label: label, data: data, borderColor: color, backgroundColor: color, 681 borderWidth: 1.5, fill: fill, tension: 0.35, pointRadius: 0 }; 682 } 683 684 function makeChartOptions() { 685 return { 686 responsive: true, 687 maintainAspectRatio: false, 688 interaction: { mode: 'index', intersect: false }, 689 plugins: { 690 legend: { 691 display: true, position: 'bottom', align: 'end', 692 labels: { 693 boxWidth: 10, 694 boxHeight: 10, 695 borderRadius: 5, 696 font: { 697 size: 12, 698 family: 'Aeonik, sans-serif' 699 }, 700 color: '#080707', 701 padding: 16, 702 useBorderRadius: true 703 } 704 }, 705 tooltip: { 706 titleFont: { 707 family: 'Aeonik, sans-serif', 708 size: 12, 709 weight: '700' 710 }, 711 bodyFont: { 712 family: 'Aeonik, sans-serif', 713 size: 11 714 }, 715 callbacks: { 716 label: function (ctx) { 717 var fmt = function(v) { 718 return new Intl.NumberFormat('de-DE', { maximumFractionDigits: 0 }).format(v) + '\u00a0\u20ac'; 719 }; 720 // 'Zulagen' dataset uses stacked values (own + zulagen). 721 // Show only the actual Zulagen portion by subtracting the 722 // 'Einzahlung' value at the same index. 723 if (ctx.dataset.label === 'Zulagen') { 724 var chart = ctx.chart; 725 var einzMeta = chart.getDatasetMeta(0); 726 var einzVal = einzMeta.hidden ? 0 : chart.data.datasets[0].data[ctx.dataIndex]; 727 var zulagenOnly = ctx.parsed.y - einzVal; 728 return ' Zulagen: ' + fmt(zulagenOnly); 729 } 730 return ' ' + ctx.dataset.label + ': ' + fmt(ctx.parsed.y); 731 } 732 } 733 } 734 }, 735 scales: { 736 x: { 737 grid: { display: false }, 738 ticks: { 739 font: { size: 12, family: 'Aeonik, sans-serif' }, 740 color: '#080707', 741 maxRotation: 0, 742 minRotation: 0, 743 callback: function (value) { 744 return this.getLabelForValue(value) + '\u00a0J'; 745 } 746 } 747 }, 748 y: { 749 border: { display: false }, 750 ticks: { 751 font: { size: 12, family: 'Aeonik, sans-serif' }, 752 color: '#080707', 753 maxRotation: 0, 754 minRotation: 0, 755 callback: function (v) { 756 return new Intl.NumberFormat('de-DE', { notation: 'compact' }).format(v) + '\u00a0\u20ac'; 757 } 758 } 759 } 760 } 761 }; 762 } 763 764 function updateChartAria(age, monthly, acc, growth) { 765 var years = RETIRE_AGE - age; 766 var fmtK = function (n) { 767 return new Intl.NumberFormat('de-DE', { maximumFractionDigits: 0 }).format(Math.round(n)); 768 }; 769 var title = 'Vermögensentwicklung von Alter ' + age + ' bis ' + RETIRE_AGE; 770 var eigenZulagen = acc.tO + acc.tZ; 771 var summary = 772 'Bei einer monatlichen Sparrate von ' + Math.round(monthly) + ' ⬠' 773 + 'wächst das prognostizierte Brutto-Endvermögen ' 774 + 'über ' + years + ' Jahre auf rund ' + fmtK(acc.cap) + ' â¬. ' 775 + 'Davon entfallen ' + fmtK(eigenZulagen) + ' ⬠auf eigene Einzahlungen und staatliche Zulagen ' 776 + 'sowie ' + fmtK(growth) + ' ⬠auf Kapitalwachstum.'; 777 778 ['growthChart-title', 'growthChart2-title'].forEach(function (id) { 779 var el = document.getElementById(id); 780 if (el) el.textContent = title; 781 }); 782 ['growthChart-summary', 'growthChart2-summary'].forEach(function (id) { 783 var el = document.getElementById(id); 784 if (el) el.textContent = summary; 785 }); 786 } 787 788 function renderChart(lbl, dO, dO2, dT) { 789 var canvas = $id('growthChart'); 790 if (!canvas) return; 791 if (gc) gc.destroy(); 792 gc = new Chart(canvas.getContext('2d'), { 793 type: 'line', 794 data: { labels: lbl, datasets: [ 795 makeDataset('Einzahlung', dO, '#F9BC86', 'origin'), 796 makeDataset('Zulagen', dO2, '#DB500B', '-1'), 797 makeDataset('Gesamtverm\u00f6gen', dT, '#F47323', '-1') 798 ]}, 799 options: makeChartOptions() 800 }); 801 } 802 803 function renderChart2(lbl, dO, dO2, dT) { 804 var canvas = document.getElementById('growthChart2'); 805 if (!canvas) return; 806 if (gc2) { gc2.destroy(); gc2 = null; } 807 gc2 = new Chart(canvas.getContext('2d'), { 808 type: 'line', 809 data: { labels: lbl, datasets: [ 810 makeDataset('Einzahlung', dO, '#F9BC86', 'origin'), 811 makeDataset('Zulagen', dO2, '#DB500B', '-1'), 812 makeDataset('Gesamtverm\u00f6gen', dT, '#F47323', '-1') 813 ]}, 814 options: makeChartOptions() 815 }); 816 } 817 818 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 819 * 7. Kinder UI 820 * âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 821 function renderKinder() { 822 var count = $id('kinder-count'); 823 var btnMin = $id('kinder-minus'); 824 var btnPls = $id('kinder-plus'); 825 var hint = $id('kinder-step-hint'); 826 var yWrap = root().querySelector('.kinder-years-wrap'); 827 var yList = $id('kinder-years-list'); 828 if (!yList) return; 829 830 if (count) count.textContent = children.length; 831 if (btnMin) btnMin.disabled = children.length === 0; 832 if (btnPls) btnPls.disabled = children.length >= MAX_KINDER; 833 if (hint) hint.textContent = children.length >= MAX_KINDER ? 'Maximum (6) erreicht' : ''; 834 if (yWrap) yWrap.classList.toggle('visible', children.length > 0); 835 836 var existing = yList.querySelectorAll('.kinder-year-input'); 837 var diff = children.length - existing.length; 838 839 if (diff > 0) { 840 for (var i = 0; i < diff; i++) { 841 var kindIdx = existing.length + i; 842 var wrap = document.createElement('div'); 843 wrap.className = 'kinder-year-input-wrap'; 844 845 var lbl = document.createElement('span'); 846 lbl.className = 'kinder-kind-label'; 847 lbl.textContent = 'Kind ' + (kindIdx + 1); 848 849 var inp = document.createElement('input'); 850 inp.type = 'text'; 851 inp.inputMode = 'numeric'; 852 inp.pattern = '[0-9]*'; 853 inp.className = 'kinder-year-input'; 854 inp.placeholder = 'Geburtsjahr'; 855 inp.min = BIRTH_MIN; 856 inp.max = BIRTH_MAX; 857 inp.dataset.idx = kindIdx; 858 inp.setAttribute('aria-label', 'Geburtsjahr Kind ' + (kindIdx + 1)); 859 860 var err = document.createElement('div'); 861 err.className = 'kinder-year-error'; 862 863 // Kindergeld always bis 25 (KINDER_MAX_ALTER) â no toggle 864 wrap.appendChild(lbl); 865 wrap.appendChild(inp); 866 wrap.appendChild(err); 867 yList.appendChild(wrap); 868 } 869 } else if (diff < 0) { 870 var rows = yList.querySelectorAll('.kinder-year-input-wrap'); 871 for (var i = rows.length - 1; i >= children.length; i--) { 872 rows[i].remove(); 873 } 874 } 875 876 // Sync birth years, labels and toggle states 877 yList.querySelectorAll('.kinder-year-input').forEach(function (inp, i) { 878 inp.dataset.idx = i; 879 if (children[i] && children[i].by && !inp.matches(':focus')) { 880 inp.value = children[i].by; 881 } 882 var lbl = inp.previousElementSibling;
883 if (lbl && lbl.classList.contains('kinder-kind-label')) { 884 lbl.textContent = 'Kind ' + (i + 1); 885 } 886 // ta always KINDER_MAX_ALTER â no toggle buttons to sync 887 }); 888 } 889 890 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 891 * 8. updateKinderDetail() 892 * Populates section 06 in Berechnungsdetails with per-child 893 * Kinderzulage info: birth year, age at depot start, until 894 * which year Kindergeld is paid, and how many years qualify. 895 * âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 896 function updateKinderDetail() { 897 var container = document.getElementById('cs-kinder-detail'); 898 if (!container) return; 899 900 var age = parseInt($id('age').value, 10); 901 var retireYear = DEPOT_START + (RETIRE_AGE - age); // year user turns 67 902 903 if (children.length === 0) { 904 container.innerHTML = 905 '<div class="calc-row">' + 906 '<span class="calc-row-label">Kinder</span>' + 907 '<span class="calc-row-value">Keine Kinder eingetragen \u2013 keine Kinderzulage</span>' + 908 '</div>'; 909 return; 910 } 911 912 // Fixed-rate info row 913 var html = 914 '<div class="calc-row">' + 915 '<span class="calc-row-label">Kinderzulage p.a.</span>' + 916 '<span class="calc-row-value accent">bis zu 300\u00a0\u20ac pro Kind / Jahr \u2013 1:1 Sparrate (max. 300\u00a0\u20ac/J. bei \u2265\u00a025\u00a0\u20ac/Monat)</span>' + 917 '</div>'; 918 919 children.forEach(function (c, i) { 920 var kindLabel = 'Kind\u00a0' + (i + 1); 921 922 if (!c.by) { 923 html += 924 '<div class="calc-row">' + 925 '<span class="calc-row-label">' + kindLabel + '</span>' + 926 '<span class="calc-row-value">Geburtsjahr noch nicht eingetragen</span>' + 927 '</div>'; 928 return; 929 } 930 931 var kindergeldBisJahr = c.by + c.ta; // e.g. 2010 + 18 = 2028 932 var ageAt2027 = DEPOT_START - c.by; // age the child has in 2027 933 934 // Eligible window within the depot/savings period 935 var windowStart = Math.max(DEPOT_START, c.by); // first year child exists & depot runs 936 var windowEnd = Math.min(retireYear, kindergeldBisJahr); // earlier of retirement or end of Kindergeld 937 var eligibleYears = Math.max(0, windowEnd - windowStart); 938 939 // Human-readable age-at-depot-start 940 var ageStr; 941 if (ageAt2027 < 0) { 942 ageStr = 'wird ' + c.by + ' geboren (nach Depot-Start)'; 943 } else if (ageAt2027 === 0) { 944 ageStr = 'wird 2027 geboren'; 945 } else { 946 ageStr = ageAt2027 + (ageAt2027 === 1 ? '\u00a0Jahr' : '\u00a0Jahre') + ' alt in 2027'; 947 } 948 949 // Time frame label 950 var rangeStr; 951 if (eligibleYears <= 0) { 952 rangeStr = 'Anspruch bereits vor 2027 abgelaufen'; 953 } else { 954 rangeStr = windowStart + '\u00a0\u2013\u00a0' + (windowEnd - 1) + 955 '\u00a0(' + eligibleYears + (eligibleYears === 1 ? '\u00a0Jahr' : '\u00a0Jahre') + ')'; 956 } 957 958 html += 959 '<div class="calc-row">' + 960 '<span class="calc-row-label">' + kindLabel + '</span>' + 961 '<span class="calc-row-value">' + 962 'Geb.\u00a0' + c.by + 963 '\u00a0\u00b7\u00a0' + ageStr + 964 '\u00a0\u00b7\u00a0Kindergeld bis ' + c.ta + '\u00a0J.\u00a0(' + kindergeldBisJahr + ')' + 965 '\u00a0\u00b7\u00a0Zulage\u00a0' + rangeStr + 966 '</span>' + 967 '</div>'; 968 }); 969 970 container.innerHTML = html; 971 } 972 973 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 974 * 9b. updateKinderSection02() 975 * Populates section 02 "Deine Förderung" with per-child 976 * Kinderzulage rows (label, formula, value) and toggles 977 * the bonus row visibility. 978 * âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 979 function updateKinderSection02() { 980 var container = document.getElementById('cs-foerderung-kinder'); 981 if (!container) return; 982 983 var age02 = parseInt($id('age').value, 10); 984 var monthly02 = parseFloat($id('contrib').value); 985 var kzPerYear = Math.min(monthly02 * 12, KZ_MAX); // 1:1 match, capped at 300 â¬/yr 986 var retYear02 = DEPOT_START + (RETIRE_AGE - age02); 987 var html = ''; 988
989 children.forEach(function (c, i) { 990 if (!c.by) return; 991 var wS = Math.max(DEPOT_START, c.by); 992 var wE = Math.min(retYear02, c.by + c.ta); 993 var eY = Math.max(0, wE - wS); 994 if (eY <= 0) return; // not eligible in depot period 995 996 var rangeLabel = wS + ' â ' + (wE - 1); 997 var kzFormatted = new Intl.NumberFormat('de-DE', { maximumFractionDigits: 0 }).format(kzPerYear); 998 var calcLabel = kzPerYear < KZ_MAX 999 ? new Intl.NumberFormat('de-DE', { maximumFractionDigits: 0 }).format(monthly02 * 12) 1000 + ' â¬/J. (1:1 Sparrate, unter 300 â¬/J.)' 1001 : '100 % Ã 300 â¬'; 1002 html += 1003 '<div class="calc-row">' 1004 + '<span class="calc-row-label">Kinderzulage für Kind ' + (i + 1) 1005 + ' (Zeitraum ' + rangeLabel + ')</span>' 1006 + '<span class="calc-row-calc">' + calcLabel + '</span>' 1007 + '<span class="calc-row-value">' + kzFormatted + ' â¬/Jahr</span>' 1008 + '</div>'; 1009 }); 1010 1011 container.innerHTML = html; 1012 } 1013 1014 /* âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1015 * 9. init() 1016 * âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 1017 function init() { 1018 var r = document.querySelector('.avr'); 1019 if (!r || r.dataset.init) return; 1020 r.dataset.init = '1'; 1021 1022 // ââ Sliders ââ 1023 [ 1024 { id: 'age', cls: '.v-age', dec: 0 }, 1025 { id: 'contrib',cls: '.v-contrib',dec: 0 }, 1026 { id: 'retax', cls: '.v-retax', dec: 0 } 1027 ].forEach(function (cfg) { 1028 var el = $id(cfg.id); 1029 paintSlider(el); 1030 el.addEventListener('input', function () { 1031 paintSlider(el); 1032 syncSliderDisplay(el, cfg.cls, cfg.dec); 1033 calc(); 1034 }); 1035 }); 1036 1037 // ââ Steppers (contrib, age) ââ 1038 [ 1039 { minusId: 'contrib-minus', plusId: 'contrib-plus', sliderId: 'contrib', cls: '.v-contrib', min: 10, max: 570, step: 5 }, 1040 { minusId: 'age-minus', plusId: 'age-plus', sliderId: 'age', cls: '.v-age', min: 18, max: 64, step: 1 } 1041 ].forEach(function (cfg) { 1042 var sliderEl = $id(cfg.sliderId); 1043 if (!sliderEl) return; 1044 1045 var step = function (dir) { 1046 var val = parseFloat(sliderEl.value) + dir * cfg.step; 1047 val = Math.min(cfg.max, Math.max(cfg.min, val)); 1048 sliderEl.value = val; 1049 paintSlider(sliderEl);
1050 $all(cfg.cls).forEach(function (el) { el.textContent = Math.round(val); }); 1051 calc(); 1052 }; 1053 1054 var minusBtn = $id(cfg.minusId); 1055 var plusBtn = $id(cfg.plusId); 1056 if (minusBtn) minusBtn.addEventListener('click', function () { step(-1); }); 1057 if (plusBtn) plusBtn.addEventListener('click', function () { step(+1); }); 1058 }); 1059 1060 // ââ Tax buttons ââ 1061 root().addEventListener('click', function (e) { 1062 var btn = e.target.closest('[data-action="tax"]'); 1063 if (!btn) return; 1064 $all('.tax-btn, .v1-tax-btn').forEach(function (b) { 1065 b.classList.remove('active'); 1066 b.setAttribute('aria-pressed', 'false'); 1067 }); 1068 btn.classList.add('active'); 1069 btn.setAttribute('aria-pressed', 'true'); 1070 $id('taxrate').value = btn.dataset.tax; 1071 var retaxPct = Math.max(0, Math.round(parseFloat(btn.dataset.tax) * 100) - 10); 1072 var retaxEl = $id('retax'); 1073 retaxEl.value = retaxPct; 1074 paintSlider(retaxEl); 1075 syncSliderDisplay(retaxEl, '.v-retax', 0); 1076 calc(); 1077 }); 1078 1079 // ââ Rendite pills ââ 1080 $all('.v1-ret-pill').forEach(function (pill) { 1081 pill.addEventListener('click', function () { 1082 var preset = pill.dataset.preset; 1083 $id('ret').value = preset;
1084 $all('.v-return').forEach(function (el) { 1085 el.textContent = parseFloat(preset).toLocaleString('de-DE', { minimumFractionDigits: 1 }); 1086 }); 1087 $all('.v1-ret-pill, .ret-preset-btn').forEach(function (b) { 1088 b.classList.remove('active'); 1089 b.setAttribute('aria-pressed', 'false'); 1090 }); 1091 $all('.v1-ret-pill[data-preset="' + preset + '"]').forEach(function (b) { 1092 b.classList.add('active'); 1093 b.setAttribute('aria-pressed', 'true'); 1094 }); 1095 calc(); 1096 }); 1097 }); 1098 1099 // ââ Rendite buttons (legacy) ââ 1100 $all('.ret-preset-btn').forEach(function (btn) { 1101 btn.dataset.preset = parseFloat(btn.textContent); 1102 btn.addEventListener('click', function () { 1103 var preset = btn.dataset.preset; 1104 $id('ret').value = preset; 1105 $all('.v-return').forEach(function (el) { 1106 el.textContent = parseFloat(preset).toLocaleString('de-DE', { minimumFractionDigits: 1 }); 1107 }); 1108 $all('.ret-preset-btn').forEach(function (b) { b.classList.remove('active'); }); 1109 btn.classList.add('active'); 1110 calc(); 1111 }); 1112 }); 1113 1114 // ââ Kinder stepper ââ 1115 $id('kinder-plus').addEventListener('click', function () { 1116 if (children.length < MAX_KINDER) { 1117 children.push({ by: null, ta: KINDER_MAX_ALTER }); 1118 renderKinder(); 1119 calc(); 1120 } 1121 }); 1122 $id('kinder-minus').addEventListener('click', function () { 1123 if (children.length > 0) { 1124 children.pop(); 1125 renderKinder(); 1126 calc(); 1127 } 1128 }); 1129 1130 // Kindergeld toggle removed â always bis 25 1131 1132 1133 // ââ Birth-year input validation ââ 1134 $id('kinder-years-list').addEventListener('input', function (e) { 1135 var inp = e.target.closest('.kinder-year-input'); 1136 if (!inp) return; 1137 var idx = parseInt(inp.dataset.idx, 10); 1138 var raw = inp.value.replace(/[^0-9]/g, ''); 1139 inp.value = raw; 1140 var err = inp.nextElementSibling; 1141 var yr = parseInt(raw, 10); 1142 if (!raw) { 1143 inp.classList.remove('invalid'); 1144 err.textContent = ''; 1145 err.classList.remove('visible'); 1146 return; 1147 } 1148 if (isNaN(yr) || yr < BIRTH_MIN || yr > BIRTH_MAX) { 1149 inp.classList.add('invalid'); 1150 err.textContent = yr > BIRTH_MAX 1151 ? 'Geburtsjahr darf nicht nach ' + BIRTH_MAX + ' liegen.' 1152 : 'Geburtsjahr darf nicht vor ' + BIRTH_MIN + ' liegen.'; 1153 err.classList.add('visible'); 1154 } else { 1155 if (children[idx]) children[idx].by = yr; 1156 var ageAtDepot = DEPOT_START - yr; 1157 var currentTa = children[idx] ? children[idx].ta : KINDER_MAX_ALTER; 1158 if (ageAtDepot >= currentTa) { 1159 inp.classList.add('invalid'); 1160 err.textContent = ageAtDepot >= 25 1161 ? 'Kind ist ' + ageAtDepot + ' J. bei Depot-Start â kein Kindergeldanspruch möglich.' 1162 : 'Kind ist ' + ageAtDepot + ' J. bei Depot-Start â bitte âKindergeld bis 25â wählen.'; 1163 err.classList.add('visible'); 1164 } else { 1165 inp.classList.remove('invalid'); 1166 err.textContent = ''; 1167 err.classList.remove('visible'); 1168 } 1169 calc(); 1170 } 1171 }); 1172 1173 // ââ PDF button ââ 1174 $id('pdf-btn').addEventListener('click', window.AVR_PDF.generatePDF); 1175 1176 // ââ E-Mail modal ââ 1177 var emailBtn = document.getElementById('email-btn'); 1178 if (emailBtn) emailBtn.addEventListener('click', window.AVR_MODAL.openEmailModal); 1179 var modalClose = document.getElementById('avr-modal-close'); 1180 if (modalClose) modalClose.addEventListener('click', window.AVR_MODAL.closeEmailModal); 1181 var modalOverlay = document.getElementById('avr-email-modal'); 1182 if (modalOverlay) modalOverlay.addEventListener('click', function (e) { 1183 if (e.target === this) window.AVR_MODAL.closeEmailModal(); 1184 }); 1185 var emailForm = document.getElementById('avr-email-form'); 1186 if (emailForm) emailForm.addEventListener('submit', window.AVR_MODAL.submitEmailForm); 1187 1188 // ââ View switching ââ 1189 $id('cta-btn').addEventListener('click', function () { showView(2); }); 1190 $id('back-btn').addEventListener('click', function () { showView(1); }); 1191 1192 // ââ Accordion ââ 1193 function toggleAccordionHeader(hdr) { 1194 var acc = hdr.closest('.accordion'); 1195 if (!acc) return; 1196 var isOpen = acc.classList.toggle('open'); 1197 hdr.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); 1198 } 1199 document.addEventListener('click', function (e) { 1200 var hdr = e.target.closest('.accordion-header'); 1201 if (hdr) toggleAccordionHeader(hdr); 1202 }); 1203 document.addEventListener('keydown', function (e) { 1204 if (e.key === 'Enter' && e.target.type === 'checkbox') { 1205 e.preventDefault(); 1206 e.target.checked = !e.target.checked; 1207 e.target.dispatchEvent(new Event('change', { bubbles: true })); 1208 return; 1209 } 1210 if (e.key !== 'Enter' && e.key !== ' ') return; 1211 var hdr = e.target.closest('.accordion-header'); 1212 if (hdr) { 1213 e.preventDefault(); 1214 toggleAccordionHeader(hdr); 1215 } 1216 }); 1217 1218 // ââ Berechnungsdetails: per-section mobile accordion ââ 1219 // On mobile (< 1024px) each calc-section header toggles its body open/closed. 1220 // On desktop the bodies are always visible via CSS (mq(large) { display: block }). 1221 $all('.calc-section-header-row').forEach(function (hdr) { 1222 hdr.addEventListener('click', function () { 1223 if (window.matchMedia('(max-width: 820px)').matches) { 1224 var section = hdr.closest('.calc-section'); 1225 if (!section) return; 1226 var isOpen = section.classList.toggle('cs-open'); 1227 var btn = section.querySelector('.calc-section-toggle'); 1228 if (btn) btn.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); 1229 } 1230 }); 1231 }); 1232 1233 // ââ Initial state ââ 1234 var activeBtn = root().querySelector('.v1-tax-btn.active'); 1235 if (activeBtn) { 1236 var retaxInit = Math.max(0, Math.round(parseFloat(activeBtn.dataset.tax) * 100) - 10); 1237 var retaxEl = $id('retax'); 1238 retaxEl.value = retaxInit; 1239 paintSlider(retaxEl); 1240 syncSliderDisplay(retaxEl, '.v-retax', 0); 1241 } 1242 1243 // ââ Sync slider display values on init (populates .v-age, .v-contrib etc.) ââ 1244 [ 1245 { id: 'age', cls: '.v-age', dec: 0 }, 1246 { id: 'contrib',cls: '.v-contrib',dec: 0 } 1247 ].forEach(function (cfg) { 1248 var el = $id(cfg.id);
1249 if (el) syncSliderDisplay(el, cfg.cls, cfg.dec); 1250 }); 1251 1252 // ââ Mobile (â¤820px): pre-open berechnungsdetails + riester ââ 1253 if (window.matchMedia('(max-width: 820px)').matches) { 1254 ['acc-berechnungsdetails', 'acc-riester-vergleich'].forEach(function (id) { 1255 var el = $id(id); 1256 if (el) el.classList.add('open'); 1257 }); 1258 } 1259 1260 renderKinder(); 1261 calc(); 1262 } 1263 1264 // ââ Debounced resize ââ 1265 var resizeTimer = null; 1266 window.addEventListener('resize', function () { 1267 clearTimeout(resizeTimer); 1268 resizeTimer = setTimeout(function () { 1269 if (lastChartData.lbl.length) { 1270 renderChart(lastChartData.lbl, lastChartData.dO, lastChartData.dO2, lastChartData.dT); 1271 if (root().classList.contains('is-view2')) { 1272 renderChart2(lastChartData.lbl, lastChartData.dO, lastChartData.dO2, lastChartData.dT); 1273 } 1274 } 1275 }, 150); 1276 }); 1277 1278 // ââ Boot ââ 1279 if (document.readyState === 'loading') { 1280 document.addEventListener('DOMContentLoaded', init); 1281 } else { 1282 init(); 1283 } 1284 1285}());
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.