changes2
This commit is contained in:
+57
-7
@@ -13,6 +13,38 @@ function todayIso() {
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function updateTermLabel() {
|
||||
const months = Math.max(1, Number($('termMonths').value || 1));
|
||||
const years = Math.max(1, Math.round(months / 12));
|
||||
$('yearsSlider').value = Math.min(50, years);
|
||||
$('yearsSliderLabel').textContent = `${months} mies. ≈ ${(months / 12).toFixed(1)} lat`;
|
||||
}
|
||||
|
||||
function setMonthsFromYearsSlider() {
|
||||
const years = Number($('yearsSlider').value || 1);
|
||||
$('termMonths').value = years * 12;
|
||||
$('yearsSliderLabel').textContent = `${years} lat = ${years * 12} mies.`;
|
||||
recalc();
|
||||
}
|
||||
|
||||
function defaultProtection() {
|
||||
return {
|
||||
commission: Number($('protectionCommission')?.value || 0),
|
||||
until: Number($('protectionMonths')?.value || 0) || ''
|
||||
};
|
||||
}
|
||||
|
||||
function applyProtectionToOverpayments() {
|
||||
const defaults = defaultProtection();
|
||||
document.querySelectorAll('#overpayments .dynamic-row').forEach(row => {
|
||||
const commissionInput = row.querySelector('[data-field="commission"]');
|
||||
const untilInput = row.querySelector('[data-field="commissionUntil"]');
|
||||
if (commissionInput) commissionInput.value = defaults.commission || 0;
|
||||
if (untilInput) untilInput.value = defaults.until || '';
|
||||
});
|
||||
recalc();
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
document.body.dataset.theme = theme;
|
||||
localStorage.setItem('mortgage-theme', theme);
|
||||
@@ -49,7 +81,8 @@ function buildRequest() {
|
||||
amount: Number(row.querySelector('[data-field="amount"]').value || 0),
|
||||
repeat: row.querySelector('[data-field="repeat"]').value,
|
||||
until_month: Number(row.querySelector('[data-field="until"]').value || 0) || null,
|
||||
commission_percent: Number(row.querySelector('[data-field="commission"]').value || 0)
|
||||
commission_percent: Number(row.querySelector('[data-field="commission"]').value || 0),
|
||||
commission_until_month: Number(row.querySelector('[data-field="commissionUntil"]').value || 0) || null
|
||||
})).filter(x => x.month > 0 && x.amount > 0);
|
||||
|
||||
const historicalMonths = [...document.querySelectorAll('#historicalMonths .dynamic-row')].map(row => {
|
||||
@@ -64,9 +97,12 @@ function buildRequest() {
|
||||
};
|
||||
}).filter(x => x.month > 0);
|
||||
|
||||
const termMonths = Math.max(1, Math.round(num('termMonths') || 1));
|
||||
|
||||
return {
|
||||
principal: num('principal'),
|
||||
years: num('years'),
|
||||
years: Math.max(1, Math.ceil(termMonths / 12)),
|
||||
term_months: termMonths,
|
||||
margin: num('margin'),
|
||||
base_rate: num('baseRate'),
|
||||
installment_type: $('installmentType').value,
|
||||
@@ -74,6 +110,8 @@ function buildRequest() {
|
||||
loan_start_date: $('loanStartDate').value || todayIso(),
|
||||
due_day: num('dueDay') || 5,
|
||||
move_due_date_to_business_day: $('moveDueDate').checked,
|
||||
overpayment_protection_months: Number($('protectionMonths').value || 0) || null,
|
||||
overpayment_protection_commission_percent: Number($('protectionCommission').value || 0),
|
||||
rate_changes: rateChanges,
|
||||
overpayments: overpayments,
|
||||
historical_months: historicalMonths
|
||||
@@ -101,15 +139,19 @@ function addRateRow(month = 13, rate = 7.0) {
|
||||
recalc();
|
||||
}
|
||||
|
||||
function addOverpaymentRow(month = 12, amount = 10000, repeat = 'once', until = '', commission = 0) {
|
||||
function addOverpaymentRow(month = 12, amount = 10000, repeat = 'once', until = '', commission = null, commissionUntil = null) {
|
||||
const defaults = defaultProtection();
|
||||
if (commission === null || commission === undefined || commission === '') commission = defaults.commission;
|
||||
if (commissionUntil === null || commissionUntil === undefined || commissionUntil === '') commissionUntil = defaults.until;
|
||||
const div = document.createElement('div');
|
||||
div.className = 'dynamic-row overpay';
|
||||
div.innerHTML = `
|
||||
<div><label class="form-label">Miesiąc</label><input data-field="month" type="number" min="1" class="form-control form-control-sm" value="${month}"></div>
|
||||
<div><label class="form-label">Kwota</label><input data-field="amount" type="number" min="1" class="form-control form-control-sm" value="${amount}"></div>
|
||||
<div><label class="form-label">Prowizja %</label><input data-field="commission" type="number" min="0" step="0.01" class="form-control form-control-sm" value="${commission}"></div>
|
||||
<div><label class="form-label">Prowizja do mies.</label><input data-field="commissionUntil" type="number" min="1" class="form-control form-control-sm" value="${commissionUntil || ''}" placeholder="bez limitu"></div>
|
||||
<div><label class="form-label">Powtarzaj</label><select data-field="repeat" class="form-select form-select-sm"><option value="once">raz</option><option value="monthly">co miesiąc</option><option value="yearly">co rok</option></select></div>
|
||||
<div><label class="form-label">Do mies.</label><input data-field="until" type="number" min="1" class="form-control form-control-sm" value="${until || ''}"></div>
|
||||
<div><label class="form-label">Nadpłaty do mies.</label><input data-field="until" type="number" min="1" class="form-control form-control-sm" value="${until || ''}"></div>
|
||||
<button class="btn btn-sm btn-outline-danger" type="button">Usuń</button>`;
|
||||
div.querySelector('[data-field="repeat"]').value = repeat;
|
||||
div.querySelector('button').onclick = () => { div.remove(); recalc(); };
|
||||
@@ -237,7 +279,9 @@ function clearRows() {
|
||||
|
||||
function applyRequest(data) {
|
||||
$('principal').value = data.principal ?? 600000;
|
||||
$('years').value = data.years ?? 25;
|
||||
const loadedMonths = data.term_months ?? ((data.years ?? 25) * 12);
|
||||
$('termMonths').value = loadedMonths;
|
||||
updateTermLabel();
|
||||
$('margin').value = data.margin ?? 2;
|
||||
$('baseRate').value = data.base_rate ?? 5.75;
|
||||
$('installmentType').value = data.installment_type ?? 'equal';
|
||||
@@ -245,9 +289,11 @@ function applyRequest(data) {
|
||||
$('loanStartDate').value = data.loan_start_date ?? todayIso();
|
||||
$('dueDay').value = data.due_day ?? 5;
|
||||
$('moveDueDate').checked = data.move_due_date_to_business_day ?? true;
|
||||
$('protectionMonths').value = data.overpayment_protection_months ?? '';
|
||||
$('protectionCommission').value = data.overpayment_protection_commission_percent ?? 0;
|
||||
clearRows();
|
||||
(data.rate_changes || []).forEach(x => addRateRow(x.month, x.annual_rate));
|
||||
(data.overpayments || []).forEach(x => addOverpaymentRow(x.month, x.amount, x.repeat, x.until_month || '', x.commission_percent || 0));
|
||||
(data.overpayments || []).forEach(x => addOverpaymentRow(x.month, x.amount, x.repeat, x.until_month || '', x.commission_percent || 0, x.commission_until_month || ''));
|
||||
(data.historical_months || []).forEach(x => addHistoricalRow(x.month, x.annual_rate ?? '', x.grace_type || 'none', x.overpayment || 0, x.overpayment_commission_percent || 0, x.note || ''));
|
||||
recalc();
|
||||
}
|
||||
@@ -282,9 +328,12 @@ async function loadNbp() {
|
||||
}
|
||||
}
|
||||
|
||||
['principal','years','baseRate','margin','installmentType','overpaymentEffect','loanStartDate','dueDay','moveDueDate'].forEach(id => $(id).addEventListener('input', recalc));
|
||||
['principal','baseRate','margin','installmentType','overpaymentEffect','loanStartDate','dueDay','moveDueDate','protectionMonths','protectionCommission'].forEach(id => $(id).addEventListener('input', recalc));
|
||||
$('termMonths').addEventListener('input', () => { updateTermLabel(); recalc(); });
|
||||
$('yearsSlider').addEventListener('input', setMonthsFromYearsSlider);
|
||||
$('addRate').onclick = () => addRateRow();
|
||||
$('addOverpayment').onclick = () => addOverpaymentRow();
|
||||
$('applyProtection').onclick = applyProtectionToOverpayments;
|
||||
$('addHistorical').onclick = () => addHistoricalRow();
|
||||
$('exportCsv').onclick = () => download('/api/export/csv', 'symulacja-kredytu.csv');
|
||||
$('exportPdf').onclick = () => download('/api/export/pdf', 'symulacja-kredytu.pdf');
|
||||
@@ -293,6 +342,7 @@ $('importJson').onclick = () => $('jsonFile').click();
|
||||
$('jsonFile').onchange = (e) => e.target.files?.[0] && importJsonFile(e.target.files[0]);
|
||||
$('loadNbp').onclick = loadNbp;
|
||||
$('loanStartDate').value = todayIso();
|
||||
updateTermLabel();
|
||||
initTheme();
|
||||
|
||||
addOverpaymentRow(24, 20000, 'once', '', 0);
|
||||
|
||||
+29
-3
@@ -13,7 +13,7 @@
|
||||
<div class="card-body d-flex flex-wrap align-items-center justify-content-between gap-3">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">Symulator kredytu hipotecznego</h1>
|
||||
<p class="text-muted mb-0">@linuiarz.pl</p>
|
||||
<p class="text-muted mb-0">@linuxiarz.pl</p>
|
||||
</div>
|
||||
<div class="d-flex gap-2 flex-wrap justify-content-end">
|
||||
<button id="themeToggle" class="btn btn-outline-secondary btn-sm" type="button" aria-label="Przełącz motyw">☀️ Jasny</button>
|
||||
@@ -38,8 +38,15 @@
|
||||
<input id="principal" type="number" class="form-control form-control-sm" value="600000" min="1">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Okres lat</label>
|
||||
<input id="years" type="number" class="form-control form-control-sm" value="25" min="1" max="50">
|
||||
<label class="form-label">Okres w miesiącach</label>
|
||||
<input id="termMonths" type="number" class="form-control form-control-sm" value="300" min="1" max="600">
|
||||
</div>
|
||||
<div class="col-12 term-slider-wrap">
|
||||
<div class="d-flex justify-content-between align-items-center gap-2">
|
||||
<label class="form-label mb-0" for="yearsSlider">Szybki wybór lat</label>
|
||||
<span id="yearsSliderLabel" class="small text-muted">25 lat = 300 mies.</span>
|
||||
</div>
|
||||
<input id="yearsSlider" type="range" class="form-range" min="1" max="50" step="1" value="25">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Stopa bazowa %</label>
|
||||
@@ -86,6 +93,25 @@
|
||||
</div>
|
||||
<div id="rateChanges" class="stack"></div>
|
||||
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between align-items-start gap-2 mb-2">
|
||||
<div>
|
||||
<h3 class="h6 mb-0">Okres ochronny nadpłat</h3>
|
||||
<div class="text-muted small">Domyślna prowizja automatycznie wpisywana do nowych nadpłat</div>
|
||||
</div>
|
||||
<button id="applyProtection" class="btn btn-sm btn-outline-secondary" type="button">Uzupełnij</button>
|
||||
</div>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-6">
|
||||
<label class="form-label">Prowizja %</label>
|
||||
<input id="protectionCommission" type="number" min="0" max="20" step="0.01" class="form-control form-control-sm" value="0">
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<label class="form-label">Do miesiąca</label>
|
||||
<input id="protectionMonths" type="number" min="1" class="form-control form-control-sm" placeholder="np. 36">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h3 class="h6 mb-0">Nadpłaty</h3>
|
||||
|
||||
@@ -88,6 +88,8 @@ body {
|
||||
.chart-card canvas { max-height: 330px; }
|
||||
.chart-card-sm canvas { max-height: 230px; }
|
||||
.form-label { font-size: .78rem; margin-bottom: .2rem; }
|
||||
.term-slider-wrap { padding: .25rem .1rem .15rem; }
|
||||
.form-range { margin-bottom: 0; }
|
||||
.form-control, .form-select { border-radius: 10px; }
|
||||
.stack { display: grid; gap: .45rem; }
|
||||
.dynamic-row {
|
||||
@@ -100,7 +102,7 @@ body {
|
||||
padding: .55rem;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.dynamic-row.overpay { grid-template-columns: .75fr 1fr .75fr .9fr .75fr auto; }
|
||||
.dynamic-row.overpay { grid-template-columns: .65fr .9fr .7fr .9fr .9fr .9fr auto; }
|
||||
.dynamic-row.historical { grid-template-columns: .65fr .8fr 1fr .9fr .7fr 1.2fr auto; }
|
||||
.stat-card { border-radius: 16px; }
|
||||
.stat-value { font-size: 1.15rem; font-weight: 700; }
|
||||
|
||||
Reference in New Issue
Block a user