spolszczenie wszystkiego i poprawki

This commit is contained in:
Mateusz Gruszczyński
2025-09-26 13:52:06 +02:00
parent fdcfaff80e
commit 6a1734024a
14 changed files with 316 additions and 282 deletions

3
.gitignore vendored
View File

@@ -4,4 +4,5 @@ instance/
venv/ venv/
.env .env
version.txt version.txt
deploy/varnish/default.vcl deploy/varnish/default.vcl
*.tar.gz

View File

@@ -1,68 +1,18 @@
-- WŁĄCZ/wyłącz FK zależnie od etapu migracji
PRAGMA foreign_keys = OFF;
BEGIN TRANSACTION; BEGIN TRANSACTION;
-- 1) Nowa tabela z właściwym FK (ON DELETE CASCADE) -- UŻYTKOWNIK
CREATE TABLE wplata_new ( ALTER TABLE user RENAME TO uzytkownik;
id INTEGER PRIMARY KEY, ALTER TABLE uzytkownik RENAME COLUMN username TO uzytkownik;
zbiorka_id INTEGER NOT NULL, ALTER TABLE uzytkownik RENAME COLUMN password_hash TO haslo_hash;
kwota REAL NOT NULL, ALTER TABLE uzytkownik RENAME COLUMN is_admin TO czy_admin;
data DATETIME,
opis TEXT,
FOREIGN KEY(zbiorka_id) REFERENCES zbiorka(id) ON DELETE CASCADE
);
-- 2) (opcjonalnie) upewnij się, że nie ma „sierotek” -- USTAWIENIA GLOBALNE
-- SELECT w.* FROM wplata w LEFT JOIN zbiorka z ON z.id = w.zbiorka_id WHERE z.id IS NULL; ALTER TABLE global_settings RENAME TO ustawienia_globalne;
ALTER TABLE ustawienia_globalne RENAME COLUMN allowed_login_hosts TO dozwolone_hosty_logowania;
-- 3) Kopiowanie danych ALTER TABLE ustawienia_globalne RENAME COLUMN site_title TO tytul_strony;
INSERT INTO wplata_new (id, zbiorka_id, kwota, data, opis) ALTER TABLE ustawienia_globalne RENAME COLUMN show_logo_in_navbar TO pokaz_logo_w_navbar;
SELECT id, zbiorka_id, kwota, data, opis ALTER TABLE ustawienia_globalne RENAME COLUMN navbar_brand_mode TO typ_navbar;
FROM wplata; ALTER TABLE ustawienia_globalne RENAME COLUMN footer_brand_mode TO typ_stopka;
ALTER TABLE ustawienia_globalne RENAME COLUMN footer_text TO stopka_text;
-- 4) Usunięcie starej tabeli
DROP TABLE wplata;
-- 5) Zmiana nazwy nowej tabeli na właściwą
ALTER TABLE wplata_new RENAME TO wplata;
-- 6) Odtwórz indeksy/trigger-y jeśli jakieś były (przykład indeksu po FK)
-- CREATE INDEX idx_wplata_zbiorka_id ON wplata(zbiorka_id);
COMMIT; COMMIT;
PRAGMA foreign_keys = ON;
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
ALTER TABLE global_settings ADD COLUMN logo_url TEXT DEFAULT '';
ALTER TABLE global_settings ADD COLUMN site_title TEXT DEFAULT '';
ALTER TABLE global_settings ADD COLUMN show_logo_in_navbar BOOLEAN DEFAULT 0;
COMMIT;
PRAGMA foreign_keys=ON;
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
-- 1) Dodajemy nowe kolumny (SQLite pozwala tylko na ADD COLUMN)
ALTER TABLE global_settings ADD COLUMN navbar_brand_mode TEXT DEFAULT 'text';
ALTER TABLE global_settings ADD COLUMN footer_brand_mode TEXT DEFAULT 'text';
ALTER TABLE global_settings ADD COLUMN footer_text TEXT;
-- 2) Backfill: zgodność wsteczna z show_logo_in_navbar
UPDATE global_settings
SET navbar_brand_mode = 'logo'
WHERE COALESCE(show_logo_in_navbar, 0) = 1;
-- 3) Upewnij się, że wartości są ustawione (na wypadek NULL-i)
UPDATE global_settings
SET navbar_brand_mode = COALESCE(navbar_brand_mode, 'text'),
footer_brand_mode = COALESCE(footer_brand_mode, 'text');

246
app.py
View File

