From 51d079a467a937e68b3d5c8f11b07b5373cccc75 Mon Sep 17 00:00:00 2001 From: Flo Ha Date: Tue, 26 May 2026 23:50:25 +0200 Subject: [PATCH] Add free-drink toggle, payment deletion, donation flow, expanded dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Crew can mark bookings as free (Gratis checkbox) on staff_user page; free anonymous bookings skip the auto-payment. - Users (and crew) can delete their UserPayments with a confirmation page. - Pay page redesigned around "Eintrittsspende": quick-pick amount buttons (drinks rounded to 5 € + 10/15/20/25/30), "Nur Drinks" escape, intro text, type=text input to dodge browser locale formatting. - Me page: always-visible donate button, intro text linking drinks + Eintrittsspende. - Day cutoff at 06:00 Berlin: festival day rolls at 06:00, applied to current_festival_day plus Frühaufsteher/Nachtschwärmer/Goldene Stunde. - Dashboard: single Finanzen section (income vs costs) with breakdown, fun facts grouped (Trinker / Getränke / Zeit / Geld / Gratis). - Offene Beträge filtered to open > 0. - Staff list shows "(das bist du)" disabled row for own user. - Renamed seed_drinks_2026 -> seed_2026, added Baumarkt 194 € entry. Co-Authored-By: Claude Opus 4.7 --- .../{seed_drinks_2026.py => seed_2026.py} | 3 +- gaehsnitz/static/suff/style.css | 127 +++++++++ gaehsnitz/suff.py | 249 ++++++++++++++++-- gaehsnitz/suff_urls.py | 8 + .../suff/confirm_delete_payment.html | 21 ++ gaehsnitz/templates/suff/dashboard.html | 203 ++++++++++++-- gaehsnitz/templates/suff/me.html | 12 +- gaehsnitz/templates/suff/pay.html | 61 +++-- .../suff/staff_confirm_delete_payment.html | 18 ++ gaehsnitz/templates/suff/staff_index.html | 6 +- gaehsnitz/templates/suff/staff_pay.html | 3 + gaehsnitz/templates/suff/staff_user.html | 25 +- 12 files changed, 653 insertions(+), 83 deletions(-) rename gaehsnitz/management/commands/{seed_drinks_2026.py => seed_2026.py} (95%) create mode 100644 gaehsnitz/templates/suff/confirm_delete_payment.html create mode 100644 gaehsnitz/templates/suff/staff_confirm_delete_payment.html diff --git a/gaehsnitz/management/commands/seed_drinks_2026.py b/gaehsnitz/management/commands/seed_2026.py similarity index 95% rename from gaehsnitz/management/commands/seed_drinks_2026.py rename to gaehsnitz/management/commands/seed_2026.py index 63d1660..a767ee3 100644 --- a/gaehsnitz/management/commands/seed_drinks_2026.py +++ b/gaehsnitz/management/commands/seed_2026.py @@ -9,6 +9,7 @@ PAYMENTS = [ # purpose, date, amount ("Toiletten", date(2026, 5, 6), 210.01), ("Anzahlung Getränke+Kühlschrank+Bänke", date(2026, 5, 18), 400.00), + ("Baumarkt", date(2026, 5, 23), 194.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), @@ -34,7 +35,7 @@ DRINKS = [ class Command(BaseCommand): - help = "Seed Drink rows for the 2026 festival from the supplier invoice." + help = "Seed Drink and Payment rows for the 2026 festival." @transaction.atomic def handle(self, *args, **options): diff --git a/gaehsnitz/static/suff/style.css b/gaehsnitz/static/suff/style.css index 79dd841..c6177ed 100644 --- a/gaehsnitz/static/suff/style.css +++ b/gaehsnitz/static/suff/style.css @@ -150,11 +150,100 @@ button:active { text-align: center; } +.intro { + font-size: 0.95rem; + color: #CCCCCC; + line-height: 1.4; + margin-bottom: 8px; +} + +.intro strong { + color: #FFCC77; +} + +.amount-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; +} + +.amount-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + min-height: 68px; + padding: 10px 8px; + border: 2px solid #885522; + border-radius: 6px; + background: #1f1f1f; + color: #FFCC77; + text-decoration: none; + transition: background-color 100ms ease, border-color 100ms ease; +} + +.amount-main { + font-size: 1.2rem; + font-weight: bold; + line-height: 1.1; +} + +.amount-sub { + font-size: 0.8rem; + color: #AAAAAA; + line-height: 1.1; + text-align: center; +} + +.amount-btn:hover, .amount-btn:focus { + border-color: #EE9933; + background: #2a1f10; +} + +.amount-btn-active { + border-color: #EE9933; + background: #2a1f10; + color: #EE9933; +} + +.amount-btn-weak { + border-color: #553311; + background: #181818; + color: #AAAAAA; + opacity: 0.85; +} + +.amount-btn-weak .amount-sub { + color: #888888; +} + +.amount-link { + color: #FFCC77; + text-decoration: underline; +} + .muted-left { font-size: 0.9rem; color: #AAAAAA; } +.user-self { + color: #888888; + cursor: not-allowed; +} + +.muted-inline { + color: #888888; + font-size: 0.85rem; + font-style: italic; +} + +.history-sub { + margin-top: 18px; + opacity: 0.85; +} + h3 { font-size: 1.1rem; font-weight: normal; @@ -191,6 +280,40 @@ section { text-shadow: 0 0 12px #CC6611; } +.for-free-toggle { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 12px; + padding: 14px 20px; + margin-bottom: 12px; + min-height: 52px; + border: 2px solid #885522; + border-radius: 6px; + background: #1f1f1f; + color: #FFCC77; + font-size: 1.1rem; + font-weight: bold; + cursor: pointer; + user-select: none; + transition: background-color 100ms ease, border-color 100ms ease; +} + +.for-free-toggle input { + width: 22px; + height: 22px; + accent-color: #EE9933; + margin: 0; + cursor: pointer; +} + +.for-free-toggle:has(input:checked) { + border-color: #EE9933; + background: #2a1f10; + color: #EE9933; +} + .drink-grid { display: grid; grid-template-columns: repeat(2, 1fr); @@ -530,6 +653,10 @@ section { grid-template-columns: 1fr auto auto; } +.history li.fin-row { + grid-template-columns: 1fr auto; +} + .link-btn-paypal { background-color: #003087; color: #FFFFFF; diff --git a/gaehsnitz/suff.py b/gaehsnitz/suff.py index b13e037..7cd77ed 100644 --- a/gaehsnitz/suff.py +++ b/gaehsnitz/suff.py @@ -1,4 +1,5 @@ -from datetime import datetime +import math +from datetime import datetime, timedelta from zoneinfo import ZoneInfo from django.conf import settings @@ -6,7 +7,7 @@ from django.contrib.admin.views.decorators import staff_member_required from django.contrib.auth import authenticate, login, logout from django.contrib.auth.decorators import login_required from django.contrib.auth import get_user_model -from django.db.models import Case, F, IntegerField, Sum, Value, When +from django.db.models import Case, Count, F, IntegerField, Sum, Value, When from django.http import Http404, HttpResponseRedirect from django.shortcuts import render from django.urls import reverse @@ -28,6 +29,12 @@ BOOKING_START = datetime(2026, 5, 15, 0, 0, 0, tzinfo=BERLIN) BOOKING_END = datetime(2026, 5, 31, 23, 59, 59, tzinfo=BERLIN) DAY_BY_WEEKDAY = {3: 1, 4: 2, 5: 3, 6: 4} +DAY_CUTOFF_HOUR = 6 + + +def _shifted_local(dt): + """Local Berlin time minus 6h — so 05:59 belongs to previous calendar day.""" + return dt.astimezone(BERLIN) - timedelta(hours=DAY_CUTOFF_HOUR) def _now(): @@ -54,7 +61,8 @@ def _require_open(request): def _current_festival_day(): - weekday = _now().weekday() + # Day rolls at 06:00 Berlin: bookings before 06:00 count as previous day. + weekday = _shifted_local(timezone.now()).weekday() return DAY_BY_WEEKDAY.get(weekday, 4) @@ -314,6 +322,31 @@ def delete_consumption_view(request, consumption_id): ) +@login_required +@require_http_methods(["GET", "POST"]) +def delete_payment_view(request, payment_id): + redirect, phase = _require_open(request) + if redirect: + return redirect + if phase != "booking": + raise Http404 + + try: + payment = UserPayment.objects.get(pk=payment_id, user=request.user) + except UserPayment.DoesNotExist: + raise Http404 + + if request.method == "POST": + payment.delete() + return HttpResponseRedirect(reverse("suff:pay")) + + return render( + request, + "suff/confirm_delete_payment.html", + {"phase": phase, "payment": payment}, + ) + + @login_required @require_http_methods(["GET", "POST"]) def pay_view(request): @@ -347,7 +380,33 @@ def pay_view(request): return HttpResponseRedirect(f"{reverse('suff:me')}?paid=1") context = _tab_context(request.user) - context.update({"phase": phase, "error": error}) + open_balance = max(float(context["open_balance"]), 0.0) + drinks_rounded = int(math.ceil(open_balance / 5.0) * 5) if open_balance > 0 else 0 + suggestions = [ + {"amount": drinks_rounded + d, "entry": round(drinks_rounded + d - open_balance, 2)} + for d in (10, 15, 20, 25, 30) + ] + prefill_raw = request.GET.get("amount") + try: + prefill_amount = float(prefill_raw) if prefill_raw else None + if prefill_amount is not None and prefill_amount <= 0: + prefill_amount = None + except ValueError: + prefill_amount = None + if prefill_amount is None: + prefill_amount = float(suggestions[2]["amount"]) + prefill_value = f"{prefill_amount:.2f}" + context.update( + { + "phase": phase, + "error": error, + "drinks_rounded": drinks_rounded, + "suggestions": suggestions, + "prefill_amount": prefill_amount, + "prefill_value": prefill_value, + "open_balance_url": f"{open_balance:.2f}", + } + ) return render(request, "suff/pay.html", context) @@ -468,6 +527,7 @@ def staff_user_view(request, username): "drinks": drinks, "current_day": _current_festival_day(), "booked_drink": booked_drink, + "booked_free": request.GET.get("free") == "1", "paid_toast": request.GET.get("paid") == "1", "is_anonymous_target": _is_anonymous(target), } @@ -486,6 +546,7 @@ def staff_book_view(request, username): target = _get_staff_target(username) drink_id = request.POST.get("drink_id") + for_free = request.POST.get("for_free") == "1" try: drink = Drink.objects.get(pk=int(drink_id)) except (Drink.DoesNotExist, TypeError, ValueError): @@ -496,16 +557,17 @@ def staff_book_view(request, username): drink=drink, amount=1, day=_current_festival_day(), - for_free=False, + for_free=for_free, ) - if _is_anonymous(target): + if _is_anonymous(target) and not for_free: UserPayment.objects.create( user=target, amount=drink.sale_price_per_bottle, method=UserPayment.Method.cash, note=f"Auto: {drink.name}", ) - return HttpResponseRedirect(f"{reverse('suff:staff_user', args=[username])}?booked={drink.id}") + suffix = "&free=1" if for_free else "" + return HttpResponseRedirect(f"{reverse('suff:staff_user', args=[username])}?booked={drink.id}{suffix}") @_staff_required @@ -549,6 +611,32 @@ def staff_pay_view(request, username): return render(request, "suff/staff_pay.html", context) +@_staff_required +@require_http_methods(["GET", "POST"]) +def staff_delete_payment_view(request, username, payment_id): + redirect, phase = _require_open(request) + if redirect: + return redirect + if phase != "booking": + raise Http404 + + target = _get_staff_target(username) + try: + payment = UserPayment.objects.get(pk=payment_id, user=target) + except UserPayment.DoesNotExist: + raise Http404 + + if request.method == "POST": + payment.delete() + return HttpResponseRedirect(reverse("suff:staff_pay", args=[username])) + + return render( + request, + "suff/staff_confirm_delete_payment.html", + {"phase": phase, "payment": payment, "tab_user": target}, + ) + + @_staff_required @require_http_methods(["GET", "POST"]) def staff_delete_consumption_view(request, username, consumption_id): @@ -597,10 +685,8 @@ 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) + total_costs = Payment.objects.filter(date__year=year).aggregate(s=Sum("amount"))["s"] or 0 + user_payments_total = UserPayment.objects.filter(created_at__year=year).aggregate(s=Sum("amount"))["s"] or 0 drinks = list(Drink.objects.filter(year=year).order_by("name")) drink_rows = [ @@ -614,9 +700,14 @@ def dashboard_view(request): ] 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) + free_drinks_value = sum((d.giveaway_purchase_value for d in drinks), 0) + unsold_purchase_value = sum((d.remaining_purchase_value for d in drinks), 0) + unsold_sale_value = sum((d.bottles_remaining * d.sale_price_per_bottle for d in drinks), 0) + + income_total = total_donations + user_payments_total + finance_balance = income_total - total_costs + finance_pct = int(round((income_total / total_costs) * 100)) if total_costs else 0 + finance_pct_capped = min(finance_pct, 100) user_rows = [] for user in User.objects.all(): @@ -624,12 +715,15 @@ def dashboard_view(request): if not consumed: continue paid = user.paid_amount + open_amount = consumed - paid + if open_amount <= 0: + continue user_rows.append( { "username": user.username, "consumed": consumed, "paid": paid, - "open": consumed - paid, + "open": open_amount, } ) user_rows.sort(key=lambda r: r["open"], reverse=True) @@ -672,24 +766,135 @@ def dashboard_view(request): r = rows[0] top_per_day.append({"label": label, "username": r["user__username"], "amount": r["amount"]}) + cons_qs = Consumption.objects.filter(drink__year=year) + paid_cons_qs = cons_qs.filter(for_free=False) + + # Time facts + first_cons = cons_qs.exclude(created_at__isnull=True).order_by("created_at").select_related("user", "drink").first() + last_cons = cons_qs.exclude(created_at__isnull=True).order_by("-created_at").select_related("user", "drink").first() + + # Day rolls at 06:00 Berlin: 05:59 counts as late "previous day", 07:00 = early "today". + # Compute time-of-day relative to 06:00 cutoff. + earliest_per_user = None + latest_per_user = None + earliest_key = None + latest_key = None + hour_buckets = {} + for c in cons_qs.exclude(created_at__isnull=True).values("user__username", "created_at", "amount"): + local = c["created_at"].astimezone(BERLIN) + # minutes after 06:00 cutoff (0 = 06:00, 1439 = 05:59 next morning) + shifted = (local.hour * 60 + local.minute - DAY_CUTOFF_HOUR * 60) % (24 * 60) + if earliest_key is None or shifted < earliest_key: + earliest_key = shifted + earliest_per_user = {"user__username": c["user__username"], "t": c["created_at"]} + if latest_key is None or shifted > latest_key: + latest_key = shifted + latest_per_user = {"user__username": c["user__username"], "t": c["created_at"]} + hour_buckets[local.hour] = hour_buckets.get(local.hour, 0) + (c["amount"] or 0) + + golden_hour = None + if hour_buckets: + top_hour, top_amount = max(hour_buckets.items(), key=lambda kv: kv[1]) + golden_hour = { + "label": f"{top_hour:02d} – {(top_hour + 1) % 24:02d} Uhr", + "amount": top_amount, + } + + # Drink mix / variety + variety_rows = ( + paid_cons_qs.values("user__username") + .annotate(distinct=Count("drink", distinct=True)) + .order_by("-distinct") + ) + variety_champ = variety_rows[0] if variety_rows and variety_rows[0]["distinct"] > 1 else None + + cat_rows = ( + paid_cons_qs.values("drink__category") + .annotate(amount=Sum("amount")) + ) + cat_total = sum(r["amount"] for r in cat_rows) or 0 + beer_amount = sum(r["amount"] for r in cat_rows if r["drink__category"] == "beer") + beer_share = int(round(beer_amount / cat_total * 100)) if cat_total else 0 + + def _top_in_categories(cats, label): + rows = ( + paid_cons_qs.filter(drink__category__in=cats) + .values("user__username") + .annotate(amount=Sum("amount")) + .order_by("-amount") + ) + if rows: + return {"label": label, "username": rows[0]["user__username"], "amount": rows[0]["amount"]} + return None + + alcfree_top = _top_in_categories(["alc_free_beer", "alc_free_radler"], "Alkoholfrei-Held") + radler_top = _top_in_categories(["radler"], "Radler-Fan") + water_top = _top_in_categories(["water"], "Wassertrinker") + + # Money / behavior + overpay_rows = [] + for user in User.objects.all(): + consumed = user.consumed_drinks_price or 0 + paid = user.paid_amount or 0 + if paid > consumed and consumed > 0: + overpay_rows.append({"username": user.username, "tip": paid - consumed}) + overpay_rows.sort(key=lambda r: r["tip"], reverse=True) + biggest_tip = overpay_rows[0] if overpay_rows else None + total_entry = sum(r["tip"] for r in overpay_rows) + + method_rows = ( + UserPayment.objects.filter(created_at__year=year) + .values("method") + .annotate(s=Sum("amount")) + ) + method_split = {r["method"]: r["s"] for r in method_rows} + + # Free drinks + free_recipient_rows = ( + cons_qs.filter(for_free=True) + .values("user__username") + .annotate(amount=Sum("amount")) + .order_by("-amount") + ) + top_free_recipient = free_recipient_rows[0] if free_recipient_rows else None + free_total_count = cons_qs.filter(for_free=True).aggregate(s=Sum("amount"))["s"] or 0 + + 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, + "total_costs": total_costs, + "user_payments_total": user_payments_total, + "income_total": income_total, + "finance_balance": finance_balance, + "finance_pct": finance_pct, + "finance_pct_capped": finance_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, + "free_drinks_value": free_drinks_value, + "unsold_purchase_value": unsold_purchase_value, + "unsold_sale_value": unsold_sale_value, "user_rows": user_rows, "top_spender": top_spender, "top_drink": top_drink, "busiest_day": busiest_day, "top_per_day": top_per_day, + "first_cons": first_cons, + "last_cons": last_cons, + "earliest_per_user": earliest_per_user, + "latest_per_user": latest_per_user, + "golden_hour": golden_hour, + "variety_champ": variety_champ, + "beer_share": beer_share, + "alcfree_top": alcfree_top, + "radler_top": radler_top, + "water_top": water_top, + "biggest_tip": biggest_tip, + "total_entry": total_entry, + "method_split": method_split, + "top_free_recipient": top_free_recipient, + "free_total_count": free_total_count, } return render(request, "suff/dashboard.html", context) diff --git a/gaehsnitz/suff_urls.py b/gaehsnitz/suff_urls.py index f228c03..db36598 100644 --- a/gaehsnitz/suff_urls.py +++ b/gaehsnitz/suff_urls.py @@ -5,6 +5,7 @@ from gaehsnitz.suff import ( closed_view, dashboard_view, delete_consumption_view, + delete_payment_view, logout_view, me_view, name_view, @@ -12,6 +13,7 @@ from gaehsnitz.suff import ( pin_view, staff_book_view, staff_delete_consumption_view, + staff_delete_payment_view, staff_index_view, staff_pay_view, staff_pin_reset_view, @@ -29,6 +31,7 @@ urlpatterns = [ path("book/", book_view, name="book"), path("book//delete/", delete_consumption_view, name="delete_consumption"), path("pay/", pay_view, name="pay"), + path("pay//delete/", delete_payment_view, name="delete_payment"), path("dashboard/", dashboard_view, name="dashboard"), path("staff/", staff_index_view, name="staff_index"), path("staff/new/", staff_register_view, name="staff_register"), @@ -36,6 +39,11 @@ urlpatterns = [ path("staff/u//pin/", staff_pin_reset_view, name="staff_pin_reset"), path("staff/u//book/", staff_book_view, name="staff_book"), path("staff/u//pay/", staff_pay_view, name="staff_pay"), + path( + "staff/u//pay//delete/", + staff_delete_payment_view, + name="staff_delete_payment", + ), path( "staff/u//book//delete/", staff_delete_consumption_view, diff --git a/gaehsnitz/templates/suff/confirm_delete_payment.html b/gaehsnitz/templates/suff/confirm_delete_payment.html new file mode 100644 index 0000000..1766dc9 --- /dev/null +++ b/gaehsnitz/templates/suff/confirm_delete_payment.html @@ -0,0 +1,21 @@ +{% extends "suff/base.html" %} + +{% block content %} +

Zahlung löschen?

+ +

+ Willst du wirklich diese Zahlung löschen? +

+ +
+ {{ payment.get_method_display }}{% if payment.note %} – {{ payment.note }}{% endif %} + {{ payment.amount|floatformat:2 }} € + {{ payment.created_at|date:"d.m. H:i" }} +
+ +
+ {% csrf_token %} + + Nein, zurück +
+{% endblock %} diff --git a/gaehsnitz/templates/suff/dashboard.html b/gaehsnitz/templates/suff/dashboard.html index 75324f4..3ce89ba 100644 --- a/gaehsnitz/templates/suff/dashboard.html +++ b/gaehsnitz/templates/suff/dashboard.html @@ -5,31 +5,55 @@

Dashboard {{ year }}

-

Refinanzierung gesamt

+

Finanzen

-
- {{ total_pct }}% +
+ {{ finance_pct }}%
-

- Spenden {{ total_donations|euro }} / Ausgaben {{ total_payments|euro }} -

-

- Bilanz: {{ total_balance|euro }} -

-
- -
-

Refinanzierung Getränke

-
-
- {{ refinance_pct }}% -
-

- Verkaufserlös {{ sales_revenue|euro }} / Einkaufspreis {{ purchase_cost|euro }} -

-

- Aktueller Gewinn: {{ drinks_profit|euro }} -

+
    +
  • + Ausgaben gesamt + {{ total_costs|euro }} +
  • +
  • + Einnahmen gesamt + {{ income_total|euro }} +
  • +
  • + Bilanz + {{ finance_balance|euro }} +
  • +
+
    +
  • + Spenden (Donations) + {{ total_donations|euro }} +
  • +
  • + Zahlungen (User-Tab) + {{ user_payments_total|euro }} +
  • +
  • + Einkaufspreis Getränke + {{ purchase_cost|euro }} +
  • +
  • + Verkaufserlös Getränke + {{ sales_revenue|euro }} +
  • +
  • + Gratis-Getränke (EK-Wert) + {{ free_drinks_value|euro }} +
  • +
  • + Unverkauft (EK-Wert) + {{ unsold_purchase_value|euro }} +
  • +
  • + Unverkauft (potenzieller VK) + {{ unsold_sale_value|euro }} +
  • +
@@ -63,7 +87,7 @@
-

Fun Facts

+

Fun Facts: Trinker

    {% if top_spender %}
  • @@ -72,6 +96,26 @@ {{ top_spender.total|euro }}
  • {% endif %} + {% if variety_champ %} +
  • + Vielfalt-Champion + {{ variety_champ.user__username }} + {{ variety_champ.distinct }} Sorten +
  • + {% endif %} + {% for f in top_per_day %} +
  • + Top {{ f.label }} + {{ f.username }} + {{ f.amount }} Flaschen +
  • + {% endfor %} +
+
+ +
+

Fun Facts: Getränke

+
    {% if top_drink %}
  • Top-Getränk @@ -79,6 +123,73 @@ {{ top_drink.amount }}x
  • {% endif %} +
  • + Bier-Anteil + am Konsum + {{ beer_share }}% +
  • + {% if alcfree_top %} +
  • + {{ alcfree_top.label }} + {{ alcfree_top.username }} + {{ alcfree_top.amount }} Flaschen +
  • + {% endif %} + {% if radler_top %} +
  • + {{ radler_top.label }} + {{ radler_top.username }} + {{ radler_top.amount }} Flaschen +
  • + {% endif %} + {% if water_top %} +
  • + {{ water_top.label }} + {{ water_top.username }} + {{ water_top.amount }} Flaschen +
  • + {% endif %} +
+
+ +
+

Fun Facts: Zeit

+
    + {% if first_cons %} +
  • + Erste Buchung + {{ first_cons.user.username }} – {{ first_cons.drink.name }} + {{ first_cons.created_at|date:"d.m. H:i" }} +
  • + {% endif %} + {% if last_cons %} +
  • + Letzter Schluck + {{ last_cons.user.username }} – {{ last_cons.drink.name }} + {{ last_cons.created_at|date:"d.m. H:i" }} +
  • + {% endif %} + {% if earliest_per_user %} +
  • + Frühaufsteher + {{ earliest_per_user.user__username }} + {{ earliest_per_user.t|date:"d.m. H:i" }} +
  • + {% endif %} + {% if latest_per_user %} +
  • + Nachtschwärmer + {{ latest_per_user.user__username }} + {{ latest_per_user.t|date:"d.m. H:i" }} +
  • + {% endif %} + {% if golden_hour %} +
  • + Goldene Stunde + {{ golden_hour.label }} + {{ golden_hour.amount }} Flaschen +
  • + {% endif %} {% if busiest_day %}
  • Härtester Tag @@ -86,16 +197,52 @@ {{ busiest_day.amount }} Flaschen
  • {% endif %} - {% for f in top_per_day %} +
+
+ +
+

Fun Facts: Geld

+
    + {% if biggest_tip %}
  • - {{ f.label }} - {{ f.username }} - {{ f.amount }} Flaschen + Bigspender-Trinkgeld + {{ biggest_tip.username }} + {{ biggest_tip.tip|euro }} +
  • + {% endif %} +
  • + Eintrittsspenden gesamt + aus Überzahlungen + {{ total_entry|euro }} +
  • + {% for method, sum in method_split.items %} +
  • + Zahlungen {{ method }} + + {{ sum|euro }}
  • {% endfor %}
+
+

Fun Facts: Gratis

+
    +
  • + Aufmerksamkeit des Hauses + Gratis-Flaschen + {{ free_total_count }}x +
  • + {% if top_free_recipient %} +
  • + Glücklicher Empfänger + {{ top_free_recipient.user__username }} + {{ top_free_recipient.amount }} gratis +
  • + {% endif %} +
+
+ diff --git a/gaehsnitz/templates/suff/me.html b/gaehsnitz/templates/suff/me.html index 9ffb182..1f4edd8 100644 --- a/gaehsnitz/templates/suff/me.html +++ b/gaehsnitz/templates/suff/me.html @@ -3,6 +3,12 @@ {% block content %}

Hallo {{ tab_user.username }}

+

+ Tipp dich rein, sobald du was trinkst. Am Ende deines Besuchs kannst du + alles zusammen mit deiner Eintrittsspende bezahlen – + bar oder per PayPal. +

+ {% if booked_drink %}
Gebucht: +1 {{ booked_drink.name }} @@ -26,9 +32,7 @@ {% endif %} -{% if open_balance > 0 %} -

Bezahlen

-{% endif %} +

Für Drinks und Eintritt spenden

{% if phase == "booking" %}
@@ -84,7 +88,7 @@ {% if request.user.is_staff %} {% endif %} diff --git a/gaehsnitz/templates/suff/pay.html b/gaehsnitz/templates/suff/pay.html index f36fa69..b38b264 100644 --- a/gaehsnitz/templates/suff/pay.html +++ b/gaehsnitz/templates/suff/pay.html @@ -1,19 +1,8 @@ {% extends "suff/base.html" %} +{% load l10n %} {% block content %} -

Bezahlen

- -

- 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. -

- - +

Spenden

Deine Rechnung @@ -22,10 +11,41 @@ Bezahlt {{ paid|floatformat:2 }} € {% endif %} - Offen + Offen (Drinks) {{ open_balance|floatformat:2 }} €
+

+ Dein Beitrag deckt die Drinks und deine + Eintrittsspende. Die Vorschläge unten runden deinen + offenen Drink-Betrag auf die nächsten 5 € und legen 10–30 € + Eintritt drauf. +

+

+ Du darfst gerne weniger geben, wenn das gerade besser + passt – kein Problem. Und wenn du mehr geben kannst, + hilft uns das sehr, die Kosten für Bands, Toiletten und Technik zu + decken. So oder so: danke, dass du da bist! +

+ +
+

Vorschläge

+
+ {% for s in suggestions %} + + {{ s.amount }} € + → {{ s.entry|floatformat:2 }} € Eintritt + + {% endfor %} + {% if open_balance > 0 %} + + {{ open_balance|floatformat:2 }} € + → Nur Drinks + + {% endif %} +
+
+ {% if error %}

{{ error }}

{% endif %} @@ -34,8 +54,8 @@ {% csrf_token %}
+ {% endif %}