From e9db945bb40c4bbe136a4dfc8946591bca47292c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Thu, 28 Aug 2025 10:27:06 +0200 Subject: [PATCH] przebudowa systemu --- .env.example | 34 +++ .gitignore | 2 +- alters.txt | 49 +++- app.py | 431 +++++++++++++++++----------- config.example.py | 14 - config.py | 45 +++ docker-compose.yml | 4 +- emergency_access.txt | 3 + run_waitress.py | 5 +- static/css/custom.css | 389 ++++++++++++++++--------- static/js/admin_dashboard.js | 0 static/js/dodaj_wplate.js | 21 ++ static/js/dodaj_zbiorke.js | 37 +++ static/js/edytuj_stan.js | 82 ++++++ static/js/edytuj_zbiorke.js | 60 ++++ static/js/mde_custom.js | 4 + static/js/progress.js | 13 + static/js/ustawienia.js | 92 ++++++ static/js/walidacja_logowanie.js | 27 ++ static/js/walidacja_rejestracja.js | 37 +++ static/js/zbiorka.js | 38 +++ templates/admin/add_wplata.html | 24 -- templates/admin/add_zbiorka.html | 51 ---- templates/admin/dashboard.html | 341 +++++++++++++++------- templates/admin/dodaj_wplate.html | 86 ++++++ templates/admin/dodaj_zbiorke.html | 125 ++++++++ templates/admin/edit_zbiorka.html | 67 ----- templates/admin/edytuj_stan.html | 132 ++++++++- templates/admin/edytuj_zbiorke.html | 154 ++++++++++ templates/admin/settings.html | 55 ---- templates/admin/ustawienia.html | 125 ++++++++ templates/base.html | 113 ++++---- templates/index.html | 103 +++++-- templates/login.html | 64 ++++- templates/register.html | 69 ++++- templates/zbiorka.html | 220 +++++++++----- 36 files changed, 2307 insertions(+), 809 deletions(-) create mode 100644 .env.example delete mode 100644 config.example.py create mode 100644 config.py create mode 100644 emergency_access.txt create mode 100644 static/js/admin_dashboard.js create mode 100644 static/js/dodaj_wplate.js create mode 100644 static/js/dodaj_zbiorke.js create mode 100644 static/js/edytuj_stan.js create mode 100644 static/js/edytuj_zbiorke.js create mode 100644 static/js/mde_custom.js create mode 100644 static/js/progress.js create mode 100644 static/js/ustawienia.js create mode 100644 static/js/walidacja_logowanie.js create mode 100644 static/js/walidacja_rejestracja.js create mode 100644 static/js/zbiorka.js delete mode 100644 templates/admin/add_wplata.html delete mode 100644 templates/admin/add_zbiorka.html create mode 100644 templates/admin/dodaj_wplate.html create mode 100644 templates/admin/dodaj_zbiorke.html delete mode 100644 templates/admin/edit_zbiorka.html create mode 100644 templates/admin/edytuj_zbiorke.html delete mode 100644 templates/admin/settings.html create mode 100644 templates/admin/ustawienia.html diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..15980da --- /dev/null +++ b/.env.example @@ -0,0 +1,34 @@ +# === Podstawowe === +APP_PORT=8080 + +# SQLAlchemy URI bazy (np. SQLite, Postgres, MySQL). +# Przykłady: +# - SQLite w katalogu instance: sqlite:///instance/baza.db +# - SQLite w bieżącym katalogu: sqlite:///baza.db +# - Postgres: postgresql+psycopg2://user:pass@host:5432/dbname +# - MySQL: mysql+pymysql://user:pass@host:3306/dbname +DATABASE_URL=sqlite:///instance/baza.db + +# Klucz sesji Flask (USTAW własną silną wartość w produkcji!) +SECRET_KEY=change_me_strong_secret + +# === Rejestracja i admin === +# Czy pozwalać na rejestrację przez formularz (True/False) +ALLOW_REGISTRATION=False + +# Dane głównego admina (tworzonego automatycznie, jeśli brak w bazie) +MAIN_ADMIN_USERNAME=admin +MAIN_ADMIN_PASSWORD=admin + +# === Indeksowanie / cache === +# Blokuj boty (ustawia także X-Robots-Tag) (True/False) +BLOCK_BOTS=True + +# Wartość nagłówka Cache-Control dla stron publicznych +CACHE_CONTROL_HEADER=max-age=600 + +# Dodatkowe PRAGMA (opcjonalnie, jeśli chcesz dokładać własne) +PRAGMA_HEADER= + +# Wartość nagłówka X-Robots-Tag, gdy BLOCK_BOTS=True +ROBOTS_TAG=noindex, nofollow, nosnippet, noarchive diff --git a/.gitignore b/.gitignore index 5e436cf..7131a71 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ __pycache__ data/ instance/ venv/ -config.py \ No newline at end of file +.env \ No newline at end of file diff --git a/alters.txt b/alters.txt index ec1a605..75c9bd1 100644 --- a/alters.txt +++ b/alters.txt @@ -1,3 +1,48 @@ -ALTER TABLE global_settings ADD COLUMN allowed_login_hosts TEXT; +-- WŁĄCZ/wyłącz FK zależnie od etapu migracji +PRAGMA foreign_keys = OFF; -ALTER TABLE zbiorka ADD COLUMN zrealizowana BOOLEAN DEFAULT 0; +BEGIN TRANSACTION; + +-- 1) Nowa tabela z właściwym FK (ON DELETE CASCADE) +CREATE TABLE wplata_new ( + id INTEGER PRIMARY KEY, + zbiorka_id INTEGER NOT NULL, + kwota REAL NOT NULL, + data DATETIME, + opis TEXT, + FOREIGN KEY(zbiorka_id) REFERENCES zbiorka(id) ON DELETE CASCADE +); + +-- 2) (opcjonalnie) upewnij się, że nie ma „sierotek” +-- SELECT w.* FROM wplata w LEFT JOIN zbiorka z ON z.id = w.zbiorka_id WHERE z.id IS NULL; + +-- 3) Kopiowanie danych +INSERT INTO wplata_new (id, zbiorka_id, kwota, data, opis) +SELECT id, zbiorka_id, kwota, data, opis +FROM wplata; + +-- 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; + +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; \ No newline at end of file diff --git a/app.py b/app.py index 6e195bb..ed70df4 100644 --- a/app.py +++ b/app.py @@ -1,9 +1,19 @@ from flask import Flask, render_template, request, redirect, url_for, flash from flask_sqlalchemy import SQLAlchemy -from flask_login import LoginManager, login_user, login_required, logout_user, current_user, UserMixin +from flask_login import ( + LoginManager, + login_user, + login_required, + logout_user, + current_user, + UserMixin, +) from werkzeug.security import generate_password_hash, check_password_hash from datetime import datetime from markupsafe import Markup +from sqlalchemy import event +from sqlalchemy.engine import Engine + import markdown as md from flask import request, flash, abort import os @@ -12,14 +22,15 @@ import socket app = Flask(__name__) # Ładujemy konfigurację z pliku config.py -app.config.from_object('config.Config') +app.config.from_object("config.Config") db = SQLAlchemy(app) login_manager = LoginManager(app) -login_manager.login_view = 'login' +login_manager.login_view = "login" # MODELE + class User(UserMixin, db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) @@ -28,10 +39,11 @@ class User(UserMixin, db.Model): def set_password(self, password): self.password_hash = generate_password_hash(password) - + def check_password(self, password): return check_password_hash(self.password_hash, password) + class Zbiorka(db.Model): id = db.Column(db.Integer, primary_key=True) nazwa = db.Column(db.String(100), nullable=False) @@ -42,26 +54,57 @@ class Zbiorka(db.Model): stan = db.Column(db.Float, default=0.0) ukryta = db.Column(db.Boolean, default=False) ukryj_kwote = db.Column(db.Boolean, default=False) - wplaty = db.relationship('Wplata', backref='zbiorka', lazy=True, order_by='Wplata.data.desc()') zrealizowana = db.Column(db.Boolean, default=False) + wplaty = db.relationship( + "Wplata", + back_populates="zbiorka", + lazy=True, + order_by="Wplata.data.desc()", + cascade="all, delete-orphan", + passive_deletes=True, + ) + + class Wplata(db.Model): id = db.Column(db.Integer, primary_key=True) - zbiorka_id = db.Column(db.Integer, db.ForeignKey('zbiorka.id'), nullable=False) + zbiorka_id = db.Column( + db.Integer, + db.ForeignKey("zbiorka.id", ondelete="CASCADE"), + nullable=False, + ) kwota = db.Column(db.Float, nullable=False) data = db.Column(db.DateTime, default=datetime.utcnow) - opis = db.Column(db.Text, nullable=True) # Opis wpłaty + opis = db.Column(db.Text, nullable=True) + + zbiorka = db.relationship("Zbiorka", back_populates="wplaty") + class GlobalSettings(db.Model): id = db.Column(db.Integer, primary_key=True) numer_konta = 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) + 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) + @login_manager.user_loader def load_user(user_id): return User.query.get(int(user_id)) + +@event.listens_for(Engine, "connect") +def set_sqlite_pragma(dbapi_connection, connection_record): + try: + cursor = dbapi_connection.cursor() + cursor.execute("PRAGMA foreign_keys=ON") + cursor.close() + except Exception: + pass + + def get_real_ip(): headers = request.headers cf_ip = headers.get("CF-Connecting-IP") @@ -83,7 +126,7 @@ def is_allowed_ip(remote_ip, allowed_hosts_str): if os.path.exists("emergency_access.txt"): return True - allowed_hosts = re.split(r'[\n,]+', allowed_hosts_str.strip()) + allowed_hosts = re.split(r"[\n,]+", allowed_hosts_str.strip()) allowed_ips = set() for host in allowed_hosts: host = host.strip() @@ -105,45 +148,58 @@ def is_allowed_ip(remote_ip, allowed_hosts_str): # Dodaj filtr Markdown – pozwala na zagnieżdżanie linków i obrazków w opisie -@app.template_filter('markdown') +@app.template_filter("markdown") def markdown_filter(text): return Markup(md.markdown(text)) + @app.context_processor -def inject_ip_allowed(): +def inject_globals(): settings = GlobalSettings.query.first() - allowed_hosts_str = settings.allowed_login_hosts if settings and settings.allowed_login_hosts else "" + allowed_hosts_str = ( + settings.allowed_login_hosts + if settings and settings.allowed_login_hosts + else "" + ) client_ip = get_real_ip() - return {'is_ip_allowed': is_allowed_ip(client_ip, allowed_hosts_str)} + return { + "is_ip_allowed": is_allowed_ip(client_ip, allowed_hosts_str), + "global_settings": settings, + } + # TRASY PUBLICZNE - -@app.route('/') +@app.route("/") def index(): zbiorki = Zbiorka.query.filter_by(ukryta=False, zrealizowana=False).all() - return render_template('index.html', zbiorki=zbiorki) + return render_template("index.html", zbiorki=zbiorki) -@app.route('/zbiorki_zrealizowane') + +@app.route("/zbiorki_zrealizowane") def zbiorki_zrealizowane(): zbiorki = Zbiorka.query.filter_by(zrealizowana=True).all() - return render_template('index.html', zbiorki=zbiorki) + return render_template("index.html", zbiorki=zbiorki) + @app.errorhandler(404) def page_not_found(e): - return redirect(url_for('index')) + return redirect(url_for("index")) -@app.route('/zbiorka/') + +@app.route("/zbiorka/") def zbiorka(zbiorka_id): zb = Zbiorka.query.get_or_404(zbiorka_id) # Jeżeli zbiórka jest ukryta i użytkownik nie jest administratorem, zwróć 404 if zb.ukryta and (not current_user.is_authenticated or not current_user.is_admin): abort(404) - return render_template('zbiorka.html', zbiorka=zb) + return render_template("zbiorka.html", zbiorka=zb) + # TRASY LOGOWANIA I REJESTRACJI -@app.route('/login', methods=['GET', 'POST']) -def login(): + +@app.route("/zaloguj", methods=["GET", "POST"]) +def zaloguj(): # Pobierz ustawienia globalne, w tym dozwolone hosty settings = GlobalSettings.query.first() allowed_hosts_str = "" @@ -153,187 +209,211 @@ def login(): # Sprawdzenie, czy adres IP klienta jest dozwolony client_ip = get_real_ip() if not is_allowed_ip(client_ip, allowed_hosts_str): - flash('Dostęp do endpointu /login jest zablokowany dla Twojego adresu IP', 'danger') - return redirect(url_for('index')) + flash( + "Dostęp do endpointu /login jest zablokowany dla Twojego adresu IP", + "danger", + ) + return redirect(url_for("index")) - if request.method == 'POST': - username = request.form['username'] - password = request.form['password'] + if request.method == "POST": + username = request.form["username"] + password = request.form["password"] user = User.query.filter_by(username=username).first() if user and user.check_password(password): login_user(user) - flash('Zalogowano pomyślnie', 'success') - next_page = request.args.get('next') - return redirect(next_page) if next_page else redirect(url_for('admin_dashboard')) + flash("Zalogowano pomyślnie", "success") + next_page = request.args.get("next") + return ( + redirect(next_page) + if next_page + else redirect(url_for("admin_dashboard")) + ) else: - flash('Nieprawidłowe dane logowania', 'danger') - return render_template('login.html') + flash("Nieprawidłowe dane logowania", "danger") + return render_template("login.html") -@app.route('/logout') +@app.route("/wyloguj") @login_required -def logout(): +def wyloguj(): logout_user() - flash('Wylogowano', 'success') - return redirect(url_for('login')) + flash("Wylogowano", "success") + return redirect(url_for("zaloguj")) -@app.route('/register', methods=['GET', 'POST']) -def register(): - if not app.config.get('ALLOW_REGISTRATION', False): - flash('Rejestracja została wyłączona przez administratora', 'danger') - return redirect(url_for('login')) - if request.method == 'POST': - username = request.form['username'] - password = request.form['password'] + +@app.route("/zarejestruj", methods=["GET", "POST"]) +def zarejestruj(): + if not app.config.get("ALLOW_REGISTRATION", False): + flash("Rejestracja została wyłączona przez administratora", "danger") + return redirect(url_for("zaloguj")) + if request.method == "POST": + username = request.form["username"] + password = request.form["password"] if User.query.filter_by(username=username).first(): - flash('Użytkownik już istnieje', 'danger') - return redirect(url_for('register')) + flash("Użytkownik już istnieje", "danger") + return redirect(url_for("register")) new_user = User(username=username) new_user.set_password(password) db.session.add(new_user) db.session.commit() - flash('Konto utworzone, możesz się zalogować', 'success') - return redirect(url_for('login')) - return render_template('register.html') + flash("Konto utworzone, możesz się zalogować", "success") + return redirect(url_for("zaloguj")) + return render_template("register.html") + # PANEL ADMINISTRACYJNY -@app.route('/admin') + +@app.route("/admin") @login_required def admin_dashboard(): if not current_user.is_admin: - flash('Brak uprawnień do panelu administracyjnego', 'danger') - return redirect(url_for('index')) + flash("Brak uprawnień do panelu administracyjnego", "danger") + return redirect(url_for("index")) active_zbiorki = Zbiorka.query.filter_by(zrealizowana=False).all() completed_zbiorki = Zbiorka.query.filter_by(zrealizowana=True).all() - return render_template('admin/dashboard.html', active_zbiorki=active_zbiorki, - completed_zbiorki=completed_zbiorki) + return render_template( + "admin/dashboard.html", + active_zbiorki=active_zbiorki, + completed_zbiorki=completed_zbiorki, + ) -@app.route('/admin/zbiorka/dodaj', methods=['GET', 'POST']) + +@app.route("/admin/zbiorka/dodaj", methods=["GET", "POST"]) @login_required -def dodaj_zbiorka(): +def dodaj_zbiorke(): if not current_user.is_admin: - flash('Brak uprawnień', 'danger') - return redirect(url_for('index')) - + flash("Brak uprawnień", "danger") + return redirect(url_for("index")) + global_settings = GlobalSettings.query.first() # Pobieramy globalne ustawienia - - if request.method == 'POST': - nazwa = request.form['nazwa'] - opis = request.form['opis'] + + if request.method == "POST": + nazwa = request.form["nazwa"] + opis = request.form["opis"] # Pozyskujemy numer konta i telefon z formularza (mogą być nadpisane ręcznie) - numer_konta = request.form['numer_konta'] - numer_telefonu_blik = request.form['numer_telefonu_blik'] - cel = float(request.form['cel']) - ukryj_kwote = 'ukryj_kwote' in request.form - + numer_konta = request.form["numer_konta"] + numer_telefonu_blik = request.form["numer_telefonu_blik"] + cel = float(request.form["cel"]) + ukryj_kwote = "ukryj_kwote" in request.form + nowa_zbiorka = Zbiorka( - nazwa=nazwa, - opis=opis, + nazwa=nazwa, + opis=opis, numer_konta=numer_konta, numer_telefonu_blik=numer_telefonu_blik, cel=cel, - ukryj_kwote=ukryj_kwote + ukryj_kwote=ukryj_kwote, ) db.session.add(nowa_zbiorka) db.session.commit() - flash('Zbiórka została dodana', 'success') - return redirect(url_for('admin_dashboard')) + flash("Zbiórka została dodana", "success") + return redirect(url_for("admin_dashboard")) - return render_template('admin/add_zbiorka.html', global_settings=global_settings) + return render_template("admin/dodaj_zbiorke.html", global_settings=global_settings) -@app.route('/admin/zbiorka/edytuj/', methods=['GET', 'POST']) + +@app.route("/admin/zbiorka/edytuj/", methods=["GET", "POST"]) @login_required def edytuj_zbiorka(zbiorka_id): if not current_user.is_admin: - flash('Brak uprawnień', 'danger') - return redirect(url_for('index')) + flash("Brak uprawnień", "danger") + return redirect(url_for("index")) zb = Zbiorka.query.get_or_404(zbiorka_id) global_settings = GlobalSettings.query.first() # Pobieramy globalne ustawienia - if request.method == 'POST': - zb.nazwa = request.form['nazwa'] - zb.opis = request.form['opis'] - zb.numer_konta = request.form['numer_konta'] - zb.numer_telefonu_blik = request.form['numer_telefonu_blik'] + if request.method == "POST": + zb.nazwa = request.form["nazwa"] + zb.opis = request.form["opis"] + zb.numer_konta = request.form["numer_konta"] + zb.numer_telefonu_blik = request.form["numer_telefonu_blik"] try: - zb.cel = float(request.form['cel']) + zb.cel = float(request.form["cel"]) except ValueError: - flash('Podano nieprawidłową wartość dla celu zbiórki', 'danger') - return render_template('admin/edit_zbiorka.html', zbiorka=zb, global_settings=global_settings) - zb.ukryj_kwote = 'ukryj_kwote' in request.form + flash("Podano nieprawidłową wartość dla celu zbiórki", "danger") + return render_template( + "admin/edytuj_zbiorke.html", zbiorka=zb, global_settings=global_settings + ) + zb.ukryj_kwote = "ukryj_kwote" in request.form db.session.commit() - flash('Zbiórka została zaktualizowana', 'success') - return redirect(url_for('admin_dashboard')) - return render_template('admin/edit_zbiorka.html', zbiorka=zb, global_settings=global_settings) + flash("Zbiórka została zaktualizowana", "success") + return redirect(url_for("admin_dashboard")) + return render_template( + "admin/edytuj_zbiorke.html", zbiorka=zb, global_settings=global_settings + ) + # TRASA DODAWANIA WPŁATY Z OPISEM # TRASA DODAWANIA WPŁATY W PANELU ADMINA -@app.route('/admin/zbiorka//wplata/dodaj', methods=['GET', 'POST']) +@app.route("/admin/zbiorka//wplata/dodaj", methods=["GET", "POST"]) @login_required -def admin_dodaj_wplate(zbiorka_id): +def dodaj_wplate(zbiorka_id): if not current_user.is_admin: - flash('Brak uprawnień', 'danger') - return redirect(url_for('index')) + 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']) - opis = request.form.get('opis', '') + if request.method == "POST": + kwota = float(request.form["kwota"]) + opis = request.form.get("opis", "") nowa_wplata = Wplata(zbiorka_id=zb.id, kwota=kwota, opis=opis) zb.stan += kwota # Aktualizacja stanu zbiórki db.session.add(nowa_wplata) db.session.commit() - flash('Wpłata została dodana', 'success') - return redirect(url_for('admin_dashboard')) - return render_template('admin/add_wplata.html', zbiorka=zb) + flash("Wpłata została dodana", "success") + return redirect(url_for("admin_dashboard")) + return render_template("admin/dodaj_wplate.html", zbiorka=zb) -@app.route('/admin/zbiorka/usun/', methods=['POST']) + +@app.route("/admin/zbiorka/usun/", methods=["POST"]) @login_required def usun_zbiorka(zbiorka_id): if not current_user.is_admin: - flash('Brak uprawnień', 'danger') - return redirect(url_for('index')) + flash("Brak uprawnień", "danger") + return redirect(url_for("index")) zb = Zbiorka.query.get_or_404(zbiorka_id) db.session.delete(zb) db.session.commit() - flash('Zbiórka została usunięta', 'success') - return redirect(url_for('admin_dashboard')) + flash("Zbiórka została usunięta", "success") + return redirect(url_for("admin_dashboard")) -@app.route('/admin/zbiorka/edytuj_stan/', methods=['GET', 'POST']) + +@app.route("/admin/zbiorka/edytuj_stan/", methods=["GET", "POST"]) @login_required def edytuj_stan(zbiorka_id): if not current_user.is_admin: - flash('Brak uprawnień', 'danger') - return redirect(url_for('index')) + flash("Brak uprawnień", "danger") + return redirect(url_for("index")) zb = Zbiorka.query.get_or_404(zbiorka_id) - if request.method == 'POST': + if request.method == "POST": try: - nowy_stan = float(request.form['stan']) + nowy_stan = float(request.form["stan"]) except ValueError: - flash('Nieprawidłowa wartość kwoty', 'danger') - return redirect(url_for('edytuj_stan', zbiorka_id=zbiorka_id)) + flash("Nieprawidłowa wartość kwoty", "danger") + return redirect(url_for("edytuj_stan", zbiorka_id=zbiorka_id)) zb.stan = nowy_stan db.session.commit() - flash('Stan zbiórki został zaktualizowany', 'success') - return redirect(url_for('admin_dashboard')) - return render_template('admin/edytuj_stan.html', zbiorka=zb) + flash("Stan zbiórki został zaktualizowany", "success") + return redirect(url_for("admin_dashboard")) + return render_template("admin/edytuj_stan.html", zbiorka=zb) -@app.route('/admin/zbiorka/toggle_visibility/', methods=['POST']) + +@app.route("/admin/zbiorka/zmien_widzialnosc/", methods=["POST"]) @login_required -def toggle_visibility(zbiorka_id): +def zmien_widzialnosc(zbiorka_id): if not current_user.is_admin: - flash('Brak uprawnień', 'danger') - return redirect(url_for('index')) + flash("Brak uprawnień", "danger") + return redirect(url_for("index")) zb = Zbiorka.query.get_or_404(zbiorka_id) zb.ukryta = not zb.ukryta db.session.commit() - flash('Zbiórka została ' + ('ukryta' if zb.ukryta else 'przywrócona'), 'success') - return redirect(url_for('admin_dashboard')) + flash("Zbiórka została " + ("ukryta" if zb.ukryta else "przywrócona"), "success") + return redirect(url_for("admin_dashboard")) + def create_admin_account(): admin = User.query.filter_by(is_admin=True).first() if not admin: - main_admin = User(username=app.config['MAIN_ADMIN_USERNAME'], is_admin=True) - main_admin.set_password(app.config['MAIN_ADMIN_PASSWORD']) + main_admin = User(username=app.config["MAIN_ADMIN_USERNAME"], is_admin=True) + main_admin.set_password(app.config["MAIN_ADMIN_PASSWORD"]) db.session.add(main_admin) db.session.commit() @@ -362,7 +442,9 @@ def apply_headers(response): response.headers.pop("Vary", None) elif request.path.startswith("/admin"): response.headers.pop("Vary", None) - response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0" + response.headers["Cache-Control"] = ( + "no-store, no-cache, must-revalidate, max-age=0" + ) else: response.headers["Vary"] = "Cookie, Accept-Encoding" default_cache = app.config.get("CACHE_CONTROL_HEADER") or "private, max-age=0" @@ -370,75 +452,96 @@ def apply_headers(response): # Blokowanie botów if app.config.get("BLOCK_BOTS", False): - cc = app.config.get("CACHE_CONTROL_HEADER") or "no-store, no-cache, must-revalidate, max-age=0" + cc = ( + app.config.get("CACHE_CONTROL_HEADER") + or "no-store, no-cache, must-revalidate, max-age=0" + ) response.headers["Cache-Control"] = cc - response.headers["X-Robots-Tag"] = app.config.get("ROBOTS_TAG") or "noindex, nofollow, nosnippet, noarchive" + response.headers["X-Robots-Tag"] = ( + app.config.get("ROBOTS_TAG") or "noindex, nofollow, nosnippet, noarchive" + ) return response -@app.route('/admin/settings', methods=['GET', 'POST']) +@app.route("/admin/ustawienia", methods=["GET", "POST"]) @login_required -def admin_settings(): +def admin_ustawienia(): if not current_user.is_admin: - flash('Brak uprawnień do panelu administracyjnego', 'danger') - return redirect(url_for('index')) - + flash("Brak uprawnień do panelu administracyjnego", "danger") + return redirect(url_for("index")) + client_ip = get_real_ip() settings = GlobalSettings.query.first() - if request.method == 'POST': - numer_konta = request.form.get('numer_konta') - numer_telefonu_blik = request.form.get('numer_telefonu_blik') - allowed_login_hosts = request.form.get('allowed_login_hosts') - - if settings is None: - settings = GlobalSettings( - numer_konta=numer_konta, - numer_telefonu_blik=numer_telefonu_blik, - allowed_login_hosts=allowed_login_hosts - ) - db.session.add(settings) - else: - settings.numer_konta = numer_konta - settings.numer_telefonu_blik = numer_telefonu_blik - settings.allowed_login_hosts = allowed_login_hosts - - db.session.commit() - flash('Ustawienia globalne zostały zaktualizowane', 'success') - return redirect(url_for('admin_dashboard')) - - return render_template('admin/settings.html', settings=settings, client_ip=client_ip) + if request.method == "POST": + numer_konta = request.form.get("numer_konta") + numer_telefonu_blik = request.form.get("numer_telefonu_blik") + allowed_login_hosts = request.form.get("allowed_login_hosts") + logo_url = request.form.get("logo_url") + site_title = request.form.get("site_title") + show_logo_in_navbar = "show_logo_in_navbar" in request.form -@app.route('/admin/zbiorka/oznacz/', methods=['POST']) + if settings is None: + settings = GlobalSettings( + numer_konta=numer_konta, + numer_telefonu_blik=numer_telefonu_blik, + allowed_login_hosts=allowed_login_hosts, + logo_url=logo_url, + site_title=site_title, + show_logo_in_navbar=show_logo_in_navbar, + ) + db.session.add(settings) + else: + settings.numer_konta = numer_konta + settings.numer_telefonu_blik = numer_telefonu_blik + settings.allowed_login_hosts = allowed_login_hosts + settings.logo_url = logo_url + settings.site_title = site_title + settings.show_logo_in_navbar = show_logo_in_navbar + + db.session.commit() + flash("Ustawienia globalne zostały zaktualizowane", "success") + return redirect(url_for("admin_dashboard")) + + return render_template( + "admin/ustawienia.html", settings=settings, client_ip=client_ip + ) + + +@app.route("/admin/zbiorka/oznacz/", methods=["POST"]) @login_required def oznacz_zbiorka(zbiorka_id): if not current_user.is_admin: - flash('Brak uprawnień do wykonania tej operacji', 'danger') - return redirect(url_for('index')) + flash("Brak uprawnień do wykonania tej operacji", "danger") + return redirect(url_for("index")) zb = Zbiorka.query.get_or_404(zbiorka_id) zb.zrealizowana = True db.session.commit() - flash('Zbiórka została oznaczona jako zrealizowana', 'success') - return redirect(url_for('admin_dashboard')) + flash("Zbiórka została oznaczona jako zrealizowana", "success") + return redirect(url_for("admin_dashboard")) -@app.route('/robots.txt') + +@app.route("/robots.txt") def robots(): if app.config.get("BLOCK_BOTS", False): - # Instrukcje dla robotów – blokujemy indeksowanie całej witryny robots_txt = "User-agent: *\nDisallow: /" else: - # Jeśli blokowanie botów wyłączone, można zwrócić pusty plik lub inne ustawienia robots_txt = "User-agent: *\nAllow: /" - return robots_txt, 200, {'Content-Type': 'text/plain'} + return robots_txt, 200, {"Content-Type": "text/plain"} -if __name__ == '__main__': +@app.route("/favicon.ico") +def favicon(): + return "", 204 + + +if __name__ == "__main__": with app.app_context(): db.create_all() # Tworzenie konta głównego admina, jeśli nie istnieje if not User.query.filter_by(is_admin=True).first(): - main_admin = User(username=app.config['MAIN_ADMIN_USERNAME'], is_admin=True) - main_admin.set_password(app.config['MAIN_ADMIN_PASSWORD']) + main_admin = User(username=app.config["MAIN_ADMIN_USERNAME"], is_admin=True) + main_admin.set_password(app.config["MAIN_ADMIN_PASSWORD"]) db.session.add(main_admin) db.session.commit() app.run(debug=True) diff --git a/config.example.py b/config.example.py deleted file mode 100644 index c7347ab..0000000 --- a/config.example.py +++ /dev/null @@ -1,14 +0,0 @@ -# config.py - -class Config: - SQLALCHEMY_DATABASE_URI = 'sqlite:///baza.db' - SECRET_KEY = 'tajny_klucz' - - # Konfiguracja rejestracji i admina - ALLOW_REGISTRATION = False - MAIN_ADMIN_USERNAME = 'admin' - MAIN_ADMIN_PASSWORD = 'admin' - # Konfiguracja ochrony przed indeksowaniem - BLOCK_BOTS = True - CACHE_CONTROL_HEADER = "max-age=10" - ROBOTS_TAG = "noindex, nofollow, nosnippet, noarchive" \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..760e65c --- /dev/null +++ b/config.py @@ -0,0 +1,45 @@ +import os + +def _get_bool(name: str, default: bool) -> bool: + val = os.environ.get(name) + if val is None: + return default + return str(val).strip().lower() in {"1", "true", "t", "yes", "y", "on"} + +def _get_str(name: str, default: str) -> str: + return os.environ.get(name, default) + +class Config: + """ + Konfiguracja aplikacji pobierana z ENV (z sensownymi domyślnymi wartościami). + Zmiennych szukamy pod nazwami: + - DATABASE_URL + - SECRET_KEY + - ALLOW_REGISTRATION + - MAIN_ADMIN_USERNAME + - MAIN_ADMIN_PASSWORD + - BLOCK_BOTS + - CACHE_CONTROL_HEADER + - PRAGMA_HEADER + - ROBOTS_TAG + """ + + # Baza danych + SQLALCHEMY_DATABASE_URI = _get_str("DATABASE_URL", "sqlite:///baza.db") + + # Flask + SECRET_KEY = _get_str("SECRET_KEY", "tajny_klucz") + + # Rejestracja i konto admina + ALLOW_REGISTRATION = _get_bool("ALLOW_REGISTRATION", False) + MAIN_ADMIN_USERNAME = _get_str("MAIN_ADMIN_USERNAME", "admin") + MAIN_ADMIN_PASSWORD = _get_str("MAIN_ADMIN_PASSWORD", "admin") + + # Indeksowanie / cache / robots + BLOCK_BOTS = _get_bool("BLOCK_BOTS", True) + CACHE_CONTROL_HEADER = _get_str("CACHE_CONTROL_HEADER", "max-age=600") + PRAGMA_HEADER = _get_str("PRAGMA_HEADER", "") + ROBOTS_TAG = _get_str("ROBOTS_TAG", "noindex, nofollow, nosnippet, noarchive") + + # (opcjonalnie) wyłącz warningi track_modifications + SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/docker-compose.yml b/docker-compose.yml index dc81b1b..9f556fa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,12 +1,10 @@ -version: '3.8' - services: app: build: context: . dockerfile: Dockerfile ports: - - "8080:8080" + - "${APP_PORT:-8080}:8080" volumes: - ./instance:/app/instance restart: unless-stopped diff --git a/emergency_access.txt b/emergency_access.txt new file mode 100644 index 0000000..8788ada --- /dev/null +++ b/emergency_access.txt @@ -0,0 +1,3 @@ +Jeśli ten plik istwnieje w katalogu apliakcji, to wylacza zebzpieczenie logowania do panelu admina z ograniczeniem IP. + +Musi miec rozszerzenie .txt \ No newline at end of file diff --git a/run_waitress.py b/run_waitress.py index 46d8c65..c34e271 100644 --- a/run_waitress.py +++ b/run_waitress.py @@ -1,3 +1,4 @@ +import os from app import app, db, create_admin_account from waitress import serve @@ -5,4 +6,6 @@ if __name__ == '__main__': with app.app_context(): db.create_all() create_admin_account() - serve(app, host='0.0.0.0', port=8080) \ No newline at end of file + + port = int(os.environ.get("APP_PORT", 8080)) + serve(app, host="0.0.0.0", port=port) \ No newline at end of file diff --git a/static/css/custom.css b/static/css/custom.css index 8d65594..2a4dcfc 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,168 +1,295 @@ -/* Import czcionki Roboto */ @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap'); -/* Globalne */ +/* ========= TOKENS ========= */ +:root { + color-scheme: dark; + + --bg: #121212; + /* główne tło */ + --surface-0: #1a1a1a; + /* navbar, header */ + --surface-1: #202020; + /* karty */ + --surface-2: #2a2a2a; + /* nagłówki kart, ciemniejsze sekcje */ + --border: #3a3a3a; + + --text: #e4e4e4; + --text-muted: #a8a8a8; + + --accent: #f5c84c; + /* żółty/amber akcent */ + --accent-600: #e3b23f; + --accent-700: #cfa033; + --accent-300: #ffe083; + + --radius: 10px; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, .5); + --shadow-md: 0 4px 12px rgba(0, 0, 0, .45); + --trans: 220ms cubic-bezier(.2, .8, .2, 1); +} + +/* ========= BASE ========= */ body { - font-family: 'Roboto', sans-serif; - background-color: #121212; - color: #dcdcdc; - padding-top: 1vh; + font-family: 'Roboto', system-ui, -apple-system, Segoe UI, Arial, sans-serif; + background: var(--bg); + color: var(--text); margin: 0; + padding-top: 1vh; } -/* Nawigacja */ -.navbar { - background-color: #1c1c1c; - border-bottom: 1px solid #444; - transition: background-color 0.3s ease; -} - -.navbar-brand { - color: #f5f5f5; - font-weight: bold; - transition: color 0.3s ease; -} - -.nav-link { - color: #cccccc; - transition: color 0.3s ease; -} - -.nav-link:hover { - color: #ffc107; -} - -/* Karty */ -.card { - background-color: #444242; - border: none; - border-radius: 0.5rem; - box-shadow: 0px 0px 8px rgba(0, 0, 0, 0.6); - margin-bottom: 20px; - transition: transform 0.2s ease, box-shadow 0.2s ease; -} - -.card:hover { - transform: translateY(-5px); - box-shadow: 0px 3px 16px rgba(0, 0, 0, 0.8); -} - -.card-header { - background-color: #272727; - border-bottom: 1px solid #444; - font-weight: bold; -} - -.card-body { - background-color: #444242; -} - -/* Przyciski */ -.btn { - text-transform: uppercase; - font-weight: bold; - transition: background-color 0.3s ease, transform 0.2s ease; -} - -.btn:hover { - transform: translateY(-2px); -} - -.btn-primary { - background-color: #2d2c2c; - border-color: #ffeb3b; - color: #ffffff; -} - -.btn-primary:hover { - background-color: #1e1e1e; - border-color: #ffc107; -} - -/* Linki */ a { - color: #ffc107; - transition: color 0.3s ease; + color: var(--accent); + text-decoration: none; + transition: color var(--trans); } a:hover { - color: #ffeb3b; + color: var(--accent-300); } -/* Progress Bar */ -.progress { - background-color: #2a2a2a; - border-radius: 0.5rem; - height: 35px; - font-size: 1.2rem; +/* ========= NAVBAR ========= */ +.navbar { + background: var(--surface-0); + border-bottom: 1px solid var(--border); } -.progress-bar { - background: linear-gradient(90deg, #ffc107, #ffeb3b); - font-weight: bold; - transition: width 0.3s ease; - animation: progressAnimation 1s ease-in-out forwards; +.navbar-brand { + color: var(--text); + font-weight: 700; + transition: color var(--trans); } -@keyframes progressAnimation { - from { width: 0%; } - to { width: var(--progress-width); } +.navbar-brand:hover { + color: var(--accent); } -/* Alerty (flash messages) */ -.alert { - opacity: 0; - animation: fadeIn 0.5s forwards; - margin-bottom: 1rem; +.nav-link { + color: var(--text-muted); + transition: color var(--trans); } -@keyframes fadeIn { - to { opacity: 1; } +.nav-link:hover, +.nav-link:focus { + color: var(--accent); } -/* Dodatkowe marginesy */ -.container { - padding: 0 15px; +/* ========= CARDS ========= */ +.card { + background: var(--surface-1); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow-sm); + margin-bottom: 20px; + transition: transform 160ms ease, box-shadow 160ms ease, border-color var(--trans); } -/* Responsywność */ -@media (max-width: 767px) { - .card { - margin-bottom: 1rem; - } - .card-title { - font-size: 1.25rem; - } - .btn { - font-size: 0.9rem; - } +.card:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); + border-color: color-mix(in srgb, var(--accent) 20%, var(--border)); } -h1 { font-size: 2rem; margin-bottom: 1rem; } -h2 { font-size: 1.7rem; margin-bottom: 0.8rem; } -h3 { font-size: 0.9rem; margin-bottom: 0.6rem; } +.card-header { + background: var(--surface-2); + border-bottom: 1px solid var(--border); + font-weight: 700; +} -/* Wspomóż */ +.card-body { + background: transparent; +} + +/* Wyróżniona karta */ .card.wspomoz-card { - border: 1px solid #ffc107 !important; - border-radius: 0.2rem !important; + border: 1px solid var(--accent) !important; + border-radius: var(--radius) !important; + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 20%, transparent); } .card.wspomoz-card .card-body, .card.wspomoz-card .card-title, .card.wspomoz-card .card-text { - color: #ffffff !important; - font-weight: bold; - font-size: 1.25rem; + color: var(--text) !important; + font-weight: 700; + font-size: 1.15rem; } -.btn-primary:focus, -.btn-primary:active, -.btn-primary.focus, -.btn-primary.active { - background-color: #2d2c2c !important; - border-color: #ffeb3b !important; - color: #ffffff !important; - box-shadow: none; +/* ========= BUTTONS ========= */ +.btn { + font-weight: 700; + border-radius: 8px; + transition: transform 120ms ease, background-color var(--trans), border-color var(--trans), color var(--trans); +} + +.btn:hover { + transform: translateY(-1px); +} + +.btn:focus-visible { + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 40%, transparent); outline: none; +} + +/* Primary = żółty */ +.btn-primary { + background-color: var(--accent); + border-color: var(--accent-600); + color: #111; +} + +.btn-primary:hover { + background-color: var(--accent-600); + border-color: var(--accent-700); + color: #111; +} + +.btn-primary:active, +.btn-primary:focus { + background-color: var(--accent-700) !important; + border-color: var(--accent-700) !important; + color: #111 !important; +} + +/* Secondary = ciemna szarość */ +.btn-secondary, +.btn-outline-primary { + background: var(--surface-1); + border: 1px solid var(--border); + color: var(--text); +} + +.btn-secondary:hover, +.btn-outline-primary:hover { + border-color: var(--accent-600); + color: var(--accent); +} + +/* ========= PROGRESS ========= */ +.progress { + background: var(--surface-2); + border-radius: 999px; + height: 14px; + overflow: hidden; + border: 1px solid var(--border); +} + +.progress-bar { + --progress-width: 0%; + width: var(--progress-width); + background: linear-gradient(90deg, var(--accent-600), var(--accent)); + transition: width var(--trans); +} + +/* ========= ALERTS ========= */ + + +/* ALERT VARIANTS */ +.alert-success { + background: rgba(40, 167, 69, 0.15); + /* lekka zieleń */ + border-color: #28a745; + color: #28a745; +} + +.alert-danger { + background: rgba(220, 53, 69, 0.15); + /* lekka czerwień */ + border-color: #dc3545; + color: #dc3545; +} + +.alert-warning { + background: rgba(255, 193, 7, 0.15); + /* lekki bursztyn */ + border-color: #ffc107; + color: #ffc107; +} + +.alert-info { + background: rgba(23, 162, 184, 0.15); + /* lekki cyjan */ + border-color: #17a2b8; + color: #17a2b8; +} + + +@keyframes fadeIn { + to { + opacity: 1; + } +} + +/* ========= TYPO ========= */ +h1 { + font-size: 2rem; + margin-bottom: .75rem; +} + +h2 { + font-size: 1.6rem; + margin-bottom: .6rem; +} + +h3 { + font-size: 1.1rem; + margin-bottom: .5rem; + color: var(--text-muted); +} + +small, +.text-muted { + color: var(--text-muted) !important; +} + +/* ========= RESPONSIVE ========= */ +.container { + padding: 0 15px; +} + +@media (max-width: 767px) { + .card { + margin-bottom: 1rem; + } + + .card-title { + font-size: 1.1rem; + } + + .btn { + font-size: .95rem; + } +} + +.table-responsive { + overflow: visible; +} + +.dropdown-menu { + z-index: 1080; +} + +/* ponad kartą/tabelą */ +@media (max-width: 576px) { + .table-responsive { + overflow-x: auto; + } +} + +/* ========= FORMS ========= */ +input.form-control, +textarea.form-control, +select.form-select { + background-color: var(--surface-1); + border: 1px solid var(--border); + color: var(--text); +} + +input.form-control:focus, +textarea.form-control:focus, +select.form-select:focus { + background-color: var(--surface-1); + border-color: var(--accent-600); + color: var(--text); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 50%, transparent); } \ No newline at end of file diff --git a/static/js/admin_dashboard.js b/static/js/admin_dashboard.js new file mode 100644 index 0000000..e69de29 diff --git a/static/js/dodaj_wplate.js b/static/js/dodaj_wplate.js new file mode 100644 index 0000000..b2173b3 --- /dev/null +++ b/static/js/dodaj_wplate.js @@ -0,0 +1,21 @@ +(function () { + const kwota = document.getElementById('kwota'); + const opis = document.getElementById('opis'); + const opisCount = document.getElementById('opisCount'); + + document.querySelectorAll('.btn-kwota').forEach(btn => { + btn.addEventListener('click', () => { + const val = btn.getAttribute('data-amount'); + if (val && kwota) { + kwota.value = Number(val).toFixed(2); + kwota.focus(); + } + }); + }); + + if (opis && opisCount) { + const updateCount = () => opisCount.textContent = opis.value.length.toString(); + opis.addEventListener('input', updateCount); + updateCount(); + } +})(); \ No newline at end of file diff --git a/static/js/dodaj_zbiorke.js b/static/js/dodaj_zbiorke.js new file mode 100644 index 0000000..b36c5c0 --- /dev/null +++ b/static/js/dodaj_zbiorke.js @@ -0,0 +1,37 @@ +(function () { + const opis = document.getElementById('opis'); + const opisCount = document.getElementById('opisCount'); + if (opis && opisCount) { + const updateCount = () => opisCount.textContent = opis.value.length.toString(); + opis.addEventListener('input', updateCount); + updateCount(); + } + + const iban = document.getElementById('numer_konta'); + if (iban) { + iban.addEventListener('input', () => { + const digits = iban.value.replace(/\D/g, '').slice(0, 26); + const chunked = digits.replace(/(.{4})/g, '$1 ').trim(); + iban.value = chunked; + }); + } + + const tel = document.getElementById('numer_telefonu_blik'); + if (tel) { + tel.addEventListener('input', () => { + const digits = tel.value.replace(/\D/g, '').slice(0, 9); + const parts = []; + if (digits.length > 0) parts.push(digits.substring(0, 3)); + if (digits.length > 3) parts.push(digits.substring(3, 6)); + if (digits.length > 6) parts.push(digits.substring(6, 9)); + tel.value = parts.join(' '); + }); + } + + const cel = document.getElementById('cel'); + if (cel) { + cel.addEventListener('change', () => { + if (cel.value && Number(cel.value) < 0.01) cel.value = '0.01'; + }); + } +})(); \ No newline at end of file diff --git a/static/js/edytuj_stan.js b/static/js/edytuj_stan.js new file mode 100644 index 0000000..8170509 --- /dev/null +++ b/static/js/edytuj_stan.js @@ -0,0 +1,82 @@ +(() => { + // Root kontenera z danymi (dataset.cel) + const root = document.querySelector('[data-module="edit-stan"]'); + if (!root) return; + + const input = root.querySelector('#stan'); + const previewPct = root.querySelector('#previewPct'); + const previewBar = root.querySelector('#previewBar'); + const previewNote = root.querySelector('#previewNote'); + + // Cel przekazany jako data atrybut + const cel = Number(root.dataset.cel || 0); + + function clamp(n) { + if (Number.isNaN(n)) return 0; + return n < 0 ? 0 : n; + } + + function pct(val) { + if (!cel || cel <= 0) return 0; + return (val / cel) * 100; + } + + function updatePreview() { + if (!input) return; + const val = clamp(Number(input.value)); + const p = Math.max(0, Math.min(100, pct(val))); + + if (previewPct) previewPct.textContent = pct(val).toFixed(1); + if (previewBar) previewBar.style.setProperty('--progress-width', p + '%'); + + if (previewNote) { + if (cel > 0) { + const diff = cel - val; + if (diff > 0) { + previewNote.textContent = 'Do celu brakuje: ' + diff.toFixed(2) + ' PLN'; + } else if (diff === 0) { + previewNote.textContent = 'Cel osiągnięty.'; + } else { + previewNote.textContent = 'Przekroczono cel o: ' + Math.abs(diff).toFixed(2) + ' PLN'; + } + } else { + previewNote.textContent = 'Brak zdefiniowanego celu — procent nie jest wyliczany.'; + } + } + } + + // Zmiana ręczna + if (input) { + input.addEventListener('input', updatePreview); + input.addEventListener('change', () => { + if (Number(input.value) < 0) input.value = '0.00'; + updatePreview(); + }); + } + + // Przyciski +/- delta + root.querySelectorAll('.btn-delta').forEach(btn => { + btn.addEventListener('click', () => { + const d = Number(btn.getAttribute('data-delta') || 0); + const cur = Number(input?.value || 0); + if (!input) return; + input.value = clamp(cur + d).toFixed(2); + updatePreview(); + input.focus(); + }); + }); + + // Ustaw na konkretną wartość + root.querySelectorAll('.btn-set').forEach(btn => { + btn.addEventListener('click', () => { + const v = Number(btn.getAttribute('data-value') || 0); + if (!input) return; + input.value = clamp(v).toFixed(2); + updatePreview(); + input.focus(); + }); + }); + + // Inicjalny podgląd + updatePreview(); +})(); diff --git a/static/js/edytuj_zbiorke.js b/static/js/edytuj_zbiorke.js new file mode 100644 index 0000000..eb35f9e --- /dev/null +++ b/static/js/edytuj_zbiorke.js @@ -0,0 +1,60 @@ +(function () { + // Licznik znaków opisu + const opis = document.getElementById('opis'); + const opisCount = document.getElementById('opisCount'); + if (opis && opisCount) { + const updateCount = () => opisCount.textContent = opis.value.length.toString(); + opis.addEventListener('input', updateCount); + updateCount(); + } + + // IBAN: tylko cyfry, auto-grupowanie co 4 + const iban = document.getElementById('numer_konta'); + if (iban) { + iban.addEventListener('input', () => { + const digits = iban.value.replace(/\D/g, '').slice(0, 26); // 26 cyfr po "PL" + const chunked = digits.replace(/(.{4})/g, '$1 ').trim(); + iban.value = chunked; + }); + } + + // BLIK telefon: tylko cyfry, format 3-3-3 + const tel = document.getElementById('numer_telefonu_blik'); + if (tel) { + tel.addEventListener('input', () => { + const digits = tel.value.replace(/\D/g, '').slice(0, 9); + const parts = []; + if (digits.length > 0) parts.push(digits.substring(0, 3)); + if (digits.length > 3) parts.push(digits.substring(3, 6)); + if (digits.length > 6) parts.push(digits.substring(6, 9)); + tel.value = parts.join(' '); + }); + } + + // „Ustaw globalne” z data-atrybutów (bez wstrzykiwania wartości w JS) + const setGlobalBtn = document.getElementById('ustaw-globalne'); + if (setGlobalBtn && iban && tel) { + setGlobalBtn.addEventListener('click', () => { + const gIban = setGlobalBtn.dataset.iban || ''; + const gBlik = setGlobalBtn.dataset.blik || ''; + if (gIban) { + iban.value = gIban.replace(/\D/g, '').replace(/(.{4})/g, '$1 ').trim(); + } + if (gBlik) { + const d = gBlik.replace(/\D/g, '').slice(0, 9); + const p = [d.slice(0, 3), d.slice(3, 6), d.slice(6, 9)].filter(Boolean).join(' '); + tel.value = p; + } + iban.dispatchEvent(new Event('input')); + tel.dispatchEvent(new Event('input')); + }); + } + + // Cel: minimalna wartość + const cel = document.getElementById('cel'); + if (cel) { + cel.addEventListener('change', () => { + if (cel.value && Number(cel.value) < 0.01) cel.value = '0.01'; + }); + } +})(); \ No newline at end of file diff --git a/static/js/mde_custom.js b/static/js/mde_custom.js new file mode 100644 index 0000000..75b25f6 --- /dev/null +++ b/static/js/mde_custom.js @@ -0,0 +1,4 @@ +var simplemde = new SimpleMDE({ + element: document.getElementById("opis"), + forceSync: true +}); \ No newline at end of file diff --git a/static/js/progress.js b/static/js/progress.js new file mode 100644 index 0000000..0cf3e9d --- /dev/null +++ b/static/js/progress.js @@ -0,0 +1,13 @@ +function animateProgressBars() { + document.querySelectorAll('.progress-bar').forEach(bar => { + const progressValue = bar.getAttribute('aria-valuenow'); + bar.style.setProperty('--progress-width', progressBarWidth(progressBarValue(progressBar))); + }); +} + +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('.progress-bar').forEach(bar => { + const width = bar.getAttribute('aria-valuenow') + '%'; + bar.style.setProperty('--progress-width', width); + }); +}); \ No newline at end of file diff --git a/static/js/ustawienia.js b/static/js/ustawienia.js new file mode 100644 index 0000000..cd98bc1 --- /dev/null +++ b/static/js/ustawienia.js @@ -0,0 +1,92 @@ +(function () { + // IBAN: tylko cyfry, auto-grupowanie co 4 (po prefiksie PL) + const iban = document.getElementById('numer_konta'); + if (iban) { + iban.addEventListener('input', () => { + const digits = iban.value.replace(/\\D/g, '').slice(0, 26); // 26 cyfr po "PL" + const chunked = digits.replace(/(.{4})/g, '$1 ').trim(); + iban.value = chunked; + }); + } + + // Telefon BLIK: tylko cyfry, format 3-3-3 + const tel = document.getElementById('numer_telefonu_blik'); + if (tel) { + tel.addEventListener('input', () => { + const digits = tel.value.replace(/\\D/g, '').slice(0, 9); + const parts = []; + if (digits.length > 0) parts.push(digits.substring(0, 3)); + if (digits.length > 3) parts.push(digits.substring(3, 6)); + if (digits.length > 6) parts.push(digits.substring(6, 9)); + tel.value = parts.join(' '); + }); + } + + // Biała lista IP/hostów — helpery + const ta = document.getElementById('allowed_login_hosts'); + const count = document.getElementById('hostsCount'); + const addBtn = document.getElementById('btn-add-host'); + const addMyBtn = document.getElementById('btn-add-my-ip'); + const input = document.getElementById('host_input'); + + function parseList(text) { + // akceptuj przecinki, średniki i nowe linie; trimuj; usuń puste + return text + .split(/[\\n,;]+/) + .map(s => s.trim()) + .filter(Boolean); + } + function formatList(arr) { + return arr.join('\\n'); + } + function dedupe(arr) { + const seen = new Set(); + const out = []; + for (const v of arr) { + const k = v.toLowerCase(); + if (!seen.has(k)) { seen.add(k); out.push(v); } + } + return out; + } + function updateCount() { + if (!ta || !count) return; + count.textContent = parseList(ta.value).length.toString(); + } + function addEntry(val) { + if (!ta || !val) return; + const list = dedupe([...parseList(ta.value), val]); + ta.value = formatList(list); + updateCount(); + } + + if (ta) { + ta.addEventListener('input', updateCount); + // inicjalny przelicznik + updateCount(); + } + + if (addBtn && input) { + addBtn.addEventListener('click', () => { + const val = (input.value || '').trim(); + if (!val) return; + addEntry(val); + input.value = ''; + input.focus(); + }); + } + + if (addMyBtn) { + addMyBtn.addEventListener('click', () => { + const ip = addMyBtn.dataset.myIp || ''; + if (ip) addEntry(ip); + }); + } + + const dedupeBtn = document.getElementById('btn-dedupe'); + if (dedupeBtn && ta) { + dedupeBtn.addEventListener('click', () => { + ta.value = formatList(dedupe(parseList(ta.value))); + updateCount(); + }); + } +})(); \ No newline at end of file diff --git a/static/js/walidacja_logowanie.js b/static/js/walidacja_logowanie.js new file mode 100644 index 0000000..c7736eb --- /dev/null +++ b/static/js/walidacja_logowanie.js @@ -0,0 +1,27 @@ +(function () { + const form = document.querySelector('form.needs-validation'); + form.addEventListener('submit', function (e) { + if (!form.checkValidity()) { + e.preventDefault(); + e.stopPropagation(); + } + form.classList.add('was-validated'); + }, false); +})(); + +const pw = document.getElementById('password'); +const toggle = document.getElementById('togglePw'); +toggle.addEventListener('click', () => { + const isText = pw.type === 'text'; + pw.type = isText ? 'password' : 'text'; + toggle.textContent = isText ? 'Pokaż' : 'Ukryj'; + toggle.setAttribute('aria-pressed', (!isText).toString()); + pw.focus(); +}); +const caps = document.getElementById('capsWarning'); +function handleCaps(e) { + const capsOn = e.getModifierState && e.getModifierState('CapsLock'); + caps.style.display = capsOn ? 'inline' : 'none'; +} +pw.addEventListener('keyup', handleCaps); +pw.addEventListener('keydown', handleCaps); \ No newline at end of file diff --git a/static/js/walidacja_rejestracja.js b/static/js/walidacja_rejestracja.js new file mode 100644 index 0000000..389da00 --- /dev/null +++ b/static/js/walidacja_rejestracja.js @@ -0,0 +1,37 @@ +(function () { + const form = document.querySelector('form.needs-validation'); + form.addEventListener('submit', function (e) { + if (!form.checkValidity()) { + e.preventDefault(); + e.stopPropagation(); + } + const pw1 = document.getElementById('password'); + const pw2 = document.getElementById('password2'); + if (pw1.value !== pw2.value) { + e.preventDefault(); + e.stopPropagation(); + pw2.setCustomValidity("Hasła muszą być identyczne."); + pw2.reportValidity(); + } else { + pw2.setCustomValidity(""); + } + form.classList.add('was-validated'); + }, false); +})(); + +const pw = document.getElementById('password'); +const toggle = document.getElementById('togglePw'); +toggle.addEventListener('click', () => { + const isText = pw.type === 'text'; + pw.type = isText ? 'password' : 'text'; + toggle.textContent = isText ? 'Pokaż' : 'Ukryj'; + pw.focus(); +}); + +const caps = document.getElementById('capsWarning'); +function handleCaps(e) { + const capsOn = e.getModifierState && e.getModifierState('CapsLock'); + caps.style.display = capsOn ? 'inline' : 'none'; +} +pw.addEventListener('keyup', handleCaps); +pw.addEventListener('keydown', handleCaps); \ No newline at end of file diff --git a/static/js/zbiorka.js b/static/js/zbiorka.js new file mode 100644 index 0000000..f83e927 --- /dev/null +++ b/static/js/zbiorka.js @@ -0,0 +1,38 @@ +(function () { + const ibanEl = document.getElementById('ibanDisplay'); + if (ibanEl) { + const digits = (ibanEl.textContent || '').replace(/\s+/g, '').replace(/^PL/i, '').replace(/\D/g, '').slice(0, 26); + if (digits) ibanEl.textContent = 'PL ' + digits.replace(/(.{4})/g, '$1 ').trim(); + } + const blikEl = document.getElementById('blikDisplay'); + if (blikEl) { + const d = (blikEl.textContent || '').replace(/\D/g, '').slice(0, 9); + const parts = [d.slice(0, 3), d.slice(3, 6), d.slice(6, 9)].filter(Boolean).join(' '); + if (parts) blikEl.textContent = parts; + } + + document.querySelectorAll('[data-copy-target]').forEach(btn => { + btn.addEventListener('click', async () => { + const sel = btn.getAttribute('data-copy-target'); + const el = sel ? document.querySelector(sel) : null; + if (!el) return; + const raw = el.textContent.replace(/\u00A0/g, ' ').trim(); + try { + await navigator.clipboard.writeText(raw); + const original = btn.textContent; + btn.textContent = 'Skopiowano!'; + btn.disabled = true; + setTimeout(() => { btn.textContent = original; btn.disabled = false; }, 1200); + } catch { + // fallback + const r = document.createRange(); + r.selectNodeContents(el); + const selObj = window.getSelection(); + selObj.removeAllRanges(); + selObj.addRange(r); + try { document.execCommand('copy'); } catch { } + selObj.removeAllRanges(); + } + }); + }); +})(); \ No newline at end of file diff --git a/templates/admin/add_wplata.html b/templates/admin/add_wplata.html deleted file mode 100644 index f2a61ab..0000000 --- a/templates/admin/add_wplata.html +++ /dev/null @@ -1,24 +0,0 @@ -{% extends 'base.html' %} -{% block title %}Dodaj wpłatę{% endblock %} -{% block content %} -
-
-
-

