Add settled balance panel, cash booking mode, and Kasse dashboard line
- Balance panel turns green with "Bezahlt ✓" + breakdown when open_balance <= 0, on all four tab pages (me, pay, staff_user, staff_pay) - booking_mode radio on me.html and staff_user.html: normal / for_free / cash_paid; cash_paid auto-creates matching UserPayment(method=cash) - Dashboard finance section shows "Kasse (bar)" sum of all cash payments for cross-checking - staff_pay prefill lower-capped at 0; "Zahlung eintragen" always visible on staff_user Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -280,6 +280,28 @@ section {
|
||||
text-shadow: 0 0 12px #CC6611;
|
||||
}
|
||||
|
||||
.total-box-settled {
|
||||
border-color: #3a7a44;
|
||||
background-color: rgba(30, 80, 40, 0.35);
|
||||
}
|
||||
|
||||
.total-settled {
|
||||
color: #66cc77;
|
||||
font-size: 1.6rem;
|
||||
font-weight: bold;
|
||||
text-shadow: 0 0 12px #2a6633;
|
||||
}
|
||||
|
||||
.total-breakdown {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 4px 6px;
|
||||
margin-top: 4px;
|
||||
font-size: 0.82rem;
|
||||
color: #889988;
|
||||
}
|
||||
|
||||
.for-free-toggle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -314,6 +336,7 @@ section {
|
||||
color: #EE9933;
|
||||
}
|
||||
|
||||
|
||||
.drink-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
|
||||
+19
-4
@@ -282,6 +282,9 @@ def book_view(request):
|
||||
raise Http404
|
||||
|
||||
drink_id = request.POST.get("drink_id")
|
||||
booking_mode = request.POST.get("booking_mode", "normal")
|
||||
for_free = booking_mode == "for_free"
|
||||
cash_paid = booking_mode == "cash_paid"
|
||||
try:
|
||||
drink = Drink.objects.get(pk=int(drink_id))
|
||||
except (Drink.DoesNotExist, TypeError, ValueError):
|
||||
@@ -292,9 +295,17 @@ def book_view(request):
|
||||
drink=drink,
|
||||
amount=1,
|
||||
day=_current_festival_day(),
|
||||
for_free=False,
|
||||
for_free=for_free,
|
||||
)
|
||||
return HttpResponseRedirect(f"{reverse('suff:me')}?booked={drink.id}")
|
||||
if cash_paid:
|
||||
UserPayment.objects.create(
|
||||
user=request.user,
|
||||
amount=drink.sale_price_per_bottle,
|
||||
method=UserPayment.Method.cash,
|
||||
note=f"Auto: {drink.name}",
|
||||
)
|
||||
free_suffix = "&free=1" if for_free else ""
|
||||
return HttpResponseRedirect(f"{reverse('suff:me')}?booked={drink.id}{free_suffix}")
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -546,7 +557,9 @@ 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"
|
||||
booking_mode = request.POST.get("booking_mode", "normal")
|
||||
for_free = booking_mode == "for_free"
|
||||
cash_paid = booking_mode == "cash_paid"
|
||||
try:
|
||||
drink = Drink.objects.get(pk=int(drink_id))
|
||||
except (Drink.DoesNotExist, TypeError, ValueError):
|
||||
@@ -559,7 +572,7 @@ def staff_book_view(request, username):
|
||||
day=_current_festival_day(),
|
||||
for_free=for_free,
|
||||
)
|
||||
if _is_anonymous(target) and not for_free:
|
||||
if cash_paid or (_is_anonymous(target) and not for_free):
|
||||
UserPayment.objects.create(
|
||||
user=target,
|
||||
amount=drink.sale_price_per_bottle,
|
||||
@@ -848,6 +861,7 @@ def dashboard_view(request):
|
||||
.annotate(s=Sum("amount"))
|
||||
)
|
||||
method_split = {r["method"]: r["s"] for r in method_rows}
|
||||
cash_total = method_split.get(UserPayment.Method.cash, 0)
|
||||
|
||||
# Free drinks
|
||||
free_recipient_rows = (
|
||||
@@ -893,6 +907,7 @@ def dashboard_view(request):
|
||||
"biggest_tip": biggest_tip,
|
||||
"total_entry": total_entry,
|
||||
"method_split": method_split,
|
||||
"cash_total": cash_total,
|
||||
"top_free_recipient": top_free_recipient,
|
||||
"free_total_count": free_total_count,
|
||||
}
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
<span class="hist-what">Zahlungen (User-Tab)</span>
|
||||
<span class="hist-price">{{ user_payments_total|euro }}</span>
|
||||
</li>
|
||||
<li class="fin-row">
|
||||
<span class="hist-what">Kasse (bar)</span>
|
||||
<span class="hist-price">{{ cash_total|euro }}</span>
|
||||
</li>
|
||||
<li class="fin-row">
|
||||
<span class="hist-what">Einkaufspreis Getränke</span>
|
||||
<span class="hist-price">{{ purchase_cost|euro }}</span>
|
||||
|
||||
@@ -21,7 +21,21 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<section class="total-box">
|
||||
<section class="total-box {% if paid and open_balance <= 0 %}total-box-settled{% endif %}">
|
||||
{% if paid and open_balance <= 0 %}
|
||||
<span class="total-settled">Bezahlt ✓</span>
|
||||
<div class="total-breakdown">
|
||||
<span>Drinks {{ total|floatformat:2 }} €</span>
|
||||
<span>·</span>
|
||||
<span>Bezahlt {{ paid|floatformat:2 }} €</span>
|
||||
<span>·</span>
|
||||
{% if open_balance < 0 %}
|
||||
<span>Spende {{ open_balance|floatformat:2|slice:"1:" }} €</span>
|
||||
{% else %}
|
||||
<span>Genau bezahlt</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="total-label">Deine Rechnung</span>
|
||||
<span class="total-value">{{ total|floatformat:2 }} €</span>
|
||||
{% if paid %}
|
||||
@@ -30,6 +44,7 @@
|
||||
<span class="total-label">Offen</span>
|
||||
<span class="total-value">{{ open_balance|floatformat:2 }} €</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<p><a href="{% url 'suff:pay' %}" class="btn-primary">Für Drinks und Eintritt spenden</a></p>
|
||||
@@ -37,19 +52,26 @@
|
||||
{% if phase == "booking" %}
|
||||
<section>
|
||||
<h3>Neues Getränk buchen</h3>
|
||||
<div class="drink-grid">
|
||||
{% for drink in drinks %}
|
||||
<form method="post" action="{% url 'suff:book' %}">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="drink_id" value="{{ drink.id }}" />
|
||||
<button type="submit" class="drink-btn drink-btn-{{ drink.category }}">
|
||||
<label class="for-free-toggle">
|
||||
<input type="radio" name="booking_mode" value="for_free" />
|
||||
<span>Gratis (z.B. Artists am Spieltag)</span>
|
||||
</label>
|
||||
<label class="for-free-toggle">
|
||||
<input type="radio" name="booking_mode" value="cash_paid" />
|
||||
<span>Direkt bar bezahlt</span>
|
||||
</label>
|
||||
<div class="drink-grid">
|
||||
{% for drink in drinks %}
|
||||
<button type="submit" name="drink_id" value="{{ drink.id }}" class="drink-btn drink-btn-{{ drink.category }}">
|
||||
<span class="drink-plus">+1</span>
|
||||
<span class="drink-name">{{ drink.name }}</span>
|
||||
<span class="drink-price">{{ drink.sale_price_per_bottle|floatformat:2 }} €</span>
|
||||
</button>
|
||||
</form>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
@@ -80,7 +102,6 @@
|
||||
<div class="empty-state">
|
||||
<p class="empty-emoji">🍺</p>
|
||||
<p>Noch nichts gebucht.</p>
|
||||
{% if phase == "booking" %}<p class="muted">Tipp dich rein, sobald du was trinkst!</p>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
@@ -4,7 +4,21 @@
|
||||
{% block content %}
|
||||
<h2>Spenden</h2>
|
||||
|
||||
<section class="total-box">
|
||||
<section class="total-box {% if paid and open_balance <= 0 %}total-box-settled{% endif %}">
|
||||
{% if paid and open_balance <= 0 %}
|
||||
<span class="total-settled">Bezahlt ✓</span>
|
||||
<div class="total-breakdown">
|
||||
<span>Drinks {{ total|floatformat:2 }} €</span>
|
||||
<span>·</span>
|
||||
<span>Bezahlt {{ paid|floatformat:2 }} €</span>
|
||||
<span>·</span>
|
||||
{% if open_balance < 0 %}
|
||||
<span>Spende {{ open_balance|floatformat:2|slice:"1:" }} €</span>
|
||||
{% else %}
|
||||
<span>Genau bezahlt</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="total-label">Deine Rechnung</span>
|
||||
<span class="total-value">{{ total|floatformat:2 }} €</span>
|
||||
{% if paid %}
|
||||
@@ -13,6 +27,7 @@
|
||||
{% endif %}
|
||||
<span class="total-label">Offen (Drinks)</span>
|
||||
<span class="total-value">{{ open_balance|floatformat:2 }} €</span>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<p class="intro">
|
||||
@@ -24,7 +39,7 @@
|
||||
<p class="intro">
|
||||
Du darfst gerne <strong>weniger</strong> geben, wenn das gerade besser
|
||||
passt – kein Problem. Und wenn du <strong>mehr</strong> geben kannst,
|
||||
hilft uns das sehr, die Kosten für Bands, Toiletten und Technik zu
|
||||
hilft uns das sehr, die Kosten für Bands, Toiletten usw. zu
|
||||
decken. So oder so: <strong>danke, dass du da bist!</strong>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -4,7 +4,21 @@
|
||||
<p class="muted">Crew-Ansicht</p>
|
||||
<h2>Zahlung für <span class="staff-target">{{ tab_user.username }}</span></h2>
|
||||
|
||||
<section class="total-box">
|
||||
<section class="total-box {% if paid and open_balance <= 0 %}total-box-settled{% endif %}">
|
||||
{% if paid and open_balance <= 0 %}
|
||||
<span class="total-settled">Bezahlt ✓</span>
|
||||
<div class="total-breakdown">
|
||||
<span>Drinks {{ total|floatformat:2 }} €</span>
|
||||
<span>·</span>
|
||||
<span>Bezahlt {{ paid|floatformat:2 }} €</span>
|
||||
<span>·</span>
|
||||
{% if open_balance < 0 %}
|
||||
<span>Spende {{ open_balance|floatformat:2|slice:"1:" }} €</span>
|
||||
{% else %}
|
||||
<span>Genau bezahlt</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="total-label">Rechnung</span>
|
||||
<span class="total-value">{{ total|floatformat:2 }} €</span>
|
||||
{% if paid %}
|
||||
@@ -13,6 +27,7 @@
|
||||
{% endif %}
|
||||
<span class="total-label">Offen</span>
|
||||
<span class="total-value">{{ open_balance|floatformat:2 }} €</span>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% if error %}
|
||||
@@ -24,7 +39,7 @@
|
||||
<label>
|
||||
Betrag (€)
|
||||
<input type="number" name="amount" step="0.01" min="0.01"
|
||||
value="{{ open_balance|floatformat:2 }}" required />
|
||||
value="{% if open_balance > 0 %}{{ open_balance|floatformat:2 }}{% else %}0.00{% endif %}" required />
|
||||
</label>
|
||||
<label>
|
||||
Methode
|
||||
|
||||
@@ -19,7 +19,21 @@
|
||||
{% if is_anonymous_target %}
|
||||
<p class="muted">Anonyme Buchungen werden automatisch als bar bezahlt eingetragen.</p>
|
||||
{% else %}
|
||||
<section class="total-box">
|
||||
<section class="total-box {% if paid and open_balance <= 0 %}total-box-settled{% endif %}">
|
||||
{% if paid and open_balance <= 0 %}
|
||||
<span class="total-settled">Bezahlt ✓</span>
|
||||
<div class="total-breakdown">
|
||||
<span>Drinks {{ total|floatformat:2 }} €</span>
|
||||
<span>·</span>
|
||||
<span>Bezahlt {{ paid|floatformat:2 }} €</span>
|
||||
<span>·</span>
|
||||
{% if open_balance < 0 %}
|
||||
<span>Spende {{ open_balance|floatformat:2|slice:"1:" }} €</span>
|
||||
{% else %}
|
||||
<span>Genau bezahlt</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<span class="total-label">Rechnung</span>
|
||||
<span class="total-value">{{ total|floatformat:2 }} €</span>
|
||||
{% if paid %}
|
||||
@@ -28,11 +42,10 @@
|
||||
<span class="total-label">Offen</span>
|
||||
<span class="total-value">{{ open_balance|floatformat:2 }} €</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% if open_balance > 0 %}
|
||||
<p><a href="{% url 'suff:staff_pay' tab_user.username %}" class="btn-primary">Zahlung eintragen</a></p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if phase == "booking" %}
|
||||
@@ -41,8 +54,12 @@
|
||||
<form method="post" action="{% url 'suff:staff_book' tab_user.username %}">
|
||||
{% csrf_token %}
|
||||
<label class="for-free-toggle">
|
||||
<input type="checkbox" name="for_free" value="1" />
|
||||
<span>Gratis (z.b. Artists am Spieltag)</span>
|
||||
<input type="radio" name="booking_mode" value="for_free" />
|
||||
<span>Gratis (z.B. Artists am Spieltag)</span>
|
||||
</label>
|
||||
<label class="for-free-toggle">
|
||||
<input type="radio" name="booking_mode" value="cash_paid" />
|
||||
<span>Direkt bar bezahlt</span>
|
||||
</label>
|
||||
<div class="drink-grid">
|
||||
{% for drink in drinks %}
|
||||
|
||||
@@ -20,7 +20,10 @@ Username = `slugify(input)`. POST name:
|
||||
## Booking
|
||||
|
||||
- `/suff/me/` shows: greeting, total/paid/open balance, drink grid, day-grouped history.
|
||||
- `+1` POST creates `Consumption(amount=1, day=current_weekday, for_free=False)`.
|
||||
- Single form wraps two radio buttons (`booking_mode`) + all drink buttons (`name="drink_id"`).
|
||||
- `booking_mode` values: `normal` (default, no radio selected), `for_free`, `cash_paid`.
|
||||
- `+1` POST creates `Consumption(amount=1, day=current_weekday, for_free=...)`.
|
||||
- `cash_paid` booking auto-creates matching `UserPayment(method=cash)` — same as anonymous walk-ins.
|
||||
- Trash icon per row → `confirm_delete.html` → POST deletes own consumption (booking phase only).
|
||||
- History grouped by festival day, newest-first per day.
|
||||
|
||||
@@ -29,6 +32,7 @@ Username = `slugify(input)`. POST name:
|
||||
- `/suff/pay/` — user enters amount + method (cash/paypal/bank/other) + optional note. Creates `UserPayment`. Pre-fills with current `open_balance`.
|
||||
- Method choices: `UserPayment.Method`.
|
||||
- Open balance = sum(Consumption.price where !for_free) − sum(UserPayment.amount).
|
||||
- Balance panel shows settled state (green "Bezahlt ✓" + breakdown) when `open_balance <= 0`; amber with "Offen X €" otherwise. Applied on `/suff/me/`, `/suff/pay/`, `/suff/staff/u/<name>/`, `/suff/staff/u/<name>/pay/`.
|
||||
|
||||
## Crew (`is_staff`)
|
||||
|
||||
@@ -36,11 +40,13 @@ Separate page tree under `/suff/staff/`:
|
||||
|
||||
- `/suff/staff/` — alphabetical user list, anonymous gast on top, "Neuen Benutzer anlegen" link.
|
||||
- `/suff/staff/new/` — register user. Name required, PIN optional 3 digits.
|
||||
- `/suff/staff/u/<name>/` — book/pay/delete for that user. Mirrors `me.html`.
|
||||
- `/suff/staff/u/<name>/` — book/pay/delete for that user. Mirrors `me.html`. "Zahlung eintragen" link always visible.
|
||||
- `/suff/staff/u/<name>/pin/` — overwrite PIN (3 digits required, no clear).
|
||||
- `/suff/staff/u/<name>/pay/` — record payment for that user.
|
||||
- `/suff/staff/u/<name>/pay/` — record payment for that user. Amount pre-fills with `max(open_balance, 0)`.
|
||||
- `/suff/staff/u/<name>/book/<id>/delete/` — delete consumption (and matching auto-payment if anon).
|
||||
|
||||
Staff booking form has same `booking_mode` radios (normal / for_free / cash_paid) as user view.
|
||||
|
||||
Target username highlighted via `.staff-target` (cyan pill) on every crew page.
|
||||
|
||||
## Anonymous walk-ins
|
||||
@@ -60,6 +66,7 @@ Target username highlighted via `.staff-target` (cyan pill) on every crew page.
|
||||
## Dashboard
|
||||
|
||||
- `/suff/dashboard/` (staff only). Donations vs. expenses with progress bar, drink inventory rows, refinance %, per-user open balances, top spender, top drink, busiest day, top drinker per day.
|
||||
- Finance section includes "Kasse (bar)" — sum of all `UserPayment(method=cash)` — for cross-checking real cash in the box.
|
||||
|
||||
## Drink categories
|
||||
|
||||
@@ -82,24 +89,10 @@ Mobile-first dark theme. `#161616` bg, `#EE9933`/`#FFCC77` amber, `#885522` brow
|
||||
|
||||
## Open ideas / next session
|
||||
|
||||
### Entry fee handling
|
||||
|
||||
Festival entry is flexible (10–30 €, sometimes paid as overpayment on drink tab). Options brainstormed:
|
||||
|
||||
- **A. Implicit overpayment** — pre-fill pay form with `open_balance + 20`, treat anything above drinks as entry/donation. Zero schema change.
|
||||
- **B. Quick-pick suggestions** — pay page offers buttons: `Nur Getränke (X €)`, `+10`, `+20`, `+30`, plus free-form. Recommended.
|
||||
- **C. Explicit split** — separate entry field/checkbox. More UI, more accurate stats (`entry = paid − consumed`), more friction.
|
||||
|
||||
Lean B. Stats can later derive entry as `paid − consumed_drinks_price` per user.
|
||||
|
||||
### Pay-on-the-spot / quick-pay-cash
|
||||
|
||||
Single button on `me` (and crew_user) page: "Offenen Betrag bar bezahlen" → creates `UserPayment(method=cash, amount=open_balance)`. Lets bar crew clear tab in one tap when guest pays cash directly. (Skipped for now, keep in mind.)
|
||||
|
||||
### Round to nearest 10 €
|
||||
|
||||
When pre-filling pay amount or showing quick-pick buttons, optionally round up to nearest multiple of 10 €. E.g. drinks 26.50 € → suggest 30, 40, 50. Easier cash handling, implicit donation/entry.
|
||||
|
||||
### Prepay vs. pay-at-end
|
||||
|
||||
Currently a single open balance. Could surface "Prepay 50 €" as a flow vs. "Pay at the end" — same data model, different framing. Maybe a "Vorkasse" preset on pay page.
|
||||
|
||||
Reference in New Issue
Block a user