@@ -65,18 +65,18 @@ APP_VERSION = f"{deploy_date}+{commit}"
app.config["APP_VERSION"] = APP_VERSION app.config["APP_VERSION"] = APP_VERSION
# MODELE # MODELE
class User(UserMixin, db.Model): class Uzytkownik(UserMixin, db.Model):
__tablename__ = "uzytkownik"
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) uzytkownik = db.Column(db.String(80), unique=True, nullable=False)
password_hash = db.Column(db.String(128), nullable=False) haslo_hash = db.Column(db.String(128), nullable=False)
is_admin = db.Column(db.Boolean, default=False) # Flaga głównego administratora czy_admin = db.Column(db.Boolean, default=False)
def set_password(self, password): def set_password(self, password):
self.password_hash = generate_password_hash(password) self.haslo_hash = generate_password_hash(password)
def check_password(self, password): def check_password(self, password):
return check_password_hash(self.password_hash, password) return check_password_hash(self.haslo_hash, password)
class Zbiorka(db.Model): class Zbiorka(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
@@ -89,6 +89,9 @@ class Zbiorka(db.Model):
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)
pokaz_postep_finanse = db.Column(db.Boolean, default=True, nullable=False)
pokaz_postep_pozycje = db.Column(db.Boolean, default=True, nullable=False)
pokaz_postep_kwotowo = db.Column(db.Boolean, default=True, nullable=False)
wplaty = db.relationship( wplaty = db.relationship(
"Wplata", "Wplata",
@@ -139,10 +142,8 @@ class Wplata(db.Model):
kwota = db.Column(Numeric(12, 2), 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)
zbiorka = db.relationship("Zbiorka", back_populates="wplaty") zbiorka = db.relationship("Zbiorka", back_populates="wplaty")
class Wydatek(db.Model): class Wydatek(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
zbiorka_id = db.Column( zbiorka_id = db.Column(
@@ -155,22 +156,24 @@ class Wydatek(db.Model):
opis = db.Column(db.Text, nullable=True) opis = db.Column(db.Text, nullable=True)
class GlobalSettings(db.Model): class UstawieniaGlobalne(db.Model):
__tablename__ = "ustawienia_globalne"
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
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)
allowed_login_hosts = db.Column(db.Text, nullable=True) dozwolone_hosty_logowania = db.Column(db.Text, nullable=True)
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) tytul_strony = db.Column(db.String(120), nullable=True)
show_logo_in_navbar = db.Column(db.Boolean, default=False) pokaz_logo_w_navbar = db.Column(db.Boolean, default=False)
navbar_brand_mode = db.Column(db.String(10), default="text") typ_navbar = db.Column(db.String(10), default="text")
footer_brand_mode = db.Column(db.String(10), default="text") typ_stopka = db.Column(db.String(10), default="text")
footer_text = db.Column(db.String(200), nullable=True) stopka_text = db.Column(db.String(200), nullable=True)
@login_manager.user_loader @login_manager.user_loader
def load_user(user_id): def load_user(user_id):
return db.session.get(User, int(user_id)) return db.session.get(Uzytkownik, int(user_id))
@event.listens_for(Engine, "connect") @event.listens_for(Engine, "connect")
@@ -249,11 +252,9 @@ def markdown_filter(text):
@app.context_processor @app.context_processor
def inject_globals(): def inject_globals():
settings = GlobalSettings.query.first() settings = UstawieniaGlobalne.query.first()
allowed_hosts_str = ( allowed_hosts_str = (
settings.allowed_login_hosts settings.dozwolone_hosty_logowania if settings and settings.dozwolone_hosty_logowania else ""
if settings and settings.allowed_login_hosts
else ""
) )
client_ip = get_real_ip() client_ip = get_real_ip()
return { return {
@@ -290,7 +291,7 @@ def zbiorka(zbiorka_id):
zb = db.session.get(Zbiorka, zbiorka_id) zb = db.session.get(Zbiorka, zbiorka_id)
if zb is None: if zb is None:
abort(404) abort(404)
if zb.ukryta and (not current_user.is_authenticated or not current_user.is_admin): if zb.ukryta and (not current_user.is_authenticated or not current_user.czy_admin):
abort(404) abort(404)
# scalona oś czasu: wpłaty + wydatki # scalona oś czasu: wpłaty + wydatki
@@ -311,34 +312,22 @@ def zbiorka(zbiorka_id):
@app.route("/zaloguj", methods=["GET", "POST"]) @app.route("/zaloguj", methods=["GET", "POST"])
def zaloguj(): def zaloguj():
# Pobierz ustawienia globalne, w tym dozwolone hosty settings = UstawieniaGlobalne.query.first()
settings = GlobalSettings.query.first() allowed_hosts_str = settings.dozwolone_hosty_logowania or "" if settings else ""
allowed_hosts_str = ""
if settings and settings.allowed_login_hosts:
allowed_hosts_str = settings.allowed_login_hosts
# Sprawdzenie, czy adres IP klienta jest dozwolony
client_ip = get_real_ip() client_ip = get_real_ip()
if not is_allowed_ip(client_ip, allowed_hosts_str): if not is_allowed_ip(client_ip, allowed_hosts_str):
flash( flash("Dostęp do tego systemu jest zablokowany dla Twojego adresu IP", "danger")
"Dostęp do tego systemu jest zablokowany dla Twojego adresu IP",
"danger",
)
return redirect(url_for("index")) return redirect(url_for("index"))
if request.method == "POST": if request.method == "POST":
username = request.form["username"] login = request.form["uzytkownik"]
password = request.form["password"] password = request.form["haslo"]
user = User.query.filter_by(username=username).first() user = Uzytkownik.query.filter_by(uzytkownik=login).first()
if user and user.check_password(password): if user and user.check_password(password):
login_user(user) login_user(user)
flash("Zalogowano pomyślnie", "success") flash("Zalogowano pomyślnie", "success")
next_page = request.args.get("next") next_page = request.args.get("next")
return ( return redirect(next_page) if next_page else redirect(url_for("admin_dashboard"))
redirect(next_page)
if next_page
else redirect(url_for("admin_dashboard"))
)
else: else:
flash("Nieprawidłowe dane logowania", "danger") flash("Nieprawidłowe dane logowania", "danger")
return render_template("login.html") return render_template("login.html")
@@ -358,12 +347,12 @@ def zarejestruj():
flash("Rejestracja została wyłączona przez administratora", "danger") flash("Rejestracja została wyłączona przez administratora", "danger")
return redirect(url_for("zaloguj")) return redirect(url_for("zaloguj"))
if request.method == "POST": if request.method == "POST":
username = request.form["username"] login = request.form["uzytkownik"]
password = request.form["password"] password = request.form["haslo"]
if User.query.filter_by(username=username).first(): if Uzytkownik.query.filter_by(uzytkownik=login).first():
flash("Użytkownik już istnieje", "danger") flash("Użytkownik już istnieje", "danger")
return redirect(url_for("register")) return redirect(url_for("register"))
new_user = User(username=username) new_user = Uzytkownik(uzytkownik=login)
new_user.set_password(password) new_user.set_password(password)
db.session.add(new_user) db.session.add(new_user)
db.session.commit() db.session.commit()
@@ -376,7 +365,7 @@ def zarejestruj():
@app.route("/admin") @app.route("/admin")
@login_required @login_required
def admin_dashboard(): def admin_dashboard():
if not current_user.is_admin: if not current_user.czy_admin:
flash("Brak uprawnień do panelu administracyjnego", "danger") flash("Brak uprawnień do panelu administracyjnego", "danger")
return redirect(url_for("index")) return redirect(url_for("index"))
active_zbiorki = Zbiorka.query.filter_by(zrealizowana=False).all() active_zbiorki = Zbiorka.query.filter_by(zrealizowana=False).all()
@@ -392,30 +381,32 @@ def admin_dashboard():
@app.route("/admin/zbiorka/edytuj/<int:zbiorka_id>", methods=["GET", "POST"]) @app.route("/admin/zbiorka/edytuj/<int:zbiorka_id>", methods=["GET", "POST"])
@login_required @login_required
def formularz_zbiorek(zbiorka_id=None): def formularz_zbiorek(zbiorka_id=None):
if not current_user.is_admin: if not current_user.czy_admin:
flash("Brak uprawnień", "danger") flash("Brak uprawnień", "danger")
return redirect(url_for("index")) return redirect(url_for("index"))
# Tryb: dodawanie vs edycja
is_edit = zbiorka_id is not None is_edit = zbiorka_id is not None
# Obiekt zbiórki ładujemy TYLKO przy edycji
zb = None zb = None
if is_edit: if is_edit:
zb = db.session.get(Zbiorka, zbiorka_id) zb = db.session.get(Zbiorka, zbiorka_id)
if zb is None: if zb is None:
abort(404) abort(404)
global_settings = GlobalSettings.query.first() global_settings = UstawieniaGlobalne.query.first()
if request.method == "POST": if request.method == "POST":
# Pola wspólne # Pola wspólne
nazwa = request.form.get("nazwa", "").strip() nazwa = request.form.get("nazwa", "").strip()
opis = request.form.get("opis", "").strip() opis = request.form.get("opis", "").strip()
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()
# Widoczność kwot i poszczególnych pasków postępu
ukryj_kwote = "ukryj_kwote" in request.form
pokaz_postep_finanse = "pokaz_postep_finanse" in request.form
pokaz_postep_pozycje = "pokaz_postep_pozycje" in request.form
pokaz_postep_kwotowo = "pokaz_postep_kwotowo" in request.form
# Cel — Decimal, > 0 # Cel — Decimal, > 0
try: try:
cel_str = request.form.get("cel", "").replace(",", ".").strip() cel_str = request.form.get("cel", "").replace(",", ".").strip()
@@ -424,23 +415,38 @@ def formularz_zbiorek(zbiorka_id=None):
raise InvalidOperation raise InvalidOperation
except (InvalidOperation, ValueError): except (InvalidOperation, ValueError):
flash("Podano nieprawidłową wartość dla celu zbiórki", "danger") flash("Podano nieprawidłową wartość dla celu zbiórki", "danger")
# Utwórz tymczasowy obiekt do ponownego renderu formularza
temp_zb = (zb or Zbiorka( # Przy błędzie celu odtwórz stan formularza (również przełączniki)
nazwa=nazwa, if is_edit and zb:
opis=opis, zb.nazwa = nazwa
numer_konta=numer_konta, zb.opis = opis
numer_telefonu_blik=numer_telefonu_blik, zb.numer_konta = numer_konta
cel=None, zb.numer_telefonu_blik = numer_telefonu_blik
ukryj_kwote=("ukryj_kwote" in request.form), zb.ukryj_kwote = ukryj_kwote
)) zb.pokaz_postep_finanse = pokaz_postep_finanse
zb.pokaz_postep_pozycje = pokaz_postep_pozycje
zb.pokaz_postep_kwotowo = pokaz_postep_kwotowo
temp_zb = zb
else:
temp_zb = Zbiorka(
nazwa=nazwa,
opis=opis,
numer_konta=numer_konta,
numer_telefonu_blik=numer_telefonu_blik,
cel=None,
ukryj_kwote=ukryj_kwote,
pokaz_postep_finanse=pokaz_postep_finanse,
pokaz_postep_pozycje=pokaz_postep_pozycje,
pokaz_postep_kwotowo=pokaz_postep_kwotowo,
)
return render_template( return render_template(
"admin/formularz_zbiorek.html", "admin/formularz_zbiorek.html",
zbiorka=temp_zb, zbiorka=temp_zb,
global_settings=global_settings, global_settings=global_settings,
) )
ukryj_kwote = "ukryj_kwote" in request.form # Lista produktów
names = request.form.getlist("item_nazwa[]") names = request.form.getlist("item_nazwa[]")
links = request.form.getlist("item_link[]") links = request.form.getlist("item_link[]")
prices = request.form.getlist("item_cena[]") prices = request.form.getlist("item_cena[]")
@@ -464,11 +470,14 @@ def formularz_zbiorek(zbiorka_id=None):
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 = cel # pozostaje Decimal zb.cel = cel
zb.ukryj_kwote = ukryj_kwote zb.ukryj_kwote = ukryj_kwote
db.session.commit() # zapisz bazowe pola zb.pokaz_postep_finanse = pokaz_postep_finanse
zb.pokaz_postep_pozycje = pokaz_postep_pozycje
zb.pokaz_postep_kwotowo = pokaz_postep_kwotowo
db.session.commit() # zapisz bazowe pola
# Nadpisz listę produktów (czyść i dodaj od nowa dla prostoty) # Nadpisz listę produktów
zb.przedmioty.clear() zb.przedmioty.clear()
for i, raw_name in enumerate(names): for i, raw_name in enumerate(names):
name = (raw_name or "").strip() name = (raw_name or "").strip()
@@ -496,11 +505,14 @@ def formularz_zbiorek(zbiorka_id=None):
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=cel, # Decimal cel=cel,
ukryj_kwote=ukryj_kwote, ukryj_kwote=ukryj_kwote,
pokaz_postep_finanse=pokaz_postep_finanse,
pokaz_postep_pozycje=pokaz_postep_pozycje,
pokaz_postep_kwotowo=pokaz_postep_kwotowo,
) )
db.session.add(nowa) db.session.add(nowa)
db.session.commit() # potrzebne ID db.session.commit() # potrzebne ID
# Dodaj produkty do nowej zbiórki # Dodaj produkty do nowej zbiórki
for i, raw_name in enumerate(names): for i, raw_name in enumerate(names):
@@ -526,14 +538,16 @@ def formularz_zbiorek(zbiorka_id=None):
# GET # GET
return render_template( return render_template(
"admin/formularz_zbiorek.html", zbiorka=zb, global_settings=global_settings "admin/formularz_zbiorek.html",
zbiorka=zb,
global_settings=global_settings
) )
@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.czy_admin:
flash("Brak uprawnień", "danger") flash("Brak uprawnień", "danger")
return redirect(url_for("index")) return redirect(url_for("index"))
@@ -565,7 +579,7 @@ def dodaj_wplate(zbiorka_id):
@app.route("/admin/zbiorka/usun/<int:zbiorka_id>", methods=["POST"]) @app.route("/admin/zbiorka/usun/<int:zbiorka_id>", methods=["POST"])
@login_required @login_required
def usun_zbiorka(zbiorka_id): def usun_zbiorka(zbiorka_id):
if not current_user.is_admin: if not current_user.czy_admin:
flash("Brak uprawnień", "danger") flash("Brak uprawnień", "danger")
return redirect(url_for("index")) return redirect(url_for("index"))
zb = db.session.get(Zbiorka, zbiorka_id) zb = db.session.get(Zbiorka, zbiorka_id)
@@ -580,7 +594,7 @@ def usun_zbiorka(zbiorka_id):
@app.route("/admin/zbiorka/edytuj_stan/<int:zbiorka_id>", methods=["GET", "POST"]) @app.route("/admin/zbiorka/edytuj_stan/<int:zbiorka_id>", methods=["GET", "POST"])
@login_required @login_required
def edytuj_stan(zbiorka_id): def edytuj_stan(zbiorka_id):
if not current_user.is_admin: if not current_user.czy_admin:
flash("Brak uprawnień", "danger") flash("Brak uprawnień", "danger")
return redirect(url_for("index")) return redirect(url_for("index"))
zb = db.session.get(Zbiorka, zbiorka_id) zb = db.session.get(Zbiorka, zbiorka_id)
@@ -602,7 +616,7 @@ def edytuj_stan(zbiorka_id):
@app.route("/admin/zbiorka/zmien_widzialnosc/<int:zbiorka_id>", methods=["POST"]) @app.route("/admin/zbiorka/zmien_widzialnosc/<int:zbiorka_id>", methods=["POST"])
@login_required @login_required
def zmien_widzialnosc(zbiorka_id): def zmien_widzialnosc(zbiorka_id):
if not current_user.is_admin: if not current_user.czy_admin:
flash("Brak uprawnień", "danger") flash("Brak uprawnień", "danger")
return redirect(url_for("index")) return redirect(url_for("index"))
zb = db.session.get(Zbiorka, zbiorka_id) zb = db.session.get(Zbiorka, zbiorka_id)
@@ -615,15 +629,17 @@ def zmien_widzialnosc(zbiorka_id):
def create_admin_account(): def create_admin_account():
admin = User.query.filter_by(is_admin=True).first() admin = Uzytkownik.query.filter_by(czy_admin=True).first()
if not admin: if not admin:
main_admin = User(username=app.config["MAIN_ADMIN_USERNAME"], is_admin=True) main_admin = Uzytkownik(
uzytkownik=app.config["MAIN_ADMIN_USERNAME"],
czy_admin=True
)
main_admin.set_password(app.config["MAIN_ADMIN_PASSWORD"]) main_admin.set_password(app.config["MAIN_ADMIN_PASSWORD"])
db.session.add(main_admin) db.session.add(main_admin)
db.session.commit() db.session.commit()
@app.after_request @app.after_request
def apply_headers(response): def apply_headers(response):
if request.path.startswith("/static/"): if request.path.startswith("/static/"):
@@ -638,9 +654,9 @@ def apply_headers(response):
return response return response
path_norm = request.path.lstrip("/") path_norm = request.path.lstrip("/")
is_admin = path_norm.startswith("admin/") or path_norm == "admin" czy_admin = path_norm.startswith("admin/") or path_norm == "admin"
if is_admin: if czy_admin:
if (response.mimetype or "").startswith("text/html"): if (response.mimetype or "").startswith("text/html"):
response.headers["Cache-Control"] = "no-store, no-cache" response.headers["Cache-Control"] = "no-store, no-cache"
response.headers.pop("ETag", None) response.headers.pop("ETag", None)
@@ -667,7 +683,7 @@ def apply_headers(response):
if ( if (
app.config.get("BLOCK_BOTS", False) app.config.get("BLOCK_BOTS", False)
and not is_admin and not czy_admin
and not request.path.startswith("/static/") and not request.path.startswith("/static/")
): ):
cc_override = app.config.get("CACHE_CONTROL_HEADER") cc_override = app.config.get("CACHE_CONTROL_HEADER")
@@ -683,60 +699,58 @@ def apply_headers(response):
@app.route("/admin/ustawienia", methods=["GET", "POST"]) @app.route("/admin/ustawienia", methods=["GET", "POST"])
@login_required @login_required
def admin_ustawienia(): def admin_ustawienia():
if not current_user.is_admin: if not current_user.czy_admin:
flash("Brak uprawnień do panelu administracyjnego", "danger") flash("Brak uprawnień do panelu administracyjnego", "danger")
return redirect(url_for("index")) return redirect(url_for("index"))
client_ip = get_real_ip() client_ip = get_real_ip()
settings = GlobalSettings.query.first() settings = UstawieniaGlobalne.query.first()
if request.method == "POST": if request.method == "POST":
numer_konta = request.form.get("numer_konta") numer_konta = request.form.get("numer_konta")
numer_telefonu_blik = request.form.get("numer_telefonu_blik") numer_telefonu_blik = request.form.get("numer_telefonu_blik")
allowed_login_hosts = request.form.get("allowed_login_hosts") dozwolone_hosty_logowania = request.form.get("dozwolone_hosty_logowania")
logo_url = request.form.get("logo_url") logo_url = request.form.get("logo_url")
site_title = request.form.get("site_title") tytul_strony = request.form.get("tytul_strony")
navbar_brand_mode = request.form.get("navbar_brand_mode", "text") typ_navbar = request.form.get("typ_navbar", "text")
footer_brand_mode = request.form.get("footer_brand_mode", "text") typ_stopka = request.form.get("typ_stopka", "text")
footer_text = request.form.get("footer_text") or None stopka_text = request.form.get("stopka_text") or None
show_logo_in_navbar = navbar_brand_mode == "logo" pokaz_logo_w_navbar = (typ_navbar == "logo")
if settings is None: if settings is None:
settings = GlobalSettings( settings = UstawieniaGlobalne(
numer_konta=numer_konta, numer_konta=numer_konta,
numer_telefonu_blik=numer_telefonu_blik, numer_telefonu_blik=numer_telefonu_blik,
allowed_login_hosts=allowed_login_hosts, dozwolone_hosty_logowania=dozwolone_hosty_logowania,
logo_url=logo_url, logo_url=logo_url,
site_title=site_title, tytul_strony=tytul_strony,
show_logo_in_navbar=show_logo_in_navbar, pokaz_logo_w_navbar=pokaz_logo_w_navbar,
navbar_brand_mode=navbar_brand_mode, typ_navbar=typ_navbar,
footer_brand_mode=footer_brand_mode, typ_stopka=typ_stopka,
footer_text=footer_text, stopka_text=stopka_text,
) )
db.session.add(settings) db.session.add(settings)
else: else:
settings.numer_konta = numer_konta settings.numer_konta = numer_konta
settings.numer_telefonu_blik = numer_telefonu_blik settings.numer_telefonu_blik = numer_telefonu_blik
settings.allowed_login_hosts = allowed_login_hosts settings.dozwolone_hosty_logowania = dozwolone_hosty_logowania
settings.logo_url = logo_url settings.logo_url = logo_url
settings.site_title = site_title settings.tytul_strony = tytul_strony
settings.show_logo_in_navbar = show_logo_in_navbar settings.pokaz_logo_w_navbar = pokaz_logo_w_navbar
settings.navbar_brand_mode = navbar_brand_mode settings.typ_navbar = typ_navbar
settings.footer_brand_mode = footer_brand_mode settings.typ_stopka = typ_stopka
settings.footer_text = footer_text settings.stopka_text = stopka_text
db.session.commit() db.session.commit()
flash("Ustawienia globalne zostały zaktualizowane", "success") flash("Ustawienia globalne zostały zaktualizowane", "success")
return redirect(url_for("admin_dashboard")) return redirect(url_for("admin_dashboard"))
return render_template( return render_template("admin/ustawienia.html", settings=settings, client_ip=client_ip)
"admin/ustawienia.html", settings=settings, client_ip=client_ip
)
@app.route("/admin/zbiorka/<int:zbiorka_id>/wydatek/dodaj", methods=["GET", "POST"]) @app.route("/admin/zbiorka/<int:zbiorka_id>/wydatek/dodaj", methods=["GET", "POST"])
@login_required @login_required
def dodaj_wydatek(zbiorka_id): def dodaj_wydatek(zbiorka_id):
if not current_user.is_admin: if not current_user.czy_admin:
flash("Brak uprawnień", "danger") flash("Brak uprawnień", "danger")
return redirect(url_for("index")) return redirect(url_for("index"))
@@ -778,7 +792,7 @@ def dodaj_wydatek(zbiorka_id):
) )
@login_required @login_required
def oznacz_zbiorka(zbiorka_id): def oznacz_zbiorka(zbiorka_id):
if not current_user.is_admin: if not current_user.czy_admin:
flash("Brak uprawnień do wykonania tej operacji", "danger") flash("Brak uprawnień do wykonania tej operacji", "danger")
return redirect(url_for("index")) return redirect(url_for("index"))
@@ -810,7 +824,7 @@ def robots():
@app.route("/admin/zbiorka/<int:zbiorka_id>/transakcje") @app.route("/admin/zbiorka/<int:zbiorka_id>/transakcje")
@login_required @login_required
def transakcje_zbiorki(zbiorka_id): def transakcje_zbiorki(zbiorka_id):
if not current_user.is_admin: if not current_user.czy_admin:
flash("Brak uprawnień", "danger"); return redirect(url_for("index")) flash("Brak uprawnień", "danger"); return redirect(url_for("index"))
zb = db.session.get(Zbiorka, zbiorka_id) zb = db.session.get(Zbiorka, zbiorka_id)
if zb is None: if zb is None:
@@ -826,7 +840,7 @@ def transakcje_zbiorki(zbiorka_id):
@app.route("/admin/wplata/<int:wplata_id>/zapisz", methods=["POST"]) @app.route("/admin/wplata/<int:wplata_id>/zapisz", methods=["POST"])
@login_required @login_required
def zapisz_wplate(wplata_id): def zapisz_wplate(wplata_id):
if not current_user.is_admin: if not current_user.czy_admin:
flash("Brak uprawnień", "danger"); return redirect(url_for("index")) flash("Brak uprawnień", "danger"); return redirect(url_for("index"))
w = db.session.get(Wplata, wplata_id) w = db.session.get(Wplata, wplata_id)
if w is None: if w is None:
@@ -852,7 +866,7 @@ def zapisz_wplate(wplata_id):
@app.route("/admin/wplata/<int:wplata_id>/usun", methods=["POST"]) @app.route("/admin/wplata/<int:wplata_id>/usun", methods=["POST"])
@login_required @login_required
def usun_wplate(wplata_id): def usun_wplate(wplata_id):
if not current_user.is_admin: if not current_user.czy_admin:
flash("Brak uprawnień", "danger"); return redirect(url_for("index")) flash("Brak uprawnień", "danger"); return redirect(url_for("index"))
w = db.session.get(Wplata, wplata_id) w = db.session.get(Wplata, wplata_id)
if w is None: if w is None:
@@ -868,7 +882,7 @@ def usun_wplate(wplata_id):
@app.route("/admin/wydatek/<int:wydatek_id>/zapisz", methods=["POST"]) @app.route("/admin/wydatek/<int:wydatek_id>/zapisz", methods=["POST"])
@login_required @login_required
def zapisz_wydatek(wydatek_id): def zapisz_wydatek(wydatek_id):
if not current_user.is_admin: if not current_user.czy_admin:
flash("Brak uprawnień", "danger"); return redirect(url_for("index")) flash("Brak uprawnień", "danger"); return redirect(url_for("index"))
x = db.session.get(Wydatek, wydatek_id) x = db.session.get(Wydatek, wydatek_id)
if x is None: if x is None:
@@ -895,7 +909,7 @@ def zapisz_wydatek(wydatek_id):
@app.route("/admin/wydatek/<int:wydatek_id>/usun", methods=["POST"]) @app.route("/admin/wydatek/<int:wydatek_id>/usun", methods=["POST"])
@login_required @login_required
def usun_wydatek(wydatek_id): def usun_wydatek(wydatek_id):
if not current_user.is_admin: if not current_user.czy_admin:
flash("Brak uprawnień", "danger"); return redirect(url_for("index")) flash("Brak uprawnień", "danger"); return redirect(url_for("index"))
x = db.session.get(Wydatek, wydatek_id) x = db.session.get(Wydatek, wydatek_id)
if x is None: if x is None:
@@ -926,14 +940,12 @@ def healthcheck():
if __name__ == "__main__": if __name__ == "__main__":
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
# Tworzenie konta głównego admina, jeśli nie istnieje stmt = select(Uzytkownik).filter_by(czy_admin=True)
stmt = select(User).filter_by(is_admin=True)
admin = db.session.execute(stmt).scalars().first() admin = db.session.execute(stmt).scalars().first()
if not admin: if not admin:
main_admin = User( main_admin = Uzytkownik(
username=app.config["MAIN_ADMIN_USERNAME"], uzytkownik=app.config["MAIN_ADMIN_USERNAME"],
is_admin=True czy_admin=True
) )
main_admin.set_password(app.config["MAIN_ADMIN_PASSWORD"]) main_admin.set_password(app.config["MAIN_ADMIN_PASSWORD"])
db.session.add(main_admin) db.session.add(main_admin)

View File

@@ -1,19 +1,20 @@
(function () { // static/js/ustawienia.js
// IBAN: tylko cyfry, auto-grupowanie co 4 (po prefiksie PL) document.addEventListener('DOMContentLoaded', () => {
// Formatowanie IBAN (PL)
const iban = document.getElementById('numer_konta'); const iban = document.getElementById('numer_konta');
if (iban) { if (iban) {
iban.addEventListener('input', () => { iban.addEventListener('input', () => {
const digits = iban.value.replace(/\\D/g, '').slice(0, 26); // 26 cyfr po "PL" const digits = iban.value.replace(/\D/g, '').slice(0, 26);
const chunked = digits.replace(/(.{4})/g, '$1 ').trim(); const chunked = digits.replace(/(.{4})/g, '$1 ').trim();
iban.value = chunked; iban.value = chunked;
}); });
} }
// Telefon BLIK: tylko cyfry, format 3-3-3 // Telefon BLIK 3-3-3
const tel = document.getElementById('numer_telefonu_blik'); const tel = document.getElementById('numer_telefonu_blik');
if (tel) { if (tel) {
tel.addEventListener('input', () => { tel.addEventListener('input', () => {
const digits = tel.value.replace(/\\D/g, '').slice(0, 9); const digits = tel.value.replace(/\D/g, '').slice(0, 9);
const parts = []; const parts = [];
if (digits.length > 0) parts.push(digits.substring(0, 3)); if (digits.length > 0) parts.push(digits.substring(0, 3));
if (digits.length > 3) parts.push(digits.substring(3, 6)); if (digits.length > 3) parts.push(digits.substring(3, 6));
@@ -22,24 +23,23 @@
}); });
} }
// Biała lista IP/hostów — helpery // Biała lista IP/hostów
const ta = document.getElementById('allowed_login_hosts'); const ta = document.getElementById('dozwolone_hosty_logowania');
const count = document.getElementById('hostsCount'); const count = document.getElementById('hostsCount');
const addBtn = document.getElementById('btn-add-host'); const addBtn = document.getElementById('btn-add-host');
const addMyBtn = document.getElementById('btn-add-my-ip'); const addMyBtn = document.getElementById('btn-add-my-ip');
const input = document.getElementById('host_input'); const input = document.getElementById('host_input');
const dedupeBtn = document.getElementById('btn-dedupe');
function parseList(text) { const parseList = (text) =>
// akceptuj przecinki, średniki i nowe linie; trimuj; usuń puste text
return text .split(/[\r\n,;]+/) // \r?\n, przecinek, średnik
.split(/[\\n,;]+/)
.map(s => s.trim()) .map(s => s.trim())
.filter(Boolean); .filter(Boolean);
}
function formatList(arr) { const formatList = (arr) => arr.join('\n');
return arr.join('\\n');
} const dedupe = (arr) => {
function dedupe(arr) {
const seen = new Set(); const seen = new Set();
const out = []; const out = [];
for (const v of arr) { for (const v of arr) {
@@ -47,22 +47,23 @@
if (!seen.has(k)) { seen.add(k); out.push(v); } if (!seen.has(k)) { seen.add(k); out.push(v); }
} }
return out; return out;
} };
function updateCount() {
const updateCount = () => {
if (!ta || !count) return; if (!ta || !count) return;
count.textContent = parseList(ta.value).length.toString(); count.textContent = String(parseList(ta.value).length);
} };
function addEntry(val) {
const addEntry = (val) => {
if (!ta || !val) return; if (!ta || !val) return;
const list = dedupe([...parseList(ta.value), val]); const list = dedupe([...parseList(ta.value), val]);
ta.value = formatList(list); ta.value = formatList(list);
updateCount(); updateCount();
} };
if (ta) { if (ta) {
ta.addEventListener('input', updateCount); ta.addEventListener('input', updateCount);
// inicjalny przelicznik updateCount(); // inicjalne przeliczenie
updateCount();
} }
if (addBtn && input) { if (addBtn && input) {
@@ -82,11 +83,10 @@
}); });
} }
const dedupeBtn = document.getElementById('btn-dedupe');
if (dedupeBtn && ta) { if (dedupeBtn && ta) {
dedupeBtn.addEventListener('click', () => { dedupeBtn.addEventListener('click', () => {
ta.value = formatList(dedupe(parseList(ta.value))); ta.value = formatList(dedupe(parseList(ta.value)));
updateCount(); updateCount();
}); });
} }
})(); });

