Skip to content

Zeug - #68

Open
afffe18 wants to merge 22 commits into
13Bytes:mainfrom
afffe18:feature/split-multiple-users
Open

Zeug#68
afffe18 wants to merge 22 commits into
13Bytes:mainfrom
afffe18:feature/split-multiple-users

Conversation

@afffe18

@afffe18 afffe18 commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Branch feature/split-multiple-users, 18 commits against main (54 files, +2914 / −993).

Request money: split between several users

  • Request money from several users at once and split it by factor, percentage or fixed amount; unset shares are calculated automatically and can be reset per person; optionally include your own share
  • The simple form stays the default, splitting is enabled via the "Single / Group" toggle; negative automatic shares are marked with a warning icon

Amount fields: dot and comma

  • New DecimalInput component replaces type="number": accepts both dot and comma as decimal separator; at most 4 decimal places are shown

Keycloak / LDAP

  • Old LDAP usernames ("firstname.lastname") are replaced with the Keycloak display name ("Firstname Lastname") on Keycloak login and once at server start; other names are left unchanged
  • Admin rights (LABEATS_ADMIN) are synced with the user's groups on every Keycloak login; without a groups claim the stored value is kept

Sign-out after update

  • Every build gets a SESSION_BUILD_ID; sessions from older builds are rejected, so every container update signs all users out once. Restarting the same image signs nobody out.

Group orders

  • Individual items are only reserved (transaction type 4, "reserved purchase") and charged when the order is settled; they can be cancelled until the order closes. Reservations count towards the balance check for users who are not creditworthy.
  • Shopper: "I'm going shopping" / sign off, set automatically when the order closes or is settled or by a new button; only one Person can be the shopper but you can take it from some else.
  • Category filter for the order window (admins and shopper)
  • Reworked settlement: adjustable prices for individual items, "Total / group items only" toggle, separate button; admins can always revert a settlement, the person who settled it for 2 h
  • Recurring orders: "hours before" (ordersStartsAt, default 72 h)

Deposit

  • Items can have a deposit amount; the buy tab shows a separate "+ deposit" button for it, booked as a regular purchase without category markups
  • Deposit as a column in the inventory; inventory sortable by name, price, deposit and account

Statistics

  • Profile: tiles for ranking, total spending, procurement, weekly/monthly average, deposit and top 3 items
  • New admin page "Statistics" with totals only: revenue, active users, deposit, outstanding balances, overdrafts, best-selling items

Notification dot

  • Red dot on the account tab (on mobile on the menu icon) when someone has collected money from you; it disappears once the account page has been opened

Minor UI fixes

  • Top-up button in the header uses the nav color,
  • filter buttons in the buy tab are consistent and persistent,
  • the creditworthy checkbox in the user popup resets when switching users
  • changed & more consistant grey shades

Cleanup and booking bugs

  • Security: removed user.getCtx, which returned the LDAP bind password without login
  • Undo now also books the item price/deposit back from the clearing account
  • Undo, transfers, closing/reverting group orders are protected against double execution
  • Fees without a target clearing account are rejected instead of being lost; no negative prices when closing a group order
  • LDAP user data no longer logged, cron errors are logged; dead code and debug logs removed; npm audit fix
  • README: instructions for local development

Deployment notes

  • 4 migrations, applied automatically via prisma migrate deploy in the entrypoint:
    • add_orders_starts_at_to_template: new column, default 72
    • add_item_deposit: new column, default 0
    • add_grouporder_shopper_and_filter: rebuilds the GroupOrder table and backfills the shopper of closed orders with the recipient of the settlement.
    • add_user_retractions_seen_at: marks all existing money collections as seen so nobody sees a stale red dot after the update
  • All users are signed out once after deployment (new build ID).
  • src/instrumentation.ts renames old LDAP usernames once at server start. This is transitional code and can be removed (together with syncKeycloakNames in src/server/syncKeycloakNames.ts) once all accounts have been migrated. Renaming on login is not affected.
  • Individual items that were still charged directly at purchase before the update are booked back from the clearing account when a settlement is reverted, so old orders remain revertible.
  • The entrypoint script does not abort if a migration fails: the app still starts, but with an incompletely migrated schema. Check the output of prisma migrate deploy in the container log after deploying.

