From 1a423a8b923180dd1d9dc6b02c4f804d244b537f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 22 Sep 2025 14:01:57 +0200 Subject: [PATCH] listy, i inne funkcje --- app.py | 268 ++++++++++++++++++++++--- static/js/kwoty_formularz.js | 153 ++++++++++++++ static/js/produkty_formularz.js | 73 +++++++ static/js/transakcje.js | 26 +++ templates/admin/dashboard.html | 10 +- templates/admin/formularz_zbiorek.html | 119 ++++++++++- templates/admin/transakcje.html | 143 +++++++++++++ templates/zbiorka.html | 149 ++++++++++++-- 8 files changed, 877 insertions(+), 64 deletions(-) create mode 100644 static/js/kwoty_formularz.js create mode 100644 static/js/produkty_formularz.js create mode 100644 static/js/transakcje.js create mode 100644 templates/admin/transakcje.html diff --git a/app.py b/app.py index 2d99525..75a609c 100644 --- a/app.py +++ b/app.py @@ -9,9 +9,9 @@ from flask_login import ( UserMixin, ) from werkzeug.security import generate_password_hash, check_password_hash -from datetime import datetime +from datetime import datetime, timezone from markupsafe import Markup -from sqlalchemy import event +from sqlalchemy import event, Numeric from sqlalchemy.engine import Engine from decimal import Decimal, InvalidOperation import markdown as md @@ -28,9 +28,16 @@ db = SQLAlchemy(app) login_manager = LoginManager(app) login_manager.login_view = "zaloguj" + +try: + from zoneinfo import ZoneInfo # Python 3.9+ +except ImportError: + from backports.zoneinfo import ZoneInfo + +LOCAL_TZ = ZoneInfo("Europe/Warsaw") + + # MODELE - - class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) @@ -50,8 +57,8 @@ class Zbiorka(db.Model): opis = db.Column(db.Text, nullable=False) numer_konta = db.Column(db.String(50), nullable=False) numer_telefonu_blik = db.Column(db.String(50), nullable=False) - cel = db.Column(db.Float, nullable=False, default=0.0) - stan = db.Column(db.Float, default=0.0) + cel = db.Column(Numeric(12, 2), nullable=False, default=0) + stan = db.Column(Numeric(12, 2), default=0) ukryta = db.Column(db.Boolean, default=False) ukryj_kwote = db.Column(db.Boolean, default=False) zrealizowana = db.Column(db.Boolean, default=False) @@ -74,6 +81,26 @@ class Zbiorka(db.Model): passive_deletes=True, ) + przedmioty = db.relationship( + "Przedmiot", + backref="zbiorka", + lazy=True, + order_by="Przedmiot.id.asc()", + cascade="all, delete-orphan", + passive_deletes=True, + ) + +class Przedmiot(db.Model): + id = db.Column(db.Integer, primary_key=True) + zbiorka_id = db.Column( + db.Integer, + db.ForeignKey("zbiorka.id", ondelete="CASCADE"), + nullable=False, + ) + nazwa = db.Column(db.String(120), nullable=False) + link = db.Column(db.String(255), nullable=True) + cena = db.Column(Numeric(12, 2), nullable=True) + kupione = db.Column(db.Boolean, default=False) class Wplata(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -82,7 +109,7 @@ class Wplata(db.Model): db.ForeignKey("zbiorka.id", ondelete="CASCADE"), nullable=False, ) - kwota = db.Column(db.Float, nullable=False) + kwota = db.Column(Numeric(12, 2), nullable=False) data = db.Column(db.DateTime, default=datetime.utcnow) opis = db.Column(db.Text, nullable=True) @@ -96,7 +123,7 @@ class Wydatek(db.Model): db.ForeignKey("zbiorka.id", ondelete="CASCADE"), nullable=False, ) - kwota = db.Column(db.Float, nullable=False) + kwota = db.Column(Numeric(12, 2), nullable=False) data = db.Column(db.DateTime, default=datetime.utcnow) opis = db.Column(db.Text, nullable=True) @@ -109,7 +136,6 @@ class GlobalSettings(db.Model): logo_url = db.Column(db.String(255), nullable=True) site_title = db.Column(db.String(120), nullable=True) show_logo_in_navbar = db.Column(db.Boolean, default=False) - show_logo_in_navbar = db.Column(db.Boolean, default=False) navbar_brand_mode = db.Column(db.String(10), default="text") footer_brand_mode = db.Column(db.String(10), default="text") footer_text = db.Column(db.String(200), nullable=True) @@ -172,7 +198,23 @@ def is_allowed_ip(remote_ip, allowed_hosts_str): return remote_ip in allowed_ips -# Dodaj filtr Markdown – pozwala na zagnieżdżanie linków i obrazków w opisie +def to_local(dt): + if dt is None: + return None + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.astimezone(LOCAL_TZ) + + +@app.template_filter("dt") +def dt_filter(dt, fmt="%Y-%m-%d %H:%M"): + try: + ldt = to_local(dt) + return ldt.strftime(fmt) if ldt else "" + except Exception: + return "" + + @app.template_filter("markdown") def markdown_filter(text): return Markup(md.markdown(text)) @@ -200,7 +242,7 @@ def index(): return render_template("index.html", zbiorki=zbiorki) -@app.route("/zbiorki_zrealizowane") +@app.route("/zrealizowane") def zbiorki_zrealizowane(): zbiorki = Zbiorka.query.filter_by(zrealizowana=True).all() return render_template("index.html", zbiorki=zbiorki) @@ -297,8 +339,6 @@ def zarejestruj(): # PANEL ADMINISTRACYJNY - - @app.route("/admin") @login_required def admin_dashboard(): @@ -336,7 +376,7 @@ def formularz_zbiorek(zbiorka_id=None): numer_konta = request.form.get("numer_konta", "").strip() numer_telefonu_blik = request.form.get("numer_telefonu_blik", "").strip() - # Cel — walidacja liczby + # Cel — walidacja liczby (Decimal, nie float) try: cel_str = request.form.get("cel", "").replace(",", ".").strip() cel = Decimal(cel_str) @@ -361,27 +401,85 @@ def formularz_zbiorek(zbiorka_id=None): ukryj_kwote = "ukryj_kwote" in request.form + names = request.form.getlist("item_nazwa[]") + links = request.form.getlist("item_link[]") + prices = request.form.getlist("item_cena[]") + + def _read_price(val): + """Zwraca Decimal(>=0) albo None; akceptuje przecinek jako separator dziesiętny.""" + if not val or not val.strip(): + return None + try: + d = Decimal(val.replace(",", ".")) + if d < 0: + return None + return d + except Exception: + return None + + # --- ZAPIS ZBIÓRKI + PRODUKTÓW --- if is_edit: - # Aktualizacja istniejącej + # Aktualizacja istniejącej zbiórki zb.nazwa = nazwa zb.opis = opis zb.numer_konta = numer_konta zb.numer_telefonu_blik = numer_telefonu_blik - zb.cel = float(cel) # jeśli masz Decimal w modelu, przypisz bez konwersji + zb.cel = cel # ❗ bez float(cel) — zostaje Decimal zb.ukryj_kwote = ukryj_kwote + db.session.commit() # najpierw zapisz bazowe pola + + # Nadpisz listę produktów (czyść i dodaj od nowa dla prostoty) + zb.przedmioty.clear() + + for i, raw_name in enumerate(names): + name = (raw_name or "").strip() + if not name: + continue + link = (links[i] if i < len(links) else "").strip() or None + cena_val = _read_price(prices[i] if i < len(prices) else "") + kupione_val = request.form.get(f"item_kupione_val_{i}") == "1" + + db.session.add(Przedmiot( + zbiorka_id=zb.id, + nazwa=name, + link=link, + cena=cena_val, # Decimal albo None + kupione=kupione_val + )) + db.session.commit() flash("Zbiórka została zaktualizowana", "success") + else: - # Utworzenie nowej + # Utworzenie nowej zbiórki nowa = Zbiorka( nazwa=nazwa, opis=opis, numer_konta=numer_konta, numer_telefonu_blik=numer_telefonu_blik, - cel=float(cel), + cel=cel, # ❗ Decimal ukryj_kwote=ukryj_kwote, ) db.session.add(nowa) + db.session.commit() # potrzebujemy ID nowej zbiórki + + # Dodaj produkty do nowej zbiórki + for i, raw_name in enumerate(names): + name = (raw_name or "").strip() + if not name: + continue + link = (links[i] if i < len(links) else "").strip() or None + cena_val = _read_price(prices[i] if i < len(prices) else "") + kupione_val = request.form.get(f"item_kupione_val_{i}") == "1" + + db.session.add(Przedmiot( + zbiorka_id=nowa.id, + nazwa=name, + link=link, + cena=cena_val, # Decimal albo None + kupione=kupione_val + )) + db.session.commit() flash("Zbiórka została dodana", "success") @@ -393,24 +491,33 @@ def formularz_zbiorek(zbiorka_id=None): ) -# TRASA DODAWANIA WPŁATY Z OPISEM -# TRASA DODAWANIA WPŁATY W PANELU ADMINA @app.route("/admin/zbiorka//wplata/dodaj", methods=["GET", "POST"]) @login_required def dodaj_wplate(zbiorka_id): if not current_user.is_admin: flash("Brak uprawnień", "danger") return redirect(url_for("index")) + zb = Zbiorka.query.get_or_404(zbiorka_id) + if request.method == "POST": - kwota = float(request.form["kwota"]) + try: + kwota = Decimal(request.form.get("kwota", "").replace(",", ".")) + if kwota <= 0: + raise InvalidOperation + except (InvalidOperation, ValueError): + flash("Nieprawidłowa kwota (musi być > 0)", "danger") + return redirect(url_for("dodaj_wplate", zbiorka_id=zbiorka_id)) + opis = request.form.get("opis", "") nowa_wplata = Wplata(zbiorka_id=zb.id, kwota=kwota, opis=opis) - zb.stan += kwota # Aktualizacja stanu zbiórki + zb.stan = (zb.stan or Decimal("0")) + kwota db.session.add(nowa_wplata) db.session.commit() flash("Wpłata została dodana", "success") - return redirect(url_for("admin_dashboard")) + + next_url = request.args.get("next") + return redirect(next_url or url_for("transakcje_zbiorki", zbiorka_id=zb.id)) return render_template("admin/dodaj_wplate.html", zbiorka=zb) @@ -436,8 +543,8 @@ def edytuj_stan(zbiorka_id): zb = Zbiorka.query.get_or_404(zbiorka_id) if request.method == "POST": try: - nowy_stan = float(request.form["stan"]) - except ValueError: + nowy_stan = Decimal(request.form.get("stan", "").replace(",", ".")) + except (InvalidOperation, ValueError): flash("Nieprawidłowa wartość kwoty", "danger") return redirect(url_for("edytuj_stan", zbiorka_id=zbiorka_id)) zb.stan = nowy_stan @@ -469,8 +576,6 @@ def create_admin_account(): db.session.commit() -from flask import request - @app.after_request def apply_headers(response): @@ -588,22 +693,28 @@ def dodaj_wydatek(zbiorka_id): if not current_user.is_admin: flash("Brak uprawnień", "danger") return redirect(url_for("index")) + zb = Zbiorka.query.get_or_404(zbiorka_id) + if request.method == "POST": try: - kwota = float(request.form["kwota"]) + kwota = Decimal(request.form.get("kwota", "").replace(",", ".")) if kwota <= 0: - raise ValueError - except (KeyError, ValueError): - flash("Nieprawidłowa kwota", "danger") + raise InvalidOperation + except (InvalidOperation, ValueError): + flash("Nieprawidłowa kwota (musi być > 0)", "danger") return redirect(url_for("dodaj_wydatek", zbiorka_id=zbiorka_id)) + opis = request.form.get("opis", "") nowy_wydatek = Wydatek(zbiorka_id=zb.id, kwota=kwota, opis=opis) - zb.stan -= kwota + zb.stan = (zb.stan or Decimal("0")) - kwota db.session.add(nowy_wydatek) db.session.commit() flash("Wydatek został dodany", "success") - return redirect(url_for("admin_dashboard")) + + next_url = request.args.get("next") + return redirect(next_url or url_for("transakcje_zbiorki", zbiorka_id=zb.id)) + return render_template("admin/dodaj_wydatek.html", zbiorka=zb) @@ -646,6 +757,97 @@ def robots(): return robots_txt, 200, {"Content-Type": "text/plain"} +@app.route("/admin/zbiorka//transakcje") +@login_required +def transakcje_zbiorki(zbiorka_id): + if not current_user.is_admin: + flash("Brak uprawnień", "danger"); return redirect(url_for("index")) + zb = Zbiorka.query.get_or_404(zbiorka_id) + aktywnosci = ( + [{"typ": "wpłata", "id": w.id, "kwota": w.kwota, "opis": w.opis, "data": w.data} for w in zb.wplaty] + + [{"typ": "wydatek","id": x.id, "kwota": x.kwota,"opis": x.opis,"data": x.data} for x in zb.wydatki] + ) + aktywnosci.sort(key=lambda a: a["data"], reverse=True) + return render_template("admin/transakcje.html", zbiorka=zb, aktywnosci=aktywnosci) + + +@app.route("/admin/wplata//zapisz", methods=["POST"]) +@login_required +def zapisz_wplate(wplata_id): + if not current_user.is_admin: + flash("Brak uprawnień", "danger"); return redirect(url_for("index")) + w = Wplata.query.get_or_404(wplata_id) + zb = w.zbiorka + try: + nowa_kwota = Decimal(request.form.get("kwota", "").replace(",", ".")) + if nowa_kwota <= 0: + raise InvalidOperation + except (InvalidOperation, ValueError): + flash("Nieprawidłowa kwota (musi być > 0)", "danger") + return redirect(url_for("transakcje_zbiorki", zbiorka_id=zb.id)) + + delta = nowa_kwota - (w.kwota or Decimal("0")) + w.kwota = nowa_kwota + w.opis = request.form.get("opis", "") + zb.stan = (zb.stan or Decimal("0")) + delta + db.session.commit() + flash("Wpłata zaktualizowana", "success") + return redirect(url_for("transakcje_zbiorki", zbiorka_id=zb.id)) + + +@app.route("/admin/wplata//usun", methods=["POST"]) +@login_required +def usun_wplate(wplata_id): + if not current_user.is_admin: + flash("Brak uprawnień", "danger"); return redirect(url_for("index")) + w = Wplata.query.get_or_404(wplata_id) + zb = w.zbiorka + zb.stan -= w.kwota + db.session.delete(w) + db.session.commit() + flash("Wpłata usunięta", "success") + return redirect(url_for("transakcje_zbiorki", zbiorka_id=zb.id)) + + +@app.route("/admin/wydatek//zapisz", methods=["POST"]) +@login_required +def zapisz_wydatek(wydatek_id): + if not current_user.is_admin: + flash("Brak uprawnień", "danger"); return redirect(url_for("index")) + x = Wydatek.query.get_or_404(wydatek_id) + zb = x.zbiorka + try: + nowa_kwota = Decimal(request.form.get("kwota", "").replace(",", ".")) + if nowa_kwota <= 0: + raise InvalidOperation + except (InvalidOperation, ValueError): + flash("Nieprawidłowa kwota (musi być > 0)", "danger") + return redirect(url_for("transakcje_zbiorki", zbiorka_id=zb.id)) + + delta = nowa_kwota - (x.kwota or Decimal("0")) + x.kwota = nowa_kwota + x.opis = request.form.get("opis", "") + # wydatki zmniejszają stan; jeżeli delta>0, stan spada bardziej + zb.stan = (zb.stan or Decimal("0")) - delta + db.session.commit() + flash("Wydatek zaktualizowany", "success") + return redirect(url_for("transakcje_zbiorki", zbiorka_id=zb.id)) + + +@app.route("/admin/wydatek//usun", methods=["POST"]) +@login_required +def usun_wydatek(wydatek_id): + if not current_user.is_admin: + flash("Brak uprawnień", "danger"); return redirect(url_for("index")) + x = Wydatek.query.get_or_404(wydatek_id) + zb = x.zbiorka + zb.stan += x.kwota + db.session.delete(x) + db.session.commit() + flash("Wydatek usunięty", "success") + return redirect(url_for("transakcje_zbiorki", zbiorka_id=zb.id)) + + @app.route("/favicon.ico") def favicon(): return "", 204 diff --git a/static/js/kwoty_formularz.js b/static/js/kwoty_formularz.js new file mode 100644 index 0000000..9107300 --- /dev/null +++ b/static/js/kwoty_formularz.js @@ -0,0 +1,153 @@ +(function () { + const tbody = document.querySelector('#produkty-body'); + const celInput = document.querySelector('#cel'); + const box = document.querySelector('#celSyncBox'); + const msg = document.querySelector('#celSyncMsg'); + const btn = document.querySelector('#btnApplyCelFromSum'); + + if (!tbody || !celInput || !box || !msg || !btn) return; + + const EPS = 0.01; // tolerancja porównania + + function parsePrice(raw) { + if (!raw) return NaN; + const s = String(raw).trim().replace(/\s+/g, '').replace(',', '.'); + const n = Number(s); + return Number.isFinite(n) && n >= 0 ? n : NaN; + } + + function getRows() { + return Array.from(tbody.querySelectorAll('tr')); + } + + function computeSum() { + const rows = getRows(); + + let hasNamed = false; + let sumAll = 0; // suma ze wszystkich wierszy z nazwą i poprawną ceną + let sumToBuy = 0; // suma tylko z wierszy NIE oznaczonych jako "Kupione" + + for (const tr of rows) { + const nameInput = tr.querySelector('input[name="item_nazwa[]"]'); + const priceInput = tr.querySelector('input[name="item_cena[]"]'); + const kupioneSwitch = tr.querySelector('.kupione-switch'); + + const name = nameInput ? nameInput.value.trim() : ''; + if (!name) continue; // ignoruj puste wiersze bez nazwy + + hasNamed = true; + + const priceVal = priceInput ? parsePrice(priceInput.value) : NaN; + if (Number.isNaN(priceVal)) continue; + + // zawsze dolicz do sumy wszystkich + sumAll += priceVal; + + // do sumy do-kupienia tylko jeśli nie jest oznaczone jako kupione + if (!(kupioneSwitch && kupioneSwitch.checked)) { + sumToBuy += priceVal; + } + } + + return { hasNamed, sumAll, sumToBuy }; + } + + + function readCel() { + const v = parsePrice(celInput.value); + return Number.isNaN(v) ? null : v; + } + + function formatPln(n) { + // Nie narzucamy locale – prosto 2 miejsca + return n.toFixed(2); + } + + function updateUI() { + const { hasNamed, sumAll, sumToBuy } = computeSum(); + + // Brak produktów (brak nazw) lub obie sumy = 0 → nic nie pokazuj + if (!hasNamed || (sumAll <= 0 && sumToBuy <= 0)) { + box.classList.add('d-none'); + btn.classList.add('d-none'); + box.classList.remove('alert-success', 'alert-info'); + msg.textContent = ''; + return; + } + + const cel = readCel(); + const target = sumToBuy; // porównujemy do kwoty POZOSTAŁE DO KUPIENIA + + // Jeśli cel nie ustawiony lub NaN → zaproponuj ustawienie celu = sumToBuy + if (cel === null) { + box.classList.remove('d-none'); + box.classList.remove('alert-success'); + box.classList.add('alert-info'); + + // pokazujemy obie sumy w komunikacie + msg.innerHTML = ` +
Wszystkie: ${formatPln(sumAll)} PLN · + Do kupienia: ${formatPln(sumToBuy)} PLN
+
Możesz ustawić cel na kwotę do kupienia.
+ `; + btn.textContent = `Ustaw cel = ${formatPln(target)} PLN`; + btn.classList.remove('d-none'); + return; + } + + // Mamy cel — porównanie do sumy do-kupienia + if (Math.abs(cel - target) <= EPS) { + box.classList.remove('d-none'); + box.classList.remove('alert-info'); + box.classList.add('alert-success'); + msg.innerHTML = ` + Suma do kupienia (${formatPln(target)} PLN) jest równa celowi. +
Wszystkie: ${formatPln(sumAll)} PLN · Do kupienia: ${formatPln(sumToBuy)} PLN
+ `; + btn.classList.add('d-none'); + } else { + box.classList.remove('d-none'); + box.classList.remove('alert-success'); + box.classList.add('alert-info'); + msg.innerHTML = ` +
Wszystkie: ${formatPln(sumAll)} PLN · + Do kupienia: ${formatPln(sumToBuy)} PLN
+
Cel: ${formatPln(cel)} PLN
+ `; + btn.textContent = `Zaktualizuj cel do ${formatPln(target)} PLN`; + btn.classList.remove('d-none'); + } + } + + btn.addEventListener('click', (e) => { + e.preventDefault(); + const { sumToBuy } = computeSum(); + if (sumToBuy > 0) { + celInput.value = formatPln(sumToBuy); + celInput.dispatchEvent(new Event('input', { bubbles: true })); + celInput.dispatchEvent(new Event('change', { bubbles: true })); + updateUI(); + } + }); + + // Reaguj na zmiany cen/nazw + tbody.addEventListener('input', (e) => { + const name = e.target.getAttribute('name'); + if (name === 'item_nazwa[]' || name === 'item_cena[]') { + updateUI(); + } + }); + + // Reaguj na zmiany celu + celInput.addEventListener('input', updateUI); + celInput.addEventListener('change', updateUI); + + // Obserwuj dodawanie/usuwanie wierszy przez inne skrypty + const mo = new MutationObserver(() => updateUI()); + mo.observe(tbody, { childList: true, subtree: true }); + + // Init po załadowaniu + document.addEventListener('DOMContentLoaded', updateUI); + // i jedno wywołanie na starcie (gdy DOMContentLoaded już był) + updateUI(); +})(); diff --git a/static/js/produkty_formularz.js b/static/js/produkty_formularz.js new file mode 100644 index 0000000..7546c6a --- /dev/null +++ b/static/js/produkty_formularz.js @@ -0,0 +1,73 @@ +(function () { + const body = document.querySelector('#produkty-body'); + const addBtn = document.querySelector('#add-row'); + const clearBtn = document.querySelector('#clear-empty'); + + if (!body) return; + + function reindexHidden() { + const rows = [...body.querySelectorAll('tr')]; + rows.forEach((tr, idx) => { + const hidden = tr.querySelector('input[type="hidden"][name^="item_kupione_val_"]'); + if (hidden) hidden.name = `item_kupione_val_${idx}`; + }); + } + + function makeRow() { + const tr = document.createElement('tr'); + tr.innerHTML = ` + + + + +
+ + + +
+ + + + `; + return tr; + } + + body.addEventListener('change', (e) => { + if (e.target.classList.contains('kupione-switch')) { + const tr = e.target.closest('tr'); + const hidden = tr.querySelector('input[type="hidden"][name^="item_kupione_val_"]'); + const label = tr.querySelector('.form-check-label'); + if (hidden) hidden.value = e.target.checked ? '1' : '0'; + if (label) label.textContent = e.target.checked ? 'Kupione' : 'Do kupienia'; + } + }); + + body.addEventListener('click', (e) => { + if (e.target.classList.contains('remove-row')) { + e.preventDefault(); + const tr = e.target.closest('tr'); + tr.remove(); + reindexHidden(); + } + }); + + addBtn?.addEventListener('click', (e) => { + e.preventDefault(); + body.appendChild(makeRow()); + reindexHidden(); + }); + + clearBtn?.addEventListener('click', (e) => { + e.preventDefault(); + [...body.querySelectorAll('tr')].forEach(tr => { + const name = tr.querySelector('input[name="item_nazwa[]"]')?.value.trim(); + const link = tr.querySelector('input[name="item_link[]"]')?.value.trim(); + const cena = tr.querySelector('input[name="item_cena[]"]')?.value.trim(); + if (!name && !link && !cena) tr.remove(); + }); + reindexHidden(); + }); + + // startowa normalizacja nazw hiddenów (ważne w trybie edycji) + reindexHidden(); +})(); diff --git a/static/js/transakcje.js b/static/js/transakcje.js new file mode 100644 index 0000000..7faa7e7 --- /dev/null +++ b/static/js/transakcje.js @@ -0,0 +1,26 @@ +document.addEventListener('DOMContentLoaded', function () { + const modalW = new bootstrap.Modal(document.getElementById('modalWplata')); + const modalX = new bootstrap.Modal(document.getElementById('modalWydatek')); + + // WPŁATA + document.querySelectorAll('.btn-edit-wplata').forEach(btn => { + btn.addEventListener('click', () => { + const form = document.getElementById('formWplata'); + form.action = btn.dataset.action; + document.getElementById('wplataKwota').value = btn.dataset.kwota || ''; + document.getElementById('wplataOpis').value = btn.dataset.opis || ''; + modalW.show(); + }); + }); + + // WYDATEK + document.querySelectorAll('.btn-edit-wydatek').forEach(btn => { + btn.addEventListener('click', () => { + const form = document.getElementById('formWydatek'); + form.action = btn.dataset.action; + document.getElementById('wydatekKwota').value = btn.dataset.kwota || ''; + document.getElementById('wydatekOpis').value = btn.dataset.opis || ''; + modalX.show(); + }); + }); +}); \ No newline at end of file diff --git a/templates/admin/dashboard.html b/templates/admin/dashboard.html index bf4c473..0af8352 100644 --- a/templates/admin/dashboard.html +++ b/templates/admin/dashboard.html @@ -95,6 +95,10 @@ href="{{ url_for('dodaj_wydatek', zbiorka_id=z.id) }}">Dodaj wydatek +
  • + Transakcje +
  • Edytuj stan @@ -147,7 +151,7 @@ {% endif %} - +
    @@ -208,6 +212,10 @@ href="{{ url_for('dodaj_wydatek', zbiorka_id=z.id) }}">Dodaj wydatek
  • +
  • + Transakcje +
  • Edytuj stan diff --git a/templates/admin/formularz_zbiorek.html b/templates/admin/formularz_zbiorek.html index 87afbf0..3baaa42 100644 --- a/templates/admin/formularz_zbiorek.html +++ b/templates/admin/formularz_zbiorek.html @@ -80,6 +80,98 @@
    + +
    +
    Lista produktów
    +

    + Wypunktuj dokładnie produkty do zakupu — podaj nazwę, opcjonalny link do sklepu i cenę. + Status domyślnie Do kupienia; przełącz na Kupione po realizacji. +

    + +
    + + + + + + + + + + + + {% set items = zbiorka.przedmioty if is_edit and zbiorka and zbiorka.przedmioty else [] + %} + {% if items %} + {% for it in items %} + {% set i = loop.index0 %} + + + + + + + + {% endfor %} + {% else %} + + + + + + + + + {% endif %} + +
    ProduktLink do sklepuCena [PLN]Status
    + + + + + + +
    + + + +
    +
    + +
    +
    + + + +
    +
    + +
    +
    + +
    + + +
    +
    + +
    +
    Dane płatności
    @@ -93,9 +185,8 @@ placeholder="12 3456 7890 1234 5678 9012 3456" required aria-describedby="ibanHelp" value="{% if is_edit and zbiorka.numer_konta %}{{ zbiorka.numer_konta }}{% elif global_settings %}{{ global_settings.numer_konta }}{% else %}{% endif %}">
    -
    - Wpisz ciąg cyfr; spacje dodadzą się automatycznie dla czytelności. -
    +
    Wpisz ciąg cyfr; spacje dodadzą się automatycznie dla + czytelności.
    @@ -116,8 +207,9 @@ + data-blik="{{ global_settings.numer_telefonu_blik }}" {% endif %}> + Wstaw globalne ustawienia +
    {% endif %} @@ -128,6 +220,16 @@
    Cel i widoczność
    + + {# === BOX: zgodność sumy produktów z celem === #} + +
    @@ -153,10 +255,9 @@
    - + {{ ' Zaktualizuj zbiórkę' if is_edit else 'Dodaj zbiórkę' }} + Anuluj -
    @@ -169,4 +270,6 @@ + + {% endblock %} \ No newline at end of file diff --git a/templates/admin/transakcje.html b/templates/admin/transakcje.html new file mode 100644 index 0000000..2fc05f1 --- /dev/null +++ b/templates/admin/transakcje.html @@ -0,0 +1,143 @@ +{% extends 'base.html' %} +{% block title %}Transakcje – {{ zbiorka.nazwa }}{% endblock %} + +{% block content %} +
    + +
    +

    Transakcje: {{ zbiorka.nazwa }}

    + +
    + +
    +
    +
    + + + + + + + + + + + + {% for a in aktywnosci %} + + + + + + + + {% else %} + + + + {% endfor %} + +
    DataTypKwotaOpis
    {{ a.data|dt("%d.%m.%Y %H:%M") }} + {{ a.typ + }} + {{ '%.2f'|format(a.kwota) }} PLN{{ a.opis or '—' }} + {% if a.typ == 'wpłata' %} + +
    + +
    + {% else %} + +
    + +
    + {% endif %} +
    Brak transakcji.
    +
    + +
    + Aktualny stan: {{ '%.2f'|format(zbiorka.stan or 0) }} PLN +
    +
    +
    +
    + +{# === MODAL: Edycja wpłaty === #} + + +{# === MODAL: Edycja wydatku === #} + +{% endblock %} + +{% block extra_scripts %} +{{ super() }} + +{% endblock %} \ No newline at end of file diff --git a/templates/zbiorka.html b/templates/zbiorka.html index 38df64e..e1fe508 100644 --- a/templates/zbiorka.html +++ b/templates/zbiorka.html @@ -4,7 +4,7 @@ {% block content %}
    - {# Postęp 0–100 #} + {# Wyliczenia postępu finansowego #} {% set has_cel = (zbiorka.cel is defined and zbiorka.cel and zbiorka.cel > 0) %} {% set progress = (zbiorka.stan / zbiorka.cel * 100) if has_cel else 0 %} {% set progress_clamped = 100 if progress > 100 else (0 if progress < 0 else progress) %} {% set @@ -26,32 +26,138 @@
    - +
    -
    + + +
    Opis
    -
    +
    {{ zbiorka.opis | markdown }}
    - -
    Postęp
    -
    -
    -
    - - {% if zbiorka.ukryj_kwote %} - — - {% else %} - {{ progress|round(1) }}% - {% endif %} -
    + + {# Czy są produkty? #} + {% set items = zbiorka.przedmioty or [] %} + {% set has_items = (items|length > 0) %} + + + {% if has_items %} +
    +
    +
    Lista zakupów
    + + {% set posortowane = items|sort(attribute='kupione') %} +
      + {% for it in posortowane %} +
    • +
      + {% if it.kupione %} + Kupione + {% else %} + Do kupienia + {% endif %} + {{ it.nazwa }} + {% if it.link %} + Sklep ↗ + {% endif %} +
      +
      + {% if not zbiorka.ukryj_kwote %} + {% if it.cena is not none %} + + {{ it.cena|round(2) }} PLN + + {% else %} + + {% endif %} + {% else %} + + {% endif %} +
      +
    • + {% endfor %} +
    +
    +
    + {% endif %} + + + {# Dodatkowe wyliczenia do postępu zakupów #} + {% set total_cnt = items|length %} + {% set kupione_cnt = (items|selectattr('kupione')|list|length) %} + {% set items_pct = (kupione_cnt / total_cnt * 100) if total_cnt > 0 else 0 %} + {% if not zbiorka.ukryj_kwote %} + {% set suma_all = (items|selectattr('cena')|map(attribute='cena')|sum) or 0 %} + {% set suma_kupione = (items|selectattr('kupione')|selectattr('cena')|map(attribute='cena')|sum) or 0 %} + {% set suma_pct = (suma_kupione / suma_all * 100) if suma_all > 0 else 0 %} + {% endif %} + +
    +
    +
    +
    Postęp
    +
    + {% if has_cel and not zbiorka.ukryj_kwote %} + + Finanse: {{ zbiorka.stan|round(2) }} / {{ zbiorka.cel|round(2) }} PLN + + {% endif %} + {% if has_items %} + Pozycje: {{ kupione_cnt }}/{{ total_cnt }} + {% if not zbiorka.ukryj_kwote and (suma_all or 0) > 0 %} + Zakupy (kwotowo): + {{ (suma_kupione or 0)|round(2) }} / {{ (suma_all or 0)|round(2) }} PLN + + {% endif %} + {% endif %} +
    +
    + + +
    + Finanse +
    +
    +
    + + {% if zbiorka.ukryj_kwote %}—{% else %}{{ progress|round(1) }}%{% endif %} + +
    + + {% if has_items %} + +
    + Zakupy (liczba pozycji) +
    +
    +
    + {{ items_pct|round(1) }}% +
    + + {% if not zbiorka.ukryj_kwote and (suma_all or 0) > 0 %} + +
    + Zakupy (kwotowo) +
    +
    +
    + {{ suma_pct|round(1) }}% +
    + {% endif %} + {% endif %} +
    +
    +
    - +
    @@ -63,7 +169,6 @@
    {{ zbiorka.numer_konta }}
    -
    Telefon BLIK @@ -109,10 +214,10 @@
    - +
    -
    Aktywność
    +
    Aktywność / Transakcje
    {% if aktywnosci and aktywnosci|length > 0 %} Łącznie pozycji: {{ aktywnosci|length }} {% endif %} @@ -123,7 +228,7 @@ {% for a in aktywnosci %}
  • - {{ a.data.strftime('%Y-%m-%d %H:%M:%S') }} + {{ a.data|dt("%d.%m.%Y %H:%M") }} {{ a.typ|capitalize }}