From 3a4e1b90e2a57b9b8d35759b8ae6ab21a3abaf12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Wed, 3 Jun 2026 13:18:08 +0200 Subject: [PATCH] changes2 --- app/main.py | 11 ++++---- app/models.py | 6 +++- app/simulator.py | 9 ++++-- app/static/app.js | 64 ++++++++++++++++++++++++++++++++++++++----- app/static/index.html | 32 ++++++++++++++++++++-- app/static/styles.css | 4 ++- 6 files changed, 107 insertions(+), 19 deletions(-) diff --git a/app/main.py b/app/main.py index 952cfa1..2f6f28c 100644 --- a/app/main.py +++ b/app/main.py @@ -345,12 +345,13 @@ def export_pdf(req: SimulationRequest): story.append(Spacer(1, 0.25 * cm)) params = [ - ["Kwota kredytu", _money(req.principal), "Okres", f"{req.years} lat"], + ["Kwota kredytu", _money(req.principal), "Okres", f"{req.term_months or req.years * 12} mies. ({round((req.term_months or req.years * 12) / 12, 1)} lat)"], ["Stopa bazowa", _pct(req.base_rate), "Marza", _pct(req.margin)], ["Oprocentowanie startowe", _pct(req.base_rate + req.margin), "Typ rat", _installment_label(req.installment_type.value)], ["Efekt nadplat", _effect_label(req.overpayment_effect.value), "Liczba rat po symulacji", str(result.summary.months)], ["Data startu", req.loan_start_date.isoformat(), "Dzien splaty", str(req.due_day)], ["Przesuwaj dni wolne", "tak" if req.move_due_date_to_business_day else "nie", "Data konca", result.summary.payoff_date or "-"], + ["Okres ochronny nadplat", str(req.overpayment_protection_months or "brak"), "Domyslna prowizja", _pct(req.overpayment_protection_commission_percent)], ] story.append(Paragraph("Parametry wejściowe", styles["Heading2"])) story.append(Table(params, colWidths=[4.9 * cm, 3.4 * cm, 4.4 * cm, 3.9 * cm], style=TableStyle([ @@ -418,13 +419,13 @@ def export_pdf(req: SimulationRequest): story.append(Spacer(1, 0.35 * cm)) story.append(Paragraph("Nadplaty", styles["Heading2"])) - over_rows = [["Miesiac", "Kwota", "Prowizja", "Powtarzanie", "Do miesiaca"]] + over_rows = [["Miesiac", "Kwota", "Prowizja", "Prow. do mies.", "Powtarzanie", "Nadplaty do mies."]] if req.overpayments: for op in sorted(req.overpayments, key=lambda x: x.month): - over_rows.append([str(op.month), _money(op.amount), _pct(op.commission_percent), _repeat_label(op.repeat), str(op.until_month or "-")]) + over_rows.append([str(op.month), _money(op.amount), _pct(op.commission_percent), str(op.commission_until_month or "bez limitu"), _repeat_label(op.repeat), str(op.until_month or "-")]) else: - over_rows.append(["-", "-", "-", "-", "-"]) - story.append(Table(over_rows, repeatRows=1, colWidths=[2.4 * cm, 3.4 * cm, 2.4 * cm, 3.4 * cm, 3.4 * cm], style=TableStyle([ + over_rows.append(["-", "-", "-", "-", "-", "-"]) + story.append(Table(over_rows, repeatRows=1, colWidths=[1.8 * cm, 2.8 * cm, 2.1 * cm, 2.7 * cm, 2.8 * cm, 2.8 * cm], style=TableStyle([ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#e5e7eb")), ("GRID", (0, 0), (-1, -1), 0.25, colors.HexColor("#cbd5e1")), ("FONTNAME", (0, 0), (-1, -1), APP_FONT), diff --git a/app/models.py b/app/models.py index 97243d7..3fa2f98 100644 --- a/app/models.py +++ b/app/models.py @@ -33,6 +33,7 @@ class Overpayment(BaseModel): repeat: Literal["once", "monthly", "yearly"] = "once" until_month: int | None = Field(default=None, ge=1) commission_percent: float = Field(default=0, ge=0, le=20, description="Prowizja od nadplaty w procentach") + commission_until_month: int | None = Field(default=None, ge=1, description="Ostatni miesiac naliczania prowizji od nadplaty") class HistoricalMonth(BaseModel): @@ -46,7 +47,8 @@ class HistoricalMonth(BaseModel): class SimulationRequest(BaseModel): principal: float = Field(gt=0) - years: int = Field(ge=1, le=50) + years: int = Field(default=25, ge=1, le=50) + term_months: int | None = Field(default=None, ge=1, le=600, description="Okres kredytu w miesiacach") margin: float = Field(ge=0, le=20, default=2.0) base_rate: float = Field(ge=0, le=30, default=5.75) installment_type: InstallmentType = InstallmentType.equal @@ -54,6 +56,8 @@ class SimulationRequest(BaseModel): loan_start_date: date = Field(default_factory=date.today) due_day: int = Field(default=5, ge=1, le=28, description="Dzien splaty raty") move_due_date_to_business_day: bool = True + overpayment_protection_months: int | None = Field(default=None, ge=1, description="Okres ochronny prowizji od nadplat w miesiacach") + overpayment_protection_commission_percent: float = Field(default=0, ge=0, le=20, description="Domyslna prowizja od nadplat w okresie ochronnym") rate_changes: list[RateChange] = Field(default_factory=list) overpayments: list[Overpayment] = Field(default_factory=list) historical_months: list[HistoricalMonth] = Field(default_factory=list) diff --git a/app/simulator.py b/app/simulator.py index 786bf16..cbb6c38 100644 --- a/app/simulator.py +++ b/app/simulator.py @@ -127,7 +127,8 @@ def _scheduled_overpayment_for_month(req: SimulationRequest, month: int) -> tupl active = True if active: total += op.amount - fee += op.amount * op.commission_percent / 100 + if op.commission_until_month is None or month <= op.commission_until_month: + fee += op.amount * op.commission_percent / 100 return total, fee @@ -144,9 +145,13 @@ def _overpayment_for_month(req: SimulationRequest, month: int, historical: dict[ return scheduled_amount + hist_amount, scheduled_fee + hist_fee +def _term_months(req: SimulationRequest) -> int: + return int(req.term_months or req.years * 12) + + def _simulate_raw(req: SimulationRequest, include_overpayments: bool = True) -> list[ScheduleRow]: balance = float(req.principal) - total_months = int(req.years * 12) + total_months = _term_months(req) rows: list[ScheduleRow] = [] fixed_payment: float | None = None month = 1 diff --git a/app/static/app.js b/app/static/app.js index c636111..8028f7f 100644 --- a/app/static/app.js +++ b/app/static/app.js @@ -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.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); diff --git a/app/static/index.html b/app/static/index.html index 9b74bf1..f308d08 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -13,7 +13,7 @@

Symulator kredytu hipotecznego

-

@linuiarz.pl

+

@linuxiarz.pl

@@ -38,8 +38,15 @@
- - + + +
+
+
+ + 25 lat = 300 mies. +
+
@@ -86,6 +93,25 @@
+
+
+
+

Okres ochronny nadpłat

+
Domyślna prowizja automatycznie wpisywana do nowych nadpłat
+
+ +
+
+
+ + +
+
+ + +
+
+

Nadpłaty

diff --git a/app/static/styles.css b/app/static/styles.css index 8440c87..57de217 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -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; }