View File

@@ -9,11 +9,11 @@
}, false); }, false);
})(); })();
const pw = document.getElementById('password'); const pw = document.getElementById("haslo");
const toggle = document.getElementById('togglePw'); const toggle = document.getElementById('togglePw');
toggle.addEventListener('click', () => { toggle.addEventListener('click', () => {
const isText = pw.type === 'text'; const isText = pw.type === 'text';
pw.type = isText ? 'password' : 'text'; pw.type = isText ? "haslo" : 'text';
toggle.textContent = isText ? 'Pokaż' : 'Ukryj'; toggle.textContent = isText ? 'Pokaż' : 'Ukryj';
toggle.setAttribute('aria-pressed', (!isText).toString()); toggle.setAttribute('aria-pressed', (!isText).toString());
pw.focus(); pw.focus();

View File

@@ -5,7 +5,7 @@
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
} }
const pw1 = document.getElementById('password'); const pw1 = document.getElementById("haslo");
const pw2 = document.getElementById('password2'); const pw2 = document.getElementById('password2');
if (pw1.value !== pw2.value) { if (pw1.value !== pw2.value) {
e.preventDefault(); e.preventDefault();
@@ -19,11 +19,11 @@
}, false); }, false);
})(); })();
const pw = document.getElementById('password'); const pw = document.getElementById("haslo");
const toggle = document.getElementById('togglePw'); const toggle = document.getElementById('togglePw');
toggle.addEventListener('click', () => { toggle.addEventListener('click', () => {
const isText = pw.type === 'text'; const isText = pw.type === 'text';
pw.type = isText ? 'password' : 'text'; pw.type = isText ? "haslo" : 'text';
toggle.textContent = isText ? 'Pokaż' : 'Ukryj'; toggle.textContent = isText ? 'Pokaż' : 'Ukryj';
pw.focus(); pw.focus();
}); });