Summary by CodeRabbit

  • New Features
    • Added deposit purchases, with deposits shown in item forms and inventory.
    • Group orders now support shopper sign-up, category filters, quantity selection, individual purchases, and more detailed cost splitting.
    • Added group money requests with factor-, percentage-, or amount-based splits.
    • Added admin statistics and expanded personal purchase statistics, including rankings and averages.
    • Added searchable, sortable user balances and sortable inventory columns.
    • Added notices for unseen money retractions.
  • Improvements
    • Decimal fields now accept either a comma or a period as the decimal separator.
    • Group orders can be reverted by administrators or by the person who closed them within two hours.
  • Documentation
    • Updated setup instructions and clarified database path handling.

afffe18 and others added 19 commits October 17, 2026 21:24
- Request money from multiple users at once (atomic, one transaction per user)
- Split by factors, percentages or absolute amounts; unset shares are
  calculated automatically and can be reset per person
- Optionally include yourself in the split (your share isn't requested)
- Equal card heights on the split page

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWPHvWv8YsGKLcJ2gfSart
…cksichtigen

Neues Feld ordersStartsAt (Standard 72h) an der Vorlage inkl. Migration.
Der Cron legt den Gruppenkauf jetzt an, sobald die nächste Deadline
höchstens ordersStartsAt Stunden entfernt ist, statt fest 3 Tage vorher.
Im Formular wird der Wert als Zahl übergeben und die Wochentage
beginnen mit Montag.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QpZB9UHK22rJeuAYwvXVoe
Nutzer aus der LDAP-Zeit heißen noch "vorname.nachname", weil NextAuth
den Namen bestehender Nutzer nie aktualisiert. Beim Keycloak-Login und
einmal beim Serverstart (instrumentation.ts) wird ein solcher Name durch
den Keycloak-Anzeigenamen ersetzt. Andere Namen bleiben unverändert.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HpKohmJY768aFwQmP9xahY
NextAuth nutzt das Keycloak-Profil nur beim Anlegen eines Nutzers, daher
wurde LABEATS_ADMIN bei späteren Logins weder vergeben noch entzogen. Der
jwt-Callback gleicht is_admin jetzt bei jedem Keycloak-Login mit den
Gruppen ab; ohne groups-Claim bleibt der gespeicherte Wert unverändert.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015C7vjctgvrGP2e4jWisqEg
…chalter

Die normale Ansicht ist wieder das einfache Formular (Betrag, ein Nutzer,
Anmerkung). Über den Schalter "Einzeln / Gruppe" neben der Überschrift
lässt sich die Aufteilung auf mehrere Nutzer einblenden. Betrag und
Anmerkung bleiben beim Umschalten erhalten.

Gruppenansicht aufgeräumt: Aufteilungsart erst sichtbar, wenn Nutzer
gewählt sind; Anteile werden beim Moduswechsel auch bei negativen
automatischen Anteilen umgerechnet; negative automatische Anteile sind mit
einem Warnsymbol markiert; keine negativen Eingaben, Prozent höchstens
100; automatische Werte als Platzhalter mit max. 4 Nachkommastellen; feste
Plätze für Zurücksetzen- und Entfernen-Knopf.

Die Split-Seite nimmt jetzt immer die volle Breite ein, damit sie beim
Umschalten nicht springt.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011uopa4yzVNAMzdHG5cNhdv
type="number" akzeptiert nur den Dezimaltrenner der Browsersprache und
leert das Feld beim anderen. Neue Komponente DecimalInput (Textfeld mit
Zifferntastatur): nimmt "," und "." an, ignoriert Buchstaben und Werte
außerhalb von min/max und zeigt höchstens 4 Nachkommastellen.

Eingesetzt bei Geld senden/einfordern (inkl. Anteile), Verrechnungskonten,
Gruppenbestellung abrechnen, Artikelpreis und Kategorie-Aufpreis (über
react-hook-form Controller).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011uopa4yzVNAMzdHG5cNhdv
Einzelartikel in einer offenen Gruppenbestellung werden nur noch
vorgemerkt (Transaktionstyp 4) und lassen sich bis zum Schließen der
Bestellung stornieren. Beim Abrechnen zahlt der Besteller Preis plus
Kategorie-Aufschlag, der Artikelpreis geht an den Empfänger der
Abrechnung. Abrechnung stornieren erstattet die Einzelartikel und merkt
sie wieder vor; ältere, schon beim Kauf gebuchte Käufe werden dabei vom
Verrechnungskonto zurückgeholt.

Offene Vormerkungen zählen beim Kontostand-Check für Käufe,
Überweisungen und Wünsche mit. Die gewählte Menge im Kaufdialog wird
jetzt auch bei Gruppenbestellungen übernommen, und "Gezahlt von" steht
auf einer Höhe mit dem Abrechnen-Knopf.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VymTx4jHy14YS6vBSrUXp
Die Session ist ein JWT-Cookie und lässt sich nur über eine Prüfung
ungültig machen. next.config.mjs schreibt beim Build einen Zeitstempel
als SESSION_BUILD_ID in den Code, beim Login landet er als build_id im
Token. Passt die Kennung nicht zum laufenden Build, wirft der
jwt-Callback und NextAuth löscht das Cookie; der Proxy lässt solche
Token ebenfalls nicht mehr durch. Ein Neustart mit demselben Image
meldet niemanden ab.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RtF3pTQLHfrUvE1MY8YWHv
Ein Artikel kann optional einen Pfandbetrag haben (neues Feld deposit,
im Artikel-Formular über das DecimalInput einstellbar). Auf der Karte im
Kaufen-Tab steht dann rechts neben dem Preis ein "+ Pfand"-Knopf, der
denselben Bestätigungsdialog mit Mengenauswahl wie der normale Kauf
öffnet. Jedes Stück wird als eigener normaler Kauf (Typ 0) nur über den
Pfandbetrag gebucht, ohne Kategorie-Aufschläge, auf das
Verrechnungskonto des Artikels. Der Kontostand-Check berücksichtigt
vorgemerkte Käufe, stornieren geht wie bei normalen Käufen. In der
Inventarübersicht der Admins steht der Pfand als eigene Spalte.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LnGACsiy32iHUzXayUeCwW
"Alle auswählen" und "Alle abwählen" haben jetzt beide den Rahmen-Stil.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LnGACsiy32iHUzXayUeCwW
Ein Klick auf Name, Preis, Pfand oder Konto sortiert aufsteigend, ein
weiterer Klick absteigend; ein Pfeil zeigt die Richtung.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LnGACsiy32iHUzXayUeCwW
…chnung

- Einkäufer: "Ich gehe einkaufen" / austragen solange offen, wird beim
  Schließen automatisch gesetzt und bei der Abrechnung auf die zahlende
  Person gesetzt; Anzeige unter dem Titel (auch in der History)
- Kategorie-Filter: Admins und Einkäufer wählen eine Kategorie, das
  Bestellfenster zeigt nur passende Artikel
- Abrechnung: vorgemerkte Einzel-Artikel mit anpassbarem Preis,
  Kosten-Toggle "Gesamt / nur Gruppen-Artikel" (Gesamt zieht die
  Einzel-Artikel ab), Abrechnung hinter eigenem Knopf
- Abrechnung stornieren: Admins immer, die abrechnende Person 2h lang
- einheitliche Darstellung (Trennlinien Gruppen-/Einzel-Artikel, Titel
  und Datum, zentrierte Karten, Icons)
- Gruppen-Artikel können nach dem Schließen nicht mehr bestellt werden

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017VymTx4jHy14YS6vBSrUXp
Im Profil stehen jetzt alle Kennzahlen als einheitliche Kacheln:
Ranking, Gesamtausgaben, Beschaffung, Wochen- und Monatsschnitt,
bezahlter Pfand und die Top 3 Lieblingsartikel mit Anzahl und
Durchschnitt pro Woche.

Neue Admin-Seite "Statistik" (nur adminProcedure, nur Summen, nichts
pro Person): Umsatz 7/30 Tage mit Durchschnitt pro Woche und Monat,
aktive Nutzer, Pfand der letzten 30 Tage, offene Guthaben, Summe der
überzogenen Konten sowie die meistverkauften Artikel im Kaufen-Tab und
in Gruppenbestellungen.

Gezählt werden nur abgebuchte Käufe (Typ 0, nicht storniert);
vorgemerkte Gruppenkäufe zählen erst nach dem Abschluss. Pfand zählt
bei den persönlichen Ausgaben mit, beim Admin-Umsatz nicht.
Durchschnitte laufen über die letzten 12 Wochen, ab dem späteren
Zeitpunkt aus erstem Kauf (bzw. erstem Verkauf des Artikels) und
Fensterbeginn.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K4Xj8GGcCUjPtYNFPFf7y1
Wenn jemand anderes Geld von einer Person einzieht, zeigt der Header am
Konto-Tab (mobil am Menü-Symbol) einen roten Punkt. Er verschwindet, sobald
die Konto-Seite die Einzüge angezeigt hat. Der Gesehen-Stand liegt pro
Nutzer in der Datenbank (User.retractionsSeenAt), die Migration markiert
bestehende Einzüge als gesehen.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S2FVn2YVSYKZece2KDGvaz
- user.getCtx entfernt: gab ohne Login das LDAP-Bind-Passwort zurück
- Undo bucht Artikelpreis/Pfand wieder vom Verrechnungskonto ab
- Undo, Überweisen, Gruppenbestellung schließen/zurücknehmen gegen
  Doppelausführung abgesichert
- Gebühren ohne Ziel-Verrechnungskonto werden abgelehnt statt verloren
- Rücknahme von Gruppen-Artikeln nutzt die Kategorien der Buchung
- Keine negativen Preise beim Gruppen-Abschluss, ganzzahlige Kaufmenge
- sendMoneyProcurement als Korrektur-Werkzeug kommentiert
- LDAP-Nutzerdaten nicht mehr ins Log, Cron-Fehler werden geloggt
- Ungenutzte Funktionen, Debug-Logs und doppelten Endpunkt entfernt
- getStats zählt per count(), eslint ignoriert .next/
- npm audit fix

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHpZzB3cmDemfMC9RyptpY
@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ba7d0833-2e03-43bb-a7cf-0d25139ff8d8
Commits

Reviewing files that changed from the base of the PR and between bb83db7 and 522b76a.

Files ignored due to path filters: 1
  • package-lock.json is excluded by !**/package-lock.json
Files selected for processing: 61
  • .env.example
  • README.md
  • eslint.config.mts
  • next.config.mjs
  • prisma/migrations/20261017200000_add_orders_starts_at_to_template/migration.sql
  • prisma/migrations/20261018121500_add_item_deposit/migration.sql
  • prisma/migrations/20261018140000_add_grouporder_shopper_and_filter/migration.sql
  • prisma/migrations/20261018160000_add_user_retractions_seen_at/migration.sql
  • prisma/schema.prisma
  • scripts/screenshots/seed.ts
  • src/components/FormElements/GroupOrderDetailView.tsx
  • src/components/FormElements/GroupOrderSplit.tsx
  • src/components/Forms/CategoryForm.tsx
  • src/components/Forms/GrouporderTemplateForm.tsx
  • src/components/Forms/ItemForm.tsx
  • src/components/General/BuyItemCard.tsx
  • src/components/General/BuyItemCardBig.tsx
  • src/components/General/DecimalInput.tsx
  • src/components/Layout/Header.tsx
  • src/components/Layout/Layout.tsx
  • src/components/PageComponents/GetMoney.tsx
  • src/components/PageComponents/InventoryOverview.tsx
  • src/components/PageComponents/RankingCard.tsx
  • src/components/PageComponents/SendMoney.tsx
  • src/components/PageComponents/TransactionList.tsx
  • src/components/PageComponents/UserOverview.tsx
  • src/helper/dataProcessing.ts
  • src/helper/generalFunctions.test.ts
  • src/helper/generalFunctions.ts
  • src/helper/globalTypes.ts
  • src/instrumentation.ts
  • src/pages/account.tsx
  • src/pages/admin/clearingAccounts.tsx
  • src/pages/admin/stats.tsx
  • src/pages/all-users.tsx
  • src/pages/api/cron/trigger.ts
  • src/pages/buy.tsx
  • src/pages/grouporders/history.tsx
  • src/pages/grouporders/index.tsx
  • src/pages/me/index.tsx
  • src/pages/split.tsx
  • src/pages/top-up.tsx
  • src/proxy.ts
  • src/server/api/routers/categories.ts
  • src/server/api/routers/groupOrders.ts
  • src/server/api/routers/items.ts
  • src/server/api/routers/transactions.ts
  • src/server/api/routers/user.ts
  • src/server/auth.ts
  • src/server/db.ts
  • src/server/helper/dbCallHelper.ts
  • src/server/helper/statistics.test.ts
  • src/server/helper/statistics.ts
  • src/server/ldap.ts
  • src/server/sessionBuild.test.ts
  • src/server/sessionBuild.ts
  • src/server/syncKeycloakAdmin.test.ts
  • src/server/syncKeycloakAdmin.ts
  • src/server/syncKeycloakNames.test.ts
  • src/server/syncKeycloakNames.ts
  • src/styles/globals.css
+---------------------------------+
| My GPU is bigger than your GPU. |
+---------------------------------+
  \
   \     /)/)
        ( '')
        /  |
      o(___)
Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Kacheln auf Kaufen, Gruppenbestellungen, Konto und Co. nutzen jetzt
denselben Verlauf wie Geld senden/einfordern, mit feinem Rauschen gegen
Banding. Über dem Header zeigt der Overscroll-Bereich die Header-Farbe.
Außerdem kompaktere Such- und Filterleiste auf der Kaufen-Seite.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WSPEf53xnfN1DksTwuXVEM
@13Bytes
13Bytes self-requested a review October 26, 2026 00:48