Dodaj wpłatę do zbiórki: {{ zbiorka.nazwa }}

-
-
-
-
- - -
-
- - -
- -
-
-
-
-{% endblock %} diff --git a/templates/admin/add_zbiorka.html b/templates/admin/add_zbiorka.html deleted file mode 100644 index 00f7ae5..0000000 --- a/templates/admin/add_zbiorka.html +++ /dev/null @@ -1,51 +0,0 @@ -{% extends 'base.html' %} -{% block title %}Dodaj zbiórkę{% endblock %} -{% block content %} -
-
-
-

Dodaj nową zbiórkę

-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- - -
-
-
-
- - - - - -{% endblock %} diff --git a/templates/admin/dashboard.html b/templates/admin/dashboard.html index c7a16b4..a244f35 100644 --- a/templates/admin/dashboard.html +++ b/templates/admin/dashboard.html @@ -1,110 +1,247 @@ {% extends 'base.html' %} {% block title %}Panel Admina{% endblock %} + {% block content %}
-

Panel Admina

- - - -

Aktywne zbiórki

-
- - - - - - - - - - - {% for z in active_zbiorki %} - - - - - - - {% else %} - - - - {% endfor %} - -
IDNazwaWidocznośćOpcje
{{ z.id }}{{ z.nazwa }} - {% if z.ukryta %} - Ukryta - {% else %} - Widoczna - {% endif %} - - Edytuj - Dodaj wpłatę - Edytuj stan - -
- -
-
- -
-
- -
-
Brak aktywnych zbiórek
-
+ + - -

