Konzept: Vereinheitlichung der Zuweisungs-UIs (Assignment UIs)¶
Datum: 2025-11-10
Punkt: 146
Status: Konzept/Planung
📋 Übersicht¶
Aktuell gibt es verschiedene UIs für die Zuweisung von Datensätzen zueinander. Diese sollen vereinheitlicht werden, um: - Konsistente Benutzererfahrung zu schaffen - Code-Wiederverwendung zu maximieren - Wartbarkeit zu verbessern - Entwicklungszeit für neue Zuweisungstypen zu reduzieren
� Detaillierter Vergleich: Groups vs. Squads vs. Teams vs. EventParticipants¶
Diese Tabelle zeigt die Unterschiede zwischen den vier Hauptfeatures für Teilnehmer-Zuordnung:
| Feature | Groups (Gruppen) | Squads (Riegen) | Teams (Mannschaften) | EventParticipants (Teilnehmer) |
|---|---|---|---|---|
| Primäre Entität | tfx_gruppen |
Virtual/In-Memory | tfx_mannschaften |
tfx_teilnehmer |
| Verknüpfungstabelle | tfx_gruppen_x_teilnehmer |
tfx_riegen_x_disziplinen |
tfx_man_x_teilnehmer |
tfx_wertungen |
| Beziehungstyp | M:N (Group ↔ Participant) | M:N (Squad ↔ Discipline) | M:N (Team ↔ Participant) | 2-stufig: M:N (Athlete ↔ Event), dann M:N (Participant ↔ Competition) |
| Event-Zuordnung | ❌ Nein (verein-übergreifend) | ❌ Nein | ❌ Nein | ✅ JA (über tfx_wertungen) |
| Wettkampf-Zuordnung | ❌ Nein | ✅ Ja, mehrere möglich | ✅ Ja, genau EINER (Pflicht!) | ✅ JA (über tfx_wertungen, M:N) |
| Verein-Zuordnung | ✅ Ja (Pflicht, int_vereineid) |
❌ Nein | ✅ Ja (Pflicht, int_vereineid) |
✅ Ja (über tfx_teilnehmer.int_vereineid) |
| Teilnehmer-Zuordnung | ✅ M:N (mehrere Teilnehmer pro Gruppe) | ✅ M:N (mehrere Teilnehmer pro Riege) | ✅ M:N (mehrere Teilnehmer pro Team) | - (ist selbst der Teilnehmer) |
| Nummer | ❌ Nein | ❌ Nein | ✅ Ja (int_nummer, Mannschafts-Nr.) |
❌ Nein |
| Startnummer | ❌ Nein | ❌ Nein | ✅ Ja (int_startnummer, optional) |
✅ Ja (tfx_wertungen.int_startnummer) |
| Riegen-Buchstabe | ❌ Nein | ❌ Nein | ✅ Ja (var_riege, z.B. "A") |
✅ Ja (tfx_wertungen.var_riege) |
| Datenbank-Persistenz | ✅ Immer persistent | ⚠️ Virtual bis erste Teilnehmer-Zuweisung | ✅ Immer persistent | ✅ Immer persistent |
| UI-Pattern | Three-Column Assignment | Three-Column Assignment | Three-Column Assignment | Liste + Filter + Inline-Edit |
| Container-Navigation | ✅ Ja (mehrere Gruppen gleichzeitig) | ✅ Ja (mehrere Riegen gleichzeitig) | ✅ Ja (mehrere Teams gleichzeitig) | ❌ Nein (alle Participants eines Events) |
| Zweck | Verein-interne Gruppierung | Wettkampf-Durchgänge organisieren | Mannschaftswettkämpfe | Event-Teilnahme + Wettkampf-Zuweisung |
🔍 Detaillierte Struktur-Unterschiede¶
1. Groups (Gruppen)¶
tfx_gruppen (int_gruppenid, int_vereineid, var_name)
↓ M:N via tfx_gruppen_x_teilnehmer
tfx_teilnehmer
2. Squads (Riegen)¶
VIRTUAL Squad Object (in-memory, nicht in DB)
↓ M:N via tfx_riegen_x_disziplinen
tfx_disziplinen (Geräte: Reck, Barren, etc.)
tfx_wertungen.var_riege)
- Virtual Squad Hinweise: Orange "Virtual"-Badge, Hinweis auf In-Memory-Status
- UI: Three-Column Master-Detail (Riegen-Liste | Verfügbare Teilnehmer | Riegen-Details mit Competitions)
3. Teams (Mannschaften) ⭐¶
tfx_mannschaften (
int_mannschaftenid,
int_wettkaempfeid NOT NULL, -- PFLICHT!
int_vereineid NOT NULL, -- PFLICHT!
int_nummer,
var_riege,
int_startnummer
)
↓ M:N via tfx_man_x_teilnehmer
tfx_teilnehmer
int_wettkaempfeid - Genau EIN Wettkampf muss zugewiesen sein!
- int_vereineid - Team gehört zu einem Verein
- Charakteristik: Ein Team = Ein Verein + Ein Wettkampf + Nummer + optional Startnummer/Riege
- Persistenz: Immer in Datenbank
- UI: Three-Column Master-Detail (Teams-Liste | Verfügbare Teilnehmer | Team-Details)
4. EventParticipants (Teilnehmer) 🎯¶
tfx_teilnehmer (
int_teilnehmerid,
int_vereineid,
var_vorname,
var_nachname,
int_geschlecht,
dat_geburtstag
)
↓ 1:N via tfx_wertungen (M:N Join-Table!)
tfx_wettkaempfe (competitions within event)
tfx_wertungen)
- tfx_wertungen = M:N Join-Table mit zusätzlichen Feldern:
- int_wettkaempfeid (Competition)
- int_teilnehmerid (Participant)
- int_startnummer (Startnummer des Teilnehmers)
- var_riege (Riegen-Buchstabe, z.B. "A", "B")
- bol_startet_nicht (DNS - Did Not Start)
- bol_ak (Außer Konkurrenz)
- var_comment (Kommentar)
- Persistenz: tfx_teilnehmer immer persistent, tfx_wertungen nur bei Wettkampf-Zuweisung
- UI: NICHT Three-Column Assignment! Stattdessen:
- Tabelle/Cards-View mit Filtern (Verein, Geschlecht, Wettkampf)
- Inline-Edit für Competition-Zuweisung (Dropdown mit Checkboxes)
- Bulk-Actions für mehrere Teilnehmer gleichzeitig
🎯 Der zentrale Unterschied¶
EventParticipants ist KEIN klassisches M:N Assignment wie Groups/Squads/Teams!
Teams (klassisches M:N Assignment):
Container (Team) ↔ Items (Participants) zuweisen
↑
Fokus: Teilnehmer zwischen Teams hin- und herbewegen
EventParticipants (2-stufige Zuordnung):
Stufe 1: Athlete → Event hinzufügen (wird zu EventParticipant)
Stufe 2: EventParticipant ↔ Competitions zuweisen (M:N)
↑
Fokus: Competitions an bestehende Participants zuweisen
Warum EventParticipants eine Listen-UI braucht: - ✅ Alle Participants eines Events in einer Liste - ✅ Filtern nach Verein, Geschlecht, Wettkampf, etc. - ✅ Inline-Edit für Competition-Zuweisung - ✅ Bulk-Actions (z.B. alle Teilnehmer eines Vereins zu einem Wettkampf zuweisen) - ❌ KEINE Container-Navigation (kein "Team 1", "Team 2" auswählen) - ❌ KEINE "Verfügbar vs Zugewiesen" Spalten
Warum Teams/Groups/Squads Three-Column Assignment brauchen: - ✅ Container-Navigation (Team/Gruppe/Riege auswählen) - ✅ "Verfügbar vs Zugewiesen" Spalten - ✅ Teilnehmer zwischen Containern verschieben - ✅ Klare Zuweisung zu genau einem Container zur Zeit
�🔢 Beziehungstypen & UI-Patterns¶
Was ist eine 1:1 Beziehung?¶
Definition: Eine 1:1 (One-to-One) Beziehung bedeutet, dass ein Datensatz der einen Seite mit maximal einem Datensatz der anderen Seite verbunden ist, und umgekehrt.
Beispiele: - Benutzer ↔ Profil: Ein Benutzer hat ein Profil, ein Profil gehört zu einem Benutzer - Person ↔ Reisepass: Eine Person hat einen Reisepass, ein Reisepass gehört zu einer Person - Wettkampf ↔ Siegerehrung: Ein Wettkampf hat eine Siegerehrung, eine Siegerehrung gehört zu einem Wettkampf
Charakteristik: - ✅ Exklusive Beziehung (1:1 Mapping) - ✅ Oft Denormalisierung oder Aufteilung großer Tabellen - ❌ Keine Mehrfachzuweisung - ❌ Keine "Verfügbar"-Liste sinnvoll
Passende UI-Patterns:
| Pattern | Beschreibung | Use Case |
|---|---|---|
| Inline-Formular | Details direkt im Hauptformular | Profil-Daten im User-Formular |
| Verknüpfter Tab | Separate Tab-Section | Erweiterte Details |
| Modal mit Einzelauswahl | Auswahl eines verknüpften Elements | Beziehung herstellen |
| Autocomplete-Feld | Typeahead-Suche | Große Auswahl an möglichen Verknüpfungen |
Beispiel UI (Person ↔ Profil - 1:1):
┌─────────────────────────────────────────────────┐
│ Person bearbeiten │
├─────────────────────────────────────────────────┤
│ Name: [Max Mustermann ] │
│ Geburtstag: [01.01.2000 ] │
│ │
│ ┌─ Profil-Details ────────────────────────┐ │
│ │ Bio: [Sportler seit... ] │ │
│ │ Website: [https://... ] │ │
│ │ Foto: [Upload...] │ │
│ └─────────────────────────────────────────┘ │
│ │
│ [Speichern] [Abbrechen] │
└─────────────────────────────────────────────────┘
Warum NICHT UnifiedAssignmentModal? - ❌ Keine "Zuweisung" im eigentlichen Sinne - ❌ Kein "Verfügbar" vs "Zugewiesen" - nur eine direkte Verknüpfung - ❌ Meist Teil des Hauptformulars (Inline-Edit)
Was ist eine 1:N Beziehung?¶
Definition: Eine 1:N (One-to-Many) Beziehung bedeutet, dass ein Datensatz der einen Seite mit mehreren Datensätzen der anderen Seite verbunden sein kann, aber jeder Datensatz der anderen Seite gehört zu maximal einem Datensatz der ersten Seite.
Beispiele: - Verein ↔ Teilnehmer: Ein Verein hat viele Teilnehmer, aber jeder Teilnehmer gehört zu einem Verein - Wettkampf ↔ Ergebnisse: Ein Wettkampf hat viele Ergebnisse, aber jedes Ergebnis gehört zu einem Wettkampf - Kategorie ↔ Produkte: Eine Kategorie hat viele Produkte, aber ein Produkt gehört zu einer Kategorie
WICHTIG - EventParticipants ist NICHT 1:N:
- ❌ Event ↔ Teilnehmer ist eigentlich M:N (Athlete ↔ Event)!
- Ein Athlete kann in mehreren Events teilnehmen
- Ein Event hat mehrere Athletes
- → Zwischentabelle: EventParticipants (Join-Table)
- ❌ EventParticipant ↔ Competition ist ebenfalls M:N!
- Ein Participant kann an mehreren Competitions teilnehmen
- Eine Competition hat mehrere Participants
- → Zwischentabelle: ParticipantCompetitions (Join-Table)
Charakteristik: - ✅ Hierarchische Beziehung (Eltern-Kind) - ✅ Klare Zugehörigkeit - ❌ Keine mehrfache Zuweisung möglich
Passende UI-Patterns:
| Pattern | Beschreibung | Use Case |
|---|---|---|
| Liste mit Filter | Haupttabelle mit Filteroptionen | Große Datenmengen (EventParticipants) |
| Dropdown-Auswahl | Auswahl des übergeordneten Elements | Wenige Optionen (Event auswählen) |
| Hierarchische Ansicht | Tree-View mit Expandable Items | Verschachtelte Strukturen |
| Master-Detail | Liste links, Details rechts | Detail-Ansicht wichtig |
Beispiel UI (EventParticipants - 1:N):
┌─────────────────────────────────────────────────┐
│ Event: Bezirksmeisterschaft 2025 │
├─────────────────────────────────────────────────┤
│ [Filter: Verein ▼] [Filter: Geschlecht ▼] │
├─────────────────────────────────────────────────┤
│ Teilnehmer-Liste (Tabelle/Cards) │
│ ┌──────────────────────────────────────────┐ │
│ │ □ Max Mustermann | TV Musterstadt | m │ │
│ │ □ Anna Beispiel | SV Beispiel | w │ │
│ │ □ Peter Test | TV Musterstadt | m │ │
│ └──────────────────────────────────────────┘ │
│ [+ Teilnehmer hinzufügen] [Bulk-Actions] │
└─────────────────────────────────────────────────┘
Warum NICHT UnifiedAssignmentModal? - ❌ Keine "Zuweisung" zwischen zwei gleichberechtigten Entitäten - ❌ Kein "Verfügbar" vs "Zugewiesen" - alle Teilnehmer gehören bereits zum Event - ❌ Fokus liegt auf Verwaltung/Anzeige, nicht auf Zuweisung
Anmerkung zu EventParticipants:
EventParticipants sieht aus wie 1:N, ist aber technisch M:N:
- Athlete ↔ Event (M:N): EventParticipants ist die Join-Table
- EventParticipant ↔ Competition (M:N): ParticipantCompetitions ist die Join-Table
Dennoch wird eine Listen-UI verwendet (nicht Assignment-UI), weil: - Fokus auf Verwaltung der Participants innerhalb eines Events - Keine "Container-Auswahl" wie bei Riegen - Bulk-Zuweisung von Competitions an Participants (Inline-Dropdown)
Was ist eine M:N Beziehung?¶
Definition: Eine M:N (Many-to-Many) Beziehung bedeutet, dass ein Datensatz der einen Seite mit mehreren Datensätzen der anderen Seite verbunden sein kann, UND umgekehrt.
Beispiele: - Riegen ↔ Teilnehmer: Eine Riege hat mehrere Teilnehmer, ein Teilnehmer kann in mehreren Riegen sein - Gruppen ↔ Teilnehmer: Eine Gruppe hat mehrere Mitglieder, ein Teilnehmer kann in mehreren Gruppen sein - Wettkämpfe ↔ Disziplinen: Ein Wettkampf hat mehrere Disziplinen, eine Disziplin kommt in mehreren Wettkämpfen vor - Mannschaften ↔ Teilnehmer: Ein Team hat mehrere Mitglieder, ein Teilnehmer kann in mehreren Teams sein - Kampfrichter ↔ Disziplinen: Ein Kampfrichter bewertet mehrere Disziplinen, eine Disziplin wird von mehreren Kampfrichtern bewertet
Charakteristik: - ✅ Gleichberechtigte Beziehung (keine Hierarchie) - ✅ Mehrfache Zuweisung in beide Richtungen - ✅ Zwischentabelle in Datenbank (Join-Table) - ✅ Items können "verfügbar" oder "zugewiesen" sein
Passende UI-Patterns:
| Pattern | Beschreibung | Use Case | Layout |
|---|---|---|---|
| Two-Column Assignment | Verfügbar ↔ Zugewiesen | Einfache M:N ohne Container | 2-Spalten |
| Three-Column Master-Detail | Container ↔ Verfügbar ↔ Details | M:N mit mehreren Containern (Riegen!) | 3-Spalten |
| Checkbox-Grid | Checkboxen für alle Optionen | Wenige Items mit zusätzlichen Feldern | Inline |
| Dropdown mit Liste | Dropdown + zugewiesene Liste | Sehr wenige Items | Vertikal |
Beispiel UI: Two-Column (Gruppen ↔ Teilnehmer):
┌─────────────────────────────────────────────────────┐
│ Gruppe: "U12 Jungs" - Mitglieder verwalten [X] │
├─────────────────────────────────────────────────────┤
│ [Search: Suche...] [Filter: Verein ▼] │
├──────────────────────┬──────────────────────────────┤
│ Verfügbar (25) │ Zugewiesen zu Gruppe (8) │
│ ┌────────────────┐ │ ┌────────────────┐ │
│ │ ☐ Max M. [→] │ │ │ ☑ Anna B. [←] │ │
│ │ ☐ Peter T. [→] │ │ │ ☑ Lisa S. [←] │ │
│ │ ☐ Tom K. [→] │ │ │ ☑ Jan W. [←] │ │
│ │ ... │ │ │ ... │ │
│ └────────────────┘ │ └────────────────┘ │
│ [Assign Selected] │ [Remove Selected] │
└──────────────────────┴──────────────────────────────┘
Beispiel UI: Three-Column Master-Detail (Riegen ↔ Teilnehmer):
┌───────────────────────────────────────────────────────────────┐
│ Riegen Management [X] │
├───────────────────────────────────────────────────────────────┤
│ [Search] [Filter: Verein ▼] [Filter: Wettkampf ▼] │
├──────────────┬─────────────────────┬──────────────────────────┤
│ Riegen (5) │ Verfügbar (50) │ Riege "A" Details │
│ ┌────────┐ │ ┌──────────────┐ │ ┌──────────────────┐ │
│ │▶ Riege A│ │ │☐ Max M. [→] │ │ │☑ Anna B. [←] │ │
│ │ Riege B│ │ │☐ Peter T.[→] │ │ │☑ Lisa S. [←] │ │
│ │ Riege C│ │ │☐ Tom K. [→] │ │ │☑ Jan W. [←] │ │
│ │ + Neu │ │ │ ... │ │ │ ... │ │
│ └────────┘ │ └──────────────┘ │ └──────────────────┘ │
│ │ │ │
│ Master-List │ Available Items │ Detail-Pane │
│ (Container) │ (zum Zuweisen) │ (Zugewiesene + Info) │
└──────────────┴─────────────────────┴──────────────────────────┘
Warum UnifiedAssignmentModal PERFEKT passt: - ✅ Klare "Verfügbar" vs "Zugewiesen" Trennung - ✅ Bidirektionale Zuweisung (hinzufügen/entfernen) - ✅ Bulk-Operationen sinnvoll - ✅ Such- und Filterfunktionen wichtig - ✅ Drei-Spalten-Layout für mehrere Container (z.B. mehrere Riegen gleichzeitig verwalten)
Vergleich: 1:N vs M:N¶
| Aspekt | 1:N (One-to-Many) | M:N (Many-to-Many) |
|---|---|---|
| Beziehung | Hierarchisch, Eltern-Kind | Gleichberechtigt |
| Mehrfachzuweisung | ❌ Nein (Kind hat nur 1 Eltern) | ✅ Ja (beide Seiten mehrfach) |
| Datenbank | Foreign Key in Child-Tabelle | Join-Tabelle erforderlich |
| UI-Fokus | Verwaltung, Anzeige, Filter | Zuweisung, Auswahl, Transfer |
| Typische Aktionen | Erstellen, Bearbeiten, Löschen, Filtern | Zuweisen, Entfernen, Bulk-Assign |
| "Verfügbar"-Konzept | ❌ Nicht sinnvoll | ✅ Zentral wichtig |
| UnifiedAssignmentModal | ❌ Nicht geeignet | ✅ Perfekt geeignet |
Beispiel-Szenarien:
1:N Szenario (EventParticipants):
Event: "Bezirksmeisterschaft 2025"
├── Teilnehmer: Max Mustermann
├── Teilnehmer: Anna Beispiel
├── Teilnehmer: Peter Test
└── Teilnehmer: Lisa Schmidt
→ Jeder Teilnehmer gehört NUR zu diesem Event
→ Fokus: Liste anzeigen, filtern, bearbeiten
→ UI: Tabelle/Cards mit Filtern
M:N Szenario (Riegen ↔ Teilnehmer):
Riege A (Boden) Teilnehmer Riege B (Sprung)
├── Max Mustermann ←───→ Max Mustermann ←───→ (auch hier!)
├── Anna Beispiel ←───→ Anna Beispiel ←───→ (auch hier!)
└── Peter Test └── Lisa Schmidt
→ Max und Anna sind in BEIDEN Riegen
→ Fokus: Items zwischen Riegen hin- und herbewegen
→ UI: Two/Three-Column Assignment mit "Verfügbar"/"Zugewiesen"
Entscheidungsbaum: Welches UI-Pattern?¶
Beziehungstyp?
│
├─ 1:1 (One-to-One)
│ └─ Inline-Formular oder Modal mit Autocomplete
│
├─ 1:N (One-to-Many)
│ └─ Anzahl Items?
│ ├─ Wenig (<20) → Master-Detail (links Liste, rechts Details)
│ ├─ Mittel (20-100) → Tabelle mit Inline-Edit
│ └─ Viel (>100) → Tabelle + Filter + Pagination
│
└─ M:N (Many-to-Many)
└─ Gibt es mehrere "Container"?
├─ Nein (1 Container)
│ └─ Anzahl Items?
│ ├─ Wenig (<20) → Single-Column (Dropdown + Liste)
│ ├─ Mittel (20-50) → Two-Column Assignment
│ └─ Viel (>50) → Two-Column + Search + Filter
│
└─ Ja (Mehrere Container, z.B. mehrere Riegen)
└─ Three-Column Master-Detail
├─ Spalte 1: Container-Auswahl (Master-List)
├─ Spalte 2: Verfügbare Items (Available-List)
└─ Spalte 3: Zugewiesene Items + Details (Detail-Pane)
Beispiele:
| Use Case | Beziehung | Container? | Items | UI-Pattern |
|---|---|---|---|---|
| User → Profil | 1:1 | - | 1 | Inline-Formular (NICHT Assignment) |
| Verein → Teilnehmer | 1:N | - | >100 | Tabelle + Filter (NICHT Assignment) |
| Athlete → Event | M:N | - | >100 | Liste + Filter (Spezialfall: Listen-UI statt Assignment)* |
| EventParticipant → Competition | M:N | - | 5-15 | Inline-Dropdown (Bulk-Zuweisung)* |
| Gruppen → Teilnehmer | M:N | 1 Gruppe | 20-50 | Two-Column Assignment |
| Riegen → Teilnehmer | M:N | Mehrere Riegen | >50 | Three-Column Master-Detail |
| Teams → Teilnehmer | M:N | 1 Team | 10-20 | Two-Column oder Dropdown+Liste |
| Wettkampf → Disziplinen | M:N | 1 Wettkampf | 10-15 | Checkbox-Grid (wegen max_score) |
* EventParticipants Spezialfall: - Technisch M:N (Athlete ↔ Event, EventParticipant ↔ Competition) - ABER: UI ist Listen-fokussiert (keine Assignment-UI) - Grund: Verwaltung innerhalb eines Events, keine Container-Navigation - Competitions werden via Inline-Dropdown bulk-zugewiesen
🎯 Betroffene Zuweisungstypen¶
Identifizierte Zuweisungen¶
| # | Zuweisung | Beziehung | Aktueller Ort | Status | UI-Pattern |
|---|---|---|---|---|---|
| 1 | Wettkämpfe ↔ Disziplinen | M:N | CompetitionFormModal.tsx |
✅ Vorhanden | Inline-Checkboxen in Modal |
| 2 | Athlete ↔ Event | M:N | EventParticipants (Page) |
✅ Vorhanden | Liste + Filter (Spezialfall)* |
| 2b | EventParticipant ↔ Competition | M:N | EventParticipants (Inline) |
✅ Vorhanden | Inline-Dropdown (Bulk) |
| 3 | Riegen ↔ Teilnehmer | M:N | SquadManagement.tsx |
✅ Vorhanden | Drag & Drop + 3-Column Master-Detail |
| 4 | Mannschaften ↔ Teilnehmer | M:N | TeamPenaltiesModal.tsx (Abzüge)components/ (fehlend) |
⚠️ Teilweise | Modal |
| 5 | Gruppen ↔ Teilnehmer | M:N | GroupMembersModal.tsx |
✅ Vorhanden | Modal mit Dropdown |
Legende Beziehungstypen: - 1:1 (One-to-One): Exklusive Beziehung, 1:1 Mapping → ❌ UnifiedAssignmentModal NICHT geeignet (Inline-Formular) - 1:N (One-to-Many): Item gehört zu maximal einem Eltern-Element → ❌ UnifiedAssignmentModal NICHT geeignet (Listen-UI) - M:N (Many-to-Many): Item kann mehrfach zugewiesen werden (z.B. Teilnehmer in mehreren Riegen) → ✅ UnifiedAssignmentModal geeignet
* EventParticipants Spezialfall: - Beziehungen sind technisch M:N (Athlete ↔ Event, EventParticipant ↔ Competition) - ABER: UI ist Listen-fokussiert statt Assignment-fokussiert - Grund: Verwaltung innerhalb eines Events, keine Container-Navigation wie bei Riegen - Dennoch könnten Teilbereiche (z.B. Competition-Zuweisung) theoretisch UnifiedAssignmentModal nutzen
Weitere identifizierte Zuweisungen¶
| # | Zuweisung | Beziehung | Potenzieller Bedarf | Priorität |
|---|---|---|---|---|
| 7 | Wettkämpfe ↔ Durchgänge | M:N | TimePlanning | Niedrig (bereits integriert) |
| 8 | Riegen ↔ Wettkämpfe | M:N | SquadManagement | Mittel |
| 9 | Kampfrichter ↔ Wettkämpfe | M:N | Jury Portal | Hoch (separates System) |
| 10 | Mannschaften ↔ Wettkämpfe | M:N | Teams Page | Hoch (aktuell fehlend?) |
🔍 Analyse der bestehenden Implementierungen¶
1. Wettkämpfe ↔ Disziplinen¶
Datei: client/src/components/CompetitionFormModal.tsx (905 Zeilen)
Pattern: - Checkboxen-Grid innerhalb des Competition-Formulars - Disziplinen gefiltert nach Geschlecht - Gruppierung nach Disziplinen-Gruppen - Inline max-score Eingabe pro Disziplin
Eigenschaften:
// Daten-Struktur
disciplines: { disciplineId: number; maxScore: number }[]
// UI-Features
- Gender-basierte Filterung
- Disziplinen-Gruppen Dropdown
- Bulk-Actions (alle setzen)
- Inline validation
- Visuelles Feedback (bereits zugewiesen)
Stärken: - ✅ Übersichtlich bei wenigen Disziplinen - ✅ Schnelle Mehrfachauswahl - ✅ Zusätzliche Parameter (maxScore) direkt sichtbar
Schwächen: - ❌ Nicht skalierbar bei vielen Disziplinen - ❌ Keine Suchfunktion - ❌ Modal bereits sehr groß (905 Zeilen - needs SoC!)
2. Teilnehmer ↔ Wettkämpfe¶
Datei: client/src/pages/EventParticipants/ (verschiedene Komponenten)
Pattern: - Inline-Auswahl in Teilnehmer-Tabelle - Bulk-Assignment möglich - Status-Anzeige (Anzahl zugewiesener Wettkämpfe)
Eigenschaften:
// UI-Features
- Multi-select für Teilnehmer
- Dropdown für Wettkampf-Auswahl
- Bulk-Zuweisung
- Status-Badge mit Count
Stärken: - ✅ Schnelle Bulk-Operationen - ✅ Übersichtliche Status-Anzeige - ✅ Direkt in der Hauptansicht
Schwächen: - ❌ Keine Detail-Ansicht der Zuweisungen - ❌ Schwer, individuelle Zuweisungen zu prüfen
3. Riegen ↔ Teilnehmer¶
Datei: client/src/pages/SquadManagement.tsx (1070 Zeilen - CRITICAL SoC needed!)
Pattern: - Drei-Spalten-Layout (Master-Detail Pattern) - Spalte 1: Riegen-Liste (Master) mit Auswahl - Spalte 2: Verfügbare Teilnehmer - Spalte 3: Details der ausgewählten Riege (Detail) - Drag & Drop Unterstützung (geplant, aktuell Click-to-assign) - Virtuelle Riegen ("Nicht zugeordnet") - Detaillierte Filterung
Eigenschaften:
// UI-Features
- Master-List: Riegen-Auswahl (links)
- Available List: Teilnehmer zum Zuweisen (mitte)
- Detail Pane: Zugewiesene Teilnehmer + Riegen-Info (rechts)
- Click-to-assign (Pfeil-Button)
- Umfangreiche Filter (gender, club, competition)
- Virtuelle "Nicht zugeordnet" Riege
- Competition-basiertes Highlighting
- PDF Export
- 3-Spalten Grid Layout: lg:grid-cols-3
Stärken: - ✅ Sehr intuitiv (Master-Detail Navigation) - ✅ Visuell ansprechend - ✅ Umfangreiche Filteroptionen - ✅ Gute Übersicht über alle Riegen - ✅ Gleichzeitige Anzeige: Riegen, Verfügbar, Zugewiesen - ✅ Competition-Highlighting (zeigt relevante Teilnehmer)
Schwächen: - ❌ Datei zu groß (1070 Zeilen - urgent refactoring!) - ❌ Komplex in der Wartung - ❌ Nicht generisch wiederverwendbar - ❌ Kein echtes Drag & Drop (nur Buttons) - ❌ Layout nur für Full-Screen optimiert
Besonderheit: Dies ist das einzige UI mit Master-Detail Pattern - zeigt mehrere "Container" (Riegen) gleichzeitig und erlaubt Navigation zwischen ihnen.
4. Gruppen ↔ Teilnehmer¶
Datei: client/src/components/GroupMembersModal.tsx (263 Zeilen)
Pattern: - Modal mit zwei Bereichen (zugewiesen | hinzufügen) - Dropdown-Auswahl für verfügbare Teilnehmer - Liste der zugewiesenen Mitglieder - Club-basierte Filterung automatisch
Eigenschaften:
// UI-Features
- Dropdown mit verfügbaren Teilnehmern
- Liste der aktuellen Mitglieder
- Add/Remove Buttons
- Automatische Filterung (gleicher Verein)
- Keine Duplikate möglich
Stärken: - ✅ Einfaches, klares Pattern - ✅ Verhindert automatisch Duplikate - ✅ Kompakt und wartbar - ✅ Gute Code-Qualität (263 Zeilen)
Schwächen: - ❌ Dropdown nicht ideal bei vielen Teilnehmern - ❌ Keine Bulk-Operationen - ❌ Keine Suchfunktion
5. Mannschaften ↔ Teilnehmer (Abzüge)¶
Datei: client/src/components/TeamPenaltiesModal.tsx (239 Zeilen)
Hinweis: Dies ist eigentlich "Mannschaften ↔ Abzugstypen", nicht Teilnehmer!
Pattern: - Modal mit Dropdown für verfügbare Abzugstypen - Liste der zugewiesenen Abzüge - Add/Remove Pattern
Eigenschaften:
// UI-Features
- Dropdown mit verfügbaren Penalty-Types
- Liste mit zugewiesenen Penalties
- Automatische Duplikat-Vermeidung
- Sofortige Punktzahl-Anzeige
Stärken: - ✅ Sehr ähnlich zu GroupMembersModal (gutes Pattern!) - ✅ Kompakt und wartbar - ✅ Klare Trennung
Schwächen: - ❌ Name irreführend ("Penalties" nicht "Members") - ❌ Eigentlich kein Teilnehmer-Assignment
Fehlend: Echte Mannschaften ↔ Teilnehmer Zuweisung!
6. Teilnehmer ↔ Veranstaltung¶
Datei: client/src/pages/EventParticipants/components/AddParticipantModal.tsx (172 Zeilen)
Pattern: - Modal mit Suche und Tabelle - Einzelne Zuweisung per Klick - Filterung durch Suchbegriff
Eigenschaften:
// UI-Features
- Suchfeld für Teilnehmer
- Tabelle mit allen verfügbaren Teilnehmern
- Gender-Badge Anzeige
- Club-Anzeige
- Add-Button pro Zeile
Stärken: - ✅ Suchfunktion vorhanden - ✅ Übersichtliche Tabelle - ✅ Guter Code (172 Zeilen) - ✅ Klare Struktur
Schwächen: - ❌ Keine Bulk-Operationen - ❌ Keine Sortierung - ❌ Keine erweiterten Filter
🎨 Unified Assignment Component - Design Konzept¶
Ziel¶
Eine wiederverwendbare UnifiedAssignmentModal Komponente, die alle Zuweisungstypen abdecken kann.
Core Features¶
1. Generische Daten-Schnittstelle¶
interface AssignmentItem {
id: number;
displayName: string;
secondaryInfo?: string;
metadata?: Record<string, any>;
badge?: {
text: string;
variant: 'success' | 'warning' | 'info' | 'error';
};
}
// NEW: Container für Master-Detail Pattern (z.B. Riegen)
interface AssignmentContainer {
id: number | string;
name: string;
displayName: string;
itemCount: number;
metadata?: Record<string, any>;
isVirtual?: boolean;
badge?: {
text: string;
variant: 'success' | 'warning' | 'info' | 'error';
};
}
interface AssignmentConfig<TItem extends AssignmentItem = AssignmentItem> {
// Titles & Labels
title: string;
availableLabel: string;
assignedLabel: string;
searchPlaceholder: string;
emptyAvailableMessage: string;
emptyAssignedMessage: string;
// Data
availableItems: TItem[];
assignedItems: TItem[];
// Master-Detail Pattern (NEW - optional, for 3-column layout)
containers?: AssignmentContainer[];
selectedContainer?: AssignmentContainer | null;
onContainerSelect?: (container: AssignmentContainer) => void;
onContainerCreate?: () => void;
onContainerDelete?: (containerId: number | string) => void;
containerLabel?: string; // e.g. "Riegen", "Teams"
// Actions
onAssign: (item: TItem, containerId?: number | string) => Promise<void>;
onUnassign: (item: TItem, containerId?: number | string) => Promise<void>;
onBulkAssign?: (items: TItem[], containerId?: number | string) => Promise<void>;
onBulkUnassign?: (items: TItem[], containerId?: number | string) => Promise<void>;
// Features
enableSearch?: boolean;
enableBulkActions?: boolean;
enableDragDrop?: boolean;
enableFilters?: boolean;
filters?: AssignmentFilter[];
// Display
displayMode?: 'two-column' | 'three-column' | 'single-column' | 'checkbox-grid';
columns?: AssignmentColumn[];
// Additional data per assignment
additionalFields?: AssignmentField[];
// Validation
validateAssignment?: (item: TItem, containerId?: number | string) => string | null;
// Custom renderers
renderItem?: (item: TItem) => React.ReactNode;
renderBadge?: (item: TItem) => React.ReactNode;
renderContainer?: (container: AssignmentContainer) => React.ReactNode; // NEW
renderDetailPane?: (container: AssignmentContainer, items: TItem[]) => React.ReactNode; // NEW
}
interface AssignmentFilter {
id: string;
label: string;
type: 'select' | 'text' | 'checkbox';
options?: { value: string; label: string }[];
filterFn: (item: AssignmentItem, value: any) => boolean;
}
interface AssignmentColumn {
id: string;
label: string;
width?: string;
render: (item: AssignmentItem) => React.ReactNode;
sortable?: boolean;
}
interface AssignmentField {
id: string;
label: string;
type: 'number' | 'text' | 'select';
required?: boolean;
defaultValue?: any;
options?: { value: any; label: string }[];
validate?: (value: any) => string | null;
}
2. UI Layouts¶
Layout 1: Two-Column (Simple Assignment)
┌─────────────────────────────────────────────────────┐
│ Modal Title [X] │
├─────────────────────────────────────────────────────┤
│ [Search] [Filter ▼] [Filter ▼] │
├──────────────────────┬──────────────────────────────┤
│ Verfügbar (50) │ Zugewiesen (12) │
│ ┌────────────────┐ │ ┌────────────────┐ │
│ │ ☐ Item 1 [→] │ │ │ ☑ Item A [X] │ │
│ │ ☐ Item 2 [→] │ │ │ ☑ Item B [X] │ │
│ │ ☐ Item 3 [→] │ │ │ ☑ Item C [X] │ │
│ │ ... │ │ │ ... │ │
│ └────────────────┘ │ └────────────────┘ │
│ [Assign Selected] │ [Remove Selected] │
└──────────────────────┴──────────────────────────────┘
Layout 1b: Three-Column (Master-Detail Pattern für SquadManagement)
┌───────────────────────────────────────────────────────────────┐
│ Squad Management [X] │
├───────────────────────────────────────────────────────────────┤
│ [Search] [Filter ▼] [Filter ▼] [Filter ▼] │
├──────────────┬─────────────────────┬──────────────────────────┤
│ Riegen (5) │ Verfügbar (50) │ Riege "A" Details │
│ ┌────────┐ │ ┌──────────────┐ │ ┌──────────────────┐ │
│ │▶ Riege A│ │ │☐ Person 1 [→]│ │ │☑ Person X [←] │ │
│ │ Riege B│ │ │☐ Person 2 [→]│ │ │☑ Person Y [←] │ │
│ │ Riege C│ │ │☐ Person 3 [→]│ │ │☑ Person Z [←] │ │
│ │ + Neu │ │ │ ... │ │ │ ... │ │
│ └────────┘ │ └──────────────┘ │ └──────────────────┘ │
│ │ │ │
│ │ [Assign Selected] │ Competitions: │
│ │ │ • Competition 1 │
│ │ │ • Competition 2 │
│ │ │ │
│ │ │ [Delete Squad] │
└──────────────┴─────────────────────┴──────────────────────────┘
Features: - Master-List (Spalte 1): Auswahl des Ziel-Containers - Available (Spalte 2): Items zum Zuweisen - Detail (Spalte 3): Zugewiesene Items + Container-Details
Layout 2: Single Column (GroupMembers Pattern)
┌─────────────────────────────────────────────────────┐
│ Modal Title [X] │
├─────────────────────────────────────────────────────┤
│ Zugewiesen (5) │
│ ┌─────────────────────────────────────────────┐ │
│ │ ● Item A [Remove] │ │
│ │ ● Item B [Remove] │ │
│ │ ● Item C [Remove] │ │
│ └─────────────────────────────────────────────┘ │
│ │
│ Verfügbar hinzufügen │
│ [Dropdown: Select item... ▼] [Add] │
│ oder │
│ [Search: Type to search...] │
│ ┌─────────────────────────────────────────────┐ │
│ │ ○ Item 1 [Add] │ │
│ │ ○ Item 2 [Add] │ │
│ │ ○ Item 3 [Add] │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
Layout 3: Inline Checkboxes (CompetitionForm Pattern)
┌─────────────────────────────────────────────────────┐
│ Select Disciplines │
│ [Filter by group: All ▼] │
│ ┌─────────────────────────────────────────────┐ │
│ │ ☑ Boden [Max Score: 10.0] │ │
│ │ ☐ Sprung [Max Score: 10.0] │ │
│ │ ☑ Barren [Max Score: 10.0] │ │
│ │ ☐ Reck [Max Score: 10.0] │ │
│ └─────────────────────────────────────────────┘ │
│ [Set all to: 10.0] [Apply] │
└─────────────────────────────────────────────────────┘
3. Varianten-Matrix¶
| Feature | Two-Column | Three-Column (Master-Detail) | Single-Column | Inline-Checkboxes |
|---|---|---|---|---|
| Drag & Drop | ✅ Ja | ✅ Ja | ❌ Nein | ❌ Nein |
| Bulk-Actions | ✅ Ja | ✅ Ja | ⚠️ Optional | ✅ Ja |
| Search | ✅ Ja | ✅ Ja | ✅ Ja | ⚠️ Optional |
| Filters | ✅ Ja | ✅✅ Advanced | ⚠️ Limited | ✅ Ja |
| Additional Fields | ⚠️ Optional | ⚠️ Optional | ⚠️ Optional | ✅ Ja |
| Multiple Containers | ❌ Nein | ✅✅ Ja (Master-List) | ❌ Nein | ❌ Nein |
| Container Details | ❌ Nein | ✅ Ja (Detail-Pane) | ❌ Nein | ❌ Nein |
| Best for | Simple 1:N | Complex M:N (Riegen) | Few items | Medium items |
| Complexity | Mittel | Hoch | Niedrig | Mittel |
| Use Case | Groups→Members | Squads→Participants | Event→Participant | Competition→Disciplines |
🏗️ Implementierungs-Roadmap¶
Phase 1: Foundation (Woche 1-2)¶
Ziel: Core UnifiedAssignmentModal Component erstellen
Tasks:
1. ✅ Analyse der bestehenden Patterns (✅ DONE - dieses Dokument)
2. Create base UnifiedAssignmentModal.tsx component
3. Implement basic two-column layout
4. Implement three-column layout (Master-Detail pattern for SquadManagement)
5. Implement basic single-column layout
6. Add search functionality
7. Add basic filter support
8. Write comprehensive tests
9. Create Storybook stories
Dateien:
- client/src/components/assignments/UnifiedAssignmentModal.tsx
- client/src/components/assignments/layouts/TwoColumnLayout.tsx
- client/src/components/assignments/layouts/ThreeColumnLayout.tsx (NEW!)
- client/src/components/assignments/layouts/SingleColumnLayout.tsx
- client/src/components/assignments/AssignmentList.tsx
- client/src/components/assignments/AssignmentItem.tsx
- client/src/components/assignments/AssignmentFilters.tsx
- client/src/components/assignments/MasterList.tsx (NEW - for 3-column)
- client/src/components/assignments/DetailPane.tsx (NEW - for 3-column)
- client/src/components/assignments/types.ts
Phase 2: Advanced Features (Woche 3-4)¶
Ziel: Erweiterte Funktionen implementieren
Tasks: 1. Implement drag & drop (react-dnd or dnd-kit) 2. Add bulk actions 3. Implement additional fields support 4. Add validation framework 5. Implement inline-checkboxes layout 6. Add custom renderers 7. Performance optimization (virtualization for large lists)
Dateien:
- client/src/components/assignments/DragDropAssignment.tsx
- client/src/components/assignments/BulkActions.tsx
- client/src/components/assignments/AdditionalFields.tsx
Phase 3: Migration (Woche 5-8)¶
Ziel: Bestehende UIs auf neues System migrieren
Priority Order:
3.1 Gruppen ↔ Teilnehmer (EINFACH - Start here!)¶
- ✅ Ähnlich zu bestehendem Pattern
- ✅ Kleine Komponente (263 Zeilen)
- ✅ Wenig Business-Logik
- Effort: ~2 Tage
- Files:
GroupMembersModal.tsx
3.2 Teilnehmer ↔ Veranstaltung (EINFACH)¶
- ✅ Klare Anforderungen
- ✅ Gute Code-Basis (172 Zeilen)
- Effort: ~2 Tage
- Files:
AddParticipantModal.tsx
3.3 Wettkämpfe ↔ Disziplinen (MITTEL)¶
- ⚠️ Zusätzliche Felder (maxScore)
- ⚠️ Komplexe Filterung
- ⚠️ Teil eines großen Modals (905 Zeilen - needs SoC anyway!)
- Effort: ~1 Woche
- Files:
CompetitionFormModal.tsx(extract discipline assignment)
3.4 Mannschaften ↔ Teilnehmer (NEU!)¶
- ⚠️ Komponente fehlt komplett
- ⚠️ Benötigt Backend-Route
- Effort: ~1 Woche
- Files:
components/TeamMembersModal.tsx(NEW)
3.5 Riegen ↔ Teilnehmer (KOMPLEX - Last!)¶
- ❌ Sehr große Datei (1070 Zeilen - CRITICAL SoC!)
- ❌ Viel Business-Logik
- ❌ Drag & Drop erforderlich
- ❌ Virtuelle Riegen-Konzept
- Effort: ~2 Wochen
- Files:
SquadManagement.tsx(major refactoring)
Phase 4: Documentation & Polish (Woche 9)¶
Ziel: Dokumentation und Feinschliff
Tasks: 1. Write comprehensive documentation 2. Create usage examples 3. Update developer guidelines 4. Create migration guide 5. Performance audit 6. Accessibility audit 7. Localization completion 8. User acceptance testing
📐 Technical Specifications¶
Component Architecture¶
UnifiedAssignmentModal (Container)
├── AssignmentHeader
│ ├── Title
│ ├── SearchBar
│ └── FilterBar
├── AssignmentContent (Layout-specific)
│ ├── TwoColumnLayout
│ │ ├── AvailableList
│ │ │ ├── AssignmentItem[]
│ │ │ └── BulkActions
│ │ └── AssignedList
│ │ ├── AssignmentItem[]
│ │ └── BulkActions
│ ├── ThreeColumnLayout (NEW - Master-Detail)
│ │ ├── MasterList (Container Selection)
│ │ │ ├── ContainerItem[]
│ │ │ └── CreateButton
│ │ ├── AvailableList
│ │ │ ├── AssignmentItem[]
│ │ │ └── BulkActions
│ │ └── DetailPane (Selected Container)
│ │ ├── ContainerInfo
│ │ ├── AssignedList
│ │ │ └── AssignmentItem[]
│ │ └── ContainerActions
│ ├── SingleColumnLayout
│ │ ├── AssignedList
│ │ ├── DropdownSelector
│ │ └── AvailableList
│ └── InlineCheckboxLayout
│ ├── FilterRow
│ └── CheckboxGrid
└── AssignmentFooter
├── Stats
└── Actions (Save/Cancel)
State Management¶
// Internal state
interface AssignmentState {
// Data
availableItems: AssignmentItem[];
assignedItems: AssignmentItem[];
// UI State
searchTerm: string;
filters: Record<string, any>;
selectedAvailable: Set<number>;
selectedAssigned: Set<number>;
// Operation State
isLoading: boolean;
isSaving: boolean;
error: string | null;
// Drag & Drop (if enabled)
draggedItem: AssignmentItem | null;
}
// Actions
type AssignmentAction =
| { type: 'SET_SEARCH'; payload: string }
| { type: 'SET_FILTER'; payload: { key: string; value: any } }
| { type: 'SELECT_AVAILABLE'; payload: number[] }
| { type: 'SELECT_ASSIGNED'; payload: number[] }
| { type: 'ASSIGN_ITEMS'; payload: AssignmentItem[] }
| { type: 'UNASSIGN_ITEMS'; payload: AssignmentItem[] }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string | null };
API Contract¶
// Backend-Seite standardisieren
interface AssignmentEndpoints {
// Get available items (not yet assigned)
getAvailable: (entityId: number, params?: Record<string, any>) => Promise<TItem[]>;
// Get assigned items
getAssigned: (entityId: number) => Promise<TItem[]>;
// Assign single item
assign: (entityId: number, itemId: number, data?: Record<string, any>) => Promise<void>;
// Unassign single item
unassign: (entityId: number, itemId: number) => Promise<void>;
// Bulk assign (optional)
bulkAssign?: (entityId: number, itemIds: number[], data?: Record<string, any>) => Promise<void>;
// Bulk unassign (optional)
bulkUnassign?: (entityId: number, itemIds: number[]) => Promise<void>;
}
🎯 Anwendungsbeispiele¶
Beispiel 1: Gruppen ↔ Teilnehmer (Single-Column)¶
<UnifiedAssignmentModal
isOpen={isOpen}
onClose={onClose}
config={{
title: t('groups.members.title', { name: group.name }),
availableLabel: t('groups.members.available'),
assignedLabel: t('groups.members.assigned'),
searchPlaceholder: t('groups.members.searchPlaceholder'),
displayMode: 'list',
enableSearch: true,
enableBulkActions: false,
availableItems: availableParticipants.map(p => ({
id: p.id,
displayName: `${p.firstname} ${p.lastname}`,
secondaryInfo: p.club,
badge: { text: p.gender, variant: 'info' }
})),
assignedItems: members.map(m => ({
id: m.id,
displayName: `${m.firstname} ${m.lastname}`,
secondaryInfo: m.club
})),
onAssign: async (item) => {
await apiPost(`/groups/${group.id}/members`, {
participantId: item.id
});
},
onUnassign: async (item) => {
await apiDelete(`/groups/${group.id}/members/${item.id}`);
},
filters: [
{
id: 'club',
label: t('common.club'),
type: 'select',
options: clubOptions,
filterFn: (item, value) => item.metadata?.clubId === value
}
]
}}
/>
Beispiel 2: Riegen ↔ Teilnehmer (Three-Column Master-Detail)¶
<UnifiedAssignmentModal
isOpen={isOpen}
onClose={onClose}
config={{
title: t('squadManagement.title'),
availableLabel: t('squadManagement.available'),
assignedLabel: t('squadManagement.assigned'),
containerLabel: t('squadManagement.squads.label'),
displayMode: 'three-column',
enableSearch: true,
enableBulkActions: true,
enableDragDrop: true, // Future: drag from available to detail pane
enableFilters: true,
// Master List (Riegen)
containers: squads.map(s => ({
id: s.id,
name: s.name,
displayName: s.name,
itemCount: s.participantCount,
isVirtual: s.isVirtual,
metadata: {
competitions: s.competitions,
hints: s.hints
},
badge: s.isVirtual ? {
text: t('squadManagement.squads.virtual'),
variant: 'warning'
} : undefined
})),
selectedContainer: selectedSquad ? {
id: selectedSquad.id,
name: selectedSquad.name,
displayName: selectedSquad.name,
itemCount: selectedSquad.participantCount,
metadata: selectedSquad
} : null,
onContainerSelect: (container) => {
const squad = squads.find(s => s.id === container.id);
setSelectedSquad(squad);
},
onContainerCreate: () => {
setIsCreateModalOpen(true);
},
onContainerDelete: async (containerId) => {
if (!window.confirm(t('squadManagement.confirmDelete'))) return;
await apiDelete(`/squads/${containerId}`);
await loadSquads();
},
// Available Items
availableItems: filteredParticipants.map(p => ({
id: p.id,
displayName: `${p.firstname} ${p.lastname}`,
secondaryInfo: p.club,
metadata: {
firstname: p.firstname,
lastname: p.lastname,
gender: p.gender,
birthYear: p.birthYear,
competitions: p.competitions,
competitionCount: p.competitionCount
},
badge: {
text: p.gender,
variant: 'info'
}
})),
// Assigned Items (from selected container)
assignedItems: selectedSquad?.participants.map(p => ({
id: p.id,
displayName: `${p.firstname} ${p.lastname}`,
secondaryInfo: p.club,
metadata: {
competitions: p.competitions,
gender: p.gender
}
})) || [],
// Table columns for available/assigned lists
columns: [
{
id: 'name',
label: t('common.name'),
render: (item) => item.displayName,
sortable: true
},
{
id: 'club',
label: t('common.club'),
render: (item) => item.secondaryInfo,
sortable: true
},
{
id: 'age',
label: t('common.age'),
render: (item) => {
const age = new Date().getFullYear() - item.metadata.birthYear;
return age;
},
sortable: true
},
{
id: 'competitions',
label: t('common.competitions'),
render: (item) => (
<span className="text-xs">
{item.metadata.competitionCount || 0} {t('common.competitions')}
</span>
)
}
],
// Actions
onAssign: async (item, containerId) => {
if (!containerId) {
alert(t('squadManagement.messages.selectSquadFirst'));
return;
}
await apiPost('/squad-management/assign', {
squadId: containerId,
participantId: item.id,
eventId: eventId
});
await forceLoadSquads();
await loadAvailableParticipants();
},
onUnassign: async (item) => {
await apiDelete(`/squad-management/unassign`, {
params: {
participantId: item.id,
eventId: eventId
}
});
await forceLoadSquads();
await loadAvailableParticipants();
},
onBulkAssign: async (items, containerId) => {
if (!containerId) {
alert(t('squadManagement.messages.selectSquadFirst'));
return;
}
await apiPost('/squad-management/bulk-assign', {
squadId: containerId,
participantIds: items.map(i => i.id),
eventId: eventId
});
await forceLoadSquads();
await loadAvailableParticipants();
},
// Filters
filters: [
{
id: 'gender',
label: t('common.gender'),
type: 'select',
options: [
{ value: '', label: t('common.all') },
{ value: 'male', label: t('gender.male') },
{ value: 'female', label: t('gender.female') }
],
filterFn: (item, value) => {
if (!value) return true;
return item.metadata.gender === value;
}
},
{
id: 'competition',
label: t('common.competition'),
type: 'select',
options: [
{ value: '', label: t('common.all') },
...allCompetitions.map(c => ({
value: c.id.toString(),
label: `${c.name} (Nr. ${c.number})`
}))
],
filterFn: (item, value) => {
if (!value) return true;
return item.metadata.competitions?.some(
c => c.id.toString() === value
);
}
},
{
id: 'club',
label: t('common.club'),
type: 'select',
options: [
{ value: '', label: t('common.all') },
...allClubs.map(club => ({ value: club, label: club }))
],
filterFn: (item, value) => {
if (!value) return true;
return item.secondaryInfo === value;
}
}
],
// Custom render for container item (in master list)
renderContainer: (container) => (
<div className={`p-4 rounded-lg border cursor-pointer transition-colors ${
selectedSquad?.id === container.id
? 'border-blue-500 bg-blue-50'
: 'hover:border-gray-300'
} ${container.isVirtual ? 'border-l-4 border-l-orange-400' : ''}`}>
<div className="flex items-center gap-2 mb-1">
<h4 className="font-medium text-gray-900">{container.displayName}</h4>
{container.badge && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
{container.badge.text}
</span>
)}
</div>
<p className="text-sm text-gray-500">
{container.itemCount} {t('squadManagement.squads.participants')}
</p>
{container.metadata?.hints?.storage && (
<p className="text-xs text-orange-600 mt-1">
💾 {container.metadata.hints.storage}
</p>
)}
{container.metadata?.competitions && (
<div className="mt-2 flex flex-wrap gap-1">
{container.metadata.competitions.slice(0, 2).map((comp, idx) => (
<span
key={idx}
className="px-2 py-1 bg-gray-100 text-gray-700 rounded text-xs"
>
{comp.name} (Nr. {comp.number})
</span>
))}
{container.metadata.competitions.length > 2 && (
<span className="px-2 py-1 bg-gray-100 text-gray-700 rounded text-xs">
+{container.metadata.competitions.length - 2}
</span>
)}
</div>
)}
</div>
),
// Custom render for detail pane
renderDetailPane: (container, items) => (
<div className="space-y-4">
<div className="bg-white rounded-lg border p-4">
<h4 className="font-medium text-gray-900 mb-2">
{t('squadManagement.squadDetails.info')}
</h4>
<dl className="space-y-2 text-sm">
<div className="flex justify-between">
<dt className="text-gray-500">{t('common.name')}:</dt>
<dd className="font-medium">{container.displayName}</dd>
</div>
<div className="flex justify-between">
<dt className="text-gray-500">{t('squadManagement.squads.participants')}:</dt>
<dd className="font-medium">{items.length}</dd>
</div>
{container.metadata?.competitions && (
<div>
<dt className="text-gray-500 mb-1">{t('common.competitions')}:</dt>
<dd className="space-y-1">
{container.metadata.competitions.map((comp, idx) => (
<div key={idx} className="text-xs bg-blue-50 px-2 py-1 rounded">
{comp.name} (Nr. {comp.number})
</div>
))}
</dd>
</div>
)}
</dl>
</div>
{container.isVirtual && (
<div className="bg-orange-50 border border-orange-200 rounded-lg p-3">
<p className="text-xs text-orange-800">
<strong>{t('squadManagement.virtualInfo.title')}</strong><br />
{t('squadManagement.virtualInfo.description')}
</p>
</div>
)}
</div>
),
validateAssignment: (item, containerId) => {
if (!containerId) {
return t('squadManagement.messages.selectSquadFirst');
}
// Add custom validation logic here
return null;
}
}}
/>
Key Features des Three-Column Layouts: - Master-List zeigt alle Container (Riegen) mit Metadaten - Auswahl eines Containers lädt dessen zugewiesene Items in Detail-Pane - Available-List zeigt filterbare Items zum Zuweisen - Detail-Pane zeigt Container-Info + zugewiesene Items - Bulk-Assignment an ausgewählten Container - Custom Renderer für Container und Detail-Pane - Unterstützt virtuelle Container (z.B. "Nicht zugeordnet")
Beispiel 2b: Riegen ↔ Teilnehmer (Two-Column Simplified - Alternative)¶
<UnifiedAssignmentModal
isOpen={isOpen}
onClose={onClose}
config={{
title: t('squadManagement.title'),
availableLabel: t('squadManagement.available'),
assignedLabel: t('squadManagement.assigned', { name: squad.name }),
displayMode: 'table',
enableSearch: true,
enableBulkActions: true,
enableDragDrop: true,
enableFilters: true,
columns: [
{
id: 'name',
label: t('common.name'),
render: (item) => `${item.metadata.firstname} ${item.metadata.lastname}`,
sortable: true
},
{
id: 'club',
label: t('common.club'),
render: (item) => item.secondaryInfo,
sortable: true
},
{
id: 'competitions',
label: t('common.competitions'),
render: (item) => (
<span className="text-xs">
{item.metadata.competitions?.join(', ')}
</span>
)
}
],
availableItems: availableParticipants,
assignedItems: squadParticipants,
onAssign: async (item) => {
await apiPost('/squad-management/assign', {
squadId: squad.id,
participantId: item.id,
eventId: eventId
});
},
onUnassign: async (item) => {
await apiDelete(`/squad-management/unassign`, {
params: { participantId: item.id, eventId: eventId }
});
},
onBulkAssign: async (items) => {
await apiPost('/squad-management/bulk-assign', {
squadId: squad.id,
participantIds: items.map(i => i.id),
eventId: eventId
});
},
filters: [
{
id: 'gender',
label: t('common.gender'),
type: 'select',
options: [
{ value: 'männlich', label: t('gender.male') },
{ value: 'weiblich', label: t('gender.female') }
],
filterFn: (item, value) => item.metadata.gender === value
},
{
id: 'competition',
label: t('common.competition'),
type: 'select',
options: competitionOptions,
filterFn: (item, value) =>
item.metadata.competitions?.includes(value)
}
]
}}
/>
Beispiel 3: Wettkämpfe ↔ Disziplinen (Inline-Checkboxes)¶
<UnifiedAssignmentModal
isOpen={isOpen}
onClose={onClose}
config={{
title: t('competitions.selectDisciplines'),
displayMode: 'checkbox-grid',
enableSearch: false,
enableFilters: true,
availableItems: disciplines.map(d => ({
id: d.id,
displayName: d.name,
metadata: {
maleAllowed: d.male_allowed,
femaleAllowed: d.female_allowed,
groupId: d.groupId
}
})),
assignedItems: formData.disciplines.map(d => ({
id: d.disciplineId,
displayName: d.name,
metadata: { maxScore: d.maxScore }
})),
additionalFields: [
{
id: 'maxScore',
label: t('competitions.maxScore'),
type: 'number',
required: true,
defaultValue: 10.0,
validate: (value) => {
if (value <= 0) return t('competitions.maxScoreMustBePositive');
if (value > 100) return t('competitions.maxScoreTooHigh');
return null;
}
}
],
onAssign: async (item) => {
// Inline: update formData directly
setFormData(prev => ({
...prev,
disciplines: [
...prev.disciplines,
{ disciplineId: item.id, maxScore: item.metadata.maxScore }
]
}));
},
onUnassign: async (item) => {
setFormData(prev => ({
...prev,
disciplines: prev.disciplines.filter(d => d.disciplineId !== item.id)
}));
},
filters: [
{
id: 'group',
label: t('disciplines.group'),
type: 'select',
options: disciplineGroupOptions,
filterFn: (item, value) => item.metadata.groupId === value
},
{
id: 'gender',
label: t('common.gender'),
type: 'checkbox',
filterFn: (item, value) => {
if (value === 'male') return item.metadata.maleAllowed;
if (value === 'female') return item.metadata.femaleAllowed;
return true;
}
}
],
validateAssignment: (item) => {
// Example: Check gender compatibility
if (formData.gender === 'männlich' && !item.metadata.maleAllowed) {
return t('competitions.disciplineNotAllowedForGender');
}
return null;
}
}}
/>
🚀 Benefits der Vereinheitlichung¶
Für Entwickler¶
- ✅ Code Reuse: ~70% weniger Code für neue Zuweisungen
- ✅ Consistency: Einheitliches Pattern für alle Zuweisungen
- ✅ Maintainability: Eine Komponente statt 6+ verschiedene
- ✅ Type Safety: Generische TypeScript Interfaces
- ✅ Testing: Einmal testen, überall nutzen
- ✅ SoC: Separation of Concerns - kleine, fokussierte Komponenten
Für Benutzer¶
- ✅ Consistency: Gleiche Bedienung überall
- ✅ Lernkurve: Einmal lernen, überall anwenden
- ✅ Features: Alle Zuweisungen erhalten gleiche Features (Search, Filter, etc.)
- ✅ Performance: Optimiert und virtualisiert
- ✅ Accessibility: Einheitliche A11y-Standards
Für Projekt¶
- ✅ Velocity: Schnellere Entwicklung neuer Features
- ✅ Quality: Höhere Code-Qualität durch Wiederverwendung
- ✅ Maintenance: Einfachere Wartung
- ✅ Scalability: Leicht erweiterbar für neue Zuweisungstypen
⚠️ Risiken & Herausforderungen¶
Technische Risiken¶
| Risiko | Wahrscheinlichkeit | Impact | Mitigation |
|---|---|---|---|
| Zu generisch → schwer verwendbar | Mittel | Hoch | Iteratives Design, User Testing |
| Performance bei vielen Items | Mittel | Mittel | Virtualisierung (react-window) |
| Drag & Drop Kompatibilität | Niedrig | Mittel | Bewährte Libraries (dnd-kit) |
| Rückwärts-Kompatibilität | Niedrig | Niedrig | Schrittweise Migration |
| Over-Engineering | Mittel | Mittel | Start simple, add features as needed |
Organisatorische Risiken¶
| Risiko | Wahrscheinlichkeit | Impact | Mitigation |
|---|---|---|---|
| Zu lange Entwicklungszeit | Mittel | Hoch | Phased Approach, MVP first |
| User Acceptance | Niedrig | Hoch | Early User Testing, Beta-Phase |
| Breaking Changes | Niedrig | Mittel | Feature Flags, Parallel Running |
📊 Effort Estimation¶
Development Effort (Person-Days)¶
| Phase | Tasks | Effort | Priority |
|---|---|---|---|
| Phase 1: Foundation | Base Component + 3 Layouts | 12 Tage | 🔴 Critical |
| Phase 2: Advanced | Drag & Drop, Bulk, Fields | 10 Tage | 🟡 High |
| Phase 3.1: Gruppen | Migration | 2 Tage | 🟡 High |
| Phase 3.2: Event | Migration | 2 Tage | 🟡 High |
| Phase 3.3: Competitions | Migration + SoC | 5 Tage | 🟢 Medium |
| Phase 3.4: Teams | New Component | 5 Tage | 🟢 Medium |
| Phase 3.5: Squads | Migration + Heavy SoC (3-col layout!) | 12 Tage | 🔵 Low |
| Phase 4: Polish | Documentation, Testing | 5 Tage | 🟡 High |
| TOTAL | 53 Tage |
Estimated Timeline: ~11 Wochen (bei 1 Entwickler, full-time)
Note: Phase 3.5 (Squads) benötigt mehr Zeit aufgrund des komplexen 3-Spalten Master-Detail Patterns.
🎯 Success Criteria¶
Must-Have (MVP)¶
- UnifiedAssignmentModal base component funktioniert
- Two-Column Layout implementiert
- Single-Column Layout implementiert
- Search funktioniert
- Basic filtering funktioniert
- Mindestens 2 Zuweisungen migriert (Gruppen + Event)
- Tests geschrieben (>80% coverage)
- Dokumentation vorhanden
Should-Have¶
- Drag & Drop funktioniert
- Bulk actions funktioniert
- Inline-Checkboxes Layout
- Additional fields support
- 4 von 6 Zuweisungen migriert
- Performance optimiert (virtualization)
- Accessibility audit passed
Nice-to-Have¶
- Alle 6 Zuweisungen migriert
- Custom renderers support
- Advanced validation
- Keyboard shortcuts
- Undo/Redo functionality
- Export/Import functionality
📝 Next Steps¶
Immediate Actions (Diese Woche)¶
- Review & Approval
- Review dieses Konzept
- Feedback einholen
-
Prioritäten validieren
-
Technical Spike
- Prototyp einer einfachen UnifiedAssignmentModal
- Test mit GroupMembersModal-Daten
-
Performance-Tests mit großen Datenmengen
-
Team Alignment
- Entwickler informieren
- Code Review Guidelines anpassen
- Migration Plan kommunizieren
Short-Term (Nächste 2 Wochen)¶
- Start Phase 1 Development
- Create base component structure
- Implement first layout (Single-Column)
- Write initial tests
- Create Storybook stories
Mid-Term (Nächste 4 Wochen)¶
- Complete Phase 1 & 2
- Start Phase 3 migrations (Gruppen, Event)
- Gather user feedback
- Iterate on design
Long-Term (Nächste 8 Wochen)¶
- Complete all migrations
- Full documentation
- Performance optimization
- Production deployment
- Legacy code cleanup
📚 Related Documents¶
API_ROUTE_MISMATCHES.md- Field mapping conventionsFRONTEND_SERVING_IMPLEMENTATION.md- Deployment patternsPRIORITY_FIXES_LOG.md- Historical fixes.github/copilot-instructions.md- Development guidelinesclient/src/components/templates/EventManagementTemplate.tsx- Template pattern
✅ Conclusion¶
Die Vereinheitlichung der Zuweisungs-UIs ist ein wichtiger Schritt zur Verbesserung der Code-Qualität und Benutzererfahrung. Mit einem klaren, phasenweisen Ansatz können wir:
- Reduktion der Code-Duplikation um ~70%
- Konsistente UX für alle Zuweisungen
- Schnellere Entwicklung neuer Features
- Bessere Wartbarkeit durch SoC
Empfehlung: Start mit Phase 1 (Foundation) und Phase 3.1 (Gruppen-Migration) als Proof of Concept.
Autor: GitHub Copilot
Datum: 2025-11-10
Version: 1.0
Status: ✅ Ready for Review