diff --git a/alters.txt b/alters.txt index e69de29..797bb4f 100644 --- a/alters.txt +++ b/alters.txt @@ -0,0 +1,2 @@ +ALTER TABLE zbiorka ALTER COLUMN numer_konta DROP NOT NULL; +ALTER TABLE zbiorka ALTER COLUMN numer_telefonu_blik DROP NOT NULL; \ No newline at end of file diff --git a/app.py b/app.py index 0ff37dc..f3a1e6c 100644 --- a/app.py +++ b/app.py @@ -82,8 +82,8 @@ class Zbiorka(db.Model): id = db.Column(db.Integer, primary_key=True) nazwa = db.Column(db.String(100), nullable=False) opis = db.Column(db.Text, nullable=False) - numer_konta = db.Column(db.String(50), nullable=False) - numer_telefonu_blik = db.Column(db.String(50), nullable=False) + numer_konta = db.Column(db.String(50), nullable=True) + numer_telefonu_blik = db.Column(db.String(50), nullable=True) cel = db.Column(Numeric(12, 2), nullable=False, default=0) stan = db.Column(Numeric(12, 2), default=0) ukryta = db.Column(db.Boolean, default=False) @@ -92,6 +92,8 @@ class Zbiorka(db.Model): pokaz_postep_finanse = db.Column(db.Boolean, default=True, nullable=False) pokaz_postep_pozycje = db.Column(db.Boolean, default=True, nullable=False) pokaz_postep_kwotowo = db.Column(db.Boolean, default=True, nullable=False) + uzyj_konta = db.Column(db.Boolean, default=True, nullable=False) + uzyj_blik = db.Column(db.Boolean, default=True, nullable=False) wplaty = db.relationship( "Wplata", @@ -386,88 +388,69 @@ def formularz_zbiorek(zbiorka_id=None): return redirect(url_for("index")) is_edit = zbiorka_id is not None - zb = None - if is_edit: - zb = db.session.get(Zbiorka, zbiorka_id) - if zb is None: - abort(404) + zb = db.session.get(Zbiorka, zbiorka_id) if is_edit else None + if is_edit and zb is None: + abort(404) global_settings = UstawieniaGlobalne.query.first() + def _temp_obj(): + t = zb or Zbiorka() + t.nazwa = (request.form.get("nazwa", "") or "").strip() + t.opis = (request.form.get("opis", "") or "").strip() + t.numer_konta = (request.form.get("numer_konta", "") or "").strip() + t.numer_telefonu_blik = (request.form.get("numer_telefonu_blik", "") or "").strip() + t.ukryj_kwote = "ukryj_kwote" in request.form + t.pokaz_postep_finanse = "pokaz_postep_finanse" in request.form + t.pokaz_postep_pozycje = "pokaz_postep_pozycje" in request.form + t.pokaz_postep_kwotowo = "pokaz_postep_kwotowo" in request.form + t.uzyj_konta = "uzyj_konta" in request.form + t.uzyj_blik = "uzyj_blik" in request.form + return t + if request.method == "POST": - # Pola wspólne + # Pola nazwa = (request.form.get("nazwa", "") or "").strip() opis = (request.form.get("opis", "") or "").strip() numer_konta = (request.form.get("numer_konta", "") or "").strip() numer_telefonu_blik = (request.form.get("numer_telefonu_blik", "") or "").strip() - # Widoczność kwot i poszczególnych pasków postępu + # Przełączniki płatności + uzyj_konta = "uzyj_konta" in request.form + uzyj_blik = "uzyj_blik" in request.form + + # Widoczność/metryki ukryj_kwote = "ukryj_kwote" in request.form pokaz_postep_finanse = "pokaz_postep_finanse" in request.form pokaz_postep_pozycje = "pokaz_postep_pozycje" in request.form pokaz_postep_kwotowo = "pokaz_postep_kwotowo" in request.form - # Walidacja wymaganych pól tekstowych (zanim uderzymy w bazę) + # Walidacje if not nazwa: flash("Nazwa jest wymagana", "danger") - temp_zb = zb or Zbiorka() - temp_zb.nazwa = nazwa - temp_zb.opis = opis - temp_zb.numer_konta = numer_konta - temp_zb.numer_telefonu_blik = numer_telefonu_blik - temp_zb.ukryj_kwote = ukryj_kwote - temp_zb.pokaz_postep_finanse = pokaz_postep_finanse - temp_zb.pokaz_postep_pozycje = pokaz_postep_pozycje - temp_zb.pokaz_postep_kwotowo = pokaz_postep_kwotowo - return render_template("admin/formularz_zbiorek.html", zbiorka=temp_zb, global_settings=global_settings) + return render_template("admin/formularz_zbiorek.html", zbiorka=_temp_obj(), global_settings=global_settings) if not opis: flash("Opis jest wymagany", "danger") - temp_zb = zb or Zbiorka() - temp_zb.nazwa = nazwa - temp_zb.opis = opis - temp_zb.numer_konta = numer_konta - temp_zb.numer_telefonu_blik = numer_telefonu_blik - temp_zb.ukryj_kwote = ukryj_kwote - temp_zb.pokaz_postep_finanse = pokaz_postep_finanse - temp_zb.pokaz_postep_pozycje = pokaz_postep_pozycje - temp_zb.pokaz_postep_kwotowo = pokaz_postep_kwotowo - return render_template("admin/formularz_zbiorek.html", zbiorka=temp_zb, global_settings=global_settings) + return render_template("admin/formularz_zbiorek.html", zbiorka=_temp_obj(), global_settings=global_settings) - if not numer_konta: - flash("Numer konta jest wymagany", "danger") - temp_zb = zb or Zbiorka() - temp_zb.nazwa = nazwa - temp_zb.opis = opis - temp_zb.numer_konta = numer_konta - temp_zb.numer_telefonu_blik = numer_telefonu_blik - temp_zb.ukryj_kwote = ukryj_kwote - temp_zb.pokaz_postep_finanse = pokaz_postep_finanse - temp_zb.pokaz_postep_pozycje = pokaz_postep_pozycje - temp_zb.pokaz_postep_kwotowo = pokaz_postep_kwotowo - return render_template("admin/formularz_zbiorek.html", zbiorka=temp_zb, global_settings=global_settings) + # Co najmniej jeden kanał + if not (uzyj_konta or uzyj_blik): + flash("Włącz co najmniej jeden kanał wpłat (konto lub BLIK).", "danger") + return render_template("admin/formularz_zbiorek.html", zbiorka=_temp_obj(), global_settings=global_settings) - if not numer_telefonu_blik: - flash("Numer telefonu BLIK jest wymagany", "danger") - temp_zb = zb or Zbiorka() - temp_zb.nazwa = nazwa - temp_zb.opis = opis - temp_zb.numer_konta = numer_konta - temp_zb.numer_telefonu_blik = numer_telefonu_blik - temp_zb.ukryj_kwote = ukryj_kwote - temp_zb.pokaz_postep_finanse = pokaz_postep_finanse - temp_zb.pokaz_postep_pozycje = pokaz_postep_pozycje - temp_zb.pokaz_postep_kwotowo = pokaz_postep_kwotowo - return render_template("admin/formularz_zbiorek.html", zbiorka=temp_zb, global_settings=global_settings) + # Warunkowe wartości + if uzyj_konta and not numer_konta: + flash("Numer konta jest wymagany (kanał przelewu włączony).", "danger") + return render_template("admin/formularz_zbiorek.html", zbiorka=_temp_obj(), global_settings=global_settings) - # Cel — Decimal, > 0; akceptuj przecinek i usuń spacje/nbsp + if uzyj_blik and not numer_telefonu_blik: + flash("Numer telefonu BLIK jest wymagany (kanał BLIK włączony).", "danger") + return render_template("admin/formularz_zbiorek.html", zbiorka=_temp_obj(), global_settings=global_settings) + + # Cel > 0 cel_raw = (request.form.get("cel", "") or "") - cel_norm = ( - cel_raw.replace(" ", "") - .replace("\u00A0", "") - .replace(",", ".") - .strip() - ) + cel_norm = cel_raw.replace(" ", "").replace("\u00A0", "").replace(",", ".").strip() try: if not cel_norm: raise InvalidOperation @@ -476,51 +459,41 @@ def formularz_zbiorek(zbiorka_id=None): raise InvalidOperation except (InvalidOperation, ValueError): flash("Podano nieprawidłową wartość dla celu zbiórki", "danger") - # Odtwórz stan formularza, ale nie używaj zbiorka.id w szablonie - temp_zb = zb or Zbiorka() - temp_zb.nazwa = nazwa - temp_zb.opis = opis - temp_zb.numer_konta = numer_konta - temp_zb.numer_telefonu_blik = numer_telefonu_blik - # cel pozostaw niewypełniony przy błędzie, aby input pokazał to co wpisał użytkownik - temp_zb.ukryj_kwote = ukryj_kwote - temp_zb.pokaz_postep_finanse = pokaz_postep_finanse - temp_zb.pokaz_postep_pozycje = pokaz_postep_pozycje - temp_zb.pokaz_postep_kwotowo = pokaz_postep_kwotowo - return render_template("admin/formularz_zbiorek.html", zbiorka=temp_zb, global_settings=global_settings) + return render_template("admin/formularz_zbiorek.html", zbiorka=_temp_obj(), global_settings=global_settings) - # Lista produktów + # Produkty names = request.form.getlist("item_nazwa[]") links = request.form.getlist("item_link[]") prices = request.form.getlist("item_cena[]") - def _read_price(val): - """Zwraca Decimal(>=0) albo None; akceptuje przecinek jako separator dziesiętny.""" + def _read_price(val: str): if not val or not val.strip(): return None try: d = Decimal(val.replace(",", ".")) - if d < 0: - return None - return d + return d if d >= 0 else None except Exception: return None - # --- ZAPIS ZBIÓRKI + PRODUKTÓW --- + # Zapis if is_edit: - # Aktualizacja istniejącej zbiórki zb.nazwa = nazwa zb.opis = opis - zb.numer_konta = numer_konta - zb.numer_telefonu_blik = numer_telefonu_blik + + # NOT NULL-safe: puste stringi gdy wyłączone + zb.uzyj_konta = uzyj_konta + zb.uzyj_blik = uzyj_blik + zb.numer_konta = numer_konta if uzyj_konta else "" + zb.numer_telefonu_blik = numer_telefonu_blik if uzyj_blik else "" + zb.cel = cel zb.ukryj_kwote = ukryj_kwote zb.pokaz_postep_finanse = pokaz_postep_finanse zb.pokaz_postep_pozycje = pokaz_postep_pozycje zb.pokaz_postep_kwotowo = pokaz_postep_kwotowo - db.session.commit() # zapisz bazowe pola + db.session.commit() - # Nadpisz listę produktów + # Nadpisz pozycje zb.przedmioty.clear() for i, raw_name in enumerate(names): name = (raw_name or "").strip() @@ -529,7 +502,6 @@ def formularz_zbiorek(zbiorka_id=None): link = (links[i] if i < len(links) else "").strip() or None cena_val = _read_price(prices[i] if i < len(prices) else "") kupione_val = request.form.get(f"item_kupione_val_{i}") == "1" - db.session.add(Przedmiot( zbiorka_id=zb.id, nazwa=name, @@ -537,17 +509,17 @@ def formularz_zbiorek(zbiorka_id=None): cena=cena_val, kupione=kupione_val )) - db.session.commit() flash("Zbiórka została zaktualizowana", "success") else: - # Utworzenie nowej zbiórki nowa = Zbiorka( nazwa=nazwa, opis=opis, - numer_konta=numer_konta, - numer_telefonu_blik=numer_telefonu_blik, + uzyj_konta=uzyj_konta, + uzyj_blik=uzyj_blik, + numer_konta=(numer_konta if uzyj_konta else ""), + numer_telefonu_blik=(numer_telefonu_blik if uzyj_blik else ""), cel=cel, ukryj_kwote=ukryj_kwote, pokaz_postep_finanse=pokaz_postep_finanse, @@ -557,7 +529,6 @@ def formularz_zbiorek(zbiorka_id=None): db.session.add(nowa) db.session.commit() # potrzebne ID - # Dodaj produkty do nowej zbiórki for i, raw_name in enumerate(names): name = (raw_name or "").strip() if not name: @@ -565,7 +536,6 @@ def formularz_zbiorek(zbiorka_id=None): link = (links[i] if i < len(links) else "").strip() or None cena_val = _read_price(prices[i] if i < len(prices) else "") kupione_val = request.form.get(f"item_kupione_val_{i}") == "1" - db.session.add(Przedmiot( zbiorka_id=nowa.id, nazwa=name, @@ -573,7 +543,6 @@ def formularz_zbiorek(zbiorka_id=None): cena=cena_val, kupione=kupione_val )) - db.session.commit() flash("Zbiórka została dodana", "success") diff --git a/static/css/custom.css b/static/css/custom.css index 4d68e19..25e9b83 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -391,34 +391,50 @@ select.form-select:focus { /* Tylko ten przycisk */ .btn.btn-outline-light.btn-opis { - color: #fff; - background-color: transparent; - border: 1px solid var(--border); - transition: none; + color: #fff; + background-color: transparent; + border: 1px solid var(--border); + transition: none; } .btn.btn-outline-light.btn-opis:hover, .btn.btn-outline-light.btn-opis:focus { - color: #fff; - background-color: #161616; - border-color: color-mix(in srgb, var(--accent) 20%, var(--border)); + color: #fff; + background-color: #161616; + border-color: color-mix(in srgb, var(--accent) 20%, var(--border)); } .btn.btn-outline-light { - color: #fff; - background-color: transparent; - border: 1px solid rgba(255,255,255,0.9); + color: #fff; + background-color: transparent; + border: 1px solid rgba(255, 255, 255, 0.9); } .btn.btn-outline-light:hover, .btn.btn-outline-light:focus { - color: #fff; - background-color: #161616; - border-color: color-mix(in srgb, var(--accent) 20%, #ffffff); + color: #fff; + background-color: #161616; + border-color: color-mix(in srgb, var(--accent) 20%, #ffffff); } .btn.btn-outline-light:active { - color: #fff; - background-color: #141414; - border-color: color-mix(in srgb, var(--accent) 24%, #ffffff); + color: #fff; + background-color: #141414; + border-color: color-mix(in srgb, var(--accent) 24%, #ffffff); } + +#kanalyWarning, +#postepyWarning { + border: 1px solid #ffc107; + background-color: #2c2c2c; + color: #fff; +} + +input:disabled, +textarea:disabled, +select:disabled { + background-color: #2b2b2b !important; + color: #bbb !important; + opacity: 1 !important; + cursor: not-allowed; +} \ No newline at end of file diff --git a/static/js/sposoby_wplat.js b/static/js/sposoby_wplat.js new file mode 100644 index 0000000..09d538d --- /dev/null +++ b/static/js/sposoby_wplat.js @@ -0,0 +1,88 @@ +(function () { + const form = document.getElementById('form-edit-zbiorka') || document.getElementById('form-add-zbiorka') || document.querySelector('form'); + + const map = [ + ['uzyj_konta', 'numer_konta'], + ['uzyj_blik', 'numer_telefonu_blik'] + ]; + + const warnBox = document.getElementById('kanalyWarning'); + + function showWarn(show) { + if (!warnBox) return; + warnBox.classList.toggle('d-none', !show); + } + + function getEl(id) { return document.getElementById(id); } + + function toggleField(chkId, inputId) { + const chk = getEl(chkId); + const inp = getEl(inputId); + if (!inp || !chk) return; + const on = chk.checked; + inp.disabled = !on; + if (on) inp.setAttribute('required', ''); + else inp.removeAttribute('required'); + } + + function atLeastOneOn() { + return map.some(([c]) => getEl(c)?.checked); + } + + function blinkInvalid(el) { + if (!el) return; + el.classList.add('is-invalid'); + setTimeout(() => el.classList.remove('is-invalid'), 400); + } + + function preventUncheckLast(e) { + const target = e.target; + if (target.checked) return; + const after = map.map(([c]) => c === target.id ? false : !!getEl(c)?.checked); + if (!after.some(Boolean)) { + e.preventDefault(); + target.checked = true; + showWarn(true); + blinkInvalid(target); + } else { + showWarn(false); + } + } + + function onToggle(chkId, inputId) { + toggleField(chkId, inputId); + showWarn(!atLeastOneOn()); + } + + map.forEach(([chkId, inputId]) => { + const chk = getEl(chkId); + if (!chk) return; + chk.addEventListener('click', preventUncheckLast); + chk.addEventListener('change', () => onToggle(chkId, inputId)); + toggleField(chkId, inputId); + }); + showWarn(!atLeastOneOn()); + + if (form) { + form.addEventListener('submit', function (e) { + if (!atLeastOneOn()) { + e.preventDefault(); + showWarn(true); + blinkInvalid(getEl('uzyj_konta') || getEl('uzyj_blik')); + (getEl('uzyj_konta') || getEl('uzyj_blik'))?.focus(); + return; + } + + for (const [chkId, inputId] of map) { + const chk = getEl(chkId), inp = getEl(inputId); + if (chk?.checked && inp && !inp.value.trim()) { + e.preventDefault(); + showWarn(true); + blinkInvalid(inp); + inp.focus(); + return; + } + } + }); + } +})(); diff --git a/templates/admin/formularz_zbiorek.html b/templates/admin/formularz_zbiorek.html index 2a9e947..cd9830d 100644 --- a/templates/admin/formularz_zbiorek.html +++ b/templates/admin/formularz_zbiorek.html @@ -17,14 +17,16 @@