View File

@@ -269,8 +269,45 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row g-3 mt-2">
<div class="col-12 col-md-4">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="pokaz_postep_finanse"
name="pokaz_postep_finanse" {% if is_edit and zbiorka.pokaz_postep_finanse or not
is_edit %}checked{% endif %}>
<label class="form-check-label" for="pokaz_postep_finanse">Pokaż postęp: Finanse</label>
</div>
</div>
<div class="col-12 col-md-4">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="pokaz_postep_pozycje"
name="pokaz_postep_pozycje" {% if is_edit and zbiorka.pokaz_postep_pozycje or not
is_edit %}checked{% endif %}>
<label class="form-check-label" for="pokaz_postep_pozycje">Pokaż postęp: Zakupy
(liczba)</label>
</div>
</div>
<div class="col-12 col-md-4">
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" id="pokaz_postep_kwotowo"
name="pokaz_postep_kwotowo" {% if is_edit and zbiorka.pokaz_postep_kwotowo or not
is_edit %}checked{% endif %}>
<label class="form-check-label" for="pokaz_postep_kwotowo">Pokaż postęp: Zakupy
(kwotowo)</label>
</div>
</div>
</div>
</div> </div>
<!-- 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">

View File

@@ -6,14 +6,24 @@
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="mb-0">Transakcje: {{ zbiorka.nazwa }}</h3> <h3 class="mb-0">Transakcje: {{ zbiorka.nazwa }}</h3>
<div class="btn-group"> <div class="btn-group" role="group" aria-label="Akcje zbiórki">
<a class="btn btn-sm btn-outline-light border" href="{{ url_for('dodaj_wplate', zbiorka_id=zbiorka.id) }}">+ <a class="btn btn-sm btn-outline-light border" href="{{ url_for('dodaj_wplate', zbiorka_id=zbiorka.id) }}">
Wpłata</a> <i class="fas fa-plus-circle"></i> Dodaj wpłatę
<a class="btn btn-sm btn-outline-light border" </a>
href="{{ url_for('dodaj_wydatek', zbiorka_id=zbiorka.id) }}">+ Wydatek</a>
<a class="btn btn-sm btn-outline-light border" <a class="btn btn-sm btn-outline-light border" href="{{ url_for('dodaj_wydatek', zbiorka_id=zbiorka.id) }}">
href="{{ url_for('zbiorka', zbiorka_id=zbiorka.id) }}">Szczegóły zbiórki</a> Dodaj wydatek
</a>
<a class="btn btn-sm btn-outline-light border" href="{{ url_for('edytuj_stan', zbiorka_id=zbiorka.id) }}">
Edytuj stan
</a>
<a class="btn btn-sm btn-outline-light border" href="{{ url_for('zbiorka', zbiorka_id=zbiorka.id) }}">
Otwórz ↗
</a>
</div> </div>
</div> </div>
<div class="card shadow-sm"> <div class="card shadow-sm">

