opcje wydatkow w zbiorce

This commit is contained in:
Mateusz Gruszczyński
2025-09-20 16:19:30 +02:00
parent b9e85ab5d4
commit 0b221696d4
5 changed files with 158 additions and 16 deletions

60
app.py
View File

@@ -65,6 +65,15 @@ class Zbiorka(db.Model):
passive_deletes=True,
)
wydatki = db.relationship(
"Wydatek",
backref="zbiorka",
lazy=True,
order_by="Wydatek.data.desc()",
cascade="all, delete-orphan",
passive_deletes=True,
)
class Wplata(db.Model):
id = db.Column(db.Integer, primary_key=True)
@@ -80,6 +89,18 @@ class Wplata(db.Model):
zbiorka = db.relationship("Zbiorka", back_populates="wplaty")
class Wydatek(db.Model):
id = db.Column(db.Integer, primary_key=True)
zbiorka_id = db.Column(
db.Integer,
db.ForeignKey("zbiorka.id", ondelete="CASCADE"),
nullable=False,
)
kwota = db.Column(db.Float, nullable=False)
data = db.Column(db.DateTime, default=datetime.utcnow)
opis = db.Column(db.Text, nullable=True)
class GlobalSettings(db.Model):
id = db.Column(db.Integer, primary_key=True)
numer_konta = db.Column(db.String(50), nullable=False)
@@ -193,10 +214,20 @@ def page_not_found(e):
@app.route("/zbiorka/<int:zbiorka_id>")
def zbiorka(zbiorka_id):
zb = Zbiorka.query.get_or_404(zbiorka_id)
# Jeżeli zbiórka jest ukryta i użytkownik nie jest administratorem, zwróć 404
if zb.ukryta and (not current_user.is_authenticated or not current_user.is_admin):
abort(404)
return render_template("zbiorka.html", zbiorka=zb)
# scalona oś czasu: wpłaty + wydatki
aktywnosci = [
{"typ": "wpłata", "kwota": w.kwota, "opis": w.opis, "data": w.data}
for w in zb.wplaty
] + [
{"typ": "wydatek", "kwota": x.kwota, "opis": x.opis, "data": x.data}
for x in zb.wydatki
]
aktywnosci.sort(key=lambda a: a["data"], reverse=True)
return render_template("zbiorka.html", zbiorka=zb, aktywnosci=aktywnosci)
# TRASY LOGOWANIA I REJESTRACJI
@@ -551,6 +582,31 @@ def admin_ustawienia():
)
@app.route("/admin/zbiorka/<int:zbiorka_id>/wydatek/dodaj", methods=["GET", "POST"])
@login_required
def dodaj_wydatek(zbiorka_id):
if not current_user.is_admin:
flash("Brak uprawnień", "danger")
return redirect(url_for("index"))
zb = Zbiorka.query.get_or_404(zbiorka_id)
if request.method == "POST":
try:
kwota = float(request.form["kwota"])
if kwota <= 0:
raise ValueError
except (KeyError, ValueError):
flash("Nieprawidłowa kwota", "danger")
return redirect(url_for("dodaj_wydatek", zbiorka_id=zbiorka_id))
opis = request.form.get("opis", "")
nowy_wydatek = Wydatek(zbiorka_id=zb.id, kwota=kwota, opis=opis)
zb.stan -= kwota
db.session.add(nowy_wydatek)
db.session.commit()
flash("Wydatek został dodany", "success")
return redirect(url_for("admin_dashboard"))
return render_template("admin/dodaj_wydatek.html", zbiorka=zb)
@app.route(
"/admin/zbiorka/oznacz/niezrealizowana/<int:zbiorka_id>",
methods=["POST"],

View File

@@ -0,0 +1,5 @@
document.addEventListener('input', (e) => {
if (e.target && e.target.id === 'opis') {
document.getElementById('opisCount').textContent = e.target.value.length;
}
});

View File

@@ -90,6 +90,11 @@
href="{{ url_for('dodaj_wplate', zbiorka_id=z.id) }}">Dodaj
wpłatę</a>
</li>
<li>
<a class="dropdown-item"
href="{{ url_for('dodaj_wydatek', zbiorka_id=z.id) }}">Dodaj
wydatek</a>
</li>
<li>
<a class="dropdown-item"
href="{{ url_for('edytuj_stan', zbiorka_id=z.id) }}">Edytuj stan</a>
@@ -198,6 +203,11 @@
href="{{ url_for('dodaj_wplate', zbiorka_id=z.id) }}">Dodaj
wpłatę</a>
</li>
<li>
<a class="dropdown-item"
href="{{ url_for('dodaj_wydatek', zbiorka_id=z.id) }}">Dodaj
wydatek</a>
</li>
<li>
<a class="dropdown-item"
href="{{ url_for('edytuj_stan', zbiorka_id=z.id) }}">Edytuj stan</a>

View File

@@ -0,0 +1,63 @@
{% extends 'base.html' %}
{% block title %}Dodaj wydatek{% 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 wydatek: <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>
<div class="card-body">
<form method="post" novalidate>
<div class="mb-3">
<label for="kwota" class="form-label">Kwota wydatku</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>
<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 wydatku (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-danger">Dodaj wydatek</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_wydatek.js') }}"></script>
{% endblock %}

View File

@@ -97,8 +97,11 @@
{% 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>
<a href="{{ url_for('dodaj_wplate', zbiorka_id=zbiorka.id) }}" class="btn btn-primary">Dodaj wpłatę</a>
</div>
<div class="d-grid mt-2">
<a href="{{ url_for('dodaj_wydatek', zbiorka_id=zbiorka.id) }}" class="btn btn-outline-light border">Dodaj
wydatek</a>
</div>
{% endif %}
</div>
@@ -106,35 +109,40 @@
</div>
</div>
<!-- Historia wpłat -->
<!-- Aktywność (wpłaty + wydatki) -->
<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>
<h5 class="card-title mb-0">Aktywność</h5>
{% if aktywnosci and aktywnosci|length > 0 %}
<small class="text-muted">Łącznie pozycji: {{ aktywnosci|length }}</small>
{% endif %}
</div>
<div class="card-body">
{% if zbiorka.wplaty and zbiorka.wplaty|length > 0 %}
{% if aktywnosci and aktywnosci|length > 0 %}
<ul class="list-group list-group-flush">
{% for w in zbiorka.wplaty %}
{% for a in aktywnosci %}
<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>
<strong>{{ a.data.strftime('%Y-%m-%d %H:%M:%S') }}</strong>
<span class="badge {% if a.typ == 'wpłata' %}bg-success{% else %}bg-danger{% endif %} ms-2">
{{ a.typ|capitalize }}
</span>
{% if a.opis %}
<span class="text-muted">— {{ a.opis }}</span>
{% endif %}
</div>
{% if not zbiorka.ukryj_kwote %}
<span class="badge bg-dark border ms-auto" style="border-color: var(--border);">
{{ w.kwota|round(2) }} PLN
{% if a.typ == 'wpłata' %}+{% else %}-{% endif %} {{ a.kwota|round(2) }} PLN
</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% else %}
<div class="text-center py-4">
<h6 class="mb-1">Brak wpłat</h6>
<p class="text-muted mb-0">Gdy pojawią się pierwsze wpłaty, zobaczysz je tutaj.</p>
<h6 class="mb-1">Brak aktywności</h6>
<p class="text-muted mb-0">Gdy pojawią się pierwsze wpłaty lub wydatki, zobaczysz je tutaj.</p>
</div>
{% endif %}
</div>