Skip to content

Dashboard: sortier- und filterbare Tabellen - #292

Merged
nichtLehdev merged 9 commits into
mainfrom
feat/dashboard-datatable
Sep 10, 2026
Merged

Dashboard: sortier- und filterbare Tabellen#292
nichtLehdev merged 9 commits into
mainfrom
feat/dashboard-datatable

Conversation

@nichtLehdev

Copy link
Copy Markdown
Owner

Alle Listen des Dashboards nutzen jetzt dieselbe Tabelle mit Sortierung und
Filtern je Spalte. Dazu kommen zwei kleinere Wege rund um die Rechnungen und
eine wahlweise Tabellenansicht für die drei Inhaltsübersichten.

Sortier- und filterbare Tabellen

Bisher hat jede Liste ihre Tabelle selbst gebaut — mit einer Sortier-Auswahl,
einem Suchfeld oder gar nichts. Die neue DataTable (TanStack Table v9) gibt
allen dasselbe Verhalten:

  • Sortierung je Spalte, Umschalt+Klick für mehrere Spalten
  • Trichter je Spalte mit Text-, Set-, Zahlen- und Datumsfilter
  • Set-Filter mit Faceting: angeboten werden nur Werte, die nach den übrigen
    Filtern noch vorkommen — eine Auswahl kann nie in eine leere Liste führen
  • Suche über alle Spalten, Menü zum Ein- und Ausblenden von Spalten, Pager

Umgestellt sind alle rund 20 Tabellen des Dashboards.

Client- oder serverseitig. Wo der Bestand überschaubar ist, holt die Seite
ihn am Stück und sortiert und filtert im Browser. Audit-Log, Benutzer,
Anmeldungen, Newsletter-Abonnenten und das Rechnungsarchiv bleiben serverseitig
geblättert und reichen Sortierung, Filter und Suche an die Abfrage weiter — im
Browser zu sortieren würde dort nur die gerade geladene Seite betreffen. Dort
trägt nur die Spalte einen Schalter, die der Server auch bedienen kann.

Die betroffenen Abfragen nehmen dafür sortBy/sortOrder entgegen und die
bisherigen Einzelwert-Filter zusätzlich als Liste. Jede Sortierung außer der
nach createdAt bekommt createdAt als zweites Kriterium, damit beim Blättern
keine Zeile doppelt auftaucht oder ausfällt.

Vom Rechnungsarchiv in die Kursrechnungen

Der Kurstitel im Archiv war bewusst kein Link: die Rechnungsliste eines Kurses
steht nur der Kursverwaltung offen, das Archiv aber jeder Person mit
invoices.view. manageableCourseIds löst die Abrechnungsberechtigung jetzt
für eine ganze Kursliste auf einmal auf. Verlinkt werden genau die Titel, hinter
denen auch eine zugängliche Seite liegt; ist genau ein Kurs gefiltert, führt
zusätzlich ein Knopf im Seitenkopf dorthin.

Rechnungen-Knopf auf allen Kursseiten

Der Weg in die Rechnungen lag an zwei Stellen und an beiden versteckt — im
Teilnehmer-Reiter der Kursübersicht und unten im Export-Menü der
Teilnehmerliste. Von Mail-Seite, Kursbearbeitung und einzelner Anmeldung führte
gar kein Weg dorthin. Ein gemeinsamer Knopf sitzt jetzt im Kopf jeder
Kursseite; ob er erscheint, entscheidet der Server über
canManageCourseInvoices, damit Knopf und Guard nicht auseinanderdriften. Die
beiden bisherigen Einsprünge entfallen.

Tabellenansicht für Termine, Kurse und Beiträge

Ein Umschalter neben "Auswählen" bietet Karten oder Tabelle an und merkt sich
die Wahl je Liste; Karten bleiben die Vorgabe. In der Tabelle wandern Status,
Kategorie und Sortierung in die Spaltenköpfe, in der Kartenansicht bleiben sie
in der Leiste. Neu für beide Ansichten ist ein Bezirksfilter — er tritt neben
die bestehende Bezirksbindung, nicht an ihre Stelle.

Bewusst gelassen

  • Status und Kategorie nehmen serverseitig nur einen Wert je Abfrage entgegen;
    eine Mehrfachauswahl im Spaltenfilter fällt dort auf den ersten Wert zurück.
    Der Bezirksfilter kann mehrere.
  • Die kleine Teilnehmertabelle innerhalb jeder Anmeldungskarte bleibt eine
    schlichte Tabelle: ein bis fünf Zeilen Kartendetail, kein Datengitter.

Geprüft

tsc --noEmit, eslint ., 556 Tests und next build laufen durch. Alle
umgestellten Seiten in der laufenden Anwendung durchgeklickt, Karten- und
Tabellenansicht, Desktop und 375 px.