View File

@@ -40,42 +40,60 @@
</div> </div>
</div> </div>
<!-- SEKCJA: Dostępy / biała lista IP -->
<!-- SEKCJA: Dostępy / biała lista IP --> <!-- SEKCJA: Dostępy / biała lista IP -->
<div class="card shadow-sm mb-4"> <div class="card shadow-sm mb-4">
<div class="card-header bg-secondary text-white d-flex align-items-center justify-content-between gap-2"> <div class="card-header bg-secondary text-white d-flex align-items-center justify-content-between gap-2">
<h3 class="card-title mb-0">Dostęp — dozwolone adresy IP / hosty</h3> <h3 class="card-title mb-0">Dostęp — dozwolone adresy IP / hosty</h3>
<small class="opacity-75">Zależnie od konfiguracji, logowanie może wymagać dopasowania do białej listy</small> <small class="opacity-75">Zależnie od konfiguracji logowanie może wymagać dopasowania do białej listy</small>
</div> </div>
<div class="card-body"> <div class="card-body">
<!-- Wiersz z inputem i przyciskiem dodawania -->
<div class="row g-3 align-items-end"> <div class="row g-3 align-items-end">
<div class="col-12 col-md-6"> <div class="col-12 col-lg-8">
<label for="host_input" class="form-label">Dodaj pojedynczy IP/host</label> <label for="host_input" class="form-label">Dodaj IP/host</label>
<input type="text" class="form-control" id="host_input" placeholder="np. 203.0.113.42 lub corp.example.com" <div class="input-group">
aria-describedby="hostAddHelp"> <input type="text" class="form-control" id="host_input"
<div id="hostAddHelp" class="form-text">Po wpisaniu kliknij „Dodaj do listy”. Duplikaty są pomijane.</div> placeholder="np. 203.0.113.42 lub corp.example.com" aria-describedby="hostAddHelp">
<button type="button" class="btn btn-outline-primary" id="btn-add-host">
<i class="fas fa-plus-circle"></i> Dodaj
</button>
</div>
<div id="hostAddHelp" class="form-text">Po wpisaniu kliknij „Dodaj”. Duplikaty są pomijane.</div>
</div> </div>
<div class="col-12 col-md-6 d-flex gap-2">
<button type="button" class="btn btn-outline-light border" id="btn-add-host">Dodaj do listy</button> <div class="col-12 col-lg-4">
<button type="button" class="btn btn-light text-dark" id="btn-add-my-ip" data-my-ip="{{ client_ip }}">Dodaj <div class="d-flex flex-wrap gap-2 justify-content-lg-end">
moje IP ({{ client_ip }})</button> <button type="button" class="btn btn-light text-dark" id="btn-add-my-ip" data-my-ip="{{ client_ip }}">
<button type="button" class="btn btn-outline-light border" id="btn-dedupe">Usuń duplikaty</button> <i class="fas fa-location-arrow"></i> Dodaj moje IP ({{ client_ip }})
</button>
<button type="button" class="btn btn-outline-secondary" id="btn-dedupe">
<i class="fas fa-broom"></i> Usuń duplikaty
</button>
</div>
</div> </div>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<label for="allowed_login_hosts" class="form-label">Dozwolone hosty logowania (jeden na linię lub rozdzielone <div class="d-flex justify-content-between align-items-center mb-1">
przecinkami)</label> <label for="dozwolone_hosty_logowania" class="form-label mb-0">
<textarea class="form-control" id="allowed_login_hosts" name="allowed_login_hosts" rows="6" Dozwolone hosty logowania (jeden na linię lub rozdzielone przecinkami)
placeholder="Adresy IP lub nazwy domen — każdy w osobnej linii lub rozdzielony przecinkiem">{{ settings.allowed_login_hosts if settings and settings.allowed_login_hosts else '' }}</textarea> </label>
<div class="d-flex justify-content-between mt-1"> <span class="badge text-bg-secondary">Pozycji: <span id="hostsCount">0</span></span>
<small class="text-muted">Akceptowane separatory: przecinek (`,`), średnik (`;`) i nowa linia.</small>
<small class="text-muted">Pozycji na liście: <span id="hostsCount">0</span></small>
</div> </div>
<textarea class="form-control" id="dozwolone_hosty_logowania" name="dozwolone_hosty_logowania" rows="6"
placeholder="Adresy IP lub nazwy domen — każdy w osobnej linii lub rozdzielony przecinkiem">{{ settings.dozwolone_hosty_logowania if settings and settings.dozwolone_hosty_logowania else '' }}</textarea>
<small class="text-muted d-block mt-1">
Akceptowane separatory: przecinek (`,`), średnik (`;`) i nowa linia.
</small>
</div> </div>
</div> </div>
</div> </div>
<!-- SEKCJA: Branding --> <!-- SEKCJA: Branding -->
<div class="card shadow-sm mb-4"> <div class="card shadow-sm mb-4">
<div class="card-header bg-secondary text-white d-flex align-items-center justify-content-between gap-2"> <div class="card-header bg-secondary text-white d-flex align-items-center justify-content-between gap-2">
@@ -99,9 +117,9 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label for="site_title" class="form-label">Tytuł serwisu</label> <label for="tytul_strony" class="form-label">Tytuł serwisu</label>
<input type="text" class="form-control" id="site_title" name="site_title" <input type="text" class="form-control" id="tytul_strony" name="tytul_strony"
value="{{ settings.site_title if settings else '' }}" placeholder="Np. Zbiórki unitraklub.pl"> value="{{ settings.tytul_strony if settings else '' }}" placeholder="Np. Zbiórki unitraklub.pl">
</div> </div>
</div> </div>
@@ -112,15 +130,15 @@
<div class="col-md-6"> <div class="col-md-6">
<h6 class="mb-2">Menu (navbar)</h6> <h6 class="mb-2">Menu (navbar)</h6>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="navbar_brand_mode" id="navbar_mode_logo" value="logo" <input class="form-check-input" type="radio" name="typ_navbar" id="navbar_mode_logo" value="logo" {% if
{% if settings and settings.navbar_brand_mode=='logo' or (settings and settings.show_logo_in_navbar) settings and settings.typ_navbar=='logo' or (settings and settings.pokaz_logo_w_navbar) %}checked{%
%}checked{% endif %}> endif %}>
<label class="form-check-label" for="navbar_mode_logo">Pokaż logo</label> <label class="form-check-label" for="navbar_mode_logo">Pokaż logo</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="navbar_brand_mode" id="navbar_mode_text" value="text" <input class="form-check-input" type="radio" name="typ_navbar" id="navbar_mode_text" value="text" {% if
{% if not settings or (settings and settings.navbar_brand_mode !='logo' and not not settings or (settings and settings.typ_navbar !='logo' and not settings.pokaz_logo_w_navbar)
settings.show_logo_in_navbar) %}checked{% endif %}> %}checked{% endif %}>
<label class="form-check-label" for="navbar_mode_text">Pokaż tekst</label> <label class="form-check-label" for="navbar_mode_text">Pokaż tekst</label>
</div> </div>
<div class="form-text mt-1">Jeśli wybierzesz logo, użyjemy adresu z pola "Tytuł serwisu".</div> <div class="form-text mt-1">Jeśli wybierzesz logo, użyjemy adresu z pola "Tytuł serwisu".</div>
@@ -130,19 +148,19 @@
<div class="col-md-6"> <div class="col-md-6">
<h6 class="mb-2">Stopka</h6> <h6 class="mb-2">Stopka</h6>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="footer_brand_mode" id="footer_mode_logo" value="logo" <input class="form-check-input" type="radio" name="typ_stopka" id="footer_mode_logo" value="logo" {% if
{% if settings and settings.footer_brand_mode=='logo' %}checked{% endif %}> settings and settings.typ_stopka=='logo' %}checked{% endif %}>
<label class="form-check-label" for="footer_mode_logo">Logo</label> <label class="form-check-label" for="footer_mode_logo">Logo</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input class="form-check-input" type="radio" name="footer_brand_mode" id="footer_mode_text" value="text" <input class="form-check-input" type="radio" name="typ_stopka" id="footer_mode_text" value="text" {% if
{% if not settings or (settings and settings.footer_brand_mode !='logo' ) %}checked{% endif %}> not settings or (settings and settings.typ_stopka !='logo' ) %}checked{% endif %}>
<label class="form-check-label" for="footer_mode_text">Tekst</label> <label class="form-check-label" for="footer_mode_text">Tekst</label>
</div> </div>
<label for="footer_text" class="form-label mt-2">Tekst w stopce (gdy wybrano „Tekst”)</label> <label for="stopka_text" class="form-label mt-2">Tekst w stopce (gdy wybrano „Tekst”)</label>
<input type="text" class="form-control" id="footer_text" name="footer_text" <input type="text" class="form-control" id="stopka_text" name="stopka_text"
value="{{ settings.footer_text if settings and settings.footer_text else '' }}" value="{{ settings.stopka_text if settings and settings.stopka_text else '' }}"
placeholder="Np. © {{ now().year if now else '2025' }} Zbiórki"> placeholder="Np. © {{ now().year if now else '2025' }} Zbiórki">
<div class="form-text">Pozostaw pusty, by użyć domyślnego.</div> <div class="form-text">Pozostaw pusty, by użyć domyślnego.</div>
</div> </div>

View File

@@ -14,13 +14,13 @@
<nav class="navbar navbar-expand-lg"> <nav class="navbar navbar-expand-lg">
<div class="container"> <div class="container">
<a class="navbar-brand d-flex align-items-center gap-2" href="{{ url_for('index') }}"> <a class="navbar-brand d-flex align-items-center gap-2" href="{{ url_for('index') }}">
{% set nav_mode = (global_settings.navbar_brand_mode if global_settings and {% set nav_mode = (global_settings.typ_navbar if global_settings and
global_settings.navbar_brand_mode else ('logo' if global_settings and global_settings.typ_navbar else ('logo' if global_settings and
global_settings.show_logo_in_navbar else 'text')) %} global_settings.pokaz_logo_w_navbar else 'text')) %}
{% if nav_mode == 'logo' and global_settings and global_settings.logo_url %} {% if nav_mode == 'logo' and global_settings and global_settings.logo_url %}
<img src="{{ global_settings.logo_url }}" alt="Logo" style="max-height:40px; vertical-align:middle;"> <img src="{{ global_settings.logo_url }}" alt="Logo" style="max-height:40px; vertical-align:middle;">
{% else %} {% else %}
<span>{{ global_settings.site_title if global_settings and global_settings.site_title else "Zbiórki" <span>{{ global_settings.tytul_strony if global_settings and global_settings.tytul_strony else "Zbiórki"
}}</span> }}</span>
{% endif %} {% endif %}
</a> </a>
@@ -72,12 +72,12 @@
<!-- stopka --> <!-- stopka -->
<footer class="mt-auto text-center py-3 border-top" style="background: var(--surface-0);"> <footer class="mt-auto text-center py-3 border-top" style="background: var(--surface-0);">
{% set footer_mode = global_settings.footer_brand_mode if global_settings and global_settings.footer_brand_mode {% set footer_mode = global_settings.typ_stopka if global_settings and global_settings.typ_stopka
else 'text' %} else 'text' %}
{% if footer_mode == 'logo' and global_settings and global_settings.logo_url %} {% if footer_mode == 'logo' and global_settings and global_settings.logo_url %}
<img src="{{ global_settings.logo_url }}" alt="Logo" style="max-height:28px;"> <img src="{{ global_settings.logo_url }}" alt="Logo" style="max-height:28px;">
{% else %} {% else %}
{{ global_settings.footer_text if global_settings and global_settings.footer_text else "© " ~ (now().year if now {{ global_settings.stopka_text if global_settings and global_settings.stopka_text else "© " ~ (now().year if now
else '2025') ~ " linuxiarz.pl" }} else '2025') ~ " linuxiarz.pl" }}
{% endif %} {% endif %}
<div class="small text-muted">v{{ APP_VERSION }}</div> <div class="small text-muted">v{{ APP_VERSION }}</div>

