249 lines
7.8 KiB
JavaScript
249 lines
7.8 KiB
JavaScript
/**
|
|
* ui.js — DOM wiring, event listeners, chart rendering
|
|
*/
|
|
|
|
// ===== Helpers =====
|
|
|
|
const fmt_eur = (n) =>
|
|
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }).format(n);
|
|
|
|
const get_params = () => ({
|
|
years: +document.getElementById('years').value,
|
|
invest_return: +document.getElementById('invest_return').value,
|
|
tax_rate: +document.getElementById('tax_rate').value,
|
|
home_price: +document.getElementById('home_price').value,
|
|
down_pct: +document.getElementById('down_pct').value,
|
|
interest_rate: +document.getElementById('interest_rate').value,
|
|
loan_years: +document.getElementById('loan_years').value,
|
|
closing_buy_pct: +document.getElementById('closing_buy_pct').value,
|
|
closing_sell_pct:+document.getElementById('closing_sell_pct').value,
|
|
appreciation: +document.getElementById('appreciation').value,
|
|
prop_tax_rate: +document.getElementById('prop_tax_rate').value,
|
|
maint_rate: +document.getElementById('maint_rate').value,
|
|
insurance: +document.getElementById('insurance').value,
|
|
monthly_rent: +document.getElementById('monthly_rent').value,
|
|
rent_increase: +document.getElementById('rent_increase').value,
|
|
renters_ins: +document.getElementById('renters_ins').value,
|
|
start_capital: +document.getElementById('start_capital').value,
|
|
});
|
|
|
|
// ===== Charts =====
|
|
|
|
const CHART_DEFAULTS = {
|
|
color: '#8b949e',
|
|
borderColor: 'rgba(255,255,255,0.08)',
|
|
};
|
|
|
|
const make_chart = (canvas_id, label_buy, label_rent) => {
|
|
const ctx = document.getElementById(canvas_id).getContext('2d');
|
|
return new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: [],
|
|
datasets: [
|
|
{
|
|
label: label_buy,
|
|
data: [],
|
|
borderColor: '#f59e0b',
|
|
backgroundColor: 'rgba(245,158,11,0.06)',
|
|
borderWidth: 2.5,
|
|
pointRadius: 3,
|
|
pointHoverRadius: 5,
|
|
fill: false,
|
|
tension: 0.35,
|
|
},
|
|
{
|
|
label: label_rent,
|
|
data: [],
|
|
borderColor: '#2dd4bf',
|
|
backgroundColor: 'rgba(45,212,191,0.06)',
|
|
borderWidth: 2.5,
|
|
pointRadius: 3,
|
|
pointHoverRadius: 5,
|
|
fill: false,
|
|
tension: 0.35,
|
|
},
|
|
],
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: { display: false },
|
|
tooltip: {
|
|
mode: 'index',
|
|
intersect: false,
|
|
backgroundColor: 'rgba(13,17,23,0.9)',
|
|
borderColor: 'rgba(255,255,255,0.12)',
|
|
borderWidth: 1,
|
|
titleColor: '#8b949e',
|
|
bodyColor: '#e6edf3',
|
|
padding: 12,
|
|
callbacks: {
|
|
label: (ctx) => ` ${ctx.dataset.label}: ${fmt_eur(ctx.parsed.y)}`,
|
|
},
|
|
},
|
|
annotation: { annotations: {} },
|
|
},
|
|
scales: {
|
|
x: {
|
|
grid: { color: 'rgba(255,255,255,0.05)' },
|
|
ticks: { color: '#6e7681', font: { size: 11 } },
|
|
},
|
|
y: {
|
|
grid: { color: 'rgba(255,255,255,0.05)' },
|
|
ticks: {
|
|
color: '#6e7681',
|
|
font: { size: 11 },
|
|
callback: (v) => fmt_eur(v),
|
|
},
|
|
},
|
|
},
|
|
interaction: { mode: 'nearest', axis: 'x', intersect: false },
|
|
},
|
|
});
|
|
};
|
|
|
|
const chart_nw = make_chart('chart_nw', 'Kaufen', 'Mieten (ETF)');
|
|
const chart_cost = make_chart('chart_cost', 'Kaufen', 'Mieten');
|
|
|
|
const update_chart = (chart, labels, data_buy, data_rent, breakeven_month, years) => {
|
|
chart.data.labels = labels;
|
|
chart.data.datasets[0].data = data_buy;
|
|
chart.data.datasets[1].data = data_rent;
|
|
|
|
// breakeven annotation
|
|
if (breakeven_month !== null && breakeven_month <= years * 12) {
|
|
const be_year = breakeven_month / 12;
|
|
chart.options.plugins.annotation.annotations = {
|
|
breakeven: {
|
|
type: 'line',
|
|
xMin: be_year - 1, // labels are "Jahr 1" ... "Jahr N", zero-indexed
|
|
xMax: be_year - 1,
|
|
borderColor: 'rgba(255,255,255,0.25)',
|
|
borderWidth: 1,
|
|
borderDash: [5, 4],
|
|
label: {
|
|
display: true,
|
|
content: `Break-Even Jahr ${be_year.toFixed(1)}`,
|
|
color: '#8b949e',
|
|
backgroundColor: 'rgba(13,17,23,0.85)',
|
|
font: { size: 11 },
|
|
position: 'start',
|
|
},
|
|
},
|
|
};
|
|
} else {
|
|
chart.options.plugins.annotation.annotations = {};
|
|
}
|
|
|
|
chart.update('none');
|
|
};
|
|
|
|
// ===== Table =====
|
|
|
|
const build_table = (result) => {
|
|
const tbody = document.getElementById('table_body');
|
|
tbody.innerHTML = '';
|
|
for (let i = 0; i < result.labels.length; i++) {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${result.labels[i]}</td>
|
|
<td>${fmt_eur(result.buyer_nw_arr[i])}</td>
|
|
<td>${fmt_eur(result.renter_nw_arr[i])}</td>
|
|
<td>${fmt_eur(result.cum_buy_arr[i])}</td>
|
|
<td>${fmt_eur(result.cum_rent_arr[i])}</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
}
|
|
};
|
|
|
|
// ===== Result cards =====
|
|
|
|
const update_results = (result) => {
|
|
document.getElementById('res_buyer_nw').textContent = fmt_eur(result.final_buyer_nw);
|
|
document.getElementById('res_renter_nw').textContent = fmt_eur(result.final_renter_nw);
|
|
document.getElementById('res_buy_cost').textContent = `Gesamtausgaben: ${fmt_eur(result.cum_buy_arr.at(-1))}`;
|
|
document.getElementById('res_rent_cost').textContent = `Gesamtausgaben: ${fmt_eur(result.cum_rent_arr.at(-1))}`;
|
|
|
|
// breakeven
|
|
const be_el = document.getElementById('res_breakeven');
|
|
if (result.breakeven_month !== null) {
|
|
const be_yr = (result.breakeven_month / 12).toFixed(1);
|
|
be_el.textContent = `Jahr ${be_yr}`;
|
|
} else {
|
|
be_el.textContent = 'Kein Break-Even';
|
|
}
|
|
|
|
// winner badge
|
|
const badge_el = document.getElementById('res_winner_badge');
|
|
if (result.winner === 'buy') {
|
|
badge_el.innerHTML = '<span class="winner-badge winner-buy">Kaufen gewinnt</span>';
|
|
} else {
|
|
badge_el.innerHTML = '<span class="winner-badge winner-rent">Mieten gewinnt</span>';
|
|
}
|
|
|
|
// monthly payment note
|
|
document.getElementById('res_monthly_payment').textContent =
|
|
`Monatliche Rate: ${fmt_eur(result.monthly_payment)} · Nettovermögen nach Kapitalertragsteuer`;
|
|
};
|
|
|
|
// ===== Main recalc =====
|
|
|
|
const recalc = () => {
|
|
const p = get_params();
|
|
const result = run_simulation(p);
|
|
|
|
update_results(result);
|
|
update_chart(chart_nw, result.labels, result.buyer_nw_arr, result.renter_nw_arr, result.breakeven_month, p.years);
|
|
update_chart(chart_cost, result.labels, result.cum_buy_arr, result.cum_rent_arr, null, p.years);
|
|
build_table(result);
|
|
};
|
|
|
|
// ===== Slider ↔ number sync =====
|
|
|
|
const wire_slider = (base_id) => {
|
|
const slider = document.getElementById(`${base_id}_slider`);
|
|
const num = document.getElementById(base_id);
|
|
if (!slider || !num) return;
|
|
|
|
slider.addEventListener('input', () => {
|
|
num.value = slider.value;
|
|
recalc();
|
|
});
|
|
num.addEventListener('input', () => {
|
|
slider.value = num.value;
|
|
recalc();
|
|
});
|
|
};
|
|
|
|
const SLIDER_IDS = [
|
|
'years', 'invest_return', 'tax_rate',
|
|
'down_pct', 'interest_rate', 'loan_years',
|
|
'closing_buy_pct', 'closing_sell_pct', 'appreciation',
|
|
'prop_tax_rate', 'maint_rate',
|
|
'rent_increase',
|
|
];
|
|
|
|
SLIDER_IDS.forEach(wire_slider);
|
|
|
|
// wire plain number inputs (no slider)
|
|
['home_price', 'insurance', 'monthly_rent', 'renters_ins', 'start_capital'].forEach((id) => {
|
|
document.getElementById(id).addEventListener('input', recalc);
|
|
});
|
|
|
|
// ===== Table toggle =====
|
|
|
|
const toggle_btn = document.getElementById('table_toggle');
|
|
const table_wrap = document.getElementById('table_wrap');
|
|
|
|
toggle_btn.addEventListener('click', () => {
|
|
const expanded = toggle_btn.getAttribute('aria-expanded') === 'true';
|
|
toggle_btn.setAttribute('aria-expanded', String(!expanded));
|
|
toggle_btn.querySelector('.toggle-icon').style.transform = expanded ? '' : 'rotate(90deg)';
|
|
table_wrap.hidden = expanded;
|
|
});
|
|
|
|
// ===== Init =====
|
|
recalc();
|