listy, i inne funkcje
This commit is contained in:
268
app.py
268
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/<int:zbiorka_id>/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/<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")
|
||||
def favicon():
|
||||
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
|
||||
wydatek</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item"
|
||||
href="{{ url_for('transakcje_zbiorki', zbiorka_id=z.id) }}">Transakcje</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item"
|
||||
href="{{ url_for('edytuj_stan', zbiorka_id=z.id) }}">Edytuj stan</a>
|
||||
@@ -147,7 +151,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- PANE: Zrealizowane -->
|
||||
<!-- PANEL: Zrealizowane -->
|
||||
<div class="tab-pane fade" id="pane-zrealizowane" role="tabpanel" aria-labelledby="tab-zrealizowane"
|
||||
tabindex="0">
|
||||
|
||||
@@ -208,6 +212,10 @@
|
||||
href="{{ url_for('dodaj_wydatek', zbiorka_id=z.id) }}">Dodaj
|
||||
wydatek</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item"
|
||||
href="{{ url_for('transakcje_zbiorki', zbiorka_id=z.id) }}">Transakcje</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item"
|
||||
href="{{ url_for('edytuj_stan', zbiorka_id=z.id) }}">Edytuj stan</a>
|
||||
|
@@ -80,6 +80,98 @@
|
||||
|
||||
<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 -->
|
||||
<div class="mb-4">
|
||||
<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"
|
||||
value="{% if is_edit and zbiorka.numer_konta %}{{ zbiorka.numer_konta }}{% elif global_settings %}{{ global_settings.numer_konta }}{% else %}{% endif %}">
|
||||
</div>
|
||||
<div id="ibanHelp" class="form-text">
|
||||
Wpisz ciąg cyfr; spacje dodadzą się automatycznie dla czytelności.
|
||||
</div>
|
||||
<div id="ibanHelp" class="form-text">Wpisz ciąg cyfr; spacje dodadzą się automatycznie dla
|
||||
czytelności.</div>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
title="Wstaw wartości z ustawień globalnych" {% if global_settings %}
|
||||
data-iban="{{ global_settings.numer_konta }}"
|
||||
data-blik="{{ global_settings.numer_telefonu_blik }}" {% endif %}>Wstaw
|
||||
globalne ustawienia</button>
|
||||
data-blik="{{ global_settings.numer_telefonu_blik }}" {% endif %}>
|
||||
Wstaw globalne ustawienia
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -128,6 +220,16 @@
|
||||
<!-- SEKCJA: Cel i widoczność -->
|
||||
<div class="mb-4">
|
||||
<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="col-12 col-md-6">
|
||||
<label for="cel" class="form-label">Cel zbiórki</label>
|
||||
@@ -153,10 +255,9 @@
|
||||
<!-- CTA -->
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<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>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
@@ -169,4 +270,6 @@
|
||||
<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/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 %}
|
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 %}
|
||||
<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 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 @@
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<!-- Kolumna: opis + progress -->
|
||||
<!-- Kolumna lewa: Opis + (opcjonalnie) Lista zakupów + Postęp -->
|
||||
<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">
|
||||
<h5 class="mb-2">Opis</h5>
|
||||
<div class="mb-4">
|
||||
<div class="mb-0">
|
||||
{{ zbiorka.opis | markdown }}
|
||||
</div>
|
||||
</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">
|
||||
{# 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 %}
|
||||
{% 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>
|
||||
|
||||
<!-- Kolumna: płatności (sticky) -->
|
||||
</div>
|
||||
|
||||
<!-- Kolumna prawa: płatności (sticky) -->
|
||||
<div class="col-md-4">
|
||||
<div class="card shadow-sm wspomoz-card position-sticky" style="top: 1rem;">
|
||||
<div class="card-body">
|
||||
@@ -63,7 +169,6 @@
|
||||
</div>
|
||||
<div class="fs-5" id="ibanDisplay">{{ zbiorka.numer_konta }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<strong>Telefon BLIK</strong>
|
||||
@@ -109,10 +214,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aktywność (wpłaty + wydatki) -->
|
||||
<!-- Aktywność -->
|
||||
<div class="card shadow-sm mt-4">
|
||||
<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 %}
|
||||
<small class="text-muted">Łącznie pozycji: {{ aktywnosci|length }}</small>
|
||||
{% endif %}
|
||||
@@ -123,7 +228,7 @@
|
||||
{% for a in aktywnosci %}
|
||||
<li class="list-group-item bg-transparent d-flex flex-wrap justify-content-between align-items-center">
|
||||
<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">
|
||||
{{ a.typ|capitalize }}
|
||||
</span>
|
||||
|
Reference in New Issue
Block a user