View File

@@ -105,7 +105,7 @@ zbiórki{% endif %}{% endblock %}
{% else %} {% else %}
<h5 class="mb-2">Brak aktywnych zbiórek</h5> <h5 class="mb-2">Brak aktywnych zbiórek</h5>
<p class="text-muted mb-4">Wygląda na to, że teraz nic nie zbieramy.</p> <p class="text-muted mb-4">Wygląda na to, że teraz nic nie zbieramy.</p>
{% if current_user.is_authenticated and current_user.is_admin %} {% if current_user.is_authenticated and current_user.czy_admin %}
<a href="{{ url_for('admin_dashboard') }}" class="btn btn-primary">Utwórz nową zbiórkę</a> <a href="{{ url_for('admin_dashboard') }}" class="btn btn-primary">Utwórz nową zbiórkę</a>
{% else %} {% else %}
<a href="{{ url_for('zbiorki_zrealizowane') }}" class="btn btn-primary">Zobacz zrealizowane</a> <a href="{{ url_for('zbiorki_zrealizowane') }}" class="btn btn-primary">Zobacz zrealizowane</a>

View File

@@ -18,21 +18,21 @@
{% endif %} {% endif %}
<div class="mb-3"> <div class="mb-3">
<label for="username" class="form-label">Nazwa użytkownika</label> <label for="uzytkownik" class="form-label">Nazwa użytkownika</label>
<input type="text" class="form-control" id="username" name="username" <input type="text" class="form-control" id="uzytkownik" name="uzytkownik"
autocomplete="username" autocapitalize="none" spellcheck="false" required autofocus> autocomplete="username" autocapitalize="none" spellcheck="false" required autofocus>
<div class="invalid-feedback">Podaj nazwę użytkownika.</div> <div class="invalid-feedback">Podaj nazwę użytkownika.</div>
</div> </div>
<div class="mb-2"> <div class="mb-2">
<label for="password" class="form-label d-flex justify-content-between align-items-center"> <label for="haslo" class="form-label d-flex justify-content-between align-items-center">
<span>Hasło</span> <span>Hasło</span>
<small id="capsWarning" class="text-muted" style="display:none;">CAPS LOCK <small id="capsWarning" class="text-muted" style="display:none;">CAPS LOCK
włączony</small> włączony</small>
</label> </label>
<div class="input-group"> <div class="input-group">
<input type="password" class="form-control" id="password" name="password" <input type="password" class="form-control" id="haslo" name="haslo"
autocomplete="current-password" required minlength="5"> autocomplete="current-password" required minlength="5">
<button type="button" class="btn btn-secondary rounded-end" id="togglePw" <button type="button" class="btn btn-secondary rounded-end" id="togglePw"
aria-label="Pokaż/ukryj hasło">Pokaż</button> aria-label="Pokaż/ukryj hasło">Pokaż</button>

