zmiany w logice i endpoitach

This commit is contained in:
Mateusz Gruszczyński
2025-08-28 12:42:57 +02:00
parent d42ce7fcc4
commit 62e40d5aee
8 changed files with 316 additions and 396 deletions

175
app.py
View File

@@ -13,7 +13,7 @@ from datetime import datetime
from markupsafe import Markup from markupsafe import Markup
from sqlalchemy import event from sqlalchemy import event
from sqlalchemy.engine import Engine from sqlalchemy.engine import Engine
from decimal import Decimal, InvalidOperation
import markdown as md import markdown as md
from flask import request, flash, abort from flask import request, flash, abort
import os import os
@@ -93,6 +93,7 @@ class GlobalSettings(db.Model):
footer_brand_mode = db.Column(db.String(10), default="text") footer_brand_mode = db.Column(db.String(10), default="text")
footer_text = db.Column(db.String(200), nullable=True) footer_text = db.Column(db.String(200), nullable=True)
@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))
@@ -283,65 +284,81 @@ def admin_dashboard():
@app.route("/admin/zbiorka/dodaj", methods=["GET", "POST"]) @app.route("/admin/zbiorka/dodaj", methods=["GET", "POST"])
@login_required
def dodaj_zbiorke():
if not current_user.is_admin:
flash("Brak uprawnień", "danger")
return redirect(url_for("index"))
global_settings = GlobalSettings.query.first() # Pobieramy globalne ustawienia
if request.method == "POST":
nazwa = request.form["nazwa"]
opis = request.form["opis"]
# Pozyskujemy numer konta i telefon z formularza (mogą być nadpisane ręcznie)
numer_konta = request.form["numer_konta"]
numer_telefonu_blik = request.form["numer_telefonu_blik"]
cel = float(request.form["cel"])
ukryj_kwote = "ukryj_kwote" in request.form
nowa_zbiorka = Zbiorka(
nazwa=nazwa,
opis=opis,
numer_konta=numer_konta,
numer_telefonu_blik=numer_telefonu_blik,
cel=cel,
ukryj_kwote=ukryj_kwote,
)
db.session.add(nowa_zbiorka)
db.session.commit()
flash("Zbiórka została dodana", "success")
return redirect(url_for("admin_dashboard"))
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 formularz_zbiorek(zbiorka_id=None):
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)
global_settings = GlobalSettings.query.first() # Pobieramy globalne ustawienia # Tryb
is_edit = zbiorka_id is not None
zb = Zbiorka.query.get_or_404(zbiorka_id) if is_edit else None
global_settings = GlobalSettings.query.first()
if request.method == "POST": if request.method == "POST":
zb.nazwa = request.form["nazwa"] # Pola wspólne
zb.opis = request.form["opis"] nazwa = request.form.get("nazwa", "").strip()
zb.numer_konta = request.form["numer_konta"] opis = request.form.get("opis", "").strip()
zb.numer_telefonu_blik = request.form["numer_telefonu_blik"]
# IBAN/telefon — oczyść z nadmiarowych znaków odstępu (zostaw spacje w prezentacji frontu)
numer_konta = request.form.get("numer_konta", "").strip()
numer_telefonu_blik = request.form.get("numer_telefonu_blik", "").strip()
# Cel — walidacja liczby
try: try:
zb.cel = float(request.form["cel"]) cel_str = request.form.get("cel", "").replace(",", ".").strip()
except ValueError: cel = Decimal(cel_str)
if cel <= Decimal("0"):
raise InvalidOperation
except (InvalidOperation, ValueError):
flash("Podano nieprawidłową wartość dla celu zbiórki", "danger") flash("Podano nieprawidłową wartość dla celu zbiórki", "danger")
return render_template( # render z dotychczasowo wpisanymi danymi (w trybie dodawania tworzymy tymczasowy obiekt)
"admin/edytuj_zbiorke.html", zbiorka=zb, global_settings=global_settings temp_zb = zb or Zbiorka(
nazwa=nazwa,
opis=opis,
numer_konta=numer_konta,
numer_telefonu_blik=numer_telefonu_blik,
cel=None,
ukryj_kwote=("ukryj_kwote" in request.form),
) )
zb.ukryj_kwote = "ukryj_kwote" in request.form return render_template(
db.session.commit() "admin/formularz_zbiorek.html",
flash("Zbiórka została zaktualizowana", "success") zbiorka=temp_zb if is_edit else None if zb is None else temp_zb,
global_settings=global_settings,
)
ukryj_kwote = "ukryj_kwote" in request.form
if is_edit:
# Aktualizacja istniejącej
zb.nazwa = nazwa
zb.opis = opis
zb.numer_konta = numer_konta
zb.numer_telefonu_blik = numer_telefonu_blik
zb.cel = float(cel) # jeśli masz Decimal w modelu, przypisz bez konwersji
zb.ukryj_kwote = ukryj_kwote
db.session.commit()
flash("Zbiórka została zaktualizowana", "success")
else:
# Utworzenie nowej
nowa = Zbiorka(
nazwa=nazwa,
opis=opis,
numer_konta=numer_konta,
numer_telefonu_blik=numer_telefonu_blik,
cel=float(cel),
ukryj_kwote=ukryj_kwote,
)
db.session.add(nowa)
db.session.commit()
flash("Zbiórka została dodana", "success")
return redirect(url_for("admin_dashboard")) return redirect(url_for("admin_dashboard"))
# GET
return render_template( return render_template(
"admin/edytuj_zbiorke.html", zbiorka=zb, global_settings=global_settings "admin/formularz_zbiorek.html", zbiorka=zb, global_settings=global_settings
) )
@@ -421,24 +438,33 @@ def create_admin_account():
db.session.commit() db.session.commit()
from flask import request
@app.after_request @app.after_request
def apply_headers(response): def apply_headers(response):
# Nagłówki niestandardowe # --- STATIC: jak wcześniej ---
custom_headers = app.config.get("ADD_HEADERS", {})
if isinstance(custom_headers, dict):
for header, value in custom_headers.items():
response.headers[header] = str(value)
if request.path.startswith("/static/"): if request.path.startswith("/static/"):
response.headers.pop("Content-Disposition", None) response.headers.pop("Content-Disposition", None)
response.headers["Vary"] = "Accept-Encoding" response.headers["Vary"] = "Accept-Encoding"
response.headers["Cache-Control"] = app.config.get("CACHE_CONTROL_HEADER_STATIC") response.headers["Cache-Control"] = app.config.get(
"CACHE_CONTROL_HEADER_STATIC"
)
if app.config.get("USE_ETAGS", True) and "ETag" not in response.headers: if app.config.get("USE_ETAGS", True) and "ETag" not in response.headers:
response.add_etag() response.add_etag()
response.make_conditional(request) response.make_conditional(request)
return response return response
# Wykluczenia path_norm = request.path.lstrip("/")
is_admin = path_norm.startswith("admin/") or path_norm == "admin"
if is_admin:
if (response.mimetype or "").startswith("text/html"):
response.headers["Cache-Control"] = "no-store, no-cache"
response.headers.pop("ETag", None)
return response
if response.status_code in (301, 302, 303, 307, 308): if response.status_code in (301, 302, 303, 307, 308):
response.headers.pop("Vary", None) response.headers.pop("Vary", None)
return response return response
@@ -452,16 +478,16 @@ def apply_headers(response):
response.headers["Content-Type"] = "text/html; charset=utf-8" response.headers["Content-Type"] = "text/html; charset=utf-8"
response.headers["Retry-After"] = "120" response.headers["Retry-After"] = "120"
response.headers.pop("Vary", None) response.headers.pop("Vary", None)
elif request.path.startswith("/admin"):
response.headers.pop("Vary", None)
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
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"
response.headers["Cache-Control"] = default_cache response.headers["Cache-Control"] = default_cache
# Blokowanie botów (ale NIE dla /static/) if (
if app.config.get("BLOCK_BOTS", False) and not request.path.startswith("/static/"): app.config.get("BLOCK_BOTS", False)
and not is_admin
and not request.path.startswith("/static/")
):
cc_override = app.config.get("CACHE_CONTROL_HEADER") cc_override = app.config.get("CACHE_CONTROL_HEADER")
if cc_override: if cc_override:
response.headers["Cache-Control"] = cc_override response.headers["Cache-Control"] = cc_override
@@ -490,7 +516,7 @@ def admin_ustawienia():
navbar_brand_mode = request.form.get("navbar_brand_mode", "text") navbar_brand_mode = request.form.get("navbar_brand_mode", "text")
footer_brand_mode = request.form.get("footer_brand_mode", "text") footer_brand_mode = request.form.get("footer_brand_mode", "text")
footer_text = request.form.get("footer_text") or None footer_text = request.form.get("footer_text") or None
show_logo_in_navbar = (navbar_brand_mode == "logo") show_logo_in_navbar = navbar_brand_mode == "logo"
if settings is None: if settings is None:
settings = GlobalSettings( settings = GlobalSettings(
@@ -525,16 +551,33 @@ def admin_ustawienia():
) )
@app.route("/admin/zbiorka/oznacz/<int:zbiorka_id>", methods=["POST"]) @app.route(
"/admin/zbiorka/oznacz/niezrealizowana/<int:zbiorka_id>",
methods=["POST"],
endpoint="oznacz_niezrealizowana",
)
@app.route(
"/admin/zbiorka/oznacz/zrealizowana/<int:zbiorka_id>",
methods=["POST"],
endpoint="oznacz_zrealizowana",
)
@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
if "niezrealizowana" in request.path:
zb.zrealizowana = False
msg = "Zbiórka została oznaczona jako niezrealizowana"
else:
zb.zrealizowana = True
msg = "Zbiórka została oznaczona jako zrealizowana"
db.session.commit() db.session.commit()
flash("Zbiórka została oznaczona jako zrealizowana", "success") flash(msg, "success")
return redirect(url_for("admin_dashboard")) return redirect(url_for("admin_dashboard"))

