Add staff dashboard, payment flow, seed extra 2026 expenses
Self-service /suff/pay/ page lets users record their own payments (cash/PayPal/bank/other) against their tab. Open balance is shown on /suff/me/ alongside total and paid amount, with a Bezahlen button when something is owed. Staff-only /suff/dashboard/ replaces the drink_stats / total_balance / user_stats CLI commands with a mobile-friendly festival view: overall refinancing progress bar (Spenden vs. Ausgaben with Bilanz), drinks refinancing bar (sales revenue vs. purchase cost with profit), per-drink sold/total/balance, open balances per user, and fun facts (top spender, top drink, busiest day, and top user per festival day). Linked from /suff/me/ when the logged-in user is staff. seed_drinks_2026 also creates the non-drink Payments we already know about (toilets, drinks/equipment down payment, band fees per stage day), idempotently keyed on (purpose, date). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
|||||||
|
from datetime import date
|
||||||
|
|
||||||
|
from django.core.management import BaseCommand
|
||||||
|
from django.db import transaction
|
||||||
|
|
||||||
|
from gaehsnitz.models import Drink, Payment
|
||||||
|
|
||||||
|
PAYMENTS = [
|
||||||
|
# purpose, date, amount
|
||||||
|
("Toiletten", date(2026, 5, 6), 210.01),
|
||||||
|
("Anzahlung Getränke+Kühlschrank+Bänke", date(2026, 5, 18), 400.00),
|
||||||
|
("Band: Six Good Years", date(2026, 6, 12), 150.00),
|
||||||
|
("Band: Melo-Komplott", date(2026, 6, 12), 100.00),
|
||||||
|
("Band: Mörtel", date(2026, 6, 12), 150.00),
|
||||||
|
("Band: Kotpiloten", date(2026, 6, 13), 150.00),
|
||||||
|
("Band: Knast", date(2026, 6, 13), 150.00),
|
||||||
|
("Band: Quast", date(2026, 6, 13), 300.00),
|
||||||
|
]
|
||||||
|
|
||||||
|
DRINKS = [
|
||||||
|
# name, crates, btl/crate, size, price/crate, deposit/crate, sale/btl
|
||||||
|
("Sterni", 12, 20, 0.5, 10.99, 3.10, 2.00),
|
||||||
|
("Krosti", 5, 20, 0.5, 16.49, 3.10, 2.50),
|
||||||
|
("Buddi", 5, 20, 0.5, 20.99, 3.10, 2.50),
|
||||||
|
("Helles", 5, 20, 0.5, 15.99, 4.50, 2.50),
|
||||||
|
("Radler", 2, 20, 0.5, 14.99, 3.10, 2.50),
|
||||||
|
("Lübzer 0,0", 1, 20, 0.5, 17.99, 3.10, 2.50),
|
||||||
|
("Freiberger 0,0", 4, 20, 0.5, 15.49, 3.10, 2.50),
|
||||||
|
("Mate", 2, 20, 0.5, 17.49, 4.50, 2.50),
|
||||||
|
("Vita Cola", 2, 12, 1.0, 10.99, 3.30, 2.50),
|
||||||
|
("Spezi", 2, 20, 0.5, 17.99, 3.10, 2.50),
|
||||||
|
("Wasser", 10, 12, 1.0, 5.99, 3.30, 1.50),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Seed Drink rows for the 2026 festival from the supplier invoice."
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
for purpose, day, amount in PAYMENTS:
|
||||||
|
obj, created = Payment.objects.update_or_create(
|
||||||
|
purpose=purpose,
|
||||||
|
date=day,
|
||||||
|
defaults={"amount": amount},
|
||||||
|
)
|
||||||
|
self.stdout.write(f"{'created' if created else 'updated'}: {obj.purpose} ({obj.date})")
|
||||||
|
|
||||||
|
for name, crates, btl, size, price, deposit, sale in DRINKS:
|
||||||
|
obj, created = Drink.objects.update_or_create(
|
||||||
|
name=name,
|
||||||
|
year=2026,
|
||||||
|
defaults={
|
||||||
|
"crates_ordered": crates,
|
||||||
|
"crates_purchased": crates,
|
||||||
|
"crates_returned": 0,
|
||||||
|
"purchase_price_per_crate": price,
|
||||||
|
"deposit_per_crate": deposit,
|
||||||
|
"bottles_per_crate": btl,
|
||||||
|
"bottle_size": size,
|
||||||
|
"sale_price_per_bottle": sale,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.stdout.write(f"{'created' if created else 'updated'}: {obj.name}")
|
||||||
@@ -88,7 +88,9 @@ label {
|
|||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"] {
|
input[type="text"],
|
||||||
|
input[type="number"],
|
||||||
|
select {
|
||||||
background-color: rgba(80, 40, 10, 0.4);
|
background-color: rgba(80, 40, 10, 0.4);
|
||||||
color: #EEEEEE;
|
color: #EEEEEE;
|
||||||
border: 2px solid #885522;
|
border: 2px solid #885522;
|
||||||
@@ -96,9 +98,24 @@ input[type="text"] {
|
|||||||
padding: 14px 12px;
|
padding: 14px 12px;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
min-height: 52px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
font-family: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"]:focus {
|
select {
|
||||||
|
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'><path fill='%23EE9933' d='M0 0l6 8 6-8z'/></svg>");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 14px center;
|
||||||
|
background-size: 12px 8px;
|
||||||
|
padding-right: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"]:focus,
|
||||||
|
input[type="number"]:focus,
|
||||||
|
select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #EE9933;
|
border-color: #EE9933;
|
||||||
}
|
}
|
||||||
@@ -377,3 +394,49 @@ section {
|
|||||||
background-color: rgba(80, 40, 10, 0.4);
|
background-color: rgba(80, 40, 10, 0.4);
|
||||||
color: #FFCC77;
|
color: #FFCC77;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 28px;
|
||||||
|
background-color: rgba(80, 40, 10, 0.4);
|
||||||
|
border: 2px solid #885522;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background-color: #EE9933;
|
||||||
|
box-shadow: 0 0 12px #CC6611;
|
||||||
|
transition: width 200ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-label {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #161616;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dash-row {
|
||||||
|
grid-template-columns: 1fr auto auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-btn-paypal {
|
||||||
|
background-color: #003087;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-btn-paypal:hover, .link-btn-paypal:focus {
|
||||||
|
background-color: #0070BA;
|
||||||
|
color: #FFFFFF;
|
||||||
|
}
|
||||||
|
|||||||
+160
-8
@@ -2,6 +2,7 @@ from datetime import datetime
|
|||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.admin.views.decorators import staff_member_required
|
||||||
from django.contrib.auth import authenticate, login, logout
|
from django.contrib.auth import authenticate, login, logout
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
@@ -13,7 +14,7 @@ from django.utils import timezone
|
|||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
|
|
||||||
from gaehsnitz.models import Consumption, Drink
|
from gaehsnitz.models import Consumption, Donation, Drink, Payment, UserPayment, current_year
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
@@ -60,17 +61,24 @@ def _normalize_name(raw):
|
|||||||
|
|
||||||
|
|
||||||
def _tab_context(user):
|
def _tab_context(user):
|
||||||
consumption = user.consumption_list.select_related("drink").order_by("-created_at", "-id")
|
year = current_year()
|
||||||
paid = (
|
consumption = user.consumption_list.filter(drink__year=year).select_related("drink").order_by("-created_at", "-id")
|
||||||
user.paid_drinks.annotate(cost=F("amount") * F("drink__sale_price_per_bottle")).aggregate(total=Sum("cost"))[
|
total = (
|
||||||
"total"
|
user.paid_drinks.filter(drink__year=year)
|
||||||
]
|
.annotate(cost=F("amount") * F("drink__sale_price_per_bottle"))
|
||||||
|
.aggregate(total=Sum("cost"))["total"]
|
||||||
or 0
|
or 0
|
||||||
)
|
)
|
||||||
|
payments = user.user_payments.filter(created_at__year=year).order_by("-created_at")
|
||||||
|
paid = payments.aggregate(sum=Sum("amount"))["sum"] or 0
|
||||||
return {
|
return {
|
||||||
"tab_user": user,
|
"tab_user": user,
|
||||||
"consumption_list": consumption,
|
"consumption_list": consumption,
|
||||||
"total": paid,
|
"total": total,
|
||||||
|
"paid": paid,
|
||||||
|
"open_balance": total - paid,
|
||||||
|
"user_payments": payments,
|
||||||
|
"payment_methods": UserPayment.Method.choices,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -165,7 +173,7 @@ def pin_view(request):
|
|||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def me_view(request):
|
def me_view(request):
|
||||||
phase = _require_open(request)
|
phase = _require_open(request)
|
||||||
drinks = Drink.objects.order_by("name") if phase == "booking" else Drink.objects.none()
|
drinks = Drink.objects.filter(year=current_year()).order_by("name") if phase == "booking" else Drink.objects.none()
|
||||||
booked_drink = None
|
booked_drink = None
|
||||||
booked_id = request.GET.get("booked")
|
booked_id = request.GET.get("booked")
|
||||||
if booked_id:
|
if booked_id:
|
||||||
@@ -180,6 +188,7 @@ def me_view(request):
|
|||||||
"drinks": drinks,
|
"drinks": drinks,
|
||||||
"current_day": _current_festival_day(),
|
"current_day": _current_festival_day(),
|
||||||
"booked_drink": booked_drink,
|
"booked_drink": booked_drink,
|
||||||
|
"paid_toast": request.GET.get("paid") == "1",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return render(request, "suff/me.html", context)
|
return render(request, "suff/me.html", context)
|
||||||
@@ -208,6 +217,149 @@ def book_view(request):
|
|||||||
return HttpResponseRedirect(f"{reverse('suff:me')}?booked={drink.id}")
|
return HttpResponseRedirect(f"{reverse('suff:me')}?booked={drink.id}")
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@require_http_methods(["GET", "POST"])
|
||||||
|
def pay_view(request):
|
||||||
|
phase = _require_open(request)
|
||||||
|
if phase == "before":
|
||||||
|
raise Http404
|
||||||
|
|
||||||
|
error = None
|
||||||
|
if request.method == "POST":
|
||||||
|
raw_amount = (request.POST.get("amount") or "").replace(",", ".").strip()
|
||||||
|
method = request.POST.get("method") or ""
|
||||||
|
note = (request.POST.get("note") or "").strip()[:64]
|
||||||
|
|
||||||
|
valid_methods = {m for m, _ in UserPayment.Method.choices}
|
||||||
|
try:
|
||||||
|
amount = float(raw_amount)
|
||||||
|
except ValueError:
|
||||||
|
amount = 0.0
|
||||||
|
|
||||||
|
if method not in valid_methods:
|
||||||
|
error = "Bitte eine Zahlungsmethode wählen."
|
||||||
|
elif amount <= 0:
|
||||||
|
error = "Bitte einen Betrag größer 0 eingeben."
|
||||||
|
else:
|
||||||
|
UserPayment.objects.create(
|
||||||
|
user=request.user,
|
||||||
|
amount=amount,
|
||||||
|
method=method,
|
||||||
|
note=note,
|
||||||
|
)
|
||||||
|
return HttpResponseRedirect(f"{reverse('suff:me')}?paid=1")
|
||||||
|
|
||||||
|
context = _tab_context(request.user)
|
||||||
|
context.update({"phase": phase, "error": error})
|
||||||
|
return render(request, "suff/pay.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
DAY_LABELS = {1: "Donnerstag", 2: "Freitag", 3: "Samstag", 4: "Sonntag"}
|
||||||
|
|
||||||
|
|
||||||
|
@staff_member_required
|
||||||
|
@require_http_methods(["GET"])
|
||||||
|
def dashboard_view(request):
|
||||||
|
year = current_year()
|
||||||
|
|
||||||
|
total_donations = Donation.objects.filter(date__year=year).aggregate(s=Sum("amount"))["s"] or 0
|
||||||
|
total_payments = Payment.objects.filter(date__year=year).aggregate(s=Sum("amount"))["s"] or 0
|
||||||
|
total_balance = total_donations - total_payments
|
||||||
|
total_pct = int(round((total_donations / total_payments) * 100)) if total_payments else 0
|
||||||
|
total_pct_capped = min(total_pct, 100)
|
||||||
|
|
||||||
|
drinks = list(Drink.objects.filter(year=year).order_by("name"))
|
||||||
|
drink_rows = [
|
||||||
|
{
|
||||||
|
"name": d.name,
|
||||||
|
"sold": d.bottles_sold,
|
||||||
|
"total": d.bottles_total,
|
||||||
|
"balance": d.balance,
|
||||||
|
}
|
||||||
|
for d in drinks
|
||||||
|
]
|
||||||
|
sales_revenue = sum((d.sale_price_total for d in drinks), 0)
|
||||||
|
purchase_cost = sum((d.purchase_price_total for d in drinks), 0)
|
||||||
|
drinks_profit = sum((d.balance for d in drinks), 0)
|
||||||
|
refinance_pct = int(round((sales_revenue / purchase_cost) * 100)) if purchase_cost else 0
|
||||||
|
refinance_pct_capped = min(refinance_pct, 100)
|
||||||
|
|
||||||
|
user_rows = []
|
||||||
|
for user in User.objects.all():
|
||||||
|
consumed = user.consumed_drinks_price
|
||||||
|
if not consumed:
|
||||||
|
continue
|
||||||
|
paid = user.paid_amount
|
||||||
|
user_rows.append(
|
||||||
|
{
|
||||||
|
"username": user.username,
|
||||||
|
"consumed": consumed,
|
||||||
|
"paid": paid,
|
||||||
|
"open": consumed - paid,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
user_rows.sort(key=lambda r: r["open"], reverse=True)
|
||||||
|
|
||||||
|
top_spender = None
|
||||||
|
if user_rows:
|
||||||
|
top = max(user_rows, key=lambda r: r["consumed"])
|
||||||
|
top_spender = {"username": top["username"], "total": top["consumed"]}
|
||||||
|
|
||||||
|
drink_amounts = (
|
||||||
|
Consumption.objects.filter(drink__year=year, for_free=False)
|
||||||
|
.values("drink__name")
|
||||||
|
.annotate(amount=Sum("amount"))
|
||||||
|
.order_by("-amount")
|
||||||
|
)
|
||||||
|
top_drink = None
|
||||||
|
if drink_amounts:
|
||||||
|
top_drink = {"name": drink_amounts[0]["drink__name"], "amount": drink_amounts[0]["amount"]}
|
||||||
|
|
||||||
|
day_amounts = (
|
||||||
|
Consumption.objects.filter(drink__year=year, for_free=False)
|
||||||
|
.values("day")
|
||||||
|
.annotate(amount=Sum("amount"))
|
||||||
|
.order_by("-amount")
|
||||||
|
)
|
||||||
|
busiest_day = None
|
||||||
|
if day_amounts:
|
||||||
|
d = day_amounts[0]
|
||||||
|
busiest_day = {"label": DAY_LABELS.get(d["day"], "?"), "amount": d["amount"]}
|
||||||
|
|
||||||
|
top_per_day = []
|
||||||
|
for day_num, label in DAY_LABELS.items():
|
||||||
|
rows = (
|
||||||
|
Consumption.objects.filter(drink__year=year, day=day_num, for_free=False)
|
||||||
|
.values("user__username")
|
||||||
|
.annotate(amount=Sum("amount"))
|
||||||
|
.order_by("-amount")
|
||||||
|
)
|
||||||
|
if rows:
|
||||||
|
r = rows[0]
|
||||||
|
top_per_day.append({"label": label, "username": r["user__username"], "amount": r["amount"]})
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"year": year,
|
||||||
|
"total_donations": total_donations,
|
||||||
|
"total_payments": total_payments,
|
||||||
|
"total_balance": total_balance,
|
||||||
|
"total_pct": total_pct,
|
||||||
|
"total_pct_capped": total_pct_capped,
|
||||||
|
"drink_rows": drink_rows,
|
||||||
|
"sales_revenue": sales_revenue,
|
||||||
|
"purchase_cost": purchase_cost,
|
||||||
|
"drinks_profit": drinks_profit,
|
||||||
|
"refinance_pct": refinance_pct,
|
||||||
|
"refinance_pct_capped": refinance_pct_capped,
|
||||||
|
"user_rows": user_rows,
|
||||||
|
"top_spender": top_spender,
|
||||||
|
"top_drink": top_drink,
|
||||||
|
"busiest_day": busiest_day,
|
||||||
|
"top_per_day": top_per_day,
|
||||||
|
}
|
||||||
|
return render(request, "suff/dashboard.html", context)
|
||||||
|
|
||||||
|
|
||||||
@require_http_methods(["POST"])
|
@require_http_methods(["POST"])
|
||||||
def logout_view(request):
|
def logout_view(request):
|
||||||
_require_open(request)
|
_require_open(request)
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ from django.urls import path
|
|||||||
|
|
||||||
from gaehsnitz.suff import (
|
from gaehsnitz.suff import (
|
||||||
book_view,
|
book_view,
|
||||||
|
dashboard_view,
|
||||||
logout_view,
|
logout_view,
|
||||||
me_view,
|
me_view,
|
||||||
name_view,
|
name_view,
|
||||||
|
pay_view,
|
||||||
pin_view,
|
pin_view,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -15,5 +17,7 @@ urlpatterns = [
|
|||||||
path("pin/", pin_view, name="pin"),
|
path("pin/", pin_view, name="pin"),
|
||||||
path("me/", me_view, name="me"),
|
path("me/", me_view, name="me"),
|
||||||
path("book/", book_view, name="book"),
|
path("book/", book_view, name="book"),
|
||||||
|
path("pay/", pay_view, name="pay"),
|
||||||
|
path("dashboard/", dashboard_view, name="dashboard"),
|
||||||
path("logout/", logout_view, name="logout"),
|
path("logout/", logout_view, name="logout"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
{% extends "suff/base.html" %}
|
||||||
|
{% load money %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Dashboard {{ year }}</h2>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Refinanzierung gesamt</h3>
|
||||||
|
<div class="progress-wrap">
|
||||||
|
<div class="progress-bar" style="width: {{ total_pct_capped }}%;"></div>
|
||||||
|
<span class="progress-label">{{ total_pct }}%</span>
|
||||||
|
</div>
|
||||||
|
<p class="muted-left">
|
||||||
|
Spenden {{ total_donations|euro }} / Ausgaben {{ total_payments|euro }}
|
||||||
|
</p>
|
||||||
|
<p class="muted-left">
|
||||||
|
Bilanz: <b>{{ total_balance|euro }}</b>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Refinanzierung Getränke</h3>
|
||||||
|
<div class="progress-wrap">
|
||||||
|
<div class="progress-bar" style="width: {{ refinance_pct_capped }}%;"></div>
|
||||||
|
<span class="progress-label">{{ refinance_pct }}%</span>
|
||||||
|
</div>
|
||||||
|
<p class="muted-left">
|
||||||
|
Verkaufserlös {{ sales_revenue|euro }} / Einkaufspreis {{ purchase_cost|euro }}
|
||||||
|
</p>
|
||||||
|
<p class="muted-left">
|
||||||
|
Aktueller Gewinn: <b>{{ drinks_profit|euro }}</b>
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Getränke</h3>
|
||||||
|
<ul class="history">
|
||||||
|
{% for d in drink_rows %}
|
||||||
|
<li class="dash-row">
|
||||||
|
<span class="hist-what"><b>{{ d.name }}</b></span>
|
||||||
|
<span class="hist-when">{{ d.sold }}/{{ d.total }} verkauft</span>
|
||||||
|
<span class="hist-price">{{ d.balance|euro }}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Offene Beträge</h3>
|
||||||
|
{% if user_rows %}
|
||||||
|
<ul class="history">
|
||||||
|
{% for u in user_rows %}
|
||||||
|
<li>
|
||||||
|
<span class="hist-when">{{ u.username }}</span>
|
||||||
|
<span class="hist-what">{{ u.consumed|euro }} − {{ u.paid|euro }}</span>
|
||||||
|
<span class="hist-price">{{ u.open|euro }}</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="muted-left">Niemand hat etwas konsumiert.</p>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h3>Fun Facts</h3>
|
||||||
|
<ul class="history">
|
||||||
|
{% if top_spender %}
|
||||||
|
<li>
|
||||||
|
<span class="hist-when">Top-Zecher</span>
|
||||||
|
<span class="hist-what">{{ top_spender.username }}</span>
|
||||||
|
<span class="hist-price">{{ top_spender.total|euro }}</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if top_drink %}
|
||||||
|
<li>
|
||||||
|
<span class="hist-when">Top-Getränk</span>
|
||||||
|
<span class="hist-what">{{ top_drink.name }}</span>
|
||||||
|
<span class="hist-price">{{ top_drink.amount }}x</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if busiest_day %}
|
||||||
|
<li>
|
||||||
|
<span class="hist-when">Härtester Tag</span>
|
||||||
|
<span class="hist-what">{{ busiest_day.label }}</span>
|
||||||
|
<span class="hist-price">{{ busiest_day.amount }} Flaschen</span>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% for f in top_per_day %}
|
||||||
|
<li>
|
||||||
|
<span class="hist-when">{{ f.label }}</span>
|
||||||
|
<span class="hist-what">{{ f.username }}</span>
|
||||||
|
<span class="hist-price">{{ f.amount }} Flaschen</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="logout-form">
|
||||||
|
<a href="{% url 'suff:me' %}" class="link-btn link-btn-secondary">Zurück</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -13,11 +13,27 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if paid_toast %}
|
||||||
|
<div class="toast" role="status">
|
||||||
|
Zahlung gespeichert. Danke!
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<section class="total-box">
|
<section class="total-box">
|
||||||
<span class="total-label">Deine Rechnung</span>
|
<span class="total-label">Deine Rechnung</span>
|
||||||
<span class="total-value">{{ total|floatformat:2 }} €</span>
|
<span class="total-value">{{ total|floatformat:2 }} €</span>
|
||||||
|
{% if paid %}
|
||||||
|
<span class="total-label">Bezahlt</span>
|
||||||
|
<span class="total-value">{{ paid|floatformat:2 }} €</span>
|
||||||
|
<span class="total-label">Offen</span>
|
||||||
|
<span class="total-value">{{ open_balance|floatformat:2 }} €</span>
|
||||||
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{% if open_balance > 0 %}
|
||||||
|
<p><a href="{% url 'suff:pay' %}" class="btn-primary">Bezahlen</a></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if phase == "booking" %}
|
{% if phase == "booking" %}
|
||||||
<section>
|
<section>
|
||||||
<h3>Neues Getränk buchen</h3>
|
<h3>Neues Getränk buchen</h3>
|
||||||
@@ -66,6 +82,12 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{% if request.user.is_staff %}
|
||||||
|
<div class="link-row">
|
||||||
|
<a href="{% url 'suff:dashboard' %}" class="link-btn link-btn-secondary">Dashboard</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<form method="post" action="{% url 'suff:logout' %}" class="logout-form">
|
<form method="post" action="{% url 'suff:logout' %}" class="logout-form">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<button type="submit" class="btn-secondary">Logout</button>
|
<button type="submit" class="btn-secondary">Logout</button>
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
{% extends "suff/base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>Bezahlen</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Bitte zahle deinen offenen Betrag mit deiner bevorzugten Methode
|
||||||
|
(z. B. Bar an der Kasse oder PayPal an Flo) und trage
|
||||||
|
den bezahlten Betrag anschließend hier ein.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="link-row">
|
||||||
|
<a href="https://www.paypal.com/paypalme/lotharwiener" target="_blank" rel="noopener noreferrer" class="link-btn link-btn-paypal">
|
||||||
|
PayPal öffnen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="total-box">
|
||||||
|
<span class="total-label">Deine Rechnung</span>
|
||||||
|
<span class="total-value">{{ total|floatformat:2 }} €</span>
|
||||||
|
{% if paid %}
|
||||||
|
<span class="total-label">Bezahlt</span>
|
||||||
|
<span class="total-value">{{ paid|floatformat:2 }} €</span>
|
||||||
|
{% endif %}
|
||||||
|
<span class="total-label">Offen</span>
|
||||||
|
<span class="total-value">{{ open_balance|floatformat:2 }} €</span>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{% if error %}
|
||||||
|
<p class="error">{{ error }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<form method="post" action="{% url 'suff:pay' %}" class="pay-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
<label>
|
||||||
|
Betrag (€)
|
||||||
|
<input type="number" name="amount" step="0.01" min="0.01"
|
||||||
|
value="{{ open_balance|floatformat:2 }}" required />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Methode
|
||||||
|
<select name="method" required>
|
||||||
|
<option value="">— wählen —</option>
|
||||||
|
{% for value, label in payment_methods %}
|
||||||
|
<option value="{{ value }}">{{ label }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Notiz (optional)
|
||||||
|
<input type="text" name="note" maxlength="64" />
|
||||||
|
</label>
|
||||||
|
<button type="submit" class="btn-primary">Zahlung eintragen</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{% if user_payments %}
|
||||||
|
<section>
|
||||||
|
<h3>Bisherige Zahlungen</h3>
|
||||||
|
<ul class="history">
|
||||||
|
{% for p in user_payments %}
|
||||||
|
<li>
|
||||||
|
<span class="hist-when">{{ p.created_at|date:"d.m. H:i" }}</span>
|
||||||
|
<span class="hist-what">{{ p.get_method_display }}{% if p.note %} – {{ p.note }}{% endif %}</span>
|
||||||
|
<span class="hist-price">{{ p.amount|floatformat:2 }} €</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="logout-form">
|
||||||
|
<a href="{% url 'suff:me' %}" class="link-btn link-btn-secondary">Zurück</a>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user