View File

@@ -14,20 +14,20 @@
<form method="post" class="needs-validation" novalidate> <form method="post" class="needs-validation" novalidate>
<div class="mb-3"> <div class="mb-3">
<label for="username" class="form-label">Nazwa użytkownika</label> <label for="uzytkownik" class="form-label">Nazwa użytkownika</label>
<input type="text" class="form-control" id="username" name="username" <input type="text" class="form-control" id="uzytkownik" name="uzytkownik"
autocomplete="username" autocapitalize="none" spellcheck="false" required autofocus> autocomplete="username" autocapitalize="none" spellcheck="false" required autofocus>
<div class="invalid-feedback">Podaj nazwę użytkownika.</div> <div class="invalid-feedback">Podaj nazwę użytkownika.</div>
</div> </div>
<div class="mb-3"> <div class="mb-3">
<label for="password" class="form-label d-flex justify-content-between align-items-center"> <label for="haslo" class="form-label d-flex justify-content-between align-items-center">
<span>Hasło</span> <span>Hasło</span>
<small id="capsWarning" class="text-muted" style="display:none;">CAPS LOCK <small id="capsWarning" class="text-muted" style="display:none;">CAPS LOCK
włączony</small> włączony</small>
</label> </label>
<div class="input-group"> <div class="input-group">
<input type="password" class="form-control" id="password" name="password" <input type="password" class="form-control" id="haslo" name="haslo"
autocomplete="new-password" required minlength="6"> autocomplete="new-password" required minlength="6">
<button type="button" class="btn btn-secondary" id="togglePw" <button type="button" class="btn btn-secondary" id="togglePw"
aria-label="Pokaż/ukryj hasło">Pokaż</button> aria-label="Pokaż/ukryj hasło">Pokaż</button>

View File

@@ -103,23 +103,28 @@
<h5 class="mb-0">Postęp</h5> <h5 class="mb-0">Postęp</h5>
<div class="d-flex flex-wrap align-items-center gap-2"> <div class="d-flex flex-wrap align-items-center gap-2">
{% if has_cel and not zbiorka.ukryj_kwote %} {% if has_cel and not zbiorka.ukryj_kwote and zbiorka.pokaz_postep_finanse %}
<span class="badge bg-dark border" style="border-color: var(--border);"> <span class="badge bg-dark border" style="border-color: var(--border);">
Finanse: {{ zbiorka.stan|round(2) }} / {{ zbiorka.cel|round(2) }} PLN Finanse: {{ zbiorka.stan|round(2) }} / {{ zbiorka.cel|round(2) }} PLN
</span> </span>
{% endif %} {% endif %}
{% if has_items %}
{% if has_items and zbiorka.pokaz_postep_pozycje %}
<span class="badge bg-secondary">Pozycje: {{ kupione_cnt }}/{{ total_cnt }}</span> <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 %}
{% if has_items and not zbiorka.ukryj_kwote and (suma_all or 0) > 0 and zbiorka.pokaz_postep_kwotowo %}
<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>
</div> </div>
<hr class="hr-bw"> <hr class="hr-bw">
<!-- Pasek: Finanse (zawsze) -->
{# Pasek: Finanse #}
{% if zbiorka.pokaz_postep_finanse %}
<div class="mb-3"> <div class="mb-3">
<small class="text-muted">Finanse</small> <small class="text-muted">Finanse</small>
<div class="progress" role="progressbar" aria-valuenow="{{ progress_clamped|round(2) }}" aria-valuemin="0" <div class="progress" role="progressbar" aria-valuenow="{{ progress_clamped|round(2) }}" aria-valuemin="0"
@@ -130,9 +135,10 @@
{% if zbiorka.ukryj_kwote %}—{% else %}{{ progress|round(1) }}%{% endif %} {% if zbiorka.ukryj_kwote %}—{% else %}{{ progress|round(1) }}%{% endif %}
</small> </small>
</div> </div>
{% endif %}
{% if has_items %} {# Pasek: Zakupy sztukami #}
<!-- Pasek: Zakupy sztukami --> {% if has_items and zbiorka.pokaz_postep_pozycje %}
<div class="mb-3"> <div class="mb-3">
<small class="text-muted">Zakupy (liczba pozycji)</small> <small class="text-muted">Zakupy (liczba pozycji)</small>
<div class="progress" role="progressbar" aria-valuenow="{{ items_pct|round(2) }}" aria-valuemin="0" <div class="progress" role="progressbar" aria-valuenow="{{ items_pct|round(2) }}" aria-valuemin="0"
@@ -141,9 +147,10 @@
</div> </div>
<small class="text-muted">{{ items_pct|round(1) }}%</small> <small class="text-muted">{{ items_pct|round(1) }}%</small>
</div> </div>
{% endif %}
{% if not zbiorka.ukryj_kwote and (suma_all or 0) > 0 %} {# Pasek: Zakupy kwotowo #}
<!-- Pasek: Zakupy kwotowo --> {% if has_items and not zbiorka.ukryj_kwote and (suma_all or 0) > 0 and zbiorka.pokaz_postep_kwotowo %}
<div> <div>
<small class="text-muted">Zakupy (kwotowo)</small> <small class="text-muted">Zakupy (kwotowo)</small>
<div class="progress" role="progressbar" aria-valuenow="{{ suma_pct|round(2) }}" aria-valuemin="0" <div class="progress" role="progressbar" aria-valuenow="{{ suma_pct|round(2) }}" aria-valuemin="0"
@@ -153,7 +160,6 @@
<small class="text-muted">{{ suma_pct|round(1) }}%</small> <small class="text-muted">{{ suma_pct|round(1) }}%</small>
</div> </div>
{% endif %} {% endif %}
{% endif %}
</div> </div>
</div> </div>
@@ -238,7 +244,7 @@
{% endif %} {% endif %}
{% if current_user.is_authenticated and current_user.is_admin %} {% if current_user.is_authenticated and current_user.czy_admin %}
<hr> <hr>
<div class="d-grid gap-2 mt-2"> <div class="d-grid gap-2 mt-2">
<a href="{{ url_for('dodaj_wplate', zbiorka_id=zbiorka.id) }}" class="btn btn-outline-light btn-sm">Dodaj <a href="{{ url_for('dodaj_wplate', zbiorka_id=zbiorka.id) }}" class="btn btn-outline-light btn-sm">Dodaj
@@ -264,7 +270,7 @@
{% 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 %}
{% if current_user.is_authenticated and current_user.is_admin %} {% if current_user.is_authenticated and current_user.czy_admin %}
<a href="{{ url_for('transakcje_zbiorki', zbiorka_id=zbiorka.id) }}" <a href="{{ url_for('transakcje_zbiorki', zbiorka_id=zbiorka.id) }}"
class="btn btn-sm btn-outline-light border"> class="btn btn-sm btn-outline-light border">
Zarządzaj Zarządzaj