@13Bytes 13Bytes left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey,
danke für die Erweiterungen!

Ein paar Dinge:

  1. Category fees can disappear before settlement — P1

The new flow delays charging a reserved item until the group order is settled. To calculate its fees later, settlement reads the categories associated with that purchase.

However, the category relationship can point to only one purchase. Buying another item with the same category moves that relationship to the newer purchase.

For example:

  1. Alice reserves an item with a €0.50 category fee.
  2. Bob reserves another item using that category.
  3. The category association moves from Alice’s reservation to Bob’s.
  4. At settlement, Alice’s reservation no longer has the category information needed to charge her fee.
    An ordinary purchase can also displace the association. The result is missing charges and too little money credited to the clearing account. Previously, fees were calculated immediately, before a later purchase could change the relationship.

Suggested fix: save the applicable fees and their destination accounts separately for every reservation, so subsequent purchases cannot change them.

  1. A reservation can arrive after the order has settled — P2

Checking whether an order is open and creating the reservation happen separately. Another request can close and settle the order between those steps.

For example:

  1. Alice clicks to reserve an item; the server checks that the order is open.
  2. The shopper closes and settles the order.
  3. Alice’s request resumes and creates the reservation without checking again.
    That reservation missed settlement, but cancellation is now blocked because the order is closed. Its amount can remain reserved against Alice’s available balance, reducing what she can spend or transfer.