View File

@@ -292,4 +292,15 @@ select.form-select:focus {
border-color: var(--accent-600); border-color: var(--accent-600);
color: var(--text); color: var(--text);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 50%, transparent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 50%, transparent);
}
/* pole edycji ciemne */
.CodeMirror {
background-color: #1e1e1e !important;
color: #e0e0e0 !important;
}
/* kursor */
.CodeMirror-cursor {
border-left: 1px solid #e0e0e0 !important;
} }

View File

@@ -1,37 +0,0 @@
(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';
});
}
})();

View File

@@ -3,7 +3,7 @@
const opis = document.getElementById('opis'); const opis = document.getElementById('opis');
const opisCount = document.getElementById('opisCount'); const opisCount = document.getElementById('opisCount');
if (opis && opisCount) { if (opis && opisCount) {
const updateCount = () => opisCount.textContent = opis.value.length.toString(); const updateCount = () => (opisCount.textContent = String(opis.value.length));
opis.addEventListener('input', updateCount); opis.addEventListener('input', updateCount);
updateCount(); updateCount();
} }
@@ -12,7 +12,7 @@
const iban = document.getElementById('numer_konta'); const iban = document.getElementById('numer_konta');
if (iban) { if (iban) {
iban.addEventListener('input', () => { iban.addEventListener('input', () => {
const digits = iban.value.replace(/\D/g, '').slice(0, 26); // 26 cyfr po "PL" const digits = iban.value.replace(/\D/g, '').slice(0, 26); // 26 cyfr po PL
const chunked = digits.replace(/(.{4})/g, '$1 ').trim(); const chunked = digits.replace(/(.{4})/g, '$1 ').trim();
iban.value = chunked; iban.value = chunked;
}); });
@@ -31,22 +31,25 @@
}); });
} }
// „Ustaw globalne” z data-atrybutów (bez wstrzykiwania wartości w JS) // „Ustaw globalne” jest tylko w trybie edycji; odpalamy warunkowo
const setGlobalBtn = document.getElementById('ustaw-globalne'); const setGlobalBtn = document.getElementById('ustaw-globalne');
if (setGlobalBtn && iban && tel) { if (setGlobalBtn && iban && tel) {
setGlobalBtn.addEventListener('click', () => { setGlobalBtn.addEventListener('click', () => {
const gIban = setGlobalBtn.dataset.iban || ''; const gIban = setGlobalBtn.dataset.iban || '';
const gBlik = setGlobalBtn.dataset.blik || ''; const gBlik = setGlobalBtn.dataset.blik || '';
if (gIban) { if (gIban) {
iban.value = gIban.replace(/\D/g, '').replace(/(.{4})/g, '$1 ').trim(); iban.value = gIban.replace(/\D/g, '').replace(/(.{4})/g, '$1 ').trim();
iban.dispatchEvent(new Event('input'));
} }
if (gBlik) { if (gBlik) {
const d = gBlik.replace(/\D/g, '').slice(0, 9); 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(' '); const p = [d.slice(0, 3), d.slice(3, 6), d.slice(6, 9)]
.filter(Boolean)
.join(' ');
tel.value = p; tel.value = p;
tel.dispatchEvent(new Event('input'));
} }
iban.dispatchEvent(new Event('input'));
tel.dispatchEvent(new Event('input'));
}); });
} }
@@ -57,4 +60,4 @@
if (cel.value && Number(cel.value) < 0.01) cel.value = '0.01'; if (cel.value && Number(cel.value) < 0.01) cel.value = '0.01';
}); });
} }
})(); })();

View File

@@ -8,7 +8,7 @@
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-4"> <div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-4">
<h2 class="mb-0">Panel Admina</h2> <h2 class="mb-0">Panel Admina</h2>
<div class="d-flex flex-wrap gap-2"> <div class="d-flex flex-wrap gap-2">
<a href="{{ url_for('dodaj_zbiorke') }}" class="btn btn-primary"> <a href="{{ url_for('formularz_zbiorek') }}" class="btn btn-primary">
Dodaj zbiórkę Dodaj zbiórkę
</a> </a>
<a href="{{ url_for('admin_ustawienia') }}" class="btn btn-outline-light border"> <a href="{{ url_for('admin_ustawienia') }}" class="btn btn-outline-light border">
@@ -78,7 +78,7 @@
<td class="text-end"> <td class="text-end">
<!-- Grupa akcji: główne + rozwijane --> <!-- Grupa akcji: główne + rozwijane -->
<div class="btn-group"> <div class="btn-group">
<a href="{{ url_for('edytuj_zbiorka', zbiorka_id=z.id) }}" <a href="{{ url_for('formularz_zbiorek', zbiorka_id=z.id) }}"
class="btn btn-sm btn-outline-light">Edytuj</a> class="btn btn-sm btn-outline-light">Edytuj</a>
<button class="btn btn-sm btn-outline-light dropdown-toggle dropdown-toggle-split" <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"> data-bs-toggle="dropdown" aria-expanded="false" aria-label="Więcej opcji">
@@ -98,10 +98,10 @@
<hr class="dropdown-divider"> <hr class="dropdown-divider">
</li> </li>
<li> <li>
<form action="{{ url_for('oznacz_zbiorka', zbiorka_id=z.id) }}" <form action="{{ url_for('oznacz_zrealizowana', zbiorka_id=z.id) }}"
method="post" class="m-0"> method="post" class="m-0">
<button type="submit" class="dropdown-item">Oznacz jako <button type="submit" class="dropdown-item">Oznacz jako
zrealizowana</button> zrealizowaną</button>
</form> </form>
</li> </li>
<li> <li>
@@ -136,7 +136,7 @@
<div class="card-body text-center py-5"> <div class="card-body text-center py-5">
<h5 class="mb-2">Brak aktywnych zbiórek</h5> <h5 class="mb-2">Brak aktywnych zbiórek</h5>
<p class="text-muted mb-4">Wygląda na to, że teraz nic nie zbieramy.</p> <p class="text-muted mb-4">Wygląda na to, że teraz nic nie zbieramy.</p>
<a href="{{ url_for('dodaj_zbiorka') }}" class="btn btn-primary">Utwórz nową zbiórkę</a> <a href="{{ url_for('formularz_zbiorek') }}" class="btn btn-primary">Utwórz nową zbiórkę</a>
</div> </div>
</div> </div>
{% endif %} {% endif %}
@@ -186,7 +186,7 @@
</td> </td>
<td class="text-end"> <td class="text-end">
<div class="btn-group"> <div class="btn-group">
<a href="{{ url_for('edytuj_zbiorka', zbiorka_id=z.id) }}" <a href="{{ url_for('formularz_zbiorek', zbiorka_id=z.id) }}"
class="btn btn-sm btn-outline-light">Edytuj</a> class="btn btn-sm btn-outline-light">Edytuj</a>
<button class="btn btn-sm btn-outline-light dropdown-toggle dropdown-toggle-split" <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"> data-bs-toggle="dropdown" aria-expanded="false" aria-label="Więcej opcji">
@@ -205,6 +205,13 @@
<li> <li>
<hr class="dropdown-divider"> <hr class="dropdown-divider">
</li> </li>
<li>
<form action="{{ url_for('oznacz_niezrealizowana', zbiorka_id=z.id) }}"
method="post" class="m-0">
<button type="submit" class="dropdown-item">Oznacz jako
niezrealizowaną</button>
</form>
</li>
<li> <li>
<form action="{{ url_for('zmien_widzialnosc', zbiorka_id=z.id) }}" <form action="{{ url_for('zmien_widzialnosc', zbiorka_id=z.id) }}"
method="post" class="m-0"> method="post" class="m-0">
@@ -236,7 +243,7 @@
<div class="card-body text-center py-5"> <div class="card-body text-center py-5">
<h5 class="mb-2">Brak zbiórek zrealizowanych</h5> <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> <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ą <a href="{{ url_for('formularz_zbiorek') }}" class="btn btn-outline-light border">Utwórz nową
zbiórkę</a> zbiórkę</a>
</div> </div>
</div> </div>

View File

@@ -1,125 +0,0 @@
{% 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 %}

View File

@@ -1,154 +0,0 @@
{% 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 %}

View File

@@ -0,0 +1,172 @@
{# templates/zbiorka_form.html #}
{% extends 'base.html' %}
{% set is_edit = zbiorka is not none %}
{% block title %}{{ 'Edytuj zbiórkę' if is_edit else '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">
<!-- Nawigacja / powrót -->
<div class="d-flex align-items-center gap-2 mb-3">
{% if is_edit %}
<a href="{{ url_for('zbiorka', zbiorka_id=zbiorka.id) }}" class="btn btn-sm btn-outline-light border">
Szczegóły zbiórki</a>
{% else %}
<a href="{{ url_for('admin_dashboard') }}" class="btn btn-sm btn-outline-light border">← Panel Admina</a>
{% endif %}
</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">
{{ 'Edytuj zbiórkę' if is_edit else 'Dodaj nową zbiórkę' }}
</h3>
{% if is_edit %}
<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>
{% else %}
<small class="opacity-75">Uzupełnij podstawowe dane i dane płatności</small>
{% endif %}
</div>
<div class="card-body">
<form method="post" novalidate id="{{ 'form-edit-zbiorka' if is_edit else 'form-add-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" maxlength="120"
placeholder="{{ 'Krótki, zrozumiały tytuł' if is_edit else 'Np. Wsparcie dla schroniska Azor' }}"
value="{{ zbiorka.nazwa if is_edit else '' }}" 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">{{ zbiorka.opis if is_edit else '' }}</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: 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"
inputmode="numeric" autocomplete="off"
placeholder="12 3456 7890 1234 5678 9012 3456" required aria-describedby="ibanHelp"
value="{% if is_edit and zbiorka.numer_konta %}{{ zbiorka.numer_konta }}{% elif global_settings %}{{ global_settings.numer_konta }}{% else %}{% endif %}">
</div>
<div id="ibanHelp" class="form-text">
Wpisz ciąg cyfr; spacje dodadzą się automatycznie dla czytelności.
</div>
</div>
<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" inputmode="tel" pattern="[0-9 ]{9,13}"
placeholder="123 456 789" required aria-describedby="blikHelp"
value="{% if is_edit and zbiorka.numer_telefonu_blik %}{{ zbiorka.numer_telefonu_blik }}{% elif global_settings %}{{ global_settings.numer_telefonu_blik }}{% else %}{% endif %}">
</div>
<div id="blikHelp" class="form-text">Dziewięć cyfr telefonu powiązanego z BLIK. Spacje
opcjonalne.</div>
</div>
{% if is_edit %}
<div class="col-12 col-md-12 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 %}>Wstaw
globalne ustawienia</button>
</div>
{% endif %}
</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"
value="{{ zbiorka.cel if is_edit else '' }}">
</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-12 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 is_edit and zbiorka.ukryj_kwote %}checked{% endif %}>
<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">
{{ ' Zaktualizuj zbiórkę' if is_edit else 'Dodaj zbiórkę' }} </button>
<a href="{{ url_for('admin_dashboard') }}" class="btn btn-outline-light border">Anuluj</a>
</div>
</form>
</div>
</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/formularz_zbiorek.js') }}"></script>
{% endblock %}