Jede Dashboard-Liste hat ihre Tabelle bisher selbst gebaut, mit jeweils
eigener (oder gar keiner) Sortierung. Diese gemeinsame Komponente auf
Basis von TanStack Table v9 gibt allen dasselbe Verhalten: Sortierung je
Spalte (Umschalt+Klick für mehrere), ein Trichter je Spalte mit Text-,
Set-, Zahlen- und Datumsfilter, eine spaltenübergreifende Suche, ein
Menü zum Ein- und Ausblenden von Spalten und einen Seitenumbruch.

Der Set-Filter arbeitet mit Faceting: er bietet nur Werte an, die nach
den übrigen Spaltenfiltern noch vorkommen, sodass keine Auswahl in eine
leere Trefferliste führt. Die Menüs hängen in einem Portal, weil die
Tabelle in einem `overflow-x-auto` scrollt und sie sonst abgeschnitten
würden.

Listen, die serverseitig geblättert werden, können Sortierung, Filter,
Suche und Blättern über die `manual*`-Schalter selbst übernehmen — im
Browser zu sortieren würde dort nur die gerade geladene Seite betreffen.
Audit-Log, Benutzer, Newsletter-Abonnenten und Anmeldungen werden
serverseitig geblättert. Damit die Spaltenköpfe der neuen Tabellen dort
über den ganzen Bestand sortieren und filtern statt nur über die gerade
geladene Seite, nehmen die Abfragen jetzt `sortBy`/`sortOrder` entgegen
und die bisherigen Einzelwert-Filter zusätzlich als Liste.

Jede Sortierung außer der nach `createdAt` bekommt `createdAt` als
zweites Kriterium: bei gleichen Werten ist die Reihenfolge sonst nicht
festgelegt und dieselbe Zeile kann beim Blättern auf zwei Seiten
auftauchen oder ganz ausfallen.

Die Obergrenze für `limit` steigt mit, weil die Tabellen 50 bis 250
Zeilen je Seite anbieten. Die Standortliste holt ihren Bestand künftig
am Stück; sie ist eine überschaubare Stammdatei, und nur so greifen
Sortierung und Spaltenfilter über alle Zeilen.
Alle Listen des Dashboards nutzen jetzt dieselbe Tabelle. Damit lässt
sich überall nach jeder Spalte sortieren und filtern, statt wie bisher
je nach Seite mit einer Sortier-Auswahlliste, einem Suchfeld oder gar
nichts vorliebzunehmen.

Wo der Bestand überschaubar ist, holt die Seite ihn am Stück und
sortiert und filtert im Browser. Audit-Log, Benutzer, Anmeldungen und
Newsletter-Abonnenten bleiben serverseitig geblättert und reichen
Sortierung, Filter und Suche an die Abfrage weiter; dort tragen nur die
Spalten einen Schalter, die der Server auch bedienen kann — ein
Bedienelement, das nichts tut, ist schlimmer als keines.

Die Listen mit gespeicherter Reihenfolge (Team, Vorstand, Posaunenrat,
Posaunenwarte, Förderverein) bekommen eine eigene Spalte dafür und
sortieren zunächst danach. Die Hoch-/Runter-Pfeile verschieben weiterhin
die gespeicherte Position, egal wonach die Ansicht gerade sortiert ist.

Das Audit-Log behält seine gestapelte Kartenansicht auf schmalen
Fenstern: fünf Spalten mit einer Detailspalte erreichen ein Telefon
sonst nur über waagerechtes Scrollen.
Das Archiv listet Rechnungen vieler Kurse nebeneinander, bot aber keinen
Weg in die Rechnungsliste des jeweiligen Kurses. Der Kurstitel war
bewusst kein Link, weil diese Seite nur Kursverwaltungen offensteht,
das Archiv aber jeder Person mit `invoices.view`.

`manageableCourseIds` löst die Abrechnungsberechtigung jetzt für eine
ganze Kursliste auf einmal auf, statt für jede Zeile einzeln. Damit kann
das Archiv genau die Titel verlinken, hinter denen auch wirklich eine
zugängliche Seite liegt; die übrigen bleiben Text. Ist genau ein Kurs
gefiltert, führt zusätzlich ein Knopf im Seitenkopf dorthin.

Die Filterleiste des Archivs wandert in die Spaltenköpfe: Status, Kurs
und Jahr sind Set-Filter, die Suche und die Sortierung laufen weiter
über die Abfrage. Die Rechnungsliste eines Kurses wird von einer
Aufzählung zur Tabelle und zeigt die Teilnehmer:innen mit an.
Der Weg in die Rechnungen eines Kurses lag bisher an zwei Stellen und an
beiden versteckt: einmal nur im Teilnehmer-Reiter der Kursübersicht,
einmal unten im Export-Menü der Teilnehmerliste. Von der Mail-Seite, der
Kursbearbeitung und einer einzelnen Anmeldung führte gar kein Weg
dorthin.

Ein gemeinsamer Knopf sitzt jetzt im Kopf jeder Kursseite. Ob er
erscheint, entscheidet der Server über `canManageCourseInvoices` — nicht
die einzelne Seite, sonst driften Knopf und Guard auseinander und es
entsteht entweder ein Knopf, der 403 wirft, oder eine Berechtigung ohne
Knopf. Die beiden bisherigen Einsprünge entfallen dafür.

Er erscheint auch, wenn die Rechnungsstellung für den Kurs nicht
freigeschaltet ist: die Rechnungsseite zeigt dann die bereits
bestehenden Rechnungen samt Hinweisbanner, und genau dorthin will man in
dem Fall. Das Formular für eine neue Anmeldung bleibt außen vor — ein
Absprung mitten im Ausfüllen kostet die Eingaben.

Die Teilnehmer-Ansicht wird dabei zur Tabelle: eine Zeile je
Teilnehmer:in, mit Sortierung und Spaltenfiltern auch über die
kursspezifischen Zusatzfelder.
Termin-, Kurs- und Beitragskarte hielten jeweils eine eigene, identische
Tabelle aus Statusbeschriftung und Farbe. Sie liegt jetzt einmal in
`content-status` und wird von allen dreien genutzt, damit derselbe
Status nicht je nach Ansicht anders heißt.
Die drei Übersichten zeigten ihre Einträge nur als Kartenraster. Wer
viele Einträge pflegt, will sie aber vergleichen und nicht durchblättern.
Ein Umschalter neben "Auswählen" bietet jetzt beides an und merkt sich
die Wahl je Liste; die Kartenansicht bleibt die Vorgabe.

In der Tabelle wandern Status, Kategorie und Sortierung in die
Spaltenköpfe, in der Kartenansicht bleiben sie in der Leiste — beides
gleichzeitig wären zwei Schalter für dieselbe Sache. Die Zeitraumwahl
gilt in beiden Ansichten, weil sie kein Spaltenmerkmal ist. Neu für
beide Ansichten ist ein Bezirksfilter; er kommt als Liste an den Server
und erlaubt in der Tabelle mehrere Bezirke auf einmal. Die Suche der
Tabelle geht ebenfalls an die Abfrage und greift über alle Seiten.

Der Bezirksfilter tritt neben die bestehende Bezirksbindung, nicht an
ihre Stelle: wer nur den eigenen Bezirk sehen darf, filtert damit
innerhalb dieser Auswahl.

Die Sortierung ist der Zustand der Liste, nicht mehr eine Ableitung aus
Spalte und Richtung. Nur so kann der dritte Klick auf einen Spaltenkopf
die Sortierung wirklich aufheben und zur Standardordnung zurückkehren —
vorher verwarf der Aufrufer die leere Sortierung und die Spalte hing auf
"absteigend" fest.

Die Filterleiste bekommt dabei ihre eigene Zeile unter Zählung und
Umschalter. Die Auswahllisten haben feste Breiten und drängelten sich in
einer gemeinsamen Zeile bei mittleren Fenstern gegenseitig weg.
Die Karten haben den Rohwert aus der Datenbank durch
`capitalizeFirstLetter` geschickt. Für die meisten Werte geht das gut,
für `OTHER` nicht: die Karte schrieb "Other", während der Filter daneben
dieselbe Kursart "Andere" nennt.

Die Zuordnung von Klartext zu Datenbankwert stand ohnehin schon in der
Filterleiste. Sie liegt jetzt in `termine-labels` und wird in beide
Richtungen aus derselben Tabelle abgeleitet, damit Filter und Liste nicht
wieder auseinanderlaufen.
Jeder Monat saß in einer Box mit grauer Kopfzeile, und darin standen
Karten — eine Verschachtelung, die es inhaltlich nicht gibt. Die
Überschrift ist jetzt Typografie mit einer Haarlinie darunter und einem
Chevron, der sich beim Zuklappen dreht. Aufklappbar bleibt sie.

Die vergangenen Termine hatten zusätzlich eine eigene Monatsebene: ein
Klapper im Klapper, zwei Ebenen für eine Liste, die man ohnehin von neu
nach alt durchliest. Sie stehen jetzt flach unter ihrem Abschnitt.

Neu ist ein Schalter in der Filterleiste, der die Monatsüberschriften
ganz abschaltet. Bei wenigen Terminen je Monat bricht das Kartenraster
sonst nach jeder einzelnen Karte um, und es steht mehr Überschrift als
Inhalt auf der Seite; ohne Gruppierung fließen alle Termine durch
dasselbe Raster.

Die Wahl wird gespeichert. Der Speicher-Hook dafür lag schon im
Ansichtsumschalter des Dashboards und ist jetzt geteilt, statt das
Abonnenten-Muster ein zweites Mal abzuschreiben.
@nichtLehdev
nichtLehdev merged commit c058cb5 into main Sep 10, 2026
8 checks passed
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.

1 participant