Suggested fix: make checking the order’s state and creating the reservation one coordinated operation that cannot race with closure or settlement.

  1. Editing a category can empty an order’s item list — P2

Editing a category creates a replacement category with a new ID and moves its items to that replacement. An existing group order’s filter still points to the old ID.

For example, an order is filtered to “Drinks.” An admin edits that category. All drinks now belong to its replacement, while the order continues filtering by the old category. Consequently, no items match, and the old category also disappears from the selector.

This disrupts ordering until the shopper or an admin resets the filter; it does not delete the items.

Suggested fix: update affected order filters when replacing a category, or have filters refer to a category identity that remains stable across edits.

Comment thread prisma/schema.prisma
name String
categories Category[]
price Float
deposit Float @default(0) // optional deposit (Pfand) in €, bought separately

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need a dedicated deposit or would a bottle deposit item be good enough?

Comment thread prisma/schema.prisma
revertedById String?
shopperId String?
// only show items of this category when ordering (set by admins or the shopper)
filterCategory Category? @relation("groupOrderFilter", fields: [filterCategoryId], references: [id], onDelete: SetNull)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to add a default to the GroupOrderTemplate as well?

setFilterCategory(groupOrder.id, e.target.value || null)
}
>
<option value="">Alle Kategorien</option>

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image.png

<span className="font-medium">Gesamt von {participants.length} Nutzer(n)</span>
<span className="text-lg font-bold text-warning">{requestedAmount.toFixed(2)}€</span>
</div>
)}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we harmonize/combine this component with the one that handels the group order multi-user money split?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants