przebudowa systemu
This commit is contained in:
34
.env.example
Normal file
34
.env.example
Normal file
@@ -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
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,4 +2,4 @@ __pycache__
|
|||||||
data/
|
data/
|
||||||
instance/
|
instance/
|
||||||
venv/
|
venv/
|
||||||
config.py
|
.env
|
49
alters.txt
49
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;
|
431
app.py
431
app.py
@@ -1,9 +1,19 @@
|
|||||||
from flask import Flask, render_template, request, redirect, url_for, flash
|
from flask import Flask, render_template, request, redirect, url_for, flash
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
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 werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
|
from sqlalchemy import event
|
||||||
|
from sqlalchemy.engine import Engine
|
||||||
|
|
||||||
import markdown as md
|
import markdown as md
|
||||||
from flask import request, flash, abort
|
from flask import request, flash, abort
|
||||||
import os
|
import os
|
||||||
@@ -12,14 +22,15 @@ import socket
|
|||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
# Ładujemy konfigurację z pliku config.py
|
# Ładujemy konfigurację z pliku config.py
|
||||||
app.config.from_object('config.Config')
|
app.config.from_object("config.Config")
|
||||||
|
|
||||||
db = SQLAlchemy(app)
|
db = SQLAlchemy(app)
|
||||||
login_manager = LoginManager(app)
|
login_manager = LoginManager(app)
|
||||||
login_manager.login_view = 'login'
|
login_manager.login_view = "login"
|
||||||
|
|
||||||
# MODELE
|
# MODELE
|
||||||
|
|
||||||
|
|
||||||
class User(UserMixin, db.Model):
|
class User(UserMixin, db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||||
@@ -28,10 +39,11 @@ class User(UserMixin, db.Model):
|
|||||||
|
|
||||||
def set_password(self, password):
|
def set_password(self, password):
|
||||||
self.password_hash = generate_password_hash(password)
|
self.password_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.password_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)
|
||||||
nazwa = db.Column(db.String(100), nullable=False)
|
nazwa = db.Column(db.String(100), nullable=False)
|
||||||
@@ -42,26 +54,57 @@ class Zbiorka(db.Model):
|
|||||||
stan = db.Column(db.Float, default=0.0)
|
stan = db.Column(db.Float, default=0.0)
|
||||||
ukryta = db.Column(db.Boolean, default=False)
|
ukryta = db.Column(db.Boolean, default=False)
|
||||||
ukryj_kwote = db.Column(db.Boolean, default=False)
|
ukryj_kwote = db.Column(db.Boolean, default=False)
|
||||||
wplaty = db.relationship('Wplata', backref='zbiorka', lazy=True, order_by='Wplata.data.desc()')
|
|
||||||
zrealizowana = db.Column(db.Boolean, default=False)
|
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):
|
class Wplata(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
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)
|
kwota = db.Column(db.Float, 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 wpłaty
|
opis = db.Column(db.Text, nullable=True)
|
||||||
|
|
||||||
|
zbiorka = db.relationship("Zbiorka", back_populates="wplaty")
|
||||||
|
|
||||||
|
|
||||||
class GlobalSettings(db.Model):
|
class GlobalSettings(db.Model):
|
||||||
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)
|
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
|
@login_manager.user_loader
|
||||||
def load_user(user_id):
|
def load_user(user_id):
|
||||||
return User.query.get(int(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():
|
def get_real_ip():
|
||||||
headers = request.headers
|
headers = request.headers
|
||||||
cf_ip = headers.get("CF-Connecting-IP")
|
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"):
|
if os.path.exists("emergency_access.txt"):
|
||||||
return True
|
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()
|
allowed_ips = set()
|
||||||
for host in allowed_hosts:
|
for host in allowed_hosts:
|
||||||
host = host.strip()
|
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
|
# 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):
|
def markdown_filter(text):
|
||||||
return Markup(md.markdown(text))
|
return Markup(md.markdown(text))
|
||||||
|
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
def inject_ip_allowed():
|
def inject_globals():
|
||||||
settings = GlobalSettings.query.first()
|
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()
|
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
|
# TRASY PUBLICZNE
|
||||||
|
@app.route("/")
|
||||||
@app.route('/')
|
|
||||||
def index():
|
def index():
|
||||||
zbiorki = Zbiorka.query.filter_by(ukryta=False, zrealizowana=False).all()
|
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():
|
def zbiorki_zrealizowane():
|
||||||
zbiorki = Zbiorka.query.filter_by(zrealizowana=True).all()
|
zbiorki = Zbiorka.query.filter_by(zrealizowana=True).all()
|
||||||
return render_template('index.html', zbiorki=zbiorki)
|
return render_template("index.html", zbiorki=zbiorki)
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
def page_not_found(e):
|
def page_not_found(e):
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
@app.route('/zbiorka/<int:zbiorka_id>')
|
|
||||||
|
@app.route("/zbiorka/<int:zbiorka_id>")
|
||||||
def zbiorka(zbiorka_id):
|
def zbiorka(zbiorka_id):
|
||||||
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
||||||
# Jeżeli zbiórka jest ukryta i użytkownik nie jest administratorem, zwróć 404
|
# 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):
|
if zb.ukryta and (not current_user.is_authenticated or not current_user.is_admin):
|
||||||
abort(404)
|
abort(404)
|
||||||
return render_template('zbiorka.html', zbiorka=zb)
|
return render_template("zbiorka.html", zbiorka=zb)
|
||||||
|
|
||||||
|
|
||||||
# TRASY LOGOWANIA I REJESTRACJI
|
# 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
|
# Pobierz ustawienia globalne, w tym dozwolone hosty
|
||||||
settings = GlobalSettings.query.first()
|
settings = GlobalSettings.query.first()
|
||||||
allowed_hosts_str = ""
|
allowed_hosts_str = ""
|
||||||
@@ -153,187 +209,211 @@ def login():
|
|||||||
# Sprawdzenie, czy adres IP klienta jest dozwolony
|
# 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('Dostęp do endpointu /login jest zablokowany dla Twojego adresu IP', 'danger')
|
flash(
|
||||||
return redirect(url_for('index'))
|
"Dostęp do endpointu /login jest zablokowany dla Twojego adresu IP",
|
||||||
|
"danger",
|
||||||
|
)
|
||||||
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
username = request.form['username']
|
username = request.form["username"]
|
||||||
password = request.form['password']
|
password = request.form["password"]
|
||||||
user = User.query.filter_by(username=username).first()
|
user = User.query.filter_by(username=username).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 redirect(next_page) if next_page else redirect(url_for('admin_dashboard'))
|
return (
|
||||||
|
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")
|
||||||
|
|
||||||
|
|
||||||
@app.route('/logout')
|
@app.route("/wyloguj")
|
||||||
@login_required
|
@login_required
|
||||||
def logout():
|
def wyloguj():
|
||||||
logout_user()
|
logout_user()
|
||||||
flash('Wylogowano', 'success')
|
flash("Wylogowano", "success")
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for("zaloguj"))
|
||||||
|
|
||||||
@app.route('/register', methods=['GET', 'POST'])
|
|
||||||
def register():
|
@app.route("/zarejestruj", methods=["GET", "POST"])
|
||||||
if not app.config.get('ALLOW_REGISTRATION', False):
|
def zarejestruj():
|
||||||
flash('Rejestracja została wyłączona przez administratora', 'danger')
|
if not app.config.get("ALLOW_REGISTRATION", False):
|
||||||
return redirect(url_for('login'))
|
flash("Rejestracja została wyłączona przez administratora", "danger")
|
||||||
if request.method == 'POST':
|
return redirect(url_for("zaloguj"))
|
||||||
username = request.form['username']
|
if request.method == "POST":
|
||||||
password = request.form['password']
|
username = request.form["username"]
|
||||||
|
password = request.form["password"]
|
||||||
if User.query.filter_by(username=username).first():
|
if User.query.filter_by(username=username).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 = User(username=username)
|
||||||
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()
|
||||||
flash('Konto utworzone, możesz się zalogować', 'success')
|
flash("Konto utworzone, możesz się zalogować", "success")
|
||||||
return redirect(url_for('login'))
|
return redirect(url_for("zaloguj"))
|
||||||
return render_template('register.html')
|
return render_template("register.html")
|
||||||
|
|
||||||
|
|
||||||
# PANEL ADMINISTRACYJNY
|
# PANEL ADMINISTRACYJNY
|
||||||
|
|
||||||
@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.is_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()
|
||||||
completed_zbiorki = Zbiorka.query.filter_by(zrealizowana=True).all()
|
completed_zbiorki = Zbiorka.query.filter_by(zrealizowana=True).all()
|
||||||
return render_template('admin/dashboard.html', active_zbiorki=active_zbiorki,
|
return render_template(
|
||||||
completed_zbiorki=completed_zbiorki)
|
"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
|
@login_required
|
||||||
def dodaj_zbiorka():
|
def dodaj_zbiorke():
|
||||||
if not current_user.is_admin:
|
if not current_user.is_admin:
|
||||||
flash('Brak uprawnień', 'danger')
|
flash("Brak uprawnień", "danger")
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
global_settings = GlobalSettings.query.first() # Pobieramy globalne ustawienia
|
global_settings = GlobalSettings.query.first() # Pobieramy globalne ustawienia
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
nazwa = request.form['nazwa']
|
nazwa = request.form["nazwa"]
|
||||||
opis = request.form['opis']
|
opis = request.form["opis"]
|
||||||
# Pozyskujemy numer konta i telefon z formularza (mogą być nadpisane ręcznie)
|
# Pozyskujemy numer konta i telefon z formularza (mogą być nadpisane ręcznie)
|
||||||
numer_konta = request.form['numer_konta']
|
numer_konta = request.form["numer_konta"]
|
||||||
numer_telefonu_blik = request.form['numer_telefonu_blik']
|
numer_telefonu_blik = request.form["numer_telefonu_blik"]
|
||||||
cel = float(request.form['cel'])
|
cel = float(request.form["cel"])
|
||||||
ukryj_kwote = 'ukryj_kwote' in request.form
|
ukryj_kwote = "ukryj_kwote" in request.form
|
||||||
|
|
||||||
nowa_zbiorka = Zbiorka(
|
nowa_zbiorka = Zbiorka(
|
||||||
nazwa=nazwa,
|
nazwa=nazwa,
|
||||||
opis=opis,
|
opis=opis,
|
||||||
numer_konta=numer_konta,
|
numer_konta=numer_konta,
|
||||||
numer_telefonu_blik=numer_telefonu_blik,
|
numer_telefonu_blik=numer_telefonu_blik,
|
||||||
cel=cel,
|
cel=cel,
|
||||||
ukryj_kwote=ukryj_kwote
|
ukryj_kwote=ukryj_kwote,
|
||||||
)
|
)
|
||||||
db.session.add(nowa_zbiorka)
|
db.session.add(nowa_zbiorka)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Zbiórka została dodana', 'success')
|
flash("Zbiórka została dodana", "success")
|
||||||
return redirect(url_for('admin_dashboard'))
|
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/<int:zbiorka_id>', methods=['GET', 'POST'])
|
|
||||||
|
@app.route("/admin/zbiorka/edytuj/<int:zbiorka_id>", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def edytuj_zbiorka(zbiorka_id):
|
def edytuj_zbiorka(zbiorka_id):
|
||||||
if not current_user.is_admin:
|
if not current_user.is_admin:
|
||||||
flash('Brak uprawnień', 'danger')
|
flash("Brak uprawnień", "danger")
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for("index"))
|
||||||
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
||||||
global_settings = GlobalSettings.query.first() # Pobieramy globalne ustawienia
|
global_settings = GlobalSettings.query.first() # Pobieramy globalne ustawienia
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
zb.nazwa = request.form['nazwa']
|
zb.nazwa = request.form["nazwa"]
|
||||||
zb.opis = request.form['opis']
|
zb.opis = request.form["opis"]
|
||||||
zb.numer_konta = request.form['numer_konta']
|
zb.numer_konta = request.form["numer_konta"]
|
||||||
zb.numer_telefonu_blik = request.form['numer_telefonu_blik']
|
zb.numer_telefonu_blik = request.form["numer_telefonu_blik"]
|
||||||
try:
|
try:
|
||||||
zb.cel = float(request.form['cel'])
|
zb.cel = float(request.form["cel"])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
flash('Podano nieprawidłową wartość dla celu zbiórki', 'danger')
|
flash("Podano nieprawidłową wartość dla celu zbiórki", "danger")
|
||||||
return render_template('admin/edit_zbiorka.html', zbiorka=zb, global_settings=global_settings)
|
return render_template(
|
||||||
zb.ukryj_kwote = 'ukryj_kwote' in request.form
|
"admin/edytuj_zbiorke.html", zbiorka=zb, global_settings=global_settings
|
||||||
|
)
|
||||||
|
zb.ukryj_kwote = "ukryj_kwote" in request.form
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Zbiórka została zaktualizowana', 'success')
|
flash("Zbiórka została zaktualizowana", "success")
|
||||||
return redirect(url_for('admin_dashboard'))
|
return redirect(url_for("admin_dashboard"))
|
||||||
return render_template('admin/edit_zbiorka.html', zbiorka=zb, global_settings=global_settings)
|
return render_template(
|
||||||
|
"admin/edytuj_zbiorke.html", zbiorka=zb, global_settings=global_settings
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# TRASA DODAWANIA WPŁATY Z OPISEM
|
# TRASA DODAWANIA WPŁATY Z OPISEM
|
||||||
# TRASA DODAWANIA WPŁATY W PANELU ADMINA
|
# TRASA DODAWANIA WPŁATY W PANELU ADMINA
|
||||||
@app.route('/admin/zbiorka/<int:zbiorka_id>/wplata/dodaj', methods=['GET', 'POST'])
|
@app.route("/admin/zbiorka/<int:zbiorka_id>/wplata/dodaj", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def admin_dodaj_wplate(zbiorka_id):
|
def dodaj_wplate(zbiorka_id):
|
||||||
if not current_user.is_admin:
|
if not current_user.is_admin:
|
||||||
flash('Brak uprawnień', 'danger')
|
flash("Brak uprawnień", "danger")
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for("index"))
|
||||||
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
kwota = float(request.form['kwota'])
|
kwota = float(request.form["kwota"])
|
||||||
opis = request.form.get('opis', '')
|
opis = request.form.get("opis", "")
|
||||||
nowa_wplata = Wplata(zbiorka_id=zb.id, kwota=kwota, opis=opis)
|
nowa_wplata = Wplata(zbiorka_id=zb.id, kwota=kwota, opis=opis)
|
||||||
zb.stan += kwota # Aktualizacja stanu zbiórki
|
zb.stan += kwota # Aktualizacja stanu zbiórki
|
||||||
db.session.add(nowa_wplata)
|
db.session.add(nowa_wplata)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Wpłata została dodana', 'success')
|
flash("Wpłata została dodana", "success")
|
||||||
return redirect(url_for('admin_dashboard'))
|
return redirect(url_for("admin_dashboard"))
|
||||||
return render_template('admin/add_wplata.html', zbiorka=zb)
|
return render_template("admin/dodaj_wplate.html", zbiorka=zb)
|
||||||
|
|
||||||
@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.is_admin:
|
||||||
flash('Brak uprawnień', 'danger')
|
flash("Brak uprawnień", "danger")
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for("index"))
|
||||||
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
||||||
db.session.delete(zb)
|
db.session.delete(zb)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Zbiórka została usunięta', 'success')
|
flash("Zbiórka została usunięta", "success")
|
||||||
return redirect(url_for('admin_dashboard'))
|
return redirect(url_for("admin_dashboard"))
|
||||||
|
|
||||||
@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.is_admin:
|
||||||
flash('Brak uprawnień', 'danger')
|
flash("Brak uprawnień", "danger")
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for("index"))
|
||||||
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
||||||
if request.method == 'POST':
|
if request.method == "POST":
|
||||||
try:
|
try:
|
||||||
nowy_stan = float(request.form['stan'])
|
nowy_stan = float(request.form["stan"])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
flash('Nieprawidłowa wartość kwoty', 'danger')
|
flash("Nieprawidłowa wartość kwoty", "danger")
|
||||||
return redirect(url_for('edytuj_stan', zbiorka_id=zbiorka_id))
|
return redirect(url_for("edytuj_stan", zbiorka_id=zbiorka_id))
|
||||||
zb.stan = nowy_stan
|
zb.stan = nowy_stan
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Stan zbiórki został zaktualizowany', 'success')
|
flash("Stan zbiórki został zaktualizowany", "success")
|
||||||
return redirect(url_for('admin_dashboard'))
|
return redirect(url_for("admin_dashboard"))
|
||||||
return render_template('admin/edytuj_stan.html', zbiorka=zb)
|
return render_template("admin/edytuj_stan.html", zbiorka=zb)
|
||||||
|
|
||||||
@app.route('/admin/zbiorka/toggle_visibility/<int:zbiorka_id>', methods=['POST'])
|
|
||||||
|
@app.route("/admin/zbiorka/zmien_widzialnosc/<int:zbiorka_id>", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def toggle_visibility(zbiorka_id):
|
def zmien_widzialnosc(zbiorka_id):
|
||||||
if not current_user.is_admin:
|
if not current_user.is_admin:
|
||||||
flash('Brak uprawnień', 'danger')
|
flash("Brak uprawnień", "danger")
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for("index"))
|
||||||
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
||||||
zb.ukryta = not zb.ukryta
|
zb.ukryta = not zb.ukryta
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Zbiórka została ' + ('ukryta' if zb.ukryta else 'przywrócona'), 'success')
|
flash("Zbiórka została " + ("ukryta" if zb.ukryta else "przywrócona"), "success")
|
||||||
return redirect(url_for('admin_dashboard'))
|
return redirect(url_for("admin_dashboard"))
|
||||||
|
|
||||||
|
|
||||||
def create_admin_account():
|
def create_admin_account():
|
||||||
admin = User.query.filter_by(is_admin=True).first()
|
admin = User.query.filter_by(is_admin=True).first()
|
||||||
if not admin:
|
if not admin:
|
||||||
main_admin = User(username=app.config['MAIN_ADMIN_USERNAME'], is_admin=True)
|
main_admin = User(username=app.config["MAIN_ADMIN_USERNAME"], is_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()
|
||||||
|
|
||||||
@@ -362,7 +442,9 @@ def apply_headers(response):
|
|||||||
response.headers.pop("Vary", None)
|
response.headers.pop("Vary", None)
|
||||||
elif request.path.startswith("/admin"):
|
elif request.path.startswith("/admin"):
|
||||||
response.headers.pop("Vary", None)
|
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:
|
else:
|
||||||
response.headers["Vary"] = "Cookie, Accept-Encoding"
|
response.headers["Vary"] = "Cookie, Accept-Encoding"
|
||||||
default_cache = app.config.get("CACHE_CONTROL_HEADER") or "private, max-age=0"
|
default_cache = app.config.get("CACHE_CONTROL_HEADER") or "private, max-age=0"
|
||||||
@@ -370,75 +452,96 @@ def apply_headers(response):
|
|||||||
|
|
||||||
# Blokowanie botów
|
# Blokowanie botów
|
||||||
if app.config.get("BLOCK_BOTS", False):
|
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["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
|
return response
|
||||||
|
|
||||||
|
|
||||||
@app.route('/admin/settings', methods=['GET', 'POST'])
|
@app.route("/admin/ustawienia", methods=["GET", "POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def admin_settings():
|
def admin_ustawienia():
|
||||||
if not current_user.is_admin:
|
if not current_user.is_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 = GlobalSettings.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')
|
allowed_login_hosts = request.form.get("allowed_login_hosts")
|
||||||
|
logo_url = request.form.get("logo_url")
|
||||||
if settings is None:
|
site_title = request.form.get("site_title")
|
||||||
settings = GlobalSettings(
|
show_logo_in_navbar = "show_logo_in_navbar" in request.form
|
||||||
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)
|
|
||||||
|
|
||||||
@app.route('/admin/zbiorka/oznacz/<int:zbiorka_id>', 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/<int:zbiorka_id>", methods=["POST"])
|
||||||
@login_required
|
@login_required
|
||||||
def oznacz_zbiorka(zbiorka_id):
|
def oznacz_zbiorka(zbiorka_id):
|
||||||
if not current_user.is_admin:
|
if not current_user.is_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"))
|
||||||
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
zb = Zbiorka.query.get_or_404(zbiorka_id)
|
||||||
zb.zrealizowana = True
|
zb.zrealizowana = True
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash('Zbiórka została oznaczona jako zrealizowana', 'success')
|
flash("Zbiórka została oznaczona jako zrealizowana", "success")
|
||||||
return redirect(url_for('admin_dashboard'))
|
return redirect(url_for("admin_dashboard"))
|
||||||
|
|
||||||
@app.route('/robots.txt')
|
|
||||||
|
@app.route("/robots.txt")
|
||||||
def robots():
|
def robots():
|
||||||
if app.config.get("BLOCK_BOTS", False):
|
if app.config.get("BLOCK_BOTS", False):
|
||||||
# Instrukcje dla robotów – blokujemy indeksowanie całej witryny
|
|
||||||
robots_txt = "User-agent: *\nDisallow: /"
|
robots_txt = "User-agent: *\nDisallow: /"
|
||||||
else:
|
else:
|
||||||
# Jeśli blokowanie botów wyłączone, można zwrócić pusty plik lub inne ustawienia
|
|
||||||
robots_txt = "User-agent: *\nAllow: /"
|
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():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
# Tworzenie konta głównego admina, jeśli nie istnieje
|
# Tworzenie konta głównego admina, jeśli nie istnieje
|
||||||
if not User.query.filter_by(is_admin=True).first():
|
if not User.query.filter_by(is_admin=True).first():
|
||||||
main_admin = User(username=app.config['MAIN_ADMIN_USERNAME'], is_admin=True)
|
main_admin = User(username=app.config["MAIN_ADMIN_USERNAME"], is_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.run(debug=True)
|
app.run(debug=True)
|
||||||
|
@@ -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"
|
|
45
config.py
Normal file
45
config.py
Normal file
@@ -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
|
@@ -1,12 +1,10 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "${APP_PORT:-8080}:8080"
|
||||||
volumes:
|
volumes:
|
||||||
- ./instance:/app/instance
|
- ./instance:/app/instance
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
3
emergency_access.txt
Normal file
3
emergency_access.txt
Normal file
@@ -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
|
@@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
from app import app, db, create_admin_account
|
from app import app, db, create_admin_account
|
||||||
from waitress import serve
|
from waitress import serve
|
||||||
|
|
||||||
@@ -5,4 +6,6 @@ if __name__ == '__main__':
|
|||||||
with app.app_context():
|
with app.app_context():
|
||||||
db.create_all()
|
db.create_all()
|
||||||
create_admin_account()
|
create_admin_account()
|
||||||
serve(app, host='0.0.0.0', port=8080)
|
|
||||||
|
port = int(os.environ.get("APP_PORT", 8080))
|
||||||
|
serve(app, host="0.0.0.0", port=port)
|
@@ -1,168 +1,295 @@
|
|||||||
/* Import czcionki Roboto */
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap');
|
@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 {
|
body {
|
||||||
font-family: 'Roboto', sans-serif;
|
font-family: 'Roboto', system-ui, -apple-system, Segoe UI, Arial, sans-serif;
|
||||||
background-color: #121212;
|
background: var(--bg);
|
||||||
color: #dcdcdc;
|
color: var(--text);
|
||||||
padding-top: 1vh;
|
|
||||||
margin: 0;
|
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 {
|
a {
|
||||||
color: #ffc107;
|
color: var(--accent);
|
||||||
transition: color 0.3s ease;
|
text-decoration: none;
|
||||||
|
transition: color var(--trans);
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: #ffeb3b;
|
color: var(--accent-300);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Progress Bar */
|
/* ========= NAVBAR ========= */
|
||||||
.progress {
|
.navbar {
|
||||||
background-color: #2a2a2a;
|
background: var(--surface-0);
|
||||||
border-radius: 0.5rem;
|
border-bottom: 1px solid var(--border);
|
||||||
height: 35px;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.progress-bar {
|
.navbar-brand {
|
||||||
background: linear-gradient(90deg, #ffc107, #ffeb3b);
|
color: var(--text);
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
transition: width 0.3s ease;
|
transition: color var(--trans);
|
||||||
animation: progressAnimation 1s ease-in-out forwards;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes progressAnimation {
|
.navbar-brand:hover {
|
||||||
from { width: 0%; }
|
color: var(--accent);
|
||||||
to { width: var(--progress-width); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Alerty (flash messages) */
|
.nav-link {
|
||||||
.alert {
|
color: var(--text-muted);
|
||||||
opacity: 0;
|
transition: color var(--trans);
|
||||||
animation: fadeIn 0.5s forwards;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
.nav-link:hover,
|
||||||
to { opacity: 1; }
|
.nav-link:focus {
|
||||||
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dodatkowe marginesy */
|
/* ========= CARDS ========= */
|
||||||
.container {
|
.card {
|
||||||
padding: 0 15px;
|
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ść */
|
.card:hover {
|
||||||
@media (max-width: 767px) {
|
transform: translateY(-2px);
|
||||||
.card {
|
box-shadow: var(--shadow-md);
|
||||||
margin-bottom: 1rem;
|
border-color: color-mix(in srgb, var(--accent) 20%, var(--border));
|
||||||
}
|
|
||||||
.card-title {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
.btn {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
.card-header {
|
||||||
h2 { font-size: 1.7rem; margin-bottom: 0.8rem; }
|
background: var(--surface-2);
|
||||||
h3 { font-size: 0.9rem; margin-bottom: 0.6rem; }
|
border-bottom: 1px solid var(--border);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
/* Wspomóż */
|
.card-body {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wyróżniona karta */
|
||||||
.card.wspomoz-card {
|
.card.wspomoz-card {
|
||||||
border: 1px solid #ffc107 !important;
|
border: 1px solid var(--accent) !important;
|
||||||
border-radius: 0.2rem !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-body,
|
||||||
.card.wspomoz-card .card-title,
|
.card.wspomoz-card .card-title,
|
||||||
.card.wspomoz-card .card-text {
|
.card.wspomoz-card .card-text {
|
||||||
color: #ffffff !important;
|
color: var(--text) !important;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
font-size: 1.25rem;
|
font-size: 1.15rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:focus,
|
/* ========= BUTTONS ========= */
|
||||||
.btn-primary:active,
|
.btn {
|
||||||
.btn-primary.focus,
|
font-weight: 700;
|
||||||
.btn-primary.active {
|
border-radius: 8px;
|
||||||
background-color: #2d2c2c !important;
|
transition: transform 120ms ease, background-color var(--trans), border-color var(--trans), color var(--trans);
|
||||||
border-color: #ffeb3b !important;
|
}
|
||||||
color: #ffffff !important;
|
|
||||||
box-shadow: none;
|
.btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus-visible {
|
||||||
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 40%, transparent);
|
||||||
outline: none;
|
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);
|
||||||
}
|
}
|
0
static/js/admin_dashboard.js
Normal file
0
static/js/admin_dashboard.js
Normal file
21
static/js/dodaj_wplate.js
Normal file
21
static/js/dodaj_wplate.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
})();
|
37
static/js/dodaj_zbiorke.js
Normal file
37
static/js/dodaj_zbiorke.js
Normal file
@@ -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';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
82
static/js/edytuj_stan.js
Normal file
82
static/js/edytuj_stan.js
Normal file
@@ -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();
|
||||||
|
})();
|
60
static/js/edytuj_zbiorke.js
Normal file
60
static/js/edytuj_zbiorke.js
Normal file
@@ -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';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
4
static/js/mde_custom.js
Normal file
4
static/js/mde_custom.js
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
var simplemde = new SimpleMDE({
|
||||||
|
element: document.getElementById("opis"),
|
||||||
|
forceSync: true
|
||||||
|
});
|
13
static/js/progress.js
Normal file
13
static/js/progress.js
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
92
static/js/ustawienia.js
Normal file
92
static/js/ustawienia.js
Normal file
@@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
27
static/js/walidacja_logowanie.js
Normal file
27
static/js/walidacja_logowanie.js
Normal file
@@ -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);
|
37
static/js/walidacja_rejestracja.js
Normal file
37
static/js/walidacja_rejestracja.js
Normal file
@@ -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);
|
38
static/js/zbiorka.js
Normal file
38
static/js/zbiorka.js
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
@@ -1,24 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
{% block title %}Dodaj wpłatę{% endblock %}
|
|
||||||
{% block content %}
|
|
||||||
<div class="container my-4">
|
|
||||||
<div class="card shadow-sm">
|
|
||||||
<div class="card-header bg-warning text-dark">
|
|
||||||
<h3 class="card-title mb-0">Dodaj wpłatę do zbiórki: {{ zbiorka.nazwa }}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="post">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="kwota" class="form-label">Kwota wpłaty (PLN)</label>
|
|
||||||
<input type="number" step="0.01" class="form-control" id="kwota" name="kwota" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="opis" class="form-label">Opis wpłaty (opcjonalnie)</label>
|
|
||||||
<textarea class="form-control" id="opis" name="opis" rows="3"></textarea>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-success">Dodaj wpłatę</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@@ -1,51 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
{% block title %}Dodaj zbiórkę{% endblock %}
|
|
||||||
{% block content %}
|
|
||||||
<div class="container my-4">
|
|
||||||
<div class="card shadow-sm">
|
|
||||||
<div class="card-header bg-success text-white">
|
|
||||||
<h3 class="card-title mb-0">Dodaj nową zbiórkę</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="post">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="nazwa" class="form-label">Nazwa zbiórki</label>
|
|
||||||
<input type="text" class="form-control" id="nazwa" name="nazwa" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="opis" class="form-label">Opis</label>
|
|
||||||
<textarea class="form-control" id="opis" name="opis" rows="6" required></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="numer_konta" class="form-label">Numer konta</label>
|
|
||||||
<input type="text" class="form-control" id="numer_konta" name="numer_konta" value="{{ global_settings.numer_konta if global_settings else '' }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="numer_telefonu_blik" class="form-label">Numer telefonu BLIK</label>
|
|
||||||
<input type="text" class="form-control" id="numer_telefonu_blik" name="numer_telefonu_blik" value="{{ global_settings.numer_telefonu_blik if global_settings else '' }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="cel" class="form-label">Cel zbiórki (PLN)</label>
|
|
||||||
<input type="number" step="0.01" class="form-control" id="cel" name="cel" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-check mb-3">
|
|
||||||
<input type="checkbox" class="form-check-input" id="ukryj_kwote" name="ukryj_kwote">
|
|
||||||
<label class="form-check-label" for="ukryj_kwote">Ukryj kwoty (cel i stan)</label>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-success">Dodaj zbiórkę</button>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Inicjalizacja edytora Markdown (SimpleMDE) -->
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
|
|
||||||
<script>
|
|
||||||
var simplemde = new SimpleMDE({
|
|
||||||
element: document.getElementById("opis"),
|
|
||||||
forceSync: true
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
@@ -1,110 +1,247 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block title %}Panel Admina{% endblock %}
|
{% block title %}Panel Admina{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-4">
|
<div class="container my-4">
|
||||||
<h2 class="mb-4">Panel Admina</h2>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<a href="{{ url_for('dodaj_zbiorka') }}" class="btn btn-success">Dodaj zbiórkę</a>
|
|
||||||
<a href="{{ url_for('admin_settings') }}" class="btn btn-primary">Ustawienia</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tabela zbiórek aktywnych -->
|
<!-- Nagłówek + akcje globalne -->
|
||||||
<h4>Aktywne zbiórki</h4>
|
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-4">
|
||||||
<div class="table-responsive mb-5">
|
<h2 class="mb-0">Panel Admina</h2>
|
||||||
<table class="table table-dark table-striped table-hover">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
<thead>
|
<a href="{{ url_for('dodaj_zbiorke') }}" class="btn btn-primary">
|
||||||
<tr>
|
➕ Dodaj zbiórkę
|
||||||
<th>ID</th>
|
</a>
|
||||||
<th>Nazwa</th>
|
<a href="{{ url_for('admin_ustawienia') }}" class="btn btn-outline-light border">
|
||||||
<th>Widoczność</th>
|
⚙️ Ustawienia
|
||||||
<th>Opcje</th>
|
</a>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
|
||||||
{% for z in active_zbiorki %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ z.id }}</td>
|
|
||||||
<td>{{ z.nazwa }}</td>
|
|
||||||
<td>
|
|
||||||
{% if z.ukryta %}
|
|
||||||
<span class="badge bg-secondary">Ukryta</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-success">Widoczna</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a href="{{ url_for('edytuj_zbiorka', zbiorka_id=z.id) }}" class="btn btn-primary btn-sm">Edytuj</a>
|
|
||||||
<a href="{{ url_for('admin_dodaj_wplate', zbiorka_id=z.id) }}" class="btn btn-warning btn-sm">Dodaj wpłatę</a>
|
|
||||||
<a href="{{ url_for('edytuj_stan', zbiorka_id=z.id) }}" class="btn btn-info btn-sm">Edytuj stan</a>
|
|
||||||
<!-- Przycisk do oznaczenia jako zrealizowana -->
|
|
||||||
<form action="{{ url_for('oznacz_zbiorka', zbiorka_id=z.id) }}" method="post" style="display: inline;">
|
|
||||||
<button type="submit" class="btn btn-warning btn-sm">Oznacz jako zrealizowana</button>
|
|
||||||
</form>
|
|
||||||
<form action="{{ url_for('toggle_visibility', zbiorka_id=z.id) }}" method="post" style="display: inline;">
|
|
||||||
<button type="submit" class="btn btn-secondary btn-sm">
|
|
||||||
{% if z.ukryta %} Pokaż {% else %} Ukryj {% endif %}
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<form action="{{ url_for('usun_zbiorka', zbiorka_id=z.id) }}" method="post" style="display: inline;">
|
|
||||||
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('Czy na pewno chcesz usunąć tę zbiórkę?');">Usuń</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% else %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="4" class="text-center">Brak aktywnych zbiórek</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tabela zbiórek zrealizowanych -->
|
<!-- Pigułki: Aktywne / Zrealizowane (zakładki w obrębie panelu) -->
|
||||||
<h4>Zrealizowane zbiórki</h4>
|
<ul class="nav nav-pills mb-3" id="adminTabs" role="tablist">
|
||||||
<div class="table-responsive">
|
<li class="nav-item" role="presentation">
|
||||||
<table class="table table-dark table-striped table-hover">
|
<button class="nav-link active" id="tab-aktywne" data-bs-toggle="tab" data-bs-target="#pane-aktywne"
|
||||||
<thead>
|
type="button" role="tab" aria-controls="pane-aktywne" aria-selected="true">
|
||||||
<tr>
|
Aktywne zbiórki
|
||||||
<th>ID</th>
|
</button>
|
||||||
<th>Nazwa</th>
|
</li>
|
||||||
<th>Widoczność</th>
|
<li class="nav-item" role="presentation">
|
||||||
<th>Opcje</th>
|
<button class="nav-link" id="tab-zrealizowane" data-bs-toggle="tab" data-bs-target="#pane-zrealizowane"
|
||||||
</tr>
|
type="button" role="tab" aria-controls="pane-zrealizowane" aria-selected="false">
|
||||||
</thead>
|
Zrealizowane
|
||||||
<tbody>
|
</button>
|
||||||
{% for z in completed_zbiorki %}
|
</li>
|
||||||
<tr>
|
</ul>
|
||||||
<td>{{ z.id }}</td>
|
|
||||||
<td>{{ z.nazwa }}</td>
|
<div class="tab-content">
|
||||||
<td>
|
|
||||||
{% if z.ukryta %}
|
<!-- PANE: Aktywne -->
|
||||||
<span class="badge bg-secondary">Ukryta</span>
|
<div class="tab-pane fade show active" id="pane-aktywne" role="tabpanel" aria-labelledby="tab-aktywne"
|
||||||
{% else %}
|
tabindex="0">
|
||||||
<span class="badge bg-success">Widoczna</span>
|
|
||||||
{% endif %}
|
{% if active_zbiorki and active_zbiorki|length > 0 %}
|
||||||
</td>
|
<div class="table-responsive mb-5">
|
||||||
<td>
|
<table class="table table-dark table-striped table-hover align-middle">
|
||||||
<a href="{{ url_for('edytuj_zbiorka', zbiorka_id=z.id) }}" class="btn btn-primary btn-sm">Edytuj</a>
|
<thead>
|
||||||
<a href="{{ url_for('admin_dodaj_wplate', zbiorka_id=z.id) }}" class="btn btn-warning btn-sm">Dodaj wpłatę</a>
|
<tr>
|
||||||
<a href="{{ url_for('edytuj_stan', zbiorka_id=z.id) }}" class="btn btn-info btn-sm">Edytuj stan</a>
|
<th style="width:72px;">ID</th>
|
||||||
<form action="{{ url_for('toggle_visibility', zbiorka_id=z.id) }}" method="post" style="display: inline;">
|
<th>Nazwa</th>
|
||||||
<button type="submit" class="btn btn-secondary btn-sm">
|
<th style="width:140px;">Widoczność</th>
|
||||||
{% if z.ukryta %} Pokaż {% else %} Ukryj {% endif %}
|
<th style="width:1%;">Opcje</th>
|
||||||
</button>
|
</tr>
|
||||||
</form>
|
</thead>
|
||||||
<form action="{{ url_for('usun_zbiorka', zbiorka_id=z.id) }}" method="post" style="display: inline;">
|
<tbody>
|
||||||
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('Czy na pewno chcesz usunąć tę zbiórkę?');">Usuń</button>
|
{% for z in active_zbiorki %}
|
||||||
</form>
|
<tr>
|
||||||
</td>
|
<td class="text-muted">{{ z.id }}</td>
|
||||||
</tr>
|
<td>
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<span class="fw-semibold">{{ z.nazwa }}</span>
|
||||||
|
{# opcjonalnie: mini-meta z celem/stanem jeśli masz te pola #}
|
||||||
|
{% if z.cel is defined or z.stan is defined %}
|
||||||
|
<small class="text-muted">
|
||||||
|
{% if z.cel is defined %} Cel: {{ z.cel|round(2) }} PLN {% endif %}
|
||||||
|
{% if z.stan is defined %} · Stan: {{ z.stan|round(2) }} PLN {% endif %}
|
||||||
|
</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if z.ukryta %}
|
||||||
|
<span class="badge bg-secondary border"
|
||||||
|
style="border-color: var(--border);">Ukryta</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge rounded-pill"
|
||||||
|
style="background: var(--accent); color:#111;">Widoczna</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<!-- Grupa akcji: główne + rozwijane -->
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="{{ url_for('edytuj_zbiorka', zbiorka_id=z.id) }}"
|
||||||
|
class="btn btn-sm btn-outline-light">Edytuj</a>
|
||||||
|
<button class="btn btn-sm btn-outline-light dropdown-toggle dropdown-toggle-split"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false" aria-label="Więcej opcji">
|
||||||
|
<span class="visually-hidden">Więcej</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-dark dropdown-menu-end shadow">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item"
|
||||||
|
href="{{ url_for('dodaj_wplate', zbiorka_id=z.id) }}">Dodaj
|
||||||
|
wpłatę</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item"
|
||||||
|
href="{{ url_for('edytuj_stan', zbiorka_id=z.id) }}">Edytuj stan</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<form action="{{ url_for('oznacz_zbiorka', zbiorka_id=z.id) }}"
|
||||||
|
method="post" class="m-0">
|
||||||
|
<button type="submit" class="dropdown-item">Oznacz jako
|
||||||
|
zrealizowana</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<form action="{{ url_for('zmien_widzialnosc', zbiorka_id=z.id) }}"
|
||||||
|
method="post" class="m-0">
|
||||||
|
<button type="submit" class="dropdown-item">
|
||||||
|
{% if z.ukryta %}Pokaż{% else %}Ukryj{% endif %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<form action="{{ url_for('usun_zbiorka', zbiorka_id=z.id) }}" method="post"
|
||||||
|
class="m-0"
|
||||||
|
onsubmit="return confirm('Czy na pewno chcesz usunąć tę zbiórkę?');">
|
||||||
|
<button type="submit" class="dropdown-item text-danger">Usuń</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr>
|
<!-- Empty state -->
|
||||||
<td colspan="4" class="text-center">Brak zbiórek zrealizowanych</td>
|
<div class="card">
|
||||||
</tr>
|
<div class="card-body text-center py-5">
|
||||||
{% endfor %}
|
<h5 class="mb-2">Brak aktywnych zbiórek</h5>
|
||||||
</tbody>
|
<p class="text-muted mb-4">Wygląda na to, że teraz nic nie zbieramy.</p>
|
||||||
</table>
|
<a href="{{ url_for('dodaj_zbiorka') }}" class="btn btn-primary">Utwórz nową zbiórkę</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PANE: Zrealizowane -->
|
||||||
|
<div class="tab-pane fade" id="pane-zrealizowane" role="tabpanel" aria-labelledby="tab-zrealizowane"
|
||||||
|
tabindex="0">
|
||||||
|
|
||||||
|
{% if completed_zbiorki and completed_zbiorki|length > 0 %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-striped table-hover align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:72px;">ID</th>
|
||||||
|
<th>Nazwa</th>
|
||||||
|
<th style="width:180px;">Status</th>
|
||||||
|
<th style="width:1%;">Opcje</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for z in completed_zbiorki %}
|
||||||
|
<tr>
|
||||||
|
<td class="text-muted">{{ z.id }}</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<span class="fw-semibold">{{ z.nazwa }}</span>
|
||||||
|
{% if z.cel is defined or z.stan is defined %}
|
||||||
|
<small class="text-muted">
|
||||||
|
{% if z.cel is defined %} Cel: {{ z.cel|round(2) }} PLN {% endif %}
|
||||||
|
{% if z.stan is defined %} · Zebrano: {{ z.stan|round(2) }} PLN {% endif %}
|
||||||
|
</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||||
|
<span class="badge rounded-pill"
|
||||||
|
style="background: var(--accent); color:#111;">Zrealizowana</span>
|
||||||
|
{% if z.ukryta %}
|
||||||
|
<span class="badge bg-secondary border"
|
||||||
|
style="border-color: var(--border);">Ukryta</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-success">Widoczna</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="{{ url_for('edytuj_zbiorka', zbiorka_id=z.id) }}"
|
||||||
|
class="btn btn-sm btn-outline-light">Edytuj</a>
|
||||||
|
<button class="btn btn-sm btn-outline-light dropdown-toggle dropdown-toggle-split"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false" aria-label="Więcej opcji">
|
||||||
|
<span class="visually-hidden">Więcej</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-dark dropdown-menu-end shadow">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item"
|
||||||
|
href="{{ url_for('dodaj_wplate', zbiorka_id=z.id) }}">Dodaj
|
||||||
|
wpłatę</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item"
|
||||||
|
href="{{ url_for('edytuj_stan', zbiorka_id=z.id) }}">Edytuj stan</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<form action="{{ url_for('zmien_widzialnosc', zbiorka_id=z.id) }}"
|
||||||
|
method="post" class="m-0">
|
||||||
|
<button type="submit" class="dropdown-item">
|
||||||
|
{% if z.ukryta %}Pokaż{% else %}Ukryj{% endif %}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<hr class="dropdown-divider">
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<form action="{{ url_for('usun_zbiorka', zbiorka_id=z.id) }}" method="post"
|
||||||
|
class="m-0"
|
||||||
|
onsubmit="return confirm('Czy na pewno chcesz usunąć tę zbiórkę?');">
|
||||||
|
<button type="submit" class="dropdown-item text-danger">Usuń</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center py-5">
|
||||||
|
<h5 class="mb-2">Brak zbiórek zrealizowanych</h5>
|
||||||
|
<p class="text-muted mb-3">Gdy jakaś zbiórka osiągnie 100%, pojawi się tutaj.</p>
|
||||||
|
<a href="{{ url_for('dodaj_zbiorka') }}" class="btn btn-outline-light border">Utwórz nową
|
||||||
|
zbiórkę</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
86
templates/admin/dodaj_wplate.html
Normal file
86
templates/admin/dodaj_wplate.html
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}Dodaj wpłatę{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container my-4">
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-3">
|
||||||
|
<a href="{{ url_for('zbiorka', zbiorka_id=zbiorka.id) }}" class="btn btn-sm btn-outline-light border">← Powrót do
|
||||||
|
zbiórki</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-secondary text-white d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||||
|
<h3 class="card-title mb-0">Dodaj wpłatę: <span class="fw-semibold">{{ zbiorka.nazwa }}</span></h3>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
{% if zbiorka.cel %}
|
||||||
|
<span class="badge bg-dark border" style="border-color: var(--border);">Cel: {{ zbiorka.cel|round(2) }}
|
||||||
|
PLN</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="badge bg-dark border" style="border-color: var(--border);">Stan: {{ zbiorka.stan|round(2) }}
|
||||||
|
PLN</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% 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) %} <div class="px-3 pt-3">
|
||||||
|
<div class="progress" role="progressbar" aria-valuenow="{{ progress_clamped|round(2) }}" aria-valuemin="0"
|
||||||
|
aria-valuemax="100" aria-label="Postęp zbiórki {{ progress_clamped|round(0) }} procent">
|
||||||
|
<div class="progress-bar" style="width: {{ progress_clamped }}%;"></div>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted d-block mt-1 mb-2">{{ progress|round(1) }}%</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body pt-0">
|
||||||
|
<form method="post" novalidate>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="kwota" class="form-label">Kwota wpłaty</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">PLN</span>
|
||||||
|
<input type="number" step="0.01" min="0.01" inputmode="decimal" class="form-control" id="kwota" name="kwota"
|
||||||
|
placeholder="0,00" required aria-describedby="kwotaHelp">
|
||||||
|
</div>
|
||||||
|
<div id="kwotaHelp" class="form-text">Podaj kwotę w złotówkach (min. 0,01).</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-wrap gap-2 mt-2">
|
||||||
|
{% for preset in [10,25,50,100,200] %}
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-light border btn-kwota" data-amount="{{ preset }}">
|
||||||
|
{{ preset }} PLN
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
{% if zbiorka.cel and zbiorka.cel > 0 %}
|
||||||
|
{% set brakujace = (zbiorka.cel - zbiorka.stan) if (zbiorka.cel - zbiorka.stan) > 0 else 0 %}
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-light border btn-kwota"
|
||||||
|
data-amount="{{ brakujace|round(2) }}">
|
||||||
|
Do celu: {{ brakujace|round(2) }} PLN
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="opis" class="form-label">Opis (opcjonalnie)</label>
|
||||||
|
<textarea class="form-control" id="opis" name="opis" rows="3" maxlength="300"
|
||||||
|
aria-describedby="opisHelp"></textarea>
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<small id="opisHelp" class="form-text text-muted">Krótka notatka do wpłaty (widoczna w systemie).</small>
|
||||||
|
<small class="text-muted"><span id="opisCount">0</span>/300</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<button type="submit" class="btn btn-success">Dodaj wpłatę</button>
|
||||||
|
<a href="{{ url_for('zbiorka', zbiorka_id=zbiorka.id) }}" class="btn btn-outline-light border">Anuluj</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
{% block extra_scripts %}
|
||||||
|
{{ super() }}
|
||||||
|
<script src="{{ url_for('static', filename='js/dodaj_wplate.js') }}"></script>
|
||||||
|
{% endblock %}
|
125
templates/admin/dodaj_zbiorke.html
Normal file
125
templates/admin/dodaj_zbiorke.html
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}Dodaj zbiórkę{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_head %}
|
||||||
|
{{ super() }}
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container my-4">
|
||||||
|
|
||||||
|
<!-- Powrót -->
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-3">
|
||||||
|
<a href="{{ url_for('admin_dashboard') }}" class="btn btn-sm btn-outline-light border">← Panel Admina</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-secondary text-white d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||||
|
<h3 class="card-title mb-0">Dodaj nową zbiórkę</h3>
|
||||||
|
<small class="opacity-75">Uzupełnij podstawowe dane i dane płatności</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" novalidate>
|
||||||
|
{# {{ form.csrf_token }} jeśli używasz Flask-WTF #}
|
||||||
|
|
||||||
|
<!-- SEKCJA: Podstawowe -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="text-muted mb-2">Podstawowe</h6>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="nazwa" class="form-label">Nazwa zbiórki</label>
|
||||||
|
<input type="text" class="form-control" id="nazwa" name="nazwa" maxlength="120"
|
||||||
|
placeholder="Np. Wsparcie dla schroniska 'Azor'" required aria-describedby="nazwaHelp">
|
||||||
|
<div id="nazwaHelp" class="form-text">Krótki, zrozumiały tytuł. Max 120 znaków.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="opis" class="form-label">Opis (Markdown)</label>
|
||||||
|
<textarea class="form-control" id="opis" name="opis" rows="8" required
|
||||||
|
aria-describedby="opisHelp"></textarea>
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<small id="opisHelp" class="form-text text-muted">
|
||||||
|
Możesz używać **Markdown** (nagłówki, listy, linki). W edytorze włącz podgląd 👁️.
|
||||||
|
</small>
|
||||||
|
<small class="text-muted"><span id="opisCount">0</span> znaków</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4" />
|
||||||
|
|
||||||
|
<!-- SEKCJA: Płatności -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="text-muted mb-2">Dane płatności</h6>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="numer_konta" class="form-label">Numer konta (IBAN)</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">PL</span>
|
||||||
|
<input type="text" class="form-control" id="numer_konta" name="numer_konta"
|
||||||
|
value="{{ global_settings.numer_konta if global_settings else '' }}" inputmode="numeric"
|
||||||
|
autocomplete="off" placeholder="12 3456 7890 1234 5678 9012 3456" required
|
||||||
|
aria-describedby="ibanHelp">
|
||||||
|
</div>
|
||||||
|
<div id="ibanHelp" class="form-text">Wpisz ciąg cyfr; spacje dodadzą się automatycznie dla czytelności.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label for="numer_telefonu_blik" class="form-label">Numer telefonu BLIK</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">+48</span>
|
||||||
|
<input type="tel" class="form-control" id="numer_telefonu_blik" name="numer_telefonu_blik"
|
||||||
|
value="{{ global_settings.numer_telefonu_blik if global_settings else '' }}" inputmode="tel"
|
||||||
|
pattern="[0-9 ]{9,13}" placeholder="123 456 789" required aria-describedby="blikHelp">
|
||||||
|
</div>
|
||||||
|
<div id="blikHelp" class="form-text">Dziewięć cyfr telefonu powiązanego z BLIK. Spacje opcjonalne.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4" />
|
||||||
|
|
||||||
|
<!-- SEKCJA: Cel i widoczność -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="text-muted mb-2">Cel i widoczność</h6>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label for="cel" class="form-label">Cel zbiórki</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">PLN</span>
|
||||||
|
<input type="number" class="form-control" id="cel" name="cel" step="0.01" min="0.01" placeholder="0,00"
|
||||||
|
required aria-describedby="celHelp">
|
||||||
|
</div>
|
||||||
|
<div id="celHelp" class="form-text">Minimalnie 0,01 PLN. Możesz to później edytować.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6 d-flex align-items-end">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" id="ukryj_kwote" name="ukryj_kwote">
|
||||||
|
<label class="form-check-label" for="ukryj_kwote">Ukryj kwoty (cel i stan)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<button type="submit" class="btn btn-success">Dodaj zbiórkę</button>
|
||||||
|
<a href="{{ url_for('admin_dashboard') }}" class="btn btn-outline-light border">Anuluj</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
|
{{ super() }}
|
||||||
|
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/mde_custom.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/dodaj_zbiorke.js') }}"></script>
|
||||||
|
{% endblock %}
|
@@ -1,67 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
{% block title %}Edytuj zbiórkę{% endblock %}
|
|
||||||
{% block content %}
|
|
||||||
<div class="container my-4">
|
|
||||||
<div class="card shadow-sm">
|
|
||||||
<div class="card-header bg-success text-white">
|
|
||||||
<h3 class="card-title mb-0">Edytuj zbiórkę</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="post">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="nazwa" class="form-label">Nazwa zbiórki</label>
|
|
||||||
<input type="text" class="form-control" id="nazwa" name="nazwa" value="{{ zbiorka.nazwa }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="opis" class="form-label">Opis</label>
|
|
||||||
<textarea class="form-control" id="opis" name="opis" rows="6" required>{{ zbiorka.opis }}</textarea>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="numer_konta" class="form-label">Numer konta</label>
|
|
||||||
<input type="text" class="form-control" id="numer_konta" name="numer_konta"
|
|
||||||
value="{{ zbiorka.numer_konta if zbiorka.numer_konta else (global_settings.numer_konta if global_settings else '') }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="numer_telefonu_blik" class="form-label">Numer telefonu BLIK</label>
|
|
||||||
<input type="text" class="form-control" id="numer_telefonu_blik" name="numer_telefonu_blik"
|
|
||||||
value="{{ zbiorka.numer_telefonu_blik if zbiorka.numer_telefonu_blik else (global_settings.numer_telefonu_blik if global_settings else '') }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="cel" class="form-label">Cel zbiórki (PLN)</label>
|
|
||||||
<input type="number" step="0.01" class="form-control" id="cel" name="cel" value="{{ zbiorka.cel }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-check mb-3">
|
|
||||||
<input type="checkbox" class="form-check-input" id="ukryj_kwote" name="ukryj_kwote" {% if zbiorka.ukryj_kwote %}checked{% endif %}>
|
|
||||||
<label class="form-check-label" for="ukryj_kwote">Ukryj kwoty (cel i stan)</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" class="btn btn-primary">Zaktualizuj zbiórkę</button>
|
|
||||||
<button type="button" class="btn btn-primary" id="ustaw-globalne">Ustaw globalne</button>
|
|
||||||
|
|
||||||
|
|
||||||
<form action="{{ url_for('oznacz_zbiorka', zbiorka_id=zbiorka.id) }}" method="post" style="display:inline;">
|
|
||||||
<button type="submit" class="btn btn-warning">Oznacz jako zrealizowana</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Inicjalizacja edytora Markdown (SimpleMDE) -->
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
|
|
||||||
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
|
|
||||||
<script>
|
|
||||||
var simplemde = new SimpleMDE({
|
|
||||||
element: document.getElementById("opis"),
|
|
||||||
forceSync: true
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
document.getElementById('ustaw-globalne').addEventListener('click', function() {
|
|
||||||
{% if global_settings %}
|
|
||||||
document.getElementById('numer_konta').value = "{{ global_settings.numer_konta }}";
|
|
||||||
document.getElementById('numer_telefonu_blik').value = "{{ global_settings.numer_telefonu_blik }}";
|
|
||||||
{% endif %}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
@@ -1,24 +1,128 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block title %}Edytuj stan zbiórki{% endblock %}
|
{% block title %}Edytuj stan zbiórki{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-4">
|
<div class="container my-4">
|
||||||
<div class="card shadow-sm">
|
|
||||||
<div class="card-header bg-info text-white">
|
<!-- Nawigacja -->
|
||||||
<h3 class="card-title mb-0">Edytuj stan zbiórki: {{ zbiorka.nazwa }}</h3>
|
<div class="d-flex align-items-center gap-2 mb-3">
|
||||||
|
<a href="{{ url_for('zbiorka', zbiorka_id=zbiorka.id) }}" class="btn btn-sm btn-outline-light border">← Szczegóły
|
||||||
|
zbiórki</a>
|
||||||
|
<a href="{{ url_for('admin_dashboard') }}" class="btn btn-sm btn-outline-light border">← Panel Admina</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# 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) %} <div class="card shadow-sm"
|
||||||
|
data-module="edit-stan" data-cel="{{ (zbiorka.cel|round(2)) if has_cel else 0 }}">
|
||||||
|
<div class="card-header bg-secondary text-white d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||||
|
<h3 class="card-title mb-0">Edytuj stan: <span class="fw-semibold">{{ zbiorka.nazwa }}</span></h3>
|
||||||
|
<div class="d-flex align-items-center flex-wrap gap-2">
|
||||||
|
{% if has_cel %}
|
||||||
|
<span class="badge bg-dark border" style="border-color: var(--border);">Cel: {{ zbiorka.cel|round(2) }}
|
||||||
|
PLN</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="badge bg-dark border" style="border-color: var(--border);">Obecnie: {{ zbiorka.stan|round(2) }}
|
||||||
|
PLN</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
|
||||||
<form method="post">
|
<!-- Mini progress (aktualny) -->
|
||||||
<div class="mb-3">
|
<div class="px-3 pt-3">
|
||||||
<label for="stan" class="form-label">Nowy stan zbiórki (PLN)</label>
|
<div class="progress" role="progressbar" aria-valuenow="{{ progress_clamped|round(2) }}" aria-valuemin="0"
|
||||||
<div class="input-group">
|
aria-valuemax="100" aria-label="Postęp zbiórki {{ progress_clamped|round(0) }} procent">
|
||||||
<span class="input-group-text">PLN</span>
|
<div class="progress-bar" style="width: {{ progress_clamped }}%;"></div>
|
||||||
<input type="number" step="0.01" class="form-control" id="stan" name="stan" value="{{ zbiorka.stan|round(2) }}" required>
|
|
||||||
|
</div>
|
||||||
|
<small class="text-muted d-block mt-1 mb-2">Aktualnie: {{ progress|round(1) }}%</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body pt-0">
|
||||||
|
<form method="post" novalidate>
|
||||||
|
{# {{ form.csrf_token }} #}
|
||||||
|
|
||||||
|
<!-- Nowy stan -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="stan" class="form-label">Nowy stan zbiórki</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">PLN</span>
|
||||||
|
<input type="number" step="0.01" min="0" class="form-control" id="stan" name="stan"
|
||||||
|
value="{{ zbiorka.stan|round(2) }}" required aria-describedby="stanHelp">
|
||||||
|
</div>
|
||||||
|
<div id="stanHelp" class="form-text">
|
||||||
|
Wpisz łączną zebraną kwotę po zmianie (nie przyrost). Skorzystaj z szybkich korekt poniżej.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Szybkie korekty -->
|
||||||
|
<div class="d-flex flex-wrap gap-2 mt-2">
|
||||||
|
{% for delta in [10,50,100,200] %}
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-light border btn-delta" data-delta="{{ delta }}">+{{
|
||||||
|
delta }} PLN</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-light border btn-delta" data-delta="-{{ delta }}">-{{
|
||||||
|
delta }} PLN</button>
|
||||||
|
{% endfor %}
|
||||||
|
{% if has_cel %}
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-light border btn-set"
|
||||||
|
data-value="{{ zbiorka.cel|round(2) }}">Ustaw: do celu</button>
|
||||||
|
{% set brakujace = (zbiorka.cel - zbiorka.stan) if (zbiorka.cel - zbiorka.stan) > 0 else 0 %}
|
||||||
|
{% if brakujace > 0 %}
|
||||||
|
<span class="badge bg-dark border" style="border-color: var(--border);">Brakuje: {{ brakujace|round(2) }}
|
||||||
|
PLN</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-light border btn-set" data-value="0">Ustaw: 0</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Podgląd po zmianie -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="card bg-dark border" style="border-color: var(--border);">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
|
||||||
|
<div>
|
||||||
|
<div class="small text-muted">Podgląd po zapisaniu</div>
|
||||||
|
<div class="fw-semibold">
|
||||||
|
Procent realizacji:
|
||||||
|
<span id="previewPct">{{ progress|round(1) }}</span>%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class="progress" aria-hidden="true">
|
||||||
|
<div class="progress-bar" style="width: {{ progress_clamped }}%;"></div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<small class="text-muted d-block mt-1" id="previewNote">
|
||||||
|
{% 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 %}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-info">Aktualizuj stan</button>
|
</div>
|
||||||
<a href="{{ url_for('admin_dashboard') }}" class="btn btn-secondary">Powrót</a>
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<button type="submit" class="btn btn-success">Aktualizuj stan</button>
|
||||||
|
<a href="{{ url_for('zbiorka', zbiorka_id=zbiorka.id) }}" class="btn btn-outline-light border">Anuluj</a>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
|
{{ super() }}
|
||||||
|
<script src="{{ url_for('static', filename='js/edytuj_stan.js') }}"></script>
|
||||||
|
{% endblock %}
|
154
templates/admin/edytuj_zbiorke.html
Normal file
154
templates/admin/edytuj_zbiorke.html
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}Edytuj zbiórkę{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_head %}
|
||||||
|
{{ super() }}
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container my-4">
|
||||||
|
|
||||||
|
<!-- Nawigacja -->
|
||||||
|
<div class="d-flex align-items-center gap-2 mb-3">
|
||||||
|
<a href="{{ url_for('zbiorka', zbiorka_id=zbiorka.id) }}" class="btn btn-sm btn-outline-light border">← Szczegóły
|
||||||
|
zbiórki</a>
|
||||||
|
<a href="{{ url_for('admin_dashboard') }}" class="btn btn-sm btn-outline-light border">← Panel Admina</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nagłówek + meta -->
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-header bg-secondary text-white d-flex flex-wrap align-items-center justify-content-between gap-2">
|
||||||
|
<h3 class="card-title mb-0">Edytuj zbiórkę</h3>
|
||||||
|
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||||
|
{% if zbiorka.cel %}
|
||||||
|
<span class="badge bg-dark border" style="border-color: var(--border);">Cel: {{ zbiorka.cel|round(2) }}
|
||||||
|
PLN</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if zbiorka.ukryj_kwote %}
|
||||||
|
<span class="badge bg-secondary">Kwoty ukryte</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-success">Kwoty widoczne</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<!-- GŁÓWNY FORMULARZ -->
|
||||||
|
<form method="post" novalidate id="form-edit-zbiorka">
|
||||||
|
{# {{ form.csrf_token }} jeśli używasz Flask-WTF #}
|
||||||
|
|
||||||
|
<!-- SEKCJA: Podstawowe -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="text-muted mb-2">Podstawowe</h6>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="nazwa" class="form-label">Nazwa zbiórki</label>
|
||||||
|
<input type="text" class="form-control" id="nazwa" name="nazwa" value="{{ zbiorka.nazwa }}"
|
||||||
|
maxlength="120" placeholder="Krótki, zrozumiały tytuł" required aria-describedby="nazwaHelp">
|
||||||
|
<div id="nazwaHelp" class="form-text">Max 120 znaków. Użyj konkretów.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="opis" class="form-label">Opis (Markdown)</label>
|
||||||
|
<textarea class="form-control" id="opis" name="opis" rows="8" required
|
||||||
|
aria-describedby="opisHelp">{{ zbiorka.opis }}</textarea>
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<small id="opisHelp" class="form-text text-muted">Wspieramy **Markdown** — użyj nagłówków, list, linków.
|
||||||
|
Włącz podgląd w edytorze 👁️.</small>
|
||||||
|
<small class="text-muted"><span id="opisCount">0</span> znaków</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4" />
|
||||||
|
|
||||||
|
<!-- SEKCJA: Dane płatności -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="text-muted mb-2">Dane płatności</h6>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="numer_konta" class="form-label">Numer konta (IBAN)</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">PL</span>
|
||||||
|
<input type="text" class="form-control" id="numer_konta" name="numer_konta"
|
||||||
|
value="{{ zbiorka.numer_konta if zbiorka.numer_konta else (global_settings.numer_konta if global_settings else '') }}"
|
||||||
|
inputmode="numeric" autocomplete="off" placeholder="12 3456 7890 1234 5678 9012 3456" required
|
||||||
|
aria-describedby="ibanHelp">
|
||||||
|
</div>
|
||||||
|
<div id="ibanHelp" class="form-text">Wpisz same cyfry — spacje dodadzą się automatycznie co 4 znaki.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label for="numer_telefonu_blik" class="form-label">Numer telefonu BLIK</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">+48</span>
|
||||||
|
<input type="tel" class="form-control" id="numer_telefonu_blik" name="numer_telefonu_blik"
|
||||||
|
value="{{ zbiorka.numer_telefonu_blik if zbiorka.numer_telefonu_blik else (global_settings.numer_telefonu_blik if global_settings else '') }}"
|
||||||
|
inputmode="tel" pattern="[0-9 ]{9,13}" placeholder="123 456 789" required aria-describedby="blikHelp">
|
||||||
|
</div>
|
||||||
|
<div id="blikHelp" class="form-text">9 cyfr. Spacje dodadzą się automatycznie (format 3-3-3).</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6 d-flex align-items-end">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-light border" id="ustaw-globalne"
|
||||||
|
title="Wstaw wartości z ustawień globalnych" {% if global_settings %}
|
||||||
|
data-iban="{{ global_settings.numer_konta }}" data-blik="{{ global_settings.numer_telefonu_blik }}" {%
|
||||||
|
endif %}>Ustaw globalne</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4" />
|
||||||
|
|
||||||
|
<!-- SEKCJA: Cel i widoczność -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="text-muted mb-2">Cel i widoczność</h6>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label for="cel" class="form-label">Cel zbiórki</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">PLN</span>
|
||||||
|
<input type="number" class="form-control" id="cel" name="cel" step="0.01" min="0.01"
|
||||||
|
value="{{ zbiorka.cel }}" placeholder="0,00" required aria-describedby="celHelp">
|
||||||
|
</div>
|
||||||
|
<div id="celHelp" class="form-text">Minimalnie 0,01 PLN. W razie potrzeby zmienisz to później.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6 d-flex align-items-end">
|
||||||
|
<div class="form-check form-switch">
|
||||||
|
<input class="form-check-input" type="checkbox" id="ukryj_kwote" name="ukryj_kwote" {% if
|
||||||
|
zbiorka.ukryj_kwote %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="ukryj_kwote">Ukryj kwoty (cel i stan)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CTA: zapisz/anuluj + osobna akcja „zrealizowana” -->
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<button type="submit" class="btn btn-success">Zaktualizuj zbiórkę</button>
|
||||||
|
<a href="{{ url_for('zbiorka', zbiorka_id=zbiorka.id) }}" class="btn btn-outline-light border">Anuluj</a>
|
||||||
|
|
||||||
|
<!-- Osobny formularz dla oznaczenia „zrealizowana” -->
|
||||||
|
<form action="{{ url_for('oznacz_zbiorka', zbiorka_id=zbiorka.id) }}" method="post" class="ms-auto">
|
||||||
|
<button type="submit" class="btn btn-outline-light"
|
||||||
|
onclick="return confirm('Oznaczyć zbiórkę jako zrealizowaną?');">
|
||||||
|
Oznacz jako zrealizowana
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
|
{{ super() }}
|
||||||
|
<script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/mde_custom.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/edytuj_zbiorke.js') }}"></script>
|
||||||
|
{% endblock %}
|
@@ -1,55 +0,0 @@
|
|||||||
{% extends 'base.html' %}
|
|
||||||
{% block title %}Ustawienia globalne{% endblock %}
|
|
||||||
{% block content %}
|
|
||||||
<div class="container my-4">
|
|
||||||
<form method="post">
|
|
||||||
<!-- Blok ustawień konta -->
|
|
||||||
<div class="card shadow-sm mb-4">
|
|
||||||
<div class="card-header bg-primary text-white">
|
|
||||||
<h3 class="card-title mb-0">Ustawienia konta</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="numer_konta" class="form-label">Globalny numer konta</label>
|
|
||||||
<input type="text" class="form-control" id="numer_konta" name="numer_konta" value="{{ settings.numer_konta if settings else '' }}" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="numer_telefonu_blik" class="form-label">Globalny numer telefonu BLIK</label>
|
|
||||||
<input type="text" class="form-control" id="numer_telefonu_blik" name="numer_telefonu_blik" value="{{ settings.numer_telefonu_blik if settings else '' }}" required>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Blok dozwolonych adresów IP -->
|
|
||||||
<div class="card shadow-sm mb-4">
|
|
||||||
<div class="card-header bg-secondary text-white d-flex justify-content-between align-items-center">
|
|
||||||
<h3 class="card-title mb-0">Dozwolone adresy IP</h3>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="allowed_login_hosts" class="form-label">Dozwolone hosty logowania</label>
|
|
||||||
<textarea class="form-control" id="allowed_login_hosts" name="allowed_login_hosts" rows="4" placeholder="Podaj adresy IP lub nazwy domen oddzielone przecinkami lub nowymi liniami">{{ settings.allowed_login_hosts if settings and settings.allowed_login_hosts else '' }}</textarea>
|
|
||||||
</div>
|
|
||||||
<p class="text-muted">Twój aktualny adres IP: <strong>{{ client_ip }}</strong></p>
|
|
||||||
<button type="button" class="btn btn-sm btn-light text-dark" onclick="dodajMojeIP()">Dodaj moje IP</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<button type="submit" class="btn btn-primary">Zapisz ustawienia</button>
|
|
||||||
<a href="{{ url_for('admin_dashboard') }}" class="btn btn-secondary">Powrót</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
function dodajMojeIP() {
|
|
||||||
const mojeIP = "{{ client_ip }}";
|
|
||||||
const textarea = document.getElementById("allowed_login_hosts");
|
|
||||||
if (!textarea.value.includes(mojeIP)) {
|
|
||||||
const separator = textarea.value.trim() === "" ? "" : "\n";
|
|
||||||
textarea.value += separator + mojeIP;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
125
templates/admin/ustawienia.html
Normal file
125
templates/admin/ustawienia.html
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}Ustawienia globalne{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container my-4">
|
||||||
|
<form method="post" novalidate id="form-global-settings">
|
||||||
|
{# {{ form.csrf_token }} jeśli używasz Flask-WTF #}
|
||||||
|
|
||||||
|
<!-- SEKCJA: Dane płatności -->
|
||||||
|
<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">
|
||||||
|
<h3 class="card-title mb-0">Dane płatności</h3>
|
||||||
|
<small class="opacity-75">Używane jako wartości domyślne przy dodawaniu/edycji zbiórek</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="numer_konta" class="form-label">Globalny numer konta (IBAN)</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">PL</span>
|
||||||
|
<input type="text" class="form-control" id="numer_konta" name="numer_konta"
|
||||||
|
value="{{ settings.numer_konta if settings else '' }}" inputmode="numeric" autocomplete="off"
|
||||||
|
placeholder="12 3456 7890 1234 5678 9012 3456" required aria-describedby="ibanHelp">
|
||||||
|
</div>
|
||||||
|
<div id="ibanHelp" class="form-text">Wpisz ciąg cyfr — spacje dodadzą się automatycznie co 4 znaki.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label for="numer_telefonu_blik" class="form-label">Globalny numer telefonu BLIK</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text">+48</span>
|
||||||
|
<input type="tel" class="form-control" id="numer_telefonu_blik" name="numer_telefonu_blik"
|
||||||
|
value="{{ settings.numer_telefonu_blik if settings else '' }}" inputmode="tel" pattern="[0-9 ]{9,13}"
|
||||||
|
placeholder="123 456 789" required aria-describedby="blikHelp">
|
||||||
|
</div>
|
||||||
|
<div id="blikHelp" class="form-text">9 cyfr. Spacje i format 3-3-3 dodajemy dla czytelności.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SEKCJA: Dostępy / biała lista IP -->
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3 align-items-end">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<label for="host_input" class="form-label">Dodaj pojedynczy IP/host</label>
|
||||||
|
<input type="text" class="form-control" id="host_input" placeholder="np. 203.0.113.42 lub corp.example.com"
|
||||||
|
aria-describedby="hostAddHelp">
|
||||||
|
<div id="hostAddHelp" class="form-text">Po wpisaniu kliknij „Dodaj do listy”. Duplikaty są pomijane.</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>
|
||||||
|
<button type="button" class="btn btn-light text-dark" id="btn-add-my-ip" data-my-ip="{{ client_ip }}">Dodaj
|
||||||
|
moje IP ({{ client_ip }})</button>
|
||||||
|
<button type="button" class="btn btn-outline-light border" id="btn-dedupe">Usuń duplikaty</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<label for="allowed_login_hosts" class="form-label">Dozwolone hosty logowania (jeden na linię lub rozdzielone
|
||||||
|
przecinkami)</label>
|
||||||
|
<textarea class="form-control" id="allowed_login_hosts" name="allowed_login_hosts" rows="6"
|
||||||
|
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>
|
||||||
|
<div class="d-flex justify-content-between mt-1">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6 class="text-muted mb-2">Branding</h6>
|
||||||
|
<div class="row g-3 align-items-end">
|
||||||
|
<!-- Logo -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="logo_url" class="form-label">Logo (adres URL PNG/SVG)</label>
|
||||||
|
<input type="text" class="form-control" id="logo_url" name="logo_url"
|
||||||
|
value="{{ settings.logo_url if settings else '' }}" placeholder="https://example.com/logo.png">
|
||||||
|
<div class="form-text">Najlepiej transparentne, do 60px wysokości.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tekst -->
|
||||||
|
<div class="col-md-6">
|
||||||
|
<label for="site_title" class="form-label">Tytuł w navbarze</label>
|
||||||
|
<input type="text" class="form-control" id="site_title" name="site_title"
|
||||||
|
value="{{ settings.site_title if settings else '' }}" placeholder="Np. Zbiórki unitraklub.pl">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Checkbox: logo w navbarze -->
|
||||||
|
<div class="form-check mt-3">
|
||||||
|
<input class="form-check-input" type="checkbox" id="show_logo_in_navbar" name="show_logo_in_navbar" {% if
|
||||||
|
settings and settings.show_logo_in_navbar %}checked{% endif %}>
|
||||||
|
<label class="form-check-label" for="show_logo_in_navbar">Wyświetlaj logo w navbarze</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if settings and settings.logo_url %}
|
||||||
|
<div class="mt-3">
|
||||||
|
<span class="form-text d-block mb-1">Podgląd logo:</span>
|
||||||
|
<img src="{{ settings.logo_url }}" alt="Logo preview" style="max-height:50px;">
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{{ url_for('admin_dashboard') }}" class="btn btn-outline-light border">Powrót</a>
|
||||||
|
<button type="submit" class="btn btn-primary">Zapisz ustawienia</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
|
{{ super() }}
|
||||||
|
<script src="{{ url_for('static', filename='js/ustawienia.js') }}"></script>
|
||||||
|
{% endblock %}
|
@@ -1,74 +1,83 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="pl">
|
<html lang="pl">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
|
||||||
<title>{% block title %}Aplikacja Zbiórek{% endblock %}</title>
|
<title>{% block title %}Aplikacja Zbiórek{% endblock %}</title>
|
||||||
<!-- Bootswatch Darkly - atrakcyjny ciemny motyw -->
|
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.0/dist/darkly/bootstrap.min.css">
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootswatch@5.3.0/dist/darkly/bootstrap.min.css">
|
||||||
<!-- Opcjonalny plik custom.css dla drobnych modyfikacji -->
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}" />
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/custom.css') }}">
|
{% block extra_head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-dark text-light">
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-dark bg-secondary">
|
<body class="d-flex flex-column min-vh-100">
|
||||||
|
<nav class="navbar navbar-expand-lg">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a class="navbar-brand" href="{{ url_for('index') }}">Zbiórki unitraklub.pl</a>
|
<a class="navbar-brand" href="{{ url_for('index') }}">
|
||||||
|
{% if global_settings and global_settings.show_logo_in_navbar and global_settings.logo_url %}
|
||||||
<!-- Przycisk rozwijania dla urządzeń mobilnych -->
|
<img src="{{ global_settings.logo_url }}" alt="Logo" style="max-height:40px; vertical-align:middle;">
|
||||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNavbar"
|
{% endif %}
|
||||||
aria-controls="mainNavbar" aria-expanded="false" aria-label="Toggle navigation">
|
{{ global_settings.site_title if global_settings and global_settings.site_title else "Zbiórki" }}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNavbar"
|
||||||
|
aria-controls="mainNavbar" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Nawigacja ukrywana na małych ekranach -->
|
|
||||||
<div class="collapse navbar-collapse" id="mainNavbar">
|
<div class="collapse navbar-collapse" id="mainNavbar">
|
||||||
<ul class="navbar-nav ms-auto">
|
<ul class="navbar-nav ms-auto">
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('index') }}">Aktualne zbiórki</a></li>
|
{% set hide_links = request.path == url_for('index') or request.path ==
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('zbiorki_zrealizowane') }}">Zrealizowane zbiórki</a></li>
|
url_for('zbiorki_zrealizowane') %}
|
||||||
{% if current_user.is_authenticated %}
|
{% if not hide_links %}
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin_dashboard') }}">Panel Admina</a></li>
|
<li class="nav-item">
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('logout') }}">Wyloguj</a></li>
|
<a class="nav-link" href="{{ url_for('index') }}">Aktualne zbiórki</a>
|
||||||
|
</li>
|
||||||
{% else %}
|
<li class="nav-item">
|
||||||
{% if is_ip_allowed %}
|
<a class="nav-link" href="{{ url_for('zbiorki_zrealizowane') }}">Zrealizowane zbiórki</a>
|
||||||
<li class="nav-item"><a class="nav-link" href="{{ url_for('login') }}">Zaloguj</a></li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if current_user.is_authenticated %}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('admin_dashboard') }}">Panel Admina</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('wyloguj') }}">Wyloguj</a></li>
|
||||||
|
{% else %}
|
||||||
|
{% if is_ip_allowed %}
|
||||||
|
<li class="nav-item"><a class="nav-link" href="{{ url_for('zaloguj') }}">Zaloguj</a></li>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="container mt-4">
|
<div class="container mt-4">
|
||||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for category, message in messages %}
|
{% for category, message in messages %}
|
||||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
<div class="alert alert-{{ category|default('secondary') }}">{{ message }}</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% block content %}{% endblock %}
|
|
||||||
|
<main class="flex-grow-1">
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- stopka -->
|
||||||
|
<footer class="mt-auto text-center py-3 border-top" style="background: var(--surface-0);">
|
||||||
|
{% if global_settings and global_settings.logo_url %}
|
||||||
|
<img src="{{ global_settings.logo_url }}" alt="Logo" style="max-height:60px; opacity:0.85;">
|
||||||
|
{% else %}
|
||||||
|
<small class="text-muted">© linuxiarz.pl</small>
|
||||||
|
{% endif %}
|
||||||
|
</footer>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<script>
|
<script src="{{ url_for('static', filename='js/progress.js') }}"></script>
|
||||||
// Funkcja aktualizująca animację paska postępu
|
|
||||||
function animateProgressBars() {
|
{% block extra_scripts %}{% endblock %}
|
||||||
document.querySelectorAll('.progress-bar').forEach(bar => {
|
|
||||||
const progressValue = bar.getAttribute('aria-valuenow');
|
|
||||||
bar.style.setProperty('--progress-width', progressBarWidth(progressBarValue(progressBar)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Funkcja wywoływana przy ładowaniu strony
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
document.querySelectorAll('.progress-bar').forEach(bar => {
|
|
||||||
const width = bar.getAttribute('aria-valuenow') + '%';
|
|
||||||
bar.style.setProperty('--progress-width', width);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
|
||||||
|
</html>
|
@@ -1,29 +1,88 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block title %}{% if request.path == url_for('zbiorki_zrealizowane') %}Zrealizowane zbiórki{% else %}Aktualnie aktywne zbiórki{% endif %}{% endblock %}
|
{% block title %}{% if request.path == url_for('zbiorki_zrealizowane') %}Zrealizowane zbiórki{% else %}Aktualnie aktywne
|
||||||
|
zbiórki{% endif %}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if request.path == url_for('zbiorki_zrealizowane') %}
|
{# Ustal kontekst listy #}
|
||||||
<h2 class="mb-4">Zrealizowane zbiórki</h2>
|
{% set is_completed_view = (request.path == url_for('zbiorki_zrealizowane')) %}
|
||||||
{% else %}
|
|
||||||
<h2 class="mb-4">Aktualnie aktywne zbiórki</h2>
|
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-4">
|
||||||
{% endif %}
|
<h2 class="mb-0">
|
||||||
<div class="row">
|
{% if is_completed_view %}Zrealizowane zbiórki{% else %}Aktualnie aktywne zbiórki{% endif %}
|
||||||
|
</h2>
|
||||||
|
<ul class="nav nav-pills">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if not is_completed_view %}active{% endif %}"
|
||||||
|
href="{{ url_for('index') }}">Aktywne</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if is_completed_view %}active{% endif %}"
|
||||||
|
href="{{ url_for('zbiorki_zrealizowane') }}">Zrealizowane</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if zbiorki and zbiorki|length > 0 %}
|
||||||
|
<div class="row g-4">
|
||||||
{% for z in zbiorki %}
|
{% for z in zbiorki %}
|
||||||
<div class="col-sm-12 col-md-6 col-lg-4 mb-4">
|
{% set progress = (z.stan / z.cel * 100) if z.cel > 0 else 0 %}
|
||||||
<div class="card">
|
{% set progress_clamped = 100 if progress > 100 else (0 if progress < 0 else progress) %} <div
|
||||||
<div class="card-body">
|
class="col-sm-12 col-md-6 col-lg-4">
|
||||||
<h5 class="card-title">{{ z.nazwa }}</h5>
|
<div class="card h-100 position-relative">
|
||||||
{% set progress = (z.stan / z.cel * 100) if z.cel > 0 else 0 %}
|
<div class="card-body d-flex flex-column">
|
||||||
<div class="progress mb-3">
|
<div class="d-flex align-items-start justify-content-between gap-2 mb-2">
|
||||||
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: {{ progress if progress < 100 else 100 }}%;" aria-valuenow="{{ progress }}" aria-valuemin="0" aria-valuemax="100">
|
<h5 class="card-title mb-0">{{ z.nazwa }}</h5>
|
||||||
{{ progress|round(2) }}%
|
|
||||||
</div>
|
{# Spójny badge zrealizowania: w zakładce „Zrealizowane” lub gdy >=100% #}
|
||||||
|
{% if is_completed_view or progress_clamped >= 100 %}
|
||||||
|
<span class="badge rounded-pill" style="background: var(--accent); color:#111;">Zrealizowana</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2 d-flex flex-wrap gap-2">
|
||||||
|
{% if z.cel > 0 %}
|
||||||
|
<span class="badge bg-dark border" style="border-color: var(--border);">Cel: {{ z.cel|round(2) }}
|
||||||
|
PLN</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="badge bg-dark border" style="border-color: var(--border);">Stan: {{ z.stan|round(2) }}
|
||||||
|
PLN</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-1">
|
||||||
|
<div class="progress" role="progressbar" aria-valuenow="{{ progress_clamped|round(2) }}"
|
||||||
|
aria-valuemin="0" aria-valuemax="100"
|
||||||
|
aria-label="Postęp zbiórki {{ progress_clamped|round(0) }} procent">
|
||||||
|
<div class="progress-bar" style="width: {{ progress_clamped }}%;"></div>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">{{ progress|round(1) }}%</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-auto pt-2">
|
||||||
|
<a href="{{ url_for('zbiorka', zbiorka_id=z.id) }}" class="stretched-link"></a>
|
||||||
|
<a href="{{ url_for('zbiorka', zbiorka_id=z.id) }}" class="btn btn-primary btn-sm">Szczegóły</a>
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ url_for('zbiorka', zbiorka_id=z.id) }}" class="btn btn-primary btn-sm">Szczegóły</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<p>Brak zbiórek</p>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center py-5">
|
||||||
|
{% if is_completed_view %}
|
||||||
|
<h5 class="mb-2">Brak zrealizowanych zbiórek</h5>
|
||||||
|
<p class="text-muted mb-4">Gdy jakaś zbiórka osiągnie 100%, pojawi się tutaj.</p>
|
||||||
|
<a href="{{ url_for('index') }}" class="btn btn-primary">Zobacz aktywne</a>
|
||||||
|
{% else %}
|
||||||
|
<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>
|
||||||
|
{% if current_user.is_authenticated and current_user.is_admin %}
|
||||||
|
<a href="{{ url_for('admin_dashboard') }}" class="btn btn-primary">Utwórz nową zbiórkę</a>
|
||||||
|
{% else %}
|
||||||
|
<a href="{{ url_for('zbiorki_zrealizowane') }}" class="btn btn-primary">Zobacz zrealizowane</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
@@ -1,19 +1,55 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block title %}Logowanie{% endblock %}
|
{% block title %}Logowanie{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-4">
|
<div class="container my-5">
|
||||||
<h3 class="mb-4">Logowanie</h3>
|
<div class="row justify-content-center">
|
||||||
<form method="post">
|
<div class="col-sm-10 col-md-8 col-lg-5">
|
||||||
<div class="mb-3">
|
<div class="card shadow-sm">
|
||||||
<label for="username" class="form-label">Nazwa użytkownika</label>
|
<div class="card-header">
|
||||||
<input type="text" class="form-control" id="username" name="username" required>
|
<h3 class="card-title mb-0">Logowanie</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label for="password" class="form-label">Hasło</label>
|
<div class="card-body">
|
||||||
<input type="password" class="form-control" id="password" name="password" required>
|
<form method="post" class="needs-validation" novalidate>
|
||||||
</div>
|
{% set next_url = request.args.get('next') %}
|
||||||
<button type="submit" class="btn btn-primary">Zaloguj</button>
|
{% if next_url %}
|
||||||
</form>
|
<input type="hidden" name="next" value="{{ next_url }}">
|
||||||
<p class="mt-3">Nie masz konta? <a href="{{ url_for('register') }}">Zarejestruj się</a></p>
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="username" class="form-label">Nazwa użytkownika</label>
|
||||||
|
<input type="text" class="form-control" id="username" name="username"
|
||||||
|
autocomplete="username" autocapitalize="none" spellcheck="false" required autofocus>
|
||||||
|
<div class="invalid-feedback">Podaj nazwę użytkownika.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="password" class="form-label d-flex justify-content-between align-items-center">
|
||||||
|
<span>Hasło</span>
|
||||||
|
<small id="capsWarning" class="text-muted" style="display:none;">CAPS LOCK
|
||||||
|
włączony</small>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" class="form-control" id="password" name="password"
|
||||||
|
autocomplete="current-password" required minlength="5">
|
||||||
|
<button type="button" class="btn btn-secondary rounded-end" id="togglePw"
|
||||||
|
aria-label="Pokaż/ukryj hasło">Pokaż</button>
|
||||||
|
<div class="invalid-feedback">Wpisz hasło (min. 5 znaków).</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Zaloguj</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/walidacja_logowanie.js') }}"></script>
|
||||||
|
{% endblock %}
|
@@ -1,18 +1,61 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block title %}Rejestracja{% endblock %}
|
{% block title %}Rejestracja{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-4">
|
<div class="container my-5">
|
||||||
<h3 class="mb-4">Rejestracja</h3>
|
<div class="row justify-content-center">
|
||||||
<form method="post">
|
<div class="col-sm-10 col-md-8 col-lg-5">
|
||||||
<div class="mb-3">
|
<div class="card shadow-sm">
|
||||||
<label for="username" class="form-label">Nazwa użytkownika</label>
|
<div class="card-header">
|
||||||
<input type="text" class="form-control" id="username" name="username" required>
|
<h3 class="card-title mb-0">Rejestracja</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label for="password" class="form-label">Hasło</label>
|
<div class="card-body">
|
||||||
<input type="password" class="form-control" id="password" name="password" required>
|
<form method="post" class="needs-validation" novalidate>
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Zarejestruj się</button>
|
<div class="mb-3">
|
||||||
</form>
|
<label for="username" class="form-label">Nazwa użytkownika</label>
|
||||||
|
<input type="text" class="form-control" id="username" name="username"
|
||||||
|
autocomplete="username" autocapitalize="none" spellcheck="false" required autofocus>
|
||||||
|
<div class="invalid-feedback">Podaj nazwę użytkownika.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="password" class="form-label d-flex justify-content-between align-items-center">
|
||||||
|
<span>Hasło</span>
|
||||||
|
<small id="capsWarning" class="text-muted" style="display:none;">CAPS LOCK
|
||||||
|
włączony</small>
|
||||||
|
</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" class="form-control" id="password" name="password"
|
||||||
|
autocomplete="new-password" required minlength="6">
|
||||||
|
<button type="button" class="btn btn-secondary" id="togglePw"
|
||||||
|
aria-label="Pokaż/ukryj hasło">Pokaż</button>
|
||||||
|
<div class="invalid-feedback">Hasło musi mieć min. 6 znaków.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label for="password2" class="form-label">Powtórz hasło</label>
|
||||||
|
<input type="password" class="form-control" id="password2" name="password2"
|
||||||
|
autocomplete="new-password" required minlength="6">
|
||||||
|
<div class="invalid-feedback">Powtórz hasło.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Zarejestruj się</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<p class="mt-3 mb-0 text-center">
|
||||||
|
Masz już konto?
|
||||||
|
<a href="{{ url_for('zaloguj') }}">Zaloguj się</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/walidacja_rejestracja.js') }}"></script>
|
||||||
|
{% endblock %}
|
@@ -1,94 +1,156 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block title %}{{ zbiorka.nazwa }}{% endblock %}
|
{% block title %}{{ zbiorka.nazwa }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-4">
|
<div class="container my-4">
|
||||||
<!-- Główna karta zbiórki -->
|
|
||||||
<div class="card mb-4 shadow-sm">
|
|
||||||
<div class="card-header bg-secondary text-white">
|
|
||||||
<h3 class="card-title mb-0"></h3>{{ zbiorka.nazwa }}</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<!-- Lewa kolumna: opis i postęp -->
|
|
||||||
<div class="col-md-8">
|
|
||||||
<h5>Opis:</h5>
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ zbiorka.opis | markdown }}
|
|
||||||
</div>
|
|
||||||
{% set progress = (zbiorka.stan / zbiorka.cel * 100) if zbiorka.cel > 0 else 0 %}
|
|
||||||
<h5>Postęp:</h5>
|
|
||||||
<div class="progress mb-3">
|
|
||||||
<div class="progress-bar progress-bar-striped progress-bar-animated"
|
|
||||||
role="progressbar"
|
|
||||||
style="width: {{ progress if progress < 100 else 100 }}%;"
|
|
||||||
aria-valuenow="{{ progress }}"
|
|
||||||
aria-valuemin="0"
|
|
||||||
aria-valuemax="100">
|
|
||||||
{{ progress|round(2) }}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<!-- Prawa kolumna: sekcja "Wspomóż" -->
|
|
||||||
<div class="col-md-4">
|
|
||||||
<div class="card wspomoz-card mb-3">
|
|
||||||
<div class="card-body">
|
|
||||||
|
|
||||||
<p class="card-text">
|
{# Postęp 0–100 #}
|
||||||
<strong>Numer konta:</strong>
|
{% set has_cel = (zbiorka.cel is defined and zbiorka.cel and zbiorka.cel > 0) %}
|
||||||
<span class="fs-4">{{ zbiorka.numer_konta }}</span>
|
{% set progress = (zbiorka.stan / zbiorka.cel * 100) if has_cel else 0 %}
|
||||||
</p>
|
{% set progress_clamped = 100 if progress > 100 else (0 if progress < 0 else progress) %} {% set
|
||||||
<p class="card-text">
|
is_done=(progress_clamped>= 100) %}
|
||||||
<strong>Telefon BLIK:</strong>
|
|
||||||
<span class="fs-4">{{ zbiorka.numer_telefonu_blik }}</span>
|
|
||||||
</p>
|
|
||||||
{% if not zbiorka.ukryj_kwote %}
|
|
||||||
<hr>
|
|
||||||
<p class="card-text">
|
|
||||||
<strong>Cel zbiórki:</strong>
|
|
||||||
<span class="fs-4">{{ zbiorka.cel|round(2) }} PLN</span>
|
|
||||||
</p>
|
|
||||||
<p class="card-text">
|
|
||||||
<strong>Stan zbiórki:</strong>
|
|
||||||
<span class="fs-4">{{ zbiorka.stan|round(2) }} PLN</span>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
<!-- Nagłówek -->
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3">
|
||||||
{% if current_user.is_authenticated and current_user.is_admin %}
|
<h2 class="mb-0">{{ zbiorka.nazwa }}</h2>
|
||||||
<a href="{{ url_for('admin_dodaj_wplate', zbiorka_id=zbiorka.id) }}" class="btn btn-primary">Dodaj wpłatę</a>
|
<div class="d-flex flex-wrap align-items-center gap-2">
|
||||||
|
{% if is_done %}
|
||||||
|
<span class="badge rounded-pill" style="background: var(--accent); color:#111;">Zrealizowana</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if zbiorka.ukryj_kwote %}
|
||||||
|
<span class="badge bg-secondary">Kwoty ukryte</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-success">Kwoty widoczne</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ url_for('index') }}" class="btn btn-primary">Powrót do listy</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="row g-4">
|
||||||
<!-- Karta z historią wpłat -->
|
<!-- Kolumna: opis + progress -->
|
||||||
<div class="card shadow-sm">
|
<div class="col-md-8">
|
||||||
<div class="card-header bg-secondary text-white">
|
<div class="card shadow-sm h-100">
|
||||||
<h3 class="card-title mb-0">Historia wpłat</h3>
|
<div class="card-body">
|
||||||
</div>
|
<h5 class="mb-2">Opis</h5>
|
||||||
<div class="card-body">
|
<div class="mb-4">
|
||||||
{% if zbiorka.wplaty|length > 0 %}
|
{{ zbiorka.opis | markdown }}
|
||||||
<ul class="list-group">
|
</div>
|
||||||
{% for w in zbiorka.wplaty %}
|
|
||||||
<li class="list-group-item">
|
<h5 class="mb-2">Postęp</h5>
|
||||||
<strong>{{ w.data.strftime('%Y-%m-%d %H:%M:%S') }}</strong> – {{ w.kwota|round(2) }} PLN
|
<div class="progress mb-2" role="progressbar" aria-valuenow="{{ progress_clamped|round(2) }}"
|
||||||
{% if w.opis %}
|
aria-valuemin="0" aria-valuemax="100" aria-label="Postęp zbiórki {{ progress_clamped|round(0) }} procent">
|
||||||
<em class="text-muted">({{ w.opis }})</em>
|
<div class="progress-bar" style="width: {{ progress_clamped }}%;"></div>
|
||||||
|
</div>
|
||||||
|
<small class="text-muted">
|
||||||
|
{% if zbiorka.ukryj_kwote %}
|
||||||
|
—
|
||||||
|
{% else %}
|
||||||
|
{{ progress|round(1) }}%
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</li>
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Kolumna: płatności (sticky) -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card shadow-sm wspomoz-card position-sticky" style="top: 1rem;">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<strong>Numer konta</strong>
|
||||||
|
<button class="btn btn-sm btn-outline-light border" type="button"
|
||||||
|
data-copy-target="#ibanDisplay">Kopiuj</button>
|
||||||
|
</div>
|
||||||
|
<div class="fs-5" id="ibanDisplay">{{ zbiorka.numer_konta }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="d-flex align-items-center justify-content-between">
|
||||||
|
<strong>Telefon BLIK</strong>
|
||||||
|
<button class="btn btn-sm btn-outline-light border" type="button"
|
||||||
|
data-copy-target="#blikDisplay">Kopiuj</button>
|
||||||
|
</div>
|
||||||
|
<div class="fs-5" id="blikDisplay">{{ zbiorka.numer_telefonu_blik }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if not zbiorka.ukryj_kwote %}
|
||||||
|
<hr class="my-3">
|
||||||
|
<div class="d-flex flex-column gap-1">
|
||||||
|
{% if has_cel %}
|
||||||
|
<div><strong>Cel:</strong> <span class="fs-6">{{ zbiorka.cel|round(2) }} PLN</span></div>
|
||||||
|
{% endif %}
|
||||||
|
<div><strong>Stan:</strong> <span class="fs-6">{{ zbiorka.stan|round(2) }} PLN</span></div>
|
||||||
|
{% if has_cel %}
|
||||||
|
{% set brak = (zbiorka.cel - zbiorka.stan) %}
|
||||||
|
<small class="text-muted">
|
||||||
|
{% if brak > 0 %}
|
||||||
|
Do celu brakuje: {{ brak|round(2) }} PLN
|
||||||
|
{% elif brak == 0 %}
|
||||||
|
Cel osiągnięty.
|
||||||
|
{% else %}
|
||||||
|
Przekroczono cel o: {{ (brak * -1)|round(2) }} PLN
|
||||||
|
{% endif %}
|
||||||
|
</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if current_user.is_authenticated and current_user.is_admin %}
|
||||||
|
<div class="d-grid mt-3">
|
||||||
|
<a href="{{ url_for('dodaj_wplate', zbiorka_id=zbiorka.id) }}" class="btn btn-primary">Dodaj
|
||||||
|
wpłatę</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Historia wpłat -->
|
||||||
|
<div class="card shadow-sm mt-4">
|
||||||
|
<div class="card-header d-flex align-items-center justify-content-between">
|
||||||
|
<h5 class="card-title mb-0">Historia wpłat</h5>
|
||||||
|
{% if zbiorka.wplaty|length > 0 %}
|
||||||
|
<small class="text-muted">Łącznie pozycji: {{ zbiorka.wplaty|length }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if zbiorka.wplaty and zbiorka.wplaty|length > 0 %}
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
{% for w in zbiorka.wplaty %}
|
||||||
|
<li class="list-group-item bg-transparent d-flex flex-wrap justify-content-between align-items-center">
|
||||||
|
<div class="me-3">
|
||||||
|
<strong>{{ w.data.strftime('%Y-%m-%d %H:%M:%S') }}</strong>
|
||||||
|
{% if w.opis %}
|
||||||
|
<span class="text-muted">— {{ w.opis }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<span class="badge bg-dark border ms-auto" style="border-color: var(--border);">
|
||||||
|
{{ w.kwota|round(2) }} PLN
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="text-center">Aktualnie brak wpłat..</p>
|
<div class="text-center py-4">
|
||||||
{% endif %}
|
<h6 class="mb-1">Brak wpłat</h6>
|
||||||
|
<p class="text-muted mb-0">Gdy pojawią się pierwsze wpłaty, zobaczysz je tutaj.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<!-- Akcje dolne -->
|
||||||
|
<div class="d-flex gap-2 justify-content-between mt-3">
|
||||||
|
<div></div>
|
||||||
|
<a href="{{ url_for('index') }}" class="btn btn-outline-light border">Powrót do listy</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_scripts %}
|
||||||
|
{{ super() }}
|
||||||
|
<script src="{{ url_for('static', filename='js/zbiorka.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='js/progress.js') }}"></script>
|
||||||
|
{% endblock %}
|
Reference in New Issue
Block a user