This commit is contained in:
Mateusz Gruszczyński
2026-06-03 22:31:29 +02:00
parent 4bdb20d9f5
commit 85ffc36e75
3 changed files with 52 additions and 6 deletions
+48 -2
View File
@@ -190,17 +190,63 @@ async def nbp_rate():
) )
CSV_MONEY_FIELDS = {
"rata",
"kapital",
"odsetki",
"nadplata",
"prowizja_nadplaty",
"saldo",
"odsetki_narastajaco",
"koszt_narastajaco",
"nadplaty_narastajaco",
}
CSV_SUMMARY_MONEY_FIELDS = {
"total_paid",
"total_interest",
"total_overpayment",
"total_overpayment_fees",
"interest_saved",
"baseline_interest",
"average_payment",
"max_payment",
}
def _csv_money(value: float) -> str:
return f"{float(value):.2f}".replace(".", ",")
@app.post("/api/export/csv") @app.post("/api/export/csv")
def export_csv(req: SimulationRequest): def export_csv(req: SimulationRequest):
result = simulate(req) result = simulate(req)
buf = io.StringIO() buf = io.StringIO()
writer = csv.writer(buf, delimiter=";") writer = csv.writer(buf, delimiter=";")
writer.writerow(["miesiac", "data_splaty", "dni", "oprocentowanie", "rata", "kapital", "odsetki", "nadplata", "prowizja_nadplaty", "saldo", "karencja", "odsetki_narastajaco", "koszt_narastajaco", "nadplaty_narastajaco"]) headers = ["miesiac", "data_splaty", "dni", "oprocentowanie", "rata", "kapital", "odsetki", "nadplata", "prowizja_nadplaty", "saldo", "karencja", "odsetki_narastajaco", "koszt_narastajaco", "nadplaty_narastajaco"]
writer.writerow(headers)
for row in result.schedule: for row in result.schedule:
writer.writerow([row.month, row.due_date, row.days, row.rate, row.payment, row.principal_part, row.interest_part, row.overpayment, row.overpayment_fee, row.remaining, row.grace_type.value, row.cumulative_interest, row.cumulative_cost, row.cumulative_overpayment]) values = {
"miesiac": row.month,
"data_splaty": row.due_date,
"dni": row.days,
"oprocentowanie": row.rate,
"rata": row.payment,
"kapital": row.principal_part,
"odsetki": row.interest_part,
"nadplata": row.overpayment,
"prowizja_nadplaty": row.overpayment_fee,
"saldo": row.remaining,
"karencja": row.grace_type.value,
"odsetki_narastajaco": row.cumulative_interest,
"koszt_narastajaco": row.cumulative_cost,
"nadplaty_narastajaco": row.cumulative_overpayment,
}
writer.writerow([_csv_money(values[h]) if h in CSV_MONEY_FIELDS else values[h] for h in headers])
writer.writerow([]) writer.writerow([])
writer.writerow(["Podsumowanie"]) writer.writerow(["Podsumowanie"])
for key, value in result.summary.model_dump().items(): for key, value in result.summary.model_dump().items():
if key in CSV_SUMMARY_MONEY_FIELDS:
value = _csv_money(value)
writer.writerow([key, value]) writer.writerow([key, value])
data = buf.getvalue().encode("utf-8-sig") data = buf.getvalue().encode("utf-8-sig")
return StreamingResponse( return StreamingResponse(
+1 -1
View File
@@ -1,5 +1,5 @@
const $ = (id) => document.getElementById(id); const $ = (id) => document.getElementById(id);
const money = (v) => new Intl.NumberFormat('pl-PL', { style: 'currency', currency: 'PLN', maximumFractionDigits: 0 }).format(v || 0); const money = (v) => new Intl.NumberFormat('pl-PL', { style: 'currency', currency: 'PLN', minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(Number(v) || 0);
const num = (id) => Number($(id).value || 0); const num = (id) => Number($(id).value || 0);
let lineChart, pieChart, barChart, detailChart; let lineChart, pieChart, barChart, detailChart;
+3 -3
View File
@@ -13,13 +13,13 @@
<div class="card-body d-flex flex-wrap align-items-center justify-content-between gap-3"> <div class="card-body d-flex flex-wrap align-items-center justify-content-between gap-3">
<div> <div>
<h1 class="h3 mb-1">Symulator kredytu hipotecznego</h1> <h1 class="h3 mb-1">Symulator kredytu hipotecznego</h1>
<p class="text-muted mb-0">@linuxiarz.pl</p> <p class="text-muted mb-0">Dane liczone są w backendzie ale nie są przechowywane na serwerze, może swoje wyliczenia eksportować na na swój dysk</p>
</div> </div>
<div class="d-flex gap-2 flex-wrap justify-content-end"> <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> <button id="themeToggle" class="btn btn-outline-secondary btn-sm" type="button" aria-label="Przełącz motyw">☀️ Jasny</button>
<button id="loadNbp" class="btn btn-outline-primary btn-sm">Pobierz stopę NBP</button> <button id="loadNbp" class="btn btn-outline-primary btn-sm">Pobierz stopę NBP</button>
<button id="exportJson" class="btn btn-outline-secondary btn-sm">Eksport JSON</button> <button id="exportJson" class="btn btn-outline-secondary btn-sm">Eksport</button>
<button id="importJson" class="btn btn-outline-secondary btn-sm">Import JSON</button> <button id="importJson" class="btn btn-outline-secondary btn-sm">Import </button>
<button id="exportCsv" class="btn btn-outline-secondary btn-sm">CSV</button> <button id="exportCsv" class="btn btn-outline-secondary btn-sm">CSV</button>
<button id="exportPdf" class="btn btn-primary btn-sm">PDF</button> <button id="exportPdf" class="btn btn-primary btn-sm">PDF</button>
<input id="jsonFile" type="file" accept="application/json,.json" hidden> <input id="jsonFile" type="file" accept="application/json,.json" hidden>