listy, i inne funkcje
This commit is contained in:
268
app.py
268
app.py
@@ -9,9 +9,9 @@ from flask_login import (
|
|||||||
UserMixin,
|
UserMixin,
|
||||||
)
|
)
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
from sqlalchemy import event
|
from sqlalchemy import event, Numeric
|
||||||
from sqlalchemy.engine import Engine
|
from sqlalchemy.engine import Engine
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
import markdown as md
|
import markdown as md
|
||||||
@@ -28,9 +28,16 @@ db = SQLAlchemy(app)
|
|||||||
login_manager = LoginManager(app)
|
login_manager = LoginManager(app)
|
||||||
login_manager.login_view = "zaloguj"
|
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
|
# MODELE
|
||||||
|
|
||||||
|
|
||||||
class User(UserMixin, db.Model):
|
class User(UserMixin, db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
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)
|
opis = db.Column(db.Text, nullable=False)
|
||||||
numer_konta = db.Column(db.String(50), nullable=False)
|
numer_konta = db.Column(db.String(50), nullable=False)
|
||||||
numer_telefonu_blik = 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)
|
cel = db.Column(Numeric(12, 2), nullable=False, default=0)
|
||||||
stan = db.Column(db.Float, default=0.0)
|
stan = db.Column(Numeric(12, 2), default=0)
|
||||||
ukryta = db.Column(db.Boolean, default=False)
|
ukryta = db.Column(db.Boolean, default=False)
|
||||||
ukryj_kwote = db.Column(db.Boolean, default=False)
|
ukryj_kwote = db.Column(db.Boolean, default=False)
|
||||||
zrealizowana = db.Column(db.Boolean, default=False)
|
zrealizowana = db.Column(db.Boolean, default=False)
|
||||||
@@ -74,6 +81,26 @@ class Zbiorka(db.Model):
|
|||||||
passive_deletes=True,
|
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):
|
class Wplata(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
@@ -82,7 +109,7 @@ class Wplata(db.Model):
|
|||||||
db.ForeignKey("zbiorka.id", ondelete="CASCADE"),
|
db.ForeignKey("zbiorka.id", ondelete="CASCADE"),
|
||||||
nullable=False,
|
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)
|
data = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
opis = db.Column(db.Text, nullable=True)
|
opis = db.Column(db.Text, nullable=True)
|
||||||
|
|
||||||
@@ -96,7 +123,7 @@ class Wydatek(db.Model):
|
|||||||
db.ForeignKey("zbiorka.id", ondelete="CASCADE"),
|
db.ForeignKey("zbiorka.id", ondelete="CASCADE"),
|
||||||
nullable=False,
|
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)
|
data = db.Column(db.DateTime, default=datetime.utcnow)
|
||||||
opis = db.Column(db.Text, nullable=True)
|
opis = db.Column(db.Text, nullable=True)
|
||||||
|
|
||||||
@@ -109,7 +136,6 @@ class GlobalSettings(db.Model):
|
|||||||
logo_url = db.Column(db.String(255), nullable=True)
|
logo_url = db.Column(db.String(255), nullable=True)
|
||||||
site_title = db.Column(db.String(120), 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)
|
||||||
show_logo_in_navbar = db.Column(db.Boolean, default=False)
|
|
||||||
navbar_brand_mode = db.Column(db.String(10), default="text")
|
navbar_brand_mode = db.Column(db.String(10), default="text")
|
||||||
footer_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)
|
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
|
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")
|
@app.template_filter("markdown")
|
||||||
def markdown_filter(text):
|
def markdown_filter(text):
|
||||||
return Markup(md.markdown(text))
|
return Markup(md.markdown(text))
|
||||||
@@ -200,7 +242,7 @@ def index():
|
|||||||
return render_template("index.html", zbiorki=zbiorki)
|
return render_template("index.html", zbiorki=zbiorki)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/zbiorki_zrealizowane")
|
@app.route("/zrealizowane")
|
||||||
def zbiorki_zrealizowane():
|
def zbiorki_zrealizowane():
|
||||||
zbiorki = Zbiorka.query.filter_by(zrealizowana=True).all()
|
zbiorki = Zbiorka.query.filter_by(zrealizowana=True).all()
|
||||||
return render_template("index.html", zbiorki=zbiorki)
|
return render_template("index.html", zbiorki=zbiorki)
|
||||||
@@ -297,8 +339,6 @@ def zarejestruj():
|
|||||||
|
|
||||||
|
|
||||||
# PANEL ADMINISTRACYJNY
|
# PANEL ADMINISTRACYJNY
|
||||||
|
|
||||||
|
|
||||||
@app.route("/admin")
|
@app.route("/admin")
|
||||||
@login_required
|
@login_required
|
||||||
def admin_dashboard():
|
def admin_dashboard():
|
||||||
@@ -336,7 +376,7 @@ def formularz_zbiorek(zbiorka_id=None):
|
|||||||
numer_konta = request.form.get("numer_konta", "").strip()
|
numer_konta = request.form.get("numer_konta", "").strip()
|
||||||
numer_telefonu_blik = request.form.get("numer_telefonu_blik", "").strip()
|
numer_telefonu_blik = request.form.get("numer_telefonu_blik", "").strip()
|
||||||
|
|
||||||
# Cel — walidacja liczby
|
# Cel — walidacja liczby (Decimal, nie float)
|
||||||
try:
|
try:
|
||||||
cel_str = request.form.get("cel", "").replace(",", ".").strip()
|
cel_str = request.form.get("cel", "").replace(",", ".").strip()
|
||||||
cel = Decimal(cel_str)
|
cel = Decimal(cel_str)
|
||||||
@@ -361,27 +401,85 @@ def formularz_zbiorek(zbiorka_id=None):
|
|||||||
|
|
||||||
ukryj_kwote = "ukryj_kwote" in request.form
|
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:
|
if is_edit:
|
||||||
# Aktualizacja istniejącej
|
# Aktualizacja istniejącej zbiórki
|
||||||
zb.nazwa = nazwa
|
zb.nazwa = nazwa
|
||||||
zb.opis = opis
|
zb.opis = opis
|
||||||
zb.numer_konta = numer_konta
|
zb.numer_konta = numer_konta
|
||||||
zb.numer_telefonu_blik = numer_telefonu_blik
|
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
|
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()
|
db.session.commit()
|
||||||
flash("Zbiórka została zaktualizowana", "success")
|
flash("Zbiórka została zaktualizowana", "success")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# Utworzenie nowej
|
# Utworzenie nowej zbiórki
|
||||||
nowa = Zbiorka(
|
nowa = Zbiorka(
|
||||||
nazwa=nazwa,
|
nazwa=nazwa,
|
||||||
opis=opis,
|
opis=opis,
|
||||||
numer_konta=numer_konta,
|
numer_konta=numer_konta,
|
||||||
numer_telefonu_blik=numer_telefonu_blik,
|
numer_telefonu_blik=numer_telefonu_blik,
|
||||||
cel=float(cel),
|
cel=cel, # ❗ Decimal
|
||||||
ukryj_kwote=ukryj_kwote,
|
ukryj_kwote=ukryj_kwote,
|
||||||
)
|
)
|
||||||
db.session.add(nowa)
|
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()
|
db.session.commit()
|
||||||
flash("Zbiórka została dodana", "success")
|
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/<int:zbiorka_id>/wplata/dodaj", methods=["GET", "POST"])
|
@app.route("/admin/zbiorka/<int:zbiorka_id>/wplata/dodaj", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def dodaj_wplate(zbiorka_id):
|
def dodaj_wplate(zbiorka_id):
|
||||||
if not current_user.is_admin:
|
if not current_user.is_admin:
|
||||||
flash("Brak uprawnień", "danger")
|
flash("Brak uprawnień", "danger")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
||||||
|
|
||||||
if request.method == "POST":
|
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", "")
|
opis = request.form.get("opis", "")
|
||||||
nowa_wplata = Wplata(zbiorka_id=zb.id, kwota=kwota, opis=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.add(nowa_wplata)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("Wpłata została dodana", "success")
|
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)
|
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)
|
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
try:
|
try:
|
||||||
nowy_stan = float(request.form["stan"])
|
nowy_stan = Decimal(request.form.get("stan", "").replace(",", "."))
|
||||||
except ValueError:
|
except (InvalidOperation, ValueError):
|
||||||
flash("Nieprawidłowa wartość kwoty", "danger")
|
flash("Nieprawidłowa wartość kwoty", "danger")
|
||||||
return redirect(url_for("edytuj_stan", zbiorka_id=zbiorka_id))
|
return redirect(url_for("edytuj_stan", zbiorka_id=zbiorka_id))
|
||||||
zb.stan = nowy_stan
|
zb.stan = nowy_stan
|
||||||
@@ -469,8 +576,6 @@ def create_admin_account():
|
|||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
from flask import request
|
|
||||||
|
|
||||||
|
|
||||||
@app.after_request
|
@app.after_request
|
||||||
def apply_headers(response):
|
def apply_headers(response):
|
||||||
@@ -588,22 +693,28 @@ def dodaj_wydatek(zbiorka_id):
|
|||||||
if not current_user.is_admin:
|
if not current_user.is_admin:
|
||||||
flash("Brak uprawnień", "danger")
|
flash("Brak uprawnień", "danger")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
try:
|
try:
|
||||||
kwota = float(request.form["kwota"])
|
kwota = Decimal(request.form.get("kwota", "").replace(",", "."))
|
||||||
if kwota <= 0:
|
if kwota <= 0:
|
||||||
raise ValueError
|
raise InvalidOperation
|
||||||
except (KeyError, ValueError):
|
except (InvalidOperation, ValueError):
|
||||||
flash("Nieprawidłowa kwota", "danger")
|
flash("Nieprawidłowa kwota (musi być > 0)", "danger")
|
||||||
return redirect(url_for("dodaj_wydatek", zbiorka_id=zbiorka_id))
|
return redirect(url_for("dodaj_wydatek", zbiorka_id=zbiorka_id))
|
||||||
|
|
||||||
opis = request.form.get("opis", "")
|
opis = request.form.get("opis", "")
|
||||||
nowy_wydatek = Wydatek(zbiorka_id=zb.id, kwota=kwota, opis=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.add(nowy_wydatek)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("Wydatek został dodany", "success")
|
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)
|
return render_template("admin/dodaj_wydatek.html", zbiorka=zb)
|
||||||
|
|
||||||
|
|
||||||
@@ -646,6 +757,97 @@ def robots():
|
|||||||
return robots_txt, 200, {"Content-Type": "text/plain"}
|
return robots_txt, 200, {"Content-Type": "text/plain"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/admin/zbiorka/<int:zbiorka_id>/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/<int:wplata_id>/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/<int:wplata_id>/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/<int:wydatek_id>/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/<int:wydatek_id>/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")
|
@app.route("/favicon.ico")
|
||||||
def favicon():
|
def favicon():
|
||||||
return "", 204
|
return "", 204
|
||||||
|
153
static/js/kwoty_formularz.js
Normal file
153
static/js/kwoty_formularz.js
Normal file
@@ -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 = `
|
||||||
|
<div>Wszystkie: <strong>${formatPln(sumAll)} PLN</strong> ·
|
||||||
|
Do kupienia: <strong>${formatPln(sumToBuy)} PLN</strong></div>
|
||||||
|
<div class="mt-1">Możesz ustawić <strong>cel</strong> na kwotę do kupienia.</div>
|
||||||
|
`;
|
||||||
|
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 <em>do kupienia</em> (<strong>${formatPln(target)} PLN</strong>) jest równa celowi.
|
||||||
|
<div class="small text-muted mt-1">Wszystkie: ${formatPln(sumAll)} PLN · Do kupienia: ${formatPln(sumToBuy)} PLN</div>
|
||||||
|
`;
|
||||||
|
btn.classList.add('d-none');
|
||||||
|
} else {
|
||||||
|
box.classList.remove('d-none');
|
||||||
|
box.classList.remove('alert-success');
|
||||||
|
box.classList.add('alert-info');
|
||||||
|
msg.innerHTML = `
|
||||||
|
<div>Wszystkie: <strong>${formatPln(sumAll)} PLN</strong> ·
|
||||||
|
Do kupienia: <strong>${formatPln(sumToBuy)} PLN</strong></div>
|
||||||
|
<div class="mt-1">Cel: <strong>${formatPln(cel)} PLN</strong></div>
|
||||||
|
`;
|
||||||
|
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();
|
||||||
|
})();
|
73
static/js/produkty_formularz.js
Normal file
73
static/js/produkty_formularz.js
Normal file
@@ -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 = `
|
||||||
|
<td><input type="text" class="form-control" name="item_nazwa[]" placeholder="np. Karma Brit 10kg" required></td>
|
||||||
|
<td><input type="url" class="form-control" name="item_link[]" placeholder="https://..."></td>
|
||||||
|
<td><input type="text" inputmode="decimal" class="form-control text-end" name="item_cena[]" placeholder="0,00"></td>
|
||||||
|
<td>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input kupione-switch" type="checkbox">
|
||||||
|
<input type="hidden" name="item_kupione_val_TMP" value="0">
|
||||||
|
<label class="form-check-label small">Do kupienia</label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-light border remove-row" title="Usuń wiersz">✕</button>
|
||||||
|
</td>`;
|
||||||
|
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();
|
||||||
|
})();
|
26
static/js/transakcje.js
Normal file
26
static/js/transakcje.js
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -95,6 +95,10 @@
|
|||||||
href="{{ url_for('dodaj_wydatek', zbiorka_id=z.id) }}">Dodaj
|
href="{{ url_for('dodaj_wydatek', zbiorka_id=z.id) }}">Dodaj
|
||||||
wydatek</a>
|
wydatek</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item"
|
||||||
|
href="{{ url_for('transakcje_zbiorki', zbiorka_id=z.id) }}">Transakcje</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item"
|
<a class="dropdown-item"
|
||||||
href="{{ url_for('edytuj_stan', zbiorka_id=z.id) }}">Edytuj stan</a>
|
href="{{ url_for('edytuj_stan', zbiorka_id=z.id) }}">Edytuj stan</a>
|
||||||
@@ -147,7 +151,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- PANE: Zrealizowane -->
|
<!-- PANEL: Zrealizowane -->
|
||||||
<div class="tab-pane fade" id="pane-zrealizowane" role="tabpanel" aria-labelledby="tab-zrealizowane"
|
<div class="tab-pane fade" id="pane-zrealizowane" role="tabpanel" aria-labelledby="tab-zrealizowane"
|
||||||
tabindex="0">
|
tabindex="0">
|
||||||
|
|
||||||
@@ -208,6 +212,10 @@
|
|||||||
href="{{ url_for('dodaj_wydatek', zbiorka_id=z.id) }}">Dodaj
|
href="{{ url_for('dodaj_wydatek', zbiorka_id=z.id) }}">Dodaj
|
||||||
wydatek</a>
|
wydatek</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item"
|
||||||
|
href="{{ url_for('transakcje_zbiorki', zbiorka_id=z.id) }}">Transakcje</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item"
|
<a class="dropdown-item"
|
||||||
href="{{ url_for('edytuj_stan', zbiorka_id=z.id) }}">Edytuj stan</a>
|
href="{{ url_for('edytuj_stan', zbiorka_id=z.id) }}">Edytuj stan</a>
|
||||||
|
@@ -80,6 +80,98 @@
|
|||||||
|
|
||||||
<hr class="my-4" />
|
<hr class="my-4" />
|
||||||
|
|
||||||
|
<!-- SEKCJA: Lista produktów -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="text-muted mb-2">Lista produktów</h6>
|
||||||
|
<p class="text-muted small mb-3">
|
||||||
|
Wypunktuj dokładnie produkty do zakupu — podaj nazwę, opcjonalny link do sklepu i cenę.
|
||||||
|
Status domyślnie <em>Do kupienia</em>; przełącz na <em>Kupione</em> po realizacji.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table align-middle" id="produkty-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="min-width:220px;">Produkt</th>
|
||||||
|
<th style="min-width:240px;">Link do sklepu</th>
|
||||||
|
<th style="width:140px;">Cena [PLN]</th>
|
||||||
|
<th style="width:160px;">Status</th>
|
||||||
|
<th style="width:60px;"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="produkty-body">
|
||||||
|
{% set items = zbiorka.przedmioty if is_edit and zbiorka and zbiorka.przedmioty else []
|
||||||
|
%}
|
||||||
|
{% if items %}
|
||||||
|
{% for it in items %}
|
||||||
|
{% set i = loop.index0 %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<input type="text" class="form-control" name="item_nazwa[]"
|
||||||
|
value="{{ it.nazwa }}" placeholder="np. Karma Brit 10kg" required>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="url" class="form-control" name="item_link[]"
|
||||||
|
value="{{ it.link or '' }}" placeholder="https://...">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<input type="text" inputmode="decimal" class="form-control text-end"
|
||||||
|
name="item_cena[]"
|
||||||
|
value="{{ (it.cena|round(2)) if it.cena is not none else '' }}"
|
||||||
|
placeholder="0,00">
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input kupione-switch" type="checkbox" {% if
|
||||||
|
it.kupione %}checked{% endif %}>
|
||||||
|
<input type="hidden" name="item_kupione_val_{{ i }}"
|
||||||
|
value="{{ 1 if it.kupione else 0 }}">
|
||||||
|
<label class="form-check-label small">{{ 'Kupione' if it.kupione else 'Do
|
||||||
|
kupienia' }}</label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-light border remove-row"
|
||||||
|
title="Usuń wiersz">✕</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<!-- pusty wiersz startowy -->
|
||||||
|
<tr>
|
||||||
|
<td><input type="text" class="form-control" name="item_nazwa[]"
|
||||||
|
placeholder="np. Karma Brit 10kg" required></td>
|
||||||
|
<td><input type="url" class="form-control" name="item_link[]"
|
||||||
|
placeholder="https://..."></td>
|
||||||
|
<td><input type="text" inputmode="decimal" class="form-control text-end"
|
||||||
|
name="item_cena[]" placeholder="0,00"></td>
|
||||||
|
<td>
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input kupione-switch" type="checkbox">
|
||||||
|
<input type="hidden" name="item_kupione_val_0" value="0">
|
||||||
|
<label class="form-check-label small">Do kupienia</label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-light border remove-row"
|
||||||
|
title="Usuń wiersz">✕</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-light border" id="add-row">+ Dodaj
|
||||||
|
pozycję</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-light border" id="clear-empty">Usuń puste
|
||||||
|
wiersze</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4" />
|
||||||
|
|
||||||
<!-- SEKCJA: Dane płatności -->
|
<!-- SEKCJA: Dane płatności -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h6 class="text-muted mb-2">Dane płatności</h6>
|
<h6 class="text-muted mb-2">Dane płatności</h6>
|
||||||
@@ -93,9 +185,8 @@
|
|||||||
placeholder="12 3456 7890 1234 5678 9012 3456" required aria-describedby="ibanHelp"
|
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 %}">
|
value="{% if is_edit and zbiorka.numer_konta %}{{ zbiorka.numer_konta }}{% elif global_settings %}{{ global_settings.numer_konta }}{% else %}{% endif %}">
|
||||||
</div>
|
</div>
|
||||||
<div id="ibanHelp" class="form-text">
|
<div id="ibanHelp" class="form-text">Wpisz ciąg cyfr; spacje dodadzą się automatycznie dla
|
||||||
Wpisz ciąg cyfr; spacje dodadzą się automatycznie dla czytelności.
|
czytelności.</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
@@ -116,8 +207,9 @@
|
|||||||
<button type="button" class="btn btn-sm btn-outline-light border" id="ustaw-globalne"
|
<button type="button" class="btn btn-sm btn-outline-light border" id="ustaw-globalne"
|
||||||
title="Wstaw wartości z ustawień globalnych" {% if global_settings %}
|
title="Wstaw wartości z ustawień globalnych" {% if global_settings %}
|
||||||
data-iban="{{ global_settings.numer_konta }}"
|
data-iban="{{ global_settings.numer_konta }}"
|
||||||
data-blik="{{ global_settings.numer_telefonu_blik }}" {% endif %}>Wstaw
|
data-blik="{{ global_settings.numer_telefonu_blik }}" {% endif %}>
|
||||||
globalne ustawienia</button>
|
Wstaw globalne ustawienia
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -128,6 +220,16 @@
|
|||||||
<!-- SEKCJA: Cel i widoczność -->
|
<!-- SEKCJA: Cel i widoczność -->
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h6 class="text-muted mb-2">Cel i widoczność</h6>
|
<h6 class="text-muted mb-2">Cel i widoczność</h6>
|
||||||
|
|
||||||
|
{# === BOX: zgodność sumy produktów z celem === #}
|
||||||
|
<div id="celSyncBox" class="alert d-none py-2 px-3 mb-3" role="alert">
|
||||||
|
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||||
|
<div id="celSyncMsg" class="small"></div>
|
||||||
|
<button type="button" id="btnApplyCelFromSum"
|
||||||
|
class="btn btn-sm btn-outline-light border d-none"></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<label for="cel" class="form-label">Cel zbiórki</label>
|
<label for="cel" class="form-label">Cel zbiórki</label>
|
||||||
@@ -153,10 +255,9 @@
|
|||||||
<!-- CTA -->
|
<!-- CTA -->
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
<button type="submit" class="btn btn-success">
|
<button type="submit" class="btn btn-success">
|
||||||
{{ ' Zaktualizuj zbiórkę' if is_edit else 'Dodaj zbiórkę' }} </button>
|
{{ ' Zaktualizuj zbiórkę' if is_edit else 'Dodaj zbiórkę' }}
|
||||||
|
</button>
|
||||||
<a href="{{ url_for('admin_dashboard') }}" class="btn btn-outline-light border">Anuluj</a>
|
<a href="{{ url_for('admin_dashboard') }}" class="btn btn-outline-light border">Anuluj</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,4 +270,6 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
|
||||||
<script src="{{ url_for('static', filename='js/mde_custom.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/mde_custom.js') }}"></script>
|
||||||
<script src="{{ url_for('static', filename='js/formularz_zbiorek.js') }}"></script>
|
<script src="{{ url_for('static', filename='js/formularz_zbiorek.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/produkty_formularz.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/kwoty_formularz.js') }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
143
templates/admin/transakcje.html
Normal file
143
templates/admin/transakcje.html
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}Transakcje – {{ zbiorka.nazwa }}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container my-4">
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h3 class="mb-0">Transakcje: {{ zbiorka.nazwa }}</h3>
|
||||||
|
<div class="btn-group">
|
||||||
|
<a class="btn btn-sm btn-outline-light border" href="{{ url_for('dodaj_wplate', zbiorka_id=zbiorka.id) }}">+
|
||||||
|
Wpłata</a>
|
||||||
|
<a class="btn btn-sm btn-outline-light border"
|
||||||
|
href="{{ url_for('dodaj_wydatek', zbiorka_id=zbiorka.id) }}">+ Wydatek</a>
|
||||||
|
<a class="btn btn-sm btn-outline-light border"
|
||||||
|
href="{{ url_for('zbiorka', zbiorka_id=zbiorka.id) }}">Szczegóły zbiórki</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Data</th>
|
||||||
|
<th>Typ</th>
|
||||||
|
<th class="text-end">Kwota</th>
|
||||||
|
<th>Opis</th>
|
||||||
|
<th class="text-end"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for a in aktywnosci %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ a.data|dt("%d.%m.%Y %H:%M") }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge {{ 'bg-success' if a.typ=='wpłata' else 'bg-danger' }}">{{ a.typ
|
||||||
|
}}</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">{{ '%.2f'|format(a.kwota) }} PLN</td>
|
||||||
|
<td class="text-muted">{{ a.opis or '—' }}</td>
|
||||||
|
<td class="text-end">
|
||||||
|
{% if a.typ == 'wpłata' %}
|
||||||
|
<button class="btn btn-sm btn-outline-light border btn-edit-wplata" data-id="{{ a.id }}"
|
||||||
|
data-kwota="{{ '%.2f'|format(a.kwota) }}" data-opis="{{ a.opis|e if a.opis }}"
|
||||||
|
data-action="{{ url_for('zapisz_wplate', wplata_id=a.id) }}">
|
||||||
|
Edytuj
|
||||||
|
</button>
|
||||||
|
<form class="d-inline" method="post"
|
||||||
|
action="{{ url_for('usun_wplate', wplata_id=a.id) }}"
|
||||||
|
onsubmit="return confirm('Usunąć wpłatę? Cofnie to wpływ na stan.');">
|
||||||
|
<button class="btn btn-sm btn-outline-danger">Usuń</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<button class="btn btn-sm btn-outline-light border btn-edit-wydatek"
|
||||||
|
data-id="{{ a.id }}" data-kwota="{{ '%.2f'|format(a.kwota) }}"
|
||||||
|
data-opis="{{ a.opis|e if a.opis }}"
|
||||||
|
data-action="{{ url_for('zapisz_wydatek', wydatek_id=a.id) }}">
|
||||||
|
Edytuj
|
||||||
|
</button>
|
||||||
|
<form class="d-inline" method="post"
|
||||||
|
action="{{ url_for('usun_wydatek', wydatek_id=a.id) }}"
|
||||||
|
onsubmit="return confirm('Usunąć wydatek? Cofnie to wpływ na stan.');">
|
||||||
|
<button class="btn btn-sm btn-outline-danger">Usuń</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5" class="text-center text-muted py-4">Brak transakcji.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="small text-muted">
|
||||||
|
Aktualny stan: <strong>{{ '%.2f'|format(zbiorka.stan or 0) }} PLN</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# === MODAL: Edycja wpłaty === #}
|
||||||
|
<div class="modal fade" id="modalWplata" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<form class="modal-content" method="post" id="formWplata">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Edytuj wpłatę</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Kwota (PLN)</label>
|
||||||
|
<input class="form-control text-end" name="kwota" step="0.01" min="0.01" id="wplataKwota"
|
||||||
|
inputmode="decimal" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Opis</label>
|
||||||
|
<textarea class="form-control" name="opis" id="wplataOpis" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-success">Zapisz</button>
|
||||||
|
<button type="button" class="btn btn-outline-light border" data-bs-dismiss="modal">Anuluj</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# === MODAL: Edycja wydatku === #}
|
||||||
|
<div class="modal fade" id="modalWydatek" tabindex="-1" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<form class="modal-content" method="post" id="formWydatek">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Edytuj wydatek</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Zamknij"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Kwota (PLN)</label>
|
||||||
|
<input class="form-control text-end" name="kwota" step="0.01" min="0.01" id="wydatekKwota"
|
||||||
|
inputmode="decimal" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Opis</label>
|
||||||
|
<textarea class="form-control" name="opis" id="wydatekOpis" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-success">Zapisz</button>
|
||||||
|
<button type="button" class="btn btn-outline-light border" data-bs-dismiss="modal">Anuluj</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
|
{{ super() }}
|
||||||
|
<script src="{{ url_for('static', filename='js/transakcje.js') }}"></script>
|
||||||
|
{% endblock %}
|
@@ -4,7 +4,7 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-4">
|
<div class="container my-4">
|
||||||
|
|
||||||
{# Postęp 0–100 #}
|
{# Wyliczenia postępu finansowego #}
|
||||||
{% set has_cel = (zbiorka.cel is defined and zbiorka.cel and zbiorka.cel > 0) %}
|
{% 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 = (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
|
{% set progress_clamped = 100 if progress > 100 else (0 if progress < 0 else progress) %} {% set
|
||||||
@@ -26,32 +26,138 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-4">
|
<div class="row g-4">
|
||||||
<!-- Kolumna: opis + progress -->
|
<!-- Kolumna lewa: Opis + (opcjonalnie) Lista zakupów + Postęp -->
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="card shadow-sm h-100">
|
|
||||||
|
<!-- Card: Opis -->
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="mb-2">Opis</h5>
|
<h5 class="mb-2">Opis</h5>
|
||||||
<div class="mb-4">
|
<div class="mb-0">
|
||||||
{{ zbiorka.opis | markdown }}
|
{{ zbiorka.opis | markdown }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h5 class="mb-2">Postęp</h5>
|
|
||||||
<div class="progress mb-2" role="progressbar" aria-valuenow="{{ progress_clamped|round(2) }}"
|
|
||||||
aria-valuemin="0" aria-valuemax="100" aria-label="Postęp zbiórki {{ progress_clamped|round(0) }} procent">
|
|
||||||
<div class="progress-bar" style="width: {{ progress_clamped }}%;"></div>
|
|
||||||
</div>
|
|
||||||
<small class="text-muted">
|
|
||||||
{% if zbiorka.ukryj_kwote %}
|
|
||||||
—
|
|
||||||
{% else %}
|
|
||||||
{{ progress|round(1) }}%
|
|
||||||
{% endif %}
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{# Czy są produkty? #}
|
||||||
|
{% set items = zbiorka.przedmioty or [] %}
|
||||||
|
{% set has_items = (items|length > 0) %}
|
||||||
|
|
||||||
|
<!-- Card: Lista zakupów (tylko gdy są produkty) -->
|
||||||
|
{% if has_items %}
|
||||||
|
<div class="card shadow-sm mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="mb-2">Lista zakupów</h5>
|
||||||
|
|
||||||
|
{% set posortowane = items|sort(attribute='kupione') %}
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{% for it in posortowane %}
|
||||||
|
<li class="list-group-item bg-transparent d-flex flex-wrap justify-content-between align-items-center">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
{% if it.kupione %}
|
||||||
|
<span class="badge bg-success">Kupione</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning text-dark">Do kupienia</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="fw-semibold">{{ it.nazwa }}</span>
|
||||||
|
{% if it.link %}
|
||||||
|
<a href="{{ it.link }}" target="_blank" rel="noopener"
|
||||||
|
class="btn btn-sm btn-outline-light border ms-2">Sklep ↗</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{% if not zbiorka.ukryj_kwote %}
|
||||||
|
{% if it.cena is not none %}
|
||||||
|
<span class="badge bg-dark border" style="border-color: var(--border);">
|
||||||
|
{{ it.cena|round(2) }} PLN
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Card: Postęp (POD listą zakupów) -->
|
||||||
|
{# 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 %}
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-2">
|
||||||
|
<h5 class="mb-0">Postęp</h5>
|
||||||
|
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||||
|
{% if has_cel and not zbiorka.ukryj_kwote %}
|
||||||
|
<span class="badge bg-dark border" style="border-color: var(--border);">
|
||||||
|
Finanse: {{ zbiorka.stan|round(2) }} / {{ zbiorka.cel|round(2) }} PLN
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if has_items %}
|
||||||
|
<span class="badge bg-secondary">Pozycje: {{ kupione_cnt }}/{{ total_cnt }}</span>
|
||||||
|
{% if not zbiorka.ukryj_kwote and (suma_all or 0) > 0 %}
|
||||||
|
<span class="badge bg-secondary">Zakupy (kwotowo):
|
||||||
|
{{ (suma_kupione or 0)|round(2) }} / {{ (suma_all or 0)|round(2) }} PLN
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pasek: Finanse (zawsze) -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<small class="text-muted">Finanse</small>
|
||||||
|
<div class="progress" role="progressbar" aria-valuenow="{{ progress_clamped|round(2) }}" aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
<div class="progress-bar" style="width: {{ progress_clamped }}%;"></div>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
{% if zbiorka.ukryj_kwote %}—{% else %}{{ progress|round(1) }}%{% endif %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if has_items %}
|
||||||
|
<!-- Pasek: Zakupy sztukami -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<small class="text-muted">Zakupy (liczba pozycji)</small>
|
||||||
|
<div class="progress" role="progressbar" aria-valuenow="{{ items_pct|round(2) }}" aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
<div class="progress-bar" style="width: {{ items_pct }}%;"></div>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">{{ items_pct|round(1) }}%</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if not zbiorka.ukryj_kwote and (suma_all or 0) > 0 %}
|
||||||
|
<!-- Pasek: Zakupy kwotowo -->
|
||||||
|
<div>
|
||||||
|
<small class="text-muted">Zakupy (kwotowo)</small>
|
||||||
|
<div class="progress" role="progressbar" aria-valuenow="{{ suma_pct|round(2) }}" aria-valuemin="0"
|
||||||
|
aria-valuemax="100">
|
||||||
|
<div class="progress-bar" style="width: {{ suma_pct }}%;"></div>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">{{ suma_pct|round(1) }}%</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Kolumna: płatności (sticky) -->
|
<!-- Kolumna prawa: płatności (sticky) -->
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card shadow-sm wspomoz-card position-sticky" style="top: 1rem;">
|
<div class="card shadow-sm wspomoz-card position-sticky" style="top: 1rem;">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
@@ -63,7 +169,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="fs-5" id="ibanDisplay">{{ zbiorka.numer_konta }}</div>
|
<div class="fs-5" id="ibanDisplay">{{ zbiorka.numer_konta }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="d-flex align-items-center justify-content-between">
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
<strong>Telefon BLIK</strong>
|
<strong>Telefon BLIK</strong>
|
||||||
@@ -109,10 +214,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Aktywność (wpłaty + wydatki) -->
|
<!-- Aktywność -->
|
||||||
<div class="card shadow-sm mt-4">
|
<div class="card shadow-sm mt-4">
|
||||||
<div class="card-header d-flex align-items-center justify-content-between">
|
<div class="card-header d-flex align-items-center justify-content-between">
|
||||||
<h5 class="card-title mb-0">Aktywność</h5>
|
<h5 class="card-title mb-0">Aktywność / Transakcje</h5>
|
||||||
{% if aktywnosci and aktywnosci|length > 0 %}
|
{% if aktywnosci and aktywnosci|length > 0 %}
|
||||||
<small class="text-muted">Łącznie pozycji: {{ aktywnosci|length }}</small>
|
<small class="text-muted">Łącznie pozycji: {{ aktywnosci|length }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -123,7 +228,7 @@
|
|||||||
{% for a in aktywnosci %}
|
{% for a in aktywnosci %}
|
||||||
<li class="list-group-item bg-transparent d-flex flex-wrap justify-content-between align-items-center">
|
<li class="list-group-item bg-transparent d-flex flex-wrap justify-content-between align-items-center">
|
||||||
<div class="me-3">
|
<div class="me-3">
|
||||||
<strong>{{ a.data.strftime('%Y-%m-%d %H:%M:%S') }}</strong>
|
<strong>{{ a.data|dt("%d.%m.%Y %H:%M") }}</strong>
|
||||||
<span class="badge {% if a.typ == 'wpłata' %}bg-success{% else %}bg-danger{% endif %} ms-2">
|
<span class="badge {% if a.typ == 'wpłata' %}bg-success{% else %}bg-danger{% endif %} ms-2">
|
||||||
{{ a.typ|capitalize }}
|
{{ a.typ|capitalize }}
|
||||||
</span>
|
</span>
|
||||||
|
Reference in New Issue
Block a user