Zrealizowane zbiórki

-
- - - - - - - - - - - {% for z in completed_zbiorki %} - - - - - - + + + +
+ + +
+ + {% if active_zbiorki and active_zbiorki|length > 0 %} +
+
IDNazwaWidocznośćOpcje
{{ z.id }}{{ z.nazwa }} - {% if z.ukryta %} - Ukryta - {% else %} - Widoczna - {% endif %} - - Edytuj - Dodaj wpłatę - Edytuj stan -
- -
-
- -
-
+ + + + + + + + + + {% for z in active_zbiorki %} + + + + + + + {% endfor %} + +
IDNazwaWidocznośćOpcje
{{ z.id }} +
+ {{ z.nazwa }} + {# opcjonalnie: mini-meta z celem/stanem jeśli masz te pola #} + {% if z.cel is defined or z.stan is defined %} + + {% if z.cel is defined %} Cel: {{ z.cel|round(2) }} PLN {% endif %} + {% if z.stan is defined %} · Stan: {{ z.stan|round(2) }} PLN {% endif %} + + {% endif %} +
+
+ {% if z.ukryta %} + Ukryta + {% else %} + Widoczna + {% endif %} + + +
+ Edytuj + + +
+
+
{% else %} - - Brak zbiórek zrealizowanych - - {% endfor %} - - -
+ +
+
+
Brak aktywnych zbiórek
+

Wygląda na to, że teraz nic nie zbieramy.

+ Utwórz nową zbiórkę +
+
+ {% endif %} + + + +
+ + {% if completed_zbiorki and completed_zbiorki|length > 0 %} +
+ + + + + + + + + + + {% for z in completed_zbiorki %} + + + + + + + {% endfor %} + +
IDNazwaStatusOpcje
{{ z.id }} +
+ {{ z.nazwa }} + {% if z.cel is defined or z.stan is defined %} + + {% if z.cel is defined %} Cel: {{ z.cel|round(2) }} PLN {% endif %} + {% if z.stan is defined %} · Zebrano: {{ z.stan|round(2) }} PLN {% endif %} + + {% endif %} +
+
+
+ Zrealizowana + {% if z.ukryta %} + Ukryta + {% else %} + Widoczna + {% endif %} +
+
+
+ Edytuj + + +
+
+
+ {% else %} +
+
+
Brak zbiórek zrealizowanych
+

Gdy jakaś zbiórka osiągnie 100%, pojawi się tutaj.

+ Utwórz nową + zbiórkę +
+
+ {% endif %} +
+ -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/templates/admin/dodaj_wplate.html b/templates/admin/dodaj_wplate.html new file mode 100644 index 0000000..3352df1 --- /dev/null +++ b/templates/admin/dodaj_wplate.html @@ -0,0 +1,86 @@ +{% extends 'base.html' %} +{% block title %}Dodaj wpłatę{% endblock %} + +{% block content %} +
+ + + +
+
+

Dodaj wpłatę: {{ zbiorka.nazwa }}

+
+ {% if zbiorka.cel %} + Cel: {{ zbiorka.cel|round(2) }} + PLN + {% endif %} + Stan: {{ zbiorka.stan|round(2) }} + PLN +
+
+ + {% set progress = (zbiorka.stan / zbiorka.cel * 100) if zbiorka.cel and zbiorka.cel > 0 else 0 %} + + {% set progress_clamped = 100 if progress > 100 else (0 if progress < 0 else progress) %}
+
+
+
+ {{ progress|round(1) }}% +
+ +
+
+ +
+ +
+ PLN + +
+
Podaj kwotę w złotówkach (min. 0,01).
+ +
+ {% for preset in [10,25,50,100,200] %} + + {% endfor %} + {% if zbiorka.cel and zbiorka.cel > 0 %} + {% set brakujace = (zbiorka.cel - zbiorka.stan) if (zbiorka.cel - zbiorka.stan) > 0 else 0 %} + + {% endif %} +
+
+ +
+ + +
+ Krótka notatka do wpłaty (widoczna w systemie). + 0/300 +
+
+ +
+ + Anuluj +
+
+
+
+
+ +{% endblock %} +{% block extra_scripts %} +{{ super() }} + +{% endblock %} \ No newline at end of file diff --git a/templates/admin/dodaj_zbiorke.html b/templates/admin/dodaj_zbiorke.html new file mode 100644 index 0000000..ebe45b3 --- /dev/null +++ b/templates/admin/dodaj_zbiorke.html @@ -0,0 +1,125 @@ +{% extends 'base.html' %} +{% block title %}Dodaj zbiórkę{% endblock %} + +{% block extra_head %} +{{ super() }} + +{% endblock %} + +{% block content %} +
+ + + + +
+
+

Dodaj nową zbiórkę

+ Uzupełnij podstawowe dane i dane płatności +
+ +
+
+ {# {{ form.csrf_token }} jeśli używasz Flask-WTF #} + + +
+
Podstawowe
+
+
+ + +
Krótki, zrozumiały tytuł. Max 120 znaków.
+
+ +
+ + +
+ + Możesz używać **Markdown** (nagłówki, listy, linki). W edytorze włącz podgląd 👁️. + + 0 znaków +
+
+
+
+ +
+ + +
+
Dane płatności
+
+
+ +
+ PL + +
+
Wpisz ciąg cyfr; spacje dodadzą się automatycznie dla czytelności. +
+
+ +
+ +
+ +48 + +
+
Dziewięć cyfr telefonu powiązanego z BLIK. Spacje opcjonalne.
+
+
+
+ +
+ + +
+
Cel i widoczność
+
+
+ +
+ PLN + +
+
Minimalnie 0,01 PLN. Możesz to później edytować.
+
+ +
+
+ + +
+
+
+
+ + +
+ + Anuluj +
+
+
+
+
+{% endblock %} + +{% block extra_scripts %} +{{ super() }} + + + +{% endblock %} \ No newline at end of file diff --git a/templates/admin/edit_zbiorka.html b/templates/admin/edit_zbiorka.html deleted file mode 100644 index b04fc97..0000000 --- a/templates/admin/edit_zbiorka.html +++ /dev/null @@ -1,67 +0,0 @@ -{% extends 'base.html' %} -{% block title %}Edytuj zbiórkę{% endblock %} -{% block content %} -
-
-
-

Edytuj zbiórkę

-
-
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
- - - - - - - -
- - -
-
-
- - - - - -{% endblock %} diff --git a/templates/admin/edytuj_stan.html b/templates/admin/edytuj_stan.html index 4ba97e0..00d70f7 100644 --- a/templates/admin/edytuj_stan.html +++ b/templates/admin/edytuj_stan.html @@ -1,24 +1,128 @@ {% extends 'base.html' %} {% block title %}Edytuj stan zbiórki{% endblock %} + {% block content %}
-
-
-

Edytuj stan zbiórki: {{ zbiorka.nazwa }}

+ + + + + {# Obliczenia wstępne (do inicjalnego podglądu) #} + {% 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) %}
+
+

Edytuj stan: {{ zbiorka.nazwa }}

+
+ {% if has_cel %} + Cel: {{ zbiorka.cel|round(2) }} + PLN + {% endif %} + Obecnie: {{ zbiorka.stan|round(2) }} + PLN +
-
-
-
- -
- PLN - + + +
+
+
+ +
+ Aktualnie: {{ progress|round(1) }}% +
+ +
+ + {# {{ form.csrf_token }} #} + + +
+ +
+ PLN + +
+
+ Wpisz łączną zebraną kwotę po zmianie (nie przyrost). Skorzystaj z szybkich korekt poniżej. +
+ + +
+ {% for delta in [10,50,100,200] %} + + + {% endfor %} + {% if has_cel %} + + {% set brakujace = (zbiorka.cel - zbiorka.stan) if (zbiorka.cel - zbiorka.stan) > 0 else 0 %} + {% if brakujace > 0 %} + Brakuje: {{ brakujace|round(2) }} + PLN + {% endif %} + {% endif %} + +
+
+ + +
+
+
+
+
+
Podgląd po zapisaniu
+
+ Procent realizacji: + {{ progress|round(1) }}% +
+
+
+
+ + + {% if has_cel %} + {% if brakujace > 0 %} + Do celu brakuje: {{ brakujace|round(2) }} PLN + {% elif brakujace == 0 %} + Cel osiągnięty. + {% else %} + Przekroczono cel o: {{ (brakujace * -1)|round(2) }} PLN + {% endif %} + {% else %} + Brak zdefiniowanego celu — procent nie jest wyliczany. + {% endif %} + +
-
- - Powrót +
+
+ + +
+ + Anuluj +
-
+
{% endblock %} + +{% block extra_scripts %} +{{ super() }} + +{% endblock %} \ No newline at end of file diff --git a/templates/admin/edytuj_zbiorke.html b/templates/admin/edytuj_zbiorke.html new file mode 100644 index 0000000..7a5e5bb --- /dev/null +++ b/templates/admin/edytuj_zbiorke.html @@ -0,0 +1,154 @@ +{% extends 'base.html' %} +{% block title %}Edytuj zbiórkę{% endblock %} + +{% block extra_head %} +{{ super() }} + +{% endblock %} + +{% block content %} +
+ + + + + +
+
+

Edytuj zbiórkę

+
+ {% if zbiorka.cel %} + Cel: {{ zbiorka.cel|round(2) }} + PLN + {% endif %} + {% if zbiorka.ukryj_kwote %} + Kwoty ukryte + {% else %} + Kwoty widoczne + {% endif %} +
+
+ +
+ +
+ {# {{ form.csrf_token }} jeśli używasz Flask-WTF #} + + +
+
Podstawowe
+
+
+ + +
Max 120 znaków. Użyj konkretów.
+
+ +
+ + +
+ Wspieramy **Markdown** — użyj nagłówków, list, linków. + Włącz podgląd w edytorze 👁️. + 0 znaków +
+
+
+
+ +
+ + +
+
Dane płatności
+
+
+ +
+ PL + +
+
Wpisz same cyfry — spacje dodadzą się automatycznie co 4 znaki.
+
+ +
+ +
+ +48 + +
+
9 cyfr. Spacje dodadzą się automatycznie (format 3-3-3).
+
+ +
+ +
+
+
+ +
+ + +
+
Cel i widoczność
+
+
+ +
+ PLN + +
+
Minimalnie 0,01 PLN. W razie potrzeby zmienisz to później.
+
+ +
+
+ + +
+
+
+
+ + +
+ + Anuluj + + + + + +
+ + +
+
+
+{% endblock %} + +{% block extra_scripts %} +{{ super() }} + + + +{% endblock %} \ No newline at end of file diff --git a/templates/admin/settings.html b/templates/admin/settings.html deleted file mode 100644 index a120cd5..0000000 --- a/templates/admin/settings.html +++ /dev/null @@ -1,55 +0,0 @@ -{% extends 'base.html' %} -{% block title %}Ustawienia globalne{% endblock %} -{% block content %} -
-
- -
-
-

Ustawienia konta

-
-
-
- - -
-
- - -
-
-
- -
-
-

Dozwolone adresy IP

- -
-
-
- - -
-

Twój aktualny adres IP: {{ client_ip }}

- -
-
- -
- - Powrót -
-
-
- - -{% endblock %} diff --git a/templates/admin/ustawienia.html b/templates/admin/ustawienia.html new file mode 100644 index 0000000..3b7aed2 --- /dev/null +++ b/templates/admin/ustawienia.html @@ -0,0 +1,125 @@ +{% extends 'base.html' %} +{% block title %}Ustawienia globalne{% endblock %} + +{% block content %} +
+
+ {# {{ form.csrf_token }} jeśli używasz Flask-WTF #} + + +
+
+

Dane płatności

+ Używane jako wartości domyślne przy dodawaniu/edycji zbiórek +
+ +
+
+
+ +
+ PL + +
+
Wpisz ciąg cyfr — spacje dodadzą się automatycznie co 4 znaki.
+
+ +
+ +
+ +48 + +
+
9 cyfr. Spacje i format 3-3-3 dodajemy dla czytelności.
+
+
+
+
+ + +
+
+

Dostęp — dozwolone adresy IP / hosty

+ Zależnie od konfiguracji, logowanie może wymagać dopasowania do białej listy +
+ +
+
+
+ + +
Po wpisaniu kliknij „Dodaj do listy”. Duplikaty są pomijane.
+
+
+ + + +
+
+ +
+ + +
+ Akceptowane separatory: przecinek (`,`), średnik (`;`) i nowa linia. + Pozycji na liście: 0 +
+
+
+
+ +
+
Branding
+
+ +
+ + +
Najlepiej transparentne, do 60px wysokości.
+
+ + +
+ + +
+
+ + +
+ + +
+ + {% if settings and settings.logo_url %} +
+ Podgląd logo: + Logo preview +
+ {% endif %} +
+ + +
+ Powrót + +
+
+
+{% endblock %} + +{% block extra_scripts %} +{{ super() }} + +{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 4e3f8c6..429dd7d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,74 +1,83 @@ + - - + + {% block title %}Aplikacja Zbiórek{% endblock %} - - - + + {% block extra_head %}{% endblock %} - -