diff --git a/docs/04_Agents/Logs/2026-03-21_Frontend_NennungsMaske.md b/docs/04_Agents/Logs/2026-03-21_Frontend_NennungsMaske.md new file mode 100644 index 00000000..f28bd737 --- /dev/null +++ b/docs/04_Agents/Logs/2026-03-21_Frontend_NennungsMaske.md @@ -0,0 +1,81 @@ +--- +type: Log +agent: Curator +date: 2026-03-21 +status: COMPLETED +--- + +# đŸ§č Session Log: 21. MĂ€rz 2026 + +## Zusammenfassung + +Diese Session hatte zwei Schwerpunkte: (1) Etablierung eines neuen **Figma → Repo → Compose**-Workflows fĂŒr die +UI-Entwicklung und (2) Implementierung der ersten vollstĂ€ndigen Feature-Maske der **Master Desktop-App** – die +`NennungsMaske`. + +## Erreichte Meilensteine + +### 1. Figma-Workflow etabliert + +- Stefan hat in **Figma Make** einen interaktiven Prototyp der Desktop-Nennungs-Maske erstellt. +- Der Export (React/TypeScript-Code + Assets) wurde direkt ins Repo unter `docs/06_Frontend/FIGMA/` kopiert. +- Dieser "brutale aber geniale" Workflow ermöglicht es, Figma-Exports als **direkte Blaupause** fĂŒr die + Compose-Implementierung zu nutzen. +- Neuer Standard-Workflow: `Figma Make (Stefan) → Export ins Repo → Compose-Implementierung (Agent)` + +### 2. Neues Feature-Modul: `nennung-feature` + +- Neues KMP-Modul erstellt: `frontend/features/nennung-feature` +- EnthĂ€lt: + - `NennungModels.kt` – Domain-Modelle (Pferd, Reiter, Bewerb, Nennung, VerkaufsArtikel) + - `NennungViewModel.kt` – State-Management mit Koin DI + - `NennungsMaske.kt` – VollstĂ€ndige 3-Spalten-Composable (Pferd/Reiter-Suche | Aktions-Hub | Verkauf/Buchungen + + Bewerbsliste) +- Mock-Daten aus dem Figma-Export ĂŒbernommen (echte Preise: Boxenpauschale 115€, Heu 13€, etc.) + +### 3. Navigation & Shell-Integration + +- `AppScreen.Nennung` in der Navigation registriert (`AppScreen.kt`) +- `expect/actual`-Pattern fĂŒr `NennungScreenContent` implementiert (JVM: vollstĂ€ndige Maske, JS: Placeholder) +- `main.kt`: `nennungFeatureModule` in Koin registriert +- `MainApp.kt`: Dashboard-Button "Nennungs-Maske öffnen" + Nennung-Branch in der Navigation + +## Fachlicher Kontext: Nennungs-Maske + +Die Nennungs-Maske ist das **HerzstĂŒck der Desktop-App**. Sie basiert auf dem Altsystem SuDo und wurde analysiert anhand +von: + +- `docs/BilderSuDo/Nennungen.PNG` +- `docs/BilderSuDo/Nennungen-Buchungen.PNG` +- `docs/BilderSuDo/NennungsTausch.PNG` +- `docs/06_Frontend/Screenshots/Desktop-Nennmaske-Entwurf_2026-03-21_11-53.png` (Stefans Figma-Entwurf) + +Layout: 3 Spalten + +- **Links:** Pferd- und Reiter-Suche + Nennungstabelle (Tabs: Reiter | Pferd | Bewerbe) +- **Mitte:** Aktions-Hub (Nennung durchfĂŒhren, Stornieren, Startliste, Ergebnisse, Abrechnung) +- **Rechts:** Verkauf/Buchungen (Tabs) + Bewerbsliste mit Filter + +## Offene Punkte / NĂ€chste Schritte + +- **Nennungstausch-Dialog** – eigene Maske/Modal (3-teilig: Quell-Nennung | Tausch-Optionen | Ziel-Nennung) +- **Keyboard-Shortcuts** – F5 (Nennung), F6 (Stornieren), F7 (Startliste), F8 (Ergebnisse), Escape (Leeren) +- **Lizenz-Badge** – grĂŒn/rot bei Reiter-Metadaten (nach Auswahl) +- **Konto-Saldo** – rot wenn negativ, bei Reiter-Info +- **Offline-Indikator** – Badge in der Titelleiste +- **Weitere Masken** – Ergebnis-Erfassung, Startlisten-Erstellung (nĂ€chste Figma-Exports von Stefan) + +## Dokumentation + +- Neu: `frontend/features/nennung-feature/` (vollstĂ€ndiges KMP-Modul) +- Neu: `docs/06_Frontend/FIGMA/` (Figma Make Export – React/TypeScript Blaupause) +- Neu: `docs/06_Frontend/Screenshots/Desktop-Nennmaske-Entwurf_2026-03-21_11-53.png` +- Update: `frontend/core/navigation/src/commonMain/kotlin/at/mocode/frontend/core/navigation/AppScreen.kt` +- Update: `frontend/shells/meldestelle-portal/src/commonMain/kotlin/MainApp.kt` +- Update: `frontend/shells/meldestelle-portal/src/jvmMain/kotlin/main.kt` + +## Build-Status + +- ✅ `./gradlew :frontend:features:nennung-feature:compileKotlinJvm` → BUILD SUCCESSFUL +- ✅ `./gradlew :frontend:shells:meldestelle-portal:compileKotlinJvm` → BUILD SUCCESSFUL +- ✅ Desktop-App startet erfolgreich (Koin initialisiert, lokale DB erstellt) diff --git a/docs/06_Frontend/FIGMA/ATTRIBUTIONS.md b/docs/06_Frontend/FIGMA/ATTRIBUTIONS.md new file mode 100644 index 00000000..ce6bb5a6 --- /dev/null +++ b/docs/06_Frontend/FIGMA/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/README.md b/docs/06_Frontend/FIGMA/README.md new file mode 100644 index 00000000..5a88cec1 --- /dev/null +++ b/docs/06_Frontend/FIGMA/README.md @@ -0,0 +1,544 @@ +# Turnierverwaltungs-Anwendung - Nennungs-Maske + +## 📋 ProjektĂŒbersicht + +Professionelle Desktop-Anwendung fĂŒr die Verwaltung von Pferdesport-Turnieren mit Fokus auf die **Nennungs-Maske** als +HerzstĂŒck der Anwendung. + +Die Anwendung ermöglicht die effiziente Erfassung von Turnier-Nennungen durch ein intelligentes Pferd-Reiter-Suchsystem +mit Cross-Reference-FunktionalitĂ€t und IMS-Kennzeichnung (Im System). + +--- + +## 🎹 Design-System + +- **Design Framework**: Material Design 3 (MUI v5+) +- **PrimĂ€rfarbe**: Indigo (#3F51B5) +- **UI-Philosophie**: Kompakt, informationsdicht, tastaturoptimiert +- **Zielplattform**: Desktop (keine Mobile-Optimierung erforderlich) + +--- + +## 🛠 Technologie-Stack + +### Core + +- **React** 18+ mit TypeScript +- **Material-UI (MUI)** v5+ fĂŒr UI-Komponenten +- **Emotion** fĂŒr CSS-in-JS Styling + +### Dependencies + +```json +{ + "@mui/material": "^5.x", + "@emotion/react": "^11.x", + "@emotion/styled": "^11.x", + "@mui/icons-material": "^5.x", + "react": "^18.x", + "react-dom": "^18.x", + "typescript": "^5.x" +} +``` + +--- + +## 📐 Layout-Struktur + +### 2-Spalten 3-Zeilen Grid (60% / 40% horizontal) + +``` +┌─────────────────────────────────┬──────────────────────┐ +│ Pferd/Reiter Suche (60%) │ Verkauf/Buchungen │ 50% +│ - Pferd Suche + Details │ (40%) │ +│ - Reiter Suche + Details │ │ +├─────────────────────────────────┎─────────────────────── +│ Navigation (Startliste | Ergebnisse | Abrechnung) │ 5% +â”œâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”Źâ”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€â”€ +│ NennungsĂŒbersicht (60%) │ BewerbsĂŒbersicht │ 45% +│ - Reiter | Pferd | Bewerbe │ (40%) │ +│ - Tabbed Interface │ - Doppelklick nennt │ +└─────────────────────────────────┮──────────────────────┘ +``` + +--- + +## 🏗 Komponenten-Architektur + +### Komponenten-Hierarchie + +``` +App.tsx +└── NennungsMaske.tsx (Hauptlayout) + ├── PferdReiterEingabe.tsx (Such-System) + ├── VerkaufBuchungen.tsx (Verkauf/Buchungen) + ├── NennungenTabelle.tsx (NennungsĂŒbersicht) + └── Bewerbsliste.tsx (BewerbsĂŒbersicht) +``` + +### Datei-Übersicht + +| Datei | Beschreibung | Zeilen | +|--------------------------|---------------------------------------|--------| +| `App.tsx` | Root Component mit MUI Theme Provider | ~30 | +| `theme.tsx` | Material Design 3 Theme (Indigo) | ~50 | +| `NennungsMaske.tsx` | Hauptlayout mit Grid-Struktur | ~120 | +| `PferdReiterEingabe.tsx` | Intelligentes Such-System | ~400 | +| `VerkaufBuchungen.tsx` | Verkauf & Buchungen Tabs | ~250 | +| `NennungenTabelle.tsx` | NennungsĂŒbersicht mit Tabs | ~180 | +| `Bewerbsliste.tsx` | BewerbsĂŒbersicht | ~180 | + +--- + +## 🔍 Core Features + +### 1. **Intelligentes Such-System** + +#### Pferd-Suche + +- **Eingabe**: Kopfnummer oder Name +- **Echtzeit-Filterung**: WĂ€hrend der Eingabe alle Treffer anzeigen +- **Nach Auswahl**: Top 4 Ergebnisse behalten (scrollbar) +- **Cross-Reference**: Zeigt automatisch Reiter des ausgewĂ€hlten Pferdes +- **IMS-Kennzeichnung**: Bereits genannte Pferde mit "IMS" Badge + +#### Reiter-Suche + +- **Eingabe**: Vorname und/oder Nachname +- **Echtzeit-Filterung**: WĂ€hrend der Eingabe alle Treffer anzeigen +- **Nach Auswahl**: Top 4 Ergebnisse behalten (scrollbar) +- **Cross-Reference**: Zeigt automatisch Pferde des ausgewĂ€hlten Reiters +- **IMS-Kennzeichnung**: Bereits genannte Reiter mit "IMS" Badge +- **Geburtsjahr**: Bei Namensgleichheit zur Unterscheidung (*1998, *2002) + +#### Tastaturnavigation + +- **↑/↓**: Navigation durch Suchergebnisse +- **Enter**: Auswahl bestĂ€tigen +- **Tab**: Zum nĂ€chsten Feld +- **Doppelklick**: Alternative zur Enter-Taste + +### 2. **IMS-System (Im System)** + +**Definition**: Pferd-Reiter-Kombinationen, die bereits fĂŒr dieses Turnier genannt haben. + +**Verhalten**: + +```typescript +// Mock-Daten Beispiel +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 +]; +``` + +**Vorteile**: + +- ✅ Schneller Zugriff auf hĂ€ufig verwendete Kombinationen +- ✅ Vermeidung von Duplikaten +- ✅ Verkauf (z.B. "Heu") kann schnell auf bestehende Pferde gebucht werden + +### 3. **Cross-Reference-FunktionalitĂ€t** + +#### Szenario A: Reiter → Pferde + +``` +1. Reiter "Ba" eingeben +2. Suchergebnis: "Thomas BAUER" (IMS) +3. Automatisch im Pferd-Feld: "Domino" (IMS) +4. Pferd auswĂ€hlen → Verkauf buchen ODER +5. Anderes Pferd suchen → Neue Nennung +``` + +#### Szenario B: Pferd → Reiter + +``` +1. Pferd "Domino" suchen +2. Pferd auswĂ€hlen +3. Automatisch im Reiter-Feld: "Thomas Bauer" (IMS) +4. Reiter bestĂ€tigen ODER +5. Anderen Reiter suchen → Pferd hat 2 Reiter +``` + +### 4. **Workflow: Nennung erstellen** + +``` +┌─────────────────────────────────────────────────────┐ +│ 1. Pferd suchen (oder via Reiter-Cross-Reference) │ +│ → Pfeiltasten navigieren │ +│ → Enter oder Doppelklick bestĂ€tigen │ +├────────────────────────────────────────────────────── +│ 2. Reiter suchen (oder via Pferd-Cross-Reference) │ +│ → Pfeiltasten navigieren │ +│ → Enter oder Doppelklick bestĂ€tigen │ +├────────────────────────────────────────────────────── +│ 3. Pferd + Reiter Details werden angezeigt │ +│ → Validierung (Lizenz, Konto-Saldo) │ +├────────────────────────────────────────────────────── +│ 4. Bewerb aus BewerbsĂŒbersicht wĂ€hlen │ +│ → Doppelklick auf Bewerb │ +├────────────────────────────────────────────────────── +│ 5. Nennung erscheint in NennungsĂŒbersicht │ +│ → Filterbar nach Reiter/Pferd/Bewerb │ +└─────────────────────────────────────────────────────┘ +``` + +### 5. **Verkauf & Buchungen** + +#### Verkauf-Tab + +- **Artikel-Liste**: Nenngeld, Stallmiete, Heu, Stroh, etc. +- **Menge anpassen**: +/- Buttons oder direkte Eingabe +- **Auto-Berechnung**: Betrag = Menge × Einzelpreis +- **Hervorhebung**: Wichtige Artikel (Nenngeld, Stallmiete) gelb hinterlegt +- **Buchung**: Nur auf ausgewĂ€hltes Pferd möglich + +#### Buchungen-Tab + +- **Übersicht**: Alle getĂ€tigten Buchungen +- **Stornierung**: Einzelne Buchungen rĂŒckgĂ€ngig machen + +### 6. **NennungsĂŒbersicht** + +#### 3 Tabs + +- **Reiter**: Alle Nennungen gefiltert nach ausgewĂ€hltem Reiter +- **Pferd**: Alle Nennungen gefiltert nach ausgewĂ€hltem Pferd +- **Bewerbe**: Alle Nennungen chronologisch + +#### Funktionen + +- **Positionieren**: Startreihenfolge manuell festlegen +- **Stornieren**: Nennung entfernen +- **Farbcodierung**: + - GrĂŒn: Startwunsch "Vorne" + - Blau: Startwunsch "Hinten" + +### 7. **BewerbsĂŒbersicht** + +- **Alle Bewerbe**: Tag, Platz, Nr, Name, Beginn, Anzahl Nennungen +- **Doppelklick**: Nennung erstellen (nur wenn Pferd + Reiter ausgewĂ€hlt) +- **Filter**: Bewerbe nach Kriterien filtern +- **Aktualisierung**: Echtzeit-Update der Nennungsanzahl + +--- + +## 🎯 Bedienkonzept + +### Kompakte Desktop-UI + +- **Font-GrĂ¶ĂŸe**: 10-11px (kompakt, informationsdicht) +- **Button-GrĂ¶ĂŸe**: `size="small"` mit reduziertem Padding +- **Tabellen**: Dense-Modus mit minimalen Zeilenhöhen +- **Icons**: 14-16px (klein aber erkennbar) + +### Tastatur-First + +- **Tab-Navigation**: Durch alle Eingabefelder +- **Pfeiltasten**: Navigation in Listen +- **Enter**: BestĂ€tigung +- **Escape**: Abbrechen (TODO) +- **Shortcuts**: (TODO: Strg+N fĂŒr Neu, Strg+S fĂŒr Speichern) + +--- + +## 📊 Datenmodell (Mock) + +### Pferd + +```typescript +interface Pferd { + id: number; + kopfnr: string; // z.B. "A123", "4568" + name: string; // z.B. "Obora's Donna" + rasse: string; // z.B. "Hannoveraner" + farbe: string; // z.B. "Brauner" + besitzer: string; // z.B. "Franz Huber" + stall: string; // z.B. "Box 12" +} +``` + +### Reiter + +```typescript +interface Reiter { + id: number; + kopfnr: string; // z.B. "201" + vorname: string; // z.B. "Thomas" + nachname: string; // z.B. "Bauer" + verein: string; // z.B. "RC Graz" + lizenz: string; // z.B. "LNR-2024-4587" + lizenzGueltig: boolean; + kontoSaldo: number; // z.B. -125.50 (negativ = Schulden) + geburtsjahr: number; // z.B. 1998 (fĂŒr Namensgleichheit) +} +``` + +### Nennung + +```typescript +interface Nennung { + id: number; + pferdId: number; + reiterId: number; + bewerbNr: string; // z.B. "1", "2a" + bewerbName: string; // z.B. "Dressur Kl. L" + tag: string; // z.B. "SA", "SO" + platz: string; // z.B. "A1", "C2" + startwunsch?: string; // z.B. "Vorne", "Hinten" +} +``` + +### Bewerb + +```typescript +interface Bewerb { + nr: string; // z.B. "1", "2a" + name: string; // z.B. "Dressur Kl. L" + tag: string; // z.B. "SA" + platz: string; // z.B. "A1" + beginn: string; // z.B. "08:00" + nenn: number; // Anzahl Nennungen +} +``` + +### Turnie-Nennung (IMS) + +```typescript +interface TurnieNennung { + reiterId: number; + pferdId: number; + bewerbNr: number; +} +``` + +--- + +## 🚀 Installation & Setup + +### Schritt 1: Projekt initialisieren + +```bash +# React + TypeScript Projekt erstellen +npx create-react-app turnierverwaltung --template typescript + +cd turnierverwaltung +``` + +### Schritt 2: Dependencies installieren + +```bash +# MUI und Dependencies +npm install @mui/material @emotion/react @emotion/styled @mui/icons-material + +# TypeScript Types (falls benötigt) +npm install --save-dev @types/react @types/react-dom +``` + +### Schritt 3: Projektstruktur erstellen + +``` +src/ +├── app/ +│ ├── App.tsx +│ ├── theme.tsx +│ └── components/ +│ ├── NennungsMaske.tsx +│ ├── PferdReiterEingabe.tsx +│ ├── VerkaufBuchungen.tsx +│ ├── NennungenTabelle.tsx +│ └── Bewerbsliste.tsx +├── styles/ +│ └── theme.css +└── index.tsx +``` + +### Schritt 4: Dateien manuell ĂŒbertragen + +**WICHTIG**: Da kein Download verfĂŒgbar ist, mĂŒssen Sie die Dateien manuell aus Figma Make kopieren: + +1. **Öffnen Sie jede Komponente** in Figma Make +2. **Kopieren Sie den Code** (Strg+A, Strg+C) +3. **Erstellen Sie die Datei** in Ihrer IDE +4. **FĂŒgen Sie den Code ein** (Strg+V) + +**Reihenfolge**: + +1. `theme.tsx` (Theme zuerst!) +2. `App.tsx` +3. `NennungsMaske.tsx` +4. `PferdReiterEingabe.tsx` +5. `VerkaufBuchungen.tsx` +6. `NennungenTabelle.tsx` +7. `Bewerbsliste.tsx` + +### Schritt 5: Starten + +```bash +npm start +``` + +--- + +## 🔄 NĂ€chste Schritte / TODO + +### Phase 1: Backend-Integration + +- [ ] REST API Endpoints definieren +- [ ] Pferde aus Datenbank laden +- [ ] Reiter aus Datenbank laden +- [ ] Nennungen persistieren +- [ ] Echtzeit-Updates (WebSocket?) + +### Phase 2: Erweiterte Features + +- [ ] **Neu-Button**: Dialog fĂŒr neue Pferde/Reiter +- [ ] **Bearbeiten-Button**: Inline-Editing von Details +- [ ] **Stornieren**: Nennung mit BestĂ€tigung löschen +- [ ] **Positionieren**: Drag & Drop fĂŒr Startreihenfolge +- [ ] **Filter**: Erweiterte Filteroptionen +- [ ] **Drucken**: Startlisten, Nennungslisten +- [ ] **Export**: PDF, Excel + +### Phase 3: Validierung & Business Logic + +- [ ] Lizenz-PrĂŒfung (abgelaufene Lizenzen warnen) +- [ ] Konto-Saldo Warnung (negative Salden) +- [ ] Doppel-Nennungen verhindern +- [ ] Zeitkonflikte erkennen +- [ ] KapazitĂ€tsgrenzen (max. Nennungen pro Bewerb) + +### Phase 4: Weitere Masken + +- [ ] **Startliste**: Reihenfolge, Startzeiten +- [ ] **Ergebnisse**: Platzierungen erfassen +- [ ] **Abrechnung**: Kosten, Zahlungen, Quittungen + +### Phase 5: UX-Verbesserungen + +- [ ] **Keyboard Shortcuts**: Strg+N, Strg+S, etc. +- [ ] **Undo/Redo**: Historie fĂŒr Änderungen +- [ ] **Suche optimieren**: Fuzzy Search, Synonyme +- [ ] **Loading States**: Spinner, Skeleton Screens +- [ ] **Error Handling**: Benutzerfreundliche Fehlermeldungen + +### Phase 6: Performance + +- [ ] **Virtualisierung**: Große Listen (1000+ EintrĂ€ge) +- [ ] **Lazy Loading**: Komponenten on-demand laden +- [ ] **Caching**: HĂ€ufig verwendete Daten + +--- + +## 💡 Design-Entscheidungen + +### Warum Material Design 3? + +- **Konsistentes Design-System**: BewĂ€hrte Patterns +- **Accessibility**: WCAG-konform out-of-the-box +- **Rich Component Library**: Weniger Custom-Code +- **Professional Look**: Moderne, cleane Optik + +### Warum Indigo als PrimĂ€rfarbe? + +- **Professionell**: VertrauenswĂŒrdig, seriös +- **Kontrast**: Gute Lesbarkeit auf hellem Hintergrund +- **Differenzierung**: Nicht das "Standard-Blau" + +### Warum 2-Spalten Layout? + +- **Workflow-orientiert**: Eingabe links, Übersicht rechts +- **Platzsparend**: Maximale Information auf einem Screen +- **Desktop-optimiert**: Nutzt breite Bildschirme effizient + +### Warum IMS-System? + +- **Performance**: Reduziert Suchzeit drastisch +- **UX**: HĂ€ufige Kombinationen sofort verfĂŒgbar +- **Fehlerminimierung**: Bereits validierte Kombinationen + +--- + +## đŸ€ Team-Übergabe Checkliste + +### FĂŒr Frontend-Entwickler + +- [ ] README durchlesen (diese Datei!) +- [ ] Komponenten-Struktur verstehen +- [ ] Mock-Daten analysieren +- [ ] Workflow nachvollziehen (IMS, Cross-Reference) +- [ ] UI/UX Konzept verinnerlichen + +### FĂŒr Backend-Entwickler + +- [ ] Datenmodell definieren (siehe "Datenmodell") +- [ ] API Endpoints spezifizieren +- [ ] Validierungs-Regeln implementieren +- [ ] Performance-Anforderungen klĂ€ren + +### FĂŒr Product Owner + +- [ ] Feature-Priorisierung (siehe "NĂ€chste Schritte") +- [ ] User Stories schreiben +- [ ] Acceptance Criteria definieren + +### FĂŒr Designer + +- [ ] Theme anpassen (Farben, Schriften) +- [ ] Icons konsistent gestalten +- [ ] Print-Layouts definieren + +--- + +## 📞 Support & Fragen + +### HĂ€ufige Fragen + +**Q: Warum verschwindet das Suchfeld nicht nach der Auswahl?** +A: Bewusste Design-Entscheidung! Die Top 4 Ergebnisse bleiben sichtbar, damit man schnell zwischen Ă€hnlichen +Pferden/Reitern wechseln kann (z.B. mehrere "Obora's..." Pferde). + +**Q: Was bedeutet IMS?** +A: "Im System" = Pferd-Reiter-Kombination hat bereits eine Nennung fĂŒr dieses Turnier. + +**Q: Warum Cross-Reference?** +A: Effizienz! Wenn ich einen Reiter suche, sehe ich sofort seine Pferde. Spart Zeit bei VerkĂ€ufen oder weiteren +Nennungen. + +**Q: Kann ein Pferd mehrere Reiter haben?** +A: Ja! Ein Pferd kann von verschiedenen Reitern in verschiedenen Bewerben geritten werden. + +**Q: Warum ist die Schrift so klein?** +A: Desktop-Anwendung fĂŒr Profis. Kompakte Darstellung = mehr Information auf einem Blick = weniger Scrollen. + +--- + +## 📄 Lizenz + +(Bitte durch Ihr Team festlegen) + +--- + +## ✍ Autoren & Mitwirkende + +- **Projekt-Owner**: [Ihr Name] +- **Prototyp**: Erstellt mit Figma Make +- **Datum**: MĂ€rz 2026 + +--- + +## 📝 Änderungshistorie + +| Version | Datum | Änderung | +|---------|------------|------------------------------------------| +| 0.1.0 | 2026-03-21 | Initialer Prototyp mit Nennungs-Maske | +| | | - IMS-System implementiert | +| | | - Cross-Reference-Suche | +| | | - 2-Spalten 3-Zeilen Layout (50%-5%-45%) | +| | | - Material Design 3 (Indigo) | + +--- + +**Ende der Dokumentation** + +Bei Fragen oder Unklarheiten: Bitte diese README erweitern und im Team diskutieren! diff --git a/docs/06_Frontend/FIGMA/guidelines.zip b/docs/06_Frontend/FIGMA/guidelines.zip new file mode 100644 index 00000000..deb4a504 Binary files /dev/null and b/docs/06_Frontend/FIGMA/guidelines.zip differ diff --git a/docs/06_Frontend/FIGMA/guidelines/Guidelines.md b/docs/06_Frontend/FIGMA/guidelines/Guidelines.md new file mode 100644 index 00000000..110f1178 --- /dev/null +++ b/docs/06_Frontend/FIGMA/guidelines/Guidelines.md @@ -0,0 +1,61 @@ +**Add your own guidelines here** + diff --git a/docs/06_Frontend/FIGMA/package.json b/docs/06_Frontend/FIGMA/package.json new file mode 100644 index 00000000..67da9a91 --- /dev/null +++ b/docs/06_Frontend/FIGMA/package.json @@ -0,0 +1,89 @@ +{ + "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", + "@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-group": "1.1.2", + "@radix-ui/react-toggle": "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/postcss.config.mjs b/docs/06_Frontend/FIGMA/postcss.config.mjs new file mode 100644 index 00000000..531dbecd --- /dev/null +++ b/docs/06_Frontend/FIGMA/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/src.zip b/docs/06_Frontend/FIGMA/src.zip new file mode 100644 index 00000000..1aafc691 Binary files /dev/null and b/docs/06_Frontend/FIGMA/src.zip differ diff --git a/docs/06_Frontend/FIGMA/src/app/App.tsx b/docs/06_Frontend/FIGMA/src/app/App.tsx new file mode 100644 index 00000000..bb496be7 --- /dev/null +++ b/docs/06_Frontend/FIGMA/src/app/App.tsx @@ -0,0 +1,74 @@ +import {ThemeProvider, createTheme} from '@mui/material/styles'; +import CssBaseline from '@mui/material/CssBaseline'; +import {NennungsMaske} from './components/NennungsMaske'; + +// Material Design 3 Theme mit Indigo PrimĂ€rfarbe +const theme = createTheme({ + palette: { + mode: 'light', + primary: { + main: '#3F51B5', // Indigo + light: '#7986CB', + dark: '#303F9F', + contrastText: '#FFFFFF', + }, + secondary: { + main: '#FF4081', + light: '#FF80AB', + dark: '#F50057', + }, + error: { + main: '#F44336', + }, + warning: { + main: '#FF9800', + }, + success: { + main: '#4CAF50', + }, + background: { + default: '#FAFAFA', + paper: '#FFFFFF', + }, + }, + typography: { + fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', + fontSize: 13, // Kompakt fĂŒr Desktop-Anwendung + }, + components: { + MuiButton: { + styleOverrides: { + root: { + textTransform: 'none', // Keine ALL CAPS + borderRadius: 8, + }, + }, + }, + MuiTextField: { + styleOverrides: { + root: { + '& .MuiInputBase-root': { + fontSize: '13px', + }, + }, + }, + }, + MuiTableCell: { + styleOverrides: { + root: { + padding: '6px 8px', // Kompakter als Standard + fontSize: '12px', + }, + }, + }, + }, +}); + +export default function App() { + return ( + + + + + ); +} diff --git a/docs/06_Frontend/FIGMA/src/app/components/Bewerbsliste.tsx b/docs/06_Frontend/FIGMA/src/app/components/Bewerbsliste.tsx new file mode 100644 index 00000000..d0ce73b2 --- /dev/null +++ b/docs/06_Frontend/FIGMA/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/src/app/components/NennungenTabelle.tsx b/docs/06_Frontend/FIGMA/src/app/components/NennungenTabelle.tsx new file mode 100644 index 00000000..1af2c8c8 --- /dev/null +++ b/docs/06_Frontend/FIGMA/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/src/app/components/NennungsMaske.tsx b/docs/06_Frontend/FIGMA/src/app/components/NennungsMaske.tsx new file mode 100644 index 00000000..4399ec0f --- /dev/null +++ b/docs/06_Frontend/FIGMA/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/src/app/components/PferdReiterEingabe.tsx b/docs/06_Frontend/FIGMA/src/app/components/PferdReiterEingabe.tsx new file mode 100644 index 00000000..42488218 --- /dev/null +++ b/docs/06_Frontend/FIGMA/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/src/app/components/VerkaufBuchungen.tsx b/docs/06_Frontend/FIGMA/src/app/components/VerkaufBuchungen.tsx new file mode 100644 index 00000000..8fad1549 --- /dev/null +++ b/docs/06_Frontend/FIGMA/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/src/app/components/figma/ImageWithFallback.tsx b/docs/06_Frontend/FIGMA/src/app/components/figma/ImageWithFallback.tsx new file mode 100644 index 00000000..ff6e48f5 --- /dev/null +++ b/docs/06_Frontend/FIGMA/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/src/app/components/ui/accordion.tsx b/docs/06_Frontend/FIGMA/src/app/components/ui/accordion.tsx new file mode 100644 index 00000000..19e8905a --- /dev/null +++ b/docs/06_Frontend/FIGMA/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/src/app/components/ui/alert-dialog.tsx b/docs/06_Frontend/FIGMA/src/app/components/ui/alert-dialog.tsx new file mode 100644 index 00000000..d49018d1 --- /dev/null +++ b/docs/06_Frontend/FIGMA/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/src/app/components/ui/alert.tsx b/docs/06_Frontend/FIGMA/src/app/components/ui/alert.tsx new file mode 100644 index 00000000..6424cc40 --- /dev/null +++ b/docs/06_Frontend/FIGMA/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/src/app/components/ui/aspect-ratio.tsx b/docs/06_Frontend/FIGMA/src/app/components/ui/aspect-ratio.tsx new file mode 100644 index 00000000..cd697698 --- /dev/null +++ b/docs/06_Frontend/FIGMA/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/src/app/components/ui/avatar.tsx b/docs/06_Frontend/FIGMA/src/app/components/ui/avatar.tsx new file mode 100644 index 00000000..cac4642f --- /dev/null +++ b/docs/06_Frontend/FIGMA/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/src/app/components/ui/badge.tsx b/docs/06_Frontend/FIGMA/src/app/components/ui/badge.tsx new file mode 100644 index 00000000..07ffa941 --- /dev/null +++ b/docs/06_Frontend/FIGMA/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/src/app/components/ui/breadcrumb.tsx b/docs/06_Frontend/FIGMA/src/app/components/ui/breadcrumb.tsx new file mode 100644 index 00000000..6916fcd1 --- /dev/null +++ b/docs/06_Frontend/FIGMA/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