diff --git a/docs/06_Frontend/FIGMA/Vision_02/NAVIGATION.md b/docs/06_Frontend/FIGMA/Vision_02/NAVIGATION.md new file mode 100644 index 00000000..43c7745f --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_02/NAVIGATION.md @@ -0,0 +1,529 @@ +# Navigation & Benutzerfluss-Diagramm + +## Übersicht: Haupt-Navigation + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ LOGIN-SEITE │ +│ │ +│ ┌────────────────────────────────────────┐ │ +│ │ Username: admin │ │ +│ │ Passwort: Admin#1234 │ │ +│ │ [Login] ───────────────────────────────────────────┐ │ +│ └────────────────────────────────────────┘ │ │ +└─────────────────────────────────────────────────────────┼───────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ HAUPTANSICHT (AdminDrawer) │ +│ │ +│ ┌──────────────────┬──────────────────────────────────────────────────┐ │ +│ │ DRAWER (Links) │ MAIN CONTENT (Rechts) │ │ +│ │ │ │ │ +│ │ ○ Veranstaltungen ──────► [Veranstaltungs-Seiten] │ │ +│ │ ○ Reiter │ │ │ +│ │ ○ Pferde │ │ │ +│ │ ○ Funktionäre │ │ │ +│ │ ○ Meisterschaften │ │ +│ │ ○ Cups │ │ │ +│ │ │ │ │ +│ │ [Logout] │ │ │ +│ └──────────────────┴──────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Detaillierter Navigationsbaum + +``` +HAUPTANSICHT (/) +│ +├─ DRAWER NAVIGATION (links) +│ │ +│ ├─ 📁 Veranstaltungen +│ │ │ +│ │ ├─ [Button: Neue Veranstaltung] +│ │ │ └─► /veranstaltung/neu +│ │ │ │ +│ │ │ └─► VERANSTALTUNGS-ANSICHT (Neue) +│ │ │ ├─ Tab: Veranstaltung - Übersicht +│ │ │ ├─ Tab: Stammdaten (A-Satz) ← STANDARDTAB +│ │ │ ├─ Tab: Organisation +│ │ │ └─ Tab: Preisliste +│ │ │ +│ │ ├─ [Veranstaltung 1] ► Turnier Pfingsten 2023 +│ │ │ └─► /veranstaltung/1 +│ │ │ │ +│ │ │ └─► VERANSTALTUNGS-ANSICHT (Bestehende) +│ │ │ └─ Tab: Veranstaltung - Übersicht (EINZIGER TAB) +│ │ │ │ +│ │ │ └─ TURNIERE-SECTION +│ │ │ │ +│ │ │ ├─ [Button: Neues Turnier] +│ │ │ │ └─► /veranstaltung/1/turnier/neu +│ │ │ │ │ +│ │ │ │ └─► TURNIER-ANSICHT (Neu) +│ │ │ │ ├─ Tab: Veranstaltung - Übersicht +│ │ │ │ ├─ Tab: Stammdaten (A-Satz) +│ │ │ │ ├─ Tab: Organisation +│ │ │ │ ├─ Tab: Bewerbe ⭐ HAUPTSEITE +│ │ │ │ └─ Tab: Preisliste +│ │ │ │ +│ │ │ └─ TURNIER-LISTE +│ │ │ │ +│ │ │ ├─ [Turnier 1] (zum Öffnen klicken) +│ │ │ │ └─► /veranstaltung/1/turnier/1 +│ │ │ │ │ +│ │ │ │ └─► TURNIER-ANSICHT (Bestehend) +│ │ │ │ └─ [Alle 5 Tabs wie oben] +│ │ │ │ +│ │ │ ├─ [Turnier 2] (zum Öffnen klicken) +│ │ │ │ └─► /veranstaltung/1/turnier/2 +│ │ │ │ +│ │ │ └─ [Turnier 3] (zum Öffnen klicken) +│ │ │ └─► /veranstaltung/1/turnier/3 +│ │ │ +│ │ ├─ [Veranstaltung 2] ► Sommerturnier 2023 +│ │ │ └─► /veranstaltung/2 +│ │ │ └─► [gleiche Struktur wie Veranstaltung 1] +│ │ │ +│ │ └─ [Veranstaltung 3] ► Herbstturnier 2023 +│ │ └─► /veranstaltung/3 +│ │ └─► [gleiche Struktur wie Veranstaltung 1] +│ │ +│ ├─ 📁 Reiter (nicht implementiert) +│ ├─ 📁 Pferde (nicht implementiert) +│ ├─ 📁 Funktionäre (nicht implementiert) +│ ├─ 📁 Meisterschaften (nicht implementiert) +│ ├─ 📁 Cups (nicht implementiert) +│ │ +│ └─ [Button: Logout] +│ └─► Zurück zur Login-Seite +│ +└─ MAIN CONTENT AREA (rechts) + └─► Zeigt jeweils die ausgewählte Seite/Tab +``` + +--- + +## BEWERBE-TAB - Detail-Navigation ⭐ + +Die wichtigste Seite der Anwendung! + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ BEWERBE-TAB (/veranstaltung/:id/turnier/:nr) │ +│ │ +│ ┌─────────────┬──────────────────────────┬──────────────────────────────┐ │ +│ │ AKTIONEN │ BEWERBS-ÜBERSICHT │ BEWERB-KONFIGURATION │ │ +│ │ (150px) │ (50%) │ (50%) │ │ +│ ├─────────────┼──────────────────────────┼──────────────────────────────┤ │ +│ │ │ │ │ │ +│ │ [Änderungen │ ┌────────────────────┐ │ ┌──────────────────────┐ │ │ +│ │ Speichern] │ │ TOOLBAR │ │ │ TABS │ │ │ +│ │ │ │ │ • Aktualisieren │ │ │ ○ Bewerb │ │ │ +│ │ └──────►│ │ • 12 Bewerbe │ │ │ ○ Bewertung │ │ │ +│ │ (Speichert│ │ • Filtern │ │ │ ○ Geldpreise │ │ │ +│ │ alle) │ └────────────────────┘ │ │ ○ Ort/Zeit │ │ │ +│ │ │ │ └──────────────────────┘ │ │ +│ │ [Änderungen │ ┌────────────────────┐ │ │ │ +│ │ Rückgängig]│ │ TABELLE │ │ [Tab-Content hier] │ │ +│ │ │ │ │ ┌─┬───┬───┬──────┐ │ │ │ │ +│ │ └──────►│ │ │T│Pl.│Bew│ ... │ │ │ ← Zeigt Details des │ │ +│ │ (Undo) │ │ │a│a │er │ │ │ │ ausgewählten Bewerbs │ │ +│ │ │ │ │g│tz │b │ │ │ │ │ │ +│ ├─────────────┤ │ │ │ │ │ │ │ │ ← Interaktive Felder │ │ +│ │ │ │ └─┴───┴───┴──────┘ │ │ │ │ +│ │ [Bewerb │ │ ▲ │ │ ← Speichern pro Feld │ │ +│ │ Einfügen] │ │ │ Klick wählt │ │ │ │ +│ │ │ │ │ │ Bewerb aus │ │ │ │ +│ │ └──────►│ │ │ │ │ │ │ +│ │ (Fügt │ │ └────────────────►│ │ │ │ +│ │ Zeile │ │ Zeigt Details │ │ │ │ +│ │ hinzu) │ │ rechts → │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ [Bewerb │ └────────────────────┘ └──────────────────────────────┘ │ +│ │ Löschen] │ │ +│ │ │ │ │ +│ │ └──────►│ (Löscht ausgewählten Bewerb) │ +│ │ │ │ +│ │ [Bewerb │ │ +│ │ Teilen] │ (Dupliziert ausgewählten Bewerb) │ +│ │ │ │ │ +│ │ └──────►│ │ +│ ├─────────────┤ │ +│ │ │ │ +│ │ [Bewerb nach│ (Verschiebt in Tabelle nach oben) │ +│ │ oben vers.]│ │ │ +│ │ │ │ └──────► Ändert Reihenfolge │ +│ │ └──────►│ │ +│ │ │ │ +│ │ [Bewerb nach│ (Verschiebt in Tabelle nach unten) │ +│ │ unten vers]│ │ │ +│ │ │ │ └──────► Ändert Reihenfolge │ +│ │ └──────►│ │ +│ ├─────────────┤ │ +│ │ │ │ +│ │ [Startliste │ (Öffnet Startlisten-Editor - noch nicht implementiert) │ +│ │ Bearbeiten]│ │ +│ │ │ │ +│ │ [Startliste │ (Öffnet Druck-Dialog - noch nicht implementiert) │ +│ │ Drucken] │ │ +│ ├─────────────┤ │ +│ │ │ │ +│ │ [Ergebnislst│ (Öffnet Ergebnislisten-Editor - noch nicht implementiert) │ +│ │ Bearbeiten]│ │ +│ │ │ │ +│ │ [Ergebnislst│ (Öffnet Druck-Dialog - noch nicht implementiert) │ +│ │ Drucken] │ │ +│ └─────────────┴──────────────────────────┴──────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Tab-Wechsel in Bewerb-Konfiguration + +``` +BEWERB-KONFIGURATION (Rechte Seite im Bewerbe-Tab) +│ +├─ TAB 1: Bewerb (Grunddaten) +│ │ +│ ├─ [Feld: Nummer] ────► Text ändern → Speichern bei "Änderungen Speichern" +│ ├─ [Feld: Abteilung] ► Text ändern → Speichern bei "Änderungen Speichern" +│ ├─ [Feld: Typ] ──────► Text ändern → Speichern bei "Änderungen Speichern" +│ ├─ [Feld: Name] ─────► Text ändern → Speichern bei "Änderungen Speichern" +│ ├─ [Feld: Bezeichnung] Text ändern → Speichern bei "Änderungen Speichern" +│ ├─ [Dropdown: Kategorie] Auswahl ändern +│ ├─ [Dropdown: Klasse] ─► Auswahl ändern +│ ├─ [Dropdown: Lizenz] ─► Auswahl ändern +│ ├─ [Feld: Maximal] ───► Zahl ändern (Pferde je Reiter) +│ ├─ [Dropdown: Pferdealter] Auswahl ändern +│ ├─ [Feld: Zeile 1] ───► Text ändern (z.B. "Pony Einsteiger Cup OÖ") +│ ├─ [Feld: Zeile 2] ───► Text ändern +│ ├─ [Feld: Zeile 3] ───► Text ändern +│ └─ [Feld: Logo Bewerb + Button "..."] +│ └─► Button öffnet Dateiauswahl (noch nicht implementiert) +│ +├─ TAB 2: Bewertung +│ │ +│ ├─ [Feld: Prüfung] ───────► Text ändern +│ ├─ [Feld: Richtverfahren] ► Text ändern (z.B. "A") +│ ├─ [Feld: Para-Grade] ────► Text ändern +│ ├─ [Feld: Richteranzahl] ─► Zahl ändern +│ ├─ [Feld: Aufgabe] ───────► Text ändern (z.B. "Aufgabe R") +│ ├─ [Feld: Aufgabennummer] ► Text ändern +│ ├─ [Feld: Maximalpunkte] ─► Zahl ändern +│ │ +│ └─ RICHTER-LISTE (dynamisch) +│ │ +│ ├─ Richter 1 +│ │ ├─ [Feld: Position] ─► Text ändern (z.B. "C") +│ │ ├─ [Feld: Name] ─────► Text ändern (z.B. "Schuster Alexandra") +│ │ └─ [Checkbox: Aktiv] ► An/Aus +│ │ +│ ├─ Richter 2 +│ │ ├─ [Feld: Position] ─► Text ändern (z.B. "C") +│ │ ├─ [Feld: Name] ─────► Text ändern (z.B. "Vankova Kamila (CZ)") +│ │ └─ [Checkbox: Aktiv] ► An/Aus +│ │ +│ └─ ... (weitere Richter) +│ +├─ TAB 3: Geldpreise +│ │ +│ ├─ SECTION: Geldpreis +│ │ ├─ [Checkbox: Geldpreis] ──────────► An/Aus +│ │ ├─ [Feld: Startgeld] ─────────────► Text ändern (z.B. "15,00") +│ │ └─ [Dropdown: Auszahlung] ────────► Auswahl (fortführend, 1/3, 1/4, 1/5) +│ │ +│ ├─ SECTION: Geldpreis für Kadererreiter +│ │ ├─ [Checkbox: Geldpreis für Kadererreiter] ► An/Aus +│ │ └─ [Feld: Startgeld für Kadererreiter] ───► Text ändern (z.B. "15,00") +│ │ +│ ├─ [Dropdown: Geldpreisvorlage wählen] ──────► Auswahl (Vorlagen) +│ │ │ +│ │ └──► Füllt Geldpreise-Tabelle automatisch +│ │ +│ └─ TABELLE: Geldpreise +│ │ +│ ├─ Spalte: Nummer +│ ├─ Spalte: Geldpreis +│ └─ [Zeigt "0 Geldpreise" wenn leer] +│ +└─ TAB 4: Ort/Zeit + │ + ├─ [Dropdown: Tag] ─────────────► Auswahl (28.05.2023, ...) + ├─ [Dropdown: Beginnzeit] ──────► Auswahl (fix um, nicht vor, ca.) + ├─ [Feld: Zeit] ────────────────► Text ändern (Format: hh:mm, z.B. "08:00") + ├─ [Feld: Reitdauer] ───────────► Text ändern (Format: mm:ss, z.B. "02:00") + ├─ [Feld: Umbau] ───────────────► Text ändern (in Minuten, z.B. "10") + ├─ [Feld: Besichtigung] ────────► Text ändern (in Minuten, z.B. "10") + ├─ [Feld: Stechen] ─────────────► Text ändern (in Minuten, leer möglich) + └─ [Dropdown: Platz] ───────────► Auswahl (Vorderer Turnierplatz, Hauptplatz, ...) +``` + +--- + +## Interaktionsfluss: Veranstaltung → Turnier → Bewerb + +``` +SCHRITT 1: Veranstaltung erstellen +┌────────────────────────────────────────┐ +│ Drawer: [Neue Veranstaltung] │ +└────────────────┬───────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ /veranstaltung/neu │ +│ │ +│ Tabs sichtbar: │ +│ • Veranstaltung - Übersicht │ +│ • Stammdaten ← STARTET HIER │ +│ • Organisation │ +│ • Preisliste │ +│ │ +│ [Daten eingeben: Name, Ort, Datum...] │ +│ [Speichern-Button] │ +└────────────────┬───────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ Veranstaltung gespeichert │ +│ → Erscheint in Drawer-Liste │ +└────────────────┬───────────────────────┘ + │ + ▼ +SCHRITT 2: Turnier erstellen +┌────────────────────────────────────────┐ +│ Drawer: [Veranstaltung 1] klicken │ +└────────────────┬───────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ /veranstaltung/1 │ +│ │ +│ Tab: Veranstaltung - Übersicht │ +│ │ +│ ┌────────────────────────────────────┐ │ +│ │ TURNIERE-SECTION │ │ +│ │ [Button: Neues Turnier] ←─ KLICK │ │ +│ └────────────────────────────────────┘ │ +└────────────────┬───────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ /veranstaltung/1/turnier/neu │ +│ │ +│ Tabs sichtbar: │ +│ • Veranstaltung - Übersicht │ +│ • Stammdaten │ +│ • Organisation │ +│ • Bewerbe ← WICHTIGSTE SEITE │ +│ • Preisliste │ +│ │ +│ [Daten eingeben: Turniername...] │ +│ [Speichern-Button] │ +└────────────────┬───────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ Turnier gespeichert │ +│ → Erscheint in Turnier-Liste │ +│ unter Veranstaltung 1 │ +└────────────────┬───────────────────────┘ + │ + ▼ +SCHRITT 3: Bewerbe konfigurieren +┌────────────────────────────────────────┐ +│ Drawer: [Turnier 1] "Öffnen" klicken │ +└────────────────┬───────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ /veranstaltung/1/turnier/1 │ +│ │ +│ [Tab "Bewerbe" auswählen] │ +└────────────────┬───────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ BEWERBE-TAB │ +│ │ +│ 1. [Bewerb Einfügen] klicken │ +│ → Neue Zeile in Tabelle │ +│ │ +│ 2. Bewerb in Tabelle auswählen │ +│ → Details erscheinen rechts │ +│ │ +│ 3. Tabs durchgehen: │ +│ • Bewerb (Grunddaten eingeben) │ +│ • Bewertung (Richter hinzufügen) │ +│ • Geldpreise (Startgeld festlegen) │ +│ • Ort/Zeit (Zeitplan konfigurieren) │ +│ │ +│ 4. [Änderungen Speichern] klicken │ +│ │ +│ 5. Weitere Bewerbe hinzufügen... │ +└────────────────────────────────────────┘ +``` + +--- + +## Tastatur-Navigation (geplant) + +``` +GLOBALE SHORTCUTS (zukünftig): +• Ctrl+S / Cmd+S ──► Speichern +• Ctrl+Z / Cmd+Z ──► Rückgängig +• Ctrl+N / Cmd+N ──► Neuer Bewerb +• Tab ────────────► Nächstes Feld +• Shift+Tab ──────► Vorheriges Feld +• Pfeiltasten ────► Navigation in Tabellen +• Enter ──────────► Zeile öffnen/bestätigen +• Esc ────────────► Dialog schließen + +BEWERBE-TAB SHORTCUTS: +• Ctrl+↑ ─────────► Bewerb nach oben +• Ctrl+↓ ─────────► Bewerb nach unten +• Ctrl+D ─────────► Bewerb duplizieren +• Delete ─────────► Bewerb löschen (mit Bestätigung) +• Ctrl+1-4 ───────► Tab-Wechsel (Bewerb/Bewertung/Geldpreise/Ort-Zeit) +``` + +--- + +## Fehlerbehandlung & Dialoge (zukünftig) + +``` +AKTIONEN MIT BESTÄTIGUNG: +┌─────────────────────────────────────────┐ +│ [Bewerb Löschen] geklickt │ +└───────────────┬─────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ ⚠️ BESTÄTIGUNGS-DIALOG │ +│ │ +│ "Bewerb 5 wirklich löschen?" │ +│ │ +│ [Abbrechen] [Löschen] ←────────────────┼──► Bewerb wird gelöscht +└─────────────────────────────────────────┘ + +SPEICHERN MIT VALIDIERUNG: +┌─────────────────────────────────────────┐ +│ [Änderungen Speichern] geklickt │ +└───────────────┬─────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ Validierung läuft... │ +│ │ +│ ✓ Alle Pflichtfelder ausgefüllt? │ +│ ✓ Zeitformat korrekt? │ +│ ✓ Nummern-Duplikate? │ +└───────────────┬─────────────────────────┘ + │ + ├──► OK ──► Speichern erfolgreich ✓ + │ + └──► Fehler ──► ❌ FEHLER-DIALOG + │ + │ "Bitte korrigieren Sie:" + │ • Feld "Nummer" ist leer + │ • Zeit-Format ungültig + │ + └─► [OK] + +UNGESPEICHERTE ÄNDERUNGEN: +┌─────────────────────────────────────────┐ +│ Benutzer verlässt Seite (z.B. klickt │ +│ auf anderen Tab oder Turnier) │ +└───────────────┬─────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ ⚠️ WARNUNG │ +│ │ +│ "Sie haben ungespeicherte Änderungen." │ +│ │ +│ [Verwerfen] [Abbrechen] [Speichern] │ +└─────────────────────────────────────────┘ +``` + +--- + +## Zusammenfassung: Wichtigste Navigations-Buttons + +| Button / Element | Aktion | Führt zu | +|------------------------------|-----------------------------------|-------------------------------------------------------| +| **LOGIN** | | | +| `[Login]` | Anmelden | Hauptansicht mit Drawer | +| **DRAWER** | | | +| `[Neue Veranstaltung]` | Erstellt neue Veranstaltung | `/veranstaltung/neu` (5 Tabs, startet auf Stammdaten) | +| `[Veranstaltung X]` | Öffnet Veranstaltung | `/veranstaltung/:id` (nur Übersicht-Tab) | +| `[Logout]` | Abmelden | Login-Seite | +| **VERANSTALTUNG-ÜBERSICHT** | | | +| `[Neues Turnier]` | Erstellt Turnier in Veranstaltung | `/veranstaltung/:id/turnier/neu` (5 Tabs) | +| `[Turnier X] → Öffnen` | Öffnet bestehendes Turnier | `/veranstaltung/:id/turnier/:nr` (5 Tabs) | +| **BEWERBE-TAB** | | | +| `[Änderungen Speichern]` | Speichert alle Änderungen | Backend-Call (zukünftig) | +| `[Änderungen Rückgängig]` | Macht Änderungen rückgängig | Undo-Funktion (zukünftig) | +| `[Bewerb Einfügen]` | Fügt neuen Bewerb hinzu | Neue Zeile in Tabelle | +| `[Bewerb Löschen]` | Löscht ausgewählten Bewerb | Zeile wird entfernt | +| `[Bewerb Teilen]` | Dupliziert Bewerb | Kopie in Tabelle | +| `[↑ Nach oben]` | Verschiebt Bewerb | Reihenfolge in Tabelle | +| `[↓ Nach unten]` | Verschiebt Bewerb | Reihenfolge in Tabelle | +| `[Startliste Bearbeiten]` | Öffnet Editor | Startlisten-Editor (zukünftig) | +| `[Startliste Drucken]` | Öffnet Druckdialog | PDF-Export (zukünftig) | +| `[Ergebnisliste Bearbeiten]` | Öffnet Editor | Ergebnislisten-Editor (zukünftig) | +| `[Ergebnisliste Drucken]` | Öffnet Druckdialog | PDF-Export (zukünftig) | +| **BEWERBE-TABELLE** | | | +| `[Tabellenzeile klicken]` | Wählt Bewerb aus | Details rechts anzeigen | +| **KONFIGURATIONS-TABS** | | | +| `[Tab: Bewerb]` | Zeigt Grunddaten | Bewerb-Felder | +| `[Tab: Bewertung]` | Zeigt Bewertung | Richter-Konfiguration | +| `[Tab: Geldpreise]` | Zeigt Geldpreise | Preisliste | +| `[Tab: Ort/Zeit]` | Zeigt Zeitplan | Ort/Zeit-Felder | +| `[Button: ...]` (bei Logo) | Dateiauswahl | File-Dialog (zukünftig) | + +--- + +## Visueller Überblick: Route-Hierarchy + +``` +/ +│ +├─ /veranstaltung/neu +│ └─ [5 Tabs: Übersicht, Stammdaten*, Organisation, Bewerbe(versteckt), Preisliste] +│ +├─ /veranstaltung/:id +│ ├─ [1 Tab: Übersicht] +│ └─ [Turniere-Section mit Button: Neues Turnier] +│ +├─ /veranstaltung/:veranstaltungId/turnier/neu +│ └─ [5 Tabs: Übersicht, Stammdaten, Organisation, Bewerbe*, Preisliste] +│ +└─ /veranstaltung/:veranstaltungId/turnier/:nr + └─ [5 Tabs: Übersicht, Stammdaten, Organisation, Bewerbe*, Preisliste] + │ + └─ Bewerbe-Tab: + ├─ Linke Sidebar: Aktions-Buttons (11 Buttons) + ├─ Mitte: Tabelle (klickbare Zeilen) + └─ Rechts: 4 Konfigurations-Tabs + ├─ Tab 1: Bewerb (14 Felder) + ├─ Tab 2: Bewertung (7 Felder + Richter-Liste) + ├─ Tab 3: Geldpreise (5 Felder + Tabelle) + └─ Tab 4: Ort/Zeit (8 Felder) +``` + +**Legende:** + +- `*` = Standard-Tab beim Öffnen +- `→` = Navigiert zu +- `├─` = Hat +- `└─` = Zeigt/Führt zu + +--- + +**Hinweis**: Dieses Diagramm zeigt die aktuelle Prototyp-Version. Zukünftige Features (Drucken, Export, erweiterte +Validierung) sind mit "(zukünftig)" markiert. diff --git a/docs/06_Frontend/FIGMA/Vision_03/ATTRIBUTIONS.md b/docs/06_Frontend/FIGMA/Vision_03/ATTRIBUTIONS.md new file mode 100644 index 00000000..ce6bb5a6 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/ATTRIBUTIONS.md @@ -0,0 +1,5 @@ +This Figma Make file includes components from [shadcn/ui](https://ui.shadcn.com/) used +under [MIT license](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md). + +This Figma Make file includes photos from [Unsplash](https://unsplash.com) used +under [license](https://unsplash.com/license). diff --git a/docs/06_Frontend/FIGMA/Vision_03/NAVIGATION.md b/docs/06_Frontend/FIGMA/Vision_03/NAVIGATION.md new file mode 100644 index 00000000..c338bdf8 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/NAVIGATION.md @@ -0,0 +1,529 @@ +# Navigation & Benutzerfluss-Diagramm + +## Übersicht: Haupt-Navigation + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ LOGIN-SEITE │ +│ │ +│ ┌────────────────────────────────────────┐ │ +│ │ Username: admin │ │ +│ │ Passwort: Admin#1234 │ │ +│ │ [Login] ──────────────────────────────────────────┐ │ +│ └────────────────────────────────────────┘ │ │ +└─────────────────────────────────────────────────────────┼───────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ HAUPTANSICHT (AdminDrawer) │ +│ │ +│ ┌──────────────────┬──────────────────────────────────────────────────┐ │ +│ │ DRAWER (Links) │ MAIN CONTENT (Rechts) │ │ +│ │ │ │ │ +│ │ ○ Veranstaltungen ──────► [Veranstaltungs-Seiten] │ │ +│ │ ○ Reiter │ │ │ +│ │ ○ Pferde │ │ │ +│ │ ○ Funktionäre │ │ │ +│ │ ○ Meisterschaften │ │ +│ │ ○ Cups │ │ │ +│ │ │ │ │ +│ │ [Logout] │ │ │ +│ └──────────────────┴──────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Detaillierter Navigationsbaum + +``` +HAUPTANSICHT (/) +│ +├─ DRAWER NAVIGATION (links) +│ │ +│ ├─ 📁 Veranstaltungen +│ │ │ +│ │ ├─ [Button: Neue Veranstaltung] +│ │ │ └─► /veranstaltung/neu +│ │ │ │ +│ │ │ └─► VERANSTALTUNGS-ANSICHT (Neue) +│ │ │ ├─ Tab: Veranstaltung - Übersicht +│ │ │ ├─ Tab: Stammdaten (A-Satz) ← STANDARDTAB +│ │ │ ├─ Tab: Organisation +│ │ │ └─ Tab: Preisliste +│ │ │ +│ │ ├─ [Veranstaltung 1] ► Turnier Pfingsten 2023 +│ │ │ └─► /veranstaltung/1 +│ │ │ │ +│ │ │ └─► VERANSTALTUNGS-ANSICHT (Bestehende) +│ │ │ └─ Tab: Veranstaltung - Übersicht (EINZIGER TAB) +│ │ │ │ +│ │ │ └─ TURNIERE-SECTION +│ │ │ │ +│ │ │ ├─ [Button: Neues Turnier] +│ │ │ │ └─► /veranstaltung/1/turnier/neu +│ │ │ │ │ +│ │ │ │ └─► TURNIER-ANSICHT (Neu) +│ │ │ │ ├─ Tab: Veranstaltung - Übersicht +│ │ │ │ ├─ Tab: Stammdaten (A-Satz) +│ │ │ │ ├─ Tab: Organisation +│ │ │ │ ├─ Tab: Bewerbe ⭐ HAUPTSEITE +│ │ │ │ └─ Tab: Preisliste +│ │ │ │ +│ │ │ └─ TURNIER-LISTE +│ │ │ │ +│ │ │ ├─ [Turnier 1] (zum Öffnen klicken) +│ │ │ │ └─► /veranstaltung/1/turnier/1 +│ │ │ │ │ +│ │ │ │ └─► TURNIER-ANSICHT (Bestehend) +│ │ │ │ └─ [Alle 5 Tabs wie oben] +│ │ │ │ +│ │ │ ├─ [Turnier 2] (zum Öffnen klicken) +│ │ │ │ └─► /veranstaltung/1/turnier/2 +│ │ │ │ +│ │ │ └─ [Turnier 3] (zum Öffnen klicken) +│ │ │ └─► /veranstaltung/1/turnier/3 +│ │ │ +│ │ ├─ [Veranstaltung 2] ► Sommerturnier 2023 +│ │ │ └─► /veranstaltung/2 +│ │ │ └─► [gleiche Struktur wie Veranstaltung 1] +│ │ │ +│ │ └─ [Veranstaltung 3] ► Herbstturnier 2023 +│ │ └─► /veranstaltung/3 +│ │ └─► [gleiche Struktur wie Veranstaltung 1] +│ │ +│ ├─ 📁 Reiter (nicht implementiert) +│ ├─ 📁 Pferde (nicht implementiert) +│ ├─ 📁 Funktionäre (nicht implementiert) +│ ├─ 📁 Meisterschaften (nicht implementiert) +│ ├─ 📁 Cups (nicht implementiert) +│ │ +│ └─ [Button: Logout] +│ └─► Zurück zur Login-Seite +│ +└─ MAIN CONTENT AREA (rechts) + └─► Zeigt jeweils die ausgewählte Seite/Tab +``` + +--- + +## BEWERBE-TAB - Detail-Navigation ⭐ + +Die wichtigste Seite der Anwendung! + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ BEWERBE-TAB (/veranstaltung/:id/turnier/:nr) │ +│ │ +│ ┌─────────────┬──────────────────────────┬──────────────────────────────┐ │ +│ │ AKTIONEN │ BEWERBS-ÜBERSICHT │ BEWERB-KONFIGURATION │ │ +│ │ (150px) │ (50%) │ (50%) │ │ +│ ├─────────────┼──────────────────────────┼──────────────────────────────┤ │ +│ │ │ │ │ │ +│ │ [Änderungen │ ┌────────────────────┐ │ ┌──────────────────────┐ │ │ +│ │ Speichern] │ │ TOOLBAR │ │ │ TABS │ │ │ +│ │ │ │ │ • Aktualisieren │ │ │ ○ Bewerb │ │ │ +│ │ └──────►│ │ • 12 Bewerbe │ │ │ ○ Bewertung │ │ │ +│ │ (Speichert│ │ • Filtern │ │ │ ○ Geldpreise │ │ │ +│ │ alle) │ └────────────────────┘ │ │ ○ Ort/Zeit │ │ │ +│ │ │ │ └──────────────────────┘ │ │ +│ │ [Änderungen │ ┌────────────────────┐ │ │ │ +│ │ Rückgängig]│ │ TABELLE │ │ [Tab-Content hier] │ │ +│ │ │ │ │ ┌─┬───┬───┬──────┐│ │ │ │ +│ │ └──────►│ │ │T│Pl.│Bew│ ... ││ │ ← Zeigt Details des │ │ +│ │ (Undo) │ │ │a│a │er │ ││ │ ausgewählten Bewerbs │ │ +│ │ │ │ │g│tz │b │ ││ │ │ │ +│ ├─────────────┤ │ │ │ │ │ ││ │ ← Interaktive Felder │ │ +│ │ │ │ └─┴───┴───┴──────┘│ │ │ │ +│ │ [Bewerb │ │ ▲ │ │ ← Speichern pro Feld │ │ +│ │ Einfügen] │ │ │ Klick wählt │ │ │ │ +│ │ │ │ │ │ Bewerb aus │ │ │ │ +│ │ └──────►│ │ │ │ │ │ │ +│ │ (Fügt │ │ └────────────────►│ │ │ │ +│ │ Zeile │ │ Zeigt Details │ │ │ │ +│ │ hinzu) │ │ rechts → │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ [Bewerb │ └────────────────────┘ └──────────────────────────────┘ │ +│ │ Löschen] │ │ +│ │ │ │ │ +│ │ └──────►│ (Löscht ausgewählten Bewerb) │ +│ │ │ │ +│ │ [Bewerb │ │ +│ │ Teilen] │ (Dupliziert ausgewählten Bewerb) │ +│ │ │ │ │ +│ │ └──────►│ │ +│ ├─────────────┤ │ +│ │ │ │ +│ │ [Bewerb nach│ (Verschiebt in Tabelle nach oben) │ +│ │ oben vers.]│ │ │ +│ │ │ │ └──────► Ändert Reihenfolge │ +│ │ └──────►│ │ +│ │ │ │ +│ │ [Bewerb nach│ (Verschiebt in Tabelle nach unten) │ +│ │ unten vers]│ │ │ +│ │ │ │ └──────► Ändert Reihenfolge │ +│ │ └──────►│ │ +│ ├─────────────┤ │ +│ │ │ │ +│ │ [Startliste │ (Öffnet Startlisten-Editor - noch nicht implementiert) │ +│ │ Bearbeiten]│ │ +│ │ │ │ +│ │ [Startliste │ (Öffnet Druck-Dialog - noch nicht implementiert) │ +│ │ Drucken] │ │ +│ ├─────────────┤ │ +│ │ │ │ +│ │ [Ergebnislst│ (Öffnet Ergebnislisten-Editor - noch nicht implementiert) │ +│ │ Bearbeiten]│ │ +│ │ │ │ +│ │ [Ergebnislst│ (Öffnet Druck-Dialog - noch nicht implementiert) │ +│ │ Drucken] │ │ +│ └─────────────┴──────────────────────────┴──────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Tab-Wechsel in Bewerb-Konfiguration + +``` +BEWERB-KONFIGURATION (Rechte Seite im Bewerbe-Tab) +│ +├─ TAB 1: Bewerb (Grunddaten) +│ │ +│ ├─ [Feld: Nummer] ────► Text ändern → Speichern bei "Änderungen Speichern" +│ ├─ [Feld: Abteilung] ► Text ändern → Speichern bei "Änderungen Speichern" +│ ├─ [Feld: Typ] ──────► Text ändern → Speichern bei "Änderungen Speichern" +│ ├─ [Feld: Name] ─────► Text ändern → Speichern bei "Änderungen Speichern" +│ ├─ [Feld: Bezeichnung] Text ändern → Speichern bei "Änderungen Speichern" +│ ├─ [Dropdown: Kategorie] Auswahl ändern +│ ├─ [Dropdown: Klasse] ─► Auswahl ändern +│ ├─ [Dropdown: Lizenz] ─► Auswahl ändern +│ ├─ [Feld: Maximal] ───► Zahl ändern (Pferde je Reiter) +│ ├─ [Dropdown: Pferdealter] Auswahl ändern +│ ├─ [Feld: Zeile 1] ───► Text ändern (z.B. "Pony Einsteiger Cup OÖ") +│ ├─ [Feld: Zeile 2] ───► Text ändern +│ ├─ [Feld: Zeile 3] ───► Text ändern +│ └─ [Feld: Logo Bewerb + Button "..."] +│ └─► Button öffnet Dateiauswahl (noch nicht implementiert) +│ +├─ TAB 2: Bewertung +│ │ +│ ├─ [Feld: Prüfung] ───────► Text ändern +│ ├─ [Feld: Richtverfahren] ► Text ändern (z.B. "A") +│ ├─ [Feld: Para-Grade] ────► Text ändern +│ ├─ [Feld: Richteranzahl] ─► Zahl ändern +│ ├─ [Feld: Aufgabe] ───────► Text ändern (z.B. "Aufgabe R") +│ ├─ [Feld: Aufgabennummer] ► Text ändern +│ ├─ [Feld: Maximalpunkte] ─► Zahl ändern +│ │ +│ └─ RICHTER-LISTE (dynamisch) +│ │ +│ ├─ Richter 1 +│ │ ├─ [Feld: Position] ─► Text ändern (z.B. "C") +│ │ ├─ [Feld: Name] ─────► Text ändern (z.B. "Schuster Alexandra") +│ │ └─ [Checkbox: Aktiv] ► An/Aus +│ │ +│ ├─ Richter 2 +│ │ ├─ [Feld: Position] ─► Text ändern (z.B. "C") +│ │ ├─ [Feld: Name] ─────► Text ändern (z.B. "Vankova Kamila (CZ)") +│ │ └─ [Checkbox: Aktiv] ► An/Aus +│ │ +│ └─ ... (weitere Richter) +│ +├─ TAB 3: Geldpreise +│ │ +│ ├─ SECTION: Geldpreis +│ │ ├─ [Checkbox: Geldpreis] ──────────► An/Aus +│ │ ├─ [Feld: Startgeld] ─────────────► Text ändern (z.B. "15,00") +│ │ └─ [Dropdown: Auszahlung] ────────► Auswahl (fortführend, 1/3, 1/4, 1/5) +│ │ +│ ├─ SECTION: Geldpreis für Kadererreiter +│ │ ├─ [Checkbox: Geldpreis für Kadererreiter] ► An/Aus +│ │ └─ [Feld: Startgeld für Kadererreiter] ───► Text ändern (z.B. "15,00") +│ │ +│ ├─ [Dropdown: Geldpreisvorlage wählen] ──────► Auswahl (Vorlagen) +│ │ │ +│ │ └──► Füllt Geldpreise-Tabelle automatisch +│ │ +│ └─ TABELLE: Geldpreise +│ │ +│ ├─ Spalte: Nummer +│ ├─ Spalte: Geldpreis +│ └─ [Zeigt "0 Geldpreise" wenn leer] +│ +└─ TAB 4: Ort/Zeit + │ + ├─ [Dropdown: Tag] ─────────────► Auswahl (28.05.2023, ...) + ├─ [Dropdown: Beginnzeit] ──────► Auswahl (fix um, nicht vor, ca.) + ├─ [Feld: Zeit] ────────────────► Text ändern (Format: hh:mm, z.B. "08:00") + ├─ [Feld: Reitdauer] ───────────► Text ändern (Format: mm:ss, z.B. "02:00") + ├─ [Feld: Umbau] ───────────────► Text ändern (in Minuten, z.B. "10") + ├─ [Feld: Besichtigung] ────────► Text ändern (in Minuten, z.B. "10") + ├─ [Feld: Stechen] ─────────────► Text ändern (in Minuten, leer möglich) + └─ [Dropdown: Platz] ───────────► Auswahl (Vorderer Turnierplatz, Hauptplatz, ...) +``` + +--- + +## Interaktionsfluss: Veranstaltung → Turnier → Bewerb + +``` +SCHRITT 1: Veranstaltung erstellen +┌────────────────────────────────────────┐ +│ Drawer: [Neue Veranstaltung] │ +└────────────────┬───────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ /veranstaltung/neu │ +│ │ +│ Tabs sichtbar: │ +│ • Veranstaltung - Übersicht │ +│ • Stammdaten ← STARTET HIER │ +│ • Organisation │ +│ • Preisliste │ +│ │ +│ [Daten eingeben: Name, Ort, Datum...] │ +│ [Speichern-Button] │ +└────────────────┬───────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ Veranstaltung gespeichert │ +│ → Erscheint in Drawer-Liste │ +└────────────────┬───────────────────────┘ + │ + ▼ +SCHRITT 2: Turnier erstellen +┌────────────────────────────────────────┐ +│ Drawer: [Veranstaltung 1] klicken │ +└────────────────┬───────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ /veranstaltung/1 │ +│ │ +│ Tab: Veranstaltung - Übersicht │ +│ │ +│ ┌────────────────────────────────────┐ │ +│ │ TURNIERE-SECTION │ │ +│ │ [Button: Neues Turnier] ←─ KLICK │ │ +│ └────────────────────────────────────┘ │ +└────────────────┬───────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ /veranstaltung/1/turnier/neu │ +│ │ +│ Tabs sichtbar: │ +│ • Veranstaltung - Übersicht │ +│ • Stammdaten │ +│ • Organisation │ +│ • Bewerbe ← WICHTIGSTE SEITE │ +│ • Preisliste │ +│ │ +│ [Daten eingeben: Turniername...] │ +│ [Speichern-Button] │ +└────────────────┬───────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ Turnier gespeichert │ +│ → Erscheint in Turnier-Liste │ +│ unter Veranstaltung 1 │ +└────────────────┬───────────────────────┘ + │ + ▼ +SCHRITT 3: Bewerbe konfigurieren +┌────────────────────────────────────────┐ +│ Drawer: [Turnier 1] "Öffnen" klicken │ +└────────────────┬───────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ /veranstaltung/1/turnier/1 │ +│ │ +│ [Tab "Bewerbe" auswählen] │ +└────────────────┬───────────────────────┘ + │ + ▼ +┌────────────────────────────────────────┐ +│ BEWERBE-TAB │ +│ │ +│ 1. [Bewerb Einfügen] klicken │ +│ → Neue Zeile in Tabelle │ +│ │ +│ 2. Bewerb in Tabelle auswählen │ +│ → Details erscheinen rechts │ +│ │ +│ 3. Tabs durchgehen: │ +│ • Bewerb (Grunddaten eingeben) │ +│ • Bewertung (Richter hinzufügen) │ +│ • Geldpreise (Startgeld festlegen) │ +│ • Ort/Zeit (Zeitplan konfigurieren) │ +│ │ +│ 4. [Änderungen Speichern] klicken │ +│ │ +│ 5. Weitere Bewerbe hinzufügen... │ +└────────────────────────────────────────┘ +``` + +--- + +## Tastatur-Navigation (geplant) + +``` +GLOBALE SHORTCUTS (zukünftig): +• Ctrl+S / Cmd+S ──► Speichern +• Ctrl+Z / Cmd+Z ──► Rückgängig +• Ctrl+N / Cmd+N ──► Neuer Bewerb +• Tab ────────────► Nächstes Feld +• Shift+Tab ──────► Vorheriges Feld +• Pfeiltasten ────► Navigation in Tabellen +• Enter ──────────► Zeile öffnen/bestätigen +• Esc ────────────► Dialog schließen + +BEWERBE-TAB SHORTCUTS: +• Ctrl+↑ ─────────► Bewerb nach oben +• Ctrl+↓ ─────────► Bewerb nach unten +• Ctrl+D ─────────► Bewerb duplizieren +• Delete ─────────► Bewerb löschen (mit Bestätigung) +• Ctrl+1-4 ───────► Tab-Wechsel (Bewerb/Bewertung/Geldpreise/Ort-Zeit) +``` + +--- + +## Fehlerbehandlung & Dialoge (zukünftig) + +``` +AKTIONEN MIT BESTÄTIGUNG: +┌─────────────────────────────────────────┐ +│ [Bewerb Löschen] geklickt │ +└───────────────┬─────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ ⚠️ BESTÄTIGUNGS-DIALOG │ +│ │ +│ "Bewerb 5 wirklich löschen?" │ +│ │ +│ [Abbrechen] [Löschen] ←────────────────┼──► Bewerb wird gelöscht +└─────────────────────────────────────────┘ + +SPEICHERN MIT VALIDIERUNG: +┌─────────────────────────────────────────┐ +│ [Änderungen Speichern] geklickt │ +└───────────────┬─────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ Validierung läuft... │ +│ │ +│ ✓ Alle Pflichtfelder ausgefüllt? │ +│ ✓ Zeitformat korrekt? │ +│ ✓ Nummern-Duplikate? │ +└───────────────┬─────────────────────────┘ + │ + ├──► OK ──► Speichern erfolgreich ✓ + │ + └──► Fehler ──► ❌ FEHLER-DIALOG + │ + │ "Bitte korrigieren Sie:" + │ • Feld "Nummer" ist leer + │ • Zeit-Format ungültig + │ + └─► [OK] + +UNGESPEICHERTE ÄNDERUNGEN: +┌─────────────────────────────────────────┐ +│ Benutzer verlässt Seite (z.B. klickt │ +│ auf anderen Tab oder Turnier) │ +└───────────────┬─────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────┐ +│ ⚠️ WARNUNG │ +│ │ +│ "Sie haben ungespeicherte Änderungen." │ +│ │ +│ [Verwerfen] [Abbrechen] [Speichern] │ +└─────────────────────────────────────────┘ +``` + +--- + +## Zusammenfassung: Wichtigste Navigations-Buttons + +| Button / Element | Aktion | Führt zu | +|------------------------------|-----------------------------------|-------------------------------------------------------| +| **LOGIN** | | | +| `[Login]` | Anmelden | Hauptansicht mit Drawer | +| **DRAWER** | | | +| `[Neue Veranstaltung]` | Erstellt neue Veranstaltung | `/veranstaltung/neu` (5 Tabs, startet auf Stammdaten) | +| `[Veranstaltung X]` | Öffnet Veranstaltung | `/veranstaltung/:id` (nur Übersicht-Tab) | +| `[Logout]` | Abmelden | Login-Seite | +| **VERANSTALTUNG-ÜBERSICHT** | | | +| `[Neues Turnier]` | Erstellt Turnier in Veranstaltung | `/veranstaltung/:id/turnier/neu` (5 Tabs) | +| `[Turnier X] → Öffnen` | Öffnet bestehendes Turnier | `/veranstaltung/:id/turnier/:nr` (5 Tabs) | +| **BEWERBE-TAB** | | | +| `[Änderungen Speichern]` | Speichert alle Änderungen | Backend-Call (zukünftig) | +| `[Änderungen Rückgängig]` | Macht Änderungen rückgängig | Undo-Funktion (zukünftig) | +| `[Bewerb Einfügen]` | Fügt neuen Bewerb hinzu | Neue Zeile in Tabelle | +| `[Bewerb Löschen]` | Löscht ausgewählten Bewerb | Zeile wird entfernt | +| `[Bewerb Teilen]` | Dupliziert Bewerb | Kopie in Tabelle | +| `[↑ Nach oben]` | Verschiebt Bewerb | Reihenfolge in Tabelle | +| `[↓ Nach unten]` | Verschiebt Bewerb | Reihenfolge in Tabelle | +| `[Startliste Bearbeiten]` | Öffnet Editor | Startlisten-Editor (zukünftig) | +| `[Startliste Drucken]` | Öffnet Druckdialog | PDF-Export (zukünftig) | +| `[Ergebnisliste Bearbeiten]` | Öffnet Editor | Ergebnislisten-Editor (zukünftig) | +| `[Ergebnisliste Drucken]` | Öffnet Druckdialog | PDF-Export (zukünftig) | +| **BEWERBE-TABELLE** | | | +| `[Tabellenzeile klicken]` | Wählt Bewerb aus | Details rechts anzeigen | +| **KONFIGURATIONS-TABS** | | | +| `[Tab: Bewerb]` | Zeigt Grunddaten | Bewerb-Felder | +| `[Tab: Bewertung]` | Zeigt Bewertung | Richter-Konfiguration | +| `[Tab: Geldpreise]` | Zeigt Geldpreise | Preisliste | +| `[Tab: Ort/Zeit]` | Zeigt Zeitplan | Ort/Zeit-Felder | +| `[Button: ...]` (bei Logo) | Dateiauswahl | File-Dialog (zukünftig) | + +--- + +## Visueller Überblick: Route-Hierarchy + +``` +/ +│ +├─ /veranstaltung/neu +│ └─ [5 Tabs: Übersicht, Stammdaten*, Organisation, Bewerbe(versteckt), Preisliste] +│ +├─ /veranstaltung/:id +│ ├─ [1 Tab: Übersicht] +│ └─ [Turniere-Section mit Button: Neues Turnier] +│ +├─ /veranstaltung/:veranstaltungId/turnier/neu +│ └─ [5 Tabs: Übersicht, Stammdaten, Organisation, Bewerbe*, Preisliste] +│ +└─ /veranstaltung/:veranstaltungId/turnier/:nr + └─ [5 Tabs: Übersicht, Stammdaten, Organisation, Bewerbe*, Preisliste] + │ + └─ Bewerbe-Tab: + ├─ Linke Sidebar: Aktions-Buttons (11 Buttons) + ├─ Mitte: Tabelle (klickbare Zeilen) + └─ Rechts: 4 Konfigurations-Tabs + ├─ Tab 1: Bewerb (14 Felder) + ├─ Tab 2: Bewertung (7 Felder + Richter-Liste) + ├─ Tab 3: Geldpreise (5 Felder + Tabelle) + └─ Tab 4: Ort/Zeit (8 Felder) +``` + +**Legende:** + +- `*` = Standard-Tab beim Öffnen +- `→` = Navigiert zu +- `├─` = Hat +- `└─` = Zeigt/Führt zu + +--- + +**Hinweis**: Dieses Diagramm zeigt die aktuelle Prototyp-Version. Zukünftige Features (Drucken, Export, erweiterte +Validierung) sind mit "(zukünftig)" markiert. diff --git a/docs/06_Frontend/FIGMA/Vision_03/README.md b/docs/06_Frontend/FIGMA/Vision_03/README.md new file mode 100644 index 00000000..f3fb1fab --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/README.md @@ -0,0 +1,1050 @@ +# Turnierverwaltungs-Anwendung - Frontend Prototyp + +## Projektübersicht + +Dies ist ein professioneller Prototyp einer Turnierverwaltungs-Anwendung für den österreichischen Pferdesportverband ( +ÖPS). Die Anwendung ist als **Desktop-First-Anwendung** konzipiert und bietet eine kompakte, tastaturoptimierte +Benutzeroberfläche zur Verwaltung von Veranstaltungen, Turnieren und Bewerben im Pferdesport. + +### Hauptmerkmale + +- **Desktop-optimierte UI**: Fokus auf kompakte Layouts und effiziente Datenerfassung +- **Hierarchische Datenstruktur**: Veranstalter (Verein) → Veranstaltungen → Turniere → Bewerbe +- **Veranstalter-Verwaltung**: Admin legt Veranstalter an → Veranstalter erhält Login → Veranstalter verwaltet eigene + Veranstaltungen +- **Material Design 3**: Moderne UI mit Primärfarbe Indigo (#3F51B5) +- **Tastaturoptimiert**: Effiziente Navigation und Dateneingabe +- **OETO-Ausschreibungs-Standard**: Tab-Struktur folgt österreichischen Richtlinien + +--- + +## Technologie-Stack + +### Core Technologies + +- **React 18** - UI Framework +- **TypeScript** - Type-safe JavaScript +- **React Router** (Data Mode) - Client-side Routing +- **Material-UI (MUI) v6** - Component Library +- **Vite** - Build Tool & Development Server + +### Styling + +- **Material-UI System** - Sx Props für Styling +- **Tailwind CSS v4** - Utility Classes (sekundär) +- **Material Design 3** - Design Language + +### Package Manager + +- **pnpm** - Fast, disk space efficient package manager + +--- + +## Projektstruktur + +``` +/ +├── src/ +│ ├── app/ +│ │ ├── components/ +│ │ │ ├── veranstaltung/ +│ │ │ │ ├── StammdatenTab.tsx # A-Satz / Stammdaten +│ │ │ │ ├── OrganisationTab.tsx # Funktionäre & Plätze +│ │ │ │ ├── PreislisteTab.tsx # Preisliste +│ │ │ │ └── UebersichtTab.tsx # Transfer/Übersicht +│ │ │ ├── turnier/ +│ │ │ │ └── BewerbeTab.tsx # Bewerbe-Verwaltung (Hauptseite) +│ │ │ ├── AdminDrawer.tsx # Haupt-Navigation +│ │ │ ├── VeranstaltungAnsicht.tsx # Veranstaltungs-View +│ │ │ └── TurnierAnsicht.tsx # Turnier-View +│ │ ├── routes.tsx # React Router Konfiguration +│ │ └── App.tsx # Root Component +│ ├── styles/ +│ │ ├── theme.css # CSS Variables & Theme +│ │ └── fonts.css # Font Imports +│ └── main.tsx # Entry Point +├── package.json +└── README.md +``` + +--- + +## Installation & Setup + +### Voraussetzungen + +- **Node.js** >= 18.x +- **pnpm** >= 8.x (empfohlen) oder npm + +### Installation + +```bash +# Repository klonen +git clone +cd turnierverwaltung + +# Dependencies installieren +pnpm install + +# Development Server starten +pnpm dev + +# Build für Production +pnpm build + +# Preview Production Build +pnpm preview +``` + +### Verfügbare Scripts + +```json +{ + "dev": "vite", // Development Server auf http://localhost:5173 + "build": "vite build", // Production Build + "preview": "vite preview" // Preview Production Build +} +``` + +--- + +## Architektur & Konzepte + +### 1. Routing-System (React Router Data Mode) + +Die Anwendung verwendet React Router's Data Mode Pattern mit einer klar definierten Route-Hierarchie: + +```typescript +// src/app/routes.tsx +const router = createBrowserRouter([ + { + path: "/", + Component: Root, + children: [ + // Neue Veranstaltung + { + path: "veranstaltung/neu", + Component: VeranstaltungAnsicht + }, + + // Bestehende Veranstaltung + { + path: "veranstaltung/:id", + Component: VeranstaltungAnsicht + }, + + // Neues Turnier in Veranstaltung + { + path: "veranstaltung/:veranstaltungId/turnier/neu", + Component: TurnierAnsicht + }, + + // Bestehendes Turnier + { + path: "veranstaltung/:veranstaltungId/turnier/:nr", + Component: TurnierAnsicht + }, + + // 404 Fallback + { + path: "*", + Component: NotFound + } + ] + } +]); +``` + +**Wichtig**: Verwenden Sie immer das `react-router` Package (nicht `react-router-dom`), da die Anwendung in einer +speziellen Umgebung läuft. + +--- + +### 2. Navigation & Benutzerfluss + +#### Hauptnavigation: AdminDrawer + +Die Anwendung verwendet eine **Drawer-Navigation** (links) mit folgenden Bereichen: + +``` +Admin - Verwaltung +├── Veranstaltungen +│ ├── Neue Veranstaltung → /veranstaltung/neu +│ └── [Liste Veranstaltungen] → /veranstaltung/:id +│ └── Turniere +│ ├── Neues Turnier → /veranstaltung/:id/turnier/neu +│ └── [Turnier-Liste] → /veranstaltung/:id/turnier/:nr +└── ... +``` + +#### Login-System + +- **Demo Credentials**: + - Username: `admin` + - Passwort: `Admin#1234` +- Login-State wird im `localStorage` gespeichert +- Keine Backend-Integration im Prototyp + +--- + +### 3. Tab-Struktur (OETO-Standard) + +#### Veranstaltungs-Tabs (Neue Veranstaltung) + +Bei einer **neuen Veranstaltung** sind alle 5 Tabs sichtbar: + +1. **Veranstaltung - Übersicht** (ehemals "Transfer") +2. **Stammdaten** (A-Satz) ← Standardtab beim Erstellen +3. **Organisation** (Funktionäre + Plätze) +4. **Bewerbe** (wird versteckt, da turnierspezifisch) +5. **Preisliste** + +#### Veranstaltungs-Tabs (Bestehende Veranstaltung) + +Bei einer **bestehenden Veranstaltung** wird nur der Übersicht-Tab angezeigt: + +1. **Veranstaltung - Übersicht** + +**Grund**: Turnierspezifische Daten (Stammdaten, Organisation, Bewerbe, Preisliste) werden nur auf Turnier-Ebene +bearbeitet. + +#### Turnier-Tabs + +Wenn ein Turnier geöffnet wird, sind alle 5 Tabs sichtbar: + +1. **Veranstaltung - Übersicht** (Read-only, zeigt Veranstaltungs-Info) +2. **Stammdaten** (A-Satz) +3. **Organisation** (Funktionäre + Plätze) +4. **Bewerbe** ⭐ **Wichtigste Seite der Anwendung** +5. **Preisliste** + +--- + +### 4. Bewerbe-Tab - Die Hauptseite + +Der **Bewerbe-Tab** ist die zentrale Konfigurationsseite des gesamten Systems. Er ist in 3 Bereiche aufgeteilt: + +``` +┌─────────────┬───────────────────────┬───────────────────────┐ +│ Aktionen │ Bewerbs-Übersicht │ Bewerb-Konfiguration │ +│ (150px) │ (50%) │ (50%) │ +└─────────────┴───────────────────────┴──────────────────────┘ +``` + +#### Links: Aktionen (150px Sidebar) + +Buttons für Bewerbs-Management: + +- **Änderungen Speichern** / **Änderungen Rückgängig** +- **Bewerb Einfügen** / **Bewerb Löschen** / **Bewerb Teilen** +- **Bewerb nach oben/unten verschieben** +- **Startliste Bearbeiten** / **Startliste Drucken** +- **Ergebnisliste Bearbeiten** / **Ergebnisliste Drucken** + +#### Mitte: Bewerbs-Übersicht (50%) + +**Toolbar**: + +- Button: Aktualisieren +- Button: X Bewerbe (zeigt Anzahl) +- Button: Filtern + +**Tabelle** mit folgenden Spalten: + +- **Tag** (Datum) +- **Platz** (Platz-Nummer) +- **Bewerb** (Bewerb-Nummer) +- **Beginn** (Uhrzeit) +- **Ende** (Uhrzeit) +- **Bewerbname** (mehrzeilig möglich) +- **ZNS** (Zusätzliche Nennung Startnummer) +- **Nennungen** (Anzahl Anmeldungen) + +**Features**: + +- Klickbare Zeilen zur Auswahl +- Hervorhebung: Bewerbe 5 & 6 haben gelben Hintergrund (`warning.50`) +- Selected State: Blau/Gelb-Orange je nach Bewerb + +#### Rechts: Bewerb-Konfiguration (50%) + +**4 Tabs** zur detaillierten Bewerbs-Konfiguration: + +##### Tab 1: Bewerb (Grunddaten) + +- Nummer +- Abteilung +- Typ (z.B. "Dressur") +- Name (z.B. "Dressurreiterprüfung") +- Bezeichnung (z.B. "Dressurreiterprüfung Reiterpass") +- Kategorie (Dropdown) +- Klasse (Dropdown) +- Lizenz (Dropdown) +- Maximal (Pferde je Reiter) +- Pferdealter (Dropdown) +- Zeile 1, 2, 3 (Zusatzinformationen wie "Pony Einsteiger Cup OÖ") +- Logo Bewerb (Dateipfad mit "..."-Button) + +##### Tab 2: Bewertung + +- Prüfung (z.B. "Dressurreiterprüfung") +- Richtverfahren (z.B. "A") +- Para-Grade +- Richteranzahl +- Aufgabe (z.B. "Aufgabe R") +- Aufgabennummer +- Maximalpunkte (Punkte je Richter) + +**Richter-Liste**: + +- Position (z.B. "C") +- Name (z.B. "Schuster Alexandra") +- Aktiv (Checkbox) + +##### Tab 3: Geldpreise + +**Section: Geldpreis** + +- Checkbox: Geldpreis +- Startgeld (z.B. "15,00") +- Auszahlung (Dropdown: fortführend, 1/3, 1/4, 1/5) + +**Section: Geldpreis für Kadererreiter** + +- Checkbox: Geldpreis für Kadererreiter +- Startgeld für Kadererreiter (z.B. "15,00") + +**Geldpreisvorlage wählen** (Dropdown) + +**Tabelle: Geldpreise** + +- Spalten: Nummer, Geldpreis +- Zeigt Anzahl der Geldpreise + +##### Tab 4: Ort/Zeit + +- Tag (Dropdown: Datum) +- Beginnzeit (Dropdown: "fix um", "nicht vor", "ca.") +- Zeit (Textfeld mit Format hh:mm) +- Reitdauer (Textfeld mit Format mm:ss) +- Umbau (Textfeld in Minuten) +- Besichtigung (Textfeld in Minuten) +- Stechen (Textfeld in Minuten) +- Platz (Dropdown: "Vorderer Turnierplatz", "Hauptplatz", etc.) + +--- + +## Datenstrukturen + +### Bewerb Interface + +```typescript +interface Bewerb { + id: number; + tag: string; // Tabellen-Datum + platz: number; // Platz-Nummer + bewerb: number; // Bewerb-Nummer + beginn: string; // Beginn-Zeit + ende: string; // End-Zeit + bewerbname: string; // Mehrzeiliger Name + zns: number; // ZNS + nennungen: number; // Anzahl Nennungen + + // Tab 1: Bewerb + nummer: string; + abteilung: string; + typ: string; + name: string; + bezeichnung: string; + kategorie: string; + klasse: string; + lizenz: string; + maximal: string; + pferdealter: string; + zeile1: string; + zeile2: string; + zeile3: string; + logoBewerbPfad: string; + + // Tab 2: Bewertung + prufung: string; + richtverfahren: string; + paraGrade: string; + richteranzahl: number; + aufgabe: string; + aufgabennr: string; + maximalPunkte: string; + richter: { + position: string; + name: string; + aktiv: boolean; + }[]; + + // Tab 3: Geldpreise + geldpreisAktiv: boolean; + startgeld: string; + auszahlung: string; + geldpreisKadererreiterAktiv: boolean; + startgeldKadererreiter: string; + geldpreisvorlage: string; + geldpreise: { + nummer: string; + betrag: string; + }[]; + + // Tab 4: Ort/Zeit + tagDatum: string; + beginnzeit: string; + beginnZeit: string; + reitdauer: string; + umbau: string; + besichtigung: string; + stechen: string; + platzName: string; +} +``` + +### Veranstaltung Interface + +```typescript +interface Veranstaltung { + id: string; + name: string; + von: string; // Datum von + bis: string; // Datum bis + ort: string; + status: string; + turniere: Turnier[]; +} +``` + +### Turnier Interface + +```typescript +interface Turnier { + nr: number; + name: string; + datum: string; + status: string; + bewerbe: Bewerb[]; +} +``` + +--- + +## Design-System + +### Farbschema (Material Design 3) + +**Primärfarbe**: Indigo (#3F51B5) + +```css +/* Theme Colors (src/styles/theme.css) */ +--primary-color: #3F51B5; +--primary-light: #757DE8; +--primary-dark: #002984; + +/* Semantic Colors */ +--background-default: #FAFAFA; +--background-paper: #FFFFFF; +--text-primary: rgba(0, 0, 0, 0.87); +--text-secondary: rgba(0, 0, 0, 0.60); +--divider: rgba(0, 0, 0, 0.12); + +/* Status Colors */ +--success-color: #4CAF50; +--warning-color: #FF9800; +--error-color: #F44336; +--info-color: #2196F3; +``` + +### Typografie + +- **Body Text**: 10px - 11px (sehr kompakt für Desktop) +- **Labels**: 10px, 600 Font Weight +- **Section Headers**: 11px - 13px, 600 Font Weight +- **Schriftart**: System Fonts (Roboto via MUI) + +### Spacing & Layout + +- **Kompakte Abstände**: 1-2 (8px - 16px) +- **Form-Felder**: + - Höhe: `small` size + - Padding: `py: 0.5` (4px) + - Font: 10px +- **Sidebar Width**: 150px (Aktionen-Sidebar im Bewerbe-Tab) +- **Drawer Width**: 280px (Haupt-Navigation) + +### Component-Sizing + +```typescript +// Standardgrößen +size="small" // Buttons, TextFields, Selects +sx={{ fontSize: '10px' }} // Text +sx={{ py: 0.5 }} // Input Padding +sx={{ gap: 1 }} // 8px Abstand +sx={{ gap: 1.5 }} // 12px Abstand +``` + +--- + +## MUI Theme Konfiguration + +Die Anwendung verwendet MUI's Default Theme mit angepasster Primärfarbe: + +```typescript +// src/main.tsx +import { createTheme, ThemeProvider } from '@mui/material/styles'; + +const theme = createTheme({ + palette: { + primary: { + main: '#3F51B5', // Indigo + }, + }, + components: { + MuiButton: { + styleOverrides: { + root: { + textTransform: 'none', // Keine Großbuchstaben + }, + }, + }, + }, +}); +``` + +--- + +## State Management + +### Aktuelle Implementierung (Prototyp) + +Der Prototyp verwendet **React Local State** mit `useState`: + +```typescript +// Beispiel: BewerbeTab.tsx +const [bewerbe, setBewerbe] = useState(mockBewerbe); +const [selectedBewerbId, setSelectedBewerbId] = useState(1); +const [detailTab, setDetailTab] = useState(0); +``` + +### Empfehlung für Production + +Für die Production-Version empfehlen wir: + +1. **React Context API** für globalen State (Login, aktuelle Veranstaltung/Turnier) +2. **Zustand** oder **Redux Toolkit** für komplexes State Management +3. **React Query** für Server-State und Caching +4. **localStorage/sessionStorage** für Persistenz + +Beispiel mit React Context: + +```typescript +// context/VeranstaltungContext.tsx +const VeranstaltungContext = createContext(null); + +export function VeranstaltungProvider({ children }: { children: ReactNode }) { + const [activeVeranstaltung, setActiveVeranstaltung] = useState(null); + const [activeTurnier, setActiveTurnier] = useState(null); + + return ( + + {children} + + ); +} +``` + +--- + +## Backend-Integration (TODO) + +### API Endpunkte (geplant) + +```typescript +// Veranstaltungen +GET /api/veranstaltungen +GET /api/veranstaltungen/:id +POST /api/veranstaltungen +PUT /api/veranstaltungen/:id +DELETE /api/veranstaltungen/:id + +// Turniere +GET /api/veranstaltungen/:veranstaltungId/turniere +GET /api/veranstaltungen/:veranstaltungId/turniere/:nr +POST /api/veranstaltungen/:veranstaltungId/turniere +PUT /api/veranstaltungen/:veranstaltungId/turniere/:nr +DELETE /api/veranstaltungen/:veranstaltungId/turniere/:nr + +// Bewerbe +GET /api/turniere/:turnierId/bewerbe +GET /api/turniere/:turnierId/bewerbe/:id +POST /api/turniere/:turnierId/bewerbe +PUT /api/turniere/:turnierId/bewerbe/:id +DELETE /api/turniere/:turnierId/bewerbe/:id + +// ÖPS Datasourcing +POST /api/ops/import/veranstaltung/:id +POST /api/ops/import/turnier/:id +``` + +### Authentifizierung + +```typescript +POST /api/auth/login +POST /api/auth/logout +GET /api/auth/me +POST /api/auth/refresh +``` + +--- + +## Entwicklungsrichtlinien + +### Code Style + +1. **TypeScript Strict Mode**: Aktiviert +2. **Naming Conventions**: + - Components: PascalCase (z.B. `BewerbeTab.tsx`) + - Functions: camelCase (z.B. `handleBewerbAendern`) + - Interfaces: PascalCase (z.B. `Bewerb`) + - CSS Classes: kebab-case (falls verwendet) + +3. **Component Structure**: + +```typescript +// 1. Imports +import React from 'react'; +import { Box, Button } from '@mui/material'; + +// 2. Interfaces/Types +interface Props { ... } + +// 3. Component +export function ComponentName({ prop1, prop2 }: Props) { + // 3.1 State + const [state, setState] = useState(); + + // 3.2 Handlers + const handleAction = () => { ... }; + + // 3.3 Effects + useEffect(() => { ... }, []); + + // 3.4 Render + return ( ... ); +} +``` + +### MUI Best Practices + +1. **Sx Props bevorzugen** statt styled components: + +```typescript +// ✅ Gut + + +// ❌ Vermeiden (im Prototyp) + +``` + +2. **Theme-basierte Werte verwenden**: + +```typescript +// ✅ Gut - Theme Colors +sx={{ color: 'primary.main', bgcolor: 'grey.50' }} + +// ❌ Vermeiden - Hardcoded +sx={{ color: '#3F51B5', bgcolor: '#FAFAFA' }} +``` + +3. **Responsive Werte** (für spätere mobile Version): + +```typescript +sx={{ + width: { xs: '100%', md: 300 }, + display: { xs: 'none', md: 'block' } +}} +``` + +### Performance-Optimierung + +1. **React.memo** für große Listen: + +```typescript +export const BewerbRow = React.memo(({ bewerb }: Props) => { ... }); +``` + +2. **useCallback** für Event Handlers in Listen: + +```typescript +const handleSelect = useCallback((id: number) => { ... }, []); +``` + +3. **Lazy Loading** für Tabs: + +```typescript +const BewerbeTab = lazy(() => import('./turnier/BewerbeTab')); +``` + +--- + +## Testing (geplant) + +### Unit Tests mit Vitest + +```typescript +// BewerbeTab.test.tsx +import { render, screen } from '@testing-library/react'; +import { BewerbeTab } from './BewerbeTab'; + +describe('BewerbeTab', () => { + it('renders 12 bewerbe', () => { + render(); + expect(screen.getByText('12 Bewerbe')).toBeInTheDocument(); + }); +}); +``` + +### E2E Tests mit Playwright + +```typescript +// e2e/bewerbe.spec.ts +test('can create new bewerb', async ({ page }) => { + await page.goto('/veranstaltung/1/turnier/1'); + await page.click('text=Bewerb Einfügen'); + await page.fill('input[name="nummer"]', '13'); + // ... +}); +``` + +--- + +## Browser-Unterstützung + +**Ziel-Browser** (Desktop): + +- Chrome/Edge >= 90 +- Firefox >= 88 +- Safari >= 14 + +**NICHT unterstützt**: + +- Internet Explorer +- Mobile Browser (vorerst) + +--- + +## Bekannte Einschränkungen (Prototyp) + +1. **Keine Backend-Integration**: Alle Daten sind Mock-Daten +2. **Keine Persistenz**: Änderungen gehen bei Page Refresh verloren +3. **Eingeschränkte Validierung**: Minimale Form-Validierung +4. **Keine Fehlerbehandlung**: Fehler-States nicht implementiert +5. **Mock-Login**: Demo-Credentials hart-kodiert +6. **Keine Exports**: Drucken/Exportieren nur als Placeholder-Buttons +7. **Keine Suche/Filter**: Filter-Funktionen nicht implementiert +8. **Keine Undo/Redo**: "Änderungen Rückgängig" nicht funktional + +--- + +## Nächste Schritte / Roadmap + +### Phase 1: Backend-Integration + +- [ ] REST API Implementation +- [ ] Authentifizierungs-System +- [ ] Datenbank-Schema (PostgreSQL empfohlen) +- [ ] ÖPS Datasourcing API-Integration + +### Phase 2: Erweiterte Features + +- [ ] Such- und Filter-Funktionen +- [ ] Sortierung in Tabellen +- [ ] Drag & Drop für Bewerbs-Reihenfolge +- [ ] Bulk-Operations (mehrere Bewerbe gleichzeitig bearbeiten) +- [ ] Undo/Redo-Funktionalität +- [ ] Auto-Save (mit Debouncing) + +### Phase 3: Export & Reporting + +- [ ] PDF-Export (Startlisten, Ergebnislisten) +- [ ] Excel-Export +- [ ] Druckvorlagen +- [ ] Berichts-Templates + +### Phase 4: Erweiterte Tabs + +- [ ] Organisation-Tab: Funktionäre-Verwaltung +- [ ] Organisation-Tab: Plätze-Verwaltung +- [ ] Preisliste-Tab: Vollständige Implementierung +- [ ] Übersicht-Tab: Dashboard mit Statistiken + +### Phase 5: Zusätzliche Module + +- [ ] Meisterschaften/Cups-Verwaltung +- [ ] Nennungs-System +- [ ] Starter-Verwaltung +- [ ] Pferde-Datenbank +- [ ] Reiter-Datenbank + +### Phase 6: Polish & Optimierung + +- [ ] Umfassendes Testing +- [ ] Performance-Optimierung +- [ ] Accessibility (WCAG 2.1 AA) +- [ ] Internationalisierung (i18n) +- [ ] Keyboard Shortcuts +- [ ] Offline-Modus (PWA) + +--- + +## Häufige Entwicklungs-Aufgaben + +### Neue Komponente hinzufügen + +```typescript +// src/app/components/MyComponent.tsx +import { Box, Typography } from '@mui/material'; + +interface MyComponentProps { + title: string; +} + +export function MyComponent({ title }: MyComponentProps) { + return ( + + + {title} + + + ); +} +``` + +### Neue Route hinzufügen + +```typescript +// src/app/routes.tsx +{ + path: "my-new-page", + Component: MyNewPage, +} +``` + +### Neuen Tab in Veranstaltung/Turnier hinzufügen + +```typescript +// In VeranstaltungAnsicht.tsx oder TurnierAnsicht.tsx +const tabs = [ + // ... bestehende Tabs + { label: 'Mein neuer Tab', component: } +]; +``` + +### MUI Component anpassen + +```typescript +// Global Theme Override +const theme = createTheme({ + components: { + MuiButton: { + styleOverrides: { + root: { + textTransform: 'none', + fontSize: '10px', + }, + }, + }, + }, +}); + +// Oder mit Sx Props + +``` + +#### 2. Tailwind Utility Classes + +```typescript +
+ Status: + Aktiv +
+``` + +#### 3. Hybrid Approach (empfohlen) + +```typescript + + + Turnier-Name + + +``` + +--- + +## 🧩 Component Patterns + +### 1. Container Component (Smart) + +```typescript +// BewerbeTab.tsx +import { useState, useEffect } from 'react'; + +export function BewerbeTab() { + const [bewerbe, setBewerbe] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Daten laden + fetchBewerbe(); + }, []); + + const fetchBewerbe = async () => { + try { + setLoading(true); + const data = await api.getBewerbe(); + setBewerbe(data); + } catch (error) { + console.error(error); + } finally { + setLoading(false); + } + }; + + const handleCreate = (bewerb: Bewerb) => { + setBewerbe([...bewerbe, bewerb]); + }; + + if (loading) return ; + + return ( + + + + + ); +} +``` + +### 2. Presentational Component (Dumb) + +```typescript +// BewerbeTable.tsx +interface Props { + bewerbe: Bewerb[]; + onEdit?: (bewerb: Bewerb) => void; + onDelete?: (id: number) => void; +} + +export function BewerbeTable({ bewerbe, onEdit, onDelete }: Props) { + return ( + + + + Nr. + Name + Klasse + + + + {bewerbe.map((bewerb) => ( + + {bewerb.nr} + {bewerb.name} + {bewerb.klasse} + + ))} + +
+ ); +} +``` + +### 3. Custom Hook + +```typescript +// hooks/useTurnier.ts +import { useState, useEffect } from 'react'; + +export function useTurnier(id: string) { + const [turnier, setTurnier] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchTurnier = async () => { + try { + setLoading(true); + const data = await api.getTurnier(id); + setTurnier(data); + } catch (err) { + setError(err as Error); + } finally { + setLoading(false); + } + }; + + fetchTurnier(); + }, [id]); + + return { turnier, loading, error }; +} + +// Usage +const { turnier, loading, error } = useTurnier('123'); +``` + +--- + +## 🔄 State Management + +### Aktuell: Local State (useState) + +```typescript +// TurnierAnsicht.tsx +export function TurnierAnsicht() { + const [activeTab, setActiveTab] = useState(0); + const [turnier, setTurnier] = useState(null); + + return ( + + setActiveTab(v)}> + + + + + {activeTab === 0 && } + {activeTab === 1 && } + + ); +} +``` + +### Empfohlen: React Context (für geteilten State) + +```typescript +// context/TurnierContext.tsx +import { createContext, useContext, useState, ReactNode } from 'react'; + +interface TurnierContextType { + turnier: Turnier | null; + setTurnier: (turnier: Turnier) => void; + bewerbe: Bewerb[]; + setBewerbe: (bewerbe: Bewerb[]) => void; +} + +const TurnierContext = createContext(undefined); + +export function TurnierProvider({ children }: { children: ReactNode }) { + const [turnier, setTurnier] = useState(null); + const [bewerbe, setBewerbe] = useState([]); + + return ( + + {children} + + ); +} + +export function useTurnierContext() { + const context = useContext(TurnierContext); + if (!context) { + throw new Error('useTurnierContext must be used within TurnierProvider'); + } + return context; +} + +// Usage in TurnierAnsicht.tsx + + + + +// Usage in Child Component +const { turnier, setTurnier } = useTurnierContext(); +``` + +### Alternative: Zustand (für komplexe Apps) + +```typescript +// store/useTurnierStore.ts +import { create } from 'zustand'; + +interface TurnierStore { + turnier: Turnier | null; + bewerbe: Bewerb[]; + nennungen: Nennung[]; + + setTurnier: (turnier: Turnier) => void; + addBewerb: (bewerb: Bewerb) => void; + addNennung: (nennung: Nennung) => void; + clearStore: () => void; +} + +export const useTurnierStore = create((set) => ({ + turnier: null, + bewerbe: [], + nennungen: [], + + setTurnier: (turnier) => set({ turnier }), + + addBewerb: (bewerb) => + set((state) => ({ bewerbe: [...state.bewerbe, bewerb] })), + + addNennung: (nennung) => + set((state) => ({ nennungen: [...state.nennungen, nennung] })), + + clearStore: () => set({ turnier: null, bewerbe: [], nennungen: [] }), +})); + +// Usage +const { turnier, addBewerb } = useTurnierStore(); +``` + +--- + +## 🌐 Routing + +### Route Konfiguration + +```typescript +// routes.tsx +import { createBrowserRouter } from 'react-router'; +import { Login } from './components/Login'; +import { Dashboard } from './components/Dashboard'; +import { VeranstalterVerwaltung } from './components/VeranstalterVerwaltung'; +import { TurnierErstellen } from './components/TurnierErstellen'; +import { TurnierAnsicht } from './components/TurnierAnsicht'; + +export const router = createBrowserRouter([ + { + path: '/', + element: , + }, + { + path: '/admin', + element: , + }, + { + path: '/veranstalter', + element: , + }, + { + path: '/veranstaltung/:id', + element: , + }, + { + path: '/turnier/:veranstaltungId/:nr', + element: , + }, + { + path: '*', + element: , + }, +]); +``` + +### Navigation + +```typescript +import { useNavigate, useParams } from 'react-router'; + +export function TurnierAnsicht() { + const navigate = useNavigate(); + const params = useParams(); + + const veranstaltungId = params.veranstaltungId; + const turnierNr = params.nr; + + const handleZurueck = () => { + navigate(`/veranstaltung/${veranstaltungId}`); + }; + + const handleToAdmin = () => { + navigate('/admin'); + }; + + return ( + + + Admin - Verwaltung + Veranstaltung + Turnier {turnierNr} + + + ); +} +``` + +--- + +## 📡 API Integration + +### API Client Setup + +```typescript +// api/client.ts +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api'; + +class ApiClient { + private token: string | null = null; + + setToken(token: string) { + this.token = token; + localStorage.setItem('token', token); + } + + clearToken() { + this.token = null; + localStorage.removeItem('token'); + } + + async request( + endpoint: string, + options: RequestInit = {} + ): Promise { + const headers: HeadersInit = { + 'Content-Type': 'application/json', + ...(this.token && { Authorization: `Bearer ${this.token}` }), + ...options.headers, + }; + + const response = await fetch(`${API_BASE_URL}${endpoint}`, { + ...options, + headers, + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error?.message || 'Request failed'); + } + + return response.json(); + } + + get(endpoint: string): Promise { + return this.request(endpoint, { method: 'GET' }); + } + + post(endpoint: string, data: any): Promise { + return this.request(endpoint, { + method: 'POST', + body: JSON.stringify(data), + }); + } + + put(endpoint: string, data: any): Promise { + return this.request(endpoint, { + method: 'PUT', + body: JSON.stringify(data), + }); + } + + delete(endpoint: string): Promise { + return this.request(endpoint, { method: 'DELETE' }); + } +} + +export const apiClient = new ApiClient(); +``` + +### API Service Layer + +```typescript +// api/turnierService.ts +import { apiClient } from './client'; + +export interface Turnier { + id: number; + veranstaltungId: number; + nr: string; + name: string; + // ... +} + +export const turnierService = { + async getTurniere(veranstaltungId?: number): Promise { + const params = veranstaltungId ? `?veranstaltungId=${veranstaltungId}` : ''; + const response = await apiClient.get<{ data: Turnier[] }>(`/turniere${params}`); + return response.data; + }, + + async getTurnierById(id: number): Promise { + const response = await apiClient.get<{ data: Turnier }>(`/turniere/${id}`); + return response.data; + }, + + async createTurnier(turnier: Partial): Promise { + const response = await apiClient.post<{ data: Turnier }>('/turniere', turnier); + return response.data; + }, + + async updateTurnier(id: number, turnier: Partial): Promise { + const response = await apiClient.put<{ data: Turnier }>(`/turniere/${id}`, turnier); + return response.data; + }, + + async deleteTurnier(id: number): Promise { + await apiClient.delete(`/turniere/${id}`); + }, +}; +``` + +### React Query Integration (empfohlen) + +```typescript +// hooks/useTurniere.ts +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { turnierService } from '../api/turnierService'; + +export function useTurniere(veranstaltungId?: number) { + return useQuery({ + queryKey: ['turniere', veranstaltungId], + queryFn: () => turnierService.getTurniere(veranstaltungId), + }); +} + +export function useTurnier(id: number) { + return useQuery({ + queryKey: ['turnier', id], + queryFn: () => turnierService.getTurnierById(id), + enabled: !!id, + }); +} + +export function useCreateTurnier() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: turnierService.createTurnier, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['turniere'] }); + }, + }); +} + +// Usage in Component +const { data: turniere, isLoading } = useTurniere(veranstaltungId); +const createMutation = useCreateTurnier(); + +const handleCreate = (turnier: Partial) => { + createMutation.mutate(turnier); +}; +``` + +--- + +## 📝 Form Handling + +### React Hook Form + +```typescript +// components/TurnierForm.tsx +import { useForm, Controller } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { z } from 'zod'; + +const turnierSchema = z.object({ + nr: z.string().min(1, 'Nummer ist erforderlich'), + name: z.string().min(1, 'Name ist erforderlich'), + znsDaten: z.string().optional(), + oetoTyp: z.enum(['national', 'international']), +}); + +type TurnierFormData = z.infer; + +export function TurnierForm({ onSubmit }: { onSubmit: (data: TurnierFormData) => void }) { + const { + control, + handleSubmit, + formState: { errors }, + } = useForm({ + resolver: zodResolver(turnierSchema), + defaultValues: { + nr: '', + name: '', + oetoTyp: 'national', + }, + }); + + return ( +
+ ( + + )} + /> + + ( + + )} + /> + + ( + + ÖTO-Typ + + + )} + /> + + + + ); +} +``` + +--- + +## 🎯 TypeScript Types + +### Type Definitions + +```typescript +// types/index.ts + +export interface Veranstalter { + id: number; + name: string; + adresse: string; + plz: string; + ort: string; + land: string; + telefon: string; + email: string; + website: string; + vereinsnummer: string; +} + +export interface Veranstaltung { + id: number; + veranstalterId: number; + name: string; + ort: string; + startDatum: string; + endDatum: string; + status: 'geplant' | 'laufend' | 'abgeschlossen'; + turniere: Turnier[]; +} + +export interface Turnier { + id: number; + veranstaltungId: number; + nr: string; + name: string; + znsDaten: string; + oetoTyp: 'national' | 'international'; + feiTyp?: string; + titel: string; + subTitel: string; + sponsoren: Sponsor[]; +} + +export interface Sponsor { + name: string; + logo: string; +} + +export interface Bewerb { + id: number; + turnierId: number; + nr: string; + name: string; + klasse: string; + tag: number; + datum: string; + beginn: string; + platz: string; + typ: string; + richter: string; + maxTeilnehmer: number; + startgebuehr: number; +} + +export interface Reiter { + id: number; + vorname: string; + nachname: string; + geburtsdatum: string; + ort: string; + land: string; + verein: string; + lizenznummer: string; +} + +export interface Pferd { + id: number; + name: string; + geschlecht: 'Hengst' | 'Stute' | 'Wallach'; + geburtsjahr: number; + rasse: string; + farbe: string; + besitzer: string; + lebensnummer: string; +} + +export interface Nennung { + id: number; + turnierId: number; + bewerbId: number; + reiterId: number; + pferdId: number; + startnummer?: number; + startwunsch?: 'vorne' | 'hinten'; + status: 'offen' | 'bestätigt' | 'gestartet' | 'abgeschlossen'; +} + +export interface Buchung { + id: number; + turnierId: number; + reiterId?: number; + pferdId?: number; + buchungstext: string; + soll: number; + haben: number; + saldo: number; + zahlungsart: 'bar' | 'scheck' | 'bankomat' | 'kreditkarte'; + status: 'offen' | 'bezahlt' | 'storniert'; +} +``` + +--- + +## 🧪 Testing + +### Component Testing (React Testing Library) + +```typescript +// __tests__/BewerbeTab.test.tsx +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { BewerbeTab } from '../components/turnier/BewerbeTab'; + +describe('BewerbeTab', () => { + it('renders bewerbe table', () => { + render(); + + expect(screen.getByText('Bewerbs-Übersicht')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Neuer Bewerb/i })).toBeInTheDocument(); + }); + + it('opens dialog when "Neuer Bewerb" is clicked', async () => { + render(); + + const button = screen.getByRole('button', { name: /Neuer Bewerb/i }); + fireEvent.click(button); + + await waitFor(() => { + expect(screen.getByText('Bewerb erstellen')).toBeInTheDocument(); + }); + }); +}); +``` + +### Hook Testing + +```typescript +// __tests__/useTurnier.test.tsx +import { renderHook, waitFor } from '@testing-library/react'; +import { useTurnier } from '../hooks/useTurnier'; + +describe('useTurnier', () => { + it('fetches turnier data', async () => { + const { result } = renderHook(() => useTurnier('123')); + + expect(result.current.loading).toBe(true); + + await waitFor(() => { + expect(result.current.loading).toBe(false); + }); + + expect(result.current.turnier).toBeDefined(); + expect(result.current.error).toBeNull(); + }); +}); +``` + +--- + +## ⚡ Performance Optimierung + +### 1. Code Splitting (Lazy Loading) + +```typescript +// App.tsx +import { lazy, Suspense } from 'react'; + +const Dashboard = lazy(() => import('./components/Dashboard')); +const TurnierAnsicht = lazy(() => import('./components/TurnierAnsicht')); + +function App() { + return ( + }> + + + ); +} +``` + +### 2. Memoization + +```typescript +import { memo, useMemo, useCallback } from 'react'; + +// Component Memoization +export const BewerbeTable = memo(({ bewerbe }: { bewerbe: Bewerb[] }) => { + return ...
; +}); + +// Value Memoization +const sortedBewerbe = useMemo(() => { + return bewerbe.sort((a, b) => a.nr.localeCompare(b.nr)); +}, [bewerbe]); + +// Function Memoization +const handleDelete = useCallback((id: number) => { + deleteBewerb(id); +}, []); +``` + +### 3. Virtual Scrolling (für große Listen) + +```typescript +import { FixedSizeList } from 'react-window'; + +function BewerbeList({ bewerbe }: { bewerbe: Bewerb[] }) { + return ( + + {({ index, style }) => ( +
+ {bewerbe[index].name} +
+ )} +
+ ); +} +``` + +--- + +## 🔨 Build & Development + +### Vite Configuration + +```typescript +// vite.config.ts +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + proxy: { + '/api': { + target: 'http://localhost:3000', + changeOrigin: true, + }, + }, + }, + build: { + outDir: 'dist', + sourcemap: true, + rollupOptions: { + output: { + manualChunks: { + 'react-vendor': ['react', 'react-dom', 'react-router'], + 'mui-vendor': ['@mui/material', '@mui/icons-material'], + }, + }, + }, + }, +}); +``` + +### Package.json Scripts + +```json +{ + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "test": "vitest", + "test:ui": "vitest --ui", + "lint": "eslint src --ext ts,tsx", + "type-check": "tsc --noEmit" + } +} +``` + +--- + +## 📦 Key Dependencies + +```json +{ + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router": "^7.1.3", + "@mui/material": "^6.3.0", + "@mui/icons-material": "^6.3.0", + "@emotion/react": "^11.13.3", + "@emotion/styled": "^11.13.0", + "tailwindcss": "^4.0.0", + "@tanstack/react-query": "^5.62.15", + "react-hook-form": "^7.55.0", + "zod": "^3.24.1", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@types/react": "^18.3.17", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.7.3", + "vite": "^6.0.7", + "vitest": "^3.0.0", + "@testing-library/react": "^16.1.0", + "@testing-library/jest-dom": "^6.6.3", + "eslint": "^9.18.0" + } +} +``` + +--- + +## 🚀 Deployment + +### Environment Variables + +```bash +# .env.development +VITE_API_BASE_URL=http://localhost:3000/api + +# .env.production +VITE_API_BASE_URL=https://api.turnierverwaltung.at/api +``` + +### Vercel Deployment + +```bash +# Install Vercel CLI +npm i -g vercel + +# Deploy +vercel --prod +``` + +--- + +**Dokumentiert von:** Frontend Developer +**Version:** 1.0 +**Datum:** 2026-03-24 diff --git a/docs/06_Frontend/FIGMA/Vision_03/docs/README.md b/docs/06_Frontend/FIGMA/Vision_03/docs/README.md new file mode 100644 index 00000000..636fe7a2 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/docs/README.md @@ -0,0 +1,308 @@ +# Turnierverwaltungs-Anwendung - Projekt-Dokumentation + +## 📋 Projekt-Übersicht + +Eine professionelle Desktop-Turnierverwaltungs-Anwendung für den Pferdesport, entwickelt mit React, TypeScript und +Material Design 3 (Material-UI). + +### Hauptziel + +Verwaltung der kompletten Hierarchie: **Veranstalter (Vereine)** → **Veranstaltungen** → **Turniere** → **Bewerbe** + +--- + +## 🎯 Zielgruppe + +- Turnier-Veranstalter +- Turnier-Organisatoren +- Turnier-Sekretariat +- Rechnungsstellen + +--- + +## 🏗️ Projekt-Status + +**Version:** Prototyp v1.0 +**Status:** Entwicklungsphase +**Letztes Update:** 2026-03-24 + +### Implementierte Features ✅ + +#### 1. **Authentication System** + +- Login-Maske mit Demo-Credentials + - User: `admin` + - Passwort: `Admin#1234` + +#### 2. **Veranstalter-Verwaltung** + +- Vollständige CRUD-Operationen +- Veranstalter-Übersicht mit Tabelle +- Veranstalter-Auswahl bei Veranstaltungs-Erstellung + +#### 3. **Veranstaltungs-Verwaltung** + +- Veranstaltung - Übersicht mit Turnieren +- Turnier-Karten mit Status-Badges +- "Neues Turnier anlegen"-Workflow + +#### 4. **Turnier-Verwaltung (8 Tabs)** + +- **Tab 1: Stammdaten** + - Turnier-Konfiguration (ZNS-Import, ÖTO/FEI-Typ) + - Turnier-Beschreibung (Titel/Sub-Titel) + - Sponsoren-Verwaltung (Name + Logo) + +- **Tab 2: Organisation** + - Zeitplan + - Kontakte & Verantwortliche + - Organisatorische Details + +- **Tab 3: Bewerbe** + - Bewerbs-Übersicht mit Tabelle + - CRUD-Operationen für Bewerbe + - Wichtigste Konfigurationsseite + +- **Tab 4: Artikel** + - Nennungs- und Startgebühren + - Stallungen & Boxen + - Zusatzleistungen + - Diverse Gebühren + +- **Tab 5: Abrechnung** ⭐ + - Buchungstabelle (Soll/Haben/Saldo) + - Teilnehmer-Auswahl (Reiter/Pferd) + - Zahlungsarten (Bar, Scheck, Bankomat, Kreditkarte) + - Direktdruck (Saldo, Rechnung) + - Gebührenverwaltung + +- **Tab 6: Nennungen** ⭐ + - Pferd & Reiter Suche mit Cross-Reference + - Nennungen-Tabelle (Reiter/Pferd/Bewerbe) + - Verkauf/Buchungen + - Bewerbsliste zum Nennen + +- **Tab 7: Startlisten** + - Platzhalter für Startlisten-Generierung + +- **Tab 8: Ergebnislisten** + - Platzhalter für Ergebnis-Erfassung + +--- + +## 🛠️ Technologie-Stack + +### Frontend + +- **React** 18+ (Function Components, Hooks) +- **TypeScript** (Type-Safety) +- **React Router** v7 (Data Mode Pattern) +- **Material-UI** v6 (Material Design 3) +- **Tailwind CSS** v4 (Utility-First CSS) + +### Design System + +- **Primärfarbe:** Indigo (#3F51B5) +- **Design-Sprache:** Material Design 3 +- **Layout:** Desktop-optimiert (1440px+) +- **Schriftgrößen:** 10px - 13px (kompakt) + +### Build Tools + +- **Vite** (Build Tool) +- **PNPM** (Package Manager) + +--- + +## 📁 Projekt-Struktur + +``` +/src +├── /app +│ ├── App.tsx # Haupt-Komponente mit Router +│ ├── routes.tsx # React Router Konfiguration +│ │ +│ └── /components +│ ├── Login.tsx # Login-Maske +│ ├── Dashboard.tsx # Admin-Übersicht mit Veranstaltungen +│ ├── VeranstalterVerwaltung.tsx +│ ├── VeranstalterAuswahl.tsx +│ ├── TurnierErstellen.tsx # Veranstaltungs-Übersicht +│ ├── TurnierAnsicht.tsx # Turnier-Tabs +│ │ +│ ├── NennungsMaske.tsx # Desktop Nennungs-Maske +│ ├── PferdReiterEingabe.tsx +│ ├── NennungenTabelle.tsx +│ ├── VerkaufBuchungen.tsx +│ ├── Bewerbsliste.tsx +│ │ +│ └── /turnier +│ ├── StammdatenTab.tsx +│ ├── OrganisationTab.tsx +│ ├── BewerbeTab.tsx +│ ├── ArtikelTab.tsx +│ ├── AbrechnungTab.tsx # NEU: Bar-Zahlungen +│ ├── NennungenTab.tsx # NEU: Wrapper +│ ├── StartlistenTab.tsx +│ └── ErgebnislistenTab.tsx +│ +├── /styles +│ ├── theme.css # Tailwind Theme + CSS Variables +│ └── fonts.css # Font Imports +│ +└── /imports # Figma-Assets (falls vorhanden) +``` + +--- + +## 🔄 Daten-Hierarchie + +``` +Veranstalter (Verein) +└── Veranstaltung (Event) + └── Turnier + ├── Stammdaten + ├── Organisation + ├── Bewerbe + │ └── Einzelne Bewerbe + ├── Artikel/Preisliste + ├── Abrechnung + │ └── Buchungen pro Teilnehmer + ├── Nennungen + │ ├── Reiter + │ ├── Pferd + │ └── Bewerbs-Nennungen + ├── Startlisten + └── Ergebnislisten +``` + +--- + +## 🎨 Design-Prinzipien + +1. **Desktop-First:** Optimiert für 1440px+ Displays +2. **Kompakt:** Kleine Schriftgrößen (10-11px), hohe Informationsdichte +3. **Tastatur-optimiert:** Tab-Navigation, Shortcuts +4. **Material Design 3:** Konsistente MUI-Komponenten +5. **Indigo-Farbschema:** Primärfarbe #3F51B5 +6. **Responsive Tabs:** Flexible Tab-Struktur für verschiedene Workflows + +--- + +## 🚀 Quick Start + +### Installation + +```bash +pnpm install +``` + +### Entwicklung + +```bash +pnpm dev +``` + +### Login + +- **Username:** admin +- **Passwort:** Admin#1234 + +--- + +## 📖 Dokumentations-Übersicht + +Für jedes Team gibt es spezifische Dokumentationen: + +1. **[ARCHITECTURE.md](./ARCHITECTURE.md)** - Lead-Architekt +2. **[BACKEND.md](./BACKEND.md)** - Backend Developer +3. **[FRONTEND.md](./FRONTEND.md)** - Frontend Developer +4. **[UI-UX.md](./UI-UX.md)** - UI/UX Designer + +--- + +## 📝 Development Log + +### Session 2026-03-24: Abrechnung & Nennungen-Integration + +#### Implementiert: + +1. **Abrechnung-Tab (Tab 5)** + - Buchungstabelle mit Soll/Haben/Saldo + - Teilnehmer-Auswahl (Reiter/Pferd Dropdown) + - Zahlungsarten: Bar, Scheck (+30€), Bankomat, Kreditkarte + - Direkt-Druck: Saldo & Rechnung + - Aktions-Buttons: Aktualisieren, Übersicht, Tabelle Leeren + - Summenzeile mit Gesamt-Saldo + - Info-Box mit Hinweisen + +2. **Nennungen-Tab (Tab 6)** + - Integration der Desktop-Nennungs-Maske + - Pferd & Reiter Eingabe mit Cross-Reference + - Nennungen-Tabelle mit drei Tabs (Reiter/Pferd/Bewerbe) + - Verkauf/Buchungen Panel + - Bewerbsliste mit Doppelklick-Nennung + - Navigation Buttons: Startliste, Ergebnisse, Abrechnung + +3. **Tab-Struktur finalisiert:** + - Stammdaten → Organisation → Bewerbe → Artikel → **Abrechnung** → **Nennungen** → Startlisten → Ergebnislisten + +#### Workflow: + +``` +Admin - Verwaltung + → Neue Veranstaltung + → Veranstalter Auswahl + → Veranstalter-Übersicht + → Veranstaltung + → Turnier-Stammdaten + → 8 Turnier-Tabs +``` + +--- + +## 🔮 Nächste Schritte + +### Backend-Integration + +- [ ] Supabase oder REST API Integration +- [ ] Datenbank-Schema implementieren +- [ ] CRUD-Operationen für alle Entitäten +- [ ] Echtzeit-Updates für Nennungen +- [ ] Zahlungs-Transaktionen persistieren + +### Frontend-Erweiterungen + +- [ ] Startlisten-Generierung implementieren +- [ ] Ergebnislisten-Erfassung implementieren +- [ ] Druck-Funktionen (PDF-Export) +- [ ] Filter & Such-Funktionen optimieren +- [ ] Excel-Export für Übersichten + +### UI/UX Verbesserungen + +- [ ] Drag & Drop für Startlisten +- [ ] Keyboard Shortcuts dokumentieren +- [ ] Loading States & Error Handling +- [ ] Toast Notifications +- [ ] Confirmation Dialogs + +--- + +## 👥 Team-Kontakte + +- **Lead-Architekt:** [Name] +- **Backend Developer:** [Name] +- **Frontend Developer:** [Name] +- **UI/UX Designer:** [Name] + +--- + +## 📄 Lizenz + +[Lizenz-Information] + +--- + +**Erstellt am:** 2026-03-24 +**Zuletzt aktualisiert:** 2026-03-24 diff --git a/docs/06_Frontend/FIGMA/Vision_03/docs/UI-UX.md b/docs/06_Frontend/FIGMA/Vision_03/docs/UI-UX.md new file mode 100644 index 00000000..d0ecf97f --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/docs/UI-UX.md @@ -0,0 +1,1047 @@ +# UI/UX Design-Dokumentation - UI/UX Designer + +## 🎨 Design-System-Übersicht + +Eine kompakte, tastaturoptimierte Desktop-Turnierverwaltungs-Anwendung im **Material Design 3** Stil mit **Indigo** als +Primärfarbe. + +**Design-Philosophie:** + +- Desktop-First (1440px+) +- Hohe Informationsdichte +- Kompakte Darstellung (10-13px Schriftgrößen) +- Tastatur-Navigation +- Professionell & Funktional + +--- + +## 🎯 Design-Prinzipien + +### 1. **Desktop-Optimierung** + +- Zielauflösung: 1440px - 1920px +- Mehrspaltiges Layout +- Hohe Informationsdichte +- Keine mobile Variante (aktuell) + +### 2. **Kompaktheit** + +- Kleine Schriftgrößen (10-13px) +- Reduzierte Abstände +- Kompakte Controls (small size) +- Maximale Nutzung des Bildschirms + +### 3. **Effizienz** + +- Tastatur-Navigation (Tab, Enter, ESC) +- Shortcuts für häufige Aktionen +- Schnelle Dateneingabe +- Minimale Klicks + +### 4. **Konsistenz** + +- Material Design 3 Guidelines +- Einheitliche Komponenten +- Konsistente Farbgebung +- Wiedererkennbare Patterns + +--- + +## 🎨 Farb-Palette + +### Primär-Farben + +``` +Indigo (Haupt-Primärfarbe) +├── Main: #3F51B5 RGB(63, 81, 181) +├── Light: #7986CB RGB(121, 134, 203) +├── Dark: #303F9F RGB(48, 63, 159) +└── Ultra: #1A237E RGB(26, 35, 126) +``` + +### Sekundär-Farben + +``` +Deep Orange +├── Main: #FF5722 RGB(255, 87, 34) +├── Light: #FF8A65 RGB(255, 138, 101) +└── Dark: #E64A19 RGB(230, 74, 25) +``` + +### Graustufen (Background & Text) + +``` +├── Grey 50: #FAFAFA → Paper Background +├── Grey 100: #F5F5F5 → Section Background +├── Grey 200: #EEEEEE → Disabled Background +├── Grey 300: #E0E0E0 → Border Color +├── Grey 500: #9E9E9E → Secondary Text +├── Grey 700: #616161 → Primary Text +└── Grey 900: #212121 → Header Text +``` + +### Semantische Farben + +``` +Success (Grün) +├── Main: #4CAF50 → Bestätigt, Bezahlt +├── Light: #81C784 +└── Dark: #388E3C + +Warning (Orange/Amber) +├── Main: #FF9800 → Warnung, Pending +├── Light: #FFB74D +└── Dark: #F57C00 + +Error (Rot) +├── Main: #F44336 → Fehler, Offen, Saldo +├── Light: #E57373 +└── Dark: #D32F2F + +Info (Blau) +├── Main: #2196F3 → Information +├── Light: #64B5F6 +└── Dark: #1976D2 +``` + +### Status-Farben (Badges) + +``` +Geplant: #2196F3 (Blau) +Laufend: #4CAF50 (Grün) +Abgeschlossen: #9E9E9E (Grau) +Offen: #F44336 (Rot) +Bestätigt: #4CAF50 (Grün) +``` + +--- + +## 📝 Typografie + +### Font Family + +``` +Primary: 'Roboto', sans-serif +Monospace: 'Roboto Mono', monospace (für Zahlen/Codes) +``` + +### Font Sizes (Kompakt für Desktop) + +``` +├── Heading 1: 18px (Seiten-Titel) +├── Heading 2: 15px (Bereich-Überschriften) +├── Heading 3: 13px (Unter-Überschriften) +├── Body: 11px (Standard-Text) +├── Small: 10px (Labels, Hilfstext) +└── Tiny: 9px (Fußnoten, Timestamps) +``` + +### Font Weights + +``` +├── Light: 300 +├── Regular: 400 (Standard) +├── Medium: 500 (Labels, Buttons) +├── Semi-Bold: 600 (Headings) +└── Bold: 700 (Wichtige Zahlen, Summen) +``` + +### Line Heights + +``` +├── Tight: 1.2 (Kompakte Listen) +├── Normal: 1.5 (Standard-Text) +└── Relaxed: 1.8 (Lange Texte) +``` + +### Text Styles (Material-UI) + +```typescript +// Typography Variants +variant="h1" → 18px, Semi-Bold +variant="h2" → 15px, Semi-Bold +variant="h3" → 13px, Medium +variant="body1" → 11px, Regular +variant="body2" → 11px, Regular +variant="caption" → 10px, Regular +variant="overline" → 10px, Medium, Uppercase +``` + +--- + +## 📐 Spacing & Layout + +### Spacing Scale + +``` +├── xs: 4px (Sehr eng) +├── sm: 8px (Standard-Innenabstand) +├── md: 16px (Zwischen-Abstand) +├── lg: 24px (Bereich-Abstand) +├── xl: 32px (Große Abstände) +└── xxl: 48px (Sehr große Abstände) +``` + +### Material-UI Spacing (8px-Basis) + +```typescript +sx={{ p: 1 }} // padding: 8px +sx={{ p: 2 }} // padding: 16px +sx={{ py: 1.5 }} // padding-top/bottom: 12px +sx={{ px: 2 }} // padding-left/right: 16px +sx={{ m: 2 }} // margin: 16px +sx={{ gap: 1 }} // gap: 8px +``` + +### Layout Grid + +``` +Desktop (1440px+) +├── Container Max-Width: 1920px +├── Sidebar Width: 240-280px +├── Content Area: Fluid (100%) +├── Gutter: 24px +└── Column Gap: 16px +``` + +### Component Heights (Kompakt) + +``` +├── AppBar: 48px +├── Tab Bar: 36px +├── Table Row: 32px +├── Button (small): 28px +├── TextField (small): 32px +└── Chip/Badge: 20px +``` + +--- + +## 🧩 Komponenten-Styles + +### 1. Buttons + +```typescript +// Primary Button + + +// Secondary Button + + +// Icon Button + + + +``` + +**Design Specs:** + +- Font Size: 11px +- Text Transform: none (keine ALL CAPS) +- Padding: 4px 16px (small) +- Min Width: 100px +- Border Radius: 4px + +### 2. Text Fields + +```typescript + +``` + +**Design Specs:** + +- Height: 32px (small) +- Font Size: 11px +- Label Font Size: 11px +- Border Radius: 4px +- Focus Color: Indigo (#3F51B5) + +### 3. Tables + +```typescript + + + + + Name + + + + + + + Wert + + + +
+``` + +**Design Specs:** + +- Row Height: 32px +- Font Size: 10px (Body), 11px (Header) +- Header Background: Grey 100 +- Zebra Stripes: Grey 50 (odd rows) +- Hover: Action Hover (#F5F5F5) + +### 4. Tabs + +```typescript + + + + +``` + +**Design Specs:** + +- Tab Height: 36px +- Font Size: 11px +- Text Transform: none +- Active Indicator: Indigo, 2px thick +- Hover Background: rgba(63, 81, 181, 0.04) + +### 5. Cards + +```typescript + + + + Titel + + + Inhalt + + + +``` + +**Design Specs:** + +- Border Radius: 8px +- Elevation: 1 (Standard), 2 (Hover) +- Padding: 16px +- Background: White + +### 6. Chips / Badges + +```typescript + +``` + +**Design Specs:** + +- Height: 20px +- Font Size: 9px +- Font Weight: 600 +- Border Radius: 10px +- Padding: 0 8px + +### 7. Dialogs + +```typescript + + + Bewerb erstellen + + + {/* Content */} + + + + + + +``` + +**Design Specs:** + +- Max Width: 600px (md), 900px (lg) +- Title Font Size: 13px +- Content Padding: 16px +- Actions Padding: 12px 16px + +--- + +## 📱 Layout-Patterns + +### 1. Master-Detail Layout (Dashboard) + +``` +┌────────────────────────────────────────────┐ +│ AppBar (48px) │ +├────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────────────────────────┐ │ +│ │ Header mit Filter & Aktionen │ │ +│ ├──────────────────────────────────────┤ │ +│ │ │ │ +│ │ Grid mit Karten │ │ +│ │ ┌────┐ ┌────┐ ┌────┐ │ │ +│ │ │Card│ │Card│ │Card│ │ │ +│ │ └────┘ └────┘ └────┘ │ │ +│ │ │ │ +│ └──────────────────────────────────────┘ │ +└────────────────────────────────────────────┘ +``` + +### 2. Tab-Based Layout (Turnier-Ansicht) + +``` +┌────────────────────────────────────────────┐ +│ AppBar mit Breadcrumbs (48px) │ +├────────────────────────────────────────────┤ +│ Tab Navigation (36px) │ +│ [Stammdaten][Organisation][Bewerbe]... │ +├────────────────────────────────────────────┤ +│ │ +│ Tab Content Area (Scrollable) │ +│ │ +│ ┌──────────────────────────────────────┐ │ +│ │ Formulare / Tabellen / Charts │ │ +│ │ │ │ +│ │ │ │ +│ └──────────────────────────────────────┘ │ +│ │ +└────────────────────────────────────────────┘ +``` + +### 3. Split-Panel Layout (Nennungs-Maske) + +``` +┌────────────────────────────────────────────┐ +│ Pferd & Reiter (60%) │ Verkauf (40%) │ +│ ┌───────────────────┐ │ ┌────────────┐ │ +│ │ Suche & Details │ │ │ Buchungen │ │ +│ │ │ │ │ │ │ +│ └───────────────────┘ │ └────────────┘ │ +├────────────────────────────────────────────┤ +│ Navigation Buttons (5%) │ +├────────────────────────────────────────────┤ +│ Nennungen (60%) │ Bewerbe (40%) │ +│ ┌───────────────────┐ │ ┌────────────┐ │ +│ │ Tabelle │ │ │ Liste │ │ +│ │ │ │ │ │ │ +│ └───────────────────┘ │ └────────────┘ │ +└────────────────────────────────────────────┘ +``` + +### 4. Table-Sidebar Layout (Abrechnung) + +``` +┌────────────────────────────────────────────┐ +│ Buchungen-Tabelle (70%) │ Aktionen (30%)│ +│ ┌─────────────────────┐ │ ┌──────────┐ │ +│ │ Table mit Buchungen │ │ │ Auswahl │ │ +│ │ │ │ ├──────────┤ │ +│ │ Soll | Haben | ... │ │ │ Buchen │ │ +│ │ │ │ ├──────────┤ │ +│ │ Summenzeile │ │ │ Drucken │ │ +│ └─────────────────────┘ │ ├──────────┤ │ +│ │ │ Zahlung │ │ +│ │ └──────────┘ │ +└────────────────────────────────────────────┘ +``` + +--- + +## 🎭 Interaktions-Patterns + +### 1. **Hover States** + +```css +/* Button Hover */ +button:hover { + background-color: rgba(63, 81, 181, 0.08); +} + +/* Table Row Hover */ +tr:hover { + background-color: rgba(0, 0, 0, 0.04); +} + +/* Card Hover */ +.card:hover { + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); + transition: all 0.2s ease; +} +``` + +### 2. **Focus States** + +```css +/* Input Focus */ +input:focus { + border-color: #3F51B5; + box-shadow: 0 0 0 2px rgba(63, 81, 181, 0.2); +} + +/* Button Focus */ +button:focus-visible { + outline: 2px solid #3F51B5; + outline-offset: 2px; +} +``` + +### 3. **Loading States** + +```typescript + + + +``` + +### 4. **Empty States** + +```typescript + + + + Keine Daten vorhanden + + +``` + +### 5. **Error States** + +```typescript + + Fehler + Beim Laden der Daten ist ein Fehler aufgetreten. + +``` + +--- + +## 🖼️ Icon-System + +### Icon Library: **Material Icons** + +```typescript +import AddIcon from '@mui/icons-material/Add'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; +import SaveIcon from '@mui/icons-material/Save'; +import CancelIcon from '@mui/icons-material/Cancel'; +``` + +### Icon Sizes + +``` +├── Small: 16px (Icon Buttons in Tables) +├── Medium: 20px (Default, Buttons) +├── Large: 24px (Headers) +└── XLarge: 32px (Empty States) +``` + +### Häufig verwendete Icons + +``` +Aktionen: +├── Add: AddIcon +├── Edit: EditIcon +├── Delete: DeleteIcon +├── Save: SaveIcon +├── Cancel: CancelIcon +├── Search: SearchIcon +└── Refresh: RefreshIcon + +Navigation: +├── Home: HomeIcon +├── Arrow Back: ArrowBackIcon +├── Arrow Forward: ArrowForwardIcon +└── Menu: MenuIcon + +Status: +├── Check Circle: CheckCircleIcon +├── Error: ErrorIcon +├── Warning: WarningIcon +└── Info: InfoIcon + +Turnier: +├── Event: EventIcon +├── Trophy: EmojiEventsIcon +├── Person: PersonIcon +├── Horse: (Custom SVG) +├── Receipt: ReceiptIcon +└── Print: PrintIcon +``` + +--- + +## 📊 Daten-Visualisierung + +### 1. Status-Badges + +```typescript +// Farb-Mapping +const statusColors = { + geplant: '#2196F3', // Blau + laufend: '#4CAF50', // Grün + abgeschlossen: '#9E9E9E', // Grau + offen: '#F44336', // Rot + bezahlt: '#4CAF50', // Grün +}; + + +``` + +### 2. Summen-Zeilen (Tables) + +```typescript + + GESAMT + + {total.toFixed(2)} € + + +``` + +### 3. Farb-kodierte Werte + +```typescript +// Saldo-Färbung + 0 ? 'error.main' : 'success.main', + fontWeight: 600, + }} +> + {saldo.toFixed(2)} € + +``` + +### 4. Progress Indicators + +```typescript + + + {progress}% + +``` + +--- + +## ♿ Accessibility (A11y) + +### 1. Keyboard Navigation + +``` +Tab: Nächstes Element +Shift + Tab: Vorheriges Element +Enter: Aktion ausführen / Dialog öffnen +Escape: Dialog schließen / Aktion abbrechen +Space: Checkbox / Radio Button togglen +Arrow Keys: Navigation in Listen / Tabs +``` + +### 2. ARIA Labels + +```typescript + + + + + +``` + +### 3. Focus Management + +```typescript +// Auto-Focus auf ersten Input im Dialog + + +// Focus Trap in Dialog + + + {/* Content */} + + +``` + +### 4. Color Contrast + +``` +WCAG AA Standard (Minimum): +├── Normal Text: 4.5:1 +└── Large Text: 3:1 + +Unsere Farben: +├── Primary (#3F51B5) auf White: 6.9:1 ✅ +├── Grey 700 (#616161) auf White: 5.7:1 ✅ +├── Grey 500 (#9E9E9E) auf White: 3.3:1 ⚠️ (nur für große Texte) +``` + +--- + +## 🎬 Animationen & Transitions + +### Transition-Timing + +```css +/* Standard */ +transition: all 0.2s ease; + +/* Hover Effekte */ +transition: transform 0.2s ease, box-shadow 0.2s ease; + +/* Dialog/Modal */ +transition: opacity 0.3s ease, transform 0.3s ease; +``` + +### Material-UI Transitions + +```typescript +import { Fade, Slide, Grow } from '@mui/material'; + +// Fade In/Out + + Content + + +// Slide + + Content + + +// Grow + + Content + +``` + +### Animation Best Practices + +``` +✅ DO: +- Subtile Hover-Effekte (2-4px translateY) +- Fade In/Out für Dialogs +- Smooth Transitions für Tabs +- Loading Spinners + +❌ DON'T: +- Zu lange Animationen (> 500ms) +- Unnötige Animationen bei jedem Klick +- Animationen die Bedienung verlangsamen +``` + +--- + +## 📸 Screenshots & Wireframes + +### 1. Login + +``` +┌─────────────────────────────────┐ +│ │ +│ [LOGO] │ +│ │ +│ Turnierverwaltung Login │ +│ │ +│ ┌───────────────────────────┐ │ +│ │ Username 🔒 │ │ +│ └───────────────────────────┘ │ +│ │ +│ ┌───────────────────────────┐ │ +│ │ Password 🔒 │ │ +│ └───────────────────────────┘ │ +│ │ +│ [ Anmelden ] │ +│ │ +└─────────────────────────────────┘ +``` + +### 2. Dashboard + +``` +┌─────────────────────────────────────────────┐ +│ 🏠 Admin - Verwaltung [+ Neue] │ +├─────────────────────────────────────────────┤ +│ │ +│ Veranstaltungen │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │Frühjahr │ │Sommer │ │Herbst │ │ +│ │Turnier │ │Cup │ │Turnier │ │ +│ │ │ │ │ │ │ │ +│ │[Laufend]│ │[Geplant]│ │[Geplant]│ │ +│ └─────────┘ └─────────┘ └─────────┘ │ +│ │ +└─────────────────────────────────────────────┘ +``` + +### 3. Turnier-Ansicht (Tabs) + +``` +┌─────────────────────────────────────────────┐ +│ 🏠 Admin > Frühjahrsturnier > Turnier A │ +├─────────────────────────────────────────────┤ +│[Stammdaten][Organisation][Bewerbe][...] │ +├─────────────────────────────────────────────┤ +│ │ +│ Turnier-Konfiguration │ +│ ┌─────────────────────────────────────┐ │ +│ │ ZNS-Daten: [Import Button] │ │ +│ │ ÖTO-Typ: ○ National ● International│ │ +│ │ FEI-Typ: [CSI**, CDI2*, etc.] │ │ +│ └─────────────────────────────────────┘ │ +│ │ +│ Turnier-Beschreibung │ +│ ┌─────────────────────────────────────┐ │ +│ │ Titel: [________________] │ │ +│ │ Sub-Titel: [________________] │ │ +│ └─────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────┘ +``` + +--- + +## 🎨 Design-Tokens (CSS Variables) + +```css +/* theme.css */ +:root { + /* Colors */ + --color-primary: #3f51b5; + --color-primary-light: #7986cb; + --color-primary-dark: #303f9f; + --color-secondary: #ff5722; + + /* Backgrounds */ + --bg-default: #fafafa; + --bg-paper: #ffffff; + --bg-hover: rgba(0, 0, 0, 0.04); + + /* Text */ + --text-primary: #212121; + --text-secondary: #757575; + --text-disabled: #bdbdbd; + + /* Borders */ + --border-color: #e0e0e0; + --border-radius: 4px; + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0,0,0,0.08); + --shadow-md: 0 2px 4px rgba(0,0,0,0.12); + --shadow-lg: 0 4px 8px rgba(0,0,0,0.15); + + /* Spacing */ + --spacing-xs: 4px; + --spacing-sm: 8px; + --spacing-md: 16px; + --spacing-lg: 24px; + --spacing-xl: 32px; + + /* Typography */ + --font-size-xs: 10px; + --font-size-sm: 11px; + --font-size-md: 13px; + --font-size-lg: 15px; + --font-size-xl: 18px; + + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-semibold: 600; + --font-weight-bold: 700; + + /* Transitions */ + --transition-fast: 0.15s ease; + --transition-base: 0.2s ease; + --transition-slow: 0.3s ease; +} +``` + +--- + +## 📋 Component Library Checklist + +### ✅ Implementierte Komponenten + +- [x] Login Form +- [x] Dashboard (Card Grid) +- [x] Navigation (Breadcrumbs, Tabs) +- [x] Tables (Data Tables mit Sorting) +- [x] Forms (Text Fields, Select, Radio, Checkbox) +- [x] Buttons (Primary, Secondary, Icon) +- [x] Dialogs (Create, Edit, Confirm) +- [x] Badges (Status Chips) +- [x] Split Panels (Nennungs-Maske, Abrechnung) + +### ⏳ Geplante Komponenten + +- [ ] Notifications/Toasts +- [ ] Date/Time Pickers +- [ ] File Upload +- [ ] PDF Preview +- [ ] Print Layout +- [ ] Charts/Graphs +- [ ] Calendar View +- [ ] Drag & Drop Interface + +--- + +## 🎯 Design-Checklist für neue Features + +``` +□ Folgt Material Design 3 Guidelines? +□ Schriftgröße 10-13px (Desktop-kompakt)? +□ Indigo (#3F51B5) als Primärfarbe verwendet? +□ Tastatur-Navigation möglich? +□ Hover/Focus States definiert? +□ Loading States vorhanden? +□ Error States behandelt? +□ Empty States designed? +□ Mobile-Ansicht berücksichtigt? (falls relevant) +□ Accessibility (A11y) beachtet? +□ Konsistent mit bestehenden Komponenten? +``` + +--- + +## 📚 Design-Ressourcen + +### Material-UI Documentation + +- https://mui.com/material-ui/ +- https://m3.material.io/ + +### Figma Files + +- [Link zu Figma-Design-Datei] + +### Color Tools + +- Material Color Tool: https://m2.material.io/resources/color/ +- Contrast Checker: https://webaim.org/resources/contrastchecker/ + +### Icon Resources + +- Material Icons: https://fonts.google.com/icons + +--- + +## 🔮 Design-Roadmap + +### Phase 1: MVP (Aktuell) + +- ✅ Basic Design System +- ✅ Kompakte Desktop-Layouts +- ✅ Alle 8 Turnier-Tabs +- ✅ Nennungs-Maske +- ✅ Abrechnung + +### Phase 2: Verbesserungen + +- [ ] Druck-Layouts (PDF-Export) +- [ ] Dark Mode +- [ ] Erweiterte Visualisierungen +- [ ] Drag & Drop für Startlisten + +### Phase 3: Erweiterungen + +- [ ] Mobile/Tablet-Responsiveness +- [ ] Custom Themes pro Veranstalter +- [ ] Animierte Dashboards +- [ ] Real-Time Collaboration UI + +--- + +**Dokumentiert von:** UI/UX Designer +**Version:** 1.0 +**Datum:** 2026-03-24 diff --git a/docs/06_Frontend/FIGMA/Vision_03/guidelines.zip b/docs/06_Frontend/FIGMA/Vision_03/guidelines.zip new file mode 100644 index 00000000..28fae346 Binary files /dev/null and b/docs/06_Frontend/FIGMA/Vision_03/guidelines.zip differ diff --git a/docs/06_Frontend/FIGMA/Vision_03/guidelines/Guidelines.md b/docs/06_Frontend/FIGMA/Vision_03/guidelines/Guidelines.md new file mode 100644 index 00000000..110f1178 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/guidelines/Guidelines.md @@ -0,0 +1,61 @@ +**Add your own guidelines here** + diff --git a/docs/06_Frontend/FIGMA/Vision_03/package.json b/docs/06_Frontend/FIGMA/Vision_03/package.json new file mode 100644 index 00000000..83bc21b3 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/package.json @@ -0,0 +1,90 @@ +{ + "name": "@figma/my-make-file", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "build": "vite build" + }, + "dependencies": { + "@emotion/react": "11.14.0", + "@emotion/styled": "11.14.1", + "@mui/icons-material": "7.3.5", + "@mui/material": "7.3.5", + "@mui/x-date-pickers": "^8.27.2", + "@popperjs/core": "2.11.8", + "@radix-ui/react-accordion": "1.2.3", + "@radix-ui/react-alert-dialog": "1.1.6", + "@radix-ui/react-aspect-ratio": "1.1.2", + "@radix-ui/react-avatar": "1.1.3", + "@radix-ui/react-checkbox": "1.1.4", + "@radix-ui/react-collapsible": "1.1.3", + "@radix-ui/react-context-menu": "2.2.6", + "@radix-ui/react-dialog": "1.1.6", + "@radix-ui/react-dropdown-menu": "2.1.6", + "@radix-ui/react-hover-card": "1.1.6", + "@radix-ui/react-label": "2.1.2", + "@radix-ui/react-menubar": "1.1.6", + "@radix-ui/react-navigation-menu": "1.2.5", + "@radix-ui/react-popover": "1.1.6", + "@radix-ui/react-progress": "1.1.2", + "@radix-ui/react-radio-group": "1.2.3", + "@radix-ui/react-scroll-area": "1.2.3", + "@radix-ui/react-select": "2.1.6", + "@radix-ui/react-separator": "1.1.2", + "@radix-ui/react-slider": "1.2.3", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-switch": "1.1.3", + "@radix-ui/react-tabs": "1.1.3", + "@radix-ui/react-toggle": "1.1.2", + "@radix-ui/react-toggle-group": "1.1.2", + "@radix-ui/react-tooltip": "1.1.8", + "canvas-confetti": "1.9.4", + "class-variance-authority": "0.7.1", + "clsx": "2.1.1", + "cmdk": "1.1.1", + "date-fns": "3.6.0", + "embla-carousel-react": "8.6.0", + "input-otp": "1.4.2", + "lucide-react": "0.487.0", + "motion": "12.23.24", + "next-themes": "0.4.6", + "react-day-picker": "8.10.1", + "react-dnd": "16.0.1", + "react-dnd-html5-backend": "16.0.1", + "react-hook-form": "7.55.0", + "react-popper": "2.3.0", + "react-resizable-panels": "2.1.7", + "react-responsive-masonry": "2.7.1", + "react-router": "7.13.0", + "react-slick": "0.31.0", + "recharts": "2.15.2", + "sonner": "2.0.3", + "tailwind-merge": "3.2.0", + "tw-animate-css": "1.3.8", + "vaul": "1.1.2" + }, + "devDependencies": { + "@tailwindcss/vite": "4.1.12", + "@vitejs/plugin-react": "4.7.0", + "tailwindcss": "4.1.12", + "vite": "6.3.5" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + }, + "pnpm": { + "overrides": { + "vite": "6.3.5" + } + } +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/postcss.config.mjs b/docs/06_Frontend/FIGMA/Vision_03/postcss.config.mjs new file mode 100644 index 00000000..531dbecd --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/postcss.config.mjs @@ -0,0 +1,15 @@ +/** + * PostCSS Configuration + * + * Tailwind CSS v4 (via @tailwindcss/vite) automatically sets up all required + * PostCSS plugins — you do NOT need to include `tailwindcss` or `autoprefixer` here. + * + * This file only exists for adding additional PostCSS plugins, if needed. + * For example: + * + * import postcssNested from 'postcss-nested' + * export default { plugins: [postcssNested()] } + * + * Otherwise, you can leave this file empty. + */ +export default {} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src.zip b/docs/06_Frontend/FIGMA/Vision_03/src.zip new file mode 100644 index 00000000..dfd3b136 Binary files /dev/null and b/docs/06_Frontend/FIGMA/Vision_03/src.zip differ diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/App.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/App.tsx new file mode 100644 index 00000000..04cd85c6 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/App.tsx @@ -0,0 +1,16 @@ +import {ThemeProvider} from '@mui/material/styles'; +import CssBaseline from '@mui/material/CssBaseline'; +import {RouterProvider} from 'react-router'; +import {theme} from './theme'; +import {router} from './routes'; + +function App() { + return ( + + + + + ); +} + +export default App; diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/Bewerbsliste.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/Bewerbsliste.tsx new file mode 100644 index 00000000..d0ce73b2 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/Bewerbsliste.tsx @@ -0,0 +1,139 @@ +import {useState} from 'react'; +import Box from '@mui/material/Box'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; +import Button from '@mui/material/Button'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import FilterListIcon from '@mui/icons-material/FilterList'; + +// Mock-Daten für Bewerbe +const mockBewerbe = [ + {tag: 'So', platz: 1, nr: '1', beginn: '08:00', nenn: 0, name: 'Dressurreiterprüfung Ratepass', klasse: 'A'}, + {tag: 'So', platz: 1, nr: '2', beginn: '08:20', nenn: 0, name: 'Dressurreiterprüfung Katecnadel', klasse: 'L'}, + {tag: 'So', platz: 1, nr: '3', beginn: '08:40', nenn: 0, name: 'Dressurreiterprüfung Idf. (Idf.)', klasse: 'M'}, + {tag: 'So', platz: 1, nr: '4', beginn: '09:00', nenn: 0, name: 'Dressurprüfung Idf. (Idf.)', klasse: 'L'}, + {tag: 'So', platz: 1, nr: '5', beginn: '09:20', nenn: 0, name: 'Führzügelklasse', klasse: 'E'}, + {tag: 'So', platz: 1, nr: '6', beginn: '09:40', nenn: 0, name: 'First Ridden', klasse: 'E'}, + {tag: 'So', platz: 1, nr: '7', beginn: '10:00', nenn: 0, name: 'Pony Dressurprüfung Kl. A', klasse: 'A'}, + {tag: 'So', platz: 1, nr: '8', beginn: '10:20', nenn: 0, name: 'Dressurreiterprüfung Kl. A', klasse: 'A'}, + {tag: 'So', platz: 1, nr: '9', beginn: '10:40', nenn: 0, name: 'Dressurprüfung Kl. A', klasse: 'A'}, + {tag: 'So', platz: 1, nr: '10', beginn: '11:00', nenn: 0, name: 'Pony Dressurprüfung Kl. A', klasse: 'A'}, + {tag: 'So', platz: 1, nr: '11', beginn: '11:20', nenn: 0, name: 'Dressurreiterprüfung Kl. L', klasse: 'L'}, + {tag: 'So', platz: 1, nr: '12', beginn: '11:40', nenn: 0, name: 'Dressurprüfung Kl. L', klasse: 'L'}, +]; + +interface Props { + selectedPferd: any; + selectedReiter: any; + onNennung: (bewerb: any) => void; +} + +export function Bewerbsliste({selectedPferd, selectedReiter, onNennung}: Props) { + const [selectedBewerb, setSelectedBewerb] = useState(null); + + const handleBewerbDoppelklick = (bewerb: any) => { + if (selectedPferd && selectedReiter) { + onNennung(bewerb); + setSelectedBewerb(bewerb.nr); + } + }; + + const canNennen = selectedPferd && selectedReiter; + + return ( + + + Bewerbsübersicht + + + + + + + + Aktualisieren + + + {mockBewerbe.length} Bewerbe + + + + 0 gefiltert + + + + + + + + Tag + Pl. + Bewerb + Beginn + Nenn. + Bewerbsname + + + + {mockBewerbe.map((bewerb, idx) => { + const isSelected = selectedBewerb === bewerb.nr; + const isClickable = canNennen; + + return ( + handleBewerbDoppelklick(bewerb)} + sx={{ + cursor: isClickable ? 'pointer' : 'default', + '&:nth-of-type(odd)': {bgcolor: isSelected ? 'primary.100' : 'action.hover'}, + '&.Mui-selected': { + bgcolor: 'primary.100', + '&:hover': { + bgcolor: 'primary.200', + }, + }, + opacity: isClickable ? 1 : 0.5, + }} + > + {bewerb.tag} + {bewerb.platz} + {bewerb.nr} + {bewerb.beginn} + {bewerb.nenn} + {bewerb.name} + + ); + })} + +
+
+ + {!canNennen && ( + + Bitte wählen Sie zuerst ein Pferd und einen Reiter aus + + )} +
+ ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/Dashboard.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/Dashboard.tsx new file mode 100644 index 00000000..08189f04 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/Dashboard.tsx @@ -0,0 +1,445 @@ +import {useState} from 'react'; +import {useNavigate} from 'react-router'; +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +import InputAdornment from '@mui/material/InputAdornment'; +import Chip from '@mui/material/Chip'; +import IconButton from '@mui/material/IconButton'; +import Grid from '@mui/material/Grid'; +import Paper from '@mui/material/Paper'; +import SearchIcon from '@mui/icons-material/Search'; +import AddIcon from '@mui/icons-material/Add'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; +import CalendarTodayIcon from '@mui/icons-material/CalendarToday'; +import LocationOnIcon from '@mui/icons-material/LocationOn'; +import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import WarningIcon from '@mui/icons-material/Warning'; +import PlayCircleIcon from '@mui/icons-material/PlayCircle'; + +// Mock-Daten für Veranstaltungen +export const veranstaltungenData = [ + { + id: 1, + name: 'Union Reit- und Fahrverein Neumarkt Frühjahrsturnier 2026', + ort: 'Reitanlage Stroblmair, Neumarkt/M., OÖ', + datum: '25.-26. April 2026', + datumVon: new Date('2026-04-25'), + datumBis: new Date('2026-04-26'), + status: 'vorbereitung' as const, + turniere: [ + { + nr: '26128', + name: 'CSN-C NEU CSNP-C NEU', + datum: '25.04.2026', + kategorie: 'C', + disziplin: 'Springen', + bewerbeAnzahl: 14, + znsStatus: 'geladen' + }, + { + nr: '26129', + name: 'CDN-C NEU CDNP-C NEU', + datum: '26.04.2026', + kategorie: 'C', + disziplin: 'Dressur', + bewerbeAnzahl: 12, + znsStatus: 'geladen' + } + ], + nennungen: 87, + letzteAktivitaet: '22.03.2026 14:30' + }, + { + id: 2, + name: 'AWÖ-Cup Stadl-Paura 2025', + ort: 'Bundesgestüt Piber, Stadl-Paura', + datum: '15.-17. Mai 2025', + datumVon: new Date('2025-05-15'), + datumBis: new Date('2025-05-17'), + status: 'abgeschlossen' as const, + turniere: [ + { + nr: '25001', + name: 'CSN-A', + datum: '15.05.2025', + kategorie: 'A', + disziplin: 'Springen', + bewerbeAnzahl: 18, + znsStatus: 'geladen' + }, + { + nr: '25002', + name: 'CDN-A', + datum: '16.05.2025', + kategorie: 'A', + disziplin: 'Dressur', + bewerbeAnzahl: 15, + znsStatus: 'geladen' + } + ], + nennungen: 142, + letzteAktivitaet: '17.05.2025 18:45' + }, + { + id: 3, + name: 'Linzer Pferdetage 2026', + ort: 'Reitsportzentrum Linz-Ebelsberg', + datum: '12.-14. Juni 2026', + datumVon: new Date('2026-06-12'), + datumBis: new Date('2026-06-14'), + status: 'vorbereitung' as const, + turniere: [ + { + nr: '26201', + name: 'CSN-B', + datum: '12.06.2026', + kategorie: 'B', + disziplin: 'Springen', + bewerbeAnzahl: 16, + znsStatus: 'ausstehend' + }, + { + nr: '26202', + name: 'CDN-B', + datum: '13.06.2026', + kategorie: 'B', + disziplin: 'Dressur', + bewerbeAnzahl: 14, + znsStatus: 'ausstehend' + } + ], + nennungen: 23, + letzteAktivitaet: '20.03.2026 09:15' + } +]; + +export function AdminVerwaltung() { + const navigate = useNavigate(); + const [searchTerm, setSearchTerm] = useState(''); + const [statusFilter, setStatusFilter] = useState<'alle' | 'vorbereitung' | 'live' | 'abgeschlossen'>('alle'); + + // Statistiken berechnen + const stats = { + gesamt: veranstaltungenData.length, + vorbereitung: veranstaltungenData.filter(v => v.status === 'vorbereitung').length, + live: veranstaltungenData.filter(v => v.status === 'live').length, + abgeschlossen: veranstaltungenData.filter(v => v.status === 'abgeschlossen').length, + }; + + // Filter + const filteredVeranstaltungen = veranstaltungenData.filter(v => { + const matchesSearch = v.name.toLowerCase().includes(searchTerm.toLowerCase()) || + v.ort.toLowerCase().includes(searchTerm.toLowerCase()) || + v.turniere.some(t => t.nr.includes(searchTerm)); + const matchesStatus = statusFilter === 'alle' || v.status === statusFilter; + return matchesSearch && matchesStatus; + }); + + const getStatusColor = (status: string) => { + switch (status) { + case 'vorbereitung': + return 'info'; + case 'live': + return 'success'; + case 'abgeschlossen': + return 'default'; + default: + return 'default'; + } + }; + + const getStatusIcon = (status: string) => { + switch (status) { + case 'vorbereitung': + return ; + case 'live': + return ; + case 'abgeschlossen': + return ; + default: + return null; + } + }; + + const getStatusLabel = (status: string) => { + switch (status) { + case 'vorbereitung': + return 'Vorbereitung'; + case 'live': + return 'Live'; + case 'abgeschlossen': + return 'Abgeschlossen'; + default: + return status; + } + }; + + const handleVeranstaltungOeffnen = (id: number) => { + navigate(`/veranstaltung/${id}`); + }; + + const handleTurnierOeffnen = (veranstaltungId: number, turnierNr: string) => { + navigate(`/veranstaltung/${veranstaltungId}/turnier/${turnierNr}`); + }; + + const handleNeueVeranstaltung = () => { + navigate('/veranstalter/auswahl'); + }; + + return ( + + {/* Header */} + + + Admin - Verwaltung + + + + {/* Content */} + + {/* Statistik-Cards - dezent und auf gesamte Breite */} + + + + + Live / Aktiv + + + {stats.live} + + + + + + + In Vorbereitung + + + {stats.vorbereitung} + + + + + + + Gesamt + + + {stats.gesamt} + + + + + + + Archiv + + + {stats.abgeschlossen} + + + + + + {/* Toolbar - neue Reihenfolge */} + + + + setSearchTerm(e.target.value)} + sx={{ + flex: 1, + maxWidth: 400, + '& .MuiInputBase-input': {fontSize: '11px'} + }} + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + + + setStatusFilter('alle')} + color={statusFilter === 'alle' ? 'primary' : 'default'} + size="small" + sx={{fontSize: '10px'}} + /> + setStatusFilter('vorbereitung')} + color={statusFilter === 'vorbereitung' ? 'primary' : 'default'} + size="small" + sx={{fontSize: '10px'}} + /> + setStatusFilter('live')} + color={statusFilter === 'live' ? 'primary' : 'default'} + size="small" + sx={{fontSize: '10px'}} + /> + setStatusFilter('abgeschlossen')} + color={statusFilter === 'abgeschlossen' ? 'primary' : 'default'} + size="small" + sx={{fontSize: '10px'}} + /> + + + + {/* Veranstaltungs-Liste - volle Breite */} + + {filteredVeranstaltungen.map((v) => ( + + + {/* Header mit Status */} + + + {v.name} + + + + + {/* Ort und Datum */} + + + + + {v.ort} + + + + + + {v.datum} + + + + + {/* Turniere */} + + + + Turniere ({v.turniere.length}): + + + {v.turniere.map((t) => ( + + + + {t.name} ({t.bewerbeAnzahl} Bewerbe) + + + {t.kategorie === 'B' || t.kategorie === 'A' ? ( + t.znsStatus === 'geladen' ? ( + + ) : ( + + ) + ) : null} + + + ))} + + + + {/* Statistik */} + + + Nennungen: {v.nennungen} + + + Letzte Aktivität: {v.letzteAktivitaet} + + + + {/* Actions */} + + + + + + + + + ))} + + + {filteredVeranstaltungen.length === 0 && ( + + + Keine Veranstaltungen gefunden + + + )} + + + ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/Login.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/Login.tsx new file mode 100644 index 00000000..225a5f63 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/Login.tsx @@ -0,0 +1,223 @@ +import {useState, useEffect} from 'react'; +import {useNavigate} from 'react-router'; +import Box from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; +import TextField from '@mui/material/TextField'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; +import InputAdornment from '@mui/material/InputAdornment'; +import Alert from '@mui/material/Alert'; +import CircularProgress from '@mui/material/CircularProgress'; +import Visibility from '@mui/icons-material/Visibility'; +import VisibilityOff from '@mui/icons-material/VisibilityOff'; +import WifiIcon from '@mui/icons-material/Wifi'; +import WifiOffIcon from '@mui/icons-material/WifiOff'; + +export function Login() { + const navigate = useNavigate(); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + const [isOnline, setIsOnline] = useState(navigator.onLine); + + // Internet-Verbindung überwachen + useEffect(() => { + const handleOnline = () => setIsOnline(true); + const handleOffline = () => setIsOnline(false); + + window.addEventListener('online', handleOnline); + window.addEventListener('offline', handleOffline); + + return () => { + window.removeEventListener('online', handleOnline); + window.removeEventListener('offline', handleOffline); + }; + }, []); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setLoading(true); + + // Simulated login delay + await new Promise(resolve => setTimeout(resolve, 800)); + + // Hardcoded credentials für Phase 1 + if (username === 'admin' && password === 'Admin#1234') { + // Login erfolgreich + localStorage.setItem('isAuthenticated', 'true'); + localStorage.setItem('userRole', 'admin'); + localStorage.setItem('username', username); + navigate('/admin'); + } else { + setError('Ungültige Anmeldedaten. Bitte überprüfen Sie Benutzername und Passwort.'); + setLoading(false); + } + }; + + return ( + + {/* Internet-Status Anzeige */} + + {isOnline ? ( + <> + + Online + + ) : ( + <> + + Offline + + )} + + + + {/* Logo & Titel */} + + + Turnierverwaltung + + + Österreichischer Pferdesportverband + + + + {/* Fehler-Anzeige */} + {error && ( + + {error} + + )} + + {/* Login-Formular */} +
+ + setUsername(e.target.value)} + fullWidth + autoFocus + disabled={loading} + sx={{'& .MuiInputBase-input': {fontSize: '12px'}}} + /> + + setPassword(e.target.value)} + fullWidth + disabled={loading} + sx={{'& .MuiInputBase-input': {fontSize: '12px'}}} + InputProps={{ + endAdornment: ( + + setShowPassword(!showPassword)} + edge="end" + size="small" + > + {showPassword ? : } + + + ), + }} + /> + + + +
+ + {/* Hinweis */} + + + Demo-Zugang (Phase 1): + + + Benutzer: admin
+ Passwort: Admin#1234 +
+
+
+
+ ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/NennungenTabelle.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/NennungenTabelle.tsx new file mode 100644 index 00000000..1af2c8c8 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/NennungenTabelle.tsx @@ -0,0 +1,129 @@ +import {useState} from 'react'; +import Box from '@mui/material/Box'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import IconButton from '@mui/material/IconButton'; +import RefreshIcon from '@mui/icons-material/Refresh'; + +interface Props { + nennungen: any[]; + selectedPferd: any; + selectedReiter: any; +} + +export function NennungenTabelle({nennungen, selectedPferd, selectedReiter}: Props) { + const [tabValue, setTabValue] = useState(0); + + // Filter basierend auf Tab + const getFilteredNennungen = () => { + if (!selectedPferd && !selectedReiter) return []; + + switch (tabValue) { + case 0: // Reiter + return selectedReiter + ? nennungen.filter(n => n.reiter === selectedReiter.vorname + ' ' + selectedReiter.nachname) + : []; + case 1: // Pferd + return selectedPferd + ? nennungen.filter(n => n.pferd === selectedPferd.name) + : []; + case 2: // Bewerbe + return (selectedPferd && selectedReiter) + ? nennungen.filter(n => + n.pferd === selectedPferd.name && + n.reiter === selectedReiter.vorname + ' ' + selectedReiter.nachname + ) + : []; + default: + return []; + } + }; + + const filteredNennungen = getFilteredNennungen(); + + return ( + + setTabValue(v)} + sx={{borderBottom: 1, borderColor: 'divider', minHeight: 32}}> + + + + + + + + + + + Aktualisieren + + + {filteredNennungen.length} Nennungen + + + + + + + + + + Tag + Pl. + Bewerb + Bewerbsname + Bemerkung + Pferd + + + + {filteredNennungen.length === 0 ? ( + + + Keine Nennungen vorhanden + + + ) : ( + filteredNennungen.map((nennung, idx) => ( + + {nennung.tag} + {nennung.platz} + {nennung.bewerbNr} + {nennung.bewerbName} + {nennung.startwunsch || '-'} + {nennung.pferd} + + )) + )} + +
+
+
+ ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/NennungsMaske.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/NennungsMaske.tsx new file mode 100644 index 00000000..bdee0109 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/NennungsMaske.tsx @@ -0,0 +1,115 @@ +import {useState} from 'react'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import {PferdReiterEingabe} from './PferdReiterEingabe'; +import {NennungenTabelle} from './NennungenTabelle'; +import {VerkaufBuchungen} from './VerkaufBuchungen'; +import {Bewerbsliste} from './Bewerbsliste'; +import ListIcon from '@mui/icons-material/List'; +import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; +import ReceiptIcon from '@mui/icons-material/Receipt'; + +export function NennungsMaske() { + const [selectedPferd, setSelectedPferd] = useState(null); + const [selectedReiter, setSelectedReiter] = useState(null); + const [nennungen, setNennungen] = useState([]); + + const handleNennung = (bewerb: any) => { + if (selectedPferd && selectedReiter) { + const neueNennung = { + tag: bewerb.tag, + platz: bewerb.platz, + bewerbNr: bewerb.nr, + bewerbName: bewerb.name, + beginn: bewerb.beginn, + pferd: selectedPferd.name, + reiter: `${selectedReiter.vorname} ${selectedReiter.nachname}`, + startwunsch: null, + }; + setNennungen([...nennungen, neueNennung]); + } + }; + + return ( + + {/* Zeile 1 (50% Höhe): Pferd/Reiter Suche + Verkauf/Buchungen */} + + {/* Links: Pferd & Reiter Eingabe (60%) */} + + + + + {/* Rechts: Verkauf/Buchungen (40%) */} + + + + + + {/* Zeile 2 (5% Höhe): Navigation Buttons */} + + + + + + + {/* Zeile 3 (45% Höhe): Nennungsübersicht + Bewerbsübersicht */} + + {/* Links: Nennungsübersicht (60%) */} + + + + + {/* Rechts: Bewerbsübersicht (40%) */} + + + + + + ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/NeuerVeranstalter.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/NeuerVeranstalter.tsx new file mode 100644 index 00000000..764fbeeb --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/NeuerVeranstalter.tsx @@ -0,0 +1,286 @@ +import {useState} from 'react'; +import {useNavigate} from 'react-router'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import Paper from '@mui/material/Paper'; +import TextField from '@mui/material/TextField'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import IconButton from '@mui/material/IconButton'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; +import Link from '@mui/material/Link'; +import Alert from '@mui/material/Alert'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import HomeIcon from '@mui/icons-material/Home'; + +export function NeuerVeranstalter() { + const navigate = useNavigate(); + + const [vereinsname, setVereinsname] = useState(''); + const [oepsNummer, setOepsNummer] = useState(''); + const [email, setEmail] = useState(''); + const [telefon, setTelefon] = useState(''); + const [ansprechpartner, setAnsprechpartner] = useState(''); + const [strasse, setStrasse] = useState(''); + const [plz, setPlz] = useState(''); + const [ort, setOrt] = useState(''); + + const handleZurueck = () => { + navigate('/veranstalter/auswahl'); + }; + + const handleZuAdmin = () => { + navigate('/admin'); + }; + + const handleSpeichern = () => { + // TODO: Backend-Integration + console.log('Neuer Veranstalter:', { + vereinsname, + oepsNummer, + email, + telefon, + ansprechpartner, + strasse, + plz, + ort + }); + + // Simuliere erfolgreiche Speicherung + alert(`Veranstalter "${vereinsname}" wurde angelegt.\n\nLogin-Daten wurden an ${email} verschickt.`); + navigate('/veranstalter/auswahl'); + }; + + return ( + + {/* Header mit Navigation */} + + + + + + + + + + Admin - Verwaltung + + + Veranstalter auswählen + + + Neuer Veranstalter + + + + + + {/* Content */} + + + {/* Header */} + + + Neuen Veranstalter anlegen + + + Legen Sie einen neuen Veranstalter (Verein) mit OEPS-Daten an. Nach dem Speichern werden automatisch + Login-Daten generiert. + + + + {/* Info Alert */} + + + Login-Daten werden automatisch verschickt + + Nach dem Anlegen werden Login-Daten generiert und an die angegebene E-Mail-Adresse verschickt. + Der Veranstalter kann dann sein Profil selbst vervollständigen. + + + {/* Formular */} + + + {/* Vereinsdaten */} + + + Vereinsdaten + + + + setVereinsname(e.target.value)} + placeholder="z.B. Reit- und Fahrverein Wels" + sx={{'& .MuiInputBase-input': {fontSize: '11px'}}} + InputLabelProps={{sx: {fontSize: '11px'}}} + /> + + setOepsNummer(e.target.value)} + placeholder="z.B. V-OOE-1234" + helperText="Offizielle Vereinsnummer des OEPS" + sx={{'& .MuiInputBase-input': {fontSize: '11px'}}} + InputLabelProps={{sx: {fontSize: '11px'}}} + FormHelperTextProps={{sx: {fontSize: '9px'}}} + /> + + + + {/* Kontaktdaten */} + + + Kontaktdaten + + + + setAnsprechpartner(e.target.value)} + placeholder="z.B. Maria Huber" + sx={{'& .MuiInputBase-input': {fontSize: '11px'}}} + InputLabelProps={{sx: {fontSize: '11px'}}} + /> + + setEmail(e.target.value)} + placeholder="z.B. office@rfv-wels.at" + helperText="Login-Daten werden an diese Adresse verschickt" + sx={{'& .MuiInputBase-input': {fontSize: '11px'}}} + InputLabelProps={{sx: {fontSize: '11px'}}} + FormHelperTextProps={{sx: {fontSize: '9px'}}} + /> + + setTelefon(e.target.value)} + placeholder="z.B. +43 7242 12345" + sx={{'& .MuiInputBase-input': {fontSize: '11px'}}} + InputLabelProps={{sx: {fontSize: '11px'}}} + /> + + + + {/* Adresse */} + + + Adresse + + + + setStrasse(e.target.value)} + placeholder="z.B. Reitweg 15" + sx={{'& .MuiInputBase-input': {fontSize: '11px'}}} + InputLabelProps={{sx: {fontSize: '11px'}}} + /> + + + setPlz(e.target.value)} + placeholder="z.B. 4600" + sx={{ + width: 120, + '& .MuiInputBase-input': {fontSize: '11px'} + }} + InputLabelProps={{sx: {fontSize: '11px'}}} + /> + + setOrt(e.target.value)} + placeholder="z.B. Wels" + sx={{'& .MuiInputBase-input': {fontSize: '11px'}}} + InputLabelProps={{sx: {fontSize: '11px'}}} + /> + + + + + + + {/* Action Buttons */} + + + + + + + + ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/PferdReiterEingabe.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/PferdReiterEingabe.tsx new file mode 100644 index 00000000..42488218 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/PferdReiterEingabe.tsx @@ -0,0 +1,558 @@ +import {useState, useEffect, useRef} from 'react'; +import Box from '@mui/material/Box'; +import TextField from '@mui/material/TextField'; +import Button from '@mui/material/Button'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; +import Chip from '@mui/material/Chip'; +import Badge from '@mui/material/Badge'; + +// Mock-Daten für Pferde +const mockPferde = [ + { + id: 1, + kopfnr: 'A123', + name: "Obora's Donna", + rasse: 'Hannoveraner', + farbe: 'Brauner', + besitzer: 'Franz Huber', + stall: 'Box 12' + }, + { + id: 2, + kopfnr: 'H597', + name: 'Weltmeyer', + rasse: 'Trakehner', + farbe: 'Schimmel', + besitzer: 'Maria Gruber', + stall: 'Box 8' + }, + { + id: 3, + kopfnr: '9939', + name: 'Rubinstein', + rasse: 'Westfale', + farbe: 'Fuchs', + besitzer: 'Johann Maier', + stall: 'Box 15' + }, + { + id: 4, + kopfnr: 'D456', + name: "Obora's Danilo", + rasse: 'Oldenburger', + farbe: 'Rappe', + besitzer: 'Anna Schmidt', + stall: 'Box 3' + }, + { + id: 5, + kopfnr: '4568', + name: 'Domino', + rasse: 'Holsteiner', + farbe: 'Brauner', + besitzer: 'Thomas Bauer', + stall: 'Box 5' + }, + { + id: 6, + kopfnr: 'B789', + name: "Obora's Dream", + rasse: 'Hannoveraner', + farbe: 'Fuchs', + besitzer: 'Franz Huber', + stall: 'Box 14' + }, +]; + +// Mock-Daten für Reiter +const mockReiter = [ + { + id: 1, + kopfnr: '201', + vorname: 'Anna', + nachname: 'Schneider', + verein: 'RV Wien', + lizenz: 'LNR-2024-4587', + lizenzGueltig: true, + kontoSaldo: 0, + geburtsjahr: 1995 + }, + { + id: 2, + kopfnr: '202', + vorname: 'Thomas', + nachname: 'Bauer', + verein: 'RC Graz', + lizenz: 'LNR-2023-1234', + lizenzGueltig: false, + kontoSaldo: -125.50, + geburtsjahr: 1998 + }, + { + id: 3, + kopfnr: '203', + vorname: 'Sophie', + nachname: 'Wagner', + verein: 'RFV Salzburg', + lizenz: 'LNR-2024-9876', + lizenzGueltig: true, + kontoSaldo: 50.00, + geburtsjahr: 1992 + }, + { + id: 4, + kopfnr: '204', + vorname: 'Michael', + nachname: 'Müller', + verein: 'RC Innsbruck', + lizenz: 'LNR-2024-5555', + lizenzGueltig: true, + kontoSaldo: 0, + geburtsjahr: 2001 + }, + { + id: 5, + kopfnr: '205', + vorname: 'Franz', + nachname: 'Huber', + verein: 'RV Linz', + lizenz: 'LNR-2024-7777', + lizenzGueltig: true, + kontoSaldo: 0, + geburtsjahr: 2002 + }, + { + id: 6, + kopfnr: '206', + vorname: 'Franz', + nachname: 'Huber', + verein: 'RC Wien', + lizenz: 'LNR-2024-8888', + lizenzGueltig: true, + kontoSaldo: 0, + geburtsjahr: 1998 + }, +]; + +// Mock-Daten für bereits getätigte Nennungen (IMS = Im System) +const turnieNennungen = [ + {reiterId: 2, pferdId: 5, bewerbNr: 3}, // Thomas Bauer mit Domino in Bewerb 3 + {reiterId: 1, pferdId: 1, bewerbNr: 2}, // Anna Schneider mit Obora's Donna in Bewerb 2 + {reiterId: 1, pferdId: 2, bewerbNr: 5}, // Anna Schneider mit Weltmeyer in Bewerb 5 +]; + +interface Props { + selectedPferd: any; + setSelectedPferd: (pferd: any) => void; + selectedReiter: any; + setSelectedReiter: (reiter: any) => void; +} + +export function PferdReiterEingabe({selectedPferd, setSelectedPferd, selectedReiter, setSelectedReiter}: Props) { + const [pferdSuche, setPferdSuche] = useState(''); + const [reiterSuche, setReiterSuche] = useState(''); + const [pferdErgebnisse, setPferdErgebnisse] = useState([]); + const [reiterErgebnisse, setReiterErgebnisse] = useState([]); + const [selectedPferdIndex, setSelectedPferdIndex] = useState(0); + const [selectedReiterIndex, setSelectedReiterIndex] = useState(0); + + const pferdInputRef = useRef(null); + const reiterInputRef = useRef(null); + + // Autofokus auf Pferd-Suchfeld beim Laden + useEffect(() => { + pferdInputRef.current?.focus(); + }, []); + + // Pferd-Suche + useEffect(() => { + if (pferdSuche.length > 0) { + // Normale Suche nach Eingabe + const results = mockPferde.filter(p => + p.kopfnr.toLowerCase().includes(pferdSuche.toLowerCase()) || + p.name.toLowerCase().includes(pferdSuche.toLowerCase()) + ); + setPferdErgebnisse(results); + setSelectedPferdIndex(0); + } else if (selectedReiter && !pferdSuche) { + // Cross-Reference: Zeige Pferde des ausgewählten Reiters + const reiterPferde = turnieNennungen + .filter(n => n.reiterId === selectedReiter.id) + .map(n => mockPferde.find(p => p.id === n.pferdId)) + .filter(Boolean); + setPferdErgebnisse(reiterPferde); + } else { + setPferdErgebnisse([]); + } + }, [pferdSuche, selectedReiter]); + + // Reiter-Suche + useEffect(() => { + if (reiterSuche.length > 0) { + // Normale Suche nach Eingabe + const results = mockReiter.filter(r => + r.vorname.toLowerCase().includes(reiterSuche.toLowerCase()) || + r.nachname.toLowerCase().includes(reiterSuche.toLowerCase()) || + `${r.vorname} ${r.nachname}`.toLowerCase().includes(reiterSuche.toLowerCase()) + ); + setReiterErgebnisse(results); + setSelectedReiterIndex(0); + } else if (selectedPferd && !reiterSuche) { + // Cross-Reference: Zeige Reiter des ausgewählten Pferdes + const pferdReiter = turnieNennungen + .filter(n => n.pferdId === selectedPferd.id) + .map(n => mockReiter.find(r => r.id === n.reiterId)) + .filter(Boolean); + setReiterErgebnisse(pferdReiter); + } else { + setReiterErgebnisse([]); + } + }, [reiterSuche, selectedPferd]); + + // Hilfsfunktion: Prüft ob Pferd im System ist (IMS) + const isPferdIMS = (pferdId: number) => { + return turnieNennungen.some(n => n.pferdId === pferdId); + }; + + // Hilfsfunktion: Prüft ob Reiter im System ist (IMS) + const isReiterIMS = (reiterId: number) => { + return turnieNennungen.some(n => n.reiterId === reiterId); + }; + + // Pferd auswählen + const handlePferdAuswahl = (pferd: any) => { + setSelectedPferd(pferd); + + // Cross-Reference: Zeige Reiter dieses Pferdes + const pferdReiter = turnieNennungen + .filter(n => n.pferdId === pferd.id) + .map(n => mockReiter.find(r => r.id === n.reiterId)) + .filter(Boolean); + + if (pferdReiter.length > 0) { + setReiterErgebnisse(pferdReiter); + } + + reiterInputRef.current?.focus(); + }; + + // Reiter auswählen + const handleReiterAuswahl = (reiter: any) => { + setSelectedReiter(reiter); + + // Cross-Reference: Zeige Pferde dieses Reiters + const reiterPferde = turnieNennungen + .filter(n => n.reiterId === reiter.id) + .map(n => mockPferde.find(p => p.id === n.pferdId)) + .filter(Boolean); + + if (reiterPferde.length > 0) { + setPferdErgebnisse(reiterPferde); + } + }; + + // Keyboard Navigation für Pferd + const handlePferdKeyDown = (e: React.KeyboardEvent) => { + if (pferdErgebnisse.length === 0) return; + + if (e.key === 'ArrowDown') { + e.preventDefault(); + setSelectedPferdIndex(prev => Math.min(prev + 1, pferdErgebnisse.length - 1)); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setSelectedPferdIndex(prev => Math.max(prev - 1, 0)); + } else if (e.key === 'Enter') { + e.preventDefault(); + if (pferdErgebnisse[selectedPferdIndex]) { + handlePferdAuswahl(pferdErgebnisse[selectedPferdIndex]); + } + } + }; + + // Keyboard Navigation für Reiter + const handleReiterKeyDown = (e: React.KeyboardEvent) => { + if (reiterErgebnisse.length === 0) return; + + if (e.key === 'ArrowDown') { + e.preventDefault(); + setSelectedReiterIndex(prev => Math.min(prev + 1, reiterErgebnisse.length - 1)); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + setSelectedReiterIndex(prev => Math.max(prev - 1, 0)); + } else if (e.key === 'Enter') { + e.preventDefault(); + if (reiterErgebnisse[selectedReiterIndex]) { + handleReiterAuswahl(reiterErgebnisse[selectedReiterIndex]); + } + } + }; + + const handlePferdLeeren = () => { + setPferdSuche(''); + setSelectedPferd(null); + setPferdErgebnisse([]); + pferdInputRef.current?.focus(); + }; + + const handleReiterLeeren = () => { + setReiterSuche(''); + setSelectedReiter(null); + setReiterErgebnisse([]); + reiterInputRef.current?.focus(); + }; + + return ( + + {/* Linke Hälfte: Pferd */} + + {/* Eingabefeld */} + + + Pferd: + + setPferdSuche(e.target.value)} + onKeyDown={handlePferdKeyDown} + sx={{ + flex: 1, + '& .MuiInputBase-input': {fontSize: '11px', py: 0.75}, + }} + /> + + + + + {/* Suchergebnisse - bleiben immer sichtbar */} + + + {pferdErgebnisse.length > 0 ? ( + (pferdSuche ? pferdErgebnisse : pferdErgebnisse.slice(0, 4)).map((pferd, idx) => { + const istIMS = isPferdIMS(pferd.id); + return ( + + handlePferdAuswahl(pferd)} + sx={{py: 0.25, display: 'flex', gap: 1}} + > + + {istIMS && ( + + )} + + + ); + }) + ) : ( + + + + )} + + + + {/* Pferd Details - erscheint nach Auswahl */} + {selectedPferd && ( + + + Pferd Details + + + Kopfnummer: {selectedPferd.kopfnr} + + + Name: {selectedPferd.name} + + + Rasse: {selectedPferd.rasse} + + + Farbe: {selectedPferd.farbe} + + + Besitzer: {selectedPferd.besitzer} + + + Stall: {selectedPferd.stall} + + + )} + + {/* Buttons */} + + + + + + + {/* Rechte Hälfte: Reiter */} + + {/* Eingabefeld */} + + + Reiter: + + setReiterSuche(e.target.value)} + onKeyDown={handleReiterKeyDown} + sx={{ + flex: 1, + '& .MuiInputBase-input': {fontSize: '11px', py: 0.75}, + }} + /> + + + + + {/* Suchergebnisse - bleiben immer sichtbar */} + + + {reiterErgebnisse.length > 0 ? ( + (reiterSuche ? reiterErgebnisse : reiterErgebnisse.slice(0, 4)).map((reiter, idx) => { + const istIMS = isReiterIMS(reiter.id); + return ( + + handleReiterAuswahl(reiter)} + sx={{py: 0.25, display: 'flex', gap: 1}} + > + + {istIMS && ( + + )} + + + ); + }) + ) : ( + + + + )} + + + + {/* Reiter Details - erscheint nach Auswahl */} + {selectedReiter && ( + + + Reiter Details + + + Name: {selectedReiter.vorname} {selectedReiter.nachname} + + + Verein: {selectedReiter.verein} + + + + Lizenz: {selectedReiter.lizenz} + + + + + Konto-Saldo: €{selectedReiter.kontoSaldo.toFixed(2)} + + + )} + + {/* Buttons */} + + + + + + + ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/TurnierAnsicht.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/TurnierAnsicht.tsx new file mode 100644 index 00000000..b154f88b --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/TurnierAnsicht.tsx @@ -0,0 +1,142 @@ +import {useState} from 'react'; +import {useParams, useNavigate} from 'react-router'; +import Box from '@mui/material/Box'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; +import Link from '@mui/material/Link'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import HomeIcon from '@mui/icons-material/Home'; +import {StammdatenTab} from './turnier/StammdatenTab'; +import {OrganisationTab} from './turnier/OrganisationTab'; +import {BewerbeTab} from './turnier/BewerbeTab'; +import {ArtikelTab} from './turnier/ArtikelTab'; +import {AbrechnungTab} from './turnier/AbrechnungTab'; +import {NennungenTab} from './turnier/NennungenTab'; +import {StartlistenTab} from './turnier/StartlistenTab'; +import {ErgebnislistenTab} from './turnier/ErgebnislistenTab'; +import {veranstaltungenData} from './Dashboard'; + +export function TurnierAnsicht() { + const params = useParams(); + const navigate = useNavigate(); + const veranstaltungId = params.veranstaltungId; + const turnierNr = params.nr; + + // Bei neu: Direkt zu Stammdaten (Tab 0), sonst Stammdaten (Tab 0) + const [activeTab, setActiveTab] = useState(0); + + // Veranstaltung laden + const veranstaltung = veranstaltungId !== 'neu' + ? veranstaltungenData.find(v => v.id === parseInt(veranstaltungId || '0')) + : null; + + // Turnier laden (wenn nicht neu) + const turnier = turnierNr !== 'neu' && veranstaltung + ? veranstaltung.turniere.find(t => t.nr === turnierNr) + : null; + + const handleZurueck = () => { + navigate(`/veranstaltung/${veranstaltungId}`); + }; + + const handleToAdmin = () => { + navigate('/admin'); + }; + + return ( + + {/* Header mit Navigation */} + + + + + + + + + + Admin - Verwaltung + + + {veranstaltung?.name || 'Veranstaltung'} + + + {turnier ? `Turnier ${turnier.nr}` : 'Neues Turnier'} + + + + + + {/* Tab Navigation */} + setActiveTab(v)} + sx={{ + borderBottom: 1, + borderColor: 'divider', + bgcolor: 'background.paper', + '& .MuiTab-root': { + fontSize: '11px', + minHeight: 36, + py: 1, + } + }} + > + + + + + + + + + + + {/* Tab Content */} + + {activeTab === 0 && } + {activeTab === 1 && } + {activeTab === 2 && } + {activeTab === 3 && } + {activeTab === 4 && } + {activeTab === 5 && } + {activeTab === 6 && } + {activeTab === 7 && } + + + ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/TurnierErstellen.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/TurnierErstellen.tsx new file mode 100644 index 00000000..e0c7b0d9 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/TurnierErstellen.tsx @@ -0,0 +1,145 @@ +import {useState} from 'react'; +import {useParams, useNavigate} from 'react-router'; +import Box from '@mui/material/Box'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; +import Link from '@mui/material/Link'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import HomeIcon from '@mui/icons-material/Home'; +import {VeranstaltungUebersicht} from './turnier/VeranstaltungUebersicht'; +import {veranstaltungenData} from './Dashboard'; +import {StammdatenTab} from './turnier/StammdatenTab'; +import {OrganisationTab} from './turnier/OrganisationTab'; +import {BewerbeTab} from './turnier/BewerbeTab'; +import {ArtikelTab} from './turnier/ArtikelTab'; + +export function TurnierErstellen() { + const params = useParams(); + const navigate = useNavigate(); + const id = params.id; + + // Bei neu: Direkt zu Stammdaten (Tab 1), sonst Veranstaltung - Übersicht (Tab 0) + const [activeTab, setActiveTab] = useState(id === 'neu' ? 1 : 0); + + // Veranstaltung laden + const veranstaltung = id !== 'neu' + ? veranstaltungenData.find(v => v.id === parseInt(id || '0')) + : null; + + const handleZurueck = () => { + navigate('/admin'); + }; + + // Für bestehende Veranstaltungen: Nur "Veranstaltung - Übersicht" Tab + // Für neue Veranstaltungen: Alle Tabs anzeigen + const istNeueVeranstaltung = id === 'neu'; + const istBestehendeVeranstaltung = !istNeueVeranstaltung && veranstaltung; + + return ( + + {/* Header mit Navigation */} + + + + + + + + + + Admin - Verwaltung + + + {veranstaltung?.name || 'Neue Veranstaltung'} + + + + + + {/* Tab Navigation */} + {istBestehendeVeranstaltung ? ( + // Nur "Veranstaltung - Übersicht" für bestehende Veranstaltungen + + + + ) : ( + // Alle Tabs für neue Veranstaltungen + setActiveTab(v)} + sx={{ + borderBottom: 1, + borderColor: 'divider', + bgcolor: 'background.paper', + '& .MuiTab-root': { + fontSize: '11px', + minHeight: 36, + py: 1, + } + }} + > + + + + + + + )} + + {/* Tab Content */} + + {istBestehendeVeranstaltung ? ( + // Nur Veranstaltung - Übersicht für bestehende Veranstaltungen + + ) : ( + // Alle Tabs für neue Veranstaltungen + <> + {activeTab === 0 && } + {activeTab === 1 && } + {activeTab === 2 && } + {activeTab === 3 && } + {activeTab === 4 && } + + )} + + + ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/VeranstalterAuswahl.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/VeranstalterAuswahl.tsx new file mode 100644 index 00000000..9054556c --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/VeranstalterAuswahl.tsx @@ -0,0 +1,263 @@ +import {useState} from 'react'; +import {useNavigate} from 'react-router'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import Paper from '@mui/material/Paper'; +import TextField from '@mui/material/TextField'; +import InputAdornment from '@mui/material/InputAdornment'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import IconButton from '@mui/material/IconButton'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; +import Link from '@mui/material/Link'; +import Chip from '@mui/material/Chip'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import HomeIcon from '@mui/icons-material/Home'; +import SearchIcon from '@mui/icons-material/Search'; +import AddIcon from '@mui/icons-material/Add'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import {veranstalterData, Veranstalter} from '../types/veranstalter'; + +export function VeranstalterAuswahl() { + const navigate = useNavigate(); + const [searchTerm, setSearchTerm] = useState(''); + const [selectedVeranstalter, setSelectedVeranstalter] = useState(null); + + const filteredVeranstalter = veranstalterData.filter(v => + v.vereinsname.toLowerCase().includes(searchTerm.toLowerCase()) || + v.oepsNummer.toLowerCase().includes(searchTerm.toLowerCase()) || + v.ort.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + const handleZurueck = () => { + navigate('/admin'); + }; + + const handleNeuerVeranstalter = () => { + navigate('/veranstalter/neu'); + }; + + const handleWeiter = () => { + if (selectedVeranstalter) { + // Gehe zur Veranstalter-Übersicht + navigate(`/veranstalter/${selectedVeranstalter.id}`); + } + }; + + return ( + + {/* Header mit Navigation */} + + + + + + + + + + Admin - Verwaltung + + + Veranstalter auswählen + + + + + + {/* Content */} + + + {/* Header */} + + + Veranstalter für neue Veranstaltung auswählen + + + Wählen Sie einen bestehenden Veranstalter aus oder legen Sie einen neuen Veranstalter an. + + + + {/* Toolbar */} + + + setSearchTerm(e.target.value)} + sx={{ + flex: 1, + '& .MuiInputBase-input': {fontSize: '11px'} + }} + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + + + + + {/* Tabelle */} + + + + + + Vereinsname + OEPS-Nummer + Ort + Ansprechpartner + E-Mail + Login + + + + {filteredVeranstalter.length === 0 && ( + + + + {searchTerm ? 'Keine Veranstalter gefunden' : 'Noch keine Veranstalter angelegt'} + + + + )} + {filteredVeranstalter.map((veranstalter) => ( + setSelectedVeranstalter(veranstalter)} + selected={selectedVeranstalter?.id === veranstalter.id} + sx={{ + cursor: 'pointer', + '&.Mui-selected': { + bgcolor: 'primary.lighter', + } + }} + > + + {selectedVeranstalter?.id === veranstalter.id && ( + + )} + + + {veranstalter.vereinsname} + + + {veranstalter.oepsNummer} + + + {veranstalter.plz} {veranstalter.ort} + + + {veranstalter.ansprechpartner} + + + {veranstalter.email} + + + {veranstalter.hasLogin ? ( + + ) : ( + + )} + + + ))} + +
+
+ + {/* Info-Box */} + + + ℹ️ Hinweis zu Veranstaltern + + + Veranstalter sind Vereine, die beim österreichischen Pferdesportverband (OEPS) registriert sind. + Beim Anlegen eines neuen Veranstalters werden automatisch Login-Daten generiert und per E-Mail verschickt. + Der Veranstalter kann dann sein Profil (Logo, Kontaktdaten, etc.) selbst verwalten. + + + + {/* Action Buttons */} + + + + +
+
+
+ ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/VeranstalterProfil.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/VeranstalterProfil.tsx new file mode 100644 index 00000000..b347f540 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/VeranstalterProfil.tsx @@ -0,0 +1,338 @@ +import {useState} from 'react'; +import {useNavigate, useParams} from 'react-router'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import Paper from '@mui/material/Paper'; +import TextField from '@mui/material/TextField'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import IconButton from '@mui/material/IconButton'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; +import Link from '@mui/material/Link'; +import Alert from '@mui/material/Alert'; +import Avatar from '@mui/material/Avatar'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import HomeIcon from '@mui/icons-material/Home'; +import CloudUploadIcon from '@mui/icons-material/CloudUpload'; +import {veranstalterData} from '../types/veranstalter'; + +export function VeranstalterProfil() { + const navigate = useNavigate(); + const params = useParams(); + const veranstalterId = params.id; + + // Lade Veranstalter-Daten + const veranstalter = veranstalterData.find(v => v.id === parseInt(veranstalterId || '0')); + + const [vereinsname, setVereinsname] = useState(veranstalter?.vereinsname || ''); + const [oepsNummer, setOepsNummer] = useState(veranstalter?.oepsNummer || ''); + const [email, setEmail] = useState(veranstalter?.email || ''); + const [telefon, setTelefon] = useState(veranstalter?.telefon || ''); + const [ansprechpartner, setAnsprechpartner] = useState(veranstalter?.ansprechpartner || ''); + const [strasse, setStrasse] = useState(veranstalter?.strasse || ''); + const [plz, setPlz] = useState(veranstalter?.plz || ''); + const [ort, setOrt] = useState(veranstalter?.ort || ''); + const [logo, setLogo] = useState(veranstalter?.logo || ''); + + const handleZurueck = () => { + navigate('/admin'); + }; + + const handleSpeichern = () => { + // TODO: Backend-Integration + console.log('Veranstalter-Profil speichern:', { + vereinsname, + oepsNummer, + email, + telefon, + ansprechpartner, + strasse, + plz, + ort, + logo + }); + + alert('Profil wurde erfolgreich aktualisiert!'); + }; + + const handleLogoUpload = () => { + // TODO: File-Upload Integration + alert('Logo-Upload wird implementiert'); + }; + + if (!veranstalter) { + return ( + + Veranstalter nicht gefunden + + ); + } + + return ( + + {/* Header mit Navigation */} + + + + + + + + + + Admin - Verwaltung + + + {vereinsname} + + + + + + {/* Content */} + + + {/* Header */} + + + Veranstalter-Profil + + + Verwalten Sie die Profildaten des Veranstalters. Diese Daten werden in Ausschreibungen und offiziellen + Dokumenten verwendet. + + + + {/* Login-Status */} + {!veranstalter.hasLogin && ( + + + Login-Daten noch nicht aktiviert + + Der Veranstalter hat sein Konto noch nicht aktiviert. Die Login-Daten wurden an {email} verschickt. + + )} + + {/* Logo-Upload */} + + + Vereinslogo + + + + + {vereinsname.charAt(0)} + + + + + Empfohlene Größe: 400x400px, Format: PNG oder JPG + + + + + + + {/* Formular */} + + + {/* Vereinsdaten */} + + + Vereinsdaten + + + + setVereinsname(e.target.value)} + sx={{'& .MuiInputBase-input': {fontSize: '11px'}}} + InputLabelProps={{sx: {fontSize: '11px'}}} + /> + + + + + + {/* Kontaktdaten */} + + + Kontaktdaten + + + + setAnsprechpartner(e.target.value)} + sx={{'& .MuiInputBase-input': {fontSize: '11px'}}} + InputLabelProps={{sx: {fontSize: '11px'}}} + /> + + setEmail(e.target.value)} + sx={{'& .MuiInputBase-input': {fontSize: '11px'}}} + InputLabelProps={{sx: {fontSize: '11px'}}} + /> + + setTelefon(e.target.value)} + sx={{'& .MuiInputBase-input': {fontSize: '11px'}}} + InputLabelProps={{sx: {fontSize: '11px'}}} + /> + + + + {/* Adresse */} + + + Adresse + + + + setStrasse(e.target.value)} + sx={{'& .MuiInputBase-input': {fontSize: '11px'}}} + InputLabelProps={{sx: {fontSize: '11px'}}} + /> + + + setPlz(e.target.value)} + sx={{ + width: 120, + '& .MuiInputBase-input': {fontSize: '11px'} + }} + InputLabelProps={{sx: {fontSize: '11px'}}} + /> + + setOrt(e.target.value)} + sx={{'& .MuiInputBase-input': {fontSize: '11px'}}} + InputLabelProps={{sx: {fontSize: '11px'}}} + /> + + + + + + + {/* Weitere Optionen */} + + + Login & Sicherheit + + + + + + + + + {/* Action Buttons */} + + + + + + + + ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/VeranstalterUebersicht.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/VeranstalterUebersicht.tsx new file mode 100644 index 00000000..aac7ff4d --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/VeranstalterUebersicht.tsx @@ -0,0 +1,481 @@ +import {useState} from 'react'; +import {useNavigate, useParams} from 'react-router'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import Paper from '@mui/material/Paper'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Grid from '@mui/material/Grid'; +import Avatar from '@mui/material/Avatar'; +import Chip from '@mui/material/Chip'; +import IconButton from '@mui/material/IconButton'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import Breadcrumbs from '@mui/material/Breadcrumbs'; +import Link from '@mui/material/Link'; +import Divider from '@mui/material/Divider'; +import TextField from '@mui/material/TextField'; +import InputAdornment from '@mui/material/InputAdornment'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; +import HomeIcon from '@mui/icons-material/Home'; +import AddIcon from '@mui/icons-material/Add'; +import SettingsIcon from '@mui/icons-material/Settings'; +import CalendarTodayIcon from '@mui/icons-material/CalendarToday'; +import LocationOnIcon from '@mui/icons-material/LocationOn'; +import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; +import EditIcon from '@mui/icons-material/Edit'; +import SearchIcon from '@mui/icons-material/Search'; +import {veranstalterData} from '../types/veranstalter'; +import {veranstaltungenData} from './Dashboard'; + +export function VeranstalterUebersicht() { + const navigate = useNavigate(); + const params = useParams(); + const veranstalterId = params.id; + + const [searchTerm, setSearchTerm] = useState(''); + const [statusFilter, setStatusFilter] = useState('alle'); + + // Lade Veranstalter-Daten + const veranstalter = veranstalterData.find(v => v.id === parseInt(veranstalterId || '0')); + + // Filter Veranstaltungen für diesen Veranstalter (später aus Backend) + // Für jetzt: Zeige alle Mock-Veranstaltungen + const alleVeranstaltungen = veranstaltungenData; + + // Filtern + const filteredVeranstaltungen = alleVeranstaltungen.filter(v => { + const matchesSearch = + v.name.toLowerCase().includes(searchTerm.toLowerCase()) || + v.ort.toLowerCase().includes(searchTerm.toLowerCase()) || + v.turniere.some(t => t.nr.includes(searchTerm)); + + const matchesStatus = statusFilter === 'alle' || v.status === statusFilter; + + return matchesSearch && matchesStatus; + }); + + const handleZurueck = () => { + navigate('/veranstalter/auswahl'); + }; + + const handleZuAdmin = () => { + navigate('/admin'); + }; + + const handleNeueVeranstaltung = () => { + // Erstelle Mock-Veranstaltung und navigiere zu ihr + // TODO: Backend-Call für neue Veranstaltung + const neueVeranstaltungId = Date.now(); // Temporäre ID + sessionStorage.setItem('selectedVeranstalterId', veranstalterId || ''); + navigate(`/veranstaltung/${neueVeranstaltungId}`); + }; + + const handleVeranstaltungOeffnen = (veranstaltungId: number) => { + navigate(`/veranstaltung/${veranstaltungId}`); + }; + + const handleProfilBearbeiten = () => { + navigate(`/veranstalter/${veranstalterId}/profil`); + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'vorbereitung': + return 'warning'; + case 'live': + return 'success'; + case 'abgeschlossen': + return 'default'; + default: + return 'default'; + } + }; + + const getStatusLabel = (status: string) => { + switch (status) { + case 'vorbereitung': + return 'Vorbereitung'; + case 'live': + return 'Live'; + case 'abgeschlossen': + return 'Abgeschlossen'; + default: + return status; + } + }; + + if (!veranstalter) { + return ( + + + Veranstalter nicht gefunden + + + ); + } + + return ( + + {/* Header mit Navigation */} + + + + + + + + + + Admin - Verwaltung + + + Veranstalter auswählen + + + {veranstalter.vereinsname} + + + + + + {/* Content */} + + + {/* Veranstalter-Info Card */} + + + {/* Logo */} + + {veranstalter.vereinsname.charAt(0)} + + + {/* Info */} + + + + + {veranstalter.vereinsname} + + + OEPS-Nummer: {veranstalter.oepsNummer} + + + + + + + + + + + Ansprechpartner + + + {veranstalter.ansprechpartner} + + + + + E-Mail + + + {veranstalter.email} + + + + + Telefon + + + {veranstalter.telefon} + + + + + Adresse + + + {veranstalter.strasse}
+ {veranstalter.plz} {veranstalter.ort} +
+
+ + + Login-Status + + {veranstalter.hasLogin ? ( + + ) : ( + + )} + + + + Mitglied seit + + + {new Date(veranstalter.createdAt).toLocaleDateString('de-AT')} + + +
+
+
+
+ + {/* Toolbar & Filter */} + + + + setSearchTerm(e.target.value)} + sx={{ + flex: 1, + maxWidth: 400, + '& .MuiInputBase-input': {fontSize: '11px'} + }} + InputProps={{ + startAdornment: ( + + + + ), + }} + /> + + + setStatusFilter('alle')} + color={statusFilter === 'alle' ? 'primary' : 'default'} + size="small" + sx={{fontSize: '10px'}} + /> + setStatusFilter('vorbereitung')} + color={statusFilter === 'vorbereitung' ? 'primary' : 'default'} + size="small" + sx={{fontSize: '10px'}} + /> + setStatusFilter('live')} + color={statusFilter === 'live' ? 'primary' : 'default'} + size="small" + sx={{fontSize: '10px'}} + /> + setStatusFilter('abgeschlossen')} + color={statusFilter === 'abgeschlossen' ? 'primary' : 'default'} + size="small" + sx={{fontSize: '10px'}} + /> + + + + {/* Veranstaltungen Liste */} + {filteredVeranstaltungen.length === 0 ? ( + + + {searchTerm || statusFilter !== 'alle' + ? 'Keine Veranstaltungen gefunden' + : 'Noch keine Veranstaltungen angelegt' + } + + + + ) : ( + + {filteredVeranstaltungen.map((veranstaltung) => ( + handleVeranstaltungOeffnen(veranstaltung.id)} + > + + + {/* Status */} + + + + + {/* Name & Details */} + + + {veranstaltung.name} + + + + + + + {veranstaltung.datum} + + + + + + + {veranstaltung.ort} + + + + + + + {veranstaltung.turniere.length} Turnier{veranstaltung.turniere.length !== 1 ? 'e' : ''} + + + + + + {/* Statistik */} + + + + Nennungen + + + {veranstaltung.nennungen} + + + + + Bewerbe + + + {veranstaltung.turniere.reduce((sum, t) => sum + t.bewerbeAnzahl, 0)} + + + + + Letzte Aktivität + + + {veranstaltung.letzteAktivitaet} + + + + + {/* Action Button */} + { + e.stopPropagation(); + handleVeranstaltungOeffnen(veranstaltung.id); + }} + sx={{ + width: 32, + height: 32, + '&:hover': { + bgcolor: 'primary.lighter' + } + }} + > + + + + + + ))} + + )} +
+
+
+ ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/VerkaufBuchungen.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/VerkaufBuchungen.tsx new file mode 100644 index 00000000..8fad1549 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/VerkaufBuchungen.tsx @@ -0,0 +1,213 @@ +import {useState} from 'react'; +import Box from '@mui/material/Box'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Toolbar from '@mui/material/Toolbar'; +import Typography from '@mui/material/Typography'; +import IconButton from '@mui/material/IconButton'; +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import AddIcon from '@mui/icons-material/Add'; +import RemoveIcon from '@mui/icons-material/Remove'; + +// Mock-Daten für Verkauf +const mockVerkaufArtikel = [ + {knr: '', text: 'Belastung', einzelpreis: 0, menge: 0, gebucht: '0.00'}, + {knr: '', text: 'Gutschrift', einzelpreis: 0, menge: 0, gebucht: '0.00'}, + {knr: '', text: 'Boxenpauschale', einzelpreis: 115.00, menge: 0, gebucht: '0.00'}, + {knr: '', text: 'Ansage', einzelpreis: 2.00, menge: 0, gebucht: '0.00'}, + {knr: '', text: 'Füttern', einzelpreis: 3.00, menge: 0, gebucht: '0.00'}, + {knr: '', text: 'Heu', einzelpreis: 13.00, menge: 0, gebucht: '0.00'}, + {knr: '', text: 'Späne', einzelpreis: 15.00, menge: 0, gebucht: '0.00'}, + {knr: '', text: 'Stroh', einzelpreis: 5.00, menge: 0, gebucht: '0.00'}, + {knr: '', text: 'Strom', einzelpreis: 50.00, menge: 0, gebucht: '0.00'}, + {knr: '', text: 'Y-Nummer', einzelpreis: 35.00, menge: 0, gebucht: '0.00'}, + {knr: '', text: 'Z-Nummer', einzelpreis: 10.00, menge: 0, gebucht: '0.00'}, +]; + +interface Props { + selectedReiter: any; +} + +export function VerkaufBuchungen({selectedReiter}: Props) { + const [tabValue, setTabValue] = useState(0); + const [verkaufMengen, setVerkaufMengen] = useState<{ [key: string]: number }>({}); + + const handleMengeChange = (text: string, delta: number) => { + setVerkaufMengen(prev => ({ + ...prev, + [text]: Math.max(0, (prev[text] || 0) + delta), + })); + }; + + return ( + + setTabValue(v)} + sx={{borderBottom: 1, borderColor: 'divider', minHeight: 32}}> + + + + + {tabValue === 0 && ( + <> + + + + + + Aktualisieren + + + {mockVerkaufArtikel.length} Artikel + + + + + + + + + + KNr + + + Menge + - + Buchungstext + Betrag + Gebucht + + + + {mockVerkaufArtikel.map((artikel, idx) => { + const menge = verkaufMengen[artikel.text] || 0; + const betrag = menge * artikel.einzelpreis; + return ( + + {artikel.knr} + + handleMengeChange(artikel.text, 1)} + sx={{width: 20, height: 20}} + > + + + + + setVerkaufMengen(prev => ({ + ...prev, + [artikel.text]: Math.max(0, parseInt(e.target.value) || 0), + }))} + sx={{ + width: 50, + '& .MuiInputBase-input': { + textAlign: 'center', + fontSize: '10px', + py: 0.25, + px: 0.5, + }, + }} + /> + + + handleMengeChange(artikel.text, -1)} + sx={{width: 20, height: 20}} + > + + + + {artikel.text} + 0 ? 600 : 400, py: 0.5}} align="right"> + {betrag.toFixed(2)} + + {artikel.gebucht} + + ); + })} + +
+
+ + )} + + {tabValue === 1 && ( + <> + + + + + + Aktualisieren + + + 0 Buchungen + + + + + + + + + Kopfnr + Menge + Buchungstext + Soll + Haben + + + + + + Keine Buchungen vorhanden + + + +
+
+ + )} +
+ ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/figma/ImageWithFallback.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/figma/ImageWithFallback.tsx new file mode 100644 index 00000000..ff6e48f5 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/figma/ImageWithFallback.tsx @@ -0,0 +1,27 @@ +import React, {useState} from 'react' + +const ERROR_IMG_SRC = + 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg==' + +export function ImageWithFallback(props: React.ImgHTMLAttributes) { + const [didError, setDidError] = useState(false) + + const handleError = () => { + setDidError(true) + } + + const {src, alt, style, className, ...rest} = props + + return didError ? ( +
+
+ Error loading image +
+
+ ) : ( + {alt} + ) +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/AbrechnungTab.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/AbrechnungTab.tsx new file mode 100644 index 00000000..2756f68f --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/AbrechnungTab.tsx @@ -0,0 +1,418 @@ +import {useState} from 'react'; +import Box from '@mui/material/Box'; +import Paper from '@mui/material/Paper'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import Typography from '@mui/material/Typography'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Button from '@mui/material/Button'; +import TextField from '@mui/material/TextField'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import Select from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; +import FormControl from '@mui/material/FormControl'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import ViewListIcon from '@mui/icons-material/ViewList'; +import DeleteSweepIcon from '@mui/icons-material/DeleteSweep'; +import RemoveCircleOutlineIcon from '@mui/icons-material/RemoveCircleOutline'; +import PrintIcon from '@mui/icons-material/Print'; +import ReceiptIcon from '@mui/icons-material/Receipt'; + +// Mock-Daten für Buchungen +const mockBuchungen = [ + {id: 1, text: 'Startgebühr Bewerb 12 - Dressur Kl. A', soll: 25.00, haben: 0, saldo: 25.00, status: 'offen'}, + {id: 2, text: 'Startgebühr Bewerb 15 - Springen Kl. B', soll: 30.00, haben: 0, saldo: 30.00, status: 'offen'}, + {id: 3, text: 'Nenngeld', soll: 15.00, haben: 0, saldo: 15.00, status: 'offen'}, + {id: 4, text: 'Box 3 Tage', soll: 45.00, haben: 0, saldo: 45.00, status: 'offen'}, +]; + +// Mock-Daten für Teilnehmer (Reiter/Pferde) +const mockTeilnehmer = [ + 'Anna Schneider - Obora\'s Donna', + 'Thomas Bauer - Domino', + 'Lisa Wagner - Bella', + 'Michael Gruber - Apollo', + 'Sarah Klein - Luna', +]; + +export function AbrechnungTab() { + const [hauptTab, setHauptTab] = useState(0); // Buchungen, Offene Posten, Rechnung + const [rechterTab, setRechterTab] = useState(2); // Auswahl, Verkauf, Buchungen, Adressen + const [selectedTeilnehmer, setSelectedTeilnehmer] = useState(''); + const [tabelleLeeren, setTabelleLeeren] = useState(false); + const [buchungsBetrag, setBuchungsBetrag] = useState('0.00'); + const [zahlungsart, setZahlungsart] = useState('bar'); + const [buchungen, setBuchungen] = useState(mockBuchungen); + + const gesamtSaldo = buchungen.reduce((sum, b) => sum + b.saldo, 0); + + const handleAktualisieren = () => { + console.log('Aktualisiere Buchungen für:', selectedTeilnehmer); + // TODO: Backend-Call + }; + + const handleTabelleLeeren = () => { + setBuchungen([]); + }; + + const handlePferdEntfernen = () => { + if (selectedTeilnehmer) { + console.log('Entferne Pferd:', selectedTeilnehmer); + setSelectedTeilnehmer(''); + } + }; + + const handleBuchen = () => { + console.log('Buche Betrag:', buchungsBetrag, 'Zahlungsart:', zahlungsart); + // TODO: Backend-Call + }; + + const handleSaldoDrucken = () => { + console.log('Drucke Saldo für:', selectedTeilnehmer); + // TODO: Druckfunktion + }; + + const handleRechnungDrucken = () => { + console.log('Drucke Rechnung für:', selectedTeilnehmer); + // TODO: Druckfunktion + }; + + const handleGebuehrBuchen = () => { + const gebuehr = zahlungsart === 'scheck' ? 30 : 0; + if (gebuehr > 0) { + console.log('Buche Gebühr:', gebuehr); + // TODO: Backend-Call + } + }; + + return ( + + {/* Linke Seite: Buchungstabelle (70%) */} + + {/* Haupt-Tabs */} + setHauptTab(v)} + sx={{ + borderBottom: 1, + borderColor: 'divider', + bgcolor: 'background.paper', + '& .MuiTab-root': {fontSize: '11px', minHeight: 36, py: 1} + }} + > + + + + + + {/* Tabellen-Aktionen */} + + + + + + + + {/* Buchungstabelle */} + + + + + + + Buchungstext + + + Soll + + + Haben + + + Saldo + + + Buchen + + + Rechnung + + + + + {buchungen.length === 0 ? ( + + + Keine Buchungen vorhanden. Bitte wählen Sie einen Reiter oder ein Pferd aus. + + + ) : ( + <> + {buchungen.map((buchung) => ( + + {buchung.text} + + {buchung.soll.toFixed(2)} € + + + {buchung.haben.toFixed(2)} € + + 0 ? 'error.main' : 'success.main' + }} + > + {buchung.saldo.toFixed(2)} € + + + + + + + + + ))} + {/* Summenzeile */} + + + GESAMT + + + {buchungen.reduce((sum, b) => sum + b.soll, 0).toFixed(2)} € + + + {buchungen.reduce((sum, b) => sum + b.haben, 0).toFixed(2)} € + + 0 ? 'error.main' : 'success.main' + }} + > + {gesamtSaldo.toFixed(2)} € + + + + + )} + +
+
+
+
+ + {/* Rechte Seite: Aktionen (30%) */} + + {/* Rechte Tabs */} + setRechterTab(v)} + sx={{ + borderBottom: 1, + borderColor: 'divider', + bgcolor: 'background.paper', + '& .MuiTab-root': {fontSize: '10px', minHeight: 36, py: 1, minWidth: 60} + }} + > + + + + + + + {/* Aktionsbereich */} + + {/* Teilnehmer-Auswahl */} + + + Nach Reiter oder Pferd + + + + + setTabelleLeeren(e.target.checked)} + /> + } + label={Tabelle leeren} + sx={{mt: 1}} + /> + + + {/* Buchen */} + + + Buchen: + + + + {parseFloat(buchungsBetrag).toFixed(2)} € + + + + + + {/* Direkt Drucken */} + + + Direkt Drucken: + + + + + + + + {/* Zahlungsart */} + + + Zahlungsart: + + setZahlungsart(e.target.value)}> + } + label={BAR} + /> + } + label={Scheck (+30 €)} + /> + } + label={Bankomat} + /> + } + label={Kreditkarte} + /> + + + + + {/* Info Box */} + + + 💡 Hinweis: Bei Barzahlung werden die Buchungen sofort verarbeitet. + Scheck-Zahlungen erfordern eine zusätzliche Gebühr von 30 €. + + + + +
+ ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/ArtikelTab.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/ArtikelTab.tsx new file mode 100644 index 00000000..ff7f42d0 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/ArtikelTab.tsx @@ -0,0 +1,345 @@ +import {useState} from 'react'; +import Box from '@mui/material/Box'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import Paper from '@mui/material/Paper'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import IconButton from '@mui/material/IconButton'; +import Checkbox from '@mui/material/Checkbox'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import DeleteIcon from '@mui/icons-material/Delete'; +import AddIcon from '@mui/icons-material/Add'; +import Divider from '@mui/material/Divider'; + +interface Gebuehr { + id: number; + bezeichnung: string; + betrag: string; + pflicht: boolean; +} + +export function ArtikelTab() { + // Nennungs- und Startgebühren + const [nenngebuehrProPferd, setNenngebuehrProPferd] = useState('0.00'); + const [startgebuehrProBewerb, setStartgebuehrProBewerb] = useState('15.00'); + const [sporteuro, setSporteuro] = useState('0.00'); + const [nachnennungsgebuehr, setNachnennungsgebuehr] = useState('0.00'); + const [nennungstauschgebuehr, setNennungstauschgebuehr] = useState('0.00'); + + // Stallungen & Boxen + const [boxenProTag, setBoxenProTag] = useState('0.00'); + const [einstreuErst, setEinstreuErst] = useState('0.00'); + const [einstreuNach, setEinstreuNach] = useState('0.00'); + const [paddockProTag, setPaddockProTag] = useState('0.00'); + + // Zusatzgebühren (dynamisch) + const [zusatzgebuehren, setZusatzgebuehren] = useState([ + {id: 1, bezeichnung: 'Stromanschluss pro Tag', betrag: '5.00', pflicht: false}, + {id: 2, bezeichnung: 'Camping pro Nacht', betrag: '10.00', pflicht: false}, + ]); + + const handleZusatzgebuehrHinzufuegen = () => { + const newId = Math.max(0, ...zusatzgebuehren.map(g => g.id)) + 1; + setZusatzgebuehren([ + ...zusatzgebuehren, + {id: newId, bezeichnung: '', betrag: '0.00', pflicht: false} + ]); + }; + + const handleZusatzgebuehrLoeschen = (id: number) => { + setZusatzgebuehren(zusatzgebuehren.filter(g => g.id !== id)); + }; + + const handleZusatzgebuehrAendern = (id: number, field: keyof Gebuehr, value: string | boolean) => { + setZusatzgebuehren(zusatzgebuehren.map(g => + g.id === id ? {...g, [field]: value} : g + )); + }; + + const handleSpeichern = () => { + console.log('Artikel speichern:', { + nenngebuehrProPferd, + startgebuehrProBewerb, + sporteuro, + nachnennungsgebuehr, + nennungstauschgebuehr, + boxenProTag, + einstreuErst, + einstreuNach, + paddockProTag, + zusatzgebuehren, + }); + // TODO: Backend Integration + }; + + return ( + + + + Nennungen & Gebühren + + + {/* Nennungs- und Startgebühren */} + + + Nennungs- und Startgebühren + + + + + + Nenngebühr pro Pferd/Reiter: + + setNenngebuehrProPferd(e.target.value)} + sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + (Grundgebühr unabhängig von Anzahl Bewerben) + + + + + + Startgebühr pro Bewerb: + + setStartgebuehrProBewerb(e.target.value)} + sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + (Pro einzelner Prüfung) + + + + + + Sporteuro (Beitrag OEPS): + + setSporteuro(e.target.value)} + sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + + + + + + Nachnennungsgebühr: + + setNachnennungsgebuehr(e.target.value)} + sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + (Nach Nennschluss) + + + + + + Nennungstausch-Gebühr: + + setNennungstauschgebuehr(e.target.value)} + sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + (Pferd- oder Reiter-Wechsel) + + + + + + {/* Stallungen & Boxen */} + + + Stallungen & Boxen + + + + + + Box pro Tag: + + setBoxenProTag(e.target.value)} + sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + + + + Einstreu (Erst-Einstreu): + + setEinstreuErst(e.target.value)} + sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + + + + Einstreu (Nachlegen): + + setEinstreuNach(e.target.value)} + sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + + + + Paddock pro Tag: + + setPaddockProTag(e.target.value)} + sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + + + + {/* Zusatzgebühren */} + + + + Zusatzgebühren + + + + + + + + + Bezeichnung + Betrag + Pflicht + + + + + {zusatzgebuehren.map((gebuehr) => ( + + + handleZusatzgebuehrAendern(gebuehr.id, 'bezeichnung', e.target.value)} + placeholder="z.B. Stromanschluss" + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.5}}} + /> + + + handleZusatzgebuehrAendern(gebuehr.id, 'betrag', e.target.value)} + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.5, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + + handleZusatzgebuehrAendern(gebuehr.id, 'pflicht', e.target.checked)} + /> + } + label={Pflicht} + /> + + + handleZusatzgebuehrLoeschen(gebuehr.id)} + > + + + + + ))} + +
+
+ + {zusatzgebuehren.length === 0 && ( + + + Keine Zusatzgebühren definiert + + + )} +
+ + {/* Hinweis */} + + + ℹ️ Hinweis zur Preisliste + + + Die Gebührenstruktur wird in der offiziellen Ausschreibung veröffentlicht und ist für alle Teilnehmer + verbindlich. Bei nationalen Turnieren der Kategorie C-Neu sind oft reduzierte Gebühren oder + Gebührenbefreiungen + üblich (z.B. kein Nenngeld, kein Sporteuro). + + + + {/* Action Buttons */} + + + + +
+
+ ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/BewerbeTab.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/BewerbeTab.tsx new file mode 100644 index 00000000..469d1918 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/BewerbeTab.tsx @@ -0,0 +1,1751 @@ +import {useState} from 'react'; +import Box from '@mui/material/Box'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import Paper from '@mui/material/Paper'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import Divider from '@mui/material/Divider'; +import Checkbox from '@mui/material/Checkbox'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import FilterListIcon from '@mui/icons-material/FilterList'; +import SaveIcon from '@mui/icons-material/Save'; +import UndoIcon from '@mui/icons-material/Undo'; +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from '@mui/icons-material/Delete'; +import ContentCutIcon from '@mui/icons-material/ContentCut'; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; +import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; +import EditIcon from '@mui/icons-material/Edit'; +import PrintIcon from '@mui/icons-material/Print'; +import FolderOpenIcon from '@mui/icons-material/FolderOpen'; + +interface Bewerb { + id: number; + tag: string; + platz: number; + bewerb: number; + beginn: string; + ende: string; + bewerbname: string; + zns: number; + nennungen: number; + // Detail-Felder + nummer: string; + abteilung: string; + typ: string; + name: string; + bezeichnung: string; + kategorie: string; + klasse: string; + lizenz: string; + maximal: string; + pferdealter: string; + zeile1: string; + zeile2: string; + zeile3: string; + logoBewerbPfad: string; + // Bewertung-Felder + prufung: string; + richtverfahren: string; + paraGrade: string; + richteranzahl: number; + aufgabe: string; + aufgabennr: string; + maximalPunkte: string; + richter: { position: string; name: string; aktiv: boolean }[]; + // Geldpreis-Felder + geldpreisAktiv: boolean; + startgeld: string; + auszahlung: string; + geldpreisKadererreiterAktiv: boolean; + startgeldKadererreiter: string; + geldpreisvorlage: string; + geldpreise: { nummer: string; betrag: string }[]; + // Ort/Zeit-Felder + tagDatum: string; + beginnzeit: string; + beginnZeit: string; + reitdauer: string; + umbau: string; + besichtigung: string; + stechen: string; + platzName: string; +} + +const mockBewerbe: Bewerb[] = [ + { + id: 1, + tag: '28.05.2023', + platz: 1, + bewerb: 1, + beginn: '08:00', + ende: '08:00', + bewerbname: 'Dressurreiterprüfung Reiterpass (Aufgabe R 1)\\nPony Einsteiger Cup OÖ', + zns: 0, + nennungen: 0, + nummer: '1', + abteilung: '', + typ: 'Dressur', + name: 'Dressurreiterprüfung', + bezeichnung: 'Dressurreiterprüfung Reiterpass', + kategorie: '', + klasse: '', + lizenz: '', + maximal: '3', + pferdealter: '', + zeile1: 'Pony Einsteiger Cup OÖ', + zeile2: '', + zeile3: '', + logoBewerbPfad: '', + prufung: 'Dressurreiterprüfung', + richtverfahren: 'A', + paraGrade: '', + richteranzahl: 2, + aufgabe: 'Aufgabe R', + aufgabennr: '', + maximalPunkte: '', + richter: [ + {position: 'C', name: 'Schuster Alexandra', aktiv: true}, + {position: 'C', name: 'Vankova Kamila (CZ)', aktiv: true} + ], + geldpreisAktiv: false, + startgeld: '15,00', + auszahlung: 'fortführend', + geldpreisKadererreiterAktiv: false, + startgeldKadererreiter: '15,00', + geldpreisvorlage: '', + geldpreise: [], + tagDatum: '28.05.2023', + beginnzeit: 'fix um', + beginnZeit: '08:00', + reitdauer: '02:00', + umbau: '10', + besichtigung: '10', + stechen: '', + platzName: 'Vorderer Turnierplatz' + }, + { + id: 2, + tag: '28.05.2023', + platz: 1, + bewerb: 2, + beginn: '08:20', + ende: '08:20', + bewerbname: 'Dressurreiterprüfung Reitenadel (Aufgabe R 4)\nPony Einsteiger Cup OÖ', + zns: 0, + nennungen: 0, + nummer: '2', + abteilung: '', + typ: 'Dressur', + name: 'Dressurreiterprüfung', + bezeichnung: 'Dressurreiterprüfung Reitenadel', + kategorie: '', + klasse: '', + lizenz: '', + maximal: '3', + pferdealter: '', + zeile1: 'Pony Einsteiger Cup OÖ', + zeile2: '', + zeile3: '', + logoBewerbPfad: '', + prufung: 'Dressurreiterprüfung', + richtverfahren: 'Richtverfahren', + paraGrade: 'Para-Grade', + richteranzahl: 3, + aufgabe: 'Aufgabe R 4', + aufgabennr: '4', + maximalPunkte: '100', + richter: [ + {position: 'Richter 1', name: 'Max Mustermann', aktiv: true}, + {position: 'Richter 2', name: 'Anna Musterfrau', aktiv: true}, + {position: 'Richter 3', name: 'Peter Muster', aktiv: true} + ], + geldpreisAktiv: false, + startgeld: '', + auszahlung: '', + geldpreisKadererreiterAktiv: false, + startgeldKadererreiter: '', + geldpreisvorlage: '', + geldpreise: [], + tagDatum: '', + beginnzeit: '', + beginnZeit: '', + reitdauer: '', + umbau: '', + besichtigung: '', + stechen: '', + platzName: '' + }, + { + id: 3, + tag: '28.05.2023', + platz: 1, + bewerb: 3, + beginn: '08:40', + ende: '08:40', + bewerbname: 'Dressurreiterprüfung lsf. (Istzfrei) (Aufgabe LF 1)', + zns: 0, + nennungen: 0, + nummer: '3', + abteilung: '', + typ: 'Dressur', + name: 'Dressurreiterprüfung', + bezeichnung: 'Dressurreiterprüfung lsf. (Istzfrei)', + kategorie: '', + klasse: '', + lizenz: '', + maximal: '3', + pferdealter: '', + zeile1: '', + zeile2: '', + zeile3: '', + logoBewerbPfad: '', + prufung: 'Dressurreiterprüfung', + richtverfahren: 'Richtverfahren', + paraGrade: 'Para-Grade', + richteranzahl: 3, + aufgabe: 'Aufgabe LF 1', + aufgabennr: '1', + maximalPunkte: '100', + richter: [ + {position: 'Richter 1', name: 'Max Mustermann', aktiv: true}, + {position: 'Richter 2', name: 'Anna Musterfrau', aktiv: true}, + {position: 'Richter 3', name: 'Peter Muster', aktiv: true} + ], + geldpreisAktiv: false, + startgeld: '', + auszahlung: '', + geldpreisKadererreiterAktiv: false, + startgeldKadererreiter: '', + geldpreisvorlage: '', + geldpreise: [], + tagDatum: '', + beginnzeit: '', + beginnZeit: '', + reitdauer: '', + umbau: '', + besichtigung: '', + stechen: '', + platzName: '' + }, + { + id: 4, + tag: '28.05.2023', + platz: 1, + bewerb: 4, + beginn: '09:00', + ende: '09:00', + bewerbname: 'Dressurreiterprüfung lsf. (Lizenfrei) (Aufgabe LF 3)', + zns: 0, + nennungen: 0, + nummer: '4', + abteilung: '', + typ: 'Dressur', + name: 'Dressurreiterprüfung', + bezeichnung: 'Dressurreiterprüfung lsf. (Lizenfrei)', + kategorie: '', + klasse: '', + lizenz: '', + maximal: '3', + pferdealter: '', + zeile1: '', + zeile2: '', + zeile3: '', + logoBewerbPfad: '', + prufung: 'Dressurreiterprüfung', + richtverfahren: 'Richtverfahren', + paraGrade: 'Para-Grade', + richteranzahl: 3, + aufgabe: 'Aufgabe LF 3', + aufgabennr: '3', + maximalPunkte: '100', + richter: [ + {position: 'Richter 1', name: 'Max Mustermann', aktiv: true}, + {position: 'Richter 2', name: 'Anna Musterfrau', aktiv: true}, + {position: 'Richter 3', name: 'Peter Muster', aktiv: true} + ], + geldpreisAktiv: false, + startgeld: '', + auszahlung: '', + geldpreisKadererreiterAktiv: false, + startgeldKadererreiter: '', + geldpreisvorlage: '', + geldpreise: [], + tagDatum: '', + beginnzeit: '', + beginnZeit: '', + reitdauer: '', + umbau: '', + besichtigung: '', + stechen: '', + platzName: '' + }, + { + id: 5, + tag: '28.05.2023', + platz: 1, + bewerb: 5, + beginn: '09:20', + ende: '09:20', + bewerbname: 'Führzügelklasse\nOÖ Kids Cup', + zns: 0, + nennungen: 0, + nummer: '5', + abteilung: '', + typ: 'Dressur', + name: 'Führzügelklasse', + bezeichnung: 'Führzügelklasse', + kategorie: '', + klasse: '', + lizenz: '', + maximal: '3', + pferdealter: '', + zeile1: 'OÖ Kids Cup', + zeile2: '', + zeile3: '', + logoBewerbPfad: '', + prufung: 'Führzügelklasse', + richtverfahren: 'Richtverfahren', + paraGrade: 'Para-Grade', + richteranzahl: 3, + aufgabe: 'Aufgabe FZ 1', + aufgabennr: '1', + maximalPunkte: '100', + richter: [ + {position: 'Richter 1', name: 'Max Mustermann', aktiv: true}, + {position: 'Richter 2', name: 'Anna Musterfrau', aktiv: true}, + {position: 'Richter 3', name: 'Peter Muster', aktiv: true} + ], + geldpreisAktiv: false, + startgeld: '', + auszahlung: '', + geldpreisKadererreiterAktiv: false, + startgeldKadererreiter: '', + geldpreisvorlage: '', + geldpreise: [], + tagDatum: '', + beginnzeit: '', + beginnZeit: '', + reitdauer: '', + umbau: '', + besichtigung: '', + stechen: '', + platzName: '' + }, + { + id: 6, + tag: '28.05.2023', + platz: 1, + bewerb: 6, + beginn: '09:40', + ende: '09:40', + bewerbname: 'First Ridden\nOÖ Kids Cup', + zns: 0, + nennungen: 0, + nummer: '6', + abteilung: '', + typ: 'Dressur', + name: 'First Ridden', + bezeichnung: 'First Ridden', + kategorie: '', + klasse: '', + lizenz: '', + maximal: '3', + pferdealter: '', + zeile1: 'OÖ Kids Cup', + zeile2: '', + zeile3: '', + logoBewerbPfad: '', + prufung: 'First Ridden', + richtverfahren: 'Richtverfahren', + paraGrade: 'Para-Grade', + richteranzahl: 3, + aufgabe: 'Aufgabe FR 1', + aufgabennr: '1', + maximalPunkte: '100', + richter: [ + {position: 'Richter 1', name: 'Max Mustermann', aktiv: true}, + {position: 'Richter 2', name: 'Anna Musterfrau', aktiv: true}, + {position: 'Richter 3', name: 'Peter Muster', aktiv: true} + ], + geldpreisAktiv: false, + startgeld: '', + auszahlung: '', + geldpreisKadererreiterAktiv: false, + startgeldKadererreiter: '', + geldpreisvorlage: '', + geldpreise: [], + tagDatum: '', + beginnzeit: '', + beginnZeit: '', + reitdauer: '', + umbau: '', + besichtigung: '', + stechen: '', + platzName: '' + }, + { + id: 7, + tag: '28.05.2023', + platz: 1, + bewerb: 7, + beginn: '10:00', + ende: '10:00', + bewerbname: 'Pony Dressurprüfung Kl. A (Aufgabe P 1)', + zns: 0, + nennungen: 0, + nummer: '7', + abteilung: '', + typ: 'Dressur', + name: 'Pony Dressurprüfung', + bezeichnung: 'Pony Dressurprüfung Kl. A', + kategorie: '', + klasse: '', + lizenz: '', + maximal: '3', + pferdealter: '', + zeile1: '', + zeile2: '', + zeile3: '', + logoBewerbPfad: '', + prufung: 'Pony Dressurprüfung', + richtverfahren: 'Richtverfahren', + paraGrade: 'Para-Grade', + richteranzahl: 3, + aufgabe: 'Aufgabe P 1', + aufgabennr: '1', + maximalPunkte: '100', + richter: [ + {position: 'Richter 1', name: 'Max Mustermann', aktiv: true}, + {position: 'Richter 2', name: 'Anna Musterfrau', aktiv: true}, + {position: 'Richter 3', name: 'Peter Muster', aktiv: true} + ], + geldpreisAktiv: false, + startgeld: '', + auszahlung: '', + geldpreisKadererreiterAktiv: false, + startgeldKadererreiter: '', + geldpreisvorlage: '', + geldpreise: [], + tagDatum: '', + beginnzeit: '', + beginnZeit: '', + reitdauer: '', + umbau: '', + besichtigung: '', + stechen: '', + platzName: '' + }, + { + id: 8, + tag: '28.05.2023', + platz: 1, + bewerb: 8, + beginn: '10:20', + ende: '10:20', + bewerbname: 'Dressurreiterprüfung Kl. A (Aufgabe DRA 1)', + zns: 0, + nennungen: 0, + nummer: '8', + abteilung: '', + typ: 'Dressur', + name: 'Dressurreiterprüfung', + bezeichnung: 'Dressurreiterprüfung Kl. A', + kategorie: '', + klasse: '', + lizenz: '', + maximal: '3', + pferdealter: '', + zeile1: '', + zeile2: '', + zeile3: '', + logoBewerbPfad: '', + prufung: 'Dressurreiterprüfung', + richtverfahren: 'Richtverfahren', + paraGrade: 'Para-Grade', + richteranzahl: 3, + aufgabe: 'Aufgabe DRA 1', + aufgabennr: '1', + maximalPunkte: '100', + richter: [ + {position: 'Richter 1', name: 'Max Mustermann', aktiv: true}, + {position: 'Richter 2', name: 'Anna Musterfrau', aktiv: true}, + {position: 'Richter 3', name: 'Peter Muster', aktiv: true} + ], + geldpreisAktiv: false, + startgeld: '', + auszahlung: '', + geldpreisKadererreiterAktiv: false, + startgeldKadererreiter: '', + geldpreisvorlage: '', + geldpreise: [], + tagDatum: '', + beginnzeit: '', + beginnZeit: '', + reitdauer: '', + umbau: '', + besichtigung: '', + stechen: '', + platzName: '' + }, + { + id: 9, + tag: '28.05.2023', + platz: 1, + bewerb: 9, + beginn: '10:40', + ende: '10:40', + bewerbname: 'Dressurreiterprüfung Kl. A (Aufgabe A 5)', + zns: 0, + nennungen: 0, + nummer: '9', + abteilung: '', + typ: 'Dressur', + name: 'Dressurreiterprüfung', + bezeichnung: 'Dressurreiterprüfung Kl. A', + kategorie: '', + klasse: '', + lizenz: '', + maximal: '3', + pferdealter: '', + zeile1: 'TS Erfolgreichstes Pony OÖ', + zeile2: '', + zeile3: '', + logoBewerbPfad: '', + prufung: 'Dressurreiterprüfung', + richtverfahren: 'Richtverfahren', + paraGrade: 'Para-Grade', + richteranzahl: 3, + aufgabe: 'Aufgabe A 5', + aufgabennr: '5', + maximalPunkte: '100', + richter: [ + {position: 'Richter 1', name: 'Max Mustermann', aktiv: true}, + {position: 'Richter 2', name: 'Anna Musterfrau', aktiv: true}, + {position: 'Richter 3', name: 'Peter Muster', aktiv: true} + ], + geldpreisAktiv: false, + startgeld: '', + auszahlung: '', + geldpreisKadererreiterAktiv: false, + startgeldKadererreiter: '', + geldpreisvorlage: '', + geldpreise: [], + tagDatum: '', + beginnzeit: '', + beginnZeit: '', + reitdauer: '', + umbau: '', + besichtigung: '', + stechen: '', + platzName: '' + }, + { + id: 10, + tag: '28.05.2023', + platz: 1, + bewerb: 10, + beginn: '11:00', + ende: '11:00', + bewerbname: 'Pony Dressurprüfung Kl. A (Aufgabe P 9)', + zns: 0, + nennungen: 0, + nummer: '10', + abteilung: '', + typ: 'Dressur', + name: 'Pony Dressurprüfung', + bezeichnung: 'Pony Dressurprüfung Kl. A', + kategorie: '', + klasse: '', + lizenz: '', + maximal: '3', + pferdealter: '', + zeile1: '', + zeile2: '', + zeile3: '', + logoBewerbPfad: '', + prufung: 'Pony Dressurprüfung', + richtverfahren: 'Richtverfahren', + paraGrade: 'Para-Grade', + richteranzahl: 3, + aufgabe: 'Aufgabe P 9', + aufgabennr: '9', + maximalPunkte: '100', + richter: [ + {position: 'Richter 1', name: 'Max Mustermann', aktiv: true}, + {position: 'Richter 2', name: 'Anna Musterfrau', aktiv: true}, + {position: 'Richter 3', name: 'Peter Muster', aktiv: true} + ], + geldpreisAktiv: false, + startgeld: '', + auszahlung: '', + geldpreisKadererreiterAktiv: false, + startgeldKadererreiter: '', + geldpreisvorlage: '', + geldpreise: [], + tagDatum: '', + beginnzeit: '', + beginnZeit: '', + reitdauer: '', + umbau: '', + besichtigung: '', + stechen: '', + platzName: '' + }, + { + id: 11, + tag: '28.05.2023', + platz: 1, + bewerb: 11, + beginn: '11:20', + ende: '11:20', + bewerbname: 'Dressurreiterprüfung Kl. L (Aufgabe DRL 1)', + zns: 0, + nennungen: 0, + nummer: '11', + abteilung: '', + typ: 'Dressur', + name: 'Dressurreiterprüfung', + bezeichnung: 'Dressurreiterprüfung Kl. L', + kategorie: '', + klasse: '', + lizenz: '', + maximal: '3', + pferdealter: '', + zeile1: '', + zeile2: '', + zeile3: '', + logoBewerbPfad: '', + prufung: 'Dressurreiterprüfung', + richtverfahren: 'Richtverfahren', + paraGrade: 'Para-Grade', + richteranzahl: 3, + aufgabe: 'Aufgabe DRL 1', + aufgabennr: '1', + maximalPunkte: '100', + richter: [ + {position: 'Richter 1', name: 'Max Mustermann', aktiv: true}, + {position: 'Richter 2', name: 'Anna Musterfrau', aktiv: true}, + {position: 'Richter 3', name: 'Peter Muster', aktiv: true} + ], + geldpreisAktiv: false, + startgeld: '', + auszahlung: '', + geldpreisKadererreiterAktiv: false, + startgeldKadererreiter: '', + geldpreisvorlage: '', + geldpreise: [], + tagDatum: '', + beginnzeit: '', + beginnZeit: '', + reitdauer: '', + umbau: '', + besichtigung: '', + stechen: '', + platzName: '' + }, + { + id: 12, + tag: '28.05.2023', + platz: 1, + bewerb: 12, + beginn: '11:40', + ende: '11:40', + bewerbname: 'Dressurprüfung Kl. L (Aufgabe L 3)', + zns: 0, + nennungen: 0, + nummer: '12', + abteilung: '', + typ: 'Dressur', + name: 'Dressurprüfung', + bezeichnung: 'Dressurprüfung Kl. L', + kategorie: '', + klasse: '', + lizenz: '', + maximal: '3', + pferdealter: '', + zeile1: '', + zeile2: '', + zeile3: '', + logoBewerbPfad: '', + prufung: 'Dressurprüfung', + richtverfahren: 'Richtverfahren', + paraGrade: 'Para-Grade', + richteranzahl: 3, + aufgabe: 'Aufgabe L 3', + aufgabennr: '3', + maximalPunkte: '100', + richter: [ + {position: 'Richter 1', name: 'Max Mustermann', aktiv: true}, + {position: 'Richter 2', name: 'Anna Musterfrau', aktiv: true}, + {position: 'Richter 3', name: 'Peter Muster', aktiv: true} + ], + geldpreisAktiv: false, + startgeld: '', + auszahlung: '', + geldpreisKadererreiterAktiv: false, + startgeldKadererreiter: '', + geldpreisvorlage: '', + geldpreise: [], + tagDatum: '', + beginnzeit: '', + beginnZeit: '', + reitdauer: '', + umbau: '', + besichtigung: '', + stechen: '', + platzName: '' + } +]; + +export function BewerbeTab() { + const [bewerbe] = useState(mockBewerbe); + const [selectedBewerbId, setSelectedBewerbId] = useState(1); + const [detailTab, setDetailTab] = useState(0); + + const selectedBewerb = bewerbe.find(b => b.id === selectedBewerbId); + + const handleSpeichern = () => { + console.log('Änderungen speichern'); + }; + + return ( + + {/* Linke Sidebar - Aktionen */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* Mitte - Bewerbs-Übersicht Tabelle (50%) */} + + {/* Toolbar */} + + + + + + + {/* Tabelle */} + + + + + Tag + Platz + Bewerb + Beginn + Ende + Bewerbname + ZNS + Nennungen + + + + {bewerbe.map((bewerb) => ( + setSelectedBewerbId(bewerb.id)} + sx={{ + cursor: 'pointer', + bgcolor: bewerb.bewerb === 5 || bewerb.bewerb === 6 ? 'warning.50' : 'inherit', + '&.Mui-selected': { + bgcolor: bewerb.bewerb === 5 || bewerb.bewerb === 6 ? 'warning.100' : 'action.selected' + } + }} + > + {bewerb.tag} + {bewerb.platz} + {bewerb.bewerb} + {bewerb.beginn} + {bewerb.ende} + {bewerb.bewerbname} + {bewerb.zns} + {bewerb.nennungen} + + ))} + +
+
+
+ + {/* Rechts - Bewerb-Konfiguration (50%) */} + {selectedBewerb && ( + + {/* Detail-Tabs */} + setDetailTab(v)} + sx={{ + borderBottom: 1, + borderColor: 'divider', + bgcolor: 'background.paper', + '& .MuiTab-root': { + fontSize: '11px', + minHeight: 36, + py: 1, + textTransform: 'none' + } + }} + > + + + + + + + {/* Tab Content */} + + {/* TAB 0: Bewerb */} + {detailTab === 0 && ( + + {/* Nummer */} + + + Nummer: + + + + + {/* Abteilung */} + + + Abteilung: + + + + + {/* Typ */} + + + Typ: + + + + + {/* Name */} + + + Name: + + + + + {/* Bezeichnung */} + + + Bezeichnung: + + + + + {/* Kategorie */} + + + Kategorie: + + + + + {/* Klasse */} + + + Klasse: + + + + + {/* Lizenz */} + + + Lizenz: + + + + + {/* Maximal */} + + + Maximal: + + + + Pferde je Reiter + + + + {/* Pferdealter */} + + + Pferdealter: + + + + + {/* Zeile 1 */} + + + Zeile 1: + + + + + {/* Zeile 2 */} + + + Zeile 2: + + + + + {/* Zeile 3 */} + + + Zeile 3: + + + + + {/* Logo Bewerb */} + + + Logo Bewerb: + + + + + + )} + + {/* TAB 1: Bewertung */} + {detailTab === 1 && ( + + + Bewertungs-Konfiguration + + + {/* Prüfung */} + + + Prüfung: + + + + + {/* Richtverfahren */} + + + Richtverfahren: + + + + + {/* Para-Grade */} + + + Para-Grade: + + + + + {/* Richteranzahl */} + + + Richteranzahl: + + + + + {/* Aufgabe */} + + + Aufgabe: + + + + + {/* Aufgabennummer */} + + + Aufgabennummer: + + + + + {/* Maximalpunkte */} + + + Maximalpunkte: + + + + + {/* Richter */} + + + Richter + + {selectedBewerb.richter.map((richter, index) => ( + + + {richter.position}: + + + + + ))} + + + + )} + + {/* TAB 2: Geldpreise */} + {detailTab === 2 && ( + + {/* Geldpreis Section */} + + + Geldpreis + + + + {/* Geldpreis Checkbox */} + + + + Geldpreis + + + + {/* Startgeld */} + + + Startgeld: + + + + + {/* Auszahlung */} + + + Auszahlung: + + + + + + + {/* Geldpreis für Kadererreiter Section */} + + + Geldpreis für Kadererreiter + + + + {/* Geldpreis für Kadererreiter Checkbox */} + + + + Geldpreis für Kadererreiter + + + + {/* Startgeld für Kadererreiter */} + + + Startgeld für Kadererreiter: + + + + + + + {/* Geldpreisvorlage */} + + + Geldpreisvorlage wählen: + + + + + {/* Geldpreise Tabelle */} + + + + {selectedBewerb.geldpreise.length} Geldpreise + + + + + + + Nummer + Geldpreis + + + + {selectedBewerb.geldpreise.length === 0 && ( + + + + + )} + +
+
+
+
+ )} + + {/* TAB 3: Ort/Zeit */} + {detailTab === 3 && ( + + {/* Tag */} + + + Tag: + + + + + {/* Beginnzeit */} + + + Beginnzeit: + + + + + {/* Zeit */} + + + + + + (hh:mm) + + + + {/* Reitdauer */} + + + Reitdauer: + + + + (mm:ss) + + + + {/* Umbau */} + + + Umbau: + + + + (mm) + + + + {/* Besichtigung */} + + + Besichtigung: + + + + (mm) + + + + {/* Stechen */} + + + Stechen: + + + + (mm) + + + + {/* Platz */} + + + Platz: + + + + + )} +
+
+ )} +
+ ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/ErgebnislistenTab.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/ErgebnislistenTab.tsx new file mode 100644 index 00000000..aad40d3b --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/ErgebnislistenTab.tsx @@ -0,0 +1,569 @@ +import {useState} from 'react'; +import Box from '@mui/material/Box'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import Paper from '@mui/material/Paper'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import IconButton from '@mui/material/IconButton'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import SearchIcon from '@mui/icons-material/Search'; +import AddIcon from '@mui/icons-material/Add'; +import RemoveIcon from '@mui/icons-material/Remove'; + +interface Ergebnis { + ergebnis: number; + nr: string; + kopfnr: string; + pferd: string; + reiter: string; + k: string; + q: string; + platziert: string; + wertung: string; + poep: string; + zgp: string; + gesamtnote: string; + geldpreis: string; + plE: string; + plH: string; + plC: string; + plM: string; +} + +interface StarterDetail { + pos: number; + nr: string; + kopfnr: string; + pferd: string; + reiter: string; + k: string; + q: string; + bemerkung: string; +} + +const mockErgebnisse: Ergebnis[] = []; +const mockStarter: StarterDetail[] = []; + +export function ErgebnislistenTab() { + const [selectedBewerb, setSelectedBewerb] = useState(1); + const [ergebnisse] = useState(mockErgebnisse); + const [starter] = useState(mockStarter); + const [parcours, setParcours] = useState('grundparcours'); + const [fehler, setFehler] = useState(''); + const [zeit, setZeit] = useState(''); + const [anzahlPlatzierte, setAnzahlPlatzierte] = useState(0); + const [geldpreis, setGeldpreis] = useState(''); + const [kadererreiterExtra, setKadererreiterExtra] = useState(false); + const [anzahlKadererreiter, setAnzahlKadererreiter] = useState(''); + + // Bewerbs-Nummern (1-12) + const bewerbe = Array.from({length: 12}, (_, i) => i + 1); + + return ( + + {/* Bewerbs-Auswahl Grid */} + + + {bewerbe.map((nr) => ( + + ))} + + + + {/* Main Content */} + + {/* Linker Bereich */} + + {/* Obere Ergebnisliste */} + + {/* Toolbar */} + + + + + + {ergebnisse.length} gefunden + + + + + {/* Tabelle */} + + + + + Ergebnis + Nr. + KopfNr + Pferd + Reiter + K + Q + Platziert + Wertung + Pöp + ZGp + Gesamtnote + Geldpreis + Pl.E + Pl.H + Pl.C + Pl.M + + + + {ergebnisse.length === 0 && ( + + + Keine Ergebnisse vorhanden + + + )} + +
+
+
+ + {/* Mittlere Section: Parcours-Auswahl */} + + setParcours(e.target.value)} + sx={{flex: 1}} + > + } + label={Grundparcours} + /> + } + label={Stechen 1} + /> + } + label={Stechen 2} + /> + } + label={Stechen 3} + /> + + + + + Fehler + + setFehler(e.target.value)} + sx={{ + width: 80, + bgcolor: 'white', + '& .MuiInputBase-input': {fontSize: '10px', py: 0.5} + }} + /> + + + + + Zeit + + setZeit(e.target.value)} + sx={{ + width: 80, + bgcolor: 'white', + '& .MuiInputBase-input': {fontSize: '10px', py: 0.5} + }} + /> + + + + + + + + + + {/* Untere Starter-Detail Tabelle */} + + {/* Toolbar */} + + + + + + + + {/* Tabelle */} + + + + + Pos. + Nr. + KopfNr + Pferd + Reiter + K + Q + Bemerkung + + + + {starter.length === 0 && ( + + + Keine Starter vorhanden + + + )} + +
+
+
+
+ + {/* Rechte Sidebar */} + + {/* Platzierung & Geldpreis */} + + + Platzierung & Geldpreis: + + + + + + Anzahl Platzierte: + + + setAnzahlPlatzierte(Math.max(0, anzahlPlatzierte - 1))} + sx={{width: 24, height: 24, border: 1, borderColor: 'divider'}} + > + + + + {anzahlPlatzierte} + + setAnzahlPlatzierte(anzahlPlatzierte + 1)} + sx={{width: 24, height: 24, border: 1, borderColor: 'divider'}} + > + + + + + + + + Geldpreis: + + setGeldpreis(e.target.value)} + sx={{ + flex: 1, + bgcolor: 'white', + '& .MuiInputBase-input': {fontSize: '10px', py: 0.5} + }} + /> + + + setKadererreiterExtra(e.target.checked)} + /> + } + label={Kadererreiter Extra:} + /> + + + + Anzahl platzierte Kadererreiter: + + setAnzahlKadererreiter(e.target.value)} + sx={{ + width: 60, + bgcolor: 'white', + '& .MuiInputBase-input': {fontSize: '10px', py: 0.5} + }} + /> + + + + + Geldpreis für Kadererreiter: + + + --- + + + + + + Summe Geldpreise: + + + --- + + + + + + {/* Bewerb */} + + + Bewerb: + + + + + + + + + {/* Ergebnisliste */} + + + Ergebnisliste: + + + + + + + + + + + + + +
+
+ ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/FunktionaereTab.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/FunktionaereTab.tsx new file mode 100644 index 00000000..08626363 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/FunktionaereTab.tsx @@ -0,0 +1,398 @@ +import {useState} from 'react'; +import Box from '@mui/material/Box'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import Paper from '@mui/material/Paper'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import IconButton from '@mui/material/IconButton'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; +import DeleteIcon from '@mui/icons-material/Delete'; +import AddIcon from '@mui/icons-material/Add'; +import SearchIcon from '@mui/icons-material/Search'; + +interface Richter { + id: number; + name: string; + qualifikation: string; + funktion: string; +} + +// Mock-Qualifikationen basierend auf OEPS-System +const qualifikationen = [ + 'D-E', 'D-A', 'D-L', 'D-M', 'D-S', 'D-GP', // Dressur + 'S-E', 'S-A', 'S-L', 'S-M', 'S-S', // Springen + 'V-E', 'V-A', 'V-L', 'V-M', 'V-S', // Vielseitigkeit + 'FEI Level 1', 'FEI Level 2', 'FEI Level 3' // International +]; + +const richterfunktionen = [ + 'Hauptrichter', + 'Beisitzer', + 'Richter bei C', + 'Richter bei H', + 'Richter bei M', + 'Richter bei B', + 'Richter bei E' +]; + +export function FunktionaereTab() { + // Einzelne Funktionäre + const [turnierleiter, setTurnierleiter] = useState(''); + const [turnierbeauftragter, setTurnierbeauftragter] = useState(''); + const [technischerDelegierter, setTechnischerDelegierter] = useState(''); + const [parcourschef, setParcourschef] = useState(''); + const [tierarzt, setTierarzt] = useState(''); + const [schmied, setSchmied] = useState(''); + const [steward, setSteward] = useState(''); + + // Richterkollegium (dynamische Liste) + const [richter, setRichter] = useState([ + {id: 1, name: 'Alexandra Schuster', qualifikation: 'D-GP', funktion: 'Hauptrichter'}, + {id: 2, name: 'Ulrike Knasmüller-Prinz', qualifikation: 'D-M', funktion: 'Beisitzer'}, + ]); + + const handleRichterHinzufuegen = () => { + const newId = Math.max(0, ...richter.map(r => r.id)) + 1; + setRichter([ + ...richter, + {id: newId, name: '', qualifikation: 'D-E', funktion: 'Beisitzer'} + ]); + }; + + const handleRichterLoeschen = (id: number) => { + setRichter(richter.filter(r => r.id !== id)); + }; + + const handleRichterAendern = (id: number, field: keyof Richter, value: string) => { + setRichter(richter.map(r => + r.id === id ? {...r, [field]: value} : r + )); + }; + + const handleSpeichern = () => { + console.log('Funktionäre speichern:', { + turnierleiter, + turnierbeauftragter, + technischerDelegierter, + parcourschef, + tierarzt, + schmied, + steward, + richter, + }); + // TODO: Backend Integration (C-Satz) + }; + + return ( + + + + Funktionäre & Offizielle (C-Satz) + + + {/* Turnier-Organisation */} + + + Turnier-Organisation + + + + + + Turnierleiter: + + setTurnierleiter(e.target.value)} + placeholder="z.B. Ursula Stroblmair" + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}} + InputProps={{ + endAdornment: ( + + + + ) + }} + /> + + + + + Turnierbeauftragte/r: + + setTurnierbeauftragter(e.target.value)} + placeholder="z.B. Rudi Kreupl" + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}} + InputProps={{ + endAdornment: ( + + + + ) + }} + /> + + + + + Technischer Delegierter (TD): + + setTechnischerDelegierter(e.target.value)} + placeholder="Optional (hauptsächlich Vielseitigkeit)" + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}} + InputProps={{ + endAdornment: ( + + + + ) + }} + /> + + + + + Steward: + + setSteward(e.target.value)} + placeholder="z.B. Barbara Hruschka" + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}} + InputProps={{ + endAdornment: ( + + + + ) + }} + /> + + + + + {/* Parcours & Technik */} + + + Parcours & Technik + + + + + + Parcourschef: + + setParcourschef(e.target.value)} + placeholder="z.B. Kurt Reitetschläger" + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}} + InputProps={{ + endAdornment: ( + + + + ) + }} + /> + + + + + {/* Medizinische Versorgung */} + + + Medizinische Versorgung + + + + + + Turniertierarzt: + + setTierarzt(e.target.value)} + placeholder="z.B. Dr. Sabine Ötschmaier" + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}} + InputProps={{ + endAdornment: ( + + + + ) + }} + /> + + + + + Schmied: + + setSchmied(e.target.value)} + placeholder="Name des Turnierschmieds" + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}} + InputProps={{ + endAdornment: ( + + + + ) + }} + /> + + + + + {/* Richterkollegium */} + + + + Richterkollegium + + + + + + + + + Name + Qualifikation + Funktion + + + + + {richter.map((r) => ( + + + handleRichterAendern(r.id, 'name', e.target.value)} + placeholder="Name des Richters" + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.5}}} + InputProps={{ + endAdornment: ( + + + + ) + }} + /> + + + + + + + + + handleRichterLoeschen(r.id)} + > + + + + + ))} + +
+
+ + {richter.length === 0 && ( + + + Keine Richter definiert + + + )} +
+ + {/* Hinweis */} + + + ℹ️ Hinweis zu Funktionären + + + Die Funktionäre werden im C-Satz der ZNS-Schnittstelle übermittelt. + Richter müssen entsprechende Qualifikationen für die jeweiligen Klassen besitzen (z.B. D-GP für Grand Prix + Dressur). + Bei internationalen Turnieren sind FEI-Lizenzen erforderlich. + + + + {/* Action Buttons */} + + + + +
+
+ ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/NennungenTab.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/NennungenTab.tsx new file mode 100644 index 00000000..f65ee574 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/NennungenTab.tsx @@ -0,0 +1,5 @@ +import {NennungsMaske} from '../NennungsMaske'; + +export function NennungenTab() { + return ; +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/OrganisationTab.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/OrganisationTab.tsx new file mode 100644 index 00000000..7158b8e0 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/OrganisationTab.tsx @@ -0,0 +1,411 @@ +import {useState} from 'react'; +import Box from '@mui/material/Box'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import Paper from '@mui/material/Paper'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import IconButton from '@mui/material/IconButton'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; +import DeleteIcon from '@mui/icons-material/Delete'; +import AddIcon from '@mui/icons-material/Add'; +import Divider from '@mui/material/Divider'; + +interface Richter { + id: number; + name: string; + qualifikation: string; + funktion: string; +} + +interface Platz { + id: number; + sparte: string; + groesse: string; + bezeichnung: string; +} + +// Mock-Qualifikationen basierend auf OEPS-System +const qualifikationen = [ + 'D-E', 'D-A', 'D-L', 'D-M', 'D-S', 'D-GP', // Dressur + 'S-E', 'S-A', 'S-L', 'S-M', 'S-S', // Springen + 'V-E', 'V-A', 'V-L', 'V-M', 'V-S', // Vielseitigkeit + 'FEI Level 1', 'FEI Level 2', 'FEI Level 3' // International +]; + +const richterfunktionen = [ + 'Hauptrichter', + 'Beisitzer', + 'Richter bei C', + 'Richter bei H', + 'Richter bei M', + 'Richter bei B', + 'Richter bei E' +]; + +const sparten = ['Dressur', 'Springen', 'Vielseitigkeit']; + +const platzgroessen = [ + '20 x 40 m', + '20 x 60 m', + '25 x 60 m', + '30 x 60 m', + 'Springplatz' +]; + +export function OrganisationTab() { + // Einzelne Funktionäre + const [turnierleiter, setTurnierleiter] = useState(''); + const [turnierbeauftragter, setTurnierbeauftragter] = useState(''); + const [technischerDelegierter, setTechnischerDelegierter] = useState(''); + const [parcourschef, setParcourschef] = useState(''); + const [tierarzt, setTierarzt] = useState(''); + const [schmied, setSchmied] = useState(''); + const [steward, setSteward] = useState(''); + + // Richterkollegium (dynamische Liste) + const [richter, setRichter] = useState([ + {id: 1, name: 'Alexandra Schuster', qualifikation: 'D-GP', funktion: 'Hauptrichter'}, + {id: 2, name: 'Ulrike Knasmüller-Prinz', qualifikation: 'D-M', funktion: 'Beisitzer'}, + ]); + + // Plätze (dynamische Liste) + const [plaetze, setPlaetze] = useState([ + {id: 1, sparte: 'Dressur', groesse: '20 x 60 m', bezeichnung: 'Hauptplatz'}, + {id: 2, sparte: 'Dressur', groesse: '20 x 40 m', bezeichnung: 'Abreiteplatz 1'}, + ]); + + const handleRichterHinzufuegen = () => { + const newId = Math.max(0, ...richter.map(r => r.id)) + 1; + setRichter([ + ...richter, + {id: newId, name: '', qualifikation: 'D-E', funktion: 'Beisitzer'} + ]); + }; + + const handleRichterLoeschen = (id: number) => { + setRichter(richter.filter(r => r.id !== id)); + }; + + const handleRichterAendern = (id: number, field: keyof Richter, value: string) => { + setRichter(richter.map(r => + r.id === id ? {...r, [field]: value} : r + )); + }; + + const handlePlatzHinzufuegen = () => { + const newId = Math.max(0, ...plaetze.map(p => p.id)) + 1; + setPlaetze([ + ...plaetze, + {id: newId, sparte: 'Dressur', groesse: '20 x 60 m', bezeichnung: ''} + ]); + }; + + const handlePlatzLoeschen = (id: number) => { + setPlaetze(plaetze.filter(p => p.id !== id)); + }; + + const handlePlatzAendern = (id: number, field: keyof Platz, value: string) => { + setPlaetze(plaetze.map(p => + p.id === id ? {...p, [field]: value} : p + )); + }; + + const handleSpeichern = () => { + console.log('Organisation speichern:', { + turnierleiter, + turnierbeauftragter, + technischerDelegierter, + parcourschef, + tierarzt, + schmied, + steward, + richter, + plaetze, + }); + // TODO: Backend Integration (C-Satz) + }; + + return ( + + + {/* === FUNKTIONÄRE === */} + + Funktionäre & Offizielle (C-Satz) + + + {/* Turnier-Organisation */} + + + Turnier-Organisation + + + + Turnierleiter: + setTurnierleiter(e.target.value)} + placeholder="Name suchen..." + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}} + /> + + Turnierbeauftragter: + setTurnierbeauftragter(e.target.value)} + placeholder="Name suchen..." + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}} + /> + + Technischer Delegierter: + setTechnischerDelegierter(e.target.value)} + placeholder="Name suchen..." + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}} + /> + + Parcourschef: + setParcourschef(e.target.value)} + placeholder="Name suchen..." + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}} + /> + + + + {/* Support-Team */} + + + Support-Team + + + + Tierarzt: + setTierarzt(e.target.value)} + placeholder="Name suchen..." + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}} + /> + + Schmied: + setSchmied(e.target.value)} + placeholder="Name suchen..." + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}} + /> + + Steward: + setSteward(e.target.value)} + placeholder="Name suchen..." + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}} + /> + + + + {/* Richterkollegium */} + + + + Richterkollegium + + + + + + + + + Name + Qualifikation + Funktion + Aktion + + + + {richter.map((r) => ( + + + handleRichterAendern(r.id, 'name', e.target.value)} + placeholder="Name suchen..." + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.5}}} + /> + + + + + + + + + handleRichterLoeschen(r.id)} + sx={{color: 'error.main'}} + > + + + + + ))} + +
+
+
+ + + + {/* === PLÄTZE === */} + + Austragungsplätze + + + + + + Plätze & Anlagen + + + + + + + + + Sparte + Größe + Bezeichnung + Aktion + + + + {plaetze.map((p) => ( + + + + + + + + + handlePlatzAendern(p.id, 'bezeichnung', e.target.value)} + placeholder="z.B. Hauptplatz, Abreiteplatz 1..." + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.5}}} + /> + + + handlePlatzLoeschen(p.id)} + sx={{color: 'error.main'}} + > + + + + + ))} + +
+
+
+ + {/* Speichern Button */} + + + +
+
+ ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/PreislisteTab.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/PreislisteTab.tsx new file mode 100644 index 00000000..c6fec89d --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/PreislisteTab.tsx @@ -0,0 +1,345 @@ +import {useState} from 'react'; +import Box from '@mui/material/Box'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import Paper from '@mui/material/Paper'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import IconButton from '@mui/material/IconButton'; +import Checkbox from '@mui/material/Checkbox'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import DeleteIcon from '@mui/icons-material/Delete'; +import AddIcon from '@mui/icons-material/Add'; +import Divider from '@mui/material/Divider'; + +interface Gebuehr { + id: number; + bezeichnung: string; + betrag: string; + pflicht: boolean; +} + +export function PreislisteTab() { + // Nennungs- und Startgebühren + const [nenngebuehrProPferd, setNenngebuehrProPferd] = useState('0.00'); + const [startgebuehrProBewerb, setStartgebuehrProBewerb] = useState('15.00'); + const [sporteuro, setSporteuro] = useState('0.00'); + const [nachnennungsgebuehr, setNachnennungsgebuehr] = useState('0.00'); + const [nennungstauschgebuehr, setNennungstauschgebuehr] = useState('0.00'); + + // Stallungen & Boxen + const [boxenProTag, setBoxenProTag] = useState('0.00'); + const [einstreuErst, setEinstreuErst] = useState('0.00'); + const [einstreuNach, setEinstreuNach] = useState('0.00'); + const [paddockProTag, setPaddockProTag] = useState('0.00'); + + // Zusatzgebühren (dynamisch) + const [zusatzgebuehren, setZusatzgebuehren] = useState([ + {id: 1, bezeichnung: 'Stromanschluss pro Tag', betrag: '5.00', pflicht: false}, + {id: 2, bezeichnung: 'Camping pro Nacht', betrag: '10.00', pflicht: false}, + ]); + + const handleZusatzgebuehrHinzufuegen = () => { + const newId = Math.max(0, ...zusatzgebuehren.map(g => g.id)) + 1; + setZusatzgebuehren([ + ...zusatzgebuehren, + {id: newId, bezeichnung: '', betrag: '0.00', pflicht: false} + ]); + }; + + const handleZusatzgebuehrLoeschen = (id: number) => { + setZusatzgebuehren(zusatzgebuehren.filter(g => g.id !== id)); + }; + + const handleZusatzgebuehrAendern = (id: number, field: keyof Gebuehr, value: string | boolean) => { + setZusatzgebuehren(zusatzgebuehren.map(g => + g.id === id ? {...g, [field]: value} : g + )); + }; + + const handleSpeichern = () => { + console.log('Preisliste speichern:', { + nenngebuehrProPferd, + startgebuehrProBewerb, + sporteuro, + nachnennungsgebuehr, + nennungstauschgebuehr, + boxenProTag, + einstreuErst, + einstreuNach, + paddockProTag, + zusatzgebuehren, + }); + // TODO: Backend Integration + }; + + return ( + + + + Nennungen & Gebühren + + + {/* Nennungs- und Startgebühren */} + + + Nennungs- und Startgebühren + + + + + + Nenngebühr pro Pferd/Reiter: + + setNenngebuehrProPferd(e.target.value)} + sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + (Grundgebühr unabhängig von Anzahl Bewerben) + + + + + + Startgebühr pro Bewerb: + + setStartgebuehrProBewerb(e.target.value)} + sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + (Pro einzelner Prüfung) + + + + + + Sporteuro (Beitrag OEPS): + + setSporteuro(e.target.value)} + sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + + + + + + Nachnennungsgebühr: + + setNachnennungsgebuehr(e.target.value)} + sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + (Nach Nennschluss) + + + + + + Nennungstausch-Gebühr: + + setNennungstauschgebuehr(e.target.value)} + sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + (Pferd- oder Reiter-Wechsel) + + + + + + {/* Stallungen & Boxen */} + + + Stallungen & Boxen + + + + + + Box pro Tag: + + setBoxenProTag(e.target.value)} + sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + + + + Einstreu (Erst-Einstreu): + + setEinstreuErst(e.target.value)} + sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + + + + Einstreu (Nachlegen): + + setEinstreuNach(e.target.value)} + sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + + + + Paddock pro Tag: + + setPaddockProTag(e.target.value)} + sx={{width: 120, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + + + + {/* Zusatzgebühren */} + + + + Zusatzgebühren + + + + + + + + + Bezeichnung + Betrag + Pflicht + + + + + {zusatzgebuehren.map((gebuehr) => ( + + + handleZusatzgebuehrAendern(gebuehr.id, 'bezeichnung', e.target.value)} + placeholder="z.B. Stromanschluss" + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.5}}} + /> + + + handleZusatzgebuehrAendern(gebuehr.id, 'betrag', e.target.value)} + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.5, textAlign: 'right'}}} + InputProps={{endAdornment: '€'}} + /> + + + handleZusatzgebuehrAendern(gebuehr.id, 'pflicht', e.target.checked)} + /> + } + label={Pflicht} + /> + + + handleZusatzgebuehrLoeschen(gebuehr.id)} + > + + + + + ))} + +
+
+ + {zusatzgebuehren.length === 0 && ( + + + Keine Zusatzgebühren definiert + + + )} +
+ + {/* Hinweis */} + + + ℹ️ Hinweis zur Preisliste + + + Die Gebührenstruktur wird in der offiziellen Ausschreibung veröffentlicht und ist für alle Teilnehmer + verbindlich. Bei nationalen Turnieren der Kategorie C-Neu sind oft reduzierte Gebühren oder + Gebührenbefreiungen + üblich (z.B. kein Nenngeld, kein Sporteuro). + + + + {/* Action Buttons */} + + + + +
+
+ ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/StammdatenTab.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/StammdatenTab.tsx new file mode 100644 index 00000000..72845788 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/StammdatenTab.tsx @@ -0,0 +1,585 @@ +import {useState} from 'react'; +import Box from '@mui/material/Box'; +import TextField from '@mui/material/TextField'; +import Button from '@mui/material/Button'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import FormControl from '@mui/material/FormControl'; +import Checkbox from '@mui/material/Checkbox'; +import FormGroup from '@mui/material/FormGroup'; +import Typography from '@mui/material/Typography'; +import Paper from '@mui/material/Paper'; +import IconButton from '@mui/material/IconButton'; +import Avatar from '@mui/material/Avatar'; +import Divider from '@mui/material/Divider'; +import {DatePicker} from '@mui/x-date-pickers/DatePicker'; +import {LocalizationProvider} from '@mui/x-date-pickers/LocalizationProvider'; +import {AdapterDateFns} from '@mui/x-date-pickers/AdapterDateFns'; +import {de} from 'date-fns/locale'; +import CloudDownloadIcon from '@mui/icons-material/CloudDownload'; +import UsbIcon from '@mui/icons-material/Usb'; +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from '@mui/icons-material/Delete'; +import ImageIcon from '@mui/icons-material/Image'; + +// Kategorien basierend auf Screenshot +const kategorienDressur = [ + 'CDN-A', 'CDN-A*', 'CDN-B', 'CDN-B*', 'CDN-C', 'CDN-C-Neu', 'CDNP-B', 'CDNP-C', 'CDNP-C-Neu' +]; + +const kategorienSpringen = [ + 'CSN-A', 'CSN-A*', 'CSN-B', 'CSN-B*', 'CSN-C', 'CSN-C-Neu', 'CSNP-A', 'CSNP-B', 'CSNP-C', 'CSNP-C-Neu' +]; + +interface StammdatenTabProps { + turnierId?: string; +} + +interface Sponsor { + id: string; + name: string; + logo: string; +} + +export function StammdatenTab({turnierId}: StammdatenTabProps) { + // Turnier-Konfiguration + const [turniernummer, setTurniernummer] = useState(''); + const [typ, setTyp] = useState('oeto'); + const [sprache, setSprache] = useState('deutsch'); + const [sparteDressur, setSparteDressur] = useState(false); + const [sparteSpringen, setSparteSpringen] = useState(false); + const [klasseC, setKlasseC] = useState(false); + const [klasseB, setKlasseB] = useState(false); + const [klasseA, setKlasseA] = useState(false); + const [selectedKategorien, setSelectedKategorien] = useState([]); + const [datumVon, setDatumVon] = useState(null); + const [datumBis, setDatumBis] = useState(null); + + // Turnier-Beschreibung + const [titel, setTitel] = useState(''); + const [subTitel, setSubTitel] = useState(''); + + // Sponsoren + const [sponsoren, setSponsoren] = useState([]); + + // ZNS Import Status + const [znsImportStatus, setZnsImportStatus] = useState<'none' | 'loading' | 'success' | 'error'>('none'); + + // Verfügbare Kategorien basierend auf Sparte UND Klasse + const verfuegbareKategorien = (() => { + const basisKategorien: string[] = []; + if (sparteDressur) basisKategorien.push(...kategorienDressur); + if (sparteSpringen) basisKategorien.push(...kategorienSpringen); + + const selectedKlassen: string[] = []; + if (klasseC) selectedKlassen.push('C', 'C-Neu'); + if (klasseB) selectedKlassen.push('B', 'B*'); + if (klasseA) selectedKlassen.push('A', 'A*'); + + if (selectedKlassen.length > 0 && basisKategorien.length > 0) { + return basisKategorien.filter(kat => { + const match = kat.match(/-(C-Neu|C|B\*|B|A\*|A)$/i); + if (match) { + const katKlasse = match[1].toUpperCase(); + return selectedKlassen.some(k => k.toUpperCase() === katKlasse); + } + return false; + }); + } + + return []; + })(); + + const handleKategorieToggle = (kategorie: string) => { + setSelectedKategorien(prev => + prev.includes(kategorie) + ? prev.filter(k => k !== kategorie) + : [...prev, kategorie] + ); + }; + + const handleZnsImport = (method: 'internet' | 'usb') => { + setZnsImportStatus('loading'); + // Simuliere Import + setTimeout(() => { + setZnsImportStatus('success'); + console.log('ZNS-Daten importiert via', method); + setTimeout(() => setZnsImportStatus('none'), 3000); + }, 2000); + }; + + const handleSponsorHinzufuegen = () => { + setSponsoren([...sponsoren, { + id: Date.now().toString(), + name: '', + logo: '' + }]); + }; + + const handleSponsorEntfernen = (id: string) => { + setSponsoren(sponsoren.filter(s => s.id !== id)); + }; + + const handleSponsorAendern = (id: string, field: 'name' | 'logo', value: string) => { + setSponsoren(sponsoren.map(s => + s.id === id ? {...s, [field]: value} : s + )); + }; + + const handleSpeichern = () => { + console.log('Turnier speichern:', { + turniernummer, + typ, + sprache, + sparteDressur, + sparteSpringen, + klasseC, + klasseB, + klasseA, + selectedKategorien, + datumVon, + datumBis, + titel, + subTitel, + sponsoren + }); + // TODO: Backend Integration + }; + + return ( + + + + + {/* ========== TURNIER-KONFIGURATION ========== */} + + + Turnier-Konfiguration + + + + {/* Turnier-Nr. */} + + + Turnier-Nr.: + + { + const value = e.target.value; + if (value === '' || (/^\d+$/.test(value) && value.length <= 5)) { + setTurniernummer(value); + } + }} + placeholder="z.B. 26128" + sx={{ + width: 160, + '& .MuiInputBase-input': {fontSize: '11px', py: 0.75} + }} + /> + + + {/* Typ: ÖTO / FEI */} + + + Typ: + + + setTyp(e.target.value)} + > + } + label={ÖTO (National)} + /> + } + label={FEI (International)} + /> + + + + + {/* ZNS-Daten Import */} + + + ZNS-Daten: + + + + + {znsImportStatus === 'success' && ( + + ✓ Erfolgreich importiert + + )} + + + + Reiter-, Pferde-, Funktionärs- und Vereinsdaten vom OEPS Backend + + + {/* Sprache */} + + + Sprache: + + + setSprache(e.target.value)} + > + } + label={Deutsch} + /> + } + label={English} + /> + + + + + + + {/* Sparten */} + + + Sparten: + + + { + setSparteDressur(e.target.checked); + setSelectedKategorien([]); + }} + /> + } + label={Dressur} + /> + { + setSparteSpringen(e.target.checked); + setSelectedKategorien([]); + }} + /> + } + label={Springen} + /> + + + + {/* Klassen */} + + + Klassen: + + + { + setKlasseC(e.target.checked); + setSelectedKategorien([]); + }} + /> + } + label={C} + /> + { + setKlasseB(e.target.checked); + setSelectedKategorien([]); + }} + /> + } + label={B} + /> + { + setKlasseA(e.target.checked); + setSelectedKategorien([]); + }} + /> + } + label={A} + /> + + + + {/* Kategorien */} + + + Kategorien: + + + {verfuegbareKategorien.length > 0 ? ( + + {verfuegbareKategorien.map((kategorie) => ( + handleKategorieToggle(kategorie)} + /> + } + label={{kategorie}} + sx={{mb: 0.25}} + /> + ))} + + ) : ( + + {!sparteDressur && !sparteSpringen + ? 'Bitte Sparte(n) auswählen' + : !klasseC && !klasseB && !klasseA + ? 'Bitte Klasse(n) auswählen' + : 'Keine Kategorien verfügbar'} + + )} + + + + {/* Datum */} + + + Datum: + + + setDatumVon(newValue)} + slotProps={{ + textField: { + size: 'small', + placeholder: 'von', + sx: {width: 160, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75}} + } + }} + /> + bis + setDatumBis(newValue)} + minDate={datumVon || undefined} + slotProps={{ + textField: { + size: 'small', + placeholder: 'bis', + sx: {width: 160, '& .MuiInputBase-input': {fontSize: '11px', py: 0.75}} + } + }} + /> + + + + + + {/* ========== TURNIER-BESCHREIBUNG ========== */} + + + Turnier-Beschreibung + + + + {/* Titel */} + + + Titel: + + setTitel(e.target.value)} + placeholder="z.B. Frühjahrs-Turnier 2026" + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}} + /> + + + {/* Sub-Titel */} + + + Sub-Titel: + + setSubTitel(e.target.value)} + placeholder="z.B. KIDS CUP • PONY EINSTEIGER CUP OÖ" + sx={{'& .MuiInputBase-input': {fontSize: '11px', py: 0.75}}} + /> + + + + + {/* ========== SPONSOREN ========== */} + + + + Sponsoren + + + + + {sponsoren.length === 0 ? ( + + + Noch keine Sponsoren hinzugefügt + + + + ) : ( + + {sponsoren.map((sponsor, index) => ( + + + {/* Logo Preview */} + + + + + {/* Inputs */} + + handleSponsorAendern(sponsor.id, 'name', e.target.value)} + placeholder="Sponsor-Name" + label="Name" + sx={{'& .MuiInputBase-input': {fontSize: '11px'}}} + /> + handleSponsorAendern(sponsor.id, 'logo', e.target.value)} + placeholder="Logo-URL oder Datei-Pfad" + label="Logo" + sx={{'& .MuiInputBase-input': {fontSize: '11px'}}} + /> + + + {/* Delete Button */} + handleSponsorEntfernen(sponsor.id)} + sx={{color: 'error.main'}} + > + + + + + ))} + + )} + + + {/* Action Buttons */} + + + + + + + + ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/StartlistenTab.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/StartlistenTab.tsx new file mode 100644 index 00000000..073c3bea --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/StartlistenTab.tsx @@ -0,0 +1,452 @@ +import {useState} from 'react'; +import Box from '@mui/material/Box'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import Paper from '@mui/material/Paper'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; +import Radio from '@mui/material/Radio'; +import RadioGroup from '@mui/material/RadioGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import SearchIcon from '@mui/icons-material/Search'; + +interface Starter { + pos: number; + nr: string; + startzeit: string; + kopfnr: string; + pferd: string; + reiter: string; + bemerkung: string; + pause: boolean; +} + +const mockStarter: Starter[] = []; + +export function StartlistenTab() { + const [selectedBewerb, setSelectedBewerb] = useState(1); + const [starter] = useState(mockStarter); + const [sortStart, setSortStart] = useState('A'); + const [sortRichtung, setSortRichtung] = useState('aufsteigend'); + const [auslosung, setAuslosung] = useState(false); + const [startnummernFixieren, setStartnummernFixieren] = useState(false); + const [beginnzeit, setBeginnzeit] = useState(''); + const [reitdauer, setReitdauer] = useState(''); + const [umbaudauer, setUmbaudauer] = useState(''); + const [besichtigung, setBesichtigung] = useState(''); + + // Bewerbs-Nummern (1-12) + const bewerbe = Array.from({length: 12}, (_, i) => i + 1); + + return ( + + {/* Bewerbs-Auswahl Grid */} + + + {bewerbe.map((nr) => ( + + ))} + + + + {/* Main Content */} + + {/* Linker Bereich: Starter-Tabelle */} + + {/* Toolbar */} + + + + + + {starter.length} gefunden + + + + + + + + {/* Tabelle */} + + + + + Pos. + Nr. + Startzeit + KopfNr + Pferd + Reiter + Bemerkung + Pause + + + + {starter.length === 0 && ( + + + Keine Starter vorhanden + + + )} + +
+
+
+ + {/* Rechte Sidebar */} + + {/* Startliste sortieren */} + + + Startliste sortieren: + + + + + + Start bei: + + setSortStart(e.target.value)} + sx={{ + width: 60, + bgcolor: 'white', + '& .MuiInputBase-input': {fontSize: '10px', py: 0.5} + }} + /> + + + setSortRichtung(e.target.value)} + > + } + label={Aufsteigend} + /> + } + label={Absteigend} + /> + + + setAuslosung(e.target.checked)} + /> + } + label={Auslosung} + /> + + setStartnummernFixieren(e.target.checked)} + /> + } + label={Startnummern fixieren} + /> + + + + + + {/* Zeit/Dauer */} + + + Zeit/Dauer: + + + + + + Beginnzeit: + + + + + + + + setBeginnzeit(e.target.value)} + placeholder="hh:mm" + sx={{ + flex: 1, + bgcolor: 'white', + '& .MuiInputBase-input': {fontSize: '10px', py: 0.5} + }} + /> + + (hh:mm) + + + + + + Reitdauer: + + setReitdauer(e.target.value)} + placeholder="mm:ss" + sx={{ + flex: 1, + bgcolor: 'white', + '& .MuiInputBase-input': {fontSize: '10px', py: 0.5} + }} + /> + + (mm:ss) + + + + + + Umbaudauer: + + setUmbaudauer(e.target.value)} + placeholder="mm" + sx={{ + flex: 1, + bgcolor: 'white', + '& .MuiInputBase-input': {fontSize: '10px', py: 0.5} + }} + /> + + (mm) + + + + + + Besichtigung: + + setBesichtigung(e.target.value)} + placeholder="mm" + sx={{ + flex: 1, + bgcolor: 'white', + '& .MuiInputBase-input': {fontSize: '10px', py: 0.5} + }} + /> + + (mm) + + + + + + + + {/* Startliste weitergeben */} + + + Startliste weitergeben: + + + + + + + + + + + + + + + +
+
+ ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/TransferTab.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/TransferTab.tsx new file mode 100644 index 00000000..eb751831 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/TransferTab.tsx @@ -0,0 +1,325 @@ +import {useState} from 'react'; +import {useParams} from 'react-router'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import Paper from '@mui/material/Paper'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Chip from '@mui/material/Chip'; +import IconButton from '@mui/material/IconButton'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import Divider from '@mui/material/Divider'; +import SaveIcon from '@mui/icons-material/Save'; +import FolderOpenIcon from '@mui/icons-material/FolderOpen'; +import AddIcon from '@mui/icons-material/Add'; +import UploadIcon from '@mui/icons-material/Upload'; +import DownloadIcon from '@mui/icons-material/Download'; +import UsbIcon from '@mui/icons-material/Usb'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import CloudUploadIcon from '@mui/icons-material/CloudUpload'; +import CloudDownloadIcon from '@mui/icons-material/CloudDownload'; +import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; +import CalendarTodayIcon from '@mui/icons-material/CalendarToday'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import WarningIcon from '@mui/icons-material/Warning'; +import {veranstaltungenData} from '../Dashboard'; + +export function TransferTab() { + const {id} = useParams(); + const [anchorEl, setAnchorEl] = useState(null); + const [selectedTurnierId, setSelectedTurnierId] = useState(null); + + // Veranstaltung laden + const veranstaltung = id !== 'neu' + ? veranstaltungenData.find(v => v.id === parseInt(id || '0')) + : null; + + const handleMenuOpen = (event: React.MouseEvent, turnierId: string) => { + setAnchorEl(event.currentTarget); + setSelectedTurnierId(turnierId); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + setSelectedTurnierId(null); + }; + + const handleNeuesTurnier = () => { + console.log('Neues Turnier erstellen für Veranstaltung:', id); + // TODO: Dialog öffnen + }; + + const handleImportZNS = (turnierId: string) => { + console.log('Import ZNS N2-Daten für Turnier:', turnierId); + handleMenuClose(); + }; + + const handleExportZNS = (turnierId: string) => { + console.log('Export ZNS für Turnier:', turnierId); + handleMenuClose(); + }; + + const handleImportUSB = (turnierId: string) => { + console.log('Import von USB für Turnier:', turnierId); + handleMenuClose(); + }; + + const handleExportUSB = (turnierId: string) => { + console.log('Export auf USB für Turnier:', turnierId); + handleMenuClose(); + }; + + const handleImportLokal = (turnierId: string) => { + console.log('Import von lokaler Datei für Turnier:', turnierId); + handleMenuClose(); + }; + + const handleExportLokal = (turnierId: string) => { + console.log('Export als lokale Datei für Turnier:', turnierId); + handleMenuClose(); + }; + + if (!veranstaltung) { + return ( + + + Veranstaltung nicht gefunden + + + ); + } + + return ( + + + {/* Veranstaltungs-Info oben */} + + + + + {veranstaltung.name} + + + + 📍 {veranstaltung.ort} + + + 📅 {veranstaltung.datum} + + + 🏆 {veranstaltung.turniere.length} Turniere + + + + + + + + {/* Button: Neues Turnier */} + + + + + {/* Turniere dieser Veranstaltung */} + + Turniere dieser Veranstaltung + + + + {veranstaltung.turniere.map((turnier) => ( + + + + + + + + {turnier.name} + + + + + + + {turnier.datum} + + + + {turnier.disziplin} + + + {turnier.bewerbeAnzahl} Bewerbe + + + {(turnier.kategorie === 'B' || turnier.kategorie === 'A') && ( + turnier.znsStatus === 'geladen' ? ( + <> + + + ZNS N2-Daten geladen + + + ) : ( + <> + + + ZNS N2-Daten ausstehend + + + ) + )} + + + + + handleMenuOpen(e, turnier.nr)} + > + + + + + {/* Actions für dieses Turnier */} + + + + {(turnier.kategorie === 'B' || turnier.kategorie === 'A') && ( + <> + + + + )} + + + + + + + + ))} + + + {veranstaltung.turniere.length === 0 && ( + + + Noch keine Turniere für diese Veranstaltung angelegt + + + + )} + + {/* Context Menu */} + + selectedTurnierId && handleImportLokal(selectedTurnierId)} sx={{fontSize: '10px'}}> + + Import von lokaler Datei + + selectedTurnierId && handleExportLokal(selectedTurnierId)} sx={{fontSize: '10px'}}> + + Export als lokale Datei + + + selectedTurnierId && handleImportUSB(selectedTurnierId)} sx={{fontSize: '10px'}}> + + Import von USB-Stick + + selectedTurnierId && handleExportUSB(selectedTurnierId)} sx={{fontSize: '10px'}}> + + Export auf USB-Stick + + {selectedTurnierId && veranstaltung.turniere.find(t => t.nr === selectedTurnierId)?.kategorie !== 'C' && ( + <> + + selectedTurnierId && handleImportZNS(selectedTurnierId)} sx={{fontSize: '10px'}}> + + ZNS N2-Daten importieren + + selectedTurnierId && handleExportZNS(selectedTurnierId)} sx={{fontSize: '10px'}}> + + ZNS Ergebnisse exportieren + + + )} + + + + ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/VeranstaltungUebersicht.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/VeranstaltungUebersicht.tsx new file mode 100644 index 00000000..5530439d --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/turnier/VeranstaltungUebersicht.tsx @@ -0,0 +1,347 @@ +import {useState} from 'react'; +import {useParams, useNavigate} from 'react-router'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import Paper from '@mui/material/Paper'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Chip from '@mui/material/Chip'; +import IconButton from '@mui/material/IconButton'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import Divider from '@mui/material/Divider'; +import SaveIcon from '@mui/icons-material/Save'; +import FolderOpenIcon from '@mui/icons-material/FolderOpen'; +import AddIcon from '@mui/icons-material/Add'; +import UploadIcon from '@mui/icons-material/Upload'; +import DownloadIcon from '@mui/icons-material/Download'; +import UsbIcon from '@mui/icons-material/Usb'; +import MoreVertIcon from '@mui/icons-material/MoreVert'; +import CloudUploadIcon from '@mui/icons-material/CloudUpload'; +import CloudDownloadIcon from '@mui/icons-material/CloudDownload'; +import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; +import CalendarTodayIcon from '@mui/icons-material/CalendarToday'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import WarningIcon from '@mui/icons-material/Warning'; +import {veranstaltungenData} from '../Dashboard'; + +export function VeranstaltungUebersicht() { + const params = useParams(); + const id = params.id; + const [anchorEl, setAnchorEl] = useState(null); + const [selectedTurnierId, setSelectedTurnierId] = useState(null); + const navigate = useNavigate(); + + // Veranstaltung laden + const veranstaltung = id !== 'neu' + ? veranstaltungenData.find(v => v.id === parseInt(id || '0')) + : null; + + // Wenn neu, zeige eine leere Ansicht für neue Veranstaltung + if (id === 'neu') { + return ( + + + + + 🆕 Neue Veranstaltung erstellen + + + Bitte wechseln Sie zu den Tabs "Stammdaten", "Organisation", "Bewerbe" oder "Preisliste", um die + Veranstaltung zu konfigurieren. + + + + + ); + } + + if (!veranstaltung) { + return ( + + + Veranstaltung nicht gefunden + + + ); + } + + const handleMenuOpen = (event: React.MouseEvent, turnierId: string) => { + setAnchorEl(event.currentTarget); + setSelectedTurnierId(turnierId); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + setSelectedTurnierId(null); + }; + + const handleNeuesTurnier = () => { + console.log('Neues Turnier erstellen für Veranstaltung:', id); + navigate(`/veranstaltung/${id}/turnier/neu`); + }; + + const handleImportZNS = (turnierId: string) => { + console.log('Import ZNS N2-Daten für Turnier:', turnierId); + handleMenuClose(); + }; + + const handleExportZNS = (turnierId: string) => { + console.log('Export ZNS für Turnier:', turnierId); + handleMenuClose(); + }; + + const handleImportUSB = (turnierId: string) => { + console.log('Import von USB für Turnier:', turnierId); + handleMenuClose(); + }; + + const handleExportUSB = (turnierId: string) => { + console.log('Export auf USB für Turnier:', turnierId); + handleMenuClose(); + }; + + const handleImportLokal = (turnierId: string) => { + console.log('Import von lokaler Datei für Turnier:', turnierId); + handleMenuClose(); + }; + + const handleExportLokal = (turnierId: string) => { + console.log('Export als lokale Datei für Turnier:', turnierId); + handleMenuClose(); + }; + + return ( + + + {/* Veranstaltungs-Info oben */} + + + + + {veranstaltung.name} + + + + 📍 {veranstaltung.ort} + + + 📅 {veranstaltung.datum} + + + 🏆 {veranstaltung.turniere.length} Turniere + + + + + + + + {/* Button: Neues Turnier */} + + + + + {/* Turniere dieser Veranstaltung */} + + Turniere dieser Veranstaltung + + + + {veranstaltung.turniere.map((turnier) => ( + + + + + + + + {turnier.name} + + + + + + + {turnier.datum} + + + + {turnier.disziplin} + + + {turnier.bewerbeAnzahl} Bewerbe + + + {(turnier.kategorie === 'B' || turnier.kategorie === 'A') && ( + turnier.znsStatus === 'geladen' ? ( + <> + + + ZNS N2-Daten geladen + + + ) : ( + <> + + + ZNS N2-Daten ausstehend + + + ) + )} + + + + + handleMenuOpen(e, turnier.nr)} + > + + + + + {/* Actions für dieses Turnier */} + + + + {(turnier.kategorie === 'B' || turnier.kategorie === 'A') && ( + <> + + + + )} + + + + + + + + ))} + + + {veranstaltung.turniere.length === 0 && ( + + + Noch keine Turniere für diese Veranstaltung angelegt + + + + )} + + {/* Context Menu */} + + selectedTurnierId && handleImportLokal(selectedTurnierId)} sx={{fontSize: '10px'}}> + + Import von lokaler Datei + + selectedTurnierId && handleExportLokal(selectedTurnierId)} sx={{fontSize: '10px'}}> + + Export als lokale Datei + + + selectedTurnierId && handleImportUSB(selectedTurnierId)} sx={{fontSize: '10px'}}> + + Import von USB-Stick + + selectedTurnierId && handleExportUSB(selectedTurnierId)} sx={{fontSize: '10px'}}> + + Export auf USB-Stick + + {selectedTurnierId && veranstaltung.turniere.find(t => t.nr === selectedTurnierId)?.kategorie !== 'C' && ( + <> + + selectedTurnierId && handleImportZNS(selectedTurnierId)} sx={{fontSize: '10px'}}> + + ZNS N2-Daten importieren + + selectedTurnierId && handleExportZNS(selectedTurnierId)} sx={{fontSize: '10px'}}> + + ZNS Ergebnisse exportieren + + + )} + + + + ); +} diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/accordion.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/accordion.tsx new file mode 100644 index 00000000..19e8905a --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/accordion.tsx @@ -0,0 +1,67 @@ +"use client"; + +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import {ChevronDownIcon} from "lucide-react"; + +import {cn} from "./utils"; + +function Accordion({ + ...props + }: React.ComponentProps) { + return ; +} + +function AccordionItem({ + className, + ...props + }: React.ComponentProps) { + return ( + + ); +} + +function AccordionTrigger({ + className, + children, + ...props + }: React.ComponentProps) { + return ( + + svg]:rotate-180", + className, + )} + {...props} + > + {children} + + + + ); +} + +function AccordionContent({ + className, + children, + ...props + }: React.ComponentProps) { + return ( + +
{children}
+
+ ); +} + +export {Accordion, AccordionItem, AccordionTrigger, AccordionContent}; diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/alert-dialog.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..d49018d1 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +"use client"; + +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; + +import {cn} from "./utils"; +import {buttonVariants} from "./button"; + +function AlertDialog({ + ...props + }: React.ComponentProps) { + return ; +} + +function AlertDialogTrigger({ + ...props + }: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogPortal({ + ...props + }: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogOverlay({ + className, + ...props + }: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogContent({ + className, + ...props + }: React.ComponentProps) { + return ( + + + + + ); +} + +function AlertDialogHeader({ + className, + ...props + }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogFooter({ + className, + ...props + }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDialogTitle({ + className, + ...props + }: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogDescription({ + className, + ...props + }: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogAction({ + className, + ...props + }: React.ComponentProps) { + return ( + + ); +} + +function AlertDialogCancel({ + className, + ...props + }: React.ComponentProps) { + return ( + + ); +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/alert.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/alert.tsx new file mode 100644 index 00000000..6424cc40 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from "react"; +import {cva, type VariantProps} from "class-variance-authority"; + +import {cn} from "./utils"; + +const alertVariants = cva( + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-card text-card-foreground", + destructive: + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +function Alert({ + className, + variant, + ...props + }: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ); +} + +function AlertTitle({className, ...props}: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function AlertDescription({ + className, + ...props + }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export {Alert, AlertTitle, AlertDescription}; diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/aspect-ratio.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/aspect-ratio.tsx new file mode 100644 index 00000000..cd697698 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/aspect-ratio.tsx @@ -0,0 +1,11 @@ +"use client"; + +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; + +function AspectRatio({ + ...props + }: React.ComponentProps) { + return ; +} + +export {AspectRatio}; diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/avatar.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/avatar.tsx new file mode 100644 index 00000000..cac4642f --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +"use client"; + +import * as React from "react"; +import * as AvatarPrimitive from "@radix-ui/react-avatar"; + +import {cn} from "./utils"; + +function Avatar({ + className, + ...props + }: React.ComponentProps) { + return ( + + ); +} + +function AvatarImage({ + className, + ...props + }: React.ComponentProps) { + return ( + + ); +} + +function AvatarFallback({ + className, + ...props + }: React.ComponentProps) { + return ( + + ); +} + +export {Avatar, AvatarImage, AvatarFallback}; diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/badge.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/badge.tsx new file mode 100644 index 00000000..07ffa941 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from "react"; +import {Slot} from "@radix-ui/react-slot"; +import {cva, type VariantProps} from "class-variance-authority"; + +import {cn} from "./utils"; + +const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); + +function Badge({ + className, + variant, + asChild = false, + ...props + }: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span"; + + return ( + + ); +} + +export {Badge, badgeVariants}; diff --git a/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/breadcrumb.tsx b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/breadcrumb.tsx new file mode 100644 index 00000000..6916fcd1 --- /dev/null +++ b/docs/06_Frontend/FIGMA/Vision_03/src/app/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from "react"; +import {Slot} from "@radix-ui/react-slot"; +import {ChevronRight, MoreHorizontal} from "lucide-react"; + +import {cn} from "./utils"; + +function Breadcrumb({...props}: React.ComponentProps